@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/CHANGELOG.md CHANGED
@@ -1,5 +1,101 @@
1
1
  # @zhin.js/adapter-qq
2
2
 
3
+ ## 7.0.5
4
+
5
+ ### Patch Changes
6
+
7
+ - 51015d6: refactor(adapters): migrate platform-permit to PermissionSubject
8
+
9
+ All adapter platform-permit checkers now accept PermissionSubject
10
+ (duck-typed from Message/CommandSession) instead of Message directly.
11
+ registerPlatformPermitChecker is replaced by host.registerPlatform in
12
+ the adapter plugin setup. activity-feedback service updated for new
13
+ PermissionHost integration.
14
+
15
+ - Updated dependencies [6f9c366]
16
+ - Updated dependencies [373a56b]
17
+ - Updated dependencies [0de46a8]
18
+ - Updated dependencies [c106ecc]
19
+ - Updated dependencies [ca92e03]
20
+ - Updated dependencies [b0f37ae]
21
+ - Updated dependencies [ba08a2f]
22
+ - Updated dependencies [b08f7fe]
23
+ - Updated dependencies [daffd4c]
24
+ - Updated dependencies [36c7400]
25
+ - Updated dependencies [a9fa72e]
26
+ - Updated dependencies [c8de3ef]
27
+ - Updated dependencies [60f0fc8]
28
+ - Updated dependencies [574c990]
29
+ - Updated dependencies [d096f16]
30
+ - Updated dependencies [3f29623]
31
+ - Updated dependencies [2916852]
32
+ - Updated dependencies [d047869]
33
+ - Updated dependencies [162fa34]
34
+ - Updated dependencies [e62561e]
35
+ - Updated dependencies [3eeeb46]
36
+ - Updated dependencies [85d0f82]
37
+ - Updated dependencies [61bfc1c]
38
+ - Updated dependencies [04bad47]
39
+ - Updated dependencies [e40b048]
40
+ - Updated dependencies [5a5b1bb]
41
+ - Updated dependencies [f1708c3]
42
+ - Updated dependencies [d254a81]
43
+ - Updated dependencies [7123c47]
44
+ - Updated dependencies [05befc1]
45
+ - Updated dependencies [0663b6a]
46
+ - Updated dependencies [f28f9b3]
47
+ - Updated dependencies [9f340f7]
48
+ - Updated dependencies [e53444f]
49
+ - Updated dependencies [5eedd26]
50
+ - Updated dependencies [92b0dd7]
51
+ - Updated dependencies [f919b6f]
52
+ - Updated dependencies [098e411]
53
+ - Updated dependencies [e1b7c01]
54
+ - Updated dependencies [9b94f87]
55
+ - Updated dependencies [a7df753]
56
+ - @zhin.js/agent@1.1.5
57
+ - @zhin.js/host-http@1.0.8
58
+ - @zhin.js/permission@1.0.1
59
+ - @zhin.js/im-contract@1.0.3
60
+ - @zhin.js/adapter@1.1.7
61
+ - @zhin.js/plugin-runtime@1.1.5
62
+ - @zhin.js/command@1.0.9
63
+ - @zhin.js/core@1.5.4
64
+ - zhin.js@6.0.4
65
+ - @zhin.js/middleware@1.0.8
66
+
67
+ ## 7.0.4
68
+
69
+ ### Patch Changes
70
+
71
+ - f8c7a54: fix: im
72
+ - Updated dependencies [f8c7a54]
73
+ - @zhin.js/logger@1.0.76
74
+ - @zhin.js/host-http@1.0.7
75
+ - @zhin.js/adapter@1.1.6
76
+ - @zhin.js/agent@1.1.4
77
+ - @zhin.js/command@1.0.8
78
+ - @zhin.js/core@1.5.3
79
+ - @zhin.js/im-contract@1.0.2
80
+ - @zhin.js/plugin-runtime@1.1.4
81
+ - zhin.js@6.0.3
82
+
83
+ ## 7.0.3
84
+
85
+ ### Patch Changes
86
+
87
+ - Updated dependencies [afc0e66]
88
+ - Updated dependencies [2e41ad5]
89
+ - Updated dependencies [9f57124]
90
+ - @zhin.js/core@1.5.2
91
+ - @zhin.js/adapter@1.1.5
92
+ - @zhin.js/im-contract@1.0.1
93
+ - @zhin.js/plugin-runtime@1.1.3
94
+ - @zhin.js/command@1.0.7
95
+ - @zhin.js/host-http@1.0.6
96
+ - @zhin.js/agent@1.1.3
97
+ - zhin.js@6.0.2
98
+
3
99
  ## 7.0.2
