@openclaw/qqbot 2026.5.2-beta.2 → 2026.5.3-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/dist/api.js +646 -0
  2. package/dist/approval-cg0SVahb.js +94 -0
  3. package/dist/channel-N6Y_Rcjp.js +551 -0
  4. package/dist/channel-plugin-api.js +2 -0
  5. package/dist/channel.setup-ByCRAe5H.js +25 -0
  6. package/dist/config-D6545NkC.js +94 -0
  7. package/dist/config-schema-DFcjQw73.js +309 -0
  8. package/dist/exec-approvals-COUsM6wZ.js +138 -0
  9. package/dist/gateway-CmSUJKSt.js +6726 -0
  10. package/dist/handler-runtime-Bqm6N0WG.js +120 -0
  11. package/dist/index.js +29 -0
  12. package/dist/narrowing-BoieBTIU.js +25 -0
  13. package/dist/outbound-BJfhwrPg.js +1357 -0
  14. package/dist/request-context-DXtpwWui.js +921 -0
  15. package/dist/resolve-D_06fV6-.js +284 -0
  16. package/dist/runtime-BJAS3eXW.js +18 -0
  17. package/dist/runtime-api.js +2 -0
  18. package/dist/secret-contract-api.js +56 -0
  19. package/dist/sender-p-B14eLG.js +1987 -0
  20. package/dist/setup-entry.js +15 -0
  21. package/dist/setup-plugin-api.js +2 -0
  22. package/dist/string-normalize-Ci6NM5DE.js +98 -0
  23. package/dist/target-parser-Y0prnrXD.js +245 -0
  24. package/package.json +15 -6
  25. package/api.ts +0 -56
  26. package/channel-plugin-api.ts +0 -1
  27. package/index.ts +0 -29
  28. package/runtime-api.ts +0 -9
  29. package/setup-entry.ts +0 -9
  30. package/setup-plugin-api.ts +0 -3
  31. package/src/bridge/approval/capability.ts +0 -237
  32. package/src/bridge/approval/handler-runtime.ts +0 -204
  33. package/src/bridge/bootstrap.ts +0 -135
  34. package/src/bridge/channel-entry.ts +0 -18
  35. package/src/bridge/commands/framework-context-adapter.ts +0 -60
  36. package/src/bridge/commands/framework-registration.ts +0 -47
  37. package/src/bridge/commands/from-parser.test.ts +0 -86
  38. package/src/bridge/commands/from-parser.ts +0 -60
  39. package/src/bridge/commands/result-dispatcher.ts +0 -76
  40. package/src/bridge/config-shared.ts +0 -132
  41. package/src/bridge/config.ts +0 -111
  42. package/src/bridge/gateway.ts +0 -174
  43. package/src/bridge/logger.ts +0 -31
  44. package/src/bridge/narrowing.ts +0 -31
  45. package/src/bridge/plugin-version.test.ts +0 -146
  46. package/src/bridge/plugin-version.ts +0 -102
  47. package/src/bridge/runtime.ts +0 -25
  48. package/src/bridge/sdk-adapter.ts +0 -131
  49. package/src/bridge/setup/finalize.ts +0 -144
  50. package/src/bridge/setup/surface.ts +0 -34
  51. package/src/bridge/tools/channel.ts +0 -58
  52. package/src/bridge/tools/index.ts +0 -15
  53. package/src/bridge/tools/remind.test.ts +0 -124
  54. package/src/bridge/tools/remind.ts +0 -91
  55. package/src/channel.setup.ts +0 -33
  56. package/src/channel.ts +0 -288
  57. package/src/command-auth.test.ts +0 -62
  58. package/src/config-schema.ts +0 -84
  59. package/src/config.test.ts +0 -364
  60. package/src/engine/access/access-control.test.ts +0 -198
  61. package/src/engine/access/access-control.ts +0 -226
  62. package/src/engine/access/index.ts +0 -16
  63. package/src/engine/access/resolve-policy.test.ts +0 -59
  64. package/src/engine/access/resolve-policy.ts +0 -57
  65. package/src/engine/access/sender-match.test.ts +0 -60
  66. package/src/engine/access/sender-match.ts +0 -55
  67. package/src/engine/access/types.ts +0 -53
  68. package/src/engine/adapter/audio.port.ts +0 -27
  69. package/src/engine/adapter/commands.port.ts +0 -22
  70. package/src/engine/adapter/history.port.ts +0 -52
  71. package/src/engine/adapter/index.ts +0 -139
  72. package/src/engine/adapter/mention-gate.port.ts +0 -50
  73. package/src/engine/adapter/types.ts +0 -38
  74. package/src/engine/api/api-client.ts +0 -212
  75. package/src/engine/api/media-chunked.test.ts +0 -336
  76. package/src/engine/api/media-chunked.ts +0 -622
  77. package/src/engine/api/media.ts +0 -218
  78. package/src/engine/api/messages.ts +0 -293
  79. package/src/engine/api/retry.ts +0 -217
  80. package/src/engine/api/routes.ts +0 -95
  81. package/src/engine/api/token.ts +0 -271
  82. package/src/engine/approval/index.test.ts +0 -22
  83. package/src/engine/approval/index.ts +0 -224
  84. package/src/engine/commands/builtin/log-helpers.ts +0 -319
  85. package/src/engine/commands/builtin/register-all.ts +0 -17
  86. package/src/engine/commands/builtin/register-approve.ts +0 -201
  87. package/src/engine/commands/builtin/register-basic.ts +0 -95
  88. package/src/engine/commands/builtin/register-clear-storage.ts +0 -187
  89. package/src/engine/commands/builtin/register-logs.ts +0 -20
  90. package/src/engine/commands/builtin/register-streaming.ts +0 -137
  91. package/src/engine/commands/builtin/state.ts +0 -31
  92. package/src/engine/commands/slash-command-auth.ts +0 -48
  93. package/src/engine/commands/slash-command-handler.ts +0 -146
  94. package/src/engine/commands/slash-commands-impl.test.ts +0 -8
  95. package/src/engine/commands/slash-commands-impl.ts +0 -61
  96. package/src/engine/commands/slash-commands.ts +0 -199
  97. package/src/engine/config/credential-backup.test.ts +0 -88
  98. package/src/engine/config/credential-backup.ts +0 -107
  99. package/src/engine/config/credentials.ts +0 -76
  100. package/src/engine/config/group.test.ts +0 -234
  101. package/src/engine/config/group.ts +0 -299
  102. package/src/engine/config/resolve.test.ts +0 -152
  103. package/src/engine/config/resolve.ts +0 -283
  104. package/src/engine/config/setup-logic.ts +0 -84
  105. package/src/engine/engine-import-boundary.test.ts +0 -73
  106. package/src/engine/gateway/codec.ts +0 -47
  107. package/src/engine/gateway/constants.ts +0 -117
  108. package/src/engine/gateway/event-dispatcher.ts +0 -177
  109. package/src/engine/gateway/gateway-connection.ts +0 -371
  110. package/src/engine/gateway/gateway.ts +0 -291
  111. package/src/engine/gateway/inbound-attachments.test.ts +0 -126
  112. package/src/engine/gateway/inbound-attachments.ts +0 -360
  113. package/src/engine/gateway/inbound-context.ts +0 -195
  114. package/src/engine/gateway/inbound-pipeline.self-echo.test.ts +0 -218
  115. package/src/engine/gateway/inbound-pipeline.ts +0 -235
  116. package/src/engine/gateway/interaction-handler.ts +0 -220
  117. package/src/engine/gateway/message-queue.test.ts +0 -282
  118. package/src/engine/gateway/message-queue.ts +0 -499
  119. package/src/engine/gateway/outbound-dispatch.test.ts +0 -231
  120. package/src/engine/gateway/outbound-dispatch.ts +0 -575
  121. package/src/engine/gateway/reconnect.ts +0 -199
  122. package/src/engine/gateway/stages/access-stage.ts +0 -132
  123. package/src/engine/gateway/stages/assembly-stage.ts +0 -156
  124. package/src/engine/gateway/stages/content-stage.test.ts +0 -77
  125. package/src/engine/gateway/stages/content-stage.ts +0 -77
  126. package/src/engine/gateway/stages/envelope-stage.test.ts +0 -152
  127. package/src/engine/gateway/stages/envelope-stage.ts +0 -144
  128. package/src/engine/gateway/stages/group-gate-stage.ts +0 -292
  129. package/src/engine/gateway/stages/index.ts +0 -18
  130. package/src/engine/gateway/stages/quote-stage.ts +0 -113
  131. package/src/engine/gateway/stages/refidx-stage.ts +0 -62
  132. package/src/engine/gateway/stages/stub-contexts.ts +0 -116
  133. package/src/engine/gateway/types.ts +0 -264
  134. package/src/engine/gateway/typing-keepalive.ts +0 -79
  135. package/src/engine/group/activation.test.ts +0 -114
  136. package/src/engine/group/activation.ts +0 -147
  137. package/src/engine/group/history.test.ts +0 -314
  138. package/src/engine/group/history.ts +0 -321
  139. package/src/engine/group/mention.test.ts +0 -141
  140. package/src/engine/group/mention.ts +0 -197
  141. package/src/engine/group/message-gating.test.ts +0 -188
  142. package/src/engine/group/message-gating.ts +0 -216
  143. package/src/engine/messaging/decode-media-path.ts +0 -82
  144. package/src/engine/messaging/media-source.ts +0 -215
  145. package/src/engine/messaging/media-type-detect.ts +0 -37
  146. package/src/engine/messaging/outbound-audio-port.ts +0 -38
  147. package/src/engine/messaging/outbound-deliver.ts +0 -810
  148. package/src/engine/messaging/outbound-media-send.ts +0 -702
  149. package/src/engine/messaging/outbound-reply.ts +0 -27
  150. package/src/engine/messaging/outbound-result-helpers.ts +0 -54
  151. package/src/engine/messaging/outbound-types.ts +0 -45
  152. package/src/engine/messaging/outbound.ts +0 -485
  153. package/src/engine/messaging/reply-dispatcher.ts +0 -597
  154. package/src/engine/messaging/reply-limiter.ts +0 -164
  155. package/src/engine/messaging/sender.ts +0 -729
  156. package/src/engine/messaging/streaming-c2c.ts +0 -1192
  157. package/src/engine/messaging/streaming-media-send.ts +0 -544
  158. package/src/engine/messaging/target-parser.ts +0 -104
  159. package/src/engine/ref/format-message-ref.ts +0 -142
  160. package/src/engine/ref/format-ref-entry.test.ts +0 -60
  161. package/src/engine/ref/format-ref-entry.ts +0 -27
  162. package/src/engine/ref/store.ts +0 -208
  163. package/src/engine/ref/types.ts +0 -27
  164. package/src/engine/session/known-users.ts +0 -137
  165. package/src/engine/session/session-store.ts +0 -204
  166. package/src/engine/tools/channel-api.ts +0 -244
  167. package/src/engine/tools/remind-logic.test.ts +0 -280
  168. package/src/engine/tools/remind-logic.ts +0 -377
  169. package/src/engine/types.ts +0 -313
  170. package/src/engine/utils/attachment-tags.test.ts +0 -186
  171. package/src/engine/utils/attachment-tags.ts +0 -174
  172. package/src/engine/utils/audio.test.ts +0 -250
  173. package/src/engine/utils/audio.ts +0 -585
  174. package/src/engine/utils/data-paths.ts +0 -38
  175. package/src/engine/utils/diagnostics.ts +0 -109
  176. package/src/engine/utils/file-utils.test.ts +0 -72
  177. package/src/engine/utils/file-utils.ts +0 -225
  178. package/src/engine/utils/format.test.ts +0 -68
  179. package/src/engine/utils/format.ts +0 -70
  180. package/src/engine/utils/image-size.test.ts +0 -158
  181. package/src/engine/utils/image-size.ts +0 -249
  182. package/src/engine/utils/log.test.ts +0 -28
  183. package/src/engine/utils/log.ts +0 -61
  184. package/src/engine/utils/media-tags.test.ts +0 -32
  185. package/src/engine/utils/media-tags.ts +0 -177
  186. package/src/engine/utils/payload.test.ts +0 -68
  187. package/src/engine/utils/payload.ts +0 -145
  188. package/src/engine/utils/platform-storage-laziness.test.ts +0 -63
  189. package/src/engine/utils/platform.test.ts +0 -148
  190. package/src/engine/utils/platform.ts +0 -343
  191. package/src/engine/utils/request-context.ts +0 -60
  192. package/src/engine/utils/string-normalize.ts +0 -91
  193. package/src/engine/utils/stt.test.ts +0 -104
  194. package/src/engine/utils/stt.ts +0 -100
  195. package/src/engine/utils/text-parsing.test.ts +0 -29
  196. package/src/engine/utils/text-parsing.ts +0 -155
  197. package/src/engine/utils/upload-cache.ts +0 -96
  198. package/src/engine/utils/voice-text.ts +0 -15
  199. package/src/exec-approvals.ts +0 -218
  200. package/src/manifest-schema.test.ts +0 -56
  201. package/src/qqbot-test-support.ts +0 -29
  202. package/src/types.ts +0 -210
  203. package/tsconfig.json +0 -16
