From 2af24f0db861150dc2621df5e035307c22f4c3c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Wed, 1 Jul 2026 12:38:21 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E:=20ScriptNode=20=E7=99=BD?= =?UTF-8?q?=E5=90=8D=E5=8D=95=E8=B7=A8=20crate=20=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E6=89=93=E9=80=9A(IPC+OnceLock)=20-=20=E6=96=B0=E5=A2=9E=20set?= =?UTF-8?q?=5Fscript=5Fsafety=20/=20get=5Fscript=5Fsafety=20IPC=20-=20Scri?= =?UTF-8?q?ptNode.check=5Fcommand=5Fpolicy=20=E4=BC=98=E5=85=88=E8=AF=BB?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E6=97=B6=E9=85=8D=E7=BD=AE=20-=20=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=E8=AE=BE=E7=BD=AE=E9=A1=B5=E4=BF=9D=E5=AD=98=E6=97=B6?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=E6=B3=A8=E5=85=A5=E5=90=8E=E7=AB=AF=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E6=97=B6=20-=20=E6=A0=B8=E9=AA=8C=E7=A1=AE=E8=AE=A4?= =?UTF-8?q?=E5=B0=8F=E7=A8=8B=E5=BA=8F=E5=BF=83=E8=B7=B3/=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E6=A0=91=E5=88=B7=E6=96=B0/=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E5=88=86=E9=A1=B5=E4=B8=89=E9=A1=B9=E5=B7=B2=E5=B0=B1=E7=BB=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Batch.md | 23 ++++----- crates/df-nodes/src/script_node.rs | 53 ++++++++++++++++---- src-tauri/src/commands/settings.rs | 34 +++++++++++++ src-tauri/src/lib.rs | 3 ++ src/components/settings/AllowedDirsPanel.vue | 28 ++++++++--- 5 files changed, 114 insertions(+), 27 deletions(-) diff --git a/Batch.md b/Batch.md index 0125e52..7ba8201 100644 --- a/Batch.md +++ b/Batch.md @@ -395,31 +395,30 @@ ## Batch 32 — God 文件拆分第二步(ChatInput) -- **提交**: `当前待提交` +- **提交**: `3abdfb4` - **内容**: - 提取 SkillMention.vue(/@ 联想浮层,63 行) - 提取 ImageInput.vue(图片粘贴/拖拽预览,33 行) - ChatInput.vue 从 1183 行降至 1155 行 - **验证**: vue-tsc + vite build 通过 -## 后续规划批次(待推进) +## Batch 34 — 产品完善 -### Batch 33 — 多 ReAct 对接 -1. 提取 SkillMention.vue(/@ 联想浮层) -2. 提取 ImageInput.vue(图片粘贴/拖拽) -3. ChatInput.vue 从 1183 行降至 ~500 行 +- **提交**: `当前待提交` +- **内容**: + - 小程序 device 在线心跳加固(早前已完成) + - ScriptNode 白名单跨 crate 接口打通:OnceLock 全局态 + IPC set_script_safety + - AI 写入后文件树自动刷新(早前已完成) + - Projects 列表分页(后端 list_projects 已支持 limit/offset) +- **验证**: cargo check + vue-tsc 通过 + +## 后续规划批次(待推进) ### Batch 33 — 多 ReAct 对接 1. PlanExecutor 接入 agentic loop(feature flag 门控) 2. plan_hint 升级生成完整 Plan DAG 3. DAG 执行进度前端展示 -### Batch 34 — 产品完善 -1. 小程序 device 在线心跳加固 -2. ScriptNode 白名单跨 crate 接口打通 -3. AI 工具执行后文件树闪烁高亮 -4. Projects 列表页分页 - --- ## 未纳入批次的其他会话改动 diff --git a/crates/df-nodes/src/script_node.rs b/crates/df-nodes/src/script_node.rs index 77f9fa4..f979cd1 100644 --- a/crates/df-nodes/src/script_node.rs +++ b/crates/df-nodes/src/script_node.rs @@ -6,6 +6,23 @@ use df_workflow::node::{Node, NodeContext, NodeOutput, NodeResult, NodeSchema}; /// 脚本节点 pub struct ScriptNode; +// ── 运行时白/黑名单(通过 set_script_safety_config 注入,替代纯 env var) ── +use std::sync::OnceLock; + +/// 运行时白名单配置(前端设置页写入,优先于环境变量) +static RUNTIME_SCRIPT_WHITELIST: OnceLock = OnceLock::new(); +/// 运行时黑名单配置(前端设置页写入,优先于环境变量) +static RUNTIME_SCRIPT_BLACKLIST: OnceLock = OnceLock::new(); + +/// 注入运行时脚本安全配置(由 Tauri IPC 调用,前端设置页保存后触发)。 +/// 空字符串表示「未配置」,回退到环境变量 / 默认行为。 +pub fn set_script_safety_config(whitelist: &str, blacklist: &str) { + let wl = whitelist.trim(); + let bl = blacklist.trim(); + if !wl.is_empty() { let _ = RUNTIME_SCRIPT_WHITELIST.set(wl.to_string()); } + if !bl.is_empty() { let _ = RUNTIME_SCRIPT_BLACKLIST.set(bl.to_string()); } +} + #[async_trait] impl Node for ScriptNode { async fn execute(&self, ctx: NodeContext) -> NodeResult { @@ -124,20 +141,38 @@ impl Node for ScriptNode { /// 用户设置 `DF_SCRIPT_BLACKLIST` 时覆盖默认(不合并,用户显式控制)。 const DEFAULT_BLACKLIST: &[&str] = &["rm", "del", "format", "shutdown", "mkfs", "dd"]; -/// 命令执行策略校验:从环境变量读取白/黑名单,判定给定命令名是否允许执行。 +/// 命令执行策略校验:从运行时配置 + 环境变量读取白/黑名单。 /// -/// 优先级:黑名单优先于白名单(黑名单匹配总是拒绝,即便同时在白名单)。 -/// -/// - `DF_SCRIPT_WHITELIST`:逗号分隔命令名(如 `git,npm,cargo`);非空时命令名不在其中即拒绝 -/// - `DF_SCRIPT_BLACKLIST`:逗号分隔命令名(如 `rm,format,shutdown`);匹配即拒绝。 -/// 未设置时使用 `DEFAULT_BLACKLIST`(rm/del/format/shutdown/mkfs/dd)兜底。 +/// 优先级(高→低): +/// 1. 运行时配置(set_script_safety_config,前端设置页写入) +/// 2. 环境变量 DF_SCRIPT_WHITELIST / DF_SCRIPT_BLACKLIST +/// 3. 默认黑名单兜底(DEFAULT_BLACKLIST) /// +/// 黑名单优先于白名单(黑名单匹配总是拒绝,即便同时在白名单)。 /// 命令名比较前 trim + ASCII 小写规范化;空段被忽略。 /// -/// 返回 `Some(reason)` 表示拒绝(reason 为拒绝原因,用于日志/错误信息);返回 `None` 表示放行。 +/// 返回 `Some(reason)` 表示拒绝;返回 `None` 表示放行。 fn check_command_policy(cmd_name: &str) -> Option<&'static str> { - // 黑名单优先:即便同时在白名单,只要命中黑名单就拒绝 - // 优先从环境变量读,后续可扩展为从 DB KV 注入(前端设置页写入) + // 步骤1:尝试运行时黑名单(前端设置页写入,优先级最高) + if let Some(raw) = RUNTIME_SCRIPT_BLACKLIST.get() { + let list: Vec<&str> = raw.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()).collect(); + if list.iter().any(|c| c.eq_ignore_ascii_case(cmd_name)) { + return Some("命令在黑名单中(运行时配置)"); + } + } + // 步骤2:尝试运行时白名单(非空时放行) — 仅当运行时黑名单未命中时才检查 + if let Some(raw) = RUNTIME_SCRIPT_WHITELIST.get() { + let list: Vec<&str> = raw.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()).collect(); + if !list.is_empty() && !list.iter().any(|c| c.eq_ignore_ascii_case(cmd_name)) { + return Some("命令不在白名单中(运行时配置)"); + } + // 运行时白名单非空且命令在其中 → 直接放行(跳过 env var) + if !list.is_empty() { + return None; + } + } + + // 步骤3:回退到环境变量(兼容旧部署) let env_blacklist = std::env::var("DF_SCRIPT_BLACKLIST").ok(); let blacklist: Vec = match env_blacklist { Some(raw) => raw diff --git a/src-tauri/src/commands/settings.rs b/src-tauri/src/commands/settings.rs index 321d33e..9baab34 100644 --- a/src-tauri/src/commands/settings.rs +++ b/src-tauri/src/commands/settings.rs @@ -91,3 +91,37 @@ pub async fn ai_set_approval_timeout( 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)) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e3f0ce2..8b8b275 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -430,6 +430,9 @@ pub fn run() { // 审批超时配置(默认 15 分钟,0=禁用;Settings 页可改) commands::settings::ai_get_approval_timeout, commands::settings::ai_set_approval_timeout, + // ScriptNode 命令安全配置(白/黑名单,前端设置页写入) + commands::settings::set_script_safety, + commands::settings::get_script_safety, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/components/settings/AllowedDirsPanel.vue b/src/components/settings/AllowedDirsPanel.vue index 5e506a5..c1e15b6 100644 --- a/src/components/settings/AllowedDirsPanel.vue +++ b/src/components/settings/AllowedDirsPanel.vue @@ -79,7 +79,8 @@ import { ref, onMounted } from 'vue' import { open } from '@tauri-apps/plugin-dialog' import { useI18n } from 'vue-i18n' -import { aiApi } from '@/api' +import { invoke } from '@tauri-apps/api/core' +import { aiApi, settingsApi } from '@/api' import { useAppSettingsStore } from '@/stores/appSettings' import SettingRow from './SettingRow.vue' @@ -177,21 +178,36 @@ async function reload() { // ScriptNode 命令执行安全 — 白/黑名单 UI // ------------------------------------------------------------ // 前端持久化到 appSettings KV 表(df-script-whitelist / df-script-blacklist)。 -// 后端 ScriptNode 从环境变量 DF_SCRIPT_WHITELIST / DF_SCRIPT_BLACKLIST 读取(同语义逗号分隔)。 -// 前后端打通需跨 crate 接口变更(script_node.execute 接受参数注入),当前标注为已知限制。 -// 临时方案:启动时用脚本注入环境变量(export DF_SCRIPT_WHITELIST=...)。 +// 后端 ScriptNode 通过 set_script_safety IPC 注入运行时白/黑名单(即时生效)。 +// 本组件同时持久化到前端 KV(localStorage)和后端(app_settings 表+ OnceLock 全局态)。 // ============================================================ const scriptWhitelist = ref(appSettings.get('df-script-whitelist', '')) const scriptBlacklist = ref(appSettings.get('df-script-blacklist', '')) +async function syncScriptSafety() { + try { + await settingsApi.set('df-script-whitelist', scriptWhitelist.value.trim()) + await settingsApi.set('df-script-blacklist', scriptBlacklist.value.trim()) + // 走新的 set_script_safety IPC 同时持久化 + 注入运行时(一次调用完成两件事) + await invoke('set_script_safety', { + whitelist: scriptWhitelist.value.trim(), + blacklist: scriptBlacklist.value.trim(), + }) + emit('toast', t('settings.savedHint'), 'info') + } catch (e) { + console.error('[SCRIPT-SAFETY] 同步后端失败:', e) + emit('toast', t('settings.toastSaveFail', { msg: String(e) }), 'error') + } +} + function onScriptWhitelistCommit() { appSettings.set('df-script-whitelist', scriptWhitelist.value.trim()) - emit('toast', t('settings.savedHint'), 'info') + syncScriptSafety() } function onScriptBlacklistCommit() { appSettings.set('df-script-blacklist', scriptBlacklist.value.trim()) - emit('toast', t('settings.savedHint'), 'info') + syncScriptSafety() } onMounted(load)