news 2026/9/16 6:10:31

TypeScript+Node+NX智能体技能工程化框架

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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: stringmaxResults?: number,任何调用方传参错误在npm run build阶段就被拦截;Nx则解决多技能并行开发时的依赖拓扑、构建缓存、影响分析和增量测试问题——当你修改了SendEmailSkill,Nx能精准告诉你哪些集成测试、哪些下游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-weatheragent-skill-emailagent-skill-crm……听起来很解耦,实则埋下三颗定时炸弹。第一颗是版本漂移weather-skill升级到v2.0用了新HTTP客户端,但crm-skill还在用v1.5的旧SDK,当Agent Orchestrator同时加载两者时,Node.js的require缓存机制会导致同一依赖(如axios)被加载两次不同版本,内存暴涨且行为不可预测;第二颗是契约失联email-skillSendEmailInput接口新增了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 --name=flight-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.3,B紧接着发布1.2.3(Git 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 sendEmail

semantic-release监听Git Push事件,扫描最近一次Tag之后的所有commit,按类型自动计算版本号:fix类commit触发patch(1.2.3→1.2.4),feat类触发minor(1.2.4→1.3.0),BREAKING CHANGE标记触发major(1.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、LlamaIndex)90%的官方示例、插件、调试工具都基于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未正确reject,5分钟定位,2小时修复——这种效率在强类型静态语言中往往需要数天。

3. 核心技能模块拆解:从接口定义到错误治理的全链路实践

3.1 Skill接口的TypeScript契约设计:超越any的严谨性

一个合格的Agent Skill,其TypeScript接口绝不能是export interface Skill { execute(input: any): Promise<any> }。这种写法等于放弃所有类型安全,是工程倒退。真正的契约应分为三层:

第一层:输入契约(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 BaseSkill<Input, Output> { // 技能唯一标识,用于日志追踪和指标聚合 abstract readonly id: string; // 技能人类可读名称,用于管理后台展示 abstract readonly name: string; // 技能描述,支持Markdown,用于自动生成文档 abstract readonly description: string; // 执行入口,所有技能必须实现 abstract execute(input: Input): Promise<Output>; // 可选的初始化钩子,在Agent启动时调用一次 async initialize?(): Promise<void> { // 例如:预热HTTP连接池、加载本地词典 } // 可选的销毁钩子,在Agent关闭时调用 async destroy?(): Promise<void> { // 例如:优雅关闭数据库连接、清理临时文件 } } // 具体技能实现 export class SearchWebSkill extends BaseSkill<SearchWebSkillInput, SearchWebSkillOutput> { readonly id = 'search-web'; readonly name = '网页搜索'; readonly description = '使用主流搜索引擎获取实时网页结果'; constructor( private readonly searchClient: SearchApiClient, private readonly logger: Logger ) { super(); } async execute(input: SearchWebSkillInput): Promise<SearchWebSkillOutput> { 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→ 关联“技能平均响应时间”SLO(P95 < 2s)
  • RATE_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): Promise<SendEmailOutput> { 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-workspace@latest agent-skills --preset=apps是最常见的错误起点。这个命令创建的是“应用型”工作区,预设了React/Vue前端和Express后端,而我们的目标是纯库(Library)工作区。正确姿势是:

# 1. 创建空工作区(不选preset) npx create-nx-workspace@latest 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 --name=agent-skill-core --directory=libs --no-interactive # 5. 生成第一个技能(天气) nx g @nx/node:library --name=agent-skill-weather --directory=libs --no-interactive --importPath=@agent-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-corelibs/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 --target=build --all --configuration=production

若所有库都能成功编译,说明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 BaseSkill<SearchWebSkillInput, SearchWebSkillOutput> { readonly id = 'search-web'; readonly name = '网页搜索'; readonly description = '使用主流搜索引擎获取实时网页结果'; constructor( private readonly searchClient: ReturnType<typeof createSearchClient>, private readonly logger: pino.Logger ) { super(); } async execute(input: SearchWebSkillInput): Promise<SearchWebSkillOutput> { // 运行时校验 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: ReturnType<typeof createSearchWebSkill>; beforeEach(() => { // 使用jest.mock模拟axios jest.mock('axios'); const mockAxios = require('axios') as jest.Mocked<typeof 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-weather

4.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 Releasenx.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/checkout@v3 with: fetch-depth: 0 - uses: actions/setup-node@v3 with: node-version: '18' - run: npm ci - run: npx nx release --dry-run -
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 6:09:03

iLoader:基于usbmuxd的IPA本地安装工具详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 6:08:11

DeepSeek API连接不稳定?从故障分类到超时重试的完整排查指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 6:07:12

AW32025超低功耗Boost芯片实现48个月鼠标续航

1. 项目概述&#xff1a;为什么一只鼠标要谈“48个月超长续航”&#xff1f;你有没有算过&#xff0c;自己一年换几节AA电池&#xff1f;我拆过不下二十款市售无线鼠标&#xff0c;平均寿命在6到9个月——不是鼠标坏了&#xff0c;是电池先扛不住。Dell这次把“48个月超长续航”…

作者头像 李华
网站建设 2026/9/16 6:07:11

微信云开发实战:构建高可用校园生活圈小程序

简介&#xff1a;本资源是一套基于微信小程序云开发&#xff08;TCB&#xff09;构建的校园生活圈完整项目源码&#xff0c;面向前端初学者与小程序开发者&#xff0c;解决高校学生日常高频需求——匿名表白、失物招领、兼职对接与二手交易。项目采用云数据库存储结构化数据、云…

作者头像 李华
网站建设 2026/9/16 6:06:52

STM32C562 ADC电压采集实战:从原理到CubeMX配置与排错

继续咱们 STM32C562 开发连载&#xff0c;这一篇聊 ADC 电压采集。做过嵌入式的人都有体会&#xff0c;ADC 是 MCU 感知外部世界最直接的窗口&#xff1a;单片机只懂 0 和 1&#xff0c;但现实里不管是电池电压、温度传感器、电位器旋钮&#xff0c;还是变频器输出的模拟信号&a…

作者头像 李华
网站建设 2026/9/16 6:06:41

前端网络请求生存指南:从XHR、Fetch到Axios的原理与选型

1. 这不是技术演进史&#xff0c;而是一份前端网络请求的“生存指南”你写过多少次axios.get(/api/user)&#xff1f;又在控制台里见过多少次Failed to fetch或CORS error&#xff1f;这些报错背后&#xff0c;从来不是某一行代码写错了&#xff0c;而是你对浏览器和服务器之间…

作者头像 李华