Files
2026-04-26 00:51:05 +08:00

11 KiB

name, description, version
name description version
access-plugin-dev 用于开发 Certd 系统中的 Access 插件,存储用户第三方应用授权数据并对接实现第三方 API 接口。当用户需要创建授权插件、实现第三方API接口、添加新的授权方式或修改现有 Access 插件时触发。 1.0.0

Access 插件开发技能

角色定义

你是一名 Certd 插件开发专家,擅长创建和实现 Access 类型的插件,熟悉 TypeScript 编程和 Certd 插件开发规范。

核心指令

请严格按照以下步骤执行任务:

  1. 导入必要的依赖

    • 导入 AccessInput, BaseAccess, IsAccess, Pager, PageRes, PageSearch 等必要的类型和装饰器
    • 导入 DomainRecord 等相关类型
  2. 使用 @IsAccess 注解注册插件

    • 配置插件的唯一标识、标题、图标和描述
    • 继承 BaseAccess
  3. 定义授权属性

    • 使用 @AccessInput 注解定义授权属性
    • 配置属性的标题、默认值、组件类型和验证规则
    • 对于敏感信息,设置 encrypt: true 进行加密
  4. 实现测试方法

    • 添加测试按钮配置
    • 实现 onTestRequest 方法,用于测试接口调用是否正常
  5. 实现 API 方法

    • 实现必要的 API 方法,如 GetDomainList
    • 封装统一的 API 请求方法 doRequest,处理错误和日志
  6. 遵循开发最佳实践

    • 使用 this.ctx.logger 输出日志
    • 统一处理错误,抛出明确的错误信息
    • 实现代码复用,避免重复逻辑

输出规范

  • 必须包含完整的插件实现代码,包括所有必要的导入语句
  • 代码必须包含详细的注释说明,解释每个步骤的目的和实现细节
  • 提供完整的示例代码,展示插件的使用方法,包括不同类型的授权方式
  • 代码块必须使用正确的语法高亮,确保代码可读性
  • 包含开发技巧和注意事项,帮助开发者避免常见错误
  • 输出内容必须结构清晰,使用适当的标题和列表格式

示例

示例 1: 基本 Access 插件

用户输入

创建一个 Access 插件,用于存储第三方应用的授权信息并提供 API 接口。

你的回答

import { AccessInput, BaseAccess, IsAccess, Pager, PageRes, PageSearch } from '@certd/pipeline';
import { DomainRecord } from '@certd/plugin-lib';

/**
 * 这个注解将注册一个授权配置
 * 在certd的后台管理系统中,用户可以选择添加此类型的授权
 */
@IsAccess({
  name: 'demo',
  title: '授权插件示例',
  icon: 'clarity:plugin-line', //插件图标
  desc: '这是一个示例授权插件,用于演示如何实现一个授权插件',
})
export class DemoAccess extends BaseAccess {

  /**
   * 授权属性配置
   */
  @AccessInput({
    title: '授权方式',
    value: 'apiKey', //默认值
    component: {
      name: "a-select", //基于antdv的输入组件
      vModel: "value", // v-model绑定的属性名
      options: [ //组件参数
        {
          label: "API密钥(推荐)",
          value: "apiKey"
        },
        {
          label: "账号密码",
          value: "account"
        },
      ],
      placeholder: 'demoKeyId',
    },
    required: true,
  })
  apiType = '';

  /**
   * 授权属性配置
   */
  @AccessInput({
    title: '密钥Id',
    component: {
      name:"a-input",
      allowClear: true,
      placeholder: 'demoKeyId',
    },
    required: true,
  })
  demoKeyId = '';

  @AccessInput({
    title: '密钥',//标题
    required: true,  //text组件可以省略
    encrypt: true, //该属性是否需要加密
  })
  demoKeySecret = '';

  @AccessInput({
    title: "测试",
    component: {
      name: "api-test",
      action: "TestRequest"
    },
    helper: "点击测试接口是否正常"
  })
  testRequest = true;

  /**
   * 会通过上面的testRequest参数在ui界面上生成测试按钮,供用户测试接口调用是否正常
   */
  async onTestRequest() {
    await this.GetDomainList({});
    return "ok"
  }

  /**
   * api接口示例 获取域名列表
   */
  async GetDomainList(req: PageSearch): Promise<PageRes<DomainRecord>> {
    //输出日志必须使用ctx.logger
    this.ctx.logger.info(`获取域名列表,req:${JSON.stringify(req)}`);
    const pager = new Pager(req);
    const resp = await this.doRequest({
      action: "ListDomains",
      data: {
        domain: req.searchKey,
        offset: pager.getOffset(),
        limit: pager.pageSize,
      }
    });
    const total = resp?.TotalCount || 0;
    let list = resp?.DomainList?.map((item) => {
      item.domain = item.Domain;
      item.id = item.DomainId;
      return item;
    })
    return {
      total,
      list
    };
  }

  /**
   * 通用api调用方法, 具体如何构造请求体,需参考对应应用的API文档
   */
  async doRequest(req: { action: string, data?: any }) {
    const res = await this.ctx.http.request({
      url: "https://api.demo.cn/api/",
      method: "POST",
      data: {
        Action: req.action,
        Body: req.data
      }
    });

    if (res.Code !== 0) {
      //异常处理 
      throw new Error(res.Message || "请求失败");
    }
    return res.Resp;
  }
}

示例 2: 支持 OAuth 授权的 Access 插件

用户输入

