From c848f950ce7b5839755d3aa07d24194ef1d8166a Mon Sep 17 00:00:00 2001 From: erio Date: Sun, 1 Mar 2026 01:53:14 +0800 Subject: [PATCH] docs+ui: add bilingual payment integration doc and rename purchase entry to recharge/subscription --- ADMIN_PAYMENT_INTEGRATION_API.md | 216 +++------ frontend/src/components/layout/AppSidebar.vue | 24 +- frontend/src/i18n/locales/en.ts | 22 +- frontend/src/i18n/locales/zh.ts | 431 +++++++++++++++++- 4 files changed, 513 insertions(+), 180 deletions(-) diff --git a/ADMIN_PAYMENT_INTEGRATION_API.md b/ADMIN_PAYMENT_INTEGRATION_API.md index db25c8d8..4cc21594 100644 --- a/ADMIN_PAYMENT_INTEGRATION_API.md +++ b/ADMIN_PAYMENT_INTEGRATION_API.md @@ -1,41 +1,40 @@ -# Sub2API Admin API: Payment Integration / 支付集成接口文档 +# ADMIN_PAYMENT_INTEGRATION_API + +> 单文件中英双语文档 / Single-file bilingual documentation (Chinese + English) + +--- ## 中文 -### 概述 - -本文档描述外部支付系统(例如 sub2apipay)对接 Sub2API 时的最小 Admin API 集合,用于完成充值发放与对账。 +### 目标 +本文档用于对接外部支付系统(如 `sub2apipay`)与 Sub2API 的 Admin API,覆盖: +- 支付成功后充值 +- 用户查询 +- 人工余额修正 +- 前端购买页参数透传 ### 基础地址 +- 生产:`https://` +- Beta:`http://:8084` -- 生产环境:`https://` -- Beta 环境:`http://:8084` +### 认证 +推荐使用: +- `x-api-key: admin-<64hex>` +- `Content-Type: application/json` +- 幂等接口额外传:`Idempotency-Key` -### 认证方式 - -以下接口均建议使用: - -- 请求头:`x-api-key: admin-<64hex>`(服务间调用推荐) -- 请求头:`Content-Type: application/json` - -说明:管理员 JWT 也可访问 admin 路由,但机器对机器调用建议使用 Admin API Key。 - -### 1) 一步完成:创建兑换码并兑换 +说明:管理员 JWT 也可访问 admin 路由,但服务间调用建议使用 Admin API Key。 +### 1) 一步完成创建并兑换 `POST /api/v1/admin/redeem-codes/create-and-redeem` -用途: - -- 原子化完成“创建固定兑换码 + 兑换给指定用户”。 -- 常用于支付回调成功后的自动充值。 - -必需请求头: +用途:原子完成“创建兑换码 + 兑换到指定用户”。 +请求头: - `x-api-key` - `Idempotency-Key` -请求体: - +请求体示例: ```json { "code": "s2p_cm1234567890", @@ -46,21 +45,12 @@ } ``` -规则: - -- `code`:外部订单映射的确定性兑换码。 -- `type`:当前推荐使用 `balance`。 -- `value`:必须大于 0。 -- `user_id`:目标用户 ID。 - 幂等语义: +- 同 `code` 且 `used_by` 一致:`200` +- 同 `code` 但 `used_by` 不一致:`409` +- 缺少 `Idempotency-Key`:`400`(`IDEMPOTENCY_KEY_REQUIRED`) -- 同一 `code` 且 `used_by` 一致:返回 `200`(幂等回放)。 -- 同一 `code` 但 `used_by` 不一致:返回 `409`(冲突)。 -- 缺少 `Idempotency-Key`:返回 `400`(`IDEMPOTENCY_KEY_REQUIRED`)。 - -示例: - +curl 示例: ```bash curl -X POST "${BASE}/api/v1/admin/redeem-codes/create-and-redeem" \ -H "x-api-key: ${KEY}" \ @@ -75,32 +65,20 @@ curl -X POST "${BASE}/api/v1/admin/redeem-codes/create-and-redeem" \ }' ``` -### 2) 查询用户(可选前置检查) - +### 2) 查询用户(可选前置校验) `GET /api/v1/admin/users/:id` -用途: - -- 支付成功后充值前,确认目标用户是否存在。 - -示例: - ```bash curl -s "${BASE}/api/v1/admin/users/123" \ -H "x-api-key: ${KEY}" ``` -### 3) 余额调整(已存在接口) - +### 3) 余额调整(已有接口) `POST /api/v1/admin/users/:id/balance` -用途: - -- 复用现有管理员接口做人工纠偏。 -- 支持 `set`、`add`、`subtract`。 - -示例(扣减): +用途:人工补偿 / 扣减,支持 `set` / `add` / `subtract`。 +请求体示例(扣减): ```json { "balance": 100.0, @@ -121,36 +99,25 @@ curl -X POST "${BASE}/api/v1/admin/users/123/balance" \ }' ``` -### 4) 购买页跳转 URL Query 参数(iframe 与新窗口统一) - -Sub2API 前端在打开 `purchase_subscription_url` 时,会给 iframe 和“新窗口打开”统一追加 query 参数,确保外部支付页拿到一致上下文。 - -追加参数: - -- `user_id`:当前登录用户 ID -- `token`:当前登录 JWT token -- `theme`:当前主题(`light` / `dark`) -- `ui_mode`:当前 UI 模式(固定 `embedded`) +### 4) 购买页 URL Query 透传(iframe / 新窗口一致) +当 Sub2API 打开 `purchase_subscription_url` 时,会统一追加: +- `user_id` +- `token` +- `theme`(`light` / `dark`) +- `ui_mode`(固定 `embedded`) 示例: - ```text https://pay.example.com/pay?user_id=123&token=&theme=light&ui_mode=embedded ``` ### 5) 失败处理建议 - -- 支付状态与充值状态分开落库。 -- 收到并验证支付回调后,立即标记“支付成功”。 -- 支付成功但充值失败的订单应允许后续重试。 -- 重试时继续使用同一 `code`,并使用新的 `Idempotency-Key`。 +- 支付成功与充值成功分状态落库 +- 回调验签成功后立即标记“支付成功” +- 支付成功但充值失败的订单允许后续重试 +- 重试保持相同 `code`,并使用新的 `Idempotency-Key` ### 6) `doc_url` 配置建议 - -Sub2API 已支持系统设置中的 `doc_url` 字段。 - -推荐配置: - - 查看链接:`https://github.com/Wei-Shaw/sub2api/blob/main/ADMIN_PAYMENT_INTEGRATION_API.md` - 下载链接:`https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/ADMIN_PAYMENT_INTEGRATION_API.md` @@ -158,40 +125,35 @@ Sub2API 已支持系统设置中的 `doc_url` 字段。 ## English -### Overview - -This document defines the minimum Admin API surface for integrating external payment systems (for example, sub2apipay) with Sub2API for recharge fulfillment and reconciliation. +### Purpose +This document describes the minimal Sub2API Admin API surface for external payment integrations (for example, `sub2apipay`), including: +- Recharge after payment success +- User lookup +- Manual balance correction +- Purchase page query parameter forwarding ### Base URL - - Production: `https://` - Beta: `http://:8084` ### Authentication - Recommended headers: - -- `x-api-key: admin-<64hex>` (recommended for server-to-server calls) +- `x-api-key: admin-<64hex>` - `Content-Type: application/json` +- `Idempotency-Key` for idempotent endpoints -Note: Admin JWT is also accepted by admin routes, but Admin API key is recommended for machine integrations. - -### 1) One-step Create + Redeem +Note: Admin JWT can also access admin routes, but Admin API Key is recommended for server-to-server integration. +### 1) Create and Redeem in one step `POST /api/v1/admin/redeem-codes/create-and-redeem` -Purpose: - -- Atomically create a deterministic redeem code and redeem it to the target user. -- Typical usage: called right after payment callback success. - -Required headers: +Use case: atomically create a redeem code and redeem it to a target user. +Headers: - `x-api-key` - `Idempotency-Key` Request body: - ```json { "code": "s2p_cm1234567890", @@ -202,21 +164,12 @@ Request body: } ``` -Rules: - -- `code`: deterministic code mapped from external order id. -- `type`: `balance` is the recommended type. -- `value`: must be greater than 0. -- `user_id`: target user id. - Idempotency behavior: +- Same `code` and same `used_by`: `200` +- Same `code` but different `used_by`: `409` +- Missing `Idempotency-Key`: `400` (`IDEMPOTENCY_KEY_REQUIRED`) -- Same `code` and same `used_by`: `200` (idempotent replay). -- Same `code` and different `used_by`: `409` (conflict). -- Missing `Idempotency-Key`: `400` (`IDEMPOTENCY_KEY_REQUIRED`). - -Example: - +curl example: ```bash curl -X POST "${BASE}/api/v1/admin/redeem-codes/create-and-redeem" \ -H "x-api-key: ${KEY}" \ @@ -231,32 +184,20 @@ curl -X POST "${BASE}/api/v1/admin/redeem-codes/create-and-redeem" \ }' ``` -### 2) Query User (Optional Pre-check) - +### 2) Query User (optional pre-check) `GET /api/v1/admin/users/:id` -Purpose: - -- Verify target user existence before final recharge/retry. - -Example: - ```bash curl -s "${BASE}/api/v1/admin/users/123" \ -H "x-api-key: ${KEY}" ``` -### 3) Balance Adjustment (Existing API) - +### 3) Balance Adjustment (existing API) `POST /api/v1/admin/users/:id/balance` -Purpose: - -- Reuse existing admin endpoint for manual reconciliation. -- Supports `set`, `add`, `subtract`. +Use case: manual correction with `set` / `add` / `subtract`. Request body example (`subtract`): - ```json { "balance": 100.0, @@ -265,8 +206,6 @@ Request body example (`subtract`): } ``` -Example: - ```bash curl -X POST "${BASE}/api/v1/admin/users/123/balance" \ -H "x-api-key: ${KEY}" \ @@ -279,35 +218,24 @@ curl -X POST "${BASE}/api/v1/admin/users/123/balance" \ }' ``` -### 4) Purchase URL Query Parameters (Iframe + New Tab) - -When Sub2API frontend opens `purchase_subscription_url`, it appends the same query parameters for both iframe and “open in new tab” to keep context consistent. - -Appended parameters: - -- `user_id`: current logged-in user id -- `token`: current logged-in JWT token -- `theme`: current theme (`light` / `dark`) -- `ui_mode`: UI mode (fixed `embedded`) +### 4) Purchase URL query forwarding (iframe and new tab) +When Sub2API opens `purchase_subscription_url`, it appends: +- `user_id` +- `token` +- `theme` (`light` / `dark`) +- `ui_mode` (fixed: `embedded`) Example: - ```text https://pay.example.com/pay?user_id=123&token=&theme=light&ui_mode=embedded ``` -### 5) Failure Handling Recommendations - -- Store payment state and recharge state separately. -- Mark payment success immediately after callback verification. -- Keep orders retryable when payment succeeded but recharge failed. -- Reuse the same deterministic `code` and a new `Idempotency-Key` when retrying. - -### 6) Suggested `doc_url` Value - -Sub2API already supports `doc_url` in system settings. - -Recommended values: +### 5) Failure handling recommendations +- Persist payment success and recharge success as separate states +- Mark payment as successful immediately after verified callback +- Allow retry for orders with payment success but recharge failure +- Keep the same `code` for retry, and use a new `Idempotency-Key` +### 6) Recommended `doc_url` - View URL: `https://github.com/Wei-Shaw/sub2api/blob/main/ADMIN_PAYMENT_INTEGRATION_API.md` - Download URL: `https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/ADMIN_PAYMENT_INTEGRATION_API.md` diff --git a/frontend/src/components/layout/AppSidebar.vue b/frontend/src/components/layout/AppSidebar.vue index e5afde9c..e2e2894b 100644 --- a/frontend/src/components/layout/AppSidebar.vue +++ b/frontend/src/components/layout/AppSidebar.vue @@ -290,6 +290,26 @@ const CreditCardIcon = { ) } +const RechargeSubscriptionIcon = { + render: () => + h( + 'svg', + { fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', 'stroke-width': '1.5' }, + [ + h('path', { + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + d: 'M2.25 7.5A2.25 2.25 0 014.5 5.25h15A2.25 2.25 0 0121.75 7.5v9A2.25 2.25 0 0119.5 18.75h-15A2.25 2.25 0 012.25 16.5v-9z' + }), + h('path', { + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + d: 'M6.75 12h3m4.5 0h3m-3-3v6' + }) + ] + ) +} + const GlobeIcon = { render: () => h( @@ -442,7 +462,7 @@ const userNavItems = computed(() => { { path: '/purchase', label: t('nav.buySubscription'), - icon: CreditCardIcon, + icon: RechargeSubscriptionIcon, hideInSimpleMode: true } ] @@ -464,7 +484,7 @@ const personalNavItems = computed(() => { { path: '/purchase', label: t('nav.buySubscription'), - icon: CreditCardIcon, + icon: RechargeSubscriptionIcon, hideInSimpleMode: true } ] diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index d8235c51..cad2e0a6 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -279,7 +279,7 @@ export default { logout: 'Logout', github: 'GitHub', mySubscriptions: 'My Subscriptions', - buySubscription: 'Purchase Subscription', + buySubscription: 'Recharge / Subscription', docs: 'Docs' }, @@ -3343,11 +3343,11 @@ export default { hideCcsImportButtonHint: 'When enabled, the "Import to CCS" button will be hidden on the API Keys page' }, purchase: { - title: 'Purchase Page', - description: 'Show a "Purchase Subscription" entry in the sidebar and open the configured URL in an iframe', - enabled: 'Show Purchase Entry', + title: 'Recharge / Subscription Page', + description: 'Show a "Recharge / Subscription" entry in the sidebar and open the configured URL in an iframe', + enabled: 'Show Recharge / Subscription Entry', enabledHint: 'Only shown in standard mode (not simple mode)', - url: 'Purchase URL', + url: 'Recharge / Subscription URL', urlPlaceholder: 'https://example.com/purchase', urlHint: 'Must be an absolute http(s) URL', iframeWarning: @@ -3575,16 +3575,16 @@ export default { retry: 'Retry' }, - // Purchase Subscription Page + // Recharge / Subscription Page purchase: { - title: 'Purchase Subscription', - description: 'Purchase a subscription via the embedded page', + title: 'Recharge / Subscription', + description: 'Recharge balance or purchase subscription via the embedded page', openInNewTab: 'Open in new tab', notEnabledTitle: 'Feature not enabled', - notEnabledDesc: 'The administrator has not enabled the purchase page. Please contact admin.', - notConfiguredTitle: 'Purchase URL not configured', + notEnabledDesc: 'The administrator has not enabled the recharge/subscription entry. Please contact admin.', + notConfiguredTitle: 'Recharge / Subscription URL not configured', notConfiguredDesc: - 'The administrator enabled the entry but has not configured a purchase URL. Please contact admin.' + 'The administrator enabled the entry but has not configured a recharge/subscription URL. Please contact admin.' }, // Announcements Page diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index 7c191579..42d3bbb5 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -270,6 +270,7 @@ export default { redeemCodes: '兑换码', ops: '运维监控', promoCodes: '优惠码', + dataManagement: '数据管理', settings: '系统设置', myAccount: '我的账户', lightMode: '浅色模式', @@ -279,8 +280,9 @@ export default { logout: '退出登录', github: 'GitHub', mySubscriptions: '我的订阅', - buySubscription: '购买订阅', - docs: '文档' + buySubscription: '充值/订阅', + docs: '文档', + sora: 'Sora 创作' }, // Auth @@ -501,6 +503,7 @@ export default { claudeCode: 'Claude Code', geminiCli: 'Gemini CLI', codexCli: 'Codex CLI', + codexCliWs: 'Codex CLI (WebSocket)', opencode: 'OpenCode' }, antigravity: { @@ -618,8 +621,10 @@ export default { firstToken: '首 Token', duration: '耗时', time: '时间', + ws: 'WS', stream: '流式', sync: '同步', + unknown: '未知', in: '输入', out: '输出', cacheRead: '读取', @@ -862,6 +867,181 @@ export default { failedToLoad: '加载仪表盘数据失败' }, + dataManagement: { + title: '数据管理', + description: '统一管理数据管理代理状态、对象存储配置和备份任务', + agent: { + title: '数据管理代理状态', + description: '系统会自动探测固定 Unix Socket,仅在可连通时启用数据管理功能。', + enabled: '数据管理代理已就绪,可继续进行数据管理操作。', + disabled: '数据管理代理不可用,当前仅可查看诊断信息。', + socketPath: 'Socket 路径', + version: '版本', + status: '状态', + uptime: '运行时长', + reasonLabel: '不可用原因', + reason: { + DATA_MANAGEMENT_AGENT_SOCKET_MISSING: '未检测到数据管理 Socket 文件', + DATA_MANAGEMENT_AGENT_UNAVAILABLE: '数据管理代理不可连通', + BACKUP_AGENT_SOCKET_MISSING: '未检测到备份 Socket 文件', + BACKUP_AGENT_UNAVAILABLE: '备份代理不可连通', + UNKNOWN: '未知原因' + } + }, + sections: { + config: { + title: '备份配置', + description: '配置备份源、保留策略与 S3 存储参数。' + }, + s3: { + title: 'S3 对象存储', + description: '配置并测试备份产物上传到标准 S3 对象存储。' + }, + backup: { + title: '备份操作', + description: '触发 PostgreSQL、Redis 与全量备份任务。' + }, + history: { + title: '备份历史', + description: '查看备份任务执行状态、错误与产物信息。' + } + }, + form: { + sourceMode: '源模式', + backupRoot: '备份根目录', + activePostgresProfile: '当前激活 PostgreSQL 配置', + activeRedisProfile: '当前激活 Redis 配置', + activeS3Profile: '当前激活 S3 账号', + retentionDays: '保留天数', + keepLast: '至少保留最近任务数', + uploadToS3: '上传到 S3', + useActivePostgresProfile: '使用当前激活 PostgreSQL 配置', + useActiveRedisProfile: '使用当前激活 Redis 配置', + useActiveS3Profile: '使用当前激活账号', + idempotencyKey: '幂等键(可选)', + secretConfigured: '已配置,留空不变', + source: { + profileID: '配置 ID(唯一)', + profileName: '配置名称', + setActive: '创建后立即设为激活配置' + }, + postgres: { + title: 'PostgreSQL', + host: '主机', + port: '端口', + user: '用户名', + password: '密码', + database: '数据库', + sslMode: 'SSL 模式', + containerName: '容器名(docker_exec 模式)' + }, + redis: { + title: 'Redis', + addr: '地址(host:port)', + username: '用户名', + password: '密码', + db: '数据库编号', + containerName: '容器名(docker_exec 模式)' + }, + s3: { + enabled: '启用 S3 上传', + profileID: '账号 ID(唯一)', + profileName: '账号名称', + endpoint: 'Endpoint(可选)', + region: 'Region', + bucket: 'Bucket', + accessKeyID: 'Access Key ID', + secretAccessKey: 'Secret Access Key', + prefix: '对象前缀', + forcePathStyle: '强制 path-style', + useSSL: '使用 SSL', + setActive: '创建后立即设为激活账号' + } + }, + sourceProfiles: { + createTitle: '创建数据源配置', + editTitle: '编辑数据源配置', + empty: '暂无配置,请先创建', + deleteConfirm: '确定删除配置 {profileID} 吗?', + columns: { + profile: '配置', + active: '激活状态', + connection: '连接信息', + database: '数据库', + updatedAt: '更新时间', + actions: '操作' + } + }, + s3Profiles: { + createTitle: '创建 S3 账号', + editTitle: '编辑 S3 账号', + empty: '暂无 S3 账号,请先创建', + editHint: '点击“编辑”将在右侧抽屉中修改账号信息。', + deleteConfirm: '确定删除 S3 账号 {profileID} 吗?', + columns: { + profile: '账号', + active: '激活状态', + storage: '存储配置', + updatedAt: '更新时间', + actions: '操作' + } + }, + history: { + total: '共 {count} 条', + empty: '暂无备份任务', + columns: { + jobID: '任务 ID', + type: '类型', + status: '状态', + triggeredBy: '触发人', + pgProfile: 'PostgreSQL 配置', + redisProfile: 'Redis 配置', + s3Profile: 'S3 账号', + finishedAt: '完成时间', + artifact: '产物', + error: '错误' + }, + status: { + queued: '排队中', + running: '执行中', + succeeded: '成功', + failed: '失败', + partial_succeeded: '部分成功' + } + }, + actions: { + refresh: '刷新状态', + disabledHint: '请先启动 datamanagementd 并确认 Socket 可连通。', + reloadConfig: '加载配置', + reloadSourceProfiles: '刷新数据源配置', + reloadProfiles: '刷新账号列表', + newSourceProfile: '新建数据源配置', + saveConfig: '保存配置', + configSaved: '配置保存成功', + testS3: '测试 S3 连接', + s3TestOK: 'S3 连接测试成功', + s3TestFailed: 'S3 连接测试失败', + newProfile: '新建账号', + saveProfile: '保存账号', + activateProfile: '设为激活', + profileIDRequired: '请输入账号 ID', + profileNameRequired: '请输入账号名称', + profileSelectRequired: '请先选择要编辑的账号', + profileCreated: 'S3 账号创建成功', + profileSaved: 'S3 账号保存成功', + profileActivated: 'S3 账号已切换为激活', + profileDeleted: 'S3 账号删除成功', + sourceProfileCreated: '数据源配置创建成功', + sourceProfileSaved: '数据源配置保存成功', + sourceProfileActivated: '数据源配置已切换为激活', + sourceProfileDeleted: '数据源配置删除成功', + createBackup: '创建备份任务', + jobCreated: '备份任务已创建:{jobID}({status})', + refreshJobs: '刷新任务', + loadMore: '加载更多' + } + }, + // Users Management users: { title: '用户管理', @@ -925,6 +1105,9 @@ export default { noApiKeys: '此用户暂无 API 密钥', group: '分组', none: '无', + groupChangedSuccess: '分组修改成功', + groupChangedWithGrant: '分组修改成功,已自动为用户添加「{group}」分组权限', + groupChangeFailed: '分组修改失败', noUsersYet: '暂无用户', createFirstUser: '创建您的第一个用户以开始使用系统', userCreated: '用户创建成功', @@ -978,6 +1161,8 @@ export default { failedToAdjust: '调整失败', emailRequired: '请输入邮箱', concurrencyMin: '并发数不能小于1', + soraStorageQuota: 'Sora 存储配额', + soraStorageQuotaHint: '单位 GB,0 表示使用分组或系统默认配额', amountRequired: '请输入有效金额', insufficientBalance: '余额不足', setAllowedGroups: '设置允许分组', @@ -1228,7 +1413,9 @@ export default { image360: '图片 360px ($)', image540: '图片 540px ($)', video: '视频(标准)($)', - videoHd: '视频(Pro-HD)($)' + videoHd: '视频(Pro-HD)($)', + storageQuota: '存储配额', + storageQuotaHint: '单位 GB,设置该分组用户的 Sora 存储配额上限,0 表示使用系统默认' }, claudeCode: { title: 'Claude Code 客户端限制', @@ -1280,14 +1467,6 @@ export default { enabled: '已启用', disabled: '已禁用' }, - claudeMaxSimulation: { - title: 'Claude Max 用量模拟', - tooltip: - '启用后,针对 Claude 模型且上游未返回写缓存时,系统会按确定性算法把输入 token 映射为少量 input,并将其余归入 1h cache creation,保持总 token 不变。', - enabled: '已启用(模拟 1h 缓存)', - disabled: '已禁用', - hint: '仅影响 usage 计费记录中的 token 分类,不保存请求级映射状态。' - }, supportedScopes: { title: '支持的模型系列', tooltip: '选择此分组支持的模型系列。未勾选的系列将不会被路由到此分组。', @@ -1489,7 +1668,19 @@ export default { sessions: { full: '活跃会话已满,新会话需等待(空闲超时:{idle}分钟)', normal: '活跃会话正常(空闲超时:{idle}分钟)' - } + }, + rpm: { + full: '已达 RPM 上限', + warning: 'RPM 接近上限', + normal: 'RPM 正常', + tieredNormal: 'RPM 限制 (三区模型) - 正常', + tieredWarning: 'RPM 限制 (三区模型) - 接近阈值', + tieredStickyOnly: 'RPM 限制 (三区模型) - 仅粘性会话 | 缓冲区: {buffer}', + tieredBlocked: 'RPM 限制 (三区模型) - 已阻塞 | 缓冲区: {buffer}', + stickyExemptNormal: 'RPM 限制 (粘性豁免) - 正常', + stickyExemptWarning: 'RPM 限制 (粘性豁免) - 接近阈值', + stickyExemptOver: 'RPM 限制 (粘性豁免) - 超限,仅粘性会话' + }, }, clearRateLimit: '清除速率限制', testConnection: '测试连接', @@ -1520,6 +1711,10 @@ export default { codeAssist: 'Code Assist', antigravityOauth: 'Antigravity OAuth', antigravityApikey: '通过 Base URL + API Key 连接', + soraApiKey: 'API Key / 上游透传', + soraApiKeyHint: '连接另一个 Sub2API 或兼容 API', + soraBaseUrlRequired: 'Sora apikey 账号必须设置上游地址(Base URL)', + soraBaseUrlInvalidScheme: 'Base URL 必须以 http:// 或 https:// 开头', upstream: '对接上游', upstreamDesc: '通过 Base URL + API Key 连接上游', api_key: 'API Key', @@ -1700,6 +1895,22 @@ export default { oauthPassthrough: '自动透传(仅替换认证)', oauthPassthroughDesc: '开启后,该 OpenAI 账号将自动透传请求与响应,仅替换认证并保留计费/并发/审计及必要安全过滤;如遇兼容性问题可随时关闭回滚。', + responsesWebsocketsV2: 'Responses WebSocket v2', + responsesWebsocketsV2Desc: + '默认关闭。开启后可启用 responses_websockets_v2 协议能力(受网关全局开关与账号类型开关约束)。', + wsMode: 'WS mode', + wsModeDesc: '仅对当前 OpenAI 账号类型生效。', + wsModeOff: '关闭(off)', + wsModeShared: '共享(shared)', + wsModeDedicated: '独享(dedicated)', + wsModeConcurrencyHint: '启用 WS mode 后,该账号并发数将作为该账号 WS 连接池上限。', + oauthResponsesWebsocketsV2: 'OAuth WebSocket Mode', + oauthResponsesWebsocketsV2Desc: + '仅对 OpenAI OAuth 生效。开启后该账号才允许使用 OpenAI WebSocket Mode 协议。', + apiKeyResponsesWebsocketsV2: 'API Key WebSocket Mode', + apiKeyResponsesWebsocketsV2Desc: + '仅对 OpenAI API Key 生效。开启后该账号才允许使用 OpenAI WebSocket Mode 协议。', + responsesWebsocketsV2PassthroughHint: '当前已开启自动透传:仅影响 HTTP 透传链路,不影响 WS mode。', codexCLIOnly: '仅允许 Codex 官方客户端', codexCLIOnlyDesc: '仅对 OpenAI OAuth 生效。开启后仅允许 Codex 官方客户端家族访问;关闭后完全绕过并保持原逻辑。', modelRestrictionDisabledByPassthrough: '已开启自动透传:模型白名单/映射不会生效。', @@ -1776,6 +1987,22 @@ export default { idleTimeoutPlaceholder: '5', idleTimeoutHint: '会话空闲超时后自动释放' }, + rpmLimit: { + label: 'RPM 限制', + hint: '限制每分钟请求数量,保护上游账号', + baseRpm: '基础 RPM', + baseRpmPlaceholder: '15', + baseRpmHint: '每分钟最大请求数,0 或留空表示不限制', + strategy: 'RPM 策略', + strategyTiered: '三区模型', + strategyStickyExempt: '粘性豁免', + strategyTieredHint: '绿区→黄区→仅粘性→阻塞,逐步限流', + strategyStickyExemptHint: '超限后仅允许粘性会话', + strategyHint: '三区模型: 超限后逐步限制; 粘性豁免: 已有会话不受限', + stickyBuffer: '粘性缓冲区', + stickyBufferPlaceholder: '默认: base RPM 的 20%', + stickyBufferHint: '超过 base RPM 后,粘性会话额外允许的请求数。为空则使用默认值(base RPM 的 20%,最小为 1)' + }, tlsFingerprint: { label: 'TLS 指纹模拟', hint: '模拟 Node.js/Claude Code 客户端的 TLS 指纹' @@ -1899,6 +2126,15 @@ export default { sessionTokenAuth: '手动输入 ST', sessionTokenDesc: '输入您已有的 Sora Session Token,支持批量输入(每行一个),系统将自动验证并创建账号。', sessionTokenPlaceholder: '粘贴您的 Sora Session Token...\n支持多个,每行一个', + sessionTokenRawLabel: '原始字符串', + sessionTokenRawPlaceholder: '粘贴 /api/auth/session 原始数据或 Session Token...', + sessionTokenRawHint: '支持粘贴完整 JSON,系统会自动解析 ST 和 AT。', + openSessionUrl: '打开获取链接', + copySessionUrl: '复制链接', + sessionUrlHint: '该链接通常可获取 AT。若返回中无 sessionToken,请从浏览器 Cookie 复制 __Secure-next-auth.session-token 作为 ST。', + parsedSessionTokensLabel: '解析出的 ST', + parsedSessionTokensEmpty: '未解析到 ST,请检查输入内容', + parsedAccessTokensLabel: '解析出的 AT', validating: '验证中...', validateAndCreate: '验证并创建账号', pleaseEnterRefreshToken: '请输入 Refresh Token', @@ -2142,6 +2378,7 @@ export default { selectTestModel: '选择测试模型', testModel: '测试模型', testPrompt: '提示词:"hi"', + soraUpstreamBaseUrlHint: '上游 Sora 服务地址(另一个 Sub2API 实例或兼容 API)', soraTestHint: 'Sora 测试将执行连通性与能力检测(/backend/me、订阅信息、Sora2 邀请码与剩余额度)。', soraTestTarget: '检测目标:Sora 账号能力', soraTestMode: '模式:连通性 + 能力探测', @@ -3517,15 +3754,21 @@ export default { hideCcsImportButtonHint: '启用后将在 API Keys 页面隐藏"导入 CCS"按钮' }, purchase: { - title: '购买订阅页面', - description: '在侧边栏展示“购买订阅”入口,并在页面内通过 iframe 打开指定链接', - enabled: '显示购买订阅入口', + title: '充值/订阅页面', + description: '在侧边栏展示“充值/订阅”入口,并在页面内通过 iframe 打开指定链接', + enabled: '显示充值/订阅入口', enabledHint: '仅在标准模式(非简单模式)下展示', - url: '购买页面 URL', + url: '充值/订阅页面 URL', urlPlaceholder: 'https://example.com/purchase', urlHint: '必须是完整的 http(s) 链接', iframeWarning: - '⚠️ iframe 提示:部分网站会通过 X-Frame-Options 或 CSP(frame-ancestors)禁止被 iframe 嵌入,出现空白时可引导用户使用“新窗口打开”。' + '⚠️ iframe 提示:部分网站会通过 X-Frame-Options 或 CSP(frame-ancestors)禁止被 iframe 嵌入,出现空白时可引导用户使用”新窗口打开”。' + }, + soraClient: { + title: 'Sora 客户端', + description: '控制是否在侧边栏展示 Sora 客户端入口', + enabled: '启用 Sora 客户端', + enabledHint: '开启后,侧边栏将显示 Sora 入口,用户可访问 Sora 功能' }, smtp: { title: 'SMTP 设置', @@ -3597,6 +3840,60 @@ export default { securityWarning: '警告:此密钥拥有完整的管理员权限,请妥善保管。', usage: '使用方法:在请求头中添加 x-api-key: ' }, + soraS3: { + title: 'Sora S3 存储配置', + description: '以多配置列表方式管理 Sora S3 端点,并可切换生效配置', + newProfile: '新建配置', + reloadProfiles: '刷新列表', + empty: '暂无 Sora S3 配置,请先创建', + createTitle: '新建 Sora S3 配置', + editTitle: '编辑 Sora S3 配置', + profileID: '配置 ID', + profileName: '配置名称', + setActive: '创建后设为生效', + saveProfile: '保存配置', + activateProfile: '设为生效', + profileCreated: 'Sora S3 配置创建成功', + profileSaved: 'Sora S3 配置保存成功', + profileDeleted: 'Sora S3 配置删除成功', + profileActivated: 'Sora S3 生效配置已切换', + profileIDRequired: '请填写配置 ID', + profileNameRequired: '请填写配置名称', + profileSelectRequired: '请先选择配置', + endpointRequired: '启用时必须填写 S3 端点', + bucketRequired: '启用时必须填写存储桶', + accessKeyRequired: '启用时必须填写 Access Key ID', + deleteConfirm: '确定删除 Sora S3 配置 {profileID} 吗?', + columns: { + profile: '配置', + active: '生效状态', + endpoint: '端点', + bucket: '存储桶', + quota: '默认配额', + updatedAt: '更新时间', + actions: '操作' + }, + enabled: '启用 S3 存储', + enabledHint: '启用后,Sora 生成的媒体文件将自动上传到 S3 存储', + endpoint: 'S3 端点', + region: '区域', + bucket: '存储桶', + prefix: '对象前缀', + accessKeyId: 'Access Key ID', + secretAccessKey: 'Secret Access Key', + secretConfigured: '(已配置,留空保持不变)', + cdnUrl: 'CDN URL', + cdnUrlHint: '可选,配置后使用 CDN URL 访问文件,否则使用预签名 URL', + forcePathStyle: '强制路径风格(Path Style)', + defaultQuota: '默认存储配额', + defaultQuotaHint: '未在用户或分组级别指定配额时的默认值,0 表示无限制', + testConnection: '测试连接', + testing: '测试中...', + testSuccess: 'S3 连接测试成功', + testFailed: 'S3 连接测试失败', + saved: 'Sora S3 设置保存成功', + saveFailed: '保存 Sora S3 设置失败' + }, streamTimeout: { title: '流超时处理', description: '配置上游响应超时时的账户处理策略,避免问题账户持续被选中', @@ -3748,15 +4045,15 @@ export default { retry: '重试' }, - // Purchase Subscription Page + // Recharge / Subscription Page purchase: { - title: '购买订阅', - description: '通过内嵌页面完成订阅购买', + title: '充值/订阅', + description: '通过内嵌页面完成充值/订阅', openInNewTab: '新窗口打开', notEnabledTitle: '该功能未开启', - notEnabledDesc: '管理员暂未开启购买订阅入口,请联系管理员。', - notConfiguredTitle: '购买链接未配置', - notConfiguredDesc: '管理员已开启入口,但尚未配置购买订阅链接,请联系管理员。' + notEnabledDesc: '管理员暂未开启充值/订阅入口,请联系管理员。', + notConfiguredTitle: '充值/订阅链接未配置', + notConfiguredDesc: '管理员已开启入口,但尚未配置充值/订阅链接,请联系管理员。' }, // Announcements Page @@ -3980,5 +4277,93 @@ export default { '

