@zhin.js/adapter-qq 7.0.3 → 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 (51) hide show
  1. package/CHANGELOG.md +80 -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.js +55 -45
  8. package/lib/index.d.ts +2 -1
  9. package/lib/index.js +2 -1
  10. package/lib/platform-permit.d.ts +2 -3
  11. package/lib/platform-permit.js +3 -11
  12. package/lib/protocol.d.ts +9 -5
  13. package/lib/protocol.js +11 -7
  14. package/lib/qq-agent-deps.d.ts +2 -2
  15. package/lib/qq-agent-deps.js +7 -7
  16. package/lib/qq-bind-persist.d.ts +2 -2
  17. package/lib/qq-bind-persist.js +5 -5
  18. package/lib/qq-bot-kind-prompt.d.ts +9 -0
  19. package/lib/qq-bot-kind-prompt.js +37 -0
  20. package/lib/qq-endpoint-commands.d.ts +13 -6
  21. package/lib/qq-endpoint-commands.js +89 -31
  22. package/lib/qq-endpoint-config.d.ts +4 -4
  23. package/lib/qq-endpoint-config.js +4 -4
  24. package/lib/qq-intents.d.ts +27 -0
  25. package/lib/qq-intents.js +50 -0
  26. package/lib/qq-runtime-state.d.ts +17 -4
  27. package/lib/qq-runtime-state.js +3 -2
  28. package/lib/webhook.js +2 -0
  29. package/lib/ws.js +2 -2
  30. package/middlewares/bot-kind-reply.js +46 -0
  31. package/middlewares/bot-kind-reply.ts +60 -0
  32. package/package.json +23 -16
  33. package/plugin.js +6 -4
  34. package/schema.json +66 -9
  35. package/src/endpoint.ts +69 -46
  36. package/src/index.ts +10 -1
  37. package/src/platform-permit.ts +4 -9
  38. package/src/protocol.ts +19 -11
  39. package/src/qq-agent-deps.ts +8 -8
  40. package/src/qq-bind-persist.ts +5 -5
  41. package/src/qq-bot-kind-prompt.ts +47 -0
  42. package/src/qq-endpoint-commands.ts +125 -34
  43. package/src/qq-endpoint-config.ts +5 -5
  44. package/src/qq-intents.ts +76 -0
  45. package/src/qq-runtime-state.ts +21 -6
  46. package/src/webhook.ts +2 -0
  47. package/src/ws.ts +2 -2
  48. /package/commands/endpoint/add/{[[name]].js → [[id]].js} +0 -0
  49. /package/commands/endpoint/add/{[[name]].ts → [[id]].ts} +0 -0
  50. /package/commands/endpoint/remove/{[name].js → [id].js} +0 -0
  51. /package/commands/endpoint/remove/{[name].ts → [id].ts} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,85 @@
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
+
3
83
  ## 7.0.3
4
84
 
5
85
  ### 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.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
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,7 +61,7 @@ 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
66
  async send({ conversation, payload }) {
71
67
  const body = formatOutbound(payload);
@@ -89,12 +85,9 @@ export class QqWebsocketEndpoint {
89
85
  break;
90
86
  }
91
87
  const messageId = `${kind}-${conversation.id}:${resolveOutboundMessageId(result)}`;
92
- logger.debug(formatCompact({
93
- op: 'qq_send',
94
- endpoint: this.#options.config.name,
95
- target: `${kind}:${conversation.id}`,
96
- messageId,
97
- }));
88
+ this.#logger.info(`send ${kind}:${conversation.id}`
89
+ + ` | id: ${messageId}`
90
+ + ` | ${truncatePreview(typeof body === 'string' ? body : String(body), 80)}`);
98
91
  return messageId;
99
92
  }
