Compare commits

..
4 Commits
Author SHA1 Message Date
ZyyandCodex 776637fccb chore: bump version to 3.2.5
Co-authored-by: Codex <noreply@openai.com>
2026-08-15 01:52:54 +08:00
ZyyandCodex 82acb08d26 fix(frontend): restore diagnostic table scrolling
Co-authored-by: Codex <noreply@openai.com>
2026-08-15 01:51:56 +08:00
ZyyandCodex 7744e1b0b4 fix(accounts): preserve rolling window usage
Co-authored-by: Codex <noreply@openai.com>
2026-08-15 01:28:44 +08:00
ZyyandCodex 0b8a1add18 feat(frontend): refresh account setup defaults and copy
Co-authored-by: Codex <noreply@openai.com>
2026-08-15 01:09:31 +08:00
10 changed files with 76 additions and 39 deletions
@@ -608,8 +608,9 @@ pub struct ProviderQuota {
impl ProviderQuota {
/// 返回账号展开面板使用的当前额度窗口用量。
///
/// 只选择具备完整时间边界且可按账号归属的窗口;优先级与 Dashboard
/// 代表性额度窗口一致,同一优先级保持 Provider 投影顺序。
/// Provider 已提供账号级本地用量时,固定窗口和无固定重置点的滚动窗口
/// 都可作为代表用量;优先级与 Dashboard 代表性额度窗口一致,
/// 同一优先级保持 Provider 投影顺序。
#[must_use]
pub fn representative_window_usage(&self) -> Option<&AccountUsage> {
self.windows
@@ -618,7 +619,6 @@ impl ProviderQuota {
.filter(|(_, window)| {
window.local_usage_attribution == QuotaLocalUsageAttribution::AccountWide
&& window.window_seconds.is_some()
&& window.reset_at.is_some()
&& window.local_usage.is_some()
})
.min_by_key(|(index, window)| (quota_usage_priority(window), *index))
@@ -6,6 +6,7 @@ use gateway_core::engine::credential::OpaqueProviderData;
use gateway_admin::{
model::{
PageSize, Revision,
accounts::AccountUsage,
auth::LoginCommand,
client_keys::ClientKeyPageSize,
observability::{DecimalAmount, RequestOutcome, TimeRange},
@@ -135,6 +136,51 @@ fn representative_quota_should_prefer_short_window_and_highest_usage() {
assert_eq!(quota.representative_used_percent(), Some(45.0));
}
#[test]
fn representative_window_usage_should_accept_account_wide_rolling_window_without_reset() {
let usage = AccountUsage {
account_id: "acct_xai".to_owned(),
request_count: 5,
success_count: 5,
input_tokens: Some(100),
output_tokens: Some(20),
cached_tokens: Some(80),
cache_write_tokens: Some(0),
reasoning_tokens: Some(0),
image_input_tokens: Some(0),
image_output_tokens: Some(0),
image_request_count: 0,
image_request_failed_count: 0,
total_tokens: Some(120),
cost_coverage: Default::default(),
costs: Vec::new(),
last_used_at: Some(Utc::now()),
request_buckets: Vec::new(),
models: Vec::new(),
};
let quota = ProviderQuota {
observed_at: None,
refresh_token_expires_at: None,
windows: vec![ProviderQuotaWindow {
key: "free-rolling-24h".to_owned(),
group: "shortTerm".to_owned(),
label: "日限额".to_owned(),
source: None,
local_usage_attribution: QuotaLocalUsageAttribution::AccountWide,
window_seconds: Some(86_400),
used_percent: None,
reset_at: None,
limit_reached: false,
local_usage: Some(usage.clone()),
provider_data: None,
}],
limit_reached: false,
provider_data: None,
};
assert_eq!(quota.representative_window_usage(), Some(&usage));
}
#[test]
fn exhausted_quota_should_project_full_usage_to_only_the_representative_window() {
let mut quota = ProviderQuota {
@@ -116,18 +116,13 @@ const modalTitle = computed(() => {
const oauthPanelTitle = computed(() => {
if (props.reauthorizing)
return accountName.value
return isXai.value ? '使用 xAI OAuth 完成账号接入' : '使用 OpenAI OAuth 完成账号接入'
return isXai.value ? 'xAI OAuth 授权' : 'OpenAI OAuth 授权'
})
const oauthPanelDescription = computed(() => {
if (props.reauthorizing) {
return isXai.value
? '生成新的授权链接,完成后粘贴回调地址、含 code 和 state 的查询字符串或授权码更新账号凭据'
: '生成新的授权链接,完成后粘贴回调地址更新账号凭据'
}
return isXai.value
? '复制授权链接到浏览器打开,完成后粘贴回调地址、含 code 和 state 的查询字符串或授权码即可导入'
: '复制授权链接到浏览器打开,完成后把回调地址粘贴到下方即可导入'
? '生成并打开授权链接 → 完成浏览器授权 → 粘贴回调地址、查询字符串或授权码'
: '生成并打开授权链接 → 完成浏览器授权 → 粘贴回调地址'
})
const canGenerateOauth = computed(() =>
@@ -152,21 +147,21 @@ const description = computed<string | undefined>(() => {
return undefined
if (props.reauthorizing) {
return isXai.value
? '完成授权后粘贴回调地址、含 code 和 state 的查询字符串或授权码,系统会更新账号凭据'
: '完成授权后粘贴回调地址,系统会更新账号凭据'
? '完成新的 xAI 授权并替换此账号凭据'
: '完成新的 OpenAI 授权并替换此账号凭据'
}
if (isBatch.value)
return '导入 CPR 多平台账号包,系统会按文档中的平台自动分流'
return '粘贴或上传 CPR 账号包,一次导入多个平台账号'
if (isXai.value) {
return mode.value === 'oauth'
? '复制 xAI 授权链接,完成后粘贴回调地址、含 code 和 state 的查询字符串或授权码,不使用 xAI API Key'
: '导入 xAI 账号文件,已存在账号会更新'
? '通过浏览器授权导入 xAI 账号'
: '粘贴或上传 xAI 账号文件,匹配已有账号时更新凭据'
}
if (mode.value === 'oauth')
return '复制 OpenAI 授权链接,完成后粘贴回调地址,系统会自动写入或更新账号'
return '通过浏览器授权导入 OpenAI 账号'
if (mode.value === 'agent_identity')
return '导入 Agent 身份文件,系统会按身份信息写入或更新账号'
return '导入 CPR、Sub2API 或 CPA 账号文件,已存在账号会更新'
return '粘贴或上传 Agent 身份文件,匹配已有账号时更新凭据'
return '粘贴或上传 CPR、Sub2API 或 CPA 账号文件,匹配已有账号时更新凭据'
})
function selectProvider(value: 'openai' | 'xai' | 'batch') {
@@ -288,7 +283,7 @@ async function copyText(value: string, successText: string) {
v-model="oauthCallback"
:aria-label="isXai ? '回调地址或授权码' : '回调地址'"
size="sm"
:placeholder="isXai ? '粘贴完整回调地址、?code=...&state=... 或裸授权码' : 'http://localhost:1455/auth/callback?code=...&state=...'"
:placeholder="isXai ? '回调地址、?code=...&state=... 或授权码' : 'http://localhost:1455/auth/callback?code=...&state=...'"
:disabled="saving"
/>
</BaseFormItem>
@@ -103,7 +103,7 @@ export function useAccountConnectionTest(options: { reload: () => Promise<unknow
if (connectionTestStatus.value === 'running') {
return {
label: '正在测试',
description: '正在发送一条真实 Responses 流式请求',
description: '正在向所选模型发送请求并接收流式响应',
icon: Clock3,
badge: 'bg-(--cp-info-bg) text-(--cp-info-text)',
iconClass: 'text-(--cp-info)',
@@ -112,7 +112,7 @@ export function useAccountConnectionTest(options: { reload: () => Promise<unknow
if (connectionTestStatus.value === 'success') {
return {
label: '连接正常',
description: '账号凭据可用,已完成 Responses 流式验证',
description: '请求已完成,可在下方查看模型、耗时和事件轨迹',
icon: CheckCircle2,
badge: 'bg-(--cp-success-bg) text-(--cp-success-text)',
iconClass: 'text-(--cp-success)',
@@ -121,7 +121,7 @@ export function useAccountConnectionTest(options: { reload: () => Promise<unknow
if (connectionTestStatus.value === 'error') {
return {
label: '测试失败',
description: '真实请求未完成,优先检查令牌状态、账号权限或上游网络',
description: '请求未完成,请在下方查看上游错误与事件轨迹',
icon: XCircle,
badge: 'bg-(--cp-danger-bg) text-(--cp-danger-text)',
iconClass: 'text-(--cp-danger)',
@@ -129,7 +129,7 @@ export function useAccountConnectionTest(options: { reload: () => Promise<unknow
}
return {
label: '准备测试',
description: '点击开始后发送一条轻量 Responses 流式请求',
description: '选择模型后,点击“开始测试”发送真实请求',
icon: Wifi,
badge: 'bg-(--cp-bg-subtle) text-(--cp-text-secondary)',
iconClass: 'text-(--cp-text-muted)',
@@ -189,11 +189,11 @@ export function useAccountOnboarding(options: {
}
if (importedCount === 0) {
throw new Error(failures.length > 0 ? `批量导入失败:${failures.join(';')}` : '批量文件没有可导入的账号')
throw new Error(failures.length > 0 ? `批量导入失败:${failures.join('、')}` : '批量文件没有可导入的账号')
}
if (failures.length > 0) {
return `已导入 ${importedCount} 个账号,${failures.length} 个文档失败:${failures.join(';')}`
return `已导入 ${importedCount} 个账号,${failures.length} 个文档失败:${failures.join('、')}`
}
return `已导入 ${importedCount} 个账号`
}
@@ -21,7 +21,7 @@ export function parseAccountSchedulingForm(
|| parsedLimit < 1
|| parsedLimit > MAX_ACCOUNT_CONCURRENCY)
) {
return { valid: false, message: `并发限制必须留空或填写 1 到 ${MAX_ACCOUNT_CONCURRENCY} 的整数` }
return { valid: false, message: `并发限制必须留空,或为 1 到 ${MAX_ACCOUNT_CONCURRENCY} 的整数` }
}
const parsedWeight = Number(weight.trim())
@@ -27,16 +27,13 @@ const open = defineModel<boolean>({ default: false })
const createdOpen = defineModel<boolean>('createdOpen', { default: false })
const form = defineModel<ApiKeyFormValue>('form', { required: true })
const title = computed(() => props.editing ? '编辑 API Key' : '创建 API Key')
const description = computed(() => props.editing
? '调整访问范围与调度限制,分组替换原子生效。'
: '为调用方选择一个或多个账号分组。')
</script>
<template>
<BaseModal
v-model="open"
:title="title"
:description="description"
description="设置调用方可用的账号分组、并发和每分钟请求上限"
variant="info"
width="680px"
body-max-height="min(680px, calc(100dvh - 190px))"
@@ -47,7 +44,7 @@ const description = computed(() => props.editing
<BaseInput
v-model="form.name"
aria-label="名称"
placeholder="例如:生产环境、测试调用方..."
placeholder="例如:生产环境"
:disabled="saving"
/>
</BaseFormItem>
@@ -56,7 +53,7 @@ const description = computed(() => props.editing
<BaseInput
v-model="form.label"
aria-label="标签(可选)"
placeholder="备注信息..."
placeholder="例如:后端服务"
:disabled="saving"
/>
</BaseFormItem>
@@ -108,7 +105,7 @@ const description = computed(() => props.editing
<BaseModal
v-model="createdOpen"
title="API Key 已创建"
description="可以立即复制或导入 CCSwitch,之后也可从密钥列表再次操作"
description="复制密钥,或直接导入 CCSwitch"
variant="success"
width="540px"
>
@@ -38,13 +38,13 @@ const authPath = computed(() =>
activePlatform.value === 'windows' ? '%userprofile%\\.codex\\auth.json' : '~/.codex/auth.json',
)
const codexAuthJson = computed(() => JSON.stringify({ OPENAI_API_KEY: keyValue.value }, null, 2))
const defaultModel = 'gpt-5.5'
const defaultModel = 'gpt-5.6-terra'
const codexConfigToml = computed(
() => `model_provider = "OpenAI"
model = "${defaultModel}"
review_model = "${defaultModel}"
model_reasoning_effort = "xhigh"
model_reasoning_effort = "max"
service_tier = "default"
disable_response_storage = true
network_access = "enabled"
@@ -70,7 +70,7 @@ const visibleFiles = computed(() => [
<BaseModal
v-model="open"
title="使用密钥"
description="将以下配置文件添加到 Codex CLI 配置目录中"
description="将下方内容分别保存到显示的 Codex CLI 配置文件"
width="760px"
>
<div class="flex flex-col gap-5">
@@ -139,7 +139,6 @@ function diagnosticNameDisplay(name: string) {
compact
row-key="key"
empty-text="暂无诊断数据"
max-height="230px"
min-width="640px"
>
<template #header-nameDisplay>
+1 -1
View File
@@ -1 +1 @@
version: "3.2.4"
version: "3.2.5"