- 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 通过
274 lines
8.9 KiB
Rust
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(())
|
|
}
|