wezard 2.0.6 → 2.1.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 +2 -2
- package/dist/daemon/chat-log.js +107 -0
- package/dist/daemon/chat-log.js.map +1 -0
- package/dist/daemon/chat-name.js +1 -1
- package/dist/daemon/inbound.js +10 -3
- package/dist/daemon/inbound.js.map +1 -1
- package/dist/daemon/index.js +125 -22
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/mirror-bridge.js +36 -22
- package/dist/daemon/mirror-bridge.js.map +1 -1
- package/dist/daemon/peers.js +172 -67
- package/dist/daemon/peers.js.map +1 -1
- package/dist/daemon/wizard.js +33 -2
- package/dist/daemon/wizard.js.map +1 -1
- package/dist/mcp/server.js +34 -12
- package/dist/mcp/server.js.map +1 -1
- package/dist/shared/chat-http.js +25 -10
- package/dist/shared/chat-http.js.map +1 -1
- package/dist/shared/chat-render.js +17 -7
- package/dist/shared/chat-render.js.map +1 -1
- package/dist/shared/role-view.js +3 -3
- package/dist/shared/role-view.js.map +1 -1
- package/package.json +4 -1
- package/web/chat.js +26 -4
- package/web/highlight.min.js +1262 -0
- package/web/hljs-github.min.css +10 -0
- package/web/markdown-it.min.js +2 -0
package/dist/daemon/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Daemon entry. Resident process — exits only on signal or fatal WS auth failure.
|
|
2
|
+
import { homedir } from "node:os";
|
|
2
3
|
import { loadConfig } from "../shared/config.js";
|
|
3
4
|
import { makeLogger } from "../shared/log.js";
|
|
4
5
|
import { bindCliBackends } from "../shared/cli-backends.js";
|
|
@@ -29,10 +30,11 @@ import { baseOfKey, bindTagLinker, keyOf, linkTags, normalizeTag, tagFromCwd, ta
|
|
|
29
30
|
import { clipForCharter, inboxPath, memoryPath, memoryRoot, proposeMemory, readMemory } from "./wizard-memory.js";
|
|
30
31
|
import { ensureStewardTask } from "./memory-steward.js";
|
|
31
32
|
import { applyChatNames, chatBaseOf, chatNameOf, clearChatName, listChatNames, normChatName, peerAddress, planChatNames, setChatName } from "./chat-name.js";
|
|
32
|
-
import { bindWizardStore, loadWizardStore, wizardStore, settleName, settleAll, evictStaleName, childrenOf, ancestorsOf, renderCharter, } from "./wizard.js";
|
|
33
|
+
import { bindWizardStore, loadWizardStore, wizardStore, settleName, settleAll, evictStaleName, childrenOf, ancestorsOf, renderCharter, renderRoster, } from "./wizard.js";
|
|
33
34
|
import { bindNoticeBox, createNoticeBox, chatAudience } from "./notices.js";
|
|
34
35
|
import { loadJobStore, renderJobOpen, renderJobClose, JOB_MEMBER_MAX } from "./jobs.js";
|
|
35
|
-
import { clipMiddle, extractResult, renderPeerEnvelope } from "./peers.js";
|
|
36
|
+
import { clipMiddle, extractResult, keepalivePingSigs, lastExchange, lastModel, renderPeerEnvelope, renderTaskEnvelope } from "./peers.js";
|
|
37
|
+
import { parseWhen, renderChatLog } from "./chat-log.js";
|
|
36
38
|
import { startGraph, stopRun, getRun, listRuns, validateSpec, waitForIdle, waitForQuorum, } from "./graph.js";
|
|
37
39
|
// pino's file transport is async; without flush, log.fatal before process.exit
|
|
38
40
|
// vanishes. Mirror anything fatal to stderr too so launchd's stderr.log captures it.
|
|
@@ -527,6 +529,8 @@ const main = async () => {
|
|
|
527
529
|
const body = (await readBody(req));
|
|
528
530
|
return { self: resolveSelf(body), body };
|
|
529
531
|
};
|
|
532
|
+
// list_peers 已并进 wizard_roster ({chat}); 路由留着, 是因为正在跑的 wizard 的
|
|
533
|
+
// MCP 进程还是旧代码, 仍会打到这里。
|
|
530
534
|
http.register("POST /peers/list", async (req, res) => {
|
|
531
535
|
const { self } = await readPeerBody(req);
|
|
532
536
|
if (!self) {
|
|
@@ -685,19 +689,104 @@ const main = async () => {
|
|
|
685
689
|
}
|
|
686
690
|
const { target, foreign } = r;
|
|
687
691
|
const turns = Math.min(Math.max(Number(body.turns ?? 6) || 6, 1), 40);
|
|
688
|
-
//
|
|
689
|
-
//
|
|
692
|
+
// 对话只从 transcript 读 (正文轮次, 去掉保温 ping/pong), 不刮终端: pane 是
|
|
693
|
+
// 截断的视口加一层 TUI 装饰, 而「它卡在哪」transcript 答得更准 —— 不在转圈
|
|
694
|
+
// 却悬着工具调用, 就是停在审批卡 / 本地弹窗上等人点。
|
|
690
695
|
const peek = await m.peekTurns(target, turns);
|
|
691
|
-
const
|
|
696
|
+
const waiting = peek.waiting ?? [];
|
|
697
|
+
const state = peek.busy
|
|
698
|
+
? "正在生成"
|
|
699
|
+
: waiting.length
|
|
700
|
+
? `停在 ${waiting.join(" / ")} 上 —— 工具调用发出去了还没结果, 多半在等人点审批卡或本地弹窗`
|
|
701
|
+
: "空闲";
|
|
692
702
|
json(res, 200, {
|
|
693
703
|
ok: true,
|
|
694
|
-
target,
|
|
695
|
-
foreign,
|
|
696
|
-
dialog: peek.dialog ?? "",
|
|
697
|
-
pane: pane?.pane ?? "",
|
|
698
|
-
error: peek.ok ? undefined : (pane?.reason ?? peek.reason),
|
|
704
|
+
name: peerAddress(cfg, self, target),
|
|
699
705
|
busy: peek.busy ?? false,
|
|
700
|
-
|
|
706
|
+
...(waiting.length ? { waiting } : {}),
|
|
707
|
+
text: [`${displayName(target)} · ${state}${foreign ? ` · 住在群 ${chatNameOf(cfg, target) || "(未命名)"}` : ""}`, peek.dialog ?? `(${peek.reason})`].join("\n"),
|
|
708
|
+
});
|
|
709
|
+
});
|
|
710
|
+
// ── 聊天记录 ────────────────────────────────────────────────────────
|
|
711
|
+
// peek_peer 读的是**一个会话**听到和说过的一切; 这条读的是**往来**: 三级收窄
|
|
712
|
+
// role (谁的视角) → chat (哪个群) → target (和谁), 再按时间窗与条数裁 —— 与
|
|
713
|
+
// rolepage 同一条轴。全部从各会话的 transcript 现算 (见 chat-log.ts), 不经 turn
|
|
714
|
+
// store。名字全局可达, 一句话可能落在任何一个会话的 transcript 里 (A 对 B 说的
|
|
715
|
+
// 在 B 那份), 所以候选是全体; 按 mtime 只留最近动过的那一批 (一次 stat, 不
|
|
716
|
+
// spawn), 点了名的 role / target 与调用方自己无条件入选。
|
|
717
|
+
const LOG_RECENT_MS = 7 * 24 * 3600_000;
|
|
718
|
+
const LOG_SESSIONS = 40;
|
|
719
|
+
const logSession = (t) => {
|
|
720
|
+
const home = baseOfKey(t.target);
|
|
721
|
+
const rec = wizards.get(t.target);
|
|
722
|
+
return {
|
|
723
|
+
name: displayName(t.target),
|
|
724
|
+
jsonlPath: t.jsonlPath,
|
|
725
|
+
homeChat: chatNameOf(cfg, home),
|
|
726
|
+
homeHuman: home.startsWith("user:") ? home.slice(5) : "人",
|
|
727
|
+
since: rec?.clonedFrom ? rec.bornAt : 0,
|
|
728
|
+
};
|
|
729
|
+
};
|
|
730
|
+
/** role / target 的写法: wizard 的名字解析成它的称呼; 解析不出的原样当作人的
|
|
731
|
+
* userid (或 `定时 <id>`) —— 记录里人就是按这个字面出现的。 */
|
|
732
|
+
const logRole = (self, ref) => {
|
|
733
|
+
const r = resolvePeer(self, ref);
|
|
734
|
+
return r.ok ? { name: displayName(r.target), target: r.target } : { name: ref };
|
|
735
|
+
};
|
|
736
|
+
http.register("POST /chats/read", async (req, res) => {
|
|
737
|
+
const { self, body } = await readPeerBody(req);
|
|
738
|
+
if (!self) {
|
|
739
|
+
json(res, 400, { ok: false, reason: "cannot resolve caller session" });
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
// 线上字段是 `with`: 请求体里的 `target` 已经是「调用方是谁」的覆盖 (resolveSelf)。
|
|
743
|
+
const b = body;
|
|
744
|
+
ensureChatNames(self);
|
|
745
|
+
const now = Date.now();
|
|
746
|
+
const [roleRef, chatRef, targetRef, sinceRef, untilRef] = [b.role, b.chat, b.with, b.since, b.until].map((x) => (x ?? "").toString().trim());
|
|
747
|
+
// 什么都没给 = 调用方这一轮所在的群; 只给 target = 调用方与它的往来。
|
|
748
|
+
const base = chatRef ? chatBaseOf(cfg, chatRef) : roleRef || targetRef ? "" : channelOf(self);
|
|
749
|
+
if (chatRef && !base) {
|
|
750
|
+
json(res, 404, { ok: false, reason: `认不出聊天 '${chatRef}' (list_chats 看有哪些)` });
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
const role = roleRef ? logRole(self, roleRef) : targetRef ? { name: displayName(self), target: self } : undefined;
|
|
754
|
+
const target = targetRef ? logRole(self, targetRef) : undefined;
|
|
755
|
+
const since = sinceRef ? parseWhen(sinceRef, now) : undefined;
|
|
756
|
+
const until = untilRef ? parseWhen(untilRef, now) : undefined;
|
|
757
|
+
if ((sinceRef && since === undefined) || (untilRef && until === undefined)) {
|
|
758
|
+
json(res, 400, { ok: false, reason: "时间认不出 —— 写 `2h` / `30m` / `3d` (多久以前)、`14:30` (今天)、`09-30 14:30` 或 ISO 时间" });
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
const limit = Math.min(Math.max(Number(b.limit ?? 30) || 30, 1), 200);
|
|
762
|
+
const per = Math.min(Math.max(Number(b.per ?? 500) || 500, 40), 4000);
|
|
763
|
+
const pinned = new Set([self, role?.target, target?.target]);
|
|
764
|
+
const all = m.transcripts();
|
|
765
|
+
// 两方之间的话只可能落在这两方里的 wizard 自己的 transcript 里 (人没有 transcript)
|
|
766
|
+
// —— 不必去翻别的会话, 「往回还有没有」也因此答得准。
|
|
767
|
+
const between = [role?.target, target?.target].filter((t) => !!t);
|
|
768
|
+
const sessions = role && target && between.length
|
|
769
|
+
? all.filter((t) => between.includes(t.target))
|
|
770
|
+
: [
|
|
771
|
+
...all.filter((t) => pinned.has(t.target)),
|
|
772
|
+
...all.filter((t) => !pinned.has(t.target) && now - t.mtime < LOG_RECENT_MS).sort((x, y) => y.mtime - x.mtime).slice(0, LOG_SESSIONS),
|
|
773
|
+
];
|
|
774
|
+
const chat = base ? chatNameOf(cfg, base) : undefined;
|
|
775
|
+
const log_ = renderChatLog(sessions.map(logSession), { role: role?.name, chat, target: target?.name, since, until, limit, per }, keepalivePingSigs(cfg.wrc.mirror.keepalive.ping), now);
|
|
776
|
+
const scope = [
|
|
777
|
+
chat !== undefined ? `${base.startsWith("user:") ? "单聊" : "群"} ${chat || base}` : "",
|
|
778
|
+
role && target ? `${role.name} ⇄ ${target.name}` : role ? `${role.name} 的往来` : "",
|
|
779
|
+
].filter(Boolean).join(" · ");
|
|
780
|
+
json(res, 200, {
|
|
781
|
+
ok: true,
|
|
782
|
+
shown: log_.shown,
|
|
783
|
+
total: log_.total,
|
|
784
|
+
text: [
|
|
785
|
+
`${scope} · ${log_.shown} 条`,
|
|
786
|
+
log_.text || "(没有读到对话)",
|
|
787
|
+
...(log_.earlier ? [`更早的: until="${log_.earlier}"`] : log_.later || !log_.shown ? [] : ["(往回已经到头)"]),
|
|
788
|
+
...(log_.later ? [`往后还有 ${log_.total - log_.shown} 条: since="${log_.later}"`] : []),
|
|
789
|
+
].join("\n"),
|
|
701
790
|
});
|
|
702
791
|
});
|
|
703
792
|
http.register("POST /peers/send", async (req, res) => {
|
|
@@ -1162,8 +1251,8 @@ const main = async () => {
|
|
|
1162
1251
|
...b,
|
|
1163
1252
|
target: p.target,
|
|
1164
1253
|
chat: chatNameOf(cfg, p.target) || p.chat,
|
|
1165
|
-
|
|
1166
|
-
|
|
1254
|
+
solo: baseOfKey(p.target).startsWith("user:"),
|
|
1255
|
+
jsonlPath: p.jsonlPath,
|
|
1167
1256
|
model: p.model,
|
|
1168
1257
|
busy: p.busy,
|
|
1169
1258
|
alive: p.paneAlive,
|
|
@@ -1180,13 +1269,16 @@ const main = async () => {
|
|
|
1180
1269
|
...briefOf(self, w.target),
|
|
1181
1270
|
target: w.target,
|
|
1182
1271
|
chat: chatNameOf(cfg, w.target),
|
|
1272
|
+
solo: baseOfKey(w.target).startsWith("user:"),
|
|
1273
|
+
jsonlPath: "",
|
|
1274
|
+
model: "",
|
|
1183
1275
|
busy: false,
|
|
1184
1276
|
alive: false,
|
|
1185
1277
|
self: false,
|
|
1186
1278
|
// 没跑过就没有活动时刻。排序拿它当最旧 —— 冷会话排在活着的后面, 正是
|
|
1187
1279
|
// 截断时该被留下的顺序。
|
|
1188
1280
|
lastActivity: 0,
|
|
1189
|
-
summary: "
|
|
1281
|
+
summary: "",
|
|
1190
1282
|
...kinOf(self, all, w.target),
|
|
1191
1283
|
}));
|
|
1192
1284
|
return [...live, ...cold];
|
|
@@ -1258,16 +1350,27 @@ const main = async () => {
|
|
|
1258
1350
|
// 自己在最前, 然后活着的, 然后最近动过的 —— 截断时留下的正是最可能有用的。
|
|
1259
1351
|
const ranked = hit.slice().sort((x, y) => Number(y.self) - Number(x.self) || Number(y.alive) - Number(x.alive) || (y.lastActivity ?? 0) - (x.lastActivity ?? 0));
|
|
1260
1352
|
const limit = Math.min(Math.max(Number(b.limit ?? 40) || 40, 1), 300);
|
|
1261
|
-
|
|
1353
|
+
// 「最近」与模型都从 transcript 读, 而且只为列出来的这一页读: 摘要用不指代任何人
|
|
1354
|
+
// 的箭头 (读名册的是另一个 wizard), 模型取它真正跑的那个 —— 绑定里的 model 只在
|
|
1355
|
+
// 有人显式选过时才有。
|
|
1356
|
+
const warm = keepalivePingSigs(cfg.wrc.mirror.keepalive.ping);
|
|
1357
|
+
const shown = ranked.slice(0, limit).map((r) => ({
|
|
1358
|
+
...r,
|
|
1359
|
+
model: r.model || (r.jsonlPath ? lastModel(r.jsonlPath) : ""),
|
|
1360
|
+
summary: r.jsonlPath ? lastExchange(r.jsonlPath, 80, warm) : "",
|
|
1361
|
+
}));
|
|
1262
1362
|
json(res, 200, {
|
|
1263
1363
|
ok: true,
|
|
1264
|
-
self: identityOf(self, self),
|
|
1265
1364
|
total: all.length,
|
|
1266
1365
|
matched: hit.length,
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1366
|
+
shown: shown.length,
|
|
1367
|
+
text: [
|
|
1368
|
+
`名册 · 列出 ${shown.length}/${hit.length} 个 (全机 ${all.length} 个) · 忙 = 正在生成, 闲 = 活着没在跑, 冷 = 没有 pane (发消息会唤醒)`,
|
|
1369
|
+
renderRoster(shown, Date.now(), homedir()),
|
|
1370
|
+
...(hit.length > shown.length
|
|
1371
|
+
? [`还有 ${hit.length - shown.length} 个没列出来 —— 用 query (名字/职责) / cwd (工作区) / chat 收窄, 或者调大 limit`]
|
|
1372
|
+
: []),
|
|
1373
|
+
].join("\n"),
|
|
1271
1374
|
});
|
|
1272
1375
|
});
|
|
1273
1376
|
// 记忆三种作用域: self 跟着自己 (wizards.json), 直写; chat / workspace 是共享的 md,
|
|
@@ -1837,7 +1940,7 @@ const main = async () => {
|
|
|
1837
1940
|
scheduledTaskInject = async (target, text, { taskId, fresh }) => {
|
|
1838
1941
|
const busy = fresh ? false : await m.isBusy(target);
|
|
1839
1942
|
if (!fresh && !busy)
|
|
1840
|
-
return m.injectText(target, text, undefined, { fromChat: true, from: { kind: "task", taskId } });
|
|
1943
|
+
return m.injectText(target, text, undefined, { fromChat: true, from: { kind: "task", taskId }, envelope: renderTaskEnvelope(taskId) });
|
|
1841
1944
|
// 执行体挂在日程的主人名下: 名字 `<主人>-task-xxxx`, parent = 主人 —— 它的
|
|
1842
1945
|
// rolepage 里看得见这一枪是谁的日程放的。落在执行目标的群/目录里。
|
|
1843
1946
|
const base = baseOfKey(target);
|
|
@@ -1864,7 +1967,7 @@ const main = async () => {
|
|
|
1864
1967
|
notifyChat(base, withTagHeader(target, busy
|
|
1865
1968
|
? `⏰ 定时任务到点时正忙, 已起白板 wizard .${runnerName} 单独执行, 完成后自动收掉`
|
|
1866
1969
|
: `⏰ 定时任务已起白板 wizard .${runnerName} 执行, 完成后自动收掉`));
|
|
1867
|
-
const inj = await m.injectText(runner, text, undefined, { fromChat: true, from: { kind: "task", taskId } });
|
|
1970
|
+
const inj = await m.injectText(runner, text, undefined, { fromChat: true, from: { kind: "task", taskId }, envelope: renderTaskEnvelope(taskId) });
|
|
1868
1971
|
if (!inj.ok) {
|
|
1869
1972
|
wizards.drop(runner);
|
|
1870
1973
|
return inj;
|