From 7b5a2c83b07811efe4d0deb82961a898c4aa9001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Thu, 1 Oct 2026 08:28:07 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20=E9=AB=98=E9=A3=8E?= =?UTF-8?q?=E9=99=A9=E5=B7=A5=E5=85=B7=E5=90=8D=E5=8D=95=E6=94=B9=E6=B3=A8?= =?UTF-8?q?=E5=86=8C=E8=A1=A8=E6=8A=95=E5=BD=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 ai_list_tools 只读投影 name+risk,前端启动拉取构建 手写名单缺 10 个 High 工具致审批二次确认漏弹,现名单改兜底保留 顺带删除零调用方 get_script_safety 死命令及注册 --- src-tauri/src/commands/ai/commands/config.rs | 36 ++++++++++++++++++++ src-tauri/src/commands/settings.rs | 11 ------ src-tauri/src/lib.rs | 3 +- src/api/ai.ts | 7 +++- src/api/types.ts | 6 ++++ src/components/AiChat.vue | 1 + src/composables/ai/useAiPanel.ts | 11 ++++++ src/composables/ai/useToolApproval.ts | 29 ++++++++++++++-- 8 files changed, 88 insertions(+), 16 deletions(-) diff --git a/src-tauri/src/commands/ai/commands/config.rs b/src-tauri/src/commands/ai/commands/config.rs index a1c0520..866361f 100644 --- a/src-tauri/src/commands/ai/commands/config.rs +++ b/src-tauri/src/commands/ai/commands/config.rs @@ -24,6 +24,8 @@ use tauri::State; // 来自 commands/ai/skills.rs,本域 IPC 复用,不重复定义。 use super::super::skills::{invalidate_skills, skills_cached, SkillInfo}; +use df_ai::ai_tools::RiskLevel; + use crate::state::AppState; /// 列出本机 Claude 技能(skills + commands + plugins 三类),供前端 `/` 联想 @@ -128,3 +130,37 @@ pub async fn ai_set_allowed_dirs( ) -> Result, String> { state.set_allowed_dirs(dirs).await.map_err(|e| e.to_string()) } + +// ============================================================ +// AI 工具风险元数据投影(只读,前端审批二次确认门控的数据源) +// ============================================================ + +/// 单个 AI 工具的风险元数据(来自 AiToolRegistry 注册时声明的 risk_level)。 +/// risk 沿用 RiskLevel 的 serde 序列化(小写 low/medium/high,与 audit risk_str 同口径)。 +#[derive(serde::Serialize)] +pub struct AiToolMeta { + pub name: String, + pub risk: RiskLevel, +} + +/// 列出全部已注册 AI 工具的名称与风险级。 +/// +/// 唯一消费方:前端启动(AiChat onMounted)拉取后构建 High 风险名单, +/// 供审批二次确认门控(useToolApproval)。数据源 = 工具注册表元数据, +/// 根治前端手写名单与 tools/*.rs 分散注册的双写漂移(新增 High 工具免改前端)。 +#[tauri::command] +pub async fn ai_list_tools(state: State<'_, AppState>) -> Result, String> { + let registry = state.ai_tools.as_ref(); + // tool_names 拿 owned 键再 get 取 risk(不给 df-ai 加迭代器导出);排序稳定输出(HashMap 遍历序随机) + let mut metas: Vec = registry + .tool_names() + .into_iter() + .filter_map(|name| { + registry + .get(&name) + .map(|t| AiToolMeta { name, risk: t.risk_level }) + }) + .collect(); + metas.sort_by(|a, b| a.name.cmp(&b.name)); + Ok(metas) +} diff --git a/src-tauri/src/commands/settings.rs b/src-tauri/src/commands/settings.rs index a02517b..d4ffb78 100644 --- a/src-tauri/src/commands/settings.rs +++ b/src-tauri/src/commands/settings.rs @@ -116,17 +116,6 @@ pub async fn set_script_safety( Ok(()) } -/// 读取脚本安全配置。 -#[tauri::command] -pub async fn get_script_safety( - state: State<'_, AppState>, -) -> Result<(String, String), String> { - // appSettings.set(JSON.stringify 带引号)与 settingsApi.set(裸串)双通道写入并存,读端归一 - let wl = state.settings.get("df-script-whitelist").await.map_err(err_str)?.map(|v| v.trim_matches('"').to_string()).unwrap_or_default(); - let bl = state.settings.get("df-script-blacklist").await.map_err(err_str)?.map(|v| v.trim_matches('"').to_string()).unwrap_or_default(); - Ok((wl, bl)) -} - /// 设置多 ReAct Plan 执行开关(默认关)。 /// 开启后 process_tool_calls 内以 JoinSet 并行执行同层工具。 #[tauri::command] diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 9944c0b..d5d5250 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -462,6 +462,8 @@ pub fn run() { // F-260619-03 Phase A: AI 工具文件访问授权目录白名单 commands::ai::ai_get_allowed_dirs, commands::ai::ai_set_allowed_dirs, + // 工具风险元数据投影(前端启动构建 High 风险名单,审批二次确认门控) + commands::ai::ai_list_tools, // F-260619-03 Phase B: 路径授权弹窗决策(消费 AiDirAuthRequired 挂起) commands::ai::ai_authorize_dir, // 审批历史面板查 ai_tool_executions 表,敏感字段截断) @@ -495,7 +497,6 @@ pub fn run() { commands::settings::ai_set_approval_timeout, // ScriptNode 命令安全配置(白/黑名单,前端设置页写入) commands::settings::set_script_safety, - commands::settings::get_script_safety, // Plan 执行开关 commands::settings::set_plan_execution, commands::settings::get_plan_execution, diff --git a/src/api/ai.ts b/src/api/ai.ts index 3acf60f..bb03fcd 100644 --- a/src/api/ai.ts +++ b/src/api/ai.ts @@ -2,7 +2,7 @@ import { invoke } from '@tauri-apps/api/core' import { listen, type UnlistenFn } from '@tauri-apps/api/event' -import type { AiChatEvent, AiConversationDetail, AiConversationSummary, AiProviderConfig, ContentPart, MentionSpan, ModelConfig, SkillInfo } from './types' +import type { AiChatEvent, AiConversationDetail, AiConversationSummary, AiProviderConfig, AiToolMeta, ContentPart, MentionSpan, ModelConfig, SkillInfo } from './types' export const aiApi = { /** @@ -272,6 +272,11 @@ export const aiApi = { return invoke('ai_get_allowed_dirs') }, + /** 列出全部已注册 AI 工具的名称与风险级(审批高风险名单的权威数据源) */ + listTools(): Promise { + return invoke('ai_list_tools') + }, + /** 设置 AI 工具授权目录(持久化 + 同步内存白名单),返回规范化后的列表 */ setAllowedDirs(dirs: string[]): Promise { return invoke('ai_set_allowed_dirs', { dirs }) diff --git a/src/api/types.ts b/src/api/types.ts index 94bfdda..a4acc1a 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -778,6 +778,12 @@ export interface SkillInfo { path: string } +/** AI 工具风险元数据(ai_list_tools 投影;risk 对齐后端 RiskLevel serde 小写) */ +export interface AiToolMeta { + name: string + risk: 'low' | 'medium' | 'high' +} + // ============================================================ // 知识库 // ============================================================ diff --git a/src/components/AiChat.vue b/src/components/AiChat.vue index 699c198..8929266 100644 --- a/src/components/AiChat.vue +++ b/src/components/AiChat.vue @@ -498,6 +498,7 @@ onMounted(async () => { await store.loadProviders() await store.loadConversations() store.loadSkills() + store.loadToolRisks() // @ 提及预加载: AiChat 的 @ 联想读取 projectsStore.projects/tasks, // 但 loadProjects/loadTasks 分别绑在 Projects.vue/Tasks.vue 的 onMounted。 // 若用户启动后直接用 AI Chat(未访问过这两个页面), store 数据为空 → @ 无匹配。 diff --git a/src/composables/ai/useAiPanel.ts b/src/composables/ai/useAiPanel.ts index 54a4392..8ce6a7c 100644 --- a/src/composables/ai/useAiPanel.ts +++ b/src/composables/ai/useAiPanel.ts @@ -13,6 +13,7 @@ import { aiApi } from '@/api' import { useAppSettingsStore } from '@/stores/appSettings' import { state } from '@/stores/ai' import { setStreaming } from './streamingGuard' +import { syncHighRiskTools } from './useToolApproval' import { nextMsgId } from './aiShared' import { t } from '@/i18n/i18n-helpers' import type { AiMessage, AiProviderConfig } from '@/api/types' @@ -158,6 +159,15 @@ async function loadSkills() { } } +/** 拉取工具风险元数据,构建 High 风险名单(审批二次确认门控的权威源;静默失败走 useToolApproval 兜底名单) */ +async function loadToolRisks() { + try { + syncHighRiskTools(await aiApi.listTools()) + } catch { + // 静默失败:e2e 垫片/老后端无此 IPC 时兜底名单仍在,不影响主流程 + } +} + /** 切换活跃 provider(后端 + 本地 state + 持久化) */ async function setProvider(providerId: string) { await aiApi.setProvider(providerId) @@ -199,6 +209,7 @@ export function useAiPanel() { cyclePanelMode, loadProviders, loadSkills, + loadToolRisks, setProvider, clearChat, // 侧栏宽度拖拽 diff --git a/src/composables/ai/useToolApproval.ts b/src/composables/ai/useToolApproval.ts index a91ce45..885a42e 100644 --- a/src/composables/ai/useToolApproval.ts +++ b/src/composables/ai/useToolApproval.ts @@ -22,12 +22,35 @@ import type { AiToolCallInfo } from '@/api/types' */ const APPROVE_LOADING_TIMEOUT_MS = 30_000 -/** High 风险工具白名单(后端 tool_registry.rs RiskLevel::High 对齐) */ -const HIGH_RISK_TOOLS = new Set([ +/** + * High 风险工具「兜底」名单——仅在后端工具元数据不可用时生效 + * (启动早期尚未拉到 ai_list_tools / e2e 浏览器垫片无此 IPC)。 + * 权威来源是后端工具注册表 risk_level:启动后经 syncHighRiskTools 整体覆盖, + * 不再手工对齐 tools/*.rs 分散注册(原双写已漂移:手写 7 项,后端 High 实际 17 项)。 + */ +const HIGH_RISK_TOOLS_FALLBACK = new Set([ 'delete_task', 'delete_project', 'restore_project', 'purge_project', 'delete_file', 'run_command', 'http_request', ]) +/** 后端注册表投影的 High 风险名单;null = 尚未拉到元数据(走兜底名单) */ +let registryHighRisk: Set | null = null + +/** + * 用后端 ai_list_tools 元数据构建 High 风险名单(权威源,整体覆盖兜底)。 + * 空结果不覆盖:注册表为空/数据异常时保留兜底名单,安全侧不放松。 + * 供启动初始化调用(useAiPanel.loadToolRisks),幂等可重复调。 + */ +export function syncHighRiskTools(tools: { name: string; risk: string }[]): void { + const high = tools.filter((t) => t.risk === 'high').map((t) => t.name) + if (high.length > 0) registryHighRisk = new Set(high) +} + +/** 审批二次确认门控:优先注册表投影,未拉到元数据时退兜底名单 */ +function isHighRiskTool(name: string): boolean { + return (registryHighRisk ?? HIGH_RISK_TOOLS_FALLBACK).has(name) +} + /** * 审批 IPC 分派(共享):按 tc.kind 决定调 aiApi.authorizeDir(path 类)或 aiApi.approve(risk 类)。 * @@ -150,7 +173,7 @@ export function useToolApproval(tc: Ref, emit: ToolApprovalEmit) approving.value = true try { // High 风险 → 先二次确认 - if (HIGH_RISK_TOOLS.has(tc.value.name)) { + if (isHighRiskTool(tc.value.name)) { const ok = await confirmDialog(highRiskConfirmMsg(tc.value.name)) if (!ok) { approving.value = false // 取消二次确认 → 解锁