@zhin.js/adapter-qq 1.0.84 → 1.1.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 (107) hide show
  1. package/CHANGELOG.md +940 -17
  2. package/PERMITS.md +25 -0
  3. package/README.md +135 -233
  4. package/adapters/qq/index.js +50 -0
  5. package/adapters/qq/index.ts +71 -0
  6. package/agents/qq/agent.json +20 -0
  7. package/agents/qq/boundaries.md +3 -0
  8. package/agents/qq/conventions.md +3 -0
  9. package/agents/qq/skills/qq/SKILL.md +22 -0
  10. package/agents/qq/skills/qq/tools/add_role/index.js +21 -0
  11. package/agents/qq/skills/qq/tools/add_role/index.ts +21 -0
  12. package/agents/qq/skills/qq/tools/channel_info/index.js +17 -0
  13. package/agents/qq/skills/qq/tools/channel_info/index.ts +17 -0
  14. package/agents/qq/skills/qq/tools/create_role/index.js +20 -0
  15. package/agents/qq/skills/qq/tools/create_role/index.ts +20 -0
  16. package/agents/qq/skills/qq/tools/list_channels/index.js +18 -0
  17. package/agents/qq/skills/qq/tools/list_channels/index.ts +18 -0
  18. package/agents/qq/skills/qq/tools/list_guilds/index.js +14 -0
  19. package/agents/qq/skills/qq/tools/list_guilds/index.ts +14 -0
  20. package/agents/qq/skills/qq/tools/list_roles/index.js +18 -0
  21. package/agents/qq/skills/qq/tools/list_roles/index.ts +18 -0
  22. package/agents/qq/skills/qq/tools/member_detail/index.js +18 -0
  23. package/agents/qq/skills/qq/tools/member_detail/index.ts +18 -0
  24. package/agents/qq/skills/qq/tools/remove_role/index.js +21 -0
  25. package/agents/qq/skills/qq/tools/remove_role/index.ts +21 -0
  26. package/agents/qq/system.md +3 -0
  27. package/commands/qq/endpoint/add/[[id]]/index.js +3 -0
  28. package/commands/qq/endpoint/add/[[id]]/index.ts +3 -0
  29. package/commands/qq/endpoint/cancel/index.js +11 -0
  30. package/commands/qq/endpoint/cancel/index.ts +15 -0
  31. package/commands/qq/endpoint/definition.js +188 -0
  32. package/commands/qq/endpoint/definition.ts +263 -0
  33. package/commands/qq/endpoint/list/index.js +3 -0
  34. package/commands/qq/endpoint/list/index.ts +3 -0
  35. package/commands/qq/endpoint/remove/[id]/index.js +3 -0
  36. package/commands/qq/endpoint/remove/[id]/index.ts +3 -0
  37. package/lib/client.d.ts +12 -0
  38. package/lib/client.js +2 -0
  39. package/lib/endpoint.d.ts +56 -0
  40. package/lib/endpoint.js +456 -0
  41. package/lib/index.d.ts +8 -18
  42. package/lib/index.js +8 -338
  43. package/lib/outbound.d.ts +17 -0
  44. package/lib/outbound.js +234 -0
  45. package/lib/platform-permit.d.ts +15 -0
  46. package/lib/platform-permit.js +41 -0
  47. package/lib/protocol.d.ts +90 -0
  48. package/lib/protocol.js +161 -0
  49. package/lib/qq-bind-api.d.ts +36 -0
  50. package/lib/qq-bind-api.js +89 -0
  51. package/lib/qq-bind-flow.d.ts +19 -0
  52. package/lib/qq-bind-flow.js +85 -0
  53. package/lib/qq-bot-kind-prompt.d.ts +9 -0
  54. package/lib/qq-bot-kind-prompt.js +37 -0
  55. package/lib/qq-intents.d.ts +27 -0
  56. package/lib/qq-intents.js +51 -0
  57. package/lib/qq-runtime-state.d.ts +30 -0
  58. package/lib/qq-runtime-state.js +14 -0
  59. package/lib/side-event-dispatch.d.ts +10 -0
  60. package/lib/side-event-dispatch.js +114 -0
  61. package/lib/webhook.d.ts +18 -0
  62. package/lib/webhook.js +103 -0
  63. package/lib/ws.d.ts +55 -0
  64. package/lib/ws.js +234 -0
  65. package/middlewares/bot-kind-reply/index.js +47 -0
  66. package/middlewares/bot-kind-reply/index.ts +65 -0
  67. package/package.json +82 -29
  68. package/plugin.js +18 -0
  69. package/schema.json +158 -0
  70. package/src/client.ts +16 -0
  71. package/src/endpoint.ts +534 -0
  72. package/src/index.ts +66 -326
  73. package/src/outbound.ts +255 -0
  74. package/src/platform-permit.ts +56 -0
  75. package/src/protocol.ts +249 -0
  76. package/src/qq-bind-api.ts +145 -0
  77. package/src/qq-bind-flow.ts +127 -0
  78. package/src/qq-bot-kind-prompt.ts +47 -0
  79. package/src/qq-intents.ts +77 -0
  80. package/src/qq-runtime-state.ts +51 -0
  81. package/src/side-event-dispatch.ts +140 -0
  82. package/src/webhook.ts +132 -0
  83. package/src/ws.ts +331 -0
  84. package/client/Dashboard.tsx +0 -230
  85. package/client/index.tsx +0 -11
  86. package/client/tsconfig.json +0 -7
  87. package/client/utils/api.ts +0 -17
  88. package/dist/index.js +0 -30
  89. package/lib/adapter.d.ts +0 -17
  90. package/lib/adapter.d.ts.map +0 -1
  91. package/lib/adapter.js +0 -49
  92. package/lib/adapter.js.map +0 -1
  93. package/lib/bot.d.ts +0 -102
  94. package/lib/bot.d.ts.map +0 -1
  95. package/lib/bot.js +0 -337
  96. package/lib/bot.js.map +0 -1
  97. package/lib/index.d.ts.map +0 -1
  98. package/lib/index.js.map +0 -1
  99. package/lib/types.d.ts +0 -16
  100. package/lib/types.d.ts.map +0 -1
  101. package/lib/types.js +0 -2
  102. package/lib/types.js.map +0 -1
  103. package/plugin.yml +0 -3
  104. package/skills/qq/SKILL.md +0 -97
  105. package/src/adapter.ts +0 -58
  106. package/src/bot.ts +0 -347
  107. package/src/types.ts +0 -24
