From 21aec77e5c3307b5973d4185baba33edcb28926f Mon Sep 17 00:00:00 2001 From: xiaojunnuo Date: Thu, 2 Apr 2026 00:10:28 +0800 Subject: [PATCH] =?UTF-8?q?perf(spaceship):=20=E6=96=B0=E5=A2=9ESpaceship?= =?UTF-8?q?=20DNS=E6=8F=92=E4=BB=B6=E5=92=8C=E6=8E=88=E6=9D=83=E6=A8=A1?= =?UTF-8?q?=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加Spaceship DNS提供商插件和授权模块,支持域名解析管理 更新相关文档和技能说明,优化错误处理和日志记录 移除调试日志,更新README项目列表 --- .trae/skills/access-plugin-dev/SKILL.md | 13 ++ .trae/skills/agent.md | 11 +- .trae/skills/dns-provider-dev/SKILL.md | 2 + README.md | 1 + packages/core/basic/src/utils/util.request.ts | 2 +- .../src/plugins/plugin-spaceship/access.ts | 148 ++++++++++++++++++ .../plugins/plugin-spaceship/dns-provider.ts | 95 +++++++++++ .../src/plugins/plugin-spaceship/index.ts | 2 + 8 files changed, 267 insertions(+), 7 deletions(-) create mode 100644 packages/ui/certd-server/src/plugins/plugin-spaceship/access.ts create mode 100644 packages/ui/certd-server/src/plugins/plugin-spaceship/dns-provider.ts create mode 100644 packages/ui/certd-server/src/plugins/plugin-spaceship/index.ts diff --git a/.trae/skills/access-plugin-dev/SKILL.md b/.trae/skills/access-plugin-dev/SKILL.md index d2c76685e..20e28a074 100644 --- a/.trae/skills/access-plugin-dev/SKILL.md +++ b/.trae/skills/access-plugin-dev/SKILL.md @@ -163,6 +163,16 @@ async doRequest(req: { action: string, data?: any }) { } ``` +--- 开发技巧:实现统一的 API 请求封装 + +**好处:** +- **代码复用**:避免在每个 API 方法中重复编写相同的 header 设置和错误处理逻辑 +- **错误处理一致**:统一捕获和处理各种错误情况,确保错误信息格式统一 +- **日志记录完善**:集中记录详细的错误信息,便于调试和问题排查 +- **接口调用简化**:调用方只需关注业务逻辑,无需关心底层请求细节 +- **易于维护**:统一修改 API 调用方式时,只需修改一处代码 + + ## 注意事项 1. **插件命名**:插件名称应简洁明了,反映其功能。 @@ -170,9 +180,12 @@ async doRequest(req: { action: string, data?: any }) { 3. **日志输出**:必须使用 `this.ctx.logger` 输出日志,而不是 `console`。 4. **错误处理**:API 调用失败时应抛出明确的错误信息。 5. **测试方法**:实现 `onTestRequest` 方法,以便用户可以测试授权是否正常。 +6. **统一接口调用方法**:封装统一的 API 请求方法,避免在每个 API 方法调用中重复编写错误处理逻辑。 ## 完整示例 +### 示例 1: 通用授权插件 + ```typescript import { AccessInput, BaseAccess, IsAccess, Pager, PageRes, PageSearch } from '@certd/pipeline'; import { DomainRecord } from '@certd/plugin-lib'; diff --git a/.trae/skills/agent.md b/.trae/skills/agent.md index c89feffc2..f6af9571b 100644 --- a/.trae/skills/agent.md +++ b/.trae/skills/agent.md @@ -6,9 +6,8 @@ Access:存储用户的第三放应用的授权数据,比如用户名密码 Task: 部署任务插件,它继承AbstractTaskPlugin类,被流水线调用execute方法,将证书部署到对应的应用上 DnsProvider: DNS提供商插件,它用于在ACME申请证书时给域名添加txt解析记录。 -在开始工作前,请阅读并加载.trae/skills下面的技能,根据skills进行相应的插件开发 -当开发过程中遇到问题,需要参考plugins目录下的其他插件,或者用户提醒你更好的做法时,你需要总结经验,更新相应的skills,让skills越来越完善,能够在以后得新插件开发中具备指导意义。 - -一般调用的api接口文档会比较复杂,你不知道接口是什么时,请务必询问用户,让用户提供API接口文档 - -完成开发后无需测试,通知用户自己去测试 \ No newline at end of file +注意事项: +1、使用技能:在开始工作前,请阅读并加载.trae/skills下面的技能,根据skills进行相应的插件开发 +2、迭代技能:当开发过程用户提醒你更好的做法时,你需要总结经验,更新相应的skills,让skills越来越完善,能够在以后得新插件开发中具备指导意义。 +3、一般调用的api接口文档会比较复杂,你不知道接口是什么时,请务必询问用户,让用户提供API接口文档 +4、完成开发后无需测试,通知用户自己去测试 \ No newline at end of file diff --git a/.trae/skills/dns-provider-dev/SKILL.md b/.trae/skills/dns-provider-dev/SKILL.md index 180a698ad..aab1540c7 100644 --- a/.trae/skills/dns-provider-dev/SKILL.md +++ b/.trae/skills/dns-provider-dev/SKILL.md @@ -126,6 +126,8 @@ if (isDev()) { ## 完整示例 +### 示例:通用 DNS Provider + ```typescript import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from '@certd/plugin-cert'; import { DemoAccess } from './access.js'; diff --git a/README.md b/README.md index 603d57838..70db801c5 100644 --- a/README.md +++ b/README.md @@ -211,3 +211,4 @@ https://certd.handfree.work/ | --------- |--------- |----------- | | [fast-crud](https://gitee.com/fast-crud/fast-crud/) | GitHub stars | 基于vue3的crud快速开发框架 | | [dev-sidecar](https://github.com/docmirror/dev-sidecar/) | GitHub stars | 直连访问github工具,无需FQ,解决github无法访问的问题 | +| [winsvc-manager](https://github.com/greper/winsvc-manager/) | GitHub stars | 可视化包装应用成为一个Windows服务,使其后台运行 | diff --git a/packages/core/basic/src/utils/util.request.ts b/packages/core/basic/src/utils/util.request.ts index 7fdc45097..d98ea3c50 100644 --- a/packages/core/basic/src/utils/util.request.ts +++ b/packages/core/basic/src/utils/util.request.ts @@ -271,7 +271,7 @@ export function createAxiosService({ logger }: { logger: ILogger }) { } const originalRequest = error.config || {}; - logger.info(`config`, originalRequest); + // logger.info(`config`, originalRequest); const retry = originalRequest.retry || {}; if (retry.status && retry.status.includes(status)) { if (retry.max > 0 && retry.count < retry.max) { diff --git a/packages/ui/certd-server/src/plugins/plugin-spaceship/access.ts b/packages/ui/certd-server/src/plugins/plugin-spaceship/access.ts new file mode 100644 index 000000000..c7c23459d --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-spaceship/access.ts @@ -0,0 +1,148 @@ +import { IsAccess, AccessInput, BaseAccess, PageSearch } from "@certd/pipeline"; + +@IsAccess({ + name: "spaceship", + title: "Spaceship.com 授权", + icon: "clarity:plugin-line", + desc: "Spaceship.com API 授权插件" +}) +export class SpaceshipAccess extends BaseAccess { + + @AccessInput({ + title: "API Key", + component: { + placeholder: "请输入 API Key" + }, + required: true, + encrypt: true, + helper: "前往 [获取 API Key](https://www.spaceship.com/application/api-manager/)" + }) + apiKey = ""; + + @AccessInput({ + title: "API Secret", + component: { + name: "a-input-password", + vModel: "value", + placeholder: "请输入 API Secret" + }, + required: true, + encrypt: true + }) + apiSecret = ""; + + @AccessInput({ + title: "测试", + component: { + name: "api-test", + action: "TestRequest" + }, + helper: "测试 API 连接是否正常" + }) + testRequest = true; + + async onTestRequest() { + await this.GetDomainList({}); + return "ok"; + } + + async doRequest(options: { + url: string; + method: 'GET' | 'POST' | 'DELETE'; + params?: any; + data?: any; + }) { + const headers = { + "X-Api-Key": this.apiKey, + "X-Api-Secret": this.apiSecret + }; + + try { + const res = await this.ctx.http.request({ + url: options.url, + method: options.method, + headers, + params: options.params, + data: options.data + }); + return res; + } catch (error: any) { + const errorMsg = []; + const status = error.status || error.response?.status; + if (error.response) { + const headers = error.response.headers; + const data = error.response.data; + + errorMsg.push(`API 请求失败: ${status}`); + + if (headers['spaceship-error-code']) { + errorMsg.push(`错误代码: ${headers['spaceship-error-code']}`); + } + + if (headers['spaceship-operation-id']) { + errorMsg.push(`操作ID: ${headers['spaceship-operation-id']}`); + } + + if (data && data.detail) { + errorMsg.push(`错误详情: ${data.detail}`); + } + + this.ctx.logger.error(`Spaceship API 错误: ${errorMsg.join(' | ')}`); + } else if (error.request) { + errorMsg.push(`请求发送失败: ${error.message}`); + this.ctx.logger.error(`Spaceship API 请求发送失败: ${error.message}`); + } else { + errorMsg.push(`请求配置错误: ${error.message}`); + this.ctx.logger.error(`Spaceship API 请求配置错误: ${error.message}`); + } + + const error2 = new Error(errorMsg.join(' | ')); + //@ts-ignore + error2.status = status; + throw error2; + } + } + + async GetDomainList(req: PageSearch) { + const take = req.pageSize || 100; + const skip = ((req.pageNo || 1) - 1) * take; + + const res = await this.doRequest({ + url: "https://spaceship.dev/api/v1/domains", + method: "GET", + params: { + take, + skip + } + }); + + return { + total: res.total || 0, + list: res.items || [] + }; + } + + async getDomainInfo(domain: string) { + try { + const res = await this.doRequest({ + url: `https://spaceship.dev/api/v1/domains/${domain}`, + method: "GET" + }); + return res; + } catch (error: any) { + if (error.status === 404) { + throw new Error(`域名 ${domain} 不存在于当前账号中`); + } + throw error; + } + } + + getCacheKey() { + const hashStr = this.apiKey + this.apiSecret; + const hashCode = this.ctx.utils.hash.sha256(hashStr); + return `spaceship-${hashCode}`; + } + +} + +new SpaceshipAccess(); \ No newline at end of file diff --git a/packages/ui/certd-server/src/plugins/plugin-spaceship/dns-provider.ts b/packages/ui/certd-server/src/plugins/plugin-spaceship/dns-provider.ts new file mode 100644 index 000000000..aec3bbb1a --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-spaceship/dns-provider.ts @@ -0,0 +1,95 @@ +import { AbstractDnsProvider, CreateRecordOptions, DomainRecord, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert"; +import { SpaceshipAccess } from "./access.js"; +import { PageRes, PageSearch } from "@certd/pipeline"; + +export type SpaceshipRecord = { + id: string; + name: string; + type: string; + content: string; + domainId: string; +}; + +@IsDnsProvider({ + name: "spaceship", + title: "Spaceship", + desc: "Spaceship 域名解析", + icon: "clarity:plugin-line", + accessType: "spaceship", + order: 99 +}) +export class SpaceshipProvider extends AbstractDnsProvider { + access!: SpaceshipAccess; + + async onInstance() { + this.access = this.ctx.access as SpaceshipAccess; + } + + async createRecord(options: CreateRecordOptions): Promise { + const { fullRecord, hostRecord, value, type, domain } = options; + this.logger.info("添加域名解析:", fullRecord, value, type, domain); + + await this.access.getDomainInfo(domain); + + const recordRes = await this.access.doRequest({ + url: `https://spaceship.dev/api/v1/domains/${domain}/records`, + method: "POST", + data: { + force: false, + items: [ + { + type: type, + value: value, + name: hostRecord, + ttl: 300 + } + ] + } + }); + + return { + id: recordRes.items[0].id, + name: hostRecord, + type: type, + content: value, + domainId: domain + }; + } + + async removeRecord(options: RemoveRecordOptions): Promise { + const recordRes = options.recordRes; + this.logger.info("删除域名解析:", recordRes); + + await this.access.doRequest({ + url: `https://spaceship.dev/api/v1/domains/${recordRes.domainId}/records`, + method: "DELETE", + data: { + Records: [ + { + type: recordRes.type, + value: recordRes.content, + name: recordRes.name + } + ] + } + }); + + this.logger.info("删除域名解析成功:", recordRes.name); + } + + async getDomainListPage(req: PageSearch): Promise> { + const res = await this.access.GetDomainList(req); + + const list = res.list.map((item: any) => ({ + domain: item.name, + id: item.name + })); + + return { + total: res.total || 0, + list: list || [] + }; + } +} + +new SpaceshipProvider(); \ No newline at end of file diff --git a/packages/ui/certd-server/src/plugins/plugin-spaceship/index.ts b/packages/ui/certd-server/src/plugins/plugin-spaceship/index.ts new file mode 100644 index 000000000..eeb902d6b --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-spaceship/index.ts @@ -0,0 +1,2 @@ +import "./access.js"; +import "./dns-provider.js"; \ No newline at end of file