import React, { useState, useEffect } from "react"; import { Provider } from "../types"; import { AppType } from "../lib/tauri-api"; import { updateCoAuthoredSetting, checkCoAuthoredSetting, extractWebsiteUrl, getApiKeyFromConfig, hasApiKeyField, setApiKeyInConfig, } from "../utils/providerConfigUtils"; import { providerPresets } from "../config/providerPresets"; import { codexProviderPresets } from "../config/codexProviderPresets"; import "./AddProviderModal.css"; interface ProviderFormProps { appType?: AppType; title: string; submitText: string; initialData?: Provider; showPresets?: boolean; onSubmit: (data: Omit) => void; onClose: () => void; } const ProviderForm: React.FC = ({ appType = "claude", title, submitText, initialData, showPresets = false, onSubmit, onClose, }) => { // 对于 Codex,需要分离 auth 和 config const isCodex = appType === "codex"; const [formData, setFormData] = useState({ name: initialData?.name || "", websiteUrl: initialData?.websiteUrl || "", settingsConfig: initialData ? JSON.stringify(initialData.settingsConfig, null, 2) : "", }); // Codex 特有的状态 const [codexAuth, setCodexAuth] = useState(""); const [codexConfig, setCodexConfig] = useState(""); const [codexApiKey, setCodexApiKey] = useState(""); const [selectedCodexPreset, setSelectedCodexPreset] = useState( null ); // 初始化 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 || ""); try { const auth = config.auth || {}; if (auth && typeof auth.OPENAI_API_KEY === "string") { setCodexApiKey(auth.OPENAI_API_KEY); } } catch { // ignore } } } }, [isCodex, initialData]); const [error, setError] = useState(""); const [disableCoAuthored, setDisableCoAuthored] = useState(false); const [selectedPreset, setSelectedPreset] = useState(null); const [apiKey, setApiKey] = useState(""); // 初始化时检查禁用签名状态 useEffect(() => { if (initialData) { const configString = JSON.stringify(initialData.settingsConfig, null, 2); const hasCoAuthoredDisabled = checkCoAuthoredSetting(configString); setDisableCoAuthored(hasCoAuthoredDisabled); } }, [initialData]); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); setError(""); if (!formData.name) { setError("请填写供应商名称"); return; } let settingsConfig: Record; if (isCodex) { // Codex: 仅要求 auth.json 必填;config.toml 可为空 if (!codexAuth.trim()) { setError("请填写 auth.json 配置"); return; } try { const authJson = JSON.parse(codexAuth); settingsConfig = { auth: authJson, config: codexConfig ?? "", }; } catch (err) { setError("auth.json 格式错误,请检查JSON语法"); return; } } else { // Claude: 原有逻辑 if (!formData.settingsConfig.trim()) { setError("请填写配置内容"); return; } try { settingsConfig = JSON.parse(formData.settingsConfig); } catch (err) { setError("配置JSON格式错误,请检查语法"); return; } } onSubmit({ name: formData.name, websiteUrl: formData.websiteUrl, settingsConfig, }); }; const handleChange = ( e: React.ChangeEvent ) => { const { name, value } = e.target; if (name === "settingsConfig") { // 当用户修改配置时,尝试自动提取官网地址 const extractedWebsiteUrl = extractWebsiteUrl(value); // 同时检查并同步选择框状态 const hasCoAuthoredDisabled = checkCoAuthoredSetting(value); setDisableCoAuthored(hasCoAuthoredDisabled); // 同步 API Key 输入框显示与值 const parsedKey = getApiKeyFromConfig(value); setApiKey(parsedKey); setFormData({ ...formData, [name]: value, // 只有在官网地址为空时才自动填入 websiteUrl: formData.websiteUrl || extractedWebsiteUrl, }); } else { setFormData({ ...formData, [name]: value, }); } }; // 处理选择框变化 const handleCoAuthoredToggle = (checked: boolean) => { setDisableCoAuthored(checked); // 更新JSON配置 const updatedConfig = updateCoAuthoredSetting( formData.settingsConfig, checked ); setFormData({ ...formData, settingsConfig: updatedConfig, }); }; const applyPreset = (preset: (typeof providerPresets)[0], index: number) => { const configString = JSON.stringify(preset.settingsConfig, null, 2); setFormData({ name: preset.name, websiteUrl: preset.websiteUrl, settingsConfig: configString, }); // 设置选中的预设 setSelectedPreset(index); // 清空 API Key 输入框,让用户重新输入 setApiKey(""); // 同步选择框状态 const hasCoAuthoredDisabled = checkCoAuthoredSetting(configString); setDisableCoAuthored(hasCoAuthoredDisabled); }; // Codex: 应用预设 const applyCodexPreset = ( preset: (typeof codexProviderPresets)[0], index: number ) => { const authString = JSON.stringify(preset.auth || {}, null, 2); setCodexAuth(authString); setCodexConfig(preset.config || ""); setFormData({ name: preset.name, websiteUrl: preset.websiteUrl, settingsConfig: formData.settingsConfig, }); setSelectedCodexPreset(index); // 清空 API Key,让用户重新输入 setCodexApiKey(""); }; // 处理 API Key 输入并自动更新配置 const handleApiKeyChange = (key: string) => { setApiKey(key); const configString = setApiKeyInConfig( formData.settingsConfig, key.trim(), { createIfMissing: selectedPreset !== null } ); // 更新表单配置 setFormData((prev) => ({ ...prev, settingsConfig: configString, })); // 同步选择框状态 const hasCoAuthoredDisabled = checkCoAuthoredSetting(configString); setDisableCoAuthored(hasCoAuthoredDisabled); }; // Codex: 处理 API Key 输入并写回 auth.json const handleCodexApiKeyChange = (key: string) => { setCodexApiKey(key); try { const auth = JSON.parse(codexAuth || "{}"); auth.OPENAI_API_KEY = key.trim(); setCodexAuth(JSON.stringify(auth, null, 2)); } catch { // ignore } }; // 根据当前配置决定是否展示 API Key 输入框 const showApiKey = selectedPreset !== null || hasApiKeyField(formData.settingsConfig); // 判断当前选中的预设是否是官方 const isOfficialPreset = selectedPreset !== null && providerPresets[selectedPreset]?.isOfficial === true; // Codex: 控制显示 API Key 与官方标记 const getCodexAuthApiKey = (authString: string): string => { try { const auth = JSON.parse(authString || "{}"); return typeof auth.OPENAI_API_KEY === "string" ? auth.OPENAI_API_KEY : ""; } catch { return ""; } }; const showCodexApiKey = selectedCodexPreset !== null || getCodexAuthApiKey(codexAuth) !== ""; const isCodexOfficialPreset = selectedCodexPreset !== null && codexProviderPresets[selectedCodexPreset]?.isOfficial === true; // 初始时从配置中同步 API Key(编辑模式) useEffect(() => { if (initialData) { const parsedKey = getApiKeyFromConfig( JSON.stringify(initialData.settingsConfig) ); if (parsedKey) setApiKey(parsedKey); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // 支持按下 ESC 关闭弹窗 useEffect(() => { const onKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") { e.preventDefault(); onClose(); } }; window.addEventListener("keydown", onKeyDown); return () => window.removeEventListener("keydown", onKeyDown); }, [onClose]); return (
{ if (e.target === e.currentTarget) onClose(); }} >
{title}
{error &&
{error}
} {showPresets && !isCodex && (
{providerPresets.map((preset, index) => { return ( ); })}
)} {showPresets && isCodex && (
{codexProviderPresets.map((preset, index) => ( ))}
)}
{!isCodex && (
handleApiKeyChange(e.target.value)} placeholder={ isOfficialPreset ? "官方登录无需填写 API Key,直接保存即可" : "只需要填这里,下方配置会自动填充" } disabled={isOfficialPreset} autoComplete="off" style={ isOfficialPreset ? { backgroundColor: "#f5f5f5", cursor: "not-allowed", color: "#999", } : {} } />
)} {isCodex && (
handleCodexApiKeyChange(e.target.value)} placeholder={ isCodexOfficialPreset ? "官方无需填写 API Key,直接保存即可" : "只需要填这里,下方 auth.json 会自动填充" } disabled={isCodexOfficialPreset} autoComplete="off" style={ isCodexOfficialPreset ? { backgroundColor: "#f5f5f5", cursor: "not-allowed", color: "#999", } : {} } />
)}
{/* Claude 或 Codex 的配置部分 */} {isCodex ? ( // Codex: 双编辑器 <>