# LLMs.txt (https://ikenxuan.github.io/amagi/docs/v6/ai/llms-txt) > 让 AI 助手理解 Amagi 文档 > 让 Claude、Cursor、Windsurf 等 AI 助手能够理解和使用 Amagi 文档 我们提供 [LLMs.txt](https://llmstxt.org/) 文件,让 AI 编码助手能够访问 Amagi 文档。 ## 可用文件 [#可用文件] **核心文档:** * [/llms.txt](/llms.txt) — 快速参考索引 * [/llms-full.txt](/llms-full.txt) — 完整的 Amagi 文档 ## 集成方式 [#集成方式] ### Cursor [#cursor] 使用 `@Docs` 功能: ``` @Docs https://ikenxuan.github.io/amagi/llms-full.txt ``` 或在聊天中直接引用: ``` 使用 Amagi 文档:https://ikenxuan.github.io/amagi/llms.txt ``` [了解更多](https://docs.cursor.com/context/@-symbols/@-docs) ### Cline (VS Code) [#cline-vs-code] 在提示中直接引用: ``` 参考 Amagi 文档:https://ikenxuan.github.io/amagi/llms-full.txt ``` [了解更多](https://cline.bot) ### Continue (VS Code) [#continue-vs-code] 在 Continue 配置中添加自定义文档: 1. 打开配置:`Ctrl+Shift+P` → "Continue: Add new custom docs" 2. 添加 URL:`https://ikenxuan.github.io/amagi/llms.txt` 3. 在聊天中使用 `@docs` 并选择 amagi 或直接在提示中引用文档 URL。 [了解更多](https://continue.dev/docs) ### Claude Code [#claude-code] 告诉 Claude 引用文档: ``` 使用 Amagi 文档:https://ikenxuan.github.io/amagi/llms.txt ``` 或添加到项目的 `.claude` 文件以自动加载。 ### Windsurf [#windsurf] 添加到 `.windsurfrules` 文件: ``` #docs https://ikenxuan.github.io/amagi/llms-full.txt ``` [了解更多](https://docs.codeium.com/windsurf/memories#memories-and-rules) ### JetBrains AI Assistant [#jetbrains-ai-assistant] JetBrains IDE(IntelliJ IDEA、PyCharm、WebStorm 等)可以通过 AI Assistant 使用文档: 1. 在 AI Assistant 聊天中直接引用: ``` 参考文档:https://ikenxuan.github.io/amagi/llms-full.txt ``` [了解更多](https://www.jetbrains.com/help/ai-assistant/) ### GitHub Copilot (VS Code) [#github-copilot-vs-code] 在 Copilot Chat 中引用文档: ``` @workspace 使用 Amagi 文档:https://ikenxuan.github.io/amagi/llms.txt ``` ### 其他 AI 工具 [#其他-ai-工具] 大多数 AI 助手都可以通过 URL 引用文档。只需提供: ``` https://ikenxuan.github.io/amagi/llms.txt ``` 对于完整文档内容,使用 `/llms-full.txt` ## 使用示例 [#使用示例] 配置完成后,你可以向 AI 助手提问: * "如何使用 Amagi SDK 获取 Bilibili 视频信息?" * "Amagi 支持哪些平台的 API?" * "如何配置 HTTP Server 模式?" * "事件系统如何使用?" * "如何处理 Douyin 的评论数据?" AI 助手会基于 LLMs.txt 文件中的文档内容给出准确的回答。 ## 获取单个页面的 Markdown [#获取单个页面的-markdown] 你可以通过在任何文档页面 URL 后添加 `.mdx` 来获取该页面的 Markdown 内容: ``` https://ikenxuan.github.io/amagi/docs/v6/usage/getting-started.mdx https://ikenxuan.github.io/amagi/docs/v6/usage/guide/sdk.mdx https://ikenxuan.github.io/amagi/docs/v6/usage/api/bilibili.mdx ``` 这对于需要特定页面内容的 AI 工具特别有用。 ## 内容协商 [#内容协商] 某些 AI 工具会发送 `Accept: text/markdown` 请求头。我们的文档站点会自动识别并返回 Markdown 格式的内容,而不是 HTML 页面。 ## 贡献 [#贡献] 发现 AI 生成的代码有问题?帮助我们改进 LLMs.txt 文件,请访问 [GitHub](https://github.com/ikenxuan/amagi)。 ## 相关链接 [#相关链接] * [LLMs.txt 规范](https://llmstxt.org/) * [GitHub 仓库](https://github.com/ikenxuan/amagi) # v6.1.3 (https://ikenxuan.github.io/amagi/docs/v6/changelog/6.1.3) > 2025-05-10 发布的重要变更 # v6.1.3 变更日志 [#v613-变更日志] ## B站 [#b站] ### `fetchDynamicCard` 接口停用 [#fetchdynamiccard-接口停用] **影响接口**: `fetchDynamicCard` / `dynamicCard` B站官方已删除 `dynamic_svr/v1/dynamic_svr/get_dynamic_detail` 接口(原获取动态卡片信息接口)。 自 v6.1.3 起,调用 `fetchDynamicCard` 将直接返回错误信息,不再转调其他接口做兼容: ```typescript { code: -404, message: '接口已停用:B站官方已删除 dynamic_svr 接口,请使用 dynamicDetail 替代', ttl: 1, data: null } ``` **迁移方式**: 使用 `fetchDynamicDetail` 替代。 ```typescript // 之前 const result = await fetchDynamicCard({ dynamic_id: '123456789' }, cookie) // 之后 const result = await fetchDynamicDetail({ dynamic_id: '123456789' }, cookie) ``` 该接口计划于 **v7.0.0** 从代码中彻底移除。 # v6.2.0 (https://ikenxuan.github.io/amagi/docs/v6/changelog/6.2.0) > 2025-06-30 发布的重要变更 # v6.2.0 变更日志 [#v620-变更日志] ## 小红书 [#小红书] ### 新增游客 Cookie 生成能力 [#新增游客-cookie-生成能力] 新增小红书 Web 端游客 Cookie 生成工具接口,可在未持有用户 Cookie 的场景下,按照 Web 端首次访问的初始化流程(生成 a1/webId,依次完成 scripting、webprofile、activate 三个会话请求)生成一份包含 `web_session` 的游客 Cookie。 ```typescript import { xiaohongshuSign } from '@ikenxuan/amagi' // 生成游客 Cookie const guestCookie = await xiaohongshuSign.createGuestCookie() // 可透传传输参数(代理、超时等) const guestCookie = await xiaohongshuSign.createGuestCookie({ proxy: { host: '127.0.0.1', port: 7890 }, timeout: 10000 }) ``` 游客 Cookie 仅提供工具能力,**不保证所有接口都能使用该游客 Cookie 请求成功**。部分接口对登录态有强校验,使用游客 Cookie 仍可能返回受限或失败的结果,请按需自行验证。 # v6.3.0 (https://ikenxuan.github.io/amagi/docs/v6/changelog/6.3.0) > 2026-07-30 发布的重要变更 # v6.3.0 变更日志 [#v630-变更日志] ## B站 [#b站] ### 新增按用户 UID 查询直播状态接口 [#新增按用户-uid-查询直播状态接口] 新增 `fetchUserLiveStatus`,可直接通过用户 UID 查询直播间是否开通、当前是否正在直播以及直播间 ID 等基础状态。 该接口不依赖用户空间动态中的直播推荐卡片,适合开播提醒、定时轮询等需要尽快感知直播状态的场景。 ```typescript import amagi from '@ikenxuan/amagi' const client = amagi({ cookies: { bilibili: '', douyin: '', kuaishou: '', xiaohongshu: '' } }) const result = await client.bilibili.fetcher.fetchUserLiveStatus({ host_mid: 2, typeMode: 'strict' }) if (result.success && result.data.data.liveStatus === 1) { console.log(`正在直播,直播间 ID:${result.data.data.roomid}`) } ``` #### 参数 [#参数] | 参数 | 类型 | 必填 | 说明 | | ---------- | --------------------- | -- | --------------------- | | `host_mid` | `number` | 是 | B站用户 UID | | `typeMode` | `'strict' \| 'loose'` | 否 | 返回类型模式,默认值为 `'loose'` | #### 主要返回字段 [#主要返回字段] | 字段 | 说明 | | ------------ | ------------------------ | | `roomStatus` | `1` 表示用户已开通直播间,`0` 表示未开通 | | `liveStatus` | `1` 表示正在直播,`0` 表示当前未开播 | | `roomid` | 直播间 ID;未开通直播间时为 `0` | | `title` | 直播间标题 | | `cover` | 直播封面 | | `url` | 直播间地址 | 如需直播分区、开播时间等完整直播间信息,可将 `roomid` 传给现有的 `fetchLiveRoomInfo`: ```typescript const liveStatus = await client.bilibili.fetcher.fetchUserLiveStatus({ host_mid: 2, typeMode: 'strict' }) if (liveStatus.success && liveStatus.data.data.liveStatus === 1) { const liveRoom = await client.bilibili.fetcher.fetchLiveRoomInfo({ room_id: String(liveStatus.data.data.roomid), typeMode: 'strict' }) } ``` ### HTTP 接口 [#http-接口] HTTP 服务同步提供以下路由: ```http GET /api/bilibili/fetch_user_live_status?host_mid=2 ``` 响应字段与 Fetcher 返回的B站原始响应类型一致。 # v6.4.0 (https://ikenxuan.github.io/amagi/docs/v6/changelog/6.4.0) > 2026-07-30 发布的重要变更 # v6.4.0 变更日志 [#v640-变更日志] ## Fetcher [#fetcher] ### 四个平台统一支持单次请求配置覆盖 [#四个平台统一支持单次请求配置覆盖] 通过 Amagi Client 或 `createBound*Fetcher` 创建的 B站、抖音、快手和小红书绑定 Fetcher,现在都会读取方法的第二个 `requestConfig` 参数,并将它与实例级请求配置合并。 单次配置拥有更高优先级,请求头会单独合并,因此可以只覆盖当前请求的 Cookie,同时保留实例配置的代理、超时和 User-Agent: ```typescript import amagi from '@ikenxuan/amagi' const client = amagi({ cookies: { bilibili: 'SESSDATA=example', douyin: '', kuaishou: '', xiaohongshu: '' }, request: { timeout: 10_000 } }) const result = await client.bilibili.fetcher.fetchComments( { oid: '170001', type: 1, number: 20, typeMode: 'strict' }, { headers: { Cookie: '' } } ) ``` 该覆盖只影响当前调用,不会修改 Client 或绑定 Fetcher 保存的配置,也不会影响同一实例上的后续请求。显式提供大写 `headers.Cookie` 时,该值还会同步成为底层 Fetcher 当前调用的 cookie 参数,确保平台签名、前置请求和最终请求处于同一身份状态。 直接导出的裸 Fetcher 也支持单次请求配置,调用签名为 `fetcher.method(options, cookie?, requestConfig?)`: ```typescript import { bilibiliFetcher } from '@ikenxuan/amagi' const result = await bilibiliFetcher.fetchComments( { oid: '170001', type: 1 }, '', { headers: { Cookie: '' } } ) ``` 裸 Fetcher 会在类型层关联参数二与参数三的 `headers.Cookie`:只要显式传入 `Cookie` 请求头,参数二就必须是相同的字符串字面量。因此匿名请求必须同时传入空字符串,不能保留登录 Cookie。请求头字段严格使用大写 `Cookie`,小写 `cookie` 会导致类型检查失败。 该调用形式同样适用于 `douyinFetcher`、`kuaishouFetcher`、`xiaohongshuFetcher` 以及默认导出上的同名静态属性。 ## B站 [#b站] ### 匿名获取评论 [#匿名获取评论] B站登录态会影响评论区热度池,不同账号可能得到不同的评论排序。需要稳定获取匿名评论结果时,可以通过上述单次请求配置清空 Cookie,无需创建第二个 Amagi Client。 评论请求仍由原 Client 实例发起,因此会继续经过同一套事件、日志和错误处理链路。 # v6.5.0 (https://ikenxuan.github.io/amagi/docs/v6/changelog/6.5.0) > 2026-08-04 发布的重要变更 # v6.5.0 变更日志 [#v650-变更日志] ## 破坏性变更 [#破坏性变更] ### 移除全部已废弃的 API [#移除全部已废弃的-api] v6.0 发布时标记为废弃(调用会抛出 `DeprecatedApiError`)的兼容 API,以及后续版本标记废弃的接口,自本版本起全部彻底移除,不再提供导入与类型导出。 **`getXxxData` 系列** `getDouyinData`、`getBilibiliData`、`getKuaishouData`、`getXiaohongshuData` 已从包导出、Amagi Client 实例和默认导出的静态属性中移除。请改用对应平台的 Fetcher: ```typescript import { bilibiliFetcher } from '@ikenxuan/amagi' const result = await bilibiliFetcher.fetchVideoInfo({ bvid: 'BV123' }, cookie) ``` **平台旧版 `api` 命名空间** `douyin` / `bilibili` / `kuaishou` / `xiaohongshu` 四个旧版 API 存根对象及其绑定工厂(`createBoundDouyinApi` 等)已移除,Amagi Client 实例上的 `client.平台.api` 属性同步移除。请统一使用 `client.平台.fetcher`。 **日志兼容模块** `logger`、`httpLogger`、`initLogger`、`logMiddleware` 已移除。v6 已改用事件驱动架构,请通过事件系统实现日志: ```typescript import { amagiEvents } from '@ikenxuan/amagi' amagiEvents.on('log:info', (data) => console.log(data.message)) amagiEvents.on('http:response', (data) => console.log(data.statusCode, data.url)) ``` **其他移除项** * `amagiClient` 别名:请使用 `createAmagiClient` 或默认导出 * `fetchDynamicCard` 及配套内容:B站官方已删除 `dynamic_svr` 接口,本次移除 Fetcher 方法、HTTP 路由 `GET /fetch_dynamic_card`、`bilibiliApiUrls.getDynamicCard`、返回类型 `BiliDynamicCard` 及相关的参数校验与方法名映射 * `ApiResponse` 类型别名:请使用 `Result` * 内部废弃管理模块(`utils/deprecation`)随上述 API 一并移除 完整的迁移方式请参考 [v6 迁移指南](/docs/v6/usage/migration-v6)。 ## 文档 [#文档] ### 文档站更新至 v6 正式版 [#文档站更新至-v6-正式版] 移除了文档中残留的 v6 Alpha / 测试版标识与 `@beta` 安装提示,安装、快速开始与迁移指南均以 v6 正式版为准。 # v6.6.0 (https://ikenxuan.github.io/amagi/docs/v6/changelog/6.6.0) > 2026-09-01 发布的实验性抖音 Passport 扫码登录功能 # v6.6.0 变更日志 [#v660-变更日志] 抖音 Passport 扫码登录是 **实验性功能**,不保证稳定性。它依赖抖音 Web 端的内部登录接口、签名参数和风控策略,服务端行为变化可能随时导致功能失效。后续版本可能调整、重命名或移除相关方法、参数、返回值和底层实现,请不要在对稳定性有要求的生产登录链路中依赖当前 API。 ## 新增:抖音 Passport 扫码登录 [#新增抖音-passport-扫码登录] 本版本新增一套基于 `login.douyin.com` Passport 体系的抖音扫码登录流程,支持: * 获取 Passport 登录二维码; * 轮询二维码状态; * 处理扫码确认后的 SSO 跳转; * 处理部分账号触发的短信二次验证; * 提交短信验证码并获取登录 Cookie; * 在调用之间传递会话 Cookie 和 `bd-ticket-guard` 设备票据状态。 这套流程与原有的 `requestLoginQrcode` 不同。后者是旧的登录二维码接口;Passport 登录提供的是一套从取码到获取登录态的完整流程。 ## 重要限制:目前独立于 Amagi Client 架构 [#重要限制目前独立于-amagi-client-架构] Passport 登录当前是独立接入的实验性模块,**尚未完整纳入 Amagi Client 的实例级架构**。它与现有通过 `client.douyin.fetcher` 使用的稳定数据接口不是同一套内部实现: * 当前推荐使用导出的 Passport 方法或 `douyinFetcher`; * 不支持通过 `client.douyin.fetcher` 的实例 Cookie 状态自动维护登录会话; * 不支持通过 `createBoundDouyinFetcher` 绑定并自动维护这套登录流程; * 登录过程中的会话状态必须由调用方保存每次返回的 `cookie`,并在下一次调用时传回; * 轮询循环也需要由调用方自行维护。 后续可能会将这套能力重构并接入统一的客户端、Fetcher 或会话管理架构。**因此当前方法的调用方式和类型不能视为长期兼容的稳定 API。** ## 当前 API [#当前-api] 可以从包根路径导入以下方法: ```ts import { checkPassportQrcode, requestPassportQrcode, sendPassportVerifyCode, validatePassportVerifyCode } from '@ikenxuan/amagi' ``` 也可以通过 `douyinFetcher` 使用这些方法: ```ts import { douyinFetcher } from '@ikenxuan/amagi' const qrcode = await douyinFetcher.requestPassportQrcode() ``` ### 获取二维码 [#获取二维码] ```ts const result = await requestPassportQrcode() if (!result.success) { throw new Error(result.message) } console.log(result.data.content) console.log(result.data.expires_in) // 必须保存,用于后续轮询;其中可能包含 Passport 会话状态和实验性模块的内部状态 const cookie = result.data.cookie const token = result.data.token ``` 返回数据包括: | 字段 | 说明 | | ------------- | ---------------------- | | `token` | 轮询二维码状态时使用的令牌 | | `content` | 二维码内容,可交给二维码生成器渲染 | | `expire_time` | 服务端返回的绝对 Unix 时间戳,单位为秒 | | `expires_in` | 获取二维码时计算出的剩余秒数,是一个时间快照 | | `cookie` | 后续调用必须继续传递的会话 Cookie | ### 轮询二维码状态 [#轮询二维码状态] ```ts const status = await checkPassportQrcode({ token }, cookie) if (!status.success) { throw new Error(status.message) } switch (status.data.status) { case 'new': case 'scanned': case 'busy': // 根据 status.data.interval 等待后继续轮询 cookie = status.data.cookie break case 'verify': // 使用 status.data.verify 进入短信二次验证流程 cookie = status.data.cookie break case 'confirmed': cookie = status.data.cookie console.log('登录完成:', status.data.logged_in) break case 'expired': case 'risk': case 'unknown': console.log(status.data.message) break } ``` 每次调用后都应使用响应中最新的 `cookie`。二维码状态可能包括: * `new`:尚未扫码; * `scanned`:已扫码,等待手机端确认; * `confirmed`:已确认并尝试完成登录; * `verify`:需要进行二次验证; * `expired`:二维码已过期; * `busy`:轮询过于频繁,按 `interval` 退避后重试; * `risk`:触发风控; * `unknown`:服务端返回了暂未适配的状态。 ### 短信二次验证 [#短信二次验证] 当轮询结果为 `verify` 时,可以把返回的验证上下文传给发码接口: ```ts const sendCode = await sendPassportVerifyCode( { verify: status.data.verify }, status.data.cookie ) if (!sendCode.success || !sendCode.data.ok) { throw new Error(sendCode.success ? sendCode.data.message : sendCode.message) } const validateCode = await validatePassportVerifyCode( { verify: status.data.verify, code: '123456', biz_trace_id: sendCode.data.biz_trace_id, verify_way: sendCode.data.verify_way }, sendCode.data.cookie ) if (!validateCode.success || !validateCode.data.ok) { throw new Error(validateCode.success ? validateCode.data.message : validateCode.message) } cookie = validateCode.data.cookie ``` `verify_way` 应优先使用服务端返回的实际值。不同账号可能返回不同的验证方式,例如 `mobile_sms_verify` 或 `assist_mobile_sms_verify`。`biz_trace_id` 也应在发送验证码和提交验证码时保持一致。 ## 兼容性与稳定性说明 [#兼容性与稳定性说明] 当前实现包含 Passport 专用的签名、`a_bogus`、环境指纹、Cookie 管理和 `bd-ticket-guard` 票据处理逻辑。这些内容不属于 Amagi 面向稳定数据接口的公共抽象,抖音也没有为这些 Web 内部接口提供稳定的第三方 API 契约。 使用时请注意: * 抖音可能修改接口地址、请求字段、签名算法、Cookie 语义或风控规则; * 同一账号、设备、网络环境下的可用验证方式可能不同; * 获取二维码成功不代表后续轮询或登录一定成功; * `confirmed`、`ok` 和 `logged_in` 等字段应结合实际返回结果判断,不应只根据 HTTP 或外层 `code` 判断; * 请妥善保管返回的登录 Cookie,不要写入日志、提交到代码仓库或暴露给不可信的第三方; * 发生异常时可以启用 debug 日志辅助排查,但仍应避免记录完整 Cookie 或验证码。 如果需要稳定的抖音数据访问能力,请继续使用现有的抖音 Fetcher API。Passport 登录仅建议用于实验、开发验证和能够接受接口变化的场景。 # 变更日志 (https://ikenxuan.github.io/amagi/docs/v6/changelog) > amagi v6 各版本的行为变化 v6 系列的版本更新记录。每个版本的破坏性变更、新接口与缺陷修复 逐版列出: * [v6.6.0](/docs/v6/changelog/6.6.0) * [v6.5.0](/docs/v6/changelog/6.5.0) * [v6.4.0](/docs/v6/changelog/6.4.0) * [v6.3.0](/docs/v6/changelog/6.3.0) * [v6.2.0](/docs/v6/changelog/6.2.0) * [v6.1.3](/docs/v6/changelog/6.1.3) # 新增接口 (https://ikenxuan.github.io/amagi/docs/v6/dev/add-api) > 学习如何为平台新增数据接口 # 新增接口 [#新增接口] 本文档以 B站为例,演示如何新增一个平台接口。 ## 需要修改的文件 [#需要修改的文件] 新增一个接口需要修改以下文件: | 序号 | 文件 | 作用 | | -- | ---------------------------------------- | ------------------ | | 1 | `types/BilibiliAPIParams.ts` | 定义参数类型 | | 2 | `validation/bilibili.ts` | 参数验证 Schema + 路由映射 | | 3 | `platform/bilibili/API.ts` | URL 拼接 | | 4 | `platform/bilibili/getdata.ts` | 数据获取逻辑 | | 5 | `types/ReturnDataType/Bilibili/index.ts` | 返回类型(可选) | | 6 | `model/fetchers/bilibili/types.ts` | Fetcher 类型 | | 7 | `model/fetchers/bilibili/*.ts` | Fetcher 实现 | | 8 | `model/fetchers/bilibili/index.ts` | 导出 | ## 完整示例:新增「获取视频标签」接口 [#完整示例新增获取视频标签接口] `types/BilibiliAPIParams.ts`: 在 `BilibiliMethodOptionsMap` 接口中添加新的参数类型: ```ts /** B站 API 方法参数映射接口 */ export interface BilibiliMethodOptionsMap { /** 获取单个视频信息 */ VideoInfoParams: { methodType: 'videoInfo' /** 稿件BVID */ bvid: string } // 新增:获取视频标签 /** 获取视频标签 */ VideoTagsParams: { methodType: 'videoTags' /** 稿件 AVID */ avid: number } } /** B站方法类型到参数的映射 */ export type BilibiliMethodOptMap = { videoInfo: BilibiliMethodOptionsMap['VideoInfoParams'] videoTags: BilibiliMethodOptionsMap['VideoTagsParams'] // 新增 } ``` `validation/bilibili.ts`: ```ts import { z } from 'zod' // 定义参数类型(与 types 中保持一致) interface VideoTagsParams { methodType: 'videoTags' avid: number } // 新增 Schema export const BilibiliVideoTagsParamsSchema: z.ZodType = z.object({ methodType: z.literal('videoTags'), avid: z.coerce.number().int().positive() }) // 在 BilibiliValidationSchemas 中注册 export const BilibiliValidationSchemas = { videoTags: BilibiliVideoTagsParamsSchema } as const // 在 BilibiliMethodRoutes 中添加路由 export const BilibiliMethodRoutes = { videoTags: '/fetch_video_tags' } as const ``` `platform/bilibili/API.ts`: ```ts class BilibiliAPI { /** 获取视频标签 */ getVideoTags(data: { avid: number }): string { return `https://api.bilibili.com/x/tag/archive/tags?aid=${data.avid}` } } // 使用示例 const api = new BilibiliAPI() const url = api.getVideoTags({ avid: 170001 }) ``` `platform/bilibili/getdata.ts`: ```ts // 模拟类型定义 interface RequestConfig { url: string method?: string headers?: Record } interface BilibiliAPI { getVideoTags(data: { avid: number }): string } declare const bilibiliApiUrls: BilibiliAPI declare const baseRequestConfig: RequestConfig declare function GlobalGetData(methodType: string, config: RequestConfig): Promise // 在 switch 语句中添加 case async function handleMethod(data: { methodType: string; avid?: number }) { switch (data.methodType) { case 'videoTags': { const result = await GlobalGetData(data.methodType, { ...baseRequestConfig, url: bilibiliApiUrls.getVideoTags({ avid: data.avid! }) }) return result } } } ``` `model/fetchers/bilibili/types.ts`: ```ts // 基础类型定义 type TypeMode = 'strict' | 'loose' type Result = | { success: true; code: number; message: string; data: T } | { success: false; code: number; message: string; error: unknown } interface BaseRequestOptions { typeMode?: TypeMode } /** B站视频标签请求参数 */ export interface BilibiliVideoTagsOptions extends BaseRequestOptions { /** 视频 AV 号 */ avid: number } /** B站 Fetcher 接口 */ export interface IBilibiliFetcher { /** 获取视频标签 */ fetchVideoTags: ( options: BilibiliVideoTagsOptions, cookie?: string ) => Promise> } /** 视频标签响应类型 */ interface VideoTagsResponse { tags: Array<{ tag_id: number tag_name: string }> } ``` `model/fetchers/bilibili/video.ts`: ```ts // 类型定义 interface BilibiliVideoTagsOptions { avid: number typeMode?: 'strict' | 'loose' } type Result = | { success: true; code: number; message: string; data: T } | { success: false; code: number; message: string; error: unknown } declare function fetchBilibiliInternal(methodType: string, params: Record, options: { cookie?: string }): Promise> /** * 获取视频标签 * @param options - 请求参数 * @param cookie - B站 Cookie */ export async function fetchVideoTags(options: BilibiliVideoTagsOptions, cookie?: string): Promise> { return fetchBilibiliInternal('videoTags', { avid: options.avid }, { cookie }) } ``` `model/fetchers/bilibili/index.ts`: ```ts // 模拟实现 const fetchVideoTags = async (options: { avid: number }, cookie?: string) => ({ success: true as const, code: 200, message: 'ok', data: { tags: [] } }) // 导出函数 export { fetchVideoTags } // 导出 fetcher 对象 export const bilibiliFetcher = { fetchVideoTags } // 类型检查 const result = bilibiliFetcher.fetchVideoTags({ avid: 170001 }) ``` ## 特殊情况处理 [#特殊情况处理] B站部分接口需要 WBI 签名: ```ts // 类型定义 interface RequestConfig { url: string method?: string } interface BilibiliAPI { yourMethod(data: { param: string }): string } declare const bilibiliApiUrls: BilibiliAPI declare const baseRequestConfig: RequestConfig declare function GlobalGetData(methodType: string, config: RequestConfig): Promise declare function wbi_sign(url: string, cookie: string): Promise async function handleWbiMethod(data: { methodType: string; param: string }, cookie: string) { const baseUrl = bilibiliApiUrls.yourMethod({ param: data.param }) // 生成 WBI 签名查询参数 const wbiSignQuery = await wbi_sign(baseUrl, cookie) const result = await GlobalGetData(data.methodType, { ...baseRequestConfig, url: baseUrl + wbiSignQuery }) return result } ``` `API.ts` 返回 URL 和 Body: ```ts class BilibiliAPI { yourPostMethod(data: { param1: string }) { return { Url: 'https://api.bilibili.com/xxx', Body: { param1: data.param1 } } } } const api = new BilibiliAPI() const { Url, Body } = api.yourPostMethod({ param1: 'test' }) ``` `getdata.ts` 使用 POST: ```ts // 类型定义 interface RequestConfig { url: string method?: string data?: Record } interface BilibiliAPI { yourPostMethod(data: { param1: string }): { Url: string; Body: Record } } declare const bilibiliApiUrls: BilibiliAPI declare const baseRequestConfig: RequestConfig declare function GlobalGetData(methodType: string, config: RequestConfig): Promise async function handlePostMethod(data: { methodType: string; param1: string }) { const { Url, Body } = bilibiliApiUrls.yourPostMethod({ param1: data.param1 }) const result = await GlobalGetData(data.methodType, { ...baseRequestConfig, method: 'POST', url: Url, data: Body }) return result } ``` B站弹幕接口返回 Protobuf 格式: ```ts // 类型定义 interface RequestConfig { url: string responseType?: 'arraybuffer' | 'json' } interface DanmakuItem { id: number content: string time: number } declare const baseRequestConfig: RequestConfig declare function fetchData(config: RequestConfig): Promise declare function parseDmSegMobileReply(buffer: ArrayBuffer): DanmakuItem[] declare function createSuccessResponse(data: T): { success: true; data: T } async function handleDanmaku(url: string) { const response = await fetchData({ ...baseRequestConfig, url, responseType: 'arraybuffer' }) // 解析 protobuf const parsed = parseDmSegMobileReply(response) return createSuccessResponse(parsed) } ``` 抖音接口需要 `a_bogus` 签名: ```ts // 类型定义 interface RequestConfig { url: string } interface DouyinSign { AB(url: string, userAgent: string): string } declare const douyinSign: DouyinSign declare const baseRequestConfig: RequestConfig declare function GlobalGetData(methodType: string, config: RequestConfig): Promise async function handleDouyinMethod(baseUrl: string, userAgent: string, methodType: string) { // 生成 a_bogus 签名 const aBogus = douyinSign.AB(baseUrl, userAgent) const signedUrl = `${baseUrl}&a_bogus=${aBogus}` const result = await GlobalGetData(methodType, { ...baseRequestConfig, url: signedUrl }) return result } ``` 小红书接口需要 `X-S` 签名: ```ts // 类型定义 interface RequestConfig { url: string headers?: Record } interface XiaohongshuSign { generateXSGet(path: string, a1Cookie: string): string generateXT(): string } declare const xiaohongshuSign: XiaohongshuSign declare const baseRequestConfig: RequestConfig declare function GlobalGetData(methodType: string, config: RequestConfig): Promise async function handleXiaohongshuMethod(path: string, a1Cookie: string, methodType: string) { const xs = xiaohongshuSign.generateXSGet(path, a1Cookie) const xt = xiaohongshuSign.generateXT() const result = await GlobalGetData(methodType, { ...baseRequestConfig, url: `https://edith.xiaohongshu.com${path}`, headers: { 'X-S': xs, 'X-T': xt } }) return result } ``` ## 验证修改 [#验证修改] 在提交前,请确保: ```bash # 1. 类型检查通过 pnpm typecheck # 2. 构建成功 pnpm build # 3. 功能测试 pnpm dev # 然后手动测试新增的接口 ``` # 项目架构 (https://ikenxuan.github.io/amagi/docs/v6/dev/architecture) > 了解 amagi 的项目结构和核心概念 # 项目架构 [#项目架构] ## 目录结构 [#目录结构] ``` amagi/ ├── packages/ │ ├── core/ # @ikenxuan/amagi 核心包 │ │ └── src/ │ │ ├── model/ │ │ │ └── fetchers/ # Fetcher API 实现 │ │ │ ├── bilibili/ │ │ │ ├── douyin/ │ │ │ ├── kuaishou/ │ │ │ └── xiaohongshu/ │ │ ├── platform/ # 平台实现 │ │ │ ├── bilibili/ │ │ │ │ ├── API.ts # URL 拼接 │ │ │ │ ├── getdata.ts # 数据获取 │ │ │ │ ├── routes.ts # HTTP 路由 │ │ │ │ └── sign/ # 签名算法 │ │ │ ├── douyin/ │ │ │ ├── kuaishou/ │ │ │ └── xiaohongshu/ │ │ ├── types/ # 类型定义 │ │ ├── validation/ # 参数验证 (Zod) │ │ ├── server/ # HTTP 服务 │ │ └── index.ts # 主入口 │ └── docs/ # 文档站点 (Fumadocs) ├── package.json ├── pnpm-workspace.yaml └── tsconfig.json ``` ## 数据流 [#数据流] ``` 用户调用 Fetcher ↓ 参数验证 (Zod) ↓ getdata.ts 处理 ↓ API.ts 拼接 URL ↓ 签名处理 (如需要) ↓ 发起 HTTP 请求 ↓ 统一格式返回 ``` ## 核心模块 [#核心模块] ### Fetcher (`model/fetchers/`) [#fetcher-modelfetchers] 对外暴露的 API 层,负责: * 定义方法签名和类型 * 处理 `typeMode` 参数 * 调用内部实现 ```ts // 类型定义 type TypeMode = 'strict' | 'loose' type Result = { success: true; data: T } | { success: false; error: unknown } interface BilibiliVideoInfoOptions { bvid: string typeMode?: TypeMode } declare function fetchBilibiliInternal(methodType: string, params: Record, options: { cookie?: string }): Promise> // model/fetchers/bilibili/video.ts export async function fetchVideoInfo(options: BilibiliVideoInfoOptions, cookie?: string): Promise> { return fetchBilibiliInternal('videoInfo', { bvid: options.bvid }, { cookie }) } ``` ### Platform (`platform/`) [#platform-platform] 各平台的具体实现: | 文件 | 职责 | | ------------ | -------------- | | `API.ts` | URL 拼接,不发请求 | | `getdata.ts` | 实际发起请求,处理签名、响应 | | `routes.ts` | HTTP 路由注册 | | `sign/` | 签名算法实现 | ### Validation (`validation/`) [#validation-validation] 基于 Zod 的参数验证: ```ts import { z } from 'zod' // 定义参数类型 interface VideoInfoParams { methodType: 'videoInfo' bvid: string } // 创建验证 Schema export const VideoInfoParamsSchema: z.ZodType = z.object({ methodType: z.literal('videoInfo'), bvid: z.string().startsWith('BV') }) // 验证示例 const result = VideoInfoParamsSchema.safeParse({ methodType: 'videoInfo', bvid: 'BV1xx411c7mD' }) ``` ### Types (`types/`) [#types-types] TypeScript 类型定义: * `*APIParams.ts` - 各平台的参数类型 * `ReturnDataType/` - 响应数据类型 ## 统一响应格式 [#统一响应格式] 所有 API 返回 `Result` 类型: ```ts type SuccessResult = { success: true code: number message: string data: T } type ErrorResult = { success: false code: number message: string error: unknown } type Result = SuccessResult | ErrorResult ``` # 贡献指南 (https://ikenxuan.github.io/amagi/docs/v6/dev/contributing) > 提交规范和 PR 流程 # 贡献指南 [#贡献指南] 感谢你对 amagi 的贡献!本文档介绍提交规范和 PR 流程。 ## 提交规范 [#提交规范] 使用 [Conventional Commits](https://www.conventionalcommits.org/) 规范: | 类型 | 说明 | 示例 | | ---------- | ------ | ----------------------------------- | | `feat` | 新功能 | `feat: add fetchVideoTags API` | | `fix` | Bug 修复 | `fix: correct parameter validation` | | `docs` | 文档更新 | `docs: update installation guide` | | `refactor` | 重构 | `refactor: simplify fetcher logic` | | `perf` | 性能优化 | `perf: cache wbi keys` | | `chore` | 构建/工具 | `chore: update dependencies` | | `test` | 测试 | `test: add unit tests for sign` | ### 提交信息格式 [#提交信息格式] ``` ():