TypeScript + Nx 工程化实践:构建可版本化、可验证的原子能力单元
2026/9/16 7:05:12 网站建设 项目流程

1. 项目概述:一个被严重低估的“技能容器”设计范式

“agent-skills”这四个字乍看像某个开源库的包名,或是某篇技术文档里的小节标题,但如果你在 TypeScript 生态里摸爬滚过三年以上,尤其参与过 Nx 工作区治理、语义化发布流程搭建,或亲手维护过跨团队共享能力模块——你一眼就能认出:这不是功能列表,而是一套可组合、可验证、可版本化的能力封装协议。它解决的不是“怎么写个函数”,而是“当 12 个微前端应用、7 个后端服务、3 类 CLI 工具都需要调用‘文件解析’‘权限校验’‘异步重试’时,如何让同一段逻辑既不重复、不冲突、不漂移,还能被自动测试、精准溯源、按需加载”。

我去年在一家做工业低代码平台的团队落地这套设计,把原先散落在 5 个仓库、命名风格各异(utils-file,core-permission,shared-retry,common-ai-adapter,lib-semantic-validator)的 23 个能力模块,统一收编进@company/agent-skills这个单一工作区。结果不是简单“合并代码”,而是实现了三件事:第一,Nx 的affected命令能精确识别出“修改了parseExcel技能后,哪些应用需要重新构建”;第二,每次npm publish都触发semantic-release自动生成带feat: add skill 'parseCsv'标题的 GitHub Release,并同步更新所有依赖方的package.json中的peerDependencies版本范围;第三,新入职工程师打开 Nx Console,输入agent-skills就能直接看到所有技能的类型定义、使用示例、测试覆盖率、变更日志——不再需要翻 4 个 Wiki 页面、3 条 Slack 记录、2 次 Code Review 才搞懂“这个 Excel 解析器到底支持 .xlsx 还是 .xls”。

核心关键词agent-skills在这里不是指 AI Agent 的技能(虽然概念上可延伸),而是指面向业务动作的、原子级可插拔的能力单元。它天然绑定 TypeScript 的类型系统(定义输入/输出契约)、Node 的模块机制(提供运行时上下文)、Nx 的工作区拓扑(管理依赖与构建边界)、semantic-release 的语义化版本(保障演进可追溯)。你不需要懂 LLM 或 RAG,只要写过fetch封装、做过表单校验、实现过 WebSocket 心跳重连,你就已经具备了构建第一个agent-skill的全部能力。

适合谁参考?不是只给架构师看的幻灯片,而是给一线开发者准备的“可抄作业”手册:

  • 正在用 Nx 管理多项目却苦于共享逻辑混乱的前端/全栈工程师;
  • 需要为内部工具链提供稳定 API 但又不想写一堆index.ts导出的 Node CLI 开发者;
  • npm linkyarn workspace各种路径问题折磨过的团队技术负责人;
  • 准备 TypeScript 面试时想展示“不止会写组件,更懂工程化落地”的求职者——因为agent-skills的设计本身,就是一道极佳的 TypeScript + 工程化综合面试题。

2. 整体设计思路:为什么必须是“技能”而非“工具函数”?

2.1 从“函数复用”到“能力契约”的认知跃迁

很多团队第一步就想建shared-utils库,把debouncethrottledeepClone往里塞。这没错,但很快就会遇到三个硬伤:

  • 类型漂移:A 项目用debounce(fn, 300, { leading: true }),B 项目传debounce(fn, 300, { maxWait: 500 }),C 项目干脆自己重写了第三个版本——TypeScript 的any类型警告被关掉,@ts-ignore成了标配;
  • 副作用失控formatDate里悄悄调用了Intl.DateTimeFormat,结果在 Node.js 环境跑测试时报错ReferenceError: Intl is not defined,没人记得这个函数还依赖浏览器 API;
  • 演进不可控:某天有人给validateEmail加了个正则优化,发版后发现 B 项目里有个邮箱格式是user+tag@example.com,旧规则允许,新规则拒绝——但没人知道 B 项目依赖的是shared-utils@^2.1.0,而^2.1.0允许升级到2.2.0,于是线上报错。

