wezard 2.4.0 → 2.4.1
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 +19 -13
- package/dist/daemon/approval.js.map +1 -1
- package/dist/daemon/chat-name.js.map +1 -1
- package/dist/daemon/config-api.js +20 -4
- package/dist/daemon/config-api.js.map +1 -1
- package/dist/daemon/inbound.js +7 -4
- package/dist/daemon/inbound.js.map +1 -1
- package/dist/daemon/index.js +119 -45
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/mirror-bridge.js +2 -1
- package/dist/daemon/mirror-bridge.js.map +1 -1
- package/dist/daemon/notices.js +31 -4
- package/dist/daemon/notices.js.map +1 -1
- package/dist/daemon/peers.js +17 -4
- package/dist/daemon/peers.js.map +1 -1
- package/dist/daemon/receipts.js +4 -4
- package/dist/daemon/receipts.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/wizard.js +10 -0
- package/dist/daemon/wizard.js.map +1 -1
- package/dist/shared/chat-http.js +16 -6
- package/dist/shared/chat-http.js.map +1 -1
- package/dist/shared/config.js +10 -0
- package/dist/shared/config.js.map +1 -1
- package/dist/shared/role-render.js +3 -1
- package/dist/shared/role-render.js.map +1 -1
- package/dist/shared/role-view.js +66 -22
- package/dist/shared/role-view.js.map +1 -1
- package/hooks/pre-tool-use.sh +9 -6
- package/package.json +1 -1
- package/web/chat.css +6 -0
- package/web/chat.js +64 -61
package/dist/daemon/index.js
CHANGED
|
@@ -40,6 +40,7 @@ import { baseOfKey, bindTagLinker, isInternalKey, keyOf, linkTags, normalizeTag,
|
|
|
40
40
|
import { appendEpisode, clipForCharter, cwdOfMd, episodePath, inboxPath, mdsOf, proposedCwds, memoryPath, memoryRoot, proposeMemory, readMemory } from "./wizard-memory.js";
|
|
41
41
|
import { retireStewardTask, startSteward, stewardEnvelope, STEWARD_ID, STEWARD_RUN_MS, STEWARD_TARGET } from "./memory-steward.js";
|
|
42
42
|
import { applyChatNames, chatBaseOf, chatNameOf, chatPolicyOf, clearChatName, listChatNames, normChatName, peerAddress, planChatNames, setChatName } from "./chat-name.js";
|
|
43
|
+
import { createTopOnlyNudge, gcNudge, policyKeyOf, renderNudge } from "./top-only-nudge.js";
|
|
43
44
|
import { bindWizardStore, loadWizardStore, wizardStore, settleName, settleAll, backfillBorn, sweepUnborn, BOOT_ID, evictStaleName, childrenOf, ancestorsOf, renderCharter, renderLead, renderLeadPlaybook, ROSTER_FRESH_MS, renderRoster, handoffAt, memoryNudgeAt, MEMORY_NUDGE_FLOOR, MEMORY_MAX, NOTE_MAX, } from "./wizard.js";
|
|
44
45
|
import { bindNoticeBox, createNoticeBox, createSeenRing, gcSeen, chatAudience, hears } from "./notices.js";
|
|
45
46
|
import { createLedger, digest, pendingGc, renderDigest, renderTable, renderAwaiting, renderStalled, renderShelved, ago as agoMs, HEARD_EVERY_MS } from "./pending-items.js";
|
|
@@ -134,7 +135,9 @@ const main = async () => {
|
|
|
134
135
|
});
|
|
135
136
|
// 人话也过交接的闸 (见 handoff.ts): 交接期间说的话进新会话, 不被旧会话带走、也不在
|
|
136
137
|
// 重开的空档里 resume 回旧 sid。
|
|
137
|
-
|
|
138
|
+
// 宪章守卫 (见「改配置会不会动到宪章」): 要等下面宪章渲染那一块立起来才有; 之前 (启动时那次自动补名) 只写不比对。
|
|
139
|
+
let guardCharter = (write) => ({ r: write(), note: Promise.resolve("") });
|
|
140
|
+
installInboundRouter(ws.client, cfg, log, { ...bridge, dispatch: async (a) => (await handedOff(a.principal), bridge.dispatch(a)) }, sourcePath, (w, c) => guardCharter(w, c));
|
|
138
141
|
// approval click → finalize 当前 liveStream, 后续 tool/text 落到 standalone。
|
|
139
142
|
// 规则 2: 用户点击授权那一刻就是"上一段对话"的边界, 截断 stream 让授权后的
|
|
140
143
|
// 工作单独成块, 比让 stream 一直长到下一个 inbound / hardTimer 更清晰。
|
|
@@ -548,24 +551,31 @@ const main = async () => {
|
|
|
548
551
|
const raw = (body.name ?? "").toString().trim();
|
|
549
552
|
// "-" is the same erase gesture `/name -` uses in the chat — one verb,
|
|
550
553
|
// one meaning, whether a human or an agent performs it.
|
|
554
|
+
// 聊天名进宪章: 过宪章守卫, 受影响的 wizard 被提醒 handoff, 调用方自己受影响的那段话放在 `charter`。
|
|
555
|
+
const byHuman = !!askerOf(self)?.who.startsWith("user:");
|
|
556
|
+
const charterOf = async (note) => ((t) => (t ? { charter: t } : {}))((await note).trim());
|
|
551
557
|
if (raw === "-") {
|
|
552
|
-
const gone = clearChatName(cfg, sourcePath, self);
|
|
553
|
-
json(res, 200, { ok: true, base: baseOfKey(self), name: "", previous: gone });
|
|
558
|
+
const { r: gone, note } = guardCharter(() => clearChatName(cfg, sourcePath, self), (g) => g ? { what: `\`name_chat\` 取消了聊天名 \`${g}\``, self, byHuman } : undefined);
|
|
559
|
+
json(res, 200, { ok: true, base: baseOfKey(self), name: "", previous: gone, ...(await charterOf(note)) });
|
|
554
560
|
return;
|
|
555
561
|
}
|
|
556
562
|
const previous = chatNameOf(cfg, self);
|
|
557
|
-
const
|
|
563
|
+
const home = baseOfKey(self);
|
|
564
|
+
const { r, note } = guardCharter(() => {
|
|
565
|
+
const r = setChatName(cfg, sourcePath, self, raw);
|
|
566
|
+
if (!r.ok)
|
|
567
|
+
return r;
|
|
568
|
+
// 默认 wizard 的名字取自聊天名; 它还叫旧聊天名 (没被单独改过) 就跟着改。
|
|
569
|
+
const cur = wizards.get(home)?.name ?? "";
|
|
570
|
+
return { ...r, wizardRenamed: previous && cur.toLowerCase() === previous.toLowerCase() ? wizards.rename(home, r.name) : undefined };
|
|
571
|
+
}, (r) => r.ok && r.name !== previous ? { what: `\`name_chat\` 把聊天${previous ? ` \`${previous}\`` : ""}改名为 \`${r.name}\``, self, byHuman } : undefined);
|
|
558
572
|
if (!r.ok) {
|
|
559
573
|
json(res, 409, { ok: false, reason: r.reason });
|
|
560
574
|
return;
|
|
561
575
|
}
|
|
562
|
-
|
|
563
|
-
const home = baseOfKey(self);
|
|
564
|
-
const cur = wizards.get(home)?.name ?? "";
|
|
565
|
-
const wizardRenamed = previous && cur.toLowerCase() === previous.toLowerCase() ? wizards.rename(home, r.name) : undefined;
|
|
566
|
-
if (wizardRenamed)
|
|
576
|
+
if (r.wizardRenamed)
|
|
567
577
|
void m.retitlePane(home);
|
|
568
|
-
json(res, 200, { ok: true, base: r.base, name: r.name, previous, ...(wizardRenamed ? { wizardRenamed } : {}) });
|
|
578
|
+
json(res, 200, { ok: true, base: r.base, name: r.name, previous, ...(r.wizardRenamed ? { wizardRenamed: r.wizardRenamed } : {}), ...(await charterOf(note)) });
|
|
569
579
|
});
|
|
570
580
|
const readPeerBody = async (req) => {
|
|
571
581
|
const body = (await readBody(req));
|
|
@@ -607,6 +617,17 @@ const main = async () => {
|
|
|
607
617
|
/** 一个 wizard 在群里的称呼: `emoji .name`, 挂它的 rolepage。名字全局唯一, 不再
|
|
608
618
|
* 因为落在哪个群而换写法 —— 引用这一段就能跟它说话 (parseTagHeader 认得)。 */
|
|
609
619
|
const relayLabel = (t, _dest) => tagLink(t, tagHead(t));
|
|
620
|
+
// 没开顶层模式的群里公开气泡多了 → 往那个群的管家 (群默认会话, target 就是 base principal) 信箱挂一行,
|
|
621
|
+
// 让它问人要不要开顶层模式 (见 top-only-nudge.ts)。路径里写聊天名: 管家照抄就能 config_set。
|
|
622
|
+
const topOnlyNudge = createTopOnlyNudge({
|
|
623
|
+
store: loadJsonMap("~/.wezard/top-only-nudge.json", gcNudge),
|
|
624
|
+
policy: () => ((p) => ({ windowMs: p.windowMin * 60_000, threshold: p.threshold, cooldownMs: p.cooldownH * 3_600_000 }))(cfg.wrc.mirror.topOnlyNudge),
|
|
625
|
+
nudge: (chat, n) => {
|
|
626
|
+
const p = cfg.wrc.mirror.topOnlyNudge;
|
|
627
|
+
notices.post([chat], renderNudge(policyKeyOf(chatNameOf(cfg, chat), chat), n, p.windowMin, p.cooldownH));
|
|
628
|
+
log.info({ chat, n }, "top-only nudge: 已提醒管家问人要不要开顶层模式");
|
|
629
|
+
},
|
|
630
|
+
});
|
|
610
631
|
/** 公开的 wizard 间对话: 落在 `channel` 这个群。 */
|
|
611
632
|
const relayPeer = (from, to, body, channel) => {
|
|
612
633
|
const text = body.trim();
|
|
@@ -617,6 +638,8 @@ const main = async () => {
|
|
|
617
638
|
// 头独占一行, 正文自成一个块 —— 只隔一个换行的话, markdown 会把正文首行
|
|
618
639
|
// 当成头那一段的续行; 表格因此整张塌成一行带竖线的文字 (表格不能打断段落)。
|
|
619
640
|
notifyChat(channel, `${head}\n\n${clipped}`);
|
|
641
|
+
if (!chatPolicyOf(cfg, channel).topOnly)
|
|
642
|
+
topOnlyNudge.count(baseOfKey(channel));
|
|
620
643
|
};
|
|
621
644
|
/** 调用方这一轮所在的公开频道: 人从哪个群叫的它 / 公开 peer 轮的那个群;
|
|
622
645
|
* 私聊轮或无记录 → 它的 home 群。notify 与 public send_peer 默认发到这里。 */
|
|
@@ -1474,6 +1497,13 @@ const main = async () => {
|
|
|
1474
1497
|
const notices = bindNoticeBox(createNoticeBox({
|
|
1475
1498
|
probe: (t) => [...handoffNudge(t), ...memoryNudge(t)],
|
|
1476
1499
|
digest: pendingDigest,
|
|
1500
|
+
// 群况快照只给长住的 (没有 parent): 分身的协调人是生它的那位, 宪章里的出生快照够用。
|
|
1501
|
+
// 表里是这个群的常驻面: 管家、长住有职责的、管家生的 (群里的执行者)、收件人自己生的;
|
|
1502
|
+
// 再往下一层的 (分身的分身, 回归测试那批 rr-*) 不进表 —— 同名册变动 hears 的取舍。
|
|
1503
|
+
snapshot: (t) => (wizards.get(t)?.parent ? undefined : () => m.chatTargets(t)
|
|
1504
|
+
.filter((x) => x !== t && wizards.get(x)?.name)
|
|
1505
|
+
.filter((x) => ((p) => !tagOfKey(x) || (!p ? !!wizards.get(x)?.description : p === t || p === baseOfKey(t)))(wizards.get(x)?.parent))
|
|
1506
|
+
.map((x) => ({ name: displayName(x), state: ((v) => (v === undefined ? "-" : v ? "idle" : "busy"))(m.idleVerdict(x)), job: wizards.get(x)?.description || (tagOfKey(x) ? "" : "steward") }))),
|
|
1477
1507
|
seen: createSeenRing(loadJsonMap(cfg.wrc.mirror.noticeSeenFile, gcSeen), (t) => m.sessionInfo(t)?.sessionId ?? ""),
|
|
1478
1508
|
cwdOf: (t) => ((c) => c.runningCwd || c.defaultCwd)(m.getCwd(t)),
|
|
1479
1509
|
}));
|
|
@@ -1548,7 +1578,9 @@ const main = async () => {
|
|
|
1548
1578
|
.slice()
|
|
1549
1579
|
.sort((x, y) => y.targets.length - x.targets.length)
|
|
1550
1580
|
.map((c) => ({ base: c.base, cwd: chatCwd(c.targets) })));
|
|
1551
|
-
|
|
1581
|
+
if (!Object.keys(plan).length)
|
|
1582
|
+
return {};
|
|
1583
|
+
const { r: named } = guardCharter(() => applyChatNames(cfg, sourcePath, plan), (n) => Object.keys(n).length ? { what: `自动补的聊天名 (${Object.values(n).map((x) => `\`${x}\``).join(" ")})` } : undefined);
|
|
1552
1584
|
for (const [base, name] of Object.entries(named)) {
|
|
1553
1585
|
log.info({ base, name }, "chat: auto-named from workspace");
|
|
1554
1586
|
// 群里不发气泡 (一次补名会命中十几个群, 那是刷屏), 但住在里面的 wizard
|
|
@@ -2861,7 +2893,27 @@ const main = async () => {
|
|
|
2861
2893
|
usageMemo = { at: Date.now(), u: { today: fold(r.today), week: fold(r.week) } };
|
|
2862
2894
|
return usageMemo.u;
|
|
2863
2895
|
};
|
|
2864
|
-
|
|
2896
|
+
/** chatPolicy 的依据: 各聊天名 ↔ principal 与它此刻的顶层模式 —— 键落盘是 principal, 人和模型认的是名字。 */
|
|
2897
|
+
const chatPolicyEvidence = (p) => {
|
|
2898
|
+
if (p[0] !== "chatPolicy")
|
|
2899
|
+
return [];
|
|
2900
|
+
const named = listChatNames(cfg);
|
|
2901
|
+
const line = (b) => `${named.filter((c) => c.base === b).map((c) => c.name).join(" / ") || "(未命名)"} = ${b} · 顶层模式 ${chatPolicyOf(cfg, b).topOnly ? "开" : "关"}`;
|
|
2902
|
+
// 查某一个聊天: 只报它; 查整张表: 报写过策略的, 其余命名聊天只报个数。
|
|
2903
|
+
if (p[1])
|
|
2904
|
+
return [line(p[1])];
|
|
2905
|
+
const set = Object.keys(cfg.chatPolicy);
|
|
2906
|
+
const rest = named.filter((c) => !set.includes(c.base)).length;
|
|
2907
|
+
return [
|
|
2908
|
+
"键写聊天名或 principal 都行, 落盘换成 principal:",
|
|
2909
|
+
...set.map(line),
|
|
2910
|
+
...(rest ? [`其余 ${rest} 个已命名聊天没写, 按默认 (顶层模式关) —— 查某个: config_get({path:"chatPolicy.<聊天名>"})`] : []),
|
|
2911
|
+
];
|
|
2912
|
+
};
|
|
2913
|
+
const configEvidence = (p) => [
|
|
2914
|
+
...tierEvidence(cfg.models, p, tierUsage, (tier) => wizards.all().filter((w) => w.tier === tier).length, { tokens: fmtTokens, cost: fmtCost }),
|
|
2915
|
+
...chatPolicyEvidence(p),
|
|
2916
|
+
];
|
|
2865
2917
|
// ── 改配置会不会动到宪章 ─────────────────────────────────────────
|
|
2866
2918
|
// 宪章是 --append-system-prompt 压进去的, 随进程终身: 改了进宪章的配置, 已在跑的 wizard
|
|
2867
2919
|
// 看不见, 要 handoff 重开 (重新渲染宪章) 才换上。不按字段清单判 —— 宪章读哪些配置随代码变,
|
|
@@ -2871,17 +2923,19 @@ const main = async () => {
|
|
|
2871
2923
|
const x = charterSections(a), y = charterSections(b);
|
|
2872
2924
|
return [...new Set([...x.keys(), ...y.keys()])].filter((k) => x.get(k) !== y.get(k));
|
|
2873
2925
|
};
|
|
2874
|
-
/** pane
|
|
2875
|
-
const
|
|
2926
|
+
/** 宪章挂在 pane 上的 wizard —— 候选。同步取: 守卫要在写之前拍快照, 中间不能 await。 */
|
|
2927
|
+
const attachedWizards = () => wizards.all().map((w) => w.target).filter((t) => !!m.sessionInfo(t)?.tmuxPane);
|
|
2928
|
+
/** 其中 pane 活着的 —— 宪章在效的只有它们; 冷的醒来时宪章重渲染, 自然是新的。一次 list-panes 答完。 */
|
|
2929
|
+
const liveOf = async (targets) => {
|
|
2876
2930
|
const snap = await runTmux(["list-panes", "-a", "-F", "#{pane_id}"]);
|
|
2877
2931
|
const live = new Set(snap.ok ? snap.stdout.split("\n").map((l) => l.trim()).filter(Boolean) : []);
|
|
2878
|
-
return
|
|
2932
|
+
return targets.filter((t) => live.has(m.sessionInfo(t)?.tmuxPane ?? ""));
|
|
2879
2933
|
};
|
|
2880
2934
|
/** 这些 wizard 此刻的宪章正文。 */
|
|
2881
2935
|
const charterSnapshot = (targets) => new Map(targets.map((t) => [t, charterText(t, charterOptsOf(t, m.getCwd(t).runningCwd || undefined), true)]));
|
|
2882
2936
|
/** 把 `next` 那一节换进活的 cfg 渲染一遍再换回 —— 同步完成, 中间没有 await, 别的模块看不见这一瞬。
|
|
2883
2937
|
* reload 项落盘后活值不变, 只有这样才算得出它 reload 之后的宪章。 */
|
|
2884
|
-
const charterUnder = (pl
|
|
2938
|
+
const charterUnder = (pl) => (targets) => {
|
|
2885
2939
|
const live = cfg;
|
|
2886
2940
|
const key = pl.path[0];
|
|
2887
2941
|
const saved = live[key];
|
|
@@ -2893,26 +2947,48 @@ const main = async () => {
|
|
|
2893
2947
|
live[key] = saved;
|
|
2894
2948
|
}
|
|
2895
2949
|
};
|
|
2896
|
-
const charterImpact = (before,
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
/** 受影响的 wizard 各挂一条提醒 (随它下一条消息到); 给调用方的那段话。调用方自己受影响就直说要它 handoff。 */
|
|
2901
|
-
const announceCharterImpact = (pl, hits, self) => {
|
|
2950
|
+
const charterImpact = (before, after) => [...before].flatMap(([t, a]) => { const secs = changedSections(a, after.get(t) ?? a); return secs.length ? [{ target: t, sections: secs }] : []; });
|
|
2951
|
+
/** 受影响的 wizard 各挂一条提醒 (随它下一条消息到); 给调用方的那段话。调用方自己受影响就直说要它 handoff。
|
|
2952
|
+
* 人发起的改动多一句「交接之前还是旧规矩」: 管家要知道它此刻向人汇报的方式还没变, 别让人以为已经生效。 */
|
|
2953
|
+
const announceCharterImpact = (c, hits) => {
|
|
2902
2954
|
if (!hits.length)
|
|
2903
2955
|
return "";
|
|
2904
|
-
const
|
|
2905
|
-
const
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
const
|
|
2956
|
+
const ask = `${c.reload ? "等守护进程 reload 之后再" : ""}调 \`handoff({brief})\` 原地重开 (手上这件告一段落就交; 宪章在 handoff 时重新生成)`;
|
|
2957
|
+
const lag = (t) => !c.byHuman ? "" : tagOfKey(t)
|
|
2958
|
+
? " —— 这是人发起的改动: 交接之前你仍按旧规矩做事, handoff 之后才按新的"
|
|
2959
|
+
: " —— 这是人发起的改动: 交接之前你仍按旧规矩向人汇报, handoff 之后才会按新规矩汇报; 回人时说一句「交接后生效」";
|
|
2960
|
+
const others = hits.filter((h) => h.target !== c.self);
|
|
2961
|
+
others.forEach((h) => notices.post([h.target], `${c.what} 改动了你的宪章 (系统提示, 变了的段: ${h.sections.join(" / ")}); 已在跑的你看不见新规矩 —— ${ask}${lag(h.target)}`));
|
|
2962
|
+
const mine = hits.find((h) => h.target === c.self);
|
|
2909
2963
|
return [
|
|
2910
2964
|
"",
|
|
2911
2965
|
`⚠ 这次改动进了宪章 (系统提示): 已在跑的 wizard 看不见, 要 handoff 才换上。受影响 ${hits.length} 个: ${hits.map((h) => displayName(h.target)).join(" ")}`,
|
|
2912
2966
|
...(others.length ? [`已给其余 ${others.length} 个各挂一条提醒 (随它们下一条消息到), 让它们${ask}`] : []),
|
|
2913
|
-
...(mine ? [`**你自己也在其中** (变了的段: ${mine.sections.join(" / ")}): 这一轮收尾后${ask}`] : []),
|
|
2967
|
+
...(mine ? [`**你自己也在其中** (变了的段: ${mine.sections.join(" / ")}): 这一轮收尾后${ask}${lag(mine.target)}`] : []),
|
|
2914
2968
|
].join("\n");
|
|
2915
2969
|
};
|
|
2970
|
+
// 所有会动宪章的写入 (config_set、人 `/name`、name_chat、自动补名) 共用这一个守卫: 写前写后各渲染一遍
|
|
2971
|
+
// 挂在 pane 上的 wizard 的宪章, 变了的、pane 还活着的挂提醒。快照到写完之间不能有 await —— 别人的写入
|
|
2972
|
+
// 插进来, 前后就对不上了。已经写了: 比对失败只少一句提醒, 不能让调用方以为没写而重发。
|
|
2973
|
+
guardCharter = (write, change) => {
|
|
2974
|
+
const targets = attachedWizards();
|
|
2975
|
+
const before = charterSnapshot(targets);
|
|
2976
|
+
const r = write();
|
|
2977
|
+
const c = change(r);
|
|
2978
|
+
if (!c)
|
|
2979
|
+
return { r, note: Promise.resolve("") };
|
|
2980
|
+
const fail = (e) => {
|
|
2981
|
+
log.warn({ err: errText(e), what: c.what }, "宪章影响比对失败");
|
|
2982
|
+
return `\n(宪章影响没比对出来: ${errText(e)} —— 改的若是进宪章的内容, 受影响的 wizard 要 handoff 才换上)`;
|
|
2983
|
+
};
|
|
2984
|
+
try {
|
|
2985
|
+
const hits = charterImpact(before, (c.after ?? charterSnapshot)(targets));
|
|
2986
|
+
return { r, note: liveOf(hits.map((h) => h.target)).then((live) => announceCharterImpact(c, hits.filter((h) => live.includes(h.target)))).catch(fail) };
|
|
2987
|
+
}
|
|
2988
|
+
catch (e) {
|
|
2989
|
+
return { r, note: Promise.resolve(fail(e)) };
|
|
2990
|
+
}
|
|
2991
|
+
};
|
|
2916
2992
|
// POST /config/get —— 新 MCP 带 `path` (可空) 拿渲染好的文本; 老 MCP 带 `key` 拿 {key, value}。
|
|
2917
2993
|
http.register("POST /config/get", async (req, res) => {
|
|
2918
2994
|
const body = (await readBody(req));
|
|
@@ -2921,7 +2997,10 @@ const main = async () => {
|
|
|
2921
2997
|
json(res, r.ok ? 200 : 400, r);
|
|
2922
2998
|
return;
|
|
2923
2999
|
}
|
|
2924
|
-
|
|
3000
|
+
// chatPolicy.<聊天名>… 换成它指向的 principal 再查 (同 config_set 的 pinChatKeys); 认不出的原样查。
|
|
3001
|
+
const p = parsePath(body.path);
|
|
3002
|
+
const base = p[0] === "chatPolicy" && p[1] ? chatBaseOf(cfg, p[1]) : "";
|
|
3003
|
+
const r = configGet(cfg, sourcePath, base ? ["chatPolicy", base, ...p.slice(2)].join(".") : body.path, configEvidence);
|
|
2925
3004
|
json(res, r.ok ? 200 : 400, r);
|
|
2926
3005
|
});
|
|
2927
3006
|
// chatPolicy 的键一律落成 base principal: 聊天名会改, 拿它当键一改名策略就悄悄失效。写名字照收,
|
|
@@ -2974,26 +3053,21 @@ const main = async () => {
|
|
|
2974
3053
|
return;
|
|
2975
3054
|
}
|
|
2976
3055
|
// 落盘时重算一遍: 等卡期间文件可能被别人改过, 按那一刻的文件打补丁、再校验。
|
|
2977
|
-
//
|
|
3056
|
+
// 宪章快照取在落盘之前 (守卫里): hot 项一 applyHot, 活的 cfg 就已经是新的了。
|
|
2978
3057
|
// 不用 resolveSelf: 它最后回落到 defaultChat, 会把认不出的调用方当成那个群的管家。
|
|
2979
3058
|
const caller = (body.sessionId?.trim() && m.targetForSession(body.sessionId.trim())) || (body.tmuxPane?.trim() && m.targetForPane(body.tmuxPane.trim())) || undefined;
|
|
3059
|
+
// 人发起 = 调用方这一轮的链头是人 (人在群里 / 单聊里叫它改的)。
|
|
3060
|
+
const byHuman = !!caller && !!askerOf(caller)?.who.startsWith("user:");
|
|
2980
3061
|
const commit = async () => {
|
|
2981
|
-
const
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
3062
|
+
const { r: now, note } = guardCharter(() => {
|
|
3063
|
+
const now = planSet(cfg, sourcePath, ask);
|
|
3064
|
+
if (!now.ok)
|
|
3065
|
+
return now;
|
|
3066
|
+
patchJsonc(sourcePath, [{ path: now.jsonPath, value: now.value }]);
|
|
3067
|
+
applyHot(cfg, now);
|
|
2985
3068
|
return now;
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
applyHot(cfg, now);
|
|
2989
|
-
// 已经落盘了: 比对失败只少一句提醒, 不能让调用方以为没写而重发。
|
|
2990
|
-
try {
|
|
2991
|
-
return { ...now, charter: announceCharterImpact(now, charterImpact(before, now), caller) };
|
|
2992
|
-
}
|
|
2993
|
-
catch (e) {
|
|
2994
|
-
log.warn({ err: errText(e), path: now.path.join(".") }, "config_set: 宪章影响比对失败");
|
|
2995
|
-
return { ...now, charter: `\n(宪章影响没比对出来: ${errText(e)} —— 改的若是进宪章的配置, 受影响的 wizard 要 handoff 才换上)` };
|
|
2996
|
-
}
|
|
3069
|
+
}, (now) => now.ok ? { what: `\`config_set ${now.path.join(".")}\``, self: caller, byHuman, reload: now.apply === "reload", after: charterUnder(now) } : undefined);
|
|
3070
|
+
return now.ok ? { ...now, charter: await note } : now;
|
|
2997
3071
|
};
|
|
2998
3072
|
const done = (now) => `已写入${now.apply === "hot" ? ", 已生效" : "; 需 reload (`./cli/wezard.sh reload`) 才生效"}:\n${renderPlan(now)}${now.charter}`;
|
|
2999
3073
|
if (pl.gate !== "card") {
|