wezard 1.4.1 → 2.0.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 +12 -7
- package/README.md +227 -112
- package/cli/wezard.sh +6 -3
- package/dist/daemon/approval.js +9 -3
- package/dist/daemon/approval.js.map +1 -1
- package/dist/daemon/cc-bridge.js.map +1 -1
- package/dist/daemon/chat-name.js +8 -14
- package/dist/daemon/chat-name.js.map +1 -1
- package/dist/daemon/detail.js +94 -9
- package/dist/daemon/detail.js.map +1 -1
- package/dist/daemon/graph.js +5 -7
- package/dist/daemon/graph.js.map +1 -1
- package/dist/daemon/inbound.js +135 -158
- package/dist/daemon/inbound.js.map +1 -1
- package/dist/daemon/index.js +452 -156
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/jobs.js +2 -2
- package/dist/daemon/mirror-bridge.js +284 -140
- package/dist/daemon/mirror-bridge.js.map +1 -1
- package/dist/daemon/mirror-store.js.map +1 -1
- package/dist/daemon/model-select.js +74 -0
- package/dist/daemon/model-select.js.map +1 -0
- package/dist/daemon/peers.js +10 -16
- package/dist/daemon/peers.js.map +1 -1
- package/dist/daemon/spawn-tmux.js +27 -4
- package/dist/daemon/spawn-tmux.js.map +1 -1
- package/dist/daemon/task-gate.js +97 -0
- package/dist/daemon/task-gate.js.map +1 -0
- package/dist/daemon/task-registry.js +132 -0
- package/dist/daemon/task-registry.js.map +1 -0
- package/dist/daemon/tasks.js +126 -54
- package/dist/daemon/tasks.js.map +1 -1
- package/dist/daemon/wizard-memory.js +57 -0
- package/dist/daemon/wizard-memory.js.map +1 -0
- package/dist/daemon/wizard.js +84 -36
- package/dist/daemon/wizard.js.map +1 -1
- package/dist/mcp/server.js +138 -76
- package/dist/mcp/server.js.map +1 -1
- package/dist/shared/chat-http.js +193 -96
- package/dist/shared/chat-http.js.map +1 -1
- package/dist/shared/chat-render.js +31 -24
- package/dist/shared/chat-render.js.map +1 -1
- package/dist/shared/chat-view.js +14 -2
- package/dist/shared/chat-view.js.map +1 -1
- package/dist/shared/config.js +23 -4
- package/dist/shared/config.js.map +1 -1
- package/dist/shared/detail-render.js +14 -7
- package/dist/shared/detail-render.js.map +1 -1
- package/dist/shared/detail-store.js +12 -5
- package/dist/shared/detail-store.js.map +1 -1
- package/dist/shared/role-render.js +52 -0
- package/dist/shared/role-render.js.map +1 -0
- package/dist/shared/role-view.js +210 -0
- package/dist/shared/role-view.js.map +1 -0
- package/dist/shared/schedule-spec.js +92 -116
- package/dist/shared/schedule-spec.js.map +1 -1
- package/dist/shared/session-label.js +103 -72
- package/dist/shared/session-label.js.map +1 -1
- package/dist/shared/std.js +20 -0
- package/dist/shared/std.js.map +1 -1
- package/dist/shared/task-file.js +120 -0
- package/dist/shared/task-file.js.map +1 -0
- package/dist/shared/trigger.js +232 -0
- package/dist/shared/trigger.js.map +1 -0
- package/dist/shared/world.js +193 -0
- package/dist/shared/world.js.map +1 -0
- package/dist/svr/index.js +68 -3
- package/dist/svr/index.js.map +1 -1
- package/package.json +1 -1
- package/web/chat.css +609 -147
- package/web/chat.js +1274 -215
package/dist/shared/std.js
CHANGED
|
@@ -6,4 +6,24 @@ export const truncate = (s, max) => (s.length <= max ? s : `${s.slice(0, max)}
|
|
|
6
6
|
export const truncateWithCount = (s, max) => s.length <= max ? s : `${s.slice(0, max)}…(+${s.length - max})`;
|
|
7
7
|
/** Flatten to a single line first — for text going into a one-line bubble. */
|
|
8
8
|
export const clipLine = (s, max) => truncate(s.replace(/\s+/g, " ").trim(), max);
|
|
9
|
+
/** `Promise.all(xs.map(f))` 但同时在飞的不超过 `limit` 个。
|
|
10
|
+
*
|
|
11
|
+
* 存在的理由是一次真实事故: 一个 `Promise.all` 铺在几百个 target 上, 每个各自
|
|
12
|
+
* spawn 两次 tmux —— 七百个并发子进程瞬间打穿 fd 上限, tmux server 被拖垮,
|
|
13
|
+
* 守护进程随后崩进重启循环 (同 CLAUDE.md 记的 launchd fd 那一条)。凡是 f 会
|
|
14
|
+
* spawn 进程或开文件的, 就不该用裸 Promise.all。
|
|
15
|
+
* 结果顺序与输入一致。 */
|
|
16
|
+
export const mapLimit = async (xs, limit, f) => {
|
|
17
|
+
const out = new Array(xs.length);
|
|
18
|
+
let next = 0;
|
|
19
|
+
const worker = async () => {
|
|
20
|
+
for (let i = next++; i < xs.length; i = next++)
|
|
21
|
+
out[i] = await f(xs[i], i);
|
|
22
|
+
};
|
|
23
|
+
await Promise.all(Array.from({ length: Math.max(1, Math.min(limit, xs.length)) }, worker));
|
|
24
|
+
return out;
|
|
25
|
+
};
|
|
26
|
+
/** Substitute `{{…}}` refs from a context map. Unknown keys are left verbatim —
|
|
27
|
+
* a prompt legitimately containing `{{foo}}` shouldn't be silently emptied. */
|
|
28
|
+
export const interpolate = (template, ctx) => template.replace(/\{\{\s*#?([\p{L}\p{N}_-]+)\s*\}\}/gu, (whole, key) => key in ctx ? ctx[key] : whole);
|
|
9
29
|
//# sourceMappingURL=std.js.map
|
package/dist/shared/std.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"std.js","sourceRoot":"","sources":["../../shared/std.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,EAAU,EAAiB,EAAE,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;AAE1F,4DAA4D;AAC5D,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,CAAS,EAAE,GAAW,EAAU,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;AAE1G,kFAAkF;AAClF,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAS,EAAE,GAAW,EAAU,EAAE,CAClE,CAAC,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,MAAM,GAAG,GAAG,GAAG,CAAC;AAElE,8EAA8E;AAC9E,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,CAAS,EAAE,GAAW,EAAU,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,GAAG,CAAC,CAAC"}
|
|
1
|
+
{"version":3,"file":"std.js","sourceRoot":"","sources":["../../shared/std.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,EAAU,EAAiB,EAAE,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;AAE1F,4DAA4D;AAC5D,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,CAAS,EAAE,GAAW,EAAU,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;AAE1G,kFAAkF;AAClF,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAS,EAAE,GAAW,EAAU,EAAE,CAClE,CAAC,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,MAAM,GAAG,GAAG,GAAG,CAAC;AAElE,8EAA8E;AAC9E,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,CAAS,EAAE,GAAW,EAAU,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,GAAG,CAAC,CAAC;AAEzG;;;;;;iBAMiB;AACjB,MAAM,CAAC,MAAM,QAAQ,GAAG,KAAK,EAC3B,EAAgB,EAChB,KAAa,EACb,CAAkC,EACpB,EAAE;IAChB,MAAM,GAAG,GAAG,IAAI,KAAK,CAAI,EAAE,CAAC,MAAM,CAAC,CAAC;IACpC,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,MAAM,MAAM,GAAG,KAAK,IAAmB,EAAE;QACvC,KAAK,IAAI,CAAC,GAAG,IAAI,EAAE,EAAE,CAAC,GAAG,EAAE,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE;YAAE,GAAG,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAE,EAAE,CAAC,CAAC,CAAC;IAC9E,CAAC,CAAC;IACF,MAAM,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC;IAC3F,OAAO,GAAG,CAAC;AACb,CAAC,CAAC;AAEF;gFACgF;AAChF,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,QAAgB,EAChB,GAAqC,EAC7B,EAAE,CACV,QAAQ,CAAC,OAAO,CAAC,qCAAqC,EAAE,CAAC,KAAK,EAAE,GAAW,EAAE,EAAE,CAC7E,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAE,CAAC,CAAC,CAAC,KAAK,CAC/B,CAAC"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// 一条定时任务 = 一份**可注入代码的配置**: `~/.wezard/tasks/<id>.task.mjs`。
|
|
2
|
+
//
|
|
3
|
+
// 为什么是文件而不是 config 里的一行记录: 定时任务天生是「到点跑一段逻辑」, 而
|
|
4
|
+
// 逻辑写不进 json。旧形状里 wizard 只能通过人话解析器间接碰到「什么时候」, 碰不到
|
|
5
|
+
// 的说法就做不到; 「放枪前先看一眼有没有活」更是完全无处安放, 只能到点先起一个
|
|
6
|
+
// wizard 再让它自己判断要不要干 —— 每一次空转都是一个 pane 加一份上下文。
|
|
7
|
+
//
|
|
8
|
+
// 现在这份文件就是配置本身: wizard 用 Read/Edit 直接改它, 存盘即生效。它导出四件
|
|
9
|
+
// 事, 每一件都可以被单独替换 (Strategy):
|
|
10
|
+
//
|
|
11
|
+
// when 何时放枪 —— 人话, 或 helpers 组合出的规约 (shared/trigger.ts)
|
|
12
|
+
// gate 放枪前的检查 (可选) —— 有副作用、可以跑命令; 返回 false 这一轮就不放
|
|
13
|
+
// prompt 到点说给 wizard 听的话, `{{var}}` 由 gate 填
|
|
14
|
+
// target/fresh 在谁的聊天/目录下办, 是否每次新起一个白板 wizard
|
|
15
|
+
// createdBy 排班的 wizard —— 日程归它 (owner), fresh 执行体挂在它的家谱下
|
|
16
|
+
//
|
|
17
|
+
// 本模块只做纯的那一半: 形状、校验、模板渲染、路径。加载与执行在 daemon/。
|
|
18
|
+
import { basename, join } from "node:path";
|
|
19
|
+
import { describeTrigger, toTrigger } from "./trigger.js";
|
|
20
|
+
export const TASKS_DIR = "~/.wezard/tasks";
|
|
21
|
+
export const TASK_EXT = ".task.mjs";
|
|
22
|
+
const DEFAULT_GATE_TIMEOUT = 120;
|
|
23
|
+
export const taskIdOfFile = (file) => basename(file).replace(/\.task\.mjs$/, "");
|
|
24
|
+
export const fileOfTaskId = (dir, id) => join(dir, `${id}${TASK_EXT}`);
|
|
25
|
+
/** 文件名即 id, 所以它必须既好认又能当路径。 */
|
|
26
|
+
export const slugify = (raw, fallback) => {
|
|
27
|
+
const s = raw.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").slice(0, 24).replace(/^-+|-+$/g, "");
|
|
28
|
+
return s || fallback;
|
|
29
|
+
};
|
|
30
|
+
export const uniqueId = (want, taken) => taken.has(want) ? [...Array(99).keys()].map((i) => `${want}-${i + 2}`).find((c) => !taken.has(c)) ?? `${want}-${Date.now() % 1000}` : want;
|
|
31
|
+
/** 模块 → 记录。认不出的 when / 空 prompt 在这里被挡住, 连带一句人话的理由。 */
|
|
32
|
+
export const normalizeTask = (mod, id, file, now = new Date()) => {
|
|
33
|
+
if (!mod || typeof mod !== "object")
|
|
34
|
+
return { ok: false, reason: "任务文件要 `export default { when, prompt }`" };
|
|
35
|
+
const m = mod;
|
|
36
|
+
const prompt = (m.prompt ?? "").toString().trim();
|
|
37
|
+
if (!prompt)
|
|
38
|
+
return { ok: false, reason: "prompt 不能为空" };
|
|
39
|
+
const tr = toTrigger(m.when, now);
|
|
40
|
+
if (!tr.ok)
|
|
41
|
+
return { ok: false, reason: tr.reason };
|
|
42
|
+
return {
|
|
43
|
+
ok: true,
|
|
44
|
+
task: {
|
|
45
|
+
id,
|
|
46
|
+
file,
|
|
47
|
+
trigger: tr.trigger,
|
|
48
|
+
when: describeTrigger(tr.trigger),
|
|
49
|
+
prompt,
|
|
50
|
+
note: (m.note ?? "").toString(),
|
|
51
|
+
target: (m.target ?? "").toString(),
|
|
52
|
+
fresh: m.fresh !== false,
|
|
53
|
+
enabled: m.enabled !== false,
|
|
54
|
+
hasGate: typeof m.gate === "function",
|
|
55
|
+
gateTimeoutSec: Math.max(5, Math.min(900, m.gateTimeoutSec ?? DEFAULT_GATE_TIMEOUT)),
|
|
56
|
+
createdBy: (m.createdBy ?? "").toString(),
|
|
57
|
+
owner: (m.createdBy ?? "").toString() || (m.target ?? "").toString(),
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
// ── 生成 ────────────────────────────────────────────────────────────
|
|
62
|
+
const js = (s) => JSON.stringify(s);
|
|
63
|
+
/** 人话 → 源码里那一行 `when`。认出来是什么就写什么, 别把原话塞进去 ——
|
|
64
|
+
* 「20 分钟后」在下一次 reload 时会重新往后数 20 分钟。 */
|
|
65
|
+
const renderWhen = (tr) => {
|
|
66
|
+
const guards = tr.guards.map((g) => g.kind === "between" ? `between("${String(g.from.hour).padStart(2, "0")}:${String(g.from.minute).padStart(2, "0")}", "${String(g.to.hour).padStart(2, "0")}:${String(g.to.minute).padStart(2, "0")}")`
|
|
67
|
+
: g.kind === "onDays" ? `onDays(${JSON.stringify(g.days)})`
|
|
68
|
+
: `not(/* ${g.kind} */)`);
|
|
69
|
+
const c = tr.clock;
|
|
70
|
+
const clock = c.kind === "every" ? `every(${c.minutes})`
|
|
71
|
+
: c.kind === "once" ? `at(${c.at})`
|
|
72
|
+
: `daily(${c.times.map((t) => `"${String(t.hour).padStart(2, "0")}:${String(t.minute).padStart(2, "0")}"`).join(", ")})`;
|
|
73
|
+
const helpers = [
|
|
74
|
+
...(guards.length ? ["and"] : []),
|
|
75
|
+
...(c.kind === "every" ? ["every"] : c.kind === "once" ? ["at"] : ["daily"]),
|
|
76
|
+
...new Set(tr.guards.map((g) => (g.kind === "between" ? "between" : g.kind === "onDays" ? "onDays" : "not"))),
|
|
77
|
+
];
|
|
78
|
+
const body = guards.length ? `and(${clock}, ${guards.join(", ")})` : clock;
|
|
79
|
+
return `({ ${helpers.join(", ")} }) => ${body}`;
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* 新任务的源码。头上那段注释是 wizard 唯一的说明书 —— 它 Read 这个文件时看到的
|
|
83
|
+
* 就是这些, 所以写全: 能改什么、gate 拿得到什么、改完要不要重启 (不要)。
|
|
84
|
+
*/
|
|
85
|
+
export const renderTaskFile = (d) => `// wezard 定时任务 · ${d.id}
|
|
86
|
+
// 这份文件**就是配置**: 直接改, 存盘即生效 (守护进程盯着这个目录, 不用 reload)。
|
|
87
|
+
//
|
|
88
|
+
// when 何时放枪。人话 ("每个工作日晚上9:30") 或组合:
|
|
89
|
+
// every("1h") | daily("09:00","18:00") | at(<epoch ms>) ← 产生时刻
|
|
90
|
+
// between("08:00","20:00") | onDays("工作日") | not(…) ← 只做筛选
|
|
91
|
+
// and(every("1h"), between("08:00","20:00")) ← 时钟 ∧ 筛子
|
|
92
|
+
// gate 放枪前的检查, 可选。**没拉到活就别惊动 wizard** 用它:
|
|
93
|
+
// 返回 false → 这一轮不放枪 (不起 wizard, 群里也不出声)
|
|
94
|
+
// 返回 { vars } → 放枪, vars 填进 prompt 里的 {{名字}}
|
|
95
|
+
// 返回 { state } → 存下来, 下一轮从 ctx.state 拿得到
|
|
96
|
+
// ctx = { now, task:{id,target,cwd}, sh(cmd), state, log }
|
|
97
|
+
// sh 的 cwd 默认是目标 wizard 的工作区; 抛错等同于返回 false。
|
|
98
|
+
// prompt 到点说给 wizard 听的话。写成零上下文也能执行的完整指令 —— 到点接活的
|
|
99
|
+
// 多半是个刚出生的白板 wizard, 它只看得见这一句。
|
|
100
|
+
//
|
|
101
|
+
// 停掉这条任务: enabled: false (留档), 或直接删掉本文件。
|
|
102
|
+
export default {
|
|
103
|
+
note: ${js(d.note)},
|
|
104
|
+
target: ${js(d.target)},
|
|
105
|
+
fresh: ${d.fresh},
|
|
106
|
+
enabled: true,
|
|
107
|
+
createdBy: ${js(d.createdBy)},
|
|
108
|
+
|
|
109
|
+
when: ${renderWhen(d.trigger)},
|
|
110
|
+
|
|
111
|
+
// gate: async ({ sh, state, log }) => {
|
|
112
|
+
// const out = await sh("git fetch -q && git log --oneline HEAD..@{u}");
|
|
113
|
+
// if (!out.trim()) return false; // 没新东西 → 这一轮不放枪
|
|
114
|
+
// return { vars: { commits: out.trim() } }; // 有 → prompt 里的 {{commits}}
|
|
115
|
+
// },
|
|
116
|
+
|
|
117
|
+
prompt: ${JSON.stringify(d.prompt)},
|
|
118
|
+
};
|
|
119
|
+
`;
|
|
120
|
+
//# sourceMappingURL=task-file.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"task-file.js","sourceRoot":"","sources":["../../shared/task-file.ts"],"names":[],"mappings":"AAAA,4DAA4D;AAC5D,EAAE;AACF,+CAA+C;AAC/C,kDAAkD;AAClD,2CAA2C;AAC3C,+CAA+C;AAC/C,EAAE;AACF,qDAAqD;AACrD,6BAA6B;AAC7B,EAAE;AACF,6DAA6D;AAC7D,uDAAuD;AACvD,gDAAgD;AAChD,gDAAgD;AAChD,+DAA+D;AAC/D,EAAE;AACF,4CAA4C;AAC5C,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAE3C,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE1D,MAAM,CAAC,MAAM,SAAS,GAAG,iBAAiB,CAAC;AAC3C,MAAM,CAAC,MAAM,QAAQ,GAAG,WAAW,CAAC;AAkEpC,MAAM,oBAAoB,GAAG,GAAG,CAAC;AAEjC,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,IAAY,EAAU,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC;AACjG,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,GAAW,EAAE,EAAU,EAAU,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,QAAQ,EAAE,CAAC,CAAC;AAE/F,8BAA8B;AAC9B,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,GAAW,EAAE,QAAgB,EAAU,EAAE;IAC/D,MAAM,CAAC,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;IAClG,OAAO,CAAC,IAAI,QAAQ,CAAC;AACvB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,IAAY,EAAE,KAA0B,EAAU,EAAE,CAC3E,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,IAAI,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAE7I,sDAAsD;AACtD,MAAM,CAAC,MAAM,aAAa,GAAG,CAC3B,GAAY,EACZ,EAAU,EACV,IAAY,EACZ,MAAY,IAAI,IAAI,EAAE,EAC0C,EAAE;IAClE,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,yCAAyC,EAAE,CAAC;IAC7G,MAAM,CAAC,GAAG,GAAiB,CAAC;IAC5B,MAAM,MAAM,GAAG,CAAC,CAAC,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC;IAClD,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC;IACzD,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAClC,IAAI,CAAC,EAAE,CAAC,EAAE;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC;IACpD,OAAO;QACL,EAAE,EAAE,IAAI;QACR,IAAI,EAAE;YACJ,EAAE;YACF,IAAI;YACJ,OAAO,EAAE,EAAE,CAAC,OAAO;YACnB,IAAI,EAAE,eAAe,CAAC,EAAE,CAAC,OAAO,CAAC;YACjC,MAAM;YACN,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,QAAQ,EAAE;YAC/B,MAAM,EAAE,CAAC,CAAC,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,QAAQ,EAAE;YACnC,KAAK,EAAE,CAAC,CAAC,KAAK,KAAK,KAAK;YACxB,OAAO,EAAE,CAAC,CAAC,OAAO,KAAK,KAAK;YAC5B,OAAO,EAAE,OAAO,CAAC,CAAC,IAAI,KAAK,UAAU;YACrC,cAAc,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,cAAc,IAAI,oBAAoB,CAAC,CAAC;YACpF,SAAS,EAAE,CAAC,CAAC,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,QAAQ,EAAE;YACzC,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,QAAQ,EAAE;SACrE;KACF,CAAC;AACJ,CAAC,CAAC;AAEF,qEAAqE;AACrE,MAAM,EAAE,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;AAEpD;yCACyC;AACzC,MAAM,UAAU,GAAG,CAAC,EAAW,EAAU,EAAE;IACzC,MAAM,MAAM,GAAG,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CACjC,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,YAAY,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,OAAO,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI;QACpM,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,UAAU,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG;YACzD,CAAC,CAAC,UAAU,CAAC,CAAC,IAAI,MAAM,CAC7B,CAAC;IACF,MAAM,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC;IACnB,MAAM,KAAK,GAAG,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,GAAG;QACtD,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,GAAG;YACjC,CAAC,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;IAC7H,MAAM,OAAO,GAAG;QACd,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACjC,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;QAC5E,GAAG,IAAI,GAAG,CAAC,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;KAC9G,CAAC;IACF,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,KAAK,KAAK,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC;IAC3E,OAAO,MAAM,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,IAAI,EAAE,CAAC;AAClD,CAAC,CAAC;AAYF;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAY,EAAU,EAAE,CAAC,oBAAoB,CAAC,CAAC,EAAE;;;;;;;;;;;;;;;;;;UAkBtE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;YACR,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC;WACb,CAAC,CAAC,KAAK;;eAEH,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;;UAEpB,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC;;;;;;;;YAQnB,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC;;CAEnC,CAAC"}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// 触发规约 (Specification) —— 「什么时候放枪」从一个三选一的枚举, 改成一棵可组合、
|
|
2
|
+
// 可序列化、可回显的小 AST。
|
|
3
|
+
//
|
|
4
|
+
// 改这一刀的理由: 枚举是封闭的, 而人的说法是开放的。「白天每隔一个小时」在旧形状
|
|
5
|
+
// 里无处安放 —— 它是两件事的**交集**: 一个产生时刻的时钟 (每小时), 和一个只会
|
|
6
|
+
// 否决的筛子 (08:00–20:00)。把这两类分开, 组合就够用了, 不必为每种新说法再加一个
|
|
7
|
+
// kind:
|
|
8
|
+
//
|
|
9
|
+
// Clock 能自己产生候选时刻, 有「下次是什么时候」 every / daily / once
|
|
10
|
+
// Guard 产生不了任何一枪, 只否决落在外面的 between / onDays / not
|
|
11
|
+
// Trigger = 一个 Clock ∧ 若干 Guard
|
|
12
|
+
//
|
|
13
|
+
// AST 而不是闭包: 闭包没法回显给人看 (「每 1 小时 · 08:00-20:00」)、没法存进 json、
|
|
14
|
+
// 没法在详情页画出来。构造器 (every/daily/and/…) 只是这棵树的字面量工厂 —— LLM
|
|
15
|
+
// 写 `and(every("1h"), between("08:00","20:00"))` 与手写对象字面量等价。
|
|
16
|
+
import { daysLabel, daysOf, durationLabel, hhmm, parseDays, parseDuration, parseInterval, parseOnceAt, parseTimeOfDay, parseWindow, RANGE_RE, } from "./schedule-spec.js";
|
|
17
|
+
// 守护进程重启/维护会吃掉整分钟的 tick。错过的那一下在 5 分钟内补跑, 超出就放弃 ——
|
|
18
|
+
// 早上 9 点的任务在中午补跑比不跑更糟。
|
|
19
|
+
const CATCHUP_MS = 5 * 60_000;
|
|
20
|
+
const minutesOfDay = (t) => t.hour * 60 + t.minute;
|
|
21
|
+
const asHM = (v) => {
|
|
22
|
+
if (typeof v !== "string")
|
|
23
|
+
return v;
|
|
24
|
+
const t = parseTimeOfDay(v);
|
|
25
|
+
if (!t)
|
|
26
|
+
throw new Error(`认不出时刻「${v}」, 写成 "09:30" / "晚上9点半" 这样`);
|
|
27
|
+
return t;
|
|
28
|
+
};
|
|
29
|
+
// ── 构造器 (任务文件里的 helpers) ──────────────────────────────────
|
|
30
|
+
/** `every("1h")` / `every(90)` / `every("每隔两个小时")` —— 每隔多久一枪。 */
|
|
31
|
+
export const every = (spec) => {
|
|
32
|
+
const n = typeof spec === "string" ? (parseDuration(spec) ?? parseInterval(spec)) : parseDuration(spec);
|
|
33
|
+
if (!n)
|
|
34
|
+
throw new Error(`认不出间隔「${spec}」, 写成 "1h" / "30m" / "两小时"`);
|
|
35
|
+
return { clock: { kind: "every", minutes: Math.max(1, Math.round(n)) }, guards: [] };
|
|
36
|
+
};
|
|
37
|
+
/** `daily("09:30")` / `daily("09:00", "18:00")` —— 每天的这几个时刻。 */
|
|
38
|
+
export const daily = (...times) => ({
|
|
39
|
+
clock: { kind: "daily", times: times.length ? times.map(asHM) : [{ hour: 9, minute: 0 }] },
|
|
40
|
+
guards: [],
|
|
41
|
+
});
|
|
42
|
+
/** `at("2026-10-01 09:00")` / `at(Date)` —— 只响一次。 */
|
|
43
|
+
export const at = (when) => {
|
|
44
|
+
const ms = when instanceof Date ? when.getTime()
|
|
45
|
+
: typeof when === "number" ? when
|
|
46
|
+
: (parseOnceAt(when, new Date()) ?? Date.parse(when));
|
|
47
|
+
if (!Number.isFinite(ms))
|
|
48
|
+
throw new Error(`认不出时刻「${String(when)}」`);
|
|
49
|
+
return { clock: { kind: "once", at: ms }, guards: [] };
|
|
50
|
+
};
|
|
51
|
+
/** `between("08:00","20:00")` —— 只在这段时间内放行。from > to 视为跨午夜。 */
|
|
52
|
+
export const between = (from, to) => ({ kind: "between", from: asHM(from), to: asHM(to) });
|
|
53
|
+
/** `onDays("工作日")` / `onDays([1,3,5])` —— 只在这几天放行。 */
|
|
54
|
+
export const onDays = (spec) => ({ kind: "onDays", days: daysOf(spec) });
|
|
55
|
+
export const not = (of) => ({ kind: "not", of });
|
|
56
|
+
/** `and(every("1h"), between("08:00","20:00"))` —— 一个时钟配若干筛子。 */
|
|
57
|
+
export const and = (base, ...guards) => ({ clock: base.clock, guards: [...base.guards, ...guards] });
|
|
58
|
+
/** 任务文件的 `when` 拿到的就是这一组; 传进去而不是让它 import, 是因为任务文件
|
|
59
|
+
* 住在 ~/.wezard/tasks/ 下, 那里没有也不该有到 dist 的相对路径。 */
|
|
60
|
+
export const TRIGGER_HELPERS = { every, daily, at, between, onDays, not, and };
|
|
61
|
+
// ── 人话 → 规约 ────────────────────────────────────────────────────
|
|
62
|
+
/**
|
|
63
|
+
* 整句话进, 规约出。认不出返回 undefined —— 调用方要把原句回给用户让他重说,
|
|
64
|
+
* 绝不能猜一个时间安静地存下去。
|
|
65
|
+
*
|
|
66
|
+
* 顺序有意为之: 一次性 (带「今天/明天/N 分钟后」的措辞) 最具体, 其次是间隔,
|
|
67
|
+
* 最后才是周期日程 —— 「每天」这种词在前两者里都不会出现。窗口与星期是筛子,
|
|
68
|
+
* 无论走哪条时钟分支都往上挂。
|
|
69
|
+
*/
|
|
70
|
+
export const parseTrigger = (text, now = new Date()) => {
|
|
71
|
+
const t = text.trim();
|
|
72
|
+
if (!t)
|
|
73
|
+
return undefined;
|
|
74
|
+
const once = parseOnceAt(t, now);
|
|
75
|
+
if (once !== undefined)
|
|
76
|
+
return { clock: { kind: "once", at: once }, guards: [] };
|
|
77
|
+
const w = parseWindow(t);
|
|
78
|
+
// 「8点到20点每小时」: 区间已被窗口吃掉, 再去 parseTimeOfDay 会把 8 点当成日程
|
|
79
|
+
// 时刻。显式区间从句子里剔掉后再找时刻。
|
|
80
|
+
const tod = parseTimeOfDay(w?.explicit ? t.replace(RANGE_RE, " ") : t, t);
|
|
81
|
+
// 软窗口 (「晚上」) 碰上具体时刻就让位 —— 那个词说的是这一刻, 不是一段。
|
|
82
|
+
const win = w && (!w.soft || !tod) ? w : undefined;
|
|
83
|
+
const days = parseDays(t);
|
|
84
|
+
const iv = parseInterval(t);
|
|
85
|
+
const guards = [
|
|
86
|
+
...(win ? [between(win.from, win.to)] : []),
|
|
87
|
+
...(days && days.length ? [onDays(days)] : []),
|
|
88
|
+
];
|
|
89
|
+
// 「每 2 天早上 9 点」这种既给间隔又给时刻的说法, 按日程理解更接近本意;
|
|
90
|
+
// 纯间隔 (不带时刻) 才走 every。
|
|
91
|
+
if (iv !== undefined && !tod)
|
|
92
|
+
return { clock: { kind: "every", minutes: Math.max(1, Math.round(iv)) }, guards };
|
|
93
|
+
if (tod)
|
|
94
|
+
return { clock: { kind: "daily", times: [tod] }, guards };
|
|
95
|
+
if (days)
|
|
96
|
+
return { clock: { kind: "daily", times: [{ hour: 9, minute: 0 }] }, guards }; // 只说了「每个工作日」
|
|
97
|
+
if (iv !== undefined)
|
|
98
|
+
return { clock: { kind: "every", minutes: Math.max(1, Math.round(iv)) }, guards };
|
|
99
|
+
return undefined;
|
|
100
|
+
};
|
|
101
|
+
/** 1.5.x 之前的三选一 `when`。老 config 里的定时靠它升格, 否则静默消失。 */
|
|
102
|
+
export const triggerOfLegacyWhen = (w) => {
|
|
103
|
+
if (w.kind === "every")
|
|
104
|
+
return { clock: { kind: "every", minutes: w.minutes ?? 60 }, guards: [] };
|
|
105
|
+
if (w.kind === "once")
|
|
106
|
+
return { clock: { kind: "once", at: w.at ?? 0 }, guards: [] };
|
|
107
|
+
return {
|
|
108
|
+
clock: { kind: "daily", times: [{ hour: w.hour ?? 9, minute: w.minute ?? 0 }] },
|
|
109
|
+
guards: w.days?.length ? [onDays(w.days)] : [],
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
/** 任务文件的 `when` 可以写成人话、helpers 表达式或直接一棵树 —— 都收敛到这里。 */
|
|
113
|
+
export const toTrigger = (when, now = new Date()) => {
|
|
114
|
+
try {
|
|
115
|
+
if (typeof when === "function") {
|
|
116
|
+
const r = when(TRIGGER_HELPERS);
|
|
117
|
+
return toTrigger(r, now);
|
|
118
|
+
}
|
|
119
|
+
if (typeof when === "string") {
|
|
120
|
+
const t = parseTrigger(when, now);
|
|
121
|
+
return t ? { ok: true, trigger: t } : { ok: false, reason: WHEN_HELP(when) };
|
|
122
|
+
}
|
|
123
|
+
if (when && typeof when === "object" && "clock" in when) {
|
|
124
|
+
const tr = when;
|
|
125
|
+
return { ok: true, trigger: { clock: tr.clock, guards: tr.guards ?? [] } };
|
|
126
|
+
}
|
|
127
|
+
return { ok: false, reason: `when 认不出: ${JSON.stringify(when)?.slice(0, 120) ?? typeof when}` };
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
return { ok: false, reason: e.message };
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
export const WHEN_HELP = (raw) => `无法理解「${raw}」。能认的说法: 每天/每个工作日/每周三 + 时刻 (晚上9:30 / 21:30 / 九点半), ` +
|
|
134
|
+
`每隔 N 分钟|小时 (每小时 / 每隔两个小时), N 分钟后, 明早 9 点; ` +
|
|
135
|
+
`再叠时间窗口 (白天 / 工作时间 / 8点到20点)。也可以在任务文件里直接写 ` +
|
|
136
|
+
`\`when: ({every, between, and}) => and(every("1h"), between("08:00","20:00"))\`。`;
|
|
137
|
+
// ── 判定 ────────────────────────────────────────────────────────────
|
|
138
|
+
/** 此刻落在这个筛子里吗。`between` 的 from > to 表示跨午夜 (「夜里」)。 */
|
|
139
|
+
export const holds = (g, now) => {
|
|
140
|
+
switch (g.kind) {
|
|
141
|
+
case "onDays":
|
|
142
|
+
return !g.days.length || g.days.includes(now.getDay());
|
|
143
|
+
case "not":
|
|
144
|
+
return !holds(g.of, now);
|
|
145
|
+
case "between": {
|
|
146
|
+
const m = now.getHours() * 60 + now.getMinutes();
|
|
147
|
+
const a = minutesOfDay(g.from);
|
|
148
|
+
const b = minutesOfDay(g.to);
|
|
149
|
+
return a <= b ? m >= a && m < b : m >= a || m < b;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
const dailyInstantsToday = (times, now) => times.map((t) => {
|
|
154
|
+
const d = new Date(now);
|
|
155
|
+
d.setHours(t.hour, t.minute, 0, 0);
|
|
156
|
+
return d.getTime();
|
|
157
|
+
});
|
|
158
|
+
const clockDue = (c, now, since) => {
|
|
159
|
+
const t = now.getTime();
|
|
160
|
+
switch (c.kind) {
|
|
161
|
+
case "once":
|
|
162
|
+
return t >= c.at && since < c.at;
|
|
163
|
+
case "every":
|
|
164
|
+
return t - since >= c.minutes * 60_000;
|
|
165
|
+
case "daily":
|
|
166
|
+
return dailyInstantsToday(c.times, now).some((inst) => t >= inst && t - inst < CATCHUP_MS && since < inst);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
/**
|
|
170
|
+
* 到点了吗。`since` = 上次触发时刻, 从没触发过就传创建时刻 —— 它同时承担三件事:
|
|
171
|
+
* 同一分钟内多次 tick 的去重、重启后不重复触发、以及「刚创建就到点」不立即打一枪。
|
|
172
|
+
*
|
|
173
|
+
* 筛子放在时钟之后判: 窗外的那一枪是**被吞掉**而不是被推迟 —— 「白天每小时」在
|
|
174
|
+
* 夜里不补跑, 早上 8 点重新开始。
|
|
175
|
+
*/
|
|
176
|
+
export const isDue = (tr, now, since) => clockDue(tr.clock, now, since) && tr.guards.every((g) => holds(g, now));
|
|
177
|
+
// 筛子恒假时 (「每周三」配「周末」) 枚举会走到天荒地老 —— 给个上限, 到顶返回
|
|
178
|
+
// 最后一个候选。回显里那个"下次"不准好过让详情页卡死。
|
|
179
|
+
const MAX_PROBE = 512;
|
|
180
|
+
function* candidates(c, since, now) {
|
|
181
|
+
const t = now.getTime();
|
|
182
|
+
if (c.kind === "once") {
|
|
183
|
+
yield c.at;
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (c.kind === "every") {
|
|
187
|
+
const step = c.minutes * 60_000;
|
|
188
|
+
let next = Math.max(since + step, t);
|
|
189
|
+
for (let i = 0; i < MAX_PROBE; i++) {
|
|
190
|
+
yield next;
|
|
191
|
+
next += step;
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
for (let d = 0; d < MAX_PROBE / 8; d++) {
|
|
196
|
+
const day = new Date(now.getFullYear(), now.getMonth(), now.getDate() + d);
|
|
197
|
+
for (const inst of dailyInstantsToday(c.times, day).sort((a, b) => a - b))
|
|
198
|
+
if (inst > t)
|
|
199
|
+
yield inst;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/** 下一次该在什么时候响 —— 时钟给候选, 筛子挑第一个过的。`since` 同 isDue。 */
|
|
203
|
+
export const nextFire = (tr, since, now = new Date()) => {
|
|
204
|
+
let last = now.getTime();
|
|
205
|
+
for (const c of candidates(tr.clock, since, now)) {
|
|
206
|
+
last = c;
|
|
207
|
+
if (tr.guards.every((g) => holds(g, new Date(c))))
|
|
208
|
+
return c;
|
|
209
|
+
}
|
|
210
|
+
return last;
|
|
211
|
+
};
|
|
212
|
+
// ── 回显 ────────────────────────────────────────────────────────────
|
|
213
|
+
const guardLabel = (g) => {
|
|
214
|
+
switch (g.kind) {
|
|
215
|
+
case "between": return `${hhmm(g.from)}-${hhmm(g.to)}`;
|
|
216
|
+
case "onDays": return daysLabel(g.days);
|
|
217
|
+
case "not": return `非(${guardLabel(g.of)})`;
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
const clockLabel = (c) => {
|
|
221
|
+
switch (c.kind) {
|
|
222
|
+
case "every": return `每 ${durationLabel(c.minutes)}`;
|
|
223
|
+
case "daily": return c.times.map(hhmm).join("、");
|
|
224
|
+
case "once": {
|
|
225
|
+
const d = new Date(c.at);
|
|
226
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")} ${hhmm({ hour: d.getHours(), minute: d.getMinutes() })}(仅一次)`;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
/** 人能一眼确认的回显 —— 存下去之前必须把它回给用户。 */
|
|
231
|
+
export const describeTrigger = (tr) => [clockLabel(tr.clock), ...tr.guards.map(guardLabel)].join(" · ");
|
|
232
|
+
//# sourceMappingURL=trigger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trigger.js","sourceRoot":"","sources":["../../shared/trigger.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// 世界视图 —— 把「谁存在、谁是谁生的、谁在驱动谁、谁被排了什么」折成一张图。
|
|
2
|
+
//
|
|
3
|
+
// chat-view 回答的是**一个聊天内部**的问题 (这个 chat 有哪些 `#tag`、某一路的
|
|
4
|
+
// 线程长什么样)。但 wizard 早就不止活在一个聊天里了: 分身可以生在别的群、
|
|
5
|
+
// send_peer 跨群寻址、一个工单的成员散在几个聊天、定时任务指向任意一个 target。
|
|
6
|
+
// 那些关系在 chat-view 里一条都画不出来 —— 它的第一步就是 `baseOfKey === base`。
|
|
7
|
+
//
|
|
8
|
+
// 本模块因此换一个折叠轴: 不按聊天切, 按**关系**切。
|
|
9
|
+
//
|
|
10
|
+
// 节点 = wizard (会话 + 身份), 按所属聊天聚簇
|
|
11
|
+
// 边 = clone (家谱) / peer (派活) / graph (流水线一步); 定时是节点上的计数
|
|
12
|
+
//
|
|
13
|
+
// 两类数据源, 边界明确:
|
|
14
|
+
// • 观测到的 (turn 记录): peer / graph 边与定时计数 —— 真发生过的往来。24h TTL,
|
|
15
|
+
// 标准 detail 存储, 所以独立 svr 收到 POST 之后画出来的图与本机一模一样。
|
|
16
|
+
// • 登记的 (wizard / job / schedule 注册表): 身份、家谱、工单、日程 —— 它们是
|
|
17
|
+
// 「安排」, 不随 TTL 消失, 但只有 daemon 有。svr 拿不到就退化成只画观测边。
|
|
18
|
+
//
|
|
19
|
+
// 纯函数, 无 IO: 注册表侧的东西由调用方以 WorldFacts 喂进来。
|
|
20
|
+
import { baseOfKey, labelFor, tagOfKey } from "./session-label.js";
|
|
21
|
+
import { isTurn, staleAt, isGhostTurn } from "./chat-view.js";
|
|
22
|
+
export const EMPTY_FACTS = { wizards: [], jobs: [], schedules: [], chatNames: {} };
|
|
23
|
+
// ── 相关性 ────────────────────────────────────────────────────────────
|
|
24
|
+
// 一台跑了几个月的机器上, "所有绑过的会话"是几百个 —— 它们绝大多数是几周前
|
|
25
|
+
// 干完一件事就再没动过的 `#tag`。把它们全画出来不是"完整", 是把图变成一堵墙:
|
|
26
|
+
// 真正在协作的那十几个 wizard 淹没在里面, 而那恰恰是这张图存在的理由。
|
|
27
|
+
//
|
|
28
|
+
// 所以画的是**当下的协作网络**, 判据四条, 满足其一即入选:
|
|
29
|
+
// 1. 在当前聊天里 —— 这一页的主题就是它, 不筛
|
|
30
|
+
// 2. 是某条边的端点 —— 关系是图的主语, 有关系就必须在场 (哪怕它已经凉了)
|
|
31
|
+
// 3. 还活着 / 正在跑 —— 此刻能叫得动的人
|
|
32
|
+
// 4. 最近动过 —— 刚停下的那些, 人还记得
|
|
33
|
+
// 其余的按聊天计数收进 `hidden`, 不是删掉, 是折起来。
|
|
34
|
+
const RECENT_MS = 6 * 3600_000;
|
|
35
|
+
/** 每个聊天最多画这么多 —— 相关性之上再加一道闸: 当前聊天本身就可能有几十个
|
|
36
|
+
* `#tag`, 全列出来同样读不动。按最近活动截断, 余下的进 `hidden`。 */
|
|
37
|
+
const PER_CHAT_MAX = 14;
|
|
38
|
+
const SELF_CHAT_MAX = 24;
|
|
39
|
+
// ── 折叠 ──────────────────────────────────────────────────────────────
|
|
40
|
+
const liveTurns = (records, now) => records.filter(isTurn).filter((r) => !isGhostTurn(r, now) && !r.agent && !!r.target);
|
|
41
|
+
const stripMd = (s) => s.replace(/[`*_~|#>]/g, "").replace(/\s+/g, " ").trim();
|
|
42
|
+
const previewOf = (r) => {
|
|
43
|
+
const texts = r.items.filter((it) => it.t === "text");
|
|
44
|
+
return stripMd(texts[texts.length - 1]?.body ?? r.userQuery ?? "").slice(0, 100);
|
|
45
|
+
};
|
|
46
|
+
/** 一条边的身份 —— 同一对端点同一种类只留一条, count 累加。 */
|
|
47
|
+
const edgeKey = (kind, from, to) => `${kind}\u0000${from}\u0000${to}`;
|
|
48
|
+
const bumpEdge = (m, kind, from, to, ts, tag) => {
|
|
49
|
+
if (!from || !to || from === to)
|
|
50
|
+
return m;
|
|
51
|
+
const k = edgeKey(kind, from, to);
|
|
52
|
+
const cur = m.get(k);
|
|
53
|
+
const merge = (list, v) => v ? [...new Set([...(list ?? []), v])] : list;
|
|
54
|
+
return m.set(k, {
|
|
55
|
+
kind,
|
|
56
|
+
from,
|
|
57
|
+
to,
|
|
58
|
+
count: (cur?.count ?? 0) + 1,
|
|
59
|
+
lastTs: Math.max(cur?.lastTs ?? 0, ts),
|
|
60
|
+
cross: baseOfKey(from) !== baseOfKey(to),
|
|
61
|
+
jobs: merge(cur?.jobs, tag?.jobs),
|
|
62
|
+
runs: merge(cur?.runs, tag?.runs),
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
/** 家谱序: 先根后分身, depth = 到根的距离。环路 (记录可手改) 在 seen 处截断。 */
|
|
66
|
+
const lineageOrder = (targets, parentOf) => {
|
|
67
|
+
const inSet = new Set(targets);
|
|
68
|
+
const kids = targets.reduce((m, t) => {
|
|
69
|
+
const p = parentOf(t);
|
|
70
|
+
return p && inSet.has(p) ? m.set(p, [...(m.get(p) ?? []), t]) : m;
|
|
71
|
+
}, new Map());
|
|
72
|
+
const roots = targets.filter((t) => { const p = parentOf(t); return !p || !inSet.has(p); });
|
|
73
|
+
const walk = (t, depth, seen) => seen.has(t)
|
|
74
|
+
? []
|
|
75
|
+
: [{ target: t, depth }, ...(kids.get(t) ?? []).flatMap((k) => walk(k, depth + 1, new Set([...seen, t])))];
|
|
76
|
+
const ordered = roots.flatMap((r) => walk(r, 0, new Set()));
|
|
77
|
+
// 环里的节点一个根都走不到 —— 补在后面, 宁可扁平也不能让它整段消失。
|
|
78
|
+
const seen = new Set(ordered.map((o) => o.target));
|
|
79
|
+
return [...ordered, ...targets.filter((t) => !seen.has(t)).map((target) => ({ target, depth: 0 }))];
|
|
80
|
+
};
|
|
81
|
+
export const buildWorld = (records, facts, scope, now) => {
|
|
82
|
+
const turns = liveTurns(records, now);
|
|
83
|
+
const byTarget = turns.reduce((m, r) => m.set(r.target, [...(m.get(r.target) ?? []), r]), new Map());
|
|
84
|
+
const factOf = new Map(facts.wizards.map((w) => [w.target, w]));
|
|
85
|
+
// 节点集 = 登记过的 ∪ 跑过轮次的。两边都要: 一个刚 spawn 还没开口的分身只在
|
|
86
|
+
// 注册表里, 一条 24h 内跑过但注册表已被清掉的会话只在 turn 记录里。
|
|
87
|
+
const targets = [...new Set([...factOf.keys(), ...byTarget.keys()])];
|
|
88
|
+
const nodes = targets.map((target) => {
|
|
89
|
+
const f = factOf.get(target);
|
|
90
|
+
const rs = (byTarget.get(target) ?? []).sort((a, b) => a.createdAt - b.createdAt);
|
|
91
|
+
const last = rs[rs.length - 1];
|
|
92
|
+
const tag = tagOfKey(target);
|
|
93
|
+
const base = baseOfKey(target);
|
|
94
|
+
const chat = f?.chat ?? facts.chatNames[base] ?? "";
|
|
95
|
+
const until = rs.reduce((mx, r) => Math.max(mx, staleAt(r)), 0);
|
|
96
|
+
// 名字全局唯一, 与聊天无关 —— 名册里的就是它; 名册缺席 (svr) 才按 slot / 聊天名推。
|
|
97
|
+
const name = (f?.name ?? "").trim() || tag || chat || target;
|
|
98
|
+
return {
|
|
99
|
+
target,
|
|
100
|
+
base,
|
|
101
|
+
tag,
|
|
102
|
+
chat,
|
|
103
|
+
name,
|
|
104
|
+
label: labelFor(name),
|
|
105
|
+
description: f?.description ?? "",
|
|
106
|
+
cwd: f?.cwd || [...rs].reverse().find((r) => r.cwd)?.cwd || "",
|
|
107
|
+
model: f?.model || [...rs].reverse().find((r) => r.model)?.model || "",
|
|
108
|
+
cli: f?.cli || last?.cli || "",
|
|
109
|
+
busy: f?.busy ?? false,
|
|
110
|
+
alive: f?.alive ?? false,
|
|
111
|
+
known: !!f,
|
|
112
|
+
self: target === scope.self,
|
|
113
|
+
local: base === scope.base,
|
|
114
|
+
parent: f?.parent,
|
|
115
|
+
inherited: !!(f?.clonedFrom ?? ""),
|
|
116
|
+
bornAt: f?.bornAt,
|
|
117
|
+
lastTs: Math.max(f?.lastActivity ?? 0, rs.reduce((mx, r) => Math.max(mx, r.updatedAt), 0)),
|
|
118
|
+
turns: rs.length,
|
|
119
|
+
preview: (last ? previewOf(last) : "") || f?.summary || "",
|
|
120
|
+
runningUntil: until > now ? until : 0,
|
|
121
|
+
taskTurns: rs.filter((r) => r.from?.kind === "task").length,
|
|
122
|
+
peerTurns: rs.filter((r) => r.from?.kind === "peer").length,
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
const known = new Set(nodes.map((n) => n.target));
|
|
126
|
+
// 一条边的两端都必须是节点 —— 派活方可能已经被回收, 那条边就无处落脚, 画一个
|
|
127
|
+
// 悬空端点只会让人以为漏了谁。
|
|
128
|
+
const link = (m, kind, from, to, ts, tag) => known.has(from) && known.has(to) ? bumpEdge(m, kind, from, to, ts, tag) : m;
|
|
129
|
+
// 观测边 —— 真的发生过的往来, 从 turn 记录里读。
|
|
130
|
+
const observed = turns.reduce((m, r) => {
|
|
131
|
+
const to = r.target;
|
|
132
|
+
const f = r.from;
|
|
133
|
+
// 定时任务没有"派活的一方" —— 它是节点上的计数 (taskTurns), 不是一条边。
|
|
134
|
+
const m1 = f?.kind === "peer" && f.from ? link(m, "peer", f.from, to, r.createdAt, { jobs: f.job }) : m;
|
|
135
|
+
// graph: 上一步的 tag 喂给这一步 —— 同聊天内解析成 target。
|
|
136
|
+
const prev = r.origin?.fromTag;
|
|
137
|
+
return prev !== undefined
|
|
138
|
+
? link(m1, "graph", prev ? `${baseOfKey(to)}#${prev}` : baseOfKey(to), to, r.createdAt, { runs: r.origin.runId })
|
|
139
|
+
: m1;
|
|
140
|
+
}, new Map());
|
|
141
|
+
// 登记边 —— 家谱。观测不到 (分身可能一句话没说), 但它是最稳定的一种关系。
|
|
142
|
+
const withLineage = facts.wizards.reduce((m, w) => (w.parent ? link(m, w.clonedFrom ? "clone" : "spawn", w.parent, w.target, w.bornAt ?? 0) : m), observed);
|
|
143
|
+
const edges = [...withLineage.values()].sort((a, b) => b.lastTs - a.lastTs);
|
|
144
|
+
// 有关系的一律留下 —— 边的端点被筛掉, 那条边就没地方落脚了。
|
|
145
|
+
const linked = new Set(edges.flatMap((e) => [e.from, e.to]));
|
|
146
|
+
const pinned = new Set([
|
|
147
|
+
...linked,
|
|
148
|
+
...facts.jobs.filter((j) => j.status === "open").flatMap((j) => [j.owner, ...j.members.map((mm) => mm.target)]),
|
|
149
|
+
...facts.schedules.flatMap((x) => [x.target, x.owner].filter(Boolean)),
|
|
150
|
+
]);
|
|
151
|
+
const relevant = (n) => n.local || n.self || pinned.has(n.target) || n.busy || n.alive || now - n.lastTs < RECENT_MS;
|
|
152
|
+
const parentOf = (t) => factOf.get(t)?.parent;
|
|
153
|
+
const byTs = (a, b) => b.lastTs - a.lastTs;
|
|
154
|
+
const bases = [...new Set(nodes.map((n) => n.base))];
|
|
155
|
+
const chats = bases
|
|
156
|
+
.map((base) => {
|
|
157
|
+
const mine = nodes.filter((n) => n.base === base);
|
|
158
|
+
const self = base === scope.base;
|
|
159
|
+
const kept = mine.filter(relevant).sort(byTs).slice(0, self ? SELF_CHAT_MAX : PER_CHAT_MAX);
|
|
160
|
+
return {
|
|
161
|
+
base,
|
|
162
|
+
name: facts.chatNames[base] ?? mine[0]?.chat ?? "",
|
|
163
|
+
self,
|
|
164
|
+
members: lineageOrder(kept.map((n) => n.target), parentOf),
|
|
165
|
+
hidden: mine.length - kept.length,
|
|
166
|
+
};
|
|
167
|
+
})
|
|
168
|
+
// 一个成员都留不下的聊天整张卡片都不画 —— 一张只写着「另有 12 个已停」的空卡
|
|
169
|
+
// 除了占位什么也没说。
|
|
170
|
+
.filter((c) => c.members.length > 0)
|
|
171
|
+
// 自己的聊天永远第一; 其余按最近活动。
|
|
172
|
+
.sort((a, b) => {
|
|
173
|
+
if (a.self !== b.self)
|
|
174
|
+
return a.self ? -1 : 1;
|
|
175
|
+
const ts = (c) => Math.max(0, ...c.members.map((mm) => nodes.find((n) => n.target === mm.target)?.lastTs ?? 0));
|
|
176
|
+
return ts(b) - ts(a);
|
|
177
|
+
});
|
|
178
|
+
// 节点表跟着卡片走 —— 画不出来的节点下发了也只是流量 (这条路由是被轮询的)。
|
|
179
|
+
const shown = new Set(chats.flatMap((c) => c.members.map((mm) => mm.target)));
|
|
180
|
+
return {
|
|
181
|
+
at: now,
|
|
182
|
+
base: scope.base,
|
|
183
|
+
self: scope.self,
|
|
184
|
+
chats,
|
|
185
|
+
nodes: nodes.filter((n) => shown.has(n.target)).sort(byTs),
|
|
186
|
+
// 两端都还在图上的边才画得出来。
|
|
187
|
+
edges: edges.filter((e) => shown.has(e.from) && shown.has(e.to)),
|
|
188
|
+
jobs: [...facts.jobs].sort((a, b) => (b.closedAt ?? b.openedAt) - (a.closedAt ?? a.openedAt)),
|
|
189
|
+
schedules: [...facts.schedules].sort((a, b) => a.nextAt - b.nextAt),
|
|
190
|
+
degraded: facts.wizards.length === 0,
|
|
191
|
+
};
|
|
192
|
+
};
|
|
193
|
+
//# sourceMappingURL=world.js.map
|