@xgjktech/xg_cwork_im 1.16.45 → 1.17.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 (63) hide show
  1. package/README.md +433 -433
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +27 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/src/connection.js +2 -2
  6. package/dist/src/connection.js.map +1 -1
  7. package/dist/src/deliver-text.test.js +22 -22
  8. package/dist/src/dispatch-mentioned-reply.d.ts.map +1 -1
  9. package/dist/src/dispatch-mentioned-reply.js +8 -0
  10. package/dist/src/dispatch-mentioned-reply.js.map +1 -1
  11. package/dist/src/plan-blocked-notice.d.ts +15 -0
  12. package/dist/src/plan-blocked-notice.d.ts.map +1 -0
  13. package/dist/src/plan-blocked-notice.js +28 -0
  14. package/dist/src/plan-blocked-notice.js.map +1 -0
  15. package/dist/src/plan-blocked-notice.test.d.ts +2 -0
  16. package/dist/src/plan-blocked-notice.test.d.ts.map +1 -0
  17. package/dist/src/plan-blocked-notice.test.js +80 -0
  18. package/dist/src/plan-blocked-notice.test.js.map +1 -0
  19. package/dist/src/plan-event-subscription.d.ts +26 -0
  20. package/dist/src/plan-event-subscription.d.ts.map +1 -0
  21. package/dist/src/plan-event-subscription.js +121 -0
  22. package/dist/src/plan-event-subscription.js.map +1 -0
  23. package/dist/src/plan-event-subscription.test.d.ts +2 -0
  24. package/dist/src/plan-event-subscription.test.d.ts.map +1 -0
  25. package/dist/src/plan-event-subscription.test.js +257 -0
  26. package/dist/src/plan-event-subscription.test.js.map +1 -0
  27. package/dist/src/plan-session-registry.d.ts +24 -0
  28. package/dist/src/plan-session-registry.d.ts.map +1 -0
  29. package/dist/src/plan-session-registry.js +84 -0
  30. package/dist/src/plan-session-registry.js.map +1 -0
  31. package/dist/src/plan-session-registry.test.d.ts +2 -0
  32. package/dist/src/plan-session-registry.test.d.ts.map +1 -0
  33. package/dist/src/plan-session-registry.test.js +109 -0
  34. package/dist/src/plan-session-registry.test.js.map +1 -0
  35. package/dist/src/plan-store-access.d.ts +16 -0
  36. package/dist/src/plan-store-access.d.ts.map +1 -0
  37. package/dist/src/plan-store-access.js +76 -0
  38. package/dist/src/plan-store-access.js.map +1 -0
  39. package/dist/src/plan-store-access.test.d.ts +2 -0
  40. package/dist/src/plan-store-access.test.d.ts.map +1 -0
  41. package/dist/src/plan-store-access.test.js +139 -0
  42. package/dist/src/plan-store-access.test.js.map +1 -0
  43. package/dist/src/plan-tool-middleware.d.ts +20 -0
  44. package/dist/src/plan-tool-middleware.d.ts.map +1 -0
  45. package/dist/src/plan-tool-middleware.js +99 -0
  46. package/dist/src/plan-tool-middleware.js.map +1 -0
  47. package/dist/src/plan-tool-middleware.test.d.ts +2 -0
  48. package/dist/src/plan-tool-middleware.test.d.ts.map +1 -0
  49. package/dist/src/plan-tool-middleware.test.js +329 -0
  50. package/dist/src/plan-tool-middleware.test.js.map +1 -0
  51. package/dist/src/plan-tool-result.d.ts +20 -0
  52. package/dist/src/plan-tool-result.d.ts.map +1 -0
  53. package/dist/src/plan-tool-result.js +78 -0
  54. package/dist/src/plan-tool-result.js.map +1 -0
  55. package/dist/src/plan-tool-result.test.d.ts +2 -0
  56. package/dist/src/plan-tool-result.test.d.ts.map +1 -0
  57. package/dist/src/plan-tool-result.test.js +107 -0
  58. package/dist/src/plan-tool-result.test.js.map +1 -0
  59. package/dist/src/subagent-monitor.js +11 -17
  60. package/dist/src/subagent-monitor.js.map +1 -1
  61. package/dist/src/turn-deliver-buffer.test.js +25 -25
  62. package/openclaw.plugin.json +27 -26
  63. package/package.json +76 -72
