From eae7edbda0fd7357163f33fb4084fcba21396f5f Mon Sep 17 00:00:00 2001 From: xiaojunnuo Date: Sun, 2 Aug 2026 23:25:52 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E6=94=AF=E6=8C=81AI=E5=BC=80=E5=8F=91?= =?UTF-8?q?=E5=9C=A8=E7=BA=BF=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +- .trae/skills/certd-online-plugin-dev/SKILL.md | 164 +++++++ .../agents/openai.yaml | 4 + .../examples/DemoAccess.yaml | 37 ++ .../examples/DemoDeploy.yaml | 100 +++++ .../examples/DemoDnsProvider.yaml | 119 +++++ .../references/access-development.md | 19 + .../references/certd-api.md | 82 ++++ .../references/component-examples.md | 423 ++++++++++++++++++ .../references/local-history.md | 25 ++ .../references/online-yaml-format.md | 55 +++ .../skills/access-plugin-dev/SKILL.md | 59 +++ .../skills/dns-provider-dev/SKILL.md | 70 +++ .../skills/task-plugin-dev/SKILL.md | 54 +++ packages/ui/certd-client/src/style/certd.less | 48 +- .../certd-client/src/views/sys/plugin/api.ts | 19 + .../components/plugin-ai-dev-dialog-body.vue | 224 ++++++++++ .../components/plugin-edit-dialog-body.vue | 18 + .../src/views/sys/plugin/crud.tsx | 11 + .../src/views/sys/plugin/use-ai-dev.tsx | 38 ++ .../sys/plugin/plugin-controller.ts | 8 + .../service/builtin-plugin-service.ts | 23 +- .../modules/plugin/service/plugin-service.ts | 117 ++++- 23 files changed, 1713 insertions(+), 8 deletions(-) create mode 100644 .trae/skills/certd-online-plugin-dev/SKILL.md create mode 100644 .trae/skills/certd-online-plugin-dev/agents/openai.yaml create mode 100644 .trae/skills/certd-online-plugin-dev/examples/DemoAccess.yaml create mode 100644 .trae/skills/certd-online-plugin-dev/examples/DemoDeploy.yaml create mode 100644 .trae/skills/certd-online-plugin-dev/examples/DemoDnsProvider.yaml create mode 100644 .trae/skills/certd-online-plugin-dev/references/access-development.md create mode 100644 .trae/skills/certd-online-plugin-dev/references/certd-api.md create mode 100644 .trae/skills/certd-online-plugin-dev/references/component-examples.md create mode 100644 .trae/skills/certd-online-plugin-dev/references/local-history.md create mode 100644 .trae/skills/certd-online-plugin-dev/references/online-yaml-format.md create mode 100644 .trae/skills/certd-online-plugin-dev/skills/access-plugin-dev/SKILL.md create mode 100644 .trae/skills/certd-online-plugin-dev/skills/dns-provider-dev/SKILL.md create mode 100644 .trae/skills/certd-online-plugin-dev/skills/task-plugin-dev/SKILL.md create mode 100644 packages/ui/certd-client/src/views/sys/plugin/components/plugin-ai-dev-dialog-body.vue create mode 100644 packages/ui/certd-client/src/views/sys/plugin/use-ai-dev.tsx diff --git a/.gitignore b/.gitignore index b3c38156f..3e9c3973f 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,6 @@ pnpm-lock.yaml # Certd 推广报告,仅本地使用 /popularize/reports/ output/ -.uploads/ \ No newline at end of file +.uploads/ +.certd-plugin-history/ +.tmp \ No newline at end of file diff --git a/.trae/skills/certd-online-plugin-dev/SKILL.md b/.trae/skills/certd-online-plugin-dev/SKILL.md new file mode 100644 index 000000000..c96fbda74 --- /dev/null +++ b/.trae/skills/certd-online-plugin-dev/SKILL.md @@ -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 地址和当前用户 Token。调用 API 时使用: + +```http +Authorization: +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}/sys/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。保存后检查中文字段不含 `?`,再调用 `/sys/plugin/find` 或 `/sys/plugin/info` 验证。 + +## API 工作流 + +1. 使用 `/sys/plugin/find` 查询插件和 Access,可通过 `keywords` 数组传递多个关键词。 +2. 查询结果包含 `editable`: + - `editable: true`:允许当前 Agent 修改并保存。 + - `editable: false`:只能读取和使用,不能修改。 +3. 读取完整 YAML 时调用 `/sys/plugin/export`。 +4. 所有插件保存统一调用 `/sys/plugin/import`,并始终传递完整 YAML: + - 新插件使用 `override: false`。 + - 已有插件使用 `override: true`;导入接口根据 `author` 和 `name` 定位并覆盖已有记录。 +5. 不使用 `/sys/plugin/add` 或 `/sys/plugin/update` 保存插件,避免保存路径分叉、字段丢失和 Windows 请求兼容性问题。 +6. 保存完成后重新调用 `/sys/plugin/find` 或 `/sys/plugin/info` 验证结果。 + +`/sys/plugin/find` 会在一次请求中分别查询内置插件和 `store` 插件,再合并返回;`store` 插件需按上述字段区分市场插件与本地插件。 + +详细请求字段见 `references/certd-api.md`。 + +## Access 协作 + +开发 Task 或 DNS Provider 前,先用 `/sys/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、环境变量和系统设置。 +- 只使用脱敏示例数据和公开文档。 +- 不要自动发布;保存、测试、审核和发布由用户确认。 diff --git a/.trae/skills/certd-online-plugin-dev/agents/openai.yaml b/.trae/skills/certd-online-plugin-dev/agents/openai.yaml new file mode 100644 index 000000000..7f732610c --- /dev/null +++ b/.trae/skills/certd-online-plugin-dev/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Certd 在线插件开发" + short_description: "通过本地 Agent Server 使用 Codex 或 Trae 开发 Certd 在线插件" + default_prompt: "启动本地 Agent Server,读取插件 YAML 和对应类型规范,按需求修改 content 并返回完整 YAML 草稿。" diff --git a/.trae/skills/certd-online-plugin-dev/examples/DemoAccess.yaml b/.trae/skills/certd-online-plugin-dev/examples/DemoAccess.yaml new file mode 100644 index 000000000..94e43a034 --- /dev/null +++ b/.trae/skills/certd-online-plugin-dev/examples/DemoAccess.yaml @@ -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; + } diff --git a/.trae/skills/certd-online-plugin-dev/examples/DemoDeploy.yaml b/.trae/skills/certd-online-plugin-dev/examples/DemoDeploy.yaml new file mode 100644 index 000000000..b4434c121 --- /dev/null +++ b/.trae/skills/certd-online-plugin-dev/examples/DemoDeploy.yaml @@ -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 //设置输出参数,可以被其他插件选择使用 + } + } diff --git a/.trae/skills/certd-online-plugin-dev/examples/DemoDnsProvider.yaml b/.trae/skills/certd-online-plugin-dev/examples/DemoDnsProvider.yaml new file mode 100644 index 000000000..badb95741 --- /dev/null +++ b/.trae/skills/certd-online-plugin-dev/examples/DemoDnsProvider.yaml @@ -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> { + 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> { + 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, + }; + } + } + diff --git a/.trae/skills/certd-online-plugin-dev/references/access-development.md b/.trae/skills/certd-online-plugin-dev/references/access-development.md new file mode 100644 index 000000000..0e638904d --- /dev/null +++ b/.trae/skills/certd-online-plugin-dev/references/access-development.md @@ -0,0 +1,19 @@ +# Access 开发规范 + +Access 插件负责保存授权配置,也负责封装平台 API/SDK,供 Task 和 DNS Provider 复用。 + +## 查询顺序 + +1. 调用 `/sys/plugin/find`,使用 `pluginType: access`。 +2. 根据 `name`、`author`、`fullName` 识别目标 Access。 +3. 使用 `/sys/plugin/export` 读取完整 YAML。 +4. 检查 `content` 中已经提供的方法。 + +## 修改规则 + +- Access 的 `editable: true` 时才允许修改。 +- 修改前先保存本地历史。 +- 优先把通用 API/SDK 能力放入 Access。 +- 业务插件通过 `dependPlugins` 依赖 Access。 +- `editable: false` 时不要尝试修改 Access,在业务插件内部实现必要的调用。 +- 不要在日志中打印完整授权配置。 diff --git a/.trae/skills/certd-online-plugin-dev/references/certd-api.md b/.trae/skills/certd-online-plugin-dev/references/certd-api.md new file mode 100644 index 000000000..2478d42f7 --- /dev/null +++ b/.trae/skills/certd-online-plugin-dev/references/certd-api.md @@ -0,0 +1,82 @@ +# Certd API + +以下接口都以前端生成提示词中的 API 地址为基础地址,并使用 `Authorization` 请求头。 + +## 查询插件 + +```http +POST /sys/plugin/find +Content-Type: application/json +Authorization: +``` + +请求示例: + +```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 时再调用 `/sys/plugin/export`。 + +## 读取插件信息 + +```http +POST /sys/plugin/info?id=12 +Authorization: +``` + +## 导出完整 YAML + +```http +POST /sys/plugin/export +Content-Type: application/json +Authorization: +``` + +```json +{ + "id": 12 +} +``` + +## 保存插件 + +已有插件使用: + +```http +POST /sys/plugin/update +``` + +新插件使用: + +```http +POST /sys/plugin/add +``` + +也可以使用完整 YAML 导入: + +```http +POST /sys/plugin/import +``` + +```json +{ + "content": "完整 YAML", + "override": true, + "type": "store" +} +``` + +保存后重新调用 `/sys/plugin/find` 或 `/sys/plugin/info` 验证。 diff --git a/.trae/skills/certd-online-plugin-dev/references/component-examples.md b/.trae/skills/certd-online-plugin-dev/references/component-examples.md new file mode 100644 index 000000000..ac3b15d21 --- /dev/null +++ b/.trae/skills/certd-online-plugin-dev/references/component-examples.md @@ -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..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", +} +``` diff --git a/.trae/skills/certd-online-plugin-dev/references/local-history.md b/.trae/skills/certd-online-plugin-dev/references/local-history.md new file mode 100644 index 000000000..945e5c8f1 --- /dev/null +++ b/.trae/skills/certd-online-plugin-dev/references/local-history.md @@ -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。 +- 恢复后通过 `/sys/plugin/update` 或 `/sys/plugin/import` 写回 Certd。 diff --git a/.trae/skills/certd-online-plugin-dev/references/online-yaml-format.md b/.trae/skills/certd-online-plugin-dev/references/online-yaml-format.md new file mode 100644 index 000000000..985745a4c --- /dev/null +++ b/.trae/skills/certd-online-plugin-dev/references/online-yaml-format.md @@ -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` 非空、版本和插件类型没有被意外修改。 diff --git a/.trae/skills/certd-online-plugin-dev/skills/access-plugin-dev/SKILL.md b/.trae/skills/certd-online-plugin-dev/skills/access-plugin-dev/SKILL.md new file mode 100644 index 000000000..a459fedbc --- /dev/null +++ b/.trae/skills/certd-online-plugin-dev/skills/access-plugin-dev/SKILL.md @@ -0,0 +1,59 @@ +--- +name: certd-online-access-plugin-dev +description: 用于开发 Certd 在线 Access 插件。输出完整 YAML,content 中返回继承 BaseAccess 的 class,并在 input 中声明授权字段。 +--- + +# 在线 Access 插件 + +读取父 Skill 的 `references/online-yaml-format.md`。不要沿用旧版 `@IsAccess`、`@AccessInput` 装饰器和独立 TypeScript 文件。 + +## 输出结构 + +- `pluginType` 使用 `access`。 +- `input` 中声明用户需要填写的授权字段。 +- 敏感字段在 input 中设置加密或密码类组件。 +- `content` 中实现授权 class 和 API 方法。 + +## `content` 模板 + +```javascript +const { BaseAccess } = await _ctx.import("@certd/pipeline") + +return class DemoAccess extends BaseAccess { + demoKeyId + demoKeySecret + + async onTestRequest() { + await this.getDomainList({ searchKey: "" }) + return "ok" + } + + async getDomainList(req) { + this.logger.info("获取域名列表", { searchKey: req.searchKey }) + const res = await this.ctx.http.request({ + url: "https://api.example.com/domains", + method: "GET", + params: { keyword: req.searchKey }, + }) + if (res.error) { + throw new Error(`获取域名列表失败: ${res.message}`) + } + return { + total: res.data?.total || 0, + list: res.data?.list || [], + } + } +} +``` + +## 编写要求 + +- 统一使用 `await _ctx.import(...)` 加载模块。 +- 使用 `_ctx.import("/@/...")` 通过绝对路径加载 `server/src/` 下的模块,`/@` 代表 `server/src` 根路径。 +- 需要记录模块加载信息时使用 `_ctx.logger`,访问执行日志使用 `this.logger`。 +- 返回继承 `BaseAccess` 的 class,不使用装饰器。 +- class 属性名必须与 YAML `input` 字段一致。 +- 所有敏感授权值只通过 `this` 和 Certd 授权上下文使用,不打印真实值。 +- `onTestRequest` 应调用实际 API 方法并在失败时抛出异常。 +- 对外 API 方法应统一处理分页、错误和返回字段。 +- 使用 `this.logger` 或框架提供的 logger,禁止 `console.log`。 diff --git a/.trae/skills/certd-online-plugin-dev/skills/dns-provider-dev/SKILL.md b/.trae/skills/certd-online-plugin-dev/skills/dns-provider-dev/SKILL.md new file mode 100644 index 000000000..ad56ebbd0 --- /dev/null +++ b/.trae/skills/certd-online-plugin-dev/skills/dns-provider-dev/SKILL.md @@ -0,0 +1,70 @@ +--- +name: certd-online-dns-provider-dev +description: 用于开发 Certd 在线 DNS Provider 插件。输出完整 YAML,content 中返回继承 AbstractDnsProvider 的 class。 +--- + +# 在线 DNS Provider 插件 + +读取父 Skill 的 `references/online-yaml-format.md`。不要沿用旧版 `@IsDnsProvider` 装饰器和独立 TypeScript 文件。 + +## 输出结构 + +- `pluginType` 使用 `dnsProvider`。 +- `input` 中配置授权选择、域名或平台所需的参数。 +- `content` 中实现创建和删除 DNS 记录的 class。 + +## `content` 模板 + +```javascript +const { AbstractDnsProvider } = await _ctx.import("@certd/pipeline") +const { DemoAccess } = await _ctx.import("/@/plugins/plugin-lib/demo/access/index.js") +_ctx.logger.info("DemoAccess:", DemoAccess) + +return class DemoDnsProvider extends AbstractDnsProvider { + accessId + + async onInstance() { + this.access = await this.getAccess(this.accessId) + } + + async createRecord(options) { + const { fullRecord, value, type, domain } = options + this.logger.info("添加 DNS 记录", { fullRecord, type, domain }) + const res = await this.ctx.http.request({ + url: "https://api.example.com/dns/records", + method: "POST", + data: { fullRecord, value, type, domain }, + }) + if (res.error) { + throw new Error(`创建 DNS 记录失败: ${res.message}`) + } + return res.data + } + + async removeRecord(options) { + const { fullRecord, value, domain } = options.recordReq + const res = await this.ctx.http.request({ + url: "https://api.example.com/dns/records", + method: "DELETE", + data: { fullRecord, value, domain }, + }) + if (res.error) { + this.logger.warn("删除 DNS 记录失败", res.message) + return + } + this.logger.info("删除 DNS 记录成功", fullRecord) + } +} +``` + +## 编写要求 + +- 统一使用 `await _ctx.import(...)` 加载模块。 +- 使用 `_ctx.import("/@/...")` 通过绝对路径加载 `server/src/` 下的模块,`/@` 代表 `server/src` 根路径。 +- 需要记录模块加载信息时使用 `_ctx.logger`。 +- 返回继承 `AbstractDnsProvider` 的 class。 +- `createRecord` 必须返回删除时需要的记录信息。 +- `removeRecord` 使用 `options.recordReq` 和 `options.recordRes`。 +- 只处理业务 API 所需的 TXT 记录参数,不在日志中输出授权密钥。 +- 网络失败、授权失败和 API 业务失败要有明确日志;创建失败必须抛出异常。 +- 保持创建和删除幂等,避免清理失败阻断无关流程。 diff --git a/.trae/skills/certd-online-plugin-dev/skills/task-plugin-dev/SKILL.md b/.trae/skills/certd-online-plugin-dev/skills/task-plugin-dev/SKILL.md new file mode 100644 index 000000000..8a41cab15 --- /dev/null +++ b/.trae/skills/certd-online-plugin-dev/skills/task-plugin-dev/SKILL.md @@ -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 `/sys/plugin/update` 或 `/sys/plugin/import` 保存。 diff --git a/packages/ui/certd-client/src/style/certd.less b/packages/ui/certd-client/src/style/certd.less index cd74d033a..01be31fd7 100644 --- a/packages/ui/certd-client/src/style/certd.less +++ b/packages/ui/certd-client/src/style/certd.less @@ -4,6 +4,7 @@ align-items: center; font-size: 16px; } + .fs-icon { font-size: 16px; } @@ -12,6 +13,7 @@ display: inline-flex; justify-content: center; align-items: center; + .fs-iconify { margin-right: 5px; } @@ -191,6 +193,7 @@ body a { color: #1890ff; + &:hover { color: #40a9ff; } @@ -216,10 +219,12 @@ span.fs-icon-svg { &:first-child { margin-right: 5px; } + &:last-child { margin-left: 5px; } } + .fs-icon, .fs-button-icon { margin: 0 !important; @@ -252,13 +257,48 @@ button.ant-btn.ant-btn-default.isPlus { } .fs-form-none-content { - .fs-form-wrapper{ - .fs-form-content{display: none;} + .fs-form-wrapper { + .fs-form-content { + display: none; + } } } -.fs-actionbar{ +.fs-actionbar { display: flex; align-items: center; - gap:4px; + gap: 4px; } + +.fs-icon-image-container { + display: inline-flex; + justify-content: center; + align-items: center; + + img { + &.font-size-16 { + width: 16px; + height: 16px; + } + + &.font-size-18 { + width: 18px; + height: 18px; + } + + &.font-size-20 { + width: 20px; + height: 20px; + } + + &.font-size-24 { + width: 24px; + height: 24px; + } + + &.font-size-32 { + width: 32px; + height: 32px; + } + } +} \ No newline at end of file diff --git a/packages/ui/certd-client/src/views/sys/plugin/api.ts b/packages/ui/certd-client/src/views/sys/plugin/api.ts index 426b8e8bf..4f16ff341 100644 --- a/packages/ui/certd-client/src/views/sys/plugin/api.ts +++ b/packages/ui/certd-client/src/views/sys/plugin/api.ts @@ -10,6 +10,24 @@ export async function GetList(query: any) { }); } +export async function FindPlugins(query: { + type?: "builtIn" | "store"; + pluginType?: string; + group?: string; + name?: string; + author?: string; + keyword?: string; + keywords?: string[]; + includeBuiltIn?: boolean; + includeStore?: boolean; +}) { + return await request({ + url: apiPrefix + "/find", + method: "post", + data: query, + }); +} + export async function AddObj(obj: any) { return await request({ url: apiPrefix + "/add", @@ -84,6 +102,7 @@ export type OnlinePluginBean = { downloadCount?: number; score?: number; aiCheckStatus?: string; + editable?: boolean; selfAuthored?: boolean; installed?: boolean; installedVersion?: string; diff --git a/packages/ui/certd-client/src/views/sys/plugin/components/plugin-ai-dev-dialog-body.vue b/packages/ui/certd-client/src/views/sys/plugin/components/plugin-ai-dev-dialog-body.vue new file mode 100644 index 000000000..1f23e6d15 --- /dev/null +++ b/packages/ui/certd-client/src/views/sys/plugin/components/plugin-ai-dev-dialog-body.vue @@ -0,0 +1,224 @@ + + + + + diff --git a/packages/ui/certd-client/src/views/sys/plugin/components/plugin-edit-dialog-body.vue b/packages/ui/certd-client/src/views/sys/plugin/components/plugin-edit-dialog-body.vue index 87de39aef..795de3194 100644 --- a/packages/ui/certd-client/src/views/sys/plugin/components/plugin-edit-dialog-body.vue +++ b/packages/ui/certd-client/src/views/sys/plugin/components/plugin-edit-dialog-body.vue @@ -3,6 +3,7 @@
插件名称: + AI 开发 发布到插件市场
@@ -41,6 +42,7 @@ import createCrudOptions from "../crud"; import { usePluginPublish } from "../use-publish"; import { usePluginStore } from "/@/store/plugin"; import { useSettingStore } from "/@/store/settings"; +import { usePluginAiDev } from "../use-ai-dev"; defineOptions({ name: "PluginEditDialogBody", @@ -61,6 +63,7 @@ const formOptionsRef: Ref = ref(); const baseFormRef: Ref = ref({}); const saveLoading = ref(false); const { isPublishingPlugin, publishLocalPlugin } = usePluginPublish(); +const { openAiDevDialog } = usePluginAiDev(); function initFormOptions() { const formCrudOptions = createCrudOptions({ @@ -161,6 +164,13 @@ async function doPublish() { }); } +async function openAiDev() { + await openAiDevDialog({ + pluginId: props.pluginId, + pluginName: pluginName.value, + }); +} + onMounted(loadPlugin); defineExpose({ @@ -186,6 +196,14 @@ defineExpose({ margin-left: auto; } + &__ai-dev { + margin-left: auto; + } + + &__ai-dev + &__publish { + margin-left: 8px; + } + &__content { display: grid; min-height: 0; diff --git a/packages/ui/certd-client/src/views/sys/plugin/crud.tsx b/packages/ui/certd-client/src/views/sys/plugin/crud.tsx index 1b9c6c61e..34203ce91 100644 --- a/packages/ui/certd-client/src/views/sys/plugin/crud.tsx +++ b/packages/ui/certd-client/src/views/sys/plugin/crud.tsx @@ -11,6 +11,7 @@ import { usePluginConfig } from "./use-config"; import { useSettingStore } from "/src/store/settings/index"; import { usePluginStore } from "/@/store/plugin"; import PluginAuthorField from "./components/plugin-author-field.vue"; +import { usePluginAiDev } from "./use-ai-dev"; export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet { const { t } = useI18n(); @@ -57,6 +58,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat const { openImportDialog } = usePluginImport(); const { openConfigDialog } = usePluginConfig(); + const { openAiDevDialog } = usePluginAiDev(); const settingStore = useSettingStore(); const pluginStore = usePluginStore(); @@ -149,6 +151,15 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat }, actionbar: { buttons: { + aiDev: { + show: true, + icon: "ion:sparkles-outline", + text: "AI 开发插件", + type: "primary", + async click() { + await openAiDevDialog(); + }, + }, add: { show: true, icon: "ion:ios-add-circle-outline", diff --git a/packages/ui/certd-client/src/views/sys/plugin/use-ai-dev.tsx b/packages/ui/certd-client/src/views/sys/plugin/use-ai-dev.tsx new file mode 100644 index 000000000..3ae803570 --- /dev/null +++ b/packages/ui/certd-client/src/views/sys/plugin/use-ai-dev.tsx @@ -0,0 +1,38 @@ +import { h, ref } from "vue"; +import { useFormDialog } from "/@/use/use-dialog"; +import PluginAiDevDialogBody from "./components/plugin-ai-dev-dialog-body.vue"; + +export type PluginAiDevOpenOptions = { + pluginId?: number | string; + pluginName?: string; +}; + +export function usePluginAiDev() { + const { openFormDialog } = useFormDialog(); + + async function openAiDevDialog(options: PluginAiDevOpenOptions = {}) { + const bodyRef = ref(); + await openFormDialog({ + title: "AI 开发插件", + columns: {}, + noneForm: true, + wrapper: { + width: 980, + destroyOnClose: true, + maskClosable: false, + footer: null, + class: "plugin-ai-dev-dialog", + }, + body: () => + h(PluginAiDevDialogBody, { + ref: bodyRef, + pluginId: options.pluginId, + pluginName: options.pluginName, + }), + }); + } + + return { + openAiDevDialog, + }; +} diff --git a/packages/ui/certd-server/src/controller/sys/plugin/plugin-controller.ts b/packages/ui/certd-server/src/controller/sys/plugin/plugin-controller.ts index 3255ea252..2fb78af9a 100644 --- a/packages/ui/certd-server/src/controller/sys/plugin/plugin-controller.ts +++ b/packages/ui/certd-server/src/controller/sys/plugin/plugin-controller.ts @@ -8,6 +8,7 @@ import { OnlinePluginPublishInfoReq, OnlinePluginPublishReq, OnlinePluginVersionSubmitReq, + PluginFindReq, PluginImportReq, PluginService, } from "../../../modules/plugin/service/plugin-service.js"; @@ -39,6 +40,12 @@ export class PluginController extends CrudController { return await super.page(body); } + @Post("/find", { description: "sys:settings:view", summary: "查询插件" }) + async find(@Body(ALL) body: PluginFindReq) { + const res = await this.service.findPlugins(body || {}); + return this.ok(res); + } + @Post("/add", { description: "sys:settings:edit", summary: "添加插件" }) async add(@Body(ALL) bean: any) { const def: any = { @@ -182,4 +189,5 @@ export class PluginController extends CrudController { const res = await this.service.exportPlugin(id); return this.ok(res); } + } diff --git a/packages/ui/certd-server/src/modules/pipeline/service/builtin-plugin-service.ts b/packages/ui/certd-server/src/modules/pipeline/service/builtin-plugin-service.ts index 6ab272c3a..1306ea4b1 100644 --- a/packages/ui/certd-server/src/modules/pipeline/service/builtin-plugin-service.ts +++ b/packages/ui/certd-server/src/modules/pipeline/service/builtin-plugin-service.ts @@ -1,5 +1,6 @@ import { Provide, Scope, ScopeEnum } from "@midwayjs/core"; -import { pluginGroups, pluginRegistry } from "@certd/pipeline"; +import { accessRegistry, pluginGroups, pluginRegistry } from "@certd/pipeline"; +import { dnsProviderRegistry } from "@certd/plugin-cert"; import { cloneDeep } from "lodash-es"; @Provide() @@ -25,6 +26,26 @@ export class BuiltInPluginService { return list; } + getAllList() { + const pluginList = this.getList(); + const accessList = accessRegistry.getDefineList().map(item => { + return { + ...item, + pluginType: "access", + }; + }); + const dnsProviderList = dnsProviderRegistry.getDefineList().map(item => { + return { + ...item, + pluginType: "dnsProvider", + }; + }); + const list = [...pluginList, ...accessList, ...dnsProviderList]; + return list.sort((a, b) => { + return (a.order ?? 10) - (b.order ?? 10); + }); + } + getGroups() { const groups: any = cloneDeep(pluginGroups); for (const key in groups) { diff --git a/packages/ui/certd-server/src/modules/plugin/service/plugin-service.ts b/packages/ui/certd-server/src/modules/plugin/service/plugin-service.ts index 06d1a9f4e..14b0430ba 100644 --- a/packages/ui/certd-server/src/modules/plugin/service/plugin-service.ts +++ b/packages/ui/certd-server/src/modules/plugin/service/plugin-service.ts @@ -2,7 +2,7 @@ import { addonRegistry, BaseService, PageReq, PlusService, SysInstallInfo, SysPluginSetting, SysSettingsService } from "@certd/lib-server"; import { PluginEntity } from "../entity/plugin.js"; import { InjectEntityModel } from "@midwayjs/typeorm"; -import { Brackets, IsNull, Not, Repository } from "typeorm"; +import { Brackets, IsNull, Like, Not, Repository } from "typeorm"; import { isComm } from "@certd/plus-core"; import { BuiltInPluginService } from "../../pipeline/service/builtin-plugin-service.js"; import { merge } from "lodash-es"; @@ -26,6 +26,18 @@ export type OnlinePluginListReq = { keyword?: string; }; +export type PluginFindReq = { + type?: "builtIn" | "store"; + pluginType?: string; + group?: string; + name?: string; + author?: string; + keyword?: string; + keywords?: string[]; + includeBuiltIn?: boolean; + includeStore?: boolean; +}; + export type OnlinePluginInstallReq = { fullName: string; version?: string; @@ -99,6 +111,7 @@ export type OnlinePluginBean = { downloadCount?: number; score?: number; aiCheckStatus?: string; + editable?: boolean; selfAuthored?: boolean; installed?: boolean; installedVersion?: string; @@ -297,6 +310,106 @@ export class PluginService extends BaseService { const keyword = `${query.name}`.trim().toLowerCase(); records = records.filter(item => (item.name || "").toLowerCase().includes(keyword)); } + const keywords = this.normalizePluginFindKeywords(query as any); + if (keywords.length > 0) { + records = records.filter(item => { + return keywords.some(keyword => { + return [item.name, item.title, item.desc, item.group, item.pluginType].some(value => `${value || ""}`.toLowerCase().includes(keyword)); + }); + }); + } + return records; + } + + private normalizePluginFindKeywords(req: PluginFindReq) { + const values = [...(req.keywords || []), req.keyword || ""]; + return values.map(item => `${item || ""}`.trim().toLowerCase()).filter((item, index, list) => item.length > 0 && list.indexOf(item) === index); + } + + async findPlugins(req: PluginFindReq = {}) { + const includeBuiltIn = req.includeBuiltIn ?? req.type !== "store"; + const includeStore = req.includeStore ?? req.type !== "builtIn"; + const records: any[] = []; + + if (includeBuiltIn) { + const builtInQuery = { + ...req, + type: "builtIn", + }; + const builtInList = this.filterBuiltInList(await this.getBuiltInEntityList(), builtInQuery); + records.push( + ...builtInList.map(item => { + const record = { + ...item, + type: "builtIn", + editable: false, + }; + delete (record as any).content; + delete (record as any).setting; + delete (record as any).sysSetting; + delete (record as any).metadata; + delete (record as any).extra; + return record; + }) + ); + } + + if (includeStore) { + const storeQuery: any = { + type: "store", + }; + if (req.pluginType) { + storeQuery.pluginType = req.pluginType; + } + if (req.group) { + storeQuery.group = req.group; + } + if (req.name) { + storeQuery.name = Like(`%${req.name}%`); + } + if (req.author) { + storeQuery.author = req.author; + } + + let storeWhere: any = storeQuery; + const keywords = this.normalizePluginFindKeywords(req); + if (keywords.length > 0) { + const searchFields = ["fullName", "name", "title", "desc", "group", "pluginType"]; + storeWhere = keywords.flatMap(keyword => { + const keywordLike = Like(`%${keyword}%`); + return searchFields.map(field => { + return { + ...storeQuery, + [field]: keywordLike, + }; + }); + }); + } + const storeList = await this.find({ + where: storeWhere, + order: { + pluginType: "ASC", + group: "ASC", + title: "ASC", + fullName: "ASC", + id: "ASC", + } as any, + }); + let bindUserId = 0; + if (this.sysSettingsService) { + const installInfo = await this.sysSettingsService.getSetting(SysInstallInfo); + bindUserId = installInfo.bindUserId || 0; + } + records.push( + ...storeList.map(item => { + return { + ...this.toOnlinePluginBean(item), + editable: canEditStorePlugin(item.developerId, bindUserId), + }; + }) + ); + } + return records; } @@ -431,7 +544,7 @@ export class PluginService extends BaseService { } async getBuiltInEntityList() { - const builtInList = this.builtInPluginService.getList(); + const builtInList = this.builtInPluginService.getAllList(); const list = await this.list({ query: { type: "builtIn",