100
93
  async recallMessage(messageId) {
@@ -114,9 +107,9 @@ export class QqWebsocketEndpoint {
114
107
  await bot.recallDirectMessage?.(channelId, qqMsgId);
115
108
  break;
116
109
  }
117
- logger.debug(formatCompact({
110
+ this.#logger.debug(formatCompact({
118
111
  op: 'qq_recall',
119
- endpoint: this.#options.config.name,
112
+ endpoint: this.#options.config.id,
120
113
  kind,
121
114
  messageId,
122
115
  }));
@@ -126,23 +119,31 @@ export class QqWebsocketEndpoint {
126
119
  if (!this.#open)
127
120
  return;
128
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)}`);
129
127
  void this.#options.gateway.receive({
130
128
  conversation,
131
129
  message: { conversation, id: msg.id },
132
- content: formatInboundContent(msg),
130
+ content,
133
131
  ...(msg.segments?.length ? { segments: msg.segments } : {}),
134
- sender: senderDisplayName(msg),
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 } : {}),
135
139
  metadata: Object.freeze({
136
- endpoint: this.#options.config.name,
137
140
  channelKind: msg.channelKind,
138
141
  userId: msg.authorId,
139
142
  guildId: msg.guildId,
140
143
  roles: msg.authorRoles,
141
- // AT 事件本身即 @ 机器人;新 Runtime 纯文本 content 需经 metadata 传递
142
- ...(msg.mentioned ? { mentioned: true } : {}),
143
144
  }),
144
145
  }).catch((err) => {
145
- logger.warn(formatCompact({
146
+ this.#logger.warn(formatCompact({
146
147
  op: 'qq_gateway_receive_failed',
147
148
  target: `${conversation.kind}:${conversation.id}`,
148
149
  error: err instanceof Error ? err.message : String(err),
@@ -188,6 +189,7 @@ export class QqWebsocketEndpoint {
188
189
  }
189
190
  /** Webhook / middleware inbound via httpHostToken POST (qq-official-bot Middleware receiver). */
190
191
  export class QqHttpEndpoint {
192
+ #logger;
191
193
  #options;
192
194
  #createBot;
193
195
  #bot = null;
@@ -197,33 +199,29 @@ export class QqHttpEndpoint {
197
199
  #unregisterAgent;
198
200
  management = createQqEndpointManagement(this);
199
201
  constructor(options) {
202
+ this.#logger = getAdapterLogger('qq', options.config.id);
200
203
  this.#options = options;
201
204
  this.#createBot = options.createBot ?? defaultCreateHttpBot;
202
205
  }
203
206
  /** Live endpoint 名(Console/AdapterIndex 展示用)。 */
204
207
  get name() {
205
- return this.#options.config.name;
208
+ return this.#options.config.id;
206
209
  }
207
210
  async start() {
208
211
  if (this.#started)
209
212
  return;
210
213
  this.#started = true;
211
214
  try {
212
- this.#unregisterAgent = registerQqAgentEndpoint(this.#options.config.name, this);
215
+ this.#unregisterAgent = registerQqAgentEndpoint(this.#options.config.id, this);
213
216
  this.#setupRoutes();
214
217
  this.#bot = this.#createBot(this.#options.config);
215
218
  this.#bindBot(this.#bot);
216
219
  await this.#bot.start();
217
- logger.info(formatCompact({
218
- op: 'connect',
219
- endpoint: this.#options.config.name,
220
- mode: this.#options.config.mode,
221
- path: this.#options.config.webhookPath,
222
- }));
220
+ this.#logger.info(`connected (${this.#options.config.mode}) | path: ${this.#options.config.webhookPath}`);
223
221
  }
224
222
  catch (error) {
225
223
  await this.stop();
226
- logger.error('Failed to connect QQ HTTP receiver:', error);
224
+ this.#logger.error('Failed to connect QQ HTTP receiver:', error);
227
225
  throw error;
228
226
  }
229
227
  }
@@ -250,7 +248,7 @@ export class QqHttpEndpoint {
250
248
  this.#bot = null;
251
249
  }
252
250
  this.#started = false;
253
- logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
251
+ this.#logger.debug(formatCompact({ op: 'disconnect' }));
254
252
  }
