# 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` |
### 提交信息格式 [#提交信息格式]
```
():