feat: 升级至 v0.3.9 — 工具批量审批 + run_command 频率限制优化 + Toast 规范

1. 工具批量审批:解决并行工具审批弹框覆盖问题,新增批量 IPC 通道和列表 UI,支持同工具多次调用分组展示;2. 审批弹框健壮性:超时 toast 防风暴、agent 状态同步清空、ErrorBoundary 防白屏;3. run_command maxFrequency 从 3 调整为 10;4. 开发规范新增 Toast 通知铁律(必须使用 MeToast)
This commit is contained in:
2026-07-21 11:43:41 +08:00
parent 76bd72b702
commit 64af91bde9
11 changed files with 608 additions and 177 deletions
+112 -7
View File
@@ -22,6 +22,11 @@ export interface ConfirmationRequest {
args: Record<string, unknown>;
riskLevel: string;
reason: string;
/**
* 过期时间戳(ms),由 waitForConfirmation 注入,用于前端倒计时 UI。
* 注意:beforeExecute 构造 request 时不带此字段,仅在 waitForConfirmation 中追加。
*/
expiresAt?: number;
}
export class ConfirmationHook implements PreToolHook {
@@ -41,8 +46,17 @@ export class ConfirmationHook implements PreToolHook {
/** 持久化自动执行的工具集合(从 ConfigService 加载,跨会话生效) */
private autoExecuteTools = new Set<string>();
/** 等待确认的 Promise 解析器 */
private pendingConfirmations = new Map<string, { resolve: (v: boolean) => void; timer: NodeJS.Timeout; toolName: string; expiresAt: number }>();
/** 等待确认的 Promise 解析器(含完整请求信息,供 getPendingConfirmations 返回) */
private pendingConfirmations = new Map<string, {
resolve: (v: boolean) => void;
timer: NodeJS.Timeout;
toolName: string;
expiresAt: number;
/** v0.3.2 批量审批:缓存完整请求信息,供 getPendingConfirmations() 重建 ConfirmationRequest */
args?: Record<string, unknown>;
riskLevel?: string;
reason?: string;
}>();
/** 确认超时时间(可从配置读取,默认 120 秒) */
private confirmationTimeoutMs = 120_000;
@@ -171,6 +185,78 @@ export class ConfirmationHook implements PreToolHook {
}
}
/**
* 批量处理用户确认响应(由 IPC tool:confirmationResponseBatch 调用)
*
* 设计要点:
* 1. 单次批量 resolve 即可处理 N 个 pending(避免逐个 IPC 往返)
* 2. remember/autoExecute 按 toolName 去重写入,避免 Map 重复赋值
* 3. 批量拒绝时若 remember=true 也会写入"拒绝"记忆(与单条语义一致)
*
* @param toolCallIds 待处理的 toolCallId 列表
* @param approved 批准/拒绝
* @param remember 会话内记住决定(按 toolName 去重)
* @param autoExecute 永久自动执行(仅 approved=true 时生效,按 toolName 去重)
* @returns 实际处理成功的 toolCallId 数组(未找到的会被跳过)
*/
resolveConfirmationsBatch(
toolCallIds: string[],
approved: boolean,
remember: boolean,
autoExecute: boolean = false,
): string[] {
const resolved: string[] = [];
// 同一 toolName 在批量中可能多次出现,仅写入一次决策记忆
const processedToolNames = new Set<string>();
for (const id of toolCallIds) {
const pending = this.pendingConfirmations.get(id);
if (!pending) continue; // 已超时或不存在,跳过
clearTimeout(pending.timer);
pending.resolve(approved);
this.pendingConfirmations.delete(id);
resolved.push(id);
// 按工具名去重写入决策(同一工具的多次调用共享一次决策)
if (!processedToolNames.has(pending.toolName)) {
processedToolNames.add(pending.toolName);
if (autoExecute && approved) {
this.setAutoExecute(pending.toolName, true);
}
if (remember) {
this.rememberedDecisions.set(pending.toolName, approved);
}
}
}
return resolved;
}
/**
* 获取当前所有等待中的确认请求(供前端批量审批 UI 拉取已积压的请求)
*
* 场景:并行工具触发的多个 IPC 事件可能在前端 state 中互相覆盖,
* 前端可在弹框打开时主动调用此接口,确保拿到完整的 pending 列表。
*
* @returns pending 确认请求的快照(含 toolCallId/toolName/args/riskLevel/reason/expiresAt
*/
getPendingConfirmations(): ConfirmationRequest[] {
const result: ConfirmationRequest[] = [];
for (const [id, pending] of this.pendingConfirmations) {
// 重建 ConfirmationRequest(前端需要 args 用于展示参数详情)
// pending 已在 waitForConfirmation 中缓存完整请求信息
result.push({
toolCallId: id,
toolName: pending.toolName,
args: pending.args ?? {},
riskLevel: pending.riskLevel ?? 'medium',
reason: pending.reason ?? `Tool "${pending.toolName}" requires confirmation`,
expiresAt: pending.expiresAt,
});
}
return result;
}
async beforeExecute(toolCall: MetonaToolCall, _sessionId: string): Promise<HookResult> {
const def = this.toolDefs.get(toolCall.name);
if (!def) {
@@ -227,7 +313,13 @@ export class ConfirmationHook implements PreToolHook {
/**
* 等待用户确认(带超时)
* 发送 expiresAt 到前端,供倒计时 UI 使用;超时时发送 toast 通知
*
* v0.3.2 修复:超时 toast 防风暴
* 并行工具全部超时时会触发 N 个 toast,用 lastToastAt 节流(3 秒内只发 1 条),
* 且消息改为汇总形式"工具确认超时,N 个工具未执行"。
*/
private lastTimeoutToastAt = 0;
private waitForConfirmation(request: ConfirmationRequest): Promise<boolean> {
return new Promise<boolean>((resolve) => {
const expiresAt = Date.now() + this.confirmationTimeoutMs;
@@ -235,12 +327,21 @@ export class ConfirmationHook implements PreToolHook {
// 设置超时
const timer = setTimeout(() => {
this.pendingConfirmations.delete(request.toolCallId);
// 超时发送 toast 通知用户
// 超时发送 toast 通知用户3 秒节流,防止并行工具风暴)
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
this.mainWindow.webContents.send('toast:show', {
type: 'warning',
message: `工具确认超时(${Math.round(this.confirmationTimeoutMs / 1000)}秒),"${request.toolName}" 未执行`,
});
const now = Date.now();
if (now - this.lastTimeoutToastAt > 3000) {
this.lastTimeoutToastAt = now;
// 统计当前还有多少 pending(含本次刚超时的)
const pendingCount = this.pendingConfirmations.size + 1;
const message = pendingCount > 1
? `工具确认超时(${Math.round(this.confirmationTimeoutMs / 1000)}秒),${pendingCount} 个工具未执行`
: `工具确认超时(${Math.round(this.confirmationTimeoutMs / 1000)}秒),"${request.toolName}" 未执行`;
this.mainWindow.webContents.send('toast:show', {
type: 'warning',
message,
});
}
}
resolve(false); // 超时视为拒绝
}, this.confirmationTimeoutMs);
@@ -250,6 +351,10 @@ export class ConfirmationHook implements PreToolHook {
timer,
toolName: request.toolName,
expiresAt,
// v0.3.2 批量审批:同步缓存完整请求信息,供 getPendingConfirmations() 返回
args: request.args,
riskLevel: request.riskLevel,
reason: request.reason,
});
// 发送确认请求到渲染进程(携带过期时间戳,供前端倒计时)
+1 -1
View File
@@ -39,7 +39,7 @@ export const DEFAULT_POLICIES: PermissionPolicy[] = [
{ toolName: 'memory_search', requiredLevel: PermissionLevel.READ },
{ toolName: 'write_file', requiredLevel: PermissionLevel.WRITE, deniedPatterns: [/\/etc(?:\/|["'\s,}]|$)/, /\/proc(?:\/|["'\s,}]|$)/, /\/System(?:\/|["'\s,}]|$)/, /C:\\Windows\\/i, /C:\\System32\\/i], requireConfirmation: true, maxFrequency: 5 },
{ toolName: 'memory_store', requiredLevel: PermissionLevel.WRITE },
{ toolName: 'run_command', requiredLevel: PermissionLevel.EXTERNAL_ACTION, deniedPatterns: [/MEMORY\.md/i], requireConfirmation: true, maxFrequency: 3 },
{ toolName: 'run_command', requiredLevel: PermissionLevel.EXTERNAL_ACTION, deniedPatterns: [/MEMORY\.md/i], requireConfirmation: true, maxFrequency: 10 },
{ toolName: 'web_fetch', requiredLevel: PermissionLevel.READ },
// web_browser — 统一浏览器工具(合并自 9 个独立 browser_* 工具)
// 由于该工具可执行 JS、点击元素等高风险操作,统一设为 EXTERNAL_ACTION