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

从 v6 迁移

v6 → v7 的四处必改、一行切回 v6 语义的 /compat,以及完整迁移矩阵

从 v6 迁移

成功与失败仍用 r.success 区分。 v7 需要调整错误分支、事件负载的读法, 并删除 typeMode。需要保留旧错误信封时,可使用 @ikenxuan/amagi/compat。 上游接口返回字段的变化仍需适配,见下方的小红书用户资料说明。

下面按「四处必改」逐条对照,完整矩阵在页尾。

一句话总结

成功分支的代码零改动,错误分支需要改读法。 除此之外的破坏性变更都能靠 编译错误发现,且有兼容层兜底。

// 这段代码 v6 / v7 都成立
const r = await client.douyin.fetcher.fetchVideoWork({ aweme_id })
if (r.success) console.log(r.data)

v7 接口补齐的兼容说明

本次增加抖音 fetchMixAweme,以及小红书 searchUser、fetchSearchHotList、fetchSubComments、fetchLiveRoom、fetchLiveStatus、fetchLiveSquare。裸 Fetcher、绑定 Fetcher 与 HTTP 路由均从同一注册表派生;旧方法保留。小红书用户搜索的公开名称为 searchUser,不是旧补丁中可能出现的 fetchSearchUser。

小红书 fetchUserProfile 改为 JSON 用户资料,平台原始响应的字段为 data.basic_info、data.interactions、data.tab_public。SDK 成功信封的 result.data 才是这份平台响应,因此应从 result.data.data 读取资料,并检查可选字段;原来的主页 HTML 初始状态路径不再适用。fetchUserNoteList 补全响应类型,主页 xsec_token 仍为必填。fetchSubComments 使用 number 自动分页;笔记搜索支持排序与类型筛选,手动翻页时复用搜索会话标识。详细参数见小红书 SDK 参考。

默认 JSON 解析会将超出安全整数范围的裸整数转为字符串,普通安全整数仍是数字,因此响应 ID 的类型可能为 string | number。大整数 ID 应按字符串传递,不要先用 Number() 转换。显式文本响应和自定义 transformResponse 由调用方控制;缺少完整响应样本的字段保留为 unknown,使用前应检查其结构。

小红书已收录端点在未提供 Cookie 时可自动初始化游客身份,并按客户端和请求配置缓存五分钟、合并并发初始化。显式传入 web_session 或 userId 却缺少 a1 时,会返回 auth / COOKIE_MISSING,请补充同一会话的完整 Cookie。

抖音新增 credentials 工具命名空间及对应顶层导出,用于显式处理 Cookie、探测登录态和申请游客凭据。实例上的该命名空间仍需显式传入 Cookie 与请求配置。现有 login 扫码会话入口不变,详见工具集。

四处必改:前后对照

下面每一处都是「v6 写法 + 它在 v7 下报的真实编译错误」对上「v7 写法」—— 「前」的报错是真实编译诊断,可以直接按错误码搜自己的代码。

一、默认导入的门面换成 v7 门面

import amagi from '@ikenxuan/amagi' 之后 amagi(options) 返回的是 v7 门面了 (createAmagiClient 变成它的 @deprecated 别名,而且是同一个函数对象)。 导出名一个没变、client 顶层的 8 个键一个没变,变的是 events / on / once 这条线:从 v6 的全局单例换成实例级总线,负载里的平铺字段全部搬进 meta。

TS 用户在这里是编译错误(B 档),JS 用户读到 undefined(A 档,没人拦):

.('api:success', () => {
  .(.platform, .methodType, .duration)
Property 'duration' does not exist on type 'ApiSuccessEvent'.
Property 'methodType' does not exist on type 'ApiSuccessEvent'.
Property 'platform' does not exist on type 'ApiSuccessEvent'.
})

v7 的读法 —— 三个字段各有归宿,另外 meta 带 requestId / clientId, 多实例并发时事件终于能归因(v6 的负载里没有任何关联 id):

.('api:success', () => {
  // methodType → endpoint,值也变了:videoWork → douyin.videoWork
  .(.., .., ..)
  .(.., ..)
})