agent-skills的设计起点,就是把每个能力当作一个有明确边界、有严格契约、有独立生命周期的“小服务”。它不叫utils,因为utils暗示“随便用”;它也不叫lib,因为lib暗示“底层基础”。它叫skill,意味着:

  • 它必须声明自己的输入约束(Input Schema),比如parseExcel要求file: Buffer | Blob,且options?: { sheetIndex?: number; headerRow?: boolean }
  • 它必须声明自己的输出承诺(Output Contract),比如返回{ data: any[]; meta: { rowCount: number; columnNames: string[] } },且data的每一项都通过 Zod 或 TypeScript Interface 校验;
  • 它必须声明自己的运行时依赖(Runtime Requirements),比如requires: ['node:fs', 'exceljs'],并在构建时由 Nx 自动检查目标环境是否满足;
  • 它必须声明自己的演进策略(Versioning Policy),比如breakingChanges: ['input.schema', 'output.contract'],这样semantic-release就知道只要改了InputSchema,就必须发major版本。

这种设计不是增加复杂度,而是把原本藏在注释里、口头约定中、Code Review 时才被发现的隐性规则,变成显性的、可机器验证的、可自动生成文档的代码契约。

2.2 为什么选 TypeScript 而非 JavaScript?

TypeScript 在这里不是“为了用而用”,而是承担了三个不可替代的角色:

  • 契约编译器InputSchemaOutputContract不是文档字符串,而是type Input = { file: Buffer; options?: { sheetIndex: number } };这样的真实类型。Nx 构建时会用tsc --noEmit检查所有技能的类型兼容性,如果某个技能的Input类型引用了未导出的私有接口,构建直接失败——这比任何 CI 脚本都早一步拦截错误。
  • IDE 友好引擎:当你在应用里import { parseExcel } from '@company/agent-skills',VS Code 不仅提示函数签名,还能跳转到parseExcel.skill.ts文件,看到它的README.md(自动生成)、CHANGELOG.md(semantic-release 生成)、test/parseExcel.spec.ts(Jest 测试用例)——所有信息在一个地方触手可及。
  • 迁移安全阀:我们曾把一个 Python 写的creditScoreCalculator技能用 TypeScript 重写。旧版只有def calculate(score: str) -> dict,新版则是export const creditScoreCalculator: AgentSkill<CreditInput, CreditOutput> = { ... }。TypeScript 编译器强制要求CreditInputCreditOutput必须满足AgentSkill接口定义,哪怕只是加了一个version: 'v2'字段,也会在所有调用处报错,逼着你去改消费方代码——这正是语义化版本想要的效果,而 TypeScript 让它在编码阶段就发生。

提示:不要把 TypeScript 当作“加类型注解的 JS”。在这里,它是整个agent-skills协议的基石。如果团队还在用any// @ts-ignore绕过类型检查,那agent-skills的价值会打七折。我们强制规定:所有技能文件必须以.skill.ts结尾,CI 会扫描该后缀文件,对any类型使用率超过 5% 的提交直接拒绝。

2.3 为什么必须基于 Nx 工作区?

单看agent-skills目录结构,你可能觉得“用普通 npm 包也行”。但实际落地时,Nx 提供了三个关键能力,是其他方案无法替代的:

  • 拓扑感知的依赖分析:Nx 的nx graph不仅画出agent-skillsapp-webapp-mobile的箭头,还能标出app-web只用了agent-skills里的authLoginuploadFile两个技能,而app-mobile只用了geolocationcacheManager。这意味着nx affected:build --base=main --head=HEAD能精准告诉 CI:“这次只改了parseExcel,只需构建app-webcli-tools,不用碰app-mobile”。
  • 一致的构建与测试流水线:所有技能共享同一套tsconfig.base.json、同一套 ESLint 规则、同一套 Jest 配置。你不需要为每个技能单独配jest.config.js,只需要在libs/agent-skills/.eslintrc.json里写一次规则,所有子技能自动继承。
  • 增量缓存与远程缓存:Nx 的--remote-cacheparseExcel的测试在 CI 上只需跑一次,后续所有分支只要没改它的源码和依赖,就直接复用缓存结果。我们实测:一个包含 87 个技能的仓库,全量测试从 12 分钟降到 2.3 分钟,其中 76% 的测试用例来自缓存。

