Compare commits

...
89 Commits
Author SHA1 Message Date
xiaojunnuo 5558376bae v1.43.0 2026-08-14 23:09:01 +08:00
xiaojunnuo 2da560a21f build: prepare to build 2026-08-14 23:03:22 +08:00
xiaojunnuo bc9a0988a7 chore: 1 2026-08-14 23:01:59 +08:00
xiaojunnuo 6a731f986e chore: 代码格式化 2026-08-14 22:25:36 +08:00
xiaojunnuo 2cb4e551fa feat(ssh): ssh连接支持跳板机 2026-08-14 21:20:47 +08:00
xiaojunnuo e5a1844852 feat(pipeline): 新增流水线批量编辑、阶段宽度调整功能
1. 新增流水线阶段和任务的批量操作能力,支持批量启用/禁用/删除选中项
2. 为流水线阶段添加可拖拽调整宽度的功能,限制宽度范围260-720px
3. 新增TEO DNS插件配置项order,添加流水线步骤批量编辑工具类与测试用例
4. 为Stage类型添加style扩展字段支持自定义样式
2026-08-14 18:33:08 +08:00
xiaojunnuo b8f82ecb5e chore: 1 2026-08-14 17:31:42 +08:00
xiaojunnuo 18583f6ce0 fix(unicloud): 修复unicloud登录接口改版后请求失败的问题 2026-08-14 10:10:12 +08:00
xiaojunnuo 1468eb1ef2 perf: 优化第三方登录设置提示 2026-08-14 09:58:19 +08:00
xiaojunnuo 8ab699a910 fix(ui): 修复oauth设置页保存后未正确刷新的问题
将保存后的loadOauthProviders调用替换为loadSysSettings,确保系统配置数据正确更新
2026-08-14 09:52:25 +08:00
xiaojunnuo b173311468 perf(ui): 域名选择器支持点击复制域名 2026-08-14 01:03:08 +08:00
xiaojunnuo 522dcd8ffb refactor: 重构运行时依赖服务,简化逻辑并修复部分配置
1.  移除废弃的getRuntimeDependencyPluginDefines方法和相关插件依赖解析逻辑
2.  简化RuntimeDepsService的锁机制、依赖收集和安装流程
3.  更新测试用例适配重构后的API
4.  调整开发环境数据库配置和运行时依赖超时时间
5.  优化注释和代码结构
2026-08-14 00:23:52 +08:00
xiaojunnuo 6809a35177 Merge remote-tracking branch 'origin/v2' into v2-dev 2026-08-13 21:57:56 +08:00
MephistoJB bd82147496 perf: Improve English localization coverage (#778) @MephistoJB
* Improve English localization coverage

* Improve English UI translations

* Improve English localization coverage

* Revert "Improve English localization coverage"

This reverts commit 5b29381ebf.

* Revert "Improve English UI translations"

This reverts commit 11c1f673da.

* Revert "Improve English localization coverage"

This reverts commit 9bc7a05795.

* Add landing page language switching
2026-08-13 21:57:36 +08:00
xiaojunnuo a4224ab1df chore: 1 2026-08-12 19:59:27 +08:00
xiaojunnuo 4c4dc8ca46 docs(install): 更新数据库安装文档的配图和补充提示
替换了旧的数据库同步步骤截图,新增两张分步截图,并补充内存溢出相关的注意事项
2026-08-12 19:51:21 +08:00
xiaojunnuo 8165a5ff89 perf(plugin-ucloud): ucloud us3部署插件增加region选择 2026-08-10 22:25:11 +08:00
xiaojunnuo 1017a38eb4 fix(pipeline): 修复过期/损坏的安装锁导致的依赖安装失败
新增锁过期检测与清理逻辑,当存在超时的安装锁或者无效的锁文件时,自动清理锁并继续安装流程,新增两个测试用例验证该修复场景
2026-08-10 21:14:12 +08:00
xiaojunnuo 80374cae8a perf(install): 补充迁移到PostgreSQL后,表自增序列从1开始的修复SQL脚本和说明 2026-08-09 23:19:26 +08:00
xiaojunnuo 6339cdffa9 feat: 优化开放接口证书自动申请功能
1. 新增TextedException统一异常类
2. 新增AccessService获取默认授权方法
3. 扩展证书流水线批量更新字段,支持自动挑战类型、SSL提供商和ACME账户
4. 调整证书申请默认重试次数为1并优化重试逻辑
5. 优化证书模板获取逻辑,支持ID为0时获取默认模板
6. 完善自动流水线创建逻辑,默认设置挑战类型为auto
7. 调整默认证书链为ISRG Root X2
8. 优化自动申请证书的参数处理,补充邮箱、ACME账户和重试次数默认值
9. 替换部分异常抛出为TextedException
2026-08-09 22:38:29 +08:00
xiaojunnuo 1042e599d9 Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2026-08-08 23:30:21 +08:00
xiaojunnuo 1a4f1cb07d refactor(certd-server): 优化流水线触发逻辑并新增配置
1. 新增流水线执行异常错误码和提示信息
2. 调整服务端口配置为7004/7005
3. 优化流水线状态查询逻辑,增加更新时间判断
4. 新增失败流水线重试间隔限制逻辑
5. 添加对应单元测试
2026-08-08 22:27:36 +08:00
xiaojunnuo edb249c2ad perf: 统一通知类型常量管理,新增开放接口通知能力 2026-08-08 20:48:52 +08:00
xiaojunnuo e9921928df perf: 部署到京东云CDN支持设置跳转类型 2026-08-07 09:15:00 +08:00
xiaojunnuo fa0c87dec3 Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2026-08-06 12:00:18 +08:00
xiaojunnuo ba64c2d147 prf: 新增阿里云ACK部署调试支持 2026-08-06 12:00:11 +08:00
xiaojunnuo cf008b9cff fix: 优化火山引擎dcdn,单个单个部署,规避超时问题 2026-08-06 11:57:40 +08:00
xiaojunnuo 9d6f7ef0f2 style: 统一代码中单引号为双引号并补全格式 2026-08-02 21:31:30 +08:00
xiaojunnuo 0ddb1f69d2 perf(cert-plugin): 证书申请支持失败重试 2026-08-02 21:07:11 +08:00
xiaojunnuo 4091abdfd7 chore: lint fix 2026-08-02 21:06:33 +08:00
xiaojunnuo 27008cb44a Merge remote-tracking branch 'origin/v2' into v2-dev 2026-08-02 19:51:18 +08:00
ahe 319f555569 Feature/偏好设置优化 (#775) @nicheng-he
* feat: 偏好设置支持从剪切板导入

在现有「复制偏好设置」基础上增加从剪切板导入,便于在浏览器间同步主题与布局配置。

* feat: 偏好设置支持保存到账号并在登录后自动同步

增加「保存到账号」能力,将本地偏好差异写入用户设置;登录成功后自动从账号拉取并应用到本地。
2026-08-02 04:27:07 +08:00
xiaojunnuo 83a6aed625 chore: 优化腾讯云删除证书提示 2026-07-30 19:08:57 +08:00
xiaojunnuo f1b67049d1 perf: 证书成功通知增加有效期显示 2026-07-28 19:22:12 +08:00
xiaojunnuo 8cbca5761e chore(pipeline): adjust npm install log truncation logic
change log output to take last 2000 chars instead of first 2000 for both stdout/stderr, and add consistent empty log placeholder text
2026-07-27 00:35:00 +08:00
xiaojunnuo 967846bef5 perf(tencent-clb): 为腾讯云CLB部署插件支持远程选择输入
- 重构插件输入配置,使用createRemoteSelectInputDefine统一封装远程选择组件
- 新增onGetCLBList、onGetListenerList、onGetDomainList方法实现数据拉取和格式转换
- 补全完整的单元测试用例,覆盖输入配置校验和数据映射逻辑
2026-07-26 19:01:18 +08:00
xiaojunnuo 18b2d3ac20 perf: 优化第三方依赖安装失败日志输出 2026-07-23 18:35:04 +08:00
xiaojunnuo 7d22fe3d7d perf: 优化SqliteError: disk I/O error报错修复提示 2026-07-23 10:49:00 +08:00
xiaojunnuo ee67b6c042 Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2026-07-21 14:13:16 +08:00
xiaojunnuo 1cfa76683b perf: 优化宝塔报错提示 2026-07-21 14:13:07 +08:00
xiaojunnuo 4662e45e58 build: release 2026-07-19 01:28:05 +08:00
xiaojunnuo 1fefbdc9ab build: publish 2026-07-19 01:14:35 +08:00
xiaojunnuo 1cb2a57c55 build: trigger build image 2026-07-19 01:14:24 +08:00
xiaojunnuo 246ee83015 v1.42.6 2026-07-19 01:13:42 +08:00
xiaojunnuo 335ddfc7a5 build: prepare to build 2026-07-19 01:12:01 +08:00
xiaojunnuo 5b500830a1 fix: 修复正常批量删除流水线报权限不足的bug 2026-07-19 01:05:03 +08:00
xiaojunnuo 5f53b81c75 perf: 优化动态加载依赖镜像地址,多次重试 2026-07-18 21:07:56 +08:00
xiaojunnuo c8c269f612 chore: 1 2026-07-17 15:44:23 +08:00
xiaojunnuo 743617dbda build: release 2026-07-15 23:54:59 +08:00
xiaojunnuo 89806b828a build: publish 2026-07-15 23:35:15 +08:00
xiaojunnuo 2e530bfdb0 build: trigger build image 2026-07-15 23:35:05 +08:00
xiaojunnuo 83495b3213 v1.42.5 2026-07-15 23:34:16 +08:00
xiaojunnuo 5108416904 build: prepare to build 2026-07-15 23:31:35 +08:00
xiaojunnuo 5589da1822 chore: 修复单元测试 2026-07-15 23:30:49 +08:00
xiaojunnuo 21e5aed3f3 chore: 修复单元测试的问题 2026-07-15 23:25:28 +08:00
xiaojunnuo 4a88f795e1 chore: vke kubeconfig 有效期校验 2026-07-15 23:10:58 +08:00
xiaojunnuo 604fa5be63 perf: 优化vke keubconfig获取方式,改成先查询,如果没有再创建临时config 2026-07-15 23:01:32 +08:00
xiaojunnuo 7ed1be994f fix: 修复dingtalk通知格式没有换行的bug 2026-07-15 23:00:42 +08:00
xiaojunnuo 6cc74a1c0a chore: lint 2026-07-15 01:25:55 +08:00
xiaojunnuo 167b303fae fix: 修复上传到cos报runtimeDepsService未初始化的问题 2026-07-15 01:09:57 +08:00
xiaojunnuo b91c9e4ea6 perf: 给SQLITE_IOERR_WRITE增加友好报错提示,将certd:latest镜像改为certd:slim 2026-07-14 21:32:32 +08:00
xiaojunnuo 584b7b6f45 build: release 2026-07-12 03:41:59 +08:00
xiaojunnuo 2312c444ef build: release 2026-07-12 03:36:09 +08:00
xiaojunnuo 0373d019f8 Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2026-07-12 03:11:04 +08:00
xiaojunnuo fe09e75b80 refactor(nginx-proxy-manager): 替换直接动态导入为运行时依赖导入
将插件中直接使用的动态导入改为通过runtimeDepsService统一处理,同时重构导入方式为runtimeImport变量,移除冗余的form-data和otplib直接导入,统一导入逻辑
2026-07-12 03:10:52 +08:00
xiaojunnuo 432c9c6cc1 build: publish 2026-07-12 02:48:34 +08:00
xiaojunnuo 5fc1d55879 build: trigger build image 2026-07-12 02:48:22 +08:00
xiaojunnuo 268cd6cc9c v1.42.4 2026-07-12 02:47:26 +08:00
xiaojunnuo 20ffe31638 build: prepare to build 2026-07-12 02:45:04 +08:00
xiaojunnuo 02d30067ea chore: 1 2026-07-12 02:44:16 +08:00
xiaojunnuo 0ce7808e08 build: prepare to build 2026-07-12 02:39:50 +08:00
xiaojunnuo d609ee2b7a chore(base-service): 完善batchDelete方法的返回类型并添加返回值
为batchDelete方法添加明确的Promise<number>返回类型,同时返回实际删除的id数量
2026-07-12 02:38:49 +08:00
xiaojunnuo 083df61fcc build: prepare to build 2026-07-12 02:33:34 +08:00
xiaojunnuo a524988be6 chore: 1 2026-07-12 02:32:49 +08:00
xiaojunnuo 8517a0b564 refactor(certd-server): 移除非运行时依赖到动态导入
将alipay-sdk、openid-client、otplib、wechatpay-node-v3改为动态导入,从dependencies移到devDependencies?不,是改为通过importRuntime动态加载,移除顶层静态引入,优化启动时依赖加载,减少初始包体积和启动耗时
2026-07-12 00:04:06 +08:00
xiaojunnuo ec69b8f11b fix(pipeline): 重构运行时依赖加载逻辑,修复火山引擎DNS解析报runtimeDepsService未初始化的bug 2026-07-11 23:40:13 +08:00
xiaojunnuo edda1b57f3 chore: 火山vod 上传后等待3秒 2026-07-09 19:09:08 +08:00
xiaojunnuo 5a7766992d chore: 修复update domain传参问题 2026-07-09 16:42:48 +08:00
xiaojunnuo 02dabe11db fix: 修复火山引擎查不到自定义源站域名的问题 2026-07-09 16:39:08 +08:00
xiaojunnuo 58024128d8 Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2026-07-09 14:47:33 +08:00
xiaojunnuo e44bf9d773 fix: 修复火山引擎自定义源站域名查询不到的问题 2026-07-09 14:47:05 +08:00
xiaojunnuo 1114223bda build: release 2026-07-09 00:00:53 +08:00
xiaojunnuo 55022e5fa6 build: publish 2026-07-08 23:48:41 +08:00
xiaojunnuo ae732fb298 build: trigger build image 2026-07-08 23:48:29 +08:00
xiaojunnuo a6ef6996c3 v1.42.3 2026-07-08 23:47:40 +08:00
xiaojunnuo 97cf5e127b build: prepare to build 2026-07-08 23:45:42 +08:00
xiaojunnuo 690df7373c chore: 旧版数据兼容性的ai 提示 2026-07-08 12:04:07 +08:00
xiaojunnuo 9cc01e5da1 Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2026-07-08 11:51:35 +08:00
xiaojunnuo 0a068a2746 fix(volcengine-alb): 修复火山引擎ALB 默认证书部署类型会部署到扩展证书的问题 2026-07-08 11:51:26 +08:00
286 changed files with 5063 additions and 3236 deletions
+1
View File
@@ -400,6 +400,7 @@ export class OAuthDemoAccess extends BaseAccess {
4. **错误处理**:API 调用失败时应抛出明确的错误信息。
5. **测试方法**:实现 `onTestRequest` 方法,以便用户可以测试授权是否正常。
6. **统一接口调用**:封装统一的 API 请求方法,避免重复编写错误处理逻辑。
7. **旧版数据兼容**: 新增注解的插件参数,必须要考虑旧版数据兼容,比如新增一个deployType参数,有两种值:`default`和`custom`,需要在使用时判空,走旧版逻辑。
## 开发技巧
+1
View File
@@ -454,3 +454,4 @@ new TencentDnsProvider();
3. **记录结构**:定义适合对应云平台的记录数据结构,至少包含 id 字段用于删除记录。
4. **日志输出**:使用 `this.logger` 输出日志,而不是 `console`,参数文本化,不要传对象,否则会输出`[object Object]}`。
5. **错误处理**:API 调用失败时应抛出明确的错误信息。
6. **旧版数据兼容**: 新增插件参数,必须要考虑旧版数据兼容,比如新增一个deployType参数,有两种值:`default`和`custom`,需要在使用时判空,走旧版逻辑。
+3 -2
View File
@@ -281,12 +281,12 @@ export class DemoTest extends AbstractTaskPlugin {
return {
value: item.siteName,
label: item.siteName,
domain: item.siteName,
domain: item.siteName, //这里必须要包含domain 否则后面分组时候全部分配到未匹配中
};
});
//将站点域名名称根据证书域名进行匹配分组,分成匹配的和不匹配的两组选项,返回给前端,供用户选择
return {
list: optionsUtils.buildGroupOptions(options, this.certDomains),
list: optionsUtils.buildGroupOptions(options, this.certDomains), //分组后的列表
total: siteRes.length,
};
}
@@ -641,6 +641,7 @@ new AliyunOSSDeploy();
4. **日志输出**:使用 `this.logger` 输出日志,而不是 `console`,参数文本化,不要传对象,否则会输出`[object Object]}`。
5. **错误处理**:执行过程中的错误应被捕获并记录。
6. **授权获取**:使用 `this.getAccess(accessId)` 获取授权信息。
7. **旧版数据兼容**: 新增@TaskInput注解的插件参数,必须要考虑旧版数据兼容,比如新增一个deployType参数,有两种值:`default`和`custom`,需要在使用时判空,走旧版逻辑。
## 部署逻辑注意事项
+8
View File
@@ -70,6 +70,8 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
- 先读本文,再按任务读取具体代码或技能文件。
- PowerShell 读取中文、Markdown、locale、文档类文件时使用 `Get-Content -Raw -Encoding UTF8`;仍乱码时先执行 `[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()`。
- PowerShell 中用 `rg` 搜索含引号、括号、反斜杠的 pattern 时,优先用单引号包裹整个 pattern,例如 `rg 'await import\("tencentcloud-sdk-nodejs' packages/ui/certd-server/src -g '*.ts'`。
- 手工编辑或创建文件时优先使用 `apply_patch`。单个文件内有多处不连续改动时,拆成多个独立的 `*** Update File` 块,每块只改一处附近上下文;不要在同一个 update hunk 里强塞多个 `@@`。
- 只有真正机械化的大批量替换、格式化或生成任务才考虑脚本/工具。若必须使用临时脚本,应放在临时目录并在同一个受控步骤内完成创建、执行、删除;不要把临时脚本落在仓库里跨多步工具调用执行。
- 不要主动运行 `pnpm install`;缺依赖、TTY、网络导致安装或测试失败时,停止尝试并说明环境问题。
- 优先沿用现有模块、插件、service、页面模式;不要为形式上的复用制造过度抽象。
- 代码可读性优先于短写法。复杂条件、三元表达式、链式调用、内联对象和多层 helper 调用要拆成命名清晰的中间变量或小方法。
@@ -134,6 +136,7 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
- 做前端任务时,先定位 `packages/ui/certd-client/src/views/certd` 下的页面,再找对应 `src/api`。
- 不要运行前端 `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>`。
- Vue 模板中需要用 JSX 动态渲染时,使用 `<script setup lang="tsx">` 编写;不要使用 `h()` 创建 VNode。
- 列表管理、后台管理、记录查询、CRUD 表格页面优先使用 Fast Crud;开发或重构前读 `.trae/skills/fast-crud-page-dev/SKILL.md`。
- 只有轻量只读展示、强交互自定义界面或既有页面模式明显不适合 Fast Crud 时,才手写 `a-table` / 自定义列表,并在回复中说明。
- 内嵌 Fast Crud 时,外层必须有稳定高度或完整 `flex: 1; min-height: 0` 链路。
@@ -210,3 +213,8 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
- 后端业务数据、接口、实体、权限、迁移:改 `packages/ui/certd-server/src/modules` 与 `src/controller`。
- 表单、列表、插件配置 UI:改 `packages/ui/certd-client/src/views/certd` 及对应 `src/api`。
## 其他注意事项
### 旧版数据兼容
- 新增插件参数时,必须要考虑旧版数据兼容,比如新增一个deployType参数,有两种值:`default`和`custom`,需要在使用时判空,走旧版逻辑。
+67
View File
@@ -3,6 +3,73 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.43.0](https://github.com/certd/certd/compare/v1.42.6...v1.43.0) (2026-08-14)
### Bug Fixes
* 优化火山引擎dcdn,单个单个部署,规避超时问题 ([cf008b9](https://github.com/certd/certd/commit/cf008b9cff54542dc551fb159578dbd49006229c))
* **pipeline:** 修复过期/损坏的安装锁导致的依赖安装失败 ([1017a38](https://github.com/certd/certd/commit/1017a38eb48d6778745d4905d9ec4f6067a261d8))
* **ui:** 修复oauth设置页保存后未正确刷新的问题 ([8ab699a](https://github.com/certd/certd/commit/8ab699a9104b47e02a88ad764e995a495deb775e))
* **unicloud:** 修复unicloud登录接口改版后请求失败的问题 ([18583f6](https://github.com/certd/certd/commit/18583f6ce025fa8d365fad5a8c9c6020f3bcfbae))
### Features
* 优化开放接口证书自动申请功能 ([6339cdf](https://github.com/certd/certd/commit/6339cdffa9f79505dada5617d8c7fcfacc419a2e))
* **pipeline:** 新增流水线批量编辑、阶段宽度调整功能 ([e5a1844](https://github.com/certd/certd/commit/e5a18448522be88bc504a45a46ad599f3c39ca30))
* **ssh:** ssh连接支持跳板机 ([2cb4e55](https://github.com/certd/certd/commit/2cb4e551fa7616f587a2d907f9b513f7df3a9957))
### Performance Improvements
* 部署到京东云CDN支持设置跳转类型 ([e992192](https://github.com/certd/certd/commit/e9921928df011c2435af73b68ebc3d773f13c16a))
* 统一通知类型常量管理,新增开放接口通知能力 ([edb249c](https://github.com/certd/certd/commit/edb249c2ad3c173dbfd04d8d156f895a6706ffd1))
* 优化宝塔报错提示 ([1cfa766](https://github.com/certd/certd/commit/1cfa76683b40da9e9198552f2226419d4afa86a3))
* 优化第三方登录设置提示 ([1468eb1](https://github.com/certd/certd/commit/1468eb1ef292a9e21a2a43131b86b8acb3688307))
* 优化第三方依赖安装失败日志输出 ([18b2d3a](https://github.com/certd/certd/commit/18b2d3ac2078d0c6636af897f8f66e4eb5ff832c))
* 优化SqliteError: disk I/O error报错修复提示 ([7d22fe3](https://github.com/certd/certd/commit/7d22fe3d7d0de97bae2ceedd051a0c16a1cd90e3))
* 证书成功通知增加有效期显示 ([f1b6704](https://github.com/certd/certd/commit/f1b67049d16ebcabe6d6d9b6cb53a8a2b7a9265c))
* **cert-plugin:** 证书申请支持失败重试 ([0ddb1f6](https://github.com/certd/certd/commit/0ddb1f69d2a46b993e792862ce9952336d532f5b))
* Improve English localization coverage ([#778](https://github.com/certd/certd/issues/778)) @MephistoJB ([bd82147](https://github.com/certd/certd/commit/bd821474969a22078b9d2e73518e765b8c9a26c7))
* **install:** 补充迁移到PostgreSQL后,表自增序列从1开始的修复SQL脚本和说明 ([80374ca](https://github.com/certd/certd/commit/80374cae8a43fd17eb180381a093267e466a0bc2))
* **plugin-ucloud:** ucloud us3部署插件增加region选择 ([8165a5f](https://github.com/certd/certd/commit/8165a5ff8986e759948c06f924d12e605b2ec8f6))
* **tencent-clb:** 为腾讯云CLB部署插件支持远程选择输入 ([967846b](https://github.com/certd/certd/commit/967846bef590b76d27e472180b96372ff55cfb6f))
* **ui:** 域名选择器支持点击复制域名 ([b173311](https://github.com/certd/certd/commit/b173311468d2c1f318d1333257c3ebcf916da071))
## [1.42.6](https://github.com/certd/certd/compare/v1.42.5...v1.42.6) (2026-07-18)
### Bug Fixes
* 修复正常批量删除流水线报权限不足的bug ([5b50083](https://github.com/certd/certd/commit/5b500830a122c6c42dab054e57fed509050f94da))
### Performance Improvements
* 优化动态加载依赖镜像地址,多次重试 ([5f53b81](https://github.com/certd/certd/commit/5f53b81c75dd242b4260ac08cae14c6d1a08a883))
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
### Bug Fixes
* 修复上传到cos报runtimeDepsService未初始化的问题 ([167b303](https://github.com/certd/certd/commit/167b303faeca02cc11cf97e4be2a3df914852167))
* 修复dingtalk通知格式没有换行的bug ([7ed1be9](https://github.com/certd/certd/commit/7ed1be994f8b4b74cdeb38743060c912c027248b))
### Performance Improvements
* 给SQLITE_IOERR_WRITE增加友好报错提示,将certd:latest镜像改为certd:slim ([b91c9e4](https://github.com/certd/certd/commit/b91c9e4ea671cb359ef164e27864de1d66cba9d3))
* 优化vke keubconfig获取方式,改成先查询,如果没有再创建临时config ([604fa5b](https://github.com/certd/certd/commit/604fa5be634d099d797bfee5c2b0f26ce0ac8461))
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
### Bug Fixes
* 修复火山引擎查不到自定义源站域名的问题 ([02dabe1](https://github.com/certd/certd/commit/02dabe11db3e9b13ca4621ce9ddd2b808bfca390))
* 修复火山引擎自定义源站域名查询不到的问题 ([e44bf9d](https://github.com/certd/certd/commit/e44bf9d77375d48ac7fd1582e69fae02dfd248fa))
* **pipeline:** 重构运行时依赖加载逻辑,修复火山引擎DNS解析报runtimeDepsService未初始化的bug ([ec69b8f](https://github.com/certd/certd/commit/ec69b8f11bfd4b20991aef74a72a47182ca79a9d))
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
### Bug Fixes
* **volcengine-alb:** 修复火山引擎ALB 默认证书部署类型会部署到扩展证书的问题 ([0a068a2](https://github.com/certd/certd/commit/0a068a274673e9768954e9f7367c267d44f3b530))
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
### Bug Fixes
+7 -5
View File
@@ -107,11 +107,13 @@ https://certd.handfree.work/
**镜像版本:**
| 版本标签 | 基础系统 | 说明 |
| --- | --- | --- | --- |
| `latest` / `[version]` | Alpine Linux | 默认版本,镜像体积小 |
| `slim` / `[version]-slim` | Debian slim | 基于glibc,dns解析兼容性好(可能需要配置security_opt -seccomp=unconfined) |
| `armv7` / `[version]-armv7` | Alpine Linux | ARMv7 架构专用版本 |
| 标签 | 指定版本 | 基础系统 | 说明 |
| --- | --- | --- | --- |
| `latest` | `[version]` | Alpine Linux | 默认版本,镜像体积小 |
| `slim` | `[version]-slim` | Debian slim | 基于glibc,dns解析兼容性好(可能需要配置security_opt -seccomp=unconfined) |
| `armv7` | `[version]-armv7` | Alpine Linux | ARMv7 架构专用版本 |
**镜像地址:**
+36
View File
@@ -3,6 +3,42 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.42.6](https://github.com/certd/certd/compare/v1.42.5...v1.42.6) (2026-07-18)
### Bug Fixes
* 修复正常批量删除流水线报权限不足的bug ([5b50083](https://github.com/certd/certd/commit/5b500830a122c6c42dab054e57fed509050f94da))
### Performance Improvements
* 优化动态加载依赖镜像地址,多次重试 ([5f53b81](https://github.com/certd/certd/commit/5f53b81c75dd242b4260ac08cae14c6d1a08a883))
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
### Bug Fixes
* 修复上传到cos报runtimeDepsService未初始化的问题 ([167b303](https://github.com/certd/certd/commit/167b303faeca02cc11cf97e4be2a3df914852167))
* 修复dingtalk通知格式没有换行的bug ([7ed1be9](https://github.com/certd/certd/commit/7ed1be994f8b4b74cdeb38743060c912c027248b))
### Performance Improvements
* 给SQLITE_IOERR_WRITE增加友好报错提示,将certd:latest镜像改为certd:slim ([b91c9e4](https://github.com/certd/certd/commit/b91c9e4ea671cb359ef164e27864de1d66cba9d3))
* 优化vke keubconfig获取方式,改成先查询,如果没有再创建临时config ([604fa5b](https://github.com/certd/certd/commit/604fa5be634d099d797bfee5c2b0f26ce0ac8461))
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
### Bug Fixes
* 修复火山引擎查不到自定义源站域名的问题 ([02dabe1](https://github.com/certd/certd/commit/02dabe11db3e9b13ca4621ce9ddd2b808bfca390))
* 修复火山引擎自定义源站域名查询不到的问题 ([e44bf9d](https://github.com/certd/certd/commit/e44bf9d77375d48ac7fd1582e69fae02dfd248fa))
* **pipeline:** 重构运行时依赖加载逻辑,修复火山引擎DNS解析报runtimeDepsService未初始化的bug ([ec69b8f](https://github.com/certd/certd/commit/ec69b8f11bfd4b20991aef74a72a47182ca79a9d))
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
### Bug Fixes
* **volcengine-alb:** 修复火山引擎ALB 默认证书部署类型会部署到扩展证书的问题 ([0a068a2](https://github.com/certd/certd/commit/0a068a274673e9768954e9f7367c267d44f3b530))
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
### Bug Fixes
+61 -2
View File
@@ -103,9 +103,14 @@ docker-compose up -d
![db-6.png](images/db-sync-1.jpg)
![db-7.png](images/db-sync-2.png)
![db-8.png](images/db-sync-3.png)
![db-8.png](images/db-sync-3-0.png)
![db-8.png](images/db-sync-3-1.png)
> 重要: 建议Fetch大小改成1000,避免内存溢出
下一步、下一步,直到数据加载设置,勾选`在加载前截断目标表`(此选项很重要,并且会清空mysql certd数据库中的数据)
![db-7.png](images/db-sync-4.png)
> 重要: 在插入行后做提交,也改成1000,避免内存溢出
#### 5. 导入完成
@@ -115,4 +120,58 @@ docker-compose up -d
访问MySQL版本测试,数据已成功迁移
确认没有问题之后,删除旧版certd
确认没有问题之后,删除旧版certd
## 三、问题
### 1. 迁移到PG之后,所有表的Sequence会从1开始
执行如下sql 语句,批量将所有表的id自增设置到当前数据的最大值
```sql
DO $$
DECLARE
rec RECORD;
seq_name TEXT;
max_id BIGINT;
BEGIN
-- 遍历所有用户表(排除系统schema)
FOR rec IN
SELECT table_schema, table_name
FROM information_schema.tables
WHERE table_type = 'BASE TABLE'
AND table_schema NOT IN ('pg_catalog', 'information_schema')
LOOP
-- 检查表是否存在名为 'id' 的列
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = rec.table_schema
AND table_name = rec.table_name
AND column_name = 'id'
) THEN
-- 获取该列关联的序列名(如果是 serial 或 identity 列)
seq_name := pg_get_serial_sequence(
quote_ident(rec.table_schema) || '.' || quote_ident(rec.table_name),
'id'
);
IF seq_name IS NOT NULL THEN
-- 查询当前表中 id 的最大值
EXECUTE format('SELECT MAX(id) FROM %I.%I', rec.table_schema, rec.table_name) INTO max_id;
IF max_id IS NULL THEN
-- 表为空,将序列重置为初始值 1(下次调用 nextval 返回 1)
PERFORM setval(seq_name, 1, false);
RAISE NOTICE '表 %.% 为空,序列 % 已重置为 1', rec.table_schema, rec.table_name, seq_name;
ELSE
-- 将序列当前值设置为最大值,下次调用 nextval 返回 max_id + 1
PERFORM setval(seq_name, max_id, true);
RAISE NOTICE '表 %.% 的序列 % 已设置为 %', rec.table_schema, rec.table_name, seq_name, max_id;
END IF;
END IF;
END IF;
END LOOP;
END $$;
```

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

+1 -1
View File
@@ -115,7 +115,7 @@
| 序号 | 名称 | 说明 |
|-----|-----|-----|
| 1.| **腾讯云-删除即将过期证书** | 仅删除未使用的证书 |
| 1.| **腾讯云-删除即将过期证书** | 仅删除即将过期且未使用的证书 |
| 2.| **腾讯云-部署证书到任意云资源** | 支持负载均衡、CDN、DDoS、直播、点播、Web应用防火墙、API网关、TEO、容器服务、对象存储、轻应用服务器、云原生微服务、云开发 |
| 3.| **腾讯云-部署到CDN(废弃)** | 已废弃,请使用v2版 |
| 4.| **腾讯云-部署到CDN-v2** | 推荐使用,支持CDN域名以及COS加速域名 |
+17 -17
View File
@@ -5,23 +5,23 @@
| 1.| **阿里ESA** | 阿里ESA DNS解析 |
| 2.| **阿里云** | 阿里云DNS解析提供商 |
| 3.| **AWS Route53** | AWS Route53 DNS解析提供商 |
| 4.| **Azure DNS** | Azure DNS 解析提供商 |
| 5.| **火山引擎** | 火山引擎DNS解析提供商 |
| 6.| **京东云** | 京东云DNS解析提供商 |
| 7.| **新网(代理方式)** | 新网域名解析(代理方式) |
| 8.| **新网** | 新网域名解析 |
| 9.| **BIND9 DNS** | 通过 SSH 连接到 BIND9 服务器,使用 nsupdate 命令管理 DNS 记录 |
| 10.| **cloudflare** | cloudflare dns provider |
| 11.| **dns.la** | dns.la |
| 12.| **Dynadot** | Dynadot DNS提供商 |
| 13.| **godaddy** | GoDaddy |
| 14.| **HiPM DNSMgr** | HiPM DNSMgr DNS 解析提供商 |
| 15.| **华为云** | 华为云DNS解析提供商 |
| 16.| **namesilo** | namesilo dns provider |
| 17.| **雨云** | 雨云DNS解析提供商 |
| 18.| **Technitium DNS Server** | Technitium DNS Server 自建DNS服务器 |
| 19.| **腾讯云** | 腾讯云域名DNS解析提供者 |
| 20.| **腾讯云EO DNS** | 腾讯云EO DNS解析提供者 |
| 4.| **腾讯云** | 腾讯云域名DNS解析提供者 |
| 5.| **腾讯云EO DNS** | 腾讯云EO DNS解析提供者 |
| 6.| **Azure DNS** | Azure DNS 解析提供商 |
| 7.| **火山引擎** | 火山引擎DNS解析提供商 |
| 8.| **京东云** | 京东云DNS解析提供商 |
| 9.| **新网(代理方式)** | 新网域名解析(代理方式) |
| 10.| **新网** | 新网域名解析 |
| 11.| **BIND9 DNS** | 通过 SSH 连接到 BIND9 服务器,使用 nsupdate 命令管理 DNS 记录 |
| 12.| **cloudflare** | cloudflare dns provider |
| 13.| **dns.la** | dns.la |
| 14.| **Dynadot** | Dynadot DNS提供商 |
| 15.| **godaddy** | GoDaddy |
| 16.| **HiPM DNSMgr** | HiPM DNSMgr DNS 解析提供商 |
| 17.| **华为云** | 华为云DNS解析提供商 |
| 18.| **namesilo** | namesilo dns provider |
| 19.| **雨云** | 雨云DNS解析提供商 |
| 20.| **Technitium DNS Server** | Technitium DNS Server 自建DNS服务器 |
| 21.| **西部数码** | west dns provider |
| 22.| **Google Cloud DNS** | Google Cloud DNS提供商 |
| 23.| **Dns提供商Demo** | dns provider示例 |
+1 -1
View File
@@ -9,5 +9,5 @@
}
},
"npmClient": "pnpm",
"version": "1.42.2"
"version": "1.43.0"
}
+1
View File
@@ -39,6 +39,7 @@
"test:unit": "cross-env NODE_ENV=unittest pnpm -r --workspace-concurrency=1 run test:unit",
"pub": "echo 1",
"dev": "pnpm run -r --parallel compile ",
"lint_all": "pnpm run -r --parallel lint ",
"pub_all": "node ./scripts/pub-all.js",
"release": "time /t >trigger/release.trigger && git add trigger/release.trigger && git commit -m \"build: release\" && git push",
"publish_to_atomgit": "node --experimental-json-modules ./scripts/publish-atomgit.js",
+5 -5
View File
@@ -4,10 +4,6 @@
"ecmaVersion": 2022,
"sourceType": "module"
},
"ignorePatterns": [
"dist",
"node_modules"
],
"extends": [
"plugin:prettier/recommended",
"prettier"
@@ -36,5 +32,9 @@
"ignoreUrls": true
}
]
}
},
"ignorePatterns": [
"node_modules/**",
"dist/**"
]
}
+20
View File
@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.43.0](https://github.com/publishlab/node-acme-client/compare/v1.42.6...v1.43.0) (2026-08-14)
**Note:** Version bump only for package @certd/acme-client
## [1.42.6](https://github.com/publishlab/node-acme-client/compare/v1.42.5...v1.42.6) (2026-07-18)
**Note:** Version bump only for package @certd/acme-client
## [1.42.5](https://github.com/publishlab/node-acme-client/compare/v1.42.4...v1.42.5) (2026-07-15)
**Note:** Version bump only for package @certd/acme-client
## [1.42.4](https://github.com/publishlab/node-acme-client/compare/v1.42.3...v1.42.4) (2026-07-11)
**Note:** Version bump only for package @certd/acme-client
## [1.42.3](https://github.com/publishlab/node-acme-client/compare/v1.42.2...v1.42.3) (2026-07-08)
**Note:** Version bump only for package @certd/acme-client
## [1.42.2](https://github.com/publishlab/node-acme-client/compare/v1.42.1...v1.42.2) (2026-07-07)
**Note:** Version bump only for package @certd/acme-client
+4 -4
View File
@@ -3,7 +3,7 @@
"description": "Simple and unopinionated ACME client",
"private": false,
"author": "nmorsman",
"version": "1.42.2",
"version": "1.43.0",
"type": "module",
"module": "./dist/index.js",
"main": "./dist/index.js",
@@ -18,7 +18,7 @@
"types"
],
"dependencies": {
"@certd/basic": "^1.42.2",
"@certd/basic": "^1.43.0",
"@peculiar/x509": "^1.11.0",
"asn1js": "^3.0.5",
"axios": "^1.9.0",
@@ -50,7 +50,7 @@
"scripts": {
"before-build": "node -e \"const fs=require('fs');fs.rmSync('dist',{recursive:true,force:true});fs.rmSync('tsconfig.tsbuildinfo',{force:true});\"",
"build": "npm run before-build && tsc -p tsconfig.build.json --skipLibCheck",
"lint": "eslint \"src/**/*.ts\" \"types/**/*.ts\"",
"lint": "eslint --fix \"src/**/*.ts\" \"types/**/*.ts\"",
"lint-types": "tsd --files \"types/index.test-d.ts\"",
"prepublishOnly": "npm run build",
"test": "mocha -t 60000 \"test/setup.js\" \"test/**/*.spec.js\"",
@@ -75,5 +75,5 @@
"bugs": {
"url": "https://github.com/publishlab/node-acme-client/issues"
},
"gitHead": "7808d028e9c7e1b1e3ebc41f12b023d7b553300b"
"gitHead": "246ee83015bf5589adc2a5fa3d1388c8d9a2a252"
}
+5 -1
View File
@@ -18,5 +18,9 @@
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-unused-vars": "off"
}
},
"ignorePatterns": [
"node_modules/**",
"dist/**"
]
}
+20
View File
@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.43.0](https://github.com/certd/certd/compare/v1.42.6...v1.43.0) (2026-08-14)
**Note:** Version bump only for package @certd/basic
## [1.42.6](https://github.com/certd/certd/compare/v1.42.5...v1.42.6) (2026-07-18)
**Note:** Version bump only for package @certd/basic
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
**Note:** Version bump only for package @certd/basic
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
**Note:** Version bump only for package @certd/basic
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
**Note:** Version bump only for package @certd/basic
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
**Note:** Version bump only for package @certd/basic
+1 -1
View File
@@ -1 +1 @@
19:56
23:03
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/basic",
"private": false,
"version": "1.42.2",
"version": "1.43.0",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -17,7 +17,7 @@
"pub": "npm publish",
"compile": "tsc --skipLibCheck --watch",
"format": "prettier --write src",
"lint": "eslint --fix"
"lint": "eslint --fix --ext .ts src"
},
"dependencies": {
"async-lock": "^1.4.1",
@@ -54,5 +54,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "7808d028e9c7e1b1e3ebc41f12b023d7b553300b"
"gitHead": "246ee83015bf5589adc2a5fa3d1388c8d9a2a252"
}
+6 -3
View File
@@ -11,9 +11,12 @@ export class LocalCache<V = any> {
cache: Map<string, { value: V; expiresAt: number }>;
constructor(opts: { clearInterval?: number } = {}) {
this.cache = new Map();
const intervalId = setInterval(() => {
this.clearExpires();
}, opts.clearInterval ?? 5 * 60 * 1000);
const intervalId = setInterval(
() => {
this.clearExpires();
},
opts.clearInterval ?? 5 * 60 * 1000
);
intervalId.unref?.();
}
+2 -2
View File
@@ -1,4 +1,4 @@
export function isDev() {
const nodeEnv = process.env.NODE_ENV || 'dev';
return nodeEnv === 'development' || nodeEnv.includes('local') || nodeEnv.startsWith('dev');
const nodeEnv = process.env.NODE_ENV || "dev";
return nodeEnv === "development" || nodeEnv.includes("local") || nodeEnv.startsWith("dev");
}
+2 -2
View File
@@ -1,8 +1,8 @@
import fs from 'fs';
import fs from "fs";
function getFileRootDir(rootDir?: string) {
if (rootDir == null) {
const userHome = process.env.HOME || process.env.USERPROFILE;
rootDir = userHome + '/.certd/storage/';
rootDir = userHome + "/.certd/storage/";
}
if (!fs.existsSync(rootDir)) {
+1 -1
View File
@@ -1,2 +1,2 @@
import mitt from 'mitt';
import mitt from "mitt";
export const mitter = mitt();
+5 -1
View File
@@ -18,5 +18,9 @@
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-unused-vars": "off"
}
},
"ignorePatterns": [
"node_modules/**",
"dist/**"
]
}
+41
View File
@@ -3,6 +3,47 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.43.0](https://github.com/certd/certd/compare/v1.42.6...v1.43.0) (2026-08-14)
### Bug Fixes
* **pipeline:** 修复过期/损坏的安装锁导致的依赖安装失败 ([1017a38](https://github.com/certd/certd/commit/1017a38eb48d6778745d4905d9ec4f6067a261d8))
### Features
* **pipeline:** 新增流水线批量编辑、阶段宽度调整功能 ([e5a1844](https://github.com/certd/certd/commit/e5a18448522be88bc504a45a46ad599f3c39ca30))
### Performance Improvements
* 统一通知类型常量管理,新增开放接口通知能力 ([edb249c](https://github.com/certd/certd/commit/edb249c2ad3c173dbfd04d8d156f895a6706ffd1))
* 优化第三方依赖安装失败日志输出 ([18b2d3a](https://github.com/certd/certd/commit/18b2d3ac2078d0c6636af897f8f66e4eb5ff832c))
## [1.42.6](https://github.com/certd/certd/compare/v1.42.5...v1.42.6) (2026-07-18)
### Performance Improvements
* 优化动态加载依赖镜像地址,多次重试 ([5f53b81](https://github.com/certd/certd/commit/5f53b81c75dd242b4260ac08cae14c6d1a08a883))
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
### Bug Fixes
* 修复上传到cos报runtimeDepsService未初始化的问题 ([167b303](https://github.com/certd/certd/commit/167b303faeca02cc11cf97e4be2a3df914852167))
### Performance Improvements
* 优化vke keubconfig获取方式,改成先查询,如果没有再创建临时config ([604fa5b](https://github.com/certd/certd/commit/604fa5be634d099d797bfee5c2b0f26ce0ac8461))
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
### Bug Fixes
* **pipeline:** 重构运行时依赖加载逻辑,修复火山引擎DNS解析报runtimeDepsService未初始化的bug ([ec69b8f](https://github.com/certd/certd/commit/ec69b8f11bfd4b20991aef74a72a47182ca79a9d))
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
**Note:** Version bump only for package @certd/pipeline
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
**Note:** Version bump only for package @certd/pipeline
+5 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/pipeline",
"private": false,
"version": "1.42.2",
"version": "1.43.0",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -18,11 +18,11 @@
"pub": "npm publish",
"compile": "tsc --skipLibCheck --watch",
"format": "prettier --write src",
"lint": "eslint --fix"
"lint": "eslint --fix --ext .ts src"
},
"dependencies": {
"@certd/basic": "^1.42.2",
"@certd/plus-core": "^1.42.2",
"@certd/basic": "^1.43.0",
"@certd/plus-core": "^1.43.0",
"dayjs": "^1.11.7",
"lodash-es": "^4.17.21",
"reflect-metadata": "^0.2.2"
@@ -51,5 +51,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "7808d028e9c7e1b1e3ebc41f12b023d7b553300b"
"gitHead": "246ee83015bf5589adc2a5fa3d1388c8d9a2a252"
}
+2 -12
View File
@@ -3,7 +3,7 @@ import { FormItemProps } from "../dt/index.js";
import { HttpClient, ILogger, utils } from "@certd/basic";
import * as _ from "lodash-es";
import { PluginRequestHandleReq } from "../plugin/index.js";
import { IRuntimeDepsService, IServiceGetter } from "../service/index.js";
import { IServiceGetter, getRuntimeDepsService } from "../service/index.js";
// export type AccessRequestHandleReqInput<T = any> = {
// id?: number;
@@ -48,23 +48,13 @@ export type AccessContext = {
export abstract class BaseAccess implements IAccess {
ctx!: AccessContext;
runtimeDepsService?: IRuntimeDepsService;
async importRuntime(specifier: string) {
if (!this.runtimeDepsService) {
throw new Error("runtimeDepsService 未初始化");
}
return await this.runtimeDepsService.importRuntime(specifier, this.ctx.logger);
return await getRuntimeDepsService().importRuntime(specifier, this.ctx.logger);
}
async setCtx(ctx: AccessContext) {
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) {
@@ -47,7 +47,7 @@ export function AccessInput(input?: AccessInputDefine): PropertyDecorator {
};
}
export async function newAccess(type: string, input: any, accessService: IAccessService, ctx?: AccessContext) {
export async function newAccess(type: string, input: any, accessService: IAccessService, ctx: AccessContext) {
const register = accessRegistry.get(type);
if (register == null) {
throw new Error(`access ${type} not found`);
+3 -3
View File
@@ -10,7 +10,7 @@ import { Decorator } from "../decorator/index.js";
import { ICnameProxyService, IEmailService, IPluginConfigService, IServiceGetter, IUrlService } from "../service/index.js";
import { FileStore } from "./file-store.js";
import { cloneDeep, forEach, merge } from "lodash-es";
import { INotificationService } from "../notification/index.js";
import { INotificationService, PipelineNotificationTypes } from "../notification/index.js";
import { taskEmitterCreate } from "../service/emit.js";
import { RunnableError } from "./exceptions.js";
@@ -480,7 +480,7 @@ export class Executor {
if (notification.type === "email" && notification.options?.receivers) {
try {
await this.options.emailService?.sendByTemplate({
type: "pipelineResult",
type: PipelineNotificationTypes.PipelineResult,
data: templateData,
receivers: notification.options?.receivers,
});
@@ -496,7 +496,7 @@ export class Executor {
useEmail: false,
logger: this.logger,
body: {
notificationType: "pipelineResult",
notificationType: PipelineNotificationTypes.PipelineResult,
title: subject,
content,
userId: this.pipeline.userId,
@@ -47,6 +47,10 @@ export type Stage = Runnable & {
concurrency: ConcurrencyStrategy;
next: NextStrategy;
maxTaskCount?: number;
style?: {
width?: number;
[key: string]: any;
};
};
export type Trigger = {
+6 -12
View File
@@ -3,7 +3,7 @@ import { Registrable } from "../registry/index.js";
import { FormItemProps, HistoryResult, Pipeline } from "../dt/index.js";
import { HttpClient, ILogger, utils } from "@certd/basic";
import * as _ from "lodash-es";
import { IEmailService, IRuntimeDepsService, IServiceGetter } from "../service/index.js";
import { IEmailService, IServiceGetter, getRuntimeDepsService } from "../service/index.js";
export type NotificationBody = {
userId?: number;
@@ -89,16 +89,16 @@ export abstract class BaseNotification implements INotification {
ctx!: NotificationContext;
http!: HttpClient;
logger!: ILogger;
runtimeDepsService?: IRuntimeDepsService;
async importRuntime(specifier: string) {
if (!this.runtimeDepsService) {
return await import(specifier);
}
return await this.runtimeDepsService.importRuntime(specifier, this.logger);
return await getRuntimeDepsService().importRuntime(specifier, this.logger);
}
async doSend(body: NotificationBody) {
if (body.content) {
const content = body.content?.replace(/\n/g, " \n");
body.content = content;
}
return await this.send(body);
}
abstract send(body: NotificationBody): Promise<void>;
@@ -109,12 +109,6 @@ export abstract class BaseNotification implements INotification {
this.ctx = ctx;
this.http = ctx.http;
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) => {
this.define = define;
@@ -0,0 +1,11 @@
/// <reference types="mocha" />
/// <reference types="node" />
import assert from "node:assert/strict";
import { PipelineNotificationTypes } from "./constants.js";
describe("PipelineNotificationTypes", () => {
it("contains the pipeline result notification type", () => {
assert.equal(PipelineNotificationTypes.PipelineResult, "pipelineResult");
});
});
@@ -0,0 +1,6 @@
/** 流水线核心使用的通知类型。业务层通知类型由 @certd/lib-server 扩展。 */
export const PipelineNotificationTypes = {
PipelineResult: "pipelineResult",
} as const;
export type PipelineNotificationType = (typeof PipelineNotificationTypes)[keyof typeof PipelineNotificationTypes];
@@ -1,3 +1,4 @@
export * from "./api.js";
export * from "./constants.js";
export * from "./registry.js";
export * from "./decorator.js";
+3 -13
View File
@@ -10,7 +10,7 @@ import { INotificationService } from "../notification/index.js";
import { Registrable } from "../registry/index.js";
import { IPluginConfigService } from "../service/config.js";
import { TaskEmitter } from "../service/emit.js";
import { ICnameProxyService, IEmailService, IRuntimeDepsService, IServiceGetter, IUrlService } from "../service/index.js";
import { ICnameProxyService, IEmailService, IServiceGetter, IUrlService, getRuntimeDepsService } from "../service/index.js";
export type PluginRequestHandleReq<T = any> = {
typeName: string;
@@ -76,7 +76,7 @@ export type ITaskPlugin = {
execute(): Promise<void | string>;
onRequest(req: PluginRequestHandleReq<any>): Promise<any>;
setCtx(ctx: TaskInstanceContext): Promise<void>;
importRuntime?(specifier: string): Promise<any>;
importRuntime(specifier: string): Promise<any>;
[key: string]: any;
};
@@ -150,13 +150,9 @@ export abstract class AbstractTaskPlugin implements ITaskPlugin {
logger!: ILogger;
http!: HttpClient;
accessService!: IAccessService;
runtimeDepsService!: IRuntimeDepsService;
async importRuntime(specifier: string) {
if (!this.runtimeDepsService) {
throw new Error("runtimeDepsService 未初始化");
}
return await this.runtimeDepsService.importRuntime(specifier, this.logger);
return await getRuntimeDepsService().importRuntime(specifier, this.logger);
}
clearLastStatus() {
@@ -178,12 +174,6 @@ export abstract class AbstractTaskPlugin implements ITaskPlugin {
this.logger = ctx.logger;
this.accessService = ctx.accessService;
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
// @ts-ignore
if (this.cert && this.cert.crt && this.cert.key) {
@@ -0,0 +1,349 @@
import assert from "assert";
import fs from "fs";
import path from "path";
import os from "os";
import { RuntimeDepsService, NpmRegistryResolver } from "./runtime.js";
describe("RuntimeDepsService", () => {
it("builds a runtime package manifest in the target directory", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-"));
const service = new RuntimeDepsService({ rootDir }, null);
service.registryResolver = {
async resolveOrdered() {
return ["https://registry.npmmirror.com"];
},
} as any;
service.commandRunner = {
async run(command: string, args: string[]) {
assert.equal(command, "pnpm");
assert.equal(args[0], "install");
assert.ok(args.includes("--ignore-workspace"));
assert.ok(args.includes("--no-frozen-lockfile"));
return { stdout: "", stderr: "", code: 0 };
},
} as any;
const result = await service.ensureDependencies({ dependencies: { foo: "^1.0.0" } });
assert.equal(result.registryUrl, "https://registry.npmmirror.com");
assert.ok(fs.existsSync(path.join(rootDir, "package.json")));
});
it("installs direct dependency maps without plugin metadata", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-direct-"));
const service = new RuntimeDepsService({ rootDir }, null);
service.registryResolver = {
async resolveOrdered() {
return [""];
},
} as any;
service.commandRunner = {
async run(command: string, args: string[]) {
assert.equal(command, "pnpm");
fs.mkdirSync(path.join(rootDir, "node_modules"), { recursive: true });
return { stdout: "", stderr: "", code: 0 };
},
} as any;
await service.ensureDependencies({ dependencies: { directPkg: "^1.0.0" } });
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
assert.deepEqual(manifest.dependencies, { directPkg: "^1.0.0" });
});
it("imports from runtime node_modules without installing", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-import-"));
const packageDir = path.join(rootDir, "node_modules", "runtime-only");
fs.mkdirSync(packageDir, { recursive: true });
fs.writeFileSync(path.join(rootDir, "package.json"), JSON.stringify({ name: "runtime-root", type: "module" }), "utf8");
fs.writeFileSync(path.join(packageDir, "package.json"), JSON.stringify({ name: "runtime-only", type: "module", main: "index.js" }), "utf8");
fs.writeFileSync(path.join(packageDir, "index.js"), "export const value = 42;\n", "utf8");
const service = new RuntimeDepsService({ rootDir }, null);
service.commandRunner = {
async run() {
throw new Error("install should not run");
},
} as any;
const mod = await service.importRuntime("runtime-only");
assert.equal(mod.value, 42);
});
it("installs configured lazy dependency when import target is missing", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-lazy-"));
const service = new RuntimeDepsService({ rootDir, lazyDependencies: { "lazy-pkg": "^1.2.3" } }, null);
service.registryResolver = {
async resolveOrdered() {
return [""];
},
} as any;
service.commandRunner = {
async run(command: string, args: string[]) {
assert.equal(command, "pnpm");
const packageDir = path.join(rootDir, "node_modules", "lazy-pkg", "sub");
fs.mkdirSync(packageDir, { recursive: true });
fs.writeFileSync(path.join(rootDir, "node_modules", "lazy-pkg", "package.json"), JSON.stringify({ name: "lazy-pkg", type: "module" }), "utf8");
fs.writeFileSync(path.join(packageDir, "entry.js"), "export const value = 7;\n", "utf8");
return { stdout: "", stderr: "", code: 0 };
},
} as any;
const mod = await service.importRuntime("lazy-pkg/sub/entry.js");
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
assert.deepEqual(manifest.dependencies, { "lazy-pkg": "^1.2.3" });
assert.equal(mod.value, 7);
});
it("reports lazy dependency installation failure without falling back to project dependencies", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-lazy-failed-"));
const service = new RuntimeDepsService({ rootDir, lazyDependencies: { "failed-pkg": "^1.2.3" } }, null);
service.registryResolver = {
async resolveOrdered() {
return [""];
},
} as any;
service.commandRunner = {
async run(command: string, args: string[]) {
assert.equal(command, "pnpm");
return { stdout: "", stderr: "package download failed", code: 1 };
},
} as any;
await assert.rejects(() => service.importRuntime("failed-pkg/sub/entry.js"), /动态依赖安装失败: failed-pkg: 动态依赖安装失败: package download failed/);
});
it("resolves scoped package names for lazy imports", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-scoped-"));
const service = new RuntimeDepsService({ rootDir, lazyDependencies: { "@scope/lazy": "^2.0.0" } }, null);
service.registryResolver = {
async resolveOrdered() {
return [""];
},
} as any;
service.commandRunner = {
async run(command: string, args: string[]) {
assert.equal(command, "pnpm");
const packageDir = path.join(rootDir, "node_modules", "@scope", "lazy", "dist");
fs.mkdirSync(packageDir, { recursive: true });
fs.writeFileSync(path.join(rootDir, "node_modules", "@scope", "lazy", "package.json"), JSON.stringify({ name: "@scope/lazy", type: "module" }), "utf8");
fs.writeFileSync(path.join(packageDir, "index.js"), "export const scoped = true;\n", "utf8");
return { stdout: "", stderr: "", code: 0 };
},
} as any;
const mod = await service.importRuntime("@scope/lazy/dist/index.js");
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
assert.deepEqual(manifest.dependencies, { "@scope/lazy": "^2.0.0" });
assert.equal(mod.scoped, true);
});
it("reports missing lazy dependency configuration", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-lazy-missing-"));
const service = new RuntimeDepsService({ rootDir, lazyDependencies: {} }, null);
await assert.rejects(() => service.importRuntime("missing-pkg/sub.js"), /未配置懒加载版本: missing-pkg/);
});
it("rejects lazy dependency installation when disabled", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-disabled-"));
const service = new RuntimeDepsService({ rootDir, enabled: false, lazyDependencies: { "lazy-pkg": "^1.0.0" } }, null);
service.commandRunner = {
async run() {
throw new Error("disabled runtime dependencies should not install packages");
},
} as any;
await assert.rejects(() => service.importRuntime("lazy-pkg/index.js"), /动态安装依赖未开启/);
});
it("prefers project dependencies over lazy dependency installation", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-project-fallback-"));
const service = new RuntimeDepsService({ rootDir, lazyDependencies: { dayjs: "^1.11.0" } }, null);
service.commandRunner = {
async run() {
throw new Error("project dependency should not trigger installation");
},
} as any;
const mod = await service.importRuntime("dayjs");
assert.equal(typeof mod.default, "function");
});
it("rejects a second installation while the first installation is running", async () => {
const rootDir = path.join(fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-concurrent-")), ".runtime-deps");
const service = new RuntimeDepsService({ rootDir }, null);
service.registryResolver = {
async resolveOrdered() {
return [""];
},
} as any;
let finishInstall: () => void;
const installFinished = new Promise<void>(resolve => {
finishInstall = resolve;
});
service.commandRunner = {
async run() {
await installFinished;
return { stdout: "", stderr: "", code: 0 };
},
} as any;
const firstInstall = service.ensureDependencies({ dependencies: { first: "^1.0.0" } });
await new Promise(resolve => setImmediate(resolve));
await assert.rejects(() => service.ensureDependencies({ dependencies: { second: "^1.0.0" } }), /动态安装依赖正在执行中/);
await assert.rejects(() => service.clearRuntimeDeps(), /仍有依赖正在安装/);
finishInstall!();
await firstInstall;
});
it("releases the installation lock after an installation failure", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-retry-"));
const service = new RuntimeDepsService({ rootDir }, null);
service.registryResolver = {
async resolveOrdered() {
return [""];
},
} as any;
let installCount = 0;
service.commandRunner = {
async run() {
installCount += 1;
if (installCount === 1) {
return { stdout: "", stderr: "installation timeout", code: 1 };
}
return { stdout: "", stderr: "", code: 0 };
},
} as any;
await assert.rejects(() => service.ensureDependencies({ dependencies: { first: "^1.0.0" } }), /动态依赖安装失败/);
await service.ensureDependencies({ dependencies: { second: "^1.0.0" } });
assert.equal(installCount, 2);
});
it("keeps previously installed dependencies when adding a later package", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-merge-"));
const service = new RuntimeDepsService({ rootDir }, null);
service.registryResolver = {
async resolveOrdered() {
return [""];
},
} as any;
service.commandRunner = {
async run(command: string, args: string[]) {
assert.equal(command, "pnpm");
if (args.includes("--version")) {
return { stdout: "9.1.0\n", stderr: "", code: 0 };
}
fs.mkdirSync(path.join(rootDir, "node_modules"), { recursive: true });
return { stdout: "", stderr: "", code: 0 };
},
} as any;
await service.ensureDependencies({ dependencies: { foo: "^1.0.0" } });
await service.ensureDependencies({ dependencies: { bar: "^2.0.0" } });
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
assert.deepEqual(manifest.dependencies, { foo: "^1.0.0", bar: "^2.0.0" });
});
it("does not pass node debugger options to pnpm child process", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-env-"));
const oldNodeOptions = process.env.NODE_OPTIONS;
const oldInspectorOptions = process.env.VSCODE_INSPECTOR_OPTIONS;
process.env.NODE_OPTIONS = "--inspect=127.0.0.1:9229 --max-old-space-size=4096";
process.env.VSCODE_INSPECTOR_OPTIONS = '{"inspectorIpc":"test"}';
try {
const service = new RuntimeDepsService({ rootDir }, null);
service.registryResolver = {
async resolveOrdered() {
return [""];
},
} as any;
service.commandRunner = {
async run(command: string, args: string[], options: { env?: NodeJS.ProcessEnv }) {
assert.equal(options.env?.NODE_OPTIONS, "--max-old-space-size=4096");
assert.equal(options.env?.VSCODE_INSPECTOR_OPTIONS, undefined);
assert.equal(options.env?.CI, "true");
assert.equal(options.env?.pnpm_config_confirm_modules_purge, "false");
return { stdout: "", stderr: "", code: 0 };
},
} as any;
await service.ensureDependencies({ dependencies: { foo: "^1.0.0" } });
} finally {
if (oldNodeOptions == null) {
delete process.env.NODE_OPTIONS;
} else {
process.env.NODE_OPTIONS = oldNodeOptions;
}
if (oldInspectorOptions == null) {
delete process.env.VSCODE_INSPECTOR_OPTIONS;
} else {
process.env.VSCODE_INSPECTOR_OPTIONS = oldInspectorOptions;
}
}
});
it("rejects clearing unexpected runtime dependency path", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-clear-invalid-"));
const service = new RuntimeDepsService({ rootDir }, null);
await assert.rejects(() => service.clearRuntimeDeps(), /动态依赖目录配置异常/);
});
});
describe("NpmRegistryResolver", () => {
it("uses fixed registry without probing", async () => {
const resolver = new NpmRegistryResolver({
mode: "fixed",
fixedUrl: "https://registry.example.com",
probeTimeoutMs: 100,
cacheTtlMs: 1000,
});
const result = await resolver.resolveOrdered();
assert.deepEqual(result, ["https://registry.example.com"]);
});
it("returns ordered list via resolveOrdered (fastest first)", async () => {
const resolver = new NpmRegistryResolver({
mode: "auto",
candidates: ["https://slow.example.com", "https://fast.example.com"],
probeTimeoutMs: 100,
cacheTtlMs: 1000,
});
resolver.probe = async (registryUrl: string) => ({
registryUrl,
ok: true,
elapsedMs: registryUrl.includes("fast") ? 10 : 50,
});
const result = await resolver.resolveOrdered();
assert.deepEqual(result, ["https://fast.example.com", "https://slow.example.com"]);
});
it("includes failed registries at the end of resolveOrdered", async () => {
const resolver = new NpmRegistryResolver({
mode: "auto",
candidates: ["https://good.example.com", "https://bad.example.com"],
probeTimeoutMs: 100,
cacheTtlMs: 1000,
});
resolver.probe = async (registryUrl: string) => {
if (registryUrl.includes("bad")) {
return { registryUrl, ok: false, elapsedMs: 200 };
}
return { registryUrl, ok: true, elapsedMs: 30 };
};
const result = await resolver.resolveOrdered();
assert.deepEqual(result, ["https://good.example.com", "https://bad.example.com"]);
});
it("returns empty ordered list when no candidates", async () => {
const resolver = new NpmRegistryResolver({ mode: "auto", candidates: [] });
const result = await resolver.resolveOrdered();
assert.deepEqual(result, []);
});
it("re-validates cached URL on resolveOrdered call", async () => {
let probeCount = 0;
const resolver = new NpmRegistryResolver({
mode: "auto",
candidates: ["https://mirror.example.com"],
cacheTtlMs: 60000,
});
resolver.probe = async (registryUrl: string) => {
probeCount++;
return { registryUrl, ok: true, elapsedMs: 10 };
};
const first = await resolver.resolveOrdered();
assert.deepEqual(first, ["https://mirror.example.com"]);
assert.equal(probeCount, 1);
const second = await resolver.resolveOrdered();
assert.deepEqual(second, ["https://mirror.example.com"]);
assert.equal(probeCount, 2);
});
});
+522 -20
View File
@@ -1,27 +1,529 @@
/**
* 运行时动态导入函数类型
*/
export type ImportRuntime = (specifier: string, logger?: ILogger) => Promise<any>;
/**
* 日志接口
*/
import { logger as defaultLogger } from "@certd/basic";
import { spawn } from "child_process";
import fs from "fs";
import { createRequire } from "module";
import path from "path";
import { pathToFileURL } from "url";
import type { Registry } from "../registry/registry.js";
export type ILogger = {
info: (message: string) => void;
warn?: (message: string) => void;
error?: (message: string, ...args: any[]) => void;
};
/**
* 运行时依赖服务参数
*/
export type EnsureRuntimeDepsOptions = {
pluginKeys: string | string[];
logger?: ILogger;
type InstallResult = {
registryUrl: string;
packageJsonPath: string;
};
/**
* 运行时依赖服务接口
*/
export interface IRuntimeDepsService {
ensureRuntimeDependencies(options: EnsureRuntimeDepsOptions): Promise<any>;
importRuntime: ImportRuntime;
type RuntimeImportResolveResult = {
resolved: string;
packageName: string;
};
type CommandRunnerResult = {
stdout: string;
stderr: string;
code: number;
};
type CommandRunner = {
run(command: string, args: string[], options: { cwd: string; timeoutMs: number; env?: NodeJS.ProcessEnv }): Promise<CommandRunnerResult>;
};
export type NpmRegistryResolverConfig = {
mode?: "auto" | "fixed" | "system";
fixedUrl?: string;
candidates?: string[];
probeTimeoutMs?: number;
cacheTtlMs?: number;
};
export type RegistryProbeResult = {
registryUrl: string;
ok: boolean;
elapsedMs: number;
};
export class NpmRegistryResolver {
config: NpmRegistryResolverConfig;
private cache?: { orderedUrls: string[]; expiresAt: number };
constructor(config?: NpmRegistryResolverConfig) {
this.config = config || {};
}
async resolveOrdered(): 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()) {
const fastUrl = cached.orderedUrls[0];
if (fastUrl) {
const probeResult = await this.probe(fastUrl);
if (probeResult.ok) {
return cached.orderedUrls;
}
}
this.cache = undefined;
}
const candidates = (config?.candidates || []).filter(Boolean);
if (candidates.length === 0) {
return [];
}
const orderedUrls = await this.internalProbeAll(candidates);
this.cache = { orderedUrls, expiresAt: Date.now() + (config?.cacheTtlMs ?? 300_000) };
return orderedUrls;
}
private async internalProbeAll(candidates: string[]): Promise<string[]> {
const probes = await Promise.allSettled(candidates.map(registryUrl => this.probe(registryUrl)));
const okList: RegistryProbeResult[] = [];
const failList: RegistryProbeResult[] = [];
for (const item of probes) {
const result = item.status === "fulfilled" ? item.value : null;
if (result && result.ok) {
okList.push(result);
} else if (result) {
failList.push(result);
}
}
okList.sort((a, b) => a.elapsedMs - b.elapsedMs);
failList.sort((a, b) => a.elapsedMs - b.elapsedMs);
return [...okList.map(r => r.registryUrl), ...failList.map(r => r.registryUrl)];
}
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 };
}
}
}
export type RuntimeDepsConfig = {
rootDir?: string;
enabled?: boolean;
installTimeoutMs?: number;
pnpmCommand?: string;
lazyDependencies?: Record<string, string>;
registry?: NpmRegistryResolverConfig;
};
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];
}
let INSTALL_LOCKER: any = null;
class DefaultCommandRunner implements CommandRunner {
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, 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 });
});
});
}
}
export class RuntimeDepsService {
runtimeDepsRootDir: string;
enabled: boolean;
installTimeoutMs: number;
pnpmCommand: string;
lazyDependencies: Record<string, string>;
registryResolver!: NpmRegistryResolver;
commandRunner: CommandRunner = new DefaultCommandRunner();
pluginLazyDependencies: Record<string, string> = {};
private registriesMap: Record<string, { registry: Registry<any>; pluginType: string; addonType?: string }> | null = null;
constructor(config: RuntimeDepsConfig, registries: any) {
this.runtimeDepsRootDir = config?.rootDir ?? "./data/.runtime-deps";
this.enabled = config?.enabled ?? true;
this.installTimeoutMs = config?.installTimeoutMs ?? 60000;
this.pnpmCommand = config?.pnpmCommand ?? "";
this.lazyDependencies = config?.lazyDependencies ?? {};
this.registryResolver = new NpmRegistryResolver(config?.registry);
if (registries) {
this.setRegistries(registries);
}
}
setRegistries(registries: { pluginRegistry?: Registry<any>; accessRegistry?: Registry<any>; notificationRegistry?: Registry<any>; dnsProviderRegistry?: Registry<any>; addonRegistry?: Registry<any> }) {
const map: Record<string, { registry: Registry<any>; pluginType: string; addonType?: string }> = {};
if (registries.pluginRegistry) {
map["plugin"] = { registry: registries.pluginRegistry, pluginType: "plugin" };
}
if (registries.accessRegistry) {
map["access"] = { registry: registries.accessRegistry, pluginType: "access" };
}
if (registries.notificationRegistry) {
map["notification"] = { registry: registries.notificationRegistry, pluginType: "notification" };
}
if (registries.dnsProviderRegistry) {
map["dnsProvider"] = { registry: registries.dnsProviderRegistry, pluginType: "dnsProvider" };
}
if (registries.addonRegistry) {
map["addon"] = { registry: registries.addonRegistry, pluginType: "addon", addonType: "" };
}
this.registriesMap = map;
}
async ensureDependencies(options: { dependencies: Record<string, string>; logger?: ILogger }): Promise<InstallResult> {
const { dependencies, logger: log } = options;
if (INSTALL_LOCKER) {
throw new Error("动态安装依赖正在执行中,请稍后再试");
}
INSTALL_LOCKER = true;
const installPromise = this.doEnsureInstalled({ dependencies, logger: log }).finally(() => {
INSTALL_LOCKER = null;
});
// 25s 内如果没返回就提前返回
// AI注意,不要改我的实现,这里提前在25s就报超时是我故意这么做的实现
// 因为前端配置的超时时间是30s,大部分的情况下install会在25s内完成,所以这里提前在25s就报超时,并且容忍60s的install
const timeoutPromise: Promise<any> = new Promise((resolve, reject) => setTimeout(() => reject(new Error("安装依赖超时,请稍后重试")), 25 * 1000));
return await Promise.race([installPromise, timeoutPromise]);
}
async importRuntime(specifier: string, logger: ILogger = defaultLogger) {
if (this.isNativeImportSpecifier(specifier)) {
//是否本地模块
return await import(specifier);
}
let resolved: string;
try {
//尝试加载本项目依赖
resolved = this.resolveProjectSpecifier(specifier).resolved;
} catch (error) {
if (!this.isModuleNotFoundError(error)) {
throw error;
}
resolved = await this.resolveLazyOrInstallSpecifier(specifier, logger);
}
if (!resolved) {
throw new Error(`依赖未安装成功: ${specifier}`);
}
return await import(pathToFileURL(resolved).href);
}
//动态获取模块解析
private async resolveLazyOrInstallSpecifier(specifier: string, logger: ILogger = defaultLogger) {
try {
//解析动态目录里面的模块是否存在
return this.resolveRuntimeSpecifier(specifier).resolved;
} catch (runtimeError: any) {
if (!this.isModuleNotFoundError(runtimeError)) {
//其他错误
throw runtimeError;
}
if (!this.enabled) {
throw new Error("动态安装依赖未开启");
}
//模块不存在,安装
await this.installLazyDependencies(specifier, logger);
try {
return this.resolveRuntimeSpecifier(specifier).resolved;
} catch (lazyError: any) {
// logger?.error?.(`动态依赖安装失败: ${lazyError.message}`);
throw new Error(`依赖加载失败,可能动态依赖未安装成功: ${lazyError.message}`, { cause: lazyError });
}
}
}
//不存在模块动态依赖安装,再获取解析
private async installLazyDependencies(specifier: string, logger?: ILogger) {
const packageName = this.parsePackageName(specifier);
const mergedDeps = this.getMergedLazyDependencies();
const lazyRange = mergedDeps[packageName];
if (!lazyRange) {
throw new Error(`动态依赖未安装且未配置懒加载版本: ${packageName}`);
}
try {
await this.ensureDependencies({ dependencies: { [packageName]: lazyRange }, logger });
} catch (lazyError: any) {
// logger?.error?.(`动态依赖安装失败: ${lazyError.message}`);
throw new Error(`动态依赖安装失败: ${packageName}: ${lazyError.message}`, { cause: lazyError });
}
}
private isNativeImportSpecifier(specifier: string) {
return specifier.startsWith(".") || specifier.startsWith("/") || specifier.startsWith("file:") || specifier.startsWith("node:");
}
//尝试加载runtime依赖
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): RuntimeImportResolveResult {
const packageName = this.parsePackageName(specifier);
const packageJsonPath = path.resolve("package.json");
const require = createRequire(packageJsonPath);
const resolved = require.resolve(specifier);
return { packageName, resolved };
}
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 isModuleNotFoundError(error: any) {
return error?.code === "MODULE_NOT_FOUND" || error?.code === "ERR_MODULE_NOT_FOUND";
}
private async doEnsureInstalled(options: { dependencies: Record<string, string>; logger?: ILogger }): Promise<InstallResult> {
let { dependencies } = options;
const log = options.logger || defaultLogger;
const rootDir = this.getRuntimeDepsRootDir();
if (!fs.existsSync(rootDir)) {
fs.mkdirSync(rootDir, { recursive: true });
}
const packageJsonPath = path.join(rootDir, "package.json");
log.info(`第三方依赖安装: ${JSON.stringify(dependencies)}`);
dependencies = this.mergeInstalledDependencies(this.readManifestDependencies(packageJsonPath), dependencies);
const manifest = { name: "certd-runtime-deps", private: true, type: "module", dependencies };
fs.writeFileSync(packageJsonPath, JSON.stringify(manifest, null, 2), "utf8");
const allRegistryUrls = await this.registryResolver.resolveOrdered();
const urlsToTry = allRegistryUrls.length > 0 ? allRegistryUrls : [""];
const command = this.getPnpmCommand();
let lastError: string | undefined;
for (const tryUrl of urlsToTry) {
const args = ["install", "--prod", "--ignore-scripts", "--ignore-workspace", "--no-frozen-lockfile", "--reporter=append-only"];
if (tryUrl) {
args.push(`--registry=${tryUrl}`);
}
const tryEnv = this.buildChildEnv(tryUrl);
log.info(`开始安装第三方依赖: ${Object.keys(dependencies).join(", ")}${tryUrl ? `,镜像: ${tryUrl}` : ""}`);
const result = await this.commandRunner.run(command, args, { cwd: rootDir, timeoutMs: this.installTimeoutMs, env: tryEnv });
if (result.code === 0) {
log.info(`${result.stdout?.slice(-2000) || "无npm安装日志输出"}`);
log.info("第三方依赖安装完成");
return { registryUrl: tryUrl, packageJsonPath };
}
const errOutput = (result.stderr || "").trim();
const outOutput = (result.stdout || "").trim();
lastError = errOutput || outOutput || "unknown error";
log.info(`镜像 ${tryUrl || "默认"} 安装失败,退出码: ${result.code}${urlsToTry.length > 1 ? ",尝试下一个镜像..." : ""}`);
log.info(` pnpm stderr: ${(errOutput || "无npm安装日志输出").slice(-2000)}`);
if (outOutput) {
log.info(` pnpm stdout: ${outOutput.slice(-2000)}`);
}
}
throw new Error(`动态依赖安装失败: ${lastError}`);
}
async clearRuntimeDeps() {
const rootDir = this.getRuntimeDepsRootDir();
const normalizedRootDir = path.normalize(rootDir);
if (!normalizedRootDir.endsWith(path.normalize(".runtime-deps"))) {
throw new Error(`动态依赖目录配置异常,拒绝清理: ${rootDir}`);
}
if (INSTALL_LOCKER) {
throw new Error("仍有依赖正在安装,请稍后");
}
if (fs.existsSync(rootDir)) {
const entries = fs.readdirSync(rootDir);
for (const entry of entries) {
fs.rmSync(path.join(rootDir, entry), { recursive: true, force: true });
}
}
}
getMergedLazyDependencies(): Record<string, string> {
return { ...this.lazyDependencies, ...this.pluginLazyDependencies };
}
collectPluginDeps(logger?: ILogger) {
if (!this.registriesMap) {
return;
}
const deps: Record<string, string> = {};
for (const { registry } of Object.values(this.registriesMap)) {
const defineList = registry.getDefineList();
for (const define of defineList) {
const dependPackages = (define as any).dependPackages as Record<string, string> | undefined;
if (!dependPackages) {
continue;
}
for (const [pkgName, range] of Object.entries(dependPackages)) {
const existing = deps[pkgName];
if (existing && !areRangesCompatible(existing, range)) {
(logger || defaultLogger).warn?.(`懒加载依赖版本冲突: ${pkgName} => ${existing} vs ${range},保留已有版本`);
continue;
}
deps[pkgName] = range;
}
}
}
this.pluginLazyDependencies = deps;
(logger || defaultLogger).info(`从插件注册表收集到 ${Object.keys(deps).length} 个懒加载依赖`);
}
refreshPluginDeps(logger?: ILogger) {
this.collectPluginDeps(logger);
}
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;
}
private getPnpmCommand() {
return this.pnpmCommand || "pnpm";
}
private buildChildEnv(registryUrl: string) {
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(" ");
}
getRuntimeDepsRootDir() {
return path.resolve(this.runtimeDepsRootDir);
}
}
let runtimeDepsServiceInstance: RuntimeDepsService | null = null;
export function initRuntimeDepsService(config: RuntimeDepsConfig, registries: any): RuntimeDepsService {
runtimeDepsServiceInstance = new RuntimeDepsService(config, registries);
return runtimeDepsServiceInstance;
}
export function getRuntimeDepsService(): RuntimeDepsService {
if (!runtimeDepsServiceInstance) {
throw new Error("RuntimeDepsService 未初始化");
}
return runtimeDepsServiceInstance!;
}
export async function importRuntime(specifier: string, logger: ILogger = defaultLogger): Promise<any> {
return getRuntimeDepsService().importRuntime(specifier, logger);
}
+5 -1
View File
@@ -18,5 +18,9 @@
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-unused-vars": "off"
}
},
"ignorePatterns": [
"node_modules/**",
"dist/**"
]
}
+20
View File
@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.43.0](https://github.com/certd/certd/compare/v1.42.6...v1.43.0) (2026-08-14)
**Note:** Version bump only for package @certd/lib-huawei
## [1.42.6](https://github.com/certd/certd/compare/v1.42.5...v1.42.6) (2026-07-18)
**Note:** Version bump only for package @certd/lib-huawei
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
**Note:** Version bump only for package @certd/lib-huawei
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
**Note:** Version bump only for package @certd/lib-huawei
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
**Note:** Version bump only for package @certd/lib-huawei
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
**Note:** Version bump only for package @certd/lib-huawei
+4 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-huawei",
"private": false,
"version": "1.42.2",
"version": "1.43.0",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
"types": "./dist/d/index.d.ts",
@@ -15,7 +15,7 @@
"pub": "npm publish",
"compile": "npm run build",
"format": "prettier --write src",
"lint": "eslint --fix"
"lint": "eslint --fix --ext .ts src"
},
"dependencies": {
"axios": "^1.9.0",
@@ -26,9 +26,10 @@
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"cross-env": "^7.0.3",
"eslint": "^8.57.0",
"esmock": "^2.7.5",
"prettier": "3.3.3",
"tslib": "^2.8.1"
},
"gitHead": "7808d028e9c7e1b1e3ebc41f12b023d7b553300b"
"gitHead": "246ee83015bf5589adc2a5fa3d1388c8d9a2a252"
}
+5 -1
View File
@@ -18,5 +18,9 @@
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-unused-vars": "off"
}
},
"ignorePatterns": [
"node_modules/**",
"dist/**"
]
}
+20
View File
@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.43.0](https://github.com/certd/certd/compare/v1.42.6...v1.43.0) (2026-08-14)
**Note:** Version bump only for package @certd/lib-iframe
## [1.42.6](https://github.com/certd/certd/compare/v1.42.5...v1.42.6) (2026-07-18)
**Note:** Version bump only for package @certd/lib-iframe
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
**Note:** Version bump only for package @certd/lib-iframe
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
**Note:** Version bump only for package @certd/lib-iframe
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
**Note:** Version bump only for package @certd/lib-iframe
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
**Note:** Version bump only for package @certd/lib-iframe
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-iframe",
"private": false,
"version": "1.42.2",
"version": "1.43.0",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -18,7 +18,7 @@
"pub": "npm publish",
"compile": "npm run build",
"format": "prettier --write src",
"lint": "eslint --fix"
"lint": "eslint --fix --ext .ts src"
},
"dependencies": {
"nanoid": "^5.0.7"
@@ -37,5 +37,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "7808d028e9c7e1b1e3ebc41f12b023d7b553300b"
"gitHead": "246ee83015bf5589adc2a5fa3d1388c8d9a2a252"
}
+1 -1
View File
@@ -1 +1 @@
export * from './lib/iframe.client.js';
export * from "./lib/iframe.client.js";
@@ -1,4 +1,4 @@
import { nanoid } from 'nanoid';
import { nanoid } from "nanoid";
export type IframeMessageData<T> = {
action: string;
@@ -32,7 +32,7 @@ export class IframeClient {
constructor(iframe?: HTMLIFrameElement, onError?: (e: any) => void) {
this.iframe = iframe;
this.onError = onError;
window.addEventListener('message', async (event: MessageEvent<IframeMessageData<any>>) => {
window.addEventListener("message", async (event: MessageEvent<IframeMessageData<any>>) => {
const data = event.data;
if (data.action) {
console.log(`收到消息[isSub:${this.isInFrame()}]`, data);
@@ -40,20 +40,20 @@ export class IframeClient {
const handler = this.handlers[data.action];
if (handler) {
const res = await handler(data);
if (data.id && data.action !== 'reply') {
await this.send('reply', res, data.id);
if (data.id && data.action !== "reply") {
await this.send("reply", res, data.id);
}
} else {
throw new Error(`action:${data.action} 未注册处理器,可能版本过低`);
}
} catch (e: any) {
console.error(e);
await this.send('reply', {}, data.id, 500, e.message);
await this.send("reply", {}, data.id, 500, e.message);
}
}
});
this.register('reply', async data => {
this.register("reply", async data => {
const req = this.requestQueue[data.replyId!];
if (req) {
req.onReply(data);
@@ -106,12 +106,12 @@ export class IframeClient {
console.log(`send message[isSub:${this.isInFrame()}]:`, reqMessageData);
if (!this.iframe) {
if (!window.parent) {
reject('当前页面不在 iframe 中');
reject("当前页面不在 iframe 中");
}
window.parent.postMessage(reqMessageData, '*');
window.parent.postMessage(reqMessageData, "*");
} else {
//子页面
this.iframe.contentWindow?.postMessage(reqMessageData, '*');
this.iframe.contentWindow?.postMessage(reqMessageData, "*");
}
} catch (e) {
console.error(e);
+5 -1
View File
@@ -18,5 +18,9 @@
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-unused-vars": "off"
}
},
"ignorePatterns": [
"node_modules/**",
"dist/**"
]
}
+20
View File
@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.43.0](https://github.com/certd/certd/compare/v1.42.6...v1.43.0) (2026-08-14)
**Note:** Version bump only for package @certd/jdcloud
## [1.42.6](https://github.com/certd/certd/compare/v1.42.5...v1.42.6) (2026-07-18)
**Note:** Version bump only for package @certd/jdcloud
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
**Note:** Version bump only for package @certd/jdcloud
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
**Note:** Version bump only for package @certd/jdcloud
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
**Note:** Version bump only for package @certd/jdcloud
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
**Note:** Version bump only for package @certd/jdcloud
+4 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/jdcloud",
"version": "1.42.2",
"version": "1.43.0",
"description": "jdcloud openApi sdk",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
@@ -13,7 +13,7 @@
"pub": "npm publish",
"compile": "npm run build",
"format": "prettier --write src",
"lint": "eslint --fix"
"lint": "eslint --fix --ext .ts src"
},
"author": "",
"license": "Apache",
@@ -35,6 +35,7 @@
"chai": "^5.1.0",
"config": "^1.30.0",
"cross-env": "^7.0.3",
"eslint": "^8.57.0",
"esmock": "^2.7.5",
"js-yaml": "^3.11.0",
"mocha": "^10.6.0",
@@ -62,5 +63,5 @@
"fetch"
]
},
"gitHead": "7808d028e9c7e1b1e3ebc41f12b023d7b553300b"
"gitHead": "246ee83015bf5589adc2a5fa3d1388c8d9a2a252"
}
+6 -6
View File
@@ -1,11 +1,11 @@
import jdCloud from "./lib/core.js";
import jdService from './lib/service.js'
import jdCloud from './lib/core.js';
import jdService from './lib/service.js';
import domainService from './repo/domainservice/v2/domainservice.js'
import cdnService from './repo/cdn/v1/cdn.js'
import sslService from './repo/ssl/v1/ssl.js'
import domainService from './repo/domainservice/v2/domainservice.js';
import cdnService from './repo/cdn/v1/cdn.js';
import sslService from './repo/ssl/v1/ssl.js';
export const JDCloud = jdCloud;
export const JDService = jdService;
export const JDDomainService = domainService;
export const JDCdnService = cdnService;
export const JDSslService = sslService;
export const JDSslService = sslService;
+5 -1
View File
@@ -19,5 +19,9 @@
"@typescript-eslint/no-empty-function": "off",
// "no-unused-expressions": "off",
"max-len": [0, 160, 2, { "ignoreUrls": true }]
}
},
"ignorePatterns": [
"node_modules/**",
"dist/**"
]
}
+20
View File
@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.43.0](https://github.com/certd/certd/compare/v1.42.6...v1.43.0) (2026-08-14)
**Note:** Version bump only for package @certd/lib-k8s
## [1.42.6](https://github.com/certd/certd/compare/v1.42.5...v1.42.6) (2026-07-18)
**Note:** Version bump only for package @certd/lib-k8s
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
**Note:** Version bump only for package @certd/lib-k8s
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
**Note:** Version bump only for package @certd/lib-k8s
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
**Note:** Version bump only for package @certd/lib-k8s
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
**Note:** Version bump only for package @certd/lib-k8s
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-k8s",
"private": false,
"version": "1.42.2",
"version": "1.43.0",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -18,10 +18,10 @@
"pub": "npm publish",
"compile": "tsc --skipLibCheck --watch",
"format": "prettier --write src",
"lint": "eslint --fix"
"lint": "eslint --fix --ext .ts src"
},
"dependencies": {
"@certd/basic": "^1.42.2",
"@certd/basic": "^1.43.0",
"@kubernetes/client-node": "0.21.0"
},
"devDependencies": {
@@ -38,5 +38,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "7808d028e9c7e1b1e3ebc41f12b023d7b553300b"
"gitHead": "246ee83015bf5589adc2a5fa3d1388c8d9a2a252"
}
@@ -10,6 +10,7 @@ export type K8sClientOpts = {
//暂时没用
lookup?: any;
skipTLSVerify?: boolean;
debug?: boolean;
};
export class K8sClient {
kubeconfig!: KubeConfig;
@@ -18,11 +19,13 @@ export class K8sClient {
client!: CoreV1Api;
logger: ILogger;
skipTLSVerify?: boolean;
debug?: boolean;
constructor(opts: K8sClientOpts) {
this.kubeConfigStr = opts.kubeConfigStr;
this.logger = opts.logger;
this.setLookup(opts.lookup);
this.skipTLSVerify = opts.skipTLSVerify;
this.debug = opts.debug;
this.init();
}
@@ -86,6 +89,9 @@ export class K8sClient {
yml.metadata = {};
}
yml.metadata.resourceVersion = existing.body.metadata.resourceVersion;
if (this.debug) {
this.logger.info("patch yaml body:", JSON.stringify(yml));
}
const res = await client.patch(yml);
return res?.body;
}
@@ -126,6 +132,9 @@ export class K8sClient {
async createSecret(opts: { namespace: string; body: V1Secret }) {
const namespace = opts.namespace || "default";
this.logger.info("create secret:", opts.body.metadata);
if (this.debug) {
this.logger.info("create secret body:", JSON.stringify(opts.body));
}
const created = await this.client.createNamespacedSecret(namespace, opts.body);
this.logger.info("new secrets:", opts.body.metadata);
return created.body;
@@ -162,6 +171,9 @@ export class K8sClient {
},
opts.body
);
if (this.debug) {
this.logger.info("create secret:", JSON.stringify(body));
}
const res = await this.createSecret({ namespace, body });
this.logger.info(`secret ${secretName} 已创建`);
return res;
@@ -173,6 +185,9 @@ export class K8sClient {
}
const newSecret = merge(oldSecret.body, opts.body);
if (this.debug) {
this.logger.info("patch secret:", JSON.stringify(newSecret));
}
const res = await this.client.replaceNamespacedSecret(secretName, namespace, newSecret);
this.logger.info(`secret ${secretName} 已更新`);
return res.body;
@@ -207,6 +222,9 @@ export class K8sClient {
const client = this.kubeconfig.makeApiClient(NetworkingV1Api);
const oldIngress = await client.readNamespacedIngress(ingressName, namespace);
const newIngress = merge(oldIngress.body, opts.body);
if (this.debug) {
this.logger.info("patch ingress:", JSON.stringify(newIngress));
}
const res = await client.replaceNamespacedIngress(ingressName, namespace, newIngress);
this.logger.info("ingress patched", opts.body);
@@ -222,6 +240,7 @@ export class K8sClient {
},
};
for (const ingress of ingressNames) {
this.logger.info(`ingress 开始重启:${ingress}`);
await this.patchIngress({ namespace, ingressName: ingress, body });
this.logger.info(`ingress已重启:${ingress}`);
}
+5 -1
View File
@@ -18,5 +18,9 @@
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-unused-vars": "off"
}
},
"ignorePatterns": [
"node_modules/**",
"dist/**"
]
}
+32
View File
@@ -3,6 +3,38 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.43.0](https://github.com/certd/certd/compare/v1.42.6...v1.43.0) (2026-08-14)
### Features
* 优化开放接口证书自动申请功能 ([6339cdf](https://github.com/certd/certd/commit/6339cdffa9f79505dada5617d8c7fcfacc419a2e))
### Performance Improvements
* 统一通知类型常量管理,新增开放接口通知能力 ([edb249c](https://github.com/certd/certd/commit/edb249c2ad3c173dbfd04d8d156f895a6706ffd1))
## [1.42.6](https://github.com/certd/certd/compare/v1.42.5...v1.42.6) (2026-07-18)
### Bug Fixes
* 修复正常批量删除流水线报权限不足的bug ([5b50083](https://github.com/certd/certd/commit/5b500830a122c6c42dab054e57fed509050f94da))
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
### Bug Fixes
* 修复上传到cos报runtimeDepsService未初始化的问题 ([167b303](https://github.com/certd/certd/commit/167b303faeca02cc11cf97e4be2a3df914852167))
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
### Bug Fixes
* **pipeline:** 重构运行时依赖加载逻辑,修复火山引擎DNS解析报runtimeDepsService未初始化的bug ([ec69b8f](https://github.com/certd/certd/commit/ec69b8f11bfd4b20991aef74a72a47182ca79a9d))
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
**Note:** Version bump only for package @certd/lib-server
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
**Note:** Version bump only for package @certd/lib-server
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/lib-server",
"version": "1.42.2",
"version": "1.43.0",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -19,7 +19,7 @@
"pub": "npm publish",
"compile": "tsc --skipLibCheck --watch",
"format": "prettier --write src",
"lint": "eslint --fix"
"lint": "eslint --fix --ext .ts src"
},
"keywords": [],
"author": "greper",
@@ -29,11 +29,11 @@
],
"license": "AGPL",
"dependencies": {
"@certd/acme-client": "^1.42.2",
"@certd/basic": "^1.42.2",
"@certd/pipeline": "^1.42.2",
"@certd/plugin-lib": "^1.42.2",
"@certd/plus-core": "^1.42.2",
"@certd/acme-client": "^1.43.0",
"@certd/basic": "^1.43.0",
"@certd/pipeline": "^1.43.0",
"@certd/plugin-lib": "^1.43.0",
"@certd/plus-core": "^1.43.0",
"@midwayjs/cache": "3.14.0",
"@midwayjs/core": "3.20.11",
"@midwayjs/i18n": "3.20.13",
@@ -69,5 +69,5 @@
"typeorm": "^0.3.20",
"typescript": "^5.4.2"
},
"gitHead": "7808d028e9c7e1b1e3ebc41f12b023d7b553300b"
"gitHead": "246ee83015bf5589adc2a5fa3d1388c8d9a2a252"
}
@@ -253,7 +253,6 @@ export abstract class BaseService<T> {
if (!Array.isArray(ids)) {
ids = [ids];
}
ids = this.filterIds(ids);
const res = await this.getRepository().find({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
@@ -266,7 +265,7 @@ export abstract class BaseService<T> {
},
});
if (!res || res.length === ids.length) {
return;
return ids;
}
throw new PermissionException("权限不足");
}
@@ -279,7 +278,13 @@ export abstract class BaseService<T> {
return item != null && item != "";
});
}
async batchDelete(ids: number[], userId: number, projectId?: number) {
async batchDelete(ids: number[], userId: number, projectId?: number): Promise<number> {
if (!ids || ids.length === 0) {
throw new ValidateException("ids不能为空");
}
if (!Array.isArray(ids)) {
ids = [ids];
}
ids = this.filterIds(ids);
if (userId != null) {
const userProjectQuery = this.buildUserProjectQuery(userId, projectId);
@@ -295,6 +300,7 @@ export abstract class BaseService<T> {
}
await this.delete(ids);
return ids.length;
}
async findOne(options: FindOneOptions<T>) {
+39 -35
View File
@@ -1,21 +1,21 @@
export const Constants = {
dataDir: './data',
dataDir: "./data",
role: {
defaultUser: 3,
},
per: {
//无需登录
guest: '_guest_',
guest: "_guest_",
//无需登录
anonymous: '_guest_',
anonymous: "_guest_",
//无需登录,有 token 时解析当前用户
guestOptionalAuth: '_guestOptionalAuth_',
guestOptionalAuth: "_guestOptionalAuth_",
//仅需要登录
authOnly: '_authOnly_',
authOnly: "_authOnly_",
//仅需要登录
loginOnly: '_authOnly_',
loginOnly: "_authOnly_",
open: '_open_',
open: "_open_",
},
res: {
serverError(message: string) {
@@ -26,102 +26,106 @@ export const Constants = {
},
error: {
code: 1,
message: 'Internal server error',
message: "Internal server error",
},
success: {
code: 0,
message: 'success',
message: "success",
},
validation: {
code: 10,
message: '参数错误',
message: "参数错误",
},
needvip: {
code: 88,
message: '需要VIP',
message: "需要VIP",
},
needsuite: {
code: 89,
message: '需要购买或升级套餐',
message: "需要购买或升级套餐",
},
loginError: {
code: 2,
message: '登录失败',
message: "登录失败",
},
codeError: {
code: 3,
message: '验证码错误',
message: "验证码错误",
},
auth: {
code: 401,
message: '您还未登录或token已过期',
message: "您还未登录或token已过期",
},
permission: {
code: 402,
message: '您没有权限',
message: "您没有权限",
},
param: {
code: 400,
message: '参数错误',
message: "参数错误",
},
notFound: {
code: 404,
message: '页面/文件/资源不存在',
message: "页面/文件/资源不存在",
},
preview: {
code: 10001,
message: '对不起,预览环境不允许修改此数据',
message: "对不起,预览环境不允许修改此数据",
},
siteOff:{
siteOff: {
code: 10010,
message: '站点已关闭',
message: "站点已关闭",
},
need2fa:{
need2fa: {
code: 10020,
message: '需要2FA认证',
message: "需要2FA认证",
},
openKeyError: {
code: 20000,
message: 'ApiToken错误',
message: "ApiToken错误",
},
openKeySignError: {
code: 20001,
message: 'ApiToken签名错误',
message: "ApiToken签名错误",
},
openKeyExpiresError: {
code: 20002,
message: 'ApiToken时间戳错误',
message: "ApiToken时间戳错误",
},
openKeySignTypeError: {
code: 20003,
message: 'ApiToken签名类型不支持',
message: "ApiToken签名类型不支持",
},
openParamError: {
code: 20010,
message: '请求参数错误',
message: "请求参数错误",
},
openCertNotFound: {
code: 20011,
message: '证书不存在',
message: "证书不存在",
},
openCertNotReady: {
code: 20012,
message: '证书还未生成',
message: "证书还未生成",
},
openCertApplying: {
code: 20013,
message: '证书正在申请中,请稍后重新获取',
message: "证书正在申请中,请稍后重新获取",
},
openDomainNoVerifier:{
openDomainNoVerifier: {
code: 20014,
message: '域名校验方式未配置',
message: "域名校验方式未配置",
},
openPipelineError: {
code: 20015,
message: "流水线执行异常,请稍后重试",
},
openEmailNotFound: {
code: 20021,
message: '用户邮箱还未配置',
message: "用户邮箱还未配置",
},
},
systemUserId: 0, // 系统级别userid固定为0
enterpriseUserId: -1 // 企业模式用户id固定为-1
enterpriseUserId: -1, // 企业模式用户id固定为-1
};
@@ -1,11 +1,11 @@
import { ALL, Body, Post, Query } from '@midwayjs/core';
import { BaseController } from './base-controller.js';
import { ALL, Body, Post, Query } from "@midwayjs/core";
import { BaseController } from "./base-controller.js";
export abstract class CrudController<T> extends BaseController {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
abstract getService<T>();
@Post('/page')
@Post("/page")
async page(@Body(ALL) body: any) {
const pageRet = await this.getService().page({
query: body.query ?? {},
@@ -16,7 +16,7 @@ export abstract class CrudController<T> extends BaseController {
return this.ok(pageRet);
}
@Post('/list')
@Post("/list")
async list(@Body(ALL) body: any) {
const listRet = await this.getService().list({
query: body.query ?? {},
@@ -25,33 +25,33 @@ export abstract class CrudController<T> extends BaseController {
return this.ok(listRet);
}
@Post('/add')
@Post("/add")
async add(@Body(ALL) bean: any) {
delete bean.id;
const id = await this.getService().add(bean);
return this.ok(id);
}
@Post('/info')
async info(@Query('id') id: number) {
@Post("/info")
async info(@Query("id") id: number) {
const bean = await this.getService().info(id);
return this.ok(bean);
}
@Post('/update')
@Post("/update")
async update(@Body(ALL) bean: any) {
await this.getService().update(bean);
return this.ok(null);
}
@Post('/delete')
async delete(@Query('id') id: number) {
@Post("/delete")
async delete(@Query("id") id: number) {
await this.getService().delete([id]);
return this.ok(null);
}
@Post('/deleteByIds')
async deleteByIds(@Body('ids') ids: number[]) {
@Post("/deleteByIds")
async deleteByIds(@Body("ids") ids: number[]) {
await this.getService().delete(ids);
return this.ok(null);
}
@@ -1,19 +1,17 @@
import { Constants } from '../constants.js';
import { BaseException } from './base-exception.js';
import { Constants } from "../constants.js";
import { BaseException } from "./base-exception.js";
import { TextException } from "./common-exception.js";
/**
* 授权异常
*/
export class AuthException extends BaseException {
constructor(message?:string) {
super('AuthException', Constants.res.auth.code, message ? message : Constants.res.auth.message);
constructor(message?: string) {
super("AuthException", Constants.res.auth.code, message ? message : Constants.res.auth.message);
}
}
export class Need2FAException extends TextException {
constructor(message:string,data:any) {
super('Need2FAException', Constants.res.need2fa.code, message ? message : Constants.res.need2fa.message,data);
constructor(message: string, data: any) {
super("Need2FAException", Constants.res.need2fa.code, message ? message : Constants.res.need2fa.message, data);
}
}
@@ -3,8 +3,8 @@
*/
export class BaseException extends Error {
code: number;
data?:any
constructor(name: string, code: number, message: string ,data?:any) {
data?: any;
constructor(name: string, code: number, message: string, data?: any) {
super(message);
this.name = name;
this.code = code;
@@ -1,10 +1,10 @@
import { Constants } from '../constants.js';
import { BaseException } from './base-exception.js';
import { Constants } from "../constants.js";
import { BaseException } from "./base-exception.js";
/**
* 验证码异常
*/
export class CodeErrorException extends BaseException {
constructor(message) {
super('CodeErrorException', Constants.res.codeError.code, message ? message : Constants.res.codeError.message);
super("CodeErrorException", Constants.res.codeError.code, message ? message : Constants.res.codeError.message);
}
}
@@ -21,3 +21,10 @@ export class TextException extends BaseException {
super(name, code, message, data);
}
}
export class TextedException extends TextException {
constructor(res: { code: number; message: string; data?: any; name?: string }) {
const { code, message, data, name } = res;
super(name || "TextedException", code, message, data);
}
}
@@ -1,12 +1,13 @@
export * from './auth-exception.js';
export * from './base-exception.js';
export * from './permission-exception.js';
export * from './preview-exception.js';
export * from './validation-exception.js';
export * from './vip-exception.js';
export * from './common-exception.js';
export * from './not-found-exception.js';
export * from './param-exception.js';
export * from './site-off-exception.js';
export * from './login-error-exception.js'
export * from './code-error-exception.js'
export * from "./auth-exception.js";
export * from "./base-exception.js";
export * from "./permission-exception.js";
export * from "./preview-exception.js";
export * from "./validation-exception.js";
export * from "./vip-exception.js";
export * from "./common-exception.js";
export * from "./not-found-exception.js";
export * from "./param-exception.js";
export * from "./site-off-exception.js";
export * from "./login-error-exception.js";
export * from "./code-error-exception.js";
export * from "./non-retryable-exception.js";
@@ -1,12 +1,12 @@
import { Constants } from '../constants.js';
import { BaseException } from './base-exception.js';
import { Constants } from "../constants.js";
import { BaseException } from "./base-exception.js";
/**
* 通用异常
*/
export class LoginErrorException extends BaseException {
leftCount: number;
constructor(message, leftCount: number) {
super('LoginErrorException', Constants.res.loginError.code, message ? message : Constants.res.loginError.message);
super("LoginErrorException", Constants.res.loginError.code, message ? message : Constants.res.loginError.message);
this.leftCount = leftCount;
}
}
@@ -0,0 +1,12 @@
import assert from "assert";
import { NonRetryableException } from "./non-retryable-exception.js";
describe("NonRetryableException", () => {
it("sets the standard error name and message", () => {
const error = new NonRetryableException("cannot retry");
assert.equal(error.name, "NonRetryableException");
assert.equal(error.message, "cannot retry");
assert.equal(error instanceof Error, true);
});
});
@@ -0,0 +1,6 @@
export class NonRetryableException extends Error {
constructor(message: string) {
super(message);
this.name = "NonRetryableException";
}
}
@@ -1,10 +1,10 @@
import { Constants } from '../constants.js';
import { BaseException } from './base-exception.js';
import { Constants } from "../constants.js";
import { BaseException } from "./base-exception.js";
/**
* 资源不存在
*/
export class NotFoundException extends BaseException {
constructor(message) {
super('NotFoundException', Constants.res.notFound.code, message ? message : Constants.res.notFound.message);
super("NotFoundException", Constants.res.notFound.code, message ? message : Constants.res.notFound.message);
}
}
@@ -1,10 +1,10 @@
import { Constants } from '../constants.js';
import { BaseException } from './base-exception.js';
import { Constants } from "../constants.js";
import { BaseException } from "./base-exception.js";
/**
* 参数异常
*/
export class ParamException extends BaseException {
constructor(message) {
super('ParamException', Constants.res.param.code, message ? message : Constants.res.param.message);
super("ParamException", Constants.res.param.code, message ? message : Constants.res.param.message);
}
}
@@ -1,10 +1,10 @@
import { Constants } from '../constants.js';
import { BaseException } from './base-exception.js';
import { Constants } from "../constants.js";
import { BaseException } from "./base-exception.js";
/**
* 授权异常
*/
export class PermissionException extends BaseException {
constructor(message?: string) {
super('PermissionException', Constants.res.permission.code, message ? message : Constants.res.permission.message);
super("PermissionException", Constants.res.permission.code, message ? message : Constants.res.permission.message);
}
}
@@ -1,14 +1,10 @@
import { Constants } from '../constants.js';
import { BaseException } from './base-exception.js';
import { Constants } from "../constants.js";
import { BaseException } from "./base-exception.js";
/**
* 预览模式
*/
export class PreviewException extends BaseException {
constructor(message) {
super(
'PreviewException',
Constants.res.preview.code,
message ? message : Constants.res.preview.message
);
super("PreviewException", Constants.res.preview.code, message ? message : Constants.res.preview.message);
}
}
@@ -1,9 +1,9 @@
import { Constants } from '../constants.js';
import { BaseException } from './base-exception.js';
import { Constants } from "../constants.js";
import { BaseException } from "./base-exception.js";
/**
*/
export class SiteOffException extends BaseException {
constructor(message) {
super('SiteOffException', Constants.res.siteOff.code, message ? message : Constants.res.siteOff.message);
super("SiteOffException", Constants.res.siteOff.code, message ? message : Constants.res.siteOff.message);
}
}
@@ -1,10 +1,10 @@
import { Constants } from '../constants.js';
import { BaseException } from './base-exception.js';
import { Constants } from "../constants.js";
import { BaseException } from "./base-exception.js";
/**
* 校验异常
*/
export class ValidateException extends BaseException {
constructor(message) {
super('ValidateException', Constants.res.validation.code, message ? message : Constants.res.validation.message);
super("ValidateException", Constants.res.validation.code, message ? message : Constants.res.validation.message);
}
}
@@ -1,16 +1,16 @@
import { Constants } from '../constants.js';
import { BaseException } from './base-exception.js';
import { Constants } from "../constants.js";
import { BaseException } from "./base-exception.js";
/**
* 需要vip异常
*/
export class NeedVIPException extends BaseException {
constructor(message) {
super('NeedVIPException', Constants.res.needvip.code, message ? message : Constants.res.needvip.message);
super("NeedVIPException", Constants.res.needvip.code, message ? message : Constants.res.needvip.message);
}
}
export class NeedSuiteException extends BaseException {
constructor(message) {
super('NeedSuiteException', Constants.res.needsuite.code, message ? message : Constants.res.needsuite.message);
super("NeedSuiteException", Constants.res.needsuite.code, message ? message : Constants.res.needsuite.message);
}
}
+9 -8
View File
@@ -1,8 +1,9 @@
export * from './base-controller.js';
export * from './constants.js';
export * from './crud-controller.js';
export * from './enum-item.js';
export * from './exception/index.js';
export * from './result.js';
export * from './base-service.js';
export * from "./mode.js"
export * from "./base-controller.js";
export * from "./constants.js";
export * from "./crud-controller.js";
export * from "./enum-item.js";
export * from "./exception/index.js";
export * from "./result.js";
export * from "./base-service.js";
export * from "./mode.js";
export * from "./notification-constants.js";
+18 -8
View File
@@ -1,12 +1,22 @@
let adminMode = "saas"
import { logger } from "@certd/basic";
import { isPlus } from "@certd/plus-core";
export function setAdminMode(mode:string = "saas"){
adminMode = mode
let adminMode = "saas";
export function setAdminMode(mode: string = "saas") {
adminMode = mode;
}
export function getAdminMode(){
return adminMode
export function getAdminMode() {
return adminMode;
}
export function isEnterprise(){
return adminMode === "enterprise"
}
export function isEnterprise() {
const isEnterprise = adminMode === "enterprise";
if (!isPlus()) {
if (isEnterprise) {
logger.warn("不是VIP,无法使用企业项目管理功能,退回普通模式");
}
return false;
}
return isEnterprise;
}
@@ -0,0 +1,18 @@
/// <reference types="mocha" />
/// <reference types="node" />
import assert from "node:assert/strict";
import { isNotificationType, NotificationTypes } from "./notification-constants.js";
describe("NotificationTypes", () => {
it("includes pipeline and server business notification types", () => {
assert.equal(NotificationTypes.PipelineResult, "pipelineResult");
assert.equal(NotificationTypes.CertDeployError, "certDeployError");
assert.equal(NotificationTypes.ForgotPassword, "forgotPassword");
});
it("recognizes registered notification types only", () => {
assert.equal(isNotificationType(NotificationTypes.CertDeployError), true);
assert.equal(isNotificationType("unknown"), false);
});
});
@@ -0,0 +1,24 @@
import { PipelineNotificationTypes } from "@certd/pipeline";
/** 服务端业务通知类型,包含流水线核心通知类型。 */
export const NotificationTypes = {
...PipelineNotificationTypes,
Common: "common",
CertApplySuccess: "certApplySuccess",
CertPullError: "certPullError",
CertDeployError: "certDeployError",
SiteCheckError: "siteCheckError",
SiteCertExpireRemind: "siteCertExpireRemind",
DomainExpirationCheck: "domainExpirationCheck",
VipExpireRemind: "vipExpireRemind",
UserExpireRemind: "userExpireRemind",
RegisterCode: "registerCode",
ForgotPassword: "forgotPassword",
GithubReleaseCheck: "githubReleaseCheck",
} as const;
export type NotificationTypeValue = (typeof NotificationTypes)[keyof typeof NotificationTypes];
export function isNotificationType(value: string): value is NotificationTypeValue {
return Object.values(NotificationTypes).includes(value as NotificationTypeValue);
}
@@ -1,11 +1,11 @@
import type { IMidwayContainer } from '@midwayjs/core';
import { Configuration } from '@midwayjs/core';
import { logger } from '@certd/basic';
import type { IMidwayContainer } from "@midwayjs/core";
import { Configuration } from "@midwayjs/core";
import { logger } from "@certd/basic";
@Configuration({
namespace: 'lib-server',
namespace: "lib-server",
})
export class LibServerConfiguration {
async onReady(container: IMidwayContainer) {
logger.info('lib start...');
logger.info("lib start...");
}
}
+7 -7
View File
@@ -1,9 +1,9 @@
import { SysSettingsEntity } from './system/index.js';
import { AccessEntity } from './user/access/entity/access.js';
import { SysSettingsEntity } from "./system/index.js";
import { AccessEntity } from "./user/access/entity/access.js";
import { AddonEntity } from "./user/index.js";
export * from './basic/index.js';
export * from './system/index.js';
export * from './user/index.js';
export { LibServerConfiguration as Configuration } from './configuration.js';
export * from "./basic/index.js";
export * from "./system/index.js";
export * from "./user/index.js";
export { LibServerConfiguration as Configuration } from "./configuration.js";
export const libServerEntities = [SysSettingsEntity, AccessEntity,AddonEntity];
export const libServerEntities = [SysSettingsEntity, AccessEntity, AddonEntity];
@@ -1,5 +1,5 @@
export * from './service/plus-service.js';
export * from './service/file-service.js';
export * from './service/encryptor.js';
export * from './service/ocr-service.js';
export * from './service/executor-queue.js';
export * from "./service/plus-service.js";
export * from "./service/file-service.js";
export * from "./service/encryptor.js";
export * from "./service/ocr-service.js";
export * from "./service/executor-queue.js";
@@ -1,8 +1,8 @@
import crypto from 'crypto';
import crypto from "crypto";
export class Encryptor {
secretKey: Buffer;
constructor(encryptSecret: string, encoding: BufferEncoding = 'base64') {
constructor(encryptSecret: string, encoding: BufferEncoding = "base64") {
this.secretKey = Buffer.from(encryptSecret, encoding);
}
// 加密函数
@@ -10,18 +10,18 @@ export class Encryptor {
const iv = crypto.randomBytes(16); // 初始化向量
// const secretKey = crypto.randomBytes(32);
// const key = Buffer.from(secretKey);
const cipher = crypto.createCipheriv('aes-256-cbc', this.secretKey, iv);
const cipher = crypto.createCipheriv("aes-256-cbc", this.secretKey, iv);
let encrypted = cipher.update(text);
encrypted = Buffer.concat([encrypted, cipher.final()]);
return iv.toString('hex') + ':' + encrypted.toString('hex');
return iv.toString("hex") + ":" + encrypted.toString("hex");
}
// 解密函数
decrypt(encryptedText: string) {
const textParts = encryptedText.split(':');
const iv = Buffer.from(textParts.shift(), 'hex');
const encrypted = Buffer.from(textParts.join(':'), 'hex');
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(this.secretKey), iv);
const textParts = encryptedText.split(":");
const iv = Buffer.from(textParts.shift(), "hex");
const encrypted = Buffer.from(textParts.join(":"), "hex");
const decipher = crypto.createDecipheriv("aes-256-cbc", Buffer.from(this.secretKey), iv);
let decrypted = decipher.update(encrypted);
decrypted = Buffer.concat([decrypted, decipher.final()]);
return decrypted.toString();
@@ -1,18 +1,18 @@
import { logger } from "@certd/basic";
export type TaskItem = {
task: ()=>Promise<void>;
}
task: () => Promise<void>;
};
export class UserTaskQueue{
export class UserTaskQueue {
userId: number;
pendingQueue: TaskItem[] = [];
runningQueue: TaskItem[] = [];
getMaxRunningCount: ()=>number ;
getMaxRunningCount: () => number;
constructor(req: { userId: number ,getMaxRunningCount: ()=>number }) {
constructor(req: { userId: number; getMaxRunningCount: () => number }) {
this.userId = req.userId;
this.getMaxRunningCount = req.getMaxRunningCount ;
this.getMaxRunningCount = req.getMaxRunningCount;
}
addTask(task: TaskItem) {
@@ -34,10 +34,10 @@ export class UserTaskQueue{
}
// 执行任务
this.runningQueue.push(task);
const call = async ()=>{
try{
const call = async () => {
try {
await task.task();
}finally{
} finally {
// 任务执行完成,从运行队列中移除
const index = this.runningQueue.indexOf(task);
if (index > -1) {
@@ -46,17 +46,16 @@ export class UserTaskQueue{
// 继续执行下一个任务
this.runTask();
}
}
};
logger.info(`[user_${this.userId}]执行任务,当前运行队列:${this.runningQueue.length}, 等待队列:${this.pendingQueue.length}`);
call()
call();
}
}
export class ExecutorQueue{
export class ExecutorQueue {
queues: Record<number, UserTaskQueue> = {};
maxRunningCount: number = 10;
setMaxRunningCount(count: number) {
this.maxRunningCount = count;
}
@@ -64,7 +63,7 @@ export class ExecutorQueue{
getUserQueue(userId: number) {
const userQueue = this.queues[userId];
if (!userQueue) {
this.queues[userId] = new UserTaskQueue({ userId, getMaxRunningCount: ()=>this.maxRunningCount });
this.queues[userId] = new UserTaskQueue({ userId, getMaxRunningCount: () => this.maxRunningCount });
}
return this.queues[userId];
}
@@ -73,7 +72,6 @@ export class ExecutorQueue{
const userQueue = this.getUserQueue(userId);
userQueue.addTask(task);
}
}
export const executorQueue = new ExecutorQueue();
export const executorQueue = new ExecutorQueue();
@@ -1,42 +1,42 @@
import { Provide, Scope, ScopeEnum } from '@midwayjs/core';
import dayjs from 'dayjs';
import path from 'path';
import fs from 'fs';
import { cache, logger, utils } from '@certd/basic';
import { NotFoundException, ParamException, PermissionException } from '../../../basic/index.js';
import { Provide, Scope, ScopeEnum } from "@midwayjs/core";
import dayjs from "dayjs";
import path from "path";
import fs from "fs";
import { cache, logger, utils } from "@certd/basic";
import { NotFoundException, ParamException, PermissionException } from "../../../basic/index.js";
export type UploadFileItem = {
filename: string;
tmpFilePath: string;
};
const uploadRootDir = './data/upload';
export const uploadTmpFileCacheKey = 'tmpfile_key_';
const uploadRootDir = "./data/upload";
export const uploadTmpFileCacheKey = "tmpfile_key_";
/**
*/
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class FileService {
async saveFile(userId: number, tmpCacheKey: any, permission: 'public' | 'private') {
async saveFile(userId: number, tmpCacheKey: any, permission: "public" | "private") {
if (tmpCacheKey.startsWith(`/${permission}`)) {
//已经保存过,不需要再次保存
return tmpCacheKey;
}
let fileName = '';
let fileName = "";
let tmpFilePath = tmpCacheKey;
if (uploadTmpFileCacheKey && tmpCacheKey.startsWith(uploadTmpFileCacheKey)) {
const tmpFile: UploadFileItem = cache.get(tmpCacheKey);
if (!tmpFile) {
throw new ParamException('文件已过期,请重新上传');
throw new ParamException("文件已过期,请重新上传");
}
tmpFilePath = tmpFile.tmpFilePath;
fileName = tmpFile.filename || path.basename(tmpFilePath);
}
if (!tmpFilePath || !fs.existsSync(tmpFilePath)) {
throw new Error('文件不存在,请重新上传');
throw new Error("文件不存在,请重新上传");
}
const date = dayjs().format('YYYY_MM_DD');
const date = dayjs().format("YYYY_MM_DD");
const random = Math.random().toString(36).substring(7);
const userIdMd5 = Buffer.from(Buffer.from(userId + '').toString('base64')).toString('hex');
const userIdMd5 = Buffer.from(Buffer.from(userId + "").toString("base64")).toString("hex");
const key = `/${permission}/${userIdMd5}/${date}/${random}_${fileName}`;
let savePath = path.join(uploadRootDir, key);
savePath = path.resolve(savePath);
@@ -44,7 +44,6 @@ export class FileService {
if (!fs.existsSync(parentDir)) {
fs.mkdirSync(parentDir, { recursive: true });
}
// eslint-disable-next-line node/no-unsupported-features/node-builtins
const copyFile = utils.promises.promisify(fs.copyFile);
await copyFile(tmpFilePath, savePath);
try {
@@ -58,29 +57,29 @@ export class FileService {
getFile(key: string, userId?: number, allowAnyPrivateUser = false) {
if (!key) {
throw new ParamException('参数错误');
throw new ParamException("参数错误");
}
if (key.indexOf('..') >= 0) {
if (key.indexOf("..") >= 0) {
//安全性判断
throw new ParamException('参数错误');
throw new ParamException("参数错误");
}
if (!key.startsWith('/')) {
throw new ParamException('参数错误');
if (!key.startsWith("/")) {
throw new ParamException("参数错误");
}
const keyArr = key.split('/');
const keyArr = key.split("/");
const permission = keyArr[1];
const userIdMd5 = keyArr[2];
if (permission !== 'public' && !allowAnyPrivateUser) {
if (permission !== "public" && !allowAnyPrivateUser) {
//非公开文件需要验证用户
const userIdStr = Buffer.from(Buffer.from(userIdMd5, 'hex').toString('base64')).toString();
const userIdStr = Buffer.from(Buffer.from(userIdMd5, "hex").toString("base64")).toString();
const userIdInt: number = parseInt(userIdStr, 10);
if (userId == null || userIdInt !== userId) {
throw new PermissionException('无访问权限');
throw new PermissionException("无访问权限");
}
}
const filePath = path.join(uploadRootDir, key);
if (!fs.existsSync(filePath)) {
throw new NotFoundException('文件不存在');
throw new NotFoundException("文件不存在");
}
return filePath;
}
@@ -15,10 +15,9 @@ export class OcrService implements IOcrService {
url: "/activation/certd/ocr",
method: "post",
data: {
image: opts.image
}
image: opts.image,
},
});
return res;
}
}
+2 -2
View File
@@ -1,2 +1,2 @@
export * from './settings/index.js';
export * from './basic/index.js';
export * from "./settings/index.js";
export * from "./basic/index.js";
@@ -1,3 +1,3 @@
export * from './service/sys-settings-service.js';
export * from './service/models.js';
export * from './entity/sys-settings.js';
export * from "./service/sys-settings-service.js";
export * from "./service/models.js";
export * from "./entity/sys-settings.js";
@@ -1,15 +1,15 @@
import { Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { SysSettingsEntity } from '../entity/sys-settings.js';
import { BaseSettings, SysInstallInfo, SysPrivateSettings, SysPublicSettings, SysSecret, SysSecretBackup } from './models.js';
import { Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { InjectEntityModel } from "@midwayjs/typeorm";
import { Repository } from "typeorm";
import { SysSettingsEntity } from "../entity/sys-settings.js";
import { BaseSettings, SysInstallInfo, SysPrivateSettings, SysPublicSettings, SysSecret, SysSecretBackup } from "./models.js";
import { getAllSslProviderDomains, setSslProviderReverseProxies, setWalkFromAuthoritative } from '@certd/acme-client';
import { cache, logger, mergeUtils, setGlobalHeaders, setGlobalProxy } from '@certd/basic';
import { isPlus } from '@certd/plus-core';
import * as dns from 'node:dns';
import { BaseService, setAdminMode } from '../../../basic/index.js';
import { executorQueue } from '../../basic/service/executor-queue.js';
import { getAllSslProviderDomains, setSslProviderReverseProxies, setWalkFromAuthoritative } from "@certd/acme-client";
import { cache, logger, mergeUtils, setGlobalHeaders, setGlobalProxy } from "@certd/basic";
import { isPlus } from "@certd/plus-core";
import * as dns from "node:dns";
import { BaseService, setAdminMode } from "../../../basic/index.js";
import { executorQueue } from "../../basic/service/executor-queue.js";
const { merge } = mergeUtils;
let lastSaveEnvVars = {};
@@ -138,7 +138,7 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
res.reverseProxies[domain] = "";
}
}
return res
return res;
}
async savePrivateSettings(bean: SysPrivateSettings) {
@@ -149,14 +149,14 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
}
async reloadSettings() {
await this.reloadPrivateSettings()
await this.reloadPublicSettings()
await this.reloadPrivateSettings();
await this.reloadPublicSettings();
}
async reloadPublicSettings() {
const publicSetting = await this.getPublicSettings()
if (isPlus()){
setAdminMode(publicSetting.adminMode )
const publicSetting = await this.getPublicSettings();
if (isPlus()) {
setAdminMode(publicSetting.adminMode);
}
}
@@ -169,7 +169,7 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
};
setGlobalProxy(opts);
setGlobalHeaders(this.parseKeyValueText(privateSetting.commonHeaders));
if (privateSetting.dnsResultOrder) {
dns.setDefaultResultOrder(privateSetting.dnsResultOrder as any);
}
@@ -183,29 +183,28 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
this.setEnvironmentVars(privateSetting.environmentVars);
setWalkFromAuthoritative(privateSetting.acmeWalkFromAuthoritative);
}
parseKeyValueText(text: string) {
const values = {};
if (typeof text !== 'string') {
if (typeof text !== "string") {
text = "";
}
text.split('\n').forEach(line => {
text.split("\n").forEach(line => {
line = line.trim();
if (!line || line.startsWith('#')) {
return
if (!line || line.startsWith("#")) {
return;
}
const arr = line.split("#")
const arr = line.split("#");
if (arr.length > 0) {
line = arr[0].trim();
}
if (!line.includes("=")) {
return
return;
}
const eqIndex = line.indexOf('=');
const eqIndex = line.indexOf("=");
const key = line.substring(0, eqIndex).trim();
const value = line.substring(eqIndex + 1).trim();
if (key && value) {
@@ -220,7 +219,7 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
//先删除旧环境变量
if (lastSaveEnvVars) {
for (const key in lastSaveEnvVars) {
delete process.env[key];
delete process.env[key];
}
}
@@ -234,7 +233,7 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
entity.setting = JSON.stringify(setting);
await this.repository.save(entity);
} else {
throw new Error('该设置不存在');
throw new Error("该设置不存在");
}
cache.delete(`settings.${key}`);
}
@@ -246,20 +245,20 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
if (settings == null) {
const backup = new SysSecretBackup();
if (installInfo.siteId == null || privateSettings.encryptSecret == null) {
logger.error('备份密钥失败,siteId或encryptSecret为空');
logger.error("备份密钥失败,siteId或encryptSecret为空");
return;
}
backup.siteId = installInfo.siteId;
backup.encryptSecret = privateSettings.encryptSecret;
await this.saveSetting(backup);
logger.info('备份密钥成功');
logger.info("备份密钥成功");
} else {
//校验是否有变化
if (settings.siteId !== installInfo.siteId) {
throw new Error(`siteId与备份不一致,可能是数据异常,请检查:backup=${settings.siteId}, current=${installInfo.siteId}`);
}
if (settings.encryptSecret !== privateSettings.encryptSecret) {
throw new Error('encryptSecret与备份不一致,可能是数据异常,请检查');
throw new Error("encryptSecret与备份不一致,可能是数据异常,请检查");
}
}
}
@@ -271,12 +270,12 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
//从备份中读取
const settings = await this.getSettingByKey(SysSecretBackup.__key__);
if (settings == null || !settings.encryptSecret) {
throw new Error('密钥备份不存在');
throw new Error("密钥备份不存在");
}
sysSecret.siteId = settings.siteId;
sysSecret.encryptSecret = settings.encryptSecret;
await this.saveSetting(sysSecret);
logger.info('密钥恢复成功');
logger.info("密钥恢复成功");
return sysSecret;
}
}
@@ -1,46 +1,46 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
/**
* 授权配置
*/
@Entity('cd_access')
@Entity("cd_access")
export class AccessEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'key_id', comment: 'key_id', length: 100 })
@Column({ name: "key_id", comment: "key_id", length: 100 })
keyId: string;
@Column({ name: 'user_id', comment: '用户id' })
@Column({ name: "user_id", comment: "用户id" })
userId: number; // 0为系统级别, -1为企业,大于1为用户
@Column({ comment: '名称', length: 100 })
@Column({ comment: "名称", length: 100 })
name: string;
@Column({ comment: '类型', length: 100 })
@Column({ comment: "类型", length: 100 })
type: string;
@Column({ name: 'subtype', comment: '子类型', length: 100, nullable: true })
@Column({ name: "subtype", comment: "子类型", length: 100, nullable: true })
subtype: string;
@Column({ name: 'setting', comment: '设置', length: 10240, nullable: true })
@Column({ name: "setting", comment: "设置", length: 10240, nullable: true })
setting: string;
@Column({ name: 'encrypt_setting', comment: '已加密设置', length: 10240, nullable: true })
@Column({ name: "encrypt_setting", comment: "已加密设置", length: 10240, nullable: true })
encryptSetting: string;
@Column({ name: 'project_id', comment: '项目id' })
@Column({ name: "project_id", comment: "项目id" })
projectId: number;
@Column({
name: 'create_time',
comment: '创建时间',
default: () => 'CURRENT_TIMESTAMP',
name: "create_time",
comment: "创建时间",
default: () => "CURRENT_TIMESTAMP",
})
createTime: Date;
@Column({
name: 'update_time',
comment: '修改时间',
default: () => 'CURRENT_TIMESTAMP',
name: "update_time",
comment: "修改时间",
default: () => "CURRENT_TIMESTAMP",
})
updateTime: Date;
}
@@ -1,5 +1,5 @@
export * from './entity/access.js';
export * from './service/access-service.js';
export * from './service/access-sys-getter.js';
export * from './service/access-getter.js';
export * from './service/encrypt-service.js';
export * from "./entity/access.js";
export * from "./service/access-service.js";
export * from "./service/access-sys-getter.js";
export * from "./service/access-getter.js";
export * from "./service/encrypt-service.js";
@@ -1,29 +1,20 @@
import { IAccessService, IRuntimeDepsService } from "@certd/pipeline";
export type AccessRuntimeDepsService = IRuntimeDepsService;
import { IAccessService } from "@certd/pipeline";
export class AccessGetter implements IAccessService {
userId: number;
projectId?: number;
runtimeDepsService?: AccessRuntimeDepsService;
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
) {
getter: <T>(id: any, userId?: number, projectId?: number, ignorePermission?: boolean) => Promise<T>;
constructor(userId: number, projectId: number, getter: (id: any, userId: number, projectId?: number, ignorePermission?: boolean) => Promise<any>) {
this.userId = userId;
this.projectId = projectId;
this.getter = getter;
this.runtimeDepsService = runtimeDepsService;
}
async getById<T = any>(id: any) {
return await this.getter<T>(id, this.userId, this.projectId, false, this.runtimeDepsService);
return await this.getter<T>(id, this.userId, this.projectId, false);
}
async getCommonById<T = any>(id: any) {
return await this.getter<T>(id, 0, null, false, this.runtimeDepsService);
return await this.getter<T>(id, 0, null, false);
}
}
@@ -1,8 +1,8 @@
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { ApplicationContext, Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import type { IMidwayContainer } from "@midwayjs/core";
import { InjectEntityModel } from "@midwayjs/typeorm";
import { In, Repository } from "typeorm";
import { AccessGetter, BaseService, PageReq, PermissionException, ValidateException } from "../../../index.js";
import type { AccessRuntimeDepsService } from "./access-getter.js";
import { AccessEntity } from "../entity/access.js";
import { AccessDefine, accessRegistry, newAccess } from "@certd/pipeline";
import { EncryptService } from "./encrypt-service.js";
@@ -20,6 +20,9 @@ export class AccessService extends BaseService<AccessEntity> {
@Inject()
encryptService: EncryptService;
@ApplicationContext()
applicationContext: IMidwayContainer;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
getRepository() {
@@ -161,7 +164,7 @@ export class AccessService extends BaseService<AccessEntity> {
};
}
async getAccessById(id: any, checkUserId: boolean, userId?: number, projectId?: number, runtimeDepsService?: AccessRuntimeDepsService): Promise<any> {
async getAccessById(id: any, checkUserId: boolean, userId?: number, projectId?: number): Promise<any> {
const entity = await this.info(id);
if (entity == null) {
throw new Error(`该授权配置不存在,请确认是否已被删除:id=${id}`);
@@ -184,20 +187,23 @@ export class AccessService extends BaseService<AccessEntity> {
id: entity.id,
...setting,
};
const taskServiceBuilder: any = await this.applicationContext.getAsync("taskServiceBuilder");
const serviceGetter = taskServiceBuilder.create({ userId: userId || 0, projectId });
const getAccessById = this.getById.bind(this);
const accessGetter = new AccessGetter(userId, projectId, getAccessById, runtimeDepsService);
const accessGetter = new AccessGetter(userId, projectId, getAccessById);
const accessContext = {
logger,
http,
utils,
accessService: accessGetter,
serviceGetter,
} as any;
const access = await newAccess(entity.type, input, accessGetter, accessContext);
return access;
}
async getById(id: any, userId: number, projectId?: number, _ignorePermission?: boolean, runtimeDepsService?: AccessRuntimeDepsService): Promise<any> {
return await this.getAccessById(id, true, userId, projectId, runtimeDepsService);
async getById(id: any, userId: number, projectId?: number, _ignorePermission?: boolean): Promise<any> {
return await this.getAccessById(id, true, userId, projectId);
}
decryptAccessEntity(entity: AccessEntity): any {
@@ -283,4 +289,15 @@ export class AccessService extends BaseService<AccessEntity> {
await this.repository.save(newAccess);
return newAccess.id;
}
async getDefaultByType({ type, userId, projectId, subtype }) {
return await this.repository.findOne({
where: {
type,
userId,
projectId,
subtype,
},
});
}
}
@@ -1,5 +1,5 @@
import { IAccessService } from '@certd/pipeline';
import { AccessService } from './access-service.js';
import { IAccessService } from "@certd/pipeline";
import { AccessService } from "./access-service.js";
export class AccessSysGetter implements IAccessService {
accessService: AccessService;
@@ -1,5 +1,5 @@
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { Encryptor, SysSecret, SysSettingsService } from '../../../system/index.js';
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { Encryptor, SysSecret, SysSettingsService } from "../../../system/index.js";
/**
* 授权
@@ -1,15 +1,6 @@
import { HttpClient, ILogger, utils } from "@certd/basic";
import {upperFirst} from "lodash-es";
import {
accessRegistry,
FormItemProps,
IAccessService,
IRuntimeDepsService,
IServiceGetter,
PluginRequestHandleReq,
Registrable
} from "@certd/pipeline";
import { upperFirst } from "lodash-es";
import { accessRegistry, FormItemProps, IAccessService, IServiceGetter, PluginRequestHandleReq, Registrable, getRuntimeDepsService } from "@certd/pipeline";
export type AddonRequestHandleReqInput<T = any> = {
id?: number;
@@ -19,7 +10,7 @@ export type AddonRequestHandleReqInput<T = any> = {
export type AddonRequestHandleReq<T = any> = {
addonType: string;
} &PluginRequestHandleReq<AddonRequestHandleReqInput<T>>;
} & PluginRequestHandleReq<AddonRequestHandleReqInput<T>>;
export type AddonInputDefine = FormItemProps & {
title: string;
@@ -48,8 +39,6 @@ export type AddonInstanceConfig = {
};
};
export interface IAddon {
ctx: AddonContext;
[key: string]: any;
@@ -67,13 +56,9 @@ export abstract class BaseAddon implements IAddon {
ctx!: AddonContext;
http!: HttpClient;
logger!: ILogger;
runtimeDepsService?: IRuntimeDepsService;
async importRuntime(specifier: string) {
if (!this.runtimeDepsService) {
return await import(specifier);
}
return await this.runtimeDepsService.importRuntime(specifier, this.logger);
return await getRuntimeDepsService().importRuntime(specifier, this.logger);
}
title!: string;
@@ -85,7 +70,7 @@ export abstract class BaseAddon implements IAddon {
if (accessId == null) {
throw new Error("您还没有配置授权");
}
const accessService = await this.ctx.serviceGetter.get<IAccessService>("accessService")
const accessService = await this.ctx.serviceGetter.get<IAccessService>("accessService");
let res: any = null;
if (isCommon) {
res = await accessService.getCommonById(accessId);
@@ -118,18 +103,12 @@ export abstract class BaseAddon implements IAddon {
this.ctx = ctx;
this.http = ctx.http;
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;
};
async onRequest(req:AddonRequestHandleReq) {
async onRequest(req: AddonRequestHandleReq) {
if (!req.action) {
throw new Error("action is required");
}
@@ -147,10 +126,8 @@ export abstract class BaseAddon implements IAddon {
}
throw new Error(`action ${req.action} not found`);
}
}
export interface IAddonGetter {
getById<T = any>(id: any): Promise<T>;
getCommonById<T = any>(id: any): Promise<T>;
@@ -48,8 +48,8 @@ export function AddonInput(input?: AddonInputDefine): PropertyDecorator {
};
}
export async function newAddon(addonType:string,type: string, input: any, ctx: AddonContext) {
const key = `${addonType}:${type}`
export async function newAddon(addonType: string, type: string, input: any, ctx: AddonContext) {
const key = `${addonType}:${type}`;
const register = addonRegistry.get(key);
if (register == null) {
throw new Error(`${addonType} ${type} not found`);
@@ -1,49 +1,46 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
/**
*/
@Entity('cd_addon')
@Entity("cd_addon")
export class AddonEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'key_id', comment: 'key_id', length: 100 })
@Column({ name: "key_id", comment: "key_id", length: 100 })
keyId: string;
@Column({ name: 'user_id', comment: '用户id' })
@Column({ name: "user_id", comment: "用户id" })
userId: number;
@Column({ comment: '名称', length: 100 })
@Column({ comment: "名称", length: 100 })
name: string;
@Column({ name: 'addon_type', comment: 'addon类型', length: 100 })
@Column({ name: "addon_type", comment: "addon类型", length: 100 })
addonType: string;
@Column({ comment: '类型', length: 100 })
@Column({ comment: "类型", length: 100 })
type: string;
@Column({ name: 'setting', comment: '设置', length: 10240, nullable: true })
@Column({ name: "setting", comment: "设置", length: 10240, nullable: true })
setting: string;
@Column({ name: 'is_system', comment: '是否系统级别', nullable: false, default: false })
@Column({ name: "is_system", comment: "是否系统级别", nullable: false, default: false })
isSystem: boolean;
@Column({ name: 'is_default', comment: '是否默认', nullable: false, default: false })
@Column({ name: "is_default", comment: "是否默认", nullable: false, default: false })
isDefault: boolean;
@Column({ name: 'project_id', comment: '项目id' })
@Column({ name: "project_id", comment: "项目id" })
projectId: number;
@Column({
name: 'create_time',
comment: '创建时间',
default: () => 'CURRENT_TIMESTAMP',
name: "create_time",
comment: "创建时间",
default: () => "CURRENT_TIMESTAMP",
})
createTime: Date;
@Column({
name: 'update_time',
comment: '修改时间',
default: () => 'CURRENT_TIMESTAMP',
name: "update_time",
comment: "修改时间",
default: () => "CURRENT_TIMESTAMP",
})
updateTime: Date;
}
@@ -1,3 +1,3 @@
export * from './api/index.js'
export * from './entity/addon.js'
export * from './service/addon-service.js'
export * from "./api/index.js";
export * from "./entity/addon.js";
export * from "./service/addon-service.js";

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