feat(codex): 增加 Codex 预设供应商(官方、PackyCode);在添加供应商时支持一键预设与 API Key 自动写入 auth.json;UI 同步 Codex 预设按钮与字段
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
|||||||
setApiKeyInConfig,
|
setApiKeyInConfig,
|
||||||
} from "../utils/providerConfigUtils";
|
} from "../utils/providerConfigUtils";
|
||||||
import { providerPresets } from "../config/providerPresets";
|
import { providerPresets } from "../config/providerPresets";
|
||||||
|
import { codexProviderPresets } from "../config/codexProviderPresets";
|
||||||
import "./AddProviderModal.css";
|
import "./AddProviderModal.css";
|
||||||
|
|
||||||
interface ProviderFormProps {
|
interface ProviderFormProps {
|
||||||
@@ -45,6 +46,10 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
// Codex 特有的状态
|
// Codex 特有的状态
|
||||||
const [codexAuth, setCodexAuth] = useState("");
|
const [codexAuth, setCodexAuth] = useState("");
|
||||||
const [codexConfig, setCodexConfig] = useState("");
|
const [codexConfig, setCodexConfig] = useState("");
|
||||||
|
const [codexApiKey, setCodexApiKey] = useState("");
|
||||||
|
const [selectedCodexPreset, setSelectedCodexPreset] = useState<number | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
// 初始化 Codex 配置
|
// 初始化 Codex 配置
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -53,6 +58,14 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
if (typeof config === "object" && config !== null) {
|
if (typeof config === "object" && config !== null) {
|
||||||
setCodexAuth(JSON.stringify(config.auth || {}, null, 2));
|
setCodexAuth(JSON.stringify(config.auth || {}, null, 2));
|
||||||
setCodexConfig(config.config || "");
|
setCodexConfig(config.config || "");
|
||||||
|
try {
|
||||||
|
const auth = config.auth || {};
|
||||||
|
if (auth && typeof auth.api_key === "string") {
|
||||||
|
setCodexApiKey(auth.api_key);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isCodex, initialData]);
|
}, [isCodex, initialData]);
|
||||||
@@ -186,6 +199,33 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
setDisableCoAuthored(hasCoAuthoredDisabled);
|
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 输入框
|
||||||
|
try {
|
||||||
|
const auth = JSON.parse(authString);
|
||||||
|
const key = typeof auth.api_key === "string" ? auth.api_key : "";
|
||||||
|
setCodexApiKey(key);
|
||||||
|
} catch {
|
||||||
|
setCodexApiKey("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 处理 API Key 输入并自动更新配置
|
// 处理 API Key 输入并自动更新配置
|
||||||
const handleApiKeyChange = (key: string) => {
|
const handleApiKeyChange = (key: string) => {
|
||||||
setApiKey(key);
|
setApiKey(key);
|
||||||
@@ -207,6 +247,18 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
setDisableCoAuthored(hasCoAuthoredDisabled);
|
setDisableCoAuthored(hasCoAuthoredDisabled);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Codex: 处理 API Key 输入并写回 auth.json
|
||||||
|
const handleCodexApiKeyChange = (key: string) => {
|
||||||
|
setCodexApiKey(key);
|
||||||
|
try {
|
||||||
|
const auth = JSON.parse(codexAuth || "{}");
|
||||||
|
auth.api_key = key.trim();
|
||||||
|
setCodexAuth(JSON.stringify(auth, null, 2));
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 根据当前配置决定是否展示 API Key 输入框
|
// 根据当前配置决定是否展示 API Key 输入框
|
||||||
const showApiKey =
|
const showApiKey =
|
||||||
selectedPreset !== null || hasApiKeyField(formData.settingsConfig);
|
selectedPreset !== null || hasApiKeyField(formData.settingsConfig);
|
||||||
@@ -216,6 +268,21 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
selectedPreset !== null &&
|
selectedPreset !== null &&
|
||||||
providerPresets[selectedPreset]?.isOfficial === true;
|
providerPresets[selectedPreset]?.isOfficial === true;
|
||||||
|
|
||||||
|
// Codex: 控制显示 API Key 与官方标记
|
||||||
|
const getCodexAuthApiKey = (authString: string): string => {
|
||||||
|
try {
|
||||||
|
const auth = JSON.parse(authString || "{}");
|
||||||
|
return typeof auth.api_key === "string" ? auth.api_key : "";
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const showCodexApiKey =
|
||||||
|
selectedCodexPreset !== null || getCodexAuthApiKey(codexAuth) !== "";
|
||||||
|
const isCodexOfficialPreset =
|
||||||
|
selectedCodexPreset !== null &&
|
||||||
|
codexProviderPresets[selectedCodexPreset]?.isOfficial === true;
|
||||||
|
|
||||||
// 初始时从配置中同步 API Key(编辑模式)
|
// 初始时从配置中同步 API Key(编辑模式)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialData) {
|
if (initialData) {
|
||||||
@@ -289,6 +356,26 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showPresets && isCodex && (
|
||||||
|
<div className="presets">
|
||||||
|
<label>一键导入(只需要填写 key)</label>
|
||||||
|
<div className="preset-buttons">
|
||||||
|
{codexProviderPresets.map((preset, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
type="button"
|
||||||
|
className={`preset-btn ${
|
||||||
|
selectedCodexPreset === index ? "selected" : ""
|
||||||
|
} ${preset.isOfficial ? "official" : ""}`}
|
||||||
|
onClick={() => applyCodexPreset(preset, index)}
|
||||||
|
>
|
||||||
|
{preset.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="name">供应商名称 *</label>
|
<label htmlFor="name">供应商名称 *</label>
|
||||||
<input
|
<input
|
||||||
@@ -333,6 +420,36 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isCodex && (
|
||||||
|
<div
|
||||||
|
className={`form-group api-key-group ${!showCodexApiKey ? "hidden" : ""}`}
|
||||||
|
>
|
||||||
|
<label htmlFor="codexApiKey">API Key *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="codexApiKey"
|
||||||
|
value={codexApiKey}
|
||||||
|
onChange={(e) => handleCodexApiKeyChange(e.target.value)}
|
||||||
|
placeholder={
|
||||||
|
isCodexOfficialPreset
|
||||||
|
? "官方无需填写 API Key,直接保存即可"
|
||||||
|
: "只需要填这里,上方 auth.json 会自动填充"
|
||||||
|
}
|
||||||
|
disabled={isCodexOfficialPreset}
|
||||||
|
autoComplete="off"
|
||||||
|
style={
|
||||||
|
isCodexOfficialPreset
|
||||||
|
? {
|
||||||
|
backgroundColor: "#f5f5f5",
|
||||||
|
cursor: "not-allowed",
|
||||||
|
color: "#999",
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="websiteUrl">官网地址</label>
|
<label htmlFor="websiteUrl">官网地址</label>
|
||||||
<input
|
<input
|
||||||
@@ -355,7 +472,17 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
<textarea
|
<textarea
|
||||||
id="codexAuth"
|
id="codexAuth"
|
||||||
value={codexAuth}
|
value={codexAuth}
|
||||||
onChange={(e) => setCodexAuth(e.target.value)}
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
setCodexAuth(value);
|
||||||
|
try {
|
||||||
|
const auth = JSON.parse(value || "{}");
|
||||||
|
const key = typeof auth.api_key === "string" ? auth.api_key : "";
|
||||||
|
setCodexApiKey(key);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}}
|
||||||
placeholder={`{
|
placeholder={`{
|
||||||
"api_key": "your-codex-api-key"
|
"api_key": "your-codex-api-key"
|
||||||
}`}
|
}`}
|
||||||
|
|||||||
31
src/config/codexProviderPresets.ts
Normal file
31
src/config/codexProviderPresets.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Codex 预设供应商配置模板
|
||||||
|
*/
|
||||||
|
export interface CodexProviderPreset {
|
||||||
|
name: string;
|
||||||
|
websiteUrl: string;
|
||||||
|
auth: Record<string, any>; // 将写入 ~/.codex/auth.json
|
||||||
|
config: string; // 将写入 ~/.codex/config.toml(TOML 字符串)
|
||||||
|
isOfficial?: boolean; // 标识是否为官方预设
|
||||||
|
}
|
||||||
|
|
||||||
|
export const codexProviderPresets: CodexProviderPreset[] = [
|
||||||
|
{
|
||||||
|
name: "Codex官方",
|
||||||
|
websiteUrl: "https://codex",
|
||||||
|
isOfficial: true,
|
||||||
|
// 官方一般不需要在 auth.json 里预置 key,由用户根据实际环境填写
|
||||||
|
auth: {},
|
||||||
|
config: `# Codex 默认配置模板\n# 根据你的 Codex 安装或文档进行调整\nmodel = "default"\ntemperature = 0.7`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PackyCode",
|
||||||
|
websiteUrl: "https://www.packycode.com",
|
||||||
|
// PackyCode 一般通过 API Key;请将占位符替换为你的实际 key
|
||||||
|
auth: {
|
||||||
|
api_key: "sk-your-api-key-here",
|
||||||
|
},
|
||||||
|
config: `# Codex 配置模板 - PackyCode\n# 如有需要可添加 base_url: \n# base_url = "https://api.packycode.com"\nmodel = "default"\ntemperature = 0.7`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
Reference in New Issue
Block a user