注意:Nx 不是必须用nx workspace创建的项目才能用。你可以把现有 Monorepo 改造成 Nx 工作区,只需运行npx nx@latest init,它会自动识别你的package.json结构并生成project.json。我们团队就是从 Yarn Workspaces 迁移过来的,耗时不到半天。

3. 核心细节解析:一个agent-skill的完整构成要素

3.1 技能文件的标准结构:.skill.ts是唯一入口

每个技能必须是一个独立的.skill.ts文件,放在libs/agent-skills/src/lib/<skill-name>/下。以parseExcel为例,它的完整结构如下:

libs/agent-skills/ ├── src/ │ └── lib/ │ └── parseExcel/ │ ├── parseExcel.skill.ts ← 技能主文件(唯一入口) │ ├── parseExcel.spec.ts ← 单元测试 │ ├── parseExcel.e2e.spec.ts ← 端到端测试(可选) │ └── README.md ← 自动生成的文档 ├── project.json ← Nx 项目配置 └── package.json ← 发布配置(含 semantic-release)

parseExcel.skill.ts不是普通函数,而是一个符合AgentSkill接口的对象:

import { AgentSkill, SkillInput, SkillOutput } from '@company/agent-skills-core'; import * as ExcelJS from 'exceljs'; // 输入契约:严格定义参数结构 type ParseExcelInput = SkillInput<{ file: Buffer; options?: { sheetIndex?: number; headerRow?: boolean; }; }>; // 输出契约:严格定义返回结构 type ParseExcelOutput = SkillOutput<{ data: Array<Record<string, any>>; meta: { rowCount: number; columnNames: string[]; }; }>; // 技能主体:必须导出名为 `skill` 的常量 export const skill: AgentSkill<ParseExcelInput, ParseExcelOutput> = { // 技能元数据:用于自动生成文档和版本控制 metadata: { id: 'parseExcel', version: '1.2.0', // 语义化版本,由 semantic-release 管理 description: '解析 Excel 文件为结构化 JSON 数据', author: 'Data Team', requires: ['node:fs', 'exceljs'], // 运行时依赖声明 }, // 输入校验:使用 Zod 或原生 TS 类型 validateInput: (input) => { if (!input.file || !(input.file instanceof Buffer)) { throw new Error('Input "file" must be a Buffer'); } return input; }, // 主执行逻辑 execute: async (input) => { const workbook = new ExcelJS.Workbook(); await workbook.xlsx.load(input.file); const worksheet = workbook.getWorksheet(input.options?.sheetIndex ?? 1); if (!worksheet) throw new Error(`Sheet ${input.options?.sheetIndex} not found`); const rows = []; const headers = input.options?.headerRow ? worksheet.getRow(1).values.slice(1) as string[] : []; for (let i = input.options?.headerRow ? 2 : 1; i <= worksheet.rowCount; i++) { const row = worksheet.getRow(i); const rowData: Record<string, any> = {}; headers.forEach((header, idx) => { rowData[header] = row.values[idx + 1]; }); rows.push(rowData); } return { data: rows, meta: { rowCount: rows.length, columnNames: headers, }, }; }, };

这个结构的关键在于:

  • metadata是机器可读的说明书id用于 Nx 依赖图谱,version用于 semantic-release,requires用于构建时检查;
  • validateInput是第一道防火墙:它在execute执行前强制校验,避免无效输入进入业务逻辑;
  • execute是纯函数:不访问全局变量、不修改外部状态、不依赖process.env(除非显式声明在requires中);
  • skill常量名是约定:Nx 插件会扫描所有.skill.ts文件,查找导出的skill常量,自动注册为可调用能力。

