feat: update API access error messages and remarks handling for improved clarity
This commit is contained in:
1 parent
70eb90fe06
commit
a912dd473a
4 files changed
+45
-27
No files matched your search
+4
-5
@@ -522,7 +522,7 @@ export const jaOverrides = {
|
||||
'apiAccess.error.headerMissingColon': 'カスタムヘッダーの {number} 行目にコロンがありません',
|
||||
'apiAccess.error.headerInvalid': 'カスタムヘッダーの {number} 行目の形式が無効です',
|
||||
'apiAccess.error.partialLoad': '一部の接続を読み込めませんでした:{errors}',
|
||||
'apiAccess.error.requiredAll': '名前、Base URL、キーを入力してください',
|
||||
'apiAccess.error.requiredAll': 'メモ、Base URL、キーを入力してください',
|
||||
'apiAccess.error.requiredBaseKey': 'Base URL と API キーを入力してください',
|
||||
'apiAccess.error.requiredKey': 'API キーを入力してください',
|
||||
'apiAccess.error.baseRequired': '{provider} 接続には Base URL が必要です',
|
||||
@@ -532,7 +532,7 @@ export const jaOverrides = {
|
||||
'apiAccess.error.remarkInvalid': 'メモは 80 文字以内で、改行や制御文字を含めないでください',
|
||||
'apiAccess.notice.updated': '接続を更新しました',
|
||||
'apiAccess.notice.added': '接続を追加しました',
|
||||
'apiAccess.deleteConfirm': '「{name}」を削除しますか?',
|
||||
'apiAccess.deleteConfirm': '「{remark}」を削除しますか?',
|
||||
'apiAccess.notice.deleted': '接続を削除しました',
|
||||
'apiAccess.notice.enabled': '接続を有効にしました',
|
||||
'apiAccess.notice.disabled': '接続を無効にしました',
|
||||
@@ -540,7 +540,7 @@ export const jaOverrides = {
|
||||
'apiAccess.count': '接続 {count} 件',
|
||||
'apiAccess.add': '追加',
|
||||
'apiAccess.matches': '一致する接続 {count} 件',
|
||||
'apiAccess.search': '名前、キー、アドレスを検索',
|
||||
'apiAccess.search': 'メモ、キー、アドレスを検索',
|
||||
'apiAccess.loading': '設定を読み込んでいます',
|
||||
'apiAccess.empty.filtered': '一致する接続がありません',
|
||||
'apiAccess.empty.none': '接続設定がありません',
|
||||
@@ -557,7 +557,7 @@ export const jaOverrides = {
|
||||
'apiAccess.disable': '接続を無効化',
|
||||
'apiAccess.status.disabled': '無効',
|
||||
'apiAccess.status.enabled': '有効',
|
||||
'apiAccess.toggleAria': '{name} 接続の{action}スイッチ',
|
||||
'apiAccess.toggleAria': '{remark} 接続の{action}スイッチ',
|
||||
'common.edit': '編集',
|
||||
'apiAccess.error.enterBaseUrl': '先に Base URL を入力してください',
|
||||
'apiAccess.error.noAvailableModels': '利用可能なモデルが見つかりません',
|
||||
@@ -571,7 +571,6 @@ export const jaOverrides = {
|
||||
'apiAccess.models.allHint': '現在、上流のすべてのモデルを公開しています',
|
||||
'apiAccess.dialog.edit': 'API 接続を編集',
|
||||
'apiAccess.dialog.add': 'API 接続を追加',
|
||||
'apiAccess.field.name': '名前',
|
||||
'apiAccess.field.keysMany': 'API キー(1 行に 1 件)',
|
||||
'apiAccess.field.key': 'API キー',
|
||||
'apiAccess.field.remark': 'メモ',
|
||||
|
||||
+8
-10
@@ -525,7 +525,7 @@ export const zhCN = {
|
||||
'apiAccess.error.headerMissingColon': '自定义请求头第 {number} 行缺少冒号',
|
||||
'apiAccess.error.headerInvalid': '自定义请求头第 {number} 行格式无效',
|
||||
'apiAccess.error.partialLoad': '部分接入读取失败:{errors}',
|
||||
'apiAccess.error.requiredAll': '名称、Base URL 和密钥不能为空',
|
||||
'apiAccess.error.requiredAll': '备注、Base URL 和密钥不能为空',
|
||||
'apiAccess.error.requiredBaseKey': 'Base URL 和 API 密钥不能为空',
|
||||
'apiAccess.error.requiredKey': 'API 密钥不能为空',
|
||||
'apiAccess.error.baseRequired': '{provider} 接入必须填写 Base URL',
|
||||
@@ -535,7 +535,7 @@ export const zhCN = {
|
||||
'apiAccess.error.remarkInvalid': '备注不能超过 80 个字符,且不能包含换行或控制字符',
|
||||
'apiAccess.notice.updated': '接入已更新',
|
||||
'apiAccess.notice.added': '接入已新增',
|
||||
'apiAccess.deleteConfirm': '确定删除“{name}”吗?',
|
||||
'apiAccess.deleteConfirm': '确定删除“{remark}”吗?',
|
||||
'apiAccess.notice.deleted': '接入已删除',
|
||||
'apiAccess.notice.enabled': '接入已启用',
|
||||
'apiAccess.notice.disabled': '接入已停用',
|
||||
@@ -543,7 +543,7 @@ export const zhCN = {
|
||||
'apiAccess.count': '{count} 个接入',
|
||||
'apiAccess.add': '新增',
|
||||
'apiAccess.matches': '{count} 个匹配接入',
|
||||
'apiAccess.search': '搜索名称、密钥或地址',
|
||||
'apiAccess.search': '搜索备注、密钥或地址',
|
||||
'apiAccess.loading': '读取配置中',
|
||||
'apiAccess.empty.filtered': '没有匹配的接入',
|
||||
'apiAccess.empty.none': '暂无接入配置',
|
||||
@@ -560,7 +560,7 @@ export const zhCN = {
|
||||
'apiAccess.disable': '停用接入',
|
||||
'apiAccess.status.disabled': '已停用',
|
||||
'apiAccess.status.enabled': '已启用',
|
||||
'apiAccess.toggleAria': '{name} 接入{action}开关',
|
||||
'apiAccess.toggleAria': '{remark} 接入{action}开关',
|
||||
'common.edit': '编辑',
|
||||
'apiAccess.error.enterBaseUrl': '请先填写 Base URL',
|
||||
'apiAccess.error.noAvailableModels': '未发现可用模型',
|
||||
@@ -574,7 +574,6 @@ export const zhCN = {
|
||||
'apiAccess.models.allHint': '当前开放上游全部模型',
|
||||
'apiAccess.dialog.edit': '编辑 API 接入',
|
||||
'apiAccess.dialog.add': '新增 API 接入',
|
||||
'apiAccess.field.name': '名称',
|
||||
'apiAccess.field.keysMany': 'API 密钥(每行一个)',
|
||||
'apiAccess.field.key': 'API 密钥',
|
||||
'apiAccess.field.remark': '备注',
|
||||
@@ -1341,7 +1340,7 @@ export const en: Record<MessageKey, string> = {
|
||||
'apiAccess.error.headerMissingColon': 'Custom header line {number} is missing a colon',
|
||||
'apiAccess.error.headerInvalid': 'Custom header line {number} is invalid',
|
||||
'apiAccess.error.partialLoad': 'Some connections could not be loaded: {errors}',
|
||||
'apiAccess.error.requiredAll': 'Name, Base URL, and key are required',
|
||||
'apiAccess.error.requiredAll': 'Remark, Base URL, and key are required',
|
||||
'apiAccess.error.requiredBaseKey': 'Base URL and API key are required',
|
||||
'apiAccess.error.requiredKey': 'API key is required',
|
||||
'apiAccess.error.baseRequired': 'A Base URL is required for {provider}',
|
||||
@@ -1351,7 +1350,7 @@ export const en: Record<MessageKey, string> = {
|
||||
'apiAccess.error.remarkInvalid': 'The remark cannot exceed 80 characters or contain line breaks or control characters',
|
||||
'apiAccess.notice.updated': 'Connection updated',
|
||||
'apiAccess.notice.added': 'Connection added',
|
||||
'apiAccess.deleteConfirm': 'Delete “{name}”?',
|
||||
'apiAccess.deleteConfirm': 'Delete “{remark}”?',
|
||||
'apiAccess.notice.deleted': 'Connection deleted',
|
||||
'apiAccess.notice.enabled': 'Connection enabled',
|
||||
'apiAccess.notice.disabled': 'Connection disabled',
|
||||
@@ -1359,7 +1358,7 @@ export const en: Record<MessageKey, string> = {
|
||||
'apiAccess.count': '{count} connections',
|
||||
'apiAccess.add': 'Add',
|
||||
'apiAccess.matches': '{count} matching connections',
|
||||
'apiAccess.search': 'Search names, keys, or addresses',
|
||||
'apiAccess.search': 'Search remarks, keys, or addresses',
|
||||
'apiAccess.loading': 'Loading configuration',
|
||||
'apiAccess.empty.filtered': 'No matching connections',
|
||||
'apiAccess.empty.none': 'No API connections',
|
||||
@@ -1376,7 +1375,7 @@ export const en: Record<MessageKey, string> = {
|
||||
'apiAccess.disable': 'Disable connection',
|
||||
'apiAccess.status.disabled': 'Disabled',
|
||||
'apiAccess.status.enabled': 'Enabled',
|
||||
'apiAccess.toggleAria': '{name} connection {action} switch',
|
||||
'apiAccess.toggleAria': '{remark} connection {action} switch',
|
||||
'common.edit': 'Edit',
|
||||
'apiAccess.error.enterBaseUrl': 'Enter a Base URL first',
|
||||
'apiAccess.error.noAvailableModels': 'No available models found',
|
||||
@@ -1390,7 +1389,6 @@ export const en: Record<MessageKey, string> = {
|
||||
'apiAccess.models.allHint': 'All upstream models are currently open',
|
||||
'apiAccess.dialog.edit': 'Edit API Connection',
|
||||
'apiAccess.dialog.add': 'Add API Connection',
|
||||
'apiAccess.field.name': 'Name',
|
||||
'apiAccess.field.keysMany': 'API Keys (one per line)',
|
||||
'apiAccess.field.key': 'API Key',
|
||||
'apiAccess.field.remark': 'Remark',
|
||||
|
||||
+20
-12
@@ -323,7 +323,7 @@ const thinkingLevelsFromModels = (models: ModelOption[]): string[] => {
|
||||
const draftFromRow = (row: ProviderRow): ProviderDraft => ({
|
||||
name: row.name,
|
||||
apiKey: definitionFor(row.section).openAi ? row.apiKeys.join('\n') : row.apiKey,
|
||||
remark: row.remark,
|
||||
remark: row.remark || (definitionFor(row.section).openAi ? row.name : ''),
|
||||
baseUrl: row.baseUrl,
|
||||
priority: row.priority === null ? '' : String(row.priority),
|
||||
models: row.models,
|
||||
@@ -383,11 +383,19 @@ export const createProviderDraft = (category: ProviderCategory): ProviderDraft =
|
||||
return {
|
||||
...draft,
|
||||
name: 'DeepSeek',
|
||||
remark: 'DeepSeek',
|
||||
baseUrl: DEEPSEEK_BASE_URL,
|
||||
thinkingLevels: [...DEEPSEEK_THINKING_LEVELS],
|
||||
};
|
||||
};
|
||||
|
||||
export const applyProviderRemarkIdentity = (
|
||||
category: ProviderCategory,
|
||||
draft: ProviderDraft,
|
||||
): ProviderDraft => definitionFor(category).openAi
|
||||
? { ...draft, name: draft.remark.trim() }
|
||||
: draft;
|
||||
|
||||
export const applyProviderPreset = (
|
||||
category: ProviderCategory,
|
||||
draft: ProviderDraft,
|
||||
@@ -696,7 +704,7 @@ export function ApiAccessPage() {
|
||||
.filter((row) => {
|
||||
const query = filter.trim().toLowerCase();
|
||||
if (!query) return true;
|
||||
return [row.name, row.remark, row.apiKey, row.baseUrl, row.models.map((model) => model.name).join(' ')]
|
||||
return [row.remark || row.name, row.apiKey, row.baseUrl, row.models.map((model) => model.name).join(' ')]
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
.includes(query);
|
||||
@@ -721,7 +729,10 @@ export function ApiAccessPage() {
|
||||
|
||||
const saveProvider = async (nextDraft: ProviderDraft): Promise<boolean> => {
|
||||
const definition = activeDefinition;
|
||||
const preparedDraft = applyProviderPreset(activeCategory, nextDraft);
|
||||
const preparedDraft = applyProviderRemarkIdentity(
|
||||
activeCategory,
|
||||
applyProviderPreset(activeCategory, nextDraft),
|
||||
);
|
||||
const baseUrlRequired = definition.openAi || definition.section === 'codex-api-key';
|
||||
const parsedApiKeys = preparedDraft.apiKey
|
||||
.split(/\r?\n/)
|
||||
@@ -729,7 +740,7 @@ export function ApiAccessPage() {
|
||||
.filter(Boolean);
|
||||
if (
|
||||
parsedApiKeys.length === 0
|
||||
|| (definition.openAi && !preparedDraft.name.trim())
|
||||
|| (definition.openAi && !preparedDraft.remark.trim())
|
||||
|| (baseUrlRequired && !preparedDraft.baseUrl.trim())
|
||||
) {
|
||||
setError(
|
||||
@@ -827,7 +838,7 @@ export function ApiAccessPage() {
|
||||
};
|
||||
|
||||
const deleteRow = async (row: ProviderRow) => {
|
||||
if (!window.confirm(t('apiAccess.deleteConfirm', { name: row.name }))) return;
|
||||
if (!window.confirm(t('apiAccess.deleteConfirm', { remark: row.remark || row.name }))) return;
|
||||
setBusy(true);
|
||||
setError('');
|
||||
try {
|
||||
@@ -992,7 +1003,7 @@ export function ApiAccessPage() {
|
||||
checked={!row.disabled}
|
||||
onChange={() => void toggleProvider(row)}
|
||||
disabled={busy}
|
||||
aria-label={t('apiAccess.toggleAria', { name: row.name, action: row.disabled ? t('common.enable') : t('common.disable') })}
|
||||
aria-label={t('apiAccess.toggleAria', { remark: row.remark || row.name, action: row.disabled ? t('common.enable') : t('common.disable') })}
|
||||
/>
|
||||
<span className="switch-track" />
|
||||
</span>
|
||||
@@ -1077,7 +1088,7 @@ function ApiProviderDialog({
|
||||
&& visibleModelOptions.every((model) => selectedModelNames.has(model.name.toLowerCase()));
|
||||
|
||||
const updateTextField = (
|
||||
field: 'name' | 'apiKey' | 'remark' | 'baseUrl' | 'priority' | 'prefix' | 'headersText' | 'excludedModelsText' | 'testModel' | 'cloakMode' | 'cloakSensitiveWordsText',
|
||||
field: 'apiKey' | 'remark' | 'baseUrl' | 'priority' | 'prefix' | 'headersText' | 'excludedModelsText' | 'testModel' | 'cloakMode' | 'cloakSensitiveWordsText',
|
||||
value: string,
|
||||
) => {
|
||||
setDraft((current) => ({ ...current, [field]: value }));
|
||||
@@ -1239,18 +1250,15 @@ function ApiProviderDialog({
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
{definition.openAi ? (
|
||||
<label><span>{t('apiAccess.field.name')}</span><input autoFocus value={draft.name} onChange={(event) => updateTextField('name', event.currentTarget.value)} placeholder="openrouter" /></label>
|
||||
) : null}
|
||||
<label><span>{t('apiAccess.field.remark')}</span><input autoFocus={definition.openAi} value={draft.remark} maxLength={80} onChange={(event) => updateTextField('remark', event.currentTarget.value)} placeholder={t('apiAccess.remarkPlaceholder')} /></label>
|
||||
<label className={definition.openAi ? 'multiline-field' : undefined}>
|
||||
<span>{definition.openAi ? t('apiAccess.field.keysMany') : t('apiAccess.field.key')}</span>
|
||||
{definition.openAi ? (
|
||||
<textarea value={draft.apiKey} onChange={(event) => updateTextField('apiKey', event.currentTarget.value)} placeholder={'sk-...\nsk-...'} rows={3} />
|
||||
) : (
|
||||
<input autoFocus type="password" value={draft.apiKey} onChange={(event) => updateTextField('apiKey', event.currentTarget.value)} placeholder="sk-..." />
|
||||
<input autoFocus={!definition.openAi} type="password" value={draft.apiKey} onChange={(event) => updateTextField('apiKey', event.currentTarget.value)} placeholder="sk-..." />
|
||||
)}
|
||||
</label>
|
||||
<label><span>{t('apiAccess.field.remark')}</span><input value={draft.remark} maxLength={80} onChange={(event) => updateTextField('remark', event.currentTarget.value)} placeholder={t('apiAccess.remarkPlaceholder')} /></label>
|
||||
<label><span>Base URL</span><input value={draft.baseUrl} onChange={(event) => updateTextField('baseUrl', event.currentTarget.value)} placeholder={activeSection === 'codex-api-key' || activeSection === 'openai-compatibility' ? t('apiAccess.baseRequiredPlaceholder') : t('apiAccess.baseOptionalPlaceholder')} /></label>
|
||||
{activeCategory === 'deepseek' ? (
|
||||
<div className="provider-preset-summary">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import {
|
||||
allModelSelectionForDiscovery,
|
||||
applyProviderRemarkIdentity,
|
||||
applyProviderPreset,
|
||||
buildProviderRecord,
|
||||
createProviderDraft,
|
||||
@@ -43,6 +44,7 @@ describe('API 接入配置合并', () => {
|
||||
const result = buildProviderRecord('openai-compatibility', prepared);
|
||||
|
||||
expect(draft.name).toBe('DeepSeek');
|
||||
expect(draft.remark).toBe('DeepSeek');
|
||||
expect(draft.baseUrl).toBe(DEEPSEEK_BASE_URL);
|
||||
expect(draft.models).toEqual([]);
|
||||
expect(result).toMatchObject({
|
||||
@@ -66,6 +68,17 @@ describe('API 接入配置合并', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('OpenAI 兼容接入使用备注自动生成内核要求的名称', () => {
|
||||
const draft = applyProviderRemarkIdentity('openai-compatibility', {
|
||||
...createProviderDraft('openai-compatibility'),
|
||||
name: '不再使用的名称',
|
||||
remark: '生产环境',
|
||||
});
|
||||
|
||||
expect(draft.remark).toBe('生产环境');
|
||||
expect(draft.name).toBe('生产环境');
|
||||
});
|
||||
|
||||
it('DeepSeek 接入单独归类,不在 OpenAI 兼容列表重复显示', () => {
|
||||
const record = {
|
||||
name: 'custom-deepseek',
|
||||
|
||||
Reference in new issue
Block a user