feat: add configuration wizard for custom Codex providers
- Add quick configuration wizard modal for custom providers - Generate auth.json and config.toml from simple inputs (API key, base URL, model name) - Extract generation logic into reusable functions (generateThirdPartyAuth, generateThirdPartyConfig) - Pre-populate custom template when selecting custom option - Add wizard button link in PresetSelector for custom mode - Update PackyCode preset to use the new generation functions
This commit is contained in:
@@ -12,7 +12,7 @@ import {
|
|||||||
validateJsonConfig,
|
validateJsonConfig,
|
||||||
} from "../utils/providerConfigUtils";
|
} from "../utils/providerConfigUtils";
|
||||||
import { providerPresets } from "../config/providerPresets";
|
import { providerPresets } from "../config/providerPresets";
|
||||||
import { codexProviderPresets } from "../config/codexProviderPresets";
|
import { codexProviderPresets, generateThirdPartyAuth, generateThirdPartyConfig } from "../config/codexProviderPresets";
|
||||||
import PresetSelector from "./ProviderForm/PresetSelector";
|
import PresetSelector from "./ProviderForm/PresetSelector";
|
||||||
import ApiKeyInput from "./ProviderForm/ApiKeyInput";
|
import ApiKeyInput from "./ProviderForm/ApiKeyInput";
|
||||||
import ClaudeConfigEditor from "./ProviderForm/ClaudeConfigEditor";
|
import ClaudeConfigEditor from "./ProviderForm/ClaudeConfigEditor";
|
||||||
@@ -72,6 +72,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
const [codexAuth, setCodexAuthState] = useState("");
|
const [codexAuth, setCodexAuthState] = useState("");
|
||||||
const [codexConfig, setCodexConfigState] = useState("");
|
const [codexConfig, setCodexConfigState] = useState("");
|
||||||
const [codexApiKey, setCodexApiKey] = useState("");
|
const [codexApiKey, setCodexApiKey] = useState("");
|
||||||
|
const [isCodexTemplateModalOpen, setIsCodexTemplateModalOpen] = 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,
|
||||||
@@ -628,14 +629,23 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
// Codex: 处理点击自定义按钮
|
// Codex: 处理点击自定义按钮
|
||||||
const handleCodexCustomClick = () => {
|
const handleCodexCustomClick = () => {
|
||||||
setSelectedCodexPreset(-1);
|
setSelectedCodexPreset(-1);
|
||||||
|
|
||||||
|
// 设置自定义模板
|
||||||
|
const customAuth = generateThirdPartyAuth("");
|
||||||
|
const customConfig = generateThirdPartyConfig(
|
||||||
|
"custom",
|
||||||
|
"https://your-api-endpoint.com/v1",
|
||||||
|
"gpt-5-codex"
|
||||||
|
);
|
||||||
|
|
||||||
setFormData({
|
setFormData({
|
||||||
name: "",
|
name: "",
|
||||||
websiteUrl: "",
|
websiteUrl: "",
|
||||||
settingsConfig: "",
|
settingsConfig: "",
|
||||||
});
|
});
|
||||||
setSettingsConfigError(validateSettingsConfig(""));
|
setSettingsConfigError(validateSettingsConfig(""));
|
||||||
setCodexAuth("");
|
setCodexAuth(JSON.stringify(customAuth, null, 2));
|
||||||
setCodexConfig("");
|
setCodexConfig(customConfig);
|
||||||
setCodexApiKey("");
|
setCodexApiKey("");
|
||||||
setCategory("custom");
|
setCategory("custom");
|
||||||
};
|
};
|
||||||
@@ -1027,6 +1037,18 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
applyCodexPreset(codexProviderPresets[index], index)
|
applyCodexPreset(codexProviderPresets[index], index)
|
||||||
}
|
}
|
||||||
onCustomClick={handleCodexCustomClick}
|
onCustomClick={handleCodexCustomClick}
|
||||||
|
renderCustomDescription={() => (
|
||||||
|
<>
|
||||||
|
手动配置供应商,需要填写完整的配置信息,或者
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsCodexTemplateModalOpen(true)}
|
||||||
|
className="text-blue-400 dark:text-blue-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors ml-1"
|
||||||
|
>
|
||||||
|
使用配置向导
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1196,6 +1218,15 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
}
|
}
|
||||||
commonConfigError={codexCommonConfigError}
|
commonConfigError={codexCommonConfigError}
|
||||||
authError={codexAuthError}
|
authError={codexAuthError}
|
||||||
|
isCustomMode={selectedCodexPreset === -1}
|
||||||
|
onWebsiteUrlChange={(url) => {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
websiteUrl: url
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
isTemplateModalOpen={isCodexTemplateModalOpen}
|
||||||
|
setIsTemplateModalOpen={setIsCodexTemplateModalOpen}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,36 +1,102 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
|
|
||||||
import { X, Save } from "lucide-react";
|
import { X, Save } from "lucide-react";
|
||||||
|
|
||||||
import { isLinux } from "../../lib/platform";
|
import { isLinux } from "../../lib/platform";
|
||||||
|
|
||||||
|
import {
|
||||||
|
generateThirdPartyAuth,
|
||||||
|
generateThirdPartyConfig,
|
||||||
|
} from "../../config/codexProviderPresets";
|
||||||
|
|
||||||
interface CodexConfigEditorProps {
|
interface CodexConfigEditorProps {
|
||||||
authValue: string;
|
authValue: string;
|
||||||
|
|
||||||
configValue: string;
|
configValue: string;
|
||||||
|
|
||||||
onAuthChange: (value: string) => void;
|
onAuthChange: (value: string) => void;
|
||||||
|
|
||||||
onConfigChange: (value: string) => void;
|
onConfigChange: (value: string) => void;
|
||||||
|
|
||||||
onAuthBlur?: () => void;
|
onAuthBlur?: () => void;
|
||||||
|
|
||||||
useCommonConfig: boolean;
|
useCommonConfig: boolean;
|
||||||
|
|
||||||
onCommonConfigToggle: (checked: boolean) => void;
|
onCommonConfigToggle: (checked: boolean) => void;
|
||||||
|
|
||||||
commonConfigSnippet: string;
|
commonConfigSnippet: string;
|
||||||
|
|
||||||
onCommonConfigSnippetChange: (value: string) => void;
|
onCommonConfigSnippetChange: (value: string) => void;
|
||||||
|
|
||||||
commonConfigError: string;
|
commonConfigError: string;
|
||||||
|
|
||||||
authError: string;
|
authError: string;
|
||||||
|
|
||||||
|
isCustomMode?: boolean; // 新增:是否为自定义模式
|
||||||
|
|
||||||
|
onWebsiteUrlChange?: (url: string) => void; // 新增:更新网址回调
|
||||||
|
|
||||||
|
isTemplateModalOpen?: boolean; // 新增:模态框状态
|
||||||
|
|
||||||
|
setIsTemplateModalOpen?: (open: boolean) => void; // 新增:设置模态框状态
|
||||||
}
|
}
|
||||||
|
|
||||||
const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||||
authValue,
|
authValue,
|
||||||
|
|
||||||
configValue,
|
configValue,
|
||||||
|
|
||||||
onAuthChange,
|
onAuthChange,
|
||||||
|
|
||||||
onConfigChange,
|
onConfigChange,
|
||||||
|
|
||||||
onAuthBlur,
|
onAuthBlur,
|
||||||
|
|
||||||
useCommonConfig,
|
useCommonConfig,
|
||||||
|
|
||||||
onCommonConfigToggle,
|
onCommonConfigToggle,
|
||||||
|
|
||||||
commonConfigSnippet,
|
commonConfigSnippet,
|
||||||
|
|
||||||
onCommonConfigSnippetChange,
|
onCommonConfigSnippetChange,
|
||||||
|
|
||||||
commonConfigError,
|
commonConfigError,
|
||||||
|
|
||||||
authError,
|
authError,
|
||||||
|
|
||||||
|
onWebsiteUrlChange,
|
||||||
|
|
||||||
|
isTemplateModalOpen: externalTemplateModalOpen,
|
||||||
|
|
||||||
|
setIsTemplateModalOpen: externalSetTemplateModalOpen,
|
||||||
}) => {
|
}) => {
|
||||||
const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false);
|
const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false);
|
||||||
|
|
||||||
|
// 使用内部状态或外部状态
|
||||||
|
|
||||||
|
const [internalTemplateModalOpen, setInternalTemplateModalOpen] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
|
const isTemplateModalOpen =
|
||||||
|
externalTemplateModalOpen ?? internalTemplateModalOpen;
|
||||||
|
|
||||||
|
const setIsTemplateModalOpen =
|
||||||
|
externalSetTemplateModalOpen ?? setInternalTemplateModalOpen;
|
||||||
|
|
||||||
|
const [templateApiKey, setTemplateApiKey] = useState("");
|
||||||
|
|
||||||
|
const [templateProviderName, setTemplateProviderName] = useState("");
|
||||||
|
|
||||||
|
const [templateBaseUrl, setTemplateBaseUrl] = useState("");
|
||||||
|
|
||||||
|
const [templateWebsiteUrl, setTemplateWebsiteUrl] = useState("");
|
||||||
|
|
||||||
|
const [templateModelName, setTemplateModelName] = useState("gpt-5-codex");
|
||||||
|
|
||||||
|
const [templateError, setTemplateError] = useState("");
|
||||||
|
|
||||||
|
// 移除自动填充逻辑,因为现在在点击自定义按钮时就已经填充
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (commonConfigError && !isCommonConfigModalOpen) {
|
if (commonConfigError && !isCommonConfigModalOpen) {
|
||||||
setIsCommonConfigModalOpen(true);
|
setIsCommonConfigModalOpen(true);
|
||||||
@@ -38,16 +104,20 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
|||||||
}, [commonConfigError, isCommonConfigModalOpen]);
|
}, [commonConfigError, isCommonConfigModalOpen]);
|
||||||
|
|
||||||
// 支持按下 ESC 关闭弹窗
|
// 支持按下 ESC 关闭弹窗
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isCommonConfigModalOpen) return;
|
if (!isCommonConfigModalOpen) return;
|
||||||
|
|
||||||
const onKeyDown = (e: KeyboardEvent) => {
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
closeModal();
|
closeModal();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener("keydown", onKeyDown);
|
window.addEventListener("keydown", onKeyDown);
|
||||||
|
|
||||||
return () => window.removeEventListener("keydown", onKeyDown);
|
return () => window.removeEventListener("keydown", onKeyDown);
|
||||||
}, [isCommonConfigModalOpen]);
|
}, [isCommonConfigModalOpen]);
|
||||||
|
|
||||||
@@ -55,6 +125,68 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
|||||||
setIsCommonConfigModalOpen(false);
|
setIsCommonConfigModalOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const applyTemplate = () => {
|
||||||
|
const trimmedKey = templateApiKey.trim();
|
||||||
|
|
||||||
|
const trimmedBaseUrl = templateBaseUrl.trim();
|
||||||
|
|
||||||
|
const trimmedModel = templateModelName.trim();
|
||||||
|
|
||||||
|
if (!trimmedKey || !trimmedBaseUrl || !trimmedModel) {
|
||||||
|
setTemplateError("请填写 API 密钥、API 基础地址和模型名称");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auth = generateThirdPartyAuth(trimmedKey);
|
||||||
|
|
||||||
|
const config = generateThirdPartyConfig(
|
||||||
|
templateProviderName || "custom",
|
||||||
|
|
||||||
|
trimmedBaseUrl,
|
||||||
|
|
||||||
|
trimmedModel
|
||||||
|
);
|
||||||
|
|
||||||
|
onAuthChange(JSON.stringify(auth, null, 2));
|
||||||
|
|
||||||
|
onConfigChange(config);
|
||||||
|
|
||||||
|
if (onWebsiteUrlChange) {
|
||||||
|
const trimmedWebsite = templateWebsiteUrl.trim();
|
||||||
|
|
||||||
|
if (trimmedWebsite) {
|
||||||
|
onWebsiteUrlChange(trimmedWebsite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTemplateApiKey("");
|
||||||
|
|
||||||
|
setTemplateProviderName("");
|
||||||
|
|
||||||
|
setTemplateBaseUrl("");
|
||||||
|
|
||||||
|
setTemplateWebsiteUrl("");
|
||||||
|
|
||||||
|
setTemplateModelName("gpt-5-codex");
|
||||||
|
|
||||||
|
setTemplateError("");
|
||||||
|
|
||||||
|
setIsTemplateModalOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTemplateInputKeyDown = (
|
||||||
|
e: React.KeyboardEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
applyTemplate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleAuthChange = (value: string) => {
|
const handleAuthChange = (value: string) => {
|
||||||
onAuthChange(value);
|
onAuthChange(value);
|
||||||
};
|
};
|
||||||
@@ -76,13 +208,20 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
|||||||
>
|
>
|
||||||
auth.json (JSON) *
|
auth.json (JSON) *
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
id="codexAuth"
|
id="codexAuth"
|
||||||
value={authValue}
|
value={authValue}
|
||||||
onChange={(e) => handleAuthChange(e.target.value)}
|
onChange={(e) => handleAuthChange(e.target.value)}
|
||||||
onBlur={onAuthBlur}
|
onBlur={onAuthBlur}
|
||||||
placeholder={`{
|
placeholder={`{
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"OPENAI_API_KEY": "sk-your-api-key-here"
|
"OPENAI_API_KEY": "sk-your-api-key-here"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}`}
|
}`}
|
||||||
rows={6}
|
rows={6}
|
||||||
required
|
required
|
||||||
@@ -97,9 +236,11 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
|||||||
data-gramm_editor="false"
|
data-gramm_editor="false"
|
||||||
data-enable-grammarly="false"
|
data-enable-grammarly="false"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{authError && (
|
{authError && (
|
||||||
<p className="text-xs text-red-500 dark:text-red-400">{authError}</p>
|
<p className="text-xs text-red-500 dark:text-red-400">{authError}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
Codex auth.json 配置内容
|
Codex auth.json 配置内容
|
||||||
</p>
|
</p>
|
||||||
@@ -113,6 +254,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
|||||||
>
|
>
|
||||||
config.toml (TOML)
|
config.toml (TOML)
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label className="inline-flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 cursor-pointer">
|
<label className="inline-flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -123,6 +265,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
|||||||
写入通用配置
|
写入通用配置
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -132,11 +275,13 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
|||||||
编辑通用配置
|
编辑通用配置
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{commonConfigError && !isCommonConfigModalOpen && (
|
{commonConfigError && !isCommonConfigModalOpen && (
|
||||||
<p className="text-xs text-red-500 dark:text-red-400 text-right">
|
<p className="text-xs text-red-500 dark:text-red-400 text-right">
|
||||||
{commonConfigError}
|
{commonConfigError}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
id="codexConfig"
|
id="codexConfig"
|
||||||
value={configValue}
|
value={configValue}
|
||||||
@@ -154,11 +299,232 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
|||||||
data-gramm_editor="false"
|
data-gramm_editor="false"
|
||||||
data-enable-grammarly="false"
|
data-enable-grammarly="false"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
Codex config.toml 配置内容
|
Codex config.toml 配置内容
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isTemplateModalOpen && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-50 flex items-center justify-center"
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
setIsTemplateModalOpen(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`absolute inset-0 bg-black/50 dark:bg-black/70${
|
||||||
|
isLinux() ? "" : " backdrop-blur-sm"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="relative mx-4 flex max-h-[90vh] w-full max-w-2xl flex-col overflow-hidden rounded-xl bg-white shadow-lg dark:bg-gray-900">
|
||||||
|
<div className="flex h-full min-h-0 flex-col" role="form">
|
||||||
|
<div className="flex items-center justify-between border-b border-gray-200 p-6 dark:border-gray-800">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
||||||
|
快速配置向导
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsTemplateModalOpen(false)}
|
||||||
|
className="rounded-md p-1 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-100"
|
||||||
|
aria-label="关闭"
|
||||||
|
>
|
||||||
|
<X size={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 min-h-0 space-y-4 overflow-auto p-6">
|
||||||
|
<div className="rounded-lg border border-blue-200 bg-blue-50 p-3 dark:border-blue-800 dark:bg-blue-900/20">
|
||||||
|
<p className="text-sm text-blue-800 dark:text-blue-200">
|
||||||
|
输入关键参数,系统将自动生成标准的 auth.json 和 config.toml
|
||||||
|
配置。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
API 密钥 *
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={templateApiKey}
|
||||||
|
onChange={(e) => {
|
||||||
|
setTemplateApiKey(e.target.value);
|
||||||
|
|
||||||
|
setTemplateError("");
|
||||||
|
}}
|
||||||
|
onKeyDown={handleTemplateInputKeyDown}
|
||||||
|
placeholder="sk-your-api-key-here"
|
||||||
|
required
|
||||||
|
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm font-mono text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
供应商名称
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={templateProviderName}
|
||||||
|
onChange={(e) => setTemplateProviderName(e.target.value)}
|
||||||
|
onKeyDown={handleTemplateInputKeyDown}
|
||||||
|
placeholder="custom(可选)"
|
||||||
|
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
将用作配置文件中的标识符,默认为 custom
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
API 基础地址 *
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
value={templateBaseUrl}
|
||||||
|
onChange={(e) => {
|
||||||
|
setTemplateBaseUrl(e.target.value);
|
||||||
|
|
||||||
|
setTemplateError("");
|
||||||
|
}}
|
||||||
|
onKeyDown={handleTemplateInputKeyDown}
|
||||||
|
placeholder="https://your-api-endpoint.com/v1"
|
||||||
|
required
|
||||||
|
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
供应商官网
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
value={templateWebsiteUrl}
|
||||||
|
onChange={(e) => setTemplateWebsiteUrl(e.target.value)}
|
||||||
|
onKeyDown={handleTemplateInputKeyDown}
|
||||||
|
placeholder="https://example.com"
|
||||||
|
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
供应商的官方网站地址(可选)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
模型名称 *
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={templateModelName}
|
||||||
|
onChange={(e) => {
|
||||||
|
setTemplateModelName(e.target.value);
|
||||||
|
|
||||||
|
setTemplateError("");
|
||||||
|
}}
|
||||||
|
onKeyDown={handleTemplateInputKeyDown}
|
||||||
|
placeholder="gpt-5-codex"
|
||||||
|
required
|
||||||
|
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(templateApiKey ||
|
||||||
|
templateProviderName ||
|
||||||
|
templateBaseUrl) && (
|
||||||
|
<div className="space-y-2 border-t border-gray-200 pt-4 dark:border-gray-700">
|
||||||
|
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
配置预览
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||||
|
auth.json
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<pre className="overflow-x-auto rounded-lg bg-gray-50 p-3 text-xs font-mono text-gray-700 dark:bg-gray-800 dark:text-gray-300">
|
||||||
|
{JSON.stringify(
|
||||||
|
generateThirdPartyAuth(templateApiKey),
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||||
|
config.toml
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<pre className="whitespace-pre-wrap rounded-lg bg-gray-50 p-3 text-xs font-mono text-gray-700 dark:bg-gray-800 dark:text-gray-300">
|
||||||
|
{templateProviderName && templateBaseUrl
|
||||||
|
? generateThirdPartyConfig(
|
||||||
|
templateProviderName,
|
||||||
|
|
||||||
|
templateBaseUrl,
|
||||||
|
|
||||||
|
templateModelName
|
||||||
|
)
|
||||||
|
: ""}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{templateError && (
|
||||||
|
<p className="text-sm text-red-500 dark:text-red-400">
|
||||||
|
{templateError}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-end gap-3 border-t border-gray-200 bg-gray-100 p-6 dark:border-gray-800 dark:bg-gray-800">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsTemplateModalOpen(false)}
|
||||||
|
className="rounded-lg px-4 py-2 text-sm font-medium text-gray-500 transition-colors hover:bg-white hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-100"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
applyTemplate();
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-2 rounded-lg bg-blue-500 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
<Save className="h-4 w-4" />
|
||||||
|
应用配置
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{isCommonConfigModalOpen && (
|
{isCommonConfigModalOpen && (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 z-50 flex items-center justify-center"
|
className="fixed inset-0 z-50 flex items-center justify-center"
|
||||||
@@ -167,6 +533,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Backdrop - 统一背景样式 */}
|
{/* Backdrop - 统一背景样式 */}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`absolute inset-0 bg-black/50 dark:bg-black/70${
|
className={`absolute inset-0 bg-black/50 dark:bg-black/70${
|
||||||
isLinux() ? "" : " backdrop-blur-sm"
|
isLinux() ? "" : " backdrop-blur-sm"
|
||||||
@@ -174,12 +541,15 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Modal - 统一窗口样式 */}
|
{/* Modal - 统一窗口样式 */}
|
||||||
|
|
||||||
<div className="relative bg-white dark:bg-gray-900 rounded-xl shadow-lg max-w-2xl w-full mx-4 max-h-[90vh] overflow-hidden flex flex-col">
|
<div className="relative bg-white dark:bg-gray-900 rounded-xl shadow-lg max-w-2xl w-full mx-4 max-h-[90vh] overflow-hidden flex flex-col">
|
||||||
{/* Header - 统一标题栏样式 */}
|
{/* Header - 统一标题栏样式 */}
|
||||||
|
|
||||||
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-800">
|
<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">
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
||||||
编辑 Codex 通用配置片段
|
编辑 Codex 通用配置片段
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={closeModal}
|
onClick={closeModal}
|
||||||
@@ -191,16 +561,21 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content - 统一内容区域样式 */}
|
{/* Content - 统一内容区域样式 */}
|
||||||
|
|
||||||
<div className="flex-1 overflow-auto p-6 space-y-4">
|
<div className="flex-1 overflow-auto p-6 space-y-4">
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
该片段会在勾选"写入通用配置"时追加到 config.toml 末尾
|
该片段会在勾选"写入通用配置"时追加到 config.toml 末尾
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
value={commonConfigSnippet}
|
value={commonConfigSnippet}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleCommonConfigSnippetChange(e.target.value)
|
handleCommonConfigSnippetChange(e.target.value)
|
||||||
}
|
}
|
||||||
placeholder={`# Common Codex config
|
placeholder={`# Common Codex config
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Add your common TOML configuration here`}
|
# Add your common TOML configuration here`}
|
||||||
rows={12}
|
rows={12}
|
||||||
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 font-mono 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 resize-y"
|
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 font-mono 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 resize-y"
|
||||||
@@ -214,6 +589,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
|||||||
data-gramm_editor="false"
|
data-gramm_editor="false"
|
||||||
data-enable-grammarly="false"
|
data-enable-grammarly="false"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{commonConfigError && (
|
{commonConfigError && (
|
||||||
<p className="text-sm text-red-500 dark:text-red-400">
|
<p className="text-sm text-red-500 dark:text-red-400">
|
||||||
{commonConfigError}
|
{commonConfigError}
|
||||||
@@ -222,6 +598,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer - 统一底部按钮样式 */}
|
{/* Footer - 统一底部按钮样式 */}
|
||||||
|
|
||||||
<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">
|
<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">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -230,6 +607,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
|||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={closeModal}
|
onClick={closeModal}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ interface PresetSelectorProps {
|
|||||||
onSelectPreset: (index: number) => void;
|
onSelectPreset: (index: number) => void;
|
||||||
onCustomClick: () => void;
|
onCustomClick: () => void;
|
||||||
customLabel?: string;
|
customLabel?: string;
|
||||||
|
renderCustomDescription?: () => React.ReactNode; // 新增:自定义描述渲染
|
||||||
}
|
}
|
||||||
|
|
||||||
const PresetSelector: React.FC<PresetSelectorProps> = ({
|
const PresetSelector: React.FC<PresetSelectorProps> = ({
|
||||||
@@ -25,6 +26,7 @@ const PresetSelector: React.FC<PresetSelectorProps> = ({
|
|||||||
onSelectPreset,
|
onSelectPreset,
|
||||||
onCustomClick,
|
onCustomClick,
|
||||||
customLabel = "自定义",
|
customLabel = "自定义",
|
||||||
|
renderCustomDescription,
|
||||||
}) => {
|
}) => {
|
||||||
const getButtonClass = (index: number, preset?: Preset) => {
|
const getButtonClass = (index: number, preset?: Preset) => {
|
||||||
const isSelected = selectedIndex === index;
|
const isSelected = selectedIndex === index;
|
||||||
@@ -48,6 +50,10 @@ const PresetSelector: React.FC<PresetSelectorProps> = ({
|
|||||||
|
|
||||||
const getDescription = () => {
|
const getDescription = () => {
|
||||||
if (selectedIndex === -1) {
|
if (selectedIndex === -1) {
|
||||||
|
// 如果提供了自定义描述渲染函数,使用它
|
||||||
|
if (renderCustomDescription) {
|
||||||
|
return renderCustomDescription();
|
||||||
|
}
|
||||||
return "手动配置供应商,需要填写完整的配置信息";
|
return "手动配置供应商,需要填写完整的配置信息";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,9 +105,9 @@ const PresetSelector: React.FC<PresetSelectorProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{getDescription() && (
|
{getDescription() && (
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
{getDescription()}
|
{getDescription()}
|
||||||
</p>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,6 +10,43 @@ export interface CodexProviderPreset {
|
|||||||
config: string; // 将写入 ~/.codex/config.toml(TOML 字符串)
|
config: string; // 将写入 ~/.codex/config.toml(TOML 字符串)
|
||||||
isOfficial?: boolean; // 标识是否为官方预设
|
isOfficial?: boolean; // 标识是否为官方预设
|
||||||
category?: ProviderCategory; // 新增:分类
|
category?: ProviderCategory; // 新增:分类
|
||||||
|
isCustomTemplate?: boolean; // 标识是否为自定义模板
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成第三方供应商的 auth.json
|
||||||
|
*/
|
||||||
|
export function generateThirdPartyAuth(apiKey: string): Record<string, any> {
|
||||||
|
return {
|
||||||
|
OPENAI_API_KEY: apiKey || "sk-your-api-key-here"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成第三方供应商的 config.toml
|
||||||
|
*/
|
||||||
|
export function generateThirdPartyConfig(
|
||||||
|
providerName: string,
|
||||||
|
baseUrl: string,
|
||||||
|
modelName = "gpt-5-codex"
|
||||||
|
): string {
|
||||||
|
// 清理供应商名称,确保符合TOML键名规范
|
||||||
|
const cleanProviderName = providerName
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9_]/g, '_')
|
||||||
|
.replace(/^_+|_+$/g, '') || 'custom';
|
||||||
|
|
||||||
|
return `model_provider = "${cleanProviderName}"
|
||||||
|
model = "${modelName}"
|
||||||
|
model_reasoning_effort = "high"
|
||||||
|
disable_response_storage = true
|
||||||
|
requires_openai_auth = true
|
||||||
|
|
||||||
|
[model_providers.${cleanProviderName}]
|
||||||
|
name = "${cleanProviderName}"
|
||||||
|
base_url = "${baseUrl}"
|
||||||
|
wire_api = "responses"
|
||||||
|
env_key = "${cleanProviderName}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const codexProviderPresets: CodexProviderPreset[] = [
|
export const codexProviderPresets: CodexProviderPreset[] = [
|
||||||
@@ -18,7 +55,6 @@ export const codexProviderPresets: CodexProviderPreset[] = [
|
|||||||
websiteUrl: "https://chatgpt.com/codex",
|
websiteUrl: "https://chatgpt.com/codex",
|
||||||
isOfficial: true,
|
isOfficial: true,
|
||||||
category: "official",
|
category: "official",
|
||||||
// 官方的 key 为null
|
|
||||||
auth: {
|
auth: {
|
||||||
OPENAI_API_KEY: null,
|
OPENAI_API_KEY: null,
|
||||||
},
|
},
|
||||||
@@ -28,21 +64,11 @@ export const codexProviderPresets: CodexProviderPreset[] = [
|
|||||||
name: "PackyCode",
|
name: "PackyCode",
|
||||||
websiteUrl: "https://codex.packycode.com/",
|
websiteUrl: "https://codex.packycode.com/",
|
||||||
category: "third_party",
|
category: "third_party",
|
||||||
// PackyCode 一般通过 API Key;请将占位符替换为你的实际 key
|
auth: generateThirdPartyAuth("sk-your-api-key-here"),
|
||||||
auth: {
|
config: generateThirdPartyConfig(
|
||||||
OPENAI_API_KEY: "sk-your-api-key-here",
|
"packycode",
|
||||||
},
|
"https://codex-api.packycode.com/v1",
|
||||||
config: `model_provider = "packycode"
|
"gpt-5-codex"
|
||||||
model = "gpt-5-codex"
|
),
|
||||||
model_reasoning_effort = "high"
|
|
||||||
disable_response_storage = true
|
|
||||||
requires_openai_auth = true
|
|
||||||
|
|
||||||
|
|
||||||
[model_providers.packycode]
|
|
||||||
name = "packycode"
|
|
||||||
base_url = "https://codex-api.packycode.com/v1"
|
|
||||||
wire_api = "responses"
|
|
||||||
env_key = "packycode"`,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user