wezard 1.3.13 → 1.3.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/daemon/approval.js +187 -6
- package/dist/daemon/approval.js.map +1 -1
- package/dist/daemon/mirror-bridge.js +142 -14
- package/dist/daemon/mirror-bridge.js.map +1 -1
- package/dist/daemon/quota.js +13 -13
- package/dist/daemon/quota.js.map +1 -1
- package/dist/shared/modal-pane.js +107 -12
- package/dist/shared/modal-pane.js.map +1 -1
- package/dist/shared/session-label.js +4 -1
- package/dist/shared/session-label.js.map +1 -1
- package/hooks/pre-tool-use.sh +23 -2
- package/package.json +1 -1
package/dist/daemon/approval.js
CHANGED
|
@@ -1093,6 +1093,151 @@ export const runMirrorPlanFlow = async ({ log, client, sessionId, chatKey, cwd,
|
|
|
1093
1093
|
log.info({ reqId, sessionId, approve }, "mirror plan resolved via send-keys");
|
|
1094
1094
|
await note(approve ? "✅ 已同意,退出 plan mode 开始执行…" : "✏️ 已选择继续完善计划。");
|
|
1095
1095
|
};
|
|
1096
|
+
const mirrorPickerSlots = new Map();
|
|
1097
|
+
const MIRROR_PICKER_SLOT_TTL_MS = 12 * 3600_000; // 与 approval.longPollSec 对齐
|
|
1098
|
+
const freshPickerSlot = (sessionId) => {
|
|
1099
|
+
const s = mirrorPickerSlots.get(sessionId);
|
|
1100
|
+
if (!s)
|
|
1101
|
+
return undefined;
|
|
1102
|
+
if (Date.now() - s.at > MIRROR_PICKER_SLOT_TTL_MS) {
|
|
1103
|
+
mirrorPickerSlots.delete(sessionId);
|
|
1104
|
+
return undefined;
|
|
1105
|
+
}
|
|
1106
|
+
return s;
|
|
1107
|
+
};
|
|
1108
|
+
export const hasMirrorPicker = (sessionId) => freshPickerSlot(sessionId) !== undefined;
|
|
1109
|
+
export const mootMirrorPicker = (sessionId) => {
|
|
1110
|
+
const s = freshPickerSlot(sessionId);
|
|
1111
|
+
if (!s)
|
|
1112
|
+
return;
|
|
1113
|
+
if (s.reqId)
|
|
1114
|
+
resolvePending(s.reqId, "moot");
|
|
1115
|
+
mirrorPickerSlots.delete(sessionId);
|
|
1116
|
+
};
|
|
1117
|
+
// ── picker 代按 → 迟到 hook 请求的去重 ─────────────────────────────────
|
|
1118
|
+
// 这类调用的 hook 在本地确认框被按掉**之后**才到 (CodeBuddy「本地面板先行」,
|
|
1119
|
+
// 与 askq 同源)。代按 Yes 的那一下就是人工审批结论 —— hook 随后送来的同一调用
|
|
1120
|
+
// 不该再发第二张卡。一次性消费 + 短 TTL: 只豁免紧跟着代按的那一次上报, 不构成
|
|
1121
|
+
// 任何持久放行。
|
|
1122
|
+
const recentPickerAllows = new Map(); // `${sessionId}|${toolName}` → at
|
|
1123
|
+
const RECENT_PICKER_TTL_MS = 60_000;
|
|
1124
|
+
const recordPickerAllow = (sessionId, toolName) => {
|
|
1125
|
+
recentPickerAllows.set(`${sessionId}|${toolName}`, Date.now());
|
|
1126
|
+
};
|
|
1127
|
+
const consumePickerAllow = (sessionId, toolName) => {
|
|
1128
|
+
const k = `${sessionId}|${toolName}`;
|
|
1129
|
+
const at = recentPickerAllows.get(k);
|
|
1130
|
+
if (at === undefined)
|
|
1131
|
+
return false;
|
|
1132
|
+
recentPickerAllows.delete(k);
|
|
1133
|
+
return Date.now() - at <= RECENT_PICKER_TTL_MS;
|
|
1134
|
+
};
|
|
1135
|
+
export const runMirrorPickerFlow = async ({ log, client, cfg, sessionId, chatKey, cwd, jsonlPath, toolName, toolInput, agent, danger, voteTimeoutMs, press }) => {
|
|
1136
|
+
if (!client.isConnected)
|
|
1137
|
+
return;
|
|
1138
|
+
const target = targetChatId(chatKey);
|
|
1139
|
+
const note = async (content) => {
|
|
1140
|
+
try {
|
|
1141
|
+
await client.sendMessage(target, { msgtype: "markdown", markdown: { content: withTagHeader(chatKey, content) } });
|
|
1142
|
+
}
|
|
1143
|
+
catch { /* best-effort */ }
|
|
1144
|
+
};
|
|
1145
|
+
const tail = agent ? `子代理 ${agent} 的原生权限确认` : "CLI 原生权限确认";
|
|
1146
|
+
const sessionName = sessionNameFor(chatKey, jsonlPath, sessionId);
|
|
1147
|
+
const { reqId, promise } = createPending({
|
|
1148
|
+
meta: {
|
|
1149
|
+
kind: "generic",
|
|
1150
|
+
createdAt: Date.now(),
|
|
1151
|
+
toolName,
|
|
1152
|
+
toolInput,
|
|
1153
|
+
cwd,
|
|
1154
|
+
sessionId,
|
|
1155
|
+
chatKey,
|
|
1156
|
+
transcriptTail: tail,
|
|
1157
|
+
danger,
|
|
1158
|
+
forceSingle: Boolean(danger),
|
|
1159
|
+
sessionName,
|
|
1160
|
+
},
|
|
1161
|
+
timeoutMs: voteTimeoutMs,
|
|
1162
|
+
});
|
|
1163
|
+
mirrorPickerSlots.set(sessionId, { reqId, at: Date.now() });
|
|
1164
|
+
try {
|
|
1165
|
+
await client.sendMessage(target, {
|
|
1166
|
+
msgtype: "template_card",
|
|
1167
|
+
template_card: buildCard({
|
|
1168
|
+
reqId,
|
|
1169
|
+
toolName,
|
|
1170
|
+
toolInput,
|
|
1171
|
+
toolInputStr: "",
|
|
1172
|
+
cwd,
|
|
1173
|
+
sessionShort: sessionId.slice(-8),
|
|
1174
|
+
sessionId,
|
|
1175
|
+
chatKey,
|
|
1176
|
+
transcriptTail: tail,
|
|
1177
|
+
windowMinutes: cfg.approval.windowMinutes,
|
|
1178
|
+
danger,
|
|
1179
|
+
forceSingle: Boolean(danger),
|
|
1180
|
+
sessionName,
|
|
1181
|
+
}),
|
|
1182
|
+
});
|
|
1183
|
+
log.info({ reqId, chatKey, toolName, agent }, "mirror picker card sent");
|
|
1184
|
+
}
|
|
1185
|
+
catch (e) {
|
|
1186
|
+
log.error({ err: e.message }, "mirror picker card send failed");
|
|
1187
|
+
resolvePending(reqId, "deny");
|
|
1188
|
+
mirrorPickerSlots.delete(sessionId);
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
let raw;
|
|
1192
|
+
try {
|
|
1193
|
+
raw = (await promise);
|
|
1194
|
+
}
|
|
1195
|
+
catch {
|
|
1196
|
+
mirrorPickerSlots.delete(sessionId);
|
|
1197
|
+
await note(`⌛ 权限确认卡已超时(${toolName}),请在 CLI 中处理。`);
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
// 本地先按掉 (哨兵读到框消失触发的 moot) — 槽位已由 mootMirrorPicker 清除。
|
|
1201
|
+
if (raw === "moot") {
|
|
1202
|
+
log.info({ reqId, sessionId }, "mirror picker mooted by local answer");
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
// 槽位保持到代按完成 — 哨兵靠它跳过在途 pane; 提前清会与代按赛跑出双按键。
|
|
1206
|
+
try {
|
|
1207
|
+
const d = raw;
|
|
1208
|
+
if (d === "deny") {
|
|
1209
|
+
const r = await press("no");
|
|
1210
|
+
await note(r.ok
|
|
1211
|
+
? `🚫 已替你拒绝 CLI 权限确认(${toolName})→ ${r.label ?? "Escape"}`
|
|
1212
|
+
: `⚠️ 已记录拒绝,但代按失败(${r.reason ?? "未知"}),请回 CLI 处理。`);
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
if (d === "allow_window" && cfg.approval.windowMinutes > 0) {
|
|
1216
|
+
setAutoWindow(chatKey, cfg.approval.windowMinutes * 60_000, { toolName, toolInput, cwd, transcriptTail: tail });
|
|
1217
|
+
// 与 handler 的 allow_window 同款 sweep: 同 chat 其它 pending 卡一并放行。
|
|
1218
|
+
const swept = resolvePendingsByChat(chatKey, "allow_window", reqId);
|
|
1219
|
+
log.info({ chatKey, minutes: cfg.approval.windowMinutes, swept: swept.length }, "auto-window opened via picker card");
|
|
1220
|
+
if (swept.length > 0)
|
|
1221
|
+
await note(`⚡ 已批量自动放行其他 ${swept.length} 个并发请求。`);
|
|
1222
|
+
}
|
|
1223
|
+
const wantSession = d === "allow_always" || d === "allow_session";
|
|
1224
|
+
const r = await press(wantSession ? "yes_session" : "yes");
|
|
1225
|
+
if (!r.ok) {
|
|
1226
|
+
await note(`⚠️ 已记录同意,但代按失败(${r.reason ?? "未知"}),请回 CLI 手动确认。`);
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
// 代按成功 → CLI 才真正派发工具 → hook 此刻才会上报同一调用; 预先记账让
|
|
1230
|
+
// handler 直接放行, 不再发第二张卡 (consumePickerAllow)。
|
|
1231
|
+
recordPickerAllow(sessionId, toolName);
|
|
1232
|
+
await note(wantSession
|
|
1233
|
+
? `🔓 已代按「本会话不再询问」(${toolName})→ ${r.label ?? ""}\n原生确认框不过 hook,「总是」规则帮不上它,已翻译成 CLI 的会话级免问。`
|
|
1234
|
+
: `🔓 已代按 CLI 权限确认(${toolName})→ ${r.label ?? "Yes"}${d === "allow_window" ? `,并开启 ${cfg.approval.windowMinutes} 分钟自动放行窗口` : ""}`);
|
|
1235
|
+
log.info({ reqId, sessionId, decision: d, pressed: r.label }, "mirror picker resolved via send-keys");
|
|
1236
|
+
}
|
|
1237
|
+
finally {
|
|
1238
|
+
mirrorPickerSlots.delete(sessionId);
|
|
1239
|
+
}
|
|
1240
|
+
};
|
|
1096
1241
|
// 多问题: 逐题顺序发卡 → 收答 → 下一题。任一题选「CLI」整体转 CLI,选「聊聊」
|
|
1097
1242
|
// 整体转讨论; 全部答完合并成单个 deny+reason 注入。卡不会一次性轰炸 N 张。
|
|
1098
1243
|
const handleAskUserQuestion = async ({ cfg, log, client, body, getMirrorTarget, flushBeforeCard, clientGone }) => {
|
|
@@ -1256,20 +1401,45 @@ const resolveApprover = (cfg, sessionId, getMirrorTarget) => {
|
|
|
1256
1401
|
// 只剩 transcript_path + agent 标记能对回父会话。session_id 无镜像绑定时, 按
|
|
1257
1402
|
// transcript_path 反推父会话并路由过去 —— 否则这类请求落到 ask 兜底, 在镜像
|
|
1258
1403
|
// pane 里变成远程无人可点的原生确认框 (卡住、无卡片), skipAll/卡片全部失联。
|
|
1404
|
+
const SUBAGENT_TRANSCRIPT_RE = /\/([^/]+)\/subagents\/agent-[^/]+\.jsonl$/;
|
|
1259
1405
|
const subagentParentOf = (transcriptPath, agentId, agentType) => {
|
|
1260
|
-
if (!transcriptPath
|
|
1406
|
+
if (!transcriptPath)
|
|
1261
1407
|
return undefined;
|
|
1262
1408
|
const norm = transcriptPath.replace(/\\/g, "/");
|
|
1263
|
-
// `<projectDir>/<encodedCwd>/<parentSid>/subagents/agent-*.jsonl` —
|
|
1264
|
-
|
|
1409
|
+
// `<projectDir>/<encodedCwd>/<parentSid>/subagents/agent-*.jsonl` — 父在上一级。
|
|
1410
|
+
// 布局本身就是子代理证据, 不要求 agent 标记 —— 部分 CodeBuddy 版本的子代理
|
|
1411
|
+
// hook 不带 agent_id/agent_type, 只认标记会让这类请求丢失父归属。
|
|
1412
|
+
const sub = norm.match(SUBAGENT_TRANSCRIPT_RE);
|
|
1265
1413
|
if (sub)
|
|
1266
1414
|
return sub[1];
|
|
1267
1415
|
// 直接落在 `<projectDir>/<uuid>.jsonl` 的主转录布局 (CC 子代理 hook 给的是父转录)
|
|
1416
|
+
// 给不出子代理证据, 必须有 agent 标记佐证才敢当父 sid 用。
|
|
1417
|
+
if (!agentId && !agentType)
|
|
1418
|
+
return undefined;
|
|
1268
1419
|
const main = norm.match(/\/([0-9a-fA-F-]{36})\.jsonl$/);
|
|
1269
1420
|
if (main)
|
|
1270
1421
|
return main[1];
|
|
1271
1422
|
return undefined;
|
|
1272
1423
|
};
|
|
1424
|
+
// ── DeferExecuteTool 下钻 ──────────────────────────────────────────────
|
|
1425
|
+
// CodeBuddy 的延迟加载工具 (ToolSearch → DeferExecuteTool) 把真实工具包进
|
|
1426
|
+
// tool_input.toolName / .toolInput。审批链 (matcher/danger/allow/deny/卡片/缓存)
|
|
1427
|
+
// 一律按内层工具裁决 —— 外层名匹配不到任何用户规则, 卡上也读不出语义。hook 侧
|
|
1428
|
+
// (pre-tool-use.sh) 已做同样下钻; 这里再剥一层是给旧版 hook / 直连调用兜底。
|
|
1429
|
+
const unwrapDeferredTool = (toolName, toolInput) => {
|
|
1430
|
+
if (toolName !== "DeferExecuteTool" || typeof toolInput !== "object" || toolInput === null)
|
|
1431
|
+
return { toolName, toolInput };
|
|
1432
|
+
const o = toolInput;
|
|
1433
|
+
const inner = [o.toolName, o.tool_name, o.name].find((v) => typeof v === "string" && v.length > 0);
|
|
1434
|
+
if (!inner)
|
|
1435
|
+
return { toolName, toolInput };
|
|
1436
|
+
// 内层入参字段名各版本有出入, 依次探测; 都没有则剥掉包装字段取余下对象。
|
|
1437
|
+
const args = [o.toolInput, o.tool_input, o.arguments, o.args, o.input].find((v) => v !== undefined);
|
|
1438
|
+
if (args !== undefined)
|
|
1439
|
+
return { toolName: inner, toolInput: args };
|
|
1440
|
+
const { toolName: _tn, tool_name: _tn2, name: _n, ...rest } = o;
|
|
1441
|
+
return { toolName: inner, toolInput: rest };
|
|
1442
|
+
};
|
|
1273
1443
|
export const makeApproveHandler = ({ cfg, log, client, sourcePath, getMirrorTarget, flushBeforeCard, nativeModal }) => {
|
|
1274
1444
|
setCardQuoteMax(cfg.approval.cardQuoteMaxChars);
|
|
1275
1445
|
const detailUrlFor = (id, approver) => buildDetailUrl(cfg.daemon.detailPublicBase, cfg.daemon.host, cfg.daemon.port, id, approver ? targetChatId(approver) : undefined);
|
|
@@ -1448,8 +1618,7 @@ export const makeApproveHandler = ({ cfg, log, client, sourcePath, getMirrorTarg
|
|
|
1448
1618
|
}
|
|
1449
1619
|
const body = (await readBody(req));
|
|
1450
1620
|
let sessionId = body.session_id ?? "";
|
|
1451
|
-
const toolName = body.tool_name ?? "";
|
|
1452
|
-
const toolInput = body.tool_input ?? {};
|
|
1621
|
+
const { toolName, toolInput } = unwrapDeferredTool(body.tool_name ?? "", body.tool_input ?? {});
|
|
1453
1622
|
const cwd = body.cwd ?? "";
|
|
1454
1623
|
const transcriptTail = body.transcript_tail ?? "";
|
|
1455
1624
|
if (!toolName) {
|
|
@@ -1462,7 +1631,10 @@ export const makeApproveHandler = ({ cfg, log, client, sourcePath, getMirrorTarg
|
|
|
1462
1631
|
// 主会话 / 已解析到归属的子代理请求 (CC/CodeBuddy 都上报父 id) 完全不受影响。
|
|
1463
1632
|
// 不 gate 在 mirror: headless 下 getMirrorTarget 同样是「session → 归属 chat」的解析
|
|
1464
1633
|
// (由 index.ts 注入 sessions store 反查), 父归属逻辑对两种模式统一生效。
|
|
1465
|
-
|
|
1634
|
+
// agent 标记之外, subagents/ 转录布局同样是子代理证据 (部分 CodeBuddy 版本
|
|
1635
|
+
// 两个标记都不带) —— 否则这类请求按主会话处理, no-approver 时挂进无人能点的 ask。
|
|
1636
|
+
const fromSubagent = Boolean(body.agent_id || body.agent_type)
|
|
1637
|
+
|| SUBAGENT_TRANSCRIPT_RE.test((body.transcript_path ?? "").replace(/\\/g, "/"));
|
|
1466
1638
|
if (fromSubagent && sessionId && !getMirrorTarget?.(sessionId)) {
|
|
1467
1639
|
const parent = subagentParentOf(body.transcript_path ?? "", body.agent_id ?? "", body.agent_type ?? "");
|
|
1468
1640
|
if (parent && parent !== sessionId) {
|
|
@@ -1620,6 +1792,15 @@ export const makeApproveHandler = ({ cfg, log, client, sourcePath, getMirrorTarg
|
|
|
1620
1792
|
settleGuard();
|
|
1621
1793
|
return;
|
|
1622
1794
|
}
|
|
1795
|
+
// picker 代按后的迟到 hook 上报 (CodeBuddy「本地面板先行」): 刚在审批卡上点过
|
|
1796
|
+
// ✅ 并被代按 Yes 的调用, 不再发第二张卡。一次性消费, 压过 mustCard —— 危险卡
|
|
1797
|
+
// 那次也是人刚点过的。
|
|
1798
|
+
if (consumePickerAllow(sessionId, toolName)) {
|
|
1799
|
+
log.info({ toolName, sessionId }, "picker-card allow replay — auto allow");
|
|
1800
|
+
json(res, 200, { decision: "allow", reason: "picker_card_allow" });
|
|
1801
|
+
settleGuard();
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1623
1804
|
// danger.skip / danger 模式的早退。第三个参数是「除 danger 外还有没有别的必发卡
|
|
1624
1805
|
// 理由」—— askRules 不能被 danger 的开关顺带关掉。守卫不在其列: 它要的是事后按框,
|
|
1625
1806
|
// 早退照样能给 (settleGuard), 拿它挡早退等于把 danger 的开关废掉。
|