@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.
- package/dist/api.js +646 -0
- package/dist/approval-cg0SVahb.js +94 -0
- package/dist/channel-N6Y_Rcjp.js +551 -0
- package/dist/channel-plugin-api.js +2 -0
- package/dist/channel.setup-ByCRAe5H.js +25 -0
- package/dist/config-D6545NkC.js +94 -0
- package/dist/config-schema-DFcjQw73.js +309 -0
- package/dist/exec-approvals-COUsM6wZ.js +138 -0
- package/dist/gateway-CmSUJKSt.js +6726 -0
- package/dist/handler-runtime-Bqm6N0WG.js +120 -0
- package/dist/index.js +29 -0
- package/dist/narrowing-BoieBTIU.js +25 -0
- package/dist/outbound-BJfhwrPg.js +1357 -0
- package/dist/request-context-DXtpwWui.js +921 -0
- package/dist/resolve-D_06fV6-.js +284 -0
- package/dist/runtime-BJAS3eXW.js +18 -0
- package/dist/runtime-api.js +2 -0
- package/dist/secret-contract-api.js +56 -0
- package/dist/sender-p-B14eLG.js +1987 -0
- package/dist/setup-entry.js +15 -0
- package/dist/setup-plugin-api.js +2 -0
- package/dist/string-normalize-Ci6NM5DE.js +98 -0
- package/dist/target-parser-Y0prnrXD.js +245 -0
- package/package.json +15 -6
- package/api.ts +0 -56
- package/channel-plugin-api.ts +0 -1
- package/index.ts +0 -29
- package/runtime-api.ts +0 -9
- package/setup-entry.ts +0 -9
- package/setup-plugin-api.ts +0 -3
- package/src/bridge/approval/capability.ts +0 -237
- package/src/bridge/approval/handler-runtime.ts +0 -204
- package/src/bridge/bootstrap.ts +0 -135
- package/src/bridge/channel-entry.ts +0 -18
- package/src/bridge/commands/framework-context-adapter.ts +0 -60
- package/src/bridge/commands/framework-registration.ts +0 -47
- package/src/bridge/commands/from-parser.test.ts +0 -86
- package/src/bridge/commands/from-parser.ts +0 -60
- package/src/bridge/commands/result-dispatcher.ts +0 -76
- package/src/bridge/config-shared.ts +0 -132
- package/src/bridge/config.ts +0 -111
- package/src/bridge/gateway.ts +0 -174
- package/src/bridge/logger.ts +0 -31
- package/src/bridge/narrowing.ts +0 -31
- package/src/bridge/plugin-version.test.ts +0 -146
- package/src/bridge/plugin-version.ts +0 -102
- package/src/bridge/runtime.ts +0 -25
- package/src/bridge/sdk-adapter.ts +0 -131
- package/src/bridge/setup/finalize.ts +0 -144
- package/src/bridge/setup/surface.ts +0 -34
- package/src/bridge/tools/channel.ts +0 -58
- package/src/bridge/tools/index.ts +0 -15
- package/src/bridge/tools/remind.test.ts +0 -124
- package/src/bridge/tools/remind.ts +0 -91
- package/src/channel.setup.ts +0 -33
- package/src/channel.ts +0 -288
- package/src/command-auth.test.ts +0 -62
- package/src/config-schema.ts +0 -84
- package/src/config.test.ts +0 -364
- package/src/engine/access/access-control.test.ts +0 -198
- package/src/engine/access/access-control.ts +0 -226
- package/src/engine/access/index.ts +0 -16
- package/src/engine/access/resolve-policy.test.ts +0 -59
- package/src/engine/access/resolve-policy.ts +0 -57
- package/src/engine/access/sender-match.test.ts +0 -60
- package/src/engine/access/sender-match.ts +0 -55
- package/src/engine/access/types.ts +0 -53
- package/src/engine/adapter/audio.port.ts +0 -27
- package/src/engine/adapter/commands.port.ts +0 -22
- package/src/engine/adapter/history.port.ts +0 -52
- package/src/engine/adapter/index.ts +0 -139
- package/src/engine/adapter/mention-gate.port.ts +0 -50
- package/src/engine/adapter/types.ts +0 -38
- package/src/engine/api/api-client.ts +0 -212
- package/src/engine/api/media-chunked.test.ts +0 -336
- package/src/engine/api/media-chunked.ts +0 -622
- package/src/engine/api/media.ts +0 -218
- package/src/engine/api/messages.ts +0 -293
- package/src/engine/api/retry.ts +0 -217
- package/src/engine/api/routes.ts +0 -95
- package/src/engine/api/token.ts +0 -271
- package/src/engine/approval/index.test.ts +0 -22
- package/src/engine/approval/index.ts +0 -224
- package/src/engine/commands/builtin/log-helpers.ts +0 -319
- package/src/engine/commands/builtin/register-all.ts +0 -17
- package/src/engine/commands/builtin/register-approve.ts +0 -201
- package/src/engine/commands/builtin/register-basic.ts +0 -95
- package/src/engine/commands/builtin/register-clear-storage.ts +0 -187
- package/src/engine/commands/builtin/register-logs.ts +0 -20
- package/src/engine/commands/builtin/register-streaming.ts +0 -137
- package/src/engine/commands/builtin/state.ts +0 -31
- package/src/engine/commands/slash-command-auth.ts +0 -48
- package/src/engine/commands/slash-command-handler.ts +0 -146
- package/src/engine/commands/slash-commands-impl.test.ts +0 -8
- package/src/engine/commands/slash-commands-impl.ts +0 -61
- package/src/engine/commands/slash-commands.ts +0 -199
- package/src/engine/config/credential-backup.test.ts +0 -88
- package/src/engine/config/credential-backup.ts +0 -107
- package/src/engine/config/credentials.ts +0 -76
- package/src/engine/config/group.test.ts +0 -234
- package/src/engine/config/group.ts +0 -299
- package/src/engine/config/resolve.test.ts +0 -152
- package/src/engine/config/resolve.ts +0 -283
- package/src/engine/config/setup-logic.ts +0 -84
- package/src/engine/engine-import-boundary.test.ts +0 -73
- package/src/engine/gateway/codec.ts +0 -47
- package/src/engine/gateway/constants.ts +0 -117
- package/src/engine/gateway/event-dispatcher.ts +0 -177
- package/src/engine/gateway/gateway-connection.ts +0 -371
- package/src/engine/gateway/gateway.ts +0 -291
- package/src/engine/gateway/inbound-attachments.test.ts +0 -126
- package/src/engine/gateway/inbound-attachments.ts +0 -360
- package/src/engine/gateway/inbound-context.ts +0 -195
- package/src/engine/gateway/inbound-pipeline.self-echo.test.ts +0 -218
- package/src/engine/gateway/inbound-pipeline.ts +0 -235
- package/src/engine/gateway/interaction-handler.ts +0 -220
- package/src/engine/gateway/message-queue.test.ts +0 -282
- package/src/engine/gateway/message-queue.ts +0 -499
- package/src/engine/gateway/outbound-dispatch.test.ts +0 -231
- package/src/engine/gateway/outbound-dispatch.ts +0 -575
- package/src/engine/gateway/reconnect.ts +0 -199
- package/src/engine/gateway/stages/access-stage.ts +0 -132
- package/src/engine/gateway/stages/assembly-stage.ts +0 -156
- package/src/engine/gateway/stages/content-stage.test.ts +0 -77
- package/src/engine/gateway/stages/content-stage.ts +0 -77
- package/src/engine/gateway/stages/envelope-stage.test.ts +0 -152
- package/src/engine/gateway/stages/envelope-stage.ts +0 -144
- package/src/engine/gateway/stages/group-gate-stage.ts +0 -292
- package/src/engine/gateway/stages/index.ts +0 -18
- package/src/engine/gateway/stages/quote-stage.ts +0 -113
- package/src/engine/gateway/stages/refidx-stage.ts +0 -62
- package/src/engine/gateway/stages/stub-contexts.ts +0 -116
- package/src/engine/gateway/types.ts +0 -264
- package/src/engine/gateway/typing-keepalive.ts +0 -79
- package/src/engine/group/activation.test.ts +0 -114
- package/src/engine/group/activation.ts +0 -147
- package/src/engine/group/history.test.ts +0 -314
- package/src/engine/group/history.ts +0 -321
- package/src/engine/group/mention.test.ts +0 -141
- package/src/engine/group/mention.ts +0 -197
- package/src/engine/group/message-gating.test.ts +0 -188
- package/src/engine/group/message-gating.ts +0 -216
- package/src/engine/messaging/decode-media-path.ts +0 -82
- package/src/engine/messaging/media-source.ts +0 -215
- package/src/engine/messaging/media-type-detect.ts +0 -37
- package/src/engine/messaging/outbound-audio-port.ts +0 -38
- package/src/engine/messaging/outbound-deliver.ts +0 -810
- package/src/engine/messaging/outbound-media-send.ts +0 -702
- package/src/engine/messaging/outbound-reply.ts +0 -27
- package/src/engine/messaging/outbound-result-helpers.ts +0 -54
- package/src/engine/messaging/outbound-types.ts +0 -45
- package/src/engine/messaging/outbound.ts +0 -485
- package/src/engine/messaging/reply-dispatcher.ts +0 -597
- package/src/engine/messaging/reply-limiter.ts +0 -164
- package/src/engine/messaging/sender.ts +0 -729
- package/src/engine/messaging/streaming-c2c.ts +0 -1192
- package/src/engine/messaging/streaming-media-send.ts +0 -544
- package/src/engine/messaging/target-parser.ts +0 -104
- package/src/engine/ref/format-message-ref.ts +0 -142
- package/src/engine/ref/format-ref-entry.test.ts +0 -60
- package/src/engine/ref/format-ref-entry.ts +0 -27
- package/src/engine/ref/store.ts +0 -208
- package/src/engine/ref/types.ts +0 -27
- package/src/engine/session/known-users.ts +0 -137
- package/src/engine/session/session-store.ts +0 -204
- package/src/engine/tools/channel-api.ts +0 -244
- package/src/engine/tools/remind-logic.test.ts +0 -280
- package/src/engine/tools/remind-logic.ts +0 -377
- package/src/engine/types.ts +0 -313
- package/src/engine/utils/attachment-tags.test.ts +0 -186
- package/src/engine/utils/attachment-tags.ts +0 -174
- package/src/engine/utils/audio.test.ts +0 -250
- package/src/engine/utils/audio.ts +0 -585
- package/src/engine/utils/data-paths.ts +0 -38
- package/src/engine/utils/diagnostics.ts +0 -109
- package/src/engine/utils/file-utils.test.ts +0 -72
- package/src/engine/utils/file-utils.ts +0 -225
- package/src/engine/utils/format.test.ts +0 -68
- package/src/engine/utils/format.ts +0 -70
- package/src/engine/utils/image-size.test.ts +0 -158
- package/src/engine/utils/image-size.ts +0 -249
- package/src/engine/utils/log.test.ts +0 -28
- package/src/engine/utils/log.ts +0 -61
- package/src/engine/utils/media-tags.test.ts +0 -32
- package/src/engine/utils/media-tags.ts +0 -177
- package/src/engine/utils/payload.test.ts +0 -68
- package/src/engine/utils/payload.ts +0 -145
- package/src/engine/utils/platform-storage-laziness.test.ts +0 -63
- package/src/engine/utils/platform.test.ts +0 -148
- package/src/engine/utils/platform.ts +0 -343
- package/src/engine/utils/request-context.ts +0 -60
- package/src/engine/utils/string-normalize.ts +0 -91
- package/src/engine/utils/stt.test.ts +0 -104
- package/src/engine/utils/stt.ts +0 -100
- package/src/engine/utils/text-parsing.test.ts +0 -29
- package/src/engine/utils/text-parsing.ts +0 -155
- package/src/engine/utils/upload-cache.ts +0 -96
- package/src/engine/utils/voice-text.ts +0 -15
- package/src/exec-approvals.ts +0 -218
- package/src/manifest-schema.test.ts +0 -56
- package/src/qqbot-test-support.ts +0 -29
- package/src/types.ts +0 -210
- 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
|
-
}
|