1. 项目概述:一个被严重低估的“技能中枢”设计范式
“agent-skills”这个词乍看像某个开源库的包名,或是某篇技术文档里的小节标题,但如果你在Node.js、TypeScript和Nx生态里摸爬滚打超过三年,就会立刻意识到——它不是功能模块,而是一套可复用、可组合、可版本化、可测试的智能体能力单元抽象体系。我第一次在Nx monorepo里看到libs/agent-skills这个目录时,以为只是几个工具函数的集合;直到把@myorg/agent-skills-email、@myorg/agent-skills-sql、@myorg/agent-skills-http三个包同时接入一个LLM调用链路,才真正理解它的设计哲学:把AI Agent的“手”和“脚”从“脑”里彻底解耦。
这完全不是简单的工具函数封装。比如一个“发送邮件”的技能,它必须自带输入Schema校验(收件人格式、附件大小限制)、失败重试策略(指数退避+最大3次)、上下文注入能力(自动带入当前会话ID和用户偏好)、可观测性埋点(耗时、成功率、错误分类),还要能被语义化版本管理(v1.2.0 → v1.3.0可能新增了对DKIM签名的支持)。这些能力,如果散落在各个Agent服务里,半年后你就会发现:改一个邮箱模板要动5个服务,加一个重试逻辑要同步7处代码,查一次失败原因得翻遍4个日志系统。而agent-skills的出现,就是为了解决这种“能力碎片化”带来的维护熵增。
它特别适合三类人:一是正在用NestJS或Express构建AI服务中台的后端工程师,你需要把LLM调用前后的所有确定性逻辑(数据库操作、API调用、文件处理)标准化;二是用Nx管理大型前端+AI混合项目的团队,你们需要让React组件、Next.js API路由、甚至Electron桌面端共享同一套技能定义;三是准备 TypeScript 面试的开发者——别再只背装饰器和泛型了,能讲清楚SkillDefinition<TInput, TOutput>接口如何约束运行时行为、SkillExecutor如何实现类型安全的插件加载,才是真正在工程层面吃透TS。我实测过,一个规范的agent-skills子项目,配合Nx的project graph分析,能让CI构建时间降低37%,因为技能包的变更不会触发无关Agent服务的重新编译。
2. 整体架构设计与核心选型逻辑
2.1 为什么必须是Nx monorepo?单Repo不是更轻量吗?
很多人第一反应是:“不就几个函数?建个独立npm包不就行了?”——这是典型的“单体思维陷阱”。当你的技能数量超过8个,且存在依赖关系(比如agent-skills-db依赖agent-skills-logger,而agent-skills-ai又依赖前两者),独立包管理会迅速失控。我见过最惨的案例:团队用Lerna管理12个技能包,每次发布都要手动确认依赖顺序,npm publish失败三次后,有人直接把package.json里的version字段写死成"1.0.0"来绕过校验。
Nx的杀手级能力在于project graph + target dependencies。当你定义libs/agent-skills-sql的project.json时,可以明确声明:
{ "targets": { "build": { "dependsOn": ["@myorg/agent-skills-core:build"] } } }这意味着Nx在CI中执行nx build agent-skills-sql时,会自动先构建agent-skills-core,且只在core的源码变更时才触发sql的重建。更关键的是,Nx的affected命令能精准识别:当你修改了libs/agent-skills-core/src/types.ts,哪些技能包、哪些Agent服务、哪些E2E测试用例会受影响——这在独立包体系里只能靠人工维护lerna.json的packages字段,出错率极高。
另一个常被忽视的点是开发体验一致性。在Nx monorepo里,所有技能包共享同一套TS配置(tsconfig.base.json)、同一套ESLint规则(.eslintrc.json)、同一套Jest测试配置(jest.preset.js)。我曾对比过:用Vite创建的独立技能包,其tsconfig.json里"moduleResolution": "node",而主项目用的是"bundler",导致类型导入时出现Cannot find module 'xxx'的诡异报错;而在Nx里,所有子项目继承tsconfig.base.json,moduleResolution统一为"bundler",问题自然消失。
2.2 TypeScript为何不可替代?用JavaScript不行吗?
这里有个残酷事实:90%的AI项目初期都用JS写,直到某天发现——无法安全地重构一个技能的输入参数。比如sendEmail技能最初只要to: string,后来业务方要求支持cc和bcc,JS里你只能靠文档和祈祷;而TS里,只需修改接口:
interface EmailInput { to: string; cc?: string[]; bcc?: string[]; // ... 其他字段 }然后全局搜索sendEmail(,所有调用处都会立刻报错,强制你补全参数。这不是IDE的提示,而是编译器的铁律。
更深层的价值在于泛型约束力。agent-skills的核心抽象是SkillDefinition<Input, Output>:
export interface SkillDefinition<Input, Output> { id: string; inputSchema: ZodSchema<Input>; execute: (input: Input, context: SkillContext) => Promise<Output>; metadata: SkillMetadata; }注意inputSchema的类型是ZodSchema<Input>,而非ZodSchema<any>。这意味着当你定义const emailSkill: SkillDefinition<EmailInput, EmailResult>时,Zod Schema的验证结果类型SafeParseReturnType<EmailInput, EmailInput>会自动与execute函数的input参数类型对齐。如果Schema里写了z.string().email(),而EmailInput.to却是number,TS会直接报错。这种“Schema即类型”的设计,在JS里根本无法实现。
还有个面试高频考点:declare global的妙用。我们在libs/agent-skills-core/src/global.d.ts里扩展Node.js全局类型:
declare global { namespace NodeJS { interface ProcessEnv { SKILL_TIMEOUT_MS?: string; SKILL_RETRY_MAX?: string; } } }这样所有技能包都能安全使用process.env.SKILL_TIMEOUT_MS,无需每次as any断言。这种类型安全的环境变量注入,在JS里只能靠运行时检查,而TS让它成为编译期保障。
2.3 semantic-release:为什么不用手动发版?它到底解决了什么痛点?
很多团队坚持手动npm version patch && npm publish,觉得“就几个包,很简单”。但当agent-skills发展到20+个子包时,手动发版就成了定时炸弹。我亲身经历过的事故:某次发布agent-skills-httpv2.1.0,忘记更新agent-skills-ai对它的peerDependency,导致下游服务安装时解析出两个不同版本的http技能,一个用fetch一个用axios,请求头被覆盖,线上订单丢失。
semantic-release的精妙之处在于将版本号生成逻辑从人脑转移到Git提交规范。我们约定:
feat:开头的commit触发minor版本(如feat(email): add DKIM support→ v1.2.0)fix:开头的commit触发patch版本(如fix(sql): handle null values in WHERE clause→ v1.3.1)BREAKING CHANGE:在commit body中触发major版本(如refactor(db): switch from Knex to Prisma)
Nx配合semantic-release后,CI流程变成:
nx affected --target=build构建所有变更的技能包nx affected --target=test运行相关单元测试npx semantic-release自动计算版本号、生成CHANGELOG、打Git tag、发布到npm
最关键的是,它强制推行基于主干的开发模式。每个PR合并到main分支,就意味着一次潜在发布。这倒逼团队写出原子化的commit——不能把“修复bug+新增功能+重构代码”混在一个commit里,否则semantic-release会错误地提升major版本。我们团队因此养成了“一个PR只做一件事”的习惯,代码审查质量直线上升。
3. 核心细节解析与实操要点
3.1 技能定义层:从接口到可执行实例的完整链条
一个技能不是函数,而是一个具备生命周期、可观测性、错误处理契约的实体。以agent-skills-sql为例,它的定义文件libs/agent-skills-sql/src/index.ts结构如下:
import { SkillDefinition, SkillContext } from '@myorg/agent-skills-core'; import { z } from 'zod'; // 1. 输入Schema:严格约束运行时数据 const SqlInputSchema = z.object({ query: z.string().min(1, 'SQL query cannot be empty'), params: z.array(z.any()).optional(), timeoutMs: z.number().int().min(100).max(30000).default(5000), }); // 2. 输出类型:明确返回结构 export type SqlOutput = { rows: Record<string, any>[]; rowCount: number; durationMs: number; }; // 3. 技能定义:绑定Schema与执行逻辑 export const sqlSkill: SkillDefinition<typeof SqlInputSchema._output, SqlOutput> = { id: 'sql', inputSchema: SqlInputSchema, metadata: { description: 'Execute parameterized SQL query against configured database', category: 'database', tags: ['sql', 'postgres', 'mysql'], }, execute: async (input, context) => { // 4. 上下文注入:自动携带traceId、userId等 const startTime = Date.now(); try { const result = await context.db.query(input.query, input.params || []); return { rows: result.rows, rowCount: result.rowCount, durationMs: Date.now() - startTime, }; } catch (error) { // 5. 标准化错误:统一错误码和消息格式 throw new SkillError('SQL_EXECUTION_FAILED', { originalError: error, query: input.query, durationMs: Date.now() - startTime, }); } }, };这里有几个极易被忽略的细节:
- Schema的
_output属性:Zod的z.object({...})类型是ZodObject<...>,但我们需要的是其解析后的JS对象类型。SqlInputSchema._output正是这个类型,它比infer更可靠,因为infer在复杂嵌套时可能推导失败。 context.db的来源:这不是硬编码的数据库连接,而是由Agent运行时通过DI容器注入的。我们在libs/agent-skills-core/src/context.ts里定义:
这样技能本身不关心数据库是PostgreSQL还是MySQL,只依赖抽象接口,极大提升可测试性。export interface SkillContext { db: DatabaseClient; // 接口,非具体实现 logger: Logger; traceId: string; userId: string; }SkillError的构造:它继承自Error,但增加了code和details字段。所有技能都抛出SkillError,上层Agent就能统一捕获并按code做降级处理(如SQL_TIMEOUT时返回缓存数据,SQL_AUTH_FAILED时触发权限重检)。
3.2 技能执行层:如何让技能在不同环境中安全运行?
技能定义只是蓝图,执行层才是落地关键。我们设计了三层执行机制:
第一层:本地开发执行器(LocalExecutor)
用于单元测试和本地调试,完全模拟生产环境:
// libs/agent-skills-core/src/executors/local-executor.ts export class LocalExecutor { constructor(private skillRegistry: SkillRegistry) {} async execute<Skill extends SkillDefinition<any, any>>( skillId: Skill['id'], input: Parameters<Skill['execute']>[0], context: SkillContext ): Promise<ReturnType<Skill['execute']>> { const skill = this.skillRegistry.get(skillId); if (!skill) throw new Error(`Skill ${skillId} not found`); // 1. 输入校验:用Zod Schema做运行时校验 const parseResult = skill.inputSchema.safeParse(input); if (!parseResult.success) { throw new SkillError('INPUT_VALIDATION_FAILED', { issues: parseResult.error.issues, }); } // 2. 超时控制:统一包装Promise const timeoutPromise = new Promise<ReturnType<Skill['execute']>>((_, reject) => { setTimeout(() => reject(new SkillError('SKILL_TIMEOUT')), context.timeoutMs || 5000); }); // 3. 执行并竞态 return Promise.race([ skill.execute(parseResult.data, context), timeoutPromise, ]); } }第二层:远程执行器(RemoteExecutor)
当技能需要隔离资源(如GPU密集型任务)或跨语言(Python写的模型推理)时启用:
// libs/agent-skills-core/src/executors/remote-executor.ts export class RemoteExecutor { private client: AxiosInstance; constructor(private baseUrl: string) { this.client = axios.create({ baseURL: baseUrl }); } async execute(skillId: string, input: any, context: SkillContext) { // 发送POST请求到独立技能服务 const response = await this.client.post(`/skills/${skillId}`, { input, context: { traceId: context.traceId, userId: context.userId }, }, { timeout: context.timeoutMs || 5000, }); if (response.data.error) { throw new SkillError(response.data.error.code, response.data.error.details); } return response.data.output; } }第三层:混合执行器(HybridExecutor)
根据技能元数据动态选择执行方式:
// libs/agent-skills-core/src/executors/hybrid-executor.ts export class HybridExecutor { constructor( private local: LocalExecutor, private remote: RemoteExecutor, private skillRegistry: SkillRegistry ) {} async execute(skillId: string, input: any, context: SkillContext) { const skill = this.skillRegistry.get(skillId); // 根据metadata决定执行方式 if (skill.metadata.category === 'ml') { return this.remote.execute(skillId, input, context); } return this.local.execute(skillId, input, context); } }提示:不要在技能定义里写
if (process.env.NODE_ENV === 'production')来切换执行方式。这违反了“技能应无环境感知”的原则。执行方式的选择必须由外部Executor决定,技能只负责“做什么”,不负责“怎么做”。
3.3 Nx项目配置:让技能包真正“活”起来
Nx的project.json不是配置文件,而是项目契约声明。libs/agent-skills-sql/project.json的关键配置:
{ "name": "agent-skills-sql", "targets": { "build": { "executor": "@nrwl/js:tsc", "options": { "tsConfig": "libs/agent-skills-sql/tsconfig.lib.json", "outputPath": "dist/libs/agent-skills-sql", "main": "libs/agent-skills-sql/src/index.ts", "assets": ["libs/agent-skills-sql/package.json"] } }, "test": { "executor": "@nrwl/jest:jest", "options": { "jestConfig": "libs/agent-skills-sql/jest.config.ts", "passWithNoTests": true } }, "lint": { "executor": "@nrwl/linter:eslint", "options": { "lintFilePatterns": ["libs/agent-skills-sql/**/*.{ts,js,tsx,jsx}"] } } }, "tags": ["type:skill", "scope:database"], "implicitDependencies": ["@myorg/agent-skills-core"] }这里有两个深度实践技巧:
implicitDependencies:声明agent-skills-sql隐式依赖agent-skills-core。这意味着当core的代码变更时,Nx会自动将sql加入affected列表,即使sql的package.json里没有显式列出core作为dependency。这是monorepo高效协作的基石。tags字段:["type:skill", "scope:database"]不仅是标签,更是Nx的查询语言。你可以运行:
这条命令会精准构建所有数据库类技能,而不碰nx run-many --target=build --projects=$(nx print-affected --select=projects --tags="type:skill,scope:database")agent-skills-email或agent-skills-http。在大型项目中,这种基于标签的批量操作比手动列项目名可靠十倍。
另外,tsconfig.lib.json必须继承tsconfig.base.json,且禁用"composite": true(因为技能包是最终产物,不是TS项目引用源):
{ "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "../../dist/out-tsc", "declaration": true, "declarationMap": true, "types": ["node"], "composite": false // 关键!避免TS项目引用冲突 } }4. 实操过程与核心环节实现
4.1 从零初始化:创建第一个技能包的完整步骤
假设你要创建agent-skills-http,以下是我在团队内部文档里写的“5分钟上手指南”,已验证过23次:
Step 1:生成新库
nx g @nrwl/js:library agent-skills-http \ --directory=libs/agent-skills-http \ --importPath=@myorg/agent-skills-http \ --publishable \ --skip-nx-json-update \ --no-interactive关键参数说明:
--publishable:生成package.json和dist输出,这是发布到npm的前提--skip-nx-json-update:避免Nx自动修改nx.json,我们稍后手动配置更清晰--no-interactive:跳过交互式提问,用CLI参数一次性搞定
Step 2:手动修正project.json删除自动生成的"root": "libs/agent-skills-http",改为:
{ "name": "agent-skills-http", "root": "libs/agent-skills-http", "sourceRoot": "libs/agent-skills-http/src", "projectType": "library", "targets": { /* 如前文所示 */ }, "tags": ["type:skill", "scope:http"], "implicitDependencies": ["@myorg/agent-skills-core"] }为什么删root?因为Nx 17+要求root必须是相对路径,而自动生成的绝对路径会导致构建失败。
Step 3:编写核心技能定义在libs/agent-skills-http/src/index.ts中:
import { SkillDefinition, SkillContext } from '@myorg/agent-skills-core'; import { z } from 'zod'; const HttpInputSchema = z.object({ url: z.string().url(), method: z.enum(['GET', 'POST', 'PUT', 'DELETE']).default('GET'), headers: z.record(z.string()).optional(), body: z.any().optional(), timeoutMs: z.number().int().min(100).max(60000).default(10000), }); export type HttpResponse = { status: number; data: any; headers: Record<string, string>; durationMs: number; }; export const httpSkill: SkillDefinition<typeof HttpInputSchema._output, HttpResponse> = { id: 'http', inputSchema: HttpInputSchema, metadata: { description: 'Make HTTP request with automatic retry and timeout', category: 'network', tags: ['http', 'api', 'rest'], }, execute: async (input, context) => { const startTime = Date.now(); try { const response = await fetch(input.url, { method: input.method, headers: input.headers || {}, body: input.body ? JSON.stringify(input.body) : undefined, signal: AbortSignal.timeout(input.timeoutMs), }); const data = await response.json(); return { status: response.status, data, headers: Object.fromEntries(response.headers.entries()), durationMs: Date.now() - startTime, }; } catch (error) { throw new SkillError('HTTP_REQUEST_FAILED', { originalError: error, url: input.url, durationMs: Date.now() - startTime, }); } }, };Step 4:添加类型导出在libs/agent-skills-http/src/index.ts末尾添加:
export * from './index';并在libs/agent-skills-http/src/public-api.ts中:
export * from './index';这是Nx publishable库的约定,确保import { httpSkill } from '@myorg/agent-skills-http'能正确工作。
Step 5:配置semantic-release在根目录创建.releaserc:
{ "plugins": [ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", "@semantic-release/npm", "@semantic-release/github" ], "branches": ["main"] }在package.json中添加scripts:
"scripts": { "release": "semantic-release" }最后,最关键的一步:在CI中配置GitHub Action:
# .github/workflows/release.yml name: Release on: push: branches: [main] jobs: release: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: token: ${{ secrets.GITHUB_TOKEN }} - uses: actions/setup-node@v3 with: node-version: '18' - run: npm ci - run: npx nx build agent-skills-http - name: Semantic Release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} run: npx semantic-release注意:
NPM_TOKEN必须是具有publish权限的令牌,且需在npm官网设置为Automation类型,否则会因2FA失败。
4.2 技能注册与发现:让Agent动态加载技能的实战方案
技能包发布后,Agent服务如何知道该用哪个版本?我们采用中心化技能注册表 + 本地缓存策略:
Step 1:构建注册表服务在apps/skill-registry中创建Express服务:
// apps/skill-registry/src/main.ts import express from 'express'; import { readFileSync } from 'fs'; const app = express(); app.use(express.json()); // GET /skills/{id}/latest 返回最新版本信息 app.get('/skills/:id/latest', (req, res) => { const skillId = req.params.id; // 从本地JSON文件读取(实际项目中可接数据库) const registry = JSON.parse(readFileSync('dist/registry.json', 'utf8')); const latest = registry[skillId]?.[0]; // 按version排序后的第一个 res.json(latest); }); app.listen(3001);Step 2:Agent启动时预加载技能
// apps/ai-agent/src/main.ts import { SkillRegistry } from '@myorg/agent-skills-core'; import { httpSkill } from '@myorg/agent-skills-http'; import { sqlSkill } from '@myorg/agent-skills-sql'; const registry = new SkillRegistry(); // 1. 注册本地技能(编译时已知) registry.register(httpSkill); registry.register(sqlSkill); // 2. 动态加载远程技能(运行时发现) async function loadRemoteSkills() { const skillIds = ['email', 'pdf-generation', 'ml-model']; for (const id of skillIds) { try { const response = await fetch(`http://localhost:3001/skills/${id}/latest`); const { version, url } = await response.json(); // 动态import远程技能包 const remoteSkill = await import(`${url}@${version}`); registry.register(remoteSkill.default); } catch (error) { console.warn(`Failed to load remote skill ${id}:`, error); } } } await loadRemoteSkills();Step 3:技能发现协议我们定义了一个极简的skill-manifest.json标准,每个技能包在dist/目录下必须包含:
{ "id": "http", "version": "1.4.2", "entryPoint": "index.js", "dependencies": ["@myorg/agent-skills-core@^2.0.0"], "capabilities": ["network", "timeout", "retry"] }Agent通过读取此文件,就能安全地解析依赖和能力,避免盲目加载不兼容版本。
4.3 测试驱动开发:为技能编写真正有效的测试
技能测试不是走形式,而是验证契约的完整性。以httpSkill为例,我们的测试覆盖四个维度:
维度1:输入校验测试
// libs/agent-skills-http/src/http.spec.ts describe('httpSkill input validation', () => { it('should reject invalid URL', () => { const result = httpSkill.inputSchema.safeParse({ url: 'not-a-url' }); expect(result.success).toBe(false); expect(result.error?.issues[0].code).toBe('invalid_string'); }); it('should accept valid URL with default method', () => { const result = httpSkill.inputSchema.safeParse({ url: 'https://api.example.com' }); expect(result.success).toBe(true); expect(result.data.method).toBe('GET'); // 验证default生效 }); });维度2:执行逻辑测试(Mock网络)
describe('httpSkill execution', () => { beforeEach(() => { // Mock fetch全局函数 global.fetch = jest.fn() as jest.Mock; }); it('should return status and data on success', async () => { (global.fetch as jest.Mock).mockResolvedValue({ status: 200, json: jest.fn().mockResolvedValue({ message: 'ok' }), headers: new Headers({ 'content-type': 'application/json' }), }); const context = { db: {} as any, logger: console, traceId: 'test-trace', userId: 'test-user', timeoutMs: 5000 }; const result = await httpSkill.execute( { url: 'https://api.example.com' }, context ); expect(result.status).toBe(200); expect(result.data).toEqual({ message: 'ok' }); expect(result.durationMs).toBeGreaterThan(0); }); });维度3:错误处理测试
it('should throw SkillError on network failure', async () => { (global.fetch as jest.Mock).mockRejectedValue(new Error('Network error')); await expect( httpSkill.execute({ url: 'https://api.example.com' }, {} as any) ).rejects.toThrow('HTTP_REQUEST_FAILED'); });维度4:集成测试(真实HTTP)
describe('httpSkill integration test', () => { // 使用msw拦截真实fetch beforeAll(() => { server.listen(); }); afterEach(() => { server.resetHandlers(); }); afterAll(() => { server.close(); }); it('should handle real HTTP response', async () => { rest.get('https://api.example.com', (req, res, ctx) => { return res(ctx.status(201), ctx.json({ id: 123 })); }); const result = await httpSkill.execute( { url: 'https://api.example.com' }, {} as any ); expect(result.status).toBe(201); expect(result.data).toEqual({ id: 123 }); }); });实操心得:我们禁止在技能测试中使用
jest.mock()去mock其他技能。因为技能之间应该通过SkillContext解耦,而不是直接调用。如果测试需要依赖另一个技能,应该在context里注入它的mock实现,这才能真实反映运行时行为。
5. 常见问题与排查技巧实录
5.1 类型错误:Zod Schema与TS类型不匹配的典型场景
问题现象:Type 'string' is not assignable to type 'never'.出现在skill.inputSchema.parse(input)调用处。
根本原因:
Zod Schema的parse方法返回类型是infer推导的,但当Schema包含z.optional(z.string())时,TS可能推导为string | undefined,而技能定义中的Input类型是{ field?: string },二者不完全等价。
解决方案:
强制使用_output类型,并在技能定义中显式标注:
// 错误写法 const InputSchema = z.object({ name: z.string().optional() }); type Input = z.infer<typeof InputSchema>; // 可能推导不准 // 正确写法 const InputSchema = z.object({ name: z.string().optional() }); export type Input = InputSchema['_output']; // 精确获取解析后类型 export const mySkill: SkillDefinition<Input, Output> = { /* ... */ };进阶技巧:
为避免重复写['_output'],创建类型别名:
type Infer<T extends ZodSchema> = T['_output']; // 然后 use: type Input = Infer<typeof InputSchema>;5.2 Nx构建失败:Cannot find module 'xxx'的排查路径
典型错误:libs/agent-skills-sql/src/index.ts:3:25 - error TS2307: Cannot find module '@myorg/agent-skills-core'
排查步骤:
- 检查路径映射:确认
tsconfig.base.json中有:"compilerOptions": { "baseUrl": ".", "paths": { "@myorg/agent-skills-core": ["libs/agent-skills-core/src/index.ts"] } } - 验证文件存在:
ls libs/agent-skills-core/src/index.ts是否存在且导出正确 - 检查project.json:
libs/agent-skills-sql/project.json中"implicitDependencies"是否包含"@myorg/agent-skills-core" - 清除缓存:
nx reset清除Nx缓存,有时缓存会导致路径解析失效 - 终极方案:在
libs/agent-skills-sql/tsconfig.lib.json中添加:"include": ["src/**/*.ts"], "exclude": ["node_modules", "dist"]
注意:不要在
tsconfig.lib.json里写"types": ["@myorg/agent-skills-core"],这会导致TS去node_modules里找,而monorepo中它应该在源码路径。
5.3 semantic-release发布失败:Cannot push to branch的根因分析
错误日志:ERROR Failed to publish package: Error: Command failed: git push --follow-tags origin main
常见原因与对策:
| 原因 | 检查命令 | 解决方案 |
|---|---|---|
| GitHub Token权限不足 | curl -H "Authorization: token $GITHUB_TOKEN" https://api.github.com/user | 在GitHub Settings → Developer settings → Personal access tokens → Generate new token,勾选public_repo和workflow |
| Git远程URL错误 | git remote get-url origin | 确保是https://github.com/owner/repo.git,不是git@github.com:owner/repo.git(SSH URL不支持token认证) |
| 分支保护规则阻止推送 | gh api repos/{owner}/{repo}/branches/main/protection | 在GitHub Settings → Branches → Branch protection rules,临时禁用Require pull request reviews before merging |
| 本地Git配置缺失 | git config user.email和git config user.name | 在CI中添加:git config --global user.email "ci@domain.com"和git config --global user.name "CI Bot" |
防坑技巧:
在.releaserc中添加verifyConditions插件,提前校验:
{ "plugins": [ ["@semantic-release/condition-travis", { "branch": "main" }], "@semantic-release/commit-analyzer", // ... ] }5.4 技能执行超时:如何精准定位是网络还是代码问题?
现象:SkillError: SKILL_TIMEOUT频繁出现,但不确定是网络延迟还是技能内部逻辑卡死。
诊断流程:
- 开启详细日志:在
SkillContext中增加debug: boolean字段,技能执行时记录关键时间点:if (context.debug) { console.time(`[SKILL:${skill.id}] total`); console.time(`[SKILL:${skill.id}] validation`); } const parseResult = skill.inputSchema.safeParse(input); if (context.debug) console.timeEnd(`[SKILL:${skill.id}] validation`); // ... 执行逻辑 if (context.debug) console.timeEnd(`[SKILL:${skill.id}] total`); - 对比本地与远程执行:在本地用
LocalExecutor运行相同输入,如果本地也超时,说明是技能代码问题;如果本地快、远程慢,说明是网络或远程服务问题。 - 抓包分析:对
RemoteExecutor,用axios.interceptors.request.use和response.use记录请求发出时间和响应到达时间:axios.interceptors.request.use(config => { config.metadata = { startTime: Date.now() }; return config; }); axios.interceptors.response.use(response => { response.config.metadata.endTime = Date.now(); console.log(`HTTP ${response.config.url} took ${response.config.metadata.endTime - response.config.metadata.startTime}ms`); return response; });
实测案例:
我们曾发现agent-skills-pdf超时,抓包显示请求发出后3秒才收到响应,但技能日志显示`