diff --git a/src-tauri/src/management_api.rs b/src-tauri/src/management_api.rs index 94a4833..6e48ebf 100644 --- a/src-tauri/src/management_api.rs +++ b/src-tauri/src/management_api.rs @@ -108,7 +108,7 @@ pub(crate) async fn upload_auth_file( ) -> Result { let name = name.trim().to_string(); if name.is_empty() || !name.to_ascii_lowercase().ends_with(".json") { - return Err("认证文件名必须以 .json 结尾".to_string()); + return Err("凭证文件名必须以 .json 结尾".to_string()); } let config = gui_config_state.snapshot()?; @@ -123,7 +123,7 @@ pub(crate) async fn upload_auth_file( .body(data) .send() .await - .map_err(|err| format_management_request_error("上传认证文件失败", &err))?; + .map_err(|err| format_management_request_error("上传凭证文件失败", &err))?; read_management_value(response).await } diff --git a/src-tauri/src/usage.rs b/src-tauri/src/usage.rs index f1421e2..05cd334 100644 --- a/src-tauri/src/usage.rs +++ b/src-tauri/src/usage.rs @@ -5029,7 +5029,7 @@ mod tests { assert_eq!(overview.tps, 0.0); assert_eq!(overview.tps_sample_count, 0); assert_eq!(overview.cache_hit_rate, 0.2); - assert!((overview.estimated_cost - 0.0003205).abs() < 0.0000001); + assert!((overview.estimated_cost - 0.0002564).abs() < 0.0000001); assert_eq!(overview.priced_requests, 1); assert_eq!(analysis.models[0].key, "gpt-5.6-terra"); assert_eq!(events.total, 1); @@ -5108,7 +5108,7 @@ mod tests { ..CostTokens::default() }; let standard = cost_for_price("openai/gpt-5.6-terra", "default", &standard_tokens, &terra); - assert!((standard - 17.1125).abs() < 0.000001); + assert!((standard - 13.69).abs() < 0.000001); let long_tokens = CostTokens { input: 300_000, @@ -5120,7 +5120,7 @@ mod tests { ..CostTokens::default() }; let long_priority = cost_for_price("gpt-5.6-terra", "priority", &long_tokens, &terra); - assert!((long_priority - 5.55).abs() < 0.000001); + assert!((long_priority - 4.44).abs() < 0.000001); assert!(official_model_price("unpriced-model").is_none()); } @@ -5135,7 +5135,7 @@ mod tests { find_model_price(&bundled, "openai/gpt-5.6-terra-high") .unwrap() .prompt, - 2.5 + 2.0 ); upsert_model_price( @@ -5152,7 +5152,7 @@ mod tests { ) .unwrap(); let prices = load_model_prices(&connection).unwrap(); - assert_eq!(prices["gpt-5.6-terra"].prompt, 2.5); + assert_eq!(prices["gpt-5.6-terra"].prompt, 2.0); assert_eq!(prices["gpt-5.6-terra"].source, "builtin"); drop(connection); fs::remove_dir_all(root).unwrap(); diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 9836e3a..bf0ada2 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -76,7 +76,7 @@ export const zhCN = { 'app.nav.thinkingAliases': '模型别名', 'app.nav.oauth': 'OAuth', 'app.nav.api': 'API 接入', - 'app.nav.authFiles': '认证文件', + 'app.nav.authFiles': '凭证文件', 'app.nav.quota': '额度查询', 'app.nav.usageRecords': '使用记录', 'app.nav.agents': '智能体配置', @@ -167,12 +167,12 @@ export const zhCN = { 'quota.credentials.other': '{count} 个凭据', 'quota.readList': '读取列表', 'quota.refreshAll': '刷新全部', - 'quota.loadingFiles': '读取认证文件中', + 'quota.loadingFiles': '读取凭证文件中', 'quota.empty.title': '暂无可查询配额', - 'quota.empty.description': '先在认证文件中添加支持配额查询的凭据。', + 'quota.empty.description': '先在凭证文件中添加支持配额查询的凭据。', 'quota.unknownProvider': '未知', 'quota.reset': '重置额度', - 'quota.fileDisabled': '认证文件已停用', + 'quota.fileDisabled': '凭证文件已停用', 'quota.refresh': '获取/刷新额度', 'quota.notFetched': '尚未获取额度', 'quota.disabled': '已停用', @@ -621,15 +621,15 @@ export const zhCN = { 'authFiles.priority.hint': '仅支持整数;数值越大优先级越高。留空或填写 0 恢复默认。', 'authFiles.priority.invalid': '请输入有效的整数优先级', 'authFiles.priority.updated': '{name} 的优先级已更新', - 'authFiles.uploaded': '已上传 {count} 个认证文件', + 'authFiles.uploaded': '已上传 {count} 个凭证文件', 'authFiles.uploadFailed': '{count} 个文件上传失败:{errors}', - 'authFiles.notice.enabled': '认证文件已启用', - 'authFiles.notice.disabled': '认证文件已停用', - 'authFiles.runtimeDeleteError': '运行时认证文件不能从磁盘删除', + 'authFiles.notice.enabled': '凭证文件已启用', + 'authFiles.notice.disabled': '凭证文件已停用', + 'authFiles.runtimeDeleteError': '运行时凭证文件不能从磁盘删除', 'authFiles.deleteConfirm': '确定删除“{name}”吗?', - 'authFiles.deleted': '认证文件已删除', + 'authFiles.deleted': '凭证文件已删除', 'common.copyFailed': '复制失败', - 'authFiles.title': '认证文件', + 'authFiles.title': '凭证文件', 'authFiles.summary': '{files} 个文件 · {disabled} 个停用', 'common.refresh': '刷新', 'common.upload': '上传', @@ -640,11 +640,11 @@ export const zhCN = { 'authFiles.filter.enabled': '已启用', 'authFiles.filter.disabled': '已停用', 'authFiles.filter.runtime': '仅运行时', - 'authFiles.loading': '读取认证文件中', - 'authFiles.empty.filtered': '没有匹配的认证文件', - 'authFiles.empty.none': '暂无认证文件', + 'authFiles.loading': '读取凭证文件中', + 'authFiles.empty.filtered': '没有匹配的凭证文件', + 'authFiles.empty.none': '暂无凭证文件', 'authFiles.empty.tryFilter': '换个筛选条件试试', - 'authFiles.empty.upload': '上传 JSON 认证文件后会显示在这里', + 'authFiles.empty.upload': '上传 JSON 凭证文件后会显示在这里', 'authFiles.unknownSize': '大小未知', 'authFiles.runtime': '运行时', 'authFiles.quota.querying': '查询中', @@ -1197,7 +1197,7 @@ export const zhCN = { 'kernel.access.openaiDescription': 'OpenAI兼容格式', 'kernel.access.claudeDescription': 'Anthropic兼容格式', 'kernel.access.geminiDescription': 'Gemini兼容格式', - 'authFiles.unnamed': '未命名认证文件', + 'authFiles.unnamed': '未命名凭证文件', 'management.notConfigured': '未配置', 'management.error.upstream': '上游请求失败', 'management.error.upstreamHttp': '上游返回 HTTP {status}', diff --git a/src/i18n/traditional.ts b/src/i18n/traditional.ts index 0af2fac..72f8666 100644 --- a/src/i18n/traditional.ts +++ b/src/i18n/traditional.ts @@ -11,7 +11,7 @@ const phraseReplacements: ReadonlyArray = [ ['智能体', '智慧代理'], ['会话', '工作階段'], ['使用记录', '使用記錄'], - ['认证文件', '認證檔案'], + ['凭证文件', '憑證檔案'], ['配置文件', '設定檔'], ['配置', '設定'], ['文件夹', '資料夾'], diff --git a/src/pages/AuthFileManagementPage.tsx b/src/pages/AuthFileManagementPage.tsx index b230c02..5d00e3a 100644 --- a/src/pages/AuthFileManagementPage.tsx +++ b/src/pages/AuthFileManagementPage.tsx @@ -544,7 +544,7 @@ export function AuthFileManagementPage() { {providerKey(file) ? : null} - + {quotaProviderForFile(file) && quotas[quotaKey(file)]?.status !== 'idle' ? : null} diff --git a/src/styles.css b/src/styles.css index 854e637..35e775e 100644 --- a/src/styles.css +++ b/src/styles.css @@ -8768,7 +8768,7 @@ body.table-col-resizing * { text-overflow: ellipsis; } -.real-auth-file-row.disabled { +.real-auth-file-row.disabled > :not(.auth-file-actions) { opacity: 0.68; } @@ -8801,6 +8801,7 @@ body.table-col-resizing * { gap: 10px; } +.real-auth-file-row .primary-button, .real-auth-file-row .secondary-button { min-width: 52px; } diff --git a/tests/i18n.test.ts b/tests/i18n.test.ts index 289e0ea..29c0f2f 100644 --- a/tests/i18n.test.ts +++ b/tests/i18n.test.ts @@ -4,6 +4,18 @@ import { en, ja, zhCN, zhTW, type MessageKey } from '../src/i18n/resources'; import { jaOverrides } from '../src/i18n/ja'; describe('i18n', () => { + it('uses credential-file terminology throughout both Chinese locales', () => { + for (const key of ['app.nav.authFiles', 'authFiles.title'] as const) { + expect(translate('zh-CN', key)).toBe('凭证文件'); + expect(translate('zh-TW', key)).toBe('憑證檔案'); + } + expect(translate('zh-CN', 'authFiles.uploaded', { count: 2 })).toBe('已上传 2 个凭证文件'); + expect(translate('zh-TW', 'authFiles.uploaded', { count: 2 })).toBe('已上傳 2 個憑證檔案'); + expect(translate('zh-CN', 'quota.fileDisabled')).toBe('凭证文件已停用'); + expect(Object.values(zhCN).filter((message) => message.includes('认证文件'))).toEqual([]); + expect(Object.values(zhTW).filter((message) => /認證(?:檔案|文件)/.test(message))).toEqual([]); + }); + it('normalizes English variants and keeps Chinese as the fallback', () => { expect(normalizeLocale('en-US')).toBe('en'); expect(normalizeLocale('en')).toBe('en'); diff --git a/tests/operationFeedback.test.ts b/tests/operationFeedback.test.ts index 8892eba..7535847 100644 --- a/tests/operationFeedback.test.ts +++ b/tests/operationFeedback.test.ts @@ -37,10 +37,32 @@ describe('控件自身反馈', () => { expect(nonEmptyCalls[0].arguments[1].getText()).toBe("'error'"); }); - it('认证文件的启用停用由按钮与状态标签反馈', () => { + it('凭证文件的启用停用由按钮与状态标签反馈', () => { expect(actionCalls('AuthFileManagementPage.tsx', 'toggleStatus', 'showNotice').calls).toHaveLength(0); }); + it('停用凭证的启用按钮使用主题高亮,停用操作仍使用次要样式', () => { + const file = 'AuthFileManagementPage.tsx'; + const source = ts.createSourceFile(file, readFileSync(new URL(`../src/pages/${file}`, import.meta.url), 'utf8'), ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX); + let button: ts.JsxOpeningElement | undefined; + const visit = (node: ts.Node) => { + if (ts.isJsxOpeningElement(node) && node.tagName.getText(source) === 'button' + && node.attributes.properties.some((attribute) => ts.isJsxAttribute(attribute) + && attribute.name.getText(source) === 'onClick' + && attribute.initializer?.getText(source).includes('toggleStatus(file)'))) button = node; + ts.forEachChild(node, visit); + }; + visit(source); + const attributes = button?.attributes.properties.filter(ts.isJsxAttribute); + expect(attributes?.find((attribute) => attribute.name.getText(source) === 'className')?.initializer?.getText(source)) + .toBe("{`${disabled ? 'primary-button' : 'secondary-button'} compact-button`}"); + expect(attributes?.find((attribute) => attribute.name.getText(source) === 'disabled')?.initializer?.getText(source)) + .toBe('{busy}'); + const styles = readFileSync(new URL('../src/styles.css', import.meta.url), 'utf8'); + expect(styles).toMatch(/\.real-auth-file-row\.disabled\s*>\s*:not\(\.auth-file-actions\)\s*\{\s*opacity:\s*0\.68;/); + expect(styles).not.toMatch(/\.real-auth-file-row\.disabled\s*\{[^}]*opacity:/); + }); + it('开关切换只后台刷新数据,不卸载整张列表和焦点控件', () => { for (const [file, action, callback] of [ ['ApiAccessPage.tsx', 'toggleProvider', 'loadProviders'],