当前浏览的是 v7 预览版文档(v7 尚未正式发布,API 可能随版本调整)。查看对应的 v6 正式版文档
@ikenxuan/amagi

新增接口

v7 中新增一个平台接口只需一份端点声明

新增接口

v7 起,给平台加一个数据接口只剩一个实质步骤:在 endpoints/ 下新建一份端点声明(用该平台的 defineXxxEndpoint,如抖音 defineDouyinEndpoint)。参数类型、运行时校验、请求构造、签名、HTTP 路由、fetcher 方法、返回类型全部由声明派生 —— 不再有 v6 那种「改 8 步、动 11–15 个文件」的流程。

实现之前建议先读两处真实代码,建立直观认识:

  • 端点声明模板:packages/core/src/platforms/douyin/endpoints/videoWork.ts
  • 注册表汇总:packages/core/src/platforms/douyin/endpoints/index.ts

第 1 步:新建端点声明文件

在 packages/core/src/platforms/<平台>/endpoints/ 下新建 <短名>.ts,用该平台的 defineXxxEndpoint 声明端点 —— 它和 type 都从同目录 ./define 里取。抖音 videoWork 的真实声明长这样:

packages/core/src/platforms/douyin/endpoints/videoWork.ts
import zod from 'zod'

import type { RequestSpec } from '../../../contracts/request'
import type { DouyinVideoWorkResponse } from '../../../types/generated'
import { createDouyinApiUrls } from '../api'
import { withDouyinReferer } from '../referer'
import { douyinBogus } from '../sign/steps'
import { DOUYIN_RISK_RETRY, defineDouyinEndpoint, type } from './define'

/**
 * 视频作品详情(新路径 `/fetch_video_work`,避免与其他作品类型共用路由)。
 *
 * 行为与旧版一致:`getWorkDetail` GET + a_bogus 签名。
 */
export const videoWork = defineDouyinEndpoint({
  name: 'douyin.videoWork',
  route: '/fetch_video_work',
  doc: {
    summary: '视频作品详细信息',
    description: '已知是视频作品时用这条;形态不确定时先用 `parseWork` 自动识别。'
  },
  params: zod.object({
    aweme_id: zod.string().min(1, { error: '作品ID不能为空' }).describe('作品 ID')
  }),
  build: (p, ctx): RequestSpec => ({
    method: 'GET',
    url: createDouyinApiUrls(ctx.userAgent).getWorkDetail(p),
    headers: withDouyinReferer(ctx, { kind: 'video', awemeId: p.aweme_id })
  }),
  sign: douyinBogus(184),
  retryOn: DOUYIN_RISK_RETRY,
  retryFresh: true,
  response: type<DouyinVideoWorkResponse>(),
  // 预留:跨平台语义视图接入时在此填 (raw) => CanonicalWork
  toCanonical: undefined
})

为什么是 defineDouyinEndpoint 而不是通用 defineEndpoint? 平台专属的这层只做一件事:把 sign 的字符串分支从宽 string 收窄到本平台签名器表推导出的名字联合 —— 抖音是 'a-bogus' | 'x-bogus'、B站 'wbi' | 'qtparam'、快手 'hxfalcon'、小红书是 xhs-* 那七个。 名字写错(如 'a-bogas')编译期即报错,不必等运行时查表抛 未注册的签名器。运行时它仍是恒等 函数,其余行为与 defineEndpoint 完全一致 —— 但直接 import 通用 defineEndpoint 会绕过这道收窄, 所以端点文件从同目录 ./define 一次性取 defineXxxEndpoint 与 type。

换成你的接口时,逐个替换这些字段(name / route / doc / params 四个是必填,缺一个编译或 CI 就会拦下来):

声明字段做什么取值来源
name端点全名 <平台>.<短名>该平台已有声明的命名习惯
routeHTTP 路由路径,同平台内唯一v6 的路径风格(snake_case)保持不变
doc文档元数据:OpenAPI 的 summary / description 的唯一出处summary 必写:中文名词短语、不带句号、≤40 字(如 '视频作品详细信息');一句话讲不完的约束才补可选的 description。不要写 tags —— 声明里根本没这个键,tag 由生成器从 name 的平台段派生
paramszod schema参数类型和校验的唯一出处,不再手写第二遍
build按参数构造请求描述URL 构造放在同目录 api.ts 的纯函数里
sign签名声明平台签名器表里的名字(defineXxxEndpoint 已把它收窄到本平台签名器名联合,写错名字编译期即报错);不签名的接口写 false

还有一处登记:在 packages/core/test/types/response-mapping.test-d.ts 里加一行 —— 补样本 + pnpm gen:types 之后,packages/core/test/contracts/response-source.test.ts 会红,并直接告诉你这一行该换成什么。

doc 在类型上是可选的(已有端点一个不改也能编译),但新增端点必写:packages/core/test/contracts/endpoint-doc.test.ts 拿四个注册表逐个断言 doc.summary 非空、≤40 字、不以句号结尾、同平台内不重复 —— 漏一个 CI 直接红。

响应类型:在 Web 控制台录一份样本