4
100
 
5
101
  ### Patch Changes
package/README.md CHANGED
@@ -6,7 +6,7 @@ Zhin.js QQ 官方机器人适配器(Plugin Runtime),默认通过 **WebSock
6
6
 
7
7
  - WebSocket Gateway 入站(默认;无需公网 HTTPS / host)
8
8
  - 解析私聊 / 群 / 频道消息
9
- - 出站 `send({ target, payload })` → QQ API(`private:` / `group:` / `channel:` / `direct:`)
9
+ - 出站 `send({ conversation, payload })` → QQ API(`conversation.kind`/`id`/`parent` 结构化寻址)
10
10
  - 约定式 `defineAdapter` / `definePlugin`(无需 `usePlugin`)
11
11
  - Webhook / middleware 模式已实现(经 `httpHostToken` 注册 POST 路由)
12
12
  - AI `@` 触发标注:群消息(GROUP_AT_MESSAGE_CREATE 仅 @ 时下发)与频道 `mentions[].bot` 会在入站 metadata 标 `mentioned: true`(新 Plugin Runtime 纯文本 content 经 metadata 传递 @)
@@ -25,8 +25,8 @@ pnpm add @zhin.js/adapter-qq
25
25
  - 配置经插件 `schema.json` 落到 `plugins.<instanceKey>`
26
26
  - **无需** `@zhin.js/host-http` / `@zhin.js/host-router`(WebSocket 路径)
27
27
 
28
- 入站:`gateway.receive({ adapter, target: 'group:…'|…, content, sender, metadata })`
29
- 出站:`send({ target, payload })` → `sendPrivateMessage` / `sendGroupMessage` / `sendGuildMessage`
28
+ 入站:`gateway.receive({ conversation, message, content, sender, metadata })`(ConversationRef 结构化寻址)
29
+ 出站:`send({ conversation, payload })` → `sendPrivateMessage` / `sendGroupMessage` / `sendGuildMessage`
30
30
 
31
31
  ## 前置条件
32
32
 
@@ -49,22 +49,49 @@ plugins:
49
49
  - name: my-qq-bot
50
50
  appid: ${QQ_APPID}
51
51
  secret: ${QQ_SECRET}
52
+ # botKind / intents 可由向导写入;`qq.endpoint add` 扫码后会追问公域/私域再写入
52
53
  ```
53
54
 
55
+ ### botKind 与 intents
56
+
57
+ WebSocket Identify 的 `intents` **必须与开放平台已开通的权限一致**,否则会断连。
58
+ 群聊无公/私域之分,两类都订阅 `GROUP_AND_C2C_EVENT`;差异只在频道消息 intent:
59
+
60
+ | `botKind` | 频道消息 intent | 共用 intents |
61
+ |-----------|-----------------|--------------|
62
+ | `public`(默认,公域) | `PUBLIC_GUILD_MESSAGES`(仅 @) | `GROUP_AND_C2C_EVENT`, `GUILDS`, `GUILD_MEMBERS`, `DIRECT_MESSAGE` |
63
+ | `private`(私域) | `GUILD_MESSAGES`(频道全量) | 同上 |
64
+
65
+ 公域机器人订阅 `GUILD_MESSAGES` 会 Identify 失败断连。显式配置 `intents` 时优先于 `botKind`。
66
+ `create-zhin` / `zhin setup` 会询问公/私域并写入;`qq.endpoint add` 扫码成功后在同一会话追问 `public`/`private`(或 `公域`/`私域`),确认后一次性写 `.env` 与 yaml。
67
+
54
68
  多账号:一个插件实例挂多个 endpoint(`endpoints` 数组逐项覆盖顶层字段,`name` 必填):
55
69
 
56
70
  ```yaml
57
71
  plugins:
58
72
  qq:
59
73
  mode: websocket
