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:
1 parent
82268cfa6f
commit
0e5b4b00ad
8 files changed
+61
-26
No files matched your search
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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}',
|
||||
|
||||
@@ -11,7 +11,7 @@ const phraseReplacements: ReadonlyArray<readonly [string, string]> = [
|
||||
['智能体', '智慧代理'],
|
||||
['会话', '工作階段'],
|
||||
['使用记录', '使用記錄'],
|
||||
['认证文件', '認證檔案'],
|
||||
['凭证文件', '憑證檔案'],
|
||||
['配置文件', '設定檔'],
|
||||
['配置', '設定'],
|
||||
['文件夹', '資料夾'],
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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'],
|
||||
|
||||
Reference in new issue
Block a user