换门面顺带带来一处纯新增:四个平台的实例上都有 login 命名空间(扫码登录会话), B站另有 refreshCredential(Cookie 续期):

const session = ...()
const session: LoginSession
...() ...() ...()

保留但形状变化:默认导出的 client 换成 v7 门面(B 档)

import amagi from '@ikenxuan/amagi' 之后 amagi(options) 拿到的是 v7 门面: createAmagiClient 变成 createClient 的 @deprecated 别名,而且是同一个函数对象。

导出名一个都没变,变的是返回值的形状:

返回值上的东西v6 门面v7 门面破坏性
顶层键startServer events on once + 四个平台一字不变(同样 8 个键)无
四个平台{ ...utils, fetcher }多一个 login(qrcode() / resume()),B站另多一个 refreshCredential;少一个键,见下行新增无破坏;减少见下行
四个平台上的 v6 URL 构造器client.<平台>.<平台>ApiUrls(摊在 utils 里的 v6 那份)撤下:v7 门面只摊 v7 那份(client.<平台>.apiUrls)。v6 那份仍可达:包顶层 import { douyinApiUrls } from '@ikenxuan/amagi' 照旧,@ikenxuan/amagi/compat 的 client.<平台>.<平台>ApiUrls 以原名保留B 档(TS 编译错误)/ A 档(JS 读到 undefined)
events / on / once全局单例 amagiEvents(12 个名字、v6 负载)实例级 EventBus(15 个名字、负载带 meta)B 档(TS 编译错误)/ A 档(JS 读到 undefined)
startServer(port, opts)第二参 { openapi? }第二参 FacadeServerOptions({ openapi?, listen? })无(放宽;openapi 行为一字不变)
startServer 的 log:mark发到全局单例,文案带 chalk 颜色发到实例总线,文案不带颜色A 档,见下方
入参Options({ cookies?, request? })ClientOptions(多一个 debug?)无(放宽:Options 与 ClientOptions 赋值互通,v6 调用点零改动)

入参这条顺带补上一个可达性缺口:ClientOptions.debug 此前只能经具名的 createClient 传,默认导入的人够不到。现在 amagi({ debug: true }) 直接可用。 Options 本身形状冻结不动,仍在顶层导出面上。

log:mark 是这次唯一的 A 档静默变化,单独记一笔。 v6 靠 amagiEvents.on('log:mark', …) 感知「服务起来了」的代码,换门面之后收不到了 —— 那条事件现在发在 client.events 上。迁移动作:把监听器挂到 client.events 上, 或者 startServer 之后自己打一行日志。

compat 入口(@ikenxuan/amagi/compat)跟着一起走:compatCreateAmagiClient 包的 仍是 createAmagiClient(也就是 createClient),信封回填与「校验失败恢复抛出」 一字未改,但它的 client.events 也跟着变成实例总线 —— compat 的职责是信封与 抛出语义,不含冻结整个 v6 门面;而全局单例上早已没有 api:* 的 emit 点 (只剩 @deprecated 的抖音 passport),继续指向它等于把一条死总线交给使用者。

二、信封读法:顶层 code 与 error.amagiError 都没了

AmagiResult<T> 是判别联合,success 是唯一判别键。v6 那套 「顶层 code + error.amagiError.errorDescription」两个键在 v7 都不存在 —— HTTP 状态在 error.http.status,平台业务码在 error.platform.code, amagi 自己的错误码在 error.code:

if (!.) {
  .(.code, ..amagiError.errorDescription)
Property 'amagiError' does not exist on type 'AmagiError'.
Property 'code' does not exist on type 'AmagiFailure'.
}

v7 的读法:

if (.) {
  . // 收窄后是具体类型,不带 | undefined
} else {
  // kind 是 12 个大类之一,platform?.code 才是平台原文码
  .(.., .., .., ..?.)
}

不想写 if 也可以:v7 的两支各带一个 ?: undefined 对侧键,所以不收窄直接读 r.data 是合法的(这就是 v6 的读法),代价是类型带 | undefined,访问其属性前需先处理:

