当前浏览的是 v7 预览版文档(v7 尚未正式发布,API 可能随版本调整)。本页是 v7 新增内容,去 v6 正式版文档首页
@ikenxuan/amagi

仓库工程

构建、测试、CI 门禁与代码约定 —— 改代码之前需要知道什么会红

仓库工程

这一页回答一个问题:我改了东西,什么会红,为什么。

仓库地图

pnpm workspace,glob 只有 packages/*:

包路径可见性用途
@ikenxuan/amagipackages/core公开发布唯一发布物
@ikenxuan/amagi-webpackages/webprivate控制台(Vite + React + HeroUI):填参 → 发请求 → 同屏看响应体与它的类型声明 → 就地生成响应类型
@ikenxuan/amagi-typegenpackages/typegenprivate响应类型生成器:本地样本 → 形状树 → TS 源码
@ikenxuan/amagi-response-typespackages/response-typesprivate生成出来的响应类型本体,整棵 src/generated/ 由 typegen 派生,手改无意义
@ikenxuan/amagi-codemodpackages/codemodprivatev6 → v7 消费方改写工具
docspackages/docsprivate本文档站

依赖方向:docs → core、web → core + web → typegen(都 workspace:*),以及 core → response-types + core → typegen。文档站的生成脚本与 packages/web/server/ 都直接按相对路径 import core 的源码(../../core/src/...,绕过包边界,因为它们要读端点注册表本身)。codemod / typegen / response-types 不依赖任何工作区包。

core 那两条挂在 devDependencies 上是刻意的,不是漏写:tsdown 只外部化 dependencies / peerDependencies,挂到那两处,dist/*.d.ts 里就会留下一个裸 import '@ikenxuan/amagi-response-types' —— 而这个包不发布,下游解析不到。挂 devDep 才会被内联进声明产物。

看起来像包但不在 workspace 里的顶层目录:docs/(v7 的设计文档与执行清单)、skills/(给 AI 代理的技能包)。两者都没有 package.json。

构建

packages/core 用 tsdown(底层 rolldown),配置在 packages/core/tsdown.config.ts:

  • 6 个入口:default/index ← src/index.ts,以及 glob exports/* ← src/exports/*.ts(axios / compat / express / sign-steps / signing)
  • 双格式:ESM(.mjs)+ CJS(.cjs),扩展名由 outExtensions 指定
  • .d.ts 逐入口生成,且只随 ESM 生成一份(CJS 的 dts 关掉了);共享类型抽成一个独立 chunk
  • define:__VERSION__ ← package.json 的 version,被 src/index.ts 与 server/openapi.ts 消费
  • 无 sourcemap、无 banner、无显式 externals(走 tsdown 默认:dependencies 与 peerDependencies 不打包)

6 个入口与 package.json 的 6 个 exports 子路径一一对应。files 只发 4 项:dist/*、LICENSE、package.json、README —— openapi.json 不进 npm 包,它只服务生成器与文档站。

命令

文档站这一侧的检查都编译真实的 dist/*.d.ts,不是 core 的源码。 examples/**/*.ts 按包名 @ikenxuan/amagi import,包名经 exports.types 解析到 dist。 所以 docs 的 typecheck / build / dev 都自带 build:core 前置 —— 少了它本地看不出来 (开发机上 dist 早就在那儿),只有 fresh clone 的 CI 会红,报 TS2307。

从仓库根:

命令展开需要先 build:core
pnpm build递归各包 build(docs 那条自含 build:core)自含
pnpm build:corecore 的 tsdown—
pnpm typecheck递归六个包的 typecheckdocs 那条自含
pnpm lint / pnpm fix递归 oxlint . / oxfmt否
pnpm test根 vitest run否(走源码别名)
pnpm test:typesvitest run --typecheck.enabled否
pnpm openapi / openapi:checkcore 的 tsx scripts/gen-openapi.mts [--check]否
pnpm deps:checkdpdm --exit-code circular:1 加四个入口:core / typegen / response-types 的 src/index.ts 与 packages/web/server/index.ts否
pnpm console / console:serverweb 的 vite / tsx watch server/index.ts,两条都自含 build:core自含
pnpm gen:typestypegen 的 tsx scripts/gen-types.mts:读本地 corpus/,写 packages/response-types/src/generated/否
pnpm types:check同一个脚本加 --check:有样本时逐字节比对产物与证据;零样本时验 barrel 与产物树是否一致否
pnpm build:docsdocs 的 build 全链,六步自含
pnpm docs:devdocs 的 dev(自含 build:core + docs:api)自含

