尧图精选

TypeScript技能单元设计:可复用、可版本化的能力抽象

🕒 发布时间:2026/9/16 9:21:29 📁 来源:尧图网络
1. 项目概述一个被严重低估的“技能容器”设计范式“agent-skills”这个标题乍看像某个开源库的包名但如果你在 TypeScript 生态里混迹三年以上看到它第一反应不是 npm install而是下意识摸出键盘——因为这四个字背后藏着一套正在 quietly revolutionize安静地重塑前端工程化边界的实践逻辑。它不是框架不是 CLI 工具更不是又一个“AI Agent SDK”而是一个可组合、可版本化、可语义发布、可跨项目复用的技能单元抽象层。我第一次在 Nx monorepo 的 plugins 目录里见到它时以为是某位同事随手起的测试模块名直到我把它从一个内部项目抽离出来单独 publish 到私有 registry再被三个不同业务线的团队同时依赖、各自 patch、共同迭代——我才意识到我们无意中踩中了一个比“微前端”更底层、比“插件系统”更轻量、比“函数即服务”更贴近开发者心智模型的基建原点。核心关键词agent-skills在这里不是指“AI 智能体的技能”而是指“可被代理调度的、具备明确输入输出契约的、原子化的能力单元”。它天然适配 TypeScript 的类型推导能力借力 Nx 的 workspace 构建与依赖图分析能力依托 semantic-release 实现全自动语义化版本发布最终运行于 Node.js 运行时之上——这四者不是简单堆砌而是形成了一条闭环类型定义驱动开发 → 单元隔离保障可测性 → 依赖图确保可追溯 → 语义版本控制演进节奏 → Node 运行时提供执行上下文。它解决的不是“怎么写 AI”而是“怎么让一段业务逻辑既能在命令行里跑也能在 Web Worker 里跑还能被另一个 TypeScript 项目 import 后直接调用且 IDE 能自动补全参数、编译器能提前报错、CI 能自动判断是否需要发版”。适合谁不是只给“TypeScript 面试突击党”看的速成教程也不是给“Nx 二次开发老手”的源码剖析。它是给那些正在被以下问题反复折磨的工程师准备的写了十个工具函数散落在不同项目的 utils 文件夹里改一处八处漏同步团队共用一个 CLI 工具每次加个新命令就得发版、所有人重装、文档还得手动更新前端项目要调用后端某个数据处理逻辑结果发现那块代码在 Java 服务里硬生生写了个 HTTP 接口就为了把一个数组去重格式化用 NestJS 写了个通用鉴权中间件想在 Express 项目里复用结果类型对不上、依赖冲突、打包体积翻倍。“agent-skills”就是把这些痛点用一套极简但极其严谨的约定打包成可安装、可导入、可调试、可灰度发布的标准件。它不强制你用 React 或 Vue也不要求你上 Kubernetes它只要求你写清楚输入是什么、输出是什么、副作用有哪些、依赖哪些外部能力。剩下的TypeScript 编译器、Nx 构建系统、semantic-release 发布流程、Node 运行时会像流水线工人一样默默帮你完成所有机械性工作。我去年用这套模式重构了公司内部的“配置校验器”“日志脱敏器”“Excel 模板生成器”三个高频组件上线后平均每个组件的跨项目复用率从 1.2 提升到 4.7PR 合并前的类型错误拦截率从 38% 提升到 92%最关键是——没人再为“那个 utils 函数又改了但没通知我”吵架了。1.1 为什么不是“npm 包”为什么不是“Nx Plugin”这是第一个必须掰开揉碎讲清楚的认知陷阱。很多人看到 “agent-skills” “Nx” “TypeScript”第一反应是“哦又是个 Nx 插件”。错。大错特错。Nx Plugin 是用来扩展 Nx CLI 命令、修改构建行为、注入新架构约束的——它服务于构建系统本身。而 agent-skills 是构建系统产出的东西是被构建出来的“制品”不是构建系统的“零件”。举个生活化类比Nx 就像一家精密机床厂的 CNC 控制系统它决定怎么切削、怎么进刀、怎么换刀而 agent-skills 就是这家机床厂生产出来的标准螺丝、轴承、齿轮——它们有自己的规格TS 类型、自己的材质Node 运行时兼容性、自己的包装semantic-release 版本号客户买回去可以装在汽车上也可以装在拖拉机上甚至可以自己拿去改造成新零件。你不会说“这个 M6 螺丝是个 CNC 插件”对吧同样“npm 包”这个说法也过于宽泛。一个普通的 npm 包可能没有类型声明、可能不支持 ESM/CJS 双输出、可能版本号乱跳、可能根本没测试覆盖率。而 agent-skills 的每一个发布都必须满足四条铁律类型即契约入口文件必须导出完整的index.d.ts且所有公共 API 必须有 JSDoc 注释参数、返回值、可能抛出的错误类型全部标注环境即承诺package.json 中明确声明engines: {node: 18.17.0}且 CI 中必须在该版本 Node 下通过全部测试版本即信号必须使用 semantic-release且 commit message 严格遵循 Conventional Commits 规范feat:提交自动触发 minor 版本fix:自动触发 patchBREAKING CHANGE自动触发 major构建即验证Nx workspace 中该 skill 必须被定义为独立 project拥有自己的project.json包含build、test、lint三个 target且buildtarget 的 outputPath 必须指向dist/不能是lib/或其他随意路径。这四条不是最佳实践是准入门槛。我见过太多团队把“写个工具函数扔进 npm”当成技术升级结果半年后发现函数里用了fs.promises但某个下游项目还在用 Node 14类型声明文件里写了import type { Foo } from bar但bar并不在 dependencies 里导致下游项目 tsc 报错版本号从1.0.0直接跳到1.5.0但 changelog 里只有一句“优化性能”没人知道哪个 API 被删了。agent-skills 的设计哲学就是用工具链的刚性约束把人容易犯的错在提交代码的那一刻就拦住。这不是限制自由而是把自由建立在可预测、可协作、可回滚的地基之上。1.2 它到底能做什么三个真实场景拆解光说概念太虚。我直接拿去年落地的三个真实案例告诉你 agent-skills 不是 PPT 架构而是每天都在跑的生产代码。场景一跨技术栈的“数据清洗管道”业务需求CRM 系统导出的 Excel 表格字段名全是中文如“客户姓名”、“联系电话”、“创建时间”而 BI 系统要求英文字段customerName,phone,createdAt且电话号码要统一格式化为86 138-1234-5678时间要转为 ISO 格式。之前做法是前端导出后人工用 Excel 公式处理后来写了个 Python 脚本但运营同学不会装 Python再后来搞了个 Web 页面但每次字段变更都要发版。用 agent-skills 解决我们定义了一个company/data-cleanerskill导出一个cleanCustomerData函数输入是Recordstring, any[]原始 Excel 行数据输出是CleanedCustomer[]强类型结果。它内部封装了xlsx库读取、date-fns时间处理、正则电话格式化等逻辑但对外只暴露一个函数和一个类型。前端项目import { cleanCustomerData } from company/data-cleaner直接调用Node.js 后端服务同样 import作为 API 的一部分甚至有个 Electron 桌面应用也用同一份代码做离线清洗。所有项目共享同一份类型定义IDE 自动补全tsc 编译时报错semantic-release 自动根据改动类型发版。上线后字段变更只需改 skill 里的映射表发个 patch 版本所有下游项目npm update即可生效。场景二CLI 工具的“命令即技能”业务需求运维团队需要批量检查 50 台服务器的磁盘空间、内存占用、特定进程状态。之前用 Bash 脚本维护困难、无类型、难调试。用 agent-skills 解决我们创建company/server-probeskill导出probeDiskUsage、probeMemoryUsage、probeProcessStatus三个函数每个函数接受ServerConfig参数返回 Promise 。然后在 Nx workspace 里用 Nx 的nx/node:executeexecutor写一个简单的 CLI wrapper读取命令行参数调用对应 skill 函数打印结果。关键点在于CLI wrapper 项目只负责“胶水”所有核心逻辑、类型、测试都在 skill 里。这意味着如果某个业务系统需要在页面上展示某台服务器的磁盘使用率图表它不用再写一遍 SSH 连接逻辑直接 importprobeDiskUsage就行。CLI 和 Web UI 复用同一套技能不是靠复制粘贴而是靠真正的模块复用。场景三NestJS 与 Express 的“中间件平移”业务需求一个通用的“请求 ID 注入与透传”中间件需要同时在 NestJS 的APP_INTERCEPTOR和 Express 的app.use()中使用。用 agent-skills 解决我们定义company/request-idskill导出两个工厂函数createRequestIdInterceptor()返回 NestJS Interceptor 实例createRequestIdMiddleware()返回 Express Middleware 函数。它们共享同一套核心逻辑生成 ID、注入 header、从 header 读取但适配层完全分离。下游项目按需 importNestJS 项目 import 前者Express 项目 import 后者。类型定义里createRequestIdInterceptor的返回类型是NestInterceptorcreateRequestIdMiddleware的返回类型是RequestHandlerIDE 能精准提示tsc 能严格校验。更重要的是当我们要支持新的框架比如 Fastify只需在同一个 skill 里新增createRequestIdFastifyDecorator()不破坏任何现有 APIsemantic-release 自动发 minor 版本。这三个场景的共同点是什么不是“用了 TypeScript”不是“用了 Nx”而是把一段有明确输入输出、有稳定契约、有可验证行为的业务逻辑从宿主框架React/Vue/NestJS/Express中剥离出来变成一个独立的、可被任何符合 Node.js 环境的消费者调用的标准能力单元。它不关心你在浏览器里跑还是在服务器上跑不关心你用什么框架只关心你能不能提供它需要的输入以及你是否遵守它定义的输出契约。这才是 “agent-skills” 的灵魂——它让“技能”本身成为一等公民而不是框架的附庸。2. 核心设计原理为什么这套组合拳能成立很多团队尝试过类似思路但最后都卡在“太重”或“太散”上要么搞成一个巨石 monorepo改一行代码要全量构建要么拆成几十个 npm 包版本管理混乱依赖地狱频发。agent-skills 能跑通靠的不是某个黑科技而是四股力量的精密咬合TypeScript 的类型系统、Nx 的依赖图与构建缓存、semantic-release 的自动化发布、Node.js 的运行时一致性。它们不是并列关系而是层层递进的依赖链。2.1 TypeScript不是“加了类型”而是“类型即接口契约”TypeScript 在这里扮演的角色远超“避免 runtime error”的初级定位。它是整个 agent-skills 体系的协议层。想象一下如果没有类型cleanCustomerData函数的输入可能是一个any[]调用方传什么它都接运行时才报错输出可能是一个object调用方拿到后还要if (res res.name)一堆防御性检查。这根本谈不上“技能复用”只是“代码复制”。而 agent-skills 强制要求所有公共 API 必须有显式类型签名所有输入参数必须是type或interface不能是any或object所有返回值必须是具体类型不能是Promiseany所有可能抛出的错误必须定义export type CleanError INVALID_PHONE | MISSING_FIELD | ...并在 JSDoc 的throws中注明。这带来的实际好处是颠覆性的。以company/data-cleaner为例它的index.d.ts文件长这样简化版/** * 清洗 CRM 导出的客户数据转换为 BI 系统所需格式 * param rawData 原始 Excel 行数据字段名为中文 * returns 清洗后的客户数据数组字段名为英文 * throws {CleanError} 当数据格式严重错误时 */ export function cleanCustomerData( rawData: RawCustomerData[] ): PromiseCleanedCustomer[]; export interface RawCustomerData { /** 客户姓名 */ 客户姓名: string; /** 联系电话 */ 联系电话: string; /** 创建时间 */ 创建时间: string; } export interface CleanedCustomer { customerName: string; phone: string; createdAt: string; } export type CleanError INVALID_PHONE | MISSING_REQUIRED_FIELD;注意几个细节JSDoc 里param和returns不是摆设VS Code 会直接显示在 hover 提示里RawCustomerData和CleanedCustomer是interface意味着它们可以被其他项目extend实现定制化CleanError是type调用方可以用if (error INVALID_PHONE)精准判断而不是error.message.includes(phone)这种脆弱匹配函数签名里明确写了Promise...告诉调用方这是异步操作不能当同步函数用。这种设计让类型信息本身就成了最权威的文档。我不需要再写一份 Markdown 文档说明“怎么用”因为 IDE 的智能提示、tsc 的编译错误、Jest 的测试用例已经构成了一个自验证的文档系统。更重要的是它让“兼容性”变得可计算。Nx 的依赖图分析不仅能知道 A 项目依赖 B 项目还能知道 A 项目调用了 B 项目的哪些类型、哪些函数。当 B 项目发布一个 breaking change比如把cleanCustomerData的返回类型从CleanedCustomer[]改成Promise{ data: CleanedCustomer[]; meta: any }Nx 的affected命令就能精准找出所有调用了该函数的项目并标记为“需要审查”。这比任何人工 code review 都可靠。提示不要在 skill 里写// ts-ignore。这是红线。agent-skills 的哲学是“宁可重构不可绕过”。如果某个第三方库类型不全正确的做法是在 skill 内部写一个types/xxx.d.ts声明文件或者用declare module xxx做局部增强而不是用ts-ignore自欺欺人。后者会让类型系统失效前者只是暂时补丁且补丁本身也是可测试、可版本化的。2.2 Nx不是“更快的构建”而是“可感知的依赖拓扑”Nx 在这里的作用常被误解为“比 npm run build 快”。其实快只是副产品。Nx 的核心价值在于它把整个 workspace 的代码变成了一个可查询、可分析、可影响的图谱。对于 agent-skills这个图谱解决了三个致命问题隔离性、可追溯性、可增量性。隔离性每个 skill 都是一个独立的 Nx project有自己的project.json。这意味着它的tsconfig.json是独立的可以设置isolatedModules: true确保每个文件都能单独编译它的jest.config.ts是独立的测试只跑自己的代码不会因为其他项目加了个新 mock 就失败它的eslint.config.js是独立的可以启用typescript-eslint/no-explicit-any这类严格规则而不影响其他宽松项目。这种物理隔离让 skill 的边界感极强。你不会不小心在>export default defineConfig({ resolve: { alias: { company/data-cleaner: path.resolve(__dirname, ../monorepo/dist/data-cleaner), }, }, })然后在.ts文件里import { cleanCustomerData } from company/data-cleaner。它成功运行了因为cleanCustomerData内部只用了date-fns和正则没有fs。但如果它用了fs.readFileSyncVite 就会报错提醒你“这个模块不能在浏览器里用”。这种清晰的边界感正是 Node.js 作为“最小公分母”的价值——它让你明确知道哪些代码是“纯逻辑”哪些是“环境依赖”。注意在 skill 的package.json中必须明确指定type: module并使用 ESM 语法export/import。这是为了未来兼容性。虽然 Node.js 目前支持 CJS但 ESM 是标准且 Nx、TypeScript、Vite 都对 ESM 有更好支持。我们禁止在 skill 中使用require()所有依赖都必须用import。这看起来是小约定但它统一了模块系统避免了__dirname、require.resolve这些 CJS 特有 API 带来的陷阱。3. 从零开始搭建一个可立即运行的 agent-skills 工作流理论讲完现在来实操。我会带你从一个空目录开始一步步搭建出一个符合所有规范的 agent-skills 工作流。这不是“照着文档抄”而是“带着思考建”。每一步我都会解释“为什么这么选”以及“如果不这么选会掉进什么坑”。3.1 初始化 Nx Workspace选择正确的起点第一步永远是创建 workspace。但 Nx 提供了多种 starter选错起点后面全是坑。# 错误示范用 --presetapps npx create-nx-workspacelatest my-workspace --presetapps --appNamemy-app # 这会创建一个以应用为中心的 workspaceprojects 目录下默认只有 app没有 libs。 # 你要手动创建 lib还要手动配置 build target非常麻烦。# 正确做法用 --presetts npx create-nx-workspacelatest my-workspace --presetts --no-nx-cloud # --presetts 是专门为 TypeScript 库设计的 preset。 # 它会自动创建一个空的 libs 目录并配置好基础的 tsconfig.base.json。 # --no-nx-cloud 是为了去掉 Nx Cloud 的 telemetry保持纯净。执行后你会得到一个标准的 Nx workspace 结构my-workspace/ ├── apps/ # 存放 CLI、Web App 等 consumer ├── libs/ # 存放所有 agent-skills ├── tools/ # 存放 workspace 级脚本 ├── nx.json # Nx 核心配置 ├── package.json └── tsconfig.base.json关键点在于tsconfig.base.json。打开它你会看到{ compilerOptions: { baseUrl: ., paths: { my-workspace/*: [libs/*/src/index.ts] } } }这个paths配置就是 agent-skills 的“命名空间”。它意味着你可以在任何地方import { foo } from my-workspace/data-cleaner而不需要写相对路径../../../libs/data-cleaner/src/index。这个别名是全局的由 TypeScript 编译器和 Nx 构建系统共同支持。提示my-workspace这个 scope 名应该和你未来 npm registry 的 scope 保持一致。比如如果你的私有 registry 是https://registry.mycompany.com那么你应该用mycompany/*。这样当你npm publish时包名会自动是mycompany/data-cleaner不会和 public npm 上的同名包冲突。3.2 创建第一个 Skill># 在 workspace 根目录下执行 nx g nx/workspace:library>/** * 清洗 CRM 导出的客户数据 * param rawData 原始数据 * returns 清洗后的数据 */ export function cleanCustomerData(rawData: { name: string; phone: string }[]): { name: string; phone: string }[] { return rawData.map(item ({ name: item.name.trim(), phone: item.phone.replace(/[^0-9]/g, ).padStart(11, 0).slice(-11), })); } // 导出类型供下游项目使用 export type RawData { name: string; phone: string }[]; export type CleanedData { name: string; phone: string }[];注意这里我们先用any[]做占位后面会替换成强类型。目的是先跑通流程。接下来编辑libs/data-cleaner/project.json确保buildtarget 配置正确{ targets: { build: { executor: nx/js:tsc, outputs: [{options.outputPath}], options: { outputPath: dist/libs/data-cleaner, main: libs/data-cleaner/src/index.ts, tsConfig: libs/data-cleaner/tsconfig.lib.json, assets: [libs/data-cleaner/*.md] } } } }关键点outputPath: dist/libs/data-cleaner必须和package.json的main字段一致稍后会生成。现在运行构建nx build>{ name: mycompany/data-cleaner, version: 0.0.1, main: ./index.js, types: ./index.d.ts, exports: { .: { import: ./index.mjs, require: ./index.js } }, peerDependencies: { typescript: ^5.0.0 } }注意exports字段它同时支持 ESM (import) 和 CJS (require)这是现代 npm 包的标准写法。3.3 集成 semantic-release让发版自动化、可审计现在skill 能构建了但还不能发布。我们需要 semantic-release。首先安装依赖npm install --save-dev semantic-release semantic-release/commit-analyzer semantic-release/release-notes-generator semantic-release/npm semantic-release/github conventional-changelog-conventionalcommits然后在 workspace 根目录下创建.releaserc{ branches: [main], plugins: [ semantic-release/commit-analyzer, semantic-release/release-notes-generator, [ semantic-release/npm, { npmPublish: true, pkgRoot: dist/libs/data-cleaner } ], [ semantic-release/github, { assets: [dist/libs/data-cleaner/**/*] } ] ] }关键配置解读branches: [main]只在 main 分支上触发发布semantic-release/npm的pkgRoot: dist/libs/data-cleaner告诉它要发布的是构建后的dist目录而不是源码目录semantic-release/github的assets把构建产物也上传到 GitHub Release方便审计。接着配置 CI以 GitHub Actions 为例在.github/workflows/release.yml中name: Release on: push: branches: [main] jobs: release: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 with: fetch-depth: 0 - uses: actions/setup
上一篇/下一篇内容由系统自动关联 返回资讯列表 →