mirror of
https://github.com/certd/certd.git
synced 2026-06-20 16:47:35 +08:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -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 仓库前应先检查该锁文件或占用状态。
|
|
||||||
@@ -0,0 +1,412 @@
|
|||||||
|
# 插件依赖按需加载方案
|
||||||
|
|
||||||
|
## 背景与目标
|
||||||
|
|
||||||
|
### 当前问题
|
||||||
|
- `packages/ui/certd-server/node_modules` 包含 50+ 个插件的所有依赖,体积庞大
|
||||||
|
- 大量云厂商 SDK(AWS、阿里云、腾讯云、华为云等)只在特定插件中使用
|
||||||
|
- 用户通常只使用少数几个插件,但必须安装所有依赖
|
||||||
|
|
||||||
|
### 目标
|
||||||
|
实现依赖的按需下载和加载:
|
||||||
|
1. 插件依赖独立管理,不占用主 `node_modules` 空间
|
||||||
|
2. 只有当用户首次使用某插件时,才动态下载该插件需要的依赖
|
||||||
|
3. 依赖安装完成后,通过 `await import()` 从独立路径加载
|
||||||
|
4. 保持现有插件代码的最小改动
|
||||||
|
|
||||||
|
## 当前架构分析
|
||||||
|
|
||||||
|
### 插件加载机制
|
||||||
|
- 插件位于 `packages/ui/certd-server/src/plugins/` 下(50+ 个插件目录)
|
||||||
|
- `AutoLoadPlugins` 类在启动时扫描 `dist/plugins` 目录并动态导入
|
||||||
|
- 插件注册到不同的 registry:`accessRegistry`, `pluginRegistry`, `dnsProviderRegistry` 等
|
||||||
|
- 插件代码已经使用 `await import()` 进行懒加载(如 `await import("@aws-sdk/client-acm")`)
|
||||||
|
|
||||||
|
### 重型依赖分布
|
||||||
|
从 `packages/ui/certd-server/package.json` 分析,以下依赖体积大且仅特定插件使用:
|
||||||
|
|
||||||
|
**云厂商 SDK(按插件分组):**
|
||||||
|
- **AWS 插件**:`@aws-sdk/client-acm`, `@aws-sdk/client-cloudfront`, `@aws-sdk/client-iam`, `@aws-sdk/client-route-53`, `@aws-sdk/client-s3`, `@aws-sdk/client-sts`
|
||||||
|
- **阿里云插件**:`@alicloud/openapi-client`, `@alicloud/pop-core`, `@alicloud/tea-typescript`, `@alicloud/fc20230330` 等
|
||||||
|
- **腾讯云插件**:`tencentcloud-sdk-nodejs`, `cos-nodejs-sdk-v5`
|
||||||
|
- **华为云插件**:`@huaweicloud/huaweicloud-sdk-cdn`, `@huaweicloud/huaweicloud-sdk-core` 等
|
||||||
|
- **Azure 插件**:`@azure/arm-dns`, `@azure/identity`
|
||||||
|
- **Google Cloud 插件**:`@google-cloud/dns`, `@google-cloud/publicca`
|
||||||
|
- **火山引擎插件**:`@volcengine/openapi`, `@volcengine/tos-sdk`
|
||||||
|
|
||||||
|
**网络/工具库:**
|
||||||
|
- `ssh2`, `socks`, `socks-proxy-agent`(SSH 相关插件)
|
||||||
|
- `ali-oss`, `qiniu`, `basic-ftp`(存储/传输插件)
|
||||||
|
- `nodemailer`(邮件通知插件)
|
||||||
|
|
||||||
|
**通用依赖(保留在主 package.json):**
|
||||||
|
- `@midwayjs/*` 系列(框架核心)
|
||||||
|
- `@certd/*` 系列(项目内部包)
|
||||||
|
- `axios`, `lodash-es`, `dayjs`, `js-yaml` 等基础工具
|
||||||
|
|
||||||
|
## 设计方案
|
||||||
|
|
||||||
|
### 架构概览
|
||||||
|
|
||||||
|
```
|
||||||
|
packages/ui/certd-server/
|
||||||
|
├── package.json # 主依赖(框架、通用工具)
|
||||||
|
├── node_modules/ # 主依赖安装目录
|
||||||
|
├── optional-deps/ # 新增:可选依赖管理目录
|
||||||
|
│ ├── package.json # 可选依赖总配置(用于 pnpm install)
|
||||||
|
│ ├── pnpm-lock.yaml # 可选依赖锁文件
|
||||||
|
│ └── node_modules/ # 可选依赖安装目录
|
||||||
|
├── src/
|
||||||
|
│ └── modules/
|
||||||
|
│ └── dependency/ # 新增:依赖管理模块
|
||||||
|
│ ├── dependency-manager.ts # 核心:依赖管理器
|
||||||
|
│ ├── dependency-registry.ts # 依赖注册表(插件 -> 依赖映射)
|
||||||
|
│ └── types.ts # 类型定义
|
||||||
|
```
|
||||||
|
|
||||||
|
### 核心组件
|
||||||
|
|
||||||
|
#### 1. 依赖管理器(DependencyManager)
|
||||||
|
|
||||||
|
**职责:**
|
||||||
|
- 检查依赖是否已安装
|
||||||
|
- 动态执行 `pnpm install` 安装缺失依赖
|
||||||
|
- 提供从 `optional-deps/node_modules` 加载依赖的方法
|
||||||
|
- 并发控制:避免多个插件同时触发安装
|
||||||
|
|
||||||
|
**关键方法:**
|
||||||
|
```typescript
|
||||||
|
class DependencyManager {
|
||||||
|
// 确保依赖已安装,返回依赖模块
|
||||||
|
async ensureAndImport<T>(packageName: string): Promise<T>
|
||||||
|
|
||||||
|
// 检查依赖是否已安装
|
||||||
|
async isInstalled(packageName: string): Promise<boolean>
|
||||||
|
|
||||||
|
// 安装依赖(带锁,避免并发)
|
||||||
|
async installDependencies(packages: string[]): Promise<void>
|
||||||
|
|
||||||
|
// 从 optional-deps/node_modules 加载依赖
|
||||||
|
async loadModule<T>(packageName: string): Promise<T>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**实现要点:**
|
||||||
|
- 使用文件锁(如 `proper-lockfile`)防止并发安装
|
||||||
|
- 安装前检查 `optional-deps/node_modules/{packageName}` 是否存在
|
||||||
|
- 安装命令:`pnpm install --dir optional-deps --ignore-workspace`
|
||||||
|
- 加载时使用绝对路径:`import('file:///absolute/path/to/optional-deps/node_modules/package')`
|
||||||
|
|
||||||
|
#### 2. 依赖注册表(DependencyRegistry)
|
||||||
|
|
||||||
|
**职责:**
|
||||||
|
- 维护插件名称到依赖列表的映射
|
||||||
|
- 提供依赖查询接口
|
||||||
|
|
||||||
|
**数据结构:**
|
||||||
|
```typescript
|
||||||
|
interface PluginDependencyConfig {
|
||||||
|
pluginName: string;
|
||||||
|
dependencies: {
|
||||||
|
packageName: string;
|
||||||
|
version: string;
|
||||||
|
optional?: boolean; // 是否可选(安装失败不阻塞)
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 示例注册
|
||||||
|
dependencyRegistry.register('plugin-aws', [
|
||||||
|
{ packageName: '@aws-sdk/client-acm', version: '^3.964.0' },
|
||||||
|
{ packageName: '@aws-sdk/client-cloudfront', version: '^3.964.0' },
|
||||||
|
{ packageName: '@aws-sdk/client-route-53', version: '^3.964.0' },
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 插件集成
|
||||||
|
|
||||||
|
**改造现有插件代码:**
|
||||||
|
|
||||||
|
改造前(`plugin-aws/libs/aws-client.ts`):
|
||||||
|
```typescript
|
||||||
|
const { ACMClient, ImportCertificateCommand } = await import("@aws-sdk/client-acm");
|
||||||
|
```
|
||||||
|
|
||||||
|
改造后:
|
||||||
|
```typescript
|
||||||
|
import { DependencyManager } from "../../../modules/dependency/dependency-manager.js";
|
||||||
|
|
||||||
|
const depManager = new DependencyManager();
|
||||||
|
const { ACMClient, ImportCertificateCommand } = await depManager.ensureAndImport("@aws-sdk/client-acm");
|
||||||
|
```
|
||||||
|
|
||||||
|
**简化方案(推荐):**
|
||||||
|
|
||||||
|
创建辅助函数,减少改动量:
|
||||||
|
```typescript
|
||||||
|
// src/modules/dependency/import-helper.ts
|
||||||
|
export async function importOptionalDep<T>(packageName: string): Promise<T> {
|
||||||
|
const depManager = new DependencyManager();
|
||||||
|
return await depManager.ensureAndImport<T>(packageName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 插件中使用
|
||||||
|
import { importOptionalDep } from "../../../modules/dependency/import-helper.js";
|
||||||
|
const { ACMClient } = await importOptionalDep("@aws-sdk/client-acm");
|
||||||
|
```
|
||||||
|
|
||||||
|
### 实施步骤
|
||||||
|
|
||||||
|
#### 阶段一:基础设施搭建
|
||||||
|
1. 创建 `optional-deps/` 目录结构
|
||||||
|
2. 生成 `optional-deps/package.json`(包含所有可选依赖)
|
||||||
|
3. 实现 `DependencyManager` 核心逻辑
|
||||||
|
4. 实现依赖安装锁机制
|
||||||
|
5. 编写单元测试
|
||||||
|
|
||||||
|
#### 阶段二:依赖迁移
|
||||||
|
6. 从主 `package.json` 移除可选依赖
|
||||||
|
7. 将依赖添加到 `optional-deps/package.json`
|
||||||
|
8. 创建依赖注册表,映射插件到依赖
|
||||||
|
|
||||||
|
#### 阶段三:插件改造
|
||||||
|
9. 创建 `import-helper.ts` 辅助函数
|
||||||
|
10. 逐步改造插件代码,使用 `importOptionalDep` 加载依赖
|
||||||
|
11. 优先改造重型依赖(AWS、阿里云、腾讯云等)
|
||||||
|
|
||||||
|
#### 阶段四:测试与优化
|
||||||
|
12. 端到端测试:验证依赖按需安装和加载
|
||||||
|
13. 性能优化:缓存已加载的模块
|
||||||
|
14. 错误处理:安装失败时的降级策略
|
||||||
|
15. 文档:编写使用说明和迁移指南
|
||||||
|
|
||||||
|
## 关键技术决策
|
||||||
|
|
||||||
|
### 1. 依赖分组策略
|
||||||
|
**选择:按插件分组**
|
||||||
|
- 每个插件声明自己需要的依赖
|
||||||
|
- 优点:职责清晰,易于维护
|
||||||
|
- 缺点:可能有重复依赖(但 pnpm 会去重)
|
||||||
|
|
||||||
|
**备选:按功能分组**
|
||||||
|
- 将依赖按功能分组(如 "aws-deps", "aliyun-deps")
|
||||||
|
- 优点:更细粒度控制
|
||||||
|
- 缺点:增加复杂度
|
||||||
|
|
||||||
|
### 2. 安装触发时机
|
||||||
|
**选择:首次使用时触发**
|
||||||
|
- 在插件的 `execute()` 或 `getClient()` 方法中触发安装
|
||||||
|
- 优点:真正的按需加载
|
||||||
|
- 缺点:首次使用有延迟
|
||||||
|
|
||||||
|
**备选:启动时预检查**
|
||||||
|
- 启动时扫描启用的插件,预安装依赖
|
||||||
|
- 优点:避免运行时延迟
|
||||||
|
- 缺点:可能安装不需要的依赖
|
||||||
|
|
||||||
|
### 3. 依赖路径解析
|
||||||
|
**选择:使用绝对路径 + `file://` 协议**
|
||||||
|
```typescript
|
||||||
|
const modulePath = path.resolve(__dirname, '../../optional-deps/node_modules', packageName);
|
||||||
|
return await import(`file://${modulePath}/index.js`);
|
||||||
|
```
|
||||||
|
|
||||||
|
**原因:**
|
||||||
|
- Node.js ESM 要求明确的 URL 格式
|
||||||
|
- 避免模块解析冲突
|
||||||
|
|
||||||
|
### 4. 并发控制
|
||||||
|
**选择:文件锁 + 内存锁双重保护**
|
||||||
|
- 使用 `proper-lockfile` 锁定 `optional-deps/` 目录
|
||||||
|
- 内存中使用 `Map` 记录正在安装的依赖
|
||||||
|
- 避免多个插件同时触发安装
|
||||||
|
|
||||||
|
### 5. 错误处理
|
||||||
|
**策略:**
|
||||||
|
- 安装失败时记录日志,抛出明确的错误信息
|
||||||
|
- 提供手动安装命令提示:`请运行: cd optional-deps && pnpm install`
|
||||||
|
- 支持降级:某些非核心依赖安装失败时,插件可以部分功能可用
|
||||||
|
|
||||||
|
## 验证方案
|
||||||
|
|
||||||
|
### 单元测试
|
||||||
|
1. 测试 `DependencyManager.isInstalled()` 正确检测依赖状态
|
||||||
|
2. 测试 `DependencyManager.installDependencies()` 成功安装依赖
|
||||||
|
3. 测试并发安装时的锁机制
|
||||||
|
4. 测试从 `optional-deps/node_modules` 加载模块
|
||||||
|
|
||||||
|
### 集成测试
|
||||||
|
1. 清空 `optional-deps/node_modules`
|
||||||
|
2. 启动服务,验证不触发安装
|
||||||
|
3. 调用 AWS 插件,验证触发安装并成功加载
|
||||||
|
4. 再次调用,验证不重复安装
|
||||||
|
5. 验证主 `node_modules` 体积减少
|
||||||
|
|
||||||
|
### 性能测试
|
||||||
|
1. 测量首次安装依赖的耗时
|
||||||
|
2. 测量后续加载的耗时(应该与正常 import 相近)
|
||||||
|
3. 对比改造前后的 `node_modules` 大小
|
||||||
|
|
||||||
|
## 风险与挑战
|
||||||
|
|
||||||
|
### 1. 首次使用延迟
|
||||||
|
**风险:** 用户首次使用插件时需要等待依赖安装(可能几十秒)
|
||||||
|
**缓解:**
|
||||||
|
- 在 UI 上显示安装进度
|
||||||
|
- 提供预安装命令:`pnpm run install-optional-deps`
|
||||||
|
- 文档说明首次使用会有延迟
|
||||||
|
|
||||||
|
### 2. 离线环境
|
||||||
|
**风险:** 离线环境无法下载依赖
|
||||||
|
**缓解:**
|
||||||
|
- 提供完整安装包(包含所有可选依赖)
|
||||||
|
- 支持手动复制 `node_modules`
|
||||||
|
|
||||||
|
### 3. 版本冲突
|
||||||
|
**风险:** 可选依赖与主依赖版本冲突
|
||||||
|
**缓解:**
|
||||||
|
- 使用 `--ignore-workspace` 隔离安装
|
||||||
|
- 定期同步主依赖版本
|
||||||
|
|
||||||
|
### 4. TypeScript 类型
|
||||||
|
**风险:** 动态导入的类型推断
|
||||||
|
**缓解:**
|
||||||
|
- 保留 `@types/*` 在主 `devDependencies`
|
||||||
|
- 使用泛型和类型断言
|
||||||
|
|
||||||
|
## 预期收益
|
||||||
|
|
||||||
|
1. **空间节省:** 主 `node_modules` 体积减少 60-70%(估算)
|
||||||
|
2. **安装速度:** 初始 `pnpm install` 速度提升 3-5 倍
|
||||||
|
3. **用户体验:** 不使用的插件不占用空间,按需加载
|
||||||
|
4. **维护性:** 依赖分组清晰,易于管理
|
||||||
|
|
||||||
|
## 后续优化
|
||||||
|
|
||||||
|
1. **依赖预热:** 在后台预安装常用插件依赖
|
||||||
|
2. **依赖缓存:** 支持从 CDN 或本地缓存安装
|
||||||
|
3. **依赖更新:** 提供命令批量更新可选依赖
|
||||||
|
4. **插件市场:** 支持从远程下载插件及其依赖配置
|
||||||
|
|
||||||
|
## 附录:依赖分类清单
|
||||||
|
|
||||||
|
### 可选依赖(迁移到 optional-deps/package.json)
|
||||||
|
|
||||||
|
**AWS 相关(plugin-aws, plugin-aws-cn):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@aws-sdk/client-acm": "^3.964.0",
|
||||||
|
"@aws-sdk/client-cloudfront": "^3.964.0",
|
||||||
|
"@aws-sdk/client-iam": "^3.964.0",
|
||||||
|
"@aws-sdk/client-route-53": "^3.964.0",
|
||||||
|
"@aws-sdk/client-s3": "^3.964.0",
|
||||||
|
"@aws-sdk/client-sts": "^3.990.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**阿里云相关(plugin-aliyun, plugin-lib/aliyun):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@alicloud/fc20230330": "^4.1.7",
|
||||||
|
"@alicloud/openapi-client": "^0.4.12",
|
||||||
|
"@alicloud/openapi-util": "^0.3.2",
|
||||||
|
"@alicloud/pop-core": "^1.7.10",
|
||||||
|
"@alicloud/sts-sdk": "^1.0.2",
|
||||||
|
"@alicloud/tea-typescript": "^1.8.0",
|
||||||
|
"@alicloud/tea-util": "^1.4.10",
|
||||||
|
"ali-oss": "^6.21.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**腾讯云相关(plugin-tencent, plugin-lib/tencent):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tencentcloud-sdk-nodejs": "^4.1.112",
|
||||||
|
"cos-nodejs-sdk-v5": "^2.14.6"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**华为云相关(plugin-huawei):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@huaweicloud/huaweicloud-sdk-cdn": "3.1.185",
|
||||||
|
"@huaweicloud/huaweicloud-sdk-core": "3.1.185",
|
||||||
|
"@huaweicloud/huaweicloud-sdk-elb": "3.1.185",
|
||||||
|
"@huaweicloud/huaweicloud-sdk-iam": "3.1.185",
|
||||||
|
"esdk-obs-nodejs": "^3.25.6"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Azure 相关(plugin-azure):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@azure/arm-dns": "^5.1.0",
|
||||||
|
"@azure/identity": "^4.13.1"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Google Cloud 相关(plugin-google, plugin-cert/google):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@google-cloud/dns": "^5.3.1",
|
||||||
|
"@google-cloud/publicca": "^1.3.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**火山引擎相关(plugin-volcengine):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@volcengine/openapi": "^1.28.1",
|
||||||
|
"@volcengine/tos-sdk": "^2.9.1"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**SSH/网络相关(plugin-host, plugin-lib/ssh):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ssh2": "^1.17.0",
|
||||||
|
"socks": "^2.8.3",
|
||||||
|
"socks-proxy-agent": "^8.0.4",
|
||||||
|
"basic-ftp": "^5.0.5"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**其他存储/传输(plugin-qiniu, plugin-lib/qiniu):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"qiniu": "^7.12.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**邮件通知(plugin-notification/email):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"nodemailer": "^6.9.16"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 主依赖(保留在主 package.json)
|
||||||
|
|
||||||
|
**框架核心:**
|
||||||
|
- `@midwayjs/*` 系列
|
||||||
|
- `@koa/cors`
|
||||||
|
- `typeorm`, `better-sqlite3`, `mysql2`, `pg`
|
||||||
|
|
||||||
|
**项目内部包:**
|
||||||
|
- `@certd/*` 系列
|
||||||
|
|
||||||
|
**通用工具:**
|
||||||
|
- `axios`, `lodash-es`, `dayjs`, `js-yaml`
|
||||||
|
- `crypto-js`, `jsonwebtoken`, `bcryptjs`
|
||||||
|
- `reflect-metadata`, `uuid`, `nanoid`
|
||||||
|
- 等等
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
本方案通过引入独立的可选依赖管理机制,实现了插件依赖的按需下载和加载。核心思路是:
|
||||||
|
|
||||||
|
1. **隔离管理:** 在 `optional-deps/` 目录下维护独立的 `package.json` 和 `node_modules`
|
||||||
|
2. **动态安装:** 通过 `DependencyManager` 在首次使用时触发 `pnpm install`
|
||||||
|
3. **路径加载:** 使用绝对路径从独立目录加载依赖模块
|
||||||
|
4. **最小改动:** 通过辅助函数 `importOptionalDep` 简化插件代码改造
|
||||||
|
|
||||||
|
该方案可以显著减少主 `node_modules` 体积,提升初始安装速度,同时保持现有架构的兼容性和可维护性。
|
||||||
@@ -1,71 +1,210 @@
|
|||||||
# Certd 开发 Agent 上下文
|
# Certd 开发 Agent 上下文
|
||||||
|
|
||||||
这个文件是给在本仓库工作的开发 agent 看的常驻项目说明。进入仓库后先读本文,再按任务读取对应导航或规则文件,避免每次重新全量扫描项目。
|
进入仓库后先读本文。本文同时包含常驻规则、仓库地图、常用入口和验证命令;不要依赖分散规则文件。
|
||||||
|
|
||||||
仓库代码导航、目录地图、常用入口和参考文件见 `.codex/repo-map.md`。更细的开发规则拆在 `.codex/agent-rules/` 下;本文只保留最高优先级的规则、架构边界和工作方式。
|
|
||||||
|
|
||||||
## 项目定位
|
## 项目定位
|
||||||
|
|
||||||
Certd 是支持私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web 管理台和后端服务,用于证书申请、续期、部署、监控、通知和开放 API 集成。
|
Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web 管理台和后端服务,用于证书申请、续期、部署、监控、通知和开放 API 集成。
|
||||||
|
|
||||||
核心产品模型是“证书流水线”:
|
核心模型是“证书流水线”:
|
||||||
|
|
||||||
- 通过 ACME 申请证书
|
- 通过 ACME 申请证书。
|
||||||
- 使用 DNS-01、HTTP-01、CNAME 代理或服务商集成完成域名验证
|
- 完成 DNS-01、HTTP-01、CNAME 代理或服务商验证。
|
||||||
- 将证书转换或导出为 pem、pfx、der、jks、p7b 等格式
|
- 导出 pem、pfx、der、jks、p7b 等证书格式。
|
||||||
- 部署证书到主机、Nginx、Kubernetes、CDN、云厂商、面板等目标
|
- 部署到主机、Nginx、Kubernetes、CDN、云厂商、面板等目标。
|
||||||
- 通知用户,并监控站点证书过期时间
|
- 通知用户,并监控站点证书过期时间。
|
||||||
|
|
||||||
系统会保存证书、云厂商凭据、SSH 信息、API Key 等敏感数据,始终按私有化/本地部署产品处理,避免泄露本地数据和配置。
|
系统会保存证书、云厂商凭据、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、单一职责等通用代码风格
|
|
||||||
|
|
||||||
## 仓库边界
|
## 仓库边界
|
||||||
|
|
||||||
这是一个 pnpm + lerna 的 monorepo。核心定位:
|
- 根仓库是 pnpm + lerna monorepo;不要引入 npm/yarn lockfile。
|
||||||
|
- `packages/pro/` 是独立 Git 工作区;修改商业版代码后,必须在 `packages/pro` 内单独执行 `git status` / `git diff`。
|
||||||
- `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` 只是占位脚本。
|
|
||||||
- 不要把 `packages/ui/certd-server/data/`、`logs/`、生成的 metadata/dist 等运行时或构建产物纳入改动,除非任务明确要求。
|
- 不要把 `packages/ui/certd-server/data/`、`logs/`、生成的 metadata/dist 等运行时或构建产物纳入改动,除非任务明确要求。
|
||||||
- 做数据库结构变更时,添加或更新迁移脚本,不要依赖 TypeORM 自动同步。
|
- 例外:分析插件动态依赖时,可以只读查看后端数据目录下的 `./data/.runtime-deps`;该目录用于 runtime-deps 动态安装第三方 SDK,不应纳入提交。
|
||||||
- 做插件相关任务时,先读取对应 `.trae/skills/<skill>/SKILL.md`,再进入具体实现。
|
|
||||||
- 后端 service 拼接可选 `projectId` 查询条件时,不要直接写 `{ userId, projectId }`;应使用 `BaseService.buildUserProjectQuery(userId, projectId)`,只有 `projectId != null` 时才加入查询条件。
|
|
||||||
|
|
||||||
## 工作方式
|
核心包:
|
||||||
|
|
||||||
- 先读本文;需要代码导航、目录入口、参考文件或验证命令时读 `.codex/repo-map.md`。
|
- `packages/ui/certd-server`:后端服务。
|
||||||
- 任务涉及后端、前端、插件、测试或代码风格时,先读取 `.codex/agent-rules/` 下对应规则文件,再查看具体代码。
|
- `packages/ui/certd-client`:前端 Web 管理台。
|
||||||
- 在 PowerShell 中读取中文、Markdown、locale、文档类文件时,显式使用 `Get-Content -Encoding utf8`;如果仍乱码,再执行 `[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()` 后重试。
|
- `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 导出结果。
|
||||||
|
|
||||||
|
## 常用验证
|
||||||
|
|
||||||
|
- 后端聚焦单测:`corepack pnpm --dir packages\ui\certd-server test:unit`。
|
||||||
|
- 后端完整测试:`corepack pnpm --dir packages\ui\certd-server test`。
|
||||||
|
- 前端构建:`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>`。
|
||||||
|
- 后端改动文件 lint fix:`corepack pnpm --dir packages\ui\certd-server run lint`。
|
||||||
|
- 其他package lint fix:`corepack pnpm --dir packages\xxx\xxxx 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'`。
|
||||||
|
- 不要主动运行 `pnpm install`;缺依赖、TTY、网络导致安装或测试失败时,停止尝试并说明环境问题。
|
||||||
|
- 优先沿用现有模块、插件、service、页面模式;不要为形式上的复用制造过度抽象。
|
||||||
|
- 代码可读性优先于短写法。复杂条件、三元表达式、链式调用、内联对象和多层 helper 调用要拆成命名清晰的中间变量或小方法。
|
||||||
|
- 方法调用链不要直接塞进另一个方法参数;先用有意义的局部变量承接返回值,再传入下一步。
|
||||||
|
- 注释优先使用中文,尤其是业务规则、兼容逻辑、协议细节和隐藏风险;文件已有英文风格或引用外部术语时可保持一致。
|
||||||
|
- 遵守 DRY 和单一职责;第三次出现的业务规则、字段转换、权限判断、Repository 选择、事务传播、金额计算等逻辑,应优先抽成合适 helper 或 service 方法。
|
||||||
|
|
||||||
|
## 后端规则
|
||||||
|
|
||||||
|
- 后端主包是 `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/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)`。
|
||||||
|
- 拼接可选 `projectId` 查询条件时,用 `BaseService.buildUserProjectQuery(userId, projectId)`;不要直接写 `{ userId, projectId }`。
|
||||||
|
- `ctx` 类型复用 `BaseService` 导出的 `ServiceContext`。
|
||||||
|
- 新增 service 方法避免与 `BaseService` 方法签名冲突,例如不要用 `delete(id)` 覆盖 `delete(ids, where?)`;改用 `deleteById` 等具体名称。
|
||||||
|
|
||||||
|
### 后端地图
|
||||||
|
|
||||||
|
- `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`。
|
- 做前端任务时,先定位 `packages/ui/certd-client/src/views/certd` 下的页面,再找对应 `src/api`。
|
||||||
- 做服务商、DNS、部署、通知相关任务时,先看 `packages/ui/certd-server/src/plugins`,再看 `packages/plugins/plugin-lib` 里的共享辅助能力。
|
- 不要运行前端 `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>`。
|
||||||
- 实现新功能或修复行为缺陷前,优先补对应单元测试并确认红灯,再实现代码并跑聚焦验证。确实不适合先写测试时,在回复中说明原因和替代验证方式。
|
- 列表管理、后台管理、记录查询、CRUD 表格页面优先使用 Fast Crud;开发或重构前读 `.trae/skills/fast-crud-page-dev/SKILL.md`。
|
||||||
- 后补单元测试时,先按正确行为写预期;如果红灯需要修改既有实现,先向用户确认这是 bug 还是既有需求,避免未经确认改变行为。
|
- 只有轻量只读展示、强交互自定义界面或既有页面模式明显不适合 Fast Crud 时,才手写 `a-table` / 自定义列表,并在回复中说明。
|
||||||
|
- 内嵌 Fast Crud 时,外层必须有稳定高度或完整 `flex: 1; min-height: 0` 链路。
|
||||||
|
- 后台管理列表展示或筛选用户字段时,优先参考 `packages/ui/certd-client/src/views/sys/suite/user-suite/crud.tsx` 的 `userId` 字段模式,用 `table-select` + `/sys/authority/user/getSimpleUserByIds` 字典回显和搜索。
|
||||||
|
- 对话框里只做确认可用 `Modal.confirm`;有字段输入、表单校验或提交字段时,必须用 `useFormDialog` / `openFormDialog`。
|
||||||
|
|
||||||
|
### 前端地图
|
||||||
|
|
||||||
|
- `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`。
|
||||||
|
- ACME / EAB:公共 EAB 可能只能创建一次账号;跨用户复用公共 EAB 时,应保存并复用同一个 ACME account private key。
|
||||||
|
- `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`,不要复制第二份。
|
||||||
|
|
||||||
|
### 流水线与插件地图
|
||||||
|
|
||||||
|
- `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`。
|
||||||
|
|
||||||
|
## 测试与验证
|
||||||
|
|
||||||
|
- 实现新功能或修复行为缺陷前,优先补单元测试并先确认红灯,再实现并跑聚焦验证。
|
||||||
|
- 确实不适合先写测试时,在回复中说明原因和替代验证方式。
|
||||||
|
- 后补单元测试时,按正确行为写预期;若红灯需要修改既有实现,先向用户确认这是 bug 还是既有需求,避免未经确认改变行为。
|
||||||
|
- 后端纯单测放在 `src/**/*.test.ts`,尽量与被测文件相邻;`test:unit` 只跑这些文件,构建/打包应排除 `*.test.ts`。
|
||||||
|
- 单测需要 mock ESM 静态 import 时,优先使用 `esmock`,不要为了测试改业务代码结构。
|
||||||
|
- 各包 `test:unit` 脚本应显式设置 `NODE_ENV=unittest`。
|
||||||
|
- 单包单测优先用 `corepack pnpm --dir <包目录> test:unit`,例如 `corepack pnpm --dir packages\ui\certd-server test:unit`。
|
||||||
- 优先对改动包运行聚焦测试或格式化/ESLint;只有跨包影响明显时再考虑更大范围构建。
|
- 优先对改动包运行聚焦测试或格式化/ESLint;只有跨包影响明显时再考虑更大范围构建。
|
||||||
|
|
||||||
## 架构边界
|
|
||||||
|
|
||||||
插件是核心能力,不是边缘功能。新增服务商、DNS 验证、证书部署、通知方式等能力,通常应该放在插件包里,或放在 `packages/ui/certd-server/src/plugins/<plugin-name>/` 下。
|
|
||||||
|
|
||||||
修改证书申请、验证、部署或通知行为时,先判断改动属于 ACME client、pipeline 核心抽象、后端 module/service/entity/controller、具体插件实现,还是前端 view/form/schema。
|
|
||||||
|
|
||||||
如果只是某个服务商或部署目标的问题,不要轻易修改共享 pipeline/core 行为,除非确实是可复用的公共能力。
|
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ https://certd.handfree.work/
|
|||||||
|
|
||||||
|
|
||||||
> [50元专业版优惠券限时领取](https://app.handfree.work/subject/#/app/certd/product) https://app.handfree.work/subject/#/app/certd/product
|
> [50元专业版优惠券限时领取](https://app.handfree.work/subject/#/app/certd/product) https://app.handfree.work/subject/#/app/certd/product
|
||||||
> handfree.work是Certd官方激活码购买平台
|
> app.handfree.work是Certd官方激活码购买平台
|
||||||
|
|
||||||
|
|
||||||
专业版、商业版特权对比
|
专业版、商业版特权对比
|
||||||
|
|||||||
@@ -3,6 +3,16 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [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)
|
## [1.41.3](https://github.com/certd/certd/compare/v1.41.2...v1.41.3) (2026-06-11)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ Certd 是一款开源、免费、全自动申请和部署更新SSL证书的工
|
|||||||
关键字:证书自动申请、证书自动更新、证书自动续期、证书自动续签、证书管理工具
|
关键字:证书自动申请、证书自动更新、证书自动续期、证书自动续签、证书管理工具
|
||||||
|
|
||||||
|
|
||||||
| 官方开源地址: | |
|
| |官方开源地址: |
|
||||||
| ---- | ---- |
|
| ---- | ---- |
|
||||||
| [Github](https://github.com/certd/certd)|  |
|
| [Github](https://github.com/certd/certd)|  |
|
||||||
| [Gitee](https://gitee.com/certd/certd) |  |
|
| [Gitee](https://gitee.com/certd/certd) |  |
|
||||||
|
|||||||
+1
-2
@@ -15,8 +15,7 @@
|
|||||||
"vitepress-plugin-lightbox": "^1.0.2"
|
"vitepress-plugin-lightbox": "^1.0.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "lerna bootstrap --hoist",
|
"start": "cd ./packages/ui/certd-server && pnpm start",
|
||||||
"start:server": "cd ./packages/ui/certd-server && pnpm start",
|
|
||||||
"devb": "lerna run dev-build",
|
"devb": "lerna run dev-build",
|
||||||
"i-all": "lerna link && lerna exec npm install ",
|
"i-all": "lerna link && lerna exec npm install ",
|
||||||
"publish": "pnpm run prepublishOnly2 && lerna publish --force-publish=pro/plus-core --conventional-commits && pnpm run afterpublishOnly ",
|
"publish": "pnpm run prepublishOnly2 && lerna publish --force-publish=pro/plus-core --conventional-commits && pnpm run afterpublishOnly ",
|
||||||
|
|||||||
@@ -59,7 +59,8 @@
|
|||||||
"before-test:unit": "node -e \"const fs=require('fs');fs.rmSync('dist-test',{recursive:true,force:true});fs.rmSync('tsconfig.test.tsbuildinfo',{force:true});\"",
|
"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 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\"",
|
||||||
"pub": "npm publish",
|
"pub": "npm publish",
|
||||||
"compile": "tsc --skipLibCheck --watch"
|
"compile": "tsc --skipLibCheck --watch",
|
||||||
|
"format": "prettier --write src"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -76,5 +77,5 @@
|
|||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/publishlab/node-acme-client/issues"
|
"url": "https://github.com/publishlab/node-acme-client/issues"
|
||||||
},
|
},
|
||||||
"gitHead": "6cbd62977731a3b72c42b5f88c49500631da0a46"
|
"gitHead": "bc731e4fb119787930e816a7d57c808b1b5cd66a"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,9 @@
|
|||||||
"test": "mocha --loader=ts-node/esm",
|
"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\"",
|
"test:unit": "cross-env NODE_ENV=unittest mocha --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
|
||||||
"pub": "npm publish",
|
"pub": "npm publish",
|
||||||
"compile": "tsc --skipLibCheck --watch"
|
"compile": "tsc --skipLibCheck --watch",
|
||||||
|
"format": "prettier --write src",
|
||||||
|
"lint": "eslint --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"async-lock": "^1.4.1",
|
"async-lock": "^1.4.1",
|
||||||
@@ -52,5 +54,5 @@
|
|||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
"typescript": "^5.4.2"
|
"typescript": "^5.4.2"
|
||||||
},
|
},
|
||||||
"gitHead": "6cbd62977731a3b72c42b5f88c49500631da0a46"
|
"gitHead": "bc731e4fb119787930e816a7d57c808b1b5cd66a"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,9 @@
|
|||||||
"test": "mocha --loader=ts-node/esm",
|
"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 --no-config --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
|
||||||
"pub": "npm publish",
|
"pub": "npm publish",
|
||||||
"compile": "tsc --skipLibCheck --watch"
|
"compile": "tsc --skipLibCheck --watch",
|
||||||
|
"format": "prettier --write src",
|
||||||
|
"lint": "eslint --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@certd/basic": "^1.41.4",
|
"@certd/basic": "^1.41.4",
|
||||||
@@ -49,5 +51,5 @@
|
|||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
"typescript": "^5.4.2"
|
"typescript": "^5.4.2"
|
||||||
},
|
},
|
||||||
"gitHead": "6cbd62977731a3b72c42b5f88c49500631da0a46"
|
"gitHead": "bc731e4fb119787930e816a7d57c808b1b5cd66a"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { FormItemProps } from "../dt/index.js";
|
|||||||
import { HttpClient, ILogger, utils } from "@certd/basic";
|
import { HttpClient, ILogger, utils } from "@certd/basic";
|
||||||
import * as _ from "lodash-es";
|
import * as _ from "lodash-es";
|
||||||
import { PluginRequestHandleReq } from "../plugin/index.js";
|
import { PluginRequestHandleReq } from "../plugin/index.js";
|
||||||
|
import { IRuntimeDepsService, IServiceGetter } from "../service/index.js";
|
||||||
|
|
||||||
// export type AccessRequestHandleReqInput<T = any> = {
|
// export type AccessRequestHandleReqInput<T = any> = {
|
||||||
// id?: number;
|
// id?: number;
|
||||||
@@ -20,6 +21,8 @@ export type AccessInputDefine = FormItemProps & {
|
|||||||
export type AccessDefine = Registrable & {
|
export type AccessDefine = Registrable & {
|
||||||
icon?: string;
|
icon?: string;
|
||||||
subtype?: string;
|
subtype?: string;
|
||||||
|
dependPlugins?: Record<string, string>;
|
||||||
|
dependPackages?: Record<string, string>;
|
||||||
input?: {
|
input?: {
|
||||||
[key: string]: AccessInputDefine;
|
[key: string]: AccessInputDefine;
|
||||||
};
|
};
|
||||||
@@ -39,13 +42,29 @@ export type AccessContext = {
|
|||||||
logger: ILogger;
|
logger: ILogger;
|
||||||
utils: typeof utils;
|
utils: typeof utils;
|
||||||
accessService: IAccessService;
|
accessService: IAccessService;
|
||||||
|
serviceGetter?: IServiceGetter;
|
||||||
|
define?: AccessDefine;
|
||||||
};
|
};
|
||||||
|
|
||||||
export abstract class BaseAccess implements IAccess {
|
export abstract class BaseAccess implements IAccess {
|
||||||
ctx!: AccessContext;
|
ctx!: AccessContext;
|
||||||
|
runtimeDepsService?: IRuntimeDepsService;
|
||||||
|
|
||||||
setCtx(ctx: AccessContext) {
|
async importRuntime(specifier: string) {
|
||||||
|
if (!this.runtimeDepsService) {
|
||||||
|
return await import(specifier);
|
||||||
|
}
|
||||||
|
return await this.runtimeDepsService.importRuntime(specifier, this.ctx.logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setCtx(ctx: AccessContext) {
|
||||||
this.ctx = ctx;
|
this.ctx = ctx;
|
||||||
|
if (!this.runtimeDepsService && this.ctx.serviceGetter) {
|
||||||
|
this.runtimeDepsService = await this.ctx.serviceGetter.get("runtimeDepsService");
|
||||||
|
}
|
||||||
|
if (this.runtimeDepsService && this.ctx.define?.name) {
|
||||||
|
await this.runtimeDepsService.ensureRuntimeDependencies({ pluginKeys: `access:${this.ctx.define.name}`, logger: this.ctx.logger });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async onRequest(req: AccessRequestHandleReq) {
|
async onRequest(req: AccessRequestHandleReq) {
|
||||||
|
|||||||
@@ -67,7 +67,9 @@ export async function newAccess(type: string, input: any, accessService: IAccess
|
|||||||
accessService,
|
accessService,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
access.setCtx(ctx);
|
ctx.define = ctx.define || register.define;
|
||||||
|
access.runtimeDepsService = (accessService as any).runtimeDepsService;
|
||||||
|
await access.setCtx(ctx);
|
||||||
access._type = type;
|
access._type = type;
|
||||||
return access;
|
return access;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -387,7 +387,7 @@ export class Executor {
|
|||||||
}),
|
}),
|
||||||
serviceGetter: this.options.serviceGetter,
|
serviceGetter: this.options.serviceGetter,
|
||||||
};
|
};
|
||||||
instance.setCtx(taskCtx);
|
await instance.setCtx(taskCtx);
|
||||||
|
|
||||||
await instance.onInstance();
|
await instance.onInstance();
|
||||||
const result = await instance.execute();
|
const result = await instance.execute();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Registrable } from "../registry/index.js";
|
|||||||
import { FormItemProps, HistoryResult, Pipeline } from "../dt/index.js";
|
import { FormItemProps, HistoryResult, Pipeline } from "../dt/index.js";
|
||||||
import { HttpClient, ILogger, utils } from "@certd/basic";
|
import { HttpClient, ILogger, utils } from "@certd/basic";
|
||||||
import * as _ from "lodash-es";
|
import * as _ from "lodash-es";
|
||||||
import { IEmailService } from "../service/index.js";
|
import { IEmailService, IRuntimeDepsService, IServiceGetter } from "../service/index.js";
|
||||||
|
|
||||||
export type NotificationBody = {
|
export type NotificationBody = {
|
||||||
userId?: number;
|
userId?: number;
|
||||||
@@ -39,6 +39,8 @@ export type NotificationInputDefine = FormItemProps & {
|
|||||||
};
|
};
|
||||||
export type NotificationDefine = Registrable & {
|
export type NotificationDefine = Registrable & {
|
||||||
needPlus?: boolean;
|
needPlus?: boolean;
|
||||||
|
dependPlugins?: Record<string, string>;
|
||||||
|
dependPackages?: Record<string, string>;
|
||||||
input?: {
|
input?: {
|
||||||
[key: string]: NotificationInputDefine;
|
[key: string]: NotificationInputDefine;
|
||||||
};
|
};
|
||||||
@@ -78,6 +80,8 @@ export type NotificationContext = {
|
|||||||
logger: ILogger;
|
logger: ILogger;
|
||||||
utils: typeof utils;
|
utils: typeof utils;
|
||||||
emailService: IEmailService;
|
emailService: IEmailService;
|
||||||
|
serviceGetter?: IServiceGetter;
|
||||||
|
define?: NotificationDefine;
|
||||||
};
|
};
|
||||||
|
|
||||||
export abstract class BaseNotification implements INotification {
|
export abstract class BaseNotification implements INotification {
|
||||||
@@ -85,6 +89,14 @@ export abstract class BaseNotification implements INotification {
|
|||||||
ctx!: NotificationContext;
|
ctx!: NotificationContext;
|
||||||
http!: HttpClient;
|
http!: HttpClient;
|
||||||
logger!: ILogger;
|
logger!: ILogger;
|
||||||
|
runtimeDepsService?: IRuntimeDepsService;
|
||||||
|
|
||||||
|
async importRuntime(specifier: string) {
|
||||||
|
if (!this.runtimeDepsService) {
|
||||||
|
return await import(specifier);
|
||||||
|
}
|
||||||
|
return await this.runtimeDepsService.importRuntime(specifier, this.logger);
|
||||||
|
}
|
||||||
|
|
||||||
async doSend(body: NotificationBody) {
|
async doSend(body: NotificationBody) {
|
||||||
return await this.send(body);
|
return await this.send(body);
|
||||||
@@ -93,10 +105,16 @@ export abstract class BaseNotification implements INotification {
|
|||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
async onInstance() {}
|
async onInstance() {}
|
||||||
setCtx(ctx: NotificationContext) {
|
async setCtx(ctx: NotificationContext) {
|
||||||
this.ctx = ctx;
|
this.ctx = ctx;
|
||||||
this.http = ctx.http;
|
this.http = ctx.http;
|
||||||
this.logger = ctx.logger;
|
this.logger = ctx.logger;
|
||||||
|
if (!this.runtimeDepsService && this.ctx.serviceGetter) {
|
||||||
|
this.runtimeDepsService = await this.ctx.serviceGetter.get("runtimeDepsService");
|
||||||
|
}
|
||||||
|
if (this.runtimeDepsService && this.ctx.define?.name) {
|
||||||
|
await this.runtimeDepsService.ensureRuntimeDependencies({ pluginKeys: `notification:${this.ctx.define.name}`, logger: this.logger });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setDefine = (define: NotificationDefine) => {
|
setDefine = (define: NotificationDefine) => {
|
||||||
this.define = define;
|
this.define = define;
|
||||||
|
|||||||
@@ -61,7 +61,8 @@ export async function newNotification(type: string, input: any, ctx: Notificatio
|
|||||||
throw new Error("ctx is required");
|
throw new Error("ctx is required");
|
||||||
}
|
}
|
||||||
plugin.setDefine(register.define);
|
plugin.setDefine(register.define);
|
||||||
plugin.setCtx(ctx);
|
ctx.define = ctx.define || register.define;
|
||||||
|
await plugin.setCtx(ctx);
|
||||||
await plugin.onInstance();
|
await plugin.onInstance();
|
||||||
return plugin;
|
return plugin;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { INotificationService } from "../notification/index.js";
|
|||||||
import { Registrable } from "../registry/index.js";
|
import { Registrable } from "../registry/index.js";
|
||||||
import { IPluginConfigService } from "../service/config.js";
|
import { IPluginConfigService } from "../service/config.js";
|
||||||
import { TaskEmitter } from "../service/emit.js";
|
import { TaskEmitter } from "../service/emit.js";
|
||||||
import { ICnameProxyService, IEmailService, IServiceGetter, IUrlService } from "../service/index.js";
|
import { ICnameProxyService, IEmailService, IRuntimeDepsService, IServiceGetter, IUrlService } from "../service/index.js";
|
||||||
|
|
||||||
export type PluginRequestHandleReq<T = any> = {
|
export type PluginRequestHandleReq<T = any> = {
|
||||||
typeName: string;
|
typeName: string;
|
||||||
@@ -46,6 +46,8 @@ export type PluginDefine = Registrable & {
|
|||||||
default?: any;
|
default?: any;
|
||||||
group?: string;
|
group?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
|
dependPlugins?: Record<string, string>;
|
||||||
|
dependPackages?: Record<string, string>;
|
||||||
input?: {
|
input?: {
|
||||||
[key: string]: TaskInputDefine;
|
[key: string]: TaskInputDefine;
|
||||||
};
|
};
|
||||||
@@ -73,6 +75,8 @@ export type ITaskPlugin = {
|
|||||||
onInstance(): Promise<void>;
|
onInstance(): Promise<void>;
|
||||||
execute(): Promise<void | string>;
|
execute(): Promise<void | string>;
|
||||||
onRequest(req: PluginRequestHandleReq<any>): Promise<any>;
|
onRequest(req: PluginRequestHandleReq<any>): Promise<any>;
|
||||||
|
setCtx(ctx: TaskInstanceContext): Promise<void>;
|
||||||
|
importRuntime?(specifier: string): Promise<any>;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -146,6 +150,14 @@ export abstract class AbstractTaskPlugin implements ITaskPlugin {
|
|||||||
logger!: ILogger;
|
logger!: ILogger;
|
||||||
http!: HttpClient;
|
http!: HttpClient;
|
||||||
accessService!: IAccessService;
|
accessService!: IAccessService;
|
||||||
|
runtimeDepsService?: IRuntimeDepsService;
|
||||||
|
|
||||||
|
async importRuntime(specifier: string) {
|
||||||
|
if (!this.runtimeDepsService) {
|
||||||
|
return await import(specifier);
|
||||||
|
}
|
||||||
|
return await this.runtimeDepsService.importRuntime(specifier, this.logger);
|
||||||
|
}
|
||||||
|
|
||||||
clearLastStatus() {
|
clearLastStatus() {
|
||||||
this._result.clearLastStatus = true;
|
this._result.clearLastStatus = true;
|
||||||
@@ -161,11 +173,17 @@ export abstract class AbstractTaskPlugin implements ITaskPlugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setCtx(ctx: TaskInstanceContext) {
|
async setCtx(ctx: TaskInstanceContext) {
|
||||||
this.ctx = ctx;
|
this.ctx = ctx;
|
||||||
this.logger = ctx.logger;
|
this.logger = ctx.logger;
|
||||||
this.accessService = ctx.accessService;
|
this.accessService = ctx.accessService;
|
||||||
this.http = ctx.http;
|
this.http = ctx.http;
|
||||||
|
if (!this.runtimeDepsService && this.ctx.serviceGetter) {
|
||||||
|
this.runtimeDepsService = await this.ctx.serviceGetter.get("runtimeDepsService");
|
||||||
|
}
|
||||||
|
if (this.runtimeDepsService && this.ctx.define?.name) {
|
||||||
|
await this.runtimeDepsService.ensureRuntimeDependencies({ pluginKeys: `plugin:${this.ctx.define.name}`, logger: this.logger });
|
||||||
|
}
|
||||||
// 将证书加入secret
|
// 将证书加入secret
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (this.cert && this.cert.crt && this.cert.key) {
|
if (this.cert && this.cert.crt && this.cert.key) {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export class PipelineEmitter {
|
|||||||
}
|
}
|
||||||
off(event: string, listener: PipelineEventListener) {
|
off(event: string, listener: PipelineEventListener) {
|
||||||
if (this.events[event]) {
|
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) {
|
once(event: string, listener: PipelineEventListener) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export * from "./cname.js";
|
|||||||
export * from "./config.js";
|
export * from "./config.js";
|
||||||
export * from "./url.js";
|
export * from "./url.js";
|
||||||
export * from "./emit.js";
|
export * from "./emit.js";
|
||||||
|
export * from "./runtime.js";
|
||||||
export type IServiceGetter = {
|
export type IServiceGetter = {
|
||||||
get: <T>(name: string) => Promise<T>;
|
get: <T>(name: string) => Promise<T>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* 运行时动态导入函数类型
|
||||||
|
*/
|
||||||
|
export type ImportRuntime = (specifier: string, logger?: ILogger) => Promise<any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日志接口
|
||||||
|
*/
|
||||||
|
export type ILogger = {
|
||||||
|
info: (message: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行时依赖服务参数
|
||||||
|
*/
|
||||||
|
export type EnsureRuntimeDepsOptions = {
|
||||||
|
pluginKeys: string | string[];
|
||||||
|
logger?: ILogger;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行时依赖服务接口
|
||||||
|
*/
|
||||||
|
export interface IRuntimeDepsService {
|
||||||
|
ensureRuntimeDependencies(options: EnsureRuntimeDepsOptions): Promise<any>;
|
||||||
|
importRuntime: ImportRuntime;
|
||||||
|
}
|
||||||
@@ -13,7 +13,9 @@
|
|||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
|
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
|
||||||
"pub": "npm publish",
|
"pub": "npm publish",
|
||||||
"compile": "npm run build"
|
"compile": "npm run build",
|
||||||
|
"format": "prettier --write src",
|
||||||
|
"lint": "eslint --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
@@ -28,5 +30,5 @@
|
|||||||
"prettier": "^2.8.8",
|
"prettier": "^2.8.8",
|
||||||
"tslib": "^2.8.1"
|
"tslib": "^2.8.1"
|
||||||
},
|
},
|
||||||
"gitHead": "6cbd62977731a3b72c42b5f88c49500631da0a46"
|
"gitHead": "bc731e4fb119787930e816a7d57c808b1b5cd66a"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,9 @@
|
|||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
|
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
|
||||||
"pub": "npm publish",
|
"pub": "npm publish",
|
||||||
"compile": "npm run build"
|
"compile": "npm run build",
|
||||||
|
"format": "prettier --write src",
|
||||||
|
"lint": "eslint --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^4.0.0"
|
"nanoid": "^4.0.0"
|
||||||
@@ -35,5 +37,5 @@
|
|||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
"typescript": "^5.4.2"
|
"typescript": "^5.4.2"
|
||||||
},
|
},
|
||||||
"gitHead": "6cbd62977731a3b72c42b5f88c49500631da0a46"
|
"gitHead": "bc731e4fb119787930e816a7d57c808b1b5cd66a"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,9 @@
|
|||||||
"dev-build": "npm run build",
|
"dev-build": "npm run build",
|
||||||
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
|
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
|
||||||
"pub": "npm publish",
|
"pub": "npm publish",
|
||||||
"compile": "npm run build"
|
"compile": "npm run build",
|
||||||
|
"format": "prettier --write src",
|
||||||
|
"lint": "eslint --fix"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "Apache",
|
"license": "Apache",
|
||||||
@@ -60,5 +62,5 @@
|
|||||||
"fetch"
|
"fetch"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"gitHead": "6cbd62977731a3b72c42b5f88c49500631da0a46"
|
"gitHead": "bc731e4fb119787930e816a7d57c808b1b5cd66a"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,9 @@
|
|||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
|
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
|
||||||
"pub": "npm publish",
|
"pub": "npm publish",
|
||||||
"compile": "tsc --skipLibCheck --watch"
|
"compile": "tsc --skipLibCheck --watch",
|
||||||
|
"format": "prettier --write src",
|
||||||
|
"lint": "eslint --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@certd/basic": "^1.41.4",
|
"@certd/basic": "^1.41.4",
|
||||||
@@ -36,5 +38,5 @@
|
|||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
"typescript": "^5.4.2"
|
"typescript": "^5.4.2"
|
||||||
},
|
},
|
||||||
"gitHead": "6cbd62977731a3b72c42b5f88c49500631da0a46"
|
"gitHead": "bc731e4fb119787930e816a7d57c808b1b5cd66a"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,22 @@
|
|||||||
{
|
{
|
||||||
"extends": "./node_modules/mwts/",
|
"parser": "@typescript-eslint/parser",
|
||||||
"ignorePatterns": ["node_modules", "dist", "test", "jest.config.js", "typings"],
|
"plugins": [
|
||||||
|
"@typescript-eslint"
|
||||||
|
],
|
||||||
|
"extends": [
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:prettier/recommended",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
"env": {
|
"env": {
|
||||||
"jest": true
|
"mocha": true
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/no-var-requires": "off",
|
||||||
|
"@typescript-eslint/ban-ts-comment": "off",
|
||||||
|
"@typescript-eslint/ban-ts-ignore": "off",
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"@typescript-eslint/no-empty-function": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": "off"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,11 +15,11 @@
|
|||||||
"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 --no-config --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
|
||||||
"test1": "midway-bin test --ts -V -f test/blank.test.ts -t 'hash-check'",
|
"test1": "midway-bin test --ts -V -f test/blank.test.ts -t 'hash-check'",
|
||||||
"cov": "midway-bin cov --ts",
|
"cov": "midway-bin cov --ts",
|
||||||
"lint": "mwts check",
|
|
||||||
"lint:fix": "mwts fix",
|
|
||||||
"prepublish": "npm run build",
|
"prepublish": "npm run build",
|
||||||
"pub": "npm publish",
|
"pub": "npm publish",
|
||||||
"compile": "tsc --skipLibCheck --watch"
|
"compile": "tsc --skipLibCheck --watch",
|
||||||
|
"format": "prettier --write src",
|
||||||
|
"lint": "eslint --fix"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "greper",
|
"author": "greper",
|
||||||
@@ -69,5 +69,5 @@
|
|||||||
"typeorm": "^0.3.11",
|
"typeorm": "^0.3.11",
|
||||||
"typescript": "^5.4.2"
|
"typescript": "^5.4.2"
|
||||||
},
|
},
|
||||||
"gitHead": "6cbd62977731a3b72c42b5f88c49500631da0a46"
|
"gitHead": "bc731e4fb119787930e816a7d57c808b1b5cd66a"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,7 +173,6 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
|
|||||||
if (privateSetting.dnsResultOrder) {
|
if (privateSetting.dnsResultOrder) {
|
||||||
dns.setDefaultResultOrder(privateSetting.dnsResultOrder as any);
|
dns.setDefaultResultOrder(privateSetting.dnsResultOrder as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (privateSetting.pipelineMaxRunningCount) {
|
if (privateSetting.pipelineMaxRunningCount) {
|
||||||
executorQueue.setMaxRunningCount(privateSetting.pipelineMaxRunningCount);
|
executorQueue.setMaxRunningCount(privateSetting.pipelineMaxRunningCount);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,29 @@
|
|||||||
import { IAccessService } from "@certd/pipeline";
|
import { IAccessService, IRuntimeDepsService } from "@certd/pipeline";
|
||||||
|
|
||||||
|
export type AccessRuntimeDepsService = IRuntimeDepsService;
|
||||||
|
|
||||||
export class AccessGetter implements IAccessService {
|
export class AccessGetter implements IAccessService {
|
||||||
userId: number;
|
userId: number;
|
||||||
projectId?: number;
|
projectId?: number;
|
||||||
getter: <T>(id: any, userId?: number, projectId?: number, ignorePermission?: boolean) => Promise<T>;
|
runtimeDepsService?: AccessRuntimeDepsService;
|
||||||
constructor(userId: number, projectId: number, getter: (id: any, userId: number, projectId?: number, ignorePermission?: boolean) => Promise<any>) {
|
getter: <T>(id: any, userId?: number, projectId?: number, ignorePermission?: boolean, runtimeDepsService?: AccessRuntimeDepsService) => Promise<T>;
|
||||||
|
constructor(
|
||||||
|
userId: number,
|
||||||
|
projectId: number,
|
||||||
|
getter: (id: any, userId: number, projectId?: number, ignorePermission?: boolean, runtimeDepsService?: AccessRuntimeDepsService) => Promise<any>,
|
||||||
|
runtimeDepsService?: AccessRuntimeDepsService
|
||||||
|
) {
|
||||||
this.userId = userId;
|
this.userId = userId;
|
||||||
this.projectId = projectId;
|
this.projectId = projectId;
|
||||||
this.getter = getter;
|
this.getter = getter;
|
||||||
|
this.runtimeDepsService = runtimeDepsService;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getById<T = any>(id: any) {
|
async getById<T = any>(id: any) {
|
||||||
return await this.getter<T>(id, this.userId, this.projectId);
|
return await this.getter<T>(id, this.userId, this.projectId, false, this.runtimeDepsService);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCommonById<T = any>(id: any) {
|
async getCommonById<T = any>(id: any) {
|
||||||
return await this.getter<T>(id, 0, null);
|
return await this.getter<T>(id, 0, null, false, this.runtimeDepsService);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
|||||||
import { InjectEntityModel } from "@midwayjs/typeorm";
|
import { InjectEntityModel } from "@midwayjs/typeorm";
|
||||||
import { In, Repository } from "typeorm";
|
import { In, Repository } from "typeorm";
|
||||||
import { AccessGetter, BaseService, PageReq, PermissionException, ValidateException } from "../../../index.js";
|
import { AccessGetter, BaseService, PageReq, PermissionException, ValidateException } from "../../../index.js";
|
||||||
|
import type { AccessRuntimeDepsService } from "./access-getter.js";
|
||||||
import { AccessEntity } from "../entity/access.js";
|
import { AccessEntity } from "../entity/access.js";
|
||||||
import { AccessDefine, accessRegistry, newAccess } from "@certd/pipeline";
|
import { AccessDefine, accessRegistry, newAccess } from "@certd/pipeline";
|
||||||
import { EncryptService } from "./encrypt-service.js";
|
import { EncryptService } from "./encrypt-service.js";
|
||||||
import { logger, utils } from "@certd/basic";
|
import { http, logger, utils } from "@certd/basic";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 授权
|
* 授权
|
||||||
@@ -160,7 +161,7 @@ export class AccessService extends BaseService<AccessEntity> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAccessById(id: any, checkUserId: boolean, userId?: number, projectId?: number): Promise<any> {
|
async getAccessById(id: any, checkUserId: boolean, userId?: number, projectId?: number, runtimeDepsService?: AccessRuntimeDepsService): Promise<any> {
|
||||||
const entity = await this.info(id);
|
const entity = await this.info(id);
|
||||||
if (entity == null) {
|
if (entity == null) {
|
||||||
throw new Error(`该授权配置不存在,请确认是否已被删除:id=${id}`);
|
throw new Error(`该授权配置不存在,请确认是否已被删除:id=${id}`);
|
||||||
@@ -183,12 +184,20 @@ export class AccessService extends BaseService<AccessEntity> {
|
|||||||
id: entity.id,
|
id: entity.id,
|
||||||
...setting,
|
...setting,
|
||||||
};
|
};
|
||||||
const accessGetter = new AccessGetter(userId, projectId, this.getById.bind(this));
|
const getAccessById = this.getById.bind(this);
|
||||||
return await newAccess(entity.type, input, accessGetter);
|
const accessGetter = new AccessGetter(userId, projectId, getAccessById, runtimeDepsService);
|
||||||
|
const accessContext = {
|
||||||
|
logger,
|
||||||
|
http,
|
||||||
|
utils,
|
||||||
|
accessService: accessGetter,
|
||||||
|
} as any;
|
||||||
|
const access = await newAccess(entity.type, input, accessGetter, accessContext);
|
||||||
|
return access;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getById(id: any, userId: number, projectId?: number): Promise<any> {
|
async getById(id: any, userId: number, projectId?: number, _ignorePermission?: boolean, runtimeDepsService?: AccessRuntimeDepsService): Promise<any> {
|
||||||
return await this.getAccessById(id, true, userId, projectId);
|
return await this.getAccessById(id, true, userId, projectId, runtimeDepsService);
|
||||||
}
|
}
|
||||||
|
|
||||||
decryptAccessEntity(entity: AccessEntity): any {
|
decryptAccessEntity(entity: AccessEntity): any {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
accessRegistry,
|
accessRegistry,
|
||||||
FormItemProps,
|
FormItemProps,
|
||||||
IAccessService,
|
IAccessService,
|
||||||
|
IRuntimeDepsService,
|
||||||
IServiceGetter,
|
IServiceGetter,
|
||||||
PluginRequestHandleReq,
|
PluginRequestHandleReq,
|
||||||
Registrable
|
Registrable
|
||||||
@@ -27,6 +28,8 @@ export type AddonInputDefine = FormItemProps & {
|
|||||||
export type AddonDefine = Registrable & {
|
export type AddonDefine = Registrable & {
|
||||||
addonType: string;
|
addonType: string;
|
||||||
needPlus?: boolean;
|
needPlus?: boolean;
|
||||||
|
dependPlugins?: Record<string, string>;
|
||||||
|
dependPackages?: Record<string, string>;
|
||||||
input?: {
|
input?: {
|
||||||
[key: string]: AddonInputDefine;
|
[key: string]: AddonInputDefine;
|
||||||
};
|
};
|
||||||
@@ -64,15 +67,20 @@ export abstract class BaseAddon implements IAddon {
|
|||||||
ctx!: AddonContext;
|
ctx!: AddonContext;
|
||||||
http!: HttpClient;
|
http!: HttpClient;
|
||||||
logger!: ILogger;
|
logger!: ILogger;
|
||||||
|
runtimeDepsService?: IRuntimeDepsService;
|
||||||
|
|
||||||
|
async importRuntime(specifier: string) {
|
||||||
|
if (!this.runtimeDepsService) {
|
||||||
|
return await import(specifier);
|
||||||
|
}
|
||||||
|
return await this.runtimeDepsService.importRuntime(specifier, this.logger);
|
||||||
|
}
|
||||||
|
|
||||||
title!: string;
|
title!: string;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
async onInstance() {}
|
async onInstance() {}
|
||||||
|
|
||||||
|
|
||||||
async getAccess<T = any>(accessId: string | number, isCommon = false) {
|
async getAccess<T = any>(accessId: string | number, isCommon = false) {
|
||||||
if (accessId == null) {
|
if (accessId == null) {
|
||||||
throw new Error("您还没有配置授权");
|
throw new Error("您还没有配置授权");
|
||||||
@@ -106,11 +114,16 @@ export abstract class BaseAddon implements IAddon {
|
|||||||
return res as T;
|
return res as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setCtx(ctx: AddonContext) {
|
||||||
setCtx(ctx: AddonContext) {
|
|
||||||
this.ctx = ctx;
|
this.ctx = ctx;
|
||||||
this.http = ctx.http;
|
this.http = ctx.http;
|
||||||
this.logger = ctx.logger;
|
this.logger = ctx.logger;
|
||||||
|
if (!this.runtimeDepsService && this.ctx.serviceGetter) {
|
||||||
|
this.runtimeDepsService = await this.ctx.serviceGetter.get("runtimeDepsService");
|
||||||
|
}
|
||||||
|
if (this.runtimeDepsService && this.define?.addonType && this.define?.name) {
|
||||||
|
await this.runtimeDepsService.ensureRuntimeDependencies({ pluginKeys: `addon:${this.define.addonType}:${this.define.name}`, logger: this.logger });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setDefine = (define:AddonDefine) => {
|
setDefine = (define:AddonDefine) => {
|
||||||
this.define = define;
|
this.define = define;
|
||||||
|
|||||||
@@ -63,9 +63,7 @@ export async function newAddon(addonType:string,type: string, input: any, ctx: A
|
|||||||
throw new Error("ctx is required");
|
throw new Error("ctx is required");
|
||||||
}
|
}
|
||||||
plugin.setDefine(register.define);
|
plugin.setDefine(register.define);
|
||||||
plugin.setCtx(ctx);
|
await plugin.setCtx(ctx);
|
||||||
await plugin.onInstance();
|
await plugin.onInstance();
|
||||||
return plugin;
|
return plugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,22 @@
|
|||||||
{
|
{
|
||||||
"extends": "./node_modules/mwts/",
|
"parser": "@typescript-eslint/parser",
|
||||||
"ignorePatterns": ["node_modules", "dist", "test", "jest.config.js", "typings"],
|
"plugins": [
|
||||||
|
"@typescript-eslint"
|
||||||
|
],
|
||||||
|
"extends": [
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:prettier/recommended",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
"env": {
|
"env": {
|
||||||
"jest": true
|
"mocha": true
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/no-var-requires": "off",
|
||||||
|
"@typescript-eslint/ban-ts-comment": "off",
|
||||||
|
"@typescript-eslint/ban-ts-ignore": "off",
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"@typescript-eslint/no-empty-function": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": "off"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,9 @@
|
|||||||
"cov": "midway-bin cov --ts",
|
"cov": "midway-bin cov --ts",
|
||||||
"prepublish": "npm run build",
|
"prepublish": "npm run build",
|
||||||
"pub": "npm publish",
|
"pub": "npm publish",
|
||||||
"compile": "npm run build"
|
"compile": "npm run build",
|
||||||
|
"format": "prettier --write src",
|
||||||
|
"lint": "eslint --fix"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "greper",
|
"author": "greper",
|
||||||
@@ -50,5 +52,5 @@
|
|||||||
"typeorm": "^0.3.11",
|
"typeorm": "^0.3.11",
|
||||||
"typescript": "^5.4.2"
|
"typescript": "^5.4.2"
|
||||||
},
|
},
|
||||||
"gitHead": "6cbd62977731a3b72c42b5f88c49500631da0a46"
|
"gitHead": "bc731e4fb119787930e816a7d57c808b1b5cd66a"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,9 @@
|
|||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
|
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
|
||||||
"pub": "npm publish",
|
"pub": "npm publish",
|
||||||
"compile": "tsc --skipLibCheck --watch"
|
"compile": "tsc --skipLibCheck --watch",
|
||||||
|
"format": "prettier --write src",
|
||||||
|
"lint": "eslint --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@certd/acme-client": "^1.41.4",
|
"@certd/acme-client": "^1.41.4",
|
||||||
@@ -41,5 +43,5 @@
|
|||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
"typescript": "^5.4.2"
|
"typescript": "^5.4.2"
|
||||||
},
|
},
|
||||||
"gitHead": "6cbd62977731a3b72c42b5f88c49500631da0a46"
|
"gitHead": "bc731e4fb119787930e816a7d57c808b1b5cd66a"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,9 @@
|
|||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"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 --no-config --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
|
||||||
"pub": "npm publish",
|
"pub": "npm publish",
|
||||||
"compile": "tsc --skipLibCheck --watch"
|
"compile": "tsc --skipLibCheck --watch",
|
||||||
|
"format": "prettier --write src",
|
||||||
|
"lint": "eslint --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alicloud/openapi-client": "^0.4.15",
|
"@alicloud/openapi-client": "^0.4.15",
|
||||||
@@ -61,5 +63,5 @@
|
|||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
"typescript": "^5.4.2"
|
"typescript": "^5.4.2"
|
||||||
},
|
},
|
||||||
"gitHead": "6cbd62977731a3b72c42b5f88c49500631da0a46"
|
"gitHead": "bc731e4fb119787930e816a7d57c808b1b5cd66a"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ export class CertConverter {
|
|||||||
if (!fs.existsSync(dir)) {
|
if (!fs.existsSync(dir)) {
|
||||||
fs.mkdirSync(dir, { recursive: true });
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
}
|
}
|
||||||
await this.exec(`keytool -importkeystore -srckeystore ${p12Path} -srcstoretype PKCS12 -srcstorepass "${jksPassword}" -destkeystore ${jksPath} -deststoretype PKCS12 -deststorepass "${jksPassword}" `);
|
await this.exec(`keytool -importkeystore -srckeystore ${p12Path} -srcstoretype PKCS12 -srcstorepass "${jksPassword}" -destkeystore ${jksPath} -deststoretype JKS -deststorepass "${jksPassword}" `);
|
||||||
fs.unlinkSync(p12Path);
|
fs.unlinkSync(p12Path);
|
||||||
|
|
||||||
const fileBuffer = fs.readFileSync(jksPath);
|
const fileBuffer = fs.readFileSync(jksPath);
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { IAccess, IAccessService, IServiceGetter, PageRes, PageSearch, Registrab
|
|||||||
export type DnsProviderDefine = Registrable & {
|
export type DnsProviderDefine = Registrable & {
|
||||||
accessType: string;
|
accessType: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
|
dependPlugins?: Record<string, string>;
|
||||||
|
dependPackages?: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CreateRecordOptions = {
|
export type CreateRecordOptions = {
|
||||||
@@ -27,6 +29,7 @@ export type DnsProviderContext = {
|
|||||||
domainParser: IDomainParser;
|
domainParser: IDomainParser;
|
||||||
serviceGetter: IServiceGetter;
|
serviceGetter: IServiceGetter;
|
||||||
accessGetter?: IAccessService;
|
accessGetter?: IAccessService;
|
||||||
|
define?: DnsProviderDefine;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DomainRecord = {
|
export type DomainRecord = {
|
||||||
@@ -61,7 +64,7 @@ export interface IDnsProvider<T = any> {
|
|||||||
|
|
||||||
removeRecord(options: RemoveRecordOptions<T>): Promise<void>;
|
removeRecord(options: RemoveRecordOptions<T>): Promise<void>;
|
||||||
|
|
||||||
setCtx(ctx: DnsProviderContext): void;
|
setCtx(ctx: DnsProviderContext): Promise<void>;
|
||||||
|
|
||||||
//中文域名是否需要punycode转码,如果返回True,则使用punycode来添加解析记录,否则使用中文域名添加解析记录
|
//中文域名是否需要punycode转码,如果返回True,则使用punycode来添加解析记录,否则使用中文域名添加解析记录
|
||||||
usePunyCode(): boolean;
|
usePunyCode(): boolean;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { HttpClient, ILogger } from "@certd/basic";
|
import { HttpClient, ILogger } from "@certd/basic";
|
||||||
import { IAccessService, PageRes, PageSearch } from "@certd/pipeline";
|
import { IAccessService, IRuntimeDepsService, PageRes, PageSearch } from "@certd/pipeline";
|
||||||
import punycode from "punycode.js";
|
import punycode from "punycode.js";
|
||||||
import { CreateRecordOptions, DnsProviderContext, DnsProviderDefine, DnsResolveRecord, DomainRecord, IDnsProvider, RemoveRecordOptions } from "./api.js";
|
import { CreateRecordOptions, DnsProviderContext, DnsProviderDefine, DnsResolveRecord, DomainRecord, IDnsProvider, RemoveRecordOptions } from "./api.js";
|
||||||
import { dnsProviderRegistry } from "./registry.js";
|
import { dnsProviderRegistry } from "./registry.js";
|
||||||
@@ -7,6 +7,14 @@ export abstract class AbstractDnsProvider<T = any> implements IDnsProvider<T> {
|
|||||||
ctx!: DnsProviderContext;
|
ctx!: DnsProviderContext;
|
||||||
http!: HttpClient;
|
http!: HttpClient;
|
||||||
logger!: ILogger;
|
logger!: ILogger;
|
||||||
|
runtimeDepsService?: IRuntimeDepsService;
|
||||||
|
|
||||||
|
async importRuntime(specifier: string) {
|
||||||
|
if (!this.runtimeDepsService) {
|
||||||
|
return await import(specifier);
|
||||||
|
}
|
||||||
|
return await this.runtimeDepsService.importRuntime(specifier, this.logger);
|
||||||
|
}
|
||||||
|
|
||||||
usePunyCode(): boolean {
|
usePunyCode(): boolean {
|
||||||
//是否使用punycode来添加解析记录
|
//是否使用punycode来添加解析记录
|
||||||
@@ -30,10 +38,16 @@ export abstract class AbstractDnsProvider<T = any> implements IDnsProvider<T> {
|
|||||||
return punycode.toUnicode(domain);
|
return punycode.toUnicode(domain);
|
||||||
}
|
}
|
||||||
|
|
||||||
setCtx(ctx: DnsProviderContext) {
|
async setCtx(ctx: DnsProviderContext) {
|
||||||
this.ctx = ctx;
|
this.ctx = ctx;
|
||||||
this.logger = ctx.logger;
|
this.logger = ctx.logger;
|
||||||
this.http = ctx.http;
|
this.http = ctx.http;
|
||||||
|
if (!this.runtimeDepsService && this.ctx.serviceGetter) {
|
||||||
|
this.runtimeDepsService = await this.ctx.serviceGetter.get("runtimeDepsService");
|
||||||
|
}
|
||||||
|
if (this.runtimeDepsService && this.ctx.define?.name) {
|
||||||
|
await this.runtimeDepsService.ensureRuntimeDependencies({ pluginKeys: `dnsProvider:${this.ctx.define.name}`, logger: this.logger });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async parseDomain(fullDomain: string) {
|
async parseDomain(fullDomain: string) {
|
||||||
@@ -68,9 +82,10 @@ export async function createDnsProvider(opts: { dnsProviderType: string; context
|
|||||||
const accessGetter: IAccessService = await context.serviceGetter.get("accessService");
|
const accessGetter: IAccessService = await context.serviceGetter.get("accessService");
|
||||||
context.accessGetter = accessGetter;
|
context.accessGetter = accessGetter;
|
||||||
}
|
}
|
||||||
|
context.define = dnsProviderDefine;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const dnsProvider: IDnsProvider = new DnsProviderClass();
|
const dnsProvider: IDnsProvider = new DnsProviderClass();
|
||||||
dnsProvider.setCtx(context);
|
await dnsProvider.setCtx(context);
|
||||||
await dnsProvider.onInstance();
|
await dnsProvider.onInstance();
|
||||||
return dnsProvider;
|
return dnsProvider;
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-30
@@ -1,12 +1,9 @@
|
|||||||
FROM node:22-alpine3.21 AS builder
|
# 根据目标平台选择基础镜像:amd64/arm64 用 trixie-slim,arm/v7 没有 trixie-slim 发布,回退到 alpine
|
||||||
|
FROM --platform=linux/amd64 node:22-trixie-slim AS base-amd64
|
||||||
# RUN apk add build-base
|
FROM --platform=linux/arm64 node:22-trixie-slim AS base-arm64
|
||||||
# RUN wget -O - https://github.com/jemalloc/jemalloc/releases/download/5.3.0/jemalloc-5.3.0.tar.bz2 | tar -xj && \
|
FROM --platform=linux/arm/v7 node:22-alpine AS base-arm-v7
|
||||||
# cd jemalloc-5.3.0 && \
|
|
||||||
# ./configure && \
|
|
||||||
# make && \
|
|
||||||
# make install
|
|
||||||
|
|
||||||
|
FROM base-${TARGETARCH}${TARGETVARIANT:+-}${TARGETVARIANT} AS builder
|
||||||
|
|
||||||
WORKDIR /workspace/
|
WORKDIR /workspace/
|
||||||
COPY . /workspace/
|
COPY . /workspace/
|
||||||
@@ -14,34 +11,44 @@ COPY . /workspace/
|
|||||||
# https://pnpm.io/zh/migration
|
# https://pnpm.io/zh/migration
|
||||||
RUN npm install -g pnpm@10.33.4
|
RUN npm install -g pnpm@10.33.4
|
||||||
|
|
||||||
#RUN cd /workspace/certd-client && pnpm install && npm run build
|
|
||||||
RUN cp /workspace/certd-client/dist/* /workspace/certd-server/public/ -rf
|
RUN cp /workspace/certd-client/dist/* /workspace/certd-server/public/ -rf
|
||||||
RUN cd /workspace/certd-server && pnpm install && npm run build-on-docker
|
RUN cd /workspace/certd-server && pnpm install && npm run build-on-docker
|
||||||
|
|
||||||
# RUN cd /workspace/certd-server && \
|
|
||||||
# pnpm install --ignore-scripts && \
|
|
||||||
# yes | pnpm approve-builds && \
|
|
||||||
# pnpm rebuild && \
|
|
||||||
# npm run build-on-docker
|
|
||||||
|
|
||||||
|
FROM base-${TARGETARCH}${TARGETVARIANT:+-}${TARGETVARIANT}
|
||||||
FROM node:22-alpine3.21
|
|
||||||
EXPOSE 7001
|
EXPOSE 7001
|
||||||
EXPOSE 7002
|
EXPOSE 7002
|
||||||
|
|
||||||
# 安装jemalloc内存分配器,优化内存占用 -- 基本没用,反而更高了
|
# 根据基础镜像发行版选择包管理器
|
||||||
# COPY --from=builder /usr/local/lib/libjemalloc.so.2 /usr/local/lib/
|
# trixie-slim -> apt-get, alpine -> apk
|
||||||
# ENV LD_PRELOAD=/usr/local/lib/libjemalloc.so.2
|
RUN if [ -f /etc/debian_version ]; then \
|
||||||
|
apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates \
|
||||||
|
gnupg \
|
||||||
|
wget \
|
||||||
|
openssl \
|
||||||
|
netcat-openbsd \
|
||||||
|
iputils-ping \
|
||||||
|
dnsutils \
|
||||||
|
iproute2 \
|
||||||
|
&& wget -O - https://packages.adoptium.net/artifactory/api/gpg/key/public | gpg --dearmor | tee /usr/share/keyrings/adoptium.gpg > /dev/null \
|
||||||
|
&& echo "deb [signed-by=/usr/share/keyrings/adoptium.gpg] https://packages.adoptium.net/artifactory/deb bookworm main" | tee /etc/apt/sources.list.d/adoptium.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends temurin-8-jre \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*; \
|
||||||
|
elif [ -f /etc/alpine-release ]; then \
|
||||||
|
apk add --no-cache \
|
||||||
|
openssl \
|
||||||
|
openjdk8-jre; \
|
||||||
|
else \
|
||||||
|
echo "Unsupported base image"; exit 1; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
RUN apk add --no-cache openssl
|
|
||||||
RUN apk add --no-cache openjdk8
|
|
||||||
RUN apk add --no-cache gcompat
|
|
||||||
WORKDIR /app/
|
WORKDIR /app/
|
||||||
COPY --from=builder /workspace/certd-server/ /app/
|
|
||||||
|
|
||||||
COPY ./patch/ssh2/*.js /app/node_modules/.pnpm/node_modules/ssh2/lib/protocol/
|
|
||||||
|
|
||||||
|
ENV TERM=xterm
|
||||||
ENV LEGO_VERSION=4.30.1
|
ENV LEGO_VERSION=4.30.1
|
||||||
ENV LEGO_DOWNLOAD_DIR=/app/tools/lego
|
ENV LEGO_DOWNLOAD_DIR=/app/tools/lego
|
||||||
|
|
||||||
@@ -57,14 +64,14 @@ RUN ARCH=$(uname -m) && \
|
|||||||
elif [ "$ARCH" = "aarch64" ]; then \
|
elif [ "$ARCH" = "aarch64" ]; then \
|
||||||
wget -O $LEGO_DOWNLOAD_DIR/lego_v${LEGO_VERSION}_linux_arm64.tar.gz https://github.com/go-acme/lego/releases/download/v${LEGO_VERSION}/lego_v${LEGO_VERSION}_linux_arm64.tar.gz; \
|
wget -O $LEGO_DOWNLOAD_DIR/lego_v${LEGO_VERSION}_linux_arm64.tar.gz https://github.com/go-acme/lego/releases/download/v${LEGO_VERSION}/lego_v${LEGO_VERSION}_linux_arm64.tar.gz; \
|
||||||
else \
|
else \
|
||||||
|
# armv7 不支持lego 不要再尝试了
|
||||||
echo "Unsupported architecture: $ARCH"; \
|
echo "Unsupported architecture: $ARCH"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
ENV TZ=Asia/Shanghai
|
ENV TZ=Asia/Shanghai
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
ENV MIDWAY_SERVER_ENV=production
|
ENV MIDWAY_SERVER_ENV=production
|
||||||
|
|
||||||
|
COPY --from=builder /workspace/certd-server/ /app/
|
||||||
|
COPY ./patch/ssh2/*.js /app/node_modules/.pnpm/node_modules/ssh2/lib/protocol/
|
||||||
CMD ["node", "--optimize-for-size", "./bootstrap.js"]
|
CMD ["node", "--optimize-for-size", "./bootstrap.js"]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -40,4 +40,7 @@ export default {
|
|||||||
pluginManagement: "Plugin Management",
|
pluginManagement: "Plugin Management",
|
||||||
pluginBetaWarning: "Custom plugins are in BETA and may have breaking changes in future",
|
pluginBetaWarning: "Custom plugins are in BETA and may have breaking changes in future",
|
||||||
pleaseSelectRecord: "Please select records first",
|
pleaseSelectRecord: "Please select records first",
|
||||||
|
clearRuntimeDeps: "Clear Runtime Deps Cache",
|
||||||
|
clearRuntimeDepsConfirm: "Are you sure to clear the runtime dependencies cache? Required dependencies will be reinstalled on the next pipeline execution.",
|
||||||
|
clearRuntimeDepsSuccess: "Runtime dependencies cache cleared successfully",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -40,4 +40,7 @@ export default {
|
|||||||
pluginManagement: "插件管理",
|
pluginManagement: "插件管理",
|
||||||
pluginBetaWarning: "自定义插件处于BETA测试版,后续可能会有破坏性变更",
|
pluginBetaWarning: "自定义插件处于BETA测试版,后续可能会有破坏性变更",
|
||||||
pleaseSelectRecord: "请先勾选记录",
|
pleaseSelectRecord: "请先勾选记录",
|
||||||
|
clearRuntimeDeps: "清理第三方依赖缓存",
|
||||||
|
clearRuntimeDepsConfirm: "确定要清理第三方依赖缓存吗?清理后下次执行流水线时将重新安装所需依赖。",
|
||||||
|
clearRuntimeDepsSuccess: "第三方依赖缓存清理成功",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -143,3 +143,10 @@ export async function GetPluginByName(name: string): Promise<PluginConfigBean> {
|
|||||||
data: { name },
|
data: { name },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function ClearRuntimeDeps(): Promise<void> {
|
||||||
|
return await request({
|
||||||
|
url: "/sys/settings/clearRuntimeDeps",
|
||||||
|
method: "post",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useI18n } from "/src/locales";
|
|||||||
import { Ref, ref, computed } from "vue";
|
import { Ref, ref, computed } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
|
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
|
||||||
import { Modal } from "ant-design-vue";
|
import { Modal, message } from "ant-design-vue";
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
import { usePluginImport } from "./use-import";
|
import { usePluginImport } from "./use-import";
|
||||||
@@ -83,6 +83,23 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||||||
await openImportDialog({ crudExpose });
|
await openImportDialog({ crudExpose });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
clearRuntimeDeps: {
|
||||||
|
show: true,
|
||||||
|
icon: "ion:trash-outline",
|
||||||
|
text: t("certd.clearRuntimeDeps"),
|
||||||
|
type: "primary",
|
||||||
|
danger: true,
|
||||||
|
async click() {
|
||||||
|
Modal.confirm({
|
||||||
|
title: t("certd.confirm"),
|
||||||
|
content: t("certd.clearRuntimeDepsConfirm"),
|
||||||
|
async onOk() {
|
||||||
|
await api.ClearRuntimeDeps();
|
||||||
|
message.success(t("certd.clearRuntimeDepsSuccess"));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
table: {
|
table: {
|
||||||
|
|||||||
@@ -21,9 +21,9 @@
|
|||||||
"test": "cross-env NODE_ENV=unittest mocha",
|
"test": "cross-env NODE_ENV=unittest mocha",
|
||||||
"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 --no-config --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
|
||||||
"cov": "cross-env c8 --all --reporter=text --reporter=lcovonly pnpm run test",
|
"cov": "cross-env c8 --all --reporter=text --reporter=lcovonly pnpm run test",
|
||||||
"lint": "mwts check",
|
"lint:check": "mwts check",
|
||||||
"format": "prettier --write src",
|
"format": "prettier --write src",
|
||||||
"lint:fix": "mwts fix",
|
"lint": "mwts fix",
|
||||||
"ci": "pnpm run cov",
|
"ci": "pnpm run cov",
|
||||||
"build-only": "cross-env NODE_ENV=production mwtsc -p tsconfig.build.json --cleanOutDir --skipLibCheck",
|
"build-only": "cross-env NODE_ENV=production mwtsc -p tsconfig.build.json --cleanOutDir --skipLibCheck",
|
||||||
"build": "pnpm run build-only && pnpm run export-metadata",
|
"build": "pnpm run build-only && pnpm run export-metadata",
|
||||||
@@ -36,22 +36,11 @@
|
|||||||
"flame": "clinic flame -- node ./bootstrap.js",
|
"flame": "clinic flame -- node ./bootstrap.js",
|
||||||
"tsc": "tsc --skipLibCheck",
|
"tsc": "tsc --skipLibCheck",
|
||||||
"slimming": "node ./slimming.js",
|
"slimming": "node ./slimming.js",
|
||||||
"pub": "echo 1"
|
"pub": "echo 1",
|
||||||
|
"compile": "tsc --skipLibCheck --watch",
|
||||||
|
"lint1": "eslint --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alicloud/fc20230330": "^4.1.7",
|
|
||||||
"@alicloud/openapi-client": "^0.4.12",
|
|
||||||
"@alicloud/openapi-util": "^0.3.2",
|
|
||||||
"@alicloud/pop-core": "^1.7.10",
|
|
||||||
"@alicloud/sts-sdk": "^1.0.2",
|
|
||||||
"@alicloud/tea-typescript": "^1.8.0",
|
|
||||||
"@alicloud/tea-util": "^1.4.10",
|
|
||||||
"@aws-sdk/client-acm": "^3.964.0",
|
|
||||||
"@aws-sdk/client-cloudfront": "^3.964.0",
|
|
||||||
"@aws-sdk/client-iam": "^3.964.0",
|
|
||||||
"@aws-sdk/client-route-53": "^3.964.0",
|
|
||||||
"@aws-sdk/client-s3": "^3.964.0",
|
|
||||||
"@aws-sdk/client-sts": "^3.990.0",
|
|
||||||
"@azure/arm-dns": "^5.1.0",
|
"@azure/arm-dns": "^5.1.0",
|
||||||
"@azure/identity": "^4.13.1",
|
"@azure/identity": "^4.13.1",
|
||||||
"@certd/acme-client": "^1.41.4",
|
"@certd/acme-client": "^1.41.4",
|
||||||
@@ -93,14 +82,12 @@
|
|||||||
"@ucloud-sdks/ucloud-sdk-js": "^0.2.4",
|
"@ucloud-sdks/ucloud-sdk-js": "^0.2.4",
|
||||||
"@volcengine/openapi": "^1.28.1",
|
"@volcengine/openapi": "^1.28.1",
|
||||||
"@volcengine/tos-sdk": "^2.9.1",
|
"@volcengine/tos-sdk": "^2.9.1",
|
||||||
"ali-oss": "^6.21.0",
|
|
||||||
"alipay-sdk": "^4.13.0",
|
"alipay-sdk": "^4.13.0",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
"basic-ftp": "^5.0.5",
|
"basic-ftp": "^5.0.5",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"better-sqlite3": "^11.1.2",
|
"better-sqlite3": "^11.1.2",
|
||||||
"cache-manager": "^6.1.0",
|
"cache-manager": "^6.1.0",
|
||||||
"cos-nodejs-sdk-v5": "^2.14.6",
|
|
||||||
"cron-parser": "^4.9.0",
|
"cron-parser": "^4.9.0",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dayjs": "^1.11.7",
|
"dayjs": "^1.11.7",
|
||||||
@@ -119,8 +106,6 @@
|
|||||||
"log4js": "^6.7.1",
|
"log4js": "^6.7.1",
|
||||||
"lru-cache": "^11.0.1",
|
"lru-cache": "^11.0.1",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"mwts": "^1.3.0",
|
|
||||||
"mwtsc": "^1.15.1",
|
|
||||||
"mysql2": "^3.14.0",
|
"mysql2": "^3.14.0",
|
||||||
"nanoid": "^5.0.7",
|
"nanoid": "^5.0.7",
|
||||||
"node-forge": "^1.3.1",
|
"node-forge": "^1.3.1",
|
||||||
@@ -141,14 +126,33 @@
|
|||||||
"ssh2": "^1.17.0",
|
"ssh2": "^1.17.0",
|
||||||
"strip-ansi": "^7.1.0",
|
"strip-ansi": "^7.1.0",
|
||||||
"svg-captcha": "^1.4.0",
|
"svg-captcha": "^1.4.0",
|
||||||
"tencentcloud-sdk-nodejs": "^4.1.112",
|
|
||||||
"typeorm": "^0.3.20",
|
"typeorm": "^0.3.20",
|
||||||
"uuid": "^10.0.0",
|
"uuid": "^10.0.0",
|
||||||
"wechatpay-node-v3": "^2.2.1",
|
"wechatpay-node-v3": "^2.2.1",
|
||||||
"whoiser": "2.0.0-beta.10",
|
"whoiser": "2.0.0-beta.10",
|
||||||
"xml2js": "^0.6.2"
|
"xml2js": "^0.6.2"
|
||||||
},
|
},
|
||||||
|
"lazyDependencies": {
|
||||||
|
"@alicloud/fc20230330": "^4.1.7",
|
||||||
|
"@alicloud/tea-typescript": "^1.8.0",
|
||||||
|
"@alicloud/openapi-client": "^0.4.12",
|
||||||
|
"@alicloud/openapi-util": "^0.3.2",
|
||||||
|
"@alicloud/pop-core": "^1.7.10",
|
||||||
|
"@alicloud/sts-sdk": "^1.0.2",
|
||||||
|
"@alicloud/tea-util": "^1.4.10",
|
||||||
|
"@aws-sdk/client-acm": "^3.964.0",
|
||||||
|
"@aws-sdk/client-cloudfront": "^3.964.0",
|
||||||
|
"@aws-sdk/client-iam": "^3.964.0",
|
||||||
|
"@aws-sdk/client-route-53": "^3.964.0",
|
||||||
|
"@aws-sdk/client-s3": "^3.964.0",
|
||||||
|
"@aws-sdk/client-sts": "^3.990.0",
|
||||||
|
"ali-oss": "^6.21.0",
|
||||||
|
"tencentcloud-sdk-nodejs": "^4.1.112",
|
||||||
|
"cos-nodejs-sdk-v5": "^2.14.6"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"mwts": "^1.3.0",
|
||||||
|
"mwtsc": "^1.15.1",
|
||||||
"@midwayjs/mock": "3.20.11",
|
"@midwayjs/mock": "3.20.11",
|
||||||
"@types/ali-oss": "^6.16.11",
|
"@types/ali-oss": "^6.16.11",
|
||||||
"@types/cache-manager": "^4.0.6",
|
"@types/cache-manager": "^4.0.6",
|
||||||
@@ -179,6 +183,7 @@
|
|||||||
"pnpm": {
|
"pnpm": {
|
||||||
"neverBuiltDependencies": []
|
"neverBuiltDependencies": []
|
||||||
},
|
},
|
||||||
|
|
||||||
"author": "anonymous",
|
"author": "anonymous",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,11 @@ import { tmpdir } from "node:os";
|
|||||||
import { DefaultUploadFileMimeType, uploadWhiteList } from "@midwayjs/upload";
|
import { DefaultUploadFileMimeType, uploadWhiteList } from "@midwayjs/upload";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { logger } from "@certd/basic";
|
import { logger } from "@certd/basic";
|
||||||
|
import { createRequire } from "module";
|
||||||
|
|
||||||
const env = process.env.NODE_ENV || "development";
|
const env = process.env.NODE_ENV || "development";
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
const pkg = require("../../package.json");
|
||||||
|
|
||||||
const development = {
|
const development = {
|
||||||
midwayLogger: {
|
midwayLogger: {
|
||||||
@@ -103,6 +106,21 @@ const development = {
|
|||||||
certd: {
|
certd: {
|
||||||
fileRootDir: "./data/files",
|
fileRootDir: "./data/files",
|
||||||
},
|
},
|
||||||
|
runtimeDeps: {
|
||||||
|
enabled: true,
|
||||||
|
rootDir: "./data/.runtime-deps",
|
||||||
|
autoInstall: true,
|
||||||
|
pnpmCommand: "",
|
||||||
|
installTimeoutMs: 120000,
|
||||||
|
lazyDependencies: pkg.lazyDependencies || {},
|
||||||
|
registry: {
|
||||||
|
mode: "auto",
|
||||||
|
fixedUrl: "",
|
||||||
|
candidates: ["https://registry.npmmirror.com", "https://registry.npmjs.org"],
|
||||||
|
probeTimeoutMs: 3000,
|
||||||
|
cacheTtlMs: 6 * 60 * 60 * 1000,
|
||||||
|
},
|
||||||
|
},
|
||||||
system: {
|
system: {
|
||||||
resetAdminPasswd: false,
|
resetAdminPasswd: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ process.on("uncaughtException", error => {
|
|||||||
logger.error("您的服务器不支持监听IPV6格式的地址(::),请配置环境变量: certd_koa_hostname=0.0.0.0");
|
logger.error("您的服务器不支持监听IPV6格式的地址(::),请配置环境变量: certd_koa_hostname=0.0.0.0");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// function startHeapLog() {
|
// function startHeapLog() {
|
||||||
// function format(bytes: any) {
|
// function format(bytes: any) {
|
||||||
// return (bytes / 1024 / 1024).toFixed(2) + ' MB';
|
// return (bytes / 1024 / 1024).toFixed(2) + ' MB';
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { getEmailSettings } from "../../../modules/sys/settings/fix.js";
|
|||||||
import { http, logger, utils } from "@certd/basic";
|
import { http, logger, utils } from "@certd/basic";
|
||||||
import { CodeService } from "../../../modules/basic/service/code-service.js";
|
import { CodeService } from "../../../modules/basic/service/code-service.js";
|
||||||
import { SmsServiceFactory } from "../../../modules/basic/sms/factory.js";
|
import { SmsServiceFactory } from "../../../modules/basic/sms/factory.js";
|
||||||
|
import { RuntimeDepsService } from "../../../modules/runtime-deps/runtime-deps-service.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*/
|
*/
|
||||||
@@ -23,6 +24,8 @@ export class SysSettingsController extends CrudController<SysSettingsService> {
|
|||||||
codeService: CodeService;
|
codeService: CodeService;
|
||||||
@Inject()
|
@Inject()
|
||||||
addonService: AddonService;
|
addonService: AddonService;
|
||||||
|
@Inject()
|
||||||
|
runtimeDepsService: RuntimeDepsService;
|
||||||
|
|
||||||
getService() {
|
getService() {
|
||||||
return this.service;
|
return this.service;
|
||||||
@@ -216,4 +219,10 @@ export class SysSettingsController extends CrudController<SysSettingsService> {
|
|||||||
const list = await addonRegistry.getDefineList("oauth");
|
const list = await addonRegistry.getDefineList("oauth");
|
||||||
return this.ok(list);
|
return this.ok(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post("/clearRuntimeDeps", { description: "sys:settings:edit" })
|
||||||
|
async clearRuntimeDeps() {
|
||||||
|
await this.runtimeDepsService.clearRuntimeDeps();
|
||||||
|
return this.ok(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { TaskServiceBuilder } from "../../../modules/pipeline/service/getter/tas
|
|||||||
import { cloneDeep } from "lodash-es";
|
import { cloneDeep } from "lodash-es";
|
||||||
import { ApiTags } from "@midwayjs/swagger";
|
import { ApiTags } from "@midwayjs/swagger";
|
||||||
import { AuthService } from "../../../modules/sys/authority/service/auth-service.js";
|
import { AuthService } from "../../../modules/sys/authority/service/auth-service.js";
|
||||||
|
import { RuntimeDepsService } from "../../../modules/runtime-deps/runtime-deps-service.js";
|
||||||
|
|
||||||
@Provide()
|
@Provide()
|
||||||
@Controller("/api/pi/handle")
|
@Controller("/api/pi/handle")
|
||||||
@@ -28,6 +29,9 @@ export class HandleController extends BaseController {
|
|||||||
@Inject()
|
@Inject()
|
||||||
notificationService: NotificationService;
|
notificationService: NotificationService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
runtimeDepsService: RuntimeDepsService;
|
||||||
|
|
||||||
@Post("/access", { description: Constants.per.authOnly, summary: "处理授权请求" })
|
@Post("/access", { description: Constants.per.authOnly, summary: "处理授权请求" })
|
||||||
async accessRequest(@Body(ALL) body: AccessRequestHandleReq) {
|
async accessRequest(@Body(ALL) body: AccessRequestHandleReq) {
|
||||||
let { projectId, userId } = await this.getProjectUserIdRead();
|
let { projectId, userId } = await this.getProjectUserIdRead();
|
||||||
@@ -59,8 +63,16 @@ export class HandleController extends BaseController {
|
|||||||
inputAccess = this.accessService.decryptAccessEntity(param);
|
inputAccess = this.accessService.decryptAccessEntity(param);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const accessGetter = new AccessGetter(userId, projectId, this.accessService.getById.bind(this.accessService));
|
const getAccessById = this.accessService.getById.bind(this.accessService);
|
||||||
const access = await newAccess(body.typeName, inputAccess, accessGetter);
|
const accessGetter = new AccessGetter(userId, projectId, getAccessById, this.runtimeDepsService);
|
||||||
|
const accessContext = {
|
||||||
|
http,
|
||||||
|
logger,
|
||||||
|
utils,
|
||||||
|
accessService: accessGetter,
|
||||||
|
define: undefined,
|
||||||
|
} as any;
|
||||||
|
const access = await newAccess(body.typeName, inputAccess, accessGetter, accessContext);
|
||||||
|
|
||||||
// mergeUtils.merge(access, body.input);
|
// mergeUtils.merge(access, body.input);
|
||||||
const res = await access.onRequest(body);
|
const res = await access.onRequest(body);
|
||||||
@@ -70,14 +82,17 @@ export class HandleController extends BaseController {
|
|||||||
|
|
||||||
@Post("/notification", { description: Constants.per.authOnly, summary: "处理通知请求" })
|
@Post("/notification", { description: Constants.per.authOnly, summary: "处理通知请求" })
|
||||||
async notificationRequest(@Body(ALL) body: NotificationRequestHandleReq) {
|
async notificationRequest(@Body(ALL) body: NotificationRequestHandleReq) {
|
||||||
|
const { projectId, userId } = await this.getProjectUserIdRead();
|
||||||
const input = body.input;
|
const input = body.input;
|
||||||
|
const serviceGetter = this.taskServiceBuilder.create({ userId, projectId });
|
||||||
|
|
||||||
const notification = await newNotification(body.typeName, input, {
|
const notification = await newNotification(body.typeName, input, {
|
||||||
http,
|
http,
|
||||||
logger,
|
logger,
|
||||||
utils,
|
utils,
|
||||||
emailService: this.emailService,
|
emailService: this.emailService,
|
||||||
});
|
serviceGetter,
|
||||||
|
} as any);
|
||||||
|
|
||||||
const res = await notification.onRequest(body);
|
const res = await notification.onRequest(body);
|
||||||
|
|
||||||
@@ -138,8 +153,8 @@ export class HandleController extends BaseController {
|
|||||||
// signal: this.abort.signal,
|
// signal: this.abort.signal,
|
||||||
utils,
|
utils,
|
||||||
serviceGetter: taskServiceGetter,
|
serviceGetter: taskServiceGetter,
|
||||||
};
|
} as any;
|
||||||
instance.setCtx(taskCtx);
|
await instance.setCtx(taskCtx);
|
||||||
mergeUtils.merge(plugin, body.input);
|
mergeUtils.merge(plugin, body.input);
|
||||||
await instance.onInstance();
|
await instance.onInstance();
|
||||||
const res = await plugin.onRequest(body);
|
const res = await plugin.onRequest(body);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { SmsServiceFactory } from "../sms/factory.js";
|
|||||||
import { CaptchaService } from "./captcha-service.js";
|
import { CaptchaService } from "./captcha-service.js";
|
||||||
import { EmailService } from "./email-service.js";
|
import { EmailService } from "./email-service.js";
|
||||||
import { CaptchaRequest } from "../../../plugins/plugin-captcha/api.js";
|
import { CaptchaRequest } from "../../../plugins/plugin-captcha/api.js";
|
||||||
|
import { RuntimeDepsService } from "../../runtime-deps/runtime-deps-service.js";
|
||||||
|
|
||||||
// {data: '<svg.../svg>', text: 'abcd'}
|
// {data: '<svg.../svg>', text: 'abcd'}
|
||||||
/**
|
/**
|
||||||
@@ -24,6 +25,9 @@ export class CodeService {
|
|||||||
@Inject()
|
@Inject()
|
||||||
captchaService: CaptchaService;
|
captchaService: CaptchaService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
runtimeDepsService: RuntimeDepsService;
|
||||||
|
|
||||||
async checkCaptcha(body: any, req: CaptchaRequest) {
|
async checkCaptcha(body: any, req: CaptchaRequest) {
|
||||||
return await this.captchaService.doValidate({ form: body, req });
|
return await this.captchaService.doValidate({ form: body, req });
|
||||||
}
|
}
|
||||||
@@ -53,9 +57,10 @@ export class CodeService {
|
|||||||
const smsConfig = sysSettings.sms.config;
|
const smsConfig = sysSettings.sms.config;
|
||||||
const sender: ISmsService = await SmsServiceFactory.createSmsService(smsType);
|
const sender: ISmsService = await SmsServiceFactory.createSmsService(smsType);
|
||||||
const accessGetter = new AccessSysGetter(this.accessService);
|
const accessGetter = new AccessSysGetter(this.accessService);
|
||||||
sender.setCtx({
|
await sender.setCtx({
|
||||||
accessService: accessGetter,
|
accessService: accessGetter,
|
||||||
config: smsConfig,
|
config: smsConfig,
|
||||||
|
runtimeDepsService: this.runtimeDepsService,
|
||||||
});
|
});
|
||||||
const smsCode = randomNumber(verificationCodeLength);
|
const smsCode = randomNumber(verificationCodeLength);
|
||||||
await sender.sendSmsCode({
|
await sender.sendSmsCode({
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export class AliyunSmsService implements ISmsService {
|
|||||||
|
|
||||||
ctx: SmsPluginCtx<AliyunSmsConfig>;
|
ctx: SmsPluginCtx<AliyunSmsConfig>;
|
||||||
|
|
||||||
setCtx(ctx: any) {
|
async setCtx(ctx: any) {
|
||||||
this.ctx = ctx;
|
this.ctx = ctx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { FormItemProps, IAccessService } from "@certd/pipeline";
|
import { FormItemProps, IAccessService } from "@certd/pipeline";
|
||||||
|
import type { RuntimeDepsService } from "../../runtime-deps/runtime-deps-service.js";
|
||||||
|
|
||||||
export interface ISmsService {
|
export interface ISmsService {
|
||||||
sendSmsCode(opts: { mobile: string; code: string; phoneCode: string }): Promise<void>;
|
sendSmsCode(opts: { mobile: string; code: string; phoneCode: string }): Promise<void>;
|
||||||
setCtx(ctx: { accessService: IAccessService; config: { [key: string]: any } }): void;
|
setCtx(ctx: { accessService: IAccessService; config: { [key: string]: any }; runtimeDepsService?: RuntimeDepsService }): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PluginInputs<T = any> = {
|
export type PluginInputs<T = any> = {
|
||||||
@@ -12,4 +13,5 @@ export type PluginInputs<T = any> = {
|
|||||||
export type SmsPluginCtx<T = any> = {
|
export type SmsPluginCtx<T = any> = {
|
||||||
accessService: IAccessService;
|
accessService: IAccessService;
|
||||||
config: T;
|
config: T;
|
||||||
|
runtimeDepsService?: RuntimeDepsService;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -68,12 +68,22 @@ export class TencentSmsService implements ISmsService {
|
|||||||
|
|
||||||
ctx: SmsPluginCtx<TencentSmsConfig>;
|
ctx: SmsPluginCtx<TencentSmsConfig>;
|
||||||
|
|
||||||
setCtx(ctx: any) {
|
async setCtx(ctx: any) {
|
||||||
this.ctx = ctx;
|
this.ctx = ctx;
|
||||||
|
if (this.ctx.runtimeDepsService) {
|
||||||
|
await this.ctx.runtimeDepsService.ensureDependencies({
|
||||||
|
dependencies: {
|
||||||
|
"tencentcloud-sdk-nodejs": "^4.1.112",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getClient() {
|
async getClient() {
|
||||||
const sdk = await import("tencentcloud-sdk-nodejs/tencentcloud/services/sms/v20210111/index.js");
|
if (!this.ctx.runtimeDepsService) {
|
||||||
|
throw new Error("动态依赖服务未初始化,无法加载腾讯云短信SDK");
|
||||||
|
}
|
||||||
|
const sdk = await this.ctx.runtimeDepsService.importRuntime("tencentcloud-sdk-nodejs/tencentcloud/services/sms/v20210111/index.js");
|
||||||
const client = sdk.v20210111.Client;
|
const client = sdk.v20210111.Client;
|
||||||
const access = await this.ctx.accessService.getById<TencentAccess>(this.ctx.config.accessId);
|
const access = await this.ctx.accessService.getById<TencentAccess>(this.ctx.config.accessId);
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export class YfySmsService implements ISmsService {
|
|||||||
|
|
||||||
ctx: SmsPluginCtx<YfySmsConfig>;
|
ctx: SmsPluginCtx<YfySmsConfig>;
|
||||||
|
|
||||||
setCtx(ctx: any) {
|
async setCtx(ctx: SmsPluginCtx<YfySmsConfig>) {
|
||||||
this.ctx = ctx;
|
this.ctx = ctx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export class CommonDnsProvider implements IDnsProvider {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
setCtx(ctx: DnsProviderContext): void {
|
async setCtx(ctx: DnsProviderContext): Promise<void> {
|
||||||
this.ctx = ctx;
|
this.ctx = ctx;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-1
@@ -13,6 +13,7 @@ import { CertInfoGetter } from "./cert-info-getter.js";
|
|||||||
import { CertInfoService } from "../../../monitor/index.js";
|
import { CertInfoService } from "../../../monitor/index.js";
|
||||||
import { ICertInfoGetter } from "@certd/plugin-lib";
|
import { ICertInfoGetter } from "@certd/plugin-lib";
|
||||||
import { CnameProviderService } from "../../../cname/service/cname-provider-service.js";
|
import { CnameProviderService } from "../../../cname/service/cname-provider-service.js";
|
||||||
|
import { RuntimeDepsService } from "../../../runtime-deps/runtime-deps-service.js";
|
||||||
|
|
||||||
const serviceNames = ["ocrService"];
|
const serviceNames = ["ocrService"];
|
||||||
export class TaskServiceGetter implements IServiceGetter {
|
export class TaskServiceGetter implements IServiceGetter {
|
||||||
@@ -38,6 +39,8 @@ export class TaskServiceGetter implements IServiceGetter {
|
|||||||
return (await this.getDomainVerifierGetter()) as T;
|
return (await this.getDomainVerifierGetter()) as T;
|
||||||
} else if (serviceName === "certInfoGetter") {
|
} else if (serviceName === "certInfoGetter") {
|
||||||
return (await this.getCertInfoGetter()) as T;
|
return (await this.getCertInfoGetter()) as T;
|
||||||
|
} else if (serviceName === "runtimeDepsService") {
|
||||||
|
return (await this.getRuntimeDepsService()) as T;
|
||||||
} else {
|
} else {
|
||||||
if (!serviceNames.includes(serviceName)) {
|
if (!serviceNames.includes(serviceName)) {
|
||||||
throw new Error(`${serviceName} not in whitelist`);
|
throw new Error(`${serviceName} not in whitelist`);
|
||||||
@@ -63,7 +66,9 @@ export class TaskServiceGetter implements IServiceGetter {
|
|||||||
|
|
||||||
async getAccessService(): Promise<AccessGetter> {
|
async getAccessService(): Promise<AccessGetter> {
|
||||||
const accessService: AccessService = await this.appCtx.getAsync("accessService");
|
const accessService: AccessService = await this.appCtx.getAsync("accessService");
|
||||||
return new AccessGetter(this.userId, this.projectId, accessService.getById.bind(accessService));
|
const runtimeDepsService = await this.getRuntimeDepsService();
|
||||||
|
const getAccessById = accessService.getById.bind(accessService);
|
||||||
|
return new AccessGetter(this.userId, this.projectId, getAccessById, runtimeDepsService);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCnameProxyService(): Promise<CnameProxyService> {
|
async getCnameProxyService(): Promise<CnameProxyService> {
|
||||||
@@ -80,6 +85,10 @@ export class TaskServiceGetter implements IServiceGetter {
|
|||||||
const domainService: DomainService = await this.appCtx.getAsync("domainService");
|
const domainService: DomainService = await this.appCtx.getAsync("domainService");
|
||||||
return new DomainVerifierGetter(this.userId, this.projectId, domainService);
|
return new DomainVerifierGetter(this.userId, this.projectId, domainService);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getRuntimeDepsService(): Promise<RuntimeDepsService> {
|
||||||
|
return await this.appCtx.getAsync("runtimeDepsService");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@Provide()
|
@Provide()
|
||||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { NotificationInstanceConfig, notificationRegistry, NotificationSendReq,
|
|||||||
import { http, utils } from "@certd/basic";
|
import { http, utils } from "@certd/basic";
|
||||||
import { EmailService } from "../../basic/service/email-service.js";
|
import { EmailService } from "../../basic/service/email-service.js";
|
||||||
import { isComm, isPlus } from "@certd/plus-core";
|
import { isComm, isPlus } from "@certd/plus-core";
|
||||||
|
import { TaskServiceBuilder } from "./getter/task-service-getter.js";
|
||||||
|
|
||||||
@Provide()
|
@Provide()
|
||||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||||
@@ -20,6 +21,9 @@ export class NotificationService extends BaseService<NotificationEntity> {
|
|||||||
@Inject()
|
@Inject()
|
||||||
sysSettingsService: SysSettingsService;
|
sysSettingsService: SysSettingsService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
taskServiceBuilder: TaskServiceBuilder;
|
||||||
|
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
getRepository() {
|
getRepository() {
|
||||||
return this.repository;
|
return this.repository;
|
||||||
@@ -199,6 +203,7 @@ export class NotificationService extends BaseService<NotificationEntity> {
|
|||||||
logger: logger,
|
logger: logger,
|
||||||
utils: utils,
|
utils: utils,
|
||||||
emailService: this.emailService,
|
emailService: this.emailService,
|
||||||
|
serviceGetter: this.taskServiceBuilder.create({ userId, projectId }),
|
||||||
},
|
},
|
||||||
body: req.body,
|
body: req.body,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -136,10 +136,10 @@ return class DemoTask extends AbstractTaskPlugin {
|
|||||||
export function getDefaultDnsPlugin() {
|
export function getDefaultDnsPlugin() {
|
||||||
const metadata = `
|
const metadata = `
|
||||||
accessType: aliyun # 授权类型名称
|
accessType: aliyun # 授权类型名称
|
||||||
#dependPlugins: # 依赖第三方库,安装插件时会安装依赖库,尽量使用certd已安装的库,比如http、lodash-es、utils
|
#dependPackages: # 依赖第三方 npm 包,运行插件时会按需安装,尽量使用 certd 已安装的库,比如 http、lodash-es、utils
|
||||||
# @alicloud/openapi-client: ^0.4.12
|
# @alicloud/openapi-client: ^0.4.12
|
||||||
#dependLibs: # 依赖的插件,应用商店安装时会先安装依赖插件
|
#dependPlugins: # 依赖的其他插件,使用 type:name 格式避免不同类型插件同名;运行插件时会同时确保被依赖插件的 dependPackages
|
||||||
# aliyun: *
|
# access:aliyun: *
|
||||||
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -13,13 +13,21 @@ import yaml from "js-yaml";
|
|||||||
import { getDefaultAccessPlugin, getDefaultDeployPlugin, getDefaultDnsPlugin } from "./default-plugin.js";
|
import { getDefaultAccessPlugin, getDefaultDeployPlugin, getDefaultDnsPlugin } from "./default-plugin.js";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import { RuntimeDepsService } from "../../runtime-deps/runtime-deps-service.js";
|
||||||
|
|
||||||
export type PluginImportReq = {
|
export type PluginImportReq = {
|
||||||
content: string;
|
content: string;
|
||||||
override?: boolean;
|
override?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function importer(modulePath: string) {
|
function isBareModuleSpecifier(modulePath: string) {
|
||||||
|
if (modulePath.startsWith(".") || modulePath.startsWith("/") || modulePath.startsWith("file:") || modulePath.startsWith("node:")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(modulePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importLocalModule(modulePath: string) {
|
||||||
if (!modulePath) {
|
if (!modulePath) {
|
||||||
throw new Error("modules path 不能为空");
|
throw new Error("modules path 不能为空");
|
||||||
}
|
}
|
||||||
@@ -41,6 +49,9 @@ export class PluginService extends BaseService<PluginEntity> {
|
|||||||
@Inject()
|
@Inject()
|
||||||
builtInPluginService: BuiltInPluginService;
|
builtInPluginService: BuiltInPluginService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
runtimeDepsService: RuntimeDepsService;
|
||||||
|
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
getRepository() {
|
getRepository() {
|
||||||
return this.repository;
|
return this.repository;
|
||||||
@@ -314,6 +325,16 @@ export class PluginService extends BaseService<PluginEntity> {
|
|||||||
}).outputText;
|
}).outputText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async importer(modulePath: string) {
|
||||||
|
if (!modulePath) {
|
||||||
|
throw new Error("modules path 不能为空");
|
||||||
|
}
|
||||||
|
if (!isBareModuleSpecifier(modulePath)) {
|
||||||
|
return await importLocalModule(modulePath);
|
||||||
|
}
|
||||||
|
return await this.runtimeDepsService.importRuntime(modulePath, logger);
|
||||||
|
}
|
||||||
|
|
||||||
private async getPluginClassFromFile(item: any) {
|
private async getPluginClassFromFile(item: any) {
|
||||||
const scriptFilePath = item.scriptFilePath;
|
const scriptFilePath = item.scriptFilePath;
|
||||||
const res = await import(`../../..${scriptFilePath}`);
|
const res = await import(`../../..${scriptFilePath}`);
|
||||||
@@ -345,6 +366,7 @@ export class PluginService extends BaseService<PluginEntity> {
|
|||||||
// const script = await this.compile(plugin.content);
|
// const script = await this.compile(plugin.content);
|
||||||
const script = plugin.content;
|
const script = plugin.content;
|
||||||
const getPluginClass = new AsyncFunction("_ctx", script);
|
const getPluginClass = new AsyncFunction("_ctx", script);
|
||||||
|
const importer = this.importer.bind(this);
|
||||||
return await getPluginClass({ logger: logger, import: importer });
|
return await getPluginClass({ logger: logger, import: importer });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error("编译插件失败:", e);
|
logger.error("编译插件失败:", e);
|
||||||
@@ -439,6 +461,24 @@ export class PluginService extends BaseService<PluginEntity> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getRuntimeDependencyPluginDefines() {
|
||||||
|
const builtInList = await this.getEnabledBuiltInList();
|
||||||
|
const customList = await this.list({
|
||||||
|
buildQuery: bq => {
|
||||||
|
bq.andWhere("type != :type", {
|
||||||
|
type: "builtIn",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const list = [...builtInList];
|
||||||
|
for (const plugin of customList) {
|
||||||
|
const metadata = plugin.metadata ? yaml.load(plugin.metadata) : {};
|
||||||
|
const extra = plugin.extra ? yaml.load(plugin.extra) : {};
|
||||||
|
list.push({ ...plugin, ...metadata, ...extra });
|
||||||
|
}
|
||||||
|
return list.filter(item => item.dependPackages);
|
||||||
|
}
|
||||||
|
|
||||||
async exportPlugin(id: number) {
|
async exportPlugin(id: number) {
|
||||||
const info = await this.info(id);
|
const info = await this.info(id);
|
||||||
if (!info) {
|
if (!info) {
|
||||||
@@ -483,6 +523,7 @@ export class PluginService extends BaseService<PluginEntity> {
|
|||||||
};
|
};
|
||||||
const extra = {
|
const extra = {
|
||||||
dependPlugins: loaded.dependPlugins,
|
dependPlugins: loaded.dependPlugins,
|
||||||
|
dependPackages: loaded.dependPackages,
|
||||||
default: loaded.default,
|
default: loaded.default,
|
||||||
showRunStrategy: loaded.showRunStrategy,
|
showRunStrategy: loaded.showRunStrategy,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import assert from "assert";
|
||||||
|
import { NpmRegistryResolver } from "./npm-registry-resolver.js";
|
||||||
|
|
||||||
|
describe("NpmRegistryResolver", () => {
|
||||||
|
it("chooses the fastest successful registry in auto mode", async () => {
|
||||||
|
const resolver = new NpmRegistryResolver();
|
||||||
|
resolver.config = {
|
||||||
|
mode: "auto",
|
||||||
|
fixedUrl: "",
|
||||||
|
candidates: ["https://slow.example.com", "https://fast.example.com"],
|
||||||
|
probeTimeoutMs: 100,
|
||||||
|
cacheTtlMs: 1000,
|
||||||
|
};
|
||||||
|
resolver.probe = async registryUrl => {
|
||||||
|
return {
|
||||||
|
registryUrl,
|
||||||
|
ok: true,
|
||||||
|
elapsedMs: registryUrl.includes("fast") ? 10 : 50,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await resolver.resolve();
|
||||||
|
|
||||||
|
assert.equal(result, "https://fast.example.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses fixed registry without probing", async () => {
|
||||||
|
const resolver = new NpmRegistryResolver();
|
||||||
|
resolver.config = {
|
||||||
|
mode: "fixed",
|
||||||
|
fixedUrl: "https://registry.example.com",
|
||||||
|
candidates: [],
|
||||||
|
probeTimeoutMs: 100,
|
||||||
|
cacheTtlMs: 1000,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await resolver.resolve();
|
||||||
|
|
||||||
|
assert.equal(result, "https://registry.example.com");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { Config, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
||||||
|
|
||||||
|
export type NpmRegistryResolverConfig = {
|
||||||
|
mode: "auto" | "fixed" | "system";
|
||||||
|
fixedUrl: string;
|
||||||
|
candidates: string[];
|
||||||
|
probeTimeoutMs: number;
|
||||||
|
cacheTtlMs: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RegistryProbeResult = {
|
||||||
|
registryUrl: string;
|
||||||
|
ok: boolean;
|
||||||
|
elapsedMs: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Provide()
|
||||||
|
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||||
|
export class NpmRegistryResolver {
|
||||||
|
@Config("runtimeDeps.registry")
|
||||||
|
config!: NpmRegistryResolverConfig;
|
||||||
|
|
||||||
|
private cache?: { registryUrl: string; expiresAt: number };
|
||||||
|
|
||||||
|
async resolve(): Promise<string> {
|
||||||
|
const config = this.config;
|
||||||
|
if (config?.mode === "fixed" && config.fixedUrl) {
|
||||||
|
return config.fixedUrl;
|
||||||
|
}
|
||||||
|
if (config?.mode === "system") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const cached = this.cache;
|
||||||
|
if (cached && cached.expiresAt > Date.now()) {
|
||||||
|
return cached.registryUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidates = (config?.candidates || []).filter(Boolean);
|
||||||
|
const probes = await Promise.allSettled(candidates.map(registryUrl => this.probe(registryUrl)));
|
||||||
|
const okList = probes.map(item => (item.status === "fulfilled" ? item.value : null)).filter((item): item is RegistryProbeResult => !!item && item.ok);
|
||||||
|
|
||||||
|
if (okList.length > 0) {
|
||||||
|
okList.sort((a, b) => a.elapsedMs - b.elapsedMs);
|
||||||
|
const best = okList[0].registryUrl;
|
||||||
|
this.cache = {
|
||||||
|
registryUrl: best,
|
||||||
|
expiresAt: Date.now() + (config?.cacheTtlMs || 0),
|
||||||
|
};
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async probe(registryUrl: string): Promise<RegistryProbeResult> {
|
||||||
|
const timeoutMs = this.config?.probeTimeoutMs || 3000;
|
||||||
|
const started = Date.now();
|
||||||
|
try {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${registryUrl.replace(/\/$/, "")}/-/ping`, { signal: controller.signal });
|
||||||
|
return {
|
||||||
|
registryUrl,
|
||||||
|
ok: res.ok,
|
||||||
|
elapsedMs: Date.now() - started,
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
registryUrl,
|
||||||
|
ok: false,
|
||||||
|
elapsedMs: Date.now() - started,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,508 @@
|
|||||||
|
import assert from "assert";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import os from "os";
|
||||||
|
import { RuntimeDepsService, type RuntimeDependencyPluginDefine } from "./runtime-deps-service.js";
|
||||||
|
import { accessRegistry, pluginRegistry } from "@certd/pipeline";
|
||||||
|
import { addonRegistry } from "@certd/lib-server";
|
||||||
|
|
||||||
|
describe("RuntimeDepsService", () => {
|
||||||
|
it("detects conflicting dependency ranges across plugins", () => {
|
||||||
|
const service = new RuntimeDepsService();
|
||||||
|
const merged = service.collectDependencies([
|
||||||
|
{ name: "a", dependPackages: { foo: "^1.0.0" } },
|
||||||
|
{ name: "b", dependPackages: { foo: "^1.2.0" } },
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.deepEqual(merged.dependencies, { foo: "^1.0.0" });
|
||||||
|
assert.equal(merged.conflicts.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports incompatible dependency ranges", () => {
|
||||||
|
const service = new RuntimeDepsService();
|
||||||
|
const merged = service.collectDependencies([
|
||||||
|
{ name: "a", dependPackages: { foo: "^1.0.0" } },
|
||||||
|
{ name: "b", dependPackages: { foo: "^2.0.0" } },
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.equal(merged.conflicts.length, 1);
|
||||||
|
assert.equal(merged.conflicts[0].packageName, "foo");
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
service.runtimeDepsRootDir = rootDir;
|
||||||
|
service.registryResolver = {
|
||||||
|
async resolve() {
|
||||||
|
return "https://registry.npmmirror.com";
|
||||||
|
},
|
||||||
|
} 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 };
|
||||||
|
}
|
||||||
|
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 plugins: RuntimeDependencyPluginDefine[] = [{ name: "a", dependPackages: { foo: "^1.0.0" } }];
|
||||||
|
const result = await service.ensureInstalled(plugins);
|
||||||
|
|
||||||
|
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();
|
||||||
|
service.runtimeDepsRootDir = rootDir;
|
||||||
|
service.registryResolver = {
|
||||||
|
async resolve() {
|
||||||
|
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({ 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();
|
||||||
|
service.runtimeDepsRootDir = rootDir;
|
||||||
|
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();
|
||||||
|
service.runtimeDepsRootDir = rootDir;
|
||||||
|
service.lazyDependencies = {
|
||||||
|
"lazy-pkg": "^1.2.3",
|
||||||
|
};
|
||||||
|
service.registryResolver = {
|
||||||
|
async resolve() {
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
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("resolves scoped package names for lazy imports", async () => {
|
||||||
|
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-scoped-"));
|
||||||
|
const service = new RuntimeDepsService();
|
||||||
|
service.runtimeDepsRootDir = rootDir;
|
||||||
|
service.lazyDependencies = {
|
||||||
|
"@scope/lazy": "^2.0.0",
|
||||||
|
};
|
||||||
|
service.registryResolver = {
|
||||||
|
async resolve() {
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
service.runtimeDepsRootDir = rootDir;
|
||||||
|
service.lazyDependencies = {};
|
||||||
|
|
||||||
|
await assert.rejects(() => service.importRuntime("missing-pkg/sub.js"), /未配置懒加载版本: missing-pkg/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to project node_modules when lazy dependency is not configured", async () => {
|
||||||
|
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-project-fallback-"));
|
||||||
|
const service = new RuntimeDepsService();
|
||||||
|
service.runtimeDepsRootDir = rootDir;
|
||||||
|
service.lazyDependencies = {};
|
||||||
|
|
||||||
|
const mod = await service.importRuntime("dayjs");
|
||||||
|
|
||||||
|
assert.equal(typeof mod.default, "function");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to project node_modules when lazy install fails", async () => {
|
||||||
|
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-project-fallback-install-"));
|
||||||
|
const service = new RuntimeDepsService();
|
||||||
|
service.runtimeDepsRootDir = rootDir;
|
||||||
|
service.lazyDependencies = {
|
||||||
|
dayjs: "^1.11.7",
|
||||||
|
};
|
||||||
|
service.registryResolver = {
|
||||||
|
async resolve() {
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
return { stdout: "", stderr: "install failed in test", code: 1 };
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const mod = await service.importRuntime("dayjs");
|
||||||
|
|
||||||
|
assert.equal(typeof mod.default, "function");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps previously installed dependencies when installing a later plugin", async () => {
|
||||||
|
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-merge-"));
|
||||||
|
const service = new RuntimeDepsService();
|
||||||
|
service.runtimeDepsRootDir = rootDir;
|
||||||
|
service.registryResolver = {
|
||||||
|
async resolve() {
|
||||||
|
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.ensureInstalled([{ name: "a", pluginType: "deploy", dependPackages: { foo: "^1.0.0" } }]);
|
||||||
|
await service.ensureInstalled([{ name: "b", pluginType: "deploy", dependPackages: { 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("includes npm dependencies from dependent plugins", () => {
|
||||||
|
const service = new RuntimeDepsService();
|
||||||
|
accessRegistry.register("runtimeDepsAccess", {
|
||||||
|
define: { name: "runtimeDepsAccess", title: "access", dependPackages: { accessOnly: "^1.0.0" } } as any,
|
||||||
|
target: async () => ({} as any),
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const resolved = service.resolvePluginDependencies({
|
||||||
|
name: "deploy",
|
||||||
|
pluginType: "deploy",
|
||||||
|
dependPlugins: { "access:runtimeDepsAccess": "*" },
|
||||||
|
dependPackages: { deployOnly: "^1.0.0" },
|
||||||
|
});
|
||||||
|
const merged = service.collectDependencies(resolved);
|
||||||
|
|
||||||
|
assert.deepEqual(merged.dependencies, {
|
||||||
|
deployOnly: "^1.0.0",
|
||||||
|
accessOnly: "^1.0.0",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
accessRegistry.unRegister("runtimeDepsAccess");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("installs dependencies by registered plugin key", async () => {
|
||||||
|
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-key-"));
|
||||||
|
const service = new RuntimeDepsService();
|
||||||
|
service.runtimeDepsRootDir = rootDir;
|
||||||
|
service.registryResolver = {
|
||||||
|
async resolve() {
|
||||||
|
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;
|
||||||
|
pluginRegistry.register("runtimeDepsKey", {
|
||||||
|
define: { name: "runtimeDepsKey", title: "key", dependPackages: { keyed: "^1.0.0" } } as any,
|
||||||
|
target: async () => ({} as any),
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await service.ensureRuntimeDependencies({ pluginKeys: "plugin:runtimeDepsKey" });
|
||||||
|
|
||||||
|
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
|
||||||
|
assert.deepEqual(manifest.dependencies, { keyed: "^1.0.0" });
|
||||||
|
} finally {
|
||||||
|
pluginRegistry.unRegister("runtimeDepsKey");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("installs dependencies from multiple plugin keys including addon subtype keys", async () => {
|
||||||
|
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-keys-"));
|
||||||
|
const service = new RuntimeDepsService();
|
||||||
|
service.runtimeDepsRootDir = rootDir;
|
||||||
|
service.registryResolver = {
|
||||||
|
async resolve() {
|
||||||
|
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;
|
||||||
|
accessRegistry.register("runtimeDepsArrayAccess", {
|
||||||
|
define: { name: "runtimeDepsArrayAccess", title: "access", dependPackages: { accessPkg: "^1.0.0" } } as any,
|
||||||
|
target: async () => ({} as any),
|
||||||
|
});
|
||||||
|
addonRegistry.register("captcha:runtimeDepsArrayAddon", {
|
||||||
|
define: { addonType: "captcha", name: "runtimeDepsArrayAddon", title: "addon", dependPackages: { addonPkg: "^2.0.0" } } as any,
|
||||||
|
target: async () => ({} as any),
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await service.ensureRuntimeDependencies({ pluginKeys: ["access:runtimeDepsArrayAccess", "addon:captcha:runtimeDepsArrayAddon"] });
|
||||||
|
|
||||||
|
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
|
||||||
|
assert.deepEqual(manifest.dependencies, {
|
||||||
|
accessPkg: "^1.0.0",
|
||||||
|
addonPkg: "^2.0.0",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
accessRegistry.unRegister("runtimeDepsArrayAccess");
|
||||||
|
addonRegistry.unRegister("captcha:runtimeDepsArrayAddon");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports missing dependent plugins", () => {
|
||||||
|
const service = new RuntimeDepsService();
|
||||||
|
|
||||||
|
assert.throws(() => service.resolvePluginDependencies({ name: "deploy", pluginType: "deploy", dependPlugins: { "access:access": "*" } }), /插件依赖缺失/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports incompatible dependent plugin versions", () => {
|
||||||
|
const service = new RuntimeDepsService();
|
||||||
|
accessRegistry.register("runtimeDepsVersionedAccess", {
|
||||||
|
define: { name: "runtimeDepsVersionedAccess", title: "access", version: "1.4.0", dependPackages: { accessOnly: "^1.0.0" } } as any,
|
||||||
|
target: async () => ({} as any),
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
service.resolvePluginDependencies({
|
||||||
|
name: "deploy",
|
||||||
|
pluginType: "deploy",
|
||||||
|
dependPlugins: { "access:runtimeDepsVersionedAccess": "^2.0.0" },
|
||||||
|
}),
|
||||||
|
/插件依赖版本冲突/
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
accessRegistry.unRegister("runtimeDepsVersionedAccess");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports bare dependent plugin names as invalid format", () => {
|
||||||
|
const service = new RuntimeDepsService();
|
||||||
|
|
||||||
|
assert.throws(() => service.resolvePluginDependencies({ name: "deploy", pluginType: "deploy", dependPlugins: { runtimeDepsBareName: "*" } }), /插件依赖格式错误/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("records runtime install environment state", async () => {
|
||||||
|
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-state-"));
|
||||||
|
const service = new RuntimeDepsService();
|
||||||
|
service.runtimeDepsRootDir = rootDir;
|
||||||
|
service.registryResolver = {
|
||||||
|
async resolve() {
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
assert.equal(args[0], "install");
|
||||||
|
return { stdout: "", stderr: "", code: 0 };
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
await service.ensureInstalled([{ name: "a", dependPackages: { foo: "^1.0.0" } }]);
|
||||||
|
|
||||||
|
const state = JSON.parse(fs.readFileSync(path.join(rootDir, "install-state.json"), "utf8"));
|
||||||
|
assert.equal(state.nodeVersion, process.version);
|
||||||
|
assert.equal(state.pnpmVersion, "9.1.0");
|
||||||
|
assert.equal(state.lastError, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("serializes installs with a file lock", async () => {
|
||||||
|
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-lock-"));
|
||||||
|
const serviceA = new RuntimeDepsService();
|
||||||
|
const serviceB = new RuntimeDepsService();
|
||||||
|
for (const service of [serviceA, serviceB]) {
|
||||||
|
service.runtimeDepsRootDir = rootDir;
|
||||||
|
service.registryResolver = {
|
||||||
|
async resolve() {
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
let installCount = 0;
|
||||||
|
const commandRunner = {
|
||||||
|
async run(command: string, args: string[]) {
|
||||||
|
assert.equal(command, "pnpm");
|
||||||
|
if (args.includes("--version")) {
|
||||||
|
return { stdout: "9.1.0\n", stderr: "", code: 0 };
|
||||||
|
}
|
||||||
|
assert.equal(args[0], "install");
|
||||||
|
installCount++;
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
|
fs.mkdirSync(path.join(rootDir, "node_modules"), { recursive: true });
|
||||||
|
return { stdout: "", stderr: "", code: 0 };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
serviceA.commandRunner = commandRunner as any;
|
||||||
|
serviceB.commandRunner = commandRunner as any;
|
||||||
|
|
||||||
|
await Promise.all([serviceA.ensureInstalled([{ name: "a", dependPackages: { foo: "^1.0.0" } }]), serviceB.ensureInstalled([{ name: "a", dependPackages: { foo: "^1.0.0" } }])]);
|
||||||
|
|
||||||
|
assert.equal(installCount, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
service.runtimeDepsRootDir = rootDir;
|
||||||
|
service.registryResolver = {
|
||||||
|
async resolve() {
|
||||||
|
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");
|
||||||
|
if (args.includes("--version")) {
|
||||||
|
return { stdout: "9.1.0\n", stderr: "", code: 0 };
|
||||||
|
}
|
||||||
|
return { stdout: "", stderr: "", code: 0 };
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
await service.ensureInstalled([{ name: "a", dependPackages: { 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("clears runtime dependency directory", async () => {
|
||||||
|
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-clear-"));
|
||||||
|
const runtimeRootDir = path.join(rootDir, ".runtime-deps");
|
||||||
|
fs.mkdirSync(path.join(runtimeRootDir, "node_modules", "foo"), { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(runtimeRootDir, "package.json"), "{}", "utf8");
|
||||||
|
const service = new RuntimeDepsService();
|
||||||
|
service.runtimeDepsRootDir = runtimeRootDir;
|
||||||
|
service.installTimeoutMs = 1000;
|
||||||
|
|
||||||
|
await service.clearRuntimeDeps();
|
||||||
|
|
||||||
|
assert.equal(fs.existsSync(runtimeRootDir), true);
|
||||||
|
assert.equal(fs.readdirSync(runtimeRootDir).length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects clearing unexpected runtime dependency path", async () => {
|
||||||
|
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-clear-invalid-"));
|
||||||
|
const service = new RuntimeDepsService();
|
||||||
|
service.runtimeDepsRootDir = rootDir;
|
||||||
|
|
||||||
|
await assert.rejects(() => service.clearRuntimeDeps(), /动态依赖目录配置异常/);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,657 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { spawn } from "child_process";
|
||||||
|
import crypto from "crypto";
|
||||||
|
import { Config, Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
||||||
|
import { createRequire } from "module";
|
||||||
|
import { pathToFileURL } from "url";
|
||||||
|
import { NpmRegistryResolver } from "./npm-registry-resolver.js";
|
||||||
|
import { Registry, accessRegistry, notificationRegistry, pluginRegistry } from "@certd/pipeline";
|
||||||
|
import { dnsProviderRegistry } from "@certd/plugin-lib";
|
||||||
|
import { addonRegistry } from "@certd/lib-server";
|
||||||
|
import { logger, ILogger } from "@certd/basic";
|
||||||
|
|
||||||
|
export type RuntimeDependencyPluginDefine = {
|
||||||
|
name: string;
|
||||||
|
key?: string;
|
||||||
|
title?: string;
|
||||||
|
version?: string;
|
||||||
|
pluginType?: string;
|
||||||
|
addonType?: string;
|
||||||
|
dependPlugins?: Record<string, string>;
|
||||||
|
dependPackages?: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RegisteredDefineLike = RuntimeDependencyPluginDefine & {
|
||||||
|
key?: string;
|
||||||
|
pluginType?: string;
|
||||||
|
addonType?: string;
|
||||||
|
dependPlugins?: Record<string, string>;
|
||||||
|
dependPackages?: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeRange(range: string) {
|
||||||
|
return range.trim().replace(/^\^/, "").replace(/^~?/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function areRangesCompatible(a: string, b: string) {
|
||||||
|
if (!a || !b) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (a === "*" || b === "*") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const left = normalizeRange(a).split(".");
|
||||||
|
const right = normalizeRange(b).split(".");
|
||||||
|
return left[0] === right[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
type DependencyConflict = {
|
||||||
|
packageName: string;
|
||||||
|
ranges: Array<{ pluginName: string; range: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CollectDependenciesResult = {
|
||||||
|
dependencies: Record<string, string>;
|
||||||
|
conflicts: DependencyConflict[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type InstallResult = {
|
||||||
|
registryUrl: string;
|
||||||
|
packageJsonPath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RuntimeImportResolveResult = {
|
||||||
|
resolved: string;
|
||||||
|
packageName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CommandRunnerResult = {
|
||||||
|
stdout: string;
|
||||||
|
stderr: string;
|
||||||
|
code: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CommandRunner = {
|
||||||
|
// @ts-ignore
|
||||||
|
run(command: string, args: string[], options: { cwd: string; timeoutMs: number; env?: NodeJS.ProcessEnv }): Promise<CommandRunnerResult>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PROCESS_LOCKS = new Map<string, Promise<unknown>>();
|
||||||
|
|
||||||
|
class DefaultCommandRunner implements CommandRunner {
|
||||||
|
// @ts-ignore
|
||||||
|
async run(command: string, args: string[], options: { cwd: string; timeoutMs: number; env?: NodeJS.ProcessEnv }): Promise<CommandRunnerResult> {
|
||||||
|
return await new Promise<CommandRunnerResult>(resolve => {
|
||||||
|
let stdout = "";
|
||||||
|
let stderr = "";
|
||||||
|
let settled = false;
|
||||||
|
const child = spawn(command, args, {
|
||||||
|
cwd: options.cwd,
|
||||||
|
env: options.env,
|
||||||
|
windowsHide: true,
|
||||||
|
// @ts-ignore
|
||||||
|
shell: process.platform === "win32",
|
||||||
|
});
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
if (settled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
settled = true;
|
||||||
|
child.kill("SIGTERM");
|
||||||
|
resolve({ stdout, stderr: stderr || `command timeout after ${options.timeoutMs}ms`, code: 1 });
|
||||||
|
}, options.timeoutMs);
|
||||||
|
|
||||||
|
child.stdout?.on("data", chunk => {
|
||||||
|
stdout += chunk.toString();
|
||||||
|
});
|
||||||
|
child.stderr?.on("data", chunk => {
|
||||||
|
stderr += chunk.toString();
|
||||||
|
});
|
||||||
|
child.on("error", error => {
|
||||||
|
if (settled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
settled = true;
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve({ stdout, stderr: error.message, code: 1 });
|
||||||
|
});
|
||||||
|
child.on("close", code => {
|
||||||
|
if (settled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
settled = true;
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve({ stdout, stderr, code: code || 0 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provide()
|
||||||
|
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||||
|
export class RuntimeDepsService {
|
||||||
|
@Config("runtimeDeps.rootDir")
|
||||||
|
runtimeDepsRootDir = "./data/.runtime-deps";
|
||||||
|
|
||||||
|
@Config("runtimeDeps.autoInstall")
|
||||||
|
autoInstall = true;
|
||||||
|
|
||||||
|
@Config("runtimeDeps.enabled")
|
||||||
|
enabled = true;
|
||||||
|
|
||||||
|
@Config("runtimeDeps.installTimeoutMs")
|
||||||
|
installTimeoutMs = 120000;
|
||||||
|
|
||||||
|
@Config("runtimeDeps.pnpmCommand")
|
||||||
|
pnpmCommand = "";
|
||||||
|
|
||||||
|
@Config("runtimeDeps.lazyDependencies")
|
||||||
|
lazyDependencies: Record<string, string> = {};
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
registryResolver!: NpmRegistryResolver;
|
||||||
|
|
||||||
|
commandRunner: CommandRunner = new DefaultCommandRunner();
|
||||||
|
|
||||||
|
private installPromises = new Map<string, Promise<InstallResult>>();
|
||||||
|
|
||||||
|
collectDependencies(plugins: RuntimeDependencyPluginDefine[]): CollectDependenciesResult {
|
||||||
|
const merged: Record<string, string> = {};
|
||||||
|
const seen: Record<string, Array<{ pluginName: string; range: string }>> = {};
|
||||||
|
|
||||||
|
for (const plugin of plugins) {
|
||||||
|
const deps = plugin.dependPackages || {};
|
||||||
|
for (const [packageName, range] of Object.entries(deps)) {
|
||||||
|
seen[packageName] ||= [];
|
||||||
|
seen[packageName].push({ pluginName: plugin.name, range });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const conflicts: DependencyConflict[] = [];
|
||||||
|
for (const [packageName, ranges] of Object.entries(seen)) {
|
||||||
|
const first = ranges[0]?.range;
|
||||||
|
if (!first) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const conflict = ranges.some(item => !areRangesCompatible(first, item.range));
|
||||||
|
if (conflict) {
|
||||||
|
conflicts.push({ packageName, ranges });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
merged[packageName] = first;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { dependencies: merged, conflicts };
|
||||||
|
}
|
||||||
|
|
||||||
|
async ensureInstalled(options: { plugins: RuntimeDependencyPluginDefine[]; logger?: ILogger }): Promise<InstallResult> {
|
||||||
|
const { plugins, logger: log } = options;
|
||||||
|
const { dependencies, conflicts } = this.resolveDependenciesFromPlugins(plugins);
|
||||||
|
if (conflicts.length > 0) {
|
||||||
|
const conflict = conflicts[0];
|
||||||
|
throw new Error(`动态依赖版本冲突: ${conflict.packageName} => ${conflict.ranges.map(item => `${item.pluginName}:${item.range}`).join(", ")}`);
|
||||||
|
}
|
||||||
|
return await this.ensureDependencies({ dependencies, logger: log });
|
||||||
|
}
|
||||||
|
|
||||||
|
async ensureDependencies(options: { dependencies: Record<string, string>; logger?: ILogger }): Promise<InstallResult> {
|
||||||
|
const { dependencies, logger: log } = options;
|
||||||
|
if (!this.enabled) {
|
||||||
|
return {
|
||||||
|
registryUrl: "",
|
||||||
|
packageJsonPath: path.join(this.getRuntimeDepsRootDir(), "package.json"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!this.autoInstall) {
|
||||||
|
return {
|
||||||
|
registryUrl: "",
|
||||||
|
packageJsonPath: path.join(this.getRuntimeDepsRootDir(), "package.json"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const dependenciesHash = this.createDependenciesHash(dependencies);
|
||||||
|
let installPromise = this.installPromises.get(dependenciesHash);
|
||||||
|
if (!installPromise) {
|
||||||
|
installPromise = this.doEnsureInstalled({ dependencies, logger: log }).catch(error => {
|
||||||
|
this.installPromises.delete(dependenciesHash);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
this.installPromises.set(dependenciesHash, installPromise);
|
||||||
|
}
|
||||||
|
return await installPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveDependenciesFromPlugins(plugins: RuntimeDependencyPluginDefine[]): CollectDependenciesResult {
|
||||||
|
const expandedPlugins = plugins.flatMap(plugin => this.resolvePluginDependencies(plugin));
|
||||||
|
return this.collectDependencies(expandedPlugins);
|
||||||
|
}
|
||||||
|
|
||||||
|
async ensureRuntimeDependencies(options: { pluginKeys: string | string[]; logger?: ILogger }): Promise<InstallResult> {
|
||||||
|
const { pluginKeys, logger: log } = options;
|
||||||
|
const keys = Array.isArray(pluginKeys) ? pluginKeys : [pluginKeys];
|
||||||
|
const pluginDefines = keys.map(pluginKey => this.getDefineByPluginKey(pluginKey));
|
||||||
|
if (pluginDefines.every(pluginDefine => !pluginDefine.dependPackages && !pluginDefine.dependPlugins)) {
|
||||||
|
return {
|
||||||
|
registryUrl: "",
|
||||||
|
packageJsonPath: path.join(this.getRuntimeDepsRootDir(), "package.json"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const expandedPluginDefines = pluginDefines.flatMap(pluginDefine => this.resolvePluginDependencies(pluginDefine));
|
||||||
|
return await this.ensureInstalled({ plugins: expandedPluginDefines, logger: log });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async doEnsureInstalled(options: { dependencies: Record<string, string>; logger?: ILogger }): Promise<InstallResult> {
|
||||||
|
let { dependencies } = options;
|
||||||
|
const log = options.logger || logger;
|
||||||
|
return await this.withInstallLock(async () => {
|
||||||
|
const rootDir = this.getRuntimeDepsRootDir();
|
||||||
|
const packageJsonPath = path.join(rootDir, "package.json");
|
||||||
|
const lockPath = path.join(rootDir, "pnpm-lock.yaml");
|
||||||
|
log.info(`第三方依赖安装: ${JSON.stringify(dependencies)}`);
|
||||||
|
dependencies = this.mergeInstalledDependencies(this.readManifestDependencies(packageJsonPath), dependencies);
|
||||||
|
const dependenciesHash = this.createDependenciesHash(dependencies);
|
||||||
|
const statePath = path.join(rootDir, "install-state.json");
|
||||||
|
const currentState = this.readInstallState(statePath);
|
||||||
|
if (currentState?.dependenciesHash === dependenciesHash && fs.existsSync(path.join(rootDir, "node_modules"))) {
|
||||||
|
log.info("第三方依赖已安装");
|
||||||
|
return { registryUrl: currentState.registryUrl || "", packageJsonPath };
|
||||||
|
}
|
||||||
|
const manifest = {
|
||||||
|
name: "certd-runtime-deps",
|
||||||
|
private: true,
|
||||||
|
type: "module",
|
||||||
|
dependencies,
|
||||||
|
};
|
||||||
|
fs.writeFileSync(packageJsonPath, JSON.stringify(manifest, null, 2), "utf8");
|
||||||
|
|
||||||
|
const registryUrl = await this.registryResolver.resolve();
|
||||||
|
const env = this.buildChildEnv(registryUrl);
|
||||||
|
const command = this.getPnpmCommand();
|
||||||
|
const pnpmVersion = await this.getPnpmVersion(command, env);
|
||||||
|
const args = ["install", "--prod", "--ignore-scripts", "--ignore-workspace", "--no-frozen-lockfile", "--reporter=append-only"];
|
||||||
|
if (registryUrl) {
|
||||||
|
args.push(`--registry=${registryUrl}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(`开始安装第三方依赖: ${Object.keys(dependencies).join(", ")}`);
|
||||||
|
const result = await this.commandRunner.run(command, args, {
|
||||||
|
cwd: rootDir,
|
||||||
|
timeoutMs: this.installTimeoutMs,
|
||||||
|
env,
|
||||||
|
});
|
||||||
|
if (result.code !== 0) {
|
||||||
|
const message = result.stderr || result.stdout || "unknown error";
|
||||||
|
this.writeInstallState(statePath, {
|
||||||
|
...currentState,
|
||||||
|
installedAt: currentState?.installedAt,
|
||||||
|
failedAt: new Date().toISOString(),
|
||||||
|
registryUrl,
|
||||||
|
dependenciesHash,
|
||||||
|
// @ts-ignore
|
||||||
|
nodeVersion: process.version,
|
||||||
|
pnpmVersion,
|
||||||
|
lockFileExists: fs.existsSync(lockPath),
|
||||||
|
lastError: message,
|
||||||
|
});
|
||||||
|
throw new Error(`动态依赖安装失败: ${message}`);
|
||||||
|
}
|
||||||
|
this.writeInstallState(statePath, {
|
||||||
|
installedAt: new Date().toISOString(),
|
||||||
|
registryUrl,
|
||||||
|
dependenciesHash,
|
||||||
|
// @ts-ignore
|
||||||
|
nodeVersion: process.version,
|
||||||
|
pnpmVersion,
|
||||||
|
lockFileExists: fs.existsSync(lockPath),
|
||||||
|
});
|
||||||
|
log.info("第三方依赖安装完成");
|
||||||
|
return { registryUrl, packageJsonPath };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async importRuntime(specifier: string,logger?:ILogger) {
|
||||||
|
if (this.isNativeImportSpecifier(specifier)) {
|
||||||
|
return await import(specifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolved = await this.resolveImportSpecifier(specifier,logger);
|
||||||
|
return await import(pathToFileURL(resolved).href);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async resolveImportSpecifier(specifier: string,logger?:ILogger) {
|
||||||
|
try {
|
||||||
|
return this.resolveRuntimeSpecifier(specifier).resolved;
|
||||||
|
} catch (runtimeError: any) {
|
||||||
|
if (!this.isModuleNotFoundError(runtimeError)) {
|
||||||
|
throw runtimeError;
|
||||||
|
}
|
||||||
|
return await this.resolveMissingRuntimeSpecifier(specifier, runtimeError,logger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async resolveMissingRuntimeSpecifier(specifier: string, runtimeError: any,logger?:ILogger) {
|
||||||
|
const packageName = this.parsePackageName(specifier);
|
||||||
|
const lazyRange = this.lazyDependencies?.[packageName];
|
||||||
|
if (!lazyRange) {
|
||||||
|
try {
|
||||||
|
return this.resolveProjectSpecifier(specifier, runtimeError).resolved;
|
||||||
|
} catch {
|
||||||
|
throw new Error(`动态依赖未安装且未配置懒加载版本: ${packageName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await this.ensureLazyDependency(packageName,logger);
|
||||||
|
return this.resolveRuntimeSpecifier(specifier).resolved;
|
||||||
|
} catch (lazyError: any) {
|
||||||
|
return this.resolveProjectSpecifier(specifier, lazyError).resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private isNativeImportSpecifier(specifier: string) {
|
||||||
|
return specifier.startsWith(".") || specifier.startsWith("/") || specifier.startsWith("file:") || specifier.startsWith("node:");
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveRuntimeSpecifier(specifier: string): RuntimeImportResolveResult {
|
||||||
|
const packageName = this.parsePackageName(specifier);
|
||||||
|
const packageJsonPath = path.join(this.getRuntimeDepsRootDir(), "package.json");
|
||||||
|
const require = createRequire(packageJsonPath);
|
||||||
|
const resolved = require.resolve(specifier);
|
||||||
|
return { packageName, resolved };
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveProjectSpecifier(specifier: string, cause?: any): RuntimeImportResolveResult {
|
||||||
|
try {
|
||||||
|
const packageName = this.parsePackageName(specifier);
|
||||||
|
const packageJsonPath = path.resolve("package.json");
|
||||||
|
const require = createRequire(packageJsonPath);
|
||||||
|
const resolved = require.resolve(specifier);
|
||||||
|
return { packageName, resolved };
|
||||||
|
} catch (projectError: any) {
|
||||||
|
if (cause) {
|
||||||
|
projectError.cause = cause;
|
||||||
|
}
|
||||||
|
throw projectError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private parsePackageName(specifier: string) {
|
||||||
|
if (!specifier || specifier.trim() !== specifier) {
|
||||||
|
throw new Error(`动态依赖导入路径无效: ${specifier}`);
|
||||||
|
}
|
||||||
|
const parts = specifier.split("/");
|
||||||
|
if (specifier.startsWith("@")) {
|
||||||
|
if (parts.length < 2 || !parts[0] || !parts[1]) {
|
||||||
|
throw new Error(`动态依赖导入路径无效: ${specifier}`);
|
||||||
|
}
|
||||||
|
return `${parts[0]}/${parts[1]}`;
|
||||||
|
}
|
||||||
|
if (!parts[0]) {
|
||||||
|
throw new Error(`动态依赖导入路径无效: ${specifier}`);
|
||||||
|
}
|
||||||
|
return parts[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensureLazyDependency(packageName: string,logger?:ILogger) {
|
||||||
|
const range = this.lazyDependencies?.[packageName];
|
||||||
|
if (!range) {
|
||||||
|
throw new Error(`动态依赖未安装且未配置懒加载版本: ${packageName}`);
|
||||||
|
}
|
||||||
|
const dependencies = {
|
||||||
|
[packageName]: range,
|
||||||
|
};
|
||||||
|
await this.ensureDependencies({ dependencies,logger });
|
||||||
|
}
|
||||||
|
|
||||||
|
private isModuleNotFoundError(error: any) {
|
||||||
|
return error?.code === "MODULE_NOT_FOUND" || error?.code === "ERR_MODULE_NOT_FOUND";
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvePluginDependencies(current: RuntimeDependencyPluginDefine): RuntimeDependencyPluginDefine[] {
|
||||||
|
const resolved: RuntimeDependencyPluginDefine[] = [];
|
||||||
|
const visited = new Set<string>();
|
||||||
|
|
||||||
|
const visit = (item: RuntimeDependencyPluginDefine) => {
|
||||||
|
const key = this.buildPluginDependencyKey(item);
|
||||||
|
if (visited.has(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
visited.add(key);
|
||||||
|
resolved.push(item);
|
||||||
|
for (const [dependencyName, expectedRange] of Object.entries(item.dependPlugins || {})) {
|
||||||
|
const dependency = this.getDefineByPluginKey(dependencyName, item);
|
||||||
|
if (!isPluginVersionCompatible(dependency, expectedRange)) {
|
||||||
|
throw new Error(`插件依赖版本冲突: ${item.name} 依赖 ${dependencyName}@${expectedRange},当前版本为 ${dependency.version || "未声明"}`);
|
||||||
|
}
|
||||||
|
visit(dependency);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
visit(current);
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildPluginDependencyKey(plugin: RuntimeDependencyPluginDefine) {
|
||||||
|
if (plugin.pluginType === "addon" && plugin.addonType) {
|
||||||
|
return `addon:${plugin.addonType}:${plugin.name}`;
|
||||||
|
}
|
||||||
|
const pluginType = plugin.pluginType === "deploy" ? "plugin" : plugin.pluginType || "unknown";
|
||||||
|
return `${pluginType}:${plugin.name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getDefineByPluginKey(pluginKey: string, owner?: RuntimeDependencyPluginDefine): RuntimeDependencyPluginDefine {
|
||||||
|
const parts = pluginKey.split(":");
|
||||||
|
const [pluginType, subtype, name] = parts;
|
||||||
|
if (parts.length < 2 || (pluginType === "addon" && parts.length !== 3) || (pluginType !== "addon" && parts.length !== 2)) {
|
||||||
|
const ownerName = owner?.name || pluginKey;
|
||||||
|
throw new Error(`插件依赖格式错误: ${ownerName} 依赖 ${pluginKey},请使用 plugin:name、access:name、notification:name、dnsProvider:name 或 addon:subtype:name 格式`);
|
||||||
|
}
|
||||||
|
const registryMap: Record<string, { registry: Registry<any>; key: string; pluginType: string; addonType?: string }> = {
|
||||||
|
plugin: { registry: pluginRegistry, key: subtype, pluginType: "plugin" },
|
||||||
|
access: { registry: accessRegistry, key: subtype, pluginType: "access" },
|
||||||
|
notification: { registry: notificationRegistry, key: subtype, pluginType: "notification" },
|
||||||
|
dnsProvider: { registry: dnsProviderRegistry, key: subtype, pluginType: "dnsProvider" },
|
||||||
|
addon: { registry: addonRegistry, key: `${subtype}:${name}`, pluginType: "addon", addonType: subtype },
|
||||||
|
};
|
||||||
|
const target = registryMap[pluginType];
|
||||||
|
if (!target) {
|
||||||
|
const ownerName = owner?.name || pluginKey;
|
||||||
|
throw new Error(`插件依赖格式错误: ${ownerName} 依赖 ${pluginKey},未知插件类型 ${pluginType}`);
|
||||||
|
}
|
||||||
|
const define = target.registry.getDefine(target.key) as RegisteredDefineLike;
|
||||||
|
if (!define) {
|
||||||
|
throw new Error(`插件依赖缺失: ${owner?.name || pluginKey} 依赖 ${pluginKey},但该插件未注册或已禁用`);
|
||||||
|
}
|
||||||
|
return { ...define, key: pluginKey, pluginType: target.pluginType, addonType: target.addonType };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async withInstallLock<T>(run: () => Promise<T>): Promise<T> {
|
||||||
|
const rootDir = this.getRuntimeDepsRootDir();
|
||||||
|
fs.mkdirSync(rootDir, { recursive: true });
|
||||||
|
const lockFile = path.join(rootDir, ".install.lock");
|
||||||
|
const previous = PROCESS_LOCKS.get(lockFile);
|
||||||
|
if (previous) {
|
||||||
|
await previous.catch(() => undefined);
|
||||||
|
}
|
||||||
|
let releaseProcessLock!: () => void;
|
||||||
|
const current = new Promise<void>(resolve => {
|
||||||
|
releaseProcessLock = resolve;
|
||||||
|
});
|
||||||
|
PROCESS_LOCKS.set(lockFile, current);
|
||||||
|
let fd: number | undefined;
|
||||||
|
try {
|
||||||
|
fd = await this.acquireFileLock(lockFile);
|
||||||
|
return await run();
|
||||||
|
} finally {
|
||||||
|
if (fd != null) {
|
||||||
|
fs.closeSync(fd);
|
||||||
|
fs.rmSync(lockFile, { force: true });
|
||||||
|
}
|
||||||
|
releaseProcessLock();
|
||||||
|
if (PROCESS_LOCKS.get(lockFile) === current) {
|
||||||
|
PROCESS_LOCKS.delete(lockFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async acquireFileLock(lockFile: string) {
|
||||||
|
const deadline = Date.now() + this.installTimeoutMs;
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
const fd = fs.openSync(lockFile, "wx");
|
||||||
|
// @ts-ignore
|
||||||
|
fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }), "utf8");
|
||||||
|
return fd;
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error?.code !== "EEXIST") {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
if (Date.now() > deadline) {
|
||||||
|
throw new Error(`动态依赖安装锁等待超时: ${lockFile}`);
|
||||||
|
}
|
||||||
|
await this.waitForExternalLock(lockFile, deadline);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async waitForExternalLock(lockFile: string, deadline: number) {
|
||||||
|
while (fs.existsSync(lockFile)) {
|
||||||
|
if (Date.now() > deadline) {
|
||||||
|
throw new Error(`动态依赖安装锁等待超时: ${lockFile}`);
|
||||||
|
}
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 300));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearRuntimeDeps() {
|
||||||
|
const rootDir = this.getRuntimeDepsRootDir();
|
||||||
|
const normalizedRootDir = path.normalize(rootDir);
|
||||||
|
if (!normalizedRootDir.endsWith(path.normalize(".runtime-deps"))) {
|
||||||
|
throw new Error(`动态依赖目录配置异常,拒绝清理: ${rootDir}`);
|
||||||
|
}
|
||||||
|
await this.withInstallLock(async () => {
|
||||||
|
if (fs.existsSync(rootDir)) {
|
||||||
|
const entries = fs.readdirSync(rootDir);
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry === ".install.lock") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const entryPath = path.join(rootDir, entry);
|
||||||
|
fs.rmSync(entryPath, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.installPromises.clear();
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private readInstallState(statePath: string): any {
|
||||||
|
if (!fs.existsSync(statePath)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(fs.readFileSync(statePath, "utf8"));
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private writeInstallState(statePath: string, state: any) {
|
||||||
|
fs.writeFileSync(statePath, JSON.stringify(state, null, 2), "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
private readManifestDependencies(packageJsonPath: string): Record<string, string> {
|
||||||
|
if (!fs.existsSync(packageJsonPath)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const manifest = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
||||||
|
return manifest.dependencies || {};
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private mergeInstalledDependencies(installed: Record<string, string>, requested: Record<string, string>) {
|
||||||
|
const dependencies = { ...installed };
|
||||||
|
for (const [packageName, range] of Object.entries(requested)) {
|
||||||
|
const installedRange = dependencies[packageName];
|
||||||
|
if (installedRange && !areRangesCompatible(installedRange, range)) {
|
||||||
|
throw new Error(`动态依赖版本冲突: ${packageName} => installed:${installedRange}, requested:${range}`);
|
||||||
|
}
|
||||||
|
dependencies[packageName] = installedRange || range;
|
||||||
|
}
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
private async getPnpmVersion(command: string, env: NodeJS.ProcessEnv) {
|
||||||
|
const rootDir = this.getRuntimeDepsRootDir();
|
||||||
|
const result = await this.commandRunner.run(command, ["--version"], {
|
||||||
|
cwd: rootDir,
|
||||||
|
timeoutMs: Math.min(this.installTimeoutMs, 10000),
|
||||||
|
env,
|
||||||
|
});
|
||||||
|
if (result.code !== 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return (result.stdout || result.stderr || "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPnpmCommand() {
|
||||||
|
if (this.pnpmCommand) {
|
||||||
|
return this.pnpmCommand;
|
||||||
|
}
|
||||||
|
return "pnpm";
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildChildEnv(registryUrl: string) {
|
||||||
|
// @ts-ignore
|
||||||
|
const env = { ...process.env };
|
||||||
|
for (const key of ["NODE_OPTIONS", "VSCODE_INSPECTOR_OPTIONS", "NODE_INSPECTOR_PORT", "NODE_DEBUG"]) {
|
||||||
|
if (!env[key]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (key === "NODE_OPTIONS") {
|
||||||
|
env[key] = this.stripDebugNodeOptions(env[key] as string);
|
||||||
|
} else {
|
||||||
|
delete env[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (registryUrl) {
|
||||||
|
env.npm_config_registry = registryUrl;
|
||||||
|
env.pnpm_config_registry = registryUrl;
|
||||||
|
}
|
||||||
|
env.CI = env.CI || "true";
|
||||||
|
env.npm_config_confirm_modules_purge = "false";
|
||||||
|
env.pnpm_config_confirm_modules_purge = "false";
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
|
||||||
|
private stripDebugNodeOptions(value: string) {
|
||||||
|
return value
|
||||||
|
.split(/\s+/)
|
||||||
|
.filter(Boolean)
|
||||||
|
.filter(item => !/^--inspect(-brk|-port)?(=|$)/.test(item))
|
||||||
|
.filter(item => !/^--debug(=|$)/.test(item))
|
||||||
|
.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
private getRuntimeDepsRootDir() {
|
||||||
|
return path.resolve(this.runtimeDepsRootDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
private createDependenciesHash(dependencies: Record<string, string>) {
|
||||||
|
return crypto.createHash("sha256").update(JSON.stringify(dependencies)).digest("hex");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPluginVersionCompatible(plugin: RuntimeDependencyPluginDefine, expectedRange: string) {
|
||||||
|
if (!expectedRange || expectedRange === "*") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!plugin.version) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return areRangesCompatible(expectedRange, plugin.version);
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import assert from "assert";
|
||||||
|
import esmock from "esmock";
|
||||||
|
|
||||||
|
describe("NetTestService.telnet", () => {
|
||||||
|
it("treats nc succeeded output as a successful port connection", async () => {
|
||||||
|
const { NetTestService } = await esmock("./nettest-service.js", {
|
||||||
|
"@certd/basic": {
|
||||||
|
http: {},
|
||||||
|
logger: {
|
||||||
|
error() {},
|
||||||
|
},
|
||||||
|
utils: {
|
||||||
|
sp: {
|
||||||
|
async spawn() {
|
||||||
|
return "Connection to baidu.com (110.242.74.102) 443 port [tcp/*] succeeded!";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const service = new NetTestService();
|
||||||
|
(service as any).isWindows = () => false;
|
||||||
|
|
||||||
|
const result = await service.telnet("baidu.com", 443);
|
||||||
|
|
||||||
|
assert.equal(result.success, true);
|
||||||
|
assert.equal(result.message, "端口连接测试成功");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -40,7 +40,8 @@ export class NetTestService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 判断测试是否成功
|
// 判断测试是否成功
|
||||||
const success = this.isWindows() ? output.includes("端口连接成功") : output.includes(" open");
|
const normalizedOutput = output.toLowerCase();
|
||||||
|
const success = this.isWindows() ? normalizedOutput.includes("端口连接成功") : normalizedOutput.includes("succeeded!") || normalizedOutput.includes("connected to") || normalizedOutput.includes(" open");
|
||||||
|
|
||||||
// 处理结果
|
// 处理结果
|
||||||
return {
|
return {
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ export class AliyunDnsProvider extends AbstractDnsProvider {
|
|||||||
async onInstance() {
|
async onInstance() {
|
||||||
const access: AliyunAccess = this.ctx.access as AliyunAccess;
|
const access: AliyunAccess = this.ctx.access as AliyunAccess;
|
||||||
|
|
||||||
this.client = new AliyunClient({ logger: this.logger });
|
this.client = new AliyunClient({ logger: this.logger, importRuntime: access.importRuntime.bind(access) });
|
||||||
await this.client.init({
|
await this.client.init({
|
||||||
accessKeyId: access.accessKeyId,
|
accessKeyId: access.accessKeyId,
|
||||||
accessKeySecret: access.accessKeySecret,
|
accessKeySecret: access.accessKeySecret,
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ export class DeployCertToAliyunAckPlugin extends AbstractTaskPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getClient(aliyunProvider: any, regionId: string) {
|
async getClient(aliyunProvider: any, regionId: string) {
|
||||||
const client = new AliyunClient({ logger: this.logger, useROAClient: true });
|
const client = new AliyunClient({ logger: this.logger, useROAClient: true, importRuntime: aliyunProvider.importRuntime.bind(aliyunProvider) });
|
||||||
await client.init({
|
await client.init({
|
||||||
accessKeyId: aliyunProvider.accessKeyId,
|
accessKeyId: aliyunProvider.accessKeyId,
|
||||||
accessKeySecret: aliyunProvider.accessKeySecret,
|
accessKeySecret: aliyunProvider.accessKeySecret,
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ export class AliyunDeployCertToALB extends AbstractTaskPlugin {
|
|||||||
async onInstance() {}
|
async onInstance() {}
|
||||||
|
|
||||||
async getLBClient(access: AliyunAccess, region: string) {
|
async getLBClient(access: AliyunAccess, region: string) {
|
||||||
const client = new AliyunClient({ logger: this.logger });
|
const client = new AliyunClient({ logger: this.logger, importRuntime: access.importRuntime.bind(access) });
|
||||||
|
|
||||||
const version = "2020-06-16";
|
const version = "2020-06-16";
|
||||||
await client.init({
|
await client.init({
|
||||||
|
|||||||
@@ -227,7 +227,11 @@ export class DeployCertToAliyunApig extends AbstractTaskPlugin {
|
|||||||
domain: item.name,
|
domain: item.name,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return optionsUtils.buildGroupOptions(options, this.certDomains);
|
const records = optionsUtils.buildGroupOptions(options, this.certDomains);
|
||||||
|
return {
|
||||||
|
list: records,
|
||||||
|
total: res?.data?.totalSize || 0,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async onGetRegionList(data: any) {
|
async onGetRegionList(data: any) {
|
||||||
|
|||||||
+19
-4
@@ -1,4 +1,4 @@
|
|||||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
import { AbstractTaskPlugin, IsTaskPlugin, Pager, PageSearch, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||||
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||||
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
||||||
import { optionsUtils } from "@certd/basic";
|
import { optionsUtils } from "@certd/basic";
|
||||||
@@ -70,6 +70,8 @@ export class DeployCertToAliyunApiGateway extends AbstractTaskPlugin {
|
|||||||
watches: ["regionEndpoint", "accessId"],
|
watches: ["regionEndpoint", "accessId"],
|
||||||
required: true,
|
required: true,
|
||||||
single: true,
|
single: true,
|
||||||
|
pager: true,
|
||||||
|
search: true,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
groupId!: string;
|
groupId!: string;
|
||||||
@@ -122,7 +124,7 @@ export class DeployCertToAliyunApiGateway extends AbstractTaskPlugin {
|
|||||||
this.logger.info(`设置${domainName}证书成功:`, ret.RequestId);
|
this.logger.info(`设置${domainName}证书成功:`, ret.RequestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onGetGroupList(data: any) {
|
async onGetGroupList(data: PageSearch) {
|
||||||
if (!this.accessId) {
|
if (!this.accessId) {
|
||||||
throw new Error("请选择Access授权");
|
throw new Error("请选择Access授权");
|
||||||
}
|
}
|
||||||
@@ -131,23 +133,36 @@ export class DeployCertToAliyunApiGateway extends AbstractTaskPlugin {
|
|||||||
}
|
}
|
||||||
const access = await this.getAccess<AliyunAccess>(this.accessId);
|
const access = await this.getAccess<AliyunAccess>(this.accessId);
|
||||||
const client = access.getClient(this.regionEndpoint);
|
const client = access.getClient(this.regionEndpoint);
|
||||||
|
|
||||||
|
const pager = new Pager(data);
|
||||||
const res = await client.doRequest({
|
const res = await client.doRequest({
|
||||||
// 接口名称
|
// 接口名称
|
||||||
action: "DescribeApiGroups",
|
action: "DescribeApiGroups",
|
||||||
// 接口版本
|
// 接口版本
|
||||||
version: "2016-07-14",
|
version: "2016-07-14",
|
||||||
data: {},
|
data: {
|
||||||
|
query: {
|
||||||
|
GroupName: data.searchKey,
|
||||||
|
PageNumber: pager.pageNo,
|
||||||
|
PageSize: pager.pageSize,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const list = res?.ApiGroupAttributes?.ApiGroupAttribute;
|
const list = res?.ApiGroupAttributes?.ApiGroupAttribute;
|
||||||
if (!list || list.length === 0) {
|
if (!list || list.length === 0) {
|
||||||
throw new Error("没有数据,您可以手动输入API网关ID");
|
throw new Error("没有数据,您可以手动输入API网关ID");
|
||||||
}
|
}
|
||||||
return list.map((item: any) => {
|
const records = list.map((item: any) => {
|
||||||
return {
|
return {
|
||||||
value: item.GroupId,
|
value: item.GroupId,
|
||||||
label: `${item.GroupName}<${item.GroupId}>`,
|
label: `${item.GroupName}<${item.GroupId}>`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
list: records,
|
||||||
|
total: res?.TotalCount || 0,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async onGetDomainList(data: any) {
|
async onGetDomainList(data: any) {
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ export class DeployCertToAliyunCDN extends AbstractTaskPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getClient(access: AliyunAccess) {
|
async getClient(access: AliyunAccess) {
|
||||||
const client = new AliyunClient({ logger: this.logger });
|
const client = new AliyunClient({ logger: this.logger, importRuntime: access.importRuntime.bind(access) });
|
||||||
await client.init({
|
await client.init({
|
||||||
accessKeyId: access.accessKeyId,
|
accessKeyId: access.accessKeyId,
|
||||||
accessKeySecret: access.accessKeySecret,
|
accessKeySecret: access.accessKeySecret,
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ export class DeployCertToAliyunDCDN extends AbstractTaskPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getClient(access: AliyunAccess) {
|
async getClient(access: AliyunAccess) {
|
||||||
const client = new AliyunClient({ logger: this.logger });
|
const client = new AliyunClient({ logger: this.logger, importRuntime: access.importRuntime.bind(access) });
|
||||||
await client.init({
|
await client.init({
|
||||||
accessKeyId: access.accessKeyId,
|
accessKeyId: access.accessKeyId,
|
||||||
accessKeySecret: access.accessKeySecret,
|
accessKeySecret: access.accessKeySecret,
|
||||||
|
|||||||
@@ -137,8 +137,8 @@ export class AliyunDeployCertToFC extends AbstractTaskPlugin {
|
|||||||
|
|
||||||
const client = await this.getClient(access);
|
const client = await this.getClient(access);
|
||||||
|
|
||||||
const $Util = await import("@alicloud/tea-util");
|
const $Util = await access.importRuntime("@alicloud/tea-util");
|
||||||
const $OpenApi = await import("@alicloud/openapi-client");
|
const $OpenApi = await access.importRuntime("@alicloud/openapi-client");
|
||||||
|
|
||||||
let privateKey = this.cert.key;
|
let privateKey = this.cert.key;
|
||||||
try {
|
try {
|
||||||
@@ -204,7 +204,7 @@ export class AliyunDeployCertToFC extends AbstractTaskPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getClient(access: AliyunAccess) {
|
async getClient(access: AliyunAccess) {
|
||||||
const $OpenApi = await import("@alicloud/openapi-client");
|
const $OpenApi = await access.importRuntime("@alicloud/openapi-client");
|
||||||
const config = new $OpenApi.Config({
|
const config = new $OpenApi.Config({
|
||||||
accessKeyId: access.accessKeyId,
|
accessKeyId: access.accessKeyId,
|
||||||
accessKeySecret: access.accessKeySecret,
|
accessKeySecret: access.accessKeySecret,
|
||||||
@@ -221,8 +221,8 @@ export class AliyunDeployCertToFC extends AbstractTaskPlugin {
|
|||||||
const access = await this.getAccess<AliyunAccess>(this.accessId);
|
const access = await this.getAccess<AliyunAccess>(this.accessId);
|
||||||
const client = await this.getClient(access);
|
const client = await this.getClient(access);
|
||||||
|
|
||||||
const $OpenApi = await import("@alicloud/openapi-client");
|
const $OpenApi = await access.importRuntime("@alicloud/openapi-client");
|
||||||
const $Util = await import("@alicloud/tea-util");
|
const $Util = await access.importRuntime("@alicloud/tea-util");
|
||||||
const params = new $OpenApi.Params({
|
const params = new $OpenApi.Params({
|
||||||
// 接口名称
|
// 接口名称
|
||||||
action: "ListCustomDomains",
|
action: "ListCustomDomains",
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export class AliyunDeployCertToNLB extends AbstractTaskPlugin {
|
|||||||
async onInstance() {}
|
async onInstance() {}
|
||||||
|
|
||||||
async getLBClient(access: AliyunAccess, region: string) {
|
async getLBClient(access: AliyunAccess, region: string) {
|
||||||
const client = new AliyunClient({ logger: this.logger });
|
const client = new AliyunClient({ logger: this.logger, importRuntime: access.importRuntime.bind(access) });
|
||||||
|
|
||||||
const version = "2022-04-30";
|
const version = "2022-04-30";
|
||||||
await client.init({
|
await client.init({
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ export class DeployCertToAliyunOSS extends AbstractTaskPlugin {
|
|||||||
|
|
||||||
async getClient(access: AliyunAccess) {
|
async getClient(access: AliyunAccess) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const OSS = await import("ali-oss");
|
const OSS = await access.importRuntime("ali-oss");
|
||||||
return new OSS.default({
|
return new OSS.default({
|
||||||
accessKeyId: access.accessKeyId,
|
accessKeyId: access.accessKeyId,
|
||||||
accessKeySecret: access.accessKeySecret,
|
accessKeySecret: access.accessKeySecret,
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ export class AliyunDeployCertToSLB extends AbstractTaskPlugin {
|
|||||||
async onInstance() {}
|
async onInstance() {}
|
||||||
|
|
||||||
async getLBClient(access: AliyunAccess, region: string) {
|
async getLBClient(access: AliyunAccess, region: string) {
|
||||||
const client = new AliyunClient({ logger: this.logger });
|
const client = new AliyunClient({ logger: this.logger, importRuntime: access.importRuntime.bind(access) });
|
||||||
const version = "2014-05-15";
|
const version = "2014-05-15";
|
||||||
await client.init({
|
await client.init({
|
||||||
accessKeyId: access.accessKeyId,
|
accessKeyId: access.accessKeyId,
|
||||||
|
|||||||
+1
-1
@@ -103,7 +103,7 @@ export class AliyunDeployCertToWafCloud extends AbstractTaskPlugin {
|
|||||||
async onInstance() {}
|
async onInstance() {}
|
||||||
|
|
||||||
async getWafClient(access: AliyunAccess) {
|
async getWafClient(access: AliyunAccess) {
|
||||||
const client = new AliyunClient({ logger: this.logger });
|
const client = new AliyunClient({ logger: this.logger, importRuntime: access.importRuntime.bind(access) });
|
||||||
await client.init({
|
await client.init({
|
||||||
accessKeyId: access.accessKeyId,
|
accessKeyId: access.accessKeyId,
|
||||||
accessKeySecret: access.accessKeySecret,
|
accessKeySecret: access.accessKeySecret,
|
||||||
|
|||||||
+1
-1
@@ -115,7 +115,7 @@ export class AliyunDeployCertToWaf extends AbstractTaskPlugin {
|
|||||||
async onInstance() {}
|
async onInstance() {}
|
||||||
|
|
||||||
async getWafClient(access: AliyunAccess) {
|
async getWafClient(access: AliyunAccess) {
|
||||||
const client = new AliyunClient({ logger: this.logger });
|
const client = new AliyunClient({ logger: this.logger, importRuntime: access.importRuntime.bind(access) });
|
||||||
await client.init({
|
await client.init({
|
||||||
accessKeyId: access.accessKeyId,
|
accessKeyId: access.accessKeyId,
|
||||||
accessKeySecret: access.accessKeySecret,
|
accessKeySecret: access.accessKeySecret,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export class AwsIAMClient {
|
|||||||
}
|
}
|
||||||
async importCertificate(certInfo: CertInfo, certName: string) {
|
async importCertificate(certInfo: CertInfo, certName: string) {
|
||||||
// 创建 IAM 客户端
|
// 创建 IAM 客户端
|
||||||
const { IAMClient, UploadServerCertificateCommand } = await import("@aws-sdk/client-iam");
|
const { IAMClient, UploadServerCertificateCommand } = await this.access.importRuntime("@aws-sdk/client-iam");
|
||||||
const iamClient = new IAMClient({
|
const iamClient = new IAMClient({
|
||||||
region: this.region, // 替换为您的 AWS 区域
|
region: this.region, // 替换为您的 AWS 区域
|
||||||
credentials: {
|
credentials: {
|
||||||
|
|||||||
+2
-2
@@ -84,7 +84,7 @@ export class AwsCNDeployToCloudFront extends AbstractTaskPlugin {
|
|||||||
}
|
}
|
||||||
//部署到CloudFront
|
//部署到CloudFront
|
||||||
|
|
||||||
const { CloudFrontClient, UpdateDistributionCommand, GetDistributionConfigCommand } = await import("@aws-sdk/client-cloudfront");
|
const { CloudFrontClient, UpdateDistributionCommand, GetDistributionConfigCommand } = await this.importRuntime("@aws-sdk/client-cloudfront");
|
||||||
const cloudFrontClient = new CloudFrontClient({
|
const cloudFrontClient = new CloudFrontClient({
|
||||||
region: this.region,
|
region: this.region,
|
||||||
credentials: {
|
credentials: {
|
||||||
@@ -135,7 +135,7 @@ export class AwsCNDeployToCloudFront extends AbstractTaskPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const access = await this.getAccess<AwsCNAccess>(this.accessId);
|
const access = await this.getAccess<AwsCNAccess>(this.accessId);
|
||||||
const { CloudFrontClient, ListDistributionsCommand } = await import("@aws-sdk/client-cloudfront");
|
const { CloudFrontClient, ListDistributionsCommand } = await this.importRuntime("@aws-sdk/client-cloudfront");
|
||||||
const cloudFrontClient = new CloudFrontClient({
|
const cloudFrontClient = new CloudFrontClient({
|
||||||
region: this.region,
|
region: this.region,
|
||||||
credentials: {
|
credentials: {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export class AwsClient {
|
|||||||
}
|
}
|
||||||
async importCertificate(certInfo: CertInfo) {
|
async importCertificate(certInfo: CertInfo) {
|
||||||
// 创建 ACM 客户端
|
// 创建 ACM 客户端
|
||||||
const { ACMClient, ImportCertificateCommand } = await import("@aws-sdk/client-acm");
|
const { ACMClient, ImportCertificateCommand } = await this.access.importRuntime("@aws-sdk/client-acm");
|
||||||
const acmClient = new ACMClient({
|
const acmClient = new ACMClient({
|
||||||
region: this.region, // 替换为您的 AWS 区域
|
region: this.region, // 替换为您的 AWS 区域
|
||||||
credentials: {
|
credentials: {
|
||||||
@@ -49,7 +49,7 @@ export class AwsClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getCallerIdentity() {
|
async getCallerIdentity() {
|
||||||
const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
|
const { STSClient, GetCallerIdentityCommand } = await this.access.importRuntime("@aws-sdk/client-sts");
|
||||||
|
|
||||||
const client = new STSClient({
|
const client = new STSClient({
|
||||||
region: this.access.region || "us-east-1",
|
region: this.access.region || "us-east-1",
|
||||||
@@ -68,7 +68,7 @@ export class AwsClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async route53ClientGet() {
|
async route53ClientGet() {
|
||||||
const { Route53Client } = await import("@aws-sdk/client-route-53");
|
const { Route53Client } = await this.access.importRuntime("@aws-sdk/client-route-53");
|
||||||
return new Route53Client({
|
return new Route53Client({
|
||||||
region: this.region,
|
region: this.region,
|
||||||
credentials: {
|
credentials: {
|
||||||
@@ -88,7 +88,7 @@ export class AwsClient {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
async route53ListHostedZones(name: string): Promise<{ Id: string; Name: string }[]> {
|
async route53ListHostedZones(name: string): Promise<{ Id: string; Name: string }[]> {
|
||||||
const { ListHostedZonesByNameCommand } = await import("@aws-sdk/client-route-53"); // ES Modules import
|
const { ListHostedZonesByNameCommand } = await this.access.importRuntime("@aws-sdk/client-route-53"); // ES Modules import
|
||||||
|
|
||||||
const client = await this.route53ClientGet();
|
const client = await this.route53ClientGet();
|
||||||
const input = {
|
const input = {
|
||||||
@@ -96,7 +96,7 @@ export class AwsClient {
|
|||||||
DNSName: name,
|
DNSName: name,
|
||||||
};
|
};
|
||||||
const command = new ListHostedZonesByNameCommand(input);
|
const command = new ListHostedZonesByNameCommand(input);
|
||||||
const response = await this.doRequest(() => client.send(command));
|
const response: any = await this.doRequest(() => client.send(command));
|
||||||
if (response.HostedZones.length === 0) {
|
if (response.HostedZones.length === 0) {
|
||||||
throw new Error(`找不到 HostedZone ${name}`);
|
throw new Error(`找不到 HostedZone ${name}`);
|
||||||
}
|
}
|
||||||
@@ -105,7 +105,7 @@ export class AwsClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async route53ListHostedZonesPage(req: PageSearch): Promise<PageRes<DomainRecord>> {
|
async route53ListHostedZonesPage(req: PageSearch): Promise<PageRes<DomainRecord>> {
|
||||||
const { ListHostedZonesByNameCommand } = await import("@aws-sdk/client-route-53"); // ES Modules import
|
const { ListHostedZonesByNameCommand } = await this.access.importRuntime("@aws-sdk/client-route-53"); // ES Modules import
|
||||||
|
|
||||||
const client = await this.route53ClientGet();
|
const client = await this.route53ClientGet();
|
||||||
const input: any = {
|
const input: any = {
|
||||||
@@ -116,7 +116,7 @@ export class AwsClient {
|
|||||||
input.DNSName = req.searchKey;
|
input.DNSName = req.searchKey;
|
||||||
}
|
}
|
||||||
const command = new ListHostedZonesByNameCommand(input);
|
const command = new ListHostedZonesByNameCommand(input);
|
||||||
const response = await this.doRequest(() => client.send(command));
|
const response: any = await this.doRequest(() => client.send(command));
|
||||||
let list: any[] = response.HostedZones || [];
|
let list: any[] = response.HostedZones || [];
|
||||||
list = list.map((item: any) => ({
|
list = list.map((item: any) => ({
|
||||||
id: item.Id.replace("/hostedzone/", ""),
|
id: item.Id.replace("/hostedzone/", ""),
|
||||||
@@ -129,7 +129,7 @@ export class AwsClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async route53ChangeRecord(req: { hostedZoneId: string; fullRecord: string; type: string; value: string; action: "UPSERT" | "DELETE" }) {
|
async route53ChangeRecord(req: { hostedZoneId: string; fullRecord: string; type: string; value: string; action: "UPSERT" | "DELETE" }) {
|
||||||
const { ChangeResourceRecordSetsCommand } = await import("@aws-sdk/client-route-53"); // ES Modules import
|
const { ChangeResourceRecordSetsCommand } = await this.access.importRuntime("@aws-sdk/client-route-53"); // ES Modules import
|
||||||
// const { Route53Client, ChangeResourceRecordSetsCommand } = require("@aws-sdk/client-route-53"); // CommonJS import
|
// const { Route53Client, ChangeResourceRecordSetsCommand } = require("@aws-sdk/client-route-53"); // CommonJS import
|
||||||
// import type { Route53ClientConfig } from "@aws-sdk/client-route-53";
|
// import type { Route53ClientConfig } from "@aws-sdk/client-route-53";
|
||||||
const client = await this.route53ClientGet();
|
const client = await this.route53ClientGet();
|
||||||
|
|||||||
+2
-2
@@ -79,7 +79,7 @@ export class AwsDeployToCloudFront extends AbstractTaskPlugin {
|
|||||||
}
|
}
|
||||||
//部署到CloudFront
|
//部署到CloudFront
|
||||||
|
|
||||||
const { CloudFrontClient, UpdateDistributionCommand, GetDistributionConfigCommand } = await import("@aws-sdk/client-cloudfront");
|
const { CloudFrontClient, UpdateDistributionCommand, GetDistributionConfigCommand } = await this.importRuntime("@aws-sdk/client-cloudfront");
|
||||||
const cloudFrontClient = new CloudFrontClient({
|
const cloudFrontClient = new CloudFrontClient({
|
||||||
region: this.region,
|
region: this.region,
|
||||||
credentials: {
|
credentials: {
|
||||||
@@ -133,7 +133,7 @@ export class AwsDeployToCloudFront extends AbstractTaskPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const access = await this.getAccess<AwsAccess>(this.accessId);
|
const access = await this.getAccess<AwsAccess>(this.accessId);
|
||||||
const { CloudFrontClient, ListDistributionsCommand } = await import("@aws-sdk/client-cloudfront");
|
const { CloudFrontClient, ListDistributionsCommand } = await this.importRuntime("@aws-sdk/client-cloudfront");
|
||||||
const cloudFrontClient = new CloudFrontClient({
|
const cloudFrontClient = new CloudFrontClient({
|
||||||
region: this.region,
|
region: this.region,
|
||||||
credentials: {
|
credentials: {
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ export class AzureAccess extends BaseAccess {
|
|||||||
|
|
||||||
this.ctx.logger.info(`找到 DNS 区域: ${matchingZone.name}, ID: ${matchingZone.id}`);
|
this.ctx.logger.info(`找到 DNS 区域: ${matchingZone.name}, ID: ${matchingZone.id}`);
|
||||||
return {
|
return {
|
||||||
id: matchingZone.id.split("/").pop()!,
|
id: matchingZone.id.split("/").pop() || "",
|
||||||
name: matchingZone.name,
|
name: matchingZone.name,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -136,7 +136,7 @@ export class AzureAccess extends BaseAccess {
|
|||||||
}
|
}
|
||||||
|
|
||||||
list = list.map((item: any) => ({
|
list = list.map((item: any) => ({
|
||||||
id: item.id.split("/").pop()!,
|
id: item.id.split("/").pop() || "",
|
||||||
domain: item.name,
|
domain: item.name,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import { TencentAccess } from "../../plugin-lib/tencent/access.js";
|
|||||||
title: "腾讯云验证码",
|
title: "腾讯云验证码",
|
||||||
desc: "",
|
desc: "",
|
||||||
showTest: false,
|
showTest: false,
|
||||||
|
dependPackages: {
|
||||||
|
"tencentcloud-sdk-nodejs": "^4.1.112",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export class TencentCaptcha extends BaseAddon implements ICaptchaAddon {
|
export class TencentCaptcha extends BaseAddon implements ICaptchaAddon {
|
||||||
@AddonInput({
|
@AddonInput({
|
||||||
@@ -50,7 +53,7 @@ export class TencentCaptcha extends BaseAddon implements ICaptchaAddon {
|
|||||||
|
|
||||||
const access = await this.getAccess<TencentAccess>(this.accessId);
|
const access = await this.getAccess<TencentAccess>(this.accessId);
|
||||||
|
|
||||||
const sdk = await import("tencentcloud-sdk-nodejs/tencentcloud/services/captcha/v20190722/index.js");
|
const sdk = await this.importRuntime("tencentcloud-sdk-nodejs/tencentcloud/services/captcha/v20190722/index.js");
|
||||||
|
|
||||||
const CaptchaClient = sdk.v20190722.Client;
|
const CaptchaClient = sdk.v20190722.Client;
|
||||||
|
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ export class AcmeAccountAccess extends BaseAccess {
|
|||||||
eabHmacKey = "";
|
eabHmacKey = "";
|
||||||
|
|
||||||
@AccessInput({
|
@AccessInput({
|
||||||
title: "ACME账号信息",
|
title: "生成ACME账号",
|
||||||
component: {
|
component: {
|
||||||
name: "refresh-input",
|
name: "refresh-input",
|
||||||
action: "GenerateAccount",
|
action: "GenerateAccount",
|
||||||
@@ -149,7 +149,7 @@ export class AcmeAccountAccess extends BaseAccess {
|
|||||||
},
|
},
|
||||||
col: { span: 24 },
|
col: { span: 24 },
|
||||||
required: true,
|
required: true,
|
||||||
helper: "请生成ACME账号,账号一旦生成不允许修改",
|
helper: "请点击右边按钮生成ACME账号,账号一旦生成不允许修改",
|
||||||
encrypt: true,
|
encrypt: true,
|
||||||
mergeScript: `
|
mergeScript: `
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -377,7 +377,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
|
|||||||
type: "acmeAccount",
|
type: "acmeAccount",
|
||||||
},
|
},
|
||||||
required: false,
|
required: false,
|
||||||
helper: "请选择颁发机构对应的ACME账号",
|
helper: "直接本地生成,无需外部注册\n点击选择按钮->添加->填写邮箱->生成账号即可",
|
||||||
mergeScript: `
|
mergeScript: `
|
||||||
return {
|
return {
|
||||||
show: ctx.compute(({form})=>{
|
show: ctx.compute(({form})=>{
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ export class CmccClient {
|
|||||||
async getToken(): Promise<string> {
|
async getToken(): Promise<string> {
|
||||||
// 检查是否有有效的token
|
// 检查是否有有效的token
|
||||||
if (this.isTokenValid()) {
|
if (this.isTokenValid()) {
|
||||||
return this.token!;
|
return this.token;
|
||||||
}
|
}
|
||||||
|
|
||||||
const datetime = this.getCurrentIsoTime();
|
const datetime = this.getCurrentIsoTime();
|
||||||
|
|||||||
@@ -36,10 +36,10 @@ export class DynadotDnsProvider extends AbstractDnsProvider<DynadotRecord> {
|
|||||||
record_type: type.toLowerCase(),
|
record_type: type.toLowerCase(),
|
||||||
record_value1: value,
|
record_value1: value,
|
||||||
record_value2: "",
|
record_value2: "",
|
||||||
}
|
},
|
||||||
]
|
];
|
||||||
|
|
||||||
await this.postRecords(domain, {subRecords, mainRecords: [], addToCurrent: true});
|
await this.postRecords(domain, { subRecords, mainRecords: [], addToCurrent: true });
|
||||||
|
|
||||||
this.logger.info("添加域名解析成功:", fullRecord, value);
|
this.logger.info("添加域名解析成功:", fullRecord, value);
|
||||||
return {
|
return {
|
||||||
@@ -79,8 +79,8 @@ export class DynadotDnsProvider extends AbstractDnsProvider<DynadotRecord> {
|
|||||||
record_type: "txt",
|
record_type: "txt",
|
||||||
record_value1: "init_txt_by_certd",
|
record_value1: "init_txt_by_certd",
|
||||||
record_value2: "",
|
record_value2: "",
|
||||||
}
|
},
|
||||||
]
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.postRecords(domain, {
|
await this.postRecords(domain, {
|
||||||
@@ -132,7 +132,7 @@ export class DynadotDnsProvider extends AbstractDnsProvider<DynadotRecord> {
|
|||||||
return { mainRecords, subRecords };
|
return { mainRecords, subRecords };
|
||||||
}
|
}
|
||||||
|
|
||||||
private async postRecords(domain: string, records: { mainRecords: MainRecordItem[]; subRecords: SubRecordItem[] ,addToCurrent: boolean}): Promise<void> {
|
private async postRecords(domain: string, records: { mainRecords: MainRecordItem[]; subRecords: SubRecordItem[]; addToCurrent: boolean }): Promise<void> {
|
||||||
await this.access.doRequest({
|
await this.access.doRequest({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
path: `/restful/v2/domains/${domain}/records`,
|
path: `/restful/v2/domains/${domain}/records`,
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ export class AliossAccess extends BaseAccess {
|
|||||||
|
|
||||||
async getClient(access: AliyunAccess) {
|
async getClient(access: AliyunAccess) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const OSS = await import("ali-oss");
|
const OSS = await access.importRuntime("ali-oss");
|
||||||
return new OSS.default({
|
return new OSS.default({
|
||||||
accessKeyId: access.accessKeyId,
|
accessKeyId: access.accessKeyId,
|
||||||
accessKeySecret: access.accessKeySecret,
|
accessKeySecret: access.accessKeySecret,
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export class AliyunAccess extends BaseAccess {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getStsClient() {
|
async getStsClient() {
|
||||||
const StsClient = await import("@alicloud/sts-sdk");
|
const StsClient = await this.importRuntime("@alicloud/sts-sdk");
|
||||||
|
|
||||||
// 配置凭证
|
// 配置凭证
|
||||||
const sts = new StsClient.default({
|
const sts = new StsClient.default({
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export class AliyunClientV2 {
|
|||||||
if (this.client) {
|
if (this.client) {
|
||||||
return this.client;
|
return this.client;
|
||||||
}
|
}
|
||||||
const $OpenApi = await import("@alicloud/openapi-client");
|
const $OpenApi = await this.access.importRuntime("@alicloud/openapi-client");
|
||||||
// const Credential = await import("@alicloud/credentials");
|
// const Credential = await import("@alicloud/credentials");
|
||||||
// //@ts-ignore
|
// //@ts-ignore
|
||||||
// const credential = new Credential.default.default({
|
// const credential = new Credential.default.default({
|
||||||
@@ -52,9 +52,9 @@ export class AliyunClientV2 {
|
|||||||
async doRequest(req: AliyunClientV2Req) {
|
async doRequest(req: AliyunClientV2Req) {
|
||||||
const client = await this.getClient();
|
const client = await this.getClient();
|
||||||
|
|
||||||
const $OpenApi = await import("@alicloud/openapi-client");
|
const $OpenApi = await this.access.importRuntime("@alicloud/openapi-client");
|
||||||
const $Util = await import("@alicloud/tea-util");
|
const $Util = await this.access.importRuntime("@alicloud/tea-util");
|
||||||
const OpenApiUtil = await import("@alicloud/openapi-util");
|
const OpenApiUtil = await this.access.importRuntime("@alicloud/openapi-util");
|
||||||
const params = new $OpenApi.Params({
|
const params = new $OpenApi.Params({
|
||||||
// 接口名称
|
// 接口名称
|
||||||
action: req.action,
|
action: req.action,
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import { getGlobalAgents, ILogger } from "@certd/basic";
|
import { getGlobalAgents, ILogger } from "@certd/basic";
|
||||||
|
import { ImportRuntime } from "@certd/pipeline";
|
||||||
|
|
||||||
export class AliyunClient {
|
export class AliyunClient {
|
||||||
client: any;
|
client: any;
|
||||||
logger: ILogger;
|
logger: ILogger;
|
||||||
agent: any;
|
agent: any;
|
||||||
useROAClient: boolean;
|
useROAClient: boolean;
|
||||||
|
importRuntime: ImportRuntime;
|
||||||
|
|
||||||
constructor(opts: { logger: ILogger; useROAClient?: boolean }) {
|
constructor(opts: { logger: ILogger; useROAClient?: boolean; importRuntime?: ImportRuntime }) {
|
||||||
this.logger = opts.logger;
|
this.logger = opts.logger;
|
||||||
this.useROAClient = opts.useROAClient || false;
|
this.useROAClient = opts.useROAClient || false;
|
||||||
|
this.importRuntime = opts.importRuntime || (async (specifier: string) => await import(specifier));
|
||||||
const agents = getGlobalAgents();
|
const agents = getGlobalAgents();
|
||||||
this.agent = agents.httpsAgent;
|
this.agent = agents.httpsAgent;
|
||||||
}
|
}
|
||||||
@@ -17,13 +20,12 @@ export class AliyunClient {
|
|||||||
if (this.useROAClient) {
|
if (this.useROAClient) {
|
||||||
return await this.getROAClient();
|
return await this.getROAClient();
|
||||||
}
|
}
|
||||||
const Core = await import("@alicloud/pop-core");
|
const Core = await this.importRuntime("@alicloud/pop-core");
|
||||||
return Core.default;
|
return Core.default;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getROAClient() {
|
async getROAClient() {
|
||||||
const Core = await import("@alicloud/pop-core");
|
const Core = await this.importRuntime("@alicloud/pop-core");
|
||||||
console.log("aliyun sdk", Core);
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return Core.ROAClient;
|
return Core.ROAClient;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export class AliossClient {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const OSS = await import("ali-oss");
|
const OSS = await this.access.importRuntime("ali-oss");
|
||||||
const ossClient = new OSS.default({
|
const ossClient = new OSS.default({
|
||||||
accessKeyId: this.access.accessKeyId,
|
accessKeyId: this.access.accessKeyId,
|
||||||
accessKeySecret: this.access.accessKeySecret,
|
accessKeySecret: this.access.accessKeySecret,
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export class AliyunSslClient {
|
|||||||
|
|
||||||
async getClient() {
|
async getClient() {
|
||||||
const access = this.opts.access;
|
const access = this.opts.access;
|
||||||
const client = new AliyunClient({ logger: this.opts.logger });
|
const client = new AliyunClient({ logger: this.opts.logger, importRuntime: access.importRuntime.bind(access) });
|
||||||
|
|
||||||
let endpoint = this.opts.endpoint || "cas.aliyuncs.com";
|
let endpoint = this.opts.endpoint || "cas.aliyuncs.com";
|
||||||
if (this.opts.endpoint == null && this.opts.region) {
|
if (this.opts.endpoint == null && this.opts.region) {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export default class S3OssClientImpl extends BaseOssClient<S3Access> {
|
|||||||
async init() {
|
async init() {
|
||||||
// import { S3Client } from "@aws-sdk/client-s3";
|
// import { S3Client } from "@aws-sdk/client-s3";
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
const { S3Client } = await import("@aws-sdk/client-s3");
|
const { S3Client } = await this.access.importRuntime("@aws-sdk/client-s3");
|
||||||
this.client = new S3Client({
|
this.client = new S3Client({
|
||||||
forcePathStyle: true,
|
forcePathStyle: true,
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
@@ -32,7 +32,7 @@ export default class S3OssClientImpl extends BaseOssClient<S3Access> {
|
|||||||
|
|
||||||
async download(filePath: string, savePath: string): Promise<void> {
|
async download(filePath: string, savePath: string): Promise<void> {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const { GetObjectCommand } = await import("@aws-sdk/client-s3");
|
const { GetObjectCommand } = await this.access.importRuntime("@aws-sdk/client-s3");
|
||||||
const key = path.join(this.rootDir, filePath);
|
const key = path.join(this.rootDir, filePath);
|
||||||
const params = {
|
const params = {
|
||||||
Bucket: this.access.bucket, // The name of the bucket. For example, 'sample_bucket_101'.
|
Bucket: this.access.bucket, // The name of the bucket. For example, 'sample_bucket_101'.
|
||||||
@@ -47,7 +47,7 @@ export default class S3OssClientImpl extends BaseOssClient<S3Access> {
|
|||||||
|
|
||||||
async listDir(dir: string): Promise<OssFileItem[]> {
|
async listDir(dir: string): Promise<OssFileItem[]> {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const { ListObjectsCommand } = await import("@aws-sdk/client-s3");
|
const { ListObjectsCommand } = await this.access.importRuntime("@aws-sdk/client-s3");
|
||||||
const dirKey = this.join(this.rootDir, dir);
|
const dirKey = this.join(this.rootDir, dir);
|
||||||
const params = {
|
const params = {
|
||||||
Bucket: this.access.bucket, // The name of the bucket. For example, 'sample_bucket_101'.
|
Bucket: this.access.bucket, // The name of the bucket. For example, 'sample_bucket_101'.
|
||||||
@@ -67,7 +67,7 @@ export default class S3OssClientImpl extends BaseOssClient<S3Access> {
|
|||||||
}
|
}
|
||||||
async upload(filePath: string, fileContent: Buffer | string) {
|
async upload(filePath: string, fileContent: Buffer | string) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const { PutObjectCommand } = await import("@aws-sdk/client-s3");
|
const { PutObjectCommand } = await this.access.importRuntime("@aws-sdk/client-s3");
|
||||||
const key = path.join(this.rootDir, filePath);
|
const key = path.join(this.rootDir, filePath);
|
||||||
this.logger.info(`开始上传文件: ${key}`);
|
this.logger.info(`开始上传文件: ${key}`);
|
||||||
const params = {
|
const params = {
|
||||||
@@ -88,7 +88,7 @@ export default class S3OssClientImpl extends BaseOssClient<S3Access> {
|
|||||||
}
|
}
|
||||||
const key = filePath;
|
const key = filePath;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const { DeleteObjectCommand } = await import("@aws-sdk/client-s3");
|
const { DeleteObjectCommand } = await this.access.importRuntime("@aws-sdk/client-s3");
|
||||||
await this.client.send(
|
await this.client.send(
|
||||||
new DeleteObjectCommand({
|
new DeleteObjectCommand({
|
||||||
Bucket: this.access.bucket,
|
Bucket: this.access.bucket,
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import { IsAccess, AccessInput, BaseAccess } from "@certd/pipeline";
|
import { IsAccess, AccessInput, BaseAccess } from "@certd/pipeline";
|
||||||
|
|
||||||
@IsAccess({
|
const tencentAccessDefine: any = {
|
||||||
name: "tencent",
|
name: "tencent",
|
||||||
title: "腾讯云",
|
title: "腾讯云",
|
||||||
icon: "svg:icon-tencentcloud",
|
icon: "svg:icon-tencentcloud",
|
||||||
order: 0,
|
order: 0,
|
||||||
})
|
dependPackages: {
|
||||||
|
"tencentcloud-sdk-nodejs": "^4.1.112",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
@IsAccess(tencentAccessDefine)
|
||||||
export class TencentAccess extends BaseAccess {
|
export class TencentAccess extends BaseAccess {
|
||||||
@AccessInput({
|
@AccessInput({
|
||||||
title: "secretId",
|
title: "secretId",
|
||||||
@@ -104,7 +109,7 @@ export class TencentAccess extends BaseAccess {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getStsClient() {
|
async getStsClient() {
|
||||||
const sdk = await import("tencentcloud-sdk-nodejs/tencentcloud/services/sts/v20180813/index.js");
|
const sdk = await (this as any).importRuntime("tencentcloud-sdk-nodejs/tencentcloud/services/sts/v20180813/index.js");
|
||||||
const StsClient = sdk.v20180813.Client;
|
const StsClient = sdk.v20180813.Client;
|
||||||
|
|
||||||
const clientConfig = {
|
const clientConfig = {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { TencentAccess } from "../access.js";
|
import { TencentAccess } from "../access.js";
|
||||||
import { ILogger, safePromise } from "@certd/basic";
|
import { ILogger, safePromise } from "@certd/basic";
|
||||||
|
import { ImportRuntime } from "@certd/pipeline";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
|
|
||||||
export class TencentCosClient {
|
export class TencentCosClient {
|
||||||
@@ -7,16 +8,18 @@ export class TencentCosClient {
|
|||||||
logger: ILogger;
|
logger: ILogger;
|
||||||
region: string;
|
region: string;
|
||||||
bucket: string;
|
bucket: string;
|
||||||
|
importRuntime: ImportRuntime;
|
||||||
|
|
||||||
constructor(opts: { access: TencentAccess; logger: ILogger; region: string; bucket: string }) {
|
constructor(opts: { access: TencentAccess; logger: ILogger; region: string; bucket: string; importRuntime?: ImportRuntime }) {
|
||||||
this.access = opts.access;
|
this.access = opts.access;
|
||||||
this.logger = opts.logger;
|
this.logger = opts.logger;
|
||||||
this.bucket = opts.bucket;
|
this.bucket = opts.bucket;
|
||||||
this.region = opts.region;
|
this.region = opts.region;
|
||||||
|
this.importRuntime = opts.importRuntime || (async (specifier: string) => await import(specifier));
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCosClient() {
|
async getCosClient() {
|
||||||
const sdk = await import("cos-nodejs-sdk-v5");
|
const sdk = await this.importRuntime("cos-nodejs-sdk-v5");
|
||||||
const clientConfig = {
|
const clientConfig = {
|
||||||
SecretId: this.access.secretId,
|
SecretId: this.access.secretId,
|
||||||
SecretKey: this.access.secretKey,
|
SecretKey: this.access.secretKey,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user