---
url: /docs/agentbuff-stack/api-keys.md
---
# 用户 API 密钥与外部接口

I10 已提供默认关闭的个人 API 密钥模块。用户在设置页创建、一次性查看、改名和撤销密钥，外部脚本可调用实际文件与异步任务接口。生产 Worker、独立本地 PostgreSQL、Queue / R2 和浏览器已验收；真实云端部署、第三方调用与负载仍待独立验证。它不是站主配置的模型、邮件、支付或数据库密钥。

## 开启与数据库升级

`packages/core/website.ts` 的公开构建配置：

```ts
apiKeys: {
  enabled: false,
  maxActiveKeys: 10,
  requestsPerMinute: 120,
  maxExpirationDays: 365,
},
```

| 字段 | 默认 | 范围与行为 |
| --- | --- | --- |
| `enabled` | `false` | 关闭时不显示设置卡片，所有已定义密钥管理 / 外部接口返回 404；不删除历史密钥 |
| `maxActiveKeys` | `10` | 1—50；每人同时启用且未过期的密钥数量，创建时检查 |
| `requestsPerMinute` | `120` | 1—1000；新密钥的插件请求窗口上限，签发时保存；改变配置不重写旧密钥 |
| `maxExpirationDays` | `365` | 1—365；新密钥最长有效期，不改变旧密钥到期时间 |

密钥属于个人账号，不读取当前团队的套餐或成员身份。至少使用已验证邮箱、非匿名、未封禁账号；改变密钥要求实际存在且创建不足 24 小时的登录会话。不会根据注册顺序或团队角色授予管理权。

新增 `0013_big_thunderbolts.sql`：`apikey` 使用安装的 `@better-auth/api-key@1.7.4` 数据库字段及哈希，`api_key_issuance` 保存签发请求的 UUID、参数摘要和密钥 ID。后者不保存明文，也不级联到密钥记录：插件清理已过期密钥后，旧创建请求仍不能重新签发。账号删除会清理其密钥及签发记录。ID 分别使用 `key_` / `kis_` 加 16 位 CUID2。

按[增量迁移教程](../database/migrations)核对目标数据库并升级历史，再一起重建前后端。不开外部 API 的站点可保持关闭。本任务主预览仍只有 0012，0013 **只在独立一次性数据库验收，尚未应用到主数据库**；主预览保持关闭，不会查询新表。维护命令核对全部迁移历史，升级代码后在迁移未齐的数据库会拒绝初始化管理员，不能跳过检查。

本模块没有新增 `.env` 密钥或云资源。文件操作需要原有 `STORAGE`；提交 / 重试任务需要原有 `TASK_QUEUE` 和任务开关。开启 `security.turnstile.enabled` 后，API 的上传、提交、重试也需要相应动作的 `X-Turnstile-Token`；密钥不绕过验证码，自动化脚本的使用方式需要先考虑这个配置。

## 设置页流程

1. 进入 `/settings`，刷新密钥列表。列表每页 20 条，仅显示名称、短前缀、权限、状态和到期时间。
2. 填写名称、有效天数和实际需要的权限，先复核再确认。取消复核不写入。
3. 创建成功后立即保存明文；关闭展示或离开页面后不能再次查看。它只保留在当前组件内存，不进入查询 / 变更缓存或本地存储。
4. 可以改名和撤销；不提供重新启用、续期或增加权限。需要新权限时签发新密钥，并撤销旧密钥。

创建 UUID 与签发在同一事务提交，同一账号 / UUID / 参数只签发一次；同 UUID 换参数返回 409。若服务端已成功但响应丢失，再发同一请求只返回 `{ id, key: null, replayed: true }`，不重发明文，不额外创建密钥。先刷新列表，未保存时撤销对应密钥、以新 UUID 再创建。页面保留原确认用于手动重试，没有自动重试。

关闭展示清除的是页面可见状态，不承诺擦除浏览器开发工具、历史网络响应或已复制到其他位置的凭据。数据库只保存插件的不可恢复哈希；管理列表、审计、邮件和正常应用日志不输出完整密钥。站主也不能从数据库取回明文。

## 鉴权与作用域

外部接口固定在 `/api/v1`，仅接受 `Authorization: Bearer <用户密钥>`。密钥前缀为 `abk_`；不接收 URL 参数中的密钥，不回退到 Cookie，也不创建模拟会话。密钥不能登录后台、购买 / 修改订阅、修改账号或管理其他密钥。

| 权限          | 允许的接口                             |
| ------------- | -------------------------------------- |
| `files:read`  | 文件列表、单条元数据、私有文件内容下载 |
| `files:write` | 上传和删除自己的文件                   |
| `tasks:read`  | 任务列表、报价 / 余额、单条任务状态    |
| `tasks:write` | 提交、取消和有版本条件的重试           |

每次读取实际密钥、当前账号和作用域。过期、撤销、邮箱不再验证、匿名或封禁会拒绝后续请求；已接受的在途请求及任务仍按原执行 / 扣费 / 退款规则完成，不宣称撤销能中断全部已接受工作。每把密钥有插件请求限制；所有密钥与浏览器进一步共用原有每账号文件 / 任务限制、容量、配额和余额，增加密钥数量不能扩大这些业务额度。

## 文件接口

| 方法与路径 | 结果 |
| --- | --- |
| `GET /api/v1/files` | `{ items, nextCursor }`；下一页用 `cursor` 传 JSON 编码的服务端游标 |
| `GET /api/v1/files/:id` | 自己文件的元数据 |
| `GET /api/v1/files/:id/content` | 实际文件字节，附件下载、禁止缓存；结果文件同样走此接口 |
| `POST /api/v1/files` | 原始二进制正文，`Content-Type` 声明类型，`X-File-Name` 为编码后的文件名；201 返回文件元数据 |
| `DELETE /api/v1/files/:id` | 原删除规则；任务仍保留的输入不能强删 |