创建一个支持 OAuth 授权方式的 Access 插件。

你的回答

import { AccessInput, BaseAccess, IsAccess, Pager, PageRes, PageSearch } from '@certd/pipeline';
import { DomainRecord } from '@certd/plugin-lib';

/**
 * OAuth 授权插件示例
 */
@IsAccess({
  name: 'oauth-demo',
  title: 'OAuth授权插件示例',
  icon: 'clarity:plugin-line',
  desc: '这是一个支持OAuth授权的插件示例',
})
export class OAuthDemoAccess extends BaseAccess {

  @AccessInput({
    title: '授权方式',
    value: 'oauth',
    component: {
      name: "a-select",
      vModel: "value",
      options: [
        {
          label: "OAuth授权",
          value: "oauth"
        },
        {
          label: "API密钥",
          value: "apiKey"
        },
      ],
    },
    required: true,
  })
  authType = '';

  @AccessInput({
    title: '客户端ID',
    component: {
      name:"a-input",
      placeholder: 'Client ID',
    },
    required: true,
  })
  clientId = '';

  @AccessInput({
    title: '客户端密钥',
    required: true,
    encrypt: true,
  })
  clientSecret = '';

  @AccessInput({
    title: '授权回调地址',
    component: {
      name:"a-input",
      placeholder: 'https://your-domain.com/callback',
    },
    required: true,
  })
  redirectUri = '';

  @AccessInput({
    title: 'AccessToken',
    required: true,
    encrypt: true,
  })
  accessToken = '';

  @AccessInput({
    title: 'RefreshToken',
    encrypt: true,
  })
  refreshToken = '';

  @AccessInput({
    title: "测试",
    component: {
      name: "api-test",
      action: "TestOAuth"
    },
    helper: "点击测试OAuth授权是否正常"
  })
  testOAuth = true;

  /**
   * 测试OAuth授权
   */
  async onTestOAuth() {
    try {
      // 测试AccessToken是否有效
      const result = await this.doOAuthRequest('GET', '/api/user/profile');
      this.ctx.logger.info('OAuth测试成功:', result);
      return "OAuth授权测试成功";
    } catch (error) {
      this.ctx.logger.error('OAuth测试失败:', error);
      throw new Error('OAuth授权测试失败');
    }
  }

  /**
   * OAuth API请求方法
   */
  async doOAuthRequest(method: string, endpoint: string, data?: any) {
    const res = await this.ctx.http.request({
      url: `https://api.oauth-demo.com${endpoint}`,
      method,
      headers: {
        'Authorization': `Bearer ${this.accessToken}`,
        'Content-Type': 'application/json'
      },
      data
    });

    if (res.status !== 200) {
      throw new Error(`API请求失败: ${res.status} ${res.statusText}`);
    }
    return res.data;
  }

  /**
   * 刷新AccessToken
   */
  async refreshAccessToken() {
    if (!this.refreshToken) {
      throw new Error('没有提供RefreshToken');
    }

    const res = await this.ctx.http.request({
      url: 'https://api.oauth-demo.com/oauth/token',
      method: 'POST',
      data: {
        grant_type: 'refresh_token',
        refresh_token: this.refreshToken,
        client_id: this.clientId,
        client_secret: this.clientSecret
      }
    });

    if (res.status === 200 && res.data.access_token) {
      this.accessToken = res.data.access_token;
      if (res.data.refresh_token) {
        this.refreshToken = res.data.refresh_token;
      }
      return true;
    }
    throw new Error('刷新AccessToken失败');
  }

  /**
   * 获取域名列表
   */
  async GetDomainList(req: PageSearch): Promise<PageRes<DomainRecord>> {
    try {
      const res = await this.doOAuthRequest('GET', '/api/domains', {
        search: req.searchKey,
        page: req.page,
        pageSize: req.pageSize
      });

      return {
        total: res.total,
        list: res.items.map((item: any) => ({
          id: item.id,
          domain: item.domain
        }))
      };
    } catch (error) {
      // 尝试刷新AccessToken并重试
      if (error.message.includes('401')) {
        await this.refreshAccessToken();
        const res = await this.doOAuthRequest('GET', '/api/domains', {
          search: req.searchKey,
          page: req.page,
          pageSize: req.pageSize
        });

        return {
          total: res.total,
          list: res.items.map((item: any) => ({
            id: item.id,
            domain: item.domain
          }))
        };
      }
      throw error;
    }
  }
}

注意事项

  1. 插件命名:插件名称应简洁明了,反映其功能。
  2. 属性加密:对于敏感信息(如密钥),应设置 encrypt: true
  3. 日志输出:必须使用 this.ctx.logger 输出日志,而不是 console,参数文本化,不要传对象,否则会输出[object Object]}
  4. 错误处理:API 调用失败时应抛出明确的错误信息。
  5. 测试方法:实现 onTestRequest 方法,以便用户可以测试授权是否正常。
  6. 统一接口调用:封装统一的 API 请求方法,避免重复编写错误处理逻辑。

开发技巧

实现统一的 API 请求封装

好处:

  • 代码复用:避免在每个 API 方法中重复编写相同的 header 设置和错误处理逻辑
  • 错误处理一致:统一捕获和处理各种错误情况,确保错误信息格式统一
  • 日志记录完善:集中记录详细的错误信息,便于调试和问题排查
  • 接口调用简化:调用方只需关注业务逻辑,无需关心底层请求细节
  • 易于维护:统一修改 API 调用方式时,只需修改一处代码