mirror of
https://github.com/certd/certd.git
synced 2026-10-13 15:49:25 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67f3daae62 | ||
|
|
c72fa68e4c | ||
|
|
1f5cda452c | ||
|
|
5a19d2513d | ||
|
|
c0f8145615 | ||
|
|
2ad14b82fa | ||
|
|
c8bdcdc778 | ||
|
|
15d2bd8484 | ||
|
|
7d4311115c | ||
|
|
3efa0582ef | ||
|
|
7624babc99 | ||
|
|
6cf8f350f5 | ||
|
|
0e08555220 | ||
|
|
fe738fa7d4 | ||
|
|
2439f4ab02 | ||
|
|
75b2b146ce | ||
|
|
1f3c096517 | ||
|
|
d264388c00 | ||
|
|
b8eaeeaa64 | ||
|
|
dba1a486cb | ||
|
|
5408ae6ad8 | ||
|
|
f5430a8925 | ||
|
|
9cc21204b1 | ||
|
|
f911e06a69 | ||
|
|
4a6eab0ce6 | ||
|
|
1d90fadd0c | ||
|
|
1b1a5ec94d | ||
|
|
d67379f297 | ||
|
|
249b98df66 | ||
|
|
aebbb5cc7d | ||
|
|
4f020aa8a5 | ||
|
|
02426c507a | ||
|
|
ffb7ae79eb | ||
|
|
f58f2f3cd6 | ||
|
|
794abc3d7a | ||
|
|
da406014ad | ||
|
|
9842a24a50 | ||
|
|
fb21ec04a6 | ||
|
|
15b74207dd | ||
|
|
6055e9b6ba | ||
|
|
df3e0814c4 | ||
|
|
81f660dc49 | ||
|
|
f56e2a07c6 | ||
|
|
a2fce6cf0b | ||
|
|
ccb5356bfa | ||
|
|
f44084dfd2 | ||
|
|
948d76f98f | ||
|
|
6606f32678 | ||
|
|
6af12d49fc | ||
|
|
9decb49d04 | ||
|
|
69466c1c41 | ||
|
|
adf5163a79 | ||
|
|
b40bc1b487 | ||
|
|
9d30ccf1f0 | ||
|
|
396810eddf | ||
|
|
5dbf6694b1 | ||
|
|
c2df8d3fa3 | ||
|
|
c149662d54 | ||
|
|
ae1c5e38d1 | ||
|
|
718b46141e | ||
|
|
9c8986be10 | ||
|
|
1d12ad73c6 | ||
|
|
30d4fc84a9 | ||
|
|
834e7750ce | ||
|
|
f38522ca0b | ||
|
|
d87821ee02 | ||
|
|
bd9e226c36 | ||
|
|
2809267d7a | ||
|
|
4de24da9d5 | ||
|
|
580981a468 | ||
|
|
5bff2c8fa5 | ||
|
|
338de1f04c | ||
|
|
5e867c86c3 | ||
|
|
07efe717d0 | ||
|
|
2b4c066a2e | ||
|
|
c03e82e4e7 | ||
|
|
a91d7c000a | ||
|
|
faf9828168 | ||
|
|
eadf10f287 | ||
|
|
ec980bf68a | ||
|
|
6f9dffe6e5 | ||
|
|
f61487284f | ||
|
|
483e7c5f51 | ||
|
|
be47d732e0 | ||
|
|
c57110e42a | ||
|
|
e8adaad01f | ||
|
|
4db0f83304 | ||
|
|
2ab23f50b2 | ||
|
|
4df94a4e2b | ||
|
|
97c417b1ad | ||
|
|
c0f194f443 | ||
|
|
e79660c5d1 | ||
|
|
deaf5b7398 | ||
|
|
e4041eabec | ||
|
|
7a1a642dc2 | ||
|
|
555f3a800b | ||
|
|
145784e73c | ||
|
|
f8e492d6cb | ||
|
|
49e04784af | ||
|
|
f843e69a1b | ||
|
|
232ac4317b | ||
|
|
b9dc03cd85 | ||
|
|
1ad7750d8b | ||
|
|
b6c772a326 | ||
|
|
ac5bb63214 | ||
|
|
4d580deeb0 | ||
|
|
cbf337a002 | ||
|
|
15ee6ead49 | ||
|
|
1cc9641f56 | ||
|
|
0cfc3b7ae9 | ||
|
|
fbfe1256a5 | ||
|
|
5074572694 | ||
|
|
e004c34ae3 | ||
|
|
5c0e5c347d | ||
|
|
2935111c6b | ||
|
|
71155eeec5 | ||
|
|
135acb5240 | ||
|
|
5558376bae | ||
|
|
2da560a21f | ||
|
|
bc9a0988a7 | ||
|
|
6a731f986e | ||
|
|
2cb4e551fa | ||
|
|
e5a1844852 | ||
|
|
b8f82ecb5e | ||
|
|
18583f6ce0 | ||
|
|
1468eb1ef2 | ||
|
|
8ab699a910 | ||
|
|
b173311468 | ||
|
|
522dcd8ffb | ||
|
|
6809a35177 | ||
|
|
bd82147496 | ||
|
|
a4224ab1df | ||
|
|
4c4dc8ca46 | ||
|
|
0afabd38c6 | ||
|
|
cacd108802 | ||
|
|
7138243c63 | ||
|
|
73b096a6e5 | ||
|
|
8165a5ff89 | ||
|
|
1017a38eb4 | ||
|
|
80374cae8a | ||
|
|
6339cdffa9 | ||
|
|
96a33bd387 | ||
|
|
1042e599d9 | ||
|
|
1a4f1cb07d | ||
|
|
edb249c2ad | ||
|
|
e9921928df | ||
|
|
fa0c87dec3 | ||
|
|
ba64c2d147 | ||
|
|
cf008b9cff | ||
|
|
3a9069f6e6 | ||
|
|
01abf348d2 | ||
|
|
eae7edbda0 | ||
|
|
d7c6a06b61 | ||
|
|
9d6f7ef0f2 | ||
|
|
0ddb1f69d2 | ||
|
|
4091abdfd7 | ||
|
|
27008cb44a | ||
|
|
b84825eae5 | ||
|
|
1c6ffe8ac6 | ||
|
|
dc0845b2d2 | ||
|
|
7ab502991e | ||
|
|
319f555569 | ||
|
|
0bf323ab97 | ||
|
|
255960d434 | ||
|
|
83a6aed625 | ||
|
|
f1b67049d1 | ||
|
|
8cbca5761e | ||
|
|
a237179a72 | ||
|
|
967846bef5 | ||
|
|
0bf9a2d3da | ||
|
|
18b2d3ac20 | ||
|
|
7d22fe3d7d | ||
|
|
ee67b6c042 | ||
|
|
1cfa76683b | ||
|
|
4662e45e58 | ||
|
|
1fefbdc9ab | ||
|
|
1cb2a57c55 | ||
|
|
246ee83015 | ||
|
|
335ddfc7a5 | ||
|
|
5b500830a1 | ||
|
|
ce4839bd80 | ||
|
|
eee22154e3 | ||
|
|
49007d3915 | ||
|
|
f9b453ca8c | ||
|
|
5f53b81c75 | ||
|
|
9d83adaac8 | ||
|
|
c8c269f612 | ||
|
|
15740904e5 | ||
|
|
85e9ff7a96 | ||
|
|
743617dbda | ||
|
|
89806b828a | ||
|
|
2e530bfdb0 | ||
|
|
83495b3213 | ||
|
|
5108416904 | ||
|
|
5589da1822 | ||
|
|
21e5aed3f3 | ||
|
|
4a88f795e1 | ||
|
|
604fa5be63 | ||
|
|
7ed1be994f | ||
|
|
6cc74a1c0a | ||
|
|
167b303fae | ||
|
|
b91c9e4ea6 | ||
|
|
947fe729cb | ||
|
|
bfb3ee4c43 | ||
|
|
bab1df2c78 | ||
|
|
00eabec771 | ||
|
|
584b7b6f45 | ||
|
|
2312c444ef | ||
|
|
0373d019f8 | ||
|
|
fe09e75b80 | ||
|
|
432c9c6cc1 | ||
|
|
5fc1d55879 | ||
|
|
268cd6cc9c | ||
|
|
20ffe31638 | ||
|
|
02d30067ea | ||
|
|
0ce7808e08 | ||
|
|
d609ee2b7a | ||
|
|
083df61fcc | ||
|
|
a524988be6 | ||
|
|
f2855d6dac | ||
|
|
8517a0b564 | ||
|
|
ec69b8f11b | ||
|
|
4250d0e266 | ||
|
|
edda1b57f3 | ||
|
|
5a7766992d | ||
|
|
02dabe11db | ||
|
|
58024128d8 | ||
|
|
e44bf9d773 | ||
|
|
1114223bda | ||
|
|
55022e5fa6 | ||
|
|
ae732fb298 | ||
|
|
a6ef6996c3 | ||
|
|
97cf5e127b | ||
|
|
690df7373c | ||
|
|
9cc01e5da1 | ||
|
|
0a068a2746 | ||
|
|
85a239ff8c | ||
|
|
a545a28dfb | ||
|
|
ac876a980c | ||
|
|
4b555e8a56 | ||
|
|
1a8d14dc44 | ||
|
|
83263a72d2 | ||
|
|
76f3ba8691 | ||
|
|
072edd7aff | ||
|
|
c0be4d702c | ||
|
|
c3d6db3f1e | ||
|
|
e05ec53eb5 | ||
|
|
8d9dad9c82 | ||
|
|
0071bcb0e4 | ||
|
|
dbdc1ccd1b | ||
|
|
2a606fdb1d | ||
|
|
3b86f30bcf | ||
|
|
7808d028e9 | ||
|
|
04bcf73b8b | ||
|
|
1f8179d519 | ||
|
|
3ffd8946f8 | ||
|
|
c78898e4c1 | ||
|
|
d3e4677ea4 | ||
|
|
d176f9cc0e | ||
|
|
a65366bbe1 | ||
|
|
20cfe74b17 | ||
|
|
b74db81304 | ||
|
|
a8adbda04a | ||
|
|
3e80d30ca6 | ||
|
|
2eb54d50a5 | ||
|
|
6995308c17 | ||
|
|
0738d120ae | ||
|
|
bad6879589 | ||
|
|
b46948c0ba | ||
|
|
3024720fc2 | ||
|
|
cf854c9278 | ||
|
|
608cc2a81f | ||
|
|
396670dc8f | ||
|
|
79f65868ca | ||
|
|
56e5524a0f | ||
|
|
1ae185d0bc | ||
|
|
82276b53a8 | ||
|
|
d5882f16be | ||
|
|
b35e7b0702 | ||
|
|
bce7d95838 | ||
|
|
4d490d0add | ||
|
|
7cff1a9842 | ||
|
|
cfba7b4daa | ||
|
|
4dff48e807 | ||
|
|
8abe0daf20 | ||
|
|
635f069012 | ||
|
|
eeb83f9024 | ||
|
|
b48831e60b | ||
|
|
095791cdc2 | ||
|
|
3e304c497e | ||
|
|
9ffea32176 | ||
|
|
f32c99d4a1 | ||
|
|
1f1b1858c7 | ||
|
|
42fcb91f2e | ||
|
|
01568ca148 | ||
|
|
0d97ad67c5 | ||
|
|
c94a5537a3 | ||
|
|
d88dfc197e | ||
|
|
f709c05c0d | ||
|
|
2c609da7a1 | ||
|
|
d770c7bd08 | ||
|
|
b9ccd4a8a0 | ||
|
|
8875e8059f | ||
|
|
6490366c68 | ||
|
|
c278946771 | ||
|
|
1562d9de36 | ||
|
|
5bb0990abb | ||
|
|
bfd3cacc68 | ||
|
|
c7e1163d59 | ||
|
|
fba7aeb71b | ||
|
|
c66a2bd77a | ||
|
|
ed58ae3c53 | ||
|
|
194463bea9 | ||
|
|
260f5ae777 | ||
|
|
e85d824337 | ||
|
|
e17fc39709 | ||
|
|
da9b297b12 | ||
|
|
807dfcd57a | ||
|
|
0a410db52a | ||
|
|
4501095106 | ||
|
|
bc731e4fb1 | ||
|
|
53561d2755 | ||
|
|
e913fe509c | ||
|
|
a3a215b7ae | ||
|
|
56f2949ac5 | ||
|
|
c1b5a35f90 | ||
|
|
48ab1fbffe | ||
|
|
5f078273b3 | ||
|
|
636338f9ed | ||
|
|
6cbd629777 | ||
|
|
5a07dce759 | ||
|
|
15484bc119 | ||
|
|
d6cd9d136d | ||
|
|
c76815756b | ||
|
|
eef93250ac | ||
|
|
a1c6cf0477 | ||
|
|
14a0ccac93 | ||
|
|
9439743b7e | ||
|
|
acbac6a9c3 |
@@ -1,36 +0,0 @@
|
||||
# 后端规则
|
||||
|
||||
主包:`packages/ui/certd-server`。后端使用 Node.js、ESM、TypeScript、MidwayJS 3、Koa、TypeORM,默认 better-sqlite3,同时支持 PostgreSQL 和 MySQL,并通过 `@certd/midway-flyway-js` 使用类似 Flyway 的 SQL 迁移机制。
|
||||
|
||||
详细入口、模块和验证命令见 `.codex/repo-map.md`。
|
||||
|
||||
## 默认开发配置
|
||||
|
||||
- HTTP 端口:`7001`
|
||||
- HTTPS 端口:`7002`
|
||||
- 默认 SQLite 数据库:`./data/db.sqlite`
|
||||
- 默认文件根目录:`./data/files`
|
||||
|
||||
## 数据与迁移
|
||||
|
||||
- 后端使用 TypeORM 实体加 SQL 迁移。
|
||||
- 重点查看 `packages/ui/certd-server/src/modules/**/entity/*.ts` 和 `packages/ui/certd-server/db/migration/*.sql`。
|
||||
- 默认配置中 `synchronize: false`,涉及表结构变更时应添加或更新迁移脚本,不要依赖 TypeORM 自动同步。
|
||||
|
||||
## 文件上传
|
||||
|
||||
使用 `/basic/file/upload` 上传文件后,接口返回的是临时缓存 key。业务保存表单或设置时,后端必须调用 `FileService.saveFile(userId, key, "public" | "private")` 转成永久文件 key 后再入库/入设置;不要直接保存 `tmpfile_key_...`,否则后续回显或下载会失效。
|
||||
|
||||
## Service 与事务
|
||||
|
||||
- 后端方法参数超过 3 个时,尽量改为对象参数传入。
|
||||
- 需要传入 `manager` / `EntityManager` 做事务传播的方法,必须使用对象参数,不要把 `manager` 作为位置参数藏在参数列表末尾。
|
||||
- 后端 service 层只有存在事务链路传播需求时才定义 `ctx`,不要为了将来可能需要而提前给普通方法加 `ctx`。
|
||||
- 事务链路方法统一采用 `method(ctx, req)` 形式,`ctx` 放第一位并承载 `manager?: EntityManager` 等横切上下文,业务参数放在 `req` 对象里,例如 `settleCommission({ manager }, { tradeId, userId, amount })`。
|
||||
- 无事务链路需求的普通查询、纯函数和简单私有方法继续使用明确参数。
|
||||
- service 内部需要根据事务上下文选择 Repository 时,优先使用 `BaseService.getRepo(ctx, EntityClass)`;不要在业务方法里反复写 `ctx.manager?.getRepository(Entity) || this.xxxRepository`。
|
||||
- 拿到 repo 后 save/update/delete/find 都能做,不需要再包一层 `saveEntity` 之类的单一用途方法。
|
||||
- service 拼接用户与项目范围查询条件时,优先使用 `BaseService.buildUserProjectQuery(userId, projectId)`;不要直接写 `{ userId, projectId }`,否则 `projectId` 为空时可能把 `null`/`undefined` 带入 TypeORM 条件,导致查询或 update 不符合预期。
|
||||
- `ctx` 类型统一从 `BaseService` 导出的 `ServiceContext` 复用,不要在每个 service 里重复定义。
|
||||
- 需要“有事务则复用、无事务则开启”时,使用 `BaseService.transactionWithCtx(ctx, callback)`:`ctx.manager` 存在则直接执行 callback,否则自动 `this.transaction()`。不要在业务代码里手写 `if (ctx.manager) { ... } else { await this.transaction(...) }`。
|
||||
- 新增方法注意不要与 `BaseService` 基类方法签名冲突,例如 `delete(id)` vs `BaseService.delete(ids, where?)`,ts-node 下会直接 TS2416 编译报错。冲突时改用具体名称如 `deleteById`。
|
||||
@@ -1,26 +0,0 @@
|
||||
# 代码风格规则
|
||||
|
||||
## 基本原则
|
||||
|
||||
- 中文 README 在部分 PowerShell 环境中可能显示乱码;`README_en.md` 可读性更好,且包含同样的高层项目说明。
|
||||
- 根包管理器是 pnpm,不要引入 npm/yarn lockfile。
|
||||
- 优先沿用现有模块、插件、服务模式,再考虑新增抽象。
|
||||
- 注意本地数据和配置里可能包含凭据、证书材料等敏感信息。
|
||||
|
||||
## 注释
|
||||
|
||||
本仓库代码注释优先使用中文,尤其是解释业务规则、兼容逻辑、协议细节和隐藏风险时;除非文件已有明确英文注释风格或引用外部英文术语,否则不要新增英文说明性注释。
|
||||
|
||||
## 可读性
|
||||
|
||||
代码可读性优先于短写法。遇到包含业务分支的复杂三元表达式、内联对象、链式调用或条件组合时,优先拆成命名清晰的中间变量、独立分支或小函数,让读代码的人能一眼看出业务意图;不要为了少写几行把逻辑压成难读的一坨。
|
||||
|
||||
在对象字面量、查询条件或函数参数里不要内联调用多层 helper,例如 `{ domain, ...this.buildUserProjectQuery(userId, projectId) }`。应先用命名变量承接结果,例如 `const userProjectQuery = this.buildUserProjectQuery(userId, projectId)`,再在对象里展开 `...userProjectQuery`,让条件构造和业务字段都更容易阅读。
|
||||
|
||||
## DRY
|
||||
|
||||
遵守 DRY 原则:同一业务规则、字段转换、权限判断、Repository 选择、事务传播、金额计算等逻辑不要在多个地方复制粘贴。第二次出现时可以先保持清晰,第三次出现前应优先抽成局部 helper、service 方法或已有公共工具;抽象要服务于减少真实重复和降低修改风险,不要为了形式上的“复用”制造过度设计。
|
||||
|
||||
## 单一职责
|
||||
|
||||
遵守单一职责原则:一个方法只负责一个清晰的业务步骤或技术步骤。流程编排方法可以串联多个步骤,但具体的校验、计算、持久化、状态变更、展示数据组装应尽量拆到命名明确的小方法中;不要让一个方法同时承担查询、校验、计算、写库、格式化返回等过多职责。
|
||||
@@ -1,31 +0,0 @@
|
||||
# 前端规则
|
||||
|
||||
主包:`packages/ui/certd-client`。前端使用 Vue 3、Vite、TypeScript、Ant Design Vue、Fast Crud、Pinia、vue-router、vue-i18n、Tailwind/Windi 相关样式工具。
|
||||
|
||||
详细入口、路由、状态、API、视图、locale 和验证命令见 `.codex/repo-map.md`。
|
||||
|
||||
## 禁跑命令
|
||||
|
||||
- 不要运行前端 `pnpm tsc` / `vue-tsc`:当前依赖组合中 `vue-tsc@1.8.27` 会直接抛内部错误 `Search string not found: "/supportedTSExtensions = .*(?=;)/"`,不是有效的项目类型检查结果。
|
||||
- 前端暂不跑单元测试;当前 `test:unit` 只是占位脚本。
|
||||
|
||||
## 格式化与校验
|
||||
|
||||
前端 TS/Vue/locale 等文件改动后,优先只对本次改动文件运行项目现有自动格式化/修复:
|
||||
|
||||
- Prettier:`packages\ui\certd-client\node_modules\.bin\prettier.cmd --write <files>`
|
||||
- ESLint:`packages\ui\certd-client\node_modules\.bin\eslint.cmd --fix <files>`
|
||||
|
||||
不要为了格式化无关文件而扩大 diff。项目保留了 `tslint` 依赖,但当前主要使用 ESLint + Prettier。
|
||||
|
||||
## Fast Crud 页面
|
||||
|
||||
- 列表管理、后台管理、记录查询、CRUD 表格类页面,默认优先使用 Fast Crud(`@fast-crud/fast-crud`、`fs-crud`、`useFs`、`createCrudOptions`)实现。
|
||||
- 只有轻量只读展示、强交互自定义界面或已有页面模式明确不适合 Fast Crud 时,才手写 `a-table` / 自定义列表,并在回复中说明原因。
|
||||
- 开发或重构这类页面前,先读取 `.trae/skills/fast-crud-page-dev/SKILL.md`,按仓库内 Fast Crud 页面拆分与验证方式实现。
|
||||
- 页面内嵌 Fast Crud 表格时,要显式给外层容器稳定高度或 `flex: 1; min-height: 0` 的撑满链路;Fast Crud 依赖外部元素高度,不能只依赖表格默认高度。
|
||||
- 后台管理列表里展示或筛选用户字段时,优先参考 `packages/ui/certd-client/src/views/sys/suite/user-suite/crud.tsx` 的 `userId` 字段模式:前端使用 `table-select` + `/sys/authority/user/getSimpleUserByIds` 字典回显和搜索;不要为了展示用户名让后端列表接口额外 `fillSimpleUser` / `userDisplay`,除非该接口本身就是用户端业务列表且已有明确模式。
|
||||
|
||||
## 对话框
|
||||
|
||||
前端对话框里只做纯确认时可以使用 `Modal.confirm`;只要对话框里有字段输入、表单校验或提交字段,统一使用 `useFormDialog` / `openFormDialog`,不要在 `Modal.confirm` 的 `content` 里手写输入框。
|
||||
@@ -1,42 +0,0 @@
|
||||
# 流水线与插件规则
|
||||
|
||||
项目最关键的架构概念是证书流水线。核心导出、关键抽象、插件目录和共享 helper 位置见 `.codex/repo-map.md`。
|
||||
|
||||
插件是核心能力,不是边缘功能。新增服务商、DNS 验证、证书部署、通知方式等能力,通常应该放在插件包里,或放在 `packages/ui/certd-server/src/plugins/<plugin-name>/` 下。
|
||||
|
||||
## 改动归属
|
||||
|
||||
修改证书申请、验证、部署或通知行为时,先判断改动属于哪里:
|
||||
|
||||
- ACME client 代码
|
||||
- pipeline 核心抽象
|
||||
- 后端 module/service/entity/controller
|
||||
- 某个具体插件实现
|
||||
- 前端 view/form/schema
|
||||
|
||||
如果只是某个服务商或部署目标的问题,不要轻易修改共享 pipeline/core 行为,除非确实是可复用的公共能力。
|
||||
|
||||
## ACME / EAB
|
||||
|
||||
- 公共 EAB(尤其是 Google EAB)可能只能创建一次 ACME 账号。要跨用户复用公共 EAB,应保存并复用同一个 ACME account private key;`accountUrl` 如果存到 `userContext` 里,只能视为当前用户缓存,因为 `userContext` 跟用户 id 走。
|
||||
- ACME 协议的 `newAccount` 支持 `onlyReturnExisting`。使用同一个 account private key 调用 `newAccount({ onlyReturnExisting: true })` 可以取回已创建账号的 URL,且不会再次消费 EAB。
|
||||
- 修改 EAB 的 `kid` 后,应重新生成绑定该 `kid` 的 account private key;否则应阻止继续申请并提示用户刷新账号私钥。
|
||||
|
||||
## 插件开发技能
|
||||
|
||||
仓库内置了 Certd 插件开发技能,供 Trae 和 Codex 共用:
|
||||
|
||||
- Trae 入口:`.trae/skills`
|
||||
- Codex 入口:`.codex/skills`
|
||||
|
||||
其中 `.codex/skills` 是指向 `.trae/skills` 的目录链接,不要复制出第二份技能内容。更新技能时只维护 `.trae/skills` 下的原始文件,Codex 会通过 `.codex/skills` 读取同一份内容。
|
||||
|
||||
当前技能包括:
|
||||
|
||||
- `access-plugin-dev`:开发 Access 授权插件
|
||||
- `dns-provider-dev`:开发 DNS Provider 插件
|
||||
- `fast-crud-page-dev`:开发或重构前端 Fast Crud 列表管理页面
|
||||
- `task-plugin-dev`:开发 Task 部署任务插件
|
||||
- `plugin-converter`:将插件转换为 YAML 配置
|
||||
|
||||
做插件相关任务时,先读取对应技能目录下的 `SKILL.md`,再进入具体实现。若用户在插件开发中指出更好的做法,应总结并更新对应技能。
|
||||
@@ -1,26 +0,0 @@
|
||||
# 测试与验证规则
|
||||
|
||||
实现新功能或修复行为缺陷前,先补对应单元测试,并先运行测试确认它处于失败状态;再实现功能或修复代码,反复运行聚焦单元测试直到通过。若某项改动确实不适合先写单元测试,应在回复中说明原因和替代验证方式。
|
||||
|
||||
后补单元测试时,应先基于对正确行为的实际预期编写测试,而不是为了迎合现有实现改写预期;如果运行后出现红灯,且通过测试需要修改已有实现,应先向用户确认这是确实的 bug,还是原本需求/既有行为就是如此;确认后再修改原始实现,避免把测试补充变成未经确认的行为改动。
|
||||
|
||||
## 后端单测
|
||||
|
||||
- 后端纯单元测试用例放在 `src` 目录内,并尽量与被测文件相邻,例如 `src/utils/random.test.ts`。
|
||||
- 对应 `test:unit` 只跑 `src/**/*.test.ts`,构建/打包配置应排除这些 `*.test.ts` 文件。
|
||||
- 单元测试需要 mock ESM 静态 import 时,优先使用 `esmock`,不要为了测试把业务代码改成构造函数注入或把逻辑挪到调用方。
|
||||
- 各包 `test:unit` 脚本应显式设置 `NODE_ENV=unittest`。
|
||||
|
||||
## 运行方式
|
||||
|
||||
单个 monorepo 包运行单元测试时,优先使用 `corepack pnpm --dir <包目录> test:unit`,例如:
|
||||
|
||||
- `corepack pnpm --dir packages\ui\certd-server test:unit`
|
||||
- `corepack pnpm --dir packages\core\basic test:unit`
|
||||
- `corepack pnpm --dir packages\plugins\plugin-lib test:unit`
|
||||
|
||||
也可以用包名过滤,例如 `corepack pnpm --filter @certd/ui-server test:unit`。
|
||||
|
||||
前端 `packages\ui\certd-client` 暂时不跑单元测试。前端改动优先使用 Prettier/ESLint 做改动文件验证。
|
||||
|
||||
优先对改动包运行聚焦测试;只有跨包影响明显时再考虑全 monorepo 构建。
|
||||
@@ -1,106 +0,0 @@
|
||||
# Certd 仓库地图
|
||||
|
||||
本文档由 Codex 子智能体只读探索后整理,用于后续开发时快速定位代码。进入仓库仍应先读取根目录 `AGENTS.md`,本文件只作为导航补充。
|
||||
|
||||
## 顶层结构
|
||||
|
||||
Certd 是 pnpm + lerna-lite monorepo。
|
||||
|
||||
- `package.json`:根脚本与 workspace 元信息
|
||||
- `pnpm-workspace.yaml`:workspace 匹配规则,包含 `packages/**`、`packages/ui/**`
|
||||
- `lerna.json`:lerna-lite 配置
|
||||
- `docs`:VitePress 文档站
|
||||
- `docker`:Docker 安装和运行相关文件
|
||||
- `packages/core/acme-client`:ACME 协议客户端
|
||||
- `packages/core/basic`:共享基础工具
|
||||
- `packages/core/pipeline`:流水线核心抽象、插件模型、执行上下文
|
||||
- `packages/libs`:共享集成库
|
||||
- `packages/plugins/plugin-lib`:证书、DNS Provider、格式转换等插件共享能力
|
||||
- `packages/plugins/plugin-cert`:证书插件包入口
|
||||
- `packages/ui/certd-server`:后端 Midway 服务
|
||||
- `packages/ui/certd-client`:前端 Vue/Vite 管理台
|
||||
- `packages/pro`:商业版独立 Git 工作区,需在该目录内单独检查状态
|
||||
|
||||
运行时或生成产物通常包括根目录 `node_modules`、`logs`、`output`、`lerna-debug.log`、`tmp-certd-client-vite*.log`,以及后端 `packages/ui/certd-server/data`、`packages/ui/certd-server/logs`、各包 `dist`、插件 metadata/yaml 导出结果。
|
||||
|
||||
## 常用验证
|
||||
|
||||
- 根目录启动后端生产模式:`pnpm run start:server`
|
||||
- 后端开发服务:`corepack pnpm --dir packages\ui\certd-server dev`
|
||||
- 后端聚焦单测:`corepack pnpm --dir packages\ui\certd-server test:unit`
|
||||
- 后端完整测试:`corepack pnpm --dir packages\ui\certd-server test`
|
||||
- 后端构建:`corepack pnpm --dir packages\ui\certd-server build`
|
||||
- 前端开发服务:`corepack pnpm --dir packages\ui\certd-client dev`
|
||||
- 前端构建:`corepack pnpm --dir packages\ui\certd-client build`
|
||||
- 前端改动文件格式化:`packages\ui\certd-client\node_modules\.bin\prettier.cmd --write <files>`
|
||||
- 前端改动文件 ESLint 修复:`packages\ui\certd-client\node_modules\.bin\eslint.cmd --fix <files>`
|
||||
|
||||
不要主动运行 `pnpm install`。前端不要运行 `pnpm tsc` / `vue-tsc`,当前依赖组合中 `vue-tsc@1.8.27` 会抛无效内部错误;前端 `test:unit` 也只是占位。
|
||||
|
||||
## 后端地图
|
||||
|
||||
主包:`packages/ui/certd-server`。
|
||||
|
||||
- `bootstrap.js`:Midway 启动入口,使用 `@midwayjs/bootstrap`
|
||||
- `src/configuration.ts`:Midway 主配置,注册组件和全局中间件
|
||||
- `src/config/config.default.ts`:端口、HTTPS、静态文件、cron、TypeORM、Flyway、上传、JWT、Swagger 默认配置
|
||||
- `src/config/loader.ts`:读取 `.env`、`.env.<env>.yaml`,支持 `certd_` 前缀环境变量覆盖嵌套配置
|
||||
- `src/modules`:业务模块根目录,例如 `basic`、`cert`、`cname`、`cron`、`login`、`monitor`、`open`、`pipeline`、`plugin`、`suite`、`sys`
|
||||
- `src/controller`:API 入口,按 `basic`、`user`、`sys`、`openapi` 分组
|
||||
- `db/migration`:SQL 迁移目录,TypeORM `synchronize: false`,表结构变更应配套迁移 SQL
|
||||
|
||||
测试使用 Mocha + Node `assert/strict`,纯单测放在 `src/**/*.test.ts`,尽量与被测文件相邻。可参考 `src/utils/random.test.ts`、`src/controller/basic/app-controller.test.ts`、`src/modules/pipeline/service/pipeline-service.test.ts`。
|
||||
|
||||
## 前端地图
|
||||
|
||||
主包:`packages/ui/certd-client`。
|
||||
|
||||
- `vite.config.ts`:Vite 配置,dev 端口 `3008`,`/api`、`/certd/api` 代理到后端 `127.0.0.1:7001`
|
||||
- `src/main.ts`:Vue 启动入口,注册 AntDV、Vben、router、全局组件、插件和偏好设置
|
||||
- `src/App.vue`:根组件,包含 `AConfigProvider`、`FsFormProvider`、`router-view`
|
||||
- `src/router/index.ts`、`src/router/resolve.ts`:路由入口,使用 `createWebHashHistory`
|
||||
- `src/router/source/modules/certd.ts`:Certd 主业务路由
|
||||
- `src/store`:Pinia store,主要有 `user`、`project`、`settings`、`plugin`
|
||||
- `src/api/service.ts`:Axios 封装
|
||||
- `src/api/tools.ts`:错误与响应工具
|
||||
- `src/views/certd`:核心业务视图,例如 `pipeline`、`cert`、`monitor`、`access`、`notification`、`open`、`project`、`suite`、`wallet`
|
||||
- `src/locales`:国际化入口与语言包
|
||||
|
||||
列表管理、后台管理、记录查询、CRUD 表格页面优先使用 Fast Crud。开发前读取 `.trae/skills/fast-crud-page-dev/SKILL.md`。常见拆分是 `api.ts`、`crud.tsx`、`index.vue`。可参考 `src/views/certd/access`、`src/views/sys/suite/user-suite/crud.tsx`、`src/views/certd/wallet/index.vue`。内嵌 `fs-crud` 时要给外层稳定高度或完整 `flex: 1; min-height: 0` 链路。
|
||||
|
||||
## 流水线与插件地图
|
||||
|
||||
核心入口:`packages/core/pipeline/src/index.ts`,导出 `core`、`dt`、`access`、`registry`、`plugin`、`context`、`decorator`、`service`、`notification`。
|
||||
|
||||
- `packages/core/pipeline/src/plugin`:任务插件抽象,例如 `AbstractTaskPlugin`、`IsTaskPlugin`、`TaskInput`、`pluginRegistry`
|
||||
- `packages/core/pipeline/src/access`:授权插件抽象,例如 `BaseAccess`、`IsAccess`、`AccessInput`、`accessRegistry`
|
||||
- `packages/core/pipeline/src/dt/pipeline.ts`:`Pipeline`、`Stage`、`Task`、`RunStrategy` 等流水线数据结构
|
||||
- `packages/core/pipeline/src/core`:执行器、上下文、运行历史、文件存储等
|
||||
- `packages/core/pipeline/src/service`:CNAME、事件、配置、邮件、URL 等 pipeline service 接口
|
||||
- `packages/ui/certd-server/src/plugins`:后端内置服务商、DNS、部署、通知等插件
|
||||
- `packages/ui/certd-server/src/plugins/plugin-cert`:证书申请核心插件
|
||||
- `packages/ui/certd-server/src/plugins/plugin-lib`:后端插件 helper/access
|
||||
- `packages/plugins/plugin-lib/src/cert`:`CertReader`、`CertConverter`、DNS Provider 公共能力
|
||||
- `packages/plugins/plugin-lib/src/cert/dns-provider`:`AbstractDnsProvider`、`dnsProviderRegistry`、`DomainParser`
|
||||
|
||||
插件开发技能入口:
|
||||
|
||||
- `.trae/skills/dns-provider-dev/SKILL.md`:DNS Provider 插件
|
||||
- `.trae/skills/task-plugin-dev/SKILL.md`:Task 部署任务插件
|
||||
- `.trae/skills/access-plugin-dev/SKILL.md`:Access 授权插件
|
||||
- `.trae/skills/plugin-converter/SKILL.md`:插件转 YAML 配置
|
||||
|
||||
改动归属判断:
|
||||
|
||||
- ACME 协议、EAB、账号、订单、挑战流程:优先看 `packages/core/acme-client` 或 `packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/acme.ts`
|
||||
- 流水线执行、任务生命周期、输入输出、注册机制:看 `packages/core/pipeline`
|
||||
- 单个云厂商 DNS 验证、证书部署、API 调用失败:改对应 `packages/ui/certd-server/src/plugins/plugin-xxx`
|
||||
- 通用证书读取、DNS Provider 公共能力、格式转换:改 `packages/plugins/plugin-lib`
|
||||
- 后端业务数据、接口、实体、权限、迁移:改 `packages/ui/certd-server/src/modules` 与 `src/controller`
|
||||
- 表单、列表、插件配置 UI:改 `packages/ui/certd-client/src/views/certd` 及对应 `src/api`
|
||||
|
||||
原则:如果只是单个服务商或部署目标的问题,不动共享 pipeline/core;只有可复用的公共语义或跨插件一致行为,才考虑上移到 `packages/core/pipeline` 或 `packages/plugins/plugin-lib`。
|
||||
|
||||
## Git 注意事项
|
||||
|
||||
子智能体探索时根仓库 `git status --short` 为空。`packages/pro` 也是独立仓库且当时未显示未提交改动,但曾出现无法删除 `packages/pro/.git/index.lock` 的警告;后续操作 pro 仓库前应先检查该锁文件或占用状态。
|
||||
@@ -80,6 +80,8 @@ jobs:
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
provenance: false
|
||||
sbom: false
|
||||
context: ./packages/ui/
|
||||
tags: |
|
||||
registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{steps.get_certd_version.outputs.result}}
|
||||
|
||||
@@ -86,39 +86,71 @@ jobs:
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
provenance: false
|
||||
sbom: false
|
||||
context: ./packages/ui/
|
||||
tags: |
|
||||
registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest
|
||||
registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{steps.get_certd_version.outputs.result}}
|
||||
registry.cn-shenzhen.aliyuncs.com/certd/certd:latest
|
||||
registry.cn-shenzhen.aliyuncs.com/certd/certd:${{steps.get_certd_version.outputs.result}}
|
||||
greper/certd:latest
|
||||
greper/certd:${{steps.get_certd_version.outputs.result}}
|
||||
ghcr.io/${{ github.repository }}:latest
|
||||
ghcr.io/${{ github.repository }}:${{steps.get_certd_version.outputs.result}}
|
||||
- name: Build slim
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
provenance: false
|
||||
sbom: false
|
||||
context: ./packages/ui/
|
||||
build-args: |
|
||||
base_type=slim
|
||||
tags: |
|
||||
registry.cn-shenzhen.aliyuncs.com/handsfree/certd:slim
|
||||
registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{steps.get_certd_version.outputs.result}}-slim
|
||||
registry.cn-shenzhen.aliyuncs.com/certd/certd:slim
|
||||
registry.cn-shenzhen.aliyuncs.com/certd/certd:${{steps.get_certd_version.outputs.result}}-slim
|
||||
greper/certd:slim
|
||||
greper/certd:${{steps.get_certd_version.outputs.result}}-slim
|
||||
ghcr.io/${{ github.repository }}:slim
|
||||
ghcr.io/${{ github.repository }}:${{steps.get_certd_version.outputs.result}}-slim
|
||||
|
||||
- name: Build armv7
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/arm/v7
|
||||
push: true
|
||||
provenance: false
|
||||
sbom: false
|
||||
context: ./packages/ui/
|
||||
tags: |
|
||||
registry.cn-shenzhen.aliyuncs.com/handsfree/certd:armv7
|
||||
registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{steps.get_certd_version.outputs.result}}-armv7
|
||||
registry.cn-shenzhen.aliyuncs.com/certd/certd:armv7
|
||||
registry.cn-shenzhen.aliyuncs.com/certd/certd:${{steps.get_certd_version.outputs.result}}-armv7
|
||||
greper/certd:armv7
|
||||
greper/certd:${{steps.get_certd_version.outputs.result}}-armv7
|
||||
ghcr.io/${{ github.repository }}:armv7
|
||||
ghcr.io/${{ github.repository }}:${{steps.get_certd_version.outputs.result}}-armv7
|
||||
|
||||
# - name: Build agent
|
||||
# uses: docker/build-push-action@v6
|
||||
# with:
|
||||
# platforms: linux/amd64,linux/arm64
|
||||
# push: true
|
||||
# context: ./packages/ui/agent/
|
||||
# tags: |
|
||||
# registry.cn-shenzhen.aliyuncs.com/handsfree/certd-agent:latest
|
||||
# registry.cn-shenzhen.aliyuncs.com/handsfree/certd-agent:${{steps.get_certd_version.outputs.result}}
|
||||
# greper/certd-agent:latest
|
||||
# greper/certd-agent:${{steps.get_certd_version.outputs.result}}
|
||||
- name: Login to Certd Docker Hub account
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.dockerhub_username2 }}
|
||||
password: ${{ secrets.dockerhub_password2 }}
|
||||
|
||||
- name: Publish to certd Docker Hub repository
|
||||
run: |
|
||||
docker buildx imagetools create --tag certd/certd:latest greper/certd:latest
|
||||
docker buildx imagetools create --tag certd/certd:${{ steps.get_certd_version.outputs.result }} greper/certd:${{ steps.get_certd_version.outputs.result }}
|
||||
docker buildx imagetools create --tag certd/certd:slim greper/certd:slim
|
||||
docker buildx imagetools create --tag certd/certd:${{ steps.get_certd_version.outputs.result }}-slim greper/certd:${{ steps.get_certd_version.outputs.result }}-slim
|
||||
docker buildx imagetools create --tag certd/certd:armv7 greper/certd:armv7
|
||||
docker buildx imagetools create --tag certd/certd:${{ steps.get_certd_version.outputs.result }}-armv7 greper/certd:${{ steps.get_certd_version.outputs.result }}-armv7
|
||||
|
||||
- name: deploy-certd-doc
|
||||
uses: tyrrrz/action-http-request@prime
|
||||
with:
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
name: stable-release
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "版本号(如 v1.42.5)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
make-stable:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to aliyun container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: registry.cn-shenzhen.aliyuncs.com
|
||||
username: ${{ secrets.aliyun_cs_username }}
|
||||
password: ${{ secrets.aliyun_cs_password }}
|
||||
|
||||
- name: Login to GitHub Packages
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.dockerhub_username2 }}
|
||||
password: ${{ secrets.dockerhub_password2 }}
|
||||
|
||||
# stable 镜像:叠加 ENV 层
|
||||
- name: Build and push stable images
|
||||
run: |
|
||||
echo "FROM greper/certd:${{ inputs.version }}
|
||||
ENV certd_release_mode=stable" > Dockerfile.stable
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--provenance=false \
|
||||
--sbom=false \
|
||||
--push \
|
||||
-f Dockerfile.stable \
|
||||
-t registry.cn-shenzhen.aliyuncs.com/handsfree/certd:stable \
|
||||
-t registry.cn-shenzhen.aliyuncs.com/certd/certd:stable \
|
||||
-t certd/certd:stable \
|
||||
-t ghcr.io/${{ github.repository }}:stable \
|
||||
.
|
||||
|
||||
# slim-stable 镜像
|
||||
- name: Build and push slim-stable images
|
||||
run: |
|
||||
echo "FROM greper/certd:${{ inputs.version }}-slim
|
||||
ENV certd_release_mode=stable" > Dockerfile.slim-stable
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--provenance=false \
|
||||
--sbom=false \
|
||||
--push \
|
||||
-f Dockerfile.slim-stable \
|
||||
-t registry.cn-shenzhen.aliyuncs.com/handsfree/certd:slim-stable \
|
||||
-t registry.cn-shenzhen.aliyuncs.com/certd/certd:slim-stable \
|
||||
-t certd/certd:slim-stable \
|
||||
-t ghcr.io/${{ github.repository }}:slim-stable \
|
||||
.
|
||||
|
||||
- name: Set AtomGit release as stable
|
||||
run: |
|
||||
export ATOMGIT_TOKEN=${{ secrets.ATOMGIT_TOKEN }}
|
||||
export VERSION=${{ inputs.version }}
|
||||
node ./scripts/set-release-stable-fetch.js
|
||||
+6
-2
@@ -37,5 +37,9 @@ pnpm-lock.yaml
|
||||
.studio/
|
||||
|
||||
# Certd 推广报告,仅本地使用
|
||||
/popularize/
|
||||
|
||||
/popularize/reports/
|
||||
output/
|
||||
.uploads/
|
||||
.certd-plugin-history/
|
||||
.tmp
|
||||
.plugin-dev/
|
||||
@@ -400,6 +400,7 @@ export class OAuthDemoAccess extends BaseAccess {
|
||||
4. **错误处理**:API 调用失败时应抛出明确的错误信息。
|
||||
5. **测试方法**:实现 `onTestRequest` 方法,以便用户可以测试授权是否正常。
|
||||
6. **统一接口调用**:封装统一的 API 请求方法,避免重复编写错误处理逻辑。
|
||||
7. **旧版数据兼容**: 新增注解的插件参数,必须要考虑旧版数据兼容,比如新增一个deployType参数,有两种值:`default`和`custom`,需要在使用时判空,走旧版逻辑。
|
||||
|
||||
## 开发技巧
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
你是一名资深nodejs工程师,擅长开发Certd开源系统的任务插件。
|
||||
certd是一款全自动证书申请部署管理工具,基于流水线的方式,通过里面申请证书插件申请证书,然后将证书传递给下一个部署任务插件,不同的部署任务插件将证书部署到用户的各个应用系统当中。
|
||||
|
||||
certd插件分成以下几种类型:
|
||||
Access:存储用户的第三放应用的授权数据,比如用户名密码,accessSecret 或 accessToken等。同时它里面的方法还负责对接第三方的api接口
|
||||
Task: 部署任务插件,它继承AbstractTaskPlugin类,被流水线调用execute方法,将证书部署到对应的应用上
|
||||
DnsProvider: DNS提供商插件,它用于在ACME申请证书时给域名添加txt解析记录。
|
||||
|
||||
注意事项:
|
||||
1、使用技能:在开始工作前,请阅读并加载.trae/skills下面的技能,根据skills进行相应的插件开发
|
||||
2、迭代技能:当开发过程用户提醒你更好的做法时,你需要总结经验,更新相应的skills,让skills越来越完善,能够在以后得新插件开发中具备指导意义。
|
||||
3、一般调用的api接口文档会比较复杂,你不知道接口是什么时,请务必询问用户,让用户提供API接口文档
|
||||
4、完成开发后无需测试,通知用户自己去测试
|
||||
@@ -0,0 +1,162 @@
|
||||
---
|
||||
name: certd-online-plugin-dev
|
||||
description: 用于通过 Certd API 开发、修改、读取、保存和恢复在线插件。当用户要求使用 Codex 或 Trae 创建 Task、DNS Provider、Access 插件,或优化已有 Certd 插件时使用。
|
||||
---
|
||||
|
||||
# Certd 在线插件开发 Skill
|
||||
|
||||
## 开发方式
|
||||
|
||||
本 Skill 使用 Certd HTTP API,不使用 WebSocket,也不依赖浏览器传递代码草稿。
|
||||
|
||||
- 前端只生成包含需求、API 地址和认证 Token 的启动提示词。
|
||||
- Agent 直接调用 Certd API 读取和保存插件 YAML。
|
||||
- Agent 的进度、日志和代码修改在 Codex/Trae 中查看。
|
||||
- 插件开发临时文件和修改历史统一保存在 Agent 工作区的 `.plugin-dev/` 下,不保存到浏览器或 Certd 后端。
|
||||
|
||||
## 启动检查
|
||||
|
||||
开始开发前先确认当前工作目录是否已经在 Certd 项目内:
|
||||
|
||||
- 应存在 `package.json`。
|
||||
- 应存在 `packages/ui/certd-server/src/plugins/`。
|
||||
- 应存在 `.trae/skills/`。
|
||||
- 应存在 `.trae/skills/certd-online-plugin-dev/SKILL.md`。
|
||||
|
||||
如果当前目录不是 Certd 项目,或缺少 `certd-online-plugin-dev` Skill,先拉取 Certd 仓库代码并切换到仓库内工作( --depth 1 拉取第一层即可):
|
||||
|
||||
1. 优先使用 `https://atomgit.com/certd/certd/`。
|
||||
2. 如果 AtomGit 拉取失败,再使用 `https://github.com/certd/certd`。
|
||||
3. 拉取后重新检查上述特征,并读取 `.trae/skills/certd-online-plugin-dev/SKILL.md`。
|
||||
|
||||
开发插件时,参考 Certd 项目下已有内置插件实现:`packages/ui/certd-server/src/plugins/`。
|
||||
|
||||
## 插件来源
|
||||
|
||||
Certd 插件按来源分为三类:
|
||||
|
||||
- 内置插件:`type: "builtIn"`,随 Certd 安装包提供。可读取并在流水线中使用,不应通过本 Skill 修改或覆盖。
|
||||
- 市场插件:`type: "store"`,且存在 `appId` 或 `developerId`。它来自在线插件市场,可能尚未安装到本地;是否可修改只能以接口返回的 `editable` 为准。
|
||||
- 本地插件:`type: "store"`,但没有 `appId` 和 `developerId`。它是当前 Certd 实例本地创建、导入或复制的插件,可直接保存;发布到市场后会带上市场归属信息。
|
||||
|
||||
不要只根据 `type: "store"` 判断插件是否来自市场,也不要自行推断编辑权限;始终使用列表结果中的 `editable` 字段。
|
||||
|
||||
## 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`:
|
||||
|
||||
```javascript
|
||||
const response = await fetch(`${apiBase}/scoped/sys/ai/plugin/find`, {
|
||||
method: "POST",
|
||||
headers: { Authorization: token, "Content-Type": "application/json; charset=utf-8" },
|
||||
body: JSON.stringify({ keywords: ["nginx"], includeBuiltIn: true, includeStore: true }),
|
||||
})
|
||||
```
|
||||
|
||||
## UTF-8 保存
|
||||
|
||||
在 Windows 上,Node 直接以 UTF-8 读取 YAML 并用 `JSON.stringify` 发送;不要让 PowerShell 转发含中文的 YAML/JSON。保存后检查中文字段不含 `?`,再调用 `/scoped/sys/ai/plugin/find` 或 `/scoped/sys/ai/plugin/info` 验证。
|
||||
|
||||
## API 工作流
|
||||
|
||||
1. 使用 `/scoped/sys/ai/plugin/find` 查询插件和 Access,可通过 `keywords` 数组传递多个关键词。
|
||||
2. 查询结果包含 `editable`:
|
||||
- `editable: true`:允许当前 Agent 修改并保存。
|
||||
- `editable: false`:只能读取和使用,不能修改。
|
||||
3. 读取完整 YAML 时调用 `/scoped/sys/ai/plugin/export`。
|
||||
4. 所有插件保存统一调用 `/scoped/sys/ai/plugin/import`,并始终传递完整 YAML:
|
||||
- 新插件使用 `override: false`。
|
||||
- 已有插件使用 `override: true`;导入接口根据 `author` 和 `name` 定位并覆盖已有记录。
|
||||
5. 不使用 `/sys/plugin/add` 或 `/sys/plugin/update` 保存插件,避免保存路径分叉、字段丢失和 Windows 请求兼容性问题。
|
||||
6. 保存完成后重新调用 `/scoped/sys/ai/plugin/find` 或 `/scoped/sys/ai/plugin/info` 验证结果。
|
||||
|
||||
`/scoped/sys/ai/plugin/find` 会在一次请求中分别查询内置插件和 `store` 插件,再合并返回;`store` 插件需按上述字段区分市场插件与本地插件。
|
||||
|
||||
详细请求字段见 `references/certd-api.md`。
|
||||
|
||||
## Access 协作
|
||||
|
||||
开发 Task 或 DNS Provider 前,先用 `/scoped/sys/ai/plugin/find` 查询对应 Access:
|
||||
|
||||
1. 如果没有对应 Access,先创建 Access 插件,再创建业务插件。
|
||||
2. 如果已有 Access,先读取它的完整 YAML 和 `content`。
|
||||
3. 如果 Access 已提供所需 API/SDK,业务插件优先复用。
|
||||
4. 如果缺少能力:
|
||||
- `editable: true`:优先修改 Access,并先保存历史。
|
||||
- `editable: false`:在当前业务插件中实现必要的 API 调用。
|
||||
5. 业务插件通过 `dependPlugins` 声明 Access 依赖。
|
||||
|
||||
详细规则见 `references/access-development.md`。
|
||||
|
||||
## 本地历史
|
||||
|
||||
开发插件时,必须在当前工作区创建并使用 `.plugin-dev/{plugin-name}` 作为插件开发目录。历史记录、临时 YAML、脚本草稿和调试记录都放在该目录下。
|
||||
|
||||
每次修改插件前,必须将完整 YAML 保存到 `.plugin-dev/{plugin-name}/history/`:
|
||||
|
||||
```text
|
||||
.plugin-dev/{plugin-name}/xxxxx.yaml
|
||||
.plugin-dev/{plugin-name}/history/
|
||||
2026-08-02T12-30-00-before-edit.yaml
|
||||
2026-08-02T12-30-00-change.md
|
||||
```
|
||||
|
||||
保存要求:
|
||||
|
||||
- 修改前保存完整 YAML。
|
||||
- 修改后保存修改摘要。
|
||||
- 恢复前再次备份当前版本。
|
||||
- 不上传历史文件,不保存 Token、证书、私钥或真实授权值。
|
||||
|
||||
详细格式见 `references/local-history.md`。
|
||||
|
||||
## YAML 和脚本规范
|
||||
|
||||
插件始终以完整 YAML 传递和保存,脚本源码放在顶层 `content` 字段。
|
||||
|
||||
- 统一使用 `await _ctx.import(...)` 引用模块。
|
||||
- `"/@/..."` 表示以绝对路径引用 `server/src/` 下的模块。
|
||||
- 最后返回继承目标基类的 class。
|
||||
- 不使用 `import`、`export`、装饰器或独立源码文件语法。
|
||||
- 使用 `this.logger` 打印插件执行日志。
|
||||
- 使用 `this.ctx.http` 访问 HTTP 能力。
|
||||
- 失败时抛出 `Error`。
|
||||
|
||||
需要字段格式时读取 `references/online-yaml-format.md`。
|
||||
需要组件示例时读取 `references/component-examples.md`。
|
||||
|
||||
## 示例插件
|
||||
|
||||
开发对应类型插件前,先读取 `examples/` 下的示例:
|
||||
|
||||
- Access:`examples/DemoAccess.yaml`
|
||||
- 部署/Task:`examples/DemoDeploy.yaml`
|
||||
- DNS Provider:`examples/DemoDnsProvider.yaml`
|
||||
|
||||
示例是完整在线插件 YAML,重点参考 `input` 配置、依赖声明和 `content` 脚本结构。
|
||||
|
||||
## 子 Skill
|
||||
|
||||
- Task:`skills/task-plugin-dev/SKILL.md`
|
||||
- DNS Provider:`skills/dns-provider-dev/SKILL.md`
|
||||
- Access:`skills/access-plugin-dev/SKILL.md`
|
||||
|
||||
## 安全边界
|
||||
|
||||
Certd 会保存证书、私钥、API Token、云厂商密钥、SSH 凭据和其他敏感授权。
|
||||
|
||||
- 禁止读取、打印或上传真实授权值。
|
||||
- 禁止把认证 Token 写入插件、历史、日志或摘要。
|
||||
- 禁止读取无关的证书、私钥、Cookie、环境变量和系统设置。
|
||||
- 只使用脱敏示例数据和公开文档。
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Certd 在线插件开发"
|
||||
short_description: "通过 Certd API 使用 Codex 或 Trae 开发在线插件"
|
||||
default_prompt: "读取在线插件 YAML 和对应类型规范,按需求修改 content,保存历史记录后通过 Certd API 写回。"
|
||||
@@ -0,0 +1,37 @@
|
||||
name: DemoAccess
|
||||
icon: logos:airflow-icon
|
||||
title: Demo-授权插件示例 # 模块-插件名
|
||||
group: null
|
||||
desc: 这只是一个示例
|
||||
version: 1.0.0
|
||||
pluginType: access
|
||||
author: greper
|
||||
input:
|
||||
username:
|
||||
title: 用户名
|
||||
required: true
|
||||
encrypt: false
|
||||
component:
|
||||
name: a-input
|
||||
allowClear: true
|
||||
password:
|
||||
title: 密码
|
||||
required: true
|
||||
encrypt: true
|
||||
component:
|
||||
name: a-input
|
||||
allowClear: true
|
||||
showRunStrategy: false
|
||||
default:
|
||||
strategy:
|
||||
runStrategy: 1
|
||||
content: |
|
||||
|
||||
// 必须使用 await import 来引入模块
|
||||
const { BaseAccess } = await import("@certd/pipeline")
|
||||
// 需要返回一个继承BaseAccess的类
|
||||
return class DemoAccess extends BaseAccess {
|
||||
// 授权的字段,跟左边input一一对应
|
||||
username;
|
||||
password;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
name: DemoDeploy
|
||||
icon: logos:amp-icon
|
||||
title: Demo-部署插件示例 # 模块-插件名
|
||||
group: cdn
|
||||
desc: 这仅仅是一个示例
|
||||
version: 1.0.0
|
||||
pluginType: deploy
|
||||
author: greper
|
||||
input:
|
||||
cert:
|
||||
title: 前置任务证书
|
||||
helper: 请选择前置任务产生的证书
|
||||
component:
|
||||
name: output-selector # 前置任务输出选择组件
|
||||
vModel: modelValue
|
||||
from:
|
||||
- ':cert:' # 前置任务输出作为输入的筛选条件 , 格式 pluginType:valueType:keyName , 为空表示任意匹配
|
||||
required: true
|
||||
certDomains:
|
||||
title: 当前证书域名
|
||||
component:
|
||||
name: cert-domains-getter
|
||||
mergeScript: |
|
||||
return {
|
||||
component:{
|
||||
inputKey: ctx.compute(({form})=>{
|
||||
return form.cert
|
||||
}),
|
||||
}
|
||||
}
|
||||
required: true
|
||||
accessId:
|
||||
title: Access授权
|
||||
helper: xxxx的授权
|
||||
component:
|
||||
name: access-selector
|
||||
type: aliyun
|
||||
required: true
|
||||
key1:
|
||||
title: 输入示例1
|
||||
required: false
|
||||
key2:
|
||||
title: 可选项
|
||||
component:
|
||||
name: a-select
|
||||
vMode: value
|
||||
options:
|
||||
- value: '1'
|
||||
label: 选项1
|
||||
- value: '2'
|
||||
label: 选项2
|
||||
required: false
|
||||
showRunStrategy: false # 用户是否可以选择运行策略, 【默认不显示】
|
||||
default:
|
||||
strategy:
|
||||
runStrategy: 0 # 运行策略, 0 = 成功后跳过,1 = 正常运行
|
||||
content: >
|
||||
|
||||
// 要用await来import模块
|
||||
|
||||
const { AbstractTaskPlugin } = await _ctx.import("@certd/pipeline")
|
||||
|
||||
// 使用_ctx.import("/@/xxx.js") 以绝对路径引用模块,/@相当于根路径
|
||||
|
||||
const {AliyunAccess} = await _ctx.import("/@/plugins/plugin-lib/aliyun/access/index.js")
|
||||
|
||||
_ctx.logger.info("AliyunAccess:",AliyunAccess)
|
||||
|
||||
// 要返回一个继承AbstractTaskPlugin的class
|
||||
|
||||
return class DemoTask extends AbstractTaskPlugin {
|
||||
// 这里是插件的输入参数,对应左边的input配置
|
||||
cert;
|
||||
certDomains;
|
||||
accessId;
|
||||
key1;
|
||||
key2;
|
||||
// 编写执行方法
|
||||
async execute(){
|
||||
// 根据accessId获取授权配置
|
||||
const access = await this.getAccess(this.accessId)
|
||||
|
||||
//必须使用this.logger打印日志
|
||||
// this.logger.info("cert:",this.cert);
|
||||
this.logger.info("certDomains:",this.certDomains);
|
||||
this.logger.info("access:",access);
|
||||
this.logger.info("key1:",this.key1);
|
||||
this.logger.info("key2:",this.key2);
|
||||
this.logger.info("开始xxx部署任务")
|
||||
// 你的部署任务代码 【必须实现】
|
||||
// this.ctx里面有一些常用的方法类,比如utils、http、logger等
|
||||
const res = await this.ctx.http.request({url:"https://www.baidu.com"})
|
||||
if(res.error){
|
||||
//抛出异常,终止任务,否则将被判定为执行成功
|
||||
throw new Error("部署失败:"+res.message)
|
||||
}
|
||||
this.logger.info("执行成功")
|
||||
// this.outputName = xxxx //设置输出参数,可以被其他插件选择使用
|
||||
|
||||
// 此处可以返回 "skip" , 表示本次任务没有做任何操作和改变 , 配合runStrategy=1使用
|
||||
// return "skip"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
name: DemoDnsProvider
|
||||
icon: fa-solid:frog
|
||||
title: Demo-Dns提供商插件示例 # 模块-插件名
|
||||
desc: 这只是一个示例
|
||||
type: custom
|
||||
version: 1.0.0
|
||||
pluginType: dnsProvider
|
||||
author: greper
|
||||
accessType: aliyun # 需要的授权类型
|
||||
showRunStrategy: false
|
||||
default:
|
||||
strategy:
|
||||
runStrategy: 1
|
||||
content: |+
|
||||
|
||||
const { AbstractDnsProvider } = await _ctx.import("@certd/pipeline")
|
||||
return class DemoDnsProvider extends AbstractDnsProvider {
|
||||
// 创建dns解析记录,用于验证域名所有权 【必须实现】
|
||||
async createRecord(options) {
|
||||
/**
|
||||
* fullRecord: '_acme-challenge.test.example.com',
|
||||
* value: 一串uuid
|
||||
* type: 'TXT',
|
||||
* domain: 'example.com'
|
||||
*/
|
||||
const { fullRecord, value, type, domain } = options;
|
||||
const access = this.ctx.access
|
||||
this.logger.info('添加域名解析:', fullRecord, value, type, domain);
|
||||
// const record = await sdk.createRecord() // 调用对应的接口创建解析记录
|
||||
|
||||
//返回解析记录,用于后面清理
|
||||
return record
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除dns解析记录,清理申请痕迹【必须实现】
|
||||
* @param options
|
||||
*/
|
||||
async removeRecord(options) {
|
||||
const { fullRecord, value } = options.recordReq;
|
||||
const record = options.recordRes; // createRecord接口返回的record
|
||||
const access = this.ctx.access
|
||||
this.logger.info('删除域名解析:', fullRecord, value);
|
||||
if (!record) {
|
||||
this.logger.info('record为空,不执行删除');
|
||||
return;
|
||||
}
|
||||
const recordId = record.id;
|
||||
// 这里调用删除txt dns解析记录接口
|
||||
// sdk.removeRecord(recordId)
|
||||
this.logger.info("删除域名解析成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取域名列表 【可选,没有实现的话,不支持在certd域名管理中导入域名列表,不影响证书申请】
|
||||
* @param req
|
||||
* @returns
|
||||
*/
|
||||
async getDomainListPage(req: PageSearch): Promise<PageRes<DomainRecord>> {
|
||||
const pager = new Pager(req);
|
||||
const params = {
|
||||
RegionId: "cn-hangzhou",
|
||||
PageSize: pager.pageSize,
|
||||
PageNumber: pager.pageNo,
|
||||
};
|
||||
|
||||
const requestOption = {
|
||||
method: "POST",
|
||||
};
|
||||
|
||||
const ret = await this.client.request("DescribeDomains", params, requestOption);
|
||||
const list =
|
||||
ret.Domains?.Domain?.map(item => ({
|
||||
id: item.DomainId,
|
||||
domain: item.DomainName,
|
||||
})) || [];
|
||||
|
||||
return {
|
||||
list,
|
||||
total: ret.TotalCount,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取域名解析记录列表 【可选,没有实现的话,不支持在站点监控里面导入网址,不影响证书申请】
|
||||
* @param domain
|
||||
* @param req
|
||||
* @returns
|
||||
*/
|
||||
async getRecordListPage(domain: string, req: PageSearch): Promise<PageRes<DnsResolveRecord>> {
|
||||
const pager = new Pager(req);
|
||||
const params = {
|
||||
RegionId: "cn-hangzhou",
|
||||
DomainName: domain,
|
||||
PageSize: pager.pageSize,
|
||||
PageNumber: pager.pageNo,
|
||||
};
|
||||
|
||||
const requestOption = {
|
||||
method: "POST",
|
||||
};
|
||||
|
||||
const ret = await this.client.request("DescribeDomainRecords", params, requestOption);
|
||||
const rawList = ret.DomainRecords?.Record || [];
|
||||
const list = rawList.map(item => ({
|
||||
id: item.RecordId,
|
||||
hostRecord: item.RR,
|
||||
fullRecord: item.RR === "@" ? domain : `${item.RR}.${domain}`,
|
||||
type: item.Type,
|
||||
value: item.Value,
|
||||
}));
|
||||
|
||||
return {
|
||||
list,
|
||||
total: ret.TotalCount,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Access 开发规范
|
||||
|
||||
Access 插件负责保存授权配置,也负责封装平台 API/SDK,供 Task 和 DNS Provider 复用。
|
||||
|
||||
## 查询顺序
|
||||
|
||||
1. 调用 `/scoped/sys/ai/plugin/find`,使用 `pluginType: access`。
|
||||
2. 根据 `name`、`author`、`fullName` 识别目标 Access。
|
||||
3. 使用 `/scoped/sys/ai/plugin/export` 读取完整 YAML。
|
||||
4. 检查 `content` 中已经提供的方法。
|
||||
|
||||
## 修改规则
|
||||
|
||||
- Access 的 `editable: true` 时才允许修改。
|
||||
- 修改前先保存本地历史。
|
||||
- 优先把通用 API/SDK 能力放入 Access。
|
||||
- 业务插件通过 `dependPlugins` 依赖 Access。
|
||||
- `editable: false` 时不要尝试修改 Access,在业务插件内部实现必要的调用。
|
||||
- 不要在日志中打印完整授权配置。
|
||||
@@ -0,0 +1,70 @@
|
||||
# Certd API
|
||||
|
||||
以下接口都以前端生成提示词中的 API 地址为基础地址,并使用 `Authorization` 请求头。
|
||||
|
||||
## 查询插件
|
||||
|
||||
```http
|
||||
POST /scoped/sys/ai/plugin/find
|
||||
Content-Type: application/json
|
||||
Authorization: <token>
|
||||
```
|
||||
|
||||
请求示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"keywords": ["aliyun", "dns"],
|
||||
"pluginType": "access",
|
||||
"includeBuiltIn": true,
|
||||
"includeStore": true
|
||||
}
|
||||
```
|
||||
|
||||
接口会分别查询内置插件和 `store` 插件,再合并返回:
|
||||
|
||||
- `type: "builtIn"`:内置插件,不通过在线开发 API 修改。
|
||||
- `type: "store"` 且有 `appId` 或 `developerId`:市场插件。
|
||||
- `type: "store"` 且没有 `appId`、`developerId`:本地插件。
|
||||
|
||||
结果中的 `editable` 是唯一的编辑权限依据;不能只按插件来源判断是否可修改。
|
||||
列表结果只返回插件基础信息,不返回 `content`、`setting`、`sysSetting`、`metadata` 或 `extra`。需要完整 YAML 时再调用 `/scoped/sys/ai/plugin/export`。
|
||||
|
||||
## 读取插件信息
|
||||
|
||||
```http
|
||||
POST /scoped/sys/ai/plugin/info?id=12
|
||||
Authorization: <token>
|
||||
```
|
||||
|
||||
## 导出完整 YAML
|
||||
|
||||
```http
|
||||
POST /scoped/sys/ai/plugin/export
|
||||
Content-Type: application/json
|
||||
Authorization: <token>
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 12
|
||||
}
|
||||
```
|
||||
|
||||
## 保存插件
|
||||
|
||||
使用完整 YAML 导入:
|
||||
|
||||
```http
|
||||
POST /scoped/sys/ai/plugin/import
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"content": "完整 YAML",
|
||||
"override": true,
|
||||
"type": "store"
|
||||
}
|
||||
```
|
||||
|
||||
保存后重新调用 `/scoped/sys/ai/plugin/find` 或 `/scoped/sys/ai/plugin/info` 验证。
|
||||
@@ -0,0 +1,423 @@
|
||||
# Component Examples
|
||||
|
||||
Source: `packages/ui/certd-server/src/plugins`.
|
||||
Detected **30** distinct component names.
|
||||
|
||||
These snippets are extracted from existing Certd plugins. For online plugins, place the object under `input.<field>.component` in the YAML document.
|
||||
|
||||
## `EmailSelector`
|
||||
|
||||
Source: `packages/ui/certd-server/src/plugins/plugin-other/plugins/plugin-deploy-to-mail.ts`
|
||||
|
||||
```ts
|
||||
component: {
|
||||
name: "EmailSelector",
|
||||
vModel: "value",
|
||||
mode: "tags",
|
||||
}
|
||||
```
|
||||
|
||||
## `ParamsShow`
|
||||
|
||||
Source: `packages/ui/certd-server/src/plugins/plugin-template/email/plugin-common.ts`
|
||||
|
||||
```ts
|
||||
component: {
|
||||
name: "ParamsShow",
|
||||
params: [
|
||||
{ label: "标题", value: "title" },
|
||||
{ label: "内容", value: "content" },
|
||||
{ label: "URL", value: "url" },
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
## `RemoteSelect`
|
||||
|
||||
Source: `packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/getter/aliyun.ts`
|
||||
|
||||
```ts
|
||||
component: {
|
||||
name: "RemoteSelect",
|
||||
vModel: "value",
|
||||
pager: true,
|
||||
single: true,
|
||||
}
|
||||
```
|
||||
|
||||
## `a-alert`
|
||||
|
||||
Source: `packages/ui/certd-server/src/plugins/plugin-template/email/plugin-base.ts`
|
||||
|
||||
```ts
|
||||
component: {
|
||||
name: "a-alert",
|
||||
props: {
|
||||
type: "info",
|
||||
message: "在标题和内容模版中,通过${name}引用参数,例如: 感谢注册,您的注册验证码为:${code}",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## `a-auto-complete`
|
||||
|
||||
Source: `packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-ack/index.ts`
|
||||
|
||||
```ts
|
||||
component: {
|
||||
name: "a-auto-complete",
|
||||
vModel: "value",
|
||||
options: [
|
||||
{ value: "cn-qingdao", label: "华北1(青岛)" },
|
||||
{ value: "cn-beijing", label: "华北2(北京)" },
|
||||
{ value: "cn-zhangjiakou", label: "华北3(张家口)" },
|
||||
{ value: "cn-huhehaote", label: "华北5(呼和浩特)" },
|
||||
{ value: "cn-wulanchabu", label: "华北6(乌兰察布)" },
|
||||
{ value: "cn-hangzhou", label: "华东1(杭州)" },
|
||||
{ value: "cn-shanghai", label: "华东2(上海)" },
|
||||
{ value: "cn-shenzhen", label: "华南1(深圳)" },
|
||||
{ value: "cn-guangzhou", label: "华南3(广州)" },
|
||||
{ value: "ap-southeast-2", label: "澳大利亚(悉尼)" },
|
||||
{ value: "ap-southeast-3", label: "马来西亚(吉隆坡)" },
|
||||
{ value: "ap-northeast-1", label: "日本(东京)" },
|
||||
{ value: "cn-chengdu", label: "西南1(成都)" },
|
||||
{ value: "ap-southeast-1", label: "新加坡" },
|
||||
{ value: "ap-southeast-5", label: "印度尼西亚(雅加达)" },
|
||||
{ value: "cn-hongkong", label: "中国香港" },
|
||||
{ value: "eu-central-1", label: "德国(法兰克福)" },
|
||||
{ value: "us-east-1", label: "美国(弗吉尼亚)" },
|
||||
{ value: "us-west-1", label: "美国(硅谷)" },
|
||||
{ value: "eu-west-1", label: "英国(伦敦)" },
|
||||
{ value: "me-east-1", label: "阿联酋(迪拜)" },
|
||||
//金融云
|
||||
{ value: "cn-beijing-finance-1", label: "华北2 金融云(邀测)" },
|
||||
{ value: "cn-hangzhou-finance", label: "华东1 金融云" },
|
||||
{ value: "cn-shanghai-finance-1", label: "华东2 金融云" },
|
||||
{ value: "cn-shenzhen-finance-1", label: "华南1 金融云" },
|
||||
],
|
||||
placeholder: "集群所属大区",
|
||||
}
|
||||
```
|
||||
|
||||
## `a-input`
|
||||
|
||||
Source: `packages/ui/certd-server/src/plugins/plugin-admin/plugin-db-backup.ts`
|
||||
|
||||
```ts
|
||||
component: {
|
||||
name: "a-input",
|
||||
type: "value",
|
||||
placeholder: `默认${defaultBackupDir}`,
|
||||
}
|
||||
```
|
||||
|
||||
## `a-input-number`
|
||||
|
||||
Source: `packages/ui/certd-server/src/plugins/plugin-acepanel/access.ts`
|
||||
|
||||
```ts
|
||||
component: {
|
||||
name: "a-input-number",
|
||||
vModel: "value",
|
||||
}
|
||||
```
|
||||
|
||||
## `a-input-password`
|
||||
|
||||
Source: `packages/ui/certd-server/src/plugins/plugin-51dns/access.ts`
|
||||
|
||||
```ts
|
||||
component: {
|
||||
name: "a-input-password",
|
||||
vModel: "value",
|
||||
placeholder: "密码",
|
||||
}
|
||||
```
|
||||
|
||||
## `a-radio-group`
|
||||
|
||||
Source: `packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-esa/index.ts`
|
||||
|
||||
```ts
|
||||
component: {
|
||||
name: "a-radio-group",
|
||||
vModel: "value",
|
||||
options: [
|
||||
{ label: "边缘证书", value: "edge" },
|
||||
{ label: "SaaS证书", value: "saas" },
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
## `a-select`
|
||||
|
||||
Source: `packages/ui/certd-server/src/plugins/plugin-admin/plugin-db-backup.ts`
|
||||
|
||||
```ts
|
||||
component: {
|
||||
name: "a-select",
|
||||
options: [
|
||||
{ label: "本地复制", value: "local" },
|
||||
{ label: "oss上传(推荐)", value: "oss" },
|
||||
{ label: "ssh上传(请使用oss上传方式)", value: "ssh", disabled: true },
|
||||
],
|
||||
placeholder: "",
|
||||
}
|
||||
```
|
||||
|
||||
## `a-switch`
|
||||
|
||||
Source: `packages/ui/certd-server/src/plugins/plugin-acepanel/access.ts`
|
||||
|
||||
```ts
|
||||
component: {
|
||||
name: "a-switch",
|
||||
vModel: "checked",
|
||||
}
|
||||
```
|
||||
|
||||
## `a-textarea`
|
||||
|
||||
Source: `packages/ui/certd-server/src/plugins/plugin-admin/plugin-script.ts`
|
||||
|
||||
```ts
|
||||
component: {
|
||||
name: "a-textarea",
|
||||
vModel: "value",
|
||||
rows: 10,
|
||||
style: "background-color: #000c17;color: #fafafa;",
|
||||
}
|
||||
```
|
||||
|
||||
## `access-selector`
|
||||
|
||||
Source: `packages/ui/certd-server/src/plugins/plugin-acepanel/plugins/plugin-deploy-to-website.ts`
|
||||
|
||||
```ts
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "acepanel",
|
||||
}
|
||||
```
|
||||
|
||||
## `api-test`
|
||||
|
||||
Source: `packages/ui/certd-server/src/plugins/plugin-51dns/access.ts`
|
||||
|
||||
```ts
|
||||
component: {
|
||||
name: "api-test",
|
||||
action: "TestRequest",
|
||||
}
|
||||
```
|
||||
|
||||
## `cert-info-updater`
|
||||
|
||||
Source: `packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/custom/index.ts`
|
||||
|
||||
```ts
|
||||
component: {
|
||||
name: "cert-info-updater",
|
||||
vModel: "modelValue",
|
||||
}
|
||||
```
|
||||
|
||||
## `dns-provider-selector`
|
||||
|
||||
Source: `packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/apply.ts`
|
||||
|
||||
```ts
|
||||
component: {
|
||||
name: "dns-provider-selector",
|
||||
}
|
||||
```
|
||||
|
||||
## `domain-selector`
|
||||
|
||||
Source: `packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/base-convert.ts`
|
||||
|
||||
```ts
|
||||
component: {
|
||||
name: "domain-selector",
|
||||
vModel: "value",
|
||||
mode: "tags",
|
||||
// open: false,
|
||||
placeholder: "请输入证书域名/IP,比如:foo.com , *.foo.com , *.sub.foo.com , *.bar.com , 123.123.123.123",
|
||||
tokenSeparators: [",", " ", ",", "、", "|"],
|
||||
search: true,
|
||||
pager: true,
|
||||
}
|
||||
```
|
||||
|
||||
## `domains-verify-plan-editor`
|
||||
|
||||
Source: `packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/apply.ts`
|
||||
|
||||
```ts
|
||||
component: {
|
||||
name: "domains-verify-plan-editor",
|
||||
}
|
||||
```
|
||||
|
||||
## `email-selector`
|
||||
|
||||
Source: `packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/base.ts`
|
||||
|
||||
```ts
|
||||
component: {
|
||||
name: "email-selector",
|
||||
vModel: "value",
|
||||
}
|
||||
```
|
||||
|
||||
## `fs-icon-selector`
|
||||
|
||||
Source: `packages/ui/certd-server/src/plugins/plugin-oauth/oidc/plugin-oidc.ts`
|
||||
|
||||
```ts
|
||||
component: {
|
||||
name: "fs-icon-selector",
|
||||
vModel: "modelValue",
|
||||
iconSets: IconSets,
|
||||
}
|
||||
```
|
||||
|
||||
## `icon-select`
|
||||
|
||||
Source: `packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/apply.ts`
|
||||
|
||||
```ts
|
||||
component: {
|
||||
name: "icon-select",
|
||||
vModel: "value",
|
||||
options: [
|
||||
{ value: "letsencrypt", label: "Let's Encrypt(免费,新手推荐,支持IP证书)", icon: "simple-icons:letsencrypt" },
|
||||
{ value: "google", label: "Google(免费)", icon: "flat-color-icons:google" },
|
||||
{ value: "zerossl", label: "ZeroSSL(免费)", icon: "emojione:digit-zero" },
|
||||
{ value: "litessl", label: "litessl(免费)", icon: "roentgen:free" },
|
||||
{ value: "sslcom", label: "SSL.com(仅主域名和www免费)", icon: "la:expeditedssl" },
|
||||
{ value: "letsencrypt_staging", label: "Let's Encrypt测试环境(仅供测试)", icon: "simple-icons:letsencrypt" },
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
## `input-password`
|
||||
|
||||
Source: `packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/base-convert.ts`
|
||||
|
||||
```ts
|
||||
component: {
|
||||
name: "input-password",
|
||||
vModel: "value",
|
||||
}
|
||||
```
|
||||
|
||||
## `notification-selector`
|
||||
|
||||
Source: `packages/ui/certd-server/src/plugins/plugin-github/plugins/plugin-check-release.ts`
|
||||
|
||||
```ts
|
||||
component: {
|
||||
name: "notification-selector",
|
||||
select: {
|
||||
mode: "tags",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## `output-selector`
|
||||
|
||||
Source: `packages/ui/certd-server/src/plugins/plugin-acepanel/plugins/plugin-deploy-to-website.ts`
|
||||
|
||||
```ts
|
||||
component: {
|
||||
name: "output-selector",
|
||||
from: [...CertApplyPluginNames],
|
||||
}
|
||||
```
|
||||
|
||||
## `pem-input`
|
||||
|
||||
Source: `packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/custom/index.ts`
|
||||
|
||||
```ts
|
||||
component: {
|
||||
name: "pem-input",
|
||||
vModel: "modelValue",
|
||||
textarea: {
|
||||
rows: 4,
|
||||
placeholder: "-----BEGIN CERTIFICATE-----\n...\n...\n-----END CERTIFICATE-----",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## `refresh-input`
|
||||
|
||||
Source: `packages/ui/certd-server/src/plugins/plugin-cert/access/acme-account-access.ts`
|
||||
|
||||
```ts
|
||||
component: {
|
||||
name: "refresh-input",
|
||||
action: "GenerateAccount",
|
||||
buttonText: "生成ACME账号",
|
||||
successMessage: "ACME账号已生成,请保存授权配置",
|
||||
type: "textarea",
|
||||
rows: 4,
|
||||
}
|
||||
```
|
||||
|
||||
## `remote-auto-complete`
|
||||
|
||||
Source: `packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-apig/index.ts`
|
||||
|
||||
```ts
|
||||
component: {
|
||||
name: "remote-auto-complete",
|
||||
}
|
||||
```
|
||||
|
||||
## `remote-select`
|
||||
|
||||
Source: `packages/ui/certd-server/src/plugins/plugin-nginx-proxy-manager/plugins/plugin-deploy-to-proxy-hosts.ts`
|
||||
|
||||
```ts
|
||||
component: {
|
||||
name: "remote-select",
|
||||
vModel: "value",
|
||||
mode: "tags",
|
||||
type: "plugin",
|
||||
action: "onGetProxyHostOptions",
|
||||
search: true,
|
||||
pager: false,
|
||||
single: false,
|
||||
watches: ["certDomains", "accessId"],
|
||||
}
|
||||
```
|
||||
|
||||
## `remote-tree-select`
|
||||
|
||||
Source: `packages/ui/certd-server/src/plugins/plugin-tencent/plugin/refresh-cert/index.ts`
|
||||
|
||||
```ts
|
||||
component: {
|
||||
name: "remote-tree-select",
|
||||
vModel: "value",
|
||||
action: TencentRefreshCert.prototype.onGetRegionsTree.name,
|
||||
pager: false,
|
||||
search: false,
|
||||
watches: ["certList"],
|
||||
}
|
||||
```
|
||||
|
||||
## `synology-device-id-getter`
|
||||
|
||||
Source: `packages/ui/certd-server/src/plugins/plugin-plus/synology/access.ts`
|
||||
|
||||
```ts
|
||||
component: {
|
||||
placeholder: "设备ID",
|
||||
name: "synology-device-id-getter",
|
||||
type: "access",
|
||||
typeName: "synology",
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,25 @@
|
||||
# 本地历史记录
|
||||
|
||||
历史记录和开发临时文件只保存在 Codex/Trae 当前工作区的 `.tmp/online-plugin-dev/` 下,不调用 Certd 后端历史接口。
|
||||
|
||||
目录格式:
|
||||
|
||||
```text
|
||||
.tmp/online-plugin-dev/
|
||||
history/
|
||||
plugin-12/
|
||||
2026-08-02T12-30-00-before-edit.yaml
|
||||
2026-08-02T12-30-00-change.md
|
||||
work/
|
||||
plugin-12.yaml
|
||||
plugin-12-content.js
|
||||
```
|
||||
|
||||
要求:
|
||||
|
||||
- 修改前保存完整 YAML。
|
||||
- 临时 YAML、脚本草稿、调试记录都放到 `.tmp/online-plugin-dev/` 下,不散落到项目目录。
|
||||
- `change.md` 只记录插件 ID、版本、时间和脱敏修改摘要。
|
||||
- 不保存 Token、证书、私钥、Cookie、环境变量和真实授权值。
|
||||
- 恢复历史版本前先备份当前 YAML。
|
||||
- 恢复后通过 `/scoped/sys/ai/plugin/import` 写回 Certd。
|
||||
@@ -0,0 +1,66 @@
|
||||
## 在线插件 YAML
|
||||
|
||||
在线插件始终以一个完整 YAML 文档传递、编辑、导入和导出。脚本源码必须放在顶层 `content` 字段中,不要输出独立的 `.ts` 文件,也不要使用 JSON Patch。
|
||||
|
||||
常用字段:
|
||||
|
||||
```yaml
|
||||
name: DemoTask
|
||||
author: demo
|
||||
title: Demo 任务
|
||||
desc: 插件说明
|
||||
icon: clarity:plugin-line
|
||||
pluginType: task
|
||||
group: other
|
||||
version: 1.0.0
|
||||
input:
|
||||
cert: # 输入字段定义
|
||||
title: 域名证书
|
||||
required: true # 是否必填
|
||||
component: # 输入字段组件
|
||||
name: cert-select # 前端vue组件名称
|
||||
output: # 【可选】输出字段定义
|
||||
cert:
|
||||
title: 证书 #输出字段标题
|
||||
type: "cert" # 输出字段类型
|
||||
dependPlugins: # 依赖的插件
|
||||
# - access:certd/DemoAccess: *
|
||||
dependPackages: # 依赖的nodejs包
|
||||
# xxx: ^1.0.0
|
||||
vip: "free" # 默认免费,plus 需要专业版
|
||||
showRunStrategy: false # 【默认不显示】
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: 0, # 运行策略, 0 = 成功后跳过,1 = 正常运行
|
||||
},
|
||||
}
|
||||
content: |
|
||||
const { AbstractTaskPlugin } = await _ctx.import("@certd/pipeline")
|
||||
const { DemoAccess } = await _ctx.import("/@/plugins/plugin-lib/demo/access/index.js")
|
||||
_ctx.logger.info("DemoAccess:", DemoAccess)
|
||||
return class DemoTask extends AbstractTaskPlugin {
|
||||
async execute() {
|
||||
this.logger.info("执行成功")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `content` 规则
|
||||
|
||||
- 统一使用 `await _ctx.import(...)` 加载模块。
|
||||
- 使用 `_ctx.import("/@/...")` 以绝对路径加载 `certd-server/src/` 下的模块,`/@` 代表 `certd-server/src` 根路径。
|
||||
- 需要确认模块时使用 `_ctx.logger`,插件执行过程使用 `this.logger`。
|
||||
- 最后返回继承目标基类的 class。
|
||||
- 不使用 `import`、`export`、装饰器或独立源码文件语法。
|
||||
- 输入字段在 class 中声明为同名属性,并与 YAML 的 `input` 配置保持一致。
|
||||
- HTTP 使用 `this.ctx.http`;
|
||||
- 不要使用 `console.log`。
|
||||
- 读取授权使用 `await this.getAccess(accessId)` 或目标基类规定的授权方式。
|
||||
- 失败时抛出 `Error`,不要吞掉错误。
|
||||
|
||||
### 编辑规则
|
||||
|
||||
- 修改已有插件时保留 `name`、`author`、`pluginType` 和已有兼容字段。
|
||||
- 只修改需求涉及的字段,避免删除未知的 YAML 字段。
|
||||
- 脚本过长时仍放在同一个 `content` block scalar 中。
|
||||
- 提交前检查 YAML 可解析、`content` 非空、版本和插件类型没有被意外修改。
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
name: certd-online-access-plugin-dev
|
||||
description: 用于开发 Certd 在线 Access 插件。输出完整 YAML,content 中返回继承 BaseAccess 的 class,并在 input 中声明授权字段。
|
||||
---
|
||||
|
||||
# 在线 Access 插件
|
||||
|
||||
读取父 Skill 的 `references/online-yaml-format.md`。不要沿用代码版 `@IsAccess`、`@AccessInput` 装饰器和独立 TypeScript 文件。
|
||||
|
||||
## 输出结构
|
||||
|
||||
- `pluginType` 使用 `access`。
|
||||
- `input` 中声明用户需要填写的授权字段。
|
||||
- 敏感字段在 input 中设置加密或密码类组件。
|
||||
- `content` 中实现授权 class 和 API 方法。
|
||||
|
||||
## `content` 模板
|
||||
|
||||
```javascript
|
||||
const { BaseAccess } = await _ctx.import("@certd/pipeline")
|
||||
|
||||
return class DemoAccess extends BaseAccess {
|
||||
demoKeyId
|
||||
demoKeySecret
|
||||
|
||||
async onTestRequest() {
|
||||
await this.getDomainList({ searchKey: "" })
|
||||
return "ok"
|
||||
}
|
||||
|
||||
async getDomainList(req) {
|
||||
this.logger.info("获取域名列表", { searchKey: req.searchKey })
|
||||
const res = await this.ctx.http.request({
|
||||
url: "https://api.example.com/domains",
|
||||
method: "GET",
|
||||
params: { keyword: req.searchKey },
|
||||
})
|
||||
if (res.error) {
|
||||
throw new Error(`获取域名列表失败: ${res.message}`)
|
||||
}
|
||||
return {
|
||||
total: res.data?.total || 0,
|
||||
list: res.data?.list || [],
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 编写要求
|
||||
|
||||
- 统一使用 `await _ctx.import(...)` 加载模块。
|
||||
- 使用 `_ctx.import("/@/...")` 通过绝对路径加载 `server/src/` 下的模块,`/@` 代表 `server/src` 根路径。
|
||||
- 需要记录模块加载信息时使用 `_ctx.logger`,访问执行日志使用 `this.logger`。
|
||||
- 返回继承 `BaseAccess` 的 class,不使用装饰器。
|
||||
- class 属性名必须与 YAML `input` 字段一致。
|
||||
- 所有敏感授权值只通过 `this` 和 Certd 授权上下文使用,不打印真实值。
|
||||
- `onTestRequest` 应调用实际 API 方法并在失败时抛出异常。
|
||||
- 对外 API 方法应统一处理分页、错误和返回字段。
|
||||
- 使用 `this.logger` 或框架提供的 logger,禁止 `console.log`。
|
||||
- 一般将API接口方法封装到Access中,其他plugin调用access的方法当做client sdk使用
|
||||
@@ -0,0 +1,71 @@
|
||||
---
|
||||
name: certd-online-dns-provider-dev
|
||||
description: 用于开发 Certd 在线 DNS Provider 插件。输出完整 YAML,content 中返回继承 AbstractDnsProvider 的 class。
|
||||
---
|
||||
|
||||
# 在线 DNS Provider 插件
|
||||
|
||||
读取父 Skill 的 `references/online-yaml-format.md`。不要沿用旧版 `@IsDnsProvider` 装饰器和独立 TypeScript 文件。
|
||||
|
||||
## 输出结构
|
||||
|
||||
- `pluginType` 使用 `dnsProvider`。
|
||||
- `input` 中配置授权选择、域名或平台所需的参数。
|
||||
- `content` 中实现创建和删除 DNS 记录的 class。
|
||||
|
||||
## `content` 模板
|
||||
|
||||
```javascript
|
||||
const { AbstractDnsProvider } = await _ctx.import("@certd/pipeline")
|
||||
const { DemoAccess } = await _ctx.import("/@/plugins/plugin-lib/demo/access/index.js")
|
||||
_ctx.logger.info("DemoAccess:", DemoAccess)
|
||||
|
||||
return class DemoDnsProvider extends AbstractDnsProvider {
|
||||
accessId
|
||||
|
||||
async onInstance() {
|
||||
this.access = await this.getAccess(this.accessId)
|
||||
}
|
||||
|
||||
async createRecord(options) {
|
||||
const { fullRecord, value, type, domain } = options
|
||||
this.logger.info("添加 DNS 记录", { fullRecord, type, domain })
|
||||
const res = await this.ctx.http.request({
|
||||
url: "https://api.example.com/dns/records",
|
||||
method: "POST",
|
||||
data: { fullRecord, value, type, domain },
|
||||
})
|
||||
if (res.error) {
|
||||
throw new Error(`创建 DNS 记录失败: ${res.message}`)
|
||||
}
|
||||
return res.data
|
||||
}
|
||||
|
||||
async removeRecord(options) {
|
||||
const { fullRecord, value, domain } = options.recordReq
|
||||
const res = await this.ctx.http.request({
|
||||
url: "https://api.example.com/dns/records",
|
||||
method: "DELETE",
|
||||
data: { fullRecord, value, domain },
|
||||
})
|
||||
if (res.error) {
|
||||
this.logger.warn("删除 DNS 记录失败", res.message)
|
||||
return
|
||||
}
|
||||
this.logger.info("删除 DNS 记录成功", fullRecord)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 编写要求
|
||||
|
||||
- 统一使用 `await _ctx.import(...)` 加载模块。
|
||||
- 使用 `_ctx.import("/@/...")` 通过绝对路径加载 `server/src/` 下的模块,`/@` 代表 `server/src` 根路径。
|
||||
- 需要记录模块加载信息时使用 `_ctx.logger`。
|
||||
- 返回继承 `AbstractDnsProvider` 的 class。
|
||||
- `createRecord` 必须返回删除时需要的记录信息。
|
||||
- `removeRecord` 使用 `options.recordReq` 和 `options.recordRes`。
|
||||
- 只处理业务 API 所需的 TXT 记录参数,不在日志中输出授权密钥。
|
||||
- 网络失败、授权失败和 API 业务失败要有明确日志;创建失败必须抛出异常。
|
||||
- 保持创建和删除幂等,避免清理失败阻断无关流程。
|
||||
- 一般将API接口方法封装到Access中,其他plugin使用access当做client sdk使用
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: certd-online-task-plugin-dev
|
||||
description: 用于开发 Certd 在线 Task 插件。输出完整 YAML,脚本源码放在 content 字段中,继承 AbstractTaskPlugin 并返回插件 class。
|
||||
---
|
||||
|
||||
# 在线 Task 插件
|
||||
|
||||
读取父 Skill 的 `references/online-yaml-format.md`。在线插件不是原来的装饰器源码文件模式。
|
||||
|
||||
## 输出结构
|
||||
|
||||
- `pluginType` 使用 `task`。
|
||||
- 保留或填写 `name`、`author`、`title`、`desc`、`icon`、`group`、`version`。
|
||||
- 输入配置放在 YAML 的 `input` 字段。
|
||||
- 执行脚本放在 YAML 顶层 `content` 字段。
|
||||
|
||||
|
||||
## runStrategy
|
||||
|
||||
整个流水线每次运行,会记录每个任务的运行结果状态,如果上次任务执行成功,且 runStrategy 为成功后跳过,那么下次运行时会跳过该任务。
|
||||
|
||||
|
||||
- `showRunStrategy`: false 【默认不显示】
|
||||
- 任务配置时是否显示 runStrategy 字段,用于指定任务的执行策略。
|
||||
- `default.strategy.runStrategy`: 0 【默认成功后跳过】
|
||||
- 0 表示成功后跳过,1 表示正常运行。
|
||||
- 设置任务的默认运行策略runStrategy。
|
||||
|
||||
当证书申请任务,申请证书成功后,会清空后续任务状态。此时后续`runStrategy = 0` 的任务,会重新执行。
|
||||
这样可以保证相同的证书不会重复部署。
|
||||
|
||||
在其他场景的部署任务,可能希望每次运行都检查是否需要部署,如果即将过期就部署,没有过期就跳过。
|
||||
此时可以设置 `default.strategy.runStrategy = 1`,然后在`execute`方法中,如果没有过期,就返回 `"skip"`。
|
||||
|
||||
## `content` 模板
|
||||
|
||||
```javascript
|
||||
const { AbstractTaskPlugin } = await _ctx.import("@certd/pipeline")
|
||||
const { DemoAccess } = await _ctx.import("/@/plugins/plugin-lib/demo/access/index.js")
|
||||
_ctx.logger.info("DemoAccess:", DemoAccess)
|
||||
|
||||
return class DemoTask extends AbstractTaskPlugin {
|
||||
cert
|
||||
certDomains
|
||||
accessId
|
||||
|
||||
async execute() {
|
||||
const access = await this.getAccess(this.accessId)
|
||||
this.logger.info("开始执行任务", { access })
|
||||
const res = await this.ctx.http.request({
|
||||
url: "https://api.example.com",
|
||||
})
|
||||
if (res.error) {
|
||||
// 抛异常让任务执行失败
|
||||
throw new Error(`任务执行失败: ${res.message}`)
|
||||
}
|
||||
this.logger.info("执行成功")
|
||||
// 还可以返回 "skip" 表示当前任务没有执行任何操作
|
||||
// 一般与 runStrategy = 1 配合使用。 (1=正常运行,0表示成功后跳过)
|
||||
// 比如检查无需更新,时就 返回 "skip"
|
||||
// return "skip"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 编写要求
|
||||
|
||||
- 统一用 `await _ctx.import(...)` 加载模块。
|
||||
- 使用 `_ctx.import("/@/...")` 通过绝对路径加载 `server/src/` 下的模块,`/@` 代表 `server/src` 根路径。
|
||||
- 需要记录模块加载信息时使用 `_ctx.logger`。
|
||||
- 返回继承 `AbstractTaskPlugin` 的 class,不写 `export class`。
|
||||
- class 属性名必须对应 `input` 配置的字段名。
|
||||
- 用 `this.logger` 记录关键步骤。
|
||||
- 用 `this.ctx.http` 请求远程 API,用 `this.getAccess` 获取授权。
|
||||
- 外部 API 返回失败或业务失败时抛出异常。
|
||||
- 对重复执行保持幂等,避免把真实 Token、证书和私钥写入日志。
|
||||
- 修改完成后把整个 YAML 通过 Certd `/scoped/sys/ai/plugin/import` 保存。
|
||||
- 一般将API接口方法封装到Access中,其他plugin使用access当做client sdk使用
|
||||
@@ -454,3 +454,4 @@ new TencentDnsProvider();
|
||||
3. **记录结构**:定义适合对应云平台的记录数据结构,至少包含 id 字段用于删除记录。
|
||||
4. **日志输出**:使用 `this.logger` 输出日志,而不是 `console`,参数文本化,不要传对象,否则会输出`[object Object]}`。
|
||||
5. **错误处理**:API 调用失败时应抛出明确的错误信息。
|
||||
6. **旧版数据兼容**: 新增插件参数,必须要考虑旧版数据兼容,比如新增一个deployType参数,有两种值:`default`和`custom`,需要在使用时判空,走旧版逻辑。
|
||||
|
||||
@@ -211,6 +211,9 @@ export class DemoTest extends AbstractTaskPlugin {
|
||||
//当以下参数变化时,触发获取选项
|
||||
watches: ['certDomains', 'accessId'],
|
||||
required: true,
|
||||
single: false, // 是否单选
|
||||
pager: true, // 是否卡其分页查询
|
||||
search: true, // 是否开启搜索
|
||||
})
|
||||
)
|
||||
siteName!: string | string[];
|
||||
@@ -260,9 +263,12 @@ export class DemoTest extends AbstractTaskPlugin {
|
||||
throw new Error('请选择Access授权');
|
||||
}
|
||||
|
||||
const pager = new Pager(req);
|
||||
|
||||
// @ts-ignore
|
||||
const access = await this.getAccess(this.accessId);
|
||||
|
||||
//
|
||||
// 根据接口情况是否支持翻页查询,和关键字查询, 传递对应的参数,pager.pageNo,pager.pageSize, req.searchKey
|
||||
// const siteRes = await access.GetDomainList(req);
|
||||
//以下是模拟数据
|
||||
const siteRes = [
|
||||
@@ -275,12 +281,12 @@ export class DemoTest extends AbstractTaskPlugin {
|
||||
return {
|
||||
value: item.siteName,
|
||||
label: item.siteName,
|
||||
domain: item.siteName,
|
||||
domain: item.siteName, //这里必须要包含domain 否则后面分组时候全部分配到未匹配中
|
||||
};
|
||||
});
|
||||
//将站点域名名称根据证书域名进行匹配分组,分成匹配的和不匹配的两组选项,返回给前端,供用户选择
|
||||
return {
|
||||
list: optionsUtils.buildGroupOptions(options, this.certDomains),
|
||||
list: optionsUtils.buildGroupOptions(options, this.certDomains), //分组后的列表
|
||||
total: siteRes.length,
|
||||
};
|
||||
}
|
||||
@@ -635,6 +641,7 @@ new AliyunOSSDeploy();
|
||||
4. **日志输出**:使用 `this.logger` 输出日志,而不是 `console`,参数文本化,不要传对象,否则会输出`[object Object]}`。
|
||||
5. **错误处理**:执行过程中的错误应被捕获并记录。
|
||||
6. **授权获取**:使用 `this.getAccess(accessId)` 获取授权信息。
|
||||
7. **旧版数据兼容**: 新增@TaskInput注解的插件参数,必须要考虑旧版数据兼容,比如新增一个deployType参数,有两种值:`default`和`custom`,需要在使用时判空,走旧版逻辑。
|
||||
|
||||
## 部署逻辑注意事项
|
||||
|
||||
|
||||
Vendored
+28
@@ -87,6 +87,34 @@
|
||||
"plus_use_prod": "false",
|
||||
"PLUS_SERVER_BASE_URL": "http://127.0.0.1:11007"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "server-local-plus2",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"cwd": "${workspaceFolder}/packages/ui/certd-server",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "dev-localplus2"],
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"env": {
|
||||
"plus_use_prod": "false",
|
||||
"PLUS_SERVER_BASE_URL": "http://127.0.0.1:11007"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "server-local-comm",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"cwd": "${workspaceFolder}/packages/ui/certd-server",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "dev-localcomm"],
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"env": {
|
||||
"plus_use_prod": "false",
|
||||
"PLUS_SERVER_BASE_URL": "http://127.0.0.1:11007"
|
||||
}
|
||||
}
|
||||
],
|
||||
"compounds": [
|
||||
|
||||
@@ -1,71 +1,284 @@
|
||||
# Certd 开发 Agent 上下文
|
||||
|
||||
这个文件是给在本仓库工作的开发 agent 看的常驻项目说明。进入仓库后先读本文,再按任务读取对应导航或规则文件,避免每次重新全量扫描项目。
|
||||
|
||||
仓库代码导航、目录地图、常用入口和参考文件见 `.codex/repo-map.md`。更细的开发规则拆在 `.codex/agent-rules/` 下;本文只保留最高优先级的规则、架构边界和工作方式。
|
||||
进入仓库后先读本文。本文同时包含常驻规则、仓库地图、常用入口和验证命令;不要依赖分散规则文件。
|
||||
思考时也要使用中文。
|
||||
|
||||
## 项目定位
|
||||
|
||||
Certd 是支持私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web 管理台和后端服务,用于证书申请、续期、部署、监控、通知和开放 API 集成。
|
||||
Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web 管理台和后端服务,用于证书申请、续期、部署、监控、通知和开放 API 集成。
|
||||
|
||||
核心产品模型是“证书流水线”:
|
||||
核心模型是“证书流水线”:
|
||||
|
||||
- 通过 ACME 申请证书
|
||||
- 使用 DNS-01、HTTP-01、CNAME 代理或服务商集成完成域名验证
|
||||
- 将证书转换或导出为 pem、pfx、der、jks、p7b 等格式
|
||||
- 部署证书到主机、Nginx、Kubernetes、CDN、云厂商、面板等目标
|
||||
- 通知用户,并监控站点证书过期时间
|
||||
- 通过 ACME 申请证书。
|
||||
- 完成 DNS-01、HTTP-01、CNAME 代理或服务商验证。
|
||||
- 导出 pem、pfx、der、jks、p7b 等证书格式。
|
||||
- 部署到主机、Nginx、Kubernetes、CDN、云厂商、面板等目标。
|
||||
- 通知用户,并监控站点证书过期时间。
|
||||
|
||||
系统会保存证书、云厂商凭据、SSH 信息、API Key 等敏感数据,始终按私有化/本地部署产品处理,避免泄露本地数据和配置。
|
||||
|
||||
## 必读索引
|
||||
|
||||
- `.codex/repo-map.md`:仓库结构、后端/前端入口、流水线与插件地图、验证命令
|
||||
- `.codex/agent-rules/backend.md`:后端、数据库迁移、文件上传、service/事务约定
|
||||
- `.codex/agent-rules/frontend.md`:前端、Fast Crud、弹窗表单、格式化和禁跑命令
|
||||
- `.codex/agent-rules/plugins.md`:流水线、插件归属、ACME/EAB、插件开发技能
|
||||
- `.codex/agent-rules/testing.md`:测试优先策略、单测位置、ESM mock、聚焦验证
|
||||
- `.codex/agent-rules/coding-style.md`:注释、可读性、DRY、单一职责等通用代码风格
|
||||
系统会保存证书、云厂商凭据、SSH 信息、API Key 等敏感数据。始终按私有化/本地部署产品处理,避免泄露本地数据和配置。
|
||||
|
||||
## 仓库边界
|
||||
|
||||
这是一个 pnpm + lerna 的 monorepo。核心定位:
|
||||
|
||||
- `packages/ui/certd-server`:后端服务
|
||||
- `packages/ui/certd-client`:前端 Web 管理台
|
||||
- `packages/core/pipeline`:流水线核心
|
||||
- `packages/core/acme-client`:ACME 协议客户端
|
||||
- `packages/plugins/plugin-lib`:通用插件辅助能力和证书相关共享代码
|
||||
|
||||
`packages/pro/` 是独立 Git 工作区,使用 `packages/pro/.git` 管理。根仓库的 `git status` / `git diff` 默认看不到这里的实际改动;修改商业版代码后,要在 `packages/pro` 目录内单独执行 `git status` / `git diff` 检查。
|
||||
|
||||
## 硬性规则
|
||||
|
||||
- 根包管理器是 pnpm,不要引入 npm/yarn lockfile。
|
||||
- 不要主动运行 `pnpm install`;用户会事先准备好 `node_modules`。如果 `pnpm install` 或测试因缺少依赖、TTY、网络问题失败,停止尝试并告知用户环境问题。
|
||||
- 前端不要运行 `pnpm tsc` / `vue-tsc`;当前依赖组合中 `vue-tsc@1.8.27` 会抛无效内部错误。前端 `test:unit` 只是占位脚本。
|
||||
- 根仓库是 pnpm + lerna monorepo;不要引入 npm/yarn lockfile。
|
||||
- `packages/pro/` 是独立 Git 工作区;修改商业版代码后,必须在 `packages/pro` 内单独执行 `git status` / `git diff`。
|
||||
- 不要把 `packages/ui/certd-server/data/`、`logs/`、生成的 metadata/dist 等运行时或构建产物纳入改动,除非任务明确要求。
|
||||
- 做数据库结构变更时,添加或更新迁移脚本,不要依赖 TypeORM 自动同步。
|
||||
- 做插件相关任务时,先读取对应 `.trae/skills/<skill>/SKILL.md`,再进入具体实现。
|
||||
- 后端 service 拼接可选 `projectId` 查询条件时,不要直接写 `{ userId, projectId }`;应使用 `BaseService.buildUserProjectQuery(userId, projectId)`,只有 `projectId != null` 时才加入查询条件。
|
||||
- 例外:分析插件动态依赖时,可以只读查看后端数据目录下的 `./data/.runtime-deps`;该目录用于 runtime-deps 动态安装第三方 SDK,不应纳入提交。
|
||||
|
||||
## 工作方式
|
||||
核心包:
|
||||
|
||||
- 先读本文;需要代码导航、目录入口、参考文件或验证命令时读 `.codex/repo-map.md`。
|
||||
- 任务涉及后端、前端、插件、测试或代码风格时,先读取 `.codex/agent-rules/` 下对应规则文件,再查看具体代码。
|
||||
- 在 PowerShell 中读取中文、Markdown、locale、文档类文件时,显式使用 `Get-Content -Encoding utf8`;如果仍乱码,再执行 `[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()` 后重试。
|
||||
- 做后端任务时,先定位 `packages/ui/certd-server/src/modules` 下的模块,以及相关 entity/service/controller。
|
||||
- 做前端任务时,先定位 `packages/ui/certd-client/src/views/certd` 下的页面,再找对应 `src/api`。
|
||||
- 做服务商、DNS、部署、通知相关任务时,先看 `packages/ui/certd-server/src/plugins`,再看 `packages/plugins/plugin-lib` 里的共享辅助能力。
|
||||
- 优先沿用现有模块、插件、服务模式,再考虑新增抽象;避免为了形式上的“复用”制造过度设计。
|
||||
- 实现新功能或修复行为缺陷前,优先补对应单元测试并确认红灯,再实现代码并跑聚焦验证。确实不适合先写测试时,在回复中说明原因和替代验证方式。
|
||||
- 后补单元测试时,先按正确行为写预期;如果红灯需要修改既有实现,先向用户确认这是 bug 还是既有需求,避免未经确认改变行为。
|
||||
- `packages/ui/certd-server`:后端服务。
|
||||
- `packages/ui/certd-client`:前端 Web 管理台。
|
||||
- `packages/core/pipeline`:流水线核心。
|
||||
- `packages/core/acme-client`:ACME 客户端。
|
||||
- `packages/plugins/plugin-lib`:插件共享能力。
|
||||
|
||||
## 仓库地图
|
||||
|
||||
- `package.json`:根脚本与 workspace 元信息。
|
||||
- `pnpm-workspace.yaml`:workspace 匹配规则,包含 `packages/**`、`packages/ui/**`。
|
||||
- `lerna.json`:lerna-lite 配置。
|
||||
- `docs`:VitePress 文档站。
|
||||
- `docker`:Docker 安装和运行相关文件。
|
||||
- `packages/core/acme-client`:ACME 协议客户端。
|
||||
- `packages/core/basic`:共享基础工具。
|
||||
- `packages/core/pipeline`:流水线核心抽象、插件模型、执行上下文。
|
||||
- `packages/libs`:共享集成库。
|
||||
- `packages/plugins/plugin-lib`:证书、DNS Provider、格式转换等插件共享能力。
|
||||
- `packages/plugins/plugin-cert`:证书插件包入口。
|
||||
- `packages/ui/certd-server`:后端 Midway 服务。
|
||||
- `packages/ui/certd-client`:前端 Vue/Vite 管理台。
|
||||
- `packages/pro`:商业版独立 Git 工作区,需在该目录内单独检查状态。
|
||||
|
||||
常见运行时或生成产物:
|
||||
|
||||
- 根目录:`node_modules`、`logs`、`output`、`lerna-debug.log`、`tmp-certd-client-vite*.log`。
|
||||
- 后端:`packages/ui/certd-server/data`、`packages/ui/certd-server/logs`。
|
||||
- 后端动态依赖:`./data/.runtime-deps`,常见于阿里云 SDK、腾讯云 SDK 等插件第三方依赖。
|
||||
- 各包:`dist`。
|
||||
- 插件:metadata/yaml 导出结果。
|
||||
|
||||
## 常用验证
|
||||
|
||||
- 前端改动文件格式化:`packages\ui\certd-client\node_modules\.bin\prettier.cmd --write <files>`。
|
||||
- 前端改动文件 ESLint 修复:`packages\ui\certd-client\node_modules\.bin\eslint.cmd --fix <files>`。
|
||||
- 后端单元测试:`cd packages\ui\certd-server && npm run unit`。
|
||||
- 后端改动文件 lint fix:`cd packages\ui\certd-server && npm run lint`(该脚本即 `mwts fix`,对改动文件执行 ESLint 自动修复)。
|
||||
- 其他package lint fix:`cd packages\xxx\xxxx && npm run lint`。
|
||||
|
||||
## 通用工作规则
|
||||
|
||||
- 先读本文,再按任务读取具体代码或技能文件。
|
||||
- 与用户沟通、代码注释、文档、日志和面向用户的界面文案,尽量使用中文;技术专有名词、代码标识符、第三方库名称和命令可保留英文。
|
||||
- PowerShell 读取中文、Markdown、locale、文档类文件时使用 `Get-Content -Raw -Encoding UTF8`;仍乱码时先执行 `[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()`。
|
||||
- PowerShell 中用 `rg` 搜索含引号、括号、反斜杠的 pattern 时,优先用单引号包裹整个 pattern,例如 `rg 'await import\("tencentcloud-sdk-nodejs' packages/ui/certd-server/src -g '*.ts'`。
|
||||
- 手工编辑或创建文件时优先使用 `apply_patch`。单个文件内有多处不连续改动时,拆成多个独立的 `*** Update File` 块,每块只改一处附近上下文;不要在同一个 update hunk 里强塞多个 `@@`。
|
||||
- 只有真正机械化的大批量替换、格式化或生成任务才考虑脚本/工具。若必须使用临时脚本,应放在临时目录并在同一个受控步骤内完成创建、执行、删除;不要把临时脚本落在仓库里跨多步工具调用执行。
|
||||
- 不要主动运行 `pnpm install`;缺依赖、TTY、网络导致安装或测试失败时,停止尝试并说明环境问题。
|
||||
- 优先沿用现有模块、插件、service、页面模式;不要为形式上的复用制造过度抽象。
|
||||
- 代码可读性优先于短写法。尽量少用三元表达式,优先使用完整的 `if/else`;复杂条件、链式调用、内联对象和多层 helper 调用要拆成命名清晰的中间变量或小方法。
|
||||
- 方法调用链不要直接塞进另一个方法参数;先用有意义的局部变量承接返回值,再传入下一步。
|
||||
- 不要在单一表达式内嵌套分支、对象构造与方法调用。优先使用清晰的 `if/else` 分支;仅在确实能降低复杂度时才提取有意义的中间变量,避免为拆分而增加阅读跳转。
|
||||
- 注释优先使用中文,尤其是业务规则、兼容逻辑、协议细节和隐藏风险;文件已有英文风格或引用外部术语时可保持一致。
|
||||
- 遵守 DRY 和单一职责;第三次出现的业务规则、字段转换、权限判断、Repository 选择、事务传播、金额计算等逻辑,应优先抽成合适 helper 或 service 方法。
|
||||
- 如无明确复用价值,不要随意新增 `normalizeXxx` 等辅助方法;优先在现有业务方法中直接处理简单兼容或字段转换逻辑,避免增加不必要的抽象层。
|
||||
|
||||
|
||||
## 测试与验证
|
||||
|
||||
- 新功能、缺陷修复和重构遵循 TDD:先编写或更新测试,覆盖正常路径、边界条件和明确的错误处理;先运行测试确认 RED,再实现最小改动,最后运行聚焦测试确认 GREEN。
|
||||
- 单元测试应优先直接测试原始业务方法,不要为了方便测试而抽取没有业务价值的 helper;数据库、RPC 和其他外部依赖可以使用 mock 隔离。
|
||||
- 确实不适合先写测试时,在回复中说明原因和替代验证方式。
|
||||
- 后补单元测试时,按正确行为写预期;若红灯需要修改既有实现,先向用户确认这是 bug 还是既有需求,避免未经确认改变行为。
|
||||
- 后端纯单测放在 `src/**/*.test.ts`,尽量与被测文件相邻;`test:unit` 只跑这些文件,构建/打包应排除 `*.test.ts`。
|
||||
- 单测需要 mock ESM 静态 import(如 `@certd/plus-core` 的 `isPlus`、第三方包)时,**优先使用 `esmock`**,通过 `esmock(被测模块路径, { 依赖模块: { 导出: mock实现 } })` 加载被测模块并替换依赖;**不要为了测试给业务代码加注入点/改业务结构**(如不需要给类加 `isPlusFn` 之类的可注入属性)。
|
||||
- 各包 `test:unit` 脚本应显式设置 `NODE_ENV=unittest`。
|
||||
- 单包单测优先用 `cd <包目录> && npm run test:unit`,例如 `cd packages\ui\certd-server && npm run test:unit`。
|
||||
- 优先对改动包运行聚焦测试或格式化/ESLint;只有跨包影响明显时再考虑更大范围构建。
|
||||
|
||||
## 架构边界
|
||||
## 规范回顾
|
||||
|
||||
插件是核心能力,不是边缘功能。新增服务商、DNS 验证、证书部署、通知方式等能力,通常应该放在插件包里,或放在 `packages/ui/certd-server/src/plugins/<plugin-name>/` 下。
|
||||
- 每次任务收尾时,回顾用户提出的要求,识别其中可跨任务复用、稳定且不与既有规则冲突的约束。
|
||||
- 只将经过确认的共性要求精炼后写入本文;一次性任务细节、临时偏好和未经确认的推断不要写入长期规范。
|
||||
- 更新本文后检查规则是否清晰、可执行,并删除重复或互相矛盾的表述。
|
||||
|
||||
修改证书申请、验证、部署或通知行为时,先判断改动属于 ACME client、pipeline 核心抽象、后端 module/service/entity/controller、具体插件实现,还是前端 view/form/schema。
|
||||
|
||||
如果只是某个服务商或部署目标的问题,不要轻易修改共享 pipeline/core 行为,除非确实是可复用的公共能力。
|
||||
## 后端规则
|
||||
|
||||
- 后端主包是 `packages/ui/certd-server`,使用 Node.js、ESM、TypeScript、MidwayJS 3、Koa、TypeORM 和 SQL 迁移。
|
||||
- 做后端任务时,先定位 `packages/ui/certd-server/src/modules` 下的模块,以及相关 entity/service/controller。
|
||||
- 表结构变更必须添加或更新 `packages/ui/certd-server/db/migration/*.sql`;不要依赖 TypeORM 自动同步。
|
||||
- 文件上传接口 `/basic/file/upload` 返回临时 key;业务保存前必须调用 `FileService.saveFile(userId, key, "public" | "private")` 转成永久 key,不能直接保存 `tmpfile_key_...`。
|
||||
- 方法参数超过 3 个时,优先改为对象参数。
|
||||
- 事务链路方法统一用 `method(ctx, req)`,`ctx` 放第一位并承载 `manager?: EntityManager`,业务参数放 `req` 对象。
|
||||
- 只有需要事务传播时才定义 `ctx`;普通查询、纯函数和简单私有方法继续使用明确参数。
|
||||
- 需要按事务上下文取 Repository 时,用 `BaseService.getRepo(ctx, EntityClass)`。
|
||||
- 需要“有事务则复用、无事务则开启”时,用 `BaseService.transactionWithCtx(ctx, callback)`。
|
||||
- 基础 CRUD 数据访问优先复用 `BaseService` 的 `find`、`findOne`、`list`、`page`、`update`、`deleteWhere` 等方法;不要从 `repository.createQueryBuilder()` 开始重复实现完整查询或更新。仅将关键词组合筛选、联表、业务排序等基类无法表达的部分放入 `list/page` 的 `buildQuery`。
|
||||
- 拼接可选 `projectId` 查询条件时,**必须**使用 `BaseService.buildUserProjectQuery(userId, projectId)`,禁止直接写 `{ userId, projectId }`。因为 `projectId` 可能为 `null`/`undefined`,直接放入查询会生成错误的 `WHERE projectId = NULL` 条件。
|
||||
- `ctx` 类型复用 `BaseService` 导出的 `ServiceContext`。
|
||||
- 新增 service 方法避免与 `BaseService` 方法签名冲突,例如不要用 `delete(id)` 覆盖 `delete(ids, where?)`;改用 `deleteById` 等具体名称。
|
||||
|
||||
### 证书仓库(cd_cert_info)空证书记录【必须保留】
|
||||
|
||||
- 保存流水线时(`PipelineService.save`)会调用 `CertInfoService.updateDomains` 同步该流水线的 active 证书记录(占位记录,`certInfo` 为空)。**这些记录必须保留,不要删除**:开放接口(OpenAPI `autoApply`)触发申请证书前,会先查询证书仓库中是否有该流水线的记录——有记录(即使证书内容为空)说明该流水线已存在、证书正在申请中,直接复用/触发已有流水线;若删掉这些记录,在证书申请成功前的空窗期内,开放接口每次调用都会重复创建新流水线。
|
||||
- 同步规则(`updateDomains`,只处理 active 记录,inactive/revoked 历史保留):① 流水线中每个证书申请任务(CertApply 类步骤)对应仓库一条 active 记录(按 `task_id` 关联),没有则创建空证书记录;② 删除孤儿 active 记录(流水线中已不存在对应任务 id 的,含 `task_id` 为空的历史遗留);③ 一个流水线多个申请任务时维护多条记录,随时保持一致。
|
||||
- `updateCertByPipelineId`(申请证书成功后写入仓库)中同样**不得删除**该流水线的空证书记录;新证书总是新建一条 active 记录,并按相同 `task_id` 把同流水线其他 active 记录标记为 inactive,避免同一流水线出现多条 active。
|
||||
- 吊销旧证书(`CertRevokeOld` 后置任务)按证书申请任务 id(`task_id`)精确匹配:只吊销同一申请任务产出的 inactive 旧证书,不误伤其他任务仍在使用的证书。
|
||||
|
||||
### 后端地图
|
||||
|
||||
- `packages/ui/certd-server/bootstrap.js`:Midway 启动入口,使用 `@midwayjs/bootstrap`。
|
||||
- `packages/ui/certd-server/src/configuration.ts`:Midway 主配置,注册组件和全局中间件。
|
||||
- `packages/ui/certd-server/src/config/config.default.ts`:端口、HTTPS、静态文件、cron、TypeORM、Flyway、上传、JWT、Swagger 默认配置。
|
||||
- `packages/ui/certd-server/src/config/loader.ts`:读取 `.env`、`.env.<env>.yaml`,支持 `certd_` 前缀环境变量覆盖嵌套配置。
|
||||
- `packages/ui/certd-server/src/modules`:业务模块根目录,常见模块包括:
|
||||
- `basic`
|
||||
- `cert`
|
||||
- `cname`
|
||||
- `cron`
|
||||
- `login`
|
||||
- `monitor`
|
||||
- `open`
|
||||
- `pipeline`
|
||||
- `plugin`
|
||||
- `suite`
|
||||
- `sys`
|
||||
- `packages/ui/certd-server/src/controller`:API 入口,按 `basic`、`user`、`sys`、`openapi` 分组。
|
||||
- `packages/ui/certd-server/db/migration`:SQL 迁移目录;TypeORM `synchronize: false`,表结构变更必须配套迁移 SQL。
|
||||
- 后端测试使用 Mocha + Node `assert/strict`;纯单测放在 `src/**/*.test.ts`,可参考 `src/utils/random.test.ts`、`src/controller/basic/app-controller.test.ts`、`src/modules/pipeline/service/pipeline-service.test.ts`。
|
||||
|
||||
## 前端规则
|
||||
|
||||
- 前端主包是 `packages/ui/certd-client`,使用 Vue 3、Vite、TypeScript、Ant Design Vue、Fast Crud、Pinia、vue-router、vue-i18n。
|
||||
- 做前端任务时,先定位 `packages/ui/certd-client/src/views/certd` 下的页面,再找对应 `src/api`。
|
||||
- 前端用户提示**统一使用 `notification`**(ant-design-vue 的 `notification.success({ message: X })` / `.error` / `.warning` / `.info`),**不要使用 `message` API**;`src/vben/` 目录(vben 框架内置布局/组件)保持原样不动。
|
||||
- 不要运行前端 `pnpm tsc` / `vue-tsc`;当前 `vue-tsc@1.8.27` 会抛无效内部错误。前端 `test:unit` 只是占位脚本,也不要跑。
|
||||
- 前端 TS/Vue/locale 改动后,只对本次改动文件运行现有 Prettier / ESLint:`packages\ui\certd-client\node_modules\.bin\prettier.cmd --write <files>` 和 `packages\ui\certd-client\node_modules\.bin\eslint.cmd --fix <files>`。
|
||||
- 需要手写 render 的地方(如 `Modal.confirm` 的 `content`、`useFormDialog` 的 `body`、动态插槽等),一律用 JSX 编写,禁止使用 `h()` 创建 VNode:
|
||||
- `.vue` 组件:把 `<script lang="ts" setup>` 改为 `<script lang="tsx" setup>`,然后在 JSX 中直接写元素(如 `content: () => (<div class="xxx">...</div>)`)。
|
||||
- `.ts` 文件:直接把文件后缀改为 `.tsx`,再写 JSX render。
|
||||
- 列表管理、后台管理、记录查询、CRUD 表格页面优先使用 Fast Crud;开发或重构前读 `.trae/skills/fast-crud-page-dev/SKILL.md`。
|
||||
- 只有轻量只读展示、强交互自定义界面或既有页面模式明显不适合 Fast Crud 时,才手写 `a-table` / 自定义列表,并在回复中说明。
|
||||
- 内嵌 Fast Crud 时,外层必须有稳定高度或完整 `flex: 1; min-height: 0` 链路。
|
||||
- 前端组件样式统一写在 `<style>` / Less / CSS 文件里,通过样式名映射到元素;尽量不要在元素上直接写 `style`。
|
||||
- 每个组件都要有一个稳定的根样式名,并把组件下方样式全部包在该根样式名内;尽量不要使用 `scoped`。
|
||||
- 可复用的公共样式名放在 `packages/ui/certd-client/src/style` 下维护,优先使用 `cd-` 前缀,避免散落在业务组件里重复定义。
|
||||
- 后台管理列表展示或筛选用户字段时,优先参考 `packages/ui/certd-client/src/views/sys/suite/user-suite/crud.tsx` 的 `userId` 字段模式,用 `table-select` + `/sys/authority/user/getSimpleUserByIds` 字典回显和搜索。
|
||||
- 对话框里只做确认可用 `Modal.confirm`;有字段输入、表单校验或提交字段时,必须用 `useFormDialog` / `openFormDialog`。
|
||||
- 使用 `table-select` / `fs-table-select` 时,`dict` 必须配置 `getNodesByValues`,支持根据已选 ID 批量懒加载选项,确保编辑回显和异步数据场景正常工作。
|
||||
|
||||
### 前端地图
|
||||
|
||||
- `packages/ui/certd-client/vite.config.ts`:Vite 配置。
|
||||
- dev 端口:`3008`
|
||||
- 代理路径:`/api`、`/certd/api`
|
||||
- 代理目标:`127.0.0.1:7001`
|
||||
- `packages/ui/certd-client/src/main.ts`:Vue 启动入口,注册 AntDV、Vben、router、全局组件、插件和偏好设置。
|
||||
- `packages/ui/certd-client/src/App.vue`:根组件,包含 `AConfigProvider`、`FsFormProvider`、`router-view`。
|
||||
- `packages/ui/certd-client/src/router/index.ts`、`src/router/resolve.ts`:路由入口,使用 `createWebHashHistory`。
|
||||
- `packages/ui/certd-client/src/router/source/modules/certd.ts`:Certd 主业务路由。
|
||||
- `packages/ui/certd-client/src/store`:Pinia store,主要包括:
|
||||
- `user`
|
||||
- `project`
|
||||
- `settings`
|
||||
- `plugin`
|
||||
- `packages/ui/certd-client/src/api/service.ts`:Axios 封装。
|
||||
- `packages/ui/certd-client/src/api/tools.ts`:错误与响应工具。
|
||||
- `packages/ui/certd-client/src/views/certd`:核心业务视图,常见目录包括:
|
||||
- `pipeline`
|
||||
- `cert`
|
||||
- `monitor`
|
||||
- `access`
|
||||
- `notification`
|
||||
- `open`
|
||||
- `project`
|
||||
- `suite`
|
||||
- `wallet`
|
||||
- `packages/ui/certd-client/src/locales`:国际化入口与语言包。
|
||||
- Fast Crud 页面常见拆分是 `api.ts`、`crud.tsx`、`index.vue`;可参考 `src/views/certd/access`、`src/views/sys/suite/user-suite/crud.tsx`、`src/views/certd/wallet/index.vue`。
|
||||
|
||||
## 流水线与插件规则
|
||||
|
||||
- 插件是核心能力。新增服务商、DNS 验证、证书部署、通知方式,通常放到插件包或 `packages/ui/certd-server/src/plugins/<plugin-name>/`。
|
||||
- 做服务商、DNS、部署、通知相关任务时,先看 `packages/ui/certd-server/src/plugins`,再看 `packages/plugins/plugin-lib`。
|
||||
- 插件依赖的第三方 SDK 可能通过 runtime-deps 动态安装到后端运行目录 `./data/.runtime-deps`。分析阿里云、腾讯云等 SDK 行为时,需要进入该目录阅读实际安装版本代码。
|
||||
- 修改证书申请、验证、部署或通知行为时,先判断归属:ACME client、pipeline 核心、后端 module/service/entity/controller、具体插件、前端 view/form/schema。
|
||||
- 单个服务商或部署目标的问题,不要轻易修改共享 pipeline/core;只有可复用公共语义或跨插件一致行为才上移到 `packages/core/pipeline` 或 `packages/plugins/plugin-lib`。
|
||||
- `newAccount({ onlyReturnExisting: true })` 可用同一个 account private key 取回已创建账号 URL,且不会再次消费 EAB。
|
||||
- 修改 EAB `kid` 后,应重新生成绑定该 `kid` 的 account private key;否则应阻止继续申请并提示刷新账号私钥。
|
||||
- 插件开发前先读对应技能:`.trae/skills/dns-provider-dev/SKILL.md`、`.trae/skills/task-plugin-dev/SKILL.md`、`.trae/skills/access-plugin-dev/SKILL.md`、`.trae/skills/plugin-converter/SKILL.md`。
|
||||
- `.codex/skills` 是指向 `.trae/skills` 的目录链接;更新技能只维护 `.trae/skills`,不要复制第二份。
|
||||
|
||||
### 插件市场身份与卡片操作
|
||||
|
||||
- 插件唯一身份是 `fullName`:有作者时为 `author/name`,内置插件为其 `name`。数据库判重、同步/安装匹配、前端列表 `key` 和 Fast Crud `rowKey` 都必须使用 `fullName`,不得退回仅按 `name` 匹配。
|
||||
- 新增或导入本地插件必须生成 `fullName`;旧插件若缺失 `fullName`,应在迁移或兼容更新时按作者和名称补齐,避免同名不同作者的插件互相覆盖或误判已安装。
|
||||
- 插件分为四类:内置插件(无作者,需通过内置插件接口获取)、导入/自开发插件(无 `developerId`)、云端同步未安装插件(有 `developerId` 且 `installed=false`)、云端已安装插件(有 `developerId` 且 `installed=true`)。
|
||||
- 云端插件是否属于当前用户,只能以当前站点绑定账号的 `userId === developerId` 判断;不要以 `localEditable` 或插件名称、作者名称等字段代替。
|
||||
- 插件管理卡片所有类型都必须有“配置”入口。导入/自开发插件应显示编辑、复制、发布、删除;当前用户自己的已安装云端插件应显示编辑、复制、发布,但云端插件一律不显示删除。云端插件按 `installed` 显示安装或卸载,且管理操作和卸载入口可同时存在,不能互相覆盖。
|
||||
|
||||
### 后置任务(AfterTask)机制
|
||||
|
||||
- 概念:流水线主体运行结束后,按触发条件执行的任务;插件声明 `supportAfterTask: true` 后即可在后置任务中复用(如吊销旧证书 `CertRevokeOld`)。触发条件 `when`:`success`(主体执行成功)、`error`(主体失败)、`turnToSuccess`(主体被手动标记成功)。
|
||||
- 实现位置:`packages/core/pipeline/src/core/executor.ts`(`runAfterTasks` / `runOneAfterTask` / `executeAfterTaskPlugin`)与 `plugin/api.ts`。
|
||||
- **后置任务与普通任务步骤的执行链路完全一致**:`executeAfterTaskPlugin` 构造无 strategy 的伪 Step 后调用共用 `executePlugin`;不要给 executePlugin 增加差异化参数——后置任务与普通插件唯一区别是触发时机判断(`matchAfterTaskWhen`)。
|
||||
- 后置任务失败:`runAfterTasks` 聚合错误(格式 `后置任务[标题]执行失败:...`)并 throw → `runWithHistory` 将流水线整体标记为失败 → 统一走 error 通知;不再单独发送后置任务失败通知。
|
||||
- 后置任务没有内置等待时长:需要等待时由插件自身处理(`AbstractTaskPlugin.sleep(ms)` + `checkSignal()`)。
|
||||
- 未触发/禁用:写日志并标记 skip/disabled(与任务同语义),状态存 `afterTask.status`,日志 key 为 `afterTask.{id}`。
|
||||
- 通知在流水线整体(主体 + 后置任务)执行完成后发送,内容附加后置任务执行结果摘要。
|
||||
|
||||
### 流水线执行与状态写入规则
|
||||
|
||||
- `run()` 时序:`notification("start")` → `runtime.start(pipeline)` → `runtime.clearNotificationStatus()`(start 时机状态不参与显示)→ `runWithHistory(主体 + 后置任务)` → 成功/失败通知。`clearNotificationStatus` 不得移到 `notification("start")` 之前。
|
||||
- **插件主动返回 "skip" 时,runStep 仍必须执行结果处理**(把实例输出属性写回 `step.status.output` / files 等):否则保存的 lastRuntime 缺少输出,下次运行按上次结果做判断时(如 SkipWhenSucceed、证书未过期判断 `readLastCert`)会读不到上次证书,导致不跳过、重新申请(曾出 bug:第二次运行状态丢失)。SkipWhenSucceed 策略跳过的步骤,输出/files 直接从 lastStatus 拷贝(executePlugin 返回 `skipped: true`),此时可跳过结果处理。
|
||||
- 事件机制:`emit.ts` 为泛型 `PipelineEmitter<T> implements TaskEmitter<T>`,`emit(name, event)` 直接传事件对象,不再包装 `{pipeline, step, event}`;`PipelineEvent<T> = { cert: T; runnableId: string; pipelineId: number }`(`plugin/api.ts`)。
|
||||
- 证书申请成功事件:`base-convert.ts` `emitCertApplySuccess` 发 `{ cert, pipelineId: ctx.pipeline.id, runnableId: ctx.step.id }`;后端监听在 `auto-pipeline-emitter-register.ts`,写入证书仓库(`updateCertByPipelineId(pipelineId, cert, fromType, taskId)`,userId/projectId 从 pipeline 实体读取)。
|
||||
|
||||
### 流水线与插件地图
|
||||
|
||||
- `packages/core/pipeline/src/index.ts`:核心导出入口,导出 `core`、`dt`、`access`、`registry`、`plugin`、`context`、`decorator`、`service`、`notification`。
|
||||
- `packages/core/pipeline/src/plugin`:任务插件抽象,主要包括:
|
||||
- `AbstractTaskPlugin`
|
||||
- `IsTaskPlugin`
|
||||
- `TaskInput`
|
||||
- `pluginRegistry`
|
||||
- `packages/core/pipeline/src/access`:授权插件抽象,主要包括:
|
||||
- `BaseAccess`
|
||||
- `IsAccess`
|
||||
- `AccessInput`
|
||||
- `accessRegistry`
|
||||
- `packages/core/pipeline/src/dt/pipeline.ts`:`Pipeline`、`Stage`、`Task`、`RunStrategy` 等流水线数据结构。
|
||||
- `packages/core/pipeline/src/core`:执行器、上下文、运行历史、文件存储等。
|
||||
- `packages/core/pipeline/src/service`:CNAME、事件、配置、邮件、URL 等 pipeline service 接口。
|
||||
- `packages/ui/certd-server/src/plugins`:后端内置服务商、DNS、部署、通知等插件。
|
||||
- `packages/ui/certd-server/src/plugins/plugin-cert`:证书申请核心插件。
|
||||
- `packages/ui/certd-server/src/plugins/plugin-lib`:后端插件 helper/access。
|
||||
- `packages/plugins/plugin-lib/src/cert`:`CertReader`、`CertConverter`、DNS Provider 公共能力。
|
||||
- `packages/plugins/plugin-lib/src/cert/dns-provider`:`AbstractDnsProvider`、`dnsProviderRegistry`、`DomainParser`。
|
||||
- ACME 协议、EAB、账号、订单、挑战流程:优先看 `packages/core/acme-client` 或 `packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/acme.ts`。
|
||||
- 流水线执行、任务生命周期、输入输出、注册机制:看 `packages/core/pipeline`。
|
||||
- 单个云厂商 DNS 验证、证书部署、API 调用失败:改对应 `packages/ui/certd-server/src/plugins/plugin-xxx`。
|
||||
- 通用证书读取、DNS Provider 公共能力、格式转换:改 `packages/plugins/plugin-lib`。
|
||||
- 后端业务数据、接口、实体、权限、迁移:改 `packages/ui/certd-server/src/modules` 与 `src/controller`。
|
||||
- 表单、列表、插件配置 UI:改 `packages/ui/certd-client/src/views/certd` 及对应 `src/api`。
|
||||
|
||||
## 其他注意事项
|
||||
|
||||
### 旧版数据兼容
|
||||
|
||||
- 新增插件参数时,必须要考虑旧版数据兼容,比如新增一个deployType参数,有两种值:`default`和`custom`,需要在使用时判空,走旧版逻辑。
|
||||
|
||||
## 前端路由与国际化
|
||||
|
||||
- 路由 `meta.title` 是 **i18n 国际化 key**,必须在 `src/locales/langs/zh-CN/` 和 `src/locales/langs/en-US/` 对应的模块文件中添加翻译。
|
||||
- 示例:路由 `title: "certd.auditLog"` 需要在中英 locales 文件中有对应 key(`"certd.auditLog": "操作日志"` / `"certd.auditLog": "Audit Log"`)。
|
||||
- 菜单通过路由自动生成,需设置 `meta.isMenu: true` 才会出现在左侧菜单。
|
||||
- Plus 版功能菜单需设置 `meta.show: () => { const settingStore = useSettingStore(); return settingStore.isPlus; }`。
|
||||
|
||||
## 审计日志
|
||||
|
||||
- 审计日志是 Plus 版功能,非 Plus 版不会写入。
|
||||
- Controller 继承 `BaseController`,通过 `this.auditLog({ content: "xxx" })` 记录日志。
|
||||
- Controller 中的 `@Post("/add", { summary: "xxxx" })`, 这个summary是必须要的,他是日志action字段的来源
|
||||
- `getAuditType()` 返回类型常量,中间件自动从 ctx.path 判定 scope(`/api/sys/` → system,其他 → user)。
|
||||
- 操作日志有系统级(scope=system)和用户级(scope=user)区分。
|
||||
|
||||
+198
@@ -3,6 +3,204 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复插件市场插件安装无效的bug ([15d2bd8](https://github.com/certd/certd/commit/15d2bd8484a9ddd142bf4ef03d839c74405f9e6d))
|
||||
* preserve Route53 TXT record values ([#791](https://github.com/certd/certd/issues/791)) ([f5430a8](https://github.com/certd/certd/commit/f5430a8925cf6cf4e16c75f2358fbd78437e6705))
|
||||
|
||||
## [1.44.1](https://github.com/certd/certd/compare/v1.44.0...v1.44.1) (2026-09-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复编辑证书申请任务导致清空ACME账号的bug ([d67379f](https://github.com/certd/certd/commit/d67379f29725bbd6dcdefe97d101023c7fd11f03))
|
||||
* 修复大量问题 ([f911e06](https://github.com/certd/certd/commit/f911e06a698d568ae228880ec89b7e25fdd1305b))
|
||||
* 修复旧流水线保存时清空证书仓库证书内容的bug ([f58f2f3](https://github.com/certd/certd/commit/f58f2f3cd603c37e55ac5868fdd2ca99aa937e44))
|
||||
* **pipeline-controller:** 处理非编辑场景下保留id的问题 ([6606f32](https://github.com/certd/certd/commit/6606f3267868e3aadaca0092ef4c83198312553c))
|
||||
* **pipeline:** 修正流水线保存时的ID判断逻辑并增加参数校验 ([6af12d4](https://github.com/certd/certd/commit/6af12d49fce1144943dc37ba574b3c9356998dcd))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **dashboard:** 新增仪表盘站点监控和域名管理的统计卡片 ([9842a24](https://github.com/certd/certd/commit/9842a24a50a841f710ee7c8050a38e3334c58394))
|
||||
|
||||
# [1.44.0](https://github.com/certd/certd/compare/v1.43.0...v1.44.0) (2026-08-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复证书申请时EAB判断逻辑,已选择Acme账号的情况下不需要EAB ([d87821e](https://github.com/certd/certd/commit/d87821ee02b17136fc925df4547223332bf22823))
|
||||
* **pipeline:** 校验定时触发器cron表达式,拦截2月31号等无效日期 ([#785](https://github.com/certd/certd/issues/785)) [@yukaidi1220](https://github.com/yukaidi1220) ([f8e492d](https://github.com/certd/certd/commit/f8e492d6cb49257e56bbd06b28fd6eb32c923c28))
|
||||
* **plugin-ai-dev:** 优化AI插件测试流程并修复日志获取问题 ([faf9828](https://github.com/certd/certd/commit/faf9828168f6ce82adc530a65eaf313d6c9904b5))
|
||||
* **plugin-item-card:** 移除分数格式化时去掉.0的逻辑 ([a91d7c0](https://github.com/certd/certd/commit/a91d7c000a2aa6456a249433ff90a47787ec90f0))
|
||||
* **ssh:** 修复 keyboard-interactive 认证可能抛类型错误的问题 ([5e867c8](https://github.com/certd/certd/commit/5e867c86c30eb43f1e5aeee8c22d47596e173a8c))
|
||||
* **start.sh:** 优化前端zip包下载校验逻辑 ([15ee6ea](https://github.com/certd/certd/commit/15ee6ead49db3620d2ade641aadbfcb13ac3e6dc))
|
||||
* **tencent access:** 修复腾讯云国际站接口endpoint拼接问题 ([f614872](https://github.com/certd/certd/commit/f61487284f79d3f5a72c9f7eee04999a93728326))
|
||||
|
||||
### Features
|
||||
|
||||
* 实现自定义ACME证书颁发机构支持 ([97c417b](https://github.com/certd/certd/commit/97c417b1ad64b9a14bee8bfe98a2e789b9278820))
|
||||
* 新增证书吊销后置任务与证书状态管理 ([1cc9641](https://github.com/certd/certd/commit/1cc9641f5669710d1e5809995ad395c843855a12))
|
||||
* 新增AI插件开发全流程能力 ([6f9dffe](https://github.com/certd/certd/commit/6f9dffe6e59cc125bd394d7db6086d66a9c71e49))
|
||||
* 支持审计日志,操作日志 ([f2855d6](https://github.com/certd/certd/commit/f2855d6dac74c9db71d92f99601e35c360b10666))
|
||||
* 支持在线插件下载安装审核等 ([255960d](https://github.com/certd/certd/commit/255960d434eebb3d567a08f737f1642ab417b5d0))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 统一镜像地址格式 certd/certd ([49007d3](https://github.com/certd/certd/commit/49007d391565099d94dec375eb88e946856506b0))
|
||||
* 新增AI插件开发受限令牌与隔离API ([3a9069f](https://github.com/certd/certd/commit/3a9069f6e6242940229fd63c686f7e445408dbbb))
|
||||
* 优化源码启动脚本,增加快速启动 ([0cfc3b7](https://github.com/certd/certd/commit/0cfc3b7ae90fa031d75f0b395556a04d4efee76c))
|
||||
* 统一将路由前缀从/certd改为/cert ([4db0f83](https://github.com/certd/certd/commit/4db0f83304dc6fd9199673c48b1805421f024601))
|
||||
* 新增官方客户端Certd Client的多语言配置与页面入口 ([fbfe125](https://github.com/certd/certd/commit/fbfe1256a5c8be6dfb2fbae7e6130eb5f6c6d946))
|
||||
* 新增证书吊销功能,添加证书状态字段 ([5074572](https://github.com/certd/certd/commit/5074572694383c8e1bf2880e26827987683dc62c))
|
||||
* 新增自定义ACME管理功能,完善证书颁发机构适配 ([2ab23f5](https://github.com/certd/certd/commit/2ab23f50b216e27495fc823d2269ca0ba6366d15))
|
||||
* 移除全局反向代理配置,迁移到自定义ACME配置 ([c57110e](https://github.com/certd/certd/commit/c57110e42aada31dd0028b56b5baec1b19722525))
|
||||
* 在线插件支持通知类型 ([c0f194f](https://github.com/certd/certd/commit/c0f194f4438bd7ca3f52f63ca088543abde7428c))
|
||||
* 增加稳定版,预览版区分,预览版是最新版,稳定版在预览版验证稳定后,再将预览版转为稳定版 ([1574090](https://github.com/certd/certd/commit/15740904e5e97ee767c39e4401dbc1d645795c38))
|
||||
* 支持AI开发在线插件 ([eae7edb](https://github.com/certd/certd/commit/eae7edbda0fd7357163f33fb4084fcba21396f5f))
|
||||
* **domain:** 域名管理增加remark字段 ([e4041ea](https://github.com/certd/certd/commit/e4041eabece1bab2c1c4b955129cf7a7fe7302d0))
|
||||
* **login,settings:** 仅第三方登录开启后,在重启10分钟后将无法使用密码登录 ([1d12ad7](https://github.com/certd/certd/commit/1d12ad73c6ca827d2eb36bda867f9d9fe62dab48))
|
||||
* **plugin:** 插件安装支持检测依赖 ([e79660c](https://github.com/certd/certd/commit/e79660c5d17ed4049508a00a359950a54470332f))
|
||||
* **plugin:** 新增插件评分展示、排序与个人插件筛选功能 ([7ab5029](https://github.com/certd/certd/commit/7ab502991e0c001777b268b9f159311ef5e000a0))
|
||||
* **server:** response优化为gzip压缩 ([f38522c](https://github.com/certd/certd/commit/f38522ca0b2185f7f35538f53cb51dcf86d0bfc8))
|
||||
* **tencent-clb:** 重构腾讯云CLB部署插件的地域选择组件,改为远程下拉选择器 ([30d4fc8](https://github.com/certd/certd/commit/30d4fc84a9e940d1d07e1f25856eb6fa13da9ba1))
|
||||
|
||||
# [1.43.0](https://github.com/certd/certd/compare/v1.42.6...v1.43.0) (2026-08-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 优化火山引擎dcdn,单个单个部署,规避超时问题 ([cf008b9](https://github.com/certd/certd/commit/cf008b9cff54542dc551fb159578dbd49006229c))
|
||||
* **pipeline:** 修复过期/损坏的安装锁导致的依赖安装失败 ([1017a38](https://github.com/certd/certd/commit/1017a38eb48d6778745d4905d9ec4f6067a261d8))
|
||||
* **ui:** 修复oauth设置页保存后未正确刷新的问题 ([8ab699a](https://github.com/certd/certd/commit/8ab699a9104b47e02a88ad764e995a495deb775e))
|
||||
* **unicloud:** 修复unicloud登录接口改版后请求失败的问题 ([18583f6](https://github.com/certd/certd/commit/18583f6ce025fa8d365fad5a8c9c6020f3bcfbae))
|
||||
|
||||
### Features
|
||||
|
||||
* 优化开放接口证书自动申请功能 ([6339cdf](https://github.com/certd/certd/commit/6339cdffa9f79505dada5617d8c7fcfacc419a2e))
|
||||
* **pipeline:** 新增流水线批量编辑、阶段宽度调整功能 ([e5a1844](https://github.com/certd/certd/commit/e5a18448522be88bc504a45a46ad599f3c39ca30))
|
||||
* **ssh:** ssh连接支持跳板机 ([2cb4e55](https://github.com/certd/certd/commit/2cb4e551fa7616f587a2d907f9b513f7df3a9957))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 部署到京东云CDN支持设置跳转类型 ([e992192](https://github.com/certd/certd/commit/e9921928df011c2435af73b68ebc3d773f13c16a))
|
||||
* 统一通知类型常量管理,新增开放接口通知能力 ([edb249c](https://github.com/certd/certd/commit/edb249c2ad3c173dbfd04d8d156f895a6706ffd1))
|
||||
* 优化宝塔报错提示 ([1cfa766](https://github.com/certd/certd/commit/1cfa76683b40da9e9198552f2226419d4afa86a3))
|
||||
* 优化第三方登录设置提示 ([1468eb1](https://github.com/certd/certd/commit/1468eb1ef292a9e21a2a43131b86b8acb3688307))
|
||||
* 优化第三方依赖安装失败日志输出 ([18b2d3a](https://github.com/certd/certd/commit/18b2d3ac2078d0c6636af897f8f66e4eb5ff832c))
|
||||
* 优化SqliteError: disk I/O error报错修复提示 ([7d22fe3](https://github.com/certd/certd/commit/7d22fe3d7d0de97bae2ceedd051a0c16a1cd90e3))
|
||||
* 证书成功通知增加有效期显示 ([f1b6704](https://github.com/certd/certd/commit/f1b67049d16ebcabe6d6d9b6cb53a8a2b7a9265c))
|
||||
* **cert-plugin:** 证书申请支持失败重试 ([0ddb1f6](https://github.com/certd/certd/commit/0ddb1f69d2a46b993e792862ce9952336d532f5b))
|
||||
* Improve English localization coverage ([#778](https://github.com/certd/certd/issues/778)) @MephistoJB ([bd82147](https://github.com/certd/certd/commit/bd821474969a22078b9d2e73518e765b8c9a26c7))
|
||||
* **install:** 补充迁移到PostgreSQL后,表自增序列从1开始的修复SQL脚本和说明 ([80374ca](https://github.com/certd/certd/commit/80374cae8a43fd17eb180381a093267e466a0bc2))
|
||||
* **plugin-ucloud:** ucloud us3部署插件增加region选择 ([8165a5f](https://github.com/certd/certd/commit/8165a5ff8986e759948c06f924d12e605b2ec8f6))
|
||||
* **tencent-clb:** 为腾讯云CLB部署插件支持远程选择输入 ([967846b](https://github.com/certd/certd/commit/967846bef590b76d27e472180b96372ff55cfb6f))
|
||||
* **ui:** 域名选择器支持点击复制域名 ([b173311](https://github.com/certd/certd/commit/b173311468d2c1f318d1333257c3ebcf916da071))
|
||||
|
||||
## [1.42.6](https://github.com/certd/certd/compare/v1.42.5...v1.42.6) (2026-07-18)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复正常批量删除流水线报权限不足的bug ([5b50083](https://github.com/certd/certd/commit/5b500830a122c6c42dab054e57fed509050f94da))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 优化动态加载依赖镜像地址,多次重试 ([5f53b81](https://github.com/certd/certd/commit/5f53b81c75dd242b4260ac08cae14c6d1a08a883))
|
||||
|
||||
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复上传到cos报runtimeDepsService未初始化的问题 ([167b303](https://github.com/certd/certd/commit/167b303faeca02cc11cf97e4be2a3df914852167))
|
||||
* 修复dingtalk通知格式没有换行的bug ([7ed1be9](https://github.com/certd/certd/commit/7ed1be994f8b4b74cdeb38743060c912c027248b))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 给SQLITE_IOERR_WRITE增加友好报错提示,将certd:latest镜像改为certd:slim ([b91c9e4](https://github.com/certd/certd/commit/b91c9e4ea671cb359ef164e27864de1d66cba9d3))
|
||||
* 优化vke keubconfig获取方式,改成先查询,如果没有再创建临时config ([604fa5b](https://github.com/certd/certd/commit/604fa5be634d099d797bfee5c2b0f26ce0ac8461))
|
||||
|
||||
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复火山引擎查不到自定义源站域名的问题 ([02dabe1](https://github.com/certd/certd/commit/02dabe11db3e9b13ca4621ce9ddd2b808bfca390))
|
||||
* 修复火山引擎自定义源站域名查询不到的问题 ([e44bf9d](https://github.com/certd/certd/commit/e44bf9d77375d48ac7fd1582e69fae02dfd248fa))
|
||||
* **pipeline:** 重构运行时依赖加载逻辑,修复火山引擎DNS解析报runtimeDepsService未初始化的bug ([ec69b8f](https://github.com/certd/certd/commit/ec69b8f11bfd4b20991aef74a72a47182ca79a9d))
|
||||
|
||||
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **volcengine-alb:** 修复火山引擎ALB 默认证书部署类型会部署到扩展证书的问题 ([0a068a2](https://github.com/certd/certd/commit/0a068a274673e9768954e9f7367c267d44f3b530))
|
||||
|
||||
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复火山视频点播源站选择不到自定义源站的bug ([0071bcb](https://github.com/certd/certd/commit/0071bcb0e4dd108c86d7ca01820a9f6e6960e440))
|
||||
* 修复企业模式下弹出邮箱绑定提醒的问题 ([8d9dad9](https://github.com/certd/certd/commit/8d9dad9c82f6f2fd3ab3040068946a33f37145b1))
|
||||
* 修复AsiaIsp CDN证书重复情况下部署失败的问题 ([c3d6db3](https://github.com/certd/certd/commit/c3d6db3f1ef2f1c897b7989521fe8809dffaded1))
|
||||
* 修复cname用阿里云校验时报找不到runtimeDepsService的错误 ([072edd7](https://github.com/certd/certd/commit/072edd7affee424ab3411f4d41d338f084d7cac6))
|
||||
|
||||
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复多域名无法使用passkey登录的bug ([d176f9c](https://github.com/certd/certd/commit/d176f9cc0ebd051a614bfac74d1616d1945fc9a3))
|
||||
* 修复企业模式下登录报projectId不能为空的问题 ([a65366b](https://github.com/certd/certd/commit/a65366bbe1aadea8baaffbdadab58a5b631d9417))
|
||||
* **login:** 修复输入法 composing 状态下回车触发提交的问题 ([b74db81](https://github.com/certd/certd/commit/b74db81304bbe68476bbec5ea4307a2264060e92))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **certd-server:** 使用 jks-go转换jks证书,大幅精简镜像大小 ([c78898e](https://github.com/certd/certd/commit/c78898e4c10dd1701467d2e42e3f72bd8f2a352f))
|
||||
* **pipeline:** 将默认历史保留条数从30调整为100 ([d3e4677](https://github.com/certd/certd/commit/d3e4677ea4fac8e7533749d7f4187e410489e536))
|
||||
|
||||
# [1.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复jdk证书格式的问题 ([260f5ae](https://github.com/certd/certd/commit/260f5ae777b83493b0c578fe30fd00ec0c873226))
|
||||
* 修复telegram - 符号转义问题 ([d5882f1](https://github.com/certd/certd/commit/d5882f16bedb09baf09ace92049b02872620f5dc))
|
||||
* **aliyun:** 修复阿里云CDN/DCDN根据证书自动匹配不到证书的bug ([1ae185d](https://github.com/certd/certd/commit/1ae185d0bc356f4678bc38ca0582ce3396f82ebe))
|
||||
|
||||
### Features
|
||||
|
||||
* 通过插件配置懒加载依赖,动态加载第三方依赖包,精简安装镜像大小 ([01568ca](https://github.com/certd/certd/commit/01568ca1489069046b5a89ebdd4ced2f7f6ddf93))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 阿里云ESA证书部署支持SaaS模式 ([82276b5](https://github.com/certd/certd/commit/82276b53a8474a18a3d0237050907c994fc748f0))
|
||||
* 【破坏性更新】 证书压缩包不再生成文件存储,而是实时打包下载,证书申请插件不再输出certZip ([7cff1a9](https://github.com/certd/certd/commit/7cff1a98424120585205889874b3ef4956a30583))
|
||||
* 火山引擎点播插件支持部署到自定义源站域名 ([095791c](https://github.com/certd/certd/commit/095791cdc2b7c1f4b913b634643afec5e30fe9b0))
|
||||
* 基础镜像改成node:22-trixie-slim,对网络兼容性更好 ([c66a2bd](https://github.com/certd/certd/commit/c66a2bd77ab6dbb3e3fe2c00562b66287a9429ea))
|
||||
* 新增橙域网络(asia-isp) CDN证书部署插件 ([b48831e](https://github.com/certd/certd/commit/b48831e60b0059bef7ef9a34ab61c9dd2f684641))
|
||||
* 优化阿里云API网关增加翻页查询 ([ed58ae3](https://github.com/certd/certd/commit/ed58ae3c5339e4a0238a92acfe7ea6d2f566ea28))
|
||||
* 优化用户体验,首次访问时弹出邮箱账号绑定用以初始化账号 ([608cc2a](https://github.com/certd/certd/commit/608cc2a81ff0b4872c9fe11ed9c9c0b4b90a12a3))
|
||||
* 优化ACME账号字段的选择提示 ([bfd3cac](https://github.com/certd/certd/commit/bfd3cacc687fc5cbc3cb2ca3cadbc140de300dc2))
|
||||
* 支持全自动匹配部署宝塔网站证书 ([4dff48e](https://github.com/certd/certd/commit/4dff48e807c32a7623ec9206cf39c88e88f89f6a))
|
||||
* **cert-plugin:** 调整更新天数自动减半逻辑,仅7天ip证书生效,其他情况下不减半 ([56e5524](https://github.com/certd/certd/commit/56e5524a0f4af3645d70bc3b3ec750b45ba8de10))
|
||||
* dns默认ipv4first ([194463b](https://github.com/certd/certd/commit/194463bea9e797315aa7a724f4b2930701570419))
|
||||
* **passkey:** passkey支持多域名rpid ([79f6586](https://github.com/certd/certd/commit/79f65868ca0f5162bbc2f935ce89abc28011d816))
|
||||
* **plugin:** 在线插件编辑支持配置第三方依赖和插件依赖 ([635f069](https://github.com/certd/certd/commit/635f069012d4193cfb7cb051c96e28eec1247ca2))
|
||||
|
||||
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复设置里面不显示tab页签,导致某些页面需要点击查询按钮才有数据出来的bug ([c1b5a35](https://github.com/certd/certd/commit/c1b5a35f90a7d4b41397717b5c27905bc68e1bfb))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **plugin:** 增加 Dynadot DNS and access 插件 ([a3a215b](https://github.com/certd/certd/commit/a3a215b7ae2b90efcde91270ce4165bbfe77dc64))
|
||||
|
||||
## [1.41.3](https://github.com/certd/certd/compare/v1.41.2...v1.41.3) (2026-06-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复litessl无法申请证书,报authorization must be pending 错误的问题 ([d6cd9d1](https://github.com/certd/certd/commit/d6cd9d136d2812b2335917305f36d6d9414507ad))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 首页夜间模式主图切换为黑色背景 ([15484bc](https://github.com/certd/certd/commit/15484bc119fef7a0ca7f3fdab01d665fde47e688))
|
||||
|
||||
## [1.41.2](https://github.com/certd/certd/compare/v1.41.1...v1.41.2) (2026-06-10)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
# 自定义 ACME 功能开发计划
|
||||
|
||||
## 一、背景与目标
|
||||
|
||||
Certd 目前仅支持内置 CA(Let's Encrypt / Google / ZeroSSL / SSL.com / litessl / LE 测试环境)。
|
||||
目标:支持用户**自定义 ACME 证书颁发机构**——用户可填写任意符合 RFC 8555 的 ACME Directory URL
|
||||
(内网 CA、企业 CA、其他公共 ACME 服务商),可选 EAB 授权,走完整的申请、续期、吊销流程。
|
||||
|
||||
## 二、需求确认(已与用户确认)
|
||||
|
||||
1. **Directory URL 填写位置**:在「ACME账号授权」中填写(账号与 CA 绑定,续期自动复用账号)。
|
||||
- 代码中 `directoryUrl` 字段、`caType === "custom"` 分支、`AcmeService.getAcmeClientByAccount()` 读取 `account.directoryUrl` 均已预留,架构改动最小。
|
||||
2. **不做系统级公共自定义 CA 账号**:仅用户自建自己的 ACME 账号。
|
||||
3. **自定义 CA 支持 EAB,选填**:填了就用,不填则跳过。
|
||||
4. **吊销链路改造**(用户追加要求):证书写入仓库时同时把 `acmeAccountAccessId` 记录到
|
||||
`cd_cert_info` 表,吊销时直接读表,**不再查询流水线**(旧数据回退兼容)。
|
||||
|
||||
## 三、现状分析
|
||||
|
||||
### 已有基础(无需改动或已预留)
|
||||
|
||||
| 位置 | 现状 |
|
||||
| --- | --- |
|
||||
| `@certd/acme-client` | 底层 `new Client({ directoryUrl })` 直接支持任意 URL,无需改动 |
|
||||
| `AcmeAccountAccess.directoryUrl` 字段 | 已定义,但 `mergeScript` 里 `show: false` 隐藏 |
|
||||
| `AcmeAccountAccess.getDirectoryUrl()` | 已有 `caType === "custom"` 分支:校验必填并返回 `directoryUrl` |
|
||||
| `AcmeAccountInfo` 类型 | 已含 `directoryUrl`、`eab` 字段 |
|
||||
| `AcmeService.getAcmeClientByAccount()` | 已支持 `account.directoryUrl \|\| acme.getDirectoryUrl(...)` |
|
||||
| `AcmeAccountAccess.createAccountInfo()` | EAB 已选填(`eabKid && eabHmacKey` 才构造),custom 不强制 EAB |
|
||||
| 前端 `Dicts.sslProviderDict` | 已含 `custom`("自定义ACME")选项 |
|
||||
|
||||
### 缺口(本次要开发)
|
||||
|
||||
1. `AcmeAccountAccess.caType` 下拉无 `custom` 选项;`directoryUrl` 被隐藏、无必填联动。
|
||||
2. `AcmeAccountAccess` EAB 字段 `show` 条件不含 custom;custom 下应为选填(内置 CA 保持必填)。
|
||||
3. `AcmeService.getAcmeClient()`(无账号临时建账号路径,生成账号时必走)用 `acme.getDirectoryUrl({ sslProvider: "custom" })` 会抛错,需要支持 `directoryUrl` 参数。
|
||||
4. `CertApplyPlugin.sslProvider` 下拉无 `custom` 选项;custom 时 ACME 账号必选、preferredChain/certProfile 显隐、DNS 持久验证限制。
|
||||
5. 吊销链路:`CertInfoService.revoke` 依赖 `resolveRevokeParams` 查流水线解析参数,需改为读表。
|
||||
6. 数据库迁移:`cd_cert_info` 新增 `acme_account_access_id` 列。
|
||||
|
||||
## 四、改动清单
|
||||
|
||||
### 4.1 数据层:cd_cert_info 新增 ACME 账号列
|
||||
|
||||
- **迁移 SQL**:`packages/ui/certd-server/db/migration/v10055__cert_info_acme_account.sql`
|
||||
|
||||
```sql
|
||||
-- 证书仓库记录增加ACME账号授权id:吊销旧证书时直接使用记录上的账号,无需再解析流水线配置
|
||||
ALTER TABLE cd_cert_info ADD COLUMN acme_account_access_id bigint NULL;
|
||||
```
|
||||
|
||||
- **Entity**:`packages/ui/certd-server/src/modules/monitor/entity/cert-info.ts`
|
||||
- 新增字段 `acmeAccountAccessId`(映射 `acme_account_access_id`,nullable)。
|
||||
|
||||
### 4.2 ACME账号授权:支持自定义CA(`access/acme-account-access.ts`)
|
||||
|
||||
1. `caType` 下拉 options 增加 `{ value: "custom", label: "自定义ACME" }`(放首位或末位均可,建议末位并注明"需自备 ACME 服务")。
|
||||
2. `directoryUrl`:
|
||||
- `mergeScript` 改为 `show: ctx.compute(({ form }) => form.access?.caType === "custom")`;
|
||||
- 动态 `required`(custom 时必填);
|
||||
- helper 说明:填写 `https://` 开头的 Directory URL,如内网 CA 端点。
|
||||
3. EAB 字段(`eabKid` / `eabHmacKey`):
|
||||
- `show` 条件增加 `custom`;
|
||||
- custom 下**选填**(mergeScript 动态 `required: false`),内置 CA 保持必填;
|
||||
- `onGenerateAccount()` 中 `needEab` 列表**不加** custom(已满足选填语义)。
|
||||
4. `getDirectoryUrl()` 已有 custom 分支,确认校验信息友好即可。
|
||||
|
||||
### 4.3 AcmeService 支持自定义 Directory URL(`plugin/cert-plugin/acme.ts`)
|
||||
|
||||
1. `AcmeServiceOptions` 增加 `directoryUrl?: string`。
|
||||
2. `getAcmeClient(email)`:
|
||||
- `const directoryUrl = this.options.directoryUrl || acme.getDirectoryUrl({ sslProvider: this.sslProvider, pkType: this.options.privateKeyType })`;
|
||||
- custom 时若未传 `directoryUrl` 给出明确报错("自定义ACME需要填写Directory URL")。
|
||||
3. `revokeCert` 方式二(证书私钥方式)同样优先用 `this.options.directoryUrl`。
|
||||
4. `buildUrlMapping`:自定义域名不在内置映射表,天然走直连/用户反代,无需改动。
|
||||
|
||||
### 4.4 证书申请任务支持自定义CA(`plugin/cert-plugin/apply.ts`)
|
||||
|
||||
1. `sslProvider` 下拉 options 增加 `{ value: "custom", label: "自定义ACME", icon: "..." }`。
|
||||
2. `onInit()` 构造 `AcmeService` 时:
|
||||
- 若 `sslProvider === "custom"`,读取 `this.acmeAccountAccessId` 对应 access 的 `account.directoryUrl`,传入 `AcmeServiceOptions.directoryUrl`;
|
||||
- custom 且未选 ACME 账号时直接报错("自定义ACME必须选择对应的ACME账号")。
|
||||
3. 校验:
|
||||
- `doCertApply()` 中 custom 时强制 `acmeAccount`(version 1 也强制,version 2 已有强制逻辑);
|
||||
- `sslProvider === "custom"` 且 `challengeType === "dns-persist"` 时提示不支持(dns-persist 是 Let's Encrypt 特有机制)。
|
||||
4. 表单显隐(现有 mergeScript 天然兼容,需回归确认):
|
||||
- `preferredChain`:custom 不在 `preferredChainSupportedProviders`,`show: false` ✓;
|
||||
- `certProfile`:仅 letsencrypt 显示 ✓;
|
||||
- `getCommonAcmeAccount()`:custom 时 `${sslProvider}CommonAcmeAccountAccessId` 不存在,返回 null ✓。
|
||||
5. `getCheckChangeInputKeys()` 已含 `sslProvider`,切换 CA 会触发重新申请 ✓。
|
||||
|
||||
### 4.5 吊销链路改造(核心,用户指定方案)
|
||||
|
||||
**写入侧**:`packages/ui/certd-server/src/modules/monitor/service/cert-info-service.ts`
|
||||
|
||||
1. `updateCertByPipelineId(pipelineId, cert, fromType, taskId)`:
|
||||
- 已查询 `pipeline` 实体,从 `pipeline.content` 解析证书申请任务输入(复用/提取 `parseCertApplyInput` 为模块内方法),取出 `acmeAccountAccessId`;
|
||||
- 传入 `updateCert()`,写入记录(含占位记录复用更新场景)。
|
||||
2. `UploadCertReq` 增加 `acmeAccountAccessId?: number`;`updateCert()` 落库该字段。
|
||||
|
||||
**吊销侧**:`CertInfoService.revoke(id, userId, projectId)`
|
||||
|
||||
1. 读 `entity.acmeAccountAccessId`:
|
||||
- **有值(新数据)**:`accessService.getAccessById(acmeAccountAccessId, true, userId, projectId)` → `access.getAccount()` 得到 `acmeAccount`,`sslProvider = acmeAccount.caType`,`useProxy/reverseProxy` 不传(`resolveUrlMapping` 有"直连失败自动启用代理"兜底)→ `acmeService.revokeCert({ cert, acmeAccount })`,**不再查流水线**;
|
||||
- **为空(旧数据 / upload 来源)**:回退现有 `resolveRevokeParams` 查流水线逻辑,保证旧版数据可吊销。
|
||||
2. `createAcmeService()` 不再需要从流水线取 `useProxy/reverseProxy`(保留参数兼容即可)。
|
||||
|
||||
**事件链路**:`modules/auto/auto-pipeline-emitter-register.ts` 无需改动
|
||||
(`updateCertByPipelineId` 内部自行解析流水线配置落库)。
|
||||
|
||||
### 4.6 前端
|
||||
|
||||
- `Dicts.sslProviderDict` 已有 `custom`,无需改;
|
||||
- ACME账号授权、证书申请任务的表单 schema 均来自后端插件定义(`@AccessInput` / `@TaskInput`),前端组件零改动;
|
||||
- 回归确认 `icon-select`(sslProvider)与 `a-select`(caType)能正常渲染新增选项。
|
||||
|
||||
## 五、实施顺序(TDD)
|
||||
|
||||
1. **写测试(RED)**:
|
||||
- `acme-account-access.test.ts`:custom + directoryUrl 生成账号(含 EAB 选填)、缺 directoryUrl 报错;
|
||||
- `acme.test.ts`:`AcmeService` 传/不传 `directoryUrl` 时 `getAcmeClient` 使用正确端点;
|
||||
- `apply.test.ts`:custom + ACME账号 申请链路(mock acme client)、custom 未选账号报错;
|
||||
- `cert-info-service.test.ts`:写入时 `acmeAccountAccessId` 落库;吊销读表(mock accessService)不再解析流水线;旧数据(列为空)回退旧逻辑。
|
||||
2. **4.1 数据层**(迁移 SQL + entity)。
|
||||
3. **4.3 AcmeService directoryUrl 支持**。
|
||||
4. **4.2 ACME账号授权 custom 支持**。
|
||||
5. **4.4 证书申请任务 custom 支持**。
|
||||
6. **4.5 吊销链路改造**。
|
||||
7. **4.6 前端回归验证**。
|
||||
8. 全量单测 + lint。
|
||||
|
||||
## 六、测试与验证
|
||||
|
||||
- 后端单测:`cd packages\ui\certd-server && npm run test:unit`(聚焦 `cert-info-service`、`acme-account-access`、`acme`、`apply` 相关测试);
|
||||
- 改动文件 lint:`cd packages\ui\certd-server && npm run lint`;
|
||||
- 手动验证:新建 caType=custom 的 ACME 账号(填 Directory URL,可选 EAB)→ 生成账号 → 证书申请任务选「自定义ACME」+ 该账号 → 申请成功 → 吊销旧证书(观察不再解析流水线)。
|
||||
|
||||
## 七、风险与兼容性
|
||||
|
||||
1. **旧版数据兼容**:`acme_account_access_id` 列可空,旧记录吊销回退查流水线逻辑,行为不变。
|
||||
2. **dns-persist 限制**:自定义 CA 不支持 DNS 持久验证(LE 特有 `_validation-persist` 机制),申请任务做显式校验提示。
|
||||
3. **代理/反代**:吊销时不再携带任务级 `useProxy/reverseProxy`,依赖 `resolveUrlMapping` 直连失败自动启用代理的兜底;自定义 CA 通常内网/直连可达,影响可接受。若后续需要可在表中再增列。
|
||||
4. **无数据库以外的结构变更**;`updateDomains` 占位记录不强制写账号列(申请成功更新时补齐)。
|
||||
5. 内置 CA 行为完全不变(`directoryUrl` 为空时走原逻辑)。
|
||||
@@ -1,4 +1,4 @@
|
||||
# Certd
|
||||
# Certd
|
||||
|
||||
中文 | [English](./README_en.md)
|
||||
|
||||
@@ -105,25 +105,57 @@ https://certd.handfree.work/
|
||||
|
||||
#### Docker镜像说明:
|
||||
|
||||
- 国内镜像地址:
|
||||
- `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest`
|
||||
- `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:armv7`、`[version]-armv7`
|
||||
- DockerHub地址:
|
||||
- `https://hub.docker.com/r/greper/certd`
|
||||
- `greper/certd:latest`
|
||||
- `greper/certd:armv7`、`greper/certd:[version]-armv7`
|
||||
- GitHub Packages地址:
|
||||
##### 1. 镜像地址格式:
|
||||
|
||||
- `ghcr.io/certd/certd:latest`
|
||||
- `ghcr.io/certd/certd:armv7`、`ghcr.io/certd/certd:[version]-armv7`
|
||||
```
|
||||
registry.cn-shenzhen.aliyuncs.com/certd/certd:[version-][system-][latest/stable]
|
||||
------------ ↑ 镜像地址 ------------- ↑ 镜像名 -- ↑指定版本- ↑基础系统- ↑最新版本类型 (中括号表示可选)
|
||||
|
||||
```
|
||||
##### 2. 版本标签:
|
||||
|
||||
**最新版本标签:**
|
||||
|
||||
| 版本 | 标签 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 最新预览版【默认】 | `certd:latest` | 指向最新开发版本,包含最新功能,但稳定性不如稳定版 |
|
||||
| 最新稳定版 | `certd:stable` | 指向经过充分测试的生产就绪版本,推荐生产环境使用 |
|
||||
|
||||
**系统分支版本:**
|
||||
|
||||
> 根据基础镜像不同,分为如下三个分支版本,没有特殊需求选择默认的即可(他们功能是一样的)
|
||||
|
||||
| 系统版本 | 版本标签 | 基础系统 | 说明 | 稳定版标签 | 指定版本 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| alpine【默认】 | `certd:latest` | Alpine Linux | 默认版本,镜像体积小 | `certd:stable` | `certd:1.43.0` |
|
||||
| slim | `certd:slim` | Debian slim | 基于glibc,dns解析兼容性好 | `certd:slim-stable` | `certd:1.43.0-slim` |
|
||||
| armv7 | `certd:armv7` | Alpine Linux | ARMv7 架构专用版本 | `certd:armv7-stable` | `certd:1.43.0-armv7` |
|
||||
|
||||
##### 3. 镜像地址:
|
||||
|
||||
| 镜像仓库 | 最新预览版 | slim | armv7 |
|
||||
| --- | --- | --- | --- |
|
||||
| 阿里云【默认】 | `registry.cn-shenzhen.aliyuncs.com/certd/certd:latest` | `certd:slim` | `certd:armv7` |
|
||||
| Docker Hub | `certd/certd:latest` | `certd:slim` |
|
||||
| GitHub Packages | `ghcr.io/certd/certd:latest` | `certd:slim` | `certd:armv7` |
|
||||
|
||||
> 1. 后面的各个版本省略了镜像地址,使用时需要将镜像地址拼接完整。
|
||||
> 2. 如果您不确定使用哪个版本,请使用默认的 `certd:latest` 版本。
|
||||
|
||||
最新版本号:
|
||||

|
||||
|
||||
|
||||
##### 3. 镜像构建说明:
|
||||
|
||||
- 镜像构建通过`Actions`自动执行,过程公开透明,请放心使用
|
||||
- [点我查看镜像构建日志](https://github.com/certd/certd/actions/workflows/build-image.yml)
|
||||
- [点我查看预览版构建日志](https://github.com/certd/certd/actions/workflows/release-image.yml)
|
||||
- [点我查看稳定版发布日志](https://github.com/certd/certd/actions/workflows/stable-release.yml)
|
||||
|
||||

|
||||
|
||||
> 注意:
|
||||
>
|
||||
##### 4. 安全注意事项:
|
||||
|
||||
> - 本应用存储的证书、授权信息等属于高度敏感数据,请做好安全防护
|
||||
> - 请务必使用HTTPS协议访问本应用,避免被中间人攻击
|
||||
> - 请务必使用web应用防火墙防护本应用,防止XSS、SQL注入等攻击
|
||||
@@ -133,7 +165,14 @@ https://certd.handfree.work/
|
||||
|
||||
## 五、生态
|
||||
|
||||
### 1. 客户端工具 SSL-Assistant
|
||||
### 1. 官方客户端 Certd Client
|
||||
|
||||
`Certd Client` 是 Certd 官方证书部署客户端,运行于应用服务器上。自动发现本机 Nginx、Apache 和 IIS 站点,从 Certd 获取新证书并部署到本机。适用于不希望服务器开放 SSH、无法由 Certd 直接访问目标服务器的场景。
|
||||
|
||||
项目主页:[AtomGit](https://atomgit.com/certd/certd-client/) | [GitHub](https://github.com/certd/certd-client/)
|
||||
下载地址:[AtomGit Releases](https://atomgit.com/certd/certd-client/releases) | [GitHub Releases](https://github.com/certd/certd-client/releases)
|
||||
|
||||
### 2. 第三方客户端 SSL-Assistant
|
||||
|
||||
`SSL Assistant` 是一个运行于主机上的证书部署管理助手客户端。
|
||||
支持自动扫描主机`Nginx`配置,然后从`Certd`拉取证书并部署。
|
||||
@@ -180,8 +219,9 @@ https://certd.handfree.work/
|
||||
3. 获得专业版功能
|
||||
|
||||
|
||||
> [50元专业版优惠券限时领取](https://app.handfree.work/subject/#/app/certd/product) https://app.handfree.work/subject/#/app/certd/product
|
||||
> handfree.work是Certd官方激活码购买平台
|
||||
> [50元专业版优惠券限时领取](https://app.handfree.work/subject/#/app/certd/product)
|
||||
> https://app.handfree.work/subject/#/app/certd/product
|
||||
> app.handfree.work是Certd官方激活码购买平台
|
||||
|
||||
|
||||
专业版、商业版特权对比
|
||||
|
||||
+44
-14
@@ -1,4 +1,4 @@
|
||||
# Certd
|
||||
# Certd
|
||||
|
||||
[中文](./README.md) | English
|
||||
|
||||
@@ -95,21 +95,44 @@ You can choose one of the following deployment methods based on your needs:
|
||||
|
||||
#### Docker Image Information:
|
||||
|
||||
- Domestic Image Addresses:
|
||||
- `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest`
|
||||
- `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:armv7`, `[version]-armv7`
|
||||
- DockerHub Addresses:
|
||||
- `https://hub.docker.com/r/greper/certd`
|
||||
- `greper/certd:latest`
|
||||
- `greper/certd:armv7`, `greper/certd:[version]-armv7`
|
||||
- GitHub Packages Addresses:
|
||||
**Release channels:**
|
||||
|
||||
- `ghcr.io/certd/certd:latest`
|
||||
- `ghcr.io/certd/certd:armv7`, `ghcr.io/certd/certd:[version]-armv7`
|
||||
| Channel | Description |
|
||||
| --- | --- |
|
||||
| `stable` / `slim-stable` | **Stable version**, production-ready and fully tested, recommended for production environments |
|
||||
| `latest` / `slim` / `armv7` | **Preview version**, latest development build with newest features but potentially less stable |
|
||||
|
||||
**Image tags:**
|
||||
|
||||
| Channel | Tag | Versioned Tag | Base System | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| **Stable** | `stable` | `[version]-stable` | Alpine Linux | Recommended for production |
|
||||
| | `slim-stable` | `[version]-slim-stable` | Debian slim | Better DNS resolution compatibility |
|
||||
| **Preview** | `latest` | `[version]` | Alpine Linux | Default, small image size |
|
||||
| | `slim` | `[version]-slim` | Debian slim | Better DNS resolution compatibility |
|
||||
| | `armv7` | `[version]-armv7` | Alpine Linux | ARMv7 architecture |
|
||||
|
||||
**Stable version image addresses:**
|
||||
|
||||
| Registry | `stable` | `slim-stable` |
|
||||
| --- | --- | --- |
|
||||
| Aliyun | `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:stable` | `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:slim-stable` |
|
||||
| Docker Hub | `greper/certd:stable` | `greper/certd:slim-stable` |
|
||||
| GitHub Packages | `ghcr.io/certd/certd:stable` | `ghcr.io/certd/certd:slim-stable` |
|
||||
|
||||
**Preview version image addresses:**
|
||||
|
||||
| Registry | `latest` | `slim` | `armv7` |
|
||||
| --- | --- | --- | --- |
|
||||
| Aliyun | `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest` | `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:slim` | `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:armv7` |
|
||||
| Docker Hub | `greper/certd:latest` | `greper/certd:slim` | `greper/certd:armv7` |
|
||||
| GitHub Packages | `ghcr.io/certd/certd:latest` | `ghcr.io/certd/certd:slim` | `ghcr.io/certd/certd:armv7` |
|
||||
|
||||
> For versioned tags, replace tag name with `[version]-tag`, e.g. replace `stable` with `[version]-stable`
|
||||
|
||||
- Images are built automatically by `Actions`, with a transparent process. Please use them with confidence.
|
||||
- [Click here to view image build logs](https://github.com/certd/certd/actions/workflows/build-image.yml)
|
||||
|
||||
- [Click here to view preview version build logs](https://github.com/certd/certd/actions/workflows/release-image.yml)
|
||||
- [Click here to view stable version release logs](https://github.com/certd/certd/actions/workflows/stable-release.yml)
|
||||

|
||||
|
||||
> Note:
|
||||
@@ -123,7 +146,14 @@ You can choose one of the following deployment methods based on your needs:
|
||||
|
||||
## 5. Ecosystem
|
||||
|
||||
### 1. Client Tool: SSL-Assistant
|
||||
### 1. Official Client: Certd Client
|
||||
|
||||
`Certd Client` is the official Certd certificate deployment client that runs on application servers. It automatically discovers local Nginx, Apache, and IIS sites, retrieves new certificates from Certd, and deploys them locally. It is designed for servers that should not expose SSH or cannot be accessed directly by Certd.
|
||||
|
||||
Project Home: [AtomGit](https://atomgit.com/certd/certd-client/) | [GitHub](https://github.com/certd/certd-client/)
|
||||
Downloads: [AtomGit Releases](https://atomgit.com/certd/certd-client/releases) | [GitHub Releases](https://github.com/certd/certd-client/releases)
|
||||
|
||||
### 2. Third-Party Client: SSL-Assistant
|
||||
|
||||
`SSL Assistant` is a certificate deployment and management assistant client that runs on hosts. It supports automatic scanning of the host's `Nginx` configuration and pulling certificates from `Certd` for deployment. This tool is very useful when you don't want to expose your SSH host password.
|
||||
|
||||
|
||||
@@ -2,9 +2,15 @@ version: '3.3' # 兼容旧版docker-compose
|
||||
services:
|
||||
certd:
|
||||
# 镜像 # ↓↓↓↓↓ ---- 镜像版本号,建议改成固定版本号,例如:certd:1.29.0
|
||||
image: registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest
|
||||
image: registry.cn-shenzhen.aliyuncs.com/certd/certd:latest # 稳定版为 certd/certd:stable
|
||||
# image: ghcr.io/certd/certd:latest # --------- 如果 报镜像not found,可以尝试其他镜像源
|
||||
# image: greper/certd:latest
|
||||
# image: certd/certd:latest
|
||||
# --------- 生产建议使用稳定版, latest改成stable即可
|
||||
# image: registry.cn-shenzhen.aliyuncs.com/certd/certd:stable
|
||||
|
||||
|
||||
# security_opt: # --------- 如果slim镜像下启动报错,尝试去掉这两行注释
|
||||
# - seccomp=unconfined # 解决slim镜像下WorkerThreadsTaskRunner::DelayedTaskScheduler::Start() 报错问题
|
||||
container_name: certd # 容器名
|
||||
restart: unless-stopped # 自动重启
|
||||
volumes:
|
||||
@@ -71,7 +77,13 @@ services:
|
||||
# - certd_typeorm_dataSource_default_password=yourpasswd # 密码
|
||||
# - certd_typeorm_dataSource_default_database=certd # 数据库名
|
||||
|
||||
# ↓↓↓↓ --------------------------------------------------------- 自动升级,上面certd的版本号要保持为latest
|
||||
# #↓↓↓↓ ---------------------------- 如果数据库服务端开启了加密传输 require_secure_transport-ON
|
||||
# - certd_typeorm_dataSource_default_ssl_rejectUnauthorized=false # 忽略证书校验,或者配置下面的三项路径
|
||||
# - certd_typeorm_dataSource_default_ssl_ca # 证书CA路径【如果上面false,无需配置】
|
||||
# - certd_typeorm_dataSource_default_ssl_key # 私钥路径【如果上面false,无需配置】
|
||||
# - certd_typeorm_dataSource_default_ssl_cert # 证书路径【如果上面false,无需配置】
|
||||
|
||||
# ↓↓↓↓ --------------------------------------------------------- 自动升级,上面certd的版本号要保持为latest / stable
|
||||
# certd-updater: # 添加 Watchtower 服务
|
||||
# image: containrrr/watchtower:latest
|
||||
# container_name: certd-updater
|
||||
|
||||
@@ -3,6 +3,214 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.44.1](https://github.com/certd/certd/compare/v1.44.0...v1.44.1) (2026-09-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复编辑证书申请任务导致清空ACME账号的bug ([d67379f](https://github.com/certd/certd/commit/d67379f29725bbd6dcdefe97d101023c7fd11f03))
|
||||
* 修复大量问题 ([f911e06](https://github.com/certd/certd/commit/f911e06a698d568ae228880ec89b7e25fdd1305b))
|
||||
* 修复旧流水线保存时清空证书仓库证书内容的bug ([f58f2f3](https://github.com/certd/certd/commit/f58f2f3cd603c37e55ac5868fdd2ca99aa937e44))
|
||||
* **pipeline-controller:** 处理非编辑场景下保留id的问题 ([6606f32](https://github.com/certd/certd/commit/6606f3267868e3aadaca0092ef4c83198312553c))
|
||||
* **pipeline:** 修正流水线保存时的ID判断逻辑并增加参数校验 ([6af12d4](https://github.com/certd/certd/commit/6af12d49fce1144943dc37ba574b3c9356998dcd))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **dashboard:** 新增仪表盘站点监控和域名管理的统计卡片 ([9842a24](https://github.com/certd/certd/commit/9842a24a50a841f710ee7c8050a38e3334c58394))
|
||||
|
||||
# [1.44.0](https://github.com/certd/certd/compare/v1.43.0...v1.44.0) (2026-08-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复证书申请时EAB判断逻辑,已选择Acme账号的情况下不需要EAB ([d87821e](https://github.com/certd/certd/commit/d87821ee02b17136fc925df4547223332bf22823))
|
||||
* **pipeline:** 校验定时触发器cron表达式,拦截2月31号等无效日期 ([#785](https://github.com/certd/certd/issues/785)) [@yukaidi1220](https://github.com/yukaidi1220) ([f8e492d](https://github.com/certd/certd/commit/f8e492d6cb49257e56bbd06b28fd6eb32c923c28))
|
||||
* **plugin-ai-dev:** 优化AI插件测试流程并修复日志获取问题 ([faf9828](https://github.com/certd/certd/commit/faf9828168f6ce82adc530a65eaf313d6c9904b5))
|
||||
* **plugin-item-card:** 移除分数格式化时去掉.0的逻辑 ([a91d7c0](https://github.com/certd/certd/commit/a91d7c000a2aa6456a249433ff90a47787ec90f0))
|
||||
* **ssh:** 修复 keyboard-interactive 认证可能抛类型错误的问题 ([5e867c8](https://github.com/certd/certd/commit/5e867c86c30eb43f1e5aeee8c22d47596e173a8c))
|
||||
* **start.sh:** 优化前端zip包下载校验逻辑 ([15ee6ea](https://github.com/certd/certd/commit/15ee6ead49db3620d2ade641aadbfcb13ac3e6dc))
|
||||
* **tencent access:** 修复腾讯云国际站接口endpoint拼接问题 ([f614872](https://github.com/certd/certd/commit/f61487284f79d3f5a72c9f7eee04999a93728326))
|
||||
|
||||
### Features
|
||||
|
||||
* 实现自定义ACME证书颁发机构支持 ([97c417b](https://github.com/certd/certd/commit/97c417b1ad64b9a14bee8bfe98a2e789b9278820))
|
||||
* 新增证书吊销后置任务与证书状态管理 ([1cc9641](https://github.com/certd/certd/commit/1cc9641f5669710d1e5809995ad395c843855a12))
|
||||
* 新增AI插件开发全流程能力 ([6f9dffe](https://github.com/certd/certd/commit/6f9dffe6e59cc125bd394d7db6086d66a9c71e49))
|
||||
* 支持审计日志,操作日志 ([f2855d6](https://github.com/certd/certd/commit/f2855d6dac74c9db71d92f99601e35c360b10666))
|
||||
* 支持在线插件下载安装审核等 ([255960d](https://github.com/certd/certd/commit/255960d434eebb3d567a08f737f1642ab417b5d0))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 统一镜像地址格式 certd/certd ([49007d3](https://github.com/certd/certd/commit/49007d391565099d94dec375eb88e946856506b0))
|
||||
* 新增AI插件开发受限令牌与隔离API ([3a9069f](https://github.com/certd/certd/commit/3a9069f6e6242940229fd63c686f7e445408dbbb))
|
||||
* 优化源码启动脚本,增加快速启动 ([0cfc3b7](https://github.com/certd/certd/commit/0cfc3b7ae90fa031d75f0b395556a04d4efee76c))
|
||||
* 统一将路由前缀从/certd改为/cert ([4db0f83](https://github.com/certd/certd/commit/4db0f83304dc6fd9199673c48b1805421f024601))
|
||||
* 新增官方客户端Certd Client的多语言配置与页面入口 ([fbfe125](https://github.com/certd/certd/commit/fbfe1256a5c8be6dfb2fbae7e6130eb5f6c6d946))
|
||||
* 新增证书吊销功能,添加证书状态字段 ([5074572](https://github.com/certd/certd/commit/5074572694383c8e1bf2880e26827987683dc62c))
|
||||
* 新增自定义ACME管理功能,完善证书颁发机构适配 ([2ab23f5](https://github.com/certd/certd/commit/2ab23f50b216e27495fc823d2269ca0ba6366d15))
|
||||
* 移除全局反向代理配置,迁移到自定义ACME配置 ([c57110e](https://github.com/certd/certd/commit/c57110e42aada31dd0028b56b5baec1b19722525))
|
||||
* 在线插件支持通知类型 ([c0f194f](https://github.com/certd/certd/commit/c0f194f4438bd7ca3f52f63ca088543abde7428c))
|
||||
* 增加稳定版,预览版区分,预览版是最新版,稳定版在预览版验证稳定后,再将预览版转为稳定版 ([1574090](https://github.com/certd/certd/commit/15740904e5e97ee767c39e4401dbc1d645795c38))
|
||||
* 支持AI开发在线插件 ([eae7edb](https://github.com/certd/certd/commit/eae7edbda0fd7357163f33fb4084fcba21396f5f))
|
||||
* **domain:** 域名管理增加remark字段 ([e4041ea](https://github.com/certd/certd/commit/e4041eabece1bab2c1c4b955129cf7a7fe7302d0))
|
||||
* **login,settings:** 仅第三方登录开启后,在重启10分钟后将无法使用密码登录 ([1d12ad7](https://github.com/certd/certd/commit/1d12ad73c6ca827d2eb36bda867f9d9fe62dab48))
|
||||
* **plugin:** 插件安装支持检测依赖 ([e79660c](https://github.com/certd/certd/commit/e79660c5d17ed4049508a00a359950a54470332f))
|
||||
* **plugin:** 新增插件评分展示、排序与个人插件筛选功能 ([7ab5029](https://github.com/certd/certd/commit/7ab502991e0c001777b268b9f159311ef5e000a0))
|
||||
* **server:** response优化为gzip压缩 ([f38522c](https://github.com/certd/certd/commit/f38522ca0b2185f7f35538f53cb51dcf86d0bfc8))
|
||||
* **tencent-clb:** 重构腾讯云CLB部署插件的地域选择组件,改为远程下拉选择器 ([30d4fc8](https://github.com/certd/certd/commit/30d4fc84a9e940d1d07e1f25856eb6fa13da9ba1))
|
||||
|
||||
# [1.43.0](https://github.com/certd/certd/compare/v1.42.6...v1.43.0) (2026-08-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 优化火山引擎dcdn,单个单个部署,规避超时问题 ([cf008b9](https://github.com/certd/certd/commit/cf008b9cff54542dc551fb159578dbd49006229c))
|
||||
* **pipeline:** 修复过期/损坏的安装锁导致的依赖安装失败 ([1017a38](https://github.com/certd/certd/commit/1017a38eb48d6778745d4905d9ec4f6067a261d8))
|
||||
* **ui:** 修复oauth设置页保存后未正确刷新的问题 ([8ab699a](https://github.com/certd/certd/commit/8ab699a9104b47e02a88ad764e995a495deb775e))
|
||||
* **unicloud:** 修复unicloud登录接口改版后请求失败的问题 ([18583f6](https://github.com/certd/certd/commit/18583f6ce025fa8d365fad5a8c9c6020f3bcfbae))
|
||||
|
||||
### Features
|
||||
|
||||
* 优化开放接口证书自动申请功能 ([6339cdf](https://github.com/certd/certd/commit/6339cdffa9f79505dada5617d8c7fcfacc419a2e))
|
||||
* **pipeline:** 新增流水线批量编辑、阶段宽度调整功能 ([e5a1844](https://github.com/certd/certd/commit/e5a18448522be88bc504a45a46ad599f3c39ca30))
|
||||
* **ssh:** ssh连接支持跳板机 ([2cb4e55](https://github.com/certd/certd/commit/2cb4e551fa7616f587a2d907f9b513f7df3a9957))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 部署到京东云CDN支持设置跳转类型 ([e992192](https://github.com/certd/certd/commit/e9921928df011c2435af73b68ebc3d773f13c16a))
|
||||
* 统一通知类型常量管理,新增开放接口通知能力 ([edb249c](https://github.com/certd/certd/commit/edb249c2ad3c173dbfd04d8d156f895a6706ffd1))
|
||||
* 优化宝塔报错提示 ([1cfa766](https://github.com/certd/certd/commit/1cfa76683b40da9e9198552f2226419d4afa86a3))
|
||||
* 优化第三方登录设置提示 ([1468eb1](https://github.com/certd/certd/commit/1468eb1ef292a9e21a2a43131b86b8acb3688307))
|
||||
* 优化第三方依赖安装失败日志输出 ([18b2d3a](https://github.com/certd/certd/commit/18b2d3ac2078d0c6636af897f8f66e4eb5ff832c))
|
||||
* 优化SqliteError: disk I/O error报错修复提示 ([7d22fe3](https://github.com/certd/certd/commit/7d22fe3d7d0de97bae2ceedd051a0c16a1cd90e3))
|
||||
* 证书成功通知增加有效期显示 ([f1b6704](https://github.com/certd/certd/commit/f1b67049d16ebcabe6d6d9b6cb53a8a2b7a9265c))
|
||||
* **cert-plugin:** 证书申请支持失败重试 ([0ddb1f6](https://github.com/certd/certd/commit/0ddb1f69d2a46b993e792862ce9952336d532f5b))
|
||||
* Improve English localization coverage ([#778](https://github.com/certd/certd/issues/778)) @MephistoJB ([bd82147](https://github.com/certd/certd/commit/bd821474969a22078b9d2e73518e765b8c9a26c7))
|
||||
* **install:** 补充迁移到PostgreSQL后,表自增序列从1开始的修复SQL脚本和说明 ([80374ca](https://github.com/certd/certd/commit/80374cae8a43fd17eb180381a093267e466a0bc2))
|
||||
* **plugin-ucloud:** ucloud us3部署插件增加region选择 ([8165a5f](https://github.com/certd/certd/commit/8165a5ff8986e759948c06f924d12e605b2ec8f6))
|
||||
* **tencent-clb:** 为腾讯云CLB部署插件支持远程选择输入 ([967846b](https://github.com/certd/certd/commit/967846bef590b76d27e472180b96372ff55cfb6f))
|
||||
* **ui:** 域名选择器支持点击复制域名 ([b173311](https://github.com/certd/certd/commit/b173311468d2c1f318d1333257c3ebcf916da071))
|
||||
|
||||
## [1.42.6](https://github.com/certd/certd/compare/v1.42.5...v1.42.6) (2026-07-18)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复正常批量删除流水线报权限不足的bug ([5b50083](https://github.com/certd/certd/commit/5b500830a122c6c42dab054e57fed509050f94da))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 优化动态加载依赖镜像地址,多次重试 ([5f53b81](https://github.com/certd/certd/commit/5f53b81c75dd242b4260ac08cae14c6d1a08a883))
|
||||
|
||||
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复上传到cos报runtimeDepsService未初始化的问题 ([167b303](https://github.com/certd/certd/commit/167b303faeca02cc11cf97e4be2a3df914852167))
|
||||
* 修复dingtalk通知格式没有换行的bug ([7ed1be9](https://github.com/certd/certd/commit/7ed1be994f8b4b74cdeb38743060c912c027248b))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 给SQLITE_IOERR_WRITE增加友好报错提示,将certd:latest镜像改为certd:slim ([b91c9e4](https://github.com/certd/certd/commit/b91c9e4ea671cb359ef164e27864de1d66cba9d3))
|
||||
* 优化vke keubconfig获取方式,改成先查询,如果没有再创建临时config ([604fa5b](https://github.com/certd/certd/commit/604fa5be634d099d797bfee5c2b0f26ce0ac8461))
|
||||
|
||||
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复火山引擎查不到自定义源站域名的问题 ([02dabe1](https://github.com/certd/certd/commit/02dabe11db3e9b13ca4621ce9ddd2b808bfca390))
|
||||
* 修复火山引擎自定义源站域名查询不到的问题 ([e44bf9d](https://github.com/certd/certd/commit/e44bf9d77375d48ac7fd1582e69fae02dfd248fa))
|
||||
* **pipeline:** 重构运行时依赖加载逻辑,修复火山引擎DNS解析报runtimeDepsService未初始化的bug ([ec69b8f](https://github.com/certd/certd/commit/ec69b8f11bfd4b20991aef74a72a47182ca79a9d))
|
||||
|
||||
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **volcengine-alb:** 修复火山引擎ALB 默认证书部署类型会部署到扩展证书的问题 ([0a068a2](https://github.com/certd/certd/commit/0a068a274673e9768954e9f7367c267d44f3b530))
|
||||
|
||||
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复火山视频点播源站选择不到自定义源站的bug ([0071bcb](https://github.com/certd/certd/commit/0071bcb0e4dd108c86d7ca01820a9f6e6960e440))
|
||||
* 修复企业模式下弹出邮箱绑定提醒的问题 ([8d9dad9](https://github.com/certd/certd/commit/8d9dad9c82f6f2fd3ab3040068946a33f37145b1))
|
||||
* 修复AsiaIsp CDN证书重复情况下部署失败的问题 ([c3d6db3](https://github.com/certd/certd/commit/c3d6db3f1ef2f1c897b7989521fe8809dffaded1))
|
||||
* 修复cname用阿里云校验时报找不到runtimeDepsService的错误 ([072edd7](https://github.com/certd/certd/commit/072edd7affee424ab3411f4d41d338f084d7cac6))
|
||||
|
||||
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复多域名无法使用passkey登录的bug ([d176f9c](https://github.com/certd/certd/commit/d176f9cc0ebd051a614bfac74d1616d1945fc9a3))
|
||||
* 修复企业模式下登录报projectId不能为空的问题 ([a65366b](https://github.com/certd/certd/commit/a65366bbe1aadea8baaffbdadab58a5b631d9417))
|
||||
* **login:** 修复输入法 composing 状态下回车触发提交的问题 ([b74db81](https://github.com/certd/certd/commit/b74db81304bbe68476bbec5ea4307a2264060e92))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **certd-server:** 使用 jks-go转换jks证书,大幅精简镜像大小 ([c78898e](https://github.com/certd/certd/commit/c78898e4c10dd1701467d2e42e3f72bd8f2a352f))
|
||||
* **pipeline:** 将默认历史保留条数从30调整为100 ([d3e4677](https://github.com/certd/certd/commit/d3e4677ea4fac8e7533749d7f4187e410489e536))
|
||||
|
||||
# [1.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复jdk证书格式的问题 ([260f5ae](https://github.com/certd/certd/commit/260f5ae777b83493b0c578fe30fd00ec0c873226))
|
||||
* 修复telegram - 符号转义问题 ([d5882f1](https://github.com/certd/certd/commit/d5882f16bedb09baf09ace92049b02872620f5dc))
|
||||
* **aliyun:** 修复阿里云CDN/DCDN根据证书自动匹配不到证书的bug ([1ae185d](https://github.com/certd/certd/commit/1ae185d0bc356f4678bc38ca0582ce3396f82ebe))
|
||||
|
||||
### Features
|
||||
|
||||
* 通过插件配置懒加载依赖,动态加载第三方依赖包,精简安装镜像大小 ([01568ca](https://github.com/certd/certd/commit/01568ca1489069046b5a89ebdd4ced2f7f6ddf93))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 阿里云ESA证书部署支持SaaS模式 ([82276b5](https://github.com/certd/certd/commit/82276b53a8474a18a3d0237050907c994fc748f0))
|
||||
* 【破坏性更新】 证书压缩包不再生成文件存储,而是实时打包下载,证书申请插件不再输出certZip ([7cff1a9](https://github.com/certd/certd/commit/7cff1a98424120585205889874b3ef4956a30583))
|
||||
* 火山引擎点播插件支持部署到自定义源站域名 ([095791c](https://github.com/certd/certd/commit/095791cdc2b7c1f4b913b634643afec5e30fe9b0))
|
||||
* 基础镜像改成node:22-trixie-slim,对网络兼容性更好 ([c66a2bd](https://github.com/certd/certd/commit/c66a2bd77ab6dbb3e3fe2c00562b66287a9429ea))
|
||||
* 新增橙域网络(asia-isp) CDN证书部署插件 ([b48831e](https://github.com/certd/certd/commit/b48831e60b0059bef7ef9a34ab61c9dd2f684641))
|
||||
* 优化阿里云API网关增加翻页查询 ([ed58ae3](https://github.com/certd/certd/commit/ed58ae3c5339e4a0238a92acfe7ea6d2f566ea28))
|
||||
* 优化用户体验,首次访问时弹出邮箱账号绑定用以初始化账号 ([608cc2a](https://github.com/certd/certd/commit/608cc2a81ff0b4872c9fe11ed9c9c0b4b90a12a3))
|
||||
* 优化ACME账号字段的选择提示 ([bfd3cac](https://github.com/certd/certd/commit/bfd3cacc687fc5cbc3cb2ca3cadbc140de300dc2))
|
||||
* 支持全自动匹配部署宝塔网站证书 ([4dff48e](https://github.com/certd/certd/commit/4dff48e807c32a7623ec9206cf39c88e88f89f6a))
|
||||
* **cert-plugin:** 调整更新天数自动减半逻辑,仅7天ip证书生效,其他情况下不减半 ([56e5524](https://github.com/certd/certd/commit/56e5524a0f4af3645d70bc3b3ec750b45ba8de10))
|
||||
* dns默认ipv4first ([194463b](https://github.com/certd/certd/commit/194463bea9e797315aa7a724f4b2930701570419))
|
||||
* **passkey:** passkey支持多域名rpid ([79f6586](https://github.com/certd/certd/commit/79f65868ca0f5162bbc2f935ce89abc28011d816))
|
||||
* **plugin:** 在线插件编辑支持配置第三方依赖和插件依赖 ([635f069](https://github.com/certd/certd/commit/635f069012d4193cfb7cb051c96e28eec1247ca2))
|
||||
|
||||
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复设置里面不显示tab页签,导致某些页面需要点击查询按钮才有数据出来的bug ([c1b5a35](https://github.com/certd/certd/commit/c1b5a35f90a7d4b41397717b5c27905bc68e1bfb))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **plugin:** 增加 Dynadot DNS and access 插件 ([a3a215b](https://github.com/certd/certd/commit/a3a215b7ae2b90efcde91270ce4165bbfe77dc64))
|
||||
|
||||
## [1.41.3](https://github.com/certd/certd/compare/v1.41.2...v1.41.3) (2026-06-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复litessl无法申请证书,报authorization must be pending 错误的问题 ([d6cd9d1](https://github.com/certd/certd/commit/d6cd9d136d2812b2335917305f36d6d9414507ad))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 首页夜间模式主图切换为黑色背景 ([15484bc](https://github.com/certd/certd/commit/15484bc119fef7a0ca7f3fdab01d665fde47e688))
|
||||
|
||||
## [1.41.2](https://github.com/certd/certd/compare/v1.41.1...v1.41.2) (2026-06-10)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **cert-plugin:** 修复DNS提供商授权无法回显的bug ([016ae86](https://github.com/certd/certd/commit/016ae865b1d914fe5792e77a08e3ab5358df5f89))
|
||||
* Parse PEM chain and import certificate chain ([#747](https://github.com/certd/certd/issues/747)) ([454912d](https://github.com/certd/certd/commit/454912d31407d350cbd170953ccbd0564e74fd6c))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 添加AWS Rate Limit应对措施 ([#748](https://github.com/certd/certd/issues/748)) ([56b8c68](https://github.com/certd/certd/commit/56b8c689ec2b5cff49010a8c765483dd36803e9d))
|
||||
* 新增站点证书监控从DNS解析记录批量导入功能 ([f9541fa](https://github.com/certd/certd/commit/f9541fab701e01ba57af061da322204c894adfb8))
|
||||
* 优化 HiPM DNSMgr 插件,添加域名查询双层策略 ([#744](https://github.com/certd/certd/issues/744)) @WUHINS ([0f3f851](https://github.com/certd/certd/commit/0f3f8519e04d95cb848e28b98a3d4fcbed481fce))
|
||||
|
||||
### Reverts
|
||||
|
||||
* Revert "perf: 添加AWS Rate Limit应对措施 (#748)" (#749) ([5e8bdac](https://github.com/certd/certd/commit/5e8bdac00850bed4f5f2a272bee42c490730ec21)), closes [#748](https://github.com/certd/certd/issues/748) [#749](https://github.com/certd/certd/issues/749)
|
||||
|
||||
## [1.41.1](https://github.com/certd/certd/compare/v1.41.0...v1.41.1) (2026-06-05)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
+15
-3
@@ -1,20 +1,32 @@
|
||||
# 镜像说明
|
||||
|
||||
## 镜像版本说明
|
||||
|
||||
| 版本标签 | 基础系统 | OpenJDK | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `latest` / `[version]` | Alpine Linux | 有 | **推荐**,默认版本,镜像体积更小,功能完整,支持所有插件 |
|
||||
| `slim` / `[version]-slim` | Debian slim | 有 | glibc 兼容性更好,适用于依赖 glibc 的特殊场景 |
|
||||
| `armv7` / `[version]-armv7` | Alpine Linux | 有 | ARMv7 架构专用版本 |
|
||||
|
||||
## 国内镜像地址:
|
||||
|
||||
* `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest`
|
||||
* `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest`
|
||||
* `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:slim`、`[version]-slim`
|
||||
* `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:armv7`、`[version]-armv7`
|
||||
|
||||
## DockerHub地址:
|
||||
* `https://hub.docker.com/r/greper/certd`
|
||||
* `greper/certd:latest`
|
||||
* `greper/certd:slim`、`greper/certd:[version]-slim`
|
||||
* `greper/certd:armv7`、`greper/certd:[version]-armv7`
|
||||
|
||||
## GitHub Packages地址:
|
||||
* `ghcr.io/certd/certd:latest`
|
||||
* `ghcr.io/certd/certd:slim`、`ghcr.io/certd/certd:[version]-slim`
|
||||
* `ghcr.io/certd/certd:armv7`、`ghcr.io/certd/certd:[version]-armv7`
|
||||
*
|
||||
|
||||
## 镜像构建公开
|
||||
镜像构建通过`Actions`自动执行,过程公开透明,请放心使用
|
||||
镜像构建通过`Actions`自动执行,过程公开透明,请放心使用
|
||||
* [点我查看镜像构建日志](https://github.com/certd/certd/actions/workflows/build-image.yml)
|
||||
|
||||

|
||||
+1
-1
@@ -6,7 +6,7 @@ Certd 是一款开源、免费、全自动申请和部署更新SSL证书的工
|
||||
关键字:证书自动申请、证书自动更新、证书自动续期、证书自动续签、证书管理工具
|
||||
|
||||
|
||||
| 官方开源地址: | |
|
||||
| |官方开源地址: |
|
||||
| ---- | ---- |
|
||||
| [Github](https://github.com/certd/certd)|  |
|
||||
| [Gitee](https://gitee.com/certd/certd) |  |
|
||||
|
||||
@@ -103,9 +103,14 @@ docker-compose up -d
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
> 重要: 建议Fetch大小改成1000,避免内存溢出
|
||||
|
||||
下一步、下一步,直到数据加载设置,勾选`在加载前截断目标表`(此选项很重要,并且会清空mysql certd数据库中的数据)
|
||||

|
||||
> 重要: 在插入行后做提交,也改成1000,避免内存溢出
|
||||
|
||||
#### 5. 导入完成
|
||||
|
||||
@@ -115,4 +120,58 @@ docker-compose up -d
|
||||
|
||||
访问MySQL版本测试,数据已成功迁移
|
||||
|
||||
确认没有问题之后,删除旧版certd
|
||||
确认没有问题之后,删除旧版certd
|
||||
|
||||
|
||||
## 三、问题
|
||||
|
||||
### 1. 迁移到PostgreSQL之后,所有表的Sequence会从1开始
|
||||
|
||||
执行如下sql 语句,批量将所有表的id自增设置到当前数据的最大值【仅迁移到postgresql需要执行】
|
||||
```sql
|
||||
DO $$
|
||||
DECLARE
|
||||
rec RECORD;
|
||||
seq_name TEXT;
|
||||
max_id BIGINT;
|
||||
BEGIN
|
||||
-- 遍历所有用户表(排除系统schema)
|
||||
FOR rec IN
|
||||
SELECT table_schema, table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_type = 'BASE TABLE'
|
||||
AND table_schema NOT IN ('pg_catalog', 'information_schema')
|
||||
LOOP
|
||||
-- 检查表是否存在名为 'id' 的列
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = rec.table_schema
|
||||
AND table_name = rec.table_name
|
||||
AND column_name = 'id'
|
||||
) THEN
|
||||
-- 获取该列关联的序列名(如果是 serial 或 identity 列)
|
||||
seq_name := pg_get_serial_sequence(
|
||||
quote_ident(rec.table_schema) || '.' || quote_ident(rec.table_name),
|
||||
'id'
|
||||
);
|
||||
|
||||
IF seq_name IS NOT NULL THEN
|
||||
-- 查询当前表中 id 的最大值
|
||||
EXECUTE format('SELECT MAX(id) FROM %I.%I', rec.table_schema, rec.table_name) INTO max_id;
|
||||
|
||||
IF max_id IS NULL THEN
|
||||
-- 表为空,将序列重置为初始值 1(下次调用 nextval 返回 1)
|
||||
PERFORM setval(seq_name, 1, false);
|
||||
RAISE NOTICE '表 %.% 为空,序列 % 已重置为 1', rec.table_schema, rec.table_name, seq_name;
|
||||
ELSE
|
||||
-- 将序列当前值设置为最大值,下次调用 nextval 返回 max_id + 1
|
||||
PERFORM setval(seq_name, max_id, true);
|
||||
RAISE NOTICE '表 %.% 的序列 % 已设置为 %', rec.table_schema, rec.table_name, seq_name, max_id;
|
||||
END IF;
|
||||
END IF;
|
||||
END IF;
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
```
|
||||
@@ -2,6 +2,50 @@
|
||||
|
||||
## 一、安装
|
||||
|
||||
### 镜像版本选择
|
||||
|
||||
Certd 提供多种 Docker 镜像版本,您可以根据需要选择:
|
||||
|
||||
#### 1. 镜像地址格式:
|
||||
|
||||
```
|
||||
registry.cn-shenzhen.aliyuncs.com/certd/certd:[version-][system-][latest/stable]
|
||||
------------ ↑ 镜像地址 ------------- ↑ 镜像名 -- ↑指定版本- ↑基础系统- ↑最新版本类型
|
||||
```
|
||||
#### 2. 版本标签:
|
||||
|
||||
**最新版本标签:**
|
||||
|
||||
| 版本 | 标签 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 最新预览版【默认】 | `certd:latest` | 指向最新开发版本,包含最新功能,但稳定性不如稳定版 |
|
||||
| 最新稳定版 | `certd:stable` | 指向经过充分测试的生产就绪版本,推荐生产环境使用 |
|
||||
|
||||
**系统分支版本:**
|
||||
|
||||
> 根据基础镜像不同,分为如下三个分支版本,没有特殊需求选择默认的即可(他们功能是一样的)
|
||||
|
||||
| 系统版本 | 版本标签 | 基础系统 | 说明 | 稳定版标签 | 指定版本 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| alpine【默认】 | `certd:latest` | Alpine Linux | 默认版本,镜像体积小,支持x86、ARM架构 | `certd:stable` | `certd:1.43.0` |
|
||||
| slim | `certd:slim` | Debian slim | 基于glibc,dns解析兼容性好 | `certd:slim-stable` | `certd:1.43.0-slim` |
|
||||
| armv7 | `certd:armv7` | Alpine Linux | ARMv7 架构专用版本 | `certd:armv7-stable` | `certd:1.43.0-armv7` |
|
||||
|
||||
#### 3. 镜像地址:
|
||||
|
||||
| 镜像仓库 | 最新预览版 | slim版 | armv7版 |
|
||||
| --- | --- | --- | --- |
|
||||
| 阿里云【默认】 | `registry.cn-shenzhen.aliyuncs.com/certd/certd:latest` | `certd:slim` | `certd:armv7` |
|
||||
| Docker Hub | `certd/certd:latest` | `certd:slim` | `certd:armv7` |
|
||||
| GitHub Packages | `ghcr.io/certd/certd:latest` | `certd:slim` | `certd:armv7` |
|
||||
|
||||
> 1. 后面的各个版本省略了镜像地址,使用时需要将镜像地址拼接完整。
|
||||
> 2. 如果您不确定使用哪个版本,请使用默认的 `certd:latest` 版本。
|
||||
|
||||
最新版本号:
|
||||

|
||||
|
||||
|
||||
### 一键脚本安装(推荐)
|
||||
|
||||
如果您的服务器未安装 Docker,该脚本会自动为您安装 Docker 和 Docker Compose,并启动 Certd 容器。
|
||||
|
||||
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
@@ -47,7 +47,13 @@ header中传入x-certd-token即可调用开放接口
|
||||
|
||||
## 客户端工具
|
||||
|
||||
### SSL-Assistant
|
||||
### 官方客户端:Certd Client
|
||||
`Certd Client` 是 Certd 的官方证书部署客户端。运行在应用服务器上,自动发现本机 Nginx、Apache 和 IIS 站点,然后从 Certd 获取新证书并部署到本机。适用于不希望服务器开放 SSH、无法由 Certd 直接访问目标服务器的场景。
|
||||
|
||||
项目主页:[AtomGit](https://atomgit.com/certd/certd-client/) | [GitHub](https://github.com/certd/certd-client/)
|
||||
下载地址:[AtomGit Releases](https://atomgit.com/certd/certd-client/releases) | [GitHub Releases](https://github.com/certd/certd-client/releases)
|
||||
|
||||
### 第三方客户端:SSL-Assistant
|
||||
`SSL Assistant` 是一个基于 Go 语言开发的跨平台证书部署管理助手。
|
||||
支持自动扫描主机`Nginx`配置,然后从Certd拉取证书并部署。
|
||||
在不想暴露ssh主机密码情况下,该工具非常好用。
|
||||
|
||||
@@ -18,68 +18,70 @@
|
||||
| 14.| **SFTP授权** | |
|
||||
| 15.| **阿里云OSS授权** | 包含地域和Bucket |
|
||||
| 16.| **APISIX授权** | |
|
||||
| 17.| **亚马逊云aws授权** | |
|
||||
| 18.| **亚马逊云科技(国区)授权** | |
|
||||
| 19.| **微软云Azure授权** | |
|
||||
| 20.| **BIND9 DNS 授权** | 通过 SSH 连接到 BIND9 服务器,使用 nsupdate 命令管理 DNS 记录 |
|
||||
| 21.| **CacheFly** | CacheFly |
|
||||
| 22.| **ACME账号** | 用于复用ACME账号私钥和账号地址,证书申请时不再临时创建账号 |
|
||||
| 23.| **EAB授权** | ZeroSSL证书申请需要EAB授权 |
|
||||
| 24.| **google cloud** | 谷歌云授权 |
|
||||
| 25.| **cloudflare授权** | |
|
||||
| 26.| **中国移动CND授权** | |
|
||||
| 27.| **授权插件示例** | 这是一个示例授权插件,用于演示如何实现一个授权插件 |
|
||||
| 28.| **dns.la授权** | |
|
||||
| 29.| **彩虹DNS** | 彩虹DNS管理系统授权 |
|
||||
| 30.| **多吉云** | |
|
||||
| 31.| **Dokploy授权** | |
|
||||
| 32.| **farcdn授权** | |
|
||||
| 33.| **FlexCDN授权** | |
|
||||
| 34.| **Gcore** | Gcore |
|
||||
| 35.| **Github授权** | |
|
||||
| 36.| **godaddy授权** | |
|
||||
| 37.| **HiPM DNSMgr** | HiPM DNSMgr API Token 授权 |
|
||||
| 38.| **金山云授权** | |
|
||||
| 39.| **FTP授权** | |
|
||||
| 40.| **七牛OSS授权** | |
|
||||
| 41.| **腾讯云COS授权** | 腾讯云对象存储授权,包含地域和存储桶 |
|
||||
| 42.| **s3/minio授权** | S3/minio oss授权 |
|
||||
| 43.| **namesilo授权** | |
|
||||
| 44.| **Next Terminal 授权** | 用于访问 Next Terminal API 的授权配置 |
|
||||
| 45.| **Nginx Proxy Manager 授权** | 用于登录 Nginx Proxy Manager,并为代理主机证书部署提供授权。 |
|
||||
| 46.| **1panel授权** | 账号和密码 |
|
||||
| 47.| **支付宝** | |
|
||||
| 48.| **白山云授权** | |
|
||||
| 49.| **宝塔云WAF授权** | 用于连接和管理宝塔云WAF服务的授权配置 |
|
||||
| 50.| **cdnfly授权** | |
|
||||
| 51.| **k8s授权** | |
|
||||
| 52.| **括彩云cdn授权** | 括彩云CDN,每月免费30G,[注册即领](https://kuocaicdn.com/register?code=8mn536rrzfbf8) |
|
||||
| 53.| **LeCDN授权** | |
|
||||
| 54.| **lucky** | |
|
||||
| 55.| **猫云授权** | |
|
||||
| 56.| **plesk授权** | |
|
||||
| 57.| **长亭雷池授权** | |
|
||||
| 58.| **群晖登录授权** | |
|
||||
| 59.| **uniCloud** | unicloud授权 |
|
||||
| 60.| **微信支付** | |
|
||||
| 61.| **易盾rcdn授权** | 易盾CDN,每月免费30G,[注册即领](https://rhcdn.yiduncdn.com/register?code=8mn536rrzfbf8) |
|
||||
| 62.| **易发云短信** | sms.yfyidc.cn/ |
|
||||
| 63.| **易盾DCDN授权** | https://user.yiduncdn.com |
|
||||
| 64.| **易支付** | |
|
||||
| 65.| **proxmox** | |
|
||||
| 66.| **Spaceship.com 授权** | Spaceship.com API 授权插件 |
|
||||
| 67.| **Technitium DNS Server** | Technitium DNS Server 自建DNS服务器授权 |
|
||||
| 68.| **UCloud授权** | 优刻得授权 |
|
||||
| 69.| **又拍云** | |
|
||||
| 70.| **网宿授权** | |
|
||||
| 71.| **西部数码授权** | |
|
||||
| 72.| **我爱云授权** | 我爱云CDN |
|
||||
| 73.| **新网授权(代理方式)** | |
|
||||
| 74.| **新网授权** | |
|
||||
| 75.| **新网互联授权** | 仅支持代理账号,ip需要加入白名单 |
|
||||
| 76.| **Zenlayer授权** | Zenlayer授权 |
|
||||
| 77.| **GoEdge授权** | |
|
||||
| 78.| **雨云授权** | https://app.rainyun.com/ |
|
||||
| 17.| **橙域网络(asia-isp)授权** | 橙域网络CDN API授权,用于部署证书到橙域CDN |
|
||||
| 18.| **亚马逊云aws授权** | |
|
||||
| 19.| **亚马逊云科技(国区)授权** | |
|
||||
| 20.| **微软云Azure授权** | |
|
||||
| 21.| **BIND9 DNS 授权** | 通过 SSH 连接到 BIND9 服务器,使用 nsupdate 命令管理 DNS 记录 |
|
||||
| 22.| **CacheFly** | CacheFly |
|
||||
| 23.| **ACME账号** | 用于复用ACME账号私钥和账号地址,证书申请时不再临时创建账号 |
|
||||
| 24.| **EAB授权** | ZeroSSL证书申请需要EAB授权 |
|
||||
| 25.| **google cloud** | 谷歌云授权 |
|
||||
| 26.| **cloudflare授权** | |
|
||||
| 27.| **中国移动CND授权** | |
|
||||
| 28.| **授权插件示例** | 这是一个示例授权插件,用于演示如何实现一个授权插件 |
|
||||
| 29.| **dns.la授权** | |
|
||||
| 30.| **彩虹DNS** | 彩虹DNS管理系统授权 |
|
||||
| 31.| **多吉云** | |
|
||||
| 32.| **Dokploy授权** | |
|
||||
| 33.| **Dynadot授权** | |
|
||||
| 34.| **farcdn授权** | |
|
||||
| 35.| **FlexCDN授权** | |
|
||||
| 36.| **Gcore** | Gcore |
|
||||
| 37.| **Github授权** | |
|
||||
| 38.| **godaddy授权** | |
|
||||
| 39.| **HiPM DNSMgr** | HiPM DNSMgr API Token 授权 |
|
||||
| 40.| **金山云授权** | |
|
||||
| 41.| **FTP授权** | |
|
||||
| 42.| **七牛OSS授权** | |
|
||||
| 43.| **腾讯云COS授权** | 腾讯云对象存储授权,包含地域和存储桶 |
|
||||
| 44.| **s3/minio授权** | S3/minio oss授权 |
|
||||
| 45.| **namesilo授权** | |
|
||||
| 46.| **Next Terminal 授权** | 用于访问 Next Terminal API 的授权配置 |
|
||||
| 47.| **Nginx Proxy Manager 授权** | 用于登录 Nginx Proxy Manager,并为代理主机证书部署提供授权。 |
|
||||
| 48.| **1panel授权** | 账号和密码 |
|
||||
| 49.| **支付宝** | |
|
||||
| 50.| **白山云授权** | |
|
||||
| 51.| **宝塔云WAF授权** | 用于连接和管理宝塔云WAF服务的授权配置 |
|
||||
| 52.| **cdnfly授权** | |
|
||||
| 53.| **k8s授权** | |
|
||||
| 54.| **括彩云cdn授权** | 括彩云CDN,每月免费30G,[注册即领](https://kuocaicdn.com/register?code=8mn536rrzfbf8) |
|
||||
| 55.| **LeCDN授权** | |
|
||||
| 56.| **lucky** | |
|
||||
| 57.| **猫云授权** | |
|
||||
| 58.| **plesk授权** | |
|
||||
| 59.| **长亭雷池授权** | |
|
||||
| 60.| **群晖登录授权** | |
|
||||
| 61.| **uniCloud** | unicloud授权 |
|
||||
| 62.| **微信支付** | |
|
||||
| 63.| **易盾rcdn授权** | 易盾CDN,每月免费30G,[注册即领](https://rhcdn.yiduncdn.com/register?code=8mn536rrzfbf8) |
|
||||
| 64.| **易发云短信** | sms.yfyidc.cn/ |
|
||||
| 65.| **易盾DCDN授权** | https://user.yiduncdn.com |
|
||||
| 66.| **易支付** | |
|
||||
| 67.| **proxmox** | |
|
||||
| 68.| **Spaceship.com 授权** | Spaceship.com API 授权插件 |
|
||||
| 69.| **Technitium DNS Server** | Technitium DNS Server 自建DNS服务器授权 |
|
||||
| 70.| **UCloud授权** | 优刻得授权 |
|
||||
| 71.| **又拍云** | |
|
||||
| 72.| **网宿授权** | |
|
||||
| 73.| **西部数码授权** | |
|
||||
| 74.| **我爱云授权** | 我爱云CDN |
|
||||
| 75.| **新网授权(代理方式)** | |
|
||||
| 76.| **新网授权** | |
|
||||
| 77.| **新网互联授权** | 仅支持代理账号,ip需要加入白名单 |
|
||||
| 78.| **Zenlayer授权** | Zenlayer授权 |
|
||||
| 79.| **GoEdge授权** | |
|
||||
| 80.| **雨云授权** | https://app.rainyun.com/ |
|
||||
|
||||
<style module>
|
||||
table th:first-of-type {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
# 任务插件
|
||||
共 `132` 款任务插件
|
||||
共 `135` 款任务插件
|
||||
## 1. 证书申请
|
||||
|
||||
| 序号 | 名称 | 说明 |
|
||||
|-----|-----|-----|
|
||||
| 1.| **证书申请(JS版)** | 免费通配符域名证书申请,支持多个域名打到同一个证书上 |
|
||||
| 2.| **已有证书托管** | 手动上传自定义证书后,自动部署(每次证书有更新,都需要手动上传一次) |
|
||||
| 3.| **获取阿里云订阅证书** | 从阿里云拉取订阅模式的商用证书(支持 API 1.0 和 2.0) |
|
||||
| 4.| **证书申请(Lego)** | 支持海量DNS解析提供商,推荐使用,一样的免费通配符域名证书申请,支持多个域名打到同一个证书上 |
|
||||
| 1.| **吊销旧证书** | 新证书申请成功,并全部部署成功后,吊销本流水线产生的旧证书。(注意 如果本流水线证书还在别处有使用或手动部署,请慎重添加此吊销任务) |
|
||||
| 2.| **证书申请(JS版)** | 免费通配符域名证书申请,支持多个域名打到同一个证书上 |
|
||||
| 3.| **已有证书托管** | 手动上传自定义证书后,自动部署(每次证书有更新,都需要手动上传一次) |
|
||||
| 4.| **获取阿里云订阅证书** | 从阿里云拉取订阅模式的商用证书(支持 API 1.0 和 2.0) |
|
||||
| 5.| **证书申请(Lego)** | 支持海量DNS解析提供商,推荐使用,一样的免费通配符域名证书申请,支持多个域名打到同一个证书上 |
|
||||
## 2. 主机
|
||||
|
||||
| 序号 | 名称 | 说明 |
|
||||
@@ -25,30 +26,31 @@
|
||||
| 序号 | 名称 | 说明 |
|
||||
|-----|-----|-----|
|
||||
| 1.| **APISIX-更新证书** | 自动更新APISIX证书 |
|
||||
| 2.| **CacheFly-部署证书到CacheFly** | 部署证书到 CacheFly |
|
||||
| 3.| **中国移动-部署证书到CDN** | 中国移动自动部署证书到CDN |
|
||||
| 4.| **多吉云-部署到多吉云CDN** | |
|
||||
| 5.| **farcdn-更新证书** | www.farcdn.net |
|
||||
| 6.| **FlexCDN-更新证书** | |
|
||||
| 7.| **Gcore-刷新Gcore证书** | 刷新现有的证书 |
|
||||
| 8.| **Gcore-部署证书到Gcore** | 仅上传 并不会部署到cdn |
|
||||
| 9.| **GoEdge-更新证书** | GoEdge |
|
||||
| 10.| **金山云-更新CDN证书** | 金山云自动更新CDN证书 |
|
||||
| 11.| **白山云-更新证书** | |
|
||||
| 12.| **cdnfly-部署证书到cdnfly** | cdnfly |
|
||||
| 13.| **天翼云-部署证书到CDN** | 部署证书到天翼云CDN和全站加速 |
|
||||
| 14.| **括彩云-部署到括彩云CDN** | 括彩云CDN,每月免费30G,[注册即领](https://kuocaicdn.com/register?code=8mn536rrzfbf8) |
|
||||
| 15.| **LeCDN-更新证书V2** | 支持新版本LeCDN |
|
||||
| 16.| **LeCDN-更新证书** | |
|
||||
| 17.| **Maoyun-更新猫云CDN证书** | |
|
||||
| 18.| **易盾-部署到易盾DCDN** | 主要是防御,http://user.yiduncdn.com/ |
|
||||
| 19.| **易盾-部署到易盾RCDN** | 易盾CDN,每月免费30G,[注册即领](https://rhcdn.yiduncdn.com/register?code=8mn536rrzfbf8) |
|
||||
| 20.| **雨云-更新证书** | app.rainyun.com |
|
||||
| 21.| **又拍云-部署证书到CDN/USS** | 支持又拍云CDN,又拍云云存储USS |
|
||||
| 22.| **网宿-更新证书** | 网宿证书自动更新 |
|
||||
| 23.| **西数-部署到虚拟主机** | 西部数码部署证书到虚拟主机 |
|
||||
| 24.| **我爱云-部署证书到我爱云CDN** | 部署证书到我爱云CDN |
|
||||
| 25.| **Zenlayer-刷新证书** | 刷新Zenlayer CDN证书 |
|
||||
| 2.| **橙域网络-部署证书到CDN** | 部署证书到橙域网络(asia-isp) CDN加速域名 |
|
||||
| 3.| **CacheFly-部署证书到CacheFly** | 部署证书到 CacheFly |
|
||||
| 4.| **中国移动-部署证书到CDN** | 中国移动自动部署证书到CDN |
|
||||
| 5.| **多吉云-部署到多吉云CDN** | |
|
||||
| 6.| **farcdn-更新证书** | www.farcdn.net |
|
||||
| 7.| **FlexCDN-更新证书** | |
|
||||
| 8.| **Gcore-刷新Gcore证书** | 刷新现有的证书 |
|
||||
| 9.| **Gcore-部署证书到Gcore** | 仅上传 并不会部署到cdn |
|
||||
| 10.| **GoEdge-更新证书** | GoEdge |
|
||||
| 11.| **金山云-更新CDN证书** | 金山云自动更新CDN证书 |
|
||||
| 12.| **白山云-更新证书** | |
|
||||
| 13.| **cdnfly-部署证书到cdnfly** | cdnfly |
|
||||
| 14.| **天翼云-部署证书到CDN** | 部署证书到天翼云CDN和全站加速 |
|
||||
| 15.| **括彩云-部署到括彩云CDN** | 括彩云CDN,每月免费30G,[注册即领](https://kuocaicdn.com/register?code=8mn536rrzfbf8) |
|
||||
| 16.| **LeCDN-更新证书V2** | 支持新版本LeCDN |
|
||||
| 17.| **LeCDN-更新证书** | |
|
||||
| 18.| **Maoyun-更新猫云CDN证书** | |
|
||||
| 19.| **易盾-部署到易盾DCDN** | 主要是防御,http://user.yiduncdn.com/ |
|
||||
| 20.| **易盾-部署到易盾RCDN** | 易盾CDN,每月免费30G,[注册即领](https://rhcdn.yiduncdn.com/register?code=8mn536rrzfbf8) |
|
||||
| 21.| **雨云-更新证书** | app.rainyun.com |
|
||||
| 22.| **又拍云-部署证书到CDN/USS** | 支持又拍云CDN,又拍云云存储USS |
|
||||
| 23.| **网宿-更新证书** | 网宿证书自动更新 |
|
||||
| 24.| **西数-部署到虚拟主机** | 西部数码部署证书到虚拟主机 |
|
||||
| 25.| **我爱云-部署证书到我爱云CDN** | 部署证书到我爱云CDN |
|
||||
| 26.| **Zenlayer-刷新证书** | 刷新Zenlayer CDN证书 |
|
||||
## 4. 面板
|
||||
|
||||
| 序号 | 名称 | 说明 |
|
||||
@@ -62,22 +64,23 @@
|
||||
| 7.| **1Panel-部署面板证书** | 更新1Panel的面板证书 |
|
||||
| 8.| **1Panel-更新站点证书** | 更新1Panel的站点证书 |
|
||||
| 9.| **宝塔-删除过期证书** | 删除证书夹中过期证书 |
|
||||
| 10.| **宝塔-WAF证书部署** | 部署宝塔云WAF/aaWAF |
|
||||
| 11.| **宝塔-面板证书部署** | 部署宝塔面板本身的ssl证书 |
|
||||
| 12.| **宝塔win-网站证书部署** | 部署到Windows版宝塔管理的站点的ssl证书 |
|
||||
| 13.| **宝塔-网站证书部署** | 部署宝塔管理的站点的ssl证书,目前支持宝塔网站站点、docker站点等。本插件也支持aaPanel。 |
|
||||
| 14.| **K8S-Apply自定义yaml** | apply自定义yaml到k8s |
|
||||
| 15.| **K8S-Ingress 证书部署** | 部署证书到k8s的Ingress |
|
||||
| 16.| **K8S-部署证书到Secret** | 部署证书到k8s的secret |
|
||||
| 17.| **lucky-更新Lucky证书** | |
|
||||
| 18.| **Plesk-部署Plesk网站证书** | |
|
||||
| 19.| **Plesk-更新证书** | 不会创建新证书记录,直接更新旧的证书 |
|
||||
| 20.| **雷池-更新证书(支持控制台和防护应用)** | 更新长亭雷池WAF的证书,支持更新控制台和防护应用的证书。 |
|
||||
| 21.| **群晖-部署证书到群晖面板** | Synology,支持6.x以上版本 |
|
||||
| 22.| **群晖-刷新OTP登录有效期** | 群晖登录状态可能30天失效,需要在失效之前登录一次,刷新有效期,您可以将其放在“部署到群晖面板”任务之后 |
|
||||
| 23.| **uniCloud-部署到服务空间** | 部署到服务空间 |
|
||||
| 24.| **Proxmox-上传证书到Proxmox** | |
|
||||
| 25.| **威联通-部署证书到威联通** | 部署证书到qnap |
|
||||
| 10.| **宝塔-全自动部署** | 根据证书域名自动匹配宝塔站点,全自动部署SSL证书。新增加速域名自动感知,自动新增部署 |
|
||||
| 11.| **宝塔-WAF证书部署** | 部署宝塔云WAF/aaWAF |
|
||||
| 12.| **宝塔-面板证书部署** | 部署宝塔面板本身的ssl证书 |
|
||||
| 13.| **宝塔win-网站证书部署** | 部署到Windows版宝塔管理的站点的ssl证书 |
|
||||
| 14.| **宝塔-网站证书部署** | 部署宝塔管理的站点的ssl证书,目前支持宝塔网站站点、docker站点等。本插件也支持aaPanel。 |
|
||||
| 15.| **K8S-Apply自定义yaml** | apply自定义yaml到k8s |
|
||||
| 16.| **K8S-Ingress 证书部署** | 部署证书到k8s的Ingress |
|
||||
| 17.| **K8S-部署证书到Secret** | 部署证书到k8s的secret |
|
||||
| 18.| **lucky-更新Lucky证书** | |
|
||||
| 19.| **Plesk-部署Plesk网站证书** | |
|
||||
| 20.| **Plesk-更新证书** | 不会创建新证书记录,直接更新旧的证书 |
|
||||
| 21.| **雷池-更新证书(支持控制台和防护应用)** | 更新长亭雷池WAF的证书,支持更新控制台和防护应用的证书。 |
|
||||
| 22.| **群晖-部署证书到群晖面板** | Synology,支持6.x以上版本 |
|
||||
| 23.| **群晖-刷新OTP登录有效期** | 群晖登录状态可能30天失效,需要在失效之前登录一次,刷新有效期,您可以将其放在“部署到群晖面板”任务之后 |
|
||||
| 24.| **uniCloud-部署到服务空间** | 部署到服务空间 |
|
||||
| 25.| **Proxmox-上传证书到Proxmox** | |
|
||||
| 26.| **威联通-部署证书到威联通** | 部署证书到qnap |
|
||||
## 5. 阿里云
|
||||
|
||||
| 序号 | 名称 | 说明 |
|
||||
@@ -90,7 +93,7 @@
|
||||
| 6.| **阿里云-部署证书至API网关** | 自动部署域名证书至阿里云API网关(APIGateway) |
|
||||
| 7.| **阿里云-部署证书至CDN** | 自动部署域名证书至阿里云CDN |
|
||||
| 8.| **阿里云-部署证书至DCDN** | 依赖证书申请前置任务,自动部署域名证书至阿里云DCDN |
|
||||
| 9.| **阿里云-部署至ESA** | 部署证书到阿里云ESA(边缘安全加速),自动删除过期证书 |
|
||||
| 9.| **阿里云-部署至ESA** | 部署证书到阿里云ESA(边缘安全加速),支持边缘证书和SaaS证书两种模式 |
|
||||
| 10.| **阿里云-部署至阿里云FC(3.0)** | 部署证书到阿里云函数计算(FC3.0) |
|
||||
| 11.| **阿里云-部署至GA** | 部署证书到阿里云GA(全球加速),支持更新默认证书和扩展证书 |
|
||||
| 12.| **阿里云-部署至直播(Live)** | 部署证书到阿里云视频直播(Live)域名 |
|
||||
@@ -113,7 +116,7 @@
|
||||
|
||||
| 序号 | 名称 | 说明 |
|
||||
|-----|-----|-----|
|
||||
| 1.| **腾讯云-删除即将过期证书** | 仅删除未使用的证书 |
|
||||
| 1.| **腾讯云-删除即将过期证书** | 仅删除即将过期且未使用的证书 |
|
||||
| 2.| **腾讯云-部署证书到任意云资源** | 支持负载均衡、CDN、DDoS、直播、点播、Web应用防火墙、API网关、TEO、容器服务、对象存储、轻应用服务器、云原生微服务、云开发 |
|
||||
| 3.| **腾讯云-部署到CDN(废弃)** | 已废弃,请使用v2版 |
|
||||
| 4.| **腾讯云-部署到CDN-v2** | 推荐使用,支持CDN域名以及COS加速域名 |
|
||||
|
||||
@@ -4,30 +4,31 @@
|
||||
|-----|-----|-----|
|
||||
| 1.| **阿里ESA** | 阿里ESA DNS解析 |
|
||||
| 2.| **阿里云** | 阿里云DNS解析提供商 |
|
||||
| 3.| **AWS Route53** | AWS Route53 DNS解析提供商 |
|
||||
| 4.| **Azure DNS** | Azure DNS 解析提供商 |
|
||||
| 5.| **火山引擎** | 火山引擎DNS解析提供商 |
|
||||
| 6.| **京东云** | 京东云DNS解析提供商 |
|
||||
| 7.| **新网(代理方式)** | 新网域名解析(代理方式) |
|
||||
| 8.| **新网** | 新网域名解析 |
|
||||
| 9.| **BIND9 DNS** | 通过 SSH 连接到 BIND9 服务器,使用 nsupdate 命令管理 DNS 记录 |
|
||||
| 10.| **cloudflare** | cloudflare dns provider |
|
||||
| 11.| **dns.la** | dns.la |
|
||||
| 12.| **godaddy** | GoDaddy |
|
||||
| 13.| **HiPM DNSMgr** | HiPM DNSMgr DNS 解析提供商 |
|
||||
| 14.| **华为云** | 华为云DNS解析提供商 |
|
||||
| 15.| **namesilo** | namesilo dns provider |
|
||||
| 16.| **雨云** | 雨云DNS解析提供商 |
|
||||
| 17.| **Technitium DNS Server** | Technitium DNS Server 自建DNS服务器 |
|
||||
| 18.| **腾讯云** | 腾讯云域名DNS解析提供者 |
|
||||
| 19.| **腾讯云EO DNS** | 腾讯云EO DNS解析提供者 |
|
||||
| 20.| **西部数码** | west dns provider |
|
||||
| 21.| **Google Cloud DNS** | Google Cloud DNS提供商 |
|
||||
| 22.| **Dns提供商Demo** | dns provider示例 |
|
||||
| 23.| **彩虹DNS** | 彩虹DNS管理系统 |
|
||||
| 24.| **Spaceship** | Spaceship 域名解析 |
|
||||
| 25.| **51dns** | 51DNS |
|
||||
| 26.| **新网互联** | 新网互联 |
|
||||
| 3.| **腾讯云** | 腾讯云域名DNS解析提供者 |
|
||||
| 4.| **腾讯云EO DNS** | 腾讯云EO DNS解析提供者 |
|
||||
| 5.| **AWS Route53** | AWS Route53 DNS解析提供商 |
|
||||
| 6.| **Azure DNS** | Azure DNS 解析提供商 |
|
||||
| 7.| **cloudflare** | cloudflare dns provider |
|
||||
| 8.| **华为云** | 华为云DNS解析提供商 |
|
||||
| 9.| **火山引擎** | 火山引擎DNS解析提供商 |
|
||||
| 10.| **Google Cloud DNS** | Google Cloud DNS提供商 |
|
||||
| 11.| **京东云** | 京东云DNS解析提供商 |
|
||||
| 12.| **新网(代理方式)** | 新网域名解析(代理方式) |
|
||||
| 13.| **新网** | 新网域名解析 |
|
||||
| 14.| **BIND9 DNS** | 通过 SSH 连接到 BIND9 服务器,使用 nsupdate 命令管理 DNS 记录 |
|
||||
| 15.| **dns.la** | dns.la |
|
||||
| 16.| **Dynadot** | Dynadot DNS提供商 |
|
||||
| 17.| **godaddy** | GoDaddy |
|
||||
| 18.| **HiPM DNSMgr** | HiPM DNSMgr DNS 解析提供商 |
|
||||
| 19.| **namesilo** | namesilo dns provider |
|
||||
| 20.| **雨云** | 雨云DNS解析提供商 |
|
||||
| 21.| **Technitium DNS Server** | Technitium DNS Server 自建DNS服务器 |
|
||||
| 22.| **西部数码** | west dns provider |
|
||||
| 23.| **Dns提供商Demo** | dns provider示例 |
|
||||
| 24.| **彩虹DNS** | 彩虹DNS管理系统 |
|
||||
| 25.| **Spaceship** | Spaceship 域名解析 |
|
||||
| 26.| **51dns** | 51DNS |
|
||||
| 27.| **新网互联** | 新网互联 |
|
||||
|
||||
<style module>
|
||||
table th:first-of-type {
|
||||
|
||||
@@ -46,8 +46,9 @@ docker compose up -d
|
||||
> 如果忘记管理员账号,请查看修改密码时的启动日志,会打印管理员账号名
|
||||
|
||||
|
||||
## 二、仅有第三方登录,没有登录窗口
|
||||
## 二、仅有第三方登录,没有登录窗口 / 自动跳到第三方登录等
|
||||
|
||||
当开启仅使用第三方登录模式时,如果第三方登录未配置或已失效,则会导致无法登录
|
||||
服务重启后的前10分钟内,可以访问 `http://你的certd地址/#/login?oauthOnly=false` 显示密码登录入口。
|
||||
|
||||
您可以通过访问 `http://你的certd地址/#/login?oauthOnly=false` 来临时关闭仅使用第三方登录模式,以使用密码登录。
|
||||
> 注意:仅使用第三方登录开启后,仅限于服务重启后的前10分钟内可使用密码登录;超过窗口期即使`oauthOnly=false`,也无法使用密码登录。
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 379 KiB |
@@ -1,15 +1,23 @@
|
||||
# google证书申请教程
|
||||
|
||||
## 1、启用API
|
||||
## 1、 添加流水线
|
||||
|
||||
点击“创建证书流水线”按钮
|
||||
|
||||
## 2、 生成google ACME账号
|
||||
|
||||

|
||||
|
||||
## 3、 获取Google EAB
|
||||
|
||||
### 3.1、启用API
|
||||
打开如下链接,启用 API
|
||||
|
||||
https://console.cloud.google.com/apis/library/publicca.googleapis.com
|
||||
|
||||
打开该链接后点击“启用”,随后等待右侧出现“API已启用”则可以关闭该页。
|
||||
|
||||
## 2、 获取授权
|
||||
以下两种方式任选其一
|
||||
### 2.1 直接获取EAB 【推荐】
|
||||
## 3.2、 创建EAB
|
||||
|
||||
|
||||
1. 打开“Google Cloud Shell”(在右上角点击激活CloudShell图标)。
|
||||
@@ -28,31 +36,13 @@ keyId: xxxxxxxxxxxxx]
|
||||
```
|
||||

|
||||
|
||||
3. 到Certd中,创建一条EAB授权记录,填写keyId(=kid) 和 b64MacKey 信息
|
||||
3. 到Certd中,创建一条EAB授权记录,填写keyId(=kid) 和 b64MacKey 信息
|
||||
注意:keyId没有`]`结尾,不要把`]`也复制了
|
||||
|
||||
## 4、 生成Google ACME账号
|
||||
|
||||
注意:EAB授权使用过一次之后,会绑定邮箱,后续再次使用时,要使用相同的邮箱,所以邮箱切记不要修改
|
||||
否则会报错 `Unknown external account binding (EAB) key. This may be due to the EAB key expiring which occurs 7 days after creation`
|
||||
|
||||
4. 创建证书流水线,选择证书提供商为google,选择EAB授权,运行流水线申请证书
|
||||
|
||||
|
||||
### 2.2 通过google服务账号接口获取授权
|
||||
|
||||
此方式可以自动获取EAB,需要服务端配置代理
|
||||
|
||||
1. 创建服务账号
|
||||
https://console.cloud.google.com/projectselector2/iam-admin/serviceaccounts/create?walkthrough_id=iam--create-service-account&hl=zh-cn#step_index=1
|
||||
|
||||
2. 选择一个项目,进入创建服务账号页面
|
||||
3. 给服务账号起一个名字,点击`创建并继续`
|
||||
4. 向此服务账号授予对项目的访问权限: `选择角色`->`基本`->`Owner`
|
||||
5. 点击完成
|
||||
6. 点击服务账号,进入服务账号详情页面
|
||||
7. 点击`添加密钥`->`创建新密钥`->`JSON`,下载密钥文件
|
||||
8. 将json文件内容粘贴到 certd中 Google服务授权输入框中
|
||||
|
||||
9. 创建证书流水线,选择证书提供商为google, 选择服务账号授权,运行流水线申请证书
|
||||
|
||||
|
||||
## 5、创建证书流水线,运行流水线申请证书
|
||||
|
||||
|
||||
+1
-1
@@ -9,5 +9,5 @@
|
||||
}
|
||||
},
|
||||
"npmClient": "pnpm",
|
||||
"version": "1.41.2"
|
||||
"version": "1.44.2"
|
||||
}
|
||||
|
||||
+7
-6
@@ -15,12 +15,11 @@
|
||||
"vitepress-plugin-lightbox": "^1.0.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "lerna bootstrap --hoist",
|
||||
"start:server": "cd ./packages/ui/certd-server && pnpm start",
|
||||
"start": "cd ./packages/ui/certd-server && pnpm start",
|
||||
"devb": "lerna run dev-build",
|
||||
"i-all": "lerna link && lerna exec npm install ",
|
||||
"publish": "pnpm run prepublishOnly2 && lerna publish --force-publish=pro/plus-core --conventional-commits && pnpm run afterpublishOnly ",
|
||||
"publish2":" npm run pub_all && pnpm run afterpublishOnly",
|
||||
"publish2": " npm run pub_all && pnpm run afterpublishOnly",
|
||||
"afterpublishOnly": "pnpm run copylogs && time /t >trigger/build.trigger && git add ./trigger/build.trigger && git commit -m \"build: trigger build image\" && TIMEOUT /T 10 && pnpm run commitAll",
|
||||
"transform-sql": "cd ./packages/ui/certd-server/db/ && node --experimental-json-modules transform.js",
|
||||
"plugin-doc-gen": "cd ./packages/ui/certd-server/ && pnpm run export-metadata",
|
||||
@@ -28,7 +27,7 @@
|
||||
"commitPro": "cd ./packages/pro/ && git add . && git commit -m \"build: publish\" && git push",
|
||||
"copylogs": "copyfiles \"CHANGELOG.md\" ./docs/guide/changelogs/",
|
||||
"prepublishOnly1": "pnpm run test:unit && pnpm run check && lerna run build ",
|
||||
"prepublishOnly2": "pnpm run test:unit && pnpm run check && pnpm run before-build && lerna run build && pnpm run plugin-doc-gen",
|
||||
"prepublishOnly2": "pnpm run before-build && lerna run build && pnpm run test:unit && pnpm run check && pnpm run plugin-doc-gen",
|
||||
"before-build": "pnpm run transform-sql && cd ./packages/core/basic && time /t >build.md && git add ./build.md && git commit -m \"build: prepare to build\"",
|
||||
"deploy1": "node --experimental-json-modules ./scripts/deploy.js ",
|
||||
"check": "node --experimental-json-modules ./scripts/publish-check.js",
|
||||
@@ -40,18 +39,20 @@
|
||||
"test:unit": "cross-env NODE_ENV=unittest pnpm -r --workspace-concurrency=1 run test:unit",
|
||||
"pub": "echo 1",
|
||||
"dev": "pnpm run -r --parallel compile ",
|
||||
"lint_all": "pnpm run -r --parallel lint ",
|
||||
"pub_all": "node ./scripts/pub-all.js",
|
||||
"release": "time /t >trigger/release.trigger && git add trigger/release.trigger && git commit -m \"build: release\" && git push",
|
||||
"publish_to_atomgit": "node --experimental-json-modules ./scripts/publish-atomgit.js",
|
||||
"publish_to_gitee": "node --experimental-json-modules ./scripts/publish-gitee.js",
|
||||
"publish_to_github": "node --experimental-json-modules ./scripts/publish-github.js",
|
||||
"get_version": "node --experimental-json-modules ./scripts/version.js"
|
||||
"get_version": "node --experimental-json-modules ./scripts/version.js",
|
||||
"stable": "node ./scripts/stable.js",
|
||||
"set-release-stable": "node ./scripts/set-release-stable.js"
|
||||
},
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"copyfiles": "^2.4.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"root": "link:",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"workspaces": [
|
||||
|
||||
@@ -4,10 +4,6 @@
|
||||
"ecmaVersion": 2022,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"dist",
|
||||
"node_modules"
|
||||
],
|
||||
"extends": [
|
||||
"plugin:prettier/recommended",
|
||||
"prettier"
|
||||
@@ -36,5 +32,9 @@
|
||||
"ignoreUrls": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"node_modules/**",
|
||||
"dist/**"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,6 +3,69 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.44.2](https://github.com/publishlab/node-acme-client/compare/v1.44.1...v1.44.2) (2026-09-03)
|
||||
|
||||
**Note:** Version bump only for package @certd/acme-client
|
||||
|
||||
## [1.44.1](https://github.com/publishlab/node-acme-client/compare/v1.44.0...v1.44.1) (2026-09-02)
|
||||
|
||||
**Note:** Version bump only for package @certd/acme-client
|
||||
|
||||
# [1.44.0](https://github.com/publishlab/node-acme-client/compare/v1.43.0...v1.44.0) (2026-08-28)
|
||||
|
||||
### Features
|
||||
|
||||
* 新增证书吊销后置任务与证书状态管理 ([1cc9641](https://github.com/publishlab/node-acme-client/commit/1cc9641f5669710d1e5809995ad395c843855a12))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 新增证书吊销功能,添加证书状态字段 ([5074572](https://github.com/publishlab/node-acme-client/commit/5074572694383c8e1bf2880e26827987683dc62c))
|
||||
* 移除全局反向代理配置,迁移到自定义ACME配置 ([c57110e](https://github.com/publishlab/node-acme-client/commit/c57110e42aada31dd0028b56b5baec1b19722525))
|
||||
|
||||
# [1.43.0](https://github.com/publishlab/node-acme-client/compare/v1.42.6...v1.43.0) (2026-08-14)
|
||||
|
||||
**Note:** Version bump only for package @certd/acme-client
|
||||
|
||||
## [1.42.6](https://github.com/publishlab/node-acme-client/compare/v1.42.5...v1.42.6) (2026-07-18)
|
||||
|
||||
**Note:** Version bump only for package @certd/acme-client
|
||||
|
||||
## [1.42.5](https://github.com/publishlab/node-acme-client/compare/v1.42.4...v1.42.5) (2026-07-15)
|
||||
|
||||
**Note:** Version bump only for package @certd/acme-client
|
||||
|
||||
## [1.42.4](https://github.com/publishlab/node-acme-client/compare/v1.42.3...v1.42.4) (2026-07-11)
|
||||
|
||||
**Note:** Version bump only for package @certd/acme-client
|
||||
|
||||
## [1.42.3](https://github.com/publishlab/node-acme-client/compare/v1.42.2...v1.42.3) (2026-07-08)
|
||||
|
||||
**Note:** Version bump only for package @certd/acme-client
|
||||
|
||||
## [1.42.2](https://github.com/publishlab/node-acme-client/compare/v1.42.1...v1.42.2) (2026-07-07)
|
||||
|
||||
**Note:** Version bump only for package @certd/acme-client
|
||||
|
||||
## [1.42.1](https://github.com/publishlab/node-acme-client/compare/v1.42.0...v1.42.1) (2026-07-06)
|
||||
|
||||
**Note:** Version bump only for package @certd/acme-client
|
||||
|
||||
# [1.42.0](https://github.com/publishlab/node-acme-client/compare/v1.41.4...v1.42.0) (2026-07-05)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 新增橙域网络(asia-isp) CDN证书部署插件 ([b48831e](https://github.com/publishlab/node-acme-client/commit/b48831e60b0059bef7ef9a34ab61c9dd2f684641))
|
||||
|
||||
## [1.41.4](https://github.com/publishlab/node-acme-client/compare/v1.41.3...v1.41.4) (2026-06-14)
|
||||
|
||||
**Note:** Version bump only for package @certd/acme-client
|
||||
|
||||
## [1.41.3](https://github.com/publishlab/node-acme-client/compare/v1.41.2...v1.41.3) (2026-06-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复litessl无法申请证书,报authorization must be pending 错误的问题 ([d6cd9d1](https://github.com/publishlab/node-acme-client/commit/d6cd9d136d2812b2335917305f36d6d9414507ad))
|
||||
|
||||
## [1.41.2](https://github.com/publishlab/node-acme-client/compare/v1.41.1...v1.41.2) (2026-06-10)
|
||||
|
||||
**Note:** Version bump only for package @certd/acme-client
|
||||
|
||||
@@ -147,21 +147,6 @@ const autoOpts = {
|
||||
const certificate = await client.auto(autoOpts);
|
||||
```
|
||||
|
||||
### Challenge priority
|
||||
|
||||
When ordering a certificate using auto mode, `acme-client` uses a priority list when selecting challenges to respond to. Its default value is `['http-01', 'dns-01']` which translates to "use `http-01` if any challenges exist, otherwise fall back to `dns-01`".
|
||||
|
||||
While most challenges can be validated using the method of your choosing, please note that **wildcard certificates can only be validated through `dns-01`**. More information regarding Let's Encrypt challenge types [can be found here](https://letsencrypt.org/docs/challenge-types/).
|
||||
|
||||
To modify challenge priority, provide a list of challenge types in `challengePriority`:
|
||||
|
||||
```js
|
||||
await client.auto({
|
||||
...,
|
||||
challengePriority: ['http-01', 'dns-01'],
|
||||
});
|
||||
```
|
||||
|
||||
### Internal challenge verification
|
||||
|
||||
When using auto mode, `acme-client` will first validate that challenges are satisfied internally before completing the challenge at the ACME provider. In some cases (firewalls, etc) this internal challenge verification might not be possible to complete.
|
||||
|
||||
@@ -1,518 +0,0 @@
|
||||
## Classes
|
||||
|
||||
<dl>
|
||||
<dt><a href="#AcmeClient">AcmeClient</a></dt>
|
||||
<dd><p>AcmeClient</p>
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
## Objects
|
||||
|
||||
<dl>
|
||||
<dt><a href="#Client">Client</a> : <code>object</code></dt>
|
||||
<dd><p>ACME client</p>
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<a name="AcmeClient"></a>
|
||||
|
||||
## AcmeClient
|
||||
AcmeClient
|
||||
|
||||
**Kind**: global class
|
||||
|
||||
* [AcmeClient](#AcmeClient)
|
||||
* [new AcmeClient(opts)](#new_AcmeClient_new)
|
||||
* [.getTermsOfServiceUrl()](#AcmeClient+getTermsOfServiceUrl) ⇒ <code>Promise.<(string\|null)></code>
|
||||
* [.getAccountUrl()](#AcmeClient+getAccountUrl) ⇒ <code>string</code>
|
||||
* [.createAccount([data])](#AcmeClient+createAccount) ⇒ <code>Promise.<object></code>
|
||||
* [.updateAccount([data])](#AcmeClient+updateAccount) ⇒ <code>Promise.<object></code>
|
||||
* [.updateAccountKey(newAccountKey, [data])](#AcmeClient+updateAccountKey) ⇒ <code>Promise.<object></code>
|
||||
* [.createOrder(data)](#AcmeClient+createOrder) ⇒ <code>Promise.<object></code>
|
||||
* [.getOrder(order)](#AcmeClient+getOrder) ⇒ <code>Promise.<object></code>
|
||||
* [.finalizeOrder(order, csr)](#AcmeClient+finalizeOrder) ⇒ <code>Promise.<object></code>
|
||||
* [.getAuthorizations(order)](#AcmeClient+getAuthorizations) ⇒ <code>Promise.<Array.<object>></code>
|
||||
* [.deactivateAuthorization(authz)](#AcmeClient+deactivateAuthorization) ⇒ <code>Promise.<object></code>
|
||||
* [.getChallengeKeyAuthorization(challenge)](#AcmeClient+getChallengeKeyAuthorization) ⇒ <code>Promise.<string></code>
|
||||
* [.verifyChallenge(authz, challenge)](#AcmeClient+verifyChallenge) ⇒ <code>Promise</code>
|
||||
* [.completeChallenge(challenge)](#AcmeClient+completeChallenge) ⇒ <code>Promise.<object></code>
|
||||
* [.waitForValidStatus(item)](#AcmeClient+waitForValidStatus) ⇒ <code>Promise.<object></code>
|
||||
* [.getCertificate(order, [preferredChain])](#AcmeClient+getCertificate) ⇒ <code>Promise.<string></code>
|
||||
* [.revokeCertificate(cert, [data])](#AcmeClient+revokeCertificate) ⇒ <code>Promise</code>
|
||||
* [.auto(opts)](#AcmeClient+auto) ⇒ <code>Promise.<string></code>
|
||||
|
||||
<a name="new_AcmeClient_new"></a>
|
||||
|
||||
### new AcmeClient(opts)
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| opts | <code>object</code> | |
|
||||
| opts.directoryUrl | <code>string</code> | ACME directory URL |
|
||||
| opts.accountKey | <code>buffer</code> \| <code>string</code> | PEM encoded account private key |
|
||||
| [opts.accountUrl] | <code>string</code> | Account URL, default: `null` |
|
||||
| [opts.externalAccountBinding] | <code>object</code> | |
|
||||
| [opts.externalAccountBinding.kid] | <code>string</code> | External account binding KID |
|
||||
| [opts.externalAccountBinding.hmacKey] | <code>string</code> | External account binding HMAC key |
|
||||
| [opts.backoffAttempts] | <code>number</code> | Maximum number of backoff attempts, default: `10` |
|
||||
| [opts.backoffMin] | <code>number</code> | Minimum backoff attempt delay in milliseconds, default: `5000` |
|
||||
| [opts.backoffMax] | <code>number</code> | Maximum backoff attempt delay in milliseconds, default: `30000` |
|
||||
|
||||
**Example**
|
||||
Create ACME client instance
|
||||
```js
|
||||
const client = new acme.Client({
|
||||
directoryUrl: acme.directory.letsencrypt.staging,
|
||||
accountKey: 'Private key goes here',
|
||||
});
|
||||
```
|
||||
**Example**
|
||||
Create ACME client instance
|
||||
```js
|
||||
const client = new acme.Client({
|
||||
directoryUrl: acme.directory.letsencrypt.staging,
|
||||
accountKey: 'Private key goes here',
|
||||
accountUrl: 'Optional account URL goes here',
|
||||
backoffAttempts: 10,
|
||||
backoffMin: 5000,
|
||||
backoffMax: 30000,
|
||||
});
|
||||
```
|
||||
**Example**
|
||||
Create ACME client with external account binding
|
||||
```js
|
||||
const client = new acme.Client({
|
||||
directoryUrl: 'https://acme-provider.example.com/directory-url',
|
||||
accountKey: 'Private key goes here',
|
||||
externalAccountBinding: {
|
||||
kid: 'YOUR-EAB-KID',
|
||||
hmacKey: 'YOUR-EAB-HMAC-KEY',
|
||||
},
|
||||
});
|
||||
```
|
||||
<a name="AcmeClient+getTermsOfServiceUrl"></a>
|
||||
|
||||
### acmeClient.getTermsOfServiceUrl() ⇒ <code>Promise.<(string\|null)></code>
|
||||
Get Terms of Service URL if available
|
||||
|
||||
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
|
||||
**Returns**: <code>Promise.<(string\|null)></code> - ToS URL
|
||||
**Example**
|
||||
Get Terms of Service URL
|
||||
```js
|
||||
const termsOfService = client.getTermsOfServiceUrl();
|
||||
|
||||
if (!termsOfService) {
|
||||
// CA did not provide Terms of Service
|
||||
}
|
||||
```
|
||||
<a name="AcmeClient+getAccountUrl"></a>
|
||||
|
||||
### acmeClient.getAccountUrl() ⇒ <code>string</code>
|
||||
Get current account URL
|
||||
|
||||
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
|
||||
**Returns**: <code>string</code> - Account URL
|
||||
**Throws**:
|
||||
|
||||
- <code>Error</code> No account URL found
|
||||
|
||||
**Example**
|
||||
Get current account URL
|
||||
```js
|
||||
try {
|
||||
const accountUrl = client.getAccountUrl();
|
||||
}
|
||||
catch (e) {
|
||||
// No account URL exists, need to create account first
|
||||
}
|
||||
```
|
||||
<a name="AcmeClient+createAccount"></a>
|
||||
|
||||
### acmeClient.createAccount([data]) ⇒ <code>Promise.<object></code>
|
||||
Create a new account
|
||||
|
||||
https://datatracker.ietf.org/doc/html/rfc8555#section-7.3
|
||||
|
||||
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
|
||||
**Returns**: <code>Promise.<object></code> - Account
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [data] | <code>object</code> | Request data |
|
||||
|
||||
**Example**
|
||||
Create a new account
|
||||
```js
|
||||
const account = await client.createAccount({
|
||||
termsOfServiceAgreed: true,
|
||||
});
|
||||
```
|
||||
**Example**
|
||||
Create a new account with contact info
|
||||
```js
|
||||
const account = await client.createAccount({
|
||||
termsOfServiceAgreed: true,
|
||||
contact: ['mailto:test@example.com'],
|
||||
});
|
||||
```
|
||||
<a name="AcmeClient+updateAccount"></a>
|
||||
|
||||
### acmeClient.updateAccount([data]) ⇒ <code>Promise.<object></code>
|
||||
Update existing account
|
||||
|
||||
https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.2
|
||||
|
||||
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
|
||||
**Returns**: <code>Promise.<object></code> - Account
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [data] | <code>object</code> | Request data |
|
||||
|
||||
**Example**
|
||||
Update existing account
|
||||
```js
|
||||
const account = await client.updateAccount({
|
||||
contact: ['mailto:foo@example.com'],
|
||||
});
|
||||
```
|
||||
<a name="AcmeClient+updateAccountKey"></a>
|
||||
|
||||
### acmeClient.updateAccountKey(newAccountKey, [data]) ⇒ <code>Promise.<object></code>
|
||||
Update account private key
|
||||
|
||||
https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.5
|
||||
|
||||
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
|
||||
**Returns**: <code>Promise.<object></code> - Account
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| newAccountKey | <code>buffer</code> \| <code>string</code> | New PEM encoded private key |
|
||||
| [data] | <code>object</code> | Additional request data |
|
||||
|
||||
**Example**
|
||||
Update account private key
|
||||
```js
|
||||
const newAccountKey = 'New private key goes here';
|
||||
const result = await client.updateAccountKey(newAccountKey);
|
||||
```
|
||||
<a name="AcmeClient+createOrder"></a>
|
||||
|
||||
### acmeClient.createOrder(data) ⇒ <code>Promise.<object></code>
|
||||
Create a new order
|
||||
|
||||
https://datatracker.ietf.org/doc/html/rfc8555#section-7.4
|
||||
|
||||
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
|
||||
**Returns**: <code>Promise.<object></code> - Order
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| data | <code>object</code> | Request data |
|
||||
|
||||
**Example**
|
||||
Create a new order
|
||||
```js
|
||||
const order = await client.createOrder({
|
||||
identifiers: [
|
||||
{ type: 'dns', value: 'example.com' },
|
||||
{ type: 'dns', value: 'test.example.com' },
|
||||
],
|
||||
});
|
||||
```
|
||||
<a name="AcmeClient+getOrder"></a>
|
||||
|
||||
### acmeClient.getOrder(order) ⇒ <code>Promise.<object></code>
|
||||
Refresh order object from CA
|
||||
|
||||
https://datatracker.ietf.org/doc/html/rfc8555#section-7.4
|
||||
|
||||
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
|
||||
**Returns**: <code>Promise.<object></code> - Order
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| order | <code>object</code> | Order object |
|
||||
|
||||
**Example**
|
||||
```js
|
||||
const order = { ... }; // Previously created order object
|
||||
const result = await client.getOrder(order);
|
||||
```
|
||||
<a name="AcmeClient+finalizeOrder"></a>
|
||||
|
||||
### acmeClient.finalizeOrder(order, csr) ⇒ <code>Promise.<object></code>
|
||||
Finalize order
|
||||
|
||||
https://datatracker.ietf.org/doc/html/rfc8555#section-7.4
|
||||
|
||||
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
|
||||
**Returns**: <code>Promise.<object></code> - Order
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| order | <code>object</code> | Order object |
|
||||
| csr | <code>buffer</code> \| <code>string</code> | PEM encoded Certificate Signing Request |
|
||||
|
||||
**Example**
|
||||
Finalize order
|
||||
```js
|
||||
const order = { ... }; // Previously created order object
|
||||
const csr = { ... }; // Previously created Certificate Signing Request
|
||||
const result = await client.finalizeOrder(order, csr);
|
||||
```
|
||||
<a name="AcmeClient+getAuthorizations"></a>
|
||||
|
||||
### acmeClient.getAuthorizations(order) ⇒ <code>Promise.<Array.<object>></code>
|
||||
Get identifier authorizations from order
|
||||
|
||||
https://datatracker.ietf.org/doc/html/rfc8555#section-7.5
|
||||
|
||||
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
|
||||
**Returns**: <code>Promise.<Array.<object>></code> - Authorizations
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| order | <code>object</code> | Order |
|
||||
|
||||
**Example**
|
||||
Get identifier authorizations
|
||||
```js
|
||||
const order = { ... }; // Previously created order object
|
||||
const authorizations = await client.getAuthorizations(order);
|
||||
|
||||
authorizations.forEach((authz) => {
|
||||
const { challenges } = authz;
|
||||
});
|
||||
```
|
||||
<a name="AcmeClient+deactivateAuthorization"></a>
|
||||
|
||||
### acmeClient.deactivateAuthorization(authz) ⇒ <code>Promise.<object></code>
|
||||
Deactivate identifier authorization
|
||||
|
||||
https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.2
|
||||
|
||||
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
|
||||
**Returns**: <code>Promise.<object></code> - Authorization
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| authz | <code>object</code> | Identifier authorization |
|
||||
|
||||
**Example**
|
||||
Deactivate identifier authorization
|
||||
```js
|
||||
const authz = { ... }; // Identifier authorization resolved from previously created order
|
||||
const result = await client.deactivateAuthorization(authz);
|
||||
```
|
||||
<a name="AcmeClient+getChallengeKeyAuthorization"></a>
|
||||
|
||||
### acmeClient.getChallengeKeyAuthorization(challenge) ⇒ <code>Promise.<string></code>
|
||||
Get key authorization for ACME challenge
|
||||
|
||||
https://datatracker.ietf.org/doc/html/rfc8555#section-8.1
|
||||
|
||||
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
|
||||
**Returns**: <code>Promise.<string></code> - Key authorization
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| challenge | <code>object</code> | Challenge object returned by API |
|
||||
|
||||
**Example**
|
||||
Get challenge key authorization
|
||||
```js
|
||||
const challenge = { ... }; // Challenge from previously resolved identifier authorization
|
||||
const key = await client.getChallengeKeyAuthorization(challenge);
|
||||
|
||||
// Write key somewhere to satisfy challenge
|
||||
```
|
||||
<a name="AcmeClient+verifyChallenge"></a>
|
||||
|
||||
### acmeClient.verifyChallenge(authz, challenge) ⇒ <code>Promise</code>
|
||||
Verify that ACME challenge is satisfied
|
||||
|
||||
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| authz | <code>object</code> | Identifier authorization |
|
||||
| challenge | <code>object</code> | Authorization challenge |
|
||||
|
||||
**Example**
|
||||
Verify satisfied ACME challenge
|
||||
```js
|
||||
const authz = { ... }; // Identifier authorization
|
||||
const challenge = { ... }; // Satisfied challenge
|
||||
await client.verifyChallenge(authz, challenge);
|
||||
```
|
||||
<a name="AcmeClient+completeChallenge"></a>
|
||||
|
||||
### acmeClient.completeChallenge(challenge) ⇒ <code>Promise.<object></code>
|
||||
Notify CA that challenge has been completed
|
||||
|
||||
https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.1
|
||||
|
||||
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
|
||||
**Returns**: <code>Promise.<object></code> - Challenge
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| challenge | <code>object</code> | Challenge object returned by API |
|
||||
|
||||
**Example**
|
||||
Notify CA that challenge has been completed
|
||||
```js
|
||||
const challenge = { ... }; // Satisfied challenge
|
||||
const result = await client.completeChallenge(challenge);
|
||||
```
|
||||
<a name="AcmeClient+waitForValidStatus"></a>
|
||||
|
||||
### acmeClient.waitForValidStatus(item) ⇒ <code>Promise.<object></code>
|
||||
Wait for ACME provider to verify status on a order, authorization or challenge
|
||||
|
||||
https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.1
|
||||
|
||||
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
|
||||
**Returns**: <code>Promise.<object></code> - Valid order, authorization or challenge
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| item | <code>object</code> | An order, authorization or challenge object |
|
||||
|
||||
**Example**
|
||||
Wait for valid challenge status
|
||||
```js
|
||||
const challenge = { ... };
|
||||
await client.waitForValidStatus(challenge);
|
||||
```
|
||||
**Example**
|
||||
Wait for valid authorization status
|
||||
```js
|
||||
const authz = { ... };
|
||||
await client.waitForValidStatus(authz);
|
||||
```
|
||||
**Example**
|
||||
Wait for valid order status
|
||||
```js
|
||||
const order = { ... };
|
||||
await client.waitForValidStatus(order);
|
||||
```
|
||||
<a name="AcmeClient+getCertificate"></a>
|
||||
|
||||
### acmeClient.getCertificate(order, [preferredChain]) ⇒ <code>Promise.<string></code>
|
||||
Get certificate from ACME order
|
||||
|
||||
https://datatracker.ietf.org/doc/html/rfc8555#section-7.4.2
|
||||
|
||||
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
|
||||
**Returns**: <code>Promise.<string></code> - Certificate
|
||||
|
||||
| Param | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| order | <code>object</code> | | Order object |
|
||||
| [preferredChain] | <code>string</code> | <code>null</code> | Indicate which certificate chain is preferred if a CA offers multiple, by exact issuer common name, default: `null` |
|
||||
|
||||
**Example**
|
||||
Get certificate
|
||||
```js
|
||||
const order = { ... }; // Previously created order
|
||||
const certificate = await client.getCertificate(order);
|
||||
```
|
||||
**Example**
|
||||
Get certificate with preferred chain
|
||||
```js
|
||||
const order = { ... }; // Previously created order
|
||||
const certificate = await client.getCertificate(order, 'DST Root CA X3');
|
||||
```
|
||||
<a name="AcmeClient+revokeCertificate"></a>
|
||||
|
||||
### acmeClient.revokeCertificate(cert, [data]) ⇒ <code>Promise</code>
|
||||
Revoke certificate
|
||||
|
||||
https://datatracker.ietf.org/doc/html/rfc8555#section-7.6
|
||||
|
||||
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| cert | <code>buffer</code> \| <code>string</code> | PEM encoded certificate |
|
||||
| [data] | <code>object</code> | Additional request data |
|
||||
|
||||
**Example**
|
||||
Revoke certificate
|
||||
```js
|
||||
const certificate = { ... }; // Previously created certificate
|
||||
const result = await client.revokeCertificate(certificate);
|
||||
```
|
||||
**Example**
|
||||
Revoke certificate with reason
|
||||
```js
|
||||
const certificate = { ... }; // Previously created certificate
|
||||
const result = await client.revokeCertificate(certificate, {
|
||||
reason: 4,
|
||||
});
|
||||
```
|
||||
<a name="AcmeClient+auto"></a>
|
||||
|
||||
### acmeClient.auto(opts) ⇒ <code>Promise.<string></code>
|
||||
Auto mode
|
||||
|
||||
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
|
||||
**Returns**: <code>Promise.<string></code> - Certificate
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| opts | <code>object</code> | |
|
||||
| opts.csr | <code>buffer</code> \| <code>string</code> | Certificate Signing Request |
|
||||
| opts.challengeCreateFn | <code>function</code> | Function returning Promise triggered before completing ACME challenge |
|
||||
| opts.challengeRemoveFn | <code>function</code> | Function returning Promise triggered after completing ACME challenge |
|
||||
| [opts.email] | <code>string</code> | Account email address |
|
||||
| [opts.termsOfServiceAgreed] | <code>boolean</code> | Agree to Terms of Service, default: `false` |
|
||||
| [opts.skipChallengeVerification] | <code>boolean</code> | Skip internal challenge verification before notifying ACME provider, default: `false` |
|
||||
| [opts.challengePriority] | <code>Array.<string></code> | Array defining challenge type priority, default: `['http-01', 'dns-01']` |
|
||||
| [opts.preferredChain] | <code>string</code> | Indicate which certificate chain is preferred if a CA offers multiple, by exact issuer common name, default: `null` |
|
||||
|
||||
**Example**
|
||||
Order a certificate using auto mode
|
||||
```js
|
||||
const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
|
||||
altNames: ['test.example.com'],
|
||||
});
|
||||
|
||||
const certificate = await client.auto({
|
||||
csr: certificateRequest,
|
||||
email: 'test@example.com',
|
||||
termsOfServiceAgreed: true,
|
||||
challengeCreateFn: async (authz, challenge, keyAuthorization) => {
|
||||
// Satisfy challenge here
|
||||
},
|
||||
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
|
||||
// Clean up challenge here
|
||||
},
|
||||
});
|
||||
```
|
||||
**Example**
|
||||
Order a certificate using auto mode with preferred chain
|
||||
```js
|
||||
const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
|
||||
altNames: ['test.example.com'],
|
||||
});
|
||||
|
||||
const certificate = await client.auto({
|
||||
csr: certificateRequest,
|
||||
email: 'test@example.com',
|
||||
termsOfServiceAgreed: true,
|
||||
preferredChain: 'DST Root CA X3',
|
||||
challengeCreateFn: async () => {},
|
||||
challengeRemoveFn: async () => {},
|
||||
});
|
||||
```
|
||||
<a name="Client"></a>
|
||||
|
||||
## Client : <code>object</code>
|
||||
ACME client
|
||||
|
||||
**Kind**: global namespace
|
||||
|
||||
@@ -49,7 +49,6 @@ function log(m) {
|
||||
csr,
|
||||
email: 'test@example.com',
|
||||
termsOfServiceAgreed: true,
|
||||
challengePriority: ['dns-01'],
|
||||
challengeCreateFn: (authz, challenge, keyAuthorization) => {
|
||||
/* TODO: Implement this */
|
||||
log(`[TODO] Add TXT record key=_acme-challenge.${authz.identifier.value} value=${keyAuthorization}`);
|
||||
|
||||
@@ -60,7 +60,6 @@ async function getCertOnDemand(client, servername, attempt = 0) {
|
||||
csr,
|
||||
email: 'test@example.com',
|
||||
termsOfServiceAgreed: true,
|
||||
challengePriority: ['http-01'],
|
||||
challengeCreateFn: (authz, challenge, keyAuthorization) => {
|
||||
challengeResponses[challenge.token] = keyAuthorization;
|
||||
},
|
||||
|
||||
@@ -59,7 +59,6 @@ async function getCertOnDemand(client, servername, attempt = 0) {
|
||||
csr,
|
||||
email: 'test@example.com',
|
||||
termsOfServiceAgreed: true,
|
||||
challengePriority: ['tls-alpn-01'],
|
||||
challengeCreateFn: async (authz, challenge, keyAuthorization) => {
|
||||
alpnResponses[authz.identifier.value] = await acme.crypto.createAlpnCertificate(authz, keyAuthorization);
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"description": "Simple and unopinionated ACME client",
|
||||
"private": false,
|
||||
"author": "nmorsman",
|
||||
"version": "1.41.2",
|
||||
"version": "1.44.2",
|
||||
"type": "module",
|
||||
"module": "./dist/index.js",
|
||||
"main": "./dist/index.js",
|
||||
@@ -18,7 +18,7 @@
|
||||
"types"
|
||||
],
|
||||
"dependencies": {
|
||||
"@certd/basic": "^1.41.2",
|
||||
"@certd/basic": "^1.44.2",
|
||||
"@peculiar/x509": "^1.11.0",
|
||||
"asn1js": "^3.0.5",
|
||||
"axios": "^1.9.0",
|
||||
@@ -33,33 +33,33 @@
|
||||
"@types/node": "^20.14.10",
|
||||
"@typescript-eslint/eslint-plugin": "^8.26.1",
|
||||
"@typescript-eslint/parser": "^8.26.1",
|
||||
"chai": "^4.4.1",
|
||||
"chai": "^5.1.0",
|
||||
"chai-as-promised": "^7.1.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"esmock": "^2.7.5",
|
||||
"jsdoc-to-markdown": "^8.0.1",
|
||||
"mocha": "^10.6.0",
|
||||
"nock": "^13.5.4",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier": "3.3.3",
|
||||
"rimraf": "^5.0.5",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsd": "^0.31.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"scripts": {
|
||||
"before-build": "node -e \"const fs=require('fs');fs.rmSync('dist',{recursive:true,force:true});fs.rmSync('tsconfig.tsbuildinfo',{force:true});\"",
|
||||
"before-build": "rimraf dist && rimraf dist-test && rimraf tsconfig.tsbuildinfo && rimraf .rollup.cache",
|
||||
"build": "npm run before-build && tsc -p tsconfig.build.json --skipLibCheck",
|
||||
"build-docs": "jsdoc2md dist/client.js > docs/client.md && jsdoc2md dist/crypto/index.js > docs/crypto.md && jsdoc2md dist/crypto/forge.js > docs/forge.md",
|
||||
"lint": "eslint \"src/**/*.ts\" \"types/**/*.ts\"",
|
||||
"lint": "eslint --fix \"src/**/*.ts\" \"types/**/*.ts\"",
|
||||
"lint-types": "tsd --files \"types/index.test-d.ts\"",
|
||||
"prepublishOnly": "npm run build && npm run build-docs",
|
||||
"prepublishOnly": "npm run build",
|
||||
"test": "mocha -t 60000 \"test/setup.js\" \"test/**/*.spec.js\"",
|
||||
"before-test:unit": "node -e \"const fs=require('fs');fs.rmSync('dist-test',{recursive:true,force:true});fs.rmSync('tsconfig.test.tsbuildinfo',{force:true});\"",
|
||||
"test:unit": "cross-env NODE_ENV=unittest npm run before-test:unit && cross-env NODE_ENV=unittest tsc -p tsconfig.test.json --skipLibCheck && cross-env NODE_ENV=unittest mocha -t 60000 \"dist-test/**/*.test.js\"",
|
||||
"test:unit": "cross-env NODE_ENV=unittest mocha --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
|
||||
"pub": "npm publish",
|
||||
"compile": "tsc --skipLibCheck --watch"
|
||||
"compile": " tsc --skipLibCheck --watch",
|
||||
"format": "prettier --write src"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -76,5 +76,5 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/publishlab/node-acme-client/issues"
|
||||
},
|
||||
"gitHead": "cdea411136fdf56352699a6e278a403e0f53a94f"
|
||||
"gitHead": "fe738fa7d4622016ed7d506c501df85393b6c857"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @ts-nocheck
|
||||
// @ts-nocheck
|
||||
/**
|
||||
* ACME API client
|
||||
*/
|
||||
@@ -20,6 +20,7 @@ class AcmeApi {
|
||||
getLocationFromHeader(resp) {
|
||||
let locationUrl = resp.headers.location;
|
||||
const mapping = this.http.urlMapping;
|
||||
// 服务器返回的是反代地址,这里还原为 ACME 原始资源 URL,供后续 JWS url 使用。
|
||||
if (mapping.mappings) {
|
||||
// eslint-disable-next-line guard-for-in,no-restricted-syntax
|
||||
for (const key in mapping.mappings) {
|
||||
@@ -237,11 +238,12 @@ class AcmeApi {
|
||||
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.6
|
||||
*
|
||||
* @param {object} data Request payload
|
||||
* @param {object} [opts] Request options, e.g. `{ includeJwsKid: false }` to sign with certificate private key (jwk) instead of account key
|
||||
* @returns {Promise<object>} HTTP response
|
||||
*/
|
||||
|
||||
revokeCert(data) {
|
||||
return this.apiResourceRequest('revokeCert', data, [200]);
|
||||
revokeCert(data, opts = {}) {
|
||||
return this.apiResourceRequest('revokeCert', data, [200], opts);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import assert from "node:assert/strict";
|
||||
import auto from "./auto.js";
|
||||
import { createCsr, createPrivateRsaKey } from "./crypto/index.js";
|
||||
|
||||
declare const describe: any;
|
||||
declare const it: any;
|
||||
|
||||
describe("auto challenge status polling", () => {
|
||||
it("polls the authorization URL after completing a challenge", async () => {
|
||||
const [, csr] = await createCsr({ commonName: "example.com" }, await createPrivateRsaKey());
|
||||
const challenge = {
|
||||
type: "dns-01",
|
||||
url: "https://ca.example/chall/1",
|
||||
token: "token",
|
||||
};
|
||||
const authz = {
|
||||
status: "pending",
|
||||
identifier: { type: "dns", value: "example.com" },
|
||||
url: "https://ca.example/authz/1",
|
||||
challenges: [challenge],
|
||||
};
|
||||
const order = {
|
||||
status: "pending",
|
||||
url: "https://ca.example/order/1",
|
||||
finalize: "https://ca.example/order/1/finalize",
|
||||
authorizations: [authz.url],
|
||||
};
|
||||
const polledUrls: string[] = [];
|
||||
const originalSetTimeout = globalThis.setTimeout;
|
||||
|
||||
(globalThis as any).setTimeout = (fn: (...args: any[]) => void) => originalSetTimeout(fn, 0);
|
||||
|
||||
try {
|
||||
const certificate = await auto(
|
||||
{
|
||||
logger: { info: () => {} },
|
||||
sslProvider: "litessl",
|
||||
getAccountUrl: () => "https://ca.example/acct/1",
|
||||
createOrder: async () => order,
|
||||
getAuthorizations: async () => [authz],
|
||||
getChallengeKeyAuthorization: async () => "key-authorization",
|
||||
verifyChallenge: async () => {},
|
||||
completeChallenge: async () => ({ ...challenge, status: "processing" }),
|
||||
waitForValidStatus: async (item: { url: string }) => {
|
||||
polledUrls.push(item.url);
|
||||
return { ...item, status: "valid" };
|
||||
},
|
||||
finalizeOrder: async () => ({ ...order, status: "valid", certificate: "https://ca.example/cert/1" }),
|
||||
getCertificate: async () => "CERTIFICATE",
|
||||
} as any,
|
||||
{
|
||||
csr,
|
||||
termsOfServiceAgreed: true,
|
||||
waitDnsDiffuseTime: 0,
|
||||
challengeCreateFn: async (_authz: any, keyAuthorizationGetter: (challenge: any) => Promise<string>) => ({
|
||||
challenge,
|
||||
keyAuthorization: await keyAuthorizationGetter(challenge),
|
||||
}),
|
||||
challengeRemoveFn: async () => {},
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(certificate, "CERTIFICATE");
|
||||
assert.deepEqual(polledUrls, [authz.url]);
|
||||
} finally {
|
||||
(globalThis as any).setTimeout = originalSetTimeout;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -16,7 +16,6 @@ const defaultOpts = {
|
||||
preferredChain: null,
|
||||
termsOfServiceAgreed: false,
|
||||
skipChallengeVerification: false,
|
||||
challengePriority: ["http-01", "dns-01"],
|
||||
challengeCreateFn: async () => {
|
||||
throw new Error("Missing challengeCreateFn()");
|
||||
},
|
||||
@@ -172,7 +171,7 @@ export default async (client, userOpts) => {
|
||||
}
|
||||
challengeCompleted = true;
|
||||
log(`[auto] [${d}] 等待返回valid状态`);
|
||||
await client.waitForValidStatus(challenge,d);
|
||||
await client.waitForValidStatus(authz,d);
|
||||
});
|
||||
|
||||
|
||||
@@ -277,7 +276,7 @@ export default async (client, userOpts) => {
|
||||
}
|
||||
|
||||
log("开始向提供商请求检查验证");
|
||||
await runPromisePa(completeChallengeTasks, 1000);
|
||||
await runPromisePa(completeChallengeTasks, 4000);
|
||||
} catch (e) {
|
||||
log(`证书申请失败${e.message}`);
|
||||
throw e;
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* Axios instance
|
||||
*/
|
||||
import axios from 'axios';
|
||||
import { parseRetryAfterHeader } from './util.js';
|
||||
const { AxiosError } = axios;
|
||||
import {getGlobalAgents, HttpError} from '@certd/basic'
|
||||
import { log } from './logger.js';
|
||||
@@ -113,12 +112,9 @@ instance.interceptors.response.use(null, async (error) => {
|
||||
log(`Caught ${code}, retry attempt ${config.retryAttempt}/${retryMaxAttempts} to URL ${config.url}`);
|
||||
|
||||
const retryAfter = (retryDefaultDelay * config.retryAttempt);
|
||||
/* Attempt to parse Retry-After header, fallback to default delay */
|
||||
const headerRetryAfter = response ? parseRetryAfterHeader(response.headers['retry-after']) : 0;
|
||||
|
||||
if (headerRetryAfter > 0) {
|
||||
const waitMinutes = (headerRetryAfter / 60).toFixed(1);
|
||||
log(`Found retry-after response header with value: ${response.headers['retry-after']}, waiting ${waitMinutes} minutes`);
|
||||
if (response?.status === 429) {
|
||||
// CA 限流按产品策略立即失败,不能依据 Retry-After 自动重试,避免继续消耗配额。
|
||||
log(`CA rate limit encountered (HTTP 429), failing immediately; Retry-After=${response.headers['retry-after']}`);
|
||||
log(JSON.stringify(response.data));
|
||||
return Promise.reject(new HttpError(error));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @ts-nocheck
|
||||
// @ts-nocheck
|
||||
/**
|
||||
* ACME client
|
||||
*
|
||||
@@ -263,7 +263,14 @@ class AcmeClient {
|
||||
const accountUrl = this.api.getAccountUrl();
|
||||
|
||||
/* Create new HTTP and API clients using new key */
|
||||
const newHttpClient = new HttpClient(this.opts.directoryUrl, newAccountKey, this.opts.externalAccountBinding);
|
||||
const newHttpClient = new HttpClient(
|
||||
this.opts.directoryUrl,
|
||||
newAccountKey,
|
||||
this.opts.externalAccountBinding,
|
||||
this.opts.urlMapping,
|
||||
this.logger,
|
||||
true,
|
||||
);
|
||||
const newApiClient = new AcmeApi(newHttpClient, accountUrl);
|
||||
|
||||
/* Get old JWK */
|
||||
@@ -280,6 +287,8 @@ class AcmeClient {
|
||||
/* Replace existing HTTP and API client */
|
||||
this.http = newHttpClient;
|
||||
this.api = newApiClient;
|
||||
// 更新本地配置,避免后续重建客户端时又使用旧账号密钥。
|
||||
this.opts.accountKey = newAccountKey;
|
||||
|
||||
return resp.data;
|
||||
}
|
||||
@@ -681,11 +690,21 @@ class AcmeClient {
|
||||
* reason: 4,
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @example Revoke certificate with certificate private key (RFC 8555 §7.6)
|
||||
* 当无法提供ACME账号私钥时,可以使用证书私钥签名吊销请求:
|
||||
* ```js
|
||||
* const client = new acme.Client({
|
||||
* directoryUrl: acme.directory.letsencrypt.production,
|
||||
* accountKey: certificateKey, // 证书私钥,而非账号私钥
|
||||
* });
|
||||
* const result = await client.revokeCertificate(certificate, {}, { includeJwsKid: false });
|
||||
* ```
|
||||
*/
|
||||
|
||||
async revokeCertificate(cert, data = {}) {
|
||||
async revokeCertificate(cert, data = {}, opts = {}) {
|
||||
data.certificate = getPemBodyAsB64u(cert);
|
||||
const resp = await this.api.revokeCert(data);
|
||||
const resp = await this.api.revokeCert(data, opts);
|
||||
return resp.data;
|
||||
}
|
||||
|
||||
@@ -699,7 +718,6 @@ class AcmeClient {
|
||||
* @param {string} [opts.email] Account email address
|
||||
* @param {boolean} [opts.termsOfServiceAgreed] Agree to Terms of Service, default: `false`
|
||||
* @param {boolean} [opts.skipChallengeVerification] Skip internal challenge verification before notifying ACME provider, default: `false`
|
||||
* @param {string[]} [opts.challengePriority] Array defining challenge type priority, default: `['http-01', 'dns-01']`
|
||||
* @param {string} [opts.preferredChain] Indicate which certificate chain is preferred if a CA offers multiple, by exact issuer common name, default: `null`
|
||||
* @returns {Promise<string>} Certificate
|
||||
*
|
||||
|
||||
@@ -527,6 +527,8 @@ export const createAlpnCertificate = async (authz, keyAuthorization, keyPem = nu
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
// TLS-ALPN-01 只需要一张短期挑战证书;有效期不能为零,否则部分 TLS 实现会直接拒绝。
|
||||
const notAfter = new Date(now.getTime() + 24 * 60 * 60 * 1000);
|
||||
const commonName = authz.identifier.value;
|
||||
|
||||
/* Pseudo-random serial - max 20 bytes, 11 for epoch (year 5138), 9 random */
|
||||
@@ -537,14 +539,8 @@ export const createAlpnCertificate = async (authz, keyAuthorization, keyPem = nu
|
||||
const [keys, signingAlgorithm] = await getWebCryptoKeyPair(keyPem);
|
||||
|
||||
const extensions = [
|
||||
/* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.3 */
|
||||
new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true), // eslint-disable-line no-bitwise
|
||||
|
||||
/* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.9 */
|
||||
new x509.BasicConstraintsExtension(true, 2, true),
|
||||
|
||||
/* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.2 */
|
||||
await x509.SubjectKeyIdentifierExtension.create(keys.publicKey),
|
||||
/* TLS 服务端挑战证书只用于签名,不是 CA 证书。 */
|
||||
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature, true),
|
||||
|
||||
/* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6 */
|
||||
createSubjectAltNameExtension([commonName]),
|
||||
@@ -562,7 +558,7 @@ export const createAlpnCertificate = async (authz, keyAuthorization, keyPem = nu
|
||||
extensions,
|
||||
serialNumber,
|
||||
notBefore: now,
|
||||
notAfter: now,
|
||||
notAfter,
|
||||
name: createCsrSubject({
|
||||
CN: commonName,
|
||||
}),
|
||||
|
||||
@@ -72,6 +72,7 @@ class HttpClient {
|
||||
*/
|
||||
|
||||
async request(url, method, opts = {}) {
|
||||
// 仅替换实际传输地址;signedRequest 已在映射前用 ACME 原始 URL 生成 JWS,协议要求两者保持一致。
|
||||
if (this.urlMapping && this.urlMapping.enabled && this.urlMapping.mappings) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const key in this.urlMapping.mappings) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { directory, getAllSslProviderDomains, getDirectoryUrl } from "./index.js";
|
||||
import { directory, getDirectoryUrl } from "./index.js";
|
||||
|
||||
declare const describe: any;
|
||||
declare const it: any;
|
||||
@@ -9,11 +9,4 @@ describe("directory helpers", () => {
|
||||
assert.equal(getDirectoryUrl({ sslProvider: "sslcom", pkType: "ec" }), directory.sslcom.ec);
|
||||
assert.equal(getDirectoryUrl({ sslProvider: "letsencrypt", pkType: "rsa" }), directory.letsencrypt.production);
|
||||
});
|
||||
|
||||
it("includes configured provider domains", () => {
|
||||
const domains = getAllSslProviderDomains();
|
||||
|
||||
assert.ok(domains.includes("acme.litessl.com"));
|
||||
assert.ok(domains.includes("acme.ssl.com"));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @ts-nocheck
|
||||
// @ts-nocheck
|
||||
/**
|
||||
* acme-client
|
||||
*/
|
||||
@@ -58,32 +58,6 @@ export function getDirectoryUrl(opts) {
|
||||
return list.production
|
||||
}
|
||||
|
||||
|
||||
export function getAllSslProviderDomains() {
|
||||
const list = Object.values(directory).map((item) => {
|
||||
let url = item.production.replace('https://', '')
|
||||
url = url.substring(0, url.indexOf('/'))
|
||||
return url
|
||||
})
|
||||
return list
|
||||
}
|
||||
|
||||
let sslProviderReverseProxies = {}
|
||||
|
||||
function initSslProviderReverseProxies() {
|
||||
for (const sslProvider of getAllSslProviderDomains()) {
|
||||
sslProviderReverseProxies[sslProvider] = ""
|
||||
}
|
||||
}
|
||||
initSslProviderReverseProxies()
|
||||
|
||||
export function getSslProviderReverseProxies() {
|
||||
return sslProviderReverseProxies
|
||||
}
|
||||
export function setSslProviderReverseProxies(reverseProxies) {
|
||||
Object.assign(sslProviderReverseProxies, reverseProxies)
|
||||
}
|
||||
|
||||
/**
|
||||
* Crypto
|
||||
*/
|
||||
|
||||
@@ -55,7 +55,6 @@ export interface ClientAutoOptions {
|
||||
email?: string;
|
||||
termsOfServiceAgreed?: boolean;
|
||||
skipChallengeVerification?: boolean;
|
||||
challengePriority?: string[];
|
||||
preferredChain?: string;
|
||||
signal?: AbortSignal;
|
||||
profile?: string;
|
||||
|
||||
@@ -13,8 +13,10 @@ import { utils } from '@certd/basic'
|
||||
|
||||
const dns = dnsSdk.promises
|
||||
|
||||
// 管理员可按当前部署环境全局开关权威 DNS 查询(例如内网环境可能无法访问权威服务器)。
|
||||
let walkFromAuthoritative = true
|
||||
export function setWalkFromAuthoritative(value = true) {
|
||||
// 这是进程级配置,修改后影响后续所有 ACME DNS-01 本地校验。
|
||||
walkFromAuthoritative = value
|
||||
}
|
||||
|
||||
|
||||
@@ -81,26 +81,17 @@ describe('http', () => {
|
||||
* Retry on HTTP errors
|
||||
*/
|
||||
|
||||
it('should retry on 429 rate limit', async () => {
|
||||
it('should fail immediately on 429 rate limit', async () => {
|
||||
let rateLimitCount = 0;
|
||||
|
||||
nock(endpoint).persist().get('/').reply(() => {
|
||||
rateLimitCount += 1;
|
||||
|
||||
if (rateLimitCount < 3) {
|
||||
return [429, 'Rate Limit Exceeded', { 'Retry-After': 1 }];
|
||||
}
|
||||
|
||||
return [200, 'ok'];
|
||||
return [429, 'Rate Limit Exceeded', { 'Retry-After': 1 }];
|
||||
});
|
||||
|
||||
assert.strictEqual(rateLimitCount, 0);
|
||||
const resp = await testClient.request(endpoint, 'get');
|
||||
|
||||
assert.isObject(resp);
|
||||
assert.strictEqual(resp.status, 200);
|
||||
assert.strictEqual(resp.data, 'ok');
|
||||
assert.strictEqual(rateLimitCount, 3);
|
||||
await assert.isRejected(testClient.request(endpoint, 'get'));
|
||||
assert.strictEqual(rateLimitCount, 1);
|
||||
});
|
||||
|
||||
it('should retry on 5xx server error', async () => {
|
||||
|
||||
@@ -251,6 +251,13 @@ describe('crypto', () => {
|
||||
testAlpnCertificate = cert;
|
||||
});
|
||||
|
||||
it(`${n}/should generate alpn certificate with a non-zero validity period`, () => {
|
||||
const info = crypto.readCertificateInfo(testAlpnCertificate);
|
||||
|
||||
assert.isBelow(info.notBefore.getTime(), info.notAfter.getTime());
|
||||
assert.isAtMost(info.notAfter.getTime() - info.notBefore.getTime(), 24 * 60 * 60 * 1000);
|
||||
});
|
||||
|
||||
it(`${n}/should generate alpn certificate with key as string`, async () => {
|
||||
const k = await createFn();
|
||||
const authz = { identifier: { value: 'test.example.com' } };
|
||||
|
||||
@@ -235,7 +235,6 @@ describe('client.auto', () => {
|
||||
termsOfServiceAgreed: true,
|
||||
challengeCreateFn: cts.assertHttpChallengeCreateFn,
|
||||
challengeRemoveFn: cts.challengeRemoveFn,
|
||||
challengePriority: ['http-01'],
|
||||
});
|
||||
|
||||
assert.isString(cert);
|
||||
@@ -251,7 +250,6 @@ describe('client.auto', () => {
|
||||
termsOfServiceAgreed: true,
|
||||
challengeCreateFn: cts.assertHttpsChallengeCreateFn,
|
||||
challengeRemoveFn: cts.challengeRemoveFn,
|
||||
challengePriority: ['http-01'],
|
||||
});
|
||||
|
||||
assert.isString(cert);
|
||||
@@ -267,7 +265,6 @@ describe('client.auto', () => {
|
||||
termsOfServiceAgreed: true,
|
||||
challengeCreateFn: cts.assertDnsChallengeCreateFn,
|
||||
challengeRemoveFn: cts.challengeRemoveFn,
|
||||
challengePriority: ['dns-01'],
|
||||
});
|
||||
|
||||
assert.isString(cert);
|
||||
@@ -283,7 +280,6 @@ describe('client.auto', () => {
|
||||
termsOfServiceAgreed: true,
|
||||
challengeCreateFn: cts.assertTlsAlpnChallengeCreateFn,
|
||||
challengeRemoveFn: cts.challengeRemoveFn,
|
||||
challengePriority: ['tls-alpn-01'],
|
||||
});
|
||||
|
||||
assert.isString(cert);
|
||||
|
||||
+1
-5
@@ -62,7 +62,6 @@ export interface ClientAutoOptions {
|
||||
email?: string;
|
||||
termsOfServiceAgreed?: boolean;
|
||||
skipChallengeVerification?: boolean;
|
||||
challengePriority?: string[];
|
||||
preferredChain?: string;
|
||||
signal?: AbortSignal;
|
||||
profile?:string;
|
||||
@@ -86,7 +85,7 @@ export class Client {
|
||||
completeChallenge(challenge: rfc8555.Challenge): Promise<rfc8555.Challenge>;
|
||||
waitForValidStatus<T = Order | Authorization | rfc8555.Challenge>(item: T): Promise<T>;
|
||||
getCertificate(order: Order, preferredChain?: string): Promise<string>;
|
||||
revokeCertificate(cert: CertificateBuffer | CertificateString, data?: rfc8555.CertificateRevocationRequest): Promise<void>;
|
||||
revokeCertificate(cert: CertificateBuffer | CertificateString, data?: rfc8555.CertificateRevocationRequest, opts?: { includeJwsKid?: boolean }): Promise<void>;
|
||||
auto(opts: ClientAutoOptions): Promise<string>;
|
||||
}
|
||||
|
||||
@@ -126,9 +125,6 @@ export const directory: {
|
||||
};
|
||||
|
||||
export function getDirectoryUrl(opts:{sslProvider:string, pkType: string}): string;
|
||||
export function getAllSslProviderDomains(): string[];
|
||||
export function getSslProviderReverseProxies(): Record<string, string>;
|
||||
export function setSslProviderReverseProxies(reverseProxies: Record<string, string>): void;
|
||||
|
||||
/**
|
||||
* Crypto
|
||||
|
||||
@@ -65,7 +65,6 @@ import * as acme from '..';
|
||||
email: 'test@example.com',
|
||||
termsOfServiceAgreed: false,
|
||||
skipChallengeVerification: false,
|
||||
challengePriority: ['http-01', 'dns-01'],
|
||||
preferredChain: 'DST Root CA X3',
|
||||
challengeCreateFn: async (authz, keyAuthorization) => ({
|
||||
challenge: authz.challenges[0],
|
||||
|
||||
@@ -18,5 +18,9 @@
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off"
|
||||
}
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"node_modules/**",
|
||||
"dist/**"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,6 +3,62 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
|
||||
|
||||
**Note:** Version bump only for package @certd/basic
|
||||
|
||||
## [1.44.1](https://github.com/certd/certd/compare/v1.44.0...v1.44.1) (2026-09-02)
|
||||
|
||||
**Note:** Version bump only for package @certd/basic
|
||||
|
||||
# [1.44.0](https://github.com/certd/certd/compare/v1.43.0...v1.44.0) (2026-08-28)
|
||||
|
||||
### Features
|
||||
|
||||
* 新增证书吊销后置任务与证书状态管理 ([1cc9641](https://github.com/certd/certd/commit/1cc9641f5669710d1e5809995ad395c843855a12))
|
||||
|
||||
# [1.43.0](https://github.com/certd/certd/compare/v1.42.6...v1.43.0) (2026-08-14)
|
||||
|
||||
**Note:** Version bump only for package @certd/basic
|
||||
|
||||
## [1.42.6](https://github.com/certd/certd/compare/v1.42.5...v1.42.6) (2026-07-18)
|
||||
|
||||
**Note:** Version bump only for package @certd/basic
|
||||
|
||||
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
|
||||
|
||||
**Note:** Version bump only for package @certd/basic
|
||||
|
||||
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
|
||||
|
||||
**Note:** Version bump only for package @certd/basic
|
||||
|
||||
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
|
||||
|
||||
**Note:** Version bump only for package @certd/basic
|
||||
|
||||
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
|
||||
|
||||
**Note:** Version bump only for package @certd/basic
|
||||
|
||||
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
|
||||
|
||||
**Note:** Version bump only for package @certd/basic
|
||||
|
||||
# [1.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
|
||||
|
||||
**Note:** Version bump only for package @certd/basic
|
||||
|
||||
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复设置里面不显示tab页签,导致某些页面需要点击查询按钮才有数据出来的bug ([c1b5a35](https://github.com/certd/certd/commit/c1b5a35f90a7d4b41397717b5c27905bc68e1bfb))
|
||||
|
||||
## [1.41.3](https://github.com/certd/certd/compare/v1.41.2...v1.41.3) (2026-06-11)
|
||||
|
||||
**Note:** Version bump only for package @certd/basic
|
||||
|
||||
## [1.41.2](https://github.com/certd/certd/compare/v1.41.1...v1.41.2) (2026-06-10)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
@@ -1 +1 @@
|
||||
23:34
|
||||
21:57
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@certd/basic",
|
||||
"private": false,
|
||||
"version": "1.41.2",
|
||||
"version": "1.44.2",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
@@ -15,7 +15,9 @@
|
||||
"test": "mocha --loader=ts-node/esm",
|
||||
"test:unit": "cross-env NODE_ENV=unittest mocha --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
|
||||
"pub": "npm publish",
|
||||
"compile": "tsc --skipLibCheck --watch"
|
||||
"compile": " tsc --skipLibCheck --watch",
|
||||
"format": "prettier --write src",
|
||||
"lint": "eslint --fix --ext .ts src"
|
||||
},
|
||||
"dependencies": {
|
||||
"async-lock": "^1.4.1",
|
||||
@@ -26,31 +28,31 @@
|
||||
"iconv-lite": "^0.6.3",
|
||||
"lodash-es": "^4.17.21",
|
||||
"log4js": "^6.9.1",
|
||||
"lru-cache": "^10.0.0",
|
||||
"lru-cache": "^11.0.1",
|
||||
"mitt": "^3.0.1",
|
||||
"nanoid": "^5.0.7",
|
||||
"node-forge": "^1.3.1",
|
||||
"nodemailer": "^6.9.3"
|
||||
"nodemailer": "^6.9.16"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.3.10",
|
||||
"@types/chai": "^4.3.12",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mocha": "^10.0.1",
|
||||
"@types/mocha": "^10.0.6",
|
||||
"@types/node-forge": "^1.3.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.26.1",
|
||||
"@typescript-eslint/parser": "^8.26.1",
|
||||
"chai": "4.3.10",
|
||||
"chai": "^5.1.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.41.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"esmock": "^2.7.5",
|
||||
"mocha": "^10.2.0",
|
||||
"prettier": "^2.8.8",
|
||||
"mocha": "^10.6.0",
|
||||
"prettier": "3.3.3",
|
||||
"rimraf": "^5.0.5",
|
||||
"ts-node": "^10.9.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "cdea411136fdf56352699a6e278a403e0f53a94f"
|
||||
"gitHead": "fe738fa7d4622016ed7d506c501df85393b6c857"
|
||||
}
|
||||
|
||||
@@ -11,9 +11,12 @@ export class LocalCache<V = any> {
|
||||
cache: Map<string, { value: V; expiresAt: number }>;
|
||||
constructor(opts: { clearInterval?: number } = {}) {
|
||||
this.cache = new Map();
|
||||
const intervalId = setInterval(() => {
|
||||
this.clearExpires();
|
||||
}, opts.clearInterval ?? 5 * 60 * 1000);
|
||||
const intervalId = setInterval(
|
||||
() => {
|
||||
this.clearExpires();
|
||||
},
|
||||
opts.clearInterval ?? 5 * 60 * 1000
|
||||
);
|
||||
intervalId.unref?.();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export function isDev() {
|
||||
const nodeEnv = process.env.NODE_ENV || 'dev';
|
||||
return nodeEnv === 'development' || nodeEnv.includes('local') || nodeEnv.startsWith('dev');
|
||||
const nodeEnv = process.env.NODE_ENV || "dev";
|
||||
return nodeEnv === "development" || nodeEnv.includes("local") || nodeEnv.startsWith("dev");
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import fs from 'fs';
|
||||
import fs from "fs";
|
||||
function getFileRootDir(rootDir?: string) {
|
||||
if (rootDir == null) {
|
||||
const userHome = process.env.HOME || process.env.USERPROFILE;
|
||||
rootDir = userHome + '/.certd/storage/';
|
||||
rootDir = userHome + "/.certd/storage/";
|
||||
}
|
||||
|
||||
if (!fs.existsSync(rootDir)) {
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
import mitt from 'mitt';
|
||||
import mitt from "mitt";
|
||||
export const mitter = mitt();
|
||||
|
||||
@@ -18,5 +18,9 @@
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off"
|
||||
}
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"node_modules/**",
|
||||
"dist/**"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,6 +3,84 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.44.2](https://github.com/certd/certd/compare/v1.44.1...v1.44.2) (2026-09-03)
|
||||
|
||||
**Note:** Version bump only for package @certd/pipeline
|
||||
|
||||
## [1.44.1](https://github.com/certd/certd/compare/v1.44.0...v1.44.1) (2026-09-02)
|
||||
|
||||
**Note:** Version bump only for package @certd/pipeline
|
||||
|
||||
# [1.44.0](https://github.com/certd/certd/compare/v1.43.0...v1.44.0) (2026-08-28)
|
||||
|
||||
### Features
|
||||
|
||||
* 新增证书吊销后置任务与证书状态管理 ([1cc9641](https://github.com/certd/certd/commit/1cc9641f5669710d1e5809995ad395c843855a12))
|
||||
* 支持在线插件下载安装审核等 ([255960d](https://github.com/certd/certd/commit/255960d434eebb3d567a08f737f1642ab417b5d0))
|
||||
|
||||
# [1.43.0](https://github.com/certd/certd/compare/v1.42.6...v1.43.0) (2026-08-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **pipeline:** 修复过期/损坏的安装锁导致的依赖安装失败 ([1017a38](https://github.com/certd/certd/commit/1017a38eb48d6778745d4905d9ec4f6067a261d8))
|
||||
|
||||
### Features
|
||||
|
||||
* **pipeline:** 新增流水线批量编辑、阶段宽度调整功能 ([e5a1844](https://github.com/certd/certd/commit/e5a18448522be88bc504a45a46ad599f3c39ca30))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 统一通知类型常量管理,新增开放接口通知能力 ([edb249c](https://github.com/certd/certd/commit/edb249c2ad3c173dbfd04d8d156f895a6706ffd1))
|
||||
* 优化第三方依赖安装失败日志输出 ([18b2d3a](https://github.com/certd/certd/commit/18b2d3ac2078d0c6636af897f8f66e4eb5ff832c))
|
||||
|
||||
## [1.42.6](https://github.com/certd/certd/compare/v1.42.5...v1.42.6) (2026-07-18)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 优化动态加载依赖镜像地址,多次重试 ([5f53b81](https://github.com/certd/certd/commit/5f53b81c75dd242b4260ac08cae14c6d1a08a883))
|
||||
|
||||
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 修复上传到cos报runtimeDepsService未初始化的问题 ([167b303](https://github.com/certd/certd/commit/167b303faeca02cc11cf97e4be2a3df914852167))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 优化vke keubconfig获取方式,改成先查询,如果没有再创建临时config ([604fa5b](https://github.com/certd/certd/commit/604fa5be634d099d797bfee5c2b0f26ce0ac8461))
|
||||
|
||||
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **pipeline:** 重构运行时依赖加载逻辑,修复火山引擎DNS解析报runtimeDepsService未初始化的bug ([ec69b8f](https://github.com/certd/certd/commit/ec69b8f11bfd4b20991aef74a72a47182ca79a9d))
|
||||
|
||||
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
|
||||
|
||||
**Note:** Version bump only for package @certd/pipeline
|
||||
|
||||
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
|
||||
|
||||
**Note:** Version bump only for package @certd/pipeline
|
||||
|
||||
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
|
||||
|
||||
**Note:** Version bump only for package @certd/pipeline
|
||||
|
||||
# [1.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
|
||||
|
||||
### Features
|
||||
|
||||
* 通过插件配置懒加载依赖,动态加载第三方依赖包,精简安装镜像大小 ([01568ca](https://github.com/certd/certd/commit/01568ca1489069046b5a89ebdd4ced2f7f6ddf93))
|
||||
|
||||
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
|
||||
|
||||
**Note:** Version bump only for package @certd/pipeline
|
||||
|
||||
## [1.41.3](https://github.com/certd/certd/compare/v1.41.2...v1.41.3) (2026-06-11)
|
||||
|
||||
**Note:** Version bump only for package @certd/pipeline
|
||||
|
||||
## [1.41.2](https://github.com/certd/certd/compare/v1.41.1...v1.41.2) (2026-06-10)
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@certd/pipeline",
|
||||
"private": false,
|
||||
"version": "1.41.2",
|
||||
"version": "1.44.2",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
@@ -14,16 +14,18 @@
|
||||
"build3": "rollup -c",
|
||||
"preview": "vite preview",
|
||||
"test": "mocha --loader=ts-node/esm",
|
||||
"test:unit": "cross-env NODE_ENV=unittest mocha --no-config --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
|
||||
"test:unit": "cross-env NODE_ENV=unittest mocha --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
|
||||
"pub": "npm publish",
|
||||
"compile": "tsc --skipLibCheck --watch"
|
||||
"compile": "tsc --skipLibCheck --watch",
|
||||
"format": "prettier --write src",
|
||||
"lint": "eslint --fix --ext .ts src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@certd/basic": "^1.41.2",
|
||||
"@certd/plus-core": "^1.41.2",
|
||||
"@certd/basic": "^1.44.2",
|
||||
"@certd/plus-core": "^1.44.2",
|
||||
"dayjs": "^1.11.7",
|
||||
"lodash-es": "^4.17.21",
|
||||
"reflect-metadata": "^0.1.13"
|
||||
"reflect-metadata": "^0.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^23.0.4",
|
||||
@@ -31,23 +33,23 @@
|
||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||
"@rollup/plugin-terser": "^0.4.3",
|
||||
"@rollup/plugin-typescript": "^11.0.0",
|
||||
"@types/chai": "^4.3.10",
|
||||
"@types/chai": "^4.3.12",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mocha": "^10.0.1",
|
||||
"@types/mocha": "^10.0.6",
|
||||
"@typescript-eslint/eslint-plugin": "^8.26.1",
|
||||
"@typescript-eslint/parser": "^8.26.1",
|
||||
"chai": "4.3.10",
|
||||
"chai": "^5.1.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.41.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"esmock": "^2.7.5",
|
||||
"mocha": "^10.2.0",
|
||||
"prettier": "^2.8.8",
|
||||
"mocha": "^10.6.0",
|
||||
"prettier": "3.3.3",
|
||||
"rimraf": "^5.0.5",
|
||||
"ts-node": "^10.9.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "cdea411136fdf56352699a6e278a403e0f53a94f"
|
||||
"gitHead": "fe738fa7d4622016ed7d506c501df85393b6c857"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { FormItemProps } from "../dt/index.js";
|
||||
import { HttpClient, ILogger, utils } from "@certd/basic";
|
||||
import * as _ from "lodash-es";
|
||||
import { PluginRequestHandleReq } from "../plugin/index.js";
|
||||
import { IServiceGetter, getRuntimeDepsService } from "../service/index.js";
|
||||
|
||||
// export type AccessRequestHandleReqInput<T = any> = {
|
||||
// id?: number;
|
||||
@@ -20,6 +21,8 @@ export type AccessInputDefine = FormItemProps & {
|
||||
export type AccessDefine = Registrable & {
|
||||
icon?: string;
|
||||
subtype?: string;
|
||||
dependPlugins?: Record<string, string>;
|
||||
dependPackages?: Record<string, string>;
|
||||
input?: {
|
||||
[key: string]: AccessInputDefine;
|
||||
};
|
||||
@@ -39,12 +42,18 @@ export type AccessContext = {
|
||||
logger: ILogger;
|
||||
utils: typeof utils;
|
||||
accessService: IAccessService;
|
||||
serviceGetter?: IServiceGetter;
|
||||
define?: AccessDefine;
|
||||
};
|
||||
|
||||
export abstract class BaseAccess implements IAccess {
|
||||
ctx!: AccessContext;
|
||||
|
||||
setCtx(ctx: AccessContext) {
|
||||
async importRuntime(specifier: string) {
|
||||
return await getRuntimeDepsService().importRuntime(specifier, this.ctx.logger);
|
||||
}
|
||||
|
||||
async setCtx(ctx: AccessContext) {
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ export function AccessInput(input?: AccessInputDefine): PropertyDecorator {
|
||||
};
|
||||
}
|
||||
|
||||
export async function newAccess(type: string, input: any, accessService: IAccessService, ctx?: AccessContext) {
|
||||
export async function newAccess(type: string, input: any, accessService: IAccessService, ctx: AccessContext) {
|
||||
const register = accessRegistry.get(type);
|
||||
if (register == null) {
|
||||
throw new Error(`access ${type} not found`);
|
||||
@@ -67,7 +67,9 @@ export async function newAccess(type: string, input: any, accessService: IAccess
|
||||
accessService,
|
||||
};
|
||||
}
|
||||
access.setCtx(ctx);
|
||||
ctx.define = ctx.define || register.define;
|
||||
access.runtimeDepsService = (accessService as any).runtimeDepsService;
|
||||
await access.setCtx(ctx);
|
||||
access._type = type;
|
||||
return access;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,406 @@
|
||||
import assert from "assert";
|
||||
import { Executor } from "./executor.js";
|
||||
import { RunHistory, RunnableCollection } from "./run-history.js";
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, TaskInput, TaskOutput } from "../plugin/index.js";
|
||||
import { AfterTask, Pipeline, ResultType } from "../dt/index.js";
|
||||
|
||||
/**
|
||||
* 测试用后置任务插件:记录执行时的输入与流水线结果
|
||||
*/
|
||||
@IsTaskPlugin({
|
||||
name: "TestAfterTaskPlugin",
|
||||
title: "测试后置任务插件",
|
||||
supportAfterTask: true,
|
||||
})
|
||||
class TestAfterTaskPlugin extends AbstractTaskPlugin {
|
||||
static calls: { message: string; pipelineResult: ResultType | undefined }[] = [];
|
||||
@TaskInput({ title: "消息" })
|
||||
message: string = "";
|
||||
|
||||
async execute() {
|
||||
TestAfterTaskPlugin.calls.push({
|
||||
message: this.message,
|
||||
pipelineResult: this.pipelineResult,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试用失败插件:execute 直接抛错
|
||||
*/
|
||||
@IsTaskPlugin({
|
||||
name: "TestAfterTaskFailPlugin",
|
||||
title: "测试失败插件",
|
||||
supportAfterTask: true,
|
||||
})
|
||||
class TestAfterTaskFailPlugin extends AbstractTaskPlugin {
|
||||
async execute() {
|
||||
throw new Error("模拟后置任务失败");
|
||||
}
|
||||
}
|
||||
// 通过类型字符串引用(插件注册由装饰器完成)
|
||||
void TestAfterTaskFailPlugin;
|
||||
|
||||
/**
|
||||
* 测试用跳过插件:execute 写入输出属性后返回 skip(模拟申请证书插件证书未过期时跳过)
|
||||
*/
|
||||
@IsTaskPlugin({
|
||||
name: "TestSkipOutputPlugin",
|
||||
title: "测试跳过输出插件",
|
||||
})
|
||||
class TestSkipOutputPlugin extends AbstractTaskPlugin {
|
||||
@TaskOutput({ title: "证书" })
|
||||
cert: any;
|
||||
|
||||
async execute() {
|
||||
this.cert = { crt: "mock-crt", key: "mock-key" };
|
||||
return "skip";
|
||||
}
|
||||
}
|
||||
void TestSkipOutputPlugin;
|
||||
|
||||
function createPipeline(afterTasks?: AfterTask[]): Pipeline {
|
||||
return {
|
||||
id: 1,
|
||||
title: "测试流水线",
|
||||
userId: 1,
|
||||
stages: [],
|
||||
afterTasks,
|
||||
} as any;
|
||||
}
|
||||
|
||||
function createExecutor(pipeline: Pipeline): Executor {
|
||||
const executor = new Executor({
|
||||
pipeline,
|
||||
storage: {} as any,
|
||||
onChanged: async () => {},
|
||||
onFinished: async () => {},
|
||||
accessService: {} as any,
|
||||
emailService: {} as any,
|
||||
notificationService: { send: async () => {} } as any,
|
||||
cnameProxyService: {} as any,
|
||||
pluginConfigService: { getPluginConfig: async () => ({ sysSetting: {} }) } as any,
|
||||
urlService: { getPipelineDetailUrl: async () => "http://localhost/detail" } as any,
|
||||
user: { id: 1, role: "user" },
|
||||
serviceGetter: {} as any,
|
||||
});
|
||||
executor.runtime = new RunHistory(1, { type: "user" }, executor.pipeline);
|
||||
executor.currentStatusMap = new RunnableCollection(pipeline);
|
||||
executor.lastStatusMap = new RunnableCollection(undefined);
|
||||
return executor;
|
||||
}
|
||||
|
||||
describe("Executor 后置任务", () => {
|
||||
beforeEach(() => {
|
||||
TestAfterTaskPlugin.calls = [];
|
||||
});
|
||||
|
||||
it("成功条件下执行后置任务,并记录状态与日志", async () => {
|
||||
const pipeline = createPipeline([
|
||||
{ id: "at1", title: "吊销旧证书", when: ["success"], type: "TestAfterTaskPlugin", input: { message: "hello" } },
|
||||
]);
|
||||
const executor = createExecutor(pipeline);
|
||||
// 模拟 runWithHistory 已写入流水线最终状态
|
||||
executor.pipeline.status = { output: {}, status: ResultType.success, result: ResultType.success, startTime: Date.now() };
|
||||
|
||||
await (executor as any).runAfterTasks({ result: ResultType.success });
|
||||
|
||||
// 插件已执行,且能读取流水线运行结果
|
||||
assert.equal(TestAfterTaskPlugin.calls.length, 1);
|
||||
assert.equal(TestAfterTaskPlugin.calls[0].message, "hello");
|
||||
assert.equal(TestAfterTaskPlugin.calls[0].pipelineResult, ResultType.success);
|
||||
// 状态记录为成功
|
||||
const afterTask = executor.pipeline.afterTasks![0];
|
||||
assert.equal(afterTask.status?.result, ResultType.success);
|
||||
// 日志写入运行历史(key: afterTask.at1),可点击查看
|
||||
const logs = executor.runtime.logs["afterTask.at1"];
|
||||
assert.ok(logs && logs.length > 0);
|
||||
assert.ok(logs.some(line => line.includes("执行成功")));
|
||||
});
|
||||
|
||||
it("条件不满足时(失败条件、实际成功)不执行后置任务,记录未触发日志与跳过状态", async () => {
|
||||
const pipeline = createPipeline([
|
||||
{ id: "at2", title: "失败后清理", when: ["error"], type: "TestAfterTaskPlugin", input: {} },
|
||||
]);
|
||||
const executor = createExecutor(pipeline);
|
||||
|
||||
await (executor as any).runAfterTasks({ result: ResultType.success });
|
||||
|
||||
assert.equal(TestAfterTaskPlugin.calls.length, 0);
|
||||
// 未触发:状态标记为 skip(与任务 skip 同语义),日志记录原因
|
||||
const afterTask = executor.pipeline.afterTasks![0];
|
||||
assert.equal(afterTask.status?.result, ResultType.skip);
|
||||
const logs = executor.runtime.logs["afterTask.at2"];
|
||||
assert.ok(logs && logs.length > 0);
|
||||
assert.ok(logs.some(line => line.includes("后置任务未触发")));
|
||||
});
|
||||
|
||||
it("失败转成功条件:上次失败本次成功才触发", async () => {
|
||||
const pipeline = createPipeline([
|
||||
{ id: "at3", title: "转成功通知", when: ["turnToSuccess"], type: "TestAfterTaskPlugin", input: {} },
|
||||
]);
|
||||
const executor = createExecutor(pipeline);
|
||||
executor.lastRuntime = {
|
||||
pipeline: { status: { status: ResultType.error, result: ResultType.error } },
|
||||
} as any;
|
||||
|
||||
await (executor as any).runAfterTasks({ result: ResultType.success });
|
||||
|
||||
assert.equal(TestAfterTaskPlugin.calls.length, 1);
|
||||
});
|
||||
|
||||
it("上次也成功时,失败转成功条件不触发", async () => {
|
||||
const pipeline = createPipeline([
|
||||
{ id: "at4", title: "转成功通知", when: ["turnToSuccess"], type: "TestAfterTaskPlugin", input: {} },
|
||||
]);
|
||||
const executor = createExecutor(pipeline);
|
||||
executor.lastRuntime = {
|
||||
pipeline: { status: { status: ResultType.success, result: ResultType.success } },
|
||||
} as any;
|
||||
|
||||
await (executor as any).runAfterTasks({ result: ResultType.success });
|
||||
|
||||
assert.equal(TestAfterTaskPlugin.calls.length, 0);
|
||||
});
|
||||
|
||||
it("后置任务失败时返回错误(流水线整体视为失败),不再单独发送通知", async () => {
|
||||
const pipeline = createPipeline([
|
||||
{ id: "at5", title: "可能失败的任务", when: ["success"], type: "TestAfterTaskFailPlugin", input: {} },
|
||||
]);
|
||||
const executor = createExecutor(pipeline);
|
||||
let noticeSent = false;
|
||||
(executor as any).options.notificationService.send = async (req: any) => {
|
||||
noticeSent = true;
|
||||
};
|
||||
|
||||
// runAfterTasks 抛出聚合错误(后置任务失败不再单独发送通知)
|
||||
await assert.rejects(
|
||||
async () => {
|
||||
await (executor as any).runAfterTasks({ result: ResultType.success });
|
||||
},
|
||||
(err: any) => {
|
||||
assert.ok(err instanceof Error);
|
||||
assert.ok(err.message.includes("后置任务[可能失败的任务]执行失败"));
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
const afterTask = executor.pipeline.afterTasks![0];
|
||||
assert.equal(afterTask.status?.result, ResultType.error);
|
||||
assert.equal(afterTask.status?.message, "模拟后置任务失败");
|
||||
assert.equal(noticeSent, false);
|
||||
// 失败日志已记录
|
||||
const logs = executor.runtime.logs["afterTask.at5"];
|
||||
assert.ok(logs.some(line => line.includes("执行失败")));
|
||||
});
|
||||
|
||||
it("disabled 的后置任务不执行,记录禁用日志与状态", async () => {
|
||||
const pipeline = createPipeline([
|
||||
{ id: "at6", title: "禁用任务", when: ["success"], type: "TestAfterTaskPlugin", input: {}, disabled: true },
|
||||
]);
|
||||
const executor = createExecutor(pipeline);
|
||||
|
||||
await (executor as any).runAfterTasks({ result: ResultType.success });
|
||||
|
||||
assert.equal(TestAfterTaskPlugin.calls.length, 0);
|
||||
// 禁用:状态标记为 disabled(与任务 disabled 同语义),日志记录原因
|
||||
const afterTask = executor.pipeline.afterTasks![0];
|
||||
assert.equal(afterTask.status?.result, ResultType.disabled);
|
||||
const logs = executor.runtime.logs["afterTask.at6"];
|
||||
assert.ok(logs && logs.length > 0);
|
||||
assert.ok(logs.some(line => line.includes("已禁用")));
|
||||
});
|
||||
});
|
||||
|
||||
describe("Executor 通知日志", () => {
|
||||
it("通知发送成功时记录日志并写入成功状态", async () => {
|
||||
const pipeline = createPipeline();
|
||||
pipeline.notifications = [
|
||||
{ id: "n1", title: "成功通知", type: "other", when: ["success"], notificationId: 0 },
|
||||
];
|
||||
const executor = createExecutor(pipeline);
|
||||
let sendCalled = false;
|
||||
(executor as any).options.notificationService.send = async () => {
|
||||
sendCalled = true;
|
||||
};
|
||||
|
||||
await executor.notification("success");
|
||||
|
||||
assert.equal(sendCalled, true);
|
||||
// 状态写在 executor.pipeline(cloneDeep 后的实例)上,随运行历史保存
|
||||
const notification = executor.pipeline.notifications![0];
|
||||
assert.equal(notification.status?.result, ResultType.success);
|
||||
const logs = executor.runtime.logs["notification.n1"];
|
||||
assert.ok(logs && logs.length > 0);
|
||||
assert.ok(logs.some(line => line.includes("通知发送成功")));
|
||||
});
|
||||
|
||||
it("通知发送失败时记录日志与失败状态", async () => {
|
||||
const pipeline = createPipeline();
|
||||
pipeline.notifications = [
|
||||
{ id: "n2", title: "失败通知", type: "other", when: ["success"], notificationId: 0 },
|
||||
];
|
||||
const executor = createExecutor(pipeline);
|
||||
(executor as any).options.notificationService.send = async () => {
|
||||
throw new Error("通知渠道异常");
|
||||
};
|
||||
|
||||
await executor.notification("success");
|
||||
|
||||
const notification = executor.pipeline.notifications![0];
|
||||
assert.equal(notification.status?.result, ResultType.error);
|
||||
const logs = executor.runtime.logs["notification.n2"];
|
||||
assert.ok(logs.some(line => line.includes("发送失败")));
|
||||
});
|
||||
|
||||
it("通知触发条件不满足时记录未触发日志,并标记跳过状态", async () => {
|
||||
const pipeline = createPipeline();
|
||||
pipeline.notifications = [
|
||||
{ id: "n3", title: "失败才通知", type: "other", when: ["error"], notificationId: 0 },
|
||||
];
|
||||
const executor = createExecutor(pipeline);
|
||||
let sendCalled = false;
|
||||
(executor as any).options.notificationService.send = async () => {
|
||||
sendCalled = true;
|
||||
};
|
||||
|
||||
// 当前时机为 success,通知条件为 error:不发送
|
||||
await executor.notification("success");
|
||||
|
||||
assert.equal(sendCalled, false);
|
||||
// 状态标记为 skip(与任务 skip 同语义,前端显示跳过图标)
|
||||
const notification = executor.pipeline.notifications![0];
|
||||
assert.equal(notification.status?.result, ResultType.skip);
|
||||
// 日志已记录未触发原因,点击通知可查看
|
||||
const logs = executor.runtime.logs["notification.n3"];
|
||||
assert.ok(logs && logs.length > 0);
|
||||
assert.ok(logs.some(line => line.includes("通知未触发")));
|
||||
});
|
||||
|
||||
it("通知已发送后,其他时机条件不满足不覆盖已发送状态", async () => {
|
||||
const pipeline = createPipeline();
|
||||
pipeline.notifications = [
|
||||
{ id: "n4", title: "开始时通知", type: "other", when: ["start", "error"], notificationId: 0 },
|
||||
];
|
||||
const executor = createExecutor(pipeline);
|
||||
let sendCalled = false;
|
||||
(executor as any).options.notificationService.send = async () => {
|
||||
sendCalled = true;
|
||||
};
|
||||
|
||||
// start 时机触发发送成功
|
||||
await executor.notification("start");
|
||||
assert.equal(sendCalled, true);
|
||||
assert.equal(executor.pipeline.notifications![0].status?.result, ResultType.success);
|
||||
// success 时机条件不满足:不应覆盖已发送的成功状态
|
||||
await executor.notification("success");
|
||||
assert.equal(executor.pipeline.notifications![0].status?.result, ResultType.success);
|
||||
});
|
||||
|
||||
it("历史残留状态被运行时清空后,未触发能正确写入跳过状态", async () => {
|
||||
const pipeline = createPipeline();
|
||||
// 模拟历史残留:上一次运行发送成功的状态残留在通知上
|
||||
pipeline.notifications = [
|
||||
{
|
||||
id: "n5",
|
||||
title: "失败才通知",
|
||||
type: "other",
|
||||
when: ["error"],
|
||||
notificationId: 0,
|
||||
status: { output: {}, status: ResultType.success, result: ResultType.success, startTime: 1, endTime: 2 },
|
||||
},
|
||||
];
|
||||
const executor = createExecutor(pipeline);
|
||||
// 运行时清空通知/后置任务的历史状态
|
||||
executor.runtime.clearNotificationStatus();
|
||||
|
||||
// 当前时机为 success,通知条件为 error:未触发应写入 skip,而不是沿用残留的 success
|
||||
await executor.notification("success");
|
||||
|
||||
const notification = executor.pipeline.notifications![0];
|
||||
assert.equal(notification.status?.result, ResultType.skip);
|
||||
});
|
||||
|
||||
it("clearNotificationStatus 清空通知与后置任务的历史状态", () => {
|
||||
const pipeline = createPipeline([
|
||||
{ id: "at9", title: "后置任务", when: ["success"], type: "TestAfterTaskPlugin", input: {} },
|
||||
]);
|
||||
pipeline.notifications = [{ id: "n6", title: "通知", type: "other", when: ["error"], notificationId: 0 }];
|
||||
pipeline.notifications[0].status = { output: {}, status: ResultType.success, result: ResultType.success, startTime: 1, endTime: 2 };
|
||||
pipeline.afterTasks![0].status = { output: {}, status: ResultType.success, result: ResultType.success, startTime: 1, endTime: 2 };
|
||||
|
||||
const runtime = new RunHistory(1, { type: "user" }, pipeline);
|
||||
runtime.clearNotificationStatus();
|
||||
|
||||
assert.equal(pipeline.notifications![0].status, undefined);
|
||||
assert.equal(pipeline.afterTasks![0].status, undefined);
|
||||
});
|
||||
|
||||
it("通知内容附加后置任务执行结果(成功/失败详情)", async () => {
|
||||
const pipeline = createPipeline([
|
||||
{ id: "at10", title: "吊销旧证书", when: ["success"], type: "TestAfterTaskPlugin", input: {} },
|
||||
{ id: "at11", title: "同步CDN", when: ["success"], type: "TestAfterTaskPlugin", input: {} },
|
||||
]);
|
||||
pipeline.notifications = [
|
||||
{ id: "n7", title: "结果通知", type: "other", when: ["success"], notificationId: 0 },
|
||||
];
|
||||
const executor = createExecutor(pipeline);
|
||||
// 模拟后置任务执行结果:一个成功、一个失败
|
||||
executor.pipeline.afterTasks![0].status = {
|
||||
output: {},
|
||||
status: ResultType.success,
|
||||
result: ResultType.success,
|
||||
startTime: 1,
|
||||
endTime: 2,
|
||||
};
|
||||
executor.pipeline.afterTasks![1].status = {
|
||||
output: {},
|
||||
status: ResultType.error,
|
||||
result: ResultType.error,
|
||||
startTime: 1,
|
||||
endTime: 2,
|
||||
message: "模拟CDN同步失败",
|
||||
};
|
||||
let content = "";
|
||||
(executor as any).options.notificationService.send = async (req: any) => {
|
||||
content = req.body.content;
|
||||
};
|
||||
|
||||
await executor.notification("success");
|
||||
|
||||
assert.ok(content.includes("后置任务执行结果"));
|
||||
assert.ok(content.includes("吊销旧证书 执行成功"));
|
||||
assert.ok(content.includes("同步CDN 执行失败"));
|
||||
assert.ok(content.includes("模拟CDN同步失败"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("Executor 步骤执行", () => {
|
||||
it("插件主动返回 skip 时,输出仍写回历史(下次运行可读取上次输出)", async () => {
|
||||
const pipeline = createPipeline();
|
||||
pipeline.stages = [
|
||||
{
|
||||
id: "stage1",
|
||||
title: "阶段",
|
||||
tasks: [
|
||||
{
|
||||
id: "task1",
|
||||
title: "任务",
|
||||
steps: [{ id: "step1", title: "申请证书", type: "TestSkipOutputPlugin", input: {} }],
|
||||
},
|
||||
],
|
||||
},
|
||||
] as any;
|
||||
const executor = createExecutor(pipeline);
|
||||
const step: any = (pipeline.stages as any)[0].tasks[0].steps[0];
|
||||
|
||||
const res = await (executor as any).runWithHistory(step, "step", async () => {
|
||||
return await (executor as any).runStep(step);
|
||||
});
|
||||
|
||||
assert.equal(res, ResultType.skip);
|
||||
// 跳过时插件写入实例的输出属性应写回历史状态(否则下次运行读不到上次的证书)
|
||||
assert.ok(step.status.output.cert, "跳过时输出应写回历史");
|
||||
assert.equal(step.status.output.cert.crt, "mock-crt");
|
||||
});
|
||||
});
|
||||
@@ -1,17 +1,17 @@
|
||||
import { ConcurrencyStrategy, NotificationWhen, Pipeline, ResultType, Runnable, RunStrategy, Stage, Step, Task, ResultError } from "../dt/index.js";
|
||||
import { AfterTask, ConcurrencyStrategy, HistoryResult, NotificationWhen, Pipeline, ResultType, Runnable, RunStrategy, Stage, Step, Task, ResultError } from "../dt/index.js";
|
||||
import { RunHistory, RunnableCollection } from "./run-history.js";
|
||||
import { AbstractTaskPlugin, ITaskPlugin, PluginDefine, pluginRegistry, TaskInstanceContext, UserInfo } from "../plugin/index.js";
|
||||
import { ContextFactory, IContext } from "./context.js";
|
||||
import { IStorage } from "./storage.js";
|
||||
import { createAxiosService, hashUtils, HttpRequestConfig, ILogger, logger, utils } from "@certd/basic";
|
||||
import { buildLogger, createAxiosService, hashUtils, HttpRequestConfig, ILogger, logger, utils } from "@certd/basic";
|
||||
import { IAccessService } from "../access/index.js";
|
||||
import { RegistryItem } from "../registry/index.js";
|
||||
import { Decorator } from "../decorator/index.js";
|
||||
import { ICnameProxyService, IEmailService, IPluginConfigService, IServiceGetter, IUrlService } from "../service/index.js";
|
||||
import { FileStore } from "./file-store.js";
|
||||
import { cloneDeep, forEach, merge } from "lodash-es";
|
||||
import { INotificationService } from "../notification/index.js";
|
||||
import { taskEmitterCreate } from "../service/emit.js";
|
||||
import { INotificationService, PipelineNotificationTypes } from "../notification/index.js";
|
||||
import { pipelineEmitter } from "../service/emit.js";
|
||||
import { RunnableError } from "./exceptions.js";
|
||||
|
||||
export type SysInfo = {
|
||||
@@ -95,21 +95,44 @@ export class Executor {
|
||||
await this.notification("start");
|
||||
|
||||
this.runtime.start(this.pipeline);
|
||||
|
||||
//清理通知 和后置任务的状态(start 时机的状态不需要显示,此后从干净状态重新判断)
|
||||
this.runtime.clearNotificationStatus();
|
||||
intervalFlushLogId = setInterval(async () => {
|
||||
await this.onChanged(this.runtime);
|
||||
}, 5000);
|
||||
|
||||
const result = await this.runWithHistory(this.pipeline, "pipeline", async () => {
|
||||
return await this.runStages(this.pipeline);
|
||||
});
|
||||
if (result === ResultType.success) {
|
||||
let result: ResultType;
|
||||
let error: any = null;
|
||||
try {
|
||||
result = await this.runWithHistory(this.pipeline, "pipeline", async () => {
|
||||
const stagesResult = await this.runStages(this.pipeline);
|
||||
|
||||
// 执行后置任务(失败时流水线整体视为执行失败)
|
||||
const afterTaskResult = await this.runAfterTasks({ result: stagesResult });
|
||||
return this.compositionResultType([...afterTaskResult, stagesResult]);
|
||||
});
|
||||
} catch (e: any) {
|
||||
result = ResultType.error;
|
||||
error = e;
|
||||
this.logger.error("pipeline 执行失败", e);
|
||||
}
|
||||
|
||||
// 通知在后置任务之后发送(通知中附加后置任务执行结果与错误信息)
|
||||
const finalResult = result;
|
||||
if (finalResult === ResultType.success) {
|
||||
if (this.lastRuntime && this.lastRuntime.pipeline.status?.status === ResultType.error) {
|
||||
await this.notification("turnToSuccess");
|
||||
} else {
|
||||
await this.notification("success");
|
||||
}
|
||||
} else if (finalResult === ResultType.skip) {
|
||||
await this.notification("skip");
|
||||
} else if (finalResult === ResultType.error) {
|
||||
await this.notification("error", error);
|
||||
}
|
||||
return result;
|
||||
|
||||
return finalResult;
|
||||
} catch (e: any) {
|
||||
await this.notification("error", e);
|
||||
this.logger.error("pipeline 执行失败", e);
|
||||
@@ -277,8 +300,53 @@ export class Executor {
|
||||
private async runStep(step: Step) {
|
||||
const currentLogger = this.runtime._loggers[step.id];
|
||||
this.currentStatusMap.add(step);
|
||||
const lastStatus = this.lastStatusMap.get(step.id);
|
||||
//执行任务
|
||||
const plugin: RegistryItem<AbstractTaskPlugin> = pluginRegistry.get(step.type);
|
||||
// @ts-ignore
|
||||
const define: PluginDefine = plugin?.define;
|
||||
|
||||
const { instance, result, skipped } = await this.executePlugin(step, currentLogger);
|
||||
if (result === ResultType.skip && skipped) {
|
||||
// SkipWhenSucceed 跳过:状态与输出已在 executePlugin 内拷贝,无需结果处理
|
||||
return result;
|
||||
}
|
||||
|
||||
//执行结果处理(插件主动返回 skip 时也会执行:插件可能已把输出写入实例属性,需写回历史)
|
||||
if (instance._result.clearLastStatus) {
|
||||
//是否需要清除所有状态
|
||||
this.lastStatusMap.clear();
|
||||
}
|
||||
//输出上下文变量到output context
|
||||
forEach(define.output, (item: any, key: any) => {
|
||||
step.status!.output[key] = instance[key];
|
||||
});
|
||||
step.status!.files = instance.getFiles();
|
||||
//更新pipeline vars
|
||||
if (Object.keys(instance._result.pipelineVars).length > 0) {
|
||||
// 判断 pipelineVars 有值时更新
|
||||
let vars = await this.pipelineContext.getObj("vars");
|
||||
vars = vars || {};
|
||||
merge(vars, instance._result.pipelineVars);
|
||||
await this.pipelineContext.setObj("vars", vars);
|
||||
}
|
||||
if (Object.keys(instance._result.pipelinePrivateVars).length > 0) {
|
||||
// 判断 pipelineVars 有值时更新
|
||||
let vars = await this.pipelineContext.getObj("privateVars");
|
||||
vars = vars || {};
|
||||
merge(vars, instance._result.pipelinePrivateVars);
|
||||
await this.pipelineContext.setObj("privateVars", vars);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 实例化插件并执行(任务步骤与后置任务共用同一执行链路)
|
||||
* 负责:插件实例化、系统参数注入、output-selector 解析、参数哈希与跳过判断、执行上下文构建与插件执行。
|
||||
* 后置任务通过伪步骤(无 strategy)复用,SkipWhenSucceed 跳过判断自然不触发。
|
||||
* @param step 步骤(后置任务传伪步骤)
|
||||
* @param currentLogger 当前节点日志logger
|
||||
*/
|
||||
private async executePlugin(step: Step, currentLogger: ILogger): Promise<{ instance: ITaskPlugin; result: any; skipped: boolean }> {
|
||||
const plugin: RegistryItem<AbstractTaskPlugin> = pluginRegistry.get(step.type);
|
||||
if (!plugin) {
|
||||
currentLogger.error(`未找到插件${step.type}`);
|
||||
@@ -295,13 +363,11 @@ export class Executor {
|
||||
currentLogger.error(`实例化插件失败:${step.type}:${e.message}`);
|
||||
throw new Error(`实例化插件失败`, e);
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const define: PluginDefine = plugin.define;
|
||||
const pluginName = define.name;
|
||||
const pluginConfig = await this.options.pluginConfigService.getPluginConfig(pluginName);
|
||||
//从outputContext读取输入参数
|
||||
const input = cloneDeep(step.input);
|
||||
const pluginConfig = await this.options.pluginConfigService.getPluginConfig(define.name);
|
||||
//从配置读取输入参数
|
||||
const input = cloneDeep(step.input || {});
|
||||
const sysInput = pluginConfig.sysSetting?.input || {};
|
||||
//注入系统设置参数
|
||||
for (const sysInputKey in sysInput) {
|
||||
@@ -327,22 +393,25 @@ export class Executor {
|
||||
}
|
||||
});
|
||||
|
||||
//计算参数哈希与是否变化(供插件 ctx.inputChanged 与跳过判断使用;后置任务伪步骤不在 lastStatusMap 中,恒为 true)
|
||||
const newInputHash = hashUtils.md5(JSON.stringify(input));
|
||||
step.status!.inputHash = newInputHash;
|
||||
//判断是否需要跳过
|
||||
const lastNode = this.lastStatusMap.get(step.id);
|
||||
const lastResult = lastNode?.status?.status;
|
||||
if (step.status) {
|
||||
step.status.inputHash = newInputHash;
|
||||
}
|
||||
const lastStatus = this.lastStatusMap.get(step.id);
|
||||
let inputChanged = true;
|
||||
const lastInputHash = lastNode?.status?.inputHash;
|
||||
const lastInputHash = lastStatus?.status?.inputHash;
|
||||
if (lastInputHash && newInputHash && lastInputHash === newInputHash) {
|
||||
//参数没有变化
|
||||
inputChanged = false;
|
||||
}
|
||||
//跳过判断(SkipWhenSucceed 策略;后置任务伪步骤无 strategy 不触发)
|
||||
if (step.strategy?.runStrategy === RunStrategy.SkipWhenSucceed && define.runStrategy !== RunStrategy.AlwaysRun) {
|
||||
const lastResult = lastStatus?.status?.status;
|
||||
if (lastResult != null && lastResult === ResultType.success && !inputChanged) {
|
||||
step.status!.output = lastNode?.status?.output;
|
||||
step.status!.files = lastNode?.status?.files;
|
||||
return ResultType.skip;
|
||||
step.status!.output = lastStatus?.status?.output;
|
||||
step.status!.files = lastStatus?.status?.files;
|
||||
return { instance, result: ResultType.skip, skipped: true };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,47 +450,220 @@ export class Executor {
|
||||
signal: this.abort.signal,
|
||||
utils,
|
||||
user: this.options.user,
|
||||
emitter: taskEmitterCreate({
|
||||
step,
|
||||
pipeline: this.pipeline,
|
||||
}),
|
||||
emitter: pipelineEmitter,
|
||||
serviceGetter: this.options.serviceGetter,
|
||||
projectId: this.pipeline.projectId,
|
||||
};
|
||||
instance.setCtx(taskCtx);
|
||||
await instance.setCtx(taskCtx);
|
||||
|
||||
if (!(instance instanceof AbstractTaskPlugin)) {
|
||||
throw new Error(`插件类型错误:${step.type}不是AbstractTaskPlugin的实例`);
|
||||
}
|
||||
await instance.onInstance();
|
||||
const result = await instance.execute();
|
||||
//执行结果处理
|
||||
if (instance._result.clearLastStatus) {
|
||||
//是否需要清除所有状态
|
||||
this.lastStatusMap.clear();
|
||||
}
|
||||
//输出上下文变量到output context
|
||||
forEach(define.output, (item: any, key: any) => {
|
||||
step.status!.output[key] = instance[key];
|
||||
// const stepOutputKey = `step.${step.id}.${key}`;
|
||||
// this.runtime.context[stepOutputKey] = instance[key];
|
||||
});
|
||||
step.status!.files = instance.getFiles();
|
||||
//更新pipeline vars
|
||||
if (Object.keys(instance._result.pipelineVars).length > 0) {
|
||||
// 判断 pipelineVars 有值时更新
|
||||
let vars = await this.pipelineContext.getObj("vars");
|
||||
vars = vars || {};
|
||||
merge(vars, instance._result.pipelineVars);
|
||||
await this.pipelineContext.setObj("vars", vars);
|
||||
}
|
||||
if (Object.keys(instance._result.pipelinePrivateVars).length > 0) {
|
||||
// 判断 pipelineVars 有值时更新
|
||||
let vars = await this.pipelineContext.getObj("privateVars");
|
||||
vars = vars || {};
|
||||
merge(vars, instance._result.pipelinePrivateVars);
|
||||
await this.pipelineContext.setObj("privateVars", vars);
|
||||
}
|
||||
return { instance, result, skipped: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行后置任务(流水线运行结束后触发)
|
||||
* 后置任务失败时流水线整体视为执行失败:收集所有失败任务的错误信息返回(不再单独发送通知)
|
||||
* @returns 聚合错误(有后置任务失败时返回 Error,否则返回 null)
|
||||
*/
|
||||
private async runAfterTasks(opts: { result: ResultType }): Promise<ResultType[]> {
|
||||
const { result } = opts;
|
||||
const pipelineResult = result;
|
||||
if (!this.pipeline.afterTasks || this.pipeline.afterTasks.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const afterTaskResults: ResultType[] = [];
|
||||
const errorList: { afterTask: AfterTask; err: Error }[] = [];
|
||||
for (const afterTask of this.pipeline.afterTasks) {
|
||||
if (this.abort.signal.aborted) {
|
||||
//用户取消,不再执行后续后置任务
|
||||
break;
|
||||
}
|
||||
if (afterTask.disabled) {
|
||||
// 已禁用:记录日志与状态(与任务 disabled 同语义)
|
||||
await this.markAfterTaskSkipped(afterTask, ResultType.disabled, `后置任务已禁用,不执行`, pipelineResult);
|
||||
continue;
|
||||
}
|
||||
if (!this.matchAfterTaskWhen(afterTask, pipelineResult)) {
|
||||
// 触发条件不满足:记录日志与跳过状态(与任务 skip 同语义)
|
||||
await this.markAfterTaskSkipped(afterTask, ResultType.skip, `后置任务未触发:当前运行结果 ${pipelineResult},触发条件 ${(afterTask.when || []).join(",")}`, pipelineResult);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const res = await this.runOneAfterTask(afterTask);
|
||||
afterTaskResults.push(res);
|
||||
} catch (err: any) {
|
||||
errorList.push({ afterTask, err });
|
||||
}
|
||||
}
|
||||
if (errorList.length > 0) {
|
||||
// 聚合所有失败后置任务的错误信息,抛出后由 runWithHistory 将流水线整体标记为失败
|
||||
const errMessage = errorList.map(item => `后置任务[${item.afterTask.title}]执行失败:${item.err.message}`).join("\n");
|
||||
throw new Error(errMessage);
|
||||
}
|
||||
return afterTaskResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录后置任务未执行(未触发/禁用)的日志与状态
|
||||
* 日志与任务一致包含“开始/结束”两条;状态与任务 skip/disabled 一致,前端显示对应图标
|
||||
*/
|
||||
private async markAfterTaskSkipped(afterTask: AfterTask, result: ResultType, message: string, pipelineResult: ResultType) {
|
||||
const logKey = this.buildAfterTaskLogKey(afterTask);
|
||||
const taskLogger = this.buildTaskLogger(logKey);
|
||||
const now = new Date().getTime();
|
||||
taskLogger.info(message);
|
||||
afterTask.status = {
|
||||
output: {},
|
||||
status: result === ResultType.disabled ? ResultType.canceled : ResultType.skip,
|
||||
result,
|
||||
startTime: now,
|
||||
endTime: now,
|
||||
message,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断后置任务触发条件是否满足(与通知 when 同语义)
|
||||
*/
|
||||
private matchAfterTaskWhen(afterTask: AfterTask, pipelineResult: ResultType): boolean {
|
||||
const whenList = afterTask.when || [];
|
||||
if (whenList.includes("success") && pipelineResult === ResultType.success) {
|
||||
return true;
|
||||
}
|
||||
if (whenList.includes("error") && pipelineResult === ResultType.error) {
|
||||
return true;
|
||||
}
|
||||
// 失败转成功:本次成功且上一次运行失败
|
||||
if (whenList.includes("turnToSuccess") && pipelineResult === ResultType.success && this.lastRuntime?.pipeline?.status?.status === ResultType.error) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行单个后置任务:记录状态与日志,失败时返回错误(由流水线整体承担失败结果)
|
||||
* @returns 失败时返回错误对象,成功/跳过/取消返回 null
|
||||
*/
|
||||
private async runOneAfterTask(afterTask: AfterTask): Promise<ResultType> {
|
||||
const logKey = this.buildAfterTaskLogKey(afterTask);
|
||||
const taskLogger = this.buildTaskLogger(logKey);
|
||||
const status: HistoryResult = {
|
||||
output: {},
|
||||
status: ResultType.start,
|
||||
result: ResultType.start,
|
||||
startTime: new Date().getTime(),
|
||||
};
|
||||
afterTask.status = status;
|
||||
taskLogger.info(`后置任务开始执行,触发条件:${(afterTask.when || []).join(",")}`);
|
||||
try {
|
||||
if (this.abort.signal.aborted) {
|
||||
//用户取消
|
||||
status.status = ResultType.canceled;
|
||||
status.result = ResultType.canceled;
|
||||
status.endTime = new Date().getTime();
|
||||
status.message = "用户取消";
|
||||
taskLogger.warn("用户取消,后置任务不执行");
|
||||
return ResultType.canceled;
|
||||
}
|
||||
const result = await this.executeAfterTaskPlugin(afterTask, taskLogger);
|
||||
if (result === "skip") {
|
||||
// 跳过:记录日志与状态(与任务 skip 同语义)
|
||||
await this.markAfterTaskSkipped(afterTask, ResultType.skip, `后置任务已跳过`, ResultType.success);
|
||||
return ResultType.skip;
|
||||
}
|
||||
status.status = ResultType.success;
|
||||
status.result = ResultType.success;
|
||||
status.endTime = new Date().getTime();
|
||||
taskLogger.info("后置任务执行成功");
|
||||
return ResultType.success;
|
||||
} catch (e: any) {
|
||||
status.status = ResultType.error;
|
||||
status.result = ResultType.error;
|
||||
status.endTime = new Date().getTime();
|
||||
status.message = e.message;
|
||||
taskLogger.error(`后置任务执行失败:`, e);
|
||||
// 抛出由 runAfterTasks 聚合;聚合错误抛出后由 runWithHistory 将流水线整体标记为失败(不再单独发送通知)
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 后置任务日志在运行历史中的key(与任务日志同机制,可点击查看)
|
||||
*/
|
||||
private buildAfterTaskLogKey(afterTask: AfterTask): string {
|
||||
return `afterTask.${afterTask.id}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建一个写入运行历史日志的logger(任务/通知/后置任务共用)
|
||||
*/
|
||||
private buildTaskLogger(logKey: string): ILogger {
|
||||
if (this.runtime._loggers[logKey]) {
|
||||
return this.runtime._loggers[logKey];
|
||||
}
|
||||
const taskLogger = buildLogger((text: string) => {
|
||||
if (!this.runtime.logs[logKey]) {
|
||||
this.runtime.logs[logKey] = [];
|
||||
}
|
||||
this.runtime.logs[logKey].push(text);
|
||||
});
|
||||
this.runtime._loggers[logKey] = taskLogger;
|
||||
return taskLogger;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行后置任务插件:与任务步骤共用同一执行链路(伪Step,插件可访问流水线上下文与运行结果)
|
||||
*/
|
||||
private async executeAfterTaskPlugin(afterTask: AfterTask, taskLogger: ILogger): Promise<void | string> {
|
||||
const plugin: RegistryItem<AbstractTaskPlugin> = pluginRegistry.get(afterTask.type);
|
||||
// 伪步骤:与任务步骤完全相同的插件执行链路(无 strategy,跳过判断自然不触发)
|
||||
const pseudoStep: Step = {
|
||||
id: this.buildAfterTaskLogKey(afterTask),
|
||||
title: afterTask.title || plugin?.define?.name,
|
||||
type: afterTask.type,
|
||||
input: afterTask.input || {},
|
||||
status: afterTask.status,
|
||||
};
|
||||
const { result } = await this.executePlugin(pseudoStep, taskLogger);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集后置任务执行结果摘要(成功/失败/跳过等),用于附加到通知内容
|
||||
*/
|
||||
private buildAfterTaskSummary(): string {
|
||||
if (!this.pipeline.afterTasks || this.pipeline.afterTasks.length === 0) {
|
||||
return "";
|
||||
}
|
||||
const lines: string[] = [];
|
||||
const resultTextMap: Record<string, string> = {
|
||||
success: "执行成功",
|
||||
error: "执行失败",
|
||||
skip: "未触发",
|
||||
disabled: "已禁用",
|
||||
canceled: "已取消",
|
||||
};
|
||||
for (const afterTask of this.pipeline.afterTasks) {
|
||||
const result = afterTask.status?.result;
|
||||
if (result == null) {
|
||||
continue;
|
||||
}
|
||||
if (result === ResultType.error) {
|
||||
lines.push(` - ${afterTask.title} 执行失败,错误详情:${afterTask.status?.message || ""}`);
|
||||
} else {
|
||||
lines.push(` - ${afterTask.title} ${resultTextMap[result] || result}`);
|
||||
}
|
||||
}
|
||||
if (lines.length === 0) {
|
||||
return "";
|
||||
}
|
||||
return `\n\n后置任务执行结果:\n${lines.join("\n")}`;
|
||||
}
|
||||
|
||||
async notification(when: NotificationWhen, error?: any) {
|
||||
if (!this.pipeline.notifications) {
|
||||
return;
|
||||
@@ -463,6 +705,10 @@ export class Executor {
|
||||
errors = error.message;
|
||||
content = `流水线ID:${this.pipeline.id},运行ID:${this.runtime.id}\n\n${this.currentStatusMap?.currentStep?.title} 执行失败\n\n错误详情:${error.message}`;
|
||||
}
|
||||
} else if (when === "skip") {
|
||||
pipelineResult = "执行跳过";
|
||||
subject = `${pipelineResult},${this.pipeline.title}【${this.pipeline.id}】`;
|
||||
content = `流水线ID:${this.pipeline.id},运行ID:${this.runtime.id}\n\n${this.currentStatusMap?.currentStep?.title} 执行跳过\n`;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
@@ -470,33 +716,62 @@ export class Executor {
|
||||
templateData.errors = errors;
|
||||
templateData.pipelineResult = pipelineResult;
|
||||
templateData.title = subject;
|
||||
// 附加后置任务执行结果(通知在后置任务之后发送,可看到后置任务的成功/失败详情)
|
||||
const afterTaskSummary = this.buildAfterTaskSummary();
|
||||
if (afterTaskSummary) {
|
||||
content += afterTaskSummary;
|
||||
}
|
||||
templateData.content = content;
|
||||
|
||||
for (const notification of this.pipeline.notifications) {
|
||||
// 记录通知日志(无论是否触发,便于确认通知发送情况)
|
||||
const logKey = `notification.${notification.id}`;
|
||||
const notificationLogger = this.buildTaskLogger(logKey);
|
||||
if (!notification.when.includes(when)) {
|
||||
// 触发条件不满足:记录“开始/结束”两条日志;若本次运行尚未触发过,则标记为跳过(与任务 skip 同语义)
|
||||
const skipMessage = `通知未触发:当前时机 ${when},通知触发条件 ${(notification.when || []).join(",")}`;
|
||||
notificationLogger.info(skipMessage);
|
||||
if (notification.status == null) {
|
||||
const now = new Date().getTime();
|
||||
notification.status = {
|
||||
output: {},
|
||||
status: ResultType.skip,
|
||||
result: ResultType.skip,
|
||||
startTime: now,
|
||||
endTime: now,
|
||||
message: skipMessage,
|
||||
};
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const notificationStatus: HistoryResult = {
|
||||
output: {},
|
||||
status: ResultType.start,
|
||||
result: ResultType.start,
|
||||
startTime: new Date().getTime(),
|
||||
};
|
||||
notification.status = notificationStatus;
|
||||
notificationLogger.info(`通知开始发送,触发时机:${when},通知方式:${notification.type},标题:${subject}`);
|
||||
|
||||
if (notification.type === "email" && notification.options?.receivers) {
|
||||
try {
|
||||
try {
|
||||
if (notification.type === "email" && notification.options?.receivers) {
|
||||
await this.options.emailService?.sendByTemplate({
|
||||
type: "pipelineResult",
|
||||
type: PipelineNotificationTypes.PipelineResult,
|
||||
data: templateData,
|
||||
receivers: notification.options?.receivers,
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error("send email error", e);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
notificationStatus.status = ResultType.success;
|
||||
notificationStatus.result = ResultType.success;
|
||||
notificationLogger.info("邮件通知发送成功");
|
||||
} else {
|
||||
//发送通知
|
||||
await this.options.notificationService.send({
|
||||
id: notification.notificationId,
|
||||
useDefault: true,
|
||||
useEmail: false,
|
||||
logger: this.logger,
|
||||
logger: notificationLogger,
|
||||
body: {
|
||||
notificationType: "pipelineResult",
|
||||
notificationType: PipelineNotificationTypes.PipelineResult,
|
||||
title: subject,
|
||||
content,
|
||||
userId: this.pipeline.userId,
|
||||
@@ -507,9 +782,17 @@ export class Executor {
|
||||
...templateData,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error("send notification error", e);
|
||||
notificationStatus.status = ResultType.success;
|
||||
notificationStatus.result = ResultType.success;
|
||||
notificationLogger.info("通知发送成功");
|
||||
}
|
||||
} catch (e: any) {
|
||||
notificationStatus.status = ResultType.error;
|
||||
notificationStatus.result = ResultType.error;
|
||||
notificationStatus.message = e.message;
|
||||
notificationLogger.error("通知发送失败:", e);
|
||||
} finally {
|
||||
notificationStatus.endTime = new Date().getTime();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,19 @@ export class RunHistory {
|
||||
this.trigger = trigger;
|
||||
}
|
||||
|
||||
clearNotificationStatus() {
|
||||
if (this.pipeline.notifications) {
|
||||
for (const item of this.pipeline!.notifications) {
|
||||
delete item.status;
|
||||
}
|
||||
}
|
||||
if (this.pipeline.afterTasks) {
|
||||
for (const item of this.pipeline!.afterTasks) {
|
||||
delete item.status;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
start(runnable: Runnable): HistoryResult {
|
||||
const now = new Date().getTime();
|
||||
this.logs[runnable.id] = [];
|
||||
|
||||
@@ -47,6 +47,10 @@ export type Stage = Runnable & {
|
||||
concurrency: ConcurrencyStrategy;
|
||||
next: NextStrategy;
|
||||
maxTaskCount?: number;
|
||||
style?: {
|
||||
width?: number;
|
||||
[key: string]: any;
|
||||
};
|
||||
};
|
||||
|
||||
export type Trigger = {
|
||||
@@ -79,7 +83,7 @@ export type Runnable = {
|
||||
export type EmailOptions = {
|
||||
receivers: string[];
|
||||
};
|
||||
export type NotificationWhen = "error" | "success" | "turnToSuccess" | "start";
|
||||
export type NotificationWhen = "error" | "success" | "turnToSuccess" | "start" | "skip";
|
||||
export type NotificationType = "email" | "other";
|
||||
export type Notification = {
|
||||
type: NotificationType;
|
||||
@@ -88,14 +92,46 @@ export type Notification = {
|
||||
notificationId: number;
|
||||
title: string;
|
||||
id: string;
|
||||
// 运行时状态(随运行历史保存,用于画布展示与日志查看)
|
||||
status?: HistoryResult;
|
||||
};
|
||||
|
||||
/**
|
||||
* 后置任务触发条件(与通知 when 同语义,去掉 start:后置任务只在流水线结束后触发)
|
||||
*/
|
||||
export type AfterTaskWhen = "success" | "error" | "turnToSuccess";
|
||||
|
||||
/**
|
||||
* 流水线后置任务:
|
||||
* 流水线整体运行结束后触发,执行一个特殊的插件任务(接收流水线上下文与运行结果),
|
||||
* 失败时流水线整体视为执行失败(不影响任务本身状态,但通知会附加失败信息)。
|
||||
* 是否需要等待由插件自身控制(如吊销旧证书插件内置等待时长参数)。
|
||||
*/
|
||||
export type AfterTask = {
|
||||
id: string;
|
||||
title: string;
|
||||
// 触发条件:流水线最终结果为成功/失败/失败转成功
|
||||
when: AfterTaskWhen[];
|
||||
// 执行的插件类型(pluginRegistry 中注册的插件,需声明 supportAfterTask)
|
||||
type: string;
|
||||
// 插件输入参数(支持 output-selector 引用步骤输出)
|
||||
input: {
|
||||
[key: string]: any;
|
||||
};
|
||||
disabled?: boolean;
|
||||
// 运行时状态(随运行历史保存,用于画布展示成功/失败图标与日志查看)
|
||||
status?: HistoryResult;
|
||||
};
|
||||
|
||||
export type Pipeline = Runnable & {
|
||||
version?: number;
|
||||
userId: any;
|
||||
projectId?: number;
|
||||
stages: Stage[];
|
||||
triggers: Trigger[];
|
||||
notifications?: Notification[];
|
||||
// 流水线运行结束后触发的后置任务(失败时流水线整体视为执行失败)
|
||||
afterTasks?: AfterTask[];
|
||||
};
|
||||
|
||||
export type Context = {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Registrable } from "../registry/index.js";
|
||||
import { FormItemProps, HistoryResult, Pipeline } from "../dt/index.js";
|
||||
import { HttpClient, ILogger, utils } from "@certd/basic";
|
||||
import * as _ from "lodash-es";
|
||||
import { IEmailService } from "../service/index.js";
|
||||
import { IEmailService, IServiceGetter, getRuntimeDepsService } from "../service/index.js";
|
||||
|
||||
export type NotificationBody = {
|
||||
userId?: number;
|
||||
@@ -39,6 +39,8 @@ export type NotificationInputDefine = FormItemProps & {
|
||||
};
|
||||
export type NotificationDefine = Registrable & {
|
||||
needPlus?: boolean;
|
||||
dependPlugins?: Record<string, string>;
|
||||
dependPackages?: Record<string, string>;
|
||||
input?: {
|
||||
[key: string]: NotificationInputDefine;
|
||||
};
|
||||
@@ -78,6 +80,8 @@ export type NotificationContext = {
|
||||
logger: ILogger;
|
||||
utils: typeof utils;
|
||||
emailService: IEmailService;
|
||||
serviceGetter?: IServiceGetter;
|
||||
define?: NotificationDefine;
|
||||
};
|
||||
|
||||
export abstract class BaseNotification implements INotification {
|
||||
@@ -86,14 +90,22 @@ export abstract class BaseNotification implements INotification {
|
||||
http!: HttpClient;
|
||||
logger!: ILogger;
|
||||
|
||||
async importRuntime(specifier: string) {
|
||||
return await getRuntimeDepsService().importRuntime(specifier, this.logger);
|
||||
}
|
||||
|
||||
async doSend(body: NotificationBody) {
|
||||
if (body.content) {
|
||||
const content = body.content?.replace(/\n/g, " \n");
|
||||
body.content = content;
|
||||
}
|
||||
return await this.send(body);
|
||||
}
|
||||
abstract send(body: NotificationBody): Promise<void>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
async onInstance() {}
|
||||
setCtx(ctx: NotificationContext) {
|
||||
async setCtx(ctx: NotificationContext) {
|
||||
this.ctx = ctx;
|
||||
this.http = ctx.http;
|
||||
this.logger = ctx.logger;
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/// <reference types="mocha" />
|
||||
/// <reference types="node" />
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { PipelineNotificationTypes } from "./constants.js";
|
||||
|
||||
describe("PipelineNotificationTypes", () => {
|
||||
it("contains the pipeline result notification type", () => {
|
||||
assert.equal(PipelineNotificationTypes.PipelineResult, "pipelineResult");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
/** 流水线核心使用的通知类型。业务层通知类型由 @certd/lib-server 扩展。 */
|
||||
export const PipelineNotificationTypes = {
|
||||
PipelineResult: "pipelineResult",
|
||||
} as const;
|
||||
|
||||
export type PipelineNotificationType = (typeof PipelineNotificationTypes)[keyof typeof PipelineNotificationTypes];
|
||||
@@ -61,7 +61,8 @@ export async function newNotification(type: string, input: any, ctx: Notificatio
|
||||
throw new Error("ctx is required");
|
||||
}
|
||||
plugin.setDefine(register.define);
|
||||
plugin.setCtx(ctx);
|
||||
ctx.define = ctx.define || register.define;
|
||||
await plugin.setCtx(ctx);
|
||||
await plugin.onInstance();
|
||||
return plugin;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./api.js";
|
||||
export * from "./constants.js";
|
||||
export * from "./registry.js";
|
||||
export * from "./decorator.js";
|
||||
|
||||
@@ -5,12 +5,12 @@ import { accessRegistry, IAccessService } from "../access/index.js";
|
||||
import { PageSearch } from "../context/index.js";
|
||||
import { FileStore } from "../core/file-store.js";
|
||||
import { CancelError, IContext, RunHistory, RunnableCollection } from "../core/index.js";
|
||||
import { FileItem, FormItemProps, Pipeline, Runnable, Step } from "../dt/index.js";
|
||||
import { FileItem, FormItemProps, Pipeline, ResultType, Runnable, Step } from "../dt/index.js";
|
||||
import { INotificationService } from "../notification/index.js";
|
||||
import { Registrable } from "../registry/index.js";
|
||||
import { IPluginConfigService } from "../service/config.js";
|
||||
import { TaskEmitter } from "../service/emit.js";
|
||||
import { ICnameProxyService, IEmailService, IServiceGetter, IUrlService } from "../service/index.js";
|
||||
import { ICnameProxyService, IEmailService, IServiceGetter, IUrlService, getRuntimeDepsService } from "../service/index.js";
|
||||
|
||||
export type PluginRequestHandleReq<T = any> = {
|
||||
typeName: string;
|
||||
@@ -46,6 +46,8 @@ export type PluginDefine = Registrable & {
|
||||
default?: any;
|
||||
group?: string;
|
||||
icon?: string;
|
||||
dependPlugins?: Record<string, string>;
|
||||
dependPackages?: Record<string, string>;
|
||||
input?: {
|
||||
[key: string]: TaskInputDefine;
|
||||
};
|
||||
@@ -67,12 +69,16 @@ export type PluginDefine = Registrable & {
|
||||
runStrategy?: any;
|
||||
pluginType?: string; //类型
|
||||
type?: string; //来源
|
||||
// 标记该插件可作为流水线后置任务使用(展示在后置任务选择器中)
|
||||
supportAfterTask?: boolean;
|
||||
};
|
||||
|
||||
export type ITaskPlugin = {
|
||||
onInstance(): Promise<void>;
|
||||
execute(): Promise<void | string>;
|
||||
onRequest(req: PluginRequestHandleReq<any>): Promise<any>;
|
||||
setCtx(ctx: TaskInstanceContext): Promise<void>;
|
||||
importRuntime(specifier: string): Promise<any>;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
@@ -88,6 +94,12 @@ export type CertTargetItem = {
|
||||
label: string;
|
||||
domain: string | string[];
|
||||
};
|
||||
|
||||
export type PipelineEvent<T = any> = {
|
||||
cert: T;
|
||||
runnableId: string;
|
||||
pipelineId: number;
|
||||
};
|
||||
export type TaskInstanceContext = {
|
||||
//流水线定义
|
||||
pipeline: Pipeline;
|
||||
@@ -134,7 +146,7 @@ export type TaskInstanceContext = {
|
||||
//项目id
|
||||
projectId?: number;
|
||||
|
||||
emitter: TaskEmitter;
|
||||
emitter: TaskEmitter<PipelineEvent>;
|
||||
|
||||
//service 容器
|
||||
serviceGetter?: IServiceGetter;
|
||||
@@ -147,6 +159,25 @@ export abstract class AbstractTaskPlugin implements ITaskPlugin {
|
||||
http!: HttpClient;
|
||||
accessService!: IAccessService;
|
||||
|
||||
get pipeline() {
|
||||
return this.ctx.pipeline;
|
||||
}
|
||||
|
||||
get step() {
|
||||
return this.ctx.step;
|
||||
}
|
||||
|
||||
/**
|
||||
* 流水线本次运行结果(后置任务插件可用:success/error/canceled/skip/disabled)
|
||||
*/
|
||||
get pipelineResult(): ResultType | undefined {
|
||||
return this.ctx?.runtime?.pipeline?.status?.result;
|
||||
}
|
||||
|
||||
async importRuntime(specifier: string) {
|
||||
return await getRuntimeDepsService().importRuntime(specifier, this.logger);
|
||||
}
|
||||
|
||||
clearLastStatus() {
|
||||
this._result.clearLastStatus = true;
|
||||
}
|
||||
@@ -161,7 +192,14 @@ export abstract class AbstractTaskPlugin implements ITaskPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
setCtx(ctx: TaskInstanceContext) {
|
||||
/**
|
||||
* 等待指定毫秒(插件内自行控制延迟,如等待部署生效后再执行)
|
||||
*/
|
||||
async sleep(ms: number) {
|
||||
await this.ctx.utils.sleep(ms);
|
||||
}
|
||||
|
||||
async setCtx(ctx: TaskInstanceContext) {
|
||||
this.ctx = ctx;
|
||||
this.logger = ctx.logger;
|
||||
this.accessService = ctx.accessService;
|
||||
@@ -244,14 +282,6 @@ export abstract class AbstractTaskPlugin implements ITaskPlugin {
|
||||
this._result.files.push(...(this.ctx.lastStatus?.status?.files || []));
|
||||
}
|
||||
|
||||
get pipeline() {
|
||||
return this.ctx.pipeline;
|
||||
}
|
||||
|
||||
get step() {
|
||||
return this.ctx.step;
|
||||
}
|
||||
|
||||
async onInstance(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import { logger } from "@certd/basic";
|
||||
import { Pipeline, Runnable } from "../dt";
|
||||
|
||||
export type PipelineEventListener = (...args: any[]) => Promise<void>;
|
||||
export type PipelineEvent<T> = {
|
||||
pipeline: Pipeline;
|
||||
step: Runnable;
|
||||
event: T;
|
||||
};
|
||||
export class PipelineEmitter {
|
||||
export class PipelineEmitter<T> implements TaskEmitter<T> {
|
||||
events: Record<string, PipelineEventListener[]>;
|
||||
constructor() {
|
||||
this.events = {};
|
||||
@@ -18,7 +11,7 @@ export class PipelineEmitter {
|
||||
}
|
||||
this.events[event].push(listener);
|
||||
}
|
||||
async emit<T>(name: string, event: PipelineEvent<T>) {
|
||||
async emit<T>(name: string, event: T) {
|
||||
const listeners = this.events[name];
|
||||
if (listeners) {
|
||||
for (const listener of listeners) {
|
||||
@@ -32,7 +25,7 @@ export class PipelineEmitter {
|
||||
}
|
||||
off(event: string, listener: PipelineEventListener) {
|
||||
if (this.events[event]) {
|
||||
this.events[event] = this.events[event].filter((l) => l !== listener);
|
||||
this.events[event] = this.events[event].filter(l => l !== listener);
|
||||
}
|
||||
}
|
||||
once(event: string, listener: PipelineEventListener) {
|
||||
@@ -46,23 +39,6 @@ export class PipelineEmitter {
|
||||
|
||||
export const pipelineEmitter = new PipelineEmitter();
|
||||
|
||||
export type TaskEmitterCreateReq = {
|
||||
step: Runnable;
|
||||
pipeline: Pipeline;
|
||||
export type TaskEmitter<T> = {
|
||||
emit: (name: string, event: T) => Promise<void>;
|
||||
};
|
||||
|
||||
export type TaskEmitter = {
|
||||
emit: <T>(name: string, event: T) => Promise<void>;
|
||||
};
|
||||
|
||||
export function taskEmitterCreate(req: TaskEmitterCreateReq) {
|
||||
return {
|
||||
emit: async <T>(name: string, event: T) => {
|
||||
await pipelineEmitter.emit(name, {
|
||||
pipeline: req.pipeline,
|
||||
step: req.step,
|
||||
event,
|
||||
});
|
||||
},
|
||||
} as TaskEmitter;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ export * from "./cname.js";
|
||||
export * from "./config.js";
|
||||
export * from "./url.js";
|
||||
export * from "./emit.js";
|
||||
export * from "./runtime.js";
|
||||
export type IServiceGetter = {
|
||||
get: <T>(name: string) => Promise<T>;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
import assert from "assert";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import { RuntimeDepsService, NpmRegistryResolver } from "./runtime.js";
|
||||
|
||||
describe("RuntimeDepsService", () => {
|
||||
it("builds a runtime package manifest in the target directory", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-"));
|
||||
const service = new RuntimeDepsService({ rootDir }, null);
|
||||
service.registryResolver = {
|
||||
async resolveOrdered() {
|
||||
return ["https://registry.npmmirror.com"];
|
||||
},
|
||||
} as any;
|
||||
service.commandRunner = {
|
||||
async run(command: string, args: string[]) {
|
||||
assert.equal(command, "pnpm");
|
||||
assert.equal(args[0], "install");
|
||||
assert.ok(args.includes("--ignore-workspace"));
|
||||
assert.ok(args.includes("--no-frozen-lockfile"));
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
},
|
||||
} as any;
|
||||
const result = await service.ensureDependencies({ dependencies: { foo: "^1.0.0" } });
|
||||
assert.equal(result.registryUrl, "https://registry.npmmirror.com");
|
||||
assert.ok(fs.existsSync(path.join(rootDir, "package.json")));
|
||||
});
|
||||
|
||||
it("installs direct dependency maps without plugin metadata", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-direct-"));
|
||||
const service = new RuntimeDepsService({ rootDir }, null);
|
||||
service.registryResolver = {
|
||||
async resolveOrdered() {
|
||||
return [""];
|
||||
},
|
||||
} as any;
|
||||
service.commandRunner = {
|
||||
async run(command: string, args: string[]) {
|
||||
assert.equal(command, "pnpm");
|
||||
fs.mkdirSync(path.join(rootDir, "node_modules"), { recursive: true });
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
},
|
||||
} as any;
|
||||
await service.ensureDependencies({ dependencies: { directPkg: "^1.0.0" } });
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
|
||||
assert.deepEqual(manifest.dependencies, { directPkg: "^1.0.0" });
|
||||
});
|
||||
|
||||
it("imports from runtime node_modules without installing", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-import-"));
|
||||
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", type: "module", main: "index.js" }), "utf8");
|
||||
fs.writeFileSync(path.join(packageDir, "index.js"), "export const value = 42;\n", "utf8");
|
||||
const service = new RuntimeDepsService({ rootDir }, null);
|
||||
service.commandRunner = {
|
||||
async run() {
|
||||
throw new Error("install should not run");
|
||||
},
|
||||
} as any;
|
||||
const mod = await service.importRuntime("runtime-only");
|
||||
assert.equal(mod.value, 42);
|
||||
});
|
||||
|
||||
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);
|
||||
service.registryResolver = {
|
||||
async resolveOrdered() {
|
||||
return [""];
|
||||
},
|
||||
} as any;
|
||||
service.commandRunner = {
|
||||
async run(command: string, args: string[]) {
|
||||
assert.equal(command, "pnpm");
|
||||
const packageDir = path.join(rootDir, "node_modules", "lazy-pkg", "sub");
|
||||
fs.mkdirSync(packageDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(rootDir, "node_modules", "lazy-pkg", "package.json"), JSON.stringify({ name: "lazy-pkg", type: "module" }), "utf8");
|
||||
fs.writeFileSync(path.join(packageDir, "entry.js"), "export const value = 7;\n", "utf8");
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
},
|
||||
} as any;
|
||||
const mod = await service.importRuntime("lazy-pkg/sub/entry.js");
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
|
||||
assert.deepEqual(manifest.dependencies, { "lazy-pkg": "^1.2.3" });
|
||||
assert.equal(mod.value, 7);
|
||||
});
|
||||
|
||||
it("reports lazy dependency installation failure without falling back to project dependencies", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-lazy-failed-"));
|
||||
const service = new RuntimeDepsService({ rootDir, lazyDependencies: { "failed-pkg": "^1.2.3" } }, null);
|
||||
service.registryResolver = {
|
||||
async resolveOrdered() {
|
||||
return [""];
|
||||
},
|
||||
} as any;
|
||||
service.commandRunner = {
|
||||
async run(command: string, args: string[]) {
|
||||
assert.equal(command, "pnpm");
|
||||
return { stdout: "", stderr: "package download failed", code: 1 };
|
||||
},
|
||||
} as any;
|
||||
|
||||
await assert.rejects(() => service.importRuntime("failed-pkg/sub/entry.js"), /动态依赖安装失败: failed-pkg: 动态依赖安装失败: package download failed/);
|
||||
});
|
||||
|
||||
it("resolves scoped package names for lazy imports", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-scoped-"));
|
||||
const service = new RuntimeDepsService({ rootDir, lazyDependencies: { "@scope/lazy": "^2.0.0" } }, null);
|
||||
service.registryResolver = {
|
||||
async resolveOrdered() {
|
||||
return [""];
|
||||
},
|
||||
} as any;
|
||||
service.commandRunner = {
|
||||
async run(command: string, args: string[]) {
|
||||
assert.equal(command, "pnpm");
|
||||
const packageDir = path.join(rootDir, "node_modules", "@scope", "lazy", "dist");
|
||||
fs.mkdirSync(packageDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(rootDir, "node_modules", "@scope", "lazy", "package.json"), JSON.stringify({ name: "@scope/lazy", type: "module" }), "utf8");
|
||||
fs.writeFileSync(path.join(packageDir, "index.js"), "export const scoped = true;\n", "utf8");
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
},
|
||||
} as any;
|
||||
const mod = await service.importRuntime("@scope/lazy/dist/index.js");
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
|
||||
assert.deepEqual(manifest.dependencies, { "@scope/lazy": "^2.0.0" });
|
||||
assert.equal(mod.scoped, true);
|
||||
});
|
||||
|
||||
it("reports missing lazy dependency configuration", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-lazy-missing-"));
|
||||
const service = new RuntimeDepsService({ rootDir, lazyDependencies: {} }, null);
|
||||
await assert.rejects(() => service.importRuntime("missing-pkg/sub.js"), /未配置懒加载版本: missing-pkg/);
|
||||
});
|
||||
|
||||
it("rejects lazy dependency installation when disabled", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-disabled-"));
|
||||
const service = new RuntimeDepsService({ rootDir, enabled: false, lazyDependencies: { "lazy-pkg": "^1.0.0" } }, null);
|
||||
service.commandRunner = {
|
||||
async run() {
|
||||
throw new Error("disabled runtime dependencies should not install packages");
|
||||
},
|
||||
} as any;
|
||||
|
||||
await assert.rejects(() => service.importRuntime("lazy-pkg/index.js"), /动态安装依赖未开启/);
|
||||
});
|
||||
|
||||
it("prefers project dependencies over lazy dependency installation", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-project-fallback-"));
|
||||
const service = new RuntimeDepsService({ rootDir, lazyDependencies: { dayjs: "^1.11.0" } }, null);
|
||||
service.commandRunner = {
|
||||
async run() {
|
||||
throw new Error("project dependency should not trigger installation");
|
||||
},
|
||||
} as any;
|
||||
const mod = await service.importRuntime("dayjs");
|
||||
assert.equal(typeof mod.default, "function");
|
||||
});
|
||||
|
||||
it("rejects a second installation while the first installation is running", async () => {
|
||||
const rootDir = path.join(fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-concurrent-")), ".runtime-deps");
|
||||
const service = new RuntimeDepsService({ rootDir }, null);
|
||||
service.registryResolver = {
|
||||
async resolveOrdered() {
|
||||
return [""];
|
||||
},
|
||||
} as any;
|
||||
|
||||
let finishInstall: () => void;
|
||||
const installFinished = new Promise<void>(resolve => {
|
||||
finishInstall = resolve;
|
||||
});
|
||||
service.commandRunner = {
|
||||
async run() {
|
||||
await installFinished;
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
},
|
||||
} as any;
|
||||
|
||||
const firstInstall = service.ensureDependencies({ dependencies: { first: "^1.0.0" } });
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
await assert.rejects(() => service.ensureDependencies({ dependencies: { second: "^1.0.0" } }), /动态安装依赖正在执行中/);
|
||||
await assert.rejects(() => service.clearRuntimeDeps(), /仍有依赖正在安装/);
|
||||
finishInstall!();
|
||||
await firstInstall;
|
||||
});
|
||||
|
||||
it("releases the installation lock after an installation failure", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-retry-"));
|
||||
const service = new RuntimeDepsService({ rootDir }, null);
|
||||
service.registryResolver = {
|
||||
async resolveOrdered() {
|
||||
return [""];
|
||||
},
|
||||
} as any;
|
||||
|
||||
let installCount = 0;
|
||||
service.commandRunner = {
|
||||
async run() {
|
||||
installCount += 1;
|
||||
if (installCount === 1) {
|
||||
return { stdout: "", stderr: "installation timeout", code: 1 };
|
||||
}
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
},
|
||||
} as any;
|
||||
|
||||
await assert.rejects(() => service.ensureDependencies({ dependencies: { first: "^1.0.0" } }), /动态依赖安装失败/);
|
||||
await service.ensureDependencies({ dependencies: { second: "^1.0.0" } });
|
||||
assert.equal(installCount, 2);
|
||||
});
|
||||
|
||||
it("keeps previously installed dependencies when adding a later package", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-merge-"));
|
||||
const service = new RuntimeDepsService({ rootDir }, null);
|
||||
service.registryResolver = {
|
||||
async resolveOrdered() {
|
||||
return [""];
|
||||
},
|
||||
} as any;
|
||||
service.commandRunner = {
|
||||
async run(command: string, args: string[]) {
|
||||
assert.equal(command, "pnpm");
|
||||
if (args.includes("--version")) {
|
||||
return { stdout: "9.1.0\n", stderr: "", code: 0 };
|
||||
}
|
||||
fs.mkdirSync(path.join(rootDir, "node_modules"), { recursive: true });
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
},
|
||||
} as any;
|
||||
await service.ensureDependencies({ dependencies: { foo: "^1.0.0" } });
|
||||
await service.ensureDependencies({ dependencies: { bar: "^2.0.0" } });
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
|
||||
assert.deepEqual(manifest.dependencies, { foo: "^1.0.0", bar: "^2.0.0" });
|
||||
});
|
||||
|
||||
it("does not pass node debugger options to pnpm child process", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-env-"));
|
||||
const oldNodeOptions = process.env.NODE_OPTIONS;
|
||||
const oldInspectorOptions = process.env.VSCODE_INSPECTOR_OPTIONS;
|
||||
process.env.NODE_OPTIONS = "--inspect=127.0.0.1:9229 --max-old-space-size=4096";
|
||||
process.env.VSCODE_INSPECTOR_OPTIONS = '{"inspectorIpc":"test"}';
|
||||
try {
|
||||
const service = new RuntimeDepsService({ rootDir }, null);
|
||||
service.registryResolver = {
|
||||
async resolveOrdered() {
|
||||
return [""];
|
||||
},
|
||||
} as any;
|
||||
service.commandRunner = {
|
||||
async run(command: string, args: string[], options: { env?: NodeJS.ProcessEnv }) {
|
||||
assert.equal(options.env?.NODE_OPTIONS, "--max-old-space-size=4096");
|
||||
assert.equal(options.env?.VSCODE_INSPECTOR_OPTIONS, undefined);
|
||||
assert.equal(options.env?.CI, "true");
|
||||
assert.equal(options.env?.pnpm_config_confirm_modules_purge, "false");
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
},
|
||||
} as any;
|
||||
await service.ensureDependencies({ dependencies: { foo: "^1.0.0" } });
|
||||
} finally {
|
||||
if (oldNodeOptions == null) {
|
||||
delete process.env.NODE_OPTIONS;
|
||||
} else {
|
||||
process.env.NODE_OPTIONS = oldNodeOptions;
|
||||
}
|
||||
if (oldInspectorOptions == null) {
|
||||
delete process.env.VSCODE_INSPECTOR_OPTIONS;
|
||||
} else {
|
||||
process.env.VSCODE_INSPECTOR_OPTIONS = oldInspectorOptions;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects clearing unexpected runtime dependency path", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-clear-invalid-"));
|
||||
const service = new RuntimeDepsService({ rootDir }, null);
|
||||
await assert.rejects(() => service.clearRuntimeDeps(), /动态依赖目录配置异常/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("NpmRegistryResolver", () => {
|
||||
it("uses fixed registry without probing", async () => {
|
||||
const resolver = new NpmRegistryResolver({
|
||||
mode: "fixed",
|
||||
fixedUrl: "https://registry.example.com",
|
||||
probeTimeoutMs: 100,
|
||||
cacheTtlMs: 1000,
|
||||
});
|
||||
const result = await resolver.resolveOrdered();
|
||||
assert.deepEqual(result, ["https://registry.example.com"]);
|
||||
});
|
||||
it("returns ordered list via resolveOrdered (fastest first)", async () => {
|
||||
const resolver = new NpmRegistryResolver({
|
||||
mode: "auto",
|
||||
candidates: ["https://slow.example.com", "https://fast.example.com"],
|
||||
probeTimeoutMs: 100,
|
||||
cacheTtlMs: 1000,
|
||||
});
|
||||
resolver.probe = async (registryUrl: string) => ({
|
||||
registryUrl,
|
||||
ok: true,
|
||||
elapsedMs: registryUrl.includes("fast") ? 10 : 50,
|
||||
});
|
||||
const result = await resolver.resolveOrdered();
|
||||
assert.deepEqual(result, ["https://fast.example.com", "https://slow.example.com"]);
|
||||
});
|
||||
it("includes failed registries at the end of resolveOrdered", async () => {
|
||||
const resolver = new NpmRegistryResolver({
|
||||
mode: "auto",
|
||||
candidates: ["https://good.example.com", "https://bad.example.com"],
|
||||
probeTimeoutMs: 100,
|
||||
cacheTtlMs: 1000,
|
||||
});
|
||||
resolver.probe = async (registryUrl: string) => {
|
||||
if (registryUrl.includes("bad")) {
|
||||
return { registryUrl, ok: false, elapsedMs: 200 };
|
||||
}
|
||||
return { registryUrl, ok: true, elapsedMs: 30 };
|
||||
};
|
||||
const result = await resolver.resolveOrdered();
|
||||
assert.deepEqual(result, ["https://good.example.com", "https://bad.example.com"]);
|
||||
});
|
||||
it("returns empty ordered list when no candidates", async () => {
|
||||
const resolver = new NpmRegistryResolver({ mode: "auto", candidates: [] });
|
||||
const result = await resolver.resolveOrdered();
|
||||
assert.deepEqual(result, []);
|
||||
});
|
||||
it("re-validates cached URL on resolveOrdered call", async () => {
|
||||
let probeCount = 0;
|
||||
const resolver = new NpmRegistryResolver({
|
||||
mode: "auto",
|
||||
candidates: ["https://mirror.example.com"],
|
||||
cacheTtlMs: 60000,
|
||||
});
|
||||
resolver.probe = async (registryUrl: string) => {
|
||||
probeCount++;
|
||||
return { registryUrl, ok: true, elapsedMs: 10 };
|
||||
};
|
||||
const first = await resolver.resolveOrdered();
|
||||
assert.deepEqual(first, ["https://mirror.example.com"]);
|
||||
assert.equal(probeCount, 1);
|
||||
const second = await resolver.resolveOrdered();
|
||||
assert.deepEqual(second, ["https://mirror.example.com"]);
|
||||
assert.equal(probeCount, 2);
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user