mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-28 00:35:32 +08:00
feat: persist custom endpoints to settings.json
- Extend AppSettings to store custom endpoints for Claude and Codex - Add Tauri commands: get/add/remove/update custom endpoints - Update frontend API with endpoint persistence methods - Modify EndpointSpeedTest to load/save custom endpoints via API - Track endpoint last used time for future sorting/cleanup - Store endpoints per app type in settings.json instead of localStorage
This commit is contained in:
@@ -82,6 +82,51 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
||||
|
||||
const hasEndpoints = entries.length > 0;
|
||||
|
||||
// 加载保存的自定义端点
|
||||
useEffect(() => {
|
||||
const loadCustomEndpoints = async () => {
|
||||
try {
|
||||
const customEndpoints = await window.api.getCustomEndpoints(appType);
|
||||
const candidates: EndpointCandidate[] = customEndpoints.map((ep) => ({
|
||||
url: ep.url,
|
||||
isCustom: true,
|
||||
}));
|
||||
|
||||
setEntries((prev) => {
|
||||
const map = new Map<string, EndpointEntry>();
|
||||
|
||||
// 先添加现有端点
|
||||
prev.forEach((entry) => {
|
||||
map.set(entry.url, entry);
|
||||
});
|
||||
|
||||
// 合并自定义端点
|
||||
candidates.forEach((candidate) => {
|
||||
const sanitized = normalizeEndpointUrl(candidate.url);
|
||||
if (sanitized && !map.has(sanitized)) {
|
||||
map.set(sanitized, {
|
||||
id: randomId(),
|
||||
url: sanitized,
|
||||
isCustom: true,
|
||||
latency: null,
|
||||
status: undefined,
|
||||
error: null,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(map.values());
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("加载自定义端点失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
if (visible) {
|
||||
loadCustomEndpoints();
|
||||
}
|
||||
}, [appType, visible]);
|
||||
|
||||
useEffect(() => {
|
||||
setEntries((prev) => {
|
||||
const map = new Map<string, EndpointEntry>();
|
||||
@@ -135,57 +180,85 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
||||
});
|
||||
}, [entries]);
|
||||
|
||||
const handleAddEndpoint = useCallback(() => {
|
||||
const candidate = customUrl.trim();
|
||||
setAddError(null);
|
||||
const handleAddEndpoint = useCallback(
|
||||
async () => {
|
||||
const candidate = customUrl.trim();
|
||||
setAddError(null);
|
||||
|
||||
if (!candidate) {
|
||||
setAddError("请输入有效的 URL");
|
||||
return;
|
||||
}
|
||||
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(candidate);
|
||||
} catch {
|
||||
setAddError("URL 格式不正确");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!parsed.protocol.startsWith("http")) {
|
||||
setAddError("仅支持 HTTP/HTTPS");
|
||||
return;
|
||||
}
|
||||
|
||||
const sanitized = normalizeEndpointUrl(parsed.toString());
|
||||
|
||||
setEntries((prev) => {
|
||||
if (prev.some((entry) => entry.url === sanitized)) {
|
||||
setAddError("该地址已存在");
|
||||
return prev;
|
||||
if (!candidate) {
|
||||
setAddError("请输入有效的 URL");
|
||||
return;
|
||||
}
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
id: randomId(),
|
||||
url: sanitized,
|
||||
isCustom: true,
|
||||
latency: null,
|
||||
status: undefined,
|
||||
error: null,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
if (!normalizedSelected) {
|
||||
onChange(sanitized);
|
||||
}
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(candidate);
|
||||
} catch {
|
||||
setAddError("URL 格式不正确");
|
||||
return;
|
||||
}
|
||||
|
||||
setCustomUrl("");
|
||||
}, [customUrl, normalizedSelected, onChange]);
|
||||
if (!parsed.protocol.startsWith("http")) {
|
||||
setAddError("仅支持 HTTP/HTTPS");
|
||||
return;
|
||||
}
|
||||
|
||||
const sanitized = normalizeEndpointUrl(parsed.toString());
|
||||
|
||||
// 检查是否已存在
|
||||
setEntries((prev) => {
|
||||
if (prev.some((entry) => entry.url === sanitized)) {
|
||||
setAddError("该地址已存在");
|
||||
return prev;
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
|
||||
if (addError) return;
|
||||
|
||||
// 保存到后端
|
||||
try {
|
||||
await window.api.addCustomEndpoint(appType, sanitized);
|
||||
|
||||
// 更新本地状态
|
||||
setEntries((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: randomId(),
|
||||
url: sanitized,
|
||||
isCustom: true,
|
||||
latency: null,
|
||||
status: undefined,
|
||||
error: null,
|
||||
},
|
||||
]);
|
||||
|
||||
if (!normalizedSelected) {
|
||||
onChange(sanitized);
|
||||
}
|
||||
|
||||
setCustomUrl("");
|
||||
} catch (error) {
|
||||
setAddError("保存失败,请重试");
|
||||
console.error("添加自定义端点失败:", error);
|
||||
}
|
||||
},
|
||||
[customUrl, normalizedSelected, onChange, appType, addError],
|
||||
);
|
||||
|
||||
const handleRemoveEndpoint = useCallback(
|
||||
(entry: EndpointEntry) => {
|
||||
async (entry: EndpointEntry) => {
|
||||
// 如果是自定义端点,从后端删除
|
||||
if (entry.isCustom) {
|
||||
try {
|
||||
await window.api.removeCustomEndpoint(appType, entry.url);
|
||||
} catch (error) {
|
||||
console.error("删除自定义端点失败:", error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新本地状态
|
||||
setEntries((prev) => {
|
||||
const next = prev.filter((item) => item.id !== entry.id);
|
||||
if (entry.url === normalizedSelected) {
|
||||
@@ -195,7 +268,7 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
||||
return next;
|
||||
});
|
||||
},
|
||||
[normalizedSelected, onChange],
|
||||
[normalizedSelected, onChange, appType],
|
||||
);
|
||||
|
||||
const runSpeedTest = useCallback(async () => {
|
||||
@@ -261,11 +334,18 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
|
||||
}, [entries, autoSelect, appType, normalizedSelected, onChange]);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(url: string) => {
|
||||
async (url: string) => {
|
||||
if (!url || url === normalizedSelected) return;
|
||||
|
||||
// 更新最后使用时间(对自定义端点)
|
||||
const entry = entries.find((e) => e.url === url);
|
||||
if (entry?.isCustom) {
|
||||
await window.api.updateEndpointLastUsed(appType, url);
|
||||
}
|
||||
|
||||
onChange(url);
|
||||
},
|
||||
[normalizedSelected, onChange],
|
||||
[normalizedSelected, onChange, appType, entries],
|
||||
);
|
||||
|
||||
// 支持按下 ESC 关闭弹窗
|
||||
|
||||
+58
-1
@@ -1,6 +1,6 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen, UnlistenFn } from "@tauri-apps/api/event";
|
||||
import { Provider, Settings } from "../types";
|
||||
import { Provider, Settings, CustomEndpoint } from "../types";
|
||||
|
||||
// 应用类型
|
||||
export type AppType = "claude" | "codex";
|
||||
@@ -335,6 +335,63 @@ export const tauriAPI = {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 获取自定义端点列表
|
||||
getCustomEndpoints: async (appType: AppType): Promise<CustomEndpoint[]> => {
|
||||
try {
|
||||
return await invoke<CustomEndpoint[]>("get_custom_endpoints", {
|
||||
app_type: appType,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("获取自定义端点列表失败:", error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
// 添加自定义端点
|
||||
addCustomEndpoint: async (appType: AppType, url: string): Promise<void> => {
|
||||
try {
|
||||
await invoke("add_custom_endpoint", {
|
||||
app_type: appType,
|
||||
url,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("添加自定义端点失败:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 删除自定义端点
|
||||
removeCustomEndpoint: async (
|
||||
appType: AppType,
|
||||
url: string,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
await invoke("remove_custom_endpoint", {
|
||||
app_type: appType,
|
||||
url,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("删除自定义端点失败:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 更新端点最后使用时间
|
||||
updateEndpointLastUsed: async (
|
||||
appType: AppType,
|
||||
url: string,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
await invoke("update_endpoint_last_used", {
|
||||
app_type: appType,
|
||||
url,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("更新端点最后使用时间失败:", error);
|
||||
// 不抛出错误,因为这不是关键操作
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// 创建全局 API 对象,兼容现有代码
|
||||
|
||||
@@ -20,6 +20,13 @@ export interface AppConfig {
|
||||
current: string;
|
||||
}
|
||||
|
||||
// 自定义端点配置
|
||||
export interface CustomEndpoint {
|
||||
url: string;
|
||||
addedAt: number;
|
||||
lastUsed?: number;
|
||||
}
|
||||
|
||||
// 应用设置类型(用于 SettingsModal 与 Tauri API)
|
||||
export interface Settings {
|
||||
// 是否在系统托盘(macOS 菜单栏)显示图标
|
||||
@@ -32,4 +39,8 @@ export interface Settings {
|
||||
codexConfigDir?: string;
|
||||
// 首选语言(可选,默认中文)
|
||||
language?: "en" | "zh";
|
||||
// Claude 自定义端点列表
|
||||
customEndpointsClaude?: Record<string, CustomEndpoint>;
|
||||
// Codex 自定义端点列表
|
||||
customEndpointsCodex?: Record<string, CustomEndpoint>;
|
||||
}
|
||||
|
||||
Vendored
+6
-1
@@ -1,6 +1,6 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
import { Provider, Settings } from "./types";
|
||||
import { Provider, Settings, CustomEndpoint } from "./types";
|
||||
import { AppType } from "./lib/tauri-api";
|
||||
import type { UnlistenFn } from "@tauri-apps/api/event";
|
||||
|
||||
@@ -58,6 +58,11 @@ declare global {
|
||||
status?: number;
|
||||
error?: string;
|
||||
}>>;
|
||||
// 自定义端点管理
|
||||
getCustomEndpoints: (appType: AppType) => Promise<CustomEndpoint[]>;
|
||||
addCustomEndpoint: (appType: AppType, url: string) => Promise<void>;
|
||||
removeCustomEndpoint: (appType: AppType, url: string) => Promise<void>;
|
||||
updateEndpointLastUsed: (appType: AppType, url: string) => Promise<void>;
|
||||
};
|
||||
platform: {
|
||||
isMac: boolean;
|
||||
|
||||
Reference in New Issue
Block a user