255
253
  async send({ conversation, payload }) {
256
254
  const body = formatOutbound(payload);
@@ -273,7 +271,11 @@ export class QqHttpEndpoint {
273
271
  result = await bot.sendDirectMessage(conversation.id, body);
274
272
  break;
275
273
  }
276
- return `${kind}-${conversation.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;
277
279
  }
278
280
  async recallMessage(messageId) {
279
281
  const { kind, channelId, qqMsgId } = parseCompoundMessageId(messageId);
@@ -292,9 +294,9 @@ export class QqHttpEndpoint {
292
294
  await bot.recallDirectMessage?.(channelId, qqMsgId);
293
295
  break;
294
296
  }
295
- logger.debug(formatCompact({
297
+ this.#logger.debug(formatCompact({
296
298
  op: 'qq_recall',
297
- endpoint: this.#options.config.name,
299
+ endpoint: this.#options.config.id,
298
300
  kind,
299
301
  messageId,
300
302
  }));
@@ -303,23 +305,31 @@ export class QqHttpEndpoint {
303
305
  if (!this.#open)
304
306
  return;
305
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)}`);
306
313
  void this.#options.gateway.receive({
307
314
  conversation,
308
315
  message: { conversation, id: msg.id },
309
- content: formatInboundContent(msg),
316
+ content,
310
317
  ...(msg.segments?.length ? { segments: msg.segments } : {}),
311
- sender: senderDisplayName(msg),
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 } : {}),
312
325
  metadata: Object.freeze({
313
- endpoint: this.#options.config.name,
314
326
  channelKind: msg.channelKind,
315
327
  userId: msg.authorId,
316
328
  guildId: msg.guildId,
317
329
  roles: msg.authorRoles,
318
- // AT 事件本身即 @ 机器人;新 Runtime 纯文本 content 需经 metadata 传递
319
- ...(msg.mentioned ? { mentioned: true } : {}),
320
330
  }),
