wezard 2.4.0 → 2.5.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.md +1 -1
- package/README.md +113 -57
- package/cli/wezard.sh +2 -0
- package/config.example.jsonc +7 -4
- package/dist/cli/wx.js +78 -0
- package/dist/cli/wx.js.map +1 -0
- package/dist/daemon/aibot-media.js +62 -0
- package/dist/daemon/aibot-media.js.map +1 -0
- package/dist/daemon/approval.js +30 -20
- package/dist/daemon/approval.js.map +1 -1
- package/dist/daemon/ask.js +3 -2
- package/dist/daemon/ask.js.map +1 -1
- package/dist/daemon/cc-session.js +27 -4
- package/dist/daemon/cc-session.js.map +1 -1
- package/dist/daemon/chat-log.js +2 -2
- package/dist/daemon/chat-log.js.map +1 -1
- package/dist/daemon/chat-name.js +6 -2
- package/dist/daemon/chat-name.js.map +1 -1
- package/dist/daemon/config-api.js +25 -10
- package/dist/daemon/config-api.js.map +1 -1
- package/dist/daemon/direction.js +53 -0
- package/dist/daemon/direction.js.map +1 -0
- package/dist/daemon/ilink.js +162 -0
- package/dist/daemon/ilink.js.map +1 -0
- package/dist/daemon/inbound.js +64 -18
- package/dist/daemon/inbound.js.map +1 -1
- package/dist/daemon/index.js +415 -109
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/jobs.js +4 -3
- package/dist/daemon/jobs.js.map +1 -1
- package/dist/daemon/last-response.js +6 -3
- package/dist/daemon/last-response.js.map +1 -1
- package/dist/daemon/media.js +108 -0
- package/dist/daemon/media.js.map +1 -0
- package/dist/daemon/mirror-bridge.js +111 -93
- package/dist/daemon/mirror-bridge.js.map +1 -1
- package/dist/daemon/model-select.js +159 -54
- package/dist/daemon/model-select.js.map +1 -1
- package/dist/daemon/notices.js +59 -12
- package/dist/daemon/notices.js.map +1 -1
- package/dist/daemon/outbound.js +2 -1
- package/dist/daemon/outbound.js.map +1 -1
- package/dist/daemon/peers.js +46 -27
- package/dist/daemon/peers.js.map +1 -1
- package/dist/daemon/pending-items.js +1 -1
- package/dist/daemon/pending-items.js.map +1 -1
- package/dist/daemon/reach.js +6 -0
- package/dist/daemon/reach.js.map +1 -0
- package/dist/daemon/receipts.js +149 -7
- package/dist/daemon/receipts.js.map +1 -1
- package/dist/daemon/route.js +11 -4
- package/dist/daemon/route.js.map +1 -1
- package/dist/daemon/spawn-tmux.js +157 -60
- package/dist/daemon/spawn-tmux.js.map +1 -1
- package/dist/daemon/steward-guard.js +48 -7
- package/dist/daemon/steward-guard.js.map +1 -1
- package/dist/daemon/top-only-nudge.js +29 -0
- package/dist/daemon/top-only-nudge.js.map +1 -0
- package/dist/daemon/weixin-cmd.js +268 -0
- package/dist/daemon/weixin-cmd.js.map +1 -0
- package/dist/daemon/weixin-port.js +226 -0
- package/dist/daemon/weixin-port.js.map +1 -0
- package/dist/daemon/weixin.js +772 -0
- package/dist/daemon/weixin.js.map +1 -0
- package/dist/daemon/wizard.js +56 -18
- package/dist/daemon/wizard.js.map +1 -1
- package/dist/mcp/server.js +43 -23
- package/dist/mcp/server.js.map +1 -1
- package/dist/shared/chat-http.js +61 -20
- package/dist/shared/chat-http.js.map +1 -1
- package/dist/shared/chat-render.js +9 -1
- package/dist/shared/chat-render.js.map +1 -1
- package/dist/shared/chat-view.js +16 -2
- package/dist/shared/chat-view.js.map +1 -1
- package/dist/shared/cli-backends.js +3 -0
- package/dist/shared/cli-backends.js.map +1 -1
- package/dist/shared/config.js +45 -10
- package/dist/shared/config.js.map +1 -1
- package/dist/shared/json-map-store.js +1 -0
- package/dist/shared/json-map-store.js.map +1 -1
- package/dist/shared/reminder.js +1 -0
- package/dist/shared/reminder.js.map +1 -1
- package/dist/shared/role-render.js +7 -4
- package/dist/shared/role-render.js.map +1 -1
- package/dist/shared/role-view.js +116 -37
- package/dist/shared/role-view.js.map +1 -1
- package/dist/shared/seen-log.js +26 -0
- package/dist/shared/seen-log.js.map +1 -0
- package/dist/shared/session-label.js +84 -31
- package/dist/shared/session-label.js.map +1 -1
- package/dist/shared/turn-state.js +1 -1
- package/dist/shared/turn-state.js.map +1 -1
- package/dist/shared/world.js.map +1 -1
- package/dist/shared/wx-text.js +101 -0
- package/dist/shared/wx-text.js.map +1 -0
- package/dist/svr/index.js +5 -2
- package/dist/svr/index.js.map +1 -1
- package/hooks/pre-tool-use.sh +9 -6
- package/package.json +6 -2
- package/web/chat.css +32 -5
- package/web/chat.js +160 -92
package/dist/daemon/approval.js
CHANGED
|
@@ -12,6 +12,7 @@ import { tagBadge, tagOfKey, withTagHeader } from "../shared/session-label.js";
|
|
|
12
12
|
import { chatPolicyOf } from "./chat-name.js";
|
|
13
13
|
import { makeStewardGuard } from "./steward-guard.js";
|
|
14
14
|
import { sessionNameFor } from "./session-name.js";
|
|
15
|
+
import { reachable } from "./reach.js";
|
|
15
16
|
// ── Routing helpers ────────────────────────────────────────────────────
|
|
16
17
|
// 卡片跟着本轮的频道走: 人在群 B 里 `.fix` 叫住在 A 的 wizard, 这一轮的审批/提问卡
|
|
17
18
|
// 要出现在 B —— 等着点的人在那儿。`principal` (chatKey) 仍是 wizard 自己的 key,
|
|
@@ -29,6 +30,8 @@ const targetChatId = (key) => {
|
|
|
29
30
|
const h = rest.indexOf("#");
|
|
30
31
|
return h >= 0 ? rest.slice(0, h) : rest;
|
|
31
32
|
};
|
|
33
|
+
/** 卡片真正要去的那个聊天 (按频道换算之后) 此刻送得到吗 —— 微信 chat 不看企微长连接。 */
|
|
34
|
+
const canReach = (client, key) => reachable(client, targetChatId(key));
|
|
32
35
|
const pickApprover = (cfg) => {
|
|
33
36
|
if (cfg.approval.approvers.length > 0)
|
|
34
37
|
return cfg.approval.approvers[0];
|
|
@@ -496,10 +499,12 @@ const decodeKey = (key) => {
|
|
|
496
499
|
return { reqId, decision: d };
|
|
497
500
|
};
|
|
498
501
|
// ── AskUserQuestion 投票卡分支 ─────────────────────────────────────────
|
|
499
|
-
//
|
|
500
|
-
//
|
|
501
|
-
//
|
|
502
|
-
//
|
|
502
|
+
// 用户在 WeCom 选了选项 → allow + updatedInput 补上 `answers` (AskUserQuestion 自己的
|
|
503
|
+
// 入参: {题目文本: 所选 label}): Claude Code 对 AskUserQuestion / ExitPlanMode 认 hook
|
|
504
|
+
// 给的 updatedInput 为「已作答」, 跳过本地面板, 工具照常落地出 tool_result。曾经走
|
|
505
|
+
// deny + 把答案塞进 reason, CLI 里显示成红色 hook error。
|
|
506
|
+
// 选「🖥️ CLI 处理」哨兵选项 → 返回 ask, CLI 弹原生 picker 由用户本地作答; 选「聊聊」
|
|
507
|
+
// → deny (本来就是「别答, 先讨论」)。codebuddy 的镜像流仍是 deny + reason (见下)。
|
|
503
508
|
// vote_interaction 不支持 button_list (SDK 类型注释明写「button_interaction 类型卡片使用」),
|
|
504
509
|
// 微信侧静默吞掉, 所以 cli 入口只能塞进 checkbox option_list 作为哨兵 id。
|
|
505
510
|
const ASKQ_PREFIX = "ASKQ|";
|
|
@@ -651,7 +656,7 @@ const buildConfirmCard = (reqId, c, done) => ({
|
|
|
651
656
|
/** 往 `principal` 的当前频道推一张确认卡; 卡发出后返回, `decided` 是人的决定 (超时 = false)。
|
|
652
657
|
* 包一层对象是因为 promise 会被 await 摊平。发不出去就抛 —— 没人看得见的卡不该被当成「人拒绝了」。 */
|
|
653
658
|
export const requestConfirm = async ({ client, principal, who, title, body, timeoutMs }) => {
|
|
654
|
-
if (!client
|
|
659
|
+
if (!canReach(client, principal))
|
|
655
660
|
throw new Error("ws_disconnected");
|
|
656
661
|
const { reqId, promise } = createPending({ meta: { kind: "generic", createdAt: Date.now(), chatKey: principal }, timeoutMs });
|
|
657
662
|
const c = { who, title, body };
|
|
@@ -775,7 +780,7 @@ const handleExitPlanMode = async ({ cfg, log, client, body, getMirrorTarget, flu
|
|
|
775
780
|
const approver = resolveApprover(cfg, body.session_id, getMirrorTarget);
|
|
776
781
|
if (!approver)
|
|
777
782
|
return { decision: "ask", reason: "no_approver" };
|
|
778
|
-
if (!client
|
|
783
|
+
if (!canReach(client, approver))
|
|
779
784
|
return { decision: "ask", reason: "ws_disconnected" };
|
|
780
785
|
const longPollMs = cfg.approval.longPollSec * 1000;
|
|
781
786
|
const { reqId, promise } = createPending({
|
|
@@ -848,6 +853,9 @@ const handleExitPlanMode = async ({ cfg, log, client, body, getMirrorTarget, flu
|
|
|
848
853
|
}
|
|
849
854
|
return { decision: "ask", reason: "plan_unknown" };
|
|
850
855
|
};
|
|
856
|
+
// Claude Code 多选答案的编码 (逆向自其 AskUserQuestion 的结果校验): ", " 连接,
|
|
857
|
+
// 含 ", " 或 `"` 的 label 用 JSON 字符串包起来, 否则它拆不回原 label。
|
|
858
|
+
const encodeAskqLabels = (labels) => labels.map((l) => (l.includes(", ") || l.includes('"') ? JSON.stringify(l) : l)).join(", ");
|
|
851
859
|
// raw 是 click 事件 listener 塞回 pending 的字符串编码 (cli / chat / picked:i,j)。
|
|
852
860
|
export const interpretAskqRaw = (raw, q) => {
|
|
853
861
|
if (raw === "cli")
|
|
@@ -861,7 +869,7 @@ export const interpretAskqRaw = (raw, q) => {
|
|
|
861
869
|
.filter((n) => Number.isInteger(n) && n >= 0 && n < q.options.length);
|
|
862
870
|
if (idxs.length === 0)
|
|
863
871
|
return { kind: "empty" };
|
|
864
|
-
return { kind: "picked", labels: idxs.map((i) => q.options[i].label)
|
|
872
|
+
return { kind: "picked", labels: encodeAskqLabels(idxs.map((i) => q.options[i].label)) };
|
|
865
873
|
}
|
|
866
874
|
return { kind: "empty" };
|
|
867
875
|
};
|
|
@@ -920,7 +928,7 @@ export const runMirrorAskqFlow = async ({ cfg, log, client, sessionId, chatKey,
|
|
|
920
928
|
return;
|
|
921
929
|
if (questions.some((q) => q.options.length === 0))
|
|
922
930
|
return;
|
|
923
|
-
if (!client
|
|
931
|
+
if (!canReach(client, chatKey))
|
|
924
932
|
return;
|
|
925
933
|
const target = targetChatId(chatKey);
|
|
926
934
|
const total = questions.length;
|
|
@@ -1069,7 +1077,7 @@ export const mootMirrorPlan = (sessionId) => {
|
|
|
1069
1077
|
mirrorPlanSlots.delete(sessionId);
|
|
1070
1078
|
};
|
|
1071
1079
|
export const runMirrorPlanFlow = async ({ cfg, log, client, sessionId, chatKey, cwd, jsonlPath, voteTimeoutMs, sendKey }) => {
|
|
1072
|
-
if (!client
|
|
1080
|
+
if (!canReach(client, chatKey))
|
|
1073
1081
|
return;
|
|
1074
1082
|
const target = targetChatId(chatKey);
|
|
1075
1083
|
const note = async (content) => {
|
|
@@ -1186,7 +1194,7 @@ const consumePickerAllow = (sessionId, toolName) => {
|
|
|
1186
1194
|
};
|
|
1187
1195
|
/** 返回「卡是否真的发出去了」— false (WS 断 / 发送失败) 时调用方应允许对同屏重试。 */
|
|
1188
1196
|
export const runMirrorPickerFlow = async ({ log, client, cfg, sessionId, chatKey, cwd, jsonlPath, toolName, toolInput, agent, danger, voteTimeoutMs, press }) => {
|
|
1189
|
-
if (!client
|
|
1197
|
+
if (!canReach(client, chatKey))
|
|
1190
1198
|
return false;
|
|
1191
1199
|
const target = targetChatId(chatKey);
|
|
1192
1200
|
const note = async (content) => {
|
|
@@ -1293,7 +1301,7 @@ export const runMirrorPickerFlow = async ({ log, client, cfg, sessionId, chatKey
|
|
|
1293
1301
|
}
|
|
1294
1302
|
};
|
|
1295
1303
|
// 多问题: 逐题顺序发卡 → 收答 → 下一题。任一题选「CLI」整体转 CLI,选「聊聊」
|
|
1296
|
-
// 整体转讨论;
|
|
1304
|
+
// 整体转讨论; 全部答完合并成一份 answers 随 allow 的 updatedInput 交回。卡不会一次性轰炸 N 张。
|
|
1297
1305
|
const handleAskUserQuestion = async ({ cfg, log, client, body, getMirrorTarget, flushBeforeCard, clientGone }) => {
|
|
1298
1306
|
// ── codebuddy 镜像流去重 ────────────────────────────────────────────────
|
|
1299
1307
|
// codebuddy 的 hook 只在本地面板被提交后到达。mirror 早已发过 vote 卡:
|
|
@@ -1319,12 +1327,12 @@ const handleAskUserQuestion = async ({ cfg, log, client, body, getMirrorTarget,
|
|
|
1319
1327
|
const approver = resolveApprover(cfg, body.session_id, getMirrorTarget);
|
|
1320
1328
|
if (!approver)
|
|
1321
1329
|
return { decision: "ask", reason: "no_approver" };
|
|
1322
|
-
if (!client
|
|
1330
|
+
if (!canReach(client, approver))
|
|
1323
1331
|
return { decision: "ask", reason: "ws_disconnected" };
|
|
1324
1332
|
const target = targetChatId(approver);
|
|
1325
1333
|
const longPollMs = cfg.approval.longPollSec * 1000;
|
|
1326
1334
|
const total = questions.length;
|
|
1327
|
-
const answers =
|
|
1335
|
+
const answers = {};
|
|
1328
1336
|
const flowStart = Date.now();
|
|
1329
1337
|
for (let i = 0; i < total; i++) {
|
|
1330
1338
|
const q = questions[i];
|
|
@@ -1415,12 +1423,13 @@ const handleAskUserQuestion = async ({ cfg, log, client, body, getMirrorTarget,
|
|
|
1415
1423
|
}
|
|
1416
1424
|
if (ans.kind === "empty")
|
|
1417
1425
|
return { decision: "ask", reason: "askq_empty_pick" };
|
|
1418
|
-
answers
|
|
1426
|
+
answers[q.question] = ans.labels; // 键是题目原文, Claude Code 按它对答案
|
|
1419
1427
|
}
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
:
|
|
1423
|
-
|
|
1428
|
+
return {
|
|
1429
|
+
decision: "allow",
|
|
1430
|
+
reason: "answered via WeCom",
|
|
1431
|
+
updated_input: { ...body.tool_input, answers },
|
|
1432
|
+
};
|
|
1424
1433
|
};
|
|
1425
1434
|
const decisionToHook = (d) => (d === "deny" ? "deny" : "allow");
|
|
1426
1435
|
// 「完全跳过审批」的模式字面量, 各家 CLI 不统一: bypassPermissions 是 Claude Code
|
|
@@ -1699,6 +1708,7 @@ export const makeApproveHandler = ({ cfg, log, client, sourcePath, getMirrorTarg
|
|
|
1699
1708
|
sessionId,
|
|
1700
1709
|
turn: openTurnsOf(owner, sessionId)[0]?.id ?? "",
|
|
1701
1710
|
toolName,
|
|
1711
|
+
toolInput,
|
|
1702
1712
|
budget: chatPolicyOf(cfg, owner).stewardBudget,
|
|
1703
1713
|
});
|
|
1704
1714
|
if (reason) {
|
|
@@ -1788,7 +1798,7 @@ export const makeApproveHandler = ({ cfg, log, client, sourcePath, getMirrorTarg
|
|
|
1788
1798
|
});
|
|
1789
1799
|
return;
|
|
1790
1800
|
}
|
|
1791
|
-
// AskUserQuestion 走单独的投票卡分支(
|
|
1801
|
+
// AskUserQuestion 走单独的投票卡分支(allow+updatedInput 交答案 / deny 先聊聊 / ask 转 CLI)。
|
|
1792
1802
|
if (toolName === "AskUserQuestion") {
|
|
1793
1803
|
// writableEnded 为 false 时的 close = 响应还没写就断线 = hook 客户端先死。
|
|
1794
1804
|
// 正常完成时 json() 先置 writableEnded, close 到来后 resolve 不再发生。
|
|
@@ -1918,7 +1928,7 @@ export const makeApproveHandler = ({ cfg, log, client, sourcePath, getMirrorTarg
|
|
|
1918
1928
|
json(res, 200, fallback(cfg, "no_approver", mustCard || guardActive));
|
|
1919
1929
|
return;
|
|
1920
1930
|
}
|
|
1921
|
-
if (!client
|
|
1931
|
+
if (!canReach(client, approver)) {
|
|
1922
1932
|
log.warn("ws not connected");
|
|
1923
1933
|
json(res, 200, fallback(cfg, "ws_disconnected", mustCard || guardActive));
|
|
1924
1934
|
return;
|