@zhin.js/adapter-qq 7.0.2 → 7.0.5

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 (52) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +36 -9
  3. package/adapters/qq.js +9 -7
  4. package/adapters/qq.ts +9 -7
  5. package/commands/endpoint/cancel.js +1 -1
  6. package/commands/endpoint/cancel.ts +1 -1
  7. package/lib/endpoint.d.ts +3 -9
  8. package/lib/endpoint.js +79 -75
  9. package/lib/index.d.ts +3 -2
  10. package/lib/index.js +3 -2
  11. package/lib/platform-permit.d.ts +2 -3
  12. package/lib/platform-permit.js +3 -11
  13. package/lib/protocol.d.ts +14 -11
  14. package/lib/protocol.js +26 -19
  15. package/lib/qq-agent-deps.d.ts +2 -2
  16. package/lib/qq-agent-deps.js +7 -7
  17. package/lib/qq-bind-persist.d.ts +2 -2
  18. package/lib/qq-bind-persist.js +5 -5
  19. package/lib/qq-bot-kind-prompt.d.ts +9 -0
  20. package/lib/qq-bot-kind-prompt.js +37 -0
  21. package/lib/qq-endpoint-commands.d.ts +13 -6
  22. package/lib/qq-endpoint-commands.js +89 -31
  23. package/lib/qq-endpoint-config.d.ts +4 -4
  24. package/lib/qq-endpoint-config.js +4 -4
  25. package/lib/qq-intents.d.ts +27 -0
  26. package/lib/qq-intents.js +50 -0
  27. package/lib/qq-runtime-state.d.ts +17 -4
  28. package/lib/qq-runtime-state.js +3 -2
  29. package/lib/webhook.js +2 -0
  30. package/lib/ws.js +2 -2
  31. package/middlewares/bot-kind-reply.js +46 -0
  32. package/middlewares/bot-kind-reply.ts +60 -0
  33. package/package.json +23 -15
  34. package/plugin.js +6 -4
  35. package/schema.json +66 -9
  36. package/src/endpoint.ts +95 -79
  37. package/src/index.ts +10 -4
  38. package/src/platform-permit.ts +4 -9
  39. package/src/protocol.ts +34 -28
  40. package/src/qq-agent-deps.ts +8 -8
  41. package/src/qq-bind-persist.ts +5 -5
  42. package/src/qq-bot-kind-prompt.ts +47 -0
  43. package/src/qq-endpoint-commands.ts +125 -34
  44. package/src/qq-endpoint-config.ts +5 -5
  45. package/src/qq-intents.ts +76 -0
  46. package/src/qq-runtime-state.ts +21 -6
  47. package/src/webhook.ts +2 -0
  48. package/src/ws.ts +2 -2
  49. /package/commands/endpoint/add/{[name:string=].js → [[id]].js} +0 -0
  50. /package/commands/endpoint/add/{[name:string=].ts → [[id]].ts} +0 -0
  51. /package/commands/endpoint/remove/{[name:string].js → [id].js} +0 -0
  52. /package/commands/endpoint/remove/{[name:string].ts → [id].ts} +0 -0
package/src/endpoint.ts CHANGED
@@ -5,18 +5,19 @@ import type {
5
5
  EndpointChannel,
6
6
  EndpointInstance,
7
7
  EndpointManagement,
8
+ EndpointSendRequest,
8
9
  } from '@zhin.js/adapter';
9
10
  import type { MessageGateway } from '@zhin.js/core/runtime';
10
11
  import type { HttpHost } from '@zhin.js/host-http';
11
- import { formatCompact, getLogger } from '@zhin.js/logger';
12
+ import { formatCompact, getAdapterLogger, truncatePreview } from '@zhin.js/logger';
12
13
  import type { CapabilityId } from '@zhin.js/plugin-runtime';
13
14
  import { formatOutbound } from './outbound.js';
14
15
  import { registerQqAgentEndpoint } from './qq-agent-deps.js';