历史做法是「抓一次响应 → 贴进第三方 JSON→TS 在线工具 → 把结果拷回仓库 → 把 JSON 扔掉」。转换那一步本来就只占十分钟,真正贵的是「重新取得证据」和「确认没漏变体」—— 而证据每次都被扔掉了。后果是:改一个字段要重新抓一遍;想知道某个字段为什么是可选的,没人答得上来;发现新变体全靠碰巧遇到。

现在这件事有工具:本地 Web 控制台(packages/web,Vite + React + Tailwind + HeroUI)。一条命令起两侧:

pnpm console   # Node 侧(默认只监听 127.0.0.1:7345)+ 浏览器侧(Vite 把 /api/* 代理过去)

要分两个终端跑就 pnpm console:server 与 pnpm console:web —— 两个都得起,只起浏览器侧时每个请求都会失败。

cookie 从环境变量读(AMAGI_COOKIE_DOUYIN / AMAGI_COOKIE_BILIBILI / AMAGI_COOKIE_KUAISHOU / AMAGI_COOKIE_XIAOHONGSHU),页面上一个字都不回显。

界面上一趟走完:左栏搜到你的新端点 → 填参(表单由端点的 zod schema 派生,一个都不手写)→ 点「发送」,或者「批量 N 组」按参数矩阵录一轮 → 同屏看响应体与它的类型声明(三栏并排:请求 / 响应 / 类型;「类型」栏还有「已提交」「diff」「对比」三页)→ 「留下 / 丢掉」 → 「生成类型」。产物落在 packages/response-types/src/generated/(独立包,core 当 devDep 装、透给下游),全量重新生成仍然是 pnpm gen:types;生成完重建一次那个包(pnpm --filter @ikenxuan/amagi-response-types build),core 的类型解析指到它的 dist/。

中间那个「人看一眼」不是形式。 录制与入库分开两步,因为「这份该不该留」无法自动判断:错误页与风控页都是合法 JSON,混进去会让业务字段全变成可选可 null,而没有任何测试会红;两份同形样本对类型的贡献又是零 —— 那两份 B站 comments 只差一个 number 参数,2.57 MB 换来零收益。

样本留在本地,不进 git(.gitignore 掉了 corpus/**/*.json,例外只有人写的 seeds.json 与 *.doc.json)。它是重新生成类型的输入,不是可提交的证据 —— 三条理由、代价,以及产物文件头那个溯源块(几份样本、参数哈希、录制日期、amagi 版本) 见贡献指南的「响应样本留在本地,不进 git」一节。

脱敏由 packages/typegen 的 scrubSample 自动做,但脱敏清单要人看:suspects 是「像密钥但没有规则匹配上」的路径。批量录制 pnpm record:corpus 还在,降级成辅助 —— 它录出来的东西还是得一份一份在控制台里过。命令全貌见贡献指南的「corpus 工作流」。

样本目前只覆盖 62 / 80 个端点,而且大多只有一份:不透明 ID(aweme_id / sec_uid / bvid)没法生成、只能来自 corpus/seeds.json 或依赖图,而多数平台的种子还没填。 所以新增端点时 response 槽位照旧写手写类型令牌(上面那张表的最后一行)—— 生成树与手写树是并存的两棵,前缀刻意不同名(生成 BilibiliEmojiList_V0 / 手写 BiliEmojiList),调用处一眼可辨读的是哪棵。

录样本不是替代手写类型,是让下一次改这个类型的人有据可查。

一份端点声明的字段大致按「调一次接口」的流程往下写(①–⑧ 对应项目架构的管线图)—— 除必填四项外都可选、按需添:

必填骨架

name / route / params / doc(见上表),其余全部可选。

② prepare(可选)

发请求前换游客 cookie、取密钥;多数端点不写。

③ build → ④ sign

build 按参数拼请求(返回数组 = 多个请求并发);sign 写签名器名 / false(不签)/ 一次性函数。

⑥ decode → ⑦ judge(可选)

⑤ 是框架发请求。非 JSON 响应(protobuf / 多段 JSON / HTML)在 decode 解;judge 覆盖平台默认判定。

⑧ 收尾整形(可选,三选一)

normalize(通用兜底)/ aggregate(同构分段并发)/ paginate.merge(翻页),最多写一个 —— aggregate 与 normalize 同时写会编译报错。

response

响应类型令牌 type<T>(),data 的类型只从这里来。

完整字段清单、每个槽位的实际使用量与设计取舍,见项目架构的「端点声明」一节与端点注册表。

第 2 步:登记进 registry

在平台注册表汇总文件(endpoints/index.ts)里把新声明登记进去:

packages/core/src/platforms/douyin/endpoints/index.ts
export const douyinRegistry = {
  parseWork,
  videoWork,
  imageAlbumWork,
  slidesWork,
  textWork,
  comments,
  commentReplies,
  userProfile,
  userVideoList,
  userFavoriteList,
  userRecommendList,
  search,
  suggestWords,
  shortUrl,
  musicInfo,
  musicAwemeList,
  mixAweme,
  liveRoomInfo,
  loginQrcode,
  emojiList,
  dynamicEmojiList,
  danmakuList,
  guestUserInfo,
  guestMusicInfo,
  guestMusicAwemeList,
  emojiResourceMeta
} as const satisfies Registry

