Compare commits
16 Commits
feature/ws
...
v3.3.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aaf1af0743 | ||
|
|
aeb0007957 | ||
|
|
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/),
|
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).
|
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
|
## [3.2.0] - 2025-09-13
|
||||||
|
|
||||||
### ✨ New Features
|
### ✨ New Features
|
||||||
|
|||||||
33
README.md
33
README.md
@@ -1,28 +1,27 @@
|
|||||||
# Claude Code & Codex 供应商切换器
|
# 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://github.com/farion1231/cc-switch/releases)
|
||||||
[](https://tauri.app/)
|
[](https://tauri.app/)
|
||||||
|
|
||||||
一个用于管理和切换 Claude Code 与 Codex 不同供应商配置的桌面应用。
|
一个用于管理和切换 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.1.0 :新增 Codex 供应商管理与一键切换,支持导入当前 Codex 配置为默认供应商,并在内部配置从 v1 → v2 迁移前自动备份(详见下文“迁移与归档”)。
|
||||||
|
|
||||||
> v3.0.0 重大更新:从 Electron 完全迁移到 Tauri 2.0,应用体积显著降低、启动性能大幅提升。
|
> v3.0.0 重大更新:从 Electron 完全迁移到 Tauri 2.0,应用体积显著降低、启动性能大幅提升。
|
||||||
|
|
||||||
## 功能特性(v3.2.0)
|
## 功能特性(v3.3.0)
|
||||||
|
|
||||||
- **全新 UI**:感谢 [TinsFox](https://github.com/TinsFox) 大佬设计的全新 UI
|
- **VS Code Codex 插件一键配置**:供应商卡片支持「应用到 VS Code / 从 VS Code 移除」,默认开启自动同步,并可跨 Code / Insiders / VSCodium 写入 `settings.json`
|
||||||
- **系统托盘(菜单栏)快速切换**:按应用分组(Claude / Codex),勾选态展示当前供应商
|
- **通用配置片段**:Claude 与 Codex 共用 JSON/TOML 片段,提供编辑器 lint、内容校验、统一错误提示与本地持久化
|
||||||
- **内置更新器**:集成 Tauri Updater,支持检测/下载/安装与一键重启
|
- **Codex 配置向导**:新增显示名称、专用 API Key URL、HTML5 校验与预设模板,方便快速配置第三方服务
|
||||||
- **单一事实源(SSOT)**:不再写每个供应商的“副本文件”,统一存于 `~/.cc-switch/config.json`
|
- **系统托盘与快捷操作**:窗口隐藏时仍可通过托盘切换供应商,并在自动同步开启时触发 VS Code 写入
|
||||||
- **一次性迁移/归档**:首次升级自动导入旧副本并归档原文件,之后不再持续归档
|
- **平台适配**:新增 Windows WSL 环境支持、Linux 自动禁用模态背景模糊解决白屏问题、macOS Dock 点击即可恢复窗口
|
||||||
- **原子写入与回滚**:写入 `auth.json`/`config.toml`/`settings.json` 时避免半写状态
|
- **UI优化**:多处 UI 和使用体验优化
|
||||||
- **深色模式优化**:Tailwind v4 适配与选择器修正
|
|
||||||
- **丰富预设与自定义**:Qwen coder、Kimi、GLM、DeepSeek、PackyCode 等;可自定义 Base URL
|
|
||||||
- **本地优先与隐私**:全部信息存储在本地 `~/.cc-switch/config.json`
|
|
||||||
|
|
||||||
## 界面预览
|
## 界面预览
|
||||||
|
|
||||||
@@ -54,7 +53,7 @@
|
|||||||
|
|
||||||
### Linux 用户
|
### Linux 用户
|
||||||
|
|
||||||
从 [Releases](../../releases) 页面下载最新版本的 `.deb` 包。
|
从 [Releases](../../releases) 页面下载最新版本的 `.deb` 包或者 `AppImage`安装包。
|
||||||
|
|
||||||
## 使用说明
|
## 使用说明
|
||||||
|
|
||||||
@@ -70,7 +69,7 @@
|
|||||||
|
|
||||||
- 在“设置”中点击“检查更新”,若内置 Updater 配置可用将直接检测与下载;否则会回退打开 Releases 页面
|
- 在“设置”中点击“检查更新”,若内置 Updater 配置可用将直接检测与下载;否则会回退打开 Releases 页面
|
||||||
|
|
||||||
### Codex 说明(v3.2.0 SSOT)
|
### Codex 说明(SSOT)
|
||||||
|
|
||||||
- 配置目录:`~/.codex/`
|
- 配置目录:`~/.codex/`
|
||||||
- live 主配置:`auth.json`(必需)、`config.toml`(可为空)
|
- live 主配置:`auth.json`(必需)、`config.toml`(可为空)
|
||||||
@@ -82,7 +81,7 @@
|
|||||||
- 导入默认:当该应用无任何供应商时,从现有 live 主配置创建一条默认项并设为当前
|
- 导入默认:当该应用无任何供应商时,从现有 live 主配置创建一条默认项并设为当前
|
||||||
- 官方登录:可切换到预设“Codex 官方登录”,重启终端后按官方流程登录
|
- 官方登录:可切换到预设“Codex 官方登录”,重启终端后按官方流程登录
|
||||||
|
|
||||||
### Claude Code 说明(v3.2.0 SSOT)
|
### Claude Code 说明(SSOT)
|
||||||
|
|
||||||
- 配置目录:`~/.claude/`
|
- 配置目录:`~/.claude/`
|
||||||
- live 主配置:`settings.json`(优先)或历史兼容 `claude.json`
|
- live 主配置:`settings.json`(优先)或历史兼容 `claude.json`
|
||||||
@@ -94,9 +93,9 @@
|
|||||||
- 导入默认:当该应用无任何供应商时,从现有 live 主配置创建一条默认项并设为当前
|
- 导入默认:当该应用无任何供应商时,从现有 live 主配置创建一条默认项并设为当前
|
||||||
- 官方登录:可切换到预设“Claude 官方登录”,重启终端后可使用 `/login` 完成登录
|
- 官方登录:可切换到预设“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`)
|
- Claude:`~/.claude/settings-*.json`(排除 `settings.json` / 历史 `claude.json`)
|
||||||
- Codex:`~/.codex/auth-*.json` 与 `config-*.toml`(按名称成对合并)
|
- Codex:`~/.codex/auth-*.json` 与 `config-*.toml`(按名称成对合并)
|
||||||
- 去重与当前项:按“名称(忽略大小写)+ API Key”去重;若当前为空,将 live 合并项设为当前
|
- 去重与当前项:按“名称(忽略大小写)+ API Key”去重;若当前为空,将 live 合并项设为当前
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cc-switch",
|
"name": "cc-switch",
|
||||||
"version": "3.2.0",
|
"version": "3.3.1",
|
||||||
"description": "Claude Code & Codex 供应商切换工具",
|
"description": "Claude Code & Codex 供应商切换工具",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "pnpm tauri dev",
|
"dev": "pnpm tauri dev",
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/lang-json": "^6.0.2",
|
"@codemirror/lang-json": "^6.0.2",
|
||||||
|
"@codemirror/lint": "^6.8.5",
|
||||||
"@codemirror/state": "^6.5.2",
|
"@codemirror/state": "^6.5.2",
|
||||||
"@codemirror/theme-one-dark": "^6.1.3",
|
"@codemirror/theme-one-dark": "^6.1.3",
|
||||||
"@codemirror/view": "^6.38.2",
|
"@codemirror/view": "^6.38.2",
|
||||||
|
|||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -11,6 +11,9 @@ importers:
|
|||||||
'@codemirror/lang-json':
|
'@codemirror/lang-json':
|
||||||
specifier: ^6.0.2
|
specifier: ^6.0.2
|
||||||
version: 6.0.2
|
version: 6.0.2
|
||||||
|
'@codemirror/lint':
|
||||||
|
specifier: ^6.8.5
|
||||||
|
version: 6.8.5
|
||||||
'@codemirror/state':
|
'@codemirror/state':
|
||||||
specifier: ^6.5.2
|
specifier: ^6.5.2
|
||||||
version: 6.5.2
|
version: 6.5.2
|
||||||
|
|||||||
893
src-tauri/Cargo.lock
generated
893
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cc-switch"
|
name = "cc-switch"
|
||||||
version = "3.2.0"
|
version = "3.3.1"
|
||||||
description = "Claude Code & Codex 供应商配置管理工具"
|
description = "Claude Code & Codex 供应商配置管理工具"
|
||||||
authors = ["Jason Young"]
|
authors = ["Jason Young"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@@ -30,6 +30,9 @@ tauri-plugin-dialog = "2"
|
|||||||
dirs = "5.0"
|
dirs = "5.0"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
|
|
||||||
|
[target.'cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))'.dependencies]
|
||||||
|
tauri-plugin-single-instance = "2"
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
objc2 = "0.5"
|
objc2 = "0.5"
|
||||||
objc2-app-kit = { version = "0.2", features = ["NSColor"] }
|
objc2-app-kit = { version = "0.2", features = ["NSColor"] }
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ pub struct Provider {
|
|||||||
pub website_url: Option<String>,
|
pub website_url: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub category: Option<String>,
|
pub category: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[serde(rename = "createdAt")]
|
||||||
|
pub created_at: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Provider {
|
impl Provider {
|
||||||
@@ -32,6 +35,7 @@ impl Provider {
|
|||||||
settings_config,
|
settings_config,
|
||||||
website_url,
|
website_url,
|
||||||
category: None,
|
category: None,
|
||||||
|
created_at: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "CC Switch",
|
"productName": "CC Switch",
|
||||||
"version": "3.2.0",
|
"version": "3.3.1",
|
||||||
"identifier": "com.ccswitch.desktop",
|
"identifier": "com.ccswitch.desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../dist",
|
"frontendDist": "../dist",
|
||||||
|
|||||||
@@ -12,7 +12,11 @@ import {
|
|||||||
validateJsonConfig,
|
validateJsonConfig,
|
||||||
} from "../utils/providerConfigUtils";
|
} from "../utils/providerConfigUtils";
|
||||||
import { providerPresets } from "../config/providerPresets";
|
import { providerPresets } from "../config/providerPresets";
|
||||||
import { codexProviderPresets } from "../config/codexProviderPresets";
|
import {
|
||||||
|
codexProviderPresets,
|
||||||
|
generateThirdPartyAuth,
|
||||||
|
generateThirdPartyConfig,
|
||||||
|
} from "../config/codexProviderPresets";
|
||||||
import PresetSelector from "./ProviderForm/PresetSelector";
|
import PresetSelector from "./ProviderForm/PresetSelector";
|
||||||
import ApiKeyInput from "./ProviderForm/ApiKeyInput";
|
import ApiKeyInput from "./ProviderForm/ApiKeyInput";
|
||||||
import ClaudeConfigEditor from "./ProviderForm/ClaudeConfigEditor";
|
import ClaudeConfigEditor from "./ProviderForm/ClaudeConfigEditor";
|
||||||
@@ -60,7 +64,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
: "",
|
: "",
|
||||||
});
|
});
|
||||||
const [category, setCategory] = useState<ProviderCategory | undefined>(
|
const [category, setCategory] = useState<ProviderCategory | undefined>(
|
||||||
initialData?.category,
|
initialData?.category
|
||||||
);
|
);
|
||||||
|
|
||||||
// Claude 模型配置状态
|
// Claude 模型配置状态
|
||||||
@@ -72,9 +76,11 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
const [codexAuth, setCodexAuthState] = useState("");
|
const [codexAuth, setCodexAuthState] = useState("");
|
||||||
const [codexConfig, setCodexConfigState] = useState("");
|
const [codexConfig, setCodexConfigState] = useState("");
|
||||||
const [codexApiKey, setCodexApiKey] = useState("");
|
const [codexApiKey, setCodexApiKey] = useState("");
|
||||||
|
const [isCodexTemplateModalOpen, setIsCodexTemplateModalOpen] =
|
||||||
|
useState(false);
|
||||||
// -1 表示自定义,null 表示未选择,>= 0 表示预设索引
|
// -1 表示自定义,null 表示未选择,>= 0 表示预设索引
|
||||||
const [selectedCodexPreset, setSelectedCodexPreset] = useState<number | null>(
|
const [selectedCodexPreset, setSelectedCodexPreset] = useState<number | null>(
|
||||||
showPresets && isCodex ? -1 : null,
|
showPresets && isCodex ? -1 : null
|
||||||
);
|
);
|
||||||
|
|
||||||
const setCodexAuth = (value: string) => {
|
const setCodexAuth = (value: string) => {
|
||||||
@@ -135,25 +141,25 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
const [codexCommonConfigSnippet, setCodexCommonConfigSnippetState] =
|
const [codexCommonConfigSnippet, setCodexCommonConfigSnippetState] =
|
||||||
useState<string>(() => {
|
useState<string>(() => {
|
||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
return DEFAULT_CODEX_COMMON_CONFIG_SNIPPET;
|
return DEFAULT_CODEX_COMMON_CONFIG_SNIPPET.trim();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const stored = window.localStorage.getItem(
|
const stored = window.localStorage.getItem(
|
||||||
CODEX_COMMON_CONFIG_STORAGE_KEY,
|
CODEX_COMMON_CONFIG_STORAGE_KEY
|
||||||
);
|
);
|
||||||
if (stored && stored.trim()) {
|
if (stored && stored.trim()) {
|
||||||
return stored;
|
return stored.trim();
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// ignore localStorage 读取失败
|
// ignore localStorage 读取失败
|
||||||
}
|
}
|
||||||
return DEFAULT_CODEX_COMMON_CONFIG_SNIPPET;
|
return DEFAULT_CODEX_COMMON_CONFIG_SNIPPET.trim();
|
||||||
});
|
});
|
||||||
const [codexCommonConfigError, setCodexCommonConfigError] = useState("");
|
const [codexCommonConfigError, setCodexCommonConfigError] = useState("");
|
||||||
const isUpdatingFromCodexCommonConfig = useRef(false);
|
const isUpdatingFromCodexCommonConfig = useRef(false);
|
||||||
// -1 表示自定义,null 表示未选择,>= 0 表示预设索引
|
// -1 表示自定义,null 表示未选择,>= 0 表示预设索引
|
||||||
const [selectedPreset, setSelectedPreset] = useState<number | null>(
|
const [selectedPreset, setSelectedPreset] = useState<number | null>(
|
||||||
showPresets ? -1 : null,
|
showPresets ? -1 : null
|
||||||
);
|
);
|
||||||
const [apiKey, setApiKey] = useState("");
|
const [apiKey, setApiKey] = useState("");
|
||||||
const [codexAuthError, setCodexAuthError] = useState("");
|
const [codexAuthError, setCodexAuthError] = useState("");
|
||||||
@@ -222,11 +228,11 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
const configString = JSON.stringify(
|
const configString = JSON.stringify(
|
||||||
initialData.settingsConfig,
|
initialData.settingsConfig,
|
||||||
null,
|
null,
|
||||||
2,
|
2
|
||||||
);
|
);
|
||||||
const hasCommon = hasCommonConfigSnippet(
|
const hasCommon = hasCommonConfigSnippet(
|
||||||
configString,
|
configString,
|
||||||
commonConfigSnippet,
|
commonConfigSnippet
|
||||||
);
|
);
|
||||||
setUseCommonConfig(hasCommon);
|
setUseCommonConfig(hasCommon);
|
||||||
setSettingsConfigError(validateSettingsConfig(configString));
|
setSettingsConfigError(validateSettingsConfig(configString));
|
||||||
@@ -242,14 +248,14 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
if (config.env) {
|
if (config.env) {
|
||||||
setClaudeModel(config.env.ANTHROPIC_MODEL || "");
|
setClaudeModel(config.env.ANTHROPIC_MODEL || "");
|
||||||
setClaudeSmallFastModel(
|
setClaudeSmallFastModel(
|
||||||
config.env.ANTHROPIC_SMALL_FAST_MODEL || "",
|
config.env.ANTHROPIC_SMALL_FAST_MODEL || ""
|
||||||
);
|
);
|
||||||
setBaseUrl(config.env.ANTHROPIC_BASE_URL || ""); // 初始化基础 URL
|
setBaseUrl(config.env.ANTHROPIC_BASE_URL || ""); // 初始化基础 URL
|
||||||
|
|
||||||
// 初始化 Kimi 模型选择
|
// 初始化 Kimi 模型选择
|
||||||
setKimiAnthropicModel(config.env.ANTHROPIC_MODEL || "");
|
setKimiAnthropicModel(config.env.ANTHROPIC_MODEL || "");
|
||||||
setKimiAnthropicSmallFastModel(
|
setKimiAnthropicSmallFastModel(
|
||||||
config.env.ANTHROPIC_SMALL_FAST_MODEL || "",
|
config.env.ANTHROPIC_SMALL_FAST_MODEL || ""
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -257,7 +263,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
// Codex 初始化时检查 TOML 通用配置
|
// Codex 初始化时检查 TOML 通用配置
|
||||||
const hasCommon = hasTomlCommonConfigSnippet(
|
const hasCommon = hasTomlCommonConfigSnippet(
|
||||||
codexConfig,
|
codexConfig,
|
||||||
codexCommonConfigSnippet,
|
codexCommonConfigSnippet
|
||||||
);
|
);
|
||||||
setUseCodexCommonConfig(hasCommon);
|
setUseCodexCommonConfig(hasCommon);
|
||||||
}
|
}
|
||||||
@@ -277,7 +283,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
if (selectedPreset !== null && selectedPreset >= 0) {
|
if (selectedPreset !== null && selectedPreset >= 0) {
|
||||||
const preset = providerPresets[selectedPreset];
|
const preset = providerPresets[selectedPreset];
|
||||||
setCategory(
|
setCategory(
|
||||||
preset?.category || (preset?.isOfficial ? "official" : undefined),
|
preset?.category || (preset?.isOfficial ? "official" : undefined)
|
||||||
);
|
);
|
||||||
} else if (selectedPreset === -1) {
|
} else if (selectedPreset === -1) {
|
||||||
setCategory("custom");
|
setCategory("custom");
|
||||||
@@ -286,7 +292,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
if (selectedCodexPreset !== null && selectedCodexPreset >= 0) {
|
if (selectedCodexPreset !== null && selectedCodexPreset >= 0) {
|
||||||
const preset = codexProviderPresets[selectedCodexPreset];
|
const preset = codexProviderPresets[selectedCodexPreset];
|
||||||
setCategory(
|
setCategory(
|
||||||
preset?.category || (preset?.isOfficial ? "official" : undefined),
|
preset?.category || (preset?.isOfficial ? "official" : undefined)
|
||||||
);
|
);
|
||||||
} else if (selectedCodexPreset === -1) {
|
} else if (selectedCodexPreset === -1) {
|
||||||
setCategory("custom");
|
setCategory("custom");
|
||||||
@@ -301,7 +307,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
if (commonConfigSnippet.trim()) {
|
if (commonConfigSnippet.trim()) {
|
||||||
window.localStorage.setItem(
|
window.localStorage.setItem(
|
||||||
COMMON_CONFIG_STORAGE_KEY,
|
COMMON_CONFIG_STORAGE_KEY,
|
||||||
commonConfigSnippet,
|
commonConfigSnippet
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
window.localStorage.removeItem(COMMON_CONFIG_STORAGE_KEY);
|
window.localStorage.removeItem(COMMON_CONFIG_STORAGE_KEY);
|
||||||
@@ -364,7 +370,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const currentSettingsError = validateSettingsConfig(
|
const currentSettingsError = validateSettingsConfig(
|
||||||
formData.settingsConfig,
|
formData.settingsConfig
|
||||||
);
|
);
|
||||||
setSettingsConfigError(currentSettingsError);
|
setSettingsConfigError(currentSettingsError);
|
||||||
if (currentSettingsError) {
|
if (currentSettingsError) {
|
||||||
@@ -395,7 +401,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (
|
const handleChange = (
|
||||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||||
) => {
|
) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
|
|
||||||
@@ -413,10 +419,10 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
// 不再从 JSON 自动提取或覆盖官网地址,只更新配置内容
|
// 不再从 JSON 自动提取或覆盖官网地址,只更新配置内容
|
||||||
updateSettingsConfigValue(value);
|
updateSettingsConfigValue(value);
|
||||||
} else {
|
} else {
|
||||||
setFormData({
|
setFormData((prev) => ({
|
||||||
...formData,
|
...prev,
|
||||||
[name]: value,
|
[name]: value,
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -425,7 +431,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
const { updatedConfig, error: snippetError } = updateCommonConfigSnippet(
|
const { updatedConfig, error: snippetError } = updateCommonConfigSnippet(
|
||||||
formData.settingsConfig,
|
formData.settingsConfig,
|
||||||
commonConfigSnippet,
|
commonConfigSnippet,
|
||||||
checked,
|
checked
|
||||||
);
|
);
|
||||||
|
|
||||||
if (snippetError) {
|
if (snippetError) {
|
||||||
@@ -458,7 +464,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
const { updatedConfig } = updateCommonConfigSnippet(
|
const { updatedConfig } = updateCommonConfigSnippet(
|
||||||
formData.settingsConfig,
|
formData.settingsConfig,
|
||||||
previousSnippet,
|
previousSnippet,
|
||||||
false,
|
false
|
||||||
);
|
);
|
||||||
// 直接更新 formData,不通过 handleChange
|
// 直接更新 formData,不通过 handleChange
|
||||||
updateSettingsConfigValue(updatedConfig);
|
updateSettingsConfigValue(updatedConfig);
|
||||||
@@ -480,7 +486,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
const removeResult = updateCommonConfigSnippet(
|
const removeResult = updateCommonConfigSnippet(
|
||||||
formData.settingsConfig,
|
formData.settingsConfig,
|
||||||
previousSnippet,
|
previousSnippet,
|
||||||
false,
|
false
|
||||||
);
|
);
|
||||||
if (removeResult.error) {
|
if (removeResult.error) {
|
||||||
setCommonConfigError(removeResult.error);
|
setCommonConfigError(removeResult.error);
|
||||||
@@ -492,7 +498,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
const addResult = updateCommonConfigSnippet(
|
const addResult = updateCommonConfigSnippet(
|
||||||
removeResult.updatedConfig,
|
removeResult.updatedConfig,
|
||||||
value,
|
value,
|
||||||
true,
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
if (addResult.error) {
|
if (addResult.error) {
|
||||||
@@ -532,7 +538,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
});
|
});
|
||||||
setSettingsConfigError(validateSettingsConfig(configString));
|
setSettingsConfigError(validateSettingsConfig(configString));
|
||||||
setCategory(
|
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")) {
|
if (preset.name?.includes("Kimi")) {
|
||||||
setKimiAnthropicModel(config.env.ANTHROPIC_MODEL || "");
|
setKimiAnthropicModel(config.env.ANTHROPIC_MODEL || "");
|
||||||
setKimiAnthropicSmallFastModel(
|
setKimiAnthropicSmallFastModel(
|
||||||
config.env.ANTHROPIC_SMALL_FAST_MODEL || "",
|
config.env.ANTHROPIC_SMALL_FAST_MODEL || ""
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -604,7 +610,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
// Codex: 应用预设
|
// Codex: 应用预设
|
||||||
const applyCodexPreset = (
|
const applyCodexPreset = (
|
||||||
preset: (typeof codexProviderPresets)[0],
|
preset: (typeof codexProviderPresets)[0],
|
||||||
index: number,
|
index: number
|
||||||
) => {
|
) => {
|
||||||
const authString = JSON.stringify(preset.auth || {}, null, 2);
|
const authString = JSON.stringify(preset.auth || {}, null, 2);
|
||||||
setCodexAuth(authString);
|
setCodexAuth(authString);
|
||||||
@@ -618,7 +624,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
|
|
||||||
setSelectedCodexPreset(index);
|
setSelectedCodexPreset(index);
|
||||||
setCategory(
|
setCategory(
|
||||||
preset.category || (preset.isOfficial ? "official" : undefined),
|
preset.category || (preset.isOfficial ? "official" : undefined)
|
||||||
);
|
);
|
||||||
|
|
||||||
// 清空 API Key,让用户重新输入
|
// 清空 API Key,让用户重新输入
|
||||||
@@ -628,14 +634,23 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
// Codex: 处理点击自定义按钮
|
// Codex: 处理点击自定义按钮
|
||||||
const handleCodexCustomClick = () => {
|
const handleCodexCustomClick = () => {
|
||||||
setSelectedCodexPreset(-1);
|
setSelectedCodexPreset(-1);
|
||||||
|
|
||||||
|
// 设置自定义模板
|
||||||
|
const customAuth = generateThirdPartyAuth("");
|
||||||
|
const customConfig = generateThirdPartyConfig(
|
||||||
|
"custom",
|
||||||
|
"https://your-api-endpoint.com/v1",
|
||||||
|
"gpt-5-codex"
|
||||||
|
);
|
||||||
|
|
||||||
setFormData({
|
setFormData({
|
||||||
name: "",
|
name: "",
|
||||||
websiteUrl: "",
|
websiteUrl: "",
|
||||||
settingsConfig: "",
|
settingsConfig: "",
|
||||||
});
|
});
|
||||||
setSettingsConfigError(validateSettingsConfig(""));
|
setSettingsConfigError(validateSettingsConfig(""));
|
||||||
setCodexAuth("");
|
setCodexAuth(JSON.stringify(customAuth, null, 2));
|
||||||
setCodexConfig("");
|
setCodexConfig(customConfig);
|
||||||
setCodexApiKey("");
|
setCodexApiKey("");
|
||||||
setCategory("custom");
|
setCategory("custom");
|
||||||
};
|
};
|
||||||
@@ -647,7 +662,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
const configString = setApiKeyInConfig(
|
const configString = setApiKeyInConfig(
|
||||||
formData.settingsConfig,
|
formData.settingsConfig,
|
||||||
key.trim(),
|
key.trim(),
|
||||||
{ createIfMissing: selectedPreset !== null && selectedPreset !== -1 },
|
{ createIfMissing: selectedPreset !== null && selectedPreset !== -1 }
|
||||||
);
|
);
|
||||||
|
|
||||||
// 更新表单配置
|
// 更新表单配置
|
||||||
@@ -689,12 +704,9 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
|
|
||||||
// Codex: 处理通用配置开关
|
// Codex: 处理通用配置开关
|
||||||
const handleCodexCommonConfigToggle = (checked: boolean) => {
|
const handleCodexCommonConfigToggle = (checked: boolean) => {
|
||||||
|
const snippet = codexCommonConfigSnippet.trim();
|
||||||
const { updatedConfig, error: snippetError } =
|
const { updatedConfig, error: snippetError } =
|
||||||
updateTomlCommonConfigSnippet(
|
updateTomlCommonConfigSnippet(codexConfig, snippet, checked);
|
||||||
codexConfig,
|
|
||||||
codexCommonConfigSnippet,
|
|
||||||
checked,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (snippetError) {
|
if (snippetError) {
|
||||||
setCodexCommonConfigError(snippetError);
|
setCodexCommonConfigError(snippetError);
|
||||||
@@ -715,16 +727,17 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
|
|
||||||
// Codex: 处理通用配置片段变化
|
// Codex: 处理通用配置片段变化
|
||||||
const handleCodexCommonConfigSnippetChange = (value: string) => {
|
const handleCodexCommonConfigSnippetChange = (value: string) => {
|
||||||
const previousSnippet = codexCommonConfigSnippet;
|
const previousSnippet = codexCommonConfigSnippet.trim();
|
||||||
|
const sanitizedValue = value.trim();
|
||||||
setCodexCommonConfigSnippet(value);
|
setCodexCommonConfigSnippet(value);
|
||||||
|
|
||||||
if (!value.trim()) {
|
if (!sanitizedValue) {
|
||||||
setCodexCommonConfigError("");
|
setCodexCommonConfigError("");
|
||||||
if (useCodexCommonConfig) {
|
if (useCodexCommonConfig) {
|
||||||
const { updatedConfig } = updateTomlCommonConfigSnippet(
|
const { updatedConfig } = updateTomlCommonConfigSnippet(
|
||||||
codexConfig,
|
codexConfig,
|
||||||
previousSnippet,
|
previousSnippet,
|
||||||
false,
|
false
|
||||||
);
|
);
|
||||||
setCodexConfig(updatedConfig);
|
setCodexConfig(updatedConfig);
|
||||||
setUseCodexCommonConfig(false);
|
setUseCodexCommonConfig(false);
|
||||||
@@ -737,12 +750,12 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
const removeResult = updateTomlCommonConfigSnippet(
|
const removeResult = updateTomlCommonConfigSnippet(
|
||||||
codexConfig,
|
codexConfig,
|
||||||
previousSnippet,
|
previousSnippet,
|
||||||
false,
|
false
|
||||||
);
|
);
|
||||||
const addResult = updateTomlCommonConfigSnippet(
|
const addResult = updateTomlCommonConfigSnippet(
|
||||||
removeResult.updatedConfig,
|
removeResult.updatedConfig,
|
||||||
value,
|
sanitizedValue,
|
||||||
true,
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
if (addResult.error) {
|
if (addResult.error) {
|
||||||
@@ -762,7 +775,10 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
// 保存 Codex 通用配置到 localStorage
|
// 保存 Codex 通用配置到 localStorage
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
try {
|
try {
|
||||||
window.localStorage.setItem(CODEX_COMMON_CONFIG_STORAGE_KEY, value);
|
window.localStorage.setItem(
|
||||||
|
CODEX_COMMON_CONFIG_STORAGE_KEY,
|
||||||
|
sanitizedValue
|
||||||
|
);
|
||||||
} catch {
|
} catch {
|
||||||
// ignore localStorage 写入失败
|
// ignore localStorage 写入失败
|
||||||
}
|
}
|
||||||
@@ -774,7 +790,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
if (!isUpdatingFromCodexCommonConfig.current) {
|
if (!isUpdatingFromCodexCommonConfig.current) {
|
||||||
const hasCommon = hasTomlCommonConfigSnippet(
|
const hasCommon = hasTomlCommonConfigSnippet(
|
||||||
value,
|
value,
|
||||||
codexCommonConfigSnippet,
|
codexCommonConfigSnippet
|
||||||
);
|
);
|
||||||
setUseCodexCommonConfig(hasCommon);
|
setUseCodexCommonConfig(hasCommon);
|
||||||
}
|
}
|
||||||
@@ -831,7 +847,12 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
// 获取当前供应商的网址
|
// 获取当前供应商的网址
|
||||||
const getCurrentWebsiteUrl = () => {
|
const getCurrentWebsiteUrl = () => {
|
||||||
if (selectedPreset !== null && selectedPreset >= 0) {
|
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 || "";
|
return formData.websiteUrl || "";
|
||||||
};
|
};
|
||||||
@@ -839,7 +860,12 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
// 获取 Codex 当前供应商的网址
|
// 获取 Codex 当前供应商的网址
|
||||||
const getCurrentCodexWebsiteUrl = () => {
|
const getCurrentCodexWebsiteUrl = () => {
|
||||||
if (selectedCodexPreset !== null && selectedCodexPreset >= 0) {
|
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 || "";
|
return formData.websiteUrl || "";
|
||||||
};
|
};
|
||||||
@@ -868,7 +894,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
codexProviderPresets[selectedCodexPreset]?.category === "official")) ||
|
codexProviderPresets[selectedCodexPreset]?.category === "official")) ||
|
||||||
category === "official";
|
category === "official";
|
||||||
|
|
||||||
// 判断是否显示 Codex 的"获取 API Key"链接
|
// 判断是否显示 Codex 的"获取 API Key"链接(国产官方、聚合站和第三方显示)
|
||||||
const shouldShowCodexApiKeyLink =
|
const shouldShowCodexApiKeyLink =
|
||||||
isCodex &&
|
isCodex &&
|
||||||
!isCodexOfficialPreset &&
|
!isCodexOfficialPreset &&
|
||||||
@@ -887,7 +913,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
// 处理模型输入变化,自动更新 JSON 配置
|
// 处理模型输入变化,自动更新 JSON 配置
|
||||||
const handleModelChange = (
|
const handleModelChange = (
|
||||||
field: "ANTHROPIC_MODEL" | "ANTHROPIC_SMALL_FAST_MODEL",
|
field: "ANTHROPIC_MODEL" | "ANTHROPIC_SMALL_FAST_MODEL",
|
||||||
value: string,
|
value: string
|
||||||
) => {
|
) => {
|
||||||
if (field === "ANTHROPIC_MODEL") {
|
if (field === "ANTHROPIC_MODEL") {
|
||||||
setClaudeModel(value);
|
setClaudeModel(value);
|
||||||
@@ -917,7 +943,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
// Kimi 模型选择处理函数
|
// Kimi 模型选择处理函数
|
||||||
const handleKimiModelChange = (
|
const handleKimiModelChange = (
|
||||||
field: "ANTHROPIC_MODEL" | "ANTHROPIC_SMALL_FAST_MODEL",
|
field: "ANTHROPIC_MODEL" | "ANTHROPIC_SMALL_FAST_MODEL",
|
||||||
value: string,
|
value: string
|
||||||
) => {
|
) => {
|
||||||
if (field === "ANTHROPIC_MODEL") {
|
if (field === "ANTHROPIC_MODEL") {
|
||||||
setKimiAnthropicModel(value);
|
setKimiAnthropicModel(value);
|
||||||
@@ -942,7 +968,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!initialData) return;
|
if (!initialData) return;
|
||||||
const parsedKey = getApiKeyFromConfig(
|
const parsedKey = getApiKeyFromConfig(
|
||||||
JSON.stringify(initialData.settingsConfig),
|
JSON.stringify(initialData.settingsConfig)
|
||||||
);
|
);
|
||||||
if (parsedKey) setApiKey(parsedKey);
|
if (parsedKey) setApiKey(parsedKey);
|
||||||
}, [initialData]);
|
}, [initialData]);
|
||||||
@@ -1023,6 +1049,18 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
applyCodexPreset(codexProviderPresets[index], index)
|
applyCodexPreset(codexProviderPresets[index], index)
|
||||||
}
|
}
|
||||||
onCustomClick={handleCodexCustomClick}
|
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}
|
commonConfigError={codexCommonConfigError}
|
||||||
authError={codexAuthError}
|
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) =>
|
onChange={(e) =>
|
||||||
handleModelChange(
|
handleModelChange(
|
||||||
"ANTHROPIC_SMALL_FAST_MODEL",
|
"ANTHROPIC_SMALL_FAST_MODEL",
|
||||||
e.target.value,
|
e.target.value
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
placeholder="例如: GLM-4.5-Air"
|
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 { X, Save } from "lucide-react";
|
||||||
|
|
||||||
import { isLinux } from "../../lib/platform";
|
import { isLinux } from "../../lib/platform";
|
||||||
|
|
||||||
|
import {
|
||||||
|
generateThirdPartyAuth,
|
||||||
|
generateThirdPartyConfig,
|
||||||
|
} from "../../config/codexProviderPresets";
|
||||||
|
|
||||||
interface CodexConfigEditorProps {
|
interface CodexConfigEditorProps {
|
||||||
authValue: string;
|
authValue: string;
|
||||||
|
|
||||||
configValue: string;
|
configValue: string;
|
||||||
|
|
||||||
onAuthChange: (value: string) => void;
|
onAuthChange: (value: string) => void;
|
||||||
|
|
||||||
onConfigChange: (value: string) => void;
|
onConfigChange: (value: string) => void;
|
||||||
|
|
||||||
onAuthBlur?: () => void;
|
onAuthBlur?: () => void;
|
||||||
|
|
||||||
useCommonConfig: boolean;
|
useCommonConfig: boolean;
|
||||||
|
|
||||||
onCommonConfigToggle: (checked: boolean) => void;
|
onCommonConfigToggle: (checked: boolean) => void;
|
||||||
|
|
||||||
commonConfigSnippet: string;
|
commonConfigSnippet: string;
|
||||||
|
|
||||||
onCommonConfigSnippetChange: (value: string) => void;
|
onCommonConfigSnippetChange: (value: string) => void;
|
||||||
|
|
||||||
commonConfigError: string;
|
commonConfigError: string;
|
||||||
|
|
||||||
authError: string;
|
authError: string;
|
||||||
|
|
||||||
|
isCustomMode?: boolean; // 新增:是否为自定义模式
|
||||||
|
|
||||||
|
onWebsiteUrlChange?: (url: string) => void; // 新增:更新网址回调
|
||||||
|
|
||||||
|
isTemplateModalOpen?: boolean; // 新增:模态框状态
|
||||||
|
|
||||||
|
setIsTemplateModalOpen?: (open: boolean) => void; // 新增:设置模态框状态
|
||||||
|
|
||||||
|
onNameChange?: (name: string) => void; // 新增:更新供应商名称回调
|
||||||
}
|
}
|
||||||
|
|
||||||
const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||||
authValue,
|
authValue,
|
||||||
|
|
||||||
configValue,
|
configValue,
|
||||||
|
|
||||||
onAuthChange,
|
onAuthChange,
|
||||||
|
|
||||||
onConfigChange,
|
onConfigChange,
|
||||||
|
|
||||||
onAuthBlur,
|
onAuthBlur,
|
||||||
|
|
||||||
useCommonConfig,
|
useCommonConfig,
|
||||||
|
|
||||||
onCommonConfigToggle,
|
onCommonConfigToggle,
|
||||||
|
|
||||||
commonConfigSnippet,
|
commonConfigSnippet,
|
||||||
|
|
||||||
onCommonConfigSnippetChange,
|
onCommonConfigSnippetChange,
|
||||||
|
|
||||||
commonConfigError,
|
commonConfigError,
|
||||||
|
|
||||||
authError,
|
authError,
|
||||||
|
|
||||||
|
onWebsiteUrlChange,
|
||||||
|
|
||||||
|
onNameChange,
|
||||||
|
|
||||||
|
isTemplateModalOpen: externalTemplateModalOpen,
|
||||||
|
|
||||||
|
setIsTemplateModalOpen: externalSetTemplateModalOpen,
|
||||||
}) => {
|
}) => {
|
||||||
const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false);
|
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(() => {
|
useEffect(() => {
|
||||||
if (commonConfigError && !isCommonConfigModalOpen) {
|
if (commonConfigError && !isCommonConfigModalOpen) {
|
||||||
setIsCommonConfigModalOpen(true);
|
setIsCommonConfigModalOpen(true);
|
||||||
@@ -38,16 +114,20 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
|||||||
}, [commonConfigError, isCommonConfigModalOpen]);
|
}, [commonConfigError, isCommonConfigModalOpen]);
|
||||||
|
|
||||||
// 支持按下 ESC 关闭弹窗
|
// 支持按下 ESC 关闭弹窗
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isCommonConfigModalOpen) return;
|
if (!isCommonConfigModalOpen) return;
|
||||||
|
|
||||||
const onKeyDown = (e: KeyboardEvent) => {
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
closeModal();
|
closeModal();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener("keydown", onKeyDown);
|
window.addEventListener("keydown", onKeyDown);
|
||||||
|
|
||||||
return () => window.removeEventListener("keydown", onKeyDown);
|
return () => window.removeEventListener("keydown", onKeyDown);
|
||||||
}, [isCommonConfigModalOpen]);
|
}, [isCommonConfigModalOpen]);
|
||||||
|
|
||||||
@@ -55,6 +135,88 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
|||||||
setIsCommonConfigModalOpen(false);
|
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) => {
|
const handleAuthChange = (value: string) => {
|
||||||
onAuthChange(value);
|
onAuthChange(value);
|
||||||
};
|
};
|
||||||
@@ -76,6 +238,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
|||||||
>
|
>
|
||||||
auth.json (JSON) *
|
auth.json (JSON) *
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
id="codexAuth"
|
id="codexAuth"
|
||||||
value={authValue}
|
value={authValue}
|
||||||
@@ -97,9 +260,11 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
|||||||
data-gramm_editor="false"
|
data-gramm_editor="false"
|
||||||
data-enable-grammarly="false"
|
data-enable-grammarly="false"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{authError && (
|
{authError && (
|
||||||
<p className="text-xs text-red-500 dark:text-red-400">{authError}</p>
|
<p className="text-xs text-red-500 dark:text-red-400">{authError}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
Codex auth.json 配置内容
|
Codex auth.json 配置内容
|
||||||
</p>
|
</p>
|
||||||
@@ -113,6 +278,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
|||||||
>
|
>
|
||||||
config.toml (TOML)
|
config.toml (TOML)
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label className="inline-flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 cursor-pointer">
|
<label className="inline-flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -123,6 +289,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
|||||||
写入通用配置
|
写入通用配置
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -132,11 +299,13 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
|||||||
编辑通用配置
|
编辑通用配置
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{commonConfigError && !isCommonConfigModalOpen && (
|
{commonConfigError && !isCommonConfigModalOpen && (
|
||||||
<p className="text-xs text-red-500 dark:text-red-400 text-right">
|
<p className="text-xs text-red-500 dark:text-red-400 text-right">
|
||||||
{commonConfigError}
|
{commonConfigError}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
id="codexConfig"
|
id="codexConfig"
|
||||||
value={configValue}
|
value={configValue}
|
||||||
@@ -154,11 +323,249 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
|||||||
data-gramm_editor="false"
|
data-gramm_editor="false"
|
||||||
data-enable-grammarly="false"
|
data-enable-grammarly="false"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
Codex config.toml 配置内容
|
Codex config.toml 配置内容
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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 && (
|
{isCommonConfigModalOpen && (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 z-50 flex items-center justify-center"
|
className="fixed inset-0 z-50 flex items-center justify-center"
|
||||||
@@ -167,6 +574,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Backdrop - 统一背景样式 */}
|
{/* Backdrop - 统一背景样式 */}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`absolute inset-0 bg-black/50 dark:bg-black/70${
|
className={`absolute inset-0 bg-black/50 dark:bg-black/70${
|
||||||
isLinux() ? "" : " backdrop-blur-sm"
|
isLinux() ? "" : " backdrop-blur-sm"
|
||||||
@@ -174,12 +582,15 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Modal - 统一窗口样式 */}
|
{/* 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">
|
<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 - 统一标题栏样式 */}
|
{/* Header - 统一标题栏样式 */}
|
||||||
|
|
||||||
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-800">
|
<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">
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
||||||
编辑 Codex 通用配置片段
|
编辑 Codex 通用配置片段
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={closeModal}
|
onClick={closeModal}
|
||||||
@@ -191,16 +602,21 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content - 统一内容区域样式 */}
|
{/* Content - 统一内容区域样式 */}
|
||||||
|
|
||||||
<div className="flex-1 overflow-auto p-6 space-y-4">
|
<div className="flex-1 overflow-auto p-6 space-y-4">
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
该片段会在勾选"写入通用配置"时追加到 config.toml 末尾
|
该片段会在勾选"写入通用配置"时追加到 config.toml 末尾
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
value={commonConfigSnippet}
|
value={commonConfigSnippet}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleCommonConfigSnippetChange(e.target.value)
|
handleCommonConfigSnippetChange(e.target.value)
|
||||||
}
|
}
|
||||||
placeholder={`# Common Codex config
|
placeholder={`# Common Codex config
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Add your common TOML configuration here`}
|
# Add your common TOML configuration here`}
|
||||||
rows={12}
|
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"
|
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-gramm_editor="false"
|
||||||
data-enable-grammarly="false"
|
data-enable-grammarly="false"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{commonConfigError && (
|
{commonConfigError && (
|
||||||
<p className="text-sm text-red-500 dark:text-red-400">
|
<p className="text-sm text-red-500 dark:text-red-400">
|
||||||
{commonConfigError}
|
{commonConfigError}
|
||||||
@@ -222,6 +639,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer - 统一底部按钮样式 */}
|
{/* 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">
|
<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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -230,6 +648,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
|||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={closeModal}
|
onClick={closeModal}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ interface PresetSelectorProps {
|
|||||||
onSelectPreset: (index: number) => void;
|
onSelectPreset: (index: number) => void;
|
||||||
onCustomClick: () => void;
|
onCustomClick: () => void;
|
||||||
customLabel?: string;
|
customLabel?: string;
|
||||||
|
renderCustomDescription?: () => React.ReactNode; // 新增:自定义描述渲染
|
||||||
}
|
}
|
||||||
|
|
||||||
const PresetSelector: React.FC<PresetSelectorProps> = ({
|
const PresetSelector: React.FC<PresetSelectorProps> = ({
|
||||||
@@ -25,6 +26,7 @@ const PresetSelector: React.FC<PresetSelectorProps> = ({
|
|||||||
onSelectPreset,
|
onSelectPreset,
|
||||||
onCustomClick,
|
onCustomClick,
|
||||||
customLabel = "自定义",
|
customLabel = "自定义",
|
||||||
|
renderCustomDescription,
|
||||||
}) => {
|
}) => {
|
||||||
const getButtonClass = (index: number, preset?: Preset) => {
|
const getButtonClass = (index: number, preset?: Preset) => {
|
||||||
const isSelected = selectedIndex === index;
|
const isSelected = selectedIndex === index;
|
||||||
@@ -48,6 +50,10 @@ const PresetSelector: React.FC<PresetSelectorProps> = ({
|
|||||||
|
|
||||||
const getDescription = () => {
|
const getDescription = () => {
|
||||||
if (selectedIndex === -1) {
|
if (selectedIndex === -1) {
|
||||||
|
// 如果提供了自定义描述渲染函数,使用它
|
||||||
|
if (renderCustomDescription) {
|
||||||
|
return renderCustomDescription();
|
||||||
|
}
|
||||||
return "手动配置供应商,需要填写完整的配置信息";
|
return "手动配置供应商,需要填写完整的配置信息";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,9 +105,9 @@ const PresetSelector: React.FC<PresetSelectorProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{getDescription() && (
|
{getDescription() && (
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
{getDescription()}
|
{getDescription()}
|
||||||
</p>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ interface ProviderListProps {
|
|||||||
onNotify?: (
|
onNotify?: (
|
||||||
message: string,
|
message: string,
|
||||||
type: "success" | "error",
|
type: "success" | "error",
|
||||||
duration?: number,
|
duration?: number
|
||||||
) => void;
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
|||||||
onNotify?.(
|
onNotify?.(
|
||||||
"未找到 VS Code 用户设置文件 (settings.json)",
|
"未找到 VS Code 用户设置文件 (settings.json)",
|
||||||
"error",
|
"error",
|
||||||
3000,
|
3000
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -157,7 +157,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
|||||||
onNotify?.(
|
onNotify?.(
|
||||||
"未找到 VS Code 用户设置文件 (settings.json)",
|
"未找到 VS Code 用户设置文件 (settings.json)",
|
||||||
"error",
|
"error",
|
||||||
3000,
|
3000
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -230,7 +230,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
|||||||
<div
|
<div
|
||||||
key={provider.id}
|
key={provider.id}
|
||||||
className={cn(
|
className={cn(
|
||||||
isCurrent ? cardStyles.selected : cardStyles.interactive,
|
isCurrent ? cardStyles.selected : cardStyles.interactive
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
@@ -243,7 +243,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
badgeStyles.success,
|
badgeStyles.success,
|
||||||
!isCurrent && "invisible",
|
!isCurrent && "invisible"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<CheckCircle2 size={12} />
|
<CheckCircle2 size={12} />
|
||||||
@@ -284,11 +284,11 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
|||||||
: handleApplyToVSCode(provider)
|
: handleApplyToVSCode(provider)
|
||||||
}
|
}
|
||||||
className={cn(
|
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",
|
!isCurrent && "invisible",
|
||||||
vscodeAppliedFor === provider.id
|
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"
|
? "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"
|
||||||
: "bg-emerald-500 text-white hover:bg-emerald-600 dark:bg-emerald-600 dark:hover:bg-emerald-700",
|
: "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={
|
title={
|
||||||
vscodeAppliedFor === provider.id
|
vscodeAppliedFor === provider.id
|
||||||
@@ -305,13 +305,13 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
|||||||
onClick={() => onSwitch(provider.id)}
|
onClick={() => onSwitch(provider.id)}
|
||||||
disabled={isCurrent}
|
disabled={isCurrent}
|
||||||
className={cn(
|
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
|
isCurrent
|
||||||
? "bg-gray-100 text-gray-400 dark:bg-gray-800 dark:text-gray-500 cursor-not-allowed"
|
? "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 ? "使用中" : "启用"}
|
{isCurrent ? "使用中" : "启用"}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -330,7 +330,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
|||||||
buttonStyles.icon,
|
buttonStyles.icon,
|
||||||
isCurrent
|
isCurrent
|
||||||
? "text-gray-400 cursor-not-allowed"
|
? "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="删除供应商"
|
title="删除供应商"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -10,6 +10,43 @@ export interface CodexProviderPreset {
|
|||||||
config: string; // 将写入 ~/.codex/config.toml(TOML 字符串)
|
config: string; // 将写入 ~/.codex/config.toml(TOML 字符串)
|
||||||
isOfficial?: boolean; // 标识是否为官方预设
|
isOfficial?: boolean; // 标识是否为官方预设
|
||||||
category?: ProviderCategory; // 新增:分类
|
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[] = [
|
export const codexProviderPresets: CodexProviderPreset[] = [
|
||||||
@@ -18,7 +55,6 @@ export const codexProviderPresets: CodexProviderPreset[] = [
|
|||||||
websiteUrl: "https://chatgpt.com/codex",
|
websiteUrl: "https://chatgpt.com/codex",
|
||||||
isOfficial: true,
|
isOfficial: true,
|
||||||
category: "official",
|
category: "official",
|
||||||
// 官方的 key 为null
|
|
||||||
auth: {
|
auth: {
|
||||||
OPENAI_API_KEY: null,
|
OPENAI_API_KEY: null,
|
||||||
},
|
},
|
||||||
@@ -28,21 +64,11 @@ export const codexProviderPresets: CodexProviderPreset[] = [
|
|||||||
name: "PackyCode",
|
name: "PackyCode",
|
||||||
websiteUrl: "https://codex.packycode.com/",
|
websiteUrl: "https://codex.packycode.com/",
|
||||||
category: "third_party",
|
category: "third_party",
|
||||||
// PackyCode 一般通过 API Key;请将占位符替换为你的实际 key
|
auth: generateThirdPartyAuth("sk-your-api-key-here"),
|
||||||
auth: {
|
config: generateThirdPartyConfig(
|
||||||
OPENAI_API_KEY: "sk-your-api-key-here",
|
"packycode",
|
||||||
},
|
"https://codex-api.packycode.com/v1",
|
||||||
config: `model_provider = "packycode"
|
"gpt-5-codex"
|
||||||
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"`,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { ProviderCategory } from "../types";
|
|||||||
export interface ProviderPreset {
|
export interface ProviderPreset {
|
||||||
name: string;
|
name: string;
|
||||||
websiteUrl: string;
|
websiteUrl: string;
|
||||||
|
// 新增:第三方/聚合等可单独配置获取 API Key 的链接
|
||||||
|
apiKeyUrl?: string;
|
||||||
settingsConfig: object;
|
settingsConfig: object;
|
||||||
isOfficial?: boolean; // 标识是否为官方预设
|
isOfficial?: boolean; // 标识是否为官方预设
|
||||||
category?: ProviderCategory; // 新增:分类
|
category?: ProviderCategory; // 新增:分类
|
||||||
@@ -28,8 +30,8 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
env: {
|
env: {
|
||||||
ANTHROPIC_BASE_URL: "https://api.deepseek.com/anthropic",
|
ANTHROPIC_BASE_URL: "https://api.deepseek.com/anthropic",
|
||||||
ANTHROPIC_AUTH_TOKEN: "",
|
ANTHROPIC_AUTH_TOKEN: "",
|
||||||
ANTHROPIC_MODEL: "deepseek-chat",
|
ANTHROPIC_MODEL: "DeepSeek-V3.1-Terminus",
|
||||||
ANTHROPIC_SMALL_FAST_MODEL: "deepseek-chat",
|
ANTHROPIC_SMALL_FAST_MODEL: "DeepSeek-V3.1-Terminus",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
category: "cn_official",
|
category: "cn_official",
|
||||||
@@ -90,6 +92,7 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
{
|
{
|
||||||
name: "PackyCode",
|
name: "PackyCode",
|
||||||
websiteUrl: "https://www.packycode.com",
|
websiteUrl: "https://www.packycode.com",
|
||||||
|
apiKeyUrl: "https://www.packycode.com/?aff=rlo54mgz",
|
||||||
settingsConfig: {
|
settingsConfig: {
|
||||||
env: {
|
env: {
|
||||||
ANTHROPIC_BASE_URL: "https://api.packycode.com",
|
ANTHROPIC_BASE_URL: "https://api.packycode.com",
|
||||||
|
|||||||
Reference in New Issue
Block a user