177 Commits

Author SHA1 Message Date
boos c3293e007f docs(nit): AI Studio 第一阶段(聚合图像/数字人)设计 spec
NIT 精英班专属 AI Studio 第一阶段:聚合 picture_agent(图像)+ digital_human(数字人)。
学员下游 key 配置 + surface 认证扩展(接受 student JWT)+ 积分轨计费 + 统一入口前端。
不碰 money_printer,视频/文本创作/PPT 留后续阶段。前置依赖第一期报考闭环。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-17 04:43:51 +08:00
boos a1b7b60ad4 docs(nit): 第一期报考闭环 A+B+C 设计 spec
NIT 报考学习平台第一期设计:独立 students 表、线下缴费上传凭证人工审核、
开课权限状态标记、平台超管后台、模块内嵌复用现有 axum/sqlx/JWT/workspace 基建。
不含学习中心/OCR/校区负责人多级权限/内容保护(留后续期)。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-17 04:24:05 +08:00
boos bfd804f719 feat(billing): UsageRecords dual-track ledger view
api.ts 新增 userApi.getBillingLedger / getBillingSummary 及 Billing* 类型;
UsageRecords.tsx 增加 USD/积分 双轨切换 tab、积分汇总卡与明细表(切到积分轨时懒加载)。
随文件一并提交此前累积的支付/Comfy Cloud/工作流前端 API 改动。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-17 03:04:11 +08:00
boos e7f3da5961 feat(billing): user billing ledger + summary endpoints
新增 GET /api/user/billing/ledger 与 /api/user/billing/summary 两个用户侧接口,
基于 Task11 的 list_user_ledger / summarize_user_ledger 返回当前用户的明细与聚合。
随文件一并提交此前累积的充值/支付/市场相关改动及测试辅助。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-17 02:56:12 +08:00
boos 5cfd533d89 feat(billing): user ledger list + summary queries 2026-06-17 01:49:46 +08:00
boos 0b7abed79a docs: 细化计费第3期(消费明细)TDD Task11-14 2026-06-17 01:44:28 +08:00
boos a89edf6f75 feat(payment): comfy 登录态/充值确认/记录管理 + Task9 积分到账调用适配
含本会话累积的支付路由功能(firebase-login 回执登录、recharge-records
列表/确认/清空、诊断卡)及 admin_confirm_user_recharge 传 config 的调用适配,
使计费 Task9(comfy 充值改积分)调用方编译完整。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-16 21:13:16 +08:00
boos 5d42671172 feat(billing): comfy recharge to points + admin credit adjust
Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-16 21:11:44 +08:00
boos ca9e45c59c feat(billing): workflow points gating + charge
Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-16 20:58:02 +08:00
boos be7538a1da feat(billing): workflow_points_cost config 2026-06-16 20:32:10 +08:00
boos cfe2098b73 feat(billing): points track apply_points_delta + balance 2026-06-16 20:24:50 +08:00
boos bd464016a8 docs: 细化计费第2期(积分轨)TDD 任务 Task6-10 2026-06-16 20:20:22 +08:00
boos 7e8430934e feat(billing): USD admin adjust writes credit_ledger
Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-16 17:46:56 +08:00
boos ab2fd271e8 feat(billing): USD recharge writes credit_ledger 2026-06-16 17:40:43 +08:00
boos 4dd2be4cc3 feat(billing): USD consume writes credit_ledger in one tx
Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-16 17:34:21 +08:00
boos fd94303433 feat(billing): credit_ledger schema + record_ledger_entry 2026-06-16 17:25:59 +08:00
boos 91dcbc5c53 docs: 计费闭环实现计划(第1期详细TDD + 后5期概要)
第1期地基: credit_ledger + user_credits 表, USD 轨三处记账
(消费/充值/管理员调整)同事务接入 ledger。每步含完整测试与代码。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-16 17:17:54 +08:00
boos 278b0c66b8 docs: 计费闭环系统设计 spec
双轨额度(USD 池 + 工作流积分池)、ledger-first 统一流水账、
消费明细/充值档位/超额预警/全量审计四块功能、A+C 混合积分计费、
Comfy 余额对账校准。分 6 期实现。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-16 17:12:02 +08:00
boos d697146941 feat(audit): login audit log with account info and IPv4
- 审计日志记录具体账号(用户名<邮箱>)
- 优先显示 IPv4,保留 IPv6
- Program Logs 页面新增 account/IPv4 列
2026-06-10 19:54:32 +08:00
boos d1f3f4e93d feat(payment): Stripe Checkout payment module
- payment_orders 建表迁移,支持多 provider(stripe)
- Stripe Checkout Session 创建、Webhook 验签、到账
- 支付成功自动为 API Key 增加额度
- payment.enabled 开关,关闭时不影响旧 Comfy Checkout
2026-06-10 19:54:27 +08:00
boos df97633300 fix(recharge): per-amount distinct checkout URLs
- 前端 /0/0/0 各自携带独立 checkoutUrl
- 后端按金额解析独立 Checkout URL 配置
- 同时校验 optionId 和 amountUsd 防止金额错配
- 移除旧单一固定 checkout URL 兜底
2026-06-10 19:54:20 +08:00
boos 3d54c7d529 fix: support user recharge record cancellation 2026-06-09 17:22:23 +08:00
boos 55b3d4d99a feat: enhance new website experiment motion 2026-06-09 16:59:04 +08:00
boos 59106e6a96 feat: refine BoosAPI console and user workspace 2026-06-09 16:48:18 +08:00
boos aaba6f152e perf: optimize proxy streaming and database access 2026-06-07 00:00:24 +08:00
boos f3d2d50f9a fix: make website boot animation resilient 2026-06-06 22:38:31 +08:00
boos 16162095f7 style: polish admin operations pages 2026-06-06 22:19:42 +08:00
boos 6629d9d0a3 fix: stabilize console topbar layout 2026-06-06 22:09:44 +08:00
boos 3c6f061709 fix: improve assistant thinking and image progress 2026-06-06 22:02:45 +08:00
boos d7d14e077e fix: stabilize website boot loader 2026-06-06 21:54:25 +08:00
boos 3b3292b934 feat: add admin operations war room 2026-06-06 21:50:23 +08:00
boos f7916a42e5 feat: enhance website flow and user workbench 2026-06-06 21:44:58 +08:00
boos f725de28bd feat: polish website and console dashboards 2026-06-06 21:15:33 +08:00
boos 97113964c8 fix: remove sidebar identity card 2026-06-06 21:01:20 +08:00
boos b429695cba fix: stabilize topbar layout 2026-06-06 20:57:33 +08:00
boos d6bf7352a0 feat: support local avatar uploads 2026-06-06 20:52:34 +08:00
boos d800ed1f9f fix: keep admin sidebar actions visible 2026-06-06 20:44:34 +08:00
boos 4855db3fc8 feat: polish user auth entrance 2026-06-06 20:38:18 +08:00
boos 6cd0a8caca fix: stream real agent reasoning 2026-06-06 20:26:31 +08:00
boos 44dc89a0dd refactor: share console page header 2026-06-06 20:18:28 +08:00
boos 823ac3c25d fix: show only generated outputs in workspace 2026-06-06 20:03:02 +08:00
boos 5ecea182bc feat: polish console shell and notification settings 2026-06-06 19:56:56 +08:00
boos cd843465b2 feat: show conversation content in usage logs 2026-06-06 19:36:42 +08:00
boos ef0f91e683 perf: speed up app loading 2026-06-06 19:21:59 +08:00
boos 9c1e5ae293 fix: keep video assistant reasoning label stable 2026-06-06 19:02:41 +08:00
boos 00b3365f5a feat: refine website gateway os hero 2026-06-06 18:59:10 +08:00
boos d5babf5f3f fix: support website color schemes 2026-06-06 18:45:12 +08:00
boos 4b757b8250 feat: add light website intro motion 2026-06-06 18:29:10 +08:00
boos def8d7a204 chore: speed up local build workflow 2026-06-06 18:10:16 +08:00
boos 25c6fdbd6c revert website high contrast redesign 2026-06-06 17:58:10 +08:00
boos 47202e6848 redesign website with high contrast motion 2026-06-06 17:54:38 +08:00
boos 1b2880739a polish auth entry surfaces 2026-06-06 17:42:14 +08:00
boos 188e457e7f polish topbar user experience 2026-06-06 17:31:41 +08:00
boos 13756adc9d polish about page experience 2026-06-06 17:25:31 +08:00
boos f026582008 fix: persist runtime proxy token 2026-06-06 17:11:51 +08:00
boos b3eb86d5ea chore: remove update center 2026-06-06 16:55:15 +08:00
boos 301d086cb6 fix: show runtime logs in program logs 2026-06-06 16:40:55 +08:00
boos 023384b14b style: refine website section spacing 2026-06-06 16:18:11 +08:00
boos d315f0ffe2 feat: polish website model radar 2026-06-06 16:14:05 +08:00
boos f0385c9c8c chore: install vercel agent skills 2026-06-06 16:07:34 +08:00
boos 339f91d014 feat: sync upstream analytics schema support 2026-06-06 16:07:13 +08:00
boos 0d231ec467 feat: enhance website model radar motion 2026-06-06 15:46:51 +08:00
boos be1fc1df48 feat: optimize routes and registration UX 2026-06-06 15:37:37 +08:00
boos dd874bae30 feat: refresh BossAI console frontend 2026-06-06 15:25:04 +08:00
boos 45aa741d9c feat: refine BossAI console and website design 2026-06-06 15:09:53 +08:00
boos 49c398db5e feat: polish BossAI website motion 2026-06-06 14:56:33 +08:00
boos 4d748dd938 feat: refine BossAI website product story 2026-06-06 06:27:28 +08:00
boos b40c522749 feat: add gsap motion to BossAI website 2026-06-06 06:18:40 +08:00
boos d467e27fd8 fix: guard embedded frontend assets 2026-06-06 06:11:15 +08:00
boos 5ef017c54e feat: serve website at root path 2026-06-06 05:59:48 +08:00
boos 03046152fd feat: isolate BossAI website with Next.js 2026-06-06 05:30:10 +08:00
boos d75daf72d6 feat: add premium public landing page 2026-06-06 05:06:37 +08:00
boos d39100b56d feat: optimize money printer factory 2026-06-06 04:56:02 +08:00
boos 2e4c5801b6 chore: tighten repository hygiene 2026-06-06 04:40:25 +08:00
boos 870b2a2da3 feat: surface admin settings attention states 2026-06-06 03:57:15 +08:00
boos 4448567368 feat: optimize admin settings page 2026-06-06 03:49:30 +08:00
boos fae0f607aa test: restore project verification suite 2026-06-06 03:35:29 +08:00
boos e4630c4cb6 fix: harden workflow smoke issues 2026-06-06 03:23:39 +08:00
boos e72e323313 feat: refine money printer comfy template mode 2026-06-06 03:09:08 +08:00
boos 2b836b3e1e Add built-in Cloud video template fallback 2026-06-06 02:58:16 +08:00
boos 598b2f48d6 Handle Comfy Cloud video job status 2026-06-05 22:01:03 +08:00
boos 3f22d7b1ab Make short video factory use Comfy templates 2026-06-05 21:35:12 +08:00
boos 5ce5ea1687 Route Comfy workflows through video API 2026-06-03 11:39:14 +08:00
boos f8741b544e Filter MoneyPrinter Comfy video templates 2026-06-03 11:29:55 +08:00
boos c316a78511 Default MoneyPrinter videos to BoosAPI route 2026-06-03 11:03:04 +08:00
boos 68ae9acb27 Clarify bundled Picture Agent template errors 2026-06-03 11:01:47 +08:00
boos 13c7d8aa4e Remove stale Picture Agent remote template state 2026-06-03 10:59:11 +08:00
boos fb5fc9f1ae Use bundled Linux Do picture templates 2026-06-03 10:55:43 +08:00
boos 5765ea48b0 Clarify Picture Agent template refresh state 2026-06-02 17:38:54 +08:00
boos 55bd3f41f8 Follow Picture Agent gallery data URL 2026-06-02 17:30:15 +08:00
boos 938597b766 Fix Picture Agent loading fallback 2026-06-02 17:23:36 +08:00
boos fe0e3faa22 Generate MoneyPrinter Comfy prompts from form 2026-06-02 17:18:23 +08:00
boos d0f26118ff Normalize MoneyPrinter Comfy workflow templates 2026-06-02 17:11:15 +08:00
boos c98d9a2c6d Load Comfy templates for MoneyPrinter 2026-06-02 17:00:46 +08:00
boos 1293ebae68 Use Comfy API for MoneyPrinter video generation 2026-06-02 16:47:48 +08:00
boos b29c7ffa7a Add native MoneyPrinter short video factory 2026-06-02 15:26:42 +08:00
boos 056e9fb651 Improve digital human template UI 2026-06-02 12:17:05 +08:00
boos fa5a2419d8 Add Comfy digital human jobs 2026-06-02 11:07:31 +08:00
boos 7b34153235 Proxy external template requests 2026-06-01 11:21:56 +08:00
boos 04792bd3f7 Improve offline template loading 2026-06-01 11:02:36 +08:00
boos bb31118792 Reference local Workflow UI repo directly 2026-05-28 17:19:32 +08:00
boos 5027767ca2 Use local Workflow UI source for BossAI deploy 2026-05-28 17:16:12 +08:00
boos fd930757f9 Move BossAI deploy worktree out of tmp 2026-05-28 17:03:08 +08:00
boos eb6ed11cff Add BossAI Debian deploy script 2026-05-28 16:59:51 +08:00
boos 931bcf3fb9 Update BossAI workflow and workspace experience 2026-05-28 10:32:20 +08:00
boos e0bd9b4194 Embed workflow UI and isolate workspace assets 2026-05-26 17:00:10 +08:00
boos 0f9fe4a81d chore: prune legacy node backend and mix workflow models 2026-05-26 15:18:33 +08:00
boos 6869504119 chore: add optional bun frontend build 2026-05-25 22:05:03 +08:00
boos 3f9f9f7665 docs: remove readme screenshots 2026-05-25 21:43:46 +08:00
boos a1f3400af3 docs: add hardware requirements 2026-05-25 21:41:47 +08:00
boos da686ecda4 chore: vendor workflow ui as submodule 2026-05-25 21:19:49 +08:00
boos 1ba293c262 feat: polish workflow studio and workspace assets 2026-05-25 20:42:38 +08:00
boos cb35d5fc6e feat: add workflow studio and postgres support 2026-05-25 16:03:07 +08:00
boos 7cdb02ef1d feat: add workspace and claude messages support 2026-05-22 05:57:37 +08:00
boos c1074ddf71 Integrate ComfyUI agents and video workflows 2026-05-22 04:46:15 +08:00
boos 3c12738138 fix: allow comfyui assets through proxy 2026-05-22 01:27:57 +08:00
boos 8de175a8c5 feat: add comfyui agent attachments 2026-05-22 01:16:37 +08:00
boos 7c6364a160 fix: enable multi tenant comfy workspace 2026-05-22 00:44:57 +08:00
boos 5ef3f8aea8 fix: route video workspace to comfyui proxy 2026-05-21 17:27:41 +02:00
boos 9e7ce80574 fix: complete account token admin routes 2026-05-21 17:16:24 +02:00
boos 65d2f1bbe5 fix: restore account health tokens and dashboard data 2026-05-21 17:07:18 +02:00
boos 0207ba748f fix: use user api key for comfyui agent 2026-05-21 16:55:32 +02:00
boos d0912e44b4 fix: route video agent to comfyui and ignore openspec 2026-05-21 16:44:30 +02:00
boos 9c84759498 feat: merge opus comfyui and login updates 2026-05-21 16:32:04 +02:00
boos c16479da7c fix(rust): sweep orphan route_channels when their model has no active account
The per-route loop in `rebuild_routes` only visits routes whose model_pattern
appears in the current `by_model` set. When the last active account for a
model goes away (account deleted, status flipped, model dropped from
model_availability), the model falls out of `by_model` entirely and its
existing auto-managed channels are never reconciled — they stay behind
pointing at a route nothing can serve.

Add a final orphan sweep over all auto-managed route_channels. Anything whose
route wasn't processed in the per-route loop gets hard-deleted. Manual-override
channels still get the same exemption as before.

Covered by two new in-memory sqlite tests:
- orphan_channel_is_cleaned_when_last_account_goes_inactive
- orphan_sweep_preserves_manual_channels
2026-05-20 19:58:45 +08:00
boos 423912277b fix(build): reuse multi-tenant ComfyUI image instead of rebuilding every run
- skip docker build when boosapi-comfyui:multi-tenant already exists;
  --rebuild-image opts back into a fresh build
- fall back to `docker load` from docker/boosapi-comfyui-multi-tenant-*.tar
  when the image is absent but the saved tar is present
- gate the whole block on `docker info` so a client-only host doesn't hang
  on build when the daemon is unreachable
2026-05-20 19:40:39 +08:00
boos cf696c01a5 fix(rust): trigger route rebuild on account writes; add segment filter
- spawn_route_rebuild after create / delete / model refresh / manual model
  upsert; gate update_account on status change so cosmetic edits don't
  burn a transaction
- list_accounts now accepts ?segment=apikey|session|oauth, filtered against
  extra_config.$.credentialMode (legacy null rows treated as apikey)
2026-05-20 19:35:53 +08:00
boos 7c9a1e2cc4 fix(rust): match main's drizzle-style camelCase response shape
Frontend reads main's drizzle-generated camelCase fields (siteId,
accessToken, oauthProvider, etc.) and a nested `site: {...}` on each
account row. Rust models were serializing snake_case with no nested
site, so PUT /api/sites/undefined/disabled-models 404s, account.site?.id
is undefined everywhere, and similar shape mismatches break Accounts,
Sites, Tokens, and Dashboard pages.

