fix(api): reflect disabled keys and platform counts
Use the native disabled flag for individual keys, dim disabled rows, and mark partially disabled groups. Name the actual add action in the empty state, count easy-mode platforms by provider group, and use the Meta icon.
This commit is contained in:
1 parent
6e34850780
commit
830fc2c1cc
12 files changed
+182
-67
No files matched your search
@@ -30,7 +30,7 @@ export const easyModeZhCN = {
|
||||
"easyMode.oauth.providerDesc.devin": "Devin / Cognition 账号授权登录",
|
||||
"easyMode.oauth.providerDesc.meta": "Muse / Meta 设备码授权登录",
|
||||
"easyMode.api.platformsConnected": "已接入 {count} 个平台",
|
||||
"easyMode.api.supportedPlatforms": "支持 OpenAI、Anthropic、Codex、Gemini、DeepSeek",
|
||||
"easyMode.api.supportedPlatforms": "支持 OpenAI、Anthropic、Codex、Gemini、DeepSeek、Vertex、xAI、Meta",
|
||||
"easyMode.api.platformName.openai": "OpenAI 格式",
|
||||
"easyMode.api.platformName.claude": "Anthropic 格式",
|
||||
"easyMode.api.platformName.codex": "Codex API",
|
||||
@@ -113,7 +113,7 @@ export const easyModeEn: Record<keyof typeof easyModeZhCN, string> = {
|
||||
"easyMode.oauth.providerDesc.devin": "Devin / Cognition authorization sign-in",
|
||||
"easyMode.oauth.providerDesc.meta": "Muse / Meta device code sign-in",
|
||||
"easyMode.api.platformsConnected": "{count} platform(s) connected",
|
||||
"easyMode.api.supportedPlatforms": "Supports OpenAI, Anthropic, Codex, Gemini, DeepSeek",
|
||||
"easyMode.api.supportedPlatforms": "Supports OpenAI, Anthropic, Codex, Gemini, DeepSeek, Vertex, xAI, Meta",
|
||||
"easyMode.api.platformName.openai": "OpenAI Format",
|
||||
"easyMode.api.platformName.claude": "Anthropic Format",
|
||||
"easyMode.api.platformName.codex": "Codex API",
|
||||
@@ -196,7 +196,7 @@ export const easyModeJa: Record<keyof typeof easyModeZhCN, string> = {
|
||||
"easyMode.oauth.providerDesc.devin": "Devin / Cognition アカウント認証ログイン",
|
||||
"easyMode.oauth.providerDesc.meta": "Muse / Meta デバイスコード認証ログイン",
|
||||
"easyMode.api.platformsConnected": "{count} 件のプラットフォームを接続済み",
|
||||
"easyMode.api.supportedPlatforms": "OpenAI、Anthropic、Codex、Gemini、DeepSeek に対応",
|
||||
"easyMode.api.supportedPlatforms": "OpenAI、Anthropic、Codex、Gemini、DeepSeek、Vertex、xAI、Meta に対応",
|
||||
"easyMode.api.platformName.openai": "OpenAI 形式",
|
||||
"easyMode.api.platformName.claude": "Anthropic 形式",
|
||||
"easyMode.api.platformName.codex": "Codex API",
|
||||
|
||||
+2
-2
@@ -1536,7 +1536,7 @@ export const jaOverrides = {
|
||||
'apiAccess.empty.filtered': '一致する接続がありません',
|
||||
'apiAccess.empty.none': '接続設定がありません',
|
||||
'apiAccess.empty.tryKeyword': '別のキーワードをお試しください',
|
||||
'apiAccess.empty.addFirst': '右上の「追加」から最初の接続を追加してください',
|
||||
'apiAccess.empty.addFirst': '右上の「{action}」から最初の接続を追加してください',
|
||||
'apiAccess.keys.count': 'キー {count} 件',
|
||||
'apiAccess.keys.summary': '{key} · 合計 {count} 件',
|
||||
'apiAccess.remarkValue': 'メモ:{remark}',
|
||||
@@ -1628,7 +1628,7 @@ export const jaOverrides = {
|
||||
'apiAccess.cloak.enableStrict': '厳格モードを有効化',
|
||||
'apiAccess.cloak.cacheUser': 'ユーザー識別子をキャッシュ',
|
||||
'apiAccess.cloak.cacheUserDescription': '偽装後のユーザー識別子を再利用します',
|
||||
'apiAccess.websocket.description': 'この Codex 接続で WebSocket を有効にします',
|
||||
'apiAccess.websocket.description': 'この Codex / xAI 接続で WebSocket を有効にします',
|
||||
'apiAccess.websocket.enable': 'WebSocket を有効化',
|
||||
'apiAccess.cooling.title': 'クールダウンを無効化',
|
||||
'apiAccess.cooling.description': '上流でレート制限が発生しても、この接続を引き続き選択可能にします',
|
||||
|
||||
@@ -1537,7 +1537,7 @@ export const en: Record<MessageKey, string> = {
|
||||
'apiAccess.empty.filtered': 'No matching connections',
|
||||
'apiAccess.empty.none': 'No API connections',
|
||||
'apiAccess.empty.tryKeyword': 'Try a different keyword',
|
||||
'apiAccess.empty.addFirst': 'Click “Add” in the upper-right to create your first connection',
|
||||
'apiAccess.empty.addFirst': 'Click “{action}” in the upper-right to create your first connection',
|
||||
'apiAccess.keys.count': '{count} keys',
|
||||
'apiAccess.keys.summary': '{key} · {count} keys total',
|
||||
'apiAccess.remarkValue': 'Remark: {remark}',
|
||||
@@ -1628,7 +1628,7 @@ export const en: Record<MessageKey, string> = {
|
||||
'apiAccess.cloak.enableStrict': 'Enable strict mode',
|
||||
'apiAccess.cloak.cacheUser': 'Cache User ID',
|
||||
'apiAccess.cloak.cacheUserDescription': 'Reuse the cloaked user identifier',
|
||||
'apiAccess.websocket.description': 'Enable WebSocket for this Codex connection',
|
||||
'apiAccess.websocket.description': 'Enable WebSocket for this Codex or xAI connection',
|
||||
'apiAccess.websocket.enable': 'Enable WebSocket',
|
||||
'apiAccess.cooling.title': 'Disable Cooldown',
|
||||
'apiAccess.cooling.description': 'Allow this connection to remain selectable after upstream rate limiting',
|
||||
|
||||
@@ -1536,7 +1536,7 @@ export const zhCN = {
|
||||
'apiAccess.empty.filtered': '没有匹配的接入',
|
||||
'apiAccess.empty.none': '暂无接入配置',
|
||||
'apiAccess.empty.tryKeyword': '换个关键词试试',
|
||||
'apiAccess.empty.addFirst': '点击右上角“新增”添加第一个接入',
|
||||
'apiAccess.empty.addFirst': '点击右上角“{action}”添加第一个接入',
|
||||
'apiAccess.keys.count': '{count} 个密钥',
|
||||
'apiAccess.keys.summary': '{key} · 共 {count} 个密钥',
|
||||
'apiAccess.remarkValue': '备注:{remark}',
|
||||
@@ -1627,7 +1627,7 @@ export const zhCN = {
|
||||
'apiAccess.cloak.enableStrict': '启用严格模式',
|
||||
'apiAccess.cloak.cacheUser': '缓存用户标识',
|
||||
'apiAccess.cloak.cacheUserDescription': '复用伪装后的用户标识',
|
||||
'apiAccess.websocket.description': '为该 Codex 接入启用 WebSocket',
|
||||
'apiAccess.websocket.description': '为该 Codex / xAI 接入启用 WebSocket',
|
||||
'apiAccess.websocket.enable': '启用 WebSocket',
|
||||
'apiAccess.cooling.title': '禁用冷却',
|
||||
'apiAccess.cooling.description': '上游限流后仍允许继续选择该接入',
|
||||
|
||||
+29
-18
@@ -84,7 +84,7 @@ import { ProviderGroupTemplateFields, ProviderKeyTemplateFields, ProviderModelFi
|
||||
import { providerText } from '../i18n/providerTemplate';
|
||||
import {
|
||||
cleanProviderGroup, effectiveProviderKey, providerGroupIdentity, providerGroupKeys, providerGroupStatus,
|
||||
providerKeyDraft, serializeProviderKey, validateProviderGroupKeys, validateProviderTemplateRecord, type ProviderKeyDraft,
|
||||
providerKeyDraft, providerKeyIsDisabled, serializeProviderKey, validateProviderGroupKeys, validateProviderTemplateRecord, type ProviderKeyDraft,
|
||||
} from '../services/providerGroups';
|
||||
import { providerGroupsApi } from '../services/managementApi';
|
||||
import {
|
||||
@@ -202,7 +202,7 @@ function SortableProviderRow({
|
||||
<article
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className={`real-provider-row${isDragging ? ' dragging' : ''}${isDragOver && !isDragging ? ' drag-over' : ''}`}
|
||||
className={`real-provider-row${isDragging ? ' dragging' : ''}${isDragOver && !isDragging ? ' drag-over' : ''}${row.disabled ? ' disabled' : ''}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@@ -346,9 +346,6 @@ const rowFromRecord = (
|
||||
.map((item) => readString(item, 'api-key', 'apiKey'))
|
||||
.filter(Boolean);
|
||||
const singleApiKey = readString(record, 'api-key', 'apiKey');
|
||||
const excludedModels = Array.isArray(record['excluded-models'])
|
||||
? record['excluded-models'].map(String)
|
||||
: [];
|
||||
return {
|
||||
section,
|
||||
index,
|
||||
@@ -361,9 +358,7 @@ const rowFromRecord = (
|
||||
baseUrl: readString(record, 'base-url', 'baseUrl'),
|
||||
models: Array.isArray(record.models)
|
||||
? record.models.flatMap((model) => modelsFromRecord([model], true)) : modelsFromRecord(record.models, true),
|
||||
disabled: grouped ? providerGroupStatus(record) === 'disabled' : definitionFor(section).openAi
|
||||
? readBoolean(record, 'disabled')
|
||||
: excludedModels.some((model) => model.trim() === '*'),
|
||||
disabled: grouped ? providerGroupStatus(record) === 'disabled' : providerKeyIsDisabled(record, record),
|
||||
priority: record.priority == null ? null : readNumber(record, 'priority'),
|
||||
authIndex: entry
|
||||
? readString(entry, 'auth-index', 'authIndex')
|
||||
@@ -372,11 +367,20 @@ const rowFromRecord = (
|
||||
};
|
||||
};
|
||||
|
||||
const rowsFromGroups = (section: ProviderSection, groups: Record<string, unknown>[]): ProviderRow[] =>
|
||||
export const providerRowsFromGroups = (section: ProviderSection, groups: Record<string, unknown>[]): ProviderRow[] =>
|
||||
providerEntries(section, groups).map(({ record, source }, index) => {
|
||||
const deepSeek = section === 'codex-api-key' && (isDeepSeekRecord(record) || isDeepSeekRecord(source.group));
|
||||
const row = rowFromRecord(section, record, index);
|
||||
return { ...row, source, category: deepSeek ? 'deepseek' : section, name: deepSeek ? 'DeepSeek' : row.name };
|
||||
const key = source.keyIndex !== undefined && Array.isArray(source.group.keys)
|
||||
? source.group.keys[source.keyIndex]
|
||||
: undefined;
|
||||
return {
|
||||
...row,
|
||||
source,
|
||||
category: deepSeek ? 'deepseek' : section,
|
||||
name: deepSeek ? 'DeepSeek' : row.name,
|
||||
disabled: isRecord(key) ? providerKeyIsDisabled(source.group, key) : row.disabled,
|
||||
};
|
||||
});
|
||||
|
||||
export const providerRemarkIdentity = (
|
||||
@@ -1184,6 +1188,8 @@ export const providerRecordWithDisabledState = (
|
||||
nextRecord.disabled = disabled;
|
||||
return nextRecord;
|
||||
}
|
||||
if (disabled) nextRecord.disabled = true;
|
||||
else delete nextRecord.disabled;
|
||||
|
||||
const excludedModels = Array.isArray(nextRecord['excluded-models'])
|
||||
? nextRecord['excluded-models'].map(String).filter((model) => model.trim() !== '*')
|
||||
@@ -1257,7 +1263,7 @@ export function ApiAccessPage() {
|
||||
|
||||
useEffect(() => {
|
||||
const providerRows = (Object.entries(records) as [ProviderSection, Record<string, unknown>[]][])
|
||||
.flatMap(([section, items]) => rowsFromGroups(section, items));
|
||||
.flatMap(([section, items]) => providerRowsFromGroups(section, items));
|
||||
if (providerRows.length === 0) {
|
||||
setApiAccessRemarks({});
|
||||
return;
|
||||
@@ -1287,7 +1293,7 @@ export function ApiAccessPage() {
|
||||
|
||||
const rows = useMemo(
|
||||
() =>
|
||||
rowsFromGroups(activeSection, records[activeSection])
|
||||
providerRowsFromGroups(activeSection, records[activeSection])
|
||||
.map((row) => ({
|
||||
...row,
|
||||
remark: apiAccessRemarks[
|
||||
@@ -1327,7 +1333,7 @@ export function ApiAccessPage() {
|
||||
section: ProviderSection, previous: Record<string, unknown>[], next: Record<string, unknown>[],
|
||||
edited?: { row: ProviderRow; remark: string },
|
||||
) => {
|
||||
const previousRows = rowsFromGroups(section, previous);
|
||||
const previousRows = providerRowsFromGroups(section, previous);
|
||||
const previousLocators = previousRows.flatMap((row) => [
|
||||
apiAccessRemarkLocatorFromRow(row), apiAccessRemarkLocatorFromRecord(section, row.source!.group),
|
||||
]);
|
||||
@@ -1337,7 +1343,7 @@ export function ApiAccessPage() {
|
||||
const previousRemarks = new Map(previousRows.map((row, index) => [
|
||||
providerRemarkIdentity(section, apiAccessRemarkLocatorFromRow(row)), resolved[index * 2] || resolved[index * 2 + 1] || '',
|
||||
]));
|
||||
const nextRows = rowsFromGroups(section, next);
|
||||
const nextRows = providerRowsFromGroups(section, next);
|
||||
const allRecords = nextRows.map(apiAccessRemarkLocatorFromRow);
|
||||
const byRemark = new Map<string, ApiAccessRemarkLocator[]>();
|
||||
for (const row of nextRows) {
|
||||
@@ -1468,7 +1474,7 @@ export function ApiAccessPage() {
|
||||
const savedRecord = definition.openAi
|
||||
? buildProviderGroupRecord(activeSection, draftToSave, currentRecord)
|
||||
: buildProviderKeyRecord(activeSection, draftToSave, currentRecord);
|
||||
const currentEntries = rowsFromGroups(activeSection, current);
|
||||
const currentEntries = providerRowsFromGroups(activeSection, current);
|
||||
const targetIndex = location ? currentEntries.findIndex((row) => {
|
||||
return row.source?.groupIndex === location.groupIndex && row.source?.keyIndex === location.keyIndex;
|
||||
}) : -1;
|
||||
@@ -1483,7 +1489,7 @@ export function ApiAccessPage() {
|
||||
const detached = location && nextList.length > current.length;
|
||||
const savedGroupIndex = location ? location.groupIndex + (detached ? 1 : 0) : nextList.length - 1;
|
||||
const savedKeyIndex = definition.openAi ? undefined : location && !detached ? location.keyIndex : 0;
|
||||
const savedRow = rowsFromGroups(activeSection, nextList).find((row) => row.source?.groupIndex === savedGroupIndex && row.source?.keyIndex === savedKeyIndex)!;
|
||||
const savedRow = providerRowsFromGroups(activeSection, nextList).find((row) => row.source?.groupIndex === savedGroupIndex && row.source?.keyIndex === savedKeyIndex)!;
|
||||
await persistEntryRemarks(activeSection, current, nextList, { row: savedRow, remark: draftToSave.remark });
|
||||
setNotice(editingRow ? t('apiAccess.notice.updated') : t('apiAccess.notice.added'));
|
||||
await loadProviders();
|
||||
@@ -1565,7 +1571,7 @@ export function ApiAccessPage() {
|
||||
};
|
||||
|
||||
const countForDefinition = (definition: ProviderDefinition) =>
|
||||
rowsFromGroups(definition.section, records[definition.section]).filter((row) => row.category === definition.id).length;
|
||||
providerRowsFromGroups(definition.section, records[definition.section]).filter((row) => row.category === definition.id).length;
|
||||
|
||||
return (
|
||||
<section className="page management-page api-access-page">
|
||||
@@ -1620,7 +1626,9 @@ export function ApiAccessPage() {
|
||||
<div className="management-empty">
|
||||
<Filter size={24} aria-hidden="true" />
|
||||
<strong>{filter ? t('apiAccess.empty.filtered') : t('apiAccess.empty.none')}</strong>
|
||||
<span>{filter ? t('apiAccess.empty.tryKeyword') : t('apiAccess.empty.addFirst')}</span>
|
||||
<span>{filter ? t('apiAccess.empty.tryKeyword') : t('apiAccess.empty.addFirst', {
|
||||
action: t(activeDefinition.openAi ? 'apiAccess.entries.addProvider' : 'apiAccess.entries.addKey'),
|
||||
})}</span>
|
||||
</div>
|
||||
) : (
|
||||
<DndContext
|
||||
@@ -1647,6 +1655,9 @@ export function ApiAccessPage() {
|
||||
<div className="provider-row-main">
|
||||
<div className="provider-row-title">
|
||||
<strong title={row.name}>{row.name}</strong>
|
||||
{Array.isArray(row.record.keys) && providerGroupStatus(row.record) === 'partial'
|
||||
? <span className="state-pill">{t('apiAccess.groups.partial')}</span>
|
||||
: null}
|
||||
{row.remark ? <span title={row.remark}>{row.remark}</span> : null}
|
||||
</div>
|
||||
<span className="provider-row-url" title={row.baseUrl || undefined}>{row.baseUrl || t('apiAccess.defaultUrl')}</span>
|
||||
|
||||
+12
-15
@@ -33,6 +33,7 @@ import {
|
||||
responseList,
|
||||
} from "../services/managementApi";
|
||||
import { appendNativeProviderGroup } from '../services/providerGroups';
|
||||
import { providerRowsFromGroups, type ProviderSection } from './ApiAccessPage';
|
||||
import {
|
||||
DEEPSEEK_BASE_URL,
|
||||
fetchModels,
|
||||
@@ -103,15 +104,9 @@ const apiSectionOptions: ApiSectionOption[] = [
|
||||
{ id: 'interactions', managementSection: 'interactions-api-key', nameKey: 'easyMode.api.platformName.gemini', name: 'Gemini Interactions', provider: 'interactions', defaultBaseUrl: 'https://generativelanguage.googleapis.com', icon: geminiIcon },
|
||||
{ id: 'vertex', managementSection: 'vertex-api-key', nameKey: 'easyMode.api.platformName.gemini', name: 'Vertex AI', provider: 'vertex', defaultBaseUrl: 'https://aiplatform.googleapis.com', icon: vertexIcon },
|
||||
{ id: 'xai', managementSection: 'xai-api-key', nameKey: 'easyMode.api.platformName.openai', name: 'xAI', provider: 'xai', defaultBaseUrl: 'https://api.x.ai/v1', icon: grokIcon },
|
||||
{ id: 'meta', managementSection: 'meta-api-key', nameKey: 'easyMode.api.platformName.openai', name: 'Meta', provider: 'meta', defaultBaseUrl: 'https://api.meta.ai/v1', icon: openaiIcon },
|
||||
{ id: 'meta', managementSection: 'meta-api-key', nameKey: 'easyMode.api.platformName.openai', name: 'Meta', provider: 'meta', defaultBaseUrl: 'https://api.meta.ai/v1', icon: metaIcon },
|
||||
];
|
||||
|
||||
const isDeepSeekRecord = (record: Record<string, unknown>) => {
|
||||
const name = readString(record, "name").trim().toLowerCase();
|
||||
const baseUrl = readString(record, "base-url", "baseUrl").trim().toLowerCase();
|
||||
return name.includes("deepseek") || /^https?:\/\/api\.deepseek\.com(?:\/|$)/i.test(baseUrl);
|
||||
};
|
||||
|
||||
export function EasyModePage({
|
||||
onExit,
|
||||
theme,
|
||||
@@ -222,14 +217,16 @@ export function EasyModePage({
|
||||
const sections = [...new Set(apiSectionOptions.map((option) => option.managementSection))];
|
||||
const recordsBySection = Object.fromEntries(await Promise.all(sections.map(async (section) => [section, await providerGroupsApi.get(section)]))) as Record<ApiManagementSection, Record<string, unknown>[]>;
|
||||
|
||||
for (const section of apiSectionOptions) {
|
||||
const sourceList = recordsBySection[section.managementSection];
|
||||
const list = section.id === "deepseek"
|
||||
? sourceList.filter(isDeepSeekRecord)
|
||||
: section.id === "codex"
|
||||
? sourceList.filter((record) => !isDeepSeekRecord(record))
|
||||
: sourceList;
|
||||
counts[section.id] = list.length;
|
||||
for (const option of apiSectionOptions) {
|
||||
const rows = providerRowsFromGroups(
|
||||
option.managementSection as ProviderSection,
|
||||
recordsBySection[option.managementSection],
|
||||
);
|
||||
counts[option.id] = new Set(rows.filter((row) => (
|
||||
option.id === 'deepseek' ? row.category === 'deepseek'
|
||||
: option.id === 'codex' ? row.category !== 'deepseek'
|
||||
: true
|
||||
)).map((row) => row.source?.groupIndex)).size;
|
||||
}
|
||||
setApiCounts(counts);
|
||||
} catch (e) {
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
--usage-log-line: #e8e8e8;
|
||||
--usage-log-hover: #f7f7f7;
|
||||
--usage-log-soft: #f5f5f5;
|
||||
--usage-log-input: #526b76;
|
||||
--usage-log-output: #16756a;
|
||||
--usage-log-reasoning: #526b76;
|
||||
--usage-log-cache-read: #16756a;
|
||||
--usage-log-cache-write: #63767d;
|
||||
--usage-log-input: #2563eb;
|
||||
--usage-log-output: #0f766e;
|
||||
--usage-log-reasoning: #b45309;
|
||||
--usage-log-cache-read: #0f766e;
|
||||
--usage-log-cache-write: #b45309;
|
||||
--usage-log-response-model: #9a5808;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
@@ -93,7 +93,7 @@
|
||||
.usage-request-log .usage-events-table {
|
||||
/* Fill spare space proportionally while retaining saved widths on narrow screens. */
|
||||
min-width: 100%;
|
||||
font-size: 12px;
|
||||
font-size: var(--font-size-meta);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
background: var(--usage-log-surface);
|
||||
color: var(--usage-log-muted);
|
||||
box-shadow: none;
|
||||
font-size: 11px;
|
||||
font-size: var(--font-size-caption);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -154,8 +154,8 @@
|
||||
|
||||
.usage-request-log .usage-events-table td small {
|
||||
color: var(--usage-log-muted);
|
||||
font-size: 10px;
|
||||
line-height: 1.6;
|
||||
font-size: var(--font-size-caption);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.usage-request-log .usage-events-table .usage-stacked-cell > small {
|
||||
@@ -169,7 +169,7 @@
|
||||
margin-top: 2px;
|
||||
overflow: hidden;
|
||||
color: var(--usage-log-muted);
|
||||
font-size: 10px;
|
||||
font-size: var(--font-size-caption);
|
||||
line-height: 1.4;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -186,7 +186,7 @@
|
||||
border: 1px solid color-mix(in srgb, var(--usage-log-response-model) 30%, transparent);
|
||||
border-radius: 3px;
|
||||
color: var(--usage-log-response-model);
|
||||
font-size: 10px;
|
||||
font-size: var(--font-size-caption);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@
|
||||
|
||||
.usage-request-log .usage-events-table .usage-td-key > strong {
|
||||
font-family: var(--font-family-mono);
|
||||
font-size: 11px;
|
||||
font-size: var(--font-size-caption);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@@ -220,7 +220,7 @@
|
||||
align-items: center;
|
||||
gap: 3px 9px;
|
||||
margin-top: 3px;
|
||||
font-size: 10px;
|
||||
font-size: var(--font-size-caption);
|
||||
font-weight: 500;
|
||||
font-family: var(--font-family-mono);
|
||||
line-height: 1.45;
|
||||
@@ -253,7 +253,7 @@
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
font-size: 11px;
|
||||
font-size: var(--font-size-meta);
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
}
|
||||
@@ -375,11 +375,11 @@
|
||||
--usage-log-line: #34363b;
|
||||
--usage-log-hover: #26282c;
|
||||
--usage-log-soft: #292b30;
|
||||
--usage-log-input: #adc0ca;
|
||||
--usage-log-output: #77cbbb;
|
||||
--usage-log-reasoning: #adc0ca;
|
||||
--usage-log-cache-read: #77cbbb;
|
||||
--usage-log-cache-write: #aaa8a4;
|
||||
--usage-log-input: #93c5fd;
|
||||
--usage-log-output: #5eead4;
|
||||
--usage-log-reasoning: #fbbf24;
|
||||
--usage-log-cache-read: #5eead4;
|
||||
--usage-log-cache-write: #fbbf24;
|
||||
--usage-log-response-model: #f0bc73;
|
||||
border-color: var(--usage-log-line);
|
||||
background: var(--usage-log-surface);
|
||||
|
||||
@@ -4979,6 +4979,10 @@ textarea:focus-visible {
|
||||
background: color-mix(in srgb, var(--theme-f4f1ea) 65%, transparent);
|
||||
}
|
||||
|
||||
.real-provider-row.disabled > :not(.provider-row-actions) {
|
||||
opacity: 0.68;
|
||||
}
|
||||
|
||||
.provider-drag-handle {
|
||||
margin-top: -2px;
|
||||
cursor: grab;
|
||||
|
||||
@@ -171,15 +171,22 @@
|
||||
top: 0;
|
||||
border: 0;
|
||||
background: var(--usage-log-surface);
|
||||
color: var(--usage-log-muted);
|
||||
font-size: 12px;
|
||||
color: #3e4c5a;
|
||||
font-size: 13px;
|
||||
font-weight: 650;
|
||||
letter-spacing: .01em;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.usage-records-page .usage-request-log .usage-events-table th .usage-th-content {
|
||||
justify-content: center;
|
||||
padding: 0 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.usage-records-page .usage-request-log .usage-events-table :is(th, td),
|
||||
.usage-records-page .usage-request-log .usage-events-table :is(th, td).align-left {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.usage-records-page .usage-request-log .usage-events-table thead th:first-child {
|
||||
@@ -227,13 +234,74 @@
|
||||
}
|
||||
|
||||
.usage-records-page .usage-request-log .usage-events-table td strong {
|
||||
font-size: 12px;
|
||||
color: var(--usage-page-text);
|
||||
font-size: 14px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.usage-records-page .usage-request-log .usage-events-table td small {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.usage-records-page .usage-request-log .usage-model-effort,
|
||||
.usage-records-page .usage-request-log .usage-access-type {
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
margin: 5px auto 0;
|
||||
padding: 1px 7px;
|
||||
border: 1px solid color-mix(in srgb, var(--ui-accent) 24%, transparent);
|
||||
border-radius: 999px;
|
||||
background: var(--ui-accent-soft);
|
||||
color: var(--ui-accent-strong);
|
||||
font-size: 11px;
|
||||
font-weight: 650;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.usage-records-page .usage-request-log .usage-access-type {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.usage-records-page .usage-request-log .usage-result {
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
margin-inline: auto;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.usage-records-page .usage-request-log .usage-result-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.usage-records-page .usage-request-log .usage-result.success {
|
||||
background: var(--success-badge-bg);
|
||||
color: var(--success-badge-text);
|
||||
}
|
||||
|
||||
.usage-records-page .usage-request-log .usage-result.failed {
|
||||
background: var(--failure-badge-bg);
|
||||
color: var(--failure-badge-text);
|
||||
}
|
||||
|
||||
.usage-records-page .usage-request-log .usage-result.canceled {
|
||||
background: var(--usage-page-soft);
|
||||
color: var(--usage-page-muted);
|
||||
}
|
||||
|
||||
.usage-records-page .usage-request-log .usage-event-metrics {
|
||||
justify-content: center;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .usage-records-page .usage-request-log .usage-events-table th {
|
||||
color: #d5deea;
|
||||
}
|
||||
|
||||
.usage-records-page .usage-request-log .usage-event-metrics {
|
||||
gap: 2px 7px;
|
||||
margin-top: 2px;
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
|
||||
:root:not([data-theme='dark']) .workspace .usage-request-log {
|
||||
--usage-log-soft: #fff;
|
||||
--usage-log-hover: #fff;
|
||||
--usage-log-hover: #f2fbf9;
|
||||
}
|
||||
|
||||
/* The usage views and outer panels should read as clean white documents in light mode.
|
||||
@@ -155,10 +155,13 @@
|
||||
:root:not([data-theme='dark']) .workspace .usage-records-page
|
||||
.usage-request-log .usage-events-table tbody tr td,
|
||||
:root:not([data-theme='dark']) .workspace .usage-records-page
|
||||
.usage-request-log .usage-events-table tbody tr:nth-child(even) td,
|
||||
.usage-request-log .usage-events-table tbody tr:nth-child(even) td {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
:root:not([data-theme='dark']) .workspace .usage-records-page
|
||||
.usage-request-log .usage-events-table tbody tr:hover td {
|
||||
background: #fff;
|
||||
background: #f2fbf9;
|
||||
}
|
||||
|
||||
:root:not([data-theme='dark']) .workspace .usage-records-page
|
||||
|
||||
@@ -98,6 +98,16 @@ describe('i18n', () => {
|
||||
expect(Object.entries(en).filter(([, message]) => /\p{Script=Han}/u.test(message))).toEqual([]);
|
||||
});
|
||||
|
||||
it('names the actual add action in the empty API access prompt', () => {
|
||||
expect(translate('zh-CN', 'apiAccess.empty.addFirst', { action: '添加 API Key' }))
|
||||
.toBe('点击右上角“添加 API Key”添加第一个接入');
|
||||
expect(translate('en', 'apiAccess.empty.addFirst', { action: 'Add Service' }))
|
||||
.toBe('Click “Add Service” in the upper-right to create your first connection');
|
||||
expect(translate('ja', 'apiAccess.empty.addFirst', { action: 'API キーを追加' }))
|
||||
.toBe('右上の「API キーを追加」から最初の接続を追加してください');
|
||||
expect(translate('zh-CN', 'easyMode.api.supportedPlatforms')).toContain('Vertex');
|
||||
});
|
||||
|
||||
it('localizes API controls and usage labels in Chinese, English, and Japanese', () => {
|
||||
for (const [key, chinese, english, japanese] of [
|
||||
['apiAccess.cloak.auto', '自动', 'Automatic', '自動'],
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
providerProxyDraftFromRecord,
|
||||
providerRecordWithDisabledState,
|
||||
providerRemarkIdentity,
|
||||
providerRowsFromGroups,
|
||||
providerSectionOrder,
|
||||
reorderProviderRecords,
|
||||
resolveProviderRecordIndex,
|
||||
@@ -589,7 +590,28 @@ describe('API 接入配置合并', () => {
|
||||
const enabled = providerRecordWithDisabledState('codex-api-key', disabled, false);
|
||||
|
||||
expect(disabled['excluded-models']).toEqual(['preview-*', '*']);
|
||||
expect(disabled.disabled).toBe(true);
|
||||
expect(enabled['excluded-models']).toEqual(['preview-*']);
|
||||
expect(enabled.disabled).toBeUndefined();
|
||||
});
|
||||
|
||||
it('列表按原生 disabled 标记显示每把密钥的启用状态', () => {
|
||||
const rows = providerRowsFromGroups('claude-api-key', [{
|
||||
name: 'claude-1',
|
||||
keys: [
|
||||
{ 'api-key': 'live-key' },
|
||||
{ 'api-key': 'paused-key', disabled: true },
|
||||
],
|
||||
}]);
|
||||
expect(rows.map((row) => row.disabled)).toEqual([false, true]);
|
||||
expect(providerRowsFromGroups('openai-compatibility', [{
|
||||
name: 'relay',
|
||||
keys: [{ 'api-key': 'a' }, { 'api-key': 'b', disabled: true }],
|
||||
}])[0].disabled).toBe(false);
|
||||
expect(providerRowsFromGroups('openai-compatibility', [{
|
||||
name: 'relay',
|
||||
keys: [{ 'api-key': 'a', disabled: true }, { 'api-key': 'b', disabled: true }],
|
||||
}])[0].disabled).toBe(true);
|
||||
});
|
||||
|
||||
it('运行时补全默认 Base URL 后仍能按原始索引找到配置记录', () => {
|
||||
|
||||
Reference in new issue
Block a user