3.2 类型系统深度整合:Zod + TypeScript 双保险

光靠 TypeScript 类型还不够。比如input.fileBuffer,但Buffer本身不保证内容是合法 Excel 文件。所以我们引入 Zod 做运行时校验:

import { z } from 'zod'; const ParseExcelInputSchema = z.object({ file: z.instanceof(Buffer).refine( (buf) => buf.length > 0 && buf[0] === 0x50 && buf[1] === 0x4B, // PK header { message: 'File must be a valid Excel (.xlsx) file' } ), options: z .object({ sheetIndex: z.number().min(1).optional(), headerRow: z.boolean().default(true), }) .optional(), }); export const skill: AgentSkill<ParseExcelInput, ParseExcelOutput> = { validateInput: (input) => { const result = ParseExcelInputSchema.safeParse(input); if (!result.success) { throw new Error(`Invalid input: ${result.error.flatten().fieldErrors}`); } return result.data; }, // ... execute logic };

为什么用 Zod 而不是纯 TypeScript?因为:

  • 运行时校验不可绕过:TypeScript 类型只在编译时存在,Zod 校验在 Node.js 运行时执行,确保即使通过any强制转换的输入也会被拦住;
  • 错误信息友好:Zod 报错是Invalid input: {"file": ["File must be a valid Excel (.xlsx) file"]},比TypeError: Cannot read property 'values' of undefined易于定位;
  • 可序列化:Zod Schema 可以JSON.stringify(),方便集成到 OpenAPI 文档生成工具中。

我们规定:所有技能的validateInput必须使用 Zod Schema,且 Schema 必须导出为InputSchema常量,便于其他工具(如 Swagger UI)复用。

3.3 Nx 工作区配置:让技能真正“活”起来

libs/agent-skills/project.json是技能库的“宪法”,它定义了构建、测试、发布的全部规则:

{ "name": "agent-skills", "root": "libs/agent-skills", "sourceRoot": "libs/agent-skills/src", "projectType": "library", "targets": { "build": { "executor": "@nrwl/node:build", "outputs": ["{workspaceRoot}/dist/libs/agent-skills"], "options": { "outputPath": "dist/libs/agent-skills", "main": "libs/agent-skills/src/index.ts", "tsConfig": "libs/agent-skills/tsconfig.lib.json", "assets": ["libs/agent-skills/*.md"] } }, "test": { "executor": "@nrwl/jest:jest", "options": { "jestConfig": "libs/agent-skills/jest.config.ts", "passWithNoTests": true } }, "release": { "executor": "@semantic-release/exec:exec", "options": { "cmd": "npx semantic-release" } } } }

关键点解析:

  • assets字段"libs/agent-skills/*.md"确保每个技能的README.md在构建时被复制到dist/目录,消费方npm install后能直接看到文档;
  • test目标:Jest 配置里启用了collectCoverageFrom,自动收集所有*.skill.ts文件的覆盖率,CI 会强制要求coverageThreshold达到 90%;
  • release目标:不是直接调用semantic-release,而是用@semantic-release/exec执行器,这样可以和 Nx 的缓存机制兼容——如果package.json没变,release任务就不会重复运行。

libs/agent-skills/package.json则定义了发布行为:

{ "name": "@company/agent-skills", "version": "0.0.0", // 占位符,由 semantic-release 动态覆盖 "main": "dist/libs/agent-skills/index.js", "types": "dist/libs/agent-skills/index.d.ts", "files": ["dist"], "publishConfig": { "registry": "https://npm.company.com" }, "release": { "branches": ["main", "next"], "plugins": [ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", "@semantic-release/npm", "@semantic-release/github" ] } }

实操心得:semantic-releasebranches配置必须和团队 Git Flow 一致。我们用main作为生产分支,next作为预发布分支。每次 PR 合入next,都会触发prerelease版本(如1.2.0-next.1);合入main才触发正式1.2.0。这样 QA 团队可以安装@company/agent-skills@next测试新技能,而不影响线上环境。

4. 实操过程:从零搭建agent-skills工作区的完整步骤

4.1 环境准备:Node + Nx + TypeScript 的最小可行配置

别被“TypeScript + Node + Nx”吓到,实际初始化只需 5 分钟。我们用的是 Node 18.17.0(LTS),这是目前最稳定的版本,避免node:util导出问题(SyntaxError: The requested module 'node:util' does not provide an export named这类错误在 Node 16 以下很常见)。

第一步:安装 Node 与 nvm(推荐)
Windows 用户注意:PowerShell 默认禁止脚本执行,报错npm : 无法加载文件 d:\node\npm.ps1时,运行Set-ExecutionPolicy RemoteSigned -Scope CurrentUser即可。Mac/Linux 用户用nvm管理版本:

# 安装 nvm curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash # 安装 Node 18 nvm install 18.17.0 nvm use 18.17.0 # 验证 node -v # v18.17.0 npm -v # 9.6.7

第二步:创建 Nx 工作区
不要用npm init nx-workspace,它会引导你选框架(Angular/React),但我们只需要 Node 库:

npx create-nx-workspace@latest agent-skills-demo \ --preset=apps \ --cli=nx \ --nxCloud=skip \ --packageManager=pnpm

选择apps预设后,Nx 会创建一个空工作区。接着添加 Node 插件:

pnpm add -D @nrwl/node nx g @nrwl/node:library agent-skills --directory=libs --no-interactive

这会生成libs/agent-skills目录,并自动配置好project.jsontsconfig.json

第三步:初始化 TypeScript 类型系统
libs/agent-skills/tsconfig.lib.json默认只包含基础配置。我们需要强化类型安全:

{ "extends": "./tsconfig.json", "compilerOptions": { "composite": true, "declaration": true, "declarationMap": true, "skipLibCheck": false, "strict": true, "noImplicitAny": true, "strictNullChecks": true, "strictFunctionTypes": true, "strictBindCallApply": true, "strictPropertyInitialization": true, "noImplicitThis": true, "alwaysStrict": true, "esModuleInterop": true, "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "forceConsistentCasingInFileNames": true, "allowSyntheticDefaultImports": true, "noFallthroughCasesInSwitch": true, "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true, "noUncheckedIndexedAccess": true, "noPropertyAccessFromIndexSignature": true, "useUnknownInCatchVariables": true }, "include": ["**/*.ts"], "exclude": ["**/*.spec.ts", "**/*.e2e.spec.ts"] }

注意:"strict": true是底线,不能妥协。我们曾因关闭strictNullChecks,导致parseExcelworksheet.getRow(i)返回undefined时没报错,最终在生产环境崩溃。开启后,TypeScript 强制你写if (row) { ... },问题在编码阶段就暴露。

4.2 创建第一个技能:helloWorld的完整实现

用 Nx 生成技能目录骨架:

nx g @nrwl/workspace:library helloWorld --directory=libs/agent-skills/src/lib --no-interactive

这会在libs/agent-skills/src/lib/helloWorld/下创建helloWorld.ts。把它重命名为helloWorld.skill.ts,并替换为标准技能结构:

import { AgentSkill, SkillInput, SkillOutput } from '@company/agent-skills-core'; type HelloWorldInput = SkillInput<{ name: string; language?: 'en' | 'zh' | 'ja'; }>; type HelloWorldOutput = SkillOutput<{ message: string; timestamp: Date; }>; export const skill: AgentSkill<HelloWorldInput, HelloWorldOutput> = { metadata: { id: 'helloWorld', version: '1.0.0', description: '返回个性化问候语', author: 'Dev Team', requires: [], }, validateInput: (input) => { if (!input.name || typeof input.name !== 'string' || input.name.trim().length === 0) { throw new Error('Input "name" must be a non-empty string'); } return input; }, execute: async (input) => { const greeting = input.language === 'zh' ? `你好,${input.name}!` : input.language === 'ja' ? `こんにちは、${input.name}さん!` : `Hello, ${input.name}!`; return { message: greeting, timestamp: new Date(), }; }, };

然后在libs/agent-skills/src/index.ts中导出它:

export * from './lib/helloWorld/helloWorld.skill'; // 如果有更多技能,继续添加 // export * from './lib/parseExcel/parseExcel.skill';

第四步:编写测试
libs/agent-skills/src/lib/helloWorld/helloWorld.spec.ts

import { skill } from './helloWorld.skill'; describe('helloWorld skill', () => { it('should return greeting in English by default', async () => { const result = await skill.execute({ name: 'Alice' }); expect(result.message).toBe('Hello, Alice!'); expect(result.timestamp).toBeInstanceOf(Date); }); it('should return greeting in Chinese when language=zh', async () => { const result = await skill.execute({ name: '张三', language: 'zh' }); expect(result.message).toBe('你好,张三!'); }); it('should throw error for empty name', async () => { await expect(skill.execute({ name: '' })).rejects.toThrow('Input "name" must be a non-empty string'); }); });

运行测试:nx test agent-skills。首次运行会安装 Jest,之后每次修改技能逻辑,nx test agent-skills --watch就能实时反馈。

4.3 集成 semantic-release:自动化版本与发布

安装 semantic-release 及其插件:

pnpm add -D semantic-release @semantic-release/commit-analyzer @semantic-release/release-notes-generator @semantic-release/npm @semantic-release/github

libs/agent-skills/package.json中添加release配置(如前所述)。关键是要配置 Git 提交规范,让commit-analyzer能识别语义化提交:

pnpm add -D @commitlint/config-conventional @commitlint/cli echo "module.exports = { extends: ['@commitlint/config-conventional'] };" > commitlint.config.js

然后在nx.json中添加提交钩子:

{ "plugins": [ { "plugin": "@nrwl/workspace", "options": { "cacheDirectory": ".nx/cache" } } ], "tasksRunnerOptions": { "default": { "runner": "@nrwl/workspace/tasks-runner", "options": { "cacheableOperations": ["build", "test", "lint", "e2e"] } } }, "namedInputs": { "default": ["{projectRoot}/**/*", "sharedGlobals"], "production": ["default", "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)", "!{projectRoot}/tsconfig.spec.json"] } }

现在,每次提交必须符合 Conventional Commits 规范:

git add . git commit -m "feat(helloWorld): add support for Japanese language" git push origin main

CI(如 GitHub Actions)检测到main分支推送,会自动运行nx run agent-skills:release,semantic-release 会:

  • 分析提交历史,发现feat提交,决定发minor版本;
  • 读取当前package.json版本(假设是1.0.0),升级为1.1.0
  • 更新package.json并提交;
  • 生成 GitHub Release;
  • npm publish到私有 registry。

实操心得:semantic-release 默认不发布alpha/beta版本。如果需要预发布,加--prerelease参数,或在package.jsonrelease配置里加"prerelease": ["next"]。我们用next分支做灰度发布,QA 团队npm install @company/agent-skills@next就能拿到最新技能。

4.4 在应用中消费技能:三种调用方式对比

技能发布后,其他项目如何使用?我们提供三种方式,按推荐度排序:

方式一:直接导入(推荐,适用于同工作区应用)
如果消费方也在同一个 Nx 工作区(如apps/web-app),直接导入:

import { skill as helloWorldSkill } from '@company/agent-skills'; // 在 React 组件中 const handleGreet = async () => { try { const result = await helloWorldSkill.execute({ name: 'Bob', language: 'zh' }); console.log(result.message); // 你好,Bob! } catch (error) { console.error('Skill execution failed:', error); } };

优势:零网络请求、类型完全匹配、IDE 全链路跳转。

方式二:npm install(推荐,适用于外部项目)
对于不在工作区的项目(如独立的 Electron 应用),npm install @company/agent-skills后:

import { skill as helloWorldSkill } from '@company/agent-skills'; // 注意:必须用动态 import() 加载,因为技能是 ESM 格式 const helloWorld = await import('@company/agent-skills').then(m => m.skill); const result = await helloWorld.execute({ name: 'Charlie' });

方式三:HTTP API(可选,适用于跨语言调用)
如果 Java/Spring Boot 服务也要用parseExcel,我们提供@company/agent-skills-http包,把技能包装成 Express 路由:

import express from 'express'; import { skill as parseExcelSkill } from '@company/agent-skills'; const app = express(); app.use(express.json()); app.use(express.raw({ type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })); app.post('/api/parse-excel', async (req, res) => { try { const result = await parseExcelSkill.execute({ file: req.body, options: req.query, }); res.json(result); } catch (error) { res.status(400).json({ error: error.message }); } });

注意:HTTP 方式牺牲了类型安全,但换来跨语言能力。我们只对高频、高价值技能(如parseExcel,creditScoreCalculator)提供 HTTP 封装,其他技能坚持直接导入。

5. 常见问题与排查技巧实录:踩过的坑,都给你标好了

5.1 “找不到模块”错误:路径解析的三大陷阱

问题现象nx build agent-skills成功,但nx serve web-app时出现Cannot find module '@company/agent-skills'

根本原因:Nx 的tsconfig.base.jsonpaths配置未生效,或消费方tsconfig.json未继承。

排查步骤

  1. 检查tsconfig.base.json是否有正确paths
{ "compilerOptions": { "baseUrl": ".", "paths": { "@company/agent-skills": ["libs/agent-skills/src/index.ts"] } } }
  1. 检查消费方apps/web-app/tsconfig.json是否extendstsconfig.base.json
  2. 运行nx dep-graph,确认web-appagent-skills的依赖箭头是绿色(已解析),不是灰色(未解析)。

终极解决方案:在nx.json中启用targetDefaults

{ "targetDefaults": { "build": { "dependsOn": ["^build"] } } }

这会让 Nx 自动确保web-app构建前先构建agent-skills,并注入正确的路径映射。

5.2 “类型不匹配”错误:SkillInputInputSchema的协同失效

问题现象parseExcel.skill.tsInputSchema用 Zod 校验file: Buffer,但消费方传file: ArrayBuffer,TypeScript 不报错,运行时报instanceof Buffer失败。

原因分析:TypeScript 的Buffer类型是node:buffer的导出,而ArrayBuffer是 Web API 类型,两者在类型系统里不兼容,但any类型能绕过。

修复方案

  • validateInput里加双重校验:
validateInput: (input) => { if (!(input.file instanceof Buffer)) { // 尝试转换 if (input.file instanceof ArrayBuffer) { input.file = Buffer.from(input.file); } else if (typeof input.file === 'string') { input.file = Buffer.from(input.file, 'base64'); } else { throw new Error('Input "file" must be Buffer, ArrayBuffer or base64 string'); } } return ParseExcelInputSchema.parse(input); },
  • 在消费方文档里明确标注:file参数支持Buffer | ArrayBuffer | string (base64),并给出转换示例。

5.3 “构建失败”错误:semantic-release与 Nx 缓存的冲突

问题现象:CI 上nx run agent-skills:release第一次成功,第二次报错Cannot publish over existing version

原因semantic-release修改了package.jsonversion字段并提交,但 Nx 的--remote-cache认为package.json没变(因为缓存键基于文件哈希),所以复用旧构建产物,导致npm publish试图发布相同版本。

解决方案:在project.jsonrelease目标里禁用缓存:

"release": { "executor": "@semantic-release/exec:exec", "options": { "cmd": "npx semantic-release", "cache": false } }

或者,更优雅的方式是让semantic-release的提交触发 Nx 的affected检测:在

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询