const  = await ...({ : '7419965435527974176' })

r.data.
'r.data' is possibly 'undefined'.

数组回调里没有 if 可用,用顶层新增的 isSuccess / isFailure / unwrap; 四种读法的完整对比在响应类型。

信封永不 reject。 参数校验失败在 v6 会 throw,v7 映射为 success: false —— v6 代码里的 try/catch 在 v7 下不进 catch,而是静默走到 else 分支。 这是最容易踩的一条,需要人判断怎么处理失败分支。

放宽(1 条,非破坏):信封两支各加一个 ?: undefined 对侧键

这个变更的方向是编译错误变合法,反向不成立 —— 原先能编译的代码照旧编译, 所以它不构成破坏性变更,但形状确实动了。

名字之前之后破坏性
AmagiSuccess<T>success / data / message / meta多一个 error?: undefined无(放宽:r.error 从 TS2339 变成 AmagiError | undefined)
AmagiFailuresuccess / error / message / meta多一个 data?: undefined无(放宽:r.data 从 TS2339 变成 T | undefined,即 v6 的读法)
isSuccess / isFailure不存在顶层新增,类型守卫无(纯新增)
unwrap / AmagiThrownError不存在顶层新增:失败即抛 + 抛出物的 Error 子类无(纯新增)
AmagiResult / AmagiSuccess / AmagiFailure / AmagiError顶层取不到顶层类型导出无(纯新增,不进运行时公开面)

运行时形状一个字节都没变,这是与 v6 的分界线。v6 的错在说谎: SuccessResult / ErrorResult 两支都声明 data(一支 T、一支 never), 声明成 never 却在运行时塞真值。v7 的对侧键运行时根本不存在,声明与 事实一致。

收窄能力没退化:success 仍是唯一判别键,if (r.success) 之后 data 是 T、 else 之后 error 是 AmagiError,都不带 | undefined。四种读法 (不收窄读 data / if 收窄 / filter(isSuccess) / unwrap)全部成立。

unwrap 与 @ikenxuan/amagi/compat 不重叠:unwrap 是 v7 的单点显式选择 (这一处调用想让失败抛就抛,返回类型是 T,抛的是带 AmagiError 全字段、 cause 不吞的 AmagiThrownError);compat 是 v6 语义的整体回填(把返回值 换回带顶层 code 的 v6 信封)。要哪种语义选哪个,混用没有意义。

{/* 本小节未被任何页面 include,是贡献者向记录。 */}

需要人判断(C 档)

只有两条,都提供兼容层:

① 参数校验失败不再抛出

// v6:校验失败走 catch
try {
  await client.douyin.fetcher.fetchVideoWork({ aweme_id: '' })
} catch (e) {
  console.error(e.message) // '抖音数据获取失败: ...'
}

// v7:校验失败走 else 分支,且带字段路径
const r = await client.douyin.fetcher.fetchVideoWork({ aweme_id: '' })
if (!r.success && r.error.kind === 'validation') {
  for (const i of r.error.issues!) console.error(i.path, i.message)
}

这是最容易踩的一条,因为 v6 代码里的 try/catch 在 v7 下不会进 catch, 而是静默走到 if (r.success) 的 else 分支 —— 如果那里没写处理就变成静默失败。 所以它属于 C 档且必须有兼容层。

② 小红书风控 HTML 不再当成功

v6 拿到 <html> 反爬页时 return response(成功透出); v7 判为 kind: 'risk' / code: 'ANTIBOT_PAGE',原始 HTML 在 error.raw。


{/* 本小节未被任何页面 include,是贡献者向记录。 */}

三、typeMode 已删

v6 的 typeMode: 'strict' | 'loose' 是「写了两万六千行类型、默认却返回 any」的 中间态。v7 只有一种类型模式,响应类型始终是端点声明里的具体类型, 参数因此多余 —— 传了是编译错误(对象字面量的多余属性检查):

