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,
|
||||
} from "../utils/providerConfigUtils";
|
||||
import { providerPresets } from "../config/providerPresets";
|
||||
import { codexProviderPresets } from "../config/codexProviderPresets";
|
||||
import { codexProviderPresets, generateThirdPartyAuth, generateThirdPartyConfig } from "../config/codexProviderPresets";
|
||||
import PresetSelector from "./ProviderForm/PresetSelector";
|
||||
import ApiKeyInput from "./ProviderForm/ApiKeyInput";
|
||||
import ClaudeConfigEditor from "./ProviderForm/ClaudeConfigEditor";
|
||||
@@ -72,6 +72,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
const [codexAuth, setCodexAuthState] = useState("");
|
||||
const [codexConfig, setCodexConfigState] = useState("");
|
||||
const [codexApiKey, setCodexApiKey] = useState("");
|
||||
const [isCodexTemplateModalOpen, setIsCodexTemplateModalOpen] = useState(false);
|
||||
// -1 表示自定义,null 表示未选择,>= 0 表示预设索引
|
||||
const [selectedCodexPreset, setSelectedCodexPreset] = useState<number | null>(
|
||||
showPresets && isCodex ? -1 : null,
|
||||
@@ -628,14 +629,23 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
// Codex: 处理点击自定义按钮
|
||||
const handleCodexCustomClick = () => {
|
||||
setSelectedCodexPreset(-1);
|
||||
|
||||
// 设置自定义模板
|
||||
const customAuth = generateThirdPartyAuth("");
|
||||
const customConfig = generateThirdPartyConfig(
|
||||
"custom",
|
||||
"https://your-api-endpoint.com/v1",
|
||||
"gpt-5-codex"
|
||||
);
|
||||
|
||||
setFormData({
|
||||
name: "",
|
||||
websiteUrl: "",
|
||||
settingsConfig: "",
|
||||
});
|
||||
setSettingsConfigError(validateSettingsConfig(""));
|
||||
setCodexAuth("");
|
||||
setCodexConfig("");
|
||||
setCodexAuth(JSON.stringify(customAuth, null, 2));
|
||||
setCodexConfig(customConfig);
|
||||
setCodexApiKey("");
|
||||
setCategory("custom");
|
||||
};
|
||||
@@ -1027,6 +1037,18 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
applyCodexPreset(codexProviderPresets[index], index)
|
||||
}
|
||||
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}
|
||||
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 { X, Save } from "lucide-react";
|
||||
|
||||
import { isLinux } from "../../lib/platform";
|
||||
|
||||
import {
|
||||
generateThirdPartyAuth,
|
||||
generateThirdPartyConfig,
|
||||
} from "../../config/codexProviderPresets";
|
||||
|
||||
interface CodexConfigEditorProps {
|
||||
authValue: string;
|
||||
|
||||
configValue: string;
|
||||
|
||||
onAuthChange: (value: string) => void;
|
||||
|
||||
onConfigChange: (value: string) => void;
|
||||
|
||||
onAuthBlur?: () => void;
|
||||
|
||||
useCommonConfig: boolean;
|
||||
|
||||
onCommonConfigToggle: (checked: boolean) => void;
|
||||
|
||||
commonConfigSnippet: string;
|
||||
|
||||
onCommonConfigSnippetChange: (value: string) => void;
|
||||
|
||||
commonConfigError: string;
|
||||
|
||||
authError: string;
|
||||
|
||||
isCustomMode?: boolean; // 新增:是否为自定义模式
|
||||
|
||||
onWebsiteUrlChange?: (url: string) => void; // 新增:更新网址回调
|
||||
|
||||
isTemplateModalOpen?: boolean; // 新增:模态框状态
|
||||
|
||||
setIsTemplateModalOpen?: (open: boolean) => void; // 新增:设置模态框状态
|
||||
}
|
||||
|
||||
const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
authValue,
|
||||
|
||||
configValue,
|
||||
|
||||
onAuthChange,
|
||||
|
||||
onConfigChange,
|
||||
|
||||
onAuthBlur,
|
||||
|
||||
useCommonConfig,
|
||||
|
||||
onCommonConfigToggle,
|
||||
|
||||
commonConfigSnippet,
|
||||
|
||||
onCommonConfigSnippetChange,
|
||||
|
||||
commonConfigError,
|
||||
|
||||
authError,
|
||||
|
||||
onWebsiteUrlChange,
|
||||
|
||||
isTemplateModalOpen: externalTemplateModalOpen,
|
||||
|
||||
setIsTemplateModalOpen: externalSetTemplateModalOpen,
|
||||
}) => {
|
||||
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(() => {
|
||||
if (commonConfigError && !isCommonConfigModalOpen) {
|
||||
setIsCommonConfigModalOpen(true);
|
||||
@@ -38,16 +104,20 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
}, [commonConfigError, isCommonConfigModalOpen]);
|
||||
|
||||
// 支持按下 ESC 关闭弹窗
|
||||
|
||||
useEffect(() => {
|
||||
if (!isCommonConfigModalOpen) return;
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
|
||||
closeModal();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
|
||||
return () => window.removeEventListener("keydown", onKeyDown);
|
||||
}, [isCommonConfigModalOpen]);
|
||||
|
||||
@@ -55,6 +125,68 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
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) => {
|
||||
onAuthChange(value);
|
||||
};
|
||||
@@ -76,13 +208,20 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
>
|
||||
auth.json (JSON) *
|
||||
</label>
|
||||
|
||||
<textarea
|
||||
id="codexAuth"
|
||||
value={authValue}
|
||||
onChange={(e) => handleAuthChange(e.target.value)}
|
||||
onBlur={onAuthBlur}
|
||||
placeholder={`{
|
||||
|
||||
|
||||
|
||||
"OPENAI_API_KEY": "sk-your-api-key-here"
|
||||
|
||||
|
||||
|
||||
}`}
|
||||
rows={6}
|
||||
required
|
||||
@@ -97,9 +236,11 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
data-gramm_editor="false"
|
||||
data-enable-grammarly="false"
|
||||
/>
|
||||
|
||||
{authError && (
|
||||
<p className="text-xs text-red-500 dark:text-red-400">{authError}</p>
|
||||
)}
|
||||
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Codex auth.json 配置内容
|
||||
</p>
|
||||
@@ -113,6 +254,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
>
|
||||
config.toml (TOML)
|
||||
</label>
|
||||
|
||||
<label className="inline-flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -123,6 +265,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
写入通用配置
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end">
|
||||
<button
|
||||
type="button"
|
||||
@@ -132,11 +275,13 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
编辑通用配置
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{commonConfigError && !isCommonConfigModalOpen && (
|
||||
<p className="text-xs text-red-500 dark:text-red-400 text-right">
|
||||
{commonConfigError}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<textarea
|
||||
id="codexConfig"
|
||||
value={configValue}
|
||||
@@ -154,11 +299,232 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
data-gramm_editor="false"
|
||||
data-enable-grammarly="false"
|
||||
/>
|
||||
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Codex config.toml 配置内容
|
||||
</p>
|
||||
</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 && (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center"
|
||||
@@ -167,6 +533,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
}}
|
||||
>
|
||||
{/* Backdrop - 统一背景样式 */}
|
||||
|
||||
<div
|
||||
className={`absolute inset-0 bg-black/50 dark:bg-black/70${
|
||||
isLinux() ? "" : " backdrop-blur-sm"
|
||||
@@ -174,12 +541,15 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
/>
|
||||
|
||||
{/* 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">
|
||||
{/* Header - 统一标题栏样式 */}
|
||||
|
||||
<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">
|
||||
编辑 Codex 通用配置片段
|
||||
</h2>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeModal}
|
||||
@@ -191,16 +561,21 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Content - 统一内容区域样式 */}
|
||||
|
||||
<div className="flex-1 overflow-auto p-6 space-y-4">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
该片段会在勾选"写入通用配置"时追加到 config.toml 末尾
|
||||
</p>
|
||||
|
||||
<textarea
|
||||
value={commonConfigSnippet}
|
||||
onChange={(e) =>
|
||||
handleCommonConfigSnippetChange(e.target.value)
|
||||
}
|
||||
placeholder={`# Common Codex config
|
||||
|
||||
|
||||
|
||||
# Add your common TOML configuration here`}
|
||||
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"
|
||||
@@ -214,6 +589,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
data-gramm_editor="false"
|
||||
data-enable-grammarly="false"
|
||||
/>
|
||||
|
||||
{commonConfigError && (
|
||||
<p className="text-sm text-red-500 dark:text-red-400">
|
||||
{commonConfigError}
|
||||
@@ -222,6 +598,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
<button
|
||||
type="button"
|
||||
@@ -230,6 +607,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeModal}
|
||||
|
||||
@@ -16,6 +16,7 @@ interface PresetSelectorProps {
|
||||
onSelectPreset: (index: number) => void;
|
||||
onCustomClick: () => void;
|
||||
customLabel?: string;
|
||||
renderCustomDescription?: () => React.ReactNode; // 新增:自定义描述渲染
|
||||
}
|
||||
|
||||
const PresetSelector: React.FC<PresetSelectorProps> = ({
|
||||
@@ -25,6 +26,7 @@ const PresetSelector: React.FC<PresetSelectorProps> = ({
|
||||
onSelectPreset,
|
||||
onCustomClick,
|
||||
customLabel = "自定义",
|
||||
renderCustomDescription,
|
||||
}) => {
|
||||
const getButtonClass = (index: number, preset?: Preset) => {
|
||||
const isSelected = selectedIndex === index;
|
||||
@@ -48,6 +50,10 @@ const PresetSelector: React.FC<PresetSelectorProps> = ({
|
||||
|
||||
const getDescription = () => {
|
||||
if (selectedIndex === -1) {
|
||||
// 如果提供了自定义描述渲染函数,使用它
|
||||
if (renderCustomDescription) {
|
||||
return renderCustomDescription();
|
||||
}
|
||||
return "手动配置供应商,需要填写完整的配置信息";
|
||||
}
|
||||
|
||||
@@ -99,9 +105,9 @@ const PresetSelector: React.FC<PresetSelectorProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
{getDescription() && (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{getDescription()}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -10,6 +10,43 @@ export interface CodexProviderPreset {
|
||||
config: string; // 将写入 ~/.codex/config.toml(TOML 字符串)
|
||||
isOfficial?: boolean; // 标识是否为官方预设
|
||||
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[] = [
|
||||
@@ -18,7 +55,6 @@ export const codexProviderPresets: CodexProviderPreset[] = [
|
||||
websiteUrl: "https://chatgpt.com/codex",
|
||||
isOfficial: true,
|
||||
category: "official",
|
||||
// 官方的 key 为null
|
||||
auth: {
|
||||
OPENAI_API_KEY: null,
|
||||
},
|
||||
@@ -28,21 +64,11 @@ export const codexProviderPresets: CodexProviderPreset[] = [
|
||||
name: "PackyCode",
|
||||
websiteUrl: "https://codex.packycode.com/",
|
||||
category: "third_party",
|
||||
// PackyCode 一般通过 API Key;请将占位符替换为你的实际 key
|
||||
auth: {
|
||||
OPENAI_API_KEY: "sk-your-api-key-here",
|
||||
},
|
||||
config: `model_provider = "packycode"
|
||||
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"`,
|
||||
auth: generateThirdPartyAuth("sk-your-api-key-here"),
|
||||
config: generateThirdPartyConfig(
|
||||
"packycode",
|
||||
"https://codex-api.packycode.com/v1",
|
||||
"gpt-5-codex"
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user