60
- intents: [GUILDS, GROUP_AND_C2C_EVENT]
61
74
  endpoints:
62
75
  - name: main-bot
63
76
  appid: ${QQ_APPID}
64
77
  secret: ${QQ_SECRET}
65
- - name: second-bot
78
+ botKind: public
79
+ intents:
80
+ - GROUP_AND_C2C_EVENT
81
+ - GUILDS
82
+ - GUILD_MEMBERS
83
+ - DIRECT_MESSAGE
84
+ - PUBLIC_GUILD_MESSAGES
85
+ - name: private-bot
66
86
  appid: ${QQ_APPID_2}
67
87
  secret: ${QQ_SECRET_2}
88
+ botKind: private
89
+ intents:
90
+ - GROUP_AND_C2C_EVENT
91
+ - GUILDS
92
+ - GUILD_MEMBERS
93
+ - DIRECT_MESSAGE
94
+ - GUILD_MESSAGES
68
95
  ```
69
96
 
70
97
  根插件 `zhin.plugins`(或项目图)需引用 `@zhin.js/adapter-qq`(`instanceKey: qq`)。
@@ -75,13 +102,13 @@ plugins:
75
102
 
76
103
  | 命令 | 说明 |
77
104
  |------|------|
78
- | `qq.endpoint add [name]` | 手机 QQ 扫码绑定:下发二维码链接 → 确认后凭据写入 `.env`(`QQ_<NAME>_APPID/SECRET`),并追加 `plugins.qq.endpoints`(重启生效) |
79
- | `qq.endpoint cancel` | 取消进行中的扫码绑定(同时只允许一个流程) |
105
+ | `qq.endpoint add [name]` | 手机 QQ 扫码绑定 → **追问公域/私域** → 一次性写入 `.env` + `plugins.qq.endpoints`(重启生效) |
106
+ | `qq.endpoint cancel` | 取消进行中的扫码绑定或待确认的公域/私域选择 |
80
107
  | `qq.endpoint list` | 列出运行中与配置中的 endpoints |
81
108
  | `qq.endpoint remove <name>` | 从配置移除 endpoint(`.env` 键保留,可手动清理) |
82
109
 
83
- add/cancel/remove 受 `master` 限制:实例配置声明了 `master`(顶层或 `endpoints[i]`)时仅
84
- master 可执行;未配置则放行(首个扫码绑定者即 owner)。二维码当前以链接文本下发
110
+ add/cancel/remove 受 `master` 限制:实例配置声明了 `master`(顶层或 `endpoints[i].master`)时仅
111
+ master 可执行;未配置则放行(首个扫码绑定者会写入该 endpoint 的 `master`)。二维码当前以链接文本下发
85
112
  (出站富媒体待迁移),用手机 QQ 打开链接即可扫码。
86
113
 
87
114
  ## 环境变量
package/adapters/qq.js CHANGED
@@ -22,26 +22,28 @@ export default defineAdapter({
22
22
  const gateway = context.use(messageGatewayToken);
23
23
  // 注册到插件运行时状态(qq.endpoint list 的"运行中"数据源)
24
24
  const state = context.use(qqRuntimeStateToken);
25
- state.endpoints.set(config.name, {
26
- name: config.name,
25
+ state.endpoints.set(config.id, {
26
+ id: config.id,
27
27
  mode: config.mode,
28
28
  });
29
- const endpoint = (config.mode === 'webhook' || config.mode === 'middleware')
30
- ? new QqHttpEndpoint({
29
+ // 正向判 websocket:TS 对 `mode === 'webhook' || mode === 'middleware'` 的
30
+ // 否定分支不做联合消减(多字面量判别式),反向写会让 config 窄化失败。
31
+ const endpoint = config.mode === 'websocket'
32
+ ? new QqWebsocketEndpoint({
31
33
  id: context.id,
32
34
  gateway,
33
- http: context.use(httpHostToken),
34
35
  config,
35
36
  })
36
- : new QqWebsocketEndpoint({
37
+ : new QqHttpEndpoint({
37
38
  id: context.id,
38
39
  gateway,
40
+ http: context.use(httpHostToken),
39
41
  config,
40
42
  });
41
43
  // 运行状态表只增不减:stop 时同步摘除,避免 stop 后 qq.endpoint list 仍显示运行中。
42
44
  const stopEndpoint = endpoint.stop.bind(endpoint);
43
45
  endpoint.stop = async () => {
44
- state.endpoints.delete(config.name);
46
+ state.endpoints.delete(config.id);
45
47
  await stopEndpoint();
46
48
  };
47
49
  return endpoint;
package/adapters/qq.ts CHANGED
@@ -37,26 +37,28 @@ export default defineAdapter<QqAdapterConfig>({
37
37
  const gateway = context.use(messageGatewayToken);
38
38
  // 注册到插件运行时状态(qq.endpoint list 的"运行中"数据源)
39
39
  const state = context.use(qqRuntimeStateToken);
40
- state.endpoints.set(config.name, {
41
- name: config.name,
40
+ state.endpoints.set(config.id, {
41
+ id: config.id,
42
42
  mode: config.mode,
43
43
  });
44
- const endpoint = (config.mode === 'webhook' || config.mode === 'middleware')
45
- ? new QqHttpEndpoint({
44
+ // 正向判 websocket:TS 对 `mode === 'webhook' || mode === 'middleware'` 的
45
+ // 否定分支不做联合消减(多字面量判别式),反向写会让 config 窄化失败。
46
+ const endpoint = config.mode === 'websocket'
47
+ ? new QqWebsocketEndpoint({
46
48
  id: context.id,
47
49
  gateway,
48
- http: context.use(httpHostToken),
49
50
  config,
50
51
  })
51
- : new QqWebsocketEndpoint({
52
+ : new QqHttpEndpoint({
52
53
  id: context.id,
53
54
  gateway,
55
+ http: context.use(httpHostToken),
54
56
  config,
55
57
  });
56
58
  // 运行状态表只增不减:stop 时同步摘除,避免 stop 后 qq.endpoint list 仍显示运行中。
57
59
  const stopEndpoint = endpoint.stop.bind(endpoint);
58
60
  endpoint.stop = async () => {
59
- state.endpoints.delete(config.name);
61
+ state.endpoints.delete(config.id);
60
62
  await stopEndpoint();
61
63
  };
62
64
  return endpoint;
@@ -3,7 +3,7 @@ import { defineCommand } from '@zhin.js/command';
3
3
  import { isQqEndpointOperator, QQ_ENDPOINT_FORBIDDEN, runQqEndpointCancel, } from "../../lib/qq-endpoint-commands.js";
4
4
  import { qqRuntimeStateToken } from "../../lib/qq-runtime-state.js";
5
5
  export default defineCommand({
6
- description: '取消进行中的 QQ 扫码绑定流程',
6
+ description: '取消进行中的 QQ 扫码绑定或公域/私域选择',
7
7
  execute({ config, input, use }) {
8
8
  if (!isQqEndpointOperator(config, input))
9
9
  return QQ_ENDPOINT_FORBIDDEN;
@@ -7,7 +7,7 @@ import {
7
7
  import { qqRuntimeStateToken } from '../../src/qq-runtime-state.js';
8
8
 
9
9
  export default defineCommand({
10
- description: '取消进行中的 QQ 扫码绑定流程',
10
+ description: '取消进行中的 QQ 扫码绑定或公域/私域选择',
11
11
  execute({ config, input, use }) {
12
12
  if (!isQqEndpointOperator(config, input)) return QQ_ENDPOINT_FORBIDDEN;
13
13
  return runQqEndpointCancel(use(qqRuntimeStateToken));
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,11 +1,11 @@
1
- import { formatCompact, getLogger } from '@zhin.js/logger';
1
+ import { formatCompact, getAdapterLogger, truncatePreview } 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
- const logger = getLogger('qq');
8
7
  export class QqWebsocketEndpoint {
8
+ #logger;
9
9
  #options;
10
10
  #createBot;
11
11
  #bot = null;
@@ -14,32 +14,28 @@ export class QqWebsocketEndpoint {
14
14
  #unregisterAgent;
15
15
  management = createQqEndpointManagement(this);
16
16
  constructor(options) {
17
+ this.#logger = getAdapterLogger('qq', options.config.id);
17
18
  this.#options = options;
18
19
  this.#createBot = options.createBot ?? defaultCreateBot;
19
20
  }
20
21
  /** Live endpoint 名(Console/AdapterIndex 展示用,如 bot appid 别名)。 */
21
22
  get name() {
22
- return this.#options.config.name;
23
+ return this.#options.config.id;
23
24
  }
24
25
  async start() {
25
26
  if (this.#started)
26
27
  return;
27
28
  this.#started = true;
28
29
  try {
29
- this.#unregisterAgent = registerQqAgentEndpoint(this.#options.config.name, this);
30
+ this.#unregisterAgent = registerQqAgentEndpoint(this.#options.config.id, this);
30
31
  this.#bot = this.#createBot(this.#options.config);
31
32
  this.#bindBot(this.#bot);
32
33
  await this.#bot.start();
33
- logger.info(formatCompact({
34
- op: 'connect',
35
- endpoint: this.#options.config.name,
36
- mode: 'websocket',
37
- appid: this.#options.config.appid,
38
- }));
34
+ this.#logger.info(`connected (websocket) | appid: ${this.#options.config.appid}`);
39
35
  }
