@gakr-gakr/qqbot 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/api.ts +56 -0
  2. package/autobot.plugin.json +167 -0
  3. package/channel-plugin-api.ts +1 -0
  4. package/index.ts +33 -0
  5. package/package.json +64 -0
  6. package/runtime-api.ts +9 -0
  7. package/secret-contract-api.ts +5 -0
  8. package/setup-entry.ts +13 -0
  9. package/setup-plugin-api.ts +3 -0
  10. package/skills/qqbot-channel/SKILL.md +262 -0
  11. package/skills/qqbot-channel/references/api_references.md +521 -0
  12. package/skills/qqbot-media/SKILL.md +37 -0
  13. package/skills/qqbot-remind/SKILL.md +153 -0
  14. package/src/bridge/approval/capability.ts +225 -0
  15. package/src/bridge/approval/handler-runtime.ts +204 -0
  16. package/src/bridge/bootstrap.ts +135 -0
  17. package/src/bridge/channel-entry.ts +18 -0
  18. package/src/bridge/commands/framework-context-adapter.ts +60 -0
  19. package/src/bridge/commands/framework-registration.ts +66 -0
  20. package/src/bridge/commands/from-parser.ts +60 -0
  21. package/src/bridge/commands/result-dispatcher.ts +76 -0
  22. package/src/bridge/config-shared.ts +132 -0
  23. package/src/bridge/config.ts +176 -0
  24. package/src/bridge/gateway.ts +178 -0
  25. package/src/bridge/logger.ts +31 -0
  26. package/src/bridge/narrowing.ts +31 -0
  27. package/src/bridge/plugin-version.ts +102 -0
  28. package/src/bridge/runtime.ts +25 -0
  29. package/src/bridge/sdk-adapter.ts +164 -0
  30. package/src/bridge/setup/finalize.ts +144 -0
  31. package/src/bridge/setup/surface.ts +34 -0
  32. package/src/bridge/tools/channel.ts +58 -0
  33. package/src/bridge/tools/index.ts +15 -0
  34. package/src/bridge/tools/remind.ts +91 -0
  35. package/src/channel.setup.ts +33 -0
  36. package/src/channel.ts +399 -0
  37. package/src/config-schema.ts +84 -0
  38. package/src/engine/access/index.ts +2 -0
  39. package/src/engine/access/resolve-policy.ts +30 -0
  40. package/src/engine/access/sender-match.ts +55 -0
  41. package/src/engine/access/types.ts +2 -0
  42. package/src/engine/adapter/audio.port.ts +27 -0
  43. package/src/engine/adapter/commands.port.ts +22 -0
  44. package/src/engine/adapter/history.port.ts +52 -0
  45. package/src/engine/adapter/index.ts +76 -0
  46. package/src/engine/adapter/mention-gate.port.ts +50 -0
  47. package/src/engine/adapter/types.ts +38 -0
  48. package/src/engine/api/api-client.ts +212 -0
  49. package/src/engine/api/media-chunked.ts +644 -0
  50. package/src/engine/api/media.ts +218 -0
  51. package/src/engine/api/messages.ts +293 -0
  52. package/src/engine/api/retry.ts +217 -0
  53. package/src/engine/api/routes.ts +95 -0
  54. package/src/engine/api/token.ts +277 -0
  55. package/src/engine/approval/index.ts +224 -0
  56. package/src/engine/commands/builtin/log-helpers.ts +341 -0
  57. package/src/engine/commands/builtin/register-all.ts +17 -0
  58. package/src/engine/commands/builtin/register-approve.ts +201 -0
  59. package/src/engine/commands/builtin/register-basic.ts +95 -0
  60. package/src/engine/commands/builtin/register-clear-storage.ts +187 -0
  61. package/src/engine/commands/builtin/register-logs.ts +20 -0
  62. package/src/engine/commands/builtin/register-streaming.ts +138 -0
  63. package/src/engine/commands/builtin/state.ts +31 -0
  64. package/src/engine/commands/slash-command-auth.ts +88 -0
  65. package/src/engine/commands/slash-command-handler.ts +168 -0
  66. package/src/engine/commands/slash-command-test-support.ts +39 -0
  67. package/src/engine/commands/slash-commands-impl.ts +61 -0
  68. package/src/engine/commands/slash-commands.ts +202 -0
  69. package/src/engine/config/credential-backup.ts +108 -0
  70. package/src/engine/config/credentials.ts +76 -0
  71. package/src/engine/config/group.ts +227 -0
  72. package/src/engine/config/resolve.ts +283 -0
  73. package/src/engine/config/setup-logic.ts +84 -0
  74. package/src/engine/gateway/active-cfg.ts +52 -0
  75. package/src/engine/gateway/codec.ts +47 -0
  76. package/src/engine/gateway/constants.ts +117 -0
  77. package/src/engine/gateway/event-dispatcher.ts +177 -0
  78. package/src/engine/gateway/gateway-connection.ts +356 -0
  79. package/src/engine/gateway/gateway.ts +267 -0
  80. package/src/engine/gateway/inbound-attachments.ts +360 -0
  81. package/src/engine/gateway/inbound-context.ts +82 -0
  82. package/src/engine/gateway/inbound-pipeline.ts +171 -0
  83. package/src/engine/gateway/interaction-handler.ts +345 -0
  84. package/src/engine/gateway/message-queue.ts +404 -0
  85. package/src/engine/gateway/outbound-dispatch.ts +590 -0
  86. package/src/engine/gateway/reconnect.ts +199 -0
  87. package/src/engine/gateway/stages/access-stage.ts +99 -0
  88. package/src/engine/gateway/stages/assembly-stage.ts +156 -0
  89. package/src/engine/gateway/stages/content-stage.ts +77 -0
  90. package/src/engine/gateway/stages/envelope-stage.ts +144 -0
  91. package/src/engine/gateway/stages/group-gate-stage.ts +223 -0
  92. package/src/engine/gateway/stages/index.ts +18 -0
  93. package/src/engine/gateway/stages/quote-stage.ts +113 -0
  94. package/src/engine/gateway/stages/refidx-stage.ts +62 -0
  95. package/src/engine/gateway/stages/stub-contexts.ts +77 -0
  96. package/src/engine/gateway/types.ts +230 -0
  97. package/src/engine/gateway/typing-keepalive.ts +102 -0
  98. package/src/engine/gateway/ws-client.ts +16 -0
  99. package/src/engine/group/activation.ts +88 -0
  100. package/src/engine/group/history.ts +321 -0
  101. package/src/engine/group/mention.ts +114 -0
  102. package/src/engine/group/message-gating.ts +108 -0
  103. package/src/engine/messaging/decode-media-path.ts +82 -0
  104. package/src/engine/messaging/media-source.ts +210 -0
  105. package/src/engine/messaging/media-type-detect.ts +27 -0
  106. package/src/engine/messaging/outbound-audio-port.ts +38 -0
  107. package/src/engine/messaging/outbound-deliver.ts +810 -0
  108. package/src/engine/messaging/outbound-media-send.ts +658 -0
  109. package/src/engine/messaging/outbound-reply.ts +27 -0
  110. package/src/engine/messaging/outbound-result-helpers.ts +54 -0
  111. package/src/engine/messaging/outbound-types.ts +47 -0
  112. package/src/engine/messaging/outbound.ts +485 -0
  113. package/src/engine/messaging/reply-dispatcher.ts +597 -0
  114. package/src/engine/messaging/reply-limiter.ts +164 -0
  115. package/src/engine/messaging/sender.ts +741 -0
  116. package/src/engine/messaging/streaming-c2c.ts +1192 -0
  117. package/src/engine/messaging/streaming-media-send.ts +544 -0
  118. package/src/engine/messaging/target-parser.ts +104 -0
  119. package/src/engine/ref/format-message-ref.ts +142 -0
  120. package/src/engine/ref/format-ref-entry.ts +27 -0
  121. package/src/engine/ref/store.ts +211 -0
  122. package/src/engine/ref/types.ts +27 -0
  123. package/src/engine/session/known-users.ts +138 -0
  124. package/src/engine/session/session-store.ts +207 -0
  125. package/src/engine/tools/channel-api.ts +244 -0
  126. package/src/engine/tools/remind-logic.ts +377 -0
  127. package/src/engine/types.ts +313 -0
  128. package/src/engine/utils/attachment-tags.ts +174 -0
  129. package/src/engine/utils/audio.ts +525 -0
  130. package/src/engine/utils/data-paths.ts +38 -0
  131. package/src/engine/utils/diagnostics.ts +93 -0
  132. package/src/engine/utils/file-utils.ts +215 -0
  133. package/src/engine/utils/format.ts +70 -0
  134. package/src/engine/utils/image-size.ts +249 -0
  135. package/src/engine/utils/log.ts +77 -0
  136. package/src/engine/utils/media-tags.ts +177 -0
  137. package/src/engine/utils/payload.ts +157 -0
  138. package/src/engine/utils/platform.ts +265 -0
  139. package/src/engine/utils/request-context.ts +60 -0
  140. package/src/engine/utils/string-normalize.ts +91 -0
  141. package/src/engine/utils/stt.ts +103 -0
  142. package/src/engine/utils/text-parsing.ts +155 -0
  143. package/src/engine/utils/upload-cache.ts +96 -0
  144. package/src/engine/utils/voice-text.ts +15 -0
  145. package/src/exec-approvals.ts +237 -0
  146. package/src/qqbot-test-support.ts +29 -0
  147. package/src/secret-contract.ts +82 -0
  148. package/src/types.ts +210 -0
  149. package/tsconfig.json +16 -0
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Active runtime config provider for the QQBot engine.
3
+ *
4
+ * Routing must re-evaluate `bindings[]` on every inbound message so that
5
+ * peer/account binding edits made via the CLI take effect without
6
+ * restarting the gateway. The provider hides the per-event lookup
7
+ * behind a typed seam and falls back to the startup snapshot when the
8
+ * runtime registry getter throws (e.g. snapshot not yet initialised).
9
+ *
10
+ * Issue #69546.
11
+ */
12
+
13
+ import type { AutoBotConfig } from "autobot/plugin-sdk/core";
14
+ import { getRuntimeConfig } from "autobot/plugin-sdk/runtime-config-snapshot";
15
+
16
+ export type GatewayCfg = AutoBotConfig;
17
+
18
+ export type GatewayCfgLoader = () => AutoBotConfig;
19
+
20
+ export interface ActiveCfgProvider {
21
+ getActiveCfg(): AutoBotConfig;
22
+ }
23
+
24
+ export interface ActiveCfgProviderOptions {
25
+ fallback: AutoBotConfig;
26
+ load?: GatewayCfgLoader;
27
+ }
28
+
29
+ export function createActiveCfgProvider(options: ActiveCfgProviderOptions): ActiveCfgProvider {
30
+ const loader = options.load ?? defaultGatewayCfgLoader;
31
+ const fallback = options.fallback;
32
+ return {
33
+ getActiveCfg(): AutoBotConfig {
34
+ return resolveActiveCfg(loader, fallback);
35
+ },
36
+ };
37
+ }
38
+
39
+ export function resolveActiveCfg(
40
+ loader: GatewayCfgLoader,
41
+ fallback: AutoBotConfig,
42
+ ): AutoBotConfig {
43
+ try {
44
+ return loader();
45
+ } catch {
46
+ return fallback;
47
+ }
48
+ }
49
+
50
+ function defaultGatewayCfgLoader(): AutoBotConfig {
51
+ return getRuntimeConfig();
52
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Gateway message decoding utilities.
3
+ *
4
+ * Extracted from `gateway.ts` — handles the various data formats that
5
+ * the QQ Bot WebSocket can deliver (string, Buffer, Buffer[], ArrayBuffer).
6
+ *
7
+ * Zero external dependencies beyond Node.js built-ins.
8
+ */
9
+
10
+ /**
11
+ * Decode raw WebSocket `data` into a UTF-8 string.
12
+ *
13
+ * The QQ Bot gateway can send data as a plain string, a single Buffer,
14
+ * an array of Buffer chunks, an ArrayBuffer, or a typed array view.
15
+ */
16
+ export function decodeGatewayMessageData(data: unknown): string {
17
+ if (typeof data === "string") {
18
+ return data;
19
+ }
20
+ if (Buffer.isBuffer(data)) {
21
+ return data.toString("utf8");
22
+ }
23
+ if (Array.isArray(data) && data.every((chunk) => Buffer.isBuffer(chunk))) {
24
+ return Buffer.concat(data).toString("utf8");
25
+ }
26
+ if (data instanceof ArrayBuffer) {
27
+ return Buffer.from(data).toString("utf8");
28
+ }
29
+ if (ArrayBuffer.isView(data)) {
30
+ return Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString("utf8");
31
+ }
32
+ return "";
33
+ }
34
+
35
+ /**
36
+ * Read the optional `message_scene.ext` array from an event payload.
37
+ *
38
+ * Guild, C2C, and Group events may carry a `message_scene` object
39
+ * with an `ext` string array used for ref-index parsing.
40
+ */
41
+ export function readOptionalMessageSceneExt(event: Record<string, unknown>): string[] | undefined {
42
+ if (!("message_scene" in event)) {
43
+ return undefined;
44
+ }
45
+ const scene = event.message_scene as { ext?: string[] } | undefined;
46
+ return scene?.ext;
47
+ }
@@ -0,0 +1,117 @@
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;
@@ -0,0 +1,177 @@
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
+ }