packages/docs 的 build 是六步串联,顺序有讲究:

build:core → docs:api → check:includes → check:sidebar → next build → check:links

两道静态门排在 next build 之前:区段或侧边栏的问题十几秒就红,不用等几分钟的整站构建。

pnpm fix / pnpm lint / pnpm sort 都用 --filter=./packages/*,所以根级的 .ts 文件永远不被检查或格式化(vitest.config.ts、oxlint.config.ts、.husky/husky-tasks.ts)。 packages/codemod 也没有定义 fix。

测试

配置在根 vitest.config.ts,include 四条:core / codemod / typegen / web 各自的 test/**/*.test.ts。codemod 与 typegen 包内还各有一份 vitest.config.ts 供单跑,但必需检查跑的是根脚本 —— 不收进这条 include 等于那些用例在 CI 里不存在。

关键别名:/^amagi\/(.*)$/ → packages/core/src/$1。core 的用例因此走源码,pnpm test 不需要 dist —— 与文档站那一侧相反。

布局

core/test/ 下的目录.test.ts.test-d.ts快照覆盖什么
platforms/(含 legacy/)7515四平台端点行为、签名、judge、配置;legacy/ 是 v6 保留实现的签名与 URL(公开面基线)
contracts/111—契约层
validation/6—2zod schema 与路由表
model/ / runtime/ / server/ / transport/4 / 4 / 5 / 6—1 / — / 1 / —事件、管线、路由、传输
contract/4—4公开面、请求面与 known-defects 快照
client/101—门面、fetcher、login 类型
types/113—响应类型与索引签名
其余(compat / errors / openapi / utils / verify)6——compat、errors、openapi、utils、verify
合计1321613

helpers/ 下 4 个文件不是测试,是共用工具(adapter 注入、熵源冻结、fixtures、监听端口)。另外三个包的 test/ 小得多:typegen 15 个、web 24 个、codemod 1 个。

test 与 test:types 的分工

pnpm test 只跑 172 个 *.test.ts、3,824 个用例(typecheck.enabled 默认关)。pnpm test:types 把 16 个 *.test-d.ts 也加进来,共 188 个文件、3,941 个用例,用 packages/core/tsconfig.test.json 做类型检查。数字都由这两条命令末尾的 Test Files / Tests 行量。

两个都必需,因为 packages/core/tsconfig.json 的 exclude 排除了 **/*.test.ts 与 **/*.test-d.ts —— pnpm typecheck 抓不到测试文件里的类型错误,只有 test:types 能。

快照锁的是什么

13 个快照文件全在 test/**/__snapshots__/:

快照锁住
contract/public-surface主入口 94 个名字(93 个具名导出 + default)、每个导出的运行时类型、client 顶层与四平台子对象的键集合、构造函数静态属性
contract/fetcher-surface四平台 static + bound 两种形态的方法名列表,以及工具集的键集合
contract/request-surfaceclient.<平台>.request 的手挑签名步骤面
contract/known-defects全套用例里 KNOWN-DEFECT: 标题的清单
platforms/legacy/api-urls若干端点的完整 URL 字符串(易变字段替换为占位)
platforms/legacy/sign-douyin / sign-kuaishou / sign-xiaohongshu冻结熵源后签名算法的确定输出
platforms/legacy/default-configsv6 保留实现的默认配置
validation/contract四平台的 methodType 列表、路由表、每个 methodType 的接受键与默认值
validation/xiaohongshu小红书校验表
model/events12 个事件名与四种负载结构
server/routesHTTP 成功响应的键集合

签名快照发生变化 = 线上功能损坏,而类型检查与 lint 都发现不了。任何情况下都不要用 vitest -u 更新签名快照 —— 快照变了先假设是自己搬错了。这是这个仓库最硬的一条规矩。

known-defects

一种把「已知错误行为」显式记账的机制:用例标题以 KNOWN-DEFECT: 开头,一个专门的用例递归扫 test/ 收集全部这类标题、快照化,并断言清单非空、每条标题够长。