321
331
  }).catch((err) => {
322
- logger.warn(formatCompact({
332
+ this.#logger.warn(formatCompact({
323
333
  op: 'qq_gateway_receive_failed',
324
334
  target: `${conversation.kind}:${conversation.id}`,
325
335
  error: err instanceof Error ? err.message : String(err),
package/lib/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
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
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';
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * QQ 官方频道 platform permit(guild/channel,非 QQ 群 group_*)
3
3
  */
4
- import { type Message } from '@zhin.js/core';
4
+ import type { PermissionSubject } from '@zhin.js/permission';
5
5
  export declare function platformPermit(perm: string): string;
6
6
  export declare function qqGuildPermitResolver(logicalPerm: string): string;
7
7
  export declare function normalizeQqGuildSenderForPermit(input: {
@@ -12,5 +12,4 @@ export declare function normalizeQqGuildSenderForPermit(input: {
12
12
  role?: string;
13
13
  permissions?: string[];
14
14
  };
15
- export declare function checkQqPlatformPermit(perm: string, message: Message<any>): boolean;
16
- export declare function registerQqPlatformPermitChecker(): () => void;
15
+ export declare function checkQqPlatformPermit(perm: string, subject: PermissionSubject): boolean;
@@ -1,7 +1,3 @@
1
- /**
2
- * QQ 官方频道 platform permit(guild/channel,非 QQ 群 group_*)
3
- */
4
- import { registerPlatformPermitChecker } from '@zhin.js/core';
5
1
  const ADAPTER = 'qq';
6
2
  export function platformPermit(perm) {
7
3
  return `platform(${ADAPTER},${perm})`;
@@ -26,10 +22,9 @@ export function normalizeQqGuildSenderForPermit(input) {
26
22
  }
27
23
  return { role: 'member', permissions };
28
24
  }
29
- export function checkQqPlatformPermit(perm, message) {
30
- const sender = message.$sender;
31
- const permissions = sender.permissions ?? [];
32
- const role = sender.role;
25
+ export function checkQqPlatformPermit(perm, subject) {
26
+ const role = subject.sender?.role?.[0];
27
+ const permissions = subject.sender?.permissions ?? [];
33
28
  const has = (t) => permissions.includes(t);
34
29
  switch (perm) {
35
30
  case 'guild_owner':
@@ -44,6 +39,3 @@ export function checkQqPlatformPermit(perm, message) {
44
39
  return false;
45
40
  }
46
41
  }
47
- export function registerQqPlatformPermitChecker() {
48
- return registerPlatformPermitChecker(ADAPTER, checkQqPlatformPermit);
49
- }
package/lib/protocol.d.ts CHANGED
@@ -5,13 +5,16 @@
5
5
  import type { ConversationRef } from '@zhin.js/im-contract';
6
6
  /** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
7
7
  export interface QqAdapterConfig {
8
- readonly name?: string;
8
+ readonly id?: string;
9
9
  readonly appid?: string;
10
10
  readonly secret?: string;
11
11
  /** Default `websocket`. `webhook` / `middleware` use httpHostToken POST. */
12
12
  readonly mode?: 'websocket' | 'webhook' | 'middleware';
13
13
  readonly sandbox?: boolean;
14
+ /** Explicit intents; when omitted, derived from `botKind` (default public). */
14
15
  readonly intents?: readonly string[];
16
+ /** public | private — drives default intents when `intents` omitted. */
17
+ readonly botKind?: string;
15
18
  readonly accessTokenUrl?: string;
16
19
  readonly gatewayUrl?: string;
17
20
  readonly webhookPath?: string;
@@ -20,12 +23,13 @@ export interface QqAdapterConfig {
20
23
  /** Transitional: legacy root `endpoints[]` with `context: qq`. */
21
24
  readonly endpoints?: ReadonlyArray<{
22
25
  readonly context?: string;
23
- readonly name?: string;
26
+ readonly id?: string;
24
27
  readonly appid?: string;
25
28
  readonly secret?: string;
26
29
  readonly mode?: 'websocket' | 'webhook' | 'middleware';
27
30
  readonly sandbox?: boolean;
28
31
  readonly intents?: readonly string[];
32
+ readonly botKind?: string;
29
33
  readonly accessTokenUrl?: string;
30
34
  readonly gatewayUrl?: string;
31
35
  readonly webhookPath?: string;
@@ -36,7 +40,7 @@ export interface QqAdapterConfig {
36
40
  export interface ResolvedQqWebsocketConfig {
37
41
  readonly context: 'qq';
38
42
  readonly mode: 'websocket';
39
- readonly name: string;
43
+ readonly id: string;
40
44
  readonly appid: string;
41
45
  readonly secret: string;
42
46
  readonly sandbox: boolean;
@@ -47,7 +51,7 @@ export interface ResolvedQqWebsocketConfig {
47
51
  export interface ResolvedQqHttpConfig {
48
52
  readonly context: 'qq';
49
53
  readonly mode: 'webhook' | 'middleware';
50
- readonly name: string;
54
+ readonly id: string;
51
55
  readonly appid: string;
52
56
  readonly secret: string;
53
57
  readonly webhookPath: string;
@@ -85,7 +89,7 @@ export declare function resolveQqConfig(config?: QqAdapterConfig): ResolvedQqCon
85
89
  * 入站归一化 → ConversationRef:`direct`(频道私信)映射为 guild 容器内的
86
90
  * private 会话;频道消息的所属 guild 进 `parent`。
87
91
  */
88
- export declare function qqInboundConversation(endpointId: string, msg: QqInboundMessage): ConversationRef;
92
+ export declare function qqInboundConversation(endpointKey: string, msg: QqInboundMessage): ConversationRef;
89
93
  /** 出站:guild 容器内的 private 会话即 QQ `direct`(频道私信)。 */
90
94
  export declare function qqOutboundKind(conversation: ConversationRef): QqChannelKind;
91
95
  export declare function senderDisplayName(msg: QqInboundMessage): string;