mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 21:30:17 +08:00
Merge branch 'main' into feat/proxy-url-refactor-full-url-mode
# Conflicts: # src/components/providers/forms/ProviderForm.tsx
This commit is contained in:
@@ -23,3 +23,4 @@ nul
|
||||
flatpak/cc-switch.deb
|
||||
flatpak-build/
|
||||
flatpak-repo/
|
||||
.worktrees/
|
||||
|
||||
+178
-1
@@ -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
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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` |
|
||||
@@ -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` |
|
||||
@@ -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 安装和自动更新 |
|
||||
|
||||
> **注意**:由于作者没有苹果开发者账号,首次打开可能出现"未知开发者"警告,请先关闭,然后前往"系统设置" → "隐私与安全性" → 点击"仍要打开",之后便可以正常打开
|
||||
|
||||
### 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` |
|
||||
@@ -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
@@ -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": {
|
||||
|
||||
Generated
+1
-1
@@ -701,7 +701,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc-switch"
|
||||
version = "3.10.3"
|
||||
version = "3.11.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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 的存储键名
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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" />}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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/",
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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": "来源:",
|
||||
|
||||
@@ -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,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>`,
|
||||
|
||||
@@ -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 |
@@ -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 验证 JSON,Codex 暂不验证)
|
||||
*/
|
||||
export async function setCommonConfigSnippet(
|
||||
appType: AppType,
|
||||
snippet: string,
|
||||
): Promise<void> {
|
||||
return invoke("set_common_config_snippet", { appType, snippet });
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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"),
|
||||
};
|
||||
|
||||
@@ -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 });
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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" });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user