diff --git a/package.json b/package.json
index 147441196..62c1c7177 100644
--- a/package.json
+++ b/package.json
@@ -54,6 +54,7 @@
"@lobehub/icons-static-svg": "^1.73.0",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-checkbox": "^1.3.3",
+ "@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.7",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d82cfdc38..6c791d5e8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -50,6 +50,9 @@ importers:
'@radix-ui/react-checkbox':
specifier: ^1.3.3
version: 1.3.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-collapsible':
+ specifier: ^1.1.12
+ version: 1.1.12(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-dialog':
specifier: ^1.1.15
version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index 49e1759f0..bd5fd8c38 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -714,6 +714,7 @@ dependencies = [
"futures",
"hyper",
"indexmap 2.11.4",
+ "json5",
"log",
"objc2 0.5.2",
"objc2-app-kit 0.2.2",
@@ -727,6 +728,7 @@ dependencies = [
"serde_json",
"serde_yaml",
"serial_test",
+ "sha2",
"tauri",
"tauri-build",
"tauri-plugin-deep-link",
@@ -2520,6 +2522,17 @@ dependencies = [
"thiserror 1.0.69",
]
+[[package]]
+name = "json5"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1"
+dependencies = [
+ "pest",
+ "pest_derive",
+ "serde",
+]
+
[[package]]
name = "jsonptr"
version = "0.6.3"
@@ -3404,6 +3417,49 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+[[package]]
+name = "pest"
+version = "2.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7"
+dependencies = [
+ "memchr",
+ "ucd-trie",
+]
+
+[[package]]
+name = "pest_derive"
+version = "2.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed"
+dependencies = [
+ "pest",
+ "pest_generator",
+]
+
+[[package]]
+name = "pest_generator"
+version = "2.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5"
+dependencies = [
+ "pest",
+ "pest_meta",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "pest_meta"
+version = "2.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365"
+dependencies = [
+ "pest",
+ "sha2",
+]
+
[[package]]
name = "phf"
version = "0.8.0"
@@ -5887,6 +5943,12 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
+[[package]]
+name = "ucd-trie"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
+
[[package]]
name = "uds_windows"
version = "1.1.0"
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 7346899a6..9547eb342 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -61,6 +61,8 @@ rusqlite = { version = "0.31", features = ["bundled", "backup"] }
indexmap = { version = "2", features = ["serde"] }
rust_decimal = "1.33"
uuid = { version = "1.11", features = ["v4"] }
+sha2 = "0.10"
+json5 = "0.4"
[target.'cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))'.dependencies]
tauri-plugin-single-instance = "2"
diff --git a/src-tauri/src/app_config.rs b/src-tauri/src/app_config.rs
index 41ade990f..18e3fbec8 100644
--- a/src-tauri/src/app_config.rs
+++ b/src-tauri/src/app_config.rs
@@ -25,6 +25,7 @@ impl McpApps {
AppType::Codex => self.codex,
AppType::Gemini => self.gemini,
AppType::OpenCode => self.opencode,
+ AppType::OpenClaw => false, // OpenClaw doesn't support MCP
}
}
@@ -35,6 +36,7 @@ impl McpApps {
AppType::Codex => self.codex = enabled,
AppType::Gemini => self.gemini = enabled,
AppType::OpenCode => self.opencode = enabled,
+ AppType::OpenClaw => {} // OpenClaw doesn't support MCP, ignore
}
}
@@ -83,6 +85,7 @@ impl SkillApps {
AppType::Codex => self.codex,
AppType::Gemini => self.gemini,
AppType::OpenCode => self.opencode,
+ AppType::OpenClaw => false, // OpenClaw doesn't support Skills
}
}
@@ -93,6 +96,7 @@ impl SkillApps {
AppType::Codex => self.codex = enabled,
AppType::Gemini => self.gemini = enabled,
AppType::OpenCode => self.opencode = enabled,
+ AppType::OpenClaw => {} // OpenClaw doesn't support Skills, ignore
}
}
@@ -238,6 +242,9 @@ pub struct McpRoot {
/// OpenCode MCP 配置(v4.0.0+,实际使用 opencode.json)
#[serde(default, skip_serializing_if = "McpConfig::is_empty")]
pub opencode: McpConfig,
+ /// OpenClaw MCP 配置(v4.1.0+,实际使用 openclaw.json)
+ #[serde(default, skip_serializing_if = "McpConfig::is_empty")]
+ pub openclaw: McpConfig,
}
impl Default for McpRoot {
@@ -250,6 +257,7 @@ impl Default for McpRoot {
codex: McpConfig::default(),
gemini: McpConfig::default(),
opencode: McpConfig::default(),
+ openclaw: McpConfig::default(),
}
}
}
@@ -272,6 +280,8 @@ pub struct PromptRoot {
pub gemini: PromptConfig,
#[serde(default)]
pub opencode: PromptConfig,
+ #[serde(default)]
+ pub openclaw: PromptConfig,
}
use crate::config::{copy_file, get_app_config_dir, get_app_config_path, write_json_file};
@@ -287,6 +297,7 @@ pub enum AppType {
Codex,
Gemini,
OpenCode,
+ OpenClaw,
}
impl AppType {
@@ -296,15 +307,16 @@ impl AppType {
AppType::Codex => "codex",
AppType::Gemini => "gemini",
AppType::OpenCode => "opencode",
+ AppType::OpenClaw => "openclaw",
}
}
/// Check if this app uses additive mode
///
/// - Switch mode (false): Only the current provider is written to live config (Claude, Codex, Gemini)
- /// - Additive mode (true): All providers are written to live config (OpenCode)
+ /// - Additive mode (true): All providers are written to live config (OpenCode, OpenClaw)
pub fn is_additive_mode(&self) -> bool {
- matches!(self, AppType::OpenCode)
+ matches!(self, AppType::OpenCode | AppType::OpenClaw)
}
/// Return an iterator over all app types
@@ -314,6 +326,7 @@ impl AppType {
AppType::Codex,
AppType::Gemini,
AppType::OpenCode,
+ AppType::OpenClaw,
]
.into_iter()
}
@@ -329,10 +342,11 @@ impl FromStr for AppType {
"codex" => Ok(AppType::Codex),
"gemini" => Ok(AppType::Gemini),
"opencode" => Ok(AppType::OpenCode),
+ "openclaw" => Ok(AppType::OpenClaw),
other => Err(AppError::localized(
"unsupported_app",
- format!("不支持的应用标识: '{other}'。可选值: claude, codex, gemini, opencode。"),
- format!("Unsupported app id: '{other}'. Allowed: claude, codex, gemini, opencode."),
+ format!("不支持的应用标识: '{other}'。可选值: claude, codex, gemini, opencode, openclaw。"),
+ format!("Unsupported app id: '{other}'. Allowed: claude, codex, gemini, opencode, openclaw."),
)),
}
}
@@ -352,6 +366,9 @@ pub struct CommonConfigSnippets {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub opencode: Option,
+
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub openclaw: Option,
}
impl CommonConfigSnippets {
@@ -362,6 +379,7 @@ impl CommonConfigSnippets {
AppType::Codex => self.codex.as_ref(),
AppType::Gemini => self.gemini.as_ref(),
AppType::OpenCode => self.opencode.as_ref(),
+ AppType::OpenClaw => self.openclaw.as_ref(),
}
}
@@ -372,6 +390,7 @@ impl CommonConfigSnippets {
AppType::Codex => self.codex = snippet,
AppType::Gemini => self.gemini = snippet,
AppType::OpenCode => self.opencode = snippet,
+ AppType::OpenClaw => self.openclaw = snippet,
}
}
}
@@ -412,6 +431,7 @@ impl Default for MultiAppConfig {
apps.insert("codex".to_string(), ProviderManager::default());
apps.insert("gemini".to_string(), ProviderManager::default());
apps.insert("opencode".to_string(), ProviderManager::default());
+ apps.insert("openclaw".to_string(), ProviderManager::default());
Self {
version: 2,
@@ -571,6 +591,7 @@ impl MultiAppConfig {
AppType::Codex => &self.mcp.codex,
AppType::Gemini => &self.mcp.gemini,
AppType::OpenCode => &self.mcp.opencode,
+ AppType::OpenClaw => &self.mcp.openclaw,
}
}
@@ -581,6 +602,7 @@ impl MultiAppConfig {
AppType::Codex => &mut self.mcp.codex,
AppType::Gemini => &mut self.mcp.gemini,
AppType::OpenCode => &mut self.mcp.opencode,
+ AppType::OpenClaw => &mut self.mcp.openclaw,
}
}
@@ -595,6 +617,7 @@ impl MultiAppConfig {
Self::auto_import_prompt_if_exists(&mut config, AppType::Codex)?;
Self::auto_import_prompt_if_exists(&mut config, AppType::Gemini)?;
Self::auto_import_prompt_if_exists(&mut config, AppType::OpenCode)?;
+ Self::auto_import_prompt_if_exists(&mut config, AppType::OpenClaw)?;
Ok(config)
}
@@ -615,6 +638,7 @@ impl MultiAppConfig {
|| !self.prompts.codex.prompts.is_empty()
|| !self.prompts.gemini.prompts.is_empty()
|| !self.prompts.opencode.prompts.is_empty()
+ || !self.prompts.openclaw.prompts.is_empty()
{
return Ok(false);
}
@@ -627,6 +651,7 @@ impl MultiAppConfig {
AppType::Codex,
AppType::Gemini,
AppType::OpenCode,
+ AppType::OpenClaw,
] {
// 复用已有的单应用导入逻辑
if Self::auto_import_prompt_if_exists(self, app)? {
@@ -697,6 +722,7 @@ impl MultiAppConfig {
AppType::Codex => &mut config.prompts.codex.prompts,
AppType::Gemini => &mut config.prompts.gemini.prompts,
AppType::OpenCode => &mut config.prompts.opencode.prompts,
+ AppType::OpenClaw => &mut config.prompts.openclaw.prompts,
};
prompts.insert(id, prompt);
@@ -725,12 +751,18 @@ impl MultiAppConfig {
let mut conflicts = Vec::new();
// 收集所有应用的 MCP
- for app in [AppType::Claude, AppType::Codex, AppType::Gemini] {
+ for app in [
+ AppType::Claude,
+ AppType::Codex,
+ AppType::Gemini,
+ AppType::OpenCode,
+ ] {
let old_servers = match app {
AppType::Claude => &self.mcp.claude.servers,
AppType::Codex => &self.mcp.codex.servers,
AppType::Gemini => &self.mcp.gemini.servers,
AppType::OpenCode => &self.mcp.opencode.servers,
+ AppType::OpenClaw => continue, // OpenClaw MCP is still in development, skip
};
for (id, entry) in old_servers {
diff --git a/src-tauri/src/commands/config.rs b/src-tauri/src/commands/config.rs
index c70939e04..87430b46f 100644
--- a/src-tauri/src/commands/config.rs
+++ b/src-tauri/src/commands/config.rs
@@ -59,6 +59,15 @@ pub async fn get_config_status(app: String) -> Result {
Ok(ConfigStatus { exists, path })
}
+ AppType::OpenClaw => {
+ let config_path = crate::openclaw_config::get_openclaw_config_path();
+ let exists = config_path.exists();
+ let path = crate::openclaw_config::get_openclaw_dir()
+ .to_string_lossy()
+ .to_string();
+
+ Ok(ConfigStatus { exists, path })
+ }
}
}
@@ -74,6 +83,7 @@ pub async fn get_config_dir(app: String) -> Result {
AppType::Codex => codex_config::get_codex_config_dir(),
AppType::Gemini => crate::gemini_config::get_gemini_dir(),
AppType::OpenCode => crate::opencode_config::get_opencode_dir(),
+ AppType::OpenClaw => crate::openclaw_config::get_openclaw_dir(),
};
Ok(dir.to_string_lossy().to_string())
@@ -86,6 +96,7 @@ pub async fn open_config_folder(handle: AppHandle, app: String) -> Result codex_config::get_codex_config_dir(),
AppType::Gemini => crate::gemini_config::get_gemini_dir(),
AppType::OpenCode => crate::opencode_config::get_opencode_dir(),
+ AppType::OpenClaw => crate::openclaw_config::get_openclaw_dir(),
};
if !config_dir.exists() {
diff --git a/src-tauri/src/commands/import_export.rs b/src-tauri/src/commands/import_export.rs
index 5e944bdc9..c4482629d 100644
--- a/src-tauri/src/commands/import_export.rs
+++ b/src-tauri/src/commands/import_export.rs
@@ -5,10 +5,15 @@ use std::path::PathBuf;
use tauri::State;
use tauri_plugin_dialog::DialogExt;
+use crate::commands::sync_support::{
+ post_sync_warning_from_result, run_post_import_sync, success_payload_with_warning,
+};
use crate::error::AppError;
use crate::services::provider::ProviderService;
use crate::store::AppState;
+// ─── File import/export ──────────────────────────────────────
+
/// 导出数据库为 SQL 备份
#[tauri::command]
pub async fn export_config_to_file(
@@ -37,27 +42,15 @@ pub async fn import_config_from_file(
state: State<'_, AppState>,
) -> Result {
let db = state.db.clone();
- let db_for_state = db.clone();
+ let db_for_sync = db.clone();
tauri::async_runtime::spawn_blocking(move || {
let path_buf = PathBuf::from(&filePath);
let backup_id = db.import_sql(&path_buf)?;
-
- // 导入后同步当前供应商到各自的 live 配置
- let app_state = AppState::new(db_for_state);
- if let Err(err) = ProviderService::sync_current_to_live(&app_state) {
- log::warn!("导入后同步 live 配置失败: {err}");
+ let warning = post_sync_warning_from_result(Ok(run_post_import_sync(db_for_sync)));
+ if let Some(msg) = warning.as_ref() {
+ log::warn!("[Import] post-import sync warning: {msg}");
}
-
- // 重新加载设置到内存缓存,确保导入的设置生效
- if let Err(err) = crate::settings::reload_settings() {
- log::warn!("导入后重载设置失败: {err}");
- }
-
- Ok::<_, AppError>(json!({
- "success": true,
- "message": "SQL imported successfully",
- "backupId": backup_id
- }))
+ Ok::<_, AppError>(success_payload_with_warning(backup_id, warning))
})
.await
.map_err(|e| format!("导入配置失败: {e}"))?
@@ -80,6 +73,8 @@ pub async fn sync_current_providers_live(state: State<'_, AppState>) -> Result(
diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs
index 7976c4316..5de019f96 100644
--- a/src-tauri/src/commands/mod.rs
+++ b/src-tauri/src/commands/mod.rs
@@ -9,6 +9,7 @@ mod import_export;
mod mcp;
mod misc;
mod omo;
+mod openclaw;
mod plugin;
mod prompt;
mod provider;
@@ -17,7 +18,10 @@ mod session_manager;
mod settings;
pub mod skill;
mod stream_check;
+mod sync_support;
mod usage;
+mod webdav_sync;
+mod workspace;
pub use config::*;
pub use deeplink::*;
@@ -28,6 +32,7 @@ pub use import_export::*;
pub use mcp::*;
pub use misc::*;
pub use omo::*;
+pub use openclaw::*;
pub use plugin::*;
pub use prompt::*;
pub use provider::*;
@@ -37,3 +42,5 @@ pub use settings::*;
pub use skill::*;
pub use stream_check::*;
pub use usage::*;
+pub use webdav_sync::*;
+pub use workspace::*;
diff --git a/src-tauri/src/commands/openclaw.rs b/src-tauri/src/commands/openclaw.rs
new file mode 100644
index 000000000..f9d0c9f7b
--- /dev/null
+++ b/src-tauri/src/commands/openclaw.rs
@@ -0,0 +1,108 @@
+use std::collections::HashMap;
+use tauri::State;
+
+use crate::openclaw_config;
+use crate::store::AppState;
+
+// ============================================================================
+// OpenClaw Provider Commands (migrated from provider.rs)
+// ============================================================================
+
+/// Import providers from OpenClaw live config to database.
+///
+/// OpenClaw uses additive mode — users may already have providers
+/// configured in openclaw.json.
+#[tauri::command]
+pub fn import_openclaw_providers_from_live(state: State<'_, AppState>) -> Result {
+ crate::services::provider::import_openclaw_providers_from_live(state.inner())
+ .map_err(|e| e.to_string())
+}
+
+/// Get provider IDs in the OpenClaw live config.
+#[tauri::command]
+pub fn get_openclaw_live_provider_ids() -> Result, String> {
+ openclaw_config::get_providers()
+ .map(|providers| providers.keys().cloned().collect())
+ .map_err(|e| e.to_string())
+}
+
+// ============================================================================
+// Agents Configuration Commands
+// ============================================================================
+
+/// Get OpenClaw default model config (agents.defaults.model)
+#[tauri::command]
+pub fn get_openclaw_default_model() -> Result
)}
+ ) : appId === "openclaw" ? (
+
+
+
+ setOpenclawProviderKey(
+ e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ""),
+ )
+ }
+ placeholder={t("openclaw.providerKeyPlaceholder")}
+ disabled={isEditMode}
+ className={
+ (existingOpenclawKeys.includes(openclawProviderKey) &&
+ !isEditMode) ||
+ (openclawProviderKey.trim() !== "" &&
+ !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(openclawProviderKey))
+ ? "border-destructive"
+ : ""
+ }
+ />
+ {existingOpenclawKeys.includes(openclawProviderKey) &&
+ !isEditMode && (
+
+ {t("openclaw.providerKeyDuplicate")}
+
+ )}
+ {openclawProviderKey.trim() !== "" &&
+ !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(openclawProviderKey) && (
+
+ {t("openclaw.providerKeyInvalid")}
+
+ )}
+ {!(
+ existingOpenclawKeys.includes(openclawProviderKey) &&
+ !isEditMode
+ ) &&
+ (openclawProviderKey.trim() === "" ||
+ /^[a-z0-9]+(-[a-z0-9]+)*$/.test(openclawProviderKey)) && (
+
+ {t("openclaw.providerKeyHint")}
+
+ )}
+
) : undefined
}
/>
@@ -1752,6 +2048,26 @@ export function ProviderForm({
/>
)}
+ {/* OpenClaw 专属字段 */}
+ {appId === "openclaw" && (
+
+ )}
+
+ {/* 配置编辑器:Codex、Claude、Gemini 分别使用不同的编辑器 */}
{appId === "codex" ? (
<>
{settingsConfigErrorField}
>
+ ) : appId === "openclaw" ? (
+ <>
+
+
+ form.setValue("settingsConfig", config)}
+ placeholder={`{
+ "baseUrl": "https://api.example.com/v1",
+ "apiKey": "your-api-key-here",
+ "api": "openai-completions",
+ "models": []
+}`}
+ rows={14}
+ showValidation={true}
+ language="json"
+ />
+
+ (
+
+
+
+ )}
+ />
+ >
) : (
<>
+
+
+
diff --git a/src/components/settings/WebdavSyncSection.tsx b/src/components/settings/WebdavSyncSection.tsx
new file mode 100644
index 000000000..1d4c48ab7
--- /dev/null
+++ b/src/components/settings/WebdavSyncSection.tsx
@@ -0,0 +1,775 @@
+import { useCallback, useEffect, useRef, useState } from "react";
+import type { ReactNode } from "react";
+import {
+ Link2,
+ UploadCloud,
+ DownloadCloud,
+ Loader2,
+ Save,
+ Check,
+ Info,
+ AlertTriangle,
+} from "lucide-react";
+import type { LucideIcon } from "lucide-react";
+import { useTranslation } from "react-i18next";
+import { useQueryClient } from "@tanstack/react-query";
+import { toast } from "sonner";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { settingsApi } from "@/lib/api";
+import type { RemoteSnapshotInfo, WebDavSyncSettings } from "@/types";
+
+// ─── WebDAV service presets ─────────────────────────────────
+
+interface WebDavPreset {
+ id: string;
+ label: string;
+ baseUrl: string;
+ hint: string;
+ matchPattern?: string; // substring match on URL
+}
+
+const WEBDAV_PRESETS: WebDavPreset[] = [
+ {
+ id: "jianguoyun",
+ label: "settings.webdavSync.presets.jianguoyun",
+ baseUrl: "https://dav.jianguoyun.com/dav/",
+ hint: "settings.webdavSync.presets.jianguoyunHint",
+ matchPattern: "jianguoyun.com",
+ },
+ {
+ id: "nextcloud",
+ label: "settings.webdavSync.presets.nextcloud",
+ baseUrl: "https://your-server/remote.php/dav/files/USERNAME/",
+ hint: "settings.webdavSync.presets.nextcloudHint",
+ matchPattern: "remote.php/dav",
+ },
+ {
+ id: "synology",
+ label: "settings.webdavSync.presets.synology",
+ baseUrl: "http://your-nas-ip:5005/",
+ hint: "settings.webdavSync.presets.synologyHint",
+ matchPattern: ":5005",
+ },
+ {
+ id: "custom",
+ label: "settings.webdavSync.presets.custom",
+ baseUrl: "",
+ hint: "",
+ },
+];
+
+/** Match a URL to one of the preset providers, or "custom". */
+function detectPreset(url: string): string {
+ if (!url) return "custom";
+ for (const preset of WEBDAV_PRESETS) {
+ if (preset.matchPattern && url.includes(preset.matchPattern)) {
+ return preset.id;
+ }
+ }
+ return "custom";
+}
+
+/** Format an RFC 3339 date string for display; falls back to raw string. */
+function formatDate(rfc3339: string): string {
+ const d = new Date(rfc3339);
+ return Number.isNaN(d.getTime()) ? rfc3339 : d.toLocaleString();
+}
+
+// ─── Types ──────────────────────────────────────────────────
+
+type ActionState =
+ | "idle"
+ | "testing"
+ | "saving"
+ | "uploading"
+ | "downloading"
+ | "fetching_remote";
+
+type DialogType = "upload" | "download" | null;
+
+interface WebdavSyncSectionProps {
+ config?: WebDavSyncSettings;
+}
+
+// ─── ActionButton ───────────────────────────────────────────
+
+/** Reusable button with loading spinner. */
+function ActionButton({
+ actionState,
+ targetState,
+ alsoActiveFor,
+ icon: Icon,
+ activeLabel,
+ idleLabel,
+ disabled,
+ ...props
+}: {
+ actionState: ActionState;
+ targetState: ActionState;
+ alsoActiveFor?: ActionState[];
+ icon: LucideIcon;
+ activeLabel: ReactNode;
+ idleLabel: ReactNode;
+} & Omit, "children">) {
+ const isActive =
+ actionState === targetState ||
+ (alsoActiveFor?.includes(actionState) ?? false);
+ return (
+
+ );
+}
+
+// ─── Main component ─────────────────────────────────────────
+
+export function WebdavSyncSection({ config }: WebdavSyncSectionProps) {
+ const { t } = useTranslation();
+ const queryClient = useQueryClient();
+ const [actionState, setActionState] = useState("idle");
+ const [dirty, setDirty] = useState(false);
+ const [passwordTouched, setPasswordTouched] = useState(false);
+ const [justSaved, setJustSaved] = useState(false);
+ const justSavedTimerRef = useRef | null>(null);
+
+ // Local form state — credentials are only persisted on explicit "Save".
+ const [form, setForm] = useState(() => ({
+ baseUrl: config?.baseUrl ?? "",
+ username: config?.username ?? "",
+ password: config?.password ?? "",
+ remoteRoot: config?.remoteRoot ?? "cc-switch-sync",
+ profile: config?.profile ?? "default",
+ }));
+
+ // Preset selector — derived from initial URL, updated on user selection
+ const [presetId, setPresetId] = useState(() =>
+ detectPreset(config?.baseUrl ?? ""),
+ );
+
+ const activePreset = WEBDAV_PRESETS.find((p) => p.id === presetId);
+
+ // Confirmation dialog state
+ const [dialogType, setDialogType] = useState(null);
+ const [remoteInfo, setRemoteInfo] = useState(null);
+
+ const closeDialog = useCallback(() => {
+ setDialogType(null);
+ setRemoteInfo(null);
+ }, []);
+
+ // Cleanup justSaved timer on unmount
+ useEffect(() => {
+ return () => {
+ if (justSavedTimerRef.current) clearTimeout(justSavedTimerRef.current);
+ };
+ }, []);
+
+ // Sync form when config is loaded/updated from backend, but not while user is editing
+ useEffect(() => {
+ if (!config || dirty) return;
+ setForm({
+ baseUrl: config.baseUrl ?? "",
+ username: config.username ?? "",
+ password: config.password ?? "",
+ remoteRoot: config.remoteRoot ?? "cc-switch-sync",
+ profile: config.profile ?? "default",
+ });
+ setPasswordTouched(false);
+ setPresetId(detectPreset(config.baseUrl ?? ""));
+ }, [config, dirty]);
+
+ const updateField = useCallback((field: keyof typeof form, value: string) => {
+ setForm((prev) => ({ ...prev, [field]: value }));
+ if (field === "password") {
+ setPasswordTouched(true);
+ }
+ setDirty(true);
+ setJustSaved(false);
+ if (justSavedTimerRef.current) {
+ clearTimeout(justSavedTimerRef.current);
+ justSavedTimerRef.current = null;
+ }
+ }, []);
+
+ const handlePresetChange = useCallback((id: string) => {
+ setPresetId(id);
+ const preset = WEBDAV_PRESETS.find((p) => p.id === id);
+ if (preset?.baseUrl) {
+ setForm((prev) => ({ ...prev, baseUrl: preset.baseUrl }));
+ setDirty(true);
+ setJustSaved(false);
+ if (justSavedTimerRef.current) {
+ clearTimeout(justSavedTimerRef.current);
+ justSavedTimerRef.current = null;
+ }
+ }
+ }, []);
+
+ // When user edits the URL, check if it still matches the current preset on blur
+ const handleBaseUrlBlur = useCallback(() => {
+ if (presetId === "custom") return;
+ const detected = detectPreset(form.baseUrl);
+ if (detected !== presetId) {
+ setPresetId("custom");
+ }
+ }, [form.baseUrl, presetId]);
+
+ const buildSettings = useCallback((): WebDavSyncSettings | null => {
+ const baseUrl = form.baseUrl.trim();
+ if (!baseUrl) return null;
+ return {
+ enabled: true,
+ baseUrl,
+ username: form.username.trim(),
+ password: form.password,
+ remoteRoot: form.remoteRoot.trim() || "cc-switch-sync",
+ profile: form.profile.trim() || "default",
+ };
+ }, [form]);
+
+ // ─── Handlers ───────────────────────────────────────────
+
+ const handleTest = useCallback(async () => {
+ const settings = buildSettings();
+ if (!settings) {
+ toast.error(t("settings.webdavSync.missingUrl"));
+ return;
+ }
+ setActionState("testing");
+ try {
+ await settingsApi.webdavTestConnection(settings, !passwordTouched);
+ toast.success(t("settings.webdavSync.testSuccess"));
+ } catch (error) {
+ toast.error(
+ t("settings.webdavSync.testFailed", {
+ error: (error as Error)?.message ?? String(error),
+ }),
+ );
+ } finally {
+ setActionState("idle");
+ }
+ }, [buildSettings, passwordTouched, t]);
+
+ const handleSave = useCallback(async () => {
+ const settings = buildSettings();
+ if (!settings) {
+ toast.error(t("settings.webdavSync.missingUrl"));
+ return;
+ }
+ setActionState("saving");
+ try {
+ await settingsApi.webdavSyncSaveSettings(settings, passwordTouched);
+ setDirty(false);
+ setPasswordTouched(false);
+ // Show "saved" indicator for 2 seconds
+ setJustSaved(true);
+ if (justSavedTimerRef.current) clearTimeout(justSavedTimerRef.current);
+ justSavedTimerRef.current = setTimeout(() => {
+ setJustSaved(false);
+ justSavedTimerRef.current = null;
+ }, 2000);
+ await queryClient.invalidateQueries();
+ } catch (error) {
+ toast.error(
+ t("settings.webdavSync.saveFailed", {
+ error: (error as Error)?.message ?? String(error),
+ }),
+ );
+ setActionState("idle");
+ return;
+ }
+
+ // Auto-test connection after save
+ setActionState("testing");
+ try {
+ await settingsApi.webdavTestConnection(settings, true);
+ toast.success(t("settings.webdavSync.saveAndTestSuccess"));
+ } catch (error) {
+ toast.warning(
+ t("settings.webdavSync.saveAndTestFailed", {
+ error: (error as Error)?.message ?? String(error),
+ }),
+ );
+ } finally {
+ setActionState("idle");
+ }
+ }, [buildSettings, passwordTouched, queryClient, t]);
+
+ /** Fetch remote info, then open upload confirmation dialog. */
+ const handleUploadClick = useCallback(async () => {
+ if (dirty) {
+ toast.error(t("settings.webdavSync.unsavedChanges"));
+ return;
+ }
+ setActionState("fetching_remote");
+ try {
+ const info = await settingsApi.webdavSyncFetchRemoteInfo();
+ if ("empty" in info) {
+ setRemoteInfo(null);
+ } else {
+ setRemoteInfo(info);
+ }
+ setDialogType("upload");
+ } catch {
+ setRemoteInfo(null);
+ toast.error(t("settings.webdavSync.fetchRemoteFailed"));
+ setActionState("idle");
+ return;
+ }
+ setActionState("idle");
+ }, [dirty, t]);
+
+ /** Actually perform the upload after user confirms. */
+ const handleUploadConfirm = useCallback(async () => {
+ if (dirty) {
+ toast.error(t("settings.webdavSync.unsavedChanges"));
+ return;
+ }
+ closeDialog();
+ setActionState("uploading");
+ try {
+ await settingsApi.webdavSyncUpload();
+ toast.success(t("settings.webdavSync.uploadSuccess"));
+ await queryClient.invalidateQueries();
+ } catch (error) {
+ toast.error(
+ t("settings.webdavSync.uploadFailed", {
+ error: (error as Error)?.message ?? String(error),
+ }),
+ );
+ } finally {
+ setActionState("idle");
+ }
+ }, [closeDialog, dirty, queryClient, t]);
+
+ /** Fetch remote info, then open download confirmation dialog. */
+ const handleDownloadClick = useCallback(async () => {
+ if (dirty) {
+ toast.error(t("settings.webdavSync.unsavedChanges"));
+ return;
+ }
+ setActionState("fetching_remote");
+ try {
+ const info = await settingsApi.webdavSyncFetchRemoteInfo();
+ if ("empty" in info) {
+ toast.info(t("settings.webdavSync.noRemoteData"));
+ return;
+ }
+ if (!info.compatible) {
+ toast.error(
+ t("settings.webdavSync.incompatibleVersion", {
+ version: info.version,
+ }),
+ );
+ return;
+ }
+ setRemoteInfo(info);
+ setDialogType("download");
+ } catch (error) {
+ toast.error(
+ t("settings.webdavSync.downloadFailed", {
+ error: (error as Error)?.message ?? String(error),
+ }),
+ );
+ } finally {
+ setActionState("idle");
+ }
+ }, [dirty, t]);
+
+ /** Actually perform the download after user confirms. */
+ const handleDownloadConfirm = useCallback(async () => {
+ if (dirty) {
+ toast.error(t("settings.webdavSync.unsavedChanges"));
+ return;
+ }
+ closeDialog();
+ setActionState("downloading");
+ try {
+ await settingsApi.webdavSyncDownload();
+ toast.success(t("settings.webdavSync.downloadSuccess"));
+ await queryClient.invalidateQueries();
+ } catch (error) {
+ toast.error(
+ t("settings.webdavSync.downloadFailed", {
+ error: (error as Error)?.message ?? String(error),
+ }),
+ );
+ } finally {
+ setActionState("idle");
+ }
+ }, [closeDialog, dirty, queryClient, t]);
+
+ // ─── Derived state ──────────────────────────────────────
+
+ const isLoading = actionState !== "idle";
+ const hasSavedConfig = Boolean(
+ config?.baseUrl?.trim() && config?.username?.trim(),
+ );
+
+ const lastSyncAt = config?.status?.lastSyncAt;
+ const lastSyncDisplay = lastSyncAt
+ ? new Date(lastSyncAt * 1000).toLocaleString()
+ : null;
+
+ // ─── Render ─────────────────────────────────────────────
+
+ return (
+
+
+
+
+ {/* Config fields */}
+
+ {/* Service preset selector */}
+
+
+
+
+
+ {/* Server URL */}
+
+
+ updateField("baseUrl", e.target.value)}
+ onBlur={handleBaseUrlBlur}
+ placeholder={t("settings.webdavSync.baseUrlPlaceholder")}
+ className="text-xs flex-1"
+ disabled={isLoading}
+ />
+
+
+ {/* Username */}
+
+
+ updateField("username", e.target.value)}
+ placeholder={t("settings.webdavSync.usernamePlaceholder")}
+ className="text-xs flex-1"
+ disabled={isLoading}
+ />
+
+
+ {/* Password */}
+
+
+ updateField("password", e.target.value)}
+ placeholder={t("settings.webdavSync.passwordPlaceholder")}
+ className="text-xs flex-1"
+ autoComplete="off"
+ disabled={isLoading}
+ />
+
+
+ {/* Preset hint */}
+ {activePreset?.hint && (
+
+
+ {t(activePreset.hint)}
+
+ )}
+
+ {/* Remote Root */}
+
+
+ updateField("remoteRoot", e.target.value)}
+ placeholder="cc-switch-sync"
+ className="text-xs flex-1"
+ disabled={isLoading}
+ />
+
+
+ {/* Profile */}
+
+
+ updateField("profile", e.target.value)}
+ placeholder="default"
+ className="text-xs flex-1"
+ disabled={isLoading}
+ />
+
+
+
+ {/* Last sync time */}
+ {lastSyncDisplay && (
+
+ {t("settings.webdavSync.lastSync", { time: lastSyncDisplay })}
+
+ )}
+
+ {/* Config buttons + save status */}
+
+
+
+
+ {/* Save status indicator */}
+ {dirty && (
+
+
+ {t("settings.webdavSync.unsaved")}
+
+ )}
+ {!dirty && justSaved && (
+
+
+ {t("settings.webdavSync.saved")}
+
+ )}
+
+
+ {/* Sync buttons */}
+
+ {!hasSavedConfig && (
+
+ {t("settings.webdavSync.saveBeforeSync")}
+
+ )}
+
+
+ {/* ─── Upload confirmation dialog ──────────────────── */}
+
+
+ {/* ─── Download confirmation dialog ────────────────── */}
+
+
+ );
+}
diff --git a/src/components/skills/UnifiedSkillsPanel.tsx b/src/components/skills/UnifiedSkillsPanel.tsx
index 7230401fd..17e23a86b 100644
--- a/src/components/skills/UnifiedSkillsPanel.tsx
+++ b/src/components/skills/UnifiedSkillsPanel.tsx
@@ -53,7 +53,7 @@ const UnifiedSkillsPanel = React.forwardRef<
const installFromZipMutation = useInstallSkillsFromZip();
const enabledCounts = useMemo(() => {
- const counts = { claude: 0, codex: 0, gemini: 0, opencode: 0 };
+ const counts = { claude: 0, codex: 0, gemini: 0, opencode: 0, openclaw: 0 };
if (!skills) return counts;
skills.forEach((skill) => {
for (const app of APP_IDS) {
diff --git a/src/components/ui/collapsible.tsx b/src/components/ui/collapsible.tsx
new file mode 100644
index 000000000..5c28cbcc3
--- /dev/null
+++ b/src/components/ui/collapsible.tsx
@@ -0,0 +1,9 @@
+import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
+
+const Collapsible = CollapsiblePrimitive.Root;
+
+const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
+
+const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
+
+export { Collapsible, CollapsibleTrigger, CollapsibleContent };
diff --git a/src/components/workspace/WorkspaceFileEditor.tsx b/src/components/workspace/WorkspaceFileEditor.tsx
new file mode 100644
index 000000000..b81fd0b4c
--- /dev/null
+++ b/src/components/workspace/WorkspaceFileEditor.tsx
@@ -0,0 +1,95 @@
+import React, { useState, useEffect, useCallback } from "react";
+import { useTranslation } from "react-i18next";
+import { toast } from "sonner";
+import { Button } from "@/components/ui/button";
+import MarkdownEditor from "@/components/MarkdownEditor";
+import { FullScreenPanel } from "@/components/common/FullScreenPanel";
+import { workspaceApi } from "@/lib/api/workspace";
+
+interface WorkspaceFileEditorProps {
+ filename: string;
+ isOpen: boolean;
+ onClose: () => void;
+}
+
+const WorkspaceFileEditor: React.FC = ({
+ filename,
+ isOpen,
+ onClose,
+}) => {
+ const { t } = useTranslation();
+ const [content, setContent] = useState("");
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] = useState(false);
+ 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();
+ }, []);
+
+ useEffect(() => {
+ if (!isOpen || !filename) return;
+
+ setLoading(true);
+ workspaceApi
+ .readFile(filename)
+ .then((data) => {
+ setContent(data ?? "");
+ })
+ .catch((err) => {
+ console.error("Failed to read workspace file:", err);
+ toast.error(t("workspace.loadFailed"));
+ })
+ .finally(() => setLoading(false));
+ }, [isOpen, filename, t]);
+
+ const handleSave = useCallback(async () => {
+ setSaving(true);
+ try {
+ await workspaceApi.writeFile(filename, content);
+ toast.success(t("workspace.saveSuccess"));
+ } catch (err) {
+ console.error("Failed to save workspace file:", err);
+ toast.error(t("workspace.saveFailed"));
+ } finally {
+ setSaving(false);
+ }
+ }, [filename, content, t]);
+
+ return (
+
+ {saving ? t("common.saving") : t("common.save")}
+
+ }
+ >
+ {loading ? (
+
+ {t("prompts.loading")}
+
+ ) : (
+
+ )}
+
+ );
+};
+
+export default WorkspaceFileEditor;
diff --git a/src/components/workspace/WorkspaceFilesPanel.tsx b/src/components/workspace/WorkspaceFilesPanel.tsx
new file mode 100644
index 000000000..2d25405af
--- /dev/null
+++ b/src/components/workspace/WorkspaceFilesPanel.tsx
@@ -0,0 +1,129 @@
+import React, { useState, useEffect } from "react";
+import { useTranslation } from "react-i18next";
+import {
+ FileCode,
+ Heart,
+ User,
+ IdCard,
+ Wrench,
+ Brain,
+ Activity,
+ Rocket,
+ Power,
+ CheckCircle2,
+ Circle,
+} from "lucide-react";
+import type { LucideIcon } from "lucide-react";
+import { workspaceApi } from "@/lib/api/workspace";
+import WorkspaceFileEditor from "./WorkspaceFileEditor";
+
+interface WorkspaceFile {
+ filename: string;
+ icon: LucideIcon;
+ descKey: string;
+}
+
+const WORKSPACE_FILES: WorkspaceFile[] = [
+ { filename: "AGENTS.md", icon: FileCode, descKey: "workspace.files.agents" },
+ { filename: "SOUL.md", icon: Heart, descKey: "workspace.files.soul" },
+ { filename: "USER.md", icon: User, descKey: "workspace.files.user" },
+ {
+ filename: "IDENTITY.md",
+ icon: IdCard,
+ descKey: "workspace.files.identity",
+ },
+ { filename: "TOOLS.md", icon: Wrench, descKey: "workspace.files.tools" },
+ { filename: "MEMORY.md", icon: Brain, descKey: "workspace.files.memory" },
+ {
+ filename: "HEARTBEAT.md",
+ icon: Activity,
+ descKey: "workspace.files.heartbeat",
+ },
+ {
+ filename: "BOOTSTRAP.md",
+ icon: Rocket,
+ descKey: "workspace.files.bootstrap",
+ },
+ { filename: "BOOT.md", icon: Power, descKey: "workspace.files.boot" },
+];
+
+const WorkspaceFilesPanel: React.FC = () => {
+ const { t } = useTranslation();
+ const [editingFile, setEditingFile] = useState(null);
+ const [fileExists, setFileExists] = useState>({});
+
+ const checkFileExistence = async () => {
+ const results: Record = {};
+ await Promise.all(
+ WORKSPACE_FILES.map(async (f) => {
+ try {
+ const content = await workspaceApi.readFile(f.filename);
+ results[f.filename] = content !== null;
+ } catch {
+ results[f.filename] = false;
+ }
+ }),
+ );
+ setFileExists(results);
+ };
+
+ useEffect(() => {
+ void checkFileExistence();
+ }, []);
+
+ const handleEditorClose = () => {
+ setEditingFile(null);
+ // Re-check file existence after closing editor (file may have been created)
+ void checkFileExistence();
+ };
+
+ return (
+
+
+ ~/.openclaw/workspace/
+
+
+
+ {WORKSPACE_FILES.map((file) => {
+ const Icon = file.icon;
+ const exists = fileExists[file.filename];
+
+ return (
+
+ );
+ })}
+
+
+
+
+ );
+};
+
+export default WorkspaceFilesPanel;
diff --git a/src/config/appConfig.tsx b/src/config/appConfig.tsx
index 6ab2b5046..95b7d2edb 100644
--- a/src/config/appConfig.tsx
+++ b/src/config/appConfig.tsx
@@ -1,6 +1,11 @@
import React from "react";
import type { AppId } from "@/lib/api/types";
-import { ClaudeIcon, CodexIcon, GeminiIcon } from "@/components/BrandIcons";
+import {
+ ClaudeIcon,
+ CodexIcon,
+ GeminiIcon,
+ OpenClawIcon,
+} from "@/components/BrandIcons";
import { ProviderIcon } from "@/components/ProviderIcon";
export interface AppConfig {
@@ -10,7 +15,13 @@ export interface AppConfig {
badgeClass: string;
}
-export const APP_IDS: AppId[] = ["claude", "codex", "gemini", "opencode"];
+export const APP_IDS: AppId[] = [
+ "claude",
+ "codex",
+ "gemini",
+ "opencode",
+ "openclaw",
+];
export const APP_ICON_MAP: Record = {
claude: {
@@ -52,4 +63,12 @@ export const APP_ICON_MAP: Record = {
badgeClass:
"bg-indigo-500/10 text-indigo-700 dark:text-indigo-300 hover:bg-indigo-500/20 border-0 gap-1.5",
},
+ openclaw: {
+ label: "OpenClaw",
+ icon: ,
+ activeClass:
+ "bg-rose-500/10 ring-1 ring-rose-500/20 hover:bg-rose-500/20 text-rose-600 dark:text-rose-400",
+ badgeClass:
+ "bg-rose-500/10 text-rose-700 dark:text-rose-300 hover:bg-rose-500/20 border-0 gap-1.5",
+ },
};
diff --git a/src/config/openclawProviderPresets.ts b/src/config/openclawProviderPresets.ts
new file mode 100644
index 000000000..6eb615797
--- /dev/null
+++ b/src/config/openclawProviderPresets.ts
@@ -0,0 +1,1084 @@
+/**
+ * OpenClaw provider presets configuration
+ * OpenClaw uses models.providers structure with custom provider configs
+ */
+import type {
+ ProviderCategory,
+ OpenClawProviderConfig,
+ OpenClawDefaultModel,
+} from "../types";
+import type { PresetTheme, TemplateValueConfig } from "./claudeProviderPresets";
+
+/** Suggested default model configuration for a preset */
+export interface OpenClawSuggestedDefaults {
+ /** Default model config to apply (agents.defaults.model) */
+ model?: OpenClawDefaultModel;
+ /** Model catalog entries to add (agents.defaults.models) */
+ modelCatalog?: Record;
+}
+
+export interface OpenClawProviderPreset {
+ name: string;
+ websiteUrl: string;
+ apiKeyUrl?: string;
+ /** OpenClaw settings_config structure */
+ settingsConfig: OpenClawProviderConfig;
+ isOfficial?: boolean;
+ isPartner?: boolean;
+ partnerPromotionKey?: string;
+ category?: ProviderCategory;
+ /** Template variable definitions */
+ templateValues?: Record;
+ /** Visual theme config */
+ theme?: PresetTheme;
+ /** Icon name */
+ icon?: string;
+ /** Icon color */
+ iconColor?: string;
+ /** Mark as custom template (for UI distinction) */
+ isCustomTemplate?: boolean;
+ /** Suggested default model configuration */
+ suggestedDefaults?: OpenClawSuggestedDefaults;
+}
+
+/**
+ * OpenClaw API protocol options
+ * @see https://github.com/openclaw/openclaw/blob/main/docs/gateway/configuration.md
+ */
+export const openclawApiProtocols = [
+ { value: "openai-completions", label: "OpenAI Completions" },
+ { value: "openai-responses", label: "OpenAI Responses" },
+ { value: "anthropic-messages", label: "Anthropic Messages" },
+ { value: "google-generative-ai", label: "Google Generative AI" },
+ { value: "bedrock-converse-stream", label: "AWS Bedrock" },
+] as const;
+
+/**
+ * OpenClaw provider presets list
+ */
+export const openclawProviderPresets: OpenClawProviderPreset[] = [
+ // ========== Chinese Officials ==========
+ {
+ name: "DeepSeek",
+ websiteUrl: "https://platform.deepseek.com",
+ apiKeyUrl: "https://platform.deepseek.com/api_keys",
+ settingsConfig: {
+ baseUrl: "https://api.deepseek.com/v1",
+ apiKey: "",
+ api: "openai-completions",
+ models: [
+ {
+ id: "deepseek-chat",
+ name: "DeepSeek V3.2",
+ contextWindow: 64000,
+ cost: { input: 0.0005, output: 0.002 },
+ },
+ {
+ id: "deepseek-reasoner",
+ name: "DeepSeek R1",
+ contextWindow: 64000,
+ cost: { input: 0.0005, output: 0.002 },
+ },
+ ],
+ },
+ category: "cn_official",
+ icon: "deepseek",
+ iconColor: "#1E88E5",
+ templateValues: {
+ apiKey: {
+ label: "API Key",
+ placeholder: "sk-...",
+ editorValue: "",
+ },
+ },
+ suggestedDefaults: {
+ model: {
+ primary: "deepseek/deepseek-chat",
+ fallbacks: ["deepseek/deepseek-reasoner"],
+ },
+ modelCatalog: {
+ "deepseek/deepseek-chat": { alias: "DeepSeek" },
+ "deepseek/deepseek-reasoner": { alias: "R1" },
+ },
+ },
+ },
+ {
+ name: "Zhipu GLM",
+ websiteUrl: "https://open.bigmodel.cn",
+ apiKeyUrl: "https://www.bigmodel.cn/claude-code?ic=RRVJPB5SII",
+ settingsConfig: {
+ baseUrl: "https://open.bigmodel.cn/api/paas/v4",
+ apiKey: "",
+ api: "openai-completions",
+ models: [
+ {
+ id: "glm-4.7",
+ name: "GLM-4.7",
+ contextWindow: 128000,
+ cost: { input: 0.001, output: 0.001 },
+ },
+ ],
+ },
+ category: "cn_official",
+ isPartner: true,
+ partnerPromotionKey: "zhipu",
+ icon: "zhipu",
+ iconColor: "#0F62FE",
+ templateValues: {
+ baseUrl: {
+ label: "Base URL",
+ placeholder: "https://open.bigmodel.cn/api/paas/v4",
+ defaultValue: "https://open.bigmodel.cn/api/paas/v4",
+ editorValue: "",
+ },
+ apiKey: {
+ label: "API Key",
+ placeholder: "",
+ editorValue: "",
+ },
+ },
+ suggestedDefaults: {
+ model: { primary: "zhipu/glm-4.7" },
+ modelCatalog: { "zhipu/glm-4.7": { alias: "GLM" } },
+ },
+ },
+ {
+ name: "Zhipu GLM en",
+ websiteUrl: "https://z.ai",
+ apiKeyUrl: "https://z.ai/subscribe?ic=8JVLJQFSKB",
+ settingsConfig: {
+ baseUrl: "https://api.z.ai/v1",
+ apiKey: "",
+ api: "openai-completions",
+ models: [
+ {
+ id: "glm-4.7",
+ name: "GLM-4.7",
+ contextWindow: 128000,
+ cost: { input: 0.001, output: 0.001 },
+ },
+ ],
+ },
+ category: "cn_official",
+ isPartner: true,
+ partnerPromotionKey: "zhipu",
+ icon: "zhipu",
+ iconColor: "#0F62FE",
+ templateValues: {
+ baseUrl: {
+ label: "Base URL",
+ placeholder: "https://api.z.ai/v1",
+ defaultValue: "https://api.z.ai/v1",
+ editorValue: "",
+ },
+ apiKey: {
+ label: "API Key",
+ placeholder: "",
+ editorValue: "",
+ },
+ },
+ suggestedDefaults: {
+ model: { primary: "zhipu-en/glm-4.7" },
+ modelCatalog: { "zhipu-en/glm-4.7": { alias: "GLM" } },
+ },
+ },
+ {
+ name: "Qwen Coder",
+ websiteUrl: "https://bailian.console.aliyun.com",
+ apiKeyUrl: "https://bailian.console.aliyun.com/#/api-key",
+ settingsConfig: {
+ baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1",
+ apiKey: "",
+ api: "openai-completions",
+ models: [
+ {
+ id: "qwen3-max",
+ name: "Qwen3 Max",
+ contextWindow: 32000,
+ cost: { input: 0.002, output: 0.006 },
+ },
+ ],
+ },
+ category: "cn_official",
+ icon: "qwen",
+ iconColor: "#FF6A00",
+ templateValues: {
+ baseUrl: {
+ label: "Base URL",
+ placeholder: "https://dashscope.aliyuncs.com/compatible-mode/v1",
+ defaultValue: "https://dashscope.aliyuncs.com/compatible-mode/v1",
+ editorValue: "",
+ },
+ apiKey: {
+ label: "API Key",
+ placeholder: "sk-...",
+ editorValue: "",
+ },
+ },
+ suggestedDefaults: {
+ model: { primary: "qwen/qwen3-max" },
+ modelCatalog: { "qwen/qwen3-max": { alias: "Qwen" } },
+ },
+ },
+ {
+ name: "Kimi k2.5",
+ websiteUrl: "https://platform.moonshot.cn/console",
+ apiKeyUrl: "https://platform.moonshot.cn/console/api-keys",
+ settingsConfig: {
+ baseUrl: "https://api.moonshot.cn/v1",
+ apiKey: "",
+ api: "openai-completions",
+ models: [
+ {
+ id: "kimi-k2.5",
+ name: "Kimi K2.5",
+ contextWindow: 131072,
+ cost: { input: 0.002, output: 0.006 },
+ },
+ ],
+ },
+ category: "cn_official",
+ icon: "kimi",
+ iconColor: "#6366F1",
+ templateValues: {
+ baseUrl: {
+ label: "Base URL",
+ placeholder: "https://api.moonshot.cn/v1",
+ defaultValue: "https://api.moonshot.cn/v1",
+ editorValue: "",
+ },
+ apiKey: {
+ label: "API Key",
+ placeholder: "sk-...",
+ editorValue: "",
+ },
+ },
+ suggestedDefaults: {
+ model: { primary: "kimi/kimi-k2.5" },
+ modelCatalog: { "kimi/kimi-k2.5": { alias: "Kimi" } },
+ },
+ },
+ {
+ name: "Kimi For Coding",
+ websiteUrl: "https://www.kimi.com/coding/docs/",
+ apiKeyUrl: "https://platform.moonshot.cn/console/api-keys",
+ settingsConfig: {
+ baseUrl: "https://api.kimi.com/v1",
+ apiKey: "",
+ api: "openai-completions",
+ models: [
+ {
+ id: "kimi-for-coding",
+ name: "Kimi For Coding",
+ contextWindow: 131072,
+ cost: { input: 0.002, output: 0.006 },
+ },
+ ],
+ },
+ category: "cn_official",
+ icon: "kimi",
+ iconColor: "#6366F1",
+ templateValues: {
+ baseUrl: {
+ label: "Base URL",
+ placeholder: "https://api.kimi.com/v1",
+ defaultValue: "https://api.kimi.com/v1",
+ editorValue: "",
+ },
+ apiKey: {
+ label: "API Key",
+ placeholder: "sk-...",
+ editorValue: "",
+ },
+ },
+ suggestedDefaults: {
+ model: { primary: "kimi-coding/kimi-for-coding" },
+ modelCatalog: { "kimi-coding/kimi-for-coding": { alias: "Kimi" } },
+ },
+ },
+ {
+ name: "MiniMax",
+ websiteUrl: "https://platform.minimaxi.com",
+ apiKeyUrl: "https://platform.minimaxi.com/subscribe/coding-plan",
+ settingsConfig: {
+ baseUrl: "https://api.minimaxi.com/v1",
+ apiKey: "",
+ api: "openai-completions",
+ models: [
+ {
+ id: "MiniMax-M2.1",
+ name: "MiniMax M2.1",
+ contextWindow: 200000,
+ cost: { input: 0.001, output: 0.004 },
+ },
+ ],
+ },
+ category: "cn_official",
+ isPartner: true,
+ partnerPromotionKey: "minimax_cn",
+ theme: {
+ backgroundColor: "#f64551",
+ textColor: "#FFFFFF",
+ },
+ icon: "minimax",
+ iconColor: "#FF6B6B",
+ templateValues: {
+ apiKey: {
+ label: "API Key",
+ placeholder: "",
+ editorValue: "",
+ },
+ },
+ suggestedDefaults: {
+ model: { primary: "minimax/MiniMax-M2.1" },
+ modelCatalog: { "minimax/MiniMax-M2.1": { alias: "MiniMax" } },
+ },
+ },
+ {
+ name: "MiniMax en",
+ websiteUrl: "https://platform.minimax.io",
+ apiKeyUrl: "https://platform.minimax.io/subscribe/coding-plan",
+ settingsConfig: {
+ baseUrl: "https://api.minimax.io/v1",
+ apiKey: "",
+ api: "openai-completions",
+ models: [
+ {
+ id: "MiniMax-M2.1",
+ name: "MiniMax M2.1",
+ contextWindow: 200000,
+ cost: { input: 0.001, output: 0.004 },
+ },
+ ],
+ },
+ category: "cn_official",
+ isPartner: true,
+ partnerPromotionKey: "minimax_en",
+ theme: {
+ backgroundColor: "#f64551",
+ textColor: "#FFFFFF",
+ },
+ icon: "minimax",
+ iconColor: "#FF6B6B",
+ templateValues: {
+ apiKey: {
+ label: "API Key",
+ placeholder: "",
+ editorValue: "",
+ },
+ },
+ suggestedDefaults: {
+ model: { primary: "minimax-en/MiniMax-M2.1" },
+ modelCatalog: { "minimax-en/MiniMax-M2.1": { alias: "MiniMax" } },
+ },
+ },
+ {
+ name: "KAT-Coder",
+ websiteUrl: "https://console.streamlake.ai",
+ apiKeyUrl: "https://console.streamlake.ai/console/api-key",
+ settingsConfig: {
+ baseUrl:
+ "https://vanchin.streamlake.ai/api/gateway/v1/endpoints/${ENDPOINT_ID}/openai",
+ apiKey: "",
+ api: "openai-completions",
+ models: [
+ {
+ id: "KAT-Coder-Pro",
+ name: "KAT-Coder Pro",
+ contextWindow: 128000,
+ cost: { input: 0.002, output: 0.006 },
+ },
+ ],
+ },
+ category: "cn_official",
+ icon: "catcoder",
+ templateValues: {
+ baseUrl: {
+ label: "Base URL",
+ placeholder:
+ "https://vanchin.streamlake.ai/api/gateway/v1/endpoints/${ENDPOINT_ID}/openai",
+ defaultValue:
+ "https://vanchin.streamlake.ai/api/gateway/v1/endpoints/${ENDPOINT_ID}/openai",
+ editorValue: "",
+ },
+ ENDPOINT_ID: {
+ label: "Endpoint ID",
+ placeholder: "",
+ editorValue: "",
+ },
+ apiKey: {
+ label: "API Key",
+ placeholder: "",
+ editorValue: "",
+ },
+ },
+ suggestedDefaults: {
+ model: { primary: "katcoder/KAT-Coder-Pro" },
+ modelCatalog: { "katcoder/KAT-Coder-Pro": { alias: "KAT-Coder" } },
+ },
+ },
+ {
+ name: "Longcat",
+ websiteUrl: "https://longcat.chat/platform",
+ apiKeyUrl: "https://longcat.chat/platform/api_keys",
+ settingsConfig: {
+ baseUrl: "https://api.longcat.chat/v1",
+ apiKey: "",
+ api: "openai-completions",
+ models: [
+ {
+ id: "LongCat-Flash-Chat",
+ name: "LongCat Flash Chat",
+ contextWindow: 128000,
+ cost: { input: 0.001, output: 0.004 },
+ },
+ ],
+ },
+ category: "cn_official",
+ icon: "longcat",
+ iconColor: "#29E154",
+ templateValues: {
+ baseUrl: {
+ label: "Base URL",
+ placeholder: "https://api.longcat.chat/v1",
+ defaultValue: "https://api.longcat.chat/v1",
+ editorValue: "",
+ },
+ apiKey: {
+ label: "API Key",
+ placeholder: "",
+ editorValue: "",
+ },
+ },
+ suggestedDefaults: {
+ model: { primary: "longcat/LongCat-Flash-Chat" },
+ modelCatalog: { "longcat/LongCat-Flash-Chat": { alias: "LongCat" } },
+ },
+ },
+ {
+ name: "DouBaoSeed",
+ websiteUrl: "https://www.volcengine.com/product/doubao",
+ apiKeyUrl: "https://www.volcengine.com/product/doubao",
+ settingsConfig: {
+ baseUrl: "https://ark.cn-beijing.volces.com/api/v3",
+ apiKey: "",
+ api: "openai-completions",
+ models: [
+ {
+ id: "doubao-seed-code-preview-latest",
+ name: "DouBao Seed Code Preview",
+ contextWindow: 128000,
+ cost: { input: 0.002, output: 0.006 },
+ },
+ ],
+ },
+ category: "cn_official",
+ icon: "doubao",
+ iconColor: "#3370FF",
+ templateValues: {
+ apiKey: {
+ label: "API Key",
+ placeholder: "",
+ editorValue: "",
+ },
+ },
+ suggestedDefaults: {
+ model: { primary: "doubaoseed/doubao-seed-code-preview-latest" },
+ modelCatalog: {
+ "doubaoseed/doubao-seed-code-preview-latest": { alias: "DouBao" },
+ },
+ },
+ },
+ {
+ name: "BaiLing",
+ websiteUrl: "https://alipaytbox.yuque.com/sxs0ba/ling/get_started",
+ settingsConfig: {
+ baseUrl: "https://api.tbox.cn/v1",
+ apiKey: "",
+ api: "openai-completions",
+ models: [
+ {
+ id: "Ling-1T",
+ name: "Ling 1T",
+ contextWindow: 128000,
+ cost: { input: 0.001, output: 0.004 },
+ },
+ ],
+ },
+ category: "cn_official",
+ templateValues: {
+ apiKey: {
+ label: "API Key",
+ placeholder: "",
+ editorValue: "",
+ },
+ },
+ suggestedDefaults: {
+ model: { primary: "bailing/Ling-1T" },
+ modelCatalog: { "bailing/Ling-1T": { alias: "BaiLing" } },
+ },
+ },
+ {
+ name: "Xiaomi MiMo",
+ websiteUrl: "https://platform.xiaomimimo.com",
+ apiKeyUrl: "https://platform.xiaomimimo.com/#/console/api-keys",
+ settingsConfig: {
+ baseUrl: "https://api.xiaomimimo.com/v1",
+ apiKey: "",
+ api: "openai-completions",
+ models: [
+ {
+ id: "mimo-v2-flash",
+ name: "MiMo V2 Flash",
+ contextWindow: 128000,
+ cost: { input: 0.001, output: 0.004 },
+ },
+ ],
+ },
+ category: "cn_official",
+ icon: "xiaomimimo",
+ iconColor: "#000000",
+ templateValues: {
+ apiKey: {
+ label: "API Key",
+ placeholder: "",
+ editorValue: "",
+ },
+ },
+ suggestedDefaults: {
+ model: { primary: "xiaomimimo/mimo-v2-flash" },
+ modelCatalog: { "xiaomimimo/mimo-v2-flash": { alias: "MiMo" } },
+ },
+ },
+
+ // ========== Aggregators ==========
+ {
+ name: "AiHubMix",
+ websiteUrl: "https://aihubmix.com",
+ apiKeyUrl: "https://aihubmix.com",
+ settingsConfig: {
+ baseUrl: "https://aihubmix.com",
+ apiKey: "",
+ api: "anthropic-messages",
+ models: [
+ {
+ id: "claude-sonnet-4-5-20250929",
+ name: "Claude Sonnet 4.5",
+ 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: "aggregator",
+ icon: "aihubmix",
+ iconColor: "#006FFB",
+ templateValues: {
+ apiKey: {
+ label: "API Key",
+ placeholder: "",
+ editorValue: "",
+ },
+ },
+ suggestedDefaults: {
+ model: {
+ primary: "aihubmix/claude-sonnet-4-5-20250929",
+ fallbacks: ["aihubmix/claude-opus-4-6"],
+ },
+ modelCatalog: {
+ "aihubmix/claude-sonnet-4-5-20250929": { alias: "Sonnet" },
+ "aihubmix/claude-opus-4-6": { alias: "Opus" },
+ },
+ },
+ },
+ {
+ name: "DMXAPI",
+ websiteUrl: "https://www.dmxapi.cn",
+ apiKeyUrl: "https://www.dmxapi.cn",
+ settingsConfig: {
+ baseUrl: "https://www.dmxapi.cn",
+ apiKey: "",
+ api: "anthropic-messages",
+ models: [
+ {
+ id: "claude-sonnet-4-5-20250929",
+ name: "Claude Sonnet 4.5",
+ 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: "aggregator",
+ isPartner: true,
+ partnerPromotionKey: "dmxapi",
+ templateValues: {
+ apiKey: {
+ label: "API Key",
+ placeholder: "",
+ editorValue: "",
+ },
+ },
+ suggestedDefaults: {
+ model: {
+ primary: "dmxapi/claude-sonnet-4-5-20250929",
+ fallbacks: ["dmxapi/claude-opus-4-6"],
+ },
+ modelCatalog: {
+ "dmxapi/claude-sonnet-4-5-20250929": { alias: "Sonnet" },
+ "dmxapi/claude-opus-4-6": { alias: "Opus" },
+ },
+ },
+ },
+ {
+ name: "OpenRouter",
+ websiteUrl: "https://openrouter.ai",
+ apiKeyUrl: "https://openrouter.ai/keys",
+ settingsConfig: {
+ baseUrl: "https://openrouter.ai/api/v1",
+ apiKey: "",
+ api: "openai-completions",
+ models: [
+ {
+ id: "anthropic/claude-sonnet-4.5",
+ name: "Claude Sonnet 4.5",
+ contextWindow: 200000,
+ cost: { input: 3, output: 15 },
+ },
+ {
+ id: "anthropic/claude-opus-4.6",
+ name: "Claude Opus 4.6",
+ contextWindow: 200000,
+ cost: { input: 5, output: 25 },
+ },
+ ],
+ },
+ category: "aggregator",
+ icon: "openrouter",
+ iconColor: "#6566F1",
+ templateValues: {
+ apiKey: {
+ label: "API Key",
+ placeholder: "sk-or-...",
+ editorValue: "",
+ },
+ },
+ suggestedDefaults: {
+ model: {
+ primary: "openrouter/anthropic/claude-sonnet-4.5",
+ fallbacks: ["openrouter/anthropic/claude-opus-4.6"],
+ },
+ modelCatalog: {
+ "openrouter/anthropic/claude-sonnet-4.5": { alias: "Sonnet" },
+ "openrouter/anthropic/claude-opus-4.6": { alias: "Opus" },
+ },
+ },
+ },
+ {
+ name: "ModelScope",
+ websiteUrl: "https://modelscope.cn",
+ apiKeyUrl: "https://modelscope.cn/my/myaccesstoken",
+ settingsConfig: {
+ baseUrl: "https://api-inference.modelscope.cn/v1",
+ apiKey: "",
+ api: "openai-completions",
+ models: [
+ {
+ id: "ZhipuAI/GLM-4.7",
+ name: "GLM-4.7",
+ contextWindow: 128000,
+ cost: { input: 0.001, output: 0.001 },
+ },
+ ],
+ },
+ category: "aggregator",
+ icon: "modelscope",
+ iconColor: "#624AFF",
+ templateValues: {
+ baseUrl: {
+ label: "Base URL",
+ placeholder: "https://api-inference.modelscope.cn/v1",
+ defaultValue: "https://api-inference.modelscope.cn/v1",
+ editorValue: "",
+ },
+ apiKey: {
+ label: "API Key",
+ placeholder: "",
+ editorValue: "",
+ },
+ },
+ suggestedDefaults: {
+ model: { primary: "modelscope/ZhipuAI/GLM-4.7" },
+ modelCatalog: { "modelscope/ZhipuAI/GLM-4.7": { alias: "GLM" } },
+ },
+ },
+ {
+ name: "SiliconFlow",
+ websiteUrl: "https://siliconflow.cn",
+ apiKeyUrl: "https://cloud.siliconflow.cn/me/account/ak",
+ settingsConfig: {
+ baseUrl: "https://api.siliconflow.cn/v1",
+ apiKey: "",
+ api: "openai-completions",
+ models: [
+ {
+ id: "Pro/MiniMaxAI/MiniMax-M2.1",
+ name: "MiniMax M2.1",
+ contextWindow: 200000,
+ cost: { input: 0.001, output: 0.004 },
+ },
+ ],
+ },
+ category: "aggregator",
+ icon: "siliconflow",
+ iconColor: "#6E29F6",
+ templateValues: {
+ apiKey: {
+ label: "API Key",
+ placeholder: "sk-...",
+ editorValue: "",
+ },
+ },
+ suggestedDefaults: {
+ model: { primary: "siliconflow/Pro/MiniMaxAI/MiniMax-M2.1" },
+ modelCatalog: {
+ "siliconflow/Pro/MiniMaxAI/MiniMax-M2.1": { alias: "MiniMax" },
+ },
+ },
+ },
+ {
+ name: "SiliconFlow en",
+ websiteUrl: "https://siliconflow.com",
+ apiKeyUrl: "https://cloud.siliconflow.com/account/ak",
+ settingsConfig: {
+ baseUrl: "https://api.siliconflow.com/v1",
+ apiKey: "",
+ api: "openai-completions",
+ models: [
+ {
+ id: "MiniMaxAI/MiniMax-M2.1",
+ name: "MiniMax M2.1",
+ contextWindow: 200000,
+ cost: { input: 0.001, output: 0.004 },
+ },
+ ],
+ },
+ category: "aggregator",
+ icon: "siliconflow",
+ iconColor: "#000000",
+ templateValues: {
+ apiKey: {
+ label: "API Key",
+ placeholder: "sk-...",
+ editorValue: "",
+ },
+ },
+ suggestedDefaults: {
+ model: { primary: "siliconflow-en/MiniMaxAI/MiniMax-M2.1" },
+ modelCatalog: {
+ "siliconflow-en/MiniMaxAI/MiniMax-M2.1": { alias: "MiniMax" },
+ },
+ },
+ },
+ {
+ name: "Nvidia",
+ websiteUrl: "https://build.nvidia.com",
+ apiKeyUrl: "https://build.nvidia.com/settings/api-keys",
+ settingsConfig: {
+ baseUrl: "https://integrate.api.nvidia.com/v1",
+ apiKey: "",
+ api: "openai-completions",
+ models: [
+ {
+ id: "moonshotai/kimi-k2.5",
+ name: "Kimi K2.5",
+ contextWindow: 131072,
+ cost: { input: 0.002, output: 0.006 },
+ },
+ ],
+ },
+ category: "aggregator",
+ icon: "nvidia",
+ iconColor: "#000000",
+ templateValues: {
+ apiKey: {
+ label: "API Key",
+ placeholder: "nvapi-...",
+ editorValue: "",
+ },
+ },
+ suggestedDefaults: {
+ model: { primary: "nvidia/moonshotai/kimi-k2.5" },
+ modelCatalog: { "nvidia/moonshotai/kimi-k2.5": { alias: "Kimi" } },
+ },
+ },
+
+ // ========== Third Party Partners ==========
+ {
+ name: "PackyCode",
+ websiteUrl: "https://www.packyapi.com",
+ apiKeyUrl: "https://www.packyapi.com/register?aff=cc-switch",
+ settingsConfig: {
+ baseUrl: "https://www.packyapi.com",
+ apiKey: "",
+ api: "anthropic-messages",
+ models: [
+ {
+ id: "claude-sonnet-4-5-20250929",
+ name: "Claude Sonnet 4.5",
+ 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: "packycode",
+ icon: "packycode",
+ templateValues: {
+ apiKey: {
+ label: "API Key",
+ placeholder: "",
+ editorValue: "",
+ },
+ },
+ suggestedDefaults: {
+ model: {
+ primary: "packycode/claude-sonnet-4-5-20250929",
+ fallbacks: ["packycode/claude-opus-4-6"],
+ },
+ modelCatalog: {
+ "packycode/claude-sonnet-4-5-20250929": { alias: "Sonnet" },
+ "packycode/claude-opus-4-6": { alias: "Opus" },
+ },
+ },
+ },
+ {
+ name: "Cubence",
+ websiteUrl: "https://cubence.com",
+ apiKeyUrl: "https://cubence.com/signup?code=CCSWITCH&source=ccs",
+ settingsConfig: {
+ baseUrl: "https://api.cubence.com",
+ apiKey: "",
+ api: "anthropic-messages",
+ models: [
+ {
+ id: "claude-sonnet-4-5-20250929",
+ name: "Claude Sonnet 4.5",
+ 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: "cubence",
+ icon: "cubence",
+ iconColor: "#000000",
+ templateValues: {
+ apiKey: {
+ label: "API Key",
+ placeholder: "",
+ editorValue: "",
+ },
+ },
+ suggestedDefaults: {
+ model: {
+ primary: "cubence/claude-sonnet-4-5-20250929",
+ fallbacks: ["cubence/claude-opus-4-6"],
+ },
+ modelCatalog: {
+ "cubence/claude-sonnet-4-5-20250929": { alias: "Sonnet" },
+ "cubence/claude-opus-4-6": { alias: "Opus" },
+ },
+ },
+ },
+ {
+ name: "AIGoCode",
+ websiteUrl: "https://aigocode.com",
+ apiKeyUrl: "https://aigocode.com/invite/CC-SWITCH",
+ settingsConfig: {
+ baseUrl: "https://api.aigocode.com",
+ apiKey: "",
+ api: "anthropic-messages",
+ models: [
+ {
+ id: "claude-sonnet-4-5-20250929",
+ name: "Claude Sonnet 4.5",
+ 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: "aigocode",
+ icon: "aigocode",
+ iconColor: "#5B7FFF",
+ templateValues: {
+ apiKey: {
+ label: "API Key",
+ placeholder: "",
+ editorValue: "",
+ },
+ },
+ suggestedDefaults: {
+ model: {
+ primary: "aigocode/claude-sonnet-4-5-20250929",
+ fallbacks: ["aigocode/claude-opus-4-6"],
+ },
+ modelCatalog: {
+ "aigocode/claude-sonnet-4-5-20250929": { alias: "Sonnet" },
+ "aigocode/claude-opus-4-6": { alias: "Opus" },
+ },
+ },
+ },
+ {
+ name: "RightCode",
+ websiteUrl: "https://www.right.codes",
+ apiKeyUrl: "https://www.right.codes/register?aff=CCSWITCH",
+ settingsConfig: {
+ baseUrl: "https://www.right.codes/claude",
+ apiKey: "",
+ api: "anthropic-messages",
+ models: [
+ {
+ id: "claude-sonnet-4-5-20250929",
+ name: "Claude Sonnet 4.5",
+ 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: "rightcode",
+ icon: "rc",
+ iconColor: "#E96B2C",
+ templateValues: {
+ apiKey: {
+ label: "API Key",
+ placeholder: "",
+ editorValue: "",
+ },
+ },
+ suggestedDefaults: {
+ model: {
+ primary: "rightcode/claude-sonnet-4-5-20250929",
+ fallbacks: ["rightcode/claude-opus-4-6"],
+ },
+ modelCatalog: {
+ "rightcode/claude-sonnet-4-5-20250929": { alias: "Sonnet" },
+ "rightcode/claude-opus-4-6": { alias: "Opus" },
+ },
+ },
+ },
+ {
+ name: "AICodeMirror",
+ websiteUrl: "https://www.aicodemirror.com",
+ apiKeyUrl: "https://www.aicodemirror.com/register?invitecode=9915W3",
+ settingsConfig: {
+ baseUrl: "https://api.aicodemirror.com/api/claudecode",
+ apiKey: "",
+ api: "anthropic-messages",
+ models: [
+ {
+ id: "claude-sonnet-4-5-20250929",
+ name: "Claude Sonnet 4.5",
+ 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: "aicodemirror",
+ icon: "aicodemirror",
+ iconColor: "#000000",
+ templateValues: {
+ apiKey: {
+ label: "API Key",
+ placeholder: "",
+ editorValue: "",
+ },
+ },
+ suggestedDefaults: {
+ model: {
+ primary: "aicodemirror/claude-sonnet-4-5-20250929",
+ fallbacks: ["aicodemirror/claude-opus-4-6"],
+ },
+ modelCatalog: {
+ "aicodemirror/claude-sonnet-4-5-20250929": { alias: "Sonnet" },
+ "aicodemirror/claude-opus-4-6": { alias: "Opus" },
+ },
+ },
+ },
+
+ // ========== Custom Template ==========
+ {
+ name: "OpenAI Compatible",
+ websiteUrl: "",
+ settingsConfig: {
+ baseUrl: "",
+ apiKey: "",
+ api: "openai-completions",
+ models: [],
+ },
+ category: "custom",
+ isCustomTemplate: true,
+ icon: "generic",
+ iconColor: "#6B7280",
+ templateValues: {
+ baseUrl: {
+ label: "Base URL",
+ placeholder: "https://api.example.com/v1",
+ editorValue: "",
+ },
+ apiKey: {
+ label: "API Key",
+ placeholder: "",
+ editorValue: "",
+ },
+ },
+ },
+];
diff --git a/src/config/opencodeProviderPresets.ts b/src/config/opencodeProviderPresets.ts
index c01c1448d..51bfa5503 100644
--- a/src/config/opencodeProviderPresets.ts
+++ b/src/config/opencodeProviderPresets.ts
@@ -844,7 +844,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
},
models: {
"claude-sonnet-4-5-20250929": { name: "Claude Sonnet 4.5" },
- "claude-opus-4-5-20251101": { name: "Claude Opus 4.5" },
+ "claude-opus-4-6": { name: "Claude Opus 4.6" },
},
},
category: "aggregator",
@@ -871,7 +871,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
},
models: {
"claude-sonnet-4-5-20250929": { name: "Claude Sonnet 4.5" },
- "claude-opus-4-5-20251101": { name: "Claude Opus 4.5" },
+ "claude-opus-4-6": { name: "Claude Opus 4.6" },
},
},
category: "aggregator",
@@ -898,7 +898,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
},
models: {
"anthropic/claude-sonnet-4.5": { name: "Claude Sonnet 4.5" },
- "anthropic/claude-opus-4.5": { name: "Claude Opus 4.5" },
+ "anthropic/claude-opus-4.6": { name: "Claude Opus 4.6" },
},
},
category: "aggregator",
@@ -952,7 +952,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
},
models: {
"claude-sonnet-4-5-20250929": { name: "Claude Sonnet 4.5" },
- "claude-opus-4-5-20251101": { name: "Claude Opus 4.5" },
+ "claude-opus-4-6": { name: "Claude Opus 4.6" },
},
},
category: "third_party",
@@ -980,7 +980,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
},
models: {
"claude-sonnet-4-5-20250929": { name: "Claude Sonnet 4.5" },
- "claude-opus-4-5-20251101": { name: "Claude Opus 4.5" },
+ "claude-opus-4-6": { name: "Claude Opus 4.6" },
},
},
category: "third_party",
@@ -1009,7 +1009,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
},
models: {
"claude-sonnet-4-5-20250929": { name: "Claude Sonnet 4.5" },
- "claude-opus-4-5-20251101": { name: "Claude Opus 4.5" },
+ "claude-opus-4-6": { name: "Claude Opus 4.6" },
},
},
category: "third_party",
@@ -1070,7 +1070,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
},
models: {
"claude-sonnet-4.5": { name: "Claude Sonnet 4.5" },
- "claude-opus-4.5": { name: "Claude Opus 4.5" },
+ "claude-opus-4.6": { name: "Claude Opus 4.6" },
},
},
category: "third_party",
diff --git a/src/hooks/useOpenClaw.ts b/src/hooks/useOpenClaw.ts
new file mode 100644
index 000000000..41d7b006b
--- /dev/null
+++ b/src/hooks/useOpenClaw.ts
@@ -0,0 +1,131 @@
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { openclawApi } from "@/lib/api/openclaw";
+import { providersApi } from "@/lib/api/providers";
+import type {
+ OpenClawEnvConfig,
+ OpenClawToolsConfig,
+ OpenClawAgentsDefaults,
+} from "@/types";
+
+/**
+ * Centralized query keys for all OpenClaw-related queries.
+ * Import this from any file that needs to invalidate OpenClaw caches.
+ */
+export const openclawKeys = {
+ all: ["openclaw"] as const,
+ liveProviderIds: ["openclaw", "liveProviderIds"] as const,
+ defaultModel: ["openclaw", "defaultModel"] as const,
+ env: ["openclaw", "env"] as const,
+ tools: ["openclaw", "tools"] as const,
+ agentsDefaults: ["openclaw", "agentsDefaults"] as const,
+};
+
+// ============================================================
+// Query hooks
+// ============================================================
+
+/**
+ * Query live provider IDs from openclaw.json config.
+ * Used by ProviderList to show "In Config" badge.
+ */
+export function useOpenClawLiveProviderIds(enabled: boolean) {
+ return useQuery({
+ queryKey: openclawKeys.liveProviderIds,
+ queryFn: () => providersApi.getOpenClawLiveProviderIds(),
+ enabled,
+ });
+}
+
+/**
+ * Query the default model from agents.defaults.model.
+ * Used by ProviderList to show which provider is the default.
+ */
+export function useOpenClawDefaultModel(enabled: boolean) {
+ return useQuery({
+ queryKey: openclawKeys.defaultModel,
+ queryFn: () => openclawApi.getDefaultModel(),
+ enabled,
+ });
+}
+
+/**
+ * Query env section of openclaw.json.
+ */
+export function useOpenClawEnv() {
+ return useQuery({
+ queryKey: openclawKeys.env,
+ queryFn: () => openclawApi.getEnv(),
+ staleTime: 30_000,
+ });
+}
+
+/**
+ * Query tools section of openclaw.json.
+ */
+export function useOpenClawTools() {
+ return useQuery({
+ queryKey: openclawKeys.tools,
+ queryFn: () => openclawApi.getTools(),
+ staleTime: 30_000,
+ });
+}
+
+/**
+ * Query agents.defaults section of openclaw.json.
+ */
+export function useOpenClawAgentsDefaults() {
+ return useQuery({
+ queryKey: openclawKeys.agentsDefaults,
+ queryFn: () => openclawApi.getAgentsDefaults(),
+ staleTime: 30_000,
+ });
+}
+
+// ============================================================
+// Mutation hooks
+// ============================================================
+
+/**
+ * Save env config. Invalidates env query on success.
+ * Toast notifications are handled by the component.
+ */
+export function useSaveOpenClawEnv() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (env: OpenClawEnvConfig) => openclawApi.setEnv(env),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: openclawKeys.env });
+ },
+ });
+}
+
+/**
+ * Save tools config. Invalidates tools query on success.
+ * Toast notifications are handled by the component.
+ */
+export function useSaveOpenClawTools() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (tools: OpenClawToolsConfig) => openclawApi.setTools(tools),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: openclawKeys.tools });
+ },
+ });
+}
+
+/**
+ * Save agents.defaults config. Invalidates both agentsDefaults and defaultModel
+ * queries on success (since changing agents.defaults may affect the default model).
+ * Toast notifications are handled by the component.
+ */
+export function useSaveOpenClawAgentsDefaults() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (defaults: OpenClawAgentsDefaults) =>
+ openclawApi.setAgentsDefaults(defaults),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: openclawKeys.agentsDefaults });
+ queryClient.invalidateQueries({ queryKey: openclawKeys.defaultModel });
+ },
+ });
+}
diff --git a/src/hooks/useProviderActions.ts b/src/hooks/useProviderActions.ts
index ede55f8a2..42a5c4120 100644
--- a/src/hooks/useProviderActions.ts
+++ b/src/hooks/useProviderActions.ts
@@ -2,8 +2,14 @@ import { useCallback } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { useTranslation } from "react-i18next";
-import { providersApi, settingsApi, type AppId } from "@/lib/api";
-import type { Provider, UsageScript } from "@/types";
+import { providersApi, settingsApi, openclawApi, type AppId } from "@/lib/api";
+import type {
+ Provider,
+ UsageScript,
+ OpenClawProviderConfig,
+ OpenClawDefaultModel,
+} from "@/types";
+import type { OpenClawSuggestedDefaults } from "@/config/openclawProviderPresets";
import {
useAddProviderMutation,
useUpdateProviderMutation,
@@ -11,6 +17,7 @@ import {
useSwitchProviderMutation,
} from "@/lib/query";
import { extractErrorMessage } from "@/utils/errorUtils";
+import { openclawKeys } from "@/hooks/useOpenClaw";
/**
* Hook for managing provider actions (add, update, delete, switch)
@@ -54,10 +61,55 @@ export function useProviderActions(activeApp: AppId) {
// 添加供应商
const addProvider = useCallback(
- async (provider: Omit & { providerKey?: string }) => {
+ async (
+ provider: Omit & {
+ providerKey?: string;
+ suggestedDefaults?: OpenClawSuggestedDefaults;
+ },
+ ) => {
await addProviderMutation.mutateAsync(provider);
+
+ // OpenClaw: register models to allowlist after adding provider
+ if (activeApp === "openclaw" && provider.suggestedDefaults) {
+ const { model, modelCatalog } = provider.suggestedDefaults;
+ let modelsRegistered = false;
+
+ try {
+ // 1. Merge model catalog (allowlist)
+ if (modelCatalog && Object.keys(modelCatalog).length > 0) {
+ const existingCatalog = (await openclawApi.getModelCatalog()) || {};
+ const mergedCatalog = { ...existingCatalog, ...modelCatalog };
+ await openclawApi.setModelCatalog(mergedCatalog);
+ modelsRegistered = true;
+ }
+
+ // 2. Set default model (only if not already set)
+ if (model) {
+ const existingDefault = await openclawApi.getDefaultModel();
+ if (!existingDefault?.primary) {
+ await openclawApi.setDefaultModel(model);
+ }
+ }
+
+ // Show success toast if models were registered
+ if (modelsRegistered) {
+ toast.success(
+ t("notifications.openclawModelsRegistered", {
+ defaultValue: "模型已注册到 /model 列表",
+ }),
+ { closeButton: true },
+ );
+ }
+ } catch (error) {
+ // Log warning but don't block main flow - provider config is already saved
+ console.warn(
+ "[OpenClaw] Failed to register models to allowlist:",
+ error,
+ );
+ }
+ }
},
- [addProviderMutation],
+ [addProviderMutation, activeApp, t],
);
// 更新供应商
@@ -104,13 +156,15 @@ export function useProviderActions(activeApp: AppId) {
);
} else {
// 普通供应商:显示切换成功
- // OpenCode: show "added to config" message instead of "switched"
- const messageKey =
- activeApp === "opencode"
- ? "notifications.addToConfigSuccess"
- : "notifications.switchSuccess";
- const defaultMessage =
- activeApp === "opencode" ? "已添加到配置" : "切换成功!";
+ // OpenCode/OpenClaw: show "added to config" message instead of "switched"
+ const isMultiProviderApp =
+ activeApp === "opencode" || activeApp === "openclaw";
+ const messageKey = isMultiProviderApp
+ ? "notifications.addToConfigSuccess"
+ : "notifications.switchSuccess";
+ const defaultMessage = isMultiProviderApp
+ ? "已添加到配置"
+ : "切换成功!";
toast.success(t(messageKey, { defaultValue: defaultMessage }), {
closeButton: true,
@@ -170,12 +224,54 @@ export function useProviderActions(activeApp: AppId) {
[activeApp, queryClient, t],
);
+ // Set provider as default model (OpenClaw only)
+ const setAsDefaultModel = useCallback(
+ async (provider: Provider) => {
+ const config = provider.settingsConfig as OpenClawProviderConfig;
+ if (!config.models || config.models.length === 0) {
+ toast.error(
+ t("notifications.openclawNoModels", {
+ defaultValue: "该供应商没有配置模型",
+ }),
+ );
+ return;
+ }
+
+ const model: OpenClawDefaultModel = {
+ primary: `${provider.id}/${config.models[0].id}`,
+ fallbacks: config.models.slice(1).map((m) => `${provider.id}/${m.id}`),
+ };
+
+ try {
+ await openclawApi.setDefaultModel(model);
+ await queryClient.invalidateQueries({
+ queryKey: openclawKeys.defaultModel,
+ });
+ toast.success(
+ t("notifications.openclawDefaultModelSet", {
+ defaultValue: "已设为默认模型",
+ }),
+ { closeButton: true },
+ );
+ } catch (error) {
+ const detail =
+ extractErrorMessage(error) ||
+ t("notifications.openclawDefaultModelSetFailed", {
+ defaultValue: "设置默认模型失败",
+ });
+ toast.error(detail);
+ }
+ },
+ [queryClient, t],
+ );
+
return {
addProvider,
updateProvider,
switchProvider,
deleteProvider,
saveUsageScript,
+ setAsDefaultModel,
isLoading:
addProviderMutation.isPending ||
updateProviderMutation.isPending ||
diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts
index 55d73d0e1..402da1682 100644
--- a/src/hooks/useSettings.ts
+++ b/src/hooks/useSettings.ts
@@ -135,9 +135,10 @@ export function useSettings(): UseSettingsResult {
const sanitizedOpencodeDir = sanitizeDir(
mergedSettings.opencodeConfigDir,
);
+ const { webdavSync: _ignoredWebdavSync, ...restSettings } = mergedSettings;
const payload: Settings = {
- ...mergedSettings,
+ ...restSettings,
claudeConfigDir: sanitizedClaudeDir,
codexConfigDir: sanitizedCodexDir,
geminiConfigDir: sanitizedGeminiDir,
@@ -251,9 +252,10 @@ export function useSettings(): UseSettingsResult {
const previousCodexDir = sanitizeDir(data?.codexConfigDir);
const previousGeminiDir = sanitizeDir(data?.geminiConfigDir);
const previousOpencodeDir = sanitizeDir(data?.opencodeConfigDir);
+ const { webdavSync: _ignoredWebdavSync, ...restSettings } = mergedSettings;
const payload: Settings = {
- ...mergedSettings,
+ ...restSettings,
claudeConfigDir: sanitizedClaudeDir,
codexConfigDir: sanitizedCodexDir,
geminiConfigDir: sanitizedGeminiDir,
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index 6de6e1a38..69acd81da 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -88,6 +88,8 @@
"addOpenCodeProvider": "Add OpenCode Provider",
"addToConfig": "Add",
"removeFromConfig": "Remove",
+ "setAsDefault": "Enable",
+ "isDefault": "Default",
"inConfig": "Added",
"addProviderHint": "Fill in the information to quickly switch providers in the list.",
"editClaudeProvider": "Edit Claude Code Provider",
@@ -162,7 +164,11 @@
"settingsSaved": "Settings saved",
"settingsSaveFailed": "Failed to save settings: {{error}}",
"openAIChatFormatHint": "This provider uses OpenAI Chat format and requires the proxy service to be enabled",
- "openLinkFailed": "Failed to open link"
+ "openLinkFailed": "Failed to open link",
+ "openclawModelsRegistered": "Models have been registered to /model list",
+ "openclawDefaultModelSet": "Set as default model",
+ "openclawDefaultModelSetFailed": "Failed to set default model",
+ "openclawNoModels": "No models configured"
},
"confirm": {
"deleteProvider": "Delete Provider",
@@ -266,6 +272,77 @@
"selectFileFailed": "Please choose a valid SQL backup file",
"configCorrupted": "SQL file may be corrupted or invalid",
"backupId": "Backup ID",
+ "webdavSync": {
+ "title": "WebDAV Cloud Sync",
+ "description": "Sync database and skill configurations across devices via WebDAV.",
+ "baseUrl": "WebDAV Server URL",
+ "baseUrlPlaceholder": "https://dav.example.com/dav/",
+ "username": "WebDAV Account",
+ "usernamePlaceholder": "Email or username",
+ "password": "WebDAV Password",
+ "passwordPlaceholder": "App password",
+ "remoteRoot": "Remote Root Directory",
+ "profile": "Sync Profile Name",
+ "test": "Test Connection",
+ "testing": "Testing...",
+ "testSuccess": "Connection successful",
+ "testFailed": "Connection failed: {{error}}",
+ "save": "Save Config",
+ "saving": "Saving...",
+ "saveFailed": "Failed to save config: {{error}}",
+ "upload": "Upload to Cloud",
+ "uploading": "Uploading...",
+ "uploadSuccess": "Uploaded to WebDAV",
+ "uploadFailed": "Upload failed: {{error}}",
+ "download": "Download from Cloud",
+ "downloading": "Downloading...",
+ "downloadSuccess": "Downloaded and restored from WebDAV",
+ "downloadFailed": "Download failed: {{error}}",
+ "lastSync": "Last sync: {{time}}",
+ "missingUrl": "Please enter the WebDAV server URL",
+ "presets": {
+ "label": "Provider",
+ "jianguoyun": "Jianguoyun",
+ "jianguoyunHint": "Generate an \"App Password\" in Jianguoyun security settings. Do not use your login password.",
+ "nextcloud": "Nextcloud",
+ "nextcloudHint": "Replace your-server with your Nextcloud domain and USERNAME with your username.",
+ "synology": "Synology NAS",
+ "synologyHint": "Install and enable the WebDAV Server package in Synology Package Center first.",
+ "custom": "Custom"
+ },
+ "remoteRootDefault": "Default: cc-switch-sync",
+ "profileDefault": "Default: default",
+ "saveAndTestSuccess": "Config saved, connection OK",
+ "saveAndTestFailed": "Config saved, but connection test failed: {{error}}",
+ "noRemoteData": "No sync data found on the remote server",
+ "incompatibleVersion": "Remote data version incompatible (v{{version}}), current supports v2",
+ "unsaved": "Unsaved",
+ "saved": "Saved",
+ "unsavedChanges": "Please save config first",
+ "saveBeforeSync": "Save configuration first to enable upload/download.",
+ "fetchingRemote": "Fetching remote info...",
+ "fetchRemoteFailed": "Failed to fetch remote info. Please check configuration and network.",
+ "confirmDownload": {
+ "title": "Restore from Cloud",
+ "deviceName": "Uploaded by",
+ "createdAt": "Uploaded at",
+ "artifacts": "Contents",
+ "warning": "This will overwrite all local data and skill configurations",
+ "confirm": "Confirm Restore"
+ },
+ "confirmUpload": {
+ "title": "Upload to Cloud",
+ "content": "The following will be synced to the WebDAV server:",
+ "dbItem": "Database (all provider configs and data)",
+ "skillsItem": "Skills (all custom skills)",
+ "targetPath": "Target path",
+ "existingData": "Existing cloud data",
+ "deviceName": "Uploaded by",
+ "createdAt": "Uploaded at",
+ "warning": "This will overwrite existing sync data on the remote server",
+ "confirm": "Confirm Upload"
+ }
+ },
"autoReload": "Data refreshed",
"languageOptionChinese": "中文",
"languageOptionEnglish": "English",
@@ -407,7 +484,8 @@
"claude": "Claude",
"codex": "Codex",
"gemini": "Gemini",
- "opencode": "OpenCode"
+ "opencode": "OpenCode",
+ "openclaw": "OpenClaw"
},
"sessionManager": {
"title": "Session Manager",
@@ -660,6 +738,38 @@
"modelOptionKeyPlaceholder": "provider",
"modelOptionValuePlaceholder": "{\"order\": [\"baseten\"]}"
},
+ "openclaw": {
+ "providerKey": "Provider Key",
+ "providerKeyPlaceholder": "my-provider",
+ "providerKeyHint": "Unique identifier in config file. Cannot be changed after creation. Use lowercase letters, numbers, and hyphens only.",
+ "providerKeyRequired": "Provider key is required",
+ "providerKeyDuplicate": "This key is already in use",
+ "providerKeyInvalid": "Invalid format. Use lowercase letters, numbers, and hyphens only.",
+ "apiProtocol": "API Protocol",
+ "selectProtocol": "Select API Protocol",
+ "apiProtocolHint": "Select the protocol type compatible with the provider's API. Most providers use OpenAI Completions format.",
+ "baseUrl": "API Endpoint",
+ "baseUrlHint": "The provider's API endpoint address.",
+ "models": "Models",
+ "addModel": "Add Model",
+ "noModels": "No models configured. Click Add Model to configure available models.",
+ "modelId": "Model ID",
+ "modelIdPlaceholder": "claude-3-sonnet",
+ "modelName": "Display Name",
+ "modelNamePlaceholder": "Claude 3 Sonnet",
+ "contextWindow": "Context Window",
+ "maxTokens": "Max Output Tokens",
+ "reasoning": "Reasoning Mode",
+ "reasoningOn": "Enabled",
+ "reasoningOff": "Disabled",
+ "inputCost": "Input Cost ($/M tokens)",
+ "outputCost": "Output Cost ($/M tokens)",
+ "advancedOptions": "Advanced Options",
+ "cacheReadCost": "Cache Read Cost ($/M tokens)",
+ "cacheWriteCost": "Cache Write Cost ($/M tokens)",
+ "cacheCostHint": "Cache costs are used to calculate Prompt Caching costs. Leave empty if not using caching.",
+ "modelsHint": "Configure the models supported by this provider. Model ID is used for API calls, Display Name for the interface."
+ },
"providerPreset": {
"label": "Provider Preset",
"custom": "Custom Configuration",
@@ -855,7 +965,8 @@
"claude": "Claude",
"codex": "Codex",
"gemini": "Gemini",
- "opencode": "OpenCode"
+ "opencode": "OpenCode",
+ "openclaw": "OpenClaw"
}
},
"userLevelPath": "User-level MCP path",
@@ -1028,6 +1139,74 @@
"deleteMessage": "Are you sure you want to delete prompt \"{{name}}\"?"
}
},
+ "workspace": {
+ "title": "Workspace Files",
+ "manage": "Workspace",
+ "files": {
+ "agents": "Agent instructions and rules",
+ "soul": "Agent personality and communication style",
+ "user": "User profile and preferences",
+ "identity": "Agent name and avatar",
+ "tools": "Local tool documentation",
+ "memory": "Long-term memory and decisions",
+ "heartbeat": "Heartbeat run checklist",
+ "bootstrap": "First-run bootstrap ritual",
+ "boot": "Gateway reboot checklist"
+ },
+ "editing": "Edit {{filename}}",
+ "saveSuccess": "Saved successfully",
+ "saveFailed": "Failed to save",
+ "loadFailed": "Failed to load"
+ },
+ "openclaw": {
+ "env": {
+ "title": "Environment Variables",
+ "description": "Manage environment variables in openclaw.json (API keys, custom variables, etc.)",
+ "keyPlaceholder": "Variable name",
+ "valuePlaceholder": "Value",
+ "add": "Add Variable",
+ "saveSuccess": "Environment variables saved",
+ "saveFailed": "Failed to save environment variables",
+ "loadFailed": "Failed to load environment variables",
+ "duplicateKey": "Duplicate variable name detected: {{key}}"
+ },
+ "tools": {
+ "title": "Tool Permissions",
+ "description": "Manage tool permissions in openclaw.json (allow/deny lists)",
+ "profile": "Permission Profile",
+ "profiles": {
+ "default": "Default",
+ "strict": "Strict",
+ "permissive": "Permissive",
+ "custom": "Custom"
+ },
+ "allowList": "Allow List",
+ "denyList": "Deny List",
+ "patternPlaceholder": "Tool name or pattern",
+ "addAllow": "Add Allow",
+ "addDeny": "Add Deny",
+ "saveSuccess": "Tool permissions saved",
+ "saveFailed": "Failed to save tool permissions",
+ "loadFailed": "Failed to load tool permissions"
+ },
+ "agents": {
+ "title": "Agents Config",
+ "description": "Manage agents.defaults in openclaw.json (default model, runtime parameters, etc.)",
+ "modelSection": "Model Configuration",
+ "primaryModel": "Primary Model",
+ "primaryModelHint": "Format: provider/model-id",
+ "fallbackModels": "Fallback Models",
+ "fallbackModelsHint": "Comma-separated, ordered by priority",
+ "runtimeSection": "Runtime Parameters",
+ "workspace": "Workspace Path",
+ "timeout": "Timeout (seconds)",
+ "contextTokens": "Context Tokens",
+ "maxConcurrent": "Max Concurrent",
+ "saveSuccess": "Agents config saved",
+ "saveFailed": "Failed to save agents config",
+ "loadFailed": "Failed to load agents config"
+ }
+ },
"env": {
"warning": {
"title": "Environment Variable Conflicts Detected",
@@ -1170,7 +1349,8 @@
"claude": "Claude",
"codex": "Codex",
"gemini": "Gemini",
- "opencode": "OpenCode"
+ "opencode": "OpenCode",
+ "openclaw": "OpenClaw"
},
"installFromZip": {
"button": "Install from ZIP",
@@ -1671,5 +1851,34 @@
"unspecifiedHigh": "Uncategorized high-effort task category for tasks that don't fit other categories with large workload. Defaults to Claude Opus 4.6 max variant.",
"writing": "Writing category for documentation, prose and technical writing. Defaults to Gemini 3 Flash fast generation model."
}
+ },
+ "openclawConfig": {
+ "defaultModel": {
+ "title": "Default Model",
+ "description": "Configure the default primary model and fallback models for OpenClaw",
+ "primary": "Primary Model",
+ "primaryPlaceholder": "e.g., deepseek/deepseek-chat",
+ "fallbacks": "Fallback Models",
+ "fallbacksPlaceholder": "e.g., openrouter/anthropic/claude-sonnet-4.5",
+ "addFallback": "Add Fallback Model",
+ "saved": "Default model configuration saved",
+ "saveFailed": "Failed to save default model"
+ },
+ "modelCatalog": {
+ "title": "Model Catalog",
+ "description": "Configure model aliases and allowlist",
+ "modelId": "Model ID",
+ "modelIdPlaceholder": "e.g., deepseek/deepseek-chat",
+ "alias": "Alias",
+ "aliasPlaceholder": "e.g., DeepSeek",
+ "addEntry": "Add Model",
+ "removeEntry": "Remove",
+ "saved": "Model catalog saved",
+ "saveFailed": "Failed to save model catalog",
+ "empty": "No model catalog entries",
+ "emptyHint": "Add models to the catalog to configure aliases"
+ },
+ "suggestedDefaults": "Apply Suggested Defaults",
+ "suggestedDefaultsHint": "Use this preset's recommended default model configuration"
}
}
diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json
index 87047b29b..4b37c5010 100644
--- a/src/i18n/locales/ja.json
+++ b/src/i18n/locales/ja.json
@@ -88,6 +88,8 @@
"addOpenCodeProvider": "OpenCode プロバイダーを追加",
"addToConfig": "追加",
"removeFromConfig": "削除",
+ "setAsDefault": "有効化",
+ "isDefault": "デフォルト",
"inConfig": "追加済み",
"addProviderHint": "一覧にすばやく切り替えられるよう、ここに情報を入力してください。",
"editClaudeProvider": "Claude Code プロバイダーを編集",
@@ -162,7 +164,11 @@
"settingsSaved": "設定を保存しました",
"settingsSaveFailed": "設定の保存に失敗しました: {{error}}",
"openAIChatFormatHint": "このプロバイダーは OpenAI Chat フォーマットを使用しており、プロキシサービスの有効化が必要です",
- "openLinkFailed": "リンクを開けませんでした"
+ "openLinkFailed": "リンクを開けませんでした",
+ "openclawModelsRegistered": "モデルが /model リストに登録されました",
+ "openclawDefaultModelSet": "デフォルトモデルに設定しました",
+ "openclawDefaultModelSetFailed": "デフォルトモデルの設定に失敗しました",
+ "openclawNoModels": "モデルが設定されていません"
},
"confirm": {
"deleteProvider": "プロバイダーを削除",
@@ -266,6 +272,77 @@
"selectFileFailed": "有効な SQL バックアップファイルを選択してください",
"configCorrupted": "SQL ファイルが壊れているか形式が無効な可能性があります",
"backupId": "バックアップ ID",
+ "webdavSync": {
+ "title": "WebDAV クラウド同期",
+ "description": "WebDAV を使ってデバイス間でデータベースとスキル設定を同期します。",
+ "baseUrl": "WebDAV サーバー URL",
+ "baseUrlPlaceholder": "https://example.com/remote.php/dav/files/user",
+ "username": "ユーザー名",
+ "usernamePlaceholder": "メールアドレスまたはユーザー名",
+ "password": "パスワード",
+ "passwordPlaceholder": "アプリパスワード",
+ "remoteRoot": "リモートルートディレクトリ",
+ "profile": "同期プロファイル名",
+ "test": "接続テスト",
+ "testing": "テスト中...",
+ "testSuccess": "接続成功",
+ "testFailed": "接続失敗:{{error}}",
+ "save": "設定を保存",
+ "saving": "保存中...",
+ "saveFailed": "設定の保存に失敗しました:{{error}}",
+ "upload": "クラウドにアップロード",
+ "uploading": "アップロード中...",
+ "uploadSuccess": "WebDAV にアップロードしました",
+ "uploadFailed": "アップロードに失敗しました:{{error}}",
+ "download": "クラウドからダウンロード",
+ "downloading": "ダウンロード中...",
+ "downloadSuccess": "WebDAV からダウンロード・復元しました",
+ "downloadFailed": "ダウンロードに失敗しました:{{error}}",
+ "lastSync": "前回の同期:{{time}}",
+ "missingUrl": "WebDAV サーバー URL を入力してください",
+ "presets": {
+ "label": "サービス",
+ "jianguoyun": "坚果云",
+ "jianguoyunHint": "坚果云の「セキュリティ設定」で「サードパーティアプリパスワード」を生成してください。ログインパスワードは使用しないでください。",
+ "nextcloud": "Nextcloud",
+ "nextcloudHint": "your-server を Nextcloud サーバーのアドレスに、USERNAME をユーザー名に置き換えてください。",
+ "synology": "Synology NAS",
+ "synologyHint": "Synology の「パッケージセンター」で WebDAV Server パッケージをインストール・有効化してください。",
+ "custom": "カスタム"
+ },
+ "remoteRootDefault": "デフォルト: cc-switch-sync",
+ "profileDefault": "デフォルト: default",
+ "saveAndTestSuccess": "設定を保存しました。接続正常です",
+ "saveAndTestFailed": "設定を保存しましたが、接続テストに失敗しました:{{error}}",
+ "noRemoteData": "クラウドに同期データが見つかりません",
+ "incompatibleVersion": "リモートデータのバージョンに互換性がありません(v{{version}})。現在 v2 をサポートしています",
+ "unsaved": "未保存",
+ "saved": "保存済み",
+ "unsavedChanges": "先に設定を保存してください",
+ "saveBeforeSync": "アップロード/ダウンロードを有効にするには、先に設定を保存してください。",
+ "fetchingRemote": "リモート情報を取得中...",
+ "fetchRemoteFailed": "リモート情報の取得に失敗しました。設定とネットワークを確認してください。",
+ "confirmDownload": {
+ "title": "クラウドから復元",
+ "deviceName": "アップロード元",
+ "createdAt": "アップロード日時",
+ "artifacts": "内容",
+ "warning": "ローカルのすべてのデータとスキル設定が上書きされます",
+ "confirm": "復元を実行"
+ },
+ "confirmUpload": {
+ "title": "クラウドにアップロード",
+ "content": "以下の内容を WebDAV サーバーに同期します:",
+ "dbItem": "データベース(すべてのプロバイダー設定とデータ)",
+ "skillsItem": "スキル(すべてのカスタムスキル)",
+ "targetPath": "保存先パス",
+ "existingData": "クラウドの既存データ",
+ "deviceName": "アップロード元",
+ "createdAt": "アップロード日時",
+ "warning": "リモートの既存同期データが上書きされます",
+ "confirm": "アップロードを実行"
+ }
+ },
"autoReload": "データを更新しました",
"languageOptionChinese": "中文",
"languageOptionEnglish": "English",
@@ -407,7 +484,8 @@
"claude": "Claude",
"codex": "Codex",
"gemini": "Gemini",
- "opencode": "OpenCode"
+ "opencode": "OpenCode",
+ "openclaw": "OpenClaw"
},
"sessionManager": {
"title": "セッション管理",
@@ -660,6 +738,38 @@
"modelOptionKeyPlaceholder": "provider",
"modelOptionValuePlaceholder": "{\"order\": [\"baseten\"]}"
},
+ "openclaw": {
+ "providerKey": "プロバイダーキー",
+ "providerKeyPlaceholder": "my-provider",
+ "providerKeyHint": "設定ファイル内のユニーク識別子。作成後は変更できません。小文字、数字、ハイフンのみ使用可能。",
+ "providerKeyRequired": "プロバイダーキーを入力してください",
+ "providerKeyDuplicate": "このキーは既に使用されています",
+ "providerKeyInvalid": "無効な形式です。小文字、数字、ハイフンのみ使用可能。",
+ "apiProtocol": "API プロトコル",
+ "selectProtocol": "API プロトコルを選択",
+ "apiProtocolHint": "プロバイダーの API と互換性のあるプロトコルタイプを選択してください。ほとんどのプロバイダーは OpenAI Completions 形式を使用します。",
+ "baseUrl": "API エンドポイント",
+ "baseUrlHint": "プロバイダーの API エンドポイントアドレス。",
+ "models": "モデル一覧",
+ "addModel": "モデルを追加",
+ "noModels": "モデルが設定されていません。「モデルを追加」をクリックして利用可能なモデルを設定してください。",
+ "modelId": "モデル ID",
+ "modelIdPlaceholder": "claude-3-sonnet",
+ "modelName": "表示名",
+ "modelNamePlaceholder": "Claude 3 Sonnet",
+ "contextWindow": "コンテキストウィンドウ",
+ "maxTokens": "最大出力トークン数",
+ "reasoning": "推論モード",
+ "reasoningOn": "有効",
+ "reasoningOff": "無効",
+ "inputCost": "入力コスト ($/M トークン)",
+ "outputCost": "出力コスト ($/M トークン)",
+ "advancedOptions": "詳細オプション",
+ "cacheReadCost": "キャッシュ読取コスト ($/M トークン)",
+ "cacheWriteCost": "キャッシュ書込コスト ($/M トークン)",
+ "cacheCostHint": "キャッシュコストは Prompt Caching のコスト計算に使用されます。キャッシュを使用しない場合は空欄のままにしてください。",
+ "modelsHint": "このプロバイダーがサポートするモデルを設定します。モデル ID は API 呼び出しに、表示名はインターフェースに使用されます。"
+ },
"providerPreset": {
"label": "プロバイダータイプ",
"custom": "カスタム設定",
@@ -855,7 +965,8 @@
"claude": "Claude",
"codex": "Codex",
"gemini": "Gemini",
- "opencode": "OpenCode"
+ "opencode": "OpenCode",
+ "openclaw": "OpenClaw"
}
},
"userLevelPath": "ユーザーレベルの MCP パス",
@@ -1028,6 +1139,74 @@
"deleteMessage": "プロンプト「{{name}}」を削除してもよろしいですか?"
}
},
+ "workspace": {
+ "title": "ワークスペースファイル",
+ "manage": "ワークスペース",
+ "files": {
+ "agents": "エージェントの操作指示とルール",
+ "soul": "エージェントの人格とコミュニケーションスタイル",
+ "user": "ユーザープロファイルと設定",
+ "identity": "エージェントの名前とアバター",
+ "tools": "ローカルツールドキュメント",
+ "memory": "長期記憶と意思決定記録",
+ "heartbeat": "ハートビート実行チェックリスト",
+ "bootstrap": "初回実行ブートストラップ",
+ "boot": "ゲートウェイ再起動チェックリスト"
+ },
+ "editing": "{{filename}} を編集",
+ "saveSuccess": "保存しました",
+ "saveFailed": "保存に失敗しました",
+ "loadFailed": "読み込みに失敗しました"
+ },
+ "openclaw": {
+ "env": {
+ "title": "環境変数",
+ "description": "openclaw.json の環境変数設定を管理(APIキー、カスタム変数など)",
+ "keyPlaceholder": "変数名",
+ "valuePlaceholder": "値",
+ "add": "変数を追加",
+ "saveSuccess": "環境変数を保存しました",
+ "saveFailed": "環境変数の保存に失敗しました",
+ "loadFailed": "環境変数の読み込みに失敗しました",
+ "duplicateKey": "重複する変数名が検出されました: {{key}}"
+ },
+ "tools": {
+ "title": "ツール権限",
+ "description": "openclaw.json のツール権限設定を管理(許可/拒否リスト)",
+ "profile": "権限プロファイル",
+ "profiles": {
+ "default": "デフォルト",
+ "strict": "厳格",
+ "permissive": "寛容",
+ "custom": "カスタム"
+ },
+ "allowList": "許可リスト",
+ "denyList": "拒否リスト",
+ "patternPlaceholder": "ツール名またはパターン",
+ "addAllow": "許可を追加",
+ "addDeny": "拒否を追加",
+ "saveSuccess": "ツール権限を保存しました",
+ "saveFailed": "ツール権限の保存に失敗しました",
+ "loadFailed": "ツール権限の読み込みに失敗しました"
+ },
+ "agents": {
+ "title": "Agents 設定",
+ "description": "openclaw.json の agents.defaults 設定を管理(デフォルトモデル、ランタイムパラメータなど)",
+ "modelSection": "モデル設定",
+ "primaryModel": "プライマリモデル",
+ "primaryModelHint": "形式: provider/model-id",
+ "fallbackModels": "フォールバックモデル",
+ "fallbackModelsHint": "カンマ区切り、優先度順",
+ "runtimeSection": "ランタイムパラメータ",
+ "workspace": "ワークスペースパス",
+ "timeout": "タイムアウト(秒)",
+ "contextTokens": "コンテキストトークン数",
+ "maxConcurrent": "最大同時実行数",
+ "saveSuccess": "Agents 設定を保存しました",
+ "saveFailed": "Agents 設定の保存に失敗しました",
+ "loadFailed": "Agents 設定の読み込みに失敗しました"
+ }
+ },
"env": {
"warning": {
"title": "競合する環境変数を検出しました",
@@ -1168,7 +1347,8 @@
"claude": "Claude",
"codex": "Codex",
"gemini": "Gemini",
- "opencode": "OpenCode"
+ "opencode": "OpenCode",
+ "openclaw": "OpenClaw"
},
"installFromZip": {
"button": "ZIP からインストール",
@@ -1652,5 +1832,34 @@
"unspecifiedHigh": "未分類の高作業量タスクカテゴリ。他のカテゴリに該当せず作業量が大きいタスクに適用。デフォルトで Claude Opus 4.6 の max バリアントを使用。",
"writing": "ライティングカテゴリ。ドキュメント、散文、技術文書に特化。デフォルトで Gemini 3 Flash 高速生成モデルを使用。"
}
+ },
+ "openclawConfig": {
+ "defaultModel": {
+ "title": "デフォルトモデル",
+ "description": "OpenClaw のデフォルトのプライマリモデルとフォールバックモデルを設定します",
+ "primary": "プライマリモデル",
+ "primaryPlaceholder": "例: deepseek/deepseek-chat",
+ "fallbacks": "フォールバックモデル",
+ "fallbacksPlaceholder": "例: openrouter/anthropic/claude-sonnet-4.5",
+ "addFallback": "フォールバックモデルを追加",
+ "saved": "デフォルトモデル設定を保存しました",
+ "saveFailed": "デフォルトモデルの保存に失敗しました"
+ },
+ "modelCatalog": {
+ "title": "モデルカタログ",
+ "description": "モデルのエイリアスと許可リストを設定します",
+ "modelId": "モデル ID",
+ "modelIdPlaceholder": "例: deepseek/deepseek-chat",
+ "alias": "エイリアス",
+ "aliasPlaceholder": "例: DeepSeek",
+ "addEntry": "モデルを追加",
+ "removeEntry": "削除",
+ "saved": "モデルカタログを保存しました",
+ "saveFailed": "モデルカタログの保存に失敗しました",
+ "empty": "モデルカタログエントリがありません",
+ "emptyHint": "エイリアスを設定するにはカタログにモデルを追加してください"
+ },
+ "suggestedDefaults": "推奨デフォルト設定を適用",
+ "suggestedDefaultsHint": "このプリセットの推奨デフォルトモデル設定を使用します"
}
}
diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json
index 731b1c558..f18795aba 100644
--- a/src/i18n/locales/zh.json
+++ b/src/i18n/locales/zh.json
@@ -88,6 +88,8 @@
"addOpenCodeProvider": "添加 OpenCode 供应商",
"addToConfig": "添加",
"removeFromConfig": "移除",
+ "setAsDefault": "启用",
+ "isDefault": "默认",
"inConfig": "已添加",
"addProviderHint": "填写信息后即可在列表中快速切换供应商。",
"editClaudeProvider": "编辑 Claude Code 供应商",
@@ -162,7 +164,11 @@
"settingsSaved": "设置已保存",
"settingsSaveFailed": "保存设置失败:{{error}}",
"openAIChatFormatHint": "此供应商使用 OpenAI Chat 格式,需要开启代理服务才能正常使用",
- "openLinkFailed": "链接打开失败"
+ "openLinkFailed": "链接打开失败",
+ "openclawModelsRegistered": "模型已注册到 /model 列表",
+ "openclawDefaultModelSet": "已设为默认模型",
+ "openclawDefaultModelSetFailed": "设置默认模型失败",
+ "openclawNoModels": "该供应商没有配置模型"
},
"confirm": {
"deleteProvider": "删除供应商",
@@ -266,6 +272,77 @@
"selectFileFailed": "请选择有效的 SQL 备份文件",
"configCorrupted": "SQL 文件可能已损坏或格式不正确",
"backupId": "备份ID",
+ "webdavSync": {
+ "title": "WebDAV 云同步",
+ "description": "通过 WebDAV 在多设备间同步数据库和技能配置。",
+ "baseUrl": "WebDAV 服务器地址",
+ "baseUrlPlaceholder": "https://dav.jianguoyun.com/dav/",
+ "username": "WebDAV 账户",
+ "usernamePlaceholder": "邮箱账号",
+ "password": "WebDAV 密码",
+ "passwordPlaceholder": "应用密码(坚果云请使用「第三方应用密码」)",
+ "remoteRoot": "远程根目录",
+ "profile": "同步配置名",
+ "test": "测试连接",
+ "testing": "测试中...",
+ "testSuccess": "连接成功",
+ "testFailed": "连接失败:{{error}}",
+ "save": "保存配置",
+ "saving": "保存中...",
+ "saveFailed": "保存配置失败:{{error}}",
+ "upload": "上传到云端",
+ "uploading": "上传中...",
+ "uploadSuccess": "已上传到 WebDAV",
+ "uploadFailed": "上传失败:{{error}}",
+ "download": "从云端下载",
+ "downloading": "下载中...",
+ "downloadSuccess": "已从 WebDAV 下载并恢复",
+ "downloadFailed": "下载失败:{{error}}",
+ "lastSync": "上次同步:{{time}}",
+ "missingUrl": "请填写 WebDAV 服务器地址",
+ "presets": {
+ "label": "服务商",
+ "jianguoyun": "坚果云",
+ "jianguoyunHint": "请在坚果云「安全选项」中生成「第三方应用密码」,不要使用登录密码。",
+ "nextcloud": "Nextcloud",
+ "nextcloudHint": "请将 your-server 替换为你的 Nextcloud 服务器地址,USERNAME 替换为你的用户名。",
+ "synology": "群晖 NAS",
+ "synologyHint": "请先在群晖「套件中心」安装并启用 WebDAV Server 套件。",
+ "custom": "自定义"
+ },
+ "remoteRootDefault": "默认: cc-switch-sync",
+ "profileDefault": "默认: default",
+ "saveAndTestSuccess": "配置已保存,连接正常",
+ "saveAndTestFailed": "配置已保存,但连接测试失败:{{error}}",
+ "noRemoteData": "云端没有找到同步数据",
+ "incompatibleVersion": "远端数据版本不兼容(v{{version}}),当前支持 v2",
+ "unsaved": "未保存",
+ "saved": "已保存",
+ "unsavedChanges": "请先保存配置",
+ "saveBeforeSync": "请先保存配置,再使用上传/下载。",
+ "fetchingRemote": "获取远端信息...",
+ "fetchRemoteFailed": "获取远端信息失败,请检查配置和网络后重试。",
+ "confirmDownload": {
+ "title": "从云端恢复",
+ "deviceName": "上传设备",
+ "createdAt": "上传时间",
+ "artifacts": "包含内容",
+ "warning": "恢复将覆盖本地所有数据和技能配置",
+ "confirm": "确认恢复"
+ },
+ "confirmUpload": {
+ "title": "上传到云端",
+ "content": "将同步以下内容到 WebDAV 服务器:",
+ "dbItem": "数据库(所有 Provider 配置和数据)",
+ "skillsItem": "技能包(所有自定义技能)",
+ "targetPath": "目标路径",
+ "existingData": "云端已有数据",
+ "deviceName": "上传设备",
+ "createdAt": "上传时间",
+ "warning": "将覆盖云端已有的同步数据",
+ "confirm": "确认上传"
+ }
+ },
"autoReload": "数据已刷新",
"languageOptionChinese": "中文",
"languageOptionEnglish": "English",
@@ -407,7 +484,8 @@
"claude": "Claude",
"codex": "Codex",
"gemini": "Gemini",
- "opencode": "OpenCode"
+ "opencode": "OpenCode",
+ "openclaw": "OpenClaw"
},
"sessionManager": {
"title": "会话管理",
@@ -660,6 +738,38 @@
"modelOptionKeyPlaceholder": "provider",
"modelOptionValuePlaceholder": "{\"order\": [\"baseten\"]}"
},
+ "openclaw": {
+ "providerKey": "供应商标识",
+ "providerKeyPlaceholder": "my-provider",
+ "providerKeyHint": "配置文件中的唯一标识符,创建后无法修改,只能使用小写字母、数字和连字符",
+ "providerKeyRequired": "请填写供应商标识",
+ "providerKeyDuplicate": "此标识已被使用,请更换",
+ "providerKeyInvalid": "标识格式无效,只能使用小写字母、数字和连字符",
+ "apiProtocol": "API 协议",
+ "selectProtocol": "选择 API 协议",
+ "apiProtocolHint": "选择与供应商 API 兼容的协议类型。大多数供应商使用 OpenAI Completions 格式。",
+ "baseUrl": "API 端点",
+ "baseUrlHint": "供应商的 API 端点地址。",
+ "models": "模型列表",
+ "addModel": "添加模型",
+ "noModels": "暂无模型配置。点击添加模型来配置可用模型。",
+ "modelId": "模型 ID",
+ "modelIdPlaceholder": "claude-3-sonnet",
+ "modelName": "显示名称",
+ "modelNamePlaceholder": "Claude 3 Sonnet",
+ "contextWindow": "上下文窗口",
+ "maxTokens": "最大输出 Tokens",
+ "reasoning": "推理模式",
+ "reasoningOn": "启用",
+ "reasoningOff": "关闭",
+ "inputCost": "输入价格 ($/M tokens)",
+ "outputCost": "输出价格 ($/M tokens)",
+ "advancedOptions": "高级选项",
+ "cacheReadCost": "缓存读取价格 ($/M tokens)",
+ "cacheWriteCost": "缓存写入价格 ($/M tokens)",
+ "cacheCostHint": "缓存价格用于计算 Prompt Caching 的成本。如不使用缓存可留空。",
+ "modelsHint": "配置该供应商支持的模型。模型 ID 用于 API 调用,显示名称用于界面展示。"
+ },
"providerPreset": {
"label": "预设供应商",
"custom": "自定义配置",
@@ -855,7 +965,8 @@
"claude": "Claude",
"codex": "Codex",
"gemini": "Gemini",
- "opencode": "OpenCode"
+ "opencode": "OpenCode",
+ "openclaw": "OpenClaw"
}
},
"userLevelPath": "用户级 MCP 配置路径",
@@ -1028,6 +1139,74 @@
"deleteMessage": "确定要删除提示词 \"{{name}}\" 吗?"
}
},
+ "workspace": {
+ "title": "Workspace 文件管理",
+ "manage": "Workspace",
+ "files": {
+ "agents": "Agent 操作指令和规则",
+ "soul": "Agent 人格和沟通风格",
+ "user": "用户档案和偏好",
+ "identity": "Agent 名称和头像",
+ "tools": "本地工具文档",
+ "memory": "长期记忆和决策记录",
+ "heartbeat": "心跳运行清单",
+ "bootstrap": "首次运行仪式",
+ "boot": "网关重启清单"
+ },
+ "editing": "编辑 {{filename}}",
+ "saveSuccess": "保存成功",
+ "saveFailed": "保存失败",
+ "loadFailed": "读取失败"
+ },
+ "openclaw": {
+ "env": {
+ "title": "环境变量",
+ "description": "管理 openclaw.json 中的环境变量配置(API Key、自定义变量等)",
+ "keyPlaceholder": "变量名",
+ "valuePlaceholder": "值",
+ "add": "添加变量",
+ "saveSuccess": "环境变量已保存",
+ "saveFailed": "保存环境变量失败",
+ "loadFailed": "读取环境变量失败",
+ "duplicateKey": "检测到重复的变量名: {{key}}"
+ },
+ "tools": {
+ "title": "工具权限",
+ "description": "管理 openclaw.json 中的工具权限配置(允许/拒绝列表)",
+ "profile": "权限模式",
+ "profiles": {
+ "default": "默认",
+ "strict": "严格",
+ "permissive": "宽松",
+ "custom": "自定义"
+ },
+ "allowList": "允许列表",
+ "denyList": "拒绝列表",
+ "patternPlaceholder": "工具名称或模式",
+ "addAllow": "添加允许",
+ "addDeny": "添加拒绝",
+ "saveSuccess": "工具权限已保存",
+ "saveFailed": "保存工具权限失败",
+ "loadFailed": "读取工具权限失败"
+ },
+ "agents": {
+ "title": "Agents 配置",
+ "description": "管理 openclaw.json 中的 agents.defaults 配置(默认模型、运行参数等)",
+ "modelSection": "模型配置",
+ "primaryModel": "主模型",
+ "primaryModelHint": "格式:provider/model-id",
+ "fallbackModels": "回退模型",
+ "fallbackModelsHint": "逗号分隔,按优先级排列",
+ "runtimeSection": "运行参数",
+ "workspace": "工作区路径",
+ "timeout": "超时时间(秒)",
+ "contextTokens": "上下文 Token 数",
+ "maxConcurrent": "最大并发数",
+ "saveSuccess": "Agents 配置已保存",
+ "saveFailed": "保存 Agents 配置失败",
+ "loadFailed": "读取 Agents 配置失败"
+ }
+ },
"env": {
"warning": {
"title": "检测到系统环境变量冲突",
@@ -1170,7 +1349,8 @@
"claude": "Claude",
"codex": "Codex",
"gemini": "Gemini",
- "opencode": "OpenCode"
+ "opencode": "OpenCode",
+ "openclaw": "OpenClaw"
},
"installFromZip": {
"button": "从 ZIP 安装",
@@ -1671,5 +1851,34 @@
"unspecifiedHigh": "未归类高工作量任务类别,适用于不适合其他类别且工作量较大的任务,默认使用 Claude Opus 4.6 的最大变体。",
"writing": "写作类别,专注于文档、散文和技术写作,默认使用 Gemini 3 Flash 快速生成模型。"
}
+ },
+ "openclawConfig": {
+ "defaultModel": {
+ "title": "默认模型",
+ "description": "配置 OpenClaw 的默认主模型和回退模型",
+ "primary": "主模型",
+ "primaryPlaceholder": "例如: deepseek/deepseek-chat",
+ "fallbacks": "回退模型",
+ "fallbacksPlaceholder": "例如: openrouter/anthropic/claude-sonnet-4.5",
+ "addFallback": "添加回退模型",
+ "saved": "默认模型配置已保存",
+ "saveFailed": "保存默认模型失败"
+ },
+ "modelCatalog": {
+ "title": "模型目录",
+ "description": "配置可用模型的别名和允许列表",
+ "modelId": "模型 ID",
+ "modelIdPlaceholder": "例如: deepseek/deepseek-chat",
+ "alias": "别名",
+ "aliasPlaceholder": "例如: DeepSeek",
+ "addEntry": "添加模型",
+ "removeEntry": "移除",
+ "saved": "模型目录已保存",
+ "saveFailed": "保存模型目录失败",
+ "empty": "暂无模型目录配置",
+ "emptyHint": "添加模型到目录以配置别名"
+ },
+ "suggestedDefaults": "应用建议默认配置",
+ "suggestedDefaultsHint": "使用此预设推荐的默认模型配置"
}
}
diff --git a/src/icons/extracted/claw.svg b/src/icons/extracted/claw.svg
new file mode 100644
index 000000000..bcbc1e10c
--- /dev/null
+++ b/src/icons/extracted/claw.svg
@@ -0,0 +1,22 @@
+
diff --git a/src/icons/extracted/index.ts b/src/icons/extracted/index.ts
index dcc9bb12d..a87ab5d88 100644
--- a/src/icons/extracted/index.ts
+++ b/src/icons/extracted/index.ts
@@ -37,6 +37,7 @@ export const icons: Record = {
notion: ``,
ollama: ``,
openai: ``,
+ openclaw: ``,
packycode: ``,
palm: ``,
perplexity: ``,
diff --git a/src/icons/extracted/metadata.ts b/src/icons/extracted/metadata.ts
index 0c103d028..6b2c8a8a1 100644
--- a/src/icons/extracted/metadata.ts
+++ b/src/icons/extracted/metadata.ts
@@ -247,6 +247,13 @@ export const iconMetadata: Record = {
keywords: ["gpt", "chatgpt"],
defaultColor: "currentColor",
},
+ openclaw: {
+ name: "openclaw",
+ displayName: "OpenClaw",
+ category: "ai-provider",
+ keywords: ["openclaw", "lobster", "claw"],
+ defaultColor: "#ff4f40",
+ },
packycode: {
name: "packycode",
displayName: "PackyCode",
diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts
index 8e48d0151..7a0bfd1c2 100644
--- a/src/lib/api/index.ts
+++ b/src/lib/api/index.ts
@@ -7,7 +7,9 @@ export { skillsApi } from "./skills";
export { usageApi } from "./usage";
export { vscodeApi } from "./vscode";
export { proxyApi } from "./proxy";
+export { openclawApi } from "./openclaw";
export { sessionsApi } from "./sessions";
+export { workspaceApi } from "./workspace";
export * as configApi from "./config";
export type { ProviderSwitchEvent } from "./providers";
export type { Prompt } from "./prompts";
diff --git a/src/lib/api/openclaw.ts b/src/lib/api/openclaw.ts
new file mode 100644
index 000000000..b448ec73c
--- /dev/null
+++ b/src/lib/api/openclaw.ts
@@ -0,0 +1,105 @@
+import { invoke } from "@tauri-apps/api/core";
+import type {
+ OpenClawDefaultModel,
+ OpenClawModelCatalogEntry,
+ OpenClawAgentsDefaults,
+ OpenClawEnvConfig,
+ OpenClawToolsConfig,
+} from "@/types";
+
+/**
+ * OpenClaw configuration API
+ *
+ * Manages ~/.openclaw/openclaw.json sections:
+ * - agents.defaults (model, catalog)
+ * - env (environment variables)
+ * - tools (permissions)
+ */
+export const openclawApi = {
+ // ============================================================
+ // Agents Configuration
+ // ============================================================
+
+ /**
+ * Get default model configuration (agents.defaults.model)
+ */
+ async getDefaultModel(): Promise {
+ return await invoke("get_openclaw_default_model");
+ },
+
+ /**
+ * Set default model configuration (agents.defaults.model)
+ */
+ async setDefaultModel(model: OpenClawDefaultModel): Promise {
+ return await invoke("set_openclaw_default_model", { model });
+ },
+
+ /**
+ * Get model catalog/allowlist (agents.defaults.models)
+ */
+ async getModelCatalog(): Promise | null> {
+ return await invoke("get_openclaw_model_catalog");
+ },
+
+ /**
+ * Set model catalog/allowlist (agents.defaults.models)
+ */
+ async setModelCatalog(
+ catalog: Record,
+ ): Promise {
+ return await invoke("set_openclaw_model_catalog", { catalog });
+ },
+
+ /**
+ * Get full agents.defaults config (all fields)
+ */
+ async getAgentsDefaults(): Promise {
+ return await invoke("get_openclaw_agents_defaults");
+ },
+
+ /**
+ * Set full agents.defaults config (all fields)
+ */
+ async setAgentsDefaults(defaults: OpenClawAgentsDefaults): Promise {
+ return await invoke("set_openclaw_agents_defaults", { defaults });
+ },
+
+ // ============================================================
+ // Env Configuration
+ // ============================================================
+
+ /**
+ * Get env config (env section of openclaw.json)
+ */
+ async getEnv(): Promise {
+ return await invoke("get_openclaw_env");
+ },
+
+ /**
+ * Set env config (env section of openclaw.json)
+ */
+ async setEnv(env: OpenClawEnvConfig): Promise {
+ return await invoke("set_openclaw_env", { env });
+ },
+
+ // ============================================================
+ // Tools Configuration
+ // ============================================================
+
+ /**
+ * Get tools config (tools section of openclaw.json)
+ */
+ async getTools(): Promise {
+ return await invoke("get_openclaw_tools");
+ },
+
+ /**
+ * Set tools config (tools section of openclaw.json)
+ */
+ async setTools(tools: OpenClawToolsConfig): Promise {
+ return await invoke("set_openclaw_tools", { tools });
+ },
+};
diff --git a/src/lib/api/providers.ts b/src/lib/api/providers.ts
index dd154b4e5..d78ed0224 100644
--- a/src/lib/api/providers.ts
+++ b/src/lib/api/providers.ts
@@ -98,6 +98,14 @@ export const providersApi = {
async getOpenCodeLiveProviderIds(): Promise {
return await invoke("get_opencode_live_provider_ids");
},
+
+ /**
+ * 获取 OpenClaw live 配置中的供应商 ID 列表
+ * 用于前端判断供应商是否已添加到 openclaw.json
+ */
+ async getOpenClawLiveProviderIds(): Promise {
+ return await invoke("get_openclaw_live_provider_ids");
+ },
};
// ============================================================================
diff --git a/src/lib/api/settings.ts b/src/lib/api/settings.ts
index cd8de3666..6b791659d 100644
--- a/src/lib/api/settings.ts
+++ b/src/lib/api/settings.ts
@@ -1,5 +1,5 @@
import { invoke } from "@tauri-apps/api/core";
-import type { Settings } from "@/types";
+import type { Settings, WebDavSyncSettings, RemoteSnapshotInfo } from "@/types";
import type { AppId } from "./types";
export interface ConfigTransferResult {
@@ -9,6 +9,15 @@ export interface ConfigTransferResult {
backupId?: string;
}
+export interface WebDavTestResult {
+ success: boolean;
+ message?: string;
+}
+
+export interface WebDavSyncResult {
+ status: string;
+}
+
export const settingsApi = {
async get(): Promise {
return await invoke("get_settings");
@@ -93,6 +102,42 @@ export const settingsApi = {
return await invoke("import_config_from_file", { filePath });
},
+ // ─── WebDAV v2 sync ───────────────────────────────────────
+
+ async webdavTestConnection(
+ settings: WebDavSyncSettings,
+ preserveEmptyPassword = true,
+ ): Promise {
+ return await invoke("webdav_test_connection", {
+ settings,
+ preserveEmptyPassword,
+ });
+ },
+
+ async webdavSyncUpload(): Promise {
+ return await invoke("webdav_sync_upload");
+ },
+
+ async webdavSyncDownload(): Promise {
+ return await invoke("webdav_sync_download");
+ },
+
+ async webdavSyncSaveSettings(
+ settings: WebDavSyncSettings,
+ passwordTouched = false,
+ ): Promise<{ success: boolean }> {
+ return await invoke("webdav_sync_save_settings", {
+ settings,
+ passwordTouched,
+ });
+ },
+
+ async webdavSyncFetchRemoteInfo(): Promise<
+ RemoteSnapshotInfo | { empty: true }
+ > {
+ return await invoke("webdav_sync_fetch_remote_info");
+ },
+
async syncCurrentProvidersLive(): Promise {
const result = (await invoke("sync_current_providers_live")) as {
success?: boolean;
diff --git a/src/lib/api/skills.ts b/src/lib/api/skills.ts
index b3f3b940f..151090773 100644
--- a/src/lib/api/skills.ts
+++ b/src/lib/api/skills.ts
@@ -2,7 +2,7 @@ import { invoke } from "@tauri-apps/api/core";
import type { AppId } from "@/lib/api/types";
-// ========== 类型定义 ==========
+export type AppType = "claude" | "codex" | "gemini" | "opencode" | "openclaw";
/** Skill 应用启用状态 */
export interface SkillApps {
@@ -10,6 +10,7 @@ export interface SkillApps {
codex: boolean;
gemini: boolean;
opencode: boolean;
+ openclaw: boolean;
}
/** 已安装的 Skill(v3.10.0+ 统一结构) */
diff --git a/src/lib/api/types.ts b/src/lib/api/types.ts
index 2e8e2b2ac..1fff721fc 100644
--- a/src/lib/api/types.ts
+++ b/src/lib/api/types.ts
@@ -1,2 +1,2 @@
// 前端统一使用 AppId 作为应用标识(与后端命令参数 `app` 一致)
-export type AppId = "claude" | "codex" | "gemini" | "opencode";
+export type AppId = "claude" | "codex" | "gemini" | "opencode" | "openclaw";
diff --git a/src/lib/api/workspace.ts b/src/lib/api/workspace.ts
new file mode 100644
index 000000000..095913483
--- /dev/null
+++ b/src/lib/api/workspace.ts
@@ -0,0 +1,11 @@
+import { invoke } from "@tauri-apps/api/core";
+
+export const workspaceApi = {
+ async readFile(filename: string): Promise {
+ return invoke("read_workspace_file", { filename });
+ },
+
+ async writeFile(filename: string, content: string): Promise {
+ return invoke("write_workspace_file", { filename, content });
+ },
+};
diff --git a/src/lib/query/mutations.ts b/src/lib/query/mutations.ts
index 89e3e486b..2b21ab330 100644
--- a/src/lib/query/mutations.ts
+++ b/src/lib/query/mutations.ts
@@ -5,6 +5,7 @@ import { providersApi, settingsApi, type AppId } from "@/lib/api";
import type { Provider, Settings } from "@/types";
import { extractErrorMessage } from "@/utils/errorUtils";
import { generateUUID } from "@/utils/uuid";
+import { openclawKeys } from "@/hooks/useOpenClaw";
export const useAddProviderMutation = (appId: AppId) => {
const queryClient = useQueryClient();
@@ -16,12 +17,12 @@ export const useAddProviderMutation = (appId: AppId) => {
) => {
let id: string;
- if (appId === "opencode") {
+ if (appId === "opencode" || appId === "openclaw") {
if (providerInput.category === "omo") {
id = `omo-${generateUUID()}`;
} else {
if (!providerInput.providerKey) {
- throw new Error("Provider key is required for OpenCode");
+ throw new Error(`Provider key is required for ${appId}`);
}
id = providerInput.providerKey;
}
@@ -29,8 +30,10 @@ export const useAddProviderMutation = (appId: AppId) => {
id = generateUUID();
}
+ const { providerKey: _providerKey, ...rest } = providerInput;
+
const newProvider: Provider = {
- ...providerInput,
+ ...rest,
id,
createdAt: Date.now(),
};
@@ -174,6 +177,7 @@ export const useSwitchProviderMutation = (appId: AppId) => {
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["providers", appId] });
+ // OpenCode/OpenClaw: also invalidate live provider IDs cache to update button state
if (appId === "opencode") {
await queryClient.invalidateQueries({
queryKey: ["opencodeLiveProviderIds"],
@@ -182,6 +186,14 @@ export const useSwitchProviderMutation = (appId: AppId) => {
queryKey: ["omo", "current-provider-id"],
});
}
+ if (appId === "openclaw") {
+ await queryClient.invalidateQueries({
+ queryKey: openclawKeys.liveProviderIds,
+ });
+ await queryClient.invalidateQueries({
+ queryKey: openclawKeys.defaultModel,
+ });
+ }
try {
await providersApi.updateTrayMenu();
diff --git a/src/lib/query/queryClient.ts b/src/lib/query/queryClient.ts
index 2d3a9e2b8..ed5001759 100644
--- a/src/lib/query/queryClient.ts
+++ b/src/lib/query/queryClient.ts
@@ -4,8 +4,8 @@ export const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 1,
- refetchOnWindowFocus: false,
- staleTime: 1000 * 60 * 5,
+ refetchOnWindowFocus: true,
+ staleTime: 0,
},
mutations: {
retry: false,
diff --git a/src/lib/schemas/settings.ts b/src/lib/schemas/settings.ts
index 74db80585..d226ef34c 100644
--- a/src/lib/schemas/settings.ts
+++ b/src/lib/schemas/settings.ts
@@ -28,6 +28,27 @@ export const settingsSchema = z.object({
// Skill 同步设置
skillSyncMethod: z.enum(["auto", "symlink", "copy"]).optional(),
+
+ // WebDAV v2 同步设置(通过专用命令保存,schema 仅用于读取)
+ webdavSync: z
+ .object({
+ enabled: z.boolean().optional(),
+ baseUrl: z.string().trim().optional().or(z.literal("")),
+ username: z.string().trim().optional().or(z.literal("")),
+ password: z.string().optional(),
+ remoteRoot: z.string().trim().optional().or(z.literal("")),
+ profile: z.string().trim().optional().or(z.literal("")),
+ status: z
+ .object({
+ lastSyncAt: z.number().nullable().optional(),
+ lastError: z.string().nullable().optional(),
+ lastRemoteEtag: z.string().nullable().optional(),
+ lastLocalManifestHash: z.string().nullable().optional(),
+ lastRemoteManifestHash: z.string().nullable().optional(),
+ })
+ .optional(),
+ })
+ .optional(),
});
export type SettingsFormData = z.infer;
diff --git a/src/types.ts b/src/types.ts
index 94af23ea7..d42b86ee4 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -160,6 +160,37 @@ export interface VisibleApps {
codex: boolean;
gemini: boolean;
opencode: boolean;
+ openclaw: boolean;
+}
+
+// WebDAV v2 同步状态
+export interface WebDavSyncStatus {
+ lastSyncAt?: number | null;
+ lastError?: string | null;
+ lastRemoteEtag?: string | null;
+ lastLocalManifestHash?: string | null;
+ lastRemoteManifestHash?: string | null;
+}
+
+// WebDAV v2 同步配置
+export interface WebDavSyncSettings {
+ enabled?: boolean;
+ baseUrl?: string;
+ username?: string;
+ password?: string;
+ remoteRoot?: string;
+ profile?: string;
+ status?: WebDavSyncStatus;
+}
+
+// 远端快照信息(下载前预览)
+export interface RemoteSnapshotInfo {
+ deviceName: string;
+ createdAt: string;
+ snapshotId: string;
+ version: number;
+ compatible: boolean;
+ artifacts: string[];
}
// 应用设置类型(用于设置对话框与 Tauri API)
@@ -193,6 +224,8 @@ export interface Settings {
geminiConfigDir?: string;
// 覆盖 OpenCode 配置目录(可选)
opencodeConfigDir?: string;
+ // 覆盖 OpenClaw 配置目录(可选)
+ openclawConfigDir?: string;
// ===== 当前供应商 ID(设备级)=====
// 当前 Claude 供应商 ID(优先于数据库 is_current)
@@ -206,6 +239,9 @@ export interface Settings {
// Skill 同步方式:auto(默认,优先 symlink)、symlink、copy
skillSyncMethod?: SkillSyncMethod;
+ // ===== WebDAV v2 同步设置 =====
+ webdavSync?: WebDavSyncSettings;
+
// ===== 终端设置 =====
// 首选终端应用(可选,默认使用系统默认终端)
// macOS: "terminal" | "iterm2" | "warp" | "alacritty" | "kitty" | "ghostty"
@@ -254,6 +290,7 @@ export interface McpApps {
codex: boolean;
gemini: boolean;
opencode: boolean;
+ openclaw: boolean;
}
// MCP 服务器条目(v3.7.0 统一结构)
@@ -391,3 +428,64 @@ export interface OpenCodeMcpServerSpec {
// 通用字段
enabled?: boolean;
}
+
+// ============================================================================
+// OpenClaw 专属配置(v3.11.0+)
+// ============================================================================
+
+// OpenClaw 模型配置
+export interface OpenClawModel {
+ id: string;
+ name: string;
+ alias?: string;
+ reasoning?: boolean; // 是否支持推理模式(如 o1、DeepSeek R1)
+ input?: string[]; // 支持的输入类型(如 ["text"]、["text", "image"])
+ cost?: {
+ input: number;
+ output: number;
+ cacheRead?: number; // 缓存读取价格
+ cacheWrite?: number; // 缓存写入价格
+ };
+ contextWindow?: number;
+ maxTokens?: number; // 最大输出 token 数
+}
+
+// OpenClaw 默认模型配置(agents.defaults.model)
+export interface OpenClawDefaultModel {
+ primary: string;
+ fallbacks?: string[];
+}
+
+// OpenClaw 模型目录条目(agents.defaults.models 中的值)
+export interface OpenClawModelCatalogEntry {
+ alias?: string;
+}
+
+// OpenClaw 供应商配置(settings_config 结构)
+// 对应 OpenClaw 的 models.providers. 配置
+export interface OpenClawProviderConfig {
+ baseUrl?: string; // API 端点
+ apiKey?: string; // API 密钥
+ api?: string; // API 协议类型(如 "openai-completions"、"anthropic")
+ models?: OpenClawModel[]; // 可用模型列表
+}
+
+// OpenClaw agents.defaults 完整配置
+export interface OpenClawAgentsDefaults {
+ model?: OpenClawDefaultModel;
+ models?: Record;
+ [key: string]: unknown; // preserve unknown fields
+}
+
+// OpenClaw env 配置(openclaw.json 的 env 节点)
+export interface OpenClawEnvConfig {
+ [key: string]: unknown;
+}
+
+// OpenClaw tools 配置(openclaw.json 的 tools 节点)
+export interface OpenClawToolsConfig {
+ profile?: string;
+ allow?: string[];
+ deny?: string[];
+ [key: string]: unknown; // preserve unknown fields
+}
diff --git a/src/types/proxy.ts b/src/types/proxy.ts
index 705c2288c..7aa6f42de 100644
--- a/src/types/proxy.ts
+++ b/src/types/proxy.ts
@@ -46,6 +46,7 @@ export interface ProxyTakeoverStatus {
codex: boolean;
gemini: boolean;
opencode: boolean;
+ openclaw: boolean;
}
export interface ProviderHealth {
diff --git a/tests/components/McpFormModal.test.tsx b/tests/components/McpFormModal.test.tsx
index c70951186..8816ceaf9 100644
--- a/tests/components/McpFormModal.test.tsx
+++ b/tests/components/McpFormModal.test.tsx
@@ -433,6 +433,7 @@ type = "stdio"
codex: false,
gemini: false,
opencode: false,
+ openclaw: false,
});
expect(onSave).toHaveBeenCalledTimes(1);
expect(toastErrorMock).not.toHaveBeenCalled();
diff --git a/tests/components/SettingsDialog.test.tsx b/tests/components/SettingsDialog.test.tsx
index 2e8015843..ec29f17fd 100644
--- a/tests/components/SettingsDialog.test.tsx
+++ b/tests/components/SettingsDialog.test.tsx
@@ -1,7 +1,8 @@
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import "@testing-library/jest-dom";
-import { createContext, useContext } from "react";
+import { createContext, useContext, type ComponentProps } from "react";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { SettingsPage } from "@/components/settings/SettingsPage";
const toastSuccessMock = vi.fn();
@@ -156,6 +157,7 @@ vi.mock("@/components/ui/dialog", () => ({
DialogHeader: ({ children }: any) => {children}
,
DialogFooter: ({ children }: any) => {children}
,
DialogTitle: ({ children }: any) => {children}
,
+ DialogDescription: ({ children }: any) => {children}
,
}));
vi.mock("@/components/ui/tabs", () => {
@@ -235,8 +237,29 @@ vi.mock("@/components/settings/AboutSection", () => ({
AboutSection: ({ isPortable }: any) => about:{String(isPortable)}
,
}));
+vi.mock("@/components/settings/WebdavSyncSection", () => ({
+ WebdavSyncSection: ({ config }: any) => (
+ webdav-sync-section:{config?.baseUrl ?? "none"}
+ ),
+}));
+
let settingsApi: any;
+const renderSettingsPage = (
+ props?: Partial>,
+) => {
+ const client = new QueryClient({
+ defaultOptions: {
+ queries: { retry: false },
+ },
+ });
+ return render(
+
+
+ ,
+ );
+};
+
describe("SettingsPage Component", () => {
beforeEach(async () => {
tMock.mockImplementation((key: string) => key);
@@ -263,7 +286,7 @@ describe("SettingsPage Component", () => {
it("should not render form content when loading", () => {
settingsMock = createSettingsMock({ settings: null, isLoading: true });
- render();
+ renderSettingsPage();
expect(screen.queryByText("language:zh")).not.toBeInTheDocument();
// 加载状态下显示 spinner 而不是表单内容
@@ -271,13 +294,24 @@ describe("SettingsPage Component", () => {
});
it("should reset import/export status when dialog transitions to open", () => {
+ const client = new QueryClient({
+ defaultOptions: {
+ queries: { retry: false },
+ },
+ });
const { rerender } = render(
- ,
+
+
+ ,
);
importExportMock.resetStatus.mockClear();
- rerender();
+ rerender(
+
+
+ ,
+ );
expect(importExportMock.resetStatus).toHaveBeenCalledTimes(1);
});
@@ -289,7 +323,7 @@ describe("SettingsPage Component", () => {
selectedFile: "/tmp/config.json",
});
- render();
+ renderSettingsPage({ onOpenChange });
expect(screen.getByText("language:zh")).toBeInTheDocument();
expect(screen.getByText("theme-settings")).toBeInTheDocument();
@@ -306,6 +340,7 @@ describe("SettingsPage Component", () => {
fireEvent.click(screen.getByText("settings.tabAdvanced"));
fireEvent.click(screen.getByText("settings.advanced.data.title"));
+ expect(screen.getByText("webdav-sync-section:none")).toBeInTheDocument();
// 有文件时,点击导入按钮执行 importConfig
fireEvent.click(
@@ -326,13 +361,7 @@ describe("SettingsPage Component", () => {
it("should pass onImportSuccess callback to useImportExport hook", async () => {
const onImportSuccess = vi.fn();
- render(
- ,
- );
+ renderSettingsPage({ onImportSuccess });
expect(useImportExportSpy).toHaveBeenCalledWith(
expect.objectContaining({ onImportSuccess }),
@@ -349,7 +378,7 @@ describe("SettingsPage Component", () => {
const onOpenChange = vi.fn();
importExportMock = createImportExportMock();
- render();
+ renderSettingsPage({ onOpenChange });
// 保存按钮在 advanced tab 中
fireEvent.click(screen.getByText("settings.tabAdvanced"));
@@ -370,7 +399,7 @@ describe("SettingsPage Component", () => {
saveSettings: vi.fn().mockResolvedValue({ requiresRestart: true }),
});
- render();
+ renderSettingsPage();
expect(
await screen.findByText("settings.restartRequired"),
@@ -390,7 +419,7 @@ describe("SettingsPage Component", () => {
const onOpenChange = vi.fn();
settingsMock = createSettingsMock({ requiresRestart: true });
- render();
+ renderSettingsPage({ onOpenChange });
expect(
await screen.findByText("settings.restartRequired"),
@@ -409,7 +438,7 @@ describe("SettingsPage Component", () => {
});
it("should trigger directory management callbacks inside advanced tab", () => {
- render();
+ renderSettingsPage();
fireEvent.click(screen.getByText("settings.tabAdvanced"));
fireEvent.click(screen.getByText("settings.advanced.configDir.title"));
diff --git a/tests/components/WebdavSyncSection.test.tsx b/tests/components/WebdavSyncSection.test.tsx
new file mode 100644
index 000000000..27a487a7b
--- /dev/null
+++ b/tests/components/WebdavSyncSection.test.tsx
@@ -0,0 +1,370 @@
+import { render, screen, fireEvent, waitFor } from "@testing-library/react";
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import "@testing-library/jest-dom";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+
+import { WebdavSyncSection } from "@/components/settings/WebdavSyncSection";
+import type { WebDavSyncSettings } from "@/types";
+
+const toastSuccessMock = vi.fn();
+const toastErrorMock = vi.fn();
+const toastWarningMock = vi.fn();
+const toastInfoMock = vi.fn();
+
+vi.mock("sonner", () => ({
+ toast: {
+ success: (...args: unknown[]) => toastSuccessMock(...args),
+ error: (...args: unknown[]) => toastErrorMock(...args),
+ warning: (...args: unknown[]) => toastWarningMock(...args),
+ info: (...args: unknown[]) => toastInfoMock(...args),
+ },
+}));
+
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
+ t: (key: string) => key,
+ }),
+}));
+
+vi.mock("@/components/ui/button", () => ({
+ Button: ({ children, ...props }: any) => ,
+}));
+
+vi.mock("@/components/ui/input", () => ({
+ Input: (props: any) => ,
+}));
+
+vi.mock("@/components/ui/select", () => ({
+ Select: ({ value, onValueChange, children }: any) => (
+
+ ),
+ SelectTrigger: ({ children }: any) => <>{children}>,
+ SelectValue: () => null,
+ SelectContent: ({ children }: any) => <>{children}>,
+ SelectItem: ({ value, children }: any) => (
+ {children}
+ ),
+}));
+
+vi.mock("@/components/ui/dialog", () => ({
+ Dialog: ({ open, children }: any) => (open ? {children}
: null),
+ DialogContent: ({ children }: any) => {children}
,
+ DialogDescription: ({ children }: any) => {children}
,
+ DialogFooter: ({ children }: any) => {children}
,
+ DialogHeader: ({ children }: any) => {children}
,
+ DialogTitle: ({ children }: any) => {children}
,
+}));
+
+const { settingsApiMock } = vi.hoisted(() => ({
+ settingsApiMock: {
+ webdavTestConnection: vi.fn(),
+ webdavSyncSaveSettings: vi.fn(),
+ webdavSyncFetchRemoteInfo: vi.fn(),
+ webdavSyncUpload: vi.fn(),
+ webdavSyncDownload: vi.fn(),
+ },
+}));
+
+vi.mock("@/lib/api", () => ({
+ settingsApi: settingsApiMock,
+}));
+
+const baseConfig: WebDavSyncSettings = {
+ enabled: true,
+ baseUrl: "https://dav.example.com/dav/",
+ username: "alice",
+ password: "secret",
+ remoteRoot: "cc-switch-sync",
+ profile: "default",
+ status: {},
+};
+
+function renderSection(config?: WebDavSyncSettings) {
+ const client = new QueryClient({
+ defaultOptions: {
+ queries: { retry: false },
+ mutations: { retry: false },
+ },
+ });
+ return render(
+
+
+ ,
+ );
+}
+
+describe("WebdavSyncSection", () => {
+ beforeEach(() => {
+ toastSuccessMock.mockReset();
+ toastErrorMock.mockReset();
+ toastWarningMock.mockReset();
+ toastInfoMock.mockReset();
+ settingsApiMock.webdavTestConnection.mockReset();
+ settingsApiMock.webdavSyncSaveSettings.mockReset();
+ settingsApiMock.webdavSyncFetchRemoteInfo.mockReset();
+ settingsApiMock.webdavSyncUpload.mockReset();
+ settingsApiMock.webdavSyncDownload.mockReset();
+
+ settingsApiMock.webdavSyncSaveSettings.mockResolvedValue({ success: true });
+ settingsApiMock.webdavTestConnection.mockResolvedValue({
+ success: true,
+ message: "ok",
+ });
+ settingsApiMock.webdavSyncFetchRemoteInfo.mockResolvedValue({
+ deviceName: "My MacBook",
+ createdAt: "2026-02-01T10:00:00Z",
+ snapshotId: "snapshot-1",
+ version: 2,
+ compatible: true,
+ artifacts: ["db.sql", "skills.zip"],
+ });
+ settingsApiMock.webdavSyncUpload.mockResolvedValue({ status: "uploaded" });
+ settingsApiMock.webdavSyncDownload.mockResolvedValue({ status: "downloaded" });
+ });
+
+ it("shows validation error when saving without base url", async () => {
+ renderSection({ ...baseConfig, baseUrl: "" });
+
+ fireEvent.click(screen.getByRole("button", { name: "settings.webdavSync.save" }));
+
+ expect(toastErrorMock).toHaveBeenCalledWith("settings.webdavSync.missingUrl");
+ expect(settingsApiMock.webdavSyncSaveSettings).not.toHaveBeenCalled();
+ });
+
+ it("saves settings and auto tests connection", async () => {
+ renderSection(baseConfig);
+
+ fireEvent.click(screen.getByRole("button", { name: "settings.webdavSync.save" }));
+
+ await waitFor(() => {
+ expect(settingsApiMock.webdavSyncSaveSettings).toHaveBeenCalledTimes(1);
+ });
+ expect(settingsApiMock.webdavSyncSaveSettings).toHaveBeenCalledWith(
+ expect.objectContaining({
+ baseUrl: "https://dav.example.com/dav/",
+ username: "alice",
+ password: "secret",
+ }),
+ false,
+ );
+ await waitFor(() => {
+ expect(settingsApiMock.webdavTestConnection).toHaveBeenCalledWith(
+ expect.objectContaining({
+ baseUrl: "https://dav.example.com/dav/",
+ }),
+ true,
+ );
+ });
+ expect(toastSuccessMock).toHaveBeenCalledWith(
+ "settings.webdavSync.saveAndTestSuccess",
+ );
+ });
+
+ it("blocks upload when there are unsaved changes", async () => {
+ renderSection(baseConfig);
+
+ fireEvent.change(
+ screen.getByPlaceholderText("settings.webdavSync.usernamePlaceholder"),
+ { target: { value: "bob" } },
+ );
+ fireEvent.click(
+ screen.getByRole("button", { name: "settings.webdavSync.upload" }),
+ );
+
+ expect(toastErrorMock).toHaveBeenCalledWith(
+ "settings.webdavSync.unsavedChanges",
+ );
+ expect(settingsApiMock.webdavSyncFetchRemoteInfo).not.toHaveBeenCalled();
+ });
+
+ it("disables sync buttons until config is saved", () => {
+ renderSection(undefined);
+
+ const uploadButton = screen.getByRole("button", {
+ name: "settings.webdavSync.upload",
+ });
+ const downloadButton = screen.getByRole("button", {
+ name: "settings.webdavSync.download",
+ });
+
+ expect(uploadButton).toBeDisabled();
+ expect(downloadButton).toBeDisabled();
+ expect(settingsApiMock.webdavSyncFetchRemoteInfo).not.toHaveBeenCalled();
+ });
+
+ it("fetches remote info and uploads after confirmation", async () => {
+ renderSection(baseConfig);
+
+ fireEvent.click(
+ screen.getByRole("button", { name: "settings.webdavSync.upload" }),
+ );
+
+ await waitFor(() => {
+ expect(settingsApiMock.webdavSyncFetchRemoteInfo).toHaveBeenCalledTimes(1);
+ });
+
+ fireEvent.click(
+ screen.getByRole("button", {
+ name: "settings.webdavSync.confirmUpload.confirm",
+ }),
+ );
+
+ await waitFor(() => {
+ expect(settingsApiMock.webdavSyncUpload).toHaveBeenCalledTimes(1);
+ });
+ expect(toastSuccessMock).toHaveBeenCalledWith(
+ "settings.webdavSync.uploadSuccess",
+ );
+ });
+
+ it("blocks upload confirmation if form changes after dialog opens", async () => {
+ renderSection(baseConfig);
+
+ fireEvent.click(
+ screen.getByRole("button", { name: "settings.webdavSync.upload" }),
+ );
+ await waitFor(() => {
+ expect(settingsApiMock.webdavSyncFetchRemoteInfo).toHaveBeenCalledTimes(1);
+ });
+
+ fireEvent.change(screen.getByPlaceholderText("cc-switch-sync"), {
+ target: { value: "new-root" },
+ });
+ fireEvent.click(
+ screen.getByRole("button", {
+ name: "settings.webdavSync.confirmUpload.confirm",
+ }),
+ );
+
+ await waitFor(() => {
+ expect(toastErrorMock).toHaveBeenCalledWith(
+ "settings.webdavSync.unsavedChanges",
+ );
+ });
+ expect(settingsApiMock.webdavSyncUpload).not.toHaveBeenCalled();
+ });
+
+ it("fetches remote info and downloads after confirmation", async () => {
+ renderSection(baseConfig);
+
+ fireEvent.click(
+ screen.getByRole("button", { name: "settings.webdavSync.download" }),
+ );
+
+ await waitFor(() => {
+ expect(settingsApiMock.webdavSyncFetchRemoteInfo).toHaveBeenCalledTimes(1);
+ });
+
+ fireEvent.click(
+ screen.getByRole("button", {
+ name: "settings.webdavSync.confirmDownload.confirm",
+ }),
+ );
+
+ await waitFor(() => {
+ expect(settingsApiMock.webdavSyncDownload).toHaveBeenCalledTimes(1);
+ });
+ expect(toastSuccessMock).toHaveBeenCalledWith(
+ "settings.webdavSync.downloadSuccess",
+ );
+ });
+
+ it("blocks download confirmation if form changes after dialog opens", async () => {
+ renderSection(baseConfig);
+
+ fireEvent.click(
+ screen.getByRole("button", { name: "settings.webdavSync.download" }),
+ );
+ await waitFor(() => {
+ expect(settingsApiMock.webdavSyncFetchRemoteInfo).toHaveBeenCalledTimes(1);
+ });
+
+ fireEvent.change(screen.getByPlaceholderText("default"), {
+ target: { value: "new-profile" },
+ });
+ fireEvent.click(
+ screen.getByRole("button", {
+ name: "settings.webdavSync.confirmDownload.confirm",
+ }),
+ );
+
+ await waitFor(() => {
+ expect(toastErrorMock).toHaveBeenCalledWith(
+ "settings.webdavSync.unsavedChanges",
+ );
+ });
+ expect(settingsApiMock.webdavSyncDownload).not.toHaveBeenCalled();
+ });
+
+ it("shows info when no remote snapshot is found for download", async () => {
+ settingsApiMock.webdavSyncFetchRemoteInfo.mockResolvedValueOnce({ empty: true });
+ renderSection(baseConfig);
+
+ fireEvent.click(
+ screen.getByRole("button", { name: "settings.webdavSync.download" }),
+ );
+
+ await waitFor(() => {
+ expect(toastInfoMock).toHaveBeenCalledWith("settings.webdavSync.noRemoteData");
+ });
+ expect(settingsApiMock.webdavSyncDownload).not.toHaveBeenCalled();
+ });
+
+ it("blocks download when remote snapshot is incompatible", async () => {
+ settingsApiMock.webdavSyncFetchRemoteInfo.mockResolvedValueOnce({
+ deviceName: "Legacy Machine",
+ createdAt: "2025-01-01T00:00:00Z",
+ snapshotId: "legacy-snapshot",
+ version: 1,
+ compatible: false,
+ artifacts: ["db.sql"],
+ });
+ renderSection(baseConfig);
+
+ fireEvent.click(
+ screen.getByRole("button", { name: "settings.webdavSync.download" }),
+ );
+
+ await waitFor(() => {
+ expect(toastErrorMock).toHaveBeenCalledWith(
+ "settings.webdavSync.incompatibleVersion",
+ );
+ });
+ expect(settingsApiMock.webdavSyncDownload).not.toHaveBeenCalled();
+ expect(
+ screen.queryByRole("button", {
+ name: "settings.webdavSync.confirmDownload.confirm",
+ }),
+ ).not.toBeInTheDocument();
+ });
+
+ it("shows error when download fails after confirmation", async () => {
+ settingsApiMock.webdavSyncDownload.mockRejectedValueOnce(new Error("boom"));
+ renderSection(baseConfig);
+
+ fireEvent.click(
+ screen.getByRole("button", { name: "settings.webdavSync.download" }),
+ );
+ await waitFor(() => {
+ expect(settingsApiMock.webdavSyncFetchRemoteInfo).toHaveBeenCalledTimes(1);
+ });
+
+ fireEvent.click(
+ screen.getByRole("button", {
+ name: "settings.webdavSync.confirmDownload.confirm",
+ }),
+ );
+
+ await waitFor(() => {
+ expect(toastErrorMock).toHaveBeenCalledWith(
+ "settings.webdavSync.downloadFailed",
+ );
+ });
+ });
+});
diff --git a/tests/integration/SettingsDialog.test.tsx b/tests/integration/SettingsDialog.test.tsx
index 0772e4a8c..be1a689f8 100644
--- a/tests/integration/SettingsDialog.test.tsx
+++ b/tests/integration/SettingsDialog.test.tsx
@@ -28,6 +28,7 @@ vi.mock("@/components/ui/dialog", () => ({
DialogHeader: ({ children }: any) => {children}
,
DialogFooter: ({ children }: any) => {children}
,
DialogTitle: ({ children }: any) => {children}
,
+ DialogDescription: ({ children }: any) => {children}
,
}));
const TabsContext = React.createContext<{
diff --git a/tests/msw/state.ts b/tests/msw/state.ts
index ac385a675..2db234840 100644
--- a/tests/msw/state.ts
+++ b/tests/msw/state.ts
@@ -58,6 +58,7 @@ const createDefaultProviders = (): ProvidersByApp => ({
},
},
opencode: {},
+ openclaw: {},
});
const createDefaultCurrent = (): CurrentProviderState => ({
@@ -65,6 +66,7 @@ const createDefaultCurrent = (): CurrentProviderState => ({
codex: "codex-1",
gemini: "gemini-1",
opencode: "",
+ openclaw: "",
});
let providers = createDefaultProviders();
@@ -84,7 +86,7 @@ let mcpConfigs: McpConfigState = {
id: "sample",
name: "Sample Claude Server",
enabled: true,
- apps: { claude: true, codex: false, gemini: false, opencode: false },
+ apps: { claude: true, codex: false, gemini: false, opencode: false, openclaw: false },
server: {
type: "stdio",
command: "claude-server",
@@ -96,7 +98,7 @@ let mcpConfigs: McpConfigState = {
id: "httpServer",
name: "HTTP Codex Server",
enabled: false,
- apps: { claude: false, codex: true, gemini: false, opencode: false },
+ apps: { claude: false, codex: true, gemini: false, opencode: false, openclaw: false },
server: {
type: "http",
url: "http://localhost:3000",
@@ -105,6 +107,7 @@ let mcpConfigs: McpConfigState = {
},
gemini: {},
opencode: {},
+ openclaw: {},
};
const cloneProviders = (value: ProvidersByApp) =>
@@ -128,7 +131,7 @@ export const resetProviderState = () => {
id: "sample",
name: "Sample Claude Server",
enabled: true,
- apps: { claude: true, codex: false, gemini: false, opencode: false },
+ apps: { claude: true, codex: false, gemini: false, opencode: false, openclaw: false },
server: {
type: "stdio",
command: "claude-server",
@@ -140,7 +143,7 @@ export const resetProviderState = () => {
id: "httpServer",
name: "HTTP Codex Server",
enabled: false,
- apps: { claude: false, codex: true, gemini: false, opencode: false },
+ apps: { claude: false, codex: true, gemini: false, opencode: false, openclaw: false },
server: {
type: "http",
url: "http://localhost:3000",
@@ -149,6 +152,7 @@ export const resetProviderState = () => {
},
gemini: {},
opencode: {},
+ openclaw: {},
};
};