上传继续验证实际字节、类型、大小及个人配额，不提供公开 R2 地址。上传没有请求 UUID 去重；响应不确定先查询文件列表，直接重发可能生成第二个文件。删除不会绕过任务输入保留或结果结算。

下面是调用示例，环境变量由调用者在自己的终端设置；不要把真实密钥提交进代码、教程或共享日志。

```sh
curl "$SITE_ORIGIN/api/v1/files" \
  -H "Authorization: Bearer $USER_API_KEY" \
  -H 'Content-Type: text/plain' \
  -H 'X-File-Name: input.txt' \
  --data-binary @input.txt
```

## 报价、任务和重试

| 方法与路径 | 结果 |
| --- | --- |
| `GET /api/v1/tasks/quote` | 原 `text-normalize` 报价、版本、个人余额与支付审核限制；按原规则首次初始化欢迎积分 |
| `GET /api/v1/tasks` | 个人任务分页列表 |
| `GET /api/v1/tasks/:id` | 个人任务、结果文件 ID 和当前 `revision` |
| `POST /api/v1/tasks` | 相同原任务参数、稳定 `requestKey` UUID 和已确认报价；201 表示提交已记录，不代表执行成功 |
| `POST /api/v1/tasks/:id/cancel` | 原取消规则；重复取消不会重复退款，运行中可能只是取消请求已记录 |
| `POST /api/v1/tasks/:id/retry` | 必须携带 `If-Match: "<revision>"`，复用原尝试预算和预扣，不重复扣费 |

提交正文示例；文件 ID、UUID 和报价版本必须来自当前站点：

```json
{
  "requestKey": "替换为本次稳定UUID",
  "inputFileId": "替换为上传返回的fil_ID",
  "processorId": "text-normalize",
  "processorVersion": 1,
  "parameters": { "trimTrailingWhitespace": true },
  "acceptedQuote": { "credits": 0, "version": "从报价接口读取" }
}
```

同账号相同 `requestKey` 与输入，API 与浏览器共用一条任务和一笔原账本预扣；改变内容沿用 UUID 返回 `REQUEST_CONFLICT`。改价后旧报价返回 `QUOTE_CHANGED`，必须重新复核。实际执行、Queue 重发、R2 结果、取消返还与故障核对继续使用[原任务生命周期](./task-lifecycle)，不另建收费路径。

重试前读单条任务的 `revision`，按 HTTP 引号格式发送 `If-Match: "2"`。服务在原任务事务锁内比较版本；重试或其他派发改变版本后，旧请求返回 `STALE_REVISION`，不能在下一次失败后重放旧请求再多执行一次。响应不确定先读取任务当前状态；重新重试是一次新的明确操作，重新读取版本，不能把 409 当成可自动无限重试。重试仍可能因原尝试预算、退款、失效文件、容量或支付审核而拒绝。

## 错误与排查

外部接口错误形如 `{ "error": { "code": "…", "message": "…" } }`；浏览器密钥管理保留认证客户端的 `{ code, message }` 格式。

| 状态 | 处理 |
| --- | --- |
| 400 / 413 | 检查严格输入、大小、文件名及重试版本头；不修改后盲目重发 |
| 401 | 缺少、无效、过期、撤销或作用域不足；密钥不能当浏览器会话 |
| 403 | 当前账号不可用；管理变更可能要求重新登录或正确 Origin |
| 404 | 模块关闭、未开放路径，或资源不属于当前账号 / 不存在 |
| 409 | UUID 内容冲突、任务版本 / 报价变化、容量 / 积分 / 状态限制；先读现状 |
| 429 | 密钥或账号额度用尽；参考 `Retry-After: 60`，它不是精确的剩余窗口承诺 |
| 503 | 所需资源 / 验证未配置或操作未确认；读取现状后手动恢复，不能当作未写入 |

密钥管理每账号每分钟最多 30 次，列表和创建重放也计入。`SESSION_NOT_FRESH` 的确认流程会先实际退出，再重新登录；退出失败不清空当前会话。上线前让调用方明确提交 UUID、上传响应丢失和重试版本规则。

## 本地验收与维护

```sh
bun keys:validate
bun run test -- --run
bun typecheck
bun lint
bun db:check
bun config:check
```

`keys:validate` 只接受 development 和 loopback PostgreSQL，创建 `api_key_acceptance_…` 一次性数据库，应用全部迁移，并只在独立 Worker bundle 中开启密钥及三积分任务。使用实际生产入口、真实 Queue / R2、真实插件及 PostgreSQL；外部 HTTP 被拒绝，邮件进入独立本地收件箱。8 并发签发只有一把明文、其余返回已处理；文件字节、单账本扣费、浏览器服务重放、结果下载、取消退款、版本重试、并发限流、作用域、过期、封禁和撤销都有断言。测试结束销毁临时数据库 / Worker / 资源，不升级当前配置数据库或改动源配置。

`bun keys:validate --ui` 额外构建独立工作台并暂停供浏览器检查，使用脚本中的一次性测试账号，不涉及真实账号或支付。验收创建 / 撤销后创建 `.local/api-key-ui-stop` 文件，让脚本核对落库状态并清理；暂停最多 12 分钟。不要将此验收服务器作为长期预览或生产部署。

实现契约参考当前安装包与[插件官方文档](https://better-auth.com/docs/plugins/api-key)。插件升级时重新核对哈希、字段、默认端点和限流行为；不能直接开放其全部管理操作或启用 API 密钥模拟会话。
