refactor: improve error handling and code formatting

- Enhanced error messages in Rust backend to include file paths
- Improved provider switching error handling with detailed messages
- Added MCP button placeholder in UI (functionality TODO)
- Applied code formatting across frontend components
- Extended error notification duration to 6s for better readability
This commit is contained in:
Jason
2025-10-08 21:22:56 +08:00
parent 6afc436946
commit e9833e9a57
20 changed files with 335 additions and 237 deletions

View File

@@ -60,17 +60,20 @@ pub fn write_codex_live_atomic(auth: &Value, config_text_opt: Option<&str>) -> R
let config_path = get_codex_config_path(); let config_path = get_codex_config_path();
if let Some(parent) = auth_path.parent() { if let Some(parent) = auth_path.parent() {
std::fs::create_dir_all(parent).map_err(|e| format!("创建 Codex 目录失败: {}", e))?; std::fs::create_dir_all(parent)
.map_err(|e| format!("创建 Codex 目录失败: {}: {}", parent.display(), e))?;
} }
// 读取旧内容用于回滚 // 读取旧内容用于回滚
let old_auth = if auth_path.exists() { let old_auth = if auth_path.exists() {
Some(fs::read(&auth_path).map_err(|e| format!("读取旧 auth.json 失败: {}", e))?) Some(fs::read(&auth_path)
.map_err(|e| format!("读取旧 auth.json 失败: {}: {}", auth_path.display(), e))?)
} else { } else {
None None
}; };
let _old_config = if config_path.exists() { let _old_config = if config_path.exists() {
Some(fs::read(&config_path).map_err(|e| format!("读取旧 config.toml 失败: {}", e))?) Some(fs::read(&config_path)
.map_err(|e| format!("读取旧 config.toml 失败: {}: {}", config_path.display(), e))?)
} else { } else {
None None
}; };
@@ -81,8 +84,13 @@ pub fn write_codex_live_atomic(auth: &Value, config_text_opt: Option<&str>) -> R
None => String::new(), None => String::new(),
}; };
if !cfg_text.trim().is_empty() { if !cfg_text.trim().is_empty() {
toml::from_str::<toml::Table>(&cfg_text) toml::from_str::<toml::Table>(&cfg_text).map_err(|e| {
.map_err(|e| format!("config.toml 格式错误: {}", e))?; format!(
"config.toml 语法错误: {} (路径: {})",
e,
config_path.display()
)
})?;
} }
// 第一步:写 auth.json // 第一步:写 auth.json

View File

@@ -336,8 +336,13 @@ pub async fn switch_provider(
if auth_path.exists() { if auth_path.exists() {
let auth: Value = crate::config::read_json_file(&auth_path)?; let auth: Value = crate::config::read_json_file(&auth_path)?;
let config_str = if config_path.exists() { let config_str = if config_path.exists() {
std::fs::read_to_string(&config_path) std::fs::read_to_string(&config_path).map_err(|e| {
.map_err(|e| format!("读取 config.toml 失败: {}", e))? format!(
"读取 config.toml 失败: {}: {}",
config_path.display(),
e
)
})?
} else { } else {
String::new() String::new()
}; };

View File

@@ -118,16 +118,19 @@ pub fn read_json_file<T: for<'a> Deserialize<'a>>(path: &Path) -> Result<T, Stri
return Err(format!("文件不存在: {}", path.display())); return Err(format!("文件不存在: {}", path.display()));
} }
let content = fs::read_to_string(path).map_err(|e| format!("读取文件失败: {}", e))?; let content = fs::read_to_string(path)
.map_err(|e| format!("读取文件失败: {}: {}", path.display(), e))?;
serde_json::from_str(&content).map_err(|e| format!("解析 JSON 失败: {}", e)) serde_json::from_str(&content)
.map_err(|e| format!("解析 JSON 失败: {}: {}", path.display(), e))
} }
/// 写入 JSON 配置文件 /// 写入 JSON 配置文件
pub fn write_json_file<T: Serialize>(path: &Path, data: &T) -> Result<(), String> { pub fn write_json_file<T: Serialize>(path: &Path, data: &T) -> Result<(), String> {
// 确保目录存在 // 确保目录存在
if let Some(parent) = path.parent() { if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| format!("创建目录失败: {}", e))?; fs::create_dir_all(parent)
.map_err(|e| format!("创建目录失败: {}: {}", parent.display(), e))?;
} }
let json = let json =
@@ -139,7 +142,8 @@ pub fn write_json_file<T: Serialize>(path: &Path, data: &T) -> Result<(), String
/// 原子写入文本文件(用于 TOML/纯文本) /// 原子写入文本文件(用于 TOML/纯文本)
pub fn write_text_file(path: &Path, data: &str) -> Result<(), String> { pub fn write_text_file(path: &Path, data: &str) -> Result<(), String> {
if let Some(parent) = path.parent() { if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| format!("创建目录失败: {}", e))?; fs::create_dir_all(parent)
.map_err(|e| format!("创建目录失败: {}: {}", parent.display(), e))?;
} }
atomic_write(path, data.as_bytes()) atomic_write(path, data.as_bytes())
} }
@@ -147,7 +151,8 @@ pub fn write_text_file(path: &Path, data: &str) -> Result<(), String> {
/// 原子写入:写入临时文件后 rename 替换,避免半写状态 /// 原子写入:写入临时文件后 rename 替换,避免半写状态
pub fn atomic_write(path: &Path, data: &[u8]) -> Result<(), String> { pub fn atomic_write(path: &Path, data: &[u8]) -> Result<(), String> {
if let Some(parent) = path.parent() { if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| format!("创建目录失败: {}", e))?; fs::create_dir_all(parent)
.map_err(|e| format!("创建目录失败: {}: {}", parent.display(), e))?;
} }
let parent = path.parent().ok_or_else(|| "无效的路径".to_string())?; let parent = path.parent().ok_or_else(|| "无效的路径".to_string())?;
@@ -164,10 +169,12 @@ pub fn atomic_write(path: &Path, data: &[u8]) -> Result<(), String> {
tmp.push(format!("{}.tmp.{}", file_name, ts)); tmp.push(format!("{}.tmp.{}", file_name, ts));
{ {
let mut f = fs::File::create(&tmp).map_err(|e| format!("创建临时文件失败: {}", e))?; let mut f = fs::File::create(&tmp)
.map_err(|e| format!("创建临时文件失败: {}: {}", tmp.display(), e))?;
f.write_all(data) f.write_all(data)
.map_err(|e| format!("写入临时文件失败: {}", e))?; .map_err(|e| format!("写入临时文件失败: {}: {}", tmp.display(), e))?;
f.flush().map_err(|e| format!("刷新临时文件失败: {}", e))?; f.flush()
.map_err(|e| format!("刷新临时文件失败: {}: {}", tmp.display(), e))?;
} }
#[cfg(unix)] #[cfg(unix)]
@@ -185,12 +192,14 @@ pub fn atomic_write(path: &Path, data: &[u8]) -> Result<(), String> {
if path.exists() { if path.exists() {
let _ = fs::remove_file(path); let _ = fs::remove_file(path);
} }
fs::rename(&tmp, path).map_err(|e| format!("原子替换失败: {}", e))?; fs::rename(&tmp, path)
.map_err(|e| format!("原子替换失败: {} -> {}: {}", tmp.display(), path.display(), e))?;
} }
#[cfg(not(windows))] #[cfg(not(windows))]
{ {
fs::rename(&tmp, path).map_err(|e| format!("原子替换失败: {}", e))?; fs::rename(&tmp, path)
.map_err(|e| format!("原子替换失败: {} -> {}: {}", tmp.display(), path.display(), e))?;
} }
Ok(()) Ok(())
} }

