插件标准化打包、测试与质量门禁

约 30 分钟

规范化组织插件工程目录,编写单元测试并通过代码质量门禁,输出可发布的正式插件包。

在前一课中,你已经系统排查了 DeepSeek Harness 插件开发中 Function Calling 的三大典型故障——Schema 校验失败与入参幻觉、工具响应超时、多轮死循环与 Token 耗尽,并分别借助防御性 Schema 与自愈反馈、超时熔断与降级处理、事件钩子与 Trajectory 日志将其定位并阻断。故障排查只是起点:要让这些修复成果随插件一起真正交付给团队或发布到企业生产环境,还必须完成工程化打包、单元测试与质量门禁校验。回望你已经掌握的 dsh 插件四要素定义、声明式配置、工具注册与生命周期拦截,一个工业级插件不仅要具备清晰的模块导出与类型声明,还必须在隔离的 Mock Context 中完成自动化测试,确保在各种宿主环境下即插即用且具备完整的依赖回滚保障。

核心概念

正式插件包发布标准与质量门禁

生活类比:就像出厂合格的家用电器插头,不仅要有标准的尺寸规格(三相接口),还要经过绝缘耐压与通电老化测试,并附带明确的安全参数铭牌,才能进入商场上架销售。

技术定义:正式插件包发布标准与质量门禁,是指基于 ESM 规范组织 Cordis 插件代码,声明严格的 package.json 元数据与 TypeScript 类型定义;在持续集成流水线中通过类型检查、Mock 上下文单元测试与生命周期销毁断言(Fiber 状态与资源回收测试),构筑全自动化的质量准入防线。

插件工程化组织、单元测试与构建实操

  1. 1

    初始化插件包结构并配置 package.json,规范 ESM 导出项与 TypeScript 类型声明。

    json
    {
      "name": "dsh-plugin-weather-gate",
      "version": "1.0.0",
      "type": "module",
      "main": "./dist/index.js",
      "types": "./dist/index.d.ts",
      "scripts": {
        "build": "tsc",
        "test": "vitest run",
        "lint": "tsc --noEmit"
      },
      "peerDependencies": {
        "@deepseek-ai/cordis": "*"
      },
      "devDependencies": {
        "@deepseek-ai/cordis": "^4.0.2",
        "typescript": "^5.5.0",
        "vitest": "^2.0.0"
      }
    }
  2. 2

    编写标准插件源码 src/index.ts,实现工具注册、配置校验与资源清理回调。

    typescript
    import type { Context } from '@deepseek-ai/cordis'
    
    export const name = 'weather-gate'
    export const inject = ['tools']
    
    export interface Config {
      apiKey?: string
      timeout?: number
    }
    
    export function apply(ctx: Context, config: Config = {}) {
      const timeout = config.timeout ?? 5000
    
      ctx.tools.register({
        name: 'query_weather',
        description: 'Query weather for a specific city',
        parameters: {
          city: { type: 'string', required: true, description: 'City name' }
        },
        output: {
          schema: { type: 'string' },
          render: (_args: any, value: string) => [{ type: 'text', text: value }]
        },
        async execute(args: { city: string }) {
          return `Weather in ${args.city}: Sunny, 24°C (timeout: ${timeout}ms)`
        }
      })
    
      ctx.effect(() => {
        return () => {
          // 注册资源释放回调
        }
      })
    }
  3. 3

    编写针对 Cordis Mock 上下文的单元测试 tests/index.spec.ts,验证插件装载、工具调用与 Fiber 销毁回滚。

    typescript
    import { describe, it, expect } from 'vitest'
    import { Context } from '@deepseek-ai/cordis'
    import * as WeatherPlugin from '../src/index.js'
    
    describe('WeatherPlugin Quality Gate Test', () => {
      it('should load plugin and register tool on ready context', async () => {
        const ctx = new Context()
        
        // Mock tools 服务仓库
        const registeredTools: any[] = []
        ctx.provide('tools', {
          register(tool: any) {
            registeredTools.push(tool)
          }
        })
    
        const fiber = ctx.plugin(WeatherPlugin, { timeout: 3000 })
        await ctx.start()
    
        expect(registeredTools).toHaveLength(1)
        expect(registeredTools[0].name).toBe('query_weather')
    
        const result = await registeredTools[0].execute({ city: 'Beijing' })
        expect(result).toContain('Beijing: Sunny, 24°C (timeout: 3000ms)')
    
        // 验证销毁行为与 Fiber 可逆性
        await fiber.dispose()
      })
    })
  4. 4

    执行质量门禁流水线:运行类型检查、单元测试并构建产物。

    bash
    pnpm lint && pnpm test && pnpm build

你应该看到

终端中测试用例全部通过(1 passed),TypeScript 类型检查无错误,并成功生成 dist 目录产物。

遇到问题:单元测试中 ctx.plugin 加载插件后 registeredTools 为空,或断言超时失败。

可能原因

  • 未在 Mock 上下文中预先调用 ctx.provide 提供 inject 中声明的依赖服务(如 tools),导致插件处于 PENDING 状态无法进入 ACTIVE。
  • 未在测试脚本中调用 await ctx.start() 或未等待异步生命周期状态机推进。

排查步骤

  1. 检查测试用例中是否已使用 ctx.provide() 注入插件所需的全部依赖键名。
  2. 在断言执行前执行 await ctx.start() 确保 Cordis 内核生命周期完整启动。
自检:为什么在测试 Cordis 插件时,推荐在每个测试用例结尾执行 fiber.dispose()?

执行 fiber.dispose() 可以验证插件内部通过 ctx.effect 注册的清理函数与资源回滚逻辑是否能正确执行,防止跨测试用例产生全局状态残留或内存泄漏,确保插件具备完整的可逆生命周期。