1. 项目概述:一个被严重低估的 TypeScript 工程化能力基座
“agent-skills”这个名称乍看像某个 AI 智能体的技能插件库,但结合热搜词agent-skills, TypeScript, node, Nx, semantic-release,再叠加全网高频出现的typescript面试、nx二次开发、typescript + nestjs、node安装及环境配置等长尾搜索行为,真相立刻清晰:这不是一个面向终端用户的“AI技能包”,而是一个面向中高级前端/全栈工程师的、可复用、可组合、可版本化管理的 TypeScript 能力模块集合工程——它的核心价值,是把“写代码的能力”本身,抽象成可声明、可装配、可测试、可发布、可追溯的标准化单元。
我做过 7 个大型微前端平台、主导过 3 套企业级 CLI 工具链建设,也带团队从零搭建过基于 Nx 的 20+ 应用单体仓库。在这些实战中,“agent-skills”这类项目从来不是锦上添花的玩具,而是解决团队协作熵增的关键基础设施。它解决的不是“能不能跑”,而是“多人协作时,如何让 A 写的工具函数、B 封装的 API 客户端、C 抽象的状态管理逻辑,在不互相污染、不重复造轮子、不版本错乱的前提下,被 D、E、F 快速发现、安全引用、精准升级”。
你可能正在经历这些典型场景:
- 新同事入职后,花两天时间翻遍 Git 仓库才找到那个“发短信验证码”的通用请求封装;
- 某个核心 utils 函数被 5 个应用直接 copy-paste,结果修复一个 XSS 漏洞要手动改 5 处;
- 发布新版本时,CI 流水线报错 “
@myorg/http-client@2.1.0依赖@myorg/validator@1.8.3,但当前 workspace 中已锁定为1.9.0”,排查 3 小时才发现是某人本地npm install后没提交 lockfile; - 面试官问“你们怎么管理跨应用共享逻辑?”,你支吾着说“放 packages 目录下…用 npm link…偶尔会出问题…”——这背后暴露的,正是缺乏一套被工程化验证过的
agent-skills实践体系。
这个项目标题的精妙之处在于:“agent” 不指代 AI 智能体,而是取其本义——代理、中介、执行者;“skills” 也不是泛泛而谈的“技能”,而是特指可被调用、有输入输出契约、具备明确职责边界的最小能力单元。比如:
skill-http-client:统一处理鉴权、错误分类、重试、埋点的 HTTP 请求代理;skill-form-validator:支持 JSON Schema 描述、返回结构化错误、兼容 React/Vue/Svelte 的表单校验器;skill-storage-manager:自动降级(localStorage → memory → throw)、支持加密、带 TTL 的键值存储代理;skill-feature-flag:对接后端开关服务、支持灰度分流、本地覆盖调试的特性开关控制器。
它们共同构成一个“能力市场”(Capability Marketplace),每个 skill 都是独立的 npm 包(即使私有),拥有自己的 README、TypeScript 类型定义、Jest 单元测试、Changelog 和语义化版本号。而 Nx,就是这个市场的“交易所系统”——它负责管理所有 skill 的构建拓扑、依赖图谱、增量编译和影响分析;semantic-release,则是自动化的“发行委员会”,根据 commit message 的规范,自动判定 patch/minor/major 版本,并完成 npm publish、Git tag、Changelog 更新三件套。
所以,如果你正面临团队规模扩大、应用数量增多、技术栈趋同但代码复用率低下等问题,“agent-skills”不是可选项,而是必选项。它不教你 TypeScript 语法,但它会告诉你:当 10 个人都在写debounce函数时,真正的 TypeScript 工程师,选择把它变成一个 versioned, typed, tested 的 skill —— 并让所有人通过pnpm add @myorg/skill-debounce一行命令接入。
2. 整体架构设计与选型逻辑:为什么是 Nx 而不是 Turborepo 或 Lerna?
2.1 核心矛盾:单体仓库(Monorepo)的“自由”与“失控”
在启动agent-skills项目前,我们首先必须直面一个根本性问题:共享代码到底该放在哪里?常见方案有三种:
方案一:每个应用各自维护一份 copy
优点:完全隔离,无耦合。
缺点:Bug 修复需同步 10 个仓库;API 变更需协调 10 个团队;类型定义不一致导致运行时隐性错误。实测某电商中台曾因formatCurrency函数在 7 个应用中有 5 种实现,导致财务对账差异达 0.3%,排查耗时 4 人日。方案二:独立仓库 + npm publish
优点:版本清晰,权限可控。
缺点:发布周期长(写完代码 → 提 MR → CI 通过 → 手动 publish → 等待 CDN 同步 → 其他项目pnpm update);本地调试困难(改一个函数,要反复 publish/test);无法做跨包的类型检查(@myorg/skill-a引用@myorg/skill-b的类型,但两者在不同仓库,TS Server 无法联动推导)。方案三:Monorepo(单体仓库)
优点:代码共存,类型即刻联动;本地修改实时生效;统一 CI/CD;依赖关系可视化。
缺点:若无强约束,极易退化为“巨型泥球”——A 项目偷偷 import B 项目的内部 utils,C 项目直接修改 D 项目的 core logic,最终形成无法拆分的依赖地狱。
agent-skills的本质,就是在 Monorepo 的“高内聚”优势与“低耦合”要求之间,建立一套可执行的治理规则。而选型的核心,就是看哪个工具能最高效地 enforce 这些规则。
2.2 Nx:不是“另一个构建工具”,而是“可编程的工程约束引擎”
Nx 的不可替代性,体现在它对三个关键维度的深度控制:
(1)依赖拓扑(Dependency Graph)的强制可溯性
Nx 会在首次nx graph时,静态分析所有import语句,生成精确的依赖图。更重要的是,它允许你用代码定义依赖规则。例如,在nx.json中添加:
"targetDefaults": { "build": { "dependsOn": ["^build"] } }, "implicitDependencies": { "package.json": { "dependencies": ["*"] } }, "namedInputs": { "default": ["{workspaceRoot}/**/*", "!{workspaceRoot}/node_modules/**"] }, "projects": { "skill-http-client": { "tags": ["type:skill", "scope:network"], "implicitDependencies": ["@myorg/skill-logger"] }, "skill-form-validator": { "tags": ["type:skill", "scope:ui"], "allowedDependencies": ["@myorg/skill-logger", "@myorg/skill-utils"] } }这段配置意味着:
skill-form-validator只能显式依赖@myorg/skill-logger和@myorg/skill-utils,如果它偷偷 import 了skill-http-client,nx dep-graph会标红警告,nx build会直接失败;- 所有
buildtarget 自动依赖上游build,确保skill-logger构建完成后再构建skill-http-client; package.json的变更会触发所有项目重建(因为可能影响依赖解析)。
这种“代码即策略”的能力,是 Turborepo(仅做缓存加速)和 Lerna(仅做版本/发布)完全不具备的。Turborepo 无法阻止非法 import,Lerna 甚至不关心 import 关系。
(2)增量构建(Incremental Build)的精准粒度
Nx 的缓存不是基于文件哈希,而是基于input hash + output hash + command hash的三元组。这意味着:
- 如果你只修改了
skill-http-client/src/interceptors/auth.interceptor.ts,Nx 会精确计算出:- 哪些 test target 受影响(只有
skill-http-client:e2e和skill-http-client:test); - 哪些 build target 需要重跑(只有
skill-http-client:build); - 哪些下游项目需要重新构建(只有直接依赖它的
app-admin-dashboard)。
- 哪些 test target 受影响(只有
- 而不会像 Webpack 那样,因一个
.d.ts文件变更,就触发整个node_modules重解析。
实测数据:在一个含 42 个 skill、18 个应用的 Nx workspace 中,单个文件修改后的nx build平均耗时 1.8s(缓存命中),而同等规模下tsc --build需 23s,pnpm run build(无增量)需 47s。这节省的不仅是时间,更是开发者等待时的注意力损耗。
(3)任务调度(Task Pipeline)的可组合性
Nx 的target不是简单的 script 别名,而是可嵌套、可参数化、可条件触发的“任务单元”。例如,为agent-skills定义一个publishpipeline:
"publish": { "executor": "@nrwl/workspace:run-commands", "options": { "commands": [ "nx affected --target=build --base=origin/main --head=HEAD --parallel=3", "nx affected --target=test --base=origin/main --head=HEAD --parallel=3", "npx semantic-release" ] } }这个publishtarget 会:
- 先找出本次 PR 影响的所有 skill(
affected); - 并行构建它们(
--parallel=3); - 并行测试它们;
- 最后交由 semantic-release 决定是否发布及版本号。
整个过程无需 shell 脚本胶水,全部在 Nx 的 task graph 中编排,天然支持nx run-many --target=publish --projects=skill-http-client,skill-form-validator这样的细粒度操作。
2.3 为什么不是 Turborepo?—— 缓存不能替代约束
Turborepo 确实以极快的缓存速度著称,但它本质上是一个“智能的 make 工具”。它能回答“这个命令上次跑过吗?输出有没有变?”,但无法回答“这个 import 是否符合架构规范?”。在agent-skills场景中,Turborepo 可以让pnpm build更快,但它无法阻止一个 junior developer 在skill-ui-kit里直接 importapp-legacy-backend/src/utils/legacy-api-helper.ts——而这恰恰是 Monorepo 最危险的滑坡。
我们曾用 Turborepo 替换过一个老项目,CI 时间从 12 分钟降到 4 分钟,但三个月后,libs/目录下出现了shared-utils,common-utils,core-utils,base-utils四个几乎相同的功能包,只因没人 enforce “utils 必须归口到@myorg/skill-utils”。Nx 的project.json中tags和allowedDependencies字段,才是防止这种熵增的真正护栏。
2.4 为什么不是 Lerna?—— 发布不是孤岛,而是流水线一环
Lerna 的核心价值在lerna publish,但它对构建、测试、依赖管理毫无建树。在agent-skills中,发布决策必须基于:
- 本次变更是否真的影响了某个 skill 的 public API?(需
nx affected --target=build检查) - 影响的 skill 是否通过了所有测试?(需
nx affected --target=test) - changelog 是否已按规范生成?(需
conventional-changelog)
Lerna 无法驱动这些前置检查。它要求你手动运行lerna run test,再手动运行lerna run build,最后再lerna publish——这中间任何一步失败,都可能导致部分包发布成功、部分失败,造成版本混乱。而 Nx 的run-many和affected是原子性的:nx affected --target=publish会确保所有受影响的 skill,要么全部发布成功,要么全部回滚(通过 CI 的 job failure 实现)。
更关键的是,Lerna 的--since逻辑基于 Git commit,而 Nx 的--base基于 Git ref,支持--base=origin/release/v2.0这样的复杂基线,这对agent-skills的多分支发布(如同时维护 v1.x LTS 和 v2.x 主线)至关重要。
3. 核心技能模块设计与 TypeScript 实现细节
3.1 Skill 的标准契约:不只是函数,而是“能力接口”
一个合格的agent-skills模块,绝非简单的一堆工具函数。它必须遵循一套严格的 TypeScript 契约,确保可发现、可理解、可信赖。我们以skill-http-client为例,解剖其骨架:
(1)明确的入口与边界
libs/skill-http-client/src/index.ts是唯一公开入口,内容必须极简:
// libs/skill-http-client/src/index.ts export * from './lib/client'; export * from './lib/interceptors'; export * from './lib/types'; export { createHttpClient } from './lib/factory';所有./lib/下的子目录,对外部使用者完全透明。createHttpClient是工厂函数,而非默认导出的实例——这保证了每个应用可以创建自己独立的 client 实例(避免全局状态污染),同时支持 DI 容器注入。
(2)类型优先(Type-First)的设计哲学
skill-http-client的核心不是fetch调用,而是HttpRequestConfig和HttpResponse<T>的类型定义:
// libs/skill-http-client/src/lib/types.ts export interface HttpRequestConfig { url: string; method: 'GET' | 'POST' | 'PUT' | 'DELETE'; headers?: Record<string, string>; data?: any; params?: Record<string, string | number>; timeout?: number; // ms withCredentials?: boolean; } export interface HttpResponse<T = any> { data: T; status: number; statusText: string; headers: Record<string, string>; config: HttpRequestConfig; request: XMLHttpRequest | undefined; // 仅浏览器 }这些类型被client.ts、interceptors.ts、factory.ts全面消费,更重要的是,它们会被下游应用直接 import 使用:
// apps/admin-app/src/app/services/user.service.ts import { HttpRequestConfig, HttpResponse } from '@myorg/skill-http-client'; @Injectable() export class UserService { constructor(private http: HttpClient) {} getUser(id: string): Observable<HttpResponse<User>> { return this.http.request({ url: `/api/users/${id}`, method: 'GET' }); } }类型即文档。当一个新成员看到HttpResponse<User>,他立刻知道返回结构;看到HttpRequestConfig,他就明白如何构造请求。这比任何 JSDoc 都有效。
(3)拦截器(Interceptor)模式:能力的可插拔性
skill-http-client不内置任何业务逻辑(如 token 刷新),而是提供useInterceptor方法:
// libs/skill-http-client/src/lib/client.ts export class HttpClient { private interceptors: Interceptor[] = []; useInterceptor(interceptor: Interceptor): this { this.interceptors.push(interceptor); return this; } async request<T>(config: HttpRequestConfig): Promise<HttpResponse<T>> { let currentConfig = { ...config }; for (const interceptor of this.interceptors) { currentConfig = await interceptor.resolve(currentConfig) ?? currentConfig; } // ... 执行 fetch } } export interface Interceptor { resolve(config: HttpRequestConfig): Promise<HttpRequestConfig> | HttpRequestConfig; }业务方可以这样使用:
// apps/ecommerce-app/src/app/core/http.interceptors.ts import { Interceptor, HttpRequestConfig } from '@myorg/skill-http-client'; export class AuthInterceptor implements Interceptor { async resolve(config: HttpRequestConfig): Promise<HttpRequestConfig> { const token = await getAccessToken(); // 业务自己的 token 获取逻辑 return { ...config, headers: { ...config.headers, Authorization: `Bearer ${token}` } }; } } // 在 AppModule 中 const httpClient = createHttpClient().useInterceptor(new AuthInterceptor());这种设计将“能力”(http client)与“策略”(auth logic)彻底解耦。skill-http-client本身不关心 token 怎么来,它只提供 hook;业务方也不用 fork 修改 client 源码,只需实现Interceptor接口。这就是agent-skills的精髓:Skill 提供能力容器,业务决定能力配方。
3.2 TypeScript 高级技巧:让类型成为第一道防线
(1)declare module的精准打补丁
skill-http-client依赖axios,但axios的类型定义过于宽泛(如any)。我们用declare module精准覆盖:
// libs/skill-http-client/src/lib/axios.d.ts declare module 'axios' { export interface AxiosRequestConfig { // 覆盖 axios 原生类型,强制要求 url 和 method url: string; method: 'GET' | 'POST' | 'PUT' | 'DELETE'; } export interface AxiosResponse<T = any> { data: T; status: number; } }这个.d.ts文件只在skill-http-client项目内生效,不影响 workspace 中其他项目对axios的使用。它让axios的类型在skill-http-client上下文中变得严格,同时保持外部兼容性。
(2)as const与字面量类型推导
skill-feature-flag需要定义开关名称,我们用as const锁定字面量类型:
// libs/skill-feature-flag/src/lib/flags.ts export const FEATURE_FLAGS = { ENABLE_NEW_CHECKOUT: 'enable-new-checkout', SHOW_BETA_TOOLS: 'show-beta-tools', DISABLE_ANALYTICS: 'disable-analytics', } as const; export type FeatureFlagKey = keyof typeof FEATURE_FLAGS; // 推导出 type FeatureFlagKey = 'ENABLE_NEW_CHECKOUT' | 'SHOW_BETA_TOOLS' | 'DISABLE_ANALYTICS' export type FeatureFlagValue = typeof FEATURE_FLAGS[FeatureFlagKey]; // 推导出 type FeatureFlagValue = 'enable-new-checkout' | 'show-beta-tools' | 'disable-analytics'下游应用使用时:
// apps/dashboard/src/app/components/chart.component.ts import { FEATURE_FLAGS, isFeatureEnabled } from '@myorg/skill-feature-flag'; // ✅ 编译期检查:'enable-new-checkout' 是合法 key isFeatureEnabled(FEATURE_FLAGS.ENABLE_NEW_CHECKOUT); // ❌ 编译错误:'non-existent-flag' 不在 FEATURE_FLAGS 中 isFeatureEnabled('non-existent-flag'); // Type '"non-existent-flag"' is not assignable to type 'FeatureFlagKey'这比运行时字符串校验强大百倍——错误在写代码时就被捕获。
(3)泛型约束与条件类型:构建类型安全的工厂
skill-storage-manager支持多种存储后端(localStorage, IndexedDB, Memory),我们用泛型约束确保类型安全:
// libs/skill-storage-manager/src/lib/storage.ts export type StorageBackend = 'localStorage' | 'indexedDB' | 'memory'; export interface StorageOptions<T extends StorageBackend> { backend: T; prefix?: string; // 根据 backend 不同,要求不同的额外参数 ...(T extends 'indexedDB' ? { dbName: string; storeName: string } : {}); ...(T extends 'localStorage' ? { maxAge?: number } : {}); } export class StorageManager<T extends StorageBackend> { constructor(private options: StorageOptions<T>) {} set<K extends string, V>(key: K, value: V): Promise<void> { // 根据 this.options.backend 分支实现 } } // 使用时,类型自动推导 const localStorageMgr = new StorageManager({ backend: 'localStorage', maxAge: 3600 }); const indexedDBMgr = new StorageManager({ backend: 'indexedDB', dbName: 'mydb', storeName: 'cache' });这里StorageOptions<T>的条件类型,确保传入backend: 'indexedDB'时,dbName和storeName是必需的;传入backend: 'localStorage'时,maxAge才是可选的。TypeScript 编译器会强制执行,杜绝配置遗漏。
4. 实操全流程:从初始化到自动化发布
4.1 初始化 workspace:避开 90% 的新手坑
不要用npx create-nx-workspace!这是官方文档的“教学路径”,但对agent-skills这类基建项目,它会生成大量无关的 demo app 和 framework 配置,徒增噪音。我们采用零配置初始化:
# 1. 创建空目录并初始化 git mkdir agent-skills && cd agent-skills git init # 2. 安装 Nx CLI(全局或局部) npm install -g nx # 或者局部安装(推荐,避免全局版本冲突) npm install -D nx # 3. 初始化最小化 workspace(不选任何 preset) npx nx@latest init --no-interactive --preset=apps --nx-cloud=false # 4. 清理掉默认生成的 apps/ 和 e2e/ 目录(我们不需要 demo app) rm -rf apps/ e2e/此时nx.json是干净的:
{ "tasksRunnerOptions": { "default": { "runner": "@nrwl/workspace/tasks-runners/default" } }, "targetDefaults": { "build": { "dependsOn": ["^build"] } } }关键点:--preset=apps是为了生成基础 workspace 结构,--nx-cloud=false避免引入 Nx Cloud 的 CI 集成(初期不需要)。
⚠️ 注意事项:Node 版本与 pnpm 的黄金组合
agent-skills对 Node 版本敏感。实测 Node 18.18.0 是目前最稳定的版本(Node 20+ 在某些@types/node下有node:util导出问题,如热搜词中提到的syntaxerror: the requested module 'node:util' does not provide an export named)。建议在项目根目录添加.nvmrc:
18.18.0并强制使用pnpm(而非 npm 或 yarn):
# 安装 pnpm npm install -g pnpm # 设置 pnpm store(避免重复下载) pnpm setup # 在 workspace 根目录启用 pnpm pnpm installpnpm的硬链接机制,能让libs/下的 50 个 skill 共享同一份node_modules,nx build时解析速度提升 40%,且pnpm link的行为比npm link更可靠,本地调试skill-a依赖skill-b时不会出现Cannot find module错误。
4.2 创建第一个 skill:skill-utils的完整流程
以skill-utils为例,演示从创建、编码、测试到发布的闭环:
# 1. 使用 Nx 命令创建 library(自动配置 project.json) nx g @nrwl/workspace:library skill-utils --directory=libs --tags=type:skill,scope:core # 2. 查看生成的结构 ls libs/skill-utils/ # ├── project.json # Nx 项目配置 # ├── src/ # 源码 # │ ├── index.ts # 入口 # │ └── lib/ # │ └── index.ts # 实际逻辑 # ├── jest.config.ts # Jest 配置 # └── tsconfig.lib.json # TypeScript 配置(1)编写核心逻辑(libs/skill-utils/src/lib/index.ts)
/** * 深度克隆对象(支持 Date, RegExp, Map, Set) * @param obj 要克隆的对象 * @returns 克隆后的新对象 */ export function deepClone<T>(obj: T): T { if (obj === null || typeof obj !== 'object') return obj; if (obj instanceof Date) return new Date(obj.getTime()) as any; if (obj instanceof RegExp) return new RegExp(obj) as any; if (obj instanceof Map) { return new Map(obj) as any; } if (obj instanceof Set) { return new Set(obj) as any; } const cloned: any = Array.isArray(obj) ? [] : {}; for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { cloned[key] = deepClone(obj[key]); } } return cloned; } /** * 防抖函数(返回可取消的函数) * @param fn 要防抖的函数 * @param delay 延迟毫秒数 * @returns 防抖后的函数,带有 cancel 方法 */ export function debounce<F extends (...args: any[]) => void>( fn: F, delay: number ): ((...args: Parameters<F>) => void) & { cancel: () => void } { let timer: ReturnType<typeof setTimeout> | null = null; const debounced = function (this: any, ...args: Parameters<F>) { if (timer) clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); timer = null; }, delay); } as any; debounced.cancel = () => { if (timer) { clearTimeout(timer); timer = null; } }; return debounced; }(2)编写类型定义(libs/skill-utils/src/lib/index.ts的顶部)
// 为 deepClone 添加泛型约束,确保返回类型与输入一致 export function deepClone<T>(obj: T): T; // 为 debounce 添加精确的返回类型 export function debounce<F extends (...args: any[]) => void>( fn: F, delay: number ): ((...args: Parameters<F>) => void) & { cancel: () => void };(3)编写单元测试(libs/skill-utils/src/lib/index.spec.ts)
import { deepClone, debounce } from './index'; describe('skill-utils', () => { describe('deepClone', () => { it('should clone plain object', () => { const original = { a: 1, b: { c: 2 } }; const cloned = deepClone(original); expect(cloned).toEqual(original); expect(cloned).not.toBe(original); expect(cloned.b).not.toBe(original.b); }); it('should clone Date', () => { const date = new Date('2023-01-01'); const cloned = deepClone(date); expect(cloned).toEqual(date); expect(cloned).not.toBe(date); }); }); describe('debounce', () => { jest.useFakeTimers(); it('should delay execution', () => { const fn = jest.fn(); const debounced = debounce(fn, 100); debounced(); expect(fn).not.toHaveBeenCalled(); jest.advanceTimersByTime(50); expect(fn).not.toHaveBeenCalled(); jest.advanceTimersByTime(50); expect(fn).toHaveBeenCalledTimes(1); }); it('should cancel pending execution', () => { const fn = jest.fn(); const debounced = debounce(fn, 100); debounced(); debounced.cancel(); jest.advanceTimersByTime(100); expect(fn).not.toHaveBeenCalled(); }); }); });(4)运行测试与构建
# 运行 skill-utils 的测试(自动使用 Jest) nx test skill-utils # 构建 skill-utils(生成 dist/ 目录,包含 .d.ts 和 .js) nx build skill-utils # 查看构建产物 ls dist/libs/skill-utils/ # ├── index.d.ts # ├── index.js # ├── index.js.map # ├── package.json # └── README.mdnx build会自动生成dist/libs/skill-utils/package.json,其中main,types,exports字段已正确配置,可直接被其他项目pnpm add @myorg/skill-utils引用。
4.3 集成 semantic-release:让发布变成“提交即发布”
agent-skills的发布必须自动化,否则skill-http-client的 bug fix 就会卡在“等发布”的环节。semantic-release 是业界标准,但需与 Nx 深度集成。
(1)安装与配置
# 在 workspace 根目录安装 pnpm add -D semantic-release @semantic-release/commit-analyzer @semantic-release/release-notes-generator @semantic-release/npm @semantic-release/github # 创建 .releaserc.json cat > .releaserc.json << 'EOF' { "branches": ["main"], "plugins": [ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", [ "@semantic-release/npm", { "npmPublish": true, "pkgRoot": "dist" } ], [ "@semantic-release/github", { "assets": ["dist/**/*"] } ] ] } EOF关键点:"@semantic-release/npm"的pkgRoot: "dist"告诉它去dist/目录下找package.json和index.js,而不是项目根目录。
(2)配置 Nx 的 publish target
在libs/skill-utils/project.json中添加:
"publish": { "executor": "nx:run-commands", "options": { "commands": [ "nx build skill-utils", "cd dist/libs/skill-utils && npx semantic-release" ] } }这样,nx run skill-utils:publish就会先构建,再发布。
(3)Commit 规范:让机器读懂你的意图
semantic-release 依赖 commit message 的格式。我们约定:
fix:开头:触发 patch 版本(0.0.X)feat:开头:触发 minor 版本(0.X.0)BREAKING CHANGE:在 body 中:触发 major 版本(X.0.0)
示例:
git commit -m "fix(skill-utils): deepClone should handle null input correctly" git commit -m "feat(skill-http-client): add support for custom timeout in request config" git commit -m "chore(release): release v1.0.0\n\nBREAKING CHANGE: remove deprecated createClient() function"(4)CI 流水线(GitHub Actions 示例)
.github/workflows/publish.yml:
name: Publish Skills on: push: branches: [main] paths: - 'libs/**' - '.releaserc.json' - 'package.json' jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: fetch-depth: 0 # 必须获取所有历史,semantic-release 需要比较 last release tag - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: '18.18.0' cache: 'pnpm' - name: Install pnpm run: npm install -g pnpm - name: Install dependencies run: pnpm install - name: Build affected skills run: npx nx affected --target=build --base=origin/main --head=HEAD --parallel=3 - name: Test affected skills run: npx nx affected --target=test --base=origin/main --head=HEAD --parallel=3 - name: Publish env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} run: npx nx affected --target=publish --base=origin/main --head=HEAD这个 workflow 的精妙之处在于:
paths过滤确保只有libs/下的变更才触发发布;nx affected确保只构建、测试、发布真正受影响的 skill,而非全部;GITHUB_TOKEN用于 GitHub Releases,NPM_TOKEN用于 npm publish,两者分离,权限最小化。
5. 常见问题与实战排错指南
5.1 “Cannot find module '@myorg/skill-utils'” —— 本地链接失效的终极解法
这是agent-skills开发中最高频的问题。现象:你在apps/demo-app中import { deepClone } from '@myorg/skill-utils',VS Code 提示类型正常,但nx serve demo-app报错Cannot find module。
排查路径:
确认
pnpm link是否生效pnpm link在 Nx workspace 中并非必须,因为 Nx 默认使用tsconfig.base.json的paths映射。检查tsconfig.base.json:"compilerOptions": { "baseUrl": ".", "paths": { "@myorg/skill-utils": ["libs/skill-utils/src/index.ts"], "@myorg/skill-http-client": ["libs/skill-http-client/src/index.ts"] } }如果
paths存在,说明走的是 TypeScript 路径映射,而非物理链接。检查
dist/目录是否存在且正确
运行nx build skill-utils,确认dist/libs/skill-utils/index.js和index.d.ts存在。如果不存在,nx serve会 fallback 到源码,但 Webpack 无法解析paths映射。Webpack 的
resolve.alias配置缺失
Nx 的 Angular/React preset 会自动配置 alias,但如果是自定义 executor,需手动添加。在apps/demo-app/project.json的buildtarget 中: