From 2b1f0f0f7e61de45fb2549f0315ae3af8c3c9531 Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 7 Feb 2026 23:52:54 +0800 Subject: [PATCH] feat(openclaw): add Env/Tools/Agents config panels - Migrate OpenClaw commands from provider.rs to dedicated commands/openclaw.rs - Add backend types and read/write for env, tools, agents.defaults sections - Create EnvPanel (API key + custom vars KV editor) - Create ToolsPanel (profile selector + allow/deny lists) - Create AgentsDefaultsPanel (default model + runtime parameters) - Extend App.tsx menu bar with Env/Tools/Agents buttons - Remove Prompts button for OpenClaw (overlaps with Workspace AGENTS.md) --- src-tauri/src/app_config.rs | 7 +- src-tauri/src/commands/mod.rs | 2 + src-tauri/src/commands/openclaw.rs | 108 +++++++++ src-tauri/src/commands/provider.rs | 57 +---- src-tauri/src/lib.rs | 8 +- src-tauri/src/openclaw_config.rs | 135 ++++++++++- src-tauri/src/prompt_files.rs | 2 +- src-tauri/src/services/provider/live.rs | 5 +- src-tauri/src/services/provider/mod.rs | 4 +- src/App.tsx | 71 +++++- .../openclaw/AgentsDefaultsPanel.tsx | 223 ++++++++++++++++++ src/components/openclaw/EnvPanel.tsx | 179 ++++++++++++++ src/components/openclaw/ToolsPanel.tsx | 204 ++++++++++++++++ src/lib/api/openclaw.ts | 73 +++++- src/types.ts | 20 ++ 15 files changed, 1017 insertions(+), 81 deletions(-) create mode 100644 src-tauri/src/commands/openclaw.rs create mode 100644 src/components/openclaw/AgentsDefaultsPanel.tsx create mode 100644 src/components/openclaw/EnvPanel.tsx create mode 100644 src/components/openclaw/ToolsPanel.tsx diff --git a/src-tauri/src/app_config.rs b/src-tauri/src/app_config.rs index 323f8a511..cf76e1fc8 100644 --- a/src-tauri/src/app_config.rs +++ b/src-tauri/src/app_config.rs @@ -735,7 +735,12 @@ impl MultiAppConfig { let mut conflicts = Vec::new(); // 收集所有应用的 MCP - for app in [AppType::Claude, AppType::Codex, AppType::Gemini, AppType::OpenCode] { + 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, diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 6a102b864..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; @@ -31,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::*; 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, String> +{ + openclaw_config::get_default_model().map_err(|e| e.to_string()) +} + +/// Set OpenClaw default model config (agents.defaults.model) +#[tauri::command] +pub fn set_openclaw_default_model( + model: openclaw_config::OpenClawDefaultModel, +) -> Result<(), String> { + openclaw_config::set_default_model(&model).map_err(|e| e.to_string()) +} + +/// Get OpenClaw model catalog/allowlist (agents.defaults.models) +#[tauri::command] +pub fn get_openclaw_model_catalog( +) -> Result>, String> { + openclaw_config::get_model_catalog().map_err(|e| e.to_string()) +} + +/// Set OpenClaw model catalog/allowlist (agents.defaults.models) +#[tauri::command] +pub fn set_openclaw_model_catalog( + catalog: HashMap, +) -> Result<(), String> { + openclaw_config::set_model_catalog(&catalog).map_err(|e| e.to_string()) +} + +/// Get full agents.defaults config (all fields) +#[tauri::command] +pub fn get_openclaw_agents_defaults( +) -> Result, String> { + openclaw_config::get_agents_defaults().map_err(|e| e.to_string()) +} + +/// Set full agents.defaults config (all fields) +#[tauri::command] +pub fn set_openclaw_agents_defaults( + defaults: openclaw_config::OpenClawAgentsDefaults, +) -> Result<(), String> { + openclaw_config::set_agents_defaults(&defaults).map_err(|e| e.to_string()) +} + +// ============================================================================ +// Env Configuration Commands +// ============================================================================ + +/// Get OpenClaw env config (env section of openclaw.json) +#[tauri::command] +pub fn get_openclaw_env() -> Result { + openclaw_config::get_env_config().map_err(|e| e.to_string()) +} + +/// Set OpenClaw env config (env section of openclaw.json) +#[tauri::command] +pub fn set_openclaw_env(env: openclaw_config::OpenClawEnvConfig) -> Result<(), String> { + openclaw_config::set_env_config(&env).map_err(|e| e.to_string()) +} + +// ============================================================================ +// Tools Configuration Commands +// ============================================================================ + +/// Get OpenClaw tools config (tools section of openclaw.json) +#[tauri::command] +pub fn get_openclaw_tools() -> Result { + openclaw_config::get_tools_config().map_err(|e| e.to_string()) +} + +/// Set OpenClaw tools config (tools section of openclaw.json) +#[tauri::command] +pub fn set_openclaw_tools(tools: openclaw_config::OpenClawToolsConfig) -> Result<(), String> { + openclaw_config::set_tools_config(&tools).map_err(|e| e.to_string()) +} diff --git a/src-tauri/src/commands/provider.rs b/src-tauri/src/commands/provider.rs index d6d51397d..da9a7ea96 100644 --- a/src-tauri/src/commands/provider.rs +++ b/src-tauri/src/commands/provider.rs @@ -320,60 +320,5 @@ pub fn get_opencode_live_provider_ids() -> Result, String> { } // ============================================================================ -// OpenClaw 专属命令 +// OpenClaw 专属命令 → 已迁移至 commands/openclaw.rs // ============================================================================ - -/// 从 OpenClaw live 配置导入供应商到数据库 -/// -/// 这是 OpenClaw 特有的功能,因为 OpenClaw 使用累加模式, -/// 用户可能已经在 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()) -} - -/// 获取 OpenClaw live 配置中的供应商 ID 列表 -/// -/// 用于前端判断供应商是否已添加到 openclaw.json -#[tauri::command] -pub fn get_openclaw_live_provider_ids() -> Result, String> { - crate::openclaw_config::get_providers() - .map(|providers| providers.keys().cloned().collect()) - .map_err(|e| e.to_string()) -} - -// ============================================================================ -// OpenClaw Agents Configuration Commands -// ============================================================================ - -/// 获取 OpenClaw 默认模型配置(agents.defaults.model) -#[tauri::command] -pub fn get_openclaw_default_model( -) -> Result, String> { - crate::openclaw_config::get_default_model().map_err(|e| e.to_string()) -} - -/// 设置 OpenClaw 默认模型配置(agents.defaults.model) -#[tauri::command] -pub fn set_openclaw_default_model( - model: crate::openclaw_config::OpenClawDefaultModel, -) -> Result<(), String> { - crate::openclaw_config::set_default_model(&model).map_err(|e| e.to_string()) -} - -/// 获取 OpenClaw 模型目录/允许列表(agents.defaults.models) -#[tauri::command] -pub fn get_openclaw_model_catalog( -) -> Result>, String> -{ - crate::openclaw_config::get_model_catalog().map_err(|e| e.to_string()) -} - -/// 设置 OpenClaw 模型目录/允许列表(agents.defaults.models) -#[tauri::command] -pub fn set_openclaw_model_catalog( - catalog: std::collections::HashMap, -) -> Result<(), String> { - crate::openclaw_config::set_model_catalog(&catalog).map_err(|e| e.to_string()) -} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 26c2f4cbd..70b05b617 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -13,8 +13,8 @@ mod gemini_config; mod gemini_mcp; mod init_status; mod mcp; -mod opencode_config; mod openclaw_config; +mod opencode_config; mod panic_hook; mod prompt; mod prompt_files; @@ -1013,6 +1013,12 @@ pub fn run() { commands::set_openclaw_default_model, commands::get_openclaw_model_catalog, commands::set_openclaw_model_catalog, + commands::get_openclaw_agents_defaults, + commands::set_openclaw_agents_defaults, + commands::get_openclaw_env, + commands::set_openclaw_env, + commands::get_openclaw_tools, + commands::set_openclaw_tools, // Global upstream proxy commands::get_global_proxy_url, commands::set_global_proxy_url, diff --git a/src-tauri/src/openclaw_config.rs b/src-tauri/src/openclaw_config.rs index ab6082c1d..30a51ef3f 100644 --- a/src-tauri/src/openclaw_config.rs +++ b/src-tauri/src/openclaw_config.rs @@ -220,12 +220,8 @@ pub fn read_openclaw_config() -> Result { let content = std::fs::read_to_string(&path).map_err(|e| AppError::io(&path, e))?; // 尝试 JSON5 解析(支持注释和尾随逗号) - json5::from_str(&content).map_err(|e| { - AppError::Config(format!( - "Failed to parse OpenClaw config as JSON5: {}", - e - )) - }) + json5::from_str(&content) + .map_err(|e| AppError::Config(format!("Failed to parse OpenClaw config as JSON5: {}", e))) } /// 写入 OpenClaw 配置文件(原子写入) @@ -420,3 +416,130 @@ fn ensure_agents_defaults_path(config: &mut Value) { config["agents"]["defaults"] = json!({}); } } + +// ============================================================================ +// Full Agents Defaults Functions +// ============================================================================ + +/// Read the full agents.defaults config +pub fn get_agents_defaults() -> Result, AppError> { + let config = read_openclaw_config()?; + + let Some(defaults_value) = config.get("agents").and_then(|a| a.get("defaults")) else { + return Ok(None); + }; + + let defaults = serde_json::from_value(defaults_value.clone()) + .map_err(|e| AppError::Config(format!("Failed to parse agents.defaults: {e}")))?; + + Ok(Some(defaults)) +} + +/// Write the full agents.defaults config +pub fn set_agents_defaults(defaults: &OpenClawAgentsDefaults) -> Result<(), AppError> { + let mut config = read_openclaw_config()?; + + if config.get("agents").is_none() { + config["agents"] = json!({}); + } + + let value = + serde_json::to_value(defaults).map_err(|e| AppError::JsonSerialize { source: e })?; + + config["agents"]["defaults"] = value; + + write_openclaw_config(&config) +} + +// ============================================================================ +// Env Configuration +// ============================================================================ + +/// OpenClaw env configuration (env section of openclaw.json) +/// +/// Stores environment variables like API keys and custom vars. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OpenClawEnvConfig { + /// All environment variable key-value pairs + #[serde(flatten)] + pub vars: HashMap, +} + +/// Read the env config section +pub fn get_env_config() -> Result { + let config = read_openclaw_config()?; + + let Some(env_value) = config.get("env") else { + return Ok(OpenClawEnvConfig { + vars: HashMap::new(), + }); + }; + + serde_json::from_value(env_value.clone()) + .map_err(|e| AppError::Config(format!("Failed to parse env config: {e}"))) +} + +/// Write the env config section +pub fn set_env_config(env: &OpenClawEnvConfig) -> Result<(), AppError> { + let mut config = read_openclaw_config()?; + + let value = serde_json::to_value(env).map_err(|e| AppError::JsonSerialize { source: e })?; + + config["env"] = value; + + write_openclaw_config(&config) +} + +// ============================================================================ +// Tools Configuration +// ============================================================================ + +/// OpenClaw tools configuration (tools section of openclaw.json) +/// +/// Controls tool permissions with profile-based allow/deny lists. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OpenClawToolsConfig { + /// Active permission profile (e.g. "default", "strict", "permissive") + #[serde(skip_serializing_if = "Option::is_none")] + pub profile: Option, + + /// Allowed tool patterns + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub allow: Vec, + + /// Denied tool patterns + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub deny: Vec, + + /// Other custom fields (preserve unknown fields) + #[serde(flatten)] + pub other: HashMap, +} + +/// Read the tools config section +pub fn get_tools_config() -> Result { + let config = read_openclaw_config()?; + + let Some(tools_value) = config.get("tools") else { + return Ok(OpenClawToolsConfig { + profile: None, + allow: Vec::new(), + deny: Vec::new(), + other: HashMap::new(), + }); + }; + + serde_json::from_value(tools_value.clone()) + .map_err(|e| AppError::Config(format!("Failed to parse tools config: {e}"))) +} + +/// Write the tools config section +pub fn set_tools_config(tools: &OpenClawToolsConfig) -> Result<(), AppError> { + let mut config = read_openclaw_config()?; + + let value = serde_json::to_value(tools).map_err(|e| AppError::JsonSerialize { source: e })?; + + config["tools"] = value; + + write_openclaw_config(&config) +} diff --git a/src-tauri/src/prompt_files.rs b/src-tauri/src/prompt_files.rs index 72a33baa4..24e6a9b90 100644 --- a/src-tauri/src/prompt_files.rs +++ b/src-tauri/src/prompt_files.rs @@ -5,8 +5,8 @@ use crate::codex_config::get_codex_auth_path; use crate::config::get_claude_settings_path; use crate::error::AppError; use crate::gemini_config::get_gemini_dir; -use crate::opencode_config::get_opencode_dir; use crate::openclaw_config::get_openclaw_dir; +use crate::opencode_config::get_opencode_dir; /// 返回指定应用所使用的提示词文件路径。 pub fn prompt_file_path(app: &AppType) -> Result { diff --git a/src-tauri/src/services/provider/live.rs b/src-tauri/src/services/provider/live.rs index e7f7500d3..01242fd5d 100644 --- a/src-tauri/src/services/provider/live.rs +++ b/src-tauri/src/services/provider/live.rs @@ -216,7 +216,10 @@ pub(crate) fn write_live_snapshot(app_type: &AppType, provider: &Provider) -> Re || provider.settings_config.get("api").is_some() || provider.settings_config.get("models").is_some() { - openclaw_config::set_provider(&provider.id, provider.settings_config.clone())?; + openclaw_config::set_provider( + &provider.id, + provider.settings_config.clone(), + )?; log::info!( "OpenClaw provider '{}' written as raw JSON to live config", provider.id diff --git a/src-tauri/src/services/provider/mod.rs b/src-tauri/src/services/provider/mod.rs index aa163ce3b..5f523c722 100644 --- a/src-tauri/src/services/provider/mod.rs +++ b/src-tauri/src/services/provider/mod.rs @@ -21,8 +21,8 @@ use crate::store::AppState; // Re-export sub-module functions for external access pub use live::{ - import_default_config, import_openclaw_providers_from_live, import_opencode_providers_from_live, - read_live_settings, sync_current_to_live, + import_default_config, import_openclaw_providers_from_live, + import_opencode_providers_from_live, read_live_settings, sync_current_to_live, }; // Internal re-exports (pub(crate)) diff --git a/src/App.tsx b/src/App.tsx index 2096b3fc6..626b020d3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,6 +17,9 @@ import { FolderArchive, Search, FolderOpen, + KeyRound, + Shield, + Cpu, } from "lucide-react"; import type { Provider, VisibleApps } from "@/types"; import type { EnvConflict } from "@/types/env"; @@ -58,6 +61,9 @@ import { Button } from "@/components/ui/button"; import { SessionManagerPage } from "@/components/sessions/SessionManagerPage"; import { useDisableCurrentOmo } from "@/lib/query/omo"; import WorkspaceFilesPanel from "@/components/workspace/WorkspaceFilesPanel"; +import EnvPanel from "@/components/openclaw/EnvPanel"; +import ToolsPanel from "@/components/openclaw/ToolsPanel"; +import AgentsDefaultsPanel from "@/components/openclaw/AgentsDefaultsPanel"; type View = | "providers" @@ -69,7 +75,10 @@ type View = | "agents" | "universal" | "sessions" - | "workspace"; + | "workspace" + | "openclawEnv" + | "openclawTools" + | "openclawAgents"; const DRAG_BAR_HEIGHT = isWindows() || isLinux() ? 0 : 28; // px const HEADER_HEIGHT = 64; // px @@ -104,6 +113,9 @@ const VALID_VIEWS: View[] = [ "universal", "sessions", "workspace", + "openclawEnv", + "openclawTools", + "openclawAgents", ]; const getInitialView = (): View => { @@ -637,6 +649,12 @@ function App() { return ; case "workspace": return ; + case "openclawEnv": + return ; + case "openclawTools": + return ; + case "openclawAgents": + return ; default: return (
@@ -796,6 +814,10 @@ function App() { })} {currentView === "sessions" && t("sessionManager.title")} {currentView === "workspace" && t("workspace.title")} + {currentView === "openclawEnv" && t("openclaw.env.title")} + {currentView === "openclawTools" && t("openclaw.tools.title")} + {currentView === "openclawAgents" && + t("openclaw.agents.title")}
) : ( @@ -975,15 +997,44 @@ function App() {
{activeApp === "openclaw" ? ( - + <> + + + + + ) : ( <> +
+ + ); +}; + +export default AgentsDefaultsPanel; diff --git a/src/components/openclaw/EnvPanel.tsx b/src/components/openclaw/EnvPanel.tsx new file mode 100644 index 000000000..9a36844a0 --- /dev/null +++ b/src/components/openclaw/EnvPanel.tsx @@ -0,0 +1,179 @@ +import React, { useState, useEffect, useCallback } from "react"; +import { useTranslation } from "react-i18next"; +import { Plus, Trash2, Save, Eye, EyeOff } from "lucide-react"; +import { toast } from "sonner"; +import { openclawApi } from "@/lib/api/openclaw"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import type { OpenClawEnvConfig } from "@/types"; + +interface EnvEntry { + key: string; + value: string; + isNew?: boolean; +} + +const EnvPanel: React.FC = () => { + const { t } = useTranslation(); + const [entries, setEntries] = useState([]); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [visibleKeys, setVisibleKeys] = useState>(new Set()); + + const loadEnv = useCallback(async () => { + try { + setLoading(true); + const env = await openclawApi.getEnv(); + const items: EnvEntry[] = Object.entries(env).map(([key, value]) => ({ + key, + value: String(value ?? ""), + })); + setEntries(items.length > 0 ? items : []); + } catch (err) { + toast.error(t("openclaw.env.loadFailed")); + console.error("Failed to load env config:", err); + } finally { + setLoading(false); + } + }, [t]); + + useEffect(() => { + void loadEnv(); + }, [loadEnv]); + + const handleSave = async () => { + try { + setSaving(true); + const env: OpenClawEnvConfig = {}; + for (const entry of entries) { + const trimmedKey = entry.key.trim(); + if (trimmedKey) { + env[trimmedKey] = entry.value; + } + } + await openclawApi.setEnv(env); + toast.success(t("openclaw.env.saveSuccess")); + // Reload to normalize + await loadEnv(); + } catch (err) { + toast.error(t("openclaw.env.saveFailed")); + console.error("Failed to save env config:", err); + } finally { + setSaving(false); + } + }; + + const addEntry = () => { + setEntries((prev) => [...prev, { key: "", value: "", isNew: true }]); + }; + + const removeEntry = (index: number) => { + setEntries((prev) => prev.filter((_, i) => i !== index)); + }; + + const updateEntry = (index: number, field: "key" | "value", val: string) => { + setEntries((prev) => + prev.map((entry, i) => + i === index ? { ...entry, [field]: val } : entry, + ), + ); + }; + + const toggleVisibility = (key: string) => { + setVisibleKeys((prev) => { + const next = new Set(prev); + if (next.has(key)) { + next.delete(key); + } else { + next.add(key); + } + return next; + }); + }; + + const isApiKey = (key: string) => /key|token|secret|password/i.test(key); + + if (loading) { + return ( +
+
+ {t("common.loading")} +
+
+ ); + } + + return ( +
+

+ {t("openclaw.env.description")} +

+ +
+ {entries.map((entry, index) => { + const sensitive = isApiKey(entry.key); + const visible = visibleKeys.has(`${index}`); + + return ( +
+
+ updateEntry(index, "key", e.target.value)} + placeholder={t("openclaw.env.keyPlaceholder")} + className="font-mono text-xs" + autoFocus={entry.isNew} + /> +
+
+ updateEntry(index, "value", e.target.value)} + placeholder={t("openclaw.env.valuePlaceholder")} + className="font-mono text-xs" + /> + {sensitive && ( + + )} +
+ +
+ ); + })} +
+ +
+ +
+ +
+
+ ); +}; + +export default EnvPanel; diff --git a/src/components/openclaw/ToolsPanel.tsx b/src/components/openclaw/ToolsPanel.tsx new file mode 100644 index 000000000..58830296f --- /dev/null +++ b/src/components/openclaw/ToolsPanel.tsx @@ -0,0 +1,204 @@ +import React, { useState, useEffect, useCallback } from "react"; +import { useTranslation } from "react-i18next"; +import { Plus, Trash2, Save } from "lucide-react"; +import { toast } from "sonner"; +import { openclawApi } from "@/lib/api/openclaw"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import type { OpenClawToolsConfig } from "@/types"; + +const PROFILE_OPTIONS = ["default", "strict", "permissive", "custom"]; + +const ToolsPanel: React.FC = () => { + const { t } = useTranslation(); + const [config, setConfig] = useState({}); + const [allowList, setAllowList] = useState([]); + const [denyList, setDenyList] = useState([]); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + + const loadTools = useCallback(async () => { + try { + setLoading(true); + const tools = await openclawApi.getTools(); + setConfig(tools); + setAllowList(tools.allow ?? []); + setDenyList(tools.deny ?? []); + } catch (err) { + toast.error(t("openclaw.tools.loadFailed")); + console.error("Failed to load tools config:", err); + } finally { + setLoading(false); + } + }, [t]); + + useEffect(() => { + void loadTools(); + }, [loadTools]); + + const handleSave = async () => { + try { + setSaving(true); + const { profile, allow, deny, ...other } = config; + const newConfig: OpenClawToolsConfig = { + ...other, + profile: config.profile, + allow: allowList.filter((s) => s.trim()), + deny: denyList.filter((s) => s.trim()), + }; + await openclawApi.setTools(newConfig); + toast.success(t("openclaw.tools.saveSuccess")); + await loadTools(); + } catch (err) { + toast.error(t("openclaw.tools.saveFailed")); + console.error("Failed to save tools config:", err); + } finally { + setSaving(false); + } + }; + + const updateListItem = ( + list: string[], + setList: React.Dispatch>, + index: number, + value: string, + ) => { + setList(list.map((item, i) => (i === index ? value : item))); + }; + + const removeListItem = ( + list: string[], + setList: React.Dispatch>, + index: number, + ) => { + setList(list.filter((_, i) => i !== index)); + }; + + if (loading) { + return ( +
+
+ {t("common.loading")} +
+
+ ); + } + + return ( +
+

+ {t("openclaw.tools.description")} +

+ + {/* Profile selector */} +
+ + +
+ + {/* Allow list */} +
+ +
+ {allowList.map((item, index) => ( +
+ + updateListItem(allowList, setAllowList, index, e.target.value) + } + placeholder={t("openclaw.tools.patternPlaceholder")} + className="font-mono text-xs" + /> + +
+ ))} + +
+
+ + {/* Deny list */} +
+ +
+ {denyList.map((item, index) => ( +
+ + updateListItem(denyList, setDenyList, index, e.target.value) + } + placeholder={t("openclaw.tools.patternPlaceholder")} + className="font-mono text-xs" + /> + +
+ ))} + +
+
+ + {/* Save button */} +
+ +
+
+ ); +}; + +export default ToolsPanel; diff --git a/src/lib/api/openclaw.ts b/src/lib/api/openclaw.ts index 251650a22..ae36d4037 100644 --- a/src/lib/api/openclaw.ts +++ b/src/lib/api/openclaw.ts @@ -1,12 +1,25 @@ import { invoke } from "@tauri-apps/api/core"; -import type { OpenClawDefaultModel, OpenClawModelCatalogEntry } from "@/types"; +import type { + OpenClawDefaultModel, + OpenClawModelCatalogEntry, + OpenClawAgentsDefaults, + OpenClawEnvConfig, + OpenClawToolsConfig, +} from "@/types"; /** - * OpenClaw agents configuration API + * OpenClaw configuration API * - * Manages agents.defaults configuration in ~/.openclaw/openclaw.json + * 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) */ @@ -40,6 +53,24 @@ export const openclawApi = { 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 }); + }, + + // ============================================================ + // Provider Import + // ============================================================ + /** * Import providers from live config (openclaw.json) to database */ @@ -53,4 +84,40 @@ export const openclawApi = { async getLiveProviderIds(): Promise { return await invoke("get_openclaw_live_provider_ids"); }, + + // ============================================================ + // 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/types.ts b/src/types.ts index 45b22d620..f76790c19 100644 --- a/src/types.ts +++ b/src/types.ts @@ -467,3 +467,23 @@ export interface OpenClawProviderConfig { 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 +}