登记后还有三件机械事:

  1. 把声明与响应类型加进该文件尾部的 re-export 列表(照抄现有行)。

  2. 去 client/method-names.ts 加一条映射。 这是必需的,不只对不规则名 —— 见下面「方法名」那条。

  3. 同步被锁死的计数判据。 当前总数是 80(抖音 26、B站 27、快手 14、小红书 13),它在这些地方被写死,逐一改到位:

    文件要改什么
    test/contracts/endpoint-doc.test.ts逐平台数 + 合计 80
    test/client/method-names.test.ts映射表条数、逐平台数,以及「不规则 N 条 / 其余 N 条是规则映射」两条 —— 你的新方法名落在哪一边决定改哪个
    test/openapi/spec.test.tspaths 条数,以及「N 个端点带响应类型」
    test/server/openapi-route.test.tspaths 条数(三处)
    test/platforms/<平台>/endpoints.test.ts该平台 registry 的端点数 + 路由清单
    test/types/response-mapping.test-d.ts该平台的 describe 标题与新增一行类型断言
    test/contract/__snapshots__/fetcher-surface.test.ts.snap四组方法名快照,跑 vitest -u 更新
    test/contracts/endpoint-paginate.test.ts声明了 paginate 才要改 —— 分页端点总数(当前 15)
    test/openapi/response-schemas.test.ts新端点的响应类型进了生成树才要改 —— it.each 的抽样验收清单

    文档站里也写着这些数字:content/docs/v7/dev/internals/registry.mdx(规模、派生物表、可选槽位使用量)与本页。

这一步之后:全部自动派生

登记完成即接口可用,以下东西不需要你维护:

  • fetcher 方法。 client.douyin.fetcher.fetchXxx 与静态 douyinFetcher.fetchXxx 都从 registry 派生(Proxy 懒加载,方法集合自动跟随注册表),参数与返回类型由声明推导,自动走 v7 信封。
  • 校验。 参数 schema 即校验,校验失败返回 success: false 信封(不再 throw)。
  • HTTP 路由。 route 由服务端自动注册成 /api/<平台>/<route>,不再手写路由文件。
  • 返回类型。 response / normalize / compute 推导出精确类型;声明过的字段有补全,平台新加的字段读出来不算编译错误。
  • 文档与测试清单。 方法与路由清单由注册表推出,不再有手写的第二份事实。

方法名:规则名也要登记

fetcher 方法名默认按「fetch + 端点短名首字母大写」派生,只有 16 个不规则名(parseWork、searchContent、convertAvToBv 等)需要那张映射表才能拼出来。

但每个新端点都必须在 client/method-names.ts 里登记一行,规则名也一样。兜底规则救不了你,因为有两处会红:

  • packages/docs/scripts/generate-docs.ts 在映射表里查不到短名时直接抛,于是 pnpm build:docs 失败 —— 而它是 CI 必需检查;
  • test/client/method-names.test.ts 把映射表的方法名集合与活 fetcher 的键逐条比对,活 fetcher 多一个方法即失败。

常见形态的写法

你想做的事写法
无参接口params: zod.object({}),fetcher 方法可以省略 options(见抖音 emojiList)
数字参数(可传字符串)zod.coerce.number()(快手的 count 就是这么强转的)
必填与文案zod.string().min(1, { error: '中文提示' }),文案直接写在 schema 上
声明式翻页paginate 槽位声明 maxPageSize / items / hasMore / nextParams / merge(收尾),一页与最终返回不同形时加 page 令牌覆盖(见 userFavoriteList / 快手 userWorkList)
签名的接口sign 给原子反爬参数清单,如 [msToken(184), aBogus(), secsdk()],按 phase 排序执行(见抖音 videoWork)
不签名的接口sign: false(见抖音 guestUserInfo / emojiResourceMeta)

验证

pnpm typecheck     # 类型检查(含文档站的三道静态门)
pnpm lint          # 代码检查
pnpm format:check  # 格式检查(oxfmt 只读版)
pnpm test          # vitest:端点用例 + fetcher 表面快照 + registry 结构锁 + doc.summary 钉子
pnpm openapi       # 重新生成 openapi.json(**必须提交**)
pnpm openapi:check # 已提交的 openapi.json 与四个 registry 逐字节一致
pnpm deps:check    # dpdm:必须报 0 环
pnpm build:docs    # 文档站整站构建,含 80 页端点文档的重新生成

这份清单之外的 openapi:check 与 build:docs 也是 CI 的必需检查 —— 只跑前四条发现不了「忘了重新生成 openapi.json」和「忘了登记方法名」。

用你的新接口跑一次端到端:client.<platform>.fetcher.fetchXxx(...) 成功返回信封后,任务就完成了。

想知道派生链路的全貌

这一页只讲「怎么做」。声明里每个字段被谁读取、派生出哪些产物、注册表如何到达 client 与 server,见端点注册表。

最后更新于 2026-10-23

On this page