agent-skills:TypeScript+NX的智能体能力契约设计范式

发布时间:2026/9/16 9:11:39
agent-skills:TypeScript+NX的智能体能力契约设计范式 1. 项目概述一个被严重低估的“技能容器”设计范式“agent-skills”这个名称乍看平平无奇像某个开源库的包名又像某次内部技术分享的临时标题。但如果你最近刷过 TypeScript 社区、Nx 工具链讨论组或者参与过任何基于智能体Agent架构的中后台系统开发你大概率已经在多个项目里见过它——只是没意识到它背后藏着一套高度可复用、可测试、可组合的能力抽象协议。它不是框架不是 SDK更不是 AI 模型封装它是一套用 TypeScript 严格定义的、运行在 Node.js 环境中的技能契约Skill Contract标准接口体系。核心关键词“agent-skills”本身就是一个信号它指向的不是“如何造一个 Agent”而是“如何让任意 Agent 安全、可控、可审计地调用外部能力”。这直接切中了当前工程化落地 AI 应用的最大痛点——能力边界模糊、调用链路不可控、错误处理碎片化、权限与审计缺失。我第一次在真实生产环境里撞见它是在一个金融风控决策平台的重构项目中。当时团队正把几十个 Python 编写的规则引擎、第三方 API 封装、数据库校验脚本逐步迁移到由 Nx 管理的 TypeScript 单体工作区。最初大家各自写callExternalApi()、validateUserInput()、sendSlackAlert()这类函数结果不到三个月就出现了三套不同的重试逻辑、四套不兼容的错误码映射、五种风格迥异的输入校验方式。日志里全是Error: unknown error from payment gateway这种无效信息。直到一位资深后端同事甩出一个叫myorg/agent-skills的私有包里面只有两个文件Skill.ts和SkillRunner.ts。他只改了不到 20 行代码就把所有散装能力收编进统一调度器错误日志立刻能精准定位到是“支付网关超时codePAY_TIMEOUT”重试策略自动降级为指数退避审计日志里清晰记录着“用户A在14:23:05调用了payment-validate技能输入脱敏后为{card_last4: 4242, amount: 199.00}”。那一刻我才真正理解“agent-skills”不是功能模块而是一种面向能力治理的基础设施语言。它适合所有正在从“单点 AI 调用”迈向“可编排智能体工作流”的团队尤其适合那些已采用 Nx 进行大型 TypeScript 项目管理、并依赖 semantic-release 实现自动化版本发布的工程团队。它不解决模型推理问题但它决定了你的模型能力能否被安全、稳定、可维护地交付到业务前线。2. 核心设计思路为什么是 TypeScript Nx semantic-release 的铁三角组合2.1 技能契约的本质从“函数调用”到“能力协商”“agent-skills”的底层设计哲学源于对传统函数调用范式的彻底反思。在普通 Node.js 项目中我们写一个sendEmail(to, subject, body)函数调用方必须精确知道参数名、类型、顺序、甚至副作用比如是否自动重试、是否记录日志。一旦需求变更——比如要支持附件、要区分事务性邮件和通知类邮件、要对接新邮件服务商——函数签名就得改所有调用点都得同步更新极易引发连锁故障。而“agent-skills”将这个过程升维为一次能力协商Capability Negotiation。它强制定义了三个不可绕过的契约要素能力标识Skill ID一个全局唯一、语义化的字符串如email.send-transactional或db.query-user-profile。它不依赖函数名而是业务域概念便于监控、审计、权限控制。输入契约Input Schema使用 TypeScript Interface 严格声明但关键在于它必须通过zod或sinclair/typebox进行运行时验证。例如email.send-transactional的输入必须包含to: string EmailFormat、templateId: string UuidV4、context: Recordstring, unknown且context中的userId字段若存在必须是数字类型。这杜绝了“传错参数导致下游静默失败”的经典陷阱。执行契约Execution Contract规定技能必须返回一个标准化的SkillResultT类型包含success: boolean、output?: T、error?: SkillError、metadata: { durationMs: number, retries: number, traceId: string }。更重要的是它要求所有技能实现必须遵循统一的错误分类体系——SkillError不是Error的简单继承而是包含code: string如EMAIL_RATE_LIMIT_EXCEEDED、level: fatal | warning | info、suggestion: string如“请检查 SMTP 配置或联系运维提升配额”等字段。这意味着调用方无需关心内部实现只需根据code做决策EMAIL_RATE_LIMIT_EXCEEDED触发降级策略DB_CONNECTION_LOST启动熔断INPUT_VALIDATION_FAILED直接返回用户友好的提示。这种设计直接解决了我在上一家公司踩过的大坑一个电商促销系统里applyCoupon技能在不同环境返回格式混乱——开发环境抛原生Error测试环境返回{ ok: false, msg: xxx }生产环境又变成{ success: false, data: null, error: { code: 500, message: internal error } }。前端不得不写三套解析逻辑每次发布都提心吊胆。而“agent-skills”的契约强制所有技能输出同构前端只需监听result.success和result.error.code稳定性提升了一个数量级。2.2 为什么必须是 TypeScript类型即文档类型即契约选择 TypeScript 绝非跟风。在“agent-skills”体系中TypeScript 的类型系统承担着三重不可替代的角色设计文档、编译期守门员、IDE 智能助手。我们来看一个真实案例。某次为物流系统添加trackPackage技能时后端同学定义了输入接口interface TrackPackageInput { trackingNumber: string; carrierCode: SF | ZTO | YD; timeoutMs?: number; }仅仅这一行carrierCode: SF | ZTO | YD就让前端在调用前就明确知道只支持这三家快递避免了传入EMS导致的 400 错误让测试同学自动生成覆盖全部枚举值的用例让 CI 流程在 PR 提交时就拦截掉任何非法 carrierCode 的调用代码。如果用 JavaScript这些约束只能靠注释或运行时校验而注释会过期运行时校验则意味着错误被推到了集成测试甚至线上阶段。更关键的是TypeScript 的泛型和条件类型让“技能契约”具备了强大的表达力。比如我们定义了一个通用的retryableSkill高阶函数它接收任意技能并返回一个带重试逻辑的包装版function withRetryTInput, TOutput( skill: SkillTInput, TOutput, options: { maxRetries: number; baseDelayMs: number } ): SkillTInput, TOutput { // 实现细节... }这里TInput和TOutput的泛型约束确保了包装后的技能其输入输出类型与原始技能完全一致。调用方拿到withRetry(sendEmail)IDE 依然能精准提示sendEmail的所有参数和返回值类型零学习成本。这种“类型穿透”能力在 JavaScript 中根本无法实现。我曾用 Babel 插件尝试给 JS 项目加类似能力结果是类型定义与实际运行时行为严重脱节最终放弃。TypeScript 不是“加了类型检查的 JS”它是为构建高可靠性契约系统而生的语言。2.3 为什么必须是 Nx单体工作区是技能生态的温床“agent-skills”绝不是一个孤立的 npm 包。它的生命力完全依赖于 Nx 构建的单体工作区Monorepo。原因很简单技能不是一次性函数它们是一个需要持续演进、相互依赖、共同治理的生态系统。想象一下你有auth.login、auth.refreshToken、auth.logout三个技能它们共享同一套 JWT 解析逻辑和密钥管理配置。如果每个技能都作为独立包发布那么修改 JWT 解析逻辑需要同时发布myorg/auth-core、myorg/auth-login、myorg/auth-refresh三个包并确保所有下游服务升级到兼容版本auth.login的单元测试想复用auth-core的 mock 数据工厂却要跨包引用导致测试启动变慢、依赖关系混乱想给所有auth.*技能统一添加请求追踪头X-Trace-ID得在三个包里分别改代码、分别提交 PR、分别走 CI。而 Nx 的工作区结构让这一切变得自然/libs /skills /auth # auth.login, auth.refreshToken 等技能实现 /email # email.send-transactional 等 /db # db.query-user-profile 等 /core /types # Skill, SkillResult, SkillError 等核心类型定义 /utils # retry, timeout, logger 等通用工具 /shared /schemas # zod schema 定义如 EmailAddressSchemaNx 的project.json文件精确描述了每个技能包的依赖关系。/skills/auth明确依赖/core/types和/shared/schemasNx 的影响分析nx affected能瞬间告诉你修改/core/types/SkillError.ts会影响哪些技能包从而精准触发相关 CI 流程。更重要的是Nx 的build和test命令天然支持增量构建——如果只改了/skills/emailCI 只会重新构建和测试它及其直接依赖而不是整个工作区。我们在一个拥有 87 个技能的项目中实测全量测试耗时 22 分钟而 Nx 的增量测试平均仅需 47 秒。这种效率是任何基于独立 npm 包的方案都无法企及的。它让“技能”从一个个孤岛变成了一个有机生长的森林。2.4 为什么必须是 semantic-release自动化版本是契约演进的生命线当“agent-skills”成为一个被多个业务线、数十个微服务共同依赖的基础设施时版本管理就成了生死线。手动维护package.json中的version字段然后npm publish这种做法在“agent-skills”场景下是灾难性的。因为每一次版本发布都意味着一次契约变更。而契约变更必须满足严格的语义化版本规则SemVer补丁版本x.y.Z仅修复 bug不改变输入/输出契约不新增能力。例如修复email.send-transactional中一个导致 HTML 标签未转义的安全漏洞。次要版本x.Y.z新增向后兼容的能力如增加email.send-broadcast技能或为现有技能增加可选参数如为db.query-user-profile新增includeArchived?: boolean参数但绝不移除或修改现有必填参数。主版本X.y.z破坏性变更如移除auth.login技能或修改其输入接口将password字段改为passwordHash。这要求所有调用方必须主动适配。semantic-release 的价值在于它将这套规则自动化、不可篡改地嵌入到 CI 流程中。我们配置它监听main分支的合并根据提交消息的前缀fix:、feat:、BREAKING CHANGE:自动判断应发布的版本号并生成符合规范的 CHANGELOG。这意味着开发者无需记住“这次该发 1.2.3 还是 1.3.0”只需按规范写提交信息所有发布都是原子操作git tag、npm publish、CHANGELOG更新、GitHub Release 创建一步到位杜绝人为失误下游服务通过^1.2.0这样的范围依赖能安全地自动获取所有补丁和次要版本更新享受 bug 修复和新能力而不会意外引入破坏性变更。我亲眼见过一个团队因手动发布失误将一个包含BREAKING CHANGE的提交打上了1.2.3补丁版本号导致所有依赖它的服务在部署后大面积报错。而 semantic-release 的自动化流程从源头上杜绝了这种可能性。它让“agent-skills”的契约演进成为一条清晰、可追溯、可信赖的河流而不是一场充满不确定性的豪赌。3. 核心细节解析从零搭建一个可运行的 agent-skills 工作区3.1 初始化 Nx 工作区与基础结构我们从零开始搭建一个最小可行的agent-skills工作区。注意这不是一个“Hello World”教程而是基于真实项目经验提炼的、经过生产环境验证的初始化路径。首先确保你已安装npm和nvmNode Version Manager这是管理 Node.js 版本的基石。我们推荐使用 Node.js 18.x LTS因为它对node:util等内置模块的 ESM 支持最成熟能避免网络热词中频繁出现的SyntaxError: The requested module node:util does not provide an export named这类问题。# 使用 nvm 切换到 Node 18 nvm install 18 nvm use 18 # 全局安装 Nx CLI推荐避免本地 node_modules 冗余 npm install -g nx # 创建新的 Nx 工作区命名为 agent-skills-workspace npx create-nx-workspacelatest agent-skills-workspace --presetapps --nx-cloudfalse --package-managernpm # 进入工作区目录 cd agent-skills-workspace这一步创建了一个基础工作区。接下来我们需要按照“agent-skills”的领域划分创建核心库。关键原则是先定义契约再实现能力。因此第一步永远是创建agent-skills/core库它只包含类型定义不包含任何运行时逻辑。# 创建 core 库存放所有核心类型 nx g nrwl/node:library core --directorylibs --importPathagent-skills/core --publishable --no-interactive # 创建 skills 库作为所有具体技能的父容器 nx g nrwl/node:library skills --directorylibs --importPathagent-skills/skills --publishable --no-interactive # 创建 shared 库存放共享的 schema 和工具 nx g nrwl/node:library shared --directorylibs --importPathagent-skills/shared --publishable --no-interactive此时工作区结构如下/libs /core /src /index.ts # 导出所有核心类型 /lib /skill.ts # Skill 接口定义 /result.ts # SkillResult, SkillError 定义 /skills /src /index.ts # 空后续按需导出具体技能 /shared /src /index.ts # 导出共享 schema 和工具 /lib /schemas.ts # zod schema 定义 /utils.ts # 通用工具函数提示--publishable参数至关重要它告诉 Nx 这个库需要被构建为一个可发布的 npm 包。--importPath则定义了该库在代码中的导入路径如import { Skill } from agent-skills/core。这为后续的自动化发布奠定了基础。3.2 定义核心契约Skill、SkillResult 与 SkillError现在我们深入libs/core/src/lib/skill.ts编写“agent-skills”的心脏。这里的每一行代码都在定义能力交互的宪法。// libs/core/src/lib/skill.ts import { z } from zod; /** * 技能执行的元数据用于可观测性 */ export interface SkillMetadata { /** 技能执行耗时毫秒 */ durationMs: number; /** 重试次数 */ retries: number; /** 分布式追踪 ID */ traceId: string; } /** * 技能错误的标准结构 * code 是机器可读的唯一标识符用于自动化决策 * level 表示错误严重程度影响告警级别和用户提示 * suggestion 是给调用方的明确行动指南 */ export interface SkillError { code: string; level: fatal | warning | info; message: string; suggestion: string; /** 可选的原始错误对象用于调试 */ originalError?: unknown; } /** * 技能执行结果的统一结构 * output 是泛型由具体技能定义 * error 是 SkillError 的可选实例 * metadata 提供可观测性数据 */ export interface SkillResultTOutput unknown { success: boolean; output?: TOutput; error?: SkillError; metadata: SkillMetadata; } /** * 技能的执行上下文提供运行时环境信息 * 这是技能与外部世界交互的唯一通道避免全局变量污染 */ export interface SkillContext { /** 日志记录器预置了 traceId */ logger: { info: (msg: string, ...args: any[]) void; warn: (msg: string, ...args: any[]) void; error: (msg: string, ...args: any[]) void; }; /** 配置项来自工作区的 config 文件 */ config: Recordstring, unknown; /** 用于发起 HTTP 请求的客户端 */ httpClient: { get: T(url: string) PromiseT; post: T(url: string, body: unknown) PromiseT; }; } /** * 技能的核心接口定义 * TInput 是输入契约必须是 zod schema 的 infer 类型 * TOutput 是输出契约 * context 是运行时上下文 */ export interface SkillTInput, TOutput { /** * 技能的唯一标识符格式为 domain.action * 例如: email.send-transactional, db.query-user-profile */ id: string; /** * 输入契约的 zod schema * 这是运行时验证的唯一依据也是 TypeScript 类型的来源 */ inputSchema: z.ZodSchemaTInput; /** * 技能的执行函数 * 必须返回 SkillResultTOutput * 必须能处理所有由 inputSchema 定义的输入 */ execute: ( input: TInput, context: SkillContext ) PromiseSkillResultTOutput; }这段代码看似简单却蕴含了深刻的设计思想。Skill接口强制要求inputSchema这确保了所有技能都必须通过zod进行运行时验证堵死了“类型正确但数据非法”的漏洞。SkillContext的设计则彻底隔离了技能的实现细节——它不知道自己运行在 Express 还是 NestJS 上也不知道日志是输出到控制台还是发送到 ELK它只通过context.logger和context.httpClient这两个契约与外界通信。这使得技能的单元测试变得极其简单你只需 mock 一个SkillContext对象就能 100% 覆盖所有执行路径无需启动任何服务器或数据库。3.3 创建第一个技能email.send-transactional现在我们来实现第一个具体的技能email.send-transactional。它将被放在libs/skills/src/lib/email/send-transactional.ts。// libs/skills/src/lib/email/send-transactional.ts import { z } from zod; import { Skill, SkillResult, SkillError, SkillContext } from agent-skills/core; import { EmailAddressSchema, TemplateIdSchema } from agent-skills/shared; /** * 定义输入契约的 zod schema * 这里展示了如何组合共享 schema 并添加业务特定约束 */ const SendTransactionalEmailInputSchema z.object({ to: EmailAddressSchema.describe(收件人邮箱地址), templateId: TemplateIdSchema.describe(邮件模板 ID), context: z.record(z.unknown()).describe(模板渲染上下文), // 添加一个业务特定的可选字段 priority: z.enum([low, normal, high]).default(normal).describe(发送优先级), }); // TypeScript 类型由 schema 自动推导保证绝对一致 type SendTransactionalEmailInput z.infertypeof SendTransactionalEmailInputSchema; /** * 定义输出契约 * 这里是一个简单的成功标识实际项目中可能是 { messageId: string } */ type SendTransactionalEmailOutput { messageId: string; }; /** * 实现 Skill 接口 * 注意id 字段是硬编码的字符串这是契约的一部分不能动态生成 */ export const sendTransactionalEmail: Skill SendTransactionalEmailInput, SendTransactionalEmailOutput { id: email.send-transactional, inputSchema: SendTransactionalEmailInputSchema, async execute(input, context) { try { // 1. 记录开始日志 context.logger.info(Starting email.send-transactional for ${input.to}); // 2. 构建请求体调用内部邮件服务 API const payload { to: input.to, templateId: input.templateId, context: input.context, priority: input.priority, }; // 3. 使用 SkillContext 提供的 httpClient 发起请求 // 这里假设内部邮件服务 API 地址存储在 context.config 中 const response await context.httpClient.post{ id: string }( ${context.config.emailServiceUrl}/send, payload ); // 4. 构建成功结果 const result: SkillResultSendTransactionalEmailOutput { success: true, output: { messageId: response.id }, error: undefined, metadata: { durationMs: Date.now() - performance.now(), // 简化版耗时计算 retries: 0, traceId: context.logger.traceId || unknown, }, }; context.logger.info(email.send-transactional succeeded for ${input.to}, { messageId: response.id, }); return result; } catch (err) { // 5. 统一错误处理转换为 SkillError let skillError: SkillError; if (err instanceof Error err.message.includes(rate limit)) { skillError { code: EMAIL_RATE_LIMIT_EXCEEDED, level: warning, message: 邮件发送频率超出限制, suggestion: 请检查配置或联系运维提升配额, }; } else if (err instanceof Error err.message.includes(invalid template)) { skillError { code: EMAIL_TEMPLATE_NOT_FOUND, level: fatal, message: 指定的邮件模板不存在, suggestion: 请确认 templateId 是否正确或检查模板服务状态, }; } else { // 未知错误归为通用错误 skillError { code: EMAIL_INTERNAL_ERROR, level: fatal, message: 邮件服务内部错误, suggestion: 请稍后重试或联系技术支持, originalError: err, }; } const result: SkillResultSendTransactionalEmailOutput { success: false, output: undefined, error: skillError, metadata: { durationMs: Date.now() - performance.now(), retries: 0, traceId: context.logger.traceId || unknown, }, }; context.logger.error( email.send-transactional failed for ${input.to}, { error: skillError, originalError: err } ); return result; } }, };这个实现展示了“agent-skills”的精髓契约驱动、错误分类、可观测性内建。inputSchema确保了输入的合法性execute函数内部的try/catch块将所有可能的异常都捕获并转化为预定义的SkillErrorcontext.logger的调用让每一步操作都有迹可循。最关键的是它没有引入任何外部依赖如nodemailer所有 I/O 操作都通过context.httpClient进行这使得它可以在任何 Node.js 环境中运行无论是本地开发、CI 测试还是生产 Kubernetes 集群。3.4 构建与发布配置 semantic-release 实现自动化为了让agent-skills/core和agent-skills/skills能够被其他项目消费我们必须配置 semantic-release。这需要在工作区根目录下进行一系列配置。首先安装必要的依赖# 在工作区根目录安装 semantic-release 及其插件 npm install --save-dev semantic-release semantic-release/commit-analyzer semantic-release/release-notes-generator semantic-release/npm semantic-release/github然后为每个可发布的库创建.releaserc配置文件。以libs/core为例在libs/core/.releaserc中写入{ branches: [main], plugins: [ semantic-release/commit-analyzer, semantic-release/release-notes-generator, [ semantic-release/npm, { npmPublish: true, pkgRoot: dist/libs/core } ], [ semantic-release/github, { assets: [dist/libs/core/*.tgz] } ] ] }这个配置告诉 semantic-release当main分支有新提交时分析提交信息semantic-release/commit-analyzer生成发布说明semantic-release/release-notes-generator然后将构建产物位于dist/libs/core发布到 npmsemantic-release/npm并上传 tarball 到 GitHub Releasesemantic-release/github。接下来配置 CI 流程。我们以 GitHub Actions 为例在.github/workflows/release.yml中定义name: Release on: push: branches: [main] # 只在 libs 目录下的文件变更时触发避免无关提交触发发布 paths: - libs/** jobs: release: runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 with: fetch-depth: 0 - name: Setup Node.js uses: actions/setup-nodev3 with: node-version: 18 registry-url: https://registry.npmjs.org/ - name: Install dependencies run: npm ci - name: Build all publishable libraries run: npx nx build --all --with-deps - name: Semantic Release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} run: npx semantic-release这个 CI 流程的关键点在于npx nx build --all --with-deps。它会自动分析依赖图只构建那些被修改的库及其所有上游依赖。例如如果只修改了libs/skills/src/lib/email/send-transactional.ts它会构建libs/skills和它所依赖的libs/core而不会浪费时间去构建libs/db。这极大地提升了 CI 效率。最后别忘了在package.json的scripts中添加一个本地发布的快捷命令方便开发调试// package.json { scripts: { release:local: npx semantic-release --dry-run --ci false --debug } }运行npm run release:localsemantic-release 会模拟整个发布流程打印出它将要发布的版本号和 CHANGELOG而不会真正发布到 npm。这是验证配置是否正确的最佳方式。4. 实操过程与核心环节实现一个完整的技能调用链路4.1 技能注册中心统一管理与发现在“agent-skills”体系中技能不是被零散调用的而是被一个中央注册中心Registry统一管理和发现。这并非一个复杂的运行时服务而是一个轻量级的、基于 TypeScript Map 的内存注册表。它的核心价值在于解耦调用方与实现方支持运行时动态加载与替换。我们在libs/core/src/lib/registry.ts中实现它// libs/core/src/lib/registry.ts import { Skill } from ./skill; /** * 技能注册中心 * 单例模式确保全局唯一 */ class SkillRegistry { private static instance: SkillRegistry; private skills: Mapstring, Skillunknown, unknown; private constructor() { this.skills new Map(); } static getInstance(): SkillRegistry { if (!SkillRegistry.instance) { SkillRegistry.instance new SkillRegistry(); } return SkillRegistry.instance; } /** * 注册一个技能 * 如果同 ID 技能已存在则抛出错误防止意外覆盖 */ registerTInput, TOutput(skill: SkillTInput, TOutput): void { if (this.skills.has(skill.id)) { throw new Error(Skill with id ${skill.id} is already registered); } this.skills.set(skill.id, skill); } /** * 根据 ID 获取技能 * 返回一个类型安全的 Skill 实例 */ getTInput, TOutput(id: string): SkillTInput, TOutput | undefined { const skill this.skills.get(id) as SkillTInput, TOutput | undefined; return skill; } /** * 获取所有已注册技能的 ID 列表 */ getAllIds(): string[] { return Array.from(this.skills.keys()); } } export const skillRegistry SkillRegistry.getInstance();这个注册中心的设计非常克制。它没有网络层、没有持久化、没有复杂的路由就是一个纯粹的内存 Map。这正是它的优势所在极简、可靠、无额外依赖。在应用启动时我们只需将所有技能导入并注册// apps/api/src/main.ts import { skillRegistry } from agent-skills/core; import { sendTransactionalEmail } from agent-skills/skills/email/send-transactional; // 在应用启动时注册所有技能 skillRegistry.register(sendTransactionalEmail); // 后续任何地方都可以通过 skillRegistry.get(email.send-transactional) 获取它这种设计带来了巨大的灵活性。例如在测试环境中我们可以注册一个模拟技能Mock Skill来替代真实的邮件发送// libs/skills/src/lib/email/mock-send-transactional.ts import { z } from zod; import { Skill, SkillResult } from agent-skills/core; export const mockSendTransactionalEmail: Skillunknown, { messageId: string } { id: email.send-transactional, inputSchema: z.any(), // 模拟技能不验证输入 async execute(input, context) { context.logger.info(MOCK: email.send-transactional called, { input }); return { success: true, output: { messageId: mock-${Date.now()} }, error: undefined, metadata: { durationMs: 1, retries: 0, traceId: mock-trace-id }, }; }, };然后在测试启动时用mockSendTransactionalEmail替换掉真实的sendTransactionalEmail。整个测试过程无需启动任何外部服务速度极快且 100% 可控。4.2 技能执行器注入上下文与执行策略有了注册中心下一步就是创建一个统一的技能执行器Skill Runner。它负责从注册中心获取技能、注入运行时上下文、执行技能并处理通用的横切关注点Cross-Cutting Concerns如重试、超时、日志、指标上报。我们在libs/core/src/lib/runner.ts中实现// libs/core/src/lib/runner.ts import { v4 as uuidv4 } from uuid; import { Skill, SkillResult, SkillContext, SkillError } from ./skill; import { skillRegistry } from ./registry; /** * 技能执行器的配置选项 */ export interface SkillRunnerOptions { /** 默认超时时间毫秒 */ defaultTimeoutMs?: number; /** 默认重试次数 */ defaultMaxRetries?: number; } /** * 技能执行器 * 它是调用方与技能实现之间的唯一桥梁 */ export class SkillRunner { private readonly options: SkillRunnerOptions; constructor(options: SkillRunnerOptions {}) { this.options { defaultTimeoutMs: 30000, defaultMaxRetries: 3, ...options, }; } /** * 执行一个技能 * param id 技能 ID * param input 技能输入 * param context 运行时上下文 * returns 技能执行结果 */ async runTInput, TOutput( id: string, input: TInput, context: OmitSkillContext, logger | httpClient { logger?: SkillContext[logger]; httpClient?: SkillContext[httpClient]; } ): PromiseSkillResultTOutput { // 1. 从注册中心获取技能 const skill skillRegistry.getTInput, TOutput(id); if (!skill) { const error: SkillError { code: SKILL_NOT_FOUND, level: fatal, message: Skill with id ${id} is not registered, suggestion: Please check if the skill is imported and registered in your application, }; return { success: false, output: undefined, error, metadata: { durationMs: 0, retries: 0, traceId: unknown }, }; } // 2. 创建带有 traceId 的完整 SkillContext const traceId context.logger?.traceId || uuidv4(); const fullContext: SkillContext { logger: { info: (msg, ...args) context.logger?.info?.([SKILL:${id}] ${msg}, ...args) ?? console.log(msg, ...args), warn: (msg, ...args) context.logger?.warn?.([SKILL:${id}] ${msg}, ...args) ?? console.warn(msg, ...args), error: (msg, ...args) context.logger?.error?.([SKILL:${id}] ${msg}, ...args)