diff --git a/scripts/dev-api.js b/scripts/dev-api.js index eaed7e6..583b81e 100644 --- a/scripts/dev-api.js +++ b/scripts/dev-api.js @@ -1146,16 +1146,31 @@ function getUid() { } function stripUiFields(config) { + if (!config || typeof config !== 'object' || Array.isArray(config)) return config // 清理根层级 ClawPanel 内部字段(version info 等),避免污染 openclaw.json // Issue #89: 这些字段被写入 openclaw.json 后导致 Gateway 无法启动(Unknown config keys) const uiRootKeys = [ 'current', 'latest', 'recommended', 'update_available', 'latest_update_available', 'is_recommended', 'ahead_of_recommended', - 'panel_version', 'source', + 'panel_version', 'source', 'qqbot', 'profiles', ] for (const key of uiRootKeys) { delete config[key] } + if (config.auth && typeof config.auth === 'object' && !Array.isArray(config.auth)) { + delete config.auth.profiles + } + if (config.agents && typeof config.agents === 'object' && !Array.isArray(config.agents)) { + delete config.agents.profiles + if (Array.isArray(config.agents.list)) { + for (const agent of config.agents.list) { + if (!agent || typeof agent !== 'object' || Array.isArray(agent)) continue + delete agent.current + delete agent.latest + delete agent.update_available + } + } + } // 清理模型测试相关的临时字段 const providers = config?.models?.providers if (providers) { @@ -1174,6 +1189,15 @@ function stripUiFields(config) { return config } +function cleanLoadedConfig(config) { + const before = JSON.stringify(config) + const cleaned = stripUiFields(config) + if (fs.existsSync(CONFIG_PATH) && JSON.stringify(cleaned) !== before) { + writeOpenclawConfigFile(cleaned) + } + return cleaned +} + // === Ed25519 设备密钥管理 === function getOrCreateDeviceKey() { @@ -1287,7 +1311,6 @@ function calibrationRichnessScore(config) { if (!config || typeof config !== 'object' || Array.isArray(config)) return 0 let score = 0 if (config.models?.providers && Object.keys(config.models.providers).length) score += 4 - if (config.auth?.profiles && Object.keys(config.auth.profiles).length) score += 3 if (config.agents?.defaults) score += 2 if (Array.isArray(config.agents?.list) && config.agents.list.length) score += 3 if (config.channels && Object.keys(config.channels).length) score += 2 @@ -1316,7 +1339,6 @@ function buildCalibrationBaseline() { $schema: 'https://openclaw.ai/schema/config.json', meta: { lastTouchedVersion: calibrationLastTouchedVersion() }, models: { providers: {} }, - auth: { profiles: {} }, agents: { defaults: { workspace: calibrationDefaultWorkspace() }, list: [], @@ -1382,9 +1404,6 @@ function normalizeCalibratedConfig(input) { config.models = config.models && typeof config.models === 'object' && !Array.isArray(config.models) ? config.models : {} config.models.providers = config.models.providers && typeof config.models.providers === 'object' && !Array.isArray(config.models.providers) ? config.models.providers : {} - config.auth = config.auth && typeof config.auth === 'object' && !Array.isArray(config.auth) ? config.auth : {} - config.auth.profiles = config.auth.profiles && typeof config.auth.profiles === 'object' && !Array.isArray(config.auth.profiles) ? config.auth.profiles : {} - config.agents = config.agents && typeof config.agents === 'object' && !Array.isArray(config.agents) ? config.agents : {} config.agents.defaults = config.agents.defaults && typeof config.agents.defaults === 'object' && !Array.isArray(config.agents.defaults) ? config.agents.defaults : {} if (!String(config.agents.defaults.workspace || '').trim()) config.agents.defaults.workspace = calibrationDefaultWorkspace() @@ -1571,18 +1590,18 @@ function patchGatewayOrigins() { if (!config.gateway) config.gateway = {} if (!config.gateway.controlUi) config.gateway.controlUi = {} config.gateway.controlUi.allowedOrigins = merged - fs.copyFileSync(CONFIG_PATH, CONFIG_PATH + '.bak') - fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2)) + writeOpenclawConfigFile(config) return true } function readOpenclawConfigOptional() { - return fs.existsSync(CONFIG_PATH) ? JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')) : {} + if (!fs.existsSync(CONFIG_PATH)) return {} + return cleanLoadedConfig(JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'))) } function readOpenclawConfigRequired() { if (!fs.existsSync(CONFIG_PATH)) throw new Error('openclaw.json 不存在') - return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')) + return cleanLoadedConfig(JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'))) } function mergeConfigsPreservingFields(existing, next) { @@ -1601,8 +1620,9 @@ function mergeConfigsPreservingFields(existing, next) { } function writeOpenclawConfigFile(config) { + const cleaned = stripUiFields(config) if (fs.existsSync(CONFIG_PATH)) fs.copyFileSync(CONFIG_PATH, CONFIG_PATH + '.bak') - fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2)) + fs.writeFileSync(CONFIG_PATH, JSON.stringify(cleaned, null, 2)) } function ensureAgentsList(config) { @@ -2690,9 +2710,7 @@ function serverCached(key, ttlMs, fn) { const handlers = { // 配置读写 read_openclaw_config() { - if (!fs.existsSync(CONFIG_PATH)) throw new Error('openclaw.json 不存在,请先安装 OpenClaw') - const content = fs.readFileSync(CONFIG_PATH, 'utf8') - return JSON.parse(content) + return readOpenclawConfigRequired() }, calibrate_openclaw_config({ mode } = {}) { @@ -4837,7 +4855,7 @@ const handlers = { const src = path.join(BACKUPS_DIR, name) if (!fs.existsSync(src)) throw new Error('备份不存在') if (fs.existsSync(CONFIG_PATH)) handlers.create_backup() - fs.copyFileSync(src, CONFIG_PATH) + writeOpenclawConfigFile(JSON.parse(fs.readFileSync(src, 'utf8'))) return true }, @@ -4866,8 +4884,7 @@ const handlers = { } } if (changed) { - fs.copyFileSync(CONFIG_PATH, CONFIG_PATH + '.bak') - fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2)) + writeOpenclawConfigFile(config) } return changed }, @@ -5009,12 +5026,11 @@ const handlers = { const backupPath = CONFIG_PATH + '.bak' if (fs.existsSync(backupPath)) { const backupContent = fs.readFileSync(backupPath, 'utf8') - JSON.parse(backupContent) - fs.writeFileSync(CONFIG_PATH, backupContent) + writeOpenclawConfigFile(JSON.parse(backupContent)) return { created: false, restored: true, message: '已从 openclaw.json.bak 恢复配置文件' } } const defaultConfig = stripUiFields(normalizeCalibratedConfig(buildCalibrationBaseline())) - fs.writeFileSync(CONFIG_PATH, JSON.stringify(defaultConfig, null, 2)) + writeOpenclawConfigFile(defaultConfig) return { created: true, restored: false, message: '配置文件已创建' } }, diff --git a/src-tauri/src/commands/agent.rs b/src-tauri/src/commands/agent.rs index 90b2368..aa052b9 100644 --- a/src-tauri/src/commands/agent.rs +++ b/src-tauri/src/commands/agent.rs @@ -5,6 +5,7 @@ use serde_json::json; use serde_json::Value; use std::fs; use std::io::Write; +use std::path::{Component, Path, PathBuf}; const AGENT_FILE_ALLOWLIST: &[&str] = &[ "AGENTS.md", @@ -17,6 +18,32 @@ const AGENT_FILE_ALLOWLIST: &[&str] = &[ "MEMORY.md", ]; +const WORKSPACE_TEXT_EXTENSIONS: &[&str] = &[ + "md", "markdown", "mdx", "txt", "json", "jsonc", "yaml", "yml", "toml", "ini", + "cfg", "conf", "log", "csv", "env", "gitignore", "gitattributes", "editorconfig", + "js", "mjs", "cjs", "ts", "tsx", "jsx", "html", "htm", "css", "scss", "less", + "rs", "py", "sh", "bash", "zsh", "fish", "ps1", "bat", "cmd", "sql", "xml", + "java", "kt", "go", "rb", "php", "c", "cc", "cpp", "h", "hpp", "vue", "svelte", + "lock", "sample", +]; + +const WORKSPACE_TEXT_BASENAMES: &[&str] = &[ + "dockerfile", + "makefile", + "readme", + "license", + ".env", + ".env.local", + ".env.example", + ".gitignore", + ".gitattributes", + ".editorconfig", + ".npmrc", +]; + +const WORKSPACE_PREVIEW_EXTENSIONS: &[&str] = &["md", "markdown", "mdx"]; +const MAX_WORKSPACE_FILE_SIZE: u64 = 1024 * 1024; + /// Workspace 状态信息 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WorkspaceStatus { @@ -105,13 +132,7 @@ fn check_workspace_status(path: &std::path::Path) -> WorkspaceCheckResult { /// 获取 agent 列表(直接读 openclaw.json,不走 CLI,毫秒级响应) #[tauri::command] pub async fn list_agents() -> Result { - let config_path = super::openclaw_dir().join("openclaw.json"); - if !config_path.exists() { - return Err("openclaw.json 不存在,请先安装 OpenClaw".to_string()); - } - let content = fs::read_to_string(&config_path).map_err(|e| format!("读取配置失败: {e}"))?; - let config: Value = - serde_json::from_str(&content).map_err(|e| format!("解析 JSON 失败: {e}"))?; + let config = super::config::load_openclaw_json()?; let agents_list = config .get("agents") @@ -224,10 +245,7 @@ pub async fn list_agents() -> Result { #[tauri::command] pub async fn get_agent_detail(id: String) -> Result { - let config_path = super::openclaw_dir().join("openclaw.json"); - let content = fs::read_to_string(&config_path).map_err(|e| format!("读取配置失败: {e}"))?; - let config: Value = - serde_json::from_str(&content).map_err(|e| format!("解析 JSON 失败: {e}"))?; + let config = super::config::load_openclaw_json()?; let defaults = config .get("agents") @@ -251,11 +269,9 @@ pub async fn get_agent_detail(id: String) -> Result { }) .unwrap_or_else(|| json!({ "id": id.clone(), "default": id == "main" })); - let workspace = agent - .get("workspace") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()) - .unwrap_or_else(|| resolve_agent_workspace(&id, &config)); + let workspace = resolve_agent_workspace_path(&id, &config) + .to_string_lossy() + .to_string(); let agent_bindings: Vec = bindings .into_iter() @@ -279,12 +295,12 @@ pub async fn get_agent_detail(id: String) -> Result { #[tauri::command] pub async fn list_agent_files(id: String) -> Result { - let config = read_openclaw_config_value()?; - let agent_dir = resolve_agent_dir(&id, &config); + let config = super::config::load_openclaw_json()?; + let workspace_dir = resolve_agent_workspace_path(&id, &config); let files: Vec = AGENT_FILE_ALLOWLIST .iter() .map(|name| { - let path = agent_dir.join(name); + let path = workspace_dir.join(name); let meta = fs::metadata(&path).ok(); json!({ "name": name, @@ -302,8 +318,8 @@ pub async fn list_agent_files(id: String) -> Result { #[tauri::command] pub async fn read_agent_file(id: String, name: String) -> Result { ensure_allowed_agent_file(&name)?; - let config = read_openclaw_config_value()?; - let path = resolve_agent_dir(&id, &config).join(&name); + let config = super::config::load_openclaw_json()?; + let path = resolve_agent_workspace_path(&id, &config).join(&name); if !path.exists() { return Ok(json!({ "exists": false, "content": "" })); } @@ -314,8 +330,8 @@ pub async fn read_agent_file(id: String, name: String) -> Result #[tauri::command] pub async fn write_agent_file(id: String, name: String, content: String) -> Result { ensure_allowed_agent_file(&name)?; - let config = read_openclaw_config_value()?; - let dir = resolve_agent_dir(&id, &config); + let config = super::config::load_openclaw_json()?; + let dir = resolve_agent_workspace_path(&id, &config); if !dir.exists() { fs::create_dir_all(&dir).map_err(|e| format!("创建目录失败: {e}"))?; } @@ -323,17 +339,154 @@ pub async fn write_agent_file(id: String, name: String, content: String) -> Resu Ok(json!({ "ok": true })) } +#[tauri::command] +pub async fn get_agent_workspace_info(id: String) -> Result { + let config = super::config::load_openclaw_json()?; + let workspace_dir = resolve_agent_workspace_path(&id, &config); + Ok(json!({ + "agentId": id, + "workspacePath": workspace_dir.to_string_lossy().to_string(), + "exists": workspace_dir.exists(), + "isDefault": id == "main", + })) +} + +#[tauri::command] +pub async fn list_agent_workspace_entries( + id: String, + relative_path: Option, +) -> Result { + let config = super::config::load_openclaw_json()?; + let workspace_dir = resolve_agent_workspace_path(&id, &config); + if !workspace_dir.exists() { + return Ok(Value::Array(Vec::new())); + } + + let target_dir = resolve_workspace_target_path(&workspace_dir, relative_path.as_deref())?; + if !target_dir.exists() { + return Err("目录不存在".to_string()); + } + if !target_dir.is_dir() { + return Err("目标不是目录".to_string()); + } + + let mut items: Vec<(u8, String, Value)> = fs::read_dir(&target_dir) + .map_err(|e| format!("读取目录失败: {e}"))? + .filter_map(|entry| { + let entry = entry.ok()?; + let path = entry.path(); + let meta = entry.metadata().ok()?; + let is_dir = meta.is_dir(); + let name = entry.file_name().to_string_lossy().to_string(); + let relative = to_workspace_relative_path(&workspace_dir, &path); + let mtime = meta + .modified() + .ok() + .map(|m| chrono::DateTime::::from(m).to_rfc3339()); + + Some(( + if is_dir { 0 } else { 1 }, + name.to_lowercase(), + json!({ + "name": name, + "relativePath": relative, + "type": if is_dir { "dir" } else { "file" }, + "size": if is_dir { 0 } else { meta.len() }, + "mtime": mtime, + "editable": !is_dir && is_workspace_text_file(&path), + "previewable": !is_dir && is_workspace_previewable_file(&path), + }), + )) + }) + .collect(); + + items.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(&b.1))); + Ok(Value::Array(items.into_iter().map(|(_, _, item)| item).collect())) +} + +#[tauri::command] +pub async fn read_agent_workspace_file( + id: String, + relative_path: String, +) -> Result { + let config = super::config::load_openclaw_json()?; + let workspace_dir = resolve_agent_workspace_path(&id, &config); + let normalized = normalize_workspace_relative_path(&relative_path)?; + if normalized.as_os_str().is_empty() { + return Err("文件路径不能为空".to_string()); + } + + let file_path = workspace_dir.join(&normalized); + if !file_path.exists() { + return Err("文件不存在".to_string()); + } + if !file_path.is_file() { + return Err("目标不是文件".to_string()); + } + + let meta = fs::metadata(&file_path).map_err(|e| format!("读取文件信息失败: {e}"))?; + if meta.len() > MAX_WORKSPACE_FILE_SIZE { + return Err("文件过大,暂不支持在面板中打开".to_string()); + } + + let mtime = meta + .modified() + .ok() + .map(|m| chrono::DateTime::::from(m).to_rfc3339()); + + let bytes = fs::read(&file_path).map_err(|e| format!("读取文件失败: {e}"))?; + if looks_binary_bytes(&bytes) { + return Err("暂不支持在面板中打开二进制文件".to_string()); + } + + let content = String::from_utf8(bytes) + .map_err(|_| "暂不支持在面板中打开非 UTF-8 文本文件".to_string())?; + + Ok(json!({ + "relativePath": normalized.to_string_lossy().replace('\\', "/"), + "path": file_path.to_string_lossy().to_string(), + "size": meta.len(), + "mtime": mtime, + "editable": true, + "previewable": is_workspace_previewable_file(&file_path), + "content": content, + })) +} + +#[tauri::command] +pub async fn write_agent_workspace_file( + id: String, + relative_path: String, + content: String, +) -> Result { + let config = super::config::load_openclaw_json()?; + let workspace_dir = resolve_agent_workspace_path(&id, &config); + let normalized = normalize_workspace_relative_path(&relative_path)?; + if normalized.as_os_str().is_empty() { + return Err("文件路径不能为空".to_string()); + } + + let file_path = workspace_dir.join(&normalized); + if let Some(parent) = file_path.parent() { + fs::create_dir_all(parent).map_err(|e| format!("创建目录失败: {e}"))?; + } + + fs::write(&file_path, content.as_bytes()).map_err(|e| format!("写入文件失败: {e}"))?; + + Ok(json!({ + "ok": true, + "relativePath": normalized.to_string_lossy().replace('\\', "/"), + "size": content.as_bytes().len(), + })) +} + #[tauri::command] pub async fn update_agent_config( app: tauri::AppHandle, id: String, config: Value, ) -> Result { - let path = super::openclaw_dir().join("openclaw.json"); - let content = fs::read_to_string(&path).map_err(|e| format!("读取配置失败: {e}"))?; - let mut root: Value = - serde_json::from_str(&content).map_err(|e| format!("解析 JSON 失败: {e}"))?; - + let mut root = super::config::load_openclaw_json()?; if root.get("agents").is_none() { root.as_object_mut() .ok_or("配置格式错误")? @@ -414,9 +567,11 @@ pub async fn update_agent_config( } } - let json_text = serde_json::to_string_pretty(&root).map_err(|e| format!("序列化失败: {e}"))?; - fs::write(&path, json_text).map_err(|e| format!("写入配置失败: {e}"))?; - let _ = super::config::do_reload_gateway(&app).await; + super::config::save_openclaw_json(&root)?; + let app2 = app.clone(); + tauri::async_runtime::spawn(async move { + let _ = super::config::do_reload_gateway(&app2).await; + }); Ok(json!({ "ok": true })) } @@ -530,14 +685,7 @@ pub async fn add_agent( /// 直接写 openclaw.json 创建 agent(CLI 不可用时的兜底方案) fn add_agent_to_config(id: &str, model: &str, workspace: &std::path::Path) -> Result<(), String> { - let config_path = super::openclaw_dir().join("openclaw.json"); - if !config_path.exists() { - return Err("openclaw.json 不存在,请先安装 OpenClaw".to_string()); - } - let content = fs::read_to_string(&config_path).map_err(|e| format!("读取配置失败: {e}"))?; - let mut config: Value = - serde_json::from_str(&content).map_err(|e| format!("解析 JSON 失败: {e}"))?; - + let mut config = super::config::load_openclaw_json()?; // 确保 agents.list 存在 if config.get("agents").is_none() { config @@ -576,11 +724,7 @@ fn add_agent_to_config(id: &str, model: &str, workspace: &std::path::Path) -> Re } list.push(agent); - // 备份 + 写回 - let bak = super::openclaw_dir().join("openclaw.json.bak"); - let _ = fs::copy(&config_path, &bak); - let json = serde_json::to_string_pretty(&config).map_err(|e| format!("序列化失败: {e}"))?; - fs::write(&config_path, json).map_err(|e| format!("写入配置失败: {e}"))?; + super::config::save_openclaw_json(&config)?; Ok(()) } @@ -593,32 +737,23 @@ pub async fn delete_agent(app: tauri::AppHandle, id: String) -> Result, emoji: Option, ) -> Result { - let path = super::openclaw_dir().join("openclaw.json"); - let content = fs::read_to_string(&path).map_err(|e| format!("读取配置失败: {e}"))?; - let mut config: Value = - serde_json::from_str(&content).map_err(|e| format!("解析 JSON 失败: {e}"))?; - + let mut config = super::config::load_openclaw_json()?; let agents_list = config .get_mut("agents") .and_then(|a| a.get_mut("list")) @@ -696,10 +827,7 @@ pub async fn update_agent_identity( .map(|s| s.to_string()) }); - let json = serde_json::to_string_pretty(&config).map_err(|e| format!("序列化失败: {e}"))?; - if let Err(e) = fs::write(&path, json) { - return Err(format!("写入配置失败: {e},请检查文件权限")); - } + super::config::save_openclaw_json(&config)?; // 删除 IDENTITY.md 文件,让配置文件生效 if let Some(ws_str) = workspace_path { @@ -774,11 +902,7 @@ pub async fn update_agent_model( id: String, model: String, ) -> Result { - let path = super::openclaw_dir().join("openclaw.json"); - let content = fs::read_to_string(&path).map_err(|e| format!("读取配置失败: {e}"))?; - let mut config: Value = - serde_json::from_str(&content).map_err(|e| format!("解析 JSON 失败: {e}"))?; - + let mut config = super::config::load_openclaw_json()?; let agents_list = config .get_mut("agents") .and_then(|a| a.get_mut("list")) @@ -796,10 +920,7 @@ pub async fn update_agent_model( .ok_or("Agent 格式错误")? .insert("model".to_string(), model_obj); - let json = serde_json::to_string_pretty(&config).map_err(|e| format!("序列化失败: {e}"))?; - if let Err(e) = fs::write(&path, json) { - return Err(format!("写入配置失败: {e},请检查文件权限")); - } + super::config::save_openclaw_json(&config)?; // 触发 Gateway 重载使配置生效 let _ = super::config::do_reload_gateway(&app).await; @@ -807,12 +928,6 @@ pub async fn update_agent_model( Ok("已更新".into()) } -fn read_openclaw_config_value() -> Result { - let path = super::openclaw_dir().join("openclaw.json"); - let content = fs::read_to_string(&path).map_err(|e| format!("读取配置失败: {e}"))?; - serde_json::from_str(&content).map_err(|e| format!("解析 JSON 失败: {e}")) -} - fn resolve_agent_workspace(id: &str, config: &Value) -> String { config .get("agents") @@ -823,6 +938,8 @@ fn resolve_agent_workspace(id: &str, config: &Value) -> String { .find(|a| a.get("id").and_then(|v| v.as_str()) == Some(id)) .and_then(|a| a.get("workspace")) .and_then(|v| v.as_str()) + .map(str::trim) + .filter(|s| !s.is_empty()) .map(|s| s.to_string()) }) .unwrap_or_else(|| { @@ -842,25 +959,94 @@ fn resolve_agent_workspace(id: &str, config: &Value) -> String { }) } -fn resolve_agent_dir(id: &str, config: &Value) -> std::path::PathBuf { - let custom_dir = config - .get("agents") - .and_then(|a| a.get("list")) - .and_then(|l| l.as_array()) - .and_then(|list| { - list.iter() - .find(|a| a.get("id").and_then(|v| v.as_str()) == Some(id)) - .and_then(|a| a.get("agentDir")) - .and_then(|v| v.as_str()) - .map(std::path::PathBuf::from) - }); - custom_dir.unwrap_or_else(|| { - if id == "main" { - super::openclaw_dir() - } else { - super::openclaw_dir().join("agents").join(id) +fn expand_user_path(raw: &str) -> std::path::PathBuf { + let trimmed = raw.trim(); + let path = if let Some(rest) = trimmed + .strip_prefix("~/") + .or_else(|| trimmed.strip_prefix("~\\")) + { + dirs::home_dir().unwrap_or_default().join(rest) + } else { + std::path::PathBuf::from(trimmed) + }; + + if path.is_absolute() { + path + } else { + std::env::current_dir() + .map(|cwd| cwd.join(&path)) + .unwrap_or(path) + } +} + +fn resolve_agent_workspace_path(id: &str, config: &Value) -> std::path::PathBuf { + expand_user_path(&resolve_agent_workspace(id, config)) +} + +fn normalize_workspace_relative_path(raw: &str) -> Result { + let trimmed = raw.trim(); + if trimmed.is_empty() { + return Ok(PathBuf::new()); + } + + let path = PathBuf::from(trimmed); + if path.is_absolute() { + return Err("不允许使用绝对路径".to_string()); + } + + let mut normalized = PathBuf::new(); + for component in path.components() { + match component { + Component::Normal(seg) => normalized.push(seg), + Component::CurDir => {} + Component::ParentDir => return Err("不允许访问工作区外部路径".to_string()), + Component::RootDir | Component::Prefix(_) => { + return Err("不允许使用绝对路径".to_string()) + } } - }) + } + Ok(normalized) +} + +fn resolve_workspace_target_path(root: &Path, relative_path: Option<&str>) -> Result { + let normalized = normalize_workspace_relative_path(relative_path.unwrap_or_default())?; + Ok(root.join(normalized)) +} + +fn to_workspace_relative_path(root: &Path, path: &Path) -> String { + path.strip_prefix(root) + .unwrap_or(path) + .components() + .filter_map(|component| match component { + Component::Normal(seg) => Some(seg.to_string_lossy().to_string()), + _ => None, + }) + .collect::>() + .join("/") +} + +fn is_workspace_text_file(path: &Path) -> bool { + if let Some(ext) = path.extension().and_then(|ext| ext.to_str()) { + if WORKSPACE_TEXT_EXTENSIONS.contains(&ext.to_ascii_lowercase().as_str()) { + return true; + } + } + + path.file_name() + .and_then(|name| name.to_str()) + .map(|name| WORKSPACE_TEXT_BASENAMES.contains(&name.to_ascii_lowercase().as_str())) + .unwrap_or(false) +} + +fn is_workspace_previewable_file(path: &Path) -> bool { + path.extension() + .and_then(|ext| ext.to_str()) + .map(|ext| WORKSPACE_PREVIEW_EXTENSIONS.contains(&ext.to_ascii_lowercase().as_str())) + .unwrap_or(false) +} + +fn looks_binary_bytes(bytes: &[u8]) -> bool { + bytes.iter().take(512).any(|b| *b == 0) } fn ensure_allowed_agent_file(name: &str) -> Result<(), String> { diff --git a/src-tauri/src/commands/config.rs b/src-tauri/src/commands/config.rs index e1139bf..16f0aec 100644 --- a/src-tauri/src/commands/config.rs +++ b/src-tauri/src/commands/config.rs @@ -701,14 +701,9 @@ fn calibration_richness_score(config: &Value) -> usize { score += 4; } if config - .pointer("/auth/profiles") - .and_then(|v| v.as_object()) - .map(|v| !v.is_empty()) - .unwrap_or(false) + .pointer("/agents/defaults") + .is_some() { - score += 3; - } - if config.pointer("/agents/defaults").is_some() { score += 2; } if config @@ -798,7 +793,6 @@ fn build_calibration_baseline() -> Value { "lastTouchedVersion": calibration_last_touched_version(), }, "models": { "providers": {} }, - "auth": { "profiles": {} }, "agents": { "defaults": { "workspace": calibration_default_workspace(), @@ -904,17 +898,6 @@ fn normalize_calibrated_config(mut config: Value) -> Value { } } - let auth = root.entry("auth").or_insert_with(|| json!({})); - if !auth.is_object() { - *auth = json!({}); - } - if let Some(auth_obj) = auth.as_object_mut() { - let profiles = auth_obj.entry("profiles").or_insert_with(|| json!({})); - if !profiles.is_object() { - *profiles = json!({}); - } - } - let agents = root.entry("agents").or_insert_with(|| json!({})); if !agents.is_object() { *agents = json!({}); @@ -1207,13 +1190,14 @@ const KNOWN_UI_FIELDS: &[&str] = &[ "latency", "testStatus", "testError", + "profiles", ]; /// 已知需要保留的合法 OpenClaw 配置字段(用于诊断报告) /// 这些字段虽然不在标准列表中,但不应被警告为未知字段 /// 注意:这些字段在 `merge_configs_preserving_fields` 中会被特殊处理 #[allow(dead_code)] -const KNOWN_LEGAL_FIELDS: &[&str] = &["browser", "profiles", "agents", "gateway", "logging", "mcp"]; +const KNOWN_LEGAL_FIELDS: &[&str] = &["browser", "agents", "gateway", "logging", "mcp"]; // KNOWN_LEGAL_FIELDS 目前在诊断逻辑中使用,用于生成报告信息 @@ -1326,7 +1310,7 @@ pub fn validate_openclaw_config() -> Result { // 检查 agents 子字段(上游 schema 只定义 agents.list) if agents_obj.contains_key("profiles") { warnings.push( - "发现 agents.profiles 字段,上游 schema 未定义此字段,将保留但建议核实" + "发现 agents.profiles 字段,上游 schema 未定义此字段,ClawPanel 会自动清理" .to_string(), ); } @@ -1555,6 +1539,39 @@ fn sync_providers_to_agent_models(config: &Value) { /// 检测配置中是否包含 UI 专属字段 fn has_ui_fields(val: &Value) -> bool { if let Some(obj) = val.as_object() { + for key in &[ + "current", + "latest", + "recommended", + "update_available", + "latest_update_available", + "is_recommended", + "ahead_of_recommended", + "panel_version", + "source", + "qqbot", + "profiles", + ] { + if obj.contains_key(*key) { + return true; + } + } + if obj + .get("auth") + .and_then(|v| v.as_object()) + .map(|auth| auth.contains_key("profiles")) + .unwrap_or(false) + { + return true; + } + if obj + .get("agents") + .and_then(|v| v.as_object()) + .map(|agents| agents.contains_key("profiles")) + .unwrap_or(false) + { + return true; + } if let Some(models_val) = obj.get("models") { if let Some(models_obj) = models_val.as_object() { if let Some(providers_val) = models_obj.get("providers") { @@ -1612,9 +1629,15 @@ fn strip_ui_fields(mut val: Value) -> Value { "source", // 渠道插件别名:OpenClaw schema 不承认 qqbot 作为根键(应写在 channels.qqbot) "qqbot", + "profiles", ] { obj.remove(*key); } + if let Some(auth_val) = obj.get_mut("auth") { + if let Some(auth_obj) = auth_val.as_object_mut() { + auth_obj.remove("profiles"); + } + } // 处理 models.providers.xxx.models 结构 if let Some(models_val) = obj.get_mut("models") { if let Some(models_obj) = models_val.as_object_mut() { @@ -1651,6 +1674,7 @@ fn strip_ui_fields(mut val: Value) -> Value { // 递归处理 agents 数组中的元素(保留 agents.list 等合法字段) if let Some(agents_val) = obj.get_mut("agents") { if let Some(agents_obj) = agents_val.as_object_mut() { + agents_obj.remove("profiles"); // 保留 agents 子字段不做修改 // 只清理 agents 数组中的元素(如果有 UI 字段) if let Some(Value::Array(arr)) = agents_obj.get_mut("list") { diff --git a/src-tauri/src/commands/pairing.rs b/src-tauri/src/commands/pairing.rs index cecc9d3..6e40723 100644 --- a/src-tauri/src/commands/pairing.rs +++ b/src-tauri/src/commands/pairing.rs @@ -104,14 +104,7 @@ pub fn auto_pair_device() -> Result { /// 将 Tauri 应用的 origin 写入 gateway.controlUi.allowedOrigins /// 避免 Gateway 因 origin not allowed 拒绝 WebSocket 握手 fn patch_gateway_origins() { - let config_path = crate::commands::openclaw_dir().join("openclaw.json"); - if !config_path.exists() { - return; - } - let Ok(content) = std::fs::read_to_string(&config_path) else { - return; - }; - let Ok(mut config) = serde_json::from_str::(&content) else { + let Ok(mut config) = super::config::load_openclaw_json() else { return; }; @@ -154,9 +147,7 @@ fn patch_gateway_origins() { } } - if let Ok(new_json) = serde_json::to_string_pretty(&config) { - let _ = std::fs::write(&config_path, new_json); - } + let _ = super::config::save_openclaw_json(&config); } #[tauri::command]