Compare commits

...

28 Commits

Author SHA1 Message Date
YoVinchen 7cca07c110 Merge branch 'main' into fix/skill-file-extension-1240 2026-03-13 18:42:47 +08:00
Jason 1582d33705 chore: bump version to v3.12.2 and add release notes 2026-03-12 23:50:33 +08:00
Jason 305c0f2e08 feat: improve empty state guidance for first-run experience
Show detailed import instructions and conditionally display common
config snippet hint for Claude/Codex/Gemini (not OpenCode/OpenClaw).
2026-03-12 23:42:15 +08:00
Jason 8e1204b1ee fix: merge Codex MCP restore backup entries during takeover
When proxy takeover is active, update_live_backup_from_provider rebuilds the Codex restore snapshot from the current provider and common-config state. We were then replacing the new mcp_servers table with the previous backup's table wholesale, which meant stopping takeover could restore stale MCP entries and silently discard MCP changes made through provider/common-config updates.

Change the backup preservation step to merge MCP entries by server id instead of replacing the entire table. New provider/common-config MCP definitions now win on conflict, while backup-only servers are retained so live-only MCP state still survives a hot-switch.

Add regression coverage for the existing preservation case and for the conflict case where both the old backup and the new provider define the same MCP server.
2026-03-12 23:15:40 +08:00
Jason 3568c98f57 fix: make Codex TOML base_url editing section-aware
Rewrite setCodexBaseUrl/extractCodexBaseUrl to understand TOML section
boundaries, ensuring base_url is written into the correct
[model_providers.<name>] section instead of being appended to file end.

- Add section-aware TOML helpers in providerConfigUtils.ts
- Extract shared update_codex_toml_field/remove_codex_toml_base_url_if
  in codex_config.rs, deduplicate proxy.rs TOML editing logic
- Replace scattered inline base_url regexes with extractCodexBaseUrl()
- Add comprehensive tests for both Rust and TypeScript implementations
2026-03-12 22:20:59 +08:00
Jason 7ca33ff901 fix: prevent common config loss during proxy takeover and stabilize snippet lifecycle
- Make sync_current_provider_for_app takeover-aware: update restore
  backup instead of overwriting live config when proxy is active
- Introduce explicit "cleared" flag for common config snippets to
  prevent auto-extraction from resurrecting user-cleared snippets
- Reorder startup: extract snippets from clean live files before
  restoring proxy takeover state
- Add one-time migration flag to skip legacy commonConfigEnabled
  migration on subsequent startups
- Add regression tests for takeover backup preservation, explicit
  clear semantics, and migration flag roundtrip
2026-03-12 17:16:22 +08:00
Jason e561084f62 Preserve common config during proxy takeover
Update takeover backup generation to rebuild effective provider settings with common config applied before saving restore snapshots.

Keep Codex mcp_servers entries when hot-switching providers under takeover so restore does not drop live-only MCP config.

