wezard 1.4.2 → 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.
Files changed (69) hide show
  1. package/CLAUDE.md +12 -7
  2. package/README.md +121 -67
  3. package/dist/daemon/approval.js +9 -3
  4. package/dist/daemon/approval.js.map +1 -1
  5. package/dist/daemon/cc-bridge.js.map +1 -1
  6. package/dist/daemon/chat-name.js +8 -18
  7. package/dist/daemon/chat-name.js.map +1 -1
  8. package/dist/daemon/detail.js +53 -12
  9. package/dist/daemon/detail.js.map +1 -1
  10. package/dist/daemon/graph.js +5 -7
  11. package/dist/daemon/graph.js.map +1 -1
  12. package/dist/daemon/inbound.js +135 -158
  13. package/dist/daemon/inbound.js.map +1 -1
  14. package/dist/daemon/index.js +381 -192
  15. package/dist/daemon/index.js.map +1 -1
  16. package/dist/daemon/jobs.js +2 -2
  17. package/dist/daemon/mirror-bridge.js +221 -134
  18. package/dist/daemon/mirror-bridge.js.map +1 -1
  19. package/dist/daemon/mirror-store.js.map +1 -1
  20. package/dist/daemon/model-select.js +74 -0
  21. package/dist/daemon/model-select.js.map +1 -0
  22. package/dist/daemon/peers.js +10 -16
  23. package/dist/daemon/peers.js.map +1 -1
  24. package/dist/daemon/spawn-tmux.js +27 -4
  25. package/dist/daemon/spawn-tmux.js.map +1 -1
  26. package/dist/daemon/task-gate.js +97 -0
  27. package/dist/daemon/task-gate.js.map +1 -0
  28. package/dist/daemon/task-registry.js +132 -0
  29. package/dist/daemon/task-registry.js.map +1 -0
  30. package/dist/daemon/tasks.js +126 -54
  31. package/dist/daemon/tasks.js.map +1 -1
  32. package/dist/daemon/wizard-memory.js +57 -0
  33. package/dist/daemon/wizard-memory.js.map +1 -0
  34. package/dist/daemon/wizard.js +84 -38
  35. package/dist/daemon/wizard.js.map +1 -1
  36. package/dist/mcp/server.js +138 -76
  37. package/dist/mcp/server.js.map +1 -1
  38. package/dist/shared/chat-http.js +185 -108
  39. package/dist/shared/chat-http.js.map +1 -1
  40. package/dist/shared/chat-render.js +26 -34
  41. package/dist/shared/chat-render.js.map +1 -1
  42. package/dist/shared/chat-view.js +14 -2
  43. package/dist/shared/chat-view.js.map +1 -1
  44. package/dist/shared/config.js +23 -4
  45. package/dist/shared/config.js.map +1 -1
  46. package/dist/shared/detail-render.js +12 -6
  47. package/dist/shared/detail-render.js.map +1 -1
  48. package/dist/shared/detail-store.js.map +1 -1
  49. package/dist/shared/role-render.js +52 -0
  50. package/dist/shared/role-render.js.map +1 -0
  51. package/dist/shared/role-view.js +210 -0
  52. package/dist/shared/role-view.js.map +1 -0
  53. package/dist/shared/schedule-spec.js +92 -116
  54. package/dist/shared/schedule-spec.js.map +1 -1
  55. package/dist/shared/session-label.js +103 -72
  56. package/dist/shared/session-label.js.map +1 -1
  57. package/dist/shared/std.js +3 -0
  58. package/dist/shared/std.js.map +1 -1
  59. package/dist/shared/task-file.js +120 -0
  60. package/dist/shared/task-file.js.map +1 -0
  61. package/dist/shared/trigger.js +232 -0
  62. package/dist/shared/trigger.js.map +1 -0
  63. package/dist/shared/world.js +7 -5
  64. package/dist/shared/world.js.map +1 -1
  65. package/dist/svr/index.js +67 -2
  66. package/dist/svr/index.js.map +1 -1
  67. package/package.json +1 -1
  68. package/web/chat.css +407 -212
  69. package/web/chat.js +932 -321
