修复: 高风险工具名单改注册表投影

新增 ai_list_tools 只读投影 name+risk,前端启动拉取构建
手写名单缺 10 个 High 工具致审批二次确认漏弹,现名单改兜底保留
顺带删除零调用方 get_script_safety 死命令及注册
This commit is contained in:
lxy
2026-10-01 08:28:07 +08:00
parent 4b445e96b8
commit 7b5a2c83b0
8 changed files with 88 additions and 16 deletions
+6 -1
View File
@@ -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<string[]>('ai_get_allowed_dirs')
},
/** 列出全部已注册 AI 工具的名称与风险级(审批高风险名单的权威数据源) */
listTools(): Promise<AiToolMeta[]> {
return invoke('ai_list_tools')
},
/** 设置 AI 工具授权目录(持久化 + 同步内存白名单),返回规范化后的列表 */
setAllowedDirs(dirs: string[]): Promise<string[]> {
return invoke<string[]>('ai_set_allowed_dirs', { dirs })
+6
View File
@@ -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'
}
// ============================================================
// 知识库
// ============================================================
+1
View File
@@ -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 数据为空 → @ 无匹配。
+11
View File
@@ -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,
// 侧栏宽度拖拽
+26 -3
View File
@@ -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<string>([
/**
* High 风险工具「兜底」名单——仅在后端工具元数据不可用时生效
* (启动早期尚未拉到 ai_list_tools / e2e 浏览器垫片无此 IPC)。
* 权威来源是后端工具注册表 risk_level:启动后经 syncHighRiskTools 整体覆盖,
* 不再手工对齐 tools/*.rs 分散注册(原双写已漂移:手写 7 项,后端 High 实际 17 项)。
*/
const HIGH_RISK_TOOLS_FALLBACK = new Set<string>([
'delete_task', 'delete_project', 'restore_project', 'purge_project', 'delete_file', 'run_command',
'http_request',
])
/** 后端注册表投影的 High 风险名单;null = 尚未拉到元数据(走兜底名单) */
let registryHighRisk: Set<string> | 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<AiToolCallInfo>, 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 // 取消二次确认 → 解锁