Compare commits
14 Commits
feature/ws
...
v3.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
077d491720 | ||
|
|
7e9930fe50 | ||
|
|
b17d915086 | ||
|
|
3e834e2c38 | ||
|
|
cae625dab1 | ||
|
|
122d7f1ad6 | ||
|
|
7eaf284400 | ||
|
|
86ef7afbdf | ||
|
|
615c431875 | ||
|
|
d041ea7a56 | ||
|
|
c4c1747563 | ||
|
|
c284fe8348 | ||
|
|
8f932b7358 | ||
|
|
8c826b3073 |
19
CHANGELOG.md
19
CHANGELOG.md
@@ -5,6 +5,25 @@ All notable changes to CC Switch will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [3.3.0] - 2025-09-22
|
||||
|
||||
### ✨ Features
|
||||
- Add “Apply to VS Code / Remove from VS Code” actions on provider cards, writing settings for Code/Insiders/VSCodium variants
|
||||
- Enable VS Code auto-sync by default with window broadcast and tray hooks so Codex switches sync silently
|
||||
- Extend the Codex provider wizard with display name, dedicated API key URL, and clearer guidance
|
||||
- Introduce shared common config snippets with JSON/TOML reuse, validation, and consistent error surfaces
|
||||
|
||||
### 🔧 Improvements
|
||||
- Keep the tray menu responsive when the window is hidden and standardize button styling and copy
|
||||
- Disable modal backdrop blur on Linux (WebKitGTK/Wayland) to avoid freezes; restore the window when clicking the macOS Dock icon
|
||||
- Support overriding config directories on WSL, refine placeholders/descriptions, and fix VS Code button wrapping on Windows
|
||||
- Add a `created_at` timestamp to provider records for future sorting and analytics
|
||||
|
||||
### 🐛 Fixes
|
||||
- Correct regex escapes and common snippet trimming in the Codex wizard to prevent validation issues
|
||||
- Harden the VS Code sync flow with more reliable TOML/JSON parsing while reducing layout jank
|
||||
- Bundle `@codemirror/lint` to reinstate live linting in config editors
|
||||
|
||||
## [3.2.0] - 2025-09-13
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
33
README.md
33
README.md
@@ -1,28 +1,27 @@
|
||||
# Claude Code & Codex 供应商切换器
|
||||
|
||||
[](https://github.com/farion1231/cc-switch/releases)
|
||||
[](https://github.com/farion1231/cc-switch/releases)
|
||||
[](https://github.com/farion1231/cc-switch/releases)
|
||||
[](https://tauri.app/)
|
||||
|
||||
一个用于管理和切换 Claude Code 与 Codex 不同供应商配置的桌面应用。
|
||||
|
||||
> v3.2.0 重点:全新 UI、macOS系统托盘、内置更新器、原子写入与回滚、改进暗色样式、单一事实源(SSOT)与一次性迁移/归档(详见下文“迁移与归档 v3.2.0”)。
|
||||
> v3.3.0 :VS Code Codex 插件一键配置/移除(默认自动同步)、Codex 通用配置片段与自定义向导增强、WSL 环境支持、跨平台托盘与 UI 优化。
|
||||
|
||||
> v3.2.0 :全新 UI、macOS系统托盘、内置更新器、原子写入与回滚、改进暗色样式、单一事实源(SSOT)与一次性迁移/归档。
|
||||
|
||||
> v3.1.0 :新增 Codex 供应商管理与一键切换,支持导入当前 Codex 配置为默认供应商,并在内部配置从 v1 → v2 迁移前自动备份(详见下文“迁移与归档”)。
|
||||
|
||||
> v3.0.0 重大更新:从 Electron 完全迁移到 Tauri 2.0,应用体积显著降低、启动性能大幅提升。
|
||||
|
||||
## 功能特性(v3.2.0)
|
||||
## 功能特性(v3.3.0)
|
||||
|
||||
- **全新 UI**:感谢 [TinsFox](https://github.com/TinsFox) 大佬设计的全新 UI
|
||||
- **系统托盘(菜单栏)快速切换**:按应用分组(Claude / Codex),勾选态展示当前供应商
|
||||
- **内置更新器**:集成 Tauri Updater,支持检测/下载/安装与一键重启
|
||||
- **单一事实源(SSOT)**:不再写每个供应商的“副本文件”,统一存于 `~/.cc-switch/config.json`
|
||||
- **一次性迁移/归档**:首次升级自动导入旧副本并归档原文件,之后不再持续归档
|
||||
- **原子写入与回滚**:写入 `auth.json`/`config.toml`/`settings.json` 时避免半写状态
|
||||
- **深色模式优化**:Tailwind v4 适配与选择器修正
|
||||
- **丰富预设与自定义**:Qwen coder、Kimi、GLM、DeepSeek、PackyCode 等;可自定义 Base URL
|
||||
- **本地优先与隐私**:全部信息存储在本地 `~/.cc-switch/config.json`
|
||||
- **VS Code Codex 插件一键配置**:供应商卡片支持「应用到 VS Code / 从 VS Code 移除」,默认开启自动同步,并可跨 Code / Insiders / VSCodium 写入 `settings.json`
|
||||
- **通用配置片段**:Claude 与 Codex 共用 JSON/TOML 片段,提供编辑器 lint、内容校验、统一错误提示与本地持久化
|
||||
- **Codex 配置向导**:新增显示名称、专用 API Key URL、HTML5 校验与预设模板,方便快速配置第三方服务
|
||||
- **系统托盘与快捷操作**:窗口隐藏时仍可通过托盘切换供应商,并在自动同步开启时触发 VS Code 写入
|
||||
- **平台适配**:新增 Windows WSL 环境支持、Linux 自动禁用模态背景模糊解决白屏问题、macOS Dock 点击即可恢复窗口
|
||||
- **UI优化**:多处 UI 和使用体验优化
|
||||
|
||||
## 界面预览
|
||||
|
||||
@@ -54,7 +53,7 @@
|
||||
|
||||
### Linux 用户
|
||||
|
||||
从 [Releases](../../releases) 页面下载最新版本的 `.deb` 包。
|
||||
从 [Releases](../../releases) 页面下载最新版本的 `.deb` 包或者 `AppImage`安装包。
|
||||
|
||||
## 使用说明
|
||||
|
||||
@@ -70,7 +69,7 @@
|
||||
|
||||
- 在“设置”中点击“检查更新”,若内置 Updater 配置可用将直接检测与下载;否则会回退打开 Releases 页面
|
||||
|
||||
### Codex 说明(v3.2.0 SSOT)
|
||||
### Codex 说明(SSOT)
|
||||
|
||||
- 配置目录:`~/.codex/`
|
||||
- live 主配置:`auth.json`(必需)、`config.toml`(可为空)
|
||||
@@ -82,7 +81,7 @@
|
||||
- 导入默认:当该应用无任何供应商时,从现有 live 主配置创建一条默认项并设为当前
|
||||
- 官方登录:可切换到预设“Codex 官方登录”,重启终端后按官方流程登录
|
||||
|
||||
### Claude Code 说明(v3.2.0 SSOT)
|
||||
### Claude Code 说明(SSOT)
|
||||
|
||||
- 配置目录:`~/.claude/`
|
||||
- live 主配置:`settings.json`(优先)或历史兼容 `claude.json`
|
||||
@@ -94,9 +93,9 @@
|
||||
- 导入默认:当该应用无任何供应商时,从现有 live 主配置创建一条默认项并设为当前
|
||||
- 官方登录:可切换到预设“Claude 官方登录”,重启终端后可使用 `/login` 完成登录
|
||||
|
||||
### 迁移与归档(v3.2.0)
|
||||
### 迁移与归档(自 v3.2.0 起)
|
||||
|
||||
- 一次性迁移:首次启动 3.2.0 会扫描旧的“副本文件”并合并到 `~/.cc-switch/config.json`
|
||||
- 一次性迁移:首次启动 3.2.0 及以上版本会扫描旧的“副本文件”并合并到 `~/.cc-switch/config.json`
|
||||
- Claude:`~/.claude/settings-*.json`(排除 `settings.json` / 历史 `claude.json`)
|
||||
- Codex:`~/.codex/auth-*.json` 与 `config-*.toml`(按名称成对合并)
|
||||
- 去重与当前项:按“名称(忽略大小写)+ API Key”去重;若当前为空,将 live 合并项设为当前
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cc-switch",
|
||||
"version": "3.2.0",
|
||||
"version": "3.3.0",
|
||||
"description": "Claude Code & Codex 供应商切换工具",
|
||||
"scripts": {
|
||||
"dev": "pnpm tauri dev",
|
||||
@@ -27,6 +27,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@codemirror/lint": "^6.8.5",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
"@codemirror/view": "^6.38.2",
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -11,6 +11,9 @@ importers:
|
||||
'@codemirror/lang-json':
|
||||
specifier: ^6.0.2
|
||||
version: 6.0.2
|
||||
'@codemirror/lint':
|
||||
specifier: ^6.8.5
|
||||
version: 6.8.5
|
||||
'@codemirror/state':
|
||||
specifier: ^6.5.2
|
||||
version: 6.5.2
|
||||
|
||||
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@@ -580,7 +580,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc-switch"
|
||||
version = "3.2.0"
|
||||
version = "3.3.0"
|
||||
dependencies = [
|
||||
"dirs 5.0.1",
|
||||
"log",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "cc-switch"
|
||||
version = "3.2.0"
|
||||
version = "3.3.0"
|
||||
description = "Claude Code & Codex 供应商配置管理工具"
|
||||
authors = ["Jason Young"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -16,6 +16,9 @@ pub struct Provider {
|
||||
pub website_url: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub category: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(rename = "createdAt")]
|
||||
pub created_at: Option<i64>,
|
||||
}
|
||||
|
||||
impl Provider {
|
||||
@@ -32,6 +35,7 @@ impl Provider {
|
||||
settings_config,
|
||||
website_url,
|
||||
category: None,
|
||||
created_at: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "CC Switch",
|
||||
"version": "3.2.0",
|
||||
"version": "3.3.0",
|
||||
"identifier": "com.ccswitch.desktop",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
|
||||
@@ -12,7 +12,11 @@ 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";
|
||||
@@ -60,7 +64,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
: "",
|
||||
});
|
||||
const [category, setCategory] = useState<ProviderCategory | undefined>(
|
||||
initialData?.category,
|
||||
initialData?.category
|
||||
);
|
||||
|
||||
// Claude 模型配置状态
|
||||
@@ -72,9 +76,11 @@ 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,
|
||||
showPresets && isCodex ? -1 : null
|
||||
);
|
||||
|
||||
const setCodexAuth = (value: string) => {
|
||||
@@ -135,25 +141,25 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
const [codexCommonConfigSnippet, setCodexCommonConfigSnippetState] =
|
||||
useState<string>(() => {
|
||||
if (typeof window === "undefined") {
|
||||
return DEFAULT_CODEX_COMMON_CONFIG_SNIPPET;
|
||||
return DEFAULT_CODEX_COMMON_CONFIG_SNIPPET.trim();
|
||||
}
|
||||
try {
|
||||
const stored = window.localStorage.getItem(
|
||||
CODEX_COMMON_CONFIG_STORAGE_KEY,
|
||||
CODEX_COMMON_CONFIG_STORAGE_KEY
|
||||
);
|
||||
if (stored && stored.trim()) {
|
||||
return stored;
|
||||
return stored.trim();
|
||||
}
|
||||
} catch {
|
||||
// ignore localStorage 读取失败
|
||||
}
|
||||
return DEFAULT_CODEX_COMMON_CONFIG_SNIPPET;
|
||||
return DEFAULT_CODEX_COMMON_CONFIG_SNIPPET.trim();
|
||||
});
|
||||
const [codexCommonConfigError, setCodexCommonConfigError] = useState("");
|
||||
const isUpdatingFromCodexCommonConfig = useRef(false);
|
||||
// -1 表示自定义,null 表示未选择,>= 0 表示预设索引
|
||||
const [selectedPreset, setSelectedPreset] = useState<number | null>(
|
||||
showPresets ? -1 : null,
|
||||
showPresets ? -1 : null
|
||||
);
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [codexAuthError, setCodexAuthError] = useState("");
|
||||
@@ -222,11 +228,11 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
const configString = JSON.stringify(
|
||||
initialData.settingsConfig,
|
||||
null,
|
||||
2,
|
||||
2
|
||||
);
|
||||
const hasCommon = hasCommonConfigSnippet(
|
||||
configString,
|
||||
commonConfigSnippet,
|
||||
commonConfigSnippet
|
||||
);
|
||||
setUseCommonConfig(hasCommon);
|
||||
setSettingsConfigError(validateSettingsConfig(configString));
|
||||
@@ -242,14 +248,14 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
if (config.env) {
|
||||
setClaudeModel(config.env.ANTHROPIC_MODEL || "");
|
||||
setClaudeSmallFastModel(
|
||||
config.env.ANTHROPIC_SMALL_FAST_MODEL || "",
|
||||
config.env.ANTHROPIC_SMALL_FAST_MODEL || ""
|
||||
);
|
||||
setBaseUrl(config.env.ANTHROPIC_BASE_URL || ""); // 初始化基础 URL
|
||||
|
||||
// 初始化 Kimi 模型选择
|
||||
setKimiAnthropicModel(config.env.ANTHROPIC_MODEL || "");
|
||||
setKimiAnthropicSmallFastModel(
|
||||
config.env.ANTHROPIC_SMALL_FAST_MODEL || "",
|
||||
config.env.ANTHROPIC_SMALL_FAST_MODEL || ""
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -257,7 +263,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
// Codex 初始化时检查 TOML 通用配置
|
||||
const hasCommon = hasTomlCommonConfigSnippet(
|
||||
codexConfig,
|
||||
codexCommonConfigSnippet,
|
||||
codexCommonConfigSnippet
|
||||
);
|
||||
setUseCodexCommonConfig(hasCommon);
|
||||
}
|
||||
@@ -277,7 +283,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
if (selectedPreset !== null && selectedPreset >= 0) {
|
||||
const preset = providerPresets[selectedPreset];
|
||||
setCategory(
|
||||
preset?.category || (preset?.isOfficial ? "official" : undefined),
|
||||
preset?.category || (preset?.isOfficial ? "official" : undefined)
|
||||
);
|
||||
} else if (selectedPreset === -1) {
|
||||
setCategory("custom");
|
||||
@@ -286,7 +292,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
if (selectedCodexPreset !== null && selectedCodexPreset >= 0) {
|
||||
const preset = codexProviderPresets[selectedCodexPreset];
|
||||
setCategory(
|
||||
preset?.category || (preset?.isOfficial ? "official" : undefined),
|
||||
preset?.category || (preset?.isOfficial ? "official" : undefined)
|
||||
);
|
||||
} else if (selectedCodexPreset === -1) {
|
||||
setCategory("custom");
|
||||
@@ -301,7 +307,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
if (commonConfigSnippet.trim()) {
|
||||
window.localStorage.setItem(
|
||||
COMMON_CONFIG_STORAGE_KEY,
|
||||
commonConfigSnippet,
|
||||
commonConfigSnippet
|
||||
);
|
||||
} else {
|
||||
window.localStorage.removeItem(COMMON_CONFIG_STORAGE_KEY);
|
||||
@@ -364,7 +370,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
}
|
||||
} else {
|
||||
const currentSettingsError = validateSettingsConfig(
|
||||
formData.settingsConfig,
|
||||
formData.settingsConfig
|
||||
);
|
||||
setSettingsConfigError(currentSettingsError);
|
||||
if (currentSettingsError) {
|
||||
@@ -395,7 +401,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
};
|
||||
|
||||
const handleChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
) => {
|
||||
const { name, value } = e.target;
|
||||
|
||||
@@ -413,10 +419,10 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
// 不再从 JSON 自动提取或覆盖官网地址,只更新配置内容
|
||||
updateSettingsConfigValue(value);
|
||||
} else {
|
||||
setFormData({
|
||||
...formData,
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
});
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -425,7 +431,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
const { updatedConfig, error: snippetError } = updateCommonConfigSnippet(
|
||||
formData.settingsConfig,
|
||||
commonConfigSnippet,
|
||||
checked,
|
||||
checked
|
||||
);
|
||||
|
||||
if (snippetError) {
|
||||
@@ -458,7 +464,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
const { updatedConfig } = updateCommonConfigSnippet(
|
||||
formData.settingsConfig,
|
||||
previousSnippet,
|
||||
false,
|
||||
false
|
||||
);
|
||||
// 直接更新 formData,不通过 handleChange
|
||||
updateSettingsConfigValue(updatedConfig);
|
||||
@@ -480,7 +486,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
const removeResult = updateCommonConfigSnippet(
|
||||
formData.settingsConfig,
|
||||
previousSnippet,
|
||||
false,
|
||||
false
|
||||
);
|
||||
if (removeResult.error) {
|
||||
setCommonConfigError(removeResult.error);
|
||||
@@ -492,7 +498,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
const addResult = updateCommonConfigSnippet(
|
||||
removeResult.updatedConfig,
|
||||
value,
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
if (addResult.error) {
|
||||
@@ -532,7 +538,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
});
|
||||
setSettingsConfigError(validateSettingsConfig(configString));
|
||||
setCategory(
|
||||
preset.category || (preset.isOfficial ? "official" : undefined),
|
||||
preset.category || (preset.isOfficial ? "official" : undefined)
|
||||
);
|
||||
|
||||
// 设置选中的预设
|
||||
@@ -558,7 +564,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
if (preset.name?.includes("Kimi")) {
|
||||
setKimiAnthropicModel(config.env.ANTHROPIC_MODEL || "");
|
||||
setKimiAnthropicSmallFastModel(
|
||||
config.env.ANTHROPIC_SMALL_FAST_MODEL || "",
|
||||
config.env.ANTHROPIC_SMALL_FAST_MODEL || ""
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@@ -604,7 +610,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
// Codex: 应用预设
|
||||
const applyCodexPreset = (
|
||||
preset: (typeof codexProviderPresets)[0],
|
||||
index: number,
|
||||
index: number
|
||||
) => {
|
||||
const authString = JSON.stringify(preset.auth || {}, null, 2);
|
||||
setCodexAuth(authString);
|
||||
@@ -618,7 +624,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
|
||||
setSelectedCodexPreset(index);
|
||||
setCategory(
|
||||
preset.category || (preset.isOfficial ? "official" : undefined),
|
||||
preset.category || (preset.isOfficial ? "official" : undefined)
|
||||
);
|
||||
|
||||
// 清空 API Key,让用户重新输入
|
||||
@@ -628,14 +634,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");
|
||||
};
|
||||
@@ -647,7 +662,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
const configString = setApiKeyInConfig(
|
||||
formData.settingsConfig,
|
||||
key.trim(),
|
||||
{ createIfMissing: selectedPreset !== null && selectedPreset !== -1 },
|
||||
{ createIfMissing: selectedPreset !== null && selectedPreset !== -1 }
|
||||
);
|
||||
|
||||
// 更新表单配置
|
||||
@@ -689,12 +704,9 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
|
||||
// Codex: 处理通用配置开关
|
||||
const handleCodexCommonConfigToggle = (checked: boolean) => {
|
||||
const snippet = codexCommonConfigSnippet.trim();
|
||||
const { updatedConfig, error: snippetError } =
|
||||
updateTomlCommonConfigSnippet(
|
||||
codexConfig,
|
||||
codexCommonConfigSnippet,
|
||||
checked,
|
||||
);
|
||||
updateTomlCommonConfigSnippet(codexConfig, snippet, checked);
|
||||
|
||||
if (snippetError) {
|
||||
setCodexCommonConfigError(snippetError);
|
||||
@@ -715,16 +727,17 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
|
||||
// Codex: 处理通用配置片段变化
|
||||
const handleCodexCommonConfigSnippetChange = (value: string) => {
|
||||
const previousSnippet = codexCommonConfigSnippet;
|
||||
const previousSnippet = codexCommonConfigSnippet.trim();
|
||||
const sanitizedValue = value.trim();
|
||||
setCodexCommonConfigSnippet(value);
|
||||
|
||||
if (!value.trim()) {
|
||||
if (!sanitizedValue) {
|
||||
setCodexCommonConfigError("");
|
||||
if (useCodexCommonConfig) {
|
||||
const { updatedConfig } = updateTomlCommonConfigSnippet(
|
||||
codexConfig,
|
||||
previousSnippet,
|
||||
false,
|
||||
false
|
||||
);
|
||||
setCodexConfig(updatedConfig);
|
||||
setUseCodexCommonConfig(false);
|
||||
@@ -737,12 +750,12 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
const removeResult = updateTomlCommonConfigSnippet(
|
||||
codexConfig,
|
||||
previousSnippet,
|
||||
false,
|
||||
false
|
||||
);
|
||||
const addResult = updateTomlCommonConfigSnippet(
|
||||
removeResult.updatedConfig,
|
||||
value,
|
||||
true,
|
||||
sanitizedValue,
|
||||
true
|
||||
);
|
||||
|
||||
if (addResult.error) {
|
||||
@@ -762,7 +775,10 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
// 保存 Codex 通用配置到 localStorage
|
||||
if (typeof window !== "undefined") {
|
||||
try {
|
||||
window.localStorage.setItem(CODEX_COMMON_CONFIG_STORAGE_KEY, value);
|
||||
window.localStorage.setItem(
|
||||
CODEX_COMMON_CONFIG_STORAGE_KEY,
|
||||
sanitizedValue
|
||||
);
|
||||
} catch {
|
||||
// ignore localStorage 写入失败
|
||||
}
|
||||
@@ -774,7 +790,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
if (!isUpdatingFromCodexCommonConfig.current) {
|
||||
const hasCommon = hasTomlCommonConfigSnippet(
|
||||
value,
|
||||
codexCommonConfigSnippet,
|
||||
codexCommonConfigSnippet
|
||||
);
|
||||
setUseCodexCommonConfig(hasCommon);
|
||||
}
|
||||
@@ -831,7 +847,12 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
// 获取当前供应商的网址
|
||||
const getCurrentWebsiteUrl = () => {
|
||||
if (selectedPreset !== null && selectedPreset >= 0) {
|
||||
return providerPresets[selectedPreset]?.websiteUrl || "";
|
||||
const preset = providerPresets[selectedPreset];
|
||||
if (!preset) return "";
|
||||
// 仅第三方供应商使用专用 apiKeyUrl,其余使用官网地址
|
||||
return preset.category === "third_party"
|
||||
? preset.apiKeyUrl || preset.websiteUrl || ""
|
||||
: preset.websiteUrl || "";
|
||||
}
|
||||
return formData.websiteUrl || "";
|
||||
};
|
||||
@@ -839,7 +860,12 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
// 获取 Codex 当前供应商的网址
|
||||
const getCurrentCodexWebsiteUrl = () => {
|
||||
if (selectedCodexPreset !== null && selectedCodexPreset >= 0) {
|
||||
return codexProviderPresets[selectedCodexPreset]?.websiteUrl || "";
|
||||
const preset = codexProviderPresets[selectedCodexPreset];
|
||||
if (!preset) return "";
|
||||
// 仅第三方供应商使用专用 apiKeyUrl,其余使用官网地址
|
||||
return preset.category === "third_party"
|
||||
? preset.apiKeyUrl || preset.websiteUrl || ""
|
||||
: preset.websiteUrl || "";
|
||||
}
|
||||
return formData.websiteUrl || "";
|
||||
};
|
||||
@@ -868,7 +894,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
codexProviderPresets[selectedCodexPreset]?.category === "official")) ||
|
||||
category === "official";
|
||||
|
||||
// 判断是否显示 Codex 的"获取 API Key"链接
|
||||
// 判断是否显示 Codex 的"获取 API Key"链接(国产官方、聚合站和第三方显示)
|
||||
const shouldShowCodexApiKeyLink =
|
||||
isCodex &&
|
||||
!isCodexOfficialPreset &&
|
||||
@@ -887,7 +913,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
// 处理模型输入变化,自动更新 JSON 配置
|
||||
const handleModelChange = (
|
||||
field: "ANTHROPIC_MODEL" | "ANTHROPIC_SMALL_FAST_MODEL",
|
||||
value: string,
|
||||
value: string
|
||||
) => {
|
||||
if (field === "ANTHROPIC_MODEL") {
|
||||
setClaudeModel(value);
|
||||
@@ -917,7 +943,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
// Kimi 模型选择处理函数
|
||||
const handleKimiModelChange = (
|
||||
field: "ANTHROPIC_MODEL" | "ANTHROPIC_SMALL_FAST_MODEL",
|
||||
value: string,
|
||||
value: string
|
||||
) => {
|
||||
if (field === "ANTHROPIC_MODEL") {
|
||||
setKimiAnthropicModel(value);
|
||||
@@ -942,7 +968,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
useEffect(() => {
|
||||
if (!initialData) return;
|
||||
const parsedKey = getApiKeyFromConfig(
|
||||
JSON.stringify(initialData.settingsConfig),
|
||||
JSON.stringify(initialData.settingsConfig)
|
||||
);
|
||||
if (parsedKey) setApiKey(parsedKey);
|
||||
}, [initialData]);
|
||||
@@ -1023,6 +1049,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>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1192,6 +1230,21 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
}
|
||||
commonConfigError={codexCommonConfigError}
|
||||
authError={codexAuthError}
|
||||
isCustomMode={selectedCodexPreset === -1}
|
||||
onWebsiteUrlChange={(url) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
websiteUrl: url,
|
||||
}));
|
||||
}}
|
||||
onNameChange={(name) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
name,
|
||||
}));
|
||||
}}
|
||||
isTemplateModalOpen={isCodexTemplateModalOpen}
|
||||
setIsTemplateModalOpen={setIsCodexTemplateModalOpen}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
@@ -1233,7 +1286,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
onChange={(e) =>
|
||||
handleModelChange(
|
||||
"ANTHROPIC_SMALL_FAST_MODEL",
|
||||
e.target.value,
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
placeholder="例如: GLM-4.5-Air"
|
||||
|
||||
@@ -1,36 +1,112 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, useRef } 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; // 新增:设置模态框状态
|
||||
|
||||
onNameChange?: (name: string) => void; // 新增:更新供应商名称回调
|
||||
}
|
||||
|
||||
const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
authValue,
|
||||
|
||||
configValue,
|
||||
|
||||
onAuthChange,
|
||||
|
||||
onConfigChange,
|
||||
|
||||
onAuthBlur,
|
||||
|
||||
useCommonConfig,
|
||||
|
||||
onCommonConfigToggle,
|
||||
|
||||
commonConfigSnippet,
|
||||
|
||||
onCommonConfigSnippetChange,
|
||||
|
||||
commonConfigError,
|
||||
|
||||
authError,
|
||||
|
||||
onWebsiteUrlChange,
|
||||
|
||||
onNameChange,
|
||||
|
||||
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 apiKeyInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const baseUrlInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const modelNameInputRef = useRef<HTMLInputElement>(null);
|
||||
const displayNameInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// 移除自动填充逻辑,因为现在在点击自定义按钮时就已经填充
|
||||
|
||||
const [templateDisplayName, setTemplateDisplayName] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (commonConfigError && !isCommonConfigModalOpen) {
|
||||
setIsCommonConfigModalOpen(true);
|
||||
@@ -38,16 +114,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 +135,88 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
setIsCommonConfigModalOpen(false);
|
||||
};
|
||||
|
||||
const closeTemplateModal = () => {
|
||||
setIsTemplateModalOpen(false);
|
||||
};
|
||||
|
||||
const applyTemplate = () => {
|
||||
const requiredInputs = [
|
||||
displayNameInputRef.current,
|
||||
apiKeyInputRef.current,
|
||||
baseUrlInputRef.current,
|
||||
modelNameInputRef.current,
|
||||
];
|
||||
|
||||
for (const input of requiredInputs) {
|
||||
if (input && !input.checkValidity()) {
|
||||
input.reportValidity();
|
||||
input.focus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const trimmedKey = templateApiKey.trim();
|
||||
|
||||
const trimmedBaseUrl = templateBaseUrl.trim();
|
||||
|
||||
const trimmedModel = templateModelName.trim();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
if (onNameChange) {
|
||||
const trimmedName = templateDisplayName.trim();
|
||||
if (trimmedName) {
|
||||
onNameChange(trimmedName);
|
||||
}
|
||||
}
|
||||
|
||||
setTemplateApiKey("");
|
||||
|
||||
setTemplateProviderName("");
|
||||
|
||||
setTemplateBaseUrl("");
|
||||
|
||||
setTemplateWebsiteUrl("");
|
||||
|
||||
setTemplateModelName("gpt-5-codex");
|
||||
|
||||
setTemplateDisplayName("");
|
||||
|
||||
closeTemplateModal();
|
||||
};
|
||||
|
||||
const handleTemplateInputKeyDown = (
|
||||
e: React.KeyboardEvent<HTMLInputElement>
|
||||
) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
|
||||
e.stopPropagation();
|
||||
|
||||
applyTemplate();
|
||||
}
|
||||
};
|
||||
|
||||
const handleAuthChange = (value: string) => {
|
||||
onAuthChange(value);
|
||||
};
|
||||
@@ -76,6 +238,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
>
|
||||
auth.json (JSON) *
|
||||
</label>
|
||||
|
||||
<textarea
|
||||
id="codexAuth"
|
||||
value={authValue}
|
||||
@@ -97,9 +260,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 +278,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 +289,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||
写入通用配置
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end">
|
||||
<button
|
||||
type="button"
|
||||
@@ -132,11 +299,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 +323,249 @@ 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) {
|
||||
closeTemplateModal();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<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={closeTemplateModal}
|
||||
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}
|
||||
ref={apiKeyInputRef}
|
||||
onChange={(e) => setTemplateApiKey(e.target.value)}
|
||||
onKeyDown={handleTemplateInputKeyDown}
|
||||
pattern=".*\S.*"
|
||||
title="请输入有效的内容"
|
||||
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={templateDisplayName}
|
||||
ref={displayNameInputRef}
|
||||
onChange={(e) => {
|
||||
setTemplateDisplayName(e.target.value);
|
||||
if (onNameChange) {
|
||||
onNameChange(e.target.value);
|
||||
}
|
||||
}}
|
||||
onKeyDown={handleTemplateInputKeyDown}
|
||||
placeholder="例如:Codex 官方"
|
||||
required
|
||||
pattern=".*\S.*"
|
||||
title="请输入有效的内容"
|
||||
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={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}
|
||||
ref={baseUrlInputRef}
|
||||
onChange={(e) => setTemplateBaseUrl(e.target.value)}
|
||||
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}
|
||||
ref={modelNameInputRef}
|
||||
onChange={(e) => setTemplateModelName(e.target.value)}
|
||||
onKeyDown={handleTemplateInputKeyDown}
|
||||
pattern=".*\S.*"
|
||||
title="请输入有效的内容"
|
||||
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>
|
||||
)}
|
||||
</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={closeTemplateModal}
|
||||
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 +574,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 +582,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 +602,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 +630,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 +639,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 +648,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>
|
||||
);
|
||||
|
||||
@@ -22,7 +22,7 @@ interface ProviderListProps {
|
||||
onNotify?: (
|
||||
message: string,
|
||||
type: "success" | "error",
|
||||
duration?: number,
|
||||
duration?: number
|
||||
) => void;
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
onNotify?.(
|
||||
"未找到 VS Code 用户设置文件 (settings.json)",
|
||||
"error",
|
||||
3000,
|
||||
3000
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -157,7 +157,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
onNotify?.(
|
||||
"未找到 VS Code 用户设置文件 (settings.json)",
|
||||
"error",
|
||||
3000,
|
||||
3000
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -230,7 +230,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
<div
|
||||
key={provider.id}
|
||||
className={cn(
|
||||
isCurrent ? cardStyles.selected : cardStyles.interactive,
|
||||
isCurrent ? cardStyles.selected : cardStyles.interactive
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
@@ -243,7 +243,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
<div
|
||||
className={cn(
|
||||
badgeStyles.success,
|
||||
!isCurrent && "invisible",
|
||||
!isCurrent && "invisible"
|
||||
)}
|
||||
>
|
||||
<CheckCircle2 size={12} />
|
||||
@@ -284,11 +284,11 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
: handleApplyToVSCode(provider)
|
||||
}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded-md transition-colors w-[130px] justify-center",
|
||||
"inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded-md transition-colors w-[130px] whitespace-nowrap justify-center",
|
||||
!isCurrent && "invisible",
|
||||
vscodeAppliedFor === provider.id
|
||||
? "bg-gray-100 text-gray-800 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700"
|
||||
: "bg-emerald-500 text-white hover:bg-emerald-600 dark:bg-emerald-600 dark:hover:bg-emerald-700",
|
||||
? "border border-gray-300 text-gray-600 hover:border-red-300 hover:text-red-600 hover:bg-red-50 dark:border-gray-600 dark:text-gray-400 dark:hover:border-red-800 dark:hover:text-red-400 dark:hover:bg-red-900/20"
|
||||
: "border border-gray-300 text-gray-700 hover:border-blue-300 hover:text-blue-600 hover:bg-blue-50 dark:border-gray-600 dark:text-gray-300 dark:hover:border-blue-700 dark:hover:text-blue-400 dark:hover:bg-blue-900/20"
|
||||
)}
|
||||
title={
|
||||
vscodeAppliedFor === provider.id
|
||||
@@ -305,13 +305,13 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
onClick={() => onSwitch(provider.id)}
|
||||
disabled={isCurrent}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded-md transition-colors",
|
||||
"inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded-md transition-colors w-[76px] justify-center whitespace-nowrap",
|
||||
isCurrent
|
||||
? "bg-gray-100 text-gray-400 dark:bg-gray-800 dark:text-gray-500 cursor-not-allowed"
|
||||
: "bg-blue-500 text-white hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700",
|
||||
: "bg-blue-500 text-white hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700"
|
||||
)}
|
||||
>
|
||||
<Play size={14} />
|
||||
{!isCurrent && <Play size={14} />}
|
||||
{isCurrent ? "使用中" : "启用"}
|
||||
</button>
|
||||
|
||||
@@ -330,7 +330,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
||||
buttonStyles.icon,
|
||||
isCurrent
|
||||
? "text-gray-400 cursor-not-allowed"
|
||||
: "text-gray-500 hover:text-red-500 hover:bg-red-100 dark:text-gray-400 dark:hover:text-red-400 dark:hover:bg-red-500/10",
|
||||
: "text-gray-500 hover:text-red-500 hover:bg-red-100 dark:text-gray-400 dark:hover:text-red-400 dark:hover:bg-red-500/10"
|
||||
)}
|
||||
title="删除供应商"
|
||||
>
|
||||
|
||||
@@ -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
|
||||
|
||||
[model_providers.${cleanProviderName}]
|
||||
name = "${cleanProviderName}"
|
||||
base_url = "${baseUrl}"
|
||||
wire_api = "responses"
|
||||
env_key = "${cleanProviderName}"
|
||||
requires_openai_auth = true`;
|
||||
}
|
||||
|
||||
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"
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -6,6 +6,8 @@ import { ProviderCategory } from "../types";
|
||||
export interface ProviderPreset {
|
||||
name: string;
|
||||
websiteUrl: string;
|
||||
// 新增:第三方/聚合等可单独配置获取 API Key 的链接
|
||||
apiKeyUrl?: string;
|
||||
settingsConfig: object;
|
||||
isOfficial?: boolean; // 标识是否为官方预设
|
||||
category?: ProviderCategory; // 新增:分类
|
||||
@@ -90,6 +92,7 @@ export const providerPresets: ProviderPreset[] = [
|
||||
{
|
||||
name: "PackyCode",
|
||||
websiteUrl: "https://www.packycode.com",
|
||||
apiKeyUrl: "https://www.packycode.com/?aff=rlo54mgz",
|
||||
settingsConfig: {
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: "https://api.packycode.com",
|
||||
|
||||
Reference in New Issue
Block a user