@@ -0,0 +1,534 @@
1
+ /**
2
+ * QQ endpoints — lifecycle, outbound, admit, agent tool surface.
3
+ */
4
+ import {
5
+ Endpoint, createEndpointLifecycle, type EndpointLifecycle,
6
+ createRecallEndpointControl,
7
+ type EndpointControl,
8
+ type EndpointChannel,
9
+ type EndpointManagement,
10
+ type EndpointSendRequest,
11
+ } from 'zhin.js/adapter';
12
+ import type { HttpHost } from '@zhin.js/host-http';
13
+ import { formatCompact, getAdapterLogger, truncatePreview } from '@zhin.js/logger';
14
+ import type { CapabilityId } from 'zhin.js';
15
+ import { formatOutbound } from './outbound.js';
16
+ import {
17
+ formatInboundContent,
18
+ parseCompoundMessageId,
19
+ qqInboundConversation,
20
+ qqOutboundKind,
21
+ resolveOutboundMessageId,
22
+ senderDisplayName,
23
+ type QqInboundMessage,
24
+ type ResolvedQqHttpConfig,
25
+ type ResolvedQqWebsocketConfig,
26
+ } from './protocol.js';
27
+ import {
28
+ defaultCreateHttpBot,
29
+ registerQqWebhookRoutes,
30
+ type CreateQqHttpBot,
31
+ type QqHttpBotTransport,
32
+ } from './webhook.js';
33
+ import {
34
+ bindQqBotInboundEvents,
35
+ defaultCreateBot,
36
+ normalizeQqMessage,
37
+ type CreateQqBot,
38
+ type QqBotTransport,
39
+ } from './ws.js';
40
+ import {
41
+ bindQqBotSideEvents,
42
+ receiveQqSideEvent,
43
+ type QqSideEventCaller,
44
+ } from './side-event-dispatch.js';
45
+
46
+ export type { CreateQqBot, QqBotTransport, QqOfficialApi } from './ws.js';
47
+ export type { CreateQqHttpBot, QqHttpBotTransport } from './webhook.js';
48
+
49
+ export interface QqEndpointOptions {
50
+ readonly id: CapabilityId;
51
+ readonly config: ResolvedQqWebsocketConfig;
52
+ readonly createBot?: CreateQqBot;
53
+ }
54
+
55
+ export class QqWebsocketEndpoint extends Endpoint<QqBotTransport> {
56
+ readonly #logger!: ReturnType<typeof getAdapterLogger>;
57
+
58
+ readonly #options: QqEndpointOptions;
59
+ readonly #createBot: CreateQqBot;
60
+ #bot: QqBotTransport | null = null;
61
+ #open = false;
62
+ readonly #lifecycle: EndpointLifecycle;
63
+ #cleanup?: () => Promise<void>;
64
+ readonly management: EndpointManagement = createQqEndpointManagement(() => this.#requireBot());
65
+ readonly control: EndpointControl = createRecallEndpointControl((id) => this.recallMessage(id));
66
+
67
+ constructor(options: QqEndpointOptions) {
68
+ super();
69
+ this.#logger = getAdapterLogger('qq', options.config.id);
70
+ this.#lifecycle = createEndpointLifecycle({ name: options.config.id, reconnect: false });
71
+ this.#options = options;
72
+ this.#createBot = options.createBot ?? defaultCreateBot;
73
+ }
74
+
75
+ get client(): QqBotTransport {
76
+ return this.#requireBot();
77
+ }
78
+
79
+ /** Live endpoint 名(Console/AdapterIndex 展示用,如 bot appid 别名)。 */
80
+ get name(): string {
81
+ return this.#options.config.id;
82
+ }
83
+
84
+ async start(): Promise<void> {
85
+ await this.#lifecycle.start(async (handle) => {
86
+ const bot = this.#createBot(this.#options.config);
87
+ this.#bot = bot;
88
+ let cancelled = false;
89
+ let stopping: Promise<void> | undefined;
90
+ const stopBot = async () => {
91
+ try { bot.removeAllListeners(); } catch { /* continue SDK cleanup */ }
92
+ try { await bot.stop(); } catch { /* best-effort SDK cleanup */ }
93
+ };
94
+ const cleanup = () => {
95
+ cancelled = true;
96
+ if (this.#bot === bot) this.#bot = null;
97
+ return stopping ??= stopBot();
98
+ };
99
+ this.#cleanup = cleanup;
100
+ handle.onForceClose(() => { void cleanup(); });
101
+ try {
102
+ this.#bindBot(bot);
103
+ await bot.start();
104
+ if (cancelled) {
105
+ // SDK startup may create resources after stop; close this exact old client again.
106
+ await stopping;
107
+ await stopBot();
108
+ return;
109
+ }
110
+ this.#logger.info(`connected (websocket) | appid: ${this.#options.config.appid}`);
111
+ } catch (error) {
112
+ const wasCancelled = cancelled;
113
+ await cleanup();
114
+ if (wasCancelled) {
115
+ await stopBot();
116
+ return;
117
+ }
118
+ const raw = error instanceof Error ? error.message : String(error);
119
+ throw new Error(
120
+ `QQ WebSocket 连接失败:请检查 appid/secret 是否配对、网关地址(gatewayUrl/accessTokenUrl)是否可达(原始错误:${raw})`,
121
+ { cause: error },
122
+ );
123
+ }
124
+ });
125
+ }
126
+
127
+ open(): void {
128
+ this.#open = true;
129
+ }
130
+
131
+ close(): void {
132
+ this.#open = false;
133
+ }
134
+
135
+ async stop(): Promise<void> {
136
+ this.#open = false;
137
+ const cleanup = this.#cleanup;
138
+ this.#cleanup = undefined;
139
+ await this.#lifecycle.stop();
140
+ await cleanup?.();
141
+ this.#logger.debug(formatCompact({ op: 'disconnect' }));
142
+ }
143
+
144
+ async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
145
+ const body = formatOutbound(payload);
146
+ const kind = qqOutboundKind(conversation);
147
+ const bot = this.#requireBot();
148
+ let result: unknown;
149
+ switch (kind) {
150
+ case 'private':
151
+ result = await bot.sendPrivateMessage(conversation.id, body);
152
+ break;
153
+ case 'group':
154
+ result = await bot.sendGroupMessage(conversation.id, body);
155
+ break;
156
+ case 'channel':
157
+ result = await bot.sendGuildMessage(conversation.id, body);
158
+ break;
159
+ case 'direct':
160
+ if (!bot.sendDirectMessage) throw new Error('QQ direct message not supported by transport');
161
+ result = await bot.sendDirectMessage(conversation.id, body);
162
+ break;
163
+ }
164
+ const messageId = `${kind}-${conversation.id}:${resolveOutboundMessageId(result)}`;
165
+ this.#logger.info(
166
+ `send ${kind}:${conversation.id}`
167
+ + ` | id: ${messageId}`
168
+ + ` | ${truncatePreview(typeof body === 'string' ? body : String(body), 80)}`,
169
+ );
170
+ return messageId;
171
+ }
172
+
173
+ async recallMessage(messageId: string): Promise<void> {
174
+ const { kind, channelId, qqMsgId } = parseCompoundMessageId(messageId);
175
+ const bot = this.#requireBot();
176
+ switch (kind) {
177
+ case 'private':
178
+ await bot.recallPrivateMessage?.(channelId, qqMsgId);
179
+ break;
180
+ case 'group':
181
+ await bot.recallGroupMessage?.(channelId, qqMsgId);
182
+ break;
183
+ case 'channel':
184
+ await bot.recallGuildMessage?.(channelId, qqMsgId);
185
+ break;
186
+ case 'direct':
187
+ await bot.recallDirectMessage?.(channelId, qqMsgId);
188
+ break;
189
+ }
190
+ this.#logger.debug(formatCompact({
191
+ op: 'qq_recall',
192
+ endpoint: this.#options.config.id,
193
+ kind,
194
+ messageId,
195
+ }));
196
+ }
197
+
198
+ /** Test / internal: admit a message when open. */
199
+ admit(msg: QqInboundMessage): void {
200
+ if (!this.#open) return;
201
+ const conversation = qqInboundConversation(String(this.#options.id), msg);
202
+ const content = formatInboundContent(msg);
203
+ this.#logger.info(
204
+ `recv ${conversation.kind}:${conversation.id}`
205
+ + (msg.authorId ? ` from ${msg.authorId}` : '')
206
+ + (msg.mentioned ? ' (mentioned)' : '')
207
+ + ` | ${truncatePreview(content, 80)}`,
208
+ );
209
+ void this.emit('message.receive', {
210
+ conversation,
211
+ message: { conversation, id: msg.id },
212
+ content,
213
+ ...(msg.segments?.length ? { segments: msg.segments } : {}),
214
+ sender: {
215
+ id: msg.authorId,
216
+ name: senderDisplayName(msg) || undefined,
217
+ ...(msg.authorRoles?.length ? { roles: msg.authorRoles } : {}),
218
+ },
219
+ endpointId: this.#options.config.id,
220
+ ...(msg.mentioned ? { mentioned: true } : {}),
221
+ metadata: Object.freeze({
222
+ channelKind: msg.channelKind,
223
+ userId: msg.authorId,
224
+ guildId: msg.guildId,
225
+ roles: msg.authorRoles,
226
+ }),
227
+ }).catch((err) => {
228
+ this.#logger.warn(formatCompact({
229
+ op: 'qq_gateway_receive_failed',
230
+ target: `${conversation.kind}:${conversation.id}`,
231
+ error: err instanceof Error ? err.message : String(err),
232
+ }));
233
+ });
234
+ }
235
+
236
+ #bindBot(bot: QqBotTransport): void {
237
+ bindQqBotInboundEvents(bot, (raw) => {
238
+ if (this.#bot !== bot) return;
239
+ this.#emitPlatformEvent('message', raw);
240
+ const msg = normalizeQqMessage(raw);
241
+ if (msg) this.admit(msg);
242
+ });
243
+ bindQqBotSideEvents(bot, (eventName, raw) => {
244
+ if (this.#bot !== bot) return;
245
+ this.#emitPlatformEvent(eventName, raw);
246
+ receiveQqSideEvent(
247
+ (name, payload) => this.emit(name, payload),
248
+ this.#options.config.id,
249
+ bot as QqSideEventCaller,
250
+ eventName,
251
+ raw,
252
+ this.#logger,
253
+ );
254
+ });
255
+ }
256
+
257
+ #emitPlatformEvent(name: string, event: unknown): void {
258
+ void this.emitPlatform(name, event).catch((error) => {
259
+ this.#logger.warn(formatCompact({
260
+ op: 'qq_platform_event_failed',
261
+ event: name,
262
+ error: error instanceof Error ? error.message : String(error),
263
+ }));
264
+ });
265
+ }
266
+
267
+ #requireBot(): QqBotTransport {
268
+ if (!this.#bot) throw new Error('QQ bot not connected');
269
+ return this.#bot;
270
+ }
271
+ }
272
+
273
+ export interface QqHttpEndpointOptions {
274
+ readonly id: CapabilityId;
275
+ readonly http: HttpHost;
276
+ readonly config: ResolvedQqHttpConfig;
277
+ readonly createBot?: CreateQqHttpBot;
278
+ }
279
+
280
+ /** Webhook / middleware inbound via httpHostToken POST (qq-official-bot Middleware receiver). */
281
+ export class QqHttpEndpoint extends Endpoint<QqHttpBotTransport> {
282
+ readonly #logger!: ReturnType<typeof getAdapterLogger>;
283
+
284
+ readonly #options: QqHttpEndpointOptions;
285
+ readonly #createBot: CreateQqHttpBot;
286
+ #bot: QqHttpBotTransport | null = null;
287
+ #open = false;
288
+ readonly #lifecycle: EndpointLifecycle;
289
+ #cleanup?: () => Promise<void>;
290
+ readonly management: EndpointManagement = createQqEndpointManagement(() => this.#requireBot());
291
+ readonly control: EndpointControl = createRecallEndpointControl((id) => this.recallMessage(id));
292
+
293
+ constructor(options: QqHttpEndpointOptions) {
294
+ super();
295
+ this.#logger = getAdapterLogger('qq', options.config.id);
296
+ this.#lifecycle = createEndpointLifecycle({ name: options.config.id, reconnect: false });
297
+ this.#options = options;
298
+ this.#createBot = options.createBot ?? defaultCreateHttpBot;
299
+ }
300
+
301
+ get client(): QqHttpBotTransport {
302
+ return this.#requireBot();
303
+ }
304
+
305
+ /** Live endpoint 名(Console/AdapterIndex 展示用)。 */
306
+ get name(): string {
307
+ return this.#options.config.id;
308
+ }
309
+
310
+ async start(): Promise<void> {
311
+ await this.#lifecycle.start(async (handle) => {
312
+ const bot = this.#createBot(this.#options.config);
313
+ this.#bot = bot;
314
+ let cancelled = false;
315
+ let stopping: Promise<void> | undefined;
316
+ const routes: Array<() => void> = [];
317
+ const stopBot = async () => {
318
+ try { bot.removeAllListeners(); } catch { /* continue SDK cleanup */ }
319
+ try { await bot.stop(); } catch { /* best-effort SDK cleanup */ }
320
+ };
321
+ const cleanup = () => {
322
+ cancelled = true;
323
+ if (this.#bot === bot) this.#bot = null;
324
+ for (const release of routes.splice(0)) release();
325
+ return stopping ??= stopBot();
326
+ };
327
+ this.#cleanup = cleanup;
328
+ handle.onForceClose(() => { void cleanup(); });
329
+ try {
330
+ this.#bindBot(bot);
331
+ routes.push(...registerQqWebhookRoutes(this.#options.http, {
332
+ config: this.#options.config,
333
+ getBot: () => cancelled ? null : bot,
334
+ }));
335
+ await bot.start();
336
+ if (cancelled) {
337
+ // SDK startup may create resources after stop; close this exact old client again.
338
+ await stopping;
339
+ await stopBot();
340
+ return;
341
+ }
342
+ this.#logger.info(`connected (${this.#options.config.mode}) | path: ${this.#options.config.webhookPath}`);
343
+ } catch (error) {
344
+ const wasCancelled = cancelled;
345
+ await cleanup();
346
+ if (wasCancelled) {
347
+ await stopBot();
348
+ return;
349
+ }
350
+ throw error;
351
+ }
352
+ });
353
+ }
354
+
355
+ open(): void {
356
+ this.#open = true;
357
+ }
358
+
359
+ close(): void {
360
+ this.#open = false;
361
+ }
362
+
363
+ async stop(): Promise<void> {
364
+ this.#open = false;
365
+ const cleanup = this.#cleanup;
366
+ this.#cleanup = undefined;
367
+ await this.#lifecycle.stop();
368
+ await cleanup?.();
369
+ this.#logger.debug(formatCompact({ op: 'disconnect' }));
370
+ }
371
+
372
+ async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
373
+ const body = formatOutbound(payload);
374
+ const kind = qqOutboundKind(conversation);
375
+ const bot = this.#requireBot();
376
+ let result: unknown;
377
+ switch (kind) {
378
+ case 'private':
379
+ result = await bot.sendPrivateMessage(conversation.id, body);
380
+ break;
381
+ case 'group':
382
+ result = await bot.sendGroupMessage(conversation.id, body);
383
+ break;
384
+ case 'channel':
385
+ result = await bot.sendGuildMessage(conversation.id, body);
386
+ break;
387
+ case 'direct':
388
+ if (!bot.sendDirectMessage) throw new Error('QQ direct message not supported by transport');
389
+ result = await bot.sendDirectMessage(conversation.id, body);
390
+ break;
391
+ }
392
+ const messageId = `${kind}-${conversation.id}:${resolveOutboundMessageId(result)}`;
393
+ this.#logger.info(
394
+ `send ${kind}:${conversation.id}`
395
+ + ` | id: ${messageId}`
396
+ + ` | ${truncatePreview(typeof body === 'string' ? body : String(body), 80)}`,
397
+ );
398
+ return messageId;
399
+ }
400
+
401
+ async recallMessage(messageId: string): Promise<void> {
402
+ const { kind, channelId, qqMsgId } = parseCompoundMessageId(messageId);
403
+ const bot = this.#requireBot();
404
+ switch (kind) {
405
+ case 'private':
406
+ await bot.recallPrivateMessage?.(channelId, qqMsgId);
407
+ break;
408
+ case 'group':
409
+ await bot.recallGroupMessage?.(channelId, qqMsgId);
410
+ break;
411
+ case 'channel':
412
+ await bot.recallGuildMessage?.(channelId, qqMsgId);
413
+ break;
414
+ case 'direct':
415
+ await bot.recallDirectMessage?.(channelId, qqMsgId);
416
+ break;
417
+ }
418
+ this.#logger.debug(formatCompact({
419
+ op: 'qq_recall',
420
+ endpoint: this.#options.config.id,
421
+ kind,
422
+ messageId,
423
+ }));
424
+ }
425
+
426
+ admit(msg: QqInboundMessage): void {
427
+ if (!this.#open) return;
428
+ const conversation = qqInboundConversation(String(this.#options.id), msg);
429
+ const content = formatInboundContent(msg);
430
+ this.#logger.info(
431
+ `recv ${conversation.kind}:${conversation.id}`
432
+ + (msg.authorId ? ` from ${msg.authorId}` : '')
433
+ + (msg.mentioned ? ' (mentioned)' : '')
434
+ + ` | ${truncatePreview(content, 80)}`,
435
+ );
436
+ void this.emit('message.receive', {
437
+ conversation,
438
+ message: { conversation, id: msg.id },
439
+ content,
440
+ ...(msg.segments?.length ? { segments: msg.segments } : {}),
441
+ sender: {
442
+ id: msg.authorId,
443
+ name: senderDisplayName(msg) || undefined,
444
+ ...(msg.authorRoles?.length ? { roles: msg.authorRoles } : {}),
445
+ },
446
+ endpointId: this.#options.config.id,
447
+ ...(msg.mentioned ? { mentioned: true } : {}),
448
+ metadata: Object.freeze({
449
+ channelKind: msg.channelKind,
450
+ userId: msg.authorId,
451
+ guildId: msg.guildId,
452
+ roles: msg.authorRoles,
453
+ }),
454
+ }).catch((err) => {
455
+ this.#logger.warn(formatCompact({
456
+ op: 'qq_gateway_receive_failed',
457
+ target: `${conversation.kind}:${conversation.id}`,
458
+ error: err instanceof Error ? err.message : String(err),
459
+ }));
460
+ });
461
+ }
462
+
463
+ #bindBot(bot: QqBotTransport): void {
464
+ bindQqBotInboundEvents(bot, (raw) => {
465
+ if (this.#bot !== bot) return;
466
+ this.#emitPlatformEvent('message', raw);
467
+ const msg = normalizeQqMessage(raw);
468
+ if (msg) this.admit(msg);
469
+ });
470
+ bindQqBotSideEvents(bot, (eventName, raw) => {
471
+ if (this.#bot !== bot) return;
472
+ this.#emitPlatformEvent(eventName, raw);
473
+ receiveQqSideEvent(
474
+ (name, payload) => this.emit(name, payload),
475
+ this.#options.config.id,
476
+ bot as QqSideEventCaller,
477
+ eventName,
478
+ raw,
479
+ this.#logger,
480
+ );
481
+ });
482
+ }
483
+
484
+ #emitPlatformEvent(name: string, event: unknown): void {
485
+ void this.emitPlatform(name, event).catch((error) => {
486
+ this.#logger.warn(formatCompact({
487
+ op: 'qq_platform_event_failed',
488
+ event: name,
489
+ error: error instanceof Error ? error.message : String(error),
490
+ }));
491
+ });
492
+ }
493
+
494
+ #requireBot(): QqHttpBotTransport {
495
+ if (!this.#bot) throw new Error('QQ bot not connected');
496
+ return this.#bot;
497
+ }
498
+ }
499
+
500
+ function createQqEndpointManagement(
501
+ requireClient: () => Pick<QqBotTransport, 'getGuilds' | 'getChannels'>,
502
+ ): EndpointManagement {
503
+ return Object.freeze<EndpointManagement>({
504
+ async listChannels(): Promise<readonly EndpointChannel[]> {
505
+ const channels: EndpointChannel[] = [];
506
+ const client = requireClient();
507
+ const guilds = await client.getGuilds();
508
+ for (const guildValue of Array.isArray(guilds) ? guilds : []) {
509
+ const guild = asRecord(guildValue);
510
+ const guildId = String(guild.id ?? guild.guild_id ?? guildValue ?? '');
511
+ if (!guildId) continue;
512
+ const guildName = String(guild.name ?? guild.guild_name ?? guildId);
513
+ const rows = await client.getChannels(guildId);
514
+ for (const channelValue of Array.isArray(rows) ? rows : []) {
515
+ const channel = asRecord(channelValue);
516
+ const id = String(channel.id ?? channel.channel_id ?? channelValue ?? '');
517
+ if (!id) continue;
518
+ channels.push({
519
+ id,
520
+ name: String(channel.name ?? channel.channel_name ?? id),
521
+ parent: { type: 'guild', id: guildId, name: guildName },
522
+ });
523
+ }
524
+ }
525
+ return channels;
526
+ },
527
+ });
528
+ }
529
+
530
+ function asRecord(value: unknown): Record<string, unknown> {
531
+ return value !== null && typeof value === 'object'
532
+ ? value as Record<string, unknown>
533
+ : {};
534
+ }