含义是「v6 行为错误但已钉死」,构成修复清单。修掉一条 → 对应用例必然失败 → 必须显式改写或删除那个用例,不允许注释掉或加 .skip。这个数字只应下降。

CI 门禁

门禁在 ci.yml(push/PR 触发),发布在 release.yml(v* tag 触发,维护者本地 pnpm run release 推出来)—— 两者分家,发布永远不被门禁拦。核心是 ci.yml 的 quality job —— 它恒跑(连只改文档的提交也跑),且是构建/预览链路的前置,任一步失败整条流水线红。

步骤什么让它红
pnpm typecheck任一包类型错误;docs 那条还串了三道静态门
pnpm lintoxlint 非 0(实际只有 no-unused-vars 是 error 级)
pnpm test172 个文件 / 3,824 个用例任一失败,含全部快照
pnpm test:types16 个 *.test-d.ts 的类型错误
pnpm openapi:check已提交的 openapi.json 与四个 registry 现算结果不逐字节一致
pnpm deps:checkdpdm 发现任何循环依赖(必须 0 环)
pnpm types:check落盘那一层自己跑不通,或 barrel 与产物树不一致(这条零样本也验,2026-09-10 补)。它验不了「产物与样本一致」 —— 样本不进 git,没有样本时脚本明说跳过比对,不假装通过;残留文件检测也在零样本时短路掉。一致性只在本地有样本的机器上验
pnpm build:docs整站构建失败,或站内出现死链
控制台前端构建pnpm --filter @ikenxuan/amagi-web run build 失败 —— 缺 index.html、Tailwind 插件没配、CSS 里 @import 的包解析不开这类 tsc --noEmit 碰不到的事;或 JS 产物超预算(两个预算:首屏 640 KB、产物合计 840 KB,2026-09-06 实测 635 KB / 784 KB),那通常是 Node-only 的东西被写成了值 import 漏进浏览器包。判据是体积不是 grep 关键字 —— minify 之后那些标识符一个都搜不到

changes job 先算一次「这次改的是不是全在不发版的包里」(判据是变更清单里有没有 ^packages/(docs|web)/ 之外的文件;docs 与 web 都 private: true,全仓只有 packages/core 会发布),用来决定跳不跳发版链路 —— 但质量门禁不跳,文档站与控制台的构建照跑。output 名字仍叫 docs_only,那是为了不动下游那个 if:。历史上这里踩过坑:用触发器的 paths-ignore 把文档站整条流水线关掉,代价是文档站的回归永远进不了 CI。

文档站的三道门

三个脚本都带「零产物即 exit 1」的自毁开关 —— 空输入下「没问题」是平凡真,必须主动拒绝。

脚本断言
check-includes.mjs<include> 引用的代码区段还存在
check-sidebar.mjs每个侧边栏条目有图标、图标名是 lucide-react 的真实导出、分区分隔符只有一层、折叠目录只许出现在 OpenAPI 子树
check-links.mjs扫 .next/server/app 的预渲染 HTML,内部 /docs 链接都指向真实页面

check-includes 同时挂在 typecheck 与 build:docs 上,这不是冗余:整站构建是最容易被环境挡住的一步(Turbopack 起不了子进程、next/font 取不到 Google Fonts 都能让它失败),而「<include> 区段在不在」不该被那一步绑住。

代码约定

提交信息

commit-msg 钩子校验类型,允许列表内嵌在 .husky/husky-tasks.ts(九种,见贡献指南)。

pre-commit 钩子目前空转:它的第一行 . "$(dirname -- "$0")/_/h" 是 husky v8 的写法, 而 v9 的调用链是「shim 先 source 那个文件、再执行本文件」,于是这一行会二次进入 shim, 算出一个不存在的路径后 exit 0,永远走不到更新 packages/core/package.json 的 timestamp 那段。 证据:那个 timestamp 已经几个月没变过。删掉钩子开头那两行即可修复。

lint 与格式化

偏离工具默认值的设置(三份同名配置分别在根、packages/core、packages/docs,内容逐字相同):

项值默认
semifalse加分号
singleQuotetrue双引号
printWidth14080
trailingComma'none'视工具而定
sortImports.newlinesBetweentrue—
oxfmt ignorePatterns**/*.html、**/*.mdx—
oxlint categories.correctness'warn'(降级)error
oxlint 唯一 error 级规则no-unused-vars(^_ 前缀豁免)—

