Compare commits

...
3 Commits
Author SHA1 Message Date
ZyyandCodex e7e5b659cd chore(release): v3.31.3
Co-authored-by: Codex <noreply@openai.com>
2026-10-25 17:31:19 +08:00
ZyyandCodex b225b3732d fix(accounts): unify Codex labels and tighten plan badge spacing
Co-authored-by: Codex <noreply@openai.com>
2026-10-25 17:28:06 +08:00
ZyyandCodex 9dca763ea4 fix(ui): polish plugin management and API key labels
Co-authored-by: Codex <noreply@openai.com>
2026-10-25 16:57:42 +08:00
43 changed files with 296 additions and 236 deletions
+4 -4
View File
@@ -1774,7 +1774,7 @@ fn map_credential_admin_error(error: CodexCredentialAdminError) -> ProviderAdmin
"OpenAI 返回的 Codex PAT 身份资料不完整或格式无效,请稍后重试",
),
},
Error::InvalidInput => (Kind::Invalid, "OpenAI 账号输入格式不合法,请检查导入内容"),
Error::InvalidInput => (Kind::Invalid, "Codex 账号输入格式不合法,请检查导入内容"),
Error::InvalidCredential => (
Kind::Invalid,
"OpenAI 凭据不完整或格式无效,请检查凭据或重新授权",
@@ -1784,8 +1784,8 @@ fn map_credential_admin_error(error: CodexCredentialAdminError) -> ProviderAdmin
Kind::Invalid,
refresh_rejection_message(code.as_deref()).unwrap_or("刷新令牌已失效,请重新授权"),
),
Error::AccountBanned { .. } => (Kind::Invalid, "OpenAI 账号已被停用,请检查账号状态"),
Error::NotFound => (Kind::NotFound, "OpenAI 账号不存在,请刷新账号列表"),
Error::AccountBanned { .. } => (Kind::Invalid, "Codex 账号已被停用,请检查账号状态"),
Error::NotFound => (Kind::NotFound, "Codex 账号不存在,请刷新账号列表"),
Error::RefreshLeaseUnavailable => {
(Kind::Conflict, "令牌刷新繁忙,请等待当前刷新完成后重试")
}
@@ -1920,7 +1920,7 @@ fn map_quota_error(error: CodexCredentialQuotaError) -> ProviderAdminError {
"OpenAI 额度查询凭据无效,请检查账号授权",
None,
),
Error::NotFound => (Kind::NotFound, "OpenAI 额度查询账号不存在", None),
Error::NotFound => (Kind::NotFound, "Codex 额度查询账号不存在", None),
Error::RevisionConflict => (
Kind::Conflict,
"账号凭据已被更新,请刷新账号列表后重试",
@@ -2443,7 +2443,7 @@ mod errors {
#[tokio::test]
async fn manual_refresh_preserves_banned_evidence_without_promoting_401_to_terminal() {
for (status, kind, message) in [
(400, Kind::Invalid, "OpenAI 账号已被停用,请检查账号状态"),
(400, Kind::Invalid, "Codex 账号已被停用,请检查账号状态"),
(
401,
Kind::BadGateway,
+2 -2
View File
@@ -387,7 +387,7 @@ V2 远程压缩通过 Responses 接口发送 `compaction_trigger`,网关和上
模板启用原生生图。Codex 可在任务需要图片时调用 `image_gen.imagegen`,
再通过代理的 Images 接口生成或编辑图片;也可以在需求中明确要求生成并使用图片。
需要支持该能力的客户端、支持图片输入的对话模型,以及有生图权限和额度的 OpenAI 账号。
需要支持该能力的客户端、支持图片输入的对话模型,以及有生图权限和额度的 Codex 账号。
代理不会增加上游权限,xAI 账号不能承接这些 Images 请求
生图不要求开启 WebSocket。要启用客户端 WebSocket,把当前 Provider 的
@@ -395,7 +395,7 @@ V2 远程压缩通过 Responses 接口发送 `compaction_trigger`,网关和上
客户端到代理、代理到上游是两段独立连接。客户端关闭 WebSocket 后,
服务端仍可能用 WebSocket 访问上游;客户端开关不控制服务端连接池和 HTTP 回退策略。
OpenAI OAuth 账号的上游传输方式默认 WS;固定为 SSE 的账号不承接必须依赖 WS 的预热及连接内续接。
Codex OAuth 账号的上游传输方式默认 WS;固定为 SSE 的账号不承接必须依赖 WS 的预热及连接内续接。
仅使用这类账号时,客户端保持 `supports_websockets = false`,避免先尝试 WS 再回退
### 时区与客户端环境信息
+7 -7
View File
@@ -1087,7 +1087,7 @@ OpenAI 的 CPR 导出保持 OAuth 账号的既有 token 与过期时间字段。
导入条目未提供 `modelAccess` 且 `settings` 未指定该字段时,新账号默认 `all`,已有账号保留原值;凭据刷新、重新授权和目录刷新均保留政策。
sub2api 的 `credentials.model_mapping` 不转换为本项目的账号模型限制
编辑 OpenAI API Key 账号时,在 `POST /api/admin/accounts/update` 的账号设置中附带 `connection`:
编辑 Codex API Key 账号时,在 `POST /api/admin/accounts/update` 的账号设置中附带 `connection`:
```json
{
@@ -1106,7 +1106,7 @@ sub2api 的 `credentials.model_mapping` 不转换为本项目的账号模型限
`connection.transport` 支持 `http`、`prefer_websocket`。省略 `connection` 时只更新账号设置;
省略 `connection.apiKey` 保留当前密钥,空字符串无效。
OpenAI OAuth 账号只接受 `connection: { transport }`,不接受 `baseUrl`、`apiKey` 或 OAuth token。
Codex OAuth 账号只接受 `connection: { transport }`,不接受 `baseUrl`、`apiKey` 或 OAuth token。
连接设置不能修改账号 ID、Provider 或认证类型,也不接受通用凭据文档。
凭据与设置在同一事务中保存,任一校验或持久化失败均不落库。
`GET /api/admin/accounts/detail` 对 API Key 账号额外返回 `credentialConfiguration: { base_url, transport }`;
@@ -1160,7 +1160,7 @@ OAuth start 使用:
失败会释放流程占用,但上游 authorization code 通常只能交换一次;已完成 token exchange 而未提交账号时应重新创建 OAuth flow
- `GET /api/admin/accounts/quota` 只读取最后一次落库快照;`POST /api/admin/accounts/quota/refresh` 才访问上游。access token
已过期时,额度刷新要求先走 credential 刷新或重新授权,不会拿过期 token 探测额度
- OpenAI 已耗尽账号首次可复核,在 `resetAt` 尚未到达或未知时按 30 分钟间隔兜底,也可发现
- Codex 已耗尽账号首次可复核,在 `resetAt` 尚未到达或未知时按 30 分钟间隔兜底,也可发现
官方提前重置。到达最早未恢复窗口的 `resetAt` 后,先等待 10 秒传播余量,再在下一轮后台检查
复核;reset 后前 5 分钟最小查询间隔为 30 秒,之后仍耗尽则改为 5 分钟。正常账号有非零用量或触顶窗口时,仍在
`resetAt + 2 分钟` 后复核,未更新则按 30 分钟间隔重试。后台每 30 秒检查条件,实际恢复时间还受
@@ -1225,7 +1225,7 @@ OAuth start 使用:
等价 USD 费用不是订阅价格或额度承诺,30 天折算不是自然月额度,记录覆盖率也不代表预测准确率
### OpenAI 账号辅助请求
### Codex 账号辅助请求
OAuth 账号的额度、个人资料、订阅与重置卡请求先使用 `openai.api.base_url`。自定义上游明确返回 HTTP 404 时,
仅回退一次到官方账号接口,沿用当前账号凭据与出站代理;其他状态码、解析错误或传输失败不触发回退。
@@ -1235,7 +1235,7 @@ OAuth 账号的额度、个人资料、订阅与重置卡请求先使用 `openai
### 账号个人信息
`GET /api/admin/accounts/personal-info?accountId=...` 需要管理员会话,提供 OpenAI/Codex OAuth 账号的个人信息。后端并发读取资料统计与订阅,
`GET /api/admin/accounts/personal-info?accountId=...` 需要管理员会话,提供 Codex OAuth 账号的个人信息。后端并发读取资料统计与订阅,
一次返回;每次请求均重新查询,不刷新 credential,不读取本地 usage/billing 记录,也不缓存或估算统计结果。
查询沿用上述 OpenAI 404 路由回退
@@ -1558,7 +1558,7 @@ HTTP 返回 `429`,`error.code` 为 `key_daily_budget_exceeded` 或 `key_weekly
定时账号预热默认关闭。`accountWarmupScheduleTime` 使用部署时区中的 `HH:MM`,
多个时段以逗号分隔,默认 `08:00`;`accountWarmupModel` 默认 `null`,开启前必须显式选择模型。
任务面向可用的 OpenAI OAuth 账号,跳过周额度耗尽及五小时窗口距离重置仍超过 30 分钟的账号。
任务面向可用的 Codex OAuth 账号,跳过周额度耗尽及五小时窗口距离重置仍超过 30 分钟的账号。
不存在的本地时刻跳过,重复时刻只执行较早一次;执行进度跨重启保留,时钟回拨不补跑已领取时刻之前的时段。
只有收到响应成功终态才记为预热成功;预热不计入客户端业务用量
@@ -1653,7 +1653,7 @@ Guardian 以 `subagent_kind` 或 `client_metadata.x-openai-subagent` 值 `guardi
### 账号亲和与换号
`openaiAccountAffinity` 控制 OpenAI 账号亲和,默认 `strict`(严格):
`openaiAccountAffinity` 控制 Codex 账号亲和,默认 `strict`(严格):
- `relaxed`(宽松):会话内请求直接按 `rotationStrategy` 选号,不优先主账号
- `preferred`(优先):会话内所有可关联请求优先主账号,并发已满、额度耗尽、停用、限流冷却或不支持当前模型时临时分流,保留会话绑定,后续请求仍优先主账号
+1 -1
View File
@@ -438,7 +438,7 @@ Client Key 鉴权完成后,API adapter 从有界请求头识别客户端,Cor
Responses 按模型目录编译候选;全局模型映射是精确映射,未命中时模型名原样交给候选 Provider。
Images 与 standalone Search 是 OpenAI Provider 自有端点:两者都不参与文本模型映射,只在 Client Key
的账号范围确实包含 OpenAI 账号时生成单一 OpenAI 候选。Images 的图片模型由 Provider 从正文旁路
的账号范围确实包含 Codex 账号时生成单一 OpenAI 候选。Images 的图片模型由 Provider 从正文旁路
提取,用于账号模型政策检查,并在中间件改写后复验当前租约账号;不要求图片模型进入文本目录。
Search 的请求身份与正文透传规则见 [OpenAI 透传](api.md#openai-透传)
+5 -6
View File
@@ -58,10 +58,9 @@
"checkBasicSettings": "Please check basic settings",
"noParameters": "No plugin parameters required",
"sensitiveConfiguration": "Sensitive configuration",
"secretsHelpTitle": "About secret configuration",
"secretsHelp": "Secrets are stored separately and never displayed, Keep secrets preserves existing values and Replace all secrets overwrites them with all values entered here",
"secretsHelp": "Saved tokens, keys and other private values are never shown",
"settingsSaved": "Settings saved: {count}",
"clearSecretsHelp": "Clear all secrets when saving",
"emptySecretPlaceholder": "Leave blank to clear this value",
"about": "About {name}",
"enterNewValue": "Enter new value",
"pluginParameters": "Parameters",
@@ -70,9 +69,9 @@
"applyAndSwitch": "Apply and switch",
"saveAndEnable": "Save and enable",
"saveSettings": "Save",
"keepExistingValues": "Keep secrets",
"replaceAllValues": "Replace all secrets",
"clearAllValues": "Clear all secrets",
"keepExistingValues": "Keep existing",
"replaceAllValues": "Replace all",
"clearAllValues": "Clear all",
"checkPluginParameters": "Please check plugin parameters",
"setRequestScopeOrCheckApplyTo": "Please set the request scope, or check \"Apply to all requests\"",
"invalidIdentityMappings": "Check the client authentication identity mappings",
+2 -2
View File
@@ -150,7 +150,7 @@
},
"scheduling": {
"roundRobin": "Round robin",
"autoReviewSlotsHelp": "Applies to OpenAI accounts only: with a regular concurrency limit of 10 and 3 auto review slots, up to 10 regular requests and 3 auto review requests can run independently",
"autoReviewSlotsHelp": "Applies to Codex accounts only: with a regular concurrency limit of 10 and 3 auto review slots, up to 10 regular requests and 3 auto review requests can run independently",
"autoReviewQueueHelp": "With dedicated slots, auto review uses a separate queue that may exceed account queue capacity, while total queue capacity, queue timeout, and API key limits still apply",
"weights": {
"load": "Load weight",
@@ -282,7 +282,7 @@
"queueTimeoutHelp": "Shared by both queues, 1–120 seconds",
"autoReview": {
"label": "Reserved auto review slots",
"description": "Extra slots per OpenAI account, 0 shares regular concurrency",
"description": "Extra slots per Codex account, 0 shares regular concurrency",
"helpTitle": "About auto review concurrency"
},
"queueTimeoutHelpTitle": "About queue timeout",
@@ -70,7 +70,8 @@
"plugins": {
"title": "Plugin compatibility notice",
"description": "Keep enabled configurations and try starting each plugin after restarting",
"incompatible": "Incompatible plugins"
"incompatible": "Incompatible plugins",
"requestErrorHelp": "If requests fail even after plugins start successfully, try disabling the relevant plugins and retrying"
},
"versionAfterRestart": "Version after restart",
"confirmation": {
@@ -132,10 +132,10 @@
"urlLength": "上游 API 地址不能超过 2048 个字符",
"urlFormat": "请输入不含认证、查询参数或片段的 HTTP 或 HTTPS 地址",
"urlRequired": "请输入完整的上游 API 地址",
"apiKeyFormat": "API Key 不能包含空格或控制字符",
"apiKeyFormat": "API密钥不能包含空格或控制字符",
"transportRequired": "请选择传输方式"
},
"apiKeyRequired": "请输入 API Key"
"apiKeyRequired": "请输入 API密钥"
},
"batchEdit": {
"title": "批量编辑账号",
@@ -167,7 +167,7 @@
"loadError": "上游设置读取失败",
"notesHelp": "最多 500 字,留空可清除备注",
"oauthUnavailable": "该账号没有 OAuth 上游设置",
"apiKeyUnavailable": "该账号没有 API Key 上游设置",
"apiKeyUnavailable": "该账号没有 API密钥上游设置",
"saved": "账号已更新"
},
"filters": {
@@ -347,8 +347,8 @@
"accountAdded": "账号已添加",
"providerRequired": "请选择账号平台",
"accountPlatformIsNotSupported": "账号平台不受支持",
"apiKeyUnsupported": "当前平台不支持 API Key 账号",
"apiKeyAccountAdded": "API Key 账号已添加",
"apiKeyUnsupported": "当前平台不支持 API密钥账号",
"apiKeyAccountAdded": "API密钥账号已添加",
"importTasksCreated": "已创建 {submitted} 个导入任务",
"importTaskCreated": "导入任务已创建",
"partialSubmission": "部分导入任务已创建,保持输入不变重试可继续提交",
@@ -111,7 +111,7 @@
},
"dashboard": {
"justUpdated": "刚刚更新",
"demoKey": "演示 Key",
"demoKey": "演示 API密钥",
"requestHealthTimeline": "请求健康时间线",
"validRequestAvailability": "有效请求可用性",
"todaySToken": "今日 Token",
+4 -4
View File
@@ -2,7 +2,7 @@
"auth": {
"signOut": "退出登录",
"adminSignIn": "管理员登录",
"apiKeySignIn": "API Key 登录",
"apiKeySignIn": "API密钥登录",
"signingIn": "正在登录",
"signIn": "登录",
"method": "选择登录方式",
@@ -11,11 +11,11 @@
"username": "管理员账号",
"usernamePlaceholder": "输入会话账号",
"passwordPlaceholder": "输入会话密钥",
"apiKeyPlaceholder": "输入 API Key",
"apiKeyPlaceholder": "输入 API密钥",
"pageLabel": "Codex Proxy RS 登录",
"password": "密码",
"apiKey": "API 密钥",
"apiKey": "API密钥",
"adminRealm": "管理员",
"keyRealm": "API 密钥"
"keyRealm": "API密钥"
}
}
@@ -28,7 +28,7 @@
"malformedJson": "请求体不是合法 JSON",
"invalidTimeRange": "时间范围不合法",
"invalidCredentials": "登录凭据错误",
"invalidAdminApiKey": "管理 API 密钥无效",
"invalidAdminApiKey": "管理 API密钥无效",
"tooManyLoginAttempts": "登录尝试过多,请稍后重试",
"upstreamResultUnknown": "上游执行结果未知,请刷新状态后再决定是否重试"
},
@@ -112,7 +112,7 @@
"name": "名称",
"createdAt": "创建时间",
"baseUrl": "基础 URL",
"apiKey": "API 密钥",
"apiKey": "API密钥",
"build": "构建"
},
"language": "界面语言",
@@ -53,7 +53,7 @@
"capacity": "容量"
},
"page": {
"description": "将账号归类管理,并为每个 API 密钥指定可使用的账号",
"description": "将账号归类管理,并为每个 API密钥指定可使用的账号",
"empty": "暂无分组",
"selectCurrentPageGroup": "选择当前页分组",
"selectGroup": "选择分组",
@@ -64,11 +64,11 @@
"batchDeleteWarning": "删除后这些分组将立即失效,此操作不可撤销",
"batchDeleteDescription": "确定删除选中的 {count} 个分组吗?账号本身不会被删除",
"disableAccountGrouping": "禁用账号分组",
"disableDescription": "禁用后,使用该分组的 API 密钥将无法再使用其中的账号",
"disableDescription": "禁用后,使用该分组的 API密钥将无法再使用其中的账号",
"confirmDisable": "确认禁用",
"disable": "确定禁用“{name}”吗?",
"affectedApiKeys": "将影响 {count} 个 API 密钥:{value}",
"affectedKeys": "将影响 {count} 个 API 密钥",
"affectedApiKeys": "将影响 {count} 个 API密钥:{value}",
"affectedKeys": "将影响 {count} 个 API密钥",
"deleteAccountGroup": "删除账号分组",
"deleteWarning": "删除后该分组将立即失效,此操作不可撤销",
"unnamedGroup": "该分组",
+18 -18
View File
@@ -9,7 +9,7 @@
"ccSwitchDescription": "管理和切换 AI 客户端配置",
"viewOnGitHub": "在 GitHub 查看 {name}",
"copiedToClipboard": "已复制到剪贴板",
"fullApiKeyNotAvailable": "完整 API Key 不可用",
"fullApiKeyNotAvailable": "完整 API密钥不可用",
"title": "使用密钥"
},
"actions": {
@@ -20,7 +20,7 @@
"disableKey": "停用",
"enableKey": "启用",
"deleteKey": "删除",
"deletedApiKeyDeletedApiKeys": "已删除 {count} 个 API Key",
"deletedApiKeyDeletedApiKeys": "已删除 {count} 个 API密钥",
"configure": "配置"
},
"day": "日",
@@ -43,24 +43,24 @@
"confirmReset": "确认重置"
},
"created": {
"apiKeyCreated": "API Key 已创建",
"apiKeyCreated": "API密钥已创建",
"description": "复制密钥,或导入到客户端",
"accessNotice": "该密钥具有网关访问权限,请仅发送给可信调用方",
"iHaveSaved": "我已保存"
},
"filters": {
"apiKeyFilteringAndOperations": "API Key 筛选与操作",
"apiKeyFilteringAndOperations": "API密钥筛选与操作",
"searchPlaceholder": "搜索名称或标签",
"searchLabel": "搜索 API Key 名称或标签",
"searchLabel": "搜索 API密钥名称或标签",
"deleteSelected": "删除 ({count})",
"createApiKey": "创建 API Key"
"createApiKey": "创建 API密钥"
},
"editor": {
"description": "配置密钥信息、分组与使用限制",
"namePlaceholder": "例如:生产环境",
"labelOptional": "标签(可选)",
"forExampleBackendService": "例如:后端服务",
"customKeyOptional": "自定义 Key(可选)",
"customKeyOptional": "自定义 API密钥(可选)",
"keyPlaceholder": "留空自动生成",
"upstreamIdentity": "上游身份",
"dailyLimit": "日限额",
@@ -69,14 +69,14 @@
"weeklyLimitUsd": "周限额(USD)",
"maximumConcurrency": "最大并发",
"requestsPerMinuteRpm": "每分钟请求数(RPM)",
"updated": "API Key 已更新",
"created": "API Key 已创建",
"updated": "API密钥已更新",
"created": "API密钥已创建",
"validation": {
"amount": "{label}必须是非负金额,最多 10 位小数",
"integer": "{label}必须是非负整数"
},
"enterApiKeyName": "请输入 API Key 名称",
"invalidKey": "自定义 Key 只能包含 HTTP 可传输的可见字符,不能包含空格或换行",
"enterApiKeyName": "请输入 API密钥名称",
"invalidKey": "自定义 API密钥只能包含 HTTP 可传输的可见字符,不能包含空格或换行",
"requestsPerMinute": "每分钟请求数"
},
"fields": {
@@ -88,16 +88,16 @@
"lastUsed": "最后使用"
},
"page": {
"description": "管理 API 密钥及账号访问范围",
"noApiKeyYet": "暂无 API Key",
"description": "管理 API密钥及账号访问范围",
"noApiKeyYet": "暂无 API密钥",
"selectCurrentPageKey": "选择当前页密钥",
"selectKey": "选择密钥",
"concurrency": "并发",
"batchDeleteWarning": "删除后这些 API Key 将立即失效,此操作不可撤销",
"batchDeleteDescription": "确定删除选中的 {count} 个 API Key 吗?",
"deleteApiKey": "删除 API Key",
"deleteWarning": "删除后该 API Key 将立即失效,此操作不可撤销",
"apiKey": "该 API Key",
"batchDeleteWarning": "删除后这些 API密钥将立即失效,此操作不可撤销",
"batchDeleteDescription": "确定删除选中的 {count} 个 API密钥吗?",
"deleteApiKey": "删除 API密钥",
"deleteWarning": "删除后该 API密钥将立即失效,此操作不可撤销",
"apiKey": "该 API密钥",
"delete": "确定删除 {name} 吗?"
},
"scope": {
@@ -4,7 +4,7 @@
"accounts": "账号管理",
"proxies": "代理管理",
"groups": "分组管理",
"apiKeys": "API 密钥",
"apiKeys": "API密钥",
"usage": "分析统计",
"plugins": "插件",
"appearance": "主题设置",
+16 -17
View File
@@ -11,7 +11,7 @@
"applyToAllRequests": "应用于所有请求",
"scopeHelpTitle": "生效请求范围说明",
"scopeHelp": "关闭后至少选择一项范围,不同条件同时满足才生效,同一条件内任意一项匹配即可",
"clientKey": "客户端 Key",
"clientKey": "API密钥",
"accountGrouping": "账号分组",
"modelScope": "模型范围",
"oneModelNamePerLineExactMatch": "每行一个模型名称,精确匹配",
@@ -58,10 +58,9 @@
"checkBasicSettings": "请检查基本设置",
"noParameters": "无需填写插件参数",
"sensitiveConfiguration": "敏感配置",
"secretsHelpTitle": "敏感配置说明",
"secretsHelp": "敏感配置单独保存,不回显已有值,选择保留时不会更改,选择替换时以本次填写的全部值覆盖",
"secretsHelp": "用于令牌、密钥等机密参数,已保存的值不会回显",
"settingsSaved": "已保存 {count} 项",
"clearSecretsHelp": "保存时清除全部敏感配置",
"emptySecretPlaceholder": "留空将清除此项",
"about": "{name}说明",
"enterNewValue": "输入新值",
"pluginParameters": "插件参数",
@@ -70,9 +69,9 @@
"applyAndSwitch": "应用并切换",
"saveAndEnable": "保存并启用",
"saveSettings": "保存设置",
"keepExistingValues": "保留敏感配置",
"replaceAllValues": "替换全部敏感配置",
"clearAllValues": "清除全部敏感配置",
"keepExistingValues": "保留现有值",
"replaceAllValues": "替换全部",
"clearAllValues": "清除全部",
"checkPluginParameters": "请检查插件参数",
"setRequestScopeOrCheckApplyTo": "请设置请求范围,或勾选“应用于所有请求”",
"invalidIdentityMappings": "请检查客户端认证身份映射",
@@ -87,7 +86,7 @@
"description": "按配置提供扩展功能",
"providesTerminalCommands": "提供终端命令",
"requestHandlingNotConfigured": "未配置请求处理",
"apiKeys": "{count} 个 Key",
"apiKeys": "{count} 个 API密钥",
"groups": "{count} 个分组",
"providers": "{count} 个 Provider",
"identityMappings": "{count} 组身份映射",
@@ -175,7 +174,7 @@
},
"frontendAuth": {
"exclusiveCertification": "独占认证",
"fallbackApiKeyIfNoMatch": "未匹配时回退 API Key",
"fallbackApiKeyIfNoMatch": "未匹配时回退 API密钥",
"disabled": "(已停用)",
"configuredUnavailable": "已配置但当前不可用",
"invalidBinding": "客户端认证绑定必须使用认证阶段和受支持的未匹配处理",
@@ -184,30 +183,30 @@
"mappingLimit": "身份映射不能超过 256 条",
"invalidIdentity": "身份标识必须是 1–256 个 UTF-8 字节,且不能含首尾空白或控制字符",
"duplicateIdentity": "同一身份标识只能配置一次",
"keyRequired": "请为每个身份标识选择客户端 Key",
"keyRequired": "请为每个身份标识选择 API密钥",
"repeatedCursor": "服务返回了重复的分页位置",
"failedToLoadClientKey": "加载客户端 Key 失败",
"failedToLoadClientKey": "加载 API密钥失败",
"enableClientAuthentication": "启用客户端认证",
"helpTitle": "客户端认证说明",
"description": "插件识别请求中的外部身份,宿主再将该身份映射到已有 Client Key,沿用这个 Key 的限额、账号范围与并发策略",
"description": "插件识别请求中的外部身份,宿主再将该身份映射到已有 API密钥,沿用这个 API密钥的限额、账号范围与并发策略",
"loginNotice": "此功能用于客户端 API 请求,不替代管理端登录",
"unmatchedProcessing": "未匹配处理",
"unmatchedHandlingInstructions": "未匹配处理说明",
"exclusiveHelp": "独占认证:插件未识别到身份时拒绝请求",
"fallbackToApiKeyWhenNoMatch": "未匹配时回退 API Key:插件未识别到身份时,继续使用宿主原有的 API Key 认证",
"fallbackToApiKeyWhenNoMatch": "未匹配时回退 API密钥:插件未识别到身份时,继续使用宿主原有的 API密钥认证",
"rejectionHelp": "插件明确拒绝或调用失败时始终拒绝请求,不会回退",
"unmatchedLabel": "客户端认证未匹配处理",
"identityMapping": "身份映射",
"identityMappingDescription": "身份映射说明",
"identityHelp": "身份标识必须与插件返回的值完全一致,同一身份只能映射一次",
"keyHelp": "选择该身份使用的 Client Key,已停用或删除的 Key 不会获得执行权限",
"reloadClientKey": "重新加载客户端 Key",
"keyHelp": "选择该身份使用的 API密钥,已停用或删除的 API密钥不会获得执行权限",
"reloadClientKey": "重新加载 API密钥",
"addMapping": "添加映射",
"identityMark": "身份标识",
"identityPlaceholder": "填写插件返回的身份标识",
"failedToLoadTryAgain": "加载失败,请重试",
"selectClientApiKey": "选择客户端 Key",
"noClientKeyYet": "暂无客户端 Key",
"selectClientApiKey": "选择 API密钥",
"noClientKeyYet": "暂无 API密钥",
"removeIdentityMapping": "移除第 {value} 条身份映射"
},
"installation": {
+13 -13
View File
@@ -150,7 +150,7 @@
},
"scheduling": {
"roundRobin": "轮询",
"autoReviewSlotsHelp": "仅对 OpenAI 账号生效,普通并发为 10、审批名额为 3 时,两类请求分别最多运行 10 个和 3 个,互不占用名额",
"autoReviewSlotsHelp": "仅对 Codex 账号生效,普通并发为 10、审批名额为 3 时,两类请求分别最多运行 10 个和 3 个,互不占用名额",
"autoReviewQueueHelp": "开启独立额度后,审批请求单独排队,可超过账号队列容量,仍受总等待容量、排队超时与密钥限额约束",
"weights": {
"load": "负载系数",
@@ -232,17 +232,17 @@
"help": "在设定时间向每个空闲的 Codex OAuth 账号发送一次请求,不保证额度窗口成功启动"
},
"adminApiKey": {
"adminApiKey": "管理员 API Key",
"adminApiKey": "管理员 API密钥",
"description": "用于外部系统集成,具有管理员权限",
"regenerate": "重新生成",
"generate": "生成",
"notGenerated": "未生成",
"enabledDescription": "管理员 API 访问已启用,完整 Key 仅在生成时回显",
"disabledDescription": "外部系统暂时无法通过 API Key 调用管理接口",
"generatedNotice": "完整 Key 仅显示一次,请立即保存",
"adminApiKeyUpdated": "管理员 API Key 已更新",
"generated": "管理员 API Key 已生成",
"adminApiKeyDeleted": "管理员 API Key 已删除"
"enabledDescription": "管理员 API 访问已启用,完整 API密钥仅在生成时回显",
"disabledDescription": "外部系统暂时无法通过 API密钥调用管理接口",
"generatedNotice": "完整 API密钥仅显示一次,请立即保存",
"adminApiKeyUpdated": "管理员 API密钥已更新",
"generated": "管理员 API密钥已生成",
"adminApiKeyDeleted": "管理员 API密钥已删除"
},
"password": {
"administratorPassword": "管理员密码",
@@ -282,7 +282,7 @@
"queueTimeoutHelp": "两个队列共用,1–120 秒",
"autoReview": {
"label": "自动审批独立并发",
"description": "每个 OpenAI 账号的额外名额,0 共用普通并发",
"description": "每个 Codex 账号的额外名额,0 共用普通并发",
"helpTitle": "自动审批独立并发说明"
},
"queueTimeoutHelpTitle": "排队超时说明",
@@ -344,9 +344,9 @@
},
"access": {
"accessRestrictions": "访问限制",
"deleteAdminApiKey": "删除管理员 API Key",
"deleteKeyDescription": "删除后外部系统将无法继续使用该 Key 调用管理接口",
"areYouSureYouWantToDelete": "确定要删除当前管理员 API Key 吗?此操作会立即生效"
"deleteAdminApiKey": "删除管理员 API密钥",
"deleteKeyDescription": "删除后外部系统将无法继续使用该 API密钥调用管理接口",
"areYouSureYouWantToDelete": "确定要删除当前管理员 API密钥吗?此操作会立即生效"
},
"tokenRefresh": {
"title": "凭据刷新",
@@ -366,7 +366,7 @@
"exclusions": "不包含请求明细与日志",
"backupRecords": "备份记录",
"backupScopeDescription": "备份范围说明",
"contents": "保留账号、Key、设置、插件及已用额度",
"contents": "保留账号、API密钥、设置、插件及已用额度",
"usageRetentionHourSummary": "用量保留小时汇总",
"creating": "创建中",
"createBackup": "创建备份",
@@ -70,7 +70,8 @@
"plugins": {
"title": "插件兼容性提醒",
"description": "重启后保留启用配置,逐个尝试启动插件",
"incompatible": "不兼容插件"
"incompatible": "不兼容插件",
"requestErrorHelp": "插件启动成功后仍可能出现请求错误,可尝试禁用相关插件后重试"
},
"versionAfterRestart": "重启后版本",
"confirmation": {
+1 -1
View File
@@ -283,7 +283,7 @@
"requestId": "请求 ID",
"upstreamRequestId": "上游请求 ID",
"responseId": "响应 ID",
"clientKeyId": "客户端 Key ID",
"clientKeyId": "API密钥 ID",
"errorCode": "错误码",
"failureClassification": "失败分类",
"duration": "耗时",
@@ -132,10 +132,10 @@
"urlLength": "上游 API 地址不能超過 2048 個字符",
"urlFormat": "請輸入不含認證、查詢參數或片段的 HTTP 或 HTTPS 地址",
"urlRequired": "請輸入完整的上游 API 地址",
"apiKeyFormat": "API Key 不能包含空格或控制字符",
"apiKeyFormat": "API密鑰不能包含空格或控制字符",
"transportRequired": "請選擇傳輸方式"
},
"apiKeyRequired": "請輸入 API Key"
"apiKeyRequired": "請輸入 API密鑰"
},
"batchEdit": {
"title": "批量編輯賬號",
@@ -167,7 +167,7 @@
"loadError": "上游設置讀取失敗",
"notesHelp": "最多 500 字,留空可清除備註",
"oauthUnavailable": "該賬號沒有 OAuth 上游設置",
"apiKeyUnavailable": "該賬號沒有 API Key 上游設置",
"apiKeyUnavailable": "該賬號沒有 API密鑰上游設置",
"saved": "賬號已更新"
},
"filters": {
@@ -347,8 +347,8 @@
"accountAdded": "賬號已添加",
"providerRequired": "請選擇賬號平臺",
"accountPlatformIsNotSupported": "賬號平臺不受支持",
"apiKeyUnsupported": "當前平臺不支持 API Key 賬號",
"apiKeyAccountAdded": "API Key 賬號已添加",
"apiKeyUnsupported": "當前平臺不支持 API密鑰賬號",
"apiKeyAccountAdded": "API密鑰賬號已添加",
"importTasksCreated": "已創建 {submitted} 個導入任務",
"importTaskCreated": "導入任務已創建",
"partialSubmission": "部分導入任務已創建,保持輸入不變重試可繼續提交",
@@ -111,7 +111,7 @@
},
"dashboard": {
"justUpdated": "剛剛更新",
"demoKey": "演示 Key",
"demoKey": "演示 API密鑰",
"requestHealthTimeline": "請求健康時間線",
"validRequestAvailability": "有效請求可用性",
"todaySToken": "今日 Token",
+4 -4
View File
@@ -2,7 +2,7 @@
"auth": {
"signOut": "退出登錄",
"adminSignIn": "管理員登錄",
"apiKeySignIn": "API Key 登錄",
"apiKeySignIn": "API密鑰登錄",
"signingIn": "正在登錄",
"signIn": "登錄",
"method": "選擇登錄方式",
@@ -11,11 +11,11 @@
"username": "管理員賬號",
"usernamePlaceholder": "輸入會話賬號",
"passwordPlaceholder": "輸入會話密鑰",
"apiKeyPlaceholder": "輸入 API Key",
"apiKeyPlaceholder": "輸入 API密鑰",
"pageLabel": "Codex Proxy RS 登錄",
"password": "密碼",
"apiKey": "API 密鑰",
"apiKey": "API密鑰",
"adminRealm": "管理員",
"keyRealm": "API 密鑰"
"keyRealm": "API密鑰"
}
}
@@ -28,7 +28,7 @@
"malformedJson": "請求體不是合法 JSON",
"invalidTimeRange": "時間範圍不合法",
"invalidCredentials": "登錄憑據錯誤",
"invalidAdminApiKey": "管理 API 密鑰無效",
"invalidAdminApiKey": "管理 API密鑰無效",
"tooManyLoginAttempts": "登錄嘗試過多,請稍後重試",
"upstreamResultUnknown": "上游執行結果未知,請刷新狀態後再決定是否重試"
},
@@ -112,7 +112,7 @@
"name": "名稱",
"createdAt": "創建時間",
"baseUrl": "基礎 URL",
"apiKey": "API 密鑰",
"apiKey": "API密鑰",
"build": "構建"
},
"language": "界面語言",
@@ -53,7 +53,7 @@
"capacity": "容量"
},
"page": {
"description": "將賬號歸類管理,併爲每個 API 密鑰指定可使用的賬號",
"description": "將賬號歸類管理,併爲每個 API密鑰指定可使用的賬號",
"empty": "暫無分組",
"selectCurrentPageGroup": "選擇當前頁分組",
"selectGroup": "選擇分組",
@@ -64,11 +64,11 @@
"batchDeleteWarning": "刪除後這些分組將立即失效,此操作不可撤銷",
"batchDeleteDescription": "確定刪除選中的 {count} 個分組嗎?賬號本身不會被刪除",
"disableAccountGrouping": "禁用賬號分組",
"disableDescription": "禁用後,使用該分組的 API 密鑰將無法再使用其中的賬號",
"disableDescription": "禁用後,使用該分組的 API密鑰將無法再使用其中的賬號",
"confirmDisable": "確認禁用",
"disable": "確定禁用“{name}”嗎?",
"affectedApiKeys": "將影響 {count} 個 API 密鑰:{value}",
"affectedKeys": "將影響 {count} 個 API 密鑰",
"affectedApiKeys": "將影響 {count} 個 API密鑰:{value}",
"affectedKeys": "將影響 {count} 個 API密鑰",
"deleteAccountGroup": "刪除賬號分組",
"deleteWarning": "刪除後該分組將立即失效,此操作不可撤銷",
"unnamedGroup": "該分組",
+18 -18
View File
@@ -9,7 +9,7 @@
"ccSwitchDescription": "管理和切換 AI 客戶端配置",
"viewOnGitHub": "在 GitHub 查看 {name}",
"copiedToClipboard": "已複製到剪貼板",
"fullApiKeyNotAvailable": "完整 API Key 不可用",
"fullApiKeyNotAvailable": "完整 API密鑰不可用",
"title": "使用密鑰"
},
"actions": {
@@ -20,7 +20,7 @@
"disableKey": "停用",
"enableKey": "啓用",
"deleteKey": "刪除",
"deletedApiKeyDeletedApiKeys": "已刪除 {count} 個 API Key",
"deletedApiKeyDeletedApiKeys": "已刪除 {count} 個 API密鑰",
"configure": "配置"
},
"day": "日",
@@ -43,24 +43,24 @@
"confirmReset": "確認重置"
},
"created": {
"apiKeyCreated": "API Key 已創建",
"apiKeyCreated": "API密鑰已創建",
"description": "複製密鑰,或導入到客戶端",
"accessNotice": "該密鑰具有網關訪問權限,請僅發送給可信調用方",
"iHaveSaved": "我已保存"
},
"filters": {
"apiKeyFilteringAndOperations": "API Key 篩選與操作",
"apiKeyFilteringAndOperations": "API密鑰篩選與操作",
"searchPlaceholder": "搜索名稱或標籤",
"searchLabel": "搜索 API Key 名稱或標籤",
"searchLabel": "搜索 API密鑰名稱或標籤",
"deleteSelected": "刪除 ({count})",
"createApiKey": "創建 API Key"
"createApiKey": "創建 API密鑰"
},
"editor": {
"description": "配置密鑰信息、分組與使用限制",
"namePlaceholder": "例如:生產環境",
"labelOptional": "標籤(可選)",
"forExampleBackendService": "例如:後端服務",
"customKeyOptional": "自定義 Key(可選)",
"customKeyOptional": "自定義 API密鑰(可選)",
"keyPlaceholder": "留空自動生成",
"upstreamIdentity": "上游身份",
"dailyLimit": "日限額",
@@ -69,14 +69,14 @@
"weeklyLimitUsd": "周限額(USD)",
"maximumConcurrency": "最大併發",
"requestsPerMinuteRpm": "每分鐘請求數(RPM)",
"updated": "API Key 已更新",
"created": "API Key 已創建",
"updated": "API密鑰已更新",
"created": "API密鑰已創建",
"validation": {
"amount": "{label}必須是非負金額,最多 10 位小數",
"integer": "{label}必須是非負整數"
},
"enterApiKeyName": "請輸入 API Key 名稱",
"invalidKey": "自定義 Key 只能包含 HTTP 可傳輸的可見字符,不能包含空格或換行",
"enterApiKeyName": "請輸入 API密鑰名稱",
"invalidKey": "自定義 API密鑰只能包含 HTTP 可傳輸的可見字符,不能包含空格或換行",
"requestsPerMinute": "每分鐘請求數"
},
"fields": {
@@ -88,16 +88,16 @@
"lastUsed": "最後使用"
},
"page": {
"description": "管理 API 密鑰及賬號訪問範圍",
"noApiKeyYet": "暫無 API Key",
"description": "管理 API密鑰及賬號訪問範圍",
"noApiKeyYet": "暫無 API密鑰",
"selectCurrentPageKey": "選擇當前頁密鑰",
"selectKey": "選擇密鑰",
"concurrency": "併發",
"batchDeleteWarning": "刪除後這些 API Key 將立即失效,此操作不可撤銷",
"batchDeleteDescription": "確定刪除選中的 {count} 個 API Key 嗎?",
"deleteApiKey": "刪除 API Key",
"deleteWarning": "刪除後該 API Key 將立即失效,此操作不可撤銷",
"apiKey": "該 API Key",
"batchDeleteWarning": "刪除後這些 API密鑰將立即失效,此操作不可撤銷",
"batchDeleteDescription": "確定刪除選中的 {count} 個 API密鑰嗎?",
"deleteApiKey": "刪除 API密鑰",
"deleteWarning": "刪除後該 API密鑰將立即失效,此操作不可撤銷",
"apiKey": "該 API密鑰",
"delete": "確定刪除 {name} 嗎?"
},
"scope": {
@@ -4,7 +4,7 @@
"accounts": "賬號管理",
"proxies": "代理管理",
"groups": "分組管理",
"apiKeys": "API 密鑰",
"apiKeys": "API密鑰",
"usage": "分析統計",
"plugins": "插件",
"appearance": "主題設置",
+16 -17
View File
@@ -11,7 +11,7 @@
"applyToAllRequests": "應用於所有請求",
"scopeHelpTitle": "生效請求範圍說明",
"scopeHelp": "關閉後至少選擇一項範圍,不同條件同時滿足才生效,同一條件內任意一項匹配即可",
"clientKey": "客戶端 Key",
"clientKey": "API密鑰",
"accountGrouping": "賬號分組",
"modelScope": "模型範圍",
"oneModelNamePerLineExactMatch": "每行一個模型名稱,精確匹配",
@@ -58,10 +58,9 @@
"checkBasicSettings": "請檢查基本設置",
"noParameters": "無需填寫插件參數",
"sensitiveConfiguration": "敏感配置",
"secretsHelpTitle": "敏感配置說明",
"secretsHelp": "敏感配置單獨保存,不回顯已有值,選擇保留時不會更改,選擇替換時以本次填寫的全部值覆蓋",
"secretsHelp": "用於令牌、密鑰等機密參數,已保存的值不會回顯",
"settingsSaved": "已保存 {count} 項",
"clearSecretsHelp": "保存時清除全部敏感配置",
"emptySecretPlaceholder": "留空將清除此項",
"about": "{name}說明",
"enterNewValue": "輸入新值",
"pluginParameters": "插件參數",
@@ -70,9 +69,9 @@
"applyAndSwitch": "應用並切換",
"saveAndEnable": "保存並啓用",
"saveSettings": "保存設置",
"keepExistingValues": "保留敏感配置",
"replaceAllValues": "替換全部敏感配置",
"clearAllValues": "清除全部敏感配置",
"keepExistingValues": "保留現有值",
"replaceAllValues": "替換全部",
"clearAllValues": "清除全部",
"checkPluginParameters": "請檢查插件參數",
"setRequestScopeOrCheckApplyTo": "請設置請求範圍,或勾選“應用於所有請求”",
"invalidIdentityMappings": "請檢查客戶端認證身份映射",
@@ -87,7 +86,7 @@
"description": "按配置提供擴展功能",
"providesTerminalCommands": "提供終端命令",
"requestHandlingNotConfigured": "未配置請求處理",
"apiKeys": "{count} 個 Key",
"apiKeys": "{count} 個 API密鑰",
"groups": "{count} 個分組",
"providers": "{count} 個 Provider",
"identityMappings": "{count} 組身份映射",
@@ -175,7 +174,7 @@
},
"frontendAuth": {
"exclusiveCertification": "獨佔認證",
"fallbackApiKeyIfNoMatch": "未匹配時回退 API Key",
"fallbackApiKeyIfNoMatch": "未匹配時回退 API密鑰",
"disabled": "(已停用)",
"configuredUnavailable": "已配置但當前不可用",
"invalidBinding": "客戶端認證綁定必須使用認證階段和受支持的未匹配處理",
@@ -184,30 +183,30 @@
"mappingLimit": "身份映射不能超過 256 條",
"invalidIdentity": "身份標識必須是 1–256 個 UTF-8 字節,且不能含首尾空白或控制字符",
"duplicateIdentity": "同一身份標識只能配置一次",
"keyRequired": "請爲每個身份標識選擇客戶端 Key",
"keyRequired": "請爲每個身份標識選擇 API密鑰",
"repeatedCursor": "服務返回了重複的分頁位置",
"failedToLoadClientKey": "加載客戶端 Key 失敗",
"failedToLoadClientKey": "加載 API密鑰失敗",
"enableClientAuthentication": "啓用客戶端認證",
"helpTitle": "客戶端認證說明",
"description": "插件識別請求中的外部身份,宿主再將該身份映射到已有 Client Key,沿用這個 Key 的限額、賬號範圍與併發策略",
"description": "插件識別請求中的外部身份,宿主再將該身份映射到已有 API密鑰,沿用這個 API密鑰的限額、賬號範圍與併發策略",
"loginNotice": "此功能用於客戶端 API 請求,不替代管理端登錄",
"unmatchedProcessing": "未匹配處理",
"unmatchedHandlingInstructions": "未匹配處理說明",
"exclusiveHelp": "獨佔認證:插件未識別到身份時拒絕請求",
"fallbackToApiKeyWhenNoMatch": "未匹配時回退 API Key:插件未識別到身份時,繼續使用宿主原有的 API Key 認證",
"fallbackToApiKeyWhenNoMatch": "未匹配時回退 API密鑰:插件未識別到身份時,繼續使用宿主原有的 API密鑰認證",
"rejectionHelp": "插件明確拒絕或調用失敗時始終拒絕請求,不會回退",
"unmatchedLabel": "客戶端認證未匹配處理",
"identityMapping": "身份映射",
"identityMappingDescription": "身份映射說明",
"identityHelp": "身份標識必須與插件返回的值完全一致,同一身份只能映射一次",
"keyHelp": "選擇該身份使用的 Client Key,已停用或刪除的 Key 不會獲得執行權限",
"reloadClientKey": "重新加載客戶端 Key",
"keyHelp": "選擇該身份使用的 API密鑰,已停用或刪除的 API密鑰不會獲得執行權限",
"reloadClientKey": "重新加載 API密鑰",
"addMapping": "添加映射",
"identityMark": "身份標識",
"identityPlaceholder": "填寫插件返回的身份標識",
"failedToLoadTryAgain": "加載失敗,請重試",
"selectClientApiKey": "選擇客戶端 Key",
"noClientKeyYet": "暫無客戶端 Key",
"selectClientApiKey": "選擇 API密鑰",
"noClientKeyYet": "暫無 API密鑰",
"removeIdentityMapping": "移除第 {value} 條身份映射"
},
"installation": {
+13 -13
View File
@@ -150,7 +150,7 @@
},
"scheduling": {
"roundRobin": "輪詢",
"autoReviewSlotsHelp": "僅對 OpenAI 賬號生效,普通併發爲 10、審批名額爲 3 時,兩類請求分別最多運行 10 個和 3 個,互不佔用名額",
"autoReviewSlotsHelp": "僅對 Codex 賬號生效,普通併發爲 10、審批名額爲 3 時,兩類請求分別最多運行 10 個和 3 個,互不佔用名額",
"autoReviewQueueHelp": "開啓獨立額度後,審批請求單獨排隊,可超過賬號隊列容量,仍受總等待容量、排隊超時與密鑰限額約束",
"weights": {
"load": "負載係數",
@@ -232,17 +232,17 @@
"help": "在設定時間向每個空閒的 Codex OAuth 賬號發送一次請求,不保證額度窗口成功啓動"
},
"adminApiKey": {
"adminApiKey": "管理員 API Key",
"adminApiKey": "管理員 API密鑰",
"description": "用於外部系統集成,具有管理員權限",
"regenerate": "重新生成",
"generate": "生成",
"notGenerated": "未生成",
"enabledDescription": "管理員 API 訪問已啓用,完整 Key 僅在生成時回顯",
"disabledDescription": "外部系統暫時無法通過 API Key 調用管理接口",
"generatedNotice": "完整 Key 僅顯示一次,請立即保存",
"adminApiKeyUpdated": "管理員 API Key 已更新",
"generated": "管理員 API Key 已生成",
"adminApiKeyDeleted": "管理員 API Key 已刪除"
"enabledDescription": "管理員 API 訪問已啓用,完整 API密鑰僅在生成時回顯",
"disabledDescription": "外部系統暫時無法通過 API密鑰調用管理接口",
"generatedNotice": "完整 API密鑰僅顯示一次,請立即保存",
"adminApiKeyUpdated": "管理員 API密鑰已更新",
"generated": "管理員 API密鑰已生成",
"adminApiKeyDeleted": "管理員 API密鑰已刪除"
},
"password": {
"administratorPassword": "管理員密碼",
@@ -282,7 +282,7 @@
"queueTimeoutHelp": "兩個隊列共用,1–120 秒",
"autoReview": {
"label": "自動審批獨立併發",
"description": "每個 OpenAI 賬號的額外名額,0 共用普通併發",
"description": "每個 Codex 賬號的額外名額,0 共用普通併發",
"helpTitle": "自動審批獨立併發說明"
},
"queueTimeoutHelpTitle": "排隊超時說明",
@@ -344,9 +344,9 @@
},
"access": {
"accessRestrictions": "訪問限制",
"deleteAdminApiKey": "刪除管理員 API Key",
"deleteKeyDescription": "刪除後外部系統將無法繼續使用該 Key 調用管理接口",
"areYouSureYouWantToDelete": "確定要刪除當前管理員 API Key 嗎?此操作會立即生效"
"deleteAdminApiKey": "刪除管理員 API密鑰",
"deleteKeyDescription": "刪除後外部系統將無法繼續使用該 API密鑰調用管理接口",
"areYouSureYouWantToDelete": "確定要刪除當前管理員 API密鑰嗎?此操作會立即生效"
},
"tokenRefresh": {
"title": "憑據刷新",
@@ -366,7 +366,7 @@
"exclusions": "不包含請求明細與日誌",
"backupRecords": "備份記錄",
"backupScopeDescription": "備份範圍說明",
"contents": "保留賬號、Key、設置、插件及已用額度",
"contents": "保留賬號、API密鑰、設置、插件及已用額度",
"usageRetentionHourSummary": "用量保留小時彙總",
"creating": "創建中",
"createBackup": "創建備份",
@@ -70,7 +70,8 @@
"plugins": {
"title": "插件兼容性提醒",
"description": "重啓後保留啓用配置,逐個嘗試啓動插件",
"incompatible": "不兼容插件"
"incompatible": "不兼容插件",
"requestErrorHelp": "插件啓動成功後仍可能出現請求錯誤,可嘗試禁用相關插件後重試"
},
"versionAfterRestart": "重啓後版本",
"confirmation": {
+1 -1
View File
@@ -283,7 +283,7 @@
"requestId": "請求 ID",
"upstreamRequestId": "上游請求 ID",
"responseId": "響應 ID",
"clientKeyId": "客戶端 Key ID",
"clientKeyId": "API密鑰 ID",
"errorCode": "錯誤碼",
"failureClassification": "失敗分類",
"duration": "耗時",
@@ -6,6 +6,7 @@ import { ZIconButton, ZMotionIcon, ZPopover, ZScrollbar } from '@codex-proxy/ui'
import {
ArrowUpCircle,
Blocks,
BringToFront,
ChartNoAxesColumn,
ChevronDown,
FolderTree,
@@ -17,7 +18,6 @@ import {
Palette,
PanelLeftClose,
PanelLeftOpen,
PanelsTopLeft,
Puzzle,
Settings,
Sun,
@@ -507,7 +507,7 @@ onBeforeUnmount(() => {
:aria-current="route.name === 'plugins' ? 'page' : undefined"
@click="navigate('/plugins')"
>
<PanelsTopLeft class="block size-4 shrink-0" />
<BringToFront class="block size-4 shrink-0" />
<span class="sidebar-label truncate leading-none">{{ t('navigation.pluginManagement') }}</span>
</button>
<button
@@ -523,6 +523,9 @@ watch(
</div>
</li>
</ul>
<p class="m-0 rounded-cp bg-cp-fill-quaternary px-3 py-2.5 text-cp-sm leading-relaxed font-normal text-cp-text-secondary">
{{ t('systemUpdate.plugins.requestErrorHelp') }}
</p>
</div>
</ZConfirmDialog>
+2 -2
View File
@@ -4,7 +4,7 @@ import { Fingerprint, Plug } from '@lucide/vue'
import { t } from '@/i18n'
const PROVIDER_DISPLAY_NAMES = {
openai: 'OpenAI',
openai: 'Codex',
xai: 'xAI',
} as const
@@ -34,7 +34,7 @@ export function formatAuthenticationLabel(value?: string | null) {
const normalized = value?.trim()
switch (normalized?.toLowerCase()) {
case 'oauth': return 'OAuth'
case 'api_key': return 'API Key'
case 'api_key': return t('common.fields.apiKey')
default: return normalized ? t('common.providers.customAuth', { normalized }) : t('common.providers.unknownAuth')
}
}
@@ -73,7 +73,7 @@ const avatarToneClass = computed(() => {
</span>
<div class="min-w-0 flex-1">
<div class="flex min-w-0 items-center gap-2">
<span class="min-w-0 flex-1 truncate text-cp font-heavy text-cp-text" :title="displayTitle">
<span class="min-w-0 truncate text-cp font-heavy text-cp-text" :title="displayTitle">
{{ displayTitle }}
</span>
<span
@@ -54,7 +54,7 @@ watch(hasPersonalInfo, (available) => {
v-if="account.capabilities.quota || quotaEntries.length > 0"
class="col-span-2 row-start-2 m-0 flex min-w-0 items-center gap-1.5 text-cp-xs font-emphasis text-cp-text-secondary"
>
<span class="shrink-0 whitespace-nowrap">{{ t('accounts.quota.quota', { provider: account.provider === 'openai' ? 'Codex' : formatProviderLabel(account.provider) }) }}</span>
<span class="shrink-0 whitespace-nowrap">{{ t('accounts.quota.quota', { provider: formatProviderLabel(account.provider) }) }}</span>
<template v-if="account.planType">
<span class="shrink-0">·</span>
<AccountPlanBadge :plan-type="account.planType" :plan-type-display="account.planTypeDisplay" size="sm" class="shrink-0" />
@@ -78,7 +78,7 @@ export function accountImportModes(provider: string | undefined) {
if (provider === 'openai') {
return [
{ label: 'OAuth', value: 'oauth' as const },
{ label: 'API Key', value: 'api_key' as const },
{ label: t('common.fields.apiKey'), value: 'api_key' as const },
{ label: 'AT', value: 'access_token' as const },
{ label: 'RT', value: 'refresh_token' as const },
{ label: t('accounts.onboarding.accountFile'), value: 'json' as const },
@@ -2,7 +2,8 @@
import type { PluginArtifactMetadata } from '@/api'
import { ZPopover, ZTag } from '@codex-proxy/ui'
import { computed, shallowRef, useId } from 'vue'
import { useResizeObserver } from '@vueuse/core'
import { computed, shallowRef, useId, useTemplateRef, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { pluginCapabilityLabel } from '../utils/model'
@@ -15,15 +16,65 @@ const { t } = useI18n({ useScope: 'global' })
const open = shallowRef(false)
const contentId = useId()
const entries = computed(() => Object.entries(props.metadata.contributes))
const visible = computed(() => entries.value.slice(0, 2))
const hidden = computed(() => entries.value.slice(2))
const container = useTemplateRef<HTMLDivElement>('container')
const measurement = useTemplateRef<HTMLDivElement>('measurement')
const visibleCount = shallowRef(0)
const entries = computed(() => Object.entries(props.metadata.contributes).map(([capability, contribution]) => ({
capability,
id: contribution.id,
label: pluginCapabilityLabel(capability),
})))
const visible = computed(() => entries.value.slice(0, visibleCount.value))
const hidden = computed(() => entries.value.slice(visibleCount.value))
function updateVisibleCount() {
if (!container.value || !measurement.value)
return
// 用完整标签测量,避免折叠结果反过来改变可用宽度的计算
const widths = Array.from(measurement.value.children, child => child.getBoundingClientRect().width)
const overflowWidth = widths.pop() ?? 0
const gap = Number.parseFloat(getComputedStyle(measurement.value).columnGap) || 0
const available = container.value.getBoundingClientRect().width
const total = widths.reduce((sum, width) => sum + width, 0) + Math.max(0, widths.length - 1) * gap
if (total <= available) {
visibleCount.value = widths.length
return
}
let used = overflowWidth
let count = 0
for (const width of widths) {
if (used + gap + width > available)
break
used += gap + width
count++
}
visibleCount.value = count
}
useResizeObserver([container, measurement], updateVisibleCount)
watch(entries, updateVisibleCount, { flush: 'post' })
watch(() => hidden.value.length, (count) => {
if (!count)
open.value = false
})
</script>
<template>
<div class="flex min-w-0 items-center gap-1 whitespace-nowrap">
<ZTag v-for="[capability, contribution] in visible" :key="capability" size="small" :type="primary ? 'primary' : 'default'" class="min-w-0">
<span class="block truncate" :title="`${pluginCapabilityLabel(capability)} · ${contribution.id}`">{{ pluginCapabilityLabel(capability) }}</span>
<div ref="container" class="relative flex min-w-0 items-center gap-1 whitespace-nowrap">
<div aria-hidden="true" inert class="pointer-events-none invisible absolute inset-0 overflow-hidden">
<div ref="measurement" class="flex w-max items-center gap-1 whitespace-nowrap">
<ZTag v-for="entry in entries" :key="entry.capability" size="small" :type="primary ? 'primary' : 'default'" class="shrink-0">
{{ entry.label }}
</ZTag>
<ZTag size="small" :type="primary ? 'primary' : 'default'" class="shrink-0 tabular-nums">
+{{ entries.length }}
</ZTag>
</div>
</div>
<ZTag v-for="entry in visible" :key="entry.capability" size="small" :type="primary ? 'primary' : 'default'" class="shrink-0">
<span :title="`${entry.label} · ${entry.id}`">{{ entry.label }}</span>
</ZTag>
<ZPopover v-if="hidden.length" v-model="open" trigger="hover-click" placement="top" class="shrink-0">
<template #reference>
@@ -34,14 +85,14 @@ const hidden = computed(() => entries.value.slice(2))
:aria-expanded="open"
:aria-describedby="open ? contentId : undefined"
>
<ZTag size="small" :type="primary ? 'primary' : 'default'">
<ZTag size="small" :type="primary ? 'primary' : 'default'" class="tabular-nums">
+{{ hidden.length }}
</ZTag>
</button>
</template>
<div :id="contentId" role="tooltip" class="flex max-w-72 flex-wrap gap-1.5 p-3">
<ZTag v-for="[capability, contribution] in hidden" :key="capability" size="small" :type="primary ? 'primary' : 'default'">
<span :title="contribution.id">{{ pluginCapabilityLabel(capability) }}</span>
<ZTag v-for="entry in hidden" :key="entry.capability" size="small" :type="primary ? 'primary' : 'default'">
<span :title="entry.id">{{ entry.label }}</span>
</ZTag>
</div>
</ZPopover>
@@ -1,7 +1,7 @@
<script setup lang="ts">
import type { JsonSchema } from '../utils/model'
import { ZEmpty, ZFormItem, ZInput, ZTag } from '@codex-proxy/ui'
import { ZEmpty, ZFormItem, ZInput, ZSegmented, ZTag } from '@codex-proxy/ui'
import { computed, nextTick, shallowRef, useTemplateRef, watch } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -15,11 +15,9 @@ const props = withDefaults(defineProps<{
schema: Record<string, unknown>
secretFields: string[]
existingSecretFields?: string[]
secretMode?: 'preserve' | 'replace' | 'clear'
disabled?: boolean
}>(), {
existingSecretFields: () => [],
secretMode: 'replace',
disabled: false,
})
@@ -31,6 +29,7 @@ const { t } = useI18n({ useScope: 'global' })
const configuration = defineModel<Record<string, unknown>>('configuration', { required: true })
const secrets = defineModel<Record<string, string>>('secrets', { required: true })
const secretMode = defineModel<'preserve' | 'replace' | 'clear'>('secretMode', { required: true })
const configurationText = shallowRef('{}')
const schemaForm = useTemplateRef<{ focusField: (key?: string) => Promise<boolean> }>('schemaForm')
const secretFieldsRoot = useTemplateRef<HTMLElement>('secretFieldsRoot')
@@ -56,6 +55,15 @@ const secretSchemas = computed(() => {
required: required.includes(name),
}))
})
const secretModes = computed(() => ([
{ label: t('plugins.configuration.keepExistingValues'), value: 'preserve' },
{ label: t('plugins.configuration.replaceAllValues'), value: 'replace' },
{ label: t('plugins.configuration.clearAllValues'), value: 'clear' },
]))
const existingSecretLabels = computed(() => props.existingSecretFields.map(name => ({
name,
label: (props.schema as JsonSchema).properties?.[name]?.title || name,
})))
const requiredConfigurationFields = computed(() => (configurationSchema.value as JsonSchema).required ?? [])
const missingConfigurationFields = computed(() => requiredConfigurationFields.value.filter(name =>
!Object.hasOwn(configuration.value, name),
@@ -65,19 +73,19 @@ const configurationFieldErrors = computed(() => Object.fromEntries(
))
const requiredSecretFields = computed(() => secretSchemas.value.filter(field => field.required))
const missingSecretFields = computed(() => requiredSecretFields.value.filter((field) => {
if (props.secretMode === 'clear')
if (secretMode.value === 'clear')
return true
if (props.secretMode === 'preserve')
if (secretMode.value === 'preserve')
return !props.existingSecretFields.includes(field.name)
return !valuePresent(secrets.value[field.name])
}))
const secretModeError = computed(() => {
if (props.secretMode === 'preserve' && props.existingSecretFields.some(name => !props.secretFields.includes(name)))
if (secretMode.value === 'preserve' && props.existingSecretFields.some(name => !props.secretFields.includes(name)))
return t('plugins.configuration.incompatibleSecrets')
if (!missingSecretFields.value.length || props.secretMode === 'replace')
if (!missingSecretFields.value.length || secretMode.value === 'replace')
return ''
const labels = missingSecretFields.value.map(field => field.schema.title || field.name).join('、')
return props.secretMode === 'clear'
return secretMode.value === 'clear'
? t('plugins.configuration.requiredSecretsCannotClear', { labels })
: t('plugins.configuration.requiredSecretsMissing', { labels })
})
@@ -87,7 +95,8 @@ const hasConfiguration = computed(() => {
|| (configurationSchema.value as JsonSchema).additionalProperties !== false
|| Object.keys(configuration.value).length > 0
})
const hasFields = computed(() => hasConfiguration.value || props.secretFields.length > 0)
const hasSecrets = computed(() => props.secretFields.length > 0 || props.existingSecretFields.length > 0)
const hasFields = computed(() => hasConfiguration.value || hasSecrets.value)
function setSecret(name: string, value: unknown) {
const next = { ...secrets.value }
@@ -142,7 +151,7 @@ async function focusInvalid() {
}
if (!missingSecretFields.value.length)
return null
if (props.secretMode !== 'replace')
if (secretMode.value !== 'replace')
return 'secretMode' as const
await nextTick()
@@ -175,22 +184,37 @@ defineExpose({ focusInvalid, validationMessage })
:field-errors="configurationFieldErrors"
/>
<template v-if="secretFields.length > 0">
<section v-if="hasSecrets" class="grid gap-3 rounded-cp bg-cp-fill-alter p-4" :aria-label="t('plugins.configuration.sensitiveConfiguration')">
<div class="flex min-w-0 flex-wrap items-center gap-2">
<strong class="text-cp leading-none text-cp-text">{{ t('plugins.configuration.sensitiveConfiguration') }}</strong>
<PluginHelpPopover :label="t('plugins.configuration.secretsHelpTitle')">
{{ t('plugins.configuration.secretsHelp') }}
</PluginHelpPopover>
<ZTag v-if="existingSecretFields.length > 0" type="success" size="small">
{{ t('plugins.configuration.settingsSaved', { count: existingSecretFields.length }) }}
</ZTag>
</div>
<p v-if="secretMode === 'clear'" class="m-0 text-cp-xs text-cp-warning-text">
{{ t('plugins.configuration.clearSecretsHelp') }}
<p class="m-0 text-cp-xs leading-relaxed text-cp-text-secondary">
{{ t('plugins.configuration.secretsHelp') }}
</p>
<div ref="secretFieldsRoot" class="contents">
<ZSegmented
v-if="existingSecretFields.length"
v-model="secretMode"
:options="secretModes"
:aria-label="t('plugins.configuration.secretsModeLabel')"
:disabled="disabled"
class="w-fit max-w-full"
/>
<p v-if="secretModeError" role="alert" class="m-0 text-cp-xs leading-relaxed text-cp-warning-text">
{{ secretModeError }}
</p>
<ul v-if="secretMode === 'preserve'" class="m-0 flex list-none flex-wrap gap-2 p-0">
<li v-for="field in existingSecretLabels" :key="field.name" class="min-w-0 wrap-anywhere">
<ZTag type="primary">
{{ field.label }}
</ZTag>
</li>
</ul>
<div v-if="secretMode === 'replace'" ref="secretFieldsRoot" class="grid items-end gap-4 sm:grid-cols-2">
<ZFormItem
v-for="field in secretMode === 'replace' ? secretSchemas : []"
v-for="field in secretSchemas"
:key="field.name"
:label="field.schema.title || field.name"
:required="field.required"
@@ -208,11 +232,11 @@ defineExpose({ focusInvalid, validationMessage })
:disabled="disabled"
:aria-label="field.schema.title || field.name"
:aria-invalid="field.required && !valuePresent(secrets[field.name])"
:placeholder="t('plugins.configuration.enterNewValue')"
:placeholder="existingSecretFields.includes(field.name) ? t('plugins.configuration.emptySecretPlaceholder') : t('plugins.configuration.enterNewValue')"
@update:model-value="setSecret(field.name, $event)"
/>
</ZFormItem>
</div>
</template>
</section>
</div>
</template>
@@ -52,11 +52,6 @@ const sections = computed(() => [
...(authenticationContribution.value ? [{ label: t('plugins.configuration.clientAuthentication'), value: 'authentication', icon: ShieldCheck }] : []),
])
const primaryLabel = computed(() => versionChanged.value ? t('plugins.configuration.applyAndSwitch') : props.instance?.configurationRequired ? t('plugins.configuration.saveAndEnable') : t('plugins.configuration.saveSettings'))
const secretModes = computed(() => ([
{ label: t('plugins.configuration.keepExistingValues'), value: 'preserve' },
{ label: t('plugins.configuration.replaceAllValues'), value: 'replace' },
{ label: t('plugins.configuration.clearAllValues'), value: 'clear' },
]))
async function focusConfigurationInvalid() {
section.value = 'general'
@@ -125,22 +120,14 @@ function clearSecrets() {
</div>
<ZSegmented v-if="sections.length > 1" v-model="section" :options="sections" :aria-label="t('plugins.configuration.pluginSettingsPartition')" :disabled="saving" class="w-fit max-w-full" />
<div v-show="section === 'general'" class="grid gap-5">
<ZSegmented
v-if="existingSecretFields.length"
v-model="secretMode"
:options="secretModes"
:aria-label="t('plugins.configuration.secretsModeLabel')"
:disabled="saving"
class="w-full sm:w-auto"
/>
<PluginConfigurationFields
ref="configurationFields"
v-model:configuration="configuration"
v-model:secrets="secretValues"
v-model:secret-mode="secretMode"
:schema="artifact.metadata.configurationSchema"
:secret-fields="artifact.metadata.secretFields"
:existing-secret-fields="existingSecretFields"
:secret-mode="secretMode"
:disabled="saving"
@validity-change="configurationValid = $event"
/>
@@ -137,7 +137,7 @@ function saveTheme(event: MouseEvent) {
>
<ZInput
v-model="query"
class="mr-3"
class="pr-3"
:aria-label="t('appearance.settings.searchToken')"
:placeholder="t('appearance.settings.searchPlaceholder')"
>
@@ -20,6 +20,7 @@ import {
import { computed, shallowRef } from 'vue'
import { useI18n } from 'vue-i18n'
import { formatProviderLabel } from '@/utils/providers'
const { t } = useI18n({ useScope: 'global' })
@@ -49,8 +50,8 @@ const controlHeight = shallowRef(38)
const syncTagVisible = shallowRef(true)
const providerOptions = computed(() => ([
{ label: 'OpenAI', value: 'openai' },
{ label: 'xAI', value: 'xai' },
{ label: formatProviderLabel('openai'), value: 'openai' },
{ label: formatProviderLabel('xai'), value: 'xai' },
{ label: t('appearance.preview.components.allPlatforms'), value: 'all' },
]))
const viewOptions = computed(() => ([
@@ -81,12 +82,12 @@ const tableColumns = computed(() => (defineTableColumns<PreviewTableRow>([
{ key: 'updatedAt', label: t('appearance.preview.components.recentlyUsed'), kind: 'datetime', size: 'lg' },
])))
const tableRows = computed<PreviewTableRow[]>(() => ([
{ id: 'relay', account: 'relay@example.com', provider: 'OpenAI', status: 'active', requests: '12,840', latency: '228 ms', updatedAt: t('appearance.preview.components.justNow') },
{ id: 'gateway', account: 'gateway@example.com', provider: 'xAI', status: 'active', requests: '9,462', latency: '316 ms', updatedAt: t('common.time.minutesAgo', { count: 2 }) },
{ id: 'quota', account: 'quota@example.com', provider: 'OpenAI', status: 'rate_limited', requests: '7,318', latency: '486 ms', updatedAt: t('common.time.minutesAgo', { count: 5 }) },
{ id: 'batch', account: 'batch@example.com', provider: 'OpenAI', status: 'active', requests: '5,904', latency: '264 ms', updatedAt: t('common.time.minutesAgo', { count: 8 }) },
{ id: 'fallback', account: 'fallback@example.com', provider: 'xAI', status: 'error', requests: '3,172', latency: '—', updatedAt: t('common.time.minutesAgo', { count: 12 }) },
{ id: 'disabled', account: 'disabled@example.com', provider: 'OpenAI', status: 'disabled', requests: '0', latency: '—', updatedAt: t('appearance.preview.components.hoursAgo') },
{ id: 'relay', account: 'relay@example.com', provider: formatProviderLabel('openai'), status: 'active', requests: '12,840', latency: '228 ms', updatedAt: t('appearance.preview.components.justNow') },
{ id: 'gateway', account: 'gateway@example.com', provider: formatProviderLabel('xai'), status: 'active', requests: '9,462', latency: '316 ms', updatedAt: t('common.time.minutesAgo', { count: 2 }) },
{ id: 'quota', account: 'quota@example.com', provider: formatProviderLabel('openai'), status: 'rate_limited', requests: '7,318', latency: '486 ms', updatedAt: t('common.time.minutesAgo', { count: 5 }) },
{ id: 'batch', account: 'batch@example.com', provider: formatProviderLabel('openai'), status: 'active', requests: '5,904', latency: '264 ms', updatedAt: t('common.time.minutesAgo', { count: 8 }) },
{ id: 'fallback', account: 'fallback@example.com', provider: formatProviderLabel('xai'), status: 'error', requests: '3,172', latency: '—', updatedAt: t('common.time.minutesAgo', { count: 12 }) },
{ id: 'disabled', account: 'disabled@example.com', provider: formatProviderLabel('openai'), status: 'disabled', requests: '0', latency: '—', updatedAt: t('appearance.preview.components.hoursAgo') },
]))
const pagination: PaginationState = {
currentPage: 2,
@@ -414,7 +415,7 @@ function statusClass(status: PreviewTableRow['status']) {
<div class="flex items-center justify-between rounded-cp bg-cp-bg-container px-4 py-3 shadow-cp-tertiary">
<div>
<strong class="block text-cp-sm text-cp-text">relay@example.com</strong>
<span class="mt-1 block text-[10px] text-cp-text-quaternary">OpenAI · OAuth</span>
<span class="mt-1 block text-[10px] text-cp-text-quaternary">{{ formatProviderLabel('openai') }} · OAuth</span>
</div>
<ZPopover>
<template #reference>
+10 -16
View File
@@ -1,20 +1,14 @@
# v3.31.2
# v3.31.3
## 表单与设置
## 账号与界面
- API Key 名称、额度和并发等字段接入表单校验,错误持续显示在对应字段下方,提交失败时自动滚动并聚焦首个错误字段
- 错误提示显示时保持表单高度稳定,修复恢复探测模型标签误触发旁边复选框的问题
- 设置及备份配置存在未保存修改时,离开页面前提供确认,减少误操作造成的草稿丢失
- 将账号平台名称统一为 Codex,同步设置帮助、账号错误提示与相关文档
- 修复测试连接弹窗中订阅标签与账号名称间距过大的问题
- 统一简体与繁体中文界面的 API 密钥称呼,账号认证类型及导入方式随界面语言显示
- 调整主题变量搜索框的右侧留白
## 交互与显示
## 插件管理
- 补齐移动侧栏的初始焦点、Tab 循环、Esc 关闭及焦点恢复,账号搜索补充可访问名称
- 收紧没有正文的确认弹窗留白,未保存设置的离开按钮简化为“放弃并离开”
- 账号状态与请求记录推理强度居中显示,收窄表格默认单元格左右内边距,精简活跃账号用量文案
- 统一首页与用量图表提示中的时间字体和下方间距,日期选择浮层与输入框保留更清楚的间隔
- 修复窗口重新激活时意外打开悬停浮层,以及紧凑行高下单行省略文本的纵向裁切
## 主题配置
- 减少进入主题配置时的同步计算,折叠的变量列表在展开后渲染,完整预览异步加载
- 管理端升级至 UI 组件库 v0.7.0
- 插件能力标签根据可用宽度展示,超出部分收进数量浮层,避免标签被截断
- 将敏感配置的保留、替换和清除操作集中到对应区域,展示已保存字段名称,并明确替换时留空将清除该项
- 系统更新的插件兼容性提醒补充请求失败时的排查提示
+1 -1
View File
@@ -1 +1 @@
version: "3.31.2"
version: "3.31.3"