Files
DevFlow/src-tauri/src/commands/settings.rs
T
lxy 75c5e23523 重构: 后端编排拆解与DRY债收敛及命名纠偏
阶段1拆解: stream 系外移 stream_relay.rs,mod.rs 3048→2607,loop_body 502→83
技能收尾: regenerate/edit 回溯补技能,新增 read_skill 工具,项目级技能扫描
DRY: emit 双写 46 处收敛 event_bus 正典,KV 引号归一收口,LOCK-SLOW 33 处单行化
警示骨架 4→1 与压缩兜底 2→1,噪音目录档位收敛,truncate 同名陷阱消除
工具元数据落 ToolMeta 八维,7 张 match 表改查 registry,parity 测试锁等价
命名: is_noise_entry/teardown_round/probe_missed_terminal_event 等 6 项纠偏
2026-10-01 19:35:19 +08:00

262 lines
8.5 KiB
Rust

//! 通用应用设置 KV IPC — 前端 localStorage 迁移目标
//!
//! 4 个 command 读写 `app_settings` 表(key/value JSON 字符串),承载主题、面板折叠态、
//! 最近使用项等前端持久化偏好。返回统一 `Result<T, String>`。
use std::collections::HashMap;
use tauri::State;
use crate::state::AppState;
use super::err_str;
/// 取单个 key 的值(JSON 字符串),不存在返回 None
#[tauri::command]
pub async fn settings_get(
state: State<'_, AppState>,
key: String,
) -> Result<Option<String>, String> {
state.settings.get(&key).await.map_err(err_str)
}
/// 写 key/value(`INSERT OR REPLACE`),刷新 updated_at
#[tauri::command]
pub async fn settings_set(
state: State<'_, AppState>,
key: String,
value: String,
) -> Result<bool, String> {
state.settings.set(&key, &value).await.map_err(err_str)
}
/// 取全部 key/value(前端启动时一次性拉回恢复偏好)
#[tauri::command]
pub async fn settings_get_all(
state: State<'_, AppState>,
) -> Result<HashMap<String, String>, String> {
let rows = state.settings.get_all().await.map_err(err_str)?;
Ok(rows.into_iter().collect())
}
/// 删除单个 key
#[tauri::command]
pub async fn settings_delete(
state: State<'_, AppState>,
key: String,
) -> Result<bool, String> {
state.settings.delete(&key).await.map_err(err_str)
}
/// 获取 DevFlow 数据目录路径(运行期确定,跨平台)
#[tauri::command]
pub async fn get_data_dir(state: State<'_, AppState>) -> Result<String, String> {
Ok(state.data_dir.to_string_lossy().to_string())
}
/// 读取审批超时分钟数(默认 15 分钟,0=禁用超时)
///
/// 前端 Settings 页启动时拉取,展示当前配置。
#[tauri::command]
pub async fn ai_get_approval_timeout(state: State<'_, AppState>) -> Result<u64, String> {
Ok(state.approval_timeout_minutes.load(std::sync::atomic::Ordering::SeqCst))
}
/// 设置审批超时分钟数并持久化(默认 15 分钟,0=禁用超时)
///
/// 前端 Settings 页保存时调用。值范围 0-1440(0=禁用,最大 24 小时)。
/// 注意:此命令与前端 AdvancedSection.vue 的 df-approval-timeout KV 持久化路径不同,
/// 前端走 appSettings.set 走 KV,这里作为后端热改入口。两者最终读写同一个 KV。
#[tauri::command]
pub async fn ai_set_approval_timeout(
state: State<'_, AppState>,
minutes: u64,
) -> Result<u64, String> {
// 范围校验(0-1440 分钟,即 0-24 小时)
if minutes > 1440 {
return Err("审批超时分钟数超出范围(最大 1440 = 24 小时)".to_string());
}
// 持久化到 Settings KV(与前端同 key,值为毫秒)
let ms = minutes * 60_000;
state
.settings
.set(
crate::state::APPROVAL_TIMEOUT_KEY,
&ms.to_string(),
)
.await
.map_err(err_str)?;
// 更新内存
state.approval_timeout_minutes.store(minutes, std::sync::atomic::Ordering::SeqCst);
tracing::info!("[APPROVAL-TIMEOUT] 已更新: {} 分钟 ({} ms)", minutes, ms);
Ok(minutes)
}
/// 设置脚本安全配置(白/黑名单)并注入到 ScriptNode 运行时。
///
/// 前端设置页「命令执行安全」保存时调用。
/// 持久化到 app_settings KV,同时通过 df_nodes::set_script_safety_config 注入内存,
/// 即时生效(无需重启)。
#[tauri::command]
pub async fn set_script_safety(
state: State<'_, AppState>,
whitelist: String,
blacklist: String,
) -> Result<(), String> {
// 持久化到 KV
state.settings.set("df-script-whitelist", &whitelist).await.map_err(err_str)?;
state.settings.set("df-script-blacklist", &blacklist).await.map_err(err_str)?;
// 注入运行时(即时生效,不依赖进程重启)
df_nodes::script_node::set_script_safety_config(&whitelist, &blacklist);
tracing::info!(
whitelist = %whitelist,
blacklist = %blacklist,
"[SCRIPT-SAFETY] 运行时配置已更新"
);
Ok(())
}
/// 设置多 ReAct Plan 执行开关(默认关)。
/// 开启后 process_tool_calls 内以 JoinSet 并行执行同层工具。
#[tauri::command]
pub async fn set_plan_execution(enabled: bool) -> Result<(), String> {
df_ai::plan_executor::set_plan_execution(enabled);
tracing::info!(enabled, "[PLAN-EXEC] IPC 开关已更新");
Ok(())
}
/// 读取多 ReAct Plan 执行开关状态。
#[tauri::command]
pub async fn is_plan_execution_enabled() -> Result<bool, String> {
Ok(df_ai::plan_executor::plan_execution_enabled())
}
/// 列出可用模板(内置 + 自定义)。
///
/// 内置模板从 assets/templates/ 加载;自定义模板从 app_settings KV(df-custom-templates)读取。
#[tauri::command]
pub async fn list_templates(state: State<'_, AppState>) -> Result<Vec<TemplateInfo>, String> {
let mut templates = Vec::new();
// 内置模板
let builtin = [
("code-review", "代码审查", "AI 驱动的代码审查流程"),
("bug-fix", "Bug 修复", "AI 驱动的 bug 修复流程"),
("feature-dev", "功能开发", "AI 驱动的功能开发全流程"),
];
for (id, name, desc) in &builtin {
templates.push(TemplateInfo {
id: id.to_string(),
name: name.to_string(),
description: desc.to_string(),
builtin: true,
yaml: String::new(), // 内置模板不返回全文(前端需要时单独加载)
});
}
// 自定义模板
if let Ok(Some(custom_json)) = state.settings.get("df-custom-templates").await {
if let Ok(custom) = serde_json::from_str::<Vec<TemplateInfo>>(&custom_json) {
templates.extend(custom);
}
}
Ok(templates)
}
/// 保存自定义模板(新建/覆盖)。
#[tauri::command]
pub async fn save_template(
state: State<'_, AppState>,
template: TemplateInfo,
) -> Result<(), String> {
let mut customs: Vec<TemplateInfo> = state
.settings
.get("df-custom-templates")
.await
.map_err(err_str)?
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default();
// 按 id 去重(覆盖同名)
customs.retain(|t| t.id != template.id);
customs.push(template);
let json = serde_json::to_string(&customs).map_err(|e| e.to_string())?;
state.settings.set("df-custom-templates", &json).await.map_err(err_str)?;
Ok(())
}
/// 删除自定义模板。
#[tauri::command]
pub async fn delete_template(state: State<'_, AppState>, template_id: String) -> Result<(), String> {
let mut customs: Vec<TemplateInfo> = state
.settings
.get("df-custom-templates")
.await
.map_err(err_str)?
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default();
let before = customs.len();
customs.retain(|t| t.id != template_id);
if customs.len() < before {
let json = serde_json::to_string(&customs).map_err(|e| e.to_string())?;
state.settings.set("df-custom-templates", &json).await.map_err(err_str)?;
}
Ok(())
}
/// 模板信息(IPC 传输用)
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct TemplateInfo {
pub id: String,
pub name: String,
pub description: String,
/// true = 内置模板(只读);false = 自定义模板
pub builtin: bool,
/// 自定义模板的 YAML 内容(内置模板为空)
pub yaml: String,
}
/// 解决合并冲突(用户/reviewer 选择解决方案后调用)。
///
/// 更新 ai_conflicts 表的 resolution + resolved_by + resolved_at,
/// 并 emit AiConflictResolved 事件供前端更新徽章。
#[tauri::command]
pub async fn resolve_conflict(
state: State<'_, AppState>,
app_handle: tauri::AppHandle,
conflict_id: String,
plan_id: String,
resolution: String,
resolved_by: String,
conversation_id: Option<String>,
) -> Result<(), String> {
let repo = df_storage::crud::ConflictRepo::new(&state.db);
let now = df_types::now_millis().to_string();
let ok = repo.resolve(&conflict_id, &resolution, &resolved_by, &now)
.await
.map_err(err_str)?;
if !ok {
return Err(format!("冲突 {} 不存在或已解决", conflict_id));
}
tracing::info!(
conflict_id = %conflict_id,
resolution = %resolution,
resolved_by = %resolved_by,
"[CONFLICT] 冲突已解决"
);
// emit AiConflictResolved 事件(双写收敛经 emit_ai_chat_event)
let ev = crate::commands::ai::AiChatEvent::AiConflictResolved {
conflict_id,
plan_id,
resolution,
resolved_by,
conversation_id,
};
crate::commands::ai::event_bus::emit_ai_chat_event(&app_handle, ev);
Ok(())
}