- 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

@@ -14,11 +14,53 @@
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
user-select: none;
min-height: 3rem;
position: relative;
}
.app-tabs {
position: absolute;
left: 2rem;
top: 0;
display: flex;
height: 100%;
}
.app-tab {
background: transparent;
color: rgba(255, 255, 255, 0.7);
border: none;
padding: 0 1.5rem;
cursor: pointer;
font-size: 0.95rem;
font-weight: 500;
transition: all 0.2s;
position: relative;
}
.app-tab:hover {
color: white;
background: rgba(255, 255, 255, 0.1);
}
.app-tab.active {
color: white;
background: rgba(255, 255, 255, 0.15);
}
.app-tab.active::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 3px;
background: white;
}
.app-header h1 {
font-size: 1.5rem;
font-weight: 500;
margin: 0 auto;
}
.header-actions {

View File

@@ -1,5 +1,6 @@
import { useState, useEffect, useRef } from "react";
import { Provider } from "./types";
import { AppType } from "./lib/tauri-api";
import ProviderList from "./components/ProviderList";
import AddProviderModal from "./components/AddProviderModal";
import EditProviderModal from "./components/EditProviderModal";
@@ -7,6 +8,7 @@ import { ConfirmDialog } from "./components/ConfirmDialog";
import "./App.css";
function App() {
const [activeApp, setActiveApp] = useState<AppType>("claude");
const [providers, setProviders] = useState<Record<string, Provider>>({});
const [currentProviderId, setCurrentProviderId] = useState<string>("");
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
@@ -60,7 +62,7 @@ function App() {
useEffect(() => {
loadProviders();
loadConfigStatus();
}, []);
}, [activeApp]); // 当切换应用时重新加载
// 清理定时器
useEffect(() => {
@@ -72,8 +74,8 @@ function App() {
}, []);
const loadProviders = async () => {
const loadedProviders = await window.api.getProviders();
const currentId = await window.api.getCurrentProvider();
const loadedProviders = await window.api.getProviders(activeApp);
const currentId = await window.api.getCurrentProvider(activeApp);
setProviders(loadedProviders);
setCurrentProviderId(currentId);
@@ -84,7 +86,7 @@ function App() {
};
const loadConfigStatus = async () => {
const status = await window.api.getClaudeConfigStatus();
const status = await window.api.getConfigStatus(activeApp);
setConfigStatus({
exists: Boolean(status?.exists),
path: String(status?.path || ""),
@@ -101,14 +103,14 @@ function App() {
...provider,
id: generateId(),
};
await window.api.addProvider(newProvider);
await window.api.addProvider(newProvider, activeApp);
await loadProviders();
setIsAddModalOpen(false);
};
const handleEditProvider = async (provider: Provider) => {
try {
await window.api.updateProvider(provider);
await window.api.updateProvider(provider, activeApp);
await loadProviders();
setEditingProviderId(null);
// 显示编辑成功提示
@@ -127,7 +129,7 @@ function App() {
title: "删除供应商",
message: `确定要删除供应商 "${provider?.name}" 吗?此操作无法撤销。`,
onConfirm: async () => {
await window.api.deleteProvider(id);
await window.api.deleteProvider(id, activeApp);
await loadProviders();
setConfirmDialog(null);
showNotification("供应商删除成功", "success");
@@ -136,12 +138,13 @@ function App() {
};
const handleSwitchProvider = async (id: string) => {
const success = await window.api.switchProvider(id);
const success = await window.api.switchProvider(id, activeApp);
if (success) {
setCurrentProviderId(id);
// 显示重启提示
const appName = activeApp === "claude" ? "Claude Code" : "Codex";
showNotification(
"切换成功!请重启 Claude Code 终端以生效",
`切换成功!请重启 ${appName} 终端以生效`,
"success",
2000,
);
@@ -153,7 +156,7 @@ function App() {
// 自动导入现有配置为"default"供应商
const handleAutoImportDefault = async () => {
try {
const result = await window.api.importCurrentConfigAsDefault();
const result = await window.api.importCurrentConfigAsDefault(activeApp);
if (result.success) {
await loadProviders();
@@ -171,13 +174,27 @@ function App() {
};
const handleOpenConfigFolder = async () => {
await window.api.openConfigFolder();
await window.api.openConfigFolder(activeApp);
};
return (
<div className="app">
<header className="app-header">
<h1>Claude Code </h1>
<div className="app-tabs">
<button
className={`app-tab ${activeApp === "claude" ? "active" : ""}`}
onClick={() => setActiveApp("claude")}
>
Claude Code
</button>
<button
className={`app-tab ${activeApp === "codex" ? "active" : ""}`}
onClick={() => setActiveApp("codex")}
>
Codex
</button>
</div>
<h1>{activeApp === "claude" ? "Claude Code" : "Codex"} </h1>
<div className="header-actions">
<button className="add-btn" onClick={() => setIsAddModalOpen(true)}>
@@ -228,6 +245,7 @@ function App() {
{isAddModalOpen && (
<AddProviderModal
appType={activeApp}
onAdd={handleAddProvider}
onClose={() => setIsAddModalOpen(false)}
/>
@@ -235,6 +253,7 @@ function App() {
{editingProviderId && providers[editingProviderId] && (
<EditProviderModal
appType={activeApp}
provider={providers[editingProviderId]}
onSave={handleEditProvider}
onClose={() => setEditingProviderId(null)}

View File

@@ -1,18 +1,22 @@
import React from "react";
import { Provider } from "../types";
import { AppType } from "../lib/tauri-api";
import ProviderForm from "./ProviderForm";
interface AddProviderModalProps {
appType: AppType;
onAdd: (provider: Omit<Provider, "id">) => void;
onClose: () => void;
}
const AddProviderModal: React.FC<AddProviderModalProps> = ({
appType,
onAdd,
onClose,
}) => {
return (
<ProviderForm
appType={appType}
title="添加新供应商"
submitText="添加"
showPresets={true}

View File

@@ -1,14 +1,17 @@
import React from "react";
import { Provider } from "../types";
import { AppType } from "../lib/tauri-api";
import ProviderForm from "./ProviderForm";
interface EditProviderModalProps {
appType: AppType;
provider: Provider;
onSave: (provider: Provider) => void;
onClose: () => void;
}
const EditProviderModal: React.FC<EditProviderModalProps> = ({
appType,
provider,
onSave,
onClose,
@@ -22,6 +25,7 @@ const EditProviderModal: React.FC<EditProviderModalProps> = ({
return (
<ProviderForm
appType={appType}
title="编辑供应商"
submitText="保存"
initialData={provider}

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">

View File

@@ -1,6 +1,9 @@
import { invoke } from "@tauri-apps/api/core";
import { Provider } from "../types";
// 应用类型
export type AppType = "claude" | "codex";
// 定义配置状态类型
interface ConfigStatus {
exists: boolean;
@@ -17,9 +20,9 @@ interface ImportResult {
// Tauri API 封装,提供统一的全局 API 接口
export const tauriAPI = {
// 获取所有供应商
getProviders: async (): Promise<Record<string, Provider>> => {
getProviders: async (app?: AppType): Promise<Record<string, Provider>> => {
try {
return await invoke("get_providers");
return await invoke("get_providers", { app });
} catch (error) {
console.error("获取供应商列表失败:", error);
return {};
@@ -27,9 +30,9 @@ export const tauriAPI = {
},
// 获取当前供应商ID
getCurrentProvider: async (): Promise<string> => {
getCurrentProvider: async (app?: AppType): Promise<string> => {
try {
return await invoke("get_current_provider");
return await invoke("get_current_provider", { app });
} catch (error) {
console.error("获取当前供应商失败:", error);
return "";
@@ -37,9 +40,9 @@ export const tauriAPI = {
},
// 添加供应商
addProvider: async (provider: Provider): Promise<boolean> => {
addProvider: async (provider: Provider, app?: AppType): Promise<boolean> => {
try {
return await invoke("add_provider", { provider });
return await invoke("add_provider", { provider, app });
} catch (error) {
console.error("添加供应商失败:", error);
throw error;
@@ -47,9 +50,12 @@ export const tauriAPI = {
},
// 更新供应商
updateProvider: async (provider: Provider): Promise<boolean> => {
updateProvider: async (
provider: Provider,
app?: AppType,
): Promise<boolean> => {
try {
return await invoke("update_provider", { provider });
return await invoke("update_provider", { provider, app });
} catch (error) {
console.error("更新供应商失败:", error);
throw error;
@@ -57,9 +63,9 @@ export const tauriAPI = {
},
// 删除供应商
deleteProvider: async (id: string): Promise<boolean> => {
deleteProvider: async (id: string, app?: AppType): Promise<boolean> => {
try {
return await invoke("delete_provider", { id });
return await invoke("delete_provider", { id, app });
} catch (error) {
console.error("删除供应商失败:", error);
throw error;
@@ -67,9 +73,12 @@ export const tauriAPI = {
},
// 切换供应商
switchProvider: async (providerId: string): Promise<boolean> => {
switchProvider: async (
providerId: string,
app?: AppType,
): Promise<boolean> => {
try {
return await invoke("switch_provider", { id: providerId });
return await invoke("switch_provider", { id: providerId, app });
} catch (error) {
console.error("切换供应商失败:", error);
return false;
@@ -77,9 +86,11 @@ export const tauriAPI = {
},
// 导入当前配置为默认供应商
importCurrentConfigAsDefault: async (): Promise<ImportResult> => {
importCurrentConfigAsDefault: async (
app?: AppType,
): Promise<ImportResult> => {
try {
const success = await invoke<boolean>("import_default_config");
const success = await invoke<boolean>("import_default_config", { app });
return {
success,
message: success ? "成功导入默认配置" : "导入失败",
@@ -117,10 +128,24 @@ export const tauriAPI = {
}
},
// 打开配置文件夹
openConfigFolder: async (): Promise<void> => {
// 获取应用配置状态(通用)
getConfigStatus: async (app?: AppType): Promise<ConfigStatus> => {
try {
await invoke("open_config_folder");
return await invoke("get_config_status", { appType: app });
} catch (error) {
console.error("获取配置状态失败:", error);
return {
exists: false,
path: "",
error: String(error),
};
}
},
// 打开配置文件夹
openConfigFolder: async (app?: AppType): Promise<void> => {
try {
await invoke("open_config_folder", { appType: app });
} catch (error) {
console.error("打开配置文件夹失败:", error);
}

18
src/vite-env.d.ts vendored
View File

@@ -1,6 +1,7 @@
/// <reference types="vite/client" />
import { Provider } from "./types";
import { AppType } from "./lib/tauri-api";
interface ImportResult {
success: boolean;
@@ -16,17 +17,18 @@ interface ConfigStatus {
declare global {
interface Window {
api: {
getProviders: () => Promise<Record<string, Provider>>;
getCurrentProvider: () => Promise<string>;
addProvider: (provider: Provider) => Promise<boolean>;
deleteProvider: (id: string) => Promise<boolean>;
updateProvider: (provider: Provider) => Promise<boolean>;
switchProvider: (providerId: string) => Promise<boolean>;
importCurrentConfigAsDefault: () => Promise<ImportResult>;
getProviders: (app?: AppType) => Promise<Record<string, Provider>>;
getCurrentProvider: (app?: AppType) => Promise<string>;
addProvider: (provider: Provider, app?: AppType) => Promise<boolean>;
deleteProvider: (id: string, app?: AppType) => Promise<boolean>;
updateProvider: (provider: Provider, app?: AppType) => Promise<boolean>;
switchProvider: (providerId: string, app?: AppType) => Promise<boolean>;
importCurrentConfigAsDefault: (app?: AppType) => Promise<ImportResult>;
getClaudeCodeConfigPath: () => Promise<string>;
getClaudeConfigStatus: () => Promise<ConfigStatus>;
getConfigStatus: (app?: AppType) => Promise<ConfigStatus>;
selectConfigFile: () => Promise<string | null>;
openConfigFolder: () => Promise<void>;
openConfigFolder: (app?: AppType) => Promise<void>;
openExternal: (url: string) => Promise<void>;
};
platform: {