mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-28 00:35:32 +08:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ac23328119 | |||
| 0c4a8d0569 | |||
| 01cc766a05 | |||
| 3590df68b8 | |||
| 54876612b3 | |||
| 3348b39089 | |||
| 99e392518a | |||
| 201527c9d9 | |||
| 924f38ebe1 | |||
| e7766d4d22 | |||
| 3e9815f5d2 | |||
| 082cb0327d | |||
| 55e21c3c19 | |||
| 3e2c8c12a5 | |||
| 90cb8db16b | |||
| 2b30819510 | |||
| 434392a669 | |||
| 24fa8a18ef | |||
| 87604b1809 | |||
| 47435bd647 | |||
| 73ca8340bb | |||
| 31712f076f | |||
| a254304a00 | |||
| e516f83c19 | |||
| b98c3ddb29 | |||
| bb3130cbe0 | |||
| 8ea9638b9d | |||
| e1e2bdef2a | |||
| c380528a27 | |||
| a8dbea1398 | |||
| 7138aca310 | |||
| 992dda5c5c |
@@ -23,3 +23,4 @@ nul
|
|||||||
flatpak/cc-switch.deb
|
flatpak/cc-switch.deb
|
||||||
flatpak-build/
|
flatpak-build/
|
||||||
flatpak-repo/
|
flatpak-repo/
|
||||||
|
.worktrees/
|
||||||
|
|||||||
+178
-1
@@ -7,9 +7,186 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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**: Provider switching now uses partial key-field merging instead of full config overwrite, preserving user's non-provider settings (plugins, MCP, permissions). 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
|
### 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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ Claude Code / Codex / Gemini official channels at 38% / 2% / 9% of original pric
|
|||||||
**Core Capabilities**
|
**Core Capabilities**
|
||||||
|
|
||||||
- **Provider Management**: One-click switching between Claude Code, Codex, and Gemini API configurations
|
- **Provider Management**: One-click switching between Claude Code, Codex, and Gemini API configurations
|
||||||
|
- **AWS Bedrock Support**: Built-in AWS Bedrock provider presets with AKSK and API Key authentication, cross-region inference support (global/us/eu/apac), covering Claude Code and OpenCode
|
||||||
- **Speed Testing**: Measure API endpoint latency with visual quality indicators
|
- **Speed Testing**: Measure API endpoint latency with visual quality indicators
|
||||||
- **Import/Export**: Backup and restore configs with auto-rotation (keep 10 most recent)
|
- **Import/Export**: Backup and restore configs with auto-rotation (keep 10 most recent)
|
||||||
- **i18n Support**: Complete Chinese/English localization (UI, errors, tray)
|
- **i18n Support**: Complete Chinese/English localization (UI, errors, tray)
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ Claude Code / Codex / Gemini 公式チャンネルが最安で元価格の 38% /
|
|||||||
**コア機能**
|
**コア機能**
|
||||||
|
|
||||||
- **プロバイダ管理**:Claude Code、Codex、Gemini の API 設定をワンクリックで切り替え
|
- **プロバイダ管理**:Claude Code、Codex、Gemini の API 設定をワンクリックで切り替え
|
||||||
|
- **AWS Bedrock 対応**:AWS Bedrock プロバイダプリセットを内蔵、AKSK および API Key 認証に対応、クロスリージョン推論(global/us/eu/apac)をサポート、Claude Code と OpenCode に対応
|
||||||
- **速度テスト**:エンドポイント遅延を計測し、品質を可視化
|
- **速度テスト**:エンドポイント遅延を計測し、品質を可視化
|
||||||
- **インポート/エクスポート**:設定をバックアップ・復元(最新 10 件を自動ローテーション)
|
- **インポート/エクスポート**:設定をバックアップ・復元(最新 10 件を自動ローテーション)
|
||||||
- **多言語対応**:UI/エラー/トレイを含む中国語・英語・日本語ローカライズ
|
- **多言語対応**:UI/エラー/トレイを含む中国語・英語・日本語ローカライズ
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ Claude Code / Codex / Gemini 官方渠道低至 3.8 / 0.2 / 0.9 折,充值更
|
|||||||
**核心功能**
|
**核心功能**
|
||||||
|
|
||||||
- **供应商管理**:一键切换 Claude Code、Codex 与 Gemini 的 API 配置
|
- **供应商管理**:一键切换 Claude Code、Codex 与 Gemini 的 API 配置
|
||||||
|
- **AWS Bedrock 支持**:内置 AWS Bedrock 供应商预设,支持 AKSK 和 API Key 两种认证方式,支持跨区域推理(global/us/eu/apac),覆盖 Claude Code 和 OpenCode
|
||||||
- **速度测试**:测量 API 端点延迟,可视化连接质量指示器
|
- **速度测试**:测量 API 端点延迟,可视化连接质量指示器
|
||||||
- **导入导出**:备份和恢复配置,自动轮换(保留最近 10 个)
|
- **导入导出**:备份和恢复配置,自动轮换(保留最近 10 个)
|
||||||
- **国际化支持**:完整的中英文本地化(UI、错误、托盘)
|
- **国际化支持**:完整的中英文本地化(UI、错误、托盘)
|
||||||
|
|||||||
@@ -0,0 +1,291 @@
|
|||||||
|
# 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**: Provider switching now preserves user's non-provider settings
|
||||||
|
- **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 (Important Change)
|
||||||
|
|
||||||
|
Provider switching now uses partial key-field merging instead of full config overwrite (#1098). When switching providers, only provider-related key-values are updated, preserving user's non-provider settings (plugins, MCP, permissions, etc.). This refactoring removed 6 frontend files 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.
|
||||||
|
- **Partial key-field merging is an important architecture change**: Provider switching no longer overwrites the entire config file, but only merges provider-related key-values. Please note this change if you previously relied on full overwrite behavior.
|
||||||
|
- **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,291 @@
|
|||||||
|
# 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)。プロバイダー切り替え時に、プロバイダー関連のキー値のみを更新し、ユーザーの非プロバイダー設定(プラグイン設定、MCP 設定、権限設定など)を保持します。このリファクタリングにより、フロントエンドファイル6つと約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 をインストールする必要があります。
|
||||||
|
- **部分キーフィールドマージは重要なアーキテクチャ変更です**: プロバイダー切り替え時に設定ファイルを完全上書きするのではなく、プロバイダー関連のキー値のみをマージするようになりました。以前の完全上書き動作に依存していた場合はご注意ください。
|
||||||
|
- **自動インポートは手動に変更されました**: 起動時に外部設定を自動インポートしなくなりました。必要に応じて「現在の設定をインポート」を手動でクリックしてください。
|
||||||
|
- **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,291 @@
|
|||||||
|
# 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)。切换供应商时,仅更新供应商相关的键值,保留用户的非供应商设置(如插件配置、MCP 配置、权限设置等)。此次重构删除了 6 个前端文件、约 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 才能使用相关功能。
|
||||||
|
- **部分键值合并是重要架构变更**:供应商切换不再全量覆写配置文件,而是仅合并供应商相关的键值。如果你之前依赖全量覆写行为,请注意此变更。
|
||||||
|
- **自动导入已改为手动**:启动时不再自动导入外部配置,请在需要时手动点击"导入当前配置"。
|
||||||
|
- **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
|
- 文档版本:v3.11.0
|
||||||
- 最后更新:2026-02-09
|
- 最后更新:2026-02-26
|
||||||
- 适用于 CC Switch v3.10.0+
|
- 适用于 CC Switch v3.11.0+
|
||||||
|
|
||||||
## 贡献
|
## 贡献
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cc-switch",
|
"name": "cc-switch",
|
||||||
"version": "3.10.3",
|
"version": "3.11.0",
|
||||||
"description": "All-in-One Assistant for Claude Code, Codex & Gemini CLI",
|
"description": "All-in-One Assistant for Claude Code, Codex & Gemini CLI",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Generated
+1
-1
@@ -701,7 +701,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc-switch"
|
name = "cc-switch"
|
||||||
version = "3.10.3"
|
version = "3.11.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cc-switch"
|
name = "cc-switch"
|
||||||
version = "3.10.3"
|
version = "3.11.0"
|
||||||
description = "All-in-One Assistant for Claude Code, Codex & Gemini CLI"
|
description = "All-in-One Assistant for Claude Code, Codex & Gemini CLI"
|
||||||
authors = ["Jason Young"]
|
authors = ["Jason Young"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@@ -352,49 +352,6 @@ impl FromStr for AppType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 通用配置片段(按应用分治)
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
||||||
pub struct CommonConfigSnippets {
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub claude: Option<String>,
|
|
||||||
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub codex: Option<String>,
|
|
||||||
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub gemini: Option<String>,
|
|
||||||
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub opencode: Option<String>,
|
|
||||||
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub openclaw: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CommonConfigSnippets {
|
|
||||||
/// 获取指定应用的通用配置片段
|
|
||||||
pub fn get(&self, app: &AppType) -> Option<&String> {
|
|
||||||
match app {
|
|
||||||
AppType::Claude => self.claude.as_ref(),
|
|
||||||
AppType::Codex => self.codex.as_ref(),
|
|
||||||
AppType::Gemini => self.gemini.as_ref(),
|
|
||||||
AppType::OpenCode => self.opencode.as_ref(),
|
|
||||||
AppType::OpenClaw => self.openclaw.as_ref(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 设置指定应用的通用配置片段
|
|
||||||
pub fn set(&mut self, app: &AppType, snippet: Option<String>) {
|
|
||||||
match app {
|
|
||||||
AppType::Claude => self.claude = snippet,
|
|
||||||
AppType::Codex => self.codex = snippet,
|
|
||||||
AppType::Gemini => self.gemini = snippet,
|
|
||||||
AppType::OpenCode => self.opencode = snippet,
|
|
||||||
AppType::OpenClaw => self.openclaw = snippet,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 多应用配置结构(向后兼容)
|
/// 多应用配置结构(向后兼容)
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct MultiAppConfig {
|
pub struct MultiAppConfig {
|
||||||
@@ -412,12 +369,6 @@ pub struct MultiAppConfig {
|
|||||||
/// Claude Skills 配置
|
/// Claude Skills 配置
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub skills: SkillStore,
|
pub skills: SkillStore,
|
||||||
/// 通用配置片段(按应用分治)
|
|
||||||
#[serde(default)]
|
|
||||||
pub common_config_snippets: CommonConfigSnippets,
|
|
||||||
/// Claude 通用配置片段(旧字段,用于向后兼容迁移)
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub claude_common_config_snippet: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_version() -> u32 {
|
fn default_version() -> u32 {
|
||||||
@@ -439,8 +390,6 @@ impl Default for MultiAppConfig {
|
|||||||
mcp: McpRoot::default(),
|
mcp: McpRoot::default(),
|
||||||
prompts: PromptRoot::default(),
|
prompts: PromptRoot::default(),
|
||||||
skills: SkillStore::default(),
|
skills: SkillStore::default(),
|
||||||
common_config_snippets: CommonConfigSnippets::default(),
|
|
||||||
claude_common_config_snippet: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -534,15 +483,6 @@ impl MultiAppConfig {
|
|||||||
updated = true;
|
updated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 迁移通用配置片段:claude_common_config_snippet → common_config_snippets.claude
|
|
||||||
if let Some(old_claude_snippet) = config.claude_common_config_snippet.take() {
|
|
||||||
log::info!(
|
|
||||||
"迁移通用配置:claude_common_config_snippet → common_config_snippets.claude"
|
|
||||||
);
|
|
||||||
config.common_config_snippets.claude = Some(old_claude_snippet);
|
|
||||||
updated = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if updated {
|
if updated {
|
||||||
log::info!("配置结构已更新(包括 MCP 迁移或 Prompt 自动导入),保存配置...");
|
log::info!("配置结构已更新(包括 MCP 迁移或 Prompt 自动导入),保存配置...");
|
||||||
config.save()?;
|
config.save()?;
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ use tauri_plugin_opener::OpenerExt;
|
|||||||
use crate::app_config::AppType;
|
use crate::app_config::AppType;
|
||||||
use crate::codex_config;
|
use crate::codex_config;
|
||||||
use crate::config::{self, get_claude_settings_path, ConfigStatus};
|
use crate::config::{self, get_claude_settings_path, ConfigStatus};
|
||||||
use crate::settings;
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_claude_config_status() -> Result<ConfigStatus, String> {
|
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;
|
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]
|
#[tauri::command]
|
||||||
pub async fn get_config_status(app: String) -> Result<ConfigStatus, String> {
|
pub async fn get_config_status(app: String) -> Result<ConfigStatus, String> {
|
||||||
match AppType::from_str(&app).map_err(|e| e.to_string())? {
|
match AppType::from_str(&app).map_err(|e| e.to_string())? {
|
||||||
@@ -163,126 +150,3 @@ pub async fn open_app_config_folder(handle: AppHandle) -> Result<bool, String> {
|
|||||||
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn get_claude_common_config_snippet(
|
|
||||||
state: tauri::State<'_, crate::store::AppState>,
|
|
||||||
) -> Result<Option<String>, String> {
|
|
||||||
state
|
|
||||||
.db
|
|
||||||
.get_config_snippet("claude")
|
|
||||||
.map_err(|e| e.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn set_claude_common_config_snippet(
|
|
||||||
snippet: String,
|
|
||||||
state: tauri::State<'_, crate::store::AppState>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
if !snippet.trim().is_empty() {
|
|
||||||
serde_json::from_str::<serde_json::Value>(&snippet).map_err(invalid_json_format_error)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let value = if snippet.trim().is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(snippet)
|
|
||||||
};
|
|
||||||
|
|
||||||
state
|
|
||||||
.db
|
|
||||||
.set_config_snippet("claude", value)
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn extract_common_config_snippet(
|
|
||||||
appType: String,
|
|
||||||
settingsConfig: Option<String>,
|
|
||||||
state: tauri::State<'_, crate::store::AppState>,
|
|
||||||
) -> Result<String, String> {
|
|
||||||
let app = AppType::from_str(&appType).map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
if let Some(settings_config) = settingsConfig.filter(|s| !s.trim().is_empty()) {
|
|
||||||
let settings: serde_json::Value =
|
|
||||||
serde_json::from_str(&settings_config).map_err(invalid_json_format_error)?;
|
|
||||||
|
|
||||||
return crate::services::provider::ProviderService::extract_common_config_snippet_from_settings(
|
|
||||||
app,
|
|
||||||
&settings,
|
|
||||||
)
|
|
||||||
.map_err(|e| e.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
crate::services::provider::ProviderService::extract_common_config_snippet(&state, app)
|
|
||||||
.map_err(|e| e.to_string())
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -123,6 +123,24 @@ pub async fn open_zip_file_dialog<R: tauri::Runtime>(
|
|||||||
|
|
||||||
// ─── Database backup management ─────────────────────────────
|
// ─── 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
|
/// List all database backup files
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn list_db_backups() -> Result<Vec<BackupEntry>, String> {
|
pub fn list_db_backups() -> Result<Vec<BackupEntry>, String> {
|
||||||
@@ -150,3 +168,9 @@ pub fn rename_db_backup(
|
|||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
Database::rename_backup(&oldFilename, &newName).map_err(|e| e.to_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())
|
||||||
|
}
|
||||||
|
|||||||
@@ -657,6 +657,7 @@ fn scan_cli_version(tool: &str) -> (Option<String>, Option<String>) {
|
|||||||
(None, Some("not installed or not executable".to_string()))
|
(None, Some("not installed or not executable".to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
fn wsl_distro_for_tool(tool: &str) -> Option<String> {
|
fn wsl_distro_for_tool(tool: &str) -> Option<String> {
|
||||||
let override_dir = match tool {
|
let override_dir = match tool {
|
||||||
"claude" => crate::settings::get_claude_override_dir(),
|
"claude" => crate::settings::get_claude_override_dir(),
|
||||||
@@ -694,12 +695,6 @@ fn wsl_distro_from_path(path: &Path) -> Option<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 非 Windows 平台不支持 WSL 路径解析
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
fn wsl_distro_from_path(_path: &Path) -> Option<String> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 打开指定提供商的终端
|
/// 打开指定提供商的终端
|
||||||
///
|
///
|
||||||
/// 根据提供商配置的环境变量启动一个带有该提供商特定设置的终端
|
/// 根据提供商配置的环境变量启动一个带有该提供商特定设置的终端
|
||||||
|
|||||||
@@ -36,19 +36,6 @@ pub async fn disable_current_omo(state: State<'_, AppState>) -> Result<(), Strin
|
|||||||
Ok(())
|
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 ───────────────────────────────────────
|
// ── OMO Slim commands ───────────────────────────────────────
|
||||||
|
|
||||||
#[tauri::command]
|
#[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())?;
|
OmoService::delete_config_file(&SLIM).map_err(|e| e.to_string())?;
|
||||||
Ok(())
|
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)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -97,27 +97,7 @@ pub fn switch_provider(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn import_default_config_internal(state: &AppState, app_type: AppType) -> Result<bool, AppError> {
|
fn import_default_config_internal(state: &AppState, app_type: AppType) -> Result<bool, AppError> {
|
||||||
let imported = ProviderService::import_default_config(state, app_type.clone())?;
|
let imported = ProviderService::import_default_config(state, app_type)?;
|
||||||
|
|
||||||
if imported {
|
|
||||||
// Extract common config snippet (mirrors old startup logic in lib.rs)
|
|
||||||
if state
|
|
||||||
.db
|
|
||||||
.get_config_snippet(app_type.as_str())
|
|
||||||
.ok()
|
|
||||||
.flatten()
|
|
||||||
.is_none()
|
|
||||||
{
|
|
||||||
match ProviderService::extract_common_config_snippet(state, app_type.clone()) {
|
|
||||||
Ok(snippet) if !snippet.is_empty() && snippet != "{}" => {
|
|
||||||
let _ = state
|
|
||||||
.db
|
|
||||||
.set_config_snippet(app_type.as_str(), Some(snippet));
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(imported)
|
Ok(imported)
|
||||||
}
|
}
|
||||||
@@ -187,6 +167,12 @@ pub fn read_live_provider_settings(app: String) -> Result<serde_json::Value, Str
|
|||||||
ProviderService::read_live_settings(app_type).map_err(|e| e.to_string())
|
ProviderService::read_live_settings(app_type).map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn patch_claude_live_settings(patch: serde_json::Value) -> Result<bool, String> {
|
||||||
|
ProviderService::patch_claude_live(patch).map_err(|e| e.to_string())?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn test_api_endpoints(
|
pub async fn test_api_endpoints(
|
||||||
urls: Vec<String>,
|
urls: Vec<String>,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
use tauri::AppHandle;
|
||||||
|
use tauri_plugin_opener::OpenerExt;
|
||||||
|
|
||||||
use crate::config::write_text_file;
|
use crate::config::write_text_file;
|
||||||
use crate::openclaw_config::get_openclaw_dir;
|
use crate::openclaw_config::get_openclaw_dir;
|
||||||
@@ -147,6 +149,141 @@ pub async fn write_daily_memory_file(filename: String, content: String) -> Resul
|
|||||||
.map_err(|e| format!("Failed to write daily memory file {filename}: {e}"))
|
.map_err(|e| format!("Failed to write daily memory file {filename}: {e}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Find the largest index `<= i` that is a valid UTF-8 char boundary.
|
||||||
|
/// Equivalent to the unstable `str::floor_char_boundary` (stabilized in 1.91).
|
||||||
|
fn floor_char_boundary(s: &str, mut i: usize) -> usize {
|
||||||
|
if i >= s.len() {
|
||||||
|
return s.len();
|
||||||
|
}
|
||||||
|
while !s.is_char_boundary(i) {
|
||||||
|
i -= 1;
|
||||||
|
}
|
||||||
|
i
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the smallest index `>= i` that is a valid UTF-8 char boundary.
|
||||||
|
/// Equivalent to the unstable `str::ceil_char_boundary` (stabilized in 1.91).
|
||||||
|
fn ceil_char_boundary(s: &str, mut i: usize) -> usize {
|
||||||
|
if i >= s.len() {
|
||||||
|
return s.len();
|
||||||
|
}
|
||||||
|
while !s.is_char_boundary(i) {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
i
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Search result for daily memory full-text search.
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct DailyMemorySearchResult {
|
||||||
|
pub filename: String,
|
||||||
|
pub date: String,
|
||||||
|
pub size_bytes: u64,
|
||||||
|
pub modified_at: u64,
|
||||||
|
pub snippet: String,
|
||||||
|
pub match_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Full-text search across all daily memory files.
|
||||||
|
///
|
||||||
|
/// Performs case-insensitive search on both the date field and file content.
|
||||||
|
/// Returns results sorted by filename descending (newest first), each with a
|
||||||
|
/// snippet showing ~120 characters of context around the first match.
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn search_daily_memory_files(
|
||||||
|
query: String,
|
||||||
|
) -> Result<Vec<DailyMemorySearchResult>, String> {
|
||||||
|
let memory_dir = get_openclaw_dir().join("workspace").join("memory");
|
||||||
|
|
||||||
|
if !memory_dir.exists() || query.is_empty() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let query_lower = query.to_lowercase();
|
||||||
|
let mut results: Vec<DailyMemorySearchResult> = Vec::new();
|
||||||
|
|
||||||
|
let entries = std::fs::read_dir(&memory_dir)
|
||||||
|
.map_err(|e| format!("Failed to read memory directory: {e}"))?;
|
||||||
|
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let name = entry.file_name().to_string_lossy().to_string();
|
||||||
|
if !name.ends_with(".md") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let meta = match entry.metadata() {
|
||||||
|
Ok(m) if m.is_file() => m,
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
let date = name.trim_end_matches(".md").to_string();
|
||||||
|
let content = std::fs::read_to_string(entry.path()).unwrap_or_default();
|
||||||
|
let content_lower = content.to_lowercase();
|
||||||
|
|
||||||
|
// Count matches in content
|
||||||
|
let content_matches: Vec<usize> = content_lower
|
||||||
|
.match_indices(&query_lower)
|
||||||
|
.map(|(i, _)| i)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Also check date field
|
||||||
|
let date_matches = date.to_lowercase().contains(&query_lower);
|
||||||
|
|
||||||
|
if content_matches.is_empty() && !date_matches {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build snippet around first content match (~120 chars of context)
|
||||||
|
let snippet = if let Some(&first_pos) = content_matches.first() {
|
||||||
|
let start = if first_pos > 50 {
|
||||||
|
floor_char_boundary(&content, first_pos - 50)
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
let end = ceil_char_boundary(&content, (first_pos + 70).min(content.len()));
|
||||||
|
let mut s = String::new();
|
||||||
|
if start > 0 {
|
||||||
|
s.push_str("...");
|
||||||
|
}
|
||||||
|
s.push_str(&content[start..end]);
|
||||||
|
if end < content.len() {
|
||||||
|
s.push_str("...");
|
||||||
|
}
|
||||||
|
s
|
||||||
|
} else {
|
||||||
|
// Date-only match — use beginning of file as preview
|
||||||
|
let end = ceil_char_boundary(&content, 120.min(content.len()));
|
||||||
|
let mut s = content[..end].to_string();
|
||||||
|
if end < content.len() {
|
||||||
|
s.push_str("...");
|
||||||
|
}
|
||||||
|
s
|
||||||
|
};
|
||||||
|
|
||||||
|
let size_bytes = meta.len();
|
||||||
|
let modified_at = meta
|
||||||
|
.modified()
|
||||||
|
.ok()
|
||||||
|
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
|
||||||
|
.map(|d| d.as_secs())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
results.push(DailyMemorySearchResult {
|
||||||
|
filename: name,
|
||||||
|
date,
|
||||||
|
size_bytes,
|
||||||
|
modified_at,
|
||||||
|
snippet,
|
||||||
|
match_count: content_matches.len(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by filename descending (newest date first)
|
||||||
|
results.sort_by(|a, b| b.filename.cmp(&a.filename));
|
||||||
|
|
||||||
|
Ok(results)
|
||||||
|
}
|
||||||
|
|
||||||
/// Delete a daily memory file (idempotent).
|
/// Delete a daily memory file (idempotent).
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn delete_daily_memory_file(filename: String) -> Result<(), String> {
|
pub async fn delete_daily_memory_file(filename: String) -> Result<(), String> {
|
||||||
@@ -201,3 +338,25 @@ pub async fn write_workspace_file(filename: String, content: String) -> Result<(
|
|||||||
write_text_file(&path, &content)
|
write_text_file(&path, &content)
|
||||||
.map_err(|e| format!("Failed to write workspace file {filename}: {e}"))
|
.map_err(|e| format!("Failed to write workspace file {filename}: {e}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Open the workspace or memory directory in the system file manager.
|
||||||
|
/// `subdir`: "workspace" opens `~/.openclaw/workspace/`,
|
||||||
|
/// "memory" opens `~/.openclaw/workspace/memory/`.
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn open_workspace_directory(handle: AppHandle, subdir: String) -> Result<bool, String> {
|
||||||
|
let dir = match subdir.as_str() {
|
||||||
|
"memory" => get_openclaw_dir().join("workspace").join("memory"),
|
||||||
|
_ => get_openclaw_dir().join("workspace"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !dir.exists() {
|
||||||
|
std::fs::create_dir_all(&dir).map_err(|e| format!("Failed to create directory: {e}"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
handle
|
||||||
|
.opener()
|
||||||
|
.open_path(dir.to_string_lossy().to_string(), None::<String>)
|
||||||
|
.map_err(|e| format!("Failed to open directory: {e}"))?;
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
use super::{lock_conn, Database};
|
use super::{lock_conn, Database};
|
||||||
use crate::config::get_app_config_dir;
|
use crate::config::get_app_config_dir;
|
||||||
use crate::error::AppError;
|
use crate::error::AppError;
|
||||||
use chrono::Utc;
|
use chrono::{Local, Utc};
|
||||||
use rusqlite::backup::Backup;
|
use rusqlite::backup::Backup;
|
||||||
use rusqlite::types::ValueRef;
|
use rusqlite::types::ValueRef;
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
@@ -182,7 +182,7 @@ impl Database {
|
|||||||
|
|
||||||
fs::create_dir_all(&backup_dir).map_err(|e| AppError::io(&backup_dir, e))?;
|
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_id = base_id.clone();
|
||||||
let mut backup_path = backup_dir.join(format!("{backup_id}.db"));
|
let mut backup_path = backup_dir.join(format!("{backup_id}.db"));
|
||||||
let mut counter = 1;
|
let mut counter = 1;
|
||||||
@@ -531,4 +531,29 @@ impl Database {
|
|||||||
log::info!("Renamed backup: {old_filename} -> {new_filename}");
|
log::info!("Renamed backup: {old_filename} -> {new_filename}");
|
||||||
Ok(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 failover;
|
||||||
pub mod mcp;
|
pub mod mcp;
|
||||||
pub mod omo;
|
|
||||||
pub mod prompts;
|
pub mod prompts;
|
||||||
pub mod providers;
|
pub mod providers;
|
||||||
pub mod proxy;
|
pub mod proxy;
|
||||||
@@ -16,4 +15,3 @@ pub mod universal_providers;
|
|||||||
// 所有 DAO 方法都通过 Database impl 提供,无需单独导出
|
// 所有 DAO 方法都通过 Database impl 提供,无需单独导出
|
||||||
// 导出 FailoverQueueItem 供外部使用
|
// 导出 FailoverQueueItem 供外部使用
|
||||||
pub use failover::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],
|
params![app_type, category],
|
||||||
)
|
)
|
||||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
.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
|
let updated = tx
|
||||||
.execute(
|
.execute(
|
||||||
"UPDATE providers SET is_current = 1 WHERE id = ?1 AND app_type = ?2 AND category = ?3",
|
"UPDATE providers SET is_current = 1 WHERE id = ?1 AND app_type = ?2 AND category = ?3",
|
||||||
|
|||||||
@@ -38,31 +38,6 @@ impl Database {
|
|||||||
Ok(())
|
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 的存储键名
|
/// 全局代理 URL 的存储键名
|
||||||
|
|||||||
@@ -58,9 +58,6 @@ impl Database {
|
|||||||
// 4. 迁移 Skills
|
// 4. 迁移 Skills
|
||||||
Self::migrate_skills(tx, config)?;
|
Self::migrate_skills(tx, config)?;
|
||||||
|
|
||||||
// 5. 迁移 Common Config
|
|
||||||
Self::migrate_common_config(tx, config)?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,34 +209,4 @@ impl Database {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 迁移通用配置片段
|
|
||||||
fn migrate_common_config(
|
|
||||||
tx: &rusqlite::Transaction<'_>,
|
|
||||||
config: &MultiAppConfig,
|
|
||||||
) -> Result<(), AppError> {
|
|
||||||
if let Some(snippet) = &config.common_config_snippets.claude {
|
|
||||||
tx.execute(
|
|
||||||
"INSERT OR REPLACE INTO settings (key, value) VALUES (?1, ?2)",
|
|
||||||
params!["common_config_claude", snippet],
|
|
||||||
)
|
|
||||||
.map_err(|e| AppError::Database(format!("Migrate settings failed: {e}")))?;
|
|
||||||
}
|
|
||||||
if let Some(snippet) = &config.common_config_snippets.codex {
|
|
||||||
tx.execute(
|
|
||||||
"INSERT OR REPLACE INTO settings (key, value) VALUES (?1, ?2)",
|
|
||||||
params!["common_config_codex", snippet],
|
|
||||||
)
|
|
||||||
.map_err(|e| AppError::Database(format!("Migrate settings failed: {e}")))?;
|
|
||||||
}
|
|
||||||
if let Some(snippet) = &config.common_config_snippets.gemini {
|
|
||||||
tx.execute(
|
|
||||||
"INSERT OR REPLACE INTO settings (key, value) VALUES (?1, ?2)",
|
|
||||||
params!["common_config_gemini", snippet],
|
|
||||||
)
|
|
||||||
.map_err(|e| AppError::Database(format!("Migrate settings failed: {e}")))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ mod tests;
|
|||||||
|
|
||||||
// DAO 类型导出供外部使用
|
// DAO 类型导出供外部使用
|
||||||
pub use dao::FailoverQueueItem;
|
pub use dao::FailoverQueueItem;
|
||||||
pub use dao::OmoGlobalConfig;
|
|
||||||
|
|
||||||
use crate::config::get_app_config_dir;
|
use crate::config::get_app_config_dir;
|
||||||
use crate::error::AppError;
|
use crate::error::AppError;
|
||||||
|
|||||||
@@ -513,8 +513,6 @@ fn schema_dry_run_does_not_write_to_disk() {
|
|||||||
mcp: Default::default(),
|
mcp: Default::default(),
|
||||||
prompts: Default::default(),
|
prompts: Default::default(),
|
||||||
skills: Default::default(),
|
skills: Default::default(),
|
||||||
common_config_snippets: Default::default(),
|
|
||||||
claude_common_config_snippet: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Dry-run should succeed without any file I/O errors
|
// Dry-run should succeed without any file I/O errors
|
||||||
@@ -563,8 +561,6 @@ fn dry_run_validates_schema_compatibility() {
|
|||||||
mcp: Default::default(),
|
mcp: Default::default(),
|
||||||
prompts: Default::default(),
|
prompts: Default::default(),
|
||||||
skills: Default::default(),
|
skills: Default::default(),
|
||||||
common_config_snippets: Default::default(),
|
|
||||||
claude_common_config_snippet: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Dry-run should validate the full migration path
|
// Dry-run should validate the full migration path
|
||||||
|
|||||||
@@ -845,12 +845,8 @@ pub fn run() {
|
|||||||
commands::get_skills_migration_result,
|
commands::get_skills_migration_result,
|
||||||
commands::get_app_config_path,
|
commands::get_app_config_path,
|
||||||
commands::open_app_config_folder,
|
commands::open_app_config_folder,
|
||||||
commands::get_claude_common_config_snippet,
|
|
||||||
commands::set_claude_common_config_snippet,
|
|
||||||
commands::get_common_config_snippet,
|
|
||||||
commands::set_common_config_snippet,
|
|
||||||
commands::extract_common_config_snippet,
|
|
||||||
commands::read_live_provider_settings,
|
commands::read_live_provider_settings,
|
||||||
|
commands::patch_claude_live_settings,
|
||||||
commands::get_settings,
|
commands::get_settings,
|
||||||
commands::save_settings,
|
commands::save_settings,
|
||||||
commands::get_rectifier_config,
|
commands::get_rectifier_config,
|
||||||
@@ -915,9 +911,11 @@ pub fn run() {
|
|||||||
commands::save_file_dialog,
|
commands::save_file_dialog,
|
||||||
commands::open_file_dialog,
|
commands::open_file_dialog,
|
||||||
commands::open_zip_file_dialog,
|
commands::open_zip_file_dialog,
|
||||||
|
commands::create_db_backup,
|
||||||
commands::list_db_backups,
|
commands::list_db_backups,
|
||||||
commands::restore_db_backup,
|
commands::restore_db_backup,
|
||||||
commands::rename_db_backup,
|
commands::rename_db_backup,
|
||||||
|
commands::delete_db_backup,
|
||||||
commands::sync_current_providers_live,
|
commands::sync_current_providers_live,
|
||||||
// Deep link import
|
// Deep link import
|
||||||
commands::parse_deeplink,
|
commands::parse_deeplink,
|
||||||
@@ -1039,11 +1037,9 @@ pub fn run() {
|
|||||||
commands::set_window_theme,
|
commands::set_window_theme,
|
||||||
commands::read_omo_local_file,
|
commands::read_omo_local_file,
|
||||||
commands::get_current_omo_provider_id,
|
commands::get_current_omo_provider_id,
|
||||||
commands::get_omo_provider_count,
|
|
||||||
commands::disable_current_omo,
|
commands::disable_current_omo,
|
||||||
commands::read_omo_slim_local_file,
|
commands::read_omo_slim_local_file,
|
||||||
commands::get_current_omo_slim_provider_id,
|
commands::get_current_omo_slim_provider_id,
|
||||||
commands::get_omo_slim_provider_count,
|
|
||||||
commands::disable_current_omo_slim,
|
commands::disable_current_omo_slim,
|
||||||
// Workspace files (OpenClaw)
|
// Workspace files (OpenClaw)
|
||||||
commands::read_workspace_file,
|
commands::read_workspace_file,
|
||||||
@@ -1053,6 +1049,8 @@ pub fn run() {
|
|||||||
commands::read_daily_memory_file,
|
commands::read_daily_memory_file,
|
||||||
commands::write_daily_memory_file,
|
commands::write_daily_memory_file,
|
||||||
commands::delete_daily_memory_file,
|
commands::delete_daily_memory_file,
|
||||||
|
commands::search_daily_memory_files,
|
||||||
|
commands::open_workspace_directory,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let app = builder
|
let app = builder
|
||||||
|
|||||||
@@ -235,6 +235,11 @@ pub struct ProviderMeta {
|
|||||||
/// - "openai_chat": OpenAI Chat Completions 格式,需要转换
|
/// - "openai_chat": OpenAI Chat Completions 格式,需要转换
|
||||||
#[serde(rename = "apiFormat", skip_serializing_if = "Option::is_none")]
|
#[serde(rename = "apiFormat", skip_serializing_if = "Option::is_none")]
|
||||||
pub api_format: Option<String>,
|
pub api_format: Option<String>,
|
||||||
|
/// Claude 认证字段名(仅 Claude 供应商使用)
|
||||||
|
/// - "ANTHROPIC_AUTH_TOKEN" (默认): 大多数第三方/聚合供应商
|
||||||
|
/// - "ANTHROPIC_API_KEY": 少数供应商需要原生 API Key
|
||||||
|
#[serde(rename = "apiKeyField", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub api_key_field: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProviderManager {
|
impl ProviderManager {
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
use super::provider::{sanitize_claude_settings_for_live, ProviderService};
|
|
||||||
use crate::app_config::{AppType, MultiAppConfig};
|
|
||||||
use crate::error::AppError;
|
use crate::error::AppError;
|
||||||
use crate::provider::Provider;
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use serde_json::Value;
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
@@ -82,150 +78,4 @@ impl ConfigService {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 同步当前供应商到对应的 live 配置。
|
|
||||||
pub fn sync_current_providers_to_live(config: &mut MultiAppConfig) -> Result<(), AppError> {
|
|
||||||
Self::sync_current_provider_for_app(config, &AppType::Claude)?;
|
|
||||||
Self::sync_current_provider_for_app(config, &AppType::Codex)?;
|
|
||||||
Self::sync_current_provider_for_app(config, &AppType::Gemini)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sync_current_provider_for_app(
|
|
||||||
config: &mut MultiAppConfig,
|
|
||||||
app_type: &AppType,
|
|
||||||
) -> Result<(), AppError> {
|
|
||||||
let (current_id, provider) = {
|
|
||||||
let manager = match config.get_manager(app_type) {
|
|
||||||
Some(manager) => manager,
|
|
||||||
None => return Ok(()),
|
|
||||||
};
|
|
||||||
|
|
||||||
if manager.current.is_empty() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let current_id = manager.current.clone();
|
|
||||||
let provider = match manager.providers.get(¤t_id) {
|
|
||||||
Some(provider) => provider.clone(),
|
|
||||||
None => {
|
|
||||||
log::warn!(
|
|
||||||
"当前应用 {app_type:?} 的供应商 {current_id} 不存在,跳过 live 同步"
|
|
||||||
);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
(current_id, provider)
|
|
||||||
};
|
|
||||||
|
|
||||||
match app_type {
|
|
||||||
AppType::Codex => Self::sync_codex_live(config, ¤t_id, &provider)?,
|
|
||||||
AppType::Claude => Self::sync_claude_live(config, ¤t_id, &provider)?,
|
|
||||||
AppType::Gemini => Self::sync_gemini_live(config, ¤t_id, &provider)?,
|
|
||||||
AppType::OpenCode => {
|
|
||||||
// OpenCode uses additive mode, no live sync needed
|
|
||||||
// OpenCode providers are managed directly in the config file
|
|
||||||
}
|
|
||||||
AppType::OpenClaw => {
|
|
||||||
// OpenClaw uses additive mode, no live sync needed
|
|
||||||
// OpenClaw providers are managed directly in the config file
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sync_codex_live(
|
|
||||||
config: &mut MultiAppConfig,
|
|
||||||
provider_id: &str,
|
|
||||||
provider: &Provider,
|
|
||||||
) -> Result<(), AppError> {
|
|
||||||
let settings = provider.settings_config.as_object().ok_or_else(|| {
|
|
||||||
AppError::Config(format!("供应商 {provider_id} 的 Codex 配置必须是对象"))
|
|
||||||
})?;
|
|
||||||
let auth = settings.get("auth").ok_or_else(|| {
|
|
||||||
AppError::Config(format!("供应商 {provider_id} 的 Codex 配置缺少 auth 字段"))
|
|
||||||
})?;
|
|
||||||
if !auth.is_object() {
|
|
||||||
return Err(AppError::Config(format!(
|
|
||||||
"供应商 {provider_id} 的 Codex auth 配置必须是 JSON 对象"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
let cfg_text = settings.get("config").and_then(Value::as_str);
|
|
||||||
|
|
||||||
crate::codex_config::write_codex_live_atomic(auth, cfg_text)?;
|
|
||||||
// 注意:MCP 同步在 v3.7.0 中已通过 McpService 进行,不再在此调用
|
|
||||||
// sync_enabled_to_codex 使用旧的 config.mcp.codex 结构,在新架构中为空
|
|
||||||
// MCP 的启用/禁用应通过 McpService::toggle_app 进行
|
|
||||||
|
|
||||||
let cfg_text_after = crate::codex_config::read_and_validate_codex_config_text()?;
|
|
||||||
if let Some(manager) = config.get_manager_mut(&AppType::Codex) {
|
|
||||||
if let Some(target) = manager.providers.get_mut(provider_id) {
|
|
||||||
if let Some(obj) = target.settings_config.as_object_mut() {
|
|
||||||
obj.insert(
|
|
||||||
"config".to_string(),
|
|
||||||
serde_json::Value::String(cfg_text_after),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sync_claude_live(
|
|
||||||
config: &mut MultiAppConfig,
|
|
||||||
provider_id: &str,
|
|
||||||
provider: &Provider,
|
|
||||||
) -> Result<(), AppError> {
|
|
||||||
use crate::config::{read_json_file, write_json_file};
|
|
||||||
|
|
||||||
let settings_path = crate::config::get_claude_settings_path();
|
|
||||||
if let Some(parent) = settings_path.parent() {
|
|
||||||
fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let settings = sanitize_claude_settings_for_live(&provider.settings_config);
|
|
||||||
write_json_file(&settings_path, &settings)?;
|
|
||||||
|
|
||||||
let live_after = read_json_file::<serde_json::Value>(&settings_path)?;
|
|
||||||
if let Some(manager) = config.get_manager_mut(&AppType::Claude) {
|
|
||||||
if let Some(target) = manager.providers.get_mut(provider_id) {
|
|
||||||
target.settings_config = live_after;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sync_gemini_live(
|
|
||||||
config: &mut MultiAppConfig,
|
|
||||||
provider_id: &str,
|
|
||||||
provider: &Provider,
|
|
||||||
) -> Result<(), AppError> {
|
|
||||||
use crate::gemini_config::{env_to_json, read_gemini_env};
|
|
||||||
|
|
||||||
ProviderService::write_gemini_live(provider)?;
|
|
||||||
|
|
||||||
// 读回实际写入的内容并更新到配置中(包含 settings.json)
|
|
||||||
let live_after_env = read_gemini_env()?;
|
|
||||||
let settings_path = crate::gemini_config::get_gemini_settings_path();
|
|
||||||
let live_after_config = if settings_path.exists() {
|
|
||||||
crate::config::read_json_file(&settings_path)?
|
|
||||||
} else {
|
|
||||||
serde_json::json!({})
|
|
||||||
};
|
|
||||||
let mut live_after = env_to_json(&live_after_env);
|
|
||||||
if let Some(obj) = live_after.as_object_mut() {
|
|
||||||
obj.insert("config".to_string(), live_after_config);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(manager) = config.get_manager_mut(&AppType::Gemini) {
|
|
||||||
if let Some(target) = manager.providers.get_mut(provider_id) {
|
|
||||||
target.settings_config = live_after;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+44
-213
@@ -1,5 +1,4 @@
|
|||||||
use crate::config::write_json_file;
|
use crate::config::write_json_file;
|
||||||
use crate::database::OmoGlobalConfig;
|
|
||||||
use crate::error::AppError;
|
use crate::error::AppError;
|
||||||
use crate::opencode_config::get_opencode_dir;
|
use crate::opencode_config::get_opencode_dir;
|
||||||
use crate::store::AppState;
|
use crate::store::AppState;
|
||||||
@@ -13,12 +12,11 @@ pub struct OmoLocalFileData {
|
|||||||
pub agents: Option<Value>,
|
pub agents: Option<Value>,
|
||||||
pub categories: Option<Value>,
|
pub categories: Option<Value>,
|
||||||
pub other_fields: Option<Value>,
|
pub other_fields: Option<Value>,
|
||||||
pub global: OmoGlobalConfig,
|
|
||||||
pub file_path: String,
|
pub file_path: String,
|
||||||
pub last_modified: Option<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 ─────────────────────────────────────────
|
// ── Variant descriptor ─────────────────────────────────────────
|
||||||
|
|
||||||
@@ -28,9 +26,7 @@ pub struct OmoVariant {
|
|||||||
pub provider_prefix: &'static str,
|
pub provider_prefix: &'static str,
|
||||||
pub plugin_name: &'static str,
|
pub plugin_name: &'static str,
|
||||||
pub plugin_prefix: &'static str,
|
pub plugin_prefix: &'static str,
|
||||||
pub known_keys: &'static [&'static str],
|
|
||||||
pub has_categories: bool,
|
pub has_categories: bool,
|
||||||
pub config_key: &'static str,
|
|
||||||
pub label: &'static str,
|
pub label: &'static str,
|
||||||
pub import_label: &'static str,
|
pub import_label: &'static str,
|
||||||
}
|
}
|
||||||
@@ -41,23 +37,7 @@ pub const STANDARD: OmoVariant = OmoVariant {
|
|||||||
provider_prefix: "omo-",
|
provider_prefix: "omo-",
|
||||||
plugin_name: "oh-my-opencode@latest",
|
plugin_name: "oh-my-opencode@latest",
|
||||||
plugin_prefix: "oh-my-opencode",
|
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,
|
has_categories: true,
|
||||||
config_key: "common_config_omo",
|
|
||||||
label: "OMO",
|
label: "OMO",
|
||||||
import_label: "Imported",
|
import_label: "Imported",
|
||||||
};
|
};
|
||||||
@@ -68,18 +48,7 @@ pub const SLIM: OmoVariant = OmoVariant {
|
|||||||
provider_prefix: "omo-slim-",
|
provider_prefix: "omo-slim-",
|
||||||
plugin_name: "oh-my-opencode-slim@latest",
|
plugin_name: "oh-my-opencode-slim@latest",
|
||||||
plugin_prefix: "oh-my-opencode-slim",
|
plugin_prefix: "oh-my-opencode-slim",
|
||||||
known_keys: &[
|
|
||||||
"$schema",
|
|
||||||
"agents",
|
|
||||||
"sisyphus_agent",
|
|
||||||
"disabled_agents",
|
|
||||||
"disabled_mcps",
|
|
||||||
"disabled_hooks",
|
|
||||||
"lsp",
|
|
||||||
"experimental",
|
|
||||||
],
|
|
||||||
has_categories: false,
|
has_categories: false,
|
||||||
config_key: "common_config_omo_slim",
|
|
||||||
label: "OMO Slim",
|
label: "OMO Slim",
|
||||||
import_label: "Imported Slim",
|
import_label: "Imported Slim",
|
||||||
};
|
};
|
||||||
@@ -135,47 +104,6 @@ impl OmoService {
|
|||||||
other
|
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 ──────────────────────────────────────
|
// ── Merge helpers ──────────────────────────────────────
|
||||||
|
|
||||||
fn insert_opt_value(result: &mut Map<String, Value>, key: &str, value: &Option<Value>) {
|
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>) {
|
fn insert_object_entries(result: &mut Map<String, Value>, value: Option<&Value>) {
|
||||||
if let Some(Value::Object(map)) = value {
|
if let Some(Value::Object(map)) = value {
|
||||||
for (k, v) in map {
|
for (k, v) in map {
|
||||||
@@ -214,9 +133,7 @@ impl OmoService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn write_config_to_file(state: &AppState, v: &OmoVariant) -> Result<(), AppError> {
|
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 current_omo = state.db.get_current_omo_provider("opencode", v.category)?;
|
||||||
|
|
||||||
let profile_data = current_omo.as_ref().map(|p| {
|
let profile_data = current_omo.as_ref().map(|p| {
|
||||||
let agents = p.settings_config.get("agents").cloned();
|
let agents = p.settings_config.get("agents").cloned();
|
||||||
let categories = if v.has_categories {
|
let categories = if v.has_categories {
|
||||||
@@ -225,15 +142,10 @@ impl OmoService {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
let other_fields = p.settings_config.get("otherFields").cloned();
|
let other_fields = p.settings_config.get("otherFields").cloned();
|
||||||
let use_common_config = p
|
(agents, categories, other_fields)
|
||||||
.settings_config
|
|
||||||
.get("useCommonConfig")
|
|
||||||
.and_then(|val| val.as_bool())
|
|
||||||
.unwrap_or(true);
|
|
||||||
(agents, categories, other_fields, use_common_config)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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);
|
let config_path = Self::config_path(v);
|
||||||
|
|
||||||
if let Some(parent) = config_path.parent() {
|
if let Some(parent) = config_path.parent() {
|
||||||
@@ -241,56 +153,20 @@ impl OmoService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
write_json_file(&config_path, &merged)?;
|
write_json_file(&config_path, &merged)?;
|
||||||
|
|
||||||
crate::opencode_config::add_plugin(v.plugin_name)?;
|
crate::opencode_config::add_plugin(v.plugin_name)?;
|
||||||
|
|
||||||
log::info!("{} config written to {config_path:?}", v.label);
|
log::info!("{} config written to {config_path:?}", v.label);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn merge_config(
|
fn build_config(v: &OmoVariant, profile_data: Option<&OmoProfileData>) -> Value {
|
||||||
v: &OmoVariant,
|
|
||||||
global: &OmoGlobalConfig,
|
|
||||||
profile_data: Option<&OmoProfileData>,
|
|
||||||
) -> Value {
|
|
||||||
let mut result = Map::new();
|
let mut result = Map::new();
|
||||||
let use_common_config = profile_data.map(|(_, _, _, uc)| *uc).unwrap_or(true);
|
if let Some((agents, categories, other_fields)) = profile_data {
|
||||||
|
Self::insert_object_entries(&mut result, other_fields.as_ref());
|
||||||
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 {
|
|
||||||
Self::insert_opt_value(&mut result, "agents", agents);
|
Self::insert_opt_value(&mut result, "agents", agents);
|
||||||
if v.has_categories {
|
if v.has_categories {
|
||||||
Self::insert_opt_value(&mut result, "categories", categories);
|
Self::insert_opt_value(&mut result, "categories", categories);
|
||||||
}
|
}
|
||||||
Self::insert_object_entries(&mut result, other_fields.as_ref());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Value::Object(result)
|
Value::Object(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,18 +186,12 @@ impl OmoService {
|
|||||||
settings.insert("categories".to_string(), categories.clone());
|
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() {
|
if !other.is_empty() {
|
||||||
settings.insert("otherFields".to_string(), Value::Object(other));
|
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 provider_id = format!("{}{}", v.provider_prefix, uuid::Uuid::new_v4());
|
||||||
let name = format!(
|
let name = format!(
|
||||||
"{} {}",
|
"{} {}",
|
||||||
@@ -384,22 +254,17 @@ impl OmoService {
|
|||||||
None
|
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() {
|
let other_fields = if other.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(Value::Object(other))
|
Some(Value::Object(other))
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut global = OmoGlobalConfig::default();
|
|
||||||
Self::merge_global_from_obj(obj, &mut global);
|
|
||||||
global.other_fields = other_fields.clone();
|
|
||||||
|
|
||||||
OmoLocalFileData {
|
OmoLocalFileData {
|
||||||
agents,
|
agents,
|
||||||
categories,
|
categories,
|
||||||
other_fields,
|
other_fields,
|
||||||
global,
|
|
||||||
file_path,
|
file_path,
|
||||||
last_modified,
|
last_modified,
|
||||||
}
|
}
|
||||||
@@ -482,26 +347,24 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_merge_config_empty() {
|
fn test_build_config_empty() {
|
||||||
let global = OmoGlobalConfig::default();
|
let merged = OmoService::build_config(&STANDARD, None);
|
||||||
let merged = OmoService::merge_config(&STANDARD, &global, None);
|
|
||||||
assert!(merged.is_object());
|
assert!(merged.is_object());
|
||||||
|
assert!(merged.as_object().unwrap().is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_merge_config_with_profile() {
|
fn test_build_config_with_profile() {
|
||||||
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!({
|
let agents = Some(serde_json::json!({
|
||||||
"sisyphus": { "model": "claude-opus-4-5" }
|
"sisyphus": { "model": "claude-opus-4-5" }
|
||||||
}));
|
}));
|
||||||
let categories = None;
|
let categories = None;
|
||||||
let other_fields = None;
|
let other_fields = Some(serde_json::json!({
|
||||||
let profile_data = (agents, categories, other_fields, true);
|
"$schema": "https://example.com/schema.json",
|
||||||
let merged = OmoService::merge_config(&STANDARD, &global, Some(&profile_data));
|
"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();
|
let obj = merged.as_object().unwrap();
|
||||||
|
|
||||||
assert_eq!(obj["$schema"], "https://example.com/schema.json");
|
assert_eq!(obj["$schema"], "https://example.com/schema.json");
|
||||||
@@ -511,28 +374,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_merge_config_without_common_config() {
|
fn test_build_local_file_data_keeps_all_non_agent_category_fields_in_other() {
|
||||||
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() {
|
|
||||||
let obj = serde_json::json!({
|
let obj = serde_json::json!({
|
||||||
"$schema": "https://example.com/schema.json",
|
"$schema": "https://example.com/schema.json",
|
||||||
"disabled_agents": ["oracle"],
|
"disabled_agents": ["oracle"],
|
||||||
@@ -555,64 +397,53 @@ mod tests {
|
|||||||
None,
|
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!(
|
assert_eq!(
|
||||||
data.global.schema_url.as_deref(),
|
other_obj.get("$schema").unwrap(),
|
||||||
Some("https://example.com/schema.json")
|
"https://example.com/schema.json"
|
||||||
);
|
);
|
||||||
assert_eq!(data.global.disabled_agents, vec!["oracle".to_string()]);
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
data.other_fields,
|
other_obj.get("disabled_agents").unwrap(),
|
||||||
Some(serde_json::json!({
|
&serde_json::json!(["oracle"])
|
||||||
"custom_top_level": { "enabled": true }
|
|
||||||
}))
|
|
||||||
);
|
);
|
||||||
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]
|
#[test]
|
||||||
fn test_merge_config_ignores_non_object_other_fields() {
|
fn test_build_config_ignores_non_object_other_fields() {
|
||||||
let global = OmoGlobalConfig {
|
|
||||||
other_fields: Some(serde_json::json!(["global_non_object"])),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let agents = None;
|
let agents = None;
|
||||||
let categories = None;
|
let categories = None;
|
||||||
let other_fields = Some(serde_json::json!("profile_non_object"));
|
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();
|
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"));
|
assert!(!obj.contains_key("profile_non_object"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_merge_config_slim_excludes_categories_and_extra_fields() {
|
fn test_build_config_slim_excludes_categories() {
|
||||||
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()
|
|
||||||
};
|
|
||||||
let agents = Some(serde_json::json!({"orchestrator": {"model": "k2"}}));
|
let agents = Some(serde_json::json!({"orchestrator": {"model": "k2"}}));
|
||||||
let categories = Some(serde_json::json!({"code": {"model": "gpt"}}));
|
let categories = Some(serde_json::json!({"code": {"model": "gpt"}}));
|
||||||
let other_fields = None;
|
let other_fields = Some(serde_json::json!({
|
||||||
let profile_data = (agents, categories, other_fields, true);
|
"$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();
|
let obj = merged.as_object().unwrap();
|
||||||
|
|
||||||
// Slim should NOT include these
|
// Slim should NOT include categories
|
||||||
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"));
|
|
||||||
assert!(!obj.contains_key("categories"));
|
assert!(!obj.contains_key("categories"));
|
||||||
|
|
||||||
// Slim SHOULD include these
|
// Slim SHOULD include these
|
||||||
|
|||||||
@@ -237,6 +237,439 @@ pub(crate) fn write_live_snapshot(app_type: &AppType, provider: &Provider) -> Re
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Key fields definitions for partial merge
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Claude env-level key fields that belong to the provider.
|
||||||
|
/// When adding a new field here, also update backfill_claude_key_fields().
|
||||||
|
const CLAUDE_KEY_ENV_FIELDS: &[&str] = &[
|
||||||
|
// --- API auth & endpoint ---
|
||||||
|
"ANTHROPIC_BASE_URL",
|
||||||
|
"ANTHROPIC_AUTH_TOKEN",
|
||||||
|
"ANTHROPIC_API_KEY",
|
||||||
|
// --- Model selection ---
|
||||||
|
"ANTHROPIC_MODEL",
|
||||||
|
"ANTHROPIC_REASONING_MODEL",
|
||||||
|
"ANTHROPIC_SMALL_FAST_MODEL",
|
||||||
|
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
||||||
|
"ANTHROPIC_DEFAULT_SONNET_MODEL",
|
||||||
|
"ANTHROPIC_DEFAULT_OPUS_MODEL",
|
||||||
|
"CLAUDE_CODE_SUBAGENT_MODEL",
|
||||||
|
// --- AWS Bedrock ---
|
||||||
|
"CLAUDE_CODE_USE_BEDROCK",
|
||||||
|
"AWS_ACCESS_KEY_ID",
|
||||||
|
"AWS_SECRET_ACCESS_KEY",
|
||||||
|
"AWS_SESSION_TOKEN",
|
||||||
|
"AWS_REGION",
|
||||||
|
"AWS_PROFILE",
|
||||||
|
"ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION",
|
||||||
|
// --- Google Vertex AI ---
|
||||||
|
"CLAUDE_CODE_USE_VERTEX",
|
||||||
|
"ANTHROPIC_VERTEX_PROJECT_ID",
|
||||||
|
"CLOUD_ML_REGION",
|
||||||
|
// --- Microsoft Foundry ---
|
||||||
|
"CLAUDE_CODE_USE_FOUNDRY",
|
||||||
|
// --- Provider behavior ---
|
||||||
|
"CLAUDE_CODE_MAX_OUTPUT_TOKENS",
|
||||||
|
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC",
|
||||||
|
"API_TIMEOUT_MS",
|
||||||
|
"DISABLE_PROMPT_CACHING",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Claude top-level key fields (legacy + modern format).
|
||||||
|
/// When adding a new field here, also update backfill_claude_key_fields().
|
||||||
|
const CLAUDE_KEY_TOP_LEVEL: &[&str] = &[
|
||||||
|
"apiBaseUrl", // legacy
|
||||||
|
"primaryModel", // legacy
|
||||||
|
"smallFastModel", // legacy
|
||||||
|
"model", // modern
|
||||||
|
"apiKey", // Bedrock API Key auth
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Codex TOML key fields.
|
||||||
|
/// When adding a new field here, also update backfill_codex_key_fields().
|
||||||
|
const CODEX_KEY_TOP_LEVEL: &[&str] = &[
|
||||||
|
"model_provider",
|
||||||
|
"model",
|
||||||
|
"model_reasoning_effort",
|
||||||
|
"review_model",
|
||||||
|
"plan_mode_reasoning_effort",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Gemini env-level key fields.
|
||||||
|
/// When adding a new field here, also update backfill_gemini_key_fields().
|
||||||
|
const GEMINI_KEY_ENV_FIELDS: &[&str] = &[
|
||||||
|
"GOOGLE_GEMINI_BASE_URL",
|
||||||
|
"GEMINI_API_KEY",
|
||||||
|
"GEMINI_MODEL",
|
||||||
|
"GOOGLE_API_KEY",
|
||||||
|
];
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Partial merge: write only key fields to live config
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Write only provider-specific key fields to live configuration,
|
||||||
|
/// preserving all other user settings in the live file.
|
||||||
|
///
|
||||||
|
/// Used for switch-mode apps (Claude, Codex, Gemini) during:
|
||||||
|
/// - `switch_normal()` — switching providers
|
||||||
|
/// - `sync_current_to_live()` — startup sync
|
||||||
|
/// - `add()` / `update()` when the provider is current
|
||||||
|
pub(crate) fn write_live_partial(app_type: &AppType, provider: &Provider) -> Result<(), AppError> {
|
||||||
|
match app_type {
|
||||||
|
AppType::Claude => write_claude_live_partial(provider),
|
||||||
|
AppType::Codex => write_codex_live_partial(provider),
|
||||||
|
AppType::Gemini => write_gemini_live_partial(provider),
|
||||||
|
// Additive mode apps still use full snapshot
|
||||||
|
AppType::OpenCode | AppType::OpenClaw => write_live_snapshot(app_type, provider),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply a JSON merge patch (RFC 7396) directly to Claude live settings.json.
|
||||||
|
/// Used for user-level preferences (attribution, thinking, etc.) that are
|
||||||
|
/// independent of the active provider.
|
||||||
|
pub fn patch_claude_live(patch: Value) -> Result<(), AppError> {
|
||||||
|
let path = get_claude_settings_path();
|
||||||
|
let mut live = if path.exists() {
|
||||||
|
read_json_file(&path).unwrap_or_else(|_| json!({}))
|
||||||
|
} else {
|
||||||
|
json!({})
|
||||||
|
};
|
||||||
|
json_merge_patch(&mut live, &patch);
|
||||||
|
let settings = sanitize_claude_settings_for_live(&live);
|
||||||
|
write_json_file(&path, &settings)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RFC 7396 JSON Merge Patch: null deletes, objects merge recursively, rest overwrites.
|
||||||
|
fn json_merge_patch(target: &mut Value, patch: &Value) {
|
||||||
|
if let Some(patch_obj) = patch.as_object() {
|
||||||
|
if !target.is_object() {
|
||||||
|
*target = json!({});
|
||||||
|
}
|
||||||
|
let target_obj = target.as_object_mut().unwrap();
|
||||||
|
for (key, value) in patch_obj {
|
||||||
|
if value.is_null() {
|
||||||
|
target_obj.remove(key);
|
||||||
|
} else if value.is_object() {
|
||||||
|
let entry = target_obj.entry(key.clone()).or_insert(json!({}));
|
||||||
|
json_merge_patch(entry, value);
|
||||||
|
// Clean up empty container objects
|
||||||
|
if entry.as_object().is_some_and(|o| o.is_empty()) {
|
||||||
|
target_obj.remove(key);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
target_obj.insert(key.clone(), value.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Claude: merge only key env and top-level fields into live settings.json
|
||||||
|
fn write_claude_live_partial(provider: &Provider) -> Result<(), AppError> {
|
||||||
|
let path = get_claude_settings_path();
|
||||||
|
|
||||||
|
// 1. Read existing live config (start from empty if file doesn't exist)
|
||||||
|
let mut live = if path.exists() {
|
||||||
|
read_json_file(&path).unwrap_or_else(|_| json!({}))
|
||||||
|
} else {
|
||||||
|
json!({})
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. Ensure live.env exists as an object
|
||||||
|
if !live.get("env").is_some_and(|v| v.is_object()) {
|
||||||
|
live.as_object_mut()
|
||||||
|
.unwrap()
|
||||||
|
.insert("env".into(), json!({}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Clear key env fields from live, then write from provider
|
||||||
|
let live_env = live.get_mut("env").unwrap().as_object_mut().unwrap();
|
||||||
|
for key in CLAUDE_KEY_ENV_FIELDS {
|
||||||
|
live_env.remove(*key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(provider_env) = provider
|
||||||
|
.settings_config
|
||||||
|
.get("env")
|
||||||
|
.and_then(|v| v.as_object())
|
||||||
|
{
|
||||||
|
for key in CLAUDE_KEY_ENV_FIELDS {
|
||||||
|
if let Some(value) = provider_env.get(*key) {
|
||||||
|
live_env.insert(key.to_string(), value.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Handle top-level legacy key fields
|
||||||
|
let live_obj = live.as_object_mut().unwrap();
|
||||||
|
for key in CLAUDE_KEY_TOP_LEVEL {
|
||||||
|
live_obj.remove(*key);
|
||||||
|
}
|
||||||
|
if let Some(provider_obj) = provider.settings_config.as_object() {
|
||||||
|
for key in CLAUDE_KEY_TOP_LEVEL {
|
||||||
|
if let Some(value) = provider_obj.get(*key) {
|
||||||
|
live_obj.insert(key.to_string(), value.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Sanitize and write
|
||||||
|
let settings = sanitize_claude_settings_for_live(&live);
|
||||||
|
write_json_file(&path, &settings)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Codex: replace auth.json entirely, partially merge config.toml key fields
|
||||||
|
fn write_codex_live_partial(provider: &Provider) -> Result<(), AppError> {
|
||||||
|
let obj = provider
|
||||||
|
.settings_config
|
||||||
|
.as_object()
|
||||||
|
.ok_or_else(|| AppError::Config("Codex 供应商配置必须是 JSON 对象".to_string()))?;
|
||||||
|
|
||||||
|
// auth.json is entirely provider-specific, replace it wholesale
|
||||||
|
let auth = obj
|
||||||
|
.get("auth")
|
||||||
|
.ok_or_else(|| AppError::Config("Codex 供应商配置缺少 'auth' 字段".to_string()))?;
|
||||||
|
|
||||||
|
let provider_config_str = obj.get("config").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
|
|
||||||
|
// Read existing config.toml (or start from empty)
|
||||||
|
let config_path = get_codex_config_path();
|
||||||
|
let existing_toml = if config_path.exists() {
|
||||||
|
std::fs::read_to_string(&config_path).unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse both existing and provider TOML
|
||||||
|
let mut live_doc = existing_toml
|
||||||
|
.parse::<toml_edit::DocumentMut>()
|
||||||
|
.unwrap_or_else(|_| toml_edit::DocumentMut::new());
|
||||||
|
|
||||||
|
// Remove key fields from live doc
|
||||||
|
let live_root = live_doc.as_table_mut();
|
||||||
|
for key in CODEX_KEY_TOP_LEVEL {
|
||||||
|
live_root.remove(key);
|
||||||
|
}
|
||||||
|
live_root.remove("model_providers");
|
||||||
|
|
||||||
|
// Parse provider TOML and extract key fields
|
||||||
|
if !provider_config_str.is_empty() {
|
||||||
|
if let Ok(provider_doc) = provider_config_str.parse::<toml_edit::DocumentMut>() {
|
||||||
|
let provider_root = provider_doc.as_table();
|
||||||
|
|
||||||
|
// Copy key top-level fields from provider
|
||||||
|
for key in CODEX_KEY_TOP_LEVEL {
|
||||||
|
if let Some(item) = provider_root.get(key) {
|
||||||
|
live_root.insert(key, item.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy model_providers table from provider
|
||||||
|
if let Some(mp) = provider_root.get("model_providers") {
|
||||||
|
live_root.insert("model_providers", mp.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write using atomic write
|
||||||
|
crate::codex_config::write_codex_live_atomic(auth, Some(&live_doc.to_string()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gemini: merge only key env fields, preserve settings.json (MCP etc.)
|
||||||
|
fn write_gemini_live_partial(provider: &Provider) -> Result<(), AppError> {
|
||||||
|
use crate::gemini_config::{get_gemini_env_path, read_gemini_env, write_gemini_env_atomic};
|
||||||
|
|
||||||
|
let auth_type = detect_gemini_auth_type(provider);
|
||||||
|
|
||||||
|
// 1. Read existing env from live .env file
|
||||||
|
let mut env_map = if get_gemini_env_path().exists() {
|
||||||
|
read_gemini_env().unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
HashMap::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. Remove key fields from existing env
|
||||||
|
for key in GEMINI_KEY_ENV_FIELDS {
|
||||||
|
env_map.remove(*key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Extract key fields from provider and merge
|
||||||
|
if let Some(provider_env) = provider
|
||||||
|
.settings_config
|
||||||
|
.get("env")
|
||||||
|
.and_then(|v| v.as_object())
|
||||||
|
{
|
||||||
|
for key in GEMINI_KEY_ENV_FIELDS {
|
||||||
|
if let Some(value) = provider_env.get(*key).and_then(|v| v.as_str()) {
|
||||||
|
if !value.is_empty() {
|
||||||
|
env_map.insert(key.to_string(), value.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Handle auth type specific behavior
|
||||||
|
match auth_type {
|
||||||
|
GeminiAuthType::GoogleOfficial => {
|
||||||
|
// Google official uses OAuth, clear all env
|
||||||
|
env_map.clear();
|
||||||
|
write_gemini_env_atomic(&env_map)?;
|
||||||
|
}
|
||||||
|
GeminiAuthType::Packycode | GeminiAuthType::Generic => {
|
||||||
|
// Validate and write env
|
||||||
|
crate::gemini_config::validate_gemini_settings_strict(&provider.settings_config)?;
|
||||||
|
write_gemini_env_atomic(&env_map)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Handle settings.json (same as write_gemini_live — preserve existing MCP etc.)
|
||||||
|
use crate::gemini_config::get_gemini_settings_path;
|
||||||
|
let settings_path = get_gemini_settings_path();
|
||||||
|
|
||||||
|
if let Some(config_value) = provider.settings_config.get("config") {
|
||||||
|
if config_value.is_object() {
|
||||||
|
let mut merged = if settings_path.exists() {
|
||||||
|
read_json_file::<Value>(&settings_path).unwrap_or_else(|_| json!({}))
|
||||||
|
} else {
|
||||||
|
json!({})
|
||||||
|
};
|
||||||
|
if let (Some(merged_obj), Some(config_obj)) =
|
||||||
|
(merged.as_object_mut(), config_value.as_object())
|
||||||
|
{
|
||||||
|
for (k, v) in config_obj {
|
||||||
|
merged_obj.insert(k.clone(), v.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
write_json_file(&settings_path, &merged)?;
|
||||||
|
} else if !config_value.is_null() {
|
||||||
|
return Err(AppError::localized(
|
||||||
|
"gemini.validation.invalid_config",
|
||||||
|
"Gemini 配置格式错误: config 必须是对象或 null",
|
||||||
|
"Gemini config invalid: config must be an object or null",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Set security flag based on auth type
|
||||||
|
match auth_type {
|
||||||
|
GeminiAuthType::GoogleOfficial => ensure_google_oauth_security_flag(provider)?,
|
||||||
|
GeminiAuthType::Packycode | GeminiAuthType::Generic => {
|
||||||
|
crate::gemini_config::write_packycode_settings()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Backfill: extract only key fields from live config
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Extract only provider-specific key fields from a live config value.
|
||||||
|
///
|
||||||
|
/// Used during backfill to ensure the provider's `settings_config` converges
|
||||||
|
/// to containing only key fields over time.
|
||||||
|
pub(crate) fn backfill_key_fields(app_type: &AppType, live_config: &Value) -> Value {
|
||||||
|
match app_type {
|
||||||
|
AppType::Claude => backfill_claude_key_fields(live_config),
|
||||||
|
AppType::Codex => backfill_codex_key_fields(live_config),
|
||||||
|
AppType::Gemini => backfill_gemini_key_fields(live_config),
|
||||||
|
// Additive mode: return full config (no backfill needed)
|
||||||
|
_ => live_config.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn backfill_claude_key_fields(live: &Value) -> Value {
|
||||||
|
let mut result = json!({});
|
||||||
|
let result_obj = result.as_object_mut().unwrap();
|
||||||
|
|
||||||
|
// Extract key env fields
|
||||||
|
if let Some(live_env) = live.get("env").and_then(|v| v.as_object()) {
|
||||||
|
let mut env_obj = serde_json::Map::new();
|
||||||
|
for key in CLAUDE_KEY_ENV_FIELDS {
|
||||||
|
if let Some(value) = live_env.get(*key) {
|
||||||
|
env_obj.insert(key.to_string(), value.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !env_obj.is_empty() {
|
||||||
|
result_obj.insert("env".to_string(), Value::Object(env_obj));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract key top-level fields
|
||||||
|
if let Some(live_obj) = live.as_object() {
|
||||||
|
for key in CLAUDE_KEY_TOP_LEVEL {
|
||||||
|
if let Some(value) = live_obj.get(*key) {
|
||||||
|
result_obj.insert(key.to_string(), value.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn backfill_codex_key_fields(live: &Value) -> Value {
|
||||||
|
let mut result = json!({});
|
||||||
|
let result_obj = result.as_object_mut().unwrap();
|
||||||
|
|
||||||
|
// auth is entirely provider-specific — keep it as-is
|
||||||
|
if let Some(auth) = live.get("auth") {
|
||||||
|
result_obj.insert("auth".to_string(), auth.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract key TOML fields from config string
|
||||||
|
if let Some(config_str) = live.get("config").and_then(|v| v.as_str()) {
|
||||||
|
if let Ok(doc) = config_str.parse::<toml_edit::DocumentMut>() {
|
||||||
|
let mut new_doc = toml_edit::DocumentMut::new();
|
||||||
|
let new_root = new_doc.as_table_mut();
|
||||||
|
|
||||||
|
// Copy key top-level fields
|
||||||
|
for key in CODEX_KEY_TOP_LEVEL {
|
||||||
|
if let Some(item) = doc.as_table().get(key) {
|
||||||
|
new_root.insert(key, item.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy model_providers table
|
||||||
|
if let Some(mp) = doc.as_table().get("model_providers") {
|
||||||
|
new_root.insert("model_providers", mp.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let toml_str = new_doc.to_string();
|
||||||
|
if !toml_str.trim().is_empty() {
|
||||||
|
result_obj.insert("config".to_string(), Value::String(toml_str));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn backfill_gemini_key_fields(live: &Value) -> Value {
|
||||||
|
let mut result = json!({});
|
||||||
|
let result_obj = result.as_object_mut().unwrap();
|
||||||
|
|
||||||
|
// Extract key env fields
|
||||||
|
if let Some(live_env) = live.get("env").and_then(|v| v.as_object()) {
|
||||||
|
let mut env_obj = serde_json::Map::new();
|
||||||
|
for key in GEMINI_KEY_ENV_FIELDS {
|
||||||
|
if let Some(value) = live_env.get(*key) {
|
||||||
|
env_obj.insert(key.to_string(), value.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !env_obj.is_empty() {
|
||||||
|
result_obj.insert("env".to_string(), Value::Object(env_obj));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
/// Sync all providers to live configuration (for additive mode apps)
|
/// Sync all providers to live configuration (for additive mode apps)
|
||||||
///
|
///
|
||||||
/// Writes all providers from the database to the live configuration file.
|
/// Writes all providers from the database to the live configuration file.
|
||||||
@@ -286,7 +719,7 @@ pub fn sync_current_to_live(state: &AppState) -> Result<(), AppError> {
|
|||||||
|
|
||||||
let providers = state.db.get_all_providers(app_type.as_str())?;
|
let providers = state.db.get_all_providers(app_type.as_str())?;
|
||||||
if let Some(provider) = providers.get(¤t_id) {
|
if let Some(provider) = providers.get(¤t_id) {
|
||||||
write_live_snapshot(&app_type, provider)?;
|
write_live_partial(&app_type, provider)?;
|
||||||
}
|
}
|
||||||
// Note: get_effective_current_provider already validates existence,
|
// Note: get_effective_current_provider already validates existence,
|
||||||
// so providers.get() should always succeed here
|
// so providers.get() should always succeed here
|
||||||
|
|||||||
@@ -27,11 +27,12 @@ pub use live::{
|
|||||||
|
|
||||||
// Internal re-exports (pub(crate))
|
// Internal re-exports (pub(crate))
|
||||||
pub(crate) use live::sanitize_claude_settings_for_live;
|
pub(crate) use live::sanitize_claude_settings_for_live;
|
||||||
pub(crate) use live::write_live_snapshot;
|
pub(crate) use live::write_live_partial;
|
||||||
|
|
||||||
// Internal re-exports
|
// Internal re-exports
|
||||||
use live::{
|
use live::{
|
||||||
remove_openclaw_provider_from_live, remove_opencode_provider_from_live, write_gemini_live,
|
backfill_key_fields, remove_openclaw_provider_from_live, remove_opencode_provider_from_live,
|
||||||
|
write_live_snapshot,
|
||||||
};
|
};
|
||||||
use usage::validate_usage_script;
|
use usage::validate_usage_script;
|
||||||
|
|
||||||
@@ -84,47 +85,6 @@ mod tests {
|
|||||||
assert_eq!(api_key, "token");
|
assert_eq!(api_key, "token");
|
||||||
assert_eq!(base_url, "https://claude.example");
|
assert_eq!(base_url, "https://claude.example");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn extract_codex_common_config_preserves_mcp_servers_base_url() {
|
|
||||||
let config_toml = r#"model_provider = "azure"
|
|
||||||
model = "gpt-4"
|
|
||||||
disable_response_storage = true
|
|
||||||
|
|
||||||
[model_providers.azure]
|
|
||||||
name = "Azure OpenAI"
|
|
||||||
base_url = "https://azure.example/v1"
|
|
||||||
wire_api = "responses"
|
|
||||||
|
|
||||||
[mcp_servers.my_server]
|
|
||||||
base_url = "http://localhost:8080"
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let settings = json!({ "config": config_toml });
|
|
||||||
let extracted = ProviderService::extract_codex_common_config(&settings)
|
|
||||||
.expect("extract_codex_common_config should succeed");
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
!extracted
|
|
||||||
.lines()
|
|
||||||
.any(|line| line.trim_start().starts_with("model_provider")),
|
|
||||||
"should remove top-level model_provider"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
!extracted
|
|
||||||
.lines()
|
|
||||||
.any(|line| line.trim_start().starts_with("model =")),
|
|
||||||
"should remove top-level model"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
!extracted.contains("[model_providers"),
|
|
||||||
"should remove entire model_providers table"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
extracted.contains("http://localhost:8080"),
|
|
||||||
"should keep mcp_servers.* base_url"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProviderService {
|
impl ProviderService {
|
||||||
@@ -173,10 +133,11 @@ impl ProviderService {
|
|||||||
|
|
||||||
// Additive mode apps (OpenCode, OpenClaw) - always write to live config
|
// Additive mode apps (OpenCode, OpenClaw) - always write to live config
|
||||||
if app_type.is_additive_mode() {
|
if app_type.is_additive_mode() {
|
||||||
// OMO providers use exclusive mode and write to dedicated config file.
|
// OMO / OMO Slim providers use exclusive mode and write to dedicated config file.
|
||||||
if matches!(app_type, AppType::OpenCode) && provider.category.as_deref() == Some("omo")
|
if matches!(app_type, AppType::OpenCode)
|
||||||
|
&& matches!(provider.category.as_deref(), Some("omo") | Some("omo-slim"))
|
||||||
{
|
{
|
||||||
// Do not auto-enable newly added OMO providers.
|
// Do not auto-enable newly added OMO / OMO Slim providers.
|
||||||
// Users must explicitly switch/apply an OMO provider to activate it.
|
// Users must explicitly switch/apply an OMO provider to activate it.
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
@@ -191,7 +152,7 @@ impl ProviderService {
|
|||||||
state
|
state
|
||||||
.db
|
.db
|
||||||
.set_current_provider(app_type.as_str(), &provider.id)?;
|
.set_current_provider(app_type.as_str(), &provider.id)?;
|
||||||
write_live_snapshot(&app_type, &provider)?;
|
write_live_partial(&app_type, &provider)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
@@ -272,7 +233,7 @@ impl ProviderService {
|
|||||||
)
|
)
|
||||||
.map_err(|e| AppError::Message(format!("更新 Live 备份失败: {e}")))?;
|
.map_err(|e| AppError::Message(format!("更新 Live 备份失败: {e}")))?;
|
||||||
} else {
|
} else {
|
||||||
write_live_snapshot(&app_type, &provider)?;
|
write_live_partial(&app_type, &provider)?;
|
||||||
// Sync MCP
|
// Sync MCP
|
||||||
McpService::sync_all_enabled(state)?;
|
McpService::sync_all_enabled(state)?;
|
||||||
}
|
}
|
||||||
@@ -299,18 +260,6 @@ impl ProviderService {
|
|||||||
state
|
state
|
||||||
.db
|
.db
|
||||||
.is_omo_provider_current(app_type.as_str(), id, "omo")?;
|
.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)?;
|
state.db.delete_provider(app_type.as_str(), id)?;
|
||||||
if was_current {
|
if was_current {
|
||||||
@@ -326,18 +275,6 @@ impl ProviderService {
|
|||||||
state
|
state
|
||||||
.db
|
.db
|
||||||
.is_omo_provider_current(app_type.as_str(), id, "omo-slim")?;
|
.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)?;
|
state.db.delete_provider(app_type.as_str(), id)?;
|
||||||
if was_current {
|
if was_current {
|
||||||
@@ -552,6 +489,8 @@ impl ProviderService {
|
|||||||
state,
|
state,
|
||||||
&crate::services::omo::STANDARD,
|
&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());
|
return Ok(SwitchResult::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -561,6 +500,9 @@ impl ProviderService {
|
|||||||
.db
|
.db
|
||||||
.set_omo_provider_current(app_type.as_str(), id, "omo-slim")?;
|
.set_omo_provider_current(app_type.as_str(), id, "omo-slim")?;
|
||||||
crate::services::OmoService::write_config_to_file(state, &crate::services::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());
|
return Ok(SwitchResult::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -578,7 +520,9 @@ impl ProviderService {
|
|||||||
// Only backfill when switching to a different provider
|
// Only backfill when switching to a different provider
|
||||||
if let Ok(live_config) = read_live_settings(app_type.clone()) {
|
if let Ok(live_config) = read_live_settings(app_type.clone()) {
|
||||||
if let Some(mut current_provider) = providers.get(¤t_id).cloned() {
|
if let Some(mut current_provider) = providers.get(¤t_id).cloned() {
|
||||||
current_provider.settings_config = live_config;
|
// Only extract key fields from live config for backfill
|
||||||
|
current_provider.settings_config =
|
||||||
|
backfill_key_fields(&app_type, &live_config);
|
||||||
if let Err(e) =
|
if let Err(e) =
|
||||||
state.db.save_provider(app_type.as_str(), ¤t_provider)
|
state.db.save_provider(app_type.as_str(), ¤t_provider)
|
||||||
{
|
{
|
||||||
@@ -602,11 +546,8 @@ impl ProviderService {
|
|||||||
state.db.set_current_provider(app_type.as_str(), id)?;
|
state.db.set_current_provider(app_type.as_str(), id)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync to live (write_gemini_live handles security flag internally for Gemini)
|
// Sync to live (partial merge: only key fields, preserving user settings)
|
||||||
write_live_snapshot(&app_type, provider)?;
|
write_live_partial(&app_type, provider)?;
|
||||||
|
|
||||||
// Sync MCP
|
|
||||||
McpService::sync_all_enabled(state)?;
|
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
@@ -616,222 +557,6 @@ impl ProviderService {
|
|||||||
sync_current_to_live(state)
|
sync_current_to_live(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract common config snippet from current provider
|
|
||||||
///
|
|
||||||
/// Extracts the current provider's configuration and removes provider-specific fields
|
|
||||||
/// (API keys, model settings, endpoints) to create a reusable common config snippet.
|
|
||||||
pub fn extract_common_config_snippet(
|
|
||||||
state: &AppState,
|
|
||||||
app_type: AppType,
|
|
||||||
) -> Result<String, AppError> {
|
|
||||||
// Get current provider
|
|
||||||
let current_id = Self::current(state, app_type.clone())?;
|
|
||||||
if current_id.is_empty() {
|
|
||||||
return Err(AppError::Message("No current provider".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let providers = state.db.get_all_providers(app_type.as_str())?;
|
|
||||||
let provider = providers
|
|
||||||
.get(¤t_id)
|
|
||||||
.ok_or_else(|| AppError::Message(format!("Provider {current_id} not found")))?;
|
|
||||||
|
|
||||||
match app_type {
|
|
||||||
AppType::Claude => Self::extract_claude_common_config(&provider.settings_config),
|
|
||||||
AppType::Codex => Self::extract_codex_common_config(&provider.settings_config),
|
|
||||||
AppType::Gemini => Self::extract_gemini_common_config(&provider.settings_config),
|
|
||||||
AppType::OpenCode => Self::extract_opencode_common_config(&provider.settings_config),
|
|
||||||
AppType::OpenClaw => Self::extract_openclaw_common_config(&provider.settings_config),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extract common config snippet from a config value (e.g. editor content).
|
|
||||||
pub fn extract_common_config_snippet_from_settings(
|
|
||||||
app_type: AppType,
|
|
||||||
settings_config: &Value,
|
|
||||||
) -> Result<String, AppError> {
|
|
||||||
match app_type {
|
|
||||||
AppType::Claude => Self::extract_claude_common_config(settings_config),
|
|
||||||
AppType::Codex => Self::extract_codex_common_config(settings_config),
|
|
||||||
AppType::Gemini => Self::extract_gemini_common_config(settings_config),
|
|
||||||
AppType::OpenCode => Self::extract_opencode_common_config(settings_config),
|
|
||||||
AppType::OpenClaw => Self::extract_openclaw_common_config(settings_config),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extract common config for Claude (JSON format)
|
|
||||||
fn extract_claude_common_config(settings: &Value) -> Result<String, AppError> {
|
|
||||||
let mut config = settings.clone();
|
|
||||||
|
|
||||||
// Fields to exclude from common config
|
|
||||||
const ENV_EXCLUDES: &[&str] = &[
|
|
||||||
// Auth
|
|
||||||
"ANTHROPIC_API_KEY",
|
|
||||||
"ANTHROPIC_AUTH_TOKEN",
|
|
||||||
// Models (5 fields)
|
|
||||||
"ANTHROPIC_MODEL",
|
|
||||||
"ANTHROPIC_REASONING_MODEL",
|
|
||||||
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
|
||||||
"ANTHROPIC_DEFAULT_OPUS_MODEL",
|
|
||||||
"ANTHROPIC_DEFAULT_SONNET_MODEL",
|
|
||||||
// Endpoint
|
|
||||||
"ANTHROPIC_BASE_URL",
|
|
||||||
];
|
|
||||||
|
|
||||||
const TOP_LEVEL_EXCLUDES: &[&str] = &[
|
|
||||||
"apiBaseUrl",
|
|
||||||
// Legacy model fields
|
|
||||||
"primaryModel",
|
|
||||||
"smallFastModel",
|
|
||||||
];
|
|
||||||
|
|
||||||
// Remove env fields
|
|
||||||
if let Some(env) = config.get_mut("env").and_then(|v| v.as_object_mut()) {
|
|
||||||
for key in ENV_EXCLUDES {
|
|
||||||
env.remove(*key);
|
|
||||||
}
|
|
||||||
// If env is empty after removal, remove the env object itself
|
|
||||||
if env.is_empty() {
|
|
||||||
config.as_object_mut().map(|obj| obj.remove("env"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove top-level fields
|
|
||||||
if let Some(obj) = config.as_object_mut() {
|
|
||||||
for key in TOP_LEVEL_EXCLUDES {
|
|
||||||
obj.remove(*key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if result is empty
|
|
||||||
if config.as_object().is_none_or(|obj| obj.is_empty()) {
|
|
||||||
return Ok("{}".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
serde_json::to_string_pretty(&config)
|
|
||||||
.map_err(|e| AppError::Message(format!("Serialization failed: {e}")))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extract common config for Codex (TOML format)
|
|
||||||
fn extract_codex_common_config(settings: &Value) -> Result<String, AppError> {
|
|
||||||
// Codex config is stored as { "auth": {...}, "config": "toml string" }
|
|
||||||
let config_toml = settings
|
|
||||||
.get("config")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.unwrap_or("");
|
|
||||||
|
|
||||||
if config_toml.is_empty() {
|
|
||||||
return Ok(String::new());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut doc = config_toml
|
|
||||||
.parse::<toml_edit::DocumentMut>()
|
|
||||||
.map_err(|e| AppError::Message(format!("TOML parse error: {e}")))?;
|
|
||||||
|
|
||||||
// Remove provider-specific fields.
|
|
||||||
let root = doc.as_table_mut();
|
|
||||||
root.remove("model");
|
|
||||||
root.remove("model_provider");
|
|
||||||
// Legacy/alt formats might use a top-level base_url.
|
|
||||||
root.remove("base_url");
|
|
||||||
|
|
||||||
// Remove entire model_providers table (provider-specific configuration)
|
|
||||||
root.remove("model_providers");
|
|
||||||
|
|
||||||
// Clean up multiple empty lines (keep at most one blank line).
|
|
||||||
let mut cleaned = String::new();
|
|
||||||
let mut blank_run = 0usize;
|
|
||||||
for line in doc.to_string().lines() {
|
|
||||||
if line.trim().is_empty() {
|
|
||||||
blank_run += 1;
|
|
||||||
if blank_run <= 1 {
|
|
||||||
cleaned.push('\n');
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
blank_run = 0;
|
|
||||||
cleaned.push_str(line);
|
|
||||||
cleaned.push('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(cleaned.trim().to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extract common config for Gemini (JSON format)
|
|
||||||
///
|
|
||||||
/// Extracts `.env` values while excluding provider-specific credentials:
|
|
||||||
/// - GOOGLE_GEMINI_BASE_URL
|
|
||||||
/// - GEMINI_API_KEY
|
|
||||||
fn extract_gemini_common_config(settings: &Value) -> Result<String, AppError> {
|
|
||||||
let env = settings.get("env").and_then(|v| v.as_object());
|
|
||||||
|
|
||||||
let mut snippet = serde_json::Map::new();
|
|
||||||
if let Some(env) = env {
|
|
||||||
for (key, value) in env {
|
|
||||||
if key == "GOOGLE_GEMINI_BASE_URL" || key == "GEMINI_API_KEY" {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let Value::String(v) = value else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let trimmed = v.trim();
|
|
||||||
if !trimmed.is_empty() {
|
|
||||||
snippet.insert(key.to_string(), Value::String(trimmed.to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if snippet.is_empty() {
|
|
||||||
return Ok("{}".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
serde_json::to_string_pretty(&Value::Object(snippet))
|
|
||||||
.map_err(|e| AppError::Message(format!("Serialization failed: {e}")))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extract common config for OpenCode (JSON format)
|
|
||||||
fn extract_opencode_common_config(settings: &Value) -> Result<String, AppError> {
|
|
||||||
// OpenCode uses a different config structure with npm, options, models
|
|
||||||
// For common config, we exclude provider-specific fields like apiKey
|
|
||||||
let mut config = settings.clone();
|
|
||||||
|
|
||||||
// Remove provider-specific fields
|
|
||||||
if let Some(obj) = config.as_object_mut() {
|
|
||||||
if let Some(options) = obj.get_mut("options").and_then(|v| v.as_object_mut()) {
|
|
||||||
options.remove("apiKey");
|
|
||||||
options.remove("baseURL");
|
|
||||||
}
|
|
||||||
// Keep npm and models as they might be common
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.is_null() || (config.is_object() && config.as_object().unwrap().is_empty()) {
|
|
||||||
return Ok("{}".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
serde_json::to_string_pretty(&config)
|
|
||||||
.map_err(|e| AppError::Message(format!("Serialization failed: {e}")))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extract common config for OpenClaw (JSON format)
|
|
||||||
fn extract_openclaw_common_config(settings: &Value) -> Result<String, AppError> {
|
|
||||||
// OpenClaw uses a different config structure with baseUrl, apiKey, api, models
|
|
||||||
// For common config, we exclude provider-specific fields like apiKey
|
|
||||||
let mut config = settings.clone();
|
|
||||||
|
|
||||||
// Remove provider-specific fields
|
|
||||||
if let Some(obj) = config.as_object_mut() {
|
|
||||||
obj.remove("apiKey");
|
|
||||||
obj.remove("baseUrl");
|
|
||||||
// Keep api and models as they might be common
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.is_null() || (config.is_object() && config.as_object().unwrap().is_empty()) {
|
|
||||||
return Ok("{}".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
serde_json::to_string_pretty(&config)
|
|
||||||
.map_err(|e| AppError::Message(format!("Serialization failed: {e}")))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Import default configuration from live files (re-export)
|
/// Import default configuration from live files (re-export)
|
||||||
///
|
///
|
||||||
/// Returns `Ok(true)` if imported, `Ok(false)` if skipped.
|
/// Returns `Ok(true)` if imported, `Ok(false)` if skipped.
|
||||||
@@ -844,6 +569,11 @@ impl ProviderService {
|
|||||||
read_live_settings(app_type)
|
read_live_settings(app_type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Patch Claude live settings directly (user-level preferences)
|
||||||
|
pub fn patch_claude_live(patch: Value) -> Result<(), AppError> {
|
||||||
|
live::patch_claude_live(patch)
|
||||||
|
}
|
||||||
|
|
||||||
/// Get custom endpoints list (re-export)
|
/// Get custom endpoints list (re-export)
|
||||||
pub fn get_custom_endpoints(
|
pub fn get_custom_endpoints(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
@@ -939,10 +669,6 @@ impl ProviderService {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn write_gemini_live(provider: &Provider) -> Result<(), AppError> {
|
|
||||||
write_gemini_live(provider)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_provider_settings(app_type: &AppType, provider: &Provider) -> Result<(), AppError> {
|
fn validate_provider_settings(app_type: &AppType, provider: &Provider) -> Result<(), AppError> {
|
||||||
match app_type {
|
match app_type {
|
||||||
AppType::Claude => {
|
AppType::Claude => {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use crate::database::Database;
|
|||||||
use crate::provider::Provider;
|
use crate::provider::Provider;
|
||||||
use crate::proxy::server::ProxyServer;
|
use crate::proxy::server::ProxyServer;
|
||||||
use crate::proxy::types::*;
|
use crate::proxy::types::*;
|
||||||
use crate::services::provider::write_live_snapshot;
|
use crate::services::provider::write_live_partial;
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -1266,7 +1266,7 @@ impl ProxyService {
|
|||||||
return Ok(false);
|
return Ok(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
write_live_snapshot(app_type, provider)
|
write_live_partial(app_type, provider)
|
||||||
.map_err(|e| format!("写入 {app_type:?} Live 配置失败: {e}"))?;
|
.map_err(|e| format!("写入 {app_type:?} Live 配置失败: {e}"))?;
|
||||||
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
|
|||||||
@@ -37,12 +37,27 @@ pub struct SessionMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn scan_sessions() -> Vec<SessionMeta> {
|
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();
|
let mut sessions = Vec::new();
|
||||||
sessions.extend(codex::scan_sessions());
|
sessions.extend(r1);
|
||||||
sessions.extend(claude::scan_sessions());
|
sessions.extend(r2);
|
||||||
sessions.extend(opencode::scan_sessions());
|
sessions.extend(r3);
|
||||||
sessions.extend(openclaw::scan_sessions());
|
sessions.extend(r4);
|
||||||
sessions.extend(gemini::scan_sessions());
|
sessions.extend(r5);
|
||||||
|
|
||||||
sessions.sort_by(|a, b| {
|
sessions.sort_by(|a, b| {
|
||||||
let a_ts = a.last_active_at.or(a.created_at).unwrap_or(0);
|
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::config::get_claude_config_dir;
|
||||||
use crate::session_manager::{SessionMessage, SessionMeta};
|
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";
|
const PROVIDER_ID: &str = "claude";
|
||||||
|
|
||||||
@@ -73,60 +75,61 @@ fn parse_session(path: &Path) -> Option<SessionMeta> {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let file = File::open(path).ok()?;
|
let (head, tail) = read_head_tail_lines(path, 10, 30).ok()?;
|
||||||
let reader = BufReader::new(file);
|
|
||||||
|
|
||||||
let mut session_id: Option<String> = None;
|
let mut session_id: Option<String> = None;
|
||||||
let mut project_dir: Option<String> = None;
|
let mut project_dir: Option<String> = None;
|
||||||
let mut created_at: Option<i64> = 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() {
|
// Extract metadata from head lines
|
||||||
let line = match line {
|
for line in &head {
|
||||||
Ok(value) => value,
|
let value: Value = match serde_json::from_str(line) {
|
||||||
Err(_) => continue,
|
|
||||||
};
|
|
||||||
let value: Value = match serde_json::from_str(&line) {
|
|
||||||
Ok(parsed) => parsed,
|
Ok(parsed) => parsed,
|
||||||
Err(_) => continue,
|
Err(_) => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
if session_id.is_none() {
|
if session_id.is_none() {
|
||||||
session_id = value
|
session_id = value
|
||||||
.get("sessionId")
|
.get("sessionId")
|
||||||
.and_then(Value::as_str)
|
.and_then(Value::as_str)
|
||||||
.map(|s| s.to_string());
|
.map(|s| s.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
if project_dir.is_none() {
|
if project_dir.is_none() {
|
||||||
project_dir = value
|
project_dir = value
|
||||||
.get("cwd")
|
.get("cwd")
|
||||||
.and_then(Value::as_str)
|
.and_then(Value::as_str)
|
||||||
.map(|s| s.to_string());
|
.map(|s| s.to_string());
|
||||||
}
|
}
|
||||||
|
if created_at.is_none() {
|
||||||
if let Some(ts) = value.get("timestamp").and_then(parse_timestamp_to_ms) {
|
created_at = value.get("timestamp").and_then(parse_timestamp_to_ms);
|
||||||
if created_at.is_none() {
|
|
||||||
created_at = Some(ts);
|
|
||||||
}
|
|
||||||
last_active_at = Some(ts);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if value.get("isMeta").and_then(Value::as_bool) == Some(true) {
|
// Extract last_active_at and summary from tail lines (reverse order)
|
||||||
continue;
|
let mut last_active_at: Option<i64> = None;
|
||||||
}
|
let mut summary: Option<String> = None;
|
||||||
|
|
||||||
let message = match value.get("message") {
|
for line in tail.iter().rev() {
|
||||||
Some(message) => message,
|
let value: Value = match serde_json::from_str(line) {
|
||||||
None => continue,
|
Ok(parsed) => parsed,
|
||||||
|
Err(_) => continue,
|
||||||
};
|
};
|
||||||
|
if last_active_at.is_none() {
|
||||||
let text = message.get("content").map(extract_text).unwrap_or_default();
|
last_active_at = value.get("timestamp").and_then(parse_timestamp_to_ms);
|
||||||
if text.trim().is_empty() {
|
}
|
||||||
continue;
|
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));
|
let session_id = session_id.or_else(|| infer_session_id_from_filename(path));
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{BufRead, BufReader};
|
use std::io::{BufRead, BufReader};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
@@ -8,10 +9,17 @@ use serde_json::Value;
|
|||||||
use crate::codex_config::get_codex_config_dir;
|
use crate::codex_config::get_codex_config_dir;
|
||||||
use crate::session_manager::{SessionMessage, SessionMeta};
|
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";
|
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> {
|
pub fn scan_sessions() -> Vec<SessionMeta> {
|
||||||
let root = get_codex_config_dir().join("sessions");
|
let root = get_codex_config_dir().join("sessions");
|
||||||
let mut files = Vec::new();
|
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> {
|
fn parse_session(path: &Path) -> Option<SessionMeta> {
|
||||||
let file = File::open(path).ok()?;
|
let (head, tail) = read_head_tail_lines(path, 10, 30).ok()?;
|
||||||
let reader = BufReader::new(file);
|
|
||||||
|
|
||||||
let mut session_id: Option<String> = None;
|
let mut session_id: Option<String> = None;
|
||||||
let mut project_dir: Option<String> = None;
|
let mut project_dir: Option<String> = None;
|
||||||
let mut created_at: Option<i64> = 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() {
|
// Extract metadata from head lines
|
||||||
let line = match line {
|
for line in &head {
|
||||||
Ok(value) => value,
|
let value: Value = match serde_json::from_str(line) {
|
||||||
Err(_) => continue,
|
|
||||||
};
|
|
||||||
let value: Value = match serde_json::from_str(&line) {
|
|
||||||
Ok(parsed) => parsed,
|
Ok(parsed) => parsed,
|
||||||
Err(_) => continue,
|
Err(_) => continue,
|
||||||
};
|
};
|
||||||
|
if created_at.is_none() {
|
||||||
if let Some(ts) = value.get("timestamp").and_then(parse_timestamp_to_ms) {
|
created_at = value.get("timestamp").and_then(parse_timestamp_to_ms);
|
||||||
if created_at.is_none() {
|
|
||||||
created_at = Some(ts);
|
|
||||||
}
|
|
||||||
last_active_at = Some(ts);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if value.get("type").and_then(Value::as_str) == Some("session_meta") {
|
if value.get("type").and_then(Value::as_str) == Some("session_meta") {
|
||||||
if let Some(payload) = value.get("payload") {
|
if let Some(payload) = value.get("payload") {
|
||||||
if session_id.is_none() {
|
if session_id.is_none() {
|
||||||
@@ -118,27 +115,34 @@ fn parse_session(path: &Path) -> Option<SessionMeta> {
|
|||||||
created_at.get_or_insert(ts);
|
created_at.get_or_insert(ts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if value.get("type").and_then(Value::as_str) != Some("response_item") {
|
// Extract last_active_at and summary from tail lines (reverse order)
|
||||||
continue;
|
let mut last_active_at: Option<i64> = None;
|
||||||
}
|
let mut summary: Option<String> = None;
|
||||||
|
|
||||||
let payload = match value.get("payload") {
|
for line in tail.iter().rev() {
|
||||||
Some(payload) => payload,
|
let value: Value = match serde_json::from_str(line) {
|
||||||
None => continue,
|
Ok(parsed) => parsed,
|
||||||
|
Err(_) => continue,
|
||||||
};
|
};
|
||||||
|
if last_active_at.is_none() {
|
||||||
if payload.get("type").and_then(Value::as_str) != Some("message") {
|
last_active_at = value.get("timestamp").and_then(parse_timestamp_to_ms);
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
if summary.is_none() && value.get("type").and_then(Value::as_str) == Some("response_item") {
|
||||||
let text = payload.get("content").map(extract_text).unwrap_or_default();
|
if let Some(payload) = value.get("payload") {
|
||||||
if text.trim().is_empty() {
|
if payload.get("type").and_then(Value::as_str) == Some("message") {
|
||||||
continue;
|
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));
|
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> {
|
fn infer_session_id_from_filename(path: &Path) -> Option<String> {
|
||||||
let file_name = path.file_name()?.to_string_lossy();
|
let file_name = path.file_name()?.to_string_lossy();
|
||||||
let re =
|
UUID_RE.find(&file_name).map(|mat| mat.as_str().to_string())
|
||||||
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())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collect_jsonl_files(root: &Path, files: &mut Vec<PathBuf>) {
|
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::openclaw_config::get_openclaw_dir;
|
||||||
use crate::session_manager::{SessionMessage, SessionMeta};
|
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";
|
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> {
|
fn parse_session(path: &Path) -> Option<SessionMeta> {
|
||||||
let file = File::open(path).ok()?;
|
let (head, tail) = read_head_tail_lines(path, 10, 30).ok()?;
|
||||||
let reader = BufReader::new(file);
|
|
||||||
|
|
||||||
let mut session_id: Option<String> = None;
|
let mut session_id: Option<String> = None;
|
||||||
let mut cwd: Option<String> = None;
|
let mut cwd: Option<String> = None;
|
||||||
let mut created_at: Option<i64> = None;
|
let mut created_at: Option<i64> = None;
|
||||||
let mut last_active_at: Option<i64> = None;
|
|
||||||
let mut summary: Option<String> = None;
|
let mut summary: Option<String> = None;
|
||||||
|
|
||||||
for line in reader.lines() {
|
// Extract metadata and first message summary from head lines
|
||||||
let line = match line {
|
for line in &head {
|
||||||
Ok(value) => value,
|
let value: Value = match serde_json::from_str(line) {
|
||||||
Err(_) => continue,
|
|
||||||
};
|
|
||||||
let value: Value = match serde_json::from_str(&line) {
|
|
||||||
Ok(parsed) => parsed,
|
Ok(parsed) => parsed,
|
||||||
Err(_) => continue,
|
Err(_) => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(ts) = value.get("timestamp").and_then(parse_timestamp_to_ms) {
|
if created_at.is_none() {
|
||||||
if created_at.is_none() {
|
created_at = value.get("timestamp").and_then(parse_timestamp_to_ms);
|
||||||
created_at = Some(ts);
|
|
||||||
}
|
|
||||||
last_active_at = Some(ts);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let event_type = value.get("type").and_then(Value::as_str).unwrap_or("");
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if event_type != "message" {
|
// OpenClaw summary is the first message content
|
||||||
continue;
|
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
|
// Extract last_active_at from tail lines (reverse order)
|
||||||
if summary.is_some() {
|
let mut last_active_at: Option<i64> = None;
|
||||||
continue;
|
for line in tail.iter().rev() {
|
||||||
}
|
let value: Value = match serde_json::from_str(line) {
|
||||||
|
Ok(parsed) => parsed,
|
||||||
let message = match value.get("message") {
|
Err(_) => continue,
|
||||||
Some(msg) => msg,
|
|
||||||
None => continue,
|
|
||||||
};
|
};
|
||||||
|
if let Some(ts) = value.get("timestamp").and_then(parse_timestamp_to_ms) {
|
||||||
let text = message.get("content").map(extract_text).unwrap_or_default();
|
last_active_at = Some(ts);
|
||||||
if text.trim().is_empty() {
|
break;
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
summary = Some(text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to filename as session ID
|
// 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);
|
.and_then(parse_timestamp_to_ms);
|
||||||
|
|
||||||
// Derive title from directory basename if no explicit title
|
// Derive title from directory basename if no explicit title
|
||||||
|
let has_title = title.is_some();
|
||||||
let display_title = title.or_else(|| {
|
let display_title = title.or_else(|| {
|
||||||
directory
|
directory
|
||||||
.as_deref()
|
.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 msg_dir = storage.join("message").join(&session_id);
|
||||||
let source_path = msg_dir.to_string_lossy().to_string();
|
let source_path = msg_dir.to_string_lossy().to_string();
|
||||||
|
|
||||||
// Get summary from first user message
|
// Skip expensive I/O if title already available from session JSON
|
||||||
let summary = get_first_user_summary(storage, &session_id);
|
let summary = if has_title {
|
||||||
|
display_title.clone()
|
||||||
|
} else {
|
||||||
|
get_first_user_summary(storage, &session_id)
|
||||||
|
};
|
||||||
|
|
||||||
Some(SessionMeta {
|
Some(SessionMeta {
|
||||||
provider_id: PROVIDER_ID.to_string(),
|
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 chrono::{DateTime, FixedOffset};
|
||||||
use serde_json::Value;
|
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> {
|
pub fn parse_timestamp_to_ms(value: &Value) -> Option<i64> {
|
||||||
let raw = value.as_str()?;
|
let raw = value.as_str()?;
|
||||||
DateTime::parse_from_rfc3339(raw)
|
DateTime::parse_from_rfc3339(raw)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "CC Switch",
|
"productName": "CC Switch",
|
||||||
"version": "3.10.3",
|
"version": "3.11.0",
|
||||||
"identifier": "com.ccswitch.desktop",
|
"identifier": "com.ccswitch.desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../dist",
|
"frontendDist": "../dist",
|
||||||
|
|||||||
@@ -2,10 +2,7 @@ use serde_json::json;
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use cc_switch_lib::{
|
use cc_switch_lib::{AppError, AppType, ConfigService, MultiAppConfig, Provider};
|
||||||
get_claude_settings_path, read_json_file, AppError, AppType, ConfigService, MultiAppConfig,
|
|
||||||
Provider, ProviderMeta,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[path = "support.rs"]
|
#[path = "support.rs"]
|
||||||
mod support;
|
mod support;
|
||||||
@@ -13,132 +10,6 @@ use support::{
|
|||||||
create_test_state, create_test_state_with_config, ensure_test_home, reset_test_fs, test_mutex,
|
create_test_state, create_test_state_with_config, ensure_test_home, reset_test_fs, test_mutex,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn sync_claude_provider_writes_live_settings() {
|
|
||||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
|
||||||
reset_test_fs();
|
|
||||||
let home = ensure_test_home();
|
|
||||||
|
|
||||||
let mut config = MultiAppConfig::default();
|
|
||||||
let provider_config = json!({
|
|
||||||
"env": {
|
|
||||||
"ANTHROPIC_AUTH_TOKEN": "test-key",
|
|
||||||
"ANTHROPIC_BASE_URL": "https://api.test"
|
|
||||||
},
|
|
||||||
"ui": {
|
|
||||||
"displayName": "Test Provider"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let provider = Provider::with_id(
|
|
||||||
"prov-1".to_string(),
|
|
||||||
"Test Claude".to_string(),
|
|
||||||
provider_config.clone(),
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
|
|
||||||
let manager = config
|
|
||||||
.get_manager_mut(&AppType::Claude)
|
|
||||||
.expect("claude manager");
|
|
||||||
manager.providers.insert("prov-1".to_string(), provider);
|
|
||||||
manager.current = "prov-1".to_string();
|
|
||||||
|
|
||||||
ConfigService::sync_current_providers_to_live(&mut config).expect("sync live settings");
|
|
||||||
|
|
||||||
let settings_path = get_claude_settings_path();
|
|
||||||
assert!(
|
|
||||||
settings_path.exists(),
|
|
||||||
"live settings should be written to {}",
|
|
||||||
settings_path.display()
|
|
||||||
);
|
|
||||||
|
|
||||||
let live_value: serde_json::Value = read_json_file(&settings_path).expect("read live file");
|
|
||||||
assert_eq!(live_value, provider_config);
|
|
||||||
|
|
||||||
// 确认 SSOT 中的供应商也同步了最新内容
|
|
||||||
let updated = config
|
|
||||||
.get_manager(&AppType::Claude)
|
|
||||||
.and_then(|m| m.providers.get("prov-1"))
|
|
||||||
.expect("provider in config");
|
|
||||||
assert_eq!(updated.settings_config, provider_config);
|
|
||||||
|
|
||||||
// 额外确认写入位置位于测试 HOME 下
|
|
||||||
assert!(
|
|
||||||
settings_path.starts_with(home),
|
|
||||||
"settings path {settings_path:?} should reside under test HOME {home:?}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn sync_codex_provider_writes_auth_and_config() {
|
|
||||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
|
||||||
reset_test_fs();
|
|
||||||
|
|
||||||
let mut config = MultiAppConfig::default();
|
|
||||||
|
|
||||||
// 注意:v3.7.0 后 MCP 同步由 McpService 独立处理,不再通过 provider 切换触发
|
|
||||||
// 此测试仅验证 auth.json 和 config.toml 基础配置的写入
|
|
||||||
|
|
||||||
let provider_config = json!({
|
|
||||||
"auth": {
|
|
||||||
"OPENAI_API_KEY": "codex-key"
|
|
||||||
},
|
|
||||||
"config": r#"base_url = "https://codex.test""#
|
|
||||||
});
|
|
||||||
|
|
||||||
let provider = Provider::with_id(
|
|
||||||
"codex-1".to_string(),
|
|
||||||
"Codex Test".to_string(),
|
|
||||||
provider_config.clone(),
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
|
|
||||||
let manager = config
|
|
||||||
.get_manager_mut(&AppType::Codex)
|
|
||||||
.expect("codex manager");
|
|
||||||
manager.providers.insert("codex-1".to_string(), provider);
|
|
||||||
manager.current = "codex-1".to_string();
|
|
||||||
|
|
||||||
ConfigService::sync_current_providers_to_live(&mut config).expect("sync codex live");
|
|
||||||
|
|
||||||
let auth_path = cc_switch_lib::get_codex_auth_path();
|
|
||||||
let config_path = cc_switch_lib::get_codex_config_path();
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
auth_path.exists(),
|
|
||||||
"auth.json should exist at {}",
|
|
||||||
auth_path.display()
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
config_path.exists(),
|
|
||||||
"config.toml should exist at {}",
|
|
||||||
config_path.display()
|
|
||||||
);
|
|
||||||
|
|
||||||
let auth_value: serde_json::Value = read_json_file(&auth_path).expect("read auth");
|
|
||||||
assert_eq!(
|
|
||||||
auth_value,
|
|
||||||
provider_config.get("auth").cloned().expect("auth object")
|
|
||||||
);
|
|
||||||
|
|
||||||
let toml_text = fs::read_to_string(&config_path).expect("read config.toml");
|
|
||||||
// 验证基础配置正确写入
|
|
||||||
assert!(
|
|
||||||
toml_text.contains("base_url"),
|
|
||||||
"config.toml should contain base_url from provider config"
|
|
||||||
);
|
|
||||||
|
|
||||||
// 当前供应商应同步最新 config 文本
|
|
||||||
let manager = config.get_manager(&AppType::Codex).expect("codex manager");
|
|
||||||
let synced = manager.providers.get("codex-1").expect("codex provider");
|
|
||||||
let synced_cfg = synced
|
|
||||||
.settings_config
|
|
||||||
.get("config")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.expect("config string");
|
|
||||||
assert_eq!(synced_cfg, toml_text);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sync_enabled_to_codex_writes_enabled_servers() {
|
fn sync_enabled_to_codex_writes_enabled_servers() {
|
||||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||||
@@ -338,46 +209,6 @@ fn sync_enabled_to_codex_returns_error_on_invalid_toml() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn sync_codex_provider_missing_auth_returns_error() {
|
|
||||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
|
||||||
reset_test_fs();
|
|
||||||
|
|
||||||
let mut config = MultiAppConfig::default();
|
|
||||||
let provider = Provider::with_id(
|
|
||||||
"codex-missing-auth".to_string(),
|
|
||||||
"No Auth".to_string(),
|
|
||||||
json!({
|
|
||||||
"config": "model = \"test\""
|
|
||||||
}),
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
let manager = config
|
|
||||||
.get_manager_mut(&AppType::Codex)
|
|
||||||
.expect("codex manager");
|
|
||||||
manager.providers.insert(provider.id.clone(), provider);
|
|
||||||
manager.current = "codex-missing-auth".to_string();
|
|
||||||
|
|
||||||
let err = ConfigService::sync_current_providers_to_live(&mut config)
|
|
||||||
.expect_err("sync should fail when auth missing");
|
|
||||||
match err {
|
|
||||||
cc_switch_lib::AppError::Config(msg) => {
|
|
||||||
assert!(msg.contains("auth"), "error message should mention auth");
|
|
||||||
}
|
|
||||||
other => panic!("unexpected error variant: {other:?}"),
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确认未产生任何 live 配置文件
|
|
||||||
assert!(
|
|
||||||
!cc_switch_lib::get_codex_auth_path().exists(),
|
|
||||||
"auth.json should not be created on failure"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
!cc_switch_lib::get_codex_config_path().exists(),
|
|
||||||
"config.toml should not be created on failure"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn write_codex_live_atomic_persists_auth_and_config() {
|
fn write_codex_live_atomic_persists_auth_and_config() {
|
||||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||||
@@ -816,107 +647,6 @@ fn create_backup_retains_only_latest_entries() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn sync_gemini_packycode_sets_security_selected_type() {
|
|
||||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
|
||||||
reset_test_fs();
|
|
||||||
let home = ensure_test_home();
|
|
||||||
|
|
||||||
let mut config = MultiAppConfig::default();
|
|
||||||
{
|
|
||||||
let manager = config
|
|
||||||
.get_manager_mut(&AppType::Gemini)
|
|
||||||
.expect("gemini manager");
|
|
||||||
manager.current = "packy-1".to_string();
|
|
||||||
manager.providers.insert(
|
|
||||||
"packy-1".to_string(),
|
|
||||||
Provider::with_id(
|
|
||||||
"packy-1".to_string(),
|
|
||||||
"PackyCode".to_string(),
|
|
||||||
json!({
|
|
||||||
"env": {
|
|
||||||
"GEMINI_API_KEY": "pk-key",
|
|
||||||
"GOOGLE_GEMINI_BASE_URL": "https://api-slb.packyapi.com"
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
Some("https://www.packyapi.com".to_string()),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ConfigService::sync_current_providers_to_live(&mut config)
|
|
||||||
.expect("syncing gemini live should succeed");
|
|
||||||
|
|
||||||
// security field is written to ~/.gemini/settings.json, not ~/.cc-switch/settings.json
|
|
||||||
let gemini_settings = home.join(".gemini").join("settings.json");
|
|
||||||
assert!(
|
|
||||||
gemini_settings.exists(),
|
|
||||||
"Gemini settings.json should exist at {}",
|
|
||||||
gemini_settings.display()
|
|
||||||
);
|
|
||||||
|
|
||||||
let raw = std::fs::read_to_string(&gemini_settings).expect("read gemini settings.json");
|
|
||||||
let value: serde_json::Value = serde_json::from_str(&raw).expect("parse gemini settings.json");
|
|
||||||
assert_eq!(
|
|
||||||
value
|
|
||||||
.pointer("/security/auth/selectedType")
|
|
||||||
.and_then(|v| v.as_str()),
|
|
||||||
Some("gemini-api-key"),
|
|
||||||
"syncing PackyCode Gemini should enforce security.auth.selectedType in Gemini settings"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn sync_gemini_google_official_sets_oauth_security() {
|
|
||||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
|
||||||
reset_test_fs();
|
|
||||||
let home = ensure_test_home();
|
|
||||||
|
|
||||||
let mut config = MultiAppConfig::default();
|
|
||||||
{
|
|
||||||
let manager = config
|
|
||||||
.get_manager_mut(&AppType::Gemini)
|
|
||||||
.expect("gemini manager");
|
|
||||||
manager.current = "google-official".to_string();
|
|
||||||
let mut provider = Provider::with_id(
|
|
||||||
"google-official".to_string(),
|
|
||||||
"Google".to_string(),
|
|
||||||
json!({
|
|
||||||
"env": {}
|
|
||||||
}),
|
|
||||||
Some("https://ai.google.dev".to_string()),
|
|
||||||
);
|
|
||||||
provider.meta = Some(ProviderMeta {
|
|
||||||
partner_promotion_key: Some("google-official".to_string()),
|
|
||||||
..ProviderMeta::default()
|
|
||||||
});
|
|
||||||
manager
|
|
||||||
.providers
|
|
||||||
.insert("google-official".to_string(), provider);
|
|
||||||
}
|
|
||||||
|
|
||||||
ConfigService::sync_current_providers_to_live(&mut config)
|
|
||||||
.expect("syncing google official gemini should succeed");
|
|
||||||
|
|
||||||
// security field is written to ~/.gemini/settings.json, not ~/.cc-switch/settings.json
|
|
||||||
let gemini_settings = home.join(".gemini").join("settings.json");
|
|
||||||
assert!(
|
|
||||||
gemini_settings.exists(),
|
|
||||||
"Gemini settings should exist at {}",
|
|
||||||
gemini_settings.display()
|
|
||||||
);
|
|
||||||
let gemini_raw = std::fs::read_to_string(&gemini_settings).expect("read gemini settings");
|
|
||||||
let gemini_value: serde_json::Value =
|
|
||||||
serde_json::from_str(&gemini_raw).expect("parse gemini settings json");
|
|
||||||
assert_eq!(
|
|
||||||
gemini_value
|
|
||||||
.pointer("/security/auth/selectedType")
|
|
||||||
.and_then(|v| v.as_str()),
|
|
||||||
Some("oauth-personal"),
|
|
||||||
"Gemini settings should record oauth-personal for Google Official"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn export_sql_writes_to_target_path() {
|
fn export_sql_writes_to_target_path() {
|
||||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||||
|
|||||||
@@ -100,9 +100,12 @@ command = "say"
|
|||||||
);
|
);
|
||||||
|
|
||||||
let config_text = std::fs::read_to_string(get_codex_config_path()).expect("read config.toml");
|
let config_text = std::fs::read_to_string(get_codex_config_path()).expect("read config.toml");
|
||||||
|
// With partial merge, only key fields (model, provider, model_providers) are
|
||||||
|
// merged into config.toml. The existing MCP section should be preserved.
|
||||||
|
// MCP sync from DB is handled separately (at startup or explicit sync).
|
||||||
assert!(
|
assert!(
|
||||||
config_text.contains("mcp_servers.echo-server"),
|
config_text.contains("mcp_servers.legacy"),
|
||||||
"config.toml should contain synced MCP servers"
|
"config.toml should preserve existing MCP servers after partial merge"
|
||||||
);
|
);
|
||||||
|
|
||||||
let current_id = app_state
|
let current_id = app_state
|
||||||
@@ -126,12 +129,9 @@ command = "say"
|
|||||||
.get("config")
|
.get("config")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
// 供应商配置应该包含在 live 文件中
|
// With partial merge, only key fields (model_provider, model, model_providers)
|
||||||
// 注意:live 文件还会包含 MCP 同步后的内容
|
// are written to the live file. MCP servers are synced separately.
|
||||||
assert!(
|
// The provider's stored config should still contain mcp_servers.latest.
|
||||||
config_text.contains("mcp_servers.latest"),
|
|
||||||
"live file should contain provider's original config"
|
|
||||||
);
|
|
||||||
assert!(
|
assert!(
|
||||||
new_config_text.contains("mcp_servers.latest"),
|
new_config_text.contains("mcp_servers.latest"),
|
||||||
"provider snapshot should contain provider's original config"
|
"provider snapshot should contain provider's original config"
|
||||||
@@ -268,11 +268,22 @@ fn switch_provider_updates_claude_live_and_state() {
|
|||||||
let legacy_provider = providers
|
let legacy_provider = providers
|
||||||
.get("old-provider")
|
.get("old-provider")
|
||||||
.expect("legacy provider still exists");
|
.expect("legacy provider still exists");
|
||||||
// 回填机制:切换前会将 live 配置回填到当前供应商
|
// Backfill mechanism: before switching, the live config's key fields are
|
||||||
// 这保护了用户在 live 文件中的手动修改
|
// backfilled to the current provider. With partial merge, only key fields
|
||||||
|
// (auth, model, endpoint) are extracted — non-key fields like workspace
|
||||||
|
// are NOT included in the backfill.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
legacy_provider.settings_config, legacy_live,
|
legacy_provider
|
||||||
"previous provider should be backfilled with live config"
|
.settings_config
|
||||||
|
.get("env")
|
||||||
|
.and_then(|env| env.get("ANTHROPIC_API_KEY"))
|
||||||
|
.and_then(|key| key.as_str()),
|
||||||
|
Some("legacy-key"),
|
||||||
|
"previous provider should be backfilled with live auth key"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
legacy_provider.settings_config.get("workspace").is_none(),
|
||||||
|
"backfill should NOT include non-key fields like workspace"
|
||||||
);
|
);
|
||||||
|
|
||||||
let new_provider = providers.get("new-provider").expect("new provider exists");
|
let new_provider = providers.get("new-provider").expect("new provider exists");
|
||||||
|
|||||||
@@ -112,9 +112,12 @@ command = "say"
|
|||||||
|
|
||||||
let config_text =
|
let config_text =
|
||||||
std::fs::read_to_string(cc_switch_lib::get_codex_config_path()).expect("read config.toml");
|
std::fs::read_to_string(cc_switch_lib::get_codex_config_path()).expect("read config.toml");
|
||||||
|
// With partial merge, only key fields (model, provider, model_providers) are
|
||||||
|
// merged into config.toml. The existing MCP section should be preserved.
|
||||||
|
// MCP sync from DB is handled separately (at startup or explicit sync).
|
||||||
assert!(
|
assert!(
|
||||||
config_text.contains("mcp_servers.echo-server"),
|
config_text.contains("mcp_servers.legacy"),
|
||||||
"config.toml should contain synced MCP servers"
|
"config.toml should preserve existing MCP servers after partial merge"
|
||||||
);
|
);
|
||||||
|
|
||||||
let current_id = state
|
let current_id = state
|
||||||
@@ -143,11 +146,6 @@ command = "say"
|
|||||||
new_config_text.contains("mcp_servers.latest"),
|
new_config_text.contains("mcp_servers.latest"),
|
||||||
"provider config should contain original MCP servers"
|
"provider config should contain original MCP servers"
|
||||||
);
|
);
|
||||||
// live 文件额外包含同步的 MCP 服务器
|
|
||||||
assert!(
|
|
||||||
config_text.contains("mcp_servers.echo-server"),
|
|
||||||
"live config should include synced MCP servers"
|
|
||||||
);
|
|
||||||
|
|
||||||
let legacy = providers
|
let legacy = providers
|
||||||
.get("old-provider")
|
.get("old-provider")
|
||||||
@@ -414,9 +412,19 @@ fn provider_service_switch_claude_updates_live_and_state() {
|
|||||||
let legacy_provider = providers
|
let legacy_provider = providers
|
||||||
.get("old-provider")
|
.get("old-provider")
|
||||||
.expect("legacy provider still exists");
|
.expect("legacy provider still exists");
|
||||||
|
// With partial merge backfill, only key fields are extracted from live config
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
legacy_provider.settings_config, legacy_live,
|
legacy_provider
|
||||||
"previous provider should receive backfilled live config"
|
.settings_config
|
||||||
|
.get("env")
|
||||||
|
.and_then(|env| env.get("ANTHROPIC_API_KEY"))
|
||||||
|
.and_then(|key| key.as_str()),
|
||||||
|
Some("legacy-key"),
|
||||||
|
"previous provider should receive backfilled auth key"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
legacy_provider.settings_config.get("workspace").is_none(),
|
||||||
|
"backfill should NOT include non-key fields like workspace"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,15 +53,12 @@ export function DeepLinkImportDialog() {
|
|||||||
const unlistenImport = listen<DeepLinkImportRequest>(
|
const unlistenImport = listen<DeepLinkImportRequest>(
|
||||||
"deeplink-import",
|
"deeplink-import",
|
||||||
async (event) => {
|
async (event) => {
|
||||||
console.log("Deep link import event received:", event.payload);
|
|
||||||
|
|
||||||
// If config is present, merge it to get the complete configuration
|
// If config is present, merge it to get the complete configuration
|
||||||
if (event.payload.config || event.payload.configUrl) {
|
if (event.payload.config || event.payload.configUrl) {
|
||||||
try {
|
try {
|
||||||
const mergedRequest = await deeplinkApi.mergeDeeplinkConfig(
|
const mergedRequest = await deeplinkApi.mergeDeeplinkConfig(
|
||||||
event.payload,
|
event.payload,
|
||||||
);
|
);
|
||||||
console.log("Config merged successfully:", mergedRequest);
|
|
||||||
setRequest(mergedRequest);
|
setRequest(mergedRequest);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to merge config:", error);
|
console.error("Failed to merge config:", error);
|
||||||
|
|||||||
@@ -192,6 +192,7 @@ export function EditProviderDialog({
|
|||||||
onCancel={() => onOpenChange(false)}
|
onCancel={() => onOpenChange(false)}
|
||||||
initialData={initialData}
|
initialData={initialData}
|
||||||
showButtons={false}
|
showButtons={false}
|
||||||
|
isCurrent={liveSettings !== null}
|
||||||
/>
|
/>
|
||||||
</FullScreenPanel>
|
</FullScreenPanel>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ interface ProviderActionsProps {
|
|||||||
isTesting?: boolean;
|
isTesting?: boolean;
|
||||||
isProxyTakeover?: boolean;
|
isProxyTakeover?: boolean;
|
||||||
isOmo?: boolean;
|
isOmo?: boolean;
|
||||||
isLastOmo?: boolean;
|
|
||||||
onSwitch: () => void;
|
onSwitch: () => void;
|
||||||
onEdit: () => void;
|
onEdit: () => void;
|
||||||
onDuplicate: () => void;
|
onDuplicate: () => void;
|
||||||
@@ -49,7 +48,6 @@ export function ProviderActions({
|
|||||||
isTesting: _isTesting, // Hidden: stream check feature disabled
|
isTesting: _isTesting, // Hidden: stream check feature disabled
|
||||||
isProxyTakeover = false,
|
isProxyTakeover = false,
|
||||||
isOmo = false,
|
isOmo = false,
|
||||||
isLastOmo = false,
|
|
||||||
onSwitch,
|
onSwitch,
|
||||||
onEdit,
|
onEdit,
|
||||||
onDuplicate,
|
onDuplicate,
|
||||||
@@ -192,11 +190,7 @@ export function ProviderActions({
|
|||||||
|
|
||||||
const buttonState = getMainButtonState();
|
const buttonState = getMainButtonState();
|
||||||
|
|
||||||
const canDelete = isOmo
|
const canDelete = isOmo || isAdditiveMode ? true : !isCurrent;
|
||||||
? !(isLastOmo && isCurrent)
|
|
||||||
: isAdditiveMode
|
|
||||||
? true
|
|
||||||
: !isCurrent;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
|
|||||||
@@ -28,9 +28,7 @@ interface ProviderCardProps {
|
|||||||
appId: AppId;
|
appId: AppId;
|
||||||
isInConfig?: boolean; // OpenCode: 是否已添加到 opencode.json
|
isInConfig?: boolean; // OpenCode: 是否已添加到 opencode.json
|
||||||
isOmo?: boolean;
|
isOmo?: boolean;
|
||||||
isLastOmo?: boolean;
|
|
||||||
isOmoSlim?: boolean;
|
isOmoSlim?: boolean;
|
||||||
isLastOmoSlim?: boolean;
|
|
||||||
onSwitch: (provider: Provider) => void;
|
onSwitch: (provider: Provider) => void;
|
||||||
onEdit: (provider: Provider) => void;
|
onEdit: (provider: Provider) => void;
|
||||||
onDelete: (provider: Provider) => void;
|
onDelete: (provider: Provider) => void;
|
||||||
@@ -94,9 +92,7 @@ export function ProviderCard({
|
|||||||
appId,
|
appId,
|
||||||
isInConfig = true,
|
isInConfig = true,
|
||||||
isOmo = false,
|
isOmo = false,
|
||||||
isLastOmo = false,
|
|
||||||
isOmoSlim = false,
|
isOmoSlim = false,
|
||||||
isLastOmoSlim = false,
|
|
||||||
onSwitch,
|
onSwitch,
|
||||||
onEdit,
|
onEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
@@ -125,7 +121,6 @@ export function ProviderCard({
|
|||||||
|
|
||||||
// OMO and OMO Slim share the same card behavior
|
// OMO and OMO Slim share the same card behavior
|
||||||
const isAnyOmo = isOmo || isOmoSlim;
|
const isAnyOmo = isOmo || isOmoSlim;
|
||||||
const isLastAnyOmo = isOmo ? isLastOmo : isLastOmoSlim;
|
|
||||||
const handleDisableAnyOmo = isOmoSlim ? onDisableOmoSlim : onDisableOmo;
|
const handleDisableAnyOmo = isOmoSlim ? onDisableOmoSlim : onDisableOmo;
|
||||||
|
|
||||||
const { data: health } = useProviderHealth(provider.id, appId);
|
const { data: health } = useProviderHealth(provider.id, appId);
|
||||||
@@ -390,7 +385,6 @@ export function ProviderCard({
|
|||||||
isTesting={isTesting}
|
isTesting={isTesting}
|
||||||
isProxyTakeover={isProxyTakeover}
|
isProxyTakeover={isProxyTakeover}
|
||||||
isOmo={isAnyOmo}
|
isOmo={isAnyOmo}
|
||||||
isLastOmo={isLastAnyOmo}
|
|
||||||
onSwitch={() => onSwitch(provider)}
|
onSwitch={() => onSwitch(provider)}
|
||||||
onEdit={() => onEdit(provider)}
|
onEdit={() => onEdit(provider)}
|
||||||
onDuplicate={() => onDuplicate(provider)}
|
onDuplicate={() => onDuplicate(provider)}
|
||||||
|
|||||||
@@ -36,9 +36,7 @@ import {
|
|||||||
} from "@/lib/query/failover";
|
} from "@/lib/query/failover";
|
||||||
import {
|
import {
|
||||||
useCurrentOmoProviderId,
|
useCurrentOmoProviderId,
|
||||||
useOmoProviderCount,
|
|
||||||
useCurrentOmoSlimProviderId,
|
useCurrentOmoSlimProviderId,
|
||||||
useOmoSlimProviderCount,
|
|
||||||
} from "@/lib/query/omo";
|
} from "@/lib/query/omo";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -142,9 +140,7 @@ export function ProviderList({
|
|||||||
|
|
||||||
const isOpenCode = appId === "opencode";
|
const isOpenCode = appId === "opencode";
|
||||||
const { data: currentOmoId } = useCurrentOmoProviderId(isOpenCode);
|
const { data: currentOmoId } = useCurrentOmoProviderId(isOpenCode);
|
||||||
const { data: omoProviderCount } = useOmoProviderCount(isOpenCode);
|
|
||||||
const { data: currentOmoSlimId } = useCurrentOmoSlimProviderId(isOpenCode);
|
const { data: currentOmoSlimId } = useCurrentOmoSlimProviderId(isOpenCode);
|
||||||
const { data: omoSlimProviderCount } = useOmoSlimProviderCount(isOpenCode);
|
|
||||||
|
|
||||||
const getFailoverPriority = useCallback(
|
const getFailoverPriority = useCallback(
|
||||||
(providerId: string): number | undefined => {
|
(providerId: string): number | undefined => {
|
||||||
@@ -293,15 +289,7 @@ export function ProviderList({
|
|||||||
appId={appId}
|
appId={appId}
|
||||||
isInConfig={isProviderInConfig(provider.id)}
|
isInConfig={isProviderInConfig(provider.id)}
|
||||||
isOmo={isOmo}
|
isOmo={isOmo}
|
||||||
isLastOmo={
|
|
||||||
isOmo && (omoProviderCount ?? 0) <= 1 && isOmoCurrent
|
|
||||||
}
|
|
||||||
isOmoSlim={isOmoSlim}
|
isOmoSlim={isOmoSlim}
|
||||||
isLastOmoSlim={
|
|
||||||
isOmoSlim &&
|
|
||||||
(omoSlimProviderCount ?? 0) <= 1 &&
|
|
||||||
isOmoSlimCurrent
|
|
||||||
}
|
|
||||||
onSwitch={onSwitch}
|
onSwitch={onSwitch}
|
||||||
onEdit={onEdit}
|
onEdit={onEdit}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
@@ -420,9 +408,7 @@ interface SortableProviderCardProps {
|
|||||||
appId: AppId;
|
appId: AppId;
|
||||||
isInConfig: boolean;
|
isInConfig: boolean;
|
||||||
isOmo: boolean;
|
isOmo: boolean;
|
||||||
isLastOmo: boolean;
|
|
||||||
isOmoSlim: boolean;
|
isOmoSlim: boolean;
|
||||||
isLastOmoSlim: boolean;
|
|
||||||
onSwitch: (provider: Provider) => void;
|
onSwitch: (provider: Provider) => void;
|
||||||
onEdit: (provider: Provider) => void;
|
onEdit: (provider: Provider) => void;
|
||||||
onDelete: (provider: Provider) => void;
|
onDelete: (provider: Provider) => void;
|
||||||
@@ -453,9 +439,7 @@ function SortableProviderCard({
|
|||||||
appId,
|
appId,
|
||||||
isInConfig,
|
isInConfig,
|
||||||
isOmo,
|
isOmo,
|
||||||
isLastOmo,
|
|
||||||
isOmoSlim,
|
isOmoSlim,
|
||||||
isLastOmoSlim,
|
|
||||||
onSwitch,
|
onSwitch,
|
||||||
onEdit,
|
onEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
@@ -500,9 +484,7 @@ function SortableProviderCard({
|
|||||||
appId={appId}
|
appId={appId}
|
||||||
isInConfig={isInConfig}
|
isInConfig={isInConfig}
|
||||||
isOmo={isOmo}
|
isOmo={isOmo}
|
||||||
isLastOmo={isLastOmo}
|
|
||||||
isOmoSlim={isOmoSlim}
|
isOmoSlim={isOmoSlim}
|
||||||
isLastOmoSlim={isLastOmoSlim}
|
|
||||||
onSwitch={onSwitch}
|
onSwitch={onSwitch}
|
||||||
onEdit={onEdit}
|
onEdit={onEdit}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ import {
|
|||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import EndpointSpeedTest from "./EndpointSpeedTest";
|
import EndpointSpeedTest from "./EndpointSpeedTest";
|
||||||
import { ApiKeySection, EndpointField } from "./shared";
|
import { ApiKeySection, EndpointField } from "./shared";
|
||||||
import type { ProviderCategory, ClaudeApiFormat } from "@/types";
|
import type {
|
||||||
|
ProviderCategory,
|
||||||
|
ClaudeApiFormat,
|
||||||
|
ClaudeApiKeyField,
|
||||||
|
} from "@/types";
|
||||||
import type { TemplateValueConfig } from "@/config/claudeProviderPresets";
|
import type { TemplateValueConfig } from "@/config/claudeProviderPresets";
|
||||||
|
|
||||||
interface EndpointCandidate {
|
interface EndpointCandidate {
|
||||||
@@ -68,6 +72,10 @@ interface ClaudeFormFieldsProps {
|
|||||||
// API Format (for third-party providers that use OpenAI Chat Completions format)
|
// API Format (for third-party providers that use OpenAI Chat Completions format)
|
||||||
apiFormat: ClaudeApiFormat;
|
apiFormat: ClaudeApiFormat;
|
||||||
onApiFormatChange: (format: ClaudeApiFormat) => void;
|
onApiFormatChange: (format: ClaudeApiFormat) => void;
|
||||||
|
|
||||||
|
// Auth Key Field (ANTHROPIC_AUTH_TOKEN vs ANTHROPIC_API_KEY)
|
||||||
|
apiKeyField: ClaudeApiKeyField;
|
||||||
|
onApiKeyFieldChange: (field: ClaudeApiKeyField) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ClaudeFormFields({
|
export function ClaudeFormFields({
|
||||||
@@ -102,6 +110,8 @@ export function ClaudeFormFields({
|
|||||||
speedTestEndpoints,
|
speedTestEndpoints,
|
||||||
apiFormat,
|
apiFormat,
|
||||||
onApiFormatChange,
|
onApiFormatChange,
|
||||||
|
apiKeyField,
|
||||||
|
onApiKeyFieldChange,
|
||||||
}: ClaudeFormFieldsProps) {
|
}: ClaudeFormFieldsProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -188,8 +198,8 @@ export function ClaudeFormFields({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* API 格式选择(仅非官方供应商显示) */}
|
{/* API 格式选择(仅非官方、非云服务商显示) */}
|
||||||
{shouldShowModelSelector && (
|
{shouldShowModelSelector && category !== "cloud_provider" && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<FormLabel htmlFor="apiFormat">
|
<FormLabel htmlFor="apiFormat">
|
||||||
{t("providerForm.apiFormat", { defaultValue: "API 格式" })}
|
{t("providerForm.apiFormat", { defaultValue: "API 格式" })}
|
||||||
@@ -219,6 +229,41 @@ export function ClaudeFormFields({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 认证字段选择(仅非官方供应商显示) */}
|
||||||
|
{shouldShowModelSelector && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<FormLabel htmlFor="apiKeyField">
|
||||||
|
{t("providerForm.authField", { defaultValue: "认证字段" })}
|
||||||
|
</FormLabel>
|
||||||
|
<Select
|
||||||
|
value={apiKeyField}
|
||||||
|
onValueChange={(v) => onApiKeyFieldChange(v as ClaudeApiKeyField)}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="apiKeyField" className="w-full">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="ANTHROPIC_AUTH_TOKEN">
|
||||||
|
{t("providerForm.authFieldAuthToken", {
|
||||||
|
defaultValue: "Auth Token (默认)",
|
||||||
|
})}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="ANTHROPIC_API_KEY">
|
||||||
|
{t("providerForm.authFieldApiKey", {
|
||||||
|
defaultValue: "API Key",
|
||||||
|
})}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{t("providerForm.authFieldHint", {
|
||||||
|
defaultValue:
|
||||||
|
"大多数第三方供应商使用 Auth Token;少数供应商需要 API Key",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 模型选择器 */}
|
{/* 模型选择器 */}
|
||||||
{shouldShowModelSelector && (
|
{shouldShowModelSelector && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import { useState, useEffect, useCallback } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
|
||||||
|
type ToggleKey = "hideAttribution" | "alwaysThinking" | "enableTeammates";
|
||||||
|
|
||||||
|
interface ClaudeQuickTogglesProps {
|
||||||
|
/** Called after a patch is applied to the live file, so the caller can mirror it in the JSON editor. */
|
||||||
|
onPatchApplied?: (patch: Record<string, unknown>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultStates: Record<ToggleKey, boolean> = {
|
||||||
|
hideAttribution: false,
|
||||||
|
alwaysThinking: false,
|
||||||
|
enableTeammates: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
function deriveStates(
|
||||||
|
cfg: Record<string, unknown>,
|
||||||
|
): Record<ToggleKey, boolean> {
|
||||||
|
const env = cfg?.env as Record<string, unknown> | undefined;
|
||||||
|
const attr = cfg?.attribution as Record<string, unknown> | undefined;
|
||||||
|
return {
|
||||||
|
hideAttribution: attr?.commit === "" && attr?.pr === "",
|
||||||
|
alwaysThinking: cfg?.alwaysThinkingEnabled === true,
|
||||||
|
enableTeammates: env?.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS === "1",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Apply RFC 7396 JSON Merge Patch in-place: null = delete, object = recurse, else overwrite. */
|
||||||
|
function jsonMergePatch(
|
||||||
|
target: Record<string, unknown>,
|
||||||
|
patch: Record<string, unknown>,
|
||||||
|
) {
|
||||||
|
for (const [key, value] of Object.entries(patch)) {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
delete target[key];
|
||||||
|
} else if (typeof value === "object" && !Array.isArray(value)) {
|
||||||
|
if (
|
||||||
|
typeof target[key] !== "object" ||
|
||||||
|
target[key] === null ||
|
||||||
|
Array.isArray(target[key])
|
||||||
|
) {
|
||||||
|
target[key] = {};
|
||||||
|
}
|
||||||
|
jsonMergePatch(
|
||||||
|
target[key] as Record<string, unknown>,
|
||||||
|
value as Record<string, unknown>,
|
||||||
|
);
|
||||||
|
if (Object.keys(target[key] as Record<string, unknown>).length === 0) {
|
||||||
|
delete target[key];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
target[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { jsonMergePatch };
|
||||||
|
|
||||||
|
export function ClaudeQuickToggles({
|
||||||
|
onPatchApplied,
|
||||||
|
}: ClaudeQuickTogglesProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [states, setStates] = useState(defaultStates);
|
||||||
|
|
||||||
|
const readLive = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const cfg = await invoke<Record<string, unknown>>(
|
||||||
|
"read_live_provider_settings",
|
||||||
|
{ app: "claude" },
|
||||||
|
);
|
||||||
|
setStates(deriveStates(cfg));
|
||||||
|
} catch {
|
||||||
|
// Live file missing or unreadable — show all unchecked
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
readLive();
|
||||||
|
}, [readLive]);
|
||||||
|
|
||||||
|
const toggle = useCallback(
|
||||||
|
async (key: ToggleKey) => {
|
||||||
|
let patch: Record<string, unknown>;
|
||||||
|
if (key === "hideAttribution") {
|
||||||
|
patch = states.hideAttribution
|
||||||
|
? { attribution: null }
|
||||||
|
: { attribution: { commit: "", pr: "" } };
|
||||||
|
} else if (key === "alwaysThinking") {
|
||||||
|
patch = states.alwaysThinking
|
||||||
|
? { alwaysThinkingEnabled: null }
|
||||||
|
: { alwaysThinkingEnabled: true };
|
||||||
|
} else {
|
||||||
|
patch = states.enableTeammates
|
||||||
|
? { env: { CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: null } }
|
||||||
|
: { env: { CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: "1" } };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optimistic update
|
||||||
|
setStates((prev) => ({ ...prev, [key]: !prev[key] }));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await invoke("patch_claude_live_settings", { patch });
|
||||||
|
onPatchApplied?.(patch);
|
||||||
|
} catch {
|
||||||
|
// Revert on failure
|
||||||
|
readLive();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[states, readLive, onPatchApplied],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap gap-x-4 gap-y-1">
|
||||||
|
{(
|
||||||
|
[
|
||||||
|
["hideAttribution", "claudeConfig.hideAttribution"],
|
||||||
|
["alwaysThinking", "claudeConfig.alwaysThinking"],
|
||||||
|
["enableTeammates", "claudeConfig.enableTeammates"],
|
||||||
|
] as const
|
||||||
|
).map(([key, i18nKey]) => (
|
||||||
|
<label
|
||||||
|
key={key}
|
||||||
|
className="flex items-center gap-1.5 text-sm cursor-pointer"
|
||||||
|
>
|
||||||
|
<Checkbox checked={states[key]} onCheckedChange={() => toggle(key)} />
|
||||||
|
{t(i18nKey)}
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { Save, Download, Loader2 } from "lucide-react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { FullScreenPanel } from "@/components/common/FullScreenPanel";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import JsonEditor from "@/components/JsonEditor";
|
|
||||||
|
|
||||||
interface CodexCommonConfigModalProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
value: string;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
error?: string;
|
|
||||||
onExtract?: () => void;
|
|
||||||
isExtracting?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CodexCommonConfigModal - Common Codex configuration editor modal
|
|
||||||
* Allows editing of common TOML configuration shared across providers
|
|
||||||
*/
|
|
||||||
export const CodexCommonConfigModal: React.FC<CodexCommonConfigModalProps> = ({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
error,
|
|
||||||
onExtract,
|
|
||||||
isExtracting,
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [isDarkMode, setIsDarkMode] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsDarkMode(document.documentElement.classList.contains("dark"));
|
|
||||||
|
|
||||||
const observer = new MutationObserver(() => {
|
|
||||||
setIsDarkMode(document.documentElement.classList.contains("dark"));
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(document.documentElement, {
|
|
||||||
attributes: true,
|
|
||||||
attributeFilter: ["class"],
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => observer.disconnect();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FullScreenPanel
|
|
||||||
isOpen={isOpen}
|
|
||||||
title={t("codexConfig.editCommonConfigTitle")}
|
|
||||||
onClose={onClose}
|
|
||||||
footer={
|
|
||||||
<>
|
|
||||||
{onExtract && (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={onExtract}
|
|
||||||
disabled={isExtracting}
|
|
||||||
className="gap-2"
|
|
||||||
>
|
|
||||||
{isExtracting ? (
|
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Download className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
{t("codexConfig.extractFromCurrent", {
|
|
||||||
defaultValue: "从编辑内容提取",
|
|
||||||
})}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button type="button" variant="outline" onClick={onClose}>
|
|
||||||
{t("common.cancel")}
|
|
||||||
</Button>
|
|
||||||
<Button type="button" onClick={onClose} className="gap-2">
|
|
||||||
<Save className="w-4 h-4" />
|
|
||||||
{t("common.save")}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{t("codexConfig.commonConfigHint")}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<JsonEditor
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
placeholder={`# Common Codex config
|
|
||||||
|
|
||||||
# Add your common TOML configuration here`}
|
|
||||||
darkMode={isDarkMode}
|
|
||||||
rows={16}
|
|
||||||
showValidation={false}
|
|
||||||
language="javascript"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<p className="text-sm text-red-500 dark:text-red-400">{error}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</FullScreenPanel>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React from "react";
|
||||||
import { CodexAuthSection, CodexConfigSection } from "./CodexConfigSections";
|
import { CodexAuthSection, CodexConfigSection } from "./CodexConfigSections";
|
||||||
import { CodexCommonConfigModal } from "./CodexCommonConfigModal";
|
|
||||||
|
|
||||||
interface CodexConfigEditorProps {
|
interface CodexConfigEditorProps {
|
||||||
authValue: string;
|
authValue: string;
|
||||||
@@ -13,23 +12,9 @@ interface CodexConfigEditorProps {
|
|||||||
|
|
||||||
onAuthBlur?: () => void;
|
onAuthBlur?: () => void;
|
||||||
|
|
||||||
useCommonConfig: boolean;
|
|
||||||
|
|
||||||
onCommonConfigToggle: (checked: boolean) => void;
|
|
||||||
|
|
||||||
commonConfigSnippet: string;
|
|
||||||
|
|
||||||
onCommonConfigSnippetChange: (value: string) => void;
|
|
||||||
|
|
||||||
commonConfigError: string;
|
|
||||||
|
|
||||||
authError: string;
|
authError: string;
|
||||||
|
|
||||||
configError: string; // config.toml 错误提示
|
configError: string;
|
||||||
|
|
||||||
onExtract?: () => void;
|
|
||||||
|
|
||||||
isExtracting?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
||||||
@@ -38,25 +23,9 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
|||||||
onAuthChange,
|
onAuthChange,
|
||||||
onConfigChange,
|
onConfigChange,
|
||||||
onAuthBlur,
|
onAuthBlur,
|
||||||
useCommonConfig,
|
|
||||||
onCommonConfigToggle,
|
|
||||||
commonConfigSnippet,
|
|
||||||
onCommonConfigSnippetChange,
|
|
||||||
commonConfigError,
|
|
||||||
authError,
|
authError,
|
||||||
configError,
|
configError,
|
||||||
onExtract,
|
|
||||||
isExtracting,
|
|
||||||
}) => {
|
}) => {
|
||||||
const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false);
|
|
||||||
|
|
||||||
// Auto-open common config modal if there's an error
|
|
||||||
useEffect(() => {
|
|
||||||
if (commonConfigError && !isCommonConfigModalOpen) {
|
|
||||||
setIsCommonConfigModalOpen(true);
|
|
||||||
}
|
|
||||||
}, [commonConfigError, isCommonConfigModalOpen]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Auth JSON Section */}
|
{/* Auth JSON Section */}
|
||||||
@@ -71,23 +40,8 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
|||||||
<CodexConfigSection
|
<CodexConfigSection
|
||||||
value={configValue}
|
value={configValue}
|
||||||
onChange={onConfigChange}
|
onChange={onConfigChange}
|
||||||
useCommonConfig={useCommonConfig}
|
|
||||||
onCommonConfigToggle={onCommonConfigToggle}
|
|
||||||
onEditCommonConfig={() => setIsCommonConfigModalOpen(true)}
|
|
||||||
commonConfigError={commonConfigError}
|
|
||||||
configError={configError}
|
configError={configError}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Common Config Modal */}
|
|
||||||
<CodexCommonConfigModal
|
|
||||||
isOpen={isCommonConfigModalOpen}
|
|
||||||
onClose={() => setIsCommonConfigModalOpen(false)}
|
|
||||||
value={commonConfigSnippet}
|
|
||||||
onChange={onCommonConfigSnippetChange}
|
|
||||||
error={commonConfigError}
|
|
||||||
onExtract={onExtract}
|
|
||||||
isExtracting={isExtracting}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -78,10 +78,6 @@ export const CodexAuthSection: React.FC<CodexAuthSectionProps> = ({
|
|||||||
interface CodexConfigSectionProps {
|
interface CodexConfigSectionProps {
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
useCommonConfig: boolean;
|
|
||||||
onCommonConfigToggle: (checked: boolean) => void;
|
|
||||||
onEditCommonConfig: () => void;
|
|
||||||
commonConfigError?: string;
|
|
||||||
configError?: string;
|
configError?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,10 +87,6 @@ interface CodexConfigSectionProps {
|
|||||||
export const CodexConfigSection: React.FC<CodexConfigSectionProps> = ({
|
export const CodexConfigSection: React.FC<CodexConfigSectionProps> = ({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
useCommonConfig,
|
|
||||||
onCommonConfigToggle,
|
|
||||||
onEditCommonConfig,
|
|
||||||
commonConfigError,
|
|
||||||
configError,
|
configError,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -117,40 +109,12 @@ export const CodexConfigSection: React.FC<CodexConfigSectionProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<label
|
||||||
<label
|
htmlFor="codexConfig"
|
||||||
htmlFor="codexConfig"
|
className="block text-sm font-medium text-foreground"
|
||||||
className="block text-sm font-medium text-foreground"
|
>
|
||||||
>
|
{t("codexConfig.configToml")}
|
||||||
{t("codexConfig.configToml")}
|
</label>
|
||||||
</label>
|
|
||||||
|
|
||||||
<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"
|
|
||||||
/>
|
|
||||||
{t("codexConfig.writeCommonConfig")}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-end">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onEditCommonConfig}
|
|
||||||
className="text-xs text-blue-500 dark:text-blue-400 hover:underline"
|
|
||||||
>
|
|
||||||
{t("codexConfig.editCommonConfig")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{commonConfigError && (
|
|
||||||
<p className="text-xs text-red-500 dark:text-red-400 text-right">
|
|
||||||
{commonConfigError}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<JsonEditor
|
<JsonEditor
|
||||||
value={value}
|
value={value}
|
||||||
|
|||||||
@@ -1,280 +0,0 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { useEffect, useState, useCallback, useMemo } from "react";
|
|
||||||
import { FullScreenPanel } from "@/components/common/FullScreenPanel";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Save, Download, Loader2 } from "lucide-react";
|
|
||||||
import JsonEditor from "@/components/JsonEditor";
|
|
||||||
|
|
||||||
interface CommonConfigEditorProps {
|
|
||||||
value: string;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
useCommonConfig: boolean;
|
|
||||||
onCommonConfigToggle: (checked: boolean) => void;
|
|
||||||
commonConfigSnippet: string;
|
|
||||||
onCommonConfigSnippetChange: (value: string) => void;
|
|
||||||
commonConfigError: string;
|
|
||||||
onEditClick: () => void;
|
|
||||||
isModalOpen: boolean;
|
|
||||||
onModalClose: () => void;
|
|
||||||
onExtract?: () => void;
|
|
||||||
isExtracting?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CommonConfigEditor({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
useCommonConfig,
|
|
||||||
onCommonConfigToggle,
|
|
||||||
commonConfigSnippet,
|
|
||||||
onCommonConfigSnippetChange,
|
|
||||||
commonConfigError,
|
|
||||||
onEditClick,
|
|
||||||
isModalOpen,
|
|
||||||
onModalClose,
|
|
||||||
onExtract,
|
|
||||||
isExtracting,
|
|
||||||
}: CommonConfigEditorProps) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [isDarkMode, setIsDarkMode] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsDarkMode(document.documentElement.classList.contains("dark"));
|
|
||||||
|
|
||||||
const observer = new MutationObserver(() => {
|
|
||||||
setIsDarkMode(document.documentElement.classList.contains("dark"));
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(document.documentElement, {
|
|
||||||
attributes: true,
|
|
||||||
attributeFilter: ["class"],
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => observer.disconnect();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Mirror value prop to local state so checkbox toggles and JsonEditor stay in sync
|
|
||||||
// (parent uses form.getValues which doesn't trigger re-renders)
|
|
||||||
const [localValue, setLocalValue] = useState(value);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setLocalValue(value);
|
|
||||||
}, [value]);
|
|
||||||
|
|
||||||
const handleLocalChange = useCallback(
|
|
||||||
(newValue: string) => {
|
|
||||||
setLocalValue(newValue);
|
|
||||||
onChange(newValue);
|
|
||||||
},
|
|
||||||
[onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
const toggleStates = useMemo(() => {
|
|
||||||
try {
|
|
||||||
const config = JSON.parse(localValue);
|
|
||||||
return {
|
|
||||||
hideAttribution:
|
|
||||||
config?.attribution?.commit === "" && config?.attribution?.pr === "",
|
|
||||||
alwaysThinking: config?.alwaysThinkingEnabled === true,
|
|
||||||
teammates:
|
|
||||||
config?.env?.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS === "1" ||
|
|
||||||
config?.env?.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS === 1,
|
|
||||||
};
|
|
||||||
} catch {
|
|
||||||
return {
|
|
||||||
hideAttribution: false,
|
|
||||||
alwaysThinking: false,
|
|
||||||
teammates: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [localValue]);
|
|
||||||
|
|
||||||
const handleToggle = useCallback(
|
|
||||||
(toggleKey: string, checked: boolean) => {
|
|
||||||
try {
|
|
||||||
const config = JSON.parse(localValue || "{}");
|
|
||||||
|
|
||||||
switch (toggleKey) {
|
|
||||||
case "hideAttribution":
|
|
||||||
if (checked) {
|
|
||||||
config.attribution = { commit: "", pr: "" };
|
|
||||||
} else {
|
|
||||||
delete config.attribution;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "alwaysThinking":
|
|
||||||
if (checked) {
|
|
||||||
config.alwaysThinkingEnabled = true;
|
|
||||||
} else {
|
|
||||||
delete config.alwaysThinkingEnabled;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "teammates":
|
|
||||||
if (!config.env) config.env = {};
|
|
||||||
if (checked) {
|
|
||||||
config.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = "1";
|
|
||||||
} else {
|
|
||||||
delete config.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS;
|
|
||||||
if (Object.keys(config.env).length === 0) delete config.env;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleLocalChange(JSON.stringify(config, null, 2));
|
|
||||||
} catch {
|
|
||||||
// Don't modify if JSON is invalid
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[localValue, handleLocalChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Label htmlFor="settingsConfig">{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"
|
|
||||||
id="useCommonConfig"
|
|
||||||
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("claudeConfig.writeCommonConfig", {
|
|
||||||
defaultValue: "写入通用配置",
|
|
||||||
})}
|
|
||||||
</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("claudeConfig.editCommonConfig", {
|
|
||||||
defaultValue: "编辑通用配置",
|
|
||||||
})}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{commonConfigError && !isModalOpen && (
|
|
||||||
<p className="text-xs text-red-500 dark:text-red-400 text-right">
|
|
||||||
{commonConfigError}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-1">
|
|
||||||
<label className="inline-flex items-center gap-2 text-sm text-muted-foreground cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={toggleStates.hideAttribution}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleToggle("hideAttribution", 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("claudeConfig.hideAttribution")}</span>
|
|
||||||
</label>
|
|
||||||
<label className="inline-flex items-center gap-2 text-sm text-muted-foreground cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={toggleStates.alwaysThinking}
|
|
||||||
onChange={(e) => handleToggle("alwaysThinking", 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("claudeConfig.alwaysThinking")}</span>
|
|
||||||
</label>
|
|
||||||
<label className="inline-flex items-center gap-2 text-sm text-muted-foreground cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={toggleStates.teammates}
|
|
||||||
onChange={(e) => handleToggle("teammates", 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("claudeConfig.enableTeammates")}</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<JsonEditor
|
|
||||||
value={localValue}
|
|
||||||
onChange={handleLocalChange}
|
|
||||||
placeholder={`{
|
|
||||||
"env": {
|
|
||||||
"ANTHROPIC_BASE_URL": "https://your-api-endpoint.com",
|
|
||||||
"ANTHROPIC_AUTH_TOKEN": "your-api-key-here"
|
|
||||||
}
|
|
||||||
}`}
|
|
||||||
darkMode={isDarkMode}
|
|
||||||
rows={14}
|
|
||||||
showValidation={true}
|
|
||||||
language="json"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FullScreenPanel
|
|
||||||
isOpen={isModalOpen}
|
|
||||||
title={t("claudeConfig.editCommonConfigTitle", {
|
|
||||||
defaultValue: "编辑通用配置片段",
|
|
||||||
})}
|
|
||||||
onClose={onModalClose}
|
|
||||||
footer={
|
|
||||||
<>
|
|
||||||
{onExtract && (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={onExtract}
|
|
||||||
disabled={isExtracting}
|
|
||||||
className="gap-2"
|
|
||||||
>
|
|
||||||
{isExtracting ? (
|
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Download className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
{t("claudeConfig.extractFromCurrent", {
|
|
||||||
defaultValue: "从编辑内容提取",
|
|
||||||
})}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button type="button" variant="outline" onClick={onModalClose}>
|
|
||||||
{t("common.cancel")}
|
|
||||||
</Button>
|
|
||||||
<Button type="button" onClick={onModalClose} className="gap-2">
|
|
||||||
<Save className="w-4 h-4" />
|
|
||||||
{t("common.save")}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{t("claudeConfig.commonConfigHint", {
|
|
||||||
defaultValue: "通用配置片段将合并到所有启用它的供应商配置中",
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
<JsonEditor
|
|
||||||
value={commonConfigSnippet}
|
|
||||||
onChange={onCommonConfigSnippetChange}
|
|
||||||
placeholder={`{
|
|
||||||
"env": {
|
|
||||||
"ANTHROPIC_BASE_URL": "https://your-api-endpoint.com"
|
|
||||||
}
|
|
||||||
}`}
|
|
||||||
darkMode={isDarkMode}
|
|
||||||
rows={16}
|
|
||||||
showValidation={true}
|
|
||||||
language="json"
|
|
||||||
/>
|
|
||||||
{commonConfigError && (
|
|
||||||
<p className="text-sm text-red-500 dark:text-red-400">
|
|
||||||
{commonConfigError}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</FullScreenPanel>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { Save, Download, Loader2 } from "lucide-react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { FullScreenPanel } from "@/components/common/FullScreenPanel";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import JsonEditor from "@/components/JsonEditor";
|
|
||||||
|
|
||||||
interface GeminiCommonConfigModalProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
value: string;
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
error?: string;
|
|
||||||
onExtract?: () => void;
|
|
||||||
isExtracting?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GeminiCommonConfigModal - Common Gemini configuration editor modal
|
|
||||||
* Allows editing of common env snippet shared across Gemini providers
|
|
||||||
*/
|
|
||||||
export const GeminiCommonConfigModal: React.FC<
|
|
||||||
GeminiCommonConfigModalProps
|
|
||||||
> = ({ isOpen, onClose, value, onChange, error, onExtract, isExtracting }) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [isDarkMode, setIsDarkMode] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsDarkMode(document.documentElement.classList.contains("dark"));
|
|
||||||
|
|
||||||
const observer = new MutationObserver(() => {
|
|
||||||
setIsDarkMode(document.documentElement.classList.contains("dark"));
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(document.documentElement, {
|
|
||||||
attributes: true,
|
|
||||||
attributeFilter: ["class"],
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => observer.disconnect();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FullScreenPanel
|
|
||||||
isOpen={isOpen}
|
|
||||||
title={t("geminiConfig.editCommonConfigTitle", {
|
|
||||||
defaultValue: "编辑 Gemini 通用配置片段",
|
|
||||||
})}
|
|
||||||
onClose={onClose}
|
|
||||||
footer={
|
|
||||||
<>
|
|
||||||
{onExtract && (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={onExtract}
|
|
||||||
disabled={isExtracting}
|
|
||||||
className="gap-2"
|
|
||||||
>
|
|
||||||
{isExtracting ? (
|
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Download className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
{t("geminiConfig.extractFromCurrent", {
|
|
||||||
defaultValue: "从编辑内容提取",
|
|
||||||
})}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button type="button" variant="outline" onClick={onClose}>
|
|
||||||
{t("common.cancel")}
|
|
||||||
</Button>
|
|
||||||
<Button type="button" onClick={onClose} className="gap-2">
|
|
||||||
<Save className="w-4 h-4" />
|
|
||||||
{t("common.save")}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{t("geminiConfig.commonConfigHint", {
|
|
||||||
defaultValue:
|
|
||||||
"该片段会写入 Gemini 的 .env(不允许包含 GOOGLE_GEMINI_BASE_URL、GEMINI_API_KEY)",
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<JsonEditor
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
placeholder={`{
|
|
||||||
"GEMINI_MODEL": "gemini-3-pro-preview"
|
|
||||||
}`}
|
|
||||||
darkMode={isDarkMode}
|
|
||||||
rows={16}
|
|
||||||
showValidation={true}
|
|
||||||
language="json"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<p className="text-sm text-red-500 dark:text-red-400">{error}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</FullScreenPanel>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React from "react";
|
||||||
import { GeminiEnvSection, GeminiConfigSection } from "./GeminiConfigSections";
|
import { GeminiEnvSection, GeminiConfigSection } from "./GeminiConfigSections";
|
||||||
import { GeminiCommonConfigModal } from "./GeminiCommonConfigModal";
|
|
||||||
|
|
||||||
interface GeminiConfigEditorProps {
|
interface GeminiConfigEditorProps {
|
||||||
envValue: string;
|
envValue: string;
|
||||||
@@ -8,15 +7,8 @@ interface GeminiConfigEditorProps {
|
|||||||
onEnvChange: (value: string) => void;
|
onEnvChange: (value: string) => void;
|
||||||
onConfigChange: (value: string) => void;
|
onConfigChange: (value: string) => void;
|
||||||
onEnvBlur?: () => void;
|
onEnvBlur?: () => void;
|
||||||
useCommonConfig: boolean;
|
|
||||||
onCommonConfigToggle: (checked: boolean) => void;
|
|
||||||
commonConfigSnippet: string;
|
|
||||||
onCommonConfigSnippetChange: (value: string) => void;
|
|
||||||
commonConfigError: string;
|
|
||||||
envError: string;
|
envError: string;
|
||||||
configError: string;
|
configError: string;
|
||||||
onExtract?: () => void;
|
|
||||||
isExtracting?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const GeminiConfigEditor: React.FC<GeminiConfigEditorProps> = ({
|
const GeminiConfigEditor: React.FC<GeminiConfigEditorProps> = ({
|
||||||
@@ -25,25 +17,9 @@ const GeminiConfigEditor: React.FC<GeminiConfigEditorProps> = ({
|
|||||||
onEnvChange,
|
onEnvChange,
|
||||||
onConfigChange,
|
onConfigChange,
|
||||||
onEnvBlur,
|
onEnvBlur,
|
||||||
useCommonConfig,
|
|
||||||
onCommonConfigToggle,
|
|
||||||
commonConfigSnippet,
|
|
||||||
onCommonConfigSnippetChange,
|
|
||||||
commonConfigError,
|
|
||||||
envError,
|
envError,
|
||||||
configError,
|
configError,
|
||||||
onExtract,
|
|
||||||
isExtracting,
|
|
||||||
}) => {
|
}) => {
|
||||||
const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false);
|
|
||||||
|
|
||||||
// Auto-open common config modal if there's an error
|
|
||||||
useEffect(() => {
|
|
||||||
if (commonConfigError && !isCommonConfigModalOpen) {
|
|
||||||
setIsCommonConfigModalOpen(true);
|
|
||||||
}
|
|
||||||
}, [commonConfigError, isCommonConfigModalOpen]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Env Section */}
|
{/* Env Section */}
|
||||||
@@ -52,10 +28,6 @@ const GeminiConfigEditor: React.FC<GeminiConfigEditorProps> = ({
|
|||||||
onChange={onEnvChange}
|
onChange={onEnvChange}
|
||||||
onBlur={onEnvBlur}
|
onBlur={onEnvBlur}
|
||||||
error={envError}
|
error={envError}
|
||||||
useCommonConfig={useCommonConfig}
|
|
||||||
onCommonConfigToggle={onCommonConfigToggle}
|
|
||||||
onEditCommonConfig={() => setIsCommonConfigModalOpen(true)}
|
|
||||||
commonConfigError={commonConfigError}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Config JSON Section */}
|
{/* Config JSON Section */}
|
||||||
@@ -64,17 +36,6 @@ const GeminiConfigEditor: React.FC<GeminiConfigEditorProps> = ({
|
|||||||
onChange={onConfigChange}
|
onChange={onConfigChange}
|
||||||
configError={configError}
|
configError={configError}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Common Config Modal */}
|
|
||||||
<GeminiCommonConfigModal
|
|
||||||
isOpen={isCommonConfigModalOpen}
|
|
||||||
onClose={() => setIsCommonConfigModalOpen(false)}
|
|
||||||
value={commonConfigSnippet}
|
|
||||||
onChange={onCommonConfigSnippetChange}
|
|
||||||
error={commonConfigError}
|
|
||||||
onExtract={onExtract}
|
|
||||||
isExtracting={isExtracting}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,10 +7,6 @@ interface GeminiEnvSectionProps {
|
|||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
onBlur?: () => void;
|
onBlur?: () => void;
|
||||||
error?: string;
|
error?: string;
|
||||||
useCommonConfig: boolean;
|
|
||||||
onCommonConfigToggle: (checked: boolean) => void;
|
|
||||||
onEditCommonConfig: () => void;
|
|
||||||
commonConfigError?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,10 +17,6 @@ export const GeminiEnvSection: React.FC<GeminiEnvSectionProps> = ({
|
|||||||
onChange,
|
onChange,
|
||||||
onBlur,
|
onBlur,
|
||||||
error,
|
error,
|
||||||
useCommonConfig,
|
|
||||||
onCommonConfigToggle,
|
|
||||||
onEditCommonConfig,
|
|
||||||
commonConfigError,
|
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isDarkMode, setIsDarkMode] = useState(false);
|
const [isDarkMode, setIsDarkMode] = useState(false);
|
||||||
@@ -53,44 +45,12 @@ export const GeminiEnvSection: React.FC<GeminiEnvSectionProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<label
|
||||||
<label
|
htmlFor="geminiEnv"
|
||||||
htmlFor="geminiEnv"
|
className="block text-sm font-medium text-foreground"
|
||||||
className="block text-sm font-medium text-foreground"
|
>
|
||||||
>
|
{t("geminiConfig.envFile", { defaultValue: "环境变量 (.env)" })}
|
||||||
{t("geminiConfig.envFile", { defaultValue: "环境变量 (.env)" })}
|
</label>
|
||||||
</label>
|
|
||||||
|
|
||||||
<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"
|
|
||||||
/>
|
|
||||||
{t("geminiConfig.writeCommonConfig", {
|
|
||||||
defaultValue: "写入通用配置",
|
|
||||||
})}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-end">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onEditCommonConfig}
|
|
||||||
className="text-xs text-blue-500 dark:text-blue-400 hover:underline"
|
|
||||||
>
|
|
||||||
{t("geminiConfig.editCommonConfig", {
|
|
||||||
defaultValue: "编辑通用配置",
|
|
||||||
})}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{commonConfigError && (
|
|
||||||
<p className="text-xs text-red-500 dark:text-red-400 text-right">
|
|
||||||
{commonConfigError}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<JsonEditor
|
<JsonEditor
|
||||||
value={value}
|
value={value}
|
||||||
|
|||||||
@@ -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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let filledCount = 0;
|
||||||
|
let alreadySetCount = 0;
|
||||||
|
let unmatchedCount = 0;
|
||||||
|
|
||||||
const updatedAgents = { ...agents };
|
const updatedAgents = { ...agents };
|
||||||
for (const agentDef of builtinAgentDefs) {
|
for (const agentDef of builtinAgentDefs) {
|
||||||
const recommendedValue = resolveRecommendedModel(agentDef.recommended);
|
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] = {
|
||||||
...updatedAgents[agentDef.key],
|
...updatedAgents[agentDef.key],
|
||||||
model: recommendedValue,
|
model: recommendedValue,
|
||||||
};
|
};
|
||||||
|
filledCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onAgentsChange(updatedAgents);
|
onAgentsChange(updatedAgents);
|
||||||
@@ -738,15 +747,50 @@ export function OmoFormFields({
|
|||||||
const updatedCategories = { ...categories };
|
const updatedCategories = { ...categories };
|
||||||
for (const catDef of OMO_BUILTIN_CATEGORIES) {
|
for (const catDef of OMO_BUILTIN_CATEGORIES) {
|
||||||
const recommendedValue = resolveRecommendedModel(catDef.recommended);
|
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] = {
|
||||||
...updatedCategories[catDef.key],
|
...updatedCategories[catDef.key],
|
||||||
model: recommendedValue,
|
model: recommendedValue,
|
||||||
};
|
};
|
||||||
|
filledCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onCategoriesChange(updatedCategories);
|
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;
|
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>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -14,6 +14,7 @@ import type {
|
|||||||
ProviderTestConfig,
|
ProviderTestConfig,
|
||||||
ProviderProxyConfig,
|
ProviderProxyConfig,
|
||||||
ClaudeApiFormat,
|
ClaudeApiFormat,
|
||||||
|
ClaudeApiKeyField,
|
||||||
} from "@/types";
|
} from "@/types";
|
||||||
import {
|
import {
|
||||||
providerPresets,
|
providerPresets,
|
||||||
@@ -39,13 +40,16 @@ import {
|
|||||||
import { OpenCodeFormFields } from "./OpenCodeFormFields";
|
import { OpenCodeFormFields } from "./OpenCodeFormFields";
|
||||||
import { OpenClawFormFields } from "./OpenClawFormFields";
|
import { OpenClawFormFields } from "./OpenClawFormFields";
|
||||||
import type { UniversalProviderPreset } from "@/config/universalProviderPresets";
|
import type { UniversalProviderPreset } from "@/config/universalProviderPresets";
|
||||||
import { applyTemplateValues } from "@/utils/providerConfigUtils";
|
import {
|
||||||
|
applyTemplateValues,
|
||||||
|
hasApiKeyField,
|
||||||
|
} from "@/utils/providerConfigUtils";
|
||||||
import { mergeProviderMeta } from "@/utils/providerMetaUtils";
|
import { mergeProviderMeta } from "@/utils/providerMetaUtils";
|
||||||
import { getCodexCustomTemplate } from "@/config/codexTemplates";
|
import { getCodexCustomTemplate } from "@/config/codexTemplates";
|
||||||
import CodexConfigEditor from "./CodexConfigEditor";
|
import CodexConfigEditor from "./CodexConfigEditor";
|
||||||
import { CommonConfigEditor } from "./CommonConfigEditor";
|
|
||||||
import GeminiConfigEditor from "./GeminiConfigEditor";
|
import GeminiConfigEditor from "./GeminiConfigEditor";
|
||||||
import JsonEditor from "@/components/JsonEditor";
|
import JsonEditor from "@/components/JsonEditor";
|
||||||
|
import { ClaudeQuickToggles, jsonMergePatch } from "./ClaudeQuickToggles";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { ProviderPresetSelector } from "./ProviderPresetSelector";
|
import { ProviderPresetSelector } from "./ProviderPresetSelector";
|
||||||
import { BasicFormFields } from "./BasicFormFields";
|
import { BasicFormFields } from "./BasicFormFields";
|
||||||
@@ -53,7 +57,6 @@ import { ClaudeFormFields } from "./ClaudeFormFields";
|
|||||||
import { CodexFormFields } from "./CodexFormFields";
|
import { CodexFormFields } from "./CodexFormFields";
|
||||||
import { GeminiFormFields } from "./GeminiFormFields";
|
import { GeminiFormFields } from "./GeminiFormFields";
|
||||||
import { OmoFormFields } from "./OmoFormFields";
|
import { OmoFormFields } from "./OmoFormFields";
|
||||||
import { OmoCommonConfigEditor } from "./OmoCommonConfigEditor";
|
|
||||||
import { parseOmoOtherFieldsObject } from "@/types/omo";
|
import { parseOmoOtherFieldsObject } from "@/types/omo";
|
||||||
import {
|
import {
|
||||||
ProviderAdvancedConfig,
|
ProviderAdvancedConfig,
|
||||||
@@ -67,18 +70,14 @@ import {
|
|||||||
useCodexConfigState,
|
useCodexConfigState,
|
||||||
useApiKeyLink,
|
useApiKeyLink,
|
||||||
useTemplateValues,
|
useTemplateValues,
|
||||||
useCommonConfigSnippet,
|
|
||||||
useCodexCommonConfig,
|
|
||||||
useSpeedTestEndpoints,
|
useSpeedTestEndpoints,
|
||||||
useCodexTomlValidation,
|
useCodexTomlValidation,
|
||||||
useGeminiConfigState,
|
useGeminiConfigState,
|
||||||
useGeminiCommonConfig,
|
|
||||||
useOmoModelSource,
|
useOmoModelSource,
|
||||||
useOpencodeFormState,
|
useOpencodeFormState,
|
||||||
useOmoDraftState,
|
useOmoDraftState,
|
||||||
useOpenclawFormState,
|
useOpenclawFormState,
|
||||||
} from "./hooks";
|
} from "./hooks";
|
||||||
import { useOmoGlobalConfig, useOmoSlimGlobalConfig } from "@/lib/query/omo";
|
|
||||||
import {
|
import {
|
||||||
CLAUDE_DEFAULT_CONFIG,
|
CLAUDE_DEFAULT_CONFIG,
|
||||||
CODEX_DEFAULT_CONFIG,
|
CODEX_DEFAULT_CONFIG,
|
||||||
@@ -117,6 +116,7 @@ interface ProviderFormProps {
|
|||||||
iconColor?: string;
|
iconColor?: string;
|
||||||
};
|
};
|
||||||
showButtons?: boolean;
|
showButtons?: boolean;
|
||||||
|
isCurrent?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProviderForm({
|
export function ProviderForm({
|
||||||
@@ -129,6 +129,7 @@ export function ProviderForm({
|
|||||||
onManageUniversalProviders,
|
onManageUniversalProviders,
|
||||||
initialData,
|
initialData,
|
||||||
showButtons = true,
|
showButtons = true,
|
||||||
|
isCurrent = false,
|
||||||
}: ProviderFormProps) {
|
}: ProviderFormProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const isEditMode = Boolean(initialData);
|
const isEditMode = Boolean(initialData);
|
||||||
@@ -186,13 +187,6 @@ export function ProviderForm({
|
|||||||
const isOmoCategory = appId === "opencode" && category === "omo";
|
const isOmoCategory = appId === "opencode" && category === "omo";
|
||||||
const isOmoSlimCategory = appId === "opencode" && category === "omo-slim";
|
const isOmoSlimCategory = appId === "opencode" && category === "omo-slim";
|
||||||
const isAnyOmoCategory = isOmoCategory || isOmoSlimCategory;
|
const isAnyOmoCategory = isOmoCategory || isOmoSlimCategory;
|
||||||
const { data: queriedStandardOmoGlobalConfig } =
|
|
||||||
useOmoGlobalConfig(isOmoCategory);
|
|
||||||
const { data: queriedSlimOmoGlobalConfig } =
|
|
||||||
useOmoSlimGlobalConfig(isOmoSlimCategory);
|
|
||||||
const queriedOmoGlobalConfig = isOmoSlimCategory
|
|
||||||
? queriedSlimOmoGlobalConfig
|
|
||||||
: queriedStandardOmoGlobalConfig;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedPresetId(initialData ? null : "custom");
|
setSelectedPresetId(initialData ? null : "custom");
|
||||||
@@ -243,6 +237,55 @@ export function ProviderForm({
|
|||||||
mode: "onSubmit",
|
mode: "onSubmit",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [localApiFormat, setLocalApiFormat] = useState<ClaudeApiFormat>(() => {
|
||||||
|
if (appId !== "claude") return "anthropic";
|
||||||
|
return initialData?.meta?.apiFormat ?? "anthropic";
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleApiFormatChange = useCallback((format: ClaudeApiFormat) => {
|
||||||
|
setLocalApiFormat(format);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const [localApiKeyField, setLocalApiKeyField] = useState<ClaudeApiKeyField>(
|
||||||
|
() => {
|
||||||
|
if (appId !== "claude") return "ANTHROPIC_AUTH_TOKEN";
|
||||||
|
if (initialData?.meta?.apiKeyField) return initialData.meta.apiKeyField;
|
||||||
|
try {
|
||||||
|
const config = initialData?.settingsConfig;
|
||||||
|
if (
|
||||||
|
config?.env &&
|
||||||
|
(config.env as Record<string, unknown>).ANTHROPIC_API_KEY !==
|
||||||
|
undefined
|
||||||
|
)
|
||||||
|
return "ANTHROPIC_API_KEY";
|
||||||
|
} catch {}
|
||||||
|
return "ANTHROPIC_AUTH_TOKEN";
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleApiKeyFieldChange = useCallback(
|
||||||
|
(field: ClaudeApiKeyField) => {
|
||||||
|
setLocalApiKeyField(field);
|
||||||
|
try {
|
||||||
|
const config = JSON.parse(form.getValues("settingsConfig") || "{}") as {
|
||||||
|
env?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
const env = (config.env ?? {}) as Record<string, unknown>;
|
||||||
|
const oldField =
|
||||||
|
field === "ANTHROPIC_API_KEY"
|
||||||
|
? "ANTHROPIC_AUTH_TOKEN"
|
||||||
|
: "ANTHROPIC_API_KEY";
|
||||||
|
if (oldField in env) {
|
||||||
|
env[field] = env[oldField];
|
||||||
|
delete env[oldField];
|
||||||
|
config.env = env;
|
||||||
|
form.setValue("settingsConfig", JSON.stringify(config, null, 2));
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
},
|
||||||
|
[form],
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
apiKey,
|
apiKey,
|
||||||
handleApiKeyChange,
|
handleApiKeyChange,
|
||||||
@@ -253,6 +296,7 @@ export function ProviderForm({
|
|||||||
selectedPresetId,
|
selectedPresetId,
|
||||||
category,
|
category,
|
||||||
appType: appId,
|
appType: appId,
|
||||||
|
apiKeyField: appId === "claude" ? localApiKeyField : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { baseUrl, handleClaudeBaseUrlChange } = useBaseUrlState({
|
const { baseUrl, handleClaudeBaseUrlChange } = useBaseUrlState({
|
||||||
@@ -276,15 +320,6 @@ export function ProviderForm({
|
|||||||
onConfigChange: (config) => form.setValue("settingsConfig", config),
|
onConfigChange: (config) => form.setValue("settingsConfig", config),
|
||||||
});
|
});
|
||||||
|
|
||||||
const [localApiFormat, setLocalApiFormat] = useState<ClaudeApiFormat>(() => {
|
|
||||||
if (appId !== "claude") return "anthropic";
|
|
||||||
return initialData?.meta?.apiFormat ?? "anthropic";
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleApiFormatChange = useCallback((format: ClaudeApiFormat) => {
|
|
||||||
setLocalApiFormat(format);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
codexAuth,
|
codexAuth,
|
||||||
codexConfig,
|
codexConfig,
|
||||||
@@ -382,37 +417,6 @@ export function ProviderForm({
|
|||||||
onConfigChange: (config) => form.setValue("settingsConfig", config),
|
onConfigChange: (config) => form.setValue("settingsConfig", config),
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
|
||||||
useCommonConfig,
|
|
||||||
commonConfigSnippet,
|
|
||||||
commonConfigError,
|
|
||||||
handleCommonConfigToggle,
|
|
||||||
handleCommonConfigSnippetChange,
|
|
||||||
isExtracting: isClaudeExtracting,
|
|
||||||
handleExtract: handleClaudeExtract,
|
|
||||||
} = useCommonConfigSnippet({
|
|
||||||
settingsConfig: form.getValues("settingsConfig"),
|
|
||||||
onConfigChange: (config) => form.setValue("settingsConfig", config),
|
|
||||||
initialData: appId === "claude" ? initialData : undefined,
|
|
||||||
selectedPresetId: selectedPresetId ?? undefined,
|
|
||||||
enabled: appId === "claude",
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
useCommonConfig: useCodexCommonConfigFlag,
|
|
||||||
commonConfigSnippet: codexCommonConfigSnippet,
|
|
||||||
commonConfigError: codexCommonConfigError,
|
|
||||||
handleCommonConfigToggle: handleCodexCommonConfigToggle,
|
|
||||||
handleCommonConfigSnippetChange: handleCodexCommonConfigSnippetChange,
|
|
||||||
isExtracting: isCodexExtracting,
|
|
||||||
handleExtract: handleCodexExtract,
|
|
||||||
} = useCodexCommonConfig({
|
|
||||||
codexConfig,
|
|
||||||
onConfigChange: handleCodexConfigChange,
|
|
||||||
initialData: appId === "codex" ? initialData : undefined,
|
|
||||||
selectedPresetId: selectedPresetId ?? undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
geminiEnv,
|
geminiEnv,
|
||||||
geminiConfig,
|
geminiConfig,
|
||||||
@@ -428,7 +432,6 @@ export function ProviderForm({
|
|||||||
handleGeminiConfigChange,
|
handleGeminiConfigChange,
|
||||||
resetGeminiConfig,
|
resetGeminiConfig,
|
||||||
envStringToObj,
|
envStringToObj,
|
||||||
envObjToString,
|
|
||||||
} = useGeminiConfigState({
|
} = useGeminiConfigState({
|
||||||
initialData: appId === "gemini" ? initialData : undefined,
|
initialData: appId === "gemini" ? initialData : undefined,
|
||||||
});
|
});
|
||||||
@@ -479,23 +482,6 @@ export function ProviderForm({
|
|||||||
[originalHandleGeminiModelChange, updateGeminiEnvField],
|
[originalHandleGeminiModelChange, updateGeminiEnvField],
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
|
||||||
useCommonConfig: useGeminiCommonConfigFlag,
|
|
||||||
commonConfigSnippet: geminiCommonConfigSnippet,
|
|
||||||
commonConfigError: geminiCommonConfigError,
|
|
||||||
handleCommonConfigToggle: handleGeminiCommonConfigToggle,
|
|
||||||
handleCommonConfigSnippetChange: handleGeminiCommonConfigSnippetChange,
|
|
||||||
isExtracting: isGeminiExtracting,
|
|
||||||
handleExtract: handleGeminiExtract,
|
|
||||||
} = useGeminiCommonConfig({
|
|
||||||
envValue: geminiEnv,
|
|
||||||
onEnvChange: handleGeminiEnvChange,
|
|
||||||
envStringToObj,
|
|
||||||
envObjToString,
|
|
||||||
initialData: appId === "gemini" ? initialData : undefined,
|
|
||||||
selectedPresetId: selectedPresetId ?? undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Extracted hooks: OpenCode / OMO / OpenClaw ─────────────────────
|
// ── Extracted hooks: OpenCode / OMO / OpenClaw ─────────────────────
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -521,7 +507,6 @@ export function ProviderForm({
|
|||||||
|
|
||||||
const omoDraft = useOmoDraftState({
|
const omoDraft = useOmoDraftState({
|
||||||
initialOmoSettings,
|
initialOmoSettings,
|
||||||
queriedOmoGlobalConfig,
|
|
||||||
isEditMode,
|
isEditMode,
|
||||||
appId,
|
appId,
|
||||||
category,
|
category,
|
||||||
@@ -535,8 +520,6 @@ export function ProviderForm({
|
|||||||
getSettingsConfig: () => form.getValues("settingsConfig"),
|
getSettingsConfig: () => form.getValues("settingsConfig"),
|
||||||
});
|
});
|
||||||
|
|
||||||
const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false);
|
|
||||||
|
|
||||||
const handleSubmit = (values: ProviderFormData) => {
|
const handleSubmit = (values: ProviderFormData) => {
|
||||||
if (appId === "claude" && templateValueEntries.length > 0) {
|
if (appId === "claude" && templateValueEntries.length > 0) {
|
||||||
const validation = validateTemplateValues();
|
const validation = validateTemplateValues();
|
||||||
@@ -560,7 +543,7 @@ export function ProviderForm({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appId === "opencode" && category !== "omo") {
|
if (appId === "opencode" && !isAnyOmoCategory) {
|
||||||
const keyPattern = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
const keyPattern = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
||||||
if (!opencodeForm.opencodeProviderKey.trim()) {
|
if (!opencodeForm.opencodeProviderKey.trim()) {
|
||||||
toast.error(t("opencode.providerKeyRequired"));
|
toast.error(t("opencode.providerKeyRequired"));
|
||||||
@@ -606,7 +589,8 @@ export function ProviderForm({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 非官方供应商必填校验:端点和 API Key
|
// 非官方供应商必填校验:端点和 API Key
|
||||||
if (category !== "official") {
|
// cloud_provider(如 Bedrock)通过模板变量处理认证,跳过通用校验
|
||||||
|
if (category !== "official" && category !== "cloud_provider") {
|
||||||
if (appId === "claude") {
|
if (appId === "claude") {
|
||||||
if (!baseUrl.trim()) {
|
if (!baseUrl.trim()) {
|
||||||
toast.error(
|
toast.error(
|
||||||
@@ -691,7 +675,6 @@ export function ProviderForm({
|
|||||||
(category === "omo" || category === "omo-slim")
|
(category === "omo" || category === "omo-slim")
|
||||||
) {
|
) {
|
||||||
const omoConfig: Record<string, unknown> = {};
|
const omoConfig: Record<string, unknown> = {};
|
||||||
omoConfig.useCommonConfig = omoDraft.useOmoCommonConfig;
|
|
||||||
if (Object.keys(omoDraft.omoAgents).length > 0) {
|
if (Object.keys(omoDraft.omoAgents).length > 0) {
|
||||||
omoConfig.agents = omoDraft.omoAgents;
|
omoConfig.agents = omoDraft.omoAgents;
|
||||||
}
|
}
|
||||||
@@ -740,9 +723,10 @@ export function ProviderForm({
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (appId === "opencode") {
|
if (appId === "opencode") {
|
||||||
if (category === "omo") {
|
if (isAnyOmoCategory) {
|
||||||
if (!isEditMode) {
|
if (!isEditMode) {
|
||||||
payload.providerKey = `omo-${crypto.randomUUID().slice(0, 8)}`;
|
const prefix = category === "omo" ? "omo" : "omo-slim";
|
||||||
|
payload.providerKey = `${prefix}-${crypto.randomUUID().slice(0, 8)}`;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
payload.providerKey = opencodeForm.opencodeProviderKey;
|
payload.providerKey = opencodeForm.opencodeProviderKey;
|
||||||
@@ -751,8 +735,8 @@ export function ProviderForm({
|
|||||||
payload.providerKey = openclawForm.openclawProviderKey;
|
payload.providerKey = openclawForm.openclawProviderKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (category === "omo" && !payload.presetCategory) {
|
if (isAnyOmoCategory && !payload.presetCategory) {
|
||||||
payload.presetCategory = "omo";
|
payload.presetCategory = category;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activePreset) {
|
if (activePreset) {
|
||||||
@@ -829,6 +813,10 @@ export function ProviderForm({
|
|||||||
appId === "claude" && category !== "official"
|
appId === "claude" && category !== "official"
|
||||||
? localApiFormat
|
? localApiFormat
|
||||||
: undefined,
|
: undefined,
|
||||||
|
apiKeyField:
|
||||||
|
appId === "claude" && category !== "official"
|
||||||
|
? localApiKeyField
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
onSubmit(payload);
|
onSubmit(payload);
|
||||||
@@ -851,7 +839,8 @@ export function ProviderForm({
|
|||||||
);
|
);
|
||||||
}, [groupedPresets]);
|
}, [groupedPresets]);
|
||||||
|
|
||||||
const shouldShowSpeedTest = category !== "official";
|
const shouldShowSpeedTest =
|
||||||
|
category !== "official" && category !== "cloud_provider";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
shouldShowApiKeyLink: shouldShowClaudeApiKeyLink,
|
shouldShowApiKeyLink: shouldShowClaudeApiKeyLink,
|
||||||
@@ -1003,10 +992,10 @@ export function ProviderForm({
|
|||||||
const preset = entry.preset as OpenCodeProviderPreset;
|
const preset = entry.preset as OpenCodeProviderPreset;
|
||||||
const config = preset.settingsConfig;
|
const config = preset.settingsConfig;
|
||||||
|
|
||||||
if (preset.category === "omo") {
|
if (preset.category === "omo" || preset.category === "omo-slim") {
|
||||||
omoDraft.resetOmoDraftState();
|
omoDraft.resetOmoDraftState();
|
||||||
form.reset({
|
form.reset({
|
||||||
name: "OMO",
|
name: preset.category === "omo" ? "OMO" : "OMO Slim",
|
||||||
websiteUrl: preset.websiteUrl ?? "",
|
websiteUrl: preset.websiteUrl ?? "",
|
||||||
settingsConfig: JSON.stringify({}, null, 2),
|
settingsConfig: JSON.stringify({}, null, 2),
|
||||||
icon: preset.icon ?? "",
|
icon: preset.icon ?? "",
|
||||||
@@ -1066,6 +1055,12 @@ export function ProviderForm({
|
|||||||
setLocalApiFormat("anthropic");
|
setLocalApiFormat("anthropic");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (preset.apiKeyField) {
|
||||||
|
setLocalApiKeyField(preset.apiKeyField);
|
||||||
|
} else {
|
||||||
|
setLocalApiKeyField("ANTHROPIC_AUTH_TOKEN");
|
||||||
|
}
|
||||||
|
|
||||||
form.reset({
|
form.reset({
|
||||||
name: preset.name,
|
name: preset.name,
|
||||||
websiteUrl: preset.websiteUrl ?? "",
|
websiteUrl: preset.websiteUrl ?? "",
|
||||||
@@ -1110,7 +1105,7 @@ export function ProviderForm({
|
|||||||
<BasicFormFields
|
<BasicFormFields
|
||||||
form={form}
|
form={form}
|
||||||
beforeNameSlot={
|
beforeNameSlot={
|
||||||
appId === "opencode" && category !== "omo" ? (
|
appId === "opencode" && !isAnyOmoCategory ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="opencode-key">
|
<Label htmlFor="opencode-key">
|
||||||
{t("opencode.providerKey")}
|
{t("opencode.providerKey")}
|
||||||
@@ -1235,10 +1230,11 @@ export function ProviderForm({
|
|||||||
{appId === "claude" && (
|
{appId === "claude" && (
|
||||||
<ClaudeFormFields
|
<ClaudeFormFields
|
||||||
providerId={providerId}
|
providerId={providerId}
|
||||||
shouldShowApiKey={shouldShowApiKey(
|
shouldShowApiKey={
|
||||||
form.getValues("settingsConfig"),
|
(category !== "cloud_provider" ||
|
||||||
isEditMode,
|
hasApiKeyField(form.getValues("settingsConfig"), "claude")) &&
|
||||||
)}
|
shouldShowApiKey(form.getValues("settingsConfig"), isEditMode)
|
||||||
|
}
|
||||||
apiKey={apiKey}
|
apiKey={apiKey}
|
||||||
onApiKeyChange={handleApiKeyChange}
|
onApiKeyChange={handleApiKeyChange}
|
||||||
category={category}
|
category={category}
|
||||||
@@ -1270,6 +1266,8 @@ export function ProviderForm({
|
|||||||
speedTestEndpoints={speedTestEndpoints}
|
speedTestEndpoints={speedTestEndpoints}
|
||||||
apiFormat={localApiFormat}
|
apiFormat={localApiFormat}
|
||||||
onApiFormatChange={handleApiFormatChange}
|
onApiFormatChange={handleApiFormatChange}
|
||||||
|
apiKeyField={localApiKeyField}
|
||||||
|
onApiKeyFieldChange={handleApiKeyFieldChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1329,7 +1327,7 @@ export function ProviderForm({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{appId === "opencode" && category !== "omo" && (
|
{appId === "opencode" && !isAnyOmoCategory && (
|
||||||
<OpenCodeFormFields
|
<OpenCodeFormFields
|
||||||
npm={opencodeForm.opencodeNpm}
|
npm={opencodeForm.opencodeNpm}
|
||||||
onNpmChange={opencodeForm.handleOpencodeNpmChange}
|
onNpmChange={opencodeForm.handleOpencodeNpmChange}
|
||||||
@@ -1396,15 +1394,8 @@ export function ProviderForm({
|
|||||||
configValue={codexConfig}
|
configValue={codexConfig}
|
||||||
onAuthChange={setCodexAuth}
|
onAuthChange={setCodexAuth}
|
||||||
onConfigChange={handleCodexConfigChange}
|
onConfigChange={handleCodexConfigChange}
|
||||||
useCommonConfig={useCodexCommonConfigFlag}
|
|
||||||
onCommonConfigToggle={handleCodexCommonConfigToggle}
|
|
||||||
commonConfigSnippet={codexCommonConfigSnippet}
|
|
||||||
onCommonConfigSnippetChange={handleCodexCommonConfigSnippetChange}
|
|
||||||
commonConfigError={codexCommonConfigError}
|
|
||||||
authError={codexAuthError}
|
authError={codexAuthError}
|
||||||
configError={codexConfigError}
|
configError={codexConfigError}
|
||||||
onExtract={handleCodexExtract}
|
|
||||||
isExtracting={isCodexExtracting}
|
|
||||||
/>
|
/>
|
||||||
{settingsConfigErrorField}
|
{settingsConfigErrorField}
|
||||||
</>
|
</>
|
||||||
@@ -1415,36 +1406,23 @@ export function ProviderForm({
|
|||||||
configValue={geminiConfig}
|
configValue={geminiConfig}
|
||||||
onEnvChange={handleGeminiEnvChange}
|
onEnvChange={handleGeminiEnvChange}
|
||||||
onConfigChange={handleGeminiConfigChange}
|
onConfigChange={handleGeminiConfigChange}
|
||||||
useCommonConfig={useGeminiCommonConfigFlag}
|
|
||||||
onCommonConfigToggle={handleGeminiCommonConfigToggle}
|
|
||||||
commonConfigSnippet={geminiCommonConfigSnippet}
|
|
||||||
onCommonConfigSnippetChange={
|
|
||||||
handleGeminiCommonConfigSnippetChange
|
|
||||||
}
|
|
||||||
commonConfigError={geminiCommonConfigError}
|
|
||||||
envError={envError}
|
envError={envError}
|
||||||
configError={geminiConfigError}
|
configError={geminiConfigError}
|
||||||
onExtract={handleGeminiExtract}
|
|
||||||
isExtracting={isGeminiExtracting}
|
|
||||||
/>
|
/>
|
||||||
{settingsConfigErrorField}
|
{settingsConfigErrorField}
|
||||||
</>
|
</>
|
||||||
) : appId === "opencode" &&
|
) : appId === "opencode" &&
|
||||||
(category === "omo" || category === "omo-slim") ? (
|
(category === "omo" || category === "omo-slim") ? (
|
||||||
<OmoCommonConfigEditor
|
<div className="space-y-2">
|
||||||
previewValue={omoDraft.mergedOmoJsonPreview}
|
<Label>{t("provider.configJson")}</Label>
|
||||||
useCommonConfig={omoDraft.useOmoCommonConfig}
|
<JsonEditor
|
||||||
onCommonConfigToggle={omoDraft.setUseOmoCommonConfig}
|
value={omoDraft.mergedOmoJsonPreview}
|
||||||
isModalOpen={omoDraft.isOmoConfigModalOpen}
|
onChange={() => {}}
|
||||||
onEditClick={omoDraft.handleOmoEditClick}
|
rows={14}
|
||||||
onModalClose={() => omoDraft.setIsOmoConfigModalOpen(false)}
|
showValidation={false}
|
||||||
onSave={omoDraft.handleOmoGlobalConfigSave}
|
language="json"
|
||||||
isSaving={omoDraft.isOmoSaving}
|
/>
|
||||||
onGlobalConfigStateChange={omoDraft.setOmoGlobalState}
|
</div>
|
||||||
globalConfigRef={omoDraft.omoGlobalConfigRef}
|
|
||||||
fieldsKey={omoDraft.omoFieldsKey}
|
|
||||||
isSlim={category === "omo-slim"}
|
|
||||||
/>
|
|
||||||
) : appId === "opencode" &&
|
) : appId === "opencode" &&
|
||||||
category !== "omo" &&
|
category !== "omo" &&
|
||||||
category !== "omo-slim" ? (
|
category !== "omo-slim" ? (
|
||||||
@@ -1499,25 +1477,53 @@ export function ProviderForm({
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<CommonConfigEditor
|
<div className="space-y-2">
|
||||||
value={form.getValues("settingsConfig")}
|
<Label htmlFor="settingsConfig">
|
||||||
onChange={(value) => form.setValue("settingsConfig", value)}
|
{t("claudeConfig.configLabel")}
|
||||||
useCommonConfig={useCommonConfig}
|
</Label>
|
||||||
onCommonConfigToggle={handleCommonConfigToggle}
|
{isEditMode && isCurrent && (
|
||||||
commonConfigSnippet={commonConfigSnippet}
|
<ClaudeQuickToggles
|
||||||
onCommonConfigSnippetChange={handleCommonConfigSnippetChange}
|
onPatchApplied={(patch) => {
|
||||||
commonConfigError={commonConfigError}
|
try {
|
||||||
onEditClick={() => setIsCommonConfigModalOpen(true)}
|
const cfg = JSON.parse(
|
||||||
isModalOpen={isCommonConfigModalOpen}
|
form.getValues("settingsConfig") || "{}",
|
||||||
onModalClose={() => setIsCommonConfigModalOpen(false)}
|
);
|
||||||
onExtract={handleClaudeExtract}
|
jsonMergePatch(cfg, patch);
|
||||||
isExtracting={isClaudeExtracting}
|
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)}
|
||||||
|
placeholder={`{
|
||||||
|
"env": {
|
||||||
|
"ANTHROPIC_AUTH_TOKEN": "your-api-key-here"
|
||||||
|
}
|
||||||
|
}`}
|
||||||
|
rows={14}
|
||||||
|
showValidation={true}
|
||||||
|
language="json"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{t(
|
||||||
|
isCurrent
|
||||||
|
? "claudeConfig.fullSettingsHint"
|
||||||
|
: "claudeConfig.fragmentSettingsHint",
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
{settingsConfigErrorField}
|
{settingsConfigErrorField}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{category !== "omo" && appId !== "opencode" && appId !== "openclaw" && (
|
{!isAnyOmoCategory && appId !== "opencode" && appId !== "openclaw" && (
|
||||||
<ProviderAdvancedConfig
|
<ProviderAdvancedConfig
|
||||||
testConfig={testConfig}
|
testConfig={testConfig}
|
||||||
proxyConfig={proxyConfig}
|
proxyConfig={proxyConfig}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { OpenCodeModel, OpenCodeProviderConfig } from "@/types";
|
import type { OpenCodeModel, OpenCodeProviderConfig } from "@/types";
|
||||||
import type { OmoGlobalConfig } from "@/types/omo";
|
|
||||||
import type { PricingModelSourceOption } from "../ProviderAdvancedConfig";
|
import type { PricingModelSourceOption } from "../ProviderAdvancedConfig";
|
||||||
|
|
||||||
// ── Default configs ──────────────────────────────────────────────────
|
// ── Default configs ──────────────────────────────────────────────────
|
||||||
@@ -52,15 +51,6 @@ export const OPENCLAW_DEFAULT_CONFIG = JSON.stringify(
|
|||||||
2,
|
2,
|
||||||
);
|
);
|
||||||
|
|
||||||
export const EMPTY_OMO_GLOBAL_CONFIG: OmoGlobalConfig = {
|
|
||||||
id: "global",
|
|
||||||
disabledAgents: [],
|
|
||||||
disabledMcps: [],
|
|
||||||
disabledHooks: [],
|
|
||||||
disabledSkills: [],
|
|
||||||
updatedAt: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── Pure functions ───────────────────────────────────────────────────
|
// ── Pure functions ───────────────────────────────────────────────────
|
||||||
|
|
||||||
export function isKnownOpencodeOptionKey(key: string): boolean {
|
export function isKnownOpencodeOptionKey(key: string): boolean {
|
||||||
|
|||||||
@@ -6,12 +6,9 @@ export { useCodexConfigState } from "./useCodexConfigState";
|
|||||||
export { useApiKeyLink } from "./useApiKeyLink";
|
export { useApiKeyLink } from "./useApiKeyLink";
|
||||||
export { useCustomEndpoints } from "./useCustomEndpoints";
|
export { useCustomEndpoints } from "./useCustomEndpoints";
|
||||||
export { useTemplateValues } from "./useTemplateValues";
|
export { useTemplateValues } from "./useTemplateValues";
|
||||||
export { useCommonConfigSnippet } from "./useCommonConfigSnippet";
|
|
||||||
export { useCodexCommonConfig } from "./useCodexCommonConfig";
|
|
||||||
export { useSpeedTestEndpoints } from "./useSpeedTestEndpoints";
|
export { useSpeedTestEndpoints } from "./useSpeedTestEndpoints";
|
||||||
export { useCodexTomlValidation } from "./useCodexTomlValidation";
|
export { useCodexTomlValidation } from "./useCodexTomlValidation";
|
||||||
export { useGeminiConfigState } from "./useGeminiConfigState";
|
export { useGeminiConfigState } from "./useGeminiConfigState";
|
||||||
export { useGeminiCommonConfig } from "./useGeminiCommonConfig";
|
|
||||||
export { useOmoModelSource } from "./useOmoModelSource";
|
export { useOmoModelSource } from "./useOmoModelSource";
|
||||||
export { useOpencodeFormState } from "./useOpencodeFormState";
|
export { useOpencodeFormState } from "./useOpencodeFormState";
|
||||||
export { useOmoDraftState } from "./useOmoDraftState";
|
export { useOmoDraftState } from "./useOmoDraftState";
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ interface UseApiKeyStateProps {
|
|||||||
selectedPresetId: string | null;
|
selectedPresetId: string | null;
|
||||||
category?: ProviderCategory;
|
category?: ProviderCategory;
|
||||||
appType?: string;
|
appType?: string;
|
||||||
|
apiKeyField?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,6 +25,7 @@ export function useApiKeyState({
|
|||||||
selectedPresetId,
|
selectedPresetId,
|
||||||
category,
|
category,
|
||||||
appType,
|
appType,
|
||||||
|
apiKeyField,
|
||||||
}: UseApiKeyStateProps) {
|
}: UseApiKeyStateProps) {
|
||||||
const [apiKey, setApiKey] = useState(() => {
|
const [apiKey, setApiKey] = useState(() => {
|
||||||
if (initialConfig) {
|
if (initialConfig) {
|
||||||
@@ -58,7 +60,7 @@ export function useApiKeyState({
|
|||||||
initialConfig || "{}",
|
initialConfig || "{}",
|
||||||
key.trim(),
|
key.trim(),
|
||||||
{
|
{
|
||||||
// 最佳实践:仅在“新增模式”且“非官方类别”时补齐缺失字段
|
// 最佳实践:仅在"新增模式"且"非官方类别"时补齐缺失字段
|
||||||
// - 新增模式:selectedPresetId !== null
|
// - 新增模式:selectedPresetId !== null
|
||||||
// - 非官方类别:category !== undefined && category !== "official"
|
// - 非官方类别:category !== undefined && category !== "official"
|
||||||
// - 官方类别:不创建字段(UI 也会禁用输入框)
|
// - 官方类别:不创建字段(UI 也会禁用输入框)
|
||||||
@@ -68,12 +70,20 @@ export function useApiKeyState({
|
|||||||
category !== undefined &&
|
category !== undefined &&
|
||||||
category !== "official",
|
category !== "official",
|
||||||
appType,
|
appType,
|
||||||
|
apiKeyField,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
onConfigChange(configString);
|
onConfigChange(configString);
|
||||||
},
|
},
|
||||||
[initialConfig, selectedPresetId, category, appType, onConfigChange],
|
[
|
||||||
|
initialConfig,
|
||||||
|
selectedPresetId,
|
||||||
|
category,
|
||||||
|
appType,
|
||||||
|
apiKeyField,
|
||||||
|
onConfigChange,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const showApiKey = useCallback(
|
const showApiKey = useCallback(
|
||||||
|
|||||||
@@ -1,308 +0,0 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import {
|
|
||||||
updateTomlCommonConfigSnippet,
|
|
||||||
hasTomlCommonConfigSnippet,
|
|
||||||
} from "@/utils/providerConfigUtils";
|
|
||||||
import { configApi } from "@/lib/api";
|
|
||||||
|
|
||||||
const LEGACY_STORAGE_KEY = "cc-switch:codex-common-config-snippet";
|
|
||||||
const DEFAULT_CODEX_COMMON_CONFIG_SNIPPET = `# Common Codex config
|
|
||||||
# Add your common TOML configuration here`;
|
|
||||||
|
|
||||||
interface UseCodexCommonConfigProps {
|
|
||||||
codexConfig: string;
|
|
||||||
onConfigChange: (config: string) => void;
|
|
||||||
initialData?: {
|
|
||||||
settingsConfig?: Record<string, unknown>;
|
|
||||||
};
|
|
||||||
selectedPresetId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 管理 Codex 通用配置片段 (TOML 格式)
|
|
||||||
* 从 config.json 读取和保存,支持从 localStorage 平滑迁移
|
|
||||||
*/
|
|
||||||
export function useCodexCommonConfig({
|
|
||||||
codexConfig,
|
|
||||||
onConfigChange,
|
|
||||||
initialData,
|
|
||||||
selectedPresetId,
|
|
||||||
}: UseCodexCommonConfigProps) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [useCommonConfig, setUseCommonConfig] = useState(false);
|
|
||||||
const [commonConfigSnippet, setCommonConfigSnippetState] = useState<string>(
|
|
||||||
DEFAULT_CODEX_COMMON_CONFIG_SNIPPET,
|
|
||||||
);
|
|
||||||
const [commonConfigError, setCommonConfigError] = useState("");
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const [isExtracting, setIsExtracting] = useState(false);
|
|
||||||
|
|
||||||
// 用于跟踪是否正在通过通用配置更新
|
|
||||||
const isUpdatingFromCommonConfig = useRef(false);
|
|
||||||
// 用于跟踪新建模式是否已初始化默认勾选
|
|
||||||
const hasInitializedNewMode = useRef(false);
|
|
||||||
|
|
||||||
// 当预设变化时,重置初始化标记,使新预设能够重新触发初始化逻辑
|
|
||||||
useEffect(() => {
|
|
||||||
hasInitializedNewMode.current = false;
|
|
||||||
}, [selectedPresetId]);
|
|
||||||
|
|
||||||
// 初始化:从 config.json 加载,支持从 localStorage 迁移
|
|
||||||
useEffect(() => {
|
|
||||||
let mounted = true;
|
|
||||||
|
|
||||||
const loadSnippet = async () => {
|
|
||||||
try {
|
|
||||||
// 使用统一 API 加载
|
|
||||||
const snippet = await configApi.getCommonConfigSnippet("codex");
|
|
||||||
|
|
||||||
if (snippet && snippet.trim()) {
|
|
||||||
if (mounted) {
|
|
||||||
setCommonConfigSnippetState(snippet);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 如果 config.json 中没有,尝试从 localStorage 迁移
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
try {
|
|
||||||
const legacySnippet =
|
|
||||||
window.localStorage.getItem(LEGACY_STORAGE_KEY);
|
|
||||||
if (legacySnippet && legacySnippet.trim()) {
|
|
||||||
// 迁移到 config.json
|
|
||||||
await configApi.setCommonConfigSnippet("codex", legacySnippet);
|
|
||||||
if (mounted) {
|
|
||||||
setCommonConfigSnippetState(legacySnippet);
|
|
||||||
}
|
|
||||||
// 清理 localStorage
|
|
||||||
window.localStorage.removeItem(LEGACY_STORAGE_KEY);
|
|
||||||
console.log(
|
|
||||||
"[迁移] Codex 通用配置已从 localStorage 迁移到 config.json",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("[迁移] 从 localStorage 迁移失败:", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("加载 Codex 通用配置失败:", error);
|
|
||||||
} finally {
|
|
||||||
if (mounted) {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadSnippet();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
mounted = false;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 初始化时检查通用配置片段(编辑模式)
|
|
||||||
useEffect(() => {
|
|
||||||
if (initialData?.settingsConfig && !isLoading) {
|
|
||||||
const config =
|
|
||||||
typeof initialData.settingsConfig.config === "string"
|
|
||||||
? initialData.settingsConfig.config
|
|
||||||
: "";
|
|
||||||
const hasCommon = hasTomlCommonConfigSnippet(config, commonConfigSnippet);
|
|
||||||
setUseCommonConfig(hasCommon);
|
|
||||||
}
|
|
||||||
}, [initialData, commonConfigSnippet, isLoading]);
|
|
||||||
|
|
||||||
// 新建模式:如果通用配置片段存在且有效,默认启用
|
|
||||||
useEffect(() => {
|
|
||||||
// 仅新建模式、加载完成、尚未初始化过
|
|
||||||
if (!initialData && !isLoading && !hasInitializedNewMode.current) {
|
|
||||||
hasInitializedNewMode.current = true;
|
|
||||||
|
|
||||||
// 检查 TOML 片段是否有实质内容(不只是注释和空行)
|
|
||||||
const lines = commonConfigSnippet.split("\n");
|
|
||||||
const hasContent = lines.some((line) => {
|
|
||||||
const trimmed = line.trim();
|
|
||||||
return trimmed && !trimmed.startsWith("#");
|
|
||||||
});
|
|
||||||
|
|
||||||
if (hasContent) {
|
|
||||||
setUseCommonConfig(true);
|
|
||||||
// 合并通用配置到当前配置
|
|
||||||
const { updatedConfig, error } = updateTomlCommonConfigSnippet(
|
|
||||||
codexConfig,
|
|
||||||
commonConfigSnippet,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
if (!error) {
|
|
||||||
isUpdatingFromCommonConfig.current = true;
|
|
||||||
onConfigChange(updatedConfig);
|
|
||||||
setTimeout(() => {
|
|
||||||
isUpdatingFromCommonConfig.current = false;
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
initialData,
|
|
||||||
commonConfigSnippet,
|
|
||||||
isLoading,
|
|
||||||
codexConfig,
|
|
||||||
onConfigChange,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 处理通用配置开关
|
|
||||||
const handleCommonConfigToggle = useCallback(
|
|
||||||
(checked: boolean) => {
|
|
||||||
const { updatedConfig, error: snippetError } =
|
|
||||||
updateTomlCommonConfigSnippet(
|
|
||||||
codexConfig,
|
|
||||||
commonConfigSnippet,
|
|
||||||
checked,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (snippetError) {
|
|
||||||
setCommonConfigError(snippetError);
|
|
||||||
setUseCommonConfig(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setCommonConfigError("");
|
|
||||||
setUseCommonConfig(checked);
|
|
||||||
// 标记正在通过通用配置更新
|
|
||||||
isUpdatingFromCommonConfig.current = true;
|
|
||||||
onConfigChange(updatedConfig);
|
|
||||||
// 在下一个事件循环中重置标记
|
|
||||||
setTimeout(() => {
|
|
||||||
isUpdatingFromCommonConfig.current = false;
|
|
||||||
}, 0);
|
|
||||||
},
|
|
||||||
[codexConfig, commonConfigSnippet, onConfigChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
// 处理通用配置片段变化
|
|
||||||
const handleCommonConfigSnippetChange = useCallback(
|
|
||||||
(value: string) => {
|
|
||||||
const previousSnippet = commonConfigSnippet;
|
|
||||||
setCommonConfigSnippetState(value);
|
|
||||||
|
|
||||||
if (!value.trim()) {
|
|
||||||
setCommonConfigError("");
|
|
||||||
// 保存到 config.json(清空)
|
|
||||||
configApi.setCommonConfigSnippet("codex", "").catch((error) => {
|
|
||||||
console.error("保存 Codex 通用配置失败:", error);
|
|
||||||
setCommonConfigError(
|
|
||||||
t("codexConfig.saveFailed", { error: String(error) }),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (useCommonConfig) {
|
|
||||||
const { updatedConfig } = updateTomlCommonConfigSnippet(
|
|
||||||
codexConfig,
|
|
||||||
previousSnippet,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
onConfigChange(updatedConfig);
|
|
||||||
setUseCommonConfig(false);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TOML 格式校验较为复杂,暂时不做校验,直接清空错误
|
|
||||||
setCommonConfigError("");
|
|
||||||
// 保存到 config.json
|
|
||||||
configApi.setCommonConfigSnippet("codex", value).catch((error) => {
|
|
||||||
console.error("保存 Codex 通用配置失败:", error);
|
|
||||||
setCommonConfigError(
|
|
||||||
t("codexConfig.saveFailed", { error: String(error) }),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 若当前启用通用配置,需要替换为最新片段
|
|
||||||
if (useCommonConfig) {
|
|
||||||
const removeResult = updateTomlCommonConfigSnippet(
|
|
||||||
codexConfig,
|
|
||||||
previousSnippet,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
if (removeResult.error) {
|
|
||||||
setCommonConfigError(removeResult.error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const addResult = updateTomlCommonConfigSnippet(
|
|
||||||
removeResult.updatedConfig,
|
|
||||||
value,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (addResult.error) {
|
|
||||||
setCommonConfigError(addResult.error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 标记正在通过通用配置更新,避免触发状态检查
|
|
||||||
isUpdatingFromCommonConfig.current = true;
|
|
||||||
onConfigChange(addResult.updatedConfig);
|
|
||||||
// 在下一个事件循环中重置标记
|
|
||||||
setTimeout(() => {
|
|
||||||
isUpdatingFromCommonConfig.current = false;
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[commonConfigSnippet, codexConfig, useCommonConfig, onConfigChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
// 当配置变化时检查是否包含通用配置(但避免在通过通用配置更新时检查)
|
|
||||||
useEffect(() => {
|
|
||||||
if (isUpdatingFromCommonConfig.current || isLoading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const hasCommon = hasTomlCommonConfigSnippet(
|
|
||||||
codexConfig,
|
|
||||||
commonConfigSnippet,
|
|
||||||
);
|
|
||||||
setUseCommonConfig(hasCommon);
|
|
||||||
}, [codexConfig, commonConfigSnippet, isLoading]);
|
|
||||||
|
|
||||||
// 从编辑器当前内容提取通用配置片段
|
|
||||||
const handleExtract = useCallback(async () => {
|
|
||||||
setIsExtracting(true);
|
|
||||||
setCommonConfigError("");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const extracted = await configApi.extractCommonConfigSnippet("codex", {
|
|
||||||
settingsConfig: JSON.stringify({
|
|
||||||
config: codexConfig ?? "",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!extracted || !extracted.trim()) {
|
|
||||||
setCommonConfigError(t("codexConfig.extractNoCommonConfig"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新片段状态
|
|
||||||
setCommonConfigSnippetState(extracted);
|
|
||||||
|
|
||||||
// 保存到后端
|
|
||||||
await configApi.setCommonConfigSnippet("codex", extracted);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("提取 Codex 通用配置失败:", error);
|
|
||||||
setCommonConfigError(
|
|
||||||
t("codexConfig.extractFailed", { error: String(error) }),
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
setIsExtracting(false);
|
|
||||||
}
|
|
||||||
}, [codexConfig, t]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
useCommonConfig,
|
|
||||||
commonConfigSnippet,
|
|
||||||
commonConfigError,
|
|
||||||
isLoading,
|
|
||||||
isExtracting,
|
|
||||||
handleCommonConfigToggle,
|
|
||||||
handleCommonConfigSnippetChange,
|
|
||||||
handleExtract,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,331 +0,0 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import {
|
|
||||||
updateCommonConfigSnippet,
|
|
||||||
hasCommonConfigSnippet,
|
|
||||||
validateJsonConfig,
|
|
||||||
} from "@/utils/providerConfigUtils";
|
|
||||||
import { configApi } from "@/lib/api";
|
|
||||||
|
|
||||||
const LEGACY_STORAGE_KEY = "cc-switch:common-config-snippet";
|
|
||||||
const DEFAULT_COMMON_CONFIG_SNIPPET = `{
|
|
||||||
"includeCoAuthoredBy": false
|
|
||||||
}`;
|
|
||||||
|
|
||||||
interface UseCommonConfigSnippetProps {
|
|
||||||
settingsConfig: string;
|
|
||||||
onConfigChange: (config: string) => void;
|
|
||||||
initialData?: {
|
|
||||||
settingsConfig?: Record<string, unknown>;
|
|
||||||
};
|
|
||||||
selectedPresetId?: string;
|
|
||||||
/** When false, the hook skips all logic and returns disabled state. Default: true */
|
|
||||||
enabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 管理 Claude 通用配置片段
|
|
||||||
* 从 config.json 读取和保存,支持从 localStorage 平滑迁移
|
|
||||||
*/
|
|
||||||
export function useCommonConfigSnippet({
|
|
||||||
settingsConfig,
|
|
||||||
onConfigChange,
|
|
||||||
initialData,
|
|
||||||
selectedPresetId,
|
|
||||||
enabled = true,
|
|
||||||
}: UseCommonConfigSnippetProps) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [useCommonConfig, setUseCommonConfig] = useState(false);
|
|
||||||
const [commonConfigSnippet, setCommonConfigSnippetState] = useState<string>(
|
|
||||||
DEFAULT_COMMON_CONFIG_SNIPPET,
|
|
||||||
);
|
|
||||||
const [commonConfigError, setCommonConfigError] = useState("");
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const [isExtracting, setIsExtracting] = useState(false);
|
|
||||||
|
|
||||||
// 用于跟踪是否正在通过通用配置更新
|
|
||||||
const isUpdatingFromCommonConfig = useRef(false);
|
|
||||||
// 用于跟踪新建模式是否已初始化默认勾选
|
|
||||||
const hasInitializedNewMode = useRef(false);
|
|
||||||
|
|
||||||
// 当预设变化时,重置初始化标记,使新预设能够重新触发初始化逻辑
|
|
||||||
useEffect(() => {
|
|
||||||
if (!enabled) return;
|
|
||||||
hasInitializedNewMode.current = false;
|
|
||||||
}, [selectedPresetId, enabled]);
|
|
||||||
|
|
||||||
// 初始化:从 config.json 加载,支持从 localStorage 迁移
|
|
||||||
useEffect(() => {
|
|
||||||
if (!enabled) {
|
|
||||||
setIsLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let mounted = true;
|
|
||||||
|
|
||||||
const loadSnippet = async () => {
|
|
||||||
try {
|
|
||||||
// 使用统一 API 加载
|
|
||||||
const snippet = await configApi.getCommonConfigSnippet("claude");
|
|
||||||
|
|
||||||
if (snippet && snippet.trim()) {
|
|
||||||
if (mounted) {
|
|
||||||
setCommonConfigSnippetState(snippet);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 如果 config.json 中没有,尝试从 localStorage 迁移
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
try {
|
|
||||||
const legacySnippet =
|
|
||||||
window.localStorage.getItem(LEGACY_STORAGE_KEY);
|
|
||||||
if (legacySnippet && legacySnippet.trim()) {
|
|
||||||
// 迁移到 config.json
|
|
||||||
await configApi.setCommonConfigSnippet("claude", legacySnippet);
|
|
||||||
if (mounted) {
|
|
||||||
setCommonConfigSnippetState(legacySnippet);
|
|
||||||
}
|
|
||||||
// 清理 localStorage
|
|
||||||
window.localStorage.removeItem(LEGACY_STORAGE_KEY);
|
|
||||||
console.log(
|
|
||||||
"[迁移] Claude 通用配置已从 localStorage 迁移到 config.json",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("[迁移] 从 localStorage 迁移失败:", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("加载通用配置失败:", error);
|
|
||||||
} finally {
|
|
||||||
if (mounted) {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadSnippet();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
mounted = false;
|
|
||||||
};
|
|
||||||
}, [enabled]);
|
|
||||||
|
|
||||||
// 初始化时检查通用配置片段(编辑模式)
|
|
||||||
useEffect(() => {
|
|
||||||
if (!enabled) return;
|
|
||||||
if (initialData && !isLoading) {
|
|
||||||
const configString = JSON.stringify(initialData.settingsConfig, null, 2);
|
|
||||||
const hasCommon = hasCommonConfigSnippet(
|
|
||||||
configString,
|
|
||||||
commonConfigSnippet,
|
|
||||||
);
|
|
||||||
setUseCommonConfig(hasCommon);
|
|
||||||
}
|
|
||||||
}, [enabled, initialData, commonConfigSnippet, isLoading]);
|
|
||||||
|
|
||||||
// 新建模式:如果通用配置片段存在且有效,默认启用
|
|
||||||
useEffect(() => {
|
|
||||||
if (!enabled) return;
|
|
||||||
// 仅新建模式、加载完成、尚未初始化过
|
|
||||||
if (!initialData && !isLoading && !hasInitializedNewMode.current) {
|
|
||||||
hasInitializedNewMode.current = true;
|
|
||||||
|
|
||||||
// 检查片段是否有实质内容
|
|
||||||
try {
|
|
||||||
const snippetObj = JSON.parse(commonConfigSnippet);
|
|
||||||
const hasContent = Object.keys(snippetObj).length > 0;
|
|
||||||
if (hasContent) {
|
|
||||||
setUseCommonConfig(true);
|
|
||||||
// 合并通用配置到当前配置
|
|
||||||
const { updatedConfig, error } = updateCommonConfigSnippet(
|
|
||||||
settingsConfig,
|
|
||||||
commonConfigSnippet,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
if (!error) {
|
|
||||||
isUpdatingFromCommonConfig.current = true;
|
|
||||||
onConfigChange(updatedConfig);
|
|
||||||
setTimeout(() => {
|
|
||||||
isUpdatingFromCommonConfig.current = false;
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore parse error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
enabled,
|
|
||||||
initialData,
|
|
||||||
commonConfigSnippet,
|
|
||||||
isLoading,
|
|
||||||
settingsConfig,
|
|
||||||
onConfigChange,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 处理通用配置开关
|
|
||||||
const handleCommonConfigToggle = useCallback(
|
|
||||||
(checked: boolean) => {
|
|
||||||
const { updatedConfig, error: snippetError } = updateCommonConfigSnippet(
|
|
||||||
settingsConfig,
|
|
||||||
commonConfigSnippet,
|
|
||||||
checked,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (snippetError) {
|
|
||||||
setCommonConfigError(snippetError);
|
|
||||||
setUseCommonConfig(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setCommonConfigError("");
|
|
||||||
setUseCommonConfig(checked);
|
|
||||||
// 标记正在通过通用配置更新
|
|
||||||
isUpdatingFromCommonConfig.current = true;
|
|
||||||
onConfigChange(updatedConfig);
|
|
||||||
// 在下一个事件循环中重置标记
|
|
||||||
setTimeout(() => {
|
|
||||||
isUpdatingFromCommonConfig.current = false;
|
|
||||||
}, 0);
|
|
||||||
},
|
|
||||||
[settingsConfig, commonConfigSnippet, onConfigChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
// 处理通用配置片段变化
|
|
||||||
const handleCommonConfigSnippetChange = useCallback(
|
|
||||||
(value: string) => {
|
|
||||||
const previousSnippet = commonConfigSnippet;
|
|
||||||
setCommonConfigSnippetState(value);
|
|
||||||
|
|
||||||
if (!value.trim()) {
|
|
||||||
setCommonConfigError("");
|
|
||||||
// 保存到 config.json(清空)
|
|
||||||
configApi.setCommonConfigSnippet("claude", "").catch((error) => {
|
|
||||||
console.error("保存通用配置失败:", error);
|
|
||||||
setCommonConfigError(
|
|
||||||
t("claudeConfig.saveFailed", { error: String(error) }),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (useCommonConfig) {
|
|
||||||
const { updatedConfig } = updateCommonConfigSnippet(
|
|
||||||
settingsConfig,
|
|
||||||
previousSnippet,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
onConfigChange(updatedConfig);
|
|
||||||
setUseCommonConfig(false);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证JSON格式
|
|
||||||
const validationError = validateJsonConfig(value, "通用配置片段");
|
|
||||||
if (validationError) {
|
|
||||||
setCommonConfigError(validationError);
|
|
||||||
} else {
|
|
||||||
setCommonConfigError("");
|
|
||||||
// 保存到 config.json
|
|
||||||
configApi.setCommonConfigSnippet("claude", value).catch((error) => {
|
|
||||||
console.error("保存通用配置失败:", error);
|
|
||||||
setCommonConfigError(
|
|
||||||
t("claudeConfig.saveFailed", { error: String(error) }),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 若当前启用通用配置且格式正确,需要替换为最新片段
|
|
||||||
if (useCommonConfig && !validationError) {
|
|
||||||
const removeResult = updateCommonConfigSnippet(
|
|
||||||
settingsConfig,
|
|
||||||
previousSnippet,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
if (removeResult.error) {
|
|
||||||
setCommonConfigError(removeResult.error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const addResult = updateCommonConfigSnippet(
|
|
||||||
removeResult.updatedConfig,
|
|
||||||
value,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (addResult.error) {
|
|
||||||
setCommonConfigError(addResult.error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 标记正在通过通用配置更新,避免触发状态检查
|
|
||||||
isUpdatingFromCommonConfig.current = true;
|
|
||||||
onConfigChange(addResult.updatedConfig);
|
|
||||||
// 在下一个事件循环中重置标记
|
|
||||||
setTimeout(() => {
|
|
||||||
isUpdatingFromCommonConfig.current = false;
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[commonConfigSnippet, settingsConfig, useCommonConfig, onConfigChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
// 当配置变化时检查是否包含通用配置(但避免在通过通用配置更新时检查)
|
|
||||||
useEffect(() => {
|
|
||||||
if (!enabled) return;
|
|
||||||
if (isUpdatingFromCommonConfig.current || isLoading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const hasCommon = hasCommonConfigSnippet(
|
|
||||||
settingsConfig,
|
|
||||||
commonConfigSnippet,
|
|
||||||
);
|
|
||||||
setUseCommonConfig(hasCommon);
|
|
||||||
}, [enabled, settingsConfig, commonConfigSnippet, isLoading]);
|
|
||||||
|
|
||||||
// 从编辑器当前内容提取通用配置片段
|
|
||||||
const handleExtract = useCallback(async () => {
|
|
||||||
setIsExtracting(true);
|
|
||||||
setCommonConfigError("");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const extracted = await configApi.extractCommonConfigSnippet("claude", {
|
|
||||||
settingsConfig,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!extracted || extracted === "{}") {
|
|
||||||
setCommonConfigError(t("claudeConfig.extractNoCommonConfig"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证 JSON 格式
|
|
||||||
const validationError = validateJsonConfig(extracted, "提取的配置");
|
|
||||||
if (validationError) {
|
|
||||||
setCommonConfigError(validationError);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新片段状态
|
|
||||||
setCommonConfigSnippetState(extracted);
|
|
||||||
|
|
||||||
// 保存到后端
|
|
||||||
await configApi.setCommonConfigSnippet("claude", extracted);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("提取通用配置失败:", error);
|
|
||||||
setCommonConfigError(
|
|
||||||
t("claudeConfig.extractFailed", { error: String(error) }),
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
setIsExtracting(false);
|
|
||||||
}
|
|
||||||
}, [settingsConfig, t]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
useCommonConfig,
|
|
||||||
commonConfigSnippet,
|
|
||||||
commonConfigError,
|
|
||||||
isLoading,
|
|
||||||
isExtracting,
|
|
||||||
handleCommonConfigToggle,
|
|
||||||
handleCommonConfigSnippetChange,
|
|
||||||
handleExtract,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,465 +0,0 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { configApi } from "@/lib/api";
|
|
||||||
|
|
||||||
const LEGACY_STORAGE_KEY = "cc-switch:gemini-common-config-snippet";
|
|
||||||
const DEFAULT_GEMINI_COMMON_CONFIG_SNIPPET = "{}";
|
|
||||||
|
|
||||||
const GEMINI_COMMON_ENV_FORBIDDEN_KEYS = [
|
|
||||||
"GOOGLE_GEMINI_BASE_URL",
|
|
||||||
"GEMINI_API_KEY",
|
|
||||||
] as const;
|
|
||||||
type GeminiForbiddenEnvKey = (typeof GEMINI_COMMON_ENV_FORBIDDEN_KEYS)[number];
|
|
||||||
|
|
||||||
interface UseGeminiCommonConfigProps {
|
|
||||||
envValue: string;
|
|
||||||
onEnvChange: (env: string) => void;
|
|
||||||
envStringToObj: (envString: string) => Record<string, string>;
|
|
||||||
envObjToString: (envObj: Record<string, unknown>) => string;
|
|
||||||
initialData?: {
|
|
||||||
settingsConfig?: Record<string, unknown>;
|
|
||||||
};
|
|
||||||
selectedPresetId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
||||||
return (
|
|
||||||
typeof value === "object" &&
|
|
||||||
value !== null &&
|
|
||||||
!Array.isArray(value) &&
|
|
||||||
Object.prototype.toString.call(value) === "[object Object]"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 管理 Gemini 通用配置片段 (JSON 格式)
|
|
||||||
* 写入 Gemini 的 .env,但会排除以下敏感字段:
|
|
||||||
* - GOOGLE_GEMINI_BASE_URL
|
|
||||||
* - GEMINI_API_KEY
|
|
||||||
*/
|
|
||||||
export function useGeminiCommonConfig({
|
|
||||||
envValue,
|
|
||||||
onEnvChange,
|
|
||||||
envStringToObj,
|
|
||||||
envObjToString,
|
|
||||||
initialData,
|
|
||||||
selectedPresetId,
|
|
||||||
}: UseGeminiCommonConfigProps) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [useCommonConfig, setUseCommonConfig] = useState(false);
|
|
||||||
const [commonConfigSnippet, setCommonConfigSnippetState] = useState<string>(
|
|
||||||
DEFAULT_GEMINI_COMMON_CONFIG_SNIPPET,
|
|
||||||
);
|
|
||||||
const [commonConfigError, setCommonConfigError] = useState("");
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const [isExtracting, setIsExtracting] = useState(false);
|
|
||||||
|
|
||||||
// 用于跟踪是否正在通过通用配置更新
|
|
||||||
const isUpdatingFromCommonConfig = useRef(false);
|
|
||||||
// 用于跟踪新建模式是否已初始化默认勾选
|
|
||||||
const hasInitializedNewMode = useRef(false);
|
|
||||||
|
|
||||||
// 当预设变化时,重置初始化标记,使新预设能够重新触发初始化逻辑
|
|
||||||
useEffect(() => {
|
|
||||||
hasInitializedNewMode.current = false;
|
|
||||||
}, [selectedPresetId]);
|
|
||||||
|
|
||||||
const parseSnippetEnv = useCallback(
|
|
||||||
(
|
|
||||||
snippetString: string,
|
|
||||||
): { env: Record<string, string>; error?: string } => {
|
|
||||||
const trimmed = snippetString.trim();
|
|
||||||
if (!trimmed) {
|
|
||||||
return { env: {} };
|
|
||||||
}
|
|
||||||
|
|
||||||
let parsed: unknown;
|
|
||||||
try {
|
|
||||||
parsed = JSON.parse(trimmed);
|
|
||||||
} catch {
|
|
||||||
return { env: {}, error: t("geminiConfig.invalidJsonFormat") };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isPlainObject(parsed)) {
|
|
||||||
return { env: {}, error: t("geminiConfig.invalidJsonFormat") };
|
|
||||||
}
|
|
||||||
|
|
||||||
const keys = Object.keys(parsed);
|
|
||||||
const forbiddenKeys = keys.filter((key) =>
|
|
||||||
GEMINI_COMMON_ENV_FORBIDDEN_KEYS.includes(key as GeminiForbiddenEnvKey),
|
|
||||||
);
|
|
||||||
if (forbiddenKeys.length > 0) {
|
|
||||||
return {
|
|
||||||
env: {},
|
|
||||||
error: t("geminiConfig.commonConfigInvalidKeys", {
|
|
||||||
keys: forbiddenKeys.join(", "),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const env: Record<string, string> = {};
|
|
||||||
for (const [key, value] of Object.entries(parsed)) {
|
|
||||||
if (typeof value !== "string") {
|
|
||||||
return {
|
|
||||||
env: {},
|
|
||||||
error: t("geminiConfig.commonConfigInvalidValues"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const normalized = value.trim();
|
|
||||||
if (!normalized) continue;
|
|
||||||
env[key] = normalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { env };
|
|
||||||
},
|
|
||||||
[t],
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasEnvCommonConfigSnippet = useCallback(
|
|
||||||
(envObj: Record<string, string>, snippetEnv: Record<string, string>) => {
|
|
||||||
const entries = Object.entries(snippetEnv);
|
|
||||||
if (entries.length === 0) return false;
|
|
||||||
return entries.every(([key, value]) => envObj[key] === value);
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const applySnippetToEnv = useCallback(
|
|
||||||
(envObj: Record<string, string>, snippetEnv: Record<string, string>) => {
|
|
||||||
const updated = { ...envObj };
|
|
||||||
for (const [key, value] of Object.entries(snippetEnv)) {
|
|
||||||
if (typeof value === "string") {
|
|
||||||
updated[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return updated;
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const removeSnippetFromEnv = useCallback(
|
|
||||||
(envObj: Record<string, string>, snippetEnv: Record<string, string>) => {
|
|
||||||
const updated = { ...envObj };
|
|
||||||
for (const [key, value] of Object.entries(snippetEnv)) {
|
|
||||||
if (typeof value === "string" && updated[key] === value) {
|
|
||||||
delete updated[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return updated;
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
// 初始化:从 config.json 加载,支持从 localStorage 迁移
|
|
||||||
useEffect(() => {
|
|
||||||
let mounted = true;
|
|
||||||
|
|
||||||
const loadSnippet = async () => {
|
|
||||||
try {
|
|
||||||
// 使用统一 API 加载
|
|
||||||
const snippet = await configApi.getCommonConfigSnippet("gemini");
|
|
||||||
|
|
||||||
if (snippet && snippet.trim()) {
|
|
||||||
if (mounted) {
|
|
||||||
setCommonConfigSnippetState(snippet);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 如果 config.json 中没有,尝试从 localStorage 迁移
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
try {
|
|
||||||
const legacySnippet =
|
|
||||||
window.localStorage.getItem(LEGACY_STORAGE_KEY);
|
|
||||||
if (legacySnippet && legacySnippet.trim()) {
|
|
||||||
const parsed = parseSnippetEnv(legacySnippet);
|
|
||||||
if (parsed.error) {
|
|
||||||
console.warn(
|
|
||||||
"[迁移] legacy Gemini 通用配置片段格式不符合当前规则,跳过迁移",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 迁移到 config.json
|
|
||||||
await configApi.setCommonConfigSnippet("gemini", legacySnippet);
|
|
||||||
if (mounted) {
|
|
||||||
setCommonConfigSnippetState(legacySnippet);
|
|
||||||
}
|
|
||||||
// 清理 localStorage
|
|
||||||
window.localStorage.removeItem(LEGACY_STORAGE_KEY);
|
|
||||||
console.log(
|
|
||||||
"[迁移] Gemini 通用配置已从 localStorage 迁移到 config.json",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("[迁移] 从 localStorage 迁移失败:", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("加载 Gemini 通用配置失败:", error);
|
|
||||||
} finally {
|
|
||||||
if (mounted) {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadSnippet();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
mounted = false;
|
|
||||||
};
|
|
||||||
}, [parseSnippetEnv]);
|
|
||||||
|
|
||||||
// 初始化时检查通用配置片段(编辑模式)
|
|
||||||
useEffect(() => {
|
|
||||||
if (initialData?.settingsConfig && !isLoading) {
|
|
||||||
try {
|
|
||||||
const env =
|
|
||||||
isPlainObject(initialData.settingsConfig.env) &&
|
|
||||||
Object.keys(initialData.settingsConfig.env).length > 0
|
|
||||||
? (initialData.settingsConfig.env as Record<string, string>)
|
|
||||||
: {};
|
|
||||||
const parsed = parseSnippetEnv(commonConfigSnippet);
|
|
||||||
if (parsed.error) return;
|
|
||||||
const hasCommon = hasEnvCommonConfigSnippet(
|
|
||||||
env,
|
|
||||||
parsed.env as Record<string, string>,
|
|
||||||
);
|
|
||||||
setUseCommonConfig(hasCommon);
|
|
||||||
} catch {
|
|
||||||
// ignore parse error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
commonConfigSnippet,
|
|
||||||
hasEnvCommonConfigSnippet,
|
|
||||||
initialData,
|
|
||||||
isLoading,
|
|
||||||
parseSnippetEnv,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 新建模式:如果通用配置片段存在且有效,默认启用
|
|
||||||
useEffect(() => {
|
|
||||||
// 仅新建模式、加载完成、尚未初始化过
|
|
||||||
if (!initialData && !isLoading && !hasInitializedNewMode.current) {
|
|
||||||
hasInitializedNewMode.current = true;
|
|
||||||
|
|
||||||
const parsed = parseSnippetEnv(commonConfigSnippet);
|
|
||||||
if (parsed.error) return;
|
|
||||||
const hasContent = Object.keys(parsed.env).length > 0;
|
|
||||||
if (!hasContent) return;
|
|
||||||
|
|
||||||
setUseCommonConfig(true);
|
|
||||||
const currentEnv = envStringToObj(envValue);
|
|
||||||
const merged = applySnippetToEnv(currentEnv, parsed.env);
|
|
||||||
const nextEnvString = envObjToString(merged);
|
|
||||||
|
|
||||||
isUpdatingFromCommonConfig.current = true;
|
|
||||||
onEnvChange(nextEnvString);
|
|
||||||
setTimeout(() => {
|
|
||||||
isUpdatingFromCommonConfig.current = false;
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
initialData,
|
|
||||||
isLoading,
|
|
||||||
commonConfigSnippet,
|
|
||||||
envValue,
|
|
||||||
envStringToObj,
|
|
||||||
envObjToString,
|
|
||||||
applySnippetToEnv,
|
|
||||||
onEnvChange,
|
|
||||||
parseSnippetEnv,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 处理通用配置开关
|
|
||||||
const handleCommonConfigToggle = useCallback(
|
|
||||||
(checked: boolean) => {
|
|
||||||
const parsed = parseSnippetEnv(commonConfigSnippet);
|
|
||||||
if (parsed.error) {
|
|
||||||
setCommonConfigError(parsed.error);
|
|
||||||
setUseCommonConfig(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (Object.keys(parsed.env).length === 0) {
|
|
||||||
setCommonConfigError(t("geminiConfig.noCommonConfigToApply"));
|
|
||||||
setUseCommonConfig(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentEnv = envStringToObj(envValue);
|
|
||||||
const updatedEnvObj = checked
|
|
||||||
? applySnippetToEnv(currentEnv, parsed.env)
|
|
||||||
: removeSnippetFromEnv(currentEnv, parsed.env);
|
|
||||||
|
|
||||||
setCommonConfigError("");
|
|
||||||
setUseCommonConfig(checked);
|
|
||||||
|
|
||||||
isUpdatingFromCommonConfig.current = true;
|
|
||||||
onEnvChange(envObjToString(updatedEnvObj));
|
|
||||||
setTimeout(() => {
|
|
||||||
isUpdatingFromCommonConfig.current = false;
|
|
||||||
}, 0);
|
|
||||||
},
|
|
||||||
[
|
|
||||||
applySnippetToEnv,
|
|
||||||
commonConfigSnippet,
|
|
||||||
envObjToString,
|
|
||||||
envStringToObj,
|
|
||||||
envValue,
|
|
||||||
onEnvChange,
|
|
||||||
parseSnippetEnv,
|
|
||||||
removeSnippetFromEnv,
|
|
||||||
t,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
// 处理通用配置片段变化
|
|
||||||
const handleCommonConfigSnippetChange = useCallback(
|
|
||||||
(value: string) => {
|
|
||||||
const previousSnippet = commonConfigSnippet;
|
|
||||||
setCommonConfigSnippetState(value);
|
|
||||||
|
|
||||||
if (!value.trim()) {
|
|
||||||
setCommonConfigError("");
|
|
||||||
// 保存到 config.json(清空)
|
|
||||||
configApi.setCommonConfigSnippet("gemini", "").catch((error) => {
|
|
||||||
console.error("保存 Gemini 通用配置失败:", error);
|
|
||||||
setCommonConfigError(
|
|
||||||
t("geminiConfig.saveFailed", { error: String(error) }),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (useCommonConfig) {
|
|
||||||
const parsed = parseSnippetEnv(previousSnippet);
|
|
||||||
if (!parsed.error && Object.keys(parsed.env).length > 0) {
|
|
||||||
const currentEnv = envStringToObj(envValue);
|
|
||||||
const updatedEnv = removeSnippetFromEnv(currentEnv, parsed.env);
|
|
||||||
onEnvChange(envObjToString(updatedEnv));
|
|
||||||
}
|
|
||||||
setUseCommonConfig(false);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 校验 JSON 格式
|
|
||||||
const parsed = parseSnippetEnv(value);
|
|
||||||
if (parsed.error) {
|
|
||||||
setCommonConfigError(parsed.error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setCommonConfigError("");
|
|
||||||
configApi.setCommonConfigSnippet("gemini", value).catch((error) => {
|
|
||||||
console.error("保存 Gemini 通用配置失败:", error);
|
|
||||||
setCommonConfigError(
|
|
||||||
t("geminiConfig.saveFailed", { error: String(error) }),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 若当前启用通用配置,需要替换为最新片段
|
|
||||||
if (useCommonConfig) {
|
|
||||||
const prevParsed = parseSnippetEnv(previousSnippet);
|
|
||||||
const prevEnv = prevParsed.error ? {} : prevParsed.env;
|
|
||||||
const nextEnv = parsed.env;
|
|
||||||
const currentEnv = envStringToObj(envValue);
|
|
||||||
|
|
||||||
const withoutOld =
|
|
||||||
Object.keys(prevEnv).length > 0
|
|
||||||
? removeSnippetFromEnv(currentEnv, prevEnv)
|
|
||||||
: currentEnv;
|
|
||||||
const withNew =
|
|
||||||
Object.keys(nextEnv).length > 0
|
|
||||||
? applySnippetToEnv(withoutOld, nextEnv)
|
|
||||||
: withoutOld;
|
|
||||||
|
|
||||||
isUpdatingFromCommonConfig.current = true;
|
|
||||||
onEnvChange(envObjToString(withNew));
|
|
||||||
setTimeout(() => {
|
|
||||||
isUpdatingFromCommonConfig.current = false;
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[
|
|
||||||
applySnippetToEnv,
|
|
||||||
commonConfigSnippet,
|
|
||||||
envObjToString,
|
|
||||||
envStringToObj,
|
|
||||||
envValue,
|
|
||||||
onEnvChange,
|
|
||||||
parseSnippetEnv,
|
|
||||||
removeSnippetFromEnv,
|
|
||||||
t,
|
|
||||||
useCommonConfig,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
// 当 env 变化时检查是否包含通用配置(但避免在通过通用配置更新时检查)
|
|
||||||
useEffect(() => {
|
|
||||||
if (isUpdatingFromCommonConfig.current || isLoading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const parsed = parseSnippetEnv(commonConfigSnippet);
|
|
||||||
if (parsed.error) return;
|
|
||||||
const envObj = envStringToObj(envValue);
|
|
||||||
setUseCommonConfig(
|
|
||||||
hasEnvCommonConfigSnippet(envObj, parsed.env as Record<string, string>),
|
|
||||||
);
|
|
||||||
}, [
|
|
||||||
envValue,
|
|
||||||
commonConfigSnippet,
|
|
||||||
envStringToObj,
|
|
||||||
hasEnvCommonConfigSnippet,
|
|
||||||
isLoading,
|
|
||||||
parseSnippetEnv,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 从编辑器当前内容提取通用配置片段
|
|
||||||
const handleExtract = useCallback(async () => {
|
|
||||||
setIsExtracting(true);
|
|
||||||
setCommonConfigError("");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const extracted = await configApi.extractCommonConfigSnippet("gemini", {
|
|
||||||
settingsConfig: JSON.stringify({
|
|
||||||
env: envStringToObj(envValue),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!extracted || extracted === "{}") {
|
|
||||||
setCommonConfigError(t("geminiConfig.extractNoCommonConfig"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证 JSON 格式
|
|
||||||
const parsed = parseSnippetEnv(extracted);
|
|
||||||
if (parsed.error) {
|
|
||||||
setCommonConfigError(t("geminiConfig.extractedConfigInvalid"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新片段状态
|
|
||||||
setCommonConfigSnippetState(extracted);
|
|
||||||
|
|
||||||
// 保存到后端
|
|
||||||
await configApi.setCommonConfigSnippet("gemini", extracted);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("提取 Gemini 通用配置失败:", error);
|
|
||||||
setCommonConfigError(
|
|
||||||
t("geminiConfig.extractFailed", { error: String(error) }),
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
setIsExtracting(false);
|
|
||||||
}
|
|
||||||
}, [envStringToObj, envValue, parseSnippetEnv, t]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
useCommonConfig,
|
|
||||||
commonConfigSnippet,
|
|
||||||
commonConfigError,
|
|
||||||
isLoading,
|
|
||||||
isExtracting,
|
|
||||||
handleCommonConfigToggle,
|
|
||||||
handleCommonConfigSnippetChange,
|
|
||||||
handleExtract,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,22 +1,11 @@
|
|||||||
import { useState, useCallback, useEffect, useRef, useMemo } from "react";
|
import { useState, useCallback, useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import type { OmoGlobalConfig } from "@/types/omo";
|
|
||||||
import {
|
import {
|
||||||
mergeOmoConfigPreview,
|
|
||||||
mergeOmoSlimConfigPreview,
|
|
||||||
buildOmoSlimProfilePreview,
|
buildOmoSlimProfilePreview,
|
||||||
} from "@/types/omo";
|
|
||||||
import { type OmoGlobalConfigFieldsRef } from "../OmoGlobalConfigFields";
|
|
||||||
import * as configApi from "@/lib/api/config";
|
|
||||||
import {
|
|
||||||
EMPTY_OMO_GLOBAL_CONFIG,
|
|
||||||
buildOmoProfilePreview,
|
buildOmoProfilePreview,
|
||||||
} from "../helpers/opencodeFormUtils";
|
} from "@/types/omo";
|
||||||
|
|
||||||
interface UseOmoDraftStateParams {
|
interface UseOmoDraftStateParams {
|
||||||
initialOmoSettings: Record<string, unknown> | undefined;
|
initialOmoSettings: Record<string, unknown> | undefined;
|
||||||
queriedOmoGlobalConfig: OmoGlobalConfig | undefined;
|
|
||||||
isEditMode: boolean;
|
isEditMode: boolean;
|
||||||
appId: string;
|
appId: string;
|
||||||
category?: string;
|
category?: string;
|
||||||
@@ -33,33 +22,15 @@ export interface OmoDraftState {
|
|||||||
>;
|
>;
|
||||||
omoOtherFieldsStr: string;
|
omoOtherFieldsStr: string;
|
||||||
setOmoOtherFieldsStr: React.Dispatch<React.SetStateAction<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;
|
mergedOmoJsonPreview: string;
|
||||||
handleOmoGlobalConfigSave: () => Promise<void>;
|
resetOmoDraftState: () => void;
|
||||||
handleOmoEditClick: () => void;
|
|
||||||
resetOmoDraftState: (useCommonConfig?: boolean) => void;
|
|
||||||
setOmoGlobalState: React.Dispatch<
|
|
||||||
React.SetStateAction<OmoGlobalConfig | null>
|
|
||||||
>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useOmoDraftState({
|
export function useOmoDraftState({
|
||||||
initialOmoSettings,
|
initialOmoSettings,
|
||||||
queriedOmoGlobalConfig,
|
|
||||||
isEditMode,
|
|
||||||
appId,
|
|
||||||
category,
|
category,
|
||||||
}: UseOmoDraftStateParams): OmoDraftState {
|
}: UseOmoDraftStateParams): OmoDraftState {
|
||||||
const { t } = useTranslation();
|
|
||||||
const isSlim = category === "omo-slim";
|
const isSlim = category === "omo-slim";
|
||||||
const commonConfigKey = isSlim ? "omo_slim" : "omo";
|
|
||||||
|
|
||||||
const [omoAgents, setOmoAgents] = useState<
|
const [omoAgents, setOmoAgents] = useState<
|
||||||
Record<string, Record<string, unknown>>
|
Record<string, Record<string, unknown>>
|
||||||
@@ -82,118 +53,25 @@ export function useOmoDraftState({
|
|||||||
return otherFields ? JSON.stringify(otherFields, null, 2) : "";
|
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(() => {
|
const mergedOmoJsonPreview = useMemo(() => {
|
||||||
if (useOmoCommonConfig) {
|
if (isSlim) {
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return JSON.stringify(
|
return JSON.stringify(
|
||||||
buildOmoProfilePreview(omoAgents, omoCategories, omoOtherFieldsStr),
|
buildOmoSlimProfilePreview(omoAgents, omoOtherFieldsStr),
|
||||||
null,
|
null,
|
||||||
2,
|
2,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [
|
return JSON.stringify(
|
||||||
useOmoCommonConfig,
|
buildOmoProfilePreview(omoAgents, omoCategories, omoOtherFieldsStr),
|
||||||
effectiveOmoGlobalConfig,
|
null,
|
||||||
omoAgents,
|
2,
|
||||||
omoCategories,
|
);
|
||||||
omoOtherFieldsStr,
|
}, [omoAgents, omoCategories, omoOtherFieldsStr, isSlim]);
|
||||||
isSlim,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Auto-detect whether common config has content for new OMO/OMO Slim profiles
|
const resetOmoDraftState = useCallback(() => {
|
||||||
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) => {
|
|
||||||
setOmoAgents({});
|
setOmoAgents({});
|
||||||
setOmoCategories({});
|
setOmoCategories({});
|
||||||
setOmoOtherFieldsStr("");
|
setOmoOtherFieldsStr("");
|
||||||
setUseOmoCommonConfig(useCommonConfig);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -203,18 +81,7 @@ export function useOmoDraftState({
|
|||||||
setOmoCategories,
|
setOmoCategories,
|
||||||
omoOtherFieldsStr,
|
omoOtherFieldsStr,
|
||||||
setOmoOtherFieldsStr,
|
setOmoOtherFieldsStr,
|
||||||
useOmoCommonConfig,
|
|
||||||
setUseOmoCommonConfig,
|
|
||||||
isOmoConfigModalOpen,
|
|
||||||
setIsOmoConfigModalOpen,
|
|
||||||
isOmoSaving,
|
|
||||||
omoGlobalConfigRef,
|
|
||||||
omoFieldsKey,
|
|
||||||
effectiveOmoGlobalConfig,
|
|
||||||
mergedOmoJsonPreview,
|
mergedOmoJsonPreview,
|
||||||
handleOmoGlobalConfigSave,
|
|
||||||
handleOmoEditClick,
|
|
||||||
resetOmoDraftState,
|
resetOmoDraftState,
|
||||||
setOmoGlobalState,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -525,15 +525,14 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
|||||||
{isLoadingTools || loadingTools[toolName] ? (
|
{isLoadingTools || loadingTools[toolName] ? (
|
||||||
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||||
) : tool?.version ? (
|
) : tool?.version ? (
|
||||||
<div className="flex items-center gap-1.5">
|
tool.latest_version &&
|
||||||
{tool.latest_version &&
|
tool.version !== tool.latest_version ? (
|
||||||
tool.version !== tool.latest_version && (
|
<span className="text-[10px] px-1.5 py-0.5 rounded-full bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 border border-yellow-500/20">
|
||||||
<span className="text-[10px] px-1.5 py-0.5 rounded-full bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 border border-yellow-500/20">
|
{tool.latest_version}
|
||||||
{tool.latest_version}
|
</span>
|
||||||
</span>
|
) : (
|
||||||
)}
|
|
||||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||||
</div>
|
)
|
||||||
) : (
|
) : (
|
||||||
<AlertCircle className="h-4 w-4 text-yellow-500" />
|
<AlertCircle className="h-4 w-4 text-yellow-500" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Pencil, RotateCcw, Check, X } from "lucide-react";
|
import { Pencil, RotateCcw, Check, X, Download, Trash2 } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -67,9 +67,20 @@ export function BackupListSection({
|
|||||||
onSettingsChange,
|
onSettingsChange,
|
||||||
}: BackupListSectionProps) {
|
}: BackupListSectionProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { backups, isLoading, restore, isRestoring, rename, isRenaming } =
|
const {
|
||||||
useBackupManager();
|
backups,
|
||||||
|
isLoading,
|
||||||
|
create,
|
||||||
|
isCreating,
|
||||||
|
restore,
|
||||||
|
isRestoring,
|
||||||
|
rename,
|
||||||
|
isRenaming,
|
||||||
|
remove,
|
||||||
|
isDeleting,
|
||||||
|
} = useBackupManager();
|
||||||
const [confirmFilename, setConfirmFilename] = useState<string | null>(null);
|
const [confirmFilename, setConfirmFilename] = useState<string | null>(null);
|
||||||
|
const [deleteFilename, setDeleteFilename] = useState<string | null>(null);
|
||||||
const [editingFilename, setEditingFilename] = useState<string | null>(null);
|
const [editingFilename, setEditingFilename] = useState<string | null>(null);
|
||||||
const [editValue, setEditValue] = useState("");
|
const [editValue, setEditValue] = useState("");
|
||||||
|
|
||||||
@@ -110,6 +121,26 @@ export function BackupListSection({
|
|||||||
setEditValue("");
|
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 () => {
|
const handleConfirmRename = async () => {
|
||||||
if (!editingFilename || !editValue.trim()) return;
|
if (!editingFilename || !editValue.trim()) return;
|
||||||
try {
|
try {
|
||||||
@@ -221,11 +252,45 @@ export function BackupListSection({
|
|||||||
|
|
||||||
{/* Backup list */}
|
{/* Backup list */}
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-sm font-medium mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
{t("settings.backupManager.title", {
|
<h4 className="text-sm font-medium">
|
||||||
defaultValue: "Database Backups",
|
{t("settings.backupManager.title", {
|
||||||
})}
|
defaultValue: "Database Backups",
|
||||||
</h4>
|
})}
|
||||||
|
</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 ? (
|
{isLoading ? (
|
||||||
<div className="text-sm text-muted-foreground py-2">Loading...</div>
|
<div className="text-sm text-muted-foreground py-2">Loading...</div>
|
||||||
@@ -298,18 +363,30 @@ export function BackupListSection({
|
|||||||
size="icon"
|
size="icon"
|
||||||
className="h-7 w-7"
|
className="h-7 w-7"
|
||||||
onClick={() => handleStartRename(backup.filename)}
|
onClick={() => handleStartRename(backup.filename)}
|
||||||
disabled={isRestoring || isRenaming}
|
disabled={isRestoring || isRenaming || isDeleting}
|
||||||
title={t("settings.backupManager.rename", {
|
title={t("settings.backupManager.rename", {
|
||||||
defaultValue: "Rename",
|
defaultValue: "Rename",
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Pencil className="h-3 w-3" />
|
<Pencil className="h-3 w-3" />
|
||||||
</Button>
|
</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
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-7 px-2 text-xs"
|
className="h-7 px-2 text-xs"
|
||||||
disabled={isRestoring}
|
disabled={isRestoring || isDeleting}
|
||||||
onClick={() => setConfirmFilename(backup.filename)}
|
onClick={() => setConfirmFilename(backup.filename)}
|
||||||
>
|
>
|
||||||
<RotateCcw className="h-3 w-3 mr-1" />
|
<RotateCcw className="h-3 w-3 mr-1" />
|
||||||
@@ -329,7 +406,7 @@ export function BackupListSection({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Confirmation Dialog */}
|
{/* Restore Confirmation Dialog */}
|
||||||
<Dialog
|
<Dialog
|
||||||
open={!!confirmFilename}
|
open={!!confirmFilename}
|
||||||
onOpenChange={(open) => !open && setConfirmFilename(null)}
|
onOpenChange={(open) => !open && setConfirmFilename(null)}
|
||||||
@@ -368,6 +445,50 @@ export function BackupListSection({
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import type { SettingsFormState } from "@/hooks/useSettings";
|
import type { SettingsFormState } from "@/hooks/useSettings";
|
||||||
import { AppWindow, MonitorUp, Power, EyeOff } from "lucide-react";
|
import { AppWindow, MonitorUp, Power, EyeOff } from "lucide-react";
|
||||||
import { ToggleRow } from "@/components/ui/toggle-row";
|
import { ToggleRow } from "@/components/ui/toggle-row";
|
||||||
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
|
||||||
interface WindowSettingsProps {
|
interface WindowSettingsProps {
|
||||||
settings: SettingsFormState;
|
settings: SettingsFormState;
|
||||||
@@ -27,13 +28,25 @@ export function WindowSettings({ settings, onChange }: WindowSettingsProps) {
|
|||||||
onCheckedChange={(value) => onChange({ launchOnStartup: value })}
|
onCheckedChange={(value) => onChange({ launchOnStartup: value })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ToggleRow
|
<AnimatePresence initial={false}>
|
||||||
icon={<EyeOff className="h-4 w-4 text-green-500" />}
|
{settings.launchOnStartup && (
|
||||||
title={t("settings.silentStartup")}
|
<motion.div
|
||||||
description={t("settings.silentStartupDescription")}
|
key="silent-startup"
|
||||||
checked={!!settings.silentStartup}
|
initial={{ opacity: 0, y: 10 }}
|
||||||
onCheckedChange={(value) => onChange({ silentStartup: value })}
|
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
|
<ToggleRow
|
||||||
icon={<MonitorUp className="h-4 w-4 text-purple-500" />}
|
icon={<MonitorUp className="h-4 w-4 text-purple-500" />}
|
||||||
|
|||||||
@@ -1,12 +1,24 @@
|
|||||||
import React, { useState, useEffect, useCallback } from "react";
|
import React, {
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
useCallback,
|
||||||
|
useRef,
|
||||||
|
useMemo,
|
||||||
|
} from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Calendar, Trash2, Plus } from "lucide-react";
|
import { Calendar, Trash2, Plus, Search, X, FolderOpen } from "lucide-react";
|
||||||
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
import { FullScreenPanel } from "@/components/common/FullScreenPanel";
|
import { FullScreenPanel } from "@/components/common/FullScreenPanel";
|
||||||
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
||||||
import MarkdownEditor from "@/components/MarkdownEditor";
|
import MarkdownEditor from "@/components/MarkdownEditor";
|
||||||
import { workspaceApi, type DailyMemoryFileInfo } from "@/lib/api/workspace";
|
import {
|
||||||
|
workspaceApi,
|
||||||
|
type DailyMemoryFileInfo,
|
||||||
|
type DailyMemorySearchResult,
|
||||||
|
} from "@/lib/api/workspace";
|
||||||
|
|
||||||
interface DailyMemoryPanelProps {
|
interface DailyMemoryPanelProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -46,6 +58,16 @@ const DailyMemoryPanel: React.FC<DailyMemoryPanelProps> = ({
|
|||||||
// Delete state
|
// Delete state
|
||||||
const [deletingFile, setDeletingFile] = useState<string | null>(null);
|
const [deletingFile, setDeletingFile] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Search state
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||||
|
const [searchResults, setSearchResults] = useState<DailyMemorySearchResult[]>(
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
const [searching, setSearching] = useState(false);
|
||||||
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
// Dark mode
|
// Dark mode
|
||||||
const [isDarkMode, setIsDarkMode] = useState(false);
|
const [isDarkMode, setIsDarkMode] = useState(false);
|
||||||
|
|
||||||
@@ -61,6 +83,100 @@ const DailyMemoryPanel: React.FC<DailyMemoryPanelProps> = ({
|
|||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Whether we are in active search mode (search open with a non-empty term)
|
||||||
|
const isActiveSearch = useMemo(
|
||||||
|
() => isSearchOpen && searchTerm.trim().length > 0,
|
||||||
|
[isSearchOpen, searchTerm],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Debounced search execution
|
||||||
|
const executeSearch = useCallback(
|
||||||
|
async (query: string) => {
|
||||||
|
if (!query.trim()) {
|
||||||
|
setSearchResults([]);
|
||||||
|
setSearching(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSearching(true);
|
||||||
|
try {
|
||||||
|
const results = await workspaceApi.searchDailyMemoryFiles(query.trim());
|
||||||
|
setSearchResults(results);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to search daily memory files:", err);
|
||||||
|
toast.error(t("workspace.dailyMemory.searchFailed"));
|
||||||
|
} finally {
|
||||||
|
setSearching(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[t],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle search input change with debounce
|
||||||
|
const handleSearchChange = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
setSearchTerm(value);
|
||||||
|
if (debounceTimerRef.current) {
|
||||||
|
clearTimeout(debounceTimerRef.current);
|
||||||
|
}
|
||||||
|
debounceTimerRef.current = setTimeout(() => {
|
||||||
|
void executeSearch(value);
|
||||||
|
}, 300);
|
||||||
|
},
|
||||||
|
[executeSearch],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Open search bar
|
||||||
|
const openSearch = useCallback(() => {
|
||||||
|
setIsSearchOpen(true);
|
||||||
|
// Focus input on next frame
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
searchInputRef.current?.focus();
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Close search bar and clear state
|
||||||
|
const closeSearch = useCallback(() => {
|
||||||
|
setIsSearchOpen(false);
|
||||||
|
setSearchTerm("");
|
||||||
|
setSearchResults([]);
|
||||||
|
setSearching(false);
|
||||||
|
if (debounceTimerRef.current) {
|
||||||
|
clearTimeout(debounceTimerRef.current);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Keyboard shortcut: Cmd/Ctrl+F to open search, Escape to close
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen || editingFile) return;
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if ((e.metaKey || e.ctrlKey) && e.key === "f") {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!isSearchOpen) {
|
||||||
|
openSearch();
|
||||||
|
} else {
|
||||||
|
searchInputRef.current?.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (e.key === "Escape" && isSearchOpen) {
|
||||||
|
e.preventDefault();
|
||||||
|
closeSearch();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
}, [isOpen, editingFile, isSearchOpen, openSearch, closeSearch]);
|
||||||
|
|
||||||
|
// Clean up debounce timer on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (debounceTimerRef.current) {
|
||||||
|
clearTimeout(debounceTimerRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Load file list
|
// Load file list
|
||||||
const loadFiles = useCallback(async () => {
|
const loadFiles = useCallback(async () => {
|
||||||
setLoadingList(true);
|
setLoadingList(true);
|
||||||
@@ -142,24 +258,44 @@ const DailyMemoryPanel: React.FC<DailyMemoryPanelProps> = ({
|
|||||||
setEditingFile(null);
|
setEditingFile(null);
|
||||||
}
|
}
|
||||||
await loadFiles();
|
await loadFiles();
|
||||||
|
// Re-trigger search if active
|
||||||
|
if (isSearchOpen && searchTerm.trim()) {
|
||||||
|
void executeSearch(searchTerm);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to delete daily memory file:", err);
|
console.error("Failed to delete daily memory file:", err);
|
||||||
toast.error(t("workspace.dailyMemory.deleteFailed"));
|
toast.error(t("workspace.dailyMemory.deleteFailed"));
|
||||||
setDeletingFile(null);
|
setDeletingFile(null);
|
||||||
}
|
}
|
||||||
}, [deletingFile, editingFile, loadFiles, t]);
|
}, [
|
||||||
|
deletingFile,
|
||||||
|
editingFile,
|
||||||
|
loadFiles,
|
||||||
|
t,
|
||||||
|
isSearchOpen,
|
||||||
|
searchTerm,
|
||||||
|
executeSearch,
|
||||||
|
]);
|
||||||
|
|
||||||
// Back from edit mode to list mode
|
// Back from edit mode to list mode — preserve search state
|
||||||
const handleBackToList = useCallback(() => {
|
const handleBackToList = useCallback(() => {
|
||||||
setEditingFile(null);
|
setEditingFile(null);
|
||||||
setContent("");
|
setContent("");
|
||||||
void loadFiles();
|
void loadFiles();
|
||||||
}, [loadFiles]);
|
// Re-trigger search if active (file content may have changed)
|
||||||
|
if (isSearchOpen && searchTerm.trim()) {
|
||||||
|
void executeSearch(searchTerm);
|
||||||
|
}
|
||||||
|
}, [loadFiles, isSearchOpen, searchTerm, executeSearch]);
|
||||||
|
|
||||||
// Close panel entirely
|
// Close panel entirely — clear search state
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
setEditingFile(null);
|
setEditingFile(null);
|
||||||
setContent("");
|
setContent("");
|
||||||
|
setIsSearchOpen(false);
|
||||||
|
setSearchTerm("");
|
||||||
|
setSearchResults([]);
|
||||||
|
setSearching(false);
|
||||||
onClose();
|
onClose();
|
||||||
}, [onClose]);
|
}, [onClose]);
|
||||||
|
|
||||||
@@ -214,24 +350,142 @@ const DailyMemoryPanel: React.FC<DailyMemoryPanelProps> = ({
|
|||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
>
|
>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Header with path and create button */}
|
{/* Header with path, search, and create button */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<p className="text-sm text-muted-foreground">
|
<p
|
||||||
~/.openclaw/workspace/memory/
|
className="text-sm text-muted-foreground shrink-0 cursor-pointer hover:text-foreground transition-colors inline-flex items-center gap-1"
|
||||||
</p>
|
onClick={() => workspaceApi.openDirectory("memory")}
|
||||||
<Button
|
title={t("workspace.openDirectory")}
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleCreateToday}
|
|
||||||
className="gap-1.5"
|
|
||||||
>
|
>
|
||||||
<Plus className="w-3.5 h-3.5" />
|
~/.openclaw/workspace/memory/
|
||||||
{t("workspace.dailyMemory.createToday")}
|
<FolderOpen className="w-3.5 h-3.5" />
|
||||||
</Button>
|
</p>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8"
|
||||||
|
onClick={isSearchOpen ? closeSearch : openSearch}
|
||||||
|
title={t("workspace.dailyMemory.searchScopeHint")}
|
||||||
|
>
|
||||||
|
<Search className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleCreateToday}
|
||||||
|
className="gap-1.5"
|
||||||
|
>
|
||||||
|
<Plus className="w-3.5 h-3.5" />
|
||||||
|
{t("workspace.dailyMemory.createToday")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* File list */}
|
{/* Search bar */}
|
||||||
{loadingList ? (
|
<AnimatePresence>
|
||||||
|
{isSearchOpen && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ height: 0, opacity: 0 }}
|
||||||
|
animate={{ height: "auto", opacity: 1 }}
|
||||||
|
exit={{ height: 0, opacity: 0 }}
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
|
className="overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground pointer-events-none" />
|
||||||
|
<Input
|
||||||
|
ref={searchInputRef}
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => handleSearchChange(e.target.value)}
|
||||||
|
placeholder={t("workspace.dailyMemory.searchPlaceholder")}
|
||||||
|
className="pl-8 pr-8 h-8 text-sm"
|
||||||
|
/>
|
||||||
|
{searchTerm && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleSearchChange("")}
|
||||||
|
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={closeSearch}
|
||||||
|
className="text-xs text-muted-foreground h-8 px-2 shrink-0"
|
||||||
|
>
|
||||||
|
{t("workspace.dailyMemory.searchCloseHint")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* Content: search results or normal file list */}
|
||||||
|
{isActiveSearch ? (
|
||||||
|
// --- Search results ---
|
||||||
|
searching ? (
|
||||||
|
<div className="flex items-center justify-center h-48 text-muted-foreground">
|
||||||
|
{t("workspace.dailyMemory.searching")}
|
||||||
|
</div>
|
||||||
|
) : searchResults.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center h-48 text-muted-foreground gap-3 border-2 border-dashed border-border rounded-xl">
|
||||||
|
<Search className="w-10 h-10 opacity-40" />
|
||||||
|
<p className="text-sm">
|
||||||
|
{t("workspace.dailyMemory.noSearchResults")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{searchResults.map((result) => (
|
||||||
|
<button
|
||||||
|
key={result.filename}
|
||||||
|
onClick={() => openFile(result.filename)}
|
||||||
|
className="w-full flex items-start gap-3 p-4 rounded-xl border border-border bg-card hover:bg-accent/50 transition-colors text-left group"
|
||||||
|
>
|
||||||
|
<div className="mt-0.5 text-muted-foreground group-hover:text-foreground transition-colors">
|
||||||
|
<Calendar className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-medium text-sm text-foreground">
|
||||||
|
{result.date}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{formatFileSize(result.sizeBytes)}
|
||||||
|
</span>
|
||||||
|
{result.matchCount > 0 && (
|
||||||
|
<span className="text-[10px] px-1.5 py-0.5 rounded-full bg-primary/10 text-primary font-medium">
|
||||||
|
{t("workspace.dailyMemory.matchCount", {
|
||||||
|
count: result.matchCount,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{result.snippet && (
|
||||||
|
<p className="text-xs text-muted-foreground mt-1 line-clamp-2 whitespace-pre-line">
|
||||||
|
{result.snippet}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setDeletingFile(result.filename);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4 text-muted-foreground hover:text-destructive transition-colors" />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : // --- Normal file list ---
|
||||||
|
loadingList ? (
|
||||||
<div className="flex items-center justify-center h-48 text-muted-foreground">
|
<div className="flex items-center justify-center h-48 text-muted-foreground">
|
||||||
{t("prompts.loading")}
|
{t("prompts.loading")}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
Circle,
|
Circle,
|
||||||
Calendar,
|
Calendar,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
|
FolderOpen,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { LucideIcon } from "lucide-react";
|
import type { LucideIcon } from "lucide-react";
|
||||||
import { workspaceApi } from "@/lib/api/workspace";
|
import { workspaceApi } from "@/lib/api/workspace";
|
||||||
@@ -83,8 +84,13 @@ const WorkspaceFilesPanel: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-6 pt-4 pb-8">
|
<div className="px-6 pt-4 pb-8">
|
||||||
<p className="text-sm text-muted-foreground mb-6">
|
<p
|
||||||
|
className="text-sm text-muted-foreground mb-6 cursor-pointer hover:text-foreground transition-colors inline-flex items-center gap-1"
|
||||||
|
onClick={() => workspaceApi.openDirectory("workspace")}
|
||||||
|
title={t("workspace.openDirectory")}
|
||||||
|
>
|
||||||
~/.openclaw/workspace/
|
~/.openclaw/workspace/
|
||||||
|
<FolderOpen className="w-3.5 h-3.5" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
|
|||||||
@@ -92,10 +92,10 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
env: {
|
env: {
|
||||||
ANTHROPIC_BASE_URL: "https://open.bigmodel.cn/api/anthropic",
|
ANTHROPIC_BASE_URL: "https://open.bigmodel.cn/api/anthropic",
|
||||||
ANTHROPIC_AUTH_TOKEN: "",
|
ANTHROPIC_AUTH_TOKEN: "",
|
||||||
ANTHROPIC_MODEL: "glm-4.7",
|
ANTHROPIC_MODEL: "glm-5",
|
||||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: "glm-4.7",
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: "glm-5",
|
||||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "glm-4.7",
|
ANTHROPIC_DEFAULT_SONNET_MODEL: "glm-5",
|
||||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "glm-4.7",
|
ANTHROPIC_DEFAULT_OPUS_MODEL: "glm-5",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
category: "cn_official",
|
category: "cn_official",
|
||||||
@@ -110,10 +110,10 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
env: {
|
env: {
|
||||||
ANTHROPIC_BASE_URL: "https://api.z.ai/api/anthropic",
|
ANTHROPIC_BASE_URL: "https://api.z.ai/api/anthropic",
|
||||||
ANTHROPIC_AUTH_TOKEN: "",
|
ANTHROPIC_AUTH_TOKEN: "",
|
||||||
ANTHROPIC_MODEL: "glm-4.7",
|
ANTHROPIC_MODEL: "glm-5",
|
||||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: "glm-4.7",
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: "glm-5",
|
||||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "glm-4.7",
|
ANTHROPIC_DEFAULT_SONNET_MODEL: "glm-5",
|
||||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "glm-4.7",
|
ANTHROPIC_DEFAULT_OPUS_MODEL: "glm-5",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
category: "cn_official",
|
category: "cn_official",
|
||||||
@@ -170,10 +170,10 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
env: {
|
env: {
|
||||||
ANTHROPIC_BASE_URL: "https://api-inference.modelscope.cn",
|
ANTHROPIC_BASE_URL: "https://api-inference.modelscope.cn",
|
||||||
ANTHROPIC_AUTH_TOKEN: "",
|
ANTHROPIC_AUTH_TOKEN: "",
|
||||||
ANTHROPIC_MODEL: "ZhipuAI/GLM-4.7",
|
ANTHROPIC_MODEL: "ZhipuAI/GLM-5",
|
||||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: "ZhipuAI/GLM-4.7",
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: "ZhipuAI/GLM-5",
|
||||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "ZhipuAI/GLM-4.7",
|
ANTHROPIC_DEFAULT_SONNET_MODEL: "ZhipuAI/GLM-5",
|
||||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "ZhipuAI/GLM-4.7",
|
ANTHROPIC_DEFAULT_OPUS_MODEL: "ZhipuAI/GLM-5",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
category: "aggregator",
|
category: "aggregator",
|
||||||
@@ -236,10 +236,10 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
ANTHROPIC_AUTH_TOKEN: "",
|
ANTHROPIC_AUTH_TOKEN: "",
|
||||||
API_TIMEOUT_MS: "3000000",
|
API_TIMEOUT_MS: "3000000",
|
||||||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 1,
|
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 1,
|
||||||
ANTHROPIC_MODEL: "MiniMax-M2.1",
|
ANTHROPIC_MODEL: "MiniMax-M2.5",
|
||||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "MiniMax-M2.1",
|
ANTHROPIC_DEFAULT_SONNET_MODEL: "MiniMax-M2.5",
|
||||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "MiniMax-M2.1",
|
ANTHROPIC_DEFAULT_OPUS_MODEL: "MiniMax-M2.5",
|
||||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: "MiniMax-M2.1",
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: "MiniMax-M2.5",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
category: "cn_official",
|
category: "cn_official",
|
||||||
@@ -262,10 +262,10 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
ANTHROPIC_AUTH_TOKEN: "",
|
ANTHROPIC_AUTH_TOKEN: "",
|
||||||
API_TIMEOUT_MS: "3000000",
|
API_TIMEOUT_MS: "3000000",
|
||||||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 1,
|
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 1,
|
||||||
ANTHROPIC_MODEL: "MiniMax-M2.1",
|
ANTHROPIC_MODEL: "MiniMax-M2.5",
|
||||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "MiniMax-M2.1",
|
ANTHROPIC_DEFAULT_SONNET_MODEL: "MiniMax-M2.5",
|
||||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "MiniMax-M2.1",
|
ANTHROPIC_DEFAULT_OPUS_MODEL: "MiniMax-M2.5",
|
||||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: "MiniMax-M2.1",
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: "MiniMax-M2.5",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
category: "cn_official",
|
category: "cn_official",
|
||||||
@@ -287,10 +287,10 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
ANTHROPIC_BASE_URL: "https://ark.cn-beijing.volces.com/api/coding",
|
ANTHROPIC_BASE_URL: "https://ark.cn-beijing.volces.com/api/coding",
|
||||||
ANTHROPIC_AUTH_TOKEN: "",
|
ANTHROPIC_AUTH_TOKEN: "",
|
||||||
API_TIMEOUT_MS: "3000000",
|
API_TIMEOUT_MS: "3000000",
|
||||||
ANTHROPIC_MODEL: "doubao-seed-code-preview-latest",
|
ANTHROPIC_MODEL: "doubao-seed-2-0-code-preview-latest",
|
||||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "doubao-seed-code-preview-latest",
|
ANTHROPIC_DEFAULT_SONNET_MODEL: "doubao-seed-2-0-code-preview-latest",
|
||||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "doubao-seed-code-preview-latest",
|
ANTHROPIC_DEFAULT_OPUS_MODEL: "doubao-seed-2-0-code-preview-latest",
|
||||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: "doubao-seed-code-preview-latest",
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: "doubao-seed-2-0-code-preview-latest",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
category: "cn_official",
|
category: "cn_official",
|
||||||
@@ -304,10 +304,10 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
env: {
|
env: {
|
||||||
ANTHROPIC_BASE_URL: "https://api.tbox.cn/api/anthropic",
|
ANTHROPIC_BASE_URL: "https://api.tbox.cn/api/anthropic",
|
||||||
ANTHROPIC_AUTH_TOKEN: "",
|
ANTHROPIC_AUTH_TOKEN: "",
|
||||||
ANTHROPIC_MODEL: "Ling-1T",
|
ANTHROPIC_MODEL: "Ling-2.5-1T",
|
||||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: "Ling-1T",
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: "Ling-2.5-1T",
|
||||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "Ling-1T",
|
ANTHROPIC_DEFAULT_SONNET_MODEL: "Ling-2.5-1T",
|
||||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "Ling-1T",
|
ANTHROPIC_DEFAULT_OPUS_MODEL: "Ling-2.5-1T",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
category: "cn_official",
|
category: "cn_official",
|
||||||
@@ -316,12 +316,10 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
name: "AiHubMix",
|
name: "AiHubMix",
|
||||||
websiteUrl: "https://aihubmix.com",
|
websiteUrl: "https://aihubmix.com",
|
||||||
apiKeyUrl: "https://aihubmix.com",
|
apiKeyUrl: "https://aihubmix.com",
|
||||||
// 说明:该供应商使用 ANTHROPIC_API_KEY(而非 ANTHROPIC_AUTH_TOKEN)
|
|
||||||
apiKeyField: "ANTHROPIC_API_KEY",
|
|
||||||
settingsConfig: {
|
settingsConfig: {
|
||||||
env: {
|
env: {
|
||||||
ANTHROPIC_BASE_URL: "https://aihubmix.com",
|
ANTHROPIC_BASE_URL: "https://aihubmix.com",
|
||||||
ANTHROPIC_API_KEY: "",
|
ANTHROPIC_AUTH_TOKEN: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// 请求地址候选(用于地址管理/测速),用户可自行选择/覆盖
|
// 请求地址候选(用于地址管理/测速),用户可自行选择/覆盖
|
||||||
@@ -338,10 +336,10 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
env: {
|
env: {
|
||||||
ANTHROPIC_BASE_URL: "https://api.siliconflow.cn",
|
ANTHROPIC_BASE_URL: "https://api.siliconflow.cn",
|
||||||
ANTHROPIC_AUTH_TOKEN: "",
|
ANTHROPIC_AUTH_TOKEN: "",
|
||||||
ANTHROPIC_MODEL: "Pro/MiniMaxAI/MiniMax-M2.1",
|
ANTHROPIC_MODEL: "Pro/MiniMaxAI/MiniMax-M2.5",
|
||||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: "Pro/MiniMaxAI/MiniMax-M2.1",
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: "Pro/MiniMaxAI/MiniMax-M2.5",
|
||||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "Pro/MiniMaxAI/MiniMax-M2.1",
|
ANTHROPIC_DEFAULT_SONNET_MODEL: "Pro/MiniMaxAI/MiniMax-M2.5",
|
||||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "Pro/MiniMaxAI/MiniMax-M2.1",
|
ANTHROPIC_DEFAULT_OPUS_MODEL: "Pro/MiniMaxAI/MiniMax-M2.5",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
category: "aggregator",
|
category: "aggregator",
|
||||||
@@ -356,10 +354,10 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
env: {
|
env: {
|
||||||
ANTHROPIC_BASE_URL: "https://api.siliconflow.com",
|
ANTHROPIC_BASE_URL: "https://api.siliconflow.com",
|
||||||
ANTHROPIC_AUTH_TOKEN: "",
|
ANTHROPIC_AUTH_TOKEN: "",
|
||||||
ANTHROPIC_MODEL: "MiniMaxAI/MiniMax-M2.1",
|
ANTHROPIC_MODEL: "MiniMaxAI/MiniMax-M2.5",
|
||||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: "MiniMaxAI/MiniMax-M2.1",
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: "MiniMaxAI/MiniMax-M2.5",
|
||||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "MiniMaxAI/MiniMax-M2.1",
|
ANTHROPIC_DEFAULT_SONNET_MODEL: "MiniMaxAI/MiniMax-M2.5",
|
||||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "MiniMaxAI/MiniMax-M2.1",
|
ANTHROPIC_DEFAULT_OPUS_MODEL: "MiniMaxAI/MiniMax-M2.5",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
category: "aggregator",
|
category: "aggregator",
|
||||||
@@ -478,6 +476,61 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
icon: "aicodemirror",
|
icon: "aicodemirror",
|
||||||
iconColor: "#000000",
|
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",
|
name: "OpenRouter",
|
||||||
websiteUrl: "https://openrouter.ai",
|
websiteUrl: "https://openrouter.ai",
|
||||||
@@ -486,10 +539,10 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
env: {
|
env: {
|
||||||
ANTHROPIC_BASE_URL: "https://openrouter.ai/api",
|
ANTHROPIC_BASE_URL: "https://openrouter.ai/api",
|
||||||
ANTHROPIC_AUTH_TOKEN: "",
|
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_HAIKU_MODEL: "anthropic/claude-haiku-4.5",
|
||||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "anthropic/claude-sonnet-4.5",
|
ANTHROPIC_DEFAULT_SONNET_MODEL: "anthropic/claude-sonnet-4.6",
|
||||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "anthropic/claude-opus-4.5",
|
ANTHROPIC_DEFAULT_OPUS_MODEL: "anthropic/claude-opus-4.6",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
category: "aggregator",
|
category: "aggregator",
|
||||||
@@ -533,4 +586,71 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
icon: "xiaomimimo",
|
icon: "xiaomimimo",
|
||||||
iconColor: "#000000",
|
iconColor: "#000000",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "AWS Bedrock (AKSK)",
|
||||||
|
websiteUrl: "https://aws.amazon.com/bedrock/",
|
||||||
|
settingsConfig: {
|
||||||
|
env: {
|
||||||
|
ANTHROPIC_BASE_URL:
|
||||||
|
"https://bedrock-runtime.${AWS_REGION}.amazonaws.com",
|
||||||
|
AWS_ACCESS_KEY_ID: "${AWS_ACCESS_KEY_ID}",
|
||||||
|
AWS_SECRET_ACCESS_KEY: "${AWS_SECRET_ACCESS_KEY}",
|
||||||
|
AWS_REGION: "${AWS_REGION}",
|
||||||
|
ANTHROPIC_MODEL: "global.anthropic.claude-opus-4-6-v1",
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL:
|
||||||
|
"global.anthropic.claude-haiku-4-5-20251001-v1:0",
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL: "global.anthropic.claude-sonnet-4-6",
|
||||||
|
ANTHROPIC_DEFAULT_OPUS_MODEL: "global.anthropic.claude-opus-4-6-v1",
|
||||||
|
CLAUDE_CODE_USE_BEDROCK: "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
category: "cloud_provider",
|
||||||
|
templateValues: {
|
||||||
|
AWS_REGION: {
|
||||||
|
label: "AWS Region",
|
||||||
|
placeholder: "us-west-2",
|
||||||
|
editorValue: "us-west-2",
|
||||||
|
},
|
||||||
|
AWS_ACCESS_KEY_ID: {
|
||||||
|
label: "Access Key ID",
|
||||||
|
placeholder: "AKIA...",
|
||||||
|
editorValue: "",
|
||||||
|
},
|
||||||
|
AWS_SECRET_ACCESS_KEY: {
|
||||||
|
label: "Secret Access Key",
|
||||||
|
placeholder: "your-secret-key",
|
||||||
|
editorValue: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
icon: "aws",
|
||||||
|
iconColor: "#FF9900",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AWS Bedrock (API Key)",
|
||||||
|
websiteUrl: "https://aws.amazon.com/bedrock/",
|
||||||
|
settingsConfig: {
|
||||||
|
apiKey: "",
|
||||||
|
env: {
|
||||||
|
ANTHROPIC_BASE_URL:
|
||||||
|
"https://bedrock-runtime.${AWS_REGION}.amazonaws.com",
|
||||||
|
AWS_REGION: "${AWS_REGION}",
|
||||||
|
ANTHROPIC_MODEL: "global.anthropic.claude-opus-4-6-v1",
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL:
|
||||||
|
"global.anthropic.claude-haiku-4-5-20251001-v1:0",
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL: "global.anthropic.claude-sonnet-4-6",
|
||||||
|
ANTHROPIC_DEFAULT_OPUS_MODEL: "global.anthropic.claude-opus-4-6-v1",
|
||||||
|
CLAUDE_CODE_USE_BEDROCK: "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
category: "cloud_provider",
|
||||||
|
templateValues: {
|
||||||
|
AWS_REGION: {
|
||||||
|
label: "AWS Region",
|
||||||
|
placeholder: "us-west-2",
|
||||||
|
editorValue: "us-west-2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
icon: "aws",
|
||||||
|
iconColor: "#FF9900",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -227,6 +227,59 @@ requires_openai_auth = true`,
|
|||||||
icon: "aicodemirror",
|
icon: "aicodemirror",
|
||||||
iconColor: "#000000",
|
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",
|
name: "OpenRouter",
|
||||||
websiteUrl: "https://openrouter.ai",
|
websiteUrl: "https://openrouter.ai",
|
||||||
|
|||||||
@@ -139,6 +139,70 @@ export const geminiProviderPresets: GeminiProviderPreset[] = [
|
|||||||
icon: "aicodemirror",
|
icon: "aicodemirror",
|
||||||
iconColor: "#000000",
|
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",
|
name: "OpenRouter",
|
||||||
websiteUrl: "https://openrouter.ai",
|
websiteUrl: "https://openrouter.ai",
|
||||||
|
|||||||
@@ -112,16 +112,14 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
|||||||
api: "openai-completions",
|
api: "openai-completions",
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
id: "glm-4.7",
|
id: "glm-5",
|
||||||
name: "GLM-4.7",
|
name: "GLM-5",
|
||||||
contextWindow: 128000,
|
contextWindow: 128000,
|
||||||
cost: { input: 0.001, output: 0.001 },
|
cost: { input: 0.001, output: 0.001 },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
category: "cn_official",
|
category: "cn_official",
|
||||||
isPartner: true,
|
|
||||||
partnerPromotionKey: "zhipu",
|
|
||||||
icon: "zhipu",
|
icon: "zhipu",
|
||||||
iconColor: "#0F62FE",
|
iconColor: "#0F62FE",
|
||||||
templateValues: {
|
templateValues: {
|
||||||
@@ -138,8 +136,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
suggestedDefaults: {
|
suggestedDefaults: {
|
||||||
model: { primary: "zhipu/glm-4.7" },
|
model: { primary: "zhipu/glm-5" },
|
||||||
modelCatalog: { "zhipu/glm-4.7": { alias: "GLM" } },
|
modelCatalog: { "zhipu/glm-5": { alias: "GLM" } },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -152,16 +150,14 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
|||||||
api: "openai-completions",
|
api: "openai-completions",
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
id: "glm-4.7",
|
id: "glm-5",
|
||||||
name: "GLM-4.7",
|
name: "GLM-5",
|
||||||
contextWindow: 128000,
|
contextWindow: 128000,
|
||||||
cost: { input: 0.001, output: 0.001 },
|
cost: { input: 0.001, output: 0.001 },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
category: "cn_official",
|
category: "cn_official",
|
||||||
isPartner: true,
|
|
||||||
partnerPromotionKey: "zhipu",
|
|
||||||
icon: "zhipu",
|
icon: "zhipu",
|
||||||
iconColor: "#0F62FE",
|
iconColor: "#0F62FE",
|
||||||
templateValues: {
|
templateValues: {
|
||||||
@@ -178,8 +174,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
suggestedDefaults: {
|
suggestedDefaults: {
|
||||||
model: { primary: "zhipu-en/glm-4.7" },
|
model: { primary: "zhipu-en/glm-5" },
|
||||||
modelCatalog: { "zhipu-en/glm-4.7": { alias: "GLM" } },
|
modelCatalog: { "zhipu-en/glm-5": { alias: "GLM" } },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -192,8 +188,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
|||||||
api: "openai-completions",
|
api: "openai-completions",
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
id: "qwen3-max",
|
id: "qwen3.5-plus",
|
||||||
name: "Qwen3 Max",
|
name: "Qwen3.5 Plus",
|
||||||
contextWindow: 32000,
|
contextWindow: 32000,
|
||||||
cost: { input: 0.002, output: 0.006 },
|
cost: { input: 0.002, output: 0.006 },
|
||||||
},
|
},
|
||||||
@@ -216,8 +212,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
suggestedDefaults: {
|
suggestedDefaults: {
|
||||||
model: { primary: "qwen/qwen3-max" },
|
model: { primary: "qwen/qwen3.5-plus" },
|
||||||
modelCatalog: { "qwen/qwen3-max": { alias: "Qwen" } },
|
modelCatalog: { "qwen/qwen3.5-plus": { alias: "Qwen" } },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -306,8 +302,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
|||||||
api: "openai-completions",
|
api: "openai-completions",
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
id: "MiniMax-M2.1",
|
id: "MiniMax-M2.5",
|
||||||
name: "MiniMax M2.1",
|
name: "MiniMax M2.5",
|
||||||
contextWindow: 200000,
|
contextWindow: 200000,
|
||||||
cost: { input: 0.001, output: 0.004 },
|
cost: { input: 0.001, output: 0.004 },
|
||||||
},
|
},
|
||||||
@@ -330,8 +326,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
suggestedDefaults: {
|
suggestedDefaults: {
|
||||||
model: { primary: "minimax/MiniMax-M2.1" },
|
model: { primary: "minimax/MiniMax-M2.5" },
|
||||||
modelCatalog: { "minimax/MiniMax-M2.1": { alias: "MiniMax" } },
|
modelCatalog: { "minimax/MiniMax-M2.5": { alias: "MiniMax" } },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -344,8 +340,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
|||||||
api: "openai-completions",
|
api: "openai-completions",
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
id: "MiniMax-M2.1",
|
id: "MiniMax-M2.5",
|
||||||
name: "MiniMax M2.1",
|
name: "MiniMax M2.5",
|
||||||
contextWindow: 200000,
|
contextWindow: 200000,
|
||||||
cost: { input: 0.001, output: 0.004 },
|
cost: { input: 0.001, output: 0.004 },
|
||||||
},
|
},
|
||||||
@@ -368,8 +364,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
suggestedDefaults: {
|
suggestedDefaults: {
|
||||||
model: { primary: "minimax-en/MiniMax-M2.1" },
|
model: { primary: "minimax-en/MiniMax-M2.5" },
|
||||||
modelCatalog: { "minimax-en/MiniMax-M2.1": { alias: "MiniMax" } },
|
modelCatalog: { "minimax-en/MiniMax-M2.5": { alias: "MiniMax" } },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -465,7 +461,7 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
|||||||
api: "openai-completions",
|
api: "openai-completions",
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
id: "doubao-seed-code-preview-latest",
|
id: "doubao-seed-2-0-code-preview-latest",
|
||||||
name: "DouBao Seed Code Preview",
|
name: "DouBao Seed Code Preview",
|
||||||
contextWindow: 128000,
|
contextWindow: 128000,
|
||||||
cost: { input: 0.002, output: 0.006 },
|
cost: { input: 0.002, output: 0.006 },
|
||||||
@@ -483,9 +479,9 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
suggestedDefaults: {
|
suggestedDefaults: {
|
||||||
model: { primary: "doubaoseed/doubao-seed-code-preview-latest" },
|
model: { primary: "doubaoseed/doubao-seed-2-0-code-preview-latest" },
|
||||||
modelCatalog: {
|
modelCatalog: {
|
||||||
"doubaoseed/doubao-seed-code-preview-latest": { alias: "DouBao" },
|
"doubaoseed/doubao-seed-2-0-code-preview-latest": { alias: "DouBao" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -498,8 +494,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
|||||||
api: "openai-completions",
|
api: "openai-completions",
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
id: "Ling-1T",
|
id: "Ling-2.5-1T",
|
||||||
name: "Ling 1T",
|
name: "Ling 2.5 1T",
|
||||||
contextWindow: 128000,
|
contextWindow: 128000,
|
||||||
cost: { input: 0.001, output: 0.004 },
|
cost: { input: 0.001, output: 0.004 },
|
||||||
},
|
},
|
||||||
@@ -514,8 +510,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
suggestedDefaults: {
|
suggestedDefaults: {
|
||||||
model: { primary: "bailing/Ling-1T" },
|
model: { primary: "bailing/Ling-2.5-1T" },
|
||||||
modelCatalog: { "bailing/Ling-1T": { alias: "BaiLing" } },
|
modelCatalog: { "bailing/Ling-2.5-1T": { alias: "BaiLing" } },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -562,8 +558,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
|||||||
api: "anthropic-messages",
|
api: "anthropic-messages",
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
id: "claude-sonnet-4-5-20250929",
|
id: "claude-sonnet-4-6",
|
||||||
name: "Claude Sonnet 4.5",
|
name: "Claude Sonnet 4.6",
|
||||||
contextWindow: 200000,
|
contextWindow: 200000,
|
||||||
cost: { input: 3, output: 15 },
|
cost: { input: 3, output: 15 },
|
||||||
},
|
},
|
||||||
@@ -587,11 +583,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
|||||||
},
|
},
|
||||||
suggestedDefaults: {
|
suggestedDefaults: {
|
||||||
model: {
|
model: {
|
||||||
primary: "aihubmix/claude-sonnet-4-5-20250929",
|
primary: "aihubmix/claude-sonnet-4-6",
|
||||||
fallbacks: ["aihubmix/claude-opus-4-6"],
|
fallbacks: ["aihubmix/claude-opus-4-6"],
|
||||||
},
|
},
|
||||||
modelCatalog: {
|
modelCatalog: {
|
||||||
"aihubmix/claude-sonnet-4-5-20250929": { alias: "Sonnet" },
|
"aihubmix/claude-sonnet-4-6": { alias: "Sonnet" },
|
||||||
"aihubmix/claude-opus-4-6": { alias: "Opus" },
|
"aihubmix/claude-opus-4-6": { alias: "Opus" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -606,8 +602,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
|||||||
api: "anthropic-messages",
|
api: "anthropic-messages",
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
id: "claude-sonnet-4-5-20250929",
|
id: "claude-sonnet-4-6",
|
||||||
name: "Claude Sonnet 4.5",
|
name: "Claude Sonnet 4.6",
|
||||||
contextWindow: 200000,
|
contextWindow: 200000,
|
||||||
cost: { input: 3, output: 15 },
|
cost: { input: 3, output: 15 },
|
||||||
},
|
},
|
||||||
@@ -631,11 +627,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
|||||||
},
|
},
|
||||||
suggestedDefaults: {
|
suggestedDefaults: {
|
||||||
model: {
|
model: {
|
||||||
primary: "dmxapi/claude-sonnet-4-5-20250929",
|
primary: "dmxapi/claude-sonnet-4-6",
|
||||||
fallbacks: ["dmxapi/claude-opus-4-6"],
|
fallbacks: ["dmxapi/claude-opus-4-6"],
|
||||||
},
|
},
|
||||||
modelCatalog: {
|
modelCatalog: {
|
||||||
"dmxapi/claude-sonnet-4-5-20250929": { alias: "Sonnet" },
|
"dmxapi/claude-sonnet-4-6": { alias: "Sonnet" },
|
||||||
"dmxapi/claude-opus-4-6": { alias: "Opus" },
|
"dmxapi/claude-opus-4-6": { alias: "Opus" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -650,8 +646,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
|||||||
api: "openai-completions",
|
api: "openai-completions",
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
id: "anthropic/claude-sonnet-4.5",
|
id: "anthropic/claude-sonnet-4.6",
|
||||||
name: "Claude Sonnet 4.5",
|
name: "Claude Sonnet 4.6",
|
||||||
contextWindow: 200000,
|
contextWindow: 200000,
|
||||||
cost: { input: 3, output: 15 },
|
cost: { input: 3, output: 15 },
|
||||||
},
|
},
|
||||||
@@ -675,11 +671,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
|||||||
},
|
},
|
||||||
suggestedDefaults: {
|
suggestedDefaults: {
|
||||||
model: {
|
model: {
|
||||||
primary: "openrouter/anthropic/claude-sonnet-4.5",
|
primary: "openrouter/anthropic/claude-sonnet-4.6",
|
||||||
fallbacks: ["openrouter/anthropic/claude-opus-4.6"],
|
fallbacks: ["openrouter/anthropic/claude-opus-4.6"],
|
||||||
},
|
},
|
||||||
modelCatalog: {
|
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" },
|
"openrouter/anthropic/claude-opus-4.6": { alias: "Opus" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -694,8 +690,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
|||||||
api: "openai-completions",
|
api: "openai-completions",
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
id: "ZhipuAI/GLM-4.7",
|
id: "ZhipuAI/GLM-5",
|
||||||
name: "GLM-4.7",
|
name: "GLM-5",
|
||||||
contextWindow: 128000,
|
contextWindow: 128000,
|
||||||
cost: { input: 0.001, output: 0.001 },
|
cost: { input: 0.001, output: 0.001 },
|
||||||
},
|
},
|
||||||
@@ -718,8 +714,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
suggestedDefaults: {
|
suggestedDefaults: {
|
||||||
model: { primary: "modelscope/ZhipuAI/GLM-4.7" },
|
model: { primary: "modelscope/ZhipuAI/GLM-5" },
|
||||||
modelCatalog: { "modelscope/ZhipuAI/GLM-4.7": { alias: "GLM" } },
|
modelCatalog: { "modelscope/ZhipuAI/GLM-5": { alias: "GLM" } },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -732,8 +728,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
|||||||
api: "openai-completions",
|
api: "openai-completions",
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
id: "Pro/MiniMaxAI/MiniMax-M2.1",
|
id: "Pro/MiniMaxAI/MiniMax-M2.5",
|
||||||
name: "MiniMax M2.1",
|
name: "MiniMax M2.5",
|
||||||
contextWindow: 200000,
|
contextWindow: 200000,
|
||||||
cost: { input: 0.001, output: 0.004 },
|
cost: { input: 0.001, output: 0.004 },
|
||||||
},
|
},
|
||||||
@@ -750,9 +746,9 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
suggestedDefaults: {
|
suggestedDefaults: {
|
||||||
model: { primary: "siliconflow/Pro/MiniMaxAI/MiniMax-M2.1" },
|
model: { primary: "siliconflow/Pro/MiniMaxAI/MiniMax-M2.5" },
|
||||||
modelCatalog: {
|
modelCatalog: {
|
||||||
"siliconflow/Pro/MiniMaxAI/MiniMax-M2.1": { alias: "MiniMax" },
|
"siliconflow/Pro/MiniMaxAI/MiniMax-M2.5": { alias: "MiniMax" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -766,8 +762,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
|||||||
api: "openai-completions",
|
api: "openai-completions",
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
id: "MiniMaxAI/MiniMax-M2.1",
|
id: "MiniMaxAI/MiniMax-M2.5",
|
||||||
name: "MiniMax M2.1",
|
name: "MiniMax M2.5",
|
||||||
contextWindow: 200000,
|
contextWindow: 200000,
|
||||||
cost: { input: 0.001, output: 0.004 },
|
cost: { input: 0.001, output: 0.004 },
|
||||||
},
|
},
|
||||||
@@ -784,9 +780,9 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
suggestedDefaults: {
|
suggestedDefaults: {
|
||||||
model: { primary: "siliconflow-en/MiniMaxAI/MiniMax-M2.1" },
|
model: { primary: "siliconflow-en/MiniMaxAI/MiniMax-M2.5" },
|
||||||
modelCatalog: {
|
modelCatalog: {
|
||||||
"siliconflow-en/MiniMaxAI/MiniMax-M2.1": { alias: "MiniMax" },
|
"siliconflow-en/MiniMaxAI/MiniMax-M2.5": { alias: "MiniMax" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -834,8 +830,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
|||||||
api: "anthropic-messages",
|
api: "anthropic-messages",
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
id: "claude-sonnet-4-5-20250929",
|
id: "claude-sonnet-4-6",
|
||||||
name: "Claude Sonnet 4.5",
|
name: "Claude Sonnet 4.6",
|
||||||
contextWindow: 200000,
|
contextWindow: 200000,
|
||||||
cost: { input: 3, output: 15 },
|
cost: { input: 3, output: 15 },
|
||||||
},
|
},
|
||||||
@@ -860,11 +856,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
|||||||
},
|
},
|
||||||
suggestedDefaults: {
|
suggestedDefaults: {
|
||||||
model: {
|
model: {
|
||||||
primary: "packycode/claude-sonnet-4-5-20250929",
|
primary: "packycode/claude-sonnet-4-6",
|
||||||
fallbacks: ["packycode/claude-opus-4-6"],
|
fallbacks: ["packycode/claude-opus-4-6"],
|
||||||
},
|
},
|
||||||
modelCatalog: {
|
modelCatalog: {
|
||||||
"packycode/claude-sonnet-4-5-20250929": { alias: "Sonnet" },
|
"packycode/claude-sonnet-4-6": { alias: "Sonnet" },
|
||||||
"packycode/claude-opus-4-6": { alias: "Opus" },
|
"packycode/claude-opus-4-6": { alias: "Opus" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -879,8 +875,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
|||||||
api: "anthropic-messages",
|
api: "anthropic-messages",
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
id: "claude-sonnet-4-5-20250929",
|
id: "claude-sonnet-4-6",
|
||||||
name: "Claude Sonnet 4.5",
|
name: "Claude Sonnet 4.6",
|
||||||
contextWindow: 200000,
|
contextWindow: 200000,
|
||||||
cost: { input: 3, output: 15 },
|
cost: { input: 3, output: 15 },
|
||||||
},
|
},
|
||||||
@@ -906,11 +902,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
|||||||
},
|
},
|
||||||
suggestedDefaults: {
|
suggestedDefaults: {
|
||||||
model: {
|
model: {
|
||||||
primary: "cubence/claude-sonnet-4-5-20250929",
|
primary: "cubence/claude-sonnet-4-6",
|
||||||
fallbacks: ["cubence/claude-opus-4-6"],
|
fallbacks: ["cubence/claude-opus-4-6"],
|
||||||
},
|
},
|
||||||
modelCatalog: {
|
modelCatalog: {
|
||||||
"cubence/claude-sonnet-4-5-20250929": { alias: "Sonnet" },
|
"cubence/claude-sonnet-4-6": { alias: "Sonnet" },
|
||||||
"cubence/claude-opus-4-6": { alias: "Opus" },
|
"cubence/claude-opus-4-6": { alias: "Opus" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -925,8 +921,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
|||||||
api: "anthropic-messages",
|
api: "anthropic-messages",
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
id: "claude-sonnet-4-5-20250929",
|
id: "claude-sonnet-4-6",
|
||||||
name: "Claude Sonnet 4.5",
|
name: "Claude Sonnet 4.6",
|
||||||
contextWindow: 200000,
|
contextWindow: 200000,
|
||||||
cost: { input: 3, output: 15 },
|
cost: { input: 3, output: 15 },
|
||||||
},
|
},
|
||||||
@@ -952,11 +948,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
|||||||
},
|
},
|
||||||
suggestedDefaults: {
|
suggestedDefaults: {
|
||||||
model: {
|
model: {
|
||||||
primary: "aigocode/claude-sonnet-4-5-20250929",
|
primary: "aigocode/claude-sonnet-4-6",
|
||||||
fallbacks: ["aigocode/claude-opus-4-6"],
|
fallbacks: ["aigocode/claude-opus-4-6"],
|
||||||
},
|
},
|
||||||
modelCatalog: {
|
modelCatalog: {
|
||||||
"aigocode/claude-sonnet-4-5-20250929": { alias: "Sonnet" },
|
"aigocode/claude-sonnet-4-6": { alias: "Sonnet" },
|
||||||
"aigocode/claude-opus-4-6": { alias: "Opus" },
|
"aigocode/claude-opus-4-6": { alias: "Opus" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -971,8 +967,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
|||||||
api: "anthropic-messages",
|
api: "anthropic-messages",
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
id: "claude-sonnet-4-5-20250929",
|
id: "claude-sonnet-4-6",
|
||||||
name: "Claude Sonnet 4.5",
|
name: "Claude Sonnet 4.6",
|
||||||
contextWindow: 200000,
|
contextWindow: 200000,
|
||||||
cost: { input: 3, output: 15 },
|
cost: { input: 3, output: 15 },
|
||||||
},
|
},
|
||||||
@@ -998,11 +994,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
|||||||
},
|
},
|
||||||
suggestedDefaults: {
|
suggestedDefaults: {
|
||||||
model: {
|
model: {
|
||||||
primary: "rightcode/claude-sonnet-4-5-20250929",
|
primary: "rightcode/claude-sonnet-4-6",
|
||||||
fallbacks: ["rightcode/claude-opus-4-6"],
|
fallbacks: ["rightcode/claude-opus-4-6"],
|
||||||
},
|
},
|
||||||
modelCatalog: {
|
modelCatalog: {
|
||||||
"rightcode/claude-sonnet-4-5-20250929": { alias: "Sonnet" },
|
"rightcode/claude-sonnet-4-6": { alias: "Sonnet" },
|
||||||
"rightcode/claude-opus-4-6": { alias: "Opus" },
|
"rightcode/claude-opus-4-6": { alias: "Opus" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1017,8 +1013,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
|||||||
api: "anthropic-messages",
|
api: "anthropic-messages",
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
id: "claude-sonnet-4-5-20250929",
|
id: "claude-sonnet-4-6",
|
||||||
name: "Claude Sonnet 4.5",
|
name: "Claude Sonnet 4.6",
|
||||||
contextWindow: 200000,
|
contextWindow: 200000,
|
||||||
cost: { input: 3, output: 15 },
|
cost: { input: 3, output: 15 },
|
||||||
},
|
},
|
||||||
@@ -1044,15 +1040,187 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
|||||||
},
|
},
|
||||||
suggestedDefaults: {
|
suggestedDefaults: {
|
||||||
model: {
|
model: {
|
||||||
primary: "aicodemirror/claude-sonnet-4-5-20250929",
|
primary: "aicodemirror/claude-sonnet-4-6",
|
||||||
fallbacks: ["aicodemirror/claude-opus-4-6"],
|
fallbacks: ["aicodemirror/claude-opus-4-6"],
|
||||||
},
|
},
|
||||||
modelCatalog: {
|
modelCatalog: {
|
||||||
"aicodemirror/claude-sonnet-4-5-20250929": { alias: "Sonnet" },
|
"aicodemirror/claude-sonnet-4-6": { alias: "Sonnet" },
|
||||||
"aicodemirror/claude-opus-4-6": { alias: "Opus" },
|
"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",
|
||||||
|
websiteUrl: "https://aws.amazon.com/bedrock/",
|
||||||
|
settingsConfig: {
|
||||||
|
// 请将 us-west-2 替换为你的 AWS Region
|
||||||
|
baseUrl: "https://bedrock-runtime.us-west-2.amazonaws.com",
|
||||||
|
apiKey: "",
|
||||||
|
api: "bedrock-converse-stream",
|
||||||
|
models: [
|
||||||
|
{
|
||||||
|
id: "anthropic.claude-opus-4-6-20250514-v1:0",
|
||||||
|
name: "Claude Opus 4.6",
|
||||||
|
contextWindow: 200000,
|
||||||
|
cost: { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "anthropic.claude-sonnet-4-6",
|
||||||
|
name: "Claude Sonnet 4.6",
|
||||||
|
contextWindow: 200000,
|
||||||
|
cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "anthropic.claude-haiku-4-5-20251022-v1:0",
|
||||||
|
name: "Claude Haiku 4.5",
|
||||||
|
contextWindow: 200000,
|
||||||
|
cost: { input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
category: "cloud_provider",
|
||||||
|
icon: "aws",
|
||||||
|
iconColor: "#FF9900",
|
||||||
|
},
|
||||||
|
|
||||||
// ========== Custom Template ==========
|
// ========== Custom Template ==========
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export const opencodeNpmPackages = [
|
|||||||
{ value: "@ai-sdk/openai", label: "OpenAI" },
|
{ value: "@ai-sdk/openai", label: "OpenAI" },
|
||||||
{ value: "@ai-sdk/openai-compatible", label: "OpenAI Compatible" },
|
{ value: "@ai-sdk/openai-compatible", label: "OpenAI Compatible" },
|
||||||
{ value: "@ai-sdk/anthropic", label: "Anthropic" },
|
{ value: "@ai-sdk/anthropic", label: "Anthropic" },
|
||||||
|
{ value: "@ai-sdk/amazon-bedrock", label: "Amazon Bedrock" },
|
||||||
{ value: "@ai-sdk/google", label: "Google (Gemini)" },
|
{ value: "@ai-sdk/google", label: "Google (Gemini)" },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
@@ -40,15 +41,15 @@ export const OPENCODE_PRESET_MODEL_VARIANTS: Record<
|
|||||||
> = {
|
> = {
|
||||||
"@ai-sdk/openai-compatible": [
|
"@ai-sdk/openai-compatible": [
|
||||||
{
|
{
|
||||||
id: "MiniMax-M2.1",
|
id: "MiniMax-M2.5",
|
||||||
name: "MiniMax M2.1",
|
name: "MiniMax M2.5",
|
||||||
contextLimit: 204800,
|
contextLimit: 204800,
|
||||||
outputLimit: 131072,
|
outputLimit: 131072,
|
||||||
modalities: { input: ["text"], output: ["text"] },
|
modalities: { input: ["text"], output: ["text"] },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "glm-4.7",
|
id: "glm-5",
|
||||||
name: "GLM 4.7",
|
name: "GLM 5",
|
||||||
contextLimit: 204800,
|
contextLimit: 204800,
|
||||||
outputLimit: 131072,
|
outputLimit: 131072,
|
||||||
modalities: { input: ["text"], output: ["text"] },
|
modalities: { input: ["text"], output: ["text"] },
|
||||||
@@ -315,6 +316,50 @@ export const OPENCODE_PRESET_MODEL_VARIANTS: Record<
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"@ai-sdk/amazon-bedrock": [
|
||||||
|
{
|
||||||
|
id: "global.anthropic.claude-opus-4-6-v1",
|
||||||
|
name: "Claude Opus 4.6",
|
||||||
|
contextLimit: 1000000,
|
||||||
|
outputLimit: 128000,
|
||||||
|
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "global.anthropic.claude-sonnet-4-6",
|
||||||
|
name: "Claude Sonnet 4.6",
|
||||||
|
contextLimit: 200000,
|
||||||
|
outputLimit: 64000,
|
||||||
|
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "global.anthropic.claude-haiku-4-5-20251001-v1:0",
|
||||||
|
name: "Claude Haiku 4.5",
|
||||||
|
contextLimit: 200000,
|
||||||
|
outputLimit: 64000,
|
||||||
|
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "us.amazon.nova-pro-v1:0",
|
||||||
|
name: "Amazon Nova Pro",
|
||||||
|
contextLimit: 300000,
|
||||||
|
outputLimit: 5000,
|
||||||
|
modalities: { input: ["text", "image"], output: ["text"] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "us.meta.llama4-maverick-17b-instruct-v1:0",
|
||||||
|
name: "Meta Llama 4 Maverick",
|
||||||
|
contextLimit: 131072,
|
||||||
|
outputLimit: 131072,
|
||||||
|
modalities: { input: ["text"], output: ["text"] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "us.deepseek.r1-v1:0",
|
||||||
|
name: "DeepSeek R1",
|
||||||
|
contextLimit: 131072,
|
||||||
|
outputLimit: 131072,
|
||||||
|
modalities: { input: ["text"], output: ["text"] },
|
||||||
|
},
|
||||||
|
],
|
||||||
"@ai-sdk/anthropic": [
|
"@ai-sdk/anthropic": [
|
||||||
{
|
{
|
||||||
id: "claude-sonnet-4-5-20250929",
|
id: "claude-sonnet-4-5-20250929",
|
||||||
@@ -441,7 +486,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
|||||||
apiKey: "",
|
apiKey: "",
|
||||||
},
|
},
|
||||||
models: {
|
models: {
|
||||||
"glm-4.7": { name: "GLM-4.7" },
|
"glm-5": { name: "GLM-5" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
category: "cn_official",
|
category: "cn_official",
|
||||||
@@ -473,7 +518,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
|||||||
apiKey: "",
|
apiKey: "",
|
||||||
},
|
},
|
||||||
models: {
|
models: {
|
||||||
"glm-4.7": { name: "GLM-4.7" },
|
"glm-5": { name: "GLM-5" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
category: "cn_official",
|
category: "cn_official",
|
||||||
@@ -599,7 +644,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
|||||||
apiKey: "",
|
apiKey: "",
|
||||||
},
|
},
|
||||||
models: {
|
models: {
|
||||||
"ZhipuAI/GLM-4.7": { name: "GLM-4.7" },
|
"ZhipuAI/GLM-5": { name: "GLM-5" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
category: "aggregator",
|
category: "aggregator",
|
||||||
@@ -703,7 +748,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
|||||||
apiKey: "",
|
apiKey: "",
|
||||||
},
|
},
|
||||||
models: {
|
models: {
|
||||||
"MiniMax-M2.1": { name: "MiniMax M2.1" },
|
"MiniMax-M2.5": { name: "MiniMax M2.5" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
category: "cn_official",
|
category: "cn_official",
|
||||||
@@ -735,7 +780,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
|||||||
apiKey: "",
|
apiKey: "",
|
||||||
},
|
},
|
||||||
models: {
|
models: {
|
||||||
"MiniMax-M2.1": { name: "MiniMax M2.1" },
|
"MiniMax-M2.5": { name: "MiniMax M2.5" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
category: "cn_official",
|
category: "cn_official",
|
||||||
@@ -767,7 +812,9 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
|||||||
apiKey: "",
|
apiKey: "",
|
||||||
},
|
},
|
||||||
models: {
|
models: {
|
||||||
"doubao-seed-code-preview-latest": { name: "Doubao Seed Code Preview" },
|
"doubao-seed-2-0-code-preview-latest": {
|
||||||
|
name: "Doubao Seed Code Preview",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
category: "cn_official",
|
category: "cn_official",
|
||||||
@@ -792,7 +839,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
|||||||
apiKey: "",
|
apiKey: "",
|
||||||
},
|
},
|
||||||
models: {
|
models: {
|
||||||
"Ling-1T": { name: "Ling 1T" },
|
"Ling-2.5-1T": { name: "Ling 2.5-1T" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
category: "cn_official",
|
category: "cn_official",
|
||||||
@@ -843,7 +890,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
|||||||
apiKey: "",
|
apiKey: "",
|
||||||
},
|
},
|
||||||
models: {
|
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" },
|
"claude-opus-4-6": { name: "Claude Opus 4.6" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -870,7 +917,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
|||||||
apiKey: "",
|
apiKey: "",
|
||||||
},
|
},
|
||||||
models: {
|
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" },
|
"claude-opus-4-6": { name: "Claude Opus 4.6" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -897,7 +944,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
|||||||
apiKey: "",
|
apiKey: "",
|
||||||
},
|
},
|
||||||
models: {
|
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" },
|
"anthropic/claude-opus-4.6": { name: "Claude Opus 4.6" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -951,7 +998,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
|||||||
apiKey: "",
|
apiKey: "",
|
||||||
},
|
},
|
||||||
models: {
|
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" },
|
"claude-opus-4-6": { name: "Claude Opus 4.6" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -979,7 +1026,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
|||||||
apiKey: "",
|
apiKey: "",
|
||||||
},
|
},
|
||||||
models: {
|
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" },
|
"claude-opus-4-6": { name: "Claude Opus 4.6" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1008,7 +1055,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
|||||||
apiKey: "",
|
apiKey: "",
|
||||||
},
|
},
|
||||||
models: {
|
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" },
|
"claude-opus-4-6": { name: "Claude Opus 4.6" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1069,7 +1116,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
|||||||
apiKey: "",
|
apiKey: "",
|
||||||
},
|
},
|
||||||
models: {
|
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" },
|
"claude-opus-4.6": { name: "Claude Opus 4.6" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1086,7 +1133,141 @@ 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/",
|
||||||
|
settingsConfig: {
|
||||||
|
npm: "@ai-sdk/amazon-bedrock",
|
||||||
|
name: "AWS Bedrock",
|
||||||
|
options: {
|
||||||
|
region: "${region}",
|
||||||
|
accessKeyId: "${accessKeyId}",
|
||||||
|
secretAccessKey: "${secretAccessKey}",
|
||||||
|
},
|
||||||
|
models: {
|
||||||
|
"global.anthropic.claude-opus-4-6-v1": { name: "Claude Opus 4.6" },
|
||||||
|
"global.anthropic.claude-sonnet-4-6": {
|
||||||
|
name: "Claude Sonnet 4.6",
|
||||||
|
},
|
||||||
|
"global.anthropic.claude-haiku-4-5-20251001-v1:0": {
|
||||||
|
name: "Claude Haiku 4.5",
|
||||||
|
},
|
||||||
|
"us.amazon.nova-pro-v1:0": { name: "Amazon Nova Pro" },
|
||||||
|
"us.meta.llama4-maverick-17b-instruct-v1:0": {
|
||||||
|
name: "Meta Llama 4 Maverick",
|
||||||
|
},
|
||||||
|
"us.deepseek.r1-v1:0": { name: "DeepSeek R1" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
category: "cloud_provider",
|
||||||
|
icon: "aws",
|
||||||
|
iconColor: "#FF9900",
|
||||||
|
templateValues: {
|
||||||
|
region: {
|
||||||
|
label: "AWS Region",
|
||||||
|
placeholder: "us-west-2",
|
||||||
|
defaultValue: "us-west-2",
|
||||||
|
editorValue: "us-west-2",
|
||||||
|
},
|
||||||
|
accessKeyId: {
|
||||||
|
label: "Access Key ID",
|
||||||
|
placeholder: "AKIA...",
|
||||||
|
editorValue: "",
|
||||||
|
},
|
||||||
|
secretAccessKey: {
|
||||||
|
label: "Secret Access Key",
|
||||||
|
placeholder: "your-secret-key",
|
||||||
|
editorValue: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "OpenAI Compatible",
|
name: "OpenAI Compatible",
|
||||||
websiteUrl: "",
|
websiteUrl: "",
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ export function useBackupManager() {
|
|||||||
queryFn: () => backupsApi.listDbBackups(),
|
queryFn: () => backupsApi.listDbBackups(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const createMutation = useMutation({
|
||||||
|
mutationFn: () => backupsApi.createDbBackup(),
|
||||||
|
onSuccess: () => refetch(),
|
||||||
|
});
|
||||||
|
|
||||||
const restoreMutation = useMutation({
|
const restoreMutation = useMutation({
|
||||||
mutationFn: (filename: string) => backupsApi.restoreDbBackup(filename),
|
mutationFn: (filename: string) => backupsApi.restoreDbBackup(filename),
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
@@ -34,12 +39,21 @@ export function useBackupManager() {
|
|||||||
onSuccess: () => refetch(),
|
onSuccess: () => refetch(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const deleteMutation = useMutation({
|
||||||
|
mutationFn: (filename: string) => backupsApi.deleteDbBackup(filename),
|
||||||
|
onSuccess: () => refetch(),
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
backups,
|
backups,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
create: createMutation.mutateAsync,
|
||||||
|
isCreating: createMutation.isPending,
|
||||||
restore: restoreMutation.mutateAsync,
|
restore: restoreMutation.mutateAsync,
|
||||||
isRestoring: restoreMutation.isPending,
|
isRestoring: restoreMutation.isPending,
|
||||||
rename: renameMutation.mutateAsync,
|
rename: renameMutation.mutateAsync,
|
||||||
isRenaming: renameMutation.isPending,
|
isRenaming: renameMutation.isPending,
|
||||||
|
remove: deleteMutation.mutateAsync,
|
||||||
|
isDeleting: deleteMutation.isPending,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+65
-71
@@ -51,15 +51,8 @@
|
|||||||
},
|
},
|
||||||
"claudeConfig": {
|
"claudeConfig": {
|
||||||
"configLabel": "Claude Code settings.json (JSON) *",
|
"configLabel": "Claude Code settings.json (JSON) *",
|
||||||
"writeCommonConfig": "Write Common Config",
|
|
||||||
"editCommonConfig": "Edit Common Config",
|
|
||||||
"editCommonConfigTitle": "Edit Common Config Snippet",
|
|
||||||
"commonConfigHint": "This snippet will be merged into settings.json when 'Write Common Config' is checked",
|
|
||||||
"fullSettingsHint": "Full Claude Code settings.json content",
|
"fullSettingsHint": "Full Claude Code settings.json content",
|
||||||
"extractFromCurrent": "Extract from Editor",
|
"fragmentSettingsHint": "Config snippet for this provider; will be written to settings.json when activated",
|
||||||
"extractNoCommonConfig": "No common config available to extract from editor",
|
|
||||||
"extractFailed": "Extract failed: {{error}}",
|
|
||||||
"saveFailed": "Save failed: {{error}}",
|
|
||||||
"hideAttribution": "Hide AI Attribution",
|
"hideAttribution": "Hide AI Attribution",
|
||||||
"alwaysThinking": "Extended Thinking",
|
"alwaysThinking": "Extended Thinking",
|
||||||
"enableTeammates": "Teammates Mode"
|
"enableTeammates": "Teammates Mode"
|
||||||
@@ -125,11 +118,7 @@
|
|||||||
"notes": "Notes",
|
"notes": "Notes",
|
||||||
"notesPlaceholder": "e.g., Company dedicated account",
|
"notesPlaceholder": "e.g., Company dedicated account",
|
||||||
"configJson": "Config JSON",
|
"configJson": "Config JSON",
|
||||||
"writeCommonConfig": "Write common config",
|
|
||||||
"editCommonConfigButton": "Edit common config",
|
|
||||||
"configJsonHint": "Please fill in complete Claude Code configuration",
|
"configJsonHint": "Please fill in complete Claude Code configuration",
|
||||||
"editCommonConfigTitle": "Edit common config snippet",
|
|
||||||
"editCommonConfigHint": "Common config snippet will be merged into all providers that enable it",
|
|
||||||
"addProvider": "Add Provider",
|
"addProvider": "Add Provider",
|
||||||
"sortUpdated": "Sort order updated",
|
"sortUpdated": "Sort order updated",
|
||||||
"usageSaved": "Usage query configuration saved",
|
"usageSaved": "Usage query configuration saved",
|
||||||
@@ -319,7 +308,17 @@
|
|||||||
"rename": "Rename",
|
"rename": "Rename",
|
||||||
"renameSuccess": "Backup renamed",
|
"renameSuccess": "Backup renamed",
|
||||||
"renameFailed": "Rename failed",
|
"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": {
|
"webdavSync": {
|
||||||
"title": "WebDAV Cloud Sync",
|
"title": "WebDAV Cloud Sync",
|
||||||
@@ -637,7 +636,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",
|
"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!",
|
"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!",
|
"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}} *",
|
"parameterConfig": "Parameter Config - {{name}} *",
|
||||||
"mainModel": "Main Model (optional)",
|
"mainModel": "Main Model (optional)",
|
||||||
@@ -667,6 +669,10 @@
|
|||||||
"apiFormatHint": "Select the input format for the provider's API",
|
"apiFormatHint": "Select the input format for the provider's API",
|
||||||
"apiFormatAnthropic": "Anthropic Messages (Native)",
|
"apiFormatAnthropic": "Anthropic Messages (Native)",
|
||||||
"apiFormatOpenAIChat": "OpenAI Chat Completions (Requires proxy)",
|
"apiFormatOpenAIChat": "OpenAI Chat Completions (Requires proxy)",
|
||||||
|
"authField": "Auth Field",
|
||||||
|
"authFieldAuthToken": "Auth Token (Default)",
|
||||||
|
"authFieldApiKey": "API Key",
|
||||||
|
"authFieldHint": "Most third-party providers use Auth Token; a few require API Key",
|
||||||
"anthropicDefaultHaikuModel": "Default Haiku Model",
|
"anthropicDefaultHaikuModel": "Default Haiku Model",
|
||||||
"anthropicDefaultSonnetModel": "Default Sonnet Model",
|
"anthropicDefaultSonnetModel": "Default Sonnet Model",
|
||||||
"anthropicDefaultOpusModel": "Default Opus Model",
|
"anthropicDefaultOpusModel": "Default Opus Model",
|
||||||
@@ -738,33 +744,15 @@
|
|||||||
"authJsonHint": "Codex auth.json configuration content",
|
"authJsonHint": "Codex auth.json configuration content",
|
||||||
"configToml": "config.toml (TOML)",
|
"configToml": "config.toml (TOML)",
|
||||||
"configTomlHint": "Codex config.toml configuration content",
|
"configTomlHint": "Codex config.toml configuration content",
|
||||||
"writeCommonConfig": "Write Common Config",
|
"apiUrlLabel": "API Request URL"
|
||||||
"editCommonConfig": "Edit Common Config",
|
|
||||||
"editCommonConfigTitle": "Edit Codex Common Config Snippet",
|
|
||||||
"commonConfigHint": "This snippet will be appended to the end of config.toml when 'Write Common Config' is checked",
|
|
||||||
"apiUrlLabel": "API Request URL",
|
|
||||||
"extractFromCurrent": "Extract from Editor",
|
|
||||||
"extractNoCommonConfig": "No common config available to extract from editor",
|
|
||||||
"extractFailed": "Extract failed: {{error}}",
|
|
||||||
"saveFailed": "Save failed: {{error}}"
|
|
||||||
},
|
},
|
||||||
"geminiConfig": {
|
"geminiConfig": {
|
||||||
"envFile": "Environment Variables (.env)",
|
"envFile": "Environment Variables (.env)",
|
||||||
"envFileHint": "Configure Gemini environment variables in .env format",
|
"envFileHint": "Configure Gemini environment variables in .env format",
|
||||||
"configJson": "Configuration File (config.json)",
|
"configJson": "Configuration File (config.json)",
|
||||||
"configJsonHint": "Configure Gemini extended parameters in JSON format (optional)",
|
"configJsonHint": "Configure Gemini extended parameters in JSON format (optional)",
|
||||||
"writeCommonConfig": "Write Common Config",
|
|
||||||
"editCommonConfig": "Edit Common Config",
|
|
||||||
"editCommonConfigTitle": "Edit Gemini Common Config Snippet",
|
|
||||||
"commonConfigHint": "This snippet writes to Gemini .env (GOOGLE_GEMINI_BASE_URL and GEMINI_API_KEY are not allowed)",
|
|
||||||
"extractFromCurrent": "Extract from Editor",
|
|
||||||
"extractNoCommonConfig": "No common config available to extract from editor",
|
|
||||||
"extractFailed": "Extract failed: {{error}}",
|
|
||||||
"saveFailed": "Save failed: {{error}}",
|
|
||||||
"extractedConfigInvalid": "Extracted config format is invalid",
|
"extractedConfigInvalid": "Extracted config format is invalid",
|
||||||
"invalidJsonFormat": "Common config snippet format error (must be valid JSON)",
|
"invalidJsonFormat": "Common config snippet format error (must be valid JSON)",
|
||||||
"commonConfigInvalidKeys": "Common config snippet must not include GOOGLE_GEMINI_BASE_URL or GEMINI_API_KEY (found: {{keys}})",
|
|
||||||
"commonConfigInvalidValues": "Common config snippet values must be strings",
|
|
||||||
"noCommonConfigToApply": "Common config snippet is empty or has no applicable entries",
|
"noCommonConfigToApply": "Common config snippet is empty or has no applicable entries",
|
||||||
"configMergeFailed": "Config merge failed: {{error}}",
|
"configMergeFailed": "Config merge failed: {{error}}",
|
||||||
"configReplaceFailed": "Config replace failed: {{error}}"
|
"configReplaceFailed": "Config replace failed: {{error}}"
|
||||||
@@ -800,38 +788,6 @@
|
|||||||
"modelOptionKeyPlaceholder": "provider",
|
"modelOptionKeyPlaceholder": "provider",
|
||||||
"modelOptionValuePlaceholder": "{\"order\": [\"baseten\"]}"
|
"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": {
|
"providerPreset": {
|
||||||
"label": "Provider Preset",
|
"label": "Provider Preset",
|
||||||
"custom": "Custom Configuration",
|
"custom": "Custom Configuration",
|
||||||
@@ -1219,22 +1175,60 @@
|
|||||||
"saveSuccess": "Saved successfully",
|
"saveSuccess": "Saved successfully",
|
||||||
"saveFailed": "Failed to save",
|
"saveFailed": "Failed to save",
|
||||||
"loadFailed": "Failed to load",
|
"loadFailed": "Failed to load",
|
||||||
|
"openDirectory": "Open in file manager",
|
||||||
"dailyMemory": {
|
"dailyMemory": {
|
||||||
"title": "Daily Memory",
|
"title": "Daily Memory",
|
||||||
"sectionTitle": "Daily Memory",
|
"sectionTitle": "Daily Memory",
|
||||||
"cardTitle": "Daily Memory Files",
|
"cardTitle": "Daily Memory Files",
|
||||||
"cardDescription": "Browse & manage daily memories",
|
"cardDescription": "Browse & manage daily memories",
|
||||||
"createToday": "Today's Note",
|
"createToday": "Add Memory",
|
||||||
"empty": "No daily memory files yet",
|
"empty": "No daily memory files yet",
|
||||||
"loadFailed": "Failed to load daily memory files",
|
"loadFailed": "Failed to load daily memory files",
|
||||||
"createFailed": "Failed to create daily memory file",
|
"createFailed": "Failed to create daily memory file",
|
||||||
"deleteSuccess": "Daily memory file deleted",
|
"deleteSuccess": "Daily memory file deleted",
|
||||||
"deleteFailed": "Failed to delete daily memory file",
|
"deleteFailed": "Failed to delete daily memory file",
|
||||||
"confirmDeleteTitle": "Delete Daily Memory",
|
"confirmDeleteTitle": "Delete Daily Memory",
|
||||||
"confirmDeleteMessage": "Delete the daily memory for {{date}}? This cannot be undone."
|
"confirmDeleteMessage": "Delete the daily memory for {{date}}? This cannot be undone.",
|
||||||
|
"searchPlaceholder": "Search full content...",
|
||||||
|
"searchScopeHint": "Full-text search across all daily memories. ⌘F",
|
||||||
|
"searchCloseHint": "Esc to close",
|
||||||
|
"noSearchResults": "No daily memories match your search.",
|
||||||
|
"searching": "Searching...",
|
||||||
|
"searchFailed": "Search failed",
|
||||||
|
"matchCount": "{{count}} match(es)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"openclaw": {
|
"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": {
|
"env": {
|
||||||
"title": "Environment Variables",
|
"title": "Environment Variables",
|
||||||
"description": "Manage environment variables in openclaw.json (API keys, custom variables, etc.)",
|
"description": "Manage environment variables in openclaw.json (API keys, custom variables, etc.)",
|
||||||
@@ -1821,10 +1815,6 @@
|
|||||||
"enabled": "Enabled",
|
"enabled": "Enabled",
|
||||||
"disabled": "OMO disabled",
|
"disabled": "OMO disabled",
|
||||||
"disableFailed": "Failed to disable OMO: {{error}}",
|
"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...",
|
"selectPlaceholder": "Select...",
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
"clearWrapped": "(Clear)",
|
"clearWrapped": "(Clear)",
|
||||||
@@ -1854,6 +1844,10 @@
|
|||||||
"customCategories": "Custom Categories",
|
"customCategories": "Custom Categories",
|
||||||
"modelConfiguration": "Model Configuration",
|
"modelConfiguration": "Model Configuration",
|
||||||
"fillRecommended": "Fill Recommended",
|
"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",
|
"configSummary": "{{agents}} agents, {{categories}} categories configured · Click ⚙ for advanced params",
|
||||||
"enabledModelsCount": "{{count}} configured models available",
|
"enabledModelsCount": "{{count}} configured models available",
|
||||||
"source": "from:",
|
"source": "from:",
|
||||||
|
|||||||
+86
-73
@@ -51,15 +51,8 @@
|
|||||||
},
|
},
|
||||||
"claudeConfig": {
|
"claudeConfig": {
|
||||||
"configLabel": "Claude Code settings.json (JSON) *",
|
"configLabel": "Claude Code settings.json (JSON) *",
|
||||||
"writeCommonConfig": "共通設定を書き込む",
|
|
||||||
"editCommonConfig": "共通設定を編集",
|
|
||||||
"editCommonConfigTitle": "共通設定スニペットを編集",
|
|
||||||
"commonConfigHint": "「共通設定を書き込む」がオンのとき settings.json にマージされます",
|
|
||||||
"fullSettingsHint": "Claude Code の settings.json 全文",
|
"fullSettingsHint": "Claude Code の settings.json 全文",
|
||||||
"extractFromCurrent": "編集内容から抽出",
|
"fragmentSettingsHint": "このプロバイダーの設定スニペット。有効化時に settings.json に書き込まれます",
|
||||||
"extractNoCommonConfig": "編集内容から抽出できる共通設定がありません",
|
|
||||||
"extractFailed": "抽出に失敗しました: {{error}}",
|
|
||||||
"saveFailed": "保存に失敗しました: {{error}}",
|
|
||||||
"hideAttribution": "AI署名を非表示",
|
"hideAttribution": "AI署名を非表示",
|
||||||
"alwaysThinking": "拡張思考",
|
"alwaysThinking": "拡張思考",
|
||||||
"enableTeammates": "Teammates モード"
|
"enableTeammates": "Teammates モード"
|
||||||
@@ -125,11 +118,7 @@
|
|||||||
"notes": "メモ",
|
"notes": "メモ",
|
||||||
"notesPlaceholder": "例: 会社用アカウント",
|
"notesPlaceholder": "例: 会社用アカウント",
|
||||||
"configJson": "Config JSON",
|
"configJson": "Config JSON",
|
||||||
"writeCommonConfig": "共通設定を書き込む",
|
|
||||||
"editCommonConfigButton": "共通設定を編集",
|
|
||||||
"configJsonHint": "Claude Code の設定をすべて入力してください",
|
"configJsonHint": "Claude Code の設定をすべて入力してください",
|
||||||
"editCommonConfigTitle": "共通設定スニペットを編集",
|
|
||||||
"editCommonConfigHint": "共通設定スニペットは、この機能をオンにしたすべてのプロバイダーへマージされます",
|
|
||||||
"addProvider": "プロバイダーを追加",
|
"addProvider": "プロバイダーを追加",
|
||||||
"sortUpdated": "並び順を更新しました",
|
"sortUpdated": "並び順を更新しました",
|
||||||
"usageSaved": "利用状況の設定を保存しました",
|
"usageSaved": "利用状況の設定を保存しました",
|
||||||
@@ -319,7 +308,17 @@
|
|||||||
"rename": "名前変更",
|
"rename": "名前変更",
|
||||||
"renameSuccess": "バックアップの名前を変更しました",
|
"renameSuccess": "バックアップの名前を変更しました",
|
||||||
"renameFailed": "名前変更に失敗しました",
|
"renameFailed": "名前変更に失敗しました",
|
||||||
"namePlaceholder": "新しい名前を入力"
|
"namePlaceholder": "新しい名前を入力",
|
||||||
|
"createBackup": "今すぐバックアップ",
|
||||||
|
"creating": "バックアップ中...",
|
||||||
|
"createSuccess": "バックアップが作成されました",
|
||||||
|
"createFailed": "バックアップに失敗しました",
|
||||||
|
"delete": "削除",
|
||||||
|
"deleting": "削除中...",
|
||||||
|
"deleteSuccess": "バックアップを削除しました",
|
||||||
|
"deleteFailed": "削除に失敗しました",
|
||||||
|
"deleteConfirmTitle": "バックアップの削除を確認",
|
||||||
|
"deleteConfirmMessage": "このバックアップは完全に削除されます。この操作は取り消せません。"
|
||||||
},
|
},
|
||||||
"webdavSync": {
|
"webdavSync": {
|
||||||
"title": "WebDAV クラウド同期",
|
"title": "WebDAV クラウド同期",
|
||||||
@@ -637,7 +636,10 @@
|
|||||||
"cubence": "Cubence は CC Switch の公式パートナーです。登録後チャージ時に \"CCSWITCH\" を入力すると、毎回 10% オフ",
|
"cubence": "Cubence は CC Switch の公式パートナーです。登録後チャージ時に \"CCSWITCH\" を入力すると、毎回 10% オフ",
|
||||||
"aigocode": "AIGoCode は CC Switch の公式パートナーです。このリンクから登録すると、初回チャージ時に 10% のボーナスクレジットがもらえます!",
|
"aigocode": "AIGoCode は CC Switch の公式パートナーです。このリンクから登録すると、初回チャージ時に 10% のボーナスクレジットがもらえます!",
|
||||||
"rightcode": "RightCode は CC Switch の公式パートナーです。このリンクから登録すると、毎回のチャージに 5% のボーナスクレジットがもらえます!",
|
"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}} *",
|
"parameterConfig": "パラメーター設定 - {{name}} *",
|
||||||
"mainModel": "メインモデル(任意)",
|
"mainModel": "メインモデル(任意)",
|
||||||
@@ -667,6 +669,10 @@
|
|||||||
"apiFormatHint": "プロバイダー API の入力フォーマットを選択",
|
"apiFormatHint": "プロバイダー API の入力フォーマットを選択",
|
||||||
"apiFormatAnthropic": "Anthropic Messages(ネイティブ)",
|
"apiFormatAnthropic": "Anthropic Messages(ネイティブ)",
|
||||||
"apiFormatOpenAIChat": "OpenAI Chat Completions(プロキシが必要)",
|
"apiFormatOpenAIChat": "OpenAI Chat Completions(プロキシが必要)",
|
||||||
|
"authField": "認証フィールド",
|
||||||
|
"authFieldAuthToken": "Auth Token(デフォルト)",
|
||||||
|
"authFieldApiKey": "API Key",
|
||||||
|
"authFieldHint": "ほとんどのサードパーティプロバイダーは Auth Token を使用します。一部は API Key が必要です",
|
||||||
"anthropicDefaultHaikuModel": "既定 Haiku モデル",
|
"anthropicDefaultHaikuModel": "既定 Haiku モデル",
|
||||||
"anthropicDefaultSonnetModel": "既定 Sonnet モデル",
|
"anthropicDefaultSonnetModel": "既定 Sonnet モデル",
|
||||||
"anthropicDefaultOpusModel": "既定 Opus モデル",
|
"anthropicDefaultOpusModel": "既定 Opus モデル",
|
||||||
@@ -738,33 +744,15 @@
|
|||||||
"authJsonHint": "Codex の auth.json 設定内容",
|
"authJsonHint": "Codex の auth.json 設定内容",
|
||||||
"configToml": "config.toml (TOML)",
|
"configToml": "config.toml (TOML)",
|
||||||
"configTomlHint": "Codex の config.toml 設定内容",
|
"configTomlHint": "Codex の config.toml 設定内容",
|
||||||
"writeCommonConfig": "共通設定を書き込む",
|
"apiUrlLabel": "API リクエスト URL"
|
||||||
"editCommonConfig": "共通設定を編集",
|
|
||||||
"editCommonConfigTitle": "Codex 共通設定スニペットを編集",
|
|
||||||
"commonConfigHint": "「共通設定を書き込む」がオンの場合、config.toml の末尾に追記されます",
|
|
||||||
"apiUrlLabel": "API リクエスト URL",
|
|
||||||
"extractFromCurrent": "編集内容から抽出",
|
|
||||||
"extractNoCommonConfig": "編集内容から抽出できる共通設定がありません",
|
|
||||||
"extractFailed": "抽出に失敗しました: {{error}}",
|
|
||||||
"saveFailed": "保存に失敗しました: {{error}}"
|
|
||||||
},
|
},
|
||||||
"geminiConfig": {
|
"geminiConfig": {
|
||||||
"envFile": "環境変数 (.env)",
|
"envFile": "環境変数 (.env)",
|
||||||
"envFileHint": ".env 形式で Gemini の環境変数を設定",
|
"envFileHint": ".env 形式で Gemini の環境変数を設定",
|
||||||
"configJson": "設定ファイル (config.json)",
|
"configJson": "設定ファイル (config.json)",
|
||||||
"configJsonHint": "Gemini 拡張パラメーターを JSON 形式で設定(任意)",
|
"configJsonHint": "Gemini 拡張パラメーターを JSON 形式で設定(任意)",
|
||||||
"writeCommonConfig": "共通設定を書き込む",
|
|
||||||
"editCommonConfig": "共通設定を編集",
|
|
||||||
"editCommonConfigTitle": "Gemini 共通設定スニペットを編集",
|
|
||||||
"commonConfigHint": "このスニペットは Gemini の .env に書き込みます(GOOGLE_GEMINI_BASE_URL、GEMINI_API_KEY は使用できません)",
|
|
||||||
"extractFromCurrent": "編集内容から抽出",
|
|
||||||
"extractNoCommonConfig": "編集内容から抽出できる共通設定がありません",
|
|
||||||
"extractFailed": "抽出に失敗しました: {{error}}",
|
|
||||||
"saveFailed": "保存に失敗しました: {{error}}",
|
|
||||||
"extractedConfigInvalid": "抽出した設定のフォーマットが不正です",
|
"extractedConfigInvalid": "抽出した設定のフォーマットが不正です",
|
||||||
"invalidJsonFormat": "共通設定スニペットの形式が不正です(有効な JSON でなければなりません)",
|
"invalidJsonFormat": "共通設定スニペットの形式が不正です(有効な JSON でなければなりません)",
|
||||||
"commonConfigInvalidKeys": "共通設定スニペットに GOOGLE_GEMINI_BASE_URL または GEMINI_API_KEY を含めることはできません(検出: {{keys}})",
|
|
||||||
"commonConfigInvalidValues": "共通設定スニペットの値は文字列である必要があります",
|
|
||||||
"noCommonConfigToApply": "共通設定スニペットが空、または適用できる項目がありません",
|
"noCommonConfigToApply": "共通設定スニペットが空、または適用できる項目がありません",
|
||||||
"configMergeFailed": "設定のマージに失敗しました: {{error}}",
|
"configMergeFailed": "設定のマージに失敗しました: {{error}}",
|
||||||
"configReplaceFailed": "設定の置換に失敗しました: {{error}}"
|
"configReplaceFailed": "設定の置換に失敗しました: {{error}}"
|
||||||
@@ -800,38 +788,6 @@
|
|||||||
"modelOptionKeyPlaceholder": "provider",
|
"modelOptionKeyPlaceholder": "provider",
|
||||||
"modelOptionValuePlaceholder": "{\"order\": [\"baseten\"]}"
|
"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": {
|
"providerPreset": {
|
||||||
"label": "プロバイダータイプ",
|
"label": "プロバイダータイプ",
|
||||||
"custom": "カスタム設定",
|
"custom": "カスタム設定",
|
||||||
@@ -1219,22 +1175,60 @@
|
|||||||
"saveSuccess": "保存しました",
|
"saveSuccess": "保存しました",
|
||||||
"saveFailed": "保存に失敗しました",
|
"saveFailed": "保存に失敗しました",
|
||||||
"loadFailed": "読み込みに失敗しました",
|
"loadFailed": "読み込みに失敗しました",
|
||||||
|
"openDirectory": "ファイルマネージャーで開く",
|
||||||
"dailyMemory": {
|
"dailyMemory": {
|
||||||
"title": "デイリーメモリー",
|
"title": "デイリーメモリー",
|
||||||
"sectionTitle": "デイリーメモリー",
|
"sectionTitle": "デイリーメモリー",
|
||||||
"cardTitle": "デイリーメモリーファイル",
|
"cardTitle": "デイリーメモリーファイル",
|
||||||
"cardDescription": "デイリーメモリーの閲覧・管理",
|
"cardDescription": "デイリーメモリーの閲覧・管理",
|
||||||
"createToday": "今日のノート",
|
"createToday": "メモリーを追加",
|
||||||
"empty": "デイリーメモリーファイルはまだありません",
|
"empty": "デイリーメモリーファイルはまだありません",
|
||||||
"loadFailed": "デイリーメモリーファイルの読み込みに失敗しました",
|
"loadFailed": "デイリーメモリーファイルの読み込みに失敗しました",
|
||||||
"createFailed": "デイリーメモリーファイルの作成に失敗しました",
|
"createFailed": "デイリーメモリーファイルの作成に失敗しました",
|
||||||
"deleteSuccess": "デイリーメモリーファイルを削除しました",
|
"deleteSuccess": "デイリーメモリーファイルを削除しました",
|
||||||
"deleteFailed": "デイリーメモリーファイルの削除に失敗しました",
|
"deleteFailed": "デイリーメモリーファイルの削除に失敗しました",
|
||||||
"confirmDeleteTitle": "デイリーメモリーを削除",
|
"confirmDeleteTitle": "デイリーメモリーを削除",
|
||||||
"confirmDeleteMessage": "{{date}} のデイリーメモリーを削除しますか?この操作は取り消せません。"
|
"confirmDeleteMessage": "{{date}} のデイリーメモリーを削除しますか?この操作は取り消せません。",
|
||||||
|
"searchPlaceholder": "全文検索...",
|
||||||
|
"searchScopeHint": "すべてのデイリーメモリーを全文検索 ⌘F",
|
||||||
|
"searchCloseHint": "Escで閉じる",
|
||||||
|
"noSearchResults": "検索に一致するデイリーメモリーはありません。",
|
||||||
|
"searching": "検索中...",
|
||||||
|
"searchFailed": "検索に失敗しました",
|
||||||
|
"matchCount": "{{count}}件一致"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"openclaw": {
|
"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": {
|
"env": {
|
||||||
"title": "環境変数",
|
"title": "環境変数",
|
||||||
"description": "openclaw.json の環境変数設定を管理(APIキー、カスタム変数など)",
|
"description": "openclaw.json の環境変数設定を管理(APIキー、カスタム変数など)",
|
||||||
@@ -1364,6 +1358,7 @@
|
|||||||
"http429": "リクエストが多すぎます。時間をおいて再試行してください",
|
"http429": "リクエストが多すぎます。時間をおいて再試行してください",
|
||||||
"parseMetadataFailed": "スキルメタデータの解析に失敗しました",
|
"parseMetadataFailed": "スキルメタデータの解析に失敗しました",
|
||||||
"getHomeDirFailed": "ユーザーのホームディレクトリを取得できません",
|
"getHomeDirFailed": "ユーザーのホームディレクトリを取得できません",
|
||||||
|
"noSkillsInZip": "ZIP ファイルにスキルが見つかりません(SKILL.md ファイルが必要です)",
|
||||||
"networkError": "ネットワークエラー",
|
"networkError": "ネットワークエラー",
|
||||||
"fsError": "ファイルシステムエラー",
|
"fsError": "ファイルシステムエラー",
|
||||||
"unknownError": "不明なエラー",
|
"unknownError": "不明なエラー",
|
||||||
@@ -1374,7 +1369,8 @@
|
|||||||
"checkRepoUrl": "リポジトリ URL とブランチ名を確認してください",
|
"checkRepoUrl": "リポジトリ URL とブランチ名を確認してください",
|
||||||
"checkDiskSpace": "ディスク容量を確認してください",
|
"checkDiskSpace": "ディスク容量を確認してください",
|
||||||
"checkPermission": "ディレクトリの権限を確認してください",
|
"checkPermission": "ディレクトリの権限を確認してください",
|
||||||
"uninstallFirst": "同名のスキルを先にアンインストールしてください"
|
"uninstallFirst": "同名のスキルを先にアンインストールしてください",
|
||||||
|
"checkZipContent": "ZIP ファイルに有効なスキルディレクトリ(SKILL.md を含む)が含まれていることを確認してください"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repo": {
|
"repo": {
|
||||||
@@ -1644,7 +1640,12 @@
|
|||||||
"toast": {
|
"toast": {
|
||||||
"saved": "プロキシ設定を保存しました",
|
"saved": "プロキシ設定を保存しました",
|
||||||
"saveFailed": "保存に失敗しました: {{error}}"
|
"saveFailed": "保存に失敗しました: {{error}}"
|
||||||
}
|
},
|
||||||
|
"invalidPort": "無効なポートです。1024〜65535 の数値を入力してください",
|
||||||
|
"invalidAddress": "無効なアドレスです。有効な IP アドレス(例: 127.0.0.1)または localhost を入力してください",
|
||||||
|
"configSaved": "プロキシ設定を保存しました",
|
||||||
|
"configSaveFailed": "設定の保存に失敗しました",
|
||||||
|
"restartRequired": "アドレスまたはポートの変更を反映するにはプロキシサービスの再起動が必要です"
|
||||||
},
|
},
|
||||||
"switchFailed": "切り替えに失敗しました: {{error}}",
|
"switchFailed": "切り替えに失敗しました: {{error}}",
|
||||||
"failover": {
|
"failover": {
|
||||||
@@ -1674,6 +1675,7 @@
|
|||||||
"info": "フェイルオーバーキューに複数のプロバイダーが設定されている場合、リクエストが失敗すると優先度順に試行します。プロバイダーが連続失敗のしきい値に達すると、サーキットブレーカーが開き、一時的にスキップされます。",
|
"info": "フェイルオーバーキューに複数のプロバイダーが設定されている場合、リクエストが失敗すると優先度順に試行します。プロバイダーが連続失敗のしきい値に達すると、サーキットブレーカーが開き、一時的にスキップされます。",
|
||||||
"configSaved": "自動フェイルオーバー設定を保存しました",
|
"configSaved": "自動フェイルオーバー設定を保存しました",
|
||||||
"configSaveFailed": "保存に失敗しました",
|
"configSaveFailed": "保存に失敗しました",
|
||||||
|
"validationFailed": "以下のフィールドが有効範囲外です: {{fields}}",
|
||||||
"retrySettings": "リトライとタイムアウト設定",
|
"retrySettings": "リトライとタイムアウト設定",
|
||||||
"failureThreshold": "失敗しきい値",
|
"failureThreshold": "失敗しきい値",
|
||||||
"failureThresholdHint": "この回数連続で失敗するとサーキットブレーカーが開きます(推奨: 3-10)",
|
"failureThresholdHint": "この回数連続で失敗するとサーキットブレーカーが開きます(推奨: 3-10)",
|
||||||
@@ -1726,6 +1728,16 @@
|
|||||||
"streamingIdle": "ストリーミングアイドルタイムアウト",
|
"streamingIdle": "ストリーミングアイドルタイムアウト",
|
||||||
"nonStreaming": "非ストリーミングタイムアウト"
|
"nonStreaming": "非ストリーミングタイムアウト"
|
||||||
},
|
},
|
||||||
|
"circuitBreaker": {
|
||||||
|
"failureThreshold": "失敗しきい値",
|
||||||
|
"successThreshold": "成功しきい値",
|
||||||
|
"timeoutSeconds": "タイムアウト(秒)",
|
||||||
|
"errorRateThreshold": "エラー率しきい値",
|
||||||
|
"minRequests": "最小リクエスト数",
|
||||||
|
"validationFailed": "以下のフィールドが有効範囲外です: {{fields}}",
|
||||||
|
"configSaved": "サーキットブレーカー設定を保存しました",
|
||||||
|
"saveFailed": "保存に失敗しました"
|
||||||
|
},
|
||||||
"universalProvider": {
|
"universalProvider": {
|
||||||
"title": "統合プロバイダー",
|
"title": "統合プロバイダー",
|
||||||
"description": "統合プロバイダーは Claude、Codex、Gemini の設定を同時に管理します。変更は有効なすべてのアプリに自動的に同期されます。",
|
"description": "統合プロバイダーは Claude、Codex、Gemini の設定を同時に管理します。変更は有効なすべてのアプリに自動的に同期されます。",
|
||||||
@@ -1750,6 +1762,7 @@
|
|||||||
"syncError": "同期に失敗しました",
|
"syncError": "同期に失敗しました",
|
||||||
"noAppsEnabled": "有効なアプリがありません",
|
"noAppsEnabled": "有効なアプリがありません",
|
||||||
"added": "統合プロバイダーを追加しました",
|
"added": "統合プロバイダーを追加しました",
|
||||||
|
"addedAndSynced": "統合プロバイダーを追加して同期しました",
|
||||||
"updated": "統合プロバイダーを更新しました",
|
"updated": "統合プロバイダーを更新しました",
|
||||||
"deleted": "統合プロバイダーを削除しました",
|
"deleted": "統合プロバイダーを削除しました",
|
||||||
"addSuccess": "統合プロバイダーを追加しました",
|
"addSuccess": "統合プロバイダーを追加しました",
|
||||||
@@ -1802,10 +1815,6 @@
|
|||||||
"enabled": "有効中",
|
"enabled": "有効中",
|
||||||
"disabled": "OMO を無効化しました",
|
"disabled": "OMO を無効化しました",
|
||||||
"disableFailed": "OMO の無効化に失敗しました: {{error}}",
|
"disableFailed": "OMO の無効化に失敗しました: {{error}}",
|
||||||
"writeCommonConfig": "共通設定に書き込む",
|
|
||||||
"editCommonConfig": "共通設定を編集",
|
|
||||||
"editCommonConfigTitle": "共通設定",
|
|
||||||
"commonConfigHint": "OMO 共通設定は有効にしたすべての OMO 設定に統合されます",
|
|
||||||
"selectPlaceholder": "選択してください...",
|
"selectPlaceholder": "選択してください...",
|
||||||
"clear": "クリア",
|
"clear": "クリア",
|
||||||
"clearWrapped": "(クリア)",
|
"clearWrapped": "(クリア)",
|
||||||
@@ -1835,6 +1844,10 @@
|
|||||||
"customCategories": "カスタムカテゴリ",
|
"customCategories": "カスタムカテゴリ",
|
||||||
"modelConfiguration": "モデル設定",
|
"modelConfiguration": "モデル設定",
|
||||||
"fillRecommended": "推奨を入力",
|
"fillRecommended": "推奨を入力",
|
||||||
|
"fillRecommendedSuccess": "推奨モデル {{count}} 件を設定しました",
|
||||||
|
"fillRecommendedPartial": "推奨モデル {{filled}} 件を設定、{{unmatched}} 件は未一致",
|
||||||
|
"fillRecommendedAllSet": "すべてのスロットにモデルが設定済みです",
|
||||||
|
"fillRecommendedNoMatch": "推奨モデルが設定済みプロバイダーに見つかりませんでした",
|
||||||
"configSummary": "{{agents}} 個の Agent、{{categories}} 個の Category を設定済み · ⚙ で詳細を展開",
|
"configSummary": "{{agents}} 個の Agent、{{categories}} 個の Category を設定済み · ⚙ で詳細を展開",
|
||||||
"enabledModelsCount": "設定済みモデル {{count}} 件",
|
"enabledModelsCount": "設定済みモデル {{count}} 件",
|
||||||
"source": "出典:",
|
"source": "出典:",
|
||||||
|
|||||||
+65
-71
@@ -51,15 +51,8 @@
|
|||||||
},
|
},
|
||||||
"claudeConfig": {
|
"claudeConfig": {
|
||||||
"configLabel": "Claude Code 配置 (JSON) *",
|
"configLabel": "Claude Code 配置 (JSON) *",
|
||||||
"writeCommonConfig": "写入通用配置",
|
|
||||||
"editCommonConfig": "编辑通用配置",
|
|
||||||
"editCommonConfigTitle": "编辑通用配置片段",
|
|
||||||
"commonConfigHint": "该片段会在勾选\"写入通用配置\"时合并到 settings.json 中",
|
|
||||||
"fullSettingsHint": "完整的 Claude Code settings.json 配置内容",
|
"fullSettingsHint": "完整的 Claude Code settings.json 配置内容",
|
||||||
"extractFromCurrent": "从编辑内容提取",
|
"fragmentSettingsHint": "此供应商的配置片段,激活后将写入 settings.json",
|
||||||
"extractNoCommonConfig": "当前编辑内容没有可提取的通用配置",
|
|
||||||
"extractFailed": "提取失败: {{error}}",
|
|
||||||
"saveFailed": "保存失败: {{error}}",
|
|
||||||
"hideAttribution": "隐藏 AI 署名",
|
"hideAttribution": "隐藏 AI 署名",
|
||||||
"alwaysThinking": "扩展思考",
|
"alwaysThinking": "扩展思考",
|
||||||
"enableTeammates": "Teammates 模式"
|
"enableTeammates": "Teammates 模式"
|
||||||
@@ -125,11 +118,7 @@
|
|||||||
"notes": "备注",
|
"notes": "备注",
|
||||||
"notesPlaceholder": "例如:公司专用账号",
|
"notesPlaceholder": "例如:公司专用账号",
|
||||||
"configJson": "配置 JSON",
|
"configJson": "配置 JSON",
|
||||||
"writeCommonConfig": "写入通用配置",
|
|
||||||
"editCommonConfigButton": "编辑通用配置",
|
|
||||||
"configJsonHint": "请填写完整的 Claude Code 配置",
|
"configJsonHint": "请填写完整的 Claude Code 配置",
|
||||||
"editCommonConfigTitle": "编辑通用配置片段",
|
|
||||||
"editCommonConfigHint": "通用配置片段将合并到所有启用它的供应商配置中",
|
|
||||||
"addProvider": "添加供应商",
|
"addProvider": "添加供应商",
|
||||||
"sortUpdated": "排序已更新",
|
"sortUpdated": "排序已更新",
|
||||||
"usageSaved": "用量查询配置已保存",
|
"usageSaved": "用量查询配置已保存",
|
||||||
@@ -319,7 +308,17 @@
|
|||||||
"rename": "重命名",
|
"rename": "重命名",
|
||||||
"renameSuccess": "备份重命名成功",
|
"renameSuccess": "备份重命名成功",
|
||||||
"renameFailed": "重命名失败",
|
"renameFailed": "重命名失败",
|
||||||
"namePlaceholder": "输入新名称"
|
"namePlaceholder": "输入新名称",
|
||||||
|
"createBackup": "立即备份",
|
||||||
|
"creating": "备份中...",
|
||||||
|
"createSuccess": "备份创建成功",
|
||||||
|
"createFailed": "备份创建失败",
|
||||||
|
"delete": "删除",
|
||||||
|
"deleting": "删除中...",
|
||||||
|
"deleteSuccess": "备份已删除",
|
||||||
|
"deleteFailed": "删除失败",
|
||||||
|
"deleteConfirmTitle": "确认删除备份",
|
||||||
|
"deleteConfirmMessage": "此备份将被永久删除,此操作无法撤消。"
|
||||||
},
|
},
|
||||||
"webdavSync": {
|
"webdavSync": {
|
||||||
"title": "WebDAV 云同步",
|
"title": "WebDAV 云同步",
|
||||||
@@ -637,7 +636,10 @@
|
|||||||
"cubence": "Cubence 是 CC Switch 的官方合作伙伴,使用此链接注册并在充值时填写 \"CCSWITCH\" 优惠码,每次充值均可享受9折优惠",
|
"cubence": "Cubence 是 CC Switch 的官方合作伙伴,使用此链接注册并在充值时填写 \"CCSWITCH\" 优惠码,每次充值均可享受9折优惠",
|
||||||
"aigocode": "AIGoCode 是 CC Switch 的官方合作伙伴,使用此链接注册首次充值时可以获得10%额度奖励!",
|
"aigocode": "AIGoCode 是 CC Switch 的官方合作伙伴,使用此链接注册首次充值时可以获得10%额度奖励!",
|
||||||
"rightcode": "RightCode 是 CC Switch 的官方合作伙伴,使用此链接注册每次充值均可赠送5%额外额度!",
|
"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}} *",
|
"parameterConfig": "参数配置 - {{name}} *",
|
||||||
"mainModel": "主模型 (可选)",
|
"mainModel": "主模型 (可选)",
|
||||||
@@ -667,6 +669,10 @@
|
|||||||
"apiFormatHint": "选择供应商 API 的输入格式",
|
"apiFormatHint": "选择供应商 API 的输入格式",
|
||||||
"apiFormatAnthropic": "Anthropic Messages (原生)",
|
"apiFormatAnthropic": "Anthropic Messages (原生)",
|
||||||
"apiFormatOpenAIChat": "OpenAI Chat Completions (需开启代理)",
|
"apiFormatOpenAIChat": "OpenAI Chat Completions (需开启代理)",
|
||||||
|
"authField": "认证字段",
|
||||||
|
"authFieldAuthToken": "Auth Token (默认)",
|
||||||
|
"authFieldApiKey": "API Key",
|
||||||
|
"authFieldHint": "大多数第三方供应商使用 Auth Token;少数供应商需要 API Key",
|
||||||
"anthropicDefaultHaikuModel": "Haiku 默认模型",
|
"anthropicDefaultHaikuModel": "Haiku 默认模型",
|
||||||
"anthropicDefaultSonnetModel": "Sonnet 默认模型",
|
"anthropicDefaultSonnetModel": "Sonnet 默认模型",
|
||||||
"anthropicDefaultOpusModel": "Opus 默认模型",
|
"anthropicDefaultOpusModel": "Opus 默认模型",
|
||||||
@@ -738,33 +744,15 @@
|
|||||||
"authJsonHint": "Codex auth.json 配置内容",
|
"authJsonHint": "Codex auth.json 配置内容",
|
||||||
"configToml": "config.toml (TOML)",
|
"configToml": "config.toml (TOML)",
|
||||||
"configTomlHint": "Codex config.toml 配置内容",
|
"configTomlHint": "Codex config.toml 配置内容",
|
||||||
"writeCommonConfig": "写入通用配置",
|
"apiUrlLabel": "API 请求地址"
|
||||||
"editCommonConfig": "编辑通用配置",
|
|
||||||
"editCommonConfigTitle": "编辑 Codex 通用配置片段",
|
|
||||||
"commonConfigHint": "该片段会在勾选'写入通用配置'时追加到 config.toml 末尾",
|
|
||||||
"apiUrlLabel": "API 请求地址",
|
|
||||||
"extractFromCurrent": "从编辑内容提取",
|
|
||||||
"extractNoCommonConfig": "当前编辑内容没有可提取的通用配置",
|
|
||||||
"extractFailed": "提取失败: {{error}}",
|
|
||||||
"saveFailed": "保存失败: {{error}}"
|
|
||||||
},
|
},
|
||||||
"geminiConfig": {
|
"geminiConfig": {
|
||||||
"envFile": "环境变量 (.env)",
|
"envFile": "环境变量 (.env)",
|
||||||
"envFileHint": "使用 .env 格式配置 Gemini 环境变量",
|
"envFileHint": "使用 .env 格式配置 Gemini 环境变量",
|
||||||
"configJson": "配置文件 (config.json)",
|
"configJson": "配置文件 (config.json)",
|
||||||
"configJsonHint": "使用 JSON 格式配置 Gemini 扩展参数(可选)",
|
"configJsonHint": "使用 JSON 格式配置 Gemini 扩展参数(可选)",
|
||||||
"writeCommonConfig": "写入通用配置",
|
|
||||||
"editCommonConfig": "编辑通用配置",
|
|
||||||
"editCommonConfigTitle": "编辑 Gemini 通用配置片段",
|
|
||||||
"commonConfigHint": "该片段会写入 Gemini 的 .env(不允许包含 GOOGLE_GEMINI_BASE_URL、GEMINI_API_KEY)",
|
|
||||||
"extractFromCurrent": "从编辑内容提取",
|
|
||||||
"extractNoCommonConfig": "当前编辑内容没有可提取的通用配置",
|
|
||||||
"extractFailed": "提取失败: {{error}}",
|
|
||||||
"saveFailed": "保存失败: {{error}}",
|
|
||||||
"extractedConfigInvalid": "提取的配置格式错误",
|
"extractedConfigInvalid": "提取的配置格式错误",
|
||||||
"invalidJsonFormat": "通用配置片段格式错误(必须是有效的 JSON)",
|
"invalidJsonFormat": "通用配置片段格式错误(必须是有效的 JSON)",
|
||||||
"commonConfigInvalidKeys": "通用配置片段不能包含 GOOGLE_GEMINI_BASE_URL 或 GEMINI_API_KEY(发现:{{keys}})",
|
|
||||||
"commonConfigInvalidValues": "通用配置片段的值必须是字符串",
|
|
||||||
"noCommonConfigToApply": "通用配置片段为空或没有可写入的内容",
|
"noCommonConfigToApply": "通用配置片段为空或没有可写入的内容",
|
||||||
"configMergeFailed": "配置合并失败: {{error}}",
|
"configMergeFailed": "配置合并失败: {{error}}",
|
||||||
"configReplaceFailed": "配置替换失败: {{error}}"
|
"configReplaceFailed": "配置替换失败: {{error}}"
|
||||||
@@ -800,38 +788,6 @@
|
|||||||
"modelOptionKeyPlaceholder": "provider",
|
"modelOptionKeyPlaceholder": "provider",
|
||||||
"modelOptionValuePlaceholder": "{\"order\": [\"baseten\"]}"
|
"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": {
|
"providerPreset": {
|
||||||
"label": "预设供应商",
|
"label": "预设供应商",
|
||||||
"custom": "自定义配置",
|
"custom": "自定义配置",
|
||||||
@@ -1219,22 +1175,60 @@
|
|||||||
"saveSuccess": "保存成功",
|
"saveSuccess": "保存成功",
|
||||||
"saveFailed": "保存失败",
|
"saveFailed": "保存失败",
|
||||||
"loadFailed": "读取失败",
|
"loadFailed": "读取失败",
|
||||||
|
"openDirectory": "在文件管理器中打开",
|
||||||
"dailyMemory": {
|
"dailyMemory": {
|
||||||
"title": "每日记忆",
|
"title": "每日记忆",
|
||||||
"sectionTitle": "每日记忆",
|
"sectionTitle": "每日记忆",
|
||||||
"cardTitle": "每日记忆文件",
|
"cardTitle": "每日记忆文件",
|
||||||
"cardDescription": "浏览管理每日记忆",
|
"cardDescription": "浏览管理每日记忆",
|
||||||
"createToday": "今日笔记",
|
"createToday": "添加记忆",
|
||||||
"empty": "暂无每日记忆文件",
|
"empty": "暂无每日记忆文件",
|
||||||
"loadFailed": "加载每日记忆文件失败",
|
"loadFailed": "加载每日记忆文件失败",
|
||||||
"createFailed": "创建每日记忆文件失败",
|
"createFailed": "创建每日记忆文件失败",
|
||||||
"deleteSuccess": "每日记忆文件已删除",
|
"deleteSuccess": "每日记忆文件已删除",
|
||||||
"deleteFailed": "删除每日记忆文件失败",
|
"deleteFailed": "删除每日记忆文件失败",
|
||||||
"confirmDeleteTitle": "删除每日记忆",
|
"confirmDeleteTitle": "删除每日记忆",
|
||||||
"confirmDeleteMessage": "确定删除 {{date}} 的每日记忆吗?此操作不可撤销。"
|
"confirmDeleteMessage": "确定删除 {{date}} 的每日记忆吗?此操作不可撤销。",
|
||||||
|
"searchPlaceholder": "搜索全文内容...",
|
||||||
|
"searchScopeHint": "全文搜索所有每日记忆 ⌘F",
|
||||||
|
"searchCloseHint": "Esc 关闭",
|
||||||
|
"noSearchResults": "没有找到匹配的每日记忆。",
|
||||||
|
"searching": "搜索中...",
|
||||||
|
"searchFailed": "搜索失败",
|
||||||
|
"matchCount": "{{count}} 处匹配"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"openclaw": {
|
"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": {
|
"env": {
|
||||||
"title": "环境变量",
|
"title": "环境变量",
|
||||||
"description": "管理 openclaw.json 中的环境变量配置(API Key、自定义变量等)",
|
"description": "管理 openclaw.json 中的环境变量配置(API Key、自定义变量等)",
|
||||||
@@ -1821,10 +1815,6 @@
|
|||||||
"enabled": "启用中",
|
"enabled": "启用中",
|
||||||
"disabled": "OMO 已停用",
|
"disabled": "OMO 已停用",
|
||||||
"disableFailed": "停用 OMO 失败: {{error}}",
|
"disableFailed": "停用 OMO 失败: {{error}}",
|
||||||
"writeCommonConfig": "写入通用配置",
|
|
||||||
"editCommonConfig": "编辑通用配置",
|
|
||||||
"editCommonConfigTitle": "通用配置",
|
|
||||||
"commonConfigHint": "OMO 通用配置将合并到所有启用它的 OMO 配置中",
|
|
||||||
"selectPlaceholder": "请选择...",
|
"selectPlaceholder": "请选择...",
|
||||||
"clear": "清空",
|
"clear": "清空",
|
||||||
"clearWrapped": "(清空)",
|
"clearWrapped": "(清空)",
|
||||||
@@ -1854,6 +1844,10 @@
|
|||||||
"customCategories": "自定义分类",
|
"customCategories": "自定义分类",
|
||||||
"modelConfiguration": "模型配置",
|
"modelConfiguration": "模型配置",
|
||||||
"fillRecommended": "填充推荐",
|
"fillRecommended": "填充推荐",
|
||||||
|
"fillRecommendedSuccess": "已填充 {{count}} 个推荐模型",
|
||||||
|
"fillRecommendedPartial": "已填充 {{filled}} 个推荐模型,{{unmatched}} 个未匹配",
|
||||||
|
"fillRecommendedAllSet": "所有槽位已有模型配置",
|
||||||
|
"fillRecommendedNoMatch": "推荐模型在已配置的供应商中未找到匹配",
|
||||||
"configSummary": "已配置 {{agents}} 个 Agent,{{categories}} 个 Category · 点击 ⚙ 展开高级参数",
|
"configSummary": "已配置 {{agents}} 个 Agent,{{categories}} 个 Category · 点击 ⚙ 展开高级参数",
|
||||||
"enabledModelsCount": "可选已配置模型 {{count}} 个",
|
"enabledModelsCount": "可选已配置模型 {{count}} 个",
|
||||||
"source": "来源:",
|
"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> = {
|
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>`,
|
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>`,
|
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>`,
|
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>`,
|
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>`,
|
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>`,
|
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>`,
|
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>`,
|
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>`,
|
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>`,
|
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>`,
|
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>`,
|
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>`,
|
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>`,
|
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>`,
|
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>`,
|
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,77 +0,0 @@
|
|||||||
// 配置相关 API
|
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
|
||||||
|
|
||||||
export type AppType = "claude" | "codex" | "gemini" | "omo" | "omo_slim";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 Claude 通用配置片段(已废弃,使用 getCommonConfigSnippet)
|
|
||||||
* @returns 通用配置片段(JSON 字符串),如果不存在则返回 null
|
|
||||||
* @deprecated 使用 getCommonConfigSnippet('claude') 替代
|
|
||||||
*/
|
|
||||||
export async function getClaudeCommonConfigSnippet(): Promise<string | null> {
|
|
||||||
return invoke<string | null>("get_claude_common_config_snippet");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置 Claude 通用配置片段(已废弃,使用 setCommonConfigSnippet)
|
|
||||||
* @param snippet - 通用配置片段(JSON 字符串)
|
|
||||||
* @throws 如果 JSON 格式无效
|
|
||||||
* @deprecated 使用 setCommonConfigSnippet('claude', snippet) 替代
|
|
||||||
*/
|
|
||||||
export async function setClaudeCommonConfigSnippet(
|
|
||||||
snippet: string,
|
|
||||||
): Promise<void> {
|
|
||||||
return invoke("set_claude_common_config_snippet", { snippet });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取通用配置片段(统一接口)
|
|
||||||
* @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 });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 提取通用配置片段
|
|
||||||
*
|
|
||||||
* 默认读取当前激活供应商的配置;若传入 `options.settingsConfig`,则从编辑器当前内容提取。
|
|
||||||
* 会自动排除差异化字段(API Key、模型配置、端点等),返回可复用的通用配置片段。
|
|
||||||
*
|
|
||||||
* @param appType - 应用类型(claude/codex/gemini)
|
|
||||||
* @param options - 可选:提取来源
|
|
||||||
* @returns 提取的通用配置片段(JSON/TOML 字符串)
|
|
||||||
*/
|
|
||||||
export type ExtractCommonConfigSnippetOptions = {
|
|
||||||
settingsConfig?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function extractCommonConfigSnippet(
|
|
||||||
appType: Exclude<AppType, "omo">,
|
|
||||||
options?: ExtractCommonConfigSnippetOptions,
|
|
||||||
): Promise<string> {
|
|
||||||
const args: Record<string, unknown> = { appType };
|
|
||||||
const settingsConfig = options?.settingsConfig;
|
|
||||||
|
|
||||||
if (typeof settingsConfig === "string" && settingsConfig.trim()) {
|
|
||||||
args.settingsConfig = settingsConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
return invoke<string>("extract_common_config_snippet", args);
|
|
||||||
}
|
|
||||||
@@ -11,6 +11,5 @@ export { proxyApi } from "./proxy";
|
|||||||
export { openclawApi } from "./openclaw";
|
export { openclawApi } from "./openclaw";
|
||||||
export { sessionsApi } from "./sessions";
|
export { sessionsApi } from "./sessions";
|
||||||
export { workspaceApi } from "./workspace";
|
export { workspaceApi } from "./workspace";
|
||||||
export * as configApi from "./config";
|
|
||||||
export type { ProviderSwitchEvent } from "./providers";
|
export type { ProviderSwitchEvent } from "./providers";
|
||||||
export type { Prompt } from "./prompts";
|
export type { Prompt } from "./prompts";
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ export const omoApi = {
|
|||||||
readLocalFile: (): Promise<OmoLocalFileData> => invoke("read_omo_local_file"),
|
readLocalFile: (): Promise<OmoLocalFileData> => invoke("read_omo_local_file"),
|
||||||
getCurrentOmoProviderId: (): Promise<string> =>
|
getCurrentOmoProviderId: (): Promise<string> =>
|
||||||
invoke("get_current_omo_provider_id"),
|
invoke("get_current_omo_provider_id"),
|
||||||
getOmoProviderCount: (): Promise<number> => invoke("get_omo_provider_count"),
|
|
||||||
disableCurrentOmo: (): Promise<void> => invoke("disable_current_omo"),
|
disableCurrentOmo: (): Promise<void> => invoke("disable_current_omo"),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -14,7 +13,5 @@ export const omoSlimApi = {
|
|||||||
invoke("read_omo_slim_local_file"),
|
invoke("read_omo_slim_local_file"),
|
||||||
getCurrentProviderId: (): Promise<string> =>
|
getCurrentProviderId: (): Promise<string> =>
|
||||||
invoke("get_current_omo_slim_provider_id"),
|
invoke("get_current_omo_slim_provider_id"),
|
||||||
getProviderCount: (): Promise<number> =>
|
|
||||||
invoke("get_omo_slim_provider_count"),
|
|
||||||
disableCurrent: (): Promise<void> => invoke("disable_current_omo_slim"),
|
disableCurrent: (): Promise<void> => invoke("disable_current_omo_slim"),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -223,6 +223,10 @@ export interface BackupEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const backupsApi = {
|
export const backupsApi = {
|
||||||
|
async createDbBackup(): Promise<string> {
|
||||||
|
return await invoke("create_db_backup");
|
||||||
|
},
|
||||||
|
|
||||||
async listDbBackups(): Promise<BackupEntry[]> {
|
async listDbBackups(): Promise<BackupEntry[]> {
|
||||||
return await invoke("list_db_backups");
|
return await invoke("list_db_backups");
|
||||||
},
|
},
|
||||||
@@ -234,4 +238,8 @@ export const backupsApi = {
|
|||||||
async renameDbBackup(oldFilename: string, newName: string): Promise<string> {
|
async renameDbBackup(oldFilename: string, newName: string): Promise<string> {
|
||||||
return await invoke("rename_db_backup", { oldFilename, newName });
|
return await invoke("rename_db_backup", { oldFilename, newName });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async deleteDbBackup(filename: string): Promise<void> {
|
||||||
|
await invoke("delete_db_backup", { filename });
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,15 @@ export interface DailyMemoryFileInfo {
|
|||||||
preview: string;
|
preview: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DailyMemorySearchResult {
|
||||||
|
filename: string;
|
||||||
|
date: string;
|
||||||
|
sizeBytes: number;
|
||||||
|
modifiedAt: number;
|
||||||
|
snippet: string;
|
||||||
|
matchCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
export const workspaceApi = {
|
export const workspaceApi = {
|
||||||
async readFile(filename: string): Promise<string | null> {
|
async readFile(filename: string): Promise<string | null> {
|
||||||
return invoke<string | null>("read_workspace_file", { filename });
|
return invoke<string | null>("read_workspace_file", { filename });
|
||||||
@@ -32,4 +41,16 @@ export const workspaceApi = {
|
|||||||
async deleteDailyMemoryFile(filename: string): Promise<void> {
|
async deleteDailyMemoryFile(filename: string): Promise<void> {
|
||||||
return invoke<void>("delete_daily_memory_file", { filename });
|
return invoke<void>("delete_daily_memory_file", { filename });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async searchDailyMemoryFiles(
|
||||||
|
query: string,
|
||||||
|
): Promise<DailyMemorySearchResult[]> {
|
||||||
|
return invoke<DailyMemorySearchResult[]>("search_daily_memory_files", {
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async openDirectory(subdir: "workspace" | "memory"): Promise<void> {
|
||||||
|
await invoke("open_workspace_directory", { subdir });
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,8 +19,12 @@ export const useAddProviderMutation = (appId: AppId) => {
|
|||||||
let id: string;
|
let id: string;
|
||||||
|
|
||||||
if (appId === "opencode" || appId === "openclaw") {
|
if (appId === "opencode" || appId === "openclaw") {
|
||||||
if (providerInput.category === "omo") {
|
if (
|
||||||
id = `omo-${generateUUID()}`;
|
providerInput.category === "omo" ||
|
||||||
|
providerInput.category === "omo-slim"
|
||||||
|
) {
|
||||||
|
const prefix = providerInput.category === "omo" ? "omo" : "omo-slim";
|
||||||
|
id = `${prefix}-${generateUUID()}`;
|
||||||
} else {
|
} else {
|
||||||
if (!providerInput.providerKey) {
|
if (!providerInput.providerKey) {
|
||||||
throw new Error(`Provider key is required for ${appId}`);
|
throw new Error(`Provider key is required for ${appId}`);
|
||||||
@@ -53,6 +57,12 @@ export const useAddProviderMutation = (appId: AppId) => {
|
|||||||
await queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: ["omo", "provider-count"],
|
queryKey: ["omo", "provider-count"],
|
||||||
});
|
});
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: ["omo-slim", "current-provider-id"],
|
||||||
|
});
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: ["omo-slim", "provider-count"],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -135,6 +145,12 @@ export const useDeleteProviderMutation = (appId: AppId) => {
|
|||||||
await queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: ["omo", "provider-count"],
|
queryKey: ["omo", "provider-count"],
|
||||||
});
|
});
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: ["omo-slim", "current-provider-id"],
|
||||||
|
});
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: ["omo-slim", "provider-count"],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -186,6 +202,9 @@ export const useSwitchProviderMutation = (appId: AppId) => {
|
|||||||
await queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: ["omo", "current-provider-id"],
|
queryKey: ["omo", "current-provider-id"],
|
||||||
});
|
});
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: ["omo-slim", "current-provider-id"],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (appId === "openclaw") {
|
if (appId === "openclaw") {
|
||||||
await queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { omoApi, omoSlimApi } from "@/lib/api/omo";
|
import { omoApi, omoSlimApi } from "@/lib/api/omo";
|
||||||
import * as configApi from "@/lib/api/config";
|
|
||||||
import type { OmoGlobalConfig } from "@/types/omo";
|
|
||||||
|
|
||||||
// ── Factory ────────────────────────────────────────────────────
|
// ── Factory ────────────────────────────────────────────────────
|
||||||
|
|
||||||
function createOmoQueryKeys(prefix: string) {
|
function createOmoQueryKeys(prefix: string) {
|
||||||
return {
|
return {
|
||||||
all: [prefix] as const,
|
all: [prefix] as const,
|
||||||
globalConfig: () => [prefix, "global-config"] as const,
|
|
||||||
currentProviderId: () => [prefix, "current-provider-id"] 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,
|
api: typeof omoApi | typeof omoSlimApi,
|
||||||
) {
|
) {
|
||||||
const keys = createOmoQueryKeys(variant);
|
const keys = createOmoQueryKeys(variant);
|
||||||
const snippetKey = variant === "omo" ? "omo" : "omo_slim";
|
|
||||||
|
|
||||||
function invalidateAll(queryClient: ReturnType<typeof useQueryClient>) {
|
function invalidateAll(queryClient: ReturnType<typeof useQueryClient>) {
|
||||||
queryClient.invalidateQueries({ queryKey: keys.globalConfig() });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["providers"] });
|
queryClient.invalidateQueries({ queryKey: ["providers"] });
|
||||||
queryClient.invalidateQueries({ queryKey: keys.currentProviderId() });
|
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) {
|
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() {
|
function useReadLocalFile() {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: () => api.readLocalFile(),
|
mutationFn: () => api.readLocalFile(),
|
||||||
@@ -116,10 +51,7 @@ function createOmoQueryHooks(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
keys,
|
keys,
|
||||||
useGlobalConfig,
|
|
||||||
useCurrentProviderId,
|
useCurrentProviderId,
|
||||||
useProviderCount,
|
|
||||||
useSaveGlobalConfig,
|
|
||||||
useReadLocalFile,
|
useReadLocalFile,
|
||||||
useDisableCurrent,
|
useDisableCurrent,
|
||||||
};
|
};
|
||||||
@@ -135,16 +67,10 @@ const omoSlimHooks = createOmoQueryHooks("omo-slim", omoSlimApi);
|
|||||||
export const omoKeys = omoHooks.keys;
|
export const omoKeys = omoHooks.keys;
|
||||||
export const omoSlimKeys = omoSlimHooks.keys;
|
export const omoSlimKeys = omoSlimHooks.keys;
|
||||||
|
|
||||||
export const useOmoGlobalConfig = omoHooks.useGlobalConfig;
|
|
||||||
export const useCurrentOmoProviderId = omoHooks.useCurrentProviderId;
|
export const useCurrentOmoProviderId = omoHooks.useCurrentProviderId;
|
||||||
export const useOmoProviderCount = omoHooks.useProviderCount;
|
|
||||||
export const useSaveOmoGlobalConfig = omoHooks.useSaveGlobalConfig;
|
|
||||||
export const useReadOmoLocalFile = omoHooks.useReadLocalFile;
|
export const useReadOmoLocalFile = omoHooks.useReadLocalFile;
|
||||||
export const useDisableCurrentOmo = omoHooks.useDisableCurrent;
|
export const useDisableCurrentOmo = omoHooks.useDisableCurrent;
|
||||||
|
|
||||||
export const useOmoSlimGlobalConfig = omoSlimHooks.useGlobalConfig;
|
|
||||||
export const useCurrentOmoSlimProviderId = omoSlimHooks.useCurrentProviderId;
|
export const useCurrentOmoSlimProviderId = omoSlimHooks.useCurrentProviderId;
|
||||||
export const useOmoSlimProviderCount = omoSlimHooks.useProviderCount;
|
|
||||||
export const useSaveOmoSlimGlobalConfig = omoSlimHooks.useSaveGlobalConfig;
|
|
||||||
export const useReadOmoSlimLocalFile = omoSlimHooks.useReadLocalFile;
|
export const useReadOmoSlimLocalFile = omoSlimHooks.useReadLocalFile;
|
||||||
export const useDisableCurrentOmoSlim = omoSlimHooks.useDisableCurrent;
|
export const useDisableCurrentOmoSlim = omoSlimHooks.useDisableCurrent;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export type ProviderCategory =
|
export type ProviderCategory =
|
||||||
| "official" // 官方
|
| "official" // 官方
|
||||||
| "cn_official" // 开源官方(原"国产官方")
|
| "cn_official" // 开源官方(原"国产官方")
|
||||||
|
| "cloud_provider" // 云服务商(AWS Bedrock 等)
|
||||||
| "aggregator" // 聚合网站
|
| "aggregator" // 聚合网站
|
||||||
| "third_party" // 第三方供应商
|
| "third_party" // 第三方供应商
|
||||||
| "custom" // 自定义
|
| "custom" // 自定义
|
||||||
@@ -145,6 +146,10 @@ export interface ProviderMeta {
|
|||||||
// - "anthropic": 原生 Anthropic Messages API 格式,直接透传
|
// - "anthropic": 原生 Anthropic Messages API 格式,直接透传
|
||||||
// - "openai_chat": OpenAI Chat Completions 格式,需要格式转换
|
// - "openai_chat": OpenAI Chat Completions 格式,需要格式转换
|
||||||
apiFormat?: "anthropic" | "openai_chat";
|
apiFormat?: "anthropic" | "openai_chat";
|
||||||
|
// Claude 认证字段名(仅 Claude 供应商使用)
|
||||||
|
// - "ANTHROPIC_AUTH_TOKEN" (默认): 大多数第三方/聚合供应商
|
||||||
|
// - "ANTHROPIC_API_KEY": 少数供应商需要原生 API Key
|
||||||
|
apiKeyField?: "ANTHROPIC_AUTH_TOKEN" | "ANTHROPIC_API_KEY";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skill 同步方式
|
// Skill 同步方式
|
||||||
@@ -155,6 +160,11 @@ export type SkillSyncMethod = "auto" | "symlink" | "copy";
|
|||||||
// - "openai_chat": OpenAI Chat Completions 格式,需要格式转换
|
// - "openai_chat": OpenAI Chat Completions 格式,需要格式转换
|
||||||
export type ClaudeApiFormat = "anthropic" | "openai_chat";
|
export type ClaudeApiFormat = "anthropic" | "openai_chat";
|
||||||
|
|
||||||
|
// Claude 认证字段类型
|
||||||
|
// - "ANTHROPIC_AUTH_TOKEN": 大多数第三方/聚合供应商使用(默认)
|
||||||
|
// - "ANTHROPIC_API_KEY": 少数供应商需要原生 API Key
|
||||||
|
export type ClaudeApiKeyField = "ANTHROPIC_AUTH_TOKEN" | "ANTHROPIC_API_KEY";
|
||||||
|
|
||||||
// 主页面显示的应用配置
|
// 主页面显示的应用配置
|
||||||
export interface VisibleApps {
|
export interface VisibleApps {
|
||||||
claude: boolean;
|
claude: boolean;
|
||||||
|
|||||||
+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 {
|
export interface OmoLocalFileData {
|
||||||
agents?: Record<string, Record<string, unknown>>;
|
agents?: Record<string, Record<string, unknown>>;
|
||||||
categories?: Record<string, Record<string, unknown>>;
|
categories?: Record<string, Record<string, unknown>>;
|
||||||
otherFields?: Record<string, unknown>;
|
otherFields?: Record<string, unknown>;
|
||||||
global: OmoGlobalConfig;
|
|
||||||
filePath: string;
|
filePath: string;
|
||||||
lastModified?: string;
|
lastModified?: string;
|
||||||
}
|
}
|
||||||
@@ -79,7 +61,7 @@ export const OMO_BUILTIN_AGENTS: OmoAgentDef[] = [
|
|||||||
display: "Oracle",
|
display: "Oracle",
|
||||||
descKey: "omo.agentDesc.oracle",
|
descKey: "omo.agentDesc.oracle",
|
||||||
tooltipKey: "omo.agentTooltip.oracle",
|
tooltipKey: "omo.agentTooltip.oracle",
|
||||||
recommended: "gpt-5.3",
|
recommended: "gpt-5.2",
|
||||||
group: "sub",
|
group: "sub",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -87,7 +69,7 @@ export const OMO_BUILTIN_AGENTS: OmoAgentDef[] = [
|
|||||||
display: "Librarian",
|
display: "Librarian",
|
||||||
descKey: "omo.agentDesc.librarian",
|
descKey: "omo.agentDesc.librarian",
|
||||||
tooltipKey: "omo.agentTooltip.librarian",
|
tooltipKey: "omo.agentTooltip.librarian",
|
||||||
recommended: "glm-4.7",
|
recommended: "gemini-3-flash",
|
||||||
group: "sub",
|
group: "sub",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -103,7 +85,7 @@ export const OMO_BUILTIN_AGENTS: OmoAgentDef[] = [
|
|||||||
display: "Multimodal-Looker",
|
display: "Multimodal-Looker",
|
||||||
descKey: "omo.agentDesc.multimodalLooker",
|
descKey: "omo.agentDesc.multimodalLooker",
|
||||||
tooltipKey: "omo.agentTooltip.multimodalLooker",
|
tooltipKey: "omo.agentTooltip.multimodalLooker",
|
||||||
recommended: "gemini-3-flash",
|
recommended: "kimi-k2.5",
|
||||||
group: "sub",
|
group: "sub",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -119,7 +101,7 @@ export const OMO_BUILTIN_AGENTS: OmoAgentDef[] = [
|
|||||||
display: "Momus",
|
display: "Momus",
|
||||||
descKey: "omo.agentDesc.momus",
|
descKey: "omo.agentDesc.momus",
|
||||||
tooltipKey: "omo.agentTooltip.momus",
|
tooltipKey: "omo.agentTooltip.momus",
|
||||||
recommended: "gpt-5.3",
|
recommended: "gpt-5.2",
|
||||||
group: "sub",
|
group: "sub",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -144,7 +126,7 @@ export const OMO_BUILTIN_CATEGORIES: OmoCategoryDef[] = [
|
|||||||
display: "Ultrabrain",
|
display: "Ultrabrain",
|
||||||
descKey: "omo.categoryDesc.ultrabrain",
|
descKey: "omo.categoryDesc.ultrabrain",
|
||||||
tooltipKey: "omo.categoryTooltip.ultrabrain",
|
tooltipKey: "omo.categoryTooltip.ultrabrain",
|
||||||
recommended: "claude-opus-4-6",
|
recommended: "gpt-5.3-codex",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "deep",
|
key: "deep",
|
||||||
@@ -158,35 +140,35 @@ export const OMO_BUILTIN_CATEGORIES: OmoCategoryDef[] = [
|
|||||||
display: "Artistry",
|
display: "Artistry",
|
||||||
descKey: "omo.categoryDesc.artistry",
|
descKey: "omo.categoryDesc.artistry",
|
||||||
tooltipKey: "omo.categoryTooltip.artistry",
|
tooltipKey: "omo.categoryTooltip.artistry",
|
||||||
recommended: "claude-opus-4-6",
|
recommended: "gemini-3-pro",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "quick",
|
key: "quick",
|
||||||
display: "Quick",
|
display: "Quick",
|
||||||
descKey: "omo.categoryDesc.quick",
|
descKey: "omo.categoryDesc.quick",
|
||||||
tooltipKey: "omo.categoryTooltip.quick",
|
tooltipKey: "omo.categoryTooltip.quick",
|
||||||
recommended: "gemini-3-flash",
|
recommended: "claude-haiku-4-5",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "unspecified-low",
|
key: "unspecified-low",
|
||||||
display: "Unspecified Low",
|
display: "Unspecified Low",
|
||||||
descKey: "omo.categoryDesc.unspecifiedLow",
|
descKey: "omo.categoryDesc.unspecifiedLow",
|
||||||
tooltipKey: "omo.categoryTooltip.unspecifiedLow",
|
tooltipKey: "omo.categoryTooltip.unspecifiedLow",
|
||||||
recommended: "gemini-3-flash",
|
recommended: "claude-sonnet-4-6",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "unspecified-high",
|
key: "unspecified-high",
|
||||||
display: "Unspecified High",
|
display: "Unspecified High",
|
||||||
descKey: "omo.categoryDesc.unspecifiedHigh",
|
descKey: "omo.categoryDesc.unspecifiedHigh",
|
||||||
tooltipKey: "omo.categoryTooltip.unspecifiedHigh",
|
tooltipKey: "omo.categoryTooltip.unspecifiedHigh",
|
||||||
recommended: "gpt-5.3-codex",
|
recommended: "claude-opus-4-6",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "writing",
|
key: "writing",
|
||||||
display: "Writing",
|
display: "Writing",
|
||||||
descKey: "omo.categoryDesc.writing",
|
descKey: "omo.categoryDesc.writing",
|
||||||
tooltipKey: "omo.categoryTooltip.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",
|
display: "Orchestrator",
|
||||||
descKey: "omo.slimAgentDesc.orchestrator",
|
descKey: "omo.slimAgentDesc.orchestrator",
|
||||||
tooltipKey: "omo.slimAgentTooltip.orchestrator",
|
tooltipKey: "omo.slimAgentTooltip.orchestrator",
|
||||||
recommended: "kimi-for-coding/k2p5",
|
recommended: "claude-opus-4-6",
|
||||||
group: "main",
|
group: "main",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -345,7 +327,7 @@ export const OMO_SLIM_BUILTIN_AGENTS: OmoAgentDef[] = [
|
|||||||
display: "Oracle",
|
display: "Oracle",
|
||||||
descKey: "omo.slimAgentDesc.oracle",
|
descKey: "omo.slimAgentDesc.oracle",
|
||||||
tooltipKey: "omo.slimAgentTooltip.oracle",
|
tooltipKey: "omo.slimAgentTooltip.oracle",
|
||||||
recommended: "openai/gpt-5.2-codex",
|
recommended: "gpt-5.2",
|
||||||
group: "sub",
|
group: "sub",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -353,7 +335,7 @@ export const OMO_SLIM_BUILTIN_AGENTS: OmoAgentDef[] = [
|
|||||||
display: "Librarian",
|
display: "Librarian",
|
||||||
descKey: "omo.slimAgentDesc.librarian",
|
descKey: "omo.slimAgentDesc.librarian",
|
||||||
tooltipKey: "omo.slimAgentTooltip.librarian",
|
tooltipKey: "omo.slimAgentTooltip.librarian",
|
||||||
recommended: "openai/gpt-5.1-codex-mini",
|
recommended: "gemini-3-flash",
|
||||||
group: "sub",
|
group: "sub",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -361,7 +343,7 @@ export const OMO_SLIM_BUILTIN_AGENTS: OmoAgentDef[] = [
|
|||||||
display: "Explorer",
|
display: "Explorer",
|
||||||
descKey: "omo.slimAgentDesc.explorer",
|
descKey: "omo.slimAgentDesc.explorer",
|
||||||
tooltipKey: "omo.slimAgentTooltip.explorer",
|
tooltipKey: "omo.slimAgentTooltip.explorer",
|
||||||
recommended: "openai/gpt-5.1-codex-mini",
|
recommended: "grok-code-fast-1",
|
||||||
group: "sub",
|
group: "sub",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -369,7 +351,7 @@ export const OMO_SLIM_BUILTIN_AGENTS: OmoAgentDef[] = [
|
|||||||
display: "Designer",
|
display: "Designer",
|
||||||
descKey: "omo.slimAgentDesc.designer",
|
descKey: "omo.slimAgentDesc.designer",
|
||||||
tooltipKey: "omo.slimAgentTooltip.designer",
|
tooltipKey: "omo.slimAgentTooltip.designer",
|
||||||
recommended: "kimi-for-coding/k2p5",
|
recommended: "gemini-3-pro",
|
||||||
group: "sub",
|
group: "sub",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -377,7 +359,7 @@ export const OMO_SLIM_BUILTIN_AGENTS: OmoAgentDef[] = [
|
|||||||
display: "Fixer",
|
display: "Fixer",
|
||||||
descKey: "omo.slimAgentDesc.fixer",
|
descKey: "omo.slimAgentDesc.fixer",
|
||||||
tooltipKey: "omo.slimAgentTooltip.fixer",
|
tooltipKey: "omo.slimAgentTooltip.fixer",
|
||||||
recommended: "openai/gpt-5.1-codex-mini",
|
recommended: "gpt-5.3-codex",
|
||||||
group: "sub",
|
group: "sub",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -406,75 +388,6 @@ export const OMO_SLIM_DISABLEABLE_HOOKS = [
|
|||||||
export const OMO_SLIM_DEFAULT_SCHEMA_URL =
|
export const OMO_SLIM_DEFAULT_SCHEMA_URL =
|
||||||
"https://raw.githubusercontent.com/alvinunreal/oh-my-opencode-slim/master/assets/oh-my-opencode-slim.schema.json";
|
"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(
|
export function buildOmoProfilePreview(
|
||||||
agents: Record<string, Record<string, unknown>>,
|
agents: Record<string, Record<string, unknown>>,
|
||||||
categories: Record<string, Record<string, unknown>> | undefined,
|
categories: Record<string, Record<string, unknown>> | undefined,
|
||||||
|
|||||||
@@ -3,86 +3,6 @@
|
|||||||
import type { TemplateValueConfig } from "../config/claudeProviderPresets";
|
import type { TemplateValueConfig } from "../config/claudeProviderPresets";
|
||||||
import { normalizeQuotes } from "@/utils/textNormalization";
|
import { normalizeQuotes } from "@/utils/textNormalization";
|
||||||
|
|
||||||
const isPlainObject = (value: unknown): value is Record<string, any> => {
|
|
||||||
return Object.prototype.toString.call(value) === "[object Object]";
|
|
||||||
};
|
|
||||||
|
|
||||||
const deepMerge = (
|
|
||||||
target: Record<string, any>,
|
|
||||||
source: Record<string, any>,
|
|
||||||
): Record<string, any> => {
|
|
||||||
Object.entries(source).forEach(([key, value]) => {
|
|
||||||
if (isPlainObject(value)) {
|
|
||||||
if (!isPlainObject(target[key])) {
|
|
||||||
target[key] = {};
|
|
||||||
}
|
|
||||||
deepMerge(target[key], value);
|
|
||||||
} else {
|
|
||||||
// 直接覆盖非对象字段(数组/基础类型)
|
|
||||||
target[key] = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return target;
|
|
||||||
};
|
|
||||||
|
|
||||||
const deepRemove = (
|
|
||||||
target: Record<string, any>,
|
|
||||||
source: Record<string, any>,
|
|
||||||
) => {
|
|
||||||
Object.entries(source).forEach(([key, value]) => {
|
|
||||||
if (!(key in target)) return;
|
|
||||||
|
|
||||||
if (isPlainObject(value) && isPlainObject(target[key])) {
|
|
||||||
// 只移除完全匹配的嵌套属性
|
|
||||||
deepRemove(target[key], value);
|
|
||||||
if (Object.keys(target[key]).length === 0) {
|
|
||||||
delete target[key];
|
|
||||||
}
|
|
||||||
} else if (isSubset(target[key], value)) {
|
|
||||||
// 只有当值完全匹配时才删除
|
|
||||||
delete target[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const isSubset = (target: any, source: any): boolean => {
|
|
||||||
if (isPlainObject(source)) {
|
|
||||||
if (!isPlainObject(target)) return false;
|
|
||||||
return Object.entries(source).every(([key, value]) =>
|
|
||||||
isSubset(target[key], value),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(source)) {
|
|
||||||
if (!Array.isArray(target) || target.length !== source.length) return false;
|
|
||||||
return source.every((item, index) => isSubset(target[index], item));
|
|
||||||
}
|
|
||||||
|
|
||||||
return target === source;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 深拷贝函数
|
|
||||||
const deepClone = <T>(obj: T): T => {
|
|
||||||
if (obj === null || typeof obj !== "object") return obj;
|
|
||||||
if (obj instanceof Date) return new Date(obj.getTime()) as T;
|
|
||||||
if (obj instanceof Array) return obj.map((item) => deepClone(item)) as T;
|
|
||||||
if (obj instanceof Object) {
|
|
||||||
const clonedObj = {} as T;
|
|
||||||
for (const key in obj) {
|
|
||||||
if (obj.hasOwnProperty(key)) {
|
|
||||||
clonedObj[key] = deepClone(obj[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return clonedObj;
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface UpdateCommonConfigResult {
|
|
||||||
updatedConfig: string;
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证JSON配置格式
|
// 验证JSON配置格式
|
||||||
export const validateJsonConfig = (
|
export const validateJsonConfig = (
|
||||||
value: string,
|
value: string,
|
||||||
@@ -102,69 +22,6 @@ export const validateJsonConfig = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 将通用配置片段写入/移除 settingsConfig
|
|
||||||
export const updateCommonConfigSnippet = (
|
|
||||||
jsonString: string,
|
|
||||||
snippetString: string,
|
|
||||||
enabled: boolean,
|
|
||||||
): UpdateCommonConfigResult => {
|
|
||||||
let config: Record<string, any>;
|
|
||||||
try {
|
|
||||||
config = jsonString ? JSON.parse(jsonString) : {};
|
|
||||||
} catch (err) {
|
|
||||||
return {
|
|
||||||
updatedConfig: jsonString,
|
|
||||||
error: "配置 JSON 解析失败,无法写入通用配置",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!snippetString.trim()) {
|
|
||||||
return {
|
|
||||||
updatedConfig: JSON.stringify(config, null, 2),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用统一的验证函数
|
|
||||||
const snippetError = validateJsonConfig(snippetString, "通用配置片段");
|
|
||||||
if (snippetError) {
|
|
||||||
return {
|
|
||||||
updatedConfig: JSON.stringify(config, null, 2),
|
|
||||||
error: snippetError,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const snippet = JSON.parse(snippetString) as Record<string, any>;
|
|
||||||
|
|
||||||
if (enabled) {
|
|
||||||
const merged = deepMerge(deepClone(config), snippet);
|
|
||||||
return {
|
|
||||||
updatedConfig: JSON.stringify(merged, null, 2),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const cloned = deepClone(config);
|
|
||||||
deepRemove(cloned, snippet);
|
|
||||||
return {
|
|
||||||
updatedConfig: JSON.stringify(cloned, null, 2),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// 检查当前配置是否已包含通用配置片段
|
|
||||||
export const hasCommonConfigSnippet = (
|
|
||||||
jsonString: string,
|
|
||||||
snippetString: string,
|
|
||||||
): boolean => {
|
|
||||||
try {
|
|
||||||
if (!snippetString.trim()) return false;
|
|
||||||
const config = jsonString ? JSON.parse(jsonString) : {};
|
|
||||||
const snippet = JSON.parse(snippetString);
|
|
||||||
if (!isPlainObject(snippet)) return false;
|
|
||||||
return isSubset(config, snippet);
|
|
||||||
} catch (err) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 读取配置中的 API Key(支持 Claude, Codex, Gemini)
|
// 读取配置中的 API Key(支持 Claude, Codex, Gemini)
|
||||||
export const getApiKeyFromConfig = (
|
export const getApiKeyFromConfig = (
|
||||||
jsonString: string,
|
jsonString: string,
|
||||||
@@ -172,6 +29,16 @@ export const getApiKeyFromConfig = (
|
|||||||
): string => {
|
): string => {
|
||||||
try {
|
try {
|
||||||
const config = JSON.parse(jsonString);
|
const config = JSON.parse(jsonString);
|
||||||
|
|
||||||
|
// 优先检查顶层 apiKey 字段(用于 Bedrock API Key 等预设)
|
||||||
|
if (
|
||||||
|
typeof config?.apiKey === "string" &&
|
||||||
|
config.apiKey &&
|
||||||
|
!config.apiKey.includes("${")
|
||||||
|
) {
|
||||||
|
return config.apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
const env = config?.env;
|
const env = config?.env;
|
||||||
|
|
||||||
if (!env) return "";
|
if (!env) return "";
|
||||||
@@ -255,6 +122,12 @@ export const hasApiKeyField = (
|
|||||||
): boolean => {
|
): boolean => {
|
||||||
try {
|
try {
|
||||||
const config = JSON.parse(jsonString);
|
const config = JSON.parse(jsonString);
|
||||||
|
|
||||||
|
// 检查顶层 apiKey 字段(用于 Bedrock API Key 等预设)
|
||||||
|
if (Object.prototype.hasOwnProperty.call(config, "apiKey")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const env = config?.env ?? {};
|
const env = config?.env ?? {};
|
||||||
|
|
||||||
if (appType === "gemini") {
|
if (appType === "gemini") {
|
||||||
@@ -278,11 +151,22 @@ export const hasApiKeyField = (
|
|||||||
export const setApiKeyInConfig = (
|
export const setApiKeyInConfig = (
|
||||||
jsonString: string,
|
jsonString: string,
|
||||||
apiKey: string,
|
apiKey: string,
|
||||||
options: { createIfMissing?: boolean; appType?: string } = {},
|
options: {
|
||||||
|
createIfMissing?: boolean;
|
||||||
|
appType?: string;
|
||||||
|
apiKeyField?: string;
|
||||||
|
} = {},
|
||||||
): string => {
|
): string => {
|
||||||
const { createIfMissing = false, appType } = options;
|
const { createIfMissing = false, appType, apiKeyField } = options;
|
||||||
try {
|
try {
|
||||||
const config = JSON.parse(jsonString);
|
const config = JSON.parse(jsonString);
|
||||||
|
|
||||||
|
// 优先检查顶层 apiKey 字段(用于 Bedrock API Key 等预设)
|
||||||
|
if (Object.prototype.hasOwnProperty.call(config, "apiKey")) {
|
||||||
|
config.apiKey = apiKey;
|
||||||
|
return JSON.stringify(config, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
if (!config.env) {
|
if (!config.env) {
|
||||||
if (!createIfMissing) return jsonString;
|
if (!createIfMissing) return jsonString;
|
||||||
config.env = {};
|
config.env = {};
|
||||||
@@ -313,13 +197,13 @@ export const setApiKeyInConfig = (
|
|||||||
return JSON.stringify(config, null, 2);
|
return JSON.stringify(config, null, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Claude API Key (优先写入已存在的字段;若两者均不存在且允许创建,则默认创建 AUTH_TOKEN 字段)
|
// Claude API Key (优先写入已存在的字段;若两者均不存在且允许创建,则使用 apiKeyField 指定的字段名)
|
||||||
if ("ANTHROPIC_AUTH_TOKEN" in env) {
|
if ("ANTHROPIC_AUTH_TOKEN" in env) {
|
||||||
env.ANTHROPIC_AUTH_TOKEN = apiKey;
|
env.ANTHROPIC_AUTH_TOKEN = apiKey;
|
||||||
} else if ("ANTHROPIC_API_KEY" in env) {
|
} else if ("ANTHROPIC_API_KEY" in env) {
|
||||||
env.ANTHROPIC_API_KEY = apiKey;
|
env.ANTHROPIC_API_KEY = apiKey;
|
||||||
} else if (createIfMissing) {
|
} else if (createIfMissing) {
|
||||||
env.ANTHROPIC_AUTH_TOKEN = apiKey;
|
env[apiKeyField ?? "ANTHROPIC_AUTH_TOKEN"] = apiKey;
|
||||||
} else {
|
} else {
|
||||||
return jsonString;
|
return jsonString;
|
||||||
}
|
}
|
||||||
@@ -329,85 +213,6 @@ export const setApiKeyInConfig = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ========== TOML Config Utilities ==========
|
|
||||||
|
|
||||||
export interface UpdateTomlCommonConfigResult {
|
|
||||||
updatedConfig: string;
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存之前的通用配置片段,用于替换操作
|
|
||||||
let previousCommonSnippet = "";
|
|
||||||
|
|
||||||
// 将通用配置片段写入/移除 TOML 配置
|
|
||||||
export const updateTomlCommonConfigSnippet = (
|
|
||||||
tomlString: string,
|
|
||||||
snippetString: string,
|
|
||||||
enabled: boolean,
|
|
||||||
): UpdateTomlCommonConfigResult => {
|
|
||||||
if (!snippetString.trim()) {
|
|
||||||
// 如果片段为空,直接返回原始配置
|
|
||||||
return {
|
|
||||||
updatedConfig: tomlString,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enabled) {
|
|
||||||
// 添加通用配置
|
|
||||||
// 先移除旧的通用配置(如果有)
|
|
||||||
let updatedConfig = tomlString;
|
|
||||||
if (previousCommonSnippet && tomlString.includes(previousCommonSnippet)) {
|
|
||||||
updatedConfig = tomlString.replace(previousCommonSnippet, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 在文件末尾添加新的通用配置
|
|
||||||
// 确保有适当的换行
|
|
||||||
const needsNewline = updatedConfig && !updatedConfig.endsWith("\n");
|
|
||||||
updatedConfig =
|
|
||||||
updatedConfig + (needsNewline ? "\n\n" : "\n") + snippetString;
|
|
||||||
|
|
||||||
// 保存当前通用配置片段
|
|
||||||
previousCommonSnippet = snippetString;
|
|
||||||
|
|
||||||
return {
|
|
||||||
updatedConfig: updatedConfig.trim() + "\n",
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// 移除通用配置
|
|
||||||
if (tomlString.includes(snippetString)) {
|
|
||||||
const updatedConfig = tomlString.replace(snippetString, "");
|
|
||||||
// 清理多余的空行
|
|
||||||
const cleaned = updatedConfig.replace(/\n{3,}/g, "\n\n").trim();
|
|
||||||
|
|
||||||
// 清空保存的状态
|
|
||||||
previousCommonSnippet = "";
|
|
||||||
|
|
||||||
return {
|
|
||||||
updatedConfig: cleaned ? cleaned + "\n" : "",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
updatedConfig: tomlString,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 检查 TOML 配置是否已包含通用配置片段
|
|
||||||
export const hasTomlCommonConfigSnippet = (
|
|
||||||
tomlString: string,
|
|
||||||
snippetString: string,
|
|
||||||
): boolean => {
|
|
||||||
if (!snippetString.trim()) return false;
|
|
||||||
|
|
||||||
// 简单检查配置是否包含片段内容
|
|
||||||
// 去除空白字符后比较,避免格式差异影响
|
|
||||||
const normalizeWhitespace = (str: string) => str.replace(/\s+/g, " ").trim();
|
|
||||||
|
|
||||||
return normalizeWhitespace(tomlString).includes(
|
|
||||||
normalizeWhitespace(snippetString),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ========== Codex base_url utils ==========
|
// ========== Codex base_url utils ==========
|
||||||
|
|
||||||
// 从 Codex 的 TOML 配置文本中提取 base_url(支持单/双引号)
|
// 从 Codex 的 TOML 配置文本中提取 base_url(支持单/双引号)
|
||||||
|
|||||||
@@ -339,8 +339,9 @@ describe("SettingsPage Component", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
fireEvent.click(screen.getByText("settings.tabAdvanced"));
|
fireEvent.click(screen.getByText("settings.tabAdvanced"));
|
||||||
fireEvent.click(screen.getByText("settings.advanced.data.title"));
|
fireEvent.click(screen.getByText("settings.advanced.cloudSync.title"));
|
||||||
expect(screen.getByText("webdav-sync-section:none")).toBeInTheDocument();
|
expect(screen.getByText("webdav-sync-section:none")).toBeInTheDocument();
|
||||||
|
fireEvent.click(screen.getByText("settings.advanced.data.title"));
|
||||||
|
|
||||||
// 有文件时,点击导入按钮执行 importConfig
|
// 有文件时,点击导入按钮执行 importConfig
|
||||||
fireEvent.click(
|
fireEvent.click(
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { providerPresets } from "@/config/claudeProviderPresets";
|
||||||
|
|
||||||
|
describe("AWS Bedrock Provider Presets", () => {
|
||||||
|
const bedrockAksk = providerPresets.find(
|
||||||
|
(p) => p.name === "AWS Bedrock (AKSK)",
|
||||||
|
);
|
||||||
|
|
||||||
|
it("should include AWS Bedrock (AKSK) preset", () => {
|
||||||
|
expect(bedrockAksk).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("AKSK preset should have required AWS env variables", () => {
|
||||||
|
const env = (bedrockAksk!.settingsConfig as any).env;
|
||||||
|
expect(env).toHaveProperty("AWS_ACCESS_KEY_ID");
|
||||||
|
expect(env).toHaveProperty("AWS_SECRET_ACCESS_KEY");
|
||||||
|
expect(env).toHaveProperty("AWS_REGION");
|
||||||
|
expect(env).toHaveProperty("CLAUDE_CODE_USE_BEDROCK", "1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("AKSK preset should have template values for AWS credentials", () => {
|
||||||
|
expect(bedrockAksk!.templateValues).toBeDefined();
|
||||||
|
expect(bedrockAksk!.templateValues!.AWS_ACCESS_KEY_ID).toBeDefined();
|
||||||
|
expect(bedrockAksk!.templateValues!.AWS_SECRET_ACCESS_KEY).toBeDefined();
|
||||||
|
expect(bedrockAksk!.templateValues!.AWS_REGION).toBeDefined();
|
||||||
|
expect(bedrockAksk!.templateValues!.AWS_REGION.editorValue).toBe(
|
||||||
|
"us-west-2",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("AKSK preset should have correct base URL template", () => {
|
||||||
|
const env = (bedrockAksk!.settingsConfig as any).env;
|
||||||
|
expect(env.ANTHROPIC_BASE_URL).toContain("bedrock-runtime");
|
||||||
|
expect(env.ANTHROPIC_BASE_URL).toContain("${AWS_REGION}");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("AKSK preset should have cloud_provider category", () => {
|
||||||
|
expect(bedrockAksk!.category).toBe("cloud_provider");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("AKSK preset should have Bedrock model as default", () => {
|
||||||
|
const env = (bedrockAksk!.settingsConfig as any).env;
|
||||||
|
expect(env.ANTHROPIC_MODEL).toContain("anthropic.claude");
|
||||||
|
});
|
||||||
|
|
||||||
|
const bedrockApiKey = providerPresets.find(
|
||||||
|
(p) => p.name === "AWS Bedrock (API Key)",
|
||||||
|
);
|
||||||
|
|
||||||
|
it("should include AWS Bedrock (API Key) preset", () => {
|
||||||
|
expect(bedrockApiKey).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("API Key preset should have apiKey field and AWS env variables", () => {
|
||||||
|
const config = bedrockApiKey!.settingsConfig as any;
|
||||||
|
expect(config).toHaveProperty("apiKey", "");
|
||||||
|
expect(config.env).toHaveProperty("AWS_REGION");
|
||||||
|
expect(config.env).toHaveProperty("CLAUDE_CODE_USE_BEDROCK", "1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("API Key preset should NOT have AKSK env variables", () => {
|
||||||
|
const env = (bedrockApiKey!.settingsConfig as any).env;
|
||||||
|
expect(env).not.toHaveProperty("AWS_ACCESS_KEY_ID");
|
||||||
|
expect(env).not.toHaveProperty("AWS_SECRET_ACCESS_KEY");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("API Key preset should have template values for region only", () => {
|
||||||
|
expect(bedrockApiKey!.templateValues).toBeDefined();
|
||||||
|
expect(bedrockApiKey!.templateValues!.AWS_REGION).toBeDefined();
|
||||||
|
expect(bedrockApiKey!.templateValues!.AWS_REGION.editorValue).toBe(
|
||||||
|
"us-west-2",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("API Key preset should have cloud_provider category", () => {
|
||||||
|
expect(bedrockApiKey!.category).toBe("cloud_provider");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
opencodeProviderPresets,
|
||||||
|
opencodeNpmPackages,
|
||||||
|
OPENCODE_PRESET_MODEL_VARIANTS,
|
||||||
|
} from "@/config/opencodeProviderPresets";
|
||||||
|
|
||||||
|
describe("AWS Bedrock OpenCode Provider Presets", () => {
|
||||||
|
it("should include @ai-sdk/amazon-bedrock in npm packages", () => {
|
||||||
|
const bedrockPkg = opencodeNpmPackages.find(
|
||||||
|
(p) => p.value === "@ai-sdk/amazon-bedrock",
|
||||||
|
);
|
||||||
|
expect(bedrockPkg).toBeDefined();
|
||||||
|
expect(bedrockPkg!.label).toBe("Amazon Bedrock");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should include Bedrock model variants", () => {
|
||||||
|
const variants = OPENCODE_PRESET_MODEL_VARIANTS["@ai-sdk/amazon-bedrock"];
|
||||||
|
expect(variants).toBeDefined();
|
||||||
|
expect(variants.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
const opusModel = variants.find((v) =>
|
||||||
|
v.id.includes("anthropic.claude-opus-4-6"),
|
||||||
|
);
|
||||||
|
expect(opusModel).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
const bedrockPreset = opencodeProviderPresets.find(
|
||||||
|
(p) => p.name === "AWS Bedrock",
|
||||||
|
);
|
||||||
|
|
||||||
|
it("should include AWS Bedrock preset", () => {
|
||||||
|
expect(bedrockPreset).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Bedrock preset should use @ai-sdk/amazon-bedrock npm package", () => {
|
||||||
|
expect(bedrockPreset!.settingsConfig.npm).toBe(
|
||||||
|
"@ai-sdk/amazon-bedrock",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Bedrock preset should have region in options", () => {
|
||||||
|
expect(bedrockPreset!.settingsConfig.options).toHaveProperty("region");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Bedrock preset should have cloud_provider category", () => {
|
||||||
|
expect(bedrockPreset!.category).toBe("cloud_provider");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Bedrock preset should have template values for AWS credentials", () => {
|
||||||
|
expect(bedrockPreset!.templateValues).toBeDefined();
|
||||||
|
expect(bedrockPreset!.templateValues!.region).toBeDefined();
|
||||||
|
expect(bedrockPreset!.templateValues!.region.editorValue).toBe(
|
||||||
|
"us-west-2",
|
||||||
|
);
|
||||||
|
expect(bedrockPreset!.templateValues!.accessKeyId).toBeDefined();
|
||||||
|
expect(bedrockPreset!.templateValues!.secretAccessKey).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Bedrock preset should include Claude models", () => {
|
||||||
|
const models = bedrockPreset!.settingsConfig.models;
|
||||||
|
expect(models).toBeDefined();
|
||||||
|
const modelIds = Object.keys(models!);
|
||||||
|
expect(
|
||||||
|
modelIds.some((id) => id.includes("anthropic.claude")),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user