const  = await ...({
  : '7419965435527974176',
  typeMode: 'strict'
Object literal may only specify known properties, and 'typeMode' does not exist in type 'PublicParamsOf<EndpointDef<ZodObject<{ aweme_id: ZodString; }, $strip>, VideoWork$1, "a-bogus" | "x-bogus", VideoWork$1, unknown>>'.
})

删掉参数即可,类型本来就是具体的:

const  = await ...({ : '7419965435527974176' })

if (.) {
  .data
AmagiSuccess.data: VideoWork$1

端点声明的返回类型

}

TypeMode / TypeControl / ConditionalReturnType / ExtractTypeMode 四个类型 一并删除(它们只在子路径导出,不在顶层)。原来靠 typeMode: 'loose' 拿 any 的地方改用三条逃生舱,见响应类型。

四、/compat:一行切回 v6 语义

不想现在改的,把 import 换成 @ikenxuan/amagi/compat —— 只改这一行, 信封回填成带顶层 code 的 v6 形状,参数校验失败恢复抛出。同一段 v6 代码, 默认入口下报两个 2339:

const  = ({ : { : '' } })

try {
  const  = await ...({ : '' })
  if (.) .(.)
  else .(.code, ..amagiError.errorDescription)
Property 'amagiError' does not exist on type 'AmagiError'.
Property 'code' does not exist on type 'AmagiFailure'.
} catch () { .('校验失败仍走这里', ) }

换成 compat 入口之后,函数体一个字符没动,编译通过:

const  = ({ : { : '' } })

try {
  const  = await ...({ : '' })
  if (.) .(.)
  else .(., ...)
} catch () {
  .('校验失败仍走这里', )
}

compat 覆盖的是信封与抛出语义,不含冻结整个 v6 门面:它的 client.events 跟着换成了实例总线,所以上面第一条(事件负载读法)在 compat 下也要改。 生命周期是 v7 全程保留、v8 移除。

兼容层:@ikenxuan/amagi/compat

一行切换,把 v7 信封回填成 v6 形状并恢复抛出行为:

// v6 写法完全不用改
import amagi from '@ikenxuan/amagi/compat'

const client = amagi({ cookies: { douyin: ck } })
try {
  const r = await client.douyin.fetcher.fetchVideoWork({ aweme_id: '' })
  if (r.success) use(r.data)
  else console.error(r.code, r.message, r.error.amagiError.errorDescription)
} catch (e) {
  /* 校验失败仍走这里 */
}

实现方式是一层薄包装,不复制业务逻辑。生命周期:v7 全程保留,v8 移除。 compat 入口在导入时打一次 log:warn 事件提示迁移,不刷屏。

兼容层刻意做成单向、只读、无状态的转换函数集合 —— 它不参与请求流程,只在返回前套一层。这样它不会成为第二套实现, 也不会因为忘了同步而与主路径漂移。


{/* 本小节未被任何页面 include,是贡献者向记录。 */}

迁移矩阵

四种调用形态全部保留

形态v6v7
client 实例amagi({cookies,request}).douyin.fetcher.fetchVideoWork(o)不变
静态 fetcheramagi.douyinFetcher.fetchVideoWork(o, ck, cfg)不变
工厂amagi.createBoundDouyinFetcher(ck).fetchVideoWork(o)不变
HTTP / 事件 / 工具集client.startServer(4567) / amagi.on(...) / amagi.douyin.sign不变

新增(不影响旧写法):client.call('douyin.videoWork', params)、 client.endpoints(platform)、client.<platform>.login。


新增 HTTP 路径

v6 抖音 5 个作品 methodType(parseWork / videoWork / imageAlbumWork / slidesWork / textWork)共用 /fetch_one_work 一条路由。 v7 拆成 5 条独立路由,新增以下 4 条:

新路径端点说明
/fetch_video_workdouyin.videoWork原 videoWork
/fetch_image_album_workdouyin.imageAlbumWork原 imageAlbumWork
/fetch_slides_workdouyin.slidesWork原 slidesWork
/fetch_text_workdouyin.textWork原 textWork

parseWork 保留原路径 /fetch_one_work,因此旧 URL 依然可用;其余四个 端点原来打的 /fetch_one_work 需要换成上表的新路径。同平台内路由唯一, 重复注册会在启动期直接抛错。