15
16
  import {
16
17
  formatInboundContent,
17
- formatInboundTarget,
18
18
  parseCompoundMessageId,
19
- parseSendTarget,
19
+ qqInboundConversation,
20
+ qqOutboundKind,
20
21
  resolveOutboundMessageId,
21
22
  senderDisplayName,
22
23
  type QqInboundMessage,
@@ -37,8 +38,6 @@ import {
37
38
  type QqBotTransport,
38
39
  } from './ws.js';
39
40
 
40
- const logger = getLogger('qq');
41
-
42
41
  export type { CreateQqBot, QqBotTransport } from './ws.js';
43
42
  export type { CreateQqHttpBot, QqHttpBotTransport } from './webhook.js';
44
43
 
@@ -50,6 +49,8 @@ export interface QqEndpointOptions {
50
49
  }
51
50
 
52
51
  export class QqWebsocketEndpoint implements EndpointInstance {
52
+ readonly #logger!: ReturnType<typeof getAdapterLogger>;
53
+
53
54
  readonly #options: QqEndpointOptions;
54
55
  readonly #createBot: CreateQqBot;
55
56
  #bot: QqBotTransport | null = null;
@@ -59,32 +60,30 @@ export class QqWebsocketEndpoint implements EndpointInstance {
59
60
  readonly management: EndpointManagement = createQqEndpointManagement(this);
60
61
 
61
62
  constructor(options: QqEndpointOptions) {
63
+ this.#logger = getAdapterLogger('qq', options.config.id);
62
64
  this.#options = options;
63
65
  this.#createBot = options.createBot ?? defaultCreateBot;
64
66
  }
65
67
 
66
68
  /** Live endpoint 名(Console/AdapterIndex 展示用,如 bot appid 别名)。 */
67
69
  get name(): string {
68
- return this.#options.config.name;
70
+ return this.#options.config.id;
69
71
  }
70
72
 
71
73
  async start(): Promise<void> {
72
74
  if (this.#started) return;
73
75
  this.#started = true;
74
76
  try {
75
- this.#unregisterAgent = registerQqAgentEndpoint(this.#options.config.name, this);
77
+ this.#unregisterAgent = registerQqAgentEndpoint(this.#options.config.id, this);
76
78
  this.#bot = this.#createBot(this.#options.config);
77
79
  this.#bindBot(this.#bot);
78
80
  await this.#bot.start();
79
- logger.info(formatCompact({
80
- op: 'connect',
81
- endpoint: this.#options.config.name,
82
- mode: 'websocket',
83
- appid: this.#options.config.appid,
84
- }));
81
+ this.#logger.info(
82
+ `connected (websocket) | appid: ${this.#options.config.appid}`,
83
+ );
85
84
  } catch (error) {
86
85
  await this.stop();
87
- logger.error('Failed to connect QQ websocket:', error);
86
+ this.#logger.error('Failed to connect QQ websocket:', error);
88
87
  const raw = error instanceof Error ? error.message : String(error);
89
88
  throw new Error(
90
89
  `QQ WebSocket 连接失败:请检查 appid/secret 是否配对、网关地址(gatewayUrl/accessTokenUrl)是否可达(原始错误:${raw})`,
@@ -115,38 +114,35 @@ export class QqWebsocketEndpoint implements EndpointInstance {
115
114
  this.#bot = null;
116
115
  }
117
116
  this.#started = false;
118
- logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
117
+ this.#logger.debug(formatCompact({ op: 'disconnect' }));
119
118
  }
120
119
 
121
- async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
120
+ async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
122
121
  const body = formatOutbound(payload);
123
- const parsed = parseSendTarget(target);
122
+ const kind = qqOutboundKind(conversation);
124
123
  const bot = this.#requireBot();
125
124
  let result: unknown;
126
- switch (parsed.kind) {
125
+ switch (kind) {
127
126
  case 'private':
128
- result = await bot.sendPrivateMessage(parsed.id, body);
127
+ result = await bot.sendPrivateMessage(conversation.id, body);
129
128
  break;
130
129
  case 'group':
131
- result = await bot.sendGroupMessage(parsed.id, body);
130
+ result = await bot.sendGroupMessage(conversation.id, body);
132
131
  break;
133
132
  case 'channel':
134
- result = await bot.sendGuildMessage(parsed.id, body);
133
+ result = await bot.sendGuildMessage(conversation.id, body);
135
134
  break;
136
135
  case 'direct':
137
136
  if (!bot.sendDirectMessage) throw new Error('QQ direct message not supported by transport');
138
- result = await bot.sendDirectMessage(parsed.id, body);
137
+ result = await bot.sendDirectMessage(conversation.id, body);
139
138
  break;
140
- default:
141
- throw new Error(`unsupported QQ target kind: ${String((parsed as ParsedSendTarget).kind)}`);
142
139
  }
143
- const messageId = `${parsed.kind}-${parsed.id}:${resolveOutboundMessageId(result)}`;
144
- logger.debug(formatCompact({
145
- op: 'qq_send',
146
- endpoint: this.#options.config.name,
147
- target,
148
- messageId,
149
- }));
140
+ const messageId = `${kind}-${conversation.id}:${resolveOutboundMessageId(result)}`;
141
+ this.#logger.info(
142
+ `send ${kind}:${conversation.id}`
143
+ + ` | id: ${messageId}`
144
+ + ` | ${truncatePreview(typeof body === 'string' ? body : String(body), 80)}`,
145
+ );
150
146
  return messageId;
151
147
  }
152
148
 
@@ -167,9 +163,9 @@ export class QqWebsocketEndpoint implements EndpointInstance {
167
163
  await bot.recallDirectMessage?.(channelId, qqMsgId);
168
164
  break;
169
165
  }
170
- logger.debug(formatCompact({
166
+ this.#logger.debug(formatCompact({
171
167
  op: 'qq_recall',
172
- endpoint: this.#options.config.name,
168
+ endpoint: this.#options.config.id,
173
169
  kind,
174
170
  messageId,
175
171
  }));
@@ -178,27 +174,36 @@ export class QqWebsocketEndpoint implements EndpointInstance {
178
174
  /** Test / internal: admit a message when open. */
179
175
  admit(msg: QqInboundMessage): void {
180
176
  if (!this.#open) return;
181
- const target = formatInboundTarget(msg);
177
+ const conversation = qqInboundConversation(String(this.#options.id), msg);
178
+ const content = formatInboundContent(msg);
179
+ this.#logger.info(
180
+ `recv ${conversation.kind}:${conversation.id}`
181
+ + (msg.authorId ? ` from ${msg.authorId}` : '')
182
+ + (msg.mentioned ? ' (mentioned)' : '')
183
+ + ` | ${truncatePreview(content, 80)}`,
184
+ );
182
185
  void this.#options.gateway.receive({
183
- adapter: this.#options.id,
184
- target,
185
- content: formatInboundContent(msg),
186
+ conversation,
187
+ message: { conversation, id: msg.id },
188
+ content,
186
189
  ...(msg.segments?.length ? { segments: msg.segments } : {}),
187
- sender: senderDisplayName(msg),
188
- id: msg.id,
190
+ sender: {
191
+ id: msg.authorId,
192
+ name: senderDisplayName(msg) || undefined,
193
+ ...(msg.authorRoles?.length ? { roles: msg.authorRoles } : {}),
194
+ },
195
+ endpointId: this.#options.config.id,
196
+ ...(msg.mentioned ? { mentioned: true } : {}),
189
197
  metadata: Object.freeze({
190
- endpoint: this.#options.config.name,
191
198
  channelKind: msg.channelKind,
192
199
  userId: msg.authorId,
193
200
  guildId: msg.guildId,
194
201
  roles: msg.authorRoles,
195
- // AT 事件本身即 @ 机器人;新 Runtime 纯文本 content 需经 metadata 传递
196
- ...(msg.mentioned ? { mentioned: true } : {}),
197
202
  }),
198
203
  }).catch((err) => {
199
- logger.warn(formatCompact({
204
+ this.#logger.warn(formatCompact({
200
205
  op: 'qq_gateway_receive_failed',
201
- target,
206
+ target: `${conversation.kind}:${conversation.id}`,
202
207
  error: err instanceof Error ? err.message : String(err),
203
208
  }));
204
209
  });
@@ -249,8 +254,6 @@ export class QqWebsocketEndpoint implements EndpointInstance {
249
254
  }
250
255
  }
251
256
 
252
- type ParsedSendTarget = ReturnType<typeof parseSendTarget>;
253
-
254
257
  export interface QqHttpEndpointOptions {
255
258
  readonly id: CapabilityId;
256
259
  readonly gateway: MessageGateway;
@@ -261,6 +264,8 @@ export interface QqHttpEndpointOptions {
261
264
 
262
265
  /** Webhook / middleware inbound via httpHostToken POST (qq-official-bot Middleware receiver). */
263
266
  export class QqHttpEndpoint implements EndpointInstance {
267
+ readonly #logger!: ReturnType<typeof getAdapterLogger>;
268
+
264
269
  readonly #options: QqHttpEndpointOptions;
265
270
  readonly #createBot: CreateQqHttpBot;
266
271
  #bot: QqHttpBotTransport | null = null;
@@ -271,13 +276,14 @@ export class QqHttpEndpoint implements EndpointInstance {
271
276
  readonly management: EndpointManagement = createQqEndpointManagement(this);
272
277
 
273
278
  constructor(options: QqHttpEndpointOptions) {
279
+ this.#logger = getAdapterLogger('qq', options.config.id);
274
280
  this.#options = options;
275
281
  this.#createBot = options.createBot ?? defaultCreateHttpBot;
276
282
  }
277
283
 
278
284
  /** Live endpoint 名(Console/AdapterIndex 展示用)。 */
279
285
  get name(): string {
280
- return this.#options.config.name;
286
+ return this.#options.config.id;
281
287
  }
282
288
 
283
289
  async start(): Promise<void> {
@@ -285,22 +291,19 @@ export class QqHttpEndpoint implements EndpointInstance {
285
291
  this.#started = true;
286
292
  try {
287
293
  this.#unregisterAgent = registerQqAgentEndpoint(
288
- this.#options.config.name,
294
+ this.#options.config.id,
289
295
  this as unknown as QqWebsocketEndpoint,
290
296
  );
291
297
  this.#setupRoutes();
292
298
  this.#bot = this.#createBot(this.#options.config);
293
299
  this.#bindBot(this.#bot);
294
300
  await this.#bot.start();
295
- logger.info(formatCompact({
296
- op: 'connect',
297
- endpoint: this.#options.config.name,
298
- mode: this.#options.config.mode,
299
- path: this.#options.config.webhookPath,
300
- }));
301
+ this.#logger.info(
302
+ `connected (${this.#options.config.mode}) | path: ${this.#options.config.webhookPath}`,
303
+ );
301
304
  } catch (error) {
302
305
  await this.stop();
303
- logger.error('Failed to connect QQ HTTP receiver:', error);
306
+ this.#logger.error('Failed to connect QQ HTTP receiver:', error);
304
307
  throw error;
305
308
  }
306
309
  }
@@ -328,32 +331,36 @@ export class QqHttpEndpoint implements EndpointInstance {
328
331
  this.#bot = null;
329
332
  }
330
333
  this.#started = false;
331
- logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
334
+ this.#logger.debug(formatCompact({ op: 'disconnect' }));
332
335
  }
333
336
 
334
- async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
337
+ async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
335
338
  const body = formatOutbound(payload);
336
- const parsed = parseSendTarget(target);
339
+ const kind = qqOutboundKind(conversation);
337
340
  const bot = this.#requireBot();
338
341
  let result: unknown;
339
- switch (parsed.kind) {
342
+ switch (kind) {
340
343
  case 'private':
341
- result = await bot.sendPrivateMessage(parsed.id, body);
344
+ result = await bot.sendPrivateMessage(conversation.id, body);
342
345
  break;
343
346
  case 'group':
344
- result = await bot.sendGroupMessage(parsed.id, body);
347
+ result = await bot.sendGroupMessage(conversation.id, body);
345
348
  break;
346
349
  case 'channel':
347
- result = await bot.sendGuildMessage(parsed.id, body);
350
+ result = await bot.sendGuildMessage(conversation.id, body);
348
351
  break;
349
352
  case 'direct':
350
353
  if (!bot.sendDirectMessage) throw new Error('QQ direct message not supported by transport');
351
- result = await bot.sendDirectMessage(parsed.id, body);
354
+ result = await bot.sendDirectMessage(conversation.id, body);
352
355
  break;
353
- default:
354
- throw new Error(`unsupported QQ target kind: ${String((parsed as ParsedSendTarget).kind)}`);
355
356
  }
356
- return `${parsed.kind}-${parsed.id}:${resolveOutboundMessageId(result)}`;
357
+ const messageId = `${kind}-${conversation.id}:${resolveOutboundMessageId(result)}`;
358
+ this.#logger.info(
359
+ `send ${kind}:${conversation.id}`
360
+ + ` | id: ${messageId}`
361
+ + ` | ${truncatePreview(typeof body === 'string' ? body : String(body), 80)}`,
362
+ );
363
+ return messageId;
357
364
  }
358
365
 
359
366
  async recallMessage(messageId: string): Promise<void> {
@@ -373,9 +380,9 @@ export class QqHttpEndpoint implements EndpointInstance {
373
380
  await bot.recallDirectMessage?.(channelId, qqMsgId);
374
381
  break;
375
382
  }
376
- logger.debug(formatCompact({
383
+ this.#logger.debug(formatCompact({
377
384
  op: 'qq_recall',
378
- endpoint: this.#options.config.name,
385
+ endpoint: this.#options.config.id,
379
386
  kind,
380
387
  messageId,
381
388
  }));
@@ -383,27 +390,36 @@ export class QqHttpEndpoint implements EndpointInstance {
383
390
 
384
391
  admit(msg: QqInboundMessage): void {
385
392
  if (!this.#open) return;
386
- const target = formatInboundTarget(msg);
393
+ const conversation = qqInboundConversation(String(this.#options.id), msg);
394
+ const content = formatInboundContent(msg);
395
+ this.#logger.info(
396
+ `recv ${conversation.kind}:${conversation.id}`
397
+ + (msg.authorId ? ` from ${msg.authorId}` : '')
398
+ + (msg.mentioned ? ' (mentioned)' : '')
399
+ + ` | ${truncatePreview(content, 80)}`,
400
+ );
387
401
  void this.#options.gateway.receive({
388
- adapter: this.#options.id,
389
- target,
390
- content: formatInboundContent(msg),
402
+ conversation,
403
+ message: { conversation, id: msg.id },
404
+ content,
391
405
  ...(msg.segments?.length ? { segments: msg.segments } : {}),
392
- sender: senderDisplayName(msg),
393
- id: msg.id,
406
+ sender: {
407
+ id: msg.authorId,
408
+ name: senderDisplayName(msg) || undefined,
409
+ ...(msg.authorRoles?.length ? { roles: msg.authorRoles } : {}),
410
+ },
411
+ endpointId: this.#options.config.id,
412
+ ...(msg.mentioned ? { mentioned: true } : {}),
394
413
  metadata: Object.freeze({
395
- endpoint: this.#options.config.name,
396
414
  channelKind: msg.channelKind,
397
415
  userId: msg.authorId,
398
416
  guildId: msg.guildId,
399
417
  roles: msg.authorRoles,
400
- // AT 事件本身即 @ 机器人;新 Runtime 纯文本 content 需经 metadata 传递
401
- ...(msg.mentioned ? { mentioned: true } : {}),
402
418
  }),
403
419
  }).catch((err) => {
404
- logger.warn(formatCompact({
420
+ this.#logger.warn(formatCompact({
405
421
  op: 'qq_gateway_receive_failed',
406
- target,
422
+ target: `${conversation.kind}:${conversation.id}`,
407
423
  error: err instanceof Error ? err.message : String(err),
408
424
  }));
409
425
  });
package/src/index.ts CHANGED
@@ -1,12 +1,9 @@
1
1
  export {
2
2
  formatInboundContent,
3
- formatInboundTarget,
4
3
  formatOutboundText,
5
- parseSendTarget,
6
4
  resolveOutboundMessageId,
7
5
  resolveQqConfig,
8
6
  senderDisplayName,
9
- type ParsedSendTarget,
10
7
  type QqAdapterConfig,
11
8
  type QqChannelKind,
12
9
  type QqInboundMessage,
@@ -16,6 +13,16 @@ export {
16
13
  type ResolvedQqWebsocketConfig,
17
14
  } from './protocol.js';
18
15
 
16
+ export {
17
+ DEFAULT_QQ_BOT_KIND,
18
+ QQ_INTENTS_BY_KIND,
19
+ defaultQqEndpointIntentFields,
20
+ parseQqBotKind,
21
+ resolveQqIntents,
22
+ type QqBotKind,
23
+ type QqIntent,
24
+ } from './qq-intents.js';
25
+
19
26
  export {
20
27
  formatOutbound,
21
28
  resolveMediaFile,
@@ -61,5 +68,4 @@ export {
61
68
  normalizeQqGuildSenderForPermit,
62
69
  platformPermit,
63
70
  qqGuildPermitResolver,
64
- registerQqPlatformPermitChecker,
65
71
  } from './platform-permit.js';
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * QQ 官方频道 platform permit(guild/channel,非 QQ 群 group_*)
3
3
  */
4
- import { registerPlatformPermitChecker, type Message } from '@zhin.js/core';
4
+ import type { PermissionSubject } from '@zhin.js/permission';
5
5
 
6
6
  const ADAPTER = 'qq';
7
7
 
@@ -36,10 +36,9 @@ export function normalizeQqGuildSenderForPermit(input: {
36
36
  return { role: 'member', permissions };
37
37
  }
38
38
 
39
- export function checkQqPlatformPermit(perm: string, message: Message<any>): boolean {
40
- const sender = message.$sender as { role?: string; permissions?: string[] };
41
- const permissions = sender.permissions ?? [];
42
- const role = sender.role;
39
+ export function checkQqPlatformPermit(perm: string, subject: PermissionSubject): boolean {
40
+ const role = subject.sender?.role?.[0];
41
+ const permissions = subject.sender?.permissions ?? [];
43
42
  const has = (t: string) => permissions.includes(t);
44
43
 
45
44
  switch (perm) {
@@ -55,7 +54,3 @@ export function checkQqPlatformPermit(perm: string, message: Message<any>): bool
55
54
  return false;
56
55
  }
57
56
  }
58
-
59
- export function registerQqPlatformPermitChecker(): () => void {
60
- return registerPlatformPermitChecker(ADAPTER, checkQqPlatformPermit);
61
- }
package/src/protocol.ts CHANGED
@@ -4,19 +4,24 @@
4
4
  */
5
5
 
6
6
  import { pickCredential } from '@zhin.js/adapter';
7
+ import type { ConversationKind, ConversationRef } from '@zhin.js/im-contract';
7
8
  import { formatCompact, getLogger } from '@zhin.js/logger';
9
+ import { resolveQqIntents } from './qq-intents.js';
8
10
 
9
11
  const logger = getLogger('qq');
10
12
 
11
13
  /** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
12
14
  export interface QqAdapterConfig {
13
- readonly name?: string;
15
+ readonly id?: string;
14
16
  readonly appid?: string;
15
17
  readonly secret?: string;
16
18
  /** Default `websocket`. `webhook` / `middleware` use httpHostToken POST. */
17
19
  readonly mode?: 'websocket' | 'webhook' | 'middleware';
18
20
  readonly sandbox?: boolean;
21
+ /** Explicit intents; when omitted, derived from `botKind` (default public). */
19
22
  readonly intents?: readonly string[];
23
+ /** public | private — drives default intents when `intents` omitted. */
24
+ readonly botKind?: string;
20
25
  readonly accessTokenUrl?: string;
21
26
  readonly gatewayUrl?: string;
22
27
  readonly webhookPath?: string;
@@ -25,12 +30,13 @@ export interface QqAdapterConfig {
25
30
  /** Transitional: legacy root `endpoints[]` with `context: qq`. */
26
31
  readonly endpoints?: ReadonlyArray<{
27
32
  readonly context?: string;
28
- readonly name?: string;
33
+ readonly id?: string;
29
34
  readonly appid?: string;
30
35
  readonly secret?: string;
31
36
  readonly mode?: 'websocket' | 'webhook' | 'middleware';
32
37
  readonly sandbox?: boolean;
33
38
  readonly intents?: readonly string[];
39
+ readonly botKind?: string;
34
40
  readonly accessTokenUrl?: string;
35
41
  readonly gatewayUrl?: string;
36
42
  readonly webhookPath?: string;
@@ -42,7 +48,7 @@ export interface QqAdapterConfig {
42
48
  export interface ResolvedQqWebsocketConfig {
43
49
  readonly context: 'qq';
44
50
  readonly mode: 'websocket';
45
- readonly name: string;
51
+ readonly id: string;
46
52
  readonly appid: string;
47
53
  readonly secret: string;
48
54
  readonly sandbox: boolean;
@@ -54,7 +60,7 @@ export interface ResolvedQqWebsocketConfig {
54
60
  export interface ResolvedQqHttpConfig {
55
61
  readonly context: 'qq';
56
62
  readonly mode: 'webhook' | 'middleware';
57
- readonly name: string;
63
+ readonly id: string;
58
64
  readonly appid: string;
59
65
  readonly secret: string;
60
66
  readonly webhookPath: string;
@@ -92,11 +98,6 @@ export interface QqWireSegment {
92
98
  readonly data?: Record<string, unknown>;
93
99
  }
94
100
 
95
- export interface ParsedSendTarget {
96
- readonly kind: QqChannelKind;
97
- readonly id: string;
98
- }
99
-
100
101
  export function resolveQqConfig(config: QqAdapterConfig = {}): ResolvedQqConfig {
101
102
  const entry = config.endpoints?.find((item) => item.context === 'qq' || !item.context);
102
103
  const appid = pickCredential(config.appid, entry?.appid, process.env.QQ_APPID, process.env.QQ_BOT_APPID);
@@ -106,8 +107,8 @@ export function resolveQqConfig(config: QqAdapterConfig = {}): ResolvedQqConfig
106
107
  'QQ adapter requires appid + secret (plugins.<key>.appid/secret or endpoints with context: qq)',
107
108
  );
108
109
  }
109
- const name = (typeof config.name === 'string' && config.name)
110
- || (typeof entry?.name === 'string' && entry.name)
110
+ const id = (typeof config.id === 'string' && config.id)
111
+ || (typeof entry?.id === 'string' && entry.id)
111
112
  || process.env.QQ_BOT_NAME
112
113
  || 'qq-bot';
113
114
  const mode = config.mode ?? entry?.mode ?? 'websocket';
@@ -118,7 +119,7 @@ export function resolveQqConfig(config: QqAdapterConfig = {}): ResolvedQqConfig
118
119
  return {
119
120
  context: 'qq',
120
121
  mode,
121
- name,
122
+ id,
122
123
  appid,
123
124
  secret,
124
125
  webhookPath,
@@ -129,34 +130,39 @@ export function resolveQqConfig(config: QqAdapterConfig = {}): ResolvedQqConfig
129
130
  return {
130
131
  context: 'qq',
131
132
  mode: 'websocket',
132
- name,
133
+ id,
133
134
  appid,
134
135
  secret,
135
136
  sandbox: config.sandbox === true || entry?.sandbox === true,
136
- intents: config.intents ?? entry?.intents,
137
+ intents: resolveQqIntents({
138
+ intents: config.intents ?? entry?.intents,
139
+ botKind: config.botKind ?? entry?.botKind,
140
+ }),
137
141
  accessTokenUrl: config.accessTokenUrl ?? entry?.accessTokenUrl,
138
142
  gatewayUrl: config.gatewayUrl ?? entry?.gatewayUrl,
139
143
  };
140
144
  }
141
145
 
142
146
  /**
143
- * Gateway reply target:`private:uid` / `group:gid` / `channel:cid` / `direct:guildId`.
147
+ * 入站归一化 → ConversationRef:`direct`(频道私信)映射为 guild 容器内的
148
+ * private 会话;频道消息的所属 guild 进 `parent`。
144
149
  */
145
- export function formatInboundTarget(msg: QqInboundMessage): string {
146
- return `${msg.channelKind}:${msg.channelId}`;
150
+ export function qqInboundConversation(endpointKey: string, msg: QqInboundMessage): ConversationRef {
151
+ const kind: ConversationKind = msg.channelKind === 'direct' ? 'private' : msg.channelKind;
152
+ return {
153
+ endpoint: { id: endpointKey, adapter: endpointKey.split('\0')[0] ?? endpointKey },
154
+ kind,
155
+ id: msg.channelId,
156
+ ...(msg.guildId && (msg.channelKind === 'channel' || msg.channelKind === 'direct')
157
+ ? { parent: { kind: 'channel' as const, id: msg.guildId } }
158
+ : {}),
159
+ };
147
160
  }
148
161
 
149
- export function parseSendTarget(target: string): ParsedSendTarget {
150
- const sep = target.indexOf(':');
151
- if (sep <= 0) {
152
- return { kind: 'private', id: target };
153
- }
154
- const head = target.slice(0, sep);
155
- const rest = target.slice(sep + 1);
156
- if (head === 'private' || head === 'group' || head === 'channel' || head === 'direct') {
157
- return { kind: head, id: rest };
158
- }
159
- return { kind: 'private', id: target };
162
+ /** 出站:guild 容器内的 private 会话即 QQ `direct`(频道私信)。 */
163
+ export function qqOutboundKind(conversation: ConversationRef): QqChannelKind {
164
+ if (conversation.kind === 'private' && conversation.parent?.kind === 'channel') return 'direct';
165
+ return conversation.kind;
160
166
  }
161
167
 
162
168
  export function senderDisplayName(msg: QqInboundMessage): string {
@@ -25,20 +25,20 @@ export interface QqAgentEndpoint {
25
25
  }
26
26
 
27
27
  export interface QqAgentDeps {
28
- getEndpoint: (endpointId: string) => QqAgentEndpoint;
28
+ getEndpoint: (endpointKey: string) => QqAgentEndpoint;
29
29
  }
30
30
 
31
31
  const endpoints = new Map<string, QqAgentEndpoint>();
32
32
  let override: QqAgentDeps | null = null;
33
33
 
34
34
  export function registerQqAgentEndpoint(
35
- endpointId: string,
35
+ endpointKey: string,
36
36
  endpoint: QqAgentEndpoint,
37
37
  ): () => void {
38
- endpoints.set(endpointId, endpoint);
38
+ endpoints.set(endpointKey, endpoint);
39
39
  return () => {
40
- if (endpoints.get(endpointId) === endpoint) {
41
- endpoints.delete(endpointId);
40
+ if (endpoints.get(endpointKey) === endpoint) {
41
+ endpoints.delete(endpointKey);
42
42
  }
43
43
  };
44
44
  }
@@ -51,9 +51,9 @@ export function setQqAgentDeps(deps: QqAgentDeps | null): void {
51
51
  export function getQqAgentDeps(): QqAgentDeps {
52
52
  if (override) return override;
53
53
  return {
54
- getEndpoint(endpointId: string): QqAgentEndpoint {
55
- const registered = endpoints.get(endpointId);
56
- if (!registered) throw new Error(`Endpoint ${endpointId} 不存在`);
54
+ getEndpoint(endpointKey: string): QqAgentEndpoint {
55
+ const registered = endpoints.get(endpointKey);
56
+ if (!registered) throw new Error(`Endpoint ${endpointKey} 不存在`);
57
57
  return registered;
58
58
  },
59
59
  };
@@ -14,9 +14,9 @@ export interface QqCredentialEnvKeys {
14
14
  }
15
15
 
16
16
  /** 按 endpoint 名称生成唯一 env 键(如 `QQ_MY_BOT_APPID`) */
17
- export function buildQqCredentialEnvKeys(endpointName: string): QqCredentialEnvKeys {
18
- const appidKey = buildEndpointEnvKey('qq', endpointName, 'appid');
19
- const secretKey = buildEndpointEnvKey('qq', endpointName, 'secret');
17
+ export function buildQqCredentialEnvKeys(endpointId: string): QqCredentialEnvKeys {
18
+ const appidKey = buildEndpointEnvKey('qq', endpointId, 'appid');
19
+ const secretKey = buildEndpointEnvKey('qq', endpointId, 'secret');
20
20
  return {
21
21
  appidKey,
22
22
  secretKey,
@@ -27,12 +27,12 @@ export function buildQqCredentialEnvKeys(endpointName: string): QqCredentialEnvK
27
27
 
28
28
  /** 写入或更新 `.env` 中的键值,并同步到当前进程 `process.env` */
29
29
  export function persistQqCredentialsToEnv(
30
- endpointName: string,
30
+ endpointId: string,
31
31
  appId: string,
32
32
  appSecret: string,
33
33
  projectRoot?: string,
34
34
  ): QqCredentialEnvKeys {
35
- const keys = buildQqCredentialEnvKeys(endpointName);
35
+ const keys = buildQqCredentialEnvKeys(endpointId);
36
36
  persistEndpointEnvValues(
37
37
  {
38
38
  [keys.appidKey]: appId,