Migrate legacy providers with inferred common-config usage to explicit commonConfigEnabled=true markers during startup and default imports, and cover the new behavior with proxy and provider regression tests.
2026-03-12 16:06:07 +08:00
YoVinchen e7a60ef296 fix(skill): support .skill file extension in ZIP import dialog (#1240) 2026-03-12 14:09:44 +08:00
Jason 3dad255a2a chore: bump version to v3.12.1 and add release notes 2026-03-12 00:15:05 +08:00
Jason 51825dac20 feat: add authHeader field to OpenClawProviderConfig and reuse type in form state
Add optional `authHeader` boolean to support vendor-specific auth headers
(e.g. Longcat). Refactor `resetOpenclawState` to use the shared
`OpenClawProviderConfig` type instead of an inline duplicate definition.
2026-03-11 23:56:41 +08:00
Jason ce985763f0 fix: rename OpenCode API format label from "OpenAI" to "OpenAI Responses" 2026-03-11 23:51:11 +08:00
Jason 19dca7cd2b feat: add CTok as sponsor and upgrade Gemini model to 3.1-pro 2026-03-11 23:47:08 +08:00
Jason 70632249a8 feat: add SiliconFlow as sponsor and update affiliate links
Replace SDS logos with SiliconFlow logos, add SiliconFlow sponsor entries
to all three README files (en/zh/ja), fix incorrect alt attribute, and
update all SiliconFlow provider preset apiKeyUrl to affiliate link.
2026-03-11 16:25:38 +08:00
Jason 55210d90d2 fix: update Compshare sponsor registration links to coding-plan page 2026-03-11 15:23:04 +08:00
Jason 239c6fb2d7 fix: prevent common config modal infinite reopen loop and add draft editing
The auto-open useEffect in CodexConfigEditor and GeminiConfigEditor
created an inescapable loop: commonConfigError triggered modal open,
closing the modal didn't clear the error, so the effect immediately
reopened it — locking the entire UI.

- Remove auto-open useEffect from both Codex and Gemini config editors
- Convert common config modals to draft editing (edit locally, validate
  before save) instead of persisting on every keystroke
- Add TOML/JSON pre-validation via parseCommonConfigSnippet before any
  merge operation to prevent invalid content from being persisted
- Expose clearCommonConfigError so editors can clear stale errors on
  modal close
2026-03-11 14:40:14 +08:00
Jason 4ac7e28b10 feat: add XCodeAPI as sponsor and fix incorrect alt attributes
- Add XCodeAPI sponsor entry to all three READMEs (zh, en, ja)
- Fix XCodeAPI logo alt from "Micu" to "XCodeAPI" in README_ZH.md
- Fix Crazyrouter logo alt from "AICoding" to "Crazyrouter" in all READMEs
2026-03-11 11:12:29 +08:00
Jason 47e956bb63 feat: add Micu API as sponsor and update affiliate links
Add Micu (米醋API) sponsor entry to all three README files (EN/ZH/JA)
with logo and description. Update apiKeyUrl for Micu provider presets
across claude, codex, opencode, and openclaw to use affiliate
registration link.
2026-03-11 00:14:40 +08:00
Jason 0bcbffb8a0 feat: rename UCloud provider to Compshare/优云智算 with i18n support
Add optional `nameKey` field to all preset interfaces for localized
display names. The preset selector and form now resolve `nameKey` via
i18next, falling back to `name` when unset.

- zh: 优云智算 / en+ja: Compshare
- Update model ID prefix ucloud/ → compshare/ in OpenClaw presets
- Update partner promotion copy and README sponsor descriptions
2026-03-10 23:43:23 +08:00
Jason 273a756869 fix: sync session search index with query data to refresh list after deletion
Replace useRef+useEffect async index rebuild with useMemo so the
FlexSearch index and the sessions array always reference the same data.
This ensures filtered search results update immediately when a session
is deleted via TanStack Query setQueryData.
2026-03-10 23:06:45 +08:00
Jason c2b60623a6 fix: avoid FK constraint failure when restoring provider_health during WebDAV sync
Split LOCAL_ONLY_TABLES into SYNC_SKIP_TABLES (export) and
SYNC_PRESERVE_TABLES (import). provider_health is skipped on export
but no longer restored on import, since it has a foreign key on
providers(id, app_type) that may not match the remote dataset.
Health data is ephemeral and rebuilds automatically at runtime.
2026-03-10 23:06:45 +08:00
Jason f4ad17d314 fix: align stream check toast i18n interpolation keys with translation placeholders 2026-03-10 23:06:45 +08:00
Jason 236f96b583 fix: correct X-Code API URL from www.x-code.cn to x-code.cc 2026-03-10 23:06:45 +08:00
Zhou Mengze 75b4ef2299 fix: interpolate proxy startup toast address and port (#1399)
Co-authored-by: 周梦泽 <mengze.zhou@dafeng-tech.com>
2026-03-10 21:02:13 +08:00
bigsong fab9874b2c fix: align OpenClaw tool permission profiles with upstream schema (#1355)
* fix: align OpenClaw tool permission profiles with upstream schema

* fix: remove dead i18n keys and save-blocking validation

- Remove unused `profiles.*` nested i18n keys (dead code, ToolsPanel uses flat `profileMinimal` etc.)
- Remove `invalidProfile` i18n key no longer referenced
- Remove handleSave validation that blocked saving allow/deny when legacy profile exists
- Keep the profile destructuring cleanup from the original PR

---------

Co-authored-by: Your Name <your.email@example.com>
Co-authored-by: Jason <farion1231@gmail.com>
2026-03-10 17:53:44 +08:00
liuxxxu 84668e2307 feat(openClaw form): add input type selection for models Advanced Options / 为模型高级选项添加输入类型选择 (#1368)
* feat(openClaw form): add input type selection for models Advanced Options / 为模型高级选项添加输入类型选择

* fix(i18n): add missing openclaw.inputTypes key to all locales

The new inputTypes field in OpenClawFormFields used defaultValue fallback,
causing English and Japanese users to see Chinese text.

---------

Co-authored-by: xu.liu2 <xu.liu2@brgroup.com>
Co-authored-by: Jason <farion1231@gmail.com>
2026-03-10 16:53:40 +08:00
wavever b4033fdd29 fix: add missing authHeader: true to Longcat provider preset (#1377)
Fixes issue where Longcat models were failing with 404 error due to missing
authHeader configuration. According to Longcat API documentation, this
setting is required for proper authentication.

Closes: #1376
2026-03-10 16:14:08 +08:00
hengm3467 471c0d9990 feat: add StepFun provider presets and step-3.5-flash (#1369)
* feat: add StepFun provider presets

* docs: regroup StepFun pricing entry

* docs: tweak StepFun zh label

* style: apply prettier fixes

* Revert "style: apply prettier fixes"

This reverts commit cff7bf2e65.
2026-03-10 10:27:50 +08:00
zuoliangyu a9c36381fc fix: toolbar compact mode not triggering on Windows due to left-side overflow (#1375)
The toolbar container used `justify-end` which caused content to overflow
to the left side. Browser `scrollWidth` does not account for left-side
overflow, so `useAutoCompact` could never detect it and compact mode
never activated — resulting in clipped tab labels (e.g. "aude" instead
of "Claude") at minimum window width.

Fix: remove `justify-end` from the toolbar container and use `ml-auto`
on the child element instead. This keeps the right-aligned appearance
but makes overflow go rightward, where `scrollWidth` correctly detects
it and triggers compact mode.

Co-authored-by: zuolan <zuolan1102@qq.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 09:30:52 +08:00
83 changed files with 5331 additions and 1765 deletions
+1
View File
@@ -24,3 +24,4 @@ flatpak/cc-switch.deb
flatpak-build/
flatpak-repo/
.worktrees/
.spec-workflow/
+80 -1
View File
@@ -5,7 +5,86 @@ 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).
## [Unreleased]
## [3.12.2] - 2026-03-12
Post-v3.12.1 work focuses on Common Config safety during proxy takeover and more reliable Codex TOML editing.
**Stats**: 5 commits | 22 files changed | +1,716 insertions | -288 deletions
### Added
- **Empty State Guidance**: Improved first-run experience with detailed import instructions and a conditional Common Config snippet hint for Claude/Codex/Gemini providers
### Changed
- **Proxy Takeover Restore Flow**: Proxy takeover hot-switch and provider sync now refresh the restore backup instead of overwriting live config files, rebuilding effective provider settings with Common Config applied so rollback preserves the real user configuration
- **Codex TOML Editing Engine**: Refactored Codex `config.toml` updates onto shared section-aware TOML helpers in Rust and TypeScript, covering `base_url` and `model` field edits across provider forms and takeover cleanup
- **Common Config Initialization Lifecycle**: Startup now auto-extracts Common Config snippets from clean live configs before takeover restoration, tracks explicit "snippet cleared" state, and persists a one-time legacy migration flag to avoid repeated backfills
### Fixed
- **Common Config Loss During Takeover**: Fixed cases where proxy takeover could drop Common Config changes, overwrite live configs during sync, or produce incomplete restore snapshots when switching providers
- **Codex Restore Snapshot Preservation**: Fixed Codex takeover restore backups so existing `mcp_servers` blocks survive provider hot-switches instead of being discarded; changed MCP backup preservation from wholesale table replacement to per-server-id merge so provider/common-config MCP updates win on conflict while live-only servers are retained
- **Cleared Snippet Resurrection**: Fixed startup auto-extraction recreating Common Config snippets that users had intentionally cleared
- **Codex `base_url` Misplacement**: Fixed Codex `base_url` extraction and editing to target the active `[model_providers.<name>]` section instead of appending to the file tail or confusing `mcp_servers.*.base_url` entries for provider endpoints
---
## [3.12.1] - 2026-03-12
### Patch Release
Stability-focused patch release fixing the Common Config modal infinite reopen loop, a WebDAV sync foreign key constraint failure, several i18n interpolation issues, and a Windows toolbar compact mode bug. Also adds **StepFun** provider presets, **OpenClaw input type selection** and **authHeader** support, upgrades Gemini to **3.1-pro**, and welcomes four new sponsor partners.
**Stats**: 19 commits | 56 files changed | +1,429 insertions | -396 deletions
### Added
#### Provider Presets
- **StepFun**: Added StepFun (阶跃星辰) provider presets including the step-3.5-flash model across supported applications (#1369, thanks @hengm3467)
#### OpenClaw Enhancements
- **Input Type Selection**: Added input type selection dropdown for model Advanced Options in OpenClaw configuration form (#1368, thanks @liuxxxu)
- **authHeader Field**: Added optional `authHeader` boolean to OpenClawProviderConfig for vendor-specific auth header support (e.g. Longcat), and refactored form state to reuse the shared type
#### Sponsor Partners
- **Micu API**: Added Micu API as sponsor partner with affiliate links
- **XCodeAPI**: Added XCodeAPI as sponsor partner
- **SiliconFlow**: Added SiliconFlow (硅基流动) as sponsor partner with affiliate links
- **CTok**: Added CTok as sponsor partner
### Changed
- **UCloud → Compshare**: Renamed UCloud provider to Compshare (优云智算) with full i18n support across all three locales (EN/ZH/JA)
- **Compshare Links**: Updated Compshare sponsor registration links to coding-plan page
- **Gemini Model Upgrade**: Upgraded default Gemini model from 2.5-pro to 3.1-pro in provider presets
### Fixed
#### Common Config & UI
- **Common Config Modal Loop**: Fixed an infinite reopen loop in the Common Config modal and added draft editing support to prevent data loss during edits
- **Toolbar Compact Mode (Windows)**: Fixed toolbar compact mode not triggering on Windows due to left-side overflow (#1375, thanks @zuoliangyu)
- **Session Search Index**: Fixed session search index not syncing with query data, causing stale list display after session deletion
#### Sync & Data
- **WebDAV Provider Health FK**: Fixed foreign key constraint failure when restoring `provider_health` table during WebDAV sync
#### Provider & Preset
- **Longcat authHeader**: Added missing `authHeader: true` to Longcat provider preset (#1377, thanks @wavever)
- **OpenClaw Tool Permissions**: Aligned OpenClaw tool permission profiles with upstream schema (#1355, thanks @bigsongeth)
- **X-Code API URL**: Corrected X-Code API URL from `www.x-code.cn` to `x-code.cc`
#### i18n & Localization
- **Stream Check Toast**: Fixed stream check toast i18n interpolation keys not matching translation placeholders
- **Proxy Startup Toast**: Fixed proxy startup toast not interpolating address and port values (#1399, thanks @Mason-mengze)
- **OpenCode API Format Label**: Renamed OpenCode API format label from "OpenAI" to "OpenAI Responses" for accuracy
---
+25 -5
View File
@@ -4,7 +4,7 @@
### The All-in-One Manager for Claude Code, Codex, Gemini CLI, OpenCode & OpenClaw
[![Version](https://img.shields.io/badge/version-3.12.0-blue.svg)](https://github.com/farion1231/cc-switch/releases)
[![Version](https://img.shields.io/badge/version-3.12.1-blue.svg)](https://github.com/farion1231/cc-switch/releases)
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey.svg)](https://github.com/farion1231/cc-switch/releases)
[![Built with Tauri](https://img.shields.io/badge/built%20with-Tauri%202-orange.svg)](https://tauri.app/)
[![Downloads](https://img.shields.io/endpoint?url=https://api.pinstudios.net/api/badges/downloads/farion1231/cc-switch/total)](https://github.com/farion1231/cc-switch/releases/latest)
@@ -34,6 +34,11 @@ MiniMax-M2.5 is a SOTA large language model designed for real-world productivity
<td>Thanks to PackyCode for sponsoring this project! PackyCode is a reliable and efficient API relay service provider, offering relay services for Claude Code, Codex, Gemini, and more. PackyCode provides special discounts for our software users: register using <a href="https://www.packyapi.com/register?aff=cc-switch">this link</a> and enter the "cc-switch" promo code during first recharge to get 10% off.</td>
</tr>
<tr>
<td width="180"><a href="https://cloud.siliconflow.cn/i/drGuwc9k"><img src="assets/partners/logos/silicon_en.jpg" alt="SiliconFlow" width="150"></a></td>
<td>Thanks to SiliconFlow for sponsoring this project! SiliconFlow is a high-performance AI infrastructure and model API platform, providing fast and reliable access to language, speech, image, and video models in one place. With pay-as-you-go billing, broad multimodal model support, high-speed inference, and enterprise-grade stability, SiliconFlow helps developers and teams build and scale AI applications more efficiently. Register via <a href="https://cloud.siliconflow.cn/i/drGuwc9k">this link</a> and complete real-name verification to receive ¥20 in bonus credit, usable across models on the platform. SiliconFlow is also now compatible with OpenClaw, allowing users to connect a SiliconFlow API key and call major AI models for free.</td>
</tr>
<tr>
<td width="180"><a href="https://aigocode.com/invite/CC-SWITCH"><img src="assets/partners/logos/aigocode.png" alt="AIGoCode" width="150"></a></td>
<td>Thanks to AIGoCode for sponsoring this project! AIGoCode is an all-in-one platform that integrates Claude Code, Codex, and the latest Gemini models, providing you with stable, efficient, and highly cost-effective AI coding services. The platform offers flexible subscription plans, zero risk of account suspension, direct access with no VPN required, and lightning-fast responses. AIGoCode has prepared a special benefit for CC Switch users: if you register via <a href="https://aigocode.com/invite/CC-SWITCH">this link</a>, you'll receive an extra 10% bonus credit on your first top-up!</td>
@@ -56,8 +61,8 @@ Claude Code / Codex / Gemini official channels at 38% / 2% / 9% of original pric
</tr>
<tr>
<td width="180"><a href="https://www.compshare.cn/coding-plan?ytag=GPU_YY_YX_git_cc-switch"><img src="assets/partners/logos/ucloud.png" alt="Ucloud" width="150"></a></td>
<td>Thanks to UCloud CompShare for sponsoring this project! CompShare is UCloud's AI cloud platform, providing stable and comprehensive domestic and international model APIs with just one key. Featuring cost-effective monthly and pay-as-you-go Coding Plan packages at 60-80% off official prices. Supports Claude Code, Codex, and API access. Enterprise-grade high concurrency, 24/7 technical support, and self-service invoicing. Users who register via <a href="https://www.compshare.cn/?ytag=GPU_YY_YX_git_cc-switch">this link</a> will receive a free 5 CNY platform trial credit!</td>
<td width="180"><a href="https://www.compshare.cn/coding-plan?ytag=GPU_YY_YX_git_cc-switch"><img src="assets/partners/logos/ucloud.png" alt="Compshare" width="150"></a></td>
<td>Thanks to Compshare for sponsoring this project! Compshare is UCloud's AI cloud platform, providing stable and comprehensive domestic and international model APIs with just one key. Featuring cost-effective monthly and pay-as-you-go Coding Plan packages at 60-80% off official prices. Supports Claude Code, Codex, and API access. Enterprise-grade high concurrency, 24/7 technical support, and self-service invoicing. Users who register via <a href="https://www.compshare.cn/coding-plan?ytag=GPU_YY_YX_git_cc-switch">this link</a> will receive a free 5 CNY platform trial credit!</td>
</tr>
<tr>
@@ -71,7 +76,7 @@ Claude Code / Codex / Gemini official channels at 38% / 2% / 9% of original pric
</tr>
<tr>
<td width="180"><a href="https://crazyrouter.com/register?aff=OZcm&ref=cc-switch"><img src="assets/partners/logos/crazyrouter.jpg" alt="AICoding" width="150"></a></td>
<td width="180"><a href="https://crazyrouter.com/register?aff=OZcm&ref=cc-switch"><img src="assets/partners/logos/crazyrouter.jpg" alt="Crazyrouter" width="150"></a></td>
<td>Thanks to Crazyrouter for sponsoring this project! Crazyrouter is a high-performance AI API aggregation platform — one API key for 300+ models including Claude Code, Codex, Gemini CLI, and more. All models at 55% of official pricing with auto-failover, smart routing, and unlimited concurrency. Crazyrouter offers an exclusive deal for CC Switch users: register via <a href="https://crazyrouter.com/register?aff=OZcm&ref=cc-switch">this link</a> to get <strong>$2 free credit</strong> instantly, plus enter promo code `CCSWITCH` on your first top-up for an extra <strong>30% bonus credit</strong>! </td>
</tr>
@@ -80,6 +85,21 @@ Claude Code / Codex / Gemini official channels at 38% / 2% / 9% of original pric
<td>Thanks to SSSAiCode for sponsoring this project! SSSAiCode is a stable and reliable API relay service, dedicated to providing stable, reliable, and affordable Claude and Codex model services, <strong>offering high cost-effective official Claude service at just ¥0.5/$ equivalent</strong>, supporting monthly and pay-as-you-go billing plans with same-day fast invoicing. SSSAiCode offers a special deal for CC Switch users: register via <a href="https://www.sssaicode.com/register?ref=DCP0SM">this link</a> to enjoy $10 extra credit on every top-up!</td>
</tr>
<tr>
<td width="180"><a href="https://www.openclaudecode.cn/register?aff=aOYQ"><img src="assets/partners/logos/mikubanner.svg" alt="Micu" width="150"></a></td>
<td>Thanks to Micu API for sponsoring this project! Micu API is a global LLM relay service provider dedicated to delivering the best cost-performance ratio with high stability. Backed by a registered enterprise for core assurance, eliminating any risk of service discontinuation, with fast official invoicing support! We champion "zero cost to try": top up from as low as ¥1 with no minimum, and get fee-free refunds anytime! Micu API offers an exclusive deal for CC Switch users: register via <a href="https://www.openclaudecode.cn/register?aff=aOYQ">this link</a> and enter promo code "ccswitch" when topping up to enjoy a <strong>10% discount</strong>!</td>
</tr>
<tr>
<td width="180"><a href="https://x-code.cc/register?aff=IbPp"><img src="assets/partners/logos/xcodeapi.png" alt="XCodeAPI" width="150"></a></td>
<td>Thanks to XCodeAPI for sponsoring this project! XCodeAPI offers a special benefit for CC Switch users: register via <a href="https://x-code.cc/register?aff=IbPp">this link</a> and get an extra 10% credit bonus on your first order! (Contact the site admin to claim)</td>
</tr>
<tr>
<td width="180"><a href="https://ctok.ai"><img src="assets/partners/logos/ctok.png" alt="CTok" width="150"></a></td>
<td>Thanks to CTok.ai for sponsoring this project! CTok.ai is dedicated to building a one-stop AI programming tool service platform. We offer professional Claude Code packages and technical community services, with support for Google Gemini and OpenAI Codex. Through carefully designed plans and a professional tech community, we provide developers with reliable service guarantees and continuous technical support, making AI-assisted programming a true productivity tool. Click <a href="https://ctok.ai">here</a> to register!</td>
</tr>
</table>
</details>
@@ -106,7 +126,7 @@ Modern AI-powered coding relies on CLI tools like Claude Code, Codex, Gemini CLI
## Features
[Full Changelog](CHANGELOG.md) | [Release Notes](docs/release-notes/v3.12.0-en.md)
[Full Changelog](CHANGELOG.md) | [Release Notes](docs/release-notes/v3.12.1-en.md)
### Provider Management
+25 -5
View File
@@ -4,7 +4,7 @@
### Claude Code、Codex、Gemini CLI、OpenCode、OpenClaw のオールインワン管理ツール
[![Version](https://img.shields.io/badge/version-3.12.0-blue.svg)](https://github.com/farion1231/cc-switch/releases)
[![Version](https://img.shields.io/badge/version-3.12.1-blue.svg)](https://github.com/farion1231/cc-switch/releases)
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey.svg)](https://github.com/farion1231/cc-switch/releases)
[![Built with Tauri](https://img.shields.io/badge/built%20with-Tauri%202-orange.svg)](https://tauri.app/)
[![Downloads](https://img.shields.io/endpoint?url=https://api.pinstudios.net/api/badges/downloads/farion1231/cc-switch/total)](https://github.com/farion1231/cc-switch/releases/latest)
@@ -34,6 +34,11 @@ MiniMax-M2.5 は、実際の生産性向上のために設計された最先端
<td>PackyCode のご支援に感謝します!PackyCode は Claude Code、Codex、Gemini などのリレーサービスを提供する信頼性の高い API 中継プラットフォームです。本ソフト利用者向けに特別割引があります:<a href="https://www.packyapi.com/register?aff=cc-switch">このリンク</a>で登録し、チャージ時に「cc-switch」クーポンを入力すると 10% オフになります。</td>
</tr>
<tr>
<td width="180"><a href="https://cloud.siliconflow.cn/i/drGuwc9k"><img src="assets/partners/logos/silicon_en.jpg" alt="SiliconFlow" width="150"></a></td>
<td>SiliconFlow のご支援に感謝します!SiliconFlow は高性能 AI インフラストラクチャおよびモデル API プラットフォームで、言語・音声・画像・動画モデルへの高速かつ信頼性の高いアクセスをワンストップで提供します。従量課金制、豊富なマルチモーダルモデル対応、高速推論、エンタープライズグレードの安定性を備え、開発者やチームがより効率的に AI アプリケーションを構築・拡張できるようサポートします。<a href="https://cloud.siliconflow.cn/i/drGuwc9k">このリンク</a>から登録し、本人確認を完了すると、プラットフォーム内の全モデルで利用可能な ¥20 のボーナスクレジットが付与されます。SiliconFlow は OpenClaw にも対応しており、SiliconFlow の API キーを接続することで主要な AI モデルを無料で呼び出すことができます。</td>
</tr>
<tr>
<td width="180"><a href="https://aigocode.com/invite/CC-SWITCH"><img src="assets/partners/logos/aigocode.png" alt="AIGoCode" width="150"></a></td>
<td>本プロジェクトは AIGoCode のスポンサー提供でお届けしています。AIGoCode は、Claude Code・Codex・最新の Gemini モデルを統合したオールインワンのAIコーディングプラットフォームで、安定性・高速性・コストパフォーマンスに優れた開発サービスを提供します。柔軟なサブスクリプションプランを備え、レスポンスも非常に高速です。さらに、CC Switch ユーザー向けの特典として、<a href="https://aigocode.com/invite/CC-SWITCH">このリンク</a>から登録すると、初回チャージ時に10%分のボーナスクレジットが付与されます!</td>
@@ -56,8 +61,8 @@ Claude Code / Codex / Gemini 公式チャンネルが最安で元価格の 38% /
</tr>
<tr>
<td width="180"><a href="https://www.compshare.cn/coding-plan?ytag=GPU_YY_YX_git_cc-switch"><img src="assets/partners/logos/ucloud.png" alt="Ucloud" width="150"></a></td>
<td>UCloud CompShare のご支援に感謝します!CompShare は UCloud 傘下の AI クラウドプラットフォームで、国内外の安定した包括的なモデル API を 1 つのキーだけで利用可能。月額・従量課金のコストパフォーマンスに優れた Coding Plan パッケージを提供し、公式価格の 60〜80% オフで利用できます。Claude Code、Codex および API アクセスに対応。エンタープライズ級の高同時接続、24 時間年中無休のテクニカルサポート、セルフサービス請求書発行に対応。<a href="https://www.compshare.cn/?ytag=GPU_YY_YX_git_cc-switch">こちらのリンク</a>から登録すると、無料で 5 元分のプラットフォーム体験クレジットがもらえます!</td>
<td width="180"><a href="https://www.compshare.cn/coding-plan?ytag=GPU_YY_YX_git_cc-switch"><img src="assets/partners/logos/ucloud.png" alt="Compshare" width="150"></a></td>
<td>Compshare のご支援に感謝します!Compshare は UCloud 傘下の AI クラウドプラットフォームで、国内外の安定した包括的なモデル API を 1 つのキーだけで利用可能。月額・従量課金のコストパフォーマンスに優れた Coding Plan パッケージを提供し、公式価格の 60〜80% オフで利用できます。Claude Code、Codex および API アクセスに対応。エンタープライズ級の高同時接続、24 時間年中無休のテクニカルサポート、セルフサービス請求書発行に対応。<a href="https://www.compshare.cn/coding-plan?ytag=GPU_YY_YX_git_cc-switch">こちらのリンク</a>から登録すると、無料で 5 元分のプラットフォーム体験クレジットがもらえます!</td>
</tr>
<tr>
@@ -71,7 +76,7 @@ Claude Code / Codex / Gemini 公式チャンネルが最安で元価格の 38% /
</tr>
<tr>
<td width="180"><a href="https://crazyrouter.com/register?aff=OZcm&ref=cc-switch"><img src="assets/partners/logos/crazyrouter.jpg" alt="AICoding" width="150"></a></td>
<td width="180"><a href="https://crazyrouter.com/register?aff=OZcm&ref=cc-switch"><img src="assets/partners/logos/crazyrouter.jpg" alt="Crazyrouter" width="150"></a></td>
<td>Crazyrouter のご支援に感謝します!Crazyrouter は高性能 AI API アグリゲーションプラットフォームです。1 つの API キーで Claude Code、Codex、Gemini CLI など 300 以上のモデルにアクセス可能。全モデルが公式価格の 55% で利用でき、自動フェイルオーバー、スマートルーティング、無制限同時接続に対応。CC Switch ユーザー向けの限定特典:<a href="https://crazyrouter.com/register?aff=OZcm&ref=cc-switch">こちらのリンク</a>から登録すると <strong>$2 の無料クレジット</strong> を即時進呈。さらに初回チャージ時にプロモコード `CCSWITCH` を入力すると <strong>30% のボーナスクレジット</strong> が追加されます!</td>
</tr>
@@ -80,6 +85,21 @@ Claude Code / Codex / Gemini 公式チャンネルが最安で元価格の 38% /
<td>SSSAiCode のご支援に感謝します!SSSAiCode は安定性と信頼性に優れた API 中継サービスで、安定的で信頼性が高く、手頃な価格の Claude・Codex モデルサービスを提供しています。<strong>高コストパフォーマンスの公式 Claude サービスを 0.5¥/$ 換算で提供</strong>、月額制・Paygo など多様な課金方式に対応し、当日の迅速な請求書発行をサポート。CC Switch ユーザー向けの特別特典:<a href="https://www.sssaicode.com/register?ref=DCP0SM">こちらのリンク</a>から登録すると、毎回のチャージで $10 の追加ボーナスを受けられます!</td>
</tr>
<tr>
<td width="180"><a href="https://www.openclaudecode.cn/register?aff=aOYQ"><img src="assets/partners/logos/mikubanner.svg" alt="Micu" width="150"></a></td>
<td>Micu API のご支援に感謝します!Micu API は、最高のコストパフォーマンスと高い安定性を追求するグローバル大規模言語モデル中継サービスプロバイダーです。法人企業がバックアップしており、サービス停止のリスクを排除、迅速な正規請求書発行に対応!「試行コストゼロ」をモットーに、最低 1 元からチャージ可能で手数料無料、いつでも返金可能!CC Switch ユーザー向けの限定特典:<a href="https://www.openclaudecode.cn/register?aff=aOYQ">こちらのリンク</a>から登録し、チャージ時にプロモコード「ccswitch」を入力すると <strong>10% 割引</strong> が適用されます!</td>
</tr>
<tr>
<td width="180"><a href="https://x-code.cc/register?aff=IbPp"><img src="assets/partners/logos/xcodeapi.png" alt="XCodeAPI" width="150"></a></td>
<td>XCodeAPI のご支援に感謝します!CC Switch ユーザー向けの特別特典:<a href="https://x-code.cc/register?aff=IbPp">こちらのリンク</a>から登録すると、初回注文で 10% の追加クレジットボーナスがもらえます!(サイト管理者に連絡して受け取りください)</td>
</tr>
<tr>
<td width="180"><a href="https://ctok.ai"><img src="assets/partners/logos/ctok.png" alt="CTok" width="150"></a></td>
<td>CTok.ai のご支援に感謝します!CTok.ai はワンストップ AI プログラミングツールサービスプラットフォームの構築に取り組んでいます。Claude Code のプロフェッショナルプランと技術コミュニティサービスを提供し、Google Gemini や OpenAI Codex にも対応しています。丁寧に設計されたプランと専門的な技術コミュニティを通じて、開発者に安定したサービス保証と継続的な技術サポートを提供し、AI アシストプログラミングを真の生産性ツールにします。<a href="https://ctok.ai">こちら</a>から登録してください!</td>
</tr>
</table>
</details>
@@ -106,7 +126,7 @@ Claude Code / Codex / Gemini 公式チャンネルが最安で元価格の 38% /
## 特長
[完全な更新履歴](CHANGELOG.md) | [リリースノート](docs/release-notes/v3.12.0-ja.md)
[完全な更新履歴](CHANGELOG.md) | [リリースノート](docs/release-notes/v3.12.1-ja.md)
### プロバイダ管理
+25 -5
View File
@@ -4,7 +4,7 @@
### Claude Code、Codex、Gemini CLI、OpenCode 和 OpenClaw 的全方位管理工具
[![Version](https://img.shields.io/badge/version-3.12.0-blue.svg)](https://github.com/farion1231/cc-switch/releases)
[![Version](https://img.shields.io/badge/version-3.12.1-blue.svg)](https://github.com/farion1231/cc-switch/releases)
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey.svg)](https://github.com/farion1231/cc-switch/releases)
[![Built with Tauri](https://img.shields.io/badge/built%20with-Tauri%202-orange.svg)](https://tauri.app/)
[![Downloads](https://img.shields.io/endpoint?url=https://api.pinstudios.net/api/badges/downloads/farion1231/cc-switch/total)](https://github.com/farion1231/cc-switch/releases/latest)
@@ -34,6 +34,11 @@ MiniMax M2.5 在编程、工具调用与搜索、办公等核心生产力场景
<td>感谢 PackyCode 赞助了本项目!PackyCode 是一家稳定、高效的API中转服务商,提供 Claude Code、Codex、Gemini 等多种中转服务。PackyCode 为本软件的用户提供了特别优惠,使用<a href="https://www.packyapi.com/register?aff=cc-switch">此链接</a>注册并在充值时填写"cc-switch"优惠码,首次充值可以享受9折优惠!</td>
</tr>
<tr>
<td width="180"><a href="https://cloud.siliconflow.cn/i/drGuwc9k"><img src="assets/partners/logos/silicon_zh.jpg" alt="SiliconFlow" width="150"></a></td>
<td>感谢硅基流动赞助了本项目!硅基流动是一个高性能 AI 基础设施与模型 API 平台,一站式提供语言、语音、图像、视频等多模态模型的快速、可靠访问。平台支持按量计费、丰富的多模态模型选择、高速推理和企业级稳定性,帮助开发者和团队更高效地构建和扩展 AI 应用。通过<a href="https://cloud.siliconflow.cn/i/drGuwc9k">此链接</a>注册并完成实名认证,即可获得 ¥20 奖励金,可在平台内跨模型使用。硅基流动现已兼容 OpenClaw,用户可接入硅基流动 API Key 免费调用主流 AI 模型。</td>
</tr>
<tr>
<td width="180"><a href="https://aigocode.com/invite/CC-SWITCH"><img src="assets/partners/logos/aigocode.png" alt="AIGoCode" width="150"></a></td>
<td>感谢 AIGoCode 赞助了本项目!AIGoCode 是一个集成了 Claude Code、Codex 以及 Gemini 最新模型的一站式平台,为你提供稳定、高效且高性价比的AI编程服务。本站提供灵活的订阅计划,零封号风险,国内直连,无需魔法,极速响应。AIGoCode 为 CC Switch 的用户提供了特别福利,通过<a href="https://aigocode.com/invite/CC-SWITCH">此链接</a>注册的用户首次充值可以获得额外10%奖励额度!</td>
@@ -57,8 +62,8 @@ Claude Code / Codex / Gemini 官方渠道低至 3.8 / 0.2 / 0.9 折,充值更
</tr>
<tr>
<td width="180"><a href="https://www.compshare.cn/coding-plan?ytag=GPU_YY_YX_git_cc-switch"><img src="assets/partners/logos/ucloud.png" alt="Ucloud" width="150"></a></td>
<td>感谢优云智算赞助了本项目!优云智算是UCloud旗下AI云平台,提供稳定、全面的国内外模型API,仅一个key即可调用。主打包月、按量的高性价比 Coding Plan 套餐,基于官方2~5折优惠。支持接入 Claude Code、Codex 及 API 调用。支持企业高并发、7*24技术支持、自助开票。通过<a href="https://www.compshare.cn/?ytag=GPU_YY_YX_git_cc-switch">此链接</a>注册的用户,可得免费5元平台体验金!</td>
<td width="180"><a href="https://www.compshare.cn/coding-plan?ytag=GPU_YY_YX_git_cc-switch"><img src="assets/partners/logos/ucloud.png" alt="优云智算" width="150"></a></td>
<td>感谢优云智算赞助了本项目!优云智算是UCloud旗下AI云平台,提供稳定、全面的国内外模型API,仅一个key即可调用。主打包月、按量的高性价比 Coding Plan 套餐,基于官方2~5折优惠。支持接入 Claude Code、Codex 及 API 调用。支持企业高并发、7*24技术支持、自助开票。通过<a href="https://www.compshare.cn/coding-plan?ytag=GPU_YY_YX_git_cc-switch">此链接</a>注册的用户,可得免费5元平台体验金!</td>
</tr>
<tr>
@@ -72,7 +77,7 @@ Claude Code / Codex / Gemini 官方渠道低至 3.8 / 0.2 / 0.9 折,充值更
</tr>
<tr>
<td width="180"><a href="https://crazyrouter.com/register?aff=OZcm&ref=cc-switch"><img src="assets/partners/logos/crazyrouter.jpg" alt="AICoding" width="150"></a></td>
<td width="180"><a href="https://crazyrouter.com/register?aff=OZcm&ref=cc-switch"><img src="assets/partners/logos/crazyrouter.jpg" alt="Crazyrouter" width="150"></a></td>
<td>感谢 Crazyrouter 赞助了本项目!Crazyrouter 是一个高性能 AI API 聚合平台——一个 API Key 即可访问 300+ 模型,包括 Claude Code、Codex、Gemini CLI 等。全部模型低至官方定价的 55%,支持自动故障转移、智能路由和无限并发。Crazyrouter 为 CC Switch 用户提供了专属优惠:通过<a href="https://crazyrouter.com/register?aff=OZcm&ref=cc-switch">此链接</a>注册即可获得 <strong>$2 免费额度</strong>,首次充值时输入优惠码 `CCSWITCH` 还可获得额外 <strong>30% 奖励额度</strong></td>
</tr>
@@ -81,6 +86,21 @@ Claude Code / Codex / Gemini 官方渠道低至 3.8 / 0.2 / 0.9 折,充值更
<td>感谢 SSSAiCode 赞助了本项目!SSSAiCode 是一家稳定可靠的API中转站,致力于提供稳定、可靠、平价的Claude、CodeX模型服务,<strong>提供高性价比折合0.5¥/$的官方Claude服务</strong>,支持包月、Paygo多种计费方式、支持当日快速开票,SSSAiCode为本软件的用户提供特别优惠,使用<a href="https://www.sssaicode.com/register?ref=DCP0SM">此链接</a>注册每次充值均可享受10$的额外奖励!</td>
</tr>
<tr>
<td width="180"><a href="https://www.openclaudecode.cn/register?aff=aOYQ"><img src="assets/partners/logos/mikubanner.svg" alt="Micu" width="150"></a></td>
<td>感谢 米醋API 赞助了本项目!米醋API 是一家致力于提供极致性价比与高稳定性的全球大模型中转服务商。米醋API 背后有实体企业做核心保障,杜绝跑路风险,支持极速正规开票!我们主打“试错零成本”:1 元起充低门槛,0 手续费随时退款!米醋API 为本软件的用户提供了特别优惠,使用<a href="https://www.openclaudecode.cn/register?aff=aOYQ">此链接</a>注册并在充值时填写"ccswitch"优惠码可享九折优惠!</td>
</tr>
<tr>
<td width="180"><a href="https://x-code.cc/register?aff=IbPp"><img src="assets/partners/logos/xcodeapi.png" alt="XCodeAPI" width="150"></a></td>
<td>感谢 XCodeAPI 赞助了本项目!XCodeAPI 为本软件的用户提供特别福利,使用<a href="https://x-code.cc/register?aff=IbPp">此链接</a>注册后首单加赠10%的额度!(联系站长领取)</td>
</tr>
<tr>
<td width="180"><a href="https://ctok.ai"><img src="assets/partners/logos/ctok.png" alt="CTok" width="150"></a></td>
<td>感谢 CTok.ai 赞助了本项目!CTok.ai 致力于打造一站式 AI 编程工具服务平台。我们提供 Claude Code 专业套餐及技术社群服务,同时支持 Google Gemini 和 OpenAI Codex。通过精心设计的套餐方案和专业的技术社群,为开发者提供稳定的服务保障和持续的技术支持,让 AI 辅助编程真正成为开发者的生产力工具。点击<a href="https://ctok.ai">这里</a>注册!</td>
</tr>
</table>
</details>
@@ -107,7 +127,7 @@ Claude Code / Codex / Gemini 官方渠道低至 3.8 / 0.2 / 0.9 折,充值更
## 功能特性
[完整更新日志](CHANGELOG.md) | [发布说明](docs/release-notes/v3.12.0-zh.md)
[完整更新日志](CHANGELOG.md) | [发布说明](docs/release-notes/v3.12.1-zh.md)
### 供应商管理
Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

+63
View File
@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_图层_2" data-name="图层 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1075.78 240.6">
<defs>
<style>
.cls-1 {
fill: #068cde;
}
.cls-2 {
fill: #02a4fd;
}
.cls-3 {
fill: #fff;
}
.cls-4 {
fill: #02a6ff;
}
</style>
</defs>
<g id="_图层_1-2" data-name="图层 1">
<path class="cls-1" d="M226.14,157.63c-3.62,0-7.24,0-10.95,0v-24.96c5.2,0,10.17,0,15.13,0,5.55-.01,8.52-4.01,10.18-8.16,1.34-3.37,1.36-7.51-1.34-11.1-3.16-4.2-7.23-5.72-12.25-5.63-3.87.07-7.74.01-11.66.01v-24.79c6.56-.43,12.93.45,19.3-1.13.4-.42.85-1.05,1.44-1.49,4.61-3.47,6.48-9.22,4.67-14.51-1.63-4.76-6.67-8.03-12.22-7.99-4.37.03-8.74,0-13.42,0,0-5.79.1-11.16-.04-16.54-.08-3.23-1.09-6.23-3.22-8.79-7.6-9.17-17.84-6.02-28.13-6.29-.39-.23-.63-1.14-.45-2.35.73-4.72.37-9.44-.24-14.13-.51-3.95-5.79-9.24-9.1-9.56-10.42-1-15.88,5.21-15.83,14.89.02,3.54,0,7.08,0,10.92h-24.44c-.76-1.03-.45-2.13-.42-3.19.15-5.2.71-10.35-1.11-15.5-2.15-6.08-11.68-9.27-17.05-5.79-5.27,3.41-7.22,7.95-6.99,13.99.14,3.56.53,7.22-.44,10.6h-23.98c-.22-.38-.37-.51-.37-.66-.05-4.56,0-9.12-.14-13.68-.15-5.18-4.72-10.76-9.31-11.57-8.81-1.55-15.64,4.23-15.64,13.24,0,4.11,0,8.21,0,12.61-4.92,0-9.32.08-13.72-.02-10.41-.22-18.81,7.79-17.84,18.57.39,4.32.06,8.7.06,13.34-5.17,0-9.9-.05-14.63.01-5.36.07-11.08,5.57-11.83,10.1-1.39,8.44,6.23,15.25,14.55,14.78,3.86-.22,7.74-.04,11.75-.04v24.92c-4.45,0-8.67-.07-12.89.02-3.2.07-6.5.62-8.75,2.93-3.6,3.69-6.1,8.11-4.12,13.5,2.13,5.8,6.42,8.43,12.98,8.43,4.21,0,8.42,0,12.83,0v24.95c-3.48,0-6.79-.12-10.08.03-3.74.18-7.43.36-10.78,2.69-4.11,2.87-6.48,8.21-5.32,12.83,1.1,4.36,7.17,9.83,11.59,9.41,4.82-.46,9.72-.1,14.69-.1,0,5.18.51,9.88-.1,14.44-1.36,10,8.64,18.06,17.22,17.5,4.62-.3,9.28-.05,14.15-.05,1.26,9.11-3.28,19.95,9.5,25.9,10.27,1.43,15.74-3.33,15.75-15.14,0-3.45,0-6.9,0-10.55h24.94c0,3.39,0,6.6,0,9.8,0,3.81.26,7.41,2.73,10.76,3.09,4.2,8.64,6.68,14,4.87,3.62-1.22,8.31-5.43,8.3-10.7,0-4.85,0-9.7,0-14.7h24.92c0,3.98-.14,7.7.03,11.41.22,4.83,1.4,9.35,5.68,12.32,5.16,3.59,12.81,2.96,17.28-2.41,3.93-7.43,2.05-14.57,2.39-21.58,5.71,0,11.1.03,16.49-.02,1.98-.02,4.05-.06,5.8-1.09,6.78-3.99,9.8-9.94,9.36-17.81-.23-4.18-.04-8.39-.04-12.56,8.59-1.68,18.23,2.96,24.73-6.3.08-.31.31-1.1.5-1.9.97-4.19,1.82-8.06-1.59-12.01-3.49-4.05-7.66-5.05-12.52-5.04ZM168.3,160.54c0,3.41-1.48,4.58-4.69,4.49-4.41-.12-8.82-.09-13.23-.05-3.15.03-4.43-1.39-4.41-4.56.06-16.85.02-33.69-.03-50.54,0-.99.44-2.13-.73-3.15-4.49,13.97-8.94,27.8-13.44,41.79h-21.8c-4.39-13.78-8.8-27.62-13.55-42.54-.15,1.94-.29,2.89-.29,3.84-.01,16.68-.08,33.36.04,50.04.03,3.7-1.18,5.38-5.05,5.16-5.51-.31-11.08.38-16.22-.44-1.24-1.59-1.21-2.95-1.21-4.26.07-27.3.18-54.6.22-81.9,0-2.16.89-3.46,2.97-3.48,9.9-.06,19.8,0,29.7.05.31,0,.63.19,1.39.44,4.22,14.29,8.51,28.79,12.8,43.3.29.05.58.1.87.15,4.53-14.59,9.07-29.17,13.66-43.95,10.35,0,20.48-.03,30.62.03,1.76.01,2.38,1.37,2.42,2.92.08,2.57.1,5.14.1,7.71-.06,24.98-.16,49.95-.15,74.93Z"/>
<rect class="cls-4" x="48.86" y="48.46" width="143.67" height="143.67" rx="10.57" ry="10.57"/>
<path class="cls-3" d="M165.55,75.28c-10.14-.06-20.27-.03-30.62-.03-4.59,14.78-9.12,29.36-13.66,43.95-.29-.05-.58-.1-.87-.15-4.29-14.51-8.58-29.01-12.8-43.3-.77-.25-1.08-.44-1.39-.44-9.9-.04-19.8-.1-29.7-.05-2.08.01-2.96,1.32-2.97,3.48-.04,27.3-.15,54.6-.22,81.9,0,1.31-.03,2.67,1.21,4.26,5.13.82,10.7.13,16.22.44,3.87.22,5.07-1.46,5.05-5.16-.12-16.68-.06-33.36-.04-50.04,0-.95.14-1.9.29-3.84,4.75,14.91,9.16,28.76,13.55,42.54h21.8c4.5-13.99,8.94-27.82,13.44-41.79,1.18,1.01.73,2.16.73,3.15.04,16.85.09,33.69.03,50.54-.01,3.17,1.26,4.59,4.41,4.56,4.41-.04,8.82-.07,13.23.05,3.21.09,4.69-1.08,4.69-4.49-.01-24.98.09-49.95.15-74.93,0-2.57-.02-5.14-.1-7.71-.05-1.55-.67-2.91-2.42-2.92Z"/>
<g>
<path class="cls-2" d="M372.64,135.48c-.13-.49-.54-.78-.71-.89-7.15-4.87-14.15-9.95-21.35-14.75-4.85-3.24-7.95-5.93-8.98-6.85-3.54-3.13-6.16-6.05-7.88-8.11,18.27.05,31.65-.03,32.44-.05.12,0,.6-.03.98-.42.22-.22.31-.45.35-.59.02-4.96.04-9.91.06-14.87,0-.78-.62-1.42-1.39-1.42-8.12.04-16.23.08-24.35.12,4.03-4.67,7.45-8.18,9.86-10.55,2.43-2.39,4.46-5.14,6.77-7.64,1.93-2.09,4.11-4.37,3.5-6.38-.2-.66-.63-1.08-.87-1.29-3.46-2.9-6.93-5.8-10.39-8.7-.41-.25-1.2-.63-2.04-.42-.82.21-1.3.88-1.47,1.12-3.4,4.75-5.17,7.07-5.2,7.12,0,0-1.32,2.51-13.36,16.27-.12.14-.48.54-.48,1.08,0,.5.31.88.49,1.07,2.96,2.73,5.93,5.46,8.89,8.2h-10.94v-37.5c0-.78-.62-1.42-1.39-1.42h-15.19c-.77,0-1.39.64-1.39,1.42v37.5h-13.87l10.91-9.45c.55-.48.65-1.31.23-1.91-1.08-1.54-2.47-3.35-4.11-5.37-1.64-2.02-3.6-4.28-5.8-6.7-2.13-2.43-4.13-4.59-5.93-6.43-1.8-1.83-3.5-3.43-5.06-4.75-.53-.45-1.31-.43-1.82.04l-10.51,9.67c-.29.27-.46.65-.46,1.05s.17.78.46,1.05c.96.88,2.1,2.06,3.39,3.49,1.33,1.47,2.71,3.04,4.15,4.7,1.44,1.66,2.87,3.36,4.26,5.04,1.41,1.71,2.71,3.27,3.89,4.68,1.17,1.39,2.11,2.54,2.83,3.44.86,1.09,1.04,1.35,1.04,1.35.02.03.03.06.05.09h-23.14c-.77,0-1.39.64-1.39,1.42v14.45c0,.78.62,1.42,1.39,1.42,10.73.14,21.47.29,32.2.43-3.44,3.44-6.64,6.34-9.41,8.73-4.74,4.08-8.53,6.9-9.53,7.64-5.15,3.8-7.72,5.7-9.46,6.47,0,0-1.99,2.06-9.47,6.03-.37.2-.63.55-.72.96-.09.41.01.84.27,1.18,3.31,4.84,6.62,9.68,9.93,14.51,2.17-1.39,5.05-3.3,8.34-5.71,1.13-.83,4.24-3.11,8.34-6.5,6.58-5.43,8.21-7.48,15.62-13.59,1.43-1.18,2.61-2.13,3.36-2.73v32.48c0,.78.62,1.42,1.39,1.42h15.19c.77,0,1.39-.64,1.39-1.42v-32.91c2.78,2.65,6.21,5.83,10.21,9.33,10.52,9.2,9.2,6.82,12.78,10.6,0,0,7.52,6.23,12.33,9.29.65.41,1.5.21,1.91-.45l8.68-14c.21-.34.27-.75.17-1.13Z"/>
<g>
<path class="cls-2" d="M481.65,98.3h-43.96c-.78,0-1.42.63-1.42,1.42v51.72c0,.78.63,1.42,1.42,1.42h43.96c.78,0,1.42-.63,1.42-1.42v-51.72c0-.78-.63-1.42-1.42-1.42ZM467.34,137.4h-15.33v-4.1h15.33v4.1ZM467.34,118.08h-15.33v-4.1h15.33v4.1Z"/>
<path class="cls-2" d="M485.91,80.91h-8.67v-6.03h6.54c.78,0,1.42-.63,1.42-1.42v-13.3c0-.78-.63-1.42-1.42-1.42h-6.54v-9.15c0-.78-.63-1.42-1.42-1.42h-12.56c-.78,0-1.42.63-1.42,1.42v9.15h-4.46v-9.15c0-.78-.63-1.42-1.42-1.42h-12.45c-.78,0-1.42.63-1.42,1.42v9.15h-5.54c-.56,0-1.04.32-1.27.79v-5.86c0-.78-.63-1.42-1.42-1.42h-48.55c-.78,0-1.42.63-1.42,1.42v12.96c0,.78.63,1.42,1.42,1.42h11.48v5.24h-9.91c-.78,0-1.42.63-1.42,1.42v76.27c0,.78.63,1.42,1.42,1.42h46.2c.78,0,1.42-.63,1.42-1.42v-54.75c.26.29.63.46,1.05.46h50.35c.78,0,1.42-.63,1.42-1.42v-12.96c0-.78-.63-1.42-1.42-1.42ZM421.7,136.6h-23.18v-3.98h23.18v3.98ZM421.7,117.28h-19.16c1.54-2.24,2.74-4.23,3.57-5.91.83-1.69,1.57-3.33,2.19-4.91,1.19-3.22,1.77-7.95,1.77-14.47v-3.02h.08v13.36c0,3.98.93,7.04,2.78,9.08,1.84,2.04,4.77,3.29,8.61,3.69l.17.03v2.16ZM442.11,80.91h-6.54c-.42,0-.79.18-1.05.46v-6.66c0-.78-.63-1.42-1.42-1.42h-9.57v-5.24h10.36c.56,0,1.04-.32,1.27-.79v6.2c0,.78.63,1.42,1.42,1.42h5.54v6.03ZM461.85,80.91h-4.46v-6.03h4.46v6.03Z"/>
</g>
<path class="cls-2" d="M608.47,127.84c-4.56-1.17-9.11-2.33-13.67-3.5-.16-20.74-.33-41.47-.49-62.21,0-.79-.63-1.43-1.42-1.43h-34.31v-10.58c0-.79-.63-1.43-1.42-1.43h-14.6c-.78,0-1.42.64-1.42,1.43v10.58h-32.96c-.78,0-1.42.64-1.42,1.43v66c0,.79.63,1.43,1.42,1.43h32.96v6.12c0,3.15.28,5.89.83,8.12.57,2.35,1.57,4.34,2.95,5.92,1.39,1.59,3.28,2.81,5.6,3.61,2.2.76,4.97,1.27,8.25,1.51,1.43.07,3.35.15,5.76.23,2.44.08,4.95.11,7.46.11s5-.02,7.33-.06c2.4-.04,4.24-.14,5.62-.29,3.19-.31,5.93-.72,8.16-1.23,3.05-.7,5.13-2.13,5.99-2.65,1.51-.92,3.64-2.22,5.55-4.66,1.81-2.3,2.48-4.4,3.28-6.89.81-2.52,1.79-5.71,1.06-9.61-.15-.82-.35-1.48-.5-1.94ZM541.15,112.97h-17.16v-9.73h17.16v9.73ZM541.15,87.12h-17.16v-9.84h17.16v9.84ZM558.59,77.28h18.51v9.84h-18.51v-9.84ZM558.59,103.24h18.51v9.73h-18.51v-9.73ZM590.3,133.5c-.06.19-.43,1.31-.83,1.94-1.12,1.76-3.83,1.83-12.61,1.89-7.06.05-.21-.03-7.33-.06-5.89-.02-7.51.05-8.56-1.22-1.02-1.24-1.31-3.54-1.44-4.56-.1-.8-.12-1.48-.11-1.95,10.48.04,20.96.08,31.44.12.02.9-.05,2.27-.56,3.83Z"/>
<path class="cls-2" d="M721.16,93.62h-39.92v-.2c6.24-3.1,12.4-6.63,18.31-10.49,6.14-4.01,12.31-8.35,18.34-12.9.35-.27.56-.69.56-1.13v-14.44c0-.78-.63-1.42-1.42-1.42h-87.02c-.78,0-1.42.63-1.42,1.42v14.32c0,.78.63,1.42,1.42,1.42h54.17c-.81.75-2.1,1.91-3.75,3.22-3.49,2.77-5.95,4.13-9.75,6.58-1.85,1.19-4.54,2.98-7.75,5.33-.03,2.76-.06,5.52-.1,8.29h-41.41c-.78,0-1.41.63-1.41,1.42v15c0,.78.63,1.42,1.41,1.42h41.41v21.77c0,1.22-.04,2.25-.11,3.05-.05.57-.06,1.11-.42,1.34-.35.22-.82.04-1.08-.04-1.08-.36-2.28-.11-3.42-.17-2.27-.12-2.51.11-5,.04-2.39-.07-4.79.16-7.17-.08-.27-.03-.93-.1-1.29.29-.44.48-.17,1.38-.04,1.75,1.43,4.35,2.86,8.69,4.29,13.04.11.5.34,1.22.92,1.83,1.16,1.24,2.98,1.26,4.12,1.25,9.2-.06,11.21-.21,11.21-.21,4.83-.36,5.78-.39,7.58-.96,1.76-.56,3.69-1.19,5.38-2.95,1.68-1.74,2.36-3.6,2.76-5.45.44-2.03.66-4.52.66-7.4v-27.11h39.92c.78,0,1.41-.63,1.41-1.42v-15c0-.78-.63-1.42-1.41-1.42Z"/>
<path class="cls-2" d="M839.47,131.72h-40.52v-59.68h33.89c.78,0,1.42-.63,1.42-1.42v-15.57c0-.78-.63-1.42-1.42-1.42h-86.74c-.78,0-1.42.63-1.42,1.42v15.57c0,.78.63,1.42,1.42,1.42h33.55v59.68h-40.41c-.78,0-1.42.63-1.42,1.42v15.35c0,.78.63,1.42,1.42,1.42h100.22c.78,0,1.42-.63,1.42-1.42v-15.35c0-.78-.63-1.42-1.42-1.42Z"/>
<path class="cls-2" d="M955.36,61.13h-39.83c.46-1.11.9-2.22,1.3-3.32.65-1.74,1.31-3.52,2-5.34.14-.37.12-.79-.06-1.14-.18-.35-.5-.62-.88-.72l-14.29-3.98c-.73-.2-1.49.2-1.73.93-2.48,7.62-5.66,15.09-9.45,22.22-3,5.64-6.11,10.87-9.28,15.62v-12.58c1.19-3.03,2.37-6.23,3.52-9.52,1.16-3.3,2.38-6.9,3.73-10.99.12-.36.09-.76-.09-1.1-.18-.34-.48-.59-.85-.7l-13.84-4.21c-.36-.11-.76-.07-1.09.11-.33.18-.58.49-.68.86-1.13,4.04-2.62,8.43-4.42,13.05-1.81,4.65-3.82,9.34-5.97,13.95-2.16,4.62-4.45,9.15-6.82,13.44-2.37,4.3-4.71,8.14-6.96,11.42-.42.61-.3,1.43.27,1.9l11.21,9.21c.31.26.72.37,1.12.31.4-.06.75-.29.97-.63l1.97-3.03v46.25c0,.78.63,1.42,1.42,1.42h15.09c.78,0,1.42-.63,1.42-1.42v-57.07l8.02,6.03c.62.47,1.5.35,1.98-.27,2.9-3.8,5.62-7.74,8.08-11.71,2.03-3.29,3.95-6.68,5.73-10.12v73.83c0,.78.63,1.42,1.42,1.42h14.98c.78,0,1.42-.63,1.42-1.42v-20.29h27.52c.78,0,1.42-.63,1.42-1.42v-15.12c0-.78-.63-1.42-1.42-1.42h-27.52v-10.01h25.11c.78,0,1.42-.63,1.42-1.42v-14.89c0-.78-.63-1.42-1.42-1.42h-25.11v-9.21h30.6c.78,0,1.42-.63,1.42-1.42v-14.66c0-.78-.63-1.42-1.42-1.42Z"/>
<path class="cls-2" d="M1074.36,137.97h-41.39v-4.55h31.04c.78,0,1.42-.63,1.42-1.42v-13.19c0-.78-.63-1.42-1.42-1.42h-2.25l8.41-9.22c.49-.53.5-1.35.02-1.89-2.83-3.22-6.98-7.17-12.38-11.75l-3.96-3.29h9.69c.78,0,1.42-.63,1.42-1.42v-10.52h8.59c.78,0,1.42-.63,1.42-1.42v-19.1c0-.78-.63-1.42-1.42-1.42h-39.96l-2.28-8.83c-.17-.67-.81-1.12-1.49-1.06l-16.17,1.36c-.42.04-.8.25-1.04.59-.24.34-.32.77-.21,1.18.61,2.31,1.17,4.58,1.68,6.75h-39.86c-.78,0-1.42.63-1.42,1.42v19.1c0,.78.63,1.42,1.42,1.42h8.47v10.52c0,.78.63,1.42,1.42,1.42h11.51c-1.07.86-2.12,1.69-3.13,2.46-1.91,1.47-3.64,2.66-5.15,3.54-1.12.66-2.24,1.28-3.31,1.84-.94.49-1.91.8-2.9.93-.38.05-.71.25-.94.55-.23.3-.33.68-.28,1.06l1.63,11.71c.1.69.68,1.21,1.38,1.22,5.55.08,11.18.06,16.74-.06,5.06-.1,10.15-.22,15.25-.36v3.26h-31.39c-.78,0-1.42.63-1.42,1.42v13.19c0,.78.63,1.42,1.42,1.42h31.39v4.55h-41.86c-.78,0-1.42.63-1.42,1.42v12.62c0,.78.63,1.42,1.42,1.42h101.32c.78,0,1.42-.63,1.42-1.42v-12.62c0-.78-.63-1.42-1.42-1.42ZM1055.75,115.62c.57.62,1.11,1.21,1.64,1.77h-24.42v-3.81c3.26-.13,6.47-.27,9.64-.4,3.37-.14,6.82-.32,10.27-.53,1.04,1.03,2.01,2.03,2.87,2.97ZM990.4,76.13v-3.3h66.73v3.3h-66.73ZM1009.78,99.75c1.14-.79,2.29-1.62,3.43-2.48,2.38-1.78,4.82-3.8,7.26-6.03h15.11l-2.47,2.47c-.29.29-.44.7-.41,1.11s.24.79.58,1.03c.92.68,1.91,1.41,2.95,2.2.21.16.43.33.66.51-5.44.39-10.57.68-15.27.87-4.02.16-7.98.26-11.83.31Z"/>
</g>
<g>
<polygon class="cls-2" points="700.41 193.09 700.29 193.09 695.84 175.52 688.42 175.52 688.42 199.6 692.65 199.6 692.65 179.03 692.76 178.9 698.35 199.6 702.12 199.6 708.05 178.9 708.05 179.03 708.05 199.6 712.28 199.6 712.28 175.52 705.2 175.52 700.41 193.09"/>
<path class="cls-2" d="M727.36,178.51c3.04.09,4.65,1.61,4.83,4.56h5.64c-.36-5.47-3.85-8.24-10.47-8.33-6.62.26-10.07,4.47-10.33,12.63.18,7.98,3.62,12.06,10.33,12.23,6.71-.09,10.2-2.78,10.47-8.07h-5.64c-.09,2.95-1.7,4.47-4.83,4.56-2.95-.17-4.52-3.08-4.7-8.72.18-5.73,1.75-8.68,4.7-8.85Z"/>
<path class="cls-2" d="M756.74,188.14c.08,2.86-.24,4.82-.98,5.86-.73,1.22-2.03,1.82-3.9,1.82s-3.21-.61-4.02-1.82c-.73-1.04-1.06-2.99-.97-5.86v-13.15h-4.87v15.1c.16,5.99,3.45,9.07,9.87,9.24,6.25-.17,9.5-3.25,9.75-9.24v-15.1h-4.87v13.15Z"/>
<polygon class="cls-2" points="782.14 188.79 792.21 188.79 792.21 184.76 782.14 184.76 782.14 179.16 792.98 179.16 792.98 175.13 776.98 175.13 776.98 199.2 793.37 199.2 793.37 195.17 782.14 195.17 782.14 188.79"/>
<rect class="cls-2" x="797.64" y="174.74" width="4.33" height="24.08"/>
<path class="cls-2" d="M822.27,188.31c-.09-1.04-.4-2.04-.94-2.99-1.34-2.6-3.8-3.9-7.38-3.9-5.19.26-7.92,3.21-8.19,8.85-.09,5.81,2.64,8.68,8.19,8.59,4.83,0,7.47-1.73,7.92-5.21h-4.7c-.45,1.48-1.52,2.17-3.22,2.08-2.06.09-3.04-1.3-2.95-4.17h11.41c0-1.13-.05-2.21-.13-3.25ZM810.99,188.18c.09-2.34,1.07-3.51,2.95-3.51,2.06,0,3.09,1.17,3.09,3.51h-6.04Z"/>
<path class="cls-2" d="M831.26,189.46c.28-3.34,1.07-5.01,2.37-5.01,1.67,0,2.6,1.08,2.79,3.25h5.3c-.19-4.24-2.84-6.45-7.96-6.63-4.93.36-7.63,3.43-8.1,9.2.37,5.78,3.07,8.75,8.1,8.93,5.02,0,7.68-2.21,7.96-6.63h-5.3c-.09,2.26-.98,3.38-2.65,3.38-1.49,0-2.33-1.8-2.51-5.41v-1.08Z"/>
<path class="cls-2" d="M927.16,189.69c.28-3.34,1.07-5.01,2.37-5.01,1.67,0,2.6,1.08,2.79,3.25h5.3c-.19-4.24-2.84-6.45-7.96-6.63-4.93.36-7.63,3.43-8.1,9.2.37,5.78,3.07,8.75,8.1,8.93,5.02,0,7.68-2.21,7.96-6.63h-5.3c-.09,2.26-.98,3.38-2.65,3.38-1.49,0-2.33-1.8-2.51-5.41v-1.08Z"/>
<path class="cls-2" d="M854.41,195.73c-1.36.26-1.97-.65-1.8-2.73v-7.81h3.37v-3.38h-3.37v-5.08c-1.56,0-3.12,0-4.69,0,0,1.69,0,3.39,0,5.08h-3.13v3.38h3.13c0,3.23-.02,6.45-.03,9.68.03.49.17,2.15,1.47,3.24.82.7,1.73.84,2.75,1,.82.13,2.18.24,3.88-.17,0-1.11,0-2.23-.01-3.34-.48.09-1,.13-1.56.13Z"/>
<path class="cls-2" d="M999.9,195.61c-1.36.26-1.97-.65-1.8-2.73v-7.81h3.37v-3.38h-3.37v-5.08c-1.56,0-3.12,0-4.69,0,0,1.69,0,3.39,0,5.08h-3.13v3.38h3.13l-.03,9.68c.03.49.17,2.15,1.47,3.24.82.7,1.73.84,2.75,1,.82.13,2.18.24,3.88-.17v-3.34c-.5.09-1.02.13-1.58.13Z"/>
<path class="cls-2" d="M863.85,184.64h-.13v-3.25h-4.7c0,.54.04,1.31.13,2.3v15.17h5.1v-9.21c.18-1.08.4-1.94.67-2.57.45-.54,1.3-.95,2.55-1.22h2.28v-4.6c-2.95-.18-4.92.95-5.91,3.39Z"/>
<path class="cls-2" d="M881.19,181.41c-5.4.26-8.23,3.21-8.49,8.85.25,5.55,3.08,8.42,8.49,8.59,5.4-.17,8.23-3.04,8.49-8.59-.25-5.64-3.08-8.59-8.49-8.85ZM881.19,195.73c-2.28,0-3.42-1.82-3.42-5.47s1.14-5.6,3.42-5.6c2.45,0,3.63,1.87,3.55,5.6,0,3.64-1.18,5.47-3.55,5.47Z"/>
<path class="cls-2" d="M908.77,185.85c-.08-.78-.14-1.32-.38-1.9-.5-1.26-1.5-1.96-1.82-2.16-1.45-.93-2.93-.77-3.33-.71-.79-.01-2.53.08-4.12,1.26-.46.34-.82.71-1.1,1.06,0-.67,0-1.34-.01-2h-5.1v17.48h5.1v-10.43c.27-2.53,1.3-3.88,3.09-4.07,2.06,0,3.09,1.36,3.09,4.07v10.43c1.56,0,3.12,0,4.68.01,0-3.79-.02-7.57-.03-11.36.01-.42,0-.99-.07-1.67Z"/>
<rect class="cls-2" x="913.06" y="174.68" width="4.81" height="4.29"/>
<rect class="cls-2" x="913.18" y="181.97" width="4.58" height="16.79"/>
<path class="cls-2" d="M950.52,188.05c-2.08-.52-3.12-1.13-3.12-1.82,0-1.04.62-1.56,1.87-1.56,1.33.09,2.04.65,2.12,1.69h4.37c-.25-3.3-2.41-4.95-6.49-4.95-4.41.35-6.7,2-6.86,4.95-.17,2.86,1.79,4.69,5.86,5.47,2.08.44,3.16,1.13,3.24,2.08,0,1.22-.75,1.82-2.24,1.82-1.58-.09-2.45-.74-2.62-1.95h-4.49c.17,3.3,2.54,4.99,7.11,5.08,4.57-.35,6.99-2.08,7.24-5.21-.67-3.47-2.66-5.34-5.99-5.6Z"/>
<path class="cls-2" d="M980.42,184.99c-.32-.09-.51-.13-.59-.13-2.84-.43-4.18-1.56-4.02-3.38.08-1.91,1.26-2.95,3.55-3.12,2.29,0,3.47,1.22,3.55,3.64h4.5c-.24-4.69-2.72-7.16-7.45-7.42-5.84.35-8.87,2.99-9.11,7.94,0,3.21,2.4,5.42,7.22,6.64.16.09.43.17.83.26,2.76.61,4.14,1.74,4.14,3.38-.08,2-1.54,3.04-4.38,3.12-2.45-.17-3.67-1.65-3.67-4.43h-4.73c-.08,5.29,2.72,7.94,8.4,7.94,6.15-.17,9.26-2.78,9.34-7.81.08-3.3-2.45-5.51-7.57-6.64Z"/>
<path class="cls-2" d="M1020.78,181.81h-3.98c0,3.43.01,6.87.02,10.3,0,.19-.07,2.13-1.58,3.1-.96.62-2.02.54-2.38.52-.37-.03-1.06-.05-1.65-.47-.81-.57-1.24-1.68-1.29-3.31v-10.15h-4.23c-.01,3.67-.03,7.34-.04,11.01-.01.42.01,1.04.21,1.75.62,2.23,2.61,4.11,4.98,4.62,2.07.45,3.82-.28,4.27-.48.4-.17.72-.36.95-.5,0,.34,0,.68.01,1.02,1.58-.01,3.15-.03,4.73-.04,0-1.2-.01-2.39-.02-3.59v-13.8Z"/>
<path class="cls-2" d="M1042.61,174.52h-4.95v9.24c-1.1-1.47-2.58-2.26-4.44-2.34-4.74.26-7.27,3.21-7.61,8.85.34,5.55,2.71,8.42,7.1,8.59,2.37,0,4.01-.87,4.95-2.6,0,.26.04.65.13,1.17v1.17h4.95c-.09-1.04-.13-2.17-.13-3.38v-20.69ZM1034.24,195.73c-2.45,0-3.64-1.82-3.55-5.47-.09-3.73,1.1-5.6,3.55-5.6,2.11.17,3.25,2.04,3.42,5.6-.17,3.47-1.31,5.29-3.42,5.47Z"/>
<rect class="cls-2" x="1046.8" y="174.52" width="4.96" height="4.29"/>
<rect class="cls-2" x="1046.92" y="181.81" width="4.71" height="16.79"/>
<path class="cls-2" d="M1063.98,181.41c-5.35.26-8.14,3.21-8.39,8.85.25,5.55,3.05,8.42,8.39,8.59,5.34-.17,8.14-3.04,8.39-8.59-.25-5.64-3.05-8.59-8.39-8.85ZM1063.98,195.73c-2.25,0-3.38-1.82-3.38-5.47s1.13-5.6,3.38-5.6c2.42,0,3.59,1.87,3.51,5.6,0,3.64-1.17,5.47-3.51,5.47Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

+146
View File
@@ -0,0 +1,146 @@
# CC Switch v3.12.1
> Stability Fixes, StepFun Presets, OpenClaw authHeader, and New Sponsor Partners
**[中文版 →](v3.12.1-zh.md) | [日本語版 →](v3.12.1-ja.md)**
---
## Overview
CC Switch v3.12.1 is a patch release focused on stability improvements and bug fixes. It resolves a Common Config modal infinite reopen loop, a WebDAV sync foreign key constraint failure, and several i18n interpolation issues. It also adds **StepFun** provider presets, **OpenClaw input type selection** and **authHeader** support, upgrades the default Gemini model to **3.1-pro**, and welcomes four new sponsor partners.
**Release Date**: 2026-03-12
**Update Scale**: 19 commits | 56 files changed | +1,429 / -396 lines
---
## Highlights
- **Common Config modal fix**: Resolved an infinite reopen loop in the Common Config modal and added draft editing support
- **WebDAV sync reliability**: Fixed a foreign key constraint failure when restoring `provider_health` during WebDAV sync
- **StepFun presets**: Added StepFun (阶跃星辰) provider presets including the step-3.5-flash model
- **OpenClaw enhancements**: Added input type selection for model Advanced Options and `authHeader` field for vendor-specific auth header support
- **Gemini model upgrade**: Upgraded default Gemini model to 3.1-pro in provider presets
- **New sponsors**: Welcomed Micu API, XCodeAPI, SiliconFlow, and CTok as sponsor partners
---
## New Features
### StepFun Provider Presets
Added provider presets for StepFun (阶跃星辰), a leading Chinese AI model provider.
- New preset entries for StepFun across supported applications
- Includes the step-3.5-flash model (#1369, thanks @hengm3467)
### OpenClaw Enhancements
Enhanced the OpenClaw configuration with more granular control and better vendor compatibility.
- Added input type selection dropdown for model Advanced Options (#1368, thanks @liuxxxu)
- Added optional `authHeader` boolean to `OpenClawProviderConfig` for vendor-specific auth header support (e.g. Longcat), and refactored form state to reuse the shared type
### Sponsor Partners
- **Micu API**: Added Micu API as sponsor partner with affiliate links
- **XCodeAPI**: Added XCodeAPI as sponsor partner
- **SiliconFlow**: Added SiliconFlow (硅基流动) as sponsor partner with affiliate links
- **CTok**: Added CTok as sponsor partner
---
## Changes
- **UCloud → Compshare**: Renamed UCloud provider to Compshare (优云智算) with full i18n support across all three locales (EN/ZH/JA)
- **Compshare Links**: Updated Compshare sponsor registration links to coding-plan page
- **Gemini Model Upgrade**: Upgraded default Gemini model from 2.5-pro to 3.1-pro in provider presets
---
## Bug Fixes
### Common Config & UI
- Fixed an infinite reopen loop in the Common Config modal and added draft editing support to prevent data loss during edits
- Fixed toolbar compact mode not triggering on Windows due to left-side overflow (#1375, thanks @zuoliangyu)
- Fixed session search index not syncing with query data, causing stale list display after session deletion
### Sync & Data
- Fixed foreign key constraint failure when restoring `provider_health` table during WebDAV sync
### Provider & Preset
- Added missing `authHeader: true` to Longcat provider preset (#1377, thanks @wavever)
- Aligned OpenClaw tool permission profiles with upstream schema (#1355, thanks @bigsongeth)
- Corrected X-Code API URL from `www.x-code.cn` to `x-code.cc`
### i18n & Localization
- Fixed stream check toast i18n interpolation keys not matching translation placeholders
- Fixed proxy startup toast not interpolating address and port values (#1399, thanks @Mason-mengze)
- Renamed OpenCode API format label from "OpenAI" to "OpenAI Responses" for accuracy
---
## Special Thanks
Thanks to all contributors for their contributions to this release!
@hengm3467 @liuxxxu @bigsongeth @zuoliangyu @wavever @Mason-mengze
---
## Download & Installation
Visit [Releases](https://github.com/farion1231/cc-switch/releases/latest) to download the appropriate version.
### System Requirements
| System | Minimum Version | Architecture |
| ------- | ------------------------------- | ----------------------------------- |
| Windows | Windows 10 or later | x64 |
| macOS | macOS 10.15 (Catalina) or later | Intel (x64) / Apple Silicon (arm64) |
| Linux | See table below | x64 |
### Windows
| File | Description |
| ------------------------------------------ | ---------------------------------------------------- |
| `CC-Switch-v3.12.1-Windows.msi` | **Recommended** - MSI installer with auto-update |
| `CC-Switch-v3.12.1-Windows-Portable.zip` | Portable version, extract and run, no registry write |
### macOS
| File | Description |
| ---------------------------------- | -------------------------------------------------------------------- |
| `CC-Switch-v3.12.1-macOS.zip` | **Recommended** - Extract and drag to Applications, Universal Binary |
| `CC-Switch-v3.12.1-macOS.tar.gz` | For Homebrew installation and auto-update |
> **Note**: Since the author doesn't have an Apple Developer account, you may see an "unidentified developer" warning on first launch. Please close it, then go to "System Settings" -> "Privacy & Security" -> click "Open Anyway", and it will open normally afterwards.
### Homebrew (macOS)
```bash
brew tap farion1231/ccswitch
brew install --cask cc-switch
```
Update:
```bash
brew upgrade --cask cc-switch
```
### Linux
| Distribution | Recommended Format | Installation Method |
| --------------------------------------- | ------------------ | ---------------------------------------------------------------------- |
| Ubuntu / Debian / Linux Mint / Pop!\_OS | `.deb` | `sudo dpkg -i CC-Switch-*.deb` or `sudo apt install ./CC-Switch-*.deb` |
| Fedora / RHEL / CentOS / Rocky Linux | `.rpm` | `sudo rpm -i CC-Switch-*.rpm` or `sudo dnf install ./CC-Switch-*.rpm` |
| openSUSE | `.rpm` | `sudo zypper install ./CC-Switch-*.rpm` |
| Arch Linux / Manjaro | `.AppImage` | Add execute permission and run directly, or use AUR |
| Other distributions / Unsure | `.AppImage` | `chmod +x CC-Switch-*.AppImage && ./CC-Switch-*.AppImage` |
+146
View File
@@ -0,0 +1,146 @@
# CC Switch v3.12.1
> 安定性修正、StepFun プリセット、OpenClaw authHeader 対応、新スポンサーパートナー
**[中文版 →](v3.12.1-zh.md) | [English →](v3.12.1-en.md)**
---
## 概要
CC Switch v3.12.1 は、安定性の改善とバグ修正に焦点を当てたパッチリリースです。共通設定モーダルの無限再オープンループ、WebDAV 同期時の外部キー制約エラー、複数の i18n 補間問題を修正しました。また、**StepFun(阶跃星辰)** プロバイダープリセットの追加、OpenClaw の**入力タイプ選択**と **authHeader** サポート、デフォルト Gemini モデルの **3.1-pro** へのアップグレード、4 つの新スポンサーパートナーの追加が含まれます。
**リリース日**: 2026-03-12
**更新規模**: 19 commits | 56 files changed | +1,429 / -396 lines
---
## ハイライト
- **共通設定モーダル修正**: 共通設定モーダルの無限再オープンループを解決し、下書き編集サポートを追加
- **WebDAV 同期の信頼性向上**: WebDAV 同期で `provider_health` 復元時の外部キー制約エラーを修正
- **StepFun プリセット**: StepFun(阶跃星辰)プロバイダープリセットを追加、step-3.5-flash モデルを含む
- **OpenClaw 強化**: モデル詳細設定に入力タイプ選択を追加、ベンダー固有の認証ヘッダーサポート用 `authHeader` フィールドを追加
- **Gemini モデルアップグレード**: プロバイダープリセットのデフォルト Gemini モデルを 3.1-pro にアップグレード
- **新スポンサー**: Micu API、XCodeAPI、SiliconFlow、CTok をスポンサーパートナーとして追加
---
## 新機能
### StepFun プロバイダープリセット
中国の主要 AI モデルプロバイダーである StepFun(阶跃星辰)のプロバイダープリセットを追加しました。
- サポート対象アプリケーション全体に StepFun プリセットエントリーを追加
- step-3.5-flash モデルを含む(#1369@hengm3467 に感謝)
### OpenClaw 強化
OpenClaw 設定をより細かく制御でき、ベンダー互換性を向上させました。
- モデル詳細設定に入力タイプ(input type)選択ドロップダウンを追加(#1368@liuxxxu に感謝)
- `OpenClawProviderConfig` にオプションの `authHeader` ブール値を追加し、ベンダー固有の認証ヘッダー(例: Longcat)をサポート。フォーム状態を共有型の再利用にリファクタリング
### スポンサーパートナー
- **Micu API**: Micu API をスポンサーパートナーとして追加、アフィリエイトリンク付き
- **XCodeAPI**: XCodeAPI をスポンサーパートナーとして追加
- **SiliconFlow**: SiliconFlow(硅基流动)をスポンサーパートナーとして追加、アフィリエイトリンク付き
- **CTok**: CTok をスポンサーパートナーとして追加
---
## 変更
- **UCloud → Compshare**: UCloud プロバイダーを Compshare(优云智算)にリネームし、3 言語(EN/ZH/JA)の完全な i18n サポートを追加
- **Compshare リンク**: Compshare スポンサー登録リンクを coding-plan ページに更新
- **Gemini モデルアップグレード**: プロバイダープリセットのデフォルト Gemini モデルを 2.5-pro から 3.1-pro にアップグレード
---
## バグ修正
### 共通設定と UI
- 共通設定モーダルの無限再オープンループを修正し、編集中のデータ損失を防ぐための下書き編集サポートを追加
- Windows でツールバーコンパクトモードが左側のオーバーフローにより機能しない問題を修正(#1375@zuoliangyu に感謝)
- セッション削除後にクエリデータと検索インデックスが同期されず、リストが更新されない問題を修正
### 同期とデータ
- WebDAV 同期で `provider_health` テーブルを復元する際の外部キー制約エラーを修正
### プロバイダーとプリセット
- Longcat プロバイダープリセットに欠落していた `authHeader: true` を追加(#1377@wavever に感謝)
- OpenClaw のツール権限プロファイルをアップストリームスキーマに合わせて修正(#1355@bigsongeth に感謝)
- X-Code API の URL を `www.x-code.cn` から `x-code.cc` に修正
### i18n とローカリゼーション
- Stream Check トーストの i18n 補間キーが翻訳プレースホルダーと一致しない問題を修正
- プロキシ起動トーストでアドレスとポート値が補間されない問題を修正(#1399@Mason-mengze に感謝)
- OpenCode の API フォーマットラベルを「OpenAI」から「OpenAI Responses」にリネームし、正確性を向上
---
## 謝辞
以下のコントリビューターの皆様、このリリースへの貢献に感謝します!
@hengm3467 @liuxxxu @bigsongeth @zuoliangyu @wavever @Mason-mengze
---
## ダウンロードとインストール
[Releases](https://github.com/farion1231/cc-switch/releases/latest) から適切なバージョンをダウンロードしてください。
### システム要件
| システム | 最小バージョン | アーキテクチャ |
| -------- | -------------------------------- | ----------------------------------- |
| Windows | Windows 10 以降 | x64 |
| macOS | macOS 10.15 (Catalina) 以降 | Intel (x64) / Apple Silicon (arm64) |
| Linux | 下表参照 | x64 |
### Windows
| ファイル | 説明 |
| ------------------------------------------ | ---------------------------------------------------- |
| `CC-Switch-v3.12.1-Windows.msi` | **推奨** - MSI インストーラー、自動更新対応 |
| `CC-Switch-v3.12.1-Windows-Portable.zip` | ポータブル版、解凍して実行、レジストリ書き込みなし |
### macOS
| ファイル | 説明 |
| ---------------------------------- | ----------------------------------------------------------------- |
| `CC-Switch-v3.12.1-macOS.zip` | **推奨** - 解凍して Applications にドラッグ、Universal Binary |
| `CC-Switch-v3.12.1-macOS.tar.gz` | Homebrew インストールと自動更新用 |
> **注意**: 作者が Apple Developer アカウントを持っていないため、初回起動時に「開発元を確認できません」という警告が表示される場合があります。一度閉じてから、「システム設定」→「プライバシーとセキュリティ」→「このまま開く」をクリックすると、その後は正常に開けます。
### Homebrew (macOS)
```bash
brew tap farion1231/ccswitch
brew install --cask cc-switch
```
更新:
```bash
brew upgrade --cask cc-switch
```
### Linux
| ディストリビューション | 推奨形式 | インストール方法 |
| --------------------------------------- | ----------- | ---------------------------------------------------------------------- |
| Ubuntu / Debian / Linux Mint / Pop!\_OS | `.deb` | `sudo dpkg -i CC-Switch-*.deb` または `sudo apt install ./CC-Switch-*.deb` |
| Fedora / RHEL / CentOS / Rocky Linux | `.rpm` | `sudo rpm -i CC-Switch-*.rpm` または `sudo dnf install ./CC-Switch-*.rpm` |
| openSUSE | `.rpm` | `sudo zypper install ./CC-Switch-*.rpm` |
| Arch Linux / Manjaro | `.AppImage` | 実行権限を追加して直接実行、または AUR を使用 |
| その他のディストリビューション / 不明 | `.AppImage` | `chmod +x CC-Switch-*.AppImage && ./CC-Switch-*.AppImage` |
+146
View File
@@ -0,0 +1,146 @@
# CC Switch v3.12.1
> 稳定性修复、StepFun 预设、OpenClaw authHeader 支持,以及新赞助商伙伴
**[English →](v3.12.1-en.md) | [日本語版 →](v3.12.1-ja.md)**
---
## 概览
CC Switch v3.12.1 是一个以稳定性改进和 Bug 修复为主的补丁版本。修复了通用配置弹窗无限重复打开的循环问题、WebDAV 同步时的外键约束失败以及多个 i18n 插值问题。同时新增了 **StepFun(阶跃星辰)** 供应商预设、OpenClaw **输入类型选择****authHeader** 支持,将默认 Gemini 模型升级到 **3.1-pro**,并欢迎四位新赞助商伙伴加入。
**发布日期**2026-03-12
**更新规模**19 commits | 56 files changed | +1,429 / -396 lines
---
## 重点内容
- **通用配置弹窗修复**:解决了通用配置弹窗无限重复打开的循环问题,并新增草稿编辑支持
- **WebDAV 同步可靠性**:修复了 WebDAV 同步恢复 `provider_health` 时的外键约束失败
- **StepFun 预设**:新增 StepFun(阶跃星辰)供应商预设,包含 step-3.5-flash 模型
- **OpenClaw 增强**:新增模型高级选项的输入类型选择和 `authHeader` 字段,支持供应商特定的认证头
- **Gemini 模型升级**:供应商预设中的默认 Gemini 模型升级到 3.1-pro
- **新赞助商**:欢迎 Micu API、XCodeAPI、SiliconFlow、CTok 加入赞助伙伴
---
## 新功能
### StepFun 供应商预设
新增 StepFun(阶跃星辰)供应商预设,阶跃星辰是领先的中国 AI 模型提供商。
- 在各支持应用中新增 StepFun 预设条目
- 包含 step-3.5-flash 模型(#1369,感谢 @hengm3467
### OpenClaw 增强
增强 OpenClaw 配置能力,提供更细粒度的控制和更好的供应商兼容性。
- 新增模型高级选项的输入类型(input type)选择下拉框(#1368,感谢 @liuxxxu
-`OpenClawProviderConfig` 中新增可选的 `authHeader` 布尔字段,支持供应商特定的认证头(如 Longcat),并重构表单状态以复用共享类型
### 赞助商伙伴
- **Micu API**:新增 Micu API 赞助商及推广链接
- **XCodeAPI**:新增 XCodeAPI 赞助商
- **SiliconFlow**:新增 SiliconFlow(硅基流动)赞助商及推广链接
- **CTok**:新增 CTok 赞助商
---
## 变更
- **UCloud → Compshare**:将 UCloud 供应商更名为 Compshare(优云智算),支持三种语言(中/英/日)的完整国际化
- **Compshare 链接**:更新 Compshare 赞助商注册链接指向 coding-plan 页面
- **Gemini 模型升级**:供应商预设中的默认 Gemini 模型从 2.5-pro 升级到 3.1-pro
---
## Bug 修复
### 通用配置与 UI
- 修复通用配置弹窗无限重复打开的循环问题,并新增草稿编辑支持以防止编辑过程中数据丢失
- 修复 Windows 下因左侧溢出导致工具栏紧凑模式不触发的问题(#1375,感谢 @zuoliangyu
- 修复会话删除后搜索索引未与查询数据同步,导致列表显示过期的问题
### 同步与数据
- 修复 WebDAV 同步恢复 `provider_health` 表时的外键约束失败
### 供应商与预设
- 为 Longcat 供应商预设补充缺失的 `authHeader: true`#1377,感谢 @wavever
- 对齐 OpenClaw 工具权限配置与上游 schema(#1355,感谢 @bigsongeth
- 修正 X-Code API URL,从 `www.x-code.cn` 改为 `x-code.cc`
### i18n 与本地化
- 修复 Stream Check Toast 的 i18n 插值 key 与翻译占位符不匹配
- 修复代理启动 Toast 未正确插值地址和端口的问题(#1399,感谢 @Mason-mengze
- 将 OpenCode API 格式标签从 "OpenAI" 改为 "OpenAI Responses",更准确地反映实际格式
---
## 特别感谢
感谢以下贡献者为本版本做出的贡献!
@hengm3467 @liuxxxu @bigsongeth @zuoliangyu @wavever @Mason-mengze
---
## 下载与安装
访问 [Releases](https://github.com/farion1231/cc-switch/releases/latest) 下载对应版本。
### 系统要求
| 系统 | 最低版本 | 架构 |
| ------- | ----------------------------- | ----------------------------------- |
| Windows | Windows 10 及以上 | x64 |
| macOS | macOS 10.15 (Catalina) 及以上 | Intel (x64) / Apple Silicon (arm64) |
| Linux | 见下表 | x64 |
### Windows
| 文件 | 说明 |
| ------------------------------------------ | ----------------------------------- |
| `CC-Switch-v3.12.1-Windows.msi` | **推荐** - MSI 安装包,支持自动更新 |
| `CC-Switch-v3.12.1-Windows-Portable.zip` | 便携版,解压即用,不写入注册表 |
### macOS
| 文件 | 说明 |
| ---------------------------------- | --------------------------------------------------------- |
| `CC-Switch-v3.12.1-macOS.zip` | **推荐** - 解压后拖入 Applications 即可,Universal Binary |
| `CC-Switch-v3.12.1-macOS.tar.gz` | 用于 Homebrew 安装和自动更新 |
> **注意**:由于作者没有苹果开发者账号,首次打开可能出现"未知开发者"警告,请先关闭,然后前往"系统设置" → "隐私与安全性" → 点击"仍要打开",之后便可以正常打开
### HomebrewmacOS
```bash
brew tap farion1231/ccswitch
brew install --cask cc-switch
```
更新:
```bash
brew upgrade --cask cc-switch
```
### Linux
| 发行版 | 推荐格式 | 安装方式 |
| --------------------------------------- | ----------- | ---------------------------------------------------------------------- |
| Ubuntu / Debian / Linux Mint / Pop!\_OS | `.deb` | `sudo dpkg -i CC-Switch-*.deb``sudo apt install ./CC-Switch-*.deb` |
| Fedora / RHEL / CentOS / Rocky Linux | `.rpm` | `sudo rpm -i CC-Switch-*.rpm``sudo dnf install ./CC-Switch-*.rpm` |
| openSUSE | `.rpm` | `sudo zypper install ./CC-Switch-*.rpm` |
| Arch Linux / Manjaro | `.AppImage` | 添加执行权限后直接运行,或使用 AUR |
| 其他发行版 / 不确定 | `.AppImage` | `chmod +x CC-Switch-*.AppImage && ./CC-Switch-*.AppImage` |
+138
View File
@@ -0,0 +1,138 @@
# CC Switch v3.12.2
> Common Config Protection During Proxy Takeover, Snippet Lifecycle Stability, Section-Aware Codex TOML Editing
**[中文版 →](v3.12.2-zh.md) | [日本語版 →](v3.12.2-ja.md)**
---
## Overview
CC Switch v3.12.2 is a reliability-focused patch release that addresses Common Config loss during proxy takeover and improves Codex TOML editing accuracy. Proxy takeover hot-switches and provider sync now update the restore backup instead of overwriting live config files; the startup sequence has been reordered so snippets are extracted from clean live files before takeover state is restored; and Codex `base_url` editing has been refactored into a section-aware model that no longer appends to the end of the file.
**Release Date**: 2026-03-12
**Update Scale**: 5 commits | 22 files changed | +1,716 / -288 lines
---
## Highlights
- **Empty state guidance**: Provider list empty state now shows detailed import instructions with a conditional Common Config snippet hint for Claude/Codex/Gemini
- **Proxy takeover restore flow rework**: Hot-switches and provider sync now refresh the restore backup instead of overwriting live config files, preserving the full user configuration on rollback
- **Snippet lifecycle stability**: Introduced a `cleared` flag to prevent auto-extraction from resurrecting cleared snippets, and reordered startup to extract from clean state
- **Section-aware Codex TOML editing**: `base_url` and `model` field reads/writes now target the correct `[model_providers.<name>]` section
- **Codex MCP config protection**: Existing `mcp_servers` blocks in restore snapshots survive provider hot-switches via per-server-id merge instead of wholesale replacement, with provider/common-config definitions winning on conflict
---
## New Features
### Empty State Guidance
Improved the first-run experience with helpful guidance when the provider list is empty.
- Empty state page shows step-by-step import instructions
- Conditionally displays a Common Config snippet hint for Claude/Codex/Gemini providers (not shown for OpenCode/OpenClaw)
---
## Changes
### Proxy Takeover Restore Flow
The proxy takeover hot-switch and provider sync logic has been reworked to protect Common Config throughout the takeover lifecycle.
- Provider sync now updates the restore backup instead of writing directly to live config files when takeover is active
- Effective provider settings are rebuilt with Common Config applied before saving restore snapshots, so rollback restores the real user configuration
- Legacy providers with inferred common config usage are automatically marked with `commonConfigEnabled=true`
### Codex TOML Editing Engine
Codex `config.toml` update logic has been refactored onto shared section-aware TOML helpers.
- New Rust module `codex_config.rs` with `update_codex_toml_field` and `remove_codex_toml_base_url_if`
- New frontend utilities `getTomlSectionRange` / `getCodexProviderSectionName` for section-aware operations
- Inline TOML editing logic scattered across `proxy.rs` now delegates to the new module
### Common Config Initialization Lifecycle
The startup sequence has been reordered for more robust snippet extraction and migration.
- Startup now auto-extracts Common Config snippets from clean live files before restoring proxy takeover state
- Introduced a snippet `cleared` flag to track whether a user intentionally cleared a snippet
- Persisted a one-time legacy migration flag to avoid repeated `commonConfigEnabled` backfills
---
## Bug Fixes
### Common Config Loss
- Fixed multiple scenarios where Common Config could be dropped during proxy takeover: sync overwriting live files, hot-switches producing incomplete restore snapshots, and provider switches losing config changes
### Codex Restore Snapshot Preservation
- Fixed Codex takeover restore backups discarding existing `mcp_servers` blocks during provider hot-switches; changed MCP backup preservation from wholesale table replacement to per-server-id merge so provider/common-config MCP updates win on conflict while backup-only servers are retained
### Cleared Snippet Resurrection
- Fixed startup auto-extraction recreating Common Config snippets that users had intentionally cleared
### Codex `base_url` Misplacement
- Fixed Codex `base_url` extraction and editing not targeting the correct `[model_providers.<name>]` section, causing it to append to the file tail or confuse `mcp_servers.*.base_url` entries for provider endpoints
---
## Download & Installation
Visit [Releases](https://github.com/farion1231/cc-switch/releases/latest) to download the appropriate version.
### System Requirements
| System | Minimum Version | Architecture |
| ------- | ------------------------------- | ----------------------------------- |
| Windows | Windows 10 or later | x64 |
| macOS | macOS 10.15 (Catalina) or later | Intel (x64) / Apple Silicon (arm64) |
| Linux | See table below | x64 |
### Windows
| File | Description |
| ------------------------------------------ | ---------------------------------------------------- |
| `CC-Switch-v3.12.2-Windows.msi` | **Recommended** - MSI installer with auto-update |
| `CC-Switch-v3.12.2-Windows-Portable.zip` | Portable version, extract and run, no registry write |
### macOS
| File | Description |
| ---------------------------------- | -------------------------------------------------------------------- |
| `CC-Switch-v3.12.2-macOS.zip` | **Recommended** - Extract and drag to Applications, Universal Binary |
| `CC-Switch-v3.12.2-macOS.tar.gz` | For Homebrew installation and auto-update |
> **Note**: Since the author doesn't have an Apple Developer account, you may see an "unidentified developer" warning on first launch. Please close it, then go to "System Settings" -> "Privacy & Security" -> click "Open Anyway", and it will open normally afterwards.
### Homebrew (macOS)
```bash
brew tap farion1231/ccswitch
brew install --cask cc-switch
```
Update:
```bash
brew upgrade --cask cc-switch
```
### Linux
| Distribution | Recommended Format | Installation Method |
| --------------------------------------- | ------------------ | ---------------------------------------------------------------------- |
| Ubuntu / Debian / Linux Mint / Pop!\_OS | `.deb` | `sudo dpkg -i CC-Switch-*.deb` or `sudo apt install ./CC-Switch-*.deb` |
| Fedora / RHEL / CentOS / Rocky Linux | `.rpm` | `sudo rpm -i CC-Switch-*.rpm` or `sudo dnf install ./CC-Switch-*.rpm` |
| openSUSE | `.rpm` | `sudo zypper install ./CC-Switch-*.rpm` |
| Arch Linux / Manjaro | `.AppImage` | Add execute permission and run directly, or use AUR |
| Other distributions / Unsure | `.AppImage` | `chmod +x CC-Switch-*.AppImage && ./CC-Switch-*.AppImage` |
+138
View File
@@ -0,0 +1,138 @@
# CC Switch v3.12.2
> プロキシテイクオーバー中の共通設定保護、Snippet ライフサイクルの安定化、Codex TOML セクション対応編集
**[中文版 →](v3.12.2-zh.md) | [English →](v3.12.2-en.md)**
---
## 概要
CC Switch v3.12.2 は、信頼性を重視したパッチリリースです。プロキシテイクオーバーモードでの共通設定(Common Config)の消失問題を解決し、Codex TOML 設定の編集精度を改善しました。テイクオーバーのホットスイッチとプロバイダー同期は、ライブ設定ファイルを上書きする代わりにリストアバックアップを更新するようになりました。起動シーケンスを再整理し、テイクオーバー状態を復元する前にクリーンなライブファイルから Snippet を抽出するようにしました。また Codex の `base_url` 編集をセクション対応モデルにリファクタリングし、ファイル末尾への誤追加を防止しました。
**リリース日**: 2026-03-12
**更新規模**: 5 commits | 22 files changed | +1,716 / -288 lines
---
## ハイライト
- **空状態ガイダンスの改善**: プロバイダーリストが空の場合に詳細なインポート手順を表示し、Claude/Codex/Gemini には共通設定 Snippet のヒントを条件付きで表示
- **プロキシテイクオーバーリストアフロー刷新**: ホットスイッチとプロバイダー同期がライブ設定ファイルの上書きではなくリストアバックアップの更新を行うようになり、ロールバック時に完全なユーザー設定を保持
- **Snippet ライフサイクルの安定化**: `cleared` フラグを導入し、クリア済み Snippet の自動再抽出を防止。起動順序を調整してクリーンな状態から抽出
- **Codex TOML セクション対応編集**: `base_url``model` フィールドの読み書きが正しい `[model_providers.<name>]` セクションを対象にするように改善
- **Codex MCP 設定の保護**: プロバイダーホットスイッチ時にリストアスナップショット内の既存 `mcp_servers` ブロックが保持されるように修正。テーブル全体の置換からサーバー ID ごとのマージに変更し、プロバイダー/共通設定の MCP 定義が競合時に優先
---
## 新機能
### 空状態ガイダンスの改善
プロバイダーリストが空の場合の初回利用体験を改善しました。
- 空状態ページにプロバイダーインポートの操作ガイドを表示
- Claude/Codex/Gemini アプリケーションに共通設定 Snippet のヒントを条件付きで表示(OpenCode/OpenClaw には非表示)
---
## 変更
### プロキシテイクオーバーリストアフロー
テイクオーバーのホットスイッチとプロバイダー同期ロジックをリファクタリングし、テイクオーバーライフサイクル全体で共通設定を保護します。
- テイクオーバーがアクティブな場合、プロバイダー同期がライブ設定ファイルへの直接書き込みではなくリストアバックアップを更新
- リストアスナップショットの保存前に共通設定を適用した実効プロバイダー設定を再構築し、ロールバックで実際のユーザー設定を復元
- 共通設定の使用が推測されるレガシープロバイダーに `commonConfigEnabled=true` を自動マーク
### Codex TOML 編集エンジン
Codex `config.toml` の更新ロジックを共有のセクション対応 TOML ヘルパーにリファクタリングしました。
- Rust 側に新モジュール `codex_config.rs` を追加(`update_codex_toml_field``remove_codex_toml_base_url_if`
- フロントエンドにセクション対応ユーティリティ `getTomlSectionRange` / `getCodexProviderSectionName` を追加
- `proxy.rs` に散在していたインライン TOML 編集ロジックを新モジュールに委譲
### 共通設定初期化ライフサイクル
Snippet の抽出とマイグレーションをより堅牢にするため、起動シーケンスを再整理しました。
- 起動時にプロキシテイクオーバー状態を復元する前に、クリーンなライブファイルから共通設定 Snippet を自動抽出
- Snippet の `cleared` フラグを導入し、ユーザーが意図的にクリアしたかどうかを追跡
- 一回限りのレガシーマイグレーションフラグを永続化し、`commonConfigEnabled` のバックフィルの繰り返しを防止
---
## バグ修正
### 共通設定の消失
- プロキシテイクオーバー中に共通設定が消失する複数のシナリオを修正:同期によるライブファイルの上書き、ホットスイッチによる不完全なリストアスナップショット、プロバイダー切り替え時の設定変更の消失
### Codex リストアスナップショットの保護
- プロバイダーホットスイッチ時に Codex テイクオーバーリストアバックアップが既存の `mcp_servers` ブロックを破棄する問題を修正。MCP バックアップ保持をテーブル全体の置換からサーバー ID ごとのマージに変更し、プロバイダー/共通設定の MCP 更新が競合時に優先され、バックアップのみのサーバーも保持
### クリア済み Snippet の復活
- 起動時の自動抽出が、ユーザーが意図的にクリアした共通設定 Snippet を再作成する問題を修正
### Codex `base_url` の配置エラー
- Codex `base_url` の抽出と編集が正しい `[model_providers.<name>]` セクションを対象にせず、ファイル末尾に追加されたり `mcp_servers.*.base_url` をプロバイダーエンドポイントと誤認する問題を修正
---
## ダウンロードとインストール
[Releases](https://github.com/farion1231/cc-switch/releases/latest) から適切なバージョンをダウンロードしてください。
### システム要件
| システム | 最小バージョン | アーキテクチャ |
| -------- | -------------------------------- | ----------------------------------- |
| Windows | Windows 10 以降 | x64 |
| macOS | macOS 10.15 (Catalina) 以降 | Intel (x64) / Apple Silicon (arm64) |
| Linux | 下表参照 | x64 |
### Windows
| ファイル | 説明 |
| ------------------------------------------ | ---------------------------------------------------- |
| `CC-Switch-v3.12.2-Windows.msi` | **推奨** - MSI インストーラー、自動更新対応 |
| `CC-Switch-v3.12.2-Windows-Portable.zip` | ポータブル版、解凍して実行、レジストリ書き込みなし |
### macOS
| ファイル | 説明 |
| ---------------------------------- | ----------------------------------------------------------------- |
| `CC-Switch-v3.12.2-macOS.zip` | **推奨** - 解凍して Applications にドラッグ、Universal Binary |
| `CC-Switch-v3.12.2-macOS.tar.gz` | Homebrew インストールと自動更新用 |
> **注意**: 作者が Apple Developer アカウントを持っていないため、初回起動時に「開発元を確認できません」という警告が表示される場合があります。一度閉じてから、「システム設定」→「プライバシーとセキュリティ」→「このまま開く」をクリックすると、その後は正常に開けます。
### Homebrew (macOS)
```bash
brew tap farion1231/ccswitch
brew install --cask cc-switch
```
更新:
```bash
brew upgrade --cask cc-switch
```
### Linux
| ディストリビューション | 推奨形式 | インストール方法 |
| --------------------------------------- | ----------- | ---------------------------------------------------------------------- |
| Ubuntu / Debian / Linux Mint / Pop!\_OS | `.deb` | `sudo dpkg -i CC-Switch-*.deb` または `sudo apt install ./CC-Switch-*.deb` |
| Fedora / RHEL / CentOS / Rocky Linux | `.rpm` | `sudo rpm -i CC-Switch-*.rpm` または `sudo dnf install ./CC-Switch-*.rpm` |
| openSUSE | `.rpm` | `sudo zypper install ./CC-Switch-*.rpm` |
| Arch Linux / Manjaro | `.AppImage` | 実行権限を追加して直接実行、または AUR を使用 |
| その他のディストリビューション / 不明 | `.AppImage` | `chmod +x CC-Switch-*.AppImage && ./CC-Switch-*.AppImage` |
+138
View File
@@ -0,0 +1,138 @@
# CC Switch v3.12.2
> 代理接管期间通用配置保护、Snippet 生命周期稳定性、Codex TOML Section 感知编辑
**[English →](v3.12.2-en.md) | [日本語版 →](v3.12.2-ja.md)**
---
## 概览
CC Switch v3.12.2 是一个以可靠性为核心的补丁版本,重点解决代理(Proxy)接管模式下通用配置(Common Config)丢失的问题,并改进了 Codex TOML 配置的编辑准确性。代理接管的热切换和供应商同步现在会更新恢复备份而非直接覆盖 live 文件;启动流程重新排序,确保先从干净的 live 文件提取 Snippet 再恢复接管状态;Codex 的 `base_url` 编辑重构为 Section 感知模式,不再错误追加到文件末尾。
**发布日期**2026-03-12
**更新规模**5 commits | 22 files changed | +1,716 / -288 lines
---
## 重点内容
- **首次使用引导优化**:供应商列表空状态显示详细的导入说明,Claude/Codex/Gemini 还会提示通用配置 Snippet 功能
- **代理接管恢复流程重构**:热切换和供应商同步现在刷新恢复备份,而非覆盖 live 配置文件,回滚时保留完整的用户配置
- **Snippet 生命周期稳定**:引入 `cleared` 标志防止已清除的 Snippet 被自动重新提取,启动顺序调整确保从干净状态提取
- **Codex TOML Section 感知编辑**`base_url``model` 字段的读写现在定位到正确的 `[model_providers.<name>]` Section
- **Codex MCP 配置保护**:热切换供应商时保留恢复快照中已有的 `mcp_servers` 配置块,按 server id 合并而非整表替换,供应商/通用配置的 MCP 定义优先
---
## 新功能
### 空状态引导优化
改善首次使用体验,当供应商列表为空时显示详细的导入说明。
- 空状态页面展示导入供应商的操作指引
- 对 Claude/Codex/Gemini 应用有条件地显示通用配置 Snippet 提示(OpenCode/OpenClaw 不显示)
---
## 变更
### 代理接管恢复流程
代理接管的热切换和供应商同步逻辑经过重构,确保通用配置在整个接管生命周期中得到保护。
- 接管活跃时,供应商同步更新恢复备份而非直接写入 live 配置文件
- 保存恢复快照前先应用通用配置,使回滚能还原真实的用户配置
- 遗留供应商中推断使用了通用配置的条目自动标记 `commonConfigEnabled=true`
### Codex TOML 编辑引擎
将 Codex `config.toml` 的更新逻辑重构到共享的 Section 感知 TOML 辅助函数上。
- Rust 端新增 `codex_config.rs` 模块,包含 `update_codex_toml_field``remove_codex_toml_base_url_if`
- 前端新增 `getTomlSectionRange` / `getCodexProviderSectionName` 等 Section 感知工具函数
- `proxy.rs` 中散落的 TOML 内联编辑逻辑统一委托给新模块
### 通用配置初始化生命周期
启动流程重新排序,通用配置 Snippet 的提取和迁移逻辑更加健壮。
- 启动时先从干净的 live 文件自动提取通用配置 Snippet,再恢复代理接管状态
- 引入 Snippet `cleared` 标志,追踪用户是否主动清除了某个 Snippet
- 持久化一次性遗留迁移标志,避免重复执行旧版 `commonConfigEnabled` 回填
---
## Bug 修复
### 通用配置丢失
- 修复代理接管期间通用配置可能被丢弃的多种场景:同步覆盖 live 文件、热切换产生不完整的恢复快照、供应商切换丢失配置变更
### Codex 恢复快照保护
- 修复 Codex 接管恢复备份在供应商热切换时丢弃已有 `mcp_servers` 配置块的问题;将 MCP 备份保留策略从整表替换改为按 server id 合并,供应商/通用配置的 MCP 定义在冲突时优先,备份中独有的服务器仍被保留
### 已清除 Snippet 复活
- 修复启动时自动提取机制重新创建用户已主动清除的通用配置 Snippet 的问题
### Codex `base_url` 位置错误
- 修复 Codex `base_url` 提取和编辑未定位到正确的 `[model_providers.<name>]` Section,导致追加到文件末尾或误将 `mcp_servers.*.base_url` 识别为供应商端点的问题
---
## 下载与安装
访问 [Releases](https://github.com/farion1231/cc-switch/releases/latest) 下载对应版本。
### 系统要求
| 系统 | 最低版本 | 架构 |
| ------- | ----------------------------- | ----------------------------------- |
| Windows | Windows 10 及以上 | x64 |
| macOS | macOS 10.15 (Catalina) 及以上 | Intel (x64) / Apple Silicon (arm64) |
| Linux | 见下表 | x64 |
### Windows
| 文件 | 说明 |
| ------------------------------------------ | ----------------------------------- |
| `CC-Switch-v3.12.2-Windows.msi` | **推荐** - MSI 安装包,支持自动更新 |
| `CC-Switch-v3.12.2-Windows-Portable.zip` | 便携版,解压即用,不写入注册表 |
### macOS
| 文件 | 说明 |
| ---------------------------------- | --------------------------------------------------------- |
| `CC-Switch-v3.12.2-macOS.zip` | **推荐** - 解压后拖入 Applications 即可,Universal Binary |
| `CC-Switch-v3.12.2-macOS.tar.gz` | 用于 Homebrew 安装和自动更新 |
> **注意**:由于作者没有苹果开发者账号,首次打开可能出现"未知开发者"警告,请先关闭,然后前往"系统设置" → "隐私与安全性" → 点击"仍要打开",之后便可以正常打开
### HomebrewmacOS
```bash
brew tap farion1231/ccswitch
brew install --cask cc-switch
```
更新:
```bash
brew upgrade --cask cc-switch
```
### Linux
| 发行版 | 推荐格式 | 安装方式 |
| --------------------------------------- | ----------- | ---------------------------------------------------------------------- |
| Ubuntu / Debian / Linux Mint / Pop!\_OS | `.deb` | `sudo dpkg -i CC-Switch-*.deb``sudo apt install ./CC-Switch-*.deb` |
| Fedora / RHEL / CentOS / Rocky Linux | `.rpm` | `sudo rpm -i CC-Switch-*.rpm``sudo dnf install ./CC-Switch-*.rpm` |
| openSUSE | `.rpm` | `sudo zypper install ./CC-Switch-*.rpm` |
| Arch Linux / Manjaro | `.AppImage` | 添加执行权限后直接运行,或使用 AUR |
| 其他发行版 / 不确定 | `.AppImage` | `chmod +x CC-Switch-*.AppImage && ./CC-Switch-*.AppImage` |
@@ -33,6 +33,7 @@ Presets are pre-configured provider templates that only require an API Key to us
| Bailian | Alibaba Cloud Bailian (Qwen) |
| Kimi | Moonshot Kimi model |
| Kimi For Coding | Kimi coding-specific model |
| StepFun | StepFun model |
| ModelScope | ModelScope community |
| KAT-Coder | KAT-Coder model |
| Longcat | Longcat AI |
@@ -92,6 +93,7 @@ Presets are pre-configured provider templates that only require an API Key to us
| Bailian | Alibaba Cloud Bailian |
| Kimi k2.5 | Moonshot Kimi-k2.5 model |
| Kimi For Coding | Kimi coding-specific model |
| StepFun | StepFun model |
| ModelScope | ModelScope community |
| KAT-Coder | KAT-Coder model |
| Longcat | Longcat AI |
@@ -124,6 +126,7 @@ Presets are pre-configured provider templates that only require an API Key to us
| Qwen Coder | Qwen coding model |
| Kimi k2.5 | Moonshot Kimi-k2.5 model |
| Kimi For Coding | Kimi coding-specific model |
| StepFun | StepFun model |
| MiniMax | MiniMax model |
| MiniMax en | MiniMax (English version) |
| KAT-Coder | KAT-Coder model |
+5 -1
View File
@@ -238,10 +238,14 @@ CC Switch includes preset official prices for common models (per million tokens)
| gemini-2.5-pro | $1.25 | $10 | $0.125 |
| gemini-2.5-flash | $0.30 | $2.50 | $0.03 |
**Chinese Provider Models (CNY)**:
**Chinese Provider Models**:
> Note: Currency follows each provider's official pricing page. StepFun is currently listed in USD.
| Model | Input | Output | Cache Read |
|-------|-------|--------|------------|
| **StepFun** | | | |
| step-3.5-flash | $0.10 | $0.30 | $0.02 |
| **DeepSeek** | | | |
| deepseek-v3.2 | ¥2.00 | ¥3.00 | ¥0.40 |
| deepseek-v3.1 | ¥4.00 | ¥12.00 | ¥0.80 |
@@ -33,6 +33,7 @@
| 百炼 | アリクラウド百炼(通义千問) |
| Kimi | Moonshot Kimi モデル |
| Kimi For Coding | Kimi プログラミング専用モデル |
| StepFun | StepFun モデル |
| ModelScope | 魔搭コミュニティ |
| KAT-Coder | KAT-Coder モデル |
| Longcat | Longcat AI |
@@ -92,6 +93,7 @@
| 百炼 | アリクラウド百炼 |
| Kimi k2.5 | Moonshot Kimi-k2.5 モデル |
| Kimi For Coding | Kimi プログラミング専用モデル |
| StepFun | StepFun モデル |
| ModelScope | 魔搭コミュニティ |
| KAT-Coder | KAT-Coder モデル |
| Longcat | Longcat AI |
@@ -124,6 +126,7 @@
| Qwen Coder | 通义千問コーディングモデル |
| Kimi k2.5 | Moonshot Kimi-k2.5 モデル |
| Kimi For Coding | Kimi プログラミング専用モデル |
| StepFun | StepFun モデル |
| MiniMax | MiniMax モデル |
| MiniMax en | MiniMax(英語版) |
| KAT-Coder | KAT-Coder モデル |
+5 -1
View File
@@ -238,10 +238,14 @@ CC Switch は一般的なモデルの公式価格(100 万 Token あたり)
| gemini-2.5-pro | $1.25 | $10 | $0.125 |
| gemini-2.5-flash | $0.30 | $2.50 | $0.03 |
**中国メーカーのモデル(人民元)**
**中国メーカーのモデル**
> 注: 通貨は各プロバイダーの公式料金ページに従います。StepFun は現在 USD 表記です。
| モデル | 入力 | 出力 | キャッシュ読取 |
|------|------|------|----------|
| **StepFun** | | | |
| step-3.5-flash | $0.10 | $0.30 | $0.02 |
| **DeepSeek** | | | |
| deepseek-v3.2 | ¥2.00 | ¥3.00 | ¥0.40 |
| deepseek-v3.1 | ¥4.00 | ¥12.00 | ¥0.80 |
+3 -1
View File
@@ -33,6 +33,7 @@
| 百炼 | 阿里云百炼(通义千问) |
| Kimi | Moonshot Kimi 模型 |
| Kimi For Coding | Kimi 编程专用模型 |
| StepFun | 阶跃星辰 Step模型 |
| ModelScope | 魔搭社区 |
| KAT-Coder | KAT-Coder 模型 |
| Longcat | Longcat AI |
@@ -92,6 +93,7 @@
| 百炼 | 阿里云百炼 |
| Kimi k2.5 | Moonshot Kimi-k2.5 模型 |
| Kimi For Coding | Kimi 编程专用模型 |
| StepFun | 阶跃星辰 Step模型 |
| ModelScope | 魔搭社区 |
| KAT-Coder | KAT-Coder 模型 |
| Longcat | Longcat AI |
@@ -124,6 +126,7 @@
| Qwen Coder | 通义千问编码模型 |
| Kimi k2.5 | Moonshot Kimi-k2.5 模型 |
| Kimi For Coding | Kimi 编程专用模型 |
| StepFun | 阶跃星辰 Step模型 |
| MiniMax | MiniMax 模型 |
| MiniMax en | MiniMax(英文版) |
| KAT-Coder | KAT-Coder 模型 |
@@ -352,4 +355,3 @@ CC Switch 支持两种方式导入供应商配置:
- 🔴 红色:延迟 > 1000ms(较慢)
![image-20260108005327817](../../assets/image-20260108005327817.png)
+5 -1
View File
@@ -238,10 +238,14 @@ CC Switch 预设了常用模型的官方价格(每百万 Token):
| gemini-2.5-pro | $1.25 | $10 | $0.125 |
| gemini-2.5-flash | $0.30 | $2.50 | $0.03 |
**中国厂商模型(人民币)**
**中国厂商模型**
> 注:币种遵循各供应商官方定价页面。StepFun 当前按美元列出。
| 模型 | 输入 | 输出 | 缓存读取 |
|------|------|------|----------|
| **StepFun** | | | |
| step-3.5-flash | $0.10 | $0.30 | $0.02 |
| **DeepSeek** | | | |
| deepseek-v3.2 | ¥2.00 | ¥3.00 | ¥0.40 |
| deepseek-v3.1 | ¥4.00 | ¥12.00 | ¥0.80 |
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "cc-switch",
"version": "3.12.0",
"version": "3.12.2",
"description": "All-in-One Assistant for Claude Code, Codex & Gemini CLI",
"type": "module",
"scripts": {
+5 -1
View File
@@ -6,7 +6,7 @@ const ICONS_TO_EXTRACT = {
// AI 服务商(必需)
aiProviders: [
'openai', 'anthropic', 'claude', 'google', 'gemini',
'deepseek', 'kimi', 'moonshot', 'zhipu', 'minimax',
'deepseek', 'kimi', 'moonshot', 'stepfun', 'zhipu', 'minimax',
'baidu', 'alibaba', 'tencent', 'meta', 'microsoft',
'cohere', 'perplexity', 'mistral', 'huggingface'
],
@@ -60,6 +60,9 @@ ALL_ICONS.forEach(iconName => {
fs.copyFileSync(sourceFile, targetFile);
console.log(`${iconName}.svg`);
extracted++;
} else if (fs.existsSync(targetFile)) {
console.log(`${iconName}.svg (kept local custom icon)`);
extracted++;
} else {
console.log(`${iconName}.svg (not found)`);
notFound.push(iconName);
@@ -110,6 +113,7 @@ export const iconMetadata: Record<string, IconMetadata> = {
deepseek: { name: 'deepseek', displayName: 'DeepSeek', category: 'ai-provider', keywords: ['deep', 'seek'], defaultColor: '#1E88E5' },
moonshot: { name: 'moonshot', displayName: 'Moonshot', category: 'ai-provider', keywords: ['kimi', 'moonshot'], defaultColor: '#6366F1' },
kimi: { name: 'kimi', displayName: 'Kimi', category: 'ai-provider', keywords: ['moonshot'], defaultColor: '#6366F1' },
stepfun: { name: 'stepfun', displayName: 'StepFun', category: 'ai-provider', keywords: ['stepfun', 'step', 'jieyue', '阶跃星辰'], defaultColor: '#005AFF' },
zhipu: { name: 'zhipu', displayName: 'Zhipu AI', category: 'ai-provider', keywords: ['chatglm', 'glm'], defaultColor: '#0F62FE' },
minimax: { name: 'minimax', displayName: 'MiniMax', category: 'ai-provider', keywords: ['minimax'], defaultColor: '#FF6B6B' },
baidu: { name: 'baidu', displayName: 'Baidu', category: 'ai-provider', keywords: ['ernie', 'wenxin'], defaultColor: '#2932E1' },
+1 -1
View File
@@ -10,7 +10,7 @@ const KEEP_LIST = [
'openai', 'anthropic', 'claude', 'google', 'gemini', 'gemma', 'palm',
'microsoft', 'azure', 'copilot', 'meta', 'llama',
'alibaba', 'qwen', 'tencent', 'hunyuan', 'baidu', 'wenxin',
'bytedance', 'doubao', 'deepseek', 'moonshot', 'kimi',
'bytedance', 'doubao', 'deepseek', 'moonshot', 'kimi', 'stepfun',
'zhipu', 'chatglm', 'glm', 'minimax', 'mistral', 'cohere',
'perplexity', 'huggingface', 'midjourney', 'stability',
'xai', 'grok', 'yi', 'zeroone', 'ollama',
+1
View File
@@ -15,6 +15,7 @@ const KNOWN_METADATA = {
deepseek: { name: 'deepseek', displayName: 'DeepSeek', category: 'ai-provider', keywords: ['deep', 'seek'], defaultColor: '#1E88E5' },
moonshot: { name: 'moonshot', displayName: 'Moonshot', category: 'ai-provider', keywords: ['kimi', 'moonshot'], defaultColor: '#6366F1' },
kimi: { name: 'kimi', displayName: 'Kimi', category: 'ai-provider', keywords: ['moonshot'], defaultColor: '#6366F1' },
stepfun: { name: 'stepfun', displayName: 'StepFun', category: 'ai-provider', keywords: ['stepfun', 'step', 'jieyue', '阶跃星辰'], defaultColor: '#005AFF' },
zhipu: { name: 'zhipu', displayName: 'Zhipu AI', category: 'ai-provider', keywords: ['chatglm', 'glm'], defaultColor: '#0F62FE' },
minimax: { name: 'minimax', displayName: 'MiniMax', category: 'ai-provider', keywords: ['minimax'], defaultColor: '#FF6B6B' },
baidu: { name: 'baidu', displayName: 'Baidu', category: 'ai-provider', keywords: ['ernie', 'wenxin'], defaultColor: '#2932E1' },
+1299 -1071
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "cc-switch"
version = "3.12.0"
version = "3.12.2"
description = "All-in-One Assistant for Claude Code, Codex & Gemini CLI"
authors = ["Jason Young"]
license = "MIT"
+333
View File
@@ -9,6 +9,7 @@ use crate::error::AppError;
use serde_json::Value;
use std::fs;
use std::path::Path;
use toml_edit::DocumentMut;
/// 获取 Codex 配置目录路径
pub fn get_codex_config_dir() -> PathBuf {
@@ -135,3 +136,335 @@ pub fn read_and_validate_codex_config_text() -> Result<String, AppError> {
validate_config_toml(&s)?;
Ok(s)
}
/// Update a field in Codex config.toml using toml_edit (syntax-preserving).
///
/// Supported fields:
/// - `"base_url"`: writes to `[model_providers.<current>].base_url` if `model_provider` exists,
/// otherwise falls back to top-level `base_url`.
/// - `"model"`: writes to top-level `model` field.
///
/// Empty value removes the field.
pub fn update_codex_toml_field(toml_str: &str, field: &str, value: &str) -> Result<String, String> {
let mut doc = toml_str
.parse::<DocumentMut>()
.map_err(|e| format!("TOML parse error: {e}"))?;
let trimmed = value.trim();
match field {
"base_url" => {
let model_provider = doc
.get("model_provider")
.and_then(|item| item.as_str())
.map(str::to_string);
if let Some(provider_key) = model_provider {
// Ensure [model_providers] table exists
if doc.get("model_providers").is_none() {
doc["model_providers"] = toml_edit::table();
}
if let Some(model_providers) = doc["model_providers"].as_table_mut() {
// Ensure [model_providers.<provider_key>] table exists
if !model_providers.contains_key(&provider_key) {
model_providers[&provider_key] = toml_edit::table();
}
if let Some(provider_table) = model_providers[&provider_key].as_table_mut() {
if trimmed.is_empty() {
provider_table.remove("base_url");
} else {
provider_table["base_url"] = toml_edit::value(trimmed);
}
return Ok(doc.to_string());
}
}
}
// Fallback: no model_provider or structure mismatch → top-level base_url
if trimmed.is_empty() {
doc.as_table_mut().remove("base_url");
} else {
doc["base_url"] = toml_edit::value(trimmed);
}
}
"model" => {
if trimmed.is_empty() {
doc.as_table_mut().remove("model");
} else {
doc["model"] = toml_edit::value(trimmed);
}
}
_ => return Err(format!("unsupported field: {field}")),
}
Ok(doc.to_string())
}
/// Remove `base_url` from the active model_provider section only if it matches `predicate`.
/// Also removes top-level `base_url` if it matches.
/// Used by proxy cleanup to strip local proxy URLs without touching user-configured URLs.
pub fn remove_codex_toml_base_url_if(toml_str: &str, predicate: impl Fn(&str) -> bool) -> String {
let mut doc = match toml_str.parse::<DocumentMut>() {
Ok(doc) => doc,
Err(_) => return toml_str.to_string(),
};
let model_provider = doc
.get("model_provider")
.and_then(|item| item.as_str())
.map(str::to_string);
if let Some(provider_key) = model_provider {
if let Some(model_providers) = doc
.get_mut("model_providers")
.and_then(|v| v.as_table_mut())
{
if let Some(provider_table) = model_providers
.get_mut(provider_key.as_str())
.and_then(|v| v.as_table_mut())
{
let should_remove = provider_table
.get("base_url")
.and_then(|item| item.as_str())
.map(&predicate)
.unwrap_or(false);
if should_remove {
provider_table.remove("base_url");
}
}
}
}
// Fallback: also clean up top-level base_url if it matches
let should_remove_root = doc
.get("base_url")
.and_then(|item| item.as_str())
.map(&predicate)
.unwrap_or(false);
if should_remove_root {
doc.as_table_mut().remove("base_url");
}
doc.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn base_url_writes_into_correct_model_provider_section() {
let input = r#"model_provider = "any"
model = "gpt-5.1-codex"
[model_providers.any]
name = "any"
wire_api = "responses"
"#;
let result = update_codex_toml_field(input, "base_url", "https://example.com/v1").unwrap();
let parsed: toml::Value = toml::from_str(&result).unwrap();
let base_url = parsed
.get("model_providers")
.and_then(|v| v.get("any"))
.and_then(|v| v.get("base_url"))
.and_then(|v| v.as_str())
.expect("base_url should be in model_providers.any");
assert_eq!(base_url, "https://example.com/v1");
// Should NOT have top-level base_url
assert!(parsed.get("base_url").is_none());
// wire_api preserved
let wire_api = parsed
.get("model_providers")
.and_then(|v| v.get("any"))
.and_then(|v| v.get("wire_api"))
.and_then(|v| v.as_str());
assert_eq!(wire_api, Some("responses"));
}
#[test]
fn base_url_creates_section_when_missing() {
let input = r#"model_provider = "custom"
model = "gpt-4"
"#;
let result = update_codex_toml_field(input, "base_url", "https://custom.api/v1").unwrap();
let parsed: toml::Value = toml::from_str(&result).unwrap();
let base_url = parsed
.get("model_providers")
.and_then(|v| v.get("custom"))
.and_then(|v| v.get("base_url"))
.and_then(|v| v.as_str())
.expect("should create section and set base_url");
assert_eq!(base_url, "https://custom.api/v1");
}
#[test]
fn base_url_falls_back_to_top_level_without_model_provider() {
let input = r#"model = "gpt-4"
"#;
let result = update_codex_toml_field(input, "base_url", "https://fallback.api/v1").unwrap();
let parsed: toml::Value = toml::from_str(&result).unwrap();
let base_url = parsed
.get("base_url")
.and_then(|v| v.as_str())
.expect("should set top-level base_url");
assert_eq!(base_url, "https://fallback.api/v1");
}
#[test]
fn clearing_base_url_removes_only_from_correct_section() {
let input = r#"model_provider = "any"
[model_providers.any]
name = "any"
base_url = "https://old.api/v1"
wire_api = "responses"
[mcp_servers.context7]
command = "npx"
"#;
let result = update_codex_toml_field(input, "base_url", "").unwrap();
let parsed: toml::Value = toml::from_str(&result).unwrap();
// base_url removed from model_providers.any
let any_section = parsed
.get("model_providers")
.and_then(|v| v.get("any"))
.expect("model_providers.any should exist");
assert!(any_section.get("base_url").is_none());
// wire_api preserved
assert_eq!(
any_section.get("wire_api").and_then(|v| v.as_str()),
Some("responses")
);
// mcp_servers untouched
assert!(parsed.get("mcp_servers").is_some());
}
#[test]
fn model_field_operates_on_top_level() {
let input = r#"model_provider = "any"
model = "gpt-4"
[model_providers.any]
name = "any"
"#;
let result = update_codex_toml_field(input, "model", "gpt-5").unwrap();
let parsed: toml::Value = toml::from_str(&result).unwrap();
assert_eq!(parsed.get("model").and_then(|v| v.as_str()), Some("gpt-5"));
// Clear model
let result2 = update_codex_toml_field(&result, "model", "").unwrap();
let parsed2: toml::Value = toml::from_str(&result2).unwrap();
assert!(parsed2.get("model").is_none());
}
#[test]
fn preserves_comments_and_whitespace() {
let input = r#"# My Codex config
model_provider = "any"
model = "gpt-4"
# Provider section
[model_providers.any]
name = "any"
base_url = "https://old.api/v1"
"#;
let result = update_codex_toml_field(input, "base_url", "https://new.api/v1").unwrap();
// Comments should be preserved
assert!(result.contains("# My Codex config"));
assert!(result.contains("# Provider section"));
}
#[test]
fn does_not_misplace_when_profiles_section_follows() {
let input = r#"model_provider = "any"
[model_providers.any]
name = "any"
base_url = "https://old.api/v1"
[profiles.default]
model = "gpt-4"
"#;
let result = update_codex_toml_field(input, "base_url", "https://new.api/v1").unwrap();
let parsed: toml::Value = toml::from_str(&result).unwrap();
// base_url in correct section
let base_url = parsed
.get("model_providers")
.and_then(|v| v.get("any"))
.and_then(|v| v.get("base_url"))
.and_then(|v| v.as_str());
assert_eq!(base_url, Some("https://new.api/v1"));
// profiles section untouched
let profile_model = parsed
.get("profiles")
.and_then(|v| v.get("default"))
.and_then(|v| v.get("model"))
.and_then(|v| v.as_str());
assert_eq!(profile_model, Some("gpt-4"));
}
#[test]
fn remove_base_url_if_predicate() {
let input = r#"model_provider = "any"
[model_providers.any]
name = "any"
base_url = "http://127.0.0.1:5000/v1"
wire_api = "responses"
"#;
let result =
remove_codex_toml_base_url_if(input, |url| url.starts_with("http://127.0.0.1"));
let parsed: toml::Value = toml::from_str(&result).unwrap();
let any_section = parsed
.get("model_providers")
.and_then(|v| v.get("any"))
.unwrap();
assert!(any_section.get("base_url").is_none());
assert_eq!(
any_section.get("wire_api").and_then(|v| v.as_str()),
Some("responses")
);
}
#[test]
fn remove_base_url_if_keeps_non_matching() {
let input = r#"model_provider = "any"
[model_providers.any]
base_url = "https://production.api/v1"
"#;
let result =
remove_codex_toml_base_url_if(input, |url| url.starts_with("http://127.0.0.1"));
let parsed: toml::Value = toml::from_str(&result).unwrap();
let base_url = parsed
.get("model_providers")
.and_then(|v| v.get("any"))
.and_then(|v| v.get("base_url"))
.and_then(|v| v.as_str());
assert_eq!(base_url, Some("https://production.api/v1"));
}
}
+13 -10
View File
@@ -212,20 +212,22 @@ pub async fn set_claude_common_config_snippet(
snippet: String,
state: tauri::State<'_, crate::store::AppState>,
) -> Result<(), String> {
let is_cleared = snippet.trim().is_empty();
if !snippet.trim().is_empty() {
serde_json::from_str::<serde_json::Value>(&snippet).map_err(invalid_json_format_error)?;
}
let value = if snippet.trim().is_empty() {
None
} else {
Some(snippet)
};
let value = if is_cleared { None } else { Some(snippet) };
state
.db
.set_config_snippet("claude", value)
.map_err(|e| e.to_string())?;
state
.db
.set_config_snippet_cleared("claude", is_cleared)
.map_err(|e| e.to_string())?;
Ok(())
}
@@ -246,6 +248,7 @@ pub async fn set_common_config_snippet(
snippet: String,
state: tauri::State<'_, crate::store::AppState>,
) -> Result<(), String> {
let is_cleared = snippet.trim().is_empty();
let old_snippet = state
.db
.get_config_snippet(&app_type)
@@ -253,11 +256,7 @@ pub async fn set_common_config_snippet(
validate_common_config_snippet(&app_type, &snippet)?;
let value = if snippet.trim().is_empty() {
None
} else {
Some(snippet)
};
let value = if is_cleared { None } else { Some(snippet) };
if matches!(app_type.as_str(), "claude" | "codex" | "gemini") {
if let Some(legacy_snippet) = old_snippet
@@ -278,6 +277,10 @@ pub async fn set_common_config_snippet(
.db
.set_config_snippet(&app_type, value)
.map_err(|e| e.to_string())?;
state
.db
.set_config_snippet_cleared(&app_type, is_cleared)
.map_err(|e| e.to_string())?;
if matches!(app_type.as_str(), "claude" | "codex" | "gemini") {
let app = AppType::from_str(&app_type).map_err(|e| e.to_string())?;
+1 -1
View File
@@ -115,7 +115,7 @@ pub async fn open_zip_file_dialog<R: tauri::Runtime>(
let dialog = app.dialog();
let result = dialog
.file()
.add_filter("ZIP", &["zip"])
.add_filter("ZIP / Skill", &["zip", "skill"])
.blocking_pick_file();
Ok(result.map(|p| p.to_string()))
+6 -4
View File
@@ -103,20 +103,22 @@ fn import_default_config_internal(state: &AppState, app_type: AppType) -> Result
// Extract common config snippet (mirrors old startup logic in lib.rs)
if state
.db
.get_config_snippet(app_type.as_str())
.ok()
.flatten()
.is_none()
.should_auto_extract_config_snippet(app_type.as_str())?
{
match ProviderService::extract_common_config_snippet(state, app_type.clone()) {
Ok(snippet) if !snippet.is_empty() && snippet != "{}" => {
let _ = state
.db
.set_config_snippet(app_type.as_str(), Some(snippet));
let _ = state
.db
.set_config_snippet_cleared(app_type.as_str(), false);
}
_ => {}
}
}
ProviderService::migrate_legacy_common_config_usage_if_needed(state, app_type.clone())?;
}
Ok(imported)
+13 -5
View File
@@ -15,9 +15,8 @@ use tempfile::NamedTempFile;
const CC_SWITCH_SQL_EXPORT_HEADER: &str = "-- CC Switch SQLite 导出";
/// Tables that are local-only (not synced via WebDAV snapshots).
/// Schema is still exported, but data rows are skipped.
const LOCAL_ONLY_TABLES: &[&str] = &[
/// Tables whose data rows are skipped when exporting for WebDAV sync.
const SYNC_SKIP_TABLES: &[&str] = &[
"proxy_request_logs",
"stream_check_logs",
"provider_health",
@@ -25,6 +24,15 @@ const LOCAL_ONLY_TABLES: &[&str] = &[
"usage_daily_rollups",
];
/// Tables whose local data is preserved (restored from local snapshot) during WebDAV import.
/// Excludes ephemeral tables like provider_health that can safely rebuild at runtime.
const SYNC_PRESERVE_TABLES: &[&str] = &[
"proxy_request_logs",
"stream_check_logs",
"proxy_live_backup",
"usage_daily_rollups",
];
/// A database backup entry for the UI
#[derive(Debug, serde::Serialize)]
#[serde(rename_all = "camelCase")]
@@ -44,7 +52,7 @@ impl Database {
/// Export SQL for sync (WebDAV), skipping local-only tables' data
pub fn export_sql_string_for_sync(&self) -> Result<String, AppError> {
let snapshot = self.snapshot_to_memory()?;
Self::dump_sql(&snapshot, LOCAL_ONLY_TABLES)
Self::dump_sql(&snapshot, SYNC_SKIP_TABLES)
}
/// 导出为 SQLite 兼容的 SQL 文本
@@ -80,7 +88,7 @@ impl Database {
/// Import SQL generated for sync, then restore local-only tables from the
/// current device snapshot before replacing the main database.
pub(crate) fn import_sql_string_for_sync(&self, sql_raw: &str) -> Result<String, AppError> {
self.import_sql_string_inner(sql_raw, LOCAL_ONLY_TABLES)
self.import_sql_string_inner(sql_raw, SYNC_PRESERVE_TABLES)
}
fn import_sql_string_inner(
+60
View File
@@ -7,6 +7,12 @@ use crate::error::AppError;
use rusqlite::params;
impl Database {
const LEGACY_COMMON_CONFIG_MIGRATED_KEY: &'static str = "common_config_legacy_migrated_v1";
fn config_snippet_cleared_key(app_type: &str) -> String {
format!("common_config_{app_type}_cleared")
}
/// 获取设置值
pub fn get_setting(&self, key: &str) -> Result<Option<String>, AppError> {
let conn = lock_conn!(self.conn);
@@ -45,6 +51,60 @@ impl Database {
self.get_setting(&format!("common_config_{app_type}"))
}
/// 检查通用配置片段是否被用户显式清空
pub fn is_config_snippet_cleared(&self, app_type: &str) -> Result<bool, AppError> {
Ok(self
.get_setting(&Self::config_snippet_cleared_key(app_type))?
.as_deref()
== Some("true"))
}
/// 设置通用配置片段是否被显式清空
pub fn set_config_snippet_cleared(
&self,
app_type: &str,
cleared: bool,
) -> Result<(), AppError> {
let key = Self::config_snippet_cleared_key(app_type);
if cleared {
self.set_setting(&key, "true")
} else {
let conn = lock_conn!(self.conn);
conn.execute("DELETE FROM settings WHERE key = ?1", params![key])
.map_err(|e| AppError::Database(e.to_string()))?;
Ok(())
}
}
/// 当前是否允许从 live 配置自动抽取通用配置片段
pub fn should_auto_extract_config_snippet(&self, app_type: &str) -> Result<bool, AppError> {
Ok(self.get_config_snippet(app_type)?.is_none()
&& !self.is_config_snippet_cleared(app_type)?)
}
/// 检查历史通用配置迁移是否已经执行过
pub fn is_legacy_common_config_migrated(&self) -> Result<bool, AppError> {
Ok(self
.get_setting(Self::LEGACY_COMMON_CONFIG_MIGRATED_KEY)?
.as_deref()
== Some("true"))
}
/// 标记历史通用配置迁移已经执行完成
pub fn set_legacy_common_config_migrated(&self, migrated: bool) -> Result<(), AppError> {
if migrated {
self.set_setting(Self::LEGACY_COMMON_CONFIG_MIGRATED_KEY, "true")
} else {
let conn = lock_conn!(self.conn);
conn.execute(
"DELETE FROM settings WHERE key = ?1",
params![Self::LEGACY_COMMON_CONFIG_MIGRATED_KEY],
)
.map_err(|e| AppError::Database(e.to_string()))?;
Ok(())
}
}
/// 设置通用配置片段
pub fn set_config_snippet(
&self,
+9
View File
@@ -1241,6 +1241,15 @@ impl Database {
"0.03",
"0",
),
// StepFun 系列
(
"step-3.5-flash",
"Step 3.5 Flash",
"0.10",
"0.30",
"0.02",
"0",
),
// ====== 国产模型 (CNY/1M tokens) ======
// Doubao (字节跳动)
(
+81 -53
View File
@@ -560,59 +560,6 @@ pub fn run() {
}
}
// 5. Auto-extract common config snippets from live files (when snippet is missing)
for app_type in crate::app_config::AppType::all() {
// Skip if snippet already exists
if app_state
.db
.get_config_snippet(app_type.as_str())
.ok()
.flatten()
.is_some()
{
continue;
}
// Try to read the live config file for this app type
let settings =
match crate::services::provider::ProviderService::read_live_settings(
app_type.clone(),
) {
Ok(s) => s,
Err(_) => continue, // No live config file, skip silently
};
// Extract common config (strip provider-specific fields)
match crate::services::provider::ProviderService::extract_common_config_snippet_from_settings(
app_type.clone(),
&settings,
) {
Ok(snippet) if !snippet.is_empty() && snippet != "{}" => {
match app_state
.db
.set_config_snippet(app_type.as_str(), Some(snippet))
{
Ok(()) => log::info!(
"✓ Auto-extracted common config snippet for {}",
app_type.as_str()
),
Err(e) => log::warn!(
"✗ Failed to save config snippet for {}: {e}",
app_type.as_str()
),
}
}
Ok(_) => log::debug!(
"○ Live config for {} has no extractable common fields",
app_type.as_str()
),
Err(e) => log::warn!(
"✗ Failed to extract config snippet for {}: {e}",
app_type.as_str()
),
}
}
// 迁移旧的 app_config_dir 配置到 Store
if let Err(e) = app_store::migrate_app_config_dir_from_settings(app.handle()) {
log::warn!("迁移 app_config_dir 失败: {e}");
@@ -797,6 +744,8 @@ pub fn run() {
}
}
initialize_common_config_snippets(&state);
// 检查 settings 表中的代理状态,自动恢复代理服务
restore_proxy_state_on_startup(&state).await;
@@ -1305,6 +1254,85 @@ async fn restore_proxy_state_on_startup(state: &store::AppState) {
}
}
fn initialize_common_config_snippets(state: &store::AppState) {
// Auto-extract common config snippets from clean live files when snippet is missing.
// This must run before proxy takeover is restored on startup, otherwise we'd read
// proxy-placeholder configs instead of the user's actual live settings.
for app_type in crate::app_config::AppType::all() {
if !state
.db
.should_auto_extract_config_snippet(app_type.as_str())
.unwrap_or(false)
{
continue;
}
let settings = match crate::services::provider::ProviderService::read_live_settings(
app_type.clone(),
) {
Ok(s) => s,
Err(_) => continue,
};
match crate::services::provider::ProviderService::extract_common_config_snippet_from_settings(
app_type.clone(),
&settings,
) {
Ok(snippet) if !snippet.is_empty() && snippet != "{}" => {
match state.db.set_config_snippet(app_type.as_str(), Some(snippet)) {
Ok(()) => {
let _ = state.db.set_config_snippet_cleared(app_type.as_str(), false);
log::info!(
"✓ Auto-extracted common config snippet for {}",
app_type.as_str()
);
}
Err(e) => log::warn!(
"✗ Failed to save config snippet for {}: {e}",
app_type.as_str()
),
}
}
Ok(_) => log::debug!(
"○ Live config for {} has no extractable common fields",
app_type.as_str()
),
Err(e) => log::warn!(
"✗ Failed to extract config snippet for {}: {e}",
app_type.as_str()
),
}
}
let should_run_legacy_migration = state
.db
.is_legacy_common_config_migrated()
.map(|done| !done)
.unwrap_or(true);
if should_run_legacy_migration {
for app_type in [
crate::app_config::AppType::Claude,
crate::app_config::AppType::Codex,
crate::app_config::AppType::Gemini,
] {
if let Err(e) = crate::services::provider::ProviderService::migrate_legacy_common_config_usage_if_needed(
state,
app_type.clone(),
) {
log::warn!(
"✗ Failed to migrate legacy common-config usage for {}: {e}",
app_type.as_str()
);
}
}
if let Err(e) = state.db.set_legacy_common_config_migrated(true) {
log::warn!("✗ Failed to persist legacy common-config migration flag: {e}");
}
}
}
// ============================================================
// 迁移错误对话框辅助函数
// ============================================================
+17 -6
View File
@@ -461,19 +461,18 @@ fn apply_common_config_to_settings(
}
}
pub(crate) fn write_live_with_common_config(
pub(crate) fn build_effective_settings_with_common_config(
db: &Database,
app_type: &AppType,
provider: &Provider,
) -> Result<(), AppError> {
) -> Result<Value, AppError> {
let snippet = db.get_config_snippet(app_type.as_str())?;
let mut effective_provider = provider.clone();
let mut effective_settings = provider.settings_config.clone();
if provider_uses_common_config(app_type, provider, snippet.as_deref()) {
if let Some(snippet_text) = snippet.as_deref() {
match apply_common_config_to_settings(app_type, &provider.settings_config, snippet_text)
{
Ok(settings) => effective_provider.settings_config = settings,
match apply_common_config_to_settings(app_type, &effective_settings, snippet_text) {
Ok(settings) => effective_settings = settings,
Err(err) => {
log::warn!(
"Failed to apply common config for {} provider '{}': {err}",
@@ -485,6 +484,18 @@ pub(crate) fn write_live_with_common_config(
}
}
Ok(effective_settings)
}
pub(crate) fn write_live_with_common_config(
db: &Database,
app_type: &AppType,
provider: &Provider,
) -> Result<(), AppError> {
let mut effective_provider = provider.clone();
effective_provider.settings_config =
build_effective_settings_with_common_config(db, app_type, provider)?;
write_live_snapshot(app_type, &effective_provider)
}
+62 -2
View File
@@ -28,8 +28,9 @@ pub use live::{
// Internal re-exports (pub(crate))
pub(crate) use live::sanitize_claude_settings_for_live;
pub(crate) use live::{
normalize_provider_common_config_for_storage, strip_common_config_from_live_settings,
sync_current_provider_for_app_to_live, write_live_with_common_config,
build_effective_settings_with_common_config, normalize_provider_common_config_for_storage,
strip_common_config_from_live_settings, sync_current_provider_for_app_to_live,
write_live_with_common_config,
};
// Internal re-exports
@@ -613,6 +614,46 @@ impl ProviderService {
state: &AppState,
app_type: AppType,
) -> Result<(), AppError> {
if app_type.is_additive_mode() {
return sync_current_provider_for_app_to_live(state, &app_type);
}
let current_id =
match crate::settings::get_effective_current_provider(&state.db, &app_type)? {
Some(id) => id,
None => return Ok(()),
};
let providers = state.db.get_all_providers(app_type.as_str())?;
let Some(provider) = providers.get(&current_id) else {
return Ok(());
};
let takeover_enabled =
futures::executor::block_on(state.db.get_proxy_config_for_app(app_type.as_str()))
.map(|config| config.enabled)
.unwrap_or(false);
let has_live_backup =
futures::executor::block_on(state.db.get_live_backup(app_type.as_str()))
.ok()
.flatten()
.is_some();
let live_taken_over = state
.proxy_service
.detect_takeover_in_live_config_for_app(&app_type);
if takeover_enabled && (has_live_backup || live_taken_over) {
futures::executor::block_on(
state
.proxy_service
.update_live_backup_from_provider(app_type.as_str(), provider),
)
.map_err(|e| AppError::Message(format!("更新 Live 备份失败: {e}")))?;
return Ok(());
}
sync_current_provider_for_app_to_live(state, &app_type)
}
@@ -670,6 +711,25 @@ impl ProviderService {
Ok(())
}
pub fn migrate_legacy_common_config_usage_if_needed(
state: &AppState,
app_type: AppType,
) -> Result<(), AppError> {
if app_type.is_additive_mode() {
return Ok(());
}
let Some(snippet) = state.db.get_config_snippet(app_type.as_str())? else {
return Ok(());
};
if snippet.trim().is_empty() {
return Ok(());
}
Self::migrate_legacy_common_config_usage(state, app_type, &snippet)
}
/// Extract common config snippet from current provider
///
/// Extracts the current provider's configuration and removes provider-specific fields
+390 -108
View File
@@ -8,7 +8,9 @@ use crate::database::Database;
use crate::provider::Provider;
use crate::proxy::server::ProxyServer;
use crate::proxy::types::*;
use crate::services::provider::write_live_with_common_config;
use crate::services::provider::{
build_effective_settings_with_common_config, write_live_with_common_config,
};
use serde_json::{json, Value};
use std::str::FromStr;
use std::sync::Arc;
@@ -17,7 +19,7 @@ use tokio::sync::RwLock;
/// 用于接管 Live 配置时的占位符(避免客户端提示缺少 key,同时不泄露真实 Token)
const PROXY_TOKEN_PLACEHOLDER: &str = "PROXY_MANAGED";
/// 代理接管模式下需要从 Claude Live 配置中移除的模型覆盖字段。
/// 代理接管模式下需要从 Claude Live 配置中移除的"模型覆盖"字段。
///
/// 原因:接管模式切换供应商时不会写回 Live 配置,如果保留这些字段,
/// Claude Code 会继续以旧模型名发起请求,导致新供应商不支持时失败。
@@ -50,7 +52,7 @@ impl ProxyService {
/// 清理接管模式下 Claude Live 配置中的模型覆盖字段。
///
/// 这可以避免接管开启后切换供应商仍使用旧模型的问题。
/// 这可以避免"接管开启后切换供应商仍使用旧模型"的问题。
/// 注意:此方法不会修改 Token/Base URL 的接管占位符,仅移除模型字段。
pub fn cleanup_claude_model_overrides_in_live(&self) -> Result<(), String> {
let mut config = self.read_claude_live()?;
@@ -1160,7 +1162,7 @@ impl ProxyService {
) -> Result<(), String> {
let app_type_str = app_type.as_str();
// 1) 优先从 Live 备份恢复(这是原始 Live的唯一可靠来源)
// 1) 优先从 Live 备份恢复(这是"原始 Live"的唯一可靠来源)
let backup = self
.db
.get_live_backup(app_type_str)
@@ -1179,7 +1181,7 @@ impl ProxyService {
return Ok(());
}
// 2.1) 优先从 SSOT(当前供应商)重建 Live(比清理字段更可用)
// 2.1) 优先从 SSOT(当前供应商)重建 Live(比"清理字段"更可用)
match self.restore_live_from_ssot_for_app(app_type) {
Ok(true) => {
log::info!("{app_type_str} Live 配置已从 SSOT 恢复(无备份兜底)");
@@ -1356,51 +1358,9 @@ impl ProxyService {
Ok(())
}
/// Remove local proxy base_url from TOML(委托给 codex_config 共享实现)
fn remove_local_toml_base_url(toml_str: &str) -> String {
use toml_edit::DocumentMut;
let mut doc = match toml_str.parse::<DocumentMut>() {
Ok(doc) => doc,
Err(_) => return toml_str.to_string(),
};
let model_provider = doc
.get("model_provider")
.and_then(|item| item.as_str())
.map(str::to_string);
if let Some(provider_key) = model_provider {
if let Some(model_providers) = doc
.get_mut("model_providers")
.and_then(|v| v.as_table_mut())
{
if let Some(provider_table) = model_providers
.get_mut(provider_key.as_str())
.and_then(|v| v.as_table_mut())
{
let should_remove = provider_table
.get("base_url")
.and_then(|item| item.as_str())
.map(Self::is_local_proxy_url)
.unwrap_or(false);
if should_remove {
provider_table.remove("base_url");
}
}
}
}
// 兜底:清理顶层 base_url(仅当它看起来像本地代理地址)
let should_remove_root = doc
.get("base_url")
.and_then(|item| item.as_str())
.map(Self::is_local_proxy_url)
.unwrap_or(false);
if should_remove_root {
doc.as_table_mut().remove("base_url");
}
doc.to_string()
crate::codex_config::remove_codex_toml_base_url_if(toml_str, Self::is_local_proxy_url)
}
fn cleanup_gemini_takeover_placeholders_in_live(&self) -> Result<(), String> {
@@ -1457,7 +1417,7 @@ impl ProxyService {
Ok(())
}
/// 检测 Live 配置是否处于被接管的残留状态
/// 检测 Live 配置是否处于"被接管"的残留状态
///
/// 用于兜底处理:当数据库备份缺失但 Live 文件已经写成代理占位符时,
/// 启动流程可以据此触发恢复逻辑。
@@ -1528,21 +1488,37 @@ impl ProxyService {
app_type: &str,
provider: &Provider,
) -> Result<(), String> {
let backup_json = match app_type {
"claude" => {
// Claude: settings_config 直接作为备份
serde_json::to_string(&provider.settings_config)
.map_err(|e| format!("序列化 Claude 配置失败: {e}"))?
let app_type_enum =
AppType::from_str(app_type).map_err(|_| format!("未知的应用类型: {app_type}"))?;
let mut effective_settings =
build_effective_settings_with_common_config(self.db.as_ref(), &app_type_enum, provider)
.map_err(|e| format!("构建 {app_type} 有效配置失败: {e}"))?;
if matches!(app_type_enum, AppType::Codex) {
let existing_backup = self
.db
.get_live_backup(app_type)
.await
.map_err(|e| format!("读取 {app_type} 现有备份失败: {e}"))?;
if let Some(existing_backup) = existing_backup {
let existing_value: Value = serde_json::from_str(&existing_backup.original_config)
.map_err(|e| format!("解析 {app_type} 现有备份失败: {e}"))?;
Self::preserve_codex_mcp_servers_in_backup(
&mut effective_settings,
&existing_value,
)?;
}
"codex" => {
// Codex: settings_config 包含 {"auth": ..., "config": ...},直接使用
serde_json::to_string(&provider.settings_config)
.map_err(|e| format!("序列化 Codex 配置失败: {e}"))?
}
"gemini" => {
// Gemini: 只提取 env 字段(与原始备份格式一致)
// proxy.rs 的 read_gemini_live() 返回 {"env": {...}}
let env_backup = if let Some(env) = provider.settings_config.get("env") {
}
let backup_json = match app_type_enum {
AppType::Claude => serde_json::to_string(&effective_settings)
.map_err(|e| format!("序列化 Claude 配置失败: {e}"))?,
AppType::Codex => serde_json::to_string(&effective_settings)
.map_err(|e| format!("序列化 Codex 配置失败: {e}"))?,
AppType::Gemini => {
// Gemini takeover 仅修改 .envsettings.json(含 mcpServers)保持原样。
let env_backup = if let Some(env) = effective_settings.get("env") {
json!({ "env": env })
} else {
json!({ "env": {} })
@@ -1550,7 +1526,9 @@ impl ProxyService {
serde_json::to_string(&env_backup)
.map_err(|e| format!("序列化 Gemini 配置失败: {e}"))?
}
_ => return Err(format!("未知的应用类型: {app_type}")),
AppType::OpenCode | AppType::OpenClaw => {
return Err(format!("未知的应用类型: {app_type}"));
}
};
self.db
@@ -1562,6 +1540,67 @@ impl ProxyService {
Ok(())
}
fn preserve_codex_mcp_servers_in_backup(
target_settings: &mut Value,
existing_backup: &Value,
) -> Result<(), String> {
let target_obj = target_settings
.as_object_mut()
.ok_or_else(|| "Codex 备份必须是 JSON 对象".to_string())?;
let target_config = target_obj
.get("config")
.and_then(|v| v.as_str())
.unwrap_or("");
let mut target_doc = if target_config.trim().is_empty() {
toml_edit::DocumentMut::new()
} else {
target_config
.parse::<toml_edit::DocumentMut>()
.map_err(|e| format!("解析新的 Codex config.toml 失败: {e}"))?
};
let existing_config = existing_backup
.get("config")
.and_then(|v| v.as_str())
.unwrap_or("");
if existing_config.trim().is_empty() {
target_obj.insert("config".to_string(), json!(target_doc.to_string()));
return Ok(());
}
let existing_doc = existing_config
.parse::<toml_edit::DocumentMut>()
.map_err(|e| format!("解析现有 Codex 备份失败: {e}"))?;
if let Some(existing_mcp_servers) = existing_doc.get("mcp_servers") {
match target_doc.get_mut("mcp_servers") {
Some(target_mcp_servers) => {
if let (Some(target_table), Some(existing_table)) = (
target_mcp_servers.as_table_like_mut(),
existing_mcp_servers.as_table_like(),
) {
for (server_id, server_item) in existing_table.iter() {
if target_table.get(server_id).is_none() {
target_table.insert(server_id, server_item.clone());
}
}
} else {
log::warn!(
"Codex config contains a non-table mcp_servers section; skipping backup MCP merge"
);
}
}
None => {
target_doc["mcp_servers"] = existing_mcp_servers.clone();
}
}
}
target_obj.insert("config".to_string(), json!(target_doc.to_string()));
Ok(())
}
/// 代理模式下切换供应商(热切换,不写 Live)
pub async fn switch_proxy_target(
&self,
@@ -1614,49 +1653,10 @@ impl ProxyService {
// ==================== Live 配置读写辅助方法 ====================
/// 更新 TOML 字符串中的 base_url
/// 更新 TOML 字符串中的 base_url(委托给 codex_config 共享实现)
fn update_toml_base_url(toml_str: &str, new_url: &str) -> String {
use toml_edit::DocumentMut;
let mut doc = match toml_str.parse::<DocumentMut>() {
Ok(doc) => doc,
Err(_) => return toml_str.to_string(),
};
// Codex 的 config.toml 通常是:
// model_provider = "any"
//
// [model_providers.any]
// base_url = "https://.../v1"
//
// 所以接管时要“精准”修改当前 model_provider 对应的 model_providers.<name>.base_url
// 避免写错位置导致 Codex 仍然走旧地址。
let model_provider = doc
.get("model_provider")
.and_then(|item| item.as_str())
.map(str::to_string);
if let Some(provider_key) = model_provider {
if doc.get("model_providers").is_none() {
doc["model_providers"] = toml_edit::table();
}
if let Some(model_providers) = doc["model_providers"].as_table_mut() {
if !model_providers.contains_key(&provider_key) {
model_providers[&provider_key] = toml_edit::table();
}
if let Some(provider_table) = model_providers[&provider_key].as_table_mut() {
provider_table["base_url"] = toml_edit::value(new_url);
return doc.to_string();
}
}
}
// 兜底:如果没有 model_provider 或结构不符合预期,则退回修改顶层 base_url。
doc["base_url"] = toml_edit::value(new_url);
doc.to_string()
crate::codex_config::update_codex_toml_field(toml_str, "base_url", new_url)
.unwrap_or_else(|_| toml_str.to_string())
}
fn read_claude_live(&self) -> Result<Value, String> {
@@ -1914,6 +1914,7 @@ impl ProxyService {
#[cfg(test)]
mod tests {
use super::*;
use crate::provider::ProviderMeta;
use serial_test::serial;
use std::env;
use tempfile::TempDir;
@@ -2166,7 +2167,7 @@ model = "gpt-5.1-codex"
db.set_current_provider("claude", "a")
.expect("set current provider");
// 模拟已接管状态:存在 Live 备份(内容不重要,会被热切换更新)
// 模拟"已接管"状态:存在 Live 备份(内容不重要,会被热切换更新)
db.save_live_backup("claude", "{\"env\":{}}")
.await
.expect("seed live backup");
@@ -2191,4 +2192,285 @@ model = "gpt-5.1-codex"
let expected = serde_json::to_string(&provider_b.settings_config).expect("serialize");
assert_eq!(backup.original_config, expected);
}
#[tokio::test]
#[serial]
async fn update_live_backup_from_provider_applies_claude_common_config() {
let _home = TempHome::new();
crate::settings::reload_settings().expect("reload settings");
let db = Arc::new(Database::memory().expect("init db"));
db.set_config_snippet(
"claude",
Some(
serde_json::json!({
"includeCoAuthoredBy": false
})
.to_string(),
),
)
.expect("set common config snippet");
let service = ProxyService::new(db.clone());
let mut provider = Provider::with_id(
"p1".to_string(),
"P1".to_string(),
json!({
"env": {
"ANTHROPIC_AUTH_TOKEN": "token",
"ANTHROPIC_BASE_URL": "https://claude.example"
}
}),
None,
);
provider.meta = Some(ProviderMeta {
common_config_enabled: Some(true),
..Default::default()
});
service
.update_live_backup_from_provider("claude", &provider)
.await
.expect("update live backup");
let backup = db
.get_live_backup("claude")
.await
.expect("get live backup")
.expect("backup exists");
let stored: Value =
serde_json::from_str(&backup.original_config).expect("parse backup json");
assert_eq!(
stored.get("includeCoAuthoredBy").and_then(|v| v.as_bool()),
Some(false),
"common config should be applied into Claude restore backup"
);
}
#[tokio::test]
#[serial]
async fn update_live_backup_from_provider_applies_codex_common_config() {
let _home = TempHome::new();
crate::settings::reload_settings().expect("reload settings");
let db = Arc::new(Database::memory().expect("init db"));
db.set_config_snippet(
"codex",
Some("disable_response_storage = true\n".to_string()),
)
.expect("set common config snippet");
let service = ProxyService::new(db.clone());
let mut provider = Provider::with_id(
"p1".to_string(),
"P1".to_string(),
json!({
"auth": {
"OPENAI_API_KEY": "token"
},
"config": r#"model_provider = "any"
model = "gpt-5"
[model_providers.any]
base_url = "https://codex.example/v1"
"#
}),
None,
);
provider.meta = Some(ProviderMeta {
common_config_enabled: Some(true),
..Default::default()
});
service
.update_live_backup_from_provider("codex", &provider)
.await
.expect("update live backup");
let backup = db
.get_live_backup("codex")
.await
.expect("get live backup")
.expect("backup exists");
let stored: Value =
serde_json::from_str(&backup.original_config).expect("parse backup json");
let config = stored
.get("config")
.and_then(|v| v.as_str())
.expect("config string");
assert!(
config.contains("disable_response_storage = true"),
"common config should be applied into Codex restore backup"
);
}
#[tokio::test]
#[serial]
async fn update_live_backup_from_provider_preserves_codex_mcp_servers() {
let _home = TempHome::new();
crate::settings::reload_settings().expect("reload settings");
let db = Arc::new(Database::memory().expect("init db"));
let service = ProxyService::new(db.clone());
db.save_live_backup(
"codex",
&serde_json::to_string(&json!({
"auth": {
"OPENAI_API_KEY": "old-token"
},
"config": r#"model_provider = "any"
model = "gpt-4"
[model_providers.any]
base_url = "https://old.example/v1"
[mcp_servers.echo]
command = "npx"
args = ["echo-server"]
"#
}))
.expect("serialize seed backup"),
)
.await
.expect("seed live backup");
let provider = Provider::with_id(
"p2".to_string(),
"P2".to_string(),
json!({
"auth": {
"OPENAI_API_KEY": "new-token"
},
"config": r#"model_provider = "any"
model = "gpt-5"
[model_providers.any]
base_url = "https://new.example/v1"
"#
}),
None,
);
service
.update_live_backup_from_provider("codex", &provider)
.await
.expect("update live backup");
let backup = db
.get_live_backup("codex")
.await
.expect("get live backup")
.expect("backup exists");
let stored: Value =
serde_json::from_str(&backup.original_config).expect("parse backup json");
let config = stored
.get("config")
.and_then(|v| v.as_str())
.expect("config string");
assert!(
config.contains("[mcp_servers.echo]"),
"existing Codex MCP section should survive proxy hot-switch backup update"
);
assert!(
config.contains("https://new.example/v1"),
"provider-specific base_url should still update to the new provider"
);
}
#[tokio::test]
#[serial]
async fn update_live_backup_from_provider_keeps_new_codex_mcp_entries_on_conflict() {
let _home = TempHome::new();
crate::settings::reload_settings().expect("reload settings");
let db = Arc::new(Database::memory().expect("init db"));
let service = ProxyService::new(db.clone());
db.save_live_backup(
"codex",
&serde_json::to_string(&json!({
"auth": {
"OPENAI_API_KEY": "old-token"
},
"config": r#"[mcp_servers.shared]
command = "old-command"
[mcp_servers.legacy]
command = "legacy-command"
"#
}))
.expect("serialize seed backup"),
)
.await
.expect("seed live backup");
let provider = Provider::with_id(
"p2".to_string(),
"P2".to_string(),
json!({
"auth": {
"OPENAI_API_KEY": "new-token"
},
"config": r#"[mcp_servers.shared]
command = "new-command"
[mcp_servers.latest]
command = "latest-command"
"#
}),
None,
);
service
.update_live_backup_from_provider("codex", &provider)
.await
.expect("update live backup");
let backup = db
.get_live_backup("codex")
.await
.expect("get live backup")
.expect("backup exists");
let stored: Value =
serde_json::from_str(&backup.original_config).expect("parse backup json");
let config = stored
.get("config")
.and_then(|v| v.as_str())
.expect("config string");
let parsed: toml::Value = toml::from_str(config).expect("parse merged codex config");
let mcp_servers = parsed
.get("mcp_servers")
.expect("mcp_servers should be present");
assert_eq!(
mcp_servers
.get("shared")
.and_then(|v| v.get("command"))
.and_then(|v| v.as_str()),
Some("new-command"),
"new provider/common-config MCP definition should win on conflict"
);
assert_eq!(
mcp_servers
.get("legacy")
.and_then(|v| v.get("command"))
.and_then(|v| v.as_str()),
Some("legacy-command"),
"backup-only MCP entries should still be preserved"
);
assert_eq!(
mcp_servers
.get("latest")
.and_then(|v| v.get("command"))
.and_then(|v| v.as_str()),
Some("latest-command"),
"new MCP entries should remain in the restore backup"
);
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "CC Switch",
"version": "3.12.0",
"version": "3.12.2",
"identifier": "com.ccswitch.desktop",
"build": {
"frontendDist": "../dist",
+252
View File
@@ -21,6 +21,80 @@ fn sanitize_provider_name(name: &str) -> String {
.to_lowercase()
}
#[test]
fn migrate_legacy_common_config_usage_marks_historical_provider_enabled() {
let _guard = test_mutex().lock().expect("acquire test mutex");
reset_test_fs();
let _home = ensure_test_home();
let mut config = MultiAppConfig::default();
{
let manager = config
.get_manager_mut(&AppType::Claude)
.expect("claude manager");
manager.current = "legacy-provider".to_string();
manager.providers.insert(
"legacy-provider".to_string(),
Provider::with_id(
"legacy-provider".to_string(),
"Legacy".to_string(),
json!({
"includeCoAuthoredBy": false,
"env": {
"ANTHROPIC_API_KEY": "legacy-key"
}
}),
None,
),
);
}
let state = create_test_state_with_config(&config).expect("create test state");
state
.db
.set_config_snippet(
AppType::Claude.as_str(),
Some(r#"{ "includeCoAuthoredBy": false }"#.to_string()),
)
.expect("set common config snippet");
ProviderService::migrate_legacy_common_config_usage_if_needed(&state, AppType::Claude)
.expect("migrate legacy common config");
let providers = state
.db
.get_all_providers(AppType::Claude.as_str())
.expect("get providers after migration");
let provider = providers
.get("legacy-provider")
.expect("legacy provider exists");
assert_eq!(
provider
.meta
.as_ref()
.and_then(|meta| meta.common_config_enabled),
Some(true),
"historical provider should be explicitly marked as using common config"
);
assert!(
provider
.settings_config
.get("includeCoAuthoredBy")
.is_none(),
"common config fields should be stripped from provider storage after migration"
);
assert_eq!(
provider
.settings_config
.get("env")
.and_then(|v| v.get("ANTHROPIC_API_KEY"))
.and_then(|v| v.as_str()),
Some("legacy-key"),
"provider-specific auth should remain untouched"
);
}
#[test]
fn provider_service_switch_codex_updates_live_and_config() {
let _guard = test_mutex().lock().expect("acquire test mutex");
@@ -164,6 +238,184 @@ command = "say"
);
}
#[test]
fn sync_current_provider_for_app_keeps_live_takeover_and_updates_restore_backup() {
let _guard = test_mutex().lock().expect("acquire test mutex");
reset_test_fs();
let _home = ensure_test_home();
let mut config = MultiAppConfig::default();
{
let manager = config
.get_manager_mut(&AppType::Claude)
.expect("claude manager");
manager.current = "current-provider".to_string();
let mut provider = Provider::with_id(
"current-provider".to_string(),
"Current".to_string(),
json!({
"env": {
"ANTHROPIC_AUTH_TOKEN": "real-token",
"ANTHROPIC_BASE_URL": "https://claude.example"
}
}),
None,
);
provider.meta = Some(ProviderMeta {
common_config_enabled: Some(true),
..Default::default()
});
manager
.providers
.insert("current-provider".to_string(), provider);
}
let state = create_test_state_with_config(&config).expect("create test state");
state
.db
.set_config_snippet(
AppType::Claude.as_str(),
Some(r#"{ "includeCoAuthoredBy": false }"#.to_string()),
)
.expect("set common config snippet");
let taken_over_live = json!({
"env": {
"ANTHROPIC_BASE_URL": "http://127.0.0.1:5000",
"ANTHROPIC_AUTH_TOKEN": "PROXY_MANAGED"
}
});
let settings_path = get_claude_settings_path();
std::fs::create_dir_all(settings_path.parent().expect("settings dir")).expect("create dir");
std::fs::write(
&settings_path,
serde_json::to_string_pretty(&taken_over_live).expect("serialize taken over live"),
)
.expect("write taken over live");
futures::executor::block_on(state.db.save_live_backup("claude", "{\"env\":{}}"))
.expect("seed live backup");
let mut proxy_config = futures::executor::block_on(state.db.get_proxy_config_for_app("claude"))
.expect("get proxy config");
proxy_config.enabled = true;
futures::executor::block_on(state.db.update_proxy_config_for_app(proxy_config))
.expect("enable takeover");
ProviderService::sync_current_provider_for_app(&state, AppType::Claude)
.expect("sync current provider should succeed");
let live_after: serde_json::Value =
read_json_file(&settings_path).expect("read live settings after sync");
assert_eq!(
live_after, taken_over_live,
"sync should not overwrite live config while takeover is active"
);
let backup = futures::executor::block_on(state.db.get_live_backup("claude"))
.expect("get live backup")
.expect("backup exists");
let backup_value: serde_json::Value =
serde_json::from_str(&backup.original_config).expect("parse backup value");
assert_eq!(
backup_value
.get("includeCoAuthoredBy")
.and_then(|v| v.as_bool()),
Some(false),
"restore backup should receive the updated effective config"
);
assert_eq!(
backup_value
.get("env")
.and_then(|v| v.get("ANTHROPIC_AUTH_TOKEN"))
.and_then(|v| v.as_str()),
Some("real-token"),
"restore backup should preserve the provider token rather than proxy placeholder"
);
}
#[test]
fn explicitly_cleared_common_snippet_is_not_auto_extracted() {
let _guard = test_mutex().lock().expect("acquire test mutex");
reset_test_fs();
let _home = ensure_test_home();
let state = create_test_state().expect("create test state");
state
.db
.set_config_snippet_cleared(AppType::Claude.as_str(), true)
.expect("mark snippet explicitly cleared");
assert!(
!state
.db
.should_auto_extract_config_snippet(AppType::Claude.as_str())
.expect("check auto-extract eligibility"),
"explicitly cleared snippets should block auto-extraction"
);
state
.db
.set_config_snippet(AppType::Claude.as_str(), Some("{}".to_string()))
.expect("set snippet");
state
.db
.set_config_snippet_cleared(AppType::Claude.as_str(), false)
.expect("clear explicit-empty marker");
assert!(
!state
.db
.should_auto_extract_config_snippet(AppType::Claude.as_str())
.expect("check auto-extract after snippet saved"),
"existing snippets should also block auto-extraction"
);
}
#[test]
fn legacy_common_config_migration_flag_roundtrip() {
let _guard = test_mutex().lock().expect("acquire test mutex");
reset_test_fs();
let _home = ensure_test_home();
let state = create_test_state().expect("create test state");
assert!(
!state
.db
.is_legacy_common_config_migrated()
.expect("initial migration flag"),
"migration flag should default to false"
);
state
.db
.set_legacy_common_config_migrated(true)
.expect("set migration flag");
assert!(
state
.db
.is_legacy_common_config_migrated()
.expect("read migration flag"),
"migration flag should persist once set"
);
state
.db
.set_legacy_common_config_migrated(false)
.expect("clear migration flag");
assert!(
!state
.db
.is_legacy_common_config_migrated()
.expect("read migration flag after clear"),
"migration flag should be removable for tests/debugging"
);
}
#[test]
fn switch_packycode_gemini_updates_security_selected_type() {
let _guard = test_mutex().lock().expect("acquire test mutex");
+2 -2
View File
@@ -996,10 +996,10 @@ function App() {
)}
<div
ref={toolbarRef}
className="flex flex-1 min-w-0 overflow-x-hidden justify-end items-center"
className="flex flex-1 min-w-0 overflow-x-hidden items-center"
>
<div
className="flex shrink-0 items-center gap-1.5"
className="flex shrink-0 items-center gap-1.5 ml-auto"
style={{ WebkitAppRegion: "no-drag" } as any}
>
{currentView === "prompts" && (
+2 -2
View File
@@ -77,10 +77,10 @@ const ToolsPanel: React.FC = () => {
const handleSave = async () => {
try {
const { allow, deny, ...other } = config;
const { profile, allow, deny, ...other } = config;
const newConfig: OpenClawToolsConfig = {
...other,
profile: config.profile,
profile,
allow: allowList.map((item) => item.value).filter((s) => s.trim()),
deny: denyList.map((item) => item.value).filter((s) => s.trim()),
};
@@ -17,6 +17,7 @@ import { UniversalProviderPanel } from "@/components/universal";
import { providerPresets } from "@/config/claudeProviderPresets";
import { codexProviderPresets } from "@/config/codexProviderPresets";
import { geminiProviderPresets } from "@/config/geminiProviderPresets";
import { extractCodexBaseUrl } from "@/utils/providerConfigUtils";
import type { OpenClawSuggestedDefaults } from "@/config/openclawProviderPresets";
import type { UniversalProviderPreset } from "@/config/universalProviderPresets";
@@ -179,11 +180,9 @@ export function AddProviderDialog({
} else if (appId === "codex") {
const config = parsedConfig.config as string | undefined;
if (config) {
const baseUrlMatch = config.match(
/base_url\s*=\s*["']([^"']+)["']/,
);
if (baseUrlMatch?.[1]) {
addUrl(baseUrlMatch[1]);
const extractedBaseUrl = extractCodexBaseUrl(config);
if (extractedBaseUrl) {
addUrl(extractedBaseUrl);
}
}
} else if (appId === "gemini") {
+4 -3
View File
@@ -13,6 +13,7 @@ import { ProviderIcon } from "@/components/ProviderIcon";
import UsageFooter from "@/components/UsageFooter";
import { ProviderHealthBadge } from "@/components/providers/ProviderHealthBadge";
import { FailoverPriorityBadge } from "@/components/providers/FailoverPriorityBadge";
import { extractCodexBaseUrl } from "@/utils/providerConfigUtils";
import { useProviderHealth } from "@/lib/query/failover";
import { useUsageQuery } from "@/lib/query/queries";
@@ -76,9 +77,9 @@ const extractApiUrl = (provider: Provider, fallbackText: string) => {
const baseUrl = (config as Record<string, any>)?.config;
if (typeof baseUrl === "string" && baseUrl.includes("base_url")) {
const match = baseUrl.match(/base_url\s*=\s*['"]([^'"]+)['"]/);
if (match?.[1]) {
return match[1];
const extractedBaseUrl = extractCodexBaseUrl(baseUrl);
if (extractedBaseUrl) {
return extractedBaseUrl;
}
}
}
@@ -1,17 +1,22 @@
import { Download, Users } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import type { AppId } from "@/lib/api/types";
interface ProviderEmptyStateProps {
appId: AppId;
onCreate?: () => void;
onImport?: () => void;
}
export function ProviderEmptyState({
appId,
onCreate,
onImport,
}: ProviderEmptyStateProps) {
const { t } = useTranslation();
const showSnippetHint =
appId === "claude" || appId === "codex" || appId === "gemini";
return (
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed border-border p-10 text-center">
@@ -19,9 +24,14 @@ export function ProviderEmptyState({
<Users className="h-7 w-7 text-muted-foreground" />
</div>
<h3 className="text-lg font-semibold">{t("provider.noProviders")}</h3>
<p className="mt-2 max-w-sm text-sm text-muted-foreground">
<p className="mt-2 max-w-lg text-sm text-muted-foreground">
{t("provider.noProvidersDescription")}
</p>
{showSnippetHint && (
<p className="mt-1 max-w-lg text-sm text-muted-foreground">
{t("provider.noProvidersDescriptionSnippet")}
</p>
)}
<div className="mt-6 flex flex-col gap-2">
{onImport && (
<Button onClick={onImport}>
@@ -298,6 +298,7 @@ export function ProviderList({
if (sortedProviders.length === 0) {
return (
<ProviderEmptyState
appId={appId}
onCreate={onCreate}
onImport={() => importMutation.mutate()}
/>
@@ -9,7 +9,7 @@ interface CodexCommonConfigModalProps {
isOpen: boolean;
onClose: () => void;
value: string;
onChange: (value: string) => void;
onSave: (value: string) => boolean;
error?: string;
onExtract?: () => void;
isExtracting?: boolean;
@@ -23,13 +23,14 @@ export const CodexCommonConfigModal: React.FC<CodexCommonConfigModalProps> = ({
isOpen,
onClose,
value,
onChange,
onSave,
error,
onExtract,
isExtracting,
}) => {
const { t } = useTranslation();
const [isDarkMode, setIsDarkMode] = useState(false);
const [draftValue, setDraftValue] = useState(value);
useEffect(() => {
setIsDarkMode(document.documentElement.classList.contains("dark"));
@@ -46,11 +47,28 @@ export const CodexCommonConfigModal: React.FC<CodexCommonConfigModalProps> = ({
return () => observer.disconnect();
}, []);
useEffect(() => {
if (isOpen) {
setDraftValue(value);
}
}, [isOpen, value]);
const handleClose = () => {
setDraftValue(value);
onClose();
};
const handleSave = () => {
if (onSave(draftValue)) {
onClose();
}
};
return (
<FullScreenPanel
isOpen={isOpen}
title={t("codexConfig.editCommonConfigTitle")}
onClose={onClose}
onClose={handleClose}
footer={
<>
{onExtract && (
@@ -71,10 +89,10 @@ export const CodexCommonConfigModal: React.FC<CodexCommonConfigModalProps> = ({
})}
</Button>
)}
<Button type="button" variant="outline" onClick={onClose}>
<Button type="button" variant="outline" onClick={handleClose}>
{t("common.cancel")}
</Button>
<Button type="button" onClick={onClose} className="gap-2">
<Button type="button" onClick={handleSave} className="gap-2">
<Save className="w-4 h-4" />
{t("common.save")}
</Button>
@@ -87,8 +105,8 @@ export const CodexCommonConfigModal: React.FC<CodexCommonConfigModalProps> = ({
</p>
<JsonEditor
value={value}
onChange={onChange}
value={draftValue}
onChange={setDraftValue}
placeholder={`# Common Codex config
# Add your common TOML configuration here`}
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import React, { useState } from "react";
import { CodexAuthSection, CodexConfigSection } from "./CodexConfigSections";
import { CodexCommonConfigModal } from "./CodexCommonConfigModal";
@@ -19,7 +19,9 @@ interface CodexConfigEditorProps {
commonConfigSnippet: string;
onCommonConfigSnippetChange: (value: string) => void;
onCommonConfigSnippetChange: (value: string) => boolean;
onCommonConfigErrorClear: () => void;
commonConfigError: string;
@@ -42,6 +44,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
onCommonConfigToggle,
commonConfigSnippet,
onCommonConfigSnippetChange,
onCommonConfigErrorClear,
commonConfigError,
authError,
configError,
@@ -50,12 +53,10 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
}) => {
const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false);
// Auto-open common config modal if there's an error
useEffect(() => {
if (commonConfigError && !isCommonConfigModalOpen) {
setIsCommonConfigModalOpen(true);
}
}, [commonConfigError, isCommonConfigModalOpen]);
const handleCloseCommonConfigModal = () => {
onCommonConfigErrorClear();
setIsCommonConfigModalOpen(false);
};
return (
<div className="space-y-6">
@@ -81,9 +82,9 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
{/* Common Config Modal */}
<CodexCommonConfigModal
isOpen={isCommonConfigModalOpen}
onClose={() => setIsCommonConfigModalOpen(false)}
onClose={handleCloseCommonConfigModal}
value={commonConfigSnippet}
onChange={onCommonConfigSnippetChange}
onSave={onCommonConfigSnippetChange}
error={commonConfigError}
onExtract={onExtract}
isExtracting={isExtracting}
@@ -9,7 +9,7 @@ interface GeminiCommonConfigModalProps {
isOpen: boolean;
onClose: () => void;
value: string;
onChange: (value: string) => void;
onSave: (value: string) => boolean;
error?: string;
onExtract?: () => void;
isExtracting?: boolean;
@@ -21,9 +21,10 @@ interface GeminiCommonConfigModalProps {
*/
export const GeminiCommonConfigModal: React.FC<
GeminiCommonConfigModalProps
> = ({ isOpen, onClose, value, onChange, error, onExtract, isExtracting }) => {
> = ({ isOpen, onClose, value, onSave, error, onExtract, isExtracting }) => {
const { t } = useTranslation();
const [isDarkMode, setIsDarkMode] = useState(false);
const [draftValue, setDraftValue] = useState(value);
useEffect(() => {
setIsDarkMode(document.documentElement.classList.contains("dark"));
@@ -40,13 +41,30 @@ export const GeminiCommonConfigModal: React.FC<
return () => observer.disconnect();
}, []);
useEffect(() => {
if (isOpen) {
setDraftValue(value);
}
}, [isOpen, value]);
const handleClose = () => {
setDraftValue(value);
onClose();
};
const handleSave = () => {
if (onSave(draftValue)) {
onClose();
}
};
return (
<FullScreenPanel
isOpen={isOpen}
title={t("geminiConfig.editCommonConfigTitle", {
defaultValue: "编辑 Gemini 通用配置片段",
})}
onClose={onClose}
onClose={handleClose}
footer={
<>
{onExtract && (
@@ -67,10 +85,10 @@ export const GeminiCommonConfigModal: React.FC<
})}
</Button>
)}
<Button type="button" variant="outline" onClick={onClose}>
<Button type="button" variant="outline" onClick={handleClose}>
{t("common.cancel")}
</Button>
<Button type="button" onClick={onClose} className="gap-2">
<Button type="button" onClick={handleSave} className="gap-2">
<Save className="w-4 h-4" />
{t("common.save")}
</Button>
@@ -86,8 +104,8 @@ export const GeminiCommonConfigModal: React.FC<
</p>
<JsonEditor
value={value}
onChange={onChange}
value={draftValue}
onChange={setDraftValue}
placeholder={`{
"GEMINI_MODEL": "gemini-3-pro-preview"
}`}
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import React, { useState } from "react";
import { GeminiEnvSection, GeminiConfigSection } from "./GeminiConfigSections";
import { GeminiCommonConfigModal } from "./GeminiCommonConfigModal";
@@ -11,7 +11,8 @@ interface GeminiConfigEditorProps {
useCommonConfig: boolean;
onCommonConfigToggle: (checked: boolean) => void;
commonConfigSnippet: string;
onCommonConfigSnippetChange: (value: string) => void;
onCommonConfigSnippetChange: (value: string) => boolean;
onCommonConfigErrorClear: () => void;
commonConfigError: string;
envError: string;
configError: string;
@@ -29,6 +30,7 @@ const GeminiConfigEditor: React.FC<GeminiConfigEditorProps> = ({
onCommonConfigToggle,
commonConfigSnippet,
onCommonConfigSnippetChange,
onCommonConfigErrorClear,
commonConfigError,
envError,
configError,
@@ -37,12 +39,10 @@ const GeminiConfigEditor: React.FC<GeminiConfigEditorProps> = ({
}) => {
const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false);
// Auto-open common config modal if there's an error
useEffect(() => {
if (commonConfigError && !isCommonConfigModalOpen) {
setIsCommonConfigModalOpen(true);
}
}, [commonConfigError, isCommonConfigModalOpen]);
const handleCloseCommonConfigModal = () => {
onCommonConfigErrorClear();
setIsCommonConfigModalOpen(false);
};
return (
<div className="space-y-6">
@@ -68,9 +68,9 @@ const GeminiConfigEditor: React.FC<GeminiConfigEditorProps> = ({
{/* Common Config Modal */}
<GeminiCommonConfigModal
isOpen={isCommonConfigModalOpen}
onClose={() => setIsCommonConfigModalOpen(false)}
onClose={handleCloseCommonConfigModal}
value={commonConfigSnippet}
onChange={onCommonConfigSnippetChange}
onSave={onCommonConfigSnippetChange}
error={commonConfigError}
onExtract={onExtract}
isExtracting={isExtracting}
@@ -17,6 +17,7 @@ import {
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { Plus, Trash2, ChevronDown, ChevronRight } from "lucide-react";
import { Checkbox } from "@/components/ui/checkbox";
import { ApiKeySection } from "./shared";
import { openclawApiProtocols } from "@/config/openclawProviderPresets";
import type { ProviderCategory, OpenClawModel } from "@/types";
@@ -101,6 +102,7 @@ export function OpenClawFormFields({
contextWindow: undefined,
maxTokens: undefined,
cost: undefined,
input: ["text"],
},
]);
};
@@ -339,7 +341,68 @@ export function OpenClawFormFields({
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="space-y-3 pt-2">
{/* Context Window, Max Tokens and Reasoning row */}
{/* Reasoning, Input Types row */}
<div className="flex items-center gap-2">
<div className="flex-1 space-y-1">
<label className="text-xs text-muted-foreground">
{t("openclaw.reasoning", {
defaultValue: "推理模式",
})}
</label>
<div className="flex items-center h-9 gap-2">
<Switch
checked={model.reasoning ?? false}
onCheckedChange={(checked) =>
handleModelChange(index, "reasoning", checked)
}
/>
<span className="text-xs text-muted-foreground">
{model.reasoning
? t("openclaw.reasoningOn", {
defaultValue: "启用",
})
: t("openclaw.reasoningOff", {
defaultValue: "关闭",
})}
</span>
</div>
</div>
<div className="flex-1 space-y-1">
<label className="text-xs text-muted-foreground">
{t("openclaw.inputTypes", {
defaultValue: "输入类型",
})}
</label>
{/* "text" is checked by default but can be unchecked
some models genuinely don't support text input, and
OpenClaw works fine with an empty or image-only array. */}
<div className="flex items-center gap-4 h-9">
{(["text", "image"] as const).map((type) => (
<label
key={type}
className="flex items-center gap-1.5 cursor-pointer select-none"
>
<Checkbox
checked={(model.input ?? ["text"]).includes(
type,
)}
onCheckedChange={(checked) => {
const current = model.input ?? ["text"];
const next = checked
? [...new Set([...current, type])]
: current.filter((v) => v !== type);
handleModelChange(index, "input", next);
}}
/>
<span className="text-xs">{type}</span>
</label>
))}
</div>
</div>
<div className="flex-1" />
</div>
{/* Context Window and Max Tokens row */}
<div className="flex items-center gap-2">
<div className="flex-1 space-y-1">
<label className="text-xs text-muted-foreground">
@@ -383,30 +446,7 @@ export function OpenClawFormFields({
placeholder="32000"
/>
</div>
<div className="flex-1 space-y-1">
<label className="text-xs text-muted-foreground">
{t("openclaw.reasoning", {
defaultValue: "推理模式",
})}
</label>
<div className="flex items-center h-9 gap-2">
<Switch
checked={model.reasoning ?? false}
onCheckedChange={(checked) =>
handleModelChange(index, "reasoning", checked)
}
/>
<span className="text-xs text-muted-foreground">
{model.reasoning
? t("openclaw.reasoningOn", {
defaultValue: "启用",
})
: t("openclaw.reasoningOff", {
defaultValue: "关闭",
})}
</span>
</div>
</div>
<div className="flex-1" />
</div>
{/* Cost row */}
@@ -447,6 +447,7 @@ export function ProviderForm({
handleCommonConfigSnippetChange: handleCodexCommonConfigSnippetChange,
isExtracting: isCodexExtracting,
handleExtract: handleCodexExtract,
clearCommonConfigError: clearCodexCommonConfigError,
} = useCodexCommonConfig({
codexConfig,
onConfigChange: handleCodexConfigChange,
@@ -530,6 +531,7 @@ export function ProviderForm({
handleCommonConfigSnippetChange: handleGeminiCommonConfigSnippetChange,
isExtracting: isGeminiExtracting,
handleExtract: handleGeminiExtract,
clearCommonConfigError: clearGeminiCommonConfigError,
} = useGeminiCommonConfig({
envValue: geminiEnv,
onEnvChange: handleGeminiEnvChange,
@@ -1033,7 +1035,7 @@ export function ProviderForm({
resetCodexConfig(auth, config);
form.reset({
name: preset.name,
name: preset.nameKey ? t(preset.nameKey) : preset.name,
websiteUrl: preset.websiteUrl ?? "",
settingsConfig: JSON.stringify({ auth, config }, null, 2),
icon: preset.icon ?? "",
@@ -1050,7 +1052,7 @@ export function ProviderForm({
resetGeminiConfig(env, config);
form.reset({
name: preset.name,
name: preset.nameKey ? t(preset.nameKey) : preset.name,
websiteUrl: preset.websiteUrl ?? "",
settingsConfig: JSON.stringify(preset.settingsConfig, null, 2),
icon: preset.icon ?? "",
@@ -1078,7 +1080,7 @@ export function ProviderForm({
opencodeForm.resetOpencodeState(config);
form.reset({
name: preset.name,
name: preset.nameKey ? t(preset.nameKey) : preset.name,
websiteUrl: preset.websiteUrl ?? "",
settingsConfig: JSON.stringify(config, null, 2),
icon: preset.icon ?? "",
@@ -1105,7 +1107,7 @@ export function ProviderForm({
// Update form fields
form.reset({
name: preset.name,
name: preset.nameKey ? t(preset.nameKey) : preset.name,
websiteUrl: preset.websiteUrl ?? "",
settingsConfig: JSON.stringify(config, null, 2),
icon: preset.icon ?? "",
@@ -1129,7 +1131,7 @@ export function ProviderForm({
setLocalApiKeyField(preset.apiKeyField ?? "ANTHROPIC_AUTH_TOKEN");
form.reset({
name: preset.name,
name: preset.nameKey ? t(preset.nameKey) : preset.name,
websiteUrl: preset.websiteUrl ?? "",
settingsConfig: JSON.stringify(config, null, 2),
icon: preset.icon ?? "",
@@ -1467,6 +1469,7 @@ export function ProviderForm({
onCommonConfigToggle={handleCodexCommonConfigToggle}
commonConfigSnippet={codexCommonConfigSnippet}
onCommonConfigSnippetChange={handleCodexCommonConfigSnippetChange}
onCommonConfigErrorClear={clearCodexCommonConfigError}
commonConfigError={codexCommonConfigError}
authError={codexAuthError}
configError={codexConfigError}
@@ -1488,6 +1491,7 @@ export function ProviderForm({
onCommonConfigSnippetChange={
handleGeminiCommonConfigSnippetChange
}
onCommonConfigErrorClear={clearGeminiCommonConfigError}
commonConfigError={geminiCommonConfigError}
envError={envError}
configError={geminiConfigError}
@@ -159,7 +159,9 @@ export function ProviderPresetSelector({
}
>
{renderPresetIcon(entry.preset)}
{entry.preset.name}
{entry.preset.nameKey
? t(entry.preset.nameKey)
: entry.preset.name}
{isPartner && (
<span className="absolute -top-1 -right-1 flex items-center gap-0.5 rounded-full bg-gradient-to-r from-amber-500 to-yellow-500 px-1.5 py-0.5 text-[10px] font-bold text-white shadow-md">
<Star className="h-2.5 w-2.5 fill-current" />
@@ -1,10 +1,12 @@
import { useState, useEffect, useCallback, useRef } from "react";
import { useTranslation } from "react-i18next";
import { parse as parseToml } from "smol-toml";
import {
updateTomlCommonConfigSnippet,
hasTomlCommonConfigSnippet,
} from "@/utils/providerConfigUtils";
import { configApi } from "@/lib/api";
import { normalizeTomlText } from "@/utils/textNormalization";
const LEGACY_STORAGE_KEY = "cc-switch:codex-common-config-snippet";
const DEFAULT_CODEX_COMMON_CONFIG_SNIPPET = `# Common Codex config
@@ -53,6 +55,30 @@ export function useCodexCommonConfig({
hasInitializedEditMode.current = false;
}, [selectedPresetId, initialEnabled]);
const parseCommonConfigSnippet = useCallback((snippetString: string) => {
const trimmed = snippetString.trim();
if (!trimmed) {
return {
hasContent: false,
};
}
try {
const parsed = parseToml(normalizeTomlText(snippetString)) as Record<
string,
unknown
>;
return {
hasContent: Object.keys(parsed).length > 0,
};
} catch (error) {
return {
hasContent: false,
error: error instanceof Error ? error.message : String(error),
};
}
}, []);
// 初始化:从 config.json 加载,支持从 localStorage 迁移
useEffect(() => {
let mounted = true;
@@ -107,36 +133,59 @@ export function useCodexCommonConfig({
// 初始化时检查通用配置片段(编辑模式)
useEffect(() => {
if (initialData?.settingsConfig && !isLoading) {
const config =
typeof initialData.settingsConfig.config === "string"
? initialData.settingsConfig.config
: "";
const inferredHasCommon = hasTomlCommonConfigSnippet(
config,
commonConfigSnippet,
);
const hasCommon = initialEnabled ?? inferredHasCommon;
setUseCommonConfig(hasCommon);
if (hasCommon && !inferredHasCommon && !hasInitializedEditMode.current) {
hasInitializedEditMode.current = true;
const { updatedConfig, error } = updateTomlCommonConfigSnippet(
codexConfig,
commonConfigSnippet,
true,
);
if (!error) {
isUpdatingFromCommonConfig.current = true;
onConfigChange(updatedConfig);
setTimeout(() => {
isUpdatingFromCommonConfig.current = false;
}, 0);
}
} else {
hasInitializedEditMode.current = true;
}
if (
!initialData?.settingsConfig ||
isLoading ||
hasInitializedEditMode.current
) {
return;
}
hasInitializedEditMode.current = true;
const parsedSnippet = parseCommonConfigSnippet(commonConfigSnippet);
if (parsedSnippet.error) {
if (commonConfigSnippet.trim()) {
setCommonConfigError(parsedSnippet.error);
}
setUseCommonConfig(false);
return;
}
const config =
typeof initialData.settingsConfig.config === "string"
? initialData.settingsConfig.config
: "";
const inferredHasCommon = hasTomlCommonConfigSnippet(
config,
commonConfigSnippet,
);
const hasCommon = initialEnabled ?? inferredHasCommon;
if (hasCommon && !inferredHasCommon) {
const { updatedConfig, error } = updateTomlCommonConfigSnippet(
codexConfig,
commonConfigSnippet,
true,
);
if (error) {
setCommonConfigError(error);
setUseCommonConfig(false);
return;
}
setCommonConfigError("");
setUseCommonConfig(true);
isUpdatingFromCommonConfig.current = true;
onConfigChange(updatedConfig);
setTimeout(() => {
isUpdatingFromCommonConfig.current = false;
}, 0);
return;
}
setCommonConfigError("");
setUseCommonConfig(hasCommon);
}, [
codexConfig,
commonConfigSnippet,
@@ -144,49 +193,75 @@ export function useCodexCommonConfig({
initialEnabled,
isLoading,
onConfigChange,
parseCommonConfigSnippet,
]);
// 新建模式:如果通用配置片段存在且有效,默认启用
useEffect(() => {
// 仅新建模式、加载完成、尚未初始化过
if (!initialData && !isLoading && !hasInitializedNewMode.current) {
hasInitializedNewMode.current = true;
// 检查 TOML 片段是否有实质内容(不只是注释和空行)
const lines = commonConfigSnippet.split("\n");
const hasContent = lines.some((line) => {
const trimmed = line.trim();
return trimmed && !trimmed.startsWith("#");
});
if (hasContent) {
setUseCommonConfig(true);
// 合并通用配置到当前配置
const { updatedConfig, error } = updateTomlCommonConfigSnippet(
codexConfig,
commonConfigSnippet,
true,
);
if (!error) {
isUpdatingFromCommonConfig.current = true;
onConfigChange(updatedConfig);
setTimeout(() => {
isUpdatingFromCommonConfig.current = false;
}, 0);
}
}
if (initialData || isLoading || hasInitializedNewMode.current) {
return;
}
hasInitializedNewMode.current = true;
const parsedSnippet = parseCommonConfigSnippet(commonConfigSnippet);
if (parsedSnippet.error) {
if (commonConfigSnippet.trim()) {
setCommonConfigError(parsedSnippet.error);
}
setUseCommonConfig(false);
return;
}
if (!parsedSnippet.hasContent) {
return;
}
const { updatedConfig, error } = updateTomlCommonConfigSnippet(
codexConfig,
commonConfigSnippet,
true,
);
if (error) {
setCommonConfigError(error);
setUseCommonConfig(false);
return;
}
setCommonConfigError("");
setUseCommonConfig(true);
isUpdatingFromCommonConfig.current = true;
onConfigChange(updatedConfig);
setTimeout(() => {
isUpdatingFromCommonConfig.current = false;
}, 0);
}, [
initialData,
commonConfigSnippet,
isLoading,
codexConfig,
onConfigChange,
parseCommonConfigSnippet,
]);
// 处理通用配置开关
const handleCommonConfigToggle = useCallback(
(checked: boolean) => {
const parsedSnippet = parseCommonConfigSnippet(commonConfigSnippet);
if (parsedSnippet.error) {
setCommonConfigError(parsedSnippet.error);
setUseCommonConfig(false);
return;
}
if (!parsedSnippet.hasContent) {
setCommonConfigError(
t("codexConfig.noCommonConfigToApply", {
defaultValue: "通用配置片段为空或没有可写入的内容",
}),
);
setUseCommonConfig(false);
return;
}
const { updatedConfig, error: snippetError } =
updateTomlCommonConfigSnippet(
codexConfig,
@@ -210,18 +285,45 @@ export function useCodexCommonConfig({
isUpdatingFromCommonConfig.current = false;
}, 0);
},
[codexConfig, commonConfigSnippet, onConfigChange],
[
codexConfig,
commonConfigSnippet,
onConfigChange,
parseCommonConfigSnippet,
t,
],
);
// 处理通用配置片段变化
const handleCommonConfigSnippetChange = useCallback(
(value: string) => {
(value: string): boolean => {
const previousSnippet = commonConfigSnippet;
setCommonConfigSnippetState(value);
if (!value.trim()) {
setCommonConfigError("");
// 保存到 config.json(清空)
if (useCommonConfig) {
const previousParsed = parseCommonConfigSnippet(previousSnippet);
let updatedConfig = codexConfig;
if (!previousParsed.error && previousParsed.hasContent) {
const removeResult = updateTomlCommonConfigSnippet(
codexConfig,
previousSnippet,
false,
);
if (removeResult.error) {
setCommonConfigError(removeResult.error);
return false;
}
updatedConfig = removeResult.updatedConfig;
}
onConfigChange(updatedConfig);
setUseCommonConfig(false);
}
setCommonConfigSnippetState("");
configApi
.setCommonConfigSnippet("codex", "")
.catch((error: unknown) => {
@@ -230,51 +332,42 @@ export function useCodexCommonConfig({
t("codexConfig.saveFailed", { error: String(error) }),
);
});
return true;
}
if (useCommonConfig) {
const { updatedConfig } = updateTomlCommonConfigSnippet(
const parsedNextSnippet = parseCommonConfigSnippet(value);
if (parsedNextSnippet.error) {
setCommonConfigError(parsedNextSnippet.error);
return false;
}
// 若当前启用通用配置,需要替换为最新片段
if (useCommonConfig) {
let nextConfig = codexConfig;
const previousParsed = parseCommonConfigSnippet(previousSnippet);
if (!previousParsed.error && previousParsed.hasContent) {
const removeResult = updateTomlCommonConfigSnippet(
codexConfig,
previousSnippet,
false,
);
onConfigChange(updatedConfig);
setUseCommonConfig(false);
if (removeResult.error) {
setCommonConfigError(removeResult.error);
return false;
}
nextConfig = removeResult.updatedConfig;
}
return;
}
// TOML 格式校验较为复杂,暂时不做校验,直接清空错误
setCommonConfigError("");
// 保存到 config.json
configApi
.setCommonConfigSnippet("codex", value)
.catch((error: unknown) => {
console.error("保存 Codex 通用配置失败:", error);
setCommonConfigError(
t("codexConfig.saveFailed", { error: String(error) }),
);
});
// 若当前启用通用配置,需要替换为最新片段
if (useCommonConfig) {
const removeResult = updateTomlCommonConfigSnippet(
codexConfig,
previousSnippet,
false,
);
if (removeResult.error) {
setCommonConfigError(removeResult.error);
return;
}
const addResult = updateTomlCommonConfigSnippet(
removeResult.updatedConfig,
nextConfig,
value,
true,
);
if (addResult.error) {
setCommonConfigError(addResult.error);
return;
return false;
}
// 标记正在通过通用配置更新,避免触发状态检查
@@ -285,8 +378,28 @@ export function useCodexCommonConfig({
isUpdatingFromCommonConfig.current = false;
}, 0);
}
setCommonConfigError("");
setCommonConfigSnippetState(value);
configApi
.setCommonConfigSnippet("codex", value)
.catch((error: unknown) => {
console.error("保存 Codex 通用配置失败:", error);
setCommonConfigError(
t("codexConfig.saveFailed", { error: String(error) }),
);
});
return true;
},
[commonConfigSnippet, codexConfig, useCommonConfig, onConfigChange],
[
commonConfigSnippet,
codexConfig,
onConfigChange,
parseCommonConfigSnippet,
t,
useCommonConfig,
],
);
// 当配置变化时检查是否包含通用配置(但避免在通过通用配置更新时检查)
@@ -294,12 +407,17 @@ export function useCodexCommonConfig({
if (isUpdatingFromCommonConfig.current || isLoading) {
return;
}
const parsedSnippet = parseCommonConfigSnippet(commonConfigSnippet);
if (parsedSnippet.error) {
setUseCommonConfig(false);
return;
}
const hasCommon = hasTomlCommonConfigSnippet(
codexConfig,
commonConfigSnippet,
);
setUseCommonConfig(hasCommon);
}, [codexConfig, commonConfigSnippet, isLoading]);
}, [codexConfig, commonConfigSnippet, isLoading, parseCommonConfigSnippet]);
// 从编辑器当前内容提取通用配置片段
const handleExtract = useCallback(async () => {
@@ -333,6 +451,10 @@ export function useCodexCommonConfig({
}
}, [codexConfig, t]);
const clearCommonConfigError = useCallback(() => {
setCommonConfigError("");
}, []);
return {
useCommonConfig,
commonConfigSnippet,
@@ -342,5 +464,6 @@ export function useCodexCommonConfig({
handleCommonConfigToggle,
handleCommonConfigSnippetChange,
handleExtract,
clearCommonConfigError,
};
}
@@ -216,43 +216,55 @@ export function useGeminiCommonConfig({
// 初始化时检查通用配置片段(编辑模式)
useEffect(() => {
if (initialData?.settingsConfig && !isLoading) {
try {
const env =
isPlainObject(initialData.settingsConfig.env) &&
Object.keys(initialData.settingsConfig.env).length > 0
? (initialData.settingsConfig.env as Record<string, string>)
: {};
const parsed = parseSnippetEnv(commonConfigSnippet);
if (parsed.error) return;
const inferredHasCommon = hasEnvCommonConfigSnippet(
env,
parsed.env as Record<string, string>,
);
const hasCommon = initialEnabled ?? inferredHasCommon;
setUseCommonConfig(hasCommon);
if (
!initialData?.settingsConfig ||
isLoading ||
hasInitializedEditMode.current
) {
return;
}
if (
hasCommon &&
!inferredHasCommon &&
!hasInitializedEditMode.current
) {
hasInitializedEditMode.current = true;
const currentEnv = envStringToObj(envValue);
const merged = applySnippetToEnv(currentEnv, parsed.env);
const nextEnvString = envObjToString(merged);
hasInitializedEditMode.current = true;
isUpdatingFromCommonConfig.current = true;
onEnvChange(nextEnvString);
setTimeout(() => {
isUpdatingFromCommonConfig.current = false;
}, 0);
} else {
hasInitializedEditMode.current = true;
try {
const env =
isPlainObject(initialData.settingsConfig.env) &&
Object.keys(initialData.settingsConfig.env).length > 0
? (initialData.settingsConfig.env as Record<string, string>)
: {};
const parsed = parseSnippetEnv(commonConfigSnippet);
if (parsed.error) {
if (commonConfigSnippet.trim()) {
setCommonConfigError(parsed.error);
}
} catch {
// ignore parse error
setUseCommonConfig(false);
return;
}
const inferredHasCommon = hasEnvCommonConfigSnippet(
env,
parsed.env as Record<string, string>,
);
const hasCommon = initialEnabled ?? inferredHasCommon;
if (hasCommon && !inferredHasCommon) {
const currentEnv = envStringToObj(envValue);
const merged = applySnippetToEnv(currentEnv, parsed.env);
const nextEnvString = envObjToString(merged);
setCommonConfigError("");
setUseCommonConfig(true);
isUpdatingFromCommonConfig.current = true;
onEnvChange(nextEnvString);
setTimeout(() => {
isUpdatingFromCommonConfig.current = false;
}, 0);
return;
}
setCommonConfigError("");
setUseCommonConfig(hasCommon);
} catch {
// ignore parse error
}
}, [
applySnippetToEnv,
@@ -270,26 +282,34 @@ export function useGeminiCommonConfig({
// 新建模式:如果通用配置片段存在且有效,默认启用
useEffect(() => {
// 仅新建模式、加载完成、尚未初始化过
if (!initialData && !isLoading && !hasInitializedNewMode.current) {
hasInitializedNewMode.current = true;
const parsed = parseSnippetEnv(commonConfigSnippet);
if (parsed.error) return;
const hasContent = Object.keys(parsed.env).length > 0;
if (!hasContent) return;
setUseCommonConfig(true);
const currentEnv = envStringToObj(envValue);
const merged = applySnippetToEnv(currentEnv, parsed.env);
const nextEnvString = envObjToString(merged);
isUpdatingFromCommonConfig.current = true;
onEnvChange(nextEnvString);
setTimeout(() => {
isUpdatingFromCommonConfig.current = false;
}, 0);
if (initialData || isLoading || hasInitializedNewMode.current) {
return;
}
hasInitializedNewMode.current = true;
const parsed = parseSnippetEnv(commonConfigSnippet);
if (parsed.error) {
if (commonConfigSnippet.trim()) {
setCommonConfigError(parsed.error);
}
setUseCommonConfig(false);
return;
}
const hasContent = Object.keys(parsed.env).length > 0;
if (!hasContent) return;
setCommonConfigError("");
setUseCommonConfig(true);
const currentEnv = envStringToObj(envValue);
const merged = applySnippetToEnv(currentEnv, parsed.env);
const nextEnvString = envObjToString(merged);
isUpdatingFromCommonConfig.current = true;
onEnvChange(nextEnvString);
setTimeout(() => {
isUpdatingFromCommonConfig.current = false;
}, 0);
}, [
initialData,
isLoading,
@@ -346,13 +366,29 @@ export function useGeminiCommonConfig({
// 处理通用配置片段变化
const handleCommonConfigSnippetChange = useCallback(
(value: string) => {
(value: string): boolean => {
const previousSnippet = commonConfigSnippet;
setCommonConfigSnippetState(value);
if (!value.trim()) {
setCommonConfigError("");
// 保存到 config.json(清空)
if (useCommonConfig) {
const parsedPrevious = parseSnippetEnv(previousSnippet);
if (
!parsedPrevious.error &&
Object.keys(parsedPrevious.env).length > 0
) {
const currentEnv = envStringToObj(envValue);
const updatedEnv = removeSnippetFromEnv(
currentEnv,
parsedPrevious.env,
);
onEnvChange(envObjToString(updatedEnv));
}
setUseCommonConfig(false);
}
setCommonConfigSnippetState("");
configApi
.setCommonConfigSnippet("gemini", "")
.catch((error: unknown) => {
@@ -361,36 +397,16 @@ export function useGeminiCommonConfig({
t("geminiConfig.saveFailed", { error: String(error) }),
);
});
if (useCommonConfig) {
const parsed = parseSnippetEnv(previousSnippet);
if (!parsed.error && Object.keys(parsed.env).length > 0) {
const currentEnv = envStringToObj(envValue);
const updatedEnv = removeSnippetFromEnv(currentEnv, parsed.env);
onEnvChange(envObjToString(updatedEnv));
}
setUseCommonConfig(false);
}
return;
return true;
}
// 校验 JSON 格式
const parsed = parseSnippetEnv(value);
if (parsed.error) {
setCommonConfigError(parsed.error);
return;
return false;
}
setCommonConfigError("");
configApi
.setCommonConfigSnippet("gemini", value)
.catch((error: unknown) => {
console.error("保存 Gemini 通用配置失败:", error);
setCommonConfigError(
t("geminiConfig.saveFailed", { error: String(error) }),
);
});
// 若当前启用通用配置,需要替换为最新片段
if (useCommonConfig) {
const prevParsed = parseSnippetEnv(previousSnippet);
@@ -413,6 +429,19 @@ export function useGeminiCommonConfig({
isUpdatingFromCommonConfig.current = false;
}, 0);
}
setCommonConfigError("");
setCommonConfigSnippetState(value);
configApi
.setCommonConfigSnippet("gemini", value)
.catch((error: unknown) => {
console.error("保存 Gemini 通用配置失败:", error);
setCommonConfigError(
t("geminiConfig.saveFailed", { error: String(error) }),
);
});
return true;
},
[
applySnippetToEnv,
@@ -487,6 +516,10 @@ export function useGeminiCommonConfig({
}
}, [envStringToObj, envValue, parseSnippetEnv, t]);
const clearCommonConfigError = useCallback(() => {
setCommonConfigError("");
}, []);
return {
useCommonConfig,
commonConfigSnippet,
@@ -496,5 +529,6 @@ export function useGeminiCommonConfig({
handleCommonConfigToggle,
handleCommonConfigSnippetChange,
handleExtract,
clearCommonConfigError,
};
}
@@ -1,5 +1,5 @@
import { useState, useCallback, useMemo } from "react";
import type { OpenClawModel } from "@/types";
import type { OpenClawModel, OpenClawProviderConfig } from "@/types";
import type { AppId } from "@/lib/api";
import { useProvidersQuery } from "@/lib/query/queries";
import { OPENCLAW_DEFAULT_CONFIG } from "../helpers/opencodeFormUtils";
@@ -31,13 +31,7 @@ export interface OpenclawFormState {
handleOpenclawApiChange: (api: string) => void;
handleOpenclawModelsChange: (models: OpenClawModel[]) => void;
handleOpenclawUserAgentChange: (enabled: boolean) => void;
resetOpenclawState: (config?: {
baseUrl?: string;
apiKey?: string;
api?: string;
models?: OpenClawModel[];
headers?: Record<string, string>;
}) => void;
resetOpenclawState: (config?: OpenClawProviderConfig) => void;
}
function parseOpenclawField<T>(
@@ -177,24 +171,15 @@ export function useOpenclawFormState({
[updateOpenclawConfig],
);
const resetOpenclawState = useCallback(
(config?: {
baseUrl?: string;
apiKey?: string;
api?: string;
models?: OpenClawModel[];
headers?: Record<string, string>;
}) => {
setOpenclawProviderKey("");
setOpenclawBaseUrl(config?.baseUrl || "");
setOpenclawApiKey(config?.apiKey || "");
setOpenclawApi(config?.api || "openai-completions");
setOpenclawModels(config?.models || []);
const ua = config?.headers ? "User-Agent" in config.headers : false;
setOpenclawUserAgent(ua);
},
[],
);
const resetOpenclawState = useCallback((config?: OpenClawProviderConfig) => {
setOpenclawProviderKey("");
setOpenclawBaseUrl(config?.baseUrl || "");
setOpenclawApiKey(config?.apiKey || "");
setOpenclawApi(config?.api || "openai-completions");
setOpenclawModels(config?.models || []);
const ua = config?.headers ? "User-Agent" in config.headers : false;
setOpenclawUserAgent(ua);
}, []);
return {
openclawProviderKey,
@@ -3,6 +3,7 @@ import type { AppId } from "@/lib/api";
import type { ProviderPreset } from "@/config/claudeProviderPresets";
import type { CodexProviderPreset } from "@/config/codexProviderPresets";
import type { ProviderMeta, EndpointCandidate } from "@/types";
import { extractCodexBaseUrl } from "@/utils/providerConfigUtils";
type PresetEntry = {
id: string;
@@ -128,10 +129,9 @@ export function useSpeedTestEndpoints({
}
| undefined;
const configStr = initialCodexConfig?.config ?? "";
// 从 TOML 中提取 base_url
const match = /base_url\s*=\s*["']([^"']+)["']/i.exec(configStr);
if (match?.[1]) {
add(match[1]);
const extractedBaseUrl = extractCodexBaseUrl(configStr);
if (extractedBaseUrl) {
add(extractedBaseUrl);
}
// 3. 预设中的 endpointCandidates
@@ -141,11 +141,9 @@ export function useSpeedTestEndpoints({
const preset = entry.preset as CodexProviderPreset;
// 添加预设自己的 baseUrl
const presetConfig = preset.config || "";
const presetMatch = /base_url\s*=\s*["']([^"']+)["']/i.exec(
presetConfig,
);
if (presetMatch?.[1]) {
add(presetMatch[1]);
const presetBaseUrl = extractCodexBaseUrl(presetConfig);
if (presetBaseUrl) {
add(presetBaseUrl);
}
// 添加预设的候选端点
if (preset.endpointCandidates) {
+46 -8
View File
@@ -24,6 +24,7 @@ export interface PresetTheme {
export interface ProviderPreset {
name: string;
nameKey?: string; // i18n key for localized display name
websiteUrl: string;
// 新增:第三方/聚合等可单独配置获取 API Key 的链接
apiKeyUrl?: string;
@@ -178,6 +179,26 @@ export const providerPresets: ProviderPreset[] = [
icon: "kimi",
iconColor: "#6366F1",
},
{
name: "StepFun",
websiteUrl: "https://platform.stepfun.ai",
apiKeyUrl: "https://platform.stepfun.ai/interface-key",
settingsConfig: {
env: {
ANTHROPIC_BASE_URL: "https://api.stepfun.ai/v1",
ANTHROPIC_AUTH_TOKEN: "",
ANTHROPIC_MODEL: "step-3.5-flash",
ANTHROPIC_DEFAULT_HAIKU_MODEL: "step-3.5-flash",
ANTHROPIC_DEFAULT_SONNET_MODEL: "step-3.5-flash",
ANTHROPIC_DEFAULT_OPUS_MODEL: "step-3.5-flash",
},
},
category: "cn_official",
endpointCandidates: ["https://api.stepfun.ai/v1"],
apiFormat: "openai_chat",
icon: "stepfun",
iconColor: "#005AFF",
},
{
name: "ModelScope",
websiteUrl: "https://modelscope.cn",
@@ -348,7 +369,7 @@ export const providerPresets: ProviderPreset[] = [
{
name: "SiliconFlow",
websiteUrl: "https://siliconflow.cn",
apiKeyUrl: "https://cloud.siliconflow.cn/me/account/ak",
apiKeyUrl: "https://cloud.siliconflow.cn/i/drGuwc9k",
settingsConfig: {
env: {
ANTHROPIC_BASE_URL: "https://api.siliconflow.cn",
@@ -368,7 +389,7 @@ export const providerPresets: ProviderPreset[] = [
{
name: "SiliconFlow en",
websiteUrl: "https://siliconflow.com",
apiKeyUrl: "https://cloud.siliconflow.com/account/ak",
apiKeyUrl: "https://cloud.siliconflow.cn/i/drGuwc9k",
settingsConfig: {
env: {
ANTHROPIC_BASE_URL: "https://api.siliconflow.com",
@@ -553,7 +574,8 @@ export const providerPresets: ProviderPreset[] = [
iconColor: "#000000",
},
{
name: "Ucloud",
name: "Compshare",
nameKey: "providerForm.presets.ucloud",
websiteUrl: "https://www.compshare.cn",
apiKeyUrl:
"https://www.compshare.cn/coding-plan?ytag=GPU_YY_YX_git_cc-switch",
@@ -573,7 +595,7 @@ export const providerPresets: ProviderPreset[] = [
{
name: "Micu",
websiteUrl: "https://www.openclaudecode.cn",
apiKeyUrl: "https://www.openclaudecode.cn",
apiKeyUrl: "https://www.openclaudecode.cn/register?aff=aOYQ",
settingsConfig: {
env: {
ANTHROPIC_BASE_URL: "https://www.openclaudecode.cn",
@@ -589,21 +611,37 @@ export const providerPresets: ProviderPreset[] = [
},
{
name: "X-Code API",
websiteUrl: "https://www.x-code.cn",
apiKeyUrl: "https://www.x-code.cn",
websiteUrl: "https://x-code.cc",
apiKeyUrl: "https://x-code.cc",
settingsConfig: {
env: {
ANTHROPIC_BASE_URL: "https://www.x-code.cn",
ANTHROPIC_BASE_URL: "https://x-code.cc",
ANTHROPIC_AUTH_TOKEN: "",
},
},
endpointCandidates: ["https://www.x-code.cn"],
endpointCandidates: ["https://x-code.cc"],
category: "third_party",
isPartner: true, // 合作伙伴
partnerPromotionKey: "x-code", // 促销信息 i18n key
icon: "x-code",
iconColor: "#000000",
},
{
name: "CTok.ai",
websiteUrl: "https://ctok.ai",
apiKeyUrl: "https://ctok.ai",
settingsConfig: {
env: {
ANTHROPIC_BASE_URL: "https://api.ctok.ai",
ANTHROPIC_AUTH_TOKEN: "",
},
},
category: "third_party",
isPartner: true, // 合作伙伴
partnerPromotionKey: "ctok", // 促销信息 i18n key
icon: "ctok",
iconColor: "#000000",
},
{
name: "OpenRouter",
websiteUrl: "https://openrouter.ai",
+26 -7
View File
@@ -6,6 +6,7 @@ import type { PresetTheme } from "./claudeProviderPresets";
export interface CodexProviderPreset {
name: string;
nameKey?: string; // i18n key for localized display name
websiteUrl: string;
// 第三方供应商可提供单独的获取 API Key 链接
apiKeyUrl?: string;
@@ -281,13 +282,14 @@ requires_openai_auth = true`,
iconColor: "#000000",
},
{
name: "Ucloud",
name: "Compshare",
nameKey: "providerForm.presets.ucloud",
websiteUrl: "https://www.compshare.cn",
apiKeyUrl:
"https://www.compshare.cn/coding-plan?ytag=GPU_YY_YX_git_cc-switch",
auth: generateThirdPartyAuth(""),
config: generateThirdPartyConfig(
"ucloud",
"compshare",
"https://api.modelverse.cn/v1",
"gpt-5.4",
),
@@ -301,7 +303,7 @@ requires_openai_auth = true`,
{
name: "Micu",
websiteUrl: "https://www.openclaudecode.cn",
apiKeyUrl: "https://www.openclaudecode.cn/v1",
apiKeyUrl: "https://www.openclaudecode.cn/register?aff=aOYQ",
auth: generateThirdPartyAuth(""),
config: generateThirdPartyConfig(
"micu",
@@ -317,21 +319,38 @@ requires_openai_auth = true`,
},
{
name: "X-Code API",
websiteUrl: "https://www.x-code.cn",
apiKeyUrl: "https://www.x-code.cn",
websiteUrl: "https://x-code.cc",
apiKeyUrl: "https://x-code.cc",
auth: generateThirdPartyAuth(""),
config: generateThirdPartyConfig(
"x-code",
"https://www.x-code.cn/v1",
"https://x-code.cc/v1",
"gpt-5.4",
),
endpointCandidates: ["https://www.x-code.cn/v1"],
endpointCandidates: ["https://x-code.cc/v1"],
category: "third_party",
isPartner: true, // 合作伙伴
partnerPromotionKey: "x-code", // 促销信息 i18n key
icon: "x-code",
iconColor: "#000000",
},
{
name: "CTok.ai",
websiteUrl: "https://ctok.ai",
apiKeyUrl: "https://ctok.ai",
auth: generateThirdPartyAuth(""),
config: generateThirdPartyConfig(
"ctok",
"https://api.ctok.ai/v1",
"gpt-5.4",
),
endpointCandidates: ["https://api.ctok.ai/v1"],
category: "third_party",
isPartner: true, // 合作伙伴
partnerPromotionKey: "ctok", // 促销信息 i18n key
icon: "ctok",
iconColor: "#000000",
},
{
name: "OpenRouter",
websiteUrl: "https://openrouter.ai",
+39 -18
View File
@@ -14,6 +14,7 @@ export interface GeminiPresetTheme {
export interface GeminiProviderPreset {
name: string;
nameKey?: string; // i18n key for localized display name
websiteUrl: string;
apiKeyUrl?: string;
settingsConfig: object;
@@ -56,11 +57,11 @@ export const geminiProviderPresets: GeminiProviderPreset[] = [
settingsConfig: {
env: {
GOOGLE_GEMINI_BASE_URL: "https://www.packyapi.com",
GEMINI_MODEL: "gemini-3-pro",
GEMINI_MODEL: "gemini-3.1-pro",
},
},
baseURL: "https://www.packyapi.com",
model: "gemini-3-pro",
model: "gemini-3.1-pro",
description: "PackyCode",
category: "third_party",
isPartner: true,
@@ -78,11 +79,11 @@ export const geminiProviderPresets: GeminiProviderPreset[] = [
settingsConfig: {
env: {
GOOGLE_GEMINI_BASE_URL: "https://api.cubence.com",
GEMINI_MODEL: "gemini-3-pro",
GEMINI_MODEL: "gemini-3.1-pro",
},
},
baseURL: "https://api.cubence.com",
model: "gemini-3-pro",
model: "gemini-3.1-pro",
description: "Cubence",
category: "third_party",
isPartner: true,
@@ -103,11 +104,11 @@ export const geminiProviderPresets: GeminiProviderPreset[] = [
settingsConfig: {
env: {
GOOGLE_GEMINI_BASE_URL: "https://api.aigocode.com",
GEMINI_MODEL: "gemini-3-pro",
GEMINI_MODEL: "gemini-3.1-pro",
},
},
baseURL: "https://api.aigocode.com",
model: "gemini-3-pro",
model: "gemini-3.1-pro",
description: "AIGoCode",
category: "third_party",
isPartner: true,
@@ -123,11 +124,11 @@ export const geminiProviderPresets: GeminiProviderPreset[] = [
settingsConfig: {
env: {
GOOGLE_GEMINI_BASE_URL: "https://api.aicodemirror.com/api/gemini",
GEMINI_MODEL: "gemini-3-pro",
GEMINI_MODEL: "gemini-3.1-pro",
},
},
baseURL: "https://api.aicodemirror.com/api/gemini",
model: "gemini-3-pro",
model: "gemini-3.1-pro",
description: "AICodeMirror",
category: "third_party",
isPartner: true,
@@ -146,11 +147,11 @@ export const geminiProviderPresets: GeminiProviderPreset[] = [
settingsConfig: {
env: {
GOOGLE_GEMINI_BASE_URL: "https://api.aicoding.sh",
GEMINI_MODEL: "gemini-3-pro",
GEMINI_MODEL: "gemini-3.1-pro",
},
},
baseURL: "https://api.aicoding.sh",
model: "gemini-3-pro",
model: "gemini-3.1-pro",
description: "AICoding",
category: "third_party",
isPartner: true,
@@ -166,11 +167,11 @@ export const geminiProviderPresets: GeminiProviderPreset[] = [
settingsConfig: {
env: {
GOOGLE_GEMINI_BASE_URL: "https://crazyrouter.com",
GEMINI_MODEL: "gemini-3-pro",
GEMINI_MODEL: "gemini-3.1-pro",
},
},
baseURL: "https://crazyrouter.com",
model: "gemini-3-pro",
model: "gemini-3.1-pro",
description: "CrazyRouter",
category: "third_party",
isPartner: true,
@@ -186,11 +187,11 @@ export const geminiProviderPresets: GeminiProviderPreset[] = [
settingsConfig: {
env: {
GOOGLE_GEMINI_BASE_URL: "https://node-hk.sssaicode.com/api",
GEMINI_MODEL: "gemini-3-pro",
GEMINI_MODEL: "gemini-3.1-pro",
},
},
baseURL: "https://node-hk.sssaicode.com/api",
model: "gemini-3-pro",
model: "gemini-3.1-pro",
description: "SSSAiCode",
category: "third_party",
isPartner: true,
@@ -203,6 +204,26 @@ export const geminiProviderPresets: GeminiProviderPreset[] = [
icon: "sssaicode",
iconColor: "#000000",
},
{
name: "CTok.ai",
websiteUrl: "https://ctok.ai",
apiKeyUrl: "https://ctok.ai",
settingsConfig: {
env: {
GOOGLE_GEMINI_BASE_URL: "https://api.ctok.ai/v1beta",
GEMINI_MODEL: "gemini-3.1-pro",
},
},
baseURL: "https://api.ctok.ai/v1beta",
model: "gemini-3.1-pro",
description: "CTok",
category: "third_party",
isPartner: true,
partnerPromotionKey: "ctok",
endpointCandidates: ["https://api.ctok.ai/v1beta"],
icon: "ctok",
iconColor: "#000000",
},
{
name: "OpenRouter",
websiteUrl: "https://openrouter.ai",
@@ -210,11 +231,11 @@ export const geminiProviderPresets: GeminiProviderPreset[] = [
settingsConfig: {
env: {
GOOGLE_GEMINI_BASE_URL: "https://openrouter.ai/api",
GEMINI_MODEL: "gemini-3-pro-preview",
GEMINI_MODEL: "gemini-3.1-pro",
},
},
baseURL: "https://openrouter.ai/api",
model: "gemini-3-pro",
model: "gemini-3.1-pro",
description: "OpenRouter",
category: "aggregator",
icon: "openrouter",
@@ -226,10 +247,10 @@ export const geminiProviderPresets: GeminiProviderPreset[] = [
settingsConfig: {
env: {
GOOGLE_GEMINI_BASE_URL: "",
GEMINI_MODEL: "gemini-3-pro",
GEMINI_MODEL: "gemini-3.1-pro",
},
},
model: "gemini-3-pro",
model: "gemini-3.1-pro",
description: "自定义 Gemini API 端点",
category: "custom",
},
+2
View File
@@ -15,6 +15,8 @@ const iconMappings = {
aliyun: { icon: "alibaba", iconColor: "#FF6A00" },
kimi: { icon: "kimi", iconColor: "#6366F1" },
moonshot: { icon: "moonshot", iconColor: "#6366F1" },
stepfun: { icon: "stepfun", iconColor: "#005AFF" },
step: { icon: "stepfun", iconColor: "#005AFF" },
baidu: { icon: "baidu", iconColor: "#2932E1" },
tencent: { icon: "tencent", iconColor: "#00A4FF" },
hunyuan: { icon: "hunyuan", iconColor: "#00A4FF" },
+84 -6
View File
@@ -19,6 +19,7 @@ export interface OpenClawSuggestedDefaults {
export interface OpenClawProviderPreset {
name: string;
nameKey?: string; // i18n key for localized display name
websiteUrl: string;
apiKeyUrl?: string;
/** OpenClaw settings_config structure */
@@ -292,6 +293,43 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
modelCatalog: { "kimi-coding/kimi-for-coding": { alias: "Kimi" } },
},
},
{
name: "StepFun",
websiteUrl: "https://platform.stepfun.ai",
apiKeyUrl: "https://platform.stepfun.ai/interface-key",
settingsConfig: {
baseUrl: "https://api.stepfun.ai/v1",
apiKey: "",
api: "openai-completions",
models: [
{
id: "step-3.5-flash",
name: "Step 3.5 Flash",
contextWindow: 262144,
},
],
},
category: "cn_official",
icon: "stepfun",
iconColor: "#005AFF",
templateValues: {
baseUrl: {
label: "Base URL",
placeholder: "https://api.stepfun.ai/v1",
defaultValue: "https://api.stepfun.ai/v1",
editorValue: "",
},
apiKey: {
label: "API Key",
placeholder: "step-...",
editorValue: "",
},
},
suggestedDefaults: {
model: { primary: "stepfun/step-3.5-flash" },
modelCatalog: { "stepfun/step-3.5-flash": { alias: "StepFun" } },
},
},
{
name: "MiniMax",
websiteUrl: "https://platform.minimaxi.com",
@@ -421,6 +459,7 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
baseUrl: "https://api.longcat.chat/v1",
apiKey: "",
api: "openai-completions",
authHeader: true,
models: [
{
id: "LongCat-Flash-Chat",
@@ -721,7 +760,7 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
{
name: "SiliconFlow",
websiteUrl: "https://siliconflow.cn",
apiKeyUrl: "https://cloud.siliconflow.cn/me/account/ak",
apiKeyUrl: "https://cloud.siliconflow.cn/i/drGuwc9k",
settingsConfig: {
baseUrl: "https://api.siliconflow.cn/v1",
apiKey: "",
@@ -757,7 +796,7 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
{
name: "SiliconFlow en",
websiteUrl: "https://siliconflow.com",
apiKeyUrl: "https://cloud.siliconflow.com/account/ak",
apiKeyUrl: "https://cloud.siliconflow.cn/i/drGuwc9k",
settingsConfig: {
baseUrl: "https://api.siliconflow.com/v1",
apiKey: "",
@@ -1226,7 +1265,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
},
},
{
name: "Ucloud",
name: "Compshare",
nameKey: "providerForm.presets.ucloud",
websiteUrl: "https://www.compshare.cn",
apiKeyUrl:
"https://www.compshare.cn/coding-plan?ytag=GPU_YY_YX_git_cc-switch",
@@ -1257,17 +1297,17 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
},
suggestedDefaults: {
model: {
primary: "ucloud/claude-opus-4-6",
primary: "compshare/claude-opus-4-6",
},
modelCatalog: {
"ucloud/claude-opus-4-6": { alias: "Opus" },
"compshare/claude-opus-4-6": { alias: "Opus" },
},
},
},
{
name: "Micu",
websiteUrl: "https://www.openclaudecode.cn",
apiKeyUrl: "https://www.openclaudecode.cn",
apiKeyUrl: "https://www.openclaudecode.cn/register?aff=aOYQ",
settingsConfig: {
baseUrl: "https://www.openclaudecode.cn",
apiKey: "",
@@ -1302,6 +1342,44 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
},
},
},
{
name: "CTok.ai",
websiteUrl: "https://ctok.ai",
apiKeyUrl: "https://ctok.ai",
settingsConfig: {
baseUrl: "https://api.ctok.ai",
apiKey: "",
api: "anthropic-messages",
models: [
{
id: "claude-opus-4-6",
name: "Claude Opus 4.6",
contextWindow: 200000,
cost: { input: 5, output: 25 },
},
],
},
category: "third_party",
isPartner: true,
partnerPromotionKey: "ctok",
icon: "ctok",
iconColor: "#000000",
templateValues: {
apiKey: {
label: "API Key",
placeholder: "",
editorValue: "",
},
},
suggestedDefaults: {
model: {
primary: "ctok/claude-opus-4-6",
},
modelCatalog: {
"ctok/claude-opus-4-6": { alias: "Opus" },
},
},
},
// ========== Cloud Providers ==========
{
name: "AWS Bedrock",
+72 -5
View File
@@ -3,6 +3,7 @@ import type { PresetTheme, TemplateValueConfig } from "./claudeProviderPresets";
export interface OpenCodeProviderPreset {
name: string;
nameKey?: string; // i18n key for localized display name
websiteUrl: string;
apiKeyUrl?: string;
settingsConfig: OpenCodeProviderConfig;
@@ -18,7 +19,7 @@ export interface OpenCodeProviderPreset {
}
export const opencodeNpmPackages = [
{ value: "@ai-sdk/openai", label: "OpenAI" },
{ value: "@ai-sdk/openai", label: "OpenAI Responses" },
{ value: "@ai-sdk/openai-compatible", label: "OpenAI Compatible" },
{ value: "@ai-sdk/anthropic", label: "Anthropic" },
{ value: "@ai-sdk/amazon-bedrock", label: "Amazon Bedrock" },
@@ -61,6 +62,11 @@ export const OPENCODE_PRESET_MODEL_VARIANTS: Record<
outputLimit: 262144,
modalities: { input: ["text", "image", "video"], output: ["text"] },
},
{
id: "step-3.5-flash",
name: "Step 3.5 Flash",
contextLimit: 262144,
},
],
"@ai-sdk/google": [
{
@@ -469,6 +475,38 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
},
},
},
{
name: "StepFun",
websiteUrl: "https://platform.stepfun.ai",
apiKeyUrl: "https://platform.stepfun.ai/interface-key",
settingsConfig: {
npm: "@ai-sdk/openai-compatible",
name: "StepFun",
options: {
baseURL: "https://api.stepfun.ai/v1",
apiKey: "",
},
models: {
"step-3.5-flash": { name: "Step 3.5 Flash" },
},
},
category: "cn_official",
icon: "stepfun",
iconColor: "#005AFF",
templateValues: {
baseURL: {
label: "Base URL",
placeholder: "https://api.stepfun.ai/v1",
defaultValue: "https://api.stepfun.ai/v1",
editorValue: "",
},
apiKey: {
label: "API Key",
placeholder: "step-...",
editorValue: "",
},
},
},
{
name: "ModelScope",
websiteUrl: "https://modelscope.cn",
@@ -1082,7 +1120,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
{
name: "Micu",
websiteUrl: "https://www.openclaudecode.cn",
apiKeyUrl: "https://www.openclaudecode.cn/v1",
apiKeyUrl: "https://www.openclaudecode.cn/register?aff=aOYQ",
settingsConfig: {
npm: "@ai-sdk/anthropic",
name: "Micu",
@@ -1110,13 +1148,13 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
},
{
name: "X-Code API",
websiteUrl: "https://www.x-code.cn",
apiKeyUrl: "https://www.x-code.cn",
websiteUrl: "https://x-code.cc",
apiKeyUrl: "https://x-code.cc",
settingsConfig: {
npm: "@ai-sdk/anthropic",
name: "X-Code API",
options: {
baseURL: "https://www.x-code.cn/v1",
baseURL: "https://x-code.cc/v1",
apiKey: "",
},
models: {
@@ -1137,6 +1175,35 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
},
},
},
{
name: "CTok.ai",
websiteUrl: "https://ctok.ai",
apiKeyUrl: "https://ctok.ai",
settingsConfig: {
npm: "@ai-sdk/anthropic",
name: "CTok",
options: {
baseURL: "https://api.ctok.ai/v1",
apiKey: "",
},
models: {
"claude-opus-4-6": { name: "Claude Opus 4.6" },
"claude-sonnet-4-6": { name: "Claude Sonnet 4.6" },
},
},
category: "third_party",
isPartner: true,
partnerPromotionKey: "ctok",
icon: "ctok",
iconColor: "#000000",
templateValues: {
apiKey: {
label: "API Key",
placeholder: "",
editorValue: "",
},
},
},
{
name: "AWS Bedrock",
websiteUrl: "https://aws.amazon.com/bedrock/",
+2
View File
@@ -43,6 +43,8 @@ export function useProxyStatus() {
onSuccess: (info) => {
toast.success(
t("proxy.server.started", {
address: info.address,
port: info.port,
defaultValue: `代理服务已启动 - ${info.address}:${info.port}`,
}),
{ closeButton: true },
+8 -53
View File
@@ -1,10 +1,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useCallback, useMemo } from "react";
import FlexSearch from "flexsearch";
import type { SessionMeta } from "@/types";
// FlexSearch Index 类型
type FlexSearchIndex = InstanceType<typeof FlexSearch.Index>;
interface UseSessionSearchOptions {
sessions: SessionMeta[];
providerFilter: string;
@@ -12,7 +9,6 @@ interface UseSessionSearchOptions {
interface UseSessionSearchResult {
search: (query: string) => SessionMeta[];
isIndexing: boolean;
}
/**
@@ -23,27 +19,14 @@ export function useSessionSearch({
sessions,
providerFilter,
}: UseSessionSearchOptions): UseSessionSearchResult {
const [isIndexing, setIsIndexing] = useState(false);
// 会话元数据索引
const indexRef = useRef<FlexSearchIndex | null>(null);
// 索引 ID 到 session 的映射
const sessionByIdxRef = useRef<SessionMeta[]>([]);
// 初始化索引
useEffect(() => {
setIsIndexing(true);
// 创建索引实例
const index = useMemo(() => {
// 使用 forward tokenizer 支持中文前缀搜索
const index = new FlexSearch.Index({
const nextIndex = new FlexSearch.Index({
tokenize: "forward",
resolution: 9,
});
// 索引所有会话
sessions.forEach((session, idx) => {
// 索引会话元数据
const metaContent = [
session.sessionId,
session.title,
@@ -54,13 +37,10 @@ export function useSessionSearch({
.filter(Boolean)
.join(" ");
index.add(idx, metaContent);
nextIndex.add(idx, metaContent);
});
indexRef.current = index;
sessionByIdxRef.current = sessions;
setIsIndexing(false);
return nextIndex;
}, [sessions]);
// 搜索函数
@@ -83,37 +63,12 @@ export function useSessionSearch({
});
}
const index = indexRef.current;
if (!index) {
// 索引未就绪,使用简单搜索
return filtered
.filter((session) => {
const haystack = [
session.sessionId,
session.title,
session.summary,
session.projectDir,
session.sourcePath,
]
.filter(Boolean)
.join(" ")
.toLowerCase();
return haystack.includes(needle);
})
.sort((a, b) => {
const aTs = a.lastActiveAt ?? a.createdAt ?? 0;
const bTs = b.lastActiveAt ?? b.createdAt ?? 0;
return bTs - aTs;
});
}
// 使用 FlexSearch 搜索
const results = index.search(needle, { limit: 100 }) as number[];
// 转换为 session 并过滤
const matchedSessions = results
.map((idx) => sessionByIdxRef.current[idx])
.map((idx) => sessions[idx])
.filter(
(session) =>
session &&
@@ -127,8 +82,8 @@ export function useSessionSearch({
return bTs - aTs;
});
},
[sessions, providerFilter],
[index, providerFilter, sessions],
);
return useMemo(() => ({ search, isIndexing }), [search, isIndexing]);
return useMemo(() => ({ search }), [search]);
}
+7 -7
View File
@@ -26,8 +26,8 @@ export function useStreamCheck(appId: AppId) {
if (result.status === "operational") {
toast.success(
t("streamCheck.operational", {
name: providerName,
time: result.responseTimeMs,
providerName: providerName,
responseTimeMs: result.responseTimeMs,
defaultValue: `${providerName} 运行正常 (${result.responseTimeMs}ms)`,
}),
{ closeButton: true },
@@ -38,8 +38,8 @@ export function useStreamCheck(appId: AppId) {
} else if (result.status === "degraded") {
toast.warning(
t("streamCheck.degraded", {
name: providerName,
time: result.responseTimeMs,
providerName: providerName,
responseTimeMs: result.responseTimeMs,
defaultValue: `${providerName} 响应较慢 (${result.responseTimeMs}ms)`,
}),
);
@@ -49,8 +49,8 @@ export function useStreamCheck(appId: AppId) {
} else {
toast.error(
t("streamCheck.failed", {
name: providerName,
error: result.message,
providerName: providerName,
message: result.message,
defaultValue: `${providerName} 检查失败: ${result.message}`,
}),
);
@@ -60,7 +60,7 @@ export function useStreamCheck(appId: AppId) {
} catch (e) {
toast.error(
t("streamCheck.error", {
name: providerName,
providerName: providerName,
error: String(e),
defaultValue: `${providerName} 检查出错: ${String(e)}`,
}),
+9 -9
View File
@@ -80,7 +80,8 @@
"tabProvider": "Provider",
"tabUniversal": "Universal",
"noProviders": "No providers added yet",
"noProvidersDescription": "Import your current live config below, or manually add a new provider",
"noProvidersDescription": "If you already have a config, click \"Import Current Config\" — all data will be safely saved in a default provider",
"noProvidersDescriptionSnippet": "Settings other than API key and endpoint (e.g. plugins) will be saved to a common config snippet for sharing across providers",
"importCurrent": "Import Current Config",
"importCurrentDescription": "Import current live config as default provider",
"currentlyUsing": "Currently Using",
@@ -706,9 +707,13 @@
"crazyrouter": "CrazyRouter offers an exclusive bonus for CC Switch users — 30% extra credit on your first top-up!",
"sssaicode": "SSAI Code offers an exclusive bonus for CC Switch users — $10 extra credit on every top-up!",
"siliconflow": "SiliconFlow is an official partner of CC Switch",
"ucloud": "UCloud offers an exclusive bonus for CC Switch users — register via this link to get ¥5 platform trial credit!",
"ucloud": "Compshare offers an exclusive bonus for CC Switch users — register via this link to get ¥5 platform trial credit!",
"micu": "Micu is an official partner of CC Switch",
"x-code": "XCodeAPI offers a special bonus for CC Switch users — register via this link and get 10% extra credit on your first order (contact admin to claim)"
"x-code": "XCodeAPI offers a special bonus for CC Switch users — register via this link and get 10% extra credit on your first order (contact admin to claim)",
"ctok": "Join the CTok community on the official website and subscribe to a plan."
},
"presets": {
"ucloud": "Compshare"
},
"parameterConfig": "Parameter Config - {{name}} *",
"mainModel": "Main Model (optional)",
@@ -1336,6 +1341,7 @@
"reasoning": "Reasoning Mode",
"reasoningOn": "Enabled",
"reasoningOff": "Disabled",
"inputTypes": "Input Types",
"inputCost": "Input Cost ($/M tokens)",
"outputCost": "Output Cost ($/M tokens)",
"advancedOptions": "Advanced Options",
@@ -1372,12 +1378,6 @@
"unsupportedProfileTitle": "Unsupported tools profile detected",
"unsupportedProfileDescription": "The current tools.profile value '{{value}}' is not in the supported OpenClaw list. It will be preserved until you choose a new value.",
"unsupportedProfileLabel": "unsupported",
"profiles": {
"default": "Default",
"strict": "Strict",
"permissive": "Permissive",
"custom": "Custom"
},
"allowList": "Allow List",
"denyList": "Deny List",
"patternPlaceholder": "Tool name or pattern",
+9 -9
View File
@@ -80,7 +80,8 @@
"tabProvider": "プロバイダー",
"tabUniversal": "統一プロバイダー",
"noProviders": "まだプロバイダーがありません",
"noProvidersDescription": "下の「現在の設定をインポート」ボタンで既存の設定を取り込むか、新しいプロバイダーを手動で追加してください",
"noProvidersDescription": "既存の設定がある場合は「現在の設定をインポート」をクリックしてください。すべてのデータが default プロバイダーに安全に保存されます",
"noProvidersDescriptionSnippet": "API キーとリクエスト URL 以外のデータ(プラグインなど)は共通設定スニペットに保存され、プロバイダー間で共有できます",
"importCurrent": "現在の設定をインポート",
"importCurrentDescription": "現在使用中の設定をデフォルトプロバイダーとしてインポート",
"currentlyUsing": "現在使用中",
@@ -706,9 +707,13 @@
"crazyrouter": "CrazyRouter は CC Switch ユーザー向けに特別ボーナスを提供しています。初回チャージで 30% の追加クレジットがもらえます!",
"sssaicode": "SSAI Code は CC Switch ユーザー向けに特別ボーナスを提供しています。チャージごとに $10 の追加クレジットがもらえます!",
"siliconflow": "SiliconFlow は CC Switch の公式パートナーです",
"ucloud": "UCloud は CC Switch ユーザー向けに特別ボーナスを提供しています。このリンクから登録すると、5元のプラットフォーム体験クレジットがもらえます!",
"ucloud": "Compshare は CC Switch ユーザー向けに特別ボーナスを提供しています。このリンクから登録すると、5元のプラットフォーム体験クレジットがもらえます!",
"micu": "Micu は CC Switch の公式パートナーです",
"x-code": "XCodeAPI は CC Switch ユーザー向けに特別ボーナスを提供しています。このリンクから登録すると、初回注文で 10% の追加クレジットがもらえます(管理者に連絡して受け取り)"
"x-code": "XCodeAPI は CC Switch ユーザー向けに特別ボーナスを提供しています。このリンクから登録すると、初回注文で 10% の追加クレジットがもらえます(管理者に連絡して受け取り)",
"ctok": "公式サイトで CTok コミュニティに参加し、プランを購読してください。"
},
"presets": {
"ucloud": "Compshare"
},
"parameterConfig": "パラメーター設定 - {{name}} *",
"mainModel": "メインモデル(任意)",
@@ -1336,6 +1341,7 @@
"reasoning": "推論モード",
"reasoningOn": "有効",
"reasoningOff": "無効",
"inputTypes": "入力タイプ",
"inputCost": "入力コスト ($/M トークン)",
"outputCost": "出力コスト ($/M トークン)",
"advancedOptions": "詳細オプション",
@@ -1372,12 +1378,6 @@
"unsupportedProfileTitle": "未対応のツールプロファイルを検出しました",
"unsupportedProfileDescription": "現在の tools.profile の値 '{{value}}' は OpenClaw の対応リストにありません。新しい値を選択するまでこの値を保持します。",
"unsupportedProfileLabel": "未対応",
"profiles": {
"default": "デフォルト",
"strict": "厳格",
"permissive": "寛容",
"custom": "カスタム"
},
"allowList": "許可リスト",
"denyList": "拒否リスト",
"patternPlaceholder": "ツール名またはパターン",
+8 -8
View File
@@ -80,7 +80,8 @@
"tabProvider": "供应商",
"tabUniversal": "统一供应商",
"noProviders": "还没有添加任何供应商",
"noProvidersDescription": "点击下方的\"导入当前配置\"按钮导入已有配置,或手动添加新的供应商",
"noProvidersDescription": "如果你已有配置,请点击\"导入当前配置\",所有数据将安全保存在 default 供应商",
"noProvidersDescriptionSnippet": "除 Key 和请求地址外的数据(如插件配置)会被保存到通用配置片段,用于在不同供应商之间共享",
"importCurrent": "导入当前配置",
"importCurrentDescription": "将当前正在使用的配置导入为默认供应商",
"currentlyUsing": "当前使用",
@@ -708,7 +709,11 @@
"siliconflow": "硅基流动是 CC Switch 的官方合作伙伴",
"ucloud": "优云智算为CC Switch 的用户提供了特殊优惠,通过此链接注册,可以获得五元平台体验金!",
"micu": "Micu 是 CC Switch 的官方合作伙伴",
"x-code": "XCodeAPI 为CC Switch 的用户提供特别福利,使用此链接注册后首单加赠10%的额度(联系站长领取)"
"x-code": "XCodeAPI 为CC Switch 的用户提供特别福利,使用此链接注册后首单加赠10%的额度(联系站长领取)",
"ctok": "官网加入CTok社群,订阅套餐。"
},
"presets": {
"ucloud": "优云智算"
},
"parameterConfig": "参数配置 - {{name}} *",
"mainModel": "主模型 (可选)",
@@ -1336,6 +1341,7 @@
"reasoning": "推理模式",
"reasoningOn": "启用",
"reasoningOff": "关闭",
"inputTypes": "输入类型",
"inputCost": "输入价格 ($/M tokens)",
"outputCost": "输出价格 ($/M tokens)",
"advancedOptions": "高级选项",
@@ -1372,12 +1378,6 @@
"unsupportedProfileTitle": "检测到不受支持的工具配置",
"unsupportedProfileDescription": "当前 tools.profile 的值“{{value}}”不在 OpenClaw 支持列表内。在你手动选择新值之前,它会被保留。",
"unsupportedProfileLabel": "不受支持",
"profiles": {
"default": "默认",
"strict": "严格",
"permissive": "宽松",
"custom": "自定义"
},
"allowList": "允许列表",
"denyList": "拒绝列表",
"patternPlaceholder": "工具名称或模式",
+21
View File
@@ -0,0 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="200" height="200">
<!-- 圆角方形背景 -->
<rect x="0" y="0" width="200" height="200" rx="40" ry="40" fill="#3B82F6"/>
<!-- 中心白色圆环 -->
<circle cx="100" cy="100" r="45" fill="white"/>
<circle cx="100" cy="100" r="25" fill="#3B82F6"/>
<!-- 装饰小圆点 -->
<circle cx="50" cy="50" r="6" fill="white" opacity="0.6"/>
<circle cx="165" cy="70" r="5" fill="white" opacity="0.5"/>
<circle cx="170" cy="140" r="7" fill="white" opacity="0.4"/>
<!-- 右下角深蓝色装饰 -->
<defs>
<clipPath id="roundedCorner">
<rect x="0" y="0" width="200" height="200" rx="40" ry="40"/>
</clipPath>
</defs>
<path d="M 200 150 Q 175 175 150 200 L 200 200 Z" fill="#2563EB" opacity="0.6" clip-path="url(#roundedCorner)"/>
</svg>

After

Width:  |  Height:  |  Size: 838 B

+2
View File
@@ -16,6 +16,7 @@ export const icons: Record<string, string> = {
cloudflare: `<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Cloudflare</title><path d="M16.493 17.4c.135-.52.08-.983-.161-1.338-.215-.328-.592-.519-1.05-.519l-8.663-.109a.148.148 0 01-.135-.082c-.027-.054-.027-.109-.027-.163.027-.082.108-.164.189-.164l8.744-.11c1.05-.054 2.153-.9 2.556-1.937l.511-1.31c.027-.055.027-.11.027-.164C17.92 8.91 15.66 7 12.942 7c-2.503 0-4.628 1.638-5.381 3.903a2.432 2.432 0 00-1.803-.491c-1.21.109-2.153 1.092-2.287 2.32-.027.328 0 .628.054.9C1.56 13.688 0 15.326 0 17.319c0 .19.027.355.027.545 0 .082.08.137.161.137h15.983c.08 0 .188-.055.215-.164l.107-.437" fill="#F38020"></path><path d="M19.238 11.75h-.242c-.054 0-.108.054-.135.109l-.35 1.2c-.134.52-.08.983.162 1.338.215.328.592.518 1.05.518l1.855.11c.054 0 .108.027.135.082.027.054.027.109.027.163-.027.082-.108.164-.188.164l-1.91.11c-1.05.054-2.153.9-2.557 1.937l-.134.355c-.027.055.026.137.107.137h6.592c.081 0 .162-.055.162-.137.107-.41.188-.846.188-1.31-.027-2.62-2.153-4.777-4.762-4.777" fill="#FCAD32"></path></svg>`,
cohere: `<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Cohere</title><path clip-rule="evenodd" d="M8.128 14.099c.592 0 1.77-.033 3.398-.703 1.897-.781 5.672-2.2 8.395-3.656 1.905-1.018 2.74-2.366 2.74-4.18A4.56 4.56 0 0018.1 1H7.549A6.55 6.55 0 001 7.55c0 3.617 2.745 6.549 7.128 6.549z" fill="#39594D" fill-rule="evenodd"></path><path clip-rule="evenodd" d="M9.912 18.61a4.387 4.387 0 012.705-4.052l3.323-1.38c3.361-1.394 7.06 1.076 7.06 4.715a5.104 5.104 0 01-5.105 5.104l-3.597-.001a4.386 4.386 0 01-4.386-4.387z" fill="#D18EE2" fill-rule="evenodd"></path><path d="M4.776 14.962A3.775 3.775 0 001 18.738v.489a3.776 3.776 0 007.551 0v-.49a3.775 3.775 0 00-3.775-3.775z" fill="#FF7759"></path></svg>`,
copilot: `<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Copilot</title><path d="M17.533 1.829A2.528 2.528 0 0015.11 0h-.737a2.531 2.531 0 00-2.484 2.087l-1.263 6.937.314-1.08a2.528 2.528 0 012.424-1.833h4.284l1.797.706 1.731-.706h-.505a2.528 2.528 0 01-2.423-1.829l-.715-2.453z" fill="url(#lobe-icons-copilot-fill-0)" transform="translate(0 1)"></path><path d="M6.726 20.16A2.528 2.528 0 009.152 22h1.566c1.37 0 2.49-1.1 2.525-2.48l.17-6.69-.357 1.228a2.528 2.528 0 01-2.423 1.83h-4.32l-1.54-.842-1.667.843h.497c1.124 0 2.113.75 2.426 1.84l.697 2.432z" fill="url(#lobe-icons-copilot-fill-1)" transform="translate(0 1)"></path><path d="M15 0H6.252c-2.5 0-4 3.331-5 6.662-1.184 3.947-2.734 9.225 1.75 9.225H6.78c1.13 0 2.12-.753 2.43-1.847.657-2.317 1.809-6.359 2.713-9.436.46-1.563.842-2.906 1.43-3.742A1.97 1.97 0 0115 0" fill="url(#lobe-icons-copilot-fill-2)" transform="translate(0 1)"></path><path d="M15 0H6.252c-2.5 0-4 3.331-5 6.662-1.184 3.947-2.734 9.225 1.75 9.225H6.78c1.13 0 2.12-.753 2.43-1.847.657-2.317 1.809-6.359 2.713-9.436.46-1.563.842-2.906 1.43-3.742A1.97 1.97 0 0115 0" fill="url(#lobe-icons-copilot-fill-3)" transform="translate(0 1)"></path><path d="M9 22h8.749c2.5 0 4-3.332 5-6.663 1.184-3.948 2.734-9.227-1.75-9.227H17.22c-1.129 0-2.12.754-2.43 1.848a1149.2 1149.2 0 01-2.713 9.437c-.46 1.564-.842 2.907-1.43 3.743A1.97 1.97 0 019 22" fill="url(#lobe-icons-copilot-fill-4)" transform="translate(0 1)"></path><path d="M9 22h8.749c2.5 0 4-3.332 5-6.663 1.184-3.948 2.734-9.227-1.75-9.227H17.22c-1.129 0-2.12.754-2.43 1.848a1149.2 1149.2 0 01-2.713 9.437c-.46 1.564-.842 2.907-1.43 3.743A1.97 1.97 0 019 22" fill="url(#lobe-icons-copilot-fill-5)" transform="translate(0 1)"></path><defs><radialGradient cx="85.44%" cy="100.653%" fx="85.44%" fy="100.653%" gradientTransform="scale(-.8553 -1) rotate(50.927 2.041 -1.946)" id="lobe-icons-copilot-fill-0" r="105.116%"><stop offset="9.6%" stop-color="#00AEFF"></stop><stop offset="77.3%" stop-color="#2253CE"></stop><stop offset="100%" stop-color="#0736C4"></stop></radialGradient><radialGradient cx="18.143%" cy="32.928%" fx="18.143%" fy="32.928%" gradientTransform="scale(.8897 1) rotate(52.069 .193 .352)" id="lobe-icons-copilot-fill-1" r="95.612%"><stop offset="0%" stop-color="#FFB657"></stop><stop offset="63.4%" stop-color="#FF5F3D"></stop><stop offset="92.3%" stop-color="#C02B3C"></stop></radialGradient><radialGradient cx="82.987%" cy="-9.792%" fx="82.987%" fy="-9.792%" gradientTransform="scale(-1 -.9441) rotate(-70.872 .142 1.17)" id="lobe-icons-copilot-fill-4" r="140.622%"><stop offset="6.6%" stop-color="#8C48FF"></stop><stop offset="50%" stop-color="#F2598A"></stop><stop offset="89.6%" stop-color="#FFB152"></stop></radialGradient><linearGradient id="lobe-icons-copilot-fill-2" x1="39.465%" x2="46.884%" y1="12.117%" y2="103.774%"><stop offset="15.6%" stop-color="#0D91E1"></stop><stop offset="48.7%" stop-color="#52B471"></stop><stop offset="65.2%" stop-color="#98BD42"></stop><stop offset="93.7%" stop-color="#FFC800"></stop></linearGradient><linearGradient id="lobe-icons-copilot-fill-3" x1="45.949%" x2="50%" y1="0%" y2="100%"><stop offset="0%" stop-color="#3DCBFF"></stop><stop offset="24.7%" stop-color="#0588F7" stop-opacity="0"></stop></linearGradient><linearGradient id="lobe-icons-copilot-fill-5" x1="83.507%" x2="83.453%" y1="-6.106%" y2="21.131%"><stop offset="5.8%" stop-color="#F8ADFA"></stop><stop offset="70.8%" stop-color="#A86EDD" stop-opacity="0"></stop></linearGradient></defs></svg>`,
ctok: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" style="flex:none;line-height:1" viewBox="0 0 200 200"><title>CTok</title><rect x="0" y="0" width="200" height="200" rx="40" ry="40" fill="#3B82F6"/><circle cx="100" cy="100" r="45" fill="white"/><circle cx="100" cy="100" r="25" fill="#3B82F6"/><circle cx="50" cy="50" r="6" fill="white" opacity="0.6"/><circle cx="165" cy="70" r="5" fill="white" opacity="0.5"/><circle cx="170" cy="140" r="7" fill="white" opacity="0.4"/><defs><clipPath id="ctok-rc"><rect x="0" y="0" width="200" height="200" rx="40" ry="40"/></clipPath></defs><path d="M 200 150 Q 175 175 150 200 L 200 200 Z" fill="#2563EB" opacity="0.6" clip-path="url(#ctok-rc)"/></svg>`,
crazyrouter: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" style="flex:none;line-height:1" viewBox="0 0 563 648"><title>CrazyRouter</title><path fill="currentColor" d="M 167.5 190 L 276.5 190 L 277 191.5 L 253 234 L 247.5 235 L 246.5 234 L 235.5 234 L 230.5 235 L 229.5 234 L 217.5 234 L 215.5 234 L 191.5 234 L 189.5 235 L 186.5 234 L 164.5 235 L 150.5 239 Q 132.7 246.7 121 260.5 Q 108.2 275.2 102 296.5 L 99 312.5 L 99 335.5 L 102 351.5 L 110 371.5 Q 118.1 386.4 130.5 397 Q 143.5 409 164.5 413 L 173.5 413 L 174.5 414 L 258.5 414 L 314.5 318 L 433.5 318 Q 449.3 314.8 457 303.5 L 462 294.5 L 465 283.5 L 465 269.5 L 460 253.5 L 449.5 241 L 440.5 236 L 430.5 234 L 332.5 234 L 258.5 361 L 212.5 361 L 212 359.5 L 310.5 190 L 438.5 190 L 448.5 192 L 461.5 197 Q 476 205 486 217.5 L 496 233.5 L 502 250.5 L 504 260.5 L 504 268.5 L 505 269.5 L 505 283.5 L 504 284.5 L 503 297.5 L 499 311.5 Q 490.8 331.8 475.5 345 L 462.5 354 L 446 360.5 L 502 452.5 L 504 458 L 456.5 458 L 454 455.5 L 416 389.5 L 398.5 362 L 336.5 362 L 335 363.5 L 285 452.5 L 280.5 458 L 167.5 458 L 166.5 457 L 153.5 456 Q 112.4 445.6 90 416.5 Q 72.7 396.3 64 367.5 L 59 343.5 L 58 314.5 L 59 313.5 L 60 297.5 L 64 280.5 L 73 257.5 Q 85.3 233.8 104.5 217 Q 116.8 206.3 132.5 199 L 149.5 193 L 166.5 191 L 167.5 190 Z"/></svg>`,
cubence: `<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 179 203" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Cubence</title><rect width="100" height="100" rx="13" transform="matrix(0.866025 -0.5 0 1 92 103)" fill="#4B5563"></rect><rect width="100" height="100" rx="13" transform="matrix(0.866025 0.5 -0.866025 0.5 88.6025 -3)" fill="#1F2937"></rect><rect width="100" height="100" rx="13" transform="matrix(0.866025 0.5 0 1 0 53)" fill="#111827"></rect><rect width="72.7816" height="72.7816" rx="13" transform="matrix(0.866025 0.5 0 1 11 73)" fill="#374151"></rect><rect width="28.1436" height="28.1436" rx="3" transform="matrix(0.866025 0.5 0 1 11 86)" fill="#E5E7EB" fill-opacity="0.9"></rect><rect width="28.1436" height="28.1436" rx="3" transform="matrix(0.866025 0.5 0 1 50 107)" fill="#E5E7EB" fill-opacity="0.9"></rect><rect width="13.8564" height="13.8564" rx="3" transform="matrix(0.866025 0.5 0 1 43 148)" fill="#E5E7EB" fill-opacity="0.9"></rect></svg>`,
deepseek: `<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>DeepSeek</title><path d="M23.748 4.482c-.254-.124-.364.113-.512.234-.051.039-.094.09-.137.136-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.156-.708-.311-.955-.65-.172-.241-.219-.51-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.093.172.187.129.323-.082.28-.18.552-.266.833-.055.179-.137.217-.329.14a5.526 5.526 0 01-1.736-1.18c-.857-.828-1.631-1.742-2.597-2.458a11.365 11.365 0 00-.689-.471c-.985-.957.13-1.743.388-1.836.27-.098.093-.432-.779-.428-.872.004-1.67.295-2.687.684a3.055 3.055 0 01-.465.137 9.597 9.597 0 00-2.883-.102c-1.885.21-3.39 1.102-4.497 2.623C.082 8.606-.231 10.684.152 12.85c.403 2.284 1.569 4.175 3.36 5.653 1.858 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.133-.284 4.994-1.86.47.234.962.327 1.78.397.63.059 1.236-.03 1.705-.128.735-.156.684-.837.419-.961-2.155-1.004-1.682-.595-2.113-.926 1.096-1.296 2.746-2.642 3.392-7.003.05-.347.007-.565 0-.845-.004-.17.035-.237.23-.256a4.173 4.173 0 001.545-.475c1.396-.763 1.96-2.015 2.093-3.517.02-.23-.004-.467-.247-.588zM11.581 18c-2.089-1.642-3.102-2.183-3.52-2.16-.392.024-.321.471-.235.763.09.288.207.486.371.739.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.167-1.361-.802-2.5-1.86-3.301-3.307-.774-1.393-1.224-2.887-1.298-4.482-.02-.386.093-.522.477-.592a4.696 4.696 0 011.529-.039c2.132.312 3.946 1.265 5.468 2.774.868.86 1.525 1.887 2.202 2.891.72 1.066 1.494 2.082 2.48 2.914.348.292.625.514.891.677-.802.09-2.14.11-3.054-.614zm1-6.44a.306.306 0 01.415-.287.302.302 0 01.2.288.306.306 0 01-.31.307.303.303 0 01-.304-.308zm3.11 1.596c-.2.081-.399.151-.59.16a1.245 1.245 0 01-.798-.254c-.274-.23-.47-.358-.552-.758a1.73 1.73 0 01.016-.588c.07-.327-.008-.537-.239-.727-.187-.156-.426-.199-.688-.199a.559.559 0 01-.254-.078c-.11-.054-.2-.19-.114-.358.028-.054.16-.186.192-.21.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.391.451.462.576.685.914.176.265.336.537.445.848.067.195-.019.354-.25.452z" fill="#4D6BFE"></path></svg>`,
@@ -64,6 +65,7 @@ export const icons: Record<string, string> = {
micu: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" style="flex:none;line-height:1" viewBox="0 0 241.39 240.6"><title>Micu</title><defs><style>.mc-1{fill:#068cde}.mc-2{fill:#fff}.mc-3{fill:#02a6ff}</style></defs><g><path class="mc-1" d="M226.14,157.63c-3.62,0-7.24,0-10.95,0v-24.96c5.2,0,10.17,0,15.13,0,5.55-.01,8.52-4.01,10.18-8.16,1.34-3.37,1.36-7.51-1.34-11.1-3.16-4.2-7.23-5.72-12.25-5.63-3.87.07-7.74.01-11.66.01v-24.79c6.56-.43,12.93.45,19.3-1.13.4-.42.85-1.05,1.44-1.49,4.61-3.47,6.48-9.22,4.67-14.51-1.63-4.76-6.67-8.03-12.22-7.99-4.37.03-8.74,0-13.42,0,0-5.79.1-11.16-.04-16.54-.08-3.23-1.09-6.23-3.22-8.79-7.6-9.17-17.84-6.02-28.13-6.29-.39-.23-.63-1.14-.45-2.35.73-4.72.37-9.44-.24-14.13-.51-3.95-5.79-9.24-9.1-9.56-10.42-1-15.88,5.21-15.83,14.89.02,3.54,0,7.08,0,10.92h-24.44c-.76-1.03-.45-2.13-.42-3.19.15-5.2.71-10.35-1.11-15.5-2.15-6.08-11.68-9.27-17.05-5.79-5.27,3.41-7.22,7.95-6.99,13.99.14,3.56.53,7.22-.44,10.6h-23.98c-.22-.38-.37-.51-.37-.66-.05-4.56,0-9.12-.14-13.68-.15-5.18-4.72-10.76-9.31-11.57-8.81-1.55-15.64,4.23-15.64,13.24,0,4.11,0,8.21,0,12.61-4.92,0-9.32.08-13.72-.02-10.41-.22-18.81,7.79-17.84,18.57.39,4.32.06,8.7.06,13.34-5.17,0-9.9-.05-14.63.01-5.36.07-11.08,5.57-11.83,10.1-1.39,8.44,6.23,15.25,14.55,14.78,3.86-.22,7.74-.04,11.75-.04v24.92c-4.45,0-8.67-.07-12.89.02-3.2.07-6.5.62-8.75,2.93-3.6,3.69-6.1,8.11-4.12,13.5,2.13,5.8,6.42,8.43,12.98,8.43,4.21,0,8.42,0,12.83,0v24.95c-3.48,0-6.79-.12-10.08.03-3.74.18-7.43.36-10.78,2.69-4.11,2.87-6.48,8.21-5.32,12.83,1.1,4.36,7.17,9.83,11.59,9.41,4.82-.46,9.72-.1,14.69-.1,0,5.18.51,9.88-.1,14.44-1.36,10,8.64,18.06,17.22,17.5,4.62-.3,9.28-.05,14.15-.05,1.26,9.11-3.28,19.95,9.5,25.9,10.27,1.43,15.74-3.33,15.75-15.14,0-3.45,0-6.9,0-10.55h24.94c0,3.39,0,6.6,0,9.8,0,3.81.26,7.41,2.73,10.76,3.09,4.2,8.64,6.68,14,4.87,3.62-1.22,8.31-5.43,8.3-10.7,0-4.85,0-9.7,0-14.7h24.92c0,3.98-.14,7.7.03,11.41.22,4.83,1.4,9.35,5.68,12.32,5.16,3.59,12.81,2.96,17.28-2.41,3.93-7.43,2.05-14.57,2.39-21.58,5.71,0,11.1.03,16.49-.02,1.98-.02,4.05-.06,5.8-1.09,6.78-3.99,9.8-9.94,9.36-17.81-.23-4.18-.04-8.39-.04-12.56,8.59-1.68,18.23,2.96,24.73-6.3.08-.31.31-1.1.5-1.9.97-4.19,1.82-8.06-1.59-12.01-3.49-4.05-7.66-5.05-12.52-5.04ZM168.3,160.54c0,3.41-1.48,4.58-4.69,4.49-4.41-.12-8.82-.09-13.23-.05-3.15.03-4.43-1.39-4.41-4.56.06-16.85.02-33.69-.03-50.54,0-.99.44-2.13-.73-3.15-4.49,13.97-8.94,27.8-13.44,41.79h-21.8c-4.39-13.78-8.8-27.62-13.55-42.54-.15,1.94-.29,2.89-.29,3.84-.01,16.68-.08,33.36.04,50.04.03,3.7-1.18,5.38-5.05,5.16-5.51-.31-11.08.38-16.22-.44-1.24-1.59-1.21-2.95-1.21-4.26.07-27.3.18-54.6.22-81.9,0-2.16.89-3.46,2.97-3.48,9.9-.06,19.8,0,29.7.05.31,0,.63.19,1.39.44,4.22,14.29,8.51,28.79,12.8,43.3.29.05.58.1.87.15,4.53-14.59,9.07-29.17,13.66-43.95,10.35,0,20.48-.03,30.62.03,1.76.01,2.38,1.37,2.42,2.92.08,2.57.1,5.14.1,7.71-.06,24.98-.16,49.95-.15,74.93Z"/><rect class="mc-3" x="48.86" y="48.46" width="143.67" height="143.67" rx="10.57" ry="10.57"/><path class="mc-2" d="M165.55,75.28c-10.14-.06-20.27-.03-30.62-.03-4.59,14.78-9.12,29.36-13.66,43.95-.29-.05-.58-.1-.87-.15-4.29-14.51-8.58-29.01-12.8-43.3-.77-.25-1.08-.44-1.39-.44-9.9-.04-19.8-.1-29.7-.05-2.08.01-2.96,1.32-2.97,3.48-.04,27.3-.15,54.6-.22,81.9,0,1.31-.03,2.67,1.21,4.26,5.13.82,10.7.13,16.22.44,3.87.22,5.07-1.46,5.05-5.16-.12-16.68-.06-33.36-.04-50.04,0-.95.14-1.9.29-3.84,4.75,14.91,9.16,28.76,13.55,42.54h21.8c4.5-13.99,8.94-27.82,13.44-41.79,1.18,1.01.73,2.16.73,3.15.04,16.85.09,33.69.03,50.54-.01,3.17,1.26,4.59,4.41,4.56,4.41-.04,8.82-.07,13.23.05,3.21.09,4.69-1.08,4.69-4.49-.01-24.98.09-49.95.15-74.93,0-2.57-.02-5.14-.1-7.71-.05-1.55-.67-2.91-2.42-2.92Z"/></g></svg>`,
ucloud: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="1em" width="1em" style="flex:none;line-height:1" viewBox="0 0 172.11 172.11"><title>UCloud</title><defs><style>.cls-1{fill:url(#uc-g56)}.cls-2{fill:url(#uc-g37)}.cls-3{fill:url(#uc-g37-2)}.cls-4{fill:url(#uc-g37-3)}.cls-5{fill:url(#uc-g37-4)}.cls-6{fill:url(#uc-g37-5)}.cls-7{fill:url(#uc-g37-6)}.cls-8{fill:url(#uc-g37-7)}.cls-9{fill:#fff}.cls-10{fill:url(#uc-g38)}</style><linearGradient id="uc-g56" x1="86.06" y1="-6.73" x2="86.06" y2="185.53" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#32303a"/><stop offset=".36" stop-color="#34323d"/><stop offset=".62" stop-color="#3a3946"/><stop offset=".85" stop-color="#444556"/><stop offset="1" stop-color="#4e5065"/></linearGradient><linearGradient id="uc-g37" x1="143.96" y1="73.06" x2="71.52" y2="34.1" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#4043ff"/><stop offset="1" stop-color="#f0f5fa"/></linearGradient><linearGradient id="uc-g37-2" x1="104.88" y1="118.84" x2="71.68" y2="66.44" xlink:href="#uc-g37"/><linearGradient id="uc-g37-3" x1="95.68" y1="72.87" x2="33.68" y2="76.43" xlink:href="#uc-g37"/><linearGradient id="uc-g37-4" x1="70" y1="130.18" x2="46.68" y2="73.38" xlink:href="#uc-g37"/><linearGradient id="uc-g37-5" x1="107.49" y1="106.07" x2="147.27" y2="159.57" xlink:href="#uc-g37"/><linearGradient id="uc-g37-6" x1="106.69" y1="50.6" x2="142.65" y2="131.51" xlink:href="#uc-g37"/><linearGradient id="uc-g37-7" x1="111.35" y1="152.42" x2="82.55" y2="89.87" xlink:href="#uc-g37"/><linearGradient id="uc-g38" x1="64.73" y1="89.4" x2="83.39" y2="89.4" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#5b5dfe"/><stop offset=".18" stop-color="#5b5dfe" stop-opacity=".99"/><stop offset=".31" stop-color="#5b5dfe" stop-opacity=".95"/><stop offset=".43" stop-color="#5b5cfe" stop-opacity=".89"/><stop offset=".54" stop-color="#5b5cfe" stop-opacity=".8"/><stop offset=".64" stop-color="#5b5bfe" stop-opacity=".68"/><stop offset=".74" stop-color="#5b5afe" stop-opacity=".54"/><stop offset=".83" stop-color="#5a59ff" stop-opacity=".38"/><stop offset=".92" stop-color="#5a58ff" stop-opacity=".19"/><stop offset="1" stop-color="#5a57ff" stop-opacity="0"/></linearGradient></defs><rect class="cls-1" width="172.11" height="172.11" rx="46.24"/><polygon class="cls-2" points="124.1 51.65 104.14 63.16 104.08 63.12 84.2 51.65 104.08 40.17 104.14 40.14 124.1 51.65"/><path class="cls-3" d="M111.61,90.51l-.1-.06-2.92-1.69a8.9,8.9,0,0,1-4.46-7.69l0-17.95L84.2,51.65l-.12.07V69.59a8.91,8.91,0,0,1-4.45,7.71L64.26,86.17v23l12.4-7.15,3.09-1.78a8.92,8.92,0,0,1,8.91,0l15.42,8.91.06,0,19.93-11.49h0Z"/><path class="cls-4" d="M84.08,66v3.55a8.91,8.91,0,0,1-4.45,7.71L64.26,86.17,44.32,74.68v0L64.26,63.17l12.41,7.15A4.94,4.94,0,0,0,84.08,66Z"/><polygon class="cls-5" points="64.26 86.17 64.26 109.2 44.32 97.7 44.32 74.68 64.26 86.17"/><polygon class="cls-6" points="124.1 97.7 124.1 120.72 124.08 120.72 104.14 132.23 104.08 132.21 104.08 109.25 104.14 109.2 124.08 97.7 124.1 97.7"/><path class="cls-7" d="M124.1,51.65v23h0l-12.48,7.21c-3.28,1.89-3,6.87-3,6.87a8.9,8.9,0,0,1-4.46-7.69l0-17.89.06,0Z"/><path class="cls-8" d="M104.08,109.18v23L84.2,120.72l-.12-.07V106.33a4.94,4.94,0,0,0-7.41-4.28l3-1.72a8.89,8.89,0,0,1,8.87,0Z"/><path class="cls-9" d="M85.28,81.09V91.24a2.56,2.56,0,0,0,3.85,2.22l8.81-5.09a2.56,2.56,0,0,0,0-4.44l-8.82-5.06A2.56,2.56,0,0,0,85.28,81.09Z"/><path class="cls-10" d="M84.08,69.59a8.91,8.91,0,0,1-4.45,7.71L64.26,86.17v23l12.4-7.15,3.09-1.78a8.82,8.82,0,0,1,4.33-1.19Z"/></svg>`,
sssaicode: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" style="flex:none;line-height:1" viewBox="0 0 512 512"><title>SSAI Code</title><defs><linearGradient id="ssc-gradLeft" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#0ff5ce" /><stop offset="100%" stop-color="#147a8a" /></linearGradient><linearGradient id="ssc-gradRight" x1="100%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stop-color="#d0e4f5" /><stop offset="100%" stop-color="#6a9ec4" /></linearGradient><linearGradient id="ssc-gradTop" x1="50%" y1="0%" x2="50%" y2="100%"><stop offset="0%" stop-color="#a0d8e8" /><stop offset="100%" stop-color="#4aafbf" /></linearGradient><linearGradient id="ssc-gradText" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#0ff5ce" /><stop offset="35%" stop-color="#4abfcf" /><stop offset="65%" stop-color="#7badd4" /><stop offset="100%" stop-color="#c0daf0" /></linearGradient><linearGradient id="ssc-gradS" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stop-color="#f0f8ff" /><stop offset="100%" stop-color="#6cbfcf" /></linearGradient><filter id="ssc-glow" x="-30%" y="-30%" width="160%" height="160%"><feGaussianBlur stdDeviation="4" result="blur" /><feMerge><feMergeNode in="blur" /><feMergeNode in="SourceGraphic" /></feMerge></filter><pattern id="ssc-binL" x="0" y="0" width="55" height="16" patternUnits="userSpaceOnUse" patternTransform="rotate(-3)"><text x="0" y="11" font-family="monospace" font-size="8" fill="rgba(0,255,210,0.25)">1001 1101</text></pattern><pattern id="ssc-binR" x="0" y="0" width="55" height="16" patternUnits="userSpaceOnUse" patternTransform="rotate(3)"><text x="0" y="11" font-family="monospace" font-size="8" fill="rgba(180,210,240,0.25)">0110 1011</text></pattern><pattern id="ssc-binT" x="0" y="0" width="50" height="16" patternUnits="userSpaceOnUse"><text x="2" y="11" font-family="monospace" font-size="8" fill="rgba(120,200,220,0.2)">10 110</text></pattern></defs><rect width="512" height="512" rx="72" fill="#08080e" /><polygon points="90,350 250,350 170,228" fill="url(#ssc-gradLeft)" opacity="0.8" /><polygon points="90,350 250,350 170,228" fill="url(#ssc-binL)" /><polygon points="262,350 422,350 342,228" fill="url(#ssc-gradRight)" opacity="0.8" /><polygon points="262,350 422,350 342,228" fill="url(#ssc-binR)" /><polygon points="176,290 336,290 256,168" fill="none" stroke="url(#ssc-gradTop)" stroke-width="2.5" opacity="0.85" /><polygon points="192,280 320,280 256,184" fill="none" stroke="url(#ssc-gradTop)" stroke-width="0.8" opacity="0.35" /><text x="256" y="316" text-anchor="middle" font-family="Georgia, 'Times New Roman', serif" font-size="120" font-weight="bold" fill="url(#ssc-gradS)" filter="url(#ssc-glow)">S</text><text x="256" y="425" text-anchor="middle" font-family="'Helvetica Neue', 'Segoe UI', Arial, sans-serif" font-size="40" font-weight="300" letter-spacing="5" fill="url(#ssc-gradText)">SSSAiCode</text></svg>`,
stepfun: `<svg width="1em" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><title>StepFun</title><g clip-path="url(#clip0_10683_3111)"><path d="M23.2964 14.7395H17.4448V23.2981H12.9565V13.2307H23.2964V14.7395ZM20.355 8.24341H10.4683V20.1165H0.63916V15.76H5.94385L5.94678 3.90942H20.355V8.24341ZM4.02002 12.5881H2.48779V2.51685H4.02002V12.5881ZM22.4272 1.60962H23.3394V2.51587H22.4272V4.32544H21.519V2.51587H19.6997V1.60962H21.519V0.702393H22.4272V1.60962Z" fill="#005AFF"/></g><defs><clipPath id="clip0_10683_3111"><rect width="24" height="24" fill="white"/></clipPath></defs></svg>`,
catcoder: `<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>KwaiKAT</title><path d="M20.42 19.311h3.418V1l-6.781 4.177-6.778-4.111.026 7.868h3.418l-.026-2.222 3.42 2.035 3.303-2.035v12.6z"></path><path d="M3.064 10.734c2.784-2.07 6.942-2.394 9.941.907l.01.01.01.013 9.16 12.24h-3.84l-7.69-10.217c-1.63-1.737-3.891-1.689-5.515-.638-1.624 1.05-2.563 3.073-1.548 5.28 1.494 3.246 6.152 3.275 7.725.108l.032-.064 2.02 2.629c-2.98 3.968-9.329 3.926-12.165-.552-2.395-3.78-.926-7.645 1.86-9.716z"></path></svg>`,
mcp: `<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>ModelContextProtocol</title><path d="M15.688 2.343a2.588 2.588 0 00-3.61 0l-9.626 9.44a.863.863 0 01-1.203 0 .823.823 0 010-1.18l9.626-9.44a4.313 4.313 0 016.016 0 4.116 4.116 0 011.204 3.54 4.3 4.3 0 013.609 1.18l.05.05a4.115 4.115 0 010 5.9l-8.706 8.537a.274.274 0 000 .393l1.788 1.754a.823.823 0 010 1.18.863.863 0 01-1.203 0l-1.788-1.753a1.92 1.92 0 010-2.754l8.706-8.538a2.47 2.47 0 000-3.54l-.05-.049a2.588 2.588 0 00-3.607-.003l-7.172 7.034-.002.002-.098.097a.863.863 0 01-1.204 0 .823.823 0 010-1.18l7.273-7.133a2.47 2.47 0 00-.003-3.537z"></path><path d="M14.485 4.703a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a4.115 4.115 0 000 5.9 4.314 4.314 0 006.016 0l7.12-6.982a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a2.588 2.588 0 01-3.61 0 2.47 2.47 0 010-3.54l7.12-6.982z"></path></svg>`,
novita: `<svg width="1em" height="1em" style="flex:none;line-height:1" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"><title>Novita</title><g clip-path="url(#clip0_3135_1230)"><path d="M15.5564 8.26172V16.5239L2.1875 29.8928H15.5564V21.6302L23.8194 29.8928H37.1875L15.5564 8.26172Z" fill="#000000"/></g><defs><clipPath id="clip0_3135_1230"><rect width="35" height="21.6311" fill="white" transform="translate(2.1875 8.26172)"/></clipPath></defs></svg>`,
+14
View File
@@ -93,6 +93,13 @@ export const iconMetadata: Record<string, IconMetadata> = {
keywords: [],
defaultColor: "currentColor",
},
ctok: {
name: "ctok",
displayName: "CTok",
category: "ai-provider",
keywords: ["ctok", "ai", "programming"],
defaultColor: "#3B82F6",
},
cubence: {
name: "cubence",
displayName: "Cubence",
@@ -387,6 +394,13 @@ export const iconMetadata: Record<string, IconMetadata> = {
keywords: ["nvidia", "nim", "gpu"],
defaultColor: "#74B71B",
},
stepfun: {
name: "stepfun",
displayName: "StepFun",
category: "ai-provider",
keywords: ["stepfun", "step", "jieyue", "阶跃星辰"],
defaultColor: "#005AFF",
},
};
export function getIconMetadata(name: string): IconMetadata | undefined {
+10
View File
@@ -0,0 +1,10 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_10683_3111)">
<path d="M23.2964 14.7395H17.4448V23.2981H12.9565V13.2307H23.2964V14.7395ZM20.355 8.24341H10.4683V20.1165H0.63916V15.76H5.94385L5.94678 3.90942H20.355V8.24341ZM4.02002 12.5881H2.48779V2.51685H4.02002V12.5881ZM22.4272 1.60962H23.3394V2.51587H22.4272V4.32544H21.519V2.51587H19.6997V1.60962H21.519V0.702393H22.4272V1.60962Z" fill="#005AFF"/>
</g>
<defs>
<clipPath id="clip0_10683_3111">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 590 B

+1
View File
@@ -523,6 +523,7 @@ export interface OpenClawProviderConfig {
api?: string; // API 协议类型(如 "openai-completions"、"anthropic"
models?: OpenClawModel[]; // 可用模型列表
headers?: Record<string, string>; // 自定义请求头(如 User-Agent
authHeader?: boolean; // 供应商自定义认证开关(如 Longcat)
}
// OpenClaw agents.defaults 完整配置
+348 -64
View File
@@ -1,7 +1,7 @@
// 供应商配置处理工具函数
import type { TemplateValueConfig } from "../config/claudeProviderPresets";
import { normalizeQuotes, normalizeTomlText } from "@/utils/textNormalization";
import { normalizeTomlText } from "@/utils/textNormalization";
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
const isPlainObject = (value: unknown): value is Record<string, any> => {
@@ -414,17 +414,234 @@ export const hasTomlCommonConfigSnippet = (
// ========== Codex base_url utils ==========
const TOML_SECTION_HEADER_PATTERN = /^\s*\[([^\]\r\n]+)\]\s*$/;
const TOML_BASE_URL_PATTERN =
/^\s*base_url\s*=\s*(["'])([^"'\r\n]+)\1\s*(?:#.*)?$/;
const TOML_MODEL_PATTERN = /^\s*model\s*=\s*(["'])([^"'\r\n]+)\1\s*(?:#.*)?$/;
const TOML_MODEL_PROVIDER_LINE_PATTERN =
/^\s*model_provider\s*=\s*(["'])([^"'\r\n]+)\1\s*(?:#.*)?$/;
const TOML_MODEL_PROVIDER_PATTERN =
/^\s*model_provider\s*=\s*(["'])([^"'\r\n]+)\1\s*(?:#.*)?$/m;
interface TomlSectionRange {
bodyEndIndex: number;
bodyStartIndex: number;
}
interface TomlAssignmentMatch {
index: number;
sectionName?: string;
value: string;
}
const finalizeTomlText = (lines: string[]): string =>
lines
.join("\n")
.replace(/\n{3,}/g, "\n\n")
.replace(/^\n+/, "");
const getTomlSectionRange = (
lines: string[],
sectionName: string,
): TomlSectionRange | undefined => {
let headerLineIndex = -1;
for (let index = 0; index < lines.length; index += 1) {
const match = lines[index].match(TOML_SECTION_HEADER_PATTERN);
if (!match) {
continue;
}
if (headerLineIndex === -1) {
if (match[1] === sectionName) {
headerLineIndex = index;
}
continue;
}
return {
bodyStartIndex: headerLineIndex + 1,
bodyEndIndex: index,
};
}
if (headerLineIndex === -1) {
return undefined;
}
return {
bodyStartIndex: headerLineIndex + 1,
bodyEndIndex: lines.length,
};
};
const getTopLevelEndIndex = (lines: string[]): number => {
const firstSectionIndex = lines.findIndex((line) =>
TOML_SECTION_HEADER_PATTERN.test(line),
);
return firstSectionIndex === -1 ? lines.length : firstSectionIndex;
};
const getTomlSectionInsertIndex = (
lines: string[],
sectionRange: TomlSectionRange,
): number => {
let insertIndex = sectionRange.bodyEndIndex;
while (
insertIndex > sectionRange.bodyStartIndex &&
lines[insertIndex - 1].trim() === ""
) {
insertIndex -= 1;
}
return insertIndex;
};
const getCodexModelProviderName = (configText: string): string | undefined => {
const match = configText.match(TOML_MODEL_PROVIDER_PATTERN);
const providerName = match?.[2]?.trim();
return providerName || undefined;
};
const getCodexProviderSectionName = (
configText: string,
): string | undefined => {
const providerName = getCodexModelProviderName(configText);
return providerName ? `model_providers.${providerName}` : undefined;
};
const findTomlAssignmentInRange = (
lines: string[],
pattern: RegExp,
startIndex: number,
endIndex: number,
sectionName?: string,
): TomlAssignmentMatch | undefined => {
for (let index = startIndex; index < endIndex; index += 1) {
const match = lines[index].match(pattern);
if (match?.[2]) {
return {
index,
sectionName,
value: match[2],
};
}
}
return undefined;
};
const findTomlAssignments = (
lines: string[],
pattern: RegExp,
): TomlAssignmentMatch[] => {
const assignments: TomlAssignmentMatch[] = [];
let currentSectionName: string | undefined;
lines.forEach((line, index) => {
const sectionMatch = line.match(TOML_SECTION_HEADER_PATTERN);
if (sectionMatch) {
currentSectionName = sectionMatch[1];
return;
}
const match = line.match(pattern);
if (!match?.[2]) {
return;
}
assignments.push({
index,
sectionName: currentSectionName,
value: match[2],
});
});
return assignments;
};
const isMcpServerSection = (sectionName?: string): boolean =>
sectionName === "mcp_servers" ||
sectionName?.startsWith("mcp_servers.") === true;
const isOtherProviderSection = (
sectionName: string | undefined,
targetSectionName: string | undefined,
): boolean =>
Boolean(
sectionName &&
sectionName !== targetSectionName &&
(sectionName === "model_providers" ||
sectionName.startsWith("model_providers.")),
);
const getRecoverableBaseUrlAssignments = (
assignments: TomlAssignmentMatch[],
targetSectionName: string | undefined,
): TomlAssignmentMatch[] =>
assignments.filter(
({ sectionName }) =>
sectionName !== targetSectionName &&
!isMcpServerSection(sectionName) &&
!isOtherProviderSection(sectionName, targetSectionName),
);
const getTopLevelModelProviderLineIndex = (lines: string[]): number => {
const topLevelEndIndex = getTopLevelEndIndex(lines);
for (let index = 0; index < topLevelEndIndex; index += 1) {
if (TOML_MODEL_PROVIDER_LINE_PATTERN.test(lines[index])) {
return index;
}
}
return -1;
};
// 从 Codex 的 TOML 配置文本中提取 base_url(支持单/双引号)
export const extractCodexBaseUrl = (
configText: string | undefined | null,
): string | undefined => {
try {
const raw = typeof configText === "string" ? configText : "";
// 归一化中文/全角引号,避免正则提取失败
const text = normalizeQuotes(raw);
const text = normalizeTomlText(raw);
if (!text) return undefined;
const m = text.match(/base_url\s*=\s*(['"])([^'\"]+)\1/);
return m && m[2] ? m[2] : undefined;
const lines = text.split("\n");
const targetSectionName = getCodexProviderSectionName(text);
if (targetSectionName) {
const sectionRange = getTomlSectionRange(lines, targetSectionName);
if (sectionRange) {
const match = findTomlAssignmentInRange(
lines,
TOML_BASE_URL_PATTERN,
sectionRange.bodyStartIndex,
sectionRange.bodyEndIndex,
targetSectionName,
);
if (match?.value) {
return match.value;
}
}
}
const topLevelMatch = findTomlAssignmentInRange(
lines,
TOML_BASE_URL_PATTERN,
0,
getTopLevelEndIndex(lines),
);
if (topLevelMatch?.value) {
return topLevelMatch.value;
}
const fallbackAssignments = getRecoverableBaseUrlAssignments(
findTomlAssignments(lines, TOML_BASE_URL_PATTERN),
targetSectionName,
);
return fallbackAssignments.length === 1
? fallbackAssignments[0].value
: undefined;
} catch {
return undefined;
}
@@ -451,36 +668,107 @@ export const setCodexBaseUrl = (
baseUrl: string,
): string => {
const trimmed = baseUrl.trim();
// 归一化原文本中的引号(既能匹配,也能输出稳定格式)
const normalizedText = normalizeQuotes(configText);
const normalizedText = normalizeTomlText(configText);
const lines = normalizedText ? normalizedText.split("\n") : [];
const targetSectionName = getCodexProviderSectionName(normalizedText);
const allAssignments = findTomlAssignments(lines, TOML_BASE_URL_PATTERN);
const recoverableAssignments = getRecoverableBaseUrlAssignments(
allAssignments,
targetSectionName,
);
// 允许清空:当 baseUrl 为空时,移除 base_url 行
if (!trimmed) {
if (!normalizedText) return normalizedText;
const next = normalizedText
.split("\n")
.filter((line) => !/^\s*base_url\s*=/.test(line))
.join("\n")
// 避免移除后留下过多空行
.replace(/\n{3,}/g, "\n\n")
// 避免开头出现空行
.replace(/^\n+/, "");
return next;
if (targetSectionName) {
const sectionRange = getTomlSectionRange(lines, targetSectionName);
const targetMatch = sectionRange
? findTomlAssignmentInRange(
lines,
TOML_BASE_URL_PATTERN,
sectionRange.bodyStartIndex,
sectionRange.bodyEndIndex,
targetSectionName,
)
: undefined;
if (targetMatch) {
lines.splice(targetMatch.index, 1);
return finalizeTomlText(lines);
}
}
if (recoverableAssignments.length === 1) {
lines.splice(recoverableAssignments[0].index, 1);
return finalizeTomlText(lines);
}
return finalizeTomlText(lines);
}
const normalizedUrl = trimmed.replace(/\s+/g, "");
const replacementLine = `base_url = "${normalizedUrl}"`;
const pattern = /base_url\s*=\s*(["'])([^"']+)\1/;
if (pattern.test(normalizedText)) {
return normalizedText.replace(pattern, replacementLine);
if (targetSectionName) {
let targetSectionRange = getTomlSectionRange(lines, targetSectionName);
const targetMatch = targetSectionRange
? findTomlAssignmentInRange(
lines,
TOML_BASE_URL_PATTERN,
targetSectionRange.bodyStartIndex,
targetSectionRange.bodyEndIndex,
targetSectionName,
)
: undefined;
if (targetMatch) {
lines[targetMatch.index] = replacementLine;
return finalizeTomlText(lines);
}
if (recoverableAssignments.length === 1) {
lines.splice(recoverableAssignments[0].index, 1);
targetSectionRange = getTomlSectionRange(lines, targetSectionName);
}
if (targetSectionRange) {
const insertIndex = getTomlSectionInsertIndex(lines, targetSectionRange);
lines.splice(insertIndex, 0, replacementLine);
return finalizeTomlText(lines);
}
if (lines.length > 0 && lines[lines.length - 1].trim() !== "") {
lines.push("");
}
lines.push(`[${targetSectionName}]`, replacementLine);
return finalizeTomlText(lines);
}
const prefix =
normalizedText && !normalizedText.endsWith("\n")
? `${normalizedText}\n`
: normalizedText;
return `${prefix}${replacementLine}\n`;
const topLevelEndIndex = getTopLevelEndIndex(lines);
const topLevelMatch = findTomlAssignmentInRange(
lines,
TOML_BASE_URL_PATTERN,
0,
topLevelEndIndex,
);
if (topLevelMatch) {
lines[topLevelMatch.index] = replacementLine;
return finalizeTomlText(lines);
}
const modelProviderIndex = getTopLevelModelProviderLineIndex(lines);
if (modelProviderIndex !== -1) {
lines.splice(modelProviderIndex + 1, 0, replacementLine);
return finalizeTomlText(lines);
}
if (lines.length === 0) {
return `${replacementLine}\n`;
}
const insertIndex = topLevelEndIndex;
lines.splice(insertIndex, 0, replacementLine);
return finalizeTomlText(lines);
};
// ========== Codex model name utils ==========
@@ -491,13 +779,16 @@ export const extractCodexModelName = (
): string | undefined => {
try {
const raw = typeof configText === "string" ? configText : "";
// 归一化中文/全角引号,避免正则提取失败
const text = normalizeQuotes(raw);
const text = normalizeTomlText(raw);
if (!text) return undefined;
// 匹配 model = "xxx" 或 model = 'xxx'
const m = text.match(/^model\s*=\s*(['"])([^'"]+)\1/m);
return m && m[2] ? m[2] : undefined;
const lines = text.split("\n");
const topLevelMatch = findTomlAssignmentInRange(
lines,
TOML_MODEL_PATTERN,
0,
getTopLevelEndIndex(lines),
);
return topLevelMatch?.value;
} catch {
return undefined;
}
@@ -509,47 +800,40 @@ export const setCodexModelName = (
modelName: string,
): string => {
const trimmed = modelName.trim();
// 归一化原文本中的引号(既能匹配,也能输出稳定格式)
const normalizedText = normalizeQuotes(configText);
const normalizedText = normalizeTomlText(configText);
const lines = normalizedText ? normalizedText.split("\n") : [];
const topLevelEndIndex = getTopLevelEndIndex(lines);
const topLevelMatch = findTomlAssignmentInRange(
lines,
TOML_MODEL_PATTERN,
0,
topLevelEndIndex,
);
// 允许清空:当 modelName 为空时,移除 model 行
if (!trimmed) {
if (!normalizedText) return normalizedText;
const next = normalizedText
.split("\n")
.filter((line) => !/^\s*model\s*=/.test(line))
.join("\n")
.replace(/\n{3,}/g, "\n\n")
.replace(/^\n+/, "");
return next;
if (topLevelMatch) {
lines.splice(topLevelMatch.index, 1);
}
return finalizeTomlText(lines);
}
const replacementLine = `model = "${trimmed}"`;
const pattern = /^model\s*=\s*["']([^"']+)["']/m;
if (pattern.test(normalizedText)) {
return normalizedText.replace(pattern, replacementLine);
if (topLevelMatch) {
lines[topLevelMatch.index] = replacementLine;
return finalizeTomlText(lines);
}
// 如果不存在 model 字段,尝试在 model_provider 之后插入
// 如果 model_provider 也不存在,则插入到开头
const providerPattern = /^model_provider\s*=\s*["'][^"']+["']/m;
const match = normalizedText.match(providerPattern);
if (match && match.index !== undefined) {
// 在 model_provider 行之后插入
const endOfLine = normalizedText.indexOf("\n", match.index);
if (endOfLine !== -1) {
return (
normalizedText.slice(0, endOfLine + 1) +
replacementLine +
"\n" +
normalizedText.slice(endOfLine + 1)
);
}
const modelProviderIndex = getTopLevelModelProviderLineIndex(lines);
if (modelProviderIndex !== -1) {
lines.splice(modelProviderIndex + 1, 0, replacementLine);
return finalizeTomlText(lines);
}
// 在文件开头插入
const lines = normalizedText.split("\n");
return `${replacementLine}\n${lines.join("\n")}`;
if (lines.length === 0) {
return `${replacementLine}\n`;
}
lines.splice(topLevelEndIndex, 0, replacementLine);
return finalizeTomlText(lines);
};
@@ -0,0 +1,121 @@
import type { ReactNode } from "react";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import CodexConfigEditor from "@/components/providers/forms/CodexConfigEditor";
import GeminiConfigEditor from "@/components/providers/forms/GeminiConfigEditor";
vi.mock("@/components/common/FullScreenPanel", () => ({
FullScreenPanel: ({
isOpen,
title,
onClose,
children,
footer,
}: {
isOpen: boolean;
title: string;
onClose: () => void;
children: ReactNode;
footer?: ReactNode;
}) =>
isOpen ? (
<div data-testid="common-config-panel">
<button type="button" onClick={onClose}>
panel-close
</button>
<h2>{title}</h2>
<div>{children}</div>
<div>{footer}</div>
</div>
) : null,
}));
vi.mock("@/components/JsonEditor", () => ({
default: ({
value,
onChange,
}: {
value: string;
onChange: (value: string) => void;
}) => (
<textarea
value={value}
onChange={(event) => onChange(event.target.value)}
aria-label="mock-editor"
/>
),
}));
describe("Common config modals", () => {
it("keeps the Codex common config modal closed after user closes it with an error present", async () => {
render(
<CodexConfigEditor
authValue="{}"
configValue=""
onAuthChange={() => {}}
onConfigChange={() => {}}
useCommonConfig={false}
onCommonConfigToggle={() => {}}
commonConfigSnippet={`base_url = "https://example.com"`}
onCommonConfigSnippetChange={() => false}
onCommonConfigErrorClear={() => {}}
commonConfigError="Invalid TOML"
authError=""
configError=""
/>,
);
expect(screen.queryByTestId("common-config-panel")).not.toBeInTheDocument();
fireEvent.click(
screen.getByRole("button", { name: /codexConfig.editCommonConfig|编辑通用配置/ }),
);
expect(screen.getByTestId("common-config-panel")).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "common.cancel" }));
await waitFor(() =>
expect(
screen.queryByTestId("common-config-panel"),
).not.toBeInTheDocument(),
);
});
it("keeps the Gemini common config modal closed after user closes it with an error present", async () => {
render(
<GeminiConfigEditor
envValue="{}"
configValue="{}"
onEnvChange={() => {}}
onConfigChange={() => {}}
useCommonConfig={false}
onCommonConfigToggle={() => {}}
commonConfigSnippet={`{"GEMINI_MODEL":"gemini-2.5-pro"}`}
onCommonConfigSnippetChange={() => false}
onCommonConfigErrorClear={() => {}}
commonConfigError="Invalid JSON"
envError=""
configError=""
/>,
);
expect(screen.queryByTestId("common-config-panel")).not.toBeInTheDocument();
fireEvent.click(
screen.getByRole("button", {
name: /geminiConfig.editCommonConfig|编辑通用配置/,
}),
);
expect(screen.getByTestId("common-config-panel")).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "common.cancel" }));
await waitFor(() =>
expect(
screen.queryByTestId("common-config-panel"),
).not.toBeInTheDocument(),
);
});
});
@@ -69,6 +69,18 @@ const renderPage = () => {
);
};
const openSearch = () => {
const searchButton = Array.from(screen.getAllByRole("button")).find((button) =>
button.querySelector(".lucide-search"),
);
if (!searchButton) {
throw new Error("Search button not found");
}
fireEvent.click(searchButton);
};
describe("SessionManagerPage", () => {
beforeEach(() => {
toastSuccessMock.mockReset();
@@ -137,4 +149,40 @@ describe("SessionManagerPage", () => {
expect(toastErrorMock).not.toHaveBeenCalled();
expect(toastSuccessMock).toHaveBeenCalled();
});
it("removes a deleted session from filtered search results", async () => {
renderPage();
await waitFor(() =>
expect(
screen.getByRole("heading", { name: "Alpha Session" }),
).toBeInTheDocument(),
);
openSearch();
fireEvent.change(screen.getByRole("textbox"), {
target: { value: "Alpha" },
});
await waitFor(() =>
expect(screen.getAllByText("Alpha Session")).toHaveLength(2),
);
fireEvent.click(screen.getByRole("button", { name: /删除会话/i }));
const dialog = screen.getByTestId("confirm-dialog");
fireEvent.click(within(dialog).getByRole("button", { name: /删除会话/i }));
await waitFor(() =>
expect(screen.queryByText("Alpha Session")).not.toBeInTheDocument(),
);
expect(screen.getByText("sessionManager.selectSession")).toBeInTheDocument();
expect(
screen.queryByText("sessionManager.emptySession"),
).not.toBeInTheDocument();
expect(toastErrorMock).not.toHaveBeenCalled();
expect(toastSuccessMock).toHaveBeenCalled();
});
});
+79
View File
@@ -0,0 +1,79 @@
import { act, renderHook, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { useCodexCommonConfig } from "@/components/providers/forms/hooks/useCodexCommonConfig";
import { useGeminiCommonConfig } from "@/components/providers/forms/hooks/useGeminiCommonConfig";
const getCommonConfigSnippetMock = vi.fn();
const setCommonConfigSnippetMock = vi.fn();
const extractCommonConfigSnippetMock = vi.fn();
vi.mock("@/lib/api", () => ({
configApi: {
getCommonConfigSnippet: (...args: unknown[]) =>
getCommonConfigSnippetMock(...args),
setCommonConfigSnippet: (...args: unknown[]) =>
setCommonConfigSnippetMock(...args),
extractCommonConfigSnippet: (...args: unknown[]) =>
extractCommonConfigSnippetMock(...args),
},
}));
describe("common config snippet saving", () => {
beforeEach(() => {
getCommonConfigSnippetMock.mockResolvedValue("");
setCommonConfigSnippetMock.mockResolvedValue(undefined);
extractCommonConfigSnippetMock.mockResolvedValue("");
});
it("does not persist an invalid Codex common config snippet", async () => {
const onConfigChange = vi.fn();
const { result } = renderHook(() =>
useCodexCommonConfig({
codexConfig: "model = \"gpt-5\"",
onConfigChange,
}),
);
await waitFor(() => expect(result.current.isLoading).toBe(false));
let saved = false;
act(() => {
saved = result.current.handleCommonConfigSnippetChange(
"base_url = https://bad.example/v1",
);
});
expect(saved).toBe(false);
expect(setCommonConfigSnippetMock).not.toHaveBeenCalled();
expect(onConfigChange).not.toHaveBeenCalled();
expect(result.current.commonConfigError).toContain("invalid value");
});
it("does not persist an invalid Gemini common config snippet", async () => {
const onEnvChange = vi.fn();
const { result } = renderHook(() =>
useGeminiCommonConfig({
envValue: "",
onEnvChange,
envStringToObj: () => ({}),
envObjToString: () => "",
}),
);
await waitFor(() => expect(result.current.isLoading).toBe(false));
let saved = false;
act(() => {
saved = result.current.handleCommonConfigSnippetChange(
JSON.stringify({ GEMINI_MODEL: 123 }),
);
});
expect(saved).toBe(false);
expect(setCommonConfigSnippetMock).not.toHaveBeenCalled();
expect(onEnvChange).not.toHaveBeenCalled();
expect(result.current.commonConfigError).toBe(
"geminiConfig.commonConfigInvalidValues",
);
});
});
+118
View File
@@ -0,0 +1,118 @@
import type { ReactNode } from "react";
import { renderHook, act, waitFor } from "@testing-library/react";
import { QueryClientProvider } from "@tanstack/react-query";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { useProxyStatus } from "@/hooks/useProxyStatus";
import { createTestQueryClient } from "../utils/testQueryClient";
const toastSuccessMock = vi.fn();
const toastErrorMock = vi.fn();
const invokeMock = vi.fn();
vi.mock("sonner", () => ({
toast: {
success: (...args: unknown[]) => toastSuccessMock(...args),
error: (...args: unknown[]) => toastErrorMock(...args),
},
}));
vi.mock("@tauri-apps/api/core", () => ({
invoke: (...args: unknown[]) => invokeMock(...args),
}));
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string, options?: Record<string, unknown>) => {
if (key === "proxy.server.started") {
return `代理服务已启动 - ${options?.address}:${options?.port}`;
}
if (typeof options?.defaultValue === "string") {
return options.defaultValue;
}
return key;
},
}),
}));
interface WrapperProps {
children: ReactNode;
}
function createWrapper() {
const queryClient = createTestQueryClient();
const wrapper = ({ children }: WrapperProps) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
return { wrapper, queryClient };
}
describe("useProxyStatus", () => {
beforeEach(() => {
invokeMock.mockReset();
toastSuccessMock.mockReset();
toastErrorMock.mockReset();
invokeMock.mockImplementation((command: string) => {
if (command === "get_proxy_status") {
return Promise.resolve({
running: false,
address: "127.0.0.1",
port: 15721,
active_connections: 0,
total_requests: 0,
success_requests: 0,
failed_requests: 0,
success_rate: 0,
uptime_seconds: 0,
current_provider: null,
current_provider_id: null,
last_request_at: null,
last_error: null,
failover_count: 0,
});
}
if (command === "get_proxy_takeover_status") {
return Promise.resolve({
claude: false,
codex: false,
gemini: false,
opencode: false,
openclaw: false,
});
}
if (command === "start_proxy_server") {
return Promise.resolve({
address: "127.0.0.1",
port: 15721,
started_at: "2026-03-10T00:00:00Z",
});
}
return Promise.resolve(null);
});
});
it("shows interpolated address and port after proxy server starts", async () => {
const { wrapper } = createWrapper();
const { result } = renderHook(() => useProxyStatus(), { wrapper });
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
await act(async () => {
await result.current.startProxyServer();
});
expect(toastSuccessMock).toHaveBeenCalledWith(
"代理服务已启动 - 127.0.0.1:15721",
{ closeButton: true },
);
});
});
+99 -3
View File
@@ -22,17 +22,21 @@ describe("Codex TOML utils", () => {
expect(extractCodexModelName(output)).toBe("gpt-5-codex");
});
it("removes model line when set to empty", () => {
it("removes only the top-level model line when set to empty", () => {
const input = [
'model_provider = "openai"',
'base_url = "https://api.example.com/v1"',
'model = "gpt-5-codex"',
"",
"[profiles.default]",
'model = "profile-model"',
"",
].join("\n");
const output = setCodexModelName(input, "");
expect(output).not.toMatch(/^\s*model\s*=/m);
expect(output).not.toMatch(/^model\s*=\s*"gpt-5-codex"$/m);
expect(output).toMatch(/^\[profiles\.default\]\nmodel = "profile-model"$/m);
expect(extractCodexModelName(output)).toBeUndefined();
expect(extractCodexBaseUrl(output)).toBe("https://api.example.com/v1");
});
@@ -51,5 +55,97 @@ describe("Codex TOML utils", () => {
const output2 = setCodexModelName(output1, " new-model \n");
expect(extractCodexModelName(output2)).toBe("new-model");
});
});
it("reads and writes base_url in the active provider section", () => {
const input = [
'model_provider = "custom"',
'model = "gpt-5.4"',
"",
"[model_providers.custom]",
'name = "custom"',
'wire_api = "responses"',
"",
"[profiles.default]",
'approval_policy = "never"',
"",
].join("\n");
const output = setCodexBaseUrl(input, "https://api.example.com/v1");
expect(output).toContain(
'[model_providers.custom]\nname = "custom"\nwire_api = "responses"\nbase_url = "https://api.example.com/v1"',
);
expect(extractCodexBaseUrl(output)).toBe("https://api.example.com/v1");
});
it("recovers a single misplaced base_url from another section", () => {
const input = [
'model_provider = "custom"',
'model = "gpt-5.4"',
"",
"[model_providers.custom]",
'name = "custom"',
'wire_api = "responses"',
"",
"[profiles.default]",
'approval_policy = "never"',
'base_url = "https://wrong.example/v1"',
"",
].join("\n");
expect(extractCodexBaseUrl(input)).toBe("https://wrong.example/v1");
const output = setCodexBaseUrl(input, "https://fixed.example/v1");
expect(output).toContain(
'[model_providers.custom]\nname = "custom"\nwire_api = "responses"\nbase_url = "https://fixed.example/v1"',
);
expect(output).not.toContain("https://wrong.example/v1");
expect(output.match(/base_url\s*=/g)).toHaveLength(1);
});
it("does not treat mcp_servers base_url as provider base_url", () => {
const input = [
'model_provider = "azure"',
'model = "gpt-4"',
"",
"[model_providers.azure]",
'name = "Azure OpenAI"',
'wire_api = "responses"',
"",
"[mcp_servers.my_server]",
'base_url = "http://localhost:8080"',
"",
].join("\n");
expect(extractCodexBaseUrl(input)).toBeUndefined();
const output = setCodexBaseUrl(input, "https://new.azure/v1");
expect(output).toContain(
'[model_providers.azure]\nname = "Azure OpenAI"\nwire_api = "responses"\nbase_url = "https://new.azure/v1"',
);
expect(output).toContain(
'[mcp_servers.my_server]\nbase_url = "http://localhost:8080"',
);
});
it("reads model only from the top-level config", () => {
const input = [
'model_provider = "custom"',
"",
"[profiles.default]",
'model = "profile-model"',
"",
].join("\n");
expect(extractCodexModelName(input)).toBeUndefined();
});
it("handles single-quoted values", () => {
const input = "base_url = 'https://api.example.com/v1'\nmodel = 'gpt-5'\n";
expect(extractCodexBaseUrl(input)).toBe("https://api.example.com/v1");
expect(extractCodexModelName(input)).toBe("gpt-5");
});
});