Compare commits

...

5 Commits

8 changed files with 39 additions and 66 deletions
+1 -1
View File
@@ -50,7 +50,7 @@ function computeUnifiedDiff(original: string, modified: string): DiffResult {
let totalAdditions = 0;
let totalDeletions = 0;
const hunks: Hunk[] = chunks.map((chunk) => {
const hunks: Hunk[] = chunks.map((chunk: Chunk) => {
const lines: UnifiedLine[] = [];
const hasDel = chunk.fromA < chunk.toA;
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useRef, useState, type ChangeEvent, type RefObject } from 'react';
import { useTranslation } from 'react-i18next';
import { authFilesApi, isAuthFileInvalidJsonObjectError } from '@/services/api';
import { authFilesApi } from '@/services/api';
import { apiClient } from '@/services/api/client';
import { useNotificationStore } from '@/stores';
import type { AuthFileItem } from '@/types';
@@ -65,17 +65,6 @@ export function useAuthFilesData(options: UseAuthFilesDataOptions): UseAuthFiles
const fileInputRef = useRef<HTMLInputElement | null>(null);
const selectionCount = selectedFiles.size;
const resolveStatusUpdateErrorMessage = useCallback(
(err: unknown) => {
if (isAuthFileInvalidJsonObjectError(err)) {
return t('auth_files.prefix_proxy_invalid_json');
}
return err instanceof Error ? err.message : '';
},
[t]
);
const toggleSelect = useCallback((name: string) => {
setSelectedFiles((prev) => {
const next = new Set(prev);
@@ -393,9 +382,10 @@ export function useAuthFilesData(options: UseAuthFilesDataOptions): UseAuthFiles
setFiles((prev) => prev.map((f) => (f.name === name ? { ...f, disabled: nextDisabled } : f)));
try {
await authFilesApi.setStatus(name, nextDisabled);
await loadFiles();
void refreshKeyStats().catch(() => {});
const res = await authFilesApi.setStatus(name, nextDisabled);
setFiles((prev) =>
prev.map((f) => (f.name === name ? { ...f, disabled: res.disabled } : f))
);
showNotification(
enabled
? t('auth_files.status_enabled_success', { name })
@@ -403,7 +393,7 @@ export function useAuthFilesData(options: UseAuthFilesDataOptions): UseAuthFiles
'success'
);
} catch (err: unknown) {
const errorMessage = resolveStatusUpdateErrorMessage(err);
const errorMessage = err instanceof Error ? err.message : '';
setFiles((prev) =>
prev.map((f) => (f.name === name ? { ...f, disabled: previousDisabled } : f))
);
@@ -417,7 +407,7 @@ export function useAuthFilesData(options: UseAuthFilesDataOptions): UseAuthFiles
});
}
},
[loadFiles, refreshKeyStats, resolveStatusUpdateErrorMessage, showNotification, t]
[showNotification, t]
);
const batchSetStatus = useCallback(
@@ -427,9 +417,6 @@ export function useAuthFilesData(options: UseAuthFilesDataOptions): UseAuthFiles
const targetNames = new Set(uniqueNames);
const nextDisabled = !enabled;
const previousDisabled = new Map(
files.map((file) => [file.name, file.disabled === true] as const)
);
setFiles((prev) =>
prev.map((file) =>
@@ -443,26 +430,31 @@ export function useAuthFilesData(options: UseAuthFilesDataOptions): UseAuthFiles
let successCount = 0;
let failCount = 0;
const failedNames = new Set<string>();
const confirmedDisabled = new Map<string, boolean>();
results.forEach((result) => {
results.forEach((result, index) => {
const name = uniqueNames[index];
if (result.status === 'fulfilled') {
successCount++;
confirmedDisabled.set(name, result.value.disabled);
} else {
failCount++;
failedNames.add(name);
}
});
if (successCount > 0) {
await loadFiles();
void refreshKeyStats().catch(() => {});
} else {
setFiles((prev) =>
prev.map((file) => {
if (!targetNames.has(file.name)) return file;
return { ...file, disabled: previousDisabled.get(file.name) === true };
})
);
}
setFiles((prev) =>
prev.map((file) => {
if (failedNames.has(file.name)) {
return { ...file, disabled: !nextDisabled };
}
if (confirmedDisabled.has(file.name)) {
return { ...file, disabled: confirmedDisabled.get(file.name) };
}
return file;
})
);
if (failCount === 0) {
showNotification(t('auth_files.batch_status_success', { count: successCount }), 'success');
@@ -475,7 +467,7 @@ export function useAuthFilesData(options: UseAuthFilesDataOptions): UseAuthFiles
deselectAll();
},
[deselectAll, files, loadFiles, refreshKeyStats, showNotification, t]
[deselectAll, showNotification, t]
);
const batchDelete = useCallback(
+3 -5
View File
@@ -342,18 +342,17 @@
"vertex_add_modal_title": "Add Vertex API Configuration",
"vertex_add_modal_key_label": "API Key:",
"vertex_add_modal_key_placeholder": "Please enter Vertex API key",
"vertex_add_modal_url_label": "Base URL (Required):",
"vertex_add_modal_url_label": "Base URL:",
"vertex_add_modal_url_placeholder": "e.g.: https://example.com/api",
"vertex_add_modal_proxy_label": "Proxy URL (Optional):",
"vertex_add_modal_proxy_placeholder": "e.g.: socks5://proxy.example.com:1080",
"vertex_edit_modal_title": "Edit Vertex API Configuration",
"vertex_edit_modal_key_label": "API Key:",
"vertex_edit_modal_url_label": "Base URL (Required):",
"vertex_edit_modal_url_label": "Base URL:",
"vertex_edit_modal_proxy_label": "Proxy URL (Optional):",
"vertex_delete_confirm": "Are you sure you want to delete this Vertex configuration?",
"vertex_models_label": "Model aliases (alias required):",
"vertex_models_label": "Model aliases:",
"vertex_models_add_btn": "Add Mapping",
"vertex_models_hint": "Each alias needs both the original model and the alias.",
"vertex_models_count": "Alias count",
"ampcode_title": "Amp CLI Integration (ampcode)",
"ampcode_modal_title": "Configure Ampcode",
@@ -1373,7 +1372,6 @@
"vertex_config_added": "Vertex configuration added successfully",
"vertex_config_updated": "Vertex configuration updated successfully",
"vertex_config_deleted": "Vertex configuration deleted successfully",
"vertex_base_url_required": "Please enter the Vertex Base URL",
"config_enabled": "Configuration enabled",
"config_disabled": "Configuration disabled",
"field_required": "Required fields cannot be empty",
+3 -5
View File
@@ -342,18 +342,17 @@
"vertex_add_modal_title": "Добавление конфигурации Vertex API",
"vertex_add_modal_key_label": "API-ключ:",
"vertex_add_modal_key_placeholder": "Введите API-ключ Vertex",
"vertex_add_modal_url_label": "Базовый URL (обязательно):",
"vertex_add_modal_url_label": "Базовый URL:",
"vertex_add_modal_url_placeholder": "например: https://example.com/api",
"vertex_add_modal_proxy_label": "URL прокси (необязательно):",
"vertex_add_modal_proxy_placeholder": "например: socks5://proxy.example.com:1080",
"vertex_edit_modal_title": "Редактирование конфигурации Vertex API",
"vertex_edit_modal_key_label": "API-ключ:",
"vertex_edit_modal_url_label": "Базовый URL (обязательно):",
"vertex_edit_modal_url_label": "Базовый URL:",
"vertex_edit_modal_proxy_label": "URL прокси (необязательно):",
"vertex_delete_confirm": "Удалить эту конфигурацию Vertex?",
"vertex_models_label": "Псевдонимы моделей (требуется псевдоним):",
"vertex_models_label": "Псевдонимы моделей:",
"vertex_models_add_btn": "Добавить сопоставление",
"vertex_models_hint": "Каждому псевдониму требуются исходная модель и псевдоним.",
"vertex_models_count": "Количество псевдонимов",
"ampcode_title": "Интеграция Amp CLI (ampcode)",
"ampcode_modal_title": "Настройка Ampcode",
@@ -1378,7 +1377,6 @@
"vertex_config_added": "Конфигурация Vertex успешно добавлена",
"vertex_config_updated": "Конфигурация Vertex успешно обновлена",
"vertex_config_deleted": "Конфигурация Vertex успешно удалена",
"vertex_base_url_required": "Введите базовый URL Vertex",
"config_enabled": "Конфигурация включена",
"config_disabled": "Конфигурация выключена",
"field_required": "Обязательные поля не могут быть пустыми",
+3 -5
View File
@@ -342,18 +342,17 @@
"vertex_add_modal_title": "添加Vertex API配置",
"vertex_add_modal_key_label": "API密钥:",
"vertex_add_modal_key_placeholder": "请输入Vertex API密钥",
"vertex_add_modal_url_label": "Base URL (必填):",
"vertex_add_modal_url_label": "Base URL:",
"vertex_add_modal_url_placeholder": "例如: https://example.com/api",
"vertex_add_modal_proxy_label": "代理 URL (可选):",
"vertex_add_modal_proxy_placeholder": "例如: socks5://proxy.example.com:1080",
"vertex_edit_modal_title": "编辑Vertex API配置",
"vertex_edit_modal_key_label": "API密钥:",
"vertex_edit_modal_url_label": "Base URL (必填):",
"vertex_edit_modal_url_label": "Base URL:",
"vertex_edit_modal_proxy_label": "代理 URL (可选):",
"vertex_delete_confirm": "确定要删除这个Vertex配置吗?",
"vertex_models_label": "模型别名 (别名必填):",
"vertex_models_label": "模型别名:",
"vertex_models_add_btn": "添加映射",
"vertex_models_hint": "每条别名需要填写原模型与别名。",
"vertex_models_count": "别名数量",
"ampcode_title": "Amp CLI 集成 (ampcode)",
"ampcode_modal_title": "配置 Ampcode",
@@ -1373,7 +1372,6 @@
"vertex_config_added": "Vertex配置添加成功",
"vertex_config_updated": "Vertex配置更新成功",
"vertex_config_deleted": "Vertex配置删除成功",
"vertex_base_url_required": "请填写Vertex Base URL",
"config_enabled": "配置已启用",
"config_disabled": "配置已停用",
"field_required": "必填字段不能为空",
-5
View File
@@ -192,10 +192,6 @@ export function AiProvidersVertexEditPage() {
const trimmedBaseUrl = (form.baseUrl ?? '').trim();
const baseUrl = trimmedBaseUrl || undefined;
if (!baseUrl) {
showNotification(t('notification.vertex_base_url_required'), 'error');
return;
}
setSaving(true);
setError('');
@@ -347,7 +343,6 @@ export function AiProvidersVertexEditPage() {
removeButtonAriaLabel={t('common.delete')}
disabled={disableControls || saving}
/>
<div className="hint">{t('ai_providers.vertex_models_hint')}</div>
</div>
<div className="form-group">
<label>{t('ai_providers.excluded_models_label')}</label>
+2 -12
View File
@@ -132,18 +132,8 @@ const OAUTH_MODEL_ALIAS_ENDPOINT = '/oauth-model-alias';
export const authFilesApi = {
list: () => apiClient.get<AuthFilesResponse>('/auth-files'),
async setStatus(name: string, disabled: boolean): Promise<AuthFileStatusResponse> {
const json = await authFilesApi.downloadJsonObject(name);
if (disabled) {
json.disabled = true;
} else {
delete json.disabled;
}
await authFilesApi.saveJsonObject(name, json);
return { status: disabled ? 'disabled' : 'enabled', disabled };
},
setStatus: (name: string, disabled: boolean) =>
apiClient.patch<AuthFileStatusResponse>('/auth-files/status', { name, disabled }),
upload: (file: File) => {
const formData = new FormData();
+2
View File
@@ -10,6 +10,7 @@ import { STORAGE_KEY_AUTH } from '@/utils/constants';
import { secureStorage } from '@/services/storage/secureStorage';
import { apiClient } from '@/services/api/client';
import { useConfigStore } from './useConfigStore';
import { useUsageStatsStore } from './useUsageStatsStore';
import { detectApiBaseFromLocation, normalizeApiBase } from '@/utils/connection';
interface AuthStoreState extends AuthState {
@@ -136,6 +137,7 @@ export const useAuthStore = create<AuthStoreState>()(
logout: () => {
restoreSessionPromise = null;
useConfigStore.getState().clearCache();
useUsageStatsStore.getState().clearUsageStats();
set({
isAuthenticated: false,
apiBase: '',