Compare commits

..
134 Commits
Author SHA1 Message Date
xiaojunnuo 0afabd38c6 chore(plugin): add online plugin uninstall function
1. 新增在线插件卸载接口与后端逻辑,支持卸载已同步的在线插件
2. 更新前端插件列表页的卸载调用逻辑,替换原有删除接口
3. 补充对应单元测试用例,验证功能正确性与边界情况
4. 移除冗余的空行注释代码
2026-08-12 00:58:49 +08:00
xiaojunnuo cacd108802 chore(plugin): 完善插件管理功能,优化作者校验与异常处理
1.  新增插件作者必填校验,修复本地插件命名逻辑
2.  优化在线插件安装的异常处理,支持隐藏错误通知
3.  更新文档规范与单元测试用例
4.  重构插件作者字段组件,支持手动输入作者信息
2026-08-12 00:43:32 +08:00
xiaojunnuo 7138243c63 chore(plugin): add vip membership requirement field for plugins
1. add vip select field in plugin crud form with free/plus options
2. replace crown icon with vip icon for vip required plugins
3. fix plugin edit dialog layout and add custom className support
4. correct chinese locale text for plugin title
5. add type ignore and default value handling for vip field
2026-08-12 00:09:33 +08:00
xiaojunnuo 73b096a6e5 chore(plugin): add vip restriction support for plugins
1. 新增数据库迁移脚本添加vip字段
2. 补充实体类和接口的vip字段支持
3. 添加插件卡片会员标识展示逻辑
4. 实现会员限制判断
2026-08-10 23:47:56 +08:00
xiaojunnuo 96a33bd387 chore: 修复企业版模式判断逻辑 2026-08-09 17:10:40 +08:00
xiaojunnuo 3a9069f6e6 perf: 新增AI插件开发受限令牌与隔离API 2026-08-05 21:13:40 +08:00
xiaojunnuo 01abf348d2 refactor(certd-online-plugin): 更新插件开发相关配置与文档 2026-08-02 23:41:47 +08:00
xiaojunnuo eae7edbda0 perf: 支持AI开发在线插件 2026-08-02 23:25:52 +08:00
xiaojunnuo d7c6a06b61 Merge remote-tracking branch 'origin/v2-dev' into v2_audit_log 2026-08-02 21:34:13 +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
xiaojunnuo b84825eae5 Merge remote-tracking branch 'origin/v2-dev' into v2_audit_log 2026-08-02 17:06:52 +08:00
xiaojunnuo 1c6ffe8ac6 chore: log add 2026-08-02 17:06:46 +08:00
xiaojunnuo dc0845b2d2 Merge remote-tracking branch 'origin/v2' into v2_audit_log 2026-08-02 17:06:31 +08:00
xiaojunnuo 7ab502991e perf(plugin): 新增插件评分展示、排序与个人插件筛选功能 2026-08-02 17:04:02 +08:00
aheandGitHub 319f555569 Feature/偏好设置优化 (#775) @nicheng-he
* feat: 偏好设置支持从剪切板导入

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

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

增加「保存到账号」能力,将本地偏好差异写入用户设置;登录成功后自动从账号拉取并应用到本地。
2026-08-02 04:27:07 +08:00
xiaojunnuo 0bf323ab97 refactor(plugin): 简化插件模块代码,复用BaseService方法 2026-08-02 02:48:39 +08:00
xiaojunnuo 255960d434 feat: 支持在线插件下载安装审核等 2026-08-02 00:46:29 +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 a237179a72 Merge remote-tracking branch 'origin/v2-dev' into v2_audit_log 2026-07-26 20:37:28 +08:00
xiaojunnuo 967846bef5 perf(tencent-clb): 为腾讯云CLB部署插件支持远程选择输入
- 重构插件输入配置,使用createRemoteSelectInputDefine统一封装远程选择组件
- 新增onGetCLBList、onGetListenerList、onGetDomainList方法实现数据拉取和格式转换
- 补全完整的单元测试用例,覆盖输入配置校验和数据映射逻辑
2026-07-26 19:01:18 +08:00
xiaojunnuo 0bf9a2d3da chore: plugin 有store page版本 2026-07-25 13:46:11 +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 ce4839bd80 chore: 完善审计日志 2026-07-19 00:49:38 +08:00
xiaojunnuo eee22154e3 chore: 1 2026-07-18 23:52:34 +08:00
xiaojunnuo 49007d3915 perf: 统一镜像地址格式 certd/certd 2026-07-18 23:51:24 +08:00
xiaojunnuo f9b453ca8c Merge remote-tracking branch 'origin/v2-dev' into v2_audit_log 2026-07-18 21:28:30 +08:00
xiaojunnuo 5f53b81c75 perf: 优化动态加载依赖镜像地址,多次重试 2026-07-18 21:07:56 +08:00
xiaojunnuo 9d83adaac8 chore: 1 2026-07-18 19:58:16 +08:00
xiaojunnuo c8c269f612 chore: 1 2026-07-17 15:44:23 +08:00
xiaojunnuo 15740904e5 perf: 增加稳定版,预览版区分,预览版是最新版,稳定版在预览版验证稳定后,再将预览版转为稳定版 2026-07-17 00:58:19 +08:00
xiaojunnuo 85e9ff7a96 Merge branch 'v2-dev' into v2_audit_log 2026-07-16 22:19:04 +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 947fe729cb chore: 继续完善审计日志 2026-07-13 00:53:19 +08:00
xiaojunnuo bfb3ee4c43 chore: 登录失败也记录日志 2026-07-12 22:28:51 +08:00
xiaojunnuo bab1df2c78 chore(i18n): add permanent VIP label and handle permanent VIP case
为国际化多语言添加永久VIP的翻译文本,同时在激活成功弹窗中处理永久会员的过期日期显示逻辑,当过期时间为-1时显示永久而非固定日期
2026-07-12 17:37:25 +08:00
xiaojunnuo 00eabec771 Merge branch 'v2-dev' into v2_audit_log 2026-07-12 17:28:21 +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 f2855d6dac feat: 支持审计日志,操作日志 2026-07-12 02:29:54 +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 4250d0e266 chore: audit log first 2026-07-10 19:24:42 +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
xiaojunnuo 85a239ff8c build: release 2026-07-07 21:20:19 +08:00
xiaojunnuo a545a28dfb Merge branch 'v2' of https://github.com/certd/certd into v2 2026-07-07 21:18:51 +08:00
xiaojunnuo ac876a980c build: release 2026-07-07 21:15:16 +08:00
xiaojunnuo 4b555e8a56 build: publish 2026-07-07 20:04:20 +08:00
xiaojunnuo 1a8d14dc44 build: trigger build image 2026-07-07 20:04:08 +08:00
xiaojunnuo 83263a72d2 v1.42.2 2026-07-07 19:59:41 +08:00
xiaojunnuo 76f3ba8691 build: prepare to build 2026-07-07 19:56:46 +08:00
xiaojunnuo 072edd7aff fix: 修复cname用阿里云校验时报找不到runtimeDepsService的错误 2026-07-07 19:51:23 +08:00
c0be4d702c fix(certd-server): 修复 AWS 中国区 CloudFront 证书部署问题 (#764) @yw-mao
- 修复证书链拆分,保留 END CERTIFICATE 结尾标记,避免 MalformedCertificate
- 更新 CloudFront 时构造干净的 IAM ViewerCertificate,强制 sni-only,
  解决 InvalidViewerCertificate 及中国区不支持 vip 的报错
- 部署完成后清理被替换掉的旧 IAM 证书,避免续期堆积
- IAM/CloudFront 权限不足时抛出可读中文提示,便于运维排查

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 16:20:07 +08:00
xiaojunnuo c3d6db3f1e fix: 修复AsiaIsp CDN证书重复情况下部署失败的问题 2026-07-07 14:15:09 +08:00
xiaojunnuo e05ec53eb5 chore: 1 2026-07-07 10:21:01 +08:00
xiaojunnuo 8d9dad9c82 fix: 修复企业模式下弹出邮箱绑定提醒的问题 2026-07-07 09:43:10 +08:00
xiaojunnuo 0071bcb0e4 fix: 修复火山视频点播源站选择不到自定义源站的bug 2026-07-07 09:42:41 +08:00
xiaojunnuo dbdc1ccd1b build: release 2026-07-07 00:35:25 +08:00
xiaojunnuo 2a606fdb1d build: publish 2026-07-07 00:22:06 +08:00
xiaojunnuo 3b86f30bcf build: trigger build image 2026-07-07 00:21:54 +08:00
xiaojunnuo 7808d028e9 v1.42.1 2026-07-07 00:20:58 +08:00
xiaojunnuo 04bcf73b8b build: prepare to build 2026-07-07 00:18:40 +08:00
xiaojunnuo 1f8179d519 build: prepare to build 2026-07-07 00:14:50 +08:00
xiaojunnuo 3ffd8946f8 Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2026-07-07 00:12:46 +08:00
xiaojunnuo c78898e4c1 perf(certd-server): 使用 jks-go转换jks证书,大幅精简镜像大小 2026-07-07 00:12:37 +08:00
xiaojunnuo d3e4677ea4 perf(pipeline): 将默认历史保留条数从30调整为100
统一修改所有新建流水线场景下的默认keepHistoryCount配置,提升历史记录留存数量
2026-07-06 19:56:54 +08:00
xiaojunnuo d176f9cc0e fix: 修复多域名无法使用passkey登录的bug 2026-07-06 14:34:05 +08:00
xiaojunnuo a65366bbe1 fix: 修复企业模式下登录报projectId不能为空的问题 2026-07-06 13:52:56 +08:00
xiaojunnuo 20cfe74b17 build: release 2026-07-05 21:41:50 +08:00
xiaojunnuo b74db81304 fix(login): 修复输入法 composing 状态下回车触发提交的问题
优化登录页面的回车提交逻辑,避免中文输入法输入过程中按回车误触发表单提交
2026-07-05 21:37:09 +08:00
xiaojunnuo a8adbda04a refactor(runtime-deps): 调整依赖相关类为单例并修复相关逻辑
1. 将NpmRegistryResolver和RuntimeDepsService从请求作用域改为单例作用域
2. 为RuntimeDepsService的安装缓存添加node_modules存在性校验
3. 优化锁文件删除逻辑,处理Windows下文件句柄未立即释放的问题
4. 跳过并修复了清理运行时依赖目录的测试用例
2026-07-05 20:30:00 +08:00
xiaojunnuo 3e80d30ca6 chore: 调整dockerfile 2026-07-05 19:51:08 +08:00
xiaojunnuo 2eb54d50a5 chore(certd-server): adjust dependency installation and docker build steps
调整了依赖包的安装位置,将cross-env和mwtsc移到devDependencies,同时修改Dockerfile中的安装命令:先安装完整依赖构建,再清理非生产依赖,并在最终镜像中仅安装生产依赖
2026-07-05 19:45:49 +08:00
xiaojunnuo 6995308c17 build(certd-server): 调整cross-env依赖的安装位置
将cross-env从devDependencies移动到dependencies中,统一管理运行时依赖
2026-07-05 19:36:41 +08:00
xiaojunnuo 0738d120ae build: publish 2026-07-05 19:31:22 +08:00
xiaojunnuo bad6879589 build: trigger build image 2026-07-05 19:31:10 +08:00
xiaojunnuo b46948c0ba v1.42.0 2026-07-05 19:23:49 +08:00
xiaojunnuo 3024720fc2 build: prepare to build 2026-07-05 19:21:52 +08:00
xiaojunnuo cf854c9278 chore(access-selector): add auto select first item function
1. 为access-selector组件添加defaultSelect属性支持自动选中第一个匹配项
2. 补全组件的typescript语法支持,新增onMounted等依赖导入
3. 在证书申请插件中启用默认选中功能
2026-07-05 01:40:08 +08:00
xiaojunnuo 608cc2a81f perf: 优化用户体验,首次访问时弹出邮箱账号绑定用以初始化账号 2026-07-05 01:14:48 +08:00
xiaojunnuo 396670dc8f chore(locales,plugin): 更新多语言文案并优化插件依赖提示
1. 修复中文文案中"sourcee"拼写错误为"来源"
2. 将插件列表启用禁用列的提示文案替换为更简洁的"点击启用/禁用"
3. 完善插件依赖和第三方依赖的帮助提示,添加格式示例说明
2026-07-04 22:31:46 +08:00
xiaojunnuo 79f65868ca perf(passkey): passkey支持多域名rpid 2026-07-04 21:56:35 +08:00
xiaojunnuo 56e5524a0f perf(cert-plugin): 调整更新天数自动减半逻辑,仅7天ip证书生效,其他情况下不减半 2026-07-04 17:01:02 +08:00
xiaojunnuo 1ae185d0bc fix(aliyun): 修复阿里云CDN/DCDN根据证书自动匹配不到证书的bug 2026-07-03 00:21:50 +08:00
xiaojunnuo 82276b53a8 perf: 阿里云ESA证书部署支持SaaS模式 2026-07-03 00:21:10 +08:00
xiaojunnuo d5882f16be fix: 修复telegram - 符号转义问题 2026-07-02 22:54:24 +08:00
370 changed files with 14972 additions and 5134 deletions
+8 -4
View File
@@ -105,8 +105,10 @@ jobs:
tags: | tags: |
registry.cn-shenzhen.aliyuncs.com/handsfree/certd:slim registry.cn-shenzhen.aliyuncs.com/handsfree/certd:slim
registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{steps.get_certd_version.outputs.result}}-slim registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{steps.get_certd_version.outputs.result}}-slim
greper/certd:slim registry.cn-shenzhen.aliyuncs.com/certd/certd:slim
greper/certd:${{steps.get_certd_version.outputs.result}}-slim registry.cn-shenzhen.aliyuncs.com/certd/certd:${{steps.get_certd_version.outputs.result}}-slim
certd/certd:slim
certd/certd:${{steps.get_certd_version.outputs.result}}-slim
ghcr.io/${{ github.repository }}:slim ghcr.io/${{ github.repository }}:slim
ghcr.io/${{ github.repository }}:${{steps.get_certd_version.outputs.result}}-slim ghcr.io/${{ github.repository }}:${{steps.get_certd_version.outputs.result}}-slim
@@ -119,8 +121,10 @@ jobs:
tags: | tags: |
registry.cn-shenzhen.aliyuncs.com/handsfree/certd:armv7 registry.cn-shenzhen.aliyuncs.com/handsfree/certd:armv7
registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{steps.get_certd_version.outputs.result}}-armv7 registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{steps.get_certd_version.outputs.result}}-armv7
greper/certd:armv7 registry.cn-shenzhen.aliyuncs.com/certd/certd:armv7
greper/certd:${{steps.get_certd_version.outputs.result}}-armv7 registry.cn-shenzhen.aliyuncs.com/certd/certd:${{steps.get_certd_version.outputs.result}}-armv7
certd/certd:armv7
certd/certd:${{steps.get_certd_version.outputs.result}}-armv7
ghcr.io/${{ github.repository }}:armv7 ghcr.io/${{ github.repository }}:armv7
ghcr.io/${{ github.repository }}:${{steps.get_certd_version.outputs.result}}-armv7 ghcr.io/${{ github.repository }}:${{steps.get_certd_version.outputs.result}}-armv7
+88
View File
@@ -0,0 +1,88 @@
name: stable-release
on:
workflow_dispatch:
inputs:
version:
description: "版本号(如 v1.42.5"
required: true
type: string
permissions:
contents: read
packages: write
jobs:
make-stable:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to aliyun container Registry
uses: docker/login-action@v3
with:
registry: registry.cn-shenzhen.aliyuncs.com
username: ${{ secrets.aliyun_cs_username }}
password: ${{ secrets.aliyun_cs_password }}
- name: Login to GitHub Packages
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.dockerhub_username }}
password: ${{ secrets.dockerhub_password }}
# stable 镜像:叠加 ENV 层
- name: Build and push stable images
run: |
echo "FROM greper/certd:${{ inputs.version }}
ENV certd_release_mode=stable" > Dockerfile.stable
docker buildx build \
--platform linux/amd64,linux/arm64 \
--push \
-f Dockerfile.stable \
-t registry.cn-shenzhen.aliyuncs.com/handsfree/certd:stable \
-t registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{ inputs.version }}-stable \
-t registry.cn-shenzhen.aliyuncs.com/certd/certd:stable \
-t registry.cn-shenzhen.aliyuncs.com/certd/certd:${{ inputs.version }}-stable \
-t certd/certd:stable \
-t certd/certd:${{ inputs.version }}-stable \
-t ghcr.io/${{ github.repository }}:stable \
-t ghcr.io/${{ github.repository }}:${{ inputs.version }}-stable \
.
# slim-stable 镜像
- name: Build and push slim-stable images
run: |
echo "FROM greper/certd:${{ inputs.version }}-slim
ENV certd_release_mode=stable" > Dockerfile.slim-stable
docker buildx build \
--platform linux/amd64,linux/arm64 \
--push \
-f Dockerfile.slim-stable \
-t registry.cn-shenzhen.aliyuncs.com/handsfree/certd:slim-stable \
-t registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{ inputs.version }}-slim-stable \
-t registry.cn-shenzhen.aliyuncs.com/certd/certd:slim-stable \
-t registry.cn-shenzhen.aliyuncs.com/certd/certd:${{ inputs.version }}-slim-stable \
-t certd/certd:slim-stable \
-t certd/certd:${{ inputs.version }}-slim-stable \
-t ghcr.io/${{ github.repository }}:slim-stable \
-t ghcr.io/${{ github.repository }}:${{ inputs.version }}-slim-stable \
.
- name: Set AtomGit release as stable
run: |
export ATOMGIT_TOKEN=${{ secrets.ATOMGIT_TOKEN }}
export VERSION=${{ inputs.version }}
npm run set-release-stable
+3 -1
View File
@@ -39,4 +39,6 @@ pnpm-lock.yaml
# Certd 推广报告,仅本地使用 # Certd 推广报告,仅本地使用
/popularize/reports/ /popularize/reports/
output/ output/
.uploads/ .uploads/
.certd-plugin-history/
.tmp
@@ -1,412 +0,0 @@
# 插件依赖按需加载方案
## 背景与目标
### 当前问题
- `packages/ui/certd-server/node_modules` 包含 50+ 个插件的所有依赖,体积庞大
- 大量云厂商 SDK(AWS、阿里云、腾讯云、华为云等)只在特定插件中使用
- 用户通常只使用少数几个插件,但必须安装所有依赖
### 目标
实现依赖的按需下载和加载:
1. 插件依赖独立管理,不占用主 `node_modules` 空间
2. 只有当用户首次使用某插件时,才动态下载该插件需要的依赖
3. 依赖安装完成后,通过 `await import()` 从独立路径加载
4. 保持现有插件代码的最小改动
## 当前架构分析
### 插件加载机制
- 插件位于 `packages/ui/certd-server/src/plugins/` 下(50+ 个插件目录)
- `AutoLoadPlugins` 类在启动时扫描 `dist/plugins` 目录并动态导入
- 插件注册到不同的 registry`accessRegistry`, `pluginRegistry`, `dnsProviderRegistry`
- 插件代码已经使用 `await import()` 进行懒加载(如 `await import("@aws-sdk/client-acm")`
### 重型依赖分布
`packages/ui/certd-server/package.json` 分析,以下依赖体积大且仅特定插件使用:
**云厂商 SDK(按插件分组):**
- **AWS 插件**`@aws-sdk/client-acm`, `@aws-sdk/client-cloudfront`, `@aws-sdk/client-iam`, `@aws-sdk/client-route-53`, `@aws-sdk/client-s3`, `@aws-sdk/client-sts`
- **阿里云插件**`@alicloud/openapi-client`, `@alicloud/pop-core`, `@alicloud/tea-typescript`, `@alicloud/fc20230330`
- **腾讯云插件**`tencentcloud-sdk-nodejs`, `cos-nodejs-sdk-v5`
- **华为云插件**`@huaweicloud/huaweicloud-sdk-cdn`, `@huaweicloud/huaweicloud-sdk-core`
- **Azure 插件**`@azure/arm-dns`, `@azure/identity`
- **Google Cloud 插件**`@google-cloud/dns`, `@google-cloud/publicca`
- **火山引擎插件**`@volcengine/openapi`, `@volcengine/tos-sdk`
**网络/工具库:**
- `ssh2`, `socks`, `socks-proxy-agent`SSH 相关插件)
- `ali-oss`, `qiniu`, `basic-ftp`(存储/传输插件)
- `nodemailer`(邮件通知插件)
**通用依赖(保留在主 package.json):**
- `@midwayjs/*` 系列(框架核心)
- `@certd/*` 系列(项目内部包)
- `axios`, `lodash-es`, `dayjs`, `js-yaml` 等基础工具
## 设计方案
### 架构概览
```
packages/ui/certd-server/
├── package.json # 主依赖(框架、通用工具)
├── node_modules/ # 主依赖安装目录
├── optional-deps/ # 新增:可选依赖管理目录
│ ├── package.json # 可选依赖总配置(用于 pnpm install
│ ├── pnpm-lock.yaml # 可选依赖锁文件
│ └── node_modules/ # 可选依赖安装目录
├── src/
│ └── modules/
│ └── dependency/ # 新增:依赖管理模块
│ ├── dependency-manager.ts # 核心:依赖管理器
│ ├── dependency-registry.ts # 依赖注册表(插件 -> 依赖映射)
│ └── types.ts # 类型定义
```
### 核心组件
#### 1. 依赖管理器(DependencyManager
**职责:**
- 检查依赖是否已安装
- 动态执行 `pnpm install` 安装缺失依赖
- 提供从 `optional-deps/node_modules` 加载依赖的方法
- 并发控制:避免多个插件同时触发安装
**关键方法:**
```typescript
class DependencyManager {
// 确保依赖已安装,返回依赖模块
async ensureAndImport<T>(packageName: string): Promise<T>
// 检查依赖是否已安装
async isInstalled(packageName: string): Promise<boolean>
// 安装依赖(带锁,避免并发)
async installDependencies(packages: string[]): Promise<void>
// 从 optional-deps/node_modules 加载依赖
async loadModule<T>(packageName: string): Promise<T>
}
```
**实现要点:**
- 使用文件锁(如 `proper-lockfile`)防止并发安装
- 安装前检查 `optional-deps/node_modules/{packageName}` 是否存在
- 安装命令:`pnpm install --dir optional-deps --ignore-workspace`
- 加载时使用绝对路径:`import('file:///absolute/path/to/optional-deps/node_modules/package')`
#### 2. 依赖注册表(DependencyRegistry
**职责:**
- 维护插件名称到依赖列表的映射
- 提供依赖查询接口
**数据结构:**
```typescript
interface PluginDependencyConfig {
pluginName: string;
dependencies: {
packageName: string;
version: string;
optional?: boolean; // 是否可选(安装失败不阻塞)
}[];
}
// 示例注册
dependencyRegistry.register('plugin-aws', [
{ packageName: '@aws-sdk/client-acm', version: '^3.964.0' },
{ packageName: '@aws-sdk/client-cloudfront', version: '^3.964.0' },
{ packageName: '@aws-sdk/client-route-53', version: '^3.964.0' },
]);
```
#### 3. 插件集成
**改造现有插件代码:**
改造前(`plugin-aws/libs/aws-client.ts`):
```typescript
const { ACMClient, ImportCertificateCommand } = await import("@aws-sdk/client-acm");
```
改造后:
```typescript
import { DependencyManager } from "../../../modules/dependency/dependency-manager.js";
const depManager = new DependencyManager();
const { ACMClient, ImportCertificateCommand } = await depManager.ensureAndImport("@aws-sdk/client-acm");
```
**简化方案(推荐):**
创建辅助函数,减少改动量:
```typescript
// src/modules/dependency/import-helper.ts
export async function importOptionalDep<T>(packageName: string): Promise<T> {
const depManager = new DependencyManager();
return await depManager.ensureAndImport<T>(packageName);
}
// 插件中使用
import { importOptionalDep } from "../../../modules/dependency/import-helper.js";
const { ACMClient } = await importOptionalDep("@aws-sdk/client-acm");
```
### 实施步骤
#### 阶段一:基础设施搭建
1. 创建 `optional-deps/` 目录结构
2. 生成 `optional-deps/package.json`(包含所有可选依赖)
3. 实现 `DependencyManager` 核心逻辑
4. 实现依赖安装锁机制
5. 编写单元测试
#### 阶段二:依赖迁移
6. 从主 `package.json` 移除可选依赖
7. 将依赖添加到 `optional-deps/package.json`
8. 创建依赖注册表,映射插件到依赖
#### 阶段三:插件改造
9. 创建 `import-helper.ts` 辅助函数
10. 逐步改造插件代码,使用 `importOptionalDep` 加载依赖
11. 优先改造重型依赖(AWS、阿里云、腾讯云等)
#### 阶段四:测试与优化
12. 端到端测试:验证依赖按需安装和加载
13. 性能优化:缓存已加载的模块
14. 错误处理:安装失败时的降级策略
15. 文档:编写使用说明和迁移指南
## 关键技术决策
### 1. 依赖分组策略
**选择:按插件分组**
- 每个插件声明自己需要的依赖
- 优点:职责清晰,易于维护
- 缺点:可能有重复依赖(但 pnpm 会去重)
**备选:按功能分组**
- 将依赖按功能分组(如 "aws-deps", "aliyun-deps"
- 优点:更细粒度控制
- 缺点:增加复杂度
### 2. 安装触发时机
**选择:首次使用时触发**
- 在插件的 `execute()``getClient()` 方法中触发安装
- 优点:真正的按需加载
- 缺点:首次使用有延迟
**备选:启动时预检查**
- 启动时扫描启用的插件,预安装依赖
- 优点:避免运行时延迟
- 缺点:可能安装不需要的依赖
### 3. 依赖路径解析
**选择:使用绝对路径 + `file://` 协议**
```typescript
const modulePath = path.resolve(__dirname, '../../optional-deps/node_modules', packageName);
return await import(`file://${modulePath}/index.js`);
```
**原因:**
- Node.js ESM 要求明确的 URL 格式
- 避免模块解析冲突
### 4. 并发控制
**选择:文件锁 + 内存锁双重保护**
- 使用 `proper-lockfile` 锁定 `optional-deps/` 目录
- 内存中使用 `Map` 记录正在安装的依赖
- 避免多个插件同时触发安装
### 5. 错误处理
**策略:**
- 安装失败时记录日志,抛出明确的错误信息
- 提供手动安装命令提示:`请运行: cd optional-deps && pnpm install`
- 支持降级:某些非核心依赖安装失败时,插件可以部分功能可用
## 验证方案
### 单元测试
1. 测试 `DependencyManager.isInstalled()` 正确检测依赖状态
2. 测试 `DependencyManager.installDependencies()` 成功安装依赖
3. 测试并发安装时的锁机制
4. 测试从 `optional-deps/node_modules` 加载模块
### 集成测试
1. 清空 `optional-deps/node_modules`
2. 启动服务,验证不触发安装
3. 调用 AWS 插件,验证触发安装并成功加载
4. 再次调用,验证不重复安装
5. 验证主 `node_modules` 体积减少
### 性能测试
1. 测量首次安装依赖的耗时
2. 测量后续加载的耗时(应该与正常 import 相近)
3. 对比改造前后的 `node_modules` 大小
## 风险与挑战
### 1. 首次使用延迟
**风险:** 用户首次使用插件时需要等待依赖安装(可能几十秒)
**缓解:**
- 在 UI 上显示安装进度
- 提供预安装命令:`pnpm run install-optional-deps`
- 文档说明首次使用会有延迟
### 2. 离线环境
**风险:** 离线环境无法下载依赖
**缓解:**
- 提供完整安装包(包含所有可选依赖)
- 支持手动复制 `node_modules`
### 3. 版本冲突
**风险:** 可选依赖与主依赖版本冲突
**缓解:**
- 使用 `--ignore-workspace` 隔离安装
- 定期同步主依赖版本
### 4. TypeScript 类型
**风险:** 动态导入的类型推断
**缓解:**
- 保留 `@types/*` 在主 `devDependencies`
- 使用泛型和类型断言
## 预期收益
1. **空间节省:**`node_modules` 体积减少 60-70%(估算)
2. **安装速度:** 初始 `pnpm install` 速度提升 3-5 倍
3. **用户体验:** 不使用的插件不占用空间,按需加载
4. **维护性:** 依赖分组清晰,易于管理
## 后续优化
1. **依赖预热:** 在后台预安装常用插件依赖
2. **依赖缓存:** 支持从 CDN 或本地缓存安装
3. **依赖更新:** 提供命令批量更新可选依赖
4. **插件市场:** 支持从远程下载插件及其依赖配置
## 附录:依赖分类清单
### 可选依赖(迁移到 optional-deps/package.json
**AWS 相关(plugin-aws, plugin-aws-cn):**
```json
{
"@aws-sdk/client-acm": "^3.964.0",
"@aws-sdk/client-cloudfront": "^3.964.0",
"@aws-sdk/client-iam": "^3.964.0",
"@aws-sdk/client-route-53": "^3.964.0",
"@aws-sdk/client-s3": "^3.964.0",
"@aws-sdk/client-sts": "^3.990.0"
}
```
**阿里云相关(plugin-aliyun, plugin-lib/aliyun):**
```json
{
"@alicloud/fc20230330": "^4.1.7",
"@alicloud/openapi-client": "^0.4.12",
"@alicloud/openapi-util": "^0.3.2",
"@alicloud/pop-core": "^1.7.10",
"@alicloud/sts-sdk": "^1.0.2",
"@alicloud/tea-typescript": "^1.8.0",
"@alicloud/tea-util": "^1.4.10",
"ali-oss": "^6.21.0"
}
```
**腾讯云相关(plugin-tencent, plugin-lib/tencent):**
```json
{
"tencentcloud-sdk-nodejs": "^4.1.112",
"cos-nodejs-sdk-v5": "^2.14.6"
}
```
**华为云相关(plugin-huawei):**
```json
{
"@huaweicloud/huaweicloud-sdk-cdn": "3.1.185",
"@huaweicloud/huaweicloud-sdk-core": "3.1.185",
"@huaweicloud/huaweicloud-sdk-elb": "3.1.185",
"@huaweicloud/huaweicloud-sdk-iam": "3.1.185",
"esdk-obs-nodejs": "^3.25.6"
}
```
**Azure 相关(plugin-azure):**
```json
{
"@azure/arm-dns": "^5.1.0",
"@azure/identity": "^4.13.1"
}
```
**Google Cloud 相关(plugin-google, plugin-cert/google):**
```json
{
"@google-cloud/dns": "^5.3.1",
"@google-cloud/publicca": "^1.3.0"
}
```
**火山引擎相关(plugin-volcengine):**
```json
{
"@volcengine/openapi": "^1.28.1",
"@volcengine/tos-sdk": "^2.9.1"
}
```
**SSH/网络相关(plugin-host, plugin-lib/ssh):**
```json
{
"ssh2": "^1.17.0",
"socks": "^2.8.3",
"socks-proxy-agent": "^8.0.4",
"basic-ftp": "^5.0.5"
}
```
**其他存储/传输(plugin-qiniu, plugin-lib/qiniu):**
```json
{
"qiniu": "^7.12.0"
}
```
**邮件通知(plugin-notification/email):**
```json
{
"nodemailer": "^6.9.16"
}
```
### 主依赖(保留在主 package.json
**框架核心:**
- `@midwayjs/*` 系列
- `@koa/cors`
- `typeorm`, `better-sqlite3`, `mysql2`, `pg`
**项目内部包:**
- `@certd/*` 系列
**通用工具:**
- `axios`, `lodash-es`, `dayjs`, `js-yaml`
- `crypto-js`, `jsonwebtoken`, `bcryptjs`
- `reflect-metadata`, `uuid`, `nanoid`
- 等等
## 总结
本方案通过引入独立的可选依赖管理机制,实现了插件依赖的按需下载和加载。核心思路是:
1. **隔离管理:**`optional-deps/` 目录下维护独立的 `package.json``node_modules`
2. **动态安装:** 通过 `DependencyManager` 在首次使用时触发 `pnpm install`
3. **路径加载:** 使用绝对路径从独立目录加载依赖模块
4. **最小改动:** 通过辅助函数 `importOptionalDep` 简化插件代码改造
该方案可以显著减少主 `node_modules` 体积,提升初始安装速度,同时保持现有架构的兼容性和可维护性。
+1
View File
@@ -400,6 +400,7 @@ export class OAuthDemoAccess extends BaseAccess {
4. **错误处理**:API 调用失败时应抛出明确的错误信息。 4. **错误处理**:API 调用失败时应抛出明确的错误信息。
5. **测试方法**:实现 `onTestRequest` 方法,以便用户可以测试授权是否正常。 5. **测试方法**:实现 `onTestRequest` 方法,以便用户可以测试授权是否正常。
6. **统一接口调用**:封装统一的 API 请求方法,避免重复编写错误处理逻辑。 6. **统一接口调用**:封装统一的 API 请求方法,避免重复编写错误处理逻辑。
7. **旧版数据兼容**: 新增注解的插件参数,必须要考虑旧版数据兼容,比如新增一个deployType参数,有两种值:`default``custom`,需要在使用时判空,走旧版逻辑。
## 开发技巧 ## 开发技巧
@@ -0,0 +1,164 @@
---
name: certd-online-plugin-dev
description: 用于通过 Certd API 开发、修改、读取、保存和恢复在线插件。当用户要求使用 Codex 或 Trae 创建 Task、DNS Provider、Access 插件,或优化已有 Certd 插件时使用。
---
# Certd 在线插件开发 Skill
## 开发方式
本 Skill 使用 Certd HTTP API,不使用 WebSocket,也不依赖浏览器传递代码草稿。
- 前端只生成包含需求、API 地址和认证 Token 的启动提示词。
- Agent 直接调用 Certd API 读取和保存插件 YAML。
- Agent 的进度、日志和代码修改在 Codex/Trae 中查看。
- 插件开发临时文件和修改历史统一保存在 Agent 工作区的 `.tmp/online-plugin-dev/` 下,不保存到浏览器或 Certd 后端。
## 启动检查
开始开发前先确认当前工作目录是否已经在 Certd 项目内:
- 应存在 `package.json`
- 应存在 `packages/ui/certd-server/src/plugins/`
- 应存在 `.trae/skills/`
- 应存在 `.trae/skills/certd-online-plugin-dev/SKILL.md`
如果当前目录不是 Certd 项目,或缺少 `certd-online-plugin-dev` Skill,先拉取 Certd 仓库代码并切换到仓库内工作( --depth 1 拉取第一层即可)
1. 优先使用 `https://atomgit.com/certd/certd/`
2. 如果 AtomGit 拉取失败,再使用 `https://github.com/certd/certd`
3. 拉取后重新检查上述特征,并读取 `.trae/skills/certd-online-plugin-dev/SKILL.md`
开发插件时,参考 Certd 项目下已有内置插件实现:`packages/ui/certd-server/src/plugins/`
## 插件来源
Certd 插件按来源分为三类:
- 内置插件:`type: "builtIn"`,随 Certd 安装包提供。可读取并在流水线中使用,不应通过本 Skill 修改或覆盖。
- 市场插件:`type: "store"`,且存在 `appId``developerId`。它来自在线插件市场,可能尚未安装到本地;是否可修改只能以接口返回的 `editable` 为准。
- 本地插件:`type: "store"`,但没有 `appId``developerId`。它是当前 Certd 实例本地创建、导入或复制的插件,可直接保存;发布到市场后会带上市场归属信息。
不要只根据 `type: "store"` 判断插件是否来自市场,也不要自行推断编辑权限;始终使用列表结果中的 `editable` 字段。
## API 认证
提示词会提供 Certd API 地址和仅限 AI 插件开发接口的受限 Token。调用 API 时使用:
```http
Authorization: <token>
Content-Type: application/json
```
不要把 Token 写入代码、历史摘要、日志、提交信息或插件 YAML。
所有 Certd API 请求统一使用 Node.js 18+ 的 `fetch`。不要使用 PowerShell 的 `Invoke-RestMethod``Invoke-WebRequest` 或 .NET HTTP 客户端发送插件 YAML/JSON;它们在 Windows 上可能造成中文乱码或使完整 YAML 导入请求长时间无响应。
Node 请求须直接读取 UTF-8 文件或在 Node 内构造 JSON,并使用 `JSON.stringify`
```javascript
const response = await fetch(`${apiBase}/scoped/sys/ai/plugin/find`, {
method: "POST",
headers: { Authorization: token, "Content-Type": "application/json; charset=utf-8" },
body: JSON.stringify({ keywords: ["nginx"], includeBuiltIn: true, includeStore: true }),
})
```
## UTF-8 保存
在 Windows 上,Node 直接以 UTF-8 读取 YAML 并用 `JSON.stringify` 发送;不要让 PowerShell 转发含中文的 YAML/JSON。保存后检查中文字段不含 `?`,再调用 `/scoped/sys/ai/plugin/find``/scoped/sys/ai/plugin/info` 验证。
## API 工作流
1. 使用 `/scoped/sys/ai/plugin/find` 查询插件和 Access,可通过 `keywords` 数组传递多个关键词。
2. 查询结果包含 `editable`
- `editable: true`:允许当前 Agent 修改并保存。
- `editable: false`:只能读取和使用,不能修改。
3. 读取完整 YAML 时调用 `/scoped/sys/ai/plugin/export`
4. 所有插件保存统一调用 `/scoped/sys/ai/plugin/import`,并始终传递完整 YAML
- 新插件使用 `override: false`
- 已有插件使用 `override: true`;导入接口根据 `author``name` 定位并覆盖已有记录。
5. 不使用 `/sys/plugin/add``/sys/plugin/update` 保存插件,避免保存路径分叉、字段丢失和 Windows 请求兼容性问题。
6. 保存完成后重新调用 `/scoped/sys/ai/plugin/find``/scoped/sys/ai/plugin/info` 验证结果。
`/scoped/sys/ai/plugin/find` 会在一次请求中分别查询内置插件和 `store` 插件,再合并返回;`store` 插件需按上述字段区分市场插件与本地插件。
详细请求字段见 `references/certd-api.md`
## Access 协作
开发 Task 或 DNS Provider 前,先用 `/scoped/sys/ai/plugin/find` 查询对应 Access
1. 如果没有对应 Access,先创建 Access 插件,再创建业务插件。
2. 如果已有 Access,先读取它的完整 YAML 和 `content`
3. 如果 Access 已提供所需 API/SDK,业务插件优先复用。
4. 如果缺少能力:
- `editable: true`:优先修改 Access,并先保存历史。
- `editable: false`:在当前业务插件中实现必要的 API 调用。
5. 业务插件通过 `dependPlugins` 声明 Access 依赖。
详细规则见 `references/access-development.md`
## 本地历史
开发插件时,必须在当前工作区创建并使用 `.tmp/online-plugin-dev/` 作为临时目录。历史记录、临时 YAML、脚本草稿和调试记录都放在该目录下。
每次修改插件前,必须将完整 YAML 保存到 `.tmp/online-plugin-dev/history/`
```text
.tmp/online-plugin-dev/
history/
plugin-12/
2026-08-02T12-30-00-before-edit.yaml
2026-08-02T12-30-00-change.md
```
保存要求:
- 修改前保存完整 YAML。
- 修改后保存修改摘要。
- 恢复前再次备份当前版本。
- 不上传历史文件,不保存 Token、证书、私钥或真实授权值。
详细格式见 `references/local-history.md`
## YAML 和脚本规范
插件始终以完整 YAML 传递和保存,脚本源码放在顶层 `content` 字段。
- 统一使用 `await _ctx.import(...)` 引用模块。
- `"/@/..."` 表示以绝对路径引用 `server/src/` 下的模块。
- 最后返回继承目标基类的 class。
- 不使用 `import``export`、装饰器或独立源码文件语法。
- 使用 `this.logger` 打印插件执行日志。
- 使用 `this.ctx.http` 访问 HTTP 能力。
- 失败时抛出 `Error`
需要字段格式时读取 `references/online-yaml-format.md`
需要组件示例时读取 `references/component-examples.md`
## 示例插件
开发对应类型插件前,先读取 `examples/` 下的示例:
- Access`examples/DemoAccess.yaml`
- 部署/Task`examples/DemoDeploy.yaml`
- DNS Provider`examples/DemoDnsProvider.yaml`
示例是完整在线插件 YAML,重点参考 `input` 配置、依赖声明和 `content` 脚本结构。
## 子 Skill
- Task`skills/task-plugin-dev/SKILL.md`
- DNS Provider`skills/dns-provider-dev/SKILL.md`
- Access`skills/access-plugin-dev/SKILL.md`
## 安全边界
Certd 会保存证书、私钥、API Token、云厂商密钥、SSH 凭据和其他敏感授权。
- 禁止读取、打印或上传真实授权值。
- 禁止把认证 Token 写入插件、历史、日志或摘要。
- 禁止读取无关的证书、私钥、Cookie、环境变量和系统设置。
- 只使用脱敏示例数据和公开文档。
- 不要自动发布;保存、测试、审核和发布由用户确认。
@@ -0,0 +1,4 @@
interface:
display_name: "Certd 在线插件开发"
short_description: "通过 Certd API 使用 Codex 或 Trae 开发在线插件"
default_prompt: "读取在线插件 YAML 和对应类型规范,按需求修改 content,保存历史记录后通过 Certd API 写回。"
@@ -0,0 +1,37 @@
name: DemoAccess
icon: logos:airflow-icon
title: Demo-授权插件示例 # 模块-插件名
group: null
desc: 这只是一个示例
version: 1.0.0
pluginType: access
author: greper
input:
username:
title: 用户名
required: true
encrypt: false
component:
name: a-input
allowClear: true
password:
title: 密码
required: true
encrypt: true
component:
name: a-input
allowClear: true
showRunStrategy: false
default:
strategy:
runStrategy: 1
content: |
// 必须使用 await import 来引入模块
const { BaseAccess } = await import("@certd/pipeline")
// 需要返回一个继承BaseAccess的类
return class DemoAccess extends BaseAccess {
// 授权的字段,跟左边input一一对应
username;
password;
}
@@ -0,0 +1,100 @@
name: DemoDeploy
icon: logos:amp-icon
title: Demo-部署插件示例 # 模块-插件名
group: cdn
desc: 这仅仅是一个示例
version: 1.0.0
pluginType: deploy
author: greper
input:
cert:
title: 前置任务证书
helper: 请选择前置任务产生的证书
component:
name: output-selector
vModel: modelValue
from:
- ':cert:'
required: true
certDomains:
title: 当前证书域名
component:
name: cert-domains-getter
mergeScript: |
return {
component:{
inputKey: ctx.compute(({form})=>{
return form.cert
}),
}
}
required: true
accessId:
title: Access授权
helper: xxxx的授权
component:
name: access-selector
type: aliyun
required: true
key1:
title: 输入示例1
required: false
key2:
title: 可选项
component:
name: a-select
vMode: value
options:
- value: '1'
label: 选项1
- value: '2'
label: 选项2
required: false
showRunStrategy: false
default:
strategy:
runStrategy: 1
content: >
// 要用await来import模块
const { AbstractTaskPlugin } = await _ctx.import("@certd/pipeline")
// 使用_ctx.import("/@/xxx.js") 以绝对路径引用模块,/@相当于根路径
const {AliyunAccess} = await _ctx.import("/@/plugins/plugin-lib/aliyun/access/index.js")
_ctx.logger.info("AliyunAccess:",AliyunAccess)
// 要返回一个继承AbstractTaskPlugin的class
return class DemoTask extends AbstractTaskPlugin {
// 这里是插件的输入参数,对应左边的input配置
cert;
certDomains;
accessId;
key1;
key2;
// 编写执行方法
async execute(){
// 根据accessId获取授权配置
const access = await this.getAccess(this.accessId)
//必须使用this.logger打印日志
// this.logger.info("cert:",this.cert);
this.logger.info("certDomains:",this.certDomains);
this.logger.info("access:",access);
this.logger.info("key1:",this.key1);
this.logger.info("key2:",this.key2);
this.logger.info("开始xxx部署任务")
// 你的部署任务代码 【必须实现】
// this.ctx里面有一些常用的方法类,比如utils、http、logger等
const res = await this.ctx.http.request({url:"https://www.baidu.com"})
if(res.error){
//抛出异常,终止任务,否则将被判定为执行成功
throw new Error("部署失败:"+res.message)
}
this.logger.info("执行成功")
// this.outputName = xxxx //设置输出参数,可以被其他插件选择使用
}
}
@@ -0,0 +1,119 @@
name: DemoDnsProvider
icon: fa-solid:frog
title: Demo-Dns提供商插件示例 # 模块-插件名
desc: 这只是一个示例
type: custom
version: 1.0.0
pluginType: dnsProvider
author: greper
accessType: aliyun # 需要的授权类型
showRunStrategy: false
default:
strategy:
runStrategy: 1
content: |+
const { AbstractDnsProvider } = await _ctx.import("@certd/pipeline")
return class DemoDnsProvider extends AbstractDnsProvider {
// 创建dns解析记录,用于验证域名所有权 【必须实现】
async createRecord(options) {
/**
* fullRecord: '_acme-challenge.test.example.com',
* value: 一串uuid
* type: 'TXT',
* domain: 'example.com'
*/
const { fullRecord, value, type, domain } = options;
const access = this.ctx.access
this.logger.info('添加域名解析:', fullRecord, value, type, domain);
// const record = await sdk.createRecord() // 调用对应的接口创建解析记录
//返回解析记录,用于后面清理
return record
}
/**
* 删除dns解析记录,清理申请痕迹【必须实现】
* @param options
*/
async removeRecord(options) {
const { fullRecord, value } = options.recordReq;
const record = options.recordRes; // createRecord接口返回的record
const access = this.ctx.access
this.logger.info('删除域名解析:', fullRecord, value);
if (!record) {
this.logger.info('record为空,不执行删除');
return;
}
const recordId = record.id;
// 这里调用删除txt dns解析记录接口
// sdk.removeRecord(recordId)
this.logger.info("删除域名解析成功");
}
/**
* 获取域名列表 【可选,没有实现的话,不支持在certd域名管理中导入域名列表,不影响证书申请】
* @param req
* @returns
*/
async getDomainListPage(req: PageSearch): Promise<PageRes<DomainRecord>> {
const pager = new Pager(req);
const params = {
RegionId: "cn-hangzhou",
PageSize: pager.pageSize,
PageNumber: pager.pageNo,
};
const requestOption = {
method: "POST",
};
const ret = await this.client.request("DescribeDomains", params, requestOption);
const list =
ret.Domains?.Domain?.map(item => ({
id: item.DomainId,
domain: item.DomainName,
})) || [];
return {
list,
total: ret.TotalCount,
};
}
/**
* 获取域名解析记录列表 【可选,没有实现的话,不支持在站点监控里面导入网址,不影响证书申请】
* @param domain
* @param req
* @returns
*/
async getRecordListPage(domain: string, req: PageSearch): Promise<PageRes<DnsResolveRecord>> {
const pager = new Pager(req);
const params = {
RegionId: "cn-hangzhou",
DomainName: domain,
PageSize: pager.pageSize,
PageNumber: pager.pageNo,
};
const requestOption = {
method: "POST",
};
const ret = await this.client.request("DescribeDomainRecords", params, requestOption);
const rawList = ret.DomainRecords?.Record || [];
const list = rawList.map(item => ({
id: item.RecordId,
hostRecord: item.RR,
fullRecord: item.RR === "@" ? domain : `${item.RR}.${domain}`,
type: item.Type,
value: item.Value,
}));
return {
list,
total: ret.TotalCount,
};
}
}
@@ -0,0 +1,19 @@
# Access 开发规范
Access 插件负责保存授权配置,也负责封装平台 API/SDK,供 Task 和 DNS Provider 复用。
## 查询顺序
1. 调用 `/scoped/sys/ai/plugin/find`,使用 `pluginType: access`
2. 根据 `name``author``fullName` 识别目标 Access。
3. 使用 `/scoped/sys/ai/plugin/export` 读取完整 YAML。
4. 检查 `content` 中已经提供的方法。
## 修改规则
- Access 的 `editable: true` 时才允许修改。
- 修改前先保存本地历史。
- 优先把通用 API/SDK 能力放入 Access。
- 业务插件通过 `dependPlugins` 依赖 Access。
- `editable: false` 时不要尝试修改 Access,在业务插件内部实现必要的调用。
- 不要在日志中打印完整授权配置。
@@ -0,0 +1,70 @@
# Certd API
以下接口都以前端生成提示词中的 API 地址为基础地址,并使用 `Authorization` 请求头。
## 查询插件
```http
POST /scoped/sys/ai/plugin/find
Content-Type: application/json
Authorization: <token>
```
请求示例:
```json
{
"keywords": ["aliyun", "dns"],
"pluginType": "access",
"includeBuiltIn": true,
"includeStore": true
}
```
接口会分别查询内置插件和 `store` 插件,再合并返回:
- `type: "builtIn"`:内置插件,不通过在线开发 API 修改。
- `type: "store"` 且有 `appId``developerId`:市场插件。
- `type: "store"` 且没有 `appId``developerId`:本地插件。
结果中的 `editable` 是唯一的编辑权限依据;不能只按插件来源判断是否可修改。
列表结果只返回插件基础信息,不返回 `content``setting``sysSetting``metadata``extra`。需要完整 YAML 时再调用 `/scoped/sys/ai/plugin/export`
## 读取插件信息
```http
POST /scoped/sys/ai/plugin/info?id=12
Authorization: <token>
```
## 导出完整 YAML
```http
POST /scoped/sys/ai/plugin/export
Content-Type: application/json
Authorization: <token>
```
```json
{
"id": 12
}
```
## 保存插件
使用完整 YAML 导入:
```http
POST /scoped/sys/ai/plugin/import
```
```json
{
"content": "完整 YAML",
"override": true,
"type": "store"
}
```
保存后重新调用 `/scoped/sys/ai/plugin/find``/scoped/sys/ai/plugin/info` 验证。
@@ -0,0 +1,423 @@
# Component Examples
Source: `packages/ui/certd-server/src/plugins`.
Detected **30** distinct component names.
These snippets are extracted from existing Certd plugins. For online plugins, place the object under `input.<field>.component` in the YAML document.
## `EmailSelector`
Source: `packages/ui/certd-server/src/plugins/plugin-other/plugins/plugin-deploy-to-mail.ts`
```ts
component: {
name: "EmailSelector",
vModel: "value",
mode: "tags",
}
```
## `ParamsShow`
Source: `packages/ui/certd-server/src/plugins/plugin-template/email/plugin-common.ts`
```ts
component: {
name: "ParamsShow",
params: [
{ label: "标题", value: "title" },
{ label: "内容", value: "content" },
{ label: "URL", value: "url" },
],
}
```
## `RemoteSelect`
Source: `packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/getter/aliyun.ts`
```ts
component: {
name: "RemoteSelect",
vModel: "value",
pager: true,
single: true,
}
```
## `a-alert`
Source: `packages/ui/certd-server/src/plugins/plugin-template/email/plugin-base.ts`
```ts
component: {
name: "a-alert",
props: {
type: "info",
message: "在标题和内容模版中,通过${name}引用参数,例如: 感谢注册,您的注册验证码为:${code}",
},
}
```
## `a-auto-complete`
Source: `packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-ack/index.ts`
```ts
component: {
name: "a-auto-complete",
vModel: "value",
options: [
{ value: "cn-qingdao", label: "华北1(青岛)" },
{ value: "cn-beijing", label: "华北2(北京)" },
{ value: "cn-zhangjiakou", label: "华北3(张家口)" },
{ value: "cn-huhehaote", label: "华北5(呼和浩特)" },
{ value: "cn-wulanchabu", label: "华北6(乌兰察布)" },
{ value: "cn-hangzhou", label: "华东1(杭州)" },
{ value: "cn-shanghai", label: "华东2(上海)" },
{ value: "cn-shenzhen", label: "华南1(深圳)" },
{ value: "cn-guangzhou", label: "华南3(广州)" },
{ value: "ap-southeast-2", label: "澳大利亚(悉尼)" },
{ value: "ap-southeast-3", label: "马来西亚(吉隆坡)" },
{ value: "ap-northeast-1", label: "日本(东京)" },
{ value: "cn-chengdu", label: "西南1(成都)" },
{ value: "ap-southeast-1", label: "新加坡" },
{ value: "ap-southeast-5", label: "印度尼西亚(雅加达)" },
{ value: "cn-hongkong", label: "中国香港" },
{ value: "eu-central-1", label: "德国(法兰克福)" },
{ value: "us-east-1", label: "美国(弗吉尼亚)" },
{ value: "us-west-1", label: "美国(硅谷)" },
{ value: "eu-west-1", label: "英国(伦敦)" },
{ value: "me-east-1", label: "阿联酋(迪拜)" },
//金融云
{ value: "cn-beijing-finance-1", label: "华北2 金融云(邀测)" },
{ value: "cn-hangzhou-finance", label: "华东1 金融云" },
{ value: "cn-shanghai-finance-1", label: "华东2 金融云" },
{ value: "cn-shenzhen-finance-1", label: "华南1 金融云" },
],
placeholder: "集群所属大区",
}
```
## `a-input`
Source: `packages/ui/certd-server/src/plugins/plugin-admin/plugin-db-backup.ts`
```ts
component: {
name: "a-input",
type: "value",
placeholder: `默认${defaultBackupDir}`,
}
```
## `a-input-number`
Source: `packages/ui/certd-server/src/plugins/plugin-acepanel/access.ts`
```ts
component: {
name: "a-input-number",
vModel: "value",
}
```
## `a-input-password`
Source: `packages/ui/certd-server/src/plugins/plugin-51dns/access.ts`
```ts
component: {
name: "a-input-password",
vModel: "value",
placeholder: "密码",
}
```
## `a-radio-group`
Source: `packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-esa/index.ts`
```ts
component: {
name: "a-radio-group",
vModel: "value",
options: [
{ label: "边缘证书", value: "edge" },
{ label: "SaaS证书", value: "saas" },
],
}
```
## `a-select`
Source: `packages/ui/certd-server/src/plugins/plugin-admin/plugin-db-backup.ts`
```ts
component: {
name: "a-select",
options: [
{ label: "本地复制", value: "local" },
{ label: "oss上传(推荐)", value: "oss" },
{ label: "ssh上传(请使用oss上传方式)", value: "ssh", disabled: true },
],
placeholder: "",
}
```
## `a-switch`
Source: `packages/ui/certd-server/src/plugins/plugin-acepanel/access.ts`
```ts
component: {
name: "a-switch",
vModel: "checked",
}
```
## `a-textarea`
Source: `packages/ui/certd-server/src/plugins/plugin-admin/plugin-script.ts`
```ts
component: {
name: "a-textarea",
vModel: "value",
rows: 10,
style: "background-color: #000c17;color: #fafafa;",
}
```
## `access-selector`
Source: `packages/ui/certd-server/src/plugins/plugin-acepanel/plugins/plugin-deploy-to-website.ts`
```ts
component: {
name: "access-selector",
type: "acepanel",
}
```
## `api-test`
Source: `packages/ui/certd-server/src/plugins/plugin-51dns/access.ts`
```ts
component: {
name: "api-test",
action: "TestRequest",
}
```
## `cert-info-updater`
Source: `packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/custom/index.ts`
```ts
component: {
name: "cert-info-updater",
vModel: "modelValue",
}
```
## `dns-provider-selector`
Source: `packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/apply.ts`
```ts
component: {
name: "dns-provider-selector",
}
```
## `domain-selector`
Source: `packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/base-convert.ts`
```ts
component: {
name: "domain-selector",
vModel: "value",
mode: "tags",
// open: false,
placeholder: "请输入证书域名/IP,比如:foo.com , *.foo.com , *.sub.foo.com , *.bar.com , 123.123.123.123",
tokenSeparators: [",", " ", "", "、", "|"],
search: true,
pager: true,
}
```
## `domains-verify-plan-editor`
Source: `packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/apply.ts`
```ts
component: {
name: "domains-verify-plan-editor",
}
```
## `email-selector`
Source: `packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/base.ts`
```ts
component: {
name: "email-selector",
vModel: "value",
}
```
## `fs-icon-selector`
Source: `packages/ui/certd-server/src/plugins/plugin-oauth/oidc/plugin-oidc.ts`
```ts
component: {
name: "fs-icon-selector",
vModel: "modelValue",
iconSets: IconSets,
}
```
## `icon-select`
Source: `packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/apply.ts`
```ts
component: {
name: "icon-select",
vModel: "value",
options: [
{ value: "letsencrypt", label: "Let's Encrypt(免费,新手推荐,支持IP证书)", icon: "simple-icons:letsencrypt" },
{ value: "google", label: "Google(免费)", icon: "flat-color-icons:google" },
{ value: "zerossl", label: "ZeroSSL(免费)", icon: "emojione:digit-zero" },
{ value: "litessl", label: "litessl(免费)", icon: "roentgen:free" },
{ value: "sslcom", label: "SSL.com(仅主域名和www免费)", icon: "la:expeditedssl" },
{ value: "letsencrypt_staging", label: "Let's Encrypt测试环境(仅供测试)", icon: "simple-icons:letsencrypt" },
],
}
```
## `input-password`
Source: `packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/base-convert.ts`
```ts
component: {
name: "input-password",
vModel: "value",
}
```
## `notification-selector`
Source: `packages/ui/certd-server/src/plugins/plugin-github/plugins/plugin-check-release.ts`
```ts
component: {
name: "notification-selector",
select: {
mode: "tags",
},
}
```
## `output-selector`
Source: `packages/ui/certd-server/src/plugins/plugin-acepanel/plugins/plugin-deploy-to-website.ts`
```ts
component: {
name: "output-selector",
from: [...CertApplyPluginNames],
}
```
## `pem-input`
Source: `packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/custom/index.ts`
```ts
component: {
name: "pem-input",
vModel: "modelValue",
textarea: {
rows: 4,
placeholder: "-----BEGIN CERTIFICATE-----\n...\n...\n-----END CERTIFICATE-----",
},
}
```
## `refresh-input`
Source: `packages/ui/certd-server/src/plugins/plugin-cert/access/acme-account-access.ts`
```ts
component: {
name: "refresh-input",
action: "GenerateAccount",
buttonText: "生成ACME账号",
successMessage: "ACME账号已生成,请保存授权配置",
type: "textarea",
rows: 4,
}
```
## `remote-auto-complete`
Source: `packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-apig/index.ts`
```ts
component: {
name: "remote-auto-complete",
}
```
## `remote-select`
Source: `packages/ui/certd-server/src/plugins/plugin-nginx-proxy-manager/plugins/plugin-deploy-to-proxy-hosts.ts`
```ts
component: {
name: "remote-select",
vModel: "value",
mode: "tags",
type: "plugin",
action: "onGetProxyHostOptions",
search: true,
pager: false,
single: false,
watches: ["certDomains", "accessId"],
}
```
## `remote-tree-select`
Source: `packages/ui/certd-server/src/plugins/plugin-tencent/plugin/refresh-cert/index.ts`
```ts
component: {
name: "remote-tree-select",
vModel: "value",
action: TencentRefreshCert.prototype.onGetRegionsTree.name,
pager: false,
search: false,
watches: ["certList"],
}
```
## `synology-device-id-getter`
Source: `packages/ui/certd-server/src/plugins/plugin-plus/synology/access.ts`
```ts
component: {
placeholder: "设备ID",
name: "synology-device-id-getter",
type: "access",
typeName: "synology",
}
```
@@ -0,0 +1,25 @@
# 本地历史记录
历史记录和开发临时文件只保存在 Codex/Trae 当前工作区的 `.tmp/online-plugin-dev/` 下,不调用 Certd 后端历史接口。
目录格式:
```text
.tmp/online-plugin-dev/
history/
plugin-12/
2026-08-02T12-30-00-before-edit.yaml
2026-08-02T12-30-00-change.md
work/
plugin-12.yaml
plugin-12-content.js
```
要求:
- 修改前保存完整 YAML。
- 临时 YAML、脚本草稿、调试记录都放到 `.tmp/online-plugin-dev/` 下,不散落到项目目录。
- `change.md` 只记录插件 ID、版本、时间和脱敏修改摘要。
- 不保存 Token、证书、私钥、Cookie、环境变量和真实授权值。
- 恢复历史版本前先备份当前 YAML。
- 恢复后通过 `/scoped/sys/ai/plugin/import` 写回 Certd。
@@ -0,0 +1,55 @@
## 在线插件 YAML
在线插件始终以一个完整 YAML 文档传递、编辑、导入和导出。脚本源码必须放在顶层 `content` 字段中,不要输出独立的 `.ts` 文件,也不要使用 JSON Patch。
常用字段:
```yaml
name: DemoTask
author: demo
title: Demo 任务
desc: 插件说明
icon: clarity:plugin-line
pluginType: task
group: other
version: 1.0.0
input:
cert:
title: 域名证书
required: true
component:
name: cert-select
output: {}
dependPlugins: []
dependPackages: []
default: {}
content: |
const { AbstractTaskPlugin } = await _ctx.import("@certd/pipeline")
const { DemoAccess } = await _ctx.import("/@/plugins/plugin-lib/demo/access/index.js")
_ctx.logger.info("DemoAccess:", DemoAccess)
return class DemoTask extends AbstractTaskPlugin {
async execute() {
this.logger.info("执行成功")
}
}
```
### `content` 规则
- 统一使用 `await _ctx.import(...)` 加载模块。
- 使用 `_ctx.import("/@/...")` 以绝对路径加载 `certd-server/src/` 下的模块,`/@` 代表 `certd-server/src` 根路径。
- 需要确认模块时使用 `_ctx.logger`,插件执行过程使用 `this.logger`
- 最后返回继承目标基类的 class。
- 不使用 `import``export`、装饰器或独立源码文件语法。
- 输入字段在 class 中声明为同名属性,并与 YAML 的 `input` 配置保持一致。
- HTTP 使用 `this.ctx.http`
- 不要使用 `console.log`
- 读取授权使用 `await this.getAccess(accessId)` 或目标基类规定的授权方式。
- 失败时抛出 `Error`,不要吞掉错误。
### 编辑规则
- 修改已有插件时保留 `name``author``pluginType` 和已有兼容字段。
- 只修改需求涉及的字段,避免删除未知的 YAML 字段。
- 脚本过长时仍放在同一个 `content` block scalar 中。
- 提交前检查 YAML 可解析、`content` 非空、版本和插件类型没有被意外修改。
@@ -0,0 +1,59 @@
---
name: certd-online-access-plugin-dev
description: 用于开发 Certd 在线 Access 插件。输出完整 YAMLcontent 中返回继承 BaseAccess 的 class,并在 input 中声明授权字段。
---
# 在线 Access 插件
读取父 Skill 的 `references/online-yaml-format.md`。不要沿用旧版 `@IsAccess``@AccessInput` 装饰器和独立 TypeScript 文件。
## 输出结构
- `pluginType` 使用 `access`
- `input` 中声明用户需要填写的授权字段。
- 敏感字段在 input 中设置加密或密码类组件。
- `content` 中实现授权 class 和 API 方法。
## `content` 模板
```javascript
const { BaseAccess } = await _ctx.import("@certd/pipeline")
return class DemoAccess extends BaseAccess {
demoKeyId
demoKeySecret
async onTestRequest() {
await this.getDomainList({ searchKey: "" })
return "ok"
}
async getDomainList(req) {
this.logger.info("获取域名列表", { searchKey: req.searchKey })
const res = await this.ctx.http.request({
url: "https://api.example.com/domains",
method: "GET",
params: { keyword: req.searchKey },
})
if (res.error) {
throw new Error(`获取域名列表失败: ${res.message}`)
}
return {
total: res.data?.total || 0,
list: res.data?.list || [],
}
}
}
```
## 编写要求
- 统一使用 `await _ctx.import(...)` 加载模块。
- 使用 `_ctx.import("/@/...")` 通过绝对路径加载 `server/src/` 下的模块,`/@` 代表 `server/src` 根路径。
- 需要记录模块加载信息时使用 `_ctx.logger`,访问执行日志使用 `this.logger`
- 返回继承 `BaseAccess` 的 class,不使用装饰器。
- class 属性名必须与 YAML `input` 字段一致。
- 所有敏感授权值只通过 `this` 和 Certd 授权上下文使用,不打印真实值。
- `onTestRequest` 应调用实际 API 方法并在失败时抛出异常。
- 对外 API 方法应统一处理分页、错误和返回字段。
- 使用 `this.logger` 或框架提供的 logger,禁止 `console.log`
@@ -0,0 +1,70 @@
---
name: certd-online-dns-provider-dev
description: 用于开发 Certd 在线 DNS Provider 插件。输出完整 YAMLcontent 中返回继承 AbstractDnsProvider 的 class。
---
# 在线 DNS Provider 插件
读取父 Skill 的 `references/online-yaml-format.md`。不要沿用旧版 `@IsDnsProvider` 装饰器和独立 TypeScript 文件。
## 输出结构
- `pluginType` 使用 `dnsProvider`
- `input` 中配置授权选择、域名或平台所需的参数。
- `content` 中实现创建和删除 DNS 记录的 class。
## `content` 模板
```javascript
const { AbstractDnsProvider } = await _ctx.import("@certd/pipeline")
const { DemoAccess } = await _ctx.import("/@/plugins/plugin-lib/demo/access/index.js")
_ctx.logger.info("DemoAccess:", DemoAccess)
return class DemoDnsProvider extends AbstractDnsProvider {
accessId
async onInstance() {
this.access = await this.getAccess(this.accessId)
}
async createRecord(options) {
const { fullRecord, value, type, domain } = options
this.logger.info("添加 DNS 记录", { fullRecord, type, domain })
const res = await this.ctx.http.request({
url: "https://api.example.com/dns/records",
method: "POST",
data: { fullRecord, value, type, domain },
})
if (res.error) {
throw new Error(`创建 DNS 记录失败: ${res.message}`)
}
return res.data
}
async removeRecord(options) {
const { fullRecord, value, domain } = options.recordReq
const res = await this.ctx.http.request({
url: "https://api.example.com/dns/records",
method: "DELETE",
data: { fullRecord, value, domain },
})
if (res.error) {
this.logger.warn("删除 DNS 记录失败", res.message)
return
}
this.logger.info("删除 DNS 记录成功", fullRecord)
}
}
```
## 编写要求
- 统一使用 `await _ctx.import(...)` 加载模块。
- 使用 `_ctx.import("/@/...")` 通过绝对路径加载 `server/src/` 下的模块,`/@` 代表 `server/src` 根路径。
- 需要记录模块加载信息时使用 `_ctx.logger`
- 返回继承 `AbstractDnsProvider` 的 class。
- `createRecord` 必须返回删除时需要的记录信息。
- `removeRecord` 使用 `options.recordReq``options.recordRes`
- 只处理业务 API 所需的 TXT 记录参数,不在日志中输出授权密钥。
- 网络失败、授权失败和 API 业务失败要有明确日志;创建失败必须抛出异常。
- 保持创建和删除幂等,避免清理失败阻断无关流程。
@@ -0,0 +1,54 @@
---
name: certd-online-task-plugin-dev
description: 用于开发 Certd 在线 Task 插件。输出完整 YAML,脚本源码放在 content 字段中,继承 AbstractTaskPlugin 并返回插件 class。
---
# 在线 Task 插件
读取父 Skill 的 `references/online-yaml-format.md`。在线插件不是原来的装饰器源码文件模式。
## 输出结构
- `pluginType` 使用 `task`
- 保留或填写 `name``author``title``desc``icon``group``version`
- 输入配置放在 YAML 的 `input` 字段。
- 执行脚本放在 YAML 顶层 `content` 字段。
## `content` 模板
```javascript
const { AbstractTaskPlugin } = await _ctx.import("@certd/pipeline")
const { DemoAccess } = await _ctx.import("/@/plugins/plugin-lib/demo/access/index.js")
_ctx.logger.info("DemoAccess:", DemoAccess)
return class DemoTask extends AbstractTaskPlugin {
cert
certDomains
accessId
async execute() {
const access = await this.getAccess(this.accessId)
this.logger.info("开始执行任务", { access })
const res = await this.ctx.http.request({
url: "https://api.example.com",
})
if (res.error) {
throw new Error(`任务执行失败: ${res.message}`)
}
this.logger.info("执行成功")
}
}
```
## 编写要求
- 统一用 `await _ctx.import(...)` 加载模块。
- 使用 `_ctx.import("/@/...")` 通过绝对路径加载 `server/src/` 下的模块,`/@` 代表 `server/src` 根路径。
- 需要记录模块加载信息时使用 `_ctx.logger`
- 返回继承 `AbstractTaskPlugin` 的 class,不写 `export class`
- class 属性名必须对应 `input` 配置的字段名。
-`this.logger` 记录关键步骤。
-`this.ctx.http` 请求远程 API,用 `this.getAccess` 获取授权。
- 外部 API 返回失败或业务失败时抛出异常。
- 对重复执行保持幂等,避免把真实 Token、证书和私钥写入日志。
- 修改完成后把整个 YAML 通过 Certd `/scoped/sys/ai/plugin/import` 保存。
+1
View File
@@ -454,3 +454,4 @@ new TencentDnsProvider();
3. **记录结构**:定义适合对应云平台的记录数据结构,至少包含 id 字段用于删除记录。 3. **记录结构**:定义适合对应云平台的记录数据结构,至少包含 id 字段用于删除记录。
4. **日志输出**:使用 `this.logger` 输出日志,而不是 `console`,参数文本化,不要传对象,否则会输出`[object Object]}` 4. **日志输出**:使用 `this.logger` 输出日志,而不是 `console`,参数文本化,不要传对象,否则会输出`[object Object]}`
5. **错误处理**:API 调用失败时应抛出明确的错误信息。 5. **错误处理**:API 调用失败时应抛出明确的错误信息。
6. **旧版数据兼容**: 新增插件参数,必须要考虑旧版数据兼容,比如新增一个deployType参数,有两种值:`default``custom`,需要在使用时判空,走旧版逻辑。
+3 -2
View File
@@ -281,12 +281,12 @@ export class DemoTest extends AbstractTaskPlugin {
return { return {
value: item.siteName, value: item.siteName,
label: item.siteName, label: item.siteName,
domain: item.siteName, domain: item.siteName, //这里必须要包含domain 否则后面分组时候全部分配到未匹配中
}; };
}); });
//将站点域名名称根据证书域名进行匹配分组,分成匹配的和不匹配的两组选项,返回给前端,供用户选择 //将站点域名名称根据证书域名进行匹配分组,分成匹配的和不匹配的两组选项,返回给前端,供用户选择
return { return {
list: optionsUtils.buildGroupOptions(options, this.certDomains), list: optionsUtils.buildGroupOptions(options, this.certDomains), //分组后的列表
total: siteRes.length, total: siteRes.length,
}; };
} }
@@ -641,6 +641,7 @@ new AliyunOSSDeploy();
4. **日志输出**:使用 `this.logger` 输出日志,而不是 `console`,参数文本化,不要传对象,否则会输出`[object Object]}` 4. **日志输出**:使用 `this.logger` 输出日志,而不是 `console`,参数文本化,不要传对象,否则会输出`[object Object]}`
5. **错误处理**:执行过程中的错误应被捕获并记录。 5. **错误处理**:执行过程中的错误应被捕获并记录。
6. **授权获取**:使用 `this.getAccess(accessId)` 获取授权信息。 6. **授权获取**:使用 `this.getAccess(accessId)` 获取授权信息。
7. **旧版数据兼容** 新增@TaskInput注解的插件参数,必须要考虑旧版数据兼容,比如新增一个deployType参数,有两种值:`default``custom`,需要在使用时判空,走旧版逻辑。
## 部署逻辑注意事项 ## 部署逻辑注意事项
+14
View File
@@ -87,6 +87,20 @@
"plus_use_prod": "false", "plus_use_prod": "false",
"PLUS_SERVER_BASE_URL": "http://127.0.0.1:11007" "PLUS_SERVER_BASE_URL": "http://127.0.0.1:11007"
} }
},
{
"name": "server-local-comm",
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}/packages/ui/certd-server",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev-localcomm"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"env": {
"plus_use_prod": "false",
"PLUS_SERVER_BASE_URL": "http://127.0.0.1:11007"
}
} }
], ],
"compounds": [ "compounds": [
+37 -3
View File
@@ -68,20 +68,24 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
## 通用工作规则 ## 通用工作规则
- 先读本文,再按任务读取具体代码或技能文件。 - 先读本文,再按任务读取具体代码或技能文件。
- 与用户沟通、代码注释、文档、日志和面向用户的界面文案,尽量使用中文;技术专有名词、代码标识符、第三方库名称和命令可保留英文。
- PowerShell 读取中文、Markdown、locale、文档类文件时使用 `Get-Content -Raw -Encoding UTF8`;仍乱码时先执行 `[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()` - 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'` - 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、网络导致安装或测试失败时,停止尝试并说明环境问题。 - 不要主动运行 `pnpm install`;缺依赖、TTY、网络导致安装或测试失败时,停止尝试并说明环境问题。
- 优先沿用现有模块、插件、service、页面模式;不要为形式上的复用制造过度抽象。 - 优先沿用现有模块、插件、service、页面模式;不要为形式上的复用制造过度抽象。
- 代码可读性优先于短写法。复杂条件、三元表达式、链式调用、内联对象和多层 helper 调用要拆成命名清晰的中间变量或小方法。 - 代码可读性优先于短写法。复杂条件、三元表达式、链式调用、内联对象和多层 helper 调用要拆成命名清晰的中间变量或小方法。
- 方法调用链不要直接塞进另一个方法参数;先用有意义的局部变量承接返回值,再传入下一步。 - 方法调用链不要直接塞进另一个方法参数;先用有意义的局部变量承接返回值,再传入下一步。
- 不要在单一表达式内嵌套分支、对象构造与方法调用。优先使用清晰的 `if/else` 分支;仅在确实能降低复杂度时才提取有意义的中间变量,避免为拆分而增加阅读跳转。
- 注释优先使用中文,尤其是业务规则、兼容逻辑、协议细节和隐藏风险;文件已有英文风格或引用外部术语时可保持一致。 - 注释优先使用中文,尤其是业务规则、兼容逻辑、协议细节和隐藏风险;文件已有英文风格或引用外部术语时可保持一致。
- 遵守 DRY 和单一职责;第三次出现的业务规则、字段转换、权限判断、Repository 选择、事务传播、金额计算等逻辑,应优先抽成合适 helper 或 service 方法。 - 遵守 DRY 和单一职责;第三次出现的业务规则、字段转换、权限判断、Repository 选择、事务传播、金额计算等逻辑,应优先抽成合适 helper 或 service 方法。
## 测试与验证 ## 测试与验证
- 务必写单元测试,覆盖主要业务逻辑 - 新功能、缺陷修复和重构遵循 TDD:先编写或更新测试,覆盖正常路径、边界条件和明确的错误处理;先运行测试确认 RED,再实现最小改动,最后运行聚焦测试确认 GREEN
- 实现新功能或修复行为缺陷前,优先补单元测试并先确认红灯,再实现并跑聚焦验证 - 单元测试应优先直接测试原始业务方法,不要为了方便测试而抽取没有业务价值的 helper;数据库、RPC 和其他外部依赖可以使用 mock 隔离
- 确实不适合先写测试时,在回复中说明原因和替代验证方式。 - 确实不适合先写测试时,在回复中说明原因和替代验证方式。
- 后补单元测试时,按正确行为写预期;若红灯需要修改既有实现,先向用户确认这是 bug 还是既有需求,避免未经确认改变行为。 - 后补单元测试时,按正确行为写预期;若红灯需要修改既有实现,先向用户确认这是 bug 还是既有需求,避免未经确认改变行为。
- 后端纯单测放在 `src/**/*.test.ts`,尽量与被测文件相邻;`test:unit` 只跑这些文件,构建/打包应排除 `*.test.ts` - 后端纯单测放在 `src/**/*.test.ts`,尽量与被测文件相邻;`test:unit` 只跑这些文件,构建/打包应排除 `*.test.ts`
@@ -90,6 +94,12 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
- 单包单测优先用 `cd <包目录> && npm run test:unit`,例如 `cd packages\ui\certd-server && npm run test:unit` - 单包单测优先用 `cd <包目录> && npm run test:unit`,例如 `cd packages\ui\certd-server && npm run test:unit`
- 优先对改动包运行聚焦测试或格式化/ESLint;只有跨包影响明显时再考虑更大范围构建。 - 优先对改动包运行聚焦测试或格式化/ESLint;只有跨包影响明显时再考虑更大范围构建。
## 规范回顾
- 每次任务收尾时,回顾用户提出的要求,识别其中可跨任务复用、稳定且不与既有规则冲突的约束。
- 只将经过确认的共性要求精炼后写入本文;一次性任务细节、临时偏好和未经确认的推断不要写入长期规范。
- 更新本文后检查规则是否清晰、可执行,并删除重复或互相矛盾的表述。
## 后端规则 ## 后端规则
@@ -102,7 +112,8 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
- 只有需要事务传播时才定义 `ctx`;普通查询、纯函数和简单私有方法继续使用明确参数。 - 只有需要事务传播时才定义 `ctx`;普通查询、纯函数和简单私有方法继续使用明确参数。
- 需要按事务上下文取 Repository 时,用 `BaseService.getRepo(ctx, EntityClass)` - 需要按事务上下文取 Repository 时,用 `BaseService.getRepo(ctx, EntityClass)`
- 需要“有事务则复用、无事务则开启”时,用 `BaseService.transactionWithCtx(ctx, callback)` - 需要“有事务则复用、无事务则开启”时,用 `BaseService.transactionWithCtx(ctx, callback)`
- 拼接可选 `projectId` 查询条件时,用 `BaseService.buildUserProjectQuery(userId, projectId)`;不要直接写 `{ userId, projectId }` - 基础 CRUD 数据访问优先复用 `BaseService``find``findOne``list``page``update``deleteWhere` 等方法;不要从 `repository.createQueryBuilder()` 开始重复实现完整查询或更新。仅将关键词组合筛选、联表、业务排序等基类无法表达的部分放入 `list/page``buildQuery`
- 拼接可选 `projectId` 查询条件时,**必须**使用 `BaseService.buildUserProjectQuery(userId, projectId)`,禁止直接写 `{ userId, projectId }`。因为 `projectId` 可能为 `null`/`undefined`,直接放入查询会生成错误的 `WHERE projectId = NULL` 条件。
- `ctx` 类型复用 `BaseService` 导出的 `ServiceContext` - `ctx` 类型复用 `BaseService` 导出的 `ServiceContext`
- 新增 service 方法避免与 `BaseService` 方法签名冲突,例如不要用 `delete(id)` 覆盖 `delete(ids, where?)`;改用 `deleteById` 等具体名称。 - 新增 service 方法避免与 `BaseService` 方法签名冲突,例如不要用 `delete(id)` 覆盖 `delete(ids, where?)`;改用 `deleteById` 等具体名称。
@@ -137,6 +148,9 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
- 列表管理、后台管理、记录查询、CRUD 表格页面优先使用 Fast Crud;开发或重构前读 `.trae/skills/fast-crud-page-dev/SKILL.md` - 列表管理、后台管理、记录查询、CRUD 表格页面优先使用 Fast Crud;开发或重构前读 `.trae/skills/fast-crud-page-dev/SKILL.md`
- 只有轻量只读展示、强交互自定义界面或既有页面模式明显不适合 Fast Crud 时,才手写 `a-table` / 自定义列表,并在回复中说明。 - 只有轻量只读展示、强交互自定义界面或既有页面模式明显不适合 Fast Crud 时,才手写 `a-table` / 自定义列表,并在回复中说明。
- 内嵌 Fast Crud 时,外层必须有稳定高度或完整 `flex: 1; min-height: 0` 链路。 - 内嵌 Fast Crud 时,外层必须有稳定高度或完整 `flex: 1; min-height: 0` 链路。
- 前端组件样式统一写在 `<style>` / Less / CSS 文件里,通过样式名映射到元素;尽量不要在元素上直接写 `style`
- 每个组件都要有一个稳定的根样式名,并把组件下方样式全部包在该根样式名内;尽量不要使用 `scoped`
- 可复用的公共样式名放在 `packages/ui/certd-client/src/style` 下维护,优先使用 `cd-` 前缀,避免散落在业务组件里重复定义。
- 后台管理列表展示或筛选用户字段时,优先参考 `packages/ui/certd-client/src/views/sys/suite/user-suite/crud.tsx``userId` 字段模式,用 `table-select` + `/sys/authority/user/getSimpleUserByIds` 字典回显和搜索。 - 后台管理列表展示或筛选用户字段时,优先参考 `packages/ui/certd-client/src/views/sys/suite/user-suite/crud.tsx``userId` 字段模式,用 `table-select` + `/sys/authority/user/getSimpleUserByIds` 字典回显和搜索。
- 对话框里只做确认可用 `Modal.confirm`;有字段输入、表单校验或提交字段时,必须用 `useFormDialog` / `openFormDialog` - 对话框里只做确认可用 `Modal.confirm`;有字段输入、表单校验或提交字段时,必须用 `useFormDialog` / `openFormDialog`
@@ -210,3 +224,23 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
- 后端业务数据、接口、实体、权限、迁移:改 `packages/ui/certd-server/src/modules``src/controller` - 后端业务数据、接口、实体、权限、迁移:改 `packages/ui/certd-server/src/modules``src/controller`
- 表单、列表、插件配置 UI:改 `packages/ui/certd-client/src/views/certd` 及对应 `src/api` - 表单、列表、插件配置 UI:改 `packages/ui/certd-client/src/views/certd` 及对应 `src/api`
## 其他注意事项
### 旧版数据兼容
- 新增插件参数时,必须要考虑旧版数据兼容,比如新增一个deployType参数,有两种值:`default``custom`,需要在使用时判空,走旧版逻辑。
## 前端路由与国际化
- 路由 `meta.title`**i18n 国际化 key**,必须在 `src/locales/langs/zh-CN/``src/locales/langs/en-US/` 对应的模块文件中添加翻译。
- 示例:路由 `title: "certd.auditLog"` 需要在中英 locales 文件中有对应 key`"certd.auditLog": "操作日志"` / `"certd.auditLog": "Audit Log"`)。
- 菜单通过路由自动生成,需设置 `meta.isMenu: true` 才会出现在左侧菜单。
- Plus 版功能菜单需设置 `meta.show: () => { const settingStore = useSettingStore(); return settingStore.isPlus; }`
## 审计日志
- 审计日志是 Plus 版功能,非 Plus 版不会写入。
- Controller 继承 `BaseController`,通过 `this.auditLog({ content: "xxx" })` 记录日志。
- Controller 中的 `@Post("/add", { summary: "xxxx" })`, 这个summary是必须要的,他是日志action字段的来源
- `getAuditType()` 返回类型常量,中间件自动从 ctx.path 判定 scope`/api/sys/` → system,其他 → user)。
- 操作日志有系统级(scope=system)和用户级(scope=user)区分。
+86
View File
@@ -3,6 +3,92 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.42.6](https://github.com/certd/certd/compare/v1.42.5...v1.42.6) (2026-07-18)
### Bug Fixes
* 修复正常批量删除流水线报权限不足的bug ([5b50083](https://github.com/certd/certd/commit/5b500830a122c6c42dab054e57fed509050f94da))
### Performance Improvements
* 优化动态加载依赖镜像地址,多次重试 ([5f53b81](https://github.com/certd/certd/commit/5f53b81c75dd242b4260ac08cae14c6d1a08a883))
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
### Bug Fixes
* 修复上传到cos报runtimeDepsService未初始化的问题 ([167b303](https://github.com/certd/certd/commit/167b303faeca02cc11cf97e4be2a3df914852167))
* 修复dingtalk通知格式没有换行的bug ([7ed1be9](https://github.com/certd/certd/commit/7ed1be994f8b4b74cdeb38743060c912c027248b))
### Performance Improvements
* 给SQLITE_IOERR_WRITE增加友好报错提示,将certd:latest镜像改为certd:slim ([b91c9e4](https://github.com/certd/certd/commit/b91c9e4ea671cb359ef164e27864de1d66cba9d3))
* 优化vke keubconfig获取方式,改成先查询,如果没有再创建临时config ([604fa5b](https://github.com/certd/certd/commit/604fa5be634d099d797bfee5c2b0f26ce0ac8461))
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
### Bug Fixes
* 修复火山引擎查不到自定义源站域名的问题 ([02dabe1](https://github.com/certd/certd/commit/02dabe11db3e9b13ca4621ce9ddd2b808bfca390))
* 修复火山引擎自定义源站域名查询不到的问题 ([e44bf9d](https://github.com/certd/certd/commit/e44bf9d77375d48ac7fd1582e69fae02dfd248fa))
* **pipeline:** 重构运行时依赖加载逻辑,修复火山引擎DNS解析报runtimeDepsService未初始化的bug ([ec69b8f](https://github.com/certd/certd/commit/ec69b8f11bfd4b20991aef74a72a47182ca79a9d))
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
### Bug Fixes
* **volcengine-alb:** 修复火山引擎ALB 默认证书部署类型会部署到扩展证书的问题 ([0a068a2](https://github.com/certd/certd/commit/0a068a274673e9768954e9f7367c267d44f3b530))
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
### Bug Fixes
* 修复火山视频点播源站选择不到自定义源站的bug ([0071bcb](https://github.com/certd/certd/commit/0071bcb0e4dd108c86d7ca01820a9f6e6960e440))
* 修复企业模式下弹出邮箱绑定提醒的问题 ([8d9dad9](https://github.com/certd/certd/commit/8d9dad9c82f6f2fd3ab3040068946a33f37145b1))
* 修复AsiaIsp CDN证书重复情况下部署失败的问题 ([c3d6db3](https://github.com/certd/certd/commit/c3d6db3f1ef2f1c897b7989521fe8809dffaded1))
* 修复cname用阿里云校验时报找不到runtimeDepsService的错误 ([072edd7](https://github.com/certd/certd/commit/072edd7affee424ab3411f4d41d338f084d7cac6))
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
### Bug Fixes
* 修复多域名无法使用passkey登录的bug ([d176f9c](https://github.com/certd/certd/commit/d176f9cc0ebd051a614bfac74d1616d1945fc9a3))
* 修复企业模式下登录报projectId不能为空的问题 ([a65366b](https://github.com/certd/certd/commit/a65366bbe1aadea8baaffbdadab58a5b631d9417))
* **login:** 修复输入法 composing 状态下回车触发提交的问题 ([b74db81](https://github.com/certd/certd/commit/b74db81304bbe68476bbec5ea4307a2264060e92))
### Performance Improvements
* **certd-server:** 使用 jks-go转换jks证书,大幅精简镜像大小 ([c78898e](https://github.com/certd/certd/commit/c78898e4c10dd1701467d2e42e3f72bd8f2a352f))
* **pipeline:** 将默认历史保留条数从30调整为100 ([d3e4677](https://github.com/certd/certd/commit/d3e4677ea4fac8e7533749d7f4187e410489e536))
# [1.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
### Bug Fixes
* 修复jdk证书格式的问题 ([260f5ae](https://github.com/certd/certd/commit/260f5ae777b83493b0c578fe30fd00ec0c873226))
* 修复telegram - 符号转义问题 ([d5882f1](https://github.com/certd/certd/commit/d5882f16bedb09baf09ace92049b02872620f5dc))
* **aliyun:** 修复阿里云CDN/DCDN根据证书自动匹配不到证书的bug ([1ae185d](https://github.com/certd/certd/commit/1ae185d0bc356f4678bc38ca0582ce3396f82ebe))
### Features
* 通过插件配置懒加载依赖,动态加载第三方依赖包,精简安装镜像大小 ([01568ca](https://github.com/certd/certd/commit/01568ca1489069046b5a89ebdd4ced2f7f6ddf93))
### Performance Improvements
* 阿里云ESA证书部署支持SaaS模式 ([82276b5](https://github.com/certd/certd/commit/82276b53a8474a18a3d0237050907c994fc748f0))
* 【破坏性更新】 证书压缩包不再生成文件存储,而是实时打包下载,证书申请插件不再输出certZip ([7cff1a9](https://github.com/certd/certd/commit/7cff1a98424120585205889874b3ef4956a30583))
* 火山引擎点播插件支持部署到自定义源站域名 ([095791c](https://github.com/certd/certd/commit/095791cdc2b7c1f4b913b634643afec5e30fe9b0))
* 基础镜像改成node:22-trixie-slim,对网络兼容性更好 ([c66a2bd](https://github.com/certd/certd/commit/c66a2bd77ab6dbb3e3fe2c00562b66287a9429ea))
* 新增橙域网络(asia-isp) CDN证书部署插件 ([b48831e](https://github.com/certd/certd/commit/b48831e60b0059bef7ef9a34ab61c9dd2f684641))
* 优化阿里云API网关增加翻页查询 ([ed58ae3](https://github.com/certd/certd/commit/ed58ae3c5339e4a0238a92acfe7ea6d2f566ea28))
* 优化用户体验,首次访问时弹出邮箱账号绑定用以初始化账号 ([608cc2a](https://github.com/certd/certd/commit/608cc2a81ff0b4872c9fe11ed9c9c0b4b90a12a3))
* 优化ACME账号字段的选择提示 ([bfd3cac](https://github.com/certd/certd/commit/bfd3cacc687fc5cbc3cb2ca3cadbc140de300dc2))
* 支持全自动匹配部署宝塔网站证书 ([4dff48e](https://github.com/certd/certd/commit/4dff48e807c32a7623ec9206cf39c88e88f89f6a))
* **cert-plugin:** 调整更新天数自动减半逻辑,仅7天ip证书生效,其他情况下不减半 ([56e5524](https://github.com/certd/certd/commit/56e5524a0f4af3645d70bc3b3ec750b45ba8de10))
* dns默认ipv4first ([194463b](https://github.com/certd/certd/commit/194463bea9e797315aa7a724f4b2930701570419))
* **passkey:** passkey支持多域名rpid ([79f6586](https://github.com/certd/certd/commit/79f65868ca0f5162bbc2f935ce89abc28011d816))
* **plugin:** 在线插件编辑支持配置第三方依赖和插件依赖 ([635f069](https://github.com/certd/certd/commit/635f069012d4193cfb7cb051c96e28eec1247ca2))
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14) ## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
### Bug Fixes ### Bug Fixes
+42 -17
View File
@@ -1,4 +1,4 @@
# Certd # Certd
中文 | [English](./README_en.md) 中文 | [English](./README_en.md)
@@ -105,31 +105,56 @@ https://certd.handfree.work/
#### Docker镜像说明: #### Docker镜像说明:
**镜像版本:** ##### 1. 镜像地址格式:
| 版本标签 | 基础系统 | 说明 | ```
| --- | --- | --- | --- | registry.cn-shenzhen.aliyuncs.com/certd/certd:[version-][system-][latest/stable]
| `latest` / `[version]` | Alpine Linux | 默认版本,镜像体积小 | ------------ ↑ 镜像地址 ------------- ↑ 镜像名 -- ↑指定版本- ↑基础系统- ↑最新版本类型
| `slim` / `[version]-slim` | Debian slim | 基于glibcdns解析兼容性好(可能需要配置security_opt -seccomp=unconfined | ```
| `armv7` / `[version]-armv7` | Alpine Linux | ARMv7 架构专用版本 | ##### 2. 版本标签:
**镜像地址** **最新版本标签**
| 镜像仓库 | latest | slim | armv7 | | 版本 | 标签 | 说明 |
| --- | --- | --- | --- | | --- | --- | --- |
| 阿里云 | `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest` | `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:slim` | `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:armv7` | | 最新预览版【默认】 | `certd:latest` | 指向最新开发版本,包含最新功能,但稳定性不如稳定版 |
| Docker Hub | `greper/certd:latest` | `greper/certd:slim` | `greper/certd:armv7` | | 最新稳定版 | `certd:stable` | 指向经过充分测试的生产就绪版本,推荐生产环境使用 |
| GitHub Packages | `ghcr.io/certd/certd:latest` | `ghcr.io/certd/certd:slim` | `ghcr.io/certd/certd:armv7` |
> 带版本号的标签请将 `latest` / `slim` / `armv7` 替换为 `[version]` / `[version]-slim` / `[version]-armv7` **系统分支版本:**
> 根据基础镜像不同,分为如下三个分支版本,没有特殊需求选择默认的即可(他们功能是一样的)
| 系统版本 | 版本标签 | 基础系统 | 说明 | 稳定版标签 |
| --- | --- | --- | --- | --- |
| alpine【默认】 | `certd:latest` | Alpine Linux | 默认版本,镜像体积小 | `certd:stable` |
| slim | `certd:slim` | Debian slim | 基于glibcdns解析兼容性好 | `certd:slim-stable` |
| armv7 | `certd:armv7` | Alpine Linux | ARMv7 架构专用版本 | `certd:armv7-stable` |
##### 2. 镜像地址:
| 镜像仓库 | 最新预览版 | slim | armv7 |
| --- | --- | --- | --- |
| 阿里云【默认】 | `registry.cn-shenzhen.aliyuncs.com/certd/certd:latest` | `certd:slim` | `certd:armv7` |
| Docker Hub | `greper/certd:latest` | `certd:slim` |
| GitHub Packages | `ghcr.io/certd/certd:latest` | `certd:slim` | `certd:armv7` |
> 注意:
> 1. 后面的各个版本省略了镜像地址,使用时需要将镜像地址拼接完整。
> 2. 稳定版在后面加 `-stable` 即可。
> 3. 如需指定具体的版本号,在冒号后面加 `version-`即可,例如 `certd:1.42.1-stable`。
##### 3. 镜像构建说明:
- 镜像构建通过`Actions`自动执行,过程公开透明,请放心使用 - 镜像构建通过`Actions`自动执行,过程公开透明,请放心使用
- [点我查看镜像构建日志](https://github.com/certd/certd/actions/workflows/build-image.yml) - [点我查看预览版构建日志](https://github.com/certd/certd/actions/workflows/release-image.yml)
- [点我查看稳定版发布日志](https://github.com/certd/certd/actions/workflows/stable-release.yml)
![](./docs/images/action/action-build.jpg) ![](./docs/images/action/action-build.jpg)
> 注意 ##### 4. 安全注意事项
>
> - 本应用存储的证书、授权信息等属于高度敏感数据,请做好安全防护 > - 本应用存储的证书、授权信息等属于高度敏感数据,请做好安全防护
> - 请务必使用HTTPS协议访问本应用,避免被中间人攻击 > - 请务必使用HTTPS协议访问本应用,避免被中间人攻击
> - 请务必使用web应用防火墙防护本应用,防止XSS、SQL注入等攻击 > - 请务必使用web应用防火墙防护本应用,防止XSS、SQL注入等攻击
+36 -13
View File
@@ -1,4 +1,4 @@
# Certd # Certd
[中文](./README.md) | English [中文](./README.md) | English
@@ -95,21 +95,44 @@ You can choose one of the following deployment methods based on your needs:
#### Docker Image Information: #### Docker Image Information:
- Domestic Image Addresses: **Release channels:**
- `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest`
- `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:armv7`, `[version]-armv7`
- DockerHub Addresses:
- `https://hub.docker.com/r/greper/certd`
- `greper/certd:latest`
- `greper/certd:armv7`, `greper/certd:[version]-armv7`
- GitHub Packages Addresses:
- `ghcr.io/certd/certd:latest` | Channel | Description |
- `ghcr.io/certd/certd:armv7`, `ghcr.io/certd/certd:[version]-armv7` | --- | --- |
| `stable` / `slim-stable` | **Stable version**, production-ready and fully tested, recommended for production environments |
| `latest` / `slim` / `armv7` | **Preview version**, latest development build with newest features but potentially less stable |
**Image tags:**
| Channel | Tag | Versioned Tag | Base System | Description |
| --- | --- | --- | --- | --- |
| **Stable** | `stable` | `[version]-stable` | Alpine Linux | Recommended for production |
| | `slim-stable` | `[version]-slim-stable` | Debian slim | Better DNS resolution compatibility |
| **Preview** | `latest` | `[version]` | Alpine Linux | Default, small image size |
| | `slim` | `[version]-slim` | Debian slim | Better DNS resolution compatibility |
| | `armv7` | `[version]-armv7` | Alpine Linux | ARMv7 architecture |
**Stable version image addresses:**
| Registry | `stable` | `slim-stable` |
| --- | --- | --- |
| Aliyun | `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:stable` | `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:slim-stable` |
| Docker Hub | `greper/certd:stable` | `greper/certd:slim-stable` |
| GitHub Packages | `ghcr.io/certd/certd:stable` | `ghcr.io/certd/certd:slim-stable` |
**Preview version image addresses:**
| Registry | `latest` | `slim` | `armv7` |
| --- | --- | --- | --- |
| Aliyun | `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest` | `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:slim` | `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:armv7` |
| Docker Hub | `greper/certd:latest` | `greper/certd:slim` | `greper/certd:armv7` |
| GitHub Packages | `ghcr.io/certd/certd:latest` | `ghcr.io/certd/certd:slim` | `ghcr.io/certd/certd:armv7` |
> For versioned tags, replace tag name with `[version]-tag`, e.g. replace `stable` with `[version]-stable`
- Images are built automatically by `Actions`, with a transparent process. Please use them with confidence. - Images are built automatically by `Actions`, with a transparent process. Please use them with confidence.
- [Click here to view image build logs](https://github.com/certd/certd/actions/workflows/build-image.yml) - [Click here to view preview version build logs](https://github.com/certd/certd/actions/workflows/release-image.yml)
- [Click here to view stable version release logs](https://github.com/certd/certd/actions/workflows/stable-release.yml)
![](./docs/images/action/action-build.jpg) ![](./docs/images/action/action-build.jpg)
> Note: > Note:
+5 -1
View File
@@ -2,9 +2,13 @@ version: '3.3' # 兼容旧版docker-compose
services: services:
certd: certd:
# 镜像 # ↓↓↓↓↓ ---- 镜像版本号,建议改成固定版本号,例如:certd:1.29.0 # 镜像 # ↓↓↓↓↓ ---- 镜像版本号,建议改成固定版本号,例如:certd:1.29.0
image: registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest image: registry.cn-shenzhen.aliyuncs.com/certd/certd:latest
# image: ghcr.io/certd/certd:latest # --------- 如果 报镜像not found,可以尝试其他镜像源 # image: ghcr.io/certd/certd:latest # --------- 如果 报镜像not found,可以尝试其他镜像源
# image: greper/certd:latest # image: greper/certd:latest
# --------- 生产建议使用稳定版, latest改成stable即可
# image: registry.cn-shenzhen.aliyuncs.com/certd/certd:stable
# security_opt: # --------- 如果slim镜像下启动报错,尝试去掉这两行注释 # security_opt: # --------- 如果slim镜像下启动报错,尝试去掉这两行注释
# - seccomp=unconfined # 解决slim镜像下WorkerThreadsTaskRunner::DelayedTaskScheduler::Start() 报错问题 # - seccomp=unconfined # 解决slim镜像下WorkerThreadsTaskRunner::DelayedTaskScheduler::Start() 报错问题
container_name: certd # 容器名 container_name: certd # 容器名
+86
View File
@@ -3,6 +3,92 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.42.6](https://github.com/certd/certd/compare/v1.42.5...v1.42.6) (2026-07-18)
### Bug Fixes
* 修复正常批量删除流水线报权限不足的bug ([5b50083](https://github.com/certd/certd/commit/5b500830a122c6c42dab054e57fed509050f94da))
### Performance Improvements
* 优化动态加载依赖镜像地址,多次重试 ([5f53b81](https://github.com/certd/certd/commit/5f53b81c75dd242b4260ac08cae14c6d1a08a883))
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
### Bug Fixes
* 修复上传到cos报runtimeDepsService未初始化的问题 ([167b303](https://github.com/certd/certd/commit/167b303faeca02cc11cf97e4be2a3df914852167))
* 修复dingtalk通知格式没有换行的bug ([7ed1be9](https://github.com/certd/certd/commit/7ed1be994f8b4b74cdeb38743060c912c027248b))
### Performance Improvements
* 给SQLITE_IOERR_WRITE增加友好报错提示,将certd:latest镜像改为certd:slim ([b91c9e4](https://github.com/certd/certd/commit/b91c9e4ea671cb359ef164e27864de1d66cba9d3))
* 优化vke keubconfig获取方式,改成先查询,如果没有再创建临时config ([604fa5b](https://github.com/certd/certd/commit/604fa5be634d099d797bfee5c2b0f26ce0ac8461))
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
### Bug Fixes
* 修复火山引擎查不到自定义源站域名的问题 ([02dabe1](https://github.com/certd/certd/commit/02dabe11db3e9b13ca4621ce9ddd2b808bfca390))
* 修复火山引擎自定义源站域名查询不到的问题 ([e44bf9d](https://github.com/certd/certd/commit/e44bf9d77375d48ac7fd1582e69fae02dfd248fa))
* **pipeline:** 重构运行时依赖加载逻辑,修复火山引擎DNS解析报runtimeDepsService未初始化的bug ([ec69b8f](https://github.com/certd/certd/commit/ec69b8f11bfd4b20991aef74a72a47182ca79a9d))
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
### Bug Fixes
* **volcengine-alb:** 修复火山引擎ALB 默认证书部署类型会部署到扩展证书的问题 ([0a068a2](https://github.com/certd/certd/commit/0a068a274673e9768954e9f7367c267d44f3b530))
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
### Bug Fixes
* 修复火山视频点播源站选择不到自定义源站的bug ([0071bcb](https://github.com/certd/certd/commit/0071bcb0e4dd108c86d7ca01820a9f6e6960e440))
* 修复企业模式下弹出邮箱绑定提醒的问题 ([8d9dad9](https://github.com/certd/certd/commit/8d9dad9c82f6f2fd3ab3040068946a33f37145b1))
* 修复AsiaIsp CDN证书重复情况下部署失败的问题 ([c3d6db3](https://github.com/certd/certd/commit/c3d6db3f1ef2f1c897b7989521fe8809dffaded1))
* 修复cname用阿里云校验时报找不到runtimeDepsService的错误 ([072edd7](https://github.com/certd/certd/commit/072edd7affee424ab3411f4d41d338f084d7cac6))
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
### Bug Fixes
* 修复多域名无法使用passkey登录的bug ([d176f9c](https://github.com/certd/certd/commit/d176f9cc0ebd051a614bfac74d1616d1945fc9a3))
* 修复企业模式下登录报projectId不能为空的问题 ([a65366b](https://github.com/certd/certd/commit/a65366bbe1aadea8baaffbdadab58a5b631d9417))
* **login:** 修复输入法 composing 状态下回车触发提交的问题 ([b74db81](https://github.com/certd/certd/commit/b74db81304bbe68476bbec5ea4307a2264060e92))
### Performance Improvements
* **certd-server:** 使用 jks-go转换jks证书,大幅精简镜像大小 ([c78898e](https://github.com/certd/certd/commit/c78898e4c10dd1701467d2e42e3f72bd8f2a352f))
* **pipeline:** 将默认历史保留条数从30调整为100 ([d3e4677](https://github.com/certd/certd/commit/d3e4677ea4fac8e7533749d7f4187e410489e536))
# [1.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
### Bug Fixes
* 修复jdk证书格式的问题 ([260f5ae](https://github.com/certd/certd/commit/260f5ae777b83493b0c578fe30fd00ec0c873226))
* 修复telegram - 符号转义问题 ([d5882f1](https://github.com/certd/certd/commit/d5882f16bedb09baf09ace92049b02872620f5dc))
* **aliyun:** 修复阿里云CDN/DCDN根据证书自动匹配不到证书的bug ([1ae185d](https://github.com/certd/certd/commit/1ae185d0bc356f4678bc38ca0582ce3396f82ebe))
### Features
* 通过插件配置懒加载依赖,动态加载第三方依赖包,精简安装镜像大小 ([01568ca](https://github.com/certd/certd/commit/01568ca1489069046b5a89ebdd4ced2f7f6ddf93))
### Performance Improvements
* 阿里云ESA证书部署支持SaaS模式 ([82276b5](https://github.com/certd/certd/commit/82276b53a8474a18a3d0237050907c994fc748f0))
* 【破坏性更新】 证书压缩包不再生成文件存储,而是实时打包下载,证书申请插件不再输出certZip ([7cff1a9](https://github.com/certd/certd/commit/7cff1a98424120585205889874b3ef4956a30583))
* 火山引擎点播插件支持部署到自定义源站域名 ([095791c](https://github.com/certd/certd/commit/095791cdc2b7c1f4b913b634643afec5e30fe9b0))
* 基础镜像改成node:22-trixie-slim,对网络兼容性更好 ([c66a2bd](https://github.com/certd/certd/commit/c66a2bd77ab6dbb3e3fe2c00562b66287a9429ea))
* 新增橙域网络(asia-isp) CDN证书部署插件 ([b48831e](https://github.com/certd/certd/commit/b48831e60b0059bef7ef9a34ab61c9dd2f684641))
* 优化阿里云API网关增加翻页查询 ([ed58ae3](https://github.com/certd/certd/commit/ed58ae3c5339e4a0238a92acfe7ea6d2f566ea28))
* 优化用户体验,首次访问时弹出邮箱账号绑定用以初始化账号 ([608cc2a](https://github.com/certd/certd/commit/608cc2a81ff0b4872c9fe11ed9c9c0b4b90a12a3))
* 优化ACME账号字段的选择提示 ([bfd3cac](https://github.com/certd/certd/commit/bfd3cacc687fc5cbc3cb2ca3cadbc140de300dc2))
* 支持全自动匹配部署宝塔网站证书 ([4dff48e](https://github.com/certd/certd/commit/4dff48e807c32a7623ec9206cf39c88e88f89f6a))
* **cert-plugin:** 调整更新天数自动减半逻辑,仅7天ip证书生效,其他情况下不减半 ([56e5524](https://github.com/certd/certd/commit/56e5524a0f4af3645d70bc3b3ec750b45ba8de10))
* dns默认ipv4first ([194463b](https://github.com/certd/certd/commit/194463bea9e797315aa7a724f4b2930701570419))
* **passkey:** passkey支持多域名rpid ([79f6586](https://github.com/certd/certd/commit/79f65868ca0f5162bbc2f935ce89abc28011d816))
* **plugin:** 在线插件编辑支持配置第三方依赖和插件依赖 ([635f069](https://github.com/certd/certd/commit/635f069012d4193cfb7cb051c96e28eec1247ca2))
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14) ## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
### Bug Fixes ### Bug Fixes
+15 -6
View File
@@ -6,13 +6,22 @@
Certd 提供多种 Docker 镜像版本,您可以根据需要选择: Certd 提供多种 Docker 镜像版本,您可以根据需要选择:
| 版本标签 | 基础系统 | 说明 | **最新版本:**
| --- | --- | --- | --- |
| `latest` / `[version]` | Alpine Linux | 默认版本,镜像体积小 |
| `slim` / `[version]-slim` | Debian slim | glibc版本,dns解析兼容性更好(可能需要配置security_opt -seccomp=unconfined|
| `armv7` / `[version]-armv7` | Alpine Linux | ARMv7 架构专用版本 |
> 如果您不确定使用哪个版本,请使用默认的 `latest` 版本。 | 版本 | 标签 | 说明 |
| --- | --- | --- |
| 预览版【默认】 | `certd:latest` | 指向最新开发版本,包含最新功能,但稳定性不如稳定版 |
| 稳定版 | `certd:stable` | 指向经过充分测试的生产就绪版本,推荐生产环境使用 |
**系统版本分支:**
| 分支版本标签 | 基础系统 | 说明 | 指定版本 | 稳定版 |
| --- | --- | --- | --- | --- |
| `certd:latest` 【默认】 | Alpine Linux | 默认版本,镜像体积小 | `certd:[version]` | `certd:[version-]stable` |
| `certd:slim` | Debian slim | glibc版本,dns解析兼容性更好(可能需要配置security_opt -seccomp=unconfined| `certd:[version-]slim` | `certd:[version-]slim-stable` |
| `certd:armv7` | Alpine Linux | ARMv7 架构专用版本 | `certd:[version]-armv7` | `certd:[version-]armv7-ststable` |
> 如果您不确定使用哪个版本,请使用默认的 `certd:latest` 版本。
### 一键脚本安装(推荐) ### 一键脚本安装(推荐)
+19 -18
View File
@@ -1,5 +1,5 @@
# 任务插件 # 任务插件
`133` 款任务插件 `134` 款任务插件
## 1. 证书申请 ## 1. 证书申请
| 序号 | 名称 | 说明 | | 序号 | 名称 | 说明 |
@@ -63,22 +63,23 @@
| 7.| **1Panel-部署面板证书** | 更新1Panel的面板证书 | | 7.| **1Panel-部署面板证书** | 更新1Panel的面板证书 |
| 8.| **1Panel-更新站点证书** | 更新1Panel的站点证书 | | 8.| **1Panel-更新站点证书** | 更新1Panel的站点证书 |
| 9.| **宝塔-删除过期证书** | 删除证书夹中过期证书 | | 9.| **宝塔-删除过期证书** | 删除证书夹中过期证书 |
| 10.| **宝塔-WAF证书部署** | 部署宝塔云WAF/aaWAF | | 10.| **宝塔-全自动部署** | 根据证书域名自动匹配宝塔站点,全自动部署SSL证书。新增加速域名自动感知,自动新增部署 |
| 11.| **宝塔-面板证书部署** | 部署宝塔面板本身的ssl证书 | | 11.| **宝塔-WAF证书部署** | 部署宝塔云WAF/aaWAF |
| 12.| **宝塔win-网站证书部署** | 部署到Windows版宝塔管理的站点的ssl证书 | | 12.| **宝塔-面板证书部署** | 部署宝塔面板本身的ssl证书 |
| 13.| **宝塔-网站证书部署** | 部署宝塔管理的站点的ssl证书,目前支持宝塔网站站点、docker站点等。本插件也支持aaPanel。 | | 13.| **宝塔win-网站证书部署** | 部署到Windows版宝塔管理的站点的ssl证书 |
| 14.| **K8S-Apply自定义yaml** | apply自定义yaml到k8s | | 14.| **宝塔-网站证书部署** | 部署宝塔管理的站点的ssl证书,目前支持宝塔网站站点、docker站点等。本插件也支持aaPanel。 |
| 15.| **K8S-Ingress 证书部署** | 部署证书到k8s的Ingress | | 15.| **K8S-Apply自定义yaml** | apply自定义yaml到k8s |
| 16.| **K8S-部署证书到Secret** | 部署证书到k8s的secret | | 16.| **K8S-Ingress 证书部署** | 部署证书到k8s的Ingress |
| 17.| **lucky-更新Lucky证书** | | | 17.| **K8S-部署证书到Secret** | 部署证书到k8s的secret |
| 18.| **Plesk-部署Plesk网站证书** | | | 18.| **lucky-更新Lucky证书** | |
| 19.| **Plesk-更新证书** | 不会创建新证书记录,直接更新旧的证书 | | 19.| **Plesk-部署Plesk网站证书** | |
| 20.| **雷池-更新证书(支持控制台和防护应用)** | 更新长亭雷池WAF的证书,支持更新控制台和防护应用的证书 | | 20.| **Plesk-更新证书** | 不会创建新证书记录,直接更新旧的证书 |
| 21.| **群晖-部署证书到群晖面板** | Synology,支持6.x以上版本 | | 21.| **雷池-更新证书(支持控制台和防护应用)** | 更新长亭雷池WAF的证书,支持更新控制台和防护应用的证书。 |
| 22.| **群晖-刷新OTP登录有效期** | 群晖登录状态可能30天失效,需要在失效之前登录一次,刷新有效期,您可以将其放在“部署到群晖面板”任务之后 | | 22.| **群晖-部署证书到群晖面板** | Synology,支持6.x以上版本 |
| 23.| **uniCloud-部署到服务空间** | 部署到服务空间 | | 23.| **群晖-刷新OTP登录有效期** | 群晖登录状态可能30天失效,需要在失效之前登录一次,刷新有效期,您可以将其放在“部署到群晖面板”任务之后 |
| 24.| **Proxmox-上传证书到Proxmox** | | | 24.| **uniCloud-部署到服务空间** | 部署到服务空间 |
| 25.| **威联通-部署证书到威联通** | 部署证书到qnap | | 25.| **Proxmox-上传证书到Proxmox** | |
| 26.| **威联通-部署证书到威联通** | 部署证书到qnap |
## 5. 阿里云 ## 5. 阿里云
| 序号 | 名称 | 说明 | | 序号 | 名称 | 说明 |
@@ -91,7 +92,7 @@
| 6.| **阿里云-部署证书至API网关** | 自动部署域名证书至阿里云API网关(APIGateway | | 6.| **阿里云-部署证书至API网关** | 自动部署域名证书至阿里云API网关(APIGateway |
| 7.| **阿里云-部署证书至CDN** | 自动部署域名证书至阿里云CDN | | 7.| **阿里云-部署证书至CDN** | 自动部署域名证书至阿里云CDN |
| 8.| **阿里云-部署证书至DCDN** | 依赖证书申请前置任务,自动部署域名证书至阿里云DCDN | | 8.| **阿里云-部署证书至DCDN** | 依赖证书申请前置任务,自动部署域名证书至阿里云DCDN |
| 9.| **阿里云-部署至ESA** | 部署证书到阿里云ESA(边缘安全加速),自动删除过期证书 | | 9.| **阿里云-部署至ESA** | 部署证书到阿里云ESA(边缘安全加速),支持边缘证书和SaaS证书两种模式 |
| 10.| **阿里云-部署至阿里云FC(3.0)** | 部署证书到阿里云函数计算(FC3.0) | | 10.| **阿里云-部署至阿里云FC(3.0)** | 部署证书到阿里云函数计算(FC3.0) |
| 11.| **阿里云-部署至GA** | 部署证书到阿里云GA(全球加速),支持更新默认证书和扩展证书 | | 11.| **阿里云-部署至GA** | 部署证书到阿里云GA(全球加速),支持更新默认证书和扩展证书 |
| 12.| **阿里云-部署至直播(Live** | 部署证书到阿里云视频直播(Live)域名 | | 12.| **阿里云-部署至直播(Live** | 部署证书到阿里云视频直播(Live)域名 |
+1 -1
View File
@@ -9,5 +9,5 @@
} }
}, },
"npmClient": "pnpm", "npmClient": "pnpm",
"version": "1.41.4" "version": "1.42.6"
} }
+5 -2
View File
@@ -19,7 +19,7 @@
"devb": "lerna run dev-build", "devb": "lerna run dev-build",
"i-all": "lerna link && lerna exec npm install ", "i-all": "lerna link && lerna exec npm install ",
"publish": "pnpm run prepublishOnly2 && lerna publish --force-publish=pro/plus-core --conventional-commits && pnpm run afterpublishOnly ", "publish": "pnpm run prepublishOnly2 && lerna publish --force-publish=pro/plus-core --conventional-commits && pnpm run afterpublishOnly ",
"publish2":" npm run pub_all && pnpm run afterpublishOnly", "publish2": " npm run pub_all && pnpm run afterpublishOnly",
"afterpublishOnly": "pnpm run copylogs && time /t >trigger/build.trigger && git add ./trigger/build.trigger && git commit -m \"build: trigger build image\" && TIMEOUT /T 10 && pnpm run commitAll", "afterpublishOnly": "pnpm run copylogs && time /t >trigger/build.trigger && git add ./trigger/build.trigger && git commit -m \"build: trigger build image\" && TIMEOUT /T 10 && pnpm run commitAll",
"transform-sql": "cd ./packages/ui/certd-server/db/ && node --experimental-json-modules transform.js", "transform-sql": "cd ./packages/ui/certd-server/db/ && node --experimental-json-modules transform.js",
"plugin-doc-gen": "cd ./packages/ui/certd-server/ && pnpm run export-metadata", "plugin-doc-gen": "cd ./packages/ui/certd-server/ && pnpm run export-metadata",
@@ -39,12 +39,15 @@
"test:unit": "cross-env NODE_ENV=unittest pnpm -r --workspace-concurrency=1 run test:unit", "test:unit": "cross-env NODE_ENV=unittest pnpm -r --workspace-concurrency=1 run test:unit",
"pub": "echo 1", "pub": "echo 1",
"dev": "pnpm run -r --parallel compile ", "dev": "pnpm run -r --parallel compile ",
"lint_all": "pnpm run -r --parallel lint ",
"pub_all": "node ./scripts/pub-all.js", "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", "release": "time /t >trigger/release.trigger && git add trigger/release.trigger && git commit -m \"build: release\" && git push",
"publish_to_atomgit": "node --experimental-json-modules ./scripts/publish-atomgit.js", "publish_to_atomgit": "node --experimental-json-modules ./scripts/publish-atomgit.js",
"publish_to_gitee": "node --experimental-json-modules ./scripts/publish-gitee.js", "publish_to_gitee": "node --experimental-json-modules ./scripts/publish-gitee.js",
"publish_to_github": "node --experimental-json-modules ./scripts/publish-github.js", "publish_to_github": "node --experimental-json-modules ./scripts/publish-github.js",
"get_version": "node --experimental-json-modules ./scripts/version.js" "get_version": "node --experimental-json-modules ./scripts/version.js",
"stable": "node ./scripts/stable.js",
"set-release-stable": "node ./scripts/set-release-stable.js"
}, },
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
+30
View File
@@ -3,6 +3,36 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.42.6](https://github.com/publishlab/node-acme-client/compare/v1.42.5...v1.42.6) (2026-07-18)
**Note:** Version bump only for package @certd/acme-client
## [1.42.5](https://github.com/publishlab/node-acme-client/compare/v1.42.4...v1.42.5) (2026-07-15)
**Note:** Version bump only for package @certd/acme-client
## [1.42.4](https://github.com/publishlab/node-acme-client/compare/v1.42.3...v1.42.4) (2026-07-11)
**Note:** Version bump only for package @certd/acme-client
## [1.42.3](https://github.com/publishlab/node-acme-client/compare/v1.42.2...v1.42.3) (2026-07-08)
**Note:** Version bump only for package @certd/acme-client
## [1.42.2](https://github.com/publishlab/node-acme-client/compare/v1.42.1...v1.42.2) (2026-07-07)
**Note:** Version bump only for package @certd/acme-client
## [1.42.1](https://github.com/publishlab/node-acme-client/compare/v1.42.0...v1.42.1) (2026-07-06)
**Note:** Version bump only for package @certd/acme-client
# [1.42.0](https://github.com/publishlab/node-acme-client/compare/v1.41.4...v1.42.0) (2026-07-05)
### Performance Improvements
* 新增橙域网络(asia-isp) CDN证书部署插件 ([b48831e](https://github.com/publishlab/node-acme-client/commit/b48831e60b0059bef7ef9a34ab61c9dd2f684641))
## [1.41.4](https://github.com/publishlab/node-acme-client/compare/v1.41.3...v1.41.4) (2026-06-14) ## [1.41.4](https://github.com/publishlab/node-acme-client/compare/v1.41.3...v1.41.4) (2026-06-14)
**Note:** Version bump only for package @certd/acme-client **Note:** Version bump only for package @certd/acme-client
-518
View File
@@ -1,518 +0,0 @@
## Classes
<dl>
<dt><a href="#AcmeClient">AcmeClient</a></dt>
<dd><p>AcmeClient</p>
</dd>
</dl>
## Objects
<dl>
<dt><a href="#Client">Client</a> : <code>object</code></dt>
<dd><p>ACME client</p>
</dd>
</dl>
<a name="AcmeClient"></a>
## AcmeClient
AcmeClient
**Kind**: global class
* [AcmeClient](#AcmeClient)
* [new AcmeClient(opts)](#new_AcmeClient_new)
* [.getTermsOfServiceUrl()](#AcmeClient+getTermsOfServiceUrl) ⇒ <code>Promise.&lt;(string\|null)&gt;</code>
* [.getAccountUrl()](#AcmeClient+getAccountUrl) ⇒ <code>string</code>
* [.createAccount([data])](#AcmeClient+createAccount) ⇒ <code>Promise.&lt;object&gt;</code>
* [.updateAccount([data])](#AcmeClient+updateAccount) ⇒ <code>Promise.&lt;object&gt;</code>
* [.updateAccountKey(newAccountKey, [data])](#AcmeClient+updateAccountKey) ⇒ <code>Promise.&lt;object&gt;</code>
* [.createOrder(data)](#AcmeClient+createOrder) ⇒ <code>Promise.&lt;object&gt;</code>
* [.getOrder(order)](#AcmeClient+getOrder) ⇒ <code>Promise.&lt;object&gt;</code>
* [.finalizeOrder(order, csr)](#AcmeClient+finalizeOrder) ⇒ <code>Promise.&lt;object&gt;</code>
* [.getAuthorizations(order)](#AcmeClient+getAuthorizations) ⇒ <code>Promise.&lt;Array.&lt;object&gt;&gt;</code>
* [.deactivateAuthorization(authz)](#AcmeClient+deactivateAuthorization) ⇒ <code>Promise.&lt;object&gt;</code>
* [.getChallengeKeyAuthorization(challenge)](#AcmeClient+getChallengeKeyAuthorization) ⇒ <code>Promise.&lt;string&gt;</code>
* [.verifyChallenge(authz, challenge)](#AcmeClient+verifyChallenge) ⇒ <code>Promise</code>
* [.completeChallenge(challenge)](#AcmeClient+completeChallenge) ⇒ <code>Promise.&lt;object&gt;</code>
* [.waitForValidStatus(item)](#AcmeClient+waitForValidStatus) ⇒ <code>Promise.&lt;object&gt;</code>
* [.getCertificate(order, [preferredChain])](#AcmeClient+getCertificate) ⇒ <code>Promise.&lt;string&gt;</code>
* [.revokeCertificate(cert, [data])](#AcmeClient+revokeCertificate) ⇒ <code>Promise</code>
* [.auto(opts)](#AcmeClient+auto) ⇒ <code>Promise.&lt;string&gt;</code>
<a name="new_AcmeClient_new"></a>
### new AcmeClient(opts)
| Param | Type | Description |
| --- | --- | --- |
| opts | <code>object</code> | |
| opts.directoryUrl | <code>string</code> | ACME directory URL |
| opts.accountKey | <code>buffer</code> \| <code>string</code> | PEM encoded account private key |
| [opts.accountUrl] | <code>string</code> | Account URL, default: `null` |
| [opts.externalAccountBinding] | <code>object</code> | |
| [opts.externalAccountBinding.kid] | <code>string</code> | External account binding KID |
| [opts.externalAccountBinding.hmacKey] | <code>string</code> | External account binding HMAC key |
| [opts.backoffAttempts] | <code>number</code> | Maximum number of backoff attempts, default: `10` |
| [opts.backoffMin] | <code>number</code> | Minimum backoff attempt delay in milliseconds, default: `5000` |
| [opts.backoffMax] | <code>number</code> | Maximum backoff attempt delay in milliseconds, default: `30000` |
**Example**
Create ACME client instance
```js
const client = new acme.Client({
directoryUrl: acme.directory.letsencrypt.staging,
accountKey: 'Private key goes here',
});
```
**Example**
Create ACME client instance
```js
const client = new acme.Client({
directoryUrl: acme.directory.letsencrypt.staging,
accountKey: 'Private key goes here',
accountUrl: 'Optional account URL goes here',
backoffAttempts: 10,
backoffMin: 5000,
backoffMax: 30000,
});
```
**Example**
Create ACME client with external account binding
```js
const client = new acme.Client({
directoryUrl: 'https://acme-provider.example.com/directory-url',
accountKey: 'Private key goes here',
externalAccountBinding: {
kid: 'YOUR-EAB-KID',
hmacKey: 'YOUR-EAB-HMAC-KEY',
},
});
```
<a name="AcmeClient+getTermsOfServiceUrl"></a>
### acmeClient.getTermsOfServiceUrl() ⇒ <code>Promise.&lt;(string\|null)&gt;</code>
Get Terms of Service URL if available
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;(string\|null)&gt;</code> - ToS URL
**Example**
Get Terms of Service URL
```js
const termsOfService = client.getTermsOfServiceUrl();
if (!termsOfService) {
// CA did not provide Terms of Service
}
```
<a name="AcmeClient+getAccountUrl"></a>
### acmeClient.getAccountUrl() ⇒ <code>string</code>
Get current account URL
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>string</code> - Account URL
**Throws**:
- <code>Error</code> No account URL found
**Example**
Get current account URL
```js
try {
const accountUrl = client.getAccountUrl();
}
catch (e) {
// No account URL exists, need to create account first
}
```
<a name="AcmeClient+createAccount"></a>
### acmeClient.createAccount([data]) ⇒ <code>Promise.&lt;object&gt;</code>
Create a new account
https://datatracker.ietf.org/doc/html/rfc8555#section-7.3
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;object&gt;</code> - Account
| Param | Type | Description |
| --- | --- | --- |
| [data] | <code>object</code> | Request data |
**Example**
Create a new account
```js
const account = await client.createAccount({
termsOfServiceAgreed: true,
});
```
**Example**
Create a new account with contact info
```js
const account = await client.createAccount({
termsOfServiceAgreed: true,
contact: ['mailto:test@example.com'],
});
```
<a name="AcmeClient+updateAccount"></a>
### acmeClient.updateAccount([data]) ⇒ <code>Promise.&lt;object&gt;</code>
Update existing account
https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.2
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;object&gt;</code> - Account
| Param | Type | Description |
| --- | --- | --- |
| [data] | <code>object</code> | Request data |
**Example**
Update existing account
```js
const account = await client.updateAccount({
contact: ['mailto:foo@example.com'],
});
```
<a name="AcmeClient+updateAccountKey"></a>
### acmeClient.updateAccountKey(newAccountKey, [data]) ⇒ <code>Promise.&lt;object&gt;</code>
Update account private key
https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.5
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;object&gt;</code> - Account
| Param | Type | Description |
| --- | --- | --- |
| newAccountKey | <code>buffer</code> \| <code>string</code> | New PEM encoded private key |
| [data] | <code>object</code> | Additional request data |
**Example**
Update account private key
```js
const newAccountKey = 'New private key goes here';
const result = await client.updateAccountKey(newAccountKey);
```
<a name="AcmeClient+createOrder"></a>
### acmeClient.createOrder(data) ⇒ <code>Promise.&lt;object&gt;</code>
Create a new order
https://datatracker.ietf.org/doc/html/rfc8555#section-7.4
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;object&gt;</code> - Order
| Param | Type | Description |
| --- | --- | --- |
| data | <code>object</code> | Request data |
**Example**
Create a new order
```js
const order = await client.createOrder({
identifiers: [
{ type: 'dns', value: 'example.com' },
{ type: 'dns', value: 'test.example.com' },
],
});
```
<a name="AcmeClient+getOrder"></a>
### acmeClient.getOrder(order) ⇒ <code>Promise.&lt;object&gt;</code>
Refresh order object from CA
https://datatracker.ietf.org/doc/html/rfc8555#section-7.4
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;object&gt;</code> - Order
| Param | Type | Description |
| --- | --- | --- |
| order | <code>object</code> | Order object |
**Example**
```js
const order = { ... }; // Previously created order object
const result = await client.getOrder(order);
```
<a name="AcmeClient+finalizeOrder"></a>
### acmeClient.finalizeOrder(order, csr) ⇒ <code>Promise.&lt;object&gt;</code>
Finalize order
https://datatracker.ietf.org/doc/html/rfc8555#section-7.4
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;object&gt;</code> - Order
| Param | Type | Description |
| --- | --- | --- |
| order | <code>object</code> | Order object |
| csr | <code>buffer</code> \| <code>string</code> | PEM encoded Certificate Signing Request |
**Example**
Finalize order
```js
const order = { ... }; // Previously created order object
const csr = { ... }; // Previously created Certificate Signing Request
const result = await client.finalizeOrder(order, csr);
```
<a name="AcmeClient+getAuthorizations"></a>
### acmeClient.getAuthorizations(order) ⇒ <code>Promise.&lt;Array.&lt;object&gt;&gt;</code>
Get identifier authorizations from order
https://datatracker.ietf.org/doc/html/rfc8555#section-7.5
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;Array.&lt;object&gt;&gt;</code> - Authorizations
| Param | Type | Description |
| --- | --- | --- |
| order | <code>object</code> | Order |
**Example**
Get identifier authorizations
```js
const order = { ... }; // Previously created order object
const authorizations = await client.getAuthorizations(order);
authorizations.forEach((authz) => {
const { challenges } = authz;
});
```
<a name="AcmeClient+deactivateAuthorization"></a>
### acmeClient.deactivateAuthorization(authz) ⇒ <code>Promise.&lt;object&gt;</code>
Deactivate identifier authorization
https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.2
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;object&gt;</code> - Authorization
| Param | Type | Description |
| --- | --- | --- |
| authz | <code>object</code> | Identifier authorization |
**Example**
Deactivate identifier authorization
```js
const authz = { ... }; // Identifier authorization resolved from previously created order
const result = await client.deactivateAuthorization(authz);
```
<a name="AcmeClient+getChallengeKeyAuthorization"></a>
### acmeClient.getChallengeKeyAuthorization(challenge) ⇒ <code>Promise.&lt;string&gt;</code>
Get key authorization for ACME challenge
https://datatracker.ietf.org/doc/html/rfc8555#section-8.1
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;string&gt;</code> - Key authorization
| Param | Type | Description |
| --- | --- | --- |
| challenge | <code>object</code> | Challenge object returned by API |
**Example**
Get challenge key authorization
```js
const challenge = { ... }; // Challenge from previously resolved identifier authorization
const key = await client.getChallengeKeyAuthorization(challenge);
// Write key somewhere to satisfy challenge
```
<a name="AcmeClient+verifyChallenge"></a>
### acmeClient.verifyChallenge(authz, challenge) ⇒ <code>Promise</code>
Verify that ACME challenge is satisfied
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
| Param | Type | Description |
| --- | --- | --- |
| authz | <code>object</code> | Identifier authorization |
| challenge | <code>object</code> | Authorization challenge |
**Example**
Verify satisfied ACME challenge
```js
const authz = { ... }; // Identifier authorization
const challenge = { ... }; // Satisfied challenge
await client.verifyChallenge(authz, challenge);
```
<a name="AcmeClient+completeChallenge"></a>
### acmeClient.completeChallenge(challenge) ⇒ <code>Promise.&lt;object&gt;</code>
Notify CA that challenge has been completed
https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.1
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;object&gt;</code> - Challenge
| Param | Type | Description |
| --- | --- | --- |
| challenge | <code>object</code> | Challenge object returned by API |
**Example**
Notify CA that challenge has been completed
```js
const challenge = { ... }; // Satisfied challenge
const result = await client.completeChallenge(challenge);
```
<a name="AcmeClient+waitForValidStatus"></a>
### acmeClient.waitForValidStatus(item) ⇒ <code>Promise.&lt;object&gt;</code>
Wait for ACME provider to verify status on a order, authorization or challenge
https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.1
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;object&gt;</code> - Valid order, authorization or challenge
| Param | Type | Description |
| --- | --- | --- |
| item | <code>object</code> | An order, authorization or challenge object |
**Example**
Wait for valid challenge status
```js
const challenge = { ... };
await client.waitForValidStatus(challenge);
```
**Example**
Wait for valid authorization status
```js
const authz = { ... };
await client.waitForValidStatus(authz);
```
**Example**
Wait for valid order status
```js
const order = { ... };
await client.waitForValidStatus(order);
```
<a name="AcmeClient+getCertificate"></a>
### acmeClient.getCertificate(order, [preferredChain]) ⇒ <code>Promise.&lt;string&gt;</code>
Get certificate from ACME order
https://datatracker.ietf.org/doc/html/rfc8555#section-7.4.2
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;string&gt;</code> - Certificate
| Param | Type | Default | Description |
| --- | --- | --- | --- |
| order | <code>object</code> | | Order object |
| [preferredChain] | <code>string</code> | <code>null</code> | Indicate which certificate chain is preferred if a CA offers multiple, by exact issuer common name, default: `null` |
**Example**
Get certificate
```js
const order = { ... }; // Previously created order
const certificate = await client.getCertificate(order);
```
**Example**
Get certificate with preferred chain
```js
const order = { ... }; // Previously created order
const certificate = await client.getCertificate(order, 'DST Root CA X3');
```
<a name="AcmeClient+revokeCertificate"></a>
### acmeClient.revokeCertificate(cert, [data]) ⇒ <code>Promise</code>
Revoke certificate
https://datatracker.ietf.org/doc/html/rfc8555#section-7.6
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
| Param | Type | Description |
| --- | --- | --- |
| cert | <code>buffer</code> \| <code>string</code> | PEM encoded certificate |
| [data] | <code>object</code> | Additional request data |
**Example**
Revoke certificate
```js
const certificate = { ... }; // Previously created certificate
const result = await client.revokeCertificate(certificate);
```
**Example**
Revoke certificate with reason
```js
const certificate = { ... }; // Previously created certificate
const result = await client.revokeCertificate(certificate, {
reason: 4,
});
```
<a name="AcmeClient+auto"></a>
### acmeClient.auto(opts) ⇒ <code>Promise.&lt;string&gt;</code>
Auto mode
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;string&gt;</code> - Certificate
| Param | Type | Description |
| --- | --- | --- |
| opts | <code>object</code> | |
| opts.csr | <code>buffer</code> \| <code>string</code> | Certificate Signing Request |
| opts.challengeCreateFn | <code>function</code> | Function returning Promise triggered before completing ACME challenge |
| opts.challengeRemoveFn | <code>function</code> | Function returning Promise triggered after completing ACME challenge |
| [opts.email] | <code>string</code> | Account email address |
| [opts.termsOfServiceAgreed] | <code>boolean</code> | Agree to Terms of Service, default: `false` |
| [opts.skipChallengeVerification] | <code>boolean</code> | Skip internal challenge verification before notifying ACME provider, default: `false` |
| [opts.challengePriority] | <code>Array.&lt;string&gt;</code> | Array defining challenge type priority, default: `['http-01', 'dns-01']` |
| [opts.preferredChain] | <code>string</code> | Indicate which certificate chain is preferred if a CA offers multiple, by exact issuer common name, default: `null` |
**Example**
Order a certificate using auto mode
```js
const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
altNames: ['test.example.com'],
});
const certificate = await client.auto({
csr: certificateRequest,
email: 'test@example.com',
termsOfServiceAgreed: true,
challengeCreateFn: async (authz, challenge, keyAuthorization) => {
// Satisfy challenge here
},
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
// Clean up challenge here
},
});
```
**Example**
Order a certificate using auto mode with preferred chain
```js
const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
altNames: ['test.example.com'],
});
const certificate = await client.auto({
csr: certificateRequest,
email: 'test@example.com',
termsOfServiceAgreed: true,
preferredChain: 'DST Root CA X3',
challengeCreateFn: async () => {},
challengeRemoveFn: async () => {},
});
```
<a name="Client"></a>
## Client : <code>object</code>
ACME client
**Kind**: global namespace
+5 -6
View File
@@ -3,7 +3,7 @@
"description": "Simple and unopinionated ACME client", "description": "Simple and unopinionated ACME client",
"private": false, "private": false,
"author": "nmorsman", "author": "nmorsman",
"version": "1.41.4", "version": "1.42.6",
"type": "module", "type": "module",
"module": "./dist/index.js", "module": "./dist/index.js",
"main": "./dist/index.js", "main": "./dist/index.js",
@@ -18,7 +18,7 @@
"types" "types"
], ],
"dependencies": { "dependencies": {
"@certd/basic": "^1.41.4", "@certd/basic": "^1.42.6",
"@peculiar/x509": "^1.11.0", "@peculiar/x509": "^1.11.0",
"asn1js": "^3.0.5", "asn1js": "^3.0.5",
"axios": "^1.9.0", "axios": "^1.9.0",
@@ -50,10 +50,9 @@
"scripts": { "scripts": {
"before-build": "node -e \"const fs=require('fs');fs.rmSync('dist',{recursive:true,force:true});fs.rmSync('tsconfig.tsbuildinfo',{force:true});\"", "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", "build": "npm run before-build && tsc -p tsconfig.build.json --skipLibCheck",
"build-docs": "jsdoc2md dist/client.js > docs/client.md && jsdoc2md dist/crypto/index.js > docs/crypto.md && jsdoc2md dist/crypto/forge.js > docs/forge.md", "lint": "eslint --fix \"src/**/*.ts\" \"types/**/*.ts\"",
"lint": "eslint \"src/**/*.ts\" \"types/**/*.ts\"",
"lint-types": "tsd --files \"types/index.test-d.ts\"", "lint-types": "tsd --files \"types/index.test-d.ts\"",
"prepublishOnly": "npm run build && npm run build-docs", "prepublishOnly": "npm run build",
"test": "mocha -t 60000 \"test/setup.js\" \"test/**/*.spec.js\"", "test": "mocha -t 60000 \"test/setup.js\" \"test/**/*.spec.js\"",
"before-test:unit": "node -e \"const fs=require('fs');fs.rmSync('dist-test',{recursive:true,force:true});fs.rmSync('tsconfig.test.tsbuildinfo',{force:true});\"", "before-test:unit": "node -e \"const fs=require('fs');fs.rmSync('dist-test',{recursive:true,force:true});fs.rmSync('tsconfig.test.tsbuildinfo',{force:true});\"",
"test:unit": "cross-env NODE_ENV=unittest npm run before-test:unit && cross-env NODE_ENV=unittest tsc -p tsconfig.test.json --skipLibCheck && cross-env NODE_ENV=unittest mocha -t 60000 \"dist-test/**/*.test.js\"", "test:unit": "cross-env NODE_ENV=unittest npm run before-test:unit && cross-env NODE_ENV=unittest tsc -p tsconfig.test.json --skipLibCheck && cross-env NODE_ENV=unittest mocha -t 60000 \"dist-test/**/*.test.js\"",
@@ -76,5 +75,5 @@
"bugs": { "bugs": {
"url": "https://github.com/publishlab/node-acme-client/issues" "url": "https://github.com/publishlab/node-acme-client/issues"
}, },
"gitHead": "bc731e4fb119787930e816a7d57c808b1b5cd66a" "gitHead": "246ee83015bf5589adc2a5fa3d1388c8d9a2a252"
} }
+28
View File
@@ -3,6 +3,34 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.42.6](https://github.com/certd/certd/compare/v1.42.5...v1.42.6) (2026-07-18)
**Note:** Version bump only for package @certd/basic
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
**Note:** Version bump only for package @certd/basic
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
**Note:** Version bump only for package @certd/basic
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
**Note:** Version bump only for package @certd/basic
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
**Note:** Version bump only for package @certd/basic
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
**Note:** Version bump only for package @certd/basic
# [1.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
**Note:** Version bump only for package @certd/basic
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14) ## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
### Bug Fixes ### Bug Fixes
+1 -1
View File
@@ -1 +1 @@
21:25 01:12
+3 -3
View File
@@ -1,7 +1,7 @@
{ {
"name": "@certd/basic", "name": "@certd/basic",
"private": false, "private": false,
"version": "1.41.4", "version": "1.42.6",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
@@ -17,7 +17,7 @@
"pub": "npm publish", "pub": "npm publish",
"compile": "tsc --skipLibCheck --watch", "compile": "tsc --skipLibCheck --watch",
"format": "prettier --write src", "format": "prettier --write src",
"lint": "eslint --fix" "lint": "eslint --fix --ext .ts src"
}, },
"dependencies": { "dependencies": {
"async-lock": "^1.4.1", "async-lock": "^1.4.1",
@@ -54,5 +54,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "bc731e4fb119787930e816a7d57c808b1b5cd66a" "gitHead": "246ee83015bf5589adc2a5fa3d1388c8d9a2a252"
} }
+6 -3
View File
@@ -11,9 +11,12 @@ export class LocalCache<V = any> {
cache: Map<string, { value: V; expiresAt: number }>; cache: Map<string, { value: V; expiresAt: number }>;
constructor(opts: { clearInterval?: number } = {}) { constructor(opts: { clearInterval?: number } = {}) {
this.cache = new Map(); this.cache = new Map();
const intervalId = setInterval(() => { const intervalId = setInterval(
this.clearExpires(); () => {
}, opts.clearInterval ?? 5 * 60 * 1000); this.clearExpires();
},
opts.clearInterval ?? 5 * 60 * 1000
);
intervalId.unref?.(); intervalId.unref?.();
} }
+2 -2
View File
@@ -1,4 +1,4 @@
export function isDev() { export function isDev() {
const nodeEnv = process.env.NODE_ENV || 'dev'; const nodeEnv = process.env.NODE_ENV || "dev";
return nodeEnv === 'development' || nodeEnv.includes('local') || nodeEnv.startsWith('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) { function getFileRootDir(rootDir?: string) {
if (rootDir == null) { if (rootDir == null) {
const userHome = process.env.HOME || process.env.USERPROFILE; const userHome = process.env.HOME || process.env.USERPROFILE;
rootDir = userHome + '/.certd/storage/'; rootDir = userHome + "/.certd/storage/";
} }
if (!fs.existsSync(rootDir)) { if (!fs.existsSync(rootDir)) {
+1 -1
View File
@@ -1,2 +1,2 @@
import mitt from 'mitt'; import mitt from "mitt";
export const mitter = mitt(); export const mitter = mitt();
+40
View File
@@ -3,6 +3,46 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.42.6](https://github.com/certd/certd/compare/v1.42.5...v1.42.6) (2026-07-18)
### Performance Improvements
* 优化动态加载依赖镜像地址,多次重试 ([5f53b81](https://github.com/certd/certd/commit/5f53b81c75dd242b4260ac08cae14c6d1a08a883))
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
### Bug Fixes
* 修复上传到cos报runtimeDepsService未初始化的问题 ([167b303](https://github.com/certd/certd/commit/167b303faeca02cc11cf97e4be2a3df914852167))
### Performance Improvements
* 优化vke keubconfig获取方式,改成先查询,如果没有再创建临时config ([604fa5b](https://github.com/certd/certd/commit/604fa5be634d099d797bfee5c2b0f26ce0ac8461))
## [1.42.4](https://github.com/certd/certd/compare/v1.42.3...v1.42.4) (2026-07-11)
### Bug Fixes
* **pipeline:** 重构运行时依赖加载逻辑,修复火山引擎DNS解析报runtimeDepsService未初始化的bug ([ec69b8f](https://github.com/certd/certd/commit/ec69b8f11bfd4b20991aef74a72a47182ca79a9d))
## [1.42.3](https://github.com/certd/certd/compare/v1.42.2...v1.42.3) (2026-07-08)
**Note:** Version bump only for package @certd/pipeline
## [1.42.2](https://github.com/certd/certd/compare/v1.42.1...v1.42.2) (2026-07-07)
**Note:** Version bump only for package @certd/pipeline
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
**Note:** Version bump only for package @certd/pipeline
# [1.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
### Features
* 通过插件配置懒加载依赖,动态加载第三方依赖包,精简安装镜像大小 ([01568ca](https://github.com/certd/certd/commit/01568ca1489069046b5a89ebdd4ced2f7f6ddf93))
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14) ## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
**Note:** Version bump only for package @certd/pipeline **Note:** Version bump only for package @certd/pipeline
+5 -5
View File
@@ -1,7 +1,7 @@
{ {
"name": "@certd/pipeline", "name": "@certd/pipeline",
"private": false, "private": false,
"version": "1.41.4", "version": "1.42.6",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
@@ -18,11 +18,11 @@
"pub": "npm publish", "pub": "npm publish",
"compile": "tsc --skipLibCheck --watch", "compile": "tsc --skipLibCheck --watch",
"format": "prettier --write src", "format": "prettier --write src",
"lint": "eslint --fix" "lint": "eslint --fix --ext .ts src"
}, },
"dependencies": { "dependencies": {
"@certd/basic": "^1.41.4", "@certd/basic": "^1.42.6",
"@certd/plus-core": "^1.41.4", "@certd/plus-core": "^1.42.6",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"reflect-metadata": "^0.2.2" "reflect-metadata": "^0.2.2"
@@ -51,5 +51,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "bc731e4fb119787930e816a7d57c808b1b5cd66a" "gitHead": "246ee83015bf5589adc2a5fa3d1388c8d9a2a252"
} }
+2 -12
View File
@@ -3,7 +3,7 @@ import { FormItemProps } from "../dt/index.js";
import { HttpClient, ILogger, utils } from "@certd/basic"; import { HttpClient, ILogger, utils } from "@certd/basic";
import * as _ from "lodash-es"; import * as _ from "lodash-es";
import { PluginRequestHandleReq } from "../plugin/index.js"; import { PluginRequestHandleReq } from "../plugin/index.js";
import { IRuntimeDepsService, IServiceGetter } from "../service/index.js"; import { IServiceGetter, getRuntimeDepsService } from "../service/index.js";
// export type AccessRequestHandleReqInput<T = any> = { // export type AccessRequestHandleReqInput<T = any> = {
// id?: number; // id?: number;
@@ -48,23 +48,13 @@ export type AccessContext = {
export abstract class BaseAccess implements IAccess { export abstract class BaseAccess implements IAccess {
ctx!: AccessContext; ctx!: AccessContext;
runtimeDepsService?: IRuntimeDepsService;
async importRuntime(specifier: string) { async importRuntime(specifier: string) {
if (!this.runtimeDepsService) { return await getRuntimeDepsService().importRuntime(specifier, this.ctx.logger);
throw new Error("runtimeDepsService 未初始化");
}
return await this.runtimeDepsService.importRuntime(specifier, this.ctx.logger);
} }
async setCtx(ctx: AccessContext) { async setCtx(ctx: AccessContext) {
this.ctx = ctx; this.ctx = ctx;
if (!this.runtimeDepsService && this.ctx.serviceGetter) {
this.runtimeDepsService = await this.ctx.serviceGetter.get("runtimeDepsService");
}
if (this.runtimeDepsService && this.ctx.define?.name) {
await this.runtimeDepsService.ensureRuntimeDependencies({ pluginKeys: `access:${this.ctx.define.name}`, logger: this.ctx.logger });
}
} }
async onRequest(req: AccessRequestHandleReq) { async onRequest(req: AccessRequestHandleReq) {
@@ -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); const register = accessRegistry.get(type);
if (register == null) { if (register == null) {
throw new Error(`access ${type} not found`); throw new Error(`access ${type} not found`);
+6 -1
View File
@@ -389,6 +389,9 @@ export class Executor {
}; };
await instance.setCtx(taskCtx); await instance.setCtx(taskCtx);
if (!(instance instanceof AbstractTaskPlugin)) {
throw new Error(`插件类型错误:${step.type}不是AbstractTaskPlugin的实例`);
}
await instance.onInstance(); await instance.onInstance();
const result = await instance.execute(); const result = await instance.execute();
//执行结果处理 //执行结果处理
@@ -398,6 +401,7 @@ export class Executor {
} }
//输出上下文变量到output context //输出上下文变量到output context
forEach(define.output, (item: any, key: any) => { forEach(define.output, (item: any, key: any) => {
// @ts-ignore
step.status!.output[key] = instance[key]; step.status!.output[key] = instance[key];
// const stepOutputKey = `step.${step.id}.${key}`; // const stepOutputKey = `step.${step.id}.${key}`;
// this.runtime.context[stepOutputKey] = instance[key]; // this.runtime.context[stepOutputKey] = instance[key];
@@ -411,7 +415,8 @@ export class Executor {
merge(vars, instance._result.pipelineVars); merge(vars, instance._result.pipelineVars);
await this.pipelineContext.setObj("vars", vars); await this.pipelineContext.setObj("vars", vars);
} }
if (Object.keys(instance._result.pipelinePrivateVars).length > 0) { // @ts-ignore
if (Object.keys(instance._result?.pipelinePrivateVars).length > 0) {
// 判断 pipelineVars 有值时更新 // 判断 pipelineVars 有值时更新
let vars = await this.pipelineContext.getObj("privateVars"); let vars = await this.pipelineContext.getObj("privateVars");
vars = vars || {}; vars = vars || {};
+6 -12
View File
@@ -3,7 +3,7 @@ import { Registrable } from "../registry/index.js";
import { FormItemProps, HistoryResult, Pipeline } from "../dt/index.js"; import { FormItemProps, HistoryResult, Pipeline } from "../dt/index.js";
import { HttpClient, ILogger, utils } from "@certd/basic"; import { HttpClient, ILogger, utils } from "@certd/basic";
import * as _ from "lodash-es"; import * as _ from "lodash-es";
import { IEmailService, IRuntimeDepsService, IServiceGetter } from "../service/index.js"; import { IEmailService, IServiceGetter, getRuntimeDepsService } from "../service/index.js";
export type NotificationBody = { export type NotificationBody = {
userId?: number; userId?: number;
@@ -89,16 +89,16 @@ export abstract class BaseNotification implements INotification {
ctx!: NotificationContext; ctx!: NotificationContext;
http!: HttpClient; http!: HttpClient;
logger!: ILogger; logger!: ILogger;
runtimeDepsService?: IRuntimeDepsService;
async importRuntime(specifier: string) { async importRuntime(specifier: string) {
if (!this.runtimeDepsService) { return await getRuntimeDepsService().importRuntime(specifier, this.logger);
return await import(specifier);
}
return await this.runtimeDepsService.importRuntime(specifier, this.logger);
} }
async doSend(body: NotificationBody) { async doSend(body: NotificationBody) {
if (body.content) {
const content = body.content?.replace(/\n/g, " \n");
body.content = content;
}
return await this.send(body); return await this.send(body);
} }
abstract send(body: NotificationBody): Promise<void>; abstract send(body: NotificationBody): Promise<void>;
@@ -109,12 +109,6 @@ export abstract class BaseNotification implements INotification {
this.ctx = ctx; this.ctx = ctx;
this.http = ctx.http; this.http = ctx.http;
this.logger = ctx.logger; this.logger = ctx.logger;
if (!this.runtimeDepsService && this.ctx.serviceGetter) {
this.runtimeDepsService = await this.ctx.serviceGetter.get("runtimeDepsService");
}
if (this.runtimeDepsService && this.ctx.define?.name) {
await this.runtimeDepsService.ensureRuntimeDependencies({ pluginKeys: `notification:${this.ctx.define.name}`, logger: this.logger });
}
} }
setDefine = (define: NotificationDefine) => { setDefine = (define: NotificationDefine) => {
this.define = define; this.define = define;
+3 -13
View File
@@ -10,7 +10,7 @@ import { INotificationService } from "../notification/index.js";
import { Registrable } from "../registry/index.js"; import { Registrable } from "../registry/index.js";
import { IPluginConfigService } from "../service/config.js"; import { IPluginConfigService } from "../service/config.js";
import { TaskEmitter } from "../service/emit.js"; import { TaskEmitter } from "../service/emit.js";
import { ICnameProxyService, IEmailService, IRuntimeDepsService, IServiceGetter, IUrlService } from "../service/index.js"; import { ICnameProxyService, IEmailService, IServiceGetter, IUrlService, getRuntimeDepsService } from "../service/index.js";
export type PluginRequestHandleReq<T = any> = { export type PluginRequestHandleReq<T = any> = {
typeName: string; typeName: string;
@@ -76,7 +76,7 @@ export type ITaskPlugin = {
execute(): Promise<void | string>; execute(): Promise<void | string>;
onRequest(req: PluginRequestHandleReq<any>): Promise<any>; onRequest(req: PluginRequestHandleReq<any>): Promise<any>;
setCtx(ctx: TaskInstanceContext): Promise<void>; setCtx(ctx: TaskInstanceContext): Promise<void>;
importRuntime?(specifier: string): Promise<any>; importRuntime(specifier: string): Promise<any>;
[key: string]: any; [key: string]: any;
}; };
@@ -150,13 +150,9 @@ export abstract class AbstractTaskPlugin implements ITaskPlugin {
logger!: ILogger; logger!: ILogger;
http!: HttpClient; http!: HttpClient;
accessService!: IAccessService; accessService!: IAccessService;
runtimeDepsService!: IRuntimeDepsService;
async importRuntime(specifier: string) { async importRuntime(specifier: string) {
if (!this.runtimeDepsService) { return await getRuntimeDepsService().importRuntime(specifier, this.logger);
throw new Error("runtimeDepsService 未初始化");
}
return await this.runtimeDepsService.importRuntime(specifier, this.logger);
} }
clearLastStatus() { clearLastStatus() {
@@ -178,12 +174,6 @@ export abstract class AbstractTaskPlugin implements ITaskPlugin {
this.logger = ctx.logger; this.logger = ctx.logger;
this.accessService = ctx.accessService; this.accessService = ctx.accessService;
this.http = ctx.http; this.http = ctx.http;
if (!this.runtimeDepsService && this.ctx.serviceGetter) {
this.runtimeDepsService = await this.ctx.serviceGetter.get("runtimeDepsService");
}
if (this.runtimeDepsService && this.ctx.define?.name) {
await this.runtimeDepsService.ensureRuntimeDependencies({ pluginKeys: `plugin:${this.ctx.define.name}`, logger: this.logger });
}
// 将证书加入secret // 将证书加入secret
// @ts-ignore // @ts-ignore
if (this.cert && this.cert.crt && this.cert.key) { if (this.cert && this.cert.crt && this.cert.key) {
@@ -2,41 +2,41 @@ import assert from "assert";
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import os from "os"; import os from "os";
import { RuntimeDepsService, type RuntimeDependencyPluginDefine } from "./runtime-deps-service.js"; import { RuntimeDepsService, NpmRegistryResolver, type RuntimeDependencyPluginDefine } from "./runtime.js";
import { accessRegistry, pluginRegistry } from "@certd/pipeline"; import { accessRegistry } from "../access/registry.js";
import { addonRegistry } from "@certd/lib-server"; import { pluginRegistry } from "../plugin/registry.js";
describe("RuntimeDepsService", () => { describe("RuntimeDepsService", () => {
it("detects conflicting dependency ranges across plugins", () => { it("detects conflicting dependency ranges across plugins", () => {
const service = new RuntimeDepsService(); const service = new RuntimeDepsService({}, null);
const merged = service.collectDependencies([ const merged = service.collectDependencies([
{ name: "a", dependPackages: { foo: "^1.0.0" } }, { name: "a", dependPackages: { foo: "^1.0.0" } },
{ name: "b", dependPackages: { foo: "^1.2.0" } }, { name: "b", dependPackages: { foo: "^1.2.0" } },
]); ]);
assert.deepEqual(merged.dependencies, { foo: "^1.0.0" }); assert.deepEqual(merged.dependencies, { foo: "^1.0.0" });
assert.equal(merged.conflicts.length, 0); assert.equal(merged.conflicts.length, 0);
}); });
it("reports incompatible dependency ranges", () => { it("reports incompatible dependency ranges", () => {
const service = new RuntimeDepsService(); const service = new RuntimeDepsService({}, null);
const merged = service.collectDependencies([ const merged = service.collectDependencies([
{ name: "a", dependPackages: { foo: "^1.0.0" } }, { name: "a", dependPackages: { foo: "^1.0.0" } },
{ name: "b", dependPackages: { foo: "^2.0.0" } }, { name: "b", dependPackages: { foo: "^2.0.0" } },
]); ]);
assert.equal(merged.conflicts.length, 1); assert.equal(merged.conflicts.length, 1);
assert.equal(merged.conflicts[0].packageName, "foo"); assert.equal(merged.conflicts[0].packageName, "foo");
}); });
it("builds a runtime package manifest in the target directory", async () => { it("builds a runtime package manifest in the target directory", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-")); const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-"));
const service = new RuntimeDepsService(); const service = new RuntimeDepsService({ rootDir }, null);
service.runtimeDepsRootDir = rootDir;
service.registryResolver = { service.registryResolver = {
async resolve() { async resolve() {
return "https://registry.npmmirror.com"; return "https://registry.npmmirror.com";
}, },
async resolveOrdered() {
return ["https://registry.npmmirror.com"];
},
} as any; } as any;
service.commandRunner = { service.commandRunner = {
async run(command: string, args: string[]) { async run(command: string, args: string[]) {
@@ -50,22 +50,22 @@ describe("RuntimeDepsService", () => {
return { stdout: "", stderr: "", code: 0 }; return { stdout: "", stderr: "", code: 0 };
}, },
} as any; } as any;
const plugins: RuntimeDependencyPluginDefine[] = [{ name: "a", dependPackages: { foo: "^1.0.0" } }]; const plugins: RuntimeDependencyPluginDefine[] = [{ name: "a", dependPackages: { foo: "^1.0.0" } }];
const result = await service.ensureInstalled({ plugins }); const result = await service.ensureInstalled({ plugins });
assert.equal(result.registryUrl, "https://registry.npmmirror.com"); assert.equal(result.registryUrl, "https://registry.npmmirror.com");
assert.ok(fs.existsSync(path.join(rootDir, "package.json"))); assert.ok(fs.existsSync(path.join(rootDir, "package.json")));
}); });
it("installs direct dependency maps without plugin metadata", async () => { it("installs direct dependency maps without plugin metadata", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-direct-")); const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-direct-"));
const service = new RuntimeDepsService(); const service = new RuntimeDepsService({ rootDir }, null);
service.runtimeDepsRootDir = rootDir;
service.registryResolver = { service.registryResolver = {
async resolve() { async resolve() {
return ""; return "";
}, },
async resolveOrdered() {
return [""];
},
} as any; } as any;
service.commandRunner = { service.commandRunner = {
async run(command: string, args: string[]) { async run(command: string, args: string[]) {
@@ -77,9 +77,7 @@ describe("RuntimeDepsService", () => {
return { stdout: "", stderr: "", code: 0 }; return { stdout: "", stderr: "", code: 0 };
}, },
} as any; } as any;
await service.ensureDependencies({ dependencies: { directPkg: "^1.0.0" } }); await service.ensureDependencies({ dependencies: { directPkg: "^1.0.0" } });
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8")); const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
assert.deepEqual(manifest.dependencies, { directPkg: "^1.0.0" }); assert.deepEqual(manifest.dependencies, { directPkg: "^1.0.0" });
}); });
@@ -91,31 +89,26 @@ describe("RuntimeDepsService", () => {
fs.writeFileSync(path.join(rootDir, "package.json"), JSON.stringify({ name: "runtime-root", type: "module" }), "utf8"); 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, "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"); fs.writeFileSync(path.join(packageDir, "index.js"), "export const value = 42;\n", "utf8");
const service = new RuntimeDepsService({ rootDir }, null);
const service = new RuntimeDepsService();
service.runtimeDepsRootDir = rootDir;
service.commandRunner = { service.commandRunner = {
async run() { async run() {
throw new Error("install should not run"); throw new Error("install should not run");
}, },
} as any; } as any;
const mod = await service.importRuntime("runtime-only"); const mod = await service.importRuntime("runtime-only");
assert.equal(mod.value, 42); assert.equal(mod.value, 42);
}); });
it("installs configured lazy dependency when import target is missing", async () => { it("installs configured lazy dependency when import target is missing", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-lazy-")); const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-lazy-"));
const service = new RuntimeDepsService(); const service = new RuntimeDepsService({ rootDir, lazyDependencies: { "lazy-pkg": "^1.2.3" } }, null);
service.runtimeDepsRootDir = rootDir;
service.lazyDependencies = {
"lazy-pkg": "^1.2.3",
};
service.registryResolver = { service.registryResolver = {
async resolve() { async resolve() {
return ""; return "";
}, },
async resolveOrdered() {
return [""];
},
} as any; } as any;
service.commandRunner = { service.commandRunner = {
async run(command: string, args: string[]) { async run(command: string, args: string[]) {
@@ -130,9 +123,7 @@ describe("RuntimeDepsService", () => {
return { stdout: "", stderr: "", code: 0 }; return { stdout: "", stderr: "", code: 0 };
}, },
} as any; } as any;
const mod = await service.importRuntime("lazy-pkg/sub/entry.js"); const mod = await service.importRuntime("lazy-pkg/sub/entry.js");
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8")); const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
assert.deepEqual(manifest.dependencies, { "lazy-pkg": "^1.2.3" }); assert.deepEqual(manifest.dependencies, { "lazy-pkg": "^1.2.3" });
assert.equal(mod.value, 7); assert.equal(mod.value, 7);
@@ -140,15 +131,14 @@ describe("RuntimeDepsService", () => {
it("resolves scoped package names for lazy imports", async () => { it("resolves scoped package names for lazy imports", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-scoped-")); const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-scoped-"));
const service = new RuntimeDepsService(); const service = new RuntimeDepsService({ rootDir, lazyDependencies: { "@scope/lazy": "^2.0.0" } }, null);
service.runtimeDepsRootDir = rootDir;
service.lazyDependencies = {
"@scope/lazy": "^2.0.0",
};
service.registryResolver = { service.registryResolver = {
async resolve() { async resolve() {
return ""; return "";
}, },
async resolveOrdered() {
return [""];
},
} as any; } as any;
service.commandRunner = { service.commandRunner = {
async run(command: string, args: string[]) { async run(command: string, args: string[]) {
@@ -163,9 +153,7 @@ describe("RuntimeDepsService", () => {
return { stdout: "", stderr: "", code: 0 }; return { stdout: "", stderr: "", code: 0 };
}, },
} as any; } as any;
const mod = await service.importRuntime("@scope/lazy/dist/index.js"); const mod = await service.importRuntime("@scope/lazy/dist/index.js");
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8")); const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
assert.deepEqual(manifest.dependencies, { "@scope/lazy": "^2.0.0" }); assert.deepEqual(manifest.dependencies, { "@scope/lazy": "^2.0.0" });
assert.equal(mod.scoped, true); assert.equal(mod.scoped, true);
@@ -173,59 +161,27 @@ describe("RuntimeDepsService", () => {
it("reports missing lazy dependency configuration", async () => { it("reports missing lazy dependency configuration", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-lazy-missing-")); const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-lazy-missing-"));
const service = new RuntimeDepsService(); const service = new RuntimeDepsService({ rootDir, lazyDependencies: {} }, null);
service.runtimeDepsRootDir = rootDir;
service.lazyDependencies = {};
await assert.rejects(() => service.importRuntime("missing-pkg/sub.js"), /未配置懒加载版本: missing-pkg/); await assert.rejects(() => service.importRuntime("missing-pkg/sub.js"), /未配置懒加载版本: missing-pkg/);
}); });
it("falls back to project node_modules when lazy dependency is not configured", async () => { it("falls back to project node_modules when lazy dependency is not configured", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-project-fallback-")); const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-project-fallback-"));
const service = new RuntimeDepsService(); const service = new RuntimeDepsService({ rootDir, lazyDependencies: {} }, null);
service.runtimeDepsRootDir = rootDir;
service.lazyDependencies = {};
const mod = await service.importRuntime("dayjs"); const mod = await service.importRuntime("dayjs");
assert.equal(typeof mod.default, "function");
});
it("falls back to project node_modules when lazy install fails", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-project-fallback-install-"));
const service = new RuntimeDepsService();
service.runtimeDepsRootDir = rootDir;
service.lazyDependencies = {
dayjs: "^1.11.7",
};
service.registryResolver = {
async resolve() {
return "";
},
} as any;
service.commandRunner = {
async run(command: string, args: string[]) {
assert.equal(command, "pnpm");
if (args.includes("--version")) {
return { stdout: "9.1.0\n", stderr: "", code: 0 };
}
return { stdout: "", stderr: "install failed in test", code: 1 };
},
} as any;
const mod = await service.importRuntime("dayjs");
assert.equal(typeof mod.default, "function"); assert.equal(typeof mod.default, "function");
}); });
it("keeps previously installed dependencies when installing a later plugin", async () => { it("keeps previously installed dependencies when installing a later plugin", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-merge-")); const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-merge-"));
const service = new RuntimeDepsService(); const service = new RuntimeDepsService({ rootDir }, null);
service.runtimeDepsRootDir = rootDir;
service.registryResolver = { service.registryResolver = {
async resolve() { async resolve() {
return ""; return "";
}, },
async resolveOrdered() {
return [""];
},
} as any; } as any;
service.commandRunner = { service.commandRunner = {
async run(command: string, args: string[]) { async run(command: string, args: string[]) {
@@ -237,22 +193,17 @@ describe("RuntimeDepsService", () => {
return { stdout: "", stderr: "", code: 0 }; return { stdout: "", stderr: "", code: 0 };
}, },
} as any; } as any;
await service.ensureInstalled({ plugins: [{ name: "a", pluginType: "deploy", dependPackages: { foo: "^1.0.0" } }] }); await service.ensureInstalled({ plugins: [{ name: "a", pluginType: "deploy", dependPackages: { foo: "^1.0.0" } }] });
await service.ensureInstalled({ plugins: [{ name: "b", pluginType: "deploy", dependPackages: { bar: "^2.0.0" } }] }); await service.ensureInstalled({ plugins: [{ name: "b", pluginType: "deploy", dependPackages: { bar: "^2.0.0" } }] });
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8")); const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
assert.deepEqual(manifest.dependencies, { assert.deepEqual(manifest.dependencies, { foo: "^1.0.0", bar: "^2.0.0" });
foo: "^1.0.0",
bar: "^2.0.0",
});
}); });
it("includes npm dependencies from dependent plugins", () => { it("includes npm dependencies from dependent plugins", async () => {
const service = new RuntimeDepsService(); const service = new RuntimeDepsService({}, { accessRegistry, pluginRegistry });
accessRegistry.register("runtimeDepsAccess", { accessRegistry.register("runtimeDepsAccess", {
define: { name: "runtimeDepsAccess", title: "access", dependPackages: { accessOnly: "^1.0.0" } } as any, define: { name: "runtimeDepsAccess", title: "access", dependPackages: { accessOnly: "^1.0.0" } } as any,
target: async () => ({} as any), target: async () => ({}) as any,
}); });
try { try {
const resolved = service.resolvePluginDependencies({ const resolved = service.resolvePluginDependencies({
@@ -262,11 +213,7 @@ describe("RuntimeDepsService", () => {
dependPackages: { deployOnly: "^1.0.0" }, dependPackages: { deployOnly: "^1.0.0" },
}); });
const merged = service.collectDependencies(resolved); const merged = service.collectDependencies(resolved);
assert.deepEqual(merged.dependencies, { deployOnly: "^1.0.0", accessOnly: "^1.0.0" });
assert.deepEqual(merged.dependencies, {
deployOnly: "^1.0.0",
accessOnly: "^1.0.0",
});
} finally { } finally {
accessRegistry.unRegister("runtimeDepsAccess"); accessRegistry.unRegister("runtimeDepsAccess");
} }
@@ -274,12 +221,14 @@ describe("RuntimeDepsService", () => {
it("installs dependencies by registered plugin key", async () => { it("installs dependencies by registered plugin key", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-key-")); const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-key-"));
const service = new RuntimeDepsService(); const service = new RuntimeDepsService({ rootDir }, { pluginRegistry, accessRegistry });
service.runtimeDepsRootDir = rootDir;
service.registryResolver = { service.registryResolver = {
async resolve() { async resolve() {
return ""; return "";
}, },
async resolveOrdered() {
return [""];
},
} as any; } as any;
service.commandRunner = { service.commandRunner = {
async run(command: string, args: string[]) { async run(command: string, args: string[]) {
@@ -293,11 +242,11 @@ describe("RuntimeDepsService", () => {
} as any; } as any;
pluginRegistry.register("runtimeDepsKey", { pluginRegistry.register("runtimeDepsKey", {
define: { name: "runtimeDepsKey", title: "key", dependPackages: { keyed: "^1.0.0" } } as any, define: { name: "runtimeDepsKey", title: "key", dependPackages: { keyed: "^1.0.0" } } as any,
target: async () => ({} as any), target: async () => ({}) as any,
}); });
try { try {
service.setRegistries({ pluginRegistry, accessRegistry });
await service.ensureRuntimeDependencies({ pluginKeys: "plugin:runtimeDepsKey" }); await service.ensureRuntimeDependencies({ pluginKeys: "plugin:runtimeDepsKey" });
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8")); const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
assert.deepEqual(manifest.dependencies, { keyed: "^1.0.0" }); assert.deepEqual(manifest.dependencies, { keyed: "^1.0.0" });
} finally { } finally {
@@ -305,58 +254,16 @@ describe("RuntimeDepsService", () => {
} }
}); });
it("installs dependencies from multiple plugin keys including addon subtype keys", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-keys-"));
const service = new RuntimeDepsService();
service.runtimeDepsRootDir = rootDir;
service.registryResolver = {
async resolve() {
return "";
},
} as any;
service.commandRunner = {
async run(command: string, args: string[]) {
assert.equal(command, "pnpm");
if (args.includes("--version")) {
return { stdout: "9.1.0\n", stderr: "", code: 0 };
}
fs.mkdirSync(path.join(rootDir, "node_modules"), { recursive: true });
return { stdout: "", stderr: "", code: 0 };
},
} as any;
accessRegistry.register("runtimeDepsArrayAccess", {
define: { name: "runtimeDepsArrayAccess", title: "access", dependPackages: { accessPkg: "^1.0.0" } } as any,
target: async () => ({} as any),
});
addonRegistry.register("captcha:runtimeDepsArrayAddon", {
define: { addonType: "captcha", name: "runtimeDepsArrayAddon", title: "addon", dependPackages: { addonPkg: "^2.0.0" } } as any,
target: async () => ({} as any),
});
try {
await service.ensureRuntimeDependencies({ pluginKeys: ["access:runtimeDepsArrayAccess", "addon:captcha:runtimeDepsArrayAddon"] });
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
assert.deepEqual(manifest.dependencies, {
accessPkg: "^1.0.0",
addonPkg: "^2.0.0",
});
} finally {
accessRegistry.unRegister("runtimeDepsArrayAccess");
addonRegistry.unRegister("captcha:runtimeDepsArrayAddon");
}
});
it("reports missing dependent plugins", () => { it("reports missing dependent plugins", () => {
const service = new RuntimeDepsService(); const service = new RuntimeDepsService({}, { accessRegistry, pluginRegistry });
assert.throws(() => service.resolvePluginDependencies({ name: "deploy", pluginType: "deploy", dependPlugins: { "access:access": "*" } }), /插件依赖缺失/); assert.throws(() => service.resolvePluginDependencies({ name: "deploy", pluginType: "deploy", dependPlugins: { "access:access": "*" } }), /插件依赖缺失/);
}); });
it("reports incompatible dependent plugin versions", () => { it("reports incompatible dependent plugin versions", () => {
const service = new RuntimeDepsService(); const service = new RuntimeDepsService({}, { accessRegistry, pluginRegistry });
accessRegistry.register("runtimeDepsVersionedAccess", { accessRegistry.register("runtimeDepsVersionedAccess", {
define: { name: "runtimeDepsVersionedAccess", title: "access", version: "1.4.0", dependPackages: { accessOnly: "^1.0.0" } } as any, define: { name: "runtimeDepsVersionedAccess", title: "access", version: "1.4.0", dependPackages: { accessOnly: "^1.0.0" } } as any,
target: async () => ({} as any), target: async () => ({}) as any,
}); });
try { try {
assert.throws( assert.throws(
@@ -374,73 +281,10 @@ describe("RuntimeDepsService", () => {
}); });
it("reports bare dependent plugin names as invalid format", () => { it("reports bare dependent plugin names as invalid format", () => {
const service = new RuntimeDepsService(); const service = new RuntimeDepsService({}, null);
assert.throws(() => service.resolvePluginDependencies({ name: "deploy", pluginType: "deploy", dependPlugins: { runtimeDepsBareName: "*" } }), /插件依赖格式错误/); assert.throws(() => service.resolvePluginDependencies({ name: "deploy", pluginType: "deploy", dependPlugins: { runtimeDepsBareName: "*" } }), /插件依赖格式错误/);
}); });
it("records runtime install environment state", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-state-"));
const service = new RuntimeDepsService();
service.runtimeDepsRootDir = rootDir;
service.registryResolver = {
async resolve() {
return "";
},
} as any;
service.commandRunner = {
async run(command: string, args: string[]) {
assert.equal(command, "pnpm");
if (args.includes("--version")) {
return { stdout: "9.1.0\n", stderr: "", code: 0 };
}
assert.equal(args[0], "install");
return { stdout: "", stderr: "", code: 0 };
},
} as any;
await service.ensureInstalled({ plugins: [{ name: "a", dependPackages: { foo: "^1.0.0" } }] });
const state = JSON.parse(fs.readFileSync(path.join(rootDir, "install-state.json"), "utf8"));
assert.equal(state.nodeVersion, process.version);
assert.equal(state.pnpmVersion, "9.1.0");
assert.equal(state.lastError, undefined);
});
it("serializes installs with a file lock", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-lock-"));
const serviceA = new RuntimeDepsService();
const serviceB = new RuntimeDepsService();
for (const service of [serviceA, serviceB]) {
service.runtimeDepsRootDir = rootDir;
service.registryResolver = {
async resolve() {
return "";
},
} as any;
}
let installCount = 0;
const commandRunner = {
async run(command: string, args: string[]) {
assert.equal(command, "pnpm");
if (args.includes("--version")) {
return { stdout: "9.1.0\n", stderr: "", code: 0 };
}
assert.equal(args[0], "install");
installCount++;
await new Promise(resolve => setTimeout(resolve, 50));
fs.mkdirSync(path.join(rootDir, "node_modules"), { recursive: true });
return { stdout: "", stderr: "", code: 0 };
},
};
serviceA.commandRunner = commandRunner as any;
serviceB.commandRunner = commandRunner as any;
await Promise.all([serviceA.ensureInstalled({ plugins: [{ name: "a", dependPackages: { foo: "^1.0.0" } }] }), serviceB.ensureInstalled({ plugins: [{ name: "a", dependPackages: { foo: "^1.0.0" } }] })]);
assert.equal(installCount, 1);
});
it("does not pass node debugger options to pnpm child process", async () => { 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 rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-env-"));
const oldNodeOptions = process.env.NODE_OPTIONS; const oldNodeOptions = process.env.NODE_OPTIONS;
@@ -448,12 +292,14 @@ describe("RuntimeDepsService", () => {
process.env.NODE_OPTIONS = "--inspect=127.0.0.1:9229 --max-old-space-size=4096"; process.env.NODE_OPTIONS = "--inspect=127.0.0.1:9229 --max-old-space-size=4096";
process.env.VSCODE_INSPECTOR_OPTIONS = '{"inspectorIpc":"test"}'; process.env.VSCODE_INSPECTOR_OPTIONS = '{"inspectorIpc":"test"}';
try { try {
const service = new RuntimeDepsService(); const service = new RuntimeDepsService({ rootDir }, null);
service.runtimeDepsRootDir = rootDir;
service.registryResolver = { service.registryResolver = {
async resolve() { async resolve() {
return ""; return "";
}, },
async resolveOrdered() {
return [""];
},
} as any; } as any;
service.commandRunner = { service.commandRunner = {
async run(command: string, args: string[], options: { env?: NodeJS.ProcessEnv }) { async run(command: string, args: string[], options: { env?: NodeJS.ProcessEnv }) {
@@ -467,7 +313,6 @@ describe("RuntimeDepsService", () => {
return { stdout: "", stderr: "", code: 0 }; return { stdout: "", stderr: "", code: 0 };
}, },
} as any; } as any;
await service.ensureInstalled({ plugins: [{ name: "a", dependPackages: { foo: "^1.0.0" } }] }); await service.ensureInstalled({ plugins: [{ name: "a", dependPackages: { foo: "^1.0.0" } }] });
} finally { } finally {
if (oldNodeOptions == null) { if (oldNodeOptions == null) {
@@ -483,26 +328,94 @@ describe("RuntimeDepsService", () => {
} }
}); });
it("clears runtime dependency directory", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-clear-"));
const runtimeRootDir = path.join(rootDir, ".runtime-deps");
fs.mkdirSync(path.join(runtimeRootDir, "node_modules", "foo"), { recursive: true });
fs.writeFileSync(path.join(runtimeRootDir, "package.json"), "{}", "utf8");
const service = new RuntimeDepsService();
service.runtimeDepsRootDir = runtimeRootDir;
service.installTimeoutMs = 1000;
await service.clearRuntimeDeps();
assert.equal(fs.existsSync(runtimeRootDir), true);
assert.equal(fs.readdirSync(runtimeRootDir).length, 0);
});
it("rejects clearing unexpected runtime dependency path", async () => { it("rejects clearing unexpected runtime dependency path", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-clear-invalid-")); const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-clear-invalid-"));
const service = new RuntimeDepsService(); const service = new RuntimeDepsService({ rootDir }, null);
service.runtimeDepsRootDir = rootDir;
await assert.rejects(() => service.clearRuntimeDeps(), /动态依赖目录配置异常/); await assert.rejects(() => service.clearRuntimeDeps(), /动态依赖目录配置异常/);
}); });
}); });
describe("NpmRegistryResolver", () => {
it("returns the fastest successful registry via resolve()", 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.resolve();
assert.equal(result, "https://fast.example.com");
});
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.resolve();
assert.equal(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, []);
const single = await resolver.resolve();
assert.equal(single, "");
});
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);
});
});
+821 -14
View File
@@ -1,27 +1,834 @@
/** import fs from "fs";
* import path from "path";
*/ import { spawn } from "child_process";
export type ImportRuntime = (specifier: string, logger?: ILogger) => Promise<any>; import crypto from "crypto";
import { createRequire } from "module";
/** import { pathToFileURL } from "url";
* import { logger as defaultLogger } from "@certd/basic";
*/ import type { Registry } from "../registry/registry.js";
export type ILogger = { export type ILogger = {
info: (message: string) => void; info: (message: string) => void;
warn?: (message: string) => void;
error?: (message: string, ...args: any[]) => void;
}; };
/** export type ImportRuntime = (specifier: string, logger?: ILogger) => Promise<any>;
*
*/
export type EnsureRuntimeDepsOptions = { export type EnsureRuntimeDepsOptions = {
pluginKeys: string | string[]; pluginKeys: string | string[];
logger?: ILogger; logger?: ILogger;
}; };
/**
*
*/
export interface IRuntimeDepsService { export interface IRuntimeDepsService {
ensureRuntimeDependencies(options: EnsureRuntimeDepsOptions): Promise<any>; ensureRuntimeDependencies(options: EnsureRuntimeDepsOptions): Promise<any>;
importRuntime: ImportRuntime; importRuntime: ImportRuntime;
} }
export type RuntimeDependencyPluginDefine = {
name: string;
key?: string;
title?: string;
version?: string;
pluginType?: string;
addonType?: string;
dependPlugins?: Record<string, string>;
dependPackages?: Record<string, string>;
};
type RegisteredDefineLike = RuntimeDependencyPluginDefine & {
key?: string;
pluginType?: string;
addonType?: string;
dependPlugins?: Record<string, string>;
dependPackages?: Record<string, string>;
};
type DependencyConflict = {
packageName: string;
ranges: Array<{ pluginName: string; range: string }>;
};
type CollectDependenciesResult = {
dependencies: Record<string, string>;
conflicts: DependencyConflict[];
};
type InstallResult = {
registryUrl: string;
packageJsonPath: string;
};
type RuntimeImportResolveResult = {
resolved: string;
packageName: string;
};
type CommandRunnerResult = {
stdout: string;
stderr: string;
code: number;
};
type CommandRunner = {
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 resolve(): Promise<string> {
const config = this.config;
if (config?.mode === "fixed" && config.fixedUrl) {
return config.fixedUrl;
}
if (config?.mode === "system") {
return "";
}
const cached = this.cache;
if (cached && cached.expiresAt > Date.now()) {
const fastUrl = cached.orderedUrls[0];
if (fastUrl) {
const probeResult = await this.probe(fastUrl);
if (probeResult.ok) {
return cached.orderedUrls[0] || "";
}
}
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[0] || "";
}
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;
autoInstall?: boolean;
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];
}
const PROCESS_LOCKS = new Map<string, Promise<unknown>>();
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;
autoInstall: boolean;
enabled: boolean;
installTimeoutMs: number;
pnpmCommand: string;
lazyDependencies: Record<string, string>;
registryResolver!: NpmRegistryResolver;
commandRunner: CommandRunner = new DefaultCommandRunner();
pluginLazyDependencies: Record<string, string> = {};
private installPromises = new Map<string, Promise<InstallResult>>();
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.autoInstall = config?.autoInstall ?? true;
this.enabled = config?.enabled ?? true;
this.installTimeoutMs = config?.installTimeoutMs ?? 120000;
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;
}
collectDependencies(plugins: RuntimeDependencyPluginDefine[]): CollectDependenciesResult {
const merged: Record<string, string> = {};
const seen: Record<string, Array<{ pluginName: string; range: string }>> = {};
for (const plugin of plugins) {
const deps = plugin.dependPackages || {};
for (const [packageName, range] of Object.entries(deps)) {
seen[packageName] ||= [];
seen[packageName].push({ pluginName: plugin.name, range });
}
}
const conflicts: DependencyConflict[] = [];
for (const [packageName, ranges] of Object.entries(seen)) {
const first = ranges[0]?.range;
if (!first) {
continue;
}
const conflict = ranges.some(item => !areRangesCompatible(first, item.range));
if (conflict) {
conflicts.push({ packageName, ranges });
continue;
}
merged[packageName] = first;
}
return { dependencies: merged, conflicts };
}
async ensureInstalled(options: { plugins: RuntimeDependencyPluginDefine[]; logger?: ILogger }): Promise<InstallResult> {
const { plugins, logger: log } = options;
const { dependencies, conflicts } = this.resolveDependenciesFromPlugins(plugins);
if (conflicts.length > 0) {
const conflict = conflicts[0];
throw new Error(`动态依赖版本冲突: ${conflict.packageName} => ${conflict.ranges.map(item => `${item.pluginName}:${item.range}`).join(", ")}`);
}
return await this.ensureDependencies({ dependencies, logger: log });
}
async ensureDependencies(options: { dependencies: Record<string, string>; logger?: ILogger }): Promise<InstallResult> {
const { dependencies, logger: log } = options;
if (!this.enabled) {
return { registryUrl: "", packageJsonPath: path.join(this.getRuntimeDepsRootDir(), "package.json") };
}
if (!this.autoInstall) {
return { registryUrl: "", packageJsonPath: path.join(this.getRuntimeDepsRootDir(), "package.json") };
}
const dependenciesHash = this.createDependenciesHash(dependencies);
let installPromise = this.installPromises.get(dependenciesHash);
if (installPromise) {
const nodeModulesPath = path.join(this.getRuntimeDepsRootDir(), "node_modules");
if (!fs.existsSync(nodeModulesPath)) {
this.installPromises.delete(dependenciesHash);
installPromise = undefined;
}
}
if (!installPromise) {
installPromise = this.doEnsureInstalled({ dependencies, logger: log }).catch(error => {
this.installPromises.delete(dependenciesHash);
throw error;
});
this.installPromises.set(dependenciesHash, installPromise);
}
return await installPromise;
}
resolveDependenciesFromPlugins(plugins: RuntimeDependencyPluginDefine[]): CollectDependenciesResult {
const expandedPlugins = plugins.flatMap(plugin => this.resolvePluginDependencies(plugin));
return this.collectDependencies(expandedPlugins);
}
async ensureRuntimeDependencies(options: { pluginKeys: string | string[]; logger?: ILogger }): Promise<InstallResult> {
const { pluginKeys, logger: log } = options;
const keys = Array.isArray(pluginKeys) ? pluginKeys : [pluginKeys];
const pluginDefines = keys.map(pluginKey => this.getDefineByPluginKey(pluginKey));
if (pluginDefines.every(pluginDefine => !pluginDefine.dependPackages && !pluginDefine.dependPlugins)) {
return { registryUrl: "", packageJsonPath: path.join(this.getRuntimeDepsRootDir(), "package.json") };
}
const expandedPluginDefines = pluginDefines.flatMap(pluginDefine => this.resolvePluginDependencies(pluginDefine));
return await this.ensureInstalled({ plugins: expandedPluginDefines, logger: log });
}
async importRuntime(specifier: string, logger: ILogger = defaultLogger) {
if (this.isNativeImportSpecifier(specifier)) {
return await import(specifier);
}
const resolved = await this.resolveImportSpecifier(specifier, logger);
return await import(pathToFileURL(resolved).href);
}
private async resolveImportSpecifier(specifier: string, logger: ILogger = defaultLogger) {
try {
return this.resolveRuntimeSpecifier(specifier).resolved;
} catch (runtimeError: any) {
if (!this.isModuleNotFoundError(runtimeError)) {
throw runtimeError;
}
return await this.resolveMissingRuntimeSpecifier(specifier, runtimeError, logger);
}
}
private async resolveMissingRuntimeSpecifier(specifier: string, runtimeError: any, logger?: ILogger) {
const packageName = this.parsePackageName(specifier);
const mergedDeps = this.getMergedLazyDependencies();
const lazyRange = mergedDeps[packageName];
if (!lazyRange) {
try {
return this.resolveProjectSpecifier(specifier, runtimeError).resolved;
} catch {
throw new Error(`动态依赖未安装且未配置懒加载版本: ${packageName}`);
}
}
try {
await this.ensureLazyDependency(packageName, logger);
return this.resolveRuntimeSpecifier(specifier).resolved;
} catch (lazyError: any) {
logger?.error?.(`动态依赖安装失败: ${lazyError.message}`);
return this.resolveProjectSpecifier(specifier, lazyError).resolved;
}
}
private isNativeImportSpecifier(specifier: string) {
return specifier.startsWith(".") || specifier.startsWith("/") || specifier.startsWith("file:") || specifier.startsWith("node:");
}
private resolveRuntimeSpecifier(specifier: string): RuntimeImportResolveResult {
const packageName = this.parsePackageName(specifier);
const packageJsonPath = path.join(this.getRuntimeDepsRootDir(), "package.json");
const require = createRequire(packageJsonPath);
const resolved = require.resolve(specifier);
return { packageName, resolved };
}
private resolveProjectSpecifier(specifier: string, cause?: any): RuntimeImportResolveResult {
try {
const packageName = this.parsePackageName(specifier);
const packageJsonPath = path.resolve("package.json");
const require = createRequire(packageJsonPath);
const resolved = require.resolve(specifier);
return { packageName, resolved };
} catch (projectError: any) {
if (cause) {
projectError.cause = cause;
}
throw projectError;
}
}
private parsePackageName(specifier: string) {
if (!specifier || specifier.trim() !== specifier) {
throw new Error(`动态依赖导入路径无效: ${specifier}`);
}
const parts = specifier.split("/");
if (specifier.startsWith("@")) {
if (parts.length < 2 || !parts[0] || !parts[1]) {
throw new Error(`动态依赖导入路径无效: ${specifier}`);
}
return `${parts[0]}/${parts[1]}`;
}
if (!parts[0]) {
throw new Error(`动态依赖导入路径无效: ${specifier}`);
}
return parts[0];
}
private async ensureLazyDependency(packageName: string, logger?: ILogger) {
const range = this.lazyDependencies?.[packageName];
if (!range) {
throw new Error(`动态依赖未安装且未配置懒加载版本: ${packageName}`);
}
await this.ensureDependencies({ dependencies: { [packageName]: range }, logger });
}
private isModuleNotFoundError(error: any) {
return error?.code === "MODULE_NOT_FOUND" || error?.code === "ERR_MODULE_NOT_FOUND";
}
resolvePluginDependencies(current: RuntimeDependencyPluginDefine): RuntimeDependencyPluginDefine[] {
const resolved: RuntimeDependencyPluginDefine[] = [];
const visited = new Set<string>();
const visit = (item: RuntimeDependencyPluginDefine) => {
const key = this.buildPluginDependencyKey(item);
if (visited.has(key)) {
return;
}
visited.add(key);
resolved.push(item);
for (const [dependencyName, expectedRange] of Object.entries(item.dependPlugins || {})) {
const dependency = this.getDefineByPluginKey(dependencyName, item);
if (!isPluginVersionCompatible(dependency, expectedRange)) {
throw new Error(`插件依赖版本冲突: ${item.name} 依赖 ${dependencyName}@${expectedRange},当前版本为 ${dependency.version || "未声明"}`);
}
visit(dependency);
}
};
visit(current);
return resolved;
}
private buildPluginDependencyKey(plugin: RuntimeDependencyPluginDefine) {
if (plugin.pluginType === "addon" && plugin.addonType) {
return `addon:${plugin.addonType}:${plugin.name}`;
}
const pluginType = plugin.pluginType === "deploy" ? "plugin" : plugin.pluginType || "unknown";
return `${pluginType}:${plugin.name}`;
}
private getDefineByPluginKey(pluginKey: string, owner?: RuntimeDependencyPluginDefine): RuntimeDependencyPluginDefine {
const parts = pluginKey.split(":");
let pluginType: string, name: string, subtype: string | undefined;
if (parts.length === 2) {
[pluginType, name] = parts;
} else if (parts.length === 3) {
[pluginType, subtype, name] = parts;
} else {
const ownerName = owner?.name || pluginKey;
throw new Error(`插件依赖格式错误: ${ownerName} 依赖 ${pluginKey}`);
}
if (!this.registriesMap) {
throw new Error("注册表未设置,请先调用 setRegistries");
}
const target = this.registriesMap[pluginType];
if (!target) {
const ownerName = owner?.name || pluginKey;
throw new Error(`插件依赖格式错误: ${ownerName} 依赖 ${pluginKey},未知插件类型 ${pluginType}`);
}
// addon 类型的 key 需要包含 subtype
const registryKey = pluginType === "addon" && subtype ? `${subtype}:${name}` : name;
const define = target.registry.getDefine(registryKey) as RegisteredDefineLike;
if (!define) {
throw new Error(`插件依赖缺失: ${owner?.name || pluginKey} 依赖 ${pluginKey},但该插件未注册或已禁用`);
}
return { ...define, key: pluginKey, pluginType: target.pluginType, addonType: target.addonType };
}
private async doEnsureInstalled(options: { dependencies: Record<string, string>; logger?: ILogger }): Promise<InstallResult> {
let { dependencies } = options;
const log = options.logger || defaultLogger;
return await this.withInstallLock(async () => {
const rootDir = this.getRuntimeDepsRootDir();
const packageJsonPath = path.join(rootDir, "package.json");
const lockPath = path.join(rootDir, "pnpm-lock.yaml");
log.info(`第三方依赖安装: ${JSON.stringify(dependencies)}`);
dependencies = this.mergeInstalledDependencies(this.readManifestDependencies(packageJsonPath), dependencies);
const dependenciesHash = this.createDependenciesHash(dependencies);
const statePath = path.join(rootDir, "install-state.json");
const currentState = this.readInstallState(statePath);
if (currentState?.dependenciesHash === dependenciesHash && fs.existsSync(path.join(rootDir, "node_modules"))) {
log.info("第三方依赖已安装");
return { registryUrl: currentState.registryUrl || "", packageJsonPath };
}
const manifest = { name: "certd-runtime-deps", private: true, type: "module", dependencies };
fs.writeFileSync(packageJsonPath, JSON.stringify(manifest, null, 2), "utf8");
const registryUrl = await this.registryResolver.resolve();
const env = this.buildChildEnv(registryUrl);
const command = this.getPnpmCommand();
const pnpmVersion = await this.getPnpmVersion(command, env);
const allRegistryUrls = await this.registryResolver.resolveOrdered();
const urlsToTry = allRegistryUrls.length > 0 ? allRegistryUrls : [""];
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 = tryUrl ? this.buildChildEnv(tryUrl) : env;
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) {
this.writeInstallState(statePath, { installedAt: new Date().toISOString(), registryUrl: tryUrl, dependenciesHash, nodeVersion: process.version, pnpmVersion, lockFileExists: fs.existsSync(lockPath) });
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)}`);
}
}
this.writeInstallState(statePath, {
...currentState,
installedAt: currentState?.installedAt,
failedAt: new Date().toISOString(),
registryUrl: urlsToTry[0],
dependenciesHash,
nodeVersion: process.version,
pnpmVersion,
lockFileExists: fs.existsSync(lockPath),
lastError,
});
throw new Error(`动态依赖安装失败: ${lastError}`);
});
}
private async withInstallLock<T>(run: () => Promise<T>): Promise<T> {
const rootDir = this.getRuntimeDepsRootDir();
fs.mkdirSync(rootDir, { recursive: true });
const lockFile = path.join(rootDir, ".install.lock");
const previous = PROCESS_LOCKS.get(lockFile);
if (previous) {
await previous.catch(() => undefined);
}
let releaseProcessLock!: () => void;
const current = new Promise<void>(resolve => {
releaseProcessLock = resolve;
});
PROCESS_LOCKS.set(lockFile, current);
let fd: number | undefined;
try {
fd = await this.acquireFileLock(lockFile);
return await run();
} finally {
if (fd != null) {
fs.closeSync(fd);
try {
fs.rmSync(lockFile, { force: true });
} catch {
try {
fs.rmSync(lockFile, { force: true });
} catch {}
}
}
releaseProcessLock();
if (PROCESS_LOCKS.get(lockFile) === current) {
PROCESS_LOCKS.delete(lockFile);
}
}
}
private async acquireFileLock(lockFile: string) {
const deadline = Date.now() + this.installTimeoutMs;
while (true) {
try {
const fd = fs.openSync(lockFile, "wx");
fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }), "utf8");
return fd;
} catch (error: any) {
if (error?.code !== "EEXIST") {
throw error;
}
if (Date.now() > deadline) {
throw new Error(`动态依赖安装锁等待超时: ${lockFile}`);
}
await this.waitForExternalLock(lockFile, deadline);
}
}
}
private async waitForExternalLock(lockFile: string, deadline: number) {
while (fs.existsSync(lockFile)) {
if (Date.now() > deadline) {
throw new Error(`动态依赖安装锁等待超时: ${lockFile}`);
}
await new Promise(resolve => setTimeout(resolve, 300));
}
}
async clearRuntimeDeps() {
const rootDir = this.getRuntimeDepsRootDir();
const normalizedRootDir = path.normalize(rootDir);
if (!normalizedRootDir.endsWith(path.normalize(".runtime-deps"))) {
throw new Error(`动态依赖目录配置异常,拒绝清理: ${rootDir}`);
}
await this.withInstallLock(async () => {
if (fs.existsSync(rootDir)) {
const entries = fs.readdirSync(rootDir);
for (const entry of entries) {
if (entry === ".install.lock") {
continue;
}
fs.rmSync(path.join(rootDir, entry), { recursive: true, force: true });
}
}
this.installPromises.clear();
return undefined;
});
}
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 readInstallState(statePath: string): any {
if (!fs.existsSync(statePath)) {
return null;
}
try {
return JSON.parse(fs.readFileSync(statePath, "utf8"));
} catch {
return null;
}
}
private writeInstallState(statePath: string, state: any) {
fs.writeFileSync(statePath, JSON.stringify(state, null, 2), "utf8");
}
private readManifestDependencies(packageJsonPath: string): Record<string, string> {
if (!fs.existsSync(packageJsonPath)) {
return {};
}
try {
const manifest = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
return manifest.dependencies || {};
} catch {
return {};
}
}
private mergeInstalledDependencies(installed: Record<string, string>, requested: Record<string, string>) {
const dependencies = { ...installed };
for (const [packageName, range] of Object.entries(requested)) {
const installedRange = dependencies[packageName];
if (installedRange && !areRangesCompatible(installedRange, range)) {
throw new Error(`动态依赖版本冲突: ${packageName} => installed:${installedRange}, requested:${range}`);
}
dependencies[packageName] = installedRange || range;
}
return dependencies;
}
private async getPnpmVersion(command: string, env: NodeJS.ProcessEnv) {
const result = await this.commandRunner.run(command, ["--version"], { cwd: this.getRuntimeDepsRootDir(), timeoutMs: Math.min(this.installTimeoutMs, 10000), env });
if (result.code !== 0) {
return "";
}
return (result.stdout || result.stderr || "").trim();
}
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);
}
private createDependenciesHash(dependencies: Record<string, string>) {
return crypto.createHash("sha256").update(JSON.stringify(dependencies)).digest("hex");
}
}
function isPluginVersionCompatible(plugin: RuntimeDependencyPluginDefine, expectedRange: string) {
if (!expectedRange || expectedRange === "*") {
return true;
}
if (!plugin.version) {
return false;
}
return areRangesCompatible(expectedRange, plugin.version);
}
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);
}
+28
View File
@@ -3,6 +3,34 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.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
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
**Note:** Version bump only for package @certd/lib-huawei
# [1.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
**Note:** Version bump only for package @certd/lib-huawei
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14) ## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
### Bug Fixes ### Bug Fixes
+4 -3
View File
@@ -1,7 +1,7 @@
{ {
"name": "@certd/lib-huawei", "name": "@certd/lib-huawei",
"private": false, "private": false,
"version": "1.41.4", "version": "1.42.6",
"main": "./dist/bundle.js", "main": "./dist/bundle.js",
"module": "./dist/bundle.js", "module": "./dist/bundle.js",
"types": "./dist/d/index.d.ts", "types": "./dist/d/index.d.ts",
@@ -15,7 +15,7 @@
"pub": "npm publish", "pub": "npm publish",
"compile": "npm run build", "compile": "npm run build",
"format": "prettier --write src", "format": "prettier --write src",
"lint": "eslint --fix" "lint": "eslint --fix --ext .ts src"
}, },
"dependencies": { "dependencies": {
"axios": "^1.9.0", "axios": "^1.9.0",
@@ -26,9 +26,10 @@
"@typescript-eslint/eslint-plugin": "^8.26.1", "@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1", "@typescript-eslint/parser": "^8.26.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint": "^8.57.0",
"esmock": "^2.7.5", "esmock": "^2.7.5",
"prettier": "3.3.3", "prettier": "3.3.3",
"tslib": "^2.8.1" "tslib": "^2.8.1"
}, },
"gitHead": "bc731e4fb119787930e816a7d57c808b1b5cd66a" "gitHead": "246ee83015bf5589adc2a5fa3d1388c8d9a2a252"
} }
+28
View File
@@ -3,6 +3,34 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.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
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
**Note:** Version bump only for package @certd/lib-iframe
# [1.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
**Note:** Version bump only for package @certd/lib-iframe
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14) ## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
### Bug Fixes ### Bug Fixes
+3 -3
View File
@@ -1,7 +1,7 @@
{ {
"name": "@certd/lib-iframe", "name": "@certd/lib-iframe",
"private": false, "private": false,
"version": "1.41.4", "version": "1.42.6",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
@@ -18,7 +18,7 @@
"pub": "npm publish", "pub": "npm publish",
"compile": "npm run build", "compile": "npm run build",
"format": "prettier --write src", "format": "prettier --write src",
"lint": "eslint --fix" "lint": "eslint --fix --ext .ts src"
}, },
"dependencies": { "dependencies": {
"nanoid": "^5.0.7" "nanoid": "^5.0.7"
@@ -37,5 +37,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "bc731e4fb119787930e816a7d57c808b1b5cd66a" "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> = { export type IframeMessageData<T> = {
action: string; action: string;
@@ -29,10 +29,12 @@ export class IframeClient {
onError?: any; onError?: any;
handlers: Record<string, (data: IframeMessageData<any>) => Promise<void>> = {}; handlers: Record<string, (data: IframeMessageData<any>) => Promise<void>> = {};
private messageHandler: (event: MessageEvent<IframeMessageData<any>>) => Promise<void>;
constructor(iframe?: HTMLIFrameElement, onError?: (e: any) => void) { constructor(iframe?: HTMLIFrameElement, onError?: (e: any) => void) {
this.iframe = iframe; this.iframe = iframe;
this.onError = onError; this.onError = onError;
window.addEventListener('message', async (event: MessageEvent<IframeMessageData<any>>) => { this.messageHandler = async (event: MessageEvent<IframeMessageData<any>>) => {
const data = event.data; const data = event.data;
if (data.action) { if (data.action) {
console.log(`收到消息[isSub:${this.isInFrame()}]`, data); console.log(`收到消息[isSub:${this.isInFrame()}]`, data);
@@ -40,20 +42,21 @@ export class IframeClient {
const handler = this.handlers[data.action]; const handler = this.handlers[data.action];
if (handler) { if (handler) {
const res = await handler(data); const res = await handler(data);
if (data.id && data.action !== 'reply') { if (data.id && data.action !== "reply") {
await this.send('reply', res, data.id); await this.send("reply", res, data.id);
} }
} else { } else {
throw new Error(`action:${data.action} 未注册处理器,可能版本过低`); throw new Error(`action:${data.action} 未注册处理器,可能版本过低`);
} }
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
await this.send('reply', {}, data.id, 500, e.message); await this.send("reply", {}, data.id, 500, e.message);
} }
} }
}); };
window.addEventListener("message", this.messageHandler);
this.register('reply', async data => { this.register("reply", async data => {
const req = this.requestQueue[data.replyId!]; const req = this.requestQueue[data.replyId!];
if (req) { if (req) {
req.onReply(data); req.onReply(data);
@@ -61,11 +64,20 @@ export class IframeClient {
} }
}); });
} }
isInFrame() {
public destroy() {
window.removeEventListener("message", this.messageHandler);
this.requestQueue = {};
this.handlers = {};
}
public close() {
this.destroy();
}
public isInFrame() {
return window.self !== window.top; return window.self !== window.top;
} }
register<T = any>(action: string, handler: (data: IframeMessageData<T>) => Promise<any>) { public register<T = any>(action: string, handler: (data: IframeMessageData<T>) => Promise<any>) {
this.handlers[action] = handler; this.handlers[action] = handler;
} }
@@ -106,12 +118,12 @@ export class IframeClient {
console.log(`send message[isSub:${this.isInFrame()}]:`, reqMessageData); console.log(`send message[isSub:${this.isInFrame()}]:`, reqMessageData);
if (!this.iframe) { if (!this.iframe) {
if (!window.parent) { if (!window.parent) {
reject('当前页面不在 iframe 中'); reject("当前页面不在 iframe 中");
} }
window.parent.postMessage(reqMessageData, '*'); window.parent.postMessage(reqMessageData, "*");
} else { } else {
//子页面 //子页面
this.iframe.contentWindow?.postMessage(reqMessageData, '*'); this.iframe.contentWindow?.postMessage(reqMessageData, "*");
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e);
+28
View File
@@ -3,6 +3,34 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.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
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
**Note:** Version bump only for package @certd/jdcloud
# [1.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
**Note:** Version bump only for package @certd/jdcloud
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14) ## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
### Bug Fixes ### Bug Fixes
+4 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@certd/jdcloud", "name": "@certd/jdcloud",
"version": "1.41.4", "version": "1.42.6",
"description": "jdcloud openApi sdk", "description": "jdcloud openApi sdk",
"main": "./dist/bundle.js", "main": "./dist/bundle.js",
"module": "./dist/bundle.js", "module": "./dist/bundle.js",
@@ -13,7 +13,7 @@
"pub": "npm publish", "pub": "npm publish",
"compile": "npm run build", "compile": "npm run build",
"format": "prettier --write src", "format": "prettier --write src",
"lint": "eslint --fix" "lint": "eslint --fix --ext .ts src"
}, },
"author": "", "author": "",
"license": "Apache", "license": "Apache",
@@ -35,6 +35,7 @@
"chai": "^5.1.0", "chai": "^5.1.0",
"config": "^1.30.0", "config": "^1.30.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint": "^8.57.0",
"esmock": "^2.7.5", "esmock": "^2.7.5",
"js-yaml": "^3.11.0", "js-yaml": "^3.11.0",
"mocha": "^10.6.0", "mocha": "^10.6.0",
@@ -62,5 +63,5 @@
"fetch" "fetch"
] ]
}, },
"gitHead": "bc731e4fb119787930e816a7d57c808b1b5cd66a" "gitHead": "246ee83015bf5589adc2a5fa3d1388c8d9a2a252"
} }
+6 -6
View File
@@ -1,11 +1,11 @@
import jdCloud from "./lib/core.js"; import jdCloud from './lib/core.js';
import jdService from './lib/service.js' import jdService from './lib/service.js';
import domainService from './repo/domainservice/v2/domainservice.js' import domainService from './repo/domainservice/v2/domainservice.js';
import cdnService from './repo/cdn/v1/cdn.js' import cdnService from './repo/cdn/v1/cdn.js';
import sslService from './repo/ssl/v1/ssl.js' import sslService from './repo/ssl/v1/ssl.js';
export const JDCloud = jdCloud; export const JDCloud = jdCloud;
export const JDService = jdService; export const JDService = jdService;
export const JDDomainService = domainService; export const JDDomainService = domainService;
export const JDCdnService = cdnService; export const JDCdnService = cdnService;
export const JDSslService = sslService; export const JDSslService = sslService;
+28
View File
@@ -3,6 +3,34 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.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
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
**Note:** Version bump only for package @certd/lib-k8s
# [1.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
**Note:** Version bump only for package @certd/lib-k8s
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14) ## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
**Note:** Version bump only for package @certd/lib-k8s **Note:** Version bump only for package @certd/lib-k8s
+4 -4
View File
@@ -1,7 +1,7 @@
{ {
"name": "@certd/lib-k8s", "name": "@certd/lib-k8s",
"private": false, "private": false,
"version": "1.41.4", "version": "1.42.6",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
@@ -18,10 +18,10 @@
"pub": "npm publish", "pub": "npm publish",
"compile": "tsc --skipLibCheck --watch", "compile": "tsc --skipLibCheck --watch",
"format": "prettier --write src", "format": "prettier --write src",
"lint": "eslint --fix" "lint": "eslint --fix --ext .ts src"
}, },
"dependencies": { "dependencies": {
"@certd/basic": "^1.41.4", "@certd/basic": "^1.42.6",
"@kubernetes/client-node": "0.21.0" "@kubernetes/client-node": "0.21.0"
}, },
"devDependencies": { "devDependencies": {
@@ -38,5 +38,5 @@
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "bc731e4fb119787930e816a7d57c808b1b5cd66a" "gitHead": "246ee83015bf5589adc2a5fa3d1388c8d9a2a252"
} }
+43
View File
@@ -3,6 +3,49 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.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
## [1.42.1](https://github.com/certd/certd/compare/v1.42.0...v1.42.1) (2026-07-06)
**Note:** Version bump only for package @certd/lib-server
# [1.42.0](https://github.com/certd/certd/compare/v1.41.4...v1.42.0) (2026-07-05)
### Features
* 通过插件配置懒加载依赖,动态加载第三方依赖包,精简安装镜像大小 ([01568ca](https://github.com/certd/certd/commit/01568ca1489069046b5a89ebdd4ced2f7f6ddf93))
### Performance Improvements
* 【破坏性更新】 证书压缩包不再生成文件存储,而是实时打包下载,证书申请插件不再输出certZip ([7cff1a9](https://github.com/certd/certd/commit/7cff1a98424120585205889874b3ef4956a30583))
* 优化用户体验,首次访问时弹出邮箱账号绑定用以初始化账号 ([608cc2a](https://github.com/certd/certd/commit/608cc2a81ff0b4872c9fe11ed9c9c0b4b90a12a3))
* 支持全自动匹配部署宝塔网站证书 ([4dff48e](https://github.com/certd/certd/commit/4dff48e807c32a7623ec9206cf39c88e88f89f6a))
* dns默认ipv4first ([194463b](https://github.com/certd/certd/commit/194463bea9e797315aa7a724f4b2930701570419))
## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14) ## [1.41.4](https://github.com/certd/certd/compare/v1.41.3...v1.41.4) (2026-06-14)
**Note:** Version bump only for package @certd/lib-server **Note:** Version bump only for package @certd/lib-server
+10 -10
View File
@@ -1,6 +1,6 @@
{ {
"name": "@certd/lib-server", "name": "@certd/lib-server",
"version": "1.41.4", "version": "1.42.6",
"description": "midway with flyway, sql upgrade way ", "description": "midway with flyway, sql upgrade way ",
"private": false, "private": false,
"type": "module", "type": "module",
@@ -19,7 +19,7 @@
"pub": "npm publish", "pub": "npm publish",
"compile": "tsc --skipLibCheck --watch", "compile": "tsc --skipLibCheck --watch",
"format": "prettier --write src", "format": "prettier --write src",
"lint": "eslint --fix" "lint": "eslint --fix --ext .ts src"
}, },
"keywords": [], "keywords": [],
"author": "greper", "author": "greper",
@@ -29,11 +29,11 @@
], ],
"license": "AGPL", "license": "AGPL",
"dependencies": { "dependencies": {
"@certd/acme-client": "^1.41.4", "@certd/acme-client": "^1.42.6",
"@certd/basic": "^1.41.4", "@certd/basic": "^1.42.6",
"@certd/pipeline": "^1.41.4", "@certd/pipeline": "^1.42.6",
"@certd/plugin-lib": "^1.41.4", "@certd/plugin-lib": "^1.42.6",
"@certd/plus-core": "^1.41.4", "@certd/plus-core": "^1.42.6",
"@midwayjs/cache": "3.14.0", "@midwayjs/cache": "3.14.0",
"@midwayjs/core": "3.20.11", "@midwayjs/core": "3.20.11",
"@midwayjs/i18n": "3.20.13", "@midwayjs/i18n": "3.20.13",
@@ -49,8 +49,6 @@
"typeorm": "^0.3.20" "typeorm": "^0.3.20"
}, },
"devDependencies": { "devDependencies": {
"mwts": "^1.3.0",
"mwtsc": "^1.4.0",
"@types/chai": "^4.3.12", "@types/chai": "^4.3.12",
"@types/mocha": "^10.0.6", "@types/mocha": "^10.0.6",
"@types/node": "^18", "@types/node": "^18",
@@ -62,6 +60,8 @@
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.1.3",
"esmock": "^2.7.5", "esmock": "^2.7.5",
"mocha": "^10.6.0", "mocha": "^10.6.0",
"mwts": "^1.3.0",
"mwtsc": "^1.4.0",
"prettier": "3.3.3", "prettier": "3.3.3",
"rimraf": "^5.0.5", "rimraf": "^5.0.5",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
@@ -69,5 +69,5 @@
"typeorm": "^0.3.20", "typeorm": "^0.3.20",
"typescript": "^5.4.2" "typescript": "^5.4.2"
}, },
"gitHead": "bc731e4fb119787930e816a7d57c808b1b5cd66a" "gitHead": "246ee83015bf5589adc2a5fa3d1388c8d9a2a252"
} }
@@ -0,0 +1,34 @@
/// <reference types="mocha" />
import assert from "node:assert/strict";
import { AuditLogContext } from "./audit.js";
// AuditLog decorator and getAuditLogOptions are removed since auditLog()
// now signals audit intent directly via ctx.auditLog.enabled
describe("AuditLogContext type", () => {
it("supports enabled flag", () => {
const ctx: AuditLogContext = {
type: "pipeline",
action: "删除流水线",
append: ["ID:5"],
content: "删除了流水线(ID:5)",
projectId: 3,
enabled: true,
};
assert.equal(ctx.enabled, true);
assert.equal(ctx.type, "pipeline");
assert.equal(ctx.content, "删除了流水线(ID:5)");
assert.equal(ctx.projectId, 3);
});
it("works with minimal fields", () => {
const ctx: AuditLogContext = {
enabled: true,
append: ["提交2条"],
};
assert.equal(ctx.enabled, true);
});
});
@@ -0,0 +1,44 @@
export type AuditLogOptions = {
type?: string;
action?: string;
content?: string;
template?: string;
disabled?: boolean;
};
export type AuditLogContext = {
type?: string;
action?: string;
append?: string | string[];
content?: string;
projectId?: number;
enabled?: boolean;
scope?: string;
userId?: number;
username?: string;
success?: boolean;
};
/** 审计日志方法的参数类型 */
export type AuditLogParam = {
type?: string;
action?: string;
content?: string;
append?: string | string[];
projectId?: number;
userId?: number;
username?: string;
};
/** AuditService.log() 参数类型 */
export type AuditLogWriteParam = {
userId: number;
type: string;
action: string;
content: string;
username?: string;
projectId?: number;
ipAddress?: string;
scope?: string;
success?: boolean;
};
@@ -1,9 +1,9 @@
import { ApplicationContext, Inject } from '@midwayjs/core'; import { ApplicationContext, Inject } from "@midwayjs/core";
import type {IMidwayContainer} from '@midwayjs/core'; import type { IMidwayContainer } from "@midwayjs/core";
import * as koa from '@midwayjs/koa'; import * as koa from "@midwayjs/koa";
import { Constants } from './constants.js'; import { Constants } from "./constants.js";
import { isEnterprise } from './mode.js'; import { isEnterprise } from "./mode.js";
import type { AuditLogContext, AuditLogParam } from "./audit.js";
export abstract class BaseController { export abstract class BaseController {
@Inject() @Inject()
@@ -41,7 +41,7 @@ export abstract class BaseController {
getUserId() { getUserId() {
const userId = this.ctx.user?.id; const userId = this.ctx.user?.id;
if (userId == null) { if (userId == null) {
throw new Error('Token已过期'); throw new Error("Token已过期");
} }
return userId; return userId;
} }
@@ -49,7 +49,7 @@ export abstract class BaseController {
getLoginUser() { getLoginUser() {
const user = this.ctx.user; const user = this.ctx.user;
if (user == null) { if (user == null) {
throw new Error('Token已过期'); throw new Error("Token已过期");
} }
return user; return user;
} }
@@ -61,73 +61,110 @@ export abstract class BaseController {
} }
} }
async getProjectId(permission:string) { async getProjectId(permission: string) {
if (!isEnterprise()) { if (!isEnterprise()) {
return undefined return undefined;
} }
let projectIdStr = this.ctx.headers["project-id"] as string; let projectIdStr = this.ctx.headers["project-id"] as string;
if (!projectIdStr){ if (!projectIdStr) {
projectIdStr = this.ctx.request.query["projectId"] as string; projectIdStr = this.ctx.request.query["projectId"] as string;
} }
if (!projectIdStr) { if (!projectIdStr) {
//这里必须抛异常,否则可能会有权限问题 //这里必须抛异常,否则可能会有权限问题
throw new Error("projectId 不能为空") throw new Error("projectId 不能为空");
} }
const userId = this.getUserId() const userId = this.getUserId();
const projectId = parseInt(projectIdStr) const projectId = parseInt(projectIdStr);
await this.checkProjectPermission(userId, projectId,permission) await this.checkProjectPermission(userId, projectId, permission);
return projectId; return projectId;
} }
async getProjectUserId(permission:string){ async getProjectUserId(permission: string) {
let userId = this.getUserId() let userId = this.getUserId();
const projectId = await this.getProjectId(permission) const projectId = await this.getProjectId(permission);
if(projectId){ if (projectId) {
userId = -1 // 企业管理模式下,用户id固定-1 userId = -1; // 企业管理模式下,用户id固定-1
} }
return { return {
projectId,userId projectId,
} userId,
};
} }
async getProjectUserIdRead(){ async getProjectUserIdRead() {
return await this.getProjectUserId("read") return await this.getProjectUserId("read");
} }
async getProjectUserIdWrite(){ async getProjectUserIdWrite() {
return await this.getProjectUserId("write") return await this.getProjectUserId("write");
} }
async getProjectUserIdAdmin(){ async getProjectUserIdAdmin() {
return await this.getProjectUserId("admin") return await this.getProjectUserId("admin");
} }
async checkProjectPermission(userId: number, projectId: number,permission:string) { async checkProjectPermission(userId: number, projectId: number, permission: string) {
const projectService:any = await this.applicationContext.getAsync("projectService"); const projectService: any = await this.applicationContext.getAsync("projectService");
await projectService.checkPermission({userId,projectId,permission}) await projectService.checkPermission({ userId, projectId, permission });
} }
/** /**
* *
* @param service * @param service
* @param id * @param id
*/ */
async checkOwner(service:any,id:number,permission:string,allowAdmin:boolean = false){ async checkOwner(service: any, id: number, permission: string, allowAdmin: boolean = false) {
let { projectId,userId } = await this.getProjectUserId(permission) const { projectId, userId } = await this.getProjectUserId(permission);
const authService:any = await this.applicationContext.getAsync("authService"); const authService: any = await this.applicationContext.getAsync("authService");
if (projectId) { if (projectId) {
await authService.checkProjectId(service, id, projectId); await authService.checkProjectId(service, id, projectId);
}else{ } else {
if (userId === Constants.systemUserId) {
if(userId === Constants.systemUserId){
//系统级别,不检查权限 //系统级别,不检查权限
}else{ } else {
if(allowAdmin){ if (allowAdmin) {
await authService.checkUserIdButAllowAdmin(this.ctx, service, id); await authService.checkUserIdButAllowAdmin(this.ctx, service, id);
}else{ } else {
await authService.checkUserId( service, id, userId); await authService.checkUserId(service, id, userId);
} }
} }
} }
return {projectId,userId} return { projectId, userId };
} }
getAuditType(): string {
return "unknown";
}
auditLog(bean: AuditLogParam = {}) {
const auditLog = this.ensureAuditLogContext();
auditLog.enabled = true;
if (bean.userId != null) {
auditLog.userId = bean.userId;
}
if (bean.username != null) {
auditLog.username = bean.username;
}
if (bean.type != null) {
auditLog.type = bean.type;
}
if (bean.action != null) {
auditLog.action = bean.action;
}
if (bean.projectId != null) {
auditLog.projectId = bean.projectId;
}
if (bean.content) {
auditLog.content = bean.content;
}
if (bean.append) {
const items = Array.isArray(bean.append) ? bean.append : [bean.append];
const old = Array.isArray(auditLog.append) ? auditLog.append : auditLog.append ? [auditLog.append] : [];
auditLog.append = [...old, ...items].filter(item => item && String(item).trim());
}
}
private ensureAuditLogContext(): AuditLogContext {
if (!this.ctx.auditLog) {
this.ctx.auditLog = {};
}
return this.ctx.auditLog;
}
} }
@@ -56,7 +56,7 @@ export abstract class BaseService<T> {
return dataSource.getRepository(entity); return dataSource.getRepository(entity);
} }
protected buildUserProjectQuery(userId: number, projectId?: number) { public buildUserProjectQuery(userId: number, projectId?: number) {
const query: { userId: number; projectId?: number; [key: string]: any } = { const query: { userId: number; projectId?: number; [key: string]: any } = {
userId, userId,
}; };
@@ -102,6 +102,18 @@ export abstract class BaseService<T> {
}); });
} }
/**
* update
*/
async updateWhere(where: any, data: any) {
await this.getRepository().update(
{
...where,
},
data
);
}
/** /**
* *
* @param ids ID集合 [1,2,3] 1,2,3 * @param ids ID集合 [1,2,3] 1,2,3
@@ -253,7 +265,6 @@ export abstract class BaseService<T> {
if (!Array.isArray(ids)) { if (!Array.isArray(ids)) {
ids = [ids]; ids = [ids];
} }
ids = this.filterIds(ids);
const res = await this.getRepository().find({ const res = await this.getRepository().find({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
@@ -266,7 +277,7 @@ export abstract class BaseService<T> {
}, },
}); });
if (!res || res.length === ids.length) { if (!res || res.length === ids.length) {
return; return ids;
} }
throw new PermissionException("权限不足"); throw new PermissionException("权限不足");
} }
@@ -279,7 +290,13 @@ export abstract class BaseService<T> {
return item != null && item != ""; 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); ids = this.filterIds(ids);
if (userId != null) { if (userId != null) {
const userProjectQuery = this.buildUserProjectQuery(userId, projectId); const userProjectQuery = this.buildUserProjectQuery(userId, projectId);
@@ -295,6 +312,7 @@ export abstract class BaseService<T> {
} }
await this.delete(ids); await this.delete(ids);
return ids.length;
} }
async findOne(options: FindOneOptions<T>) { async findOne(options: FindOneOptions<T>) {
+35 -35
View File
@@ -1,21 +1,21 @@
export const Constants = { export const Constants = {
dataDir: './data', dataDir: "./data",
role: { role: {
defaultUser: 3, defaultUser: 3,
}, },
per: { per: {
//无需登录 //无需登录
guest: '_guest_', guest: "_guest_",
//无需登录 //无需登录
anonymous: '_guest_', anonymous: "_guest_",
//无需登录,有 token 时解析当前用户 //无需登录,有 token 时解析当前用户
guestOptionalAuth: '_guestOptionalAuth_', guestOptionalAuth: "_guestOptionalAuth_",
//仅需要登录 //仅需要登录
authOnly: '_authOnly_', authOnly: "_authOnly_",
//仅需要登录 //仅需要登录
loginOnly: '_authOnly_', loginOnly: "_authOnly_",
open: '_open_', open: "_open_",
}, },
res: { res: {
serverError(message: string) { serverError(message: string) {
@@ -26,102 +26,102 @@ export const Constants = {
}, },
error: { error: {
code: 1, code: 1,
message: 'Internal server error', message: "Internal server error",
}, },
success: { success: {
code: 0, code: 0,
message: 'success', message: "success",
}, },
validation: { validation: {
code: 10, code: 10,
message: '参数错误', message: "参数错误",
}, },
needvip: { needvip: {
code: 88, code: 88,
message: '需要VIP', message: "需要VIP",
}, },
needsuite: { needsuite: {
code: 89, code: 89,
message: '需要购买或升级套餐', message: "需要购买或升级套餐",
}, },
loginError: { loginError: {
code: 2, code: 2,
message: '登录失败', message: "登录失败",
}, },
codeError: { codeError: {
code: 3, code: 3,
message: '验证码错误', message: "验证码错误",
}, },
auth: { auth: {
code: 401, code: 401,
message: '您还未登录或token已过期', message: "您还未登录或token已过期",
}, },
permission: { permission: {
code: 402, code: 402,
message: '您没有权限', message: "您没有权限",
}, },
param: { param: {
code: 400, code: 400,
message: '参数错误', message: "参数错误",
}, },
notFound: { notFound: {
code: 404, code: 404,
message: '页面/文件/资源不存在', message: "页面/文件/资源不存在",
}, },
preview: { preview: {
code: 10001, code: 10001,
message: '对不起,预览环境不允许修改此数据', message: "对不起,预览环境不允许修改此数据",
}, },
siteOff:{ siteOff: {
code: 10010, code: 10010,
message: '站点已关闭', message: "站点已关闭",
}, },
need2fa:{ need2fa: {
code: 10020, code: 10020,
message: '需要2FA认证', message: "需要2FA认证",
}, },
openKeyError: { openKeyError: {
code: 20000, code: 20000,
message: 'ApiToken错误', message: "ApiToken错误",
}, },
openKeySignError: { openKeySignError: {
code: 20001, code: 20001,
message: 'ApiToken签名错误', message: "ApiToken签名错误",
}, },
openKeyExpiresError: { openKeyExpiresError: {
code: 20002, code: 20002,
message: 'ApiToken时间戳错误', message: "ApiToken时间戳错误",
}, },
openKeySignTypeError: { openKeySignTypeError: {
code: 20003, code: 20003,
message: 'ApiToken签名类型不支持', message: "ApiToken签名类型不支持",
}, },
openParamError: { openParamError: {
code: 20010, code: 20010,
message: '请求参数错误', message: "请求参数错误",
}, },
openCertNotFound: { openCertNotFound: {
code: 20011, code: 20011,
message: '证书不存在', message: "证书不存在",
}, },
openCertNotReady: { openCertNotReady: {
code: 20012, code: 20012,
message: '证书还未生成', message: "证书还未生成",
}, },
openCertApplying: { openCertApplying: {
code: 20013, code: 20013,
message: '证书正在申请中,请稍后重新获取', message: "证书正在申请中,请稍后重新获取",
}, },
openDomainNoVerifier:{ openDomainNoVerifier: {
code: 20014, code: 20014,
message: '域名校验方式未配置', message: "域名校验方式未配置",
}, },
openEmailNotFound: { openEmailNotFound: {
code: 20021, code: 20021,
message: '用户邮箱还未配置', message: "用户邮箱还未配置",
}, },
}, },
systemUserId: 0, // 系统级别userid固定为0 systemUserId: 0, // 系统级别userid固定为0
enterpriseUserId: -1 // 企业模式用户id固定为-1 enterpriseUserId: -1, // 企业模式用户id固定为-1
}; };
@@ -0,0 +1,20 @@
import { createRequestParamDecorator } from "@midwayjs/core";
export const AuditLog = (opts: { type?: string; action?: string; content?: string; enabled?: boolean } = {}) => {
return createRequestParamDecorator(ctx => {
if (!ctx.auditLog) {
ctx.auditLog = {};
}
ctx.auditLog.enabled = opts.enabled !== false;
if (opts.type != null) {
ctx.auditLog.type = opts.type;
}
if (opts.action != null) {
ctx.auditLog.action = opts.action;
}
if (opts.content != null) {
ctx.auditLog.content = opts.content;
}
return ctx.auditLog;
});
};
@@ -0,0 +1 @@
export * from "./decoractor.js";
@@ -1,11 +1,11 @@
import { ALL, Body, Post, Query } from '@midwayjs/core'; import { ALL, Body, Post, Query } from "@midwayjs/core";
import { BaseController } from './base-controller.js'; import { BaseController } from "./base-controller.js";
export abstract class CrudController<T> extends BaseController { export abstract class CrudController<T> extends BaseController {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
abstract getService<T>(); abstract getService<T>();
@Post('/page') @Post("/page")
async page(@Body(ALL) body: any) { async page(@Body(ALL) body: any) {
const pageRet = await this.getService().page({ const pageRet = await this.getService().page({
query: body.query ?? {}, query: body.query ?? {},
@@ -16,7 +16,7 @@ export abstract class CrudController<T> extends BaseController {
return this.ok(pageRet); return this.ok(pageRet);
} }
@Post('/list') @Post("/list")
async list(@Body(ALL) body: any) { async list(@Body(ALL) body: any) {
const listRet = await this.getService().list({ const listRet = await this.getService().list({
query: body.query ?? {}, query: body.query ?? {},
@@ -25,33 +25,33 @@ export abstract class CrudController<T> extends BaseController {
return this.ok(listRet); return this.ok(listRet);
} }
@Post('/add') @Post("/add")
async add(@Body(ALL) bean: any) { async add(@Body(ALL) bean: any) {
delete bean.id; delete bean.id;
const id = await this.getService().add(bean); const id = await this.getService().add(bean);
return this.ok(id); return this.ok(id);
} }
@Post('/info') @Post("/info")
async info(@Query('id') id: number) { async info(@Query("id") id: number) {
const bean = await this.getService().info(id); const bean = await this.getService().info(id);
return this.ok(bean); return this.ok(bean);
} }
@Post('/update') @Post("/update")
async update(@Body(ALL) bean: any) { async update(@Body(ALL) bean: any) {
await this.getService().update(bean); await this.getService().update(bean);
return this.ok(null); return this.ok(null);
} }
@Post('/delete') @Post("/delete")
async delete(@Query('id') id: number) { async delete(@Query("id") id: number) {
await this.getService().delete([id]); await this.getService().delete([id]);
return this.ok(null); return this.ok(null);
} }
@Post('/deleteByIds') @Post("/deleteByIds")
async deleteByIds(@Body('ids') ids: number[]) { async deleteByIds(@Body("ids") ids: number[]) {
await this.getService().delete(ids); await this.getService().delete(ids);
return this.ok(null); return this.ok(null);
} }
@@ -1,19 +1,17 @@
import { Constants } from '../constants.js'; import { Constants } from "../constants.js";
import { BaseException } from './base-exception.js'; import { BaseException } from "./base-exception.js";
import { TextException } from "./common-exception.js"; import { TextException } from "./common-exception.js";
/** /**
* *
*/ */
export class AuthException extends BaseException { export class AuthException extends BaseException {
constructor(message?:string) { constructor(message?: string) {
super('AuthException', Constants.res.auth.code, message ? message : Constants.res.auth.message); super("AuthException", Constants.res.auth.code, message ? message : Constants.res.auth.message);
} }
} }
export class Need2FAException extends TextException { export class Need2FAException extends TextException {
constructor(message:string,data:any) { constructor(message: string, data: any) {
super('Need2FAException', Constants.res.need2fa.code, message ? message : Constants.res.need2fa.message,data); super("Need2FAException", Constants.res.need2fa.code, message ? message : Constants.res.need2fa.message, data);
} }
} }
@@ -3,8 +3,8 @@
*/ */
export class BaseException extends Error { export class BaseException extends Error {
code: number; code: number;
data?:any data?: any;
constructor(name: string, code: number, message: string ,data?:any) { constructor(name: string, code: number, message: string, data?: any) {
super(message); super(message);
this.name = name; this.name = name;
this.code = code; this.code = code;
@@ -1,10 +1,10 @@
import { Constants } from '../constants.js'; import { Constants } from "../constants.js";
import { BaseException } from './base-exception.js'; import { BaseException } from "./base-exception.js";
/** /**
* *
*/ */
export class CodeErrorException extends BaseException { export class CodeErrorException extends BaseException {
constructor(message) { 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);
} }
} }
@@ -1,12 +1,13 @@
export * from './auth-exception.js'; export * from "./auth-exception.js";
export * from './base-exception.js'; export * from "./base-exception.js";
export * from './permission-exception.js'; export * from "./permission-exception.js";
export * from './preview-exception.js'; export * from "./preview-exception.js";
export * from './validation-exception.js'; export * from "./validation-exception.js";
export * from './vip-exception.js'; export * from "./vip-exception.js";
export * from './common-exception.js'; export * from "./common-exception.js";
export * from './not-found-exception.js'; export * from "./not-found-exception.js";
export * from './param-exception.js'; export * from "./param-exception.js";
export * from './site-off-exception.js'; export * from "./site-off-exception.js";
export * from './login-error-exception.js' export * from "./login-error-exception.js";
export * from './code-error-exception.js' export * from "./code-error-exception.js";
export * from "./non-retryable-exception.js";
@@ -1,12 +1,14 @@
import { Constants } from '../constants.js'; import { Constants } from "../constants.js";
import { BaseException } from './base-exception.js'; import { BaseException } from "./base-exception.js";
/** /**
* *
*/ */
export class LoginErrorException extends BaseException { export class LoginErrorException extends BaseException {
leftCount: number; leftCount: number;
constructor(message, leftCount: number) { userId?: number;
super('LoginErrorException', Constants.res.loginError.code, message ? message : Constants.res.loginError.message); constructor(message, leftCount: number, userId?: number) {
super("LoginErrorException", Constants.res.loginError.code, message ? message : Constants.res.loginError.message);
this.leftCount = leftCount; this.leftCount = leftCount;
this.userId = userId;
} }
} }
@@ -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 { Constants } from "../constants.js";
import { BaseException } from './base-exception.js'; import { BaseException } from "./base-exception.js";
/** /**
* *
*/ */
export class NotFoundException extends BaseException { export class NotFoundException extends BaseException {
constructor(message) { 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 { Constants } from "../constants.js";
import { BaseException } from './base-exception.js'; import { BaseException } from "./base-exception.js";
/** /**
* *
*/ */
export class ParamException extends BaseException { export class ParamException extends BaseException {
constructor(message) { 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 { Constants } from "../constants.js";
import { BaseException } from './base-exception.js'; import { BaseException } from "./base-exception.js";
/** /**
* *
*/ */
export class PermissionException extends BaseException { export class PermissionException extends BaseException {
constructor(message?: string) { 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 { Constants } from "../constants.js";
import { BaseException } from './base-exception.js'; import { BaseException } from "./base-exception.js";
/** /**
* *
*/ */
export class PreviewException extends BaseException { export class PreviewException extends BaseException {
constructor(message) { constructor(message) {
super( super("PreviewException", Constants.res.preview.code, message ? message : Constants.res.preview.message);
'PreviewException',
Constants.res.preview.code,
message ? message : Constants.res.preview.message
);
} }
} }
@@ -1,9 +1,9 @@
import { Constants } from '../constants.js'; import { Constants } from "../constants.js";
import { BaseException } from './base-exception.js'; import { BaseException } from "./base-exception.js";
/** /**
*/ */
export class SiteOffException extends BaseException { export class SiteOffException extends BaseException {
constructor(message) { 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 { Constants } from "../constants.js";
import { BaseException } from './base-exception.js'; import { BaseException } from "./base-exception.js";
/** /**
* *
*/ */
export class ValidateException extends BaseException { export class ValidateException extends BaseException {
constructor(message) { 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 { Constants } from "../constants.js";
import { BaseException } from './base-exception.js'; import { BaseException } from "./base-exception.js";
/** /**
* vip异常 * vip异常
*/ */
export class NeedVIPException extends BaseException { export class NeedVIPException extends BaseException {
constructor(message) { 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 { export class NeedSuiteException extends BaseException {
constructor(message) { 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);
} }
} }
+10 -8
View File
@@ -1,8 +1,10 @@
export * from './base-controller.js'; export * from "./base-controller.js";
export * from './constants.js'; export * from "./constants.js";
export * from './crud-controller.js'; export * from "./crud-controller.js";
export * from './enum-item.js'; export * from "./enum-item.js";
export * from './exception/index.js'; export * from "./exception/index.js";
export * from './result.js'; export * from "./result.js";
export * from './base-service.js'; export * from "./base-service.js";
export * from "./mode.js" export * from "./audit.js";
export * from "./mode.js";
export * from "./core/index.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"){ let adminMode = "saas";
adminMode = mode
export function setAdminMode(mode: string = "saas") {
adminMode = mode;
} }
export function getAdminMode(){ export function getAdminMode() {
return adminMode return adminMode;
} }
export function isEnterprise(){ export function isEnterprise() {
return adminMode === "enterprise" const isEnterprise = adminMode === "enterprise";
} if (!isPlus()) {
if (isEnterprise) {
logger.warn("不是VIP,无法使用企业项目管理功能,退回普通模式");
}
return false;
}
return isEnterprise;
}
@@ -1,11 +1,11 @@
import type { IMidwayContainer } from '@midwayjs/core'; import type { IMidwayContainer } from "@midwayjs/core";
import { Configuration } from '@midwayjs/core'; import { Configuration } from "@midwayjs/core";
import { logger } from '@certd/basic'; import { logger } from "@certd/basic";
@Configuration({ @Configuration({
namespace: 'lib-server', namespace: "lib-server",
}) })
export class LibServerConfiguration { export class LibServerConfiguration {
async onReady(container: IMidwayContainer) { 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 { SysSettingsEntity } from "./system/index.js";
import { AccessEntity } from './user/access/entity/access.js'; import { AccessEntity } from "./user/access/entity/access.js";
import { AddonEntity } from "./user/index.js"; import { AddonEntity } from "./user/index.js";
export * from './basic/index.js'; export * from "./basic/index.js";
export * from './system/index.js'; export * from "./system/index.js";
export * from './user/index.js'; export * from "./user/index.js";
export { LibServerConfiguration as Configuration } from './configuration.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/plus-service.js";
export * from './service/file-service.js'; export * from "./service/file-service.js";
export * from './service/encryptor.js'; export * from "./service/encryptor.js";
export * from './service/ocr-service.js'; export * from "./service/ocr-service.js";
export * from './service/executor-queue.js'; export * from "./service/executor-queue.js";
@@ -1,8 +1,8 @@
import crypto from 'crypto'; import crypto from "crypto";
export class Encryptor { export class Encryptor {
secretKey: Buffer; secretKey: Buffer;
constructor(encryptSecret: string, encoding: BufferEncoding = 'base64') { constructor(encryptSecret: string, encoding: BufferEncoding = "base64") {
this.secretKey = Buffer.from(encryptSecret, encoding); this.secretKey = Buffer.from(encryptSecret, encoding);
} }
// 加密函数 // 加密函数
@@ -10,18 +10,18 @@ export class Encryptor {
const iv = crypto.randomBytes(16); // 初始化向量 const iv = crypto.randomBytes(16); // 初始化向量
// const secretKey = crypto.randomBytes(32); // const secretKey = crypto.randomBytes(32);
// const key = Buffer.from(secretKey); // 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); let encrypted = cipher.update(text);
encrypted = Buffer.concat([encrypted, cipher.final()]); encrypted = Buffer.concat([encrypted, cipher.final()]);
return iv.toString('hex') + ':' + encrypted.toString('hex'); return iv.toString("hex") + ":" + encrypted.toString("hex");
} }
// 解密函数 // 解密函数
decrypt(encryptedText: string) { decrypt(encryptedText: string) {
const textParts = encryptedText.split(':'); const textParts = encryptedText.split(":");
const iv = Buffer.from(textParts.shift(), 'hex'); const iv = Buffer.from(textParts.shift(), "hex");
const encrypted = Buffer.from(textParts.join(':'), 'hex'); const encrypted = Buffer.from(textParts.join(":"), "hex");
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(this.secretKey), iv); const decipher = crypto.createDecipheriv("aes-256-cbc", Buffer.from(this.secretKey), iv);
let decrypted = decipher.update(encrypted); let decrypted = decipher.update(encrypted);
decrypted = Buffer.concat([decrypted, decipher.final()]); decrypted = Buffer.concat([decrypted, decipher.final()]);
return decrypted.toString(); return decrypted.toString();
@@ -1,18 +1,18 @@
import { logger } from "@certd/basic"; import { logger } from "@certd/basic";
export type TaskItem = { export type TaskItem = {
task: ()=>Promise<void>; task: () => Promise<void>;
} };
export class UserTaskQueue{ export class UserTaskQueue {
userId: number; userId: number;
pendingQueue: TaskItem[] = []; pendingQueue: TaskItem[] = [];
runningQueue: TaskItem[] = []; runningQueue: TaskItem[] = [];
getMaxRunningCount: ()=>number ; getMaxRunningCount: () => number;
constructor(req: { userId: number ,getMaxRunningCount: ()=>number }) { constructor(req: { userId: number; getMaxRunningCount: () => number }) {
this.userId = req.userId; this.userId = req.userId;
this.getMaxRunningCount = req.getMaxRunningCount ; this.getMaxRunningCount = req.getMaxRunningCount;
} }
addTask(task: TaskItem) { addTask(task: TaskItem) {
@@ -34,10 +34,10 @@ export class UserTaskQueue{
} }
// 执行任务 // 执行任务
this.runningQueue.push(task); this.runningQueue.push(task);
const call = async ()=>{ const call = async () => {
try{ try {
await task.task(); await task.task();
}finally{ } finally {
// 任务执行完成,从运行队列中移除 // 任务执行完成,从运行队列中移除
const index = this.runningQueue.indexOf(task); const index = this.runningQueue.indexOf(task);
if (index > -1) { if (index > -1) {
@@ -46,17 +46,16 @@ export class UserTaskQueue{
// 继续执行下一个任务 // 继续执行下一个任务
this.runTask(); this.runTask();
} }
} };
logger.info(`[user_${this.userId}]执行任务,当前运行队列:${this.runningQueue.length}, 等待队列:${this.pendingQueue.length}`); logger.info(`[user_${this.userId}]执行任务,当前运行队列:${this.runningQueue.length}, 等待队列:${this.pendingQueue.length}`);
call() call();
} }
} }
export class ExecutorQueue{ export class ExecutorQueue {
queues: Record<number, UserTaskQueue> = {}; queues: Record<number, UserTaskQueue> = {};
maxRunningCount: number = 10; maxRunningCount: number = 10;
setMaxRunningCount(count: number) { setMaxRunningCount(count: number) {
this.maxRunningCount = count; this.maxRunningCount = count;
} }
@@ -64,7 +63,7 @@ export class ExecutorQueue{
getUserQueue(userId: number) { getUserQueue(userId: number) {
const userQueue = this.queues[userId]; const userQueue = this.queues[userId];
if (!userQueue) { if (!userQueue) {
this.queues[userId] = new UserTaskQueue({ userId, getMaxRunningCount: ()=>this.maxRunningCount }); this.queues[userId] = new UserTaskQueue({ userId, getMaxRunningCount: () => this.maxRunningCount });
} }
return this.queues[userId]; return this.queues[userId];
} }
@@ -73,7 +72,6 @@ export class ExecutorQueue{
const userQueue = this.getUserQueue(userId); const userQueue = this.getUserQueue(userId);
userQueue.addTask(task); 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 { Provide, Scope, ScopeEnum } from "@midwayjs/core";
import dayjs from 'dayjs'; import dayjs from "dayjs";
import path from 'path'; import path from "path";
import fs from 'fs'; import fs from "fs";
import { cache, logger, utils } from '@certd/basic'; import { cache, logger, utils } from "@certd/basic";
import { NotFoundException, ParamException, PermissionException } from '../../../basic/index.js'; import { NotFoundException, ParamException, PermissionException } from "../../../basic/index.js";
export type UploadFileItem = { export type UploadFileItem = {
filename: string; filename: string;
tmpFilePath: string; tmpFilePath: string;
}; };
const uploadRootDir = './data/upload'; const uploadRootDir = "./data/upload";
export const uploadTmpFileCacheKey = 'tmpfile_key_'; export const uploadTmpFileCacheKey = "tmpfile_key_";
/** /**
*/ */
@Provide() @Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true }) @Scope(ScopeEnum.Request, { allowDowngrade: true })
export class FileService { 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}`)) { if (tmpCacheKey.startsWith(`/${permission}`)) {
//已经保存过,不需要再次保存 //已经保存过,不需要再次保存
return tmpCacheKey; return tmpCacheKey;
} }
let fileName = ''; let fileName = "";
let tmpFilePath = tmpCacheKey; let tmpFilePath = tmpCacheKey;
if (uploadTmpFileCacheKey && tmpCacheKey.startsWith(uploadTmpFileCacheKey)) { if (uploadTmpFileCacheKey && tmpCacheKey.startsWith(uploadTmpFileCacheKey)) {
const tmpFile: UploadFileItem = cache.get(tmpCacheKey); const tmpFile: UploadFileItem = cache.get(tmpCacheKey);
if (!tmpFile) { if (!tmpFile) {
throw new ParamException('文件已过期,请重新上传'); throw new ParamException("文件已过期,请重新上传");
} }
tmpFilePath = tmpFile.tmpFilePath; tmpFilePath = tmpFile.tmpFilePath;
fileName = tmpFile.filename || path.basename(tmpFilePath); fileName = tmpFile.filename || path.basename(tmpFilePath);
} }
if (!tmpFilePath || !fs.existsSync(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 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}`; const key = `/${permission}/${userIdMd5}/${date}/${random}_${fileName}`;
let savePath = path.join(uploadRootDir, key); let savePath = path.join(uploadRootDir, key);
savePath = path.resolve(savePath); savePath = path.resolve(savePath);
@@ -44,7 +44,6 @@ export class FileService {
if (!fs.existsSync(parentDir)) { if (!fs.existsSync(parentDir)) {
fs.mkdirSync(parentDir, { recursive: true }); fs.mkdirSync(parentDir, { recursive: true });
} }
// eslint-disable-next-line node/no-unsupported-features/node-builtins
const copyFile = utils.promises.promisify(fs.copyFile); const copyFile = utils.promises.promisify(fs.copyFile);
await copyFile(tmpFilePath, savePath); await copyFile(tmpFilePath, savePath);
try { try {
@@ -58,29 +57,29 @@ export class FileService {
getFile(key: string, userId?: number, allowAnyPrivateUser = false) { getFile(key: string, userId?: number, allowAnyPrivateUser = false) {
if (!key) { 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('/')) { if (!key.startsWith("/")) {
throw new ParamException('参数错误'); throw new ParamException("参数错误");
} }
const keyArr = key.split('/'); const keyArr = key.split("/");
const permission = keyArr[1]; const permission = keyArr[1];
const userIdMd5 = keyArr[2]; 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); const userIdInt: number = parseInt(userIdStr, 10);
if (userId == null || userIdInt !== userId) { if (userId == null || userIdInt !== userId) {
throw new PermissionException('无访问权限'); throw new PermissionException("无访问权限");
} }
} }
const filePath = path.join(uploadRootDir, key); const filePath = path.join(uploadRootDir, key);
if (!fs.existsSync(filePath)) { if (!fs.existsSync(filePath)) {
throw new NotFoundException('文件不存在'); throw new NotFoundException("文件不存在");
} }
return filePath; return filePath;
} }
@@ -15,10 +15,9 @@ export class OcrService implements IOcrService {
url: "/activation/certd/ocr", url: "/activation/certd/ocr",
method: "post", method: "post",
data: { data: {
image: opts.image image: opts.image,
} },
}); });
return res; return res;
} }
} }
@@ -164,7 +164,7 @@ export class PlusService {
baseURL: plusRequestService.getBaseURL(), baseURL: plusRequestService.getBaseURL(),
method: "post", method: "post",
headers: { headers: {
Authorization: `Berear ${token}`, Authorization: `Bearer ${token}`,
}, },
}); });
const res = await http.request(config); const res = await http.request(config);
@@ -173,4 +173,9 @@ export class PlusService {
} }
return res.data; return res.data;
} }
async request(config: HttpRequestConfig) {
const plusRequestService = await this.getPlusRequestService();
return await plusRequestService.request(config);
}
} }
+2 -2
View File
@@ -1,2 +1,2 @@
export * from './settings/index.js'; export * from "./settings/index.js";
export * from './basic/index.js'; export * from "./basic/index.js";
@@ -1,33 +1,33 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
/** /**
*/ */
@Entity('sys_settings') @Entity("sys_settings")
export class SysSettingsEntity { export class SysSettingsEntity {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id: number;
@Column({ comment: 'key', length: 100 }) @Column({ comment: "key", length: 100 })
key: string; key: string;
@Column({ comment: '名称', length: 100 }) @Column({ comment: "名称", length: 100 })
title: string; title: string;
@Column({ name: 'setting', comment: '设置', length: 1024, nullable: true }) @Column({ name: "setting", comment: "设置", length: 1024, nullable: true })
setting: string; setting: string;
// public 公开读,私有写, private 私有读,私有写 // public 公开读,私有写, private 私有读,私有写
@Column({ name: 'access', comment: '访问权限' }) @Column({ name: "access", comment: "访问权限" })
access: string; access: string;
@Column({ @Column({
name: 'create_time', name: "create_time",
comment: '创建时间', comment: "创建时间",
default: () => 'CURRENT_TIMESTAMP', default: () => "CURRENT_TIMESTAMP",
}) })
createTime: Date; createTime: Date;
@Column({ @Column({
name: 'update_time', name: "update_time",
comment: '修改时间', comment: "修改时间",
default: () => 'CURRENT_TIMESTAMP', default: () => "CURRENT_TIMESTAMP",
}) })
updateTime: Date; updateTime: Date;
} }
@@ -1,3 +1,3 @@
export * from './service/sys-settings-service.js'; export * from "./service/sys-settings-service.js";
export * from './service/models.js'; export * from "./service/models.js";
export * from './entity/sys-settings.js'; export * from "./entity/sys-settings.js";
@@ -1,19 +1,19 @@
import { cloneDeep } from 'lodash-es'; import { cloneDeep } from "lodash-es";
export class BaseSettings { export class BaseSettings {
static __key__: string; static __key__: string;
static __title__: string; static __title__: string;
static __access__ = 'private'; static __access__ = "private";
static getCacheKey() { static getCacheKey() {
return 'settings.' + this.__key__; return "settings." + this.__key__;
} }
} }
export class SysPublicSettings extends BaseSettings { export class SysPublicSettings extends BaseSettings {
static __key__ = 'sys.public'; static __key__ = "sys.public";
static __title__ = '系统公共设置'; static __title__ = "系统公共设置";
static __access__ = 'public'; static __access__ = "public";
registerEnabled = false; registerEnabled = false;
userValidTimeEnabled?: boolean = false; userValidTimeEnabled?: boolean = false;
@@ -34,19 +34,15 @@ export class SysPublicSettings extends BaseSettings {
aiChatEnabled = true; aiChatEnabled = true;
homePageEnabled = true; homePageEnabled = true;
//验证码是否开启 //验证码是否开启
captchaEnabled = false; captchaEnabled = false;
//验证码类型 //验证码类型
captchaType?: string; captchaType?: string;
captchaAddonId?: number; captchaAddonId?: number;
//流水线是否启用有效期 //流水线是否启用有效期
pipelineValidTimeEnabled?: boolean = false; pipelineValidTimeEnabled?: boolean = false;
//证书域名添加到监控 //证书域名添加到监控
certDomainAddToMonitorEnabled?: boolean = false; certDomainAddToMonitorEnabled?: boolean = false;
@@ -60,12 +56,15 @@ export class SysPublicSettings extends BaseSettings {
// 第三方OAuth配置 // 第三方OAuth配置
oauthEnabled?: boolean = false; oauthEnabled?: boolean = false;
oauthProviders: Record<string, { oauthProviders: Record<
type: string; string,
title: string; {
addonId: number; type: string;
icon?: string; title: string;
}> = {}; addonId: number;
icon?: string;
}
> = {};
notice?: string; notice?: string;
@@ -73,40 +72,37 @@ export class SysPublicSettings extends BaseSettings {
} }
export class SysPrivateSettings extends BaseSettings { export class SysPrivateSettings extends BaseSettings {
static __title__ = '系统私有设置'; static __title__ = "系统私有设置";
static __access__ = 'private'; static __access__ = "private";
static __key__ = 'sys.private'; static __key__ = "sys.private";
jwtKey?: string; jwtKey?: string;
encryptSecret?: string; encryptSecret?: string;
httpsProxy? = ''; httpsProxy? = "";
httpProxy? = ''; httpProxy? = "";
noProxy? = ''; noProxy? = "";
commonHeaders?: string = ''; commonHeaders?: string = "";
reverseProxies?: Record<string, string> = {}; reverseProxies?: Record<string, string> = {};
dnsResultOrder? = ''; dnsResultOrder? = "";
commonCnameEnabled?: boolean = true; commonCnameEnabled?: boolean = true;
httpRequestTimeout?: number = 30; httpRequestTimeout?: number = 30;
pipelineMaxRunningCount?: number; pipelineMaxRunningCount?: number;
environmentVars?: string = "";
environmentVars?: string = '';
acmeWalkFromAuthoritative?: boolean = true; acmeWalkFromAuthoritative?: boolean = true;
sms?: { sms?: {
type?: string; type?: string;
config?: any; config?: any;
} = { } = {
type: 'aliyun', type: "aliyun",
config: {}, config: {},
}; };
removeSecret() { removeSecret() {
const clone = cloneDeep(this); const clone = cloneDeep(this);
@@ -117,9 +113,9 @@ export class SysPrivateSettings extends BaseSettings {
} }
export class SysInstallInfo extends BaseSettings { export class SysInstallInfo extends BaseSettings {
static __title__ = '系统安装信息'; static __title__ = "系统安装信息";
static __key__ = 'sys.install'; static __key__ = "sys.install";
static __access__ = 'private'; static __access__ = "private";
installTime?: number; installTime?: number;
siteId?: string; siteId?: string;
bindUserId?: number; bindUserId?: number;
@@ -130,21 +126,20 @@ export class SysInstallInfo extends BaseSettings {
} }
export class SysLicenseInfo extends BaseSettings { export class SysLicenseInfo extends BaseSettings {
static __title__ = '授权许可信息'; static __title__ = "授权许可信息";
static __key__ = 'sys.license'; static __key__ = "sys.license";
static __access__ = 'private'; static __access__ = "private";
license?: string; license?: string;
} }
export type EmailTemplate = { export type EmailTemplate = {
addonId?: number; addonId?: number;
} };
export class SysEmailConf extends BaseSettings { export class SysEmailConf extends BaseSettings {
static __title__ = '邮箱配置'; static __title__ = "邮箱配置";
static __key__ = 'sys.email'; static __key__ = "sys.email";
static __access__ = 'private'; static __access__ = "private";
host: string; host: string;
port: number; port: number;
@@ -160,18 +155,18 @@ export class SysEmailConf extends BaseSettings {
sender: string; sender: string;
usePlus?: boolean; usePlus?: boolean;
templates:{ templates: {
registerCode?: EmailTemplate, registerCode?: EmailTemplate;
forgotPassword?: EmailTemplate, forgotPassword?: EmailTemplate;
pipelineResult?: EmailTemplate, pipelineResult?: EmailTemplate;
common?: EmailTemplate, common?: EmailTemplate;
} };
} }
export class SysSiteInfo extends BaseSettings { export class SysSiteInfo extends BaseSettings {
static __title__ = '站点信息'; static __title__ = "站点信息";
static __key__ = 'sys.site'; static __key__ = "sys.site";
static __access__ = 'public'; static __access__ = "public";
title?: string; title?: string;
slogan?: string; slogan?: string;
logo?: string; logo?: string;
@@ -179,9 +174,9 @@ export class SysSiteInfo extends BaseSettings {
} }
export class SysSecretBackup extends BaseSettings { export class SysSecretBackup extends BaseSettings {
static __title__ = '密钥信息备份'; static __title__ = "密钥信息备份";
static __key__ = 'sys.secret.backup'; static __key__ = "sys.secret.backup";
static __access__ = 'private'; static __access__ = "private";
siteId?: string; siteId?: string;
encryptSecret?: string; encryptSecret?: string;
} }
@@ -190,9 +185,9 @@ export class SysSecretBackup extends BaseSettings {
* *
*/ */
export class SysSecret extends BaseSettings { export class SysSecret extends BaseSettings {
static __title__ = '密钥信息'; static __title__ = "密钥信息";
static __key__ = 'sys.secret'; static __key__ = "sys.secret";
static __access__ = 'private'; static __access__ = "private";
siteId?: string; siteId?: string;
encryptSecret?: string; encryptSecret?: string;
} }
@@ -215,9 +210,9 @@ export type MenuItem = {
children?: MenuItem[]; children?: MenuItem[];
}; };
export class SysHeaderMenus extends BaseSettings { export class SysHeaderMenus extends BaseSettings {
static __title__ = '顶部菜单'; static __title__ = "顶部菜单";
static __key__ = 'sys.header.menus'; static __key__ = "sys.header.menus";
static __access__ = 'public'; static __access__ = "public";
menus: MenuItem[]; menus: MenuItem[];
} }
@@ -228,9 +223,9 @@ export type PaymentItem = {
}; };
export class SysPaymentSetting extends BaseSettings { export class SysPaymentSetting extends BaseSettings {
static __title__ = '支付设置'; static __title__ = "支付设置";
static __key__ = 'sys.payment'; static __key__ = "sys.payment";
static __access__ = 'private'; static __access__ = "private";
yizhifu?: PaymentItem = { enabled: false }; yizhifu?: PaymentItem = { enabled: false };
@@ -240,9 +235,9 @@ export class SysPaymentSetting extends BaseSettings {
} }
export class SysSuiteSetting extends BaseSettings { export class SysSuiteSetting extends BaseSettings {
static __title__ = '套餐设置'; static __title__ = "套餐设置";
static __key__ = 'sys.suite'; static __key__ = "sys.suite";
static __access__ = 'private'; static __access__ = "private";
enabled: boolean = false; enabled: boolean = false;
@@ -257,26 +252,25 @@ export class SysSuiteSetting extends BaseSettings {
} }
export class SysAutoFixSetting extends BaseSettings { export class SysAutoFixSetting extends BaseSettings {
static __title__ = '自动修复记录'; static __title__ = "自动修复记录";
static __key__ = 'sys.auto.fix'; static __key__ = "sys.auto.fix";
static __access__ = 'private'; static __access__ = "private";
fixed: Record<string, boolean> = {}; fixed: Record<string, boolean> = {};
} }
export type SiteHidden = { export type SiteHidden = {
enabled: boolean; enabled: boolean;
openPath?: string; openPath?: string;
//md5 hash 两次后保存 //md5 hash 两次后保存
openPassword?: string; openPassword?: string;
autoHiddenTimes?: number; autoHiddenTimes?: number;
hiddenOpenApi?: boolean hiddenOpenApi?: boolean;
}; };
export class SysSafeSetting extends BaseSettings { export class SysSafeSetting extends BaseSettings {
static __title__ = '站点安全设置'; static __title__ = "站点安全设置";
static __key__ = 'sys.safe'; static __key__ = "sys.safe";
static __access__ = 'private'; static __access__ = "private";
// 站点隐藏 // 站点隐藏
hidden: SiteHidden = { hidden: SiteHidden = {
@@ -285,3 +279,11 @@ export class SysSafeSetting extends BaseSettings {
autoHiddenTimes: 5, autoHiddenTimes: 5,
}; };
} }
export class SysPluginSetting extends BaseSettings {
static __title__ = "系统插件设置";
static __key__ = "sys.plugin";
static __access__ = "private";
lastSyncTime?: number;
}
@@ -1,15 +1,15 @@
import { Provide, Scope, ScopeEnum } from '@midwayjs/core'; import { Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { InjectEntityModel } from '@midwayjs/typeorm'; import { InjectEntityModel } from "@midwayjs/typeorm";
import { Repository } from 'typeorm'; import { Repository } from "typeorm";
import { SysSettingsEntity } from '../entity/sys-settings.js'; import { SysSettingsEntity } from "../entity/sys-settings.js";
import { BaseSettings, SysInstallInfo, SysPrivateSettings, SysPublicSettings, SysSecret, SysSecretBackup } from './models.js'; import { BaseSettings, SysInstallInfo, SysPrivateSettings, SysPublicSettings, SysSecret, SysSecretBackup } from "./models.js";
import { getAllSslProviderDomains, setSslProviderReverseProxies, setWalkFromAuthoritative } from '@certd/acme-client'; import { getAllSslProviderDomains, setSslProviderReverseProxies, setWalkFromAuthoritative } from "@certd/acme-client";
import { cache, logger, mergeUtils, setGlobalHeaders, setGlobalProxy } from '@certd/basic'; import { cache, logger, mergeUtils, setGlobalHeaders, setGlobalProxy } from "@certd/basic";
import { isPlus } from '@certd/plus-core'; import { isPlus } from "@certd/plus-core";
import * as dns from 'node:dns'; import * as dns from "node:dns";
import { BaseService, setAdminMode } from '../../../basic/index.js'; import { BaseService, setAdminMode } from "../../../basic/index.js";
import { executorQueue } from '../../basic/service/executor-queue.js'; import { executorQueue } from "../../basic/service/executor-queue.js";
const { merge } = mergeUtils; const { merge } = mergeUtils;
let lastSaveEnvVars = {}; let lastSaveEnvVars = {};
@@ -138,7 +138,7 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
res.reverseProxies[domain] = ""; res.reverseProxies[domain] = "";
} }
} }
return res return res;
} }
async savePrivateSettings(bean: SysPrivateSettings) { async savePrivateSettings(bean: SysPrivateSettings) {
@@ -149,14 +149,14 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
} }
async reloadSettings() { async reloadSettings() {
await this.reloadPrivateSettings() await this.reloadPrivateSettings();
await this.reloadPublicSettings() await this.reloadPublicSettings();
} }
async reloadPublicSettings() { async reloadPublicSettings() {
const publicSetting = await this.getPublicSettings() const publicSetting = await this.getPublicSettings();
if (isPlus()){ if (isPlus()) {
setAdminMode(publicSetting.adminMode ) setAdminMode(publicSetting.adminMode);
} }
} }
@@ -169,7 +169,7 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
}; };
setGlobalProxy(opts); setGlobalProxy(opts);
setGlobalHeaders(this.parseKeyValueText(privateSetting.commonHeaders)); setGlobalHeaders(this.parseKeyValueText(privateSetting.commonHeaders));
if (privateSetting.dnsResultOrder) { if (privateSetting.dnsResultOrder) {
dns.setDefaultResultOrder(privateSetting.dnsResultOrder as any); dns.setDefaultResultOrder(privateSetting.dnsResultOrder as any);
} }
@@ -183,29 +183,28 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
this.setEnvironmentVars(privateSetting.environmentVars); this.setEnvironmentVars(privateSetting.environmentVars);
setWalkFromAuthoritative(privateSetting.acmeWalkFromAuthoritative); setWalkFromAuthoritative(privateSetting.acmeWalkFromAuthoritative);
} }
parseKeyValueText(text: string) { parseKeyValueText(text: string) {
const values = {}; const values = {};
if (typeof text !== 'string') { if (typeof text !== "string") {
text = ""; text = "";
} }
text.split('\n').forEach(line => { text.split("\n").forEach(line => {
line = line.trim(); line = line.trim();
if (!line || line.startsWith('#')) { if (!line || line.startsWith("#")) {
return return;
} }
const arr = line.split("#") const arr = line.split("#");
if (arr.length > 0) { if (arr.length > 0) {
line = arr[0].trim(); line = arr[0].trim();
} }
if (!line.includes("=")) { if (!line.includes("=")) {
return return;
} }
const eqIndex = line.indexOf('='); const eqIndex = line.indexOf("=");
const key = line.substring(0, eqIndex).trim(); const key = line.substring(0, eqIndex).trim();
const value = line.substring(eqIndex + 1).trim(); const value = line.substring(eqIndex + 1).trim();
if (key && value) { if (key && value) {
@@ -220,7 +219,7 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
//先删除旧环境变量 //先删除旧环境变量
if (lastSaveEnvVars) { if (lastSaveEnvVars) {
for (const key in 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); entity.setting = JSON.stringify(setting);
await this.repository.save(entity); await this.repository.save(entity);
} else { } else {
throw new Error('该设置不存在'); throw new Error("该设置不存在");
} }
cache.delete(`settings.${key}`); cache.delete(`settings.${key}`);
} }
@@ -246,20 +245,20 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
if (settings == null) { if (settings == null) {
const backup = new SysSecretBackup(); const backup = new SysSecretBackup();
if (installInfo.siteId == null || privateSettings.encryptSecret == null) { if (installInfo.siteId == null || privateSettings.encryptSecret == null) {
logger.error('备份密钥失败,siteId或encryptSecret为空'); logger.error("备份密钥失败,siteId或encryptSecret为空");
return; return;
} }
backup.siteId = installInfo.siteId; backup.siteId = installInfo.siteId;
backup.encryptSecret = privateSettings.encryptSecret; backup.encryptSecret = privateSettings.encryptSecret;
await this.saveSetting(backup); await this.saveSetting(backup);
logger.info('备份密钥成功'); logger.info("备份密钥成功");
} else { } else {
//校验是否有变化 //校验是否有变化
if (settings.siteId !== installInfo.siteId) { if (settings.siteId !== installInfo.siteId) {
throw new Error(`siteId与备份不一致,可能是数据异常,请检查:backup=${settings.siteId}, current=${installInfo.siteId}`); throw new Error(`siteId与备份不一致,可能是数据异常,请检查:backup=${settings.siteId}, current=${installInfo.siteId}`);
} }
if (settings.encryptSecret !== privateSettings.encryptSecret) { 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__); const settings = await this.getSettingByKey(SysSecretBackup.__key__);
if (settings == null || !settings.encryptSecret) { if (settings == null || !settings.encryptSecret) {
throw new Error('密钥备份不存在'); throw new Error("密钥备份不存在");
} }
sysSecret.siteId = settings.siteId; sysSecret.siteId = settings.siteId;
sysSecret.encryptSecret = settings.encryptSecret; sysSecret.encryptSecret = settings.encryptSecret;
await this.saveSetting(sysSecret); await this.saveSetting(sysSecret);
logger.info('密钥恢复成功'); logger.info("密钥恢复成功");
return sysSecret; return sysSecret;
} }
} }

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