Compare commits

..
50 Commits
Author SHA1 Message Date
xiaojunnuo b12f7c54d4 build: release 2026-10-03 02:47:15 +08:00
xiaojunnuo b055bbecfc build: publish 2026-10-03 01:01:10 +08:00
xiaojunnuo a52a6ce8f6 build: trigger build image 2026-10-03 01:01:00 +08:00
xiaojunnuo fcbf1045fe v1.45.0 2026-10-03 01:00:08 +08:00
xiaojunnuo f5cfe054b7 build: prepare to build 2026-10-03 00:51:15 +08:00
xiaojunnuo 5ded5ff2b6 test: 更新测试用例的断言数据和测试描述 2026-10-03 00:51:09 +08:00
xiaojunnuo 8d31346cd3 build: prepare to build 2026-10-03 00:22:38 +08:00
xiaojunnuo 44b41b6a08 chore(certd-server): 同步迁移脚本 2026-10-02 03:17:46 +08:00
xiaojunnuo 4420472f8f perf: 宝塔站点列表支持搜索和排序 2026-10-01 21:49:39 +08:00
xiaojunnuo c4497a7018 chore: 更新DNS持久验证的支持证书类型为Google证书
更新了多个位置的提示文本,将原Let's Encrypt测试环境的说明替换为Google证书,同时调整了验证方式的说明顺序和内容,还修改了插件加载模式为metadata,优化了代码中的格式细节。
2026-10-01 10:47:28 +08:00
xiaojunnuo 03f1a7bd0e chore: 修复获取证书颁发者时未处理空directoryUrl的问题
修改两处代码,在获取DNS持久化颁发者时,当directoryUrl为空时使用accountUri作为备用,避免因缺少必要参数导致的错误
2026-10-01 10:01:41 +08:00
xiaojunnuo 97d2b50074 fix: 支持google persist校验方式 2026-09-30 18:19:23 +08:00
xiaojunnuo 2d0b85b305 fix: 缺失运行时嵌套依赖时添加清除缓存提示
当加载运行时依赖时出现嵌套模块未找到错误,添加明确的错误提示,指导用户前往插件管理页面清除缓存依赖后重试。同时新增单元测试验证该提示逻辑。
2026-09-30 16:34:11 +08:00
xiaojunnuo 28052c9387 chore: clb tencent 2026-09-30 16:22:59 +08:00
xiaojunnuo c4277350bc perf(tencent clb): 修改所有CLB地域的endpoint为就近接入 2026-09-30 15:58:13 +08:00
xiaojunnuo 97160373d6 Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2026-09-30 15:32:57 +08:00
xiaojunnuo 0ce24618a1 docs: 修正HTTPS环境变量命名为小写格式 2026-09-29 15:06:06 +08:00
xiaojunnuo f60e63fbaf perf(monitor): 站点证书监控支持手动选中触发检查 2026-09-20 23:04:49 +08:00
xiaojunnuo 2735feb330 fix(webhook): 修复模版转义换行符,避免手动上传的证书报错
1.  将webhook插件内的replaceTemplate方法迁移到stringUtils公共工具
2.  优化模板替换处理,支持转义更多控制字符和兼容非字符串变量
3.  添加对应的单元测试用例
2026-09-20 23:04:13 +08:00
xiaojunnuo b6381d79fc chore: 批量优化插件开发相关配置与文档
1.  更新gitignore,新增.tmp_chunks忽略项
2.  切换certd插件加载模式为dev
3.  修正插件示例yaml的title注释
2026-09-20 01:26:02 +08:00
xiaojunnuo 241b2e5584 perf: 网宿插件双鉴权支持 2026-09-20 01:25:52 +08:00
xiaojunnuo bb1f232f41 fix: [BUG]使用天翼云CDN部署证书时无法匹配域名
https://github.com/certd/certd/issues/805
2026-09-19 18:05:22 +08:00
xiaojunnuo f82d3a5fcc chore(monitor): 补全站点新增/导入时的IP检查相关布尔默认值
解决批量导入等场景未传ipCheck等字段导致数据库存NULL,编辑弹窗开关过不了校验的issue #803,同时新增对应单测验证默认值和显式传参的正确性
2026-09-19 18:00:15 +08:00
xiaojunnuo bbeaf617d1 perf(open-api): openkey增加用途备注
1. 新增数据库字段存储开放接口密钥备注信息
2. 多语言添加密钥备注相关文案
3. 开放密钥接口支持备注参数传递与CRUD
4. 前端页面支持备注的展示、搜索与编辑
5. 调整插件加载模式为metadata并修复代码格式
2026-09-19 17:41:02 +08:00
xiaojunnuo ea7d14c289 fix(ctyun-cdn): 修复天翼云CDN部署插件的域名选择问题
1.  添加产品类型监听,切换产品类型时重新拉取域名列表
2.  未选择产品类型时抛出明确错误提示
3.  补全域名选项的domain字段以支持证书域名匹配分组
4.  新增单元测试覆盖上述场景
2026-09-18 01:12:36 +08:00
xiaojunnuo 3272a68ff6 style(certd client page): 调整头部布局和VIP按钮展示逻辑
优化页面头部子标题区域的对齐样式,移除非Plus版本的提示文本和条件渲染逻辑,统一展示VIP按钮
2026-09-18 00:18:13 +08:00
xiaojunnuo b5715d99ff Merge branch 'v2-dev' into v2_client 2026-09-17 23:52:49 +08:00
xiaojunnuo 3f5ba14fde ci(workflow): 修正stable-release.yml里版本号示例格式
将版本号示例的v前缀移除,和输入要求保持一致
2026-09-17 23:26:32 +08:00
xiaojunnuo 07f0d3b4d6 fix(config): 修复环境变量加载逻辑并更新配置文件
1.  重构loadDotEnv方法,支持指定加载的env文件路径,增加文件存在校验
2.  跳过注释和空行的环境变量加载,添加加载日志
3.  在默认配置中加载对应环境的env文件
4.  更新开发和生产环境的env配置项
2026-09-17 23:23:35 +08:00
xiaojunnuo 3dbbe72fc1 feat(client): 新增客户端管理功能
实现了客户端管理的完整流程:
1. 新增数据库表与迁移脚本,存储客户端信息与心跳快照
2. 实现服务端心跳上报接口与管理API
3. 新增前端页面与国际化多语言支持
4. 完善客户端在线状态判定逻辑与分页查询功能
2026-09-17 22:46:03 +08:00
xiaojunnuo 7f006d1359 refactor(plugin): 迁移同步插件逻辑到页面组件并优化上下文传递
1. 将plugin crud组件中的syncOnlinePlugins方法迁移到index.vue中
2. 把syncLoading和syncOnlinePlugins加入页面上下文传递给crud组件
3. 移除crud组件中冗余的syncLoading声明
4. 调整页面初始化调用顺序
2026-09-16 00:15:16 +08:00
xiaojunnuo d30f1c4785 refactor(plugin): 优化在线插件同步逻辑与时间格式化
1.  将自动同步相关逻辑抽离到页面组件统一管理
2.  使用dayjs替代原生Date进行时间格式化
3.  移除crud.tsx中冗余的同步配置加载代码
4.  统一维护自动同步的间隔时间常量
2026-09-15 23:52:29 +08:00
xiaojunnuo 96e0122b93 build: release 2026-09-12 20:26:56 +08:00
xiaojunnuo 7786a87b21 refactor(certd-server): 移除getReleaseMode函数,改用配置注入获取发布模式
1.  从环境配置文件中注释移除certd_release_mode的注释,启用稳定发布模式
2.  在AppController和SettingsController中注入release.mode配置替代原有的全局函数获取方式
3.  移除冗余的getReleaseMode工具函数
4.  在SettingsController中添加发布模式的日志打印
2026-09-12 20:22:18 +08:00
xiaojunnuo 34476ba14f build: publish 2026-09-12 19:24:56 +08:00
xiaojunnuo 4b942385a8 build: trigger build image 2026-09-12 19:24:46 +08:00
xiaojunnuo 14809127d6 v1.44.4 2026-09-12 19:23:35 +08:00
xiaojunnuo bd8b251d6e build: prepare to build 2026-09-12 19:17:14 +08:00
xiaojunnuo 5563abb13a fix: 修复证书插件配置无法打开的bug 2026-09-12 19:13:23 +08:00
xiaojunnuo 51e83e4c38 fix: 修复添加步骤会修改所有步骤标题的bug 2026-09-12 18:51:53 +08:00
xiaojunnuo 2b1a5296fe chore(certd-client): fix step form save data overwritten issue and add no payment tip
1. 修复步骤表单保存时因复用reactive对象导致已保存步骤被覆盖的问题,深拷贝currentStep后再传出
2. 新增无支付方式时显示暂不支持在线支付的提示
2026-09-12 18:49:24 +08:00
xiaojunnuo bded368cbb perf: 新增余额管理,管理员充值功能
1. 新增余额管理页面,支持用户余额查询与充值
2. 新增余额变更记录页面,查看用户余额明细
3. 补充多语言国际化文案
4. 修复免费订单显示文案为0元而非免费
5. 隐藏审核页面的操作按钮
6. 注释掉无用的堆日志启动代码
2026-09-12 16:06:53 +08:00
xiaojunnuo 6a649aaca0 perf(acme-account-access): AcmeAccount授权允许修改eab,重新生成account
修改了三处ACME账号表单的禁用判断条件,新增判断当editAccount为true时不禁用控件,允许在编辑已有账号时修改相关配置
2026-09-12 14:44:43 +08:00
xiaojunnuo e971145686 perf(ui): 商业版套餐购买不要求设置支付方式
1. 将套餐设置页面的"需要开启支付方式"提示改为"建议"
2. 新增支付方式为空且余额不足时的红色警告提示
3. 默认开启返利余额使用并修复支付字典加载异常处理
2026-09-12 14:28:52 +08:00
xiaojunnuo bbca449f94 Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2026-09-12 09:32:36 +08:00
xiaojunnuo e6852c2715 docs: 更新AGENTS.md编码规范说明
refactor(plugin-fastly): 简化多行API调用代码
refactor(plugin-serv
2026-09-11 19:01:29 +08:00
xiaojunnuo 47cb523307 chore(cloudflare): 优化权限说明的换行展示
修复Cloudflare插件的权限说明文本排版,将权限项用换行分开展示,提升可读性
2026-09-10 15:13:38 +08:00
xiaojunnuo cbf46b5c48 build: release 2026-09-07 00:07:49 +08:00
xiaojunnuo 7ed3105d97 build: publish 2026-09-06 23:51:23 +08:00
xiaojunnuo 61b945c08f build: trigger build image 2026-09-06 23:51:13 +08:00
140 changed files with 3438 additions and 397 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ on:
workflow_dispatch:
inputs:
version:
description: "版本号(如 v1.42.5)"
description: "版本号(如 1.42.5)"
required: true
type: string
+2 -1
View File
@@ -42,4 +42,5 @@ output/
.uploads/
.certd-plugin-history/
.tmp
.plugin-dev/
.plugin-dev/
.tmp_chunks/
+60
View File
@@ -392,6 +392,66 @@ export class OAuthDemoAccess extends BaseAccess {
}
```
## 字段动态显隐(mergeScript)
多种鉴权方式共用一个授权时,用 `@AccessInput` 上的 `mergeScript` 按所选方式显隐字段:
```typescript
@AccessInput({
title: '鉴权方式',
value: 'apikey',
component: {
name: 'a-select',
vModel: 'value',
options: [
{ label: 'AccessKey签名', value: 'aksk' },
{ label: 'ApiKey', value: 'apikey' },
],
},
required: true,
})
authType = 'apikey';
@AccessInput({
title: 'accessKeyId',
component: { name: 'a-input', vModel: 'value' },
required: true,
mergeScript: `
return {
show: ctx.compute(({form})=>{
return !form.access.authType || form.access.authType === 'aksk';
})
}
`,
})
accessKeyId = '';
```
要点:
- `mergeScript` 返回的对象会深合并进本字段定义,可动态化的属性有 `show`、`required`、`helper`、
`component` 下的属性(`options`、`placeholder` 等)以及 `component.on` 事件。
- **动态判断必须写在 `ctx.compute(({form})=>{ ... })` 回调里**。`mergeScript` 本身在插件类
定义阶段就被拼成字符串,那时表单还没打开,用参数在服务端判断字段值没有意义。
- 授权表单字段值在 `form.access.xxx`(任务表单才是 `form.xxx`)。
- 历史授权没有新字段,条件里要用 `!form.access.authType || ...` 兜底走旧逻辑,保证老授权还能编辑保存。
- 联动其他字段用 `component.on` 事件回调,回调参数是 `{ form }`:
```typescript
mergeScript: `
return {
component: {
on: {
selectedChange: (scope) => {
const form = scope.form;
form.acmeAccountAccessId = null;
},
},
},
}
`,
```
## 注意事项
1. **插件命名**:插件名称应简洁明了,反映其功能。
+26 -4
View File
@@ -9,7 +9,7 @@ description: 用于通过 Certd API 开发、修改、读取、保存和恢复
本 Skill 使用 Certd HTTP API,不使用 WebSocket,也不依赖浏览器传递代码草稿。
- 前端只生成包含需求、API 地址和认证 Token 的启动提示词。
- 前端只生成包含需求、插件作者、API 地址和认证 Token 的启动提示词。
- Agent 直接调用 Certd API 读取和保存插件 YAML。
- Agent 的进度、日志和代码修改在 Codex/Trae 中查看。
- 插件开发临时文件和修改历史统一保存在 Agent 工作区的 `.plugin-dev/` 下,不保存到浏览器或 Certd 后端。
@@ -43,15 +43,13 @@ Certd 插件按来源分为三类:
## API 认证
提示词会提供 Certd API 地址和仅限 AI 插件开发接口的受限 Token。调用 API 时使用:
提示词会提供 Certd API 地址和仅限 AI 插件开发接口的受限的短时效的 Token。调用 API 时使用:
```http
Authorization: <token>
Content-Type: application/json
```
不要把 Token 写入代码、历史摘要、日志、提交信息或插件 YAML。
所有 Certd API 请求统一使用 Node.js 的 `fetch`。不要使用 PowerShell 的 `Invoke-RestMethod`、`Invoke-WebRequest` 或 .NET HTTP 客户端发送插件 YAML/JSON;它们在 Windows 上可能造成中文乱码或使完整 YAML 导入请求长时间无响应。
Node 请求须直接读取 UTF-8 文件或在 Node 内构造 JSON,并使用 `JSON.stringify`:
@@ -118,6 +116,7 @@ const response = await fetch(`${apiBase}/scoped/sys/ai/plugin/find`, {
- 修改后保存修改摘要。
- 恢复前再次备份当前版本。
- 不上传历史文件,不保存 Token、证书、私钥或真实授权值。
- 修改完一版后立即提交更新到certd,但不要发布到市场。
详细格式见 `references/local-history.md`。
@@ -125,6 +124,7 @@ const response = await fetch(`${apiBase}/scoped/sys/ai/plugin/find`, {
插件始终以完整 YAML 传递和保存,脚本源码放在顶层 `content` 字段。
- 新插件的 `author` 必须使用启动提示词中给出的插件作者,不要自行编造作者名;修改已有插件时保留原 `author`,不要改成其他作者。
- 统一使用 `await _ctx.import(...)` 引用模块。
- `"/@/..."` 表示以绝对路径引用 `server/src/` 下的模块。
- 最后返回继承目标基类的 class。
@@ -135,6 +135,28 @@ const response = await fetch(`${apiBase}/scoped/sys/ai/plugin/find`, {
需要字段格式时读取 `references/online-yaml-format.md`。
需要组件示例时读取 `references/component-examples.md`。
需要按其他字段动态显隐/必填/联动时读取 `references/merge-script.md`。
### 字段动态显隐(mergeScript)
字段定义支持 `mergeScript`:脚本 `return` 的对象会合并进本字段,用来做「选了 A 才显示 B」这类联动。
```yaml
mergeScript: |2-
return {
show: ctx.compute(({form})=>{
return form.access.authType === 'apikey';
})
}
```
- 动态逻辑必须写在 `ctx.compute(({form})=>{ ... })` 回调里;`mergeScript` 本身在插件类定义阶段就
被拼成字符串,那时表单还没打开,用参数在 node 侧判断字段值没有任何意义。
- 授权表单字段值在 `form.access.xxx`,任务表单字段值在 `form.xxx`。
- 新增可选字段时要给历史数据兜底,例如 `!form.access.authType || form.access.authType === 'aksk'`。
完整用法(能力、可动态化的属性、事件联动、常见错误、验证方式)见 `references/merge-script.md`。
## 示例插件
@@ -1,6 +1,6 @@
name: DemoAccess
icon: logos:airflow-icon
title: Demo-授权插件示例 # 模块-插件名
title: Demo-授权插件示例 # 格式:平台-功能 ,例如: 网宿-授权 ; 阿里云-授权 ; 天翼云-授权
group: null
desc: 这只是一个示例
version: 1.0.0
@@ -1,6 +1,6 @@
name: DemoDeploy
icon: logos:amp-icon
title: Demo-部署插件示例 # 模块-插件名
title: Demo-部署插件示例 # 格式:平台-功能 ,例如: 网宿-更新CDNpro证书 ; 阿里云-上传到CAS证书管理服务 ; 天翼云-更新CDN证书
group: cdn
desc: 这仅仅是一个示例
version: 1.0.0
@@ -1,6 +1,6 @@
name: DemoDnsProvider
icon: fa-solid:frog
title: Demo-Dns提供商插件示例 # 模块-插件名
title: Demo-Dns提供商插件示例 # 格式:平台-功能 ,例如: 阿里云-DNS提供商 ;腾讯云-DNS提供商
desc: 这只是一个示例
type: custom
version: 1.0.0
@@ -0,0 +1,181 @@
# mergeScript 用法
来源:前端 `packages/ui/certd-client/src/use/use-refrence.tsx`,类型定义来自 `@fast-crud/fast-crud`。
`mergeScript` 用来让输入字段的定义**依赖当前表单里的其他字段值动态变化**,例如:
选了某个鉴权方式才显示对应字段、切换类型后清空已选值、下拉选项跟着另一个字段变。
它写成字符串放在字段定义上(内置插件放在 `@AccessInput` / `@TaskInput` 里,在线插件放在 `input.<字段>` 下):
```ts
@AccessInput({
title: "API Key",
component: { name: "a-input", vModel: "value" },
mergeScript: `
return {
show: ctx.compute(({form})=>{
//access里面 要用 form.access.[字段名]
return form.access.authType === 'apikey';
//deploy插件里面要用form.[字段名]
return form.authType === 'apikey';
})
}
`,
})
apiKey = "";
```
## 执行时机
字段定义被前端加载时,对**每一个字段**执行一次:
```js
const func = new Function('ctx', formItem.mergeScript);
const merged = func(ctx); // 字段脚本只执行这一次
merge(formItem, merged); // 返回值深合并进该字段的定义
delete formItem.mergeScript; // 执行完就删掉,最终定义里不再有 mergeScript
```
要点:
- 脚本体内**必须 `return` 一个对象**,返回的对象会深合并到本字段的定义上。
- 脚本本身只在加载字段时跑一次,**拿不到、也不应该判断用户填了什么值**。
- 真正的动态逻辑写在返回对象里的 `ctx.compute(...)` 回调中,由 fast-crud 在渲染时求值。
## ctx 提供的能力
| 能力 | 说明 |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `ctx.compute(fn)` | 最常用。返回一个响应式的计算值:`fn` 里读到的表单字段会被自动追踪,字段变化时重新计算,视图跟着刷新 |
`ctx.compute` 的回调参数是一个 context 对象,常用字段:
- `form`:当前表单数据。**这是唯一常用的一个**。
- `row`:行数据。
- `mode`:表单模式 `add` / `edit` / `view`。
- `getComponentRef(key)`:取其他字段组件的 ref。
注意:Certd 在把 `form` 传进回调前做过一次兼容处理(`form.input || form.body || form`),
所以两种表单取值路径不同:
- **授权(Access)表单**:字段值在 `form.access.xxx`(字段 key 会被加上 `access.` 前缀)。
- **任务(Task)表单**:字段值直接是 `form.xxx`。
## 可以动态化的属性
返回的对象里,任何字段定义属性都可以用 `ctx.compute`,常用的有:
| 属性 | 用途 |
| -------------------- | ------------------------------------------------------------------------------------------------- |
| `show` | 显隐(最常用) |
| `required` | 动态必填,例如 `required: ctx.compute(({form}) => form.targetType === 'ingress')` |
| `disabled` | 动态禁用,例如账号已创建后不允许再改:`disabled: ctx.compute(({form}) => !!form.access?.account)` |
| `helper` | 动态帮助文案,回调里按条件 `return` 不同字符串 |
| `value` | 动态默认值 |
| `component` 下的属性 | 动态 `options`、`placeholder`、`inputKey`、`type`、`disabled` 等 |
| `component.on` | 事件处理,见下面「联动」 |
| `col` / `order` | 布局与排序 |
只写了一层 `show` 时最简形式就是本文开头的例子。
## 联动(监听事件并改动其他字段)
`component.on` 的事件回调**不**用 `ctx.compute` 包,它本身就是回调,参数是
`{ form, $event, ... }`;`form` 同样做过 `form.input || form.body || form` 的兼容处理。
事件名按组件而定,例如 `selectedChange`。
```ts
mergeScript: `
return {
component: {
on: {
selectedChange: (scope) => {
const form = scope.form;
// 切换颁发机构后清空 ACME 账号,避免账号与机构不匹配
form.acmeAccountAccessId = null;
},
},
},
}
`,
```
另一种写法是把回调也做成计算值(需要用到组件上配置的 `onSelectedChange` / `onChange` 属性):
```ts
mergeScript: `
return {
component: {
onSelectedChange: ctx.compute(({form}) => {
return ($event) => {
form.dnsProviderAccessType = $event.accessType;
};
}),
},
}
`,
```
两种写法都能用,优先参考同目录已有插件的写法,保持一致。
## 常见错误
- **不要用 node 侧变量决定条件**。`mergeScript` 是在插件类定义阶段(node 侧)拼出来的字符串,
此时表单还没打开,拿不到任何用户输入;用参数拼条件等于把逻辑写死在服务端。
条件必须原样落到 `ctx.compute` 回调里。
- **不要用 `if` 直接判断字段值**。回调必须 `return` 一个值,写成
`show: ctx.compute(({form}) => { if (...) { return true } })` 也可以,但不要漏掉 `return`。
- **不要漏掉历史数据兼容**。新增可选字段时,历史数据里没有这个字段(值为 `undefined`),
条件要按旧行为兜底,例如 `!form.access.authType || form.access.authType === 'aksk'`。
- **不要访问 `form.access` 以外的路径**去读授权字段,也不要在授权表单里按任务表单的路径取值。
- **不要依赖脚本执行时的副作用**(打印日志、改全局状态),它在每次打开表单时都会重新执行。
## YAML 写法(在线插件)
`mergeScript` 是含换行的脚本字符串,必须用 YAML 块标量:
```yaml
input:
apiKey:
title: API Key
component:
name: a-input
vModel: value
mergeScript: |2-
return {
show: ctx.compute(({form})=>{
return form.access.authType === 'apikey';
})
}
```
导出生成的缩进比较特殊(块标量内容统一缩进),**不要手工调整缩进**:
用脚本生成 YAML,并保证导出后再导入内容一致。转换成 block scalar 时注意:
- `|` / `|2-` 等块标量不需要转义换行,不要把脚本写成带 `\n` 的单行。
- 脚本里不要出现 `??` 这类容易被误判为乱码的写法,用显式 `if (x == null)` 代替。
## 验证方式
前端表单没法在插件开发流程里直接断言,可以在后端加一个 `src/**/*.test.ts`,
按前端 `useReference` 的路径执行字段上的 `mergeScript`:
```ts
const computedGetters = [];
const ctx = {
compute: getter => {
computedGetters.push(getter);
return getter;
},
asyncCompute: () => undefined,
computed: () => undefined,
};
new Function('ctx', mergeScript)(ctx);
// 用假的 form 求值,断言显隐结果
const show = computedGetters[0]({ form: { access: { authType: 'apikey' } } });
```
参考实现:`packages/ui/certd-server/src/plugins/plugin-wangsu/access-input.test.ts`。
要点是断言「同一个 show 函数换 `authType` 结果会变」,证明显隐确实是运行时算的,
而不是在 node 侧定死的。
@@ -7,7 +7,7 @@
```yaml
name: DemoTask
author: demo
title: Demo 任务
title: Demo-插件名称 # 格式:平台-功能 ,例如: 网宿-更新CDNpro证书 ; 阿里云-上传到CAS证书管理服务 ; 天翼云-更新CDN证书
desc: 插件说明
icon: clarity:plugin-line
pluginType: task
@@ -61,6 +61,8 @@ content: |
### 编辑规则
- 修改已有插件时保留 `name`、`author`、`pluginType` 和已有兼容字段。
- 新插件的 `author` 使用启动提示词中给出的插件作者,不要自行编造作者名;启动提示词未给出作者时先向用户确认。
- `fullName` 由 `author/name` 组成,作者变化会导致插件身份变化,不要随意修改。
- 只修改需求涉及的字段,避免删除未知的 YAML 字段。
- 脚本过长时仍放在同一个 `content` block scalar 中。
- 提交前检查 YAML 可解析、`content` 非空、版本和插件类型没有被意外修改。
@@ -57,4 +57,41 @@ return class DemoAccess extends BaseAccess {
- `onTestRequest` 应调用实际 API 方法并在失败时抛出异常。
- 对外 API 方法应统一处理分页、错误和返回字段。
- 使用 `this.logger` 或框架提供的 logger,禁止 `console.log`。
- 一般将API接口方法封装到Access中,其他plugin调用access的方法当做client sdk使用
- 一般将API接口方法封装到Access中,其他plugin调用access的方法当做client sdk使用
## 字段动态显隐
多种鉴权方式(例如「ApiKey」和「AccessKey签名」)共用一个授权时,
用字段上的 `mergeScript` 按所选方式显隐对应字段,只让用户看到当前方式需要的字段:
```yaml
input:
authType:
title: 鉴权方式
value: apikey
component:
name: a-select
vModel: value
options:
- { label: AccessKey签名, value: aksk }
- { label: ApiKey, value: apikey }
accessKeyId:
title: accessKeyId
component:
name: a-input
vModel: value
mergeScript: |2-
return {
show: ctx.compute(({form})=>{
return !form.access.authType || form.access.authType === 'aksk';
})
}
```
- 判断必须写在 `ctx.compute(({form})=>{ ... })` 回调里;脚本本身在插件定义阶段就拼好了,
那时表单还没打开,用参数在服务端判断字段值没有意义。
- 授权表单的字段值在 `form.access.xxx`(任务表单才是 `form.xxx`)。
- 历史授权没有 `authType` 字段,条件里要用 `!form.access.authType || ...` 兜底走旧逻辑。
完整用法见父 Skill 的 `references/merge-script.md`。
+1 -1
View File
@@ -75,7 +75,7 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
- 只有真正机械化的大批量替换、格式化或生成任务才考虑脚本/工具。若必须使用临时脚本,应放在临时目录并在同一个受控步骤内完成创建、执行、删除;不要把临时脚本落在仓库里跨多步工具调用执行。
- 不要主动运行 `pnpm install`;缺依赖、TTY、网络导致安装或测试失败时,停止尝试并说明环境问题。
- 优先沿用现有模块、插件、service、页面模式;不要为形式上的复用制造过度抽象。
- 代码可读性优先于短写法。尽量少用三元表达式,优先使用完整的 `if/else`;复杂条件、链式调用、内联对象和多层 helper 调用要拆成命名清晰的中间变量或小方法。
- 【重要重要重要】代码可读性优先于短写法。尽量少用三元表达式,优先使用完整的 `if/else`【其他技能与此条要求冲突的,以此条要求为准】;复杂条件、链式调用、内联对象和多层 helper 调用要拆成命名清晰的中间变量或小方法。
- 方法调用链不要直接塞进另一个方法参数;先用有意义的局部变量承接返回值,再传入下一步。
- 不要在单一表达式内嵌套分支、对象构造与方法调用。优先使用清晰的 `if/else` 分支;仅在确实能降低复杂度时才提取有意义的中间变量,避免为拆分而增加阅读跳转。
- 注释优先使用中文,尤其是业务规则、兼容逻辑、协议细节和隐藏风险;文件已有英文风格或引用外部术语时可保持一致。
+36
View File
@@ -3,6 +3,42 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
### Bug Fixes
* [BUG]使用天翼云CDN部署证书时无法匹配域名 ([bb1f232](https://github.com/certd/certd/commit/bb1f232f415c522851dede979f5d609dda844fd3))
* 缺失运行时嵌套依赖时添加清除缓存提示 ([2d0b85b](https://github.com/certd/certd/commit/2d0b85b3054fc1aa82b3ab5db34a2a56308f71f2))
* 支持google persist校验方式 ([97d2b50](https://github.com/certd/certd/commit/97d2b5007409ec79ea864d437c473a2f9b5dfae4))
* **config:** 修复环境变量加载逻辑并更新配置文件 ([07f0d3b](https://github.com/certd/certd/commit/07f0d3b4d626628a0e3dfc7e5c52e8c4a98bad47))
* **ctyun-cdn:** 修复天翼云CDN部署插件的域名选择问题 ([ea7d14c](https://github.com/certd/certd/commit/ea7d14c289b8720781bf101bfef59eac7bb03765))
* **webhook:** 修复模版转义换行符,避免手动上传的证书报错 ([2735feb](https://github.com/certd/certd/commit/2735feb3302f8308708dafc8b295adcd4b91d7f0))
### Features
* **client:** 新增客户端管理功能 ([3dbbe72](https://github.com/certd/certd/commit/3dbbe72fc15c114ca6b5e8ff45cb21f258b80650))
### Performance Improvements
* 宝塔站点列表支持搜索和排序 ([4420472](https://github.com/certd/certd/commit/4420472f8fac272d30fb8ccb1cc2ee9763989001))
* 网宿插件双鉴权支持 ([241b2e5](https://github.com/certd/certd/commit/241b2e55848471e61b5d2be0c87a7dc9a0d29557))
* **monitor:** 站点证书监控支持手动选中触发检查 ([f60e63f](https://github.com/certd/certd/commit/f60e63fbaf00460d5050cd7705bd75d57dec202d))
* **open-api:** openkey增加用途备注 ([bbeaf61](https://github.com/certd/certd/commit/bbeaf617d19750515f6eca51863e74e23bc1de50))
* **tencent clb:** 修改所有CLB地域的endpoint为就近接入 ([c427735](https://github.com/certd/certd/commit/c4277350bcc8cf034cf4e76db2a6daa1842bc5d7))
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
### Bug Fixes
* 修复添加步骤会修改所有步骤标题的bug ([51e83e4](https://github.com/certd/certd/commit/51e83e4c38f7f319314fdc38ef33cf5720b2dde0))
* 修复证书插件配置无法打开的bug ([5563abb](https://github.com/certd/certd/commit/5563abb13a62140f4dee668ab768cf4ff9371ddb))
### Performance Improvements
* 新增余额管理,管理员充值功能 ([bded368](https://github.com/certd/certd/commit/bded368cbb8974b6937ae260e448c25fae5c33ad))
* **acme-account-access:** AcmeAccount授权允许修改eab,重新生成account ([6a649aa](https://github.com/certd/certd/commit/6a649aaca083ccd3b517f7d676a58220aa33b37d))
* **ui:** 商业版套餐购买不要求设置支付方式 ([e971145](https://github.com/certd/certd/commit/e9711456860d7db40914a0c295534f7bca56f83c))
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
### Bug Fixes
+47
View File
@@ -3,6 +3,53 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
### Bug Fixes
* [BUG]使用天翼云CDN部署证书时无法匹配域名 ([bb1f232](https://github.com/certd/certd/commit/bb1f232f415c522851dede979f5d609dda844fd3))
* 缺失运行时嵌套依赖时添加清除缓存提示 ([2d0b85b](https://github.com/certd/certd/commit/2d0b85b3054fc1aa82b3ab5db34a2a56308f71f2))
* 支持google persist校验方式 ([97d2b50](https://github.com/certd/certd/commit/97d2b5007409ec79ea864d437c473a2f9b5dfae4))
* **config:** 修复环境变量加载逻辑并更新配置文件 ([07f0d3b](https://github.com/certd/certd/commit/07f0d3b4d626628a0e3dfc7e5c52e8c4a98bad47))
* **ctyun-cdn:** 修复天翼云CDN部署插件的域名选择问题 ([ea7d14c](https://github.com/certd/certd/commit/ea7d14c289b8720781bf101bfef59eac7bb03765))
* **webhook:** 修复模版转义换行符,避免手动上传的证书报错 ([2735feb](https://github.com/certd/certd/commit/2735feb3302f8308708dafc8b295adcd4b91d7f0))
### Features
* **client:** 新增客户端管理功能 ([3dbbe72](https://github.com/certd/certd/commit/3dbbe72fc15c114ca6b5e8ff45cb21f258b80650))
### Performance Improvements
* 宝塔站点列表支持搜索和排序 ([4420472](https://github.com/certd/certd/commit/4420472f8fac272d30fb8ccb1cc2ee9763989001))
* 网宿插件双鉴权支持 ([241b2e5](https://github.com/certd/certd/commit/241b2e55848471e61b5d2be0c87a7dc9a0d29557))
* **monitor:** 站点证书监控支持手动选中触发检查 ([f60e63f](https://github.com/certd/certd/commit/f60e63fbaf00460d5050cd7705bd75d57dec202d))
* **open-api:** openkey增加用途备注 ([bbeaf61](https://github.com/certd/certd/commit/bbeaf617d19750515f6eca51863e74e23bc1de50))
* **tencent clb:** 修改所有CLB地域的endpoint为就近接入 ([c427735](https://github.com/certd/certd/commit/c4277350bcc8cf034cf4e76db2a6daa1842bc5d7))
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
### Bug Fixes
* 修复添加步骤会修改所有步骤标题的bug ([51e83e4](https://github.com/certd/certd/commit/51e83e4c38f7f319314fdc38ef33cf5720b2dde0))
* 修复证书插件配置无法打开的bug ([5563abb](https://github.com/certd/certd/commit/5563abb13a62140f4dee668ab768cf4ff9371ddb))
### Performance Improvements
* 新增余额管理,管理员充值功能 ([bded368](https://github.com/certd/certd/commit/bded368cbb8974b6937ae260e448c25fae5c33ad))
* **acme-account-access:** AcmeAccount授权允许修改eab,重新生成account ([6a649aa](https://github.com/certd/certd/commit/6a649aaca083ccd3b517f7d676a58220aa33b37d))
* **ui:** 商业版套餐购买不要求设置支付方式 ([e971145](https://github.com/certd/certd/commit/e9711456860d7db40914a0c295534f7bca56f83c))
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
### Bug Fixes
* **plugin-service:** 修复同步自己开发的云端插件不显示已安装状态问题 ([dac94b6](https://github.com/certd/certd/commit/dac94b64f0ceb7176e2cf07ce220be0aa5ed98d2))
* **plugin:** 修复插件配置保存与加载的相关问题 ([bd94334](https://github.com/certd/certd/commit/bd9433478a43d5d64ce6370386d0fe0aaff07d2e))
### Performance Improvements
* **plugin:** add Fastly custom TLS certificates integration ([#797](https://github.com/certd/certd/issues/797)) [@szhu25](https://github.com/szhu25) ([2a2e963](https://github.com/certd/certd/commit/2a2e963914c4dfc192cbe0a4bbb9c115e626da01))
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
### Bug Fixes
+2 -2
View File
@@ -5,8 +5,8 @@
`Certd`默认启用https,监听7002端口
如果你想关闭https,或者修改端口,可以在环境变量中配置
```shell
CERTD_HTTPS_ENABLE=true
CERTD_HTTPS_port=7002
certd_https_enabled=true
certd_https_port=7002
```
+1 -1
View File
@@ -9,5 +9,5 @@
}
},
"npmClient": "pnpm",
"version": "1.44.3"
"version": "1.45.0"
}
+10
View File
@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.45.0](https://github.com/publishlab/node-acme-client/compare/v1.44.4...v1.45.0) (2026-10-02)
### Bug Fixes
* 支持google persist校验方式 ([97d2b50](https://github.com/publishlab/node-acme-client/commit/97d2b5007409ec79ea864d437c473a2f9b5dfae4))
## [1.44.4](https://github.com/publishlab/node-acme-client/compare/v1.44.3...v1.44.4) (2026-09-12)
**Note:** Version bump only for package @certd/acme-client
## [1.44.3](https://github.com/publishlab/node-acme-client/compare/v1.44.2...v1.44.3) (2026-09-06)
**Note:** Version bump only for package @certd/acme-client
+4 -3
View File
@@ -3,7 +3,7 @@
"description": "Simple and unopinionated ACME client",
"private": false,
"author": "nmorsman",
"version": "1.44.3",
"version": "1.45.0",
"type": "module",
"module": "./dist/index.js",
"main": "./dist/index.js",
@@ -18,7 +18,7 @@
"types"
],
"dependencies": {
"@certd/basic": "^1.44.3",
"@certd/basic": "^1.45.0",
"@peculiar/x509": "^1.11.0",
"asn1js": "^3.0.5",
"axios": "^1.9.0",
@@ -27,6 +27,7 @@
"https-proxy-agent": "^7.0.5",
"lodash-es": "^4.17.21",
"node-forge": "^1.3.1",
"psl": "^1.15.0",
"punycode.js": "^2.3.1"
},
"devDependencies": {
@@ -76,5 +77,5 @@
"bugs": {
"url": "https://github.com/publishlab/node-acme-client/issues"
},
"gitHead": "67f3daae625ba734e7e390dd84acc9e3bc1930bc"
"gitHead": "fcbf1045fe3806fa1a0bebf5038a6f67aec283d0"
}
@@ -0,0 +1,21 @@
// @ts-ignore
import psl from "psl";
export function getDnsPersistIssuer(directoryUrl?: string) {
if (!directoryUrl) {
return "letsencrypt.org";
}
let directory: URL;
try {
directory = new URL(directoryUrl);
} catch {
throw new Error(`ACME Directory URL无效,无法生成DNS持久验证Issuer:${directoryUrl}`);
}
const issuer = psl.get(directory.hostname);
if (!issuer) {
throw new Error(`无法从ACME Directory URL获取可注册主域名:${directory.hostname}`);
}
return issuer;
}
+1
View File
@@ -79,3 +79,4 @@ export * from './verify.js'
export * from './error.js'
export * from './util.js'
export * from './dns-persist.js'
+10
View File
@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
### Bug Fixes
* **webhook:** 修复模版转义换行符,避免手动上传的证书报错 ([2735feb](https://github.com/certd/certd/commit/2735feb3302f8308708dafc8b295adcd4b91d7f0))
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
**Note:** Version bump only for package @certd/basic
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
**Note:** Version bump only for package @certd/basic
+1 -1
View File
@@ -1 +1 @@
23:46
00:51
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/basic",
"private": false,
"version": "1.44.3",
"version": "1.45.0",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -54,5 +54,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "67f3daae625ba734e7e390dd84acc9e3bc1930bc"
"gitHead": "fcbf1045fe3806fa1a0bebf5038a6f67aec283d0"
}
@@ -1,6 +1,21 @@
import dayjs from "dayjs";
export const stringUtils = {
replaceTemplate(target: string, body: Record<string, unknown>, urlEncode = false) {
let bodyStr = target;
for (const key of Object.keys(body)) {
const rawValue = body[key] == null ? "" : String(body[key]);
let value = urlEncode ? encodeURIComponent(rawValue) : rawValue;
value = value.replace(/[\u0000-\u001f]/g, char => {
const escapes: Record<string, string> = { "\b": "\\b", "\f": "\\f", "\n": "\\n", "\r": "\\r", "\t": "\\t" };
return escapes[char] ?? `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`;
});
bodyStr = bodyStr.replaceAll(`\${${key}}`, value);
bodyStr = bodyStr.replaceAll(`{${key}}`, value);
}
return bodyStr;
},
maxLength(str?: string, length = 100) {
if (str) {
return str.length > length ? str.slice(0, length) + "..." : str;
+10
View File
@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
### Bug Fixes
* 缺失运行时嵌套依赖时添加清除缓存提示 ([2d0b85b](https://github.com/certd/certd/commit/2d0b85b3054fc1aa82b3ab5db34a2a56308f71f2))
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
**Note:** Version bump only for package @certd/pipeline
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
**Note:** Version bump only for package @certd/pipeline
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/pipeline",
"private": false,
"version": "1.44.3",
"version": "1.45.0",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -21,8 +21,8 @@
"lint": "eslint --fix --ext .ts src"
},
"dependencies": {
"@certd/basic": "^1.44.3",
"@certd/plus-core": "^1.44.3",
"@certd/basic": "^1.45.0",
"@certd/plus-core": "^1.45.0",
"dayjs": "^1.11.7",
"lodash-es": "^4.17.21",
"reflect-metadata": "^0.2.2"
@@ -51,5 +51,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "67f3daae625ba734e7e390dd84acc9e3bc1930bc"
"gitHead": "fcbf1045fe3806fa1a0bebf5038a6f67aec283d0"
}
@@ -64,6 +64,28 @@ describe("RuntimeDepsService", () => {
assert.equal(mod.value, 42);
});
it("logs a cache clearing hint when a nested runtime dependency is missing", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-missing-nested-"));
const packageDir = path.join(rootDir, "node_modules", "runtime-only");
fs.mkdirSync(packageDir, { recursive: true });
fs.writeFileSync(path.join(rootDir, "package.json"), JSON.stringify({ name: "runtime-root", type: "module" }), "utf8");
fs.writeFileSync(path.join(packageDir, "package.json"), JSON.stringify({ name: "runtime-only", main: "index.js" }), "utf8");
fs.writeFileSync(path.join(packageDir, "index.js"), "module.exports = require('missing-nested');\n", "utf8");
const logs: string[] = [];
const logger = {
info() {},
warn() {},
error(message: string, ...args: any[]) {
logs.push([message, ...args.map(item => item?.message || String(item))].join(" "));
},
};
const service = new RuntimeDepsService({ rootDir }, null);
await assert.rejects(() => service.importRuntime("runtime-only", logger));
assert.ok(logs.some(message => message.includes("Cannot find module 'missing-nested'")));
assert.ok(logs.some(message => message.includes("插件管理页面->清除缓存依赖->重启certd")));
});
it("installs configured lazy dependency when import target is missing", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-lazy-"));
const service = new RuntimeDepsService({ rootDir, lazyDependencies: { "lazy-pkg": "^1.2.3" } }, null);
+11 -1
View File
@@ -251,6 +251,7 @@ export class RuntimeDepsService {
}
let resolved: string;
let fromRuntimeDeps = false;
try {
//尝试加载本项目依赖
resolved = this.resolveProjectSpecifier(specifier).resolved;
@@ -258,13 +259,22 @@ export class RuntimeDepsService {
if (!this.isModuleNotFoundError(error)) {
throw error;
}
fromRuntimeDeps = true;
resolved = await this.resolveLazyOrInstallSpecifier(specifier, logger);
}
if (!resolved) {
throw new Error(`依赖未安装成功: ${specifier}`);
}
return await import(pathToFileURL(resolved).href);
try {
return await import(pathToFileURL(resolved).href);
} catch (error: any) {
if (fromRuntimeDeps && this.isModuleNotFoundError(error)) {
logger.error?.("动态依赖加载失败:", error);
logger.error?.("动态依赖加载失败,请前往插件管理页面->清除缓存依赖->重启certd后重试");
}
throw error;
}
}
//动态获取模块解析
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
**Note:** Version bump only for package @certd/lib-huawei
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
**Note:** Version bump only for package @certd/lib-huawei
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
**Note:** Version bump only for package @certd/lib-huawei
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-huawei",
"private": false,
"version": "1.44.3",
"version": "1.45.0",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
"types": "./dist/d/index.d.ts",
@@ -31,5 +31,5 @@
"prettier": "3.3.3",
"tslib": "^2.8.1"
},
"gitHead": "67f3daae625ba734e7e390dd84acc9e3bc1930bc"
"gitHead": "fcbf1045fe3806fa1a0bebf5038a6f67aec283d0"
}
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
**Note:** Version bump only for package @certd/lib-iframe
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
**Note:** Version bump only for package @certd/lib-iframe
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
**Note:** Version bump only for package @certd/lib-iframe
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-iframe",
"private": false,
"version": "1.44.3",
"version": "1.45.0",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -37,5 +37,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "67f3daae625ba734e7e390dd84acc9e3bc1930bc"
"gitHead": "fcbf1045fe3806fa1a0bebf5038a6f67aec283d0"
}
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
**Note:** Version bump only for package @certd/jdcloud
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
**Note:** Version bump only for package @certd/jdcloud
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
**Note:** Version bump only for package @certd/jdcloud
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/jdcloud",
"version": "1.44.3",
"version": "1.45.0",
"description": "jdcloud openApi sdk",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
@@ -63,5 +63,5 @@
"fetch"
]
},
"gitHead": "67f3daae625ba734e7e390dd84acc9e3bc1930bc"
"gitHead": "fcbf1045fe3806fa1a0bebf5038a6f67aec283d0"
}
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
**Note:** Version bump only for package @certd/lib-k8s
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
**Note:** Version bump only for package @certd/lib-k8s
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
**Note:** Version bump only for package @certd/lib-k8s
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-k8s",
"private": false,
"version": "1.44.3",
"version": "1.45.0",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -21,7 +21,7 @@
"lint": "eslint --fix --ext .ts src"
},
"dependencies": {
"@certd/basic": "^1.44.3",
"@certd/basic": "^1.45.0",
"@kubernetes/client-node": "0.21.0"
},
"devDependencies": {
@@ -38,5 +38,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "67f3daae625ba734e7e390dd84acc9e3bc1930bc"
"gitHead": "fcbf1045fe3806fa1a0bebf5038a6f67aec283d0"
}
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
### Features
* **client:** 新增客户端管理功能 ([3dbbe72](https://github.com/certd/certd/commit/3dbbe72fc15c114ca6b5e8ff45cb21f258b80650))
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
### Bug Fixes
* 修复证书插件配置无法打开的bug ([5563abb](https://github.com/certd/certd/commit/5563abb13a62140f4dee668ab768cf4ff9371ddb))
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
**Note:** Version bump only for package @certd/lib-server
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/lib-server",
"version": "1.44.3",
"version": "1.45.0",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -29,11 +29,11 @@
],
"license": "AGPL",
"dependencies": {
"@certd/acme-client": "^1.44.3",
"@certd/basic": "^1.44.3",
"@certd/pipeline": "^1.44.3",
"@certd/plugin-lib": "^1.44.3",
"@certd/plus-core": "^1.44.3",
"@certd/acme-client": "^1.45.0",
"@certd/basic": "^1.45.0",
"@certd/pipeline": "^1.45.0",
"@certd/plugin-lib": "^1.45.0",
"@certd/plus-core": "^1.45.0",
"@midwayjs/cache": "3.14.0",
"@midwayjs/core": "3.20.11",
"@midwayjs/i18n": "3.20.13",
@@ -69,5 +69,5 @@
"typeorm": "^0.3.20",
"typescript": "^5.4.2"
},
"gitHead": "67f3daae625ba734e7e390dd84acc9e3bc1930bc"
"gitHead": "fcbf1045fe3806fa1a0bebf5038a6f67aec283d0"
}
@@ -133,12 +133,12 @@ export class SysPrivateSettings extends BaseSettings {
// 内置项不在此配置 Directory URL:其端点由运行时按加密算法(pkType)通过 acme.getDirectoryUrl 获取,
// 例如 ZeroSSL、SSL.com 的 RSA 与 EC 证书使用不同的端点,写死单一 URL 会导致 EC 申请走错端点。
customAcmeProviders?: CustomAcmeProvider[] = [
{ sslProvider: "letsencrypt", title: "Let's Encrypt", directoryUrl: "", needEAB: false, builtIn: true },
{ sslProvider: "letsencrypt_staging", title: "Let's Encrypt测试环境", directoryUrl: "", needEAB: false, builtIn: true },
{ sslProvider: "letsencrypt", title: "Let's Encrypt(支持7天IP证书)", directoryUrl: "", needEAB: false, builtIn: true },
{ sslProvider: "google", title: "Google", directoryUrl: "", needEAB: true, builtIn: true },
{ sslProvider: "zerossl", title: "ZeroSSL", directoryUrl: "", needEAB: true, builtIn: true },
{ sslProvider: "litessl", title: "litessl", directoryUrl: "", needEAB: true, builtIn: true },
{ sslProvider: "litessl", title: "Litessl", directoryUrl: "", needEAB: true, builtIn: true },
{ sslProvider: "sslcom", title: "SSL.com", directoryUrl: "", needEAB: true, builtIn: true },
{ sslProvider: "letsencrypt_staging", title: "Let's Encrypt测试环境(只能生成测试证书)", directoryUrl: "", needEAB: false, builtIn: true },
];
removeSecret() {
@@ -174,11 +174,11 @@ export class AccessService extends BaseService<AccessEntity> {
throw new ValidateException("userId不能为空");
}
if (userId !== entity.userId) {
throw new PermissionException("您对该Access授权无访问权限");
throw new PermissionException(`您对该Access授权${entity.name}(${entity.id})无访问权限`);
}
}
if (projectId != null && projectId !== entity.projectId) {
throw new PermissionException("您对该Access授权无访问权限");
throw new PermissionException(`您对该Access授权${entity.name}(${entity.id})无访问权限`);
}
// const access = accessRegistry.get(entity.type);
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
**Note:** Version bump only for package @certd/midway-flyway-js
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/midway-flyway-js",
"version": "1.44.3",
"version": "1.45.0",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -52,5 +52,5 @@
"typeorm": "^0.3.20",
"typescript": "^5.4.2"
},
"gitHead": "67f3daae625ba734e7e390dd84acc9e3bc1930bc"
"gitHead": "fcbf1045fe3806fa1a0bebf5038a6f67aec283d0"
}
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
**Note:** Version bump only for package @certd/plugin-cert
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
**Note:** Version bump only for package @certd/plugin-cert
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
**Note:** Version bump only for package @certd/plugin-cert
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-cert",
"private": false,
"version": "1.44.3",
"version": "1.45.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -20,7 +20,7 @@
"lint": "eslint --fix --ext .ts src"
},
"dependencies": {
"@certd/plugin-lib": "^1.44.3"
"@certd/plugin-lib": "^1.45.0"
},
"devDependencies": {
"@types/chai": "^4.3.12",
@@ -38,5 +38,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "67f3daae625ba734e7e390dd84acc9e3bc1930bc"
"gitHead": "fcbf1045fe3806fa1a0bebf5038a6f67aec283d0"
}
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
**Note:** Version bump only for package @certd/plugin-lib
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
**Note:** Version bump only for package @certd/plugin-lib
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
**Note:** Version bump only for package @certd/plugin-lib
+5 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-lib",
"private": false,
"version": "1.44.3",
"version": "1.45.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -17,9 +17,9 @@
"lint": "eslint --fix --ext .ts src"
},
"dependencies": {
"@certd/acme-client": "^1.44.3",
"@certd/basic": "^1.44.3",
"@certd/pipeline": "^1.44.3",
"@certd/acme-client": "^1.45.0",
"@certd/basic": "^1.45.0",
"@certd/pipeline": "^1.45.0",
"dayjs": "^1.11.7",
"jszip": "^3.10.1",
"lodash-es": "^4.17.21",
@@ -45,5 +45,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "67f3daae625ba734e7e390dd84acc9e3bc1930bc"
"gitHead": "fcbf1045fe3806fa1a0bebf5038a6f67aec283d0"
}
+23
View File
@@ -3,6 +3,29 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
### Features
* **client:** 新增客户端管理功能 ([3dbbe72](https://github.com/certd/certd/commit/3dbbe72fc15c114ca6b5e8ff45cb21f258b80650))
### Performance Improvements
* **monitor:** 站点证书监控支持手动选中触发检查 ([f60e63f](https://github.com/certd/certd/commit/f60e63fbaf00460d5050cd7705bd75d57dec202d))
* **open-api:** openkey增加用途备注 ([bbeaf61](https://github.com/certd/certd/commit/bbeaf617d19750515f6eca51863e74e23bc1de50))
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
### Bug Fixes
* 修复添加步骤会修改所有步骤标题的bug ([51e83e4](https://github.com/certd/certd/commit/51e83e4c38f7f319314fdc38ef33cf5720b2dde0))
* 修复证书插件配置无法打开的bug ([5563abb](https://github.com/certd/certd/commit/5563abb13a62140f4dee668ab768cf4ff9371ddb))
### Performance Improvements
* 新增余额管理,管理员充值功能 ([bded368](https://github.com/certd/certd/commit/bded368cbb8974b6937ae260e448c25fae5c33ad))
* **ui:** 商业版套餐购买不要求设置支付方式 ([e971145](https://github.com/certd/certd/commit/e9711456860d7db40914a0c295534f7bca56f83c))
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
### Bug Fixes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/ui-client",
"version": "1.44.3",
"version": "1.45.0",
"private": true,
"scripts": {
"dev": "vite --open",
@@ -104,8 +104,8 @@
"zod-defaults": "^0.1.3"
},
"devDependencies": {
"@certd/lib-iframe": "^1.44.3",
"@certd/pipeline": "^1.44.3",
"@certd/lib-iframe": "^1.45.0",
"@certd/pipeline": "^1.45.0",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@types/chai": "^4.3.12",
@@ -19,6 +19,7 @@ import sysauthority from "./certd/sys-authority";
import syscname from "./certd/sys-cname";
import tutorial from "./certd/tutorial";
import cron from "./certd/cron";
import client from "./certd/client";
// Note: @ is reserved in locale messages; use {'@'} when needed.
export default {
@@ -43,4 +44,5 @@ export default {
...syscname,
...tutorial,
...cron,
...client,
};
@@ -19,6 +19,7 @@ export default {
subdomainConfirmContent: "{domain} appears to be a subdomain. Only delegated subdomains and free second-level subdomains need to be maintained here. Otherwise certificate application may fail. Continue?",
importFromProvider: "Import from Domain Provider",
importFromResolveRecords: "Import from DNS Records",
importFromResolveRecordsTooltip: "Import DNS records as monitoring sites",
syncExpirationDate: "Sync Domain Expiration Time",
syncTaskSubmitted: "Sync task submitted",
syncExpirationProgress: "Sync Domain Expiration Progress",
@@ -0,0 +1,20 @@
export default {
clientManager: "Client Manager",
client: {
title: "Client Manager",
description: "View online status and managed site count of connected Certd clients. Clients report a heartbeat every 10 minutes and are marked offline if no heartbeat arrives for 30 minutes.",
plusRequired: "This feature requires the Pro edition",
machineName: "Machine",
clientId: "Client ID",
version: "Version",
os: "OS",
status: "Status",
online: "Online",
offline: "Offline",
siteCount: "Sites",
httpsSiteCount: "HTTPS Sites",
syncedSiteCount: "Synced",
failedSiteCount: "Failed",
lastHeartbeatAt: "Last Heartbeat",
},
};
@@ -53,6 +53,8 @@ export default {
suiteSetting: "Suite Settings",
orderManager: "Order Management",
userSuites: "User Suites",
balanceManager: "Balance Management",
balanceLog: "Balance Change Records",
inviteCommissionSetting: "Incentive Plan Settings",
inviteLevel: "Promotion Levels",
inviteUserLevel: "User Promotion Levels",
@@ -3,6 +3,9 @@ export default {
scopeOpenApiOnly: "Open API Only",
scopeFullAccount: "Full Account Permissions",
scopeHelper: "Open API only allows access to open APIs; full account permissions allow access to all APIs",
keyRemark: "Key Purpose",
keyRemarkHelper: "Describe the purpose of this key for easier identification",
keyRemarkMaxChars: "Maximum 512 characters",
gen: {
text: "API Test",
title: "x-certd-token",
@@ -7,6 +7,8 @@ export default {
settingLink: "Site Monitoring Settings",
limitInfo: "Basic edition limited to 1, professional and above unlimited, current",
checkAll: "Check All",
checkBatch: "Check Selected",
batchCheckConfirm: "Check the {count} selected sites now?",
checkNow: "Check Now",
batchDelete: "Batch Delete",
confirmTitle: "Confirm",
@@ -19,6 +19,7 @@ import sysauthority from "./certd/sys-authority";
import syscname from "./certd/sys-cname";
import tutorial from "./certd/tutorial";
import cron from "./certd/cron";
import client from "./certd/client";
// Note: @ is reserved in locale messages; use {'@'} when needed.
export default {
@@ -43,4 +44,5 @@ export default {
...syscname,
...tutorial,
...cron,
...client,
};
@@ -19,6 +19,7 @@ export default {
subdomainConfirmContent: "检测到{domain}为子域名,只有托管子域名和免费二级子域名才需要在此处维护,否则会导致申请证书失败,请确认是否继续?",
importFromProvider: "从域名提供商导入",
importFromResolveRecords: "从解析记录导入",
importFromResolveRecordsTooltip: "从域名的解析记录中导入并作为监控站点",
syncExpirationDate: "同步域名过期时间",
syncTaskSubmitted: "同步任务已提交",
syncExpirationProgress: "同步域名过期时间进度",
@@ -0,0 +1,20 @@
export default {
clientManager: "客户端管理",
client: {
title: "客户端管理",
description: "查看已接入的 Certd 客户端在线状态与管理的站点数量;客户端每 10 分钟上报一次心跳,超过 30 分钟未上报则标记为离线",
plusRequired: "此功能需要专业版",
machineName: "机器名",
clientId: "客户端ID",
version: "版本",
os: "系统",
status: "在线状态",
online: "在线",
offline: "离线",
siteCount: "站点数",
httpsSiteCount: "HTTPS站点",
syncedSiteCount: "已同步",
failedSiteCount: "异常",
lastHeartbeatAt: "上次心跳",
},
};
@@ -54,6 +54,8 @@ export default {
suiteSetting: "套餐设置",
orderManager: "订单管理",
userSuites: "用户套餐",
balanceManager: "余额管理",
balanceLog: "余额变更记录",
inviteCommissionSetting: "激励计划设置",
inviteLevel: "推广等级",
inviteUserLevel: "用户推广等级",
@@ -3,6 +3,9 @@ export default {
scopeOpenApiOnly: "仅开放接口",
scopeFullAccount: "账户所有权限",
scopeHelper: "仅开放接口只可以访问开放接口,账户所有权限可以访问所有接口",
keyRemark: "密钥用途",
keyRemarkHelper: "说明该密钥的用途,便于区分和查找",
keyRemarkMaxChars: "最大512个字符",
gen: {
text: "接口测试",
title: "x-certd-token",
@@ -7,6 +7,8 @@ export default {
settingLink: "站点监控设置",
limitInfo: "基础版限制1条,专业版以上无限制,当前",
checkAll: "检查全部",
checkBatch: "检查选中",
batchCheckConfirm: "确认触发检查选中的{count}个站点吗?",
checkNow: "立即检查",
batchDelete: "批量删除",
confirmTitle: "确认",
@@ -1,4 +1,4 @@
import { useProjectStore } from "/@/store/project";
import { useProjectStore } from "/@/store/project";
import { useSettingStore } from "/@/store/settings";
export const certdResources = [
@@ -128,6 +128,17 @@ export const certdResources = [
keepAlive: true,
},
},
{
title: "certd.clientManager",
name: "ClientManager",
path: "/cert/client",
component: "/certd/client/index.vue",
meta: {
icon: "ion:desktop-outline",
auth: true,
keepAlive: true,
},
},
{
title: "certd.sysResources.currentProject",
name: "ProjectMemberManager",
@@ -317,6 +317,34 @@ export const sysResources = [
keepAlive: true,
},
},
{
title: "certd.sysResources.balanceManager",
name: "SysWallet",
path: "/sys/suite/wallet",
component: "/sys/suite/wallet/index.vue",
meta: {
show: () => {
const settingStore = useSettingStore();
return settingStore.isComm;
},
icon: "ion:wallet-outline",
permission: "sys:settings:edit",
auth: true,
keepAlive: true,
},
},
{
title: "certd.sysResources.balanceLog",
name: "SysWalletLog",
path: "/sys/suite/wallet-log",
component: "/sys/suite/wallet/log/index.vue",
meta: {
isMenu: false,
permission: "sys:settings:view",
auth: true,
keepAlive: false,
},
},
{
title: "certd.sysResources.inviteCommissionSetting",
name: "SysInviteCommissionSetting",
@@ -26,6 +26,14 @@ export function fillPluginDefine(plugin: any) {
if (plugin.sysSetting?.metadata) {
merge(plugin.input, plugin.sysSetting.metadata?.input || {});
const keys = Object.keys(plugin.input);
for (const key of keys) {
//修复旧版本错误引入的key导致报错的问题 【多引入进了一个type=CertApply】
const column = plugin.input[key];
if (typeof column != "object") {
delete plugin.input[key];
}
}
// 应用选项映射
for (const key of Object.keys(plugin.input)) {
const inputDef = plugin.input[key];
@@ -51,10 +51,11 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
rowHandle: {
width: 120,
fixed: "right",
show: false,
buttons: {
view: { show: false },
edit: { show: false },
remove: { show: true },
remove: { show: false },
copy: { show: false },
},
},
@@ -3,7 +3,7 @@
<template #header>
<div class="title">
DNS持久验证记录
<span class="red sub" style="color: red">当前仅 Let's Encrypt 测试环境 可以申请 DNS 持久验证证书。</span>
<span class="red sub" style="color: red">当前仅 Google证书 可以申请 DNS 持久验证。</span>
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding"></fs-crud>
@@ -235,7 +235,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
} else if (value === "dns-persist") {
Modal.confirm({
title: "请前往DNS持久验证记录页面添加记录",
content: "DNS持久验证需要先配置ACME账号和_validation-persist持久TXT记录,续期时不再增删DNS记录;当前仅 Let's Encrypt 测试环境 可以申请。",
content: "DNS持久验证需要先配置ACME账号和_validation-persist持久TXT记录,续期时不再增删DNS记录;当前仅 Google证书 可以申请。",
async onOk() {
router.push({
path: "/cert/cert/dns-persist",
@@ -0,0 +1,29 @@
import { request } from "/src/api/service";
const apiPrefix = "/client";
export const clientApi = {
async GetList(query: any) {
return await request({
url: apiPrefix + "/page",
method: "post",
data: query,
});
},
async GetObj(id: number) {
return await request({
url: apiPrefix + "/info",
method: "post",
params: { id },
});
},
async DelObj(id: number) {
return await request({
url: apiPrefix + "/delete",
method: "post",
params: { id },
});
},
};
@@ -0,0 +1,243 @@
// @ts-ignore
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import dayjs from "dayjs";
import { useDicts } from "../dicts";
import { useProjectStore } from "/@/store/project";
import { useI18n } from "/src/locales";
import { clientApi } from "./api";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const { t } = useI18n();
const api = clientApi;
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
const delRequest = async (req: DelReq) => {
const { row } = req;
return await api.DelObj(row.id);
};
const editRequest = async (req: EditReq) => {};
const addRequest = async (req: AddReq) => {};
const { myProjectDict } = useDicts();
const projectStore = useProjectStore();
return {
id: "clientCrud",
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
pagination: {
pageSizeOptions: ["10", "20", "50", "100", "200"],
},
actionbar: {
buttons: {
add: {
show: false,
},
},
},
rowHandle: {
fixed: "right",
width: 120,
buttons: {
view: {
show: false,
},
edit: {
show: false,
},
},
},
search: {
initialForm: {
...projectStore.getSearchForm(),
},
},
columns: {
id: {
title: "ID",
key: "id",
type: "number",
search: {
show: false,
},
column: {
width: 70,
align: "center",
},
form: {
show: false,
},
},
machineName: {
title: t("certd.client.machineName"),
search: {
show: true,
},
type: "text",
form: {
show: false,
},
column: {
width: 180,
ellipsis: true,
},
},
clientId: {
title: t("certd.client.clientId"),
search: {
show: true,
},
type: "text",
form: {
show: false,
},
column: {
width: 220,
ellipsis: true,
},
},
version: {
title: t("certd.client.version"),
search: {
show: false,
},
type: "text",
form: {
show: false,
},
column: {
width: 90,
align: "center",
},
},
os: {
title: t("certd.client.os"),
search: {
show: false,
},
type: "text",
form: {
show: false,
},
column: {
width: 100,
align: "center",
},
},
online: {
title: t("certd.client.status"),
search: {
show: false,
},
type: "text",
form: {
show: false,
},
column: {
width: 100,
align: "center",
cellRender({ value, row }) {
const online = value === true;
const heartbeat = row.lastHeartbeatAt ? dayjs(row.lastHeartbeatAt).format("YYYY-MM-DD HH:mm:ss") : "";
return (
<a-tooltip title={heartbeat}>
<a-tag color={online ? "green" : "red"}>{online ? t("certd.client.online") : t("certd.client.offline")}</a-tag>
</a-tooltip>
);
},
},
},
siteCount: {
title: t("certd.client.siteCount"),
search: {
show: false,
},
type: "number",
form: {
show: false,
},
column: {
width: 90,
align: "center",
},
},
httpsSiteCount: {
title: t("certd.client.httpsSiteCount"),
search: {
show: false,
},
type: "number",
form: {
show: false,
},
column: {
width: 110,
align: "center",
},
},
syncedSiteCount: {
title: t("certd.client.syncedSiteCount"),
search: {
show: false,
},
type: "number",
form: {
show: false,
},
column: {
width: 90,
align: "center",
},
},
failedSiteCount: {
title: t("certd.client.failedSiteCount"),
search: {
show: false,
},
type: "number",
form: {
show: false,
},
column: {
width: 80,
align: "center",
},
},
lastHeartbeatAt: {
title: t("certd.client.lastHeartbeatAt"),
search: {
show: false,
},
type: "text",
form: {
show: false,
},
column: {
width: 170,
sorter: true,
cellRender({ value }) {
if (!value) {
return "-";
}
return dayjs(value).format("YYYY-MM-DD HH:mm:ss");
},
},
},
projectId: {
title: t("certd.fields.projectName"),
type: "dict-select",
dict: myProjectDict,
form: {
show: false,
},
},
},
},
};
}
@@ -0,0 +1,37 @@
<template>
<fs-page>
<template #header>
<div class="title flex items-center">
{{ t("certd.client.title") }}
<div class="sub flex items-center flex-1">
<div>
{{ t("certd.client.description") }}
</div>
<vip-button class="ml-5" mode="button"></vip-button>
</div>
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding"></fs-crud>
</fs-page>
</template>
<script lang="ts" setup>
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { useMounted } from "/@/use/use-mounted";
import { useSettingStore } from "/@/store/settings";
import { useI18n } from "/src/locales";
const { t } = useI18n();
const settingStore = useSettingStore();
defineOptions({
name: "ClientManager",
});
const context: any = {
permission: {
isProjectPermission: true,
},
};
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context });
useMounted(() => crudExpose.doRefresh());
</script>
@@ -63,6 +63,13 @@ export const siteInfoApi = {
method: "post",
});
},
async CheckBatch(ids: number[]) {
return await request({
url: apiPrefix + "/checkBatch",
method: "post",
data: { ids },
});
},
async Import(form: any) {
return await request({
@@ -100,6 +100,23 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
});
}
function checkBatch() {
if (selectedRowKeys.value.length === 0) {
notification.error({ message: t("monitor.selectRecordsFirst") });
return;
}
Modal.confirm({
title: t("monitor.confirmTitle"),
content: t("monitor.batchCheckConfirm", { count: selectedRowKeys.value.length }),
onOk: async () => {
await siteInfoApi.CheckBatch(selectedRowKeys.value);
notification.success({ message: t("monitor.checkSubmitted") });
selectedRowKeys.value = [];
await crudExpose.doRefresh();
},
});
}
const GroupTypeSite = "site";
const groupDictRef = createGroupDictRef(GroupTypeSite);
@@ -253,6 +270,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
show: hasActionPermission("write"),
title: t("certd.domain.importFromResolveRecords"),
text: t("certd.domain.importFromResolveRecords"),
tooltip: { title: t("certd.domain.importFromResolveRecordsTooltip") },
type: "primary",
// needPlus: true,
color: "gold",
@@ -274,6 +292,15 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
checkAll();
},
},
checkBatch: {
show: true,
text: t("monitor.checkBatch"),
type: "primary",
icon: "ion:play-forward-outline",
click() {
checkBatch();
},
},
},
},
rowHandle: {
@@ -13,10 +13,15 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
// 编辑允许修改备注与权限范围。
// keyId/keySecret 属于密钥本体,列表中 keySecret 是脱敏串,误提交会把真实密钥写坏,故不提交。
const editRequest = async (req: EditReq) => {
const { form, row } = req;
form.id = row.id;
const res = await api.UpdateObj(form);
const res = await api.UpdateObj({
id: row.id,
remark: form.remark,
scope: form.scope,
});
return res;
};
const delRequest = async (req: DelReq) => {
@@ -68,7 +73,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
buttons: {
view: { show: true },
copy: { show: false },
edit: { show: false },
edit: { show: true },
remove: { show: true },
gen: {
text: t("certd.gen.text"),
@@ -193,6 +198,22 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
sorter: true,
},
},
remark: {
title: t("certd.keyRemark"),
type: "text",
search: {
show: true,
},
column: {
width: 200,
sorter: true,
},
form: {
show: true,
helper: t("certd.keyRemarkHelper"),
rules: [{ max: 512, message: t("certd.keyRemarkMaxChars") }],
},
},
projectId: {
title: t("certd.fields.projectName"),
type: "dict-select",
@@ -280,7 +280,10 @@ function useStepForm() {
return;
}
callback.value("save", currentStep);
// currentStep 是复用同一份 reactive 对象,直接传给回调会被 task.steps 保存同一引用,
// 后续再次 stepOpen 会 delete 掉其属性,导致已保存的步骤(如第一个步骤)被覆盖。
// 这里深拷贝一份再传出,保证每个步骤是独立对象。
callback.value("save", cloneDeep(currentStep));
stepDrawerClose();
};
@@ -53,9 +53,14 @@
<div class="flex-o mt-5">
<span class="label">{{ $t("certd.order.paymentMethod") }}:</span>
<div v-if="durationSelected.price === 0 || expectedThirdPartyAmount === 0">{{ $t("certd.order.free") }}</div>
<div v-if="durationSelected.price === 0 || expectedThirdPartyAmount === 0">0元</div>
<div v-else-if="noPaymentMethod">当前仅支持余额支付</div>
<fs-dict-select v-else v-model:value="formRef.payType" :dict="paymentsDictRef" style="width: 200px"> </fs-dict-select>
</div>
<div v-if="showBalanceInsufficientTip" class="flex-o mt-5">
<span class="color-red">余额不足,请联系管理员,充值或设置套餐</span>
</div>
</div>
</a-modal>
</template>
@@ -88,12 +93,17 @@ async function open(opts: OrderModalOpenReq) {
formRef.value.productId = opts.product.id;
formRef.value.duration = opts.duration;
formRef.value.num = opts.num ?? 1;
formRef.value.useRebateBalance = false;
formRef.value.useRebateBalance = true;
try {
wallet.value = await GetWalletSummary();
} catch (e) {
wallet.value = { availableAmount: 0 };
}
try {
await paymentsDictRef.loadDict();
} catch (e) {
// 支付方式加载失败时按无可用支付方式处理
}
}
const paymentsDictRef = dict({
async getData() {
@@ -119,6 +129,23 @@ const expectedThirdPartyAmount = computed(() => {
return Math.max(0, (durationSelected.value?.price || 0) - expectedRebateAmount.value);
});
const noPaymentMethod = computed(() => {
return paymentsDictRef.data.length === 0;
});
const balanceInsufficient = computed(() => {
const price = durationSelected.value?.price || 0;
if (price <= 0) {
return false;
}
const availableAmount = wallet.value?.availableAmount || 0;
return availableAmount < price;
});
const showBalanceInsufficientTip = computed(() => {
return noPaymentMethod.value && balanceInsufficient.value;
});
function amountToYuan(amount: number) {
return util.amount.toYuan(amount || 0);
}
@@ -21,7 +21,17 @@
</div>
</template>
<template v-else-if="step === 1"
><label class="plugin-ai-dev__label">开发需求</label><a-textarea v-model:value="requirement" :rows="6" /><a-button
><div v-if="mode === 'new'" class="plugin-ai-dev__field">
<label>插件作者</label>
<PluginAuthorField v-model="author" editable />
<div class="plugin-ai-dev__hint">自动读取绑定账号已注册的插件作者,也可以手动输入作者名称。</div>
</div>
<div v-else class="plugin-ai-dev__field">
<label>插件作者</label>
<a-input :value="author" disabled />
<div class="plugin-ai-dev__hint">修改已有插件时沿用该插件当前作者,作者变化会导致插件身份变化。</div>
</div>
<label class="plugin-ai-dev__label">开发需求</label><a-textarea v-model:value="requirement" :rows="6" /><a-button
class="plugin-ai-dev__generate"
type="primary"
:loading="creating"
@@ -120,6 +130,7 @@ import * as api from "../api";
import * as pipelineApi from "/@/views/certd/pipeline/api";
import * as historyApi from "/@/views/certd/pipeline/api.history";
import DependPluginsInput from "./depend-plugins-input.vue";
import PluginAuthorField from "./plugin-author-field.vue";
import PluginEditDialogBody from "./plugin-edit-dialog-body.vue";
import PipelineEditDialog from "/@/views/certd/pipeline/components/pipeline-edit-dialog.vue";
import { useFormDialog } from "/@/use/use-dialog";
@@ -135,6 +146,7 @@ const pluginTypeOptions = [
{ value: "notification", label: "通知插件", desc: "通过消息、邮件等方式发送通知" },
// { value: "addon", label: "Addon", desc: "扩展流水线的通用辅助能力" },
];
const author = ref("");
const requirement = ref(
"例如:开发一个部署证书到阿里云DCDN的插件\nAPI接口请参考:https://help.aliyun.com/zh/edge-security-acceleration/dcdn/developer-reference/api-dcdn-2018-01-15-overview。\n要求: \n1、能够引用上传到阿里云CAS步骤输出的证书。\n2、支持在UI上选择账户下某区域下的DCDN的域名列表。\n3、支持一次性选择多个域名进行部署"
);
@@ -183,7 +195,33 @@ async function createPrompt() {
try {
const token = await api.GetScopedAccessToken(["sys/ai"]);
accessToken.value = token.token;
prompt.value = `你是 Certd 在线插件开发 Agent。\n\n开发模式:${mode.value === "new" ? "开发新插件" : "修改已有插件"}\n插件类型:${pluginType.value || "按已有插件类型"}\n插件 ID:${pluginPath.value.at(-1) || "无"}\n用户需求:\n${requirement.value.trim()}\n\n如果用户需求描述与他选择的插件类型有冲突,你需要跟用户确认是否选错插件类型。 \nCertd 地址:${window.location.origin}\n受限 AccessToken(6小时有效):${token.token}\n\n请先读取 .trae/skills/certd-online-plugin-dev/SKILL.md,按插件类型开发并提交版本。仅调用 /scoped/sys/ai/plugin/ 前缀接口,完成后报告提交结果,不自动发布。\n如果当前工作目录不是 Certd 项目,或缺少 certd-online-plugin-dev Skill,先拉取 Certd 仓库代码并切换到仓库内工作(git clone https://atomgit.com/certd/certd --depth 1 )`;
const authorName = author.value.trim();
let authorText = authorName;
if (!authorText && mode.value === "edit") {
// 修改已有插件时作者保持原样,不能凭空补一个作者出来
authorText = "无(保持原插件的 author 字段不变,不要新增作者)";
}
if (!authorText) {
authorText = "未指定(请先向用户确认作者名称,不要自行编造作者名)";
}
prompt.value = `
你是 Certd 在线插件开发 Agent。
开发模式:${mode.value === "new" ? "开发新插件" : "修改已有插件"}
插件类型:${pluginType.value || "按已有插件类型"}
插件 ID:${pluginPath.value.at(-1) || "无"}
插件作者:${authorText}
用户需求:
${requirement.value.trim()}
如果用户需求描述与他选择的插件类型有冲突,你需要跟用户确认是否选错插件类型。
Certd 地址:${window.location.origin}
受限 AccessToken(6小时有效):${token.token}
请先读取 .trae/skills/certd-online-plugin-dev/SKILL.md,按插件类型开发并提交版本。仅调用 /scoped/sys/ai/plugin/ 前缀接口,完成后报告提交结果,不自动发布。
如果当前工作目录不是 Certd 项目,或缺少 certd-online-plugin-dev Skill,先拉取 Certd 仓库代码并切换到仓库内工作(git clone https://atomgit.com/certd/certd --depth 1 )
本次开发的在线插件无需写单元测试。
`;
notification.success({ message: "启动提示词已生成" });
} finally {
creating.value = false;
@@ -259,24 +297,31 @@ async function loadRecentPlugins(force = false) {
async function loadSelectedPlugin() {
const selectedValue = pluginPath.value.at(-1);
if (!selectedValue) {
recentPlugins.value = [];
setSelectedPlugin(null);
return;
}
if (props.pluginId && selectedValue === String(props.pluginId)) {
const selected = await api.GetObj(props.pluginId);
recentPlugins.value = selected ? [selected] : [];
setSelectedPlugin(selected);
return;
}
let selected = await api.GetObj(selectedValue);
if (selected) {
recentPlugins.value = [selected];
return;
if (!selected) {
const fullName = selectedValue.includes(":") ? selectedValue.slice(selectedValue.indexOf(":") + 1) : selectedValue;
const result: any = await api.FindPlugins({ includeBuiltIn: true, includeStore: true, keyword: fullName });
const records: any[] = Array.isArray(result) ? result : result?.records || [];
selected = records.find(item => String(item.fullName || item.name) === fullName) || records[0];
}
setSelectedPlugin(selected);
}
function setSelectedPlugin(plugin: any) {
recentPlugins.value = plugin ? [plugin] : [];
const pluginAuthor = String(plugin?.author || "").trim();
if (pluginAuthor) {
// 修改已有插件时,作者默认沿用该插件当前作者,避免插件身份被改错
author.value = pluginAuthor;
}
const fullName = selectedValue.includes(":") ? selectedValue.slice(selectedValue.indexOf(":") + 1) : selectedValue;
const result: any = await api.FindPlugins({ includeBuiltIn: true, includeStore: true, keyword: fullName });
const records: any[] = Array.isArray(result) ? result : result?.records || [];
selected = records.find(item => String(item.fullName || item.name) === fullName) || records[0];
recentPlugins.value = selected ? [selected] : [];
}
function formatTime(value: any) {
@@ -361,9 +406,9 @@ watch(mode, value => {
<style lang="less">
.plugin-ai-dev {
display: flex;
height: 60vh;
height: 65vh;
flex-direction: column;
gap: 20px;
gap: 10px;
&__steps {
flex: none;
@@ -373,7 +418,7 @@ watch(mode, value => {
display: flex;
flex: 1;
flex-direction: column;
gap: 18px;
gap: 10px;
padding: 8px 20px;
overflow-y: auto;
}
@@ -1,6 +1,6 @@
<template>
<div class="plugin-author-field">
<a-input :value="modelValue" placeholder="请输入作者名称" :disabled="authorRegistered" @update:value="emit('update:modelValue', $event)" />
<a-input :value="modelValue" placeholder="请输入作者名称" :disabled="inputDisabled" @update:value="emit('update:modelValue', $event)" />
<a-button type="link" size="small" :loading="loading" @click="handleAuthorAction">
{{ authorRegistered ? "修改邮箱" : "注册作者" }}
</a-button>
@@ -14,12 +14,22 @@ import { usePluginPublish } from "../use-publish";
defineOptions({ name: "PluginAuthorField" });
const props = defineProps<{ modelValue?: string }>();
const props = withDefaults(
defineProps<{
modelValue?: string;
// 允许手动输入作者名称,默认使用后端已注册作者且不可修改
editable?: boolean;
}>(),
{
editable: false,
}
);
const emit = defineEmits<{ (event: "update:modelValue", value: string): void }>();
const loading = ref(false);
const { registerPluginAuthor, updatePluginAuthorEmail } = usePluginPublish();
const registeredAuthor = ref<api.OnlinePluginAuthorBean>();
const authorRegistered = computed(() => !!registeredAuthor.value?.id);
const inputDisabled = computed(() => authorRegistered.value && !props.editable);
const isLocalAuthor = computed(() => {
return (
String(props.modelValue || "")
@@ -68,7 +68,7 @@ const labelCol = ref({
},
});
const wrapperCol = ref({ span: 16 });
const configForm: any = reactive({ type: props.plugin.fullName || props.plugin.name });
const configForm: any = reactive({});
const showDict = dict({
data: [
@@ -306,7 +306,6 @@ provide("get:scope", () => {
return getScope();
});
provide("get:plugin:type", () => {
debugger;
return props.plugin.pluginType;
});
</script>
@@ -14,10 +14,13 @@ import { usePluginStore } from "/@/store/plugin";
import PluginAuthorField from "./components/plugin-author-field.vue";
import { usePluginAiDev } from "./use-ai-dev";
import { useOnlineInstall } from "./use-online-install";
import dayjs from "dayjs";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const { t } = useI18n();
const lastSyncTime = context.lastSyncTime;
const syncLoading = context.syncLoading;
const syncOnlinePlugins = context.syncOnlinePlugins;
let lastType = "";
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
if (lastType && lastType != query?.query?.type) {
@@ -65,10 +68,8 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const settingStore = useSettingStore();
const pluginStore = usePluginStore();
const syncLoading = ref(false);
const upgradeAllLoading = ref(false);
const lastSyncTime = ref(0);
const autoSyncInterval = 30 * 24 * 60 * 60 * 1000;
const syncButtonTitle = computed(() => {
if (!lastSyncTime.value) {
return t("certd.onlinePluginNotSynced");
@@ -79,39 +80,13 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
});
function formatSyncTime(time: number) {
return new Date(time).toLocaleString();
}
function needAutoSync(time: number) {
if (!time) {
return true;
}
return Date.now() - time > autoSyncInterval;
return dayjs(time).format("YYYY-MM-DD HH:mm:ss");
}
function isEditablePlugin(row: any) {
return row.editable === true;
}
async function syncOnlinePlugins(options?: { showSuccess?: boolean }) {
if (syncLoading.value) {
return;
}
syncLoading.value = true;
try {
await api.OnlinePluginSync();
const setting = await api.OnlinePluginSetting();
lastSyncTime.value = setting.lastSyncTime || Date.now();
await pluginStore.reload();
crudExpose.doRefresh();
if (options?.showSuccess !== false) {
notification.success({ message: t("certd.onlinePluginSyncSuccess") });
}
} finally {
syncLoading.value = false;
}
}
async function upgradeAllOnlinePlugins() {
if (upgradeAllLoading.value) {
return;
@@ -157,18 +132,6 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
upgradeAllLoading.value = false;
}
}
async function loadOnlinePluginSetting() {
const setting = await api.OnlinePluginSetting();
lastSyncTime.value = setting.lastSyncTime || 0;
if (needAutoSync(lastSyncTime.value)) {
await syncOnlinePlugins({ showSuccess: false });
}
}
loadOnlinePluginSetting().catch(e => {
console.warn("load online plugin setting failed", e);
});
return {
crudOptions: {
settings: {
@@ -22,18 +22,42 @@ import PluginItemCard from "./components/plugin-item-card.vue";
import OnlinePluginDetail from "./components/online-plugin-detail.vue";
import { useI18n } from "/src/locales";
import { useMounted } from "/@/use/use-mounted";
import { computed, onBeforeUnmount, onMounted } from "vue";
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
import { usePluginStore } from "/@/store/plugin";
import { useFormDialog } from "/@/use/use-dialog";
import { notification } from "ant-design-vue";
import * as api from "./api";
const { t } = useI18n();
const pluginStore = usePluginStore();
const { openFormDialog } = useFormDialog();
const lastSyncTime = ref<number>(0);
const syncLoading = ref(false);
const autoSyncInterval = 3 * 24 * 60 * 60 * 1000;
defineOptions({
name: "SysPlugin",
});
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
async function syncOnlinePlugins(options?: { showSuccess?: boolean }) {
if (syncLoading.value) {
return;
}
syncLoading.value = true;
try {
await api.OnlinePluginSync();
const setting = await api.OnlinePluginSetting();
lastSyncTime.value = setting.lastSyncTime || Date.now();
await pluginStore.clear();
crudExpose.doRefresh();
if (options?.showSuccess !== false) {
notification.success({ message: t("certd.onlinePluginSyncSuccess") });
}
} finally {
syncLoading.value = false;
}
}
const context = { lastSyncTime, syncLoading, syncOnlinePlugins };
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context });
const pluginList = computed(() => {
return crudBinding.value?.data || [];
});
@@ -77,9 +101,25 @@ async function handlePluginChanged(payload: { action: string }) {
crudExpose.doRefresh();
}
function needAutoSync(time: number) {
if (!time) {
return true;
}
return Date.now() - time > autoSyncInterval;
}
async function autoSyncOnlinePlugins() {
const setting = await api.OnlinePluginSetting();
lastSyncTime.value = setting.lastSyncTime || 0;
if (needAutoSync(lastSyncTime.value)) {
await syncOnlinePlugins({ showSuccess: false });
}
}
// 页面打开后获取列表数据
useMounted(async () => {
await crudExpose.doRefresh();
autoSyncOnlinePlugins();
crudExpose.doRefresh();
});
onMounted(() => {
@@ -3,7 +3,7 @@
<template #header>
<div class="title">
套餐设置
<span class="sub"> 需要<router-link :to="{ path: '/sys/settings', query: { tab: 'payment' } }">开启至少一种支付方式</router-link></span>
<span class="sub"> 建议<router-link :to="{ path: '/sys/settings', query: { tab: 'payment' } }">开启至少一种支付方式</router-link></span>
</div>
</template>
@@ -38,7 +38,7 @@
<a-form-item label=" " :colon="false">
<loading-button type="primary" html-type="button" :click="onClick">保存</loading-button>
<div class="helper">需要 <router-link :to="{ path: '/sys/settings', query: { tab: 'payment' } }">开启至少一种支付方式</router-link></div>
<div class="helper">建议 <router-link :to="{ path: '/sys/settings', query: { tab: 'payment' } }">开启至少一种支付方式</router-link></div>
</a-form-item>
</a-form>
</div>
@@ -0,0 +1,35 @@
import { request } from "/src/api/service";
const apiPrefix = "/sys/wallet";
export async function GetList(query: any) {
return await request({
url: apiPrefix + "/page",
method: "post",
data: query,
});
}
export async function Recharge(data: { userId: number; amount: number; remark?: string }) {
return await request({
url: apiPrefix + "/recharge",
method: "post",
data,
});
}
export async function GetWalletLogs(query: any) {
return await request({
url: apiPrefix + "/log/page",
method: "post",
data: query,
});
}
export async function GetSimpleUserByIds(ids: number[]) {
return await request({
url: "/sys/authority/user/getSimpleUserByIds",
method: "post",
data: { ids },
});
}
@@ -0,0 +1,193 @@
import { CreateCrudOptionsProps, CreateCrudOptionsRet, dict, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { notification } from "ant-design-vue";
import { useRouter } from "vue-router";
import { util } from "/@/utils";
import { useFormDialog } from "/@/use/use-dialog";
import createCrudOptionsUser from "/@/views/sys/authority/user/crud";
import PriceInput from "/@/views/sys/suite/product/price-input.vue";
import * as api from "./api";
export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const { openFormDialog } = useFormDialog();
const router = useRouter();
const userDict = dict({
async getNodesByValues(ids: number[]) {
return await api.GetSimpleUserByIds(ids);
},
value: "id",
label: "nickName",
});
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
function moneyColumn(title: string) {
return {
title,
type: "number",
column: {
width: 120,
component: {
name: PriceInput,
vModel: "modelValue",
edit: false,
},
},
};
}
async function openRecharge(userId?: number) {
await openFormDialog({
title: "用户余额充值",
wrapper: { width: 520 },
initialForm: {
userId: userId ?? null,
amountYuan: null,
remark: "",
},
columns: {
userId: {
title: "用户",
type: "table-select",
dict: userDict,
form: {
col: { span: 24 },
rules: [{ required: true, message: "请选择用户" }],
component: {
disabled: userId != null,
crossPage: true,
multiple: false,
select: {
placeholder: "请选择用户",
},
createCrudOptions: createCrudOptionsUser,
},
},
},
amountYuan: {
title: "充值金额",
type: "number",
form: {
col: { span: 24 },
rules: [{ required: true, message: "请输入充值金额" }],
component: {
name: "a-input-number",
vModel: "value",
min: 0,
precision: 2,
addonAfter: "元",
style: { width: "100%" },
},
},
},
remark: {
title: "备注",
type: "text",
form: {
col: { span: 24 },
helper: "选填",
},
},
},
async onSubmit(form: any) {
await api.Recharge({
userId: form.userId,
amount: util.amount.toCent(form.amountYuan || 0),
remark: form.remark,
});
await crudExpose.doRefresh();
notification.success({ message: "充值成功" });
},
});
}
return {
crudOptions: {
request: {
pageRequest,
},
toolbar: { show: false },
actionbar: {
buttons: {
add: { show: false },
recharge: {
text: "余额充值",
type: "primary",
click: () => openRecharge(),
},
log: {
text: "余额变更记录",
type: "primary",
click: () => router.push("/sys/suite/wallet-log"),
},
},
},
rowHandle: {
width: 220,
fixed: "right",
buttons: {
view: { show: false },
edit: { show: false },
copy: { show: false },
remove: { show: false },
walletLog: {
text: "余额变更记录",
type: "link",
click: ({ row }) => router.push({ path: "/sys/suite/wallet-log", query: { userId: row.userId } }),
},
recharge: {
text: "充值",
type: "link",
click: ({ row }) => openRecharge(row.userId),
},
},
},
columns: {
id: {
title: "ID",
type: "number",
column: { width: 80 },
form: { show: false },
},
userId: {
title: "用户",
type: "table-select",
dict: userDict,
search: { show: true, col: { span: 3 } },
column: { width: 160 },
form: {
show: false,
component: {
crossPage: true,
multiple: false,
select: {
placeholder: "点击选择用户",
},
createCrudOptions: createCrudOptionsUser,
},
},
},
availableAmount: moneyColumn("可用余额"),
totalAmount: moneyColumn("总余额"),
frozenAmount: moneyColumn("冻结余额"),
totalIncomeAmount: moneyColumn("累计收入"),
totalConsumedAmount: moneyColumn("累计消费"),
totalWithdrawAmount: moneyColumn("累计提现"),
createTime: {
title: "创建时间",
type: "datetime",
form: { show: false },
column: { sorter: true, width: 160 },
},
updateTime: {
title: "更新时间",
type: "datetime",
form: { show: false },
column: { width: 160 },
},
},
},
};
}
@@ -0,0 +1,28 @@
<template>
<fs-page>
<template #header>
<div class="title">
余额管理
<span class="sub">管理用户余额,可给用户充值</span>
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
</fs-page>
</template>
<script lang="ts" setup>
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { useMounted } from "/@/use/use-mounted";
defineOptions({
name: "SysWallet",
});
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
// 页面打开后获取列表数据
useMounted(async () => {
await crudExpose.doRefresh();
});
</script>
@@ -0,0 +1,100 @@
import { CreateCrudOptionsRet, dict, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { useRoute } from "vue-router";
import { util } from "/@/utils";
import createCrudOptionsUser from "/@/views/sys/authority/user/crud";
import * as api from "../api";
function moneyText(amount: number) {
const yuan = util.amount.toYuan(Math.abs(amount || 0));
if (amount < 0) {
return `-¥${yuan}`;
}
return `¥${yuan}`;
}
export default function (): CreateCrudOptionsRet {
const route = useRoute();
const userIdFromQuery = route.query.userId ? Number(route.query.userId) : undefined;
const userDict = dict({
async getNodesByValues(ids: number[]) {
return await api.GetSimpleUserByIds(ids);
},
value: "id",
label: "nickName",
});
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetWalletLogs(query);
};
return {
crudOptions: {
request: { pageRequest },
search: {
initialForm: { userId: userIdFromQuery },
},
actionbar: { show: false },
toolbar: { show: false },
rowHandle: { show: false },
columns: {
createTime: { title: "时间", type: "datetime", column: { width: 180 } },
userId: {
title: "用户",
type: "table-select",
dict: userDict,
search: { show: true, col: { span: 3 } },
column: { width: 160 },
form: {
show: false,
component: {
crossPage: true,
multiple: false,
select: { placeholder: "点击选择用户" },
createCrudOptions: createCrudOptionsUser,
},
},
},
type: {
title: "类型",
type: "dict-select",
dict: dict({
data: [
{ label: "收益入账", value: "income", color: "success" },
{ label: "余额抵扣", value: "consume", color: "default" },
{ label: "提现扣减", value: "withdraw", color: "warning" },
],
}),
search: { show: true, col: { span: 3 } },
column: { width: 120 },
},
amount: {
title: "变动金额",
type: "number",
column: {
width: 120,
cellRender({ value }) {
const amount = Number(value || 0);
return <span class={amount < 0 ? "text-green-500" : "text-red-500"}>{moneyText(amount)}</span>;
},
},
},
balanceAfter: {
title: "变动后余额",
type: "number",
column: {
width: 130,
cellRender({ value }) {
return <span class="text-red-500">{moneyText(Number(value || 0))}</span>;
},
},
},
remark: {
title: "备注",
type: "text",
column: { minWidth: 220 },
},
},
},
};
}
@@ -0,0 +1,45 @@
<template>
<fs-page>
<template #header>
<div class="title">
{{ pageTitle }}
<span class="sub">{{ pageSub }}</span>
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
</fs-page>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import { useRoute } from "vue-router";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { useMounted } from "/@/use/use-mounted";
defineOptions({
name: "SysWalletLog",
});
const route = useRoute();
const isUserLog = computed(() => route.query.userId != null);
const pageTitle = computed(() => {
if (isUserLog.value) {
return "用户余额变更记录";
}
return "余额变更记录";
});
const pageSub = computed(() => {
if (isUserLog.value) {
return "查看该用户的余额变更明细";
}
return "查看所有用户的余额变更明细";
});
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
// 页面打开后获取列表数据
useMounted(async () => {
await crudExpose.doRefresh();
});
</script>
+1
View File
@@ -1 +1,2 @@
certd_plugin_loadmode=metadata
# certd_release_mode=dev
+29
View File
@@ -3,6 +3,35 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.45.0](https://github.com/certd/certd/compare/v1.44.4...v1.45.0) (2026-10-02)
### Bug Fixes
* [BUG]使用天翼云CDN部署证书时无法匹配域名 ([bb1f232](https://github.com/certd/certd/commit/bb1f232f415c522851dede979f5d609dda844fd3))
* 支持google persist校验方式 ([97d2b50](https://github.com/certd/certd/commit/97d2b5007409ec79ea864d437c473a2f9b5dfae4))
* **config:** 修复环境变量加载逻辑并更新配置文件 ([07f0d3b](https://github.com/certd/certd/commit/07f0d3b4d626628a0e3dfc7e5c52e8c4a98bad47))
* **ctyun-cdn:** 修复天翼云CDN部署插件的域名选择问题 ([ea7d14c](https://github.com/certd/certd/commit/ea7d14c289b8720781bf101bfef59eac7bb03765))
* **webhook:** 修复模版转义换行符,避免手动上传的证书报错 ([2735feb](https://github.com/certd/certd/commit/2735feb3302f8308708dafc8b295adcd4b91d7f0))
### Features
* **client:** 新增客户端管理功能 ([3dbbe72](https://github.com/certd/certd/commit/3dbbe72fc15c114ca6b5e8ff45cb21f258b80650))
### Performance Improvements
* 宝塔站点列表支持搜索和排序 ([4420472](https://github.com/certd/certd/commit/4420472f8fac272d30fb8ccb1cc2ee9763989001))
* 网宿插件双鉴权支持 ([241b2e5](https://github.com/certd/certd/commit/241b2e55848471e61b5d2be0c87a7dc9a0d29557))
* **monitor:** 站点证书监控支持手动选中触发检查 ([f60e63f](https://github.com/certd/certd/commit/f60e63fbaf00460d5050cd7705bd75d57dec202d))
* **open-api:** openkey增加用途备注 ([bbeaf61](https://github.com/certd/certd/commit/bbeaf617d19750515f6eca51863e74e23bc1de50))
* **tencent clb:** 修改所有CLB地域的endpoint为就近接入 ([c427735](https://github.com/certd/certd/commit/c4277350bcc8cf034cf4e76db2a6daa1842bc5d7))
## [1.44.4](https://github.com/certd/certd/compare/v1.44.3...v1.44.4) (2026-09-12)
### Performance Improvements
* 新增余额管理,管理员充值功能 ([bded368](https://github.com/certd/certd/commit/bded368cbb8974b6937ae260e448c25fae5c33ad))
* **acme-account-access:** AcmeAccount授权允许修改eab,重新生成account ([6a649aa](https://github.com/certd/certd/commit/6a649aaca083ccd3b517f7d676a58220aa33b37d))
## [1.44.3](https://github.com/certd/certd/compare/v1.44.2...v1.44.3) (2026-09-06)
### Bug Fixes
@@ -0,0 +1,24 @@
-- 客户端登记表:certd-client 心跳上报后记录在线状态;客户端自身信息保留独立列,站点统计等快照存 content JSON,后续扩展直接往 JSON 加字段。
CREATE TABLE `cd_client`
(
`id` bigint PRIMARY KEY AUTO_INCREMENT NOT NULL,
`user_id` bigint NOT NULL,
`project_id` bigint,
`client_id` varchar(64) NOT NULL,
`key_id` varchar(64),
`machine_name` varchar(255),
`version` varchar(64),
`os` varchar(64),
`content` longtext,
`last_heartbeat_at` bigint NOT NULL,
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE = InnoDB ROW_FORMAT = DYNAMIC;
CREATE INDEX `index_client_user_id` ON `cd_client` (`user_id`);
CREATE INDEX `index_client_project_id` ON `cd_client` (`project_id`);
CREATE UNIQUE INDEX `index_client_user_client` ON `cd_client` (`user_id`, `client_id`);
-- 开放接口密钥用途备注
ALTER TABLE cd_open_key ADD COLUMN `remark` varchar(512);
@@ -0,0 +1,24 @@
-- 客户端登记表:certd-client 心跳上报后记录在线状态;客户端自身信息保留独立列,站点统计等快照存 content JSON,后续扩展直接往 JSON 加字段。
CREATE TABLE "cd_client"
(
"id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY NOT NULL,
"user_id" bigint NOT NULL,
"project_id" bigint,
"client_id" varchar(64) NOT NULL,
"key_id" varchar(64),
"machine_name" varchar(255),
"version" varchar(64),
"os" varchar(64),
"content" text,
"last_heartbeat_at" bigint NOT NULL,
"create_time" timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP),
"update_time" timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP)
);
CREATE INDEX "index_client_user_id" ON "cd_client" ("user_id");
CREATE INDEX "index_client_project_id" ON "cd_client" ("project_id");
CREATE UNIQUE INDEX "index_client_user_client" ON "cd_client" ("user_id", "client_id");
-- 开放接口密钥用途备注
ALTER TABLE cd_open_key ADD COLUMN "remark" varchar(512);
@@ -0,0 +1,24 @@
-- 客户端登记表:certd-client 心跳上报后记录在线状态;客户端自身信息保留独立列,站点统计等快照存 content JSON,后续扩展直接往 JSON 加字段。
CREATE TABLE "cd_client"
(
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
"user_id" integer NOT NULL,
"project_id" integer,
"client_id" varchar(64) NOT NULL,
"key_id" varchar(64),
"machine_name" varchar(255),
"version" varchar(64),
"os" varchar(64),
"content" text,
"last_heartbeat_at" integer NOT NULL,
"create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
"update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)
);
CREATE INDEX "index_client_user_id" ON "cd_client" ("user_id");
CREATE INDEX "index_client_project_id" ON "cd_client" ("project_id");
CREATE UNIQUE INDEX "index_client_user_client" ON "cd_client" ("user_id", "client_id");
-- 开放接口密钥用途备注
ALTER TABLE cd_open_key ADD COLUMN "remark" varchar(512);
+13 -3
View File
@@ -76,15 +76,20 @@ async function genMetadata() {
fs.rmSync("./metadata", { recursive: true });
fs.mkdirSync("./metadata", { recursive: true });
const warnings = []
for (const key in modules) {
const module = modules[key];
const entry = Object.entries(module);
if (entry.length > 1) {
console.log(`[warning] 文件 ${key} 导出了 ${entry.length} 个对象: ${entry.map(([name, value]) => name).join(", ")}`);
}
// if (entry.length > 1) {
// warnings.push(`[warning] 文件 ${key} 导出了 ${entry.length} 个对象: ${entry.map(([name, value]) => name).join(", ")}`);
// }
const isManyExport = entry.length > 1;
for (const [name, value] of entry) {
//如果有define属性
if (value.define) {
if (isManyExport) {
throw new Error(`插件文件禁止导出多个对象, ${key} 导出了 ${entry.length} 个对象: ${entry.map(([name, value]) => name).join(", ")}`);
}
//那么就是插件
let location = key.substring(4);
location = location.substring(0, location.length - 3);
@@ -121,6 +126,11 @@ async function genMetadata() {
}
}
}
if (warnings.length > 0) {
for (const warning of warnings) {
console.log(warning);
}
}
}
async function genPluginMd() {
@@ -43,7 +43,7 @@ input:
return {
component: {
disabled: ctx.compute(({form})=> !!form.access?.account)
disabled: ctx.compute(({form})=> !!form.access?.account && !form.access?.editAccount)
}
}
@@ -85,7 +85,7 @@ input:
return form.access?._needEAB === true;
}),
component: {
disabled: ctx.compute(({form})=> !!form.access?.account)
disabled: ctx.compute(({form})=> !!form.access?.account && !form.access?.editAccount)
}
}
@@ -103,7 +103,7 @@ input:
return form.access?._needEAB === true;
}),
component: {
disabled: ctx.compute(({form})=> !!form.access?.account)
disabled: ctx.compute(({form})=> !!form.access?.account && !form.access?.editAccount)
}
}
@@ -9,7 +9,9 @@ input:
placeholder: api token,用户 API 令牌
helper: |-
前往 [获取API令牌](https://dash.cloudflare.com/profile/api-tokens),注意是令牌,不是密钥。
token权限必须包含:[Zone区域-Zone区域-Edit编辑], [Zone区域-DNS-Edit编辑]
token权限必须包含两条:
[Zone区域-Zone区域-Edit编辑]
[Zone区域-DNS-Edit编辑]
required: true
encrypt: true
proxy:
@@ -3,25 +3,87 @@ title: 网宿授权
desc: ''
icon: svg:icon-wangsu
input:
authType:
title: 鉴权方式
value: aksk
component:
name: a-select
vModel: value
allowClear: false
options:
- label: AccessKey签名
value: aksk
- label: ApiKey(CDN Pro必须用这个)
value: apikey
helper: 网宿接口鉴权方式
required: false
accessKeyId:
title: accessKeyId
component:
name: a-input
vModel: value
placeholder: accessKeyId
component:
name: a-input
vModel: value
helper: '[点击前往获取AccessKey](https://console.wangsu.com/account/accessKey?rsr=ws)'
encrypt: false
required: true
mergeScript: |2-
return {
show: ctx.compute(({form})=>{
return !form.access.authType || form.access.authType === 'aksk';
})
}
accessKeySecret:
title: accessKeySecret
component:
name: a-input
vModel: value
placeholder: accessKeySecret
component:
name: a-input
vModel: value
encrypt: true
required: true
mergeScript: |2-
return {
show: ctx.compute(({form})=>{
return !form.access.authType || form.access.authType === 'aksk';
})
}
apiUser:
title: API账号名
component:
name: a-input
vModel: value
placeholder: API账号名
helper: 控制台里的 API 账号名,与 API Key 配套使用
encrypt: false
required: false
mergeScript: |2-
return {
show: ctx.compute(({form})=>{
return form.access.authType === 'apikey';
})
}
apiKey:
title: API Key
component:
name: a-input
vModel: value
placeholder: API Key
helper: 控制台里的 API Key,[点击前往获取](https://console.wangsu.com/account/apiManage)
encrypt: true
required: false
mergeScript: |2-
return {
show: ctx.compute(({form})=>{
return form.access.authType === 'apikey';
})
}
testRequest:
title: 测试
component:
@@ -59,6 +59,7 @@ input:
vModel: value
mode: tags
action: GetSiteList
search: true
watches:
- certDomains
- accessId
@@ -80,17 +80,17 @@ input:
helper: >
1. <b>DNS直接验证</b>:当域名dns解析已被本系统支持时(即下方DNS解析服务商选项中可选),推荐选择此方式
2.
2. <b>DNS持久验证</b>:需要先配置ACME账号和_validation-persist持久TXT记录,续期时不再增删DNS记录;当前仅
Google证书 可以申请
3.
<b>CNAME代理验证</b>:支持任何注册商的域名,第一次需要手动添加[CNAME记录](#/certd/cname/record)(如果经常申请失败,建议将DNS服务器修改为阿里云/腾讯云的,然后使用DNS直接验证)
3. <b>HTTP文件验证</b>:不支持泛域名,需要配置网站文件上传(IP证书必须选它)
4. <b>HTTP文件验证</b>:不支持泛域名,需要配置网站文件上传(IP证书必须选它)
4. <b>多DNS提供商</b>:每个域名可以选择独立的DNS提供商
5. <b>多DNS提供商</b>:每个域名可以选择独立的DNS提供商或校验方式
5. <b>自动匹配</b>:此处无需选择校验方式,需要在[域名管理](#/certd/cert/domain)中提前配置好校验方式
6. <b>DNS持久验证</b>:需要先配置ACME账号和_validation-persist持久TXT记录,续期时不再增删DNS记录;当前仅
Let's Encrypt 测试环境 可以申请
6. <b>自动匹配</b>:此处无需选择校验方式,需要在[域名管理](#/certd/cert/domain)中提前配置好校验方式
order: 0
dnsProviderType:
title: DNS解析服务商
@@ -80,6 +80,7 @@ input:
watches:
- certDomains
- accessId
- productCode
required: true
mergeScript: |2-
+14 -14
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/ui-server",
"version": "1.44.3",
"version": "1.45.0",
"description": "fast-server base midway",
"private": true,
"type": "module",
@@ -43,20 +43,20 @@
"lint1": "eslint --fix --ext .ts src"
},
"dependencies": {
"@certd/acme-client": "^1.44.3",
"@certd/basic": "^1.44.3",
"@certd/commercial-core": "^1.44.3",
"@certd/acme-client": "^1.45.0",
"@certd/basic": "^1.45.0",
"@certd/commercial-core": "^1.45.0",
"@certd/cv4pve-api-javascript": "^8.4.2",
"@certd/jdcloud": "^1.44.3",
"@certd/lib-huawei": "^1.44.3",
"@certd/lib-k8s": "^1.44.3",
"@certd/lib-server": "^1.44.3",
"@certd/midway-flyway-js": "^1.44.3",
"@certd/pipeline": "^1.44.3",
"@certd/plugin-cert": "^1.44.3",
"@certd/plugin-lib": "^1.44.3",
"@certd/plugin-plus": "^1.44.3",
"@certd/plus-core": "^1.44.3",
"@certd/jdcloud": "^1.45.0",
"@certd/lib-huawei": "^1.45.0",
"@certd/lib-k8s": "^1.45.0",
"@certd/lib-server": "^1.45.0",
"@certd/midway-flyway-js": "^1.45.0",
"@certd/pipeline": "^1.45.0",
"@certd/plugin-cert": "^1.45.0",
"@certd/plugin-lib": "^1.45.0",
"@certd/plugin-plus": "^1.45.0",
"@certd/plus-core": "^1.45.0",
"@koa/cors": "^5.0.0",
"@midwayjs/bootstrap": "3.20.11",
"@midwayjs/cache": "3.14.0",
@@ -249,6 +249,7 @@ const development = {
],
},
} as MidwayConfig;
loadDotEnv(`.env.${env}`);
loadDotEnv();
mergeConfig(development, "development");
+10 -2
View File
@@ -47,10 +47,18 @@ export function mergeConfig(config: any, envType: string) {
return config;
}
export function loadDotEnv() {
const envStr = fs.readFileSync(".env").toString();
export function loadDotEnv(envName = ".env") {
if (!fs.existsSync(envName)) {
return;
}
logger.info("load env file:", envName);
const envStr = fs.readFileSync(envName).toString();
envStr.split("\n").forEach(line => {
const [key, value] = line.trim().split("=");
if (key.trim().startsWith("#") || key.trim() === "") {
return;
}
logger.info("load env var:", key, value);
const oldValue = process.env[key];
if (!oldValue) {
process.env[key] = value;
@@ -45,6 +45,7 @@ process.on("uncaughtException", error => {
// setInterval(log, 200);
// log()
// }
// startHeapLog();
@Configuration({
@@ -115,11 +116,10 @@ export class MainConfiguration {
//授权处理
AuthorityMiddleware,
AuditLogMiddleware,
//resetPasswd,重置密码模式下不提供服务
ResetPasswdMiddleware,
]);
this.app.getMiddleware().insertFirst(async (ctx: IMidwayKoaContext, next: NextFunction) => {
await next();
const path = ctx.path;

Some files were not shown because too many files have changed in this diff Show More