feat: ComfyUI video assistant SaaS plan + shared types cleanup

- Add plan/2026-05-18-comfyui-video-assistant-saas.md (gpt-5.5 generated)
- Add src/server/ and src/shared/ type definitions
- Update comfyui routes and api_key auth
- Remove old metapi docs, deploy/k3s, and deprecated configs
This commit is contained in:
boos
2026-05-18 11:28:20 +08:00
parent bfdb01e542
commit f2434e725d
120 changed files with 3637 additions and 7174 deletions
-3
View File
@@ -1,3 +0,0 @@
dist/
cache/
.temp/
-106
View File
@@ -1,106 +0,0 @@
import { existsSync, globSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import config from './config';
const getAliasEntry = (aliasConfig: unknown, specifier: string) => {
if (!aliasConfig) return undefined;
if (Array.isArray(aliasConfig)) {
return aliasConfig.find(
(entry) =>
entry &&
typeof entry === 'object' &&
'find' in entry &&
((typeof entry.find === 'string' && entry.find === specifier) ||
(entry.find instanceof RegExp && entry.find.test(specifier))),
);
}
if (typeof aliasConfig === 'object' && aliasConfig !== null && specifier in aliasConfig) {
return {
find: specifier,
replacement: String((aliasConfig as Record<string, unknown>)[specifier]),
};
}
return undefined;
};
const getAlias = (aliasConfig: unknown, specifier: string): string | undefined => {
const aliasEntry = getAliasEntry(aliasConfig, specifier);
return aliasEntry && typeof aliasEntry === 'object' && 'replacement' in aliasEntry ? String(aliasEntry.replacement) : undefined;
};
const repoRoot = fileURLToPath(new URL('../..', import.meta.url));
const getExpectedEntry = (hoistedRelativePath: string, pnpmPattern: string) => {
let currentRoot = repoRoot;
while (true) {
const hoistedEntry = resolve(currentRoot, hoistedRelativePath);
if (existsSync(hoistedEntry)) return hoistedEntry;
const [pnpmEntry] = globSync(resolve(currentRoot, pnpmPattern));
if (pnpmEntry) return pnpmEntry;
const parentRoot = dirname(currentRoot);
if (parentRoot === currentRoot) break;
currentRoot = parentRoot;
}
return undefined;
};
describe('docs vitepress config', () => {
it('ships copied main-app favicon assets for docs', () => {
expect(existsSync(resolve(repoRoot, 'docs/public/favicon.png'))).toBe(true);
expect(existsSync(resolve(repoRoot, 'docs/public/favicon-64.png'))).toBe(true);
expect(existsSync(resolve(repoRoot, 'docs/public/favicon.ico'))).toBe(true);
});
it('ships a fallback favicon.ico for browsers that probe the default path', () => {
expect(existsSync(resolve(repoRoot, 'docs/public/favicon.ico'))).toBe(true);
});
it('declares the main-app favicon assets in docs head tags', () => {
const iconLinks =
config.head?.filter(
(entry) =>
entry[0] === 'link' &&
typeof entry[1] === 'object' &&
entry[1] !== null &&
'rel' in entry[1] &&
(entry[1].rel === 'icon' || entry[1].rel === 'shortcut icon'),
) ?? [];
expect(iconLinks.some((entry) => typeof entry[1] === 'object' && entry[1] !== null && 'href' in entry[1] && entry[1].href === '/favicon.png')).toBe(true);
expect(iconLinks.some((entry) => typeof entry[1] === 'object' && entry[1] !== null && 'href' in entry[1] && entry[1].href === '/favicon-64.png')).toBe(true);
expect(iconLinks.some((entry) => typeof entry[1] === 'object' && entry[1] !== null && 'href' in entry[1] && entry[1].href === '/favicon.ico')).toBe(true);
});
it('aliases dayjs to the ESM entry for mermaid browser compatibility', () => {
const aliasConfig = config.vite?.resolve?.alias;
const alias = getAlias(aliasConfig, 'dayjs');
const aliasEntry = getAliasEntry(aliasConfig, 'dayjs');
expect(alias).toBe(getExpectedEntry('node_modules/dayjs/esm/index.js', 'node_modules/.pnpm/dayjs@*/node_modules/dayjs/esm/index.js'));
expect(alias && existsSync(alias)).toBe(true);
expect(Array.isArray(aliasConfig)).toBe(true);
expect(aliasEntry && typeof aliasEntry === 'object' && 'find' in aliasEntry && aliasEntry.find instanceof RegExp).toBe(true);
expect(aliasEntry && typeof aliasEntry === 'object' && 'find' in aliasEntry && aliasEntry.find instanceof RegExp && aliasEntry.find.test('dayjs/plugin/duration.js')).toBe(false);
});
it('aliases sanitize-url to a browser-loadable source entry', () => {
const alias = getAlias(config.vite?.resolve?.alias, '@braintree/sanitize-url');
expect(alias).toBe(
getExpectedEntry(
'node_modules/@braintree/sanitize-url/src/index.ts',
'node_modules/.pnpm/@braintree+sanitize-url@*/node_modules/@braintree/sanitize-url/src/index.ts',
),
);
expect(alias && existsSync(alias)).toBe(true);
});
});
-123
View File
@@ -1,123 +0,0 @@
import { existsSync, globSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { defineConfig } from 'vitepress';
import { withMermaid } from 'vitepress-plugin-mermaid';
const repoRoot = fileURLToPath(new URL('../..', import.meta.url));
function resolveDependencyEntry(hoistedRelativePath: string, pnpmPattern: string) {
let currentRoot = repoRoot;
while (true) {
const hoistedEntry = resolve(currentRoot, hoistedRelativePath);
if (existsSync(hoistedEntry)) return hoistedEntry;
const [pnpmEntry] = globSync(resolve(currentRoot, pnpmPattern));
if (pnpmEntry) return pnpmEntry;
const parentRoot = dirname(currentRoot);
if (parentRoot === currentRoot) break;
currentRoot = parentRoot;
}
return undefined;
}
const dayjsEsmEntry = resolveDependencyEntry(
'node_modules/dayjs/esm/index.js',
'node_modules/.pnpm/dayjs@*/node_modules/dayjs/esm/index.js',
);
const sanitizeUrlSourceEntry = resolveDependencyEntry(
'node_modules/@braintree/sanitize-url/src/index.ts',
'node_modules/.pnpm/@braintree+sanitize-url@*/node_modules/@braintree/sanitize-url/src/index.ts',
);
if (!dayjsEsmEntry) {
throw new Error('Unable to resolve the dayjs ESM entry required by vitepress-plugin-mermaid.');
}
if (!sanitizeUrlSourceEntry) {
throw new Error('Unable to resolve the sanitize-url source entry required by vitepress-plugin-mermaid.');
}
export default withMermaid(
defineConfig({
lang: 'zh-CN',
title: 'Metapi 文档',
description: 'Metapi 使用文档、FAQ 与维护协作指南',
head: [
['link', { rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicon.png' }],
['link', { rel: 'icon', type: 'image/png', sizes: '64x64', href: '/favicon-64.png' }],
['link', { rel: 'shortcut icon', href: '/favicon.ico' }],
],
cleanUrls: true,
lastUpdated: true,
ignoreDeadLinks: true,
vite: {
resolve: {
alias: [
{ find: /^dayjs$/, replacement: dayjsEsmEntry },
{ find: /^@braintree\/sanitize-url$/, replacement: sanitizeUrlSourceEntry },
],
},
},
themeConfig: {
siteTitle: 'Metapi Docs',
logo: '/logos/logo-icon-512.png',
nav: [
{ text: '首页', link: '/' },
{ text: '快速上手', link: '/getting-started' },
{ text: '上游接入', link: '/upstream-integration' },
{ text: 'OAuth 管理', link: '/oauth' },
{ text: 'FAQ', link: '/faq' },
{ text: '文档维护', link: '/README' },
{ text: '项目主页', link: 'https://github.com/cita-777/boosapi' },
],
sidebar: [
{
text: '开始',
items: [
{ text: '文档首页', link: '/' },
{ text: '快速上手', link: '/getting-started' },
{ text: '部署指南', link: '/deployment' },
],
},
{
text: '使用与运维',
items: [
{ text: '上游接入', link: '/upstream-integration' },
{ text: 'OAuth 管理', link: '/oauth' },
{ text: '配置说明', link: '/configuration' },
{ text: 'K3s 更新中心(高级)', link: '/k3s-update-center' },
{ text: '客户端接入', link: '/client-integration' },
{ text: '管理 API', link: '/management-api' },
{ text: '运维手册', link: '/operations' },
{ text: '常见问题 FAQ', link: '/faq' },
],
},
{
text: '文档维护',
items: [
{ text: '文档维护与贡献', link: '/README' },
{ text: '目录规范', link: '/project-structure' },
{ text: 'FAQ/教程贡献规范', link: '/community/faq-tutorial-guidelines' },
],
},
],
socialLinks: [
{ icon: 'github', link: 'https://github.com/cita-777/boosapi' },
],
outline: {
level: [2, 3],
},
footer: {
message: 'MIT Licensed',
copyright: 'Copyright (c) 2026 Metapi Contributors',
},
search: {
provider: 'local',
},
},
}),
);
-64
View File
@@ -1,64 +0,0 @@
# 文档维护与贡献
> `/README` 路由为兼容旧链接与维护工作流而保留。面向所有用户的公开入口始终是 [文档首页](/)。
## 这页适合谁
- 文档维护者:调整导航、重构内容结构、检查首页是否仍然聚焦对外说明。
- FAQ / 教程贡献者:补充排障经验、接入说明、案例沉淀。
- 功能发布或运维负责人:在能力变更后同步校正文档入口、截图和索引。
## 文档站运行
本地预览:
```bash
npm run docs:dev
```
构建静态站点:
```bash
npm run docs:build
```
## 内容地图
| 内容领域 | 首选页面 | 什么时候改这里 |
|------|--------|------------|
| 对外第一印象、产品定位、核心入口 | [文档首页](/) | 需要调整公开落地页信息架构、首页 CTA 或首屏导航时 |
| 新用户部署与首条请求 | [快速上手](./getting-started.md) | 新安装流程、默认端口、首次调用步骤变化时 |
| 上游平台选择与接法 | [上游接入](./upstream-integration.md) | 平台支持范围、官方预设、API 请求地址池、自动识别规则变化时 |
| Provider OAuth 授权 | [OAuth 管理](./oauth.md) | 支持的 OAuth provider、授权流程、回调方式或自动重绑能力变化时 |
| 生产部署与回滚 | [部署指南](./deployment.md) | Docker Compose、反向代理、升级回滚策略变更时 |
| K3s / Helm 高级升级面板 | [K3s 更新中心(高级)](./k3s-update-center.md) | 需要说明谁适合使用更新中心、helper 怎么配、K3s/Helm 发布链路怎么接入时 |
| 环境变量、参数和配置项 | [配置说明](./configuration.md) | 设置页 / 通知设置 / 下游密钥这些 UI 入口变化,或仅剩 env-only 的部署级参数变化时 |
| 客户端与工具接入 | [客户端接入](./client-integration.md) | Open WebUI、Cherry Studio、Cursor 等接入方式变化时 |
| 管理后台脚本化调用 | [管理 API](./management-api.md) | 需要说明如何用脚本批量导入站点/账号,或对接外部自动化时 |
| 运维排障与日常维护 | [运维手册](./operations.md) / [常见问题](./faq.md) | 新排障案例、备份恢复、健康检查、典型报错变化时 |
| FAQ / 教程协作沉淀 | [FAQ/教程贡献规范](./community/faq-tutorial-guidelines.md) | 需要新增教程、FAQ 模板、内容提交流程时 |
| 仓库目录与组织约定 | [目录规范](./project-structure.md) | 目录结构、归档策略或命名约定变化时 |
| 工程守则与漂移治理 | [Harness Engineering](./engineering/harness-engineering.md) | 需要更新仓库级黄金原则、自动巡检范围或垃圾回收流程时 |
## 维护约定
- [文档首页](/) 是唯一公共落地页。不要在 `/README` 或其他主题页重新堆一份 `文档中心总览`。
- 优先补强现有页面,再决定是否新增页面。只有在读者对象、内容边界和长期维护者都明确时再扩展路由。
- 导航、侧边栏和页面标题应反映公开入口和维护入口的分工,避免两个首页争夺注意力。
- 内链优先使用站内 clean URL 形式,保证 VitePress 构建结果和部署路径一致。
- 涉及界面、架构或流程变化时,同步检查首页截图、架构图和相关说明是否仍然匹配当前产品。
## 更新前自检
1. 先确认目标读者是谁,以及内容该落在哪个现有页面。
2. 如果入口结构变了,同步更新 `docs/.vitepress/config.ts` 的 `nav` 和 `sidebar`。
3. 检查新增或修改的内链是否还能在站内自然发现,不要把关键入口只藏在单个页面里。
4. 运行 `npm run docs:build`,确认没有引入构建错误,并额外抽查关键入口与内链。
## 相关入口
- [FAQ/教程贡献规范](./community/faq-tutorial-guidelines.md)
- [返回文档首页](/)
- [项目贡献流程](https://github.com/cita-777/metapi/blob/main/CONTRIBUTING.md)
- [安全策略](https://github.com/cita-777/metapi/blob/main/SECURITY.md)
- [行为准则](https://github.com/cita-777/metapi/blob/main/CODE_OF_CONDUCT.md)
-188
View File
@@ -1,188 +0,0 @@
# 🔌 客户端接入
本文档说明如何将下游客户端连接到 Metapi 代理网关。
[返回文档中心](./README.md)
---
## 通用配置
Metapi 暴露标准 OpenAI / Claude 兼容接口,下游客户端通常只需配置两项:
| 配置项 | 值 |
|--------|-----|
| **Base URL** | 按客户端字段行为填写:会自动补 `/v1` 的字段填 `https://your-domain.com`;要求完整 API URL 的字段填 `https://your-domain.com/v1` |
| **API Key** | 你设置的 `PROXY_TOKEN` 值,或创建的[下游 API Key](./configuration.md#下游-api-key-策略) |
模型列表自动从 `GET /v1/models` 获取,无需手动配置。
> [!TIP]
> 如果不确定客户端是否自动补 `/v1`,先试根域名,报 404 再改成带 `/v1` 的完整路径。
## 支持的接口
| 接口 | 方法 | 说明 |
|------|------|------|
| `/v1/responses` | POST | OpenAI Responses |
| `/v1/chat/completions` | POST | OpenAI Chat Completions |
| `/v1/messages` | POST | Claude Messages |
| `/v1/completions` | POST | OpenAI Completions(Legacy) |
| `/v1/embeddings` | POST | 向量嵌入 |
| `/v1/files` | POST / GET | OpenAI Files 上传与列举 |
| `/v1/files/:fileId` | GET / DELETE | OpenAI Files 读取元信息与删除 |
| `/v1/files/:fileId/content` | GET | OpenAI Files 原始内容读取 |
| `/v1/images/generations` | POST | 图像生成 |
| `/v1/models` | GET | 模型列表 |
## 已验证兼容的客户端
### ChatGPT-Next-Web
| 配置项 | 值 |
|--------|-----|
| Settings → Custom Endpoint | `https://your-domain.com`(该字段会自动访问 `/v1/*`) |
| API Key | `PROXY_TOKEN` |
### Open WebUI
| 配置项 | 值 |
|--------|-----|
| Settings → Connections → OpenAI API URL | `https://your-domain.com/v1` |
| API Key | `PROXY_TOKEN` |
### Cherry Studio
| 配置项 | 值 |
|--------|-----|
| 模型提供商 → OpenAI → API 地址 | `https://your-domain.com/v1` |
| API Key | `PROXY_TOKEN` |
> 说明:Metapi 现在支持标准 OpenAI `/v1/files` 文件链路。
> Cherry Studio、Open WebUI 等客户端如果通过标准 OpenAI Files + `Responses` / Chat 文件块发送 PDF、Markdown、JSON、图片、音频附件,即可经由 Metapi 转发。
> 如果某个客户端版本仍使用私有附件协议而不是标准 `/v1/files`,则仍需单独适配。
### Cursor
| 配置项 | 值 |
|--------|-----|
| Settings → Models → OpenAI API Key | `PROXY_TOKEN` |
| Override OpenAI Base URL | `https://your-domain.com/v1` |
### Claude Code
在 `~/.claude/settings.json` 中配置:
```json
{
"env": {
"ANTHROPIC_BASE_URL": "https://your-domain.com",
"ANTHROPIC_API_KEY": "your-proxy-sk-token",
"ANTHROPIC_AUTH_TOKEN": "your-proxy-sk-token",
"CLAUDE_CODE_ATTRIBUTION_HEADER": "0",
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"
}
}
```
**环境变量说明:**
| 变量 | 作用 |
|------|------|
| `ANTHROPIC_BASE_URL` | 指向 Metapi 的根域名,Claude Code 会自动拼接 `/v1/messages` |
| `ANTHROPIC_API_KEY` | 认证令牌,填 `PROXY_TOKEN` 值 |
| `ANTHROPIC_AUTH_TOKEN` | 部分版本读取此变量而非 `ANTHROPIC_API_KEY`,建议两个都设 |
| `CLAUDE_CODE_ATTRIBUTION_HEADER` | 设为 `0` 禁用 Attribution 头,避免部分上游不支持该头导致报错 |
| `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` | 设为 `1` 减少非必要请求(遥测等),降低 Token 消耗 |
> [!IMPORTANT]
> `ANTHROPIC_BASE_URL` 填根域名即可,**不要**手动拼接 `/v1`。
> 上述变量由 Claude Code 客户端读取,属于客户端行为开关;Metapi 服务端只能处理已经发来的请求。
### Codex CLI
`~/.codex/config.toml`
```toml
model = "gpt-5"
model_provider = "boosapi"
[model_providers.boosapi]
name = "boosapi"
base_url = "https://your-domain.com/v1"
```
`~/.codex/auth.json`
```json
{
"OPENAI_API_KEY": "your-proxy-sk-token"
}
```
> 提示:`model` 需要是你在 Metapi `GET /v1/models` 可见的模型名。
### Roo Code / Kilo Code
配置方式与 Cursor 类似,在设置中填入 `https://your-domain.com/v1` 和 API Key。
### 其他客户端
所有支持 OpenAI API 格式的客户端均可接入,只需找到 Base URL 和 API Key 的配置位置即可:
| 你的客户端... | 填法 |
|--------------|------|
| 有 "Base URL" 字段,会自动补 `/v1` | 填 `https://your-domain.com` |
| 有 "OpenAI API URL" 字段,要求完整路径 | 填 `https://your-domain.com/v1` |
| 有 "Anthropic" 选项 | 填 `https://your-domain.com`,Metapi 自动处理 `/v1/messages` |
| 只能填 API Key | 填 `PROXY_TOKEN` 值即可 |
> [!TIP]
> 不确定选哪种?先用 `/v1/models` 测试连通性,再配置模型。
## 下游 API Key 策略
除了全局 `PROXY_TOKEN`,你还可以创建多个项目级下游 Key,每个 Key 可独立配置过期时间、费用/请求上限、模型白名单、路由白名单和站点倍率,适用于多团队/多项目共用一个 Metapi 实例的场景。
详细字段说明见 [配置说明 → 下游 API Key 策略](./configuration.md#下游-api-key-策略)。
---
## 快速自检
部署完成后,用以下命令验证链路:
```bash
# 1. 检查模型列表
curl -sS https://your-domain.com/v1/models \
-H "Authorization: Bearer <PROXY_TOKEN>" | head -50
# 2. 测试对话(非流式)
curl -sS https://your-domain.com/v1/chat/completions \
-H "Authorization: Bearer <PROXY_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hi"}]}'
# 3. 测试文件上传
curl -sS https://your-domain.com/v1/files \
-H "Authorization: Bearer <PROXY_TOKEN>" \
-F "purpose=assistants" \
-F "file=@./sample.pdf"
# 4. 测试流式
curl -sS https://your-domain.com/v1/chat/completions \
-H "Authorization: Bearer <PROXY_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hi"}],"stream":true}'
```
## 常见问题
遇到流式响应异常、模型列表为空、客户端提示 401/403 等问题,请参阅 [常见问题 FAQ](./faq.md)。
## 下一步
- [配置说明](./configuration.md) — 环境变量详解与下游 API Key 策略
- [上游接入](./upstream-integration.md) — 添加和管理上游平台
- [运维手册](./operations.md) — 日志排查与健康检查
- [常见问题](./faq.md) — 更多故障排查
-110
View File
@@ -1,110 +0,0 @@
# FAQ / 教程贡献规范
> 目标:让任何人都可以提交可复用、可检索、可维护的经验内容,避免口口相传丢失。
## 适用范围
- FAQ(常见问题与报错处理)
- 使用教程(从 0 到 1、进阶技巧、集成示例)
- 运维经验(排障、监控、升级、回滚)
## 维护元数据(必填)
所有 FAQ / 教程内容都应在标题下方补一段维护元数据:
```md
> owner: @github-id / team
> audience: 新手 / 运维 / 集成开发者
> last verified: YYYY-MM-DD
> obsolete: false
```
- `owner`:后续负责校对、修订、答疑的人或团队。
- `audience`:目标读者,避免把新手教程写成排障手册,或把运维经验写成泛泛介绍。
- `last verified`:最近一次按文档步骤、截图或行为重新核对的日期。
- `obsolete`:默认写 `false`;如果内容已失效,改为 `true`,并在正文开头补替代方案、迁移路径或删除计划。
## FAQ 模板
复制以下结构提交:
```md
## 问题标题(简短、可搜索)
> owner: @github-id / team
> audience: 新手 / 运维 / 集成开发者
> last verified: YYYY-MM-DD
> obsolete: false
### 现象
- 具体报错信息:
- 出现场景:
### 根因
- 触发原因 1
- 触发原因 2
### 解决步骤
1. 步骤一
2. 步骤二
3. 验证结果
### 预防建议
- 建议一
- 建议二
```
## 教程模板
复制以下结构提交:
```md
# 教程标题
> owner: @github-id / team
> audience: 新手 / 运维 / 集成开发者
> last verified: YYYY-MM-DD
> obsolete: false
## 适用人群
- 新手 / 运维 / 集成开发者
## 前置条件
- 条件一
- 条件二
## 操作步骤
1. ...
2. ...
3. ...
## 验证方式
- 成功标准 1
- 成功标准 2
## 常见坑位
- 坑位 1 与规避方式
- 坑位 2 与规避方式
```
## 命名与归档建议
- FAQ 统一追加到 [`/docs/faq.md`](../faq.md)
- 教程建议新增 `docs/community/tutorial-<topic>.md`
- 文件名使用小写英文与 `-`,例如:`tutorial-openwebui.md`
## 提交要求
- 标题可检索:包含关键词(平台名、报错码、功能名)
- 步骤可复现:避免 `试一下`、`差不多` 这类描述
- 版本要明确:注明适用版本、部署方式
- 结果可验证:给出成功判定标准
- 元数据完整:`owner / audience / last verified / obsolete` 四项缺一不可
## 维护原则
- 过时内容要标记 `已失效` 并给替代方案
- 同类问题合并,避免 FAQ 膨胀重复
- 优先给最短修复路径,再补原理解释
- 正文有实质更新时,同步刷新 `last verified`
- 负责人变更时,及时更新 `owner`,不要留下无人维护的教程
-349
View File
@@ -1,349 +0,0 @@
# ⚙️ 配置说明
本文档按实际使用场景说明 Metapi 的配置入口。
对大多数用户来说,日常配置优先通过管理后台完成;环境变量主要用于首次启动、部署级参数和当前没有 UI 的高级项。
[返回文档中心](./README.md)
---
## 概述
Metapi 当前有三类主要配置入口:
1. **管理后台「设置」** — 适合日常系统设置与运行时调整
2. **管理后台「通知设置」与「下游密钥」** — 适合通知渠道和项目级下游 Key 管理
3. **环境变量** — 适合首次启动、部署级参数、OAuth client 覆盖、Deploy Helper token 等当前没有 UI 的项
下表可用于快速判断:
| 你要改什么 | 优先去哪里 | 说明 |
|----------|------------|------|
| 日常系统设置 | 管理后台「设置」 | 大部分运行时配置都在这里,保存后直接生效或按提示重启 |
| 通知渠道 | 管理后台「通知设置」 | Webhook / Bark / Server酱 / Telegram / SMTP 都有 UI |
| 下游项目级 Key | 管理后台「下游密钥」 | 不要再回到环境变量里硬塞 |
| 首次启动令牌、端口、数据目录 | `.env` / 容器环境变量 | 这类属于部署级初始化 |
| OAuth 客户端 ID / Secret | `.env` / 容器环境变量 | 当前没有 UI |
| Deploy Helper token / helper 进程参数 | `.env` / helper manifest | 当前没有 UI,且属于集群侧部署参数 |
| 少数高级部署级参数 | `.env` / 容器环境变量 | 例如日志保留、部分探测细粒度参数 |
---
## 配置入口总览
### 1. 管理后台「设置」
侧边栏入口:**系统 → 设置**
当前已经能直接在这里配置的内容包括:
| UI 项 | 对应能力 | 生效方式 |
|------|----------|----------|
| 管理员登录令牌 | `AUTH_TOKEN` 的后续修改 | 保存后即时生效 |
| 定时任务 | `CHECKIN_CRON`、`BALANCE_REFRESH_CRON`、日志清理计划 | 保存后即时生效 |
| 系统代理 | `SYSTEM_PROXY_URL` | 保存后即时生效 |
| 代理失败判定 | 失败关键词、空内容失败判定 | 保存后即时生效 |
| Codex 上游传输与会话并发 | WebSocket 开关、并发与队列参数 | 保存后即时生效 |
| 批量测活 | 后台模型可用性探测开关 | 保存后即时生效 |
| 下游访问令牌 | `PROXY_TOKEN` | 保存后即时生效 |
| 路由策略 | 成本/余额/使用率权重、默认单价、首字超时、协议回退、失败冷却上限 | 保存后即时生效 |
| 全局品牌屏蔽 | 全局品牌屏蔽 | 保存后即时生效,并触发路由重建 |
| 全局模型白名单 | 全局模型白名单 | 保存后即时生效,并触发路由重建 |
| 数据库迁移 / 运行数据库 | `DB_TYPE`、`DB_URL`、`DB_SSL` | 保存后下次后端重启生效 |
| 更新中心 | K3s / Helm 更新中心配置 | 保存后即时生效 |
| 会话与安全 | `ADMIN_IP_ALLOWLIST` | 保存后即时生效 |
> [!TIP]
> `AUTH_TOKEN` 和 `PROXY_TOKEN` 并不是“只能靠环境变量改”的配置。
> 正常情况下,**首次启动先给一个值,后续都可以在 UI 里改**。
### 2. 管理后台「通知设置」
侧边栏入口:**系统 → 通知设置**
当前已经有独立页面可直接配置:
| UI 项 | 说明 | 生效方式 |
|------|------|----------|
| Webhook | 企业微信 / 飞书 / 通用 Webhook | 保存后即时生效 |
| Bark | Bark 推送地址与开关 | 保存后即时生效 |
| Server酱 | SendKey 与开关 | 保存后即时生效 |
| Telegram | API Base URL、Chat ID、Topic ID、Bot Token、是否走系统代理 | 保存后即时生效 |
| SMTP | SMTP 主机、端口、账号、密码、发件/收件地址 | 保存后即时生效 |
| 告警冷静期 | `NOTIFY_COOLDOWN_SEC` | 保存后即时生效 |
通知设置页面已经支持:
- 直接保存
- 直接发测试通知
- 屏蔽回显已保存的敏感字段
通知配置可直接在该页面完成,无需先记环境变量名。
### 3. 管理后台「下游密钥」
侧边栏入口:**控制台 → 下游密钥**
「下游密钥」页面负责的是项目级下游 API Key,而不是全局 `PROXY_TOKEN`。
适合在这里配置的内容:
- Key 名称
- 过期时间
- 费用 / 请求上限
- 模型白名单
- 路由白名单
- 站点权重倍率
- 启停、重置用量、趋势与统计
这类能力可直接在页面里完成,不需要额外依赖环境变量。
---
## 首次启动时,至少准备这些环境变量
**首次把服务跑起来**时,建议先在环境变量里准备以下几项:
| 变量名 | 说明 | 默认值 |
|--------|------|--------|
| `AUTH_TOKEN` | 初始管理员登录令牌 | `change-me-admin-token` |
| `PROXY_TOKEN` | 初始下游访问令牌 | `change-me-proxy-sk-token` |
| `PORT` | 服务监听端口 | `4000` |
| `DATA_DIR` | 数据目录(SQLite 默认落这里) | `./data` |
| `TZ` | 时区 | `Asia/Shanghai` |
说明:
- `AUTH_TOKEN` 只是**第一次登录前**必须要有;登录后可以去「设置」里改。
- `PROXY_TOKEN` 也只是建议先给一个初始值;后续可以在「设置」里改。
- `PORT`、`DATA_DIR`、`TZ` 这类属于部署级参数,更适合留在环境变量。
---
## 环境变量配置
### 1. 启动与部署级
这类配置要么属于进程启动参数,要么属于当前确实没有 UI 的部署项:
| 变量名 | 说明 | 默认值 |
|--------|------|--------|
| `PORT` | 服务监听端口 | `4000` |
| `DATA_DIR` | 数据目录(SQLite 数据库存储位置) | `./data` |
| `TZ` | 时区 | `Asia/Shanghai` |
| `ACCOUNT_CREDENTIAL_SECRET` | 账号凭证加密密钥(用于加密存储的上游账号密码) | 默认使用 `AUTH_TOKEN` |
### 2. OAuth 与 Provider 登录
这一节只在你需要覆盖默认 OAuth client 配置时才看。
| 变量名 | 说明 | 默认值 |
|--------|------|--------|
| `CODEX_CLIENT_ID` | 覆盖内置 Codex OAuth Client ID | 内置默认值 |
| `CLAUDE_CLIENT_ID` | 覆盖内置 Claude OAuth Client ID | 内置默认值 |
| `CLAUDE_CLIENT_SECRET` | 预留的 Claude OAuth Client Secret(默认留空) | 空 |
| `GEMINI_CLI_CLIENT_ID` | 覆盖内置 Gemini CLI OAuth Client ID | 内置默认值 |
| `GEMINI_CLI_CLIENT_SECRET` | 覆盖内置 Gemini CLI OAuth Client Secret | 内置默认值 |
说明:
- `Antigravity` 当前不需要额外环境变量即可启用。
- 如果你的部署环境访问 provider 受限,优先先在 UI 里配置**系统代理**。
- 如果 OAuth 页面运行在远程服务器上,还要考虑 SSH 隧道或手动回填 callback,详见 [OAuth 管理](./oauth.md)。
### 3. K3s 更新中心与 Deploy Helper
这里要分清楚两层:
- **主 Metapi 后台里的日常更新中心配置**:优先在 UI 里填
- **主服务访问 helper 的 token / helper 自己的监听参数**:仍然是环境变量
#### 主 Metapi 服务
| 变量名 | 说明 | 默认值 |
|--------|------|--------|
| `DEPLOY_HELPER_TOKEN` | 主服务访问 Deploy Helper 的 Bearer Token | 空 |
| `UPDATE_CENTER_HELPER_TOKEN` | `DEPLOY_HELPER_TOKEN` 的兼容别名,二选一即可 | 空 |
#### Deploy Helper 服务
| 变量名 | 说明 | 默认值 |
|--------|------|--------|
| `DEPLOY_HELPER_HOST` | helper 监听地址 | `0.0.0.0` |
| `DEPLOY_HELPER_PORT` | helper 监听端口 | `9850` |
| `DEPLOY_HELPER_TOKEN` | helper Bearer Token,必须和主服务一致 | 空 |
#### 更新中心里真正建议在 UI 配的字段
这些字段不建议再教用户去改 env,而是直接去:
**设置 → 更新中心**
- `helperBaseUrl`
- `namespace`
- `releaseName`
- `chartRef`
- `imageRepository`
- `githubReleasesEnabled`
- `dockerHubTagsEnabled`
- `defaultDeploySource`
完整接入步骤见 [K3s 更新中心(高级)](./k3s-update-center.md)。
### 4. 当前没有 UI 的高级部署级参数
下面这些参数目前更偏部署级,仍然建议通过环境变量维护:
| 变量名 | 说明 | 默认值 |
|--------|------|--------|
| `TOKEN_ROUTER_CACHE_TTL_MS` | Token 路由缓存 TTL(毫秒) | `1500` |
| `PROXY_LOG_RETENTION_DAYS` | 代理日志保留天数 | `30` |
| `PROXY_LOG_RETENTION_PRUNE_INTERVAL_MINUTES` | 代理日志清理任务执行间隔(分钟) | `30` |
| `MODEL_AVAILABILITY_PROBE_INTERVAL_MS` | 批量测活间隔(毫秒) | `1800000` |
| `MODEL_AVAILABILITY_PROBE_TIMEOUT_MS` | 批量测活单次探测超时(毫秒) | `15000` |
| `MODEL_AVAILABILITY_PROBE_CONCURRENCY` | 批量测活并发数 | `1` |
注意:
- **批量测活开关本身**已经在 UI 里有了
- 这里只剩下间隔、超时、并发这些更高级的细项还没有 UI
---
## 常见配置与入口对照
### 通常已经有 UI 的配置
- 管理员令牌
- 下游访问令牌
- 系统代理
- 定时任务
- 路由策略
- 批量测活开关
- 安全白名单
- 通知渠道
- 下游密钥
- 数据库运行配置
- 更新中心主体配置
### 通常仍需看环境变量的配置
- 端口
- 数据目录
- 时区
- 账号凭证加密密钥
- OAuth client 覆盖
- Deploy Helper token
- helper 进程自身监听参数
- 少数高级部署级性能 / 清理参数
---
## UI 与环境变量的关系
Metapi 当前的配置关系可以概括为:
1. **环境变量负责启动默认值和部署参数**
2. **UI 负责用户日常操作和运行时调整**
3. **UI 保存后的值会持久化到当前运行数据库**
4. **大多数 UI 设置会覆盖原始默认值**
例外主要有两类:
- **纯部署级参数**:例如端口、数据目录
- **保存后需重启的配置**:例如运行数据库类型 / 连接串 / SSL
---
## 通知渠道详细说明
虽然我更推荐直接去「通知设置」页面,但为了方便查字段,这里保留一个速查表。
### Webhook
| UI / 变量 | 说明 | 默认值 |
|--------|------|--------|
| `WEBHOOK_ENABLED` | 启用 Webhook 通知 | `true` |
| `WEBHOOK_URL` | Webhook 推送地址 | 空 |
### Bark(iOS 推送)
| UI / 变量 | 说明 | 默认值 |
|--------|------|--------|
| `BARK_ENABLED` | 启用 Bark 推送 | `true` |
| `BARK_URL` | Bark 推送地址 | 空 |
### Server酱
| UI / 变量 | 说明 | 默认值 |
|--------|------|--------|
| `SERVERCHAN_ENABLED` | 启用 Server酱 通知 | `true` |
| `SERVERCHAN_KEY` | Server酱 SendKey | 空 |
### Telegram Bot
| UI / 变量 | 说明 | 默认值 |
|--------|------|--------|
| `TELEGRAM_ENABLED` | 启用 Telegram 通知 | `false` |
| `TELEGRAM_BOT_TOKEN` | Telegram Bot Token(形如 `123456:abc`) | 空 |
| `TELEGRAM_CHAT_ID` | 接收消息的 Chat ID(如 `-100xxxx` 或 `@channel`) | 空 |
**配置步骤:**
1. **创建 Bot**:在 Telegram 中搜索 [@BotFather](https://t.me/BotFather),发送 `/newbot`,按提示设置名称后获取 Bot Token
2. **获取 Chat ID**:
- 个人聊天:给 Bot 发消息后,通过 `getUpdates` 或 @userinfobot / @getmyid_bot 查看 `chat.id`
- 群组:把 Bot 拉进群并发送消息后,通过 `getUpdates` 查看群组 Chat ID
- 频道:可直接使用 `@your_channel`(前提是 Bot 是频道管理员)
3. **填入位置**:优先去 **通知设置** 页面填写
4. **大陆服务器反代**:如果服务器不能直连 Telegram,可在 UI 里填写 `Telegram API Base URL`
5. **测试**:保存后直接点“发送测试通知”
### SMTP 邮件
| UI / 变量 | 说明 | 默认值 |
|--------|------|--------|
| `SMTP_ENABLED` | 启用邮件通知 | `false` |
| `SMTP_HOST` | SMTP 服务器地址 | 空 |
| `SMTP_PORT` | SMTP 端口 | `587` |
| `SMTP_SECURE` | 使用 SSL/TLS | `false` |
| `SMTP_USER` | SMTP 用户名 | 空 |
| `SMTP_PASS` | SMTP 密码 | 空 |
| `SMTP_FROM` | 发件人地址 | 空 |
| `SMTP_TO` | 收件人地址 | 空 |
### 告警控制
| UI / 变量 | 说明 | 默认值 |
|--------|------|--------|
| `NOTIFY_COOLDOWN_SEC` | 相同告警冷静期(秒),防止同一事件重复通知 | `300` |
---
## 站点公告
管理后台新增了「站点公告」页面,用于保存和浏览 Metapi 已同步到本地的上游公告记录。
- 首次发现的上游公告会写入站内通知,并按现有通知渠道外发一次
- 后续重复同步只更新本地公告记录,不会重复外发同一条公告
- 当前支持的上游公告来源包括 `new-api`、`done-hub` 与 `sub2api`
- 「清空公告」只删除 Metapi 本地保存的公告记录,不会修改上游站点数据
## 更新提醒
更新中心现在会在后台定时检查 GitHub Releases / Docker Hub 的可部署候选,并把结果保存为本地运行时状态。
- 首次发现新的版本候选或新的 Docker digest 时,会写入站内通知,并按现有通知渠道外发一次
- 相同候选后续重复检查只更新本地运行时状态,不会重复外发同一条提醒
- 这类提醒不会自动触发部署,只是把用户带到「设置 → 更新中心」继续手动确认和执行
- K3s 用户可以在收到提醒后直接去更新中心部署;Compose 用户也可以收到提醒,但仍按自己的升级方式处理
## 下一步
- [部署指南](./deployment.md) — Docker Compose 与反向代理
- [K3s 更新中心(高级)](./k3s-update-center.md) — K3s / Helm 用户的后台升级入口
- [客户端接入](./client-integration.md) — 对接下游应用
- [上游接入](./upstream-integration.md) — 添加和管理上游平台
- [OAuth 管理](./oauth.md) — 授权 Codex / Claude / Gemini CLI / Antigravity
- [运维手册](./operations.md) — 备份、日志与健康检查
-530
View File
@@ -1,530 +0,0 @@
# 🚢 部署指南
[返回文档中心](./README.md)
---
## 支持的运行方式
| 场景 | 推荐方式 | 对外访问方式 | 数据位置 |
|------|----------|--------------|----------|
| 云服务器 / NAS / 家用主机长期运行 | Docker / Docker Compose / Zeabur | 固定服务地址,例如 `http://your-host:4000` 或反向代理域名 | 你挂载的 `DATA_DIR` / 持久化卷 |
| 免费云部署(24h 在线) | Render + TiDB + UptimeRobot | Render 分配的 `.onrender.com` 域名或自定义域名 | TiDB Serverless(外部 MySQL 数据库) |
| 个人电脑本地使用 | 桌面版安装包 | 桌面窗口;默认本机客户端直连使用 `http://127.0.0.1:4000`,局域网客户端可使用当前机器 IP + `4000`;如有覆盖则按 `BOOSAPI_DESKTOP_SERVER_PORT` 的实际端口访问 | Electron `app.getPath('userData')/data` |
| 二次开发 / 调试 | 本地开发 | 前端 `http://localhost:5173`,后端默认 `http://localhost:4000` | 仓库内 `./data` 或自定义 `DATA_DIR` |
> [!NOTE]
> - 当前不再提供 `Release` 压缩包 + Node.js 运行时的独立部署路径。
> - 生产/长期运行请用 Docker 系列方案;桌面版面向单机本地使用;源码运行请走本地开发流程。
## K3s / Helm 更新中心
如果你现在只是一个最普通的 Docker / Docker Compose 部署,请先跳过这节。
这套能力只适用于:
- 你已经在 K3s / Kubernetes 中部署了 BoosAPI
- 而且当前 BoosAPI 是通过 Helm release 管理的
它不适用于:
- 只有一个裸 Docker Compose 容器
- 想直接从管理后台更新外部 Docker 主机上的容器
但如果你是老用户,**正在计划从 Docker Compose 迁到 K3s / Helm,以获得滚动更新能力**,那么这一节和对应专题页是值得提前看的。它写的不是"怎么原地升级 Compose",而是"迁移完成后你会如何使用更新中心"。
如果你已经通过 Helm 在 K3s / Kubernetes 中部署 BoosAPI,并希望在管理后台中:
- 查看当前运行版本
- 检查 GitHub Releases / Docker Hub 的稳定版
- 通过集群内 helper 手动触发一次升级
请直接阅读:
- [K3s 更新中心](./k3s-update-center.md)
这页会单独说明 helper 部署、主服务 token 对齐、设置页字段含义、实际升级顺序和已知限制。
## Zeabur 一键部署
<a href="https://zeabur.com/templates/DOX5PR">
<img alt="Deploy on Zeabur" src="https://zeabur.com/button.svg" height="28">
</a>
点击按钮即可一键部署到 [Zeabur](https://zeabur.com),无需手动配置 Docker 或服务器。
模板会自动完成:
- 拉取 `1467078763/boosapi:latest` 镜像
- 配置 HTTP 端口(4000)
- 挂载持久化存储(`/app/data`)
- 分配域名
部署时需要填写以下变量:
| 变量 | 说明 |
|------|------|
| `AUTH_TOKEN` | 后台管理员登录令牌(请设置强密码) |
| `PROXY_TOKEN` | 下游客户端调用 `/v1/*` 时使用的 Bearer Token |
| `TZ` | 服务时区,影响定时任务和日志(如 `Asia/Shanghai`) |
| `PORT` | 内部监听端口(默认 `4000`,一般无需修改) |
部署完成后,通过 Zeabur 分配的域名访问后台管理面板即可。
---
## Render 一键部署(免费 24h 运行)
<a href="https://render.com/deploy?repo=https://github.com/cita-777/boosapi">
<img alt="Deploy to Render" src="https://render.com/images/deploy-to-render-button.svg" height="28">
</a>
通过 **Render + TiDB + UptimeRobot** 组合,可以实现 **完全免费的 24 小时持续运行**:
| 组件 | 作用 | 免费额度 |
|------|------|----------|
| [Render](https://render.com) | 运行 BoosAPI 容器 | Free Web Service(750 小时/月,闲置 15 分钟自动休眠) |
| [TiDB Serverless](https://tidbcloud.com) | MySQL 兼容数据库,替代 SQLite 实现数据持久化 | 5 GiB 存储 + 5000 万 Request Units/月 |
| [UptimeRobot](https://uptimerobot.com) | 每 5 分钟 ping 一次,防止 Render 免费实例休眠 | 50 个免费监控 |
> [!IMPORTANT]
> Render 免费版 **不支持持久化磁盘**,容器重启后本地文件会丢失。因此 **必须使用外部数据库**(推荐 TiDB Serverless),不能使用默认的 SQLite。
### 步骤 1:注册 TiDB Serverless 并获取连接串
1. 前往 [TiDB Cloud](https://tidbcloud.com) 注册账号
2. 创建一个 **Serverless** 集群(免费)
3. 在集群概览页点击 **Connect**,在弹出的面板中:
- **Connection Type**: Public
- **Database**: ⚠️ **必须改为 `test`**(默认是 `sys`,这是系统库,不允许建表!)
- 点击 **Generate Password** 生成密码并妥善保存
4. 使用面板中显示的参数拼接 `DB_URL`:
```
mysql://<USERNAME>:***@<HOST>:4000/test?ssl={"rejectUnauthorized":true}
```
> ⚠️ 注意:`<HOST>`、`<USERNAME>`、`<PASSWORD>` 从 Connect 面板中获取,**数据库名必须用 `test` 而非默认的 `sys`**。
> [!TIP]
> 这里只是以 TiDB 作为示例,你也可以使用其他提供免费额度的云数据库方案(如 Neon、Supabase 等),只需将 `DB_TYPE` 设为对应的 `mysql` 或 `postgres`,并填入正确的连接串即可。
### 步骤 2:部署到 Render
**方式一:一键部署(推荐)**
1. 点击上方 **Deploy to Render** 按钮
2. 如果你 Fork 了仓库,也可以使用你自己的仓库地址
3. 在 Render 界面中填写环境变量(见下表)
**方式二:手动创建**
1. 在 [Render Dashboard](https://dashboard.render.com) 点击 **New → Web Service**
2. 连接你的 GitHub 仓库(或使用公开仓库地址 `https://github.com/cita-777/boosapi`)
3. 配置:
- **Environment**: Docker
- **Dockerfile Path**: `./docker/Dockerfile`
- **Docker Build Context**: `.`(仓库根目录)
- **Instance Type**: Free
4. 添加环境变量(见下表)
### 环境变量配置(Render)
| 变量 | 说明 | 示例值 |
|------|------|--------|
| `AUTH_TOKEN` | 管理后台登录令牌(**必填**) | 你的强密码 |
| `PROXY_TOKEN` | 代理接口 Bearer Token(**必填**) | 你的代理密钥 |
| `DB_TYPE` | 数据库类型(**必填**) | `mysql` |
| `DB_URL` | TiDB 连接串(**必填**) | `mysql://user:***@host:4000/db?ssl=...` |
| `DB_SSL` | 启用 SSL 连接 | `true` |
| `TZ` | 时区 | `Asia/Shanghai` |
| `PORT` | 服务端口(默认即可) | `4000` |
### 步骤 3:配置 UptimeRobot 防休眠
Render 免费实例在 15 分钟无流量后会自动休眠。使用 UptimeRobot 定时 ping 可以保持实例 24h 在线:
1. 前往 [UptimeRobot](https://uptimerobot.com) 注册免费账号
2. 添加新监控:
- **Monitor Type**: HTTP(s)
- **URL**: `https://your-app.onrender.com`(替换为 Render 分配的域名)
- **Monitoring Interval**: 5 minutes
3. 保存即可,UptimeRobot 会每 5 分钟访问一次你的服务,防止休眠
> [!TIP]
> 部署完成后,通过 Render 分配的 `.onrender.com` 域名访问后台,使用 `AUTH_TOKEN` 登录即可。也可以在 Render 设置中绑定自定义域名。
---
## Docker Compose 部署(推荐)
### 标准步骤
```bash
mkdir boosapi && cd boosapi
# 创建 docker-compose.yml
cat > docker-compose.yml << 'EOF'
services:
boosapi:
image: 1467078763/boosapi:latest
container_name: boosapi
restart: unless-stopped
ports:
- "4000:4000"
environment:
- AUTH_TOKEN=your-admin-token
- PROXY_TOKEN=your-proxy-sk-token
- TZ=Asia/Shanghai
volumes:
- ./data:/app/data
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:4000/healthz"]
interval: 30s
timeout: 10s
retries: 3
start_period: 15s
EOF
# 启动
docker compose up -d
# 查看日志
docker compose logs -f
```
### 使用 `.env` 文件
如果不想把密钥写在 `docker-compose.yml` 里,可以创建 `.env` 文件:
```bash
# .env
AUTH_TOKEN=your-admin-token
PROXY_TOKEN=your-proxy-sk-token
TZ=Asia/Shanghai
PORT=4000
```
然后在 `docker-compose.yml` 中引用:
```yaml
services:
boosapi:
image: 1467078763/boosapi:latest
container_name: boosapi
restart: unless-stopped
ports:
- "${PORT:-4000}:${PORT:-4000}"
environment:
- AUTH_TOKEN=${AUTH_TOKEN}
- PROXY_TOKEN=${PROXY_TOKEN}
- TZ=${TZ:-Asia/Shanghai}
volumes:
- ./data:/app/data
```
启动时指定 env 文件:
```bash
docker compose --env-file .env up -d
```
> ⚠️ `.env` 文件包含敏感信息,请勿提交到 Git 仓库。确保已添加到 `.gitignore`。
### 全部环境变量
| 变量 | 说明 | 默认值 |
|------|------|--------|
| `AUTH_TOKEN` | 管理后台登录令牌(**必填**) | `change-me-admin-token` |
| `PROXY_TOKEN` | 下游客户端调用 `/v1/*` 的 Bearer Token(**必填**) | — |
| `PORT` | HTTP 监听端口 | `4000` |
| `HOST` | 监听地址 | `0.0.0.0` |
| `DATA_DIR` | 数据目录 | `/app/data` |
| `TZ` | 时区 | `UTC` |
| `DB_TYPE` | 数据库类型:`sqlite` / `mysql` / `postgres` | `sqlite` |
| `DB_URL` | 数据库连接串(外部数据库时必填) | — |
| `DB_SSL` | 启用数据库 SSL 连接 | `false` |
| `LOG_LEVEL` | 日志级别:`debug` / `info` / `warn` / `error` | `info` |
| `REDIS_URL` | Redis 连接串(可选,用于缓存/速率限制) | — |
| `BOOSAPI_DESKTOP` | 桌面版标记(请勿手动设置) | — |
| `BOOSAPI_LOG_DIR` | 桌面版日志目录(请勿手动设置) | — |
### Docker Compose 扩展配置
#### 挂载自定义目录
```yaml
volumes:
- /your/custom/data/path:/app/data
```
#### 使用外部数据库(MySQL/PostgreSQL)
```yaml
environment:
- DB_TYPE=mysql
- DB_URL=mysql://user:password@host:3306/boosapi?ssl={"rejectUnauthorized":true}
- DB_SSL=true
```
#### 使用 Redis 加速
```yaml
environment:
- REDIS_URL=redis://:password@redis:6379/0
# 额外声明 Redis 服务(可选)
services:
redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
- ./redis-data:/data
```
#### 资源限制
```yaml
services:
boosapi:
deploy:
resources:
limits:
memory: 1G
reservations:
memory: 256M
```
### Docker Compose 生产部署检查清单
- [ ] `AUTH_TOKEN` 和 `PROXY_TOKEN` 使用强随机密码
- [ ] `.env` 文件已加入 `.gitignore`
- [ ] 宿主机端口不与现有服务冲突
- [ ] 数据目录已挂载且定期备份
- [ ] 启用了 `restart: unless-stopped`
- [ ] 配置了健康检查
- [ ] 日志轮转已配置(Docker 默认 json-file 会撑满磁盘)
---
## Docker 命令部署
适合快速测试或单容器场景:
```bash
docker run -d --name boosapi \
-p 4000:4000 \
-e AUTH_TOKEN=your-admin-token \
-e PROXY_TOKEN=your-proxy-sk-token \
-e TZ=Asia/Shanghai \
-v ./data:/app/data \
--restart unless-stopped \
1467078763/boosapi:latest
```
> **路径说明:**
> - `./data:/app/data` — 相对路径,数据存到当前目录下的 `data` 文件夹
> - 也可以使用绝对路径:`/your/custom/path:/app/data`
### Docker Run 常用选项
| 选项 | 作用 |
|------|------|
| `-d` | 后台运行 |
| `--restart unless-stopped` | 宕机或宿主机重启后自动拉起 |
| `--name boosapi` | 固定容器名,方便管理 |
| `-p 4000:4000` | 宿主机端口:容器端口 |
| `-v ./data:/app/data` | 持久化数据 |
| `-e VAR=value` | 设置环境变量 |
| `--network host` | 使用宿主机网络(Linux 专用,跳过端口映射) |
| `--log-opt max-size=10m --log-opt max-file=3` | 限制日志大小 |
### 停止与删除
```bash
docker stop boosapi
docker rm boosapi
```
---
## 桌面版部署(Windows / macOS / Linux)
桌面版面向个人电脑本地使用,基本安装与配置流程见 [快速上手 → 桌面版启动](./getting-started.md#方式二-桌面版启动-windows-macos-linux)。
以下是部署相关的补充说明。
### 桌面版特性
- 内置本地 BoosAPI 服务,无需手动准备 Node.js 运行环境
- 托盘菜单支持重新打开窗口、重启后端、开机自启
- 支持基于 GitHub Releases 的应用内更新检查
> [!IMPORTANT]
> 桌面版首次启动时,如果没有显式注入 `AUTH_TOKEN`,管理员登录令牌默认是 `change-me-admin-token`。
> 这只适合本机初始调试使用,首次登录后应立即修改。
> [!NOTE]
> 服务器部署不再提供裸 Node.js Release 压缩包,统一推荐 Docker / Docker Compose。
### 桌面版升级
1. 通过应用内更新提示安装新版本,或从 Releases 下载最新安装包覆盖安装
2. 用户数据目录会保留,升级后自动继续使用原有数据
3. 如需排查启动问题,优先查看 `app.getPath('userData')/logs` 下的最新日志
Linux 安装包选择建议:
- Fedora / RHEL / CentOS / openSUSE 优先使用 `.rpm`
- Debian / Ubuntu / Linux Mint 优先使用 `.deb`
- 其他发行版可使用 `.AppImage`
---
## 本地开发运行(源码调试)
开发、调试或提交 PR 的完整流程见 [快速上手 → 本地开发启动](./getting-started.md#方式三-本地开发启动) 和 [CONTRIBUTING.md](../CONTRIBUTING.md)。
> [!NOTE]
> 这条路径是开发流程,不是下载 `Release` 包后再手动跑 Node.js 的替代说法。
---
## 反向代理
以下反向代理配置面向 Docker / 服务器模式。桌面版内置后端默认监听 `0.0.0.0:4000`,但通常仍作为单机桌面应用使用;如果要给局域网或公网客户端访问,请自行配置防火墙、反向代理和认证边界。若你显式设置了 `BOOSAPI_DESKTOP_SERVER_PORT`,请把示例里的 `4000` 改成对应端口。
### Nginx
流式请求(SSE)需要关闭缓冲,否则流式输出会异常:
```nginx
server {
listen 443 ssl;
server_name your-domain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://127.0.0.1:4000;
# SSE 关键配置
proxy_buffering off;
proxy_cache off;
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding off;
# 标准代理头
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket 支持
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# 超时设置(长对话/SSE 场景)
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
}
```
### Caddy
```
your-domain.com {
reverse_proxy localhost:4000 {
flush_interval -1
}
}
```
### 限制管理后台访问
如果只想暴露 API 给客户端,限制后台管理界面只能内网访问:
```nginx
# 管理后台:仅内网
location /admin/ {
allow 10.0.0.0/8;
allow 172.16.0.0/12;
allow 192.168.0.0/16;
deny all;
proxy_pass http://127.0.0.1:4000/;
}
# API:对外
location /v1/ {
proxy_pass http://127.0.0.1:4000;
}
```
---
## 升级
```bash
# 拉取最新镜像
docker compose pull
# 重新启动(数据不受影响)
docker compose up -d
# 清理旧镜像
docker image prune -f
```
> [!TIP]
> 建议升级前先备份数据目录。完整备份策略见 [运维手册 → 数据备份](./operations.md#数据备份)。
---
## 回滚
如果升级后出现问题:
1. 停止当前容器:`docker compose down`
2. 指定旧版本标签重新启动:
```bash
# 查看本地已有镜像
docker images 1467078763/boosapi
# 指定旧版本启动
docker compose run --rm boosapi --tag <old-version>
# 或修改 docker-compose.yml 中的 image 标签后重新 up
```
3. 如数据也受影响,从备份恢复(见 [运维手册 → 数据备份与恢复](./operations.md#数据备份))
核心思路:升级前备份数据目录(或数据库),出问题时停止服务、还原数据、指定旧版镜像重启。
---
## 数据持久化
不同运行方式的数据目录不同:
| 运行方式 | 数据目录 | 说明 |
|----------|----------|------|
| Docker / Docker Compose / Zeabur | 容器内 `DATA_DIR`(常见为 `/app/data`) | 需要映射到宿主机目录或平台持久化卷 |
| Render + TiDB | TiDB Serverless(外部 MySQL) | 无本地持久化,数据全部存储在 TiDB 云端数据库 |
| 本地开发 | `DATA_DIR`,默认 `./data` | 位于当前仓库工作目录 |
| 桌面版 | `app.getPath('userData')/data` | 不在仓库目录里,升级桌面应用时会保留 |
桌面版日志位于 `app.getPath('userData')/logs`;Docker / 本地开发模式的日志则跟随各自进程输出或你配置的日志目录。
只要备份了对应的数据目录,升级、重启通常都不会丢失现有配置和 SQLite 数据。
完整备份策略见 [运维手册 → 数据备份](./operations.md#数据备份)。
---
## 下一步
- [配置说明](./configuration.md) — 详细环境变量
- [运维手册](./operations.md) — 日志排查、健康检查
-140
View File
@@ -1,140 +0,0 @@
# Metapi Harness Engineering
This document captures the repo-level engineering taste that should remain true
even when future work is performed by autonomous agents. The goal is not to
replace feature tests. The goal is to keep the repository readable,
review-friendly, and hard to drift.
## Why This Exists
`metapi` already has strong local discipline in several areas:
- architecture tests that pin important boundaries
- schema parity and runtime bootstrap verification across databases
- shared protocol helpers that deliberately replace parallel implementations
What was still missing was one place that states the mechanical rules behind
those choices and a lightweight loop that continuously checks for drift. This
document is that shared contract.
## Golden Principles
### 1. One Invariant, One Source Of Truth
If a helper, contract, or workflow already owns an invariant, new work should
reuse that home instead of creating a second implementation.
Examples:
- protocol shaping and normalization belong in `src/server/transformers/**`
- endpoint fallback belongs in `src/server/routes/proxy/endpointFlow.ts`
- proxy success/failure bookkeeping belongs in
`src/server/proxy-core/surfaces/sharedSurface.ts`
- schema heuristics belong in `src/server/db/schemaMetadata.ts`
### 2. Routes Are Adapters, Not Owners
`src/server/routes/**` should remain thin. Route files may register endpoints,
read Fastify request state, and delegate to shared orchestration. They should
not become the place where retry loops, protocol conversion, stream lifecycle,
or billing logic lives.
If a helper is imported outside a single route file, that helper should move to
`proxy-core`, `services`, `transformers`, or another neutral home.
### 3. Transformers Must Stay Protocol-Pure
Transformers are the protocol boundary. They may depend on canonical/shared
contracts, but they should not reach back into route files, Fastify handlers,
OAuth services, token routing, or runtime dispatch details.
In practice this means:
- `canonical` is the request truth source
- `shared/normalized` is the response truth source
- compatibility layers may orchestrate retries or fallback bodies, but they do
not redefine protocol semantics
### 4. Proxy-Core Follows The Golden Path
Proxy orchestration should prefer shared, tested paths instead of bespoke
surface-local logic.
Current preferred path:
- endpoint ranking and request shaping via `upstreamEndpoint.ts`
- endpoint-attempt loops via `executeEndpointFlow()`
- whole-body response decoding via `readRuntimeResponseText()`
- OAuth refresh, sticky/lease behavior, billing, and proxy logging via
`sharedSurface.ts`
- Codex header/session semantics via provider profiles and header utils
- Codex websocket transport via the dedicated websocket runtime, not generic
fetch executors
### 5. Platform Capability Must Be Explicit
Platform behavior is easy to let drift because it spans adapters, discovery,
endpoint preference, and routing. Any platform-specific behavior that matters
at runtime should be stated once and reused, not re-inferred in multiple
subsystems.
Thin adapters should stay honest. Feature-complete adapters should be tested as
feature-complete. Do not silently “upgrade” support through generic defaults.
### 6. Database Changes Must Stay Contract-Driven
Schema work already has one of the strongest harnesses in the repo. Keep it
that way.
- update Drizzle schema and SQLite migration history together
- regenerate checked-in contract/artifacts together
- keep cross-dialect bootstrap/upgrade generation contract-driven
- keep legacy startup compatibility narrow and spec-owned
### 7. Web Pages Are Orchestration Surfaces
Top-level pages should not be reused as shared component libraries.
- no page-to-page imports for reusable UI
- mobile behavior should reuse shared primitives first
- extract repeated modals, drawers, and panels into domain subfolders early
## First-Wave Drift Checks
The first repo-level drift loop is intentionally small and high-signal. It
checks for:
- transformer imports from `routes/proxy`
- new proxy-core surface body reads that bypass `readRuntimeResponseText()`
- new imports from `routes/proxy` inside `proxy-core` beyond the current debt
baseline
- new top-level page-to-page imports in the admin UI beyond the current debt
baseline
These checks live in `scripts/dev/repo-drift-check.ts` and are wired into CI.
## Tracked Debt Vs New Violations
The repository already contains some known architectural debt, especially where
`proxy-core` still imports helpers from `routes/proxy` and where one admin page
reuses another page's export.
The harness uses a ratchet:
- tracked debt is reported so it stays visible
- new violations fail CI
This keeps the repo moving forward without forcing a risky “rewrite
everything first” migration.
## Garbage Collection Loop
The harness loop is intentionally conservative:
1. rules are encoded in repo docs and executable checks
2. CI blocks new drift
3. a scheduled workflow generates a drift report artifact
4. targeted cleanup PRs can remove tracked debt in small slices
The goal is steady principal payments on technical debt, not occasional heroic
cleanup sprints.
-233
View File
@@ -1,233 +0,0 @@
# ❓ 常见问题(FAQ)
[返回文档中心](./README.md)
---
<a id="account-auth-guide"></a>
## 账号认证方式与兼容性(Session / API Key / OAuth)
### Q: Access Token、Cookie、API Key、OAuth 应该选哪个?
**A:** 现在最稳妥的判断方式不是只看“有没有 Token”,而是先看你接的是哪一类上游:
| 方式 | 特点 | 适用场景 |
|------|------|----------|
| **Access Token(系统访问令牌)** | 稳定、适合长期使用、适合多账号 | 标准 New API / One API / OneHub / DoneHub / Veloera 等面板站 |
| **Cookie(浏览器登录态)** | 兼容性强,但更容易过期 | Access Token 不可用、AnyRouter 一类魔改站点、特殊兜底场景 |
| **API Key** | 最简单,适合代理调用 | OpenAI / Claude / Gemini 兼容入口、CPA、各类官方预设 |
| **OAuth** | 不手填普通凭证,走浏览器授权 | Codex、Claude、Gemini CLI、Antigravity |
结论:
1. 面板型站点优先 **Access Token**
2. Access Token 拿不到时再考虑 **Cookie**
3. 兼容接口、CPA、官方预设优先 **API Key**
4. Provider 原生授权优先 **OAuth**
### Q: 在 Metapi 里怎么切换认证方式?
**A:** 入口已经分成两条:
1. **普通站点**:在「账号管理 / API Key 管理」里选择 Session 或 API Key
2. **OAuth provider**:在左侧「OAuth 管理」里完成授权
如果你不确定该去哪一页,先看 [上游接入](/upstream-integration)。
### Q: 以 NewAPI 为例,Access Token 在哪里生成?
**A:** 通常路径是:**控制台 → 个人设置 → 安全设置 → 系统访问令牌**。
如果你确实需要 Cookie 兜底,可在浏览器 `F12 → Application → Cookie` 获取对应登录态。
### Q: 什么情况下应该直接走 OAuth?
**A:** 当你接的不是一个普通面板站,而是 provider 自己的账号授权时:
- Codex
- Claude
- Gemini CLI
- Antigravity
这时不要再把它当成“账号管理里的一条普通 Session”去处理,直接看 [OAuth 管理](/oauth)。
### Q: AnyRouter 这类魔改站点为什么更容易报错?
**A:** AnyRouter 在 Metapi 中按 NewAPI 兼容站点处理,但不少实例会隐藏 Access Token 入口,或额外加登录盾。建议:
1. 如果站点后台能生成系统访问令牌,优先用 Session 模式 + Access Token
2. 如果没有 Access Token 入口,或验证时被盾/认证页拦截,再改用 Cookie 导入
3. 若返回 HTML 盾页错误,先在浏览器完成验证后再重新绑定
### Q: CPA / CLIProxyAPI 应该怎么接?
**A:** 这类站点现在走 `cliproxyapi` 平台类型,推荐直接加 API Key,不建议把它当成可签到、可登录的面板站。详细步骤见 [上游接入](/upstream-integration)。
### Q: Sub2API 站点怎么处理?
**A:** Sub2API 常见 JWT 短期会话机制,和传统 NewAPI 站点差异较大。当前建议:
1. 在「凭证模式」里选择 Session 模式,分别粘贴 F12 界面中的 `auth_token`、`refresh_token`、`token_expires_at` 字段进行验证,无需配置用户 ID
2. 不要使用账号密码登录,Metapi 不支持代替 Sub2API 做登录
3. Sub2API 通常为订阅制使用,不支持签到;如果你只关心代理调用,也可以直接改用 API Key 模式
4. 若 `GET /v1/models` 为空,先确认该账号下已有可用用户 API Key,Metapi 会再尝试用它发现模型
详细操作说明见 [上游接入](/upstream-integration)。
## 部署相关
### Q: 启动后无法访问管理后台
**A:** 排查步骤:
1. 确认容器正常运行:`docker compose ps`
2. 确认端口映射正确:`docker compose logs | grep listening`
3. 检查防火墙是否放行了端口(默认 4000)
4. 如果使用反向代理,确认代理配置正确
### Q: 登录失败,提示令牌无效
**A:** 先确认你输入的是管理员令牌,而不是代理令牌。登录后台使用的是 `AUTH_TOKEN`,注意:
- 初始管理员令牌 = 启动时设置的 `AUTH_TOKEN`
- 如果你在非 Compose 场景未显式设置 `AUTH_TOKEN`,默认值是 `change-me-admin-token`(仅建议本地调试)
- 若复用旧 SQLite `data/` 目录,或当前实例已切到 MySQL / Postgres,系统会优先读取当前运行数据库中的 `auth_token` 设置(可能覆盖当前环境变量)
- 使用 `.env` 文件时,确认文件路径正确,且值不需要加引号
### Q: Docker Compose 启动报错 `AUTH_TOKEN is required`
**A:** 使用了 `${AUTH_TOKEN:?}` 语法,需要先设置环境变量:
```bash
export AUTH_TOKEN=your-token
export PROXY_TOKEN=your-proxy-token
docker compose up -d
```
或使用 `.env` 文件。
### Q: 桌面版启动失败
**A:** 排查步骤:
1. 确认下载的是与你系统匹配的桌面安装包
2. Windows 首次运行若出现未知发布者提示,先确认安装包来自官方 Releases
3. 如果桌面版提示后端启动失败,优先查看托盘菜单中的日志目录
4. 如需部署到服务器,请改用 Docker / Docker Compose,而不是桌面安装包
---
## 代理相关
### Q: 下游客户端提示 401 / 403
**A:** 排查:
- 确认使用的是 `PROXY_TOKEN`(代理令牌),而非 `AUTH_TOKEN`(管理令牌)
- 确认反向代理正确透传了 `Authorization` 请求头
- 检查是否设置了 `ADMIN_IP_ALLOWLIST` 限制了访问
### Q: `GET /v1/models` 返回空列表
**A:** 可能原因:
1. 未添加任何站点或账号
2. 账号处于 `unhealthy` 状态 — 在账号管理页面检查并刷新
3. 未同步 Token — 在 Token 管理页面点击「同步」
4. 模型未发现 — 手动触发模型刷新;若近期改过站点/账号/路由,可到「设置 → 清除缓存并重建路由」或「TokenRoutes → 重建路由」后再试
### Q: 非流式正常,但流式输出异常(卡住、乱码、截断)
**A:** 几乎都是反向代理配置问题。请确认:
1. Nginx:添加 `proxy_buffering off;`
2. 未改写 `text/event-stream` Content-Type
3. 无 CDN 或中间层缓存 SSE 响应
完整 Nginx 配置参考 [部署指南](./deployment.md#nginx)。
### Q: 某模型显示可用,但实际调用失败
**A:** 在管理后台的「模型测试器」中直测该模型,查看具体失败原因:
- **上游账号状态异常**:账号凭证过期或被禁用
- **通道处于冷却期**:近期该通道请求失败,系统自动冷却(默认 10 分钟)
- **上游模型下线**:上游站点已移除该模型
- **余额不足**:对应账号余额已耗尽
### Q: 请求延迟很高
**A:** 排查方向:
- 在代理日志中查看具体延迟分布
- 检查是否因冷却导致使用了较远/较慢的上游
- 调整路由权重,降低 `COST_WEIGHT`、提高成功率高的通道优先级
---
## 下游 API Key 相关
### Q: 如何限制不同项目/团队的用量
**A:** 在管理后台 **设置 → 下游 API Key** 中为每个项目创建独立的 Key,可单独配置:
- 费用上限(MaxCost)和请求上限(MaxRequests)
- 模型白名单(限制可用模型,支持通配符和正则)
- 路由白名单(限制可走的路由规则)
- 站点倍率(控制不同项目的上游偏好)
### Q: 下游 Key 和 PROXY_TOKEN 有什么区别
**A:** `PROXY_TOKEN` 是全局代理令牌,拥有完整权限。下游 Key 是项目级的细粒度控制,可设置过期时间、用量上限和模型限制,适合多团队共用的场景。
---
## 签到相关
### Q: 签到一直失败
**A:** 可能原因:
- 上游站点不支持签到功能
- 账号凭证已过期(系统会尝试自动重登录)
- 站点接口变更 — 检查 Metapi 是否为最新版本
### Q: 签到成功但奖励显示为 0
**A:** 部分站点的签到接口不返回奖励金额。Metapi 会尝试从收入日志推算奖励,但可能存在延迟。
---
## 数据相关
### Q: 数据迁移怎么做
**A:** 两种方式:
1. **应用内导入导出**(推荐):在管理后台 → 导入/导出 页面操作,适合 SQLite / MySQL / Postgres / Desktop 之间迁移
2. **目录迁移**:仅适用于当前运行在 SQLite 的实例(包括桌面版本地数据目录)
3. **数据库原生迁移**:如果当前运行库是 MySQL / Postgres,请使用数据库自己的备份/恢复工具,而不是只拷贝 `data/`
### Q: 如何清理历史数据
**A:** 代理日志和签到日志会持续增长。在管理后台对应页面可以清理历史记录;如果还需要把路由占用统计一并归零,可到「设置 → 清除占用与使用日志」。
### Q: 开源发布时如何避免泄露敏感信息
**A:**
- 确认 `.gitignore` 包含 `.env`、`data/`、`tmp/`
- 发布前执行一次密钥轮换(上游账号密码、通知 SMTP、Webhook 地址)
- 使用全新仓库或清理 Git 历史后再公开
- 检查备份 JSON 文件中是否包含凭证
---
## 更多帮助
如果以上内容未能解决你的问题:
- [搜索已有 Issue](https://github.com/cita-777/metapi/issues?q=is%3Aissue) — 看看是否有人遇到过相同问题
- [提交新 Issue](https://github.com/cita-777/metapi/issues/new) — 报告 Bug 或提出功能建议
- [参与讨论](https://github.com/cita-777/metapi/discussions) — 使用疑问、经验分享
- [文档中心](./README.md) — 查看所有文档
-292
View File
@@ -1,292 +0,0 @@
# 🚀 快速上手
本文档帮助你在 10 分钟内完成 Metapi 的首次部署。
[返回文档中心](./README.md)
---
## 前置条件
按你的使用场景准备对应环境:
| 场景 | 推荐方式 | 需要准备 |
|------|----------|----------|
| 云服务器 / NAS / 家用主机长期运行 | Docker / Docker Compose | Docker 与 Docker Compose |
| 免费云部署(24h 在线) | Render + TiDB + UptimeRobot | 注册 Render / TiDB Cloud / UptimeRobot 免费账号,详见 [Render 部署指南](./deployment.md#render-一键部署免费-24h-运行) |
| 个人电脑本地使用 | 桌面版安装包 | 从 [Releases](https://github.com/cita-777/metapi/releases) 下载对应系统的桌面安装包 |
| 二次开发 / 调试 | 本地开发 | Node.js 20+ 与 npm |
> [!NOTE]
> - 当前不再把 `Release` 压缩包 + Node.js 运行时作为独立部署路径。
> - 想直接运行成品,请用 Docker 或桌面版;想改代码,请走本地开发流程。
## 方式一:Docker Compose 部署(推荐)
### 1. 创建项目目录
```bash
mkdir boosapi && cd boosapi
```
### 2. 创建 `docker-compose.yml`
```yaml
services:
metapi:
image: 1467078763/boosapi:latest
ports:
- "4000:4000"
volumes:
- ./data:/app/data
environment:
AUTH_TOKEN: ${AUTH_TOKEN:?AUTH_TOKEN is required}
PROXY_TOKEN: ${PROXY_TOKEN:?PROXY_TOKEN is required}
CHECKIN_CRON: "0 8 * * *"
BALANCE_REFRESH_CRON: "0 * * * *"
PORT: ${PORT:-4000}
DATA_DIR: /app/data
TZ: ${TZ:-Asia/Shanghai}
restart: unless-stopped
```
### 3. 设置令牌并启动
```bash
# AUTH_TOKEN = 管理后台初始管理员令牌(登录后台时输入这个值)
export AUTH_TOKEN=your-admin-token
# PROXY_TOKEN = 下游客户端调用 /v1/* 使用的令牌
export PROXY_TOKEN=your-proxy-sk-token
docker compose up -d
```
### 4. 访问管理后台
打开 `http://localhost:4000`,使用 `AUTH_TOKEN` 的值登录。
> [!TIP]
> 初始管理员令牌就是启动时配置的 `AUTH_TOKEN`。
> 如果未显式设置(非 Compose 场景),默认值为 `change-me-admin-token`(仅建议本地调试)。
> 若你在后台「设置」里修改过管理员令牌,后续登录请使用新令牌。
## 方式二:桌面版启动(Windows / macOS / Linux)
如果你是在个人电脑上本地使用,请直接下载桌面版安装包:
1. 打开 [Releases](https://github.com/cita-777/metapi/releases) 下载与你系统匹配的桌面安装包
2. 安装并启动 Metapi Desktop
3. 桌面壳会自动启动本地服务并保存数据,无需手动准备 Node.js 环境
Linux 安装包选择建议:
- Fedora / RHEL / CentOS / openSUSE 优先下载 `.rpm`
- Debian / Ubuntu / Linux Mint 优先下载 `.deb`
- 其他发行版或想免安装直接运行时,可下载 `.AppImage`
| 项目 | 说明 |
|------|------|
| 管理界面 | 应用启动后会直接打开桌面窗口,不需要假设固定的 `http://localhost:4000` |
| 本地后端地址 | 桌面版内置服务默认监听 `0.0.0.0:4000`;桌面窗口和本机 curl 可继续使用 `http://127.0.0.1:4000`,局域网其他设备请使用当前机器的实际 IP + `4000`;如需改端口,可显式设置 `METAPI_DESKTOP_SERVER_PORT` |
| 数据目录 | 保存在 `app.getPath('userData')/data`,不是仓库里的 `./data` |
| 日志目录 | 保存在 `app.getPath('userData')/logs`;托盘菜单提供 `Open Logs Folder` |
> [!IMPORTANT]
> 桌面版首次启动时,如果你没有额外注入 `AUTH_TOKEN`,默认管理员令牌就是 `change-me-admin-token`。
> 首次登录后建议立即到「设置」里改成你自己的强密码令牌。
> [!TIP]
> - Windows 下常见路径是 `%APPDATA%\Metapi\data` 和 `%APPDATA%\Metapi\logs`。
> - 如果没有额外覆盖端口,本机其他客户端可以直接连接 `http://127.0.0.1:4000`。
> - Linux 用户建议优先选原生包:Fedora 系列用 `.rpm`,Debian/Ubuntu 系列用 `.deb`。
> [!WARNING]
> **端口冲突排障:** 桌面版默认使用 `4000` 端口;如果该端口被其他应用占用:
> - 设置环境变量 `METAPI_DESKTOP_SERVER_PORT=<指定端口>` 改到一个空闲端口
> - 或关闭占用 `4000` 的应用后重启 Metapi Desktop
> [!NOTE]
> 服务器部署统一推荐 Docker / Docker Compose,不再提供裸 Node.js 的 Release 压缩包。
## 方式三:本地开发启动
```bash
git clone https://github.com/cita-777/metapi.git
cd boosapi
npm install
npm run db:migrate
npm run dev
```
- 前端地址:`http://localhost:5173`(Vite dev server)
- 后端地址:`http://localhost:4000`
- 这是源码开发流程,不是免 Docker 的成品部署包
## 首次使用流程
完成部署后,按以下顺序配置:
> [!TIP] 从 ALL-API-Hub 迁移(可选)
> 如果你使用过 ALL-API-Hub,Metapi 兼容其导出的备份设置,可直接导入,无需手动逐项配置。
>
> 导入后刷新账号状态可能出现个别账号令牌过期,点击重新绑定按钮按照下面步骤2的方法获取Access Token或者Cookie等即可。
>
> ![ALL-API-Hub备份导入](./screenshots/allapi-hub-backup.png)
### 步骤 1:添加站点
进入 **站点管理**,添加你使用的上游中转站:
- 填写站点名称(自己想怎么取就怎么取)和 URL
- 按你手上的上游形态选择:
- 有后台面板:`new-api` / `one-api` / `one-hub` / `done-hub` / `veloera` / `anyrouter` / `sub2api`
- 通用兼容接口:`openai` / `claude` / `gemini` / `cliproxyapi`
- 官方入口:直接在下拉里选对应**官方预设**,例如阿里云 / 智谱 / 豆包 Coding Plan,DeepSeek,Moonshot,MiniMax,ModelScope
- 平台通常可自动检测;如果因为防护页、反向代理或特殊路径导致检测失败,再手动选择。
- 可选是否开启系统代理,方便国内机器访问国外中转站。
- 可选站点权重,站点权重越大,路由将更加频繁使用这个站点的模型。
- 如果这个站点的控制台 URL 和真实 API 请求地址不同,不要直接把主站点 URL 改掉,而是在表单下方补「API 请求地址池」。
> [!IMPORTANT]
> 通用平台常见写法是填控制台地址或 provider base URL;但如果你选的是**官方预设**,请保留预设自动带出的完整路径,哪怕它本来就包含 `/v1`、`/anthropic` 或 `/api/coding/...`。
如果你不确定该选哪个平台或预设,先看 [上游接入](/upstream-integration)。
![站点管理](./screenshots/site-management.png)
### 步骤 2:添加连接(账号 / API Key / OAuth)
这一步不要再死记“所有站点都先加账号”。现在推荐按场景分流:
#### 2A. 面板型站点:添加账号 / Session
进入 **连接管理中的账号管理**,为每个站点添加已注册的账号:
![账号管理](./screenshots/account-management.png)
- 填入用户名和访问凭证
![账号凭证](./screenshots/account-credentials.png)
- 系统会自动登录并获取余额信息
![账号余额](./screenshots/account-balance.png)
- 启用自动签到(如站点支持)
适合这一分支的平台:
- `new-api`
- `one-api`
- `one-hub`
- `done-hub`
- `veloera`
- `anyrouter`
- `sub2api`
#### 2B. 兼容接口 / 官方预设 / CPA:添加 API Key
进入 **连接管理中的 API Key 管理**,为站点添加你的 API Key:
![API Key 管理](./screenshots/api-key-management.png)
适合这一分支的平台:
- `openai`
- `claude`
- `gemini`
- `cliproxyapi`
- 所有官方预设(Coding Plan、DeepSeek、Moonshot、MiniMax、ModelScope 等)
#### 2C. Provider 原生授权:走 OAuth 管理
如果你要接的是:
- Codex
- Claude provider 账号
- Gemini CLI
- Antigravity
那就不要在这里手填普通账号,而是直接去左侧菜单 **OAuth 管理** 完成授权,详见 [OAuth 管理](/oauth)。
### 步骤 3:同步账号令牌(可选,仅面板型站点)
进入 **连接管理中的账号令牌管理**:
- 点击「同步」从上游账号拉取 账号令牌
- 或手动添加已有的账号令牌,添加后上游站点的令牌管理页面会同步出现令牌,如下图所示。
![Token管理](./screenshots/token-management.png)
如果你走的是 API Key-only 或 OAuth 流程,这一步通常不是必需的。
### 步骤 4:路由管理
进入 **路由管理**:
- 系统会自动发现模型并生成路由规则
- 点击右上角的刷新选中概率可以显示并将概率载入缓存中
- 可以手动调整通道的优先级和权重
- 关于路由权重参数调优,参考 [配置说明 → 智能路由](/configuration#智能路由)
- 左侧可以进行品牌、站点、接口等的筛选,如下图所示:
![路由筛选](./screenshots/routes-filter.png)
- **可以通过创建群组,从而对上游模型进行匹配和重定向,如果建立下图群组,下游访问Metapi时获取的claude-opus-4-6模型将在命中样本中智能选取,日志中可以看见映射。** ![路由群组示例](./screenshots/route-group.png)
- **可以在使用日志中看见下游的请求模型和实际分配给下游使用的模型**
![日志中的模型映射](./screenshots/proxy-logs-mapping.png)
### 步骤 5:验证代理
**Metapi还有更多功能,可以在设置中寻找,请尽情探索,有建议可以提出Issue改进。**
按运行方式选择验证入口:
| 运行方式 | 管理界面 | 代理接口基地址 |
|----------|----------|----------------|
| Docker / Docker Compose | `http://localhost:4000` | `http://localhost:4000` |
| 本地开发 | `http://localhost:5173` | `http://localhost:4000` |
| 桌面版 | 直接使用桌面窗口 | 默认 `http://127.0.0.1:4000`;如果设置了 `METAPI_DESKTOP_SERVER_PORT`,则按日志里的实际端口访问,局域网其他设备改用当前机器 IP + 同一端口 |
### Docker / 本地开发:直接用 curl 验证
```bash
# 检查模型列表
curl -sS http://localhost:4000/v1/models \
-H "Authorization: Bearer your-proxy-sk-token"
# 测试对话
curl -sS http://localhost:4000/v1/chat/completions \
-H "Authorization: Bearer your-proxy-sk-token" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hi"}]}'
```
### 桌面版:默认直接用 4000 验证
打开托盘菜单的 `Open Logs Folder`,在最新日志里查找类似下面的启动信息:
```text
Dashboard: http://127.0.0.1:4000
Proxy API: http://127.0.0.1:4000/v1/chat/completions
```
如果你没有覆盖端口,可直接执行:
```bash
curl -sS http://127.0.0.1:4000/v1/models \
-H "Authorization: Bearer your-proxy-sk-token"
```
如果你显式设置了 `METAPI_DESKTOP_SERVER_PORT`,再把上面的 `4000` 替换成日志里的实际端口。返回正常响应,说明代理链路已经可用。
如果你要从同一局域网的其他设备访问桌面版,把上面的 `127.0.0.1` 替换成这台电脑的实际局域网 IP,并确认系统防火墙已放行对应端口。
## 下一步
- [上游接入](./upstream-integration.md) — 当前代码支持哪些上游、默认该走哪个连接分段
- [部署指南](./deployment.md) — 反向代理、HTTPS、升级策略
- [配置说明](./configuration.md) — 详细环境变量与路由参数
- [客户端接入](./client-integration.md) — 对接 Open WebUI、Cherry Studio 等
-305
View File
@@ -1,305 +0,0 @@
---
layout: home
hero:
name: "Metapi 文档中心"
text: "中转站的中转站"
tagline: "将分散的 AI 中转站聚合为一个统一网关"
image:
src: /logos/logo-full.png
alt: Metapi
actions:
- theme: brand
text: 10 分钟快速上手
link: /getting-started
- theme: alt
text: 上游接入
link: /upstream-integration
- theme: alt
text: 常见问题 FAQ
link: /faq
features:
- title: 快速上手
details: 从部署到第一条请求,按步骤完成最小可用环境搭建。
link: /getting-started
- title: 上游接入
details: 按平台类型、官方预设和 API 请求地址池的现状,快速判断站点该怎么接。
link: /upstream-integration
- title: OAuth 管理
details: 直接接入 Codex、Claude、Gemini CLI、Antigravity 等 provider 授权账号。
link: /oauth
- title: 问题排查
details: 汇总高频报错、根因定位和标准修复路径,降低重复沟通成本。
link: /faq
---
## 项目架构
<div class="home-architecture">
<img
src="./screenshots/metapi-architecture.png"
alt="Metapi Federated AI Model Aggregation Gateway Architecture"
/>
</div>
## 项目界面预览
<div class="home-carousel" aria-label="项目界面自动轮播预览">
<button class="home-carousel-nav home-carousel-prev" type="button" aria-label="上一张">
<span aria-hidden="true">‹</span>
</button>
<button class="home-carousel-nav home-carousel-next" type="button" aria-label="下一张">
<span aria-hidden="true">›</span>
</button>
<div class="home-carousel-track">
<figure class="home-carousel-slide">
<img src="./screenshots/dashboard.png" alt="仪表盘" />
<figcaption>仪表盘</figcaption>
</figure>
<figure class="home-carousel-slide">
<img src="./screenshots/model-marketplace.png" alt="模型广场" />
<figcaption>模型广场</figcaption>
</figure>
<figure class="home-carousel-slide">
<img src="./screenshots/routes.png" alt="智能路由" />
<figcaption>智能路由</figcaption>
</figure>
<figure class="home-carousel-slide">
<img src="./screenshots/accounts.png" alt="账号管理" />
<figcaption>账号管理</figcaption>
</figure>
<figure class="home-carousel-slide">
<img src="./screenshots/sites.png" alt="站点管理" />
<figcaption>站点管理</figcaption>
</figure>
<figure class="home-carousel-slide">
<img src="./screenshots/tokens.png" alt="令牌管理" />
<figcaption>令牌管理</figcaption>
</figure>
<figure class="home-carousel-slide">
<img src="./screenshots/playground.png" alt="模型操练场" />
<figcaption>模型操练场</figcaption>
</figure>
<figure class="home-carousel-slide">
<img src="./screenshots/monitor.png" alt="可用性监控" />
<figcaption>可用性监控</figcaption>
</figure>
</div>
</div>
<style>
.home-architecture {
max-width: 800px;
margin: 0 auto;
}
.home-architecture img {
width: 100%;
border-radius: 16px;
border: 1px solid var(--vp-c-divider);
box-shadow: 0 12px 28px rgba(2, 8, 20, 0.08);
}
.home-carousel {
position: relative;
width: 100%;
margin: 0;
overflow: hidden;
border: 1px solid var(--vp-c-divider);
border-radius: 16px;
background: var(--vp-c-bg-soft);
box-shadow: 0 10px 24px rgba(2, 8, 20, 0.06);
}
.home-carousel-track {
display: flex;
width: 100%;
transform: translateX(0%);
transition: transform 420ms ease;
will-change: transform;
}
.home-carousel-slide {
flex: 0 0 100%;
margin: 0;
}
.home-carousel-slide img {
display: block;
width: 100%;
height: auto;
max-height: min(78vh, 980px);
object-fit: contain;
background: var(--vp-c-bg);
}
.home-carousel-slide figcaption {
padding: 12px 14px;
font-size: 22px;
line-height: 1.2;
font-weight: 600;
color: var(--vp-c-text-1);
}
.home-carousel-nav {
position: absolute;
top: 0;
bottom: 0;
width: 14%;
border: 0;
background: transparent;
color: #fff;
z-index: 2;
display: flex;
align-items: center;
opacity: 0;
transition: opacity 0.2s ease;
cursor: pointer;
}
.home-carousel-nav span {
display: inline-flex;
width: 40px;
height: 40px;
border-radius: 999px;
align-items: center;
justify-content: center;
background: rgba(17, 24, 39, 0.56);
font-size: 28px;
line-height: 1;
user-select: none;
}
.home-carousel:hover .home-carousel-nav {
opacity: 1;
}
.home-carousel-prev {
left: 0;
justify-content: flex-start;
padding-left: 12px;
background: linear-gradient(90deg, rgba(15, 23, 42, 0.2), transparent);
}
.home-carousel-next {
right: 0;
justify-content: flex-end;
padding-right: 12px;
background: linear-gradient(270deg, rgba(15, 23, 42, 0.2), transparent);
}
@media (max-width: 640px) {
.home-carousel-slide figcaption {
font-size: 18px;
padding: 10px 12px;
}
.home-carousel-nav {
opacity: 1;
width: 18%;
}
.home-carousel-nav span {
width: 34px;
height: 34px;
font-size: 24px;
}
}
</style>
<script setup>
import { onBeforeUnmount, onMounted } from 'vue';
let disposeCarousel = null;
onMounted(() => {
const root = document.querySelector('.home-carousel');
if (!root) return;
const track = root.querySelector('.home-carousel-track');
const slides = root.querySelectorAll('.home-carousel-slide');
const prevBtn = root.querySelector('.home-carousel-prev');
const nextBtn = root.querySelector('.home-carousel-next');
if (!track || slides.length === 0) return;
const total = slides.length;
let index = 0;
let timerId = null;
const render = () => {
track.style.transform = `translateX(-${index * 100}%)`;
};
const goNext = () => {
index = (index + 1) % total;
render();
};
const goPrev = () => {
index = (index - 1 + total) % total;
render();
};
const stopAutoPlay = () => {
if (timerId !== null) {
window.clearInterval(timerId);
timerId = null;
}
};
const startAutoPlay = () => {
stopAutoPlay();
timerId = window.setInterval(goNext, 3000);
};
const onMouseEnter = () => stopAutoPlay();
const onMouseLeave = () => startAutoPlay();
const onPrevClick = () => {
goPrev();
startAutoPlay();
};
const onNextClick = () => {
goNext();
startAutoPlay();
};
root.addEventListener('mouseenter', onMouseEnter);
root.addEventListener('mouseleave', onMouseLeave);
prevBtn?.addEventListener('click', onPrevClick);
nextBtn?.addEventListener('click', onNextClick);
render();
startAutoPlay();
disposeCarousel = () => {
stopAutoPlay();
root.removeEventListener('mouseenter', onMouseEnter);
root.removeEventListener('mouseleave', onMouseLeave);
prevBtn?.removeEventListener('click', onPrevClick);
nextBtn?.removeEventListener('click', onNextClick);
};
});
onBeforeUnmount(() => {
if (typeof disposeCarousel === 'function') {
disposeCarousel();
}
});
</script>
## 从这里开始
- 初次部署或首次接入:从 [快速上手](/getting-started) 开始,先跑通最小可用链路。
- 不确定上游平台该怎么选:先看 [上游接入](/upstream-integration),再决定走 `账号管理` 还是 `API Key管理`。
- 准备上线或升级回滚:查看 [部署指南](/deployment) 与 [运维手册](/operations)。
- 需要补齐环境变量或路由参数:直接查 [配置说明](/configuration)。
- 正在处理客户端或第三方工具接入:优先看 [客户端接入](/client-integration)。
- 遇到高频报错或异常表现:去 [常见问题 FAQ](/faq) 快速定位根因。
## 文档维护入口
- `/` 保持为面向所有读者的公开落地页,不再承担维护说明或贡献说明的二次首页职责。
- 维护文档站、梳理导航、补充内容地图时,请进入 [文档维护与贡献](/README)。
- 新增 FAQ 或教程前,请先阅读 [FAQ/教程贡献规范](/community/faq-tutorial-guidelines)。
-421
View File
@@ -1,421 +0,0 @@
# ☸️ K3s 更新中心(高级)
[返回部署指南](./deployment.md) · [返回配置说明](./configuration.md) · [返回文档中心](./README.md)
---
本页适用于已经把 Metapi 部署到 **K3s / Kubernetes + Helm** 的用户。
K3s 更新中心的日常入口位于:
```text
设置 → 更新中心
```
本文主要说明三件事:
- 哪些配置直接在后台页面里填写
- 哪些配置仍然需要主服务环境变量或 helper manifest
- 哪些前提不满足时,页面虽然能打开,但无法真正部署
如果你现在的环境只是一个最普通的 **Docker Compose 安装**,只有一个 Metapi 容器,其他什么都没有,那么先记住一句话:
> [!IMPORTANT]
> **如果你只是想原地给现有 Compose 容器加一个“后台点按钮更新”的能力,那你现在大概率用不上这页。**
>
> 当前“更新中心”不会直接帮你更新一台普通 Docker 主机上的 Compose 容器。它依赖集群里的 Deploy Helper 去执行 `helm upgrade` 和 `kubectl rollout status`,所以只适用于已经有 K3s / Kubernetes / Helm 的环境。
>
> 但如果你是老用户,正打算 **从 Docker Compose 迁到 K3s / Helm,以获得滚动更新能力**,那这页仍然值得看,因为它描述的就是迁移完成后的目标形态。
## 配置到底在哪里配
更新中心涉及后台页面、主服务环境变量、helper 部署清单三层配置,最容易混淆,建议先看下面这张对照表。
| 你要配什么 | 去哪里配 | 说明 |
|------|------|------|
| 是否启用更新中心 | UI:设置 → 更新中心 | 日常使用优先在 UI 操作 |
| Deploy Helper URL | UI:设置 → 更新中心 | 例如集群内 Service 地址 |
| Namespace / Release Name / Chart Ref / Image Repository | UI:设置 → 更新中心 | 都是页面里直接填的部署配置 |
| GitHub Releases / Docker Hub / 默认部署来源 | UI:设置 → 更新中心 | 属于页面里的版本来源策略 |
| 主 BoosAPI 到 helper 的认证 token | 主 BoosAPI 环境变量 | `DEPLOY_HELPER_TOKEN` 或 `UPDATE_CENTER_HELPER_TOKEN` |
| helper 自己监听在哪个地址和端口 | helper 环境变量 / manifest | `DEPLOY_HELPER_HOST` / `DEPLOY_HELPER_PORT` |
| helper 自己的 Bearer Token | helper 环境变量 / manifest | `DEPLOY_HELPER_TOKEN`,且必须与主服务一致 |
| chart 是否真的支持 digest 部署 | chart 文件本身 | 这不是 UI,也不是普通 env,要看 chart 模板 |
其中可以这样理解:
- 日常使用时,更新中心的大部分配置直接在 **设置 → 更新中心** 页面填写
- 环境变量主要负责主服务与 helper 之间的认证,以及 helper 自身的监听参数
## 先判断:是否适用
| 你的现状 | 是否适用 | 你现在该怎么做 |
|------|------------|--------------|
| 只有一个 `docker compose up -d` 跑起来的 Metapi 容器 | 不需要 | 继续用普通 Docker 升级方式 |
| 目前是 Docker Compose,准备迁到 K3s / Helm 来获得滚动更新 | 需要 | 把本页当成迁移后的目标架构说明,先完成迁移,再启用更新中心 |
| 有 K3s / Kubernetes 集群,但 Metapi 不是用 Helm release 部署的 | 暂时不建议 | 这套更新中心无法直接接管现有部署 |
| Metapi 已经是 Helm release,想在后台里看版本并手动点一次升级 | 需要 | 继续看下文 |
## 如果你现在只有一个 Docker Compose 容器
这就是大多数用户的真实情况。
对这类用户来说,当前版本最实用的升级路径仍然是普通 Docker 流程:
```bash
docker compose pull
docker compose up -d
```
如果你还想更稳一点,可以先看镜像版本或仓库 release,再决定要不要拉新镜像。
你现在**不需要额外准备**这些东西:
- K3s / Kubernetes 集群
- Helm
- Deploy Helper
- 额外的 Bearer Token
- 集群内 Service 地址
除非你本来就在用 K3s / Helm,否则本页可以直接跳过。
## 如果你正准备从 Docker Compose 迁到 K3s
这其实是一个很合理的升级方向。
很多老用户想迁到 K3s / Helm,核心诉求通常不是“为了多一个页面”,而是为了获得这些能力:
- 发布时尽量减少停机时间
- 用 Helm 管理版本和回滚
- 后续在后台里看版本来源,并人工触发一次升级
对这类用户来说,本页不是“现在立刻就能用”的操作手册,而是:
- “迁移完成后,你会得到什么能力”
- “为了用上这个能力,集群侧要提前满足什么前提”
### 推荐的迁移顺序
如果你现在还是 Docker Compose 用户,建议按下面的顺序规划:
1. 先备份当前实例数据。
- 如果当前运行库还是 SQLite,先备份 `data/` 目录。
- 具体备份方式见 [运维手册](./operations.md)。
2. 评估迁移后的运行数据库。
- 如果你迁到 K3s 只是想“单副本 + 更规范部署”,SQLite 仍然可以先用。
- 如果你更看重后续可维护性、滚动发布体验和外部持久化,通常更建议切到 MySQL / PostgreSQL。
3. 在新环境里先把 Metapi 作为 Helm release 跑起来。
4. 确认新实例可正常登录、数据正常、代理请求正常。
5. 再部署 Deploy Helper,并启用更新中心。
### 一个很重要的认知
“迁到 K3s” 和 “启用更新中心” 是两件相关但不同的事:
- 迁到 K3s / Helm
- 是部署形态升级
- 启用更新中心
- 是迁移完成后,额外获得的一个后台升级入口
所以更准确的理解应该是:
- **先把 Docker Compose 用户迁成 Helm 用户,再谈更新中心。**
## 那这个功能到底是干什么的
对已经在 K3s / Kubernetes 里跑 Metapi 的用户,这个“更新中心”主要解决的是:
- 在后台里看当前运行版本
- 看官方 GitHub Releases / Docker Hub 有没有新的稳定版
- 在后台定时检查新版本,并在首次发现时提醒一次
- 确认 Deploy Helper 是否健康
- 点一次按钮,触发一轮 Helm 升级
- 在页面里回看部署日志
它的定位更接近:
- “集群内人工触发升级面板”
而不是:
- “单容器自动升级器”
- “Docker Compose 一键更新”
- “无人值守自动发布系统”
## 使用它之前,你至少要已经有这些东西
- 一个可用的 K3s / Kubernetes 集群
- 用 Helm 部署的 Metapi release
- 你的 chart 至少支持下面三个 values:
- `image.repository`
- `image.tag`
- `image.digest`
- 目标 Deployment 带有 `app.kubernetes.io/instance=<releaseName>` 标签
- 主 BoosAPI 服务可以访问:
- GitHub API
- Docker Hub API
- Deploy Helper 的集群内地址
如果上面有任何一项不成立,就先不要配更新中心。
## helper 是什么
Deploy Helper 是一个跑在集群里的小服务。它不负责对外提供 Metapi 功能,只负责接受主 BoosAPI 发来的部署请求,然后在集群里执行:
- `helm history`
- `helm get values`
- `helm upgrade`
- `kubectl rollout status`
- 必要时 `helm rollback`
所以你可以把它理解成:
- “一个专门替管理后台执行 Helm/Kubectl 的集群内代理”
## 什么时候你才应该去配 helper
只有当你已经满足下面这个目标时,才值得去配:
- “我已经在 K3s / Helm 里跑 Metapi 了,现在想在后台里看版本,并且人工点一下完成升级”
如果你现在只是:
- “我有一个 Compose 容器,想在后台里点按钮更新它”
那答案是:
- 当前版本还不支持这个场景。
如果你现在是:
- “我正在把 Compose 老实例迁到 K3s,希望迁完以后用后台来做后续版本升级”
那答案是:
- 这是一个合理场景,本页正是说明迁移后该如何接入更新中心。
## K3s / Helm 用户的接入步骤
如果你确实已经是 K3s / Helm 用户,接入顺序建议如下。
### 1. 先部署 Deploy Helper
仓库里带了一个最小示例清单:
- `deploy/k3s/boosapi-deploy-helper.yaml`
- `deploy/k3s/chart`
使用前至少要改这些值:
- `namespace`
- `DEPLOY_HELPER_TOKEN`
- `image`
如果你准备直接按仓库示例接入,推荐把它们放到这两个路径:
- `/opt/boosapi-k3s/boosapi-deploy-helper.yaml`
- `/opt/boosapi-k3s/chart`
这里有一个很容易漏掉、但会直接决定更新中心是否真的可靠的约束:
- 后台里的 `Chart Ref`
- helper Pod 里挂载进去的 chart 路径
这两个值必须指向同一份 chart。
如果你在设置页把 `Chart Ref` 填成 `/opt/boosapi-k3s/chart`,但 helper Pod 里根本没有把那份 chart 挂进去,那么更新中心即使能看到版本,也无法稳定把 digest 部署到真正运行中的 Pod。
最小部署命令示例:
```bash
kubectl create namespace ai
mkdir -p /opt/boosapi-k3s/chart
cp -R deploy/k3s/chart/. /opt/boosapi-k3s/chart/
cp deploy/k3s/boosapi-deploy-helper.yaml /opt/boosapi-k3s/boosapi-deploy-helper.yaml
kubectl apply -f /opt/boosapi-k3s/boosapi-deploy-helper.yaml
```
### 1.1 首次接入前一定要做的自检
新用户最容易踩的坑不是 helper 起不来,而是:
- chart 虽然能执行 `helm upgrade`
- 但它其实没有消费 `image.digest`
- 同时 `imagePullPolicy` 还停留在 `IfNotPresent`
这样更新中心会显示任务成功、Helm revision 也会增加,但线上真正运行的镜像可能根本没变。
仓库自带的 `deploy/k3s/chart` 已经把这件事处理成默认正确:
- `values.yaml` 里包含 `image.digest`
- `templates/deployment.yaml` 在有 digest 时会渲染成 `repository@digest`
- chart 默认 `imagePullPolicy: Always`
- helper 示例清单也默认 `imagePullPolicy: Always`
你可以在接入前先跑一次自检:
```bash
helm template boosapi /opt/boosapi-k3s/chart \
--set image.repository=1467078763/boosapi \
--set image.tag=latest \
--set-string image.digest=sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef \
| grep -n 'image:'
```
你期望看到的是:
- `image: "1467078763/boosapi@sha256:..."`
如果输出仍然是:
- `image: "1467078763/boosapi:latest"`
那说明这份 chart 还是 tag 语义,先不要继续配置更新中心。
### 2. 让主 BoosAPI 和 helper 用同一个 token
主 BoosAPI 端支持这两个环境变量,设置一个即可:
- `DEPLOY_HELPER_TOKEN`
- `UPDATE_CENTER_HELPER_TOKEN`
helper 端使用:
- `DEPLOY_HELPER_TOKEN`
它们的值必须完全一致。
这一步仍然是**环境变量级配置**,因为它本质上是主服务与 helper 之间的认证,而不是普通用户日常在 UI 里调的业务参数。
### 3. 在后台“设置 → 更新中心”里填配置
从这一步开始,**优先按 UI 来操作**,不要再回头把下面这些字段硬塞进环境变量。
需要填写的核心字段有:
| 字段 | 怎么理解 | 典型示例 |
|------|--------|----------|
| `Deploy Helper URL` | 主 BoosAPI 访问 helper 的地址 | `http://boosapi-deploy-helper.ai.svc.cluster.local:9850` |
| `Namespace` | 目标 release 所在命名空间 | `ai` |
| `Release Name` | Helm release 名 | `metapi` |
| `Chart Ref` | helper Pod 能访问到的 chart 引用 | `/opt/boosapi-k3s/chart` |
| `Image Repository` | 升级时写入 chart 的镜像仓库 | `1467078763/boosapi` |
| `默认部署来源` | 默认从 GitHub 还是 Docker Hub 取版本 | `GitHub Releases` |
另外还有 3 个开关:
- `启用更新中心`
- `GitHub Releases`
- `Docker Hub`
补充一点:
- `Docker Hub` 里的**主候选**仍然优先 `latest` / `main` / 稳定 SemVer 标签
- 页面还会自动列出最近推送的 `dev`、分支名、临时标签或 `sha-*` 这类**非稳定标签**,并一起带出 digest
- 如果你要部署一个更老或更特殊、没出现在自动列表里的标签,仍然可以在 Docker Hub 卡片底部用“手动部署 Docker Hub 标签”填写
对已经跑起来的 K3s / Helm 用户来说,更新中心的日常配置主要就在这一页:
```text
设置 → 更新中心
```
### 4. 实际操作顺序
推荐这样用:
1. 先保存配置
2. 等后台自动发现新版本,或手动点一次“检查更新”立即刷新
3. 确认页面状态都正常:
- 当前运行版本正常显示
- 版本来源发现了可部署版本
- 如果 Docker Hub 显示的是 `latest @ sha256:...`,说明页面已经识别到 alias tag 当前指向的具体镜像 digest
- Deploy Helper 显示健康
4. 如果你要跟随稳定候选,直接点部署按钮;如果你要切到最近的 `dev` / 分支 / 临时标签,可以直接点自动列出的那一项;只有更特殊的 tag 才需要手动填写,必要时连 digest 一起填
5. 在页面下方看部署日志
6. 如果升级后发现问题,可以直接在“回退历史”里点旧 revision 回滚;只要该 revision 当时记录了 digest,就会跟着一起回到对应镜像
如果实时日志流断开,页面会自动回退到最近任务快照。
### 4.1 新用户最值得先做的一次确认
第一次接入时,不要急着点部署。先确认下面 4 件事同时成立:
1. `Chart Ref` 和 helper 挂载路径指向的是同一份 chart。
2. 这份 chart 支持 `image.digest`,并且在有 digest 时会渲染成 `repository@digest`。
3. chart 和 helper 都没有继续使用会掩盖新镜像的 `imagePullPolicy: IfNotPresent`。
4. 页面里显示的目标 digest,和 helper 最终读到的运行中 digest 能对上。
只要这 4 件事成立,新用户就不会再遇到“页面说部署成功,但线上还是旧镜像”的假成功。
## 这套能力当前不适合什么场景
- 只有一台 Docker 主机、一个 Compose 文件、一个 Metapi 容器
- 想让后台直接远程更新 Docker Compose 服务
- 没有 Helm release,只是手工 apply 的 Deployment
- 需要全自动无人值守升级
- 需要灰度、分批、审批、回滚编排等更完整的发布系统
## About 页和更新中心的关系
About 页里的“更新提醒”更像一个轻量入口:
- 让你知道大概有没有新版本
真正的配置和部署入口仍然在:
- “设置 → 更新中心”
所以即使你是普通 Docker Compose 用户,也可以把 About 页里的版本提醒当成“看看最近有没有新版本”的地方,但不要把它理解成“马上就能自动更新我这台机器”。
## 常见问题
### 为什么我在设置页里能看到更新中心,但还是没法用
因为这个页面会跟着功能一起出现,但真正能否部署,取决于你有没有:
- K3s / Kubernetes
- Helm release
- Deploy Helper
- 主服务的 `DEPLOY_HELPER_TOKEN`(或兼容别名 `UPDATE_CENTER_HELPER_TOKEN`)与 helper 侧 `DEPLOY_HELPER_TOKEN` 保持一致
另外,如果你想用“按 digest 精确部署/回退”这条链路,还要确认你的 chart 没有忽略 `image.digest` 这个值;否则页面虽然能显示 digest,真正部署时还是只会落到 tag 语义。
缺其中任何一项,都只能看,不能真正部署。
### 为什么页面显示任务成功,但线上镜像还是没变
最常见的根因只有两类:
- 你的 chart 其实没有消费 `image.digest`
- 你的 Deployment 或 helper 还在使用 `imagePullPolicy: IfNotPresent`
这两种情况叠在一起时,就会出现“Helm revision 往前走了,但实际运行的 imageID 没动”的假成功。
如果你是第一次接入,最稳妥的方式就是直接从仓库自带的:
- `deploy/k3s/chart`
- `deploy/k3s/boosapi-deploy-helper.yaml`
开始,不要自己先抄一份简化版再改。
### 为什么我只有 Docker Compose,按钮没有意义
因为当前部署链路不是去操作外部 Docker 主机,而是去调用集群里的 helper,然后由 helper 执行 `helm` / `kubectl`。
### 那普通 Docker Compose 用户现在该怎么升级
继续使用正常 Docker 流程即可:
```bash
docker compose pull
docker compose up -d
```
如果你担心直接升级,可以先看仓库 Releases 或镜像 tag,再决定是否升级。
## 相关入口
- [部署指南](./deployment.md)
- [配置说明](./configuration.md)
- [运维手册](./operations.md)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 845 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

-719
View File
@@ -1,719 +0,0 @@
# 🛠️ 管理 API
**注意:以下内容适用于有一定技术能力的用户,否则请跳过此页面!!!**
本文档说明如何直接调用 Metapi 管理后台的 `/api/*` 接口,用脚本完成站点与账号管理。
[返回文档维护页](./README.md)
[返回文档首页](/)
---
## 适用范围
这组接口适合以下场景:
- 初始化新实例,批量导入站点和账号
- 用 Shell / Python / CI 脚本维护现有管理数据
- 在外部系统里接入 Metapi 后台能力,而不是手工点页面
> [!IMPORTANT]
> 本页介绍的是 **管理 API**,不是下游客户端调用的 `/v1/*` 代理接口。
> - 管理 API 使用 `AUTH_TOKEN`
> - 代理接口使用 `PROXY_TOKEN`
## 认证方式
所有受保护的管理接口都需要携带:
```http
Authorization: Bearer <AUTH_TOKEN>
```
其中:
- `AUTH_TOKEN` 就是你启动 Metapi 时配置的管理员令牌
- 如果你后来在「设置」里修改过管理员令牌,脚本里也要同步改成新值
- 管理端如果启用了 `ADMIN_IP_ALLOWLIST`,脚本调用方的来源 IP 也必须命中白名单
管理 API 不需要额外先调用“登录接口”;脚本直接带 `AUTH_TOKEN` 即可。
推荐先准备两个环境变量:
```bash
export METAPI_ADMIN_BASE_URL="http://127.0.0.1:4000"
# 或者
# export METAPI_ADMIN_BASE_URL="https://your-domain.com"
export METAPI_AUTH_TOKEN="your-admin-token"
```
之后所有请求都可以复用:
```bash
curl -sS "${METAPI_ADMIN_BASE_URL}/api/sites" \
-H "Authorization: Bearer ${METAPI_AUTH_TOKEN}"
```
## 通用约定
| 项目 | 说明 |
|------|------|
| 接口前缀 | 管理接口统一在 `/api/*` |
| 请求体 | 默认使用 `application/json` |
| 认证方式 | `Authorization: Bearer <AUTH_TOKEN>` |
| 成功判定 | 除了看 HTTP 状态码,也要看响应体里的 `success` / `error` / `message` |
| 业务失败返回 | 有些接口会返回 `200`,但响应体里是 `success: false` |
| 常见错误码 | `400` 参数错误、`401/403` 认证失败、`409` 资源冲突、`429` 触发限流 |
> [!TIP]
> 对脚本来说,最稳妥的做法是:
> 1. 先检查 HTTP 状态码
> 2. 再检查返回 JSON 里的 `success`、`error`、`message`
> 3. 不要只靠 `200` 判断调用成功
## 常用脚本流程
脚本化添加一个新站点并绑定账号,通常按下面顺序:
1. `POST /api/sites/detect` 自动识别平台
2. 如命中官方预设,记下返回的 `initializationPresetId`
3. `POST /api/sites` 创建站点
4. `POST /api/accounts/verify-token` 验证凭证
5. `POST /api/accounts` 保存账号
如果你要自动化的是 Codex / Claude / Gemini CLI / Antigravity 这类 provider 授权,则改看本文后面的 **OAuth 接口**,不要强行走普通账号导入。
最小流程示例:
```bash
# 1. 自动识别站点类型
curl -sS "${METAPI_ADMIN_BASE_URL}/api/sites/detect" \
-H "Authorization: Bearer ${METAPI_AUTH_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"url": "https://api.example.com"
}'
# 2. 创建站点
curl -sS "${METAPI_ADMIN_BASE_URL}/api/sites" \
-H "Authorization: Bearer ${METAPI_AUTH_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "My New API",
"url": "https://api.example.com",
"platform": "new-api"
}'
# 3. 验证 API Key 或 Session Token
curl -sS "${METAPI_ADMIN_BASE_URL}/api/accounts/verify-token" \
-H "Authorization: Bearer ${METAPI_AUTH_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"siteId": 1,
"accessToken": "sk-or-session-token",
"credentialMode": "apikey"
}'
# 4. 保存账号
curl -sS "${METAPI_ADMIN_BASE_URL}/api/accounts" \
-H "Authorization: Bearer ${METAPI_AUTH_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"siteId": 1,
"accessToken": "sk-or-session-token",
"credentialMode": "apikey"
}'
```
## 站点接口
### 1. 获取站点列表
`GET /api/sites`
返回当前全部站点。响应除了站点自身字段,还会带上聚合出来的 `totalBalance` 和 `subscriptionSummary`。
示例:
```bash
curl -sS "${METAPI_ADMIN_BASE_URL}/api/sites" \
-H "Authorization: Bearer ${METAPI_AUTH_TOKEN}"
```
示例响应:
```json
[
{
"id": 1,
"name": "My New API",
"url": "https://api.example.com",
"platform": "new-api",
"status": "active",
"useSystemProxy": false,
"totalBalance": 42.5,
"subscriptionSummary": null
}
]
```
### 2. 自动识别站点类型
`POST /api/sites/detect`
用于在真正创建站点前,先根据 URL 探测平台类型;如果命中官方预设,还会顺带返回 `initializationPresetId`。
请求体:
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `url` | `string` | 是 | 待识别的站点地址。通用平台通常传根地址;如果你想命中官方预设,则直接传预设对应的完整地址 |
示例:
```bash
curl -sS "${METAPI_ADMIN_BASE_URL}/api/sites/detect" \
-H "Authorization: Bearer ${METAPI_AUTH_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"url": "https://api.example.com"
}'
```
成功响应:
```json
{
"url": "https://api.example.com",
"platform": "new-api"
}
```
命中预设时,响应可能类似:
```json
{
"url": "https://coding.dashscope.aliyuncs.com/v1",
"platform": "openai",
"initializationPresetId": "codingplan-openai"
}
```
失败响应:
```json
{
"error": "Could not detect platform"
}
```
### 3. 创建站点
`POST /api/sites`
请求体字段:
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `name` | `string` | 是 | 站点名称 |
| `url` | `string` | 是 | 站点地址。通用平台通常用根地址;官方预设可直接保留完整官方路径 |
| `platform` | `string` | 否 | 平台类型;留空时服务端会再次尝试自动识别 |
| `initializationPresetId` | `string` | 否 | 官方预设 ID;用于保留预设语义、初始化建议和后续跳转 |
| `proxyUrl` | `string \| null` | 否 | 站点专用代理地址,支持 `http(s)` / `socks` |
| `useSystemProxy` | `boolean` | 否 | 是否使用全局 `SYSTEM_PROXY_URL` |
| `customHeaders` | `string \| null` | 否 | 自定义请求头,注意这里传的是 **JSON 字符串** |
| `externalCheckinUrl` | `string \| null` | 否 | 外部签到地址,需为 `http(s)` |
| `status` | `string` | 否 | `active` 或 `disabled` |
| `isPinned` | `boolean` | 否 | 是否置顶 |
| `sortOrder` | `number` | 否 | 排序值,非负整数 |
| `globalWeight` | `number` | 否 | 站点全局权重,必须为正数 |
| `apiEndpoints` | `array` | 否 | 仅用于 API 请求的数据面地址池;不替代主站点 URL |
示例:
```bash
curl -sS "${METAPI_ADMIN_BASE_URL}/api/sites" \
-H "Authorization: Bearer ${METAPI_AUTH_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "Proxy Site",
"url": "https://proxy-site.example.com",
"platform": "new-api",
"proxyUrl": "socks5://127.0.0.1:1080",
"useSystemProxy": true,
"customHeaders": "{\"cf-access-client-id\":\"site-client-id\",\"x-site-scope\":\"internal\"}",
"externalCheckinUrl": "https://checkin.example.com/welfare",
"globalWeight": 1.5
}'
```
成功响应示例:
```json
{
"id": 1,
"name": "Proxy Site",
"url": "https://proxy-site.example.com",
"platform": "new-api",
"proxyUrl": "socks5://127.0.0.1:1080",
"useSystemProxy": true,
"customHeaders": "{\"cf-access-client-id\":\"site-client-id\",\"x-site-scope\":\"internal\"}",
"externalCheckinUrl": "https://checkin.example.com/welfare",
"status": "active",
"globalWeight": 1.5
}
```
常见失败:
- `409`:同一个 `(platform, url)` 已存在
- `400`:`proxyUrl`、`externalCheckinUrl`、`customHeaders`、`globalWeight` 等字段格式不合法
> [!TIP]
> `customHeaders` 不是对象,而是 JSON 字符串;并且 value 必须是字符串。
### 4. 使用官方预设创建站点
如果你前一步 `detect` 已经拿到了 `initializationPresetId`,推荐在创建时显式带回去:
```bash
curl -sS "${METAPI_ADMIN_BASE_URL}/api/sites" \
-H "Authorization: Bearer ${METAPI_AUTH_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "Aliyun CodingPlan",
"url": "https://coding.dashscope.aliyuncs.com/v1",
"platform": "openai",
"initializationPresetId": "codingplan-openai"
}'
```
这样响应里也会保留该预设信息,便于脚本按预设类型做后续分流。
## 账号接口
### 1. 获取账号列表
`GET /api/accounts`
返回账号快照对象,而不是裸数组。响应里包含:
- `generatedAt`:快照生成时间
- `accounts`:当前全部账号,以及关联站点、凭证模式、能力信息、今日花费、运行健康状态等聚合字段
- `sites`:前端常用的站点列表快照
示例:
```bash
curl -sS "${METAPI_ADMIN_BASE_URL}/api/accounts" \
-H "Authorization: Bearer ${METAPI_AUTH_TOKEN}"
```
示例响应:
```json
{
"generatedAt": "2026-04-09T06:00:00.000Z",
"accounts": [
{
"id": 1,
"siteId": 1,
"username": "alice",
"status": "active",
"credentialMode": "apikey",
"capabilities": {
"canCheckin": false,
"canRefreshBalance": false,
"proxyOnly": true
},
"site": {
"id": 1,
"name": "My New API",
"url": "https://api.example.com",
"platform": "new-api"
}
}
],
"sites": [
{
"id": 1,
"name": "My New API",
"url": "https://api.example.com",
"platform": "new-api"
}
]
}
```
### 2. 验证凭证
`POST /api/accounts/verify-token`
建议在真正保存账号前先调用一次。这样脚本可以在入库前拿到更明确的错误信息。
请求体字段:
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `siteId` | `number` | 是 | 目标站点 ID |
| `accessToken` | `string` | 是 | Session Token / Session Cookie / API Key |
| `platformUserId` | `number` | 否 | 某些 `new-api` / `anyrouter` 站点需要手动传 |
| `credentialMode` | `string` | 否 | `auto` / `session` / `apikey`,脚本里推荐显式传 |
#### 验证 API Key
```bash
curl -sS "${METAPI_ADMIN_BASE_URL}/api/accounts/verify-token" \
-H "Authorization: Bearer ${METAPI_AUTH_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"siteId": 1,
"accessToken": "sk-fast-verify",
"credentialMode": "apikey"
}'
```
成功响应:
```json
{
"success": true,
"tokenType": "apikey",
"modelCount": 2,
"models": ["gpt-5-mini", "gpt-4o-mini"]
}
```
#### 验证 Session Token
```bash
curl -sS "${METAPI_ADMIN_BASE_URL}/api/accounts/verify-token" \
-H "Authorization: Bearer ${METAPI_AUTH_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"siteId": 1,
"accessToken": "session=xxxx",
"credentialMode": "session"
}'
```
成功响应:
```json
{
"success": true,
"tokenType": "session",
"userInfo": {
"username": "alice"
},
"balance": 12.34,
"apiToken": "sk-generated-by-upstream"
}
```
> [!TIP]
> 对 `new-api` / `anyrouter` 一类站点,如果返回 `needsUserId: true`,说明脚本里要补上 `platformUserId` 再重试。
### 3. 保存账号
`POST /api/accounts`
请求体字段:
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `siteId` | `number` | 是 | 目标站点 ID |
| `accessToken` | `string` | 是 | 输入凭证;即使是 API Key 模式,也先放这里 |
| `username` | `string` | 否 | 用户名;Session 模式下通常可由服务端自动识别 |
| `apiToken` | `string` | 否 | 可显式指定要保存的 API Key;留空时服务端会自动推断 |
| `platformUserId` | `number` | 否 | 平台用户 ID,New API 系站点常用 |
| `checkinEnabled` | `boolean` | 否 | Session 模式下是否开启签到 |
| `credentialMode` | `string` | 否 | `auto` / `session` / `apikey` |
| `refreshToken` | `string` | 否 | `sub2api` 会话可附带刷新令牌 |
| `tokenExpiresAt` | `number \| string` | 否 | `sub2api` 会话过期时间戳 |
| `skipModelFetch` | `boolean` | 否 | 为 `true` 时可跳过即时模型拉取 |
#### 保存 API Key 账号
```bash
curl -sS "${METAPI_ADMIN_BASE_URL}/api/accounts" \
-H "Authorization: Bearer ${METAPI_AUTH_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"siteId": 1,
"accessToken": "sk-proxy-only",
"credentialMode": "apikey"
}'
```
成功响应示例:
```json
{
"id": 1,
"siteId": 1,
"tokenType": "apikey",
"credentialMode": "apikey",
"capabilities": {
"canCheckin": false,
"canRefreshBalance": false,
"proxyOnly": true
},
"modelCount": 1,
"apiTokenFound": true,
"queued": true,
"jobId": "task_xxx",
"message": "已添加为 API Key 账号,后台正在同步模型和路由信息。"
}
```
#### 保存 Session 账号
```bash
curl -sS "${METAPI_ADMIN_BASE_URL}/api/accounts" \
-H "Authorization: Bearer ${METAPI_AUTH_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"siteId": 1,
"accessToken": "session=xxxx",
"credentialMode": "session",
"platformUserId": 1001,
"checkinEnabled": true
}'
```
常见失败:
- `400` + `requiresVerification: true`:凭证还没有通过验证,建议先调用 `/api/accounts/verify-token`
- `400`:`siteId` 不存在、Token 为空、凭证类型与 `credentialMode` 不匹配
> [!TIP]
> 对纯脚本导入场景,建议始终显式传 `credentialMode`,不要依赖 `auto` 推断。
### 4. 重新绑定 Session Token
`POST /api/accounts/:id/rebind-session`
用于账号 Session 过期后,脚本直接把新 Session Token 重新写回账号。
请求体字段:
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `accessToken` | `string` | 是 | 新的 Session Token |
| `platformUserId` | `number` | 否 | 覆盖或补充平台用户 ID |
| `refreshToken` | `string` | 否 | `sub2api` 会话可附带 |
| `tokenExpiresAt` | `number \| string` | 否 | `sub2api` 会话过期时间戳 |
示例:
```bash
curl -sS "${METAPI_ADMIN_BASE_URL}/api/accounts/1/rebind-session" \
-H "Authorization: Bearer ${METAPI_AUTH_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"accessToken": "session=new-token",
"platformUserId": 1001
}'
```
成功响应示例:
```json
{
"success": true,
"tokenType": "session",
"credentialMode": "session",
"apiTokenFound": true
}
```
## OAuth 接口
如果你要自动化的是 Codex / Claude / Gemini CLI / Antigravity 这类 provider 授权,请使用下面这组接口。
### 1. 获取可用 provider
`GET /api/oauth/providers`
示例:
```bash
curl -sS "${METAPI_ADMIN_BASE_URL}/api/oauth/providers" \
-H "Authorization: Bearer ${METAPI_AUTH_TOKEN}"
```
示例响应:
```json
{
"providers": [
{
"provider": "codex",
"label": "Codex",
"platform": "codex",
"enabled": true,
"loginType": "oauth",
"requiresProjectId": false,
"supportsDirectAccountRouting": true,
"supportsCloudValidation": true,
"supportsNativeProxy": true
}
]
}
```
### 2. 启动 OAuth 授权
`POST /api/oauth/providers/:provider/start`
请求体字段:
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `projectId` | `string` | 否 | 某些 provider 可选传入的项目 ID,例如 Gemini CLI |
| `proxyUrl` | `string \| null` | 否 | 本次 OAuth 流程使用的显式代理;传 `null` 可清空继承代理 |
示例:
```bash
curl -sS "${METAPI_ADMIN_BASE_URL}/api/oauth/providers/codex/start" \
-H "Authorization: Bearer ${METAPI_AUTH_TOKEN}" \
-H "Content-Type: application/json" \
-d '{}'
```
成功响应:
```json
{
"provider": "codex",
"state": "oauth-state-123",
"authorizationUrl": "https://auth.example.com/authorize?...",
"instructions": {
"redirectUri": "http://localhost:1455/auth/callback",
"callbackPort": 1455,
"callbackPath": "/auth/callback",
"manualCallbackDelayMs": 15000
}
}
```
### 3. 查询 OAuth 会话状态
`GET /api/oauth/sessions/:state`
示例:
```bash
curl -sS "${METAPI_ADMIN_BASE_URL}/api/oauth/sessions/oauth-state-123" \
-H "Authorization: Bearer ${METAPI_AUTH_TOKEN}"
```
典型状态包括:
- `pending`
- `success`
- `error`
### 4. 手动回填 callback URL
`POST /api/oauth/sessions/:state/manual-callback`
当浏览器已经完成授权,但回调没有自动打到 Metapi 时,可以手动提交最终 callback URL。
```bash
curl -sS "${METAPI_ADMIN_BASE_URL}/api/oauth/sessions/oauth-state-123/manual-callback" \
-H "Authorization: Bearer ${METAPI_AUTH_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"callbackUrl": "http://localhost:1455/auth/callback?code=demo&state=oauth-state-123"
}'
```
### 5. 列出已有 OAuth 连接
`GET /api/oauth/connections`
示例:
```bash
curl -sS "${METAPI_ADMIN_BASE_URL}/api/oauth/connections?limit=50&offset=0" \
-H "Authorization: Bearer ${METAPI_AUTH_TOKEN}"
```
### 6. 重绑或删除 OAuth 连接
| 接口 | 作用 |
|------|------|
| `POST /api/oauth/connections/:accountId/rebind` | 为已有 OAuth 账号重新发起授权 |
| `DELETE /api/oauth/connections/:accountId` | 删除 OAuth 连接 |
## 常见失败返回
### 站点创建冲突
当同一个 `(platform, url)` 已存在时,会返回:
```json
{
"error": "A new-api site with URL https://api.example.com already exists."
}
```
### 需要补平台用户 ID
某些 `new-api` / `anyrouter` 站点会返回:
```json
{
"success": false,
"needsUserId": true,
"message": "This site requires a user ID. Please fill in your site user ID."
}
```
这时请在 `/api/accounts/verify-token` 或 `/api/accounts` 里补上 `platformUserId`。
### 平台用户 ID 不匹配
```json
{
"success": false,
"invalidUserId": true,
"message": "The provided user ID does not match this token. Please check your site user ID."
}
```
### 命中防护盾
```json
{
"success": false,
"shieldBlocked": true,
"message": "This site is shielded by anti-bot challenge. Create an API key on the target site and import that key."
}
```
这通常意味着账号密码或 Session 流程被防护页拦住了,更适合改用 API Key 模式。
### 验证过于频繁
`POST /api/accounts/verify-token` 对同一来源 IP 有速率限制。超过限制后会返回:
```json
{
"success": false,
"message": "请求过于频繁,请稍后再试"
}
```
建议脚本在这类错误上做退避重试。
## 下一步
- [上游接入](./upstream-integration.md) — 查看不同站点类型应该怎么填 URL、凭证和 User ID
- [OAuth 管理](/oauth) — 查看浏览器授权、回调和重绑逻辑
- [配置说明](./configuration.md) — 查看 `AUTH_TOKEN`、`ADMIN_IP_ALLOWLIST`、系统代理等配置项
- [客户端接入](./client-integration.md) — 如果你要接的是 `/v1/*` 代理接口而不是后台管理接口,请看这里
-224
View File
@@ -1,224 +0,0 @@
# 🔐 OAuth 管理
本文档介绍 Metapi 里的「OAuth 管理」页面,适合需要直接授权 provider 账号的场景。
[返回文档中心](/)
---
## 这页解决什么问题
不是所有上游都适合手填 API Key、Access Token 或 Cookie。
对下面这类 provider 账号,更推荐直接走 OAuth:
- Codex
- Claude
- Gemini CLI
- Antigravity
这类接法的特点是:
- 使用浏览器授权,而不是手填用户名密码
- 授权成功后,Metapi 会自动创建或复用对应 provider 的站点
- 账号会按 OAuth 连接保存,后续刷新和重绑也走同一套流程
如果你接的是 New API、One API、Sub2API、CPA、OpenAI-compatible、Claude-compatible 这类**普通站点或网关**,请看 [上游接入](/upstream-integration)。
---
## 入口在哪里
管理后台左侧菜单已经有独立入口:
```text
OAuth 管理
```
这不是站点编辑器里的一个隐藏选项,而是和「站点管理」「账号管理」并列的一页。
---
## 当前支持的 provider
| Provider | 对应平台 | 自动创建的站点名 | 典型用途 |
|------|------|------|------|
| Codex | `codex` | `ChatGPT Codex OAuth` | 直接用 Codex 账号授权 |
| Claude | `claude` | `Anthropic Claude OAuth` | 直接用 Claude / Anthropic 账号授权 |
| Gemini CLI | `gemini-cli` | `Google Gemini CLI OAuth` | 复用 Gemini CLI / Google Cloud 账号授权,可选输入 Project ID |
| Antigravity | `antigravity` | `Google Antigravity OAuth` | 复用 Antigravity 账号授权 |
授权完成后,这些站点会出现在「站点管理」里,但通常**不建议你手动创建**它们。
---
## 授权前的准备
### 1. 确保 Metapi 能访问 provider 的 OAuth 端点
如果你的部署环境访问外网受限,可以:
- 先配置全局 `SYSTEM_PROXY_URL`
- 或在 OAuth 启动 / 重绑时使用单次代理参数
相关环境变量见 [配置说明](/configuration) 里的「OAuth 与 Provider 登录」一节。
### 2. 远程部署要提前考虑回调方式
OAuth 默认使用 Metapi 本机上的 loopback 回调地址,例如本机 `127.0.0.1` 端口。
如果你的 Metapi 跑在远程服务器上,而浏览器跑在本地电脑上,常见做法有两个:
1. **SSH 隧道**:按页面给出的命令,把回调端口转发到远端
2. **手动回填 callback URL**:如果浏览器已经完成授权,但自动回调没打通,可把最终回调地址手动贴回管理页
### 3. 了解它和普通站点接入的边界
OAuth 连接更适合“provider 原生账号授权”,而不是:
- 面板站点账号密码登录
- New API / One API 后台管理
- CPA / OpenAI-compatible 的普通 API Key 托管
---
## 标准流程
### 步骤 1:打开 OAuth 管理
进入左侧菜单「OAuth 管理」,等待页面加载 provider 列表和已有连接。
### 步骤 2:点击要连接的 provider
页面会为 provider 发起一个 OAuth 会话,并弹出授权窗口。
Metapi 会同时给出:
- 授权链接
- 本机回调端口
- 手动回填等待时间
- 如果当前是远程访问,还会给出 SSH 隧道命令模板
### 步骤 3:在 provider 页面完成授权
完成授权后,Metapi 会轮询当前会话状态:
- `pending`:等待回调
- `success`:授权成功
- `error`:授权失败,需要重新检查浏览器回调或网络
### 步骤 4:必要时手动回填 callback URL
如果弹窗里已经能看到类似 `...?code=...&state=...` 的回调地址,但 Metapi 页面还没成功:
1. 复制浏览器最终回调 URL
2. 回到「OAuth 管理」
3. 粘贴到手动回填区域提交
### 步骤 5:确认站点与连接都已出现
成功后通常会看到两层结果:
- 「OAuth 管理」页里出现新的连接记录
- 「站点管理」页里出现对应 provider 的站点行
---
## 和普通站点 / API Key 的区别
| 方式 | 入口 | 适合什么 | 典型例子 |
|------|------|------|------|
| 普通站点 + Session | 站点管理 / 账号管理 | 有后台面板,需要签到、余额、账号令牌管理 | New API、One API、DoneHub、AnyRouter、Sub2API |
| 普通站点 + API Key | 站点管理 / API Key 管理 | 只关心代理调用和模型列表 | OpenAI-compatible、Claude-compatible、CPA |
| OAuth 连接 | OAuth 管理 | 需要 provider 官方授权、刷新、重绑 | Codex、Claude、Gemini CLI、Antigravity |
简单判断:
- 你拿到的是 **站点后台地址**,优先看 [上游接入](/upstream-integration)
- 你拿到的是 **provider 登录授权**,优先看这页
---
## 自动生成的站点有什么用
OAuth 成功后,Metapi 会确保对应 provider 的站点存在。这样做是为了让 OAuth 连接也能融入现有的:
- 站点列表
- 路由通道
- 账号归属
- 代理与重绑逻辑
但这类站点和普通面板站点仍然不同:
- 它们通常不是拿来手动登录的
- 不要把它理解成“我又多了一个可签到的面板站”
- 更准确地说,它是 OAuth 账号在 Metapi 里的宿主站点
---
## 管理 API 里怎么自动化
如果你想脚本化处理 OAuth,可以用这些接口:
| 接口 | 作用 |
|------|------|
| `GET /api/oauth/providers` | 获取当前可用 provider 列表 |
| `POST /api/oauth/providers/:provider/start` | 启动 OAuth 流程 |
| `GET /api/oauth/sessions/:state` | 轮询会话状态 |
| `POST /api/oauth/sessions/:state/manual-callback` | 手动回填 callback URL |
| `GET /api/oauth/connections` | 列出现有连接 |
| `POST /api/oauth/connections/:accountId/rebind` | 重绑已有 OAuth 连接 |
| `DELETE /api/oauth/connections/:accountId` | 删除 OAuth 连接 |
脚本示例见 [管理 API](/management-api)。
---
## 常见问题
### provider 显示“当前不可用”
通常说明回调监听器不可用,或当前 provider 的启动条件不满足。优先检查:
1. 服务是否刚启动但回调监听失败
2. 端口是否被占用
3. 当前环境是否缺少必要的 OAuth 配置
### 浏览器授权成功了,但页面一直停在“等待授权完成”
优先怀疑回调链路没通:
1. 如果是远程服务器,先按页面提示建 SSH 隧道
2. 如果不方便建隧道,直接用手动 callback 回填
3. 如果 provider 页面最终没有 `code` / `state` 参数,说明授权本身还没成功
### OAuth 连接需要系统代理吗
有可能需要,尤其是:
- 国内服务器访问 OpenAI / Anthropic / Google OAuth 端点
- 服务器本身不能直连 provider
优先使用:
1. 全局 `SYSTEM_PROXY_URL`
2. OAuth 启动 / 重绑时指定单次代理
### OAuth 成功后为什么还会在站点管理看到一行站点
这是预期行为。Metapi 需要一个明确的站点记录来承载:
- 账号所属平台
- 路由与通道归属
- 后续重绑 / 刷新逻辑
它不代表你又新增了一个普通面板站点。
---
## 相关文档
- [上游接入](/upstream-integration)
- [管理 API](/management-api)
- [配置说明](/configuration)
- [常见问题 FAQ](/faq)
-234
View File
@@ -1,234 +0,0 @@
# 🔧 运维手册
[返回文档中心](./README.md)
---
## 数据备份
### 方式一:目录备份(SQLite / Desktop,推荐)
如果当前运行的是 SQLite,最简单的备份方式是直接备份数据目录:
- Docker / 本地开发:仓库内的 `data/`
- Desktop:应用用户数据目录下的 `data/` 子目录
```bash
# 手动备份
cp -r data/ data-backup-$(date +%Y%m%d)/
# 自动备份(crontab)
0 2 * * * cp -r /path/to/metapi/data/ /path/to/backups/metapi-$(date +\%Y\%m\%d)/
```
建议:
- 每日自动备份一次
- 保留最近 7~30 天
- Desktop 备份前先退出应用
- 如果当前运行库已切到 MySQL / Postgres,不能只备份 `data/`
- 备份文件不要提交到 Git
### 方式二:数据库原生备份(MySQL / PostgreSQL)
如果当前运行库已经切到 MySQL / Postgres,请使用数据库自己的备份工具或云快照。
**MySQL 备份示例:**
```bash
# 全量导出(替换为你的实际连接信息)
mysqldump -h <HOST> -u <USER> -p<PASSWORD> metapi > metapi-backup-$(date +%Y%m%d).sql
# 自动备份(crontab,每天凌晨 3 点)
0 3 * * * mysqldump -h <HOST> -u <USER> -p<PASSWORD> metapi | gzip > /path/to/backups/metapi-$(date +\%Y\%m\%d).sql.gz
```
**PostgreSQL 备份示例:**
```bash
# 全量导出
pg_dump -h <HOST> -U <USER> -d metapi -F c -f metapi-backup-$(date +%Y%m%d).dump
# 自动备份(crontab,每天凌晨 3 点,使用 .pgpass 免交互密码)
0 3 * * * pg_dump -h <HOST> -U <USER> -d metapi -F c -f /path/to/backups/metapi-$(date +\%Y\%m\%d).dump
```
**云托管数据库:** RDS、PlanetScale、Neon 等可直接使用平台的自动快照功能。
建议:
- 升级、迁移、执行「重新初始化系统」前先做一次库级备份
- 备份对象是当前 metapi 正在使用的运行库,而不只是本地 `data/`
- 恢复后重启 Metapi 一次,确认它重新连接到了正确的运行库
- 建议保留最近 7~30 天的备份,定期清理过期文件
### 方式三:应用内导出
在管理后台 → 「导入/导出」页面:
- **全量导出**:站点、账号、Token、路由、设置
- **仅账号**:站点和账号信息
- **仅偏好**:设置和通知配置
导出为 JSON 文件,可用于跨实例迁移。
## 数据恢复
### 目录恢复(SQLite / Desktop)
服务端 SQLite 可按下面流程恢复;Desktop 则先退出应用,再替换应用用户数据目录下的 `data/` 后重新启动。
```bash
# 1. 停止容器
docker compose down
# 2. 替换数据目录
rm -rf data/
cp -r data-backup-20260228/ data/
# 3. 重新启动
docker compose up -d
```
### 应用内导入
在管理后台 → 「导入/导出」页面上传之前导出的 JSON 文件。系统会自动校验数据完整性。
### 数据库恢复(MySQL / PostgreSQL)
如果当前运行库是 MySQL / Postgres,请先用数据库自己的恢复流程把备份恢复到目标库,再重启 Metapi。若实例保存过运行库配置,重启后仍会优先连接该外部库,而不是自动回退到本地 SQLite。
## 日志排查
### Docker 环境
```bash
# 查看实时日志
docker compose logs -f
# 查看最近 100 行
docker compose logs --tail 100
# 只看错误
docker compose logs -f 2>&1 | grep -i error
```
### 本地开发
```bash
npm run dev
# 日志直接输出到终端
```
### Desktop
- 优先使用托盘菜单的 `Open Logs Folder`
- Desktop 内置后端的数据目录和日志目录都位于应用用户数据目录下
### 重点关注的日志
| 关键词 | 含义 | 处理方式 |
|--------|------|----------|
| `auth failed` | 上游站点鉴权失败 | 检查账号凭证是否过期,系统会自动尝试重登录 |
| `no available channel` | 路由无可用通道 | 检查 Token 是否同步、通道是否被冷却;可在路由页查看冷却状态 |
| `channel cooling` | 通道进入冷却期 | 通道在请求失败后自动冷却 10 分钟,期间不会被路由选中;无需干预,会自动恢复 |
| `upstream 429` | 上游限流 | 该上游站点触发了速率限制;路由引擎会自动切换其他通道,冷却期后重试 |
| `upstream 5xx` | 上游服务器错误 | 上游站点临时不可用,路由引擎会自动故障转移到其他通道 |
| `notify failed` | 通知发送失败 | 检查 [通知渠道配置](./configuration.md#通知渠道) |
| `checkin failed` | 签到失败 | 检查账号状态和站点连通性 |
| `balance refresh failed` | 余额刷新失败 | 检查账号凭证,可能需要重新登录 |
| `proxy timeout` | 代理请求超时 | 上游响应过慢;检查网络延迟或考虑切换其他通道 |
| `token expired` | Token 过期 | 系统会自动尝试续签;若反复出现,手动刷新 Token |
## 健康检查
### 手动检查
```bash
# 检查服务是否响应
curl -sS http://localhost:4000/v1/models \
-H "Authorization: Bearer <PROXY_TOKEN>" | head -5
# 检查特定模型可用性
curl -sS http://localhost:4000/v1/chat/completions \
-H "Authorization: Bearer <PROXY_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"ping"}]}'
```
以上示例默认服务端部署监听 `localhost:4000`。Desktop 内置后端默认监听 `0.0.0.0:4000`;本机排查通常可直接使用 `127.0.0.1:4000`,如果显式设置了 `METAPI_DESKTOP_SERVER_PORT`,则按日志里的实际端口访问;局域网排查改用当前机器的实际 IP。
### 自动化监控建议
- 定时请求 `/v1/models`,检查返回状态码和模型数量
- 定时抽样请求 `/v1/chat/completions`,检查端到端可用性
- SQLite / Desktop:监控磁盘空间(SQLite WAL 日志可能增长)
- MySQL / Postgres:监控外部数据库空间、连接数和慢查询
- 监控 Docker 容器状态
## 常见运维操作
### 清理代理日志
代理日志会持续增长。如果磁盘空间紧张,可在管理后台 → 代理日志页面清理历史记录。
### 重置账号状态
如果账号状态异常(`unhealthy`),可以在账号管理页面:
1. 点击「刷新」重新检测账号健康状态
2. 如凭证过期,系统会尝试自动重登录
3. 手动禁用/启用账号
### 强制刷新模型
在管理后台手动触发:
- 余额刷新:立即更新所有账号余额
- 模型刷新:重新发现所有上游模型
- 签到:立即执行一次签到
### 系统代理
在管理后台「设置 → 系统代理」中保存全局代理地址后,只有启用了「使用系统代理」的站点才会走这条出站代理。
- 单个站点可在站点页直接开关
- 站点页支持批量开启 / 关闭系统代理
- 修改系统代理地址后,Metapi 会自动失效站点代理缓存,通常无需手工重启
### 清理缓存并重建路由
当模型列表(`GET /v1/models`)、路由列表或选择概率明显滞后时,使用以下操作:
| 操作 | 位置 | 适用场景 |
|------|------|----------|
| **清除缓存并重建路由** | 设置 → 清除缓存并重建路由 | 全局刷新:清空模型发现缓存、自动路由和自动通道,后台触发模型刷新与路由重建 |
| **重建路由** | TokenRoutes → 重建路由 | 局部刷新:调整账号、Token、路由规则后手动重新生成自动路由 |
优先使用「重建路由」做局部刷新,问题持续时再用全局清除。
### 批量操作
管理后台支持以下批量运维动作:
- 站点:批量启用、禁用、删除、开启系统代理、关闭系统代理
- 账号:批量刷新余额、启用、禁用、删除
- Token:批量启用、禁用、删除
批量操作完成后,界面会返回成功/失败数量;删除站点或账号后,路由相关缓存会自动失效。
### 重新初始化系统
这是高风险操作,位于「设置 → 危险操作」。
- 会清空当前 metapi 正在使用的全部业务数据
- 如果当前运行在外部 MySQL / Postgres,会先清空该外部库中的 metapi 数据,再切回默认 SQLite
- 管理员 Token 会重置为 `change-me-admin-token`
- 当前登录会话会立即退出,页面刷新后回到首装状态
执行前建议先做一次导出或数据库备份。
## 下一步
- [常见问题](./faq.md) — 常见报错与修复
- [配置说明](./configuration.md) — 环境变量详解
- [上游接入](./upstream-integration.md) — 平台特定的连接与排障
- [客户端接入](./client-integration.md) — 下游客户端对接
@@ -1,109 +0,0 @@
# Platform Adapter Alignment Design
**Date**: 2026-03-02
**Goal**: Align metapi platform adapters with all-api-hub behavior for full platform compatibility.
## Background
metapi supports 11 upstream platforms, but several adapters are incomplete compared to all-api-hub's implementations. This design covers 5 changes to close the gaps.
## Changes
### 1. Compatibility User-ID Headers (NewApiAdapter)
**Problem**: metapi only sends `New-Api-User` header. VoAPI, RIX_API, neo-Api, and other forks read different header names.
**Solution**: Modify `NewApiAdapter.authHeaders()` to fan out the userId across all 6 known header names:
- `New-API-User`, `Veloera-User`, `voapi-user`, `User-id`, `Rix-Api-User`, `neo-api-user`
**Files**: `src/server/services/platforms/newApi.ts`
**Impact**: All NewAPI-family platforms (new-api, anyrouter, vo-api, super-api, rix-api, neo-api) benefit automatically.
---
### 2. Sub2API Adapter Rewrite
**Problem**: Current Sub2ApiAdapter extends OneApiAdapter (9 lines, detection only). Sub2API uses JWT auth with completely different endpoints.
**Solution**: Rewrite Sub2ApiAdapter as standalone class extending BasePlatformAdapter:
- Auth: JWT Bearer token (no cookie, no session)
- User info: `GET /api/v1/auth/me` → `{ code: 0, data: { id, username, email, balance_usd } }`
- Balance: Convert `balance_usd` to quota units
- Check-in: Not supported (return unsupported)
- Models: `GET /v1/models` (OpenAI-compatible, same as current)
- Login: Not supported (JWT only)
- Token management: Not available
**Files**: `src/server/services/platforms/sub2api.ts`
---
### 3. Veloera Adapter Enhancement
**Problem**: metapi uses OneAPI's `/api/user/checkin` for Veloera check-in, but Veloera uses a different endpoint.
**Solution**: Override `checkin()` to use Veloera's actual endpoint:
- Check-in: `POST /api/user/checkin` (same path but Veloera also supports `/api/user/check_in_status` for status)
- Quota conversion: Already correct at 1,000,000 divisor
Note: Veloera's checkin path is actually the same as NewAPI. The key difference in all-api-hub is the check-in *status* endpoint (`/api/user/check_in_status`), which metapi doesn't use directly. The existing checkin implementation is likely fine. Main enhancement: add compat headers support.
**Files**: `src/server/services/platforms/veloera.ts`
---
### 4. OneHub Adapter Enhancement
**Problem**: OneHubAdapter is 10 lines, just URL detection. Missing OneHub-specific endpoints.
**Solution**: Add OneHub-specific overrides:
- Models: `GET /api/available_model` as fallback (returns `{ data: { model_name: {...}, ... } }`)
- User groups: `GET /api/user_group_map` (returns `{ data: { group_name: ratio, ... } }`)
- Token listing: Handle `{ data: [...] }` response envelope (vs bare array)
**Files**: `src/server/services/platforms/oneHub.ts`
---
### 5. DoneHub Adapter Enhancement
**Problem**: DoneHubAdapter has checkin disabled + fallback model discovery. Missing DoneHub-specific features.
**Solution**: Add DoneHub-specific overrides:
- Inherit OneHub's model/group methods
- Override model discovery fallback: try `/api/available_model` (already done)
- Keep checkin disabled (already done)
DoneHub is already partially implemented. The main missing piece is inheriting from OneHubAdapter instead of OneApiAdapter to get OneHub's enhanced methods.
**Files**: `src/server/services/platforms/doneHub.ts`
---
## Architecture
No new patterns introduced. All changes fit within the existing class hierarchy:
```
BasePlatformAdapter
├── OneApiAdapter
│ ├── OneHubAdapter (enhanced)
│ │ └── DoneHubAdapter (re-parented)
│ └── (Sub2ApiAdapter removed from this branch)
├── NewApiAdapter (compat headers added)
│ └── AnyRouterAdapter
├── VeloeraAdapter (minor enhancement)
├── Sub2ApiAdapter (rewritten, now extends BasePlatformAdapter)
└── ... (OpenAI, Claude, Gemini, CLIProxyAPI unchanged)
```
## Detection Order
Update detection order in `index.ts` to ensure Sub2API is detected before OneAPI (already the case).
## Risk Assessment
- **Low risk**: Header changes are additive (extra headers don't break existing platforms)
- **Medium risk**: Sub2API rewrite changes auth flow entirely (but current implementation doesn't work anyway)
- **Low risk**: OneHub/DoneHub enhancements add fallback paths (existing paths still work)
@@ -1,841 +0,0 @@
# Platform Adapter Alignment Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Align metapi's platform adapters with all-api-hub behavior so all supported platforms (VoAPI, Super-API, RIX_API, Neo-API, Sub2API, OneHub, DoneHub, Veloera) work correctly.
**Architecture:** Modify 5 existing adapter files within the class-based adapter hierarchy. No new patterns or dependencies. All changes are additive or replacement of stub implementations.
**Tech Stack:** TypeScript, Node.js, undici (HTTP), vitest (testing)
---
### Task 1: Add Compatibility User-ID Headers to NewApiAdapter
**Files:**
- Modify: `src/server/services/platforms/newApi.ts:32-36`
- Test: `src/server/services/platforms/newApi.test.ts`
**Step 1: Write the failing test**
Add a test that verifies all 6 compat headers are sent when userId is present.
```typescript
// In newApi.test.ts, add:
it('sends all compatibility user-id headers when userId is known', async () => {
const receivedHeaders: Record<string, string> = {};
server = createServer((req, res) => {
// Capture headers from the request
for (const name of ['new-api-user', 'veloera-user', 'voapi-user', 'user-id', 'rix-api-user', 'neo-api-user']) {
const val = req.headers[name];
if (val) receivedHeaders[name] = String(val);
}
if (req.url === '/api/user/self') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, data: { id: 42, username: 'test', quota: 500000, used_quota: 0 } }));
return;
}
res.writeHead(404).end();
});
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', () => resolve()));
const addr = server.address() as AddressInfo;
baseUrl = `http://127.0.0.1:${addr.port}`;
const adapter = new NewApiAdapter();
// Use a fake JWT with id=42 so userId is discovered
const fakeJwt = `header.${Buffer.from(JSON.stringify({ id: 42 })).toString('base64url')}.sig`;
await adapter.getBalance(baseUrl, fakeJwt, 42);
expect(receivedHeaders['new-api-user']).toBe('42');
expect(receivedHeaders['veloera-user']).toBe('42');
expect(receivedHeaders['voapi-user']).toBe('42');
expect(receivedHeaders['user-id']).toBe('42');
expect(receivedHeaders['rix-api-user']).toBe('42');
expect(receivedHeaders['neo-api-user']).toBe('42');
});
```
**Step 2: Run test to verify it fails**
Run: `cd /d/Desktop/metapi工作区/metapi && npx vitest run src/server/services/platforms/newApi.test.ts`
Expected: FAIL — only `new-api-user` header is present
**Step 3: Write minimal implementation**
In `src/server/services/platforms/newApi.ts`, replace the `authHeaders` method (lines 32-36):
```typescript
// Before:
private authHeaders(accessToken: string, userId?: number): Record<string, string> {
const headers: Record<string, string> = { Authorization: `Bearer ${accessToken}` };
if (userId) headers['New-Api-User'] = String(userId);
return headers;
}
// After:
private authHeaders(accessToken: string, userId?: number): Record<string, string> {
const headers: Record<string, string> = { Authorization: `Bearer ${accessToken}` };
if (userId) {
const value = String(userId);
headers['New-API-User'] = value;
headers['Veloera-User'] = value;
headers['voapi-user'] = value;
headers['User-id'] = value;
headers['Rix-Api-User'] = value;
headers['neo-api-user'] = value;
}
return headers;
}
```
**Step 4: Run test to verify it passes**
Run: `cd /d/Desktop/metapi工作区/metapi && npx vitest run src/server/services/platforms/newApi.test.ts`
Expected: PASS
**Step 5: Run all existing tests to check for regressions**
Run: `cd /d/Desktop/metapi工作区/metapi && npx vitest run`
Expected: All tests PASS
**Step 6: Commit**
```bash
git add src/server/services/platforms/newApi.ts src/server/services/platforms/newApi.test.ts
git commit -m "feat: add compatibility user-id headers to NewApiAdapter
Fan out userId across all 6 known One-API/New-API family header names
(New-API-User, Veloera-User, voapi-user, User-id, Rix-Api-User, neo-api-user)
to match all-api-hub behavior. This ensures VoAPI, Super-API, RIX_API,
Neo-API, and Veloera deployments correctly identify the requesting user."
```
---
### Task 2: Rewrite Sub2API Adapter
**Files:**
- Rewrite: `src/server/services/platforms/sub2api.ts`
- Create: `src/server/services/platforms/sub2api.test.ts`
**Step 1: Write the failing tests**
Create `src/server/services/platforms/sub2api.test.ts`:
```typescript
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
import { AddressInfo } from 'node:net';
import { Sub2ApiAdapter } from './sub2api.js';
describe('Sub2ApiAdapter', () => {
let server: ReturnType<typeof createServer>;
let baseUrl: string;
let adapter: Sub2ApiAdapter;
beforeEach(() => {
adapter = new Sub2ApiAdapter();
});
afterEach(async () => {
if (server) {
await new Promise<void>((resolve, reject) => {
server.close((err?: Error) => (err ? reject(err) : resolve()));
});
}
});
function startServer(handler: (req: IncomingMessage, res: ServerResponse) => void) {
return new Promise<void>((resolve) => {
server = createServer(handler);
server.listen(0, '127.0.0.1', () => {
const addr = server.address() as AddressInfo;
baseUrl = `http://127.0.0.1:${addr.port}`;
resolve();
});
});
}
it('detects sub2api from URL', async () => {
expect(await adapter.detect('https://sub2api.example.com')).toBe(true);
expect(await adapter.detect('https://example.com')).toBe(false);
});
it('returns unsupported for checkin', async () => {
const result = await adapter.checkin('http://localhost', 'token');
expect(result.success).toBe(false);
expect(result.message).toContain('not supported');
});
it('fetches balance from /api/v1/auth/me', async () => {
await startServer((req, res) => {
if (req.url === '/api/v1/auth/me') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
code: 0,
message: 'success',
data: { id: 1, username: 'testuser', email: 'test@example.com', balance: 12.5 },
}));
return;
}
res.writeHead(404).end();
});
const balance = await adapter.getBalance(baseUrl, 'jwt-token');
expect(balance.balance).toBeGreaterThan(0);
expect(balance.used).toBe(0);
});
it('fetches user info from /api/v1/auth/me', async () => {
await startServer((req, res) => {
if (req.url === '/api/v1/auth/me') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
code: 0,
message: 'success',
data: { id: 1, username: 'testuser', email: 'test@example.com', balance: 5.0 },
}));
return;
}
res.writeHead(404).end();
});
const userInfo = await adapter.getUserInfo(baseUrl, 'jwt-token');
expect(userInfo).not.toBeNull();
expect(userInfo!.username).toBe('testuser');
});
it('falls back to email local part when username is empty', async () => {
await startServer((req, res) => {
if (req.url === '/api/v1/auth/me') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
code: 0,
message: 'success',
data: { id: 1, username: '', email: 'alice@example.com', balance: 0 },
}));
return;
}
res.writeHead(404).end();
});
const userInfo = await adapter.getUserInfo(baseUrl, 'jwt-token');
expect(userInfo!.username).toBe('alice');
});
it('fetches models via /v1/models', async () => {
await startServer((req, res) => {
if (req.url === '/v1/models') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
data: [{ id: 'gpt-4o' }, { id: 'claude-3-opus' }],
}));
return;
}
res.writeHead(404).end();
});
const models = await adapter.getModels(baseUrl, 'jwt-token');
expect(models).toEqual(['gpt-4o', 'claude-3-opus']);
});
it('handles non-zero code as error in /api/v1/auth/me', async () => {
await startServer((req, res) => {
if (req.url === '/api/v1/auth/me') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
code: 401,
message: 'token expired',
data: null,
}));
return;
}
res.writeHead(404).end();
});
await expect(adapter.getBalance(baseUrl, 'expired-token')).rejects.toThrow();
});
it('login returns unsupported', async () => {
const result = await adapter.login('http://localhost', 'user', 'pass');
expect(result.success).toBe(false);
});
it('token management returns empty/false', async () => {
expect(await adapter.getApiToken('http://localhost', 'token')).toBeNull();
expect(await adapter.getApiTokens('http://localhost', 'token')).toEqual([]);
expect(await adapter.createApiToken('http://localhost', 'token')).toBe(false);
expect(await adapter.deleteApiToken('http://localhost', 'token', 'key')).toBe(false);
});
});
```
**Step 2: Run test to verify it fails**
Run: `cd /d/Desktop/metapi工作区/metapi && npx vitest run src/server/services/platforms/sub2api.test.ts`
Expected: FAIL — current Sub2ApiAdapter doesn't have these methods
**Step 3: Rewrite the adapter**
Replace `src/server/services/platforms/sub2api.ts` entirely:
```typescript
import { BasePlatformAdapter, CheckinResult, BalanceInfo, UserInfo } from './base.js';
/**
* Sub2API adapter.
*
* Sub2API uses JWT-based auth with endpoints under /api/v1/*.
* It does NOT support: login, check-in, token management, or usage stats.
* Balance is derived from a USD amount returned by /api/v1/auth/me.
*/
export class Sub2ApiAdapter extends BasePlatformAdapter {
readonly platformName = 'sub2api';
async detect(url: string): Promise<boolean> {
return url.toLowerCase().includes('sub2api');
}
/**
* Parse the Sub2API { code, message, data } envelope.
* code === 0 means success; anything else is an error.
*/
private parseSub2ApiEnvelope<T>(body: any, endpoint: string): T {
if (!body || typeof body !== 'object') {
throw new Error(`Invalid response from ${endpoint}`);
}
if (typeof body.code !== 'number') {
throw new Error(`Invalid response format from ${endpoint}`);
}
if (body.code !== 0) {
const message = typeof body.message === 'string' && body.message.trim()
? body.message.trim()
: `Error code ${body.code} from ${endpoint}`;
throw new Error(message);
}
if (body.data === undefined) {
throw new Error(`Missing data in response from ${endpoint}`);
}
return body.data as T;
}
/**
* Extract display name: prefer username, fall back to email local part.
*/
private getDisplayName(username?: string, email?: string): string {
const name = (username || '').trim();
if (name) return name;
const mail = (email || '').trim();
if (!mail) return '';
const atIndex = mail.indexOf('@');
return atIndex > 0 ? mail.slice(0, atIndex) : mail;
}
/**
* Fetch user data from /api/v1/auth/me.
*/
private async fetchAuthMe(baseUrl: string, accessToken: string): Promise<{
id: number;
username: string;
email: string;
balance: number;
}> {
const endpoint = '/api/v1/auth/me';
const res = await this.fetchJson<any>(`${baseUrl}${endpoint}`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
const data = this.parseSub2ApiEnvelope<any>(res, endpoint);
const id = typeof data.id === 'number' ? data.id
: typeof data.id === 'string' ? Number.parseInt(data.id, 10)
: NaN;
if (!Number.isFinite(id) || id <= 0) {
throw new Error(`Invalid user ID in response from ${endpoint}`);
}
const balance = typeof data.balance === 'number' ? data.balance
: typeof data.balance === 'string' ? Number.parseFloat(data.balance)
: 0;
return {
id,
username: typeof data.username === 'string' ? data.username : '',
email: typeof data.email === 'string' ? data.email : '',
balance: Number.isFinite(balance) ? balance : 0,
};
}
/**
* Convert USD balance to internal quota unit.
* Uses the same conversion factor as all-api-hub (500000 per USD).
*/
private usdToQuota(balanceUsd: number): number {
return Math.round(Math.max(0, balanceUsd) * 500000);
}
// --- Login: Not supported (JWT only) ---
override async login(): Promise<{ success: false; message: string }> {
return { success: false, message: 'Sub2API uses JWT authentication; login is not supported' };
}
// --- User Info ---
override async getUserInfo(baseUrl: string, accessToken: string): Promise<UserInfo | null> {
try {
const user = await this.fetchAuthMe(baseUrl, accessToken);
return {
username: this.getDisplayName(user.username, user.email),
email: user.email || undefined,
};
} catch {
return null;
}
}
// --- Check-in: Not supported ---
async checkin(): Promise<CheckinResult> {
return { success: false, message: 'Check-in is not supported by Sub2API' };
}
// --- Balance ---
async getBalance(baseUrl: string, accessToken: string): Promise<BalanceInfo> {
const user = await this.fetchAuthMe(baseUrl, accessToken);
const quotaValue = this.usdToQuota(user.balance);
// Sub2API only provides current balance, no usage breakdown
return {
balance: quotaValue / 500000,
used: 0,
quota: quotaValue / 500000,
};
}
// --- Models: Standard OpenAI-compatible endpoint ---
async getModels(baseUrl: string, apiToken: string): Promise<string[]> {
try {
const res = await this.fetchJson<any>(`${baseUrl}/v1/models`, {
headers: { Authorization: `Bearer ${apiToken}` },
});
return (res?.data || []).map((m: any) => m.id).filter(Boolean);
} catch {
return [];
}
}
}
```
**Step 4: Run tests to verify they pass**
Run: `cd /d/Desktop/metapi工作区/metapi && npx vitest run src/server/services/platforms/sub2api.test.ts`
Expected: All PASS
**Step 5: Run all tests for regressions**
Run: `cd /d/Desktop/metapi工作区/metapi && npx vitest run`
Expected: All PASS
**Step 6: Commit**
```bash
git add src/server/services/platforms/sub2api.ts src/server/services/platforms/sub2api.test.ts
git commit -m "feat: rewrite Sub2API adapter with proper JWT auth
Sub2API uses JWT-based auth with /api/v1/* endpoints, completely different
from One-API. The old stub just inherited OneApiAdapter and couldn't work.
Now implements:
- User info + balance via GET /api/v1/auth/me ({code, message, data} envelope)
- USD balance to quota conversion
- Display name fallback (username → email local part)
- Unsupported: login, check-in, token management
- Models: standard /v1/models (OpenAI-compatible)"
```
---
### Task 3: Enhance OneHub Adapter
**Files:**
- Modify: `src/server/services/platforms/oneHub.ts`
- Create: `src/server/services/platforms/oneHub.test.ts`
**Step 1: Write the failing tests**
Create `src/server/services/platforms/oneHub.test.ts`:
```typescript
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
import { AddressInfo } from 'node:net';
import { OneHubAdapter } from './oneHub.js';
describe('OneHubAdapter', () => {
let server: ReturnType<typeof createServer>;
let baseUrl: string;
afterEach(async () => {
if (server) {
await new Promise<void>((resolve, reject) => {
server.close((err?: Error) => (err ? reject(err) : resolve()));
});
}
});
function startServer(handler: (req: IncomingMessage, res: ServerResponse) => void) {
return new Promise<void>((resolve) => {
server = createServer(handler);
server.listen(0, '127.0.0.1', () => {
const addr = server.address() as AddressInfo;
baseUrl = `http://127.0.0.1:${addr.port}`;
resolve();
});
});
}
it('falls back to /api/available_model when /v1/models fails', async () => {
await startServer((req, res) => {
if (req.url === '/v1/models') {
res.writeHead(401, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'unauthorized' }));
return;
}
if (req.url === '/api/available_model') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
data: {
'gpt-4o': { price: { input: 0.5, output: 1.5 } },
'claude-3-opus': { price: { input: 1, output: 3 } },
},
}));
return;
}
res.writeHead(404).end();
});
const adapter = new OneHubAdapter();
const models = await adapter.getModels(baseUrl, 'token');
expect(models).toEqual(expect.arrayContaining(['gpt-4o', 'claude-3-opus']));
});
it('returns user groups from /api/user_group_map', async () => {
await startServer((req, res) => {
if (req.url === '/api/user_group_map') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ data: { default: 1.0, vip: 0.8 } }));
return;
}
res.writeHead(404).end();
});
const adapter = new OneHubAdapter();
const groups = await adapter.getUserGroups(baseUrl, 'token');
expect(groups).toEqual(expect.arrayContaining(['default', 'vip']));
});
it('parses token list from {data: [...]} envelope', async () => {
await startServer((req, res) => {
if (req.url?.startsWith('/api/token/')) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
data: [
{ key: 'sk-hub-abc', name: 'my-token', status: 1, id: 1 },
],
}));
return;
}
res.writeHead(404).end();
});
const adapter = new OneHubAdapter();
const tokens = await adapter.getApiTokens(baseUrl, 'token');
expect(tokens.length).toBe(1);
expect(tokens[0].key).toBe('sk-hub-abc');
});
});
```
**Step 2: Run test to verify it fails**
Run: `cd /d/Desktop/metapi工作区/metapi && npx vitest run src/server/services/platforms/oneHub.test.ts`
Expected: FAIL — OneHubAdapter doesn't override getModels or getUserGroups
**Step 3: Implement OneHub overrides**
Replace `src/server/services/platforms/oneHub.ts`:
```typescript
import { OneApiAdapter } from './oneApi.js';
export class OneHubAdapter extends OneApiAdapter {
readonly platformName = 'one-hub';
async detect(url: string): Promise<boolean> {
const normalized = url.toLowerCase();
return normalized.includes('onehub') || normalized.includes('one-hub');
}
/**
* OneHub model discovery: try /v1/models first, fall back to /api/available_model.
* The /api/available_model endpoint returns { data: { model_name: { price: ... }, ... } }
* where the keys are model names.
*/
override async getModels(baseUrl: string, apiToken: string, platformUserId?: number): Promise<string[]> {
// Try standard OpenAI-compatible endpoint first
let openAiModels: string[] = [];
try {
openAiModels = await super.getModels(baseUrl, apiToken, platformUserId);
} catch {}
if (openAiModels.length > 0) return openAiModels;
// Fall back to OneHub's /api/available_model
try {
const res = await this.fetchJson<any>(`${baseUrl}/api/available_model`, {
headers: { Authorization: `Bearer ${apiToken}` },
});
const payload = res?.data && typeof res.data === 'object' ? res.data : res;
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
const models = Object.keys(payload).filter(Boolean);
if (models.length > 0) return models;
}
} catch {}
return [];
}
/**
* OneHub user groups: /api/user_group_map returns { data: { group_name: ratio, ... } }
*/
override async getUserGroups(baseUrl: string, accessToken: string): Promise<string[]> {
try {
const res = await this.fetchJson<any>(`${baseUrl}/api/user_group_map`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
const source = res?.data || res;
if (source && typeof source === 'object' && !Array.isArray(source)) {
const groups = Object.keys(source).map((k) => k.trim()).filter(Boolean);
if (groups.length > 0) return Array.from(new Set(groups));
}
} catch {}
// Fall back to parent implementation
return super.getUserGroups(baseUrl, accessToken);
}
}
```
**Step 4: Run tests**
Run: `cd /d/Desktop/metapi工作区/metapi && npx vitest run src/server/services/platforms/oneHub.test.ts`
Expected: All PASS
**Step 5: Run all tests**
Run: `cd /d/Desktop/metapi工作区/metapi && npx vitest run`
Expected: All PASS
**Step 6: Commit**
```bash
git add src/server/services/platforms/oneHub.ts src/server/services/platforms/oneHub.test.ts
git commit -m "feat: enhance OneHub adapter with model and group discovery
Add OneHub-specific overrides:
- getModels: fallback to /api/available_model when /v1/models unavailable
- getUserGroups: use /api/user_group_map endpoint
Aligns with all-api-hub's OneHub service implementation."
```
---
### Task 4: Enhance DoneHub Adapter (Re-parent to OneHub)
**Files:**
- Modify: `src/server/services/platforms/doneHub.ts`
- Modify: `src/server/services/platforms/doneHub.test.ts`
**Step 1: Add test for inherited OneHub behavior**
Add to existing `doneHub.test.ts`:
```typescript
it('inherits OneHub model discovery from /api/available_model', async () => {
// Already tested — existing test "falls back to /api/available_model" covers this
// Just verify it still works after re-parenting
const adapter = new DoneHubAdapter();
const models = await adapter.getModels(baseUrl, 'token');
expect(models).toEqual(['gpt-4o', 'deepseek-chat']);
});
it('inherits OneHub user group discovery', async () => {
// Add /api/user_group_map handler to server
// This requires modifying the server setup
});
```
**Step 2: Re-parent DoneHub to extend OneHub**
Modify `src/server/services/platforms/doneHub.ts`:
```typescript
import { OneHubAdapter } from './oneHub.js';
import type { CheckinResult } from './base.js';
export class DoneHubAdapter extends OneHubAdapter {
readonly platformName = 'done-hub';
async detect(url: string): Promise<boolean> {
const normalized = url.toLowerCase();
return normalized.includes('donehub') || normalized.includes('done-hub');
}
// DoneHub deployments generally do not expose /api/user/checkin.
override async checkin(_baseUrl: string, _accessToken: string): Promise<CheckinResult> {
return { success: false, message: 'checkin endpoint not found' };
}
// getModels is inherited from OneHubAdapter which already has /api/available_model fallback
// No need to override — remove the duplicate implementation
}
```
**Step 3: Run existing DoneHub tests**
Run: `cd /d/Desktop/metapi工作区/metapi && npx vitest run src/server/services/platforms/doneHub.test.ts`
Expected: All PASS (behavior unchanged, just cleaner inheritance)
**Step 4: Run all tests**
Run: `cd /d/Desktop/metapi工作区/metapi && npx vitest run`
Expected: All PASS
**Step 5: Commit**
```bash
git add src/server/services/platforms/doneHub.ts src/server/services/platforms/doneHub.test.ts
git commit -m "refactor: re-parent DoneHub to extend OneHub adapter
DoneHub is downstream of OneHub. By extending OneHubAdapter instead of
OneApiAdapter, DoneHub inherits OneHub's /api/available_model fallback
and /api/user_group_map support. Removes duplicate getModels override
that OneHub now provides."
```
---
### Task 5: Enhance Veloera Adapter with Compat Headers
**Files:**
- Modify: `src/server/services/platforms/veloera.ts`
**Step 1: Review current state**
The Veloera adapter already has:
- Correct quota conversion (1,000,000 divisor)
- Working checkin via `/api/user/checkin`
- Working balance via `/api/user/self`
- Working models via `/v1/models`
The main gap: Veloera doesn't send compat user-id headers in its requests. Since VeloeraAdapter extends BasePlatformAdapter (not NewApiAdapter), it uses simple `Authorization: Bearer` without any user-id headers.
**Step 2: Add compat headers to Veloera balance/checkin requests**
Modify `src/server/services/platforms/veloera.ts` to include `Veloera-User` header when userId is available:
```typescript
import { BasePlatformAdapter, CheckinResult, BalanceInfo } from './base.js';
export class VeloeraAdapter extends BasePlatformAdapter {
readonly platformName = 'veloera';
async detect(url: string): Promise<boolean> {
try {
const res = await this.fetchJson<any>(`${url}/api/status`);
return res?.success === true && (
res?.data?.system_name?.toLowerCase().includes('veloera') ||
res?.data?.version?.includes('veloera')
);
} catch {
return false;
}
}
private veloeraHeaders(accessToken: string, userId?: number): Record<string, string> {
const headers: Record<string, string> = { Authorization: `Bearer ${accessToken}` };
if (userId) {
const value = String(userId);
headers['Veloera-User'] = value;
headers['New-API-User'] = value;
headers['User-id'] = value;
}
return headers;
}
async checkin(baseUrl: string, accessToken: string, platformUserId?: number): Promise<CheckinResult> {
try {
const res = await this.fetchJson<any>(`${baseUrl}/api/user/checkin`, {
method: 'POST',
headers: this.veloeraHeaders(accessToken, platformUserId),
});
if (res?.success) {
return { success: true, message: res.message || 'Check-in successful', reward: res.data?.reward?.toString() };
}
return { success: false, message: res?.message || 'Check-in failed' };
} catch (err: any) {
return { success: false, message: err.message };
}
}
async getBalance(baseUrl: string, accessToken: string, platformUserId?: number): Promise<BalanceInfo> {
const res = await this.fetchJson<any>(`${baseUrl}/api/user/self`, {
headers: this.veloeraHeaders(accessToken, platformUserId),
});
const data = res?.data;
const quota = (data?.quota || 0) / 1000000;
const used = (data?.used_quota || 0) / 1000000;
const todayIncome = Number.isFinite(data?.today_income) ? (data.today_income / 1000000) : undefined;
const todayQuotaConsumption = Number.isFinite(data?.today_quota_consumption) ? (data.today_quota_consumption / 1000000) : undefined;
return { balance: quota - used, used, quota, todayIncome, todayQuotaConsumption };
}
async getModels(baseUrl: string, apiToken: string, _platformUserId?: number): Promise<string[]> {
const res = await this.fetchJson<any>(`${baseUrl}/v1/models`, {
headers: { Authorization: `Bearer ${apiToken}` },
});
return (res?.data || []).map((m: any) => m.id).filter(Boolean);
}
}
```
**Step 3: Run all tests**
Run: `cd /d/Desktop/metapi工作区/metapi && npx vitest run`
Expected: All PASS
**Step 4: Commit**
```bash
git add src/server/services/platforms/veloera.ts
git commit -m "feat: add compat user-id headers to Veloera adapter
Add Veloera-User, New-API-User, and User-id headers to balance and
checkin requests. Accept platformUserId parameter in checkin/getBalance
for consistency with other adapters."
```
---
### Task 6: Update Income Log Fallback for Sub2API
**Files:**
- Check: `src/server/services/balanceService.ts`
**Step 1: Verify Sub2API is NOT in the income log fallback list**
The `supportsTodayIncomeLogFallback()` function only includes `new-api`, `anyrouter`, `one-api`, `veloera`. Sub2API should NOT be in this list (it doesn't support log endpoints). Verify this is already correct — no change needed.
**Step 2: Run full test suite**
Run: `cd /d/Desktop/metapi工作区/metapi && npx vitest run`
Expected: All PASS — this is the final verification
**Step 3: Final commit (if any remaining changes)**
No code changes expected for this task. It's a verification step.
-99
View File
@@ -1,99 +0,0 @@
# Source Encoding Cleanup Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Remove mojibake-corrupted Chinese literals from source and tests, then add regression coverage so the corruption does not re-enter the repository unnoticed.
**Architecture:** Keep the fix narrow: add one regression test that scans `src/` for known mojibake markers, then replace each corrupted literal with the intended UTF-8 text in the affected source and test files. Verification stays focused on the new guard plus the currently failing Vitest files.
**Tech Stack:** TypeScript, Vitest, Node.js filesystem APIs
---
### Task 1: Add a failing mojibake regression test
**Files:**
- Create: `src/encoding.mojibake.test.ts`
- Test: `src/encoding.mojibake.test.ts`
**Step 1: Write the failing test**
Create a Vitest file that scans `src/**/*.{ts,tsx,js,jsx}` and fails when any source line contains known mojibake markers currently present in the repo.
**Step 2: Run test to verify it fails**
Run: `npm test -- src/encoding.mojibake.test.ts`
Expected: FAIL with matches from the currently corrupted files.
**Step 3: Keep the failure output readable**
List file paths, line numbers, and the matched marker text in the assertion message.
**Step 4: Re-run test to verify it still fails for the expected reason**
Run: `npm test -- src/encoding.mojibake.test.ts`
Expected: FAIL and only point at real mojibake matches.
### Task 2: Repair corrupted source literals
**Files:**
- Modify: `src/server/routes/api/accounts.ts`
- Modify: `src/web/i18n.supplement.ts`
**Step 1: Replace the unsupported-platform mojibake message**
Use a single readable Chinese message in `accounts.ts`, aligned with the existing translation key in `i18n.supplement.ts`.
**Step 2: Remove or normalize the matching corrupted i18n entry**
Keep only the valid UTF-8 translation key/value pair for that backend message.
**Step 3: Run the mojibake test**
Run: `npm test -- src/encoding.mojibake.test.ts`
Expected: still FAIL, but no longer report the repaired source files.
### Task 3: Repair corrupted test fixtures and selectors
**Files:**
- Modify: `src/web/pages/dashboard.site-speed-button.test.tsx`
- Modify: `src/server/services/platforms/newApi.test.ts`
**Step 1: Fix the dashboard selector text**
Replace the corrupted speed-button label match with the intended UTF-8 text.
**Step 2: Fix corrupted upstream fixture/expectation messages in `newApi.test.ts`**
Use readable Chinese strings that match the adapter’s intended behavior.
**Step 3: Run focused tests**
Run: `npm test -- src/web/pages/dashboard.site-speed-button.test.tsx`
Expected: PASS
Run: `npm test -- src/server/services/platforms/newApi.test.ts`
Expected: PASS
### Task 4: Verify end-to-end cleanup
**Files:**
- Test: `src/encoding.mojibake.test.ts`
- Test: `src/web/pages/accounts.rebind-panel-focus.test.tsx`
- Test: `src/web/pages/dashboard.site-speed-button.test.tsx`
- Test: `src/server/services/platforms/newApi.test.ts`
**Step 1: Re-run the regression guard**
Run: `npm test -- src/encoding.mojibake.test.ts`
Expected: PASS
**Step 2: Re-run the CI-facing tests**
Run: `npm test -- src/web/pages/accounts.rebind-panel-focus.test.tsx src/web/pages/dashboard.site-speed-button.test.tsx`
Expected: PASS
**Step 3: Re-run the backend platform test**
Run: `npm test -- src/server/services/platforms/newApi.test.ts`
Expected: PASS
@@ -1,49 +0,0 @@
---
title: Model Refresh Health + Toast Feedback
date: 2026-03-10
owner: codex
status: approved
---
# Goal
When users click the "模型" refresh action, failed model discovery should mark the account runtime health as `unhealthy` and surface a clear failure toast. Success should show the discovered model names and count.
# Scope
- Applies to all account types (Session + API Key).
- Server determines success/failure and error category; UI uses returned payload for toast text.
# Non-goals
- Changing upstream adapters' auth logic.
- Altering auto-refresh schedules or background tasks.
# Architecture
- Update `refreshModelsForAccount(...)` to return structured outcome:
- `status`: `success` | `failed`
- `modelCount`
- `modelsPreview` (first N names)
- `errorCode` (e.g. `timeout`, `unauthorized`, `empty_models`, `unknown`)
- `errorMessage` (human-readable)
- On failure: write `runtimeHealth = unhealthy` with a reason derived from `errorCode`.
- `/api/models/check/:accountId` should return the above outcome.
- UI toast reads outcome and displays:
- Success: `已获取到模型:A、B、C(共 N 个)`
- Failure: `模型获取失败(请求超时)` / `模型获取失败,API Key 已无效` / `模型获取失败:<message>`
# Data Flow
1. User clicks "模型".
2. UI calls `/api/models/check/:accountId`.
3. Server refreshes models and returns structured outcome.
4. UI shows toast based on outcome and reloads list.
# Error Classification
- `timeout`: error message contains `timeout` / `timed out` / `请求超时`.
- `unauthorized`: HTTP 401/403 or message contains `invalid`, `unauthorized`, `无权`, `未提供令牌`.
- `empty_models`: request succeeded but model list empty.
- `unknown`: fallback for other errors.
# Testing
- Unit tests for `refreshModelsForAccount`:
- Failure updates runtime health to `unhealthy`.
- Failure classification maps to the right `errorCode`.
- Success returns `modelsPreview` and `modelCount`.
- UI behavior: verify toast text for success vs. each failure category.
@@ -1,160 +0,0 @@
# Downstream Key UI And Site Links Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Restyle the downstream key page to match the restrained management-console pages and standardize site buttons across pages using the accounts page interaction.
**Architecture:** Introduce one shared site link component based on the accounts page behavior, then update each affected page to consume it. Refactor downstream key layout styling toward existing `card`, `badge`, and `data-table` patterns without changing backend behavior.
**Tech Stack:** React, TypeScript, React Router, Vitest, React Test Renderer
---
### Task 1: Extract the shared site button
**Files:**
- Create: `src/web/components/SiteBadgeLink.tsx`
- Test: `src/web/components/site-badge-link.test.tsx`
- Modify: `src/web/pages/Accounts.tsx`
- Modify: `src/web/index.css`
**Step 1: Write the failing test**
Add a component test that renders a site badge link and asserts:
- it renders the site name,
- it has the shared interactive class,
- clicking navigates to `/sites?focusSiteId=<id>`.
**Step 2: Run test to verify it fails**
Run: `npm test -- src/web/components/site-badge-link.test.tsx`
Expected: FAIL because the component does not exist yet.
**Step 3: Write minimal implementation**
- Create a small component that wraps `Link` from `react-router-dom`.
- Accept `siteId`, `siteName`, and optional badge class props.
- Reproduce the accounts page badge-link styling and fallback behavior.
- Move the accounts page site badge rendering to this component.
- Add any missing shared CSS class if accounts currently relies on page-local styling.
**Step 4: Run test to verify it passes**
Run: `npm test -- src/web/components/site-badge-link.test.tsx src/web/pages/accounts.segmented-connections.test.tsx`
Expected: PASS
**Step 5: Commit**
```bash
git add src/web/components/SiteBadgeLink.tsx src/web/components/site-badge-link.test.tsx src/web/pages/Accounts.tsx src/web/index.css
git commit -m "refactor: extract shared site badge link"
```
### Task 2: Apply the shared site button across affected pages
**Files:**
- Modify: `src/web/pages/Models.tsx`
- Modify: `src/web/pages/ProxyLogs.tsx`
- Modify: `src/web/pages/TokenRoutes.tsx`
- Modify: `src/web/pages/DownstreamKeys.tsx`
- Test: `src/web/pages/Models.marketplace-text.test.tsx`
- Test: `src/web/pages/tokenRoutes.group-collapse.test.tsx`
- Test: `src/web/pages/DownstreamKeys.test.tsx`
**Step 1: Write failing assertions**
Add or update tests to assert the affected pages render the shared site button class and, where practical, point to the focused site management route.
**Step 2: Run test to verify it fails**
Run: `npm test -- src/web/pages/Models.marketplace-text.test.tsx src/web/pages/tokenRoutes.group-collapse.test.tsx src/web/pages/DownstreamKeys.test.tsx`
Expected: FAIL because the pages still use inconsistent site rendering.
**Step 3: Write minimal implementation**
- Replace page-local site label rendering with `SiteBadgeLink`.
- Only convert labels that represent navigable management-site references.
- Preserve non-site filters, external URLs, and unrelated badges.
**Step 4: Run test to verify it passes**
Run: `npm test -- src/web/pages/Models.marketplace-text.test.tsx src/web/pages/tokenRoutes.group-collapse.test.tsx src/web/pages/DownstreamKeys.test.tsx src/web/pages/ProxyLogs.server-driven.test.tsx`
Expected: PASS
**Step 5: Commit**
```bash
git add src/web/pages/Models.tsx src/web/pages/ProxyLogs.tsx src/web/pages/TokenRoutes.tsx src/web/pages/DownstreamKeys.tsx src/web/pages/Models.marketplace-text.test.tsx src/web/pages/tokenRoutes.group-collapse.test.tsx src/web/pages/DownstreamKeys.test.tsx
git commit -m "feat: unify site badge navigation across pages"
```
### Task 3: Restyle downstream keys page
**Files:**
- Modify: `src/web/pages/DownstreamKeys.tsx`
- Test: `src/web/pages/DownstreamKeys.test.tsx`
- Modify: `src/web/index.css`
**Step 1: Write failing assertions**
Add focused assertions for the downstream keys page structure where needed, such as shared management-page card classes, reduced bespoke styling hooks, or the new site badge placement.
**Step 2: Run test to verify it fails**
Run: `npm test -- src/web/pages/DownstreamKeys.test.tsx`
Expected: FAIL after adding assertions that reflect the updated restrained layout.
**Step 3: Write minimal implementation**
- Reduce the page’s dashboard-like presentation.
- Align summary, filters, and batch action sections with the same management-console card language used in sites/accounts.
- Reuse shared badge/link patterns instead of one-off visual treatments where practical.
- Keep operations, data fetching, and drawer behavior unchanged.
**Step 4: Run test to verify it passes**
Run: `npm test -- src/web/pages/DownstreamKeys.test.tsx`
Expected: PASS
**Step 5: Commit**
```bash
git add src/web/pages/DownstreamKeys.tsx src/web/pages/DownstreamKeys.test.tsx src/web/index.css
git commit -m "style: align downstream key page with management UI"
```
### Task 4: Final verification
**Files:**
- Modify: none
**Step 1: Run targeted verification**
Run:
- `npm test -- src/web/components/site-badge-link.test.tsx`
- `npm test -- src/web/pages/DownstreamKeys.test.tsx`
- `npm test -- src/web/pages/Models.marketplace-text.test.tsx`
- `npm test -- src/web/pages/tokenRoutes.group-collapse.test.tsx`
- `npm test -- src/web/pages/ProxyLogs.server-driven.test.tsx`
- `npm test -- src/web/pages/accounts.segmented-connections.test.tsx`
Expected: PASS
**Step 2: Run broader sanity verification if time permits**
Run: `npm test -- src/web/App.sidebar.test.ts src/web/App.sidebar-mobile.test.tsx`
Expected: PASS
**Step 3: Commit final polish**
```bash
git add -A
git commit -m "test: verify downstream key UI and site link consistency"
```
@@ -1,254 +0,0 @@
# Single Source Consolidation PR1 Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Land PR1 for the highest-value duplication and double-source issues around responses normalization, shared proxy surface orchestration, provider runtime headers, token-route contract sharing, account mutation workflow, and standard API platform helpers.
**Architecture:** Keep PR1 focused on extracting shared single-source modules and rewiring existing callers to consume them. Do not expand this PR into schema metamodel rewrites, `chatFormatsCore` breakup, or mobile page scaffold work; those remain follow-up PR scope once the highest-risk protocol and workflow duplications are removed.
**Tech Stack:** TypeScript, Fastify, Vitest, shared server/web modules under `src/shared`, server proxy-core surfaces/providers/services, React admin pages.
---
## Task 1: Savepoint Commit
**Files:**
- Commit: `src/server/config.ts`
- Commit: `src/server/config.test.ts`
- Commit: `src/server/db/proxyFileSchemaCompatibility.ts`
- Commit: `src/server/db/proxyFileSchemaCompatibility.test.ts`
**Step 1: Verify the savepoint changes**
Run: `npm test -- src/server/config.test.ts src/server/db/proxyFileSchemaCompatibility.test.ts`
Expected: PASS
**Step 2: Commit the savepoint**
Run:
```bash
git add src/server/config.ts src/server/config.test.ts src/server/db/proxyFileSchemaCompatibility.ts src/server/db/proxyFileSchemaCompatibility.test.ts
git commit -m "fix: trust proxy headers and align proxy file mysql indexes"
```
**Step 3: Cherry-pick into PR1 branch**
Run:
```bash
git cherry-pick <savepoint-commit>
```
Expected: first commit preserved inside PR1 branch before the larger consolidation commit.
## Task 2: Responses Single Source
**Files:**
- Create: `src/server/transformers/openai/responses/normalization.ts`
- Modify: `src/server/transformers/openai/responses/conversion.ts`
- Modify: `src/server/transformers/openai/responses/compatibility.ts`
- Test: `src/server/transformers/openai/responses/conversion.test.ts`
**Step 1: Move shared normalization helpers into one source**
Keep `normalizeResponsesInputForCompatibility`, `normalizeResponsesMessageContent`, and related block normalization in `normalization.ts`.
**Step 2: Rewire both callers**
Make `conversion.ts` and `compatibility.ts` import the shared helpers instead of maintaining their own independent copies.
**Step 3: Verify parity**
Run: `npm test -- src/server/transformers/openai/responses/conversion.test.ts`
Expected: PASS, including assertions that compatibility exports point at the shared implementation.
## Task 3: Shared Surface Orchestration
**Files:**
- Create: `src/server/proxy-core/surfaces/sharedSurface.ts`
- Modify: `src/server/proxy-core/surfaces/chatSurface.ts`
- Modify: `src/server/proxy-core/surfaces/openAiResponsesSurface.ts`
- Modify: `src/server/proxy-core/surfaces/geminiSurface.ts`
- Test: `src/server/proxy-core/surfaces/sharedSurface.test.ts`
**Step 1: Extract common orchestration primitives**
Centralize channel selection, dispatch, proxy logging, retry/failure handling, and common failure response shaping in `sharedSurface.ts`.
**Step 2: Rewire surface callers**
Make chat and responses surfaces consume the shared orchestration helpers instead of each keeping a parallel flow.
**Step 3: Verify behavior**
Run: `npm test -- src/server/proxy-core/surfaces/sharedSurface.test.ts`
Expected: PASS
## Task 4: Provider Header Builders
**Files:**
- Create: `src/server/proxy-core/providers/headerUtils.ts`
- Modify: `src/server/proxy-core/providers/codexProviderProfile.ts`
- Modify: `src/server/proxy-core/providers/claudeProviderProfile.ts`
- Modify: `src/server/proxy-core/providers/geminiCliProviderProfile.ts`
- Modify: `src/server/routes/proxy/upstreamEndpoint.ts`
- Test: `src/server/proxy-core/providers/headerUtils.test.ts`
**Step 1: Extract header parsing and runtime header construction**
Keep Codex, Claude, and Gemini CLI runtime header shaping in `headerUtils.ts`.
**Step 2: Remove duplicate builders**
Make both provider profiles and `upstreamEndpoint.ts` call the shared header builders.
**Step 3: Verify behavior**
Run: `npm test -- src/server/proxy-core/providers/headerUtils.test.ts`
Expected: PASS
## Task 5: Cross-Layer Token Route Contract
**Files:**
- Create: `src/shared/tokenRouteContract.js`
- Create: `src/shared/tokenRouteContract.d.ts`
- Create: `src/shared/tokenRoutePatterns.js`
- Create: `src/shared/tokenRoutePatterns.d.ts`
- Modify: `src/server/services/tokenRouter.ts`
- Modify: `src/server/routes/api/tokens.ts`
- Modify: `src/web/pages/token-routes/utils.ts`
- Modify: `src/web/pages/token-routes/types.ts`
- Modify: `src/web/pages/helpers/routeListVisibility.ts`
- Modify: `src/web/pages/Settings.tsx`
- Test: `src/shared/tokenRouteContract.test.ts`
- Test: `src/shared/tokenRoutePatterns.test.ts`
**Step 1: Extract shared route mode and pattern matching**
Make route mode normalization and model pattern matching authoritative in `src/shared`.
**Step 2: Rewire server and web**
Make token router, route APIs, token-routes helpers, and settings consume the shared contract instead of maintaining separate copies.
**Step 3: Verify behavior**
Run: `npm test -- src/shared/tokenRouteContract.test.ts src/shared/tokenRoutePatterns.test.ts`
Expected: PASS
## Task 6: Account Mutation Workflow
**Files:**
- Create: `src/server/services/accountMutationWorkflow.ts`
- Modify: `src/server/routes/api/accounts.ts`
- Modify: `src/server/routes/api/accountTokens.ts`
- Test: `src/server/services/accountMutationWorkflow.test.ts`
**Step 1: Extract convergence workflow**
Move token sync, default token ensure, balance refresh, model refresh, and route rebuild sequencing into `accountMutationWorkflow.ts`.
**Step 2: Rewire controllers**
Make account and account-token APIs call the workflow service instead of each owning private orchestration logic.
**Step 3: Verify behavior**
Run: `npm test -- src/server/services/accountMutationWorkflow.test.ts`
Expected: PASS
## Task 7: Standard API Provider Base
**Files:**
- Create: `src/server/services/platforms/standardApiProvider.ts`
- Modify: `src/server/services/platforms/openai.ts`
- Modify: `src/server/services/platforms/claude.ts`
- Modify: `src/server/services/platforms/gemini.ts`
- Modify: `src/server/services/platforms/cliproxyapi.ts`
- Test: `src/server/services/platforms/standardApiProvider.test.ts`
- Test: `src/server/services/platforms/llmUpstream.test.ts`
**Step 1: Extract shared standard API adapter logic**
Centralize base URL normalization, `/v1/models` resolution, unsupported login/checkin/balance defaults, and common model fetch behavior.
**Step 2: Rewire adapters**
Make OpenAI, Claude, Gemini, and CLIProxyAPI adapters extend the shared base instead of repeating the same template logic.
**Step 3: Verify behavior**
Run: `npm test -- src/server/services/platforms/standardApiProvider.test.ts src/server/services/platforms/llmUpstream.test.ts`
Expected: PASS
## Task 8: Shared Input File Resolution And Web Helpers
**Files:**
- Modify: `src/server/services/proxyInputFileResolver.ts`
- Modify: `src/server/services/proxyInputFileResolver.test.ts`
- Modify: `src/server/proxy-core/surfaces/inputFilesSurface.ts`
- Create: `src/web/pages/helpers/accountConnection.ts`
- Create: `src/web/pages/helpers/accountConnection.test.ts`
- Modify: `src/web/pages/Accounts.tsx`
- Modify: `src/web/pages/Tokens.tsx`
- Modify: `src/web/api.ts`
- Modify: `src/web/api.test.ts`
**Step 1: Make route-level file inlining reuse the service implementation**
Replace the duplicate `inputFilesSurface.ts` implementation with a re-export of the shared resolver service.
**Step 2: Extract shared web account helpers**
Move `resolveAccountCredentialMode`, `parsePositiveInt`, and `isTruthyFlag` into a single helper consumed by both Accounts and Tokens.
**Step 3: De-duplicate proxy test aliases**
Make `testProxy` / `proxyTest` and stream variants reuse the same implementation function.
**Step 4: Verify behavior**
Run: `npm test -- src/server/services/proxyInputFileResolver.test.ts src/web/pages/helpers/accountConnection.test.ts src/web/api.test.ts`
Expected: PASS
## Task 9: PR1 Verification And Commit
**Files:**
- Verify: `scripts/dev/copy-runtime-db-generated.ts`
- Verify: `scripts/dev/copy-runtime-db-generated.test.ts`
- Verify: all files touched in Tasks 2-8
**Step 1: Run focused regression suite**
Run:
```bash
npm test -- scripts/dev/copy-runtime-db-generated.test.ts src/server/proxy-core/cliProfiles/codexProfile.test.ts src/server/proxy-core/providers/headerUtils.test.ts src/server/proxy-core/surfaces/sharedSurface.test.ts src/server/routes/api/oauth.test.ts src/server/services/accountMutationWorkflow.test.ts src/server/services/modelService.discovery.test.ts src/server/services/oauth/oauthAccount.test.ts src/server/services/platforms/standardApiProvider.test.ts src/server/services/platforms/llmUpstream.test.ts src/server/services/proxyInputFileResolver.test.ts src/server/transformers/openai/responses/conversion.test.ts src/shared/tokenRouteContract.test.ts src/shared/tokenRoutePatterns.test.ts src/web/api.test.ts src/web/pages/helpers/accountConnection.test.ts
```
Expected: PASS
**Step 2: Run server build**
Run: `npm run build:server`
Expected: PASS
**Step 3: Commit PR1 consolidation work**
Run:
```bash
git add docs/plans/2026-03-22-single-source-consolidation-pr1.md scripts/dev/copy-runtime-db-generated.test.ts scripts/dev/copy-runtime-db-generated.ts src/server/proxy-core/cliProfiles/codexProfile.ts src/server/proxy-core/cliProfiles/codexProfile.test.ts src/server/proxy-core/providers/headerUtils.ts src/server/proxy-core/providers/headerUtils.test.ts src/server/proxy-core/providers/claudeProviderProfile.ts src/server/proxy-core/providers/codexProviderProfile.ts src/server/proxy-core/providers/geminiCliProviderProfile.ts src/server/proxy-core/surfaces/chatSurface.ts src/server/proxy-core/surfaces/geminiSurface.ts src/server/proxy-core/surfaces/inputFilesSurface.ts src/server/proxy-core/surfaces/openAiResponsesSurface.ts src/server/proxy-core/surfaces/sharedSurface.ts src/server/proxy-core/surfaces/sharedSurface.test.ts src/server/routes/api/accountTokens.ts src/server/routes/api/accounts.ts src/server/routes/api/oauth.test.ts src/server/routes/api/tokens.ts src/server/routes/proxy/downstreamClientContext.ts src/server/routes/proxy/responsesWebsocket.ts src/server/routes/proxy/upstreamEndpoint.ts src/server/services/accountMutationWorkflow.ts src/server/services/accountMutationWorkflow.test.ts src/server/services/backupService.ts src/server/services/modelService.discovery.test.ts src/server/services/modelService.ts src/server/services/oauth/oauthAccount.ts src/server/services/oauth/oauthAccount.test.ts src/server/services/oauth/quota.ts src/server/services/oauth/service.ts src/server/services/platforms/claude.ts src/server/services/platforms/cliproxyapi.ts src/server/services/platforms/gemini.ts src/server/services/platforms/llmUpstream.test.ts src/server/services/platforms/openai.ts src/server/services/platforms/standardApiProvider.ts src/server/services/platforms/standardApiProvider.test.ts src/server/services/proxyInputFileResolver.ts src/server/services/proxyInputFileResolver.test.ts src/server/services/siteProxy.ts src/server/services/tokenRouter.ts src/server/transformers/anthropic/messages/inbound.ts src/server/transformers/openai/responses/compatibility.ts src/server/transformers/openai/responses/conversion.test.ts src/server/transformers/openai/responses/conversion.ts src/server/transformers/openai/responses/normalization.ts src/shared/tokenRouteContract.d.ts src/shared/tokenRouteContract.js src/shared/tokenRouteContract.test.ts src/shared/tokenRoutePatterns.d.ts src/shared/tokenRoutePatterns.js src/shared/tokenRoutePatterns.test.ts src/web/api.ts src/web/api.test.ts src/web/pages/Accounts.tsx src/web/pages/Settings.tsx src/web/pages/Tokens.tsx src/web/pages/helpers/accountConnection.ts src/web/pages/helpers/accountConnection.test.ts src/web/pages/helpers/routeListVisibility.ts src/web/pages/token-routes/types.ts src/web/pages/token-routes/utils.ts
git commit -m "refactor: consolidate single-source routing and protocol helpers"
```
### Deferred After PR1
**Keep out of this PR:**
- `shared/chatFormatsCore.ts` breakup
- Gemini `generate-content` module boundary cleanup
- schema contract/introspection meta-rule unification
- `legacySchemaCompat.ts` feature compatibility dedupe
- mobile page scaffold extraction
- large-file decomposition for `Settings.tsx`, `ModelTester.tsx`, `tokenRouter.ts`, `modelService.ts`
@@ -1,96 +0,0 @@
# Single Source Consolidation Full-Scope Follow-Through
## Context
This document supersedes the scoped execution boundary of
[`2026-03-22-single-source-consolidation-pr1.md`](./2026-03-22-single-source-consolidation-pr1.md)
for PR #244. The PR1 document remains a historical snapshot of the initial
landing slice. This document captures the expanded branch-wide finish that
continued until the single-source, workflow, schema, and web admin duplication
themes were fully closed for this PR.
## Goals
- Eliminate active double-source implementations for proxy protocols and runtime headers.
- Centralize account, OAuth, route refresh, and platform discovery workflows.
- Collapse schema metadata normalization and feature compatibility drift points.
- Remove repeated web admin page scaffolding and large inline modal/panel implementations.
- Finish the branch in a merge-ready state with focused regressions and formal builds.
## Scope Delivered In This Branch
### Server and Shared Runtime
- OpenAI Responses normalization and conversion now flow through one shared implementation boundary.
- Shared proxy surface orchestration owns retry recovery, success bookkeeping, and surface-level failure handling.
- Provider header/body runtime shaping is centralized behind provider profiles and shared header utilities.
- OAuth identity now treats structured columns as the canonical source, with compatibility/backfill isolated to a removable layer.
- Route refresh and rebuild sequencing flows through `routeRefreshWorkflow`, not controller-local orchestration.
- Platform discovery moved out of `modelService` into dedicated registry-backed discovery modules.
- Schema metadata normalization is shared by contract and introspection code paths.
- Legacy schema compatibility derives feature-owned allowances from feature specs instead of maintaining a second whitelist.
### Web Admin
- Token-route, brand, and downstream client display rules now rely on shared helpers and registries instead of page-local duplicates.
- `ResponsiveBatchActionBar` centralizes repeated batch action shells across admin list pages.
- `ResponsiveFilterPanel` centralizes the responsive "desktop controls vs mobile sheet" scaffold across admin pages.
- `Settings`, `Accounts`, `ModelTester`, and `DownstreamKeys` no longer keep their heaviest modal or drawer UIs inline in the page file.
- Brand matching metadata is owned by `brandRegistry`, with `BrandIcon` reduced to rendering concerns.
## Commit Groups
### Protocol and Workflow Consolidation
- review-driven cleanup commits
- OAuth identity and route refresh workflow commits
- downstream client/runtime header/profile consolidation commits
- platform discovery extraction commits
### Schema and Shared Contract Consolidation
- shared schema metadata normalization
- legacy schema compatibility derivation from feature specs
- shared token-route contract and pattern ownership
### Web Admin Decomposition
- responsive batch action scaffold
- responsive filter scaffold
- settings modal extraction
- accounts model management modal extraction
- model tester panel extraction
- downstream key drawer extraction
## Final Verification Matrix
Run these groups before calling the branch complete.
### Protocol and Surface
- `npm test -- src/server/proxy-core/surfaces/sharedSurface.test.ts src/server/routes/proxy src/server/transformers/openai/responses src/server/transformers/anthropic/messages src/server/services/proxyInputFileResolver.test.ts`
### OAuth, Workflow, and Platforms
- `npm test -- src/server/routes/api src/server/services/accountMutationWorkflow.test.ts src/server/services/modelService.discovery.test.ts src/server/services/oauth/oauthAccount.test.ts src/server/services/platforms/standardApiProvider.test.ts src/server/services/platforms/llmUpstream.test.ts`
### Schema
- `npm run test:schema:unit`
- `npm run schema:contract`
- `npm run smoke:db:sqlite`
### Web and Build
- `npm test -- src/web/pages/responsiveFilterPanel.architecture.test.ts src/web/pages/checkin.mobile.test.tsx src/web/pages/logs.mobile.test.tsx src/web/pages/programLogs.mobile-layout.test.tsx src/web/pages/accounts.batch-actions.test.tsx src/web/pages/sites.mobile-actions.test.tsx src/web/pages/tokens.batch-actions.test.tsx src/web/pages/DownstreamKeys.test.tsx src/web/pages/DownstreamKeys.mobile.test.tsx src/web/pages/downstreamKeys.mobile-layout.test.tsx src/web/pages/tokenRoutes.mobile-layout.test.tsx src/web/pages/tokenRoutes.mobile.test.tsx src/web/pages/models.mobile-layout.test.tsx`
- `npm run build`
## Done Definition
The branch is complete when:
- no page under `src/web/pages` imports `MobileFilterSheet` directly;
- no active protocol path keeps parallel single-source conversions alive;
- controller layers do not own route rebuild choreography;
- schema metadata and compatibility rules do not keep a second handwritten truth source;
- focused regressions and formal builds pass from the commands above.
-97
View File
@@ -1,97 +0,0 @@
# SaaS 用户管理重构计划
**目标:** 将现有单用户系统重构为 SaaS 多租户架构,支持组织管理、成员邀请、套餐限制
**核心变更:**
1. 新增 `organizations` + `organization_members` 表
2. 新增 `invitations` 表(邀请码注册)
3. 用户注册改为必须关联组织或创建组织
4. 合并 `/api/admin/users` 和 `/api/users` 路由
5. 前端增加组织管理页面
6. 新增 `plan_limits` 套餐限制系统
---
## 任务清单
### Task 1: 数据库 Schema — 新增组织相关表
**文件:** `src/server/db/schema.ts`
新增三个表:
- `organizations` (id, name, slug, plan, status, createdAt, updatedAt)
- `organization_members` (id, orgId, userId, role, status, joinedAt)
- `invitations` (id, orgId, email, token, role, invitedBy, status, expiresAt, createdAt)
修改 `users` 表:新增 `emailVerified`, `avatarUrl`
### Task 2: 后端 Service — OrganizationService
**文件:** `src/server/services/organizationService.ts`
- `createOrganization(name, creatorUserId)` → 创建组织 + 添加创建者为 owner
- `getOrganization(id)` → 查组织信息
- `listUserOrganizations(userId)` → 用户所在组织列表
- `addMember(orgId, userId, role)` → 添加成员
- `removeMember(orgId, userId)` → 移除成员
- `updateMemberRole(orgId, userId, role)` → 改角色
### Task 3: 后端 Service — InvitationService
**文件:** `src/server/services/invitationService.ts`
- `createInvitation(orgId, email, role, invitedBy)` → 生成邀请
- `acceptInvitation(token, userId)` → 接受邀请
- `listInvitations(orgId)` → 组织邀请列表
- `cancelInvitation(id)` → 撤销邀请
### Task 4: 重构 UserService — 支持组织注册
**文件:** `src/server/services/userService.ts`
- 注册时可以选择加入组织或创建新组织
- 增加 `plan_limits` 查询
- JWT payload 增加 `orgId`
### Task 5: 重构 Routes — 合并 admin/users
**文件:** `src/server/routes/api/users.ts`
- 移除 `/api/users/register` 和 `/api/users/login` 的公开注册(改为邀请制)
- 合并 admin 功能到 `users.ts`,通过 `requireAdmin` 中间件控制
- 删除 `src/server/routes/api/adminUsers.ts`
### Task 6: 新增 Organization Routes
**文件:** `src/server/routes/api/organizations.ts`
- `GET /api/organizations` — 用户所在组织列表
- `POST /api/organizations` — 创建组织
- `GET /api/organizations/:id` — 组织详情(仅成员)
- `PATCH /api/organizations/:id` — 更新组织
- `GET /api/organizations/:id/members` — 成员列表
- `POST /api/organizations/:id/members` — 邀请成员
- `DELETE /api/organizations/:id/members/:userId` — 移除成员
- `POST /api/organizations/:id/invitations` — 创建邀请
- `GET /api/organizations/:id/invitations` — 邀请列表
### Task 7: Plan limits 枚举
**文件:** `src/server/services/planService.ts`
```typescript
export const PLAN_LIMITS = {
free: { maxMembers: 3, maxApiKeys: 5, maxRequestsPerDay: 1000 },
pro: { maxMembers: 10, maxApiKeys: 20, maxRequestsPerDay: 10000 },
enterprise: { maxMembers: 100, maxApiKeys: 100, maxRequestsPerDay: 100000 },
} as const;
```
### Task 8: 前端 — 组织管理页面
**文件:** `src/web/pages/OrganizationSettings.tsx`
- 组织信息编辑(名称、slug)
- 成员管理列表(角色切换、移除)
- 邀请管理(生成邀请链接、撤销)
- 套餐信息与用量展示
-105
View File
@@ -1,105 +0,0 @@
# Metapi 项目结构
本文档只说明当前仓库里长期维护的主目录,帮助贡献者快速判断代码、脚本和文档应当放在哪里。
## 顶层目录
```text
metapi/
├── build/ # 打包静态资源(如 Electron 图标)
├── data/ # 默认运行时数据目录(SQLite、日志、导出文件)
├── dist/ # 构建产物(web / server / desktop)
├── docker/ # Dockerfile、Compose 与部署模板
├── docs/ # VitePress 文档、截图、Logo 与社区规范
├── drizzle/ # Drizzle SQL 迁移与 meta 快照
├── scripts/ # 开发脚本、桌面打包钩子、一次性 codemod
├── src/
│ ├── desktop/ # Electron 主进程与桌面运行时
│ ├── server/ # Fastify 服务、数据库、代理路由与业务服务
│ └── web/ # React 管理后台
├── tmp/ # 临时调试文件(已 gitignore)
├── restart.bat # Windows 快捷重启入口,转发到 scripts/dev/restart.bat
├── package.json # 脚本入口与依赖清单
├── electron-builder.yml # 桌面打包配置
├── drizzle.config.ts # Drizzle 配置
├── vite.config.ts # Web 构建配置
└── tsconfig*.json # TypeScript 配置
```
## 源码目录
### `src/server`
```text
src/server/
├── index.ts # Fastify 启动、运行时初始化、启动摘要输出
├── config.ts # 环境变量解析与 Fastify 配置
├── desktop.ts # 桌面模式下的静态资源与公开路由适配
├── nativeModuleGuard.ts # better-sqlite3 ABI 兼容检查
├── db/ # schema、连接、迁移、兼容列修复
├── middleware/ # 认证等通用中间件
├── routes/
│ ├── api/ # 管理端 API(sites / accounts / tokens / settings ...)
│ └── proxy/ # OpenAI / Claude / Gemini 兼容代理入口
├── services/ # 业务服务、平台适配器、日志 / 文件 / 路由 / 迁移能力
└── transformers/ # 协议转换与共享归一化层
```
- 新的管理接口优先放在 `routes/api/`。
- 新的 `/v1/*`、多模态、文件、视频等兼容代理逻辑放在 `routes/proxy/`。
- 协议格式转换或跨协议共享归一化,优先落在 `transformers/` 而不是路由文件里。
### `src/web`
```text
src/web/
├── main.tsx # Vite 入口
├── App.tsx # 路由与页面装配
├── api.ts # 管理端 API 客户端
├── authSession.ts # 登录态管理
├── appLocalState.ts # 本地安装 / UI 状态
├── docsLink.ts # 文档链接映射
├── i18n*.ts* # 国际化
├── components/ # 通用组件与图表组件
├── pages/ # 路由页与页面级 helpers
└── public/ # Web 静态资源
```
- 页面级测试和 helper 与页面代码同目录维护。
- 通用展示组件放 `components/`;只被单页消费的纯逻辑优先放 `pages/helpers/`。
### `src/desktop`
```text
src/desktop/
├── main.ts # Electron 主进程入口
├── runtime.ts # 桌面运行时端口 / 路径解析
└── runtime.test.ts # 桌面运行时测试
```
## 脚本与文档目录
```text
scripts/
├── dev/ # 本地开发脚本(run-server.ts / restart.bat / db-smoke.ts)
├── desktop/ # Electron 打包钩子(afterPack / afterSign)
└── codemods/ # 一次性仓库级重构脚本
```
```text
docs/
├── .vitepress/ # 文档站导航与主题配置
├── community/ # 社区贡献规范
├── engineering/ # 仓库级工程规则、harness 与漂移治理说明
├── public/ # 文档站公开静态资源
├── logos/ # 可编辑 Logo 源文件与草稿
├── screenshots/ # 文档截图
└── *.md # 面向用户和维护者的文档页面
```
## 放置规则
- 测试文件尽量与被测源码同目录,命名使用 `*.test.ts` 或 `*.test.tsx`。
- 运行时数据放 `data/`,临时排障文件放 `tmp/`,不要散落在仓库根目录。
- 桌面打包脚本统一放 `scripts/desktop/`,不要把一次性签名或打包命令写进根目录批处理。
- 文档站真正对外可访问的静态资源放 `docs/public/`;仍需继续编辑的素材保留在 `docs/logos/` 或 `docs/screenshots/`。
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 478 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 496 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 329 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 696 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 473 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 435 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 394 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 498 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 324 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 599 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 410 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 486 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 419 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 301 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 634 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 504 KiB

-698
View File
@@ -1,698 +0,0 @@
# 🔌 上游接入指南
本文档详细说明如何将不同类型的 AI 中转站、官方兼容入口和 OAuth 连接接入 Metapi。
[返回文档中心](./README.md)
---
## 概述
Metapi 当前支持三类上游接入方式:
1. **中转聚合平台** — New API / One API / OneHub / DoneHub / Veloera / AnyRouter / Sub2API / CPA 等
2. **官方 API 端点** — OpenAI / Claude (Anthropic) / Gemini (Google) 直连
3. **官方预设与 OAuth 连接** — Coding Plan / DeepSeek / Moonshot / MiniMax / ModelScope 等官方兼容入口,以及 Codex / Claude / Gemini CLI / Antigravity 的浏览器授权登录
其中:
- **平台类型** 决定协议适配和管理能力
- **官方预设** 还是落在 `openai` / `claude` 这类平台上,但会自动带出官方地址和推荐模型
- **OAuth 连接** 不在「站点管理」里手填账号密码,而是在「OAuth 管理」里单独授权
每种接入方式略有不同,本文档按类型分别说明。
---
## 🎯 快速接入流程
### 通用步骤
1. **登录管理后台** — 访问 `http://your-metapi-host:4000`,使用 `AUTH_TOKEN` 登录
2. **进入站点管理** — 点击左侧菜单「站点管理」
3. **添加站点** — 点击「添加站点」按钮
4. **填写站点信息** — 根据站点类型填写对应字段;如果是官方入口,也可以直接选择预设
5. **添加连接** — 按场景继续添加账号、API Key,或改去「OAuth 管理」授权
6. **验证连接** — 系统自动验证账号可用性并获取模型列表
### 一句话判断该走哪条路
| 你手上有什么 | 推荐入口 | 推荐方式 |
|------|------|------|
| 有后台面板的聚合站 | 站点管理 | 先加站点,再加账号 / Session |
| 只有 Base URL + API Key 的兼容接口 | 站点管理 | 先加站点,再加 API Key |
| 官方 Coding / API 兼容入口 | 站点管理 | 直接选择官方预设 |
| Codex / Claude / Gemini CLI / Antigravity 的 provider 账号 | OAuth 管理 | 直接浏览器授权 |
---
## 📦 中转聚合平台接入
### New API
**适用平台:** New API 及其衍生版本(AnyRouter、VO-API、Super-API、RIX-API、Neo-API 等)
#### 站点配置
| 字段 | 说明 | 示例 |
|------|------|------|
| **站点名称** | 自定义名称,便于识别 | `我的 New API` |
| **站点 URL** | New API 部署地址(**不含** `/v1` 后缀) | `https://api.example.com` |
| **平台类型** | 选择 `new-api` | - |
| **代理 URL** | (可选)该站点专用代理地址 | `http://proxy.example.com:7890` |
| **使用系统代理** | 是否使用全局 `SYSTEM_PROXY_URL` | 默认关闭 |
#### 账号凭证类型
New API 支持三种凭证类型:
##### 1. 用户名密码登录(推荐 AnyRouter 使用)
- **适用场景:** 有完整账号权限,需要自动签到、余额查询、账号令牌管理
- **填写方式:**
- 用户名:`your-username`
- 密码:`your-password`
- **自动获取:** 系统自动登录并获取 Access Token 和账号令牌
##### 2. Access Token / Session Cookie
- **适用场景:** 已有登录凭证,无需密码
- **填写方式:**
- 在「Access Token」字段填入以下任一格式:
- Session Cookie:**(最不推荐)**
- 可通过浏览器 F12 获取 ![Session Cookie 获取](./screenshots/session-cookie-f12.png)
- 一般为如下格式:`session=MTczNjQxMjM0NXxEdi1CQUFFQ180SUFBUkFCRUFBQVB2LUNBQUVHYzNSeWFXNW5EQThBRFhObGMzTnBiMjVmZEdGaWJHVUdjM1J5YVc1bkRBSUFBQT09fGRlYWRiZWVmMTIzNDU2Nzg5MGFiY2RlZjEyMzQ1Njc4OTBhYmNkZWY=`
- 系统访问令牌和用户 ID **(推荐非 AnyRouter 的其他 New API 站点使用)**
- ![](./screenshots/account-management.png)
- **自动解析:** 系统自动识别凭证类型并提取用户信息
##### 3. API Key(仅代理)
- **适用场景:** 仅用于模型调用,不需要余额管理、自动签到等功能
- **填写方式:**
- 在「API Token」字段填入:`sk-xxxxxxxxxxxxxx`
- **限制:** 无法使用签到、余额刷新、账号令牌管理等功能
#### 特殊说明
**User ID 自动探测:** New API 通常需要在请求头中携带 `New-API-User` / `Veloera-User` / `voapi-user` 等字段。Metapi 会自动:
1. 从 JWT Token 中解码 User ID
2. 从 Session Cookie 中提取 User ID(支持 Gob 编码解析)
3. 通过探测常见 ID 范围验证可用性
如果以上方法都不能获取到 ID,则需要用户手动获取。
**防护盾穿透:** 自动处理阿里云盾 / Cloudflare 等 JS 挑战(`acw_sc__v2` / `cdn_sec_tc`),无需手动配置。
---
### One API
**适用平台:** One API 原版及兼容分支
#### 站点配置
| 字段 | 说明 | 示例 |
|------|------|------|
| **站点名称** | 自定义名称 | `One API 主站` |
| **站点 URL** | One API 部署地址 | `https://oneapi.example.com` |
| **平台类型** | 选择 `one-api` | - |
#### 账号凭证
One API 支持与 New API 相同的三种凭证类型(用户名密码 / Access Token / API Key),配置方式相同。
---
### OneHub
**适用平台:** OneHub(One API 增强分支)
#### 站点配置
| 字段 | 说明 | 示例 |
|------|------|------|
| **站点名称** | 自定义名称 | `OneHub 站点` |
| **站点 URL** | OneHub 部署地址 | `https://onehub.example.com` |
| **平台类型** | 选择 `one-hub` | - |
#### 账号凭证
OneHub 继承 One API 的凭证体系,支持用户名密码、Access Token、API Key 三种方式。
**额外功能:** OneHub 支持 Token 分组(`token_group`),Metapi 会自动识别并保留分组信息。
---
### DoneHub
**适用平台:** DoneHub(OneHub 增强分支)
#### 站点配置
| 字段 | 说明 | 示例 |
|------|------|------|
| **站点名称** | 自定义名称 | `DoneHub 站点` |
| **站点 URL** | DoneHub 部署地址 | `https://donehub.example.com` |
| **平台类型** | 选择 `done-hub` | - |
#### 账号凭证
DoneHub 完全兼容 OneHub 的凭证体系,配置方式相同,DoneHub 获取令牌位置如下图所示:![DoneHub 令牌位置](./screenshots/donehub-token.png)
---
### Veloera
**适用平台:** Veloera API 网关
#### 站点配置
| 字段 | 说明 | 示例 |
|------|------|------|
| **站点名称** | 自定义名称 | `Veloera 网关` |
| **站点 URL** | Veloera 部署地址 | `https://veloera.example.com` |
| **平台类型** | 选择 `veloera` | - |
#### 账号凭证
Veloera 基于 New API 架构,支持相同的凭证类型。特别注意:
- Veloera 需要 `Veloera-User` 请求头,Metapi 会自动添加
---
### Sub2API
**适用平台:** Sub2API 订阅制中转平台
#### 站点配置
| 字段 | 说明 | 示例 |
|------|------|------|
| **站点名称** | 自定义名称 | `Sub2API 订阅站` |
| **站点 URL** | Sub2API 部署地址 | `https://sub2api.example.com` |
| **平台类型** | 选择 `sub2api` | - |
#### 账号凭证
Sub2API 常见 JWT 短期会话机制,和传统 NewAPI 站点差异较大。按下面步骤进行添加:
首先去中转站点 F12 打开如下界面:
![Sub2API 认证字段示例](./screenshots/sub2api-auth-f12.png)
然后回到 Metapi 账号添加处:
![Sub2API Session 配置](./screenshots/sub2api-session-config.png)
1. 在「凭证模式」里选择 Session 模式,分别粘贴 F12 界面中的 `auth_token`、`refresh_token`、`token_expires_at` 字段进行验证,无需配置用户 ID。
2. 不要使用账号密码登录,Metapi 不支持代替 Sub2API 做登录
3. Sub2API 通常为订阅制使用,不支持签到;如果你只关心代理调用,也可以直接改用 API Key 模式
4. 若 `GET /v1/models` 为空,先确认该账号下已有可用用户 API Key,Metapi 会再尝试用它发现模型
---
### CLIProxyAPI / CPA
**适用平台:** CLIProxyAPI / CPA 一类标准 API provider
#### 站点配置
| 字段 | 说明 | 示例 |
|------|------|------|
| **站点名称** | 自定义名称 | `本地 CPA` |
| **站点 URL** | CPA 暴露的 Base URL | `http://127.0.0.1:8317` |
| **平台类型** | 选择 `cliproxyapi` | - |
#### 账号凭证
CPA 这类站点推荐直接使用 **API Key**:
- 在「API Token」字段填入 CPA 提供的密钥
- 不建议把它当成可签到、可查余额、可账号登录的传统面板站
#### 特殊说明
- 本地默认地址常见为 `http://127.0.0.1:8317`
- 远端实例也可能通过 `cliproxy` / `cpa` 风格 URL 或响应头被自动识别
- 当前重点支持 **模型发现** 与 **代理调用**
- 不支持用户名密码登录、自动签到、站点余额 / 用户信息抓取
---
## 🌐 官方 API 端点接入
### OpenAI
**适用场景:** 直连 OpenAI 官方 API 或 OpenAI 兼容端点
#### 站点配置
| 字段 | 说明 | 示例 |
|------|------|------|
| **站点名称** | 自定义名称 | `OpenAI 官方` |
| **站点 URL** | OpenAI API 端点(**不含** `/v1` 后缀) | `https://api.openai.com` |
| **平台类型** | 选择 `openai` | - |
| **代理 URL** | (推荐)配置代理以访问 OpenAI | `http://proxy.example.com:7890` |
#### 账号凭证
**仅支持 API Key:**
- 在「API Token」字段填入:`sk-proj-xxxxxxxxxxxxxx`
- 不支持用户名密码登录(OpenAI 无此接口)
#### 功能限制
| 功能 | 支持情况 |
|------|----------|
| 模型列表获取 | ✅ 支持(`/v1/models`) |
| 代理调用 | ✅ 支持 |
| 余额查询 | ❌ 不支持(OpenAI 无公开接口) |
| 自动签到 | ❌ 不适用 |
| 账号令牌管理 | ❌ 不适用 |
---
### Claude (Anthropic)
**适用场景:** 直连 Anthropic Claude API
#### 站点配置
| 字段 | 说明 | 示例 |
|------|------|------|
| **站点名称** | 自定义名称 | `Claude 官方` |
| **站点 URL** | Anthropic API 端点 | `https://api.anthropic.com` |
| **平台类型** | 选择 `claude` | - |
#### 账号凭证
**仅支持 API Key:**
- 在「API Token」字段填入:`sk-ant-api03-xxxxxxxxxxxxxx`
#### 功能限制
| 功能 | 支持情况 |
|------|----------|
| 模型列表获取 | ✅ 支持(内置模型目录) |
| 代理调用 | ✅ 支持(自动转换 OpenAI ⇄ Claude 格式) |
| 余额查询 | ❌ 不支持 |
| 自动签到 | ❌ 不适用 |
| 账号令牌管理 | ❌ 不适用 |
**协议转换:** Metapi 自动处理 OpenAI 格式与 Claude Messages API 格式的双向转换,下游客户端可使用 OpenAI SDK 调用 Claude 模型。
---
### Gemini (Google)
**适用场景:** 直连 Google Gemini API
#### 站点配置
| 字段 | 说明 | 示例 |
|------|------|------|
| **站点名称** | 自定义名称 | `Gemini 官方` |
| **站点 URL** | Gemini API 端点 | `https://generativelanguage.googleapis.com` |
| **平台类型** | 选择 `gemini` | - |
#### 账号凭证
**仅支持 API Key:**
- 在「API Token」字段填入:`AIzaSyxxxxxxxxxxxxxx`
#### 功能限制
| 功能 | 支持情况 |
|------|----------|
| 模型列表获取 | ✅ 支持(`/v1beta/models`) |
| 代理调用 | ✅ 支持(自动转换 OpenAI ⇄ Gemini 格式) |
| 余额查询 | ❌ 不支持 |
| 自动签到 | ❌ 不适用 |
| 账号令牌管理 | ❌ 不适用 |
**协议转换:** Metapi 自动处理 OpenAI 格式与 Gemini `generateContent` API 格式的双向转换。
---
## 🧩 官方兼容预设(推荐)
除了手动选择 `openai` / `claude` 平台,现在站点编辑器里还内置了一批**官方预设**。这类预设更适合:
- 直接接官方兼容入口
- 自动填入官方推荐 URL
- 先用 API Key 初始化,再补推荐模型
- 保留接入语义,后续创建完站点后更容易知道下一步怎么配
### 当前内置预设
| 预设 | 底层平台 | 默认地址 | 推荐说明 |
|------|------|------|------|
| 阿里云 CodingPlan / OpenAI | `openai` | `https://coding.dashscope.aliyuncs.com/v1` | 推荐先加 API Key,再补编程模型 |
| 阿里云 CodingPlan / Claude | `claude` | `https://coding.dashscope.aliyuncs.com/apps/anthropic` | 适合 Claude Code 一类工具直连 |
| 智谱 Coding Plan / OpenAI | `openai` | `https://open.bigmodel.cn/api/coding/paas/v4` | 推荐先加 API Key,再补 GLM 编程模型 |
| 智谱 Coding Plan / Claude | `claude` | `https://open.bigmodel.cn/api/anthropic` | 当前更适合手动选择预设 |
| DeepSeek / OpenAI | `openai` | `https://api.deepseek.com/v1` | 适合直接接 DeepSeek 官方兼容入口 |
| DeepSeek / Claude | `claude` | `https://api.deepseek.com/anthropic` | 适合 Claude 兼容用法 |
| Moonshot(Kimi) / OpenAI | `openai` | `https://api.moonshot.cn/v1` | 适合 Kimi 官方 OpenAI 兼容入口 |
| Moonshot(Kimi) / Claude | `claude` | `https://api.moonshot.cn/anthropic` | 适合 Claude 兼容用法 |
| MiniMax / OpenAI | `openai` | `https://api.minimaxi.com/v1` | 推荐先加 API Key |
| MiniMax / Claude | `claude` | `https://api.minimaxi.com/anthropic` | 适合 Claude Code 兼容入口 |
| ModelScope / OpenAI | `openai` | `https://api-inference.modelscope.cn/v1` | 适合开源编程模型兼容接入 |
| ModelScope / Claude | `claude` | `https://api-inference.modelscope.cn` | 适合 Claude 兼容接入 |
| 豆包 Coding Plan / OpenAI | `openai` | `https://ark.cn-beijing.volces.com/api/coding/v3` | 适合火山方舟 Coding Plan |
#### 使用建议
1. 选中预设后,优先保留它自动填好的 URL
2. 如果你只是接一个普通自建网关,不想保留预设语义,再手动改回通用 `openai` / `claude`
3. 对官方预设来说,**不要机械地把 `/v1` 或 `/anthropic` 删掉**,以预设默认值为准
---
## 🔐 OAuth 连接接入
有些 provider 更适合在左侧菜单 **OAuth 管理** 里直接授权,而不是在「站点管理 → 账号管理」里手填凭证。
### 当前推荐走 OAuth 的 provider
| Provider | 对应平台 | 自动创建的站点名 | 适用说明 |
|------|------|------|------|
| Codex | `codex` | `ChatGPT Codex OAuth` | 直接授权 Codex 账号 |
| Claude | `claude` | `Anthropic Claude OAuth` | 直接授权 Claude / Anthropic 账号 |
| Gemini CLI | `gemini-cli` | `Google Gemini CLI OAuth` | 支持浏览器授权,可选 Project ID |
| Antigravity | `antigravity` | `Google Antigravity OAuth` | 适合 Antigravity provider 登录 |
#### 什么时候应该走 OAuth
- 你接的不是一个普通面板站,而是 provider 自己的账号
- 你不想手填 Cookie / Session / API Key
- 你希望后续刷新、重绑也继续走 provider 官方授权流程
#### 使用建议
1. 进入左侧菜单 **OAuth 管理**
2. 选择对应 provider 发起授权
3. 如果是远程部署,按页面提示使用 SSH 隧道或手动回填 callback URL
4. 授权成功后,Metapi 会自动创建或复用对应站点与连接
完整流程见 [OAuth 管理](./oauth.md)。
---
## 🔧 高级配置
### 站点级代理
每个站点可单独配置代理,优先级高于全局 `SYSTEM_PROXY_URL`:
```
站点专用代理 > 全局系统代理 > 直连
```
**配置方式:**
1. 在站点编辑页面填写「代理 URL」字段
2. 格式:`http://proxy-host:port` 或 `socks5://proxy-host:port`
3. 支持 HTTP / HTTPS / SOCKS5 代理
### 站点权重
**全局权重(`global_weight`):** 影响该站点下所有通道的路由概率。
- 默认值:`1.0`
- 范围:`0.1` ~ `10.0`
- 示例:
- 设置为 `2.0` — 该站点通道被选中的概率翻倍
- 设置为 `0.5` — 该站点通道被选中的概率减半
**配置位置:** 站点编辑页面 → 高级设置 → 全局权重
### 外部签到 URL
**适用场景:** 某些站点的签到接口非标准路径,或需要通过外部服务触发签到。
**配置方式:**
1. 在站点编辑页面填写「外部签到 URL」
2. Metapi 会向该 URL 发送 POST 请求执行签到
3. 请求头自动携带账号凭证
### API 请求地址池
现在不少站点已经不是“一个 URL 同时负责后台面板和 `/v1/*` API 请求”。
Metapi 当前支持把这两层拆开:
- **主站点 URL**:用于登录、签到、后台接口、系统访问令牌管理
- **API 请求地址池**:用于真正转发 `/v1/*`、`/chat/completions`、`/responses` 等模型请求
适合填写 API 请求地址池的场景:
1. 控制台地址和 API 网关地址不同
2. 同一个站点有多个 API host,需要健康切换
3. 你想保留面板地址不变,只把模型流量导向专用网关
简单理解:
```text
主站点 URL = 控制面
API 请求地址池 = 数据面
```
---
## 🔍 站点自动检测
Metapi 支持自动识别站点类型,当前检测优先级如下:
### 1. 官方预设检测
下列地址会优先识别成官方预设,并返回 `initializationPresetId`:
| URL / 特征 | 识别结果 |
|------|------|
| `coding.dashscope.aliyuncs.com/v1` | `codingplan-openai` |
| `coding.dashscope.aliyuncs.com/apps/anthropic` | `codingplan-claude` |
| `open.bigmodel.cn/api/coding/paas/v4` | `zhipu-coding-plan-openai` |
| `api.deepseek.com/v1` | `deepseek-openai` |
| `api.deepseek.com/anthropic` | `deepseek-claude` |
| `api.moonshot.cn/v1` | `moonshot-openai` |
| `api.moonshot.cn/anthropic` | `moonshot-claude` |
| `api.minimaxi.com/v1` | `minimax-openai` |
| `api.minimaxi.com/anthropic` | `minimax-claude` |
| `api-inference.modelscope.cn/v1` | `modelscope-openai` |
| `api-inference.modelscope.cn` | `modelscope-claude` |
| `ark.cn-beijing.volces.com/api/coding/v3` | `doubao-coding-openai` |
> [!NOTE]
> 智谱 Coding Plan 的 Claude 兼容入口当前不会按 URL 强制自动识别,更适合手动选预设。
### 2. URL 特征检测
根据 URL 中的关键字自动识别:
| URL 特征 | 识别为 |
|----------|--------|
| `api.openai.com` | OpenAI |
| `api.anthropic.com` | Claude |
| `generativelanguage.googleapis.com` | Gemini |
| `chatgpt.com/backend-api/codex` | Codex |
| `127.0.0.1:8317` / `localhost:8317` / `cliproxy` | CLIProxyAPI / CPA |
| `anyrouter` | AnyRouter |
| `donehub` / `done-hub` | DoneHub |
| `onehub` / `one-hub` | OneHub |
| `veloera` | Veloera |
| `sub2api` | Sub2API |
### 3. 页面标题检测
访问站点首页,解析 `<title>` 标签识别平台类型。
### 4. API 探测
依次尝试各平台的特征接口:
- New API:`/api/status` 返回 `system_name`
- One API:`/api/status` 返回特定结构
- OpenAI:`/v1/models` 返回模型列表
- CPA:`/v0/management/openai-compatibility` 返回兼容信息或特征响应头
**手动指定:** 如果自动检测失败,可在添加站点时手动选择平台类型或直接选择官方预设。
---
## 📊 账号健康状态
Metapi 自动追踪每个账号的健康状态:
| 状态 | 说明 | 触发条件 |
|------|------|----------|
| `healthy` | 健康 | 最近请求成功,余额充足 |
| `degraded` | 降级 | 部分模型不可用,或余额不足 |
| `unhealthy` | 不健康 | 连续失败,或凭证过期 |
| `disabled` | 已禁用 | 手动禁用或站点禁用 |
**自动恢复:** `unhealthy` 状态的账号会定期重试,成功后自动恢复为 `healthy`。
---
## 📰 站点公告
Metapi 会定期抓取已接入站点的公告,并在首次发现时写入站内通知与「站点公告」页面。
当前支持的公告来源:
- `new-api`:读取 `/api/notice` 的全站公告
- `done-hub`:读取 `/api/notice` 的全站公告
- `sub2api`:读取 `/api/v1/announcements` 的公告列表
行为约定:
1. 首次发现的公告会触发一次 Metapi 通知
2. 已经保存过的公告再次同步时只更新本地记录
3. `sub2api` 这类需要登录态的公告接口,会优先使用站点下已启用账号的会话令牌
4. 「站点公告」页面的清空操作只影响 Metapi 本地数据库
---
## 🛠️ 故障排查
### 问题:添加站点后无法获取模型列表
**可能原因:**
1. 站点 URL 填写错误
- 通用平台常见是填根地址,错误地带上了不该带的 `/v1`
- 官方预设则相反,**不要**把预设自动带出的 `/v1` / `/anthropic` / `/api/coding/...` 手动删掉
2. 网络不通(检查代理配置或防火墙)
3. 凭证无效(重新验证账号密码、Session 或 API Key)
4. 主站点 URL 和真实 API 请求地址不同,但还没配置 API 请求地址池
**解决方法:**
- 在站点详情页点击「测试连接」
- 查看「事件日志」中的错误信息
- 检查「代理日志」中的请求详情
### 问题:New API 账号提示「需要 New-API-User 头」
**原因:** 该站点是 New API 衍生版本,需要 User ID。
**解决方法:**
1. Metapi 会自动探测 User ID,通常无需手动配置
2. 如果自动探测失败,可在账号编辑页面的「额外配置」中手动填写:
```json
{
"platformUserId": 12345
}
```
### 问题:CPA 为什么没有账号登录、签到、余额刷新
**原因:** `cliproxyapi` / CPA 在 Metapi 里按标准 API provider 处理,重点支持模型发现与代理调用,不按传统面板站处理。
**解决方法:**
- 直接使用 API Key 模式
- 不要再按「用户名密码登录 + 自动签到」的思路排查
### 问题:什么时候不该再用账号管理,而应该去 OAuth 管理
**原因:** 你接的不是普通面板站,而是 provider 自己的授权账号。
**解决方法:**
- Codex / Claude / Gemini CLI / Antigravity 这类连接直接去左侧 **OAuth 管理**
- 不要在账号管理里反复尝试 Cookie / Session / API Key 兜底
### 问题:签到失败
**可能原因:**
1. 凭证过期(Access Token 有效期到期)
2. 站点签到接口变更
3. 已经签到过(部分站点限制每日一次)
**解决方法:**
- 查看「签到记录」中的失败原因
- 尝试重新登录获取新凭证
- 配置「外部签到 URL」使用备用签到方式
### 问题:余额显示不准确
**原因:** 不同平台的余额单位不同。
**说明:**
- New API 系列:余额单位为「美元」,内部存储为 `quota / 500000`
- OpenAI / Claude / Gemini / CPA:官方或兼容 API 通常无余额查询接口,显示为 `N/A`
---
## 📝 最佳实践
### 1. 凭证选择建议
| 场景 | 推荐凭证类型 | 原因 |
|------|-------------|------|
| 个人站点,需要完整功能 | 用户名密码 / Access Token | 支持自动签到、账号令牌管理 |
| 共享账号,只读权限 | Access Token | 避免密码泄露 |
| 仅用于模型调用 | API Token | 最小权限原则 |
| Provider 原生账号 | OAuth | 更适合后续刷新、重绑与统一管理 |
### 2. 站点命名规范
建议使用清晰的命名规则,便于管理:
```text
[平台类型] - [站点特征] - [用途]
```
示例:
- `New API - 主站 - 生产环境`
- `OneHub - 备用 - 测试`
- `OpenAI - 官方 - 高优先级`
- `CPA - 本地 - 调试`
### 3. 代理配置策略
- **国内访问 OpenAI / Claude / Gemini / 部分 OAuth provider:** 通常需要代理
- **国内中转站:** 通常不需要代理
- **海外中转站:** 根据网络情况选择
- **远程 OAuth 部署:** 除了代理,还要考虑回调端口连通性
### 4. 定期维护
- **每周检查:** 账号健康状态、余额预警
- **每月清理:** 禁用长期不可用的站点和账号
- **凭证轮换:** 定期更新 Access Token 和 API Key
- **官方预设站点:** 新增模型时记得补充推荐模型和 API 请求地址池
---
## 🔗 相关文档
- [配置说明](./configuration.md) — 环境变量与路由参数
- [OAuth 管理](./oauth.md) — Codex / Claude / Gemini CLI / Antigravity 授权接入
- [客户端接入](./client-integration.md) — 下游应用配置
- [常见问题](./faq.md) — 故障排查与优化建议
---
## 💡 提示
- 添加站点后,系统会自动发现可用模型并生成路由表,无需手动配置
- 支持同时接入多个相同类型的站点(如多个 New API 实例)
- 站点禁用后,关联的所有账号和路由通道会自动禁用
- 删除站点会级联删除所有关联账号、Token 和路由配置,请谨慎操作
- 如果你看到站点创建成功后的“官方预设”提示,请按它建议的下一步走,一般会更省事