Merge branch 'main' into feat/proxy-url-refactor-full-url-mode

# Conflicts:
#	src/components/providers/forms/ProviderForm.tsx
This commit is contained in:
YoVinchen
2026-02-27 10:44:34 +08:00
62 changed files with 2391 additions and 2155 deletions
+1
View File
@@ -23,3 +23,4 @@ nul
flatpak/cc-switch.deb
flatpak-build/
flatpak-repo/
.worktrees/
+178 -1
View File
@@ -7,9 +7,186 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
---
## [3.11.0] - 2026-02-26
### Feature Release
This release introduces **OpenClaw** as the fifth supported application, a full **Session Manager** for browsing conversation history across all apps, an independent **Backup Management** panel, **Oh My OpenCode (OMO)** integration, and 50+ other features, fixes, and improvements across 147 commits.
**Stats**: 147 commits | 274 files changed | +32,179 insertions | -5,467 deletions
### Added
#### OpenClaw Support (New Application)
- **OpenClaw Integration**: Full management support for OpenClaw as the fifth application in CC Switch, including provider switching, configuration panels (Env / Tools / Agents Defaults), Workspace file management (HEARTBEAT / BOOTSTRAP / BOOT), daily memory files, and additive overlay mode
- **OpenClaw Provider Presets**: 13+ built-in provider presets with brand icon and complete i18n (zh/en/ja)
- **OpenClaw Form Fields**: Dedicated provider form with providerKey input, model allowlist auto-registration, and default model button
- **OpenClaw Config Panels**: Env editor, Tools editor, and Agents Defaults editor backed by JSON5 read/write (`openclaw_config.rs`)
#### Session Manager
- **Session Manager**: Browse and search conversation history for Claude Code, Codex, Gemini CLI, OpenCode, and OpenClaw with table-of-contents navigation and in-session search
- **Session App Filter**: Auto-filter sessions by current app when entering the session page
- **Session Performance**: Parallel directory scanning and head-tail JSONL reading for faster session list loading
#### Backup Management
- **Backup Panel**: Independent backup management panel with configurable backup policy (max count, auto-cleanup) and backup rename support
- **Periodic Backup**: Hourly automatic backup timer during runtime
- **Pre-Migration Backup**: Automatic backup before database schema migrations with backfill warning
- **Delete Backup**: Delete individual backup files with confirmation dialog
- **Backup Time Fix**: Use local time instead of UTC for backup file names
#### Oh My OpenCode (OMO)
- **OMO Integration**: Full Oh My OpenCode config file management with agent model selection, category configuration, and recommended model fill
- **OMO Slim**: Lightweight oh-my-opencode-slim mode support with OmoVariant parameterization
- **OMO Cross-Exclusion**: Enforce OMO ↔ OMO Slim mutual exclusion at the database level
#### Workspace
- **Daily Memory Search**: Full-text search across daily memory files with date-sorted display
- **Clickable Paths**: Directory paths in workspace panels are now clickable; renamed “Today's Note” to “Add Memory”
- **Workspace Files Panel**: Manage bootstrap markdown files for OpenClaw (HEARTBEAT / BOOTSTRAP / BOOT types)
#### Provider Presets
- **AWS Bedrock**: Support for AKSK and API Key authentication modes (Claude and OpenCode)
- **SSAI Code**: Partner provider preset across all five apps
- **CrazyRouter**: Partner provider preset with custom icon
- **AICoding**: Partner provider preset with i18n promotion text
- **Bailian**: Renamed from Qwen Coder with new icon; updated domestic model providers to latest versions
#### Proxy & Network
- **Thinking Budget Rectifier**: New rectifier for thinking budget parameters with dedicated module (`thinking_budget_rectifier.rs`)
- **WebDAV Auto Sync**: Automatic periodic sync with large file protection mechanism
#### UI & UX
- **Theme Animation**: Circular reveal animation when toggling between light and dark themes
- **Claude Quick Toggles**: Quick toggle switches in the Claude config JSON editor for common settings
- **Dynamic Endpoint Hint**: Context-aware hint text in endpoint input based on API format selection
- **AppSwitcher Auto Compact**: Automatically collapse to compact mode based on available width, with smooth transition animation
- **App Transition**: Fade-in/fade-out animation when switching between OpenClaw and other apps
- **Silent Startup Conditional**: Show silent startup option only when launch-on-startup is enabled
#### Settings & Environment
- **First-Run Confirmation**: Confirmation dialogs for proxy and usage features on first use
- **Local Proxy Toggle**: `enableLocalProxy` setting to control proxy UI visibility on the home page
- **Environment Check**: More granular local environment detection (installed CLI tool versions, Volta path detection)
#### Usage & Pricing
- **Usage Dashboard Enhancement**: Auto-refresh control, robust formatting, and request log table improvements
- **New Model Pricing**: Added pricing data for claude-opus-4-6 and gpt-5.3-codex with incremental data seeding
### Changed
#### Architecture
- **Partial Key-Field Merging (⚠️ Breaking)**: Provider switching now uses partial key-field merging instead of full config overwrite, preserving user's non-provider settings (plugins, MCP, permissions). The "Common Config Snippet" feature has been removed as it is no longer needed. Removes 6 frontend files and ~150 lines of backend dead code (#1098)
- **Manual Import**: Replaced auto-import on startup with manual “Import Current Config” button in empty state, reducing ~47 lines of startup code
- **OMO Variant Parameterization**: Eliminated ~250 lines of OMO/OMO Slim code duplication via `OmoVariant` struct with STANDARD/SLIM constants
- **OMO Common Config Removal**: Removed the two-layer merge system for OMO common config (-1,733 lines across 21 files)
#### Code Quality
- **ProviderForm Decomposition**: Extracted ProviderForm.tsx from 2,227 lines to 1,526 lines by splitting into 5 focused modules (opencodeFormUtils, useOmoModelSource, useOpencodeFormState, useOmoDraftState, useOpenclawFormState)
- **Shared MCP/Skills Components**: Extracted AppCountBar, AppToggleGroup, and ListItemRow shared components to eliminate duplication across MCP and Skills panels
- **OpenClaw TanStack Query Migration**: Migrated Env, Tools, and AgentsDefaults panels from manual useState/useEffect to centralized TanStack Query hooks
#### Settings Layout
- **Proxy Tab**: Split Advanced tab into dedicated Proxy tab (local proxy, failover, rectifiers, global outbound proxy); moved pricing config to Usage dashboard as collapsible accordion. SettingsPage reduced from ~716 to ~426 lines with 5-tab layout: General | Proxy | Advanced | Usage | About
- **Data Section Split**: Split data accordion into Import/Export and Cloud Sync sections for better discoverability
#### Terminal & Config
- **Unified Terminal Selection**: Consolidated terminal preference to global settings; added WezTerm support and terminal name mapping (iterm2 → iterm)
- **OpenClaw Agents Panel**: Primary model field set to read-only; detailed model fields (context window, max tokens, reasoning, cost) moved to advanced options
- **Claude Model Update**: Updated Claude model references from 4.5 to 4.6 across all provider presets
### Fixed
- **Windows Home Dir Regression**: Prevent providers/settings “disappearing” after upgrading from v3.10.2 → v3.10.3 when `HOME` differs from the real user profile directory; restore default path resolution and auto-detect the v3.10.3 legacy database location.
#### Critical
- **Windows Home Dir Regression**: Restored default home directory resolution on Windows to prevent providers/settings “disappearing” when `HOME` env var differs from the real user profile directory (Git/MSYS environments); auto-detects v3.10.3 legacy database location
- **Linux White Screen**: Disabled WebKitGTK hardware acceleration on AMD GPUs (Cezanne/Radeon Vega) to prevent EGL initialization failure causing blank screen on startup
- **OpenAI Beta Parameter**: Stopped appending `?beta=true` to OpenAI Chat Completions endpoints, fixing request failures for Nvidia and other `apiFormat=”openai_chat”` providers
- **Health Check Auth Mode**: Health check now respects provider's auth_mode setting instead of always using x-api-key header
#### Provider & Preset
- **OpenClaw /v1 Prefix**: Removed /v1 prefix from OpenClaw anthropic-messages presets to prevent double path (/v1/v1/messages) with Anthropic SDK auto-append
- **Opus Pricing**: Corrected Opus pricing from $15/$75 to $5/$25 and upgraded model ID to claude-opus-4-6
- **AIGoCode URLs**: Unified API base URL to https://api.aigocode.com across all apps; removed trailing /v1 suffix
- **Zhipu GLM**: Removed outdated partner status from Claude, OpenCode, and OpenClaw presets
- **API Key Visibility**: Restored API Key input field when creating new Claude providers (was incorrectly hidden for non-cloud_provider categories)
#### OMO / OMO Slim
- **OMO Slim Category Checks**: Added missing omo-slim category checks across add/form/mutation paths
- **OMO Slim Cache Invalidation**: Invalidate OMO Slim query cache after provider mutations to prevent stale UI state
- **OMO Recommended Models**: Synced agent/category recommended models with upstream sources; fixed provider/model format to pure model IDs
- **OMO Fill Feedback**: Added toast feedback when “Fill Recommended” button silently fails
- **OMO Last-Provider Restriction**: Removed last-provider deletion restriction for OMO/OMO Slim plugins
- **OpenCode Model Validation**: Reject saving OpenCode providers without at least one configured model
#### OpenClaw
- **OpenClaw P0-P3 Fixes**: Fixed 25 missing i18n keys, replaced key={index} with stable crypto.randomUUID(), excluded openclaw from ProxyToggle/FailoverToggle, added deep link merge_additive_config(), unified serde(flatten) naming, added directory existence checks, removed dead code, added duplicate key validation
- **OpenClaw Robustness**: Fixed EnvPanel visibleKeys using entry key names instead of array indices; added NaN guards; validated provider ID and model before import
- **OpenClaw i18n Dedup**: Merged duplicate openclaw i18n keys to restore provider form translations
#### Platform
- **Window Flash**: Prevented window flicker on silent startup (Windows)
- **Title Bar Theme**: Title bar now follows dark/light mode theme changes
- **Skills Path Separator**: Fixed path separator matching for skill installation status on Windows (supports both `/` and `\`)
- **WSL Conditional Compilation**: Added `#[cfg(target_os = “windows”)]` to WSL helper functions to eliminate dead_code warnings on non-Windows platforms
#### UI
- **Toolbar Clipping**: Removed toolbar height limit that was clipping AppSwitcher
- **Update Badge**: Show update badge instead of green check when a newer version is available
- **Session Button Visibility**: Only show Session Manager button for Claude and Codex apps
- **Directory Spacing**: Added vertical spacing between directory setting sections
- **Dark Mode Cards**: Unified SQL import/export card styling in dark mode
- **OpenClaw Scroll**: Enabled scrolling for OpenClaw configuration panel content
#### i18n & Localization
- **Session Manager i18n**: Replaced hardcoded Chinese strings with i18n keys for relative time, role labels, and UI elements
- **OpenClaw Default Model Label**: Renamed “Enable/Default” to “Set as Default / Current Default” with wider button
- **Daily Memory Sort**: Sort daily memory files by filename date (YYYY-MM-DD.md) instead of modification time
- **Backup Name i18n**: Use local time for backup file names
#### Other
- **Skill Doc URL**: Use actual branch from download_repo for documentation URL; switched from /tree/ to /blob/ pointing to SKILL.md
- **OpenCode Install Detection**: Added install.sh priority paths (OPENCODE_INSTALL_DIR > XDG_BIN_DIR > ~/bin > ~/.opencode/bin) with path dedup and cross-platform executable candidates
- **Provider Auto-Import**: Removed auto-import side effect from useProvidersQuery queryFn; users now trigger import manually via empty state button
- **Manual Backup Validation**: Treat missing database file as error during manual backup to prevent false success toast
### Performance
- **Session Panel Loading**: Parallel directory scanning and head-tail JSONL reading for Codex, OpenClaw, and OpenCode session providers
- **Query Cache Cleanup**: Removed unnecessary TanStack Query cache overhead for Tauri local IPC calls
### Documentation
- **Sponsors**: Added/updated SSSAiCode, Crazyrouter, AICoding, Right Code, and MiniMax sponsor entries across all README languages
- **User Manual**: Added user manual documentation (#979)
### Maintenance
- **Pre-Release Cleanup**: Removed debug logs, fixed clippy warnings, added missing Japanese translations, and formatted code
- **UI Exclusions**: Hidden MCP, Skills, proxy/pricing, stream check, and model test panels for OpenClaw where not applicable
---
+302
View File
@@ -0,0 +1,302 @@
# CC Switch v3.11.0
> OpenClaw Support, Session Manager, Backup Management & 50+ Improvements
**[中文版 →](release-note-v3.11.0-zh.md) | [日本語版 →](release-note-v3.11.0-ja.md)**
---
## Overview
CC Switch v3.11.0 is a major update that adds full management support for **OpenClaw** as the fifth application, introduces a new **Session Manager** and **Backup Management** feature. Additionally, **Oh My OpenCode (OMO) integration**, the **partial key-field merging** architecture upgrade for provider switching, **settings page refactoring**, and many other improvements make the overall experience more polished.
**Release Date**: 2026-02-26
**Update Scale**: 147 commits | 274 files changed | +32,179 / -5,467 lines
---
## Highlights
- **OpenClaw Support**: Fifth managed application with 13 provider presets, Env/Tools/AgentsDefaults config editors, and Workspace file management
- **Session Manager**: Browse conversation history across all five apps with table-of-contents navigation and in-session search
- **Backup Management**: Independent backup panel with configurable policies, periodic backups, and pre-migration auto-backup
- **Oh My OpenCode Integration**: Full OMO config management with OMO Slim lightweight mode support
- **Partial Key-Field Merging (⚠️ Breaking Change)**: Provider switching now only replaces provider-related fields, preserving all other settings; the "Common Config Snippet" feature has been removed
- **Settings Page Refactoring**: 5-tab layout with ~40% code reduction
- **6 New Provider Presets**: AWS Bedrock, SSAI Code, CrazyRouter, AICoding, and more
- **Thinking Budget Rectifier**: Fine-grained thinking budget control
- **Theme Switch Animation**: Circular reveal transition animation
- **WebDAV Auto Sync**: Automatic sync with large file protection
---
## Main Features
### OpenClaw Support (New Fifth App)
Full management support for OpenClaw, the fifth managed application following Claude Code, Codex, Gemini CLI, and OpenCode.
- **Provider Management**: Add, edit, switch, and delete OpenClaw providers with 13 built-in presets
- **Config Editors**: Three dedicated panels for Env (environment variables), Tools, and AgentsDefaults
- **Workspace Panel**: HEARTBEAT/BOOTSTRAP/BOOT file management and daily memory
- **Additive Overlay Mode**: Support config overlay instead of overwrite
- **Default Model Button**: One-click to fill recommended models; auto-register suggested models to allowlist when adding providers
- **Brand & Interaction**: Dedicated brand icon, fade-in/fade-out transition animation when switching apps
- **Deep Link Support**: Import OpenClaw provider configurations via URL
- **Full Internationalization**: Complete Chinese/English/Japanese support
### Session Manager
A brand-new session manager to browse and search conversation history.
- Browse conversation history across Claude Code, Codex, Gemini CLI, OpenCode, and OpenClaw (#867, thanks @TinsFox)
- Table-of-contents navigation and in-session search
- Auto-filter by current app when entering the session page
- Parallel directory scanning + head-tail JSONL reading for optimized loading performance
### Backup Management
An independent backup management panel for better data safety.
- Configurable backup policy: maximum backup count and auto-cleanup rules
- Hourly automatic backup timer during runtime
- Auto-backup before database schema migrations with backfill warning
- Support backup rename and deletion (with confirmation dialog)
- Backup filenames use local time for better clarity
### Oh My OpenCode (OMO) Integration
Full Oh My OpenCode config file management.
- Agent model selection, category configuration, and recommended model fill (#972, thanks @yovinchen)
- Improved agent model selection UX with lowercase key fix (#1004, thanks @yovinchen)
- OMO Slim lightweight mode support
- OMO ↔ OMO Slim mutual exclusion (enforced at database level)
### Workspace
- Full-text search across daily memory files, sorted by date
- Clickable directory paths for quick file location access
### Toolbar
- AppSwitcher auto-collapses to compact mode based on available width
- Smooth transition animation for compact mode toggle
### Settings
- First-use confirmation dialogs for proxy and usage features to prevent accidental operations
- New `enableLocalProxy` switch to control proxy UI visibility on home page
- More granular local environment checks: CLI tool version detection (#870, thanks @kv-chiu), Volta path detection (#969, thanks @myjustify)
### Provider Presets
- **AWS Bedrock**: Support for AKSK and API Key authentication modes (#1047, thanks @keithyt06)
- **SSAI Code**: Partner preset across all five apps
- **CrazyRouter**: Partner preset with dedicated icon
- **AICoding**: Partner preset with i18n promotion text
- Updated domestic model provider presets to latest versions
- Renamed Qwen Coder to Bailian (#965, thanks @zhu-jl18)
### Other New Features
- **Thinking Budget Rectifier**: Fine-grained thinking budget allocation control (#1005, thanks @yovinchen)
- **WebDAV Auto Sync**: Automatic sync with large file protection (#923, thanks @clx20000410; #1043, thanks @SaladDay)
- **Theme Switch Animation**: Circular reveal transition for a smoother visual experience (#905, thanks @funnytime75)
- **Claude Config Editor Quick Toggles**: Quick toggle switches for common settings (#1012, thanks @JIA-ss)
- **Dynamic Endpoint Hint**: Context-aware hint text based on API format selection (#860, thanks @zhu-jl18)
- **Usage Dashboard Enhancement**: Auto-refresh control and robust formatting (#942, thanks @yovinchen)
- **New Pricing Data**: claude-opus-4-6 and gpt-5.3-codex (#943, thanks @yovinchen)
- **Silent Startup Optimization**: Silent startup option only shown when launch-on-startup is enabled
---
## Architecture Improvements
### Partial Key-Field Merging (⚠️ Breaking Change)
Provider switching now uses partial key-field merging instead of full config overwrite (#1098).
**Before**: Switching providers overwrote the entire `settings_config` to the live config file. This meant that any non-provider settings the user manually added to the live file (plugins, MCP config, permissions, etc.) would be lost on every switch. To work around this, previous versions offered a "Common Config Snippet" feature that let users define shared config to be merged on every switch.
**After**: Switching providers now only replaces provider-related key-values (API keys, endpoints, models, etc.), leaving all other settings intact. The "Common Config Snippet" feature is therefore no longer needed and has been removed.
**Impact & Migration**:
- If you **didn't use** Common Config Snippets, this change is fully transparent — switching just works better now
- If you **used** Common Config Snippets to preserve custom settings (MCP config, permissions, etc.), those settings are now automatically preserved during switches — no action needed
- If you used Common Config Snippets for other purposes (e.g., injecting extra config on every switch), please manually add those settings to your live config file after upgrading
This refactoring removed 6 frontend files (3 components + 3 hooks) and ~150 lines of backend dead code.
### Manual Import Replaces Auto-Import
Startup no longer auto-imports external configurations. Users now click "Import Current Config" manually, preventing accidental data overwrites.
### OmoVariant Parameterization
Eliminated ~250 lines of duplicated code in the OMO module via `OmoVariant` struct parameterization.
### OMO Common Config Removal
Removed the two-layer merge system, reducing ~1,733 lines of code and simplifying the architecture.
### ProviderForm Decomposition
Reduced ProviderForm component from 2,227 lines to 1,526 lines by extracting 5 independent modules (opencodeFormUtils, useOmoModelSource, useOpencodeFormState, useOmoDraftState, useOpenclawFormState), significantly improving maintainability.
### Shared MCP/Skills Components
Extracted AppCountBar, AppToggleGroup, and ListItemRow shared components to reduce duplication across MCP and Skills panels (#897, thanks @PeanutSplash).
### Settings Page Refactoring
Refactored settings page to a 5-tab layout (General | Proxy | Advanced | Usage | About), reducing SettingsPage code from ~716 to ~426 lines.
### Other Improvements
- Unified terminal selection via global settings with WezTerm support added
- Updated Claude model references from 4.5 to 4.6
---
## Bug Fixes
### Critical Fixes
- **Windows Home Dir Regression**: Restored default home directory resolution to prevent providers/settings "disappearing" when `HOME` env var differs from the real user profile directory in Git/MSYS environments
- **Linux White Screen**: Disabled WebKitGTK hardware acceleration on AMD GPUs (Cezanne/Radeon Vega) to prevent blank screen on startup (#986, thanks @ThendCN)
- **OpenAI Beta Parameter**: Stopped appending `?beta=true` to `/v1/chat/completions` endpoints, fixing request failures for Nvidia and other `apiFormat="openai_chat"` providers (#1052, thanks @jnorthrup)
- **Health Check Auth**: Health check now respects provider's `auth_mode` setting, preventing failures for proxy services that only support Bearer authentication (#824, thanks @Jassy930)
### Provider Preset Fixes
- Fixed OpenClaw `/v1` prefix causing double path (/v1/v1/messages)
- Corrected Opus pricing ($15/$75 → $5/$25) and upgraded to 4.6
- Unified AIGoCode URL to `https://api.aigocode.com` across all apps
- Removed outdated partner status from Zhipu GLM presets
- Restored API Key input visibility when creating new Claude providers
- Hide quick toggles for non-active providers, show context-aware JSON editor hints
### OMO Fixes
- Added missing omo-slim category checks across add/form/mutation paths
- Fixed OMO Slim query cache invalidation after provider mutations
- Synced OMO agent/category recommended models with upstream sources
- Added toast feedback for "Fill Recommended" button silent failures
- Removed last-provider deletion restriction for OMO/OMO Slim
- Reject saving OpenCode providers without configured models (#932, thanks @yovinchen)
### OpenClaw Fixes
- Fixed 25 missing i18n keys, replaced key={index} with stable IDs, added deep link additive merge, and other code review issues
- Enhanced EnvPanel robustness (NaN guards, entry key names instead of array indices)
- Merged duplicate i18n keys to restore provider form translations
### Platform Fixes
- Windows silent startup window flicker (#901, thanks @funnytime75)
- Title bar dark mode theme following (#903, thanks @funnytime75)
- Windows Skills path separator matching (#868, thanks @stmoonar)
- WSL helper functions conditional compilation
### UI Fixes
- Toolbar height clipping causing AppSwitcher to be obscured
- Show update badge instead of green checkmark when newer version available
- Session Manager button only visible for Claude/Codex apps
- Unified SQL import/export card dark mode styling (#1067, thanks @SaladDay)
### Other Fixes
- Replaced hardcoded Chinese strings in Session Manager with i18n keys
- Fixed Skill documentation URL branch and path resolution (#977, thanks @yovinchen)
- Added missing OpenCode install.sh installation path detection (#988, thanks @zhu-jl18)
- Fixed Skill ZIP symlink resolution (#1040, thanks @yovinchen)
- Added missing OpenCode checkbox in MCP add/edit form (#1026, thanks @yovinchen)
- Removed auto-import side effect from useProvidersQuery queryFn
---
## Performance
- Parallel directory scanning + head-tail JSONL reading for session panel, significantly improving session list loading speed
- Removed unnecessary TanStack Query cache overhead for Tauri local IPC calls
---
## Documentation
- Sponsor updates: SSSAiCode, Crazyrouter, AICoding, Right Code, MiniMax
- Added user manual documentation (#979, thanks @yovinchen)
---
## Notes & Considerations
- **OpenClaw is a newly supported app**: OpenClaw CLI must be installed first to use related features.
- **⚠️ Common Config Snippet feature has been removed**: Since provider switching now uses partial key-field merging (only replacing API keys, endpoints, models, etc.), user's other settings are automatically preserved, making Common Config Snippets unnecessary. See the "Architecture Improvements" section above for migration details.
- **Auto-import changed to manual**: External configurations are no longer auto-imported on startup. Click "Import Current Config" manually when needed.
- **OMO and OMO Slim are mutually exclusive**: Only one can be active at a time. Switching to one automatically disables the other.
- **Backup is enabled by default**: Automatic hourly backup during runtime. Adjust the policy in the Backup panel.
---
## Special Thanks
Thanks to all contributors for their contributions to this release!
@TinsFox @keithyt06 @kv-chiu @SaladDay @jnorthrup @JIA-ss @clx20000410 @ThendCN @yovinchen @zhu-jl18 @myjustify @funnytime75 @PeanutSplash @Jassy930 @stmoonar
---
## 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.11.0-Windows.msi` | **Recommended** - MSI installer with auto-update |
| `CC-Switch-v3.11.0-Windows-Portable.zip` | Portable version, extract and run, no registry write |
### macOS
| File | Description |
| -------------------------------- | -------------------------------------------------------------------- |
| `CC-Switch-v3.11.0-macOS.zip` | **Recommended** - Extract and drag to Applications, Universal Binary |
| `CC-Switch-v3.11.0-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` |
+302
View File
@@ -0,0 +1,302 @@
# CC Switch v3.11.0
> OpenClaw サポート、セッションマネージャー、バックアップ管理と 50 以上の改善
**[中文版 →](release-note-v3.11.0-zh.md) | [English →](release-note-v3.11.0-en.md)**
---
## 概要
CC Switch v3.11.0 は大規模なアップデートです。5番目のアプリケーション **OpenClaw** の完全管理サポートを追加し、新しい**セッションマネージャー**と**バックアップ管理**機能を導入しました。さらに、**Oh My OpenCode (OMO) 統合**、プロバイダー切り替えの**部分キーフィールドマージ**アーキテクチャアップグレード、**設定ページのリファクタリング**など、多数の改善により全体的な体験がさらに向上しました。
**リリース日**: 2026-02-26
**更新規模**: 147 commits | 274 files changed | +32,179 / -5,467 lines
---
## ハイライト
- **OpenClaw サポート**: 5番目の管理対象アプリ、13 のプロバイダープリセット、Env/Tools/AgentsDefaults 設定エディター、Workspace ファイル管理
- **セッションマネージャー**: 5つのアプリの会話履歴を閲覧、目次ナビゲーションとセッション内検索
- **バックアップ管理**: 独立バックアップパネル、設定可能なポリシー、定期バックアップ、マイグレーション前自動バックアップ
- **Oh My OpenCode 統合**: 完全な OMO 設定管理、OMO Slim 軽量モードサポート
- **部分キーフィールドマージ(⚠️ 破壊的変更)**: プロバイダー切り替え時にプロバイダー関連フィールドのみ置換し、その他の設定を保持;「共通設定スニペット」機能は削除されました
- **設定ページリファクタリング**: 5タブレイアウト、コード量約 40% 削減
- **6つの新プロバイダープリセット**: AWS Bedrock、SSAI Code、CrazyRouter、AICoding など
- **Thinking Budget Rectifier**: より精密な thinking budget 制御
- **テーマ切り替えアニメーション**: 円形リビール遷移アニメーション
- **WebDAV 自動同期**: 自動同期と大容量ファイル保護
---
## 主な機能
### OpenClaw サポート(新しい5番目のアプリ)
Claude Code、Codex、Gemini CLI、OpenCode に続く5番目の管理対象アプリケーションとして OpenClaw の完全管理サポートを追加しました。
- **プロバイダー管理**: OpenClaw プロバイダーの追加、編集、切り替え、削除、13 の内蔵プリセット
- **設定エディター**: Env(環境変数)、Tools(ツール)、AgentsDefaults(エージェントデフォルト)の3つの専用パネル
- **Workspace パネル**: HEARTBEAT/BOOTSTRAP/BOOT ファイル管理とデイリーメモリ
- **Additive オーバーレイモード**: 上書きではなく設定の重ね合わせをサポート
- **デフォルトモデルボタン**: ワンクリックで推奨モデルを入力、プロバイダー追加時に候補モデルを allowlist に自動登録
- **ブランドとインタラクション**: 専用ブランドアイコン、アプリ切り替えフェード遷移アニメーション
- **ディープリンクサポート**: URL 経由で OpenClaw プロバイダー設定をインポート
- **完全な国際化**: 中/英/日 三言語完全対応
### セッションマネージャー
会話履歴を閲覧・検索できる新しいセッションマネージャーです。
- Claude Code、Codex、Gemini CLI、OpenCode、OpenClaw の5つのアプリの会話履歴を閲覧(#867@TinsFox に感謝)
- 目次ナビゲーションとセッション内検索
- セッションページに入ると現在のアプリで自動フィルター
- 並列ディレクトリスキャン + ヘッドテール JSONL 読み取りで読み込みパフォーマンスを最適化
### バックアップ管理
データの安全性を高める独立バックアップ管理パネルです。
- 設定可能なバックアップポリシー: 最大バックアップ数、自動クリーンアップルール
- ランタイム中の1時間ごとの定期自動バックアップ
- データベースマイグレーション前の自動バックアップ、バックフィル警告プロンプト
- バックアップのリネームと削除をサポート(確認ダイアログ付き)
- バックアップファイル名にローカルタイムを使用、より直感的に
### Oh My OpenCode (OMO) 統合
完全な Oh My OpenCode 設定ファイル管理です。
- エージェントモデル選択、カテゴリ設定、推奨モデル入力(#972@yovinchen に感謝)
- エージェントモデル選択 UX の改善、lowercase key 問題の修正(#1004@yovinchen に感謝)
- OMO Slim 軽量モードサポート
- OMO と OMO Slim の相互排他(データベースレベルで一貫性を保証)
### ワークスペース
- デイリーメモリファイルの全文検索、日付順ソート
- ディレクトリパスがクリック可能に、ファイル位置をすばやく開く
### ツールバー
- AppSwitcher がウィンドウ幅に応じて自動的にコンパクトモードに折りたたみ
- コンパクトモード切り替えのスムーズ遷移アニメーション
### 設定
- プロキシと使用量機能に初回使用確認ダイアログを追加、誤操作を防止
- `enableLocalProxy` スイッチを追加、ホーム画面のプロキシ UI 表示を制御
- より詳細なローカル環境チェック: CLI ツールバージョン検出(#870@kv-chiu に感謝)、Volta パス検出(#969@myjustify に感謝)
### プロバイダープリセット
- **AWS Bedrock**: AKSK と API Key の2種類の認証方式をサポート(#1047@keithyt06 に感謝)
- **SSAI Code**: パートナープリセット、5アプリ対応
- **CrazyRouter**: パートナープリセットと専用アイコン
- **AICoding**: パートナープリセットとプロモーションテキスト
- 国内モデルプロバイダープリセットを最新版に更新
- Qwen Coder を百炼 (Bailian) にリネーム(#965@zhu-jl18 に感謝)
### その他の新機能
- **Thinking Budget Rectifier**: より精密な thinking budget 制御(#1005@yovinchen に感謝)
- **WebDAV 自動同期**: 自動同期設定と大容量ファイル保護(#923@clx20000410 に感謝;#1043@SaladDay に感謝)
- **テーマ切り替えアニメーション**: 円形リビール遷移アニメーション(#905@funnytime75 に感謝)
- **Claude 設定エディタークイックトグル**: よく使う設定項目のクイック切り替え(#1012@JIA-ss に感謝)
- **動的エンドポイントヒント**: API フォーマット選択に基づく動的ヒントテキスト(#860@zhu-jl18 に感謝)
- **使用量ダッシュボード強化**: 自動更新、堅牢なフォーマット(#942@yovinchen に感謝)
- **新しい価格データ**: claude-opus-4-6 と gpt-5.3-codex#943@yovinchen に感謝)
- **サイレント起動の最適化**: サイレント起動オプションは自動起動が有効な場合のみ表示
---
## アーキテクチャ改善
### 部分キーフィールドマージ(⚠️ 破壊的変更)
プロバイダー切り替えを完全な設定上書きから部分キーフィールドマージ戦略に変更しました(#1098)。
**変更前**: プロバイダーを切り替えると、`settings_config` 全体がライブ設定ファイルに上書きされていました。つまり、ユーザーがライブファイルに手動で追加した非プロバイダー設定(プラグイン設定、MCP 設定、権限設定など)は、切り替えのたびに失われていました。この問題を補うため、以前のバージョンでは「共通設定スニペット」機能を提供し、毎回の切り替え時にマージされる共通設定を定義できました。
**変更後**: プロバイダー切り替え時に、プロバイダー関連のキー値(API キー、エンドポイント、モデルなど)のみが置換され、その他の設定はそのまま保持されます。そのため「共通設定スニペット」機能は不要となり、削除されました。
**影響と移行**:
- 共通設定スニペットを**使用していなかった**場合、この変更は完全に透過的で、切り替え体験が向上するだけです
- カスタム設定(MCP 設定、権限など)を保持するために共通設定スニペットを**使用していた**場合、それらの設定は切り替え時に自動的に保持されるようになり、追加の操作は不要です
- 共通設定スニペットを他の目的(切り替え時に追加設定を注入するなど)で使用していた場合は、アップグレード後にライブ設定ファイルに手動で設定を追加してください
このリファクタリングにより、フロントエンドファイル 6 つ(コンポーネント 3 つ + hooks 3 つ)と約 150 行のバックエンドデッドコードを削除しました。
### 手動インポートに変更
起動時の自動インポートを廃止し、手動の「現在の設定をインポート」ボタンに変更。意図しないユーザーデータの上書きを防止します。
### OmoVariant パラメータ化
`OmoVariant` 構造体によるパラメータ化で、OMO モジュールの約250行の重複コードを削除しました。
### OMO 共通設定の削除
2層マージシステムを削除し、約1,733行のコードを削減、アーキテクチャを簡素化しました。
### ProviderForm 分割
ProviderForm コンポーネントを2,227行から1,526行に削減し、5つの独立モジュール(opencodeFormUtils、useOmoModelSource、useOpencodeFormState、useOmoDraftState、useOpenclawFormState)に分離。保守性が大幅に向上しました。
### MCP/Skills 共有コンポーネント
AppCountBar、AppToggleGroup、ListItemRow などの共有コンポーネントを抽出し、MCP と Skills パネルの重複コードを削減(#897@PeanutSplash に感謝)。
### 設定ページリファクタリング
設定ページを5タブレイアウト(一般 | プロキシ | 詳細 | 使用量 | 情報)にリファクタリング。SettingsPage のコードを約716行から約426行に削減しました。
### その他の改善
- ターミナル統一: グローバル設定でターミナル選択を統一、WezTerm サポートを追加
- Claude モデル参照を 4.5 から 4.6 に更新
---
## バグ修正
### 重大な修正
- **Windows ホームディレクトリ回帰**: デフォルトのホームディレクトリ解決を復元し、Git/MSYS 環境でのデータベースパス変更によるデータ「消失」を防止
- **Linux 白画面**: AMD GPU の WebKitGTK ハードウェアアクセラレーションを無効化し、一部の Linux システムの起動白画面問題を解決(#986@ThendCN に感謝)
- **OpenAI Beta パラメータ**: `/v1/chat/completions``?beta=true` を追加しないように修正、Nvidia など OpenAI Chat 形式を使用するプロバイダーのリクエスト失敗を修正(#1052@jnorthrup に感謝)
- **ヘルスチェック認証**: プロバイダーの `auth_mode` 設定を尊重し、Bearer 認証のみをサポートするプロキシサービスのヘルスチェック失敗を回避(#824@Jassy930 に感謝)
### プロバイダープリセット修正
- OpenClaw `/v1` プレフィックスの二重パス問題を修正
- Opus 価格修正($15/$75 → $5/$25)と 4.6 へのアップグレード
- AIGoCode URL を `https://api.aigocode.com` に統一
- Zhipu GLM の古いパートナーステータスを削除
- 新規 Claude プロバイダー作成時の API Key 入力フィールドの表示を復元
- 非アクティブプロバイダーのクイックトグルを非表示、コンテキスト対応の JSON エディターヒントを表示
### OMO 修正
- omo-slim カテゴリチェックの補完(add/form/mutation パス)
- OMO Slim プロバイダー変更後のクエリキャッシュ無効化を修正
- OMO agent/category 推奨モデルをアップストリームソースと同期
- 「推奨を入力」ボタン失敗時の toast フィードバックを追加
- OMO/OMO Slim の最後のプロバイダー削除制限を撤廃
- OpenCode でモデル未設定時の保存を拒否(#932@yovinchen に感謝)
### OpenClaw 修正
- 25個の欠落 i18n キー、key={index} を安定 ID に置換、ディープリンク additive マージなどのコードレビュー問題を修正
- EnvPanel 堅牢性強化(NaN ガード、配列インデックスではなくエントリーキー名を使用)
- i18n 重複キーのマージ、プロバイダーフォーム翻訳を復元
### プラットフォーム修正
- Windows サイレント起動時のウィンドウフラッシュ(#901@funnytime75 に感謝)
- タイトルバーのダークモード追従(#903@funnytime75 に感謝)
- Windows の Skills パスセパレーターマッチング(#868@stmoonar に感謝)
- WSL ヘルパー関数の条件付きコンパイル
### UI 修正
- ツールバーの高さクリッピングによる AppSwitcher の遮蔽を修正
- 新バージョンがある場合、緑のチェックマークではなく更新バッジを表示
- セッションマネージャーボタンを Claude/Codex アプリでのみ表示
- SQL インポート/エクスポートカードのダークモードスタイルを統一(#1067@SaladDay に感謝)
### その他の修正
- セッションマネージャーのハードコードされた中国語文字列を i18n キーに置換
- Skill ドキュメント URL のブランチとパスを修正(#977@yovinchen に感謝)
- OpenCode install.sh インストールパス検出の補完(#988@zhu-jl18 に感謝)
- Skill ZIP シンボリックリンク解決の修正(#1040@yovinchen に感謝)
- MCP フォームに OpenCode チェックボックスを追加(#1026@yovinchen に感謝)
- useProvidersQuery の自動インポート副作用を削除
---
## パフォーマンス最適化
- セッションパネルの並列ディレクトリスキャン + ヘッドテール JSONL 読み取りで、セッションリスト読み込み速度を大幅向上
- Tauri ローカル IPC の不要な query cache を削除し、メモリ使用量を削減
---
## ドキュメント
- スポンサー更新: SSSAiCode、Crazyrouter、AICoding、Right Code、MiniMax
- ユーザーマニュアルを追加(#979@yovinchen に感謝)
---
## 注意事項
- **OpenClaw は新しくサポートされたアプリです**: 関連機能を使用するには、先に OpenClaw CLI をインストールする必要があります。
- **⚠️ 共通設定スニペット機能は削除されました**: プロバイダー切り替えが部分キーフィールドマージ(API キー、エンドポイント、モデルなどのみ置換)に変更されたため、ユーザーのその他の設定は自動的に保持され、共通設定スニペットは不要になりました。移行の詳細は上記「アーキテクチャ改善」セクションを参照してください。
- **自動インポートは手動に変更されました**: 起動時に外部設定を自動インポートしなくなりました。必要に応じて「現在の設定をインポート」を手動でクリックしてください。
- **OMO と OMO Slim は相互排他**: 同時に一つだけ有効にできます。切り替え時にもう一方は自動的に無効になります。
- **バックアップ機能はデフォルトで有効**: ランタイム中に1時間ごとに自動バックアップします。バックアップパネルでポリシーを調整できます。
---
## 特別な感謝
以下のコントリビューターの皆様、このリリースへの貢献に感謝します!
@TinsFox @keithyt06 @kv-chiu @SaladDay @jnorthrup @JIA-ss @clx20000410 @ThendCN @yovinchen @zhu-jl18 @myjustify @funnytime75 @PeanutSplash @Jassy930 @stmoonar
---
## ダウンロードとインストール
[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.11.0-Windows.msi` | **推奨** - MSI インストーラー、自動更新対応 |
| `CC-Switch-v3.11.0-Windows-Portable.zip` | ポータブル版、解凍して実行、レジストリ書き込みなし |
### macOS
| ファイル | 説明 |
| -------------------------------- | ----------------------------------------------------------------- |
| `CC-Switch-v3.11.0-macOS.zip` | **推奨** - 解凍して Applications にドラッグ、Universal Binary |
| `CC-Switch-v3.11.0-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` |
+302
View File
@@ -0,0 +1,302 @@
# CC Switch v3.11.0
> OpenClaw 支持、会话管理器、备份管理与 50+ 项改进
**[English →](release-note-v3.11.0-en.md) | [日本語版 →](release-note-v3.11.0-ja.md)**
---
## 概览
CC Switch v3.11.0 是一次大规模更新,新增第五个应用 **OpenClaw** 的完整管理支持,同时带来全新的**会话管理器**和**备份管理**功能。此外,**Oh My OpenCode (OMO) 集成**、供应商切换的**部分键值合并**架构升级、**设置页面重构**等多项改进使整体体验更加完善。
**发布日期**2026-02-26
**更新规模**147 commits | 274 files changed | +32,179 / -5,467 lines
---
## 重点内容
- **OpenClaw 支持**:第五个受管理应用,含 13 个供应商预设、Env/Tools/AgentsDefaults 配置编辑器、Workspace 文件管理
- **会话管理器**:浏览五个应用的历史会话,支持目录导航和会话内搜索
- **备份管理**:独立备份面板,可配置策略、定时备份、迁移前自动备份
- **Oh My OpenCode 集成**:完整 OMO 配置管理,支持 OMO Slim 轻量模式
- **部分键值合并(⚠️ 破坏性变更)**:供应商切换改为仅替换供应商相关字段,保留用户的其余设置;"通用配置片段"功能因此移除
- **设置页面重构**:5 标签页布局,代码量减少约 40%
- **6 组新供应商预设**AWS Bedrock、SSAI Code、CrazyRouter、AICoding 等
- **Thinking Budget Rectifier**:代理矫正器,更精细的 thinking budget 控制
- **主题切换动画**:圆形揭示过渡动画,视觉体验升级
- **WebDAV 自动同步**:支持自动同步与大文件防护
---
## 主要功能
### OpenClaw 支持(新增第五应用)
CC Switch 新增对 OpenClaw 的完整管理支持,这是继 Claude Code、Codex、Gemini CLI、OpenCode 之后的第五个受管理应用。
- **供应商管理**:新增、编辑、切换、删除 OpenClaw 供应商,含 13 个内置预设
- **配置编辑器**:Env(环境变量)、Tools(工具)、AgentsDefaults(代理默认值)三个专属配置面板
- **Workspace 面板**:支持 HEARTBEAT/BOOTSTRAP/BOOT 文件管理及每日记忆
- **Additive 叠加模式**:支持配置叠加而非覆盖
- **默认模型按钮**:一键填充推荐模型,添加供应商时自动将建议模型注册到 allowlist
- **品牌与交互**:专属品牌图标、应用切换淡入淡出过渡动画
- **深链接支持**:通过 URL 导入 OpenClaw 供应商配置
- **完整国际化**:中/英/日三语全面支持
### 会话管理器 Sessions
全新的会话管理器,帮助你浏览和检索历史会话记录。
- 支持浏览 Claude Code、Codex、Gemini CLI、OpenCode、OpenClaw 五个应用的历史会话(#867,感谢 @TinsFox
- 目录导航和会话内搜索
- 进入会话页面时默认过滤为当前应用,快速定位
- 并行目录扫描 + 头尾 JSONL 读取,优化加载性能
### 备份管理 Backup
独立的备份管理面板,让数据安全更有保障。
- 可配置备份策略:最大备份数量、自动清理规则
- 运行时每小时定期自动备份
- 数据库迁移前自动备份,带回填警告提示
- 支持备份重命名和删除(含确认对话框)
- 备份文件名使用本地时间,更直观
### Oh My OpenCode (OMO) 集成
完整的 Oh My OpenCode 配置文件管理。
- Agent 模型选择、Category 配置、推荐模型填充(#972,感谢 @yovinchen
- 改进 Agent 模型选择 UX,修复 lowercase key 问题(#1004,感谢 @yovinchen
- OMO Slim 轻量模式支持
- OMO 与 OMO Slim 互斥切换(数据库层级强制保证一致性)
### 工作空间 Workspace
- 每日记忆文件全文搜索,按日期排序
- 目录路径可点击跳转,快速打开文件位置
### 工具栏 Toolbar
- AppSwitcher 根据窗口宽度自动折叠为紧凑模式
- 紧凑模式切换平滑过渡动画
### 设置 Settings
- 代理和用量功能新增首次使用确认对话框,避免误操作
- 新增 `enableLocalProxy` 开关,控制主页代理 UI 显示
- 更精细的本地环境检查:CLI 工具版本检测(#870,感谢 @kv-chiu)、Volta 路径检测(#969,感谢 @myjustify
### 供应商预设 Preset
- **AWS Bedrock**:支持 AKSK 和 API Key 两种认证方式(#1047,感谢 @keithyt06
- **SSAI Code**:合作伙伴预设,覆盖五端
- **CrazyRouter**:合作伙伴预设及专属图标
- **AICoding**:合作伙伴预设及推广文案
- 更新国内模型供应商预设至最新版本
- Qwen Coder 重命名为百炼 (Bailian)#965,感谢 @zhu-jl18
### 其他新功能
- **Thinking Budget Rectifier**:代理矫正器,更精细地控制 thinking budget 分配(#1005,感谢 @yovinchen
- **WebDAV 自动同步**:支持自动同步配置,并增加大文件防护(#923,感谢 @clx20000410#1043,感谢 @SaladDay
- **主题切换动画**:圆形揭示过渡动画,视觉体验更流畅(#905,感谢 @funnytime75
- **Claude 配置编辑器快速开关**:快速切换常用配置项(#1012,感谢 @JIA-ss
- **动态端点提示**:根据 API 格式选择动态显示端点提示文本(#860,感谢 @zhu-jl18
- **用量仪表盘增强**:自动刷新、更强健的数据格式化(#942,感谢 @yovinchen
- **新增定价数据**claude-opus-4-6 和 gpt-5.3-codex#943,感谢 @yovinchen
- **静默启动优化**:静默启动选项仅在开机启动开启时显示
---
## 架构改进
### 部分键值合并(⚠️ 破坏性变更)
供应商切换从全量配置覆写改为部分键值合并策略(#1098)。
**变更前**:切换供应商时,整个 `settings_config` 会覆写到 live 配置文件。这意味着用户在 live 文件中手动添加的非供应商设置(插件配置、MCP 配置、权限设置等)会在每次切换时丢失。为了弥补这个问题,之前版本提供了"通用配置片段"功能,让用户定义每次切换时都会合并的公共配置。
**变更后**:切换供应商时,仅替换供应商相关的键值(API Key、端点、模型等),用户的其余设置完整保留。因此"通用配置片段"功能不再需要,已被移除。
**影响与迁移**
- 如果你之前**没有使用**通用配置片段功能,此变更对你完全透明,切换体验只会更好
- 如果你之前**使用了**通用配置片段功能来保留自定义设置(如 MCP 配置、权限等),升级后这些设置会在切换时自动保留,无需额外操作
- 如果你利用通用配置片段做其他用途(如在切换时注入额外配置),请在升级后手动将这些配置写入 live 配置文件中
此次重构删除了 6 个前端文件(3 个组件 + 3 个 hooks)、约 150 行后端死代码。
### 手动导入替代自动导入
启动时不再自动导入外部配置,改为手动点击"导入当前配置"按钮,避免意外覆盖用户数据。
### OMO Variant 参数化
通过 `OmoVariant` 结构体参数化消除 OMO 模块约 250 行重复代码。
### OMO 公共配置移除
删除二层合并系统,减少约 1,733 行代码,简化架构。
### ProviderForm 拆分
ProviderForm 组件从 2,227 行减至 1,526 行,提取 5 个独立模块(opencodeFormUtils、useOmoModelSource、useOpencodeFormState、useOmoDraftState、useOpenclawFormState),可维护性显著提升。
### MCP/Skills 共享组件
提取 AppCountBar、AppToggleGroup、ListItemRow 等共享组件,减少 MCP 和 Skills 面板的重复代码(#897,感谢 @PeanutSplash)。
### 设置页面重构
设置页面重构为 5 标签页布局(通用 | 代理 | 高级 | 用量 | 关于),SettingsPage 代码从约 716 行减至约 426 行。
### 其他改进
- 终端统一:全局设置统一终端选择,新增 WezTerm 支持
- Claude 模型引用从 4.5 更新到 4.6
---
## Bug 修复
### 严重修复
- **Windows 主目录回归**:恢复默认主目录解析,防止 Git/MSYS 环境下数据库路径变更导致数据"丢失"
- **Linux 白屏**:禁用 AMD GPU 的 WebKitGTK 硬件加速,解决部分 Linux 系统启动白屏问题(#986,感谢 @ThendCN
- **OpenAI Beta 参数**:不再为 `/v1/chat/completions` 添加 `?beta=true`,修复 Nvidia 等使用 OpenAI Chat 格式的供应商请求失败(#1052,感谢 @jnorthrup
- **健康检查认证**:尊重供应商 `auth_mode` 设置,避免仅支持 Bearer 认证的代理服务健康检查失败(#824,感谢 @Jassy930
### 供应商预设修复
- 修复 OpenClaw `/v1` 前缀双重路径问题
- Opus 定价修正($15/$75 → $5/$25)并升级到 4.6
- AIGoCode URL 统一为 `https://api.aigocode.com`
- Zhipu GLM 移除过时合作伙伴状态
- 新建 Claude 供应商时 API Key 输入框可见性恢复
- 非活跃供应商隐藏快速开关,显示上下文感知的 JSON 编辑器提示
### OMO 修复
- omo-slim 分类检查补齐(add/form/mutation 路径)
- OMO Slim 供应商变更后正确失效查询缓存
- OMO agent/category 推荐模型与上游源同步
- "填充推荐"按钮失败时增加 toast 反馈
- 移除 OMO/OMO Slim 最后一个供应商的删除限制
- OpenCode 未配置模型时拒绝保存(#932,感谢 @yovinchen
### OpenClaw 修复
- 修复 25 个缺失 i18n key、替换 key={index} 为稳定 ID、深链接 additive 合并等代码审查问题
- EnvPanel 健壮性增强(NaN 守卫、使用条目键名而非数组索引)
- i18n 重复键合并,恢复供应商表单翻译
### 平台修复
- Windows 静默启动时窗口闪烁(#901,感谢 @funnytime75
- 标题栏暗黑模式跟随主题(#903,感谢 @funnytime75
- Windows Skills 路径分隔符匹配(#868,感谢 @stmoonar
- WSL 辅助函数条件编译
### UI 修复
- 工具栏高度裁切导致 AppSwitcher 被遮挡
- 有新版本时显示更新徽章而非绿色对勾
- 仅 Claude/Codex 应用显示会话管理器按钮
- SQL 导入/导出卡片暗黑模式样式统一(#1067,感谢 @SaladDay
### 其他修复
- 会话管理器硬编码中文字符串替换为 i18n key
- Skill 文档 URL 分支和路径修正(#977,感谢 @yovinchen
- OpenCode install.sh 安装路径检测补齐(#988,感谢 @zhu-jl18
- Skill ZIP 符号链接解析修复(#1040,感谢 @yovinchen
- MCP 表单补齐 OpenCode 复选框(#1026,感谢 @yovinchen
- useProvidersQuery 中自动导入副作用移除
---
## 性能优化
- 会话面板并行目录扫描 + 头尾 JSONL 读取,大幅提升会话列表加载速度
- 移除 Tauri 本地 IPC 不必要的 query cache,减少内存占用
---
## 文档
- 赞助商更新:SSSAiCode、Crazyrouter、AICoding、Right Code、MiniMax
- 新增用户手册(#979,感谢 @yovinchen
---
## 说明与注意事项
- **OpenClaw 为新支持的应用**:需要先安装 OpenClaw CLI 才能使用相关功能。
- **⚠️ 通用配置片段功能已移除**:由于供应商切换改为部分键值合并(仅替换 API Key、端点、模型等字段),用户的其余设置会自动保留,"通用配置片段"功能不再需要。详见上方"架构改进"章节的迁移说明。
- **自动导入已改为手动**:启动时不再自动导入外部配置,请在需要时手动点击"导入当前配置"。
- **OMO 与 OMO Slim 互斥**:同一时间只能启用其中一个,切换时另一个会自动禁用。
- **备份功能默认开启**:运行时每小时自动备份,可在备份面板调整策略。
---
## 特别感谢
感谢以下贡献者为本版本做出的贡献!
@TinsFox @keithyt06 @kv-chiu @SaladDay @jnorthrup @JIA-ss @clx20000410 @ThendCN @yovinchen @zhu-jl18 @myjustify @funnytime75 @PeanutSplash @Jassy930 @stmoonar
---
## 下载与安装
访问 [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.11.0-Windows.msi` | **推荐** - MSI 安装包,支持自动更新 |
| `CC-Switch-v3.11.0-Windows-Portable.zip` | 便携版,解压即用,不写入注册表 |
### macOS
| 文件 | 说明 |
| -------------------------------- | --------------------------------------------------------- |
| `CC-Switch-v3.11.0-macOS.zip` | **推荐** - 解压后拖入 Applications 即可,Universal Binary |
| `CC-Switch-v3.11.0-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` |
+3 -3
View File
@@ -99,9 +99,9 @@
## 版本信息
- 文档版本:v3.10.3
- 最后更新:2026-02-09
- 适用于 CC Switch v3.10.0+
- 文档版本:v3.11.0
- 最后更新:2026-02-26
- 适用于 CC Switch v3.11.0+
## 贡献
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "cc-switch",
"version": "3.10.3",
"version": "3.11.0",
"description": "All-in-One Assistant for Claude Code, Codex & Gemini CLI",
"type": "module",
"scripts": {
+1 -1
View File
@@ -701,7 +701,7 @@ dependencies = [
[[package]]
name = "cc-switch"
version = "3.10.3"
version = "3.11.0"
dependencies = [
"anyhow",
"async-stream",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "cc-switch"
version = "3.10.3"
version = "3.11.0"
description = "All-in-One Assistant for Claude Code, Codex & Gemini CLI"
authors = ["Jason Young"]
license = "MIT"
-81
View File
@@ -7,7 +7,6 @@ use tauri_plugin_opener::OpenerExt;
use crate::app_config::AppType;
use crate::codex_config;
use crate::config::{self, get_claude_settings_path, ConfigStatus};
use crate::settings;
#[tauri::command]
pub async fn get_claude_config_status() -> Result<ConfigStatus, String> {
@@ -16,18 +15,6 @@ pub async fn get_claude_config_status() -> Result<ConfigStatus, String> {
use std::str::FromStr;
fn invalid_json_format_error(error: serde_json::Error) -> String {
let lang = settings::get_settings()
.language
.unwrap_or_else(|| "zh".to_string());
match lang.as_str() {
"en" => format!("Invalid JSON format: {error}"),
"ja" => format!("JSON形式が無効です: {error}"),
_ => format!("无效的 JSON 格式: {error}"),
}
}
#[tauri::command]
pub async fn get_config_status(app: String) -> Result<ConfigStatus, String> {
match AppType::from_str(&app).map_err(|e| e.to_string())? {
@@ -163,71 +150,3 @@ pub async fn open_app_config_folder(handle: AppHandle) -> Result<bool, String> {
Ok(true)
}
#[tauri::command]
pub async fn get_common_config_snippet(
app_type: String,
state: tauri::State<'_, crate::store::AppState>,
) -> Result<Option<String>, String> {
state
.db
.get_config_snippet(&app_type)
.map_err(|e| e.to_string())
}
#[tauri::command]
pub async fn set_common_config_snippet(
app_type: String,
snippet: String,
state: tauri::State<'_, crate::store::AppState>,
) -> Result<(), String> {
if !snippet.trim().is_empty() {
match app_type.as_str() {
"claude" | "gemini" | "omo" | "omo-slim" => {
serde_json::from_str::<serde_json::Value>(&snippet)
.map_err(invalid_json_format_error)?;
}
"codex" => {}
_ => {}
}
}
let value = if snippet.trim().is_empty() {
None
} else {
Some(snippet)
};
state
.db
.set_config_snippet(&app_type, value)
.map_err(|e| e.to_string())?;
if app_type == "omo"
&& state
.db
.get_current_omo_provider("opencode", "omo")
.map_err(|e| e.to_string())?
.is_some()
{
crate::services::OmoService::write_config_to_file(
state.inner(),
&crate::services::omo::STANDARD,
)
.map_err(|e| e.to_string())?;
}
if app_type == "omo-slim"
&& state
.db
.get_current_omo_provider("opencode", "omo-slim")
.map_err(|e| e.to_string())?
.is_some()
{
crate::services::OmoService::write_config_to_file(
state.inner(),
&crate::services::omo::SLIM,
)
.map_err(|e| e.to_string())?;
}
Ok(())
}
+24
View File
@@ -123,6 +123,24 @@ pub async fn open_zip_file_dialog<R: tauri::Runtime>(
// ─── Database backup management ─────────────────────────────
/// Manually create a database backup
#[tauri::command]
pub async fn create_db_backup(state: State<'_, AppState>) -> Result<String, String> {
let db = state.db.clone();
tauri::async_runtime::spawn_blocking(move || match db.backup_database_file()? {
Some(path) => Ok(path
.file_name()
.map(|f| f.to_string_lossy().into_owned())
.unwrap_or_default()),
None => Err(AppError::Config(
"Database file not found, backup skipped".to_string(),
)),
})
.await
.map_err(|e| format!("Backup failed: {e}"))?
.map_err(|e: AppError| e.to_string())
}
/// List all database backup files
#[tauri::command]
pub fn list_db_backups() -> Result<Vec<BackupEntry>, String> {
@@ -150,3 +168,9 @@ pub fn rename_db_backup(
) -> Result<String, String> {
Database::rename_backup(&oldFilename, &newName).map_err(|e| e.to_string())
}
/// Delete a database backup file
#[tauri::command]
pub fn delete_db_backup(filename: String) -> Result<(), String> {
Database::delete_backup(&filename).map_err(|e| e.to_string())
}
-26
View File
@@ -36,19 +36,6 @@ pub async fn disable_current_omo(state: State<'_, AppState>) -> Result<(), Strin
Ok(())
}
#[tauri::command]
pub async fn get_omo_provider_count(state: State<'_, AppState>) -> Result<usize, String> {
let providers = state
.db
.get_all_providers("opencode")
.map_err(|e| e.to_string())?;
let count = providers
.values()
.filter(|p| p.category.as_deref() == Some("omo"))
.count();
Ok(count)
}
// ── OMO Slim commands ───────────────────────────────────────
#[tauri::command]
@@ -84,16 +71,3 @@ pub async fn disable_current_omo_slim(state: State<'_, AppState>) -> Result<(),
OmoService::delete_config_file(&SLIM).map_err(|e| e.to_string())?;
Ok(())
}
#[tauri::command]
pub async fn get_omo_slim_provider_count(state: State<'_, AppState>) -> Result<usize, String> {
let providers = state
.db
.get_all_providers("opencode")
.map_err(|e| e.to_string())?;
let count = providers
.values()
.filter(|p| p.category.as_deref() == Some("omo-slim"))
.count();
Ok(count)
}
+27 -2
View File
@@ -5,7 +5,7 @@
use super::{lock_conn, Database};
use crate::config::get_app_config_dir;
use crate::error::AppError;
use chrono::Utc;
use chrono::{Local, Utc};
use rusqlite::backup::Backup;
use rusqlite::types::ValueRef;
use rusqlite::Connection;
@@ -182,7 +182,7 @@ impl Database {
fs::create_dir_all(&backup_dir).map_err(|e| AppError::io(&backup_dir, e))?;
let base_id = format!("db_backup_{}", Utc::now().format("%Y%m%d_%H%M%S"));
let base_id = format!("db_backup_{}", Local::now().format("%Y%m%d_%H%M%S"));
let mut backup_id = base_id.clone();
let mut backup_path = backup_dir.join(format!("{backup_id}.db"));
let mut counter = 1;
@@ -531,4 +531,29 @@ impl Database {
log::info!("Renamed backup: {old_filename} -> {new_filename}");
Ok(new_filename)
}
/// Delete a backup file permanently.
pub fn delete_backup(filename: &str) -> Result<(), AppError> {
// Validate filename (path traversal + .db suffix)
if filename.contains("..")
|| filename.contains('/')
|| filename.contains('\\')
|| !filename.ends_with(".db")
{
return Err(AppError::InvalidInput(
"Invalid backup filename".to_string(),
));
}
let backup_path = get_app_config_dir().join("backups").join(filename);
if !backup_path.exists() {
return Err(AppError::InvalidInput(format!(
"Backup file not found: {filename}"
)));
}
fs::remove_file(&backup_path).map_err(|e| AppError::io(&backup_path, e))?;
log::info!("Deleted backup: {filename}");
Ok(())
}
}
-2
View File
@@ -4,7 +4,6 @@
pub mod failover;
pub mod mcp;
pub mod omo;
pub mod prompts;
pub mod providers;
pub mod proxy;
@@ -16,4 +15,3 @@ pub mod universal_providers;
// 所有 DAO 方法都通过 Database impl 提供,无需单独导出
// 导出 FailoverQueueItem 供外部使用
pub use failover::FailoverQueueItem;
pub use omo::OmoGlobalConfig;
-77
View File
@@ -1,77 +0,0 @@
use crate::database::Database;
use crate::error::AppError;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OmoGlobalConfig {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub schema_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sisyphus_agent: Option<serde_json::Value>,
#[serde(default)]
pub disabled_agents: Vec<String>,
#[serde(default)]
pub disabled_mcps: Vec<String>,
#[serde(default)]
pub disabled_hooks: Vec<String>,
#[serde(default)]
pub disabled_skills: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lsp: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub experimental: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub background_task: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub browser_automation_engine: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub claude_code: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub other_fields: Option<serde_json::Value>,
pub updated_at: String,
}
impl Default for OmoGlobalConfig {
fn default() -> Self {
Self {
id: "global".to_string(),
schema_url: None,
sisyphus_agent: None,
disabled_agents: vec![],
disabled_mcps: vec![],
disabled_hooks: vec![],
disabled_skills: vec![],
lsp: None,
experimental: None,
background_task: None,
browser_automation_engine: None,
claude_code: None,
other_fields: None,
updated_at: chrono::Utc::now().to_rfc3339(),
}
}
}
impl Database {
pub fn get_omo_global_config(&self, key: &str) -> Result<OmoGlobalConfig, AppError> {
let json_str = self.get_setting(key)?;
match json_str {
Some(s) => serde_json::from_str::<OmoGlobalConfig>(&s)
.map_err(|e| AppError::Config(format!("Failed to parse {key}: {e}"))),
None => Ok(OmoGlobalConfig::default()),
}
}
pub fn save_omo_global_config(
&self,
key: &str,
config: &OmoGlobalConfig,
) -> Result<(), AppError> {
let json_str = serde_json::to_string(config)
.map_err(|e| AppError::Config(format!("JSON serialization failed: {e}")))?;
self.set_setting(key, &json_str)?;
Ok(())
}
}
+13
View File
@@ -375,6 +375,19 @@ impl Database {
params![app_type, category],
)
.map_err(|e| AppError::Database(e.to_string()))?;
// OMO ↔ OMO Slim mutually exclusive: deactivate the opposite category
let opposite = match category {
"omo" => Some("omo-slim"),
"omo-slim" => Some("omo"),
_ => None,
};
if let Some(opp) = opposite {
tx.execute(
"UPDATE providers SET is_current = 0 WHERE app_type = ?1 AND category = ?2",
params![app_type, opp],
)
.map_err(|e| AppError::Database(e.to_string()))?;
}
let updated = tx
.execute(
"UPDATE providers SET is_current = 1 WHERE id = ?1 AND app_type = ?2 AND category = ?3",
-25
View File
@@ -38,31 +38,6 @@ impl Database {
Ok(())
}
// --- Config Snippets 辅助方法 ---
/// 获取通用配置片段
pub fn get_config_snippet(&self, app_type: &str) -> Result<Option<String>, AppError> {
self.get_setting(&format!("common_config_{app_type}"))
}
/// 设置通用配置片段
pub fn set_config_snippet(
&self,
app_type: &str,
snippet: Option<String>,
) -> Result<(), AppError> {
let key = format!("common_config_{app_type}");
if let Some(value) = snippet {
self.set_setting(&key, &value)
} else {
// 如果为 None 则删除
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(())
}
}
// --- 全局出站代理 ---
/// 全局代理 URL 的存储键名
-1
View File
@@ -33,7 +33,6 @@ mod tests;
// DAO 类型导出供外部使用
pub use dao::FailoverQueueItem;
pub use dao::OmoGlobalConfig;
use crate::config::get_app_config_dir;
use crate::error::AppError;
+2 -4
View File
@@ -845,8 +845,6 @@ pub fn run() {
commands::get_skills_migration_result,
commands::get_app_config_path,
commands::open_app_config_folder,
commands::get_common_config_snippet,
commands::set_common_config_snippet,
commands::read_live_provider_settings,
commands::patch_claude_live_settings,
commands::get_settings,
@@ -913,9 +911,11 @@ pub fn run() {
commands::save_file_dialog,
commands::open_file_dialog,
commands::open_zip_file_dialog,
commands::create_db_backup,
commands::list_db_backups,
commands::restore_db_backup,
commands::rename_db_backup,
commands::delete_db_backup,
commands::sync_current_providers_live,
// Deep link import
commands::parse_deeplink,
@@ -1037,11 +1037,9 @@ pub fn run() {
commands::set_window_theme,
commands::read_omo_local_file,
commands::get_current_omo_provider_id,
commands::get_omo_provider_count,
commands::disable_current_omo,
commands::read_omo_slim_local_file,
commands::get_current_omo_slim_provider_id,
commands::get_omo_slim_provider_count,
commands::disable_current_omo_slim,
// Workspace files (OpenClaw)
commands::read_workspace_file,
+44 -213
View File
@@ -1,5 +1,4 @@
use crate::config::write_json_file;
use crate::database::OmoGlobalConfig;
use crate::error::AppError;
use crate::opencode_config::get_opencode_dir;
use crate::store::AppState;
@@ -13,12 +12,11 @@ pub struct OmoLocalFileData {
pub agents: Option<Value>,
pub categories: Option<Value>,
pub other_fields: Option<Value>,
pub global: OmoGlobalConfig,
pub file_path: String,
pub last_modified: Option<String>,
}
type OmoProfileData = (Option<Value>, Option<Value>, Option<Value>, bool);
type OmoProfileData = (Option<Value>, Option<Value>, Option<Value>);
// ── Variant descriptor ─────────────────────────────────────────
@@ -28,9 +26,7 @@ pub struct OmoVariant {
pub provider_prefix: &'static str,
pub plugin_name: &'static str,
pub plugin_prefix: &'static str,
pub known_keys: &'static [&'static str],
pub has_categories: bool,
pub config_key: &'static str,
pub label: &'static str,
pub import_label: &'static str,
}
@@ -41,23 +37,7 @@ pub const STANDARD: OmoVariant = OmoVariant {
provider_prefix: "omo-",
plugin_name: "oh-my-opencode@latest",
plugin_prefix: "oh-my-opencode",
known_keys: &[
"$schema",
"agents",
"categories",
"sisyphus_agent",
"disabled_agents",
"disabled_mcps",
"disabled_hooks",
"disabled_skills",
"lsp",
"experimental",
"background_task",
"browser_automation_engine",
"claude_code",
],
has_categories: true,
config_key: "common_config_omo",
label: "OMO",
import_label: "Imported",
};
@@ -68,18 +48,7 @@ pub const SLIM: OmoVariant = OmoVariant {
provider_prefix: "omo-slim-",
plugin_name: "oh-my-opencode-slim@latest",
plugin_prefix: "oh-my-opencode-slim",
known_keys: &[
"$schema",
"agents",
"sisyphus_agent",
"disabled_agents",
"disabled_mcps",
"disabled_hooks",
"lsp",
"experimental",
],
has_categories: false,
config_key: "common_config_omo_slim",
label: "OMO Slim",
import_label: "Imported Slim",
};
@@ -135,47 +104,6 @@ impl OmoService {
other
}
fn extract_string_array(val: &Value) -> Vec<String> {
val.as_array()
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default()
}
fn merge_global_from_obj(obj: &Map<String, Value>, global: &mut OmoGlobalConfig) {
if let Some(v) = obj.get("$schema") {
global.schema_url = v.as_str().map(|s| s.to_string());
}
for (key, target) in [
("disabled_agents", &mut global.disabled_agents),
("disabled_mcps", &mut global.disabled_mcps),
("disabled_hooks", &mut global.disabled_hooks),
("disabled_skills", &mut global.disabled_skills),
] {
if let Some(v) = obj.get(key) {
*target = Self::extract_string_array(v);
}
}
for (key, target) in [
("sisyphus_agent", &mut global.sisyphus_agent),
("lsp", &mut global.lsp),
("experimental", &mut global.experimental),
("background_task", &mut global.background_task),
(
"browser_automation_engine",
&mut global.browser_automation_engine,
),
("claude_code", &mut global.claude_code),
] {
if let Some(v) = obj.get(key) {
*target = Some(v.clone());
}
}
}
// ── Merge helpers ──────────────────────────────────────
fn insert_opt_value(result: &mut Map<String, Value>, key: &str, value: &Option<Value>) {
@@ -184,15 +112,6 @@ impl OmoService {
}
}
fn insert_string_array(result: &mut Map<String, Value>, key: &str, values: &[String]) {
if !values.is_empty() {
result.insert(
key.to_string(),
serde_json::to_value(values).unwrap_or(Value::Array(vec![])),
);
}
}
fn insert_object_entries(result: &mut Map<String, Value>, value: Option<&Value>) {
if let Some(Value::Object(map)) = value {
for (k, v) in map {
@@ -214,9 +133,7 @@ impl OmoService {
}
pub fn write_config_to_file(state: &AppState, v: &OmoVariant) -> Result<(), AppError> {
let global = state.db.get_omo_global_config(v.config_key)?;
let current_omo = state.db.get_current_omo_provider("opencode", v.category)?;
let profile_data = current_omo.as_ref().map(|p| {
let agents = p.settings_config.get("agents").cloned();
let categories = if v.has_categories {
@@ -225,15 +142,10 @@ impl OmoService {
None
};
let other_fields = p.settings_config.get("otherFields").cloned();
let use_common_config = p
.settings_config
.get("useCommonConfig")
.and_then(|val| val.as_bool())
.unwrap_or(true);
(agents, categories, other_fields, use_common_config)
(agents, categories, other_fields)
});
let merged = Self::merge_config(v, &global, profile_data.as_ref());
let merged = Self::build_config(v, profile_data.as_ref());
let config_path = Self::config_path(v);
if let Some(parent) = config_path.parent() {
@@ -241,56 +153,20 @@ impl OmoService {
}
write_json_file(&config_path, &merged)?;
crate::opencode_config::add_plugin(v.plugin_name)?;
log::info!("{} config written to {config_path:?}", v.label);
Ok(())
}
fn merge_config(
v: &OmoVariant,
global: &OmoGlobalConfig,
profile_data: Option<&OmoProfileData>,
) -> Value {
fn build_config(v: &OmoVariant, profile_data: Option<&OmoProfileData>) -> Value {
let mut result = Map::new();
let use_common_config = profile_data.map(|(_, _, _, uc)| *uc).unwrap_or(true);
if use_common_config {
if let Some(url) = &global.schema_url {
result.insert("$schema".to_string(), Value::String(url.clone()));
}
Self::insert_opt_value(&mut result, "sisyphus_agent", &global.sisyphus_agent);
Self::insert_string_array(&mut result, "disabled_agents", &global.disabled_agents);
Self::insert_string_array(&mut result, "disabled_mcps", &global.disabled_mcps);
Self::insert_string_array(&mut result, "disabled_hooks", &global.disabled_hooks);
if v.has_categories {
Self::insert_string_array(&mut result, "disabled_skills", &global.disabled_skills);
Self::insert_opt_value(&mut result, "background_task", &global.background_task);
Self::insert_opt_value(
&mut result,
"browser_automation_engine",
&global.browser_automation_engine,
);
Self::insert_opt_value(&mut result, "claude_code", &global.claude_code);
}
Self::insert_opt_value(&mut result, "lsp", &global.lsp);
Self::insert_opt_value(&mut result, "experimental", &global.experimental);
Self::insert_object_entries(&mut result, global.other_fields.as_ref());
}
if let Some((agents, categories, other_fields, _)) = profile_data {
if let Some((agents, categories, other_fields)) = profile_data {
Self::insert_object_entries(&mut result, other_fields.as_ref());
Self::insert_opt_value(&mut result, "agents", agents);
if v.has_categories {
Self::insert_opt_value(&mut result, "categories", categories);
}
Self::insert_object_entries(&mut result, other_fields.as_ref());
}
Value::Object(result)
}
@@ -310,18 +186,12 @@ impl OmoService {
settings.insert("categories".to_string(), categories.clone());
}
}
settings.insert("useCommonConfig".to_string(), Value::Bool(true));
let other = Self::extract_other_fields_with_keys(&obj, v.known_keys);
let other = Self::extract_other_fields_with_keys(&obj, &["agents", "categories"]);
if !other.is_empty() {
settings.insert("otherFields".to_string(), Value::Object(other));
}
let mut global = state.db.get_omo_global_config(v.config_key)?;
Self::merge_global_from_obj(&obj, &mut global);
global.updated_at = chrono::Utc::now().to_rfc3339();
state.db.save_omo_global_config(v.config_key, &global)?;
let provider_id = format!("{}{}", v.provider_prefix, uuid::Uuid::new_v4());
let name = format!(
"{} {}",
@@ -384,22 +254,17 @@ impl OmoService {
None
};
let other = Self::extract_other_fields_with_keys(obj, v.known_keys);
let other = Self::extract_other_fields_with_keys(obj, &["agents", "categories"]);
let other_fields = if other.is_empty() {
None
} else {
Some(Value::Object(other))
};
let mut global = OmoGlobalConfig::default();
Self::merge_global_from_obj(obj, &mut global);
global.other_fields = other_fields.clone();
OmoLocalFileData {
agents,
categories,
other_fields,
global,
file_path,
last_modified,
}
@@ -482,26 +347,24 @@ mod tests {
}
#[test]
fn test_merge_config_empty() {
let global = OmoGlobalConfig::default();
let merged = OmoService::merge_config(&STANDARD, &global, None);
fn test_build_config_empty() {
let merged = OmoService::build_config(&STANDARD, None);
assert!(merged.is_object());
assert!(merged.as_object().unwrap().is_empty());
}
#[test]
fn test_merge_config_with_profile() {
let global = OmoGlobalConfig {
schema_url: Some("https://example.com/schema.json".to_string()),
disabled_agents: vec!["explore".to_string()],
..Default::default()
};
fn test_build_config_with_profile() {
let agents = Some(serde_json::json!({
"sisyphus": { "model": "claude-opus-4-5" }
}));
let categories = None;
let other_fields = None;
let profile_data = (agents, categories, other_fields, true);
let merged = OmoService::merge_config(&STANDARD, &global, Some(&profile_data));
let other_fields = Some(serde_json::json!({
"$schema": "https://example.com/schema.json",
"disabled_agents": ["explore"]
}));
let profile_data = (agents, categories, other_fields);
let merged = OmoService::build_config(&STANDARD, Some(&profile_data));
let obj = merged.as_object().unwrap();
assert_eq!(obj["$schema"], "https://example.com/schema.json");
@@ -511,28 +374,7 @@ mod tests {
}
#[test]
fn test_merge_config_without_common_config() {
let global = OmoGlobalConfig {
schema_url: Some("https://example.com/schema.json".to_string()),
disabled_agents: vec!["explore".to_string()],
..Default::default()
};
let agents = Some(serde_json::json!({
"sisyphus": { "model": "claude-opus-4-5" }
}));
let categories = None;
let other_fields = None;
let profile_data = (agents, categories, other_fields, false);
let merged = OmoService::merge_config(&STANDARD, &global, Some(&profile_data));
let obj = merged.as_object().unwrap();
assert!(!obj.contains_key("$schema"));
assert!(!obj.contains_key("disabled_agents"));
assert!(obj.contains_key("agents"));
}
#[test]
fn test_build_local_file_data_keeps_unknown_top_level_fields_in_global() {
fn test_build_local_file_data_keeps_all_non_agent_category_fields_in_other() {
let obj = serde_json::json!({
"$schema": "https://example.com/schema.json",
"disabled_agents": ["oracle"],
@@ -555,64 +397,53 @@ mod tests {
None,
);
// All non-agents/categories fields should be in other_fields
let other = data.other_fields.unwrap();
let other_obj = other.as_object().unwrap();
assert_eq!(
data.global.schema_url.as_deref(),
Some("https://example.com/schema.json")
other_obj.get("$schema").unwrap(),
"https://example.com/schema.json"
);
assert_eq!(data.global.disabled_agents, vec!["oracle".to_string()]);
assert_eq!(
data.other_fields,
Some(serde_json::json!({
"custom_top_level": { "enabled": true }
}))
other_obj.get("disabled_agents").unwrap(),
&serde_json::json!(["oracle"])
);
assert_eq!(data.global.other_fields, data.other_fields);
assert_eq!(
other_obj.get("custom_top_level").unwrap(),
&serde_json::json!({"enabled": true})
);
// agents and categories should NOT be in other_fields
assert!(!other_obj.contains_key("agents"));
assert!(!other_obj.contains_key("categories"));
}
#[test]
fn test_merge_config_ignores_non_object_other_fields() {
let global = OmoGlobalConfig {
other_fields: Some(serde_json::json!(["global_non_object"])),
..Default::default()
};
fn test_build_config_ignores_non_object_other_fields() {
let agents = None;
let categories = None;
let other_fields = Some(serde_json::json!("profile_non_object"));
let profile_data = (agents, categories, other_fields, true);
let profile_data = (agents, categories, other_fields);
let merged = OmoService::merge_config(&STANDARD, &global, Some(&profile_data));
let merged = OmoService::build_config(&STANDARD, Some(&profile_data));
let obj = merged.as_object().unwrap();
assert!(!obj.contains_key("0"));
assert!(!obj.contains_key("global_non_object"));
assert!(!obj.contains_key("profile_non_object"));
}
#[test]
fn test_merge_config_slim_excludes_categories_and_extra_fields() {
let global = OmoGlobalConfig {
schema_url: Some("https://slim.schema".to_string()),
disabled_agents: vec!["oracle".to_string()],
disabled_skills: vec!["playwright".to_string()],
background_task: Some(serde_json::json!({"key": "val"})),
browser_automation_engine: Some(serde_json::json!({"provider": "pw"})),
claude_code: Some(serde_json::json!({"mcp": true})),
..Default::default()
};
fn test_build_config_slim_excludes_categories() {
let agents = Some(serde_json::json!({"orchestrator": {"model": "k2"}}));
let categories = Some(serde_json::json!({"code": {"model": "gpt"}}));
let other_fields = None;
let profile_data = (agents, categories, other_fields, true);
let other_fields = Some(serde_json::json!({
"$schema": "https://slim.schema",
"disabled_agents": ["oracle"]
}));
let profile_data = (agents, categories, other_fields);
let merged = OmoService::merge_config(&SLIM, &global, Some(&profile_data));
let merged = OmoService::build_config(&SLIM, Some(&profile_data));
let obj = merged.as_object().unwrap();
// Slim should NOT include these
assert!(!obj.contains_key("disabled_skills"));
assert!(!obj.contains_key("background_task"));
assert!(!obj.contains_key("browser_automation_engine"));
assert!(!obj.contains_key("claude_code"));
// Slim should NOT include categories
assert!(!obj.contains_key("categories"));
// Slim SHOULD include these
+5 -24
View File
@@ -260,18 +260,6 @@ impl ProviderService {
state
.db
.is_omo_provider_current(app_type.as_str(), id, "omo")?;
let omo_count = state
.db
.get_all_providers(app_type.as_str())?
.values()
.filter(|p| p.category.as_deref() == Some("omo"))
.count();
if omo_count <= 1 && was_current {
return Err(AppError::Message(
"无法删除当前启用的最后一个 OMO 配置,请先停用".to_string(),
));
}
state.db.delete_provider(app_type.as_str(), id)?;
if was_current {
@@ -287,18 +275,6 @@ impl ProviderService {
state
.db
.is_omo_provider_current(app_type.as_str(), id, "omo-slim")?;
let slim_count = state
.db
.get_all_providers(app_type.as_str())?
.values()
.filter(|p| p.category.as_deref() == Some("omo-slim"))
.count();
if slim_count <= 1 && was_current {
return Err(AppError::Message(
"无法删除当前启用的最后一个 OMO Slim 配置,请先停用".to_string(),
));
}
state.db.delete_provider(app_type.as_str(), id)?;
if was_current {
@@ -513,6 +489,8 @@ impl ProviderService {
state,
&crate::services::omo::STANDARD,
)?;
// OMO ↔ OMO Slim mutually exclusive: remove Slim config
let _ = crate::services::OmoService::delete_config_file(&crate::services::omo::SLIM);
return Ok(SwitchResult::default());
}
@@ -522,6 +500,9 @@ impl ProviderService {
.db
.set_omo_provider_current(app_type.as_str(), id, "omo-slim")?;
crate::services::OmoService::write_config_to_file(state, &crate::services::omo::SLIM)?;
// OMO ↔ OMO Slim mutually exclusive: remove Standard config
let _ =
crate::services::OmoService::delete_config_file(&crate::services::omo::STANDARD);
return Ok(SwitchResult::default());
}
+20 -5
View File
@@ -37,12 +37,27 @@ pub struct SessionMessage {
}
pub fn scan_sessions() -> Vec<SessionMeta> {
let (r1, r2, r3, r4, r5) = std::thread::scope(|s| {
let h1 = s.spawn(codex::scan_sessions);
let h2 = s.spawn(claude::scan_sessions);
let h3 = s.spawn(opencode::scan_sessions);
let h4 = s.spawn(openclaw::scan_sessions);
let h5 = s.spawn(gemini::scan_sessions);
(
h1.join().unwrap_or_default(),
h2.join().unwrap_or_default(),
h3.join().unwrap_or_default(),
h4.join().unwrap_or_default(),
h5.join().unwrap_or_default(),
)
});
let mut sessions = Vec::new();
sessions.extend(codex::scan_sessions());
sessions.extend(claude::scan_sessions());
sessions.extend(opencode::scan_sessions());
sessions.extend(openclaw::scan_sessions());
sessions.extend(gemini::scan_sessions());
sessions.extend(r1);
sessions.extend(r2);
sessions.extend(r3);
sessions.extend(r4);
sessions.extend(r5);
sessions.sort_by(|a, b| {
let a_ts = a.last_active_at.or(a.created_at).unwrap_or(0);
@@ -7,7 +7,9 @@ use serde_json::Value;
use crate::config::get_claude_config_dir;
use crate::session_manager::{SessionMessage, SessionMeta};
use super::utils::{extract_text, parse_timestamp_to_ms, path_basename, truncate_summary};
use super::utils::{
extract_text, parse_timestamp_to_ms, path_basename, read_head_tail_lines, truncate_summary,
};
const PROVIDER_ID: &str = "claude";
@@ -73,60 +75,61 @@ fn parse_session(path: &Path) -> Option<SessionMeta> {
return None;
}
let file = File::open(path).ok()?;
let reader = BufReader::new(file);
let (head, tail) = read_head_tail_lines(path, 10, 30).ok()?;
let mut session_id: Option<String> = None;
let mut project_dir: Option<String> = None;
let mut created_at: Option<i64> = None;
let mut last_active_at: Option<i64> = None;
let mut summary: Option<String> = None;
for line in reader.lines() {
let line = match line {
Ok(value) => value,
Err(_) => continue,
};
let value: Value = match serde_json::from_str(&line) {
// Extract metadata from head lines
for line in &head {
let value: Value = match serde_json::from_str(line) {
Ok(parsed) => parsed,
Err(_) => continue,
};
if session_id.is_none() {
session_id = value
.get("sessionId")
.and_then(Value::as_str)
.map(|s| s.to_string());
}
if project_dir.is_none() {
project_dir = value
.get("cwd")
.and_then(Value::as_str)
.map(|s| s.to_string());
}
if let Some(ts) = value.get("timestamp").and_then(parse_timestamp_to_ms) {
if created_at.is_none() {
created_at = Some(ts);
}
last_active_at = Some(ts);
if created_at.is_none() {
created_at = value.get("timestamp").and_then(parse_timestamp_to_ms);
}
}
if value.get("isMeta").and_then(Value::as_bool) == Some(true) {
continue;
}
// Extract last_active_at and summary from tail lines (reverse order)
let mut last_active_at: Option<i64> = None;
let mut summary: Option<String> = None;
let message = match value.get("message") {
Some(message) => message,
None => continue,
for line in tail.iter().rev() {
let value: Value = match serde_json::from_str(line) {
Ok(parsed) => parsed,
Err(_) => continue,
};
let text = message.get("content").map(extract_text).unwrap_or_default();
if text.trim().is_empty() {
continue;
if last_active_at.is_none() {
last_active_at = value.get("timestamp").and_then(parse_timestamp_to_ms);
}
if summary.is_none() {
if value.get("isMeta").and_then(Value::as_bool) == Some(true) {
continue;
}
if let Some(message) = value.get("message") {
let text = message.get("content").map(extract_text).unwrap_or_default();
if !text.trim().is_empty() {
summary = Some(text);
}
}
}
if last_active_at.is_some() && summary.is_some() {
break;
}
summary = Some(text);
}
let session_id = session_id.or_else(|| infer_session_id_from_filename(path));
@@ -1,6 +1,7 @@
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use regex::Regex;
use serde_json::Value;
@@ -8,10 +9,17 @@ use serde_json::Value;
use crate::codex_config::get_codex_config_dir;
use crate::session_manager::{SessionMessage, SessionMeta};
use super::utils::{extract_text, parse_timestamp_to_ms, path_basename, truncate_summary};
use super::utils::{
extract_text, parse_timestamp_to_ms, path_basename, read_head_tail_lines, truncate_summary,
};
const PROVIDER_ID: &str = "codex";
static UUID_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}")
.unwrap()
});
pub fn scan_sessions() -> Vec<SessionMeta> {
let root = get_codex_config_dir().join("sessions");
let mut files = Vec::new();
@@ -74,32 +82,21 @@ pub fn load_messages(path: &Path) -> Result<Vec<SessionMessage>, String> {
}
fn parse_session(path: &Path) -> Option<SessionMeta> {
let file = File::open(path).ok()?;
let reader = BufReader::new(file);
let (head, tail) = read_head_tail_lines(path, 10, 30).ok()?;
let mut session_id: Option<String> = None;
let mut project_dir: Option<String> = None;
let mut created_at: Option<i64> = None;
let mut last_active_at: Option<i64> = None;
let mut summary: Option<String> = None;
for line in reader.lines() {
let line = match line {
Ok(value) => value,
Err(_) => continue,
};
let value: Value = match serde_json::from_str(&line) {
// Extract metadata from head lines
for line in &head {
let value: Value = match serde_json::from_str(line) {
Ok(parsed) => parsed,
Err(_) => continue,
};
if let Some(ts) = value.get("timestamp").and_then(parse_timestamp_to_ms) {
if created_at.is_none() {
created_at = Some(ts);
}
last_active_at = Some(ts);
if created_at.is_none() {
created_at = value.get("timestamp").and_then(parse_timestamp_to_ms);
}
if value.get("type").and_then(Value::as_str) == Some("session_meta") {
if let Some(payload) = value.get("payload") {
if session_id.is_none() {
@@ -118,27 +115,34 @@ fn parse_session(path: &Path) -> Option<SessionMeta> {
created_at.get_or_insert(ts);
}
}
continue;
}
}
if value.get("type").and_then(Value::as_str) != Some("response_item") {
continue;
}
// Extract last_active_at and summary from tail lines (reverse order)
let mut last_active_at: Option<i64> = None;
let mut summary: Option<String> = None;
let payload = match value.get("payload") {
Some(payload) => payload,
None => continue,
for line in tail.iter().rev() {
let value: Value = match serde_json::from_str(line) {
Ok(parsed) => parsed,
Err(_) => continue,
};
if payload.get("type").and_then(Value::as_str) != Some("message") {
continue;
if last_active_at.is_none() {
last_active_at = value.get("timestamp").and_then(parse_timestamp_to_ms);
}
let text = payload.get("content").map(extract_text).unwrap_or_default();
if text.trim().is_empty() {
continue;
if summary.is_none() && value.get("type").and_then(Value::as_str) == Some("response_item") {
if let Some(payload) = value.get("payload") {
if payload.get("type").and_then(Value::as_str) == Some("message") {
let text = payload.get("content").map(extract_text).unwrap_or_default();
if !text.trim().is_empty() {
summary = Some(text);
}
}
}
}
if last_active_at.is_some() && summary.is_some() {
break;
}
summary = Some(text);
}
let session_id = session_id.or_else(|| infer_session_id_from_filename(path));
@@ -166,10 +170,7 @@ fn parse_session(path: &Path) -> Option<SessionMeta> {
fn infer_session_id_from_filename(path: &Path) -> Option<String> {
let file_name = path.file_name()?.to_string_lossy();
let re =
Regex::new(r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}")
.ok()?;
re.find(&file_name).map(|mat| mat.as_str().to_string())
UUID_RE.find(&file_name).map(|mat| mat.as_str().to_string())
}
fn collect_jsonl_files(root: &Path, files: &mut Vec<PathBuf>) {
@@ -7,7 +7,9 @@ use serde_json::Value;
use crate::openclaw_config::get_openclaw_dir;
use crate::session_manager::{SessionMessage, SessionMeta};
use super::utils::{extract_text, parse_timestamp_to_ms, path_basename, truncate_summary};
use super::utils::{
extract_text, parse_timestamp_to_ms, path_basename, read_head_tail_lines, truncate_summary,
};
const PROVIDER_ID: &str = "openclaw";
@@ -114,30 +116,22 @@ pub fn load_messages(path: &Path) -> Result<Vec<SessionMessage>, String> {
}
fn parse_session(path: &Path) -> Option<SessionMeta> {
let file = File::open(path).ok()?;
let reader = BufReader::new(file);
let (head, tail) = read_head_tail_lines(path, 10, 30).ok()?;
let mut session_id: Option<String> = None;
let mut cwd: Option<String> = None;
let mut created_at: Option<i64> = None;
let mut last_active_at: Option<i64> = None;
let mut summary: Option<String> = None;
for line in reader.lines() {
let line = match line {
Ok(value) => value,
Err(_) => continue,
};
let value: Value = match serde_json::from_str(&line) {
// Extract metadata and first message summary from head lines
for line in &head {
let value: Value = match serde_json::from_str(line) {
Ok(parsed) => parsed,
Err(_) => continue,
};
if let Some(ts) = value.get("timestamp").and_then(parse_timestamp_to_ms) {
if created_at.is_none() {
created_at = Some(ts);
}
last_active_at = Some(ts);
if created_at.is_none() {
created_at = value.get("timestamp").and_then(parse_timestamp_to_ms);
}
let event_type = value.get("type").and_then(Value::as_str).unwrap_or("");
@@ -161,25 +155,28 @@ fn parse_session(path: &Path) -> Option<SessionMeta> {
continue;
}
if event_type != "message" {
continue;
// OpenClaw summary is the first message content
if event_type == "message" && summary.is_none() {
if let Some(message) = value.get("message") {
let text = message.get("content").map(extract_text).unwrap_or_default();
if !text.trim().is_empty() {
summary = Some(text);
}
}
}
}
// Extract first message content for summary
if summary.is_some() {
continue;
}
let message = match value.get("message") {
Some(msg) => msg,
None => continue,
// Extract last_active_at from tail lines (reverse order)
let mut last_active_at: Option<i64> = None;
for line in tail.iter().rev() {
let value: Value = match serde_json::from_str(line) {
Ok(parsed) => parsed,
Err(_) => continue,
};
let text = message.get("content").map(extract_text).unwrap_or_default();
if text.trim().is_empty() {
continue;
if let Some(ts) = value.get("timestamp").and_then(parse_timestamp_to_ms) {
last_active_at = Some(ts);
break;
}
summary = Some(text);
}
// Fall back to filename as session ID
@@ -136,6 +136,7 @@ fn parse_session(storage: &Path, path: &Path) -> Option<SessionMeta> {
.and_then(parse_timestamp_to_ms);
// Derive title from directory basename if no explicit title
let has_title = title.is_some();
let display_title = title.or_else(|| {
directory
.as_deref()
@@ -147,8 +148,12 @@ fn parse_session(storage: &Path, path: &Path) -> Option<SessionMeta> {
let msg_dir = storage.join("message").join(&session_id);
let source_path = msg_dir.to_string_lossy().to_string();
// Get summary from first user message
let summary = get_first_user_summary(storage, &session_id);
// Skip expensive I/O if title already available from session JSON
let summary = if has_title {
display_title.clone()
} else {
get_first_user_summary(storage, &session_id)
};
Some(SessionMeta {
provider_id: PROVIDER_ID.to_string(),
@@ -1,6 +1,50 @@
use std::fs::File;
use std::io::{self, BufRead, BufReader, Seek, SeekFrom};
use std::path::Path;
use chrono::{DateTime, FixedOffset};
use serde_json::Value;
/// Read the first `head_n` lines and last `tail_n` lines from a file.
/// For small files (< 16 KB), reads all lines once to avoid unnecessary seeking.
pub fn read_head_tail_lines(
path: &Path,
head_n: usize,
tail_n: usize,
) -> io::Result<(Vec<String>, Vec<String>)> {
let file = File::open(path)?;
let file_len = file.metadata()?.len();
// For small files, read all lines once and split
if file_len < 16_384 {
let reader = BufReader::new(file);
let all: Vec<String> = reader.lines().map_while(Result::ok).collect();
let head = all.iter().take(head_n).cloned().collect();
let skip = all.len().saturating_sub(tail_n);
let tail = all.into_iter().skip(skip).collect();
return Ok((head, tail));
}
// Read head lines from the beginning
let reader = BufReader::new(file);
let head: Vec<String> = reader.lines().take(head_n).map_while(Result::ok).collect();
// Seek to last ~16 KB for tail lines
let seek_pos = file_len.saturating_sub(16_384);
let mut file2 = File::open(path)?;
file2.seek(SeekFrom::Start(seek_pos))?;
let tail_reader = BufReader::new(file2);
let all_tail: Vec<String> = tail_reader.lines().map_while(Result::ok).collect();
// Skip first partial line if we seeked into the middle of a line
let skip_first = if seek_pos > 0 { 1 } else { 0 };
let usable: Vec<String> = all_tail.into_iter().skip(skip_first).collect();
let skip = usable.len().saturating_sub(tail_n);
let tail = usable.into_iter().skip(skip).collect();
Ok((head, tail))
}
pub fn parse_timestamp_to_ms(value: &Value) -> Option<i64> {
let raw = value.as_str()?;
DateTime::parse_from_rfc3339(raw)
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "CC Switch",
"version": "3.10.3",
"version": "3.11.0",
"identifier": "com.ccswitch.desktop",
"build": {
"frontendDist": "../dist",
-3
View File
@@ -53,15 +53,12 @@ export function DeepLinkImportDialog() {
const unlistenImport = listen<DeepLinkImportRequest>(
"deeplink-import",
async (event) => {
console.log("Deep link import event received:", event.payload);
// If config is present, merge it to get the complete configuration
if (event.payload.config || event.payload.configUrl) {
try {
const mergedRequest = await deeplinkApi.mergeDeeplinkConfig(
event.payload,
);
console.log("Config merged successfully:", mergedRequest);
setRequest(mergedRequest);
} catch (error) {
console.error("Failed to merge config:", error);
@@ -192,6 +192,7 @@ export function EditProviderDialog({
onCancel={() => onOpenChange(false)}
initialData={initialData}
showButtons={false}
isCurrent={liveSettings !== null}
/>
</FullScreenPanel>
);
+1 -7
View File
@@ -24,7 +24,6 @@ interface ProviderActionsProps {
isTesting?: boolean;
isProxyTakeover?: boolean;
isOmo?: boolean;
isLastOmo?: boolean;
onSwitch: () => void;
onEdit: () => void;
onDuplicate: () => void;
@@ -49,7 +48,6 @@ export function ProviderActions({
isTesting: _isTesting, // Hidden: stream check feature disabled
isProxyTakeover = false,
isOmo = false,
isLastOmo = false,
onSwitch,
onEdit,
onDuplicate,
@@ -192,11 +190,7 @@ export function ProviderActions({
const buttonState = getMainButtonState();
const canDelete = isOmo
? !(isLastOmo && isCurrent)
: isAdditiveMode
? true
: !isCurrent;
const canDelete = isOmo || isAdditiveMode ? true : !isCurrent;
return (
<div className="flex items-center gap-1.5">
@@ -28,9 +28,7 @@ interface ProviderCardProps {
appId: AppId;
isInConfig?: boolean; // OpenCode: 是否已添加到 opencode.json
isOmo?: boolean;
isLastOmo?: boolean;
isOmoSlim?: boolean;
isLastOmoSlim?: boolean;
onSwitch: (provider: Provider) => void;
onEdit: (provider: Provider) => void;
onDelete: (provider: Provider) => void;
@@ -94,9 +92,7 @@ export function ProviderCard({
appId,
isInConfig = true,
isOmo = false,
isLastOmo = false,
isOmoSlim = false,
isLastOmoSlim = false,
onSwitch,
onEdit,
onDelete,
@@ -125,7 +121,6 @@ export function ProviderCard({
// OMO and OMO Slim share the same card behavior
const isAnyOmo = isOmo || isOmoSlim;
const isLastAnyOmo = isOmo ? isLastOmo : isLastOmoSlim;
const handleDisableAnyOmo = isOmoSlim ? onDisableOmoSlim : onDisableOmo;
const { data: health } = useProviderHealth(provider.id, appId);
@@ -390,7 +385,6 @@ export function ProviderCard({
isTesting={isTesting}
isProxyTakeover={isProxyTakeover}
isOmo={isAnyOmo}
isLastOmo={isLastAnyOmo}
onSwitch={() => onSwitch(provider)}
onEdit={() => onEdit(provider)}
onDuplicate={() => onDuplicate(provider)}
-18
View File
@@ -36,9 +36,7 @@ import {
} from "@/lib/query/failover";
import {
useCurrentOmoProviderId,
useOmoProviderCount,
useCurrentOmoSlimProviderId,
useOmoSlimProviderCount,
} from "@/lib/query/omo";
import { useCallback } from "react";
import { Input } from "@/components/ui/input";
@@ -142,9 +140,7 @@ export function ProviderList({
const isOpenCode = appId === "opencode";
const { data: currentOmoId } = useCurrentOmoProviderId(isOpenCode);
const { data: omoProviderCount } = useOmoProviderCount(isOpenCode);
const { data: currentOmoSlimId } = useCurrentOmoSlimProviderId(isOpenCode);
const { data: omoSlimProviderCount } = useOmoSlimProviderCount(isOpenCode);
const getFailoverPriority = useCallback(
(providerId: string): number | undefined => {
@@ -293,15 +289,7 @@ export function ProviderList({
appId={appId}
isInConfig={isProviderInConfig(provider.id)}
isOmo={isOmo}
isLastOmo={
isOmo && (omoProviderCount ?? 0) <= 1 && isOmoCurrent
}
isOmoSlim={isOmoSlim}
isLastOmoSlim={
isOmoSlim &&
(omoSlimProviderCount ?? 0) <= 1 &&
isOmoSlimCurrent
}
onSwitch={onSwitch}
onEdit={onEdit}
onDelete={onDelete}
@@ -420,9 +408,7 @@ interface SortableProviderCardProps {
appId: AppId;
isInConfig: boolean;
isOmo: boolean;
isLastOmo: boolean;
isOmoSlim: boolean;
isLastOmoSlim: boolean;
onSwitch: (provider: Provider) => void;
onEdit: (provider: Provider) => void;
onDelete: (provider: Provider) => void;
@@ -453,9 +439,7 @@ function SortableProviderCard({
appId,
isInConfig,
isOmo,
isLastOmo,
isOmoSlim,
isLastOmoSlim,
onSwitch,
onEdit,
onDelete,
@@ -500,9 +484,7 @@ function SortableProviderCard({
appId={appId}
isInConfig={isInConfig}
isOmo={isOmo}
isLastOmo={isLastOmo}
isOmoSlim={isOmoSlim}
isLastOmoSlim={isLastOmoSlim}
onSwitch={onSwitch}
onEdit={onEdit}
onDelete={onDelete}
@@ -1,164 +0,0 @@
import { useTranslation } from "react-i18next";
import { useEffect, useState } from "react";
import { FullScreenPanel } from "@/components/common/FullScreenPanel";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { Save, FolderInput, Loader2 } from "lucide-react";
import JsonEditor from "@/components/JsonEditor";
import {
OmoGlobalConfigFields,
type OmoGlobalConfigFieldsRef,
} from "./OmoGlobalConfigFields";
import type { OmoGlobalConfig } from "@/types/omo";
interface OmoCommonConfigEditorProps {
previewValue: string;
useCommonConfig: boolean;
onCommonConfigToggle: (checked: boolean) => void;
isModalOpen: boolean;
onEditClick: () => void;
onModalClose: () => void;
onSave: () => Promise<void>;
isSaving: boolean;
onGlobalConfigStateChange: (config: OmoGlobalConfig) => void;
globalConfigRef: React.RefObject<OmoGlobalConfigFieldsRef | null>;
fieldsKey: number;
isSlim?: boolean;
}
export function OmoCommonConfigEditor({
previewValue,
useCommonConfig,
onCommonConfigToggle,
isModalOpen,
onEditClick,
onModalClose,
onSave,
isSaving,
onGlobalConfigStateChange,
globalConfigRef,
fieldsKey,
isSlim = false,
}: OmoCommonConfigEditorProps) {
const { t } = useTranslation();
const [isDarkMode, setIsDarkMode] = useState(false);
const [isImporting, setIsImporting] = useState(false);
useEffect(() => {
const syncDarkMode = () =>
setIsDarkMode(document.documentElement.classList.contains("dark"));
syncDarkMode();
const observer = new MutationObserver(syncDarkMode);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
});
return () => observer.disconnect();
}, []);
const handleImportLocal = async () => {
if (!globalConfigRef.current) return;
setIsImporting(true);
try {
await globalConfigRef.current.importFromLocal();
} finally {
setIsImporting(false);
}
};
return (
<>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label>{t("provider.configJson")}</Label>
<div className="flex items-center gap-2">
<label className="inline-flex items-center gap-2 text-sm text-muted-foreground cursor-pointer">
<input
type="checkbox"
checked={useCommonConfig}
onChange={(e) => onCommonConfigToggle(e.target.checked)}
className="w-4 h-4 text-blue-500 bg-white dark:bg-gray-800 border-border-default rounded focus:ring-blue-500 dark:focus:ring-blue-400 focus:ring-2"
/>
<span>
{t("omo.writeCommonConfig", {
defaultValue: "Write to common config",
})}
</span>
</label>
</div>
</div>
<div className="flex items-center justify-end">
<button
type="button"
onClick={onEditClick}
className="text-xs text-blue-400 dark:text-blue-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
>
{t("omo.editCommonConfig", { defaultValue: "Edit common config" })}
</button>
</div>
<JsonEditor
value={previewValue}
onChange={() => {}}
darkMode={isDarkMode}
rows={14}
showValidation={false}
language="json"
/>
</div>
<FullScreenPanel
isOpen={isModalOpen}
title={t("omo.editCommonConfigTitle", {
defaultValue: "Edit OMO Common Config",
})}
onClose={onModalClose}
footer={
<>
<Button
type="button"
variant="outline"
onClick={handleImportLocal}
disabled={isImporting}
className="gap-2"
>
{isImporting ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<FolderInput className="w-4 h-4" />
)}
{t("common.import", { defaultValue: "Import" })}
</Button>
<Button type="button" variant="outline" onClick={onModalClose}>
{t("common.cancel")}
</Button>
<Button
type="button"
onClick={onSave}
disabled={isSaving}
className="gap-2"
>
{isSaving ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Save className="w-4 h-4" />
)}
{t("common.save")}
</Button>
</>
}
>
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
{t("omo.commonConfigHint", {
defaultValue:
"OMO common config will be merged into all OMO configs that enable it",
})}
</p>
<OmoGlobalConfigFields
key={fieldsKey}
ref={globalConfigRef as React.Ref<OmoGlobalConfigFieldsRef>}
onStateChange={onGlobalConfigStateChange}
hideSaveButtons
isSlim={isSlim}
/>
</div>
</FullScreenPanel>
</>
);
}
@@ -722,14 +722,23 @@ export function OmoFormFields({
return;
}
let filledCount = 0;
let alreadySetCount = 0;
let unmatchedCount = 0;
const updatedAgents = { ...agents };
for (const agentDef of builtinAgentDefs) {
const recommendedValue = resolveRecommendedModel(agentDef.recommended);
if (recommendedValue && !updatedAgents[agentDef.key]?.model) {
if (!recommendedValue) {
unmatchedCount++;
} else if (updatedAgents[agentDef.key]?.model) {
alreadySetCount++;
} else {
updatedAgents[agentDef.key] = {
...updatedAgents[agentDef.key],
model: recommendedValue,
};
filledCount++;
}
}
onAgentsChange(updatedAgents);
@@ -738,15 +747,50 @@ export function OmoFormFields({
const updatedCategories = { ...categories };
for (const catDef of OMO_BUILTIN_CATEGORIES) {
const recommendedValue = resolveRecommendedModel(catDef.recommended);
if (recommendedValue && !updatedCategories[catDef.key]?.model) {
if (!recommendedValue) {
unmatchedCount++;
} else if (updatedCategories[catDef.key]?.model) {
alreadySetCount++;
} else {
updatedCategories[catDef.key] = {
...updatedCategories[catDef.key],
model: recommendedValue,
};
filledCount++;
}
}
onCategoriesChange(updatedCategories);
}
if (filledCount > 0 && unmatchedCount === 0) {
toast.success(
t("omo.fillRecommendedSuccess", {
defaultValue: "Filled {{count}} recommended models",
count: filledCount,
}),
);
} else if (filledCount > 0 && unmatchedCount > 0) {
toast.success(
t("omo.fillRecommendedPartial", {
defaultValue:
"Filled {{filled}} recommended models, {{unmatched}} unmatched",
filled: filledCount,
unmatched: unmatchedCount,
}),
);
} else if (alreadySetCount > 0 && unmatchedCount === 0) {
toast.info(
t("omo.fillRecommendedAllSet", {
defaultValue: "All slots already have models configured",
}),
);
} else {
toast.warning(
t("omo.fillRecommendedNoMatch", {
defaultValue: "Recommended models not found in configured providers",
}),
);
}
};
const configuredAgentCount = Object.keys(agents).length;
@@ -1,773 +0,0 @@
import {
useState,
useEffect,
useCallback,
forwardRef,
useImperativeHandle,
} from "react";
import { useTranslation } from "react-i18next";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Save,
Loader2,
X,
FolderInput,
RotateCcw,
ChevronsUpDown,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { toast } from "sonner";
import type { OmoGlobalConfig } from "@/types/omo";
import {
OMO_DISABLEABLE_AGENTS,
OMO_DISABLEABLE_MCPS,
OMO_DISABLEABLE_HOOKS,
OMO_DISABLEABLE_SKILLS,
OMO_DEFAULT_SCHEMA_URL,
OMO_SISYPHUS_AGENT_PLACEHOLDER,
OMO_LSP_PLACEHOLDER,
OMO_EXPERIMENTAL_PLACEHOLDER,
OMO_BACKGROUND_TASK_PLACEHOLDER,
OMO_BROWSER_AUTOMATION_PLACEHOLDER,
OMO_CLAUDE_CODE_PLACEHOLDER,
OMO_SLIM_DISABLEABLE_AGENTS,
OMO_SLIM_DISABLEABLE_MCPS,
OMO_SLIM_DISABLEABLE_HOOKS,
OMO_SLIM_DEFAULT_SCHEMA_URL,
} from "@/types/omo";
import {
useOmoGlobalConfig,
useSaveOmoGlobalConfig,
useReadOmoLocalFile,
useOmoSlimGlobalConfig,
useSaveOmoSlimGlobalConfig,
useReadOmoSlimLocalFile,
} from "@/lib/query/omo";
interface PresetOption {
readonly value: string;
readonly label: string;
}
export interface OmoGlobalConfigFieldsRef {
buildCurrentConfig: () => OmoGlobalConfig;
buildCurrentConfigStrict: () => OmoGlobalConfig;
importFromLocal: () => Promise<void>;
}
interface OmoGlobalConfigFieldsProps {
onStateChange?: (config: OmoGlobalConfig) => void;
hideSaveButtons?: boolean;
isSlim?: boolean;
}
type OmoAdvancedFieldKey =
| "lspStr"
| "experimentalStr"
| "backgroundTaskStr"
| "browserStr"
| "claudeCodeStr";
const OMO_ADVANCED_JSON_FIELDS: ReadonlyArray<{
key: OmoAdvancedFieldKey;
labelKey: string;
defaultLabel: string;
placeholder: string;
minHeight: string;
}> = [
{
key: "lspStr",
labelKey: "omo.advancedLsp",
defaultLabel: "LSP Config",
placeholder: OMO_LSP_PLACEHOLDER,
minHeight: "200px",
},
{
key: "experimentalStr",
labelKey: "omo.advancedExperimental",
defaultLabel: "Experimental Features",
placeholder: OMO_EXPERIMENTAL_PLACEHOLDER,
minHeight: "120px",
},
{
key: "backgroundTaskStr",
labelKey: "omo.advancedBackgroundTask",
defaultLabel: "Background Tasks",
placeholder: OMO_BACKGROUND_TASK_PLACEHOLDER,
minHeight: "250px",
},
{
key: "browserStr",
labelKey: "omo.advancedBrowserAutomation",
defaultLabel: "Browser Automation",
placeholder: OMO_BROWSER_AUTOMATION_PLACEHOLDER,
minHeight: "80px",
},
{
key: "claudeCodeStr",
labelKey: "omo.advancedClaudeCode",
defaultLabel: "Claude Code",
placeholder: OMO_CLAUDE_CODE_PLACEHOLDER,
minHeight: "180px",
},
];
const OMO_SLIM_ADVANCED_KEYS: ReadonlySet<OmoAdvancedFieldKey> = new Set([
"lspStr",
"experimentalStr",
]);
function TagListEditor({
label,
values,
onChange,
placeholder,
presets,
}: {
label: string;
values: string[];
onChange: (values: string[]) => void;
placeholder?: string;
presets?: readonly PresetOption[];
}) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [search, setSearch] = useState("");
const toggleValue = (v: string) => {
if (values.includes(v)) {
onChange(values.filter((x) => x !== v));
} else {
onChange([...values, v]);
}
};
const customValue = search.trim();
const canAddCustom = customValue.length > 0 && !values.includes(customValue);
const triggerText =
values.length === 0
? placeholder || t("omo.selectPlaceholder", { defaultValue: "Select..." })
: values.length === 1
? values[0]
: `${values[0]} +${values.length - 1}`;
const availablePresets = presets?.filter(
(p) =>
!search.trim() ||
p.label.toLowerCase().includes(search.toLowerCase()) ||
p.value.toLowerCase().includes(search.toLowerCase()),
);
return (
<div className="space-y-1.5">
<div className="flex items-center justify-between">
<Label className="text-sm">{label}</Label>
{values.length > 0 && (
<Button
type="button"
variant="ghost"
size="sm"
className="h-6 px-1.5 text-xs text-muted-foreground"
onClick={() => onChange([])}
>
{t("omo.clear", { defaultValue: "Clear" })}
</Button>
)}
</div>
{values.length > 0 && (
<div className="flex flex-wrap gap-1">
{values.map((v, i) => (
<Badge
key={`${v}-${i}`}
variant="secondary"
className="text-xs gap-1"
>
{v}
<button
type="button"
onClick={() => onChange(values.filter((_, idx) => idx !== i))}
className="hover:text-destructive"
>
<X className="h-3 w-3" />
</button>
</Badge>
))}
</div>
)}
<DropdownMenu open={open} onOpenChange={setOpen} modal={false}>
<DropdownMenuTrigger asChild>
<button
type="button"
className={cn(
"flex items-center justify-between w-full h-8 px-3 rounded-md border border-input bg-background text-sm",
"hover:bg-accent hover:text-accent-foreground transition-colors",
open && "ring-2 ring-ring",
)}
aria-expanded={open}
>
<span
className={cn(
"truncate",
values.length > 0 ? "text-foreground" : "text-muted-foreground",
)}
>
{triggerText}
</span>
<ChevronsUpDown className="h-4 w-4 shrink-0 text-muted-foreground" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
sideOffset={6}
className="w-[var(--radix-dropdown-menu-trigger-width)] p-0 z-[120]"
>
<div className="p-1.5 border-b border-border/30">
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
onKeyDown={(e) => {
e.stopPropagation();
if (e.key === "Enter" && canAddCustom) {
e.preventDefault();
onChange([...values, customValue]);
setSearch("");
}
}}
placeholder={
placeholder ||
t("omo.searchOrType", {
defaultValue: "Search or type custom value...",
})
}
className="h-7 text-sm"
autoFocus
/>
</div>
{canAddCustom && (
<button
type="button"
className="w-full px-2.5 py-1.5 text-left text-sm border-b border-border/30 hover:bg-accent"
onMouseDown={(e) => e.preventDefault()}
onClick={() => {
onChange([...values, customValue]);
setSearch("");
}}
>
+ {customValue}
</button>
)}
<div className="max-h-48 overflow-auto py-1">
{availablePresets && availablePresets.length > 0 ? (
availablePresets.map((p) => {
const checked = values.includes(p.value);
return (
<DropdownMenuCheckboxItem
key={p.value}
checked={checked}
onSelect={(e) => e.preventDefault()}
onCheckedChange={() => toggleValue(p.value)}
className="text-sm"
>
{p.label}
</DropdownMenuCheckboxItem>
);
})
) : (
<div className="px-2.5 py-2 text-sm text-muted-foreground">
{t("omo.noMatches", { defaultValue: "No matches" })}
</div>
)}
</div>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}
function JsonTextareaField({
label,
value,
onChange,
placeholder,
minHeight,
}: {
label: string;
value: string;
onChange: (value: string) => void;
placeholder?: string;
minHeight?: string;
}) {
return (
<div className="space-y-1.5">
<Label className="text-sm">{label}</Label>
<Textarea
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder || "{}"}
className="font-mono text-sm"
style={{ minHeight: minHeight || "100px" }}
/>
</div>
);
}
export const OmoGlobalConfigFields = forwardRef<
OmoGlobalConfigFieldsRef,
OmoGlobalConfigFieldsProps
>(function OmoGlobalConfigFields(
{ onStateChange, hideSaveButtons, isSlim = false },
ref,
) {
const { t } = useTranslation();
const { data: standardConfig } = useOmoGlobalConfig(!isSlim);
const { data: slimConfig } = useOmoSlimGlobalConfig(isSlim);
const config = isSlim ? slimConfig : standardConfig;
const standardSaveMutation = useSaveOmoGlobalConfig();
const slimSaveMutation = useSaveOmoSlimGlobalConfig();
const saveMutation = isSlim ? slimSaveMutation : standardSaveMutation;
const standardReadLocal = useReadOmoLocalFile();
const slimReadLocal = useReadOmoSlimLocalFile();
const defaultSchemaUrl = isSlim
? OMO_SLIM_DEFAULT_SCHEMA_URL
: OMO_DEFAULT_SCHEMA_URL;
const [schemaUrl, setSchemaUrl] = useState(defaultSchemaUrl);
const [sisyphusAgentStr, setSisyphusAgentStr] = useState("");
const [disabledAgents, setDisabledAgents] = useState<string[]>([]);
const [disabledMcps, setDisabledMcps] = useState<string[]>([]);
const [disabledHooks, setDisabledHooks] = useState<string[]>([]);
const [disabledSkills, setDisabledSkills] = useState<string[]>([]);
const [lspStr, setLspStr] = useState("");
const [experimentalStr, setExperimentalStr] = useState("");
const [backgroundTaskStr, setBackgroundTaskStr] = useState("");
const [browserStr, setBrowserStr] = useState("");
const [claudeCodeStr, setClaudeCodeStr] = useState("");
const [otherFieldsStr, setOtherFieldsStr] = useState("");
const [loaded, setLoaded] = useState(false);
const applyGlobalState = useCallback((global: OmoGlobalConfig) => {
setSchemaUrl(global.schemaUrl || defaultSchemaUrl);
setSisyphusAgentStr(
global.sisyphusAgent ? JSON.stringify(global.sisyphusAgent, null, 2) : "",
);
setDisabledAgents(global.disabledAgents || []);
setDisabledMcps(global.disabledMcps || []);
setDisabledHooks(global.disabledHooks || []);
setDisabledSkills(global.disabledSkills || []);
setLspStr(global.lsp ? JSON.stringify(global.lsp, null, 2) : "");
setExperimentalStr(
global.experimental ? JSON.stringify(global.experimental, null, 2) : "",
);
setBackgroundTaskStr(
global.backgroundTask
? JSON.stringify(global.backgroundTask, null, 2)
: "",
);
setBrowserStr(
global.browserAutomationEngine
? JSON.stringify(global.browserAutomationEngine, null, 2)
: "",
);
setClaudeCodeStr(
global.claudeCode ? JSON.stringify(global.claudeCode, null, 2) : "",
);
setOtherFieldsStr(
global.otherFields ? JSON.stringify(global.otherFields, null, 2) : "",
);
}, []);
useEffect(() => {
if (config && !loaded) {
applyGlobalState(config);
setLoaded(true);
}
}, [config, loaded, applyGlobalState]);
const parseJsonField = useCallback(
(
fieldName: string,
raw: string,
strict: boolean,
): Record<string, unknown> | undefined => {
if (!raw.trim()) return undefined;
try {
const parsed: unknown = JSON.parse(raw);
if (
typeof parsed !== "object" ||
parsed === null ||
Array.isArray(parsed)
) {
if (strict) {
throw new Error(
t("omo.jsonMustBeObject", {
field: fieldName,
defaultValue: "{{field}} must be a JSON object",
}),
);
}
return undefined;
}
return parsed as Record<string, unknown>;
} catch (error) {
if (strict) {
if (error instanceof Error) {
throw error;
}
throw new Error(
t("omo.jsonInvalid", {
field: fieldName,
defaultValue: "{{field}} contains invalid JSON",
}),
);
}
return undefined;
}
},
[t],
);
const buildCurrentConfigInternal = useCallback(
(strict: boolean): OmoGlobalConfig => {
return {
id: "global",
schemaUrl: schemaUrl || undefined,
sisyphusAgent: parseJsonField(
t("omo.sisyphusAgentConfig", {
defaultValue: "Sisyphus Agent",
}),
sisyphusAgentStr,
strict,
),
disabledAgents,
disabledMcps,
disabledHooks,
disabledSkills,
lsp: parseJsonField(
t("omo.advancedLsp", { defaultValue: "LSP" }),
lspStr,
strict,
),
experimental: parseJsonField(
t("omo.advancedExperimental", { defaultValue: "Experimental" }),
experimentalStr,
strict,
),
backgroundTask: parseJsonField(
t("omo.advancedBackgroundTask", {
defaultValue: "Background Task",
}),
backgroundTaskStr,
strict,
),
browserAutomationEngine: parseJsonField(
t("omo.advancedBrowserAutomation", {
defaultValue: "Browser Automation",
}),
browserStr,
strict,
),
claudeCode: parseJsonField(
t("omo.advancedClaudeCode", { defaultValue: "Claude Code" }),
claudeCodeStr,
strict,
),
otherFields: parseJsonField(
t("omo.otherFields", {
defaultValue: "Other Config",
}),
otherFieldsStr,
strict,
),
updatedAt: new Date().toISOString(),
};
},
[
schemaUrl,
sisyphusAgentStr,
disabledAgents,
disabledMcps,
disabledHooks,
disabledSkills,
lspStr,
experimentalStr,
backgroundTaskStr,
browserStr,
claudeCodeStr,
otherFieldsStr,
parseJsonField,
],
);
const buildCurrentConfig = useCallback(
() => buildCurrentConfigInternal(false),
[buildCurrentConfigInternal],
);
const buildCurrentConfigStrict = useCallback(
() => buildCurrentConfigInternal(true),
[buildCurrentConfigInternal],
);
useEffect(() => {
if (loaded && onStateChange) {
onStateChange(buildCurrentConfig());
}
}, [loaded, onStateChange, buildCurrentConfig]);
const handleSaveGlobal = useCallback(async () => {
try {
const result = buildCurrentConfigStrict();
await saveMutation.mutateAsync(result);
toast.success(
t("omo.globalConfigSaved", {
defaultValue: "Global config saved",
}),
);
} catch (err) {
toast.error(String(err));
}
}, [buildCurrentConfigStrict, saveMutation, t]);
const disabledCount =
disabledAgents.length +
disabledMcps.length +
disabledHooks.length +
disabledSkills.length;
const advancedFieldValues: Record<OmoAdvancedFieldKey, string> = {
lspStr,
experimentalStr,
backgroundTaskStr,
browserStr,
claudeCodeStr,
};
const advancedFieldSetters: Record<
OmoAdvancedFieldKey,
(value: string) => void
> = {
lspStr: setLspStr,
experimentalStr: setExperimentalStr,
backgroundTaskStr: setBackgroundTaskStr,
browserStr: setBrowserStr,
claudeCodeStr: setClaudeCodeStr,
};
const disabledEditorConfigs = [
{
key: "agents",
label: t("omo.disabledAgents", { defaultValue: "Agents" }),
values: disabledAgents,
onChange: setDisabledAgents,
placeholder: t("omo.disabledAgentsPlaceholder", {
defaultValue: "Disabled Agents",
}),
presets: isSlim ? OMO_SLIM_DISABLEABLE_AGENTS : OMO_DISABLEABLE_AGENTS,
},
{
key: "mcps",
label: t("omo.disabledMcps", { defaultValue: "MCPs" }),
values: disabledMcps,
onChange: setDisabledMcps,
placeholder: t("omo.disabledMcpsPlaceholder", {
defaultValue: "Disabled MCPs",
}),
presets: isSlim ? OMO_SLIM_DISABLEABLE_MCPS : OMO_DISABLEABLE_MCPS,
},
{
key: "hooks",
label: t("omo.disabledHooks", { defaultValue: "Hooks" }),
values: disabledHooks,
onChange: setDisabledHooks,
placeholder: t("omo.disabledHooksPlaceholder", {
defaultValue: "Disabled Hooks",
}),
presets: isSlim ? OMO_SLIM_DISABLEABLE_HOOKS : OMO_DISABLEABLE_HOOKS,
},
...(!isSlim
? [
{
key: "skills" as const,
label: t("omo.disabledSkills", { defaultValue: "Skills" }),
values: disabledSkills,
onChange: setDisabledSkills,
placeholder: t("omo.disabledSkillsPlaceholder", {
defaultValue: "Disabled Skills",
}),
presets: OMO_DISABLEABLE_SKILLS,
},
]
: []),
];
const readLocalFile = isSlim ? slimReadLocal : standardReadLocal;
const handleImportGlobalFromLocal = useCallback(async () => {
try {
const data = await readLocalFile.mutateAsync();
applyGlobalState(data.global);
toast.success(
t("omo.importGlobalSuccess", {
defaultValue: "Imported global config from local file (unsaved)",
}),
);
} catch (err) {
toast.error(
t("omo.importGlobalFailed", {
error: String(err),
defaultValue: "Failed to read local file: {{error}}",
}),
);
}
}, [readLocalFile, applyGlobalState, t]);
useImperativeHandle(
ref,
() => ({
buildCurrentConfig,
buildCurrentConfigStrict,
importFromLocal: handleImportGlobalFromLocal,
}),
[buildCurrentConfig, buildCurrentConfigStrict, handleImportGlobalFromLocal],
);
return (
<div className="space-y-4">
{!hideSaveButtons && (
<div className="flex items-center justify-end gap-1.5">
<Button
type="button"
variant="ghost"
size="sm"
className="h-7 text-xs"
disabled={readLocalFile.isPending}
onClick={handleImportGlobalFromLocal}
>
{readLocalFile.isPending ? (
<Loader2 className="h-3.5 w-3.5 mr-1 animate-spin" />
) : (
<FolderInput className="h-3.5 w-3.5 mr-1" />
)}
{t("omo.importLocal", { defaultValue: "Import Local" })}
</Button>
<Button
type="button"
variant="outline"
size="sm"
className="h-7 text-xs"
disabled={saveMutation.isPending}
onClick={handleSaveGlobal}
>
{saveMutation.isPending ? (
<Loader2 className="h-3.5 w-3.5 mr-1 animate-spin" />
) : (
<Save className="h-3.5 w-3.5 mr-1" />
)}
{t("omo.saveGlobalConfig", { defaultValue: "Save Global Config" })}
</Button>
</div>
)}
<div className="space-y-1.5">
<div className="flex items-center justify-between">
<Label className="text-sm">
{t("omo.schemaUrl", { defaultValue: "$schema" })}
</Label>
{schemaUrl !== OMO_DEFAULT_SCHEMA_URL && (
<Button
type="button"
variant="ghost"
size="sm"
className="h-6 text-xs px-1.5"
onClick={() => setSchemaUrl(OMO_DEFAULT_SCHEMA_URL)}
>
<RotateCcw className="h-3 w-3 mr-0.5" />
{t("omo.resetDefault", { defaultValue: "Reset" })}
</Button>
)}
</div>
<Input
value={schemaUrl}
onChange={(e) => setSchemaUrl(e.target.value)}
placeholder={defaultSchemaUrl}
className="text-sm h-8"
/>
</div>
{!isSlim && (
<div className="rounded-md border border-border/40 bg-muted/10 p-2 space-y-2">
<Label className="text-sm font-semibold">
{t("omo.sisyphusAgentConfig", {
defaultValue: "Sisyphus Agent",
})}
</Label>
<Textarea
value={sisyphusAgentStr}
onChange={(e) => setSisyphusAgentStr(e.target.value)}
placeholder={OMO_SISYPHUS_AGENT_PLACEHOLDER}
className="font-mono text-sm"
style={{ minHeight: "140px" }}
/>
</div>
)}
<div className="rounded-md border border-border/40 bg-muted/10 p-2 space-y-3">
<div className="flex items-center gap-2">
<Label className="text-sm font-semibold">
{t("omo.disabledItems", { defaultValue: "Disabled Items" })}
</Label>
{disabledCount > 0 && (
<Badge variant="secondary" className="text-xs h-5">
{disabledCount}
</Badge>
)}
</div>
{disabledEditorConfigs.map((editor) => (
<TagListEditor
key={editor.key}
label={editor.label}
values={editor.values}
onChange={editor.onChange}
placeholder={editor.placeholder}
presets={editor.presets}
/>
))}
</div>
<div className="rounded-md border border-border/40 bg-muted/10 p-2 space-y-2">
<Label className="text-sm font-semibold">
{t("omo.advanced", { defaultValue: "Advanced Settings" })}
</Label>
{OMO_ADVANCED_JSON_FIELDS.filter(
(field) => !isSlim || OMO_SLIM_ADVANCED_KEYS.has(field.key),
).map((field) => (
<JsonTextareaField
key={field.key}
label={t(field.labelKey, { defaultValue: field.defaultLabel })}
value={advancedFieldValues[field.key]}
onChange={advancedFieldSetters[field.key]}
placeholder={field.placeholder}
minHeight={field.minHeight}
/>
))}
<JsonTextareaField
label={t("omo.otherFields", {
defaultValue: "Other Config",
})}
value={otherFieldsStr}
onChange={setOtherFieldsStr}
/>
</div>
</div>
);
});
+37 -43
View File
@@ -57,7 +57,6 @@ import { ClaudeFormFields } from "./ClaudeFormFields";
import { CodexFormFields } from "./CodexFormFields";
import { GeminiFormFields } from "./GeminiFormFields";
import { OmoFormFields } from "./OmoFormFields";
import { OmoCommonConfigEditor } from "./OmoCommonConfigEditor";
import { parseOmoOtherFieldsObject } from "@/types/omo";
import {
ProviderAdvancedConfig,
@@ -79,7 +78,6 @@ import {
useOmoDraftState,
useOpenclawFormState,
} from "./hooks";
import { useOmoGlobalConfig, useOmoSlimGlobalConfig } from "@/lib/query/omo";
import {
CLAUDE_DEFAULT_CONFIG,
CODEX_DEFAULT_CONFIG,
@@ -118,6 +116,7 @@ interface ProviderFormProps {
iconColor?: string;
};
showButtons?: boolean;
isCurrent?: boolean;
}
export function ProviderForm({
@@ -130,6 +129,7 @@ export function ProviderForm({
onManageUniversalProviders,
initialData,
showButtons = true,
isCurrent = false,
}: ProviderFormProps) {
const { t } = useTranslation();
const isEditMode = Boolean(initialData);
@@ -187,13 +187,6 @@ export function ProviderForm({
const isOmoCategory = appId === "opencode" && category === "omo";
const isOmoSlimCategory = appId === "opencode" && category === "omo-slim";
const isAnyOmoCategory = isOmoCategory || isOmoSlimCategory;
const { data: queriedStandardOmoGlobalConfig } =
useOmoGlobalConfig(isOmoCategory);
const { data: queriedSlimOmoGlobalConfig } =
useOmoSlimGlobalConfig(isOmoSlimCategory);
const queriedOmoGlobalConfig = isOmoSlimCategory
? queriedSlimOmoGlobalConfig
: queriedStandardOmoGlobalConfig;
useEffect(() => {
setSelectedPresetId(initialData ? null : "custom");
@@ -522,7 +515,6 @@ export function ProviderForm({
const omoDraft = useOmoDraftState({
initialOmoSettings,
queriedOmoGlobalConfig,
isEditMode,
appId,
category,
@@ -691,7 +683,6 @@ export function ProviderForm({
(category === "omo" || category === "omo-slim")
) {
const omoConfig: Record<string, unknown> = {};
omoConfig.useCommonConfig = omoDraft.useOmoCommonConfig;
if (Object.keys(omoDraft.omoAgents).length > 0) {
omoConfig.agents = omoDraft.omoAgents;
}
@@ -1252,7 +1243,8 @@ export function ProviderForm({
<ClaudeFormFields
providerId={providerId}
shouldShowApiKey={
hasApiKeyField(form.getValues("settingsConfig"), "claude") &&
(category !== "cloud_provider" ||
hasApiKeyField(form.getValues("settingsConfig"), "claude")) &&
shouldShowApiKey(form.getValues("settingsConfig"), isEditMode)
}
apiKey={apiKey}
@@ -1435,20 +1427,16 @@ export function ProviderForm({
</>
) : appId === "opencode" &&
(category === "omo" || category === "omo-slim") ? (
<OmoCommonConfigEditor
previewValue={omoDraft.mergedOmoJsonPreview}
useCommonConfig={omoDraft.useOmoCommonConfig}
onCommonConfigToggle={omoDraft.setUseOmoCommonConfig}
isModalOpen={omoDraft.isOmoConfigModalOpen}
onEditClick={omoDraft.handleOmoEditClick}
onModalClose={() => omoDraft.setIsOmoConfigModalOpen(false)}
onSave={omoDraft.handleOmoGlobalConfigSave}
isSaving={omoDraft.isOmoSaving}
onGlobalConfigStateChange={omoDraft.setOmoGlobalState}
globalConfigRef={omoDraft.omoGlobalConfigRef}
fieldsKey={omoDraft.omoFieldsKey}
isSlim={category === "omo-slim"}
/>
<div className="space-y-2">
<Label>{t("provider.configJson")}</Label>
<JsonEditor
value={omoDraft.mergedOmoJsonPreview}
onChange={() => {}}
rows={14}
showValidation={false}
language="json"
/>
</div>
) : appId === "opencode" &&
category !== "omo" &&
category !== "omo-slim" ? (
@@ -1507,22 +1495,24 @@ export function ProviderForm({
<Label htmlFor="settingsConfig">
{t("claudeConfig.configLabel")}
</Label>
<ClaudeQuickToggles
onPatchApplied={(patch) => {
try {
const cfg = JSON.parse(
form.getValues("settingsConfig") || "{}",
);
jsonMergePatch(cfg, patch);
form.setValue(
"settingsConfig",
JSON.stringify(cfg, null, 2),
);
} catch {
// invalid JSON in editor — skip mirror
}
}}
/>
{isEditMode && isCurrent && (
<ClaudeQuickToggles
onPatchApplied={(patch) => {
try {
const cfg = JSON.parse(
form.getValues("settingsConfig") || "{}",
);
jsonMergePatch(cfg, patch);
form.setValue(
"settingsConfig",
JSON.stringify(cfg, null, 2),
);
} catch {
// invalid JSON in editor — skip mirror
}
}}
/>
)}
<JsonEditor
value={form.watch("settingsConfig")}
onChange={(value) => form.setValue("settingsConfig", value)}
@@ -1536,7 +1526,11 @@ export function ProviderForm({
language="json"
/>
<p className="text-xs text-muted-foreground">
{t("claudeConfig.fullSettingsHint")}
{t(
isCurrent
? "claudeConfig.fullSettingsHint"
: "claudeConfig.fragmentSettingsHint",
)}
</p>
</div>
{settingsConfigErrorField}
@@ -1,5 +1,4 @@
import type { OpenCodeModel, OpenCodeProviderConfig } from "@/types";
import type { OmoGlobalConfig } from "@/types/omo";
import type { PricingModelSourceOption } from "../ProviderAdvancedConfig";
// ── Default configs ──────────────────────────────────────────────────
@@ -52,15 +51,6 @@ export const OPENCLAW_DEFAULT_CONFIG = JSON.stringify(
2,
);
export const EMPTY_OMO_GLOBAL_CONFIG: OmoGlobalConfig = {
id: "global",
disabledAgents: [],
disabledMcps: [],
disabledHooks: [],
disabledSkills: [],
updatedAt: "",
};
// ── Pure functions ───────────────────────────────────────────────────
export function isKnownOpencodeOptionKey(key: string): boolean {
@@ -1,22 +1,11 @@
import { useState, useCallback, useEffect, useRef, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import type { OmoGlobalConfig } from "@/types/omo";
import { useState, useCallback, useMemo } from "react";
import {
mergeOmoConfigPreview,
mergeOmoSlimConfigPreview,
buildOmoSlimProfilePreview,
} from "@/types/omo";
import { type OmoGlobalConfigFieldsRef } from "../OmoGlobalConfigFields";
import * as configApi from "@/lib/api/config";
import {
EMPTY_OMO_GLOBAL_CONFIG,
buildOmoProfilePreview,
} from "../helpers/opencodeFormUtils";
} from "@/types/omo";
interface UseOmoDraftStateParams {
initialOmoSettings: Record<string, unknown> | undefined;
queriedOmoGlobalConfig: OmoGlobalConfig | undefined;
isEditMode: boolean;
appId: string;
category?: string;
@@ -33,33 +22,15 @@ export interface OmoDraftState {
>;
omoOtherFieldsStr: string;
setOmoOtherFieldsStr: React.Dispatch<React.SetStateAction<string>>;
useOmoCommonConfig: boolean;
setUseOmoCommonConfig: React.Dispatch<React.SetStateAction<boolean>>;
isOmoConfigModalOpen: boolean;
setIsOmoConfigModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
isOmoSaving: boolean;
omoGlobalConfigRef: React.RefObject<OmoGlobalConfigFieldsRef | null>;
omoFieldsKey: number;
effectiveOmoGlobalConfig: OmoGlobalConfig;
mergedOmoJsonPreview: string;
handleOmoGlobalConfigSave: () => Promise<void>;
handleOmoEditClick: () => void;
resetOmoDraftState: (useCommonConfig?: boolean) => void;
setOmoGlobalState: React.Dispatch<
React.SetStateAction<OmoGlobalConfig | null>
>;
resetOmoDraftState: () => void;
}
export function useOmoDraftState({
initialOmoSettings,
queriedOmoGlobalConfig,
isEditMode,
appId,
category,
}: UseOmoDraftStateParams): OmoDraftState {
const { t } = useTranslation();
const isSlim = category === "omo-slim";
const commonConfigKey = isSlim ? "omo_slim" : "omo";
const [omoAgents, setOmoAgents] = useState<
Record<string, Record<string, unknown>>
@@ -82,118 +53,25 @@ export function useOmoDraftState({
return otherFields ? JSON.stringify(otherFields, null, 2) : "";
});
const [omoGlobalState, setOmoGlobalState] = useState<OmoGlobalConfig | null>(
null,
);
const [isOmoConfigModalOpen, setIsOmoConfigModalOpen] = useState(false);
const [useOmoCommonConfig, setUseOmoCommonConfig] = useState(() => {
const raw = initialOmoSettings?.useCommonConfig;
return typeof raw === "boolean" ? raw : true;
});
const [isOmoSaving, setIsOmoSaving] = useState(false);
const omoGlobalConfigRef = useRef<OmoGlobalConfigFieldsRef>(null);
const [omoFieldsKey, setOmoFieldsKey] = useState(0);
const effectiveOmoGlobalConfig =
omoGlobalState ?? queriedOmoGlobalConfig ?? EMPTY_OMO_GLOBAL_CONFIG;
const mergedOmoJsonPreview = useMemo(() => {
if (useOmoCommonConfig) {
if (isSlim) {
const merged = mergeOmoSlimConfigPreview(
effectiveOmoGlobalConfig,
omoAgents,
omoOtherFieldsStr,
);
return JSON.stringify(merged, null, 2);
}
const merged = mergeOmoConfigPreview(
effectiveOmoGlobalConfig,
omoAgents,
omoCategories,
omoOtherFieldsStr,
);
return JSON.stringify(merged, null, 2);
} else {
if (isSlim) {
return JSON.stringify(
buildOmoSlimProfilePreview(omoAgents, omoOtherFieldsStr),
null,
2,
);
}
if (isSlim) {
return JSON.stringify(
buildOmoProfilePreview(omoAgents, omoCategories, omoOtherFieldsStr),
buildOmoSlimProfilePreview(omoAgents, omoOtherFieldsStr),
null,
2,
);
}
}, [
useOmoCommonConfig,
effectiveOmoGlobalConfig,
omoAgents,
omoCategories,
omoOtherFieldsStr,
isSlim,
]);
return JSON.stringify(
buildOmoProfilePreview(omoAgents, omoCategories, omoOtherFieldsStr),
null,
2,
);
}, [omoAgents, omoCategories, omoOtherFieldsStr, isSlim]);
// Auto-detect whether common config has content for new OMO/OMO Slim profiles
useEffect(() => {
if (
appId !== "opencode" ||
(category !== "omo" && category !== "omo-slim") ||
isEditMode
)
return;
let active = true;
(async () => {
let next = false;
try {
const raw = await configApi.getCommonConfigSnippet(commonConfigKey);
if (raw) {
const parsed = JSON.parse(raw) as Record<string, unknown>;
next = Object.keys(parsed).some(
(k) => k !== "id" && k !== "updatedAt",
);
}
} catch {}
if (active) setUseOmoCommonConfig(next);
})();
return () => {
active = false;
};
}, [appId, category, isEditMode, commonConfigKey]);
const handleOmoGlobalConfigSave = useCallback(async () => {
if (!omoGlobalConfigRef.current) return;
setIsOmoSaving(true);
try {
const config = omoGlobalConfigRef.current.buildCurrentConfigStrict();
await configApi.setCommonConfigSnippet(
commonConfigKey,
JSON.stringify(config),
);
setIsOmoConfigModalOpen(false);
toast.success(
t("omo.globalConfigSaved", { defaultValue: "Global config saved" }),
);
} catch (err) {
toast.error(String(err));
} finally {
setIsOmoSaving(false);
}
}, [t, commonConfigKey]);
const handleOmoEditClick = useCallback(() => {
setOmoFieldsKey((k) => k + 1);
setIsOmoConfigModalOpen(true);
}, []);
const resetOmoDraftState = useCallback((useCommonConfig = true) => {
const resetOmoDraftState = useCallback(() => {
setOmoAgents({});
setOmoCategories({});
setOmoOtherFieldsStr("");
setUseOmoCommonConfig(useCommonConfig);
}, []);
return {
@@ -203,18 +81,7 @@ export function useOmoDraftState({
setOmoCategories,
omoOtherFieldsStr,
setOmoOtherFieldsStr,
useOmoCommonConfig,
setUseOmoCommonConfig,
isOmoConfigModalOpen,
setIsOmoConfigModalOpen,
isOmoSaving,
omoGlobalConfigRef,
omoFieldsKey,
effectiveOmoGlobalConfig,
mergedOmoJsonPreview,
handleOmoGlobalConfigSave,
handleOmoEditClick,
resetOmoDraftState,
setOmoGlobalState,
};
}
+132 -11
View File
@@ -1,7 +1,7 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { Pencil, RotateCcw, Check, X } from "lucide-react";
import { Pencil, RotateCcw, Check, X, Download, Trash2 } from "lucide-react";
import {
Dialog,
DialogContent,
@@ -67,9 +67,20 @@ export function BackupListSection({
onSettingsChange,
}: BackupListSectionProps) {
const { t } = useTranslation();
const { backups, isLoading, restore, isRestoring, rename, isRenaming } =
useBackupManager();
const {
backups,
isLoading,
create,
isCreating,
restore,
isRestoring,
rename,
isRenaming,
remove,
isDeleting,
} = useBackupManager();
const [confirmFilename, setConfirmFilename] = useState<string | null>(null);
const [deleteFilename, setDeleteFilename] = useState<string | null>(null);
const [editingFilename, setEditingFilename] = useState<string | null>(null);
const [editValue, setEditValue] = useState("");
@@ -110,6 +121,26 @@ export function BackupListSection({
setEditValue("");
};
const handleDelete = async () => {
if (!deleteFilename) return;
try {
await remove(deleteFilename);
setDeleteFilename(null);
toast.success(
t("settings.backupManager.deleteSuccess", {
defaultValue: "Backup deleted",
}),
);
} catch (error) {
const detail =
extractErrorMessage(error) ||
t("settings.backupManager.deleteFailed", {
defaultValue: "Delete failed",
});
toast.error(detail);
}
};
const handleConfirmRename = async () => {
if (!editingFilename || !editValue.trim()) return;
try {
@@ -221,11 +252,45 @@ export function BackupListSection({
{/* Backup list */}
<div>
<h4 className="text-sm font-medium mb-2">
{t("settings.backupManager.title", {
defaultValue: "Database Backups",
})}
</h4>
<div className="flex items-center justify-between mb-2">
<h4 className="text-sm font-medium">
{t("settings.backupManager.title", {
defaultValue: "Database Backups",
})}
</h4>
<Button
variant="outline"
size="sm"
className="h-7 px-2 text-xs"
disabled={isCreating || isRestoring}
onClick={async () => {
try {
await create();
toast.success(
t("settings.backupManager.createSuccess", {
defaultValue: "Backup created successfully",
}),
);
} catch (error) {
const detail =
extractErrorMessage(error) ||
t("settings.backupManager.createFailed", {
defaultValue: "Backup failed",
});
toast.error(detail);
}
}}
>
<Download className="h-3 w-3 mr-1" />
{isCreating
? t("settings.backupManager.creating", {
defaultValue: "Backing up...",
})
: t("settings.backupManager.createBackup", {
defaultValue: "Backup Now",
})}
</Button>
</div>
{isLoading ? (
<div className="text-sm text-muted-foreground py-2">Loading...</div>
@@ -298,18 +363,30 @@ export function BackupListSection({
size="icon"
className="h-7 w-7"
onClick={() => handleStartRename(backup.filename)}
disabled={isRestoring || isRenaming}
disabled={isRestoring || isRenaming || isDeleting}
title={t("settings.backupManager.rename", {
defaultValue: "Rename",
})}
>
<Pencil className="h-3 w-3" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-destructive hover:text-destructive"
onClick={() => setDeleteFilename(backup.filename)}
disabled={isRestoring || isDeleting}
title={t("settings.backupManager.delete", {
defaultValue: "Delete",
})}
>
<Trash2 className="h-3 w-3" />
</Button>
<Button
variant="ghost"
size="sm"
className="h-7 px-2 text-xs"
disabled={isRestoring}
disabled={isRestoring || isDeleting}
onClick={() => setConfirmFilename(backup.filename)}
>
<RotateCcw className="h-3 w-3 mr-1" />
@@ -329,7 +406,7 @@ export function BackupListSection({
)}
</div>
{/* Confirmation Dialog */}
{/* Restore Confirmation Dialog */}
<Dialog
open={!!confirmFilename}
onOpenChange={(open) => !open && setConfirmFilename(null)}
@@ -368,6 +445,50 @@ export function BackupListSection({
</DialogFooter>
</DialogContent>
</Dialog>
{/* Delete Confirmation Dialog */}
<Dialog
open={!!deleteFilename}
onOpenChange={(open) => !open && setDeleteFilename(null)}
>
<DialogContent className="max-w-md" zIndex="alert">
<DialogHeader>
<DialogTitle>
{t("settings.backupManager.deleteConfirmTitle", {
defaultValue: "Confirm Delete",
})}
</DialogTitle>
<DialogDescription>
{t("settings.backupManager.deleteConfirmMessage", {
defaultValue:
"This backup will be permanently deleted. This action cannot be undone.",
})}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="outline"
onClick={() => setDeleteFilename(null)}
disabled={isDeleting}
>
{t("common.cancel", { defaultValue: "Cancel" })}
</Button>
<Button
variant="destructive"
onClick={handleDelete}
disabled={isDeleting}
>
{isDeleting
? t("settings.backupManager.deleting", {
defaultValue: "Deleting...",
})
: t("settings.backupManager.delete", {
defaultValue: "Delete",
})}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}
+20 -7
View File
@@ -2,6 +2,7 @@ import { useTranslation } from "react-i18next";
import type { SettingsFormState } from "@/hooks/useSettings";
import { AppWindow, MonitorUp, Power, EyeOff } from "lucide-react";
import { ToggleRow } from "@/components/ui/toggle-row";
import { AnimatePresence, motion } from "framer-motion";
interface WindowSettingsProps {
settings: SettingsFormState;
@@ -27,13 +28,25 @@ export function WindowSettings({ settings, onChange }: WindowSettingsProps) {
onCheckedChange={(value) => onChange({ launchOnStartup: value })}
/>
<ToggleRow
icon={<EyeOff className="h-4 w-4 text-green-500" />}
title={t("settings.silentStartup")}
description={t("settings.silentStartupDescription")}
checked={!!settings.silentStartup}
onCheckedChange={(value) => onChange({ silentStartup: value })}
/>
<AnimatePresence initial={false}>
{settings.launchOnStartup && (
<motion.div
key="silent-startup"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
transition={{ duration: 0.3 }}
>
<ToggleRow
icon={<EyeOff className="h-4 w-4 text-green-500" />}
title={t("settings.silentStartup")}
description={t("settings.silentStartupDescription")}
checked={!!settings.silentStartup}
onCheckedChange={(value) => onChange({ silentStartup: value })}
/>
</motion.div>
)}
</AnimatePresence>
<ToggleRow
icon={<MonitorUp className="h-4 w-4 text-purple-500" />}
+58 -3
View File
@@ -476,6 +476,61 @@ export const providerPresets: ProviderPreset[] = [
icon: "aicodemirror",
iconColor: "#000000",
},
{
name: "AICoding",
websiteUrl: "https://www.aicoding.sh",
apiKeyUrl: "https://www.aicoding.sh/i/CCSWITCH",
settingsConfig: {
env: {
ANTHROPIC_BASE_URL: "https://api.aicoding.sh",
ANTHROPIC_AUTH_TOKEN: "",
},
},
endpointCandidates: ["https://api.aicoding.sh"],
category: "third_party",
isPartner: true, // 合作伙伴
partnerPromotionKey: "aicoding", // 促销信息 i18n key
icon: "aicoding",
iconColor: "#000000",
},
{
name: "CrazyRouter",
websiteUrl: "https://www.crazyrouter.com",
apiKeyUrl: "https://www.crazyrouter.com/register?aff=OZcm&ref=cc-switch",
settingsConfig: {
env: {
ANTHROPIC_BASE_URL: "https://crazyrouter.com",
ANTHROPIC_AUTH_TOKEN: "",
},
},
endpointCandidates: ["https://crazyrouter.com"],
category: "third_party",
isPartner: true, // 合作伙伴
partnerPromotionKey: "crazyrouter", // 促销信息 i18n key
icon: "crazyrouter",
iconColor: "#000000",
},
{
name: "SSSAiCode",
websiteUrl: "https://www.sssaicode.com",
apiKeyUrl: "https://www.sssaicode.com/register?ref=DCP0SM",
settingsConfig: {
env: {
ANTHROPIC_BASE_URL: "https://node-hk.sssaicode.com/api",
ANTHROPIC_AUTH_TOKEN: "",
},
},
endpointCandidates: [
"https://node-hk.sssaicode.com/api",
"https://claude2.sssaicode.com/api",
"https://anti.sssaicode.com/api",
],
category: "third_party",
isPartner: true, // 合作伙伴
partnerPromotionKey: "sssaicode", // 促销信息 i18n key
icon: "sssaicode",
iconColor: "#000000",
},
{
name: "OpenRouter",
websiteUrl: "https://openrouter.ai",
@@ -484,10 +539,10 @@ export const providerPresets: ProviderPreset[] = [
env: {
ANTHROPIC_BASE_URL: "https://openrouter.ai/api",
ANTHROPIC_AUTH_TOKEN: "",
ANTHROPIC_MODEL: "anthropic/claude-sonnet-4.5",
ANTHROPIC_MODEL: "anthropic/claude-sonnet-4.6",
ANTHROPIC_DEFAULT_HAIKU_MODEL: "anthropic/claude-haiku-4.5",
ANTHROPIC_DEFAULT_SONNET_MODEL: "anthropic/claude-sonnet-4.5",
ANTHROPIC_DEFAULT_OPUS_MODEL: "anthropic/claude-opus-4.5",
ANTHROPIC_DEFAULT_SONNET_MODEL: "anthropic/claude-sonnet-4.6",
ANTHROPIC_DEFAULT_OPUS_MODEL: "anthropic/claude-opus-4.6",
},
},
category: "aggregator",
+53
View File
@@ -227,6 +227,59 @@ requires_openai_auth = true`,
icon: "aicodemirror",
iconColor: "#000000",
},
{
name: "AICoding",
websiteUrl: "https://www.aicoding.sh",
apiKeyUrl: "https://www.aicoding.sh/i/CCSWITCH",
auth: generateThirdPartyAuth(""),
config: generateThirdPartyConfig(
"aicoding",
"https://api.aicoding.sh",
"gpt-5.3-codex",
),
endpointCandidates: ["https://api.aicoding.sh"],
isPartner: true,
partnerPromotionKey: "aicoding",
icon: "aicoding",
iconColor: "#000000",
},
{
name: "CrazyRouter",
websiteUrl: "https://www.crazyrouter.com",
apiKeyUrl: "https://www.crazyrouter.com/register?aff=OZcm&ref=cc-switch",
auth: generateThirdPartyAuth(""),
config: generateThirdPartyConfig(
"crazyrouter",
"https://crazyrouter.com/v1",
"gpt-5.3-codex",
),
endpointCandidates: ["https://crazyrouter.com/v1"],
isPartner: true,
partnerPromotionKey: "crazyrouter",
icon: "crazyrouter",
iconColor: "#000000",
},
{
name: "SSSAiCode",
websiteUrl: "https://www.sssaicode.com",
apiKeyUrl: "https://www.sssaicode.com/register?ref=DCP0SM",
auth: generateThirdPartyAuth(""),
config: generateThirdPartyConfig(
"sssaicode",
"https://node-hk.sssaicode.com/api",
"gpt-5.3-codex",
),
endpointCandidates: [
"https://node-hk.sssaicode.com/api/v1",
"https://claude2.sssaicode.com/api/v1",
"https://anti.sssaicode.com/api/v1",
],
category: "third_party",
isPartner: true, // 合作伙伴
partnerPromotionKey: "sssaicode", // 促销信息 i18n key
icon: "sssaicode",
iconColor: "#000000",
},
{
name: "OpenRouter",
websiteUrl: "https://openrouter.ai",
+64
View File
@@ -139,6 +139,70 @@ export const geminiProviderPresets: GeminiProviderPreset[] = [
icon: "aicodemirror",
iconColor: "#000000",
},
{
name: "AICoding",
websiteUrl: "https://www.aicoding.sh",
apiKeyUrl: "https://www.aicoding.sh/i/CCSWITCH",
settingsConfig: {
env: {
GOOGLE_GEMINI_BASE_URL: "https://api.aicoding.sh",
GEMINI_MODEL: "gemini-3-pro",
},
},
baseURL: "https://api.aicoding.sh",
model: "gemini-3-pro",
description: "AICoding",
category: "third_party",
isPartner: true,
partnerPromotionKey: "aicoding",
endpointCandidates: ["https://api.aicoding.sh"],
icon: "aicoding",
iconColor: "#000000",
},
{
name: "CrazyRouter",
websiteUrl: "https://www.crazyrouter.com",
apiKeyUrl: "https://www.crazyrouter.com/register?aff=OZcm&ref=cc-switch",
settingsConfig: {
env: {
GOOGLE_GEMINI_BASE_URL: "https://crazyrouter.com",
GEMINI_MODEL: "gemini-3-pro",
},
},
baseURL: "https://crazyrouter.com",
model: "gemini-3-pro",
description: "CrazyRouter",
category: "third_party",
isPartner: true,
partnerPromotionKey: "crazyrouter",
endpointCandidates: ["https://crazyrouter.com"],
icon: "crazyrouter",
iconColor: "#000000",
},
{
name: "SSSAiCode",
websiteUrl: "https://www.sssaicode.com",
apiKeyUrl: "https://www.sssaicode.com/register?ref=DCP0SM",
settingsConfig: {
env: {
GOOGLE_GEMINI_BASE_URL: "https://node-hk.sssaicode.com/api",
GEMINI_MODEL: "gemini-3-pro",
},
},
baseURL: "https://node-hk.sssaicode.com/api",
model: "gemini-3-pro",
description: "SSSAiCode",
category: "third_party",
isPartner: true,
partnerPromotionKey: "sssaicode",
endpointCandidates: [
"https://node-hk.sssaicode.com/api",
"https://claude2.sssaicode.com/api",
"https://anti.sssaicode.com/api",
],
icon: "sssaicode",
iconColor: "#000000",
},
{
name: "OpenRouter",
websiteUrl: "https://openrouter.ai",
+170 -37
View File
@@ -120,8 +120,6 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
],
},
category: "cn_official",
isPartner: true,
partnerPromotionKey: "zhipu",
icon: "zhipu",
iconColor: "#0F62FE",
templateValues: {
@@ -160,8 +158,6 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
],
},
category: "cn_official",
isPartner: true,
partnerPromotionKey: "zhipu",
icon: "zhipu",
iconColor: "#0F62FE",
templateValues: {
@@ -562,8 +558,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
api: "anthropic-messages",
models: [
{
id: "claude-sonnet-4-5-20250929",
name: "Claude Sonnet 4.5",
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6",
contextWindow: 200000,
cost: { input: 3, output: 15 },
},
@@ -587,11 +583,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
},
suggestedDefaults: {
model: {
primary: "aihubmix/claude-sonnet-4-5-20250929",
primary: "aihubmix/claude-sonnet-4-6",
fallbacks: ["aihubmix/claude-opus-4-6"],
},
modelCatalog: {
"aihubmix/claude-sonnet-4-5-20250929": { alias: "Sonnet" },
"aihubmix/claude-sonnet-4-6": { alias: "Sonnet" },
"aihubmix/claude-opus-4-6": { alias: "Opus" },
},
},
@@ -606,8 +602,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
api: "anthropic-messages",
models: [
{
id: "claude-sonnet-4-5-20250929",
name: "Claude Sonnet 4.5",
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6",
contextWindow: 200000,
cost: { input: 3, output: 15 },
},
@@ -631,11 +627,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
},
suggestedDefaults: {
model: {
primary: "dmxapi/claude-sonnet-4-5-20250929",
primary: "dmxapi/claude-sonnet-4-6",
fallbacks: ["dmxapi/claude-opus-4-6"],
},
modelCatalog: {
"dmxapi/claude-sonnet-4-5-20250929": { alias: "Sonnet" },
"dmxapi/claude-sonnet-4-6": { alias: "Sonnet" },
"dmxapi/claude-opus-4-6": { alias: "Opus" },
},
},
@@ -650,8 +646,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
api: "openai-completions",
models: [
{
id: "anthropic/claude-sonnet-4.5",
name: "Claude Sonnet 4.5",
id: "anthropic/claude-sonnet-4.6",
name: "Claude Sonnet 4.6",
contextWindow: 200000,
cost: { input: 3, output: 15 },
},
@@ -675,11 +671,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
},
suggestedDefaults: {
model: {
primary: "openrouter/anthropic/claude-sonnet-4.5",
primary: "openrouter/anthropic/claude-sonnet-4.6",
fallbacks: ["openrouter/anthropic/claude-opus-4.6"],
},
modelCatalog: {
"openrouter/anthropic/claude-sonnet-4.5": { alias: "Sonnet" },
"openrouter/anthropic/claude-sonnet-4.6": { alias: "Sonnet" },
"openrouter/anthropic/claude-opus-4.6": { alias: "Opus" },
},
},
@@ -834,8 +830,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
api: "anthropic-messages",
models: [
{
id: "claude-sonnet-4-5-20250929",
name: "Claude Sonnet 4.5",
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6",
contextWindow: 200000,
cost: { input: 3, output: 15 },
},
@@ -860,11 +856,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
},
suggestedDefaults: {
model: {
primary: "packycode/claude-sonnet-4-5-20250929",
primary: "packycode/claude-sonnet-4-6",
fallbacks: ["packycode/claude-opus-4-6"],
},
modelCatalog: {
"packycode/claude-sonnet-4-5-20250929": { alias: "Sonnet" },
"packycode/claude-sonnet-4-6": { alias: "Sonnet" },
"packycode/claude-opus-4-6": { alias: "Opus" },
},
},
@@ -879,8 +875,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
api: "anthropic-messages",
models: [
{
id: "claude-sonnet-4-5-20250929",
name: "Claude Sonnet 4.5",
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6",
contextWindow: 200000,
cost: { input: 3, output: 15 },
},
@@ -906,11 +902,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
},
suggestedDefaults: {
model: {
primary: "cubence/claude-sonnet-4-5-20250929",
primary: "cubence/claude-sonnet-4-6",
fallbacks: ["cubence/claude-opus-4-6"],
},
modelCatalog: {
"cubence/claude-sonnet-4-5-20250929": { alias: "Sonnet" },
"cubence/claude-sonnet-4-6": { alias: "Sonnet" },
"cubence/claude-opus-4-6": { alias: "Opus" },
},
},
@@ -925,8 +921,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
api: "anthropic-messages",
models: [
{
id: "claude-sonnet-4-5-20250929",
name: "Claude Sonnet 4.5",
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6",
contextWindow: 200000,
cost: { input: 3, output: 15 },
},
@@ -952,11 +948,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
},
suggestedDefaults: {
model: {
primary: "aigocode/claude-sonnet-4-5-20250929",
primary: "aigocode/claude-sonnet-4-6",
fallbacks: ["aigocode/claude-opus-4-6"],
},
modelCatalog: {
"aigocode/claude-sonnet-4-5-20250929": { alias: "Sonnet" },
"aigocode/claude-sonnet-4-6": { alias: "Sonnet" },
"aigocode/claude-opus-4-6": { alias: "Opus" },
},
},
@@ -971,8 +967,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
api: "anthropic-messages",
models: [
{
id: "claude-sonnet-4-5-20250929",
name: "Claude Sonnet 4.5",
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6",
contextWindow: 200000,
cost: { input: 3, output: 15 },
},
@@ -998,11 +994,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
},
suggestedDefaults: {
model: {
primary: "rightcode/claude-sonnet-4-5-20250929",
primary: "rightcode/claude-sonnet-4-6",
fallbacks: ["rightcode/claude-opus-4-6"],
},
modelCatalog: {
"rightcode/claude-sonnet-4-5-20250929": { alias: "Sonnet" },
"rightcode/claude-sonnet-4-6": { alias: "Sonnet" },
"rightcode/claude-opus-4-6": { alias: "Opus" },
},
},
@@ -1017,8 +1013,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
api: "anthropic-messages",
models: [
{
id: "claude-sonnet-4-5-20250929",
name: "Claude Sonnet 4.5",
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6",
contextWindow: 200000,
cost: { input: 3, output: 15 },
},
@@ -1044,16 +1040,153 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
},
suggestedDefaults: {
model: {
primary: "aicodemirror/claude-sonnet-4-5-20250929",
primary: "aicodemirror/claude-sonnet-4-6",
fallbacks: ["aicodemirror/claude-opus-4-6"],
},
modelCatalog: {
"aicodemirror/claude-sonnet-4-5-20250929": { alias: "Sonnet" },
"aicodemirror/claude-sonnet-4-6": { alias: "Sonnet" },
"aicodemirror/claude-opus-4-6": { alias: "Opus" },
},
},
},
{
name: "AICoding",
websiteUrl: "https://www.aicoding.sh",
apiKeyUrl: "https://www.aicoding.sh/i/CCSWITCH",
settingsConfig: {
baseUrl: "https://api.aicoding.sh",
apiKey: "",
api: "anthropic-messages",
models: [
{
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6",
contextWindow: 200000,
cost: { input: 3, output: 15 },
},
{
id: "claude-opus-4-6",
name: "Claude Opus 4.6",
contextWindow: 200000,
cost: { input: 5, output: 25 },
},
],
},
category: "third_party",
isPartner: true,
partnerPromotionKey: "aicoding",
icon: "aicoding",
iconColor: "#000000",
templateValues: {
apiKey: {
label: "API Key",
placeholder: "",
editorValue: "",
},
},
suggestedDefaults: {
model: {
primary: "aicoding/claude-sonnet-4-6",
fallbacks: ["aicoding/claude-opus-4-6"],
},
modelCatalog: {
"aicoding/claude-sonnet-4-6": { alias: "Sonnet" },
"aicoding/claude-opus-4-6": { alias: "Opus" },
},
},
},
{
name: "CrazyRouter",
websiteUrl: "https://www.crazyrouter.com",
apiKeyUrl: "https://www.crazyrouter.com/register?aff=OZcm&ref=cc-switch",
settingsConfig: {
baseUrl: "https://crazyrouter.com/v1",
apiKey: "",
api: "anthropic-messages",
models: [
{
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6",
contextWindow: 200000,
cost: { input: 3, output: 15 },
},
{
id: "claude-opus-4-6",
name: "Claude Opus 4.6",
contextWindow: 200000,
cost: { input: 5, output: 25 },
},
],
},
category: "third_party",
isPartner: true,
partnerPromotionKey: "crazyrouter",
icon: "crazyrouter",
iconColor: "#000000",
templateValues: {
apiKey: {
label: "API Key",
placeholder: "",
editorValue: "",
},
},
suggestedDefaults: {
model: {
primary: "crazyrouter/claude-sonnet-4-6",
fallbacks: ["crazyrouter/claude-opus-4-6"],
},
modelCatalog: {
"crazyrouter/claude-sonnet-4-6": { alias: "Sonnet" },
"crazyrouter/claude-opus-4-6": { alias: "Opus" },
},
},
},
{
name: "SSSAiCode",
websiteUrl: "https://www.sssaicode.com",
apiKeyUrl: "https://www.sssaicode.com/register?ref=DCP0SM",
settingsConfig: {
baseUrl: "https://node-hk.sssaicode.com/api",
apiKey: "",
api: "anthropic-messages",
models: [
{
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6",
contextWindow: 200000,
cost: { input: 3, output: 15 },
},
{
id: "claude-opus-4-6",
name: "Claude Opus 4.6",
contextWindow: 200000,
cost: { input: 5, output: 25 },
},
],
},
category: "third_party",
isPartner: true,
partnerPromotionKey: "sssaicode",
icon: "sssaicode",
iconColor: "#000000",
templateValues: {
apiKey: {
label: "API Key",
placeholder: "",
editorValue: "",
},
},
suggestedDefaults: {
model: {
primary: "sssaicode/claude-sonnet-4-6",
fallbacks: ["sssaicode/claude-opus-4-6"],
},
modelCatalog: {
"sssaicode/claude-sonnet-4-6": { alias: "Sonnet" },
"sssaicode/claude-opus-4-6": { alias: "Opus" },
},
},
},
// ========== Cloud Providers ==========
{
name: "AWS Bedrock",
+94 -8
View File
@@ -890,7 +890,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
apiKey: "",
},
models: {
"claude-sonnet-4-5-20250929": { name: "Claude Sonnet 4.5" },
"claude-sonnet-4-6": { name: "Claude Sonnet 4.6" },
"claude-opus-4-6": { name: "Claude Opus 4.6" },
},
},
@@ -917,7 +917,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
apiKey: "",
},
models: {
"claude-sonnet-4-5-20250929": { name: "Claude Sonnet 4.5" },
"claude-sonnet-4-6": { name: "Claude Sonnet 4.6" },
"claude-opus-4-6": { name: "Claude Opus 4.6" },
},
},
@@ -944,7 +944,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
apiKey: "",
},
models: {
"anthropic/claude-sonnet-4.5": { name: "Claude Sonnet 4.5" },
"anthropic/claude-sonnet-4.6": { name: "Claude Sonnet 4.6" },
"anthropic/claude-opus-4.6": { name: "Claude Opus 4.6" },
},
},
@@ -998,7 +998,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
apiKey: "",
},
models: {
"claude-sonnet-4-5-20250929": { name: "Claude Sonnet 4.5" },
"claude-sonnet-4-6": { name: "Claude Sonnet 4.6" },
"claude-opus-4-6": { name: "Claude Opus 4.6" },
},
},
@@ -1026,7 +1026,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
apiKey: "",
},
models: {
"claude-sonnet-4-5-20250929": { name: "Claude Sonnet 4.5" },
"claude-sonnet-4-6": { name: "Claude Sonnet 4.6" },
"claude-opus-4-6": { name: "Claude Opus 4.6" },
},
},
@@ -1055,7 +1055,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
apiKey: "",
},
models: {
"claude-sonnet-4-5-20250929": { name: "Claude Sonnet 4.5" },
"claude-sonnet-4-6": { name: "Claude Sonnet 4.6" },
"claude-opus-4-6": { name: "Claude Opus 4.6" },
},
},
@@ -1116,7 +1116,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
apiKey: "",
},
models: {
"claude-sonnet-4.5": { name: "Claude Sonnet 4.5" },
"claude-sonnet-4.6": { name: "Claude Sonnet 4.6" },
"claude-opus-4.6": { name: "Claude Opus 4.6" },
},
},
@@ -1133,7 +1133,93 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
},
},
},
{
name: "AICoding",
websiteUrl: "https://www.aicoding.sh",
apiKeyUrl: "https://www.aicoding.sh/i/CCSWITCH",
settingsConfig: {
npm: "@ai-sdk/anthropic",
name: "AICoding",
options: {
baseURL: "https://api.aicoding.sh",
apiKey: "",
},
models: {
"claude-sonnet-4-6": { name: "Claude Sonnet 4.6" },
"claude-opus-4-6": { name: "Claude Opus 4.6" },
},
},
category: "third_party",
isPartner: true,
partnerPromotionKey: "aicoding",
icon: "aicoding",
iconColor: "#000000",
templateValues: {
apiKey: {
label: "API Key",
placeholder: "",
editorValue: "",
},
},
},
{
name: "CrazyRouter",
websiteUrl: "https://www.crazyrouter.com",
apiKeyUrl: "https://www.crazyrouter.com/register?aff=OZcm&ref=cc-switch",
settingsConfig: {
npm: "@ai-sdk/anthropic",
name: "CrazyRouter",
options: {
baseURL: "https://crazyrouter.com",
apiKey: "",
},
models: {
"claude-sonnet-4-6": { name: "Claude Sonnet 4.6" },
"claude-opus-4-6": { name: "Claude Opus 4.6" },
},
},
category: "third_party",
isPartner: true,
partnerPromotionKey: "crazyrouter",
icon: "crazyrouter",
iconColor: "#000000",
templateValues: {
apiKey: {
label: "API Key",
placeholder: "",
editorValue: "",
},
},
},
{
name: "SSSAiCode",
websiteUrl: "https://www.sssaicode.com",
apiKeyUrl: "https://www.sssaicode.com/register?ref=DCP0SM",
settingsConfig: {
npm: "@ai-sdk/anthropic",
name: "SSSAiCode",
options: {
baseURL: "https://node-hk.sssaicode.com/api",
apiKey: "",
},
models: {
"claude-sonnet-4-6": { name: "Claude Sonnet 4.6" },
"claude-opus-4-6": { name: "Claude Opus 4.6" },
},
},
category: "third_party",
isPartner: true,
partnerPromotionKey: "sssaicode",
icon: "sssaicode",
iconColor: "#000000",
templateValues: {
apiKey: {
label: "API Key",
placeholder: "",
editorValue: "",
},
},
},
{
name: "AWS Bedrock",
websiteUrl: "https://aws.amazon.com/bedrock/",
+14
View File
@@ -13,6 +13,11 @@ export function useBackupManager() {
queryFn: () => backupsApi.listDbBackups(),
});
const createMutation = useMutation({
mutationFn: () => backupsApi.createDbBackup(),
onSuccess: () => refetch(),
});
const restoreMutation = useMutation({
mutationFn: (filename: string) => backupsApi.restoreDbBackup(filename),
onSuccess: async () => {
@@ -34,12 +39,21 @@ export function useBackupManager() {
onSuccess: () => refetch(),
});
const deleteMutation = useMutation({
mutationFn: (filename: string) => backupsApi.deleteDbBackup(filename),
onSuccess: () => refetch(),
});
return {
backups,
isLoading,
create: createMutation.mutateAsync,
isCreating: createMutation.isPending,
restore: restoreMutation.mutateAsync,
isRestoring: restoreMutation.isPending,
rename: renameMutation.mutateAsync,
isRenaming: renameMutation.isPending,
remove: deleteMutation.mutateAsync,
isDeleting: deleteMutation.isPending,
};
}
+50 -38
View File
@@ -52,6 +52,7 @@
"claudeConfig": {
"configLabel": "Claude Code settings.json (JSON) *",
"fullSettingsHint": "Full Claude Code settings.json content",
"fragmentSettingsHint": "Config snippet for this provider; will be written to settings.json when activated",
"hideAttribution": "Hide AI Attribution",
"alwaysThinking": "Extended Thinking",
"enableTeammates": "Teammates Mode"
@@ -308,7 +309,17 @@
"rename": "Rename",
"renameSuccess": "Backup renamed",
"renameFailed": "Rename failed",
"namePlaceholder": "Enter new name"
"namePlaceholder": "Enter new name",
"createBackup": "Backup Now",
"creating": "Backing up...",
"createSuccess": "Backup created successfully",
"createFailed": "Backup failed",
"delete": "Delete",
"deleting": "Deleting...",
"deleteSuccess": "Backup deleted",
"deleteFailed": "Delete failed",
"deleteConfirmTitle": "Confirm Delete",
"deleteConfirmMessage": "This backup will be permanently deleted. This action cannot be undone."
},
"webdavSync": {
"title": "WebDAV Cloud Sync",
@@ -626,7 +637,10 @@
"cubence": "Cubence is an official partner of CC Switch. Register using this link and enter \"CCSWITCH\" promo code during recharge to get 10% off every top-up",
"aigocode": "AIGoCode is an official partner of CC Switch. Register using this link and get 10% bonus credit on your first top-up!",
"rightcode": "RightCode is an official partner of CC Switch. Register using this link and get 5% bonus credit on every top-up!",
"aicodemirror": "AICodeMirror is an official partner of CC Switch. Register using this link to get 20% off!"
"aicodemirror": "AICodeMirror is an official partner of CC Switch. Register using this link to get 20% off!",
"aicoding": "AI Coding offers an exclusive discount for CC Switch users — 10% off your first top-up!",
"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!"
},
"parameterConfig": "Parameter Config - {{name}} *",
"mainModel": "Main Model (optional)",
@@ -778,38 +792,6 @@
"modelOptionKeyPlaceholder": "provider",
"modelOptionValuePlaceholder": "{\"order\": [\"baseten\"]}"
},
"openclaw": {
"providerKey": "Provider Key",
"providerKeyPlaceholder": "my-provider",
"providerKeyHint": "Unique identifier in config file. Cannot be changed after creation. Use lowercase letters, numbers, and hyphens only.",
"providerKeyRequired": "Provider key is required",
"providerKeyDuplicate": "This key is already in use",
"providerKeyInvalid": "Invalid format. Use lowercase letters, numbers, and hyphens only.",
"apiProtocol": "API Protocol",
"selectProtocol": "Select API Protocol",
"apiProtocolHint": "Select the protocol type compatible with the provider's API. Most providers use OpenAI Completions format.",
"baseUrl": "API Endpoint",
"baseUrlHint": "The provider's API endpoint address.",
"models": "Models",
"addModel": "Add Model",
"noModels": "No models configured. Click Add Model to configure available models.",
"modelId": "Model ID",
"modelIdPlaceholder": "claude-3-sonnet",
"modelName": "Display Name",
"modelNamePlaceholder": "Claude 3 Sonnet",
"contextWindow": "Context Window",
"maxTokens": "Max Output Tokens",
"reasoning": "Reasoning Mode",
"reasoningOn": "Enabled",
"reasoningOff": "Disabled",
"inputCost": "Input Cost ($/M tokens)",
"outputCost": "Output Cost ($/M tokens)",
"advancedOptions": "Advanced Options",
"cacheReadCost": "Cache Read Cost ($/M tokens)",
"cacheWriteCost": "Cache Write Cost ($/M tokens)",
"cacheCostHint": "Cache costs are used to calculate Prompt Caching costs. Leave empty if not using caching.",
"modelsHint": "Configure the models supported by this provider. Model ID is used for API calls, Display Name for the interface."
},
"providerPreset": {
"label": "Provider Preset",
"custom": "Custom Configuration",
@@ -1221,6 +1203,36 @@
}
},
"openclaw": {
"providerKey": "Provider Key",
"providerKeyPlaceholder": "my-provider",
"providerKeyHint": "Unique identifier in config file. Cannot be changed after creation. Use lowercase letters, numbers, and hyphens only.",
"providerKeyRequired": "Provider key is required",
"providerKeyDuplicate": "This key is already in use",
"providerKeyInvalid": "Invalid format. Use lowercase letters, numbers, and hyphens only.",
"apiProtocol": "API Protocol",
"selectProtocol": "Select API Protocol",
"apiProtocolHint": "Select the protocol type compatible with the provider's API. Most providers use OpenAI Completions format.",
"baseUrl": "API Endpoint",
"baseUrlHint": "The provider's API endpoint address.",
"models": "Models",
"addModel": "Add Model",
"noModels": "No models configured. Click Add Model to configure available models.",
"modelId": "Model ID",
"modelIdPlaceholder": "claude-3-sonnet",
"modelName": "Display Name",
"modelNamePlaceholder": "Claude 3 Sonnet",
"contextWindow": "Context Window",
"maxTokens": "Max Output Tokens",
"reasoning": "Reasoning Mode",
"reasoningOn": "Enabled",
"reasoningOff": "Disabled",
"inputCost": "Input Cost ($/M tokens)",
"outputCost": "Output Cost ($/M tokens)",
"advancedOptions": "Advanced Options",
"cacheReadCost": "Cache Read Cost ($/M tokens)",
"cacheWriteCost": "Cache Write Cost ($/M tokens)",
"cacheCostHint": "Cache costs are used to calculate Prompt Caching costs. Leave empty if not using caching.",
"modelsHint": "Configure the models supported by this provider. Model ID is used for API calls, Display Name for the interface.",
"env": {
"title": "Environment Variables",
"description": "Manage environment variables in openclaw.json (API keys, custom variables, etc.)",
@@ -1807,10 +1819,6 @@
"enabled": "Enabled",
"disabled": "OMO disabled",
"disableFailed": "Failed to disable OMO: {{error}}",
"writeCommonConfig": "Write to common config",
"editCommonConfig": "Edit common config",
"editCommonConfigTitle": "Common Config",
"commonConfigHint": "OMO common config will be merged into all OMO configs that enable it",
"selectPlaceholder": "Select...",
"clear": "Clear",
"clearWrapped": "(Clear)",
@@ -1840,6 +1848,10 @@
"customCategories": "Custom Categories",
"modelConfiguration": "Model Configuration",
"fillRecommended": "Fill Recommended",
"fillRecommendedSuccess": "Filled {{count}} recommended models",
"fillRecommendedPartial": "Filled {{filled}} recommended models, {{unmatched}} unmatched",
"fillRecommendedAllSet": "All slots already have models configured",
"fillRecommendedNoMatch": "Recommended models not found in configured providers",
"configSummary": "{{agents}} agents, {{categories}} categories configured · Click ⚙ for advanced params",
"enabledModelsCount": "{{count}} configured models available",
"source": "from:",
+71 -40
View File
@@ -52,6 +52,7 @@
"claudeConfig": {
"configLabel": "Claude Code settings.json (JSON) *",
"fullSettingsHint": "Claude Code の settings.json 全文",
"fragmentSettingsHint": "このプロバイダーの設定スニペット。有効化時に settings.json に書き込まれます",
"hideAttribution": "AI署名を非表示",
"alwaysThinking": "拡張思考",
"enableTeammates": "Teammates モード"
@@ -308,7 +309,17 @@
"rename": "名前変更",
"renameSuccess": "バックアップの名前を変更しました",
"renameFailed": "名前変更に失敗しました",
"namePlaceholder": "新しい名前を入力"
"namePlaceholder": "新しい名前を入力",
"createBackup": "今すぐバックアップ",
"creating": "バックアップ中...",
"createSuccess": "バックアップが作成されました",
"createFailed": "バックアップに失敗しました",
"delete": "削除",
"deleting": "削除中...",
"deleteSuccess": "バックアップを削除しました",
"deleteFailed": "削除に失敗しました",
"deleteConfirmTitle": "バックアップの削除を確認",
"deleteConfirmMessage": "このバックアップは完全に削除されます。この操作は取り消せません。"
},
"webdavSync": {
"title": "WebDAV クラウド同期",
@@ -626,7 +637,10 @@
"cubence": "Cubence は CC Switch の公式パートナーです。登録後チャージ時に \"CCSWITCH\" を入力すると、毎回 10% オフ",
"aigocode": "AIGoCode は CC Switch の公式パートナーです。このリンクから登録すると、初回チャージ時に 10% のボーナスクレジットがもらえます!",
"rightcode": "RightCode は CC Switch の公式パートナーです。このリンクから登録すると、毎回のチャージに 5% のボーナスクレジットがもらえます!",
"aicodemirror": "AICodeMirror は CC Switch の公式パートナーです。このリンクから登録すると20%オフ!"
"aicodemirror": "AICodeMirror は CC Switch の公式パートナーです。このリンクから登録すると20%オフ!",
"aicoding": "AI Coding は CC Switch ユーザー向けに特別割引を提供しています。初回チャージが 10% オフ!",
"crazyrouter": "CrazyRouter は CC Switch ユーザー向けに特別ボーナスを提供しています。初回チャージで 30% の追加クレジットがもらえます!",
"sssaicode": "SSAI Code は CC Switch ユーザー向けに特別ボーナスを提供しています。チャージごとに $10 の追加クレジットがもらえます!"
},
"parameterConfig": "パラメーター設定 - {{name}} *",
"mainModel": "メインモデル(任意)",
@@ -778,38 +792,6 @@
"modelOptionKeyPlaceholder": "provider",
"modelOptionValuePlaceholder": "{\"order\": [\"baseten\"]}"
},
"openclaw": {
"providerKey": "プロバイダーキー",
"providerKeyPlaceholder": "my-provider",
"providerKeyHint": "設定ファイル内のユニーク識別子。作成後は変更できません。小文字、数字、ハイフンのみ使用可能。",
"providerKeyRequired": "プロバイダーキーを入力してください",
"providerKeyDuplicate": "このキーは既に使用されています",
"providerKeyInvalid": "無効な形式です。小文字、数字、ハイフンのみ使用可能。",
"apiProtocol": "API プロトコル",
"selectProtocol": "API プロトコルを選択",
"apiProtocolHint": "プロバイダーの API と互換性のあるプロトコルタイプを選択してください。ほとんどのプロバイダーは OpenAI Completions 形式を使用します。",
"baseUrl": "API エンドポイント",
"baseUrlHint": "プロバイダーの API エンドポイントアドレス。",
"models": "モデル一覧",
"addModel": "モデルを追加",
"noModels": "モデルが設定されていません。「モデルを追加」をクリックして利用可能なモデルを設定してください。",
"modelId": "モデル ID",
"modelIdPlaceholder": "claude-3-sonnet",
"modelName": "表示名",
"modelNamePlaceholder": "Claude 3 Sonnet",
"contextWindow": "コンテキストウィンドウ",
"maxTokens": "最大出力トークン数",
"reasoning": "推論モード",
"reasoningOn": "有効",
"reasoningOff": "無効",
"inputCost": "入力コスト ($/M トークン)",
"outputCost": "出力コスト ($/M トークン)",
"advancedOptions": "詳細オプション",
"cacheReadCost": "キャッシュ読取コスト ($/M トークン)",
"cacheWriteCost": "キャッシュ書込コスト ($/M トークン)",
"cacheCostHint": "キャッシュコストは Prompt Caching のコスト計算に使用されます。キャッシュを使用しない場合は空欄のままにしてください。",
"modelsHint": "このプロバイダーがサポートするモデルを設定します。モデル ID は API 呼び出しに、表示名はインターフェースに使用されます。"
},
"providerPreset": {
"label": "プロバイダータイプ",
"custom": "カスタム設定",
@@ -1221,6 +1203,36 @@
}
},
"openclaw": {
"providerKey": "プロバイダーキー",
"providerKeyPlaceholder": "my-provider",
"providerKeyHint": "設定ファイル内のユニーク識別子。作成後は変更できません。小文字、数字、ハイフンのみ使用可能。",
"providerKeyRequired": "プロバイダーキーを入力してください",
"providerKeyDuplicate": "このキーは既に使用されています",
"providerKeyInvalid": "無効な形式です。小文字、数字、ハイフンのみ使用可能。",
"apiProtocol": "API プロトコル",
"selectProtocol": "API プロトコルを選択",
"apiProtocolHint": "プロバイダーの API と互換性のあるプロトコルタイプを選択してください。ほとんどのプロバイダーは OpenAI Completions 形式を使用します。",
"baseUrl": "API エンドポイント",
"baseUrlHint": "プロバイダーの API エンドポイントアドレス。",
"models": "モデル一覧",
"addModel": "モデルを追加",
"noModels": "モデルが設定されていません。「モデルを追加」をクリックして利用可能なモデルを設定してください。",
"modelId": "モデル ID",
"modelIdPlaceholder": "claude-3-sonnet",
"modelName": "表示名",
"modelNamePlaceholder": "Claude 3 Sonnet",
"contextWindow": "コンテキストウィンドウ",
"maxTokens": "最大出力トークン数",
"reasoning": "推論モード",
"reasoningOn": "有効",
"reasoningOff": "無効",
"inputCost": "入力コスト ($/M トークン)",
"outputCost": "出力コスト ($/M トークン)",
"advancedOptions": "詳細オプション",
"cacheReadCost": "キャッシュ読取コスト ($/M トークン)",
"cacheWriteCost": "キャッシュ書込コスト ($/M トークン)",
"cacheCostHint": "キャッシュコストは Prompt Caching のコスト計算に使用されます。キャッシュを使用しない場合は空欄のままにしてください。",
"modelsHint": "このプロバイダーがサポートするモデルを設定します。モデル ID は API 呼び出しに、表示名はインターフェースに使用されます。",
"env": {
"title": "環境変数",
"description": "openclaw.json の環境変数設定を管理(APIキー、カスタム変数など)",
@@ -1350,6 +1362,7 @@
"http429": "リクエストが多すぎます。時間をおいて再試行してください",
"parseMetadataFailed": "スキルメタデータの解析に失敗しました",
"getHomeDirFailed": "ユーザーのホームディレクトリを取得できません",
"noSkillsInZip": "ZIP ファイルにスキルが見つかりません(SKILL.md ファイルが必要です)",
"networkError": "ネットワークエラー",
"fsError": "ファイルシステムエラー",
"unknownError": "不明なエラー",
@@ -1360,7 +1373,8 @@
"checkRepoUrl": "リポジトリ URL とブランチ名を確認してください",
"checkDiskSpace": "ディスク容量を確認してください",
"checkPermission": "ディレクトリの権限を確認してください",
"uninstallFirst": "同名のスキルを先にアンインストールしてください"
"uninstallFirst": "同名のスキルを先にアンインストールしてください",
"checkZipContent": "ZIP ファイルに有効なスキルディレクトリ(SKILL.md を含む)が含まれていることを確認してください"
}
},
"repo": {
@@ -1630,7 +1644,12 @@
"toast": {
"saved": "プロキシ設定を保存しました",
"saveFailed": "保存に失敗しました: {{error}}"
}
},
"invalidPort": "無効なポートです。1024〜65535 の数値を入力してください",
"invalidAddress": "無効なアドレスです。有効な IP アドレス(例: 127.0.0.1)または localhost を入力してください",
"configSaved": "プロキシ設定を保存しました",
"configSaveFailed": "設定の保存に失敗しました",
"restartRequired": "アドレスまたはポートの変更を反映するにはプロキシサービスの再起動が必要です"
},
"switchFailed": "切り替えに失敗しました: {{error}}",
"failover": {
@@ -1660,6 +1679,7 @@
"info": "フェイルオーバーキューに複数のプロバイダーが設定されている場合、リクエストが失敗すると優先度順に試行します。プロバイダーが連続失敗のしきい値に達すると、サーキットブレーカーが開き、一時的にスキップされます。",
"configSaved": "自動フェイルオーバー設定を保存しました",
"configSaveFailed": "保存に失敗しました",
"validationFailed": "以下のフィールドが有効範囲外です: {{fields}}",
"retrySettings": "リトライとタイムアウト設定",
"failureThreshold": "失敗しきい値",
"failureThresholdHint": "この回数連続で失敗するとサーキットブレーカーが開きます(推奨: 3-10)",
@@ -1712,6 +1732,16 @@
"streamingIdle": "ストリーミングアイドルタイムアウト",
"nonStreaming": "非ストリーミングタイムアウト"
},
"circuitBreaker": {
"failureThreshold": "失敗しきい値",
"successThreshold": "成功しきい値",
"timeoutSeconds": "タイムアウト(秒)",
"errorRateThreshold": "エラー率しきい値",
"minRequests": "最小リクエスト数",
"validationFailed": "以下のフィールドが有効範囲外です: {{fields}}",
"configSaved": "サーキットブレーカー設定を保存しました",
"saveFailed": "保存に失敗しました"
},
"universalProvider": {
"title": "統合プロバイダー",
"description": "統合プロバイダーは Claude、Codex、Gemini の設定を同時に管理します。変更は有効なすべてのアプリに自動的に同期されます。",
@@ -1736,6 +1766,7 @@
"syncError": "同期に失敗しました",
"noAppsEnabled": "有効なアプリがありません",
"added": "統合プロバイダーを追加しました",
"addedAndSynced": "統合プロバイダーを追加して同期しました",
"updated": "統合プロバイダーを更新しました",
"deleted": "統合プロバイダーを削除しました",
"addSuccess": "統合プロバイダーを追加しました",
@@ -1788,10 +1819,6 @@
"enabled": "有効中",
"disabled": "OMO を無効化しました",
"disableFailed": "OMO の無効化に失敗しました: {{error}}",
"writeCommonConfig": "共通設定に書き込む",
"editCommonConfig": "共通設定を編集",
"editCommonConfigTitle": "共通設定",
"commonConfigHint": "OMO 共通設定は有効にしたすべての OMO 設定に統合されます",
"selectPlaceholder": "選択してください...",
"clear": "クリア",
"clearWrapped": "(クリア)",
@@ -1821,6 +1848,10 @@
"customCategories": "カスタムカテゴリ",
"modelConfiguration": "モデル設定",
"fillRecommended": "推奨を入力",
"fillRecommendedSuccess": "推奨モデル {{count}} 件を設定しました",
"fillRecommendedPartial": "推奨モデル {{filled}} 件を設定、{{unmatched}} 件は未一致",
"fillRecommendedAllSet": "すべてのスロットにモデルが設定済みです",
"fillRecommendedNoMatch": "推奨モデルが設定済みプロバイダーに見つかりませんでした",
"configSummary": "{{agents}} 個の Agent、{{categories}} 個の Category を設定済み · ⚙ で詳細を展開",
"enabledModelsCount": "設定済みモデル {{count}} 件",
"source": "出典:",
+50 -38
View File
@@ -52,6 +52,7 @@
"claudeConfig": {
"configLabel": "Claude Code 配置 (JSON) *",
"fullSettingsHint": "完整的 Claude Code settings.json 配置内容",
"fragmentSettingsHint": "此供应商的配置片段,激活后将写入 settings.json",
"hideAttribution": "隐藏 AI 署名",
"alwaysThinking": "扩展思考",
"enableTeammates": "Teammates 模式"
@@ -308,7 +309,17 @@
"rename": "重命名",
"renameSuccess": "备份重命名成功",
"renameFailed": "重命名失败",
"namePlaceholder": "输入新名称"
"namePlaceholder": "输入新名称",
"createBackup": "立即备份",
"creating": "备份中...",
"createSuccess": "备份创建成功",
"createFailed": "备份创建失败",
"delete": "删除",
"deleting": "删除中...",
"deleteSuccess": "备份已删除",
"deleteFailed": "删除失败",
"deleteConfirmTitle": "确认删除备份",
"deleteConfirmMessage": "此备份将被永久删除,此操作无法撤消。"
},
"webdavSync": {
"title": "WebDAV 云同步",
@@ -626,7 +637,10 @@
"cubence": "Cubence 是 CC Switch 的官方合作伙伴,使用此链接注册并在充值时填写 \"CCSWITCH\" 优惠码,每次充值均可享受9折优惠",
"aigocode": "AIGoCode 是 CC Switch 的官方合作伙伴,使用此链接注册首次充值时可以获得10%额度奖励!",
"rightcode": "RightCode 是 CC Switch 的官方合作伙伴,使用此链接注册每次充值均可赠送5%额外额度!",
"aicodemirror": "AICodeMirror 是 CC Switch 的官方合作伙伴,使用此链接注册可享受8折优惠!"
"aicodemirror": "AICodeMirror 是 CC Switch 的官方合作伙伴,使用此链接注册可享受8折优惠!",
"aicoding": "AI Coding 为 CC Switch 的用户提供了特殊优惠,首次充值可以享受 9 折优惠!",
"crazyrouter": "CrazyRouter 为 CC Switch 的用户提供了特殊优惠,首次充值赠予 30% 额外额度!",
"sssaicode": "SSAI Code 为 CC Switch 的用户提供了特殊优惠,每次充值赠予额外 $10 额度!"
},
"parameterConfig": "参数配置 - {{name}} *",
"mainModel": "主模型 (可选)",
@@ -778,38 +792,6 @@
"modelOptionKeyPlaceholder": "provider",
"modelOptionValuePlaceholder": "{\"order\": [\"baseten\"]}"
},
"openclaw": {
"providerKey": "供应商标识",
"providerKeyPlaceholder": "my-provider",
"providerKeyHint": "配置文件中的唯一标识符,创建后无法修改,只能使用小写字母、数字和连字符",
"providerKeyRequired": "请填写供应商标识",
"providerKeyDuplicate": "此标识已被使用,请更换",
"providerKeyInvalid": "标识格式无效,只能使用小写字母、数字和连字符",
"apiProtocol": "API 协议",
"selectProtocol": "选择 API 协议",
"apiProtocolHint": "选择与供应商 API 兼容的协议类型。大多数供应商使用 OpenAI Completions 格式。",
"baseUrl": "API 端点",
"baseUrlHint": "供应商的 API 端点地址。",
"models": "模型列表",
"addModel": "添加模型",
"noModels": "暂无模型配置。点击添加模型来配置可用模型。",
"modelId": "模型 ID",
"modelIdPlaceholder": "claude-3-sonnet",
"modelName": "显示名称",
"modelNamePlaceholder": "Claude 3 Sonnet",
"contextWindow": "上下文窗口",
"maxTokens": "最大输出 Tokens",
"reasoning": "推理模式",
"reasoningOn": "启用",
"reasoningOff": "关闭",
"inputCost": "输入价格 ($/M tokens)",
"outputCost": "输出价格 ($/M tokens)",
"advancedOptions": "高级选项",
"cacheReadCost": "缓存读取价格 ($/M tokens)",
"cacheWriteCost": "缓存写入价格 ($/M tokens)",
"cacheCostHint": "缓存价格用于计算 Prompt Caching 的成本。如不使用缓存可留空。",
"modelsHint": "配置该供应商支持的模型。模型 ID 用于 API 调用,显示名称用于界面展示。"
},
"providerPreset": {
"label": "预设供应商",
"custom": "自定义配置",
@@ -1221,6 +1203,36 @@
}
},
"openclaw": {
"providerKey": "供应商标识",
"providerKeyPlaceholder": "my-provider",
"providerKeyHint": "配置文件中的唯一标识符,创建后无法修改,只能使用小写字母、数字和连字符",
"providerKeyRequired": "请填写供应商标识",
"providerKeyDuplicate": "此标识已被使用,请更换",
"providerKeyInvalid": "标识格式无效,只能使用小写字母、数字和连字符",
"apiProtocol": "API 协议",
"selectProtocol": "选择 API 协议",
"apiProtocolHint": "选择与供应商 API 兼容的协议类型。大多数供应商使用 OpenAI Completions 格式。",
"baseUrl": "API 端点",
"baseUrlHint": "供应商的 API 端点地址。",
"models": "模型列表",
"addModel": "添加模型",
"noModels": "暂无模型配置。点击添加模型来配置可用模型。",
"modelId": "模型 ID",
"modelIdPlaceholder": "claude-3-sonnet",
"modelName": "显示名称",
"modelNamePlaceholder": "Claude 3 Sonnet",
"contextWindow": "上下文窗口",
"maxTokens": "最大输出 Tokens",
"reasoning": "推理模式",
"reasoningOn": "启用",
"reasoningOff": "关闭",
"inputCost": "输入价格 ($/M tokens)",
"outputCost": "输出价格 ($/M tokens)",
"advancedOptions": "高级选项",
"cacheReadCost": "缓存读取价格 ($/M tokens)",
"cacheWriteCost": "缓存写入价格 ($/M tokens)",
"cacheCostHint": "缓存价格用于计算 Prompt Caching 的成本。如不使用缓存可留空。",
"modelsHint": "配置该供应商支持的模型。模型 ID 用于 API 调用,显示名称用于界面展示。",
"env": {
"title": "环境变量",
"description": "管理 openclaw.json 中的环境变量配置(API Key、自定义变量等)",
@@ -1807,10 +1819,6 @@
"enabled": "启用中",
"disabled": "OMO 已停用",
"disableFailed": "停用 OMO 失败: {{error}}",
"writeCommonConfig": "写入通用配置",
"editCommonConfig": "编辑通用配置",
"editCommonConfigTitle": "通用配置",
"commonConfigHint": "OMO 通用配置将合并到所有启用它的 OMO 配置中",
"selectPlaceholder": "请选择...",
"clear": "清空",
"clearWrapped": "(清空)",
@@ -1840,6 +1848,10 @@
"customCategories": "自定义分类",
"modelConfiguration": "模型配置",
"fillRecommended": "填充推荐",
"fillRecommendedSuccess": "已填充 {{count}} 个推荐模型",
"fillRecommendedPartial": "已填充 {{filled}} 个推荐模型,{{unmatched}} 个未匹配",
"fillRecommendedAllSet": "所有槽位已有模型配置",
"fillRecommendedNoMatch": "推荐模型在已配置的供应商中未找到匹配",
"configSummary": "已配置 {{agents}} 个 Agent{{categories}} 个 Category · 点击 ⚙ 展开高级参数",
"enabledModelsCount": "可选已配置模型 {{count}} 个",
"source": "来源:",
+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated by Pixelmator Pro 3.6.17 -->
<svg width="470" height="470" viewBox="0 0 470 470" xmlns="http://www.w3.org/2000/svg">
<g id="Group">
<path id="Path" fill="#a78bfa" stroke="none" d="M 33 73 L 137 13 L 263 83 L 159 143 Z"/>
<path id="path1" fill="#a78bfa" stroke="none" opacity="0.92" d="M 33 73 L 33 213 L 159 283 L 159 143 Z"/>
<path id="path2" fill="#ffffff" stroke="none" d="M 207 247 L 311 187 L 431 257 L 327 317 Z"/>
<path id="path3" fill="#a78bfa" stroke="none" opacity="0.92" d="M 207 247 L 207 387 L 327 457 L 327 317 Z"/>
<path id="path4" fill="#fdba74" stroke="none" d="M 327 317 L 431 257 L 431 397 L 327 457 Z"/>
<path id="path5" fill="none" stroke="#2b1b4b" stroke-width="22" stroke-linecap="round" stroke-linejoin="round" d="M 33 73 L 137 13 L 263 83 L 159 143 L 33 73"/>
<path id="path6" fill="none" stroke="#2b1b4b" stroke-width="22" stroke-linecap="round" stroke-linejoin="round" d="M 33 73 L 33 213 L 159 283 L 159 143"/>
<path id="path7" fill="none" stroke="#2b1b4b" stroke-width="22" stroke-linecap="round" stroke-linejoin="round" d="M 159 143 L 263 83 L 263 223"/>
<path id="path8" fill="none" stroke="#2b1b4b" stroke-width="22" stroke-linecap="round" stroke-linejoin="round" d="M 207 247 L 311 187 L 431 257 L 327 317 L 207 247"/>
<path id="path9" fill="none" stroke="#2b1b4b" stroke-width="22" stroke-linecap="round" stroke-linejoin="round" d="M 207 247 L 207 387 L 327 457 L 327 317"/>
<path id="path10" fill="none" stroke="#2b1b4b" stroke-width="22" stroke-linecap="round" stroke-linejoin="round" d="M 327 317 L 431 257 L 431 397 L 327 457"/>
<path id="path11" fill="none" stroke="#2b1b4b" stroke-width="22" stroke-linecap="round" stroke-linejoin="round" d="M 245 163 L 365 163"/>
<path id="path12" fill="none" stroke="#2b1b4b" stroke-width="22" stroke-linecap="round" stroke-linejoin="round" d="M 175 197 L 335 117"/>
<path id="path13" fill="none" stroke="#2b1b4b" stroke-width="22" stroke-linecap="round" stroke-linejoin="round" d="M 365 163 L 365 241"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 24 KiB

+3
View File
@@ -3,6 +3,7 @@
export const icons: Record<string, string> = {
aicodemirror: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" style="flex:none;line-height:1" viewBox="0 0 1017.97 1056.47"><title>AICodeMirror</title><path fill="#E4906E" fill-rule="nonzero" d="M944.92 1014.53c-17.29,-9.23 -33.98,-19.28 -50.08,-30.16 -5.39,-3.65 -14.99,-11.7 -28.81,-24.16 -6.06,-5.47 -14.07,-13.51 -24.03,-24.13 -15.15,-16.17 -29.61,-29.9 -41.69,-40.4 -3.98,-3.46 -14.2,-11.02 -30.68,-22.69 -6.24,-4.42 -12.88,-12.15 -18.63,-19.09 -23.98,-29.04 -49.53,-58.44 -76.66,-88.19 -11.93,-13.1 -25.64,-26.11 -35.61,-36.5 -28.72,-29.92 -51.92,-51.96 -78.23,-79.66 -11.24,-11.83 -20.52,-21.3 -27.85,-28.41 -0.56,-0.53 -1.3,-0.84 -2.08,-0.84 -0.51,0 -1.02,0.14 -1.47,0.39 -9.76,5.54 -17.53,14.33 -24.91,23.31 -10.71,13.01 -21.86,26.65 -33.44,40.91 -4.37,5.38 -7.9,9.46 -10.56,12.24 -5.43,5.67 -9.83,10.88 -15.08,15.39 -9.57,8.23 -19.57,16.01 -29.98,23.31 -14.73,10.32 -29.07,20.29 -43.04,29.9 -5.22,3.61 -13.68,9.81 -20.14,15.41 -25.71,22.32 -53.59,46.12 -83.65,71.41 -9.46,7.95 -19.65,16.88 -34.02,29.22 -25.66,22.03 -52.94,40.06 -81.67,55.65 -7.71,4.19 -14.15,8.2 -19.32,12.01 -19.3,14.23 -33.84,25.65 -43.62,34.28 -25.99,22.91 -43.04,37.82 -51.16,44.73 -9.19,7.8 -18.6,17.05 -28.42,25.54 -2.71,2.35 -5.7,3.03 -8.96,2.01 -0.78,-0.24 -1.25,-1.02 -1.1,-1.82 0.36,-2 1.19,-4.1 2.47,-6.32 6.86,-11.81 14.46,-23.09 19.95,-36.03 3.48,-8.23 7.87,-16.52 13.18,-24.89 2.03,-3.19 4.73,-8.77 8.11,-16.74 2.98,-7.02 7.34,-15.05 13.07,-24.12 5.79,-9.14 16,-23.36 30.63,-42.67 7.66,-10.11 19.49,-23.6 35.49,-40.47 4.9,-5.16 12.21,-11.87 21.92,-20.14 12.12,-10.31 23.53,-21.19 34.23,-32.65 11.73,-12.54 16.99,-22.33 27.39,-40.8 2.37,-4.19 6.49,-9.43 12.37,-15.71 8.27,-8.82 17,-17.23 26.21,-25.23 30.11,-26.18 55.17,-47.43 75.17,-63.76 8.66,-7.08 26.42,-21.39 39.65,-30.77 17.11,-12.13 28.62,-20.44 34.53,-24.91 4.5,-3.4 8.93,-6.6 13.3,-10.56 26.03,-23.54 51.66,-45.71 77.28,-70.7 0.42,-0.41 0.51,-1.06 0.21,-1.58 -6.8,-11.78 -12.84,-21.8 -18.11,-30.06 -10.22,-15.99 -22.07,-29.65 -35.57,-40.99 -7.56,-6.36 -18.41,-13.85 -28.65,-20.43 -15.08,-9.66 -30.62,-21.97 -46.63,-36.9 -35.08,-32.73 -67.65,-71.22 -85.32,-115.42 -5.53,-13.85 -10.8,-29.31 -15.8,-46.37 -5.89,-20.13 -12.37,-35.63 -23.22,-51.27 -8.93,-12.9 -15.77,-21.94 -19.58,-35.93 -1.27,-4.67 -2.93,-12.75 -4.99,-24.23 -2.07,-11.54 -6.54,-22.62 -13.41,-33.25 -7.54,-11.68 -13.66,-21.04 -18.33,-28.08 -3.68,-5.53 -7.02,-12.39 -9.63,-18.53 -3.9,-9.18 -8.14,-15.7 -13.6,-23.37 -3.94,-5.53 -5.07,-12.75 0,-18.32 4.14,-4.57 17.49,-3.02 21.56,-1.13 3.86,1.81 8.1,5.13 12.71,9.94 16.16,16.88 26.41,27.77 30.74,32.66 4.69,5.31 11.21,13.79 16.69,19.94 20.19,22.63 36.17,39.74 47.36,59.71 10.46,18.66 16.41,30.42 29.84,44.67 9.32,9.92 17.94,19.33 25.85,28.23 9.01,10.15 19.25,22.95 30.72,38.39 7.54,10.17 13.89,20.11 19.05,29.84 6.39,12.05 10.8,30.19 15.13,41.41 4.88,12.67 12.52,23.25 22.92,31.75 0.58,0.47 6.79,5.44 18.62,14.89 13.54,10.82 23.74,23.47 30.61,37.96 4.55,9.58 7.82,16.16 9.8,19.74 6.62,11.85 14.64,22.05 24.07,30.59 8.99,8.14 17.47,13.2 31.06,22.64 4.28,2.96 6.68,5.98 10.65,2.54 7.08,-6.11 13.73,-10.71 17.96,-14.53 6.12,-5.54 11.71,-11.84 16.79,-18.92 3.5,-4.88 8.77,-10.16 15.19,-14.42 22.77,-15.02 38.17,-31.11 63.32,-55.15 22.13,-21.17 46.22,-47.56 69.25,-66.8 32.17,-26.89 54.99,-45.9 68.46,-57.01 17.15,-14.15 35.82,-30.97 56.02,-50.46 16.06,-15.5 29.25,-27.72 39.57,-36.66 9.78,-8.47 17.55,-14.8 23.31,-18.98 8.52,-6.2 18.55,-10.61 30.56,-15.03 12.34,-4.55 23.44,-11.06 35.67,-17.61 9.07,-4.85 19.76,-9.89 30.05,-8.84 0.5,0.06 0.97,0.32 1.3,0.72 0.85,1.07 1.01,2.48 0.48,4.23 -2.1,6.99 -5.15,13.55 -9.66,20.87 -6.42,10.42 -11.51,19.46 -15.29,27.11 -6.09,12.35 -9.66,19.49 -10.69,21.43 -7.78,14.65 -17.56,27.97 -29.34,39.97 -4.8,4.89 -12.93,12.92 -24.37,24.07 -14.23,13.87 -25.02,30.77 -37.12,50.78 -15.21,25.13 -29.56,48.47 -43.06,70.01 -5.21,8.29 -13.68,15.13 -21.58,21.47 -25.71,20.7 -46.75,41.48 -70.98,64.67 -1.97,1.88 -4.98,4.47 -9.03,7.76 -22.62,18.36 -45.88,35.93 -69.78,52.74 -5.96,4.2 -13.77,11.24 -23.42,21.11 -17.12,17.5 -25.93,26.56 -26.42,27.19 -1.22,1.54 -1.08,3.09 0.41,4.67 14.11,14.81 34.25,37.65 60.42,68.51 11.89,14.01 24.87,27.08 36.03,39.46 8.75,9.7 16.81,22.11 31.82,42.59 2.69,3.68 13.53,16.07 32.5,37.17 5.17,5.76 11.64,14.47 19.4,26.12 16.37,24.6 36.28,56.2 59.73,94.79 4.2,6.92 7.74,12.33 10.62,16.21 5.41,7.29 10.37,13.74 14.92,20.97 6.26,9.94 11.3,19.92 15.11,29.92 4.29,11.27 7.73,19.49 10.32,24.69 7.21,14.5 14.81,28.41 22.8,41.73 3.44,5.75 6.78,13.03 6.11,20.05 -0.07,0.76 -0.71,1.34 -1.48,1.34 -0.25,0 -0.49,-0.06 -0.71,-0.18l0 0.01z"/></svg>`,
aicoding: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" style="flex:none;line-height:1" viewBox="0 0 470 470"><title>AICoding</title><path fill="#a78bfa" d="M 33 73 L 137 13 L 263 83 L 159 143 Z"/><path fill="#a78bfa" opacity="0.92" d="M 33 73 L 33 213 L 159 283 L 159 143 Z"/><path fill="#fff" d="M 207 247 L 311 187 L 431 257 L 327 317 Z"/><path fill="#a78bfa" opacity="0.92" d="M 207 247 L 207 387 L 327 457 L 327 317 Z"/><path fill="#fdba74" d="M 327 317 L 431 257 L 431 397 L 327 457 Z"/><path fill="none" stroke="#2b1b4b" stroke-width="22" stroke-linecap="round" stroke-linejoin="round" d="M 33 73 L 137 13 L 263 83 L 159 143 L 33 73"/><path fill="none" stroke="#2b1b4b" stroke-width="22" stroke-linecap="round" stroke-linejoin="round" d="M 33 73 L 33 213 L 159 283 L 159 143"/><path fill="none" stroke="#2b1b4b" stroke-width="22" stroke-linecap="round" stroke-linejoin="round" d="M 159 143 L 263 83 L 263 223"/><path fill="none" stroke="#2b1b4b" stroke-width="22" stroke-linecap="round" stroke-linejoin="round" d="M 207 247 L 311 187 L 431 257 L 327 317 L 207 247"/><path fill="none" stroke="#2b1b4b" stroke-width="22" stroke-linecap="round" stroke-linejoin="round" d="M 207 247 L 207 387 L 327 457 L 327 317"/><path fill="none" stroke="#2b1b4b" stroke-width="22" stroke-linecap="round" stroke-linejoin="round" d="M 327 317 L 431 257 L 431 397 L 327 457"/><path fill="none" stroke="#2b1b4b" stroke-width="22" stroke-linecap="round" stroke-linejoin="round" d="M 245 163 L 365 163"/><path fill="none" stroke="#2b1b4b" stroke-width="22" stroke-linecap="round" stroke-linejoin="round" d="M 175 197 L 335 117"/><path fill="none" stroke="#2b1b4b" stroke-width="22" stroke-linecap="round" stroke-linejoin="round" d="M 365 163 L 365 241"/></svg>`,
aigocode: `<svg height="1em" width="1em" style="flex:none;line-height:1" viewBox="0 0 648 564" xmlns="http://www.w3.org/2000/svg"><title>AiGoCode</title><g transform="translate(0,564) scale(0.1,-0.1)"><path fill="#7C6AEF" d="M5392 5379 c-26 -10 -36 -28 -56 -108 -24 -94 -47 -140 -101 -199 -56 -62 -117 -96 -219 -121 -101 -25 -116 -35 -116 -75 0 -45 21 -60 123 -89 190 -53 271 -147 336 -384 13 -46 38 -61 85 -49 25 6 36 28 56 111 7 28 23 72 36 99 13 28 21 54 18 58 -3 5 -2 7 3 6 4 -2 29 18 55 44 41 41 145 110 173 114 56 8 126 27 131 36 4 6 7 30 8 54 1 49 -5 54 -104 74 -164 33 -280 148 -320 320 -23 101 -52 129 -108 109z"/><path fill="#5B7FFF" d="M1770 4814 c-138 -25 -301 -93 -425 -177 -80 -55 -227 -197 -280 -272 -98 -138 -161 -279 -201 -447 -16 -66 -18 -164 -21 -1098 -4 -1130 -4 -1144 57 -1331 80 -242 222 -434 444 -598 l56 -42 0 -337 c0 -309 2 -340 19 -379 31 -66 87 -93 158 -74 22 6 202 117 404 249 200 131 398 259 439 285 144 89 48 81 1095 88 912 5 932 6 1012 27 243 64 441 178 599 344 166 176 274 408 304 648 13 110 13 2016 0 2120 -25 190 -83 347 -183 498 -160 240 -384 400 -677 484 l-75 22 -1325 2 c-1086 2 -1339 0 -1400 -12z m1068 -1455 l-3 -751 -71 -18 c-71 -18 -154 -55 -205 -91 -14 -10 -29 -16 -33 -12 -3 3 -6 91 -6 195 l0 188 -306 0 -305 0 -21 -47 c-11 -27 -33 -75 -48 -108 -16 -33 -44 -96 -62 -140 l-34 -80 -172 -3 c-101 -1 -172 1 -172 7 0 5 14 40 31 78 31 68 101 227 254 578 335 769 358 814 434 874 94 74 122 79 449 80 l272 1 -2 -751z m794 717 c41 -13 103 -42 139 -62 67 -40 202 -168 232 -221 l17 -31 -77 -65 c-43 -35 -101 -80 -129 -101 l-52 -36 -39 57 c-79 116 -204 177 -313 154 -66 -14 -105 -42 -130 -91 -19 -37 -20 -60 -20 -400 0 -339 1 -363 20 -399 26 -51 61 -78 128 -97 143 -42 327 52 366 186 l13 45 -163 3 -163 2 -3 157 c-2 111 0 157 9 160 6 2 263 3 570 1 487 -3 562 -5 593 -19 98 -45 125 -159 58 -244 -39 -50 -66 -55 -322 -55 l-236 0 -6 -27 c-18 -83 -29 -115 -62 -183 -69 -143 -230 -269 -402 -316 -81 -22 -271 -25 -350 -5 -173 43 -289 145 -341 298 -19 57 -20 81 -17 534 l3 474 33 67 c55 112 168 199 307 235 79 20 246 10 337 -21z m828 -1515 c67 -71 67 -73 -62 -396 -45 -110 -102 -254 -128 -320 -254 -639 -272 -681 -298 -702 -74 -62 -192 -15 -192 77 0 12 39 116 86 233 48 117 97 239 110 272 119 311 336 833 354 852 36 37 85 32 130 -16z m-1574 -205 c16 -13 19 -29 22 -128 l3 -113 -195 -155 c-108 -85 -196 -158 -196 -161 0 -4 17 -19 38 -35 128 -96 327 -272 339 -301 16 -39 17 -143 2 -177 -15 -33 -34 -39 -67 -22 -48 26 -566 446 -584 474 -23 35 -23 89 1 128 16 27 150 143 376 326 39 31 79 65 90 75 44 41 128 103 139 103 7 0 21 -6 32 -14z m713 -25 c52 -37 53 -33 -92 -551 -36 -129 -80 -289 -98 -355 -39 -143 -62 -175 -129 -175 -47 0 -92 20 -114 52 -24 34 -19 90 18 217 19 64 78 271 131 461 53 190 98 353 101 364 6 16 14 18 79 14 54 -4 81 -11 104 -27z m1116 -351 c55 -45 117 -98 138 -120 61 -64 48 -115 -50 -192 -32 -25 -118 -94 -191 -152 -136 -108 -163 -120 -217 -100 -26 10 -47 62 -39 97 10 46 22 62 94 119 36 29 93 77 128 106 l62 55 -65 59 c-71 65 -77 84 -49 141 24 47 44 67 68 67 12 0 66 -36 121 -80z"/><path fill="#5B7FFF" d="M2328 3755 c-37 -20 -54 -53 -153 -280 -48 -110 -96 -219 -106 -242 l-18 -43 234 0 235 0 0 290 0 290 -82 0 c-53 -1 -93 -6 -110 -15z"/></g></svg>`,
alibaba: `<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Alibaba</title><path d="M24 14.014c-2.8 1.512-5.62 2.896-8.759 3.524-.7.139-1.476.139-2.187.043-.678-.085-1.017-.682-.776-1.31.23-.585.536-1.181.93-1.671.852-1.065 1.814-2.034 2.678-3.088a15.75 15.75 0 001.422-2.054c.306-.511.164-1.129-.372-1.384-.897-.437-1.859-.745-2.81-1.075-.11-.043-.274.074-.492.149.273.244.47.425.743.67-2.821.48-5.49 1.16-8.08 2.098-.012.053-.033.095-.023.117.383.585.208 1.032-.35 1.394a2.365 2.365 0 00-.568.522c1.706.5 3.226.213 4.68-.735-.087-.127-.175-.244-.262-.372.546.096.874.394.918.862.011.107-.054.213-.087.32-.077-.086-.175-.17-.24-.267-.045-.064-.056-.138-.088-.245-1.728 1.15-3.587 1.438-5.632.842 0 .404-.022.745.011 1.075.022.287-.098.415-.36.564-.591.362-1.204.735-1.696 1.214-.59.585-.371 1.299.427 1.597.907.34 1.859.35 2.81.234 1.126-.139 2.23-.32 3.456-.49-1.433.67-2.844 1.14-4.33 1.33-1.04.14-2.078.214-3.106-.084-1.476-.415-2.133-1.501-1.75-2.96.361-1.363 1.236-2.449 2.176-3.45 3.139-3.332 7.108-5.024 11.7-5.365 1.072-.074 2.155.064 3.16.511 1.411.639 2.002 1.99 1.313 3.354-.448.905-1.072 1.735-1.695 2.555-.612.809-1.301 1.554-1.946 2.331-.186.234-.361.48-.503.745-.274.5-.088.83.492.778 1.213-.118 2.45-.213 3.62-.511 1.716-.437 3.389-1.054 5.084-1.597.175-.043.339-.107.492-.17z" fill="#FF6003" fill-rule="evenodd"></path></svg>`,
anthropic: `<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>Anthropic</title><path d="M13.827 3.52h3.603L24 20h-3.603l-6.57-16.48zm-7.258 0h3.767L16.906 20h-3.674l-1.343-3.461H5.017l-1.344 3.46H0L6.57 3.522zm4.132 9.959L8.453 7.687 6.205 13.48H10.7z"></path></svg>`,
@@ -15,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>`,
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>`,
doubao: `<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Doubao</title><path d="M5.31 15.756c.172-3.75 1.883-5.999 2.549-6.739-3.26 2.058-5.425 5.658-6.358 8.308v1.12C1.501 21.513 4.226 24 7.59 24a6.59 6.59 0 002.2-.375c.353-.12.7-.248 1.039-.378.913-.899 1.65-1.91 2.243-2.992-4.877 2.431-7.974.072-7.763-4.5l.002.001z" fill="#1E37FC"></path><path d="M22.57 10.283c-1.212-.901-4.109-2.404-7.397-2.8.295 3.792.093 8.766-2.1 12.773a12.782 12.782 0 01-2.244 2.992c3.764-1.448 6.746-3.457 8.596-5.219 2.82-2.683 3.353-5.178 3.361-6.66a2.737 2.737 0 00-.216-1.084v-.002z" fill="#37E1BE"></path><path d="M14.303 1.867C12.955.7 11.248 0 9.39 0 7.532 0 5.883.677 4.545 1.807 2.791 3.29 1.627 5.557 1.5 8.125v9.201c.932-2.65 3.097-6.25 6.357-8.307.5-.318 1.025-.595 1.569-.829 1.883-.801 3.878-.932 5.746-.706-.222-2.83-.718-5.002-.87-5.617h.001z" fill="#A569FF"></path><path d="M17.305 4.961a199.47 199.47 0 01-1.08-1.094c-.202-.213-.398-.419-.586-.622l-1.333-1.378c.151.615.648 2.786.869 5.617 3.288.395 6.185 1.898 7.396 2.8-1.306-1.275-3.475-3.487-5.266-5.323z" fill="#1E37FC"></path></svg>`,
@@ -58,6 +60,7 @@ export const icons: Record<string, string> = {
aihubmix: `<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>AiHubMix</title><path d="M12 24c6.627 0 12-5.373 12-12S18.627 0 12 0 0 5.373 0 12s5.373 12 12 12z" fill="#006FFB"></path><path clip-rule="evenodd" d="M11.24 8.393c.095-.644.302-1.47.624-2.48L12 5.496l.136.417c.322 1.01.53 1.836.624 2.48.071.472.071 1.072 0 1.8-.072.731-.072 1.336 0 1.814.106.7.426 1.281.96 1.744a2.795 2.795 0 001.89.708 2.78 2.78 0 002.034-.84c.56-.559.842-1.234.848-2.024.003-.7.075-1.472.216-2.316.069-.422.14-.775.21-1.06l.095-.384.168.356a7.862 7.862 0 01.76 3.244v.16a7.84 7.84 0 01-.624 3.089 7.952 7.952 0 01-4.228 4.228 7.841 7.841 0 01-3.089.623 7.84 7.84 0 01-3.089-.623 7.952 7.952 0 01-4.228-4.228 7.84 7.84 0 01-.623-3.09v-.159a7.862 7.862 0 01.759-3.244l.169-.356.093.385c.072.284.143.637.211 1.059.141.844.213 1.616.216 2.316.006.79.29 1.465.848 2.024.563.56 1.241.84 2.035.84.715 0 1.345-.236 1.889-.708a2.79 2.79 0 00.96-1.744c.073-.478.073-1.083 0-1.814-.071-.728-.071-1.328 0-1.8zm.76 9.694c1.097 0 2.125-.26 3.085-.778a6.379 6.379 0 001.77-1.399c.063-.07-.01-.178-.101-.153-.37.1-.75.15-1.144.15a4.236 4.236 0 01-2.18-.59 4.253 4.253 0 01-1.35-1.233.099.099 0 00-.16 0 4.253 4.253 0 01-1.35 1.232 4.236 4.236 0 01-2.18.591c-.393 0-.774-.05-1.143-.15-.091-.025-.165.083-.102.153a6.38 6.38 0 001.77 1.399c.96.518 1.988.778 3.085.778z" fill="#fff" fill-rule="evenodd"></path></svg>`,
opencode: `<svg height="1em" width="1em" style="flex:none;line-height:1" viewBox="0 0 240 300" xmlns="http://www.w3.org/2000/svg"><title>OpenCode</title><g clip-path="url(#clip0_1401_86274)"><mask id="mask0_1401_86274" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="240" height="300"><path d="M240 0H0V300H240V0Z" fill="white"/></mask><g mask="url(#mask0_1401_86274)"><path d="M180 240H60V120H180V240Z" fill="#CFCECD"/><path d="M180 60H60V240H180V60ZM240 300H0V0H240V300Z" fill="#211E1E"/></g></g><defs><clipPath id="clip0_1401_86274"><rect width="240" height="300" fill="white"/></clipPath></defs></svg>`,
siliconflow: `<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>SiliconCloud</title><path clip-rule="evenodd" d="M22.956 6.521H12.522c-.577 0-1.044.468-1.044 1.044v3.13c0 .577-.466 1.044-1.043 1.044H1.044c-.577 0-1.044.467-1.044 1.044v4.174C0 17.533.467 18 1.044 18h10.434c.577 0 1.044-.467 1.044-1.043v-3.13c0-.578.466-1.044 1.043-1.044h9.391c.577 0 1.044-.467 1.044-1.044V7.565c0-.576-.467-1.044-1.044-1.044z" fill="#6E29F6" fill-rule="evenodd"></path></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>`,
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>`,
nvidia: `<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Nvidia</title><path d="M10.212 8.976V7.62c.127-.01.256-.017.388-.021 3.596-.117 5.957 3.184 5.957 3.184s-2.548 3.647-5.282 3.647a3.227 3.227 0 01-1.063-.175v-4.109c1.4.174 1.681.812 2.523 2.258l1.873-1.627a4.905 4.905 0 00-3.67-1.846 6.594 6.594 0 00-.729.044m0-4.476v2.025c.13-.01.259-.019.388-.024 5.002-.174 8.261 4.226 8.261 4.226s-3.743 4.69-7.643 4.69c-.338 0-.675-.031-1.007-.092v1.25c.278.038.558.057.838.057 3.629 0 6.253-1.91 8.794-4.169.421.347 2.146 1.193 2.501 1.564-2.416 2.083-8.048 3.763-11.24 3.763-.308 0-.603-.02-.894-.048V19.5H24v-15H10.21zm0 9.756v1.068c-3.356-.616-4.287-4.21-4.287-4.21a7.173 7.173 0 014.287-2.138v1.172h-.005a3.182 3.182 0 00-2.502 1.178s.615 2.276 2.507 2.931m-5.961-3.3c1.436-1.935 3.604-3.148 5.961-3.336V6.523C5.81 6.887 2 10.723 2 10.723s2.158 6.427 8.21 7.015v-1.166C5.77 16 4.25 10.958 4.25 10.958h-.002z" fill="#74B71B" fill-rule="nonzero"></path></svg>`,
+73
View File
@@ -0,0 +1,73 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
<defs>
<!-- Teal-cyan gradient for left triangle -->
<linearGradient id="gradLeft" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#0ff5ce" />
<stop offset="100%" stop-color="#147a8a" />
</linearGradient>
<!-- Light blue-white gradient for right triangle -->
<linearGradient id="gradRight" x1="100%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#d0e4f5" />
<stop offset="100%" stop-color="#6a9ec4" />
</linearGradient>
<!-- Top triangle gradient -->
<linearGradient id="gradTop" x1="50%" y1="0%" x2="50%" y2="100%">
<stop offset="0%" stop-color="#a0d8e8" />
<stop offset="100%" stop-color="#4aafbf" />
</linearGradient>
<!-- Text gradient -->
<linearGradient id="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>
<!-- S letter gradient -->
<linearGradient id="gradS" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#f0f8ff" />
<stop offset="100%" stop-color="#6cbfcf" />
</linearGradient>
<!-- Glow filter -->
<filter id="glow" x="-30%" y="-30%" width="160%" height="160%">
<feGaussianBlur stdDeviation="4" result="blur" />
<feMerge>
<feMergeNode in="blur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
<!-- Binary pattern left -->
<pattern id="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>
<!-- Binary pattern right -->
<pattern id="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>
<!-- Binary pattern top -->
<pattern id="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>
<!-- Background -->
<rect width="512" height="512" rx="72" fill="#08080e" />
<!-- Left triangle (bottom-left, overlapping center) -->
<polygon points="90,350 250,350 170,228" fill="url(#gradLeft)" opacity="0.8" />
<polygon points="90,350 250,350 170,228" fill="url(#binL)" />
<!-- Right triangle (bottom-right, overlapping center) -->
<polygon points="262,350 422,350 342,228" fill="url(#gradRight)" opacity="0.8" />
<polygon points="262,350 422,350 342,228" fill="url(#binR)" />
<!-- Top triangle (center, overlapping both) -->
<polygon points="176,290 336,290 256,168" fill="none" stroke="url(#gradTop)" stroke-width="2.5" opacity="0.85" />
<!-- Subtle inner triangle -->
<polygon points="192,280 320,280 256,184" fill="none" stroke="url(#gradTop)" stroke-width="0.8" opacity="0.35" />
<!-- Central "S" with glow -->
<text x="256" y="316" text-anchor="middle" font-family="Georgia, 'Times New Roman', serif" font-size="120" font-weight="bold" fill="url(#gradS)" filter="url(#glow)">S</text>
<!-- "SSSAiCode" 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(#gradText)">SSSAiCode</text>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

-28
View File
@@ -1,28 +0,0 @@
// 配置相关 API
import { invoke } from "@tauri-apps/api/core";
export type AppType = "claude" | "codex" | "gemini" | "omo" | "omo_slim";
/**
* 获取通用配置片段(统一接口)
* @param appType - 应用类型(claude/codex/gemini
* @returns 通用配置片段(原始字符串),如果不存在则返回 null
*/
export async function getCommonConfigSnippet(
appType: AppType,
): Promise<string | null> {
return invoke<string | null>("get_common_config_snippet", { appType });
}
/**
* 设置通用配置片段(统一接口)
* @param appType - 应用类型(claude/codex/gemini
* @param snippet - 通用配置片段(原始字符串)
* @throws 如果格式无效(Claude/Gemini 验证 JSONCodex 暂不验证)
*/
export async function setCommonConfigSnippet(
appType: AppType,
snippet: string,
): Promise<void> {
return invoke("set_common_config_snippet", { appType, snippet });
}
-1
View File
@@ -11,6 +11,5 @@ export { proxyApi } from "./proxy";
export { openclawApi } from "./openclaw";
export { sessionsApi } from "./sessions";
export { workspaceApi } from "./workspace";
export * as configApi from "./config";
export type { ProviderSwitchEvent } from "./providers";
export type { Prompt } from "./prompts";
-3
View File
@@ -5,7 +5,6 @@ export const omoApi = {
readLocalFile: (): Promise<OmoLocalFileData> => invoke("read_omo_local_file"),
getCurrentOmoProviderId: (): Promise<string> =>
invoke("get_current_omo_provider_id"),
getOmoProviderCount: (): Promise<number> => invoke("get_omo_provider_count"),
disableCurrentOmo: (): Promise<void> => invoke("disable_current_omo"),
};
@@ -14,7 +13,5 @@ export const omoSlimApi = {
invoke("read_omo_slim_local_file"),
getCurrentProviderId: (): Promise<string> =>
invoke("get_current_omo_slim_provider_id"),
getProviderCount: (): Promise<number> =>
invoke("get_omo_slim_provider_count"),
disableCurrent: (): Promise<void> => invoke("disable_current_omo_slim"),
};
+8
View File
@@ -223,6 +223,10 @@ export interface BackupEntry {
}
export const backupsApi = {
async createDbBackup(): Promise<string> {
return await invoke("create_db_backup");
},
async listDbBackups(): Promise<BackupEntry[]> {
return await invoke("list_db_backups");
},
@@ -234,4 +238,8 @@ export const backupsApi = {
async renameDbBackup(oldFilename: string, newName: string): Promise<string> {
return await invoke("rename_db_backup", { oldFilename, newName });
},
async deleteDbBackup(filename: string): Promise<void> {
await invoke("delete_db_backup", { filename });
},
};
+15
View File
@@ -57,6 +57,12 @@ export const useAddProviderMutation = (appId: AppId) => {
await queryClient.invalidateQueries({
queryKey: ["omo", "provider-count"],
});
await queryClient.invalidateQueries({
queryKey: ["omo-slim", "current-provider-id"],
});
await queryClient.invalidateQueries({
queryKey: ["omo-slim", "provider-count"],
});
}
try {
@@ -139,6 +145,12 @@ export const useDeleteProviderMutation = (appId: AppId) => {
await queryClient.invalidateQueries({
queryKey: ["omo", "provider-count"],
});
await queryClient.invalidateQueries({
queryKey: ["omo-slim", "current-provider-id"],
});
await queryClient.invalidateQueries({
queryKey: ["omo-slim", "provider-count"],
});
}
try {
@@ -190,6 +202,9 @@ export const useSwitchProviderMutation = (appId: AppId) => {
await queryClient.invalidateQueries({
queryKey: ["omo", "current-provider-id"],
});
await queryClient.invalidateQueries({
queryKey: ["omo-slim", "current-provider-id"],
});
}
if (appId === "openclaw") {
await queryClient.invalidateQueries({
-74
View File
@@ -1,16 +1,12 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { omoApi, omoSlimApi } from "@/lib/api/omo";
import * as configApi from "@/lib/api/config";
import type { OmoGlobalConfig } from "@/types/omo";
// ── Factory ────────────────────────────────────────────────────
function createOmoQueryKeys(prefix: string) {
return {
all: [prefix] as const,
globalConfig: () => [prefix, "global-config"] as const,
currentProviderId: () => [prefix, "current-provider-id"] as const,
providerCount: () => [prefix, "provider-count"] as const,
};
}
@@ -19,49 +15,10 @@ function createOmoQueryHooks(
api: typeof omoApi | typeof omoSlimApi,
) {
const keys = createOmoQueryKeys(variant);
const snippetKey = variant === "omo" ? "omo" : "omo_slim";
function invalidateAll(queryClient: ReturnType<typeof useQueryClient>) {
queryClient.invalidateQueries({ queryKey: keys.globalConfig() });
queryClient.invalidateQueries({ queryKey: ["providers"] });
queryClient.invalidateQueries({ queryKey: keys.currentProviderId() });
queryClient.invalidateQueries({ queryKey: keys.providerCount() });
}
function useGlobalConfig(enabled = true) {
return useQuery({
queryKey: keys.globalConfig(),
enabled,
queryFn: async (): Promise<OmoGlobalConfig> => {
const raw = await configApi.getCommonConfigSnippet(snippetKey);
if (!raw) {
return {
id: "global",
disabledAgents: [],
disabledMcps: [],
disabledHooks: [],
disabledSkills: [],
updatedAt: new Date().toISOString(),
};
}
try {
return JSON.parse(raw) as OmoGlobalConfig;
} catch (error) {
console.warn(
`[${variant}] invalid global config json, fallback to defaults`,
error,
);
return {
id: "global",
disabledAgents: [],
disabledMcps: [],
disabledHooks: [],
disabledSkills: [],
updatedAt: new Date().toISOString(),
};
}
},
});
}
function useCurrentProviderId(enabled = true) {
@@ -75,28 +32,6 @@ function createOmoQueryHooks(
});
}
function useProviderCount(enabled = true) {
return useQuery({
queryKey: keys.providerCount(),
queryFn:
"getOmoProviderCount" in api
? (api as typeof omoApi).getOmoProviderCount
: (api as typeof omoSlimApi).getProviderCount,
enabled,
});
}
function useSaveGlobalConfig() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (input: OmoGlobalConfig) => {
const jsonStr = JSON.stringify(input);
await configApi.setCommonConfigSnippet(snippetKey, jsonStr);
},
onSuccess: () => invalidateAll(queryClient),
});
}
function useReadLocalFile() {
return useMutation({
mutationFn: () => api.readLocalFile(),
@@ -116,10 +51,7 @@ function createOmoQueryHooks(
return {
keys,
useGlobalConfig,
useCurrentProviderId,
useProviderCount,
useSaveGlobalConfig,
useReadLocalFile,
useDisableCurrent,
};
@@ -135,16 +67,10 @@ const omoSlimHooks = createOmoQueryHooks("omo-slim", omoSlimApi);
export const omoKeys = omoHooks.keys;
export const omoSlimKeys = omoSlimHooks.keys;
export const useOmoGlobalConfig = omoHooks.useGlobalConfig;
export const useCurrentOmoProviderId = omoHooks.useCurrentProviderId;
export const useOmoProviderCount = omoHooks.useProviderCount;
export const useSaveOmoGlobalConfig = omoHooks.useSaveGlobalConfig;
export const useReadOmoLocalFile = omoHooks.useReadLocalFile;
export const useDisableCurrentOmo = omoHooks.useDisableCurrent;
export const useOmoSlimGlobalConfig = omoSlimHooks.useGlobalConfig;
export const useCurrentOmoSlimProviderId = omoSlimHooks.useCurrentProviderId;
export const useOmoSlimProviderCount = omoSlimHooks.useProviderCount;
export const useSaveOmoSlimGlobalConfig = omoSlimHooks.useSaveGlobalConfig;
export const useReadOmoSlimLocalFile = omoSlimHooks.useReadLocalFile;
export const useDisableCurrentOmoSlim = omoSlimHooks.useDisableCurrent;
+16 -103
View File
@@ -1,25 +1,7 @@
export interface OmoGlobalConfig {
id: string;
schemaUrl?: string;
sisyphusAgent?: Record<string, unknown>;
disabledAgents: string[];
disabledMcps: string[];
disabledHooks: string[];
disabledSkills: string[];
lsp?: Record<string, unknown>;
experimental?: Record<string, unknown>;
backgroundTask?: Record<string, unknown>;
browserAutomationEngine?: Record<string, unknown>;
claudeCode?: Record<string, unknown>;
otherFields?: Record<string, unknown>;
updatedAt: string;
}
export interface OmoLocalFileData {
agents?: Record<string, Record<string, unknown>>;
categories?: Record<string, Record<string, unknown>>;
otherFields?: Record<string, unknown>;
global: OmoGlobalConfig;
filePath: string;
lastModified?: string;
}
@@ -79,7 +61,7 @@ export const OMO_BUILTIN_AGENTS: OmoAgentDef[] = [
display: "Oracle",
descKey: "omo.agentDesc.oracle",
tooltipKey: "omo.agentTooltip.oracle",
recommended: "gpt-5.3",
recommended: "gpt-5.2",
group: "sub",
},
{
@@ -87,7 +69,7 @@ export const OMO_BUILTIN_AGENTS: OmoAgentDef[] = [
display: "Librarian",
descKey: "omo.agentDesc.librarian",
tooltipKey: "omo.agentTooltip.librarian",
recommended: "glm-4.7",
recommended: "gemini-3-flash",
group: "sub",
},
{
@@ -103,7 +85,7 @@ export const OMO_BUILTIN_AGENTS: OmoAgentDef[] = [
display: "Multimodal-Looker",
descKey: "omo.agentDesc.multimodalLooker",
tooltipKey: "omo.agentTooltip.multimodalLooker",
recommended: "gemini-3-flash",
recommended: "kimi-k2.5",
group: "sub",
},
{
@@ -119,7 +101,7 @@ export const OMO_BUILTIN_AGENTS: OmoAgentDef[] = [
display: "Momus",
descKey: "omo.agentDesc.momus",
tooltipKey: "omo.agentTooltip.momus",
recommended: "gpt-5.3",
recommended: "gpt-5.2",
group: "sub",
},
{
@@ -144,7 +126,7 @@ export const OMO_BUILTIN_CATEGORIES: OmoCategoryDef[] = [
display: "Ultrabrain",
descKey: "omo.categoryDesc.ultrabrain",
tooltipKey: "omo.categoryTooltip.ultrabrain",
recommended: "claude-opus-4-6",
recommended: "gpt-5.3-codex",
},
{
key: "deep",
@@ -158,35 +140,35 @@ export const OMO_BUILTIN_CATEGORIES: OmoCategoryDef[] = [
display: "Artistry",
descKey: "omo.categoryDesc.artistry",
tooltipKey: "omo.categoryTooltip.artistry",
recommended: "claude-opus-4-6",
recommended: "gemini-3-pro",
},
{
key: "quick",
display: "Quick",
descKey: "omo.categoryDesc.quick",
tooltipKey: "omo.categoryTooltip.quick",
recommended: "gemini-3-flash",
recommended: "claude-haiku-4-5",
},
{
key: "unspecified-low",
display: "Unspecified Low",
descKey: "omo.categoryDesc.unspecifiedLow",
tooltipKey: "omo.categoryTooltip.unspecifiedLow",
recommended: "gemini-3-flash",
recommended: "claude-sonnet-4-6",
},
{
key: "unspecified-high",
display: "Unspecified High",
descKey: "omo.categoryDesc.unspecifiedHigh",
tooltipKey: "omo.categoryTooltip.unspecifiedHigh",
recommended: "gpt-5.3-codex",
recommended: "claude-opus-4-6",
},
{
key: "writing",
display: "Writing",
descKey: "omo.categoryDesc.writing",
tooltipKey: "omo.categoryTooltip.writing",
recommended: "claude-opus-4-6",
recommended: "gemini-3-flash",
},
];
@@ -337,7 +319,7 @@ export const OMO_SLIM_BUILTIN_AGENTS: OmoAgentDef[] = [
display: "Orchestrator",
descKey: "omo.slimAgentDesc.orchestrator",
tooltipKey: "omo.slimAgentTooltip.orchestrator",
recommended: "kimi-for-coding/k2p5",
recommended: "claude-opus-4-6",
group: "main",
},
{
@@ -345,7 +327,7 @@ export const OMO_SLIM_BUILTIN_AGENTS: OmoAgentDef[] = [
display: "Oracle",
descKey: "omo.slimAgentDesc.oracle",
tooltipKey: "omo.slimAgentTooltip.oracle",
recommended: "openai/gpt-5.2-codex",
recommended: "gpt-5.2",
group: "sub",
},
{
@@ -353,7 +335,7 @@ export const OMO_SLIM_BUILTIN_AGENTS: OmoAgentDef[] = [
display: "Librarian",
descKey: "omo.slimAgentDesc.librarian",
tooltipKey: "omo.slimAgentTooltip.librarian",
recommended: "openai/gpt-5.1-codex-mini",
recommended: "gemini-3-flash",
group: "sub",
},
{
@@ -361,7 +343,7 @@ export const OMO_SLIM_BUILTIN_AGENTS: OmoAgentDef[] = [
display: "Explorer",
descKey: "omo.slimAgentDesc.explorer",
tooltipKey: "omo.slimAgentTooltip.explorer",
recommended: "openai/gpt-5.1-codex-mini",
recommended: "grok-code-fast-1",
group: "sub",
},
{
@@ -369,7 +351,7 @@ export const OMO_SLIM_BUILTIN_AGENTS: OmoAgentDef[] = [
display: "Designer",
descKey: "omo.slimAgentDesc.designer",
tooltipKey: "omo.slimAgentTooltip.designer",
recommended: "kimi-for-coding/k2p5",
recommended: "gemini-3-pro",
group: "sub",
},
{
@@ -377,7 +359,7 @@ export const OMO_SLIM_BUILTIN_AGENTS: OmoAgentDef[] = [
display: "Fixer",
descKey: "omo.slimAgentDesc.fixer",
tooltipKey: "omo.slimAgentTooltip.fixer",
recommended: "openai/gpt-5.1-codex-mini",
recommended: "gpt-5.3-codex",
group: "sub",
},
];
@@ -406,75 +388,6 @@ export const OMO_SLIM_DISABLEABLE_HOOKS = [
export const OMO_SLIM_DEFAULT_SCHEMA_URL =
"https://raw.githubusercontent.com/alvinunreal/oh-my-opencode-slim/master/assets/oh-my-opencode-slim.schema.json";
export function mergeOmoConfigPreview(
global: OmoGlobalConfig | undefined,
agents: Record<string, Record<string, unknown>>,
categories: Record<string, Record<string, unknown>> | undefined,
otherFieldsStr: string,
options?: { slim?: boolean },
): Record<string, unknown> {
const result: Record<string, unknown> = {};
const isSlim = options?.slim ?? false;
if (global) {
if (global.schemaUrl) result["$schema"] = global.schemaUrl;
if (!isSlim) {
if (global.sisyphusAgent) result["sisyphus_agent"] = global.sisyphusAgent;
}
if (global.disabledAgents?.length)
result["disabled_agents"] = global.disabledAgents;
if (global.disabledMcps?.length)
result["disabled_mcps"] = global.disabledMcps;
if (global.disabledHooks?.length)
result["disabled_hooks"] = global.disabledHooks;
if (!isSlim) {
if (global.disabledSkills?.length)
result["disabled_skills"] = global.disabledSkills;
if (global.lsp) result["lsp"] = global.lsp;
if (global.experimental) result["experimental"] = global.experimental;
if (global.backgroundTask)
result["background_task"] = global.backgroundTask;
if (global.browserAutomationEngine)
result["browser_automation_engine"] = global.browserAutomationEngine;
if (global.claudeCode) result["claude_code"] = global.claudeCode;
}
if (global.otherFields) {
for (const [k, v] of Object.entries(global.otherFields)) {
result[k] = v;
}
}
}
if (Object.keys(agents).length > 0) result["agents"] = agents;
if (!isSlim && categories && Object.keys(categories).length > 0)
result["categories"] = categories;
try {
const other = parseOmoOtherFieldsObject(otherFieldsStr);
if (other) {
for (const [k, v] of Object.entries(other)) {
result[k] = v;
}
}
} catch {}
return result;
}
/** @deprecated Use mergeOmoConfigPreview with options.slim=true */
export function mergeOmoSlimConfigPreview(
global: OmoGlobalConfig | undefined,
agents: Record<string, Record<string, unknown>>,
otherFieldsStr: string,
): Record<string, unknown> {
return mergeOmoConfigPreview(global, agents, undefined, otherFieldsStr, {
slim: true,
});
}
export function buildOmoProfilePreview(
agents: Record<string, Record<string, unknown>>,
categories: Record<string, Record<string, unknown>> | undefined,
+6 -26
View File
@@ -1,19 +1,9 @@
import { describe, expect, it } from "vitest";
import {
mergeOmoConfigPreview,
buildOmoProfilePreview,
parseOmoOtherFieldsObject,
type OmoGlobalConfig,
} from "@/types/omo";
const EMPTY_GLOBAL: OmoGlobalConfig = {
id: "global",
disabledAgents: [],
disabledMcps: [],
disabledHooks: [],
disabledSkills: [],
updatedAt: "2026-01-01T00:00:00.000Z",
};
describe("parseOmoOtherFieldsObject", () => {
it("解析对象 JSON", () => {
expect(parseOmoOtherFieldsObject('{ "foo": 1 }')).toEqual({ foo: 1 });
@@ -29,22 +19,12 @@ describe("parseOmoOtherFieldsObject", () => {
});
});
describe("mergeOmoConfigPreview", () => {
describe("buildOmoProfilePreview", () => {
it("只合并 otherFields 的对象值,忽略数组", () => {
const mergedFromArray = mergeOmoConfigPreview(
EMPTY_GLOBAL,
{},
{},
'["a", "b"]',
);
expect(mergedFromArray).toEqual({});
const fromArray = buildOmoProfilePreview({}, {}, '["a", "b"]');
expect(fromArray).toEqual({});
const mergedFromObject = mergeOmoConfigPreview(
EMPTY_GLOBAL,
{},
{},
'{ "foo": "bar" }',
);
expect(mergedFromObject).toEqual({ foo: "bar" });
const fromObject = buildOmoProfilePreview({}, {}, '{ "foo": "bar" }');
expect(fromObject).toEqual({ foo: "bar" });
});
});