@@ -0,0 +1,121 @@
1
+ import { isPlanEventPayload, planEventStream } from "@xgjktech/xg-openclaw-shared";
2
+ import { getToken } from "./auth.js";
3
+ import { sendTextMessage } from "./send-service.js";
4
+ /**
5
+ * xg-harness-tools 的 plugin id:streams 过滤需要与其 openclaw.plugin.json 的 id 一致。
6
+ */
7
+ const HARNESS_PLUGIN_ID = "xg-harness-tools";
8
+ const LOG_PREFIX = "[xg_cwork_im] [plan-event-subscription]";
9
+ /**
10
+ * `deliveryContext` 类型在 `@xgjktech/xg-openclaw-shared` 侧声明为 `unknown`(跨插件契约不引入
11
+ * openclaw 依赖),运行时实际形状是 openclaw `DeliveryContext`(`{ to?, accountId?, ... }`),
12
+ * 这里只做安全的结构读取,不假设其余字段存在。
13
+ */
14
+ function resolveFromDeliveryContext(deliveryContext) {
15
+ if (typeof deliveryContext !== "object" || deliveryContext === null) {
16
+ return undefined;
17
+ }
18
+ const to = deliveryContext.to;
19
+ if (typeof to !== "string" || to === "") {
20
+ return undefined;
21
+ }
22
+ return { groupId: to };
23
+ }
24
+ /**
25
+ * `agentAccountId`/`messageChannel` → IM 侧 groupId 的反查目前没有映射表
26
+ * (spec §1.4:若映射彻底无法建立,该次事件应放弃推群,不影响正确性,
27
+ * 因为主感知路径本就是 SQLite 反查而非事件)。保留函数形态与 spec 一致,
28
+ * 便于未来接入映射表时零改动补上实现。
29
+ */
30
+ function resolveFromAccountAndChannel(_agentAccountId, _messageChannel) {
31
+ return undefined;
32
+ }
33
+ /**
34
+ * 路由定位(spec §1.2 优先级):
35
+ * 1. `payload.deliveryContext`
36
+ * 2. `payload.agentAccountId` / `payload.messageChannel`
37
+ * 3. 备用:`payload.sessionKey ?? event.sessionKey`(当前无 sessionKey→群 映射,直接放弃)
38
+ *
39
+ * ⚠️ 这些路由字段都在 `event.data`(即 payload)内部,`AgentEventPayload` 顶层没有
40
+ * `deliveryContext`/`agentAccountId`/`messageChannel`(DESIGN.md §3 已编译级确认),
41
+ * 只有 `sessionKey` 在顶层也存在,用作备用兜底的次优先级来源。
42
+ */
43
+ function resolveDeliveryTarget(payload, event) {
44
+ if (payload.deliveryContext !== undefined) {
45
+ return resolveFromDeliveryContext(payload.deliveryContext);
46
+ }
47
+ if (payload.agentAccountId !== undefined || payload.messageChannel !== undefined) {
48
+ return resolveFromAccountAndChannel(payload.agentAccountId, payload.messageChannel);
49
+ }
50
+ const sessionKey = payload.sessionKey ?? event.sessionKey;
51
+ if (sessionKey === undefined) {
52
+ return undefined;
53
+ }
54
+ // 当前无 sessionKey → 群 的映射表(spec §1.4:若没有可以不建,直接放弃推群)
55
+ return undefined;
56
+ }
57
+ function buildBlockedNoticeText(payload) {
58
+ if (payload.taskStatus !== "blocked" || payload.blockedNote === undefined) {
59
+ return undefined;
60
+ }
61
+ const note = payload.blockedNote.trim();
62
+ if (note === "") {
63
+ return undefined;
64
+ }
65
+ return `计划「${payload.goal}」卡在任务「${payload.taskTitle}」:${note}\n需要人工介入才能继续。`;
66
+ }
67
+ /**
68
+ * 构造事件订阅的 `handle` 回调,可独立单测(不依赖真实 api/网络)。
69
+ *
70
+ * 降级纪律同 middleware:handle 内全身 try/catch,绝不外抛(与 middleware 同进程)。
71
+ *
72
+ * ⚠️ 当前 OpenClaw 版本下 xg-harness-tools 恒因缺少 runId 而不发送此事件,
73
+ * 此 handle 不会被触发,保留以便未来该限制解除后零改动生效。
74
+ * 不得将其作为任何正确性判断依据——权威状态来自 TASK-2 的 SQLite 只读反查。
75
+ */
76
+ export function createPlanEventHandler(deps) {
77
+ return async (event, _ctx) => {
78
+ try {
79
+ if (!isPlanEventPayload(event.data)) {
80
+ return;
81
+ }
82
+ const payload = event.data;
83
+ const target = resolveDeliveryTarget(payload, { sessionKey: event.sessionKey });
84
+ if (!target) {
85
+ console.warn(`${LOG_PREFIX} 无法定位投递目标,放弃本次推群 planId=${payload.planId} taskIndex=${payload.taskIndex}`);
86
+ return;
87
+ }
88
+ const content = buildBlockedNoticeText(payload);
89
+ if (!content) {
90
+ return;
91
+ }
92
+ await deps.sendText(target.groupId, content);
93
+ }
94
+ catch (err) {
95
+ console.warn(`${LOG_PREFIX} handle 内部异常,已吞: ${String(err)}`);
96
+ }
97
+ };
98
+ }
99
+ /**
100
+ * 注册计划事件订阅(spec §1,前瞻性预留)。
101
+ *
102
+ * ⚠️ 当前 OpenClaw 版本下 xg-harness-tools 恒因缺少 runId 而不发送此事件,
103
+ * 此订阅不会被触发,保留以便未来该限制解除后零改动生效。
104
+ */
105
+ export function registerPlanEventSubscription(api, config) {
106
+ const handle = createPlanEventHandler({
107
+ sendText: async (groupId, content) => {
108
+ const identity = await getToken(config);
109
+ await sendTextMessage(config, identity.token, groupId, content);
110
+ },
111
+ });
112
+ api.agent.events.registerAgentEventSubscription({
113
+ id: "xg-cwork-im-plan-progress",
114
+ description: "订阅 xg-harness-tools 计划进度事件,推送到群。前瞻性预留:" +
115
+ "当前 OpenClaw 版本下 xg-harness-tools 恒因缺少 runId 而不发送此事件," +
116
+ "此订阅暂不会被触发,保留以便未来该限制解除后零改动生效。",
117
+ streams: [planEventStream(HARNESS_PLUGIN_ID)],
118
+ handle,
119
+ });
120
+ }
121
+ //# sourceMappingURL=plan-event-subscription.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan-event-subscription.js","sourceRoot":"","sources":["../../src/plan-event-subscription.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=plan-event-subscription.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan-event-subscription.test.d.ts","sourceRoot":"","sources":["../../src/plan-event-subscription.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,257 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import { createPlanEventHandler } from "./plan-event-subscription.js";
4
+ const FAKE_CTX = {
5
+ getRunContext: () => undefined,
6
+ setRunContext: () => { },
7
+ clearRunContext: () => { },
8
+ };
9
+ function makeBlockedPayload(overrides = {}) {
10
+ return {
11
+ kind: "plan_update",
12
+ planId: "plan-1",
13
+ goal: "迁移配置到新版本",
14
+ action: "advance",
15
+ taskIndex: 0,
16
+ taskTitle: "备份旧配置",
17
+ taskStatus: "blocked",
18
+ blockedNote: "缺少权限",
19
+ summary: { total: 3, completed: 1, blocked: 1, inProgress: 0 },
20
+ runId: "run-1",
21
+ ...overrides,
22
+ };
23
+ }
24
+ class RecordingSendText {
25
+ calls = [];
26
+ impl = async (groupId, content) => {
27
+ this.calls.push({ groupId, content });
28
+ };
29
+ sendText = (groupId, content) => this.impl(groupId, content);
30
+ }
31
+ describe("createPlanEventHandler: isPlanEventPayload 守卫", () => {
32
+ it("rejects malformed data without pushing or throwing", async () => {
33
+ const recorder = new RecordingSendText();
34
+ const handle = createPlanEventHandler({ sendText: recorder.sendText });
35
+ const event = {
36
+ runId: "run-1",
37
+ seq: 1,
38
+ stream: "xg-harness-tools.plan",
39
+ ts: Date.now(),
40
+ data: { kind: "not_plan_update" },
41
+ };
42
+ await assert.doesNotReject(() => Promise.resolve(handle(event, FAKE_CTX)));
43
+ assert.equal(recorder.calls.length, 0);
44
+ });
45
+ it("rejects when data is not an object at all", async () => {
46
+ const recorder = new RecordingSendText();
47
+ const handle = createPlanEventHandler({ sendText: recorder.sendText });
48
+ const event = {
49
+ runId: "run-1",
50
+ seq: 1,
51
+ stream: "xg-harness-tools.plan",
52
+ ts: Date.now(),
53
+ data: null,
54
+ };
55
+ await assert.doesNotReject(() => Promise.resolve(handle(event, FAKE_CTX)));
56
+ assert.equal(recorder.calls.length, 0);
57
+ });
58
+ it("rejects a payload that violates kind only — everything else (routing + copy fields) is valid (pins the guard)", async () => {
59
+ const recorder = new RecordingSendText();
60
+ const handle = createPlanEventHandler({ sendText: recorder.sendText });
61
+ const event = {
62
+ runId: "run-1",
63
+ seq: 1,
64
+ stream: "xg-harness-tools.plan",
65
+ ts: Date.now(),
66
+ data: makeBlockedPayload({
67
+ kind: "some_other_plugin_event", // 唯一的不合法项
68
+ deliveryContext: { to: "group-1" },
69
+ }),
70
+ };
71
+ await assert.doesNotReject(() => Promise.resolve(handle(event, FAKE_CTX)));
72
+ assert.equal(recorder.calls.length, 0);
73
+ });
74
+ it("rejects a payload with a malformed summary — everything else is valid", async () => {
75
+ const recorder = new RecordingSendText();
76
+ const handle = createPlanEventHandler({ sendText: recorder.sendText });
77
+ const event = {
78
+ runId: "run-1",
79
+ seq: 1,
80
+ stream: "xg-harness-tools.plan",
81
+ ts: Date.now(),
82
+ data: makeBlockedPayload({
83
+ summary: {},
84
+ deliveryContext: { to: "group-1" },
85
+ }),
86
+ };
87
+ await assert.doesNotReject(() => Promise.resolve(handle(event, FAKE_CTX)));
88
+ assert.equal(recorder.calls.length, 0);
89
+ });
90
+ });
91
+ describe("createPlanEventHandler: 路由字段取自 event.data 而非顶层", () => {
92
+ it("uses deliveryContext.to from event.data, ignoring a same-shaped decoy on the top-level event", async () => {
93
+ const recorder = new RecordingSendText();
94
+ const handle = createPlanEventHandler({ sendText: recorder.sendText });
95
+ const event = {
96
+ runId: "run-1",
97
+ seq: 1,
98
+ stream: "xg-harness-tools.plan",
99
+ ts: Date.now(),
100
+ sessionKey: "decoy-top-level-session-key",
101
+ // 顶层伪造一个同名但不存在于真实 AgentEventPayload 上的字段,确认代码不会误取
102
+ deliveryContext: { to: "decoy-top-level-group" },
103
+ data: makeBlockedPayload({ deliveryContext: { to: "real-group-from-data" } }),
104
+ };
105
+ await handle(event, FAKE_CTX);
106
+ assert.equal(recorder.calls.length, 1);
107
+ assert.equal(recorder.calls[0].groupId, "real-group-from-data");
108
+ });
109
+ });
110
+ describe("createPlanEventHandler: blocked payload 推送", () => {
111
+ it("assembles the spec §1.3 copy and calls sendText", async () => {
112
+ const recorder = new RecordingSendText();
113
+ const handle = createPlanEventHandler({ sendText: recorder.sendText });
114
+ const event = {
115
+ runId: "run-1",
116
+ seq: 1,
117
+ stream: "xg-harness-tools.plan",
118
+ ts: Date.now(),
119
+ data: makeBlockedPayload({
120
+ deliveryContext: { to: "group-1" },
121
+ goal: "迁移配置到新版本",
122
+ taskTitle: "备份旧配置",
123
+ blockedNote: "缺少权限",
124
+ }),
125
+ };
126
+ await handle(event, FAKE_CTX);
127
+ assert.equal(recorder.calls.length, 1);
128
+ assert.equal(recorder.calls[0].groupId, "group-1");
129
+ assert.equal(recorder.calls[0].content, "计划「迁移配置到新版本」卡在任务「备份旧配置」:缺少权限\n需要人工介入才能继续。");
130
+ });
131
+ it("does not push for non-blocked task status", async () => {
132
+ const recorder = new RecordingSendText();
133
+ const handle = createPlanEventHandler({ sendText: recorder.sendText });
134
+ const event = {
135
+ runId: "run-1",
136
+ seq: 1,
137
+ stream: "xg-harness-tools.plan",
138
+ ts: Date.now(),
139
+ data: makeBlockedPayload({
140
+ deliveryContext: { to: "group-1" },
141
+ taskStatus: "completed",
142
+ blockedNote: undefined,
143
+ }),
144
+ };
145
+ await handle(event, FAKE_CTX);
146
+ assert.equal(recorder.calls.length, 0);
147
+ });
148
+ it("does not push when no delivery target can be resolved", async () => {
149
+ const recorder = new RecordingSendText();
150
+ const handle = createPlanEventHandler({ sendText: recorder.sendText });
151
+ const event = {
152
+ runId: "run-1",
153
+ seq: 1,
154
+ stream: "xg-harness-tools.plan",
155
+ ts: Date.now(),
156
+ data: makeBlockedPayload(), // 无 deliveryContext/agentAccountId/messageChannel/sessionKey
157
+ };
158
+ await handle(event, FAKE_CTX);
159
+ assert.equal(recorder.calls.length, 0);
160
+ });
161
+ it("does not push when taskStatus is not blocked, even if blockedNote still carries a stale value (pins the taskStatus check)", async () => {
162
+ const recorder = new RecordingSendText();
163
+ const handle = createPlanEventHandler({ sendText: recorder.sendText });
164
+ const event = {
165
+ runId: "run-1",
166
+ seq: 1,
167
+ stream: "xg-harness-tools.plan",
168
+ ts: Date.now(),
169
+ data: makeBlockedPayload({
170
+ deliveryContext: { to: "group-1" },
171
+ taskStatus: "completed",
172
+ blockedNote: "缺少权限", // 上游残留的旧阻塞原因,taskStatus 已不再是 blocked
173
+ }),
174
+ };
175
+ await handle(event, FAKE_CTX);
176
+ assert.equal(recorder.calls.length, 0);
177
+ });
178
+ it("does not push when deliveryContext.to is an empty string, everything else valid (pins the empty-string check)", async () => {
179
+ const recorder = new RecordingSendText();
180
+ const handle = createPlanEventHandler({ sendText: recorder.sendText });
181
+ const event = {
182
+ runId: "run-1",
183
+ seq: 1,
184
+ stream: "xg-harness-tools.plan",
185
+ ts: Date.now(),
186
+ data: makeBlockedPayload({
187
+ deliveryContext: { to: "" },
188
+ }),
189
+ };
190
+ await handle(event, FAKE_CTX);
191
+ assert.equal(recorder.calls.length, 0);
192
+ });
193
+ it("does not push when deliveryContext.to is not a string", async () => {
194
+ const recorder = new RecordingSendText();
195
+ const handle = createPlanEventHandler({ sendText: recorder.sendText });
196
+ const event = {
197
+ runId: "run-1",
198
+ seq: 1,
199
+ stream: "xg-harness-tools.plan",
200
+ ts: Date.now(),
201
+ data: makeBlockedPayload({
202
+ deliveryContext: { to: 123 },
203
+ }),
204
+ };
205
+ await handle(event, FAKE_CTX);
206
+ assert.equal(recorder.calls.length, 0);
207
+ });
208
+ it("does not push when deliveryContext itself is not an object", async () => {
209
+ const recorder = new RecordingSendText();
210
+ const handle = createPlanEventHandler({ sendText: recorder.sendText });
211
+ const event = {
212
+ runId: "run-1",
213
+ seq: 1,
214
+ stream: "xg-harness-tools.plan",
215
+ ts: Date.now(),
216
+ data: makeBlockedPayload({
217
+ deliveryContext: "not-an-object",
218
+ }),
219
+ };
220
+ await handle(event, FAKE_CTX);
221
+ assert.equal(recorder.calls.length, 0);
222
+ });
223
+ it("does not push when blockedNote is whitespace-only, everything else valid (pins the trim/blank check)", async () => {
224
+ const recorder = new RecordingSendText();
225
+ const handle = createPlanEventHandler({ sendText: recorder.sendText });
226
+ const event = {
227
+ runId: "run-1",
228
+ seq: 1,
229
+ stream: "xg-harness-tools.plan",
230
+ ts: Date.now(),
231
+ data: makeBlockedPayload({
232
+ deliveryContext: { to: "group-1" },
233
+ blockedNote: " ",
234
+ }),
235
+ };
236
+ await handle(event, FAKE_CTX);
237
+ assert.equal(recorder.calls.length, 0);
238
+ });
239
+ });
240
+ describe("createPlanEventHandler: handle 内部出错不外泄", () => {
241
+ it("resolves normally even when sendText rejects", async () => {
242
+ const recorder = new RecordingSendText();
243
+ recorder.impl = async () => {
244
+ throw new Error("boom");
245
+ };
246
+ const handle = createPlanEventHandler({ sendText: recorder.sendText });
247
+ const event = {
248
+ runId: "run-1",
249
+ seq: 1,
250
+ stream: "xg-harness-tools.plan",
251
+ ts: Date.now(),
252
+ data: makeBlockedPayload({ deliveryContext: { to: "group-1" } }),
253
+ };
254
+ await assert.doesNotReject(() => Promise.resolve(handle(event, FAKE_CTX)));
255
+ });
256
+ });
257
+ //# sourceMappingURL=plan-event-subscription.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan-event-subscription.test.js","sourceRoot":"","sources":["../../src/plan-event-subscription.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import type { BlockedTaskNotice } from "./plan-blocked-notice.js";
2
+ export interface ToolCallTarget {
3
+ groupId: string;
4
+ token: string;
5
+ sessionKey?: string;
6
+ }
7
+ /** 测试用:替换时间源,避免测 TTL 时 sleep。 */
8
+ export declare function setNowFnForTest(fn: () => number): void;
9
+ export declare function registerToolCallTarget(toolCallId: string, target: ToolCallTarget): void;
10
+ /** 取出并删除(一次性消费)。未命中返回 undefined。 */
11
+ export declare function takeToolCallTarget(toolCallId: string): ToolCallTarget | undefined;
12
+ /**
13
+ * 传入本次扫描出的全部 notice,返回其中「需要推送的新增项」,并同步更新集合。
14
+ *
15
+ * 语义(按 DESIGN.md D2):
16
+ * - 新增 = 本次 keys − 已推送 keys → 返回它们
17
+ * - 清理 = 已推送集中「本次已不在」的 key → 移除(让 blocked→修复→再 blocked 能重新推)
18
+ * - 已推送集 = 已推送 ∪ 新增
19
+ * - 按 planId 分桶
20
+ */
21
+ export declare function diffAndMarkBlockedNotices(planId: string, notices: readonly BlockedTaskNotice[]): BlockedTaskNotice[];
22
+ /** 测试用:重置模块级缓存与 stateDir */
23
+ export declare function resetPlanSessionRegistryForTest(): void;
24
+ //# sourceMappingURL=plan-session-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan-session-registry.d.ts","sourceRoot":"","sources":["../../src/plan-session-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAElE,MAAM,WAAW,cAAc;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AAeD,iCAAiC;AACjC,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,MAAM,GAAG,IAAI,CAEtD;AA0BD,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,GAAG,IAAI,CAMvF;AAED,oCAAoC;AACpC,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAUjF;AAED;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CACrC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,SAAS,iBAAiB,EAAE,GACtC,iBAAiB,EAAE,CAsBrB;AAED,4BAA4B;AAC5B,wBAAgB,+BAA+B,IAAI,IAAI,CAItD"}
@@ -0,0 +1,84 @@
1
+ /** toolCallId → 投递目标表的 TTL:超过这个时长未被消费的登记视为过期。 */
2
+ const TOOL_CALL_TARGET_TTL_MS = 10 * 60 * 1000;
3
+ /** toolCallId → 投递目标表的容量上限:超出时逐出最旧的条目(插入序)。 */
4
+ const TOOL_CALL_TARGET_CAPACITY = 500;
5
+ let nowFn = () => Date.now();
6
+ /** 测试用:替换时间源,避免测 TTL 时 sleep。 */
7
+ export function setNowFnForTest(fn) {
8
+ nowFn = fn;
9
+ }
10
+ const toolCallTargets = new Map();
11
+ /** planId → 已推送 blocked notice key 集合 */
12
+ const blockedNotifiedKeys = new Map();
13
+ function evictExpiredToolCallTargets() {
14
+ const now = nowFn();
15
+ for (const [toolCallId, entry] of toolCallTargets) {
16
+ if (now - entry.registeredAt > TOOL_CALL_TARGET_TTL_MS) {
17
+ toolCallTargets.delete(toolCallId);
18
+ }
19
+ }
20
+ }
21
+ function evictOldestToolCallTargetsOverCapacity() {
22
+ while (toolCallTargets.size > TOOL_CALL_TARGET_CAPACITY) {
23
+ const oldestKey = toolCallTargets.keys().next().value;
24
+ if (oldestKey === undefined) {
25
+ break;
26
+ }
27
+ toolCallTargets.delete(oldestKey);
28
+ }
29
+ }
30
+ export function registerToolCallTarget(toolCallId, target) {
31
+ evictExpiredToolCallTargets();
32
+ // 重新登记同一 id 应刷新插入序(视为新条目),先删再插
33
+ toolCallTargets.delete(toolCallId);
34
+ toolCallTargets.set(toolCallId, { target, registeredAt: nowFn() });
35
+ evictOldestToolCallTargetsOverCapacity();
36
+ }
37
+ /** 取出并删除(一次性消费)。未命中返回 undefined。 */
38
+ export function takeToolCallTarget(toolCallId) {
39
+ const entry = toolCallTargets.get(toolCallId);
40
+ if (!entry) {
41
+ return undefined;
42
+ }
43
+ toolCallTargets.delete(toolCallId);
44
+ if (nowFn() - entry.registeredAt > TOOL_CALL_TARGET_TTL_MS) {
45
+ return undefined;
46
+ }
47
+ return entry.target;
48
+ }
49
+ /**
50
+ * 传入本次扫描出的全部 notice,返回其中「需要推送的新增项」,并同步更新集合。
51
+ *
52
+ * 语义(按 DESIGN.md D2):
53
+ * - 新增 = 本次 keys − 已推送 keys → 返回它们
54
+ * - 清理 = 已推送集中「本次已不在」的 key → 移除(让 blocked→修复→再 blocked 能重新推)
55
+ * - 已推送集 = 已推送 ∪ 新增
56
+ * - 按 planId 分桶
57
+ */
58
+ export function diffAndMarkBlockedNotices(planId, notices) {
59
+ const notified = blockedNotifiedKeys.get(planId) ?? new Set();
60
+ const currentKeys = new Set(notices.map((n) => n.key));
61
+ const fresh = notices.filter((n) => !notified.has(n.key));
62
+ for (const key of notified) {
63
+ if (!currentKeys.has(key)) {
64
+ notified.delete(key);
65
+ }
66
+ }
67
+ for (const notice of fresh) {
68
+ notified.add(notice.key);
69
+ }
70
+ if (notified.size > 0) {
71
+ blockedNotifiedKeys.set(planId, notified);
72
+ }
73
+ else {
74
+ blockedNotifiedKeys.delete(planId);
75
+ }
76
+ return fresh;
77
+ }
78
+ /** 测试用:重置模块级缓存与 stateDir */
79
+ export function resetPlanSessionRegistryForTest() {
80
+ toolCallTargets.clear();
81
+ blockedNotifiedKeys.clear();
82
+ nowFn = () => Date.now();
83
+ }
84
+ //# sourceMappingURL=plan-session-registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan-session-registry.js","sourceRoot":"","sources":["../../src/plan-session-registry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=plan-session-registry.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan-session-registry.test.d.ts","sourceRoot":"","sources":["../../src/plan-session-registry.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,109 @@
1
+ import assert from "node:assert/strict";
2
+ import { beforeEach, describe, it } from "node:test";
3
+ import { diffAndMarkBlockedNotices, registerToolCallTarget, resetPlanSessionRegistryForTest, setNowFnForTest, takeToolCallTarget, } from "./plan-session-registry.js";
4
+ function makeTarget(overrides = {}) {
5
+ return {
6
+ groupId: "group-1",
7
+ token: "token-1",
8
+ ...overrides,
9
+ };
10
+ }
11
+ function makeNotice(overrides = {}) {
12
+ return {
13
+ key: "plan-1::0::缺少权限",
14
+ taskIndex: 0,
15
+ taskTitle: "备份旧配置",
16
+ note: "缺少权限",
17
+ content: "计划「X」卡在任务「备份旧配置」:缺少权限\n需要人工介入才能继续。",
18
+ ...overrides,
19
+ };
20
+ }
21
+ beforeEach(() => {
22
+ resetPlanSessionRegistryForTest();
23
+ });
24
+ describe("registerToolCallTarget / takeToolCallTarget", () => {
25
+ it("hits with the registered target and deletes it on consume (take twice → second undefined)", () => {
26
+ registerToolCallTarget("call-1", makeTarget());
27
+ const first = takeToolCallTarget("call-1");
28
+ assert.deepEqual(first, makeTarget());
29
+ const second = takeToolCallTarget("call-1");
30
+ assert.equal(second, undefined);
31
+ });
32
+ it("misses for an unregistered id", () => {
33
+ assert.equal(takeToolCallTarget("does-not-exist"), undefined);
34
+ });
35
+ it("expires entries past TTL (injected now, no sleep)", () => {
36
+ let t = 0;
37
+ setNowFnForTest(() => t);
38
+ registerToolCallTarget("call-ttl", makeTarget());
39
+ t = 10 * 60 * 1000 + 1; // TTL + 1ms
40
+ assert.equal(takeToolCallTarget("call-ttl"), undefined);
41
+ });
42
+ it("does not expire an entry read exactly at TTL boundary", () => {
43
+ let t = 0;
44
+ setNowFnForTest(() => t);
45
+ registerToolCallTarget("call-boundary", makeTarget());
46
+ t = 10 * 60 * 1000; // exactly TTL, not yet exceeded
47
+ assert.ok(takeToolCallTarget("call-boundary"));
48
+ });
49
+ it("evicts the oldest entry when capacity is exceeded", () => {
50
+ let t = 0;
51
+ setNowFnForTest(() => t);
52
+ for (let i = 0; i < 500; i++) {
53
+ t += 1;
54
+ registerToolCallTarget(`call-${i}`, makeTarget({ groupId: `g${i}` }));
55
+ }
56
+ // 表已满 500 条,call-0 是最旧的
57
+ t += 1;
58
+ registerToolCallTarget("call-500", makeTarget({ groupId: "g500" }));
59
+ assert.equal(takeToolCallTarget("call-0"), undefined, "oldest entry should have been evicted");
60
+ const kept = takeToolCallTarget("call-500");
61
+ assert.ok(kept);
62
+ assert.equal(kept.groupId, "g500");
63
+ });
64
+ });
65
+ describe("diffAndMarkBlockedNotices", () => {
66
+ it("returns all notices on first call", () => {
67
+ const notices = [makeNotice()];
68
+ const fresh = diffAndMarkBlockedNotices("plan-1", notices);
69
+ assert.deepEqual(fresh, notices);
70
+ });
71
+ it("returns empty on repeated identical call", () => {
72
+ const notices = [makeNotice()];
73
+ diffAndMarkBlockedNotices("plan-1", notices);
74
+ const second = diffAndMarkBlockedNotices("plan-1", notices);
75
+ assert.deepEqual(second, []);
76
+ });
77
+ it("treats a note change as a new notification (different key)", () => {
78
+ const first = [makeNotice({ key: "plan-1::0::缺少权限" })];
79
+ diffAndMarkBlockedNotices("plan-1", first);
80
+ const changed = [makeNotice({ key: "plan-1::0::权限问题已升级", note: "权限问题已升级" })];
81
+ const fresh = diffAndMarkBlockedNotices("plan-1", changed);
82
+ assert.deepEqual(fresh, changed);
83
+ });
84
+ it("clears a key once the task is no longer blocked, allowing re-notification later", () => {
85
+ const notice = makeNotice({ key: "plan-1::0::缺少权限" });
86
+ diffAndMarkBlockedNotices("plan-1", [notice]);
87
+ // task 修复:本次扫描不再含该 key
88
+ const clearedFresh = diffAndMarkBlockedNotices("plan-1", []);
89
+ assert.deepEqual(clearedFresh, []);
90
+ // 再次 blocked,同 key 应视为新增重新推送
91
+ const reblocked = diffAndMarkBlockedNotices("plan-1", [notice]);
92
+ assert.deepEqual(reblocked, [notice]);
93
+ });
94
+ it("keeps separate planIds independent", () => {
95
+ const noticeA = makeNotice({ key: "plan-a::0::原因A" });
96
+ const noticeB = makeNotice({ key: "plan-b::0::原因B" });
97
+ const freshA = diffAndMarkBlockedNotices("plan-a", [noticeA]);
98
+ const freshB = diffAndMarkBlockedNotices("plan-b", [noticeB]);
99
+ assert.deepEqual(freshA, [noticeA]);
100
+ assert.deepEqual(freshB, [noticeB]);
101
+ // plan-a 重复调用不应受 plan-b 影响,反之亦然
102
+ assert.deepEqual(diffAndMarkBlockedNotices("plan-a", [noticeA]), []);
103
+ assert.deepEqual(diffAndMarkBlockedNotices("plan-b", [noticeB]), []);
104
+ });
105
+ it("returns an empty array and does not throw when there are no notices", () => {
106
+ assert.deepEqual(diffAndMarkBlockedNotices("plan-empty", []), []);
107
+ });
108
+ });
109
+ //# sourceMappingURL=plan-session-registry.test.js.map