@zhin.js/adapter-qq 7.0.1 → 7.0.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # @zhin.js/adapter-qq
2
2
 
3
+ ## 7.0.3
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [afc0e66]
8
+ - Updated dependencies [2e41ad5]
9
+ - Updated dependencies [9f57124]
10
+ - @zhin.js/core@1.5.2
11
+ - @zhin.js/adapter@1.1.5
12
+ - @zhin.js/im-contract@1.0.1
13
+ - @zhin.js/plugin-runtime@1.1.3
14
+ - @zhin.js/command@1.0.7
15
+ - @zhin.js/host-http@1.0.6
16
+ - @zhin.js/agent@1.1.3
17
+ - zhin.js@6.0.2
18
+
19
+ ## 7.0.2
20
+
21
+ ### Patch Changes
22
+
23
+ - Updated dependencies [696ab1b]
24
+ - @zhin.js/agent@1.1.2
25
+ - zhin.js@6.0.1
26
+
3
27
  ## 7.0.1
4
28
 
5
29
  ### Patch Changes
package/lib/endpoint.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * QQ endpoints — lifecycle, outbound, admit, agent tool surface.
3
3
  */
4
- import type { EndpointInstance, EndpointManagement } from '@zhin.js/adapter';
4
+ import type { EndpointInstance, EndpointManagement, EndpointSendRequest } from '@zhin.js/adapter';
5
5
  import type { MessageGateway } from '@zhin.js/core/runtime';
6
6
  import type { HttpHost } from '@zhin.js/host-http';
7
7
  import type { CapabilityId } from '@zhin.js/plugin-runtime';
