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:
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
|
|||||||
54
src/App.tsx
54
src/App.tsx
@@ -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}`}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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")}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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: [
|
||||||
|
|||||||
@@ -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",
|
||||||
},
|
},
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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")) {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -150,7 +150,9 @@ export const tauriAPI = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 选择配置目录(可选默认路径)
|
// 选择配置目录(可选默认路径)
|
||||||
selectConfigDirectory: async (defaultPath?: string): Promise<string | null> => {
|
selectConfigDirectory: async (
|
||||||
|
defaultPath?: string,
|
||||||
|
): Promise<string | null> => {
|
||||||
try {
|
try {
|
||||||
// 后端参数为 snake_case:default_path
|
// 后端参数为 snake_case:default_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_case:default_name
|
// 后端参数为 snake_case:default_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);
|
||||||
|
|||||||
@@ -25,5 +25,5 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
|||||||
<UpdateProvider>
|
<UpdateProvider>
|
||||||
<App />
|
<App />
|
||||||
</UpdateProvider>
|
</UpdateProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
22
src/vite-env.d.ts
vendored
@@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user