保留且形状不变(59 个)

类别导出
入口default amagi CreateApp createAmagiClient(名字全保留;返回的 client 形状变了,见下方「默认导出的 client 换成 v7 门面」)
静态 fetcherdouyinFetcher bilibiliFetcher kuaishouFetcher xiaohongshuFetcher
bound fetcher 工厂createBound{Douyin,Bilibili,Kuaishou,Xiaohongshu}Fetcher
平台工具集douyinUtils bilibiliUtils kuaishouUtils xiaohongshuUtils
路由工厂create{Douyin,Bilibili,Kuaishou,Xiaohongshu}Routes
签名douyinSign kuaishouSign xiaohongshuSign wbi_sign qtparam av2bv bv2av parseDmSegMobileReply
URL 构造器{douyin,bilibili,kuaishou,xiaohongshu}ApiUrls(顶层导出本身一字不变;但不再摊在 client.<平台> 与静态工具集上,见「默认导出的 client 换成 v7 门面」一节)
passportdouyinPassport requestPassportQrcode checkPassportQrcode sendPassportVerifyCode validatePassportVerifyCode
事件amagiEvents 与 12 个 emit*
传输fetchData fetchResponse
错误ApiError ValidationError handleError
平台常量类型CommentType DynamicType MajorType AdditionalType

注意:单个 fetcher 方法(fetchVideoInfo 等 80 个)不在顶层导出里 —— 它们只能通过 fetcher 对象访问(amagi.douyinFetcher.fetchVideoWork)。 唯一例外是 4 个 passport 方法,v6 在入口里显式具名导出了它们;v7 沿用这 四个导出以免破坏。

全局单例 amagiEvents 与 12 个 emit* 自由函数一个都没删、形状一字未改。 但 v7 的 client.events 是另一条总线(实例级),名字对齐、负载换形, 逐条见下方「事件系统」一节。

保留但形状变化(8 个,B 档)

导出v6v7迁移动作
validateDouyinParams validateBilibiliParams validateKuaishouParams validateXiaohongshuParams抛 ZodError返回 ValidateOutcome,不抛想保留抛出行为用 assertValidParams
createSuccessResponse(data, message, code)(data, meta, message?)主要内部使用
createErrorResponse(error, message, code, data)(error: AmagiError, meta)同上
isNetworkErrorResult判 {success:false, error.amagiError}改判 r.error.kind === 'network'保留同名 deprecated 转发
getHeadersAndData顶层导出,业务层直接用移入 transport,不再对外需要响应头改用 meta.trace

以及类型层面的 Result / SuccessResult / ErrorResult —— 顶层 code 删除、error 形状重做。

事件系统:实例总线的 12 个事件名与负载形状(A 档 / B 档)

两条总线并存,别当成一条:

总线怎么拿事件名负载生命周期
v6 全局单例amagiEvents、amagi.on(...)(静态)12 个(AmagiEventType)v6 *EventData,带 timestamp: Datev7 全程保留、形状一字不改,v8 移除
v7 实例总线client.events、client.on(...)同样 12 个(AmagiBusEventMap)v7 形状:带 meta / trace,没有 timestampv7 起就是这一条

实例总线的 12 个事件名都能 on,其中 10 个真的会发:log:info / log:debug 两个名字可以监听,但 v7 核心链路不产生它们(v6 唯一那处 info 日志在 v7 是 分页的正常终止条件,debug 日志则由 partial: 'tolerate' + meta.trace 取代)。

负载形状:监听器搬家要改读法

事件名v6 负载(全局单例,不变)v7 实例总线负载
log:* ×5{ level, message, args?, timestamp }{ level, message, args?, meta? }
http:request http:response{ method, url, statusCode, responseTime, clientIP?, requestSize?, responseSize?, timestamp }{ meta, trace }
http:errorNetworkErrorEventData{ meta, trace, status }
network:retry{ errorCode, attempt, maxRetries, delayMs, url?, timestamp }{ meta, trace, code, errno?, status?, attempt, maxRetries, delayMs }
network:error{ errorCode, message, retries, url?, timestamp }{ meta, trace, code, errno?, message, attempts }
api:success{ platform, methodType, response, statusCode, duration, timestamp }{ meta, data }
api:error{ platform, methodType, errorCode?, errorMessage, url?, duration?, timestamp }{ meta, error }

