mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-06 20:02:49 +08:00
fix: sanitize polluted profiles fields in openclaw config
This commit is contained in:
@@ -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: '配置文件已创建' }
|
||||
},
|
||||
|
||||
|
||||
@@ -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 创建 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<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> {
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user