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/) |
| 基于vue3的crud快速开发框架 |
| [dev-sidecar](https://github.com/docmirror/dev-sidecar/) |
| 直连访问github工具,无需FQ,解决github无法访问的问题 |
+| [winsvc-manager](https://github.com/greper/winsvc-manager/) |
| 可视化包装应用成为一个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