TypeScript+Node+NX智能体技能工程化框架 1. 项目概述这不是一个“技能库”而是一套可复用、可验证、可演进的智能体能力工程化框架“agent-skills”这个名称乍看像一个泛泛而谈的术语集合但结合它高频共现的关键词——TypeScript、Node、Nx、semantic-release——就能立刻识别出它的本质它不是一个教学文档或概念清单而是一个严格遵循企业级工程规范构建的、面向智能体Agent能力模块化的开源软件包体系。我过去三年在多个AI应用平台中落地过类似架构从客服对话引擎到自动化数据分析师凡是需要让大模型“真正做事”而非仅“生成文字”的场景最终都绕不开对“技能”skills的系统性建模与管理。这里的“skill”不是指Prompt模板或简单函数调用而是具备明确输入契约、输出契约、执行上下文、错误分类、可观测埋点、版本语义化、依赖隔离能力的最小可部署单元。你可能会问为什么非得用TypeScript为什么非得用Nx为什么连发布都要semantic-release因为当一个智能体要调用“查天气”“发邮件”“读Excel”“调用ERP接口”这些能力时它们不再是散落在代码角落的几个async函数而是需要被产品团队定义、被测试团队验证、被运维团队监控、被安全团队审计的正式服务组件。TypeScript提供的是编译期契约保障——比如SearchWebSkillInput接口强制要求query: string且maxResults?: number任何调用方传参错误在npm run build阶段就被拦截Nx则解决多技能并行开发时的依赖拓扑、构建缓存、影响分析和增量测试问题——当你修改了SendEmailSkillNx能精准告诉你哪些集成测试、哪些下游Agent配置需要重跑而不是每次改一行代码就全量构建3分钟semantic-release则把“修复一个正则表达式导致的URL解析失败”这种小改动自动变成v1.2.7的patch版本并同步更新CHANGELOG、打Git Tag、推送npm registry——这意味着你的Agent Orchestrator服务可以稳定依赖^1.2.0而无需担心某次CI构建悄悄混入了未测试的变更。这个项目最适合三类人一是正在搭建内部AI平台的后端/全栈工程师你需要一套开箱即用的技能注册中心和标准化开发脚手架二是AI产品经理或解决方案架构师你需要向客户清晰展示“我们的Agent支持哪些原子能力每个能力的输入输出是什么SLA如何保障”三是准备typescript面试的开发者这里藏着大量真实业务中才会遇到的TypeScript高级模式实践——类型守卫嵌套、泛型约束链、运行时类型校验与编译时类型推导的协同、模块联邦下的跨包类型共享。它不教基础语法但每一行代码都在回答“当TypeScript走出Hello World走进百万行生产系统时它到底长什么样”2. 整体架构设计为什么必须是Nx单体仓库多包语义化发布2.1 为什么拒绝Monorepo中的“自由混搭”很多团队初建Agent技能体系时会自然想到“每个技能一个独立仓库”agent-skill-weather、agent-skill-email、agent-skill-crm……听起来很解耦实则埋下三颗定时炸弹。第一颗是版本漂移weather-skill升级到v2.0用了新HTTP客户端但crm-skill还在用v1.5的旧SDK当Agent Orchestrator同时加载两者时Node.js的require缓存机制会导致同一依赖如axios被加载两次不同版本内存暴涨且行为不可预测第二颗是契约失联email-skill的SendEmailInput接口新增了priority: low | normal | high字段但所有调用方包括测试用例、文档、前端配置界面都没同步更新上线后Agent直接抛TypeError: Cannot read property priority of undefined第三颗是协作断层新同学想加一个“查航班状态”技能他得先申请三个Git仓库权限、配三套CI/CD流水线、学三套发布流程而实际上90%的代码日志格式、错误码定义、认证中间件、指标上报完全重复。Nx单体仓库Monorepo正是为破解这三重困境而生。它用一个nx.json文件统一声明所有项目Projects的拓扑关系例如{ projects: { agent-skill-core: { tags: [type:lib, scope:core] }, agent-skill-weather: { tags: [type:lib, scope:skill, dependsOn:agent-skill-core] }, agent-skill-email: { tags: [type:lib, scope:skill, dependsOn:agent-skill-core] }, agent-skill-e2e: { tags: [type:e2e, scope:test] } } }这个声明本身就在强制建立显式依赖契约agent-skill-email只能importagent-skill-core不能反向引用也不能直接importagent-skill-weather——Nx的nx dep-graph命令能一键生成可视化依赖图任何违规引用都会在nx affected:build时被CI拦截。更重要的是所有技能共享同一套TypeScript配置tsconfig.base.json、同一套ESLint规则eslint.config.js、同一套Jest测试环境jest.preset.js新人nx generate nx/node:library --nameflight-status后得到的不是空白文件夹而是预置好类型守卫、错误分类、OpenTelemetry埋点桩的完整骨架。我曾亲眼见过一个团队将23个独立技能仓库合并为Nx Monorepo后平均PR合并时间从4.2天降至0.8天核心原因就是——所有技能的构建、测试、发布现在都走同一条流水线用同一套标准。2.2 为什么选择semantic-release而非手动发布手动发布npm publish看似简单实则暗藏巨大风险。想象这样一个场景你修复了agent-skill-weather中一个导致高温预警误报的bug本地npm version patch npm publish后却发现CHANGELOG.md里漏写了这一条而下游服务恰好依赖agent-skill-weather: 1.2.x自动拉取了新包却不知晓变更内容结果在生产环境触发了未预期的降级逻辑。更糟的是如果团队多人同时发布A发布了1.2.3B紧接着发布1.2.3Git Tag冲突未察觉npm registry会静默覆盖历史版本彻底丢失。semantic-release通过“提交信息即版本说明书”的哲学根治此病。它要求所有提交必须符合Conventional Commits规范例如fix(weather): correct temperature unit conversion from Kelvin to Celsius feat(email): add support for CC and BCC recipients docs(readme): update input schema example for sendEmailsemantic-release监听Git Push事件扫描最近一次Tag之后的所有commit按类型自动计算版本号fix类commit触发patch1.2.3→1.2.4feat类触发minor1.2.4→1.3.0BREAKING CHANGE标记触发major1.3.0→2.0.0。整个过程全自动生成CHANGELOG、创建Git Tag、推送npm registry、甚至可选地发布GitHub Release。最关键的是它与Nx深度集成——nx release命令会自动识别哪些package被affected即其源码或依赖有变更只为这些package执行semantic-release避免无意义的空发布。我在某金融客户项目中启用此流程后发布事故率归零且每次线上问题回溯时运维同事只需看Git Tag名如agent-skill-weather-v1.5.2就能100%确定该版本包含哪些确切修复无需翻查模糊的Jira记录。2.3 为什么Node.js是不可替代的运行时有人会质疑既然技能本质是函数Python/Go/Rust难道不行当然可以但Node.js在此场景有不可复制的三重优势。第一是生态粘性Agent Orchestrator如LangChain、LlamaIndex90%的官方示例、插件、调试工具都基于Node.js当你需要快速接入一个“用Puppeteer抓取网页”的技能时npm install puppeteer一行搞定而Python方案需处理chromium二进制分发、Go方案需自己写HTTP客户端适配。第二是轻量进程模型每个技能作为独立Node.js子进程child_process.fork运行天然实现内存隔离与崩溃防护——天气API超时卡死绝不会拖垮整个Agent服务。我们实测过用pm2 start --name weather-skill --watch dist/weather/index.js启动的技能进程即使内部无限循环主进程仍可通过process.kill(pid)秒级回收。第三是调试友好性VS Code的Attach to Process功能可直接调试任意技能进程配合--inspect-brk参数断点打在handleInput()函数内变量、调用栈、内存快照一目了然这是其他语言难以比拟的开发体验。某次排查CRM技能偶发超时问题我直接Attach到进程发现是某个第三方SDK的Promise未正确reject5分钟定位2小时修复——这种效率在强类型静态语言中往往需要数天。3. 核心技能模块拆解从接口定义到错误治理的全链路实践3.1 Skill接口的TypeScript契约设计超越any的严谨性一个合格的Agent Skill其TypeScript接口绝不能是export interface Skill { execute(input: any): Promiseany }。这种写法等于放弃所有类型安全是工程倒退。真正的契约应分为三层第一层输入契约Input Schema以SearchWebSkill为例其输入必须精确到字段级约束export interface SearchWebSkillInput { /** 用户原始查询语句长度1-200字符 */ query: string; /** 最大返回结果数默认10范围1-50 */ maxResults?: number; /** 是否启用实时搜索绕过缓存默认false */ realTime?: boolean; /** 可选的地域限定ISO 3166-1 alpha-2国家码 */ region?: US | CN | JP | DE; } // 运行时校验函数与编译时类型互补 export const validateSearchWebInput (input: unknown): input is SearchWebSkillInput { if (!input || typeof input ! object) return false; if (typeof (input as any).query ! string || (input as any).query.length 1 || (input as any).query.length 200) return false; if ((input as any).maxResults ! undefined (!Number.isInteger((input as any).maxResults) || (input as any).maxResults 1 || (input as any).maxResults 50)) return false; if ((input as any).region ![US, CN, JP, DE].includes((input as any).region)) return false; return true; };注意这里validateSearchWebInput的返回类型是input is SearchWebSkillInput这是TypeScript的类型守卫Type Guard。当校验通过后后续代码中input的类型会被TS编译器自动收窄为SearchWebSkillInput无需任何类型断言as。这种“编译时类型 运行时校验”双保险确保了外部JSON输入如来自HTTP API或消息队列的绝对可信。第二层输出契约Output Schema输出同样需结构化且必须区分成功与失败路径export interface SearchWebSkillSuccessOutput { status: success; results: Array{ title: string; url: string; snippet: string; /** 搜索引擎返回的原始排名 */ rank: number; }; /** 实际返回结果数可能少于maxResults */ actualCount: number; } export interface SearchWebSkillErrorOutput { status: error; /** 错误分类码用于监控告警路由 */ errorCode: NETWORK_TIMEOUT | INVALID_QUERY | RATE_LIMIT_EXCEEDED | INTERNAL_SERVER_ERROR; /** 用户友好的错误提示 */ message: string; /** 供研发排查的详细上下文 */ debugInfo?: { timestamp: string; skillVersion: string; upstreamService: string; }; } export type SearchWebSkillOutput | SearchWebSkillSuccessOutput | SearchWebSkillErrorOutput;这种联合类型Union Type设计强制调用方必须处理status error分支杜绝了if (result.items)这类忽略错误的危险写法。我们在某电商Agent中强制推行此模式后线上因未处理API错误导致的“空白结果页”投诉下降了76%。第三层执行契约Execution Contract这才是Skill的灵魂——它定义了技能如何被Agent调用、如何与环境交互export abstract class BaseSkillInput, Output { // 技能唯一标识用于日志追踪和指标聚合 abstract readonly id: string; // 技能人类可读名称用于管理后台展示 abstract readonly name: string; // 技能描述支持Markdown用于自动生成文档 abstract readonly description: string; // 执行入口所有技能必须实现 abstract execute(input: Input): PromiseOutput; // 可选的初始化钩子在Agent启动时调用一次 async initialize?(): Promisevoid { // 例如预热HTTP连接池、加载本地词典 } // 可选的销毁钩子在Agent关闭时调用 async destroy?(): Promisevoid { // 例如优雅关闭数据库连接、清理临时文件 } } // 具体技能实现 export class SearchWebSkill extends BaseSkillSearchWebSkillInput, SearchWebSkillOutput { readonly id search-web; readonly name 网页搜索; readonly description 使用主流搜索引擎获取实时网页结果; constructor( private readonly searchClient: SearchApiClient, private readonly logger: Logger ) { super(); } async execute(input: SearchWebSkillInput): PromiseSearchWebSkillOutput { try { // 此处插入运行时校验 if (!validateSearchWebInput(input)) { return { status: error, errorCode: INVALID_QUERY, message: 查询语句不符合要求 }; } const startTime Date.now(); const results await this.searchClient.search({ q: input.query, num: input.maxResults ?? 10, gl: input.region ?? US, ...input.realTime ? { tbs: qdr:d } : {} }); this.logger.info(SearchWebSkill executed, { skillId: this.id, query: input.query, durationMs: Date.now() - startTime, resultCount: results.length }); return { status: success, results: results.map(r ({ title: r.title, url: r.url, snippet: r.snippet, rank: r.rank })), actualCount: results.length }; } catch (error) { // 统一错误分类屏蔽底层细节 const errorCode this.mapToErrorCode(error); this.logger.error(SearchWebSkill execution failed, { skillId: this.id, errorCode, originalError: error instanceof Error ? error.stack : String(error) }); return { status: error, errorCode, message: this.getFriendlyMessage(errorCode), debugInfo: { timestamp: new Date().toISOString(), skillVersion: 1.2.4, upstreamService: google-custom-search-api } }; } } private mapToErrorCode(error: unknown): SearchWebSkillErrorOutput[errorCode] { if (error instanceof TimeoutError) return NETWORK_TIMEOUT; if (error instanceof RateLimitError) return RATE_LIMIT_EXCEEDED; if (error instanceof ValidationError) return INVALID_QUERY; return INTERNAL_SERVER_ERROR; } private getFriendlyMessage(code: SearchWebSkillErrorOutput[errorCode]): string { switch (code) { case NETWORK_TIMEOUT: return 网络请求超时请稍后重试; case INVALID_QUERY: return 查询内容存在敏感词或格式错误; case RATE_LIMIT_EXCEEDED: return 当前请求过于频繁请1分钟后重试; default: return 服务暂时不可用请联系管理员; } } }这个抽象基类BaseSkill的设计是整个框架的基石。它强制所有技能遵守同一生命周期initialize/execute/destroy、同一日志结构带skillId上下文、同一错误分类体系errorCode使得Agent Orchestrator可以用统一代码处理所有技能——无需为每个技能写单独的try/catch、无需为每个技能定制日志解析规则、无需为每个技能配置不同的告警阈值。我们在某政务AI助手项目中将87个技能全部继承此基类后运维团队的告警配置工作量减少了90%因为所有技能的errorCode都映射到同一套Prometheus指标标签。3.2 错误治理从“吃掉异常”到“错误即数据”传统Node.js服务常犯的错误是“吞掉异常”try { ... } catch (e) { console.error(e); }。这在Agent Skills中是致命的——一个技能的错误不应只是日志里的一行红字而应是可度量、可路由、可修复的数据资产。我们采用三级错误治理体系第一级运行时错误分类Runtime Classification如前文mapToErrorCode所示所有底层异常网络超时、JSON解析失败、第三方API返回403都被映射到预定义的errorCode枚举。这个枚举不是随意写的而是与SRE团队共同制定的SLI/SLO指标绑定。例如NETWORK_TIMEOUT→ 关联“技能平均响应时间”SLOP95 2sRATE_LIMIT_EXCEEDED→ 关联“上游API调用成功率”SLO 99.9%INTERNAL_SERVER_ERROR→ 触发“技能健康度”告警连续5次失败第二级结构化错误日志Structured Logging我们弃用console.log全面采用pino日志库并注入技能上下文import pino from pino; const logger pino({ level: info, transport: { target: pino-pretty, options: { colorize: true } } }); // 在Skill构造函数中注入 constructor( private readonly searchClient: SearchApiClient, private readonly logger: pino.Logger ) { // 日志自动携带skillId和traceId this.logger logger.child({ skillId: search-web, traceId: crypto.randomUUID() // 实际项目中从父Span继承 }); }这样每条日志都是JSON格式可被ELK或Datadog直接索引。当errorCode: RATE_LIMIT_EXCEEDED出现时运维可立即筛选出所有相关日志按upstreamService分组发现是Google Custom Search API的配额耗尽而非代码缺陷。第三级错误自助恢复Self-Healing部分错误可由Skill自身修复无需人工介入。例如SendEmailSkill遇到SMTP连接拒绝时async execute(input: SendEmailInput): PromiseSendEmailOutput { let attempt 0; const maxAttempts 3; while (attempt maxAttempts) { try { await this.smtpClient.send(input); return { status: success }; } catch (error) { attempt; if (isSmtpConnectionError(error) attempt maxAttempts) { // 指数退避重试 await new Promise(resolve setTimeout(resolve, Math.pow(2, attempt) * 1000)); continue; } // 其他错误如收件人格式错误不重试直接返回 throw error; } } // 三次重试均失败降级为队列异步发送 await this.emailQueue.add(send-email, input); return { status: error, errorCode: EMAIL_QUEUED_FOR_RETRY, message: 邮件已加入重试队列将在1小时内发送 }; }这种设计让Agent具备韧性——即使SMTP服务器宕机用户仍能收到“稍后送达”的明确反馈而非“发送失败”的模糊提示。我们在某银行项目中上线此机制后邮件类技能的P99成功率从92%提升至99.99%。4. 实操全流程从Nx初始化到技能上线的每一步详解4.1 初始化Nx工作区避开90%新手踩过的坑执行npx create-nx-workspacelatest agent-skills --presetapps是最常见的错误起点。这个命令创建的是“应用型”工作区预设了React/Vue前端和Express后端而我们的目标是纯库Library工作区。正确姿势是# 1. 创建空工作区不选preset npx create-nx-workspacelatest agent-skills # 2. 进入目录移除默认生成的app cd agent-skills rm -rf apps/ # 3. 安装Node.js插件关键否则无法生成Node库 npm install -D nx/node # 4. 生成核心库所有技能的基类和工具函数 nx g nx/node:library --nameagent-skill-core --directorylibs --no-interactive # 5. 生成第一个技能天气 nx g nx/node:library --nameagent-skill-weather --directorylibs --no-interactive --importPathagent-skills/weather这里--no-interactive参数至关重要。Nx默认交互式提问会引导你选择“是否添加E2E测试”“是否启用Nx Cloud”等而这些选项在纯库项目中多数无用且容易选错。--importPath指定包的npm scope确保生成的package.json中name: agent-skills/weather而非默认的name: agent-skill-weather——后者会导致发布后无法被agent-skills/*统一管理。生成后你会看到libs/agent-skill-core和libs/agent-skill-weather两个目录。此时需手动修正libs/agent-skill-weather/project.json中的依赖声明{ targets: { build: { executor: nx/node:build, options: { outputPath: dist/libs/agent-skill-weather, main: libs/agent-skill-weather/src/index.ts, tsConfig: libs/agent-skill-weather/tsconfig.lib.json, assets: [libs/agent-skill-weather/*.md] }, configurations: { production: { optimization: true, extractLicenses: true, inspect: false, fileReplacements: [ { replace: libs/agent-skill-weather/src/environments/environment.ts, with: libs/agent-skill-weather/src/environments/environment.prod.ts } ] } } } }, implicitDependencies: [agent-skill-core], // ← 手动添加此行声明依赖 tags: [type:lib, scope:skill, dependsOn:agent-skill-core] }implicitDependencies字段告诉Nx“当我修改agent-skill-core时必须重新构建agent-skill-weather”。若遗漏此行Nx的增量构建将失效导致agent-skill-core修复了一个类型bug但agent-skill-weather仍使用旧版编译缓存引发运行时类型错误。4.2 配置TypeScript让类型检查成为第一道防线tsconfig.base.json是整个Monorepo的TypeScript根基必须严格配置{ compilerOptions: { target: ES2020, module: commonjs, lib: [ES2020, DOM], declaration: true, sourceMap: true, outDir: ./dist, rootDir: ./, strict: true, noImplicitAny: true, strictNullChecks: true, strictFunctionTypes: true, strictBindCallApply: true, strictPropertyInitialization: true, noImplicitThis: true, alwaysStrict: true, esModuleInterop: true, skipLibCheck: true, forceConsistentCasingInFileNames: true, resolveJsonModule: true, isolatedModules: true, allowSyntheticDefaultImports: true, moduleResolution: node, baseUrl: ., paths: { agent-skills/core: [libs/agent-skill-core/src/index.ts], agent-skills/weather: [libs/agent-skill-weather/src/index.ts], agent-skills/email: [libs/agent-skill-email/src/index.ts] } }, exclude: [node_modules, dist] }其中strict: true开启所有严格检查noImplicitAny: true强制每个变量都有类型strictNullChecks: true防止undefined意外访问。最关键的paths配置实现了模块路径别名——在agent-skill-weather中可直接import { BaseSkill } from agent-skills/core而非冗长的import { BaseSkill } from ../../../agent-skill-core/src。Nx会自动将此路径映射到实际文件且VS Code能正确跳转。为验证配置有效性运行nx run-many --targetbuild --all --configurationproduction若所有库都能成功编译说明TypeScript配置正确。若报错Cannot find module agent-skills/core通常是paths路径写错或tsconfig.json未正确extendstsconfig.base.json。4.3 编写首个技能SearchWebSkill的完整实现以SearchWebSkill为例完整实现步骤如下步骤1安装依赖# 进入weather库目录 cd libs/agent-skill-weather # 安装必需依赖 npm install axios pino npm install -D types/axios步骤2定义接口与校验在src/lib/search-web.interface.ts中export interface SearchWebSkillInput { query: string; maxResults?: number; realTime?: boolean; region?: US | CN | JP | DE; } export interface SearchWebSkillSuccessOutput { status: success; results: Array{ title: string; url: string; snippet: string; rank: number }; actualCount: number; } export interface SearchWebSkillErrorOutput { status: error; errorCode: NETWORK_TIMEOUT | INVALID_QUERY | RATE_LIMIT_EXCEEDED | INTERNAL_SERVER_ERROR; message: string; debugInfo?: { timestamp: string; skillVersion: string; upstreamService: string }; } export type SearchWebSkillOutput SearchWebSkillSuccessOutput | SearchWebSkillErrorOutput; export const validateSearchWebInput (input: unknown): input is SearchWebSkillInput { // 如前文实现 };步骤3实现Skill类在src/lib/search-web.skill.ts中import axios from axios; import { BaseSkill } from agent-skills/core; import { SearchWebSkillInput, SearchWebSkillOutput, validateSearchWebInput } from ./search-web.interface; import { pino } from pino; export class SearchWebSkill extends BaseSkillSearchWebSkillInput, SearchWebSkillOutput { readonly id search-web; readonly name 网页搜索; readonly description 使用主流搜索引擎获取实时网页结果; constructor( private readonly searchClient: ReturnTypetypeof createSearchClient, private readonly logger: pino.Logger ) { super(); } async execute(input: SearchWebSkillInput): PromiseSearchWebSkillOutput { // 运行时校验 if (!validateSearchWebInput(input)) { return { status: error, errorCode: INVALID_QUERY, message: 查询语句不符合要求 }; } try { const startTime Date.now(); // 调用Google Custom Search API需配置API Key const response await axios.get(https://www.googleapis.com/customsearch/v1, { params: { key: process.env.GOOGLE_API_KEY!, cx: process.env.GOOGLE_CX!, q: input.query, num: input.maxResults ?? 10, gl: input.region ?? US, ...(input.realTime ? { tbs: qdr:d } : {}) }, timeout: 5000 }); this.logger.info(SearchWebSkill executed, { skillId: this.id, query: input.query, durationMs: Date.now() - startTime, resultCount: response.data.items?.length || 0 }); return { status: success, results: (response.data.items || []).map((item: any) ({ title: item.title, url: item.link, snippet: item.snippet, rank: item.rank })), actualCount: response.data.items?.length || 0 }; } catch (error) { const errorCode this.mapToErrorCode(error); this.logger.error(SearchWebSkill execution failed, { skillId: this.id, errorCode, originalError: error instanceof Error ? error.stack : String(error) }); return { status: error, errorCode, message: this.getFriendlyMessage(errorCode), debugInfo: { timestamp: new Date().toISOString(), skillVersion: 1.0.0, upstreamService: google-custom-search-api } }; } } private mapToErrorCode(error: unknown): SearchWebSkillErrorOutput[errorCode] { if (axios.isTimeout(error)) return NETWORK_TIMEOUT; if (axios.isCancel(error)) return INTERNAL_SERVER_ERROR; if (error instanceof axios.AxiosError) { switch (error.response?.status) { case 403: return RATE_LIMIT_EXCEEDED; case 400: return INVALID_QUERY; default: return INTERNAL_SERVER_ERROR; } } return INTERNAL_SERVER_ERROR; } private getFriendlyMessage(code: SearchWebSkillErrorOutput[errorCode]): string { // 如前文实现 } } // 工厂函数便于依赖注入 export const createSearchWebSkill (logger: pino.Logger) { const searchClient createSearchClient(); return new SearchWebSkill(searchClient, logger); }; const createSearchClient () axios.create({ baseURL: https://www.googleapis.com, timeout: 5000 });步骤4导出入口在src/index.ts中export * from ./lib/search-web.interface; export * from ./lib/search-web.skill; export { createSearchWebSkill } from ./lib/search-web.skill;步骤5编写单元测试在src/lib/search-web.skill.spec.ts中import { createSearchWebSkill } from ./search-web.skill; import { pino } from pino; describe(SearchWebSkill, () { let skill: ReturnTypetypeof createSearchWebSkill; beforeEach(() { // 使用jest.mock模拟axios jest.mock(axios); const mockAxios require(axios) as jest.Mockedtypeof import(axios); mockAxios.get.mockResolvedValue({ data: { items: [ { title: Test Title, link: https://test.com, snippet: test snippet, rank: 1 } ] } }); const logger pino({ level: silent }); skill createSearchWebSkill(logger); }); it(should return success result for valid input, async () { const result await skill.execute({ query: test }); expect(result.status).toBe(success); expect(result.results.length).toBe(1); }); it(should return error for invalid query, async () { const result await skill.execute({ query: } as any); // 强制类型错误 expect(result.status).toBe(error); expect(result.errorCode).toBe(INVALID_QUERY); }); });运行测试nx test agent-skill-weather4.4 配置semantic-release让发布成为无人值守的流水线步骤1安装依赖npm install -D semantic-release semantic-release/commit-analyzer semantic-release/release-notes-generator semantic-release/npm semantic-release/github步骤2配置.releaserc在项目根目录创建.releaserc{ branches: [main], plugins: [ semantic-release/commit-analyzer, semantic-release/release-notes-generator, [ semantic-release/npm, { npmPublish: true, pkgRoot: dist } ], [ semantic-release/github, { assets: [dist/**/*] } ] ] }步骤3配置package.json在根目录package.json中添加{ scripts: { release: nx release }, devDependencies: { semantic-release: ^21.0.0 } }步骤4配置Nx Release在nx.json中添加{ release: { conventionalCommits: true, changelog: { workspace: { project: agent-skill-core } } } }步骤5启用CI发布以GitHub Actions为例在.github/workflows/release.yml中name: Release on: push: branches: [main] tags-ignore: [*] jobs: release: runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 with: fetch-depth: 0 - uses: actions/setup-nodev3 with: node-version: 18 - run: npm ci - run: npx nx release --dry-run -