diff --git a/LOCAL_SETUP_NOTES.md b/LOCAL_SETUP_NOTES.md new file mode 100644 index 00000000..b215db01 --- /dev/null +++ b/LOCAL_SETUP_NOTES.md @@ -0,0 +1,168 @@ +# Sub2API 本地开发环境搭建记录 + +> 2026-04-19 @ macOS (darwin/arm64) + +## 一、环境依赖 + +| 项 | 版本 | 说明 | +|---|---|---| +| Go | 1.24.3(本机)→ 1.26.2(auto via GOTOOLCHAIN) | go.mod 要求 1.26.2,靠 Go 1.21+ 的 GOTOOLCHAIN 机制自动下载 | +| Node | v24.13.0 | ≥18 即可 | +| pnpm | v10.33.0 | 现装:`npm install -g pnpm` | +| Docker | OrbStack | 已跑 mysql8@3306,端口冲突规避见下 | +| 端口占用 | 8080 / 5433 / 6380 均空闲 | 5432/6379 留给可能的其他 PG/Redis | + +## 二、部署步骤(实际执行) + +### 1. 拉源码 +```bash +git clone https://github.com/Wei-Shaw/sub2api.git /Users/mini/Work/dev/sub2api +``` + +### 2. 起依赖容器(非默认端口) +```bash +docker run -d --name sub2api-pg \ + -e POSTGRES_PASSWORD=devpass -e POSTGRES_DB=sub2api \ + -p 5433:5432 postgres:15 + +docker run -d --name sub2api-redis \ + -p 6380:6379 redis:7 +``` + +### 3. 安装 pnpm(首次) +```bash +npm install -g pnpm +``` + +### 4. 构建前端 +```bash +cd /Users/mini/Work/dev/sub2api/frontend +pnpm install # ~9 秒 +pnpm run build # ~8 秒,产物输出到 ../backend/internal/web/dist/ +``` + +### 5. 构建后端(⚠️ 必带 `-tags embed`) +```bash +cd /Users/mini/Work/dev/sub2api/backend +go build -tags embed -o sub2api ./cmd/server +# 产物:105MB Mach-O 64-bit arm64 +``` + +### 6. 生成 config.yaml +```bash +cp /Users/mini/Work/dev/sub2api/deploy/config.example.yaml \ + /Users/mini/Work/dev/sub2api/backend/config.yaml +``` + +修改四处(见下方问题点 Issue #1 说明 sslmode): + +| 字段 | 原值 | 改为 | +|---|---|---| +| `database.port` | 5432 | **5433** | +| `database.password` | `"your_secure_password_here"` | `"devpass"` | +| `database.sslmode` | `"prefer"` | **`"disable"`** | +| `redis.port` | 6379 | **6380** | +| `jwt.secret` | `"change-this-..."` | `openssl rand -hex 32` 产出的 64 位 hex | + +### 7. 启动 + 验证 +```bash +cd /Users/mini/Work/dev/sub2api/backend +nohup ./sub2api > /tmp/sub2api.log 2>&1 & +# 等 5-10 秒让服务完成 pricing 数据下载 +curl -si http://localhost:8080 | head +# 期望:HTTP/1.1 200 OK,HTML 含 Sub2API - AI API Gateway +``` + +--- + +## 三、遇到的问题与解法 + +### Issue #1 — `sslmode: "prefer"` 启动失败 +**现象**:后端启动立即退出,日志: +``` +Failed to initialize application: acquire migrations lock: +pq: unsupported sslmode "prefer"; only "require" (default), +"verify-full", "verify-ca", and "disable" supported +``` + +**根因**:`config.example.yaml` 默认的 `sslmode: "prefer"` 是 libpq(C 驱动)的模式,Go 的 `lib/pq` 不支持。 + +**解法**:本地 Docker Postgres 没配 SSL,改成 `disable`: +```yaml +database: + sslmode: "disable" +``` +生产若走带 SSL 的 PG,用 `require` 或 `verify-full`。 + +--- + +### Issue #2 — go.mod 要求 Go 1.26.2,本机只有 1.24.3 +**现象**:首次 `go build` 触发: +``` +go: downloading go1.26.2 (darwin/arm64) +``` + +**根因**:`backend/go.mod` 第一行 `go 1.26.2` 写死。 + +**解法**:**无需手动升级 Go**。Go 1.21+ 的 GOTOOLCHAIN 机制会自动下载指定版本并透明切换。首次 build 比较慢(下载 toolchain + 全部依赖),后续会缓存。 + +--- + +### Issue #3 — frontend 构建产物路径是相对路径到 backend +**现象**:`pnpm run build` 的日志显示产物写到 `../backend/internal/web/dist/`。 + +**说明**:**这是预期行为**。Vite 配置把输出指向 backend 的 embed 目录,配合 `go build -tags embed` 把 dist 打进 Go 二进制。所以: +- 每次改前端代码都要重新 `pnpm run build` 然后 `go build -tags embed` +- 如果 `go build` 时忘了 `-tags embed`,后端启动后访问 `/` 会 404 + +--- + +### Issue #4 — 日志文件 `/app/data/logs/sub2api.log` 写入失败 +**现象**:启动日志里有 WARN: +``` +日志文件输出初始化失败,降级为仅标准输出 +path=/app/data/logs/sub2api.log err=mkdir /app: read-only file system +``` + +**根因**:默认配置指向容器内路径 `/app/data/logs/`,本地裸跑在 macOS 上 `/app` 不可写。 + +**影响**:无功能影响,只是降级到 stdout。我们用 `nohup ./sub2api > /tmp/sub2api.log 2>&1 &` 已经把 stdout 重定向了,日志照样完整。 + +**若要消除 WARN**:修改 config 里 `logging.file_path`(或等同字段)指向本地可写路径,如 `/tmp/sub2api/logs/sub2api.log`,并 `mkdir -p` 目录。 + +--- + +## 四、当前状态 + +``` +Backend PID: 26921 +HTTP: 200 @ http://localhost:8080 +页面: Sub2API - AI API Gateway(Setup Wizard 入口) +Pricing: 已下载 177 个模型价格 +Containers: sub2api-pg (Up), sub2api-redis (Up) +``` + +--- + +## 五、下次重启命令 + +```bash +# 启动依赖(容器如果 stopped) +docker start sub2api-pg sub2api-redis + +# 启动后端 +cd /Users/mini/Work/dev/sub2api/backend +nohup ./sub2api > /tmp/sub2api.log 2>&1 & + +# 停止 +pkill -f "/Users/mini/Work/dev/sub2api/backend/sub2api" +docker stop sub2api-pg sub2api-redis +``` + +## 六、清理重来 + +```bash +pkill -f "/Users/mini/Work/dev/sub2api/backend/sub2api" +docker rm -f sub2api-pg sub2api-redis +rm -rf /Users/mini/Work/dev/sub2api +``` diff --git a/backend/config.prod.yaml b/backend/config.prod.yaml new file mode 100644 index 00000000..f50437d5 --- /dev/null +++ b/backend/config.prod.yaml @@ -0,0 +1,1064 @@ +# Sub2API Configuration File +# Sub2API 配置文件 +# +# Copy this file to /etc/sub2api/config.yaml and modify as needed +# 复制此文件到 /etc/sub2api/config.yaml 并根据需要修改 +# +# Documentation / 文档: https://github.com/Wei-Shaw/sub2api + +# ============================================================================= +# Server Configuration +# 服务器配置 +# ============================================================================= +server: + # Bind address (0.0.0.0 for all interfaces) + # 绑定地址(0.0.0.0 表示监听所有网络接口) + host: "0.0.0.0" + # Port to listen on + # 监听端口 + port: 8080 + # Mode: "debug" for development, "release" for production + # 运行模式:"debug" 用于开发,"release" 用于生产环境 + mode: "release" + # Frontend base URL used to generate external links in emails (e.g. password reset) + # 用于生成邮件中的外部链接(例如:重置密码链接)的前端基础地址 + # Example: "https://example.com" + frontend_url: "https://ai.puro.im" + # Trusted proxies for X-Forwarded-For parsing (CIDR/IP). Empty disables trusted proxies. + # 信任的代理地址(CIDR/IP 格式),用于解析 X-Forwarded-For 头。留空则禁用代理信任。 + trusted_proxies: + - "127.0.0.1/32" + - "::1/128" + - "172.16.0.0/12" + # Global max request body size in bytes (default: 256MB) + # 全局最大请求体大小(字节,默认 256MB) + # Applies to all requests, especially important for h2c first request memory protection + # 适用于所有请求,对 h2c 第一请求的内存保护尤为重要 + max_request_body_size: 268435456 + # HTTP/2 Cleartext (h2c) configuration + # HTTP/2 Cleartext (h2c) 配置 + h2c: + # Enable HTTP/2 Cleartext for client connections + # 启用 HTTP/2 Cleartext 客户端连接 + enabled: true + # Max concurrent streams per connection + # 每个连接的最大并发流数量 + max_concurrent_streams: 50 + # Idle timeout for connections (seconds) + # 连接空闲超时时间(秒) + idle_timeout: 75 + # Max frame size in bytes (default: 1MB) + # 最大帧大小(字节,默认 1MB) + max_read_frame_size: 1048576 + # Max upload buffer per connection in bytes (default: 2MB) + # 每个连接的最大上传缓冲区(字节,默认 2MB) + max_upload_buffer_per_connection: 2097152 + # Max upload buffer per stream in bytes (default: 512KB) + # 每个流的最大上传缓冲区(字节,默认 512KB) + max_upload_buffer_per_stream: 524288 + +# ============================================================================= +# Run Mode Configuration +# 运行模式配置 +# ============================================================================= +# Run mode: "standard" (default) or "simple" (for internal use) +# 运行模式:"standard"(默认)或 "simple"(内部使用) +# - standard: Full SaaS features with billing/balance checks +# - standard: 完整 SaaS 功能,包含计费和余额校验 +# - simple: Hides SaaS features and skips billing/balance checks +# - simple: 隐藏 SaaS 功能,跳过计费和余额校验 +run_mode: "standard" + +# ============================================================================= +# CORS Configuration +# 跨域资源共享 (CORS) 配置 +# ============================================================================= +cors: + # Allowed origins list. Leave empty to disable cross-origin requests. + # 允许的来源列表。留空则禁用跨域请求。 + allowed_origins: [] + # Allow credentials (cookies/authorization headers). Cannot be used with "*". + # 允许携带凭证(cookies/授权头)。不能与 "*" 通配符同时使用。 + allow_credentials: true + +# ============================================================================= +# Security Configuration +# 安全配置 +# ============================================================================= +security: + url_allowlist: + # Enable URL allowlist validation (disable to skip all URL checks) + # 启用 URL 白名单验证(禁用则跳过所有 URL 检查) + enabled: false + # Allowed upstream hosts for API proxying + # 允许代理的上游 API 主机列表 + upstream_hosts: + - "api.openai.com" + - "api.anthropic.com" + - "api.kimi.com" + - "open.bigmodel.cn" + - "api.minimaxi.com" + - "generativelanguage.googleapis.com" + - "cloudcode-pa.googleapis.com" + - "*.openai.azure.com" + # Allowed hosts for pricing data download + # 允许下载定价数据的主机列表 + pricing_hosts: + - "raw.githubusercontent.com" + # Allowed hosts for CRS sync (required when using CRS sync) + # 允许 CRS 同步的主机列表(使用 CRS 同步功能时必须配置) + crs_hosts: [] + # Allow localhost/private IPs for upstream/pricing/CRS (use only in trusted networks) + # 允许本地/私有 IP 地址用于上游/定价/CRS(仅在可信网络中使用) + allow_private_hosts: true + # Allow http:// URLs when allowlist is disabled (default: false, require https) + # 白名单禁用时是否允许 http:// URL(默认: false,要求 https) + allow_insecure_http: true + response_headers: + # Enable configurable response header filtering (default: true) + # 启用可配置的响应头过滤(默认启用,过滤上游敏感响应头) + enabled: true + # Extra allowed response headers from upstream + # 额外允许的上游响应头 + additional_allowed: [] + # Force-remove response headers from upstream + # 强制移除的上游响应头 + force_remove: [] + csp: + # Enable Content-Security-Policy header + # 启用内容安全策略 (CSP) 响应头 + enabled: true + # Default CSP policy (override if you host assets on other domains) + # 默认 CSP 策略(如果静态资源托管在其他域名,请自行覆盖) + # Note: __CSP_NONCE__ will be replaced with 'nonce-xxx' at request time for inline script security + # 注意:__CSP_NONCE__ 会在请求时被替换为 'nonce-xxx',用于内联脚本安全 + policy: "default-src 'self'; script-src 'self' __CSP_NONCE__ https://challenges.cloudflare.com https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https:; frame-src https://challenges.cloudflare.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" + proxy_probe: + # Allow skipping TLS verification for proxy probe (debug only) + # 允许代理探测时跳过 TLS 证书验证(仅用于调试) + insecure_skip_verify: false + proxy_fallback: + # Allow auxiliary services (update check, pricing data) to fallback to direct + # connection when proxy initialization fails. Does NOT affect AI gateway connections. + # 辅助服务(更新检查、定价数据拉取)代理初始化失败时是否允许回退直连。 + # 不影响 AI 账号网关连接。默认 false:fail-fast 防止 IP 泄露。 + allow_direct_on_error: false + +# ============================================================================= +# Gateway Configuration +# 网关配置 +# ============================================================================= +gateway: + # Timeout for waiting upstream response headers (seconds) + # 等待上游响应头超时时间(秒) + response_header_timeout: 600 + # Max request body size in bytes (default: 256MB) + # 请求体最大字节数(默认 256MB) + max_body_size: 268435456 + # Max bytes to read for non-stream upstream responses (default: 8MB) + # 非流式上游响应体读取上限(默认 8MB) + upstream_response_read_max_bytes: 8388608 + # Max bytes to read for proxy probe responses (default: 1MB) + # 代理探测响应体读取上限(默认 1MB) + proxy_probe_response_read_max_bytes: 1048576 + # Enable Gemini upstream response header debug logs (default: false) + # 是否开启 Gemini 上游响应头调试日志(默认 false) + gemini_debug_response_headers: false + # Sora max request body size in bytes (0=use max_body_size) + # Sora 请求体最大字节数(0=使用 max_body_size) + sora_max_body_size: 268435456 + # Sora stream timeout (seconds, 0=disable) + # Sora 流式请求总超时(秒,0=禁用) + sora_stream_timeout_seconds: 900 + # Sora non-stream timeout (seconds, 0=disable) + # Sora 非流式请求超时(秒,0=禁用) + sora_request_timeout_seconds: 180 + # Sora stream enforcement mode: force/error + # Sora stream 强制策略:force/error + sora_stream_mode: "force" + # Sora model filters + # Sora 模型过滤配置 + sora_model_filters: + # Hide prompt-enhance models by default + # 默认隐藏 prompt-enhance 模型 + hide_prompt_enhance: true + # Require API key for /sora/media proxy (default: false) + # /sora/media 是否强制要求 API Key(默认 true) + sora_media_require_api_key: true + # Sora media temporary signing key (empty disables signed URL) + # Sora 媒体临时签名密钥(为空则禁用签名) + sora_media_signing_key: "" + # Signed URL TTL seconds (<=0 disables) + # 临时签名 URL 有效期(秒,<=0 表示禁用) + sora_media_signed_url_ttl_seconds: 900 + # Connection pool isolation strategy: + # 连接池隔离策略: + # - proxy: Isolate by proxy, same proxy shares connection pool (suitable for few proxies, many accounts) + # - proxy: 按代理隔离,同一代理共享连接池(适合代理少、账户多) + # - account: Isolate by account, same account shares connection pool (suitable for few accounts, strict isolation) + # - account: 按账户隔离,同一账户共享连接池(适合账户少、需严格隔离) + # - account_proxy: Isolate by account+proxy combination (default, finest granularity) + # - account_proxy: 按账户+代理组合隔离(默认,最细粒度) + connection_pool_isolation: "account_proxy" + # Force Codex CLI mode: treat all /openai/v1/responses requests as Codex CLI. + # 强制按 Codex CLI 处理 /openai/v1/responses 请求(用于网关未透传/改写 User-Agent 的兜底)。 + # + # 注意:开启后会影响所有客户端的行为(不仅限于 VS Code / Codex CLI),请谨慎开启。 + force_codex_cli: false + # Optional: template file used to build the final top-level Codex `instructions`. + # 可选:用于构建最终 Codex 顶层 `instructions` 的模板文件路径。 + # + # This is applied on the `/v1/messages -> Responses/Codex` conversion path, + # after Claude `system` has already been normalized into Codex `instructions`. + # 该模板作用于 `/v1/messages -> Responses/Codex` 转换链路,且发生在 Claude `system` + # 已经被归一化为 Codex `instructions` 之后。 + # + # The template can reference: + # 模板可引用: + # - {{ .ExistingInstructions }} : converted client instructions/system + # - {{ .OriginalModel }} : original requested model + # - {{ .NormalizedModel }} : normalized routing model + # - {{ .BillingModel }} : billing model + # - {{ .UpstreamModel }} : final upstream model + # + # If you want to preserve client system prompts, keep {{ .ExistingInstructions }} + # somewhere in the template. If omitted, the template output fully replaces it. + # 如需保留客户端 system 提示词,请在模板中显式包含 {{ .ExistingInstructions }}。 + # 若省略,则模板输出会完全覆盖它。 + # + # Docker users can mount a host file to /app/data/codex-instructions.md.tmpl + # and point this field there. + # Docker 用户可将宿主机文件挂载到 /app/data/codex-instructions.md.tmpl, + # 然后把本字段指向该路径。 + forced_codex_instructions_template_file: "" + # OpenAI 透传模式是否放行客户端超时头(如 x-stainless-timeout) + # 默认 false:过滤超时头,降低上游提前断流风险。 + openai_passthrough_allow_timeout_headers: false + # OpenAI Responses WebSocket 配置(默认开启,可按需回滚到 HTTP) + openai_ws: + # 新版 WS mode 路由(默认关闭)。关闭时保持当前 legacy 实现行为。 + mode_router_v2_enabled: false + # ingress 默认模式:off|ctx_pool|passthrough(仅 mode_router_v2_enabled=true 生效) + # 兼容旧值:shared/dedicated 会按 ctx_pool 处理。 + ingress_mode_default: ctx_pool + # 全局总开关,默认 true;关闭时所有请求保持原有 HTTP/SSE 路由 + enabled: true + # 按账号类型细分开关 + oauth_enabled: true + apikey_enabled: true + # 全局强制 HTTP(紧急回滚开关) + force_http: false + # 允许在 WSv2 下按策略恢复 store=true(默认 false) + allow_store_recovery: false + # ingress 模式收到 previous_response_not_found 时,自动去掉 previous_response_id 重试一次(默认 true) + ingress_previous_response_recovery_enabled: true + # store=false 且无可复用会话连接时的策略: + # strict=强制新建连接(隔离优先),adaptive=仅在高风险失败后强制新建,off=尽量复用(性能优先) + store_disabled_conn_mode: strict + # store=false 且无可复用会话连接时,是否强制新建连接(默认 true,优先会话隔离) + # 兼容旧配置:仅在 store_disabled_conn_mode 未配置时生效 + store_disabled_force_new_conn: true + # 是否启用 WSv2 generate=false 预热(默认 false) + prewarm_generate_enabled: false + # 协议 feature 开关,v2 优先于 v1 + responses_websockets: false + responses_websockets_v2: true + # 连接池参数(按账号池化复用) + max_conns_per_account: 128 + min_idle_per_account: 4 + max_idle_per_account: 12 + # 是否按账号并发动态计算连接池上限: + # effective_max_conns = min(max_conns_per_account, ceil(account.concurrency * factor)) + dynamic_max_conns_by_account_concurrency_enabled: true + # 按账号类型分别设置系数(OAuth / API Key) + oauth_max_conns_factor: 1.0 + apikey_max_conns_factor: 1.0 + dial_timeout_seconds: 10 + read_timeout_seconds: 900 + write_timeout_seconds: 120 + pool_target_utilization: 0.7 + queue_limit_per_conn: 64 + # 流式写出批量 flush 参数 + event_flush_batch_size: 1 + event_flush_interval_ms: 10 + # 预热触发冷却(毫秒) + prewarm_cooldown_ms: 300 + # WS 回退到 HTTP 后的冷却时间(秒),用于避免 WS/HTTP 来回抖动;0 表示关闭冷却 + fallback_cooldown_seconds: 30 + # WS 重试退避参数(毫秒) + retry_backoff_initial_ms: 120 + retry_backoff_max_ms: 2000 + # 抖动比例(0-1) + retry_jitter_ratio: 0.2 + # 单次请求 WS 重试总预算(毫秒);建议设置为有限值,避免重试拉高 TTFT 长尾 + retry_total_budget_ms: 5000 + # payload_schema 日志采样率(0-1);降低热路径日志放大 + payload_log_sample_rate: 0.2 + # 调度与粘连参数 + lb_top_k: 7 + sticky_session_ttl_seconds: 3600 + # 会话哈希迁移兼容开关:新 key 未命中时回退读取旧 SHA-256 key + session_hash_read_old_fallback: true + # 会话哈希迁移兼容开关:写入时双写旧 SHA-256 key(短 TTL) + session_hash_dual_write_old: true + # context 元数据迁移兼容开关:保留旧 ctxkey.* 读取/注入桥接 + metadata_bridge_enabled: true + sticky_response_id_ttl_seconds: 3600 + # 兼容旧键:当 sticky_response_id_ttl_seconds 缺失时回退该值 + sticky_previous_response_ttl_seconds: 3600 + scheduler_score_weights: + priority: 1.0 + load: 1.0 + queue: 0.7 + error_rate: 0.8 + ttft: 0.5 + # HTTP upstream connection pool settings (HTTP/2 + multi-proxy scenario defaults) + # HTTP 上游连接池配置(HTTP/2 + 多代理场景默认值) + # Max idle connections across all hosts + # 所有主机的最大空闲连接数 + max_idle_conns: 2560 + # Max idle connections per host + # 每个主机的最大空闲连接数 + max_idle_conns_per_host: 120 + # Max connections per host + # 每个主机的最大连接数 + max_conns_per_host: 1024 + # Idle connection timeout (seconds) + # 空闲连接超时时间(秒) + idle_conn_timeout_seconds: 90 + # Upstream client cache settings + # 上游连接池客户端缓存配置 + # max_upstream_clients: Max cached clients, evicts least recently used when exceeded + # max_upstream_clients: 最大缓存客户端数量,超出后淘汰最久未使用的 + max_upstream_clients: 5000 + # client_idle_ttl_seconds: Client idle reclaim threshold (seconds), reclaimed when idle and no active requests + # client_idle_ttl_seconds: 客户端空闲回收阈值(秒),超时且无活跃请求时回收 + client_idle_ttl_seconds: 900 + # Concurrency slot expiration time (minutes) + # 并发槽位过期时间(分钟) + concurrency_slot_ttl_minutes: 30 + # Stream data interval timeout (seconds), 0=disable + # 流数据间隔超时(秒),0=禁用 + stream_data_interval_timeout: 180 + # Stream keepalive interval (seconds), 0=disable + # 流式 keepalive 间隔(秒),0=禁用 + stream_keepalive_interval: 10 + # SSE max line size in bytes (default: 40MB) + # SSE 单行最大字节数(默认 40MB) + max_line_size: 41943040 + # Log upstream error response body summary (safe/truncated; does not log request content) + # 记录上游错误响应体摘要(安全/截断;不记录请求内容) + log_upstream_error_body: true + # Max bytes to log from upstream error body + # 记录上游错误响应体的最大字节数 + log_upstream_error_body_max_bytes: 2048 + # Auto inject anthropic-beta header for API-key accounts when needed (default: off) + # 需要时自动为 API-key 账户注入 anthropic-beta 头(默认:关闭) + inject_beta_for_apikey: false + # Allow failover on selected 400 errors (default: off) + # 允许在特定 400 错误时进行故障转移(默认:关闭) + failover_on_400: false + # Scheduling configuration + # 调度配置 + scheduling: + # Sticky session max waiting queue size + # 粘性会话最大排队长度 + sticky_session_max_waiting: 3 + # Sticky session wait timeout (duration) + # 粘性会话等待超时(时间段) + sticky_session_wait_timeout: 120s + # Fallback wait timeout (duration) + # 兜底排队等待超时(时间段) + fallback_wait_timeout: 30s + # Fallback max waiting queue size + # 兜底最大排队长度 + fallback_max_waiting: 100 + # Enable batch load calculation for scheduling + # 启用调度批量负载计算 + load_batch_enabled: true + # Slot cleanup interval (duration) + # 并发槽位清理周期(时间段) + slot_cleanup_interval: 30s + # 是否允许受控回源到 DB(默认 true,保持现有行为) + db_fallback_enabled: true + # 受控回源超时(秒),0 表示不额外收紧超时 + db_fallback_timeout_seconds: 0 + # 受控回源限流(实例级 QPS),0 表示不限制 + db_fallback_max_qps: 0 + # outbox 轮询周期(秒) + outbox_poll_interval_seconds: 1 + # outbox 滞后告警阈值(秒) + outbox_lag_warn_seconds: 5 + # outbox 触发强制重建阈值(秒) + outbox_lag_rebuild_seconds: 10 + # outbox 连续滞后触发次数 + outbox_lag_rebuild_failures: 3 + # outbox 积压触发重建阈值(行数) + outbox_backlog_rebuild_rows: 10000 + # 全量重建周期(秒),0 表示禁用 + full_rebuild_interval_seconds: 300 + # TLS fingerprint simulation / TLS 指纹伪装 + # Default profile "claude_cli_v2" simulates Node.js 20.x + # 默认模板 "claude_cli_v2" 模拟 Node.js 20.x 指纹 + tls_fingerprint: + enabled: true + # profiles: + # profile_1: + # name: "Custom Profile 1" + # profile_2: + # name: "Custom Profile 2" + +# ============================================================================= +# Logging Configuration +# 日志配置 +# ============================================================================= +log: + # Log level: debug/info/warn/error + # 日志级别:debug/info/warn/error + level: "info" + # Log format: json/console + # 日志格式:json/console + format: "console" + # Service name field written into each log line + # 每条日志都会附带 service 字段 + service_name: "sub2api" + # Environment field written into each log line + # 每条日志都会附带 env 字段 + env: "production" + # Include caller information + # 是否输出调用方位置信息 + caller: true + # Stacktrace threshold: none/error/fatal + # 堆栈输出阈值:none/error/fatal + stacktrace_level: "error" + output: + # Keep stdout/stderr output for container log collection + # 保持标准输出用于容器日志采集 + to_stdout: true + # Enable file output (default path auto-derived) + # 启用文件输出(默认路径自动推导) + to_file: true + # Empty means: + # - DATA_DIR set: {{DATA_DIR}}/logs/sub2api.log + # - otherwise: /app/data/logs/sub2api.log + # 留空时: + # - 设置 DATA_DIR:{{DATA_DIR}}/logs/sub2api.log + # - 否则:/app/data/logs/sub2api.log + file_path: "" + rotation: + # Max file size before rotation (MB) + # 单文件滚动阈值(MB) + max_size_mb: 100 + # Number of rotated files to keep (0 means unlimited) + # 保留历史文件数量(0 表示不限制) + max_backups: 10 + # Number of days to keep old log files (0 means unlimited) + # 历史日志保留天数(0 表示不限制) + max_age_days: 7 + # Compress rotated files + # 是否压缩历史日志 + compress: true + # Use local time for timestamp in rotated filename + # 滚动文件名时间戳使用本地时区 + local_time: true + sampling: + # Enable zap sampler (reduce high-frequency repetitive logs) + # 启用 zap 采样(减少高频重复日志) + enabled: false + # Number of first entries per second to always log + # 每秒无采样保留的前 N 条日志 + initial: 100 + # Thereafter keep 1 out of N entries per second + # 之后每 N 条保留 1 条 + thereafter: 100 + +# ============================================================================= +# Sora Direct Client Configuration +# Sora 直连配置 +# ============================================================================= +sora: + client: + # Sora backend base URL + # Sora 上游 Base URL + base_url: "https://sora.chatgpt.com/backend" + # Request timeout (seconds) + # 请求超时(秒) + timeout_seconds: 120 + # Max retries for upstream requests + # 上游请求最大重试次数 + max_retries: 3 + # Account+proxy cooldown window after Cloudflare challenge (seconds, 0 to disable) + # Cloudflare challenge 后按账号+代理冷却窗口(秒,0 表示关闭) + cloudflare_challenge_cooldown_seconds: 900 + # Poll interval (seconds) + # 轮询间隔(秒) + poll_interval_seconds: 2 + # Max poll attempts + # 最大轮询次数 + max_poll_attempts: 600 + # Recent task query limit (image) + # 最近任务查询数量(图片轮询) + recent_task_limit: 50 + # Recent task query max limit (fallback) + # 最近任务查询最大数量(回退) + recent_task_limit_max: 200 + # Enable debug logs for Sora upstream requests + # 启用 Sora 直连调试日志 + # 调试日志会输出上游请求尝试、重试、响应摘要;Authorization/openai-sentinel-token 等敏感头会自动脱敏 + debug: false + # Allow Sora client to fetch token via OpenAI token provider + # 是否允许 Sora 客户端通过 OpenAI token provider 取 token(默认 false,避免误走 OpenAI 刷新链路) + use_openai_token_provider: false + # Optional custom headers (key-value) + # 额外请求头(键值对) + headers: {} + # Default User-Agent for Sora requests + # Sora 默认 User-Agent + user_agent: "Sora/1.2026.007 (Android 15; 24122RKC7C; build 2600700)" + # Disable TLS fingerprint for Sora upstream + # 关闭 Sora 上游 TLS 指纹伪装 + disable_tls_fingerprint: false + # curl_cffi sidecar for Sora only (required) + # 仅 Sora 链路使用的 curl_cffi sidecar(必需) + curl_cffi_sidecar: + # Sora 强制通过 sidecar 请求,必须启用 + # Sora is forced to use sidecar only; keep enabled=true + enabled: true + # Sidecar base URL (default endpoint: /request) + # sidecar 基础地址(默认请求端点:/request) + base_url: "http://sora-curl-cffi-sidecar:8080" + # curl_cffi impersonate profile, e.g. chrome131/chrome124/safari18_0 + # curl_cffi 指纹伪装 profile,例如 chrome131/chrome124/safari18_0 + impersonate: "chrome131" + # Sidecar request timeout (seconds) + # sidecar 请求超时(秒) + timeout_seconds: 60 + # Reuse session key per account+proxy to let sidecar persist cookies/session + # 按账号+代理复用 session key,让 sidecar 持久化 cookies/session + session_reuse_enabled: true + # Session TTL in sidecar (seconds) + # sidecar 会话 TTL(秒) + session_ttl_seconds: 3600 + storage: + # Storage type (local only for now) + # 存储类型(首发仅支持 local) + type: "local" + # Local base path; empty uses /app/data/sora + # 本地存储基础路径;为空使用 /app/data/sora + local_path: "" + # Fallback to upstream URL when download fails + # 下载失败时回退到上游 URL + fallback_to_upstream: true + # Max concurrent downloads + # 并发下载上限 + max_concurrent_downloads: 4 + # Download timeout (seconds) + # 下载超时(秒) + download_timeout_seconds: 120 + # Max download bytes + # 最大下载字节数 + max_download_bytes: 209715200 + # Enable debug logs for media storage + # 启用媒体存储调试日志 + debug: false + cleanup: + # Enable cleanup task + # 启用清理任务 + enabled: true + # Retention days + # 保留天数 + retention_days: 7 + # Cron schedule + # Cron 调度表达式 + schedule: "0 3 * * *" + +# Token refresh behavior +# token 刷新行为控制 +token_refresh: + # Whether OpenAI refresh flow is allowed to sync linked Sora accounts + # 是否允许 OpenAI 刷新流程同步覆盖 linked_openai_account_id 关联的 Sora 账号 token + sync_linked_sora_accounts: false + +# ============================================================================= +# API Key Auth Cache Configuration +# API Key 认证缓存配置 +# ============================================================================= +api_key_auth_cache: + # L1 cache size (entries), in-process LRU/TTL cache + # L1 缓存容量(条目数),进程内 LRU/TTL 缓存 + l1_size: 65535 + # L1 cache TTL (seconds) + # L1 缓存 TTL(秒) + l1_ttl_seconds: 15 + # L2 cache TTL (seconds), stored in Redis + # L2 缓存 TTL(秒),Redis 中存储 + l2_ttl_seconds: 300 + # Negative cache TTL (seconds) + # 负缓存 TTL(秒) + negative_ttl_seconds: 30 + # TTL jitter percent (0-100) + # TTL 抖动百分比(0-100) + jitter_percent: 10 + # Enable singleflight for cache misses + # 缓存未命中时启用 singleflight 合并回源 + singleflight: true + +# ============================================================================= +# Dashboard Cache Configuration +# 仪表盘缓存配置 +# ============================================================================= +dashboard_cache: + # Enable dashboard cache + # 启用仪表盘缓存 + enabled: true + # Redis key prefix for multi-environment isolation + # Redis key 前缀,用于多环境隔离 + key_prefix: "sub2api:" + # Fresh TTL (seconds); within this window cached stats are considered fresh + # 新鲜阈值(秒);命中后处于该窗口视为新鲜数据 + stats_fresh_ttl_seconds: 15 + # Cache TTL (seconds) stored in Redis + # Redis 缓存 TTL(秒) + stats_ttl_seconds: 30 + # Async refresh timeout (seconds) + # 异步刷新超时(秒) + stats_refresh_timeout_seconds: 30 + +# ============================================================================= +# Dashboard Aggregation Configuration +# 仪表盘预聚合配置(重启生效) +# ============================================================================= +dashboard_aggregation: + # Enable aggregation job + # 启用聚合作业 + enabled: true + # Refresh interval (seconds) + # 刷新间隔(秒) + interval_seconds: 60 + # Lookback window (seconds) for late-arriving data + # 回看窗口(秒),处理迟到数据 + lookback_seconds: 120 + # Allow manual backfill + # 允许手动回填 + backfill_enabled: false + # Backfill max range (days) + # 回填最大跨度(天) + backfill_max_days: 31 + # Recompute recent N days on startup + # 启动时重算最近 N 天 + recompute_days: 2 + # Retention windows (days) + # 保留窗口(天) + retention: + # Raw usage_logs retention + # 原始 usage_logs 保留天数 + usage_logs_days: 90 + # Hourly aggregation retention + # 小时聚合保留天数 + hourly_days: 180 + # Daily aggregation retention + # 日聚合保留天数 + daily_days: 730 + +# ============================================================================= +# Usage Cleanup Task Configuration +# 使用记录清理任务配置(重启生效) +# ============================================================================= +usage_cleanup: + # Enable cleanup task worker + # 启用清理任务执行器 + enabled: true + # Max date range (days) per task + # 单次任务最大时间跨度(天) + max_range_days: 31 + # Batch delete size + # 单批删除数量 + batch_size: 5000 + # Worker interval (seconds) + # 执行器轮询间隔(秒) + worker_interval_seconds: 10 + # Task execution timeout (seconds) + # 单次任务最大执行时长(秒) + task_timeout_seconds: 1800 + +# ============================================================================= +# HTTP 写接口幂等配置 +# Idempotency Configuration +# ============================================================================= +idempotency: + # Observe-only 模式: + # true: 观察期,不带 Idempotency-Key 仍放行(但会记录) + # false: 强制期,不带 Idempotency-Key 直接拒绝(仅对接入幂等保护的接口生效) + observe_only: true + # 关键写接口幂等记录 TTL(秒) + default_ttl_seconds: 86400 + # 系统操作接口(update/rollback/restart)幂等记录 TTL(秒) + system_operation_ttl_seconds: 3600 + # processing 锁超时(秒) + processing_timeout_seconds: 30 + # 可重试失败退避窗口(秒) + failed_retry_backoff_seconds: 5 + # 持久化响应体最大长度(字节) + max_stored_response_len: 65536 + # 过期幂等记录清理周期(秒) + cleanup_interval_seconds: 60 + # 每轮清理最大删除条数 + cleanup_batch_size: 500 + +# ============================================================================= +# Concurrency Wait Configuration +# 并发等待配置 +# ============================================================================= +concurrency: + # SSE ping interval during concurrency wait (seconds) + # 并发等待期间的 SSE ping 间隔(秒) + ping_interval: 10 + +# ============================================================================= +# Database Configuration (PostgreSQL) +# 数据库配置 (PostgreSQL) +# ============================================================================= +database: + # Database host address + # 数据库主机地址 + host: "postgres" + # Database port + # 数据库端口 + port: 5432 + # Database username + # 数据库用户名 + user: "postgres" + # Database password + # 数据库密码 + password: "2c106836b2331974005b6edd561a48fd527efafb2d4a36e4" + # Database name + # 数据库名称 + dbname: "sub2api" + # SSL mode: disable, prefer, require, verify-ca, verify-full + # SSL 模式:disable(禁用), prefer(优先加密,默认), require(要求), verify-ca(验证CA), verify-full(完全验证) + # 默认值为 "prefer",数据库支持 SSL 时自动使用加密连接,不支持时回退明文 + sslmode: "disable" + # Max open connections (高并发场景建议 256+,需配合 PostgreSQL max_connections 调整) + # 最大打开连接数 + max_open_conns: 256 + # Max idle connections (建议为 max_open_conns 的 50%,减少频繁建连开销) + # 最大空闲连接数 + max_idle_conns: 128 + # Connection max lifetime (minutes) + # 连接最大存活时间(分钟) + conn_max_lifetime_minutes: 30 + # Connection max idle time (minutes) + # 空闲连接最大存活时间(分钟) + conn_max_idle_time_minutes: 5 + +# ============================================================================= +# Redis Configuration +# Redis 配置 +# ============================================================================= +redis: + # Redis host address + # Redis 主机地址 + host: "redis" + # Redis port + # Redis 端口 + port: 6379 + # Redis password (leave empty if no password is set) + # Redis 密码(如果未设置密码则留空) + password: "" + # Database number (0-15) + # 数据库编号(0-15) + db: 0 + # Connection pool size (max concurrent connections) + # 连接池大小(最大并发连接数) + pool_size: 1024 + # Minimum number of idle connections (高并发场景建议 128+,保持足够热连接) + # 最小空闲连接数 + min_idle_conns: 128 + # Enable TLS/SSL connection + # 是否启用 TLS/SSL 连接 + enable_tls: false + +# ============================================================================= +# Ops Monitoring (Optional) +# 运维监控 (可选) +# ============================================================================= +ops: + # Enable ops monitoring features (background jobs and APIs) + # 是否启用运维监控功能(后台任务和接口) + # Set to false to hide ops menu in sidebar and disable all ops features + # 设置为 false 可在左侧栏隐藏运维监控菜单并禁用所有运维监控功能 + # Other detailed settings (cleanup, aggregation, etc.) are configured in ops settings dialog + # 其他详细设置(数据清理、预聚合等)在运维监控设置对话框中配置 + enabled: true + +# ============================================================================= +# JWT Configuration +# JWT 配置 +# ============================================================================= +jwt: + # IMPORTANT: Change this to a random string in production! + # 重要:生产环境中请更改为随机字符串! + # Generate with / 生成命令: openssl rand -hex 32 + secret: "24c0e7ca39f406779a7f3aa03e573bd4e182d88cc00ef77854199b23789b5086" + # Token expiration time in hours (max 168) + # 令牌过期时间(小时,最大 168) + expire_hour: 24 + # Access Token 过期时间(分钟) + # 优先级说明: + # - >0: 按分钟生效(优先于 expire_hour) + # - =0: 回退使用 expire_hour + access_token_expire_minutes: 0 + +# ============================================================================= +# TOTP (2FA) Configuration +# TOTP 双因素认证配置 +# ============================================================================= +totp: + # IMPORTANT: Set a fixed encryption key for TOTP secrets. + # 重要:设置固定的 TOTP 加密密钥。 + # If left empty, a random key will be generated on each startup, causing all + # existing TOTP configurations to become invalid (users won't be able to + # login with 2FA). + # 如果留空,每次启动将生成随机密钥,导致现有的 TOTP 配置失效(用户无法使用 + # 双因素认证登录)。 + # Generate with / 生成命令: openssl rand -hex 32 + encryption_key: "" + +# ============================================================================= +# LinuxDo Connect OAuth Login (SSO) +# LinuxDo Connect OAuth 登录(用于 Sub2API 用户登录) +# ============================================================================= +linuxdo_connect: + enabled: false + client_id: "" + client_secret: "" + authorize_url: "https://connect.linux.do/oauth2/authorize" + token_url: "https://connect.linux.do/oauth2/token" + userinfo_url: "https://connect.linux.do/api/user" + scopes: "user" + # 示例: "https://your-domain.com/api/v1/auth/oauth/linuxdo/callback" + redirect_url: "" + # 安全提示: + # - 建议使用同源相对路径(以 / 开头),避免把 token 重定向到意外的第三方域名 + # - 该地址不应包含 #fragment(本实现使用 URL fragment 传递 access_token) + frontend_redirect_url: "/auth/linuxdo/callback" + token_auth_method: "client_secret_post" # client_secret_post | client_secret_basic | none + # 注意:当 token_auth_method=none(public client)时,必须启用 PKCE + use_pkce: false + userinfo_email_path: "" + userinfo_id_path: "" + userinfo_username_path: "" + +# ============================================================================= +# Generic OIDC OAuth Login (SSO) +# 通用 OIDC OAuth 登录(用于 Sub2API 用户登录) +# ============================================================================= +oidc_connect: + enabled: false + provider_name: "OIDC" + client_id: "" + client_secret: "" + # 例如: "https://keycloak.example.com/realms/myrealm" + issuer_url: "" + # 可选: OIDC Discovery URL。为空时可手动填写 authorize/token/userinfo/jwks + discovery_url: "" + authorize_url: "" + token_url: "" + # 可选(仅补充 email/username,不用于 sub 可信绑定) + userinfo_url: "" + # validate_id_token=true 时必填 + jwks_url: "" + scopes: "openid email profile" + # 示例: "https://your-domain.com/api/v1/auth/oauth/oidc/callback" + redirect_url: "" + # 安全提示: + # - 建议使用同源相对路径(以 / 开头),避免把 token 重定向到意外的第三方域名 + # - 该地址不应包含 #fragment(本实现使用 URL fragment 传递 access_token) + frontend_redirect_url: "/auth/oidc/callback" + token_auth_method: "client_secret_post" # client_secret_post | client_secret_basic | none + # 注意:当 token_auth_method=none(public client)时,必须启用 PKCE + use_pkce: false + # 开启后强制校验 id_token 的签名和 claims(推荐) + validate_id_token: true + allowed_signing_algs: "RS256,ES256,PS256" + # 允许的时钟偏移(秒) + clock_skew_seconds: 120 + # 若 Provider 返回 email_verified=false,是否拒绝登录 + require_email_verified: false + userinfo_email_path: "" + userinfo_id_path: "" + userinfo_username_path: "" + +# ============================================================================= +# Default Settings +# 默认设置 +# ============================================================================= +default: + # Initial admin account (created on first run) + # 初始管理员账户(首次运行时创建) + admin_email: "admin@example.com" + admin_password: "admin123" + + # Default settings for new users + # 新用户默认设置 + # Max concurrent requests per user + # 每用户最大并发请求数 + user_concurrency: 5 + # Initial balance for new users + # 新用户初始余额 + user_balance: 0 + + # API key settings + # API 密钥设置 + # Prefix for generated API keys + # 生成的 API 密钥前缀 + api_key_prefix: "sk-" + + # Rate multiplier (affects billing calculation) + # 费率倍数(影响计费计算) + rate_multiplier: 1.0 + +# ============================================================================= +# Rate Limiting +# 速率限制 +# ============================================================================= +rate_limit: + # Cooldown time (in minutes) when upstream returns 529 (overloaded) + # 上游返回 529(过载)时的冷却时间(分钟) + overload_cooldown_minutes: 10 + +# ============================================================================= +# Pricing Data Source (Optional) +# 定价数据源(可选) +# ============================================================================= +pricing: + # URL to fetch model pricing data (default: pinned model-price-repo commit) + # 获取模型定价数据的 URL(默认:固定 commit 的 model-price-repo) + remote_url: "https://raw.githubusercontent.com/Wei-Shaw/model-price-repo/refs/heads/main//model_prices_and_context_window.json" + # Hash verification URL (optional) + # 哈希校验 URL(可选) + hash_url: "https://raw.githubusercontent.com/Wei-Shaw/model-price-repo/refs/heads/main//model_prices_and_context_window.sha256" + # Local data directory for caching + # 本地数据缓存目录 + data_dir: "./data" + # Fallback pricing file + # 备用定价文件 + fallback_file: "./resources/model-pricing/model_prices_and_context_window.json" + # Update interval in hours + # 更新间隔(小时) + update_interval_hours: 24 + # Hash check interval in minutes + # 哈希检查间隔(分钟) + hash_check_interval_minutes: 10 + +# ============================================================================= +# Billing Configuration +# 计费配置 +# ============================================================================= +billing: + circuit_breaker: + # Enable circuit breaker for billing service + # 启用计费服务熔断器 + enabled: true + # Number of failures before opening circuit + # 触发熔断的失败次数阈值 + failure_threshold: 5 + # Time to wait before attempting reset (seconds) + # 熔断后重试等待时间(秒) + reset_timeout_seconds: 30 + # Number of requests to allow in half-open state + # 半开状态允许通过的请求数 + half_open_requests: 3 + +# ============================================================================= +# Turnstile Configuration +# Turnstile 人机验证配置 +# ============================================================================= +turnstile: + # Require Turnstile in release mode (when enabled, login/register will fail if not configured) + # 在 release 模式下要求 Turnstile 验证(启用后,若未配置则登录/注册会失败) + required: false + +# ============================================================================= +# Gemini OAuth (Required for Gemini accounts) +# Gemini OAuth 配置(Gemini 账户必需) +# ============================================================================= +# Sub2API supports TWO Gemini OAuth modes: +# Sub2API 支持两种 Gemini OAuth 模式: +# +# 1. Code Assist OAuth (requires GCP project_id) +# 1. Code Assist OAuth(需要 GCP project_id) +# - Uses: cloudcode-pa.googleapis.com (Code Assist API) +# - 使用:cloudcode-pa.googleapis.com(Code Assist API) +# +# 2. AI Studio OAuth (no project_id needed) +# 2. AI Studio OAuth(不需要 project_id) +# - Uses: generativelanguage.googleapis.com (AI Studio API) +# - 使用:generativelanguage.googleapis.com(AI Studio API) +# +# Default: Uses Gemini CLI's public OAuth credentials (same as Google's official CLI tool) +# 默认:使用 Gemini CLI 的公开 OAuth 凭证(与 Google 官方 CLI 工具相同) +gemini: + oauth: + # OAuth 客户端配置说明: + # 1) 留空 client_id/client_secret:使用 Gemini CLI 内置 OAuth Client(其 client_secret 需通过环境变量注入) + # - GEMINI_CLI_OAUTH_CLIENT_SECRET + # 2) 同时设置 client_id/client_secret:使用你自建的 OAuth Client(推荐,权限更完整) + # + # 注意:client_id 与 client_secret 必须同时为空或同时非空。 + client_id: "" + client_secret: "" + # Optional scopes (space-separated). Leave empty to auto-select based on oauth_type. + # 可选的权限范围(空格分隔)。留空则根据 oauth_type 自动选择。 + scopes: "" + quota: + # Optional: local quota simulation for Gemini Code Assist (local billing). + # 可选:Gemini Code Assist 本地配额模拟(本地计费)。 + # These values are used for UI progress + precheck scheduling, not official Google quotas. + # 这些值用于 UI 进度显示和预检调度,并非 Google 官方配额。 + tiers: + LEGACY: + # Pro model requests per day + # Pro 模型每日请求数 + pro_rpd: 50 + # Flash model requests per day + # Flash 模型每日请求数 + flash_rpd: 1500 + # Cooldown time (minutes) after hitting quota + # 达到配额后的冷却时间(分钟) + cooldown_minutes: 30 + PRO: + # Pro model requests per day + # Pro 模型每日请求数 + pro_rpd: 1500 + # Flash model requests per day + # Flash 模型每日请求数 + flash_rpd: 4000 + # Cooldown time (minutes) after hitting quota + # 达到配额后的冷却时间(分钟) + cooldown_minutes: 5 + ULTRA: + # Pro model requests per day + # Pro 模型每日请求数 + pro_rpd: 2000 + # Flash model requests per day (0 = unlimited) + # Flash 模型每日请求数(0 = 无限制) + flash_rpd: 0 + # Cooldown time (minutes) after hitting quota + # 达到配额后的冷却时间(分钟) + cooldown_minutes: 5 + +# ============================================================================= +# Update Configuration (在线更新配置) +# ============================================================================= +update: + # Proxy URL for accessing GitHub (used for online updates and pricing data) + # 用于访问 GitHub 的代理地址(用于在线更新和定价数据获取) + # Supports: http, https, socks5, socks5h + # Examples: + # - HTTP proxy: "http://127.0.0.1:7890" + # - SOCKS5 proxy: "socks5://127.0.0.1:1080" + # - With authentication: "http://user:pass@proxy.example.com:8080" + # Leave empty for direct connection (recommended for overseas servers) + # 留空表示直连(适用于海外服务器) + proxy_url: "" diff --git a/backend/sub2api-linux b/backend/sub2api-linux new file mode 100755 index 00000000..f8f8f1d8 Binary files /dev/null and b/backend/sub2api-linux differ diff --git a/docs/superpowers/plans/2026-04-20-portal-i18n-pricing.md b/docs/superpowers/plans/2026-04-20-portal-i18n-pricing.md new file mode 100644 index 00000000..52851a32 --- /dev/null +++ b/docs/superpowers/plans/2026-04-20-portal-i18n-pricing.md @@ -0,0 +1,1262 @@ +# PURO Portal i18n + Pricing Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Ship bilingual (zh/en) support across 5 portal pages (Landing, Docs, Login-narrative, Register-narrative, **new** Pricing) by mounting a dark-tech `PuroLocaleSwitcher` in each top nav and extracting all hard-coded Chinese into i18n keys with English translations. + +**Architecture:** +- Reuse the existing `vue-i18n` infrastructure (`setLocale()`, `availableLocales`, `sub2api_locale` localStorage key). Only add portal-specific namespaces: `landing.*`, `docs.*`, `pricing.*`, `auth.narrative.*`. +- The current admin `LocaleSwitcher.vue` uses Tailwind gray/dark palette that clashes with `.puro-page` dark-tech theme — build a new `PuroLocaleSwitcher.vue` styled with puro.css tokens (`--cyan`, `--bg-0`, `--border`, `--font-mono`), reusing the same `setLocale()` core. +- Pricing page is ported from `docs/design-drafts/v2/Pricing.html` as a fidelity Vue component, i18n-native from commit one (no "extract later" debt). + +**Tech Stack:** Vue 3 + TS (` + + +``` + +- [ ] **Step 2: Verify component typechecks** + +Run: `pnpm run typecheck` (from `frontend/`) +Expected: PASS, no new errors. + +- [ ] **Step 3: Commit** + +```bash +git add frontend/src/components/puro/PuroLocaleSwitcher.vue +git commit -m "feat(i18n): add PuroLocaleSwitcher for portal pages" +``` + +--- + +## Task 2: Mount switcher in `LandingView` nav + +**Files:** +- Modify: `frontend/src/views/landing/LandingView.vue` (template nav block + script import; NO i18n text extraction in this task — that happens in Task 5) + +- [ ] **Step 1: Import the switcher** + +In ` + + +``` + +- [ ] **Step 2: Typecheck** + +Run: `pnpm run typecheck` +Expected: PASS. + +- [ ] **Step 3: Commit** + +```bash +git add frontend/src/components/puro/PricingCalculator.vue +git commit -m "feat(pricing): add PricingCalculator subcomponent" +``` + +--- + +## Task 9: Build `PricingView` (i18n-native) + add route + +**Files:** +- Create: `frontend/src/views/pricing/PricingView.vue` +- Modify: `frontend/src/router/index.ts` (add `/pricing` route) +- Modify: `frontend/src/i18n/locales/zh.ts` (add `pricing.*`) +- Modify: `frontend/src/i18n/locales/en.ts` (add `pricing.*`) + +**Source:** `docs/design-drafts/v2/Pricing.html` — port verbatim, extract Chinese strings to keys. + +**Decisions locked:** +- Header subkicker: ZH `// preview · 最终定价以开售为准` / EN `// preview · final pricing TBD at launch` +- Calculator header pill: ZH `// estimated · 以实际计费为准` / EN `// estimated · for reference only` +- Enterprise card → `mailto:contact@puro.im` +- Binding card → `/register` router-link (no `/binding` page) +- Tier CTAs → `/register` router-link +- Final CTA `Docs` link → `/docs` + +**SOON chip:** before writing the template, the subagent audits the backend for these features (grep at `backend/`): +1. API Key monthly budget / 402 Payment Required → look for `budget` / `Payment Required` +2. Zero-log mode → look for `zero_log` / `zeroLog` +3. Priority scheduling → look for `priority` +4. RPM limits (60/120/300) → look for rate limiter +5. Subscription failover → look for `failover` / `cooling` + +For each feature NOT found: wrap the `
` with an extra chip `{{ $t('pricing.soonChip') }}` (chip = small pill, amber/muted). Add chip CSS in same scoped style. + +- [ ] **Step 1: Audit backend for advertised features** + +Run: `cd /Users/mini/Work/dev/sub2api && grep -ril -E "budget|zero_log|priority_schedul|failover|cooling" backend/ 2>/dev/null | head -20` + +Document which features are implemented; the rest get `SOON` chips. + +- [ ] **Step 2: Add `pricing` namespace to `zh.ts`** with structure: + +```ts +pricing: { + hero: { + kicker: '// pricing · 充多少 · 用多少 · 永不过期', + previewPill: '// preview · 最终定价以开售为准', + title1: '一次充值,', + titleAccent: '全平台', + title2: '通用', + sub: '同一份积分可以用在 Claude / ChatGPT / Gemini 任意池上。我们把你的订阅额度变成真正的 API 余额 —— 相比官方 API 便宜 {discount}。', + subDiscount: '至多 70%', + underline: '余额永不过期 · 支持支付宝 / 微信 / USDT · 无隐藏订阅费' + }, + soonChip: 'SOON', + tiers: { + starter: { + flag: 'STARTER', + tierLabel: 'tier · 01', + headline: '先尝尝鲜,跑通接入', + credit: '充 $9.9 → 得 {credit} 积分 {bonus}', + creditAmount: '$12', + creditBonus: '+21%', + discountTag: '相当于官方 API · 0.5 折起', + cta: '充值 →', + features: { + allModels: '可用所有模型 / 所有池', + oneKey: '{count} 个 API Key', + oneKeyCount: '1', + rpm60: '60 RPM 速率限制', + log7: '基础日志(7 天保留)', + noBYOS: '自带订阅接入', + noTeam: '团队 / 多人协作' + } + }, + pro: { + flag: '◆ 推荐', + tierLabel: 'tier · 02', + headline: '个人重度用户 · 最划算', + credit: '充 $29.9 → 得 {credit} 积分 {bonus}', + creditAmount: '$45', + creditBonus: '+50%', + discountTag: '相当于官方 API · 3-7 折', + cta: '立即充值 →', + features: { + allModels: '可用所有模型 / 所有池', + threeKeys: '{count} 个 API Key · 独立预算', + threeKeysCount: '3', + rpm120: '120 RPM 速率限制', + log30: '调用日志(30 天保留)', + byos: '自带订阅接入(无限个)', + failover: '多账号 failover 调度' + } + }, + scale: { + flag: '⚡ 限时 +100%', + tierLabel: 'tier · 03', + headline: '小团队 / 长跑项目', + credit: '充 $99 → 得 {credit} 积分 {bonus}', + creditAmount: '$198', + creditBonus: '+100%', + discountTag: '相当于官方 API · 2-5 折', + cta: '充值 →', + features: { + proAll: '所有 Pro 能力', + tenKeys: '{count} 个 API Key · 独立预算', + tenKeysCount: '10', + rpm300: '300 RPM 速率限制', + log90: '调用日志(90 天保留)', + priority: '请求优先级加权调度', + community: 'Slack / Discord 群组支持' + } + }, + custom: { + flag: 'CUSTOM', + tierLabel: 'tier · 04', + headline: '自定义金额 · 按需充值', + creditPrefix: '得约', + bonusPrefix: '+', + discountTag: '根据金额阶梯自动匹配折扣', + cta: '定制充值 →', + features: { + noExpire: '积分永不过期', + proAll: 'Pro 全部能力', + tier: '阶梯 +21% ~ +100%', + pay: '支付宝 / 微信 / USDT', + slider: '拖动滑块预览赠送' + } + } + }, + custom: { + enterprise: { + title: 'Enterprise · 企业定制', + desc: '专属订阅池、SLA、合规审计、私有化部署、发票结算。规模 >$500/月起可申请。', + cta: '联系商务 →' + }, + binding: { + title: '已有订阅?直接接入', + desc: '有 Claude Max / ChatGPT Pro?免费注册后绑定,只为 PURO 路由费买单 —— 按次 {price}。', + price: '$0.0008/request', + cta: '接入我的订阅 →' + } + }, + calc: { + kicker: '// cost estimator', + previewPill: '// estimated · 以实际计费为准', + title: '算算你能省多少?', + sub: '按你的使用场景,对比 PURO 和官方 API 的月度花费差。数字会根据你选的场景自动更新。', + reqLabel: '日均请求数', + tokLabel: '平均每请求 tokens', + mixLabel: 'Claude 占比', + monthlyTok: '月度 tokens 消耗', + officialCost: '官方 API 价格', + puroCost: 'PURO 价格(含 +50% 赠送)', + savings: '节省', + recLabel: '建议充值', + recStarter: '≈ Starter 档够用', + recPro: '≈ Pro 档 1 个月', + recScale: '≈ Scale 档 · 1 个月' + }, + works: { + kicker: '// works everywhere', + title: '一个 key,所有工具通用', + sub: '只要支持自定义 {baseUrl} 或 OpenAI / Anthropic API,都能直接接入 PURO。', + baseUrl: 'base_url' + }, + faq: { + kicker: '// frequently asked', + title: '你可能想问的', + noAnswer: '没找到答案?{contact} · 通常 2 小时内回复。', + contact: '发邮件给我们 ↗', + q1: 'PURO 和 API 中转站 / API 代理有什么不同?', + a1: '中转站只是把官方 API 请求转一手,价格取决于你预付多少 balance。PURO 的不同是 —— 我们让你 {bold}。你原本就在付的 $20/月,不再只能在官网聊天里用,而是通过统一 API 喂给 Cursor、Claude Code、任何 SDK。同时我们也提供按量充值的官方 API 备用池,两种模式可以混用。', + a1bold: '把已有的 Claude Pro / ChatGPT Plus 订阅变成 API', + q2: '用订阅跑 API 会不会被封号?', + a2: '我们会自动控制每个订阅的请求节奏,并在触发限流时把请求 failover 到池子里的其他订阅。实际上 PURO 的调用模式比你在官方客户端直接复制粘贴大段对话 {bold}。你绑定多个订阅时,单个账号的 RPM 会被压到足够安全的阈值内。另外所有凭证用 AES-256 加密存储,请求链路不经过第三方。', + a2bold: '更不容易触发风控', + q3: '积分会过期吗?可以退款吗?', + a3: '{bold}你可以攒着慢慢用 —— 包括几个月都不用。首次充值 7 天内未产生任何调用可全额退款,之后按剩余积分 85% 比例退。详见退款政策。', + a3bold: '积分永不过期。', + q4: '支持哪些支付方式?', + a4: '国内:支付宝 · 微信支付。国际:Stripe 信用卡 · USDT (TRC20 / ERC20) · PayPal。企业充值支持 Invoice 对公打款,人民币开票。', + q5: '一个 PURO 账号可以绑定多少个订阅?', + a5Starter: 'Starter 档:不支持绑定自带订阅', + a5Pro: 'Pro 档及以上:无限制,你可以把 10 个 ChatGPT Plus + 3 个 Claude Pro 一起绑上去,统一调度', + a5Enterprise: 'Enterprise:支持跨团队共享池,按组织维度隔离', + q6: '如果某个订阅触发限流了会怎样?', + a6: 'PURO 的调度器会把受限的订阅自动标记为 cooling 状态,暂时从池子里摘除。同一请求会立刻被 failover 到池内其他健康订阅上 —— 调用方通常 {bold}。你可以在 Dashboard 看到每个订阅的当前状态和剩余配额。', + a6bold: '感受不到中断', + q7: '计费精度?超量会怎么办?', + a7: '按实际 token 数 + 模型单价计费,精度到 4 位小数。每个 API Key 可设置独立月度预算,达到后 402 Payment Required,不会继续扣费。账户总余额不足时同样会返回 402,且 Dashboard 有 80% / 95% 两级提醒邮件。', + q8: '数据会被用于训练吗?', + a8: '{bold}所有请求仅用于路由转发,不入库、不留存内容(仅保留元数据如模型、token 数、延迟,用于计费和日志)。Pro 档及以上可选开启"零日志模式",我们连请求 ID 都不记录。', + a8bold: '不会。', + q9: '可以私有化部署吗?', + a9: 'Enterprise 档支持 Docker / K8s 私有化部署,控制面和数据面可以分开。授权按年订阅,包含升级和技术支持。', + q10: '支持哪些模型?会跟进新模型吗?', + a10: '当前覆盖 Claude(Sonnet 4.5 / Opus 4 / Haiku 4.5)、ChatGPT(GPT-5 / GPT-5 Codex / GPT-4.1)、Gemini(2.5 Pro / 2.5 Flash)。每当官方发布新模型,我们通常在 {bold}。完整模型列表见文档。', + a10bold: '24 小时内上线' + }, + finalCta: { + kicker: '// ready to start', + title: '5 分钟,拿到你第一个 sk-puro-* key', + subtitle: '绑定你的第一个订阅即可开始。', + ctaPrimary: '免费注册 →', + ctaDocs: '查看文档' + } +} +``` + +**Note:** The zip's final CTA contains `注册送 $5 测试积分` — DROP this line per Stage 1 decision (no $5 bonus). The subtitle above is pruned. + +- [ ] **Step 3: Mirror `pricing` namespace in `en.ts`** + +Translation guidelines: +- Hero title: `"Top up once," / "unlimited across" / "all platforms"` +- Hero sub: `"The same credits work across Claude / ChatGPT / Gemini pools. We turn your subscription into real API balance — {discount} cheaper than the official API."` +- Tier CTAs: `"Top up →"` / `"Buy Pro →"` / `"Top up →"` / `"Custom top-up →"` +- Tier flags: `"STARTER"` / `"◆ RECOMMENDED"` / `"⚡ LIMITED +100%"` / `"CUSTOM"` +- FAQ answers: professional/concise tone, keep technical terms English + +- [ ] **Step 4: Write `PricingView.vue`** + +Structure: + +```vue + + + + + +``` + +**Subagent execution note:** This task is large. Full template is ~400 lines of Vue. Subagent should: +1. Open `docs/design-drafts/v2/Pricing.html` in one tab +2. Open the target `PricingView.vue` being written +3. Copy each section (hero → tier grid → custom row → calc → works → faq → final-cta), replacing raw Chinese with `$t(...)` lookups per the schema in Step 2 +4. Keep all SVG/HTML structure verbatim +5. Apply the SOON chip to any unimplemented feature per Step 1 audit results +6. Remove the $5 bonus line from final CTA + +- [ ] **Step 5: Add `/pricing` route** + +Modify `frontend/src/router/index.ts`. Add new route entry (public, no auth guard): + +```ts +{ + path: '/pricing', + name: 'pricing', + component: () => import('@/views/pricing/PricingView.vue'), + meta: { requiresAuth: false, title: 'Pricing · PURO AI' } +} +``` + +Add this near the `/docs` route (public portal section). + +- [ ] **Step 6: Add `定价 / Pricing` link to LandingView nav** + +Modify `LandingView.vue` `.nav-links` block: + +```vue + +``` + +(Landing's Pricing link was added as part of Task 3's nav update for Docs; Landing gets it here.) + +- [ ] **Step 7: Typecheck + build** + +Run: `pnpm run typecheck && pnpm run build` +Expected: PASS. + +- [ ] **Step 8: Commit** + +```bash +git add frontend/src/views/pricing/ frontend/src/router/index.ts frontend/src/views/landing/LandingView.vue frontend/src/i18n/locales/zh.ts frontend/src/i18n/locales/en.ts +git commit -m "feat(pricing): add PricingView with bilingual i18n + nav link" +``` + +--- + +## Task 10: Verification + PR + deploy + +**Files:** none changed + +- [ ] **Step 1: Run full typecheck + build** + +```bash +cd frontend +pnpm run typecheck +pnpm run build +``` + +Expected: both PASS. + +- [ ] **Step 2: Scan for leftover hard-coded Chinese in portal views** + +```bash +cd /Users/mini/Work/dev/sub2api +grep -rnP "[\x{4e00}-\x{9fa5}]" frontend/src/views/landing/ frontend/src/views/docs/ frontend/src/views/pricing/ 2>/dev/null | grep -v "^.*://" | grep -vE "" +``` + +Expected: empty output (only things that should remain are comments, which this grep filters). + +- [ ] **Step 3: Start preview and manually verify** + +```bash +cd frontend +pnpm run preview +``` + +Open in browser (http://localhost:4173): +- `/` — Landing: switcher in top-right, click `EN` → all text flips to English, refresh → stays English +- `/pricing` — Pricing: 4 tiers render, calculator sliders work, FAQ accordions open, switcher works +- `/docs` — Docs: tables render, copy-code works, switcher works +- `/login` — narrative panel + form, switcher top-right +- `/register` — narrative panel + form, switcher top-right + +For each page: toggle EN ↔ ZH at least twice, confirm no flashes of untranslated Chinese. + +- [ ] **Step 4: Stop preview** + +Ctrl-C the preview server. Verify no zombie processes: +```bash +pgrep -f "vite.*preview" +``` +Expected: no output. If any: `pkill -f "vite.*preview"`. + +- [ ] **Step 5: Push branch and open PR** + +```bash +git push -u origin feat/portal-i18n-pricing +gh pr create --title "feat: PURO portal i18n (zh/en) + Pricing page" --body "$(cat <<'EOF' +## Summary +- Puro-themed `PuroLocaleSwitcher` mounted in Landing/Docs/Login/Register top nav +- Full i18n extraction for LandingView / DocsView / Login & Register narrative panels (zh + en) +- New `/pricing` page ported from design zip, i18n-native, with preview pricing pill + SOON chips for unshipped features +- Nav adds 定价 / Pricing link on Landing + Docs + +## Test plan +- [ ] Typecheck + build pass +- [ ] Toggle EN/ZH on /, /pricing, /docs, /login, /register — all text switches +- [ ] Refresh persists locale (localStorage `sub2api_locale`) +- [ ] Pricing calculator sliders update live; custom tier bonus updates +- [ ] Admin pages (`/dashboard`, `/admin/*`) unaffected — no CSS regressions + +🤖 Generated with [Claude Code](https://claude.com/claude-code) +EOF +)" +``` + +- [ ] **Step 6: Merge when CI green** + +```bash +gh pr merge --squash --delete-branch +``` + +- [ ] **Step 7: Verify production deploy** + +```bash +curl -sSf -o /dev/null -w "%{http_code}\n" https://ai.puro.im/ +curl -sSf -o /dev/null -w "%{http_code}\n" https://ai.puro.im/pricing +curl -sSf -o /dev/null -w "%{http_code}\n" https://ai.puro.im/docs +``` + +Expected: `200 200 200`. + +- [ ] **Step 8: Cleanup worktree** + +```bash +cd /Users/mini/Work/dev/sub2api +git worktree remove ../sub2api-portal-i18n +``` + +--- + +## Appendix: Translation style guide (EN) + +- **Register:** tech-product, concise, no marketing fluff +- **Technical terms:** keep English (`OAuth`, `SDK`, `API key`, `RPM`, `base_url`, `tokens`) +- **Brand tone:** PURO speaks to developers first, so answers in FAQ stay factual, not salesy +- **Punctuation:** English full stops / commas (not `,` or `。`) +- **Numbers:** keep format from zh (e.g., `$29.9`, `$198`) + +## Appendix: Key guarantees + +- All 5 pages continue to render correctly in zh after extraction (regression check at Task 10 Step 3) +- `setLocale()` remains the single source of truth — no custom storage added +- `PuroLocaleSwitcher` reuses `setLocale()` imports; does not duplicate i18n plumbing +- No changes to admin pages, AppHeader, or existing `LocaleSwitcher.vue` (decoupled) diff --git a/docs/superpowers/plans/fidelity-delta-report.md b/docs/superpowers/plans/fidelity-delta-report.md new file mode 100644 index 00000000..a507233b --- /dev/null +++ b/docs/superpowers/plans/fidelity-delta-report.md @@ -0,0 +1,445 @@ +# PURO AI Fidelity Delta Report + +> Compare: `docs/design-drafts/v2/{Landing,Login,Register}.html` vs `frontend/src/views/{landing,auth}/*.vue` +> Generated: 2026-04-19 + +--- + +## 1. LandingView + +**Zip:** `docs/design-drafts/v2/Landing.html` +**Vue:** `frontend/src/views/landing/LandingView.vue` + +--- + +### Copy / Text Deltas + +| # | Zip (HTML) | Vue (SFC) | Severity | Notes | +|---|---|---|---|---| +| L-T1 | Hero eyebrow: `NEW 统一接入多个 AI 平台 · 零改动切换` | Hero eyebrow: `ChatGPT Plus · Claude Pro · Codex · Gemini` (plain pill, no badge) | **important** | Zip has a highlighted NEW badge + tagline; Vue shows only a product-list pill with no "NEW" call-out and no `统一接入…` text | +| L-T2 | Hero sub: `聚合成统一 API,零改动接入 OpenAI / Anthropic SDK` — pill is a code-styled inline token | Vue: `聚合成统一 API,零改动接入 OpenAI / Anthropic SDK` — plain text, no pill styling | **cosmetic** | Inline `` with monospace border lost | +| L-T3 | Hero CTA buttons: `立即开始 →` (primary) + `查看文档` (ghost) | Hero CTA buttons: `登录 →` (primary) + `联系咨询` (ghost) | **important** | Primary button label changed from "立即开始" to "登录"; secondary changed from "查看文档" to "联系咨询". Zip routes unauthenticated users to Register first; Vue routes to Login | +| L-T4 | Hero micro-text: `无需信用卡 · 用你已有的订阅 · 5 分钟跑通` (three dot-separated items, each a ``) | Hero micro-text: `已验证可用 Codex CLI · Claude Code · curl · 服务器出口新加坡` (single plain string) | **important** | Entirely different social-proof claims; zip emphasises zero-friction onboarding, Vue emphasises tech compatibility | +| L-T5 | Model-wall section kicker: `// providers` | Vue model-wall section kicker: `支持的 AI 平台` (Chinese, no `//` prefix) | **cosmetic** | Zip uses monospace `// providers` pattern; Vue switches to Chinese heading | +| L-T6 | Model-wall subtitle: `通过 OAuth 直接复用你的订阅,无需申请官方 API key` | Vue sub: `无需申请官方 API key,也无需切换账号` | **cosmetic** | Minor copy rewrite; same meaning | +| L-T7 | Features section title: `付一次订阅,
用起一整个模型池` | Vue section title: `一套 key,三件武器` | **important** | Substantially different headline; zip's "pay once" framing vs Vue's "one key, three weapons" | +| L-T8 | Features section sub: `把散落在各个平台的订阅,整合成开发者真正能用的基础设施` | Vue: *(sub entirely absent)* | **important** | Vue omits the features section subtitle entirely | +| L-T9 | Feature card 2 copy: `某个 ChatGPT Plus 触发限流,自动 failover 到下一个。重启、刷新 token 全自动。` | Vue: `某个上游触发限流 / 冷却时,流量切到下一个健康账号,token 刷新全自动。` | **cosmetic** | Light rewrite, same meaning | +| L-T10 | Feature bullets (all 3 cards): 3-bullet lists under a dashed-border divider | Vue: *bullets entirely absent* from all feature cards | **important** | All 9 feature bullet items (`OpenAI Responses API 兼容`, `限流/5xx 自动 failover`, etc.) are missing | +| L-T11 | Code demo section kicker: `// integration` | Vue: `快速接入` | **cosmetic** | | +| L-T12 | Dashboard section sub (long): `不像第三方 API 池子那种"扣了多少不告诉你"。你能看到每次调用:扣了哪个账号、跑了哪个模型、用了多少 tokens、花了多少钱、上游响应几秒。` | Vue: `不像第三方 API 池子那种"扣了多少不告诉你"——扣哪个账号、跑哪个模型、用了多少 tokens、上游响应几秒,一目了然。` | **cosmetic** | Condensed; "花了多少钱" detail removed from Vue version | +| L-T13 | CTA banner: `把订阅变成 API — 5 分钟` + `绑定第一个账号,生成 sk- key,把 base_url 指过来。就这些。` | *CTA banner entirely absent from Vue* | **important** | The mid-page conversion CTA section is completely missing from Vue | +| L-T14 | Pricing section (full 3-tier grid + `pricing-more-link`) | *Pricing section entirely absent from Vue* | **important** | Full pricing section with 3 tiers ($9.9 / $29.9 / $99), all tier copy, and "查看完整定价" link is missing | +| L-T15 | FAQ section (6 expandable `
` + "查看全部 10 个问题" link) | *FAQ section entirely absent from Vue* | **important** | All FAQ content missing | +| L-T16 | Footer brand tagline: `把多个 AI 订阅聚合成统一 API。让「已经付过钱」的订阅真正为你工作。` | Vue: `Self-hosted on puro.im` | **important** | Zip has Chinese product tagline; Vue shows English self-hosted note | +| L-T17 | Footer meta: `© 2026 puro.im · Built with ♥ in Shanghai` | Vue: `© 2026 puro.im · MIT License\nfork of Wei-Shaw/sub2api` | **cosmetic** | Different but both acceptable | +| L-T18 | Footer "产品" column: 文档 / 定价 / FAQ / 功能 | Vue "产品" column: 文档 / 更新日志 | **important** | Vue removes 定价/FAQ/功能 links | +| L-T19 | Footer "账户" column: 登录 / 注册 / Dashboard / 绑定订阅 | Vue footer has "联系" column (admin@puro.im / git.puro.im) instead of "账户" column | **important** | Entire "账户" column replaced with "联系" column | +| L-T20 | Footer system-status line: `all systems operational` (with green dot) | *Absent from Vue footer* | **cosmetic** | | +| L-T21 | Stat card labels (dashboard): `Requests · 24h`, `Tokens · 24h`, `Avg latency`, `Est. savings` | Vue: `今日请求`, `输入 Tokens`, `输出 Tokens`, `今日费用` | **important** | Different stat names and dimensions (4th stat is "savings" in zip vs "cost" in Vue; zip has 1 tokens stat vs Vue has 2 separate I/O token stats) | +| L-T22 | Log table columns: Time / Provider / Model / Tokens / Cost / Latency / Status | Vue: 时间 / 模型 / 上游 / 状态 / 用量 (5 cols, Chinese, Cost and Latency absent) | **important** | Zip has 7-column table with Cost + Latency; Vue drops those two columns | + +--- + +### Structure Deltas + +| # | What zip has | Vue status | Severity | +|---|---|---|---| +| L-S1 | Nav links include `#pricing` and `#faq` anchor links | Vue nav only has `#features` + `/docs` (no pricing/faq) | **important** | +| L-S2 | Hero: `` with 3 `` children and `.dot` separators | Vue: single plain string in `.hero-micro`, no separators | **cosmetic** | +| L-S3 | Model cards: icon logo (`