View File

@@ -22,7 +22,7 @@ function App() {
const [currentProviderId, setCurrentProviderId] = useState<string>(""); const [currentProviderId, setCurrentProviderId] = useState<string>("");
const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [isAddModalOpen, setIsAddModalOpen] = useState(false);
const [editingProviderId, setEditingProviderId] = useState<string | null>( const [editingProviderId, setEditingProviderId] = useState<string | null>(
null null,
); );
const [notification, setNotification] = useState<{ const [notification, setNotification] = useState<{
message: string; message: string;
@@ -42,7 +42,7 @@ function App() {
const showNotification = ( const showNotification = (
message: string, message: string,
type: "success" | "error", type: "success" | "error",
duration = 3000 duration = 3000,
) => { ) => {
// 清除之前的定时器 // 清除之前的定时器
if (timeoutRef.current) { if (timeoutRef.current) {
@@ -208,24 +208,33 @@ function App() {
}; };
const handleSwitchProvider = async (id: string) => { const handleSwitchProvider = async (id: string) => {
const success = await window.api.switchProvider(id, activeApp); try {
if (success) { const success = await window.api.switchProvider(id, activeApp);
setCurrentProviderId(id); if (success) {
// 显示重启提示 setCurrentProviderId(id);
const appName = t(`apps.${activeApp}`); // 显示重启提示
showNotification( const appName = t(`apps.${activeApp}`);
t("notifications.switchSuccess", { appName }), showNotification(
"success", t("notifications.switchSuccess", { appName }),
2000 "success",
); 2000,
// 更新托盘菜单 );
await window.api.updateTrayMenu(); // 更新托盘菜单
await window.api.updateTrayMenu();
if (activeApp === "claude") { if (activeApp === "claude") {
await syncClaudePlugin(id, true); await syncClaudePlugin(id, true);
}
} else {
showNotification(t("notifications.switchFailed"), "error");
} }
} else { } catch (error) {
showNotification(t("notifications.switchFailed"), "error"); const detail = extractErrorMessage(error);
const msg = detail
? `${t("notifications.switchFailed")}: ${detail}`
: t("notifications.switchFailed");
// 详细错误展示稍长时间,便于用户阅读
showNotification(msg, "error", detail ? 6000 : 3000);
} }
}; };
@@ -297,6 +306,15 @@ function App() {
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<AppSwitcher activeApp={activeApp} onSwitch={setActiveApp} /> <AppSwitcher activeApp={activeApp} onSwitch={setActiveApp} />
<button
onClick={() => {
/* TODO: MCP 功能待实现 */
}}
className="inline-flex items-center gap-2 px-6 py-2 text-sm font-medium rounded-md transition-colors bg-emerald-500 text-white hover:bg-emerald-600 dark:bg-emerald-600 dark:hover:bg-emerald-700"
>
MCP
</button>
<button <button
onClick={() => setIsAddModalOpen(true)} onClick={() => setIsAddModalOpen(true)}
className={`inline-flex items-center gap-2 ${buttonStyles.primary}`} className={`inline-flex items-center gap-2 ${buttonStyles.primary}`}

View File

@@ -3,7 +3,7 @@ import { CheckCircle, Loader2, AlertCircle } from "lucide-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
interface ImportProgressModalProps { interface ImportProgressModalProps {
status: 'importing' | 'success' | 'error'; status: "importing" | "success" | "error";
message?: string; message?: string;
backupId?: string; backupId?: string;
onComplete?: () => void; onComplete?: () => void;
@@ -15,16 +15,20 @@ export function ImportProgressModal({
message, message,
backupId, backupId,
onComplete, onComplete,
onSuccess onSuccess,
}: ImportProgressModalProps) { }: ImportProgressModalProps) {
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(() => { useEffect(() => {
if (status === 'success') { if (status === "success") {
console.log('[ImportProgressModal] Success detected, starting 2 second countdown'); console.log(
"[ImportProgressModal] Success detected, starting 2 second countdown",
);
// 成功后等待2秒自动关闭并刷新数据 // 成功后等待2秒自动关闭并刷新数据
const timer = setTimeout(() => { const timer = setTimeout(() => {
console.log('[ImportProgressModal] 2 seconds elapsed, calling callbacks...'); console.log(
"[ImportProgressModal] 2 seconds elapsed, calling callbacks...",
);
if (onSuccess) { if (onSuccess) {
onSuccess(); onSuccess();
} }
@@ -34,7 +38,7 @@ export function ImportProgressModal({
}, 2000); }, 2000);
return () => { return () => {
console.log('[ImportProgressModal] Cleanup timer'); console.log("[ImportProgressModal] Cleanup timer");
clearTimeout(timer); clearTimeout(timer);
}; };
} }
@@ -46,7 +50,7 @@ export function ImportProgressModal({
<div className="relative bg-white dark:bg-gray-900 rounded-xl shadow-2xl p-8 max-w-md w-full mx-4"> <div className="relative bg-white dark:bg-gray-900 rounded-xl shadow-2xl p-8 max-w-md w-full mx-4">
<div className="flex flex-col items-center text-center"> <div className="flex flex-col items-center text-center">
{status === 'importing' && ( {status === "importing" && (
<> <>
<Loader2 className="w-12 h-12 text-blue-500 animate-spin mb-4" /> <Loader2 className="w-12 h-12 text-blue-500 animate-spin mb-4" />
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2"> <h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">
@@ -58,7 +62,7 @@ export function ImportProgressModal({
</> </>
)} )}
{status === 'success' && ( {status === "success" && (
<> <>
<CheckCircle className="w-12 h-12 text-green-500 mb-4" /> <CheckCircle className="w-12 h-12 text-green-500 mb-4" />
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2"> <h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">
@@ -75,7 +79,7 @@ export function ImportProgressModal({
</> </>
)} )}
{status === 'error' && ( {status === "error" && (
<> <>
<AlertCircle className="w-12 h-12 text-red-500 mb-4" /> <AlertCircle className="w-12 h-12 text-red-500 mb-4" />
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2"> <h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">

View File

@@ -53,7 +53,8 @@ const JsonEditor: React.FC<JsonEditorProps> = ({
} }
} catch (e) { } catch (e) {
// 简单处理JSON解析错误 // 简单处理JSON解析错误
const message = e instanceof SyntaxError ? e.message : t("jsonEditor.invalidJson"); const message =
e instanceof SyntaxError ? e.message : t("jsonEditor.invalidJson");
diagnostics.push({ diagnostics.push({
from: 0, from: 0,
to: doc.length, to: doc.length,

View File

@@ -42,11 +42,11 @@ const collectTemplatePaths = (
source: unknown, source: unknown,
templateKeys: string[], templateKeys: string[],
currentPath: TemplatePath = [], currentPath: TemplatePath = [],
acc: TemplatePath[] = [] acc: TemplatePath[] = [],
): TemplatePath[] => { ): TemplatePath[] => {
if (typeof source === "string") { if (typeof source === "string") {
const hasPlaceholder = templateKeys.some((key) => const hasPlaceholder = templateKeys.some((key) =>
source.includes(`\${${key}}`) source.includes(`\${${key}}`),
); );
if (hasPlaceholder) { if (hasPlaceholder) {
acc.push([...currentPath]); acc.push([...currentPath]);
@@ -56,14 +56,14 @@ const collectTemplatePaths = (
if (Array.isArray(source)) { if (Array.isArray(source)) {
source.forEach((item, index) => source.forEach((item, index) =>
collectTemplatePaths(item, templateKeys, [...currentPath, index], acc) collectTemplatePaths(item, templateKeys, [...currentPath, index], acc),
); );
return acc; return acc;
} }
if (source && typeof source === "object") { if (source && typeof source === "object") {
Object.entries(source).forEach(([key, value]) => Object.entries(source).forEach(([key, value]) =>
collectTemplatePaths(value, templateKeys, [...currentPath, key], acc) collectTemplatePaths(value, templateKeys, [...currentPath, key], acc),
); );
} }
@@ -82,7 +82,7 @@ const getValueAtPath = (source: any, path: TemplatePath) => {
const setValueAtPath = ( const setValueAtPath = (
target: any, target: any,
path: TemplatePath, path: TemplatePath,
value: unknown value: unknown,
): any => { ): any => {
if (path.length === 0) { if (path.length === 0) {
return value; return value;
@@ -120,7 +120,7 @@ const setValueAtPath = (
const applyTemplateValuesToConfigString = ( const applyTemplateValuesToConfigString = (
presetConfig: any, presetConfig: any,
currentConfigString: string, currentConfigString: string,
values: TemplateValueMap values: TemplateValueMap,
) => { ) => {
const replacedConfig = applyTemplateValues(presetConfig, values); const replacedConfig = applyTemplateValues(presetConfig, values);
const templateKeys = Object.keys(values); const templateKeys = Object.keys(values);
@@ -203,7 +203,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
: "", : "",
}); });
const [category, setCategory] = useState<ProviderCategory | undefined>( const [category, setCategory] = useState<ProviderCategory | undefined>(
initialData?.category initialData?.category,
); );
// Claude 模型配置状态 // Claude 模型配置状态
@@ -224,7 +224,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
useState(false); useState(false);
// 新建供应商:收集端点测速弹窗中的“自定义端点”,提交时一次性落盘到 meta.custom_endpoints // 新建供应商:收集端点测速弹窗中的“自定义端点”,提交时一次性落盘到 meta.custom_endpoints
const [draftCustomEndpoints, setDraftCustomEndpoints] = useState<string[]>( const [draftCustomEndpoints, setDraftCustomEndpoints] = useState<string[]>(
[] [],
); );
// 端点测速弹窗状态 // 端点测速弹窗状态
const [isEndpointModalOpen, setIsEndpointModalOpen] = useState(false); const [isEndpointModalOpen, setIsEndpointModalOpen] = useState(false);
@@ -232,7 +232,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
useState(false); useState(false);
// -1 表示自定义null 表示未选择,>= 0 表示预设索引 // -1 表示自定义null 表示未选择,>= 0 表示预设索引
const [selectedCodexPreset, setSelectedCodexPreset] = useState<number | null>( const [selectedCodexPreset, setSelectedCodexPreset] = useState<number | null>(
showPresets && isCodex ? -1 : null showPresets && isCodex ? -1 : null,
); );
const setCodexAuth = (value: string) => { const setCodexAuth = (value: string) => {
@@ -244,7 +244,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
setCodexConfigState((prev) => setCodexConfigState((prev) =>
typeof value === "function" typeof value === "function"
? (value as (input: string) => string)(prev) ? (value as (input: string) => string)(prev)
: value : value,
); );
}; };
@@ -305,7 +305,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
} }
try { try {
const stored = window.localStorage.getItem( const stored = window.localStorage.getItem(
CODEX_COMMON_CONFIG_STORAGE_KEY CODEX_COMMON_CONFIG_STORAGE_KEY,
); );
if (stored && stored.trim()) { if (stored && stored.trim()) {
return stored.trim(); return stored.trim();
@@ -322,7 +322,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
// -1 表示自定义null 表示未选择,>= 0 表示预设索引 // -1 表示自定义null 表示未选择,>= 0 表示预设索引
const [selectedPreset, setSelectedPreset] = useState<number | null>( const [selectedPreset, setSelectedPreset] = useState<number | null>(
showPresets ? -1 : null showPresets ? -1 : null,
); );
const [apiKey, setApiKey] = useState(""); const [apiKey, setApiKey] = useState("");
const [codexAuthError, setCodexAuthError] = useState(""); const [codexAuthError, setCodexAuthError] = useState("");
@@ -390,11 +390,11 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
const configString = JSON.stringify( const configString = JSON.stringify(
initialData.settingsConfig, initialData.settingsConfig,
null, null,
2 2,
); );
const hasCommon = hasCommonConfigSnippet( const hasCommon = hasCommonConfigSnippet(
configString, configString,
commonConfigSnippet commonConfigSnippet,
); );
setUseCommonConfig(hasCommon); setUseCommonConfig(hasCommon);
setSettingsConfigError(validateSettingsConfig(configString)); setSettingsConfigError(validateSettingsConfig(configString));
@@ -410,14 +410,14 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
if (config.env) { if (config.env) {
setClaudeModel(config.env.ANTHROPIC_MODEL || ""); setClaudeModel(config.env.ANTHROPIC_MODEL || "");
setClaudeSmallFastModel( setClaudeSmallFastModel(
config.env.ANTHROPIC_SMALL_FAST_MODEL || "" config.env.ANTHROPIC_SMALL_FAST_MODEL || "",
); );
setBaseUrl(config.env.ANTHROPIC_BASE_URL || ""); // 初始化基础 URL setBaseUrl(config.env.ANTHROPIC_BASE_URL || ""); // 初始化基础 URL
// 初始化 Kimi 模型选择 // 初始化 Kimi 模型选择
setKimiAnthropicModel(config.env.ANTHROPIC_MODEL || ""); setKimiAnthropicModel(config.env.ANTHROPIC_MODEL || "");
setKimiAnthropicSmallFastModel( setKimiAnthropicSmallFastModel(
config.env.ANTHROPIC_SMALL_FAST_MODEL || "" config.env.ANTHROPIC_SMALL_FAST_MODEL || "",
); );
} }
} }
@@ -425,7 +425,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
// Codex 初始化时检查 TOML 通用配置 // Codex 初始化时检查 TOML 通用配置
const hasCommon = hasTomlCommonConfigSnippet( const hasCommon = hasTomlCommonConfigSnippet(
codexConfig, codexConfig,
codexCommonConfigSnippet codexCommonConfigSnippet,
); );
setUseCodexCommonConfig(hasCommon); setUseCodexCommonConfig(hasCommon);
} }
@@ -445,7 +445,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
if (selectedPreset !== null && selectedPreset >= 0) { if (selectedPreset !== null && selectedPreset >= 0) {
const preset = providerPresets[selectedPreset]; const preset = providerPresets[selectedPreset];
setCategory( setCategory(
preset?.category || (preset?.isOfficial ? "official" : undefined) preset?.category || (preset?.isOfficial ? "official" : undefined),
); );
} else if (selectedPreset === -1) { } else if (selectedPreset === -1) {
setCategory("custom"); setCategory("custom");
@@ -454,7 +454,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
if (selectedCodexPreset !== null && selectedCodexPreset >= 0) { if (selectedCodexPreset !== null && selectedCodexPreset >= 0) {
const preset = codexProviderPresets[selectedCodexPreset]; const preset = codexProviderPresets[selectedCodexPreset];
setCategory( setCategory(
preset?.category || (preset?.isOfficial ? "official" : undefined) preset?.category || (preset?.isOfficial ? "official" : undefined),
); );
} else if (selectedCodexPreset === -1) { } else if (selectedCodexPreset === -1) {
setCategory("custom"); setCategory("custom");
@@ -506,7 +506,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
if (commonConfigSnippet.trim()) { if (commonConfigSnippet.trim()) {
window.localStorage.setItem( window.localStorage.setItem(
COMMON_CONFIG_STORAGE_KEY, COMMON_CONFIG_STORAGE_KEY,
commonConfigSnippet commonConfigSnippet,
); );
} else { } else {
window.localStorage.removeItem(COMMON_CONFIG_STORAGE_KEY); window.localStorage.removeItem(COMMON_CONFIG_STORAGE_KEY);
@@ -569,7 +569,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
} }
} else { } else {
const currentSettingsError = validateSettingsConfig( const currentSettingsError = validateSettingsConfig(
formData.settingsConfig formData.settingsConfig,
); );
setSettingsConfigError(currentSettingsError); setSettingsConfigError(currentSettingsError);
if (currentSettingsError) { if (currentSettingsError) {
@@ -634,7 +634,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
}; };
const handleChange = ( const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement> e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) => { ) => {
const { name, value } = e.target; const { name, value } = e.target;
@@ -664,7 +664,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
const { updatedConfig, error: snippetError } = updateCommonConfigSnippet( const { updatedConfig, error: snippetError } = updateCommonConfigSnippet(
formData.settingsConfig, formData.settingsConfig,
commonConfigSnippet, commonConfigSnippet,
checked checked,
); );
if (snippetError) { if (snippetError) {
@@ -697,7 +697,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
const { updatedConfig } = updateCommonConfigSnippet( const { updatedConfig } = updateCommonConfigSnippet(
formData.settingsConfig, formData.settingsConfig,
previousSnippet, previousSnippet,
false false,
); );
// 直接更新 formData不通过 handleChange // 直接更新 formData不通过 handleChange
updateSettingsConfigValue(updatedConfig); updateSettingsConfigValue(updatedConfig);
@@ -719,7 +719,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
const removeResult = updateCommonConfigSnippet( const removeResult = updateCommonConfigSnippet(
formData.settingsConfig, formData.settingsConfig,
previousSnippet, previousSnippet,
false false,
); );
if (removeResult.error) { if (removeResult.error) {
setCommonConfigError(removeResult.error); setCommonConfigError(removeResult.error);
@@ -731,7 +731,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
const addResult = updateCommonConfigSnippet( const addResult = updateCommonConfigSnippet(
removeResult.updatedConfig, removeResult.updatedConfig,
value, value,
true true,
); );
if (addResult.error) { if (addResult.error) {
@@ -775,11 +775,11 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
? config.editorValue ? config.editorValue
: (config.defaultValue ?? ""), : (config.defaultValue ?? ""),
}, },
]) ]),
); );
appliedSettingsConfig = applyTemplateValues( appliedSettingsConfig = applyTemplateValues(
preset.settingsConfig, preset.settingsConfig,
initialTemplateValues initialTemplateValues,
); );
} }
@@ -794,7 +794,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
}); });
setSettingsConfigError(validateSettingsConfig(configString)); setSettingsConfigError(validateSettingsConfig(configString));
setCategory( setCategory(
preset.category || (preset.isOfficial ? "official" : undefined) preset.category || (preset.isOfficial ? "official" : undefined),
); );
// 设置选中的预设 // 设置选中的预设
@@ -824,7 +824,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
if (preset.name?.includes("Kimi")) { if (preset.name?.includes("Kimi")) {
setKimiAnthropicModel(config.env.ANTHROPIC_MODEL || ""); setKimiAnthropicModel(config.env.ANTHROPIC_MODEL || "");
setKimiAnthropicSmallFastModel( setKimiAnthropicSmallFastModel(
config.env.ANTHROPIC_SMALL_FAST_MODEL || "" config.env.ANTHROPIC_SMALL_FAST_MODEL || "",
); );
} }
} else { } else {
@@ -872,7 +872,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
// Codex: 应用预设 // Codex: 应用预设
const applyCodexPreset = ( const applyCodexPreset = (
preset: (typeof codexProviderPresets)[0], preset: (typeof codexProviderPresets)[0],
index: number index: number,
) => { ) => {
const authString = JSON.stringify(preset.auth || {}, null, 2); const authString = JSON.stringify(preset.auth || {}, null, 2);
setCodexAuth(authString); setCodexAuth(authString);
@@ -890,7 +890,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
setSelectedCodexPreset(index); setSelectedCodexPreset(index);
setCategory( setCategory(
preset.category || (preset.isOfficial ? "official" : undefined) preset.category || (preset.isOfficial ? "official" : undefined),
); );
// 清空 API Key让用户重新输入 // 清空 API Key让用户重新输入
@@ -906,7 +906,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
const customConfig = generateThirdPartyConfig( const customConfig = generateThirdPartyConfig(
"custom", "custom",
"https://your-api-endpoint.com/v1", "https://your-api-endpoint.com/v1",
"gpt-5-codex" "gpt-5-codex",
); );
setFormData({ setFormData({
@@ -929,7 +929,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
const configString = setApiKeyInConfig( const configString = setApiKeyInConfig(
formData.settingsConfig, formData.settingsConfig,
key.trim(), key.trim(),
{ createIfMissing: selectedPreset !== null && selectedPreset !== -1 } { createIfMissing: selectedPreset !== null && selectedPreset !== -1 },
); );
// 更新表单配置 // 更新表单配置
@@ -1025,7 +1025,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
const { updatedConfig } = updateTomlCommonConfigSnippet( const { updatedConfig } = updateTomlCommonConfigSnippet(
codexConfig, codexConfig,
previousSnippet, previousSnippet,
false false,
); );
setCodexConfig(updatedConfig); setCodexConfig(updatedConfig);
setUseCodexCommonConfig(false); setUseCodexCommonConfig(false);
@@ -1038,12 +1038,12 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
const removeResult = updateTomlCommonConfigSnippet( const removeResult = updateTomlCommonConfigSnippet(
codexConfig, codexConfig,
previousSnippet, previousSnippet,
false false,
); );
const addResult = updateTomlCommonConfigSnippet( const addResult = updateTomlCommonConfigSnippet(
removeResult.updatedConfig, removeResult.updatedConfig,
sanitizedValue, sanitizedValue,
true true,
); );
if (addResult.error) { if (addResult.error) {
@@ -1065,7 +1065,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
try { try {
window.localStorage.setItem( window.localStorage.setItem(
CODEX_COMMON_CONFIG_STORAGE_KEY, CODEX_COMMON_CONFIG_STORAGE_KEY,
sanitizedValue sanitizedValue,
); );
} catch { } catch {
// ignore localStorage 写入失败 // ignore localStorage 写入失败
@@ -1078,7 +1078,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
if (!isUpdatingFromCodexCommonConfig.current) { if (!isUpdatingFromCodexCommonConfig.current) {
const hasCommon = hasTomlCommonConfigSnippet( const hasCommon = hasTomlCommonConfigSnippet(
value, value,
codexCommonConfigSnippet codexCommonConfigSnippet,
); );
setUseCodexCommonConfig(hasCommon); setUseCodexCommonConfig(hasCommon);
} }
@@ -1306,7 +1306,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
// 处理模型输入变化,自动更新 JSON 配置 // 处理模型输入变化,自动更新 JSON 配置
const handleModelChange = ( const handleModelChange = (
field: "ANTHROPIC_MODEL" | "ANTHROPIC_SMALL_FAST_MODEL", field: "ANTHROPIC_MODEL" | "ANTHROPIC_SMALL_FAST_MODEL",
value: string value: string,
) => { ) => {
if (field === "ANTHROPIC_MODEL") { if (field === "ANTHROPIC_MODEL") {
setClaudeModel(value); setClaudeModel(value);
@@ -1336,7 +1336,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
// Kimi 模型选择处理函数 // Kimi 模型选择处理函数
const handleKimiModelChange = ( const handleKimiModelChange = (
field: "ANTHROPIC_MODEL" | "ANTHROPIC_SMALL_FAST_MODEL", field: "ANTHROPIC_MODEL" | "ANTHROPIC_SMALL_FAST_MODEL",
value: string value: string,
) => { ) => {
if (field === "ANTHROPIC_MODEL") { if (field === "ANTHROPIC_MODEL") {
setKimiAnthropicModel(value); setKimiAnthropicModel(value);
@@ -1361,7 +1361,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
useEffect(() => { useEffect(() => {
if (!initialData) return; if (!initialData) return;
const parsedKey = getApiKeyFromConfig( const parsedKey = getApiKeyFromConfig(
JSON.stringify(initialData.settingsConfig) JSON.stringify(initialData.settingsConfig),
); );
if (parsedKey) setApiKey(parsedKey); if (parsedKey) setApiKey(parsedKey);
}, [initialData]); }, [initialData]);
@@ -1544,7 +1544,9 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
templateValueEntries.length > 0 && ( templateValueEntries.length > 0 && (
<div className="space-y-3"> <div className="space-y-3">
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100"> <h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100">
{t("providerForm.parameterConfig", { name: selectedTemplatePreset.name.trim() })} {t("providerForm.parameterConfig", {
name: selectedTemplatePreset.name.trim(),
})}
</h3> </h3>
<div className="space-y-4"> <div className="space-y-4">
{templateValueEntries.map(([key, config]) => ( {templateValueEntries.map(([key, config]) => (
@@ -1583,14 +1585,14 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
applyTemplateValuesToConfigString( applyTemplateValuesToConfigString(
selectedTemplatePreset.settingsConfig, selectedTemplatePreset.settingsConfig,
formData.settingsConfig, formData.settingsConfig,
nextValues nextValues,
); );
setFormData((prevForm) => ({ setFormData((prevForm) => ({
...prevForm, ...prevForm,
settingsConfig: configString, settingsConfig: configString,
})); }));
setSettingsConfigError( setSettingsConfigError(
validateSettingsConfig(configString) validateSettingsConfig(configString),
); );
} catch (err) { } catch (err) {
console.error("更新模板值失败:", err); console.error("更新模板值失败:", err);
@@ -1830,7 +1832,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
onChange={(e) => onChange={(e) =>
handleModelChange( handleModelChange(
"ANTHROPIC_SMALL_FAST_MODEL", "ANTHROPIC_SMALL_FAST_MODEL",
e.target.value e.target.value,
) )
} }
placeholder={t("providerForm.fastModelPlaceholder")} placeholder={t("providerForm.fastModelPlaceholder")}

View File

@@ -170,7 +170,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
trimmedBaseUrl, trimmedBaseUrl,
trimmedModel trimmedModel,
); );
onAuthChange(JSON.stringify(auth, null, 2)); onAuthChange(JSON.stringify(auth, null, 2));
@@ -208,7 +208,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
}; };
const handleTemplateInputKeyDown = ( const handleTemplateInputKeyDown = (
e: React.KeyboardEvent<HTMLInputElement> e: React.KeyboardEvent<HTMLInputElement>,
) => { ) => {
if (e.key === "Enter") { if (e.key === "Enter") {
e.preventDefault(); e.preventDefault();
@@ -509,7 +509,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
{JSON.stringify( {JSON.stringify(
generateThirdPartyAuth(templateApiKey), generateThirdPartyAuth(templateApiKey),
null, null,
2 2,
)} )}
</pre> </pre>
</div> </div>
@@ -526,7 +526,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
templateBaseUrl, templateBaseUrl,
templateModelName templateModelName,
) )
: ""} : ""}
</pre> </pre>

View File

@@ -210,81 +210,85 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
}); });
}, [entries]); }, [entries]);
const handleAddEndpoint = useCallback( const handleAddEndpoint = useCallback(async () => {
async () => { const candidate = customUrl.trim();
const candidate = customUrl.trim(); let errorMsg: string | null = null;
let errorMsg: string | null = null;
if (!candidate) { if (!candidate) {
errorMsg = t("endpointTest.enterValidUrl"); errorMsg = t("endpointTest.enterValidUrl");
} }
let parsed: URL | null = null; let parsed: URL | null = null;
if (!errorMsg) { if (!errorMsg) {
try {
parsed = new URL(candidate);
} catch {
errorMsg = t("endpointTest.invalidUrlFormat");
}
}
if (!errorMsg && parsed && !parsed.protocol.startsWith("http")) {
errorMsg = t("endpointTest.onlyHttps");
}
let sanitized = "";
if (!errorMsg && parsed) {
sanitized = normalizeEndpointUrl(parsed.toString());
// 使用当前 entries 做去重校验,避免依赖可能过期的 addError
const isDuplicate = entries.some((entry) => entry.url === sanitized);
if (isDuplicate) {
errorMsg = t("endpointTest.urlExists");
}
}
if (errorMsg) {
setAddError(errorMsg);
return;
}
setAddError(null);
// 保存到后端
try { try {
if (providerId) { parsed = new URL(candidate);
await window.api.addCustomEndpoint(appType, providerId, sanitized); } catch {
} errorMsg = t("endpointTest.invalidUrlFormat");
// 更新本地状态
setEntries((prev) => {
if (prev.some((e) => e.url === sanitized)) return prev;
return [
...prev,
{
id: randomId(),
url: sanitized,
isCustom: true,
latency: null,
status: undefined,
error: null,
},
];
});
if (!normalizedSelected) {
onChange(sanitized);
}
setCustomUrl("");
} catch (error) {
const message =
error instanceof Error ? error.message : String(error);
setAddError(message || t("endpointTest.saveFailed"));
console.error(t("endpointTest.addEndpointFailed"), error);
} }
}, }
[customUrl, entries, normalizedSelected, onChange, appType, providerId, t],
); if (!errorMsg && parsed && !parsed.protocol.startsWith("http")) {
errorMsg = t("endpointTest.onlyHttps");
}
let sanitized = "";
if (!errorMsg && parsed) {
sanitized = normalizeEndpointUrl(parsed.toString());
// 使用当前 entries 做去重校验,避免依赖可能过期的 addError
const isDuplicate = entries.some((entry) => entry.url === sanitized);
if (isDuplicate) {
errorMsg = t("endpointTest.urlExists");
}
}
if (errorMsg) {
setAddError(errorMsg);
return;
}
setAddError(null);
// 保存到后端
try {
if (providerId) {
await window.api.addCustomEndpoint(appType, providerId, sanitized);
}
// 更新本地状态
setEntries((prev) => {
if (prev.some((e) => e.url === sanitized)) return prev;
return [
...prev,
{
id: randomId(),
url: sanitized,
isCustom: true,
latency: null,
status: undefined,
error: null,
},
];
});
if (!normalizedSelected) {
onChange(sanitized);
}
setCustomUrl("");
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setAddError(message || t("endpointTest.saveFailed"));
console.error(t("endpointTest.addEndpointFailed"), error);
}
}, [
customUrl,
entries,
normalizedSelected,
onChange,
appType,
providerId,
t,
]);
const handleRemoveEndpoint = useCallback( const handleRemoveEndpoint = useCallback(
async (entry: EndpointEntry) => { async (entry: EndpointEntry) => {
@@ -358,7 +362,9 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
return { return {
...entry, ...entry,
latency: latency:
typeof match.latency === "number" ? Math.round(match.latency) : null, typeof match.latency === "number"
? Math.round(match.latency)
: null,
status: match.status, status: match.status,
error: match.error ?? null, error: match.error ?? null,
}; };
@@ -367,7 +373,9 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
if (autoSelect) { if (autoSelect) {
const successful = results const successful = results
.filter((item) => typeof item.latency === "number" && item.latency !== null) .filter(
(item) => typeof item.latency === "number" && item.latency !== null,
)
.sort((a, b) => (a.latency! || 0) - (b.latency! || 0)); .sort((a, b) => (a.latency! || 0) - (b.latency! || 0));
const best = successful[0]; const best = successful[0];
if (best && best.url && best.url !== normalizedSelected) { if (best && best.url && best.url !== normalizedSelected) {
@@ -376,7 +384,9 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
} }
} catch (error) { } catch (error) {
const message = const message =
error instanceof Error ? error.message : `${t("endpointTest.testFailed", { error: String(error) })}`; error instanceof Error
? error.message
: `${t("endpointTest.testFailed", { error: String(error) })}`;
setLastError(message); setLastError(message);
} finally { } finally {
setIsTesting(false); setIsTesting(false);
@@ -554,22 +564,26 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{latency !== null ? ( {latency !== null ? (
<div className="text-right"> <div className="text-right">
<div className={`font-mono text-sm font-medium ${ <div
latency < 300 className={`font-mono text-sm font-medium ${
? "text-green-600 dark:text-green-400" latency < 300
: latency < 500 ? "text-green-600 dark:text-green-400"
? "text-yellow-600 dark:text-yellow-400" : latency < 500
: latency < 800 ? "text-yellow-600 dark:text-yellow-400"
? "text-orange-600 dark:text-orange-400" : latency < 800
: "text-red-600 dark:text-red-400" ? "text-orange-600 dark:text-orange-400"
}`}> : "text-red-600 dark:text-red-400"
}`}
>
{latency}ms {latency}ms
</div> </div>
</div> </div>
) : isTesting ? ( ) : isTesting ? (
<Loader2 className="h-4 w-4 animate-spin text-gray-400" /> <Loader2 className="h-4 w-4 animate-spin text-gray-400" />
) : entry.error ? ( ) : entry.error ? (
<div className="text-xs text-gray-400">{t("endpointTest.failed")}</div> <div className="text-xs text-gray-400">
{t("endpointTest.failed")}
</div>
) : ( ) : (
<div className="text-xs text-gray-400"></div> <div className="text-xs text-gray-400"></div>
)} )}

View File

@@ -52,7 +52,11 @@ const KimiModelSelector: React.FC<KimiModelSelectorProps> = ({
}); });
if (!response.ok) { if (!response.ok) {
throw new Error(t("kimiSelector.requestFailed", { error: `${response.status} ${response.statusText}` })); throw new Error(
t("kimiSelector.requestFailed", {
error: `${response.status} ${response.statusText}`,
}),
);
} }
const data = await response.json(); const data = await response.json();
@@ -64,7 +68,11 @@ const KimiModelSelector: React.FC<KimiModelSelectorProps> = ({
} }
} catch (err) { } catch (err) {
console.error(t("kimiSelector.fetchModelsFailed") + ":", err); console.error(t("kimiSelector.fetchModelsFailed") + ":", err);
setError(err instanceof Error ? err.message : t("kimiSelector.fetchModelsFailed")); setError(
err instanceof Error
? err.message
: t("kimiSelector.fetchModelsFailed"),
);
} finally { } finally {
setLoading(false); setLoading(false);
} }

View File

@@ -16,7 +16,7 @@ interface ProviderListProps {
onNotify?: ( onNotify?: (
message: string, message: string,
type: "success" | "error", type: "success" | "error",
duration?: number duration?: number,
) => void; ) => void;
} }
@@ -154,7 +154,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
<div <div
key={provider.id} key={provider.id}
className={cn( className={cn(
isCurrent ? cardStyles.selected : cardStyles.interactive isCurrent ? cardStyles.selected : cardStyles.interactive,
)} )}
> >
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
@@ -167,7 +167,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
<div <div
className={cn( className={cn(
badgeStyles.success, badgeStyles.success,
!isCurrent && "invisible" !isCurrent && "invisible",
)} )}
> >
<CheckCircle2 size={12} /> <CheckCircle2 size={12} />
@@ -183,7 +183,9 @@ const ProviderList: React.FC<ProviderListProps> = ({
handleUrlClick(provider.websiteUrl!); handleUrlClick(provider.websiteUrl!);
}} }}
className="inline-flex items-center gap-1 text-blue-500 dark:text-blue-400 hover:opacity-90 transition-colors" className="inline-flex items-center gap-1 text-blue-500 dark:text-blue-400 hover:opacity-90 transition-colors"
title={t("providerForm.visitWebsite", { url: provider.websiteUrl })} title={t("providerForm.visitWebsite", {
url: provider.websiteUrl,
})}
> >
{provider.websiteUrl} {provider.websiteUrl}
</button> </button>
@@ -212,7 +214,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
"inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded-md transition-colors w-full whitespace-nowrap justify-center", "inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded-md transition-colors w-full whitespace-nowrap justify-center",
claudeApplied claudeApplied
? "border border-gray-300 text-gray-600 hover:border-red-300 hover:text-red-600 hover:bg-red-50 dark:border-gray-600 dark:text-gray-400 dark:hover:border-red-800 dark:hover:text-red-400 dark:hover:bg-red-900/20" ? "border border-gray-300 text-gray-600 hover:border-red-300 hover:text-red-600 hover:bg-red-50 dark:border-gray-600 dark:text-gray-400 dark:hover:border-red-800 dark:hover:text-red-400 dark:hover:bg-red-900/20"
: "border border-gray-300 text-gray-700 hover:border-green-300 hover:text-green-600 hover:bg-green-50 dark:border-gray-600 dark:text-gray-300 dark:hover:border-green-700 dark:hover:text-green-400 dark:hover:bg-green-900/20" : "border border-gray-300 text-gray-700 hover:border-green-300 hover:text-green-600 hover:bg-green-50 dark:border-gray-600 dark:text-gray-300 dark:hover:border-green-700 dark:hover:text-green-400 dark:hover:bg-green-900/20",
)} )}
title={ title={
claudeApplied claudeApplied
@@ -234,7 +236,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
"inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-md transition-colors w-[90px] justify-center whitespace-nowrap", "inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-md transition-colors w-[90px] justify-center whitespace-nowrap",
isCurrent isCurrent
? "bg-gray-100 text-gray-400 dark:bg-gray-800 dark:text-gray-500 cursor-not-allowed" ? "bg-gray-100 text-gray-400 dark:bg-gray-800 dark:text-gray-500 cursor-not-allowed"
: "bg-blue-500 text-white hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700" : "bg-blue-500 text-white hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700",
)} )}
> >
{isCurrent ? <Check size={14} /> : <Play size={14} />} {isCurrent ? <Check size={14} /> : <Play size={14} />}
@@ -256,7 +258,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
buttonStyles.icon, buttonStyles.icon,
isCurrent isCurrent
? "text-gray-400 cursor-not-allowed" ? "text-gray-400 cursor-not-allowed"
: "text-gray-500 hover:text-red-500 hover:bg-red-100 dark:text-gray-400 dark:hover:text-red-400 dark:hover:bg-red-500/10" : "text-gray-500 hover:text-red-500 hover:bg-red-100 dark:text-gray-400 dark:hover:text-red-400 dark:hover:bg-red-500/10",
)} )}
title={t("provider.deleteProvider")} title={t("provider.deleteProvider")}
> >

View File

@@ -26,7 +26,10 @@ interface SettingsModalProps {
onImportSuccess?: () => void | Promise<void>; onImportSuccess?: () => void | Promise<void>;
} }
export default function SettingsModal({ onClose, onImportSuccess }: SettingsModalProps) { export default function SettingsModal({
onClose,
onImportSuccess,
}: SettingsModalProps) {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const normalizeLanguage = (lang?: string | null): "zh" | "en" => const normalizeLanguage = (lang?: string | null): "zh" | "en" =>
@@ -67,10 +70,12 @@ export default function SettingsModal({ onClose, onImportSuccess }: SettingsModa
// 导入/导出相关状态 // 导入/导出相关状态
const [isImporting, setIsImporting] = useState(false); const [isImporting, setIsImporting] = useState(false);
const [importStatus, setImportStatus] = useState<'idle' | 'importing' | 'success' | 'error'>('idle'); const [importStatus, setImportStatus] = useState<
"idle" | "importing" | "success" | "error"
>("idle");
const [importError, setImportError] = useState<string>(""); const [importError, setImportError] = useState<string>("");
const [importBackupId, setImportBackupId] = useState<string>(""); const [importBackupId, setImportBackupId] = useState<string>("");
const [selectedImportFile, setSelectedImportFile] = useState<string>(''); const [selectedImportFile, setSelectedImportFile] = useState<string>("");
useEffect(() => { useEffect(() => {
loadSettings(); loadSettings();
@@ -340,7 +345,7 @@ export default function SettingsModal({ onClose, onImportSuccess }: SettingsModa
// 如果未知或为空,回退到 releases 首页 // 如果未知或为空,回退到 releases 首页
if (!targetVersion || targetVersion === unknownLabel) { if (!targetVersion || targetVersion === unknownLabel) {
await window.api.openExternal( await window.api.openExternal(
"https://github.com/farion1231/cc-switch/releases" "https://github.com/farion1231/cc-switch/releases",
); );
return; return;
} }
@@ -348,7 +353,7 @@ export default function SettingsModal({ onClose, onImportSuccess }: SettingsModa
? targetVersion ? targetVersion
: `v${targetVersion}`; : `v${targetVersion}`;
await window.api.openExternal( await window.api.openExternal(
`https://github.com/farion1231/cc-switch/releases/tag/${tag}` `https://github.com/farion1231/cc-switch/releases/tag/${tag}`,
); );
} catch (error) { } catch (error) {
console.error(t("console.openReleaseNotesFailed"), error); console.error(t("console.openReleaseNotesFailed"), error);
@@ -358,7 +363,7 @@ export default function SettingsModal({ onClose, onImportSuccess }: SettingsModa
// 导出配置处理函数 // 导出配置处理函数
const handleExportConfig = async () => { const handleExportConfig = async () => {
try { try {
const defaultName = `cc-switch-config-${new Date().toISOString().split('T')[0]}.json`; const defaultName = `cc-switch-config-${new Date().toISOString().split("T")[0]}.json`;
const filePath = await window.api.saveFileDialog(defaultName); const filePath = await window.api.saveFileDialog(defaultName);
if (!filePath) return; // 用户取消了 if (!filePath) return; // 用户取消了
@@ -380,8 +385,8 @@ export default function SettingsModal({ onClose, onImportSuccess }: SettingsModa
const filePath = await window.api.openFileDialog(); const filePath = await window.api.openFileDialog();
if (filePath) { if (filePath) {
setSelectedImportFile(filePath); setSelectedImportFile(filePath);
setImportStatus('idle'); // 重置状态 setImportStatus("idle"); // 重置状态
setImportError(''); setImportError("");
} }
} catch (error) { } catch (error) {
console.error(t("settings.selectFileFailed") + ":", error); console.error(t("settings.selectFileFailed") + ":", error);
@@ -394,22 +399,22 @@ export default function SettingsModal({ onClose, onImportSuccess }: SettingsModa
if (!selectedImportFile || isImporting) return; if (!selectedImportFile || isImporting) return;
setIsImporting(true); setIsImporting(true);
setImportStatus('importing'); setImportStatus("importing");
try { try {
const result = await window.api.importConfigFromFile(selectedImportFile); const result = await window.api.importConfigFromFile(selectedImportFile);
if (result.success) { if (result.success) {
setImportBackupId(result.backupId || ''); setImportBackupId(result.backupId || "");
setImportStatus('success'); setImportStatus("success");
// ImportProgressModal 会在2秒后触发数据刷新回调 // ImportProgressModal 会在2秒后触发数据刷新回调
} else { } else {
setImportError(result.message || t("settings.configCorrupted")); setImportError(result.message || t("settings.configCorrupted"));
setImportStatus('error'); setImportStatus("error");
} }
} catch (error) { } catch (error) {
setImportError(String(error)); setImportError(String(error));
setImportStatus('error'); setImportStatus("error");
} finally { } finally {
setIsImporting(false); setIsImporting(false);
} }
@@ -642,18 +647,22 @@ export default function SettingsModal({ onClose, onImportSuccess }: SettingsModa
disabled={!selectedImportFile || isImporting} disabled={!selectedImportFile || isImporting}
className={`px-3 py-2 text-xs font-medium rounded-lg transition-colors text-white ${ className={`px-3 py-2 text-xs font-medium rounded-lg transition-colors text-white ${
!selectedImportFile || isImporting !selectedImportFile || isImporting
? 'bg-gray-400 cursor-not-allowed' ? "bg-gray-400 cursor-not-allowed"
: 'bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700' : "bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700"
}`} }`}
> >
{isImporting ? t("settings.importing") : t("settings.import")} {isImporting
? t("settings.importing")
: t("settings.import")}
</button> </button>
</div> </div>
{/* 显示选择的文件 */} {/* 显示选择的文件 */}
{selectedImportFile && ( {selectedImportFile && (
<div className="text-xs text-gray-600 dark:text-gray-400 px-2 py-1 bg-gray-50 dark:bg-gray-900 rounded break-all"> <div className="text-xs text-gray-600 dark:text-gray-400 px-2 py-1 bg-gray-50 dark:bg-gray-900 rounded break-all">
{selectedImportFile.split('/').pop() || selectedImportFile.split('\\').pop() || selectedImportFile} {selectedImportFile.split("/").pop() ||
selectedImportFile.split("\\").pop() ||
selectedImportFile}
</div> </div>
)} )}
</div> </div>
@@ -757,15 +766,15 @@ export default function SettingsModal({ onClose, onImportSuccess }: SettingsModa
</div> </div>
{/* Import Progress Modal */} {/* Import Progress Modal */}
{importStatus !== 'idle' && ( {importStatus !== "idle" && (
<ImportProgressModal <ImportProgressModal
status={importStatus} status={importStatus}
message={importError} message={importError}
backupId={importBackupId} backupId={importBackupId}
onComplete={() => { onComplete={() => {
setImportStatus('idle'); setImportStatus("idle");
setImportError(''); setImportError("");
setSelectedImportFile(''); setSelectedImportFile("");
}} }}
onSuccess={() => { onSuccess={() => {
if (onImportSuccess) { if (onImportSuccess) {
@@ -773,7 +782,12 @@ export default function SettingsModal({ onClose, onImportSuccess }: SettingsModa
} }
void window.api void window.api
.updateTrayMenu() .updateTrayMenu()
.catch((error) => console.error("[SettingsModal] Failed to refresh tray menu", error)); .catch((error) =>
console.error(
"[SettingsModal] Failed to refresh tray menu",
error,
),
);
}} }}
/> />
)} )}

View File

@@ -32,7 +32,7 @@ export function generateThirdPartyAuth(apiKey: string): Record<string, any> {
export function generateThirdPartyConfig( export function generateThirdPartyConfig(
providerName: string, providerName: string,
baseUrl: string, baseUrl: string,
modelName = "gpt-5-codex" modelName = "gpt-5-codex",
): string { ): string {
// 清理供应商名称确保符合TOML键名规范 // 清理供应商名称确保符合TOML键名规范
const cleanProviderName = const cleanProviderName =
@@ -71,7 +71,7 @@ export const codexProviderPresets: CodexProviderPreset[] = [
config: generateThirdPartyConfig( config: generateThirdPartyConfig(
"packycode", "packycode",
"https://codex-api.packycode.com/v1", "https://codex-api.packycode.com/v1",
"gpt-5-codex" "gpt-5-codex",
), ),
// Codex 请求地址候选(用于地址管理/测速) // Codex 请求地址候选(用于地址管理/测速)
endpointCandidates: [ endpointCandidates: [

View File

@@ -110,7 +110,8 @@ export const providerPresets: ProviderPreset[] = [
apiKeyUrl: "https://console.streamlake.ai/console/wanqing/api-key", apiKeyUrl: "https://console.streamlake.ai/console/wanqing/api-key",
settingsConfig: { settingsConfig: {
env: { env: {
ANTHROPIC_BASE_URL: "https://vanchin.streamlake.ai/api/gateway/v1/endpoints/${ENDPOINT_ID}/claude-code-proxy", ANTHROPIC_BASE_URL:
"https://vanchin.streamlake.ai/api/gateway/v1/endpoints/${ENDPOINT_ID}/claude-code-proxy",
ANTHROPIC_AUTH_TOKEN: "", ANTHROPIC_AUTH_TOKEN: "",
ANTHROPIC_MODEL: "KAT-Coder", ANTHROPIC_MODEL: "KAT-Coder",
ANTHROPIC_SMALL_FAST_MODEL: "KAT-Coder", ANTHROPIC_SMALL_FAST_MODEL: "KAT-Coder",
@@ -146,5 +147,4 @@ export const providerPresets: ProviderPreset[] = [
], ],
category: "third_party", category: "third_party",
}, },
]; ];

View File

@@ -20,7 +20,8 @@ const getInitialLanguage = (): "zh" | "en" => {
const navigatorLang = const navigatorLang =
typeof navigator !== "undefined" typeof navigator !== "undefined"
? navigator.language?.toLowerCase() ?? navigator.languages?.[0]?.toLowerCase() ? (navigator.language?.toLowerCase() ??
navigator.languages?.[0]?.toLowerCase())
: undefined; : undefined;
if (navigatorLang?.startsWith("zh")) { if (navigatorLang?.startsWith("zh")) {

View File

@@ -22,9 +22,10 @@ export const isLinux = (): boolean => {
try { try {
const ua = navigator.userAgent || ""; const ua = navigator.userAgent || "";
// WebKitGTK/Chromium 在 Linux/Wayland/X11 下 UA 通常包含 Linux 或 X11 // WebKitGTK/Chromium 在 Linux/Wayland/X11 下 UA 通常包含 Linux 或 X11
return /linux|x11/i.test(ua) && !/android/i.test(ua) && !isMac() && !isWindows(); return (
/linux|x11/i.test(ua) && !/android/i.test(ua) && !isMac() && !isWindows()
);
} catch { } catch {
return false; return false;
} }
}; };

View File

@@ -150,7 +150,9 @@ export const tauriAPI = {
}, },
// 选择配置目录(可选默认路径) // 选择配置目录(可选默认路径)
selectConfigDirectory: async (defaultPath?: string): Promise<string | null> => { selectConfigDirectory: async (
defaultPath?: string,
): Promise<string | null> => {
try { try {
// 后端参数为 snake_casedefault_path // 后端参数为 snake_casedefault_path
return await invoke("pick_directory", { default_path: defaultPath }); return await invoke("pick_directory", { default_path: defaultPath });
@@ -384,7 +386,9 @@ export const tauriAPI = {
// theirs: 导入导出与文件对话框 // theirs: 导入导出与文件对话框
// 导出配置到文件 // 导出配置到文件
exportConfigToFile: async (filePath: string): Promise<{ exportConfigToFile: async (
filePath: string,
): Promise<{
success: boolean; success: boolean;
message: string; message: string;
filePath: string; filePath: string;
@@ -398,7 +402,9 @@ export const tauriAPI = {
}, },
// 从文件导入配置 // 从文件导入配置
importConfigFromFile: async (filePath: string): Promise<{ importConfigFromFile: async (
filePath: string,
): Promise<{
success: boolean; success: boolean;
message: string; message: string;
backupId?: string; backupId?: string;
@@ -415,7 +421,9 @@ export const tauriAPI = {
saveFileDialog: async (defaultName: string): Promise<string | null> => { saveFileDialog: async (defaultName: string): Promise<string | null> => {
try { try {
// 后端参数为 snake_casedefault_name // 后端参数为 snake_casedefault_name
const result = await invoke<string | null>("save_file_dialog", { default_name: defaultName }); const result = await invoke<string | null>("save_file_dialog", {
default_name: defaultName,
});
return result; return result;
} catch (error) { } catch (error) {
console.error("打开保存对话框失败:", error); console.error("打开保存对话框失败:", error);

View File

@@ -25,5 +25,5 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
<UpdateProvider> <UpdateProvider>
<App /> <App />
</UpdateProvider> </UpdateProvider>
</React.StrictMode> </React.StrictMode>,
); );

View File

@@ -178,16 +178,16 @@ export const getApiKeyFromConfig = (jsonString: string): string => {
// 模板变量替换 // 模板变量替换
export const applyTemplateValues = ( export const applyTemplateValues = (
config: any, config: any,
templateValues: Record<string, TemplateValueConfig> | undefined templateValues: Record<string, TemplateValueConfig> | undefined,
): any => { ): any => {
const resolvedValues = Object.fromEntries( const resolvedValues = Object.fromEntries(
Object.entries(templateValues ?? {}).map(([key, value]) => { Object.entries(templateValues ?? {}).map(([key, value]) => {
const resolvedValue = const resolvedValue =
value.editorValue !== undefined value.editorValue !== undefined
? value.editorValue ? value.editorValue
: value.defaultValue ?? ""; : (value.defaultValue ?? "");
return [key, resolvedValue]; return [key, resolvedValue];
}) }),
); );
const replaceInString = (str: string): string => { const replaceInString = (str: string): string => {
@@ -384,6 +384,7 @@ export const setCodexBaseUrl = (
return configText.replace(pattern, replacementLine); return configText.replace(pattern, replacementLine);
} }
const prefix = configText && !configText.endsWith("\n") ? `${configText}\n` : configText; const prefix =
configText && !configText.endsWith("\n") ? `${configText}\n` : configText;
return `${prefix}${replacementLine}\n`; return `${prefix}${replacementLine}\n`;
}; };

22
src/vite-env.d.ts vendored
View File

@@ -64,31 +64,33 @@ declare global {
testApiEndpoints: ( testApiEndpoints: (
urls: string[], urls: string[],
options?: { timeoutSecs?: number }, options?: { timeoutSecs?: number },
) => Promise<Array<{ ) => Promise<
url: string; Array<{
latency: number | null; url: string;
status?: number; latency: number | null;
error?: string; status?: number;
}>>; error?: string;
}>
>;
// 自定义端点管理 // 自定义端点管理
getCustomEndpoints: ( getCustomEndpoints: (
appType: AppType, appType: AppType,
providerId: string providerId: string,
) => Promise<CustomEndpoint[]>; ) => Promise<CustomEndpoint[]>;
addCustomEndpoint: ( addCustomEndpoint: (
appType: AppType, appType: AppType,
providerId: string, providerId: string,
url: string url: string,
) => Promise<void>; ) => Promise<void>;
removeCustomEndpoint: ( removeCustomEndpoint: (
appType: AppType, appType: AppType,
providerId: string, providerId: string,
url: string url: string,
) => Promise<void>; ) => Promise<void>;
updateEndpointLastUsed: ( updateEndpointLastUsed: (
appType: AppType, appType: AppType,
providerId: string, providerId: string,
url: string url: string,
) => Promise<void>; ) => Promise<void>;
}; };
platform: { platform: {