40
36
  catch (error) {
41
37
  await this.stop();
42
- logger.error('Failed to connect QQ websocket:', error);
38
+ this.#logger.error('Failed to connect QQ websocket:', error);
43
39
  const raw = error instanceof Error ? error.message : String(error);
44
40
  throw new Error(`QQ WebSocket 连接失败:请检查 appid/secret 是否配对、网关地址(gatewayUrl/accessTokenUrl)是否可达(原始错误:${raw})`, { cause: error });
45
41
  }
@@ -65,38 +61,33 @@ export class QqWebsocketEndpoint {
65
61
  this.#bot = null;
66
62
  }
67
63
  this.#started = false;
68
- logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
64
+ this.#logger.debug(formatCompact({ op: 'disconnect' }));
69
65
  }
70
- async send({ target, payload }) {
66
+ async send({ conversation, payload }) {
71
67
  const body = formatOutbound(payload);
72
- const parsed = parseSendTarget(target);
68
+ const kind = qqOutboundKind(conversation);
73
69
  const bot = this.#requireBot();
74
70
  let result;
75
- switch (parsed.kind) {
71
+ switch (kind) {
76
72
  case 'private':
77
- result = await bot.sendPrivateMessage(parsed.id, body);
73
+ result = await bot.sendPrivateMessage(conversation.id, body);
78
74
  break;
79
75
  case 'group':
80
- result = await bot.sendGroupMessage(parsed.id, body);
76
+ result = await bot.sendGroupMessage(conversation.id, body);
81
77
  break;
82
78
  case 'channel':
83
- result = await bot.sendGuildMessage(parsed.id, body);
79
+ result = await bot.sendGuildMessage(conversation.id, body);
84
80
  break;
85
81
  case 'direct':
86
82
  if (!bot.sendDirectMessage)
87
83
  throw new Error('QQ direct message not supported by transport');
88
- result = await bot.sendDirectMessage(parsed.id, body);
84
+ result = await bot.sendDirectMessage(conversation.id, body);
89
85
  break;
90
- default:
91
- throw new Error(`unsupported QQ target kind: ${String(parsed.kind)}`);
92
86
  }
93
- const messageId = `${parsed.kind}-${parsed.id}:${resolveOutboundMessageId(result)}`;
94
- logger.debug(formatCompact({
95
- op: 'qq_send',
96
- endpoint: this.#options.config.name,
97
- target,
98
- messageId,
99
- }));
87
+ const messageId = `${kind}-${conversation.id}:${resolveOutboundMessageId(result)}`;
88
+ this.#logger.info(`send ${kind}:${conversation.id}`
89
+ + ` | id: ${messageId}`
90
+ + ` | ${truncatePreview(typeof body === 'string' ? body : String(body), 80)}`);
100
91
  return messageId;
101
92
  }
102
93
  async recallMessage(messageId) {
@@ -116,9 +107,9 @@ export class QqWebsocketEndpoint {
116
107
  await bot.recallDirectMessage?.(channelId, qqMsgId);
117
108
  break;
118
109
  }
119
- logger.debug(formatCompact({
110
+ this.#logger.debug(formatCompact({
120
111
  op: 'qq_recall',
121
- endpoint: this.#options.config.name,
112
+ endpoint: this.#options.config.id,
122
113
  kind,
123
114
  messageId,
124
115
  }));
@@ -127,27 +118,34 @@ export class QqWebsocketEndpoint {
127
118
  admit(msg) {
128
119
  if (!this.#open)
129
120
  return;
130
- const target = formatInboundTarget(msg);
121
+ const conversation = qqInboundConversation(String(this.#options.id), msg);
122
+ const content = formatInboundContent(msg);
123
+ this.#logger.info(`recv ${conversation.kind}:${conversation.id}`
124
+ + (msg.authorId ? ` from ${msg.authorId}` : '')
125
+ + (msg.mentioned ? ' (mentioned)' : '')
126
+ + ` | ${truncatePreview(content, 80)}`);
131
127
  void this.#options.gateway.receive({
132
- adapter: this.#options.id,
133
- target,
134
- content: formatInboundContent(msg),
128
+ conversation,
129
+ message: { conversation, id: msg.id },
130
+ content,
135
131
  ...(msg.segments?.length ? { segments: msg.segments } : {}),
136
- sender: senderDisplayName(msg),
137
- id: msg.id,
132
+ sender: {
133
+ id: msg.authorId,
134
+ name: senderDisplayName(msg) || undefined,
135
+ ...(msg.authorRoles?.length ? { roles: msg.authorRoles } : {}),
136
+ },
137
+ endpointId: this.#options.config.id,
138
+ ...(msg.mentioned ? { mentioned: true } : {}),
138
139
  metadata: Object.freeze({
139
- endpoint: this.#options.config.name,
140
140
  channelKind: msg.channelKind,
141
141
  userId: msg.authorId,
142
142
  guildId: msg.guildId,
143
143
  roles: msg.authorRoles,
144
- // AT 事件本身即 @ 机器人;新 Runtime 纯文本 content 需经 metadata 传递
145
- ...(msg.mentioned ? { mentioned: true } : {}),
146
144
  }),
147
145
  }).catch((err) => {
148
- logger.warn(formatCompact({
146
+ this.#logger.warn(formatCompact({
149
147
  op: 'qq_gateway_receive_failed',
150
- target,
148
+ target: `${conversation.kind}:${conversation.id}`,
151
149
  error: err instanceof Error ? err.message : String(err),
152
150
  }));
153
151
  });
@@ -191,6 +189,7 @@ export class QqWebsocketEndpoint {
191
189
  }
192
190
  /** Webhook / middleware inbound via httpHostToken POST (qq-official-bot Middleware receiver). */
193
191
  export class QqHttpEndpoint {
192
+ #logger;
194
193
  #options;
195
194
  #createBot;
196
195
  #bot = null;
@@ -200,33 +199,29 @@ export class QqHttpEndpoint {
200
199
  #unregisterAgent;
201
200
  management = createQqEndpointManagement(this);
202
201
  constructor(options) {
202
+ this.#logger = getAdapterLogger('qq', options.config.id);
203
203
  this.#options = options;
204
204
  this.#createBot = options.createBot ?? defaultCreateHttpBot;
205
205
  }
206
206
  /** Live endpoint 名(Console/AdapterIndex 展示用)。 */
207
207
  get name() {
208
- return this.#options.config.name;
208
+ return this.#options.config.id;
209
209
  }
210
210
  async start() {
211
211
  if (this.#started)
212
212
  return;
213
213
  this.#started = true;
214
214
  try {
215
- this.#unregisterAgent = registerQqAgentEndpoint(this.#options.config.name, this);
215
+ this.#unregisterAgent = registerQqAgentEndpoint(this.#options.config.id, this);
216
216
  this.#setupRoutes();
217
217
  this.#bot = this.#createBot(this.#options.config);
218
218
  this.#bindBot(this.#bot);
219
219
  await this.#bot.start();
220
- logger.info(formatCompact({
221
- op: 'connect',
222
- endpoint: this.#options.config.name,
223
- mode: this.#options.config.mode,
224
- path: this.#options.config.webhookPath,
225
- }));
220
+ this.#logger.info(`connected (${this.#options.config.mode}) | path: ${this.#options.config.webhookPath}`);
226
221
  }
227
222
  catch (error) {
228
223
  await this.stop();
229
- logger.error('Failed to connect QQ HTTP receiver:', error);
224
+ this.#logger.error('Failed to connect QQ HTTP receiver:', error);
230
225
  throw error;
231
226
  }
232
227
  }
@@ -253,32 +248,34 @@ export class QqHttpEndpoint {
253
248
  this.#bot = null;
254
249
  }
255
250
  this.#started = false;
256
- logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
251
+ this.#logger.debug(formatCompact({ op: 'disconnect' }));
257
252
  }
258
- async send({ target, payload }) {
253
+ async send({ conversation, payload }) {
259
254
  const body = formatOutbound(payload);
260
- const parsed = parseSendTarget(target);
255
+ const kind = qqOutboundKind(conversation);
261
256
  const bot = this.#requireBot();
262
257
  let result;
263
- switch (parsed.kind) {
258
+ switch (kind) {
264
259
  case 'private':
265
- result = await bot.sendPrivateMessage(parsed.id, body);
260
+ result = await bot.sendPrivateMessage(conversation.id, body);
266
261
  break;
267
262
  case 'group':
268
- result = await bot.sendGroupMessage(parsed.id, body);
263
+ result = await bot.sendGroupMessage(conversation.id, body);
269
264
  break;
270
265
  case 'channel':
271
- result = await bot.sendGuildMessage(parsed.id, body);
266
+ result = await bot.sendGuildMessage(conversation.id, body);
272
267
  break;
273
268
  case 'direct':
274
269
  if (!bot.sendDirectMessage)
275
270
  throw new Error('QQ direct message not supported by transport');
276
- result = await bot.sendDirectMessage(parsed.id, body);
271
+ result = await bot.sendDirectMessage(conversation.id, body);
277
272
  break;
278
- default:
279
- throw new Error(`unsupported QQ target kind: ${String(parsed.kind)}`);
280
273
  }
281
- return `${parsed.kind}-${parsed.id}:${resolveOutboundMessageId(result)}`;
274
+ const messageId = `${kind}-${conversation.id}:${resolveOutboundMessageId(result)}`;
275
+ this.#logger.info(`send ${kind}:${conversation.id}`
276
+ + ` | id: ${messageId}`
277
+ + ` | ${truncatePreview(typeof body === 'string' ? body : String(body), 80)}`);
278
+ return messageId;
282
279
  }
283
280
  async recallMessage(messageId) {
284
281
  const { kind, channelId, qqMsgId } = parseCompoundMessageId(messageId);
@@ -297,9 +294,9 @@ export class QqHttpEndpoint {
297
294
  await bot.recallDirectMessage?.(channelId, qqMsgId);
298
295
  break;
299
296
  }
300
- logger.debug(formatCompact({
297
+ this.#logger.debug(formatCompact({
301
298
  op: 'qq_recall',
302
- endpoint: this.#options.config.name,
299
+ endpoint: this.#options.config.id,
303
300
  kind,
304
301
  messageId,
305
302
  }));
@@ -307,27 +304,34 @@ export class QqHttpEndpoint {
307
304
  admit(msg) {
308
305
  if (!this.#open)
309
306
  return;
310
- const target = formatInboundTarget(msg);
307
+ const conversation = qqInboundConversation(String(this.#options.id), msg);
308
+ const content = formatInboundContent(msg);
309
+ this.#logger.info(`recv ${conversation.kind}:${conversation.id}`
310
+ + (msg.authorId ? ` from ${msg.authorId}` : '')
311
+ + (msg.mentioned ? ' (mentioned)' : '')
312
+ + ` | ${truncatePreview(content, 80)}`);
311
313
  void this.#options.gateway.receive({
312
- adapter: this.#options.id,
313
- target,
314
- content: formatInboundContent(msg),
314
+ conversation,
315
+ message: { conversation, id: msg.id },
316
+ content,
315
317
  ...(msg.segments?.length ? { segments: msg.segments } : {}),
316
- sender: senderDisplayName(msg),
317
- id: msg.id,
318
+ sender: {
319
+ id: msg.authorId,
320
+ name: senderDisplayName(msg) || undefined,
321
+ ...(msg.authorRoles?.length ? { roles: msg.authorRoles } : {}),
322
+ },
323
+ endpointId: this.#options.config.id,
324
+ ...(msg.mentioned ? { mentioned: true } : {}),
318
325
  metadata: Object.freeze({
319
- endpoint: this.#options.config.name,
320
326
  channelKind: msg.channelKind,
321
327
  userId: msg.authorId,
322
328
  guildId: msg.guildId,
323
329
  roles: msg.authorRoles,
324
- // AT 事件本身即 @ 机器人;新 Runtime 纯文本 content 需经 metadata 传递
325
- ...(msg.mentioned ? { mentioned: true } : {}),
326
330
  }),
327
331
  }).catch((err) => {
328
- logger.warn(formatCompact({
332
+ this.#logger.warn(formatCompact({
329
333
  op: 'qq_gateway_receive_failed',
330
- target,
334
+ target: `${conversation.kind}:${conversation.id}`,
331
335
  error: err instanceof Error ? err.message : String(err),
332
336
  }));
333
337
  });
package/lib/index.d.ts CHANGED
@@ -1,7 +1,8 @@
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
+ export { DEFAULT_QQ_BOT_KIND, QQ_INTENTS_BY_KIND, defaultQqEndpointIntentFields, parseQqBotKind, resolveQqIntents, type QqBotKind, type QqIntent, } from './qq-intents.js';
2
3
  export { formatOutbound, resolveMediaFile, type QqOutboundElem, type QqOutboundMessage, } from './outbound.js';
3
4
  export { QqHttpEndpoint, QqWebsocketEndpoint, type CreateQqBot, type CreateQqHttpBot, type QqBotTransport, type QqEndpointOptions, type QqHttpBotTransport, type QqHttpEndpointOptions, } from './endpoint.js';
4
5
  export { registerQqWebhookRoutes, handleQqWebhookRequest, readRequestBodyText, defaultCreateHttpBot, type QqWebhookHandler, } from './webhook.js';
5
6
  export { bindQqBotInboundEvents, defaultCreateBot, normalizeQqMessage, } from './ws.js';
6
7
  export { getQqAgentDeps, registerQqAgentEndpoint, setQqAgentDeps, type QqAgentDeps, type QqAgentEndpoint, } from './qq-agent-deps.js';
7
- export { checkQqPlatformPermit, normalizeQqGuildSenderForPermit, platformPermit, qqGuildPermitResolver, registerQqPlatformPermitChecker, } from './platform-permit.js';
8
+ export { checkQqPlatformPermit, normalizeQqGuildSenderForPermit, platformPermit, qqGuildPermitResolver, } from './platform-permit.js';
package/lib/index.js CHANGED
@@ -1,7 +1,8 @@
1
- export { formatInboundContent, formatInboundTarget, formatOutboundText, parseSendTarget, resolveOutboundMessageId, resolveQqConfig, senderDisplayName, } from './protocol.js';
1
+ export { formatInboundContent, formatOutboundText, resolveOutboundMessageId, resolveQqConfig, senderDisplayName, } from './protocol.js';
2
+ export { DEFAULT_QQ_BOT_KIND, QQ_INTENTS_BY_KIND, defaultQqEndpointIntentFields, parseQqBotKind, resolveQqIntents, } from './qq-intents.js';
2
3
  export { formatOutbound, resolveMediaFile, } from './outbound.js';
3
4
  export { QqHttpEndpoint, QqWebsocketEndpoint, } from './endpoint.js';
4
5
  export { registerQqWebhookRoutes, handleQqWebhookRequest, readRequestBodyText, defaultCreateHttpBot, } from './webhook.js';
5
6
  export { bindQqBotInboundEvents, defaultCreateBot, normalizeQqMessage, } from './ws.js';
6
7
  export { getQqAgentDeps, registerQqAgentEndpoint, setQqAgentDeps, } from './qq-agent-deps.js';
7
- export { checkQqPlatformPermit, normalizeQqGuildSenderForPermit, platformPermit, qqGuildPermitResolver, registerQqPlatformPermitChecker, } from './platform-permit.js';
8
+ export { checkQqPlatformPermit, normalizeQqGuildSenderForPermit, platformPermit, qqGuildPermitResolver, } from './platform-permit.js';