@openclaw/qqbot 2026.5.2 → 2026.5.3-beta.2

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,117 +0,0 @@
1
- /**
2
- * QQ Bot WebSocket Gateway protocol constants.
3
- *
4
- * Extracted from `gateway.ts` to share between both plugin versions.
5
- * Zero external dependencies.
6
- */
7
-
8
- /** QQ Bot WebSocket intents grouped by permission level. */
9
- const INTENTS = {
10
- GUILDS: 1 << 0,
11
- GUILD_MEMBERS: 1 << 1,
12
- PUBLIC_GUILD_MESSAGES: 1 << 30,
13
- DIRECT_MESSAGE: 1 << 12,
14
- GROUP_AND_C2C: 1 << 25,
15
- /** Button interaction callbacks (INTERACTION_CREATE). */
16
- INTERACTION: 1 << 26,
17
- } as const;
18
-
19
- /** Full intent mask: groups + DMs + channels + interaction. */
20
- export const FULL_INTENTS =
21
- INTENTS.PUBLIC_GUILD_MESSAGES |
22
- INTENTS.DIRECT_MESSAGE |
23
- INTENTS.GROUP_AND_C2C |
24
- INTENTS.INTERACTION;
25
-
26
- /** Exponential backoff delays for reconnection attempts (ms). */
27
- export const RECONNECT_DELAYS = [1000, 2000, 5000, 10000, 30000, 60000] as const;
28
-
29
- /** Delay after receiving a rate-limit close code (ms). */
30
- export const RATE_LIMIT_DELAY = 60000;
31
-
32
- /** Maximum reconnection attempts before giving up. */
33
- export const MAX_RECONNECT_ATTEMPTS = 100;
34
-
35
- /** How many quick disconnects before warning about permissions. */
36
- export const MAX_QUICK_DISCONNECT_COUNT = 3;
37
-
38
- /** A disconnect within this window (ms) counts as "quick". */
39
- export const QUICK_DISCONNECT_THRESHOLD = 5000;
40
-
41
- // ============ Opcode Constants ============
42
-
43
- /** Gateway opcodes used by the QQ Bot WebSocket protocol. */
44
- export const GatewayOp = {
45
- /** Server → Client: Dispatch event (type + data). */
46
- DISPATCH: 0,
47
- /** Client → Server: Heartbeat. */
48
- HEARTBEAT: 1,
49
- /** Client → Server: Identify (initial auth). */
50
- IDENTIFY: 2,
51
- /** Client → Server: Resume a dropped session. */
52
- RESUME: 6,
53
- /** Server → Client: Request client to reconnect. */
54
- RECONNECT: 7,
55
- /** Server → Client: Invalid session. */
56
- INVALID_SESSION: 9,
57
- /** Server → Client: Hello (heartbeat interval). */
58
- HELLO: 10,
59
- /** Server → Client: Heartbeat ACK. */
60
- HEARTBEAT_ACK: 11,
61
- } as const;
62
-
63
- // ============ Close Codes ============
64
-
65
- /** WebSocket close codes used by the QQ Gateway. */
66
- export const GatewayCloseCode = {
67
- /** Normal closure — do not reconnect. */
68
- NORMAL: 1000,
69
- /** Authentication failed — refresh token then reconnect. */
70
- AUTH_FAILED: 4004,
71
- /** Session invalid — clear session, refresh token, reconnect. */
72
- INVALID_SESSION: 4006,
73
- /** Sequence number out of range — clear session, refresh token, reconnect. */
74
- SEQ_OUT_OF_RANGE: 4007,
75
- /** Rate limited — wait before reconnecting. */
76
- RATE_LIMITED: 4008,
77
- /** Session timed out — clear session, refresh token, reconnect. */
78
- SESSION_TIMEOUT: 4009,
79
- /** Server internal error (range start) — clear session, refresh token, reconnect. */
80
- SERVER_ERROR_START: 4900,
81
- /** Server internal error (range end). */
82
- SERVER_ERROR_END: 4913,
83
- /** Insufficient intents — fatal, do not reconnect. */
84
- INSUFFICIENT_INTENTS: 4914,
85
- /** Disallowed intents — fatal, do not reconnect. */
86
- DISALLOWED_INTENTS: 4915,
87
- } as const;
88
-
89
- // ============ Dispatch Event Types ============
90
-
91
- /** Event type strings dispatched under opcode 0 (DISPATCH). */
92
- export const GatewayEvent = {
93
- READY: "READY",
94
- RESUMED: "RESUMED",
95
- C2C_MESSAGE_CREATE: "C2C_MESSAGE_CREATE",
96
- AT_MESSAGE_CREATE: "AT_MESSAGE_CREATE",
97
- DIRECT_MESSAGE_CREATE: "DIRECT_MESSAGE_CREATE",
98
- /** Group message that explicitly @-mentions the bot. */
99
- GROUP_AT_MESSAGE_CREATE: "GROUP_AT_MESSAGE_CREATE",
100
- /**
101
- * Group message that does NOT mention the bot. Still dispatched to the
102
- * pipeline so the group history buffer and the `requireMention=false`
103
- * path can observe it.
104
- */
105
- GROUP_MESSAGE_CREATE: "GROUP_MESSAGE_CREATE",
106
- INTERACTION_CREATE: "INTERACTION_CREATE",
107
- } as const;
108
-
109
- // ============ Interaction Type Constants ============
110
-
111
- /** Interaction sub-types carried in `InteractionEvent.data.type`. */
112
- export const InteractionType = {
113
- /** Remote config query — bot reports its current claw_cfg snapshot. */
114
- CONFIG_QUERY: 2001,
115
- /** Remote config update — caller pushes new settings. */
116
- CONFIG_UPDATE: 2002,
117
- } as const;
@@ -1,177 +0,0 @@
1
- /**
2
- * Event dispatcher — convert raw WebSocket op=0 events into QueuedMessage objects.
3
- *
4
- * Pure mapping logic with zero side effects (except known-user recording).
5
- * Independently testable.
6
- */
7
-
8
- import { recordKnownUser } from "../session/known-users.js";
9
- import type { InteractionEvent } from "../types.js";
10
- import { parseRefIndices } from "../utils/text-parsing.js";
11
- import { readOptionalMessageSceneExt } from "./codec.js";
12
- import { GatewayEvent } from "./constants.js";
13
- import type { QueuedMessage } from "./message-queue.js";
14
- import type {
15
- C2CMessageEvent,
16
- GuildMessageEvent,
17
- GroupMessageEvent,
18
- EngineLogger,
19
- } from "./types.js";
20
-
21
- // ============ Dispatch result ============
22
-
23
- type DispatchResult =
24
- | { action: "ready"; data: unknown; sessionId: string }
25
- | { action: "resumed"; data: unknown }
26
- | { action: "message"; msg: QueuedMessage }
27
- | { action: "interaction"; event: InteractionEvent }
28
- | { action: "ignore" };
29
-
30
- // ============ dispatchEvent ============
31
-
32
- /**
33
- * Map a raw op=0 event into a structured dispatch result.
34
- *
35
- * Returns "message" for events that should be queued for processing,
36
- * "ready"/"resumed" for session lifecycle events, and "ignore" otherwise.
37
- */
38
- export function dispatchEvent(
39
- eventType: string,
40
- data: unknown,
41
- accountId: string,
42
- _log?: EngineLogger,
43
- ): DispatchResult {
44
- if (eventType === GatewayEvent.READY) {
45
- const d = data as { session_id: string };
46
- return { action: "ready", data, sessionId: d.session_id };
47
- }
48
-
49
- if (eventType === GatewayEvent.RESUMED) {
50
- return { action: "resumed", data };
51
- }
52
-
53
- if (eventType === GatewayEvent.C2C_MESSAGE_CREATE) {
54
- const ev = data as C2CMessageEvent;
55
- recordKnownUser({
56
- openid: ev.author.user_openid,
57
- type: "c2c",
58
- accountId,
59
- });
60
- const refs = parseRefIndices(ev.message_scene?.ext, ev.message_type, ev.msg_elements);
61
- return {
62
- action: "message",
63
- msg: {
64
- type: "c2c",
65
- senderId: ev.author.user_openid,
66
- content: ev.content,
67
- messageId: ev.id,
68
- timestamp: ev.timestamp,
69
- attachments: ev.attachments,
70
- refMsgIdx: refs.refMsgIdx,
71
- msgIdx: refs.msgIdx,
72
- msgType: ev.message_type,
73
- msgElements: ev.msg_elements,
74
- },
75
- };
76
- }
77
-
78
- if (eventType === GatewayEvent.AT_MESSAGE_CREATE) {
79
- const ev = data as GuildMessageEvent;
80
- const refs = parseRefIndices(
81
- readOptionalMessageSceneExt(ev as unknown as Record<string, unknown>),
82
- );
83
- return {
84
- action: "message",
85
- msg: {
86
- type: "guild",
87
- senderId: ev.author.id,
88
- senderName: ev.author.username,
89
- content: ev.content,
90
- messageId: ev.id,
91
- timestamp: ev.timestamp,
92
- channelId: ev.channel_id,
93
- guildId: ev.guild_id,
94
- attachments: ev.attachments,
95
- refMsgIdx: refs.refMsgIdx,
96
- msgIdx: refs.msgIdx,
97
- },
98
- };
99
- }
100
-
101
- if (eventType === GatewayEvent.DIRECT_MESSAGE_CREATE) {
102
- const ev = data as GuildMessageEvent;
103
- const refs = parseRefIndices(
104
- readOptionalMessageSceneExt(ev as unknown as Record<string, unknown>),
105
- );
106
- return {
107
- action: "message",
108
- msg: {
109
- type: "dm",
110
- senderId: ev.author.id,
111
- senderName: ev.author.username,
112
- content: ev.content,
113
- messageId: ev.id,
114
- timestamp: ev.timestamp,
115
- guildId: ev.guild_id,
116
- attachments: ev.attachments,
117
- refMsgIdx: refs.refMsgIdx,
118
- msgIdx: refs.msgIdx,
119
- },
120
- };
121
- }
122
-
123
- if (eventType === GatewayEvent.GROUP_AT_MESSAGE_CREATE) {
124
- return { action: "message", msg: buildGroupQueuedMessage(data, accountId, eventType) };
125
- }
126
-
127
- if (eventType === GatewayEvent.GROUP_MESSAGE_CREATE) {
128
- return { action: "message", msg: buildGroupQueuedMessage(data, accountId, eventType) };
129
- }
130
-
131
- if (eventType === GatewayEvent.INTERACTION_CREATE) {
132
- return { action: "interaction", event: data as InteractionEvent };
133
- }
134
-
135
- return { action: "ignore" };
136
- }
137
-
138
- /**
139
- * Build a {@link QueuedMessage} from a raw QQ group event payload.
140
- *
141
- * Used for both `GROUP_AT_MESSAGE_CREATE` (bot was @-ed) and
142
- * `GROUP_MESSAGE_CREATE` (non-@ background chatter). The only difference
143
- * between the two is the carried `eventType` — downstream gating uses
144
- * that to decide whether to treat the message as a bot-directed turn.
145
- */
146
- function buildGroupQueuedMessage(
147
- data: unknown,
148
- accountId: string,
149
- eventType: string,
150
- ): QueuedMessage {
151
- const ev = data as GroupMessageEvent;
152
- recordKnownUser({
153
- openid: ev.author.member_openid,
154
- type: "group",
155
- groupOpenid: ev.group_openid,
156
- accountId,
157
- });
158
- const refs = parseRefIndices(ev.message_scene?.ext, ev.message_type, ev.msg_elements);
159
- return {
160
- type: "group",
161
- senderId: ev.author.member_openid,
162
- senderName: ev.author.username,
163
- senderIsBot: ev.author.bot,
164
- content: ev.content,
165
- messageId: ev.id,
166
- timestamp: ev.timestamp,
167
- groupOpenid: ev.group_openid,
168
- attachments: ev.attachments,
169
- refMsgIdx: refs.refMsgIdx,
170
- msgIdx: refs.msgIdx,
171
- msgType: ev.message_type,
172
- msgElements: ev.msg_elements,
173
- eventType,
174
- mentions: ev.mentions,
175
- messageScene: ev.message_scene,
176
- };
177
- }
@@ -1,371 +0,0 @@
1
- /**
2
- * GatewayConnection — WebSocket lifecycle, heartbeat, reconnect, and session persistence.
3
- *
4
- * Encapsulates all connection state as class fields (replaces 11 closure variables).
5
- * Event handling and message processing are delegated to injected handlers.
6
- */
7
-
8
- import WebSocket from "ws";
9
- import {
10
- trySlashCommand,
11
- type SlashCommandHandlerContext,
12
- } from "../commands/slash-command-handler.js";
13
- import {
14
- clearTokenCache,
15
- getAccessToken,
16
- getGatewayUrl,
17
- getPluginUserAgent,
18
- startBackgroundTokenRefresh,
19
- stopBackgroundTokenRefresh,
20
- } from "../messaging/sender.js";
21
- import { flushRefIndex } from "../ref/store.js";
22
- import { flushKnownUsers } from "../session/known-users.js";
23
- import { clearSession, loadSession, saveSession } from "../session/session-store.js";
24
- import type { InteractionEvent } from "../types.js";
25
- import { decodeGatewayMessageData } from "./codec.js";
26
- import { FULL_INTENTS, RATE_LIMIT_DELAY, GatewayOp } from "./constants.js";
27
- import { dispatchEvent } from "./event-dispatcher.js";
28
- import { createMessageQueue, type QueuedMessage } from "./message-queue.js";
29
- import { ReconnectState } from "./reconnect.js";
30
- import type { GatewayAccount, EngineLogger, GatewayPluginRuntime, WSPayload } from "./types.js";
31
-
32
- // ============ Connection context ============
33
-
34
- interface GatewayConnectionContext {
35
- account: GatewayAccount;
36
- abortSignal: AbortSignal;
37
- cfg: unknown;
38
- log?: EngineLogger;
39
- runtime: GatewayPluginRuntime;
40
- onReady?: (data: unknown) => void;
41
- /** Called when a RESUMED event is received (reconnect success). */
42
- onResumed?: (data: unknown) => void;
43
- onError?: (error: Error) => void;
44
- /** Process a queued message (inbound pipeline → outbound dispatch). */
45
- handleMessage: (event: QueuedMessage) => Promise<void>;
46
- /** Called when an INTERACTION_CREATE event is received (e.g. approval button clicks). */
47
- onInteraction?: (event: InteractionEvent) => void;
48
- }
49
-
50
- // ============ GatewayConnection ============
51
-
52
- export class GatewayConnection {
53
- // ---- Connection state ----
54
- private isAborted = false;
55
- private currentWs: WebSocket | null = null;
56
- private heartbeatInterval: ReturnType<typeof setInterval> | null = null;
57
- private sessionId: string | null = null;
58
- private lastSeq: number | null = null;
59
- private isConnecting = false;
60
- private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
61
- private shouldRefreshToken = false;
62
-
63
- private readonly reconnect: ReconnectState;
64
- private readonly msgQueue;
65
- private readonly ctx: GatewayConnectionContext;
66
-
67
- constructor(ctx: GatewayConnectionContext) {
68
- this.ctx = ctx;
69
- this.reconnect = new ReconnectState(ctx.account.accountId, ctx.log);
70
- this.msgQueue = createMessageQueue({
71
- accountId: ctx.account.accountId,
72
- log: ctx.log,
73
- isAborted: () => this.isAborted,
74
- });
75
- }
76
-
77
- /** Start the connection loop. Resolves when abortSignal fires. */
78
- async start(): Promise<void> {
79
- this.restoreSession();
80
- this.registerAbortHandler();
81
- await this.connect();
82
- return new Promise<void>((resolve) => {
83
- this.ctx.abortSignal.addEventListener("abort", () => resolve());
84
- });
85
- }
86
-
87
- // ============ Session persistence ============
88
-
89
- private restoreSession(): void {
90
- const { account, log } = this.ctx;
91
- const saved = loadSession(account.accountId, account.appId);
92
- if (saved) {
93
- this.sessionId = saved.sessionId;
94
- this.lastSeq = saved.lastSeq;
95
- log?.info(`Restored session: sessionId=${this.sessionId}, lastSeq=${this.lastSeq}`);
96
- }
97
- }
98
-
99
- private saveCurrentSession(): void {
100
- const { account } = this.ctx;
101
- if (!this.sessionId) {
102
- return;
103
- }
104
- saveSession({
105
- sessionId: this.sessionId,
106
- lastSeq: this.lastSeq,
107
- lastConnectedAt: Date.now(),
108
- intentLevelIndex: 0,
109
- accountId: account.accountId,
110
- savedAt: Date.now(),
111
- appId: account.appId,
112
- });
113
- }
114
-
115
- // ============ Abort + cleanup ============
116
-
117
- private registerAbortHandler(): void {
118
- const { account, abortSignal, log: _log } = this.ctx;
119
- abortSignal.addEventListener("abort", () => {
120
- this.isAborted = true;
121
- if (this.reconnectTimer) {
122
- clearTimeout(this.reconnectTimer);
123
- this.reconnectTimer = null;
124
- }
125
- this.cleanup();
126
- stopBackgroundTokenRefresh(account.appId);
127
- flushKnownUsers();
128
- flushRefIndex();
129
- });
130
- }
131
-
132
- private cleanup(): void {
133
- if (this.heartbeatInterval) {
134
- clearInterval(this.heartbeatInterval);
135
- this.heartbeatInterval = null;
136
- }
137
- if (
138
- this.currentWs &&
139
- (this.currentWs.readyState === WebSocket.OPEN ||
140
- this.currentWs.readyState === WebSocket.CONNECTING)
141
- ) {
142
- this.currentWs.close();
143
- }
144
- this.currentWs = null;
145
- }
146
-
147
- // ============ Reconnect ============
148
-
149
- private scheduleReconnect(customDelay?: number): void {
150
- const { account: _account, log } = this.ctx;
151
- if (this.isAborted || this.reconnect.isExhausted()) {
152
- log?.error(`Max reconnect attempts reached or aborted`);
153
- return;
154
- }
155
- if (this.reconnectTimer) {
156
- clearTimeout(this.reconnectTimer);
157
- this.reconnectTimer = null;
158
- }
159
- const delay = this.reconnect.getNextDelay(customDelay);
160
- this.reconnectTimer = setTimeout(() => {
161
- this.reconnectTimer = null;
162
- if (!this.isAborted) {
163
- void this.connect();
164
- }
165
- }, delay);
166
- }
167
-
168
- // ============ Connect ============
169
-
170
- private async connect(): Promise<void> {
171
- const { account, log } = this.ctx;
172
-
173
- if (this.isConnecting) {
174
- log?.debug?.(`Already connecting, skip`);
175
- return;
176
- }
177
- this.isConnecting = true;
178
-
179
- try {
180
- this.cleanup();
181
- if (this.shouldRefreshToken) {
182
- log?.debug?.(`Refreshing token...`);
183
- clearTokenCache(account.appId);
184
- this.shouldRefreshToken = false;
185
- }
186
-
187
- const accessToken = await getAccessToken(account.appId, account.clientSecret);
188
- log?.info(`✅ Access token obtained successfully`);
189
- const gatewayUrl = await getGatewayUrl(accessToken, account.appId);
190
- log?.info(`Connecting to ${gatewayUrl}`);
191
-
192
- const ws = new WebSocket(gatewayUrl, {
193
- headers: { "User-Agent": getPluginUserAgent() },
194
- });
195
- this.currentWs = ws;
196
-
197
- // ---- Slash command interception ----
198
- const slashCtx: SlashCommandHandlerContext = {
199
- account,
200
- log,
201
- getMessagePeerId: (msg) => this.msgQueue.getMessagePeerId(msg),
202
- getQueueSnapshot: (peerId) => this.msgQueue.getSnapshot(peerId),
203
- };
204
-
205
- const trySlashCommandOrEnqueue = async (msg: QueuedMessage): Promise<void> => {
206
- const result = await trySlashCommand(msg, slashCtx);
207
- if (result === "enqueue") {
208
- this.msgQueue.enqueue(msg);
209
- } else if (result === "urgent") {
210
- const peerId = this.msgQueue.getMessagePeerId(msg);
211
- this.msgQueue.clearUserQueue(peerId);
212
- this.msgQueue.executeImmediate(msg);
213
- }
214
- // "handled" — command executed, nothing to queue.
215
- };
216
-
217
- // ---- WebSocket: open ----
218
- ws.on("open", () => {
219
- log?.info(`WebSocket connected`);
220
- this.isConnecting = false;
221
- this.reconnect.onConnected();
222
- this.msgQueue.startProcessor(this.ctx.handleMessage);
223
- startBackgroundTokenRefresh(account.appId, account.clientSecret, { log });
224
- });
225
-
226
- // ---- WebSocket: message ----
227
- ws.on("message", async (data) => {
228
- try {
229
- const rawData = decodeGatewayMessageData(data);
230
- const payload = JSON.parse(rawData) as WSPayload;
231
- const { op, d, s, t } = payload;
232
-
233
- if (s) {
234
- this.lastSeq = s;
235
- this.saveCurrentSession();
236
- }
237
-
238
- switch (op) {
239
- case GatewayOp.HELLO:
240
- this.handleHello(ws, d, accessToken);
241
- break;
242
-
243
- case GatewayOp.DISPATCH: {
244
- log?.debug?.(`Dispatch event: t=${t}, d=${JSON.stringify(d)}`);
245
- const result = dispatchEvent(t ?? "", d, account.accountId, log);
246
- if (result.action === "ready") {
247
- this.sessionId = result.sessionId;
248
- this.saveCurrentSession();
249
- this.ctx.onReady?.(result.data);
250
- } else if (result.action === "resumed") {
251
- (this.ctx.onResumed ?? this.ctx.onReady)?.(result.data);
252
- this.saveCurrentSession();
253
- } else if (result.action === "interaction") {
254
- this.ctx.onInteraction?.(result.event);
255
- } else if (result.action === "message") {
256
- void trySlashCommandOrEnqueue(result.msg);
257
- }
258
- break;
259
- }
260
-
261
- case GatewayOp.HEARTBEAT_ACK:
262
- break;
263
-
264
- case GatewayOp.RECONNECT:
265
- this.cleanup();
266
- this.scheduleReconnect();
267
- break;
268
-
269
- case GatewayOp.INVALID_SESSION: {
270
- const canResume = d as boolean;
271
- if (!canResume) {
272
- this.sessionId = null;
273
- this.lastSeq = null;
274
- clearSession(account.accountId);
275
- this.shouldRefreshToken = true;
276
- }
277
- this.cleanup();
278
- this.scheduleReconnect(3000);
279
- break;
280
- }
281
- }
282
- } catch (err) {
283
- log?.error(`Message parse error: ${err instanceof Error ? err.message : String(err)}`);
284
- }
285
- });
286
-
287
- // ---- WebSocket: close ----
288
- ws.on("close", (code, reason) => {
289
- log?.info(`WebSocket closed: ${code} ${reason.toString()}`);
290
- this.isConnecting = false;
291
- this.handleClose(code);
292
- });
293
-
294
- // ---- WebSocket: error ----
295
- ws.on("error", (err) => {
296
- log?.error(`WebSocket error: ${err.message}`);
297
- this.ctx.onError?.(err);
298
- });
299
- } catch (err) {
300
- this.isConnecting = false;
301
- const errMsg = err instanceof Error ? err.message : String(err);
302
- log?.error(`Connection failed: ${errMsg}`);
303
- if (errMsg.includes("Too many requests") || errMsg.includes("100001")) {
304
- this.scheduleReconnect(RATE_LIMIT_DELAY);
305
- } else {
306
- this.scheduleReconnect();
307
- }
308
- }
309
- }
310
-
311
- // ============ Protocol handlers ============
312
-
313
- private handleHello(ws: WebSocket, d: unknown, accessToken: string): void {
314
- if (this.sessionId && this.lastSeq !== null) {
315
- ws.send(
316
- JSON.stringify({
317
- op: GatewayOp.RESUME,
318
- d: {
319
- token: `QQBot ${accessToken}`,
320
- session_id: this.sessionId,
321
- seq: this.lastSeq,
322
- },
323
- }),
324
- );
325
- } else {
326
- ws.send(
327
- JSON.stringify({
328
- op: GatewayOp.IDENTIFY,
329
- d: {
330
- token: `QQBot ${accessToken}`,
331
- intents: FULL_INTENTS,
332
- shard: [0, 1],
333
- },
334
- }),
335
- );
336
- }
337
-
338
- const interval = (d as { heartbeat_interval: number }).heartbeat_interval;
339
- if (this.heartbeatInterval) {
340
- clearInterval(this.heartbeatInterval);
341
- }
342
- this.heartbeatInterval = setInterval(() => {
343
- if (ws.readyState === WebSocket.OPEN) {
344
- ws.send(JSON.stringify({ op: GatewayOp.HEARTBEAT, d: this.lastSeq }));
345
- }
346
- }, interval);
347
- }
348
-
349
- private handleClose(code: number): void {
350
- const { account } = this.ctx;
351
- const action = this.reconnect.handleClose(code, this.isAborted);
352
-
353
- if (action.clearSession) {
354
- this.sessionId = null;
355
- this.lastSeq = null;
356
- clearSession(account.accountId);
357
- }
358
- if (action.refreshToken) {
359
- this.shouldRefreshToken = true;
360
- }
361
-
362
- this.cleanup();
363
-
364
- if (action.fatal) {
365
- return;
366
- }
367
- if (action.shouldReconnect) {
368
- this.scheduleReconnect(action.reconnectDelay);
369
- }
370
- }
371
- }