三处容易踩的差别:

  1. platform / methodType / duration / statusCode 都进了 meta (meta.platform / meta.endpoint(全名,如 douyin.videoWork)/ meta.durationMs / trace[].status),顶层读不到了。
  2. timestamp: Date 没有了。 归因改用 meta.requestId / meta.clientId —— v6 的负载里没有任何关联 id,多实例并发时分不清事件是谁发的。 与调用无关的日志(log:mark)本来就没有 meta。
  3. network:* 的 errorCode 拆成两个字段。 v6 的 errorCode 装的是传输层 errno(ECONNRESET),v7 分成 code(AmagiErrorCode,如 RATE_LIMITED) 与 errno(原样的 errno,拿到响应时没有);url 去 trace.url 读。 network:error 的 retries(重试了几次)换成 attempts(一共发了几次)。

破坏性分级:TS 用户是 B 档(负载类型变了,d.platform 直接编译错误), JS 用户是 A 档(读到 undefined,没人拦)。两条总线的名字完全一样, 所以判断依据是从哪儿拿的总线,不是事件名。

类型名是 AmagiBusEventMap / AmagiBusEventName / AMAGI_BUS_EVENT_NAMES, 不叫 AmagiEventMap:顶层已经有 v6 的同名类型,两个同名 interface 一起进 dts 会被打包器给其中一个加后缀。三个名字都已进公开面:前两个是 export type, AMAGI_BUS_EVENT_NAMES 是运行时导出。11 个负载 interface 本身不逐个导出, 用 AmagiBusEventMap['api:success'] 这样的索引访问取。

删除(79 个,B 档)

导出数量删除理由
逐个导出的 *ParamsSchema41schema 归端点声明持有,不再逐个对外。需要时用 client.endpoints() 取
method-keys.ts(4×3 组 *InternalMethods / *FetcherMethods / *MethodToFetcher + MethodMaps + toFetcherMethod)14内部零引用
api-spec.ts(4 个 *ApiRoutes + 4 个 *MethodMapping + getApiRoute + getEnglishMethodName)10内部零引用;导出的路由表与真实服务完全不符,曾是一份错误的公开文档
*MethodRoutes4路由由端点声明持有
*ValidationSchemas4同上
registerXxxRoutes4与 createXxxRoutes 是同一函数的别名
bilibiliErrorCodeMap isSmsCodeVerifyWay2前者合并进 bilibiliJudge,后者收进会话引擎

另外这些不在顶层导出、但在子路径导出的也一并删除:

  • 4 个 *APIErrorCode 枚举(约 180 行)。bilibiliAPIErrorCode 是字符串枚举 却用来比数字码(实测 Object.values(...).includes(-101) 为 false), xiaohongshuAPIErrorCode 是混合枚举导致 Object.values() 泄漏反向映射键。 替代物是 AmagiErrorCode 字符串联合 + error.platform.code。
  • TypeMode / TypeControl / ConditionalReturnType / ExtractTypeMode
  • overload-types.ts 的 6 个重载类型别名(约 130 行)
  • platform/bilibili/sign/CorrespondPath.ts / dm_img.ts / types/ReturnDataType/Kuaishou/WorkComments.ts(三个从未被导入的文件)

发布节奏

版本内容破坏性
6.7.0切环 + 契约下沉 + wbi 走 transport无
7.0.0-beta.x统一信封 + 会话抽象 + 删死码有,附本文档
7.0.0稳定 + compat 子路径同上
7.1.0端点注册表迁移完成(若选方案 A 混搭路径)无(对外形态不变)
8.0.0移除 compat;startServer 默认绑 127.0.0.1;语义视图有

最后更新于 2026-10-23

On this page