fix(auth): rename credential file labels and highlight enable action

- Use credential-file terminology in Simplified and Traditional Chinese
- Highlight the Enable button for disabled credentials without dimming actions
- Add regression tests for terminology and button styling
This commit is contained in:
lzt404 committed 2026-09-07 01:26:23 +08:00
1 parent 82268cfa6f
commit 0e5b4b00ad
8 files changed
+61 -26

No files matched your search

+2 -2
View File
@@ -108,7 +108,7 @@ pub(crate) async fn upload_auth_file(
) -> Result<serde_json::Value, String> {
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
}
+5 -5
View File
@@ -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();
+15 -15
View File
@@ -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}',
+1 -1
View File
@@ -11,7 +11,7 @@ const phraseReplacements: ReadonlyArray<readonly [string, string]> = [
['智能体', '智慧代理'],
['会话', '工作階段'],
['使用记录', '使用記錄'],
['认证文件', '認證檔案'],
['凭证文件', '憑證檔案'],
['配置文件', '設定檔'],
['配置', '設定'],
['文件夹', '資料夾'],
+1 -1
View File
@@ -544,7 +544,7 @@ export function AuthFileManagementPage() {
{providerKey(file) ? <button type="button" className="secondary-button compact-button" onClick={() => void openOauthModels(file)} disabled={busy} title={t('authFiles.models.settings')}>{t('authFiles.models.button')}</button> : null}
<button type="button" className="secondary-button compact-button auth-file-priority-button" onClick={() => openPriorityEditor(file)} disabled={busy} title={t('authFiles.priority.hint')}><Pencil size={14} />{t('authFiles.priority.button', { priority })}</button>
<button type="button" className="icon-button quiet" onClick={() => void copyName(name)} disabled={busy} title={t('authFiles.copyName')}>{copied === name ? <Check size={16} /> : <Copy size={16} />}</button>
<button type="button" className="secondary-button compact-button" onClick={() => void toggleStatus(file)} disabled={busy}>{disabled ? t('common.enable') : t('common.disable')}</button>
<button type="button" className={`${disabled ? 'primary-button' : 'secondary-button'} compact-button`} onClick={() => void toggleStatus(file)} disabled={busy}>{disabled ? t('common.enable') : t('common.disable')}</button>
<button type="button" className="icon-button danger" onClick={() => void deleteFile(file)} disabled={busy || isRuntimeOnly(file)} title={t('common.delete')}><Trash2 size={16} /></button>
</div>
{quotaProviderForFile(file) && quotas[quotaKey(file)]?.status !== 'idle' ? <AuthFileQuotaSummary quota={quotas[quotaKey(file)] ?? idleQuota()} /> : null}
+2 -1
View File
@@ -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;
}
+12
View File
@@ -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');
+23 -1
View File
@@ -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'],