Files
DevFlow/src-tauri/src/commands/settings.rs
T
lxy d5b0459a8a 新增: DockerNode+CI状态读取+模板CRUD+CIStatus面板
- DockerNode: 环境检测(docker --version)+授权+容器内执行(12个测试)

- ci_status.rs: Gitea commit status API 读取(7个测试,失败返回空不阻塞)

- CIStatus.vue: CI检查面板(通过/失败/pending 汇总+可点击跳转)

- 模板CRUD IPC: list/save/delete templates(内置只读+自定义KV持久化)

- TemplateInfo 结构体(IPC传输用)

- 零编译警告,vue-tsc+vite build 通过
2026-07-02 01:02:39 +08:00

274 lines
8.9 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(())
}
/// 读取脚本安全配置。
#[tauri::command]
pub async fn get_script_safety(
state: State<'_, AppState>,
) -> Result<(String, String), String> {
let wl = state.settings.get("df-script-whitelist").await.map_err(err_str)?.unwrap_or_default();
let bl = state.settings.get("df-script-blacklist").await.map_err(err_str)?.unwrap_or_default();
Ok((wl, bl))
}
/// 设置多 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 get_plan_execution() -> 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> {
use tauri::Emitter;
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 事件
let ev = crate::commands::ai::AiChatEvent::AiConflictResolved {
conflict_id,
plan_id,
resolution,
resolved_by,
conversation_id,
};
let _ = app_handle.emit("ai-chat-event", ev.clone());
let _ = state.ai_event_bus.publish_event(ev);
Ok(())
}