@@ -0,0 +1,52 @@
1
+ // Rolepage 的消息片段 —— 一条消息的正文 HTML, 左右对齐与头像由客户端按视角包装
2
+ // (同一条消息, 从发话方看靠右、从收信方看靠左, 服务端不该替它决定)。
3
+ //
4
+ // 入 发话方说的那句 (userQuery), markdown 由客户端渲染
5
+ // 出 该 wizard 这一轮的回复: 复用 renderTurnGroup (文本 + 工具细节 + 本轮用量),
6
+ // 去掉问句 —— 问句已经是上面那条入消息了
7
+ // 断点 视角 role 自己的 /clear /new
8
+ import { renderCutMark, renderTurnGroup, escHtml, hashStr, tagSig } from "./detail-render.js";
9
+ import { isKeepaliveTurn, isTurn, staleAt, turnDone } from "./chat-view.js";
10
+ // 入消息顶上的一行归因: 这句话不是人打的字时说清是谁派的。
11
+ // 时刻不在这里 —— 它写在气泡外的 .mwho 上 (每条消息只报一次时间), 所以
12
+ // 没有归因要说时整行省掉, 免得留下一条空的内边距。
13
+ const inMeta = (r) => {
14
+ const bits = [
15
+ r.origin ? `<span class="mchip graph" title="graph ${escHtml(r.origin.runId)}">🕸 轮 ${r.origin.round}/${r.origin.rounds} · 步 ${r.origin.step}/${r.origin.steps}</span>` : "",
16
+ r.from?.kind === "task" ? `<span class="mchip task">⏰ 定时 ${escHtml(r.from.taskId ?? "")}</span>` : "",
17
+ r.from?.kind === "peer" && r.from.public ? `<span class="mchip pub">公开</span>` : "",
18
+ r.from?.job ? `<span class="mchip job">📋 ${escHtml(r.from.job)}</span>` : "",
19
+ ].filter(Boolean);
20
+ return bits.length ? `<div class="mmeta">${bits.join("")}</div>` : "";
21
+ };
22
+ const renderIn = (r) => tagSig(`<section class="bubble mq" data-key="${escHtml(r.id)}:in">${inMeta(r)}` +
23
+ `<div class="md-body"></div><script type="text/plain" class="md-src">${escHtml(r.userQuery ?? "")}</script></section>`);
24
+ /** 子 agent 的轮次内联进父轮 —— 与旧线程视图同一规则 (见 chat-view.threadEntries)。 */
25
+ const childrenOf = (records, turnId, now) => records.filter(isTurn)
26
+ .filter((r) => r.agent?.parentTurnId === turnId)
27
+ .sort((a, b) => a.createdAt - b.createdAt)
28
+ .map((c) => renderTurnGroup(c, now));
29
+ const base = (m, dir) => ({
30
+ from: m.from, to: m.to, channel: m.channel,
31
+ fromName: dir.nameOf(m.from), fromLabel: dir.labelOf(m.from),
32
+ toName: dir.nameOf(m.to), toLabel: dir.labelOf(m.to),
33
+ });
34
+ export const renderMsg = (m, records, dir, now) => {
35
+ const r = m.turn;
36
+ const html = m.dir === "in" ? renderIn(r) : renderTurnGroup(r, now, childrenOf(records, r.id, now), false).html;
37
+ const live = m.dir === "out" && !turnDone(r, now);
38
+ return {
39
+ id: m.id, turnId: r.id, dir: m.dir, ...base(m, dir), ts: m.ts,
40
+ ping: isKeepaliveTurn(r),
41
+ html, sig: hashStr(html),
42
+ staleAt: live ? staleAt(r) : 0,
43
+ };
44
+ };
45
+ export const renderMark = (mk, role, dir) => {
46
+ const f = renderCutMark(mk);
47
+ return {
48
+ id: `m:${mk.id}`, turnId: mk.id, dir: "mark", ...base({ from: role, to: role, channel: "" }, dir),
49
+ ts: mk.createdAt, ping: false, html: f.html, sig: f.sig, staleAt: 0,
50
+ };
51
+ };
52
+ //# sourceMappingURL=role-render.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"role-render.js","sourceRoot":"","sources":["../../shared/role-render.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,210 @@
1
+ // Rolepage 的视图模型 —— 从**一个 role 的视角**重新折叠 turn 记录。纯函数, 无 IO。
2
+ //
3
+ // chat-view 折的是「一个聊天里有哪些会话」; 名字全局唯一以后这个轴不对了: wizard
4
+ // 不属于某个聊天, 它在好几个群里被人叫、跟好几个同伴私聊。人想看的是它的 IM ——
5
+ // 它参与了哪些群聊与单聊、在每一处说了什么、听到了什么。
6
+ //
7
+ // 一轮 turn 在这里拆成两条消息:
8
+ // 入 发话方 → 该 wizard userQuery
9
+ // 出 该 wizard → 发话方 本轮的回复 (文本 + 工具细节)
10
+ // 发话方: 同伴 (from.kind=peer) / 定时任务 (task) / 人 (speaker; 老记录没有就是
11
+ // 「人」)。消息落在哪个会话由 channel 决定 —— 公开频道 (群 / 与人的单聊) 按 base
12
+ // 归并, 私聊按对端 wizard 归并。
13
+ //
14
+ // role id: wizard 就是它的 target key; 人是 `human:<userid>` (`human:` = 不知道是谁);
15
+ // 定时任务是 `task:<id>` —— 它只发不收, 不能切成视角。人不用 `user:<id>`: 那正是
16
+ // 「与这个人的单聊」里默认 wizard 的 target, 两者字面相同, 同一条消息会变成自己对自己说。
17
+ import { baseOfKey, labelFor, stripSigil, tagOfKey } from "./session-label.js";
18
+ import { isGhostTurn, isKeepaliveTurn, isMark, isTurn, summarizeTag } from "./chat-view.js";
19
+ // ── 频道与发话方 ─────────────────────────────────────────────────────
20
+ /** 见 TurnDetailRecord.channel: 老记录按 from 推 —— peer 私聊, 其余在 home。 */
21
+ export const channelOf = (r) => r.channel !== undefined
22
+ ? r.channel
23
+ : r.from?.kind === "peer" && !r.from.public
24
+ ? ""
25
+ : baseOfKey(r.target ?? "");
26
+ /** `user:<id>` (speaker / 单聊 base) → 人的 role id。 */
27
+ export const humanOf = (principal) => `human:${principal.replace(/^user:/, "").replace(/#.*$/, "")}`;
28
+ export const senderOf = (r) => {
29
+ if (r.from?.kind === "peer")
30
+ return r.from.from || "human:";
31
+ if (r.from?.kind === "task")
32
+ return `task:${r.from.taskId || "?"}`;
33
+ if (r.speaker)
34
+ return humanOf(r.speaker);
35
+ const ch = channelOf(r);
36
+ // 与人的单聊里发话的只可能是那个人。
37
+ return ch.startsWith("user:") ? humanOf(ch) : "human:";
38
+ };
39
+ /** 顶层会话里的轮次: 去空壳、去子 agent (它内联在父轮里)。保温 ping 留着 ——
40
+ * 它不是对话, 但它是真花销, 时间轴上抹掉就等于说这段时间什么都没发生;
41
+ * 视图把它折成一行 (见 isPing)。 */
42
+ const convTurns = (records, now) => records.filter(isTurn).filter((r) => !!r.target && !r.agent && !isGhostTurn(r, now));
43
+ /** 保温 ping 的那一条 —— 进时间轴, 但不进会话列表的条数/预览, 也不算一条关系。 */
44
+ export const isPing = (m) => isKeepaliveTurn(m.turn);
45
+ /** 一轮 → 入/出两条消息。没问话的 (/clear 之后的续跑) 只有出; 还没产出也没在跑
46
+ * 的只有入。 */
47
+ export const messagesOfTurn = (r) => {
48
+ const w = r.target;
49
+ const who = senderOf(r);
50
+ const channel = channelOf(r);
51
+ const firstOut = r.items.reduce((m, it) => Math.min(m, it.ts), Infinity);
52
+ const inMsg = r.userQuery?.trim()
53
+ ? [{ id: `${r.id}:in`, turn: r, dir: "in", from: who, to: w, channel, ts: r.createdAt }]
54
+ : [];
55
+ const outMsg = r.items.length > 0 || !r.closed
56
+ ? [{ id: `${r.id}:out`, turn: r, dir: "out", from: w, to: who, channel, ts: Math.max(r.createdAt + 1, Number.isFinite(firstOut) ? firstOut : r.createdAt + 1) }]
57
+ : [];
58
+ return [...inMsg, ...outMsg];
59
+ };
60
+ export const allMessages = (records, now) => convTurns(records, now).flatMap(messagesOfTurn).sort((a, b) => a.ts - b.ts);
61
+ const fold = (s) => stripSigil(s).toLowerCase();
62
+ export const makeDirectory = (records, facts) => {
63
+ const facts_ = new Map(facts.wizards.map((w) => [w.target, w]));
64
+ const turnTargets = new Set(records.filter(isTurn).map((r) => r.target).filter((t) => !!t));
65
+ const wizards = new Set([...facts_.keys(), ...turnTargets]);
66
+ const chatName = (base) => facts.chatNames[base] ?? "";
67
+ const nameOf = (id) => {
68
+ if (id.startsWith("task:"))
69
+ return `定时 ${id.slice(5)}`;
70
+ if (wizards.has(id))
71
+ return (facts_.get(id)?.name ?? "").trim() || tagOfKey(id) || chatName(baseOfKey(id)) || id;
72
+ if (id === "human:")
73
+ return "人";
74
+ return id.startsWith("human:") ? id.slice(6) : id;
75
+ };
76
+ const labelOf = (id) => id.startsWith("task:") ? "⏰" : wizards.has(id) ? labelFor(nameOf(id)) : "👤";
77
+ const byName = new Map([...wizards].map((t) => [fold(nameOf(t)), t]));
78
+ const resolve = (ref) => {
79
+ const r = (ref ?? "").trim();
80
+ if (!r)
81
+ return undefined;
82
+ if (wizards.has(r))
83
+ return r;
84
+ const named = byName.get(fold(r));
85
+ if (named)
86
+ return named;
87
+ if (/^human:[^#]*$/.test(r))
88
+ return r;
89
+ return /^user:[^#]+$/.test(r) ? humanOf(r) : undefined;
90
+ };
91
+ return { nameOf, labelOf, resolve, fact: (id) => facts_.get(id), chatName, isWizard: (id) => wizards.has(id) };
92
+ };
93
+ const involves = (m, role) => m.from === role || m.to === role;
94
+ const other = (m, role) => (m.from === role ? m.to : m.from);
95
+ export const convKeyOf = (m, role) => (m.channel ? `c:${m.channel}` : `p:${other(m, role)}`);
96
+ const stripMd = (s) => s.replace(/[`*_~|#>]/g, "").replace(/\s+/g, " ").trim();
97
+ const msgText = (m) => {
98
+ if (m.dir === "in")
99
+ return m.turn.userQuery ?? "";
100
+ const t = [...m.turn.items].reverse().find((it) => it.t === "text");
101
+ return t?.body ?? "";
102
+ };
103
+ const previewOf = (m, dir) => m ? `${dir.nameOf(m.from)}: ${stripMd(msgText(m)).slice(0, 80)}` : "";
104
+ const SUB_MAX = 40;
105
+ /** 一个 role 参与的全部会话, 最近活动在前。wizard 的 home 频道即使还没说过话也在列。 */
106
+ export const convsOf = (all_, role, dir) => {
107
+ // 会话列表讲的是"谁跟谁说过什么", ping 不是话: 条数与预览都不该被它顶掉。
108
+ const msgs = all_.filter((m) => !isPing(m));
109
+ const mine = msgs.filter((m) => involves(m, role));
110
+ const homes = dir.isWizard(role) ? [`c:${baseOfKey(role)}`] : [];
111
+ const keys = [...new Set([...mine.map((m) => convKeyOf(m, role)), ...homes])];
112
+ return keys
113
+ .map((key) => {
114
+ if (key.startsWith("p:")) {
115
+ const peer = key.slice(2);
116
+ const ms = mine.filter((m) => !m.channel && other(m, role) === peer);
117
+ const last = ms[ms.length - 1];
118
+ return {
119
+ key, kind: "wizard", name: dir.nameOf(peer), label: dir.labelOf(peer), base: "", peer,
120
+ lastTs: last?.ts ?? 0, preview: previewOf(last, dir), count: ms.length, subs: [],
121
+ };
122
+ }
123
+ const base = key.slice(2);
124
+ const all = msgs.filter((m) => m.channel === base);
125
+ const last = all[all.length - 1];
126
+ const others = [...new Set(all.flatMap((m) => [m.from, m.to]))].filter((r) => r !== role && !r.startsWith("task:"));
127
+ const subs = others
128
+ .map((r) => {
129
+ const pair = all.filter((m) => involves(m, role) && other(m, role) === r);
130
+ const seen = all.filter((m) => involves(m, r));
131
+ return {
132
+ role: r, name: dir.nameOf(r), label: dir.labelOf(r), count: pair.length,
133
+ lastTs: (pair[pair.length - 1] ?? seen[seen.length - 1])?.ts ?? 0,
134
+ };
135
+ })
136
+ // 与我有往来的排前, 再按最近。
137
+ .sort((a, b) => Number(b.count > 0) - Number(a.count > 0) || b.lastTs - a.lastTs)
138
+ .slice(0, SUB_MAX);
139
+ return {
140
+ key, kind: "group", base,
141
+ name: dir.chatName(base) || (base.startsWith("user:") ? dir.nameOf(humanOf(base)) : base.replace(/^chat:/, "").slice(0, 10)),
142
+ label: "💬",
143
+ lastTs: last?.ts ?? 0, preview: previewOf(last, dir), count: all.length, subs,
144
+ };
145
+ })
146
+ .sort((a, b) => b.lastTs - a.lastTs);
147
+ };
148
+ /** 一个会话窗口里的消息。`withRole` 只对公开频道有意义: 当前 role 与它在这个频道里的往来。 */
149
+ export const convMessages = (msgs, role, key, withRole) => {
150
+ if (key.startsWith("p:")) {
151
+ const peer = key.slice(2);
152
+ return msgs.filter((m) => !m.channel && involves(m, role) && other(m, role) === peer);
153
+ }
154
+ const base = key.slice(2);
155
+ const ch = msgs.filter((m) => m.channel === base);
156
+ return withRole ? ch.filter((m) => involves(m, role) && other(m, role) === withRole) : ch;
157
+ };
158
+ export const sessionsOf = (records, role, now) => {
159
+ const turns = records.filter(isTurn).filter((r) => r.target === role && !r.agent && !isGhostTurn(r, now))
160
+ .sort((a, b) => a.createdAt - b.createdAt);
161
+ const groups = turns.reduce((acc, r) => {
162
+ const sid = r.sessionId ?? "";
163
+ const cur = acc[acc.length - 1];
164
+ return cur && cur.sessionId === sid ? [...acc.slice(0, -1), { sessionId: sid, rs: [...cur.rs, r] }] : [...acc, { sessionId: sid, rs: [r] }];
165
+ }, []);
166
+ return groups.map((g, i) => ({
167
+ sessionId: g.sessionId,
168
+ start: g.rs[0].createdAt,
169
+ end: groups[i + 1]?.rs[0]?.createdAt ?? Infinity,
170
+ turns: g.rs.length,
171
+ lastTs: g.rs.reduce((m, r) => Math.max(m, r.updatedAt), 0),
172
+ }));
173
+ };
174
+ export const inSpan = (span) => (ts) => !span || (ts >= span.start && ts < span.end);
175
+ /** 视角 role 自己的上下文断点 (/clear /new) —— 画在它参与的每个会话窗口里。 */
176
+ export const marksOf = (records, role) => records.filter(isMark).filter((m) => m.target === role);
177
+ export const roleInfo = (id, dir, facts, stats) => {
178
+ const f = dir.fact(id);
179
+ const ref = (t) => ({ id: t, name: dir.nameOf(t), label: dir.labelOf(t) });
180
+ const wiz = dir.isWizard(id);
181
+ const kids = facts.wizards.filter((w) => w.parent === id);
182
+ return {
183
+ id,
184
+ kind: wiz ? "wizard" : id.startsWith("task:") ? "task" : "human",
185
+ name: dir.nameOf(id),
186
+ label: dir.labelOf(id),
187
+ description: f?.description ?? "",
188
+ cwd: f?.cwd || stats?.cwd || "",
189
+ model: f?.model || stats?.model || "",
190
+ bornAt: f?.bornAt,
191
+ chat: wiz ? dir.chatName(baseOfKey(id)) : "",
192
+ parent: f?.parent ? ref(f.parent) : undefined,
193
+ clones: kids.filter((w) => w.clonedFrom).map((w) => ref(w.target)),
194
+ spawns: kids.filter((w) => !w.clonedFrom).map((w) => ref(w.target)),
195
+ alive: f?.alive ?? false,
196
+ busy: f?.busy ?? false,
197
+ forkedFrom: f?.clonedFrom ?? "",
198
+ };
199
+ };
200
+ /** 页脚总账: 视角 role 自己跑过的全部轮次 (含子 agent 与 ping —— 那都是真花销)。 */
201
+ export const roleStats = (records, role, now, span) => {
202
+ const turns = records.filter(isTurn)
203
+ .filter((r) => r.target === role && !isGhostTurn(r, now) && inSpan(span)(r.createdAt))
204
+ .sort((a, b) => a.createdAt - b.createdAt);
205
+ return turns.length ? summarizeTag(role, turns, now) : undefined;
206
+ };
207
+ /** 与别的 role 有没有关系 (家谱 / 私聊 / 派活) —— 决定顶栏要不要出「关系图」入口。 */
208
+ export const hasRelations = (msgs, role, info) => !!info.parent || info.clones.length > 0 || info.spawns.length > 0 ||
209
+ msgs.some((m) => involves(m, role) && (m.turn.from?.kind === "peer"));
210
+ //# sourceMappingURL=role-view.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"role-view.js","sourceRoot":"","sources":["../../shared/role-view.ts"],"names":[],"mappings":"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"}
@@ -1,16 +1,10 @@
1
- // 定时规格 —— 把一句人话里的「什么时候」解析成三种形状之一, 外加到点判定与
2
- // 下次触发时刻。整模块是纯函数: 时钟由调用方传进来 (now), 这里只做匹配与算术。
1
+ // 「什么时候」的**词法层**: 把一句人话拆成零件 —— 星期、时刻、间隔、时间窗口。
2
+ // 纯函数, 且刻意不持有任何判定逻辑: 零件拼成规约 (Trigger)、到点判定与下次触发
3
+ // 全在 shared/trigger.ts。分这一刀是因为两边的变化方向不同 —— 这里跟着"人怎么
4
+ // 说"长, 那里跟着"怎么算"长。
3
5
  //
4
- // 三种形状覆盖了「比 /loop 更长」的全部实际需求:
5
- // daily 某几天的 HH:MM (days 为空 = 每天) —— 「每个工作日晚上 9:30」
6
- // every 每隔 N 分钟 —— 「每两小时」
7
- // once 一个绝对时刻 —— 「明早 9 点」「20 分钟后」, 触发后自删
8
- //
9
- // 刻意不做 cron 表达式: 人在群里说的是「每个工作日晚上九点半」, 让 wizard 把这句
10
- // 原样递进来比让它翻译成 `30 21 * * 1-5` 更不容易错 —— 翻译错了没人看得出来。
11
- // 守护进程重启/维护会吃掉整分钟的 tick。错过的那一下在 5 分钟内补跑, 超出就放弃 ——
12
- // 早上 9 点的任务在中午补跑比不跑更糟。
13
- const CATCHUP_MS = 5 * 60_000;
6
+ // 刻意不做 cron 表达式: 人在群里说的是「白天每隔一个小时」, 让 wizard 把这句原样
7
+ // 递进来, 比让它翻译成 `0 8-20/1 * * *` 更不容易错 —— 翻译错了没人看得出来。
14
8
  // ── 汉字数字 ────────────────────────────────────────────────────────
15
9
  const CN_DIGIT = {
16
10
  零: 0, 〇: 0, 一: 1, 二: 2, 两: 2, 三: 3, 四: 4, 五: 5,
@@ -33,6 +27,8 @@ export const cnToNum = (raw) => {
33
27
  return tens * 10 + ones;
34
28
  };
35
29
  const NUM = "[0-9零〇一二三四五六七八九十两]";
30
+ /** 量词。「每隔一**个**小时」「每**个**小时」—— 漏掉它整句就解析不出来。 */
31
+ const CLASSIFIER = "(?:个|個)?";
36
32
  // ── 星期 ────────────────────────────────────────────────────────────
37
33
  const CN_WEEKDAY = {
38
34
  日: 0, 天: 0, 一: 1, 二: 2, 三: 3, 四: 4, 五: 5, 六: 6,
@@ -58,8 +54,11 @@ export const parseDays = (t) => {
58
54
  return [];
59
55
  return undefined;
60
56
  };
57
+ /** 星期的人话写法, 供 Trigger 的 onDays 直接收人话。`[]` = 每天。 */
58
+ export const daysOf = (spec) => Array.isArray(spec) ? uniqSorted([...spec]) : (parseDays(String(spec)) ?? []);
61
59
  // ── 时刻 ────────────────────────────────────────────────────────────
62
60
  // 时段词只在 12 小时制下有意义: 「晚上 21:30」里的 21 已经说清楚了, 再 +12 就废了。
61
+ const MERIDIEM = /下午|傍晚|晚上|夜里|夜間|今晚|每晚|晚|中午|正午|凌晨|半夜|早上|早晨|清晨|上午|每早|明早|noon|midnight|(?<![a-z])[ap]m\b/i;
63
62
  const shiftMeridiem = (h, t) => {
64
63
  if (h > 12)
65
64
  return h;
@@ -74,11 +73,16 @@ const shiftMeridiem = (h, t) => {
74
73
  return h;
75
74
  };
76
75
  const QUARTER = { 半: 30, 一刻: 15, 三刻: 45 };
77
- /** 「晚上九点半」「21:30」「9:30pm」→ {hour, minute}。没提时刻返回 undefined。 */
78
- export const parseTimeOfDay = (t) => {
76
+ /**
77
+ * 「晚上九点半」「21:30」「9:30pm」→ {hour, minute}。没提时刻返回 undefined。
78
+ * `ctx` 是这句话的其余部分: 「晚上8点到10点」里的「10点」自己不带时段词, 得回
79
+ * 整句去借 —— 否则后半段会掉到上午。段内自带时段词时以段内为准。
80
+ */
81
+ export const parseTimeOfDay = (t, ctx = t) => {
82
+ const mer = MERIDIEM.test(t) ? t : ctx;
79
83
  const digital = t.match(/(\d{1,2})\s*[::]\s*(\d{1,2})/);
80
84
  if (digital) {
81
- const h = shiftMeridiem(Number(digital[1]), t);
85
+ const h = shiftMeridiem(Number(digital[1]), mer);
82
86
  return { hour: h % 24, minute: Number(digital[2]) % 60 };
83
87
  }
84
88
  const spoken = t.match(new RegExp(`(${NUM}{1,3})\\s*(?:点|時|时|时钟|o'?clock)\\s*(半|一刻|三刻)?\\s*(?:(${NUM}{1,3})\\s*分)?`));
@@ -87,7 +91,7 @@ export const parseTimeOfDay = (t) => {
87
91
  if (h0 === undefined || h0 > 24)
88
92
  return undefined;
89
93
  const m = spoken[2] ? QUARTER[spoken[2]] : spoken[3] ? (cnToNum(spoken[3]) ?? 0) : 0;
90
- return { hour: shiftMeridiem(h0, t) % 24, minute: m % 60 };
94
+ return { hour: shiftMeridiem(h0, mer) % 24, minute: m % 60 };
91
95
  }
92
96
  const bare = t.match(/(\d{1,2})\s*(am|pm)\b/i);
93
97
  if (bare)
@@ -97,19 +101,45 @@ export const parseTimeOfDay = (t) => {
97
101
  // ── 间隔 ────────────────────────────────────────────────────────────
98
102
  const UNIT_MIN = {
99
103
  分: 1, 分钟: 1, 分鐘: 1, min: 1, mins: 1, minute: 1, minutes: 1, m: 1,
100
- 小时: 60, 小時: 60, 钟头: 60, 鐘頭: 60, 时: 60, hour: 60, hours: 60, hr: 60, h: 60,
104
+ 小时: 60, 小時: 60, 钟头: 60, 鐘頭: 60, 时: 60, hour: 60, hours: 60, hr: 60, hrs: 60, h: 60,
101
105
  天: 1440, 日: 1440, day: 1440, days: 1440, d: 1440,
102
106
  };
103
- /** 「每隔两小时」「每 30 分钟」「每半小时」「every 15m」→ 分钟数。 */
107
+ const CN_UNIT = "分钟|分鐘|分|小时|小時|钟头|鐘頭|天|日";
108
+ /** 省掉数字时只认「小时」类: 「每天」是日程不是间隔, 咬住它会把每天 9 点变成每 24 小时。 */
109
+ const CN_HOUR_UNIT = "小时|小時|钟头|鐘頭";
110
+ /** 「两小时」「30 分钟」「1h」「90m」→ 分钟数。纯时长, 不含「每」。 */
111
+ export const parseDuration = (raw) => {
112
+ if (typeof raw === "number")
113
+ return raw > 0 ? raw : undefined;
114
+ const t = raw.trim();
115
+ const half = t.match(new RegExp(`^半\\s*${CLASSIFIER}\\s*(${CN_UNIT})$`));
116
+ if (half)
117
+ return (UNIT_MIN[half[1]] ?? 60) / 2;
118
+ const cn = t.match(new RegExp(`^(${NUM}{1,4})\\s*${CLASSIFIER}\\s*(${CN_UNIT})$`));
119
+ if (cn) {
120
+ const n = cnToNum(cn[1]);
121
+ return n && n > 0 ? n * (UNIT_MIN[cn[2]] ?? 1) : undefined;
122
+ }
123
+ const en = t.match(/^(\d{1,5})?\s*(minutes?|mins?|hours?|hrs?|days?|[mhd])$/i);
124
+ if (en) {
125
+ const n = en[1] ? Number(en[1]) : 1;
126
+ return n > 0 ? n * (UNIT_MIN[en[2].toLowerCase()] ?? 1) : undefined;
127
+ }
128
+ return undefined;
129
+ };
130
+ /** 「每隔两个小时」「每 30 分钟」「每半小时」「每小时」「every 15m」→ 分钟数。 */
104
131
  export const parseInterval = (t) => {
105
- const half = t.match(/每\s*隔?\s*半\s*(小时|小時|钟头|鐘頭|天|日|hour)/);
132
+ const half = t.match(new RegExp(`每\\s*隔?\\s*半\\s*${CLASSIFIER}\\s*(${CN_UNIT}|hour)`));
106
133
  if (half)
107
134
  return (UNIT_MIN[half[1]] ?? 60) / 2;
108
- const cn = t.match(new RegExp(`每\\s*隔?\\s*(${NUM}{1,3})\\s*(分钟|分鐘|分|小时|小時|钟头|鐘頭|天|日)`));
135
+ const cn = t.match(new RegExp(`每\\s*隔?\\s*(${NUM}{1,3})\\s*${CLASSIFIER}\\s*(${CN_UNIT})`));
109
136
  if (cn) {
110
137
  const n = cnToNum(cn[1]);
111
138
  return n && n > 0 ? n * (UNIT_MIN[cn[2]] ?? 1) : undefined;
112
139
  }
140
+ const bare = t.match(new RegExp(`每\\s*隔?\\s*${CLASSIFIER}\\s*(${CN_HOUR_UNIT})`));
141
+ if (bare)
142
+ return UNIT_MIN[bare[1]] ?? 60;
113
143
  const en = t.match(/every\s*(\d{1,4})?\s*(minutes?|mins?|hours?|hrs?|days?|[mhd])\b/i);
114
144
  if (en) {
115
145
  const n = en[1] ? Number(en[1]) : 1;
@@ -117,124 +147,70 @@ export const parseInterval = (t) => {
117
147
  }
118
148
  return undefined;
119
149
  };
150
+ // ── 时间窗口 ────────────────────────────────────────────────────────
151
+ // 「白天每小时」里的「白天」不是触发时刻, 是**筛子** —— 它自己产生不了任何一枪,
152
+ // 只否决落在窗外的那些。所以它在这里是独立零件, 在 Trigger 里是 guard。
153
+ // `soft` = 这个词同时也是时段词:「晚上9:30」里的「晚上」说的是 21 点这一刻, 不是
154
+ // 一段窗口。软窗口只在整句没给出具体时刻时才作数 (见 parseTrigger)。
155
+ const NAMED_WINDOW = [
156
+ { re: /白天|日间|日間|daytime/i, from: "08:00", to: "20:00", soft: false },
157
+ { re: /工作时间|上班时间|工作時間|上班時間|office\s*hours|business\s*hours/i, from: "09:00", to: "18:00", soft: false },
158
+ { re: /夜里|夜间|夜間|晚上|深夜|overnight|at\s*night/i, from: "20:00", to: "08:00", soft: true },
159
+ ];
160
+ const SEG = "[^,,。;;\\s]{1,12}";
161
+ /** 显式区间的连接部分 —— parseTrigger 要把它从句子里剔掉再找日程时刻。 */
162
+ export const RANGE_RE = new RegExp(`(${SEG})\\s*(?:到|至|~|~|-|–|—|until|to)\\s*(${SEG})`, "i");
163
+ /** 「白天」「8点到20点」「09:00-18:00」→ 一天里的一段。没提返回 undefined。 */
164
+ export const parseWindow = (t) => {
165
+ const range = t.match(RANGE_RE);
166
+ if (range) {
167
+ const from = parseTimeOfDay(range[1], t);
168
+ const to = parseTimeOfDay(range[2], t);
169
+ if (from && to)
170
+ return { from, to, soft: false, explicit: true };
171
+ }
172
+ const named = NAMED_WINDOW.find((w) => w.re.test(t));
173
+ return named
174
+ ? { from: parseTimeOfDay(named.from), to: parseTimeOfDay(named.to), soft: named.soft, explicit: false }
175
+ : undefined;
176
+ };
120
177
  // ── 一次性 ──────────────────────────────────────────────────────────
121
178
  const atOn = (now, addDays, hour, minute) => new Date(now.getFullYear(), now.getMonth(), now.getDate() + addDays, hour, minute, 0, 0).getTime();
122
- const parseOnce = (t, now) => {
123
- const rel = t.match(new RegExp(`(半|${NUM}{1,4})\\s*(分钟|分鐘|分|小时|小時|钟头|鐘頭|天)\\s*(?:之)?后`))
179
+ /** 「20 分钟后」「明早 9 点」「今晚」→ 绝对时刻 (epoch ms)。不是一次性的返回 undefined。 */
180
+ export const parseOnceAt = (t, now) => {
181
+ const rel = t.match(new RegExp(`(半|${NUM}{1,4})\\s*${CLASSIFIER}\\s*(${CN_UNIT})\\s*(?:之)?后`))
124
182
  ?? t.match(/\bin\s*(\d{1,4})\s*(minutes?|mins?|hours?|hrs?|days?|[mhd])\b/i);
125
183
  if (rel) {
126
184
  const unit = UNIT_MIN[rel[2].toLowerCase()] ?? 1;
127
185
  const n = rel[1] === "半" ? 0.5 : (cnToNum(rel[1]) ?? Number(rel[1]));
128
186
  if (n > 0)
129
- return { kind: "once", at: now.getTime() + Math.round(n * unit) * 60_000 };
187
+ return now.getTime() + Math.round(n * unit) * 60_000;
130
188
  }
131
189
  const dayWord = t.match(/(今天|今晚|今夜|明天|明早|明晚|后天|後天|tomorrow|tonight)/i);
132
190
  if (!dayWord)
133
191
  return undefined;
134
192
  const addDays = /后天|後天/.test(dayWord[1]) ? 2 : /今天|今晚|今夜|tonight/i.test(dayWord[1]) ? 0 : 1;
135
193
  const tod = parseTimeOfDay(t) ?? (/晚|tonight/i.test(dayWord[1]) ? { hour: 21, minute: 0 } : undefined);
136
- if (!tod)
137
- return undefined;
138
- return { kind: "once", at: atOn(now, addDays, tod.hour, tod.minute) };
139
- };
140
- // ── 入口 ────────────────────────────────────────────────────────────
141
- /**
142
- * 整句话进, 规格出。认不出返回 undefined —— 调用方要把原句回给用户让他重说,
143
- * 绝不能猜一个时间安静地存下去。
144
- *
145
- * 顺序有意为之: 一次性(带「今天/明天/N 分钟后」的措辞) 最具体, 其次是间隔,
146
- * 最后才是周期日程 —— 「每天」这种词在前两者里都不会出现。
147
- */
148
- export const parseWhen = (text, now = new Date()) => {
149
- const t = text.trim();
150
- if (!t)
151
- return undefined;
152
- const once = parseOnce(t, now);
153
- if (once)
154
- return once;
155
- const tod = parseTimeOfDay(t);
156
- // 「每 2 天早上 9 点」这种既给间隔又给时刻的说法, 按日程理解更接近本意;
157
- // 纯间隔 (不带时刻) 才走 every。
158
- const iv = parseInterval(t);
159
- if (iv !== undefined && !tod)
160
- return { kind: "every", minutes: Math.max(1, Math.round(iv)) };
161
- const days = parseDays(t);
162
- if (tod)
163
- return { kind: "daily", days: days ?? [], hour: tod.hour, minute: tod.minute };
164
- if (days)
165
- return { kind: "daily", days, hour: 9, minute: 0 }; // 只说了「每个工作日」: 默认早上 9 点
166
- if (iv !== undefined)
167
- return { kind: "every", minutes: Math.max(1, Math.round(iv)) };
168
- return undefined;
169
- };
170
- // ── 判定 ────────────────────────────────────────────────────────────
171
- const instantToday = (w, now) => {
172
- if (w.days.length && !w.days.includes(now.getDay()))
173
- return undefined;
174
- const d = new Date(now);
175
- d.setHours(w.hour, w.minute, 0, 0);
176
- return d.getTime();
177
- };
178
- /**
179
- * 到点了吗。`since` = 上次触发时刻, 从没触发过就传创建时刻 —— 它同时承担三件事:
180
- * 同一分钟内多次 tick 的去重、重启后不重复触发、以及「刚创建就到点」不立即打一枪。
181
- */
182
- export const isDue = (when, now, since) => {
183
- const t = now.getTime();
184
- switch (when.kind) {
185
- case "once":
186
- return t >= when.at && since < when.at;
187
- case "every":
188
- return t - since >= when.minutes * 60_000;
189
- case "daily": {
190
- const inst = instantToday(when, now);
191
- return inst !== undefined && t >= inst && t - inst < CATCHUP_MS && since < inst;
192
- }
193
- }
194
- };
195
- /** 下一次该在什么时候响。`since` 同 isDue。 */
196
- export const nextFire = (when, since, now = new Date()) => {
197
- if (when.kind === "once")
198
- return when.at;
199
- if (when.kind === "every")
200
- return Math.max(since + when.minutes * 60_000, now.getTime());
201
- for (let i = 0; i < 8; i++) {
202
- const c = new Date(now.getFullYear(), now.getMonth(), now.getDate() + i, when.hour, when.minute, 0, 0);
203
- if (c.getTime() > now.getTime() && (!when.days.length || when.days.includes(c.getDay())))
204
- return c.getTime();
205
- }
206
- return now.getTime();
194
+ return tod ? atOn(now, addDays, tod.hour, tod.minute) : undefined;
207
195
  };
208
- // ── 回显 ────────────────────────────────────────────────────────────
209
- const hhmm = (h, m) => `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
196
+ // ── 回显零件 ────────────────────────────────────────────────────────
197
+ export const hhmm = (t) => `${String(t.hour).padStart(2, "0")}:${String(t.minute).padStart(2, "0")}`;
210
198
  const CN_DAY_NAME = ["日", "一", "二", "三", "四", "五", "六"];
211
- const daysLabel = (days) => {
199
+ export const daysLabel = (days) => {
212
200
  if (!days.length)
213
201
  return "每天";
214
- const k = days.join(",");
202
+ const k = [...days].join(",");
215
203
  if (k === "1,2,3,4,5")
216
204
  return "每个工作日";
217
205
  if (k === "0,6")
218
206
  return "每逢周末";
219
207
  return `每周${days.map((d) => CN_DAY_NAME[d]).join("、")}`;
220
208
  };
221
- /** 人能一眼确认的回显 —— 存下去之前必须把它回给用户。 */
222
- export const describeWhen = (when) => {
223
- switch (when.kind) {
224
- case "daily":
225
- return `${daysLabel(when.days)} ${hhmm(when.hour, when.minute)}`;
226
- case "every": {
227
- const { minutes: n } = when;
228
- if (n % 1440 === 0)
229
- return `每 ${n / 1440} 天`;
230
- if (n % 60 === 0)
231
- return `每 ${n / 60} 小时`;
232
- return n > 60 ? `每 ${Math.floor(n / 60)} 小时 ${n % 60} 分` : `每 ${n} 分钟`;
233
- }
234
- case "once": {
235
- const d = new Date(when.at);
236
- return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")} ${hhmm(d.getHours(), d.getMinutes())}(仅一次)`;
237
- }
238
- }
209
+ export const durationLabel = (n) => {
210
+ if (n % 1440 === 0)
211
+ return `${n / 1440} 天`;
212
+ if (n % 60 === 0)
213
+ return `${n / 60} 小时`;
214
+ return n > 60 ? `${Math.floor(n / 60)} 小时 ${n % 60} 分` : `${n} 分钟`;
239
215
  };
240
216
  //# sourceMappingURL=schedule-spec.js.map