2025-09-17 10:36:28 +08:00
|
|
|
|
import React, { useState, useEffect, useRef } from "react";
|
2025-09-11 22:33:55 +08:00
|
|
|
|
import { Provider, ProviderCategory } from "../types";
|
2025-08-30 21:54:11 +08:00
|
|
|
|
import { AppType } from "../lib/tauri-api";
|
2025-08-21 22:20:57 +08:00
|
|
|
|
import {
|
2025-09-16 22:59:00 +08:00
|
|
|
|
updateCommonConfigSnippet,
|
|
|
|
|
|
hasCommonConfigSnippet,
|
2025-08-26 10:37:44 +08:00
|
|
|
|
getApiKeyFromConfig,
|
|
|
|
|
|
hasApiKeyField,
|
|
|
|
|
|
setApiKeyInConfig,
|
2025-09-17 10:44:30 +08:00
|
|
|
|
updateTomlCommonConfigSnippet,
|
|
|
|
|
|
hasTomlCommonConfigSnippet,
|
2025-09-18 08:35:09 +08:00
|
|
|
|
validateJsonConfig,
|
2025-08-21 22:20:57 +08:00
|
|
|
|
} from "../utils/providerConfigUtils";
|
|
|
|
|
|
import { providerPresets } from "../config/providerPresets";
|
2025-08-30 22:08:41 +08:00
|
|
|
|
import { codexProviderPresets } from "../config/codexProviderPresets";
|
2025-09-06 23:13:01 +08:00
|
|
|
|
import PresetSelector from "./ProviderForm/PresetSelector";
|
|
|
|
|
|
import ApiKeyInput from "./ProviderForm/ApiKeyInput";
|
|
|
|
|
|
import ClaudeConfigEditor from "./ProviderForm/ClaudeConfigEditor";
|
|
|
|
|
|
import CodexConfigEditor from "./ProviderForm/CodexConfigEditor";
|
|
|
|
|
|
import KimiModelSelector from "./ProviderForm/KimiModelSelector";
|
2025-09-13 17:04:46 +08:00
|
|
|
|
import { X, AlertCircle, Save } from "lucide-react";
|
2025-09-11 22:33:55 +08:00
|
|
|
|
// 分类仅用于控制少量交互(如官方禁用 API Key),不显示介绍组件
|
2025-08-08 15:03:38 +08:00
|
|
|
|
|
2025-09-16 22:59:00 +08:00
|
|
|
|
const COMMON_CONFIG_STORAGE_KEY = "cc-switch:common-config-snippet";
|
2025-09-17 10:44:30 +08:00
|
|
|
|
const CODEX_COMMON_CONFIG_STORAGE_KEY = "cc-switch:codex-common-config-snippet";
|
2025-09-16 22:59:00 +08:00
|
|
|
|
const DEFAULT_COMMON_CONFIG_SNIPPET = `{
|
|
|
|
|
|
"includeCoAuthoredBy": false
|
|
|
|
|
|
}`;
|
2025-09-17 10:44:30 +08:00
|
|
|
|
const DEFAULT_CODEX_COMMON_CONFIG_SNIPPET = `# Common Codex config
|
|
|
|
|
|
# Add your common TOML configuration here`;
|
2025-09-16 22:59:00 +08:00
|
|
|
|
|
2025-08-08 15:03:38 +08:00
|
|
|
|
interface ProviderFormProps {
|
2025-08-30 21:54:11 +08:00
|
|
|
|
appType?: AppType;
|
2025-08-21 22:20:57 +08:00
|
|
|
|
title: string;
|
|
|
|
|
|
submitText: string;
|
|
|
|
|
|
initialData?: Provider;
|
|
|
|
|
|
showPresets?: boolean;
|
|
|
|
|
|
onSubmit: (data: Omit<Provider, "id">) => void;
|
|
|
|
|
|
onClose: () => void;
|
2025-08-08 15:03:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const ProviderForm: React.FC<ProviderFormProps> = ({
|
2025-08-30 21:54:11 +08:00
|
|
|
|
appType = "claude",
|
2025-08-08 15:03:38 +08:00
|
|
|
|
title,
|
|
|
|
|
|
submitText,
|
|
|
|
|
|
initialData,
|
|
|
|
|
|
showPresets = false,
|
|
|
|
|
|
onSubmit,
|
2025-08-21 22:20:57 +08:00
|
|
|
|
onClose,
|
2025-08-08 15:03:38 +08:00
|
|
|
|
}) => {
|
2025-08-30 21:54:11 +08:00
|
|
|
|
// 对于 Codex,需要分离 auth 和 config
|
|
|
|
|
|
const isCodex = appType === "codex";
|
|
|
|
|
|
|
2025-08-08 15:03:38 +08:00
|
|
|
|
const [formData, setFormData] = useState({
|
2025-08-21 22:20:57 +08:00
|
|
|
|
name: initialData?.name || "",
|
|
|
|
|
|
websiteUrl: initialData?.websiteUrl || "",
|
|
|
|
|
|
settingsConfig: initialData
|
|
|
|
|
|
? JSON.stringify(initialData.settingsConfig, null, 2)
|
|
|
|
|
|
: "",
|
|
|
|
|
|
});
|
2025-09-11 22:33:55 +08:00
|
|
|
|
const [category, setCategory] = useState<ProviderCategory | undefined>(
|
|
|
|
|
|
initialData?.category,
|
|
|
|
|
|
);
|
2025-08-30 21:54:11 +08:00
|
|
|
|
|
2025-09-12 12:04:19 +08:00
|
|
|
|
// Claude 模型配置状态
|
|
|
|
|
|
const [claudeModel, setClaudeModel] = useState("");
|
|
|
|
|
|
const [claudeSmallFastModel, setClaudeSmallFastModel] = useState("");
|
2025-09-12 20:14:59 +08:00
|
|
|
|
const [baseUrl, setBaseUrl] = useState(""); // 新增:基础 URL 状态
|
2025-09-12 12:04:19 +08:00
|
|
|
|
|
2025-08-30 21:54:11 +08:00
|
|
|
|
// Codex 特有的状态
|
2025-09-17 22:39:45 +08:00
|
|
|
|
const [codexAuth, setCodexAuthState] = useState("");
|
|
|
|
|
|
const [codexConfig, setCodexConfigState] = useState("");
|
2025-08-30 22:08:41 +08:00
|
|
|
|
const [codexApiKey, setCodexApiKey] = useState("");
|
2025-09-03 15:58:02 +08:00
|
|
|
|
// -1 表示自定义,null 表示未选择,>= 0 表示预设索引
|
2025-08-30 22:08:41 +08:00
|
|
|
|
const [selectedCodexPreset, setSelectedCodexPreset] = useState<number | null>(
|
2025-09-06 23:13:01 +08:00
|
|
|
|
showPresets && isCodex ? -1 : null,
|
2025-08-30 22:08:41 +08:00
|
|
|
|
);
|
2025-08-30 21:54:11 +08:00
|
|
|
|
|
2025-09-17 22:39:45 +08:00
|
|
|
|
const setCodexAuth = (value: string) => {
|
|
|
|
|
|
setCodexAuthState(value);
|
2025-09-18 08:35:09 +08:00
|
|
|
|
setCodexAuthError(validateCodexAuth(value));
|
2025-09-17 22:39:45 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const setCodexConfig = (value: string) => {
|
|
|
|
|
|
setCodexConfigState(value);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const setCodexCommonConfigSnippet = (value: string) => {
|
|
|
|
|
|
setCodexCommonConfigSnippetState(value);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-08-30 21:54:11 +08:00
|
|
|
|
// 初始化 Codex 配置
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (isCodex && initialData) {
|
|
|
|
|
|
const config = initialData.settingsConfig;
|
|
|
|
|
|
if (typeof config === "object" && config !== null) {
|
|
|
|
|
|
setCodexAuth(JSON.stringify(config.auth || {}, null, 2));
|
|
|
|
|
|
setCodexConfig(config.config || "");
|
2025-08-30 22:08:41 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const auth = config.auth || {};
|
2025-08-31 16:39:38 +08:00
|
|
|
|
if (auth && typeof auth.OPENAI_API_KEY === "string") {
|
|
|
|
|
|
setCodexApiKey(auth.OPENAI_API_KEY);
|
2025-08-30 22:08:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// ignore
|
|
|
|
|
|
}
|
2025-08-30 21:54:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [isCodex, initialData]);
|
2025-09-06 23:13:01 +08:00
|
|
|
|
|
2025-08-21 22:20:57 +08:00
|
|
|
|
const [error, setError] = useState("");
|
2025-09-16 22:59:00 +08:00
|
|
|
|
const [useCommonConfig, setUseCommonConfig] = useState(false);
|
|
|
|
|
|
const [commonConfigSnippet, setCommonConfigSnippet] = useState<string>(() => {
|
|
|
|
|
|
if (typeof window === "undefined") {
|
|
|
|
|
|
return DEFAULT_COMMON_CONFIG_SNIPPET;
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
const stored = window.localStorage.getItem(COMMON_CONFIG_STORAGE_KEY);
|
|
|
|
|
|
if (stored && stored.trim()) {
|
|
|
|
|
|
return stored;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// ignore localStorage 读取失败
|
|
|
|
|
|
}
|
|
|
|
|
|
return DEFAULT_COMMON_CONFIG_SNIPPET;
|
|
|
|
|
|
});
|
|
|
|
|
|
const [commonConfigError, setCommonConfigError] = useState("");
|
2025-09-18 08:35:09 +08:00
|
|
|
|
const [settingsConfigError, setSettingsConfigError] = useState("");
|
2025-09-17 10:36:28 +08:00
|
|
|
|
// 用于跟踪是否正在通过通用配置更新
|
|
|
|
|
|
const isUpdatingFromCommonConfig = useRef(false);
|
2025-09-17 10:44:30 +08:00
|
|
|
|
|
|
|
|
|
|
// Codex 通用配置状态
|
|
|
|
|
|
const [useCodexCommonConfig, setUseCodexCommonConfig] = useState(false);
|
2025-09-17 22:39:45 +08:00
|
|
|
|
const [codexCommonConfigSnippet, setCodexCommonConfigSnippetState] = useState<string>(() => {
|
2025-09-17 10:44:30 +08:00
|
|
|
|
if (typeof window === "undefined") {
|
|
|
|
|
|
return DEFAULT_CODEX_COMMON_CONFIG_SNIPPET;
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
2025-09-17 22:39:45 +08:00
|
|
|
|
const stored = window.localStorage.getItem(
|
|
|
|
|
|
CODEX_COMMON_CONFIG_STORAGE_KEY,
|
|
|
|
|
|
);
|
2025-09-17 10:44:30 +08:00
|
|
|
|
if (stored && stored.trim()) {
|
|
|
|
|
|
return stored;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// ignore localStorage 读取失败
|
|
|
|
|
|
}
|
|
|
|
|
|
return DEFAULT_CODEX_COMMON_CONFIG_SNIPPET;
|
|
|
|
|
|
});
|
|
|
|
|
|
const [codexCommonConfigError, setCodexCommonConfigError] = useState("");
|
|
|
|
|
|
const isUpdatingFromCodexCommonConfig = useRef(false);
|
2025-09-03 15:58:02 +08:00
|
|
|
|
// -1 表示自定义,null 表示未选择,>= 0 表示预设索引
|
|
|
|
|
|
const [selectedPreset, setSelectedPreset] = useState<number | null>(
|
2025-09-06 23:13:01 +08:00
|
|
|
|
showPresets ? -1 : null,
|
2025-09-03 15:58:02 +08:00
|
|
|
|
);
|
2025-08-21 22:20:57 +08:00
|
|
|
|
const [apiKey, setApiKey] = useState("");
|
2025-09-18 08:35:09 +08:00
|
|
|
|
const [codexAuthError, setCodexAuthError] = useState("");
|
2025-08-08 15:03:38 +08:00
|
|
|
|
|
2025-09-06 23:13:01 +08:00
|
|
|
|
// Kimi 模型选择状态
|
|
|
|
|
|
const [kimiAnthropicModel, setKimiAnthropicModel] = useState("");
|
|
|
|
|
|
const [kimiAnthropicSmallFastModel, setKimiAnthropicSmallFastModel] =
|
|
|
|
|
|
useState("");
|
2025-09-13 15:36:43 +08:00
|
|
|
|
|
2025-09-18 08:35:09 +08:00
|
|
|
|
const validateSettingsConfig = (value: string): string => {
|
|
|
|
|
|
return validateJsonConfig(value, "配置内容");
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const validateCodexAuth = (value: string): string => {
|
|
|
|
|
|
if (!value.trim()) {
|
|
|
|
|
|
return "";
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
const parsed = JSON.parse(value);
|
|
|
|
|
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
|
|
|
|
return "auth.json 必须是 JSON 对象";
|
|
|
|
|
|
}
|
|
|
|
|
|
return "";
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return "auth.json 格式错误,请检查JSON语法";
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const updateSettingsConfigValue = (value: string) => {
|
|
|
|
|
|
setFormData((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
settingsConfig: value,
|
|
|
|
|
|
}));
|
|
|
|
|
|
setSettingsConfigError(validateSettingsConfig(value));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-12 15:20:49 +08:00
|
|
|
|
// 初始化自定义模式的默认配置
|
|
|
|
|
|
useEffect(() => {
|
2025-09-13 15:36:43 +08:00
|
|
|
|
if (
|
|
|
|
|
|
showPresets &&
|
|
|
|
|
|
selectedPreset === -1 &&
|
|
|
|
|
|
!initialData &&
|
|
|
|
|
|
formData.settingsConfig === ""
|
|
|
|
|
|
) {
|
2025-09-12 15:20:49 +08:00
|
|
|
|
// 设置自定义模板
|
|
|
|
|
|
const customTemplate = {
|
|
|
|
|
|
env: {
|
|
|
|
|
|
ANTHROPIC_BASE_URL: "https://your-api-endpoint.com",
|
|
|
|
|
|
ANTHROPIC_AUTH_TOKEN: "",
|
|
|
|
|
|
// 可选配置
|
|
|
|
|
|
// ANTHROPIC_MODEL: "your-model-name",
|
|
|
|
|
|
// ANTHROPIC_SMALL_FAST_MODEL: "your-fast-model-name"
|
2025-09-13 15:36:43 +08:00
|
|
|
|
},
|
2025-09-12 15:20:49 +08:00
|
|
|
|
};
|
2025-09-18 08:35:09 +08:00
|
|
|
|
const templateString = JSON.stringify(customTemplate, null, 2);
|
2025-09-13 15:36:43 +08:00
|
|
|
|
|
2025-09-18 08:35:09 +08:00
|
|
|
|
updateSettingsConfigValue(templateString);
|
2025-09-12 15:20:49 +08:00
|
|
|
|
setApiKey("");
|
|
|
|
|
|
}
|
|
|
|
|
|
}, []); // 只在组件挂载时执行一次
|
2025-09-06 23:13:01 +08:00
|
|
|
|
|
2025-09-16 22:59:00 +08:00
|
|
|
|
// 初始化时检查通用配置片段
|
2025-08-08 15:03:38 +08:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (initialData) {
|
2025-09-17 10:44:30 +08:00
|
|
|
|
if (!isCodex) {
|
|
|
|
|
|
const configString = JSON.stringify(initialData.settingsConfig, null, 2);
|
|
|
|
|
|
const hasCommon = hasCommonConfigSnippet(
|
|
|
|
|
|
configString,
|
|
|
|
|
|
commonConfigSnippet,
|
|
|
|
|
|
);
|
|
|
|
|
|
setUseCommonConfig(hasCommon);
|
2025-09-18 08:35:09 +08:00
|
|
|
|
setSettingsConfigError(validateSettingsConfig(configString));
|
2025-09-13 15:36:43 +08:00
|
|
|
|
|
2025-09-17 10:44:30 +08:00
|
|
|
|
// 初始化模型配置(编辑模式)
|
|
|
|
|
|
if (
|
|
|
|
|
|
initialData.settingsConfig &&
|
|
|
|
|
|
typeof initialData.settingsConfig === "object"
|
|
|
|
|
|
) {
|
|
|
|
|
|
const config = initialData.settingsConfig as {
|
|
|
|
|
|
env?: Record<string, any>;
|
|
|
|
|
|
};
|
|
|
|
|
|
if (config.env) {
|
|
|
|
|
|
setClaudeModel(config.env.ANTHROPIC_MODEL || "");
|
|
|
|
|
|
setClaudeSmallFastModel(config.env.ANTHROPIC_SMALL_FAST_MODEL || "");
|
|
|
|
|
|
setBaseUrl(config.env.ANTHROPIC_BASE_URL || ""); // 初始化基础 URL
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化 Kimi 模型选择
|
|
|
|
|
|
setKimiAnthropicModel(config.env.ANTHROPIC_MODEL || "");
|
|
|
|
|
|
setKimiAnthropicSmallFastModel(
|
|
|
|
|
|
config.env.ANTHROPIC_SMALL_FAST_MODEL || "",
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2025-09-06 23:13:01 +08:00
|
|
|
|
}
|
2025-09-17 10:44:30 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
// Codex 初始化时检查 TOML 通用配置
|
|
|
|
|
|
const hasCommon = hasTomlCommonConfigSnippet(
|
|
|
|
|
|
codexConfig,
|
|
|
|
|
|
codexCommonConfigSnippet,
|
|
|
|
|
|
);
|
|
|
|
|
|
setUseCodexCommonConfig(hasCommon);
|
2025-09-06 23:13:01 +08:00
|
|
|
|
}
|
2025-08-08 15:03:38 +08:00
|
|
|
|
}
|
2025-09-17 10:44:30 +08:00
|
|
|
|
}, [initialData, commonConfigSnippet, codexCommonConfigSnippet, isCodex, codexConfig]);
|
2025-08-08 15:03:38 +08:00
|
|
|
|
|
2025-09-11 22:33:55 +08:00
|
|
|
|
// 当选择预设变化时,同步类别
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!showPresets) return;
|
|
|
|
|
|
if (!isCodex) {
|
|
|
|
|
|
if (selectedPreset !== null && selectedPreset >= 0) {
|
|
|
|
|
|
const preset = providerPresets[selectedPreset];
|
2025-09-13 15:36:43 +08:00
|
|
|
|
setCategory(
|
|
|
|
|
|
preset?.category || (preset?.isOfficial ? "official" : undefined),
|
|
|
|
|
|
);
|
2025-09-11 22:33:55 +08:00
|
|
|
|
} else if (selectedPreset === -1) {
|
|
|
|
|
|
setCategory("custom");
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (selectedCodexPreset !== null && selectedCodexPreset >= 0) {
|
|
|
|
|
|
const preset = codexProviderPresets[selectedCodexPreset];
|
2025-09-13 15:36:43 +08:00
|
|
|
|
setCategory(
|
|
|
|
|
|
preset?.category || (preset?.isOfficial ? "official" : undefined),
|
|
|
|
|
|
);
|
2025-09-11 22:33:55 +08:00
|
|
|
|
} else if (selectedCodexPreset === -1) {
|
|
|
|
|
|
setCategory("custom");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [showPresets, isCodex, selectedPreset, selectedCodexPreset]);
|
|
|
|
|
|
|
2025-09-16 22:59:00 +08:00
|
|
|
|
// 同步本地存储的通用配置片段
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (typeof window === "undefined") return;
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (commonConfigSnippet.trim()) {
|
|
|
|
|
|
window.localStorage.setItem(
|
|
|
|
|
|
COMMON_CONFIG_STORAGE_KEY,
|
|
|
|
|
|
commonConfigSnippet,
|
|
|
|
|
|
);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
window.localStorage.removeItem(COMMON_CONFIG_STORAGE_KEY);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// ignore
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [commonConfigSnippet]);
|
|
|
|
|
|
|
2025-08-08 15:03:38 +08:00
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
2025-08-21 22:20:57 +08:00
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
setError("");
|
2025-08-08 15:03:38 +08:00
|
|
|
|
|
|
|
|
|
|
if (!formData.name) {
|
2025-08-21 22:20:57 +08:00
|
|
|
|
setError("请填写供应商名称");
|
|
|
|
|
|
return;
|
2025-08-08 15:03:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-21 22:20:57 +08:00
|
|
|
|
let settingsConfig: Record<string, any>;
|
|
|
|
|
|
|
2025-08-30 21:54:11 +08:00
|
|
|
|
if (isCodex) {
|
2025-09-18 08:35:09 +08:00
|
|
|
|
const currentAuthError = validateCodexAuth(codexAuth);
|
|
|
|
|
|
setCodexAuthError(currentAuthError);
|
|
|
|
|
|
if (currentAuthError) {
|
|
|
|
|
|
setError(currentAuthError);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-08-31 16:39:38 +08:00
|
|
|
|
// Codex: 仅要求 auth.json 必填;config.toml 可为空
|
|
|
|
|
|
if (!codexAuth.trim()) {
|
|
|
|
|
|
setError("请填写 auth.json 配置");
|
2025-08-30 21:54:11 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const authJson = JSON.parse(codexAuth);
|
2025-08-31 17:07:35 +08:00
|
|
|
|
|
|
|
|
|
|
// 非官方预设强制要求 OPENAI_API_KEY
|
|
|
|
|
|
if (selectedCodexPreset !== null) {
|
|
|
|
|
|
const preset = codexProviderPresets[selectedCodexPreset];
|
|
|
|
|
|
const isOfficial = Boolean(preset?.isOfficial);
|
|
|
|
|
|
if (!isOfficial) {
|
|
|
|
|
|
const key =
|
|
|
|
|
|
typeof authJson.OPENAI_API_KEY === "string"
|
|
|
|
|
|
? authJson.OPENAI_API_KEY.trim()
|
|
|
|
|
|
: "";
|
|
|
|
|
|
if (!key) {
|
|
|
|
|
|
setError("请填写 OPENAI_API_KEY");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-30 21:54:11 +08:00
|
|
|
|
settingsConfig = {
|
|
|
|
|
|
auth: authJson,
|
2025-08-31 16:39:38 +08:00
|
|
|
|
config: codexConfig ?? "",
|
2025-08-30 21:54:11 +08:00
|
|
|
|
};
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
setError("auth.json 格式错误,请检查JSON语法");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
2025-09-18 08:35:09 +08:00
|
|
|
|
const currentSettingsError = validateSettingsConfig(
|
|
|
|
|
|
formData.settingsConfig,
|
|
|
|
|
|
);
|
|
|
|
|
|
setSettingsConfigError(currentSettingsError);
|
|
|
|
|
|
if (currentSettingsError) {
|
|
|
|
|
|
setError(currentSettingsError);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-08-30 21:54:11 +08:00
|
|
|
|
// Claude: 原有逻辑
|
|
|
|
|
|
if (!formData.settingsConfig.trim()) {
|
|
|
|
|
|
setError("请填写配置内容");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
settingsConfig = JSON.parse(formData.settingsConfig);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
setError("配置JSON格式错误,请检查语法");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-08-08 15:03:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onSubmit({
|
|
|
|
|
|
name: formData.name,
|
|
|
|
|
|
websiteUrl: formData.websiteUrl,
|
2025-08-21 22:20:57 +08:00
|
|
|
|
settingsConfig,
|
2025-09-11 22:33:55 +08:00
|
|
|
|
// 仅在用户选择了预设或手动选择“自定义”时持久化分类
|
|
|
|
|
|
...(category ? { category } : {}),
|
2025-08-21 22:20:57 +08:00
|
|
|
|
});
|
|
|
|
|
|
};
|
2025-08-08 15:03:38 +08:00
|
|
|
|
|
|
|
|
|
|
const handleChange = (
|
2025-09-06 23:13:01 +08:00
|
|
|
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
2025-08-08 15:03:38 +08:00
|
|
|
|
) => {
|
2025-08-21 22:20:57 +08:00
|
|
|
|
const { name, value } = e.target;
|
|
|
|
|
|
|
|
|
|
|
|
if (name === "settingsConfig") {
|
2025-09-17 10:36:28 +08:00
|
|
|
|
// 只有在不是通过通用配置更新时,才检查并同步选择框状态
|
|
|
|
|
|
if (!isUpdatingFromCommonConfig.current) {
|
|
|
|
|
|
const hasCommon = hasCommonConfigSnippet(value, commonConfigSnippet);
|
|
|
|
|
|
setUseCommonConfig(hasCommon);
|
|
|
|
|
|
}
|
2025-08-21 22:20:57 +08:00
|
|
|
|
|
2025-08-26 10:37:44 +08:00
|
|
|
|
// 同步 API Key 输入框显示与值
|
|
|
|
|
|
const parsedKey = getApiKeyFromConfig(value);
|
|
|
|
|
|
setApiKey(parsedKey);
|
|
|
|
|
|
|
2025-09-06 09:30:09 +08:00
|
|
|
|
// 不再从 JSON 自动提取或覆盖官网地址,只更新配置内容
|
2025-09-18 08:35:09 +08:00
|
|
|
|
updateSettingsConfigValue(value);
|
2025-08-08 15:03:38 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
setFormData({
|
|
|
|
|
|
...formData,
|
|
|
|
|
|
[name]: value,
|
2025-08-21 22:20:57 +08:00
|
|
|
|
});
|
2025-08-08 15:03:38 +08:00
|
|
|
|
}
|
2025-08-21 22:20:57 +08:00
|
|
|
|
};
|
2025-08-08 15:03:38 +08:00
|
|
|
|
|
2025-09-16 22:59:00 +08:00
|
|
|
|
// 处理通用配置开关
|
|
|
|
|
|
const handleCommonConfigToggle = (checked: boolean) => {
|
|
|
|
|
|
const { updatedConfig, error: snippetError } = updateCommonConfigSnippet(
|
2025-08-21 22:20:57 +08:00
|
|
|
|
formData.settingsConfig,
|
2025-09-16 22:59:00 +08:00
|
|
|
|
commonConfigSnippet,
|
2025-09-06 23:13:01 +08:00
|
|
|
|
checked,
|
2025-08-21 22:20:57 +08:00
|
|
|
|
);
|
2025-09-16 22:59:00 +08:00
|
|
|
|
|
|
|
|
|
|
if (snippetError) {
|
|
|
|
|
|
setCommonConfigError(snippetError);
|
2025-09-18 08:35:09 +08:00
|
|
|
|
if (snippetError.includes("配置 JSON 解析失败")) {
|
|
|
|
|
|
setSettingsConfigError("配置JSON格式错误,请检查语法");
|
|
|
|
|
|
}
|
2025-09-16 22:59:00 +08:00
|
|
|
|
setUseCommonConfig(false);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setCommonConfigError("");
|
|
|
|
|
|
setUseCommonConfig(checked);
|
2025-09-17 10:36:28 +08:00
|
|
|
|
// 标记正在通过通用配置更新
|
|
|
|
|
|
isUpdatingFromCommonConfig.current = true;
|
2025-09-18 08:35:09 +08:00
|
|
|
|
updateSettingsConfigValue(updatedConfig);
|
2025-09-17 10:36:28 +08:00
|
|
|
|
// 在下一个事件循环中重置标记
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
isUpdatingFromCommonConfig.current = false;
|
|
|
|
|
|
}, 0);
|
2025-09-16 22:59:00 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleCommonConfigSnippetChange = (value: string) => {
|
|
|
|
|
|
const previousSnippet = commonConfigSnippet;
|
|
|
|
|
|
setCommonConfigSnippet(value);
|
|
|
|
|
|
|
|
|
|
|
|
if (!value.trim()) {
|
|
|
|
|
|
setCommonConfigError("");
|
|
|
|
|
|
if (useCommonConfig) {
|
|
|
|
|
|
const { updatedConfig } = updateCommonConfigSnippet(
|
|
|
|
|
|
formData.settingsConfig,
|
|
|
|
|
|
previousSnippet,
|
|
|
|
|
|
false,
|
|
|
|
|
|
);
|
2025-09-17 10:36:28 +08:00
|
|
|
|
// 直接更新 formData,不通过 handleChange
|
2025-09-18 08:35:09 +08:00
|
|
|
|
updateSettingsConfigValue(updatedConfig);
|
2025-09-16 22:59:00 +08:00
|
|
|
|
setUseCommonConfig(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证JSON格式
|
2025-09-18 08:35:09 +08:00
|
|
|
|
const validationError = validateJsonConfig(value, "通用配置片段");
|
|
|
|
|
|
if (validationError) {
|
|
|
|
|
|
setCommonConfigError(validationError);
|
|
|
|
|
|
} else {
|
2025-09-16 22:59:00 +08:00
|
|
|
|
setCommonConfigError("");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 若当前启用通用配置且格式正确,需要替换为最新片段
|
2025-09-18 08:35:09 +08:00
|
|
|
|
if (useCommonConfig && !validationError) {
|
2025-09-16 22:59:00 +08:00
|
|
|
|
const removeResult = updateCommonConfigSnippet(
|
|
|
|
|
|
formData.settingsConfig,
|
|
|
|
|
|
previousSnippet,
|
|
|
|
|
|
false,
|
|
|
|
|
|
);
|
2025-09-18 08:35:09 +08:00
|
|
|
|
if (removeResult.error) {
|
|
|
|
|
|
setCommonConfigError(removeResult.error);
|
|
|
|
|
|
if (removeResult.error.includes("配置 JSON 解析失败")) {
|
|
|
|
|
|
setSettingsConfigError("配置JSON格式错误,请检查语法");
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-09-16 22:59:00 +08:00
|
|
|
|
const addResult = updateCommonConfigSnippet(
|
|
|
|
|
|
removeResult.updatedConfig,
|
|
|
|
|
|
value,
|
|
|
|
|
|
true,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (addResult.error) {
|
|
|
|
|
|
setCommonConfigError(addResult.error);
|
2025-09-18 08:35:09 +08:00
|
|
|
|
if (addResult.error.includes("配置 JSON 解析失败")) {
|
|
|
|
|
|
setSettingsConfigError("配置JSON格式错误,请检查语法");
|
|
|
|
|
|
}
|
2025-09-16 22:59:00 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-17 10:36:28 +08:00
|
|
|
|
// 标记正在通过通用配置更新,避免触发状态检查
|
|
|
|
|
|
isUpdatingFromCommonConfig.current = true;
|
2025-09-18 08:35:09 +08:00
|
|
|
|
updateSettingsConfigValue(addResult.updatedConfig);
|
2025-09-17 10:36:28 +08:00
|
|
|
|
// 在下一个事件循环中重置标记
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
isUpdatingFromCommonConfig.current = false;
|
|
|
|
|
|
}, 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 保存通用配置到 localStorage
|
2025-09-18 08:35:09 +08:00
|
|
|
|
if (!validationError && typeof window !== "undefined") {
|
2025-09-17 10:36:28 +08:00
|
|
|
|
try {
|
|
|
|
|
|
window.localStorage.setItem(COMMON_CONFIG_STORAGE_KEY, value);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// ignore localStorage 写入失败
|
|
|
|
|
|
}
|
2025-09-16 22:59:00 +08:00
|
|
|
|
}
|
2025-08-21 22:20:57 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const applyPreset = (preset: (typeof providerPresets)[0], index: number) => {
|
|
|
|
|
|
const configString = JSON.stringify(preset.settingsConfig, null, 2);
|
2025-08-08 15:03:38 +08:00
|
|
|
|
|
|
|
|
|
|
setFormData({
|
|
|
|
|
|
name: preset.name,
|
|
|
|
|
|
websiteUrl: preset.websiteUrl,
|
2025-08-21 22:20:57 +08:00
|
|
|
|
settingsConfig: configString,
|
|
|
|
|
|
});
|
2025-09-18 08:35:09 +08:00
|
|
|
|
setSettingsConfigError(validateSettingsConfig(configString));
|
2025-09-13 15:36:43 +08:00
|
|
|
|
setCategory(
|
|
|
|
|
|
preset.category || (preset.isOfficial ? "official" : undefined),
|
|
|
|
|
|
);
|
2025-08-21 22:20:57 +08:00
|
|
|
|
|
2025-08-21 21:46:48 +08:00
|
|
|
|
// 设置选中的预设
|
2025-08-21 22:20:57 +08:00
|
|
|
|
setSelectedPreset(index);
|
|
|
|
|
|
|
|
|
|
|
|
// 清空 API Key 输入框,让用户重新输入
|
|
|
|
|
|
setApiKey("");
|
2025-09-12 20:14:59 +08:00
|
|
|
|
setBaseUrl(""); // 清空基础 URL
|
2025-08-21 22:20:57 +08:00
|
|
|
|
|
2025-09-16 22:59:00 +08:00
|
|
|
|
// 同步通用配置状态
|
|
|
|
|
|
const hasCommon = hasCommonConfigSnippet(
|
|
|
|
|
|
configString,
|
|
|
|
|
|
commonConfigSnippet,
|
|
|
|
|
|
);
|
|
|
|
|
|
setUseCommonConfig(hasCommon);
|
|
|
|
|
|
setCommonConfigError("");
|
2025-09-06 23:13:01 +08:00
|
|
|
|
|
2025-09-12 12:04:19 +08:00
|
|
|
|
// 如果预设包含模型配置,初始化模型输入框
|
|
|
|
|
|
if (preset.settingsConfig && typeof preset.settingsConfig === "object") {
|
2025-09-06 23:13:01 +08:00
|
|
|
|
const config = preset.settingsConfig as { env?: Record<string, any> };
|
|
|
|
|
|
if (config.env) {
|
2025-09-12 12:04:19 +08:00
|
|
|
|
setClaudeModel(config.env.ANTHROPIC_MODEL || "");
|
|
|
|
|
|
setClaudeSmallFastModel(config.env.ANTHROPIC_SMALL_FAST_MODEL || "");
|
2025-09-13 15:36:43 +08:00
|
|
|
|
|
2025-09-12 12:04:19 +08:00
|
|
|
|
// 如果是 Kimi 预设,同步 Kimi 模型选择
|
|
|
|
|
|
if (preset.name?.includes("Kimi")) {
|
|
|
|
|
|
setKimiAnthropicModel(config.env.ANTHROPIC_MODEL || "");
|
2025-09-13 15:36:43 +08:00
|
|
|
|
setKimiAnthropicSmallFastModel(
|
|
|
|
|
|
config.env.ANTHROPIC_SMALL_FAST_MODEL || "",
|
|
|
|
|
|
);
|
2025-09-12 12:04:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setClaudeModel("");
|
|
|
|
|
|
setClaudeSmallFastModel("");
|
2025-09-06 23:13:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-21 22:20:57 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-03 15:58:02 +08:00
|
|
|
|
// 处理点击自定义按钮
|
|
|
|
|
|
const handleCustomClick = () => {
|
|
|
|
|
|
setSelectedPreset(-1);
|
2025-09-13 15:36:43 +08:00
|
|
|
|
|
2025-09-12 12:04:19 +08:00
|
|
|
|
// 设置自定义模板
|
|
|
|
|
|
const customTemplate = {
|
|
|
|
|
|
env: {
|
|
|
|
|
|
ANTHROPIC_BASE_URL: "https://your-api-endpoint.com",
|
2025-09-12 15:20:49 +08:00
|
|
|
|
ANTHROPIC_AUTH_TOKEN: "",
|
2025-09-12 12:04:19 +08:00
|
|
|
|
// 可选配置
|
|
|
|
|
|
// ANTHROPIC_MODEL: "your-model-name",
|
|
|
|
|
|
// ANTHROPIC_SMALL_FAST_MODEL: "your-fast-model-name"
|
2025-09-13 15:36:43 +08:00
|
|
|
|
},
|
2025-09-12 12:04:19 +08:00
|
|
|
|
};
|
2025-09-18 08:35:09 +08:00
|
|
|
|
const templateString = JSON.stringify(customTemplate, null, 2);
|
2025-09-13 15:36:43 +08:00
|
|
|
|
|
2025-09-03 15:58:02 +08:00
|
|
|
|
setFormData({
|
|
|
|
|
|
name: "",
|
|
|
|
|
|
websiteUrl: "",
|
2025-09-18 08:35:09 +08:00
|
|
|
|
settingsConfig: templateString,
|
2025-09-03 15:58:02 +08:00
|
|
|
|
});
|
2025-09-18 08:35:09 +08:00
|
|
|
|
setSettingsConfigError(validateSettingsConfig(templateString));
|
2025-09-12 15:20:49 +08:00
|
|
|
|
setApiKey("");
|
2025-09-12 20:14:59 +08:00
|
|
|
|
setBaseUrl("https://your-api-endpoint.com"); // 设置默认的基础 URL
|
2025-09-16 22:59:00 +08:00
|
|
|
|
setUseCommonConfig(false);
|
|
|
|
|
|
setCommonConfigError("");
|
2025-09-12 12:04:19 +08:00
|
|
|
|
setClaudeModel("");
|
|
|
|
|
|
setClaudeSmallFastModel("");
|
2025-09-06 23:13:01 +08:00
|
|
|
|
setKimiAnthropicModel("");
|
|
|
|
|
|
setKimiAnthropicSmallFastModel("");
|
2025-09-11 22:33:55 +08:00
|
|
|
|
setCategory("custom");
|
2025-09-03 15:58:02 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-08-30 22:08:41 +08:00
|
|
|
|
// Codex: 应用预设
|
|
|
|
|
|
const applyCodexPreset = (
|
|
|
|
|
|
preset: (typeof codexProviderPresets)[0],
|
2025-09-06 23:13:01 +08:00
|
|
|
|
index: number,
|
2025-08-30 22:08:41 +08:00
|
|
|
|
) => {
|
|
|
|
|
|
const authString = JSON.stringify(preset.auth || {}, null, 2);
|
|
|
|
|
|
setCodexAuth(authString);
|
|
|
|
|
|
setCodexConfig(preset.config || "");
|
|
|
|
|
|
|
2025-09-06 23:13:01 +08:00
|
|
|
|
setFormData((prev) => ({
|
|
|
|
|
|
...prev,
|
2025-08-30 22:08:41 +08:00
|
|
|
|
name: preset.name,
|
|
|
|
|
|
websiteUrl: preset.websiteUrl,
|
2025-09-06 23:13:01 +08:00
|
|
|
|
}));
|
2025-08-30 22:08:41 +08:00
|
|
|
|
|
|
|
|
|
|
setSelectedCodexPreset(index);
|
2025-09-13 15:36:43 +08:00
|
|
|
|
setCategory(
|
|
|
|
|
|
preset.category || (preset.isOfficial ? "official" : undefined),
|
|
|
|
|
|
);
|
2025-08-30 22:08:41 +08:00
|
|
|
|
|
2025-08-30 22:09:19 +08:00
|
|
|
|
// 清空 API Key,让用户重新输入
|
|
|
|
|
|
setCodexApiKey("");
|
2025-08-30 22:08:41 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-03 15:58:02 +08:00
|
|
|
|
// Codex: 处理点击自定义按钮
|
|
|
|
|
|
const handleCodexCustomClick = () => {
|
|
|
|
|
|
setSelectedCodexPreset(-1);
|
|
|
|
|
|
setFormData({
|
|
|
|
|
|
name: "",
|
|
|
|
|
|
websiteUrl: "",
|
|
|
|
|
|
settingsConfig: "",
|
|
|
|
|
|
});
|
2025-09-18 08:35:09 +08:00
|
|
|
|
setSettingsConfigError(validateSettingsConfig(""));
|
2025-09-03 15:58:02 +08:00
|
|
|
|
setCodexAuth("");
|
|
|
|
|
|
setCodexConfig("");
|
|
|
|
|
|
setCodexApiKey("");
|
2025-09-11 22:33:55 +08:00
|
|
|
|
setCategory("custom");
|
2025-09-03 15:58:02 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-08-21 22:20:57 +08:00
|
|
|
|
// 处理 API Key 输入并自动更新配置
|
|
|
|
|
|
const handleApiKeyChange = (key: string) => {
|
|
|
|
|
|
setApiKey(key);
|
|
|
|
|
|
|
2025-08-26 10:37:44 +08:00
|
|
|
|
const configString = setApiKeyInConfig(
|
|
|
|
|
|
formData.settingsConfig,
|
|
|
|
|
|
key.trim(),
|
2025-09-06 23:13:01 +08:00
|
|
|
|
{ createIfMissing: selectedPreset !== null && selectedPreset !== -1 },
|
2025-08-26 10:37:44 +08:00
|
|
|
|
);
|
2025-08-21 22:20:57 +08:00
|
|
|
|
|
2025-08-26 10:37:44 +08:00
|
|
|
|
// 更新表单配置
|
2025-09-18 08:35:09 +08:00
|
|
|
|
updateSettingsConfigValue(configString);
|
2025-08-21 22:20:57 +08:00
|
|
|
|
|
2025-09-16 22:59:00 +08:00
|
|
|
|
// 同步通用配置开关
|
|
|
|
|
|
const hasCommon = hasCommonConfigSnippet(
|
|
|
|
|
|
configString,
|
|
|
|
|
|
commonConfigSnippet,
|
|
|
|
|
|
);
|
|
|
|
|
|
setUseCommonConfig(hasCommon);
|
2025-08-26 10:37:44 +08:00
|
|
|
|
};
|
2025-08-21 22:20:57 +08:00
|
|
|
|
|
2025-09-12 20:14:59 +08:00
|
|
|
|
// 处理基础 URL 变化
|
|
|
|
|
|
const handleBaseUrlChange = (url: string) => {
|
|
|
|
|
|
setBaseUrl(url);
|
2025-09-13 15:36:43 +08:00
|
|
|
|
|
2025-09-12 20:14:59 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const config = JSON.parse(formData.settingsConfig || "{}");
|
|
|
|
|
|
if (!config.env) {
|
|
|
|
|
|
config.env = {};
|
|
|
|
|
|
}
|
|
|
|
|
|
config.env.ANTHROPIC_BASE_URL = url.trim();
|
2025-09-13 15:36:43 +08:00
|
|
|
|
|
2025-09-18 08:35:09 +08:00
|
|
|
|
updateSettingsConfigValue(JSON.stringify(config, null, 2));
|
2025-09-12 20:14:59 +08:00
|
|
|
|
} catch {
|
|
|
|
|
|
// ignore
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-08-30 22:08:41 +08:00
|
|
|
|
// Codex: 处理 API Key 输入并写回 auth.json
|
|
|
|
|
|
const handleCodexApiKeyChange = (key: string) => {
|
|
|
|
|
|
setCodexApiKey(key);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const auth = JSON.parse(codexAuth || "{}");
|
2025-08-31 16:39:38 +08:00
|
|
|
|
auth.OPENAI_API_KEY = key.trim();
|
2025-08-30 22:08:41 +08:00
|
|
|
|
setCodexAuth(JSON.stringify(auth, null, 2));
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// ignore
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-17 10:44:30 +08:00
|
|
|
|
// Codex: 处理通用配置开关
|
|
|
|
|
|
const handleCodexCommonConfigToggle = (checked: boolean) => {
|
|
|
|
|
|
const { updatedConfig, error: snippetError } = updateTomlCommonConfigSnippet(
|
|
|
|
|
|
codexConfig,
|
|
|
|
|
|
codexCommonConfigSnippet,
|
|
|
|
|
|
checked,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (snippetError) {
|
|
|
|
|
|
setCodexCommonConfigError(snippetError);
|
|
|
|
|
|
setUseCodexCommonConfig(false);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setCodexCommonConfigError("");
|
|
|
|
|
|
setUseCodexCommonConfig(checked);
|
|
|
|
|
|
// 标记正在通过通用配置更新
|
|
|
|
|
|
isUpdatingFromCodexCommonConfig.current = true;
|
|
|
|
|
|
setCodexConfig(updatedConfig);
|
|
|
|
|
|
// 在下一个事件循环中重置标记
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
isUpdatingFromCodexCommonConfig.current = false;
|
|
|
|
|
|
}, 0);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Codex: 处理通用配置片段变化
|
|
|
|
|
|
const handleCodexCommonConfigSnippetChange = (value: string) => {
|
|
|
|
|
|
const previousSnippet = codexCommonConfigSnippet;
|
|
|
|
|
|
setCodexCommonConfigSnippet(value);
|
|
|
|
|
|
|
|
|
|
|
|
if (!value.trim()) {
|
|
|
|
|
|
setCodexCommonConfigError("");
|
|
|
|
|
|
if (useCodexCommonConfig) {
|
|
|
|
|
|
const { updatedConfig } = updateTomlCommonConfigSnippet(
|
|
|
|
|
|
codexConfig,
|
|
|
|
|
|
previousSnippet,
|
|
|
|
|
|
false,
|
|
|
|
|
|
);
|
|
|
|
|
|
setCodexConfig(updatedConfig);
|
|
|
|
|
|
setUseCodexCommonConfig(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TOML 不需要验证 JSON 格式,直接更新
|
|
|
|
|
|
if (useCodexCommonConfig) {
|
|
|
|
|
|
const removeResult = updateTomlCommonConfigSnippet(
|
|
|
|
|
|
codexConfig,
|
|
|
|
|
|
previousSnippet,
|
|
|
|
|
|
false,
|
|
|
|
|
|
);
|
|
|
|
|
|
const addResult = updateTomlCommonConfigSnippet(
|
|
|
|
|
|
removeResult.updatedConfig,
|
|
|
|
|
|
value,
|
|
|
|
|
|
true,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (addResult.error) {
|
|
|
|
|
|
setCodexCommonConfigError(addResult.error);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 标记正在通过通用配置更新
|
|
|
|
|
|
isUpdatingFromCodexCommonConfig.current = true;
|
|
|
|
|
|
setCodexConfig(addResult.updatedConfig);
|
|
|
|
|
|
// 在下一个事件循环中重置标记
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
isUpdatingFromCodexCommonConfig.current = false;
|
|
|
|
|
|
}, 0);
|
|
|
|
|
|
}
|
2025-09-17 22:39:45 +08:00
|
|
|
|
|
2025-09-17 10:44:30 +08:00
|
|
|
|
// 保存 Codex 通用配置到 localStorage
|
|
|
|
|
|
if (typeof window !== "undefined") {
|
|
|
|
|
|
try {
|
2025-09-17 22:39:45 +08:00
|
|
|
|
window.localStorage.setItem(
|
|
|
|
|
|
CODEX_COMMON_CONFIG_STORAGE_KEY,
|
|
|
|
|
|
value,
|
|
|
|
|
|
);
|
2025-09-17 10:44:30 +08:00
|
|
|
|
} catch {
|
|
|
|
|
|
// ignore localStorage 写入失败
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Codex: 处理 config 变化
|
|
|
|
|
|
const handleCodexConfigChange = (value: string) => {
|
|
|
|
|
|
if (!isUpdatingFromCodexCommonConfig.current) {
|
|
|
|
|
|
const hasCommon = hasTomlCommonConfigSnippet(
|
|
|
|
|
|
value,
|
|
|
|
|
|
codexCommonConfigSnippet,
|
|
|
|
|
|
);
|
|
|
|
|
|
setUseCodexCommonConfig(hasCommon);
|
|
|
|
|
|
}
|
|
|
|
|
|
setCodexConfig(value);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-08-26 10:37:44 +08:00
|
|
|
|
// 根据当前配置决定是否展示 API Key 输入框
|
2025-09-12 15:20:49 +08:00
|
|
|
|
// 自定义模式(-1)也需要显示 API Key 输入框
|
2025-08-26 10:37:44 +08:00
|
|
|
|
const showApiKey =
|
2025-09-13 15:36:43 +08:00
|
|
|
|
selectedPreset !== null ||
|
2025-09-03 15:58:02 +08:00
|
|
|
|
(!showPresets && hasApiKeyField(formData.settingsConfig));
|
2025-08-21 22:20:57 +08:00
|
|
|
|
|
2025-08-29 09:03:11 +08:00
|
|
|
|
// 判断当前选中的预设是否是官方
|
2025-08-29 11:35:17 +08:00
|
|
|
|
const isOfficialPreset =
|
2025-09-11 22:33:55 +08:00
|
|
|
|
(selectedPreset !== null &&
|
|
|
|
|
|
selectedPreset >= 0 &&
|
|
|
|
|
|
(providerPresets[selectedPreset]?.isOfficial === true ||
|
|
|
|
|
|
providerPresets[selectedPreset]?.category === "official")) ||
|
|
|
|
|
|
category === "official";
|
2025-08-29 09:03:11 +08:00
|
|
|
|
|
2025-09-06 23:13:01 +08:00
|
|
|
|
// 判断当前选中的预设是否是 Kimi
|
|
|
|
|
|
const isKimiPreset =
|
|
|
|
|
|
selectedPreset !== null &&
|
|
|
|
|
|
selectedPreset >= 0 &&
|
|
|
|
|
|
providerPresets[selectedPreset]?.name?.includes("Kimi");
|
|
|
|
|
|
|
|
|
|
|
|
// 判断当前编辑的是否是 Kimi 提供商(通过名称或配置判断)
|
|
|
|
|
|
const isEditingKimi =
|
|
|
|
|
|
initialData &&
|
|
|
|
|
|
(formData.name.includes("Kimi") ||
|
|
|
|
|
|
formData.name.includes("kimi") ||
|
|
|
|
|
|
(formData.settingsConfig.includes("api.moonshot.cn") &&
|
|
|
|
|
|
formData.settingsConfig.includes("ANTHROPIC_MODEL")));
|
|
|
|
|
|
|
|
|
|
|
|
// 综合判断是否应该显示 Kimi 模型选择器
|
|
|
|
|
|
const shouldShowKimiSelector = isKimiPreset || isEditingKimi;
|
|
|
|
|
|
|
2025-09-12 20:14:59 +08:00
|
|
|
|
// 判断是否显示基础 URL 输入框(仅自定义模式显示)
|
|
|
|
|
|
const showBaseUrlInput = selectedPreset === -1 && !isCodex;
|
|
|
|
|
|
|
2025-09-13 13:23:32 +08:00
|
|
|
|
// 判断是否显示"获取 API Key"链接(国产官方、聚合站和第三方显示)
|
2025-09-13 15:36:43 +08:00
|
|
|
|
const shouldShowApiKeyLink =
|
|
|
|
|
|
!isCodex &&
|
|
|
|
|
|
!isOfficialPreset &&
|
|
|
|
|
|
(category === "cn_official" ||
|
|
|
|
|
|
category === "aggregator" ||
|
|
|
|
|
|
category === "third_party" ||
|
|
|
|
|
|
(selectedPreset !== null &&
|
|
|
|
|
|
selectedPreset >= 0 &&
|
|
|
|
|
|
(providerPresets[selectedPreset]?.category === "cn_official" ||
|
|
|
|
|
|
providerPresets[selectedPreset]?.category === "aggregator" ||
|
|
|
|
|
|
providerPresets[selectedPreset]?.category === "third_party")));
|
|
|
|
|
|
|
2025-09-12 20:14:59 +08:00
|
|
|
|
// 获取当前供应商的网址
|
|
|
|
|
|
const getCurrentWebsiteUrl = () => {
|
|
|
|
|
|
if (selectedPreset !== null && selectedPreset >= 0) {
|
|
|
|
|
|
return providerPresets[selectedPreset]?.websiteUrl || "";
|
|
|
|
|
|
}
|
|
|
|
|
|
return formData.websiteUrl || "";
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 获取 Codex 当前供应商的网址
|
|
|
|
|
|
const getCurrentCodexWebsiteUrl = () => {
|
|
|
|
|
|
if (selectedCodexPreset !== null && selectedCodexPreset >= 0) {
|
|
|
|
|
|
return codexProviderPresets[selectedCodexPreset]?.websiteUrl || "";
|
|
|
|
|
|
}
|
|
|
|
|
|
return formData.websiteUrl || "";
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-08-30 22:08:41 +08:00
|
|
|
|
// Codex: 控制显示 API Key 与官方标记
|
|
|
|
|
|
const getCodexAuthApiKey = (authString: string): string => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const auth = JSON.parse(authString || "{}");
|
2025-08-31 16:39:38 +08:00
|
|
|
|
return typeof auth.OPENAI_API_KEY === "string" ? auth.OPENAI_API_KEY : "";
|
2025-08-30 22:08:41 +08:00
|
|
|
|
} catch {
|
|
|
|
|
|
return "";
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-09-06 23:13:01 +08:00
|
|
|
|
|
2025-09-03 15:58:02 +08:00
|
|
|
|
// 自定义模式(-1)不显示独立的 API Key 输入框
|
2025-08-30 22:08:41 +08:00
|
|
|
|
const showCodexApiKey =
|
2025-09-05 21:26:01 +08:00
|
|
|
|
(selectedCodexPreset !== null && selectedCodexPreset !== -1) ||
|
2025-09-03 15:58:02 +08:00
|
|
|
|
(!showPresets && getCodexAuthApiKey(codexAuth) !== "");
|
2025-09-06 23:13:01 +08:00
|
|
|
|
|
2025-09-11 22:33:55 +08:00
|
|
|
|
// 不再渲染分类介绍组件,避免造成干扰
|
|
|
|
|
|
|
2025-08-30 22:08:41 +08:00
|
|
|
|
const isCodexOfficialPreset =
|
2025-09-11 22:33:55 +08:00
|
|
|
|
(selectedCodexPreset !== null &&
|
|
|
|
|
|
selectedCodexPreset >= 0 &&
|
|
|
|
|
|
(codexProviderPresets[selectedCodexPreset]?.isOfficial === true ||
|
|
|
|
|
|
codexProviderPresets[selectedCodexPreset]?.category === "official")) ||
|
|
|
|
|
|
category === "official";
|
2025-08-30 22:08:41 +08:00
|
|
|
|
|
2025-09-12 20:14:59 +08:00
|
|
|
|
// 判断是否显示 Codex 的"获取 API Key"链接
|
2025-09-13 15:36:43 +08:00
|
|
|
|
const shouldShowCodexApiKeyLink =
|
|
|
|
|
|
isCodex &&
|
|
|
|
|
|
!isCodexOfficialPreset &&
|
|
|
|
|
|
(category === "cn_official" ||
|
|
|
|
|
|
category === "aggregator" ||
|
|
|
|
|
|
category === "third_party" ||
|
|
|
|
|
|
(selectedCodexPreset !== null &&
|
|
|
|
|
|
selectedCodexPreset >= 0 &&
|
|
|
|
|
|
(codexProviderPresets[selectedCodexPreset]?.category ===
|
|
|
|
|
|
"cn_official" ||
|
|
|
|
|
|
codexProviderPresets[selectedCodexPreset]?.category ===
|
|
|
|
|
|
"aggregator" ||
|
|
|
|
|
|
codexProviderPresets[selectedCodexPreset]?.category ===
|
|
|
|
|
|
"third_party")));
|
2025-09-12 20:14:59 +08:00
|
|
|
|
|
2025-09-12 12:04:19 +08:00
|
|
|
|
// 处理模型输入变化,自动更新 JSON 配置
|
2025-09-13 15:36:43 +08:00
|
|
|
|
const handleModelChange = (
|
|
|
|
|
|
field: "ANTHROPIC_MODEL" | "ANTHROPIC_SMALL_FAST_MODEL",
|
|
|
|
|
|
value: string,
|
|
|
|
|
|
) => {
|
|
|
|
|
|
if (field === "ANTHROPIC_MODEL") {
|
2025-09-12 12:04:19 +08:00
|
|
|
|
setClaudeModel(value);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setClaudeSmallFastModel(value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新 JSON 配置
|
|
|
|
|
|
try {
|
2025-09-13 15:36:43 +08:00
|
|
|
|
const currentConfig = formData.settingsConfig
|
|
|
|
|
|
? JSON.parse(formData.settingsConfig)
|
|
|
|
|
|
: { env: {} };
|
2025-09-12 12:04:19 +08:00
|
|
|
|
if (!currentConfig.env) currentConfig.env = {};
|
2025-09-13 15:36:43 +08:00
|
|
|
|
|
2025-09-12 12:04:19 +08:00
|
|
|
|
if (value.trim()) {
|
|
|
|
|
|
currentConfig.env[field] = value.trim();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
delete currentConfig.env[field];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-18 08:35:09 +08:00
|
|
|
|
updateSettingsConfigValue(JSON.stringify(currentConfig, null, 2));
|
2025-09-12 12:04:19 +08:00
|
|
|
|
} catch (err) {
|
|
|
|
|
|
// 如果 JSON 解析失败,不做处理
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-06 23:13:01 +08:00
|
|
|
|
// Kimi 模型选择处理函数
|
|
|
|
|
|
const handleKimiModelChange = (
|
|
|
|
|
|
field: "ANTHROPIC_MODEL" | "ANTHROPIC_SMALL_FAST_MODEL",
|
|
|
|
|
|
value: string,
|
|
|
|
|
|
) => {
|
|
|
|
|
|
if (field === "ANTHROPIC_MODEL") {
|
|
|
|
|
|
setKimiAnthropicModel(value);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setKimiAnthropicSmallFastModel(value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新配置 JSON
|
|
|
|
|
|
try {
|
|
|
|
|
|
const currentConfig = JSON.parse(formData.settingsConfig || "{}");
|
|
|
|
|
|
if (!currentConfig.env) currentConfig.env = {};
|
|
|
|
|
|
currentConfig.env[field] = value;
|
|
|
|
|
|
|
|
|
|
|
|
const updatedConfigString = JSON.stringify(currentConfig, null, 2);
|
2025-09-18 08:35:09 +08:00
|
|
|
|
updateSettingsConfigValue(updatedConfigString);
|
2025-09-06 23:13:01 +08:00
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error("更新 Kimi 模型配置失败:", err);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-08-26 10:37:44 +08:00
|
|
|
|
// 初始时从配置中同步 API Key(编辑模式)
|
|
|
|
|
|
useEffect(() => {
|
2025-09-10 19:46:38 +08:00
|
|
|
|
if (!initialData) return;
|
|
|
|
|
|
const parsedKey = getApiKeyFromConfig(
|
|
|
|
|
|
JSON.stringify(initialData.settingsConfig),
|
|
|
|
|
|
);
|
|
|
|
|
|
if (parsedKey) setApiKey(parsedKey);
|
|
|
|
|
|
}, [initialData]);
|
2025-08-08 15:03:38 +08:00
|
|
|
|
|
2025-08-26 12:34:47 +08:00
|
|
|
|
// 支持按下 ESC 关闭弹窗
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const onKeyDown = (e: KeyboardEvent) => {
|
2025-08-26 15:12:27 +08:00
|
|
|
|
if (e.key === "Escape") {
|
2025-08-26 12:34:47 +08:00
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
onClose();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-08-26 15:12:27 +08:00
|
|
|
|
window.addEventListener("keydown", onKeyDown);
|
|
|
|
|
|
return () => window.removeEventListener("keydown", onKeyDown);
|
2025-08-26 12:34:47 +08:00
|
|
|
|
}, [onClose]);
|
|
|
|
|
|
|
2025-08-08 15:03:38 +08:00
|
|
|
|
return (
|
2025-08-26 15:12:27 +08:00
|
|
|
|
<div
|
2025-09-06 16:21:21 +08:00
|
|
|
|
className="fixed inset-0 z-50 flex items-center justify-center"
|
2025-08-26 15:12:27 +08:00
|
|
|
|
onMouseDown={(e) => {
|
|
|
|
|
|
if (e.target === e.currentTarget) onClose();
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2025-09-06 16:21:21 +08:00
|
|
|
|
{/* Backdrop */}
|
2025-09-13 21:45:34 +08:00
|
|
|
|
<div className="absolute inset-0 bg-black/50 dark:bg-black/70 backdrop-blur-sm" />
|
2025-09-06 16:21:21 +08:00
|
|
|
|
|
|
|
|
|
|
{/* Modal */}
|
2025-09-13 21:45:34 +08:00
|
|
|
|
<div className="relative bg-white dark:bg-gray-900 rounded-xl shadow-lg max-w-3xl w-full mx-4 max-h-[90vh] overflow-hidden flex flex-col">
|
2025-09-06 16:21:21 +08:00
|
|
|
|
{/* Header */}
|
2025-09-13 21:45:34 +08:00
|
|
|
|
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-800">
|
|
|
|
|
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
|
|
|
|
|
{title}
|
|
|
|
|
|
</h2>
|
2025-08-26 12:34:47 +08:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={onClose}
|
2025-09-13 21:45:34 +08:00
|
|
|
|
className="p-1 text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors"
|
2025-09-06 16:21:21 +08:00
|
|
|
|
aria-label="关闭"
|
2025-08-26 12:34:47 +08:00
|
|
|
|
>
|
2025-09-06 16:21:21 +08:00
|
|
|
|
<X size={18} />
|
2025-08-26 12:34:47 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-06 16:21:21 +08:00
|
|
|
|
<form onSubmit={handleSubmit} className="flex flex-col flex-1 min-h-0">
|
|
|
|
|
|
<div className="flex-1 overflow-auto p-6 space-y-6">
|
|
|
|
|
|
{error && (
|
2025-09-13 21:45:34 +08:00
|
|
|
|
<div className="flex items-center gap-3 p-4 bg-red-100 dark:bg-red-900/20 border border-red-500/20 dark:border-red-500/30 rounded-lg">
|
|
|
|
|
|
<AlertCircle
|
|
|
|
|
|
size={20}
|
|
|
|
|
|
className="text-red-500 dark:text-red-400 flex-shrink-0"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<p className="text-red-500 dark:text-red-400 text-sm font-medium">
|
|
|
|
|
|
{error}
|
|
|
|
|
|
</p>
|
2025-09-06 16:21:21 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2025-08-26 15:12:27 +08:00
|
|
|
|
|
2025-08-30 21:54:11 +08:00
|
|
|
|
{showPresets && !isCodex && (
|
2025-09-06 23:13:01 +08:00
|
|
|
|
<PresetSelector
|
|
|
|
|
|
presets={providerPresets}
|
|
|
|
|
|
selectedIndex={selectedPreset}
|
|
|
|
|
|
onSelectPreset={(index) =>
|
|
|
|
|
|
applyPreset(providerPresets[index], index)
|
|
|
|
|
|
}
|
|
|
|
|
|
onCustomClick={handleCustomClick}
|
|
|
|
|
|
/>
|
2025-08-26 15:12:27 +08:00
|
|
|
|
)}
|
2025-08-08 15:03:38 +08:00
|
|
|
|
|
2025-08-30 22:08:41 +08:00
|
|
|
|
{showPresets && isCodex && (
|
2025-09-06 23:13:01 +08:00
|
|
|
|
<PresetSelector
|
|
|
|
|
|
presets={codexProviderPresets}
|
|
|
|
|
|
selectedIndex={selectedCodexPreset}
|
|
|
|
|
|
onSelectPreset={(index) =>
|
|
|
|
|
|
applyCodexPreset(codexProviderPresets[index], index)
|
|
|
|
|
|
}
|
|
|
|
|
|
onCustomClick={handleCodexCustomClick}
|
|
|
|
|
|
/>
|
2025-08-30 22:08:41 +08:00
|
|
|
|
)}
|
|
|
|
|
|
|
2025-09-06 16:21:21 +08:00
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<label
|
|
|
|
|
|
htmlFor="name"
|
2025-09-13 21:45:34 +08:00
|
|
|
|
className="block text-sm font-medium text-gray-900 dark:text-gray-100"
|
2025-09-06 16:21:21 +08:00
|
|
|
|
>
|
|
|
|
|
|
供应商名称 *
|
|
|
|
|
|
</label>
|
2025-08-21 22:20:57 +08:00
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
2025-08-26 15:12:27 +08:00
|
|
|
|
id="name"
|
|
|
|
|
|
name="name"
|
|
|
|
|
|
value={formData.name}
|
|
|
|
|
|
onChange={handleChange}
|
|
|
|
|
|
placeholder="例如:Anthropic 官方"
|
|
|
|
|
|
required
|
2025-08-21 22:20:57 +08:00
|
|
|
|
autoComplete="off"
|
2025-09-13 21:45:34 +08:00
|
|
|
|
className="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 focus:border-blue-500 dark:focus:border-blue-400 transition-colors"
|
2025-08-21 22:20:57 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2025-08-08 15:03:38 +08:00
|
|
|
|
|
2025-09-12 12:04:19 +08:00
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<label
|
|
|
|
|
|
htmlFor="websiteUrl"
|
2025-09-13 21:45:34 +08:00
|
|
|
|
className="block text-sm font-medium text-gray-900 dark:text-gray-100"
|
2025-09-12 12:04:19 +08:00
|
|
|
|
>
|
|
|
|
|
|
官网地址
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="url"
|
|
|
|
|
|
id="websiteUrl"
|
|
|
|
|
|
name="websiteUrl"
|
|
|
|
|
|
value={formData.websiteUrl}
|
|
|
|
|
|
onChange={handleChange}
|
|
|
|
|
|
placeholder="https://example.com(可选)"
|
|
|
|
|
|
autoComplete="off"
|
2025-09-13 21:45:34 +08:00
|
|
|
|
className="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 focus:border-blue-500 dark:focus:border-blue-400 transition-colors"
|
2025-09-12 12:04:19 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-06 16:21:21 +08:00
|
|
|
|
{!isCodex && showApiKey && (
|
2025-09-12 20:14:59 +08:00
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<ApiKeyInput
|
|
|
|
|
|
value={apiKey}
|
|
|
|
|
|
onChange={handleApiKeyChange}
|
|
|
|
|
|
required={!isOfficialPreset}
|
|
|
|
|
|
placeholder={
|
|
|
|
|
|
isOfficialPreset
|
|
|
|
|
|
? "官方登录无需填写 API Key,直接保存即可"
|
|
|
|
|
|
: shouldShowKimiSelector
|
|
|
|
|
|
? "填写后可获取模型列表"
|
|
|
|
|
|
: "只需要填这里,下方配置会自动填充"
|
|
|
|
|
|
}
|
|
|
|
|
|
disabled={isOfficialPreset}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{shouldShowApiKeyLink && getCurrentWebsiteUrl() && (
|
|
|
|
|
|
<div className="-mt-1 pl-1">
|
|
|
|
|
|
<a
|
|
|
|
|
|
href={getCurrentWebsiteUrl()}
|
|
|
|
|
|
target="_blank"
|
|
|
|
|
|
rel="noopener noreferrer"
|
2025-09-13 21:45:34 +08:00
|
|
|
|
className="text-xs text-blue-400 dark:text-blue-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
|
2025-09-12 20:14:59 +08:00
|
|
|
|
>
|
2025-09-12 21:36:32 +08:00
|
|
|
|
获取 API Key
|
2025-09-12 20:14:59 +08:00
|
|
|
|
</a>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 基础 URL 输入框 - 仅在自定义模式下显示 */}
|
|
|
|
|
|
{!isCodex && showBaseUrlInput && (
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<label
|
|
|
|
|
|
htmlFor="baseUrl"
|
2025-09-13 21:45:34 +08:00
|
|
|
|
className="block text-sm font-medium text-gray-900 dark:text-gray-100"
|
2025-09-12 20:14:59 +08:00
|
|
|
|
>
|
|
|
|
|
|
请求地址
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="url"
|
|
|
|
|
|
id="baseUrl"
|
|
|
|
|
|
value={baseUrl}
|
|
|
|
|
|
onChange={(e) => handleBaseUrlChange(e.target.value)}
|
|
|
|
|
|
placeholder="https://your-api-endpoint.com"
|
|
|
|
|
|
autoComplete="off"
|
2025-09-13 21:45:34 +08:00
|
|
|
|
className="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 focus:border-blue-500 dark:focus:border-blue-400 transition-colors"
|
2025-09-12 20:14:59 +08:00
|
|
|
|
/>
|
2025-09-13 21:45:34 +08:00
|
|
|
|
<div className="p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700 rounded-lg">
|
|
|
|
|
|
<p className="text-xs text-amber-600 dark:text-amber-400">
|
2025-09-12 20:14:59 +08:00
|
|
|
|
💡 填写兼容 Claude API 的服务端点地址
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-09-06 23:13:01 +08:00
|
|
|
|
)}
|
|
|
|
|
|
|
2025-09-12 20:14:59 +08:00
|
|
|
|
{!isCodex && shouldShowKimiSelector && (
|
2025-09-06 23:13:01 +08:00
|
|
|
|
<KimiModelSelector
|
|
|
|
|
|
apiKey={apiKey}
|
|
|
|
|
|
anthropicModel={kimiAnthropicModel}
|
|
|
|
|
|
anthropicSmallFastModel={kimiAnthropicSmallFastModel}
|
|
|
|
|
|
onModelChange={handleKimiModelChange}
|
|
|
|
|
|
disabled={isOfficialPreset}
|
|
|
|
|
|
/>
|
2025-08-30 21:54:11 +08:00
|
|
|
|
)}
|
2025-08-26 15:12:27 +08:00
|
|
|
|
|
2025-09-06 16:21:21 +08:00
|
|
|
|
{isCodex && showCodexApiKey && (
|
2025-09-12 20:14:59 +08:00
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<ApiKeyInput
|
|
|
|
|
|
id="codexApiKey"
|
|
|
|
|
|
label="API Key"
|
|
|
|
|
|
value={codexApiKey}
|
|
|
|
|
|
onChange={handleCodexApiKeyChange}
|
|
|
|
|
|
placeholder={
|
|
|
|
|
|
isCodexOfficialPreset
|
|
|
|
|
|
? "官方无需填写 API Key,直接保存即可"
|
|
|
|
|
|
: "只需要填这里,下方 auth.json 会自动填充"
|
|
|
|
|
|
}
|
|
|
|
|
|
disabled={isCodexOfficialPreset}
|
|
|
|
|
|
required={
|
|
|
|
|
|
selectedCodexPreset !== null &&
|
|
|
|
|
|
selectedCodexPreset >= 0 &&
|
|
|
|
|
|
!isCodexOfficialPreset
|
|
|
|
|
|
}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{shouldShowCodexApiKeyLink && getCurrentCodexWebsiteUrl() && (
|
|
|
|
|
|
<div className="-mt-1 pl-1">
|
|
|
|
|
|
<a
|
|
|
|
|
|
href={getCurrentCodexWebsiteUrl()}
|
|
|
|
|
|
target="_blank"
|
|
|
|
|
|
rel="noopener noreferrer"
|
2025-09-13 21:45:34 +08:00
|
|
|
|
className="text-xs text-blue-400 dark:text-blue-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
|
2025-09-12 20:14:59 +08:00
|
|
|
|
>
|
2025-09-12 21:36:32 +08:00
|
|
|
|
获取 API Key
|
2025-09-12 20:14:59 +08:00
|
|
|
|
</a>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2025-08-30 22:08:41 +08:00
|
|
|
|
)}
|
|
|
|
|
|
|
2025-08-30 21:54:11 +08:00
|
|
|
|
{/* Claude 或 Codex 的配置部分 */}
|
|
|
|
|
|
{isCodex ? (
|
2025-09-06 23:13:01 +08:00
|
|
|
|
<CodexConfigEditor
|
|
|
|
|
|
authValue={codexAuth}
|
|
|
|
|
|
configValue={codexConfig}
|
|
|
|
|
|
onAuthChange={setCodexAuth}
|
2025-09-17 10:44:30 +08:00
|
|
|
|
onConfigChange={handleCodexConfigChange}
|
2025-09-06 23:13:01 +08:00
|
|
|
|
onAuthBlur={() => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const auth = JSON.parse(codexAuth || "{}");
|
|
|
|
|
|
const key =
|
|
|
|
|
|
typeof auth.OPENAI_API_KEY === "string"
|
|
|
|
|
|
? auth.OPENAI_API_KEY
|
|
|
|
|
|
: "";
|
|
|
|
|
|
setCodexApiKey(key);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// ignore
|
2025-09-06 16:21:21 +08:00
|
|
|
|
}
|
2025-09-06 23:13:01 +08:00
|
|
|
|
}}
|
2025-09-17 10:44:30 +08:00
|
|
|
|
useCommonConfig={useCodexCommonConfig}
|
|
|
|
|
|
onCommonConfigToggle={handleCodexCommonConfigToggle}
|
|
|
|
|
|
commonConfigSnippet={codexCommonConfigSnippet}
|
|
|
|
|
|
onCommonConfigSnippetChange={handleCodexCommonConfigSnippetChange}
|
|
|
|
|
|
commonConfigError={codexCommonConfigError}
|
2025-09-18 08:35:09 +08:00
|
|
|
|
authError={codexAuthError}
|
2025-09-06 23:13:01 +08:00
|
|
|
|
/>
|
|
|
|
|
|
) : (
|
2025-09-12 12:04:19 +08:00
|
|
|
|
<>
|
2025-09-12 20:14:59 +08:00
|
|
|
|
{/* 可选的模型配置输入框 - 仅在非官方且非 Kimi 时显示 */}
|
|
|
|
|
|
{!isOfficialPreset && !shouldShowKimiSelector && (
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<label
|
|
|
|
|
|
htmlFor="anthropicModel"
|
2025-09-13 21:45:34 +08:00
|
|
|
|
className="block text-sm font-medium text-gray-900 dark:text-gray-100"
|
2025-09-12 20:14:59 +08:00
|
|
|
|
>
|
|
|
|
|
|
主模型 (可选)
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
id="anthropicModel"
|
|
|
|
|
|
value={claudeModel}
|
2025-09-13 15:36:43 +08:00
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
handleModelChange("ANTHROPIC_MODEL", e.target.value)
|
|
|
|
|
|
}
|
2025-09-12 20:14:59 +08:00
|
|
|
|
placeholder="例如: GLM-4.5"
|
|
|
|
|
|
autoComplete="off"
|
2025-09-13 21:45:34 +08:00
|
|
|
|
className="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 focus:border-blue-500 dark:focus:border-blue-400 transition-colors"
|
2025-09-12 20:14:59 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2025-09-13 15:36:43 +08:00
|
|
|
|
|
2025-09-12 20:14:59 +08:00
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<label
|
|
|
|
|
|
htmlFor="anthropicSmallFastModel"
|
2025-09-13 21:45:34 +08:00
|
|
|
|
className="block text-sm font-medium text-gray-900 dark:text-gray-100"
|
2025-09-12 20:14:59 +08:00
|
|
|
|
>
|
|
|
|
|
|
快速模型 (可选)
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
id="anthropicSmallFastModel"
|
|
|
|
|
|
value={claudeSmallFastModel}
|
2025-09-13 15:36:43 +08:00
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
handleModelChange(
|
|
|
|
|
|
"ANTHROPIC_SMALL_FAST_MODEL",
|
|
|
|
|
|
e.target.value,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2025-09-12 20:14:59 +08:00
|
|
|
|
placeholder="例如: GLM-4.5-Air"
|
|
|
|
|
|
autoComplete="off"
|
2025-09-13 21:45:34 +08:00
|
|
|
|
className="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 focus:border-blue-500 dark:focus:border-blue-400 transition-colors"
|
2025-09-12 20:14:59 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2025-09-12 12:04:19 +08:00
|
|
|
|
</div>
|
2025-09-13 15:36:43 +08:00
|
|
|
|
|
2025-09-13 21:45:34 +08:00
|
|
|
|
<div className="p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700 rounded-lg">
|
|
|
|
|
|
<p className="text-xs text-amber-600 dark:text-amber-400">
|
2025-09-12 20:14:59 +08:00
|
|
|
|
💡 留空将使用供应商的默认模型
|
|
|
|
|
|
</p>
|
2025-09-12 12:04:19 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2025-09-13 15:36:43 +08:00
|
|
|
|
|
2025-09-12 12:04:19 +08:00
|
|
|
|
<ClaudeConfigEditor
|
|
|
|
|
|
value={formData.settingsConfig}
|
|
|
|
|
|
onChange={(value) =>
|
|
|
|
|
|
handleChange({
|
|
|
|
|
|
target: { name: "settingsConfig", value },
|
|
|
|
|
|
} as React.ChangeEvent<HTMLTextAreaElement>)
|
|
|
|
|
|
}
|
2025-09-16 22:59:00 +08:00
|
|
|
|
useCommonConfig={useCommonConfig}
|
|
|
|
|
|
onCommonConfigToggle={handleCommonConfigToggle}
|
|
|
|
|
|
commonConfigSnippet={commonConfigSnippet}
|
|
|
|
|
|
onCommonConfigSnippetChange={handleCommonConfigSnippetChange}
|
|
|
|
|
|
commonConfigError={commonConfigError}
|
2025-09-18 08:35:09 +08:00
|
|
|
|
configError={settingsConfigError}
|
2025-09-12 12:04:19 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</>
|
2025-08-30 21:54:11 +08:00
|
|
|
|
)}
|
2025-08-08 15:03:38 +08:00
|
|
|
|
</div>
|
2025-08-26 12:34:47 +08:00
|
|
|
|
|
2025-09-06 16:21:21 +08:00
|
|
|
|
{/* Footer */}
|
2025-09-13 21:45:34 +08:00
|
|
|
|
<div className="flex items-center justify-end gap-3 p-6 border-t border-gray-200 dark:border-gray-800 bg-gray-100 dark:bg-gray-800">
|
2025-09-06 16:21:21 +08:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={onClose}
|
2025-09-13 21:45:34 +08:00
|
|
|
|
className="px-4 py-2 text-sm font-medium text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-white dark:hover:bg-gray-700 rounded-lg transition-colors"
|
2025-09-06 16:21:21 +08:00
|
|
|
|
>
|
2025-08-26 15:12:27 +08:00
|
|
|
|
取消
|
|
|
|
|
|
</button>
|
2025-09-06 16:21:21 +08:00
|
|
|
|
<button
|
|
|
|
|
|
type="submit"
|
2025-09-13 21:45:34 +08:00
|
|
|
|
className="px-4 py-2 bg-blue-500 dark:bg-blue-600 text-white rounded-lg hover:bg-blue-600 dark:hover:bg-blue-700 transition-colors text-sm font-medium flex items-center gap-2"
|
2025-09-06 16:21:21 +08:00
|
|
|
|
>
|
2025-09-13 17:04:46 +08:00
|
|
|
|
<Save className="w-4 h-4" />
|
2025-08-26 15:12:27 +08:00
|
|
|
|
{submitText}
|
|
|
|
|
|
</button>
|
2025-08-26 12:34:47 +08:00
|
|
|
|
</div>
|
2025-08-08 15:03:38 +08:00
|
|
|
|
</form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-08-21 22:20:57 +08:00
|
|
|
|
);
|
|
|
|
|
|
};
|
2025-08-08 15:03:38 +08:00
|
|
|
|
|
2025-08-21 22:20:57 +08:00
|
|
|
|
export default ProviderForm;
|