- Add #[serde(rename_all = "camelCase")] to every model struct in
  src/db/models/* (Account, AccountToken, Site, User, Event,
  TokenRoute, RouteChannel, BackgroundTask, DownstreamApiKey,
  Organization/Member/Invitation, SiteAnnouncement)
- list_accounts now nests `site: {...}` per row, matching main's
  `{...account, site: {...}}` shape from accountsOverviewService
- admin user-detail endpoint emits camelCase keys (totalRequests,
  totalCost, last7DaysCost, recentLogs, promptTokens, completionTokens,
  totalTokens, createdAt) to match the frontend
- routes refresh endpoint now emits both affected_routes and
  affectedRoutes for forward compat

Co-Authored-By: Kiro <noreply@kiro.dev>
2026-05-20 07:21:40 +08:00
boos 9c0d4ec69f feat(rust): wire downstream-key billing, add /v1/responses, ComfyUI shim
Previously every executor took `_state: AppState` (unused) and `check_auth`
discarded the matched downstream key id, so `usage_logs` was orphan code.
This change plumbs the key id from auth → handlers → executors → billing,
adds the OpenAI Responses API endpoint, and exposes shim aliases under
`/api/comfyui-agent/proxy/openai/*` so embedded ComfyUI's OpenAI nodes
flow through the same auth/conductor/billing pipeline.

- check_auth now returns Option<i64> (managed key id) instead of unit
- record_billing helper writes usage_logs after each successful upstream call
- extract_usage handles both chat-completions and responses usage shapes
- conductor wrappers record billing post-Success; stream variant scans
  chunks in reverse for the final usage event
- POST /v1/responses (stream + non-stream) via ProxyConductor
- shim aliases mounted on the same Router so auth/failover/billing work for free
- ComfyUIProcess injects BOOSAPI_BUILTIN_KEY env + --comfy-api-base on spawn
- bootstrap built-in `comfyui-agent` downstream key at startup (idempotent)
- get_auth_header in vendored ComfyUI falls back to BOOSAPI_BUILTIN_KEY env

Co-Authored-By: Kiro <noreply@kiro.dev>
2026-05-20 06:20:39 +08:00
boos 22751effd5 fix: refresh user logout state 2026-05-19 10:41:40 +02:00
boos 4fe84707e5 feat: complete user login admin keys and deployment docs 2026-05-19 10:06:52 +02:00
boos 812a7c6c07 fix: persist site api endpoints and account verification 2026-05-19 11:22:09 +08:00
boos b6aae5f301 feat: unify user and admin login entry 2026-05-19 10:53:38 +08:00
boos 27c2aca312 fix: show site presets and api key site options 2026-05-19 10:49:41 +08:00
boos 11144d0697 fix: complete gemini frontend and usage follow-up 2026-05-19 10:37:29 +08:00
boos 31e95c2634 fix: usage SQL 查询 ambiguous column
l.created_at 缺少表别名前缀导致歧义错误
2026-05-19 03:06:11 +08:00
boos 499dba6104 fix: 完善用户路由和注册回调
- App.tsx 导入 hasValidUserSession 修复 TS 编译错误
- UserRegister 增加 onLogin 回调,注册成功自动跳转
2026-05-19 02:47:23 +08:00
boos d432ded68f feat: 用户管理页面增加用量统计标签
- 用户列表新增总请求、总消耗($)、7天消耗列
- 点击用户行展开用量详情(趋势+最近日志)
- 添加"用量统计"标签页切换
2026-05-19 02:47:19 +08:00
boos 1cef28169c feat: 管理员创建用户自动生成密钥 & 用量统计 API
- create_user() 自动生成 sk-user-* 密钥并关联用户
  (使用事务确保用户+密钥原子创建)
- 新增 GET /api/admin/users/{id}/usage 返回用量统计
  (总请求/成本、7天成本、30天趋势、最近50条日志)
- api.ts 新增 getUserUsage(id) 方法
- mod.rs 导出 stats 模块供 user 路由复用
2026-05-19 02:47:15 +08:00
boos a0d3743564 fix: 修复后端 API 路径不匹配
Gemini 创建的路由为 /v1/user/logs 和 /v1/user/summary,
但前端 api.ts 调用的是 /api/user/logs 和 /api/user/summary,
改为 /api/ 前缀修复 404。
2026-05-19 02:36:57 +08:00
boos c9e98df3f1 chore: 优化 ComfyUI Docker 配置
- .dockerignore: 精简忽略规则
- config.toml: 添加 [comfyui] 配置段
- Dockerfile: 本地源码构建、清华镜像加速
2026-05-19 02:33:23 +08:00
boos ba07517974 fix: 密钥管理排除站点列表缺少无账号站点
loadExclusionSources() 原只从 accounts 提取站点,
新增的/无关联账号的站点不显示。
增加 api.getSites() 调用,合并所有站点到排除选项。
2026-05-19 02:33:18 +08:00
boos c8a523d973 fix: 站点管理预设选项添加 label 标签
SiteInitializationPreset 类型缺少 label 字段,
导致 coding plan、deepseek 等预设在下拉菜单显示空白。
给接口添加 label: string,全部 13 个预设补上中文标签。
2026-05-19 02:33:13 +08:00
boos 5890a9b4ac feat: 添加用户端后端 API 路由
- 新增 GET /api/user/logs(用户 API 调用历史)
- 新增 GET /api/user/summary(用户使用统计摘要)
- stats.rs: ProxyLogRow 改为 pub 供 user 模块复用
2026-05-19 02:33:08 +08:00
boos bf00296a90 feat: 新增普通用户界面
- 重构 App.tsx 路由,分离普通用户/管理员导航
  (userSidebarGroups / userAuthed 状态)
- UserDashboard 重写为概览页:用量统计、最近记录、快速入口
- 新增 UsageRecords 使用记录页(分页)
- 新增 UserKeys 密钥管理页(创建/删除 sk-user- 密钥)
- 新增 UserProfile 个人资料页(修改用户名/密码)
- UserLogin 增加 onLogin 回调
- api.ts 增加 userApi 工具方法
2026-05-19 02:32:58 +08:00
boos 4f0c4959bf Merge pull request 'feat: update logo image' (#5) from feature/update-logo into refactor/rust-backend
Reviewed-on: boos/BoosAPI#5
2026-05-18 09:24:44 +00:00
Aibo117 dbfe07c736 feat: update all logo and favicon files
Replace logo.png, favicon.png, favicon-64.png, and desktop-icon.png
with the new brand logo.
2026-05-18 17:23:54 +08:00
Aibo117 bde045d0cf feat: update logo image
Replace the default logo with the new brand logo.
2026-05-18 17:19:15 +08:00
boos e9595017e9 feat: support admin account login 2026-05-18 17:01:39 +08:00
boos f986fe0377 feat: complete Rust backend UI compatibility 2026-05-18 16:55:09 +08:00
boos f2434e725d 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
2026-05-18 11:28:20 +08:00
boos bfdb01e542 feat: add 视频助手 nav item pointing to /comfyui-agent 2026-05-18 10:16:55 +08:00
boos 3779826a15 fix: add /api/users/login route alias for frontend compat; return JSON error for unmatched API routes instead of SPA index.html 2026-05-18 10:14:07 +08:00
boos b89b71a95f docs: update README tech stack and dev guide for Rust backend
- Rewrote 技术栈 section (Axum/Tokio/SQLx/TokenRouter)
- Rewrote 本地开发 section (cargo run + npm build)
- Added 项目结构 section describing Rust backend layout
- Added 环境变量 reference (BOOSAPI_*__* naming)
- Updated Docker Compose examples for Rust config vars
- Removed all Node.js runtime/TypeScript refs from backend docs
2026-05-18 09:37:04 +08:00
boos 0eb7c32585 fix: restore frontend source code (src/web/) that was accidentally deleted
Frontend source is needed for development/building the SPA.
Only Node.js runtime server code should be removed.
2026-05-18 09:33:56 +08:00
boos 5d29f03d50 refactor: purge Node.js, Rust is the one true backend
- Moved boosapi-rs/* to repo root
- Deleted all Node.js source (src/server/, src/web/, scripts/)
- Deleted node_modules, package.json, tsconfig, vite.config
- Updated .gitignore and AGENTS.md for Rust-only stack
- Fixed static file path for frontend dist/web/
- Dockerfile stays the same (already Rust-only)
- Kept dist/web/ for Rust static serving
2026-05-18 09:11:29 +08:00
boos 725f44699a refactor: ComfyUI Agent with structured parsing + image gen + TTS
- New system prompt with 【CHARACTER】【SCENE】【TTS】【WORKFLOW】 markers
- Backend parses structured sections, auto-calls gpt-image-2 for character/scene images
- Auto-calls TTS API for dialogue lines
- Case-insensitive field parsing, robust whitespace handling
- Static file serving with SPA fallback for frontend
- No Node.js runtime dependency for serving
2026-05-18 04:02:02 +08:00
boos 56391f2193 feat: Phase 5-6 - Proxy Conductor, Token Router, Services, Dockerfile
- Proxy Conductor: retry/fallback/auth-refresh orchestration loop
- Token Router: exact/wildcard/regex model routing with channel selection
- Services: Cron scheduler, OAuth token refresh, Update center, Announcements
- User routes: profile + API key management
- DB queries module
- Dockerfile: multi-stage build
- Proxy routes integrated with Conductor + TokenRouter
2026-05-18 03:00:09 +08:00
boos 78d46eeddb feat: Search routes migrated to Rust
- POST /api/search — cross-entity search across 6 categories
- Searches: sites, accounts, account_tokens, checkin_logs,
  proxy_logs, and model_availability
- All via SQL LIKE, no external search engine dependency
- Model aggregation with deduplication
- 18/18 tests pass
2026-05-17 17:15:03 +08:00
boos 7a3a88f738 feat: Monitor (LDOH proxy) routes migrated to Rust
- GET/PUT /api/monitor/config — LDOH cookie management via settings table
- POST /api/monitor/session — cookie auth token
- GET /monitor-proxy/ldoh/{*path} — reverse proxy with content rewriting
- Cookie-based auth, HTML/JS/CSS path rewriting
- 18/18 tests pass
2026-05-17 17:13:28 +08:00
boos adb349eecc feat: Stats routes — model-by-site + site-distribution
- GET /api/stats/model-by-site — model usage aggregated by model name
- GET /api/stats/site-distribution — per-site usage summary
- Both query existing model_day_usage table with day-range filter
- 18/18 tests pass
2026-05-17 17:10:35 +08:00
boos 15f73a2793 fix: ComfyUI wildcard route syntax for axum 0.8+
- Changed  to  in comfyui proxy route
- Added config.toml for local dev
- Verified: Rust backend starts and serves /healthz, /api/sites,
  /api/events, /api/tasks, /api/settings, /api/organizations/plans
- 18/18 tests pass
2026-05-17 17:08:36 +08:00
boos fe1a7c2115 feat: User profile routes migrated to Rust
- PATCH /api/users/me — update own username
- POST /api/users/me/password — change password (verify old first)
- GET /api/users/{id} — get user profile (self or admin)
- JWT auth for all endpoints
- 18/18 tests pass
2026-05-17 17:04:54 +08:00
boos 07f427877b feat: Background task SSE streaming endpoint
- Added /api/update-center/tasks/{id}/stream SSE endpoint
- Polls DB for task status updates every 1s (up to 2 min)
- Streams status + log events, ends on completion/timeout
- Added async-stream dependency
- 18/18 tests pass
2026-05-17 17:03:12 +08:00
boos e94c6cf853 feat: Proxy extensions — audio, video, completions, embeddings
- Generic execute_proxy_request executor for arbitrary paths
- POST /v1/completions, /v1/embeddings
- POST /v1/audio/speech, /v1/audio/transcriptions, /v1/audio/translations
- POST /v1/videos
- All reuse existing auth + route resolution infra
- 18/18 tests pass
2026-05-17 17:01:15 +08:00
boos 84b4b16ebd feat: Proxy extensions — /v1/completions + /v1/embeddings
- Added generic execute_proxy_request executor for any path suffix
- POST /v1/completions — forwards to upstream /completions
- POST /v1/embeddings — forwards to upstream /embeddings
- Both use existing auth + route resolution infra
- 18/18 tests pass
2026-05-17 16:59:43 +08:00
boos 4ac9c2385f feat: Organizations + Invitations routes migrated to Rust
- New migration: organizations, organization_members, invitations tables
- New model: Organization, OrganizationMember, Invitation
- Full CRUD: list/create/get/update orgs, members, invitations
- Public invitation lookup + accept flow
- JWT auth for all protected endpoints
- Admin role verification for org management
- 18/18 tests pass
2026-05-17 16:58:27 +08:00
boos fac1aa4a88 feat: Background tasks table + tasks route migrated to Rust
- New migration: background_tasks table
- New model: BackgroundTask (background_task.rs)
- GET /api/tasks with limit param
- GET /api/tasks/{id} with 404 handling
- 18/18 tests pass
2026-05-17 16:56:15 +08:00
boos d7d3670f02 feat: User API Keys routes migrated to Rust
- New user_api_keys.rs route module
- JWT-authenticated CRUD on downstream_api_keys scoped to user_id
- GET/POST/PATCH/DELETE /api/user-api-keys with ownership verification
- Uses existing downstream_api_keys table with user_id FK
- Supports all fields: name, tags, enabled, expires_at, max_cost, etc.
- 18/18 tests pass
2026-05-17 13:37:17 +08:00
boos 9329248e59 feat: Site Announcements routes migrated to Rust
- New SiteAnnouncement model (site_announcement.rs)
- Route handlers: list (with siteId/platform/read/status filters),
  mark read, mark all read, delete all
- In-memory status filtering for active/expired/dismissed (same as Node.js)
- Uses existing site_announcements table from migration
- 18/18 tests pass
2026-05-17 13:34:53 +08:00
boos 2793a7990d feat: Checkin + Events routes migrated to Rust
- /api/checkin — list logs, stats, accounts, toggle, run
- /api/events — list, count, mark read, clear
- CheckinLog + Event models
- cargo check ✅ | cargo test 18/18 ✅
2026-05-17 13:29:17 +08:00
boos af05b40799 feat: Events API routes migrated to Rust
- GET /api/events — list with filters (type, read, pagination)
- GET /api/events/count — unread count
- POST /api/events/{id}/read — mark one as read
- POST /api/events/read-all — mark all as read
- DELETE /api/events — clear all events
- Event model with sqlx FromRow
2026-05-17 13:28:16 +08:00
boos 8568859da6 feat: User management + JWT auth routes
- Admin users CRUD: /api/admin/users + /api/admin/users/{id}
- Public auth: /api/auth/register, /api/auth/login, /api/auth/me
- User model + password hashing via existing auth module
- JWT token generation and validation
- cargo check ✅ | cargo test 18/18 ✅
2026-05-17 13:24:37 +08:00
boos 7987833ba9 feat: Rust refactor — full backend rewrite (Phases 1-6)
Phase 1-3: Scaffold + types + auth (JWT, Argon2, API keys)
Phase 4a: Admin API (sites/accounts/tokens/downstream-keys/settings)
Phase 5: Proxy core (/v1/chat, /v1/images, /v1/models)
Phase 6: ComfyUI proxy routes migrated

- 80+ source files, all cargo check + 18 tests green
- Drop-in compatible with Node.js API surface
2026-05-17 13:20:52 +08:00
boos ad0bb86e31 feat: ComfyUI embedded dev setup + proxy auth with downstream keys
- COMFYUI_BASE configurable via env var (default 127.0.0.1:8188)
- All /api/comfyui/ routes protected by downstream API Key or admin token
- docker/Dockerfile.comfyui + docker-compose.comfyui.yml for dev
- npm scripts: comfyui:install, comfyui:start, comfyui:stop
- .gitignore / .env.example updated
2026-05-17 13:04:46 +08:00
boos 2fe8d8528a fix: ComfyUI Agent uses upstream API via env vars
- Read BOOSAPI_UPSTREAM_BASE/BOOSAPI_UPSTREAM_KEY from env
- Default to api.boos.lat/v1 with sk-boos4721
- Document: internal tool bypasses proxy /v1/ routing layer
- Update AGENTS.md with architecture decision
2026-05-17 04:23:20 +08:00
boos 5e3d4ee7d4 refactor: ComfyUI Agent - fix auth, upstream API, and SSE streaming
- Fix auth: use upstream API key (sk-boos4721) instead of local proxy
- Simplify SSE: remove nested SSE-in-SSE parsing, stream as plain text
- Fix frontend: add Authorization header from localStorage
- Fix ComfyUI nodes: default to api.boos.lat with correct API key
- All verified: frontend streaming works end-to-end with gpt-5.5
2026-05-17 04:17:12 +08:00
1326 changed files with 161861 additions and 261602 deletions
+24
View File
@@ -0,0 +1,24 @@
# Cargo build acceleration.
#
# The biggest single speedup is the `mold` linker (~3-5x faster link
# time on debug builds). `build-and-run.sh` enables mold/lld automatically
# when available, so the block below is kept as an optional manual setup for
# direct `cargo build` / `cargo run` usage:
#
# sudo apt-get install -y mold
#
# (or build it from source: https://github.com/rui314/mold)
#
# Without mold/lld, the [profile.dev] tweaks in Cargo.toml still give a
# meaningful speedup (line-tables-only debuginfo + bumped codegen-units).
# Optional: uncomment once mold is on PATH if you want direct cargo commands
# to use it without going through build-and-run.sh.
# [target.x86_64-unknown-linux-gnu]
# linker = "clang"
# rustflags = ["-C", "link-arg=-fuse-ld=mold"]
[build]
# Default to all CPUs for parallel codegen. Override with --jobs N if
# you need to leave headroom for other work.
# jobs = 8
+20 -9
View File
@@ -1,9 +1,20 @@
node_modules comfy_source/venv
dist comfy_source/.git
data comfy_source/__pycache__
.git **/__pycache__
.env target/
*.md node_modules/
docs dist/
.github release/
*.log data/
logs/
tmp/
.agents/
.cache/
.codex/
.claude/
.comfyui/
boosapi
bossai
boosapi.db
docker/*.tar
-30
View File
@@ -1,30 +0,0 @@
# Required in production: generate a unique 32+ byte random secret in your local .env, keep it out of git, and do not reuse AUTH_TOKEN.
ACCOUNT_CREDENTIAL_SECRET=REPLACE_WITH_STRONG_RANDOM_SECRET
AUTH_TOKEN=change-me-admin-token
CHECKIN_CRON=0 8 * * *
BALANCE_REFRESH_CRON=0 * * * *
# Optional OAuth overrides. Fill the matching credentials when an override is enabled; Claude requires a client secret with no fallback.
CLAUDE_CLIENT_ID=
CLAUDE_CLIENT_SECRET=
# Codex OAuth override only needs CODEX_CLIENT_ID.
CODEX_CLIENT_ID=
# Gemini CLI override requires both GEMINI_CLI_CLIENT_ID and GEMINI_CLI_CLIENT_SECRET together.
GEMINI_CLI_CLIENT_ID=
GEMINI_CLI_CLIENT_SECRET=
PROXY_TOKEN=change-me-proxy-sk-token
SYSTEM_PROXY_URL=
WEBHOOK_URL=
BARK_URL=
SERVERCHAN_KEY=
TELEGRAM_ENABLED=false
TELEGRAM_BOT_TOKEN=
TELEGRAM_CHAT_ID=
MODEL_AVAILABILITY_PROBE_CONCURRENCY=1
MODEL_AVAILABILITY_PROBE_ENABLED=false
MODEL_AVAILABILITY_PROBE_INTERVAL_MS=1800000
MODEL_AVAILABILITY_PROBE_TIMEOUT_MS=15000
PORT=4000
DATA_DIR=./data
NOTIFY_COOLDOWN_SEC=300
ADMIN_IP_ALLOWLIST=
TZ=Asia/Shanghai
+22 -17
View File
@@ -1,28 +1,33 @@
/target
**/*.rs.bk
.env
.env.*
data/
node_modules/ node_modules/
website/.next/
website/out/
dist/ dist/
release/ release/
data/*
!data/.gitkeep
.env
*.log *.log
*.tar.gz *.tsbuildinfo
logs/ logs/
tmp/* tmp/
!tmp/.gitkeep !tmp/.gitkeep
.worktrees/
# Local desktop/debug artifacts
/.tmp-*
/main.js
/NUL
/.playwright-cli/
.DS_Store .DS_Store
Thumbs.db Thumbs.db
docker/data/ docker/data/
docker/.env docker/.env
# Local planning docs
docs/plan/
docs/plans/
.ace-tool/ .ace-tool/
.comfyui/
.claude/
.codex/
.agents/
.cache/
openspec/
boosapi
bossai
docker/*.tar
boosapi.db-shm
boosapi.db-wal
.tours/
bun.lock
+4
View File
@@ -0,0 +1,4 @@
[submodule "vendor/ComfyUI_frontend"]
path = vendor/ComfyUI_frontend
url = https://github.com/Comfy-Org/ComfyUI_frontend.git
branch = main
-1
View File
@@ -1 +0,0 @@
rebrand-video-agent-20260515-020021
-71
View File
@@ -1,71 +0,0 @@
# ComfyUI Integration for Metapi
## Repository
/root/metapi
## Mode
implement
## Files to Create
### 1. src/server/routes/api/comfyui.ts
Simple pass-through proxy to ComfyUI API at http://127.0.0.1:8188.
Follow `tasks.ts` pattern (simple route, no auth needed for now):
- `POST /api/comfyui/prompt` — forward JSON body to ComfyUI POST /prompt, return response
- `GET /api/comfyui/history` — forward to ComfyUI GET /history
- `GET /api/comfyui/history/:id` — forward to ComfyUI GET /history/{id}
- `GET /api/comfyui/queue` — forward to ComfyUI GET /queue
- `GET /api/comfyui/view` — forward to ComfyUI GET /view (image output access, preserve query params)
- `GET /api/comfyui/ws` — NOT needed (WebSocket is complex, skip for now)
Use undici `fetch` for all requests (already a dep). Handle errors gracefully.
Add CORS headers for cross-origin iframe embedding.
### 2. src/web/pages/ComfyUI.tsx
Simple iframe-based embedding page:
```tsx
import { useTheme } from '@/lib/ThemeContext';
export default function ComfyUI() {
const { theme } = useTheme();
const isDark = theme === 'dark';
return (
<div style={{ width: '100%', height: '100vh', display: 'flex', flexDirection: 'column' }}>
<iframe
src="/api/comfyui/proxy/"
style={{ width: '100%', height: '100%', border: 'none' }}
title="ComfyUI"
/>
</div>
);
}
```
Actually, since iframe needs same-origin, instead proxy the full ComfyUI HTML.
Use the API routes above and create a simple index page that embeds via /api/comfyui/proxy/.
Better approach: Add a GET /api/comfyui/ route that returns HTML page with iframe pointing to /api/comfyui/proxy/ui/, and add a proxy handler for /api/comfyui/proxy/* that forwards everything to ComfyUI.
## Files to Modify
### 3. src/server/index.ts
Add `import { comfyuiRoutes } from './routes/api/comfyui.js';`
Add `await app.register(comfyuiRoutes);` after other route registrations (line ~235 area)
### 4. src/web/App.tsx
- Add `const ComfyUI = lazy(() => import('./pages/ComfyUI.js'));` (replace ModelTester import or add alongside)
- Change route: `<Route path="/playground" element={<ComfyUI />} />` (replace ModelTester)
- Keep ModelTester import but rename or keep — simplest: just swap the component
## Constraints
- Follow existing patterns (tasks.ts for routes, App.tsx for routing)
- Don't add new npm dependencies
- Don't commit changes
- Run `npm run typecheck` at the end
## Acceptance
- npm run typecheck passes
- Server starts without errors
-44
View File
@@ -1,44 +0,0 @@
# HCW Dispatch Brief
## Goal
Implement three features in metapi: User Management (SaaS), TTS Proxy, and ComfyUI integration.
## Repository
/root/metapi
## Mode
implement
## Features
### Feature 1: User Management (SaaS)
- Add `users` table to `src/server/db/schema.ts` (id, username, email, passwordHash, role: admin|user, status, timestamps)
- Add `userId` column to `downstreamApiKeys` (nullable for backward compat)
- `src/server/services/userService.ts` — password hashing (crypto.scrypt), JWT (crypto.createHmac), user CRUD
- `src/server/contracts/userRoutePayloads.ts` — Zod schemas
- `src/server/routes/api/users.ts` — register, login, me, admin list/manage
- `src/server/routes/api/userApiKeys.ts` — user-scoped API key CRUD
- `src/server/middleware/auth.ts` — add userAuthMiddleware (JWT) + requireAdmin helper
- `src/server/services/downstreamApiKeyService.ts` — check user status for key
### Feature 2: TTS Proxy
- `src/server/routes/proxy/audio.ts` — three OpenAI-compatible endpoints: POST /v1/audio/speech, POST /v1/audio/transcriptions, POST /v1/audio/translations
- Follow `images.ts` pattern: channel selection, multipart parsing, retry, log
- `src/server/routes/proxy/router.ts` — register audioProxyRoute
### Feature 3: ComfyUI Integration
- `src/server/routes/api/comfyui.ts` — POST /api/comfyui/prompt (proxy to ComfyUI /prompt), GET /api/comfyui/history/:id, GET /api/comfyui/queue, WS /api/comfyui/ws -> proxy to ComfyUI WS
- `src/web/pages/ComfyUI.tsx` — replace ModelTester playground page, iframe embed or reverse-proxy UI
- `src/web/App.tsx` — route /playground -> ComfyUI page
## Constraints
- Only modify files listed above
- Follow existing metapi conventions (patterns from images.ts, videos.ts, auth.ts, App.tsx)
- No new npm dependencies for user auth (use crypto module)
- Generate Drizzle migration after schema changes
- Run npm run typecheck before reporting done
## Acceptance Checks
- npm run typecheck exits 0
- npm run db:generate produces migration
- Server starts without errors
@@ -1,40 +0,0 @@
# Task 1: Rebrand Metapi → BoosAPI
## Scope
- Replace ALL "Metapi"/"metapi" → "BoosAPI"/"BoosAPI" in user-facing strings
- Files: package.json, src/web/** (App.tsx, i18n.tsx, i18n.supplement.ts, index.html, docsLink.ts, About.tsx, etc.)
- Also update src/desktop/** (main.ts, runtime.ts) and src/server/** if user-facing
- Do NOT change: package name in npm (package.json "name" field), internal DB schema column names, lockfiles
- About page: specifically replace "Metapi" → "BoosAPI"
## Acceptance
- grep -ri "metapi" src/web/ shows 0 matches
- About page shows "BoosAPI" not "Metapi"
- Server still starts and compiles
# Task 2: Conversational ComfyUI Agent
## Architecture
Backend: src/server/routes/api/comfyuiAgent.ts
- SSE streaming endpoint POST /api/comfyui-agent/chat
- Accepts { messages: Array<{role, content}>, sessionId?: string }
- System prompt: AI video creation assistant for ComfyUI
- Tools: generate character prompts (front/back/left/right), generate images, scene prompts, TTS
- Uses Metapi's own LLM endpoint for reasoning + image gen API + TTS API
Frontend: src/web/pages/ComfyUIAgent.tsx
- Chat UI with message bubbles
- Streaming response display (SSE)
- Inline image display for generated character images
- Input box for user messages
- ComfyUI workflow export button
Wiring: App.tsx
- Add lazy import for ComfyUIAgent
- Route at /comfyui-agent
- Sidebar item "视频助手(对话)"
## Acceptance
- npm run typecheck passes
- Server starts without errors
- Chat endpoint accepts messages and streams responses
-1
View File
@@ -1 +0,0 @@
25.0.0
+59 -61
View File
@@ -1,74 +1,72 @@
# Metapi Engineering Rules # BoosAPI Engineering Rules
These rules apply to the whole repository unless a deeper `AGENTS.md` overrides > **AI开发**: 使用 **gpt-5.5**(`api.boos.lat/v1`,key: `sk-boos4721`)进行开发工作。
them. They are intentionally opinionated and mechanical so humans and agents can > 本项目为纯 Rust 项目,无 Node.js 运行时依赖。
make small, consistent changes without re-learning the codebase each time.
## Golden Principles ## 项目结构
- Prefer one source of truth. If a helper, contract, or workflow already owns ```
an invariant, extend it instead of creating a parallel implementation. boosapi-rs/ ← 根目录(已迁移至此)
- Fix the family, not just the symptom. When a bug comes from a repeated ├── src/
pattern, sweep adjacent paths in the same subsystem before calling the work │ ├── main.rs ← 入口
done. │ ├── lib.rs
- Keep changes narrow and reviewable. Land one coherent slice at a time and │ ├── config.rs
avoid bundling unrelated cleanup into the same patch. │ ├── error.rs
│ ├── routes/
│ │ ├── mod.rs
│ │ ├── comfyui_agent.rs ← ComfyUI Agent SSE 聊天
│ │ ├── comfyui/ ← ComfyUI 代理路由
│ │ ├── proxy/ ← OpenAI 兼容代理
│ │ ├── auth/ ← 认证
│ │ └── ...
│ ├── proxy/ ← 代理核心(Conductor/Token Router)
│ ├── services/ ← 后台服务
│ ├── db/ ← 数据库
│ └── transformers/ ← 协议转换
├── dist/web/ ← 前端构建产物(由 Rust 静态 serve)
├── migrations/ ← SQLite 迁移
├── config.toml ← 运行配置
├── Dockerfile
└── Cargo.toml
## Server Layers ## ComfyUI Agent
- `src/server/routes/**` are adapters, not owners. Route files may register ComfyUI Agent 默认走 **本地模式**:复用本服务自身的 `/v1/*` 路由,bearer 用启动时引导出来的内置 downstream key(`name='comfyui-agent' AND user_id IS NULL`,密钥格式 `sk-comfyui-{uuid_v4_hex}`,同时镜像写入 `settings.builtin_key.comfyui_agent.secret`)。
Fastify endpoints, parse request context, and delegate. They must not own
protocol conversion, retry policy, stream lifecycle, billing, or
persistence.
- If a helper is imported by anything outside one route file, it does not
belong under `src/server/routes/proxy/`.
- `src/server/proxy-core/**` owns proxy orchestration. Endpoint fallback should
flow through `executeEndpointFlow()`. Channel/session bookkeeping should flow
through `sharedSurface.ts`.
- `src/server/transformers/**` are protocol-pure. Do not import from
`src/server/routes/**`, Fastify, OAuth services, token router, or runtime
dispatch modules. If a transformer needs a shared contract, move it to a
neutral module first.
- Whole-body upstream reads in proxy orchestration should use
`readRuntimeResponseText()` instead of direct `.text()` reads.
## Platform And Routing Rules 行为契约:
- **Local(默认)**:`upstream_base` = `http://{server.host}/v1`(host 是 `0.0.0.0` / `::` 时改写为 `127.0.0.1`),bearer = 内置 key;`/api/comfyui-agent/status` 回 `mode: "local"` + `keyName: "comfyui-agent"`
- **FallbackRemote**:当 `BOOSAPI_UPSTREAM_BASE`(trim 后非空)被设置时切换到外部上游;`BOOSAPI_UPSTREAM_KEY` 缺省时仍用内置 key;status 回 `mode: "fallback_remote"` + `keyName: "external"`
- 内置 key 受保护:admin 列表对其 key 字段打码(`••••<后4位>`),删除返 403,禁用(`enabled=false`)返 403;`builtin_key.*` settings 不在 list 中暴露,PUT 返 403
- 启动期 `ensure_comfyui_agent_key` 是幂等的:缺密钥就生成、`enabled=0` 自动恢复并 `tracing::warn!`、settings 与 downstream_api_keys 双向同步
- Platform behavior must be explicit. Detection, endpoint preference, discovery 支持的结构化标记(输出由 LLM 生成、由 agent SSE 解析下发):
transport, and management capability should come from one declared capability - 【CHARACTER】 → 自动调用 gpt-image-2 生成四视图
story, not scattered `if platform === ...` branches. - 【SCENE】 → 自动生成场景图
- Thin adapters must stay honest. Do not let a platform look feature-complete - 【TTS】 → 自动调用 TTS API
through inherited defaults if the underlying upstream does not support the - 【WORKFLOW】 → 输出 ComfyUI 工作流 JSON
feature.
- Retry classification and routing health classification should share the same
failure vocabulary whenever possible.
## Database Rules ## 运行
- One schema change requires three synchronized outputs: update the Drizzle ```bash
schema, update SQLite migration history, and regenerate checked-in schema # 开发
artifacts together. cargo run --release
- Cross-dialect bootstrap and upgrade SQL must be generated from the schema
contract. Do not hand-write new MySQL/Postgres schema patches in feature
code.
- Legacy schema compatibility is temporary and spec-owned. Additive startup
shims should stay narrow and trace back to a feature compatibility spec.
## Web Rules # 环境变量
BOOSAPI_SERVER__HOST=0.0.0.0
BOOSAPI_SERVER__PORT=4000
BOOSAPI_UPSTREAM_BASE=https://api.boos.lat/v1
BOOSAPI_UPSTREAM_KEY=sk-boos4721
BOOSAPI_AUTH__AUTH_TOKEN=change-me-admin-token
```
- Pages are orchestration surfaces, not shared utility libraries. Do not import ## 前端
one top-level page from another top-level page.
- Mobile behavior should reuse existing shared primitives first:
`ResponsiveFilterPanel`, `ResponsiveBatchActionBar`, `MobileCard`,
`useIsMobile`, and `mobileLayout.ts`.
- When a page grows a second complex modal, drawer, or panel family, extract it
into a domain subfolder before adding more inline state and rendering logic.
## Guardrails 前端为 React SPA,使用 Vite 构建,产物在 `dist/web/`:
- 开发时用 Vite dev server(需 Node.js)
- 生产时由 Rust 后端直接 serve 静态文件
- Run `npm run repo:drift-check` before finishing changes that touch shared ### 登录与会话
architecture boundaries.
- If you add a new boundary-heavy module, add or extend an architecture test in - 登录页位于 `src/web/pages/auth/`:`LoginShell` 提供共享外壳,`AdminLoginPage`(token / 账密双模式)与 `UserLoginPage`(账密)分别承担两类受众。OAuth 入口用 `OAuthSlot` 渲染占位区域,**实际行为由后续 change 接入**——当前不要在这里挂任何 onClick / 跳转。
the same area so the rule becomes executable. - 会话状态统一走 `src/web/authSession.ts` 的 `commitSession / clearSession / getSessionToken`,受众通过 `'admin' | 'user'` 区分;旧的 `persistAuthSession / clearAuthSession / getAuthToken / hasValidAuthSession`(及 `*UserSession` 同族)保留为 `@deprecated` 薄壳,新代码不要再引用。
- Keep local planning files under `docs/plans/`. They are intentionally ignored - 登录错误文案统一走 `src/web/loginError.ts` 的 `resolveLoginErrorMessage(status, raw)`;新增登录错误请加在该处而不是各自分支里手写。
by git and should not be treated as published documentation.
-106
View File
@@ -1,106 +0,0 @@
# Code of Conduct / 行为准则
## Our Pledge / 我们的承诺
We as members, contributors, and maintainers pledge to make participation in the Metapi community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
作为 Metapi 社区的成员、贡献者和维护者,我们承诺让每个人都能在无骚扰的环境中参与,无论年龄、体型、残疾、种族、性别认同和表达、经验水平、国籍、个人外貌、种族、宗教或性取向如何。
## Our Standards / 我们的标准
### Expected Behavior / 期望的行为
Examples of behavior that contributes to a positive environment / 有助于营造积极环境的行为示例:
- Using welcoming and inclusive language / 使用友好和包容的语言
- Being respectful of differing viewpoints and experiences / 尊重不同的观点和经验
- Gracefully accepting constructive criticism / 优雅地接受建设性批评
- Focusing on what is best for the community / 关注对社区最有利的事情
- Showing empathy towards other community members / 对其他社区成员表现出同理心
- Providing helpful and constructive feedback / 提供有益和建设性的反馈
- Assuming good intent and asking clarifying questions / 假定善意并提出澄清性问题
### Unacceptable Behavior / 不可接受的行为
Examples of unacceptable behavior / 不可接受行为的示例:
- Harassment, discrimination, or abusive language / 骚扰、歧视或辱骂性语言
- Trolling, insulting/derogatory comments, and personal or political attacks / 挑衅、侮辱性/贬损性评论以及人身或政治攻击
- Public or private harassment or intimidation / 公开或私下的骚扰或恐吓
- Publishing others' private information (e.g., physical or email address) without explicit permission / 未经明确许可发布他人的私人信息(例如,实际地址或电子邮件地址)
- Deliberate disruption of discussions or project activities / 故意破坏讨论或项目活动
- Repeatedly ignoring maintainer moderation requests / 反复忽视维护者的管理要求
- Other conduct which could reasonably be considered inappropriate in a professional setting / 在专业环境中可能被合理认为不当的其他行为
## Reporting / 举报
If you experience or witness unacceptable behavior, or have any other concerns, please report it by:
如果您遇到或目睹不可接受的行为,或有任何其他疑虑,请通过以下方式举报:
1. **Email** / **邮件**: `cita-777@users.noreply.github.com` with subject prefix `[Metapi Conduct]` / 主题前缀为 `[Metapi Conduct]`
2. **GitHub**: Use [GitHub's built-in reporting tools](https://docs.github.com/en/communities/maintaining-your-safety-on-github/reporting-abuse-or-spam) for urgent platform-level issues / 对于紧急的平台级问题,使用 [GitHub 的内置举报工具](https://docs.github.com/en/communities/maintaining-your-safety-on-github/reporting-abuse-or-spam)
### What to Include / 应包含的内容
When reporting, please include / 举报时请包含:
- Your contact information / 您的联系信息
- Names (usernames, real names) of individuals involved / 涉及人员的姓名(用户名、真实姓名)
- Description of the incident / 事件描述
- Links to relevant issues, PRs, or discussions / 相关 issue、PR 或讨论的链接
- Screenshots or logs (with sensitive information redacted) / 截图或日志(删除敏感信息)
- Timestamps and context / 时间戳和上下文
- Any other information that would be helpful / 任何其他有用的信息
### Confidentiality / 保密性
All reports will be handled with discretion. We will keep reporter details as private as practical and do not allow retaliation for good-faith reports.
所有举报都将谨慎处理。我们将尽可能保持举报者信息的私密性,并且不允许对善意举报进行报复。
## Enforcement / 执行
Project maintainers are responsible for clarifying and enforcing this code of conduct. They have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned with this Code of Conduct.
项目维护者负责阐明和执行本行为准则。他们有权利和责任删除、编辑或拒绝不符合本行为准则的评论、提交、代码、wiki 编辑、issue 和其他贡献。
### Enforcement Actions / 执行措施
Depending on the severity and context, maintainers may take actions including / 根据严重程度和上下文,维护者可能采取的措施包括:
1. **Warning** / **警告**: A private or public warning about the behavior / 对行为进行私下或公开警告
2. **Temporary Ban** / **临时禁止**: Temporary restriction from interacting in project spaces / 暂时限制在项目空间中的互动
3. **Permanent Ban** / **永久禁止**: Permanent removal from all project spaces / 永久从所有项目空间中移除
4. **Content Removal** / **内容删除**: Editing or removing comments, issues, discussions, or PR content / 编辑或删除评论、issue、讨论或 PR 内容
5. **Thread Locking** / **话题锁定**: Locking threads to prevent further discussion / 锁定话题以防止进一步讨论
6. **Platform Escalation** / **平台升级**: Escalating serious cases to GitHub or other hosting providers / 将严重案例上报给 GitHub 或其他托管平台
### Response Timeline / 响应时间
- We aim to acknowledge conduct reports within **7 days** when possible. / 我们力求在 **7 天**内确认行为举报(如有可能)。
- We will review the available evidence before taking action. / 我们将在采取行动之前审查可用证据。
- Complex cases may require additional time for investigation. / 复杂案例可能需要额外时间进行调查。
## Scope / 适用范围
This Code of Conduct applies to all project spaces, including / 本行为准则适用于所有项目空间,包括:
- GitHub repository (issues, PRs, discussions, code reviews) / GitHub 仓库(issue、PR、讨论、代码审查)
- Documentation and wiki / 文档和 wiki
- Community forums and chat channels / 社区论坛和聊天频道
- Official social media accounts / 官方社交媒体账号
- Project events (online or offline) / 项目活动(线上或线下)
- Any other spaces where an individual is representing the Metapi project / 个人代表 Metapi 项目的任何其他空间
## Attribution / 归属
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.1.
本行为准则改编自 [Contributor Covenant](https://www.contributor-covenant.org/) 2.1 版本。
## Questions / 问题
If you have questions about this Code of Conduct, please open an issue or contact the maintainers at `cita-777@users.noreply.github.com`.
如果您对本行为准则有疑问,请开启 issue 或通过 `cita-777@users.noreply.github.com` 联系维护者。
-224
View File
@@ -1,224 +0,0 @@
# Contributing / 贡献指南
Thank you for your interest in contributing to Metapi! / 感谢您对 Metapi 项目的贡献!
Metapi is a meta-aggregation layer for AI API platforms (New API, One API, OneHub, etc.), providing unified proxy, intelligent routing, and centralized management.
Metapi 是 AI API 聚合平台(New API、One API、OneHub 等)的元聚合层,提供统一代理、智能路由和集中管理。
## Before You Start / 开始之前
- Check existing [Issues](https://github.com/cita-777/metapi/issues) and [Pull Requests](https://github.com/cita-777/metapi/pulls) to avoid duplicates. / 检查现有的 [Issues](https://github.com/cita-777/metapi/issues) 和 [Pull Requests](https://github.com/cita-777/metapi/pulls) 以避免重复。
- For major changes, open an issue first to discuss your proposal. / 对于重大更改,请先开启 issue 讨论您的提案。
- Read our [Code of Conduct](CODE_OF_CONDUCT.md). / 阅读我们的[行为准则](CODE_OF_CONDUCT.md)。
## Local Development Setup / 本地开发环境设置
### Prerequisites / 前置要求
- Node.js 20+ / Node.js 20 或更高版本
- npm or compatible package manager / npm 或兼容的包管理器
### Setup Steps / 设置步骤
1. **Fork and clone the repository** / **Fork 并克隆仓库**
```bash
git clone https://github.com/YOUR_USERNAME/metapi.git
cd boosapi
```
2. **Install dependencies** / **安装依赖**
```bash
npm install
```
3. **Create environment file** / **创建环境文件**
```bash
# Windows PowerShell
Copy-Item .env.example .env
# Linux/macOS/Git Bash
cp .env.example .env
```
Edit `.env` and set your tokens / 编辑 `.env` 并设置您的令牌:
```env
AUTH_TOKEN=your-dev-admin-token
PROXY_TOKEN=your-dev-proxy-token
```
4. **Initialize database** / **初始化数据库**
```bash
npm run db:migrate
```
5. **Start development server** / **启动开发服务器**
```bash
npm run dev
```
The app will be available at `http://localhost:4000` (backend) and `http://localhost:5173` (frontend).
应用将在 `http://localhost:4000`(后端)和 `http://localhost:5173`(前端)可用。
## Development Commands / 开发命令
### Web Application / Web 应用
```bash
npm run dev # Start backend + frontend with hot reload / 启动后端 + 前端热更新
npm run dev:server # Start backend only / 仅启动后端
npm run build # Build all (web + server + desktop) / 构建全部
npm run build:web # Build frontend only / 仅构建前端
npm run build:server # Build backend only / 仅构建后端
```
### Desktop Application / 桌面应用
```bash
npm run dev:desktop # Start desktop app in dev mode / 开发模式启动桌面应用
npm run build:desktop # Build desktop app / 构建桌面应用
npm run dist:desktop # Package desktop app / 打包桌面应用
npm run dist:desktop:mac:intel # Package for macOS Intel / 打包 macOS Intel 版本
```
### Documentation / 文档
```bash
npm run docs:dev # Start VitePress dev server / 启动 VitePress 开发服务器
npm run docs:build # Build documentation / 构建文档
npm run docs:preview # Preview built docs / 预览构建的文档
```
### Testing / 测试
```bash
npm test # Run all tests / 运行所有测试
npm run test:watch # Run tests in watch mode / 监听模式运行测试
npm run smoke:db # Database smoke test (SQLite) / 数据库冒烟测试(SQLite)
npm run smoke:db:mysql # MySQL smoke test / MySQL 冒烟测试
npm run smoke:db:postgres # PostgreSQL smoke test / PostgreSQL 冒烟测试
```
### Database / 数据库
```bash
npm run db:generate # Generate Drizzle migration files / 生成 Drizzle 迁移文件
npm run db:migrate # Run database migrations / 运行数据库迁移
npm run schema:generate # Generate schema artifacts / 生成 schema 构件
```
## Project Structure / 项目结构
```
metapi/
├── src/
│ ├── server/ # Backend (Fastify) / 后端(Fastify)
│ │ ├── routes/ # API routes / API 路由
│ │ ├── services/ # Business logic / 业务逻辑
│ │ ├── db/ # Database & ORM / 数据库与 ORM
│ │ └── middleware/ # Middleware / 中间件
│ ├── web/ # Frontend (React + Vite) / 前端(React + Vite)
│ └── desktop/ # Electron desktop app / Electron 桌面应用
├── docs/ # VitePress documentation / VitePress 文档
├── drizzle/ # Database migrations / 数据库迁移
└── scripts/ # Build & dev scripts / 构建与开发脚本
```
## Pull Request Guidelines / Pull Request 指南
### Before Submitting / 提交之前
1. **Keep PRs focused and small** / **保持 PR 专注且小巧**
- One feature or fix per PR / 每个 PR 一个功能或修复
- Split large changes into multiple PRs / 将大型更改拆分为多个 PR
2. **Write tests** / **编写测试**
- Add tests for new features / 为新功能添加测试
- Update tests for behavior changes / 为行为变更更新测试
- Ensure all tests pass: `npm test` / 确保所有测试通过:`npm test`
3. **Update documentation** / **更新文档**
- Update README if adding user-facing features / 如果添加面向用户的功能,请更新 README
- Update docs/ for configuration or API changes / 配置或 API 更改请更新 docs/
- Add JSDoc comments for new functions / 为新函数添加 JSDoc 注释
4. **Run checks** / **运行检查**
- Documentation changes: `npm run docs:build` / 文档更改:`npm run docs:build`
- Code changes: `npm test && npm run build` / 代码更改:`npm test && npm run build`
- Database changes: `npm run smoke:db` / 数据库更改:`npm run smoke:db`
- Architecture / repo drift changes: `npm run repo:drift-check` / 架构与仓库漂移检查:`npm run repo:drift-check`
5. **Follow code style** / **遵循代码风格**
- Use TypeScript for type safety / 使用 TypeScript 确保类型安全
- Follow existing code patterns / 遵循现有代码模式
- Follow repo-level engineering rules in `AGENTS.md` / 遵循仓库根目录 `AGENTS.md` 中的工程规则
- Keep functions small and focused / 保持函数小而专注
### Commit Messages / 提交信息
Use conventional commit format / 使用约定式提交格式:
```
<type>: <description>
[optional body]
```
Types / 类型:
- `feat`: New feature / 新功能
- `fix`: Bug fix / 错误修复
- `docs`: Documentation / 文档
- `refactor`: Code refactoring / 代码重构
- `test`: Tests / 测试
- `chore`: Build/tooling / 构建/工具
Examples / 示例:
```
feat: add AnyRouter platform adapter
fix: handle empty model list in dashboard
docs: update Docker deployment guide
refactor: extract route selection logic
test: add tests for checkin reward parser
chore: upgrade Vite to 6.0
```
### What Not to Commit / 不要提交的内容
- Runtime data: `data/`, `tmp/` / 运行时数据:`data/`、`tmp/`
- Environment files: `.env` (only `.env.example` is tracked) / 环境文件:`.env`(仅跟踪 `.env.example`)
- Build artifacts: `dist/`, `node_modules/` / 构建产物:`dist/`、`node_modules/`
- IDE-specific files (unless beneficial to all contributors) / IDE 特定文件(除非对所有贡献者有益)
## Platform Adapters / 平台适配器
If you're adding support for a new AI API platform / 如果您要添加对新 AI API 平台的支持:
1. Create adapter in `src/server/services/platforms/` / 在 `src/server/services/platforms/` 中创建适配器
2. Implement required interfaces: login, balance, models, proxy / 实现必需接口:登录、余额、模型、代理
3. Add platform tests / 添加平台测试
4. Update documentation with platform details / 更新文档说明平台详情
## Windows Development Notes / Windows 开发注意事项
- Use `restart.bat` to restart dev server (clears port locks) / 使用 `restart.bat` 重启开发服务器(清除端口锁定)
- Use PowerShell `Copy-Item` instead of `cp` / 使用 PowerShell 的 `Copy-Item` 而不是 `cp`
- If Node.js upgrade breaks scripts, run `npm install` again / 如果 Node.js 升级导致脚本损坏,请重新运行 `npm install`
## Getting Help / 获取帮助
- 📖 [Documentation](https://boosapi.cita777.me) / [文档](https://boosapi.cita777.me)
- 💬 [GitHub Discussions](https://github.com/cita-777/metapi/discussions) / [GitHub 讨论区](https://github.com/cita-777/metapi/discussions)
- 🐛 [Issue Tracker](https://github.com/cita-777/metapi/issues) / [Issue 跟踪](https://github.com/cita-777/metapi/issues)
## License / 许可证
By contributing, you agree that your contributions will be licensed under the [MIT License](LICENSE).
通过贡献,您同意您的贡献将根据 [MIT 许可证](LICENSE) 授权。
Generated
+4177
View File
File diff suppressed because it is too large Load Diff
+112
View File
@@ -0,0 +1,112 @@
[package]
name = "boosapi"
version = "1.3.0"
edition = "2021"
description = "AI API Gateway — Rust rewrite of BoosAPI"
[dependencies]
# Web framework
axum = { version = "0.8", features = ["macros", "ws", "multipart"] }
tower = "0.5"
tower-http = { version = "0.6", features = ["cors", "trace", "fs", "compression-gzip"] }
# Async runtime
async-stream = "0.3"
tokio = { version = "1", features = ["full"] }
tokio-util = { version = "0.7", features = ["io"] }
# Database
sqlx = { version = "0.8", features = [
"runtime-tokio",
"sqlite",
"postgres",
"any",
"migrate",
"chrono",
] }
# HTTP client
reqwest = { version = "0.12", features = ["stream", "json", "multipart"] }
lettre = { version = "0.11", default-features = false, features = ["builder", "hostname", "pool", "rustls-tls", "smtp-transport"] }
# Serialization
serde = { version = "1", features = ["derive"] }
serde_json = "1"
regex = "1"
# Auth
jsonwebtoken = "9"
argon2 = "0.5"
# Config
figment = { version = "0.10", features = ["env", "toml"] }
dotenvy = "0.15"
# Logging
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
# Error handling
thiserror = "2"
anyhow = "1"
# CLI
clap = { version = "4", features = ["derive"] }
# Time
chrono = { version = "0.4", features = ["serde"] }
# UUID
uuid = { version = "1", features = ["v4", "serde"] }
# Streaming
futures = "0.3"
tokio-tungstenite = { version = "0.26", features = ["rustls-tls-webpki-roots"] }
tokio-stream = "0.1"
bytes = "1"
# Misc
once_cell = "1"
parking_lot = "0.12"
mime = "0.3"
base64 = "0.22"
hmac = "0.12"
sha2 = "0.10"
hex = "0.4"
include_dir = "0.7"
mime_guess = "2"
[dev-dependencies]
rstest = "0.24"
wiremock = "0.6"
tracing-test = "0.2"
tempfile = "3"
axum-test = "16"
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
# Faster production-like build for staging/manual verification. This keeps
# optimizations on but avoids the slow LTO + single-codegen-unit release path.
[profile.fast-release]
inherits = "release"
opt-level = 2
lto = false
codegen-units = 256
incremental = true
# Dev-profile speedups: line-tables-only debuginfo (much smaller .debug
# sections → much faster link) and 256 codegen units (more parallelism
# during codegen, slightly worse runtime — fine for dev). Dependencies
# build with no debuginfo at all since we rarely step into them.
[profile.dev]
opt-level = 0
debug = "line-tables-only"
codegen-units = 256
incremental = true
[profile.dev.package."*"]
opt-level = 0
debug = false
+263
View File
@@ -0,0 +1,263 @@
# BoosAPI 部署与环境指南
本文档介绍如何配置和运行 BoosAPI 的开发环境与生产环境。
## 1. 快速参数参考 (CLI Args)
后端二进制文件支持以下命令行参数,优先级高于 `config.toml` 和环境变量:
- `-c, --config <PATH>`: 指定配置文件路径 (默认: `config.toml`)
- `-p, --port <PORT>`: 覆盖监听端口
- `--host <HOST>`: 覆盖监听地址 (如 `0.0.0.0`)
- `--log-level <LEVEL>`: 设置日志级别 (`trace`, `debug`, `info`, `warn`, `error`)
- `--migrate-only`: 仅运行数据库迁移后退出
- `--check-config`: 加载并验证配置后退出
- `-h, --help, -help`: 显示帮助信息
- `-V, --version, -version`: 显示版本信息
示例:
```bash
./boosapi --config prod.toml --port 8080 --log-level warn
# 仅执行迁移
./boosapi --migrate-only
```
---
## 2. 开发环境 (Development)
### 前提条件
- **Rust**: 1.85+ (推荐使用 rustup)
- **Node.js**: 20+ (仅用于前端构建)
- **SQLite**: 系统自带或手动安装
### 步骤
1. **克隆项目**:
```bash
git clone https://github.com/boos/boosapi.git
cd boosapi
```
2. **配置环境**:
复制 `config.toml.example` 并根据需要修改:
```bash
cp config.toml.example config.toml
```
或者直接设置环境变量。
3. **运行前端 (热更新模式)**:
```bash
npm install
# 启动 vite 开发服务器
npm run dev:web
```
默认访问 `http://localhost:5173`。Vite 会将 `/api` 请求代理到后端 `localhost:4000`。
4. **运行后端**:
```bash
# 使用默认配置运行
cargo run
# 推荐:使用 cargo-watch 自动重载
# cargo install cargo-watch
# cargo watch -x run
```
---
## 3. 生产环境 (Production)
### 单二进制生产包
推荐用项目脚本生成生产包:
```bash
./build-and-run.sh --production
```
生产模式会构建主前端和 Workflow UI,并把静态资产嵌入 Rust 二进制。部署目录通常只需要:
```txt
boosapi
config.toml
```
部署目录只需要运行产物和配置。仓库里的 `.agents/`、`.cache/`、本地二进制、Docker tar 包、运行日志等均为本地缓存或构建产物,不应进入发布包。`Cargo.lock` 应保留在仓库中,用于保证生产构建依赖可复现。
启动前检查配置:
```bash
./boosapi --check-config
./boosapi --migrate-only
./boosapi --config config.toml
```
### 推荐:Docker Compose 部署
1. **准备 docker-compose.yml**:
```yaml
services:
boosapi:
image: ghcr.io/boos/boosapi:latest
container_name: boosapi
ports:
- "4000:4000"
volumes:
- ./data:/app/data
- ./config.toml:/app/config.toml
environment:
- BOOSAPI_AUTH__AUTH_TOKEN=your-secure-admin-token
- BOOSAPI_AUTH__JWT_SECRET=your-secure-jwt-secret
- BOOSAPI_DB__URL=sqlite:/app/data/boosapi.db?mode=rwc
restart: unless-stopped
```
2. **启动**:
```bash
docker compose up -d
```
### 传统 Linux 部署 (Systemd + Nginx)
1. **构建前端与后端**:
```bash
# 构建前端
npm run build
# 构建后端
cargo build --release
```
2. **安装**:
将生成的二进制文件 `target/release/boosapi` 和静态资源目录 `dist/web` 拷贝到部署目录 (如 `/opt/boosapi`)。
3. **初始化数据库**:
```bash
/opt/boosapi/boosapi --migrate-only
```
4. **Systemd 配置** (`/etc/systemd/system/boosapi.service`):
```ini
[Unit]
Description=BoosAPI Service
After=network.target
[Service]
Type=simple
User=www-data
WorkingDirectory=/opt/boosapi
ExecStart=/opt/boosapi/boosapi --config /opt/boosapi/config.toml
Restart=always
Environment=BOOSAPI_SERVER__LOG_LEVEL=info
[Install]
WantedBy=multi-user.target
```
5. **Nginx 反向代理配置**:
```nginx
server {
listen 80;
server_name api.yourdomain.com;
location / {
proxy_pass http://127.0.0.1:4000;
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;
# 支持 WebSockets 与 SSE
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
proxy_cache off;
}
}
```
---
## 4. 关键配置项
| 配置项 (config.toml) | 环境变量 (ENV) | 说明 |
|-------------------|--------------|------|
| `server.host` | `BOOSAPI_SERVER__HOST` | 后端监听地址 (默认 127.0.0.1) |
| `server.port` | `BOOSAPI_SERVER__PORT` | 后端监听端口 (默认 4000) |
| `auth.auth_token` | `BOOSAPI_AUTH__AUTH_TOKEN` | 管理员登录 Token (静态,必填) |
| `auth.jwt_secret` | `BOOSAPI_AUTH__JWT_SECRET` | JWT 签名密钥 (普通用户,必填) |
| `db.url` | `BOOSAPI_DB__URL` | SQLite 数据库路径 (如 `sqlite:data/boosapi.db`) |
| `comfyui.backend` | `BOOSAPI_COMFYUI__BACKEND` | Workflow 后端:`cloud` / `runninghub` / `external` / `boosapi` / `docker` |
| `comfyui.base_url` | `BOOSAPI_COMFYUI__BASE_URL` | Comfy Cloud、RunningHub 或外部 ComfyUI API 地址 |
| `comfyui.api_key` | `BOOSAPI_COMFYUI__API_KEY` | Workflow 后端 API Key,按后端需要填写 |
| `web_dir` | `BOOSAPI_WEB_DIR` | 前端静态资源目录 (默认 `dist/web`) |
### Workflow 后端配置
BoosAPI 的 `/workflows` 页面使用内嵌 ComfyUI 前端,后端由 `[comfyui]` 选择。
Comfy Cloud:
```toml
[comfyui]
backend = "cloud"
base_url = "https://cloud.comfy.org"
api_key = "your-comfy-cloud-api-key"
```
RunningHub:
```toml
[comfyui]
backend = "runninghub"
base_url = "https://www.runninghub.cn/your-comfy-compatible-endpoint"
api_key = "your-runninghub-api-key"
```
外部完整版 ComfyUI:
```toml
[comfyui]
backend = "external"
base_url = "http://127.0.0.1:8188"
```
BoosAPI API-native 执行器:
```toml
[comfyui]
backend = "boosapi"
```
说明:
- `cloud`、`runninghub`、`external` 和 `boosapi` 模式不要求 BoosAPI 机器有 GPU。
- `docker` / 本地 ComfyUI 模式需要本机有完整模型目录和匹配的 NVIDIA GPU。
- 生产构建应包含 `dist/workflow-ui`、`dist/workflow-ui-cloud`、`dist/workflow-ui-external` 中可用的 Workflow UI 资产;脚本会尽量自动安装与构建。
---
## 5. 运维操作 (Ops)
### 检查配置
在启动前验证配置文件是否正确:
```bash
./boosapi --check-config
```
### 数据库备份
BoosAPI 使用 SQLite,只需备份数据库文件即可:
```bash
cp data/boosapi.db data/boosapi.db.bak
```
如使用 PostgreSQL,请按数据库自身的 `pg_dump` / `pg_restore` 流程备份恢复。
### 查看日志
如果使用 Systemd:
```bash
journalctl -u boosapi -f
```
如果使用 Docker:
```bash
docker logs -f boosapi
```
+14 -100
View File
@@ -1,102 +1,16 @@
FROM ubuntu:24.04 AS base FROM rust:1.83-alpine AS builder
RUN apk add --no-cache musl-dev pkgconfig openssl-dev
ENV DEBIAN_FRONTEND=noninteractive WORKDIR /app
ENV NODE_VERSION=22 COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs
# Base deps RUN cargo build --release 2>/dev/null || true
RUN apt-get update && apt-get install -y \
curl git build-essential pkg-config libssl-dev \
python3 python3-pip python3-venv \
&& rm -rf /var/lib/apt/lists/*
# Node.js
RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - \
&& apt-get install -y nodejs \
&& npm install -g npm@latest
# Rust (for RTK)
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
ENV PATH="/root/.cargo/bin:${PATH}"
# ========== RTK ==========
FROM base AS rtk-builder
WORKDIR /build
RUN git clone https://github.com/rtk-ai/rtk.git && cd rtk && cargo build --release
RUN cp /build/rtk/target/release/rtk /usr/local/bin/rtk
# ========== ComfyUI ==========
FROM base AS comfyui
WORKDIR /app/comfy
RUN git clone https://github.com/comfyanonymous/ComfyUI.git .
RUN python3 -m venv venv \
&& . venv/bin/activate \
&& pip install --upgrade pip \
&& pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu \
&& pip install -r requirements.txt \
&& pip install comfy-cli
ENV PATH="/app/comfy/venv/bin:${PATH}"
# ========== Metapi ==========
FROM base AS boosapi
WORKDIR /app/boosapi
COPY . . COPY . .
RUN npm install RUN cargo build --release
RUN npm run build --if-present
# ========== Final ========== FROM alpine:3.20
FROM ubuntu:24.04 RUN apk add --no-cache ca-certificates tzdata sqlite-libs
ENV DEBIAN_FRONTEND=noninteractive WORKDIR /app
COPY --from=builder /app/target/release/boosapi .
RUN apt-get update && apt-get install -y \ COPY --from=builder /app/migrations ./migrations
curl python3 python3-pip python3-venv nodejs \ EXPOSE 4000
ca-certificates supervisor \ CMD ["./boosapi"]
&& rm -rf /var/lib/apt/lists/*
# Copy components
COPY --from=rtk-builder /usr/local/bin/rtk /usr/local/bin/rtk
COPY --from=comfyui /app/comfy /app/comfy
COPY --from=comfyui /app/comfy/venv /app/comfy/venv
COPY --from=boosapi /app/boosapi /app/boosapi
COPY --from=boosapi /usr/lib/node_modules /usr/lib/node_modules
# Node path
ENV PATH="/app/comfy/venv/bin:/usr/local/bin:${PATH}"
ENV COMFYUI_BASE=http://127.0.0.1:8188
ENV AUTH_TOKEN=change-me-admin-token
WORKDIR /app/boosapi
# Supervisor config
RUN mkdir -p /var/log/supervisor
COPY <<'EOF' /etc/supervisor/conf.d/all.conf
[supervisord]
nodaemon=true
user=root
logfile=/var/log/supervisor/supervisord.log
pidfile=/tmp/supervisord.pid
[program:boosapi-server]
command=npm run dev:server
directory=/app/boosapi
autorestart=true
stdout_logfile=/var/log/supervisor/boosapi-server.log
stderr_logfile=/var/log/supervisor/boosapi-server.err
[program:boosapi-web]
command=npx vite --host 0.0.0.0 --port 5173
directory=/app/boosapi
autorestart=true
stdout_logfile=/var/log/supervisor/boosapi-web.log
stderr_logfile=/var/log/supervisor/boosapi-web.err
[program:comfyui]
command=python3 main.py --listen 0.0.0.0 --port 8188
directory=/app/comfy
autorestart=true
stdout_logfile=/var/log/supervisor/comfyui.log
stderr_logfile=/var/log/supervisor/comfyui.err
EOF
EXPOSE 4000 5173 8188
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/all.conf"]
-253
View File
@@ -1,253 +0,0 @@
# 模型白名单功能 - PR 准备文档
## 功能概述
实现全局模型白名单功能,允许管理员配置只启用特定模型的路由。当白名单配置后,不在白名单中的模型路由会被自动移除,实现精细化的模型访问控制。
## 业务场景
- **成本控制**: 只允许使用价格合理的模型
- **合规要求**: 限制只使用审批通过的模型
- **简化管理**: 隐藏不需要的模型,减少用户困惑
- **测试验证**: 在特定场景下只开放测试模型
## 技术实现
### 1. 配置层 (src/server/config.ts)
```typescript
globalAllowedModels: [] as string[]
```
- 默认为空数组(向后兼容)
- 存储在内存中,启动时从数据库加载
### 2. 数据持久化 (src/server/routes/api/settings.ts)
**数据库存储:**
- Key: `global_allowed_models`
- Value: JSON 字符串数组
**API 接口:**
#### GET /api/settings/runtime
返回当前白名单配置
#### PUT /api/settings/runtime
```json
{
"globalAllowedModels": ["gpt-4", "gpt-4o", "claude-3-sonnet"]
}
```
**处理流程:**
1. 验证输入(必须是字符串数组)
2. 去重、trim、过滤空值
3. 比较新旧配置
4. 更新数据库和内存配置
5. 如果配置变更,自动触发路由重建
### 3. 路由重建 (src/server/services/modelService.ts)
**核心函数:** `rebuildTokenRoutesFromAvailability()`
```typescript
// 加载白名单
const globalAllowedModels = new Set(
config.globalAllowedModels.map(m => m.toLowerCase().trim()).filter(Boolean)
);
// 判断模型是否允许
function isModelAllowedByWhitelist(modelName: string): boolean {
if (globalAllowedModels.size === 0) return true; // 向后兼容
return globalAllowedModels.has(modelName.toLowerCase().trim());
}
// 添加模型候选时过滤
const addModelCandidate = (modelNameRaw, accountId, tokenId, siteId) => {
const modelName = (modelNameRaw || '').trim();
if (!modelName) return;
if (!isModelAllowedByWhitelist(modelName)) return; // 白名单过滤
// ... 其他过滤逻辑
};
```
**处理逻辑:**
1. 从 `token_model_availability` 加载所有可用模型
2. 通过 `addModelCandidate()` 过滤,只保留白名单中的模型
3. 创建/更新路由和渠道
4. 删除不在白名单中的旧路由
### 4. 模型候选API (src/server/routes/api/stats.ts)
**接口:** GET /api/models/token-candidates
**返回字段:**
- `models`: 有token的模型
- `modelsWithoutToken`: 无token的模型
- `modelsMissingTokenGroups`: 缺少token分组的模型
**过滤逻辑:**
```typescript
if (globalAllowedModels.size > 0) {
// 白名单模式:只返回白名单中的模型
for (const [modelName, candidates] of Object.entries(result)) {
if (globalAllowedModels.has(modelName.toLowerCase().trim())) {
filteredResult[modelName] = candidates;
}
}
} else {
// 向后兼容:返回所有模型
Object.assign(filteredResult, result);
}
```
### 5. 前端UI (src/web/pages/Settings.tsx)
**新增卡片:** "全局模型白名单"
**功能特性:**
- 输入框手动添加模型名称(支持回车)
- 可用模型列表展示(点击快速添加)
- 已选模型徽章显示(绿色徽章)
- 点击 × 删除模型
- "保存模型白名单"按钮
- 保存成功后显示提示并自动刷新页面
**交互流程:**
1. 加载当前白名单配置
2. 加载可用模型列表
3. 用户添加/删除模型
4. 点击保存触发API调用
5. 后端自动重建路由
6. 前端显示成功提示
## 数据流程图
```
用户输入 → Settings API
↓
数据库存储 (global_allowed_models)
↓
内存配置更新 (config.globalAllowedModels)
↓
触发路由重建 (异步后台任务)
↓
加载 token_model_availability
↓
白名单过滤 (isModelAllowedByWhitelist)
↓
创建/更新/删除路由
↓
候选API过滤 (token-candidates)
↓
前端显示过滤后的模型列表
```
## 向后兼容性
| 场景 | 行为 |
|------|------|
| 白名单为空 | 允许所有模型(原有行为) |
| 白名单有值 | 只允许白名单中的模型 |
| 数据库无配置 | 默认为空数组 |
| API未传该字段 | 不修改现有配置 |
## 测试验证
### 功能测试清单
- [x] 白名单为空时,所有模型可见
- [x] 设置白名单后,只显示白名单模型
- [x] 大小写不敏感匹配(GPT-4 = gpt-4)
- [x] 自动trim空格
- [x] 保存后自动触发路由重建
- [x] 路由重建成功后,路由正确过滤
- [x] 候选API正确过滤三个返回字段
- [ ] 前端UI交互正常
- [ ] 输入框支持回车添加
- [ ] 可用模型列表正确显示
- [ ] 点击模型快速添加
- [ ] 删除模型功能正常
- [ ] 保存成功提示显示
### API 测试
```bash
# 查看白名单
curl -H "Authorization: Bearer test-admin-token" \
http://localhost:4000/api/settings/runtime
# 设置白名单
curl -X PUT \
-H "Authorization: Bearer test-admin-token" \
-H "Content-Type: application/json" \
-d '{"globalAllowedModels": ["gpt-4", "gpt-4o"]}' \
http://localhost:4000/api/settings/runtime
# 查看模型候选
curl -H "Authorization: Bearer test-admin-token" \
http://localhost:4000/api/models/token-candidates
```
## 文件修改列表
| 文件 | 修改内容 | 行数 |
|------|---------|-----|
| src/server/config.ts | 新增 globalAllowedModels 配置 | 1 |
| src/server/routes/api/settings.ts | 类型定义、加载、保存、触发重建 | ~80 |
| src/server/services/modelService.ts | 白名单过滤逻辑 | ~20 |
| src/server/routes/api/stats.ts | 候选API过滤 | ~30 |
| src/web/pages/Settings.tsx | 前端UI组件 | ~150 |
**总计:** 约 280 行代码新增/修改
## 性能影响
- **内存:** 新增一个 Set 数据结构,大小为白名单模型数量
- **路由重建:** 无额外开销(仅增加一个 O(1) 的 Set 查找)
- **候选API:** 增加过滤循环,复杂度 O(n),n为模型数量
**结论:** 性能影响可忽略不计
## 安全考虑
- ✅ 仅管理员可配置(需要 admin token)
- ✅ 输入验证(类型检查、trim、去重)
- ✅ 无SQL注入风险(使用 ORM)
- ✅ 配置变更自动记录日志
## 后续优化建议
1. **批量导入**: 支持从文件批量导入白名单
2. **预设模板**: 提供常用模型组合模板
3. **分组管理**: 支持多个白名单分组
4. **权限控制**: 不同用户看到不同白名单
5. **审计日志**: 记录白名单变更历史
## 已知限制
1. 白名单只支持精确匹配,不支持通配符或正则
2. 模型名称大小写不敏感,但保留原始大小写显示
3. 白名单全局生效,不支持按站点或账号单独配置
## 相关 Issue
- Issue #XXX: 模型访问控制需求
- Issue #XXX: 简化模型列表显示
## 测试环境
- Node.js: v20.x
- pnpm: 8.x
- SQLite: 3.x
- 测试服务器: http://localhost:4000
- 真实模型数据已验证
## 下一步计划
1. ✅ 完成功能开发和测试
2. ✅ 编写PR文档
3. ⏳ 等待用户前端测试验证
4. ⏳ 合并到主仓库
5. ⏳ 开始第二个功能:新站点跳转选择
+391 -42
View File
@@ -2,27 +2,30 @@
# BoosAPI # BoosAPI
**AI 聚合网关 + 内置 ComfyUI — 将分散的中转站整合为一个统一入口** **AI 聚合网关 + Workflow Studio — 将分散的中转站整合为一个统一入口**
<p> <p>
把你各处注册的 New API / One API / OneHub / DoneHub / Veloera / AnyRouter / Sub2API 等站点, 把你各处注册的 New API / One API / OneHub / DoneHub / Veloera / AnyRouter / Sub2API 等站点,
<br> <br>
汇聚成 <strong>一个 API Key、一个入口</strong>,自动发现模型、智能路由、成本最优。 汇聚成 <strong>一个 API Key、一个入口</strong>,自动发现模型、智能路由、成本最优。
<br> <br>
<strong>内置 ComfyUI 对话助手</strong>,自然语言生成图像/视频工作流,开箱即用。 <strong>Workflow Studio / 视频助手</strong> — 用内嵌 ComfyUI 前端、多 API 后端和自然语言助手组合图像、视频、语音与文本能力。
<br> <br>
<strong>SaaS 多租户架构</strong>,组织管理、成员邀请、套餐限制,开箱即用。 <strong>SaaS 多租户架构</strong>,组织管理、成员邀请、套餐限制,开箱即用。
</p> </p>
<p align="center"> <p align="center">
<img alt="Node.js" src="https://img.shields.io/badge/Node.js-22.15%2B-339933?logo=node.js&style=flat"> <img alt="Rust" src="https://img.shields.io/badge/Rust-1.85%2B-000000?logo=rust&style=flat">
<img alt="TypeScript" src="https://img.shields.io/badge/TypeScript-5.x-3178C6?logo=typescript&style=flat"> <img alt="SQLx" src="https://img.shields.io/badge/SQLx-0.8-3E6E93?style=flat">
<img alt="License" src="https://img.shields.io/badge/license-MIT-brightgreen?style=flat"> <img alt="License" src="https://img.shields.io/badge/license-MIT-brightgreen?style=flat">
<img alt="React" src="https://img.shields.io/badge/React-19-61DAFB?logo=react&style=flat">
<img alt="Axum" src="https://img.shields.io/badge/Axum-0.8-3182F6?style=flat">
</p> </p>
<p align="center"> <p align="center">
<a href="README.md"><strong>中文</strong></a> | <a href="README.md"><strong>中文</strong></a> |
<a href="README_EN.md">English</a> <a href="README_EN.md">English</a> |
<a href="DEPLOY.md">部署指南</a>
</p> </p>
</div> </div>
@@ -50,22 +53,37 @@
| 📊 余额分散各处 | **集中看板** 一目了然,余额不足自动告警 | | 📊 余额分散各处 | **集中看板** 一目了然,余额不足自动告警 |
| ✅ 每天签到领额度 | **自动签到** 定时执行,奖励自动追踪 | | ✅ 每天签到领额度 | **自动签到** 定时执行,奖励自动追踪 |
| 🤷 不知道各站有什么模型 | **自动模型发现**,上游新增模型零配置 | | 🤷 不知道各站有什么模型 | **自动模型发现**,上游新增模型零配置 |
| 🎨 想用 ComfyUI 但不想配工作流 | **对话助手**,自然语言描述即可生成图像/视频 | | 🎨 想编排图像/视频流程但不想跑本地模型 | **Workflow Studio + 对话助手**,直接复用 BoosAPI 模型路由 |
| 👥 多人协作管理中转站 | **组织管理**,成员邀请,角色权限控制 | | 👥 多人协作管理中转站 | **组织管理**,成员邀请,角色权限控制 |
| 📋 不同团队需要不同配额 | **套餐系统**,Free/Pro/Enterprise 三级限制 | | 📋 不同团队需要不同配额 | **套餐系统**,Free/Pro/Enterprise 三级限制 |
--- ## 2026-06 更新重点
- **后台登录收口**:管理员后台只允许使用 `BOOSAPI_AUTH__AUTH_TOKEN` 这类 API Token 登录;普通用户仍使用账号密码登录用户工作台。
- **日志边界清晰**:管理员「使用日志」保留在 `/logs`,系统/程序日志保留在 `/events`;`/api/events` 现在只接受管理员 API Token,普通用户不会再看到运行日志。
- **通知隔离**:普通用户顶部通知只读取用户公告;管理员通知面板继续聚合签到、余额、令牌、代理、状态与公告事件。
- **后台信息架构**:侧栏按接入管理、创作应用、系统管理、我的账户精简;外部监控入口已移除,系统日志入口保留在系统管理分组。
- **运维与 IP 管理**:新增运维监控视图与 IP 管理页面,支持代理质量检测、连接测试、批量操作与更清晰的状态展示。
- **用户自助工作台**:API Key、计费、账户充值、使用记录、工作空间和模型测试统一到用户侧;工作空间支持分页、一键选择和默认预览。
- **Comfy Checkout 充值**:用户先在系统内创建充值记录,再跳转到配置的 Comfy Checkout 链接付款;充值记录会保留用于后续对账。
- **Workflow UI 更新**:`npm run workflow-ui:install:cloud` 会安装当前 Comfy Cloud 前端资产并写入构建元数据,Rust 服务会对 JS/CSS 走 no-cache,避免继续显示旧版本。
- **本地化时间**:前端展示时间优先使用浏览器/UA 时区,后端无时区时间按 UTC 解析后再转成本地显示。
## ✨ 核心功能 ## ✨ 核心功能
### 🎨 内置 ComfyUI 对话助手 ### 🎨 Workflow Studio 与视频助手(Rust)
- **自然语言生成工作流**:描述你想要的效果,AI 自动构建 ComfyUI 工作流 - **自然语言描述脚本**:说想法 → AI 自动提取角色、场景、对白
- **定向视图生成**:前/后/左/右 多角度图像生成 - **自动生成四视图**:每个角色正/背/左/右 4 角度,调用 gpt-image-2 生成图片
- **场景/视频生成**:文字描述直接生成视频内容 - **场景生成**:自动为每个场景生成配图
- **SSE 实时流式响应**:生成进度实时反馈 - **TTS 语音合成**:对话台词自动转语音
- 侧边栏「对话助手」入口,与聚合网关同一界面 - **Workflow Studio**:内嵌 ComfyUI 前端,可切换 Comfy Cloud、RunningHub、外部 ComfyUI、BoosAPI API-native 执行器或本地 Docker 完整版 ComfyUI
- 自动对接 ComfyUI API(`:8188`),无需额外配置 - **远程执行优先**:使用 Comfy Cloud、RunningHub、外部 API 或 BoosAPI 上游路由时,BoosAPI 服务器不需要 GPU
- **外部 ComfyUI 可选代理**:已有独立 ComfyUI 服务时,可通过 `comfyui.base_url` 接入 `/api/comfyui/proxy/*`
- **Rust 后端**(boosapi-rs):高性能 SSE 流式响应,零 Node.js 运行时依赖
- 侧边栏「视频助手」入口,与聚合网关同一界面
- **默认走本地模式**:复用本服务自身的 `/v1/*` 路由 + 启动时引导出来的内置 downstream key(`comfyui-agent`),开箱即用,无需额外配置
- **回退远端**:设置 `BOOSAPI_UPSTREAM_BASE`(可选 `BOOSAPI_UPSTREAM_KEY`)即切换为外部上游;`/api/comfyui-agent/status` 的 `mode` 字段会反映当前模式
### 🌐 统一代理网关 ### 🌐 统一代理网关
@@ -148,6 +166,103 @@
--- ---
## 🖥️ 硬件配置建议
BoosAPI 本身是 Rust 网关与管理后台,不在本地跑大模型推理时不需要 GPU。实际硬件主要取决于你是否把图像/视频生成放在本机 ComfyUI 上执行。
| 使用模式 | CPU | 内存 | 磁盘 | GPU |
|----------|-----|------|------|-----|
| 仅网关 / 管理后台 / `/v1/*` 代理 | 2 核起步,4 核推荐 | 2GB 起步,4GB 推荐 | 10GB+ | 不需要 |
| BoosAPI + Workflow UI + 远程模型 API / Comfy Cloud / RunningHub | 4 核推荐 | 4GB 起步,8GB 推荐 | 20-50GB | 不需要 |
| 外部独立 ComfyUI 代理 | 4 核推荐 | 8GB 推荐 | 50GB+ | BoosAPI 机器不需要;ComfyUI 机器按模型需求配置 |
| 本机 Docker / 本地 ComfyUI 轻量文生图 | 4-8 核 | 16GB+ | 100GB+ | NVIDIA 8GB VRAM 勉强,12GB+ 推荐 |
| 本机视频生成 / 数字人 / 大工作流 | 8 核+ | 32GB 起步,64GB 推荐 | 200GB-1TB | NVIDIA 24GB VRAM 起步,48GB+ 更稳 |
推荐的 SaaS 网关部署规格:
```txt
4 核 CPU
8GB 内存
50GB SSD
无需 GPU
```
如果图像、视频、语音能力走 BoosAPI 上游路由、Comfy Cloud、RunningHub 或其他远程 API,本机只负责网关、数据库、Workflow UI 代理、日志和 Workspace 归档,上面这档配置通常足够。只有把 ComfyUI 模型和视频生成放在本机运行时,才需要高显存 NVIDIA GPU。
## 🎛️ Workflow API 后端模式
BoosAPI 的 Workflow 使用内嵌 ComfyUI 前端,后端可按配置切换:
| backend | 用途 |
|---------|------|
| `cloud` | 代理 Comfy Cloud API,适合默认创作、官方订阅模型和稳定执行 |
| `runninghub` | 代理 RunningHub 的 ComfyUI 兼容 API,适合使用 RunningHub 专属插件、工作流和模型 |
| `external` | 代理你自己部署的完整版 ComfyUI |
| `boosapi` | 使用 BoosAPI 自带 Rust Workflow/API 节点执行器 |
| `docker` | 本地完整版 ComfyUI 模式,适合有本地 GPU 和完整模型目录的环境 |
推荐配置是默认 `cloud`,需要 RunningHub 专属插件或模型时切到 `runninghub`。RunningHub 模式会复用 external 兼容前端,并对 `/object_info`、模型库、`/system_stats` 做短期缓存,减少每次打开工作流时的卡顿。
配置示例:
```toml
[comfyui]
backend = "cloud"
base_url = "https://cloud.comfy.org"
api_key = "your-comfy-cloud-api-key"
```
```toml
[comfyui]
backend = "runninghub"
base_url = "https://www.runninghub.cn/your-comfy-compatible-endpoint"
api_key = "your-runninghub-api-key"
```
```toml
[comfyui]
backend = "external"
base_url = "http://127.0.0.1:8188"
```
```toml
[comfyui]
backend = "boosapi"
```
### 将 Comfy 视频节点包装为 BoosAPI `/v1/videos`
ComfyUI/Comfy Cloud 没有 OpenAI 兼容 `/v1/videos`,但 BoosAPI 可以把一个已保存的视频工作流模板包装成统一视频模型:
1. 在工作流页面保存一个真实 Comfy 视频模板,模板需要能被 `POST /prompt` 执行。
2. 模板图里最好保存节点映射:
- `prompt`:提示词节点和字段
- `negativePrompt`:反向提示词节点和字段,可选
- `ratio` / `resolution` / `duration`:画幅、分辨率和时长节点字段,可选
3. 在模型路由里新增一个模型,例如 `comfy-wan-video`。
4. 让该路由的上游地址或实际模型映射指向模板:
- 上游地址:`comfy://<workflow_public_id>`
- 或模型映射:`comfy:<workflow_public_id>`
之后用户可以直接调用:
```bash
curl http://127.0.0.1:4000/v1/videos \
-H "Authorization: Bearer <BoosAPI downstream key>" \
-H "Content-Type: application/json" \
-d '{
"model": "comfy-wan-video",
"prompt": "cinematic product video, smooth camera movement",
"duration": 5,
"ratio": "9:16",
"resolution": "1080p"
}'
```
返回的 `id` 是 Comfy `prompt_id`,可用 `GET /v1/videos/{id}?model=comfy-wan-video` 查询 Comfy history。
> Workflow UI 有 cloud 与 external/runninghub 两套前端资产。开发环境可用 `npm run workflow-ui:install:cloud`、`npm run workflow-ui:install:external` 单独安装;安装脚本会写入 `boosapi-workflow-ui.json` 构建元数据,生产构建会把可用资产嵌入 Rust 二进制。若浏览器「关于」仍显示旧前端版本,请重启 Rust 服务并强刷浏览器缓存。
## 🚀 快速开始 ## 🚀 快速开始
### Docker Compose(推荐) ### Docker Compose(推荐)
@@ -158,66 +273,300 @@ mkdir boosapi && cd boosapi
cat > docker-compose.yml << 'EOF' cat > docker-compose.yml << 'EOF'
services: services:
boosapi: boosapi:
image: 1467078763/boosapi:latest image: ghcr.io/boos/boosapi:latest
ports: ports:
- "4000:4000" - "4000:4000"
volumes: volumes:
- ./data:/app/data - ./data:/app/data
environment: environment:
AUTH_TOKEN: ${AUTH_TOKEN:?AUTH_TOKEN is required} BOOSAPI_AUTH__AUTH_TOKEN: ${AUTH_TOKEN?required}
PROXY_TOKEN: ${PROXY_TOKEN:?PROXY_TOKEN is required} BOOSAPI_AUTH__PROXY_TOKEN: ${PROXY_TOKEN?required}
CHECKIN_CRON: "0 8 * * *" BOOSAPI_UPSTREAM_BASE: https://api.boos.lat/v1
BALANCE_REFRESH_CRON: "0 * * * *" BOOSAPI_UPSTREAM_KEY: ${UPSTREAM_KEY}
PORT: ${PORT:-4000} BOOSAPI_SERVER__PORT: "4000"
DATA_DIR: /app/data
TZ: ${TZ:-Asia/Shanghai}
restart: unless-stopped restart: unless-stopped
EOF EOF
# AUTH_TOKEN = 管理后台登录令牌 # AUTH_TOKEN = 管理后台登录令牌(管理员只支持 API Token 登录)
# PROXY_TOKEN = 下游客户端调用 /v1/* 的 Token # PROXY_TOKEN = 下游客户端调用 /v1/* 的 Token
export AUTH_TOKEN=your-admin-token export AUTH_TOKEN=your-admin-token
export PROXY_TOKEN=your-proxy-sk-token export PROXY_TOKEN=sk-your-proxy-token
docker compose up -d docker compose up -d
``` ```
启动后访问 `http://localhost:4000`,用 `AUTH_TOKEN` 登录即可。 启动后访问 `http://localhost:4000/boos`,用 `AUTH_TOKEN` 登录管理员后台;普通用户从用户登录页使用账号密码进入用户工作台。
> [!IMPORTANT] > [!IMPORTANT]
> 请务必修改 `AUTH_TOKEN` 和 `PROXY_TOKEN`,不要使用默认值。 > 请务必修改 `AUTH_TOKEN` 和 `PROXY_TOKEN`,不要使用默认值。
--- ---
## 📁 项目结构
```
.
├── Cargo.toml # Rust 项目定义
├── config.toml # 默认配置文件
├── Dockerfile # 多阶段构建
├── docker/
│ └── docker-compose.yml
├── src/
│ ├── main.rs # 入口
│ ├── lib.rs # 库入口
│ ├── config.rs # 配置加载(环境变量 + config.toml)
│ ├── error.rs # 统一错误处理
│ ├── db/ # 数据库模型与查询
│ ├── routes/
│ │ ├── mod.rs # 路由组装 + 静态文件 serve
│ │ ├── comfyui_agent.rs # 视频/工作流助手 SSE 聊天
│ │ ├── comfyui/ # 外部 ComfyUI 代理(prompt/history/view)
│ │ ├── proxy/ # OpenAI 兼容代理入口
│ │ ├── auth.rs # 用户注册/登录 JWT
│ │ ├── checkin.rs # 自动签到
│ │ └── ...
│ ├── proxy/ # 代理引擎核心
│ │ ├── conductor/ # Proxy Conductor(故障转移)
│ │ ├── token_router/ # Token 路由引擎
│ │ └── executors/ # 执行器(chat/image/embedding)
│ ├── transformers/ # 协议转换
│ │ ├── openai/ # OpenAI Compatible
│ │ ├── anthropic/ # Claude Messages
│ │ └── gemini/ # Gemini GenerateContent
│ └── services/ # 后台服务
│ ├── cron/ # 定时任务
│ ├── billing/ # 计费逻辑
│ └── notification/ # 通知推送
├── migrations/ # SQLite 迁移
├── src/web/ # 管理台前端源码(React + Vite)
│ ├── authSession.ts # 统一会话工具:commitSession/clearSession/getSessionToken(按 'admin' | 'user' 分桶)
│ ├── loginError.ts # 登录错误归一化(resolveLoginErrorMessage)
│ ├── pages/
│ │ ├── auth/ # 登录页拆分:LoginShell + AdminLoginPage(token-only) + UserLoginPage(OAuthSlot 占位)
│ │ ├── ComfyUIAgent.tsx # 视频/工作流助手聊天界面
│ │ ├── Workflows.tsx # Workflow Studio 入口
│ │ └── ...
│ └── components/
├── website/ # 独立官网(Next.js 16 App Router + React 19)
└── dist/web/ # 前端构建产物(由 Rust serve)
```
## 🔧 环境变量
所有配置通过环境变量覆盖,命名规则:`BOOSAPI_<SECTION>__<KEY>`(双下划线分隔层级):
```bash
# 服务
BOOSAPI_SERVER__HOST=0.0.0.0
BOOSAPI_SERVER__PORT=4000
BOOSAPI_SERVER__LOG_LEVEL=info
# 认证
BOOSAPI_AUTH__AUTH_TOKEN=your-admin-token
BOOSAPI_AUTH__PROXY_TOKEN=sk-your-proxy-token
BOOSAPI_AUTH__JWT_SECRET=your-jwt-secret
# 数据库(默认 SQLite)
BOOSAPI_DB__URL=sqlite:data/boosapi.db?mode=rwc
# PostgreSQL 准备/迁移
# 当前版本保留 SQLite 作为运行主路径;可先把 SQLite 核心数据迁移到 PostgreSQL。
# 完整 PostgreSQL 业务查询适配需要后续切换运行时 SQL 方言。
# 示例:
# boosapi migrate-sqlite-to-postgres \
# --sqlite-url 'sqlite:data/boosapi.db?mode=rwc' \
# --postgres-url 'postgres://boosapi:password@127.0.0.1:5432/boosapi'
# 目标库已有数据时默认拒绝;确认覆盖时追加 --force-truncate。
# 上游 API(视频/工作流助手回退用,默认本地模式时不需要设置)
# 设置 BOOSAPI_UPSTREAM_BASE 后切换到 fallback-remote 模式;BOOSAPI_UPSTREAM_KEY 缺省时仍用内置 comfyui-agent key
BOOSAPI_UPSTREAM_BASE=https://api.boos.lat/v1
BOOSAPI_UPSTREAM_KEY=sk-your-key
# 前端静态目录(默认 dist/web)
BOOSAPI_WEB_DIR=dist/web
# Workflow 后端
BOOSAPI_COMFYUI__BACKEND=cloud
BOOSAPI_COMFYUI__BASE_URL=https://cloud.comfy.org
BOOSAPI_COMFYUI__API_KEY=your-workflow-backend-key
```
## 🏗️ 技术栈 ## 🏗️ 技术栈
| 层 | 技术 | | 层 | 技术 |
|----|------| |----|------|
| 后端框架 | Fastify | | 后端框架 | **Axum 0.8**(Rust) |
| 前端框架 | React 18 + Vite | | 运行时 | **Tokio**(异步运行时) |
| 语言 | TypeScript | | 数据库 | **SQLite** 运行主路径 + **PostgreSQL** schema/迁移准备(SQLx) |
| 并行计算 | **Rayon**(CPU 密集任务) |
| 认证鉴权 | **JWT** + API Key 双模式 |
| 代理引擎 | 自定义 Token Router + Proxy Conductor |
| 协议转换 | OpenAI / Claude / Gemini 全兼容 |
| Workflow Studio | 内嵌 ComfyUI 前端 + Cloud / RunningHub / External / BoosAPI 多后端 |
| 视频助手 | SSE 流式 + gpt-image-2 + TTS 自动生成 |
| 管理台前端 | React + Vite(仅构建工具) |
| 独立官网 | Next.js 16 App Router + React 19 + TypeScript |
| 样式 | Tailwind CSS v4 | | 样式 | Tailwind CSS v4 |
| 数据库 | SQLite / MySQL / PostgreSQL + Drizzle ORM |
| 数据可视化 | VChart | | 数据可视化 | VChart |
| 定时任务 | node-cron | | 容器化 | Docker + Docker Compose + K3s |
| 容器化 | Docker + Docker Compose | | 构建工具 | Cargo(后端)/ npm(前端静态构建) |
| 测试 | Vitest |
> **运行时零 Node.js 依赖**,Node.js 仅用作前端构建工具。生产环境只需 Rust 编译后的单二进制文件。
> 独立官网位于 `website/`。默认构建会把官网静态导出到 `dist/website/`,Rust 后端会让根路径 `/` 显示官网;管理台仍由 `dist/web/` 提供,管理员入口是 `/boos`,登录后进入 `/dashboard`。
--- ---
## 🛠️ 本地开发 ## 🛠️ 本地开发
```bash ### 后端(Rust)
npm install
npm run db:migrate
npm run dev # 前后端热更新
```
```bash ```bash
npm run build # 构建前端 + 后端 # 最快验证:只检查 Rust 后端,不构建前端、不启动服务
npm run build:web # 仅构建前端 ./build-and-run.sh --check
npm run build:server # 仅构建后端
npm test # 运行全部测试 # 日常启动:复用前端缓存,后端 dev profile 编译后启动
npm run db:generate # 生成迁移文件 ./build-and-run.sh
# 只重启/编译后端,不碰 Vite/Next
./build-and-run.sh --backend-only
# 接近生产性能但比 release 快
./build-and-run.sh --fast-release
# 完整生产包:构建并嵌入前端/Workflow UI,只输出 boosapi + config.toml
./build-and-run.sh --production
# 运行测试
cargo test
# 环境变量配置
export BOOSAPI_SERVER__HOST=0.0.0.0
export BOOSAPI_SERVER__PORT=4000
export BOOSAPI_AUTH__AUTH_TOKEN=your-admin-token
export BOOSAPI_AUTH__PROXY_TOKEN=sk-your-proxy-token
export BOOSAPI_UPSTREAM_BASE=https://api.boos.lat/v1
export BOOSAPI_UPSTREAM_KEY=sk-your-upstream-key
```
构建加速说明:
- 脚本会在 Linux 下自动使用 `mold` / `lld` 快速链接器;可用 `BOOSAPI_FAST_LINKER=off` 关闭。
- `BOOSAPI_CARGO_JOBS=8 ./build-and-run.sh --check` 可限制 Cargo 并发,避免把开发机打满。
- 生产性能验证优先用 `--fast-release`,真正发布再用 `--production` 或 `--release`。
### 前端(React SPA)
前端使用 Vite 构建,产物由 Rust 后端静态 serve:
```bash
# 安装依赖(首次)
npm install
# 开发模式(热更新,需要 Node.js)
cd src/web && npx vite --host 0.0.0.0
# 类型检查与构建生产版本
npm run typecheck:web
npm run build:web
# 输出到 dist/web/,Rust 后端自动 serve
```
Workflow UI 前端资产单独更新:
```bash
# Comfy Cloud 前端
npm run workflow-ui:install:cloud
# External / RunningHub 前端
npm run workflow-ui:install:external
```
可选使用 Bun 加速主前端构建:
```bash
# 一次性使用 Bun 构建主前端
./build-and-run.sh --bun
# 或设置环境变量,auto 会优先使用可用的 bun,缺省回退 npm/npx
BOOSAPI_FRONTEND_PM=bun ./build-and-run.sh --no-run
BOOSAPI_FRONTEND_PM=auto ./build-and-run.sh
```
> Workflow UI 子模块仍按官方 `pnpm-lock.yaml` 使用 pnpm 构建,避免 ComfyUI 前端依赖兼容问题。
> 生产环境不需要 Node.js 进程。`npm run build` 后,Rust 后端直接 serve 静态文件。
> 前端 API 请求通过 Vite 代理(开发)或同域访问(生产)指向 `localhost:4000`。
### 独立官网(Next.js)
官网已经从管理台中隔离,使用 Next.js 16 App Router、React 19、TypeScript、Tailwind CSS 4 和自定义 SVG/CSS motion system:
```bash
# 首次安装
npm --prefix website install
# 本地开发官网
npm run website:dev
# 类型检查与生产构建
npm run website:typecheck
npm run website:build
```
`./build-and-run.sh` 会同时构建管理台 `dist/web/` 和官网 `dist/website/`。打开主域名 `/` 显示官网;后台路径 `/boos`、`/dashboard`、`/user/*`、`/api/*`、`/v1/*` 等仍由 BoosAPI Rust 服务处理。
### 生产单二进制构建
```bash
./build-and-run.sh --production
```
生产模式会构建主前端与 Workflow UI 资产,并将它们嵌入 Rust 二进制。部署目录通常只需要:
```txt
boosapi
config.toml
```
仓库卫生说明:
- `Cargo.lock` 应随代码提交,保证 Rust 应用在 CI、生产构建和本地排障时依赖版本一致。
- `boosapi.db`、`config.toml` 是本地运行态文件;实际部署时不要提交带真实密钥、Comfy Cloud Key 或生产数据的改动。
- `.agents/`、`.cache/`、`boosapi` / `bossai` 本地二进制、`docker/*.tar` 等构建缓存与大文件已加入忽略规则。
- 旧的 `boosapi-comfyui-multi-tenant-*.tar` 不再作为当前 Workflow 路线的必需产物,生产包应优先使用单二进制 + 配置文件。
### Docker 部署
```bash
# 构建
docker build -t boosapi .
# 运行
docker run -d \
-p 4000:4000 \
-e BOOSAPI_AUTH__AUTH_TOKEN=your-admin-token \
-e BOOSAPI_AUTH__PROXY_TOKEN=sk-your-proxy-token \
-v $(pwd)/data:/app/data \
boosapi
```
### Docker Compose
```yaml
services:
boosapi:
build: .
ports:
- "4000:4000"
volumes:
- ./data:/app/data
environment:
BOOSAPI_AUTH__AUTH_TOKEN: ${AUTH_TOKEN?required}
BOOSAPI_AUTH__PROXY_TOKEN: ${PROXY_TOKEN?required}
BOOSAPI_UPSTREAM_BASE: https://api.boos.lat/v1
BOOSAPI_UPSTREAM_KEY: sk-your-upstream-key
restart: unless-stopped
``` ```
--- ---
+140 -151
View File
@@ -2,206 +2,195 @@
# BoosAPI # BoosAPI
**AI Aggregation Gateway + Built-in ComfyUI — Unify scattered relay stations into one entry point** **AI Aggregation Gateway + Workflow Studio — one endpoint for many AI relay stations**
<p> <p>
Aggregate your New API / One API / OneHub / DoneHub / Veloera / AnyRouter / Sub2API stations Aggregate New API / One API / OneHub / DoneHub / Veloera / AnyRouter / Sub2API stations
<br> <br>
into <strong>one API Key, one endpoint</strong> — with auto model discovery, smart routing, and cost optimization. into <strong>one API key and one compatible endpoint</strong>, with model discovery, smart routing, logs, billing, and failover.
<br> <br>
<strong>Built-in ComfyUI chat assistant</strong> — generate images/videos from natural language, ready out of the box. Workflow Studio embeds the ComfyUI frontend and can run against Comfy Cloud, RunningHub, your own ComfyUI, or BoosAPI's API-native executor.
<br>
<strong>SaaS multi-tenant architecture</strong> — organization management, member invitations, plan limits.
</p> </p>
<p align="center"> <p align="center">
<img alt="Node.js" src="https://img.shields.io/badge/Node.js-22.15%2B-339933?logo=node.js&style=flat"> <img alt="Rust" src="https://img.shields.io/badge/Rust-1.85%2B-000000?logo=rust&style=flat">
<img alt="TypeScript" src="https://img.shields.io/badge/TypeScript-5.x-3178C6?logo=typescript&style=flat"> <img alt="Axum" src="https://img.shields.io/badge/Axum-0.8-3182F6?style=flat">
<img alt="SQLx" src="https://img.shields.io/badge/SQLx-0.8-3E6E93?style=flat">
<img alt="React" src="https://img.shields.io/badge/React-19-61DAFB?logo=react&style=flat">
<img alt="License" src="https://img.shields.io/badge/license-MIT-brightgreen?style=flat"> <img alt="License" src="https://img.shields.io/badge/license-MIT-brightgreen?style=flat">
</p> </p>
<p align="center"> <p align="center">
<a href="README.md">中文</a> | <a href="README.md">中文</a> |
<a href="README_EN.md"><strong>English</strong></a> <a href="README_EN.md"><strong>English</strong></a> |
<a href="DEPLOY.md">Deployment</a>
</p> </p>
</div> </div>
--- ---
## 📖 Introduction ## Introduction
With the growing ecosystem of AI relay stations (New API / One API based), managing balances, models, and API keys across multiple stations is tedious and time-consuming. BoosAPI is a self-hosted Rust gateway for AI relay stations. It centralizes upstream sites, accounts, downstream API keys, model routes, usage logs, image/video generation, user dashboards, and Workflow Studio in one service.
**BoosAPI** acts as a **meta-aggregation layer** above these stations, unifying them under **a single entry point** — all downstream tools connect seamlessly without configuration changes. Supported upstreams:
### Supported Upstreams - **Relay panels**: New API, One API, OneHub, DoneHub, Veloera, AnyRouter, Sub2API
- **Compatible APIs**: OpenAI / Claude / Gemini compatible, cliproxyapi / CPA
- **Preset providers**: Aliyun, Zhipu, Doubao Coding Plan, DeepSeek, Moonshot, MiniMax, ModelScope
- **OAuth connections**: Codex, Claude, Gemini CLI, Antigravity
- **Panels**: New API, One API, OneHub, DoneHub, Veloera, AnyRouter, Sub2API ## Key Features
- **Generic**: OpenAI / Claude / Gemini compatible, cliproxyapi / CPA
- **Official**: Aliyun, Zhipu, Doubao Coding Plan, DeepSeek, Moonshot, MiniMax, ModelScope
- **OAuth**: Codex, Claude, Gemini CLI, Antigravity
| Pain Point | BoosAPI Solution | ### Unified API Gateway
|------------|-----------------|
| 🔑 One key per station, messy configs | **Unified proxy entry + multi-key strategy** |
| 💸 Which station is cheapest? | **Smart routing** by cost, balance, usage |
| 🔄 Manual failover | **Auto failover** with cooldown and retry |
| 📊 Scattered balances | **Central dashboard** with alerts |
| ✅ Daily check-ins | **Auto check-in** with reward tracking |
| 🤷 What models are available? | **Auto model discovery**, zero config |
| 🎨 ComfyUI workflows too complex? | **Chat assistant**, describe in natural language |
| 👥 Need multi-user access? | **Organizations**, member invites, role-based access |
| 📋 Different teams need quotas? | **Plan system**, Free/Pro/Enterprise tiers |
--- - OpenAI-compatible `/v1/chat/completions`, `/v1/responses`, `/v1/images/generations`, `/v1/models`, and files endpoints
- Claude-compatible `/v1/messages`, including request/response and streaming event conversion
- Smart token routing, channel cooldown, failover, model discovery, and usage accounting
- Admin and user dashboards with API key management and model visibility controls
## ✨ Features ### Workflow Studio
### 🎨 Built-in ComfyUI Chat Assistant Workflow Studio uses an embedded ComfyUI frontend. The execution backend is selected by `comfyui.backend`:
- **Natural language to workflow**: describe what you want, AI builds the ComfyUI pipeline | backend | Purpose |
- **Multi-view generation**: front/back/left/right directional image generation |---------|---------|
- **Scene/video generation**: text description directly generates video content | `cloud` | Proxy Comfy Cloud API for cloud execution and subscription models |
- **SSE real-time streaming**: generation progress feedback | `runninghub` | Proxy RunningHub's ComfyUI-compatible API for its plugins, models, and workflows |
- Sidebar "Chat Assistant" entry, same UI as the aggregation gateway | `external` | Proxy a full ComfyUI instance you deploy yourself |
- Auto-connects to ComfyUI API (`:8188`), zero configuration required | `boosapi` | Use BoosAPI's Rust API-native workflow executor and configured model routes |
| `docker` | Local full ComfyUI mode for GPU machines and local model directories |
### 🌐 Unified Proxy Gateway Cloud, RunningHub, external API, and BoosAPI executor modes do not require a GPU on the BoosAPI server. A GPU is only needed when you run local ComfyUI inference on the same machine.
- Compatible with **OpenAI** and **Claude** downstream formats ### Assistants
- Responses / Chat Completions / Messages / Completions / Embeddings / Images / Models / Files
- SSE streaming with automatic format conversion
### 🧠 Smart Routing Engine - Video / workflow assistant with Markdown chat, reasoning sections, attachments, and model route selection
- Picture assistant using `/v1/images/generations`, background tasks, progress display, cancellation, and Workspace archive
- Volcengine video task templates for create-task and query-task flows
- Auto-discovers models from all upstream stations ## Hardware Guide
- Four-tier cost signals: measured → configured → catalog → fallback
- Multi-channel weighted distribution (cost 40%, balance 30%, usage 30%)
- Auto cooldown on failure (default 10 min), seamless channel switching
### 📡 Multi-Platform Management | Mode | CPU | Memory | Disk | GPU |
|------|-----|--------|------|-----|
| Gateway, admin UI, `/v1/*` proxy only | 2 cores minimum, 4 recommended | 2GB minimum, 4GB recommended | 10GB+ | Not required |
| BoosAPI + Workflow UI + remote APIs / Comfy Cloud / RunningHub | 4 cores recommended | 4GB minimum, 8GB recommended | 20-50GB | Not required |
| External ComfyUI proxy | 4 cores recommended | 8GB recommended | 50GB+ | Not on BoosAPI; required by the ComfyUI machine as needed |
| Local Docker / local ComfyUI image generation | 4-8 cores | 16GB+ | 100GB+ | NVIDIA 8GB VRAM minimum, 12GB+ recommended |
| Local video / digital human / large workflows | 8 cores+ | 32GB minimum, 64GB recommended | 200GB-1TB | NVIDIA 24GB VRAM minimum, 48GB+ recommended |
| Platform | Adapter | Recommended SaaS gateway server:
|----------|---------|
| New API | `new-api` |
| One API | `one-api` |
| OneHub | `onehub` |
| DoneHub | `done-hub` |
| Veloera | `veloera` |
| AnyRouter | `anyrouter` |
| Sub2API | `sub2api` |
### 👥 Account & Token Management ```txt
4 CPU cores
- Multi-station, multi-account with health tracking (healthy / unhealthy / degraded / disabled) 8GB RAM
- Encrypted credential storage, auto-renewal, cascading disable 50GB SSD
No GPU required
### 💰 Balance Management
- Scheduled refresh (default hourly), income/expense trends
- Fallback estimation when API is unavailable
### 👥 SaaS Multi-Tenant
- **Organizations**: Create orgs, each with independent members and quotas
- **Member Invitations**: Email-based invites with admin/member roles
- **Three Plans**: Free (3 members) / Pro (10) / Enterprise (100)
- **Role Control**: Owner / Admin / Member with granular permissions
- **Plan Enforcement**: Auto-limit members, API keys, daily requests
- **Invite Links**: Token-based, 48h expiry
### 🔔 Alerts & Notifications
Five channels: Webhook / Bark / ServerChan / Telegram Bot / SMTP
Scenarios: low balance, station failure, check-in failure, proxy errors, token expiry, daily digest
### 🔌 REST API Overview
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/organizations` | GET/POST | List / Create org |
| `/api/organizations/plans` | GET | List available plans |
| `/api/organizations/:id` | GET/PATCH | Org detail / Update |
| `/api/organizations/:id/members` | GET | List members |
| `/api/organizations/:id/members/:uid` | PATCH/DELETE | Change role / Remove |
| `/api/organizations/:id/invitations` | GET/POST | List / Create invite |
| `/api/organizations/:id/invitations/:invId` | DELETE | Cancel invite |
| `/api/invitations/:token` | GET | Public invite lookup |
| `/api/invitations/:token/accept` | POST | Accept invitation |
| `/api/users/register` | POST | Register new user |
| `/api/users/login` | POST | Login (returns JWT) |
| `/api/users/me` | GET/PATCH | Profile / Update |
| `/api/user-api-keys` | GET/POST | List / Create API key |
---
## 🚀 Quick Start
### Docker Compose (Recommended)
```bash
mkdir boosapi && cd boosapi
cat > docker-compose.yml << 'EOF'
services:
boosapi:
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
EOF
export AUTH_TOKEN=your-admin-token
export PROXY_TOKEN=your-proxy-sk-token
docker compose up -d
``` ```
Open `http://localhost:4000` and log in with `AUTH_TOKEN`. ## Quick Start
--- ```bash
cp config.toml.example config.toml
./build-and-run.sh
```
## 🏗️ Tech Stack For a production bundle with embedded web assets:
```bash
./build-and-run.sh --production
```
The production output is intended to run with only:
```txt
boosapi
config.toml
```
Web assets and Workflow UI assets are embedded in the Rust binary when built for production.
## Workflow Backend Configuration
Comfy Cloud:
```toml
[comfyui]
backend = "cloud"
base_url = "https://cloud.comfy.org"
api_key = "your-comfy-cloud-api-key"
```
RunningHub:
```toml
[comfyui]
backend = "runninghub"
base_url = "https://www.runninghub.cn/your-comfy-compatible-endpoint"
api_key = "your-runninghub-api-key"
```
External ComfyUI:
```toml
[comfyui]
backend = "external"
base_url = "http://127.0.0.1:8188"
```
BoosAPI API-native executor:
```toml
[comfyui]
backend = "boosapi"
```
## Environment Variables
Configuration can be overridden with `BOOSAPI_<SECTION>__<KEY>`:
```bash
BOOSAPI_SERVER__HOST=0.0.0.0
BOOSAPI_SERVER__PORT=4000
BOOSAPI_AUTH__AUTH_TOKEN=your-admin-token
BOOSAPI_AUTH__PROXY_TOKEN=sk-your-proxy-token
BOOSAPI_AUTH__JWT_SECRET=your-jwt-secret
BOOSAPI_DB__URL=sqlite:data/boosapi.db?mode=rwc
```
## Tech Stack
| Layer | Tech | | Layer | Tech |
|-------|------| |-------|------|
| Backend | Fastify | | Backend | Rust, Axum 0.8, Tokio |
| Frontend | React 18 + Vite | | Database | SQLx, SQLite runtime path, PostgreSQL schema/migration preparation |
| Language | TypeScript | | Proxy engine | Token Router + Proxy Conductor |
| Styling | Tailwind CSS v4 | | Protocols | OpenAI / Claude / Gemini compatible transformers |
| Database | SQLite / MySQL / PostgreSQL + Drizzle ORM | | Frontend | React 19 + Vite |
| Visualization | VChart | | Workflow UI | Embedded ComfyUI frontend variants for cloud/external-compatible backends |
| Container | Docker + Docker Compose | | Static serving | Rust binary serves `dist/web` and Workflow UI assets |
| Test | Vitest | | Container | Docker / Docker Compose |
--- Node.js is only a build-time tool for the frontend. Production does not require a Node.js process.
## 🛠️ Local Development ## Local Development
```bash ```bash
npm install cargo check
npm run db:migrate cargo run
npm run dev pnpm install
pnpm run dev:web
``` ```
Build checks:
```bash ```bash
npm run build # Build web + server cargo check
npm test # Run tests pnpm run typecheck:web
npx vite build
``` ```
--- ## License
## 📜 License
[MIT](LICENSE) [MIT](LICENSE)
-178
View File
@@ -1,178 +0,0 @@
# Security Policy / 安全政策
## Overview / 概述
The security of Metapi is important to us. This document outlines our security policy and how to report vulnerabilities.
Metapi 的安全对我们很重要。本文档概述了我们的安全政策以及如何报告漏洞。
Since Metapi is a self-hosted meta-aggregation layer that manages sensitive credentials (API keys, account passwords) and proxies AI API requests, security is a critical concern.
由于 Metapi 是一个自托管的元聚合层,管理敏感凭证(API 密钥、账号密码)并代理 AI API 请求,因此安全性至关重要。
## Supported Versions / 支持的版本
We provide security updates for the following versions / 我们为以下版本提供安全更新:
| Version / 版本 | Supported / 支持状态 |
| -------------- | -------------------- |
| Latest release / 最新版本 | ✅ Fully supported / 完全支持 |
| `main` branch / `main` 分支 | ✅ Supported / 支持 |
| Older releases / 旧版本 | ⚠️ Best effort / 尽力而为 |
| Forks / 分支 | ❌ Not supported / 不支持 |
**Recommendation** / **建议**: Always use the latest stable release from [GitHub Releases](https://github.com/cita-777/metapi/releases) or the `main` branch for the most up-to-date security patches.
始终使用 [GitHub Releases](https://github.com/cita-777/metapi/releases) 的最新稳定版本或 `main` 分支以获得最新的安全补丁。
## Security Considerations / 安全注意事项
When deploying Metapi, please consider / 部署 Metapi 时,请考虑:
### Credential Storage / 凭证存储
- All sensitive credentials (API keys, passwords) are encrypted at rest in the database / 所有敏感凭证(API 密钥、密码)在数据库中静态加密存储
- Use strong `AUTH_TOKEN` and `PROXY_TOKEN` values / 使用强 `AUTH_TOKEN` 和 `PROXY_TOKEN` 值
- Never commit `.env` files or expose tokens in logs / 切勿提交 `.env` 文件或在日志中暴露令牌
### Network Security / 网络安全
- Deploy behind HTTPS/TLS in production / 在生产环境中部署在 HTTPS/TLS 后面
- Use `ADMIN_IP_ALLOWLIST` to restrict admin access / 使用 `ADMIN_IP_ALLOWLIST` 限制管理员访问
- Consider firewall rules to limit access to port 4000 / 考虑使用防火墙规则限制对端口 4000 的访问
### Database Security / 数据库安全
- Secure your database with strong credentials / 使用强凭证保护您的数据库
- Regularly backup the `data/` directory / 定期备份 `data/` 目录
- For production, consider using MySQL/PostgreSQL instead of SQLite / 对于生产环境,考虑使用 MySQL/PostgreSQL 而不是 SQLite
### Docker Security / Docker 安全
- Keep Docker images up to date / 保持 Docker 镜像最新
- Use volume mounts carefully to avoid exposing sensitive data / 谨慎使用卷挂载以避免暴露敏感数据
- Run containers with minimal privileges / 以最小权限运行容器
## Reporting a Vulnerability / 报告漏洞
**⚠️ Please do NOT report security vulnerabilities through public GitHub issues, discussions, or pull requests.**
**⚠️ 请勿通过公开的 GitHub issue、讨论或 pull request 报告安全漏洞。**
### Reporting Channels / 报告渠道
Use one of these private channels / 使用以下私密渠道之一:
1. **GitHub Security Advisory** (Preferred) / **GitHub 安全公告**(首选)
- Go to: https://github.com/cita-777/metapi/security/advisories/new
- This allows for coordinated disclosure and CVE assignment / 这允许协调披露和 CVE 分配
2. **Email** / **邮件**
- Send to: `cita-777@users.noreply.github.com`
- Subject: `[Metapi Security] <brief description>` / 主题:`[Metapi Security] <简要描述>`
### What to Include / 应包含的内容
To help us understand and address the issue quickly, please include / 为了帮助我们快速理解和解决问题,请包含:
- **Description** / **描述**: Clear description of the vulnerability / 漏洞的清晰描述
- **Impact** / **影响**: What an attacker could achieve / 攻击者可能实现的目标
- **Affected versions** / **受影响的版本**: Version, commit hash, or deployment mode / 版本、提交哈希或部署模式
- **Affected components** / **受影响的组件**: Specific endpoints, modules, or configuration / 特定端点、模块或配置
- **Reproduction steps** / **复现步骤**: Step-by-step instructions to reproduce / 逐步复现说明
- **Proof of concept** / **概念验证**: Code, screenshots, or logs (with secrets redacted) / 代码、截图或日志(删除敏感信息)
- **Attack preconditions** / **攻击前提条件**: Required access level, network position, etc. / 所需访问级别、网络位置等
- **Suggested fix** / **建议修复**: If you have ideas for mitigation or patches / 如果您有缓解措施或补丁的想法
### Example Report / 报告示例
```
Subject: [Metapi Security] SQL Injection in account search
Description:
The account search endpoint is vulnerable to SQL injection through the
'name' parameter, allowing unauthorized database access.
Impact:
An authenticated attacker could extract all account credentials from
the database.
Affected Version:
v1.2.2 and earlier
Reproduction:
1. Login to Metapi admin panel
2. Navigate to /api/accounts/search?name=' OR '1'='1
3. Observe all accounts returned
Proof of Concept:
[Screenshot attached with sensitive data redacted]
Suggested Fix:
Use parameterized queries instead of string concatenation in
src/server/routes/api/accounts.ts:45
```
## Response Process / 响应流程
Our security response process / 我们的安全响应流程:
1. **Acknowledgment** / **确认**: We aim to acknowledge your report within **3 business days** / 我们力求在 **3 个工作日**内确认您的报告
2. **Initial Triage** / **初步分类**: We will provide initial assessment or follow-up questions within **7 business days** / 我们将在 **7 个工作日**内提供初步评估或后续问题
3. **Investigation** / **调查**: We may request additional details, logs, or reproduction steps / 我们可能会要求额外的细节、日志或复现步骤
4. **Fix Development** / **修复开发**: We will develop and test a fix / 我们将开发并测试修复方案
5. **Coordinated Disclosure** / **协调披露**: We will coordinate disclosure timing with you / 我们将与您协调披露时间
- Please keep the vulnerability confidential until we release a fix / 请在我们发布修复之前对漏洞保密
- We will credit you in the security advisory (unless you prefer to remain anonymous) / 我们将在安全公告中致谢您(除非您希望保持匿名)
6. **Release** / **发布**: We will release a patched version and publish a security advisory / 我们将发布修补版本并发布安全公告
### Severity Assessment / 严重性评估
We use the following severity levels / 我们使用以下严重性级别:
- **Critical** / **严重**: Remote code execution, credential theft, data breach / 远程代码执行、凭证盗窃、数据泄露
- **High** / **高**: Authentication bypass, privilege escalation / 身份验证绕过、权限提升
- **Medium** / **中**: Information disclosure, denial of service / 信息泄露、拒绝服务
- **Low** / **低**: Minor information leaks, configuration issues / 轻微信息泄露、配置问题
## Public Disclosure / 公开披露
If a vulnerability is accidentally posted publicly / 如果漏洞被意外公开发布:
1. We will immediately assess the risk / 我们将立即评估风险
2. We may redact sensitive details from the public post / 我们可能会从公开帖子中删除敏感细节
3. We will redirect the reporter to this private process / 我们将把报告者重定向到此私密流程
4. We will expedite the fix and release process / 我们将加快修复和发布流程
## Security Updates / 安全更新
Security updates will be announced through / 安全更新将通过以下方式公布:
- [GitHub Security Advisories](https://github.com/cita-777/metapi/security/advisories)
- [GitHub Releases](https://github.com/cita-777/metapi/releases) with `[SECURITY]` tag / 带有 `[SECURITY]` 标签
- Project README and documentation / 项目 README 和文档
Subscribe to repository notifications to stay informed / 订阅仓库通知以保持了解。
## Bug Bounty / 漏洞赏金
We currently do not offer a paid bug bounty program. However, we deeply appreciate security researchers who responsibly disclose vulnerabilities and will publicly acknowledge your contribution (with your permission).
我们目前不提供付费漏洞赏金计划。但是,我们非常感谢负责任地披露漏洞的安全研究人员,并将公开致谢您的贡献(经您许可)。
## Questions / 问题
If you have questions about this security policy, please contact `cita-777@users.noreply.github.com`.
如果您对本安全政策有疑问,请联系 `cita-777@users.noreply.github.com`。
---
**Thank you for helping keep Metapi and its users safe!**
**感谢您帮助保护 Metapi 及其用户的安全!**
-208
View File
@@ -1,208 +0,0 @@
# 本地测试环境搭建指南
## 目录结构
```
D:\Code\Projects\Metapi\
├── metapi # 主仓库(upstream)
├── metapi-routing-ux-optimization # 开发 worktree (模型白名单功能)
├── metapi-upstream-latest # 其他 worktree
└── ...
```
## 快速启动测试服务器
### 1. 进入开发 worktree
```bash
cd D:/Code/Projects/Metapi/metapi-routing-ux-optimization
```
### 2. 安装依赖(首次运行)
```bash
pnpm install
```
### 3. 构建项目
```bash
pnpm build
```
### 4. 启动测试服务器
```bash
DATA_DIR="./tmp/test-db" node dist/server/index.js
```
服务器将在 http://localhost:4000 启动
**测试Token:** `test-admin-token`
### 5. 访问前端
打开浏览器访问: http://localhost:4000
登录使用 Token: `test-admin-token`
## 测试数据准备
### 方式1: 导入真实站点
在前端"站点管理"中添加真实的 new-api 站点:
- 站点URL: 真实的 new-api 地址
- 平台: new-api
- 添加账号和token
然后触发模型发现:
```bash
curl -X POST -H "Authorization: Bearer test-admin-token" \
-H "Content-Type: application/json" \
-d '{"refreshModels": true, "wait": true}' \
http://localhost:4000/api/routes/rebuild
```
### 方式2: 使用测试脚本(可选)
```bash
DATA_DIR="./tmp/test-db" node scripts/seed-test-data.js
```
## 测试模型白名单功能
### 1. 进入设置页面
点击左侧导航"设置",滚动到"全局模型白名单"卡片
### 2. 测试功能
#### 添加模型到白名单
- **方式1**: 在输入框输入模型名称,按回车
- **方式2**: 点击"可用模型列表"中的模型
#### 删除模型
- 点击已选模型徽章上的 × 按钮
#### 保存配置
- 点击"保存模型白名单"按钮
- 系统自动触发路由重建
### 3. 验证效果
#### 查看路由
进入"令牌路由"页面,确认只有白名单中的模型有路由
#### 查看模型候选
```bash
curl -H "Authorization: Bearer test-admin-token" \
http://localhost:4000/api/models/token-candidates | python -m json.tool
```
应该只返回白名单中的模型
### 4. API 测试命令
#### 查看当前白名单
```bash
curl -H "Authorization: Bearer test-admin-token" \
http://localhost:4000/api/settings/runtime | python -m json.tool | grep -A 5 "globalAllowedModels"
```
#### 设置白名单
```bash
curl -X PUT \
-H "Authorization: Bearer test-admin-token" \
-H "Content-Type: application/json" \
-d '{"globalAllowedModels": ["gpt-4", "gpt-4o"]}' \
http://localhost:4000/api/settings/runtime | python -m json.tool
```
#### 清空白名单(允许所有模型)
```bash
curl -X PUT \
-H "Authorization: Bearer test-admin-token" \
-H "Content-Type: application/json" \
-d '{"globalAllowedModels": []}' \
http://localhost:4000/api/settings/runtime | python -m json.tool
```
## 停止测试服务器
### 查找进程
```bash
# Windows
netstat -ano | grep ":4000" | grep "LISTENING"
# Linux/Mac
lsof -i :4000
```
### 停止进程
```bash
# Windows (替换 PID)
taskkill //F //PID <PID>
# Linux/Mac
kill <PID>
```
## 开发工作流
### 修改代码后重新构建
```bash
pnpm build
```
### 运行测试
```bash
pnpm test
```
### 提交代码
```bash
git add .
git commit -m "feat: 实现模型白名单功能"
git push origin routing-ux-optimization
```
## 常见问题
### Q: 路由列表为空?
A: 检查 token_model_availability 表是否有数据,触发模型发现重新加载
### Q: 白名单保存后路由消失?
A: 正常现象,白名单会过滤掉不在列表中的模型路由
### Q: 如何恢复所有模型?
A: 清空白名单(设置为空数组 [])即可
## 数据库位置
测试数据库存储在: `./tmp/test-db/hub.db`
查看数据库:
```bash
sqlite3 ./tmp/test-db/hub.db
```
常用查询:
```sql
-- 查看所有站点
SELECT id, name, url, platform FROM sites WHERE status = 'active';
-- 查看所有账号
SELECT id, site_id, username, status FROM accounts;
-- 查看所有token
SELECT id, account_id, name, value_status FROM account_tokens;
-- 查看模型可用性
SELECT COUNT(*) FROM token_model_availability;
-- 查看路由
SELECT COUNT(*) FROM token_routes;
```
File diff suppressed because one or more lines are too long
BIN
View File
Binary file not shown.
-298
View File
@@ -1,298 +0,0 @@
"""
BoosAPI Custom Nodes for ComfyUI
==================================
Call local BoosAPI endpoints directly from ComfyUI workflows:
- Text generation (gpt-5.5)
- Image generation (gpt-image-2)
- Text-to-Speech
- Video generation (async)
Default: http://127.0.0.1:4000/v1 (same container)
Override via env:
BOOSAPI_BASE = https://api.boos.lat/v1 (standalone ComfyUI)
BOOSAPI_KEY=***
"""
import os
import json
import time
import requests
import torch
import numpy as np
from PIL import Image
from io import BytesIO
import folder_paths
# ── Config ──────────────────────────────────────────────────────────────────
# When running inside the same container as BoosAPI, use localhost.
# For standalone ComfyUI installs, set BOOSAPI_BASE env to https://api.boos.lat/v1
BOOSAPI_BASE = os.getenv("BOOSAPI_BASE", "http://127.0.0.1:4000/v1")
BOOSAPI_KEY = os.getenv("BOOSAPI_KEY", "admin")
HEADERS = {
"Authorization": f"Bearer {BOOSAPI_KEY}",
"Content-Type": "application/json",
}
def _call_api(endpoint: str, payload: dict, timeout: int = 120) -> dict:
url = f"{BOOSAPI_BASE}/{endpoint.lstrip('/')}"
resp = requests.post(url, headers=HEADERS, json=payload, timeout=timeout)
if not resp.ok:
raise RuntimeError(f"BoosAPI {endpoint} failed HTTP {resp.status_code}: {resp.text}")
return resp.json()
# ── Shared helpers ──────────────────────────────────────────────────────────
def _pil_to_tensor(img: Image.Image) -> torch.Tensor:
"""PIL -> ComfyUI IMAGE (1, H, W, 3) float32 0-1."""
img = img.convert("RGB")
arr = np.array(img).astype(np.float32) / 255.0
return torch.from_numpy(arr).unsqueeze(0)
def _tensor_to_pil(tensor: torch.Tensor) -> Image.Image:
"""ComfyUI IMAGE -> PIL."""
arr = (tensor.squeeze(0).cpu().numpy() * 255).astype(np.uint8)
return Image.fromarray(arr)
# ── Node: Text Generation (gpt-5.5) ─────────────────────────────────────────
class BoosAPITextGen:
"""Call gpt-5.5 chat completion. Useful for prompt engineering within workflows."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"multiline": True, "default": ""}),
"model": ("STRING", {"default": "gpt-5.5"}),
"temperature": ("FLOAT", {"default": 0.7, "min": 0, "max": 2, "step": 0.1}),
"max_tokens": ("INT", {"default": 2048, "min": 1, "max": 32768}),
},
"optional": {
"system_prompt": ("STRING", {"multiline": True, "default": ""}),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("text",)
FUNCTION = "generate"
CATEGORY = "BoosAPI"
def generate(self, prompt, model, temperature, max_tokens, system_prompt=""):
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
messages.append({"role": "user", "content": prompt})
data = _call_api("chat/completions", {
"model": model,
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens,
})
text = data["choices"][0]["message"]["content"]
return (text,)
# ── Node: Image Generation (gpt-image-2) ────────────────────────────────────
class BoosAPIImageGen:
"""Generate image via BoosAPI. Replaces CheckpointLoader+KSampler+VAE chain."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"multiline": True, "default": ""}),
"model": (["gpt-image-2", "dall-e-3"], {"default": "gpt-image-2"}),
"size": (["1024x1024", "1792x1024", "1024x1792"], {"default": "1024x1024"}),
},
"optional": {
"negative_prompt": ("STRING", {"multiline": True, "default": ""}),
"n": ("INT", {"default": 1, "min": 1, "max": 4}),
},
}
RETURN_TYPES = ("IMAGE", "STRING")
RETURN_NAMES = ("image", "image_url")
FUNCTION = "generate"
CATEGORY = "BoosAPI"
def generate(self, prompt, model, size, negative_prompt="", n=1):
payload = {
"model": model,
"prompt": prompt,
"n": n,
"size": size,
}
if negative_prompt:
payload["negative_prompt"] = negative_prompt
data = _call_api("images/generations", payload)
# Download first image
image_url = data["data"][0]["url"]
img_resp = requests.get(image_url, timeout=60)
img = Image.open(BytesIO(img_resp.content))
tensor = _pil_to_tensor(img)
return (tensor, image_url)
# ── Node: Text-to-Speech ────────────────────────────────────────────────────
class BoosAPITTS:
"""Generate speech audio from text via BoosAPI TTS."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": ("STRING", {"multiline": True, "default": ""}),
"voice": (["alloy", "echo", "fable", "onyx", "nova", "shimmer"], {"default": "alloy"}),
},
"optional": {
"speed": ("FLOAT", {"default": 1.0, "min": 0.25, "max": 4.0, "step": 0.25}),
},
}
RETURN_TYPES = ("STRING",) # file path to generated audio
RETURN_NAMES = ("audio_path",)
FUNCTION = "generate"
CATEGORY = "BoosAPI"
OUTPUT_NODE = True
def generate(self, text, voice, speed=1.0):
url = f"{BOOSAPI_BASE}/audio/speech"
resp = requests.post(
url, headers=HEADERS,
json={"model": "tts-1", "input": text, "voice": voice, "speed": speed},
timeout=120,
)
if not resp.ok:
raise RuntimeError(f"TTS failed HTTP {resp.status_code}: {resp.text}")
# Save to ComfyUI output directory
output_dir = folder_paths.get_output_directory()
os.makedirs(output_dir, exist_ok=True)
filename = f"boosapi_tts_{int(time.time())}.mp3"
filepath = os.path.join(output_dir, filename)
with open(filepath, "wb") as f:
f.write(resp.content)
print(f"[BoosAPI] TTS saved: {filepath}")
return (filepath,)
# ── Node: Video Generation (async) ─────────────────────────────────────────
class BoosAPIVideoGen:
"""Generate video via BoosAPI (async). Returns task ID for polling."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"multiline": True, "default": ""}),
"model": ("STRING", {"default": "cogvideo-5b"}),
"size": (["1024x1024", "1024x576", "576x1024", "1920x1080"], {"default": "1024x576"}),
},
"optional": {
"negative_prompt": ("STRING", {"multiline": True, "default": ""}),
"duration": ("INT", {"default": 5, "min": 2, "max": 30}),
"fps": ("INT", {"default": 16, "min": 8, "max": 30}),
},
}
RETURN_TYPES = ("STRING", "STRING")
RETURN_NAMES = ("task_id", "status_url")
FUNCTION = "generate"
CATEGORY = "BoosAPI"
def generate(self, prompt, model, size, negative_prompt="", duration=5, fps=16):
width, height = size.split("x")
payload = {
"model": model,
"prompt": prompt,
"width": int(width),
"height": int(height),
"duration": duration,
"fps": fps,
}
if negative_prompt:
payload["negative_prompt"] = negative_prompt
data = _call_api("videos", payload)
task_id = data.get("id", "")
status_url = f"{BOOSAPI_BASE}/videos/{task_id}"
return (task_id, status_url)
class BoosAPIVideoStatus:
"""Poll video generation status. Returns download URL when done."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"task_id": ("STRING", {"default": ""}),
},
"optional": {
"wait": ("BOOLEAN", {"default": True, "label_on": "Wait until done", "label_off": "Check once"}),
"poll_interval": ("INT", {"default": 5, "min": 1, "max": 60}),
"max_polls": ("INT", {"default": 60, "min": 1, "max": 600}),
},
}
RETURN_TYPES = ("STRING", "STRING")
RETURN_NAMES = ("status", "video_url")
FUNCTION = "poll"
CATEGORY = "BoosAPI"
def poll(self, task_id, wait=True, poll_interval=5, max_polls=60):
url = f"{BOOSAPI_BASE}/videos/{task_id}"
for attempt in range(max_polls):
resp = requests.get(url, headers=HEADERS, timeout=30)
if not resp.ok:
return ("error", f"HTTP {resp.status_code}")
data = resp.json()
status = data.get("status", "unknown")
if status == "completed":
video_url = data.get("video_url") or data.get("output", {}).get("video_url", "")
return ("completed", video_url)
elif status in ("failed", "error"):
error_msg = data.get("error", str(data))
return (f"failed: {error_msg}", "")
if not wait:
return (status, "")
time.sleep(poll_interval)
return ("timeout", "")
# ── Registration ────────────────────────────────────────────────────────────
NODE_CLASS_MAPPINGS = {
"BoosAPITextGen": BoosAPITextGen,
"BoosAPIImageGen": BoosAPIImageGen,
"BoosAPITTS": BoosAPITTS,
"BoosAPIVideoGen": BoosAPIVideoGen,
"BoosAPIVideoStatus": BoosAPIVideoStatus,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"BoosAPITextGen": "BoosAPI Text Gen (gpt-5.5)",
"BoosAPIImageGen": "BoosAPI Image Gen (gpt-image-2)",
"BoosAPITTS": "BoosAPI Text-to-Speech",
"BoosAPIVideoGen": "BoosAPI Video Gen (async)",
"BoosAPIVideoStatus": "BoosAPI Video Status",
}
+625
View File
@@ -0,0 +1,625 @@
#!/usr/bin/env bash
# 编译前后端并启动服务
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$ROOT_DIR"
# ── 工具函数 ──────────────────────────────────────────────────────────────────
log() { echo "[$(date '+%H:%M:%S')] $*"; }
ok() { echo "[$(date '+%H:%M:%S')] ✓ $*"; }
fail() { echo "[$(date '+%H:%M:%S')] ✗ $*" >&2; exit 1; }
# ── 参数 ──────────────────────────────────────────────────────────────────────
BUILD_PROFILE="${BOOSAPI_BUILD_PROFILE:-dev}"
FRONTEND_ONLY=0
BACKEND_ONLY=0
CHECK_ONLY=0
NO_RUN=0
FORCE_FRONTEND=0
SKIP_FRONTEND=0
PRODUCTION_BUNDLE=0
PRODUCTION_DIR="${BOOSAPI_PRODUCTION_DIR:-$ROOT_DIR/dist/production}"
CARGO_FEATURES="${BOOSAPI_CARGO_FEATURES:-}"
CARGO_JOBS="${BOOSAPI_CARGO_JOBS:-}"
FAST_LINKER="${BOOSAPI_FAST_LINKER:-auto}"
FRONTEND_PM="${BOOSAPI_FRONTEND_PM:-auto}"
BUILD_TIMINGS=()
usage() {
cat <<'EOF'
用法: ./build-and-run.sh [选项]
选项:
--dev 使用 dev 模式编译后端(默认,最快)
--release 使用 release 模式编译后端(更慢但性能好)
--fast-release 使用 fast-release 模式编译后端(比 release 快,性能接近生产)
--profile <name> 指定构建 profile:dev / release / fast-release
--production 生产编译:先构建前端并嵌入二进制,再输出 dist/production/{boosapi,config.toml}
--prod 同 --production
--production-dir <dir>
指定生产产物目录(默认:dist/production)
--check 只执行 cargo check,不构建前端、不启动服务(最快验证)
--backend-check 同 --check
--frontend-only 只构建前端
--backend-only 只构建后端
--force-frontend 强制重建前端,忽略缓存
--skip-frontend 跳过前端构建(沿用现有 dist/web)
--bun 主前端使用 Bun 执行 Vite 构建(Workflow UI 子模块会自动准备 pnpm)
--no-run 构建后不启动服务
BOOSAPI_BUILD_PROFILE 可设置默认 profile(dev / release / fast-release)
BOOSAPI_CARGO_FEATURES 可覆盖后端 Cargo features(默认:空)
BOOSAPI_CARGO_JOBS 可设置 cargo 并发任务数,例如 8
BOOSAPI_FAST_LINKER 可设置 auto / off / mold / lld(默认:auto,Linux 下优先 mold)
BOOSAPI_FRONTEND_PM 可设置主前端构建器:auto / npm / bun(默认:auto)
BOOSAPI_PRODUCTION_DIR 可设置 --production 默认产物目录
-h, --help 显示帮助
示例:
./build-and-run.sh --release
./build-and-run.sh --fast-release
./build-and-run.sh --check
./build-and-run.sh --bun
./build-and-run.sh --production
./build-and-run.sh --production --production-dir /opt/boosapi-release
BOOSAPI_BUILD_PROFILE=release ./build-and-run.sh
EOF
}
while [ $# -gt 0 ]; do
case "$1" in
--dev) BUILD_PROFILE="dev" ;;
--release) BUILD_PROFILE="release" ;;
--fast-release) BUILD_PROFILE="fast-release" ;;
--production|--prod)
PRODUCTION_BUNDLE=1
BUILD_PROFILE="release"
NO_RUN=1
;;
--production-dir)
shift
[ $# -gt 0 ] || fail "--production-dir 需要目录参数"
PRODUCTION_DIR="$1"
;;
--check|--backend-check)
CHECK_ONLY=1
BACKEND_ONLY=1
NO_RUN=1
;;
--profile)
shift
[ $# -gt 0 ] || fail "--profile 需要参数:dev / release / fast-release"
BUILD_PROFILE="$1"
;;
--frontend-only) FRONTEND_ONLY=1 ;;
--backend-only) BACKEND_ONLY=1 ;;
--force-frontend) FORCE_FRONTEND=1 ;;
--skip-frontend) SKIP_FRONTEND=1 ;;
--bun) FRONTEND_PM="bun" ;;
--no-run) NO_RUN=1 ;;
--rebuild-image) log "--rebuild-image 已废弃:内置 Docker ComfyUI 工作台已移除,忽略该参数" ;;
-h|--help) usage; exit 0 ;;
*) echo "未知参数: $1" >&2; usage >&2; exit 1 ;;
esac
shift
done
if [ "$FRONTEND_ONLY" -eq 1 ] && [ "$BACKEND_ONLY" -eq 1 ]; then
fail "--frontend-only 和 --backend-only 不能同时使用"
fi
if [ "$PRODUCTION_BUNDLE" -eq 1 ] && [ "$FRONTEND_ONLY" -eq 1 ]; then
fail "--production 不能和 --frontend-only 同时使用"
fi
if [ "$PRODUCTION_BUNDLE" -eq 1 ] && [ "$BACKEND_ONLY" -eq 1 ]; then
fail "--production 需要先构建前端以嵌入 web,不能和 --backend-only 同时使用"
fi
if [ "$PRODUCTION_BUNDLE" -eq 1 ] && [ "$CHECK_ONLY" -eq 1 ]; then
fail "--production 不能和 --check 同时使用"
fi
case "$BUILD_PROFILE" in
dev|debug) BUILD_PROFILE="dev" ;;
release|fast-release) ;;
*) fail "未知构建 profile: $BUILD_PROFILE(可选:dev / release / fast-release)" ;;
esac
case "$FAST_LINKER" in
auto|off|none|mold|lld) ;;
*) fail "未知 BOOSAPI_FAST_LINKER: $FAST_LINKER(可选:auto / off / mold / lld)" ;;
esac
timer_start() {
date +%s
}
record_timing() {
local label="$1"
local started="$2"
local ended
ended="$(date +%s)"
BUILD_TIMINGS+=("${label}: $((ended - started))s")
}
print_timing_summary() {
[ "${#BUILD_TIMINGS[@]}" -gt 0 ] || return 0
log "构建耗时:"
local item
for item in "${BUILD_TIMINGS[@]}"; do
log " - $item"
done
}
append_rustflags() {
local flags="$1"
if [[ " ${RUSTFLAGS:-} " != *" $flags "* ]]; then
export RUSTFLAGS="${RUSTFLAGS:+$RUSTFLAGS }$flags"
fi
}
build_cargo_args() {
CARGO_BASE_ARGS=()
CARGO_FEATURE_ARGS=()
if [ -n "$CARGO_JOBS" ]; then
CARGO_BASE_ARGS+=(--jobs "$CARGO_JOBS")
fi
if [ -n "$CARGO_FEATURES" ]; then
CARGO_FEATURE_ARGS=(--features "$CARGO_FEATURES")
fi
}
configure_fast_linker() {
local mode="$FAST_LINKER"
case "$(uname -s 2>/dev/null || true)" in
Linux) ;;
*)
[ "$mode" = "auto" ] && return 0
fail "BOOSAPI_FAST_LINKER=$mode 目前只在 Linux 构建环境自动配置"
;;
esac
if [ "$mode" = "off" ] || [ "$mode" = "none" ]; then
return 0
fi
if [ "$mode" = "auto" ]; then
if command -v mold >/dev/null 2>&1; then
mode="mold"
elif command -v ld.lld >/dev/null 2>&1 || command -v lld >/dev/null 2>&1; then
mode="lld"
else
log "提示: 安装 mold 或 lld 可显著缩短 Rust 链接时间;当前使用系统默认链接器"
return 0
fi
fi
case "$mode" in
mold)
command -v mold >/dev/null 2>&1 || fail "BOOSAPI_FAST_LINKER=mold 但未找到 mold"
if command -v clang >/dev/null 2>&1; then
append_rustflags "-C linker=clang"
elif ! command -v gcc >/dev/null 2>&1 && [ -z "${RUSTC_LINKER:-}" ]; then
fail "启用 mold 需要 clang 或 gcc 作为 linker driver"
fi
append_rustflags "-C link-arg=-fuse-ld=mold"
log "Rust 快速链接器: mold(BOOSAPI_FAST_LINKER=$FAST_LINKER)"
;;
lld)
if ! command -v ld.lld >/dev/null 2>&1 && ! command -v lld >/dev/null 2>&1; then
fail "BOOSAPI_FAST_LINKER=lld 但未找到 ld.lld/lld"
fi
if command -v clang >/dev/null 2>&1; then
append_rustflags "-C linker=clang"
elif ! command -v gcc >/dev/null 2>&1 && [ -z "${RUSTC_LINKER:-}" ]; then
fail "启用 lld 需要 clang 或 gcc 作为 linker driver"
fi
append_rustflags "-C link-arg=-fuse-ld=lld"
log "Rust 快速链接器: lld(BOOSAPI_FAST_LINKER=$FAST_LINKER)"
;;
esac
}
server_port() {
if [ -n "${BOOSAPI_SERVER__PORT:-}" ]; then
printf '%s\n' "$BOOSAPI_SERVER__PORT"
return
fi
awk '
/^\[server\]/ { in_server = 1; next }
/^\[/ { in_server = 0 }
in_server && /^[[:space:]]*port[[:space:]]*=/ {
gsub(/[^0-9]/, "", $0)
print $0
exit
}
' "$ROOT_DIR/config.toml" 2>/dev/null || true
}
is_running_pid() {
local pid="$1"
[ -n "$pid" ] && kill -0 "$pid" 2>/dev/null
}
stop_pids() {
local label="$1"
shift || true
local pids=()
local pid
for pid in "$@"; do
if [ -n "$pid" ] && [ "$pid" != "$$" ] && is_running_pid "$pid"; then
pids+=("$pid")
fi
done
if [ "${#pids[@]}" -eq 0 ]; then
return 0
fi
log "停止旧进程 ${label}: ${pids[*]}"
kill -TERM "${pids[@]}" 2>/dev/null || true
for _ in 1 2 3 4 5; do
local alive=()
for pid in "${pids[@]}"; do
if is_running_pid "$pid"; then
alive+=("$pid")
fi
done
if [ "${#alive[@]}" -eq 0 ]; then
ok "旧进程已停止"
return 0
fi
sleep 1
pids=("${alive[@]}")
done
log "旧进程未及时退出,强制结束: ${pids[*]}"
kill -KILL "${pids[@]}" 2>/dev/null || true
}
stop_previous_server() {
local binary_pids=()
local port_pids=()
local port
while IFS= read -r pid; do
[ -n "$pid" ] && binary_pids+=("$pid")
done < <(pgrep -f "target/(debug|release|fast-release)/boosapi" 2>/dev/null || true)
stop_pids "boosapi binary" "${binary_pids[@]}"
port="$(server_port)"
port="${port:-4000}"
if command -v fuser >/dev/null 2>&1; then
while IFS= read -r pid; do
[ -n "$pid" ] && port_pids+=("$pid")
done < <(fuser "${port}/tcp" 2>/dev/null | tr ' ' '\n' || true)
stop_pids "占用 ${port}/tcp" "${port_pids[@]}"
else
log "未找到 fuser,跳过端口 ${port}/tcp 占用检查"
fi
}
ensure_pnpm() {
if command -v pnpm >/dev/null 2>&1; then
return 0
fi
command -v corepack >/dev/null 2>&1 || fail "未找到 pnpm/corepack,无法自动准备 pnpm。请安装 Node.js 22+ 或 pnpm"
log "未找到 pnpm,尝试通过 corepack 自动启用..."
corepack enable
corepack prepare pnpm@latest --activate
command -v pnpm >/dev/null 2>&1 || fail "pnpm 自动准备失败,请手动安装 pnpm"
}
install_frontend_dependencies() {
local frontend_pm="$1"
if [ "$frontend_pm" = "bun" ]; then
command -v bun >/dev/null 2>&1 || fail "未找到 bun,请安装 Bun 或移除 --bun"
if [ ! -d "$ROOT_DIR/node_modules" ] || [ "$FORCE_FRONTEND" -eq 1 ]; then
log "安装主前端依赖 (bun install)..."
bun install
fi
return 0
fi
command -v node >/dev/null 2>&1 || fail "未找到 node,请先安装 Node.js"
ensure_pnpm
if [ ! -d "$ROOT_DIR/node_modules" ] || [ "$FORCE_FRONTEND" -eq 1 ]; then
log "安装主前端依赖 (pnpm install)..."
pnpm install --frozen-lockfile
fi
}
build_frontend() {
local started
started="$(timer_start)"
if [ "$SKIP_FRONTEND" -eq 1 ]; then
log "跳过前端构建(--skip-frontend)"
record_timing "前端检查" "$started"
return 0
fi
local frontend_pm="$FRONTEND_PM"
case "$frontend_pm" in
auto)
if command -v bun >/dev/null 2>&1; then
frontend_pm="bun"
else
frontend_pm="npm"
fi
;;
npm|bun) ;;
*) fail "未知 BOOSAPI_FRONTEND_PM: $frontend_pm(可选:auto / npm / bun)" ;;
esac
install_frontend_dependencies "$frontend_pm"
local fingerprint_file="$ROOT_DIR/dist/web/.boosapi-build-fingerprint"
local fingerprint
fingerprint="$(
{
printf 'build-script-v2\n'
printf 'frontend_pm=%s\n' "$frontend_pm"
printf 'node='
node --version 2>/dev/null || true
printf 'npx='
npx --version 2>/dev/null || true
printf 'bun='
bun --version 2>/dev/null || true
for path in package.json pnpm-lock.yaml package-lock.json yarn.lock vite.config.ts tsconfig.json tsconfig.web.json tsconfig.transpile.json src/web src/shared; do
[ -e "$ROOT_DIR/$path" ] && find "$ROOT_DIR/$path" -type f -print
done | sort | xargs -r sha256sum
} | sha256sum | awk '{print $1}'
)"
if [ "$FORCE_FRONTEND" -eq 0 ] \
&& [ -f "$fingerprint_file" ] \
&& [ -f "$ROOT_DIR/dist/web/index.html" ] \
&& [ "$(cat "$fingerprint_file" 2>/dev/null || true)" = "$fingerprint" ]; then
ok "前端无变化,跳过 Vite 构建 → dist/web/"
record_timing "前端检查" "$started"
return 0
fi
log "构建前端 (Vite via ${frontend_pm})..."
# 直接调用 vite build;桌面端构建链路已移除。
if [ "$frontend_pm" = "bun" ]; then
bunx --bun vite build
else
pnpm exec vite build
fi
mkdir -p "$ROOT_DIR/dist/web"
printf '%s\n' "$fingerprint" > "$fingerprint_file"
ok "前端构建完成 → dist/web/"
record_timing "前端构建" "$started"
}
install_website_dependencies() {
command -v node >/dev/null 2>&1 || fail "未找到 node,请先安装 Node.js"
if [ ! -d "$ROOT_DIR/website/node_modules" ] || [ "$FORCE_FRONTEND" -eq 1 ]; then
log "安装官网依赖 (npm --prefix website install)..."
npm --prefix "$ROOT_DIR/website" install
fi
}
build_public_website() {
local started
started="$(timer_start)"
if [ "$SKIP_FRONTEND" -eq 1 ]; then
log "跳过官网构建(--skip-frontend)"
record_timing "官网检查" "$started"
return 0
fi
[ -f "$ROOT_DIR/website/package.json" ] || fail "未找到 website/package.json,无法构建官网"
install_website_dependencies
local fingerprint_file="$ROOT_DIR/dist/website/.bossai-website-build-fingerprint"
local fingerprint
fingerprint="$(
{
printf 'website-build-v1\n'
printf 'node='
node --version 2>/dev/null || true
printf 'npm='
npm --version 2>/dev/null || true
for path in website/package.json website/package-lock.json website/next.config.ts website/postcss.config.mjs website/tsconfig.json website/app website/public; do
[ -e "$ROOT_DIR/$path" ] && find "$ROOT_DIR/$path" -type f -print
done | sort | xargs -r sha256sum
} | sha256sum | awk '{print $1}'
)"
if [ "$FORCE_FRONTEND" -eq 0 ] \
&& [ -f "$fingerprint_file" ] \
&& [ -f "$ROOT_DIR/dist/website/index.html" ] \
&& [ "$(cat "$fingerprint_file" 2>/dev/null || true)" = "$fingerprint" ]; then
ok "官网无变化,跳过 Next 构建 → dist/website/"
record_timing "官网检查" "$started"
return 0
fi
log "构建官网 (Next.js static export)..."
npm --prefix "$ROOT_DIR/website" run build
rm -rf "$ROOT_DIR/dist/website"
mkdir -p "$ROOT_DIR/dist/website"
cp -R "$ROOT_DIR/website/out/." "$ROOT_DIR/dist/website/"
printf '%s\n' "$fingerprint" > "$fingerprint_file"
ok "官网构建完成 → dist/website/"
record_timing "官网构建" "$started"
}
ensure_workflow_ui_for_embedding() {
local started
started="$(timer_start)"
if [ "$SKIP_FRONTEND" -eq 1 ]; then
log "跳过 Workflow UI 构建检查(--skip-frontend)"
record_timing "Workflow UI 检查" "$started"
return 0
fi
if [ -f "$ROOT_DIR/dist/workflow-ui-external/index.html" ] \
&& [ -f "$ROOT_DIR/dist/workflow-ui-cloud/index.html" ] \
&& [ -f "$ROOT_DIR/dist/workflow-ui/index.html" ] \
&& [ "$FORCE_FRONTEND" -eq 0 ]; then
ok "Workflow UI 已存在,跳过构建 → dist/workflow-ui-{external,cloud}/"
record_timing "Workflow UI 检查" "$started"
return 0
fi
command -v git >/dev/null 2>&1 || fail "未找到 git,无法构建 Workflow UI"
command -v node >/dev/null 2>&1 || fail "未找到 node,无法构建 Workflow UI"
ensure_pnpm
log "构建 Workflow UI 双前端(external + cloud,用于嵌入二进制)..."
pnpm run workflow-ui:install
ok "Workflow UI 构建完成 → dist/workflow-ui-external/ dist/workflow-ui-cloud/"
record_timing "Workflow UI 构建" "$started"
}
build_backend() {
local started
started="$(timer_start)"
if ! command -v cargo >/dev/null 2>&1; then
fail "未找到 cargo,请先安装 Rust 工具链"
fi
configure_fast_linker
build_cargo_args
if [ "$CHECK_ONLY" -eq 1 ]; then
if [ "$BUILD_PROFILE" = "release" ]; then
log "检查后端 (cargo check --release ${CARGO_BASE_ARGS[*]} ${CARGO_FEATURE_ARGS[*]})..."
cargo check "${CARGO_BASE_ARGS[@]}" --release "${CARGO_FEATURE_ARGS[@]}"
elif [ "$BUILD_PROFILE" = "fast-release" ]; then
log "检查后端 (cargo check --profile fast-release ${CARGO_BASE_ARGS[*]} ${CARGO_FEATURE_ARGS[*]})..."
cargo check "${CARGO_BASE_ARGS[@]}" --profile fast-release "${CARGO_FEATURE_ARGS[@]}"
else
log "检查后端 (cargo check ${CARGO_BASE_ARGS[*]} ${CARGO_FEATURE_ARGS[*]})..."
cargo check "${CARGO_BASE_ARGS[@]}" "${CARGO_FEATURE_ARGS[@]}"
fi
ok "后端检查完成"
record_timing "后端检查" "$started"
return 0
fi
if [ "$BUILD_PROFILE" = "release" ]; then
log "构建后端 (cargo build --release ${CARGO_BASE_ARGS[*]} ${CARGO_FEATURE_ARGS[*]})..."
cargo build "${CARGO_BASE_ARGS[@]}" --release "${CARGO_FEATURE_ARGS[@]}"
BINARY="$ROOT_DIR/target/release/boosapi"
ok "后端构建完成 → target/release/boosapi"
elif [ "$BUILD_PROFILE" = "fast-release" ]; then
log "构建后端 (cargo build --profile fast-release ${CARGO_BASE_ARGS[*]} ${CARGO_FEATURE_ARGS[*]})..."
cargo build "${CARGO_BASE_ARGS[@]}" --profile fast-release "${CARGO_FEATURE_ARGS[@]}"
BINARY="$ROOT_DIR/target/fast-release/boosapi"
ok "后端构建完成 → target/fast-release/boosapi"
else
log "构建后端 (cargo build ${CARGO_BASE_ARGS[*]} ${CARGO_FEATURE_ARGS[*]})..."
cargo build "${CARGO_BASE_ARGS[@]}" "${CARGO_FEATURE_ARGS[@]}"
BINARY="$ROOT_DIR/target/debug/boosapi"
ok "后端构建完成 → target/debug/boosapi"
fi
record_timing "后端构建" "$started"
}
build_production_bundle() {
local started
started="$(timer_start)"
[ -n "${BINARY:-}" ] || fail "生产打包失败:后端二进制路径为空"
[ -f "$BINARY" ] || fail "生产打包失败:未找到二进制 $BINARY"
[ -f "$ROOT_DIR/config.toml" ] || fail "生产打包失败:未找到 config.toml"
rm -rf "$PRODUCTION_DIR"
mkdir -p "$PRODUCTION_DIR"
cp "$BINARY" "$PRODUCTION_DIR/boosapi"
cp "$ROOT_DIR/config.toml" "$PRODUCTION_DIR/config.toml"
chmod +x "$PRODUCTION_DIR/boosapi"
if command -v strip >/dev/null 2>&1; then
log "压缩生产二进制符号表 (strip)..."
strip "$PRODUCTION_DIR/boosapi" 2>/dev/null || log "strip 失败,保留未压缩二进制"
fi
ok "生产产物已生成 → $PRODUCTION_DIR"
log "产物内容:"
(cd "$PRODUCTION_DIR" && ls -lh boosapi config.toml)
log "部署时只需要复制该目录内的 boosapi 和 config.toml;主 Web 前端已嵌入二进制。"
record_timing "生产打包" "$started"
}
cleanup_started_server() {
if [ -n "${STARTED_PID:-}" ] && is_running_pid "$STARTED_PID"; then
log "停止本次启动的后端进程: $STARTED_PID"
kill -TERM "$STARTED_PID" 2>/dev/null || true
wait "$STARTED_PID" 2>/dev/null || true
fi
}
# ── 构建 / 启动 ────────────────────────────────────────────────────────────────
if [ "$FRONTEND_ONLY" -eq 1 ]; then
build_frontend
build_public_website
print_timing_summary
log "跳过启动(--frontend-only)"
exit 0
fi
if [ "$BACKEND_ONLY" -ne 1 ]; then
build_frontend
build_public_website
if [ "$PRODUCTION_BUNDLE" -eq 1 ]; then
ensure_workflow_ui_for_embedding
fi
fi
build_backend
if [ "$CHECK_ONLY" -eq 1 ]; then
print_timing_summary
log "跳过启动(--check)"
exit 0
fi
if [ "$PRODUCTION_BUNDLE" -eq 1 ]; then
build_production_bundle
print_timing_summary
exit 0
fi
if [ "$NO_RUN" -eq 1 ]; then
print_timing_summary
log "跳过启动(--no-run)"
exit 0
fi
print_timing_summary
stop_previous_server
log "启动服务..."
if [ "$BACKEND_ONLY" -eq 1 ]; then
exec "$BINARY"
fi
"$BINARY" &
STARTED_PID=$!
trap cleanup_started_server INT TERM EXIT
ok "后端已启动,PID: $STARTED_PID"
ok "前后端已就绪(profile: $BUILD_PROFILE),按 Ctrl+C 停止服务"
wait "$STARTED_PID"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 308 KiB

+304
View File
@@ -0,0 +1,304 @@
"""
BoosAPI ComfyUI Custom Nodes
让 ComfyUI workflow 调用 BoosAPI 的 AI 模型(图像生成、LLM 等)。
配置:通过环境变量设置 BoosAPI 地址,或使用默认 http://127.0.0.1:4000/v1
环境变量:
BOOSAPI_BASE — BoosAPI 地址(默认 http://127.0.0.1:4000)
BOOSAPI_KEY — API Key(默认 sk-boos4721)
"""
import json
import os
import requests
# ── Config ───────────────────────────────────────────────────────────────
BOOSAPI_BASE_RAW = os.environ.get("BOOSAPI_BASE", "http://127.0.0.1:4000")
BOOSAPI_KEY = os.environ.get("BOOSAPI_KEY", "sk-boos4721")
HEADERS = {
"Authorization": f"Bearer {BOOSAPI_KEY}",
"Content-Type": "application/json",
}
FALLBACK_CHAT_MODELS = ["gpt-5.5", "gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex", "gpt-5.2"]
FALLBACK_IMAGE_MODELS = ["gpt-image-2", "gpt-image-1", "dall-e-3", "dall-e-2"]
FALLBACK_ALL_MODELS = FALLBACK_IMAGE_MODELS + FALLBACK_CHAT_MODELS
def normalize_boosapi_root(base):
base = str(base or "http://127.0.0.1:4000").strip().rstrip("/")
if base.endswith("/v1"):
base = base[:-3].rstrip("/")
return base or "http://127.0.0.1:4000"
BOOSAPI_ROOT = normalize_boosapi_root(BOOSAPI_BASE_RAW)
BOOSAPI_V1 = f"{BOOSAPI_ROOT}/v1"
def raise_for_status_with_body(resp):
try:
resp.raise_for_status()
except requests.HTTPError as e:
body = (resp.text or "").strip()
if body:
print(f"[BoosAPI] HTTP {resp.status_code} response: {body[:2000]}")
raise requests.HTTPError(f"{e}; response body: {body[:1000]}", response=resp)
raise
def _ordered_unique(values):
seen = set()
result = []
for value in values:
value = str(value or "").strip()
if not value or value in seen:
continue
seen.add(value)
result.append(value)
return result
def fetch_boosapi_models():
"""Fetch models exposed by BoosAPI's own /v1/models route."""
try:
resp = requests.get(f"{BOOSAPI_V1}/models", headers=HEADERS, timeout=8)
resp.raise_for_status()
data = resp.json()
models = []
for item in data.get("data", []):
if isinstance(item, dict):
models.append(item.get("id") or item.get("name"))
elif isinstance(item, str):
models.append(item)
return _ordered_unique(models)
except Exception as e:
print(f"[BoosAPI] model list fallback: {e}")
return None
def model_choices(kind="all"):
models = fetch_boosapi_models()
if models is None:
if kind == "image":
return FALLBACK_IMAGE_MODELS
if kind == "chat":
return FALLBACK_CHAT_MODELS
return FALLBACK_ALL_MODELS
if kind == "image":
image_like = [
model for model in models
if any(token in model.lower() for token in ("image", "dall", "flux", "sd", "midjourney"))
]
return _ordered_unique(image_like + models) or FALLBACK_IMAGE_MODELS
if kind == "chat":
chat_like = [
model for model in models
if not any(token in model.lower() for token in ("image", "dall", "flux", "sd", "midjourney"))
]
return _ordered_unique(chat_like + models) or FALLBACK_CHAT_MODELS
return _ordered_unique(models) or FALLBACK_ALL_MODELS
def default_model(kind="all"):
choices = model_choices(kind)
return choices[0] if choices else FALLBACK_ALL_MODELS[0]
# ── Node Mappings ────────────────────────────────────────────────────────
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
# ═════════════════════════════════════════════════════════════════════════
# Node: BoosAPIImageGen — 图像生成 (OpenAI DALL-E / 兼容格式)
# ═════════════════════════════════════════════════════════════════════════
class BoosAPIImageGen:
"""调用 BoosAPI /v1/images/generations 生成图像"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"multiline": True, "default": ""}),
"model": (model_choices("image"), {"default": default_model("image")}),
"size": (["1024x1024", "1792x1024", "1024x1792"], {"default": "1024x1024"}),
"quality": (["standard", "hd"], {"default": "standard"}),
"n": ("INT", {"default": 1, "min": 1, "max": 4}),
},
"optional": {
"style": (["vivid", "natural"], {"default": "vivid"}),
},
}
RETURN_TYPES = ("IMAGE", "STRING")
RETURN_NAMES = ("image", "metadata")
FUNCTION = "generate"
OUTPUT_NODE = True
CATEGORY = "BoosAPI"
def generate(self, prompt, model, size, quality, n, style="vivid"):
url = f"{BOOSAPI_V1}/images/generations"
payload = {
"model": model,
"prompt": prompt,
"n": n,
"size": size,
"quality": quality,
"style": style,
"response_format": "b64_json",
}
try:
resp = requests.post(url, headers=HEADERS, json=payload, timeout=120)
raise_for_status_with_body(resp)
data = resp.json()
images = data.get("data", [])
# Return first image as b64
from io import BytesIO
import base64
import torch
from PIL import Image
b64_data = images[0]["b64_json"]
img_bytes = base64.b64decode(b64_data)
pil_img = Image.open(BytesIO(img_bytes)).convert("RGB")
img_tensor = torch.from_numpy(
__import__("numpy").array(pil_img).astype("float32") / 255.0
).unsqueeze(0)
metadata = json.dumps({
"model": model,
"size": size,
"revised_prompt": data.get("data", [{}])[0].get("revised_prompt", ""),
})
return (img_tensor, metadata)
except Exception as e:
print(f"[BoosAPI] ImageGen failed: {e}")
raise
# ═════════════════════════════════════════════════════════════════════════
# Node: BoosAPILLM — LLM Chat (流式文本生成)
# ═════════════════════════════════════════════════════════════════════════
class BoosAPILLM:
"""调用 BoosAPI /v1/chat/completions 生成文本"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"multiline": True, "default": ""}),
"model": (model_choices("chat"), {"default": default_model("chat")}),
"system_prompt": ("STRING", {"multiline": True, "default": ""}),
"temperature": ("FLOAT", {"default": 0.7, "min": 0.0, "max": 2.0}),
"max_tokens": ("INT", {"default": 2048, "min": 1, "max": 16384}),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("text",)
FUNCTION = "chat"
OUTPUT_NODE = True
CATEGORY = "BoosAPI"
def chat(self, prompt, model, system_prompt, temperature, max_tokens):
url = f"{BOOSAPI_V1}/chat/completions"
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
messages.append({"role": "user", "content": prompt})
payload = {
"model": model,
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens,
"stream": False,
}
try:
resp = requests.post(url, headers=HEADERS, json=payload, timeout=120)
raise_for_status_with_body(resp)
data = resp.json()
text = data["choices"][0]["message"]["content"]
return {"ui": {"text": (text,)}, "result": (text,)}
except Exception as e:
print(f"[BoosAPI] LLM chat failed: {e}")
raise
# ═════════════════════════════════════════════════════════════════════════
# Node: BoosAPITextToImage — 文生图 (ComfyUI 原生格式)
# ═════════════════════════════════════════════════════════════════════════
class BoosAPITextToImage:
"""输入文本提示词,通过 BoosAPI 生成图像,输出 ComfyUI IMAGE 格式"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"positive": ("STRING", {"multiline": True, "default": ""}),
"model": (model_choices("image"), {"default": default_model("image")}),
},
"optional": {
"negative": ("STRING", {"multiline": True, "default": ""}),
"size": (["1024x1024", "1792x1024", "1024x1792"], {"default": "1024x1024"}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "generate"
OUTPUT_NODE = True
CATEGORY = "BoosAPI"
def generate(self, positive, model, negative="", size="1024x1024"):
# Build prompt with negative guidance
prompt = positive
if negative:
prompt = f"{positive}\n避免: {negative}"
url = f"{BOOSAPI_V1}/images/generations"
payload = {
"model": model,
"prompt": prompt,
"n": 1,
"size": size,
"response_format": "b64_json",
}
try:
resp = requests.post(url, headers=HEADERS, json=payload, timeout=120)
raise_for_status_with_body(resp)
data = resp.json()
import base64
from io import BytesIO
import torch
from PIL import Image
import numpy as np
b64 = data["data"][0]["b64_json"]
img = Image.open(BytesIO(base64.b64decode(b64))).convert("RGB")
tensor = torch.from_numpy(np.array(img).astype("float32") / 255.0).unsqueeze(0)
return (tensor,)
except Exception as e:
print(f"[BoosAPI] T2I failed: {e}")
raise
# ═════════════════════════════════════════════════════════════════════════
# Register Nodes
# ═════════════════════════════════════════════════════════════════════════
NODE_CLASS_MAPPINGS = {
"BoosAPIImageGen": BoosAPIImageGen,
"BoosAPILLM": BoosAPILLM,
"BoosAPITextToImage": BoosAPITextToImage,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"BoosAPIImageGen": "BoosAPI 图像生成",
"BoosAPILLM": "BoosAPI LLM 对话",
"BoosAPITextToImage": "BoosAPI 文生图",
}
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
+76
View File
@@ -0,0 +1,76 @@
[server]
host = "0.0.0.0"
port = 4000
log_level = "info"
cors_origins = ["*"]
[db]
url = "sqlite:boosapi.db?mode=rwc"
max_connections = 10
ssl = false
# PostgreSQL preparation example:
# url = "postgres://boosapi:password@127.0.0.1:5432/boosapi"
# Note: this build can create/migrate PostgreSQL schemas and copy SQLite data
# with `boosapi migrate-sqlite-to-postgres`; full runtime query compatibility
# is completed in a follow-up SQL dialect pass.
[auth]
jwt_secret = "boosapi-dev-secret-change-in-production"
jwt_expiry_hours = 72
auth_token = "boos4721"
proxy_token = "sk-boos4721"
[proxy]
upstream_timeout_secs = 300
first_byte_timeout_secs = 60
max_retries = 3
proxy_log_retention_days = 30
# Optional proxy for external template galleries/workflow template assets.
# Supports http://, https:// and socks5:// proxy URLs.
# template_proxy_url = "http://127.0.0.1:7890"
[payment]
enabled = false
# Public HTTPS origin used to build Stripe success/cancel URLs and webhook docs.
# Example: "https://tybossai.com"
public_base_url = ""
product_name_prefix = "BossAI 账户充值"
currency = "usd"
min_amount_usd = 1.0
max_amount_usd = 100000.0
order_timeout_minutes = 30
max_pending_orders = 3
success_path = "/user/recharge?payment=success&order={ORDER_ID}"
cancel_path = "/user/recharge?payment=cancel&order={ORDER_ID}"
# Prefer environment variables for secrets:
# BOOSAPI_PAYMENT__STRIPE_SECRET_KEY=sk_live_...
# BOOSAPI_PAYMENT__STRIPE_WEBHOOK_SECRET=whsec_...
stripe_secret_key = ""
stripe_webhook_secret = ""
[comfyui]
enabled = false
python_bin = "python3"
port = 8188
extra_args = []
max_restarts = 5
# backend modes:
# - "boosapi": use BoosAPI's own Rust Workflow/API node executor.
# - "cloud": proxy to Comfy Cloud and use native ComfyUI /prompt and /ws.
# - "runninghub": proxy to RunningHub's ComfyUI-compatible API with external-compatible frontend.
# - "external": proxy to your own full ComfyUI server.
# - "docker": legacy local Docker ComfyUI mode; not recommended for the current API Workflow path.
backend = "cloud"
# Native Comfy task costs are charged against the user's downstream API Key
# only after successful output archival. Keep 0 while pricing is not finalized.
money_printer_video_cost = 0.0
digital_human_video_cost = 0.0
# docker_image = "boosapi-comfyui:multi-tenant"
# container_name = "boosapi-comfyui-workflow"
# base_url = "https://www.runninghub.cn/proxy-plus/94d1330ea62d4ae980ffb018ecdf82a2"
# For RunningHub, set backend = "runninghub" and use your RunningHub proxy/API URL.
# RunningHub proxy URL already contains the API key. Keep this empty unless
# you switch to a backend that requires Authorization / X-API-Key headers.
base_url = "https://cloud.comfy.org"
api_key = "comfyui-cbe4ebb1e22ee4ef2dab4d9293a76d5272a25e5ec847ac57387ab2b30798d86f"
agent_model = "gpt-5.5"
-6
View File
@@ -1,6 +0,0 @@
apiVersion: v2
name: boosapi
description: Minimal boosapi chart for digest-aware k3s update-center setups
type: application
version: 0.1.0
appVersion: "1.3.0"
-27
View File
@@ -1,27 +0,0 @@
{{- define "metapi.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- define "metapi.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := include "metapi.name" . -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- define "metapi.envSecretName" -}}
{{- printf "%s-env" (include "metapi.fullname" .) | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- define "metapi.labels" -}}
app.kubernetes.io/name: {{ include "metapi.name" . }}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}
@@ -1,59 +0,0 @@
{{- if and .Values.persistence.hostPath (gt (int .Values.replicaCount) 1) -}}
{{- fail "deploy/k3s/chart uses node-local hostPath storage; keep replicaCount at 1 or switch persistence.hostPath to shared storage before scaling" -}}
{{- end }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "boosapi.fullname" . }}
labels:
{{- include "boosapi.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
revisionHistoryLimit: 2
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: {{ .Values.deployment.strategy.maxSurge }}
maxUnavailable: {{ .Values.deployment.strategy.maxUnavailable }}
selector:
matchLabels:
app.kubernetes.io/name: {{ include "boosapi.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
template:
metadata:
labels:
app.kubernetes.io/name: {{ include "boosapi.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
annotations:
boosapi/image-tag: {{ .Values.image.tag | quote }}
boosapi/image-digest: {{ .Values.image.digest | quote }}
checksum/env-secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum | quote }}
spec:
containers:
- name: boosapi
image: "{{ .Values.image.repository }}{{ if .Values.image.digest }}@{{ .Values.image.digest }}{{ else }}:{{ .Values.image.tag }}{{ end }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
envFrom:
- secretRef:
name: {{ include "boosapi.envSecretName" . }}
ports:
- name: http
containerPort: 4000
readinessProbe:
tcpSocket:
port: http
initialDelaySeconds: 10
periodSeconds: 10
livenessProbe:
tcpSocket:
port: http
initialDelaySeconds: 30
periodSeconds: 20
volumeMounts:
- name: boosapi-data
mountPath: /app/data
volumes:
- name: boosapi-data
hostPath:
path: {{ .Values.persistence.hostPath }}
type: DirectoryOrCreate
-23
View File
@@ -1,23 +0,0 @@
apiVersion: v1
kind: Secret
metadata:
name: {{ include "boosapi.envSecretName" . }}
labels:
{{- include "boosapi.labels" . | nindent 4 }}
type: Opaque
stringData:
AUTH_TOKEN: {{ required "env.authToken is required" .Values.env.authToken | quote }}
PROXY_TOKEN: {{ required "env.proxyToken is required" .Values.env.proxyToken | quote }}
DB_TYPE: {{ .Values.env.dbType | quote }}
{{- if eq (lower (toString .Values.env.dbType)) "sqlite" }}
DB_URL: {{ default "./data/hub.db" .Values.env.dbUrl | quote }}
{{- else }}
DB_URL: {{ required "env.dbUrl is required when env.dbType is mysql/postgres" .Values.env.dbUrl | quote }}
{{- end }}
DB_SSL: {{ .Values.env.dbSsl | quote }}
DATA_DIR: {{ .Values.env.dataDir | quote }}
PORT: {{ .Values.env.port | quote }}
TZ: {{ .Values.env.tz | quote }}
CHECKIN_CRON: {{ .Values.env.checkinCron | quote }}
BALANCE_REFRESH_CRON: {{ .Values.env.balanceRefreshCron | quote }}
DEPLOY_HELPER_TOKEN: {{ .Values.env.deployHelperToken | quote }}
-18
View File
@@ -1,18 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "boosapi.fullname" . }}
labels:
{{- include "boosapi.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
selector:
app.kubernetes.io/name: {{ include "boosapi.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
ports:
- name: http
port: {{ .Values.service.port }}
targetPort: http
{{- if and (or (eq .Values.service.type "NodePort") (eq .Values.service.type "LoadBalancer")) .Values.service.nodePort }}
nodePort: {{ .Values.service.nodePort }}
{{- end }}
-33
View File
@@ -1,33 +0,0 @@
replicaCount: 1
image:
repository: 1467078763/boosapi
tag: latest
digest: ""
pullPolicy: Always
service:
type: NodePort
port: 4000
nodePort: 30400
deployment:
strategy:
maxSurge: 1
maxUnavailable: 0
persistence:
hostPath: /opt/1panel/docker/compose/boosapihub/data
env:
authToken: ""
proxyToken: ""
dbType: postgres
dbUrl: ""
dbSsl: "false"
dataDir: /app/data
port: "4000"
tz: Asia/Shanghai
checkinCron: "0 8 * * *"
balanceRefreshCron: "0 * * * *"
deployHelperToken: ""
-123
View File
@@ -1,123 +0,0 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: boosapi-deploy-helper
namespace: ai
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: boosapi-deploy-helper-admin
namespace: ai
subjects:
- kind: ServiceAccount
name: boosapi-deploy-helper
namespace: ai
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: admin
---
apiVersion: v1
kind: Secret
metadata:
name: boosapi-deploy-helper-auth
namespace: ai
type: Opaque
stringData:
DEPLOY_HELPER_TOKEN: replace-with-a-shared-long-random-token
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: boosapi-deploy-helper
namespace: ai
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: boosapi-deploy-helper
template:
metadata:
labels:
app.kubernetes.io/name: boosapi-deploy-helper
spec:
serviceAccountName: boosapi-deploy-helper
containers:
- name: helper
image: 1467078763/boosapi:latest
imagePullPolicy: Always
command: ["/bin/sh", "-lc"]
args:
- |
TOKEN="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)"
NAMESPACE="$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)"
cat >/tmp/kubeconfig <<EOF
apiVersion: v1
kind: Config
clusters:
- name: in-cluster
cluster:
certificate-authority: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
server: https://kubernetes.default.svc
contexts:
- name: in-cluster
context:
cluster: in-cluster
namespace: ${NAMESPACE}
user: helper
current-context: in-cluster
users:
- name: helper
user:
token: ${TOKEN}
EOF
export KUBECONFIG=/tmp/kubeconfig
exec npm run start:deploy-helper
ports:
- containerPort: 9850
name: http
env:
- name: DEPLOY_HELPER_HOST
value: 0.0.0.0
- name: DEPLOY_HELPER_PORT
value: "9850"
- name: DEPLOY_HELPER_TOKEN
valueFrom:
secretKeyRef:
name: boosapi-deploy-helper-auth
key: DEPLOY_HELPER_TOKEN
readinessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 15
periodSeconds: 20
volumeMounts:
- name: boosapi-chart
mountPath: /opt/boosapi-k3s/chart
readOnly: true
volumes:
- name: boosapi-chart
hostPath:
path: /opt/boosapi-k3s/chart
type: Directory
---
apiVersion: v1
kind: Service
metadata:
name: boosapi-deploy-helper
namespace: ai
spec:
selector:
app.kubernetes.io/name: boosapi-deploy-helper
ports:
- name: http
port: 9850
targetPort: http
+15 -92
View File
@@ -1,95 +1,18 @@
FROM node:22-bookworm-slim AS boosapi-builder # Build stage
FROM rust:1.85-slim-bookworm AS builder
WORKDIR /app WORKDIR /app
RUN echo 'Acquire::http::Proxy "false";' > /etc/apt/apt.conf.d/99no-proxy && apt-get update && apt-get install -y --no-install-recommends python3 make g++ && rm -rf /var/lib/apt/lists/* RUN apt-get update && apt-get install -y pkg-config libssl-dev
ENV PYTHON=/usr/bin/python3 COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs
COPY package.json package-lock.json ./ RUN cargo build --release 2>/dev/null || true
RUN npm ci --ignore-scripts --no-audit --no-fund
RUN npm rebuild esbuild sharp better-sqlite3 --no-audit --no-fund
COPY . . COPY . .
RUN npm run build:web RUN cargo build --release
RUN npx tsc -p tsconfig.json --outDir dist --rootDir src --skipLibCheck --noEmit false 2>/dev/null; true
RUN ls dist/server/index.js 2>/dev/null || (echo "=== Server build still missing, trying project reference build ===" && npx tsc -p tsconfig.server.json --skipLibCheck --noEmit false 2>/dev/null; true)
RUN npm prune --omit=dev --no-audit --no-fund
# ---- Final image ---- # Runtime stage
FROM ubuntu:24.04 FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
ARG HTTP_PROXY WORKDIR /app
ARG HTTPS_PROXY COPY --from=builder /app/target/release/boosapi /app/
ARG ALL_PROXY COPY --from=builder /app/migrations /app/migrations
ARG NO_PROXY EXPOSE 4000
CMD ["/app/boosapi"]
ENV DEBIAN_FRONTEND=noninteractive
ENV NODE_ENV=production
ENV COMFYUI_BASE=http://127.0.0.1:8188
ENV HTTP_PROXY=${HTTP_PROXY}
ENV HTTPS_PROXY=${HTTPS_PROXY}
ENV ALL_PROXY=${ALL_PROXY}
ENV NO_PROXY=${NO_PROXY}
# Install system deps: Node.js 22, Python 3, supervisor
RUN echo 'Acquire::http::Proxy "false";' > /etc/apt/apt.conf.d/99no-proxy && apt-get update && apt-get install -y --no-install-recommends \
curl ca-certificates gnupg \
python3 python3-pip python3-venv \
supervisor git \
&& curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/*
# Install RTK from pre-built binary
COPY rtk /usr/local/bin/rtk
RUN chmod +x /usr/local/bin/rtk
# Set up ComfyUI (workflow editor frontend, calls BoosAPI API via custom nodes)
# CPU PyTorch is fine — all generation goes through BoosAPI's API
WORKDIR /app/comfy
RUN git clone https://github.com/comfyanonymous/ComfyUI.git . \
&& python3 -m venv venv \
&& . venv/bin/activate \
&& pip install --upgrade pip \
&& pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu \
&& grep -v kornia_rs requirements.txt | pip install -r /dev/stdin \
&& rm -rf ~/.cache/pip
ENV PATH="/app/comfy/venv/bin:${PATH}"
# Copy boosapi (built)
WORKDIR /app/boosapi
COPY --from=boosapi-builder /app/dist ./dist
COPY --from=boosapi-builder /app/node_modules ./node_modules
COPY --from=boosapi-builder /app/package.json ./
COPY --from=boosapi-builder /app/drizzle ./drizzle
RUN mkdir -p data
# Copy BoosAPI custom nodes for ComfyUI (call local BoosAPI API)
COPY --from=boosapi-builder /app/boosapi_comfy_nodes.py /app/comfy/custom_nodes/boosapi_comfy_nodes.py
# Supervisor config
RUN mkdir -p /var/log/supervisor
COPY <<'SUPERVISOR' /etc/supervisor/conf.d/all.conf
[supervisord]
nodaemon=true
user=root
logfile=/var/log/supervisor/supervisord.log
pidfile=/tmp/supervisord.pid
[program:boosapi]
command=sh -c "node dist/server/db/migrate.js && node dist/server/index.js"
directory=/app/boosapi
autorestart=true
stdout_logfile=/var/log/supervisor/boosapi.log
stderr_logfile=/var/log/supervisor/boosapi.err
[program:comfyui]
command=python3 main.py --listen 0.0.0.0 --port 8188 --cpu
directory=/app/comfy
autorestart=true
startretries=3
stdout_logfile=/var/log/supervisor/comfyui.log
stderr_logfile=/var/log/supervisor/comfyui.err
SUPERVISOR
EXPOSE 4000 8188
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/all.conf"]
+33
View File
@@ -0,0 +1,33 @@
FROM boosapi-comfyui:multi-tenant
ARG COMFYUI_TAG=v0.22.2
WORKDIR /workspace/ComfyUI
COPY docker/build-comfyui-src /tmp/ComfyUI-updated
RUN find /workspace/ComfyUI -mindepth 1 \
! -path '/workspace/ComfyUI/venv' \
! -path '/workspace/ComfyUI/venv/*' \
-exec rm -rf {} + \
&& cp -a /tmp/ComfyUI-updated/. /workspace/ComfyUI/ \
&& rm -rf /tmp/ComfyUI-updated \
&& UV_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple \
UV_EXTRA_INDEX_URL=https://mirrors.tuna.tsinghua.edu.cn/pytorch-wheels/cpu/ \
uv pip install --python venv/bin/python -r requirements.txt \
&& UV_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple \
uv pip install --python venv/bin/python six \
&& mkdir -p /workspace/ComfyUI/custom_nodes/boosapi_nodes
COPY comfy/boosapi_comfy_nodes.py /workspace/ComfyUI/custom_nodes/boosapi_nodes/__init__.py
COPY scripts/comfyui/install-runninghub-template-nodes.sh /tmp/install-runninghub-template-nodes.sh
RUN bash /tmp/install-runninghub-template-nodes.sh \
&& rm -f /tmp/install-runninghub-template-nodes.sh
RUN python - <<'PY'
from pathlib import Path
path = Path("/workspace/ComfyUI/custom_nodes/boosapi_nodes/__init__.py")
if not path.exists():
raise SystemExit("BoosAPI custom node missing after ComfyUI update")
PY
Binary file not shown.
-16
View File
@@ -1,16 +0,0 @@
services:
boosapi:
image: 1467078763/boosapi:latest
ports:
- "127.0.0.1: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
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.
-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

Some files were not shown because too many files have changed in this diff Show More