- feat(codex): 引入 Codex 应用与供应商切换(管理 auth.json/config.toml,支持备份与恢复)

- feat(core): 多应用配置 v2(claude/codex)与 ProviderManager;支持 v1→v2 自动迁移
- feat(ui): 新增 Codex 页签与双编辑器表单;统一 window.api 支持 app 参数
- feat(tauri): 新增 get_config_status/open_config_folder/open_external 命令并适配 Codex
- fix(codex): 主配置缺失时不执行默认导入(对齐 Claude 行为)
- chore: 配置目录展示与重启提示等细节优化
This commit is contained in:
Jason
2025-08-30 21:54:11 +08:00
parent 0e803b53d8
commit c10ace7a84
13 changed files with 891 additions and 279 deletions

View File

@@ -1,5 +1,6 @@
import React, { useState, useEffect } from "react";
import { Provider } from "../types";
import { AppType } from "../lib/tauri-api";
import {
updateCoAuthoredSetting,
checkCoAuthoredSetting,
@@ -12,6 +13,7 @@ import { providerPresets } from "../config/providerPresets";
import "./AddProviderModal.css";
interface ProviderFormProps {
appType?: AppType;
title: string;
submitText: string;
initialData?: Provider;
@@ -21,6 +23,7 @@ interface ProviderFormProps {
}
const ProviderForm: React.FC<ProviderFormProps> = ({
appType = "claude",
title,
submitText,
initialData,
@@ -28,6 +31,9 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
onSubmit,
onClose,
}) => {
// 对于 Codex需要分离 auth 和 config
const isCodex = appType === "codex";
const [formData, setFormData] = useState({
name: initialData?.name || "",
websiteUrl: initialData?.websiteUrl || "",
@@ -35,6 +41,21 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
? JSON.stringify(initialData.settingsConfig, null, 2)
: "",
});
// Codex 特有的状态
const [codexAuth, setCodexAuth] = useState("");
const [codexConfig, setCodexConfig] = useState("");
// 初始化 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 || "");
}
}
}, [isCodex, initialData]);
const [error, setError] = useState("");
const [disableCoAuthored, setDisableCoAuthored] = useState(false);
const [selectedPreset, setSelectedPreset] = useState<number | null>(null);
@@ -58,18 +79,38 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
return;
}
if (!formData.settingsConfig.trim()) {
setError("请填写配置内容");
return;
}
let settingsConfig: Record<string, any>;
try {
settingsConfig = JSON.parse(formData.settingsConfig);
} catch (err) {
setError("配置JSON格式错误请检查语法");
return;
if (isCodex) {
// Codex: 验证两个文件
if (!codexAuth.trim() || !codexConfig.trim()) {
setError("请填写 auth.json 和 config.toml 配置");
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({
@@ -226,7 +267,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
<div className="modal-body">
{error && <div className="error-message">{error}</div>}
{showPresets && (
{showPresets && !isCodex && (
<div className="presets">
<label> key</label>
<div className="preset-buttons">
@@ -262,33 +303,35 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
/>
</div>
<div
className={`form-group api-key-group ${!showApiKey ? "hidden" : ""}`}
>
<label htmlFor="apiKey">API Key *</label>
<input
type="text"
id="apiKey"
value={apiKey}
onChange={(e) => handleApiKeyChange(e.target.value)}
placeholder={
isOfficialPreset
? "官方登录无需填写 API Key直接保存即可"
: "只需要填这里,下方配置会自动填充"
}
disabled={isOfficialPreset}
autoComplete="off"
style={
isOfficialPreset
? {
backgroundColor: "#f5f5f5",
cursor: "not-allowed",
color: "#999",
}
: {}
}
/>
</div>
{!isCodex && (
<div
className={`form-group api-key-group ${!showApiKey ? "hidden" : ""}`}
>
<label htmlFor="apiKey">API Key *</label>
<input
type="text"
id="apiKey"
value={apiKey}
onChange={(e) => handleApiKeyChange(e.target.value)}
placeholder={
isOfficialPreset
? "官方登录无需填写 API Key直接保存即可"
: "只需要填这里,下方配置会自动填充"
}
disabled={isOfficialPreset}
autoComplete="off"
style={
isOfficialPreset
? {
backgroundColor: "#f5f5f5",
cursor: "not-allowed",
color: "#999",
}
: {}
}
/>
</div>
)}
<div className="form-group">
<label htmlFor="websiteUrl"></label>
@@ -303,39 +346,80 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
/>
</div>
<div className="form-group">
<div className="label-with-checkbox">
<label htmlFor="settingsConfig">
Claude Code (JSON) *
</label>
<label className="checkbox-label">
<input
type="checkbox"
checked={disableCoAuthored}
onChange={(e) => handleCoAuthoredToggle(e.target.checked)}
{/* Claude 或 Codex 的配置部分 */}
{isCodex ? (
// Codex: 双编辑器
<>
<div className="form-group">
<label htmlFor="codexAuth">auth.json (JSON) *</label>
<textarea
id="codexAuth"
value={codexAuth}
onChange={(e) => setCodexAuth(e.target.value)}
placeholder={`{
"api_key": "your-codex-api-key"
}`}
rows={6}
style={{ fontFamily: "monospace", fontSize: "14px" }}
required
/>
Claude Code
</label>
</div>
<textarea
id="settingsConfig"
name="settingsConfig"
value={formData.settingsConfig}
onChange={handleChange}
placeholder={`{
<small className="field-hint">Codex auth.json </small>
</div>
<div className="form-group">
<label htmlFor="codexConfig">config.toml (TOML) *</label>
<textarea
id="codexConfig"
value={codexConfig}
onChange={(e) => setCodexConfig(e.target.value)}
placeholder={`# Codex configuration
model = "codex-model"
temperature = 0.7`}
rows={8}
style={{ fontFamily: "monospace", fontSize: "14px" }}
required
/>
<small className="field-hint">
Codex config.toml
</small>
</div>
</>
) : (
// Claude: 原有的单编辑器
<div className="form-group">
<div className="label-with-checkbox">
<label htmlFor="settingsConfig">
Claude Code (JSON) *
</label>
<label className="checkbox-label">
<input
type="checkbox"
checked={disableCoAuthored}
onChange={(e) => handleCoAuthoredToggle(e.target.checked)}
/>
Claude Code
</label>
</div>
<textarea
id="settingsConfig"
name="settingsConfig"
value={formData.settingsConfig}
onChange={handleChange}
placeholder={`{
"env": {
"ANTHROPIC_BASE_URL": "https://api.anthropic.com",
"ANTHROPIC_AUTH_TOKEN": "sk-your-api-key-here"
}
}`}
rows={12}
style={{ fontFamily: "monospace", fontSize: "14px" }}
required
/>
<small className="field-hint">
Claude Code settings.json
</small>
</div>
rows={12}
style={{ fontFamily: "monospace", fontSize: "14px" }}
required
/>
<small className="field-hint">
Claude Code settings.json
</small>
</div>
)}
</div>
<div className="modal-footer">