wezard 2.5.1 → 2.6.0
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/.claude-plugin/plugin.json +1 -1
- package/CLAUDE.md +3 -0
- package/README.md +43 -1
- package/cli/wezard.sh +5 -0
- package/config.example.jsonc +22 -5
- package/dist/cli/codex-hook.js +91 -0
- package/dist/cli/codex-hook.js.map +1 -0
- package/dist/cli/codex-sync.js +64 -0
- package/dist/cli/codex-sync.js.map +1 -0
- package/dist/cli/init.js +7 -3
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/sync.js +16 -5
- package/dist/cli/sync.js.map +1 -1
- package/dist/daemon/aibot-media.js +6 -2
- package/dist/daemon/aibot-media.js.map +1 -1
- package/dist/daemon/approval.js +44 -21
- package/dist/daemon/approval.js.map +1 -1
- package/dist/daemon/bot-mux.js +157 -0
- package/dist/daemon/bot-mux.js.map +1 -0
- package/dist/daemon/cc-session.js +8 -1
- package/dist/daemon/cc-session.js.map +1 -1
- package/dist/daemon/cfg-sync.js +1 -0
- package/dist/daemon/cfg-sync.js.map +1 -1
- package/dist/daemon/claim.js +45 -1
- package/dist/daemon/claim.js.map +1 -1
- package/dist/daemon/config-api.js +59 -23
- package/dist/daemon/config-api.js.map +1 -1
- package/dist/daemon/detail.js +4 -0
- package/dist/daemon/detail.js.map +1 -1
- package/dist/daemon/http.js +9 -0
- package/dist/daemon/http.js.map +1 -1
- package/dist/daemon/inbound.js +54 -28
- package/dist/daemon/inbound.js.map +1 -1
- package/dist/daemon/index.js +181 -139
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/jobs.js +1 -1
- package/dist/daemon/jobs.js.map +1 -1
- package/dist/daemon/last-response.js +12 -6
- package/dist/daemon/last-response.js.map +1 -1
- package/dist/daemon/mirror-bridge.js +351 -144
- package/dist/daemon/mirror-bridge.js.map +1 -1
- package/dist/daemon/peers.js +12 -16
- package/dist/daemon/peers.js.map +1 -1
- package/dist/daemon/picker-policy.js +46 -0
- package/dist/daemon/picker-policy.js.map +1 -0
- package/dist/daemon/quota.js +10 -7
- package/dist/daemon/quota.js.map +1 -1
- package/dist/daemon/reach.js +5 -1
- package/dist/daemon/reach.js.map +1 -1
- package/dist/daemon/receipts.js +1 -1
- package/dist/daemon/receipts.js.map +1 -1
- package/dist/daemon/route.js +67 -26
- package/dist/daemon/route.js.map +1 -1
- package/dist/daemon/session-scan.js +31 -7
- package/dist/daemon/session-scan.js.map +1 -1
- package/dist/daemon/spawn-tmux.js +167 -73
- package/dist/daemon/spawn-tmux.js.map +1 -1
- package/dist/daemon/steward-model.js +24 -0
- package/dist/daemon/steward-model.js.map +1 -0
- package/dist/daemon/subagent-tail.js +5 -0
- package/dist/daemon/subagent-tail.js.map +1 -1
- package/dist/daemon/task-registry.js +6 -1
- package/dist/daemon/task-registry.js.map +1 -1
- package/dist/daemon/tasks.js +1 -1
- package/dist/daemon/tasks.js.map +1 -1
- package/dist/daemon/tips.js +1 -1
- package/dist/daemon/tips.js.map +1 -1
- package/dist/daemon/wizard-memory.js +12 -6
- package/dist/daemon/wizard-memory.js.map +1 -1
- package/dist/daemon/wizard.js +7 -5
- package/dist/daemon/wizard.js.map +1 -1
- package/dist/daemon/ws.js +79 -10
- package/dist/daemon/ws.js.map +1 -1
- package/dist/mcp/server.js +19 -18
- package/dist/mcp/server.js.map +1 -1
- package/dist/shared/chat-http.js +63 -29
- package/dist/shared/chat-http.js.map +1 -1
- package/dist/shared/chat-view.js +10 -3
- package/dist/shared/chat-view.js.map +1 -1
- package/dist/shared/cli-backends.js +11 -0
- package/dist/shared/cli-backends.js.map +1 -1
- package/dist/shared/codex-approval.js +57 -0
- package/dist/shared/codex-approval.js.map +1 -0
- package/dist/shared/codex-hooks.js +134 -0
- package/dist/shared/codex-hooks.js.map +1 -0
- package/dist/shared/codex.js +125 -0
- package/dist/shared/codex.js.map +1 -0
- package/dist/shared/config.js +85 -21
- package/dist/shared/config.js.map +1 -1
- package/dist/shared/detail-render.js +24 -16
- package/dist/shared/detail-render.js.map +1 -1
- package/dist/shared/detail-store.js +11 -10
- package/dist/shared/detail-store.js.map +1 -1
- package/dist/shared/modal-pane.js +19 -1
- package/dist/shared/modal-pane.js.map +1 -1
- package/dist/shared/model-tier-migration.js +43 -0
- package/dist/shared/model-tier-migration.js.map +1 -0
- package/dist/shared/model-tiers.js +35 -0
- package/dist/shared/model-tiers.js.map +1 -0
- package/dist/shared/reminder.js +19 -4
- package/dist/shared/reminder.js.map +1 -1
- package/dist/shared/session-label.js +27 -2
- package/dist/shared/session-label.js.map +1 -1
- package/dist/shared/tool-hints.js +144 -0
- package/dist/shared/tool-hints.js.map +1 -0
- package/dist/shared/workspace-trust.js +69 -0
- package/dist/shared/workspace-trust.js.map +1 -0
- package/dist/shared/world.js +5 -4
- package/dist/shared/world.js.map +1 -1
- package/dist/svr/index.js +3 -0
- package/dist/svr/index.js.map +1 -1
- package/hooks/pre-tool-use.sh +1 -1
- package/package.json +6 -1
package/dist/daemon/approval.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { closeSync, fstatSync, openSync, readFileSync, readSync } from "node:fs";
|
|
2
|
+
import { defaultChatOfBot } from "../shared/config.js";
|
|
3
|
+
import { botTagOf } from "./bot-mux.js";
|
|
2
4
|
import { createPending, getPending, getResolvedSnapshot, resolvePending, resolvePendingsByChat, failPending, stashResolved, markCardSent, isReloadError } from "./pending.js";
|
|
3
5
|
import { cacheGet, cachePut, cacheKey, isAutoWindowActive, autoWindowRemainingMs, setAutoWindow, clearAutoWindow, getWindowMeta, } from "./session-cache.js";
|
|
4
6
|
import { evaluateAllow, ruleMatchesAny, alwaysAllowRulesFor, splitSegments, NEVER_RULE_ALLOW } from "../shared/allow-rules.js";
|
|
@@ -6,9 +8,9 @@ import { redact } from "./redact.js";
|
|
|
6
8
|
import { dangerOf, dangerEarlyExit, selfConfigWriteOf } from "./danger.js";
|
|
7
9
|
import { appendUnique } from "../shared/config-writer.js";
|
|
8
10
|
import { claudeConfigWrite } from "../shared/claude-config-path.js";
|
|
9
|
-
import { recordApproval, recordApprovalDecision, buildDetailUrl, getDetail, openTurnsOf } from "./detail.js";
|
|
11
|
+
import { recordApproval, recordApprovalDecision, buildDetailUrl, getDetail, openTurnsOf, targetOfSession } from "./detail.js";
|
|
10
12
|
import { json, readBody } from "./http.js";
|
|
11
|
-
import { tagBadge, tagOfKey, withTagHeader } from "../shared/session-label.js";
|
|
13
|
+
import { botOfKey, tagBadge, tagOfKey, withTagHeader, withBot } from "../shared/session-label.js";
|
|
12
14
|
import { chatPolicyOf } from "./chat-name.js";
|
|
13
15
|
import { makeStewardGuard } from "./steward-guard.js";
|
|
14
16
|
import { sessionNameFor } from "./session-name.js";
|
|
@@ -32,13 +34,18 @@ const targetChatId = (key) => {
|
|
|
32
34
|
};
|
|
33
35
|
/** 卡片真正要去的那个聊天 (按频道换算之后) 此刻送得到吗 —— 微信 chat 不看企微长连接。 */
|
|
34
36
|
const canReach = (client, key) => reachable(client, targetChatId(key));
|
|
37
|
+
// 绑了镜像的会话, 卡片发到它的聊天 —— 机器人随会话 key 走。没绑镜像的不属于任何聊天、任何机器人:
|
|
38
|
+
// 先全局 (主机器人), 全局都空才按配置顺序借副机器人自己的审批人 / 默认聊天 (挂上它的 `~<bot>`,
|
|
39
|
+
// 卡片才从那个机器人推出去)。
|
|
35
40
|
const pickApprover = (cfg) => {
|
|
36
41
|
if (cfg.approval.approvers.length > 0)
|
|
37
42
|
return cfg.approval.approvers[0];
|
|
38
43
|
if (cfg.defaultChat)
|
|
39
44
|
return cfg.defaultChat;
|
|
40
|
-
return
|
|
45
|
+
return cfg.bots.flatMap((b) => [...b.approvers, b.defaultChat].filter(Boolean).map((p) => withBot(p, b.id)))[0];
|
|
41
46
|
};
|
|
47
|
+
/** 副机器人 `bot` 自己的审批人 / 默认聊天 (挂 `~bot`); 都没配 = undefined —— 不回落主机器人, 那是冒用它的连接。 */
|
|
48
|
+
const botApprover = (cfg, bot) => ((b) => (b ? [...b.approvers, b.defaultChat].filter(Boolean).map((p) => withBot(p, bot))[0] : undefined))(cfg.bots.find((x) => x.id === bot));
|
|
42
49
|
const TRUNC = (s, n) => (s.length > n ? `${s.slice(0, n - 1)}…` : s);
|
|
43
50
|
const HOME_RE = /^\/Users\/[^/]+/;
|
|
44
51
|
const fmtPath = (p) => p.replace(HOME_RE, "~");
|
|
@@ -390,7 +397,7 @@ const evictBatches = () => {
|
|
|
390
397
|
}
|
|
391
398
|
};
|
|
392
399
|
const newBatchId = () => `b${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
393
|
-
const batchKeyOf = (sessionId, toolName) =>
|
|
400
|
+
const batchKeyOf = (sessionId, toolName, chatId) => JSON.stringify([sessionId, toolName, chatId]);
|
|
394
401
|
// 单条成员渲染: 展开第 N 项的输入摘要, 单行化以适配批量列表的紧凑布局。
|
|
395
402
|
const PER_MEMBER_MAX = 120;
|
|
396
403
|
const BATCH_MAX_VISIBLE = 8;
|
|
@@ -1450,7 +1457,12 @@ const fallback = (cfg, reason, forceSingle) => ({
|
|
|
1450
1457
|
});
|
|
1451
1458
|
const resolveApprover = (cfg, sessionId, getMirrorTarget) => {
|
|
1452
1459
|
const mirror = sessionId ? getMirrorTarget?.(sessionId) : undefined;
|
|
1453
|
-
|
|
1460
|
+
if (mirror)
|
|
1461
|
+
return mirror;
|
|
1462
|
+
// 镜像对不上时, 这个会话跑过的轮次还记得它在谁名下: 归副机器人就按那个机器人兑。认不出 / 归主机器人 =
|
|
1463
|
+
// 原样 (pickApprover, 主机器人优先)。单机器人时 bot 恒为 "", 不变。
|
|
1464
|
+
const bot = sessionId ? botOfKey(targetOfSession(sessionId) ?? "") : "";
|
|
1465
|
+
return bot ? botApprover(cfg, bot) : pickApprover(cfg);
|
|
1454
1466
|
};
|
|
1455
1467
|
// ── Subagent 归属解析 ──────────────────────────────────────────────────
|
|
1456
1468
|
// CC / CodeBuddy 正常把子代理工具调用的 hook session_id 上报成**父会话** id
|
|
@@ -1518,7 +1530,7 @@ export const makeApproveHandler = ({ cfg, log, client, sourcePath, getMirrorTarg
|
|
|
1518
1530
|
return;
|
|
1519
1531
|
const text = cmd.length > cap ? `${cmd.slice(0, cap)}\n…(已截断,完整命令共 ${cmd.length} 字)` : cmd;
|
|
1520
1532
|
const chunks = chunkText(text, FULLCMD_CHUNK_CHARS);
|
|
1521
|
-
const target =
|
|
1533
|
+
const target = batch.chatId;
|
|
1522
1534
|
for (let i = 0; i < chunks.length; i++) {
|
|
1523
1535
|
const head = i === 0
|
|
1524
1536
|
? `🔐 待审批完整命令(${cmd.length} 字${chunks.length > 1 ? `,${i + 1}/${chunks.length}` : ""}):\n`
|
|
@@ -1600,7 +1612,7 @@ export const makeApproveHandler = ({ cfg, log, client, sourcePath, getMirrorTarg
|
|
|
1600
1612
|
if (batch.flushed)
|
|
1601
1613
|
return;
|
|
1602
1614
|
batch.flushed = true;
|
|
1603
|
-
activeBatches.delete(batchKeyOf(batch.sessionId, batch.toolName));
|
|
1615
|
+
activeBatches.delete(batchKeyOf(batch.sessionId, batch.toolName, batch.chatId));
|
|
1604
1616
|
const isMulti = batch.members.length > 1;
|
|
1605
1617
|
const card = isMulti
|
|
1606
1618
|
? buildBatchCard(batch, batch.members[0]?.transcriptTail ?? "")
|
|
@@ -1644,13 +1656,13 @@ export const makeApproveHandler = ({ cfg, log, client, sourcePath, getMirrorTarg
|
|
|
1644
1656
|
catch (e) {
|
|
1645
1657
|
log.warn({ batchId: batch.batchId, err: e.message }, "command prelude send failed");
|
|
1646
1658
|
}
|
|
1647
|
-
const sendCard = (c) => client.sendMessage(
|
|
1659
|
+
const sendCard = (c) => client.sendMessage(batch.chatId, { msgtype: "template_card", template_card: c });
|
|
1648
1660
|
try {
|
|
1649
1661
|
await sendCard(card);
|
|
1650
1662
|
}
|
|
1651
1663
|
catch (e) {
|
|
1652
1664
|
// quote 体可能超平台未公开的长度上限 — 缩到安全值重试一次, 保住审批流。
|
|
1653
|
-
log.warn({ batchId: batch.batchId, err: e.message }, "card send failed — retrying with shrunk quote");
|
|
1665
|
+
log.warn({ batchId: batch.batchId, approver: batch.approver, chatId: batch.chatId, err: e.message ?? e.errmsg }, "card send failed — retrying with shrunk quote");
|
|
1654
1666
|
await sendCard(shrinkQuote(card));
|
|
1655
1667
|
}
|
|
1656
1668
|
log.info({ batchId: batch.batchId, count: batch.members.length, multi: isMulti, approver: batch.approver, tool: batch.toolName }, "batch flushed");
|
|
@@ -1661,8 +1673,9 @@ export const makeApproveHandler = ({ cfg, log, client, sourcePath, getMirrorTarg
|
|
|
1661
1673
|
markCardSent(batch.members[0].reqId);
|
|
1662
1674
|
}
|
|
1663
1675
|
catch (e) {
|
|
1664
|
-
|
|
1665
|
-
|
|
1676
|
+
const message = e.message ?? e.errmsg ?? String(e);
|
|
1677
|
+
log.error({ batchId: batch.batchId, approver: batch.approver, chatId: batch.chatId, err: message }, "batch send failed");
|
|
1678
|
+
const err = new Error(`send_card_fail:${message}`);
|
|
1666
1679
|
for (const m of batch.members)
|
|
1667
1680
|
failPending(m.reqId, err);
|
|
1668
1681
|
// 单成员 batch 摘除自己的 batchById 条目, 多成员保留 (后续 click 兜底
|
|
@@ -1677,6 +1690,7 @@ export const makeApproveHandler = ({ cfg, log, client, sourcePath, getMirrorTarg
|
|
|
1677
1690
|
return;
|
|
1678
1691
|
}
|
|
1679
1692
|
const body = (await readBody(req));
|
|
1693
|
+
const nativePermission = body.cli_backend === "codex" && body.hook_event_name === "PermissionRequest";
|
|
1680
1694
|
let sessionId = body.session_id ?? "";
|
|
1681
1695
|
const { toolName, toolInput } = unwrapDeferredTool(body.tool_name ?? "", body.tool_input ?? {});
|
|
1682
1696
|
const cwd = body.cwd ?? "";
|
|
@@ -1720,7 +1734,7 @@ export const makeApproveHandler = ({ cfg, log, client, sourcePath, getMirrorTarg
|
|
|
1720
1734
|
// wezard 自改配置的写 (见 selfConfigWriteOf): matcher 也放不掉它。
|
|
1721
1735
|
const selfWrite = selfConfigWriteOf(toolName, toolInput);
|
|
1722
1736
|
// Matcher: only intercept matching tools — others pass.
|
|
1723
|
-
if (!selfWrite && !new RegExp(cfg.approval.matcher).test(toolName)) {
|
|
1737
|
+
if (!nativePermission && !selfWrite && !new RegExp(cfg.approval.matcher).test(toolName)) {
|
|
1724
1738
|
json(res, 200, { decision: "allow", reason: "matcher_skip" });
|
|
1725
1739
|
return;
|
|
1726
1740
|
}
|
|
@@ -1744,12 +1758,13 @@ export const makeApproveHandler = ({ cfg, log, client, sourcePath, getMirrorTarg
|
|
|
1744
1758
|
// ⏱窗口 / 会话缓存), 批准后 settleClaudeConfigModal 去把框按掉。
|
|
1745
1759
|
// 没有活 pane (未镜像的本地会话) → 不介入: 那种情形用户就在键盘前,
|
|
1746
1760
|
// 自己按掉即可, 拦下来只是挡工作。
|
|
1747
|
-
const guardHit = cfg.approval.claudeConfigGuard ? claudeConfigWrite(toolName, toolInput) : undefined;
|
|
1761
|
+
const guardHit = cfg.approval.claudeConfigGuard && body.cli_backend !== "codex" ? claudeConfigWrite(toolName, toolInput) : undefined;
|
|
1748
1762
|
const guardActive = Boolean(guardHit && nativeModal?.hasPane(sessionId));
|
|
1749
1763
|
if (guardHit) {
|
|
1750
1764
|
log.info({ toolName, sessionId, path: guardHit.path, why: guardHit.why, guardActive }, guardActive ? "claude-config guard armed" : "claude-config write detected (no live pane, passing through)");
|
|
1751
1765
|
}
|
|
1752
1766
|
const approver = resolveApprover(cfg, sessionId, getMirrorTarget);
|
|
1767
|
+
const chatId = approver ? targetChatId(approver) : "";
|
|
1753
1768
|
// 守卫的收尾 = 把 CC 那个不过 hook 的原生确认框按掉。它挂在**每一条 allow 出口**
|
|
1754
1769
|
// 上, 而不是靠"强制发卡 + 人工点"来触发 —— 死锁的成因是没人按框, 不是没人点卡。
|
|
1755
1770
|
//
|
|
@@ -1774,7 +1789,7 @@ export const makeApproveHandler = ({ cfg, log, client, sourcePath, getMirrorTarg
|
|
|
1774
1789
|
log.info({ toolName, sessionId, rule: danger.rule }, "danger hit — forcing single approval");
|
|
1775
1790
|
// 必发卡 = 危险名单 或 askRules 命中。这两者的语义是「每次都要人单独看一眼」,
|
|
1776
1791
|
// 所以压过放行/窗口/缓存。守卫不在其列 —— 见上面 settleGuard 的注释。
|
|
1777
|
-
const mustCard = Boolean(danger) || Boolean(askHit);
|
|
1792
|
+
const mustCard = nativePermission || Boolean(danger) || Boolean(askHit);
|
|
1778
1793
|
// allowRules: matcher 拦下的工具里再挖细粒度豁免 (Bash 可按命令前缀区分)。
|
|
1779
1794
|
// 交互卡工具 (AskUserQuestion 等) 在引擎内部硬保护, 规则写了也不放行。
|
|
1780
1795
|
// 用 evaluateAllow 而非 ruleAllows: 未命中时要把"因为哪一段"带到卡上,
|
|
@@ -1853,7 +1868,7 @@ export const makeApproveHandler = ({ cfg, log, client, sourcePath, getMirrorTarg
|
|
|
1853
1868
|
// 否则请求挂在长轮询上等一张子代理场景下没人点的卡, 最终 ask 回退成远端看不见的
|
|
1854
1869
|
// 原生 picker。denyRules 已在上方生效 (拒绝不是审批), AskUserQuestion / ExitPlanMode
|
|
1855
1870
|
// 的交互卡也已在上方分支返回, 均不受影响。
|
|
1856
|
-
if (BYPASS_MODES.has(body.permission_mode ?? "")) {
|
|
1871
|
+
if (!nativePermission && BYPASS_MODES.has(body.permission_mode ?? "")) {
|
|
1857
1872
|
log.info({ toolName, sessionId, mode: body.permission_mode }, "permission-mode bypass auto allow");
|
|
1858
1873
|
json(res, 200, { decision: "allow", reason: `permission_mode:${body.permission_mode}` });
|
|
1859
1874
|
settleGuard();
|
|
@@ -1863,7 +1878,7 @@ export const makeApproveHandler = ({ cfg, log, client, sourcePath, getMirrorTarg
|
|
|
1863
1878
|
// 名单 / askRules / ⏱窗口 / 会话缓存 (与 danger.skip 的区别: 那个只豁免命中
|
|
1864
1879
|
// 名单的调用, 普通调用照审)。denyRules / EnterPlanMode 拦截在前面已生效
|
|
1865
1880
|
// (拒绝不是审批); AskUserQuestion / ExitPlanMode 交互卡在上方分支已返回, 不受影响。
|
|
1866
|
-
if (cfg.approval.danger.skipAll) {
|
|
1881
|
+
if (!nativePermission && cfg.approval.danger.skipAll) {
|
|
1867
1882
|
log.info({ toolName, sessionId }, "danger.skipAll auto allow");
|
|
1868
1883
|
json(res, 200, { decision: "allow", reason: "danger_skip_all" });
|
|
1869
1884
|
settleGuard();
|
|
@@ -1881,7 +1896,7 @@ export const makeApproveHandler = ({ cfg, log, client, sourcePath, getMirrorTarg
|
|
|
1881
1896
|
// danger.skip / danger 模式的早退。第三个参数是「除 danger 外还有没有别的必发卡
|
|
1882
1897
|
// 理由」—— askRules 不能被 danger 的开关顺带关掉。守卫不在其列: 它要的是事后按框,
|
|
1883
1898
|
// 早退照样能给 (settleGuard), 拿它挡早退等于把 danger 的开关废掉。
|
|
1884
|
-
const earlyExit = dangerEarlyExit(cfg, danger, Boolean(askHit) || Boolean(selfWrite));
|
|
1899
|
+
const earlyExit = dangerEarlyExit(cfg, danger, nativePermission || Boolean(askHit) || Boolean(selfWrite));
|
|
1885
1900
|
if (earlyExit) {
|
|
1886
1901
|
log.info({ toolName, sessionId, reason: earlyExit }, "danger switch early exit");
|
|
1887
1902
|
json(res, 200, { decision: "allow", reason: earlyExit });
|
|
@@ -1982,14 +1997,14 @@ export const makeApproveHandler = ({ cfg, log, client, sourcePath, getMirrorTarg
|
|
|
1982
1997
|
// 选择普通 buildCard 或 buildBatchCard。0 = 关闭聚合, 立即 flush。
|
|
1983
1998
|
// 续接的请求不再进 batch: 卡已经在群里, 重发就是重复轰炸。
|
|
1984
1999
|
const member = { reqId, toolInput: display, originalToolInput: toolInput, toolInputStr, cwd, transcriptTail };
|
|
1985
|
-
const bk = batchKeyOf(sessionId, toolName);
|
|
2000
|
+
const bk = batchKeyOf(sessionId, toolName, chatId);
|
|
1986
2001
|
// 必发卡请求既不 join 也不被 join — 一次这样的操作 = 一张卡 = 一次点击。
|
|
1987
2002
|
// 合流会把它塞进带「⏱全过 / ✅总是」的批量卡, 一次点击就连它一起放行了。
|
|
1988
2003
|
const existing = mustCard ? undefined : activeBatches.get(bk);
|
|
1989
2004
|
if (resumeId) {
|
|
1990
2005
|
// no-op: 直接进下面的长轮询, 等旧卡上的点击。
|
|
1991
2006
|
}
|
|
1992
|
-
else if (existing && !existing.flushed) {
|
|
2007
|
+
else if (existing && !existing.flushed && existing.chatId === chatId) {
|
|
1993
2008
|
existing.members.push(member);
|
|
1994
2009
|
log.info({ batchId: existing.batchId, count: existing.members.length, reqId }, "batch joined");
|
|
1995
2010
|
}
|
|
@@ -1999,6 +2014,7 @@ export const makeApproveHandler = ({ cfg, log, client, sourcePath, getMirrorTarg
|
|
|
1999
2014
|
sessionId,
|
|
2000
2015
|
toolName,
|
|
2001
2016
|
approver,
|
|
2017
|
+
chatId,
|
|
2002
2018
|
windowMinutes: cfg.approval.windowMinutes,
|
|
2003
2019
|
danger: danger?.rule,
|
|
2004
2020
|
forceSingle: mustCard,
|
|
@@ -2031,7 +2047,8 @@ export const makeApproveHandler = ({ cfg, log, client, sourcePath, getMirrorTarg
|
|
|
2031
2047
|
return;
|
|
2032
2048
|
}
|
|
2033
2049
|
log.warn({ err: e.message, reqId }, "approval timed out");
|
|
2034
|
-
|
|
2050
|
+
const failure = e.message?.startsWith("send_card_fail:") ? e.message : "approver_timeout";
|
|
2051
|
+
json(res, 200, fallback(cfg, failure, mustCard || guardActive));
|
|
2035
2052
|
return;
|
|
2036
2053
|
}
|
|
2037
2054
|
// 必发卡的决策一律不留痕: 不写 session cache、不开自动窗口 (卡上本就没这两个
|
|
@@ -2159,9 +2176,15 @@ export const installApprovalEventListener = (client, log, cfg, onApproved) => {
|
|
|
2159
2176
|
// 回复目标: 待决时在 pending 里, 已决时 pending 已删 —— 回落到 resolved
|
|
2160
2177
|
// 暂存。两个都没有才用 defaultChat (`#tag` 会话会因此回到主会话, 不理想,
|
|
2161
2178
|
// 但那已经是记录过期的边缘情形)。
|
|
2179
|
+
// 最后一档按点击帧是哪个机器人收的兑到它自己的默认聊天 (副机器人没配 = 不回), 不回落主机器人的
|
|
2180
|
+
// 全局 defaultChat; 单机器人时 botTagOf 恒 "", 就是原来的 cfg.defaultChat。
|
|
2162
2181
|
const target = getPending(reqId)?.chatKey
|
|
2163
2182
|
?? getResolvedSnapshot(reqId)?.meta.chatKey
|
|
2164
|
-
?? cfg
|
|
2183
|
+
?? defaultChatOfBot(cfg, botTagOf(frame));
|
|
2184
|
+
if (!target) {
|
|
2185
|
+
log.info({ reqId, bot: botTagOf(frame) }, "full command: 记录已过期且该机器人没有默认聊天, 不回");
|
|
2186
|
+
return;
|
|
2187
|
+
}
|
|
2165
2188
|
const cmd = rec && rec.kind === "approval" ? commandOf(rec.toolName, rec.toolInput) : "";
|
|
2166
2189
|
// 只能走主动 markdown 分块 (≤1800 字单条)。曾试过对点击事件做 stream 被动
|
|
2167
2190
|
// 回复 (单条 20480 字节) —— 企微服务端拒绝: errcode=846605 invalid req_id,
|