仓库里的 .md 文件不交给 oxfmt —— 现存的 README、设计文档在 oxfmt 眼里全是「未格式化」的。 写 Markdown 时不要为了让 oxfmt --check 变绿去格式化它们。

注释语言

代码注释与 JSDoc 一律中文,且倾向于写「为什么」而不是「做什么」:解释取舍、记录踩过的坑、指出容易误解之处。这不是风格偏好 —— 这个仓库的很多约束(依赖方向、快照纪律、装配点)只有靠注释才能传递。写新代码时保持这个密度。

生成物 vs 提交物

路径状态由什么重新生成
packages/core/dist/gitignorepnpm build:core
packages/docs/.source/gitignorefumadocs-mdx(docs 的 postinstall)
packages/docs/content/docs/v7/usage/api/http/gitignorepnpm docs:api(80 个端点页 + 索引)
packages/docs/content/docs/v7/usage/api/sdk/gitignorepnpm docs:api(4 个平台页)
packages/docs/content/docs/v7/usage/api/types/gitignorepnpm docs:api(8 页,TypeDoc 抽 JSON 后的渲染产物)
corpus/(仓库根,不在 packages/core/ 下)gitignore(三类例外见下一行)控制台录制(pnpm console)或 pnpm record:corpus。响应体(*.responses.json)是运行时内容,只留本地
corpus/seeds.json、corpus/**/*.doc.json 与 corpus/**/*.requests.json提交手写的根种子与注释 sidecar,加上请求集合(只含公开作品 / 账号 / 关键词的真值,凭证走 .env,别人拿它就能重放)—— 这是 corpus/**/*.json 那条 gitignore 刻意留的三个例外。当前 66 份、54 KB,覆盖 62 个端点(B站 23、抖音 22、快手 4、小红书 13)
packages/response-types/src/generated/提交pnpm gen:types —— 两层 barrel(根与 <平台>/index.ts)只有全量生成会写,控制台的单端点「就地生成」按设计不碰它们。当前 155 个文件,覆盖 65 个端点(B站 23、抖音 25、小红书 13、快手 4)
packages/core/openapi.json提交pnpm openapi,CI 用 --check 逐字节守住
packages/core/CHANGELOG.md提交已冻结 —— release-please 时代的产物;新 changelog 由 changelogithub 直接写成 GitHub Release,不落文件
13 个 *.snap提交vitest(不要随手 -u)

有两处生成物要你手动同步:改了端点声明重跑 pnpm openapi,录了新样本重跑 pnpm gen:types,两次都得把产物一起提交。漏跑会不会红,两边的答案不一样:openapi.json CI 逐字节守得住;生成的响应类型只在树不自洽时红(pnpm types:check 零样本时验「barrel 与产物树一致」,2026-09-10 补),而「产物与手上样本一致」在 CI 上没有证据,只有本地跑才验得了。其余提交物(*.snap)由 vitest 自己写回,不用手动重跑。

codemod

packages/codemod 是 v6 → v7 的消费方代码改写工具,仓库内私有、不发布。纯文本启发式改写,不解析 AST。

九条规则:

规则做什么
typeMode-strict删掉 typeMode: 'strict'(已是默认)
typeMode-loose删掉 typeMode: 'loose' 并注入 TODO
error-amagiErrorr.error.amagiError.errorDescription → r.error.message
error-errorDescriptionr.error.errorDescription → r.error.message
register-routesregisterXxxRoutes( → createXxxRoutes(
api-routes-import从 import 里删 XxxApiRoutes 并注入 TODO
r-code-read检测 .code 读法,只标不改
validation-catchcatch 块里出现 ZodError / .issues 时注入 TODO
events-payload-meta实例总线监听器的负载读法进 meta(d.platform → d.meta.platform 等);顶层 amagiEvents 一字不动

调用方式:

pnpm exec tsx packages/codemod/src/cli.ts v6-to-v7 <目录或文件>

改动就地写回,跑之前先备份或先对副本运行。它是幂等的 —— 已有的 // TODO(amagi-v7): 行在跑规则前会被换成哨符、跑完原位放回,所以连跑两次不会翻倍。

最后更新于 2026-10-23

On this page