新增: 路径授权三档化 once/session/always

Phase B 弹窗 once/always → once/session/always 三档(本次单次执行后清/当前会话切会话清/始终落KV)。修 once 原写 session 致本次实为本会话名实不符。AllowedDirs 加 once 层 + ai_authorize_dir decision 改 match 四分支 + 执行后 clear_once。
This commit is contained in:
2026-06-25 00:23:07 +08:00
parent 56a08fb5c1
commit 24c3fa08f1
10 changed files with 116 additions and 46 deletions

View File

@@ -31,8 +31,8 @@ const _pendingApprovalIds = new Set<string>()
* 按钮无 loading 中间态、用户无重审入口;loading 现下沉到 ToolCard 局部 ref,语义正确。
*
* path_auth 审批链阶段3b(统一审批模型):**按 tc.kind 分派**两条 IPC——
* - kind='path'(路径授权挂起):调 aiApi.authorizeDir(toolCallId, decision) once/always/deny。
* once=本次会话授权 / always=写持久白名单 / deny=拒绝。
* - kind='path'(路径授权挂起):调 aiApi.authorizeDir(toolCallId, decision) once/session/always/deny。
* once=本次单次(执行后清) / session=当前会话(切会话清) / always=写持久白名单 / deny=拒绝。
* - kind='risk'(普通 RiskLevel 审批,默认):调 aiApi.approve(toolCallId, approved) 一次性 approve/reject。
* approved=true→放行执行,approved=false→拒绝。
* 入口据 findToolCall(toolCallId).kind 判定,kind 缺省走 risk 老路径(兼容非统一开关 / 老 toolCall)。
@@ -40,16 +40,16 @@ const _pendingApprovalIds = new Set<string>()
* 调用方传 decision 但 tc.kind='risk' 时仍走 approve(approved)分支(忽略 decision)。
*
* @param approved risk 类专用(approve/reject, approve=true/reject=false);path 类忽略。
* @param decision path 类专用('once'|'always'|'deny');risk 类忽略。调用方须据 tc.kind 传对应入参。
* @param decision path 类专用('once'|'session'|'always'|'deny');risk 类忽略。调用方须据 tc.kind 传对应入参。
*/
async function approveToolCall(toolCallId: string, approved: boolean, decision?: 'once' | 'always' | 'deny') {
async function approveToolCall(toolCallId: string, approved: boolean, decision?: 'once' | 'session' | 'always' | 'deny') {
// F-260616-06: 防抖——同一 id 仍在处理中则跳过(竞态点击/网络慢重试)
if (_pendingApprovalIds.has(toolCallId)) return
_pendingApprovalIds.add(toolCallId)
try {
// 复用 findToolCall(反向扫描)避免重复实现查找逻辑;仅缓存引用用于 IPC 失败回滚
const tc = findToolCall(toolCallId)
// path_auth 审批链阶段3b:按 tc.kind 分派 IPC。kind='path' 走 authorizeDir(once/always/deny);
// path_auth 审批链阶段3b:按 tc.kind 分派 IPC。kind='path' 走 authorizeDir(once/session/always/deny);
// 其余(risk/缺省)走 approve(approve/reject)。
const isPathKind = !!tc && tc.kind === 'path'
// 重启看门狗覆盖审批执行→续生成窗口(useAiEvents.ts AiApprovalRequired/AiDirAuthRequired 已 clear,
@@ -95,7 +95,7 @@ async function approveToolCall(toolCallId: string, approved: boolean, decision?:
*
* path_auth 审批链阶段3b:统一开关开后,pendingApprovals 可能混合 path + risk 两类挂起。
* 批量操作语义:
* - decision='approve':risk 类 approved=true;path 类 decision='once'(批量授权本次,
* - decision='approve':risk 类 approved=true;path 类 decision='once'(批量授权本次单次,
* 不批量写持久白名单——always 是单卡语义,批量误写持久白名单风险高,故批量只 once)。
* - decision='reject':risk 类 approved=false;path 类 decision='deny'。
* 单条失败不中断批量(已由 approveToolCall 内部回滚该条状态)。
@@ -106,7 +106,7 @@ async function batchApprove(decision: 'approve' | 'reject') {
for (const p of items) {
try {
if (p.kind === 'path') {
// path 类:approve→once(本次,不批量写持久白名单), reject→deny
// path 类:approve→once(本次单次,不批量写持久白名单), reject→deny
await approveToolCall(p.id, false, decision === 'approve' ? 'once' : 'deny')
} else {
// risk 类:approve/reject boolean