点击确认创建您的 API 密钥。

⚠️ 重要:
  • 创建后请立即复制密钥(sk-xxx)
  • 密钥只显示一次,丢失需重新生成

🚀 如何使用:
将密钥配置到支持 OpenAI 接口的任何客户端(如 ChatBox、OpenCat 等),即可开始使用!

👉 点击"创建"按钮

' } } + }, + + // Sora 创作 + sora: { + title: 'Sora 创作', + description: '使用 Sora AI 生成视频与图片', + notEnabled: '功能未开放', + notEnabledDesc: '管理员尚未启用 Sora 创作功能,请联系管理员开通。', + tabGenerate: '生成', + tabLibrary: '作品库', + noActiveGenerations: '暂无生成任务', + startGenerating: '在下方输入提示词,开始创作', + storage: '存储', + promptPlaceholder: '描述你想创作的内容...', + generate: '生成', + generating: '生成中...', + selectModel: '选择模型', + statusPending: '等待中', + statusGenerating: '生成中', + statusCompleted: '已完成', + statusFailed: '失败', + statusCancelled: '已取消', + cancel: '取消', + delete: '删除', + save: '保存到云端', + saved: '已保存', + retry: '重试', + download: '下载', + justNow: '刚刚', + minutesAgo: '{n} 分钟前', + hoursAgo: '{n} 小时前', + noSavedWorks: '暂无保存的作品', + saveWorksHint: '生成完成后,将作品保存到作品库', + filterAll: '全部', + filterVideo: '视频', + filterImage: '图片', + confirmDelete: '确定删除此作品?', + loading: '加载中...', + loadMore: '加载更多', + noStorageWarningTitle: '未配置存储', + noStorageWarningDesc: '生成的内容仅通过上游临时链接提供,约 15 分钟后过期。建议管理员配置 S3 存储。', + mediaTypeVideo: '视频', + mediaTypeImage: '图片', + notificationCompleted: '生成完成', + notificationFailed: '生成失败', + notificationCompletedBody: '您的 {model} 任务已完成', + notificationFailedBody: '您的 {model} 任务失败了', + upstreamExpiresSoon: '即将过期', + upstreamExpired: '链接已过期', + upstreamCountdown: '剩余 {time}', + previewTitle: '作品预览', + closePreview: '关闭', + beforeUnloadWarning: '您有未保存的生成内容,确定要离开吗?', + downloadTitle: '下载生成内容', + downloadExpirationWarning: '此链接约 15 分钟后过期,请尽快下载保存。', + downloadNow: '立即下载', + referenceImage: '参考图', + removeImage: '移除', + imageTooLarge: '图片大小不能超过 20MB', + // Sora 暗色主题新增 + welcomeTitle: '将你的想象力变成视频', + welcomeSubtitle: '输入一段描述,Sora 将为你创作逼真的视频或图片。尝试以下示例开始创作。', + queueTasks: '个任务', + queueWaiting: '队列中等待', + waiting: '等待中', + waited: '已等待', + errorCategory: '内容策略限制', + savedToCloud: '已保存到云端', + downloadLocal: '本地下载', + canDownload: '可下载', + regenrate: '重新生成', + creatorPlaceholder: '描述你想要生成的视频或图片...', + videoModels: '视频模型', + imageModels: '图片模型', + noStorageConfigured: '存储未配置', + selectCredential: '选择凭证', + apiKeys: 'API 密钥', + subscriptions: '订阅', + subscription: '订阅', + noCredentialHint: '请先创建 API Key 或联系管理员分配订阅', + uploadReference: '上传参考图片', + generatingCount: '正在生成 {current}/{max}', + noStorageToastMessage: '管理员未开通云存储,生成完成后请使用"本地下载"保存文件,否则将会丢失。', + galleryCount: '共 {count} 个作品', + galleryEmptyTitle: '还没有任何作品', + galleryEmptyDesc: '你的创作成果将会展示在这里。前往生成页,开始你的第一次创作吧。', + startCreating: '开始创作', + yesterday: '昨天' } }