@@ -26,10 +26,7 @@ export declare class QqWebsocketEndpoint implements EndpointInstance {
26
26
  open(): void;
27
27
  close(): void;
28
28
  stop(): Promise<void>;
29
- send({ target, payload }: {
30
- readonly target: string;
31
- readonly payload: unknown;
32
- }): Promise<string>;
29
+ send({ conversation, payload }: EndpointSendRequest): Promise<string>;
33
30
  recallMessage(messageId: string): Promise<void>;
34
31
  /** Test / internal: admit a message when open. */
35
32
  admit(msg: QqInboundMessage): void;
@@ -60,10 +57,7 @@ export declare class QqHttpEndpoint implements EndpointInstance {
60
57
  open(): void;
61
58
  close(): void;
62
59
  stop(): Promise<void>;
63
- send({ target, payload }: {
64
- readonly target: string;
65
- readonly payload: unknown;
66
- }): Promise<string>;
60
+ send({ conversation, payload }: EndpointSendRequest): Promise<string>;
67
61
  recallMessage(messageId: string): Promise<void>;
68
62
  admit(msg: QqInboundMessage): void;
69
63
  getGuilds(): Promise<unknown[]>;
package/lib/endpoint.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { formatCompact, getLogger } from '@zhin.js/logger';
2
2
  import { formatOutbound } from './outbound.js';
3
3
  import { registerQqAgentEndpoint } from './qq-agent-deps.js';
4
- import { formatInboundContent, formatInboundTarget, parseCompoundMessageId, parseSendTarget, resolveOutboundMessageId, senderDisplayName, } from './protocol.js';
4
+ import { formatInboundContent, parseCompoundMessageId, qqInboundConversation, qqOutboundKind, resolveOutboundMessageId, senderDisplayName, } from './protocol.js';
5
5
  import { defaultCreateHttpBot, registerQqWebhookRoutes, } from './webhook.js';
6
6
  import { bindQqBotInboundEvents, defaultCreateBot, normalizeQqMessage, } from './ws.js';
7
7
  const logger = getLogger('qq');
@@ -67,34 +67,32 @@ export class QqWebsocketEndpoint {
67
67
  this.#started = false;
68
68
  logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
69
69
  }
70
- async send({ target, payload }) {
70
+ async send({ conversation, payload }) {
71
71
  const body = formatOutbound(payload);
72
- const parsed = parseSendTarget(target);
72
+ const kind = qqOutboundKind(conversation);
73
73
  const bot = this.#requireBot();
74
74
  let result;
75
- switch (parsed.kind) {
75
+ switch (kind) {
76
76
  case 'private':
77
- result = await bot.sendPrivateMessage(parsed.id, body);
77
+ result = await bot.sendPrivateMessage(conversation.id, body);
78
78
  break;
79
79
  case 'group':
80
- result = await bot.sendGroupMessage(parsed.id, body);
80
+ result = await bot.sendGroupMessage(conversation.id, body);
81
81
  break;
82
82
  case 'channel':
83
- result = await bot.sendGuildMessage(parsed.id, body);
83
+ result = await bot.sendGuildMessage(conversation.id, body);
84
84
  break;
85
85
  case 'direct':
86
86
  if (!bot.sendDirectMessage)
87
87
  throw new Error('QQ direct message not supported by transport');
88
- result = await bot.sendDirectMessage(parsed.id, body);
88
+ result = await bot.sendDirectMessage(conversation.id, body);
89
89
  break;
90
- default:
91
- throw new Error(`unsupported QQ target kind: ${String(parsed.kind)}`);
92
90
  }
93
- const messageId = `${parsed.kind}-${parsed.id}:${resolveOutboundMessageId(result)}`;
91
+ const messageId = `${kind}-${conversation.id}:${resolveOutboundMessageId(result)}`;
94
92
  logger.debug(formatCompact({
95
93
  op: 'qq_send',
96
94
  endpoint: this.#options.config.name,
97
- target,
95
+ target: `${kind}:${conversation.id}`,
98
96
  messageId,
99
97
  }));
100
98
  return messageId;
@@ -127,14 +125,13 @@ export class QqWebsocketEndpoint {
127
125
  admit(msg) {
128
126
  if (!this.#open)
129
127
  return;
130
- const target = formatInboundTarget(msg);
128
+ const conversation = qqInboundConversation(String(this.#options.id), msg);
131
129
  void this.#options.gateway.receive({
132
- adapter: this.#options.id,
133
- target,
130
+ conversation,
131
+ message: { conversation, id: msg.id },
134
132
  content: formatInboundContent(msg),
135
133
  ...(msg.segments?.length ? { segments: msg.segments } : {}),
136
134
  sender: senderDisplayName(msg),
137
- id: msg.id,
138
135
  metadata: Object.freeze({
139
136
  endpoint: this.#options.config.name,
140
137
  channelKind: msg.channelKind,
@@ -147,7 +144,7 @@ export class QqWebsocketEndpoint {
147
144
  }).catch((err) => {
148
145
  logger.warn(formatCompact({
149
146
  op: 'qq_gateway_receive_failed',
150
- target,
147
+ target: `${conversation.kind}:${conversation.id}`,
151
148
  error: err instanceof Error ? err.message : String(err),
152
149
  }));
153
150
  });
@@ -255,30 +252,28 @@ export class QqHttpEndpoint {
255
252
  this.#started = false;
256
253
  logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
257
254
  }
258
- async send({ target, payload }) {
255
+ async send({ conversation, payload }) {
259
256
  const body = formatOutbound(payload);
260
- const parsed = parseSendTarget(target);
257
+ const kind = qqOutboundKind(conversation);
261
258
  const bot = this.#requireBot();
262
259
  let result;
263
- switch (parsed.kind) {
260
+ switch (kind) {
264
261
  case 'private':
265
- result = await bot.sendPrivateMessage(parsed.id, body);
262
+ result = await bot.sendPrivateMessage(conversation.id, body);
266
263
  break;
267
264
  case 'group':
268
- result = await bot.sendGroupMessage(parsed.id, body);
265
+ result = await bot.sendGroupMessage(conversation.id, body);
269
266
  break;
270
267
  case 'channel':
271
- result = await bot.sendGuildMessage(parsed.id, body);
268
+ result = await bot.sendGuildMessage(conversation.id, body);
272
269
  break;
273
270
  case 'direct':
274
271
  if (!bot.sendDirectMessage)
275
272
  throw new Error('QQ direct message not supported by transport');
276
- result = await bot.sendDirectMessage(parsed.id, body);
273
+ result = await bot.sendDirectMessage(conversation.id, body);
277
274
  break;
278
- default:
279
- throw new Error(`unsupported QQ target kind: ${String(parsed.kind)}`);
280
275
  }
281
- return `${parsed.kind}-${parsed.id}:${resolveOutboundMessageId(result)}`;
276
+ return `${kind}-${conversation.id}:${resolveOutboundMessageId(result)}`;
282
277
  }
283
278
  async recallMessage(messageId) {
284
279
  const { kind, channelId, qqMsgId } = parseCompoundMessageId(messageId);
@@ -307,14 +302,13 @@ export class QqHttpEndpoint {
307
302
  admit(msg) {
308
303
  if (!this.#open)
309
304
  return;
310
- const target = formatInboundTarget(msg);
305
+ const conversation = qqInboundConversation(String(this.#options.id), msg);
311
306
  void this.#options.gateway.receive({
312
- adapter: this.#options.id,
313
- target,
307
+ conversation,
308
+ message: { conversation, id: msg.id },
314
309
  content: formatInboundContent(msg),
315
310
  ...(msg.segments?.length ? { segments: msg.segments } : {}),
316
311
  sender: senderDisplayName(msg),
317
- id: msg.id,
318
312
  metadata: Object.freeze({
319
313
  endpoint: this.#options.config.name,
320
314
  channelKind: msg.channelKind,
@@ -327,7 +321,7 @@ export class QqHttpEndpoint {
327
321
  }).catch((err) => {
328
322
  logger.warn(formatCompact({
329
323
  op: 'qq_gateway_receive_failed',
330
- target,
324
+ target: `${conversation.kind}:${conversation.id}`,
331
325
  error: err instanceof Error ? err.message : String(err),
332
326
  }));
333
327
  });
package/lib/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { formatInboundContent, formatInboundTarget, formatOutboundText, parseSendTarget, resolveOutboundMessageId, resolveQqConfig, senderDisplayName, type ParsedSendTarget, type QqAdapterConfig, type QqChannelKind, type QqInboundMessage, type QqWireSegment, type ResolvedQqConfig, type ResolvedQqHttpConfig, type ResolvedQqWebsocketConfig, } from './protocol.js';
1
+ export { formatInboundContent, formatOutboundText, resolveOutboundMessageId, resolveQqConfig, senderDisplayName, type QqAdapterConfig, type QqChannelKind, type QqInboundMessage, type QqWireSegment, type ResolvedQqConfig, type ResolvedQqHttpConfig, type ResolvedQqWebsocketConfig, } from './protocol.js';
2
2
  export { formatOutbound, resolveMediaFile, type QqOutboundElem, type QqOutboundMessage, } from './outbound.js';
3
3
  export { QqHttpEndpoint, QqWebsocketEndpoint, type CreateQqBot, type CreateQqHttpBot, type QqBotTransport, type QqEndpointOptions, type QqHttpBotTransport, type QqHttpEndpointOptions, } from './endpoint.js';
4
4
  export { registerQqWebhookRoutes, handleQqWebhookRequest, readRequestBodyText, defaultCreateHttpBot, type QqWebhookHandler, } from './webhook.js';
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { formatInboundContent, formatInboundTarget, formatOutboundText, parseSendTarget, resolveOutboundMessageId, resolveQqConfig, senderDisplayName, } from './protocol.js';
1
+ export { formatInboundContent, formatOutboundText, resolveOutboundMessageId, resolveQqConfig, senderDisplayName, } from './protocol.js';
2
2
  export { formatOutbound, resolveMediaFile, } from './outbound.js';
3
3
  export { QqHttpEndpoint, QqWebsocketEndpoint, } from './endpoint.js';
4
4
  export { registerQqWebhookRoutes, handleQqWebhookRequest, readRequestBodyText, defaultCreateHttpBot, } from './webhook.js';
package/lib/protocol.d.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  * QQ Official Bot protocol helpers — no legacy Adapter/Endpoint / segment-mapper.
3
3
  * Canonicalization is owned by gateway/core before endpoint.send.
4
4
  */
5
+ import type { ConversationRef } from '@zhin.js/im-contract';
5
6
  /** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
6
7
  export interface QqAdapterConfig {
7
8
  readonly name?: string;
@@ -79,16 +80,14 @@ export interface QqWireSegment {
79
80
  readonly type: string;
80
81
  readonly data?: Record<string, unknown>;
81
82
  }
82
- export interface ParsedSendTarget {
83
- readonly kind: QqChannelKind;
84
- readonly id: string;
85
- }
86
83
  export declare function resolveQqConfig(config?: QqAdapterConfig): ResolvedQqConfig;
87
84
  /**
88
- * Gateway reply target:`private:uid` / `group:gid` / `channel:cid` / `direct:guildId`.
85
+ * 入站归一化 → ConversationRef:`direct`(频道私信)映射为 guild 容器内的
86
+ * private 会话;频道消息的所属 guild 进 `parent`。
89
87
  */
90
- export declare function formatInboundTarget(msg: QqInboundMessage): string;
91
- export declare function parseSendTarget(target: string): ParsedSendTarget;
88
+ export declare function qqInboundConversation(endpointId: string, msg: QqInboundMessage): ConversationRef;
89
+ /** 出站:guild 容器内的 private 会话即 QQ `direct`(频道私信)。 */
90
+ export declare function qqOutboundKind(conversation: ConversationRef): QqChannelKind;
92
91
  export declare function senderDisplayName(msg: QqInboundMessage): string;
93
92
  export declare function formatInboundContent(msg: QqInboundMessage): string;
94
93
  /**
package/lib/protocol.js CHANGED
@@ -43,22 +43,25 @@ export function resolveQqConfig(config = {}) {
43
43
  };
44
44
  }
45
45
  /**
46
- * Gateway reply target:`private:uid` / `group:gid` / `channel:cid` / `direct:guildId`.
46
+ * 入站归一化 → ConversationRef:`direct`(频道私信)映射为 guild 容器内的
47
+ * private 会话;频道消息的所属 guild 进 `parent`。
47
48
  */
48
- export function formatInboundTarget(msg) {
49
- return `${msg.channelKind}:${msg.channelId}`;
49
+ export function qqInboundConversation(endpointId, msg) {
50
+ const kind = msg.channelKind === 'direct' ? 'private' : msg.channelKind;
51
+ return {
52
+ endpoint: { id: endpointId, adapter: endpointId.split('\0')[0] ?? endpointId },
53
+ kind,
54
+ id: msg.channelId,
55
+ ...(msg.guildId && (msg.channelKind === 'channel' || msg.channelKind === 'direct')
56
+ ? { parent: { kind: 'channel', id: msg.guildId } }
57
+ : {}),
58
+ };
50
59
  }
51
- export function parseSendTarget(target) {
52
- const sep = target.indexOf(':');
53
- if (sep <= 0) {
54
- return { kind: 'private', id: target };
55
- }
56
- const head = target.slice(0, sep);
57
- const rest = target.slice(sep + 1);
58
- if (head === 'private' || head === 'group' || head === 'channel' || head === 'direct') {
59
- return { kind: head, id: rest };
60
- }
61
- return { kind: 'private', id: target };
60
+ /** 出站:guild 容器内的 private 会话即 QQ `direct`(频道私信)。 */
61
+ export function qqOutboundKind(conversation) {
62
+ if (conversation.kind === 'private' && conversation.parent?.kind === 'channel')
63
+ return 'direct';
64
+ return conversation.kind;
62
65
  }
63
66
  export function senderDisplayName(msg) {
64
67
  return msg.authorName || msg.authorId;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-qq",
3
- "version": "7.0.1",
3
+ "version": "7.0.3",
4
4
  "description": "Zhin.js QQ Official adapter for Plugin Runtime (WebSocket gateway)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -35,20 +35,21 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "qq-official-bot": "^1.2.3",
38
- "@zhin.js/adapter": "1.1.4",
39
- "@zhin.js/command": "1.0.6",
40
- "@zhin.js/core": "1.5.1",
41
- "@zhin.js/host-http": "1.0.5",
38
+ "@zhin.js/adapter": "1.1.5",
39
+ "@zhin.js/command": "1.0.7",
40
+ "@zhin.js/core": "1.5.2",
41
+ "@zhin.js/host-http": "1.0.6",
42
+ "@zhin.js/im-contract": "1.0.1",
42
43
  "@zhin.js/logger": "1.0.75",
43
- "@zhin.js/plugin-runtime": "1.1.2"
44
+ "@zhin.js/plugin-runtime": "1.1.3"
44
45
  },
45
46
  "peerDependencies": {
46
47
  "zod": "^4.0.0",
47
- "@zhin.js/adapter": "1.1.4",
48
- "@zhin.js/agent": "1.1.1",
49
- "@zhin.js/core": "1.5.1",
50
- "@zhin.js/plugin-runtime": "1.1.2",
51
- "zhin.js": "6.0.1"
48
+ "@zhin.js/adapter": "1.1.5",
49
+ "@zhin.js/agent": "1.1.3",
50
+ "@zhin.js/core": "1.5.2",
51
+ "@zhin.js/plugin-runtime": "1.1.3",
52
+ "zhin.js": "6.0.2"
52
53
  },
53
54
  "peerDependenciesMeta": {
54
55
  "zhin.js": {
@@ -66,9 +67,9 @@
66
67
  "typescript": "^6.0.3",
67
68
  "vitest": "^4.1.10",
68
69
  "zod": "^4.4.3",
69
- "@zhin.js/agent": "1.1.1",
70
- "@zhin.js/host-http": "1.0.5",
71
- "zhin.js": "6.0.1"
70
+ "@zhin.js/agent": "1.1.3",
71
+ "@zhin.js/host-http": "1.0.6",
72
+ "zhin.js": "6.0.2"
72
73
  },
73
74
  "files": [
74
75
  "adapters",
package/src/endpoint.ts CHANGED
@@ -5,6 +5,7 @@ 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';
@@ -14,9 +15,9 @@ 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,
@@ -118,33 +119,31 @@ export class QqWebsocketEndpoint implements EndpointInstance {
118
119
  logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
119
120
  }
120
121
 
121
- async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
122
+ async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
122
123
  const body = formatOutbound(payload);
123
- const parsed = parseSendTarget(target);
124
+ const kind = qqOutboundKind(conversation);
124
125
  const bot = this.#requireBot();
125
126
  let result: unknown;
126
- switch (parsed.kind) {
127
+ switch (kind) {
127
128
  case 'private':
128
- result = await bot.sendPrivateMessage(parsed.id, body);
129
+ result = await bot.sendPrivateMessage(conversation.id, body);
129
130
  break;
130
131
  case 'group':
131
- result = await bot.sendGroupMessage(parsed.id, body);
132
+ result = await bot.sendGroupMessage(conversation.id, body);
132
133
  break;
133
134
  case 'channel':
134
- result = await bot.sendGuildMessage(parsed.id, body);
135
+ result = await bot.sendGuildMessage(conversation.id, body);
135
136
  break;
136
137
  case 'direct':
137
138
  if (!bot.sendDirectMessage) throw new Error('QQ direct message not supported by transport');
138
- result = await bot.sendDirectMessage(parsed.id, body);
139
+ result = await bot.sendDirectMessage(conversation.id, body);
139
140
  break;
140
- default:
141
- throw new Error(`unsupported QQ target kind: ${String((parsed as ParsedSendTarget).kind)}`);
142
141
  }
143
- const messageId = `${parsed.kind}-${parsed.id}:${resolveOutboundMessageId(result)}`;
142
+ const messageId = `${kind}-${conversation.id}:${resolveOutboundMessageId(result)}`;
144
143
  logger.debug(formatCompact({
145
144
  op: 'qq_send',
146
145
  endpoint: this.#options.config.name,
147
- target,
146
+ target: `${kind}:${conversation.id}`,
148
147
  messageId,
149
148
  }));
150
149
  return messageId;
@@ -178,14 +177,13 @@ export class QqWebsocketEndpoint implements EndpointInstance {
178
177
  /** Test / internal: admit a message when open. */
179
178
  admit(msg: QqInboundMessage): void {
180
179
  if (!this.#open) return;
181
- const target = formatInboundTarget(msg);
180
+ const conversation = qqInboundConversation(String(this.#options.id), msg);
182
181
  void this.#options.gateway.receive({
183
- adapter: this.#options.id,
184
- target,
182
+ conversation,
183
+ message: { conversation, id: msg.id },
185
184
  content: formatInboundContent(msg),
186
185
  ...(msg.segments?.length ? { segments: msg.segments } : {}),
187
186
  sender: senderDisplayName(msg),
188
- id: msg.id,
189
187
  metadata: Object.freeze({
190
188
  endpoint: this.#options.config.name,
191
189
  channelKind: msg.channelKind,
@@ -198,7 +196,7 @@ export class QqWebsocketEndpoint implements EndpointInstance {
198
196
  }).catch((err) => {
199
197
  logger.warn(formatCompact({
200
198
  op: 'qq_gateway_receive_failed',
201
- target,
199
+ target: `${conversation.kind}:${conversation.id}`,
202
200
  error: err instanceof Error ? err.message : String(err),
203
201
  }));
204
202
  });
@@ -249,8 +247,6 @@ export class QqWebsocketEndpoint implements EndpointInstance {
249
247
  }
250
248
  }
251
249
 
252
- type ParsedSendTarget = ReturnType<typeof parseSendTarget>;
253
-
254
250
  export interface QqHttpEndpointOptions {
255
251
  readonly id: CapabilityId;
256
252
  readonly gateway: MessageGateway;
@@ -331,29 +327,27 @@ export class QqHttpEndpoint implements EndpointInstance {
331
327
  logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
332
328
  }
333
329
 
334
- async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
330
+ async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
335
331
  const body = formatOutbound(payload);
336
- const parsed = parseSendTarget(target);
332
+ const kind = qqOutboundKind(conversation);
337
333
  const bot = this.#requireBot();
338
334
  let result: unknown;
339
- switch (parsed.kind) {
335
+ switch (kind) {
340
336
  case 'private':
341
- result = await bot.sendPrivateMessage(parsed.id, body);
337
+ result = await bot.sendPrivateMessage(conversation.id, body);
342
338
  break;
343
339
  case 'group':
344
- result = await bot.sendGroupMessage(parsed.id, body);
340
+ result = await bot.sendGroupMessage(conversation.id, body);
345
341
  break;
346
342
  case 'channel':
347
- result = await bot.sendGuildMessage(parsed.id, body);
343
+ result = await bot.sendGuildMessage(conversation.id, body);
348
344
  break;
349
345
  case 'direct':
350
346
  if (!bot.sendDirectMessage) throw new Error('QQ direct message not supported by transport');
351
- result = await bot.sendDirectMessage(parsed.id, body);
347
+ result = await bot.sendDirectMessage(conversation.id, body);
352
348
  break;
353
- default:
354
- throw new Error(`unsupported QQ target kind: ${String((parsed as ParsedSendTarget).kind)}`);
355
349
  }
356
- return `${parsed.kind}-${parsed.id}:${resolveOutboundMessageId(result)}`;
350
+ return `${kind}-${conversation.id}:${resolveOutboundMessageId(result)}`;
357
351
  }
358
352
 
359
353
  async recallMessage(messageId: string): Promise<void> {
@@ -383,14 +377,13 @@ export class QqHttpEndpoint implements EndpointInstance {
383
377
 
384
378
  admit(msg: QqInboundMessage): void {
385
379
  if (!this.#open) return;
386
- const target = formatInboundTarget(msg);
380
+ const conversation = qqInboundConversation(String(this.#options.id), msg);
387
381
  void this.#options.gateway.receive({
388
- adapter: this.#options.id,
389
- target,
382
+ conversation,
383
+ message: { conversation, id: msg.id },
390
384
  content: formatInboundContent(msg),
391
385
  ...(msg.segments?.length ? { segments: msg.segments } : {}),
392
386
  sender: senderDisplayName(msg),
393
- id: msg.id,
394
387
  metadata: Object.freeze({
395
388
  endpoint: this.#options.config.name,
396
389
  channelKind: msg.channelKind,
@@ -403,7 +396,7 @@ export class QqHttpEndpoint implements EndpointInstance {
403
396
  }).catch((err) => {
404
397
  logger.warn(formatCompact({
405
398
  op: 'qq_gateway_receive_failed',
406
- target,
399
+ target: `${conversation.kind}:${conversation.id}`,
407
400
  error: err instanceof Error ? err.message : String(err),
408
401
  }));
409
402
  });
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,
package/src/protocol.ts CHANGED
@@ -4,6 +4,7 @@
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';
8
9
 
9
10
  const logger = getLogger('qq');
@@ -92,11 +93,6 @@ export interface QqWireSegment {
92
93
  readonly data?: Record<string, unknown>;
93
94
  }
94
95
 
95
- export interface ParsedSendTarget {
96
- readonly kind: QqChannelKind;
97
- readonly id: string;
98
- }
99
-
100
96
  export function resolveQqConfig(config: QqAdapterConfig = {}): ResolvedQqConfig {
101
97
  const entry = config.endpoints?.find((item) => item.context === 'qq' || !item.context);
102
98
  const appid = pickCredential(config.appid, entry?.appid, process.env.QQ_APPID, process.env.QQ_BOT_APPID);
@@ -140,23 +136,25 @@ export function resolveQqConfig(config: QqAdapterConfig = {}): ResolvedQqConfig
140
136
  }
141
137
 
142
138
  /**
143
- * Gateway reply target:`private:uid` / `group:gid` / `channel:cid` / `direct:guildId`.
139
+ * 入站归一化 → ConversationRef:`direct`(频道私信)映射为 guild 容器内的
140
+ * private 会话;频道消息的所属 guild 进 `parent`。
144
141
  */
145
- export function formatInboundTarget(msg: QqInboundMessage): string {
146
- return `${msg.channelKind}:${msg.channelId}`;
142
+ export function qqInboundConversation(endpointId: string, msg: QqInboundMessage): ConversationRef {
143
+ const kind: ConversationKind = msg.channelKind === 'direct' ? 'private' : msg.channelKind;
144
+ return {
145
+ endpoint: { id: endpointId, adapter: endpointId.split('\0')[0] ?? endpointId },
146
+ kind,
147
+ id: msg.channelId,
148
+ ...(msg.guildId && (msg.channelKind === 'channel' || msg.channelKind === 'direct')
149
+ ? { parent: { kind: 'channel' as const, id: msg.guildId } }
150
+ : {}),
151
+ };
147
152
  }
148
153
 
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 };
154
+ /** 出站:guild 容器内的 private 会话即 QQ `direct`(频道私信)。 */
155
+ export function qqOutboundKind(conversation: ConversationRef): QqChannelKind {
156
+ if (conversation.kind === 'private' && conversation.parent?.kind === 'channel') return 'direct';
157
+ return conversation.kind;
160
158
  }
161
159
 
162
160
  export function senderDisplayName(msg: QqInboundMessage): string {