fix: sanitize polluted profiles fields in openclaw config

This commit is contained in:
晴天
2026-04-02 00:09:03 +08:00
parent 88d4c67ae6
commit 17dbf2bc81
4 changed files with 380 additions and 163 deletions

View File

@@ -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: '配置文件已创建' }
},

View File

@@ -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<Value, 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 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<Value, String> {
#[tauri::command]
pub async fn get_agent_detail(id: String) -> Result<Value, String> {
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<Value, String> {
})
.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<Value> = bindings
.into_iter()
@@ -279,12 +295,12 @@ pub async fn get_agent_detail(id: String) -> Result<Value, String> {
#[tauri::command]
pub async fn list_agent_files(id: String) -> Result<Value, String> {
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<Value> = 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<Value, String> {
#[tauri::command]
pub async fn read_agent_file(id: String, name: String) -> Result<Value, String> {
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<Value, String>
#[tauri::command]
pub async fn write_agent_file(id: String, name: String, content: String) -> Result<Value, String> {
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<Value, String> {
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<String>,
) -> Result<Value, String> {
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::<chrono::Utc>::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<Value, String> {
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::<chrono::Utc>::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<Value, String> {
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<Value, String> {
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 创建 agentCLI 不可用时的兜底方案)
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<String, S
}
// 1. 从 openclaw.json 的 agents.list 中移除
let config_path = super::openclaw_dir().join("openclaw.json");
if config_path.exists() {
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}"))?;
if let Some(list) = config
.get_mut("agents")
.and_then(|a| a.get_mut("list"))
.and_then(|l| l.as_array_mut())
{
list.retain(|a| a.get("id").and_then(|v| v.as_str()) != Some(&id));
}
// 同时清理 agents.profiles 中的配置
if let Some(profiles) = config
.get_mut("agents")
.and_then(|a| a.get_mut("profiles"))
.and_then(|p| p.as_object_mut())
{
profiles.remove(&id);
}
// 备份 + 写回
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}"))?;
let mut config = super::config::load_openclaw_json()?;
if let Some(list) = config
.get_mut("agents")
.and_then(|a| a.get_mut("list"))
.and_then(|l| l.as_array_mut())
{
list.retain(|a| a.get("id").and_then(|v| v.as_str()) != Some(&id));
}
// 同时清理 agents.profiles 中的配置
if let Some(profiles) = config
.get_mut("agents")
.and_then(|a| a.get_mut("profiles"))
.and_then(|p| p.as_object_mut())
{
profiles.remove(&id);
}
super::config::save_openclaw_json(&config)?;
// 2. 删除 agent 目录workspace + sessions 等)
let agent_dir = super::openclaw_dir().join("agents").join(&id);
@@ -642,11 +777,7 @@ pub async fn update_agent_identity(
name: Option<String>,
emoji: Option<String>,
) -> Result<String, String> {
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<String, String> {
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<Value, String> {
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<PathBuf, String> {
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<PathBuf, String> {
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::<Vec<_>>()
.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> {

View File

@@ -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<Value, String> {
// 检查 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") {

View File

@@ -104,14 +104,7 @@ pub fn auto_pair_device() -> Result<String, String> {
/// 将 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::<serde_json::Value>(&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]