@@ -1,280 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import {
3
- parseRelativeTime,
4
- isCronExpression,
5
- formatDelay,
6
- generateJobName,
7
- buildReminderPrompt,
8
- executeRemind,
9
- executeScheduledRemind,
10
- prepareRemindCronAction,
11
- } from "./remind-logic.js";
12
-
13
- describe("engine/tools/remind-logic", () => {
14
- describe("parseRelativeTime", () => {
15
- it("parses minutes shorthand", () => {
16
- expect(parseRelativeTime("5m")).toBe(5 * 60_000);
17
- });
18
-
19
- it("parses hours shorthand", () => {
20
- expect(parseRelativeTime("1h")).toBe(3_600_000);
21
- });
22
-
23
- it("parses combined hours and minutes", () => {
24
- expect(parseRelativeTime("1h30m")).toBe(90 * 60_000);
25
- });
26
-
27
- it("parses days", () => {
28
- expect(parseRelativeTime("2d")).toBe(2 * 86_400_000);
29
- });
30
-
31
- it("parses seconds", () => {
32
- expect(parseRelativeTime("45s")).toBe(45_000);
33
- });
34
-
35
- it("treats plain numbers as minutes", () => {
36
- expect(parseRelativeTime("10")).toBe(10 * 60_000);
37
- });
38
-
39
- it("returns null for unparseable input", () => {
40
- expect(parseRelativeTime("never")).toBeNull();
41
- });
42
-
43
- it("is case insensitive", () => {
44
- expect(parseRelativeTime("5M")).toBe(5 * 60_000);
45
- });
46
- });
47
-
48
- describe("isCronExpression", () => {
49
- it("detects standard 5-field cron", () => {
50
- expect(isCronExpression("0 8 * * *")).toBe(true);
51
- });
52
-
53
- it("detects weekday range cron", () => {
54
- expect(isCronExpression("0 9 * * 1-5")).toBe(true);
55
- });
56
-
57
- it("rejects short input", () => {
58
- expect(isCronExpression("5m")).toBe(false);
59
- });
60
-
61
- it("rejects too many fields", () => {
62
- expect(isCronExpression("0 0 0 0 0 0 0")).toBe(false);
63
- });
64
- });
65
-
66
- describe("formatDelay", () => {
67
- it("formats seconds", () => {
68
- expect(formatDelay(45_000)).toBe("45s");
69
- });
70
-
71
- it("formats minutes", () => {
72
- expect(formatDelay(300_000)).toBe("5m");
73
- });
74
-
75
- it("formats hours", () => {
76
- expect(formatDelay(3_600_000)).toBe("1h");
77
- });
78
-
79
- it("formats hours and minutes", () => {
80
- expect(formatDelay(5_400_000)).toBe("1h30m");
81
- });
82
- });
83
-
84
- describe("generateJobName", () => {
85
- it("returns short content as-is", () => {
86
- expect(generateJobName("drink water")).toBe("Reminder: drink water");
87
- });
88
-
89
- it("truncates long content", () => {
90
- const long = "a very long reminder content that exceeds twenty characters";
91
- const name = generateJobName(long);
92
- expect(name.length).toBeLessThan(40);
93
- expect(name).toContain("…");
94
- });
95
- });
96
-
97
- describe("buildReminderPrompt", () => {
98
- it("includes the content in the prompt", () => {
99
- const prompt = buildReminderPrompt("drink water");
100
- expect(prompt).toContain("drink water");
101
- });
102
- });
103
-
104
- describe("executeRemind", () => {
105
- it("renders internal scheduling output without exposing cronParams", () => {
106
- const result = executeRemind({ action: "list" });
107
- expect(result.details).toEqual({
108
- _instruction: "Gateway cron action prepared for internal QQ reminder scheduling.",
109
- action: "list",
110
- summary: undefined,
111
- });
112
- expect((result.details as { _instruction: string })._instruction).not.toContain(
113
- "Use the cron tool",
114
- );
115
- expect(result.details).not.toHaveProperty("cronParams");
116
- });
117
- });
118
-
119
- describe("prepareRemindCronAction", () => {
120
- it("returns error when removing without jobId", () => {
121
- const result = prepareRemindCronAction({ action: "remove" });
122
- expect(result).toEqual({
123
- ok: false,
124
- error: "jobId is required when action=remove. Use action=list first.",
125
- });
126
- });
127
-
128
- it("returns error when content is missing for add", () => {
129
- const result = prepareRemindCronAction({ action: "add", to: "qqbot:c2c:123", time: "5m" });
130
- expect(result).toEqual({ ok: false, error: "content is required when action=add" });
131
- });
132
-
133
- it("returns error when delay is too short", () => {
134
- const result = prepareRemindCronAction({
135
- action: "add",
136
- content: "test",
137
- to: "qqbot:c2c:123",
138
- time: "10s",
139
- });
140
- expect(result).toEqual({ ok: false, error: "Reminder delay must be at least 30 seconds" });
141
- });
142
-
143
- it("builds once job with delivery envelope for relative time", () => {
144
- const result = prepareRemindCronAction({
145
- action: "add",
146
- content: "test reminder",
147
- to: "qqbot:c2c:123",
148
- time: "5m",
149
- });
150
- expect(result.ok).toBe(true);
151
- expect(result.ok ? result.cronAction.action : undefined).toBe("add");
152
- const job = result.ok && result.cronAction.action === "add" ? result.cronAction.job : null;
153
- expect(job?.schedule.kind).toBe("at");
154
- expect(job?.payload.kind).toBe("agentTurn");
155
- expect(job?.delivery).toEqual({
156
- mode: "announce",
157
- channel: "qqbot",
158
- to: "qqbot:c2c:123",
159
- accountId: "default",
160
- });
161
- });
162
-
163
- it("builds cron job with delivery envelope for cron expression", () => {
164
- const result = prepareRemindCronAction({
165
- action: "add",
166
- content: "test reminder",
167
- to: "qqbot:c2c:123",
168
- time: "0 8 * * *",
169
- });
170
- expect(result.ok).toBe(true);
171
- const job = result.ok && result.cronAction.action === "add" ? result.cronAction.job : null;
172
- expect(job?.schedule.kind).toBe("cron");
173
- expect(job?.delivery.to).toBe("qqbot:c2c:123");
174
- });
175
-
176
- it("falls back to ctx.fallbackTo when to is omitted", () => {
177
- const result = prepareRemindCronAction(
178
- { action: "add", content: "test", time: "5m" },
179
- { fallbackTo: "qqbot:c2c:ctx-target", fallbackAccountId: "alt" },
180
- );
181
- expect(result.ok).toBe(true);
182
- const job = result.ok && result.cronAction.action === "add" ? result.cronAction.job : null;
183
- expect(job?.delivery.to).toBe("qqbot:c2c:ctx-target");
184
- expect(job?.delivery.accountId).toBe("alt");
185
- });
186
-
187
- it("prefers AI-supplied to over ctx fallback", () => {
188
- const result = prepareRemindCronAction(
189
- { action: "add", content: "test", time: "5m", to: "qqbot:group:ai-chosen" },
190
- { fallbackTo: "qqbot:c2c:ctx-target", fallbackAccountId: "alt" },
191
- );
192
- expect(result.ok).toBe(true);
193
- const job = result.ok && result.cronAction.action === "add" ? result.cronAction.job : null;
194
- expect(job?.delivery.to).toBe("qqbot:group:ai-chosen");
195
- expect(job?.delivery.accountId).toBe("alt");
196
- });
197
-
198
- it("returns error when neither AI nor ctx provides a target", () => {
199
- const result = prepareRemindCronAction({ action: "add", content: "test", time: "5m" });
200
- expect(result).toEqual({
201
- ok: false,
202
- error:
203
- "Unable to determine delivery target for action=add. " +
204
- "The reminder can only be scheduled from within an active conversation.",
205
- });
206
- });
207
- });
208
-
209
- describe("executeScheduledRemind", () => {
210
- it("runs cron.add directly for relative reminders", async () => {
211
- const calls: unknown[] = [];
212
- const result = await executeScheduledRemind(
213
- { action: "add", content: "test reminder", to: "qqbot:c2c:123", time: "5m" },
214
- {},
215
- async (params) => {
216
- calls.push(params);
217
- return { id: "job-1" };
218
- },
219
- );
220
-
221
- expect(calls).toHaveLength(1);
222
- expect(calls[0]).toMatchObject({
223
- action: "add",
224
- job: {
225
- sessionTarget: "isolated",
226
- payload: { kind: "agentTurn" },
227
- delivery: {
228
- mode: "announce",
229
- channel: "qqbot",
230
- to: "qqbot:c2c:123",
231
- accountId: "default",
232
- },
233
- },
234
- });
235
- expect(result.details).toEqual({
236
- ok: true,
237
- action: "add",
238
- summary: '⏰ Reminder in 5m: "test reminder"',
239
- cronResult: { id: "job-1" },
240
- });
241
- });
242
-
243
- it("runs cron list and remove through the scheduler", async () => {
244
- const calls: unknown[] = [];
245
- await executeScheduledRemind({ action: "list" }, {}, async (params) => {
246
- calls.push(params);
247
- return { jobs: [] };
248
- });
249
- await executeScheduledRemind({ action: "remove", jobId: "job-1" }, {}, async (params) => {
250
- calls.push(params);
251
- return { ok: true };
252
- });
253
-
254
- expect(calls).toEqual([{ action: "list" }, { action: "remove", jobId: "job-1" }]);
255
- });
256
-
257
- it("does not call scheduler when validation fails", async () => {
258
- const result = await executeScheduledRemind({ action: "add", time: "5m" }, {}, async () => {
259
- throw new Error("should not run");
260
- });
261
-
262
- expect((result.details as { error: string }).error).toContain("content");
263
- });
264
-
265
- it("returns a clear error when Gateway cron fails", async () => {
266
- const result = await executeScheduledRemind(
267
- { action: "remove", jobId: "job-1" },
268
- {},
269
- async () => {
270
- throw new Error("gateway unavailable");
271
- },
272
- );
273
-
274
- expect(result.details).toEqual({
275
- error: "Failed to run Gateway cron action: gateway unavailable",
276
- action: "remove",
277
- });
278
- });
279
- });
280
- });
@@ -1,377 +0,0 @@
1
- /**
2
- * QQBot reminder tool core logic.
3
- * QQBot 提醒工具核心逻辑。
4
- *
5
- * Pure functions for time parsing, cron detection, job building,
6
- * and remind execution. The framework registration shell
7
- * (bridge/tools/remind.ts) delegates all business logic here and
8
- * supplies request-level context fallbacks (`to`, `accountId`).
9
- */
10
-
11
- /**
12
- * Reminder tool input parameters.
13
- * 提醒工具的输入参数。
14
- */
15
- export interface RemindParams {
16
- action: "add" | "list" | "remove";
17
- content?: string;
18
- to?: string;
19
- time?: string;
20
- timezone?: string;
21
- name?: string;
22
- jobId?: string;
23
- }
24
-
25
- /**
26
- * Context supplied by the bridge layer so the engine can remain free of
27
- * framework / AsyncLocalStorage dependencies. `fallbackTo` and
28
- * `fallbackAccountId` are consulted only when the corresponding AI-supplied
29
- * parameter is missing.
30
- */
31
- interface RemindExecuteContext {
32
- fallbackTo?: string;
33
- fallbackAccountId?: string;
34
- }
35
-
36
- export type RemindCronAction =
37
- | { action: "list" }
38
- | { action: "remove"; jobId: string }
39
- | {
40
- action: "add";
41
- job: ReturnType<typeof buildOnceJob>["job"] | ReturnType<typeof buildCronJob>["job"];
42
- };
43
-
44
- type RemindCronScheduler = (params: RemindCronAction) => Promise<unknown>;
45
-
46
- type RemindCronPlan =
47
- | {
48
- ok: true;
49
- action: RemindParams["action"];
50
- cronAction: RemindCronAction;
51
- summary?: string;
52
- }
53
- | {
54
- ok: false;
55
- error: string;
56
- };
57
-
58
- const PREPARED_CRON_PARAMS_INSTRUCTION =
59
- "Gateway cron action prepared for internal QQ reminder scheduling.";
60
-
61
- /**
62
- * JSON Schema for AI tool parameters (used by framework registration).
63
- * AI Tool 参数的 JSON Schema 定义(供框架注册使用)。
64
- */
65
- export const RemindSchema = {
66
- type: "object",
67
- properties: {
68
- action: {
69
- type: "string",
70
- description:
71
- "Action type. add=create a reminder, list=show reminders, remove=delete a reminder.",
72
- enum: ["add", "list", "remove"],
73
- },
74
- content: {
75
- type: "string",
76
- description:
77
- 'Reminder content, for example "drink water" or "join the meeting". Required when action=add.',
78
- },
79
- to: {
80
- type: "string",
81
- description:
82
- "Optional delivery target. The runtime automatically resolves the current " +
83
- "conversation target, so you usually do not need to supply this. " +
84
- "Direct-message format: qqbot:c2c:user_openid. Group format: qqbot:group:group_openid.",
85
- },
86
- time: {
87
- type: "string",
88
- description:
89
- "Time description. Supported formats:\n" +
90
- '1. Relative time, for example "5m", "1h", "1h30m", or "2d"\n' +
91
- '2. Cron expression, for example "0 8 * * *" or "0 9 * * 1-5"\n' +
92
- "Values containing spaces are treated as cron expressions; everything else is treated as a one-shot relative delay.\n" +
93
- "Required when action=add.",
94
- },
95
- timezone: {
96
- type: "string",
97
- description: 'Timezone used for cron reminders. Defaults to "Asia/Shanghai".',
98
- },
99
- name: {
100
- type: "string",
101
- description: "Optional reminder job name. Defaults to the first 20 characters of content.",
102
- },
103
- jobId: {
104
- type: "string",
105
- description: "Job ID to remove. Required when action=remove; fetch it with list first.",
106
- },
107
- },
108
- required: ["action"],
109
- } as const;
110
-
111
- /**
112
- * Parse a relative time string into milliseconds.
113
- * 解析相对时间字符串为毫秒数。
114
- *
115
- * Supports: "5m", "1h", "1h30m", "2d", "45s", plain number (as minutes).
116
- *
117
- * @returns Milliseconds or null if unparseable.
118
- */
119
- export function parseRelativeTime(timeStr: string): number | null {
120
- const s = timeStr.toLowerCase();
121
- if (/^\d+$/.test(s)) {
122
- return Number.parseInt(s, 10) * 60_000;
123
- }
124
-
125
- let totalMs = 0;
126
- let matched = false;
127
- const regex = /(\d+(?:\.\d+)?)\s*(d|h|m|s)/g;
128
- let match: RegExpExecArray | null;
129
- while ((match = regex.exec(s)) !== null) {
130
- matched = true;
131
- const value = Number.parseFloat(match[1]);
132
- const unit = match[2];
133
- switch (unit) {
134
- case "d":
135
- totalMs += value * 86_400_000;
136
- break;
137
- case "h":
138
- totalMs += value * 3_600_000;
139
- break;
140
- case "m":
141
- totalMs += value * 60_000;
142
- break;
143
- case "s":
144
- totalMs += value * 1_000;
145
- break;
146
- }
147
- }
148
- return matched ? Math.round(totalMs) : null;
149
- }
150
-
151
- /**
152
- * Check whether a time string is a cron expression (3–6 space-separated fields).
153
- * 判断时间字符串是否为 cron 表达式。
154
- */
155
- export function isCronExpression(timeStr: string): boolean {
156
- const parts = timeStr.trim().split(/\s+/);
157
- if (parts.length < 3 || parts.length > 6) {
158
- return false;
159
- }
160
- return parts.every((p) => /^[0-9*?/,LW#-]/.test(p));
161
- }
162
-
163
- /**
164
- * Generate a cron job name from reminder content (first 20 chars).
165
- * 根据提醒内容生成 cron job 名称。
166
- */
167
- export function generateJobName(content: string): string {
168
- const trimmed = content.trim();
169
- const short = trimmed.length > 20 ? `${trimmed.slice(0, 20)}…` : trimmed;
170
- return `Reminder: ${short}`;
171
- }
172
-
173
- /** Build the reminder system prompt sent to the AI. */
174
- export function buildReminderPrompt(content: string): string {
175
- return (
176
- `You are a warm reminder assistant. Please remind the user about: ${content}. ` +
177
- `Requirements: (1) do not reply with HEARTBEAT_OK (2) do not explain who you are ` +
178
- `(3) output a direct and caring reminder message (4) you may add a short encouraging line ` +
179
- `(5) keep it within 2-3 sentences (6) use a small amount of emoji.`
180
- );
181
- }
182
-
183
- /** Build cron job params for a one-shot delayed reminder. */
184
- function buildOnceJob(params: RemindParams, delayMs: number, to: string, accountId: string) {
185
- const atMs = Date.now() + delayMs;
186
- const content = params.content!;
187
- const name = params.name || generateJobName(content);
188
- return {
189
- action: "add" as const,
190
- job: {
191
- name,
192
- schedule: { kind: "at" as const, atMs },
193
- sessionTarget: "isolated" as const,
194
- wakeMode: "now" as const,
195
- deleteAfterRun: true,
196
- payload: {
197
- kind: "agentTurn" as const,
198
- message: buildReminderPrompt(content),
199
- },
200
- delivery: {
201
- mode: "announce" as const,
202
- channel: "qqbot" as const,
203
- to,
204
- accountId,
205
- },
206
- },
207
- };
208
- }
209
-
210
- /** Build cron job params for a recurring cron reminder. */
211
- function buildCronJob(params: RemindParams, to: string, accountId: string) {
212
- const content = params.content!;
213
- const name = params.name || generateJobName(content);
214
- const tz = params.timezone || "Asia/Shanghai";
215
- return {
216
- action: "add" as const,
217
- job: {
218
- name,
219
- schedule: { kind: "cron" as const, expr: params.time!.trim(), tz },
220
- sessionTarget: "isolated" as const,
221
- wakeMode: "now" as const,
222
- payload: {
223
- kind: "agentTurn" as const,
224
- message: buildReminderPrompt(content),
225
- },
226
- delivery: {
227
- mode: "announce" as const,
228
- channel: "qqbot" as const,
229
- to,
230
- accountId,
231
- },
232
- },
233
- };
234
- }
235
-
236
- /** Format a delay in milliseconds as a short string (e.g. "5m", "1h30m"). */
237
- export function formatDelay(ms: number): string {
238
- const totalSeconds = Math.round(ms / 1000);
239
- if (totalSeconds < 60) {
240
- return `${totalSeconds}s`;
241
- }
242
- const totalMinutes = Math.round(ms / 60_000);
243
- if (totalMinutes < 60) {
244
- return `${totalMinutes}m`;
245
- }
246
- const hours = Math.floor(totalMinutes / 60);
247
- const minutes = totalMinutes % 60;
248
- if (minutes === 0) {
249
- return `${hours}h`;
250
- }
251
- return `${hours}h${minutes}m`;
252
- }
253
-
254
- function json(data: unknown) {
255
- return {
256
- content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
257
- details: data,
258
- };
259
- }
260
-
261
- function formatSchedulerError(error: unknown): string {
262
- return error instanceof Error ? error.message : String(error);
263
- }
264
-
265
- export function prepareRemindCronAction(
266
- params: RemindParams,
267
- ctx: RemindExecuteContext = {},
268
- ): RemindCronPlan {
269
- if (params.action === "list") {
270
- return { ok: true, action: "list", cronAction: { action: "list" } };
271
- }
272
-
273
- if (params.action === "remove") {
274
- if (!params.jobId) {
275
- return { ok: false, error: "jobId is required when action=remove. Use action=list first." };
276
- }
277
- return {
278
- ok: true,
279
- action: "remove",
280
- cronAction: { action: "remove", jobId: params.jobId },
281
- };
282
- }
283
-
284
- if (!params.content) {
285
- return { ok: false, error: "content is required when action=add" };
286
- }
287
- const resolvedTo = params.to || ctx.fallbackTo;
288
- if (!resolvedTo) {
289
- return {
290
- ok: false,
291
- error:
292
- "Unable to determine delivery target for action=add. " +
293
- "The reminder can only be scheduled from within an active conversation.",
294
- };
295
- }
296
- if (!params.time) {
297
- return { ok: false, error: "time is required when action=add" };
298
- }
299
- const resolvedAccountId = ctx.fallbackAccountId || "default";
300
-
301
- if (isCronExpression(params.time)) {
302
- return {
303
- ok: true,
304
- action: "add",
305
- cronAction: buildCronJob(params, resolvedTo, resolvedAccountId),
306
- summary: `⏰ Recurring reminder: "${params.content}" (${params.time}, tz=${params.timezone || "Asia/Shanghai"})`,
307
- };
308
- }
309
-
310
- const delayMs = parseRelativeTime(params.time);
311
- if (delayMs == null) {
312
- return {
313
- ok: false,
314
- error: `Could not parse time format: ${params.time}. Use values like 5m, 1h, 1h30m, or a cron expression.`,
315
- };
316
- }
317
- if (delayMs < 30_000) {
318
- return { ok: false, error: "Reminder delay must be at least 30 seconds" };
319
- }
320
-
321
- return {
322
- ok: true,
323
- action: "add",
324
- cronAction: buildOnceJob(params, delayMs, resolvedTo, resolvedAccountId),
325
- summary: `⏰ Reminder in ${formatDelay(delayMs)}: "${params.content}"`,
326
- };
327
- }
328
-
329
- /**
330
- * Execute the reminder tool logic.
331
- * 执行提醒工具逻辑。
332
- *
333
- * Validates params, parses time, and returns a structured result
334
- * containing cron job params that the framework shell passes back
335
- * as the tool output.
336
- *
337
- * When the AI omits `to` / `accountId`, the bridge layer can supply
338
- * `ctx.fallbackTo` / `ctx.fallbackAccountId` (typically resolved from
339
- * the request-scoped AsyncLocalStorage) to fill them in.
340
- */
341
- export function executeRemind(params: RemindParams, ctx: RemindExecuteContext = {}) {
342
- const plan = prepareRemindCronAction(params, ctx);
343
- if (!plan.ok) {
344
- return json({ error: plan.error });
345
- }
346
- return json({
347
- _instruction: PREPARED_CRON_PARAMS_INSTRUCTION,
348
- action: plan.action,
349
- summary: plan.summary,
350
- });
351
- }
352
-
353
- export async function executeScheduledRemind(
354
- params: RemindParams,
355
- ctx: RemindExecuteContext,
356
- scheduler: RemindCronScheduler,
357
- ) {
358
- const plan = prepareRemindCronAction(params, ctx);
359
- if (!plan.ok) {
360
- return json({ error: plan.error });
361
- }
362
-
363
- try {
364
- const cronResult = await scheduler(plan.cronAction);
365
- return json({
366
- ok: true,
367
- action: plan.action,
368
- summary: plan.summary,
369
- cronResult,
370
- });
371
- } catch (error) {
372
- return json({
373
- error: `Failed to run Gateway cron action: ${formatSchedulerError(error)}`,
374
- action: plan.action,
375
- });
376
- }
377
- }