@zhin.js/adapter-qq 1.1.16 → 1.1.18

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 (76) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +56 -5
  3. package/adapters/qq/index.js +7 -2
  4. package/adapters/qq/index.ts +7 -2
  5. package/agents/qq/boundaries.md +2 -0
  6. package/agents/qq/conversation.js +92 -0
  7. package/agents/qq/conversation.ts +99 -0
  8. package/agents/qq/messages.js +60 -0
  9. package/agents/qq/messages.ts +60 -0
  10. package/agents/qq/skills/qq/SKILL.md +8 -2
  11. package/agents/qq/skills/qq/tools/guild_kick_member/index.js +30 -0
  12. package/agents/qq/skills/qq/tools/guild_kick_member/index.ts +29 -0
  13. package/agents/qq/skills/qq/tools/guild_mute_member/index.js +31 -0
  14. package/agents/qq/skills/qq/tools/guild_mute_member/index.ts +30 -0
  15. package/agents/qq/skills/qq-conversation/SKILL.md +24 -0
  16. package/agents/qq/skills/qq-conversation/tools/conversation_info/index.js +78 -0
  17. package/agents/qq/skills/qq-conversation/tools/conversation_info/index.ts +76 -0
  18. package/agents/qq/skills/qq-group-admin/SKILL.md +34 -0
  19. package/agents/qq/skills/qq-group-admin/tools/group_kick/index.js +39 -0
  20. package/agents/qq/skills/qq-group-admin/tools/group_kick/index.ts +38 -0
  21. package/agents/qq/skills/qq-group-admin/tools/handle_join_request/index.js +33 -0
  22. package/agents/qq/skills/qq-group-admin/tools/handle_join_request/index.ts +32 -0
  23. package/agents/qq/skills/qq-group-admin/tools/recall_member_message/index.js +25 -0
  24. package/agents/qq/skills/qq-group-admin/tools/recall_member_message/index.ts +25 -0
  25. package/agents/qq/skills/qq-group-admin/tools/set_mute/index.js +37 -0
  26. package/agents/qq/skills/qq-group-admin/tools/set_mute/index.ts +36 -0
  27. package/agents/qq/skills/qq-group-directory/SKILL.md +24 -0
  28. package/agents/qq/skills/qq-group-directory/tools/group_info/index.js +16 -0
  29. package/agents/qq/skills/qq-group-directory/tools/group_info/index.ts +16 -0
  30. package/agents/qq/skills/qq-group-directory/tools/group_member_info/index.js +19 -0
  31. package/agents/qq/skills/qq-group-directory/tools/group_member_info/index.ts +19 -0
  32. package/agents/qq/skills/qq-group-directory/tools/group_members/index.js +19 -0
  33. package/agents/qq/skills/qq-group-directory/tools/group_members/index.ts +19 -0
  34. package/agents/qq/skills/qq-group-state/SKILL.md +21 -0
  35. package/agents/qq/skills/qq-group-state/tools/group_blacklist/index.js +19 -0
  36. package/agents/qq/skills/qq-group-state/tools/group_blacklist/index.ts +19 -0
  37. package/agents/qq/skills/qq-group-state/tools/list_join_requests/index.js +19 -0
  38. package/agents/qq/skills/qq-group-state/tools/list_join_requests/index.ts +19 -0
  39. package/agents/qq/skills/qq-group-state/tools/list_muted/index.js +21 -0
  40. package/agents/qq/skills/qq-group-state/tools/list_muted/index.ts +21 -0
  41. package/agents/qq/skills/qq-messages/SKILL.md +27 -0
  42. package/agents/qq/skills/qq-messages/tools/quote_reply/index.js +21 -0
  43. package/agents/qq/skills/qq-messages/tools/quote_reply/index.ts +21 -0
  44. package/agents/qq/skills/qq-messages/tools/recall_own_message/index.js +18 -0
  45. package/agents/qq/skills/qq-messages/tools/recall_own_message/index.ts +17 -0
  46. package/commands/qq/endpoint/definition.js +35 -22
  47. package/commands/qq/endpoint/definition.ts +37 -22
  48. package/lib/console.d.ts +8 -0
  49. package/lib/console.js +84 -0
  50. package/lib/endpoint.d.ts +10 -1
  51. package/lib/endpoint.js +134 -8
  52. package/lib/group-admin.d.ts +36 -0
  53. package/lib/group-admin.js +113 -0
  54. package/lib/protocol.d.ts +5 -0
  55. package/lib/protocol.js +4 -0
  56. package/lib/qq-console-spec.d.ts +17 -0
  57. package/lib/qq-console-spec.js +10 -0
  58. package/lib/qq-console.d.ts +11 -0
  59. package/lib/qq-console.js +116 -0
  60. package/lib/qq-endpoint-write.d.ts +22 -0
  61. package/lib/qq-endpoint-write.js +34 -0
  62. package/lib/qq-runtime-state.d.ts +19 -0
  63. package/lib/qq-runtime-state.js +1 -0
  64. package/package.json +19 -11
  65. package/pages/index/index.js +15 -0
  66. package/pages/index/index.ts +16 -0
  67. package/plugin.js +5 -2
  68. package/schema.json +174 -0
  69. package/src/console.ts +96 -0
  70. package/src/endpoint.ts +137 -8
  71. package/src/group-admin.ts +153 -0
  72. package/src/protocol.ts +8 -0
  73. package/src/qq-console-spec.ts +18 -0
  74. package/src/qq-console.ts +124 -0
  75. package/src/qq-endpoint-write.ts +52 -0
  76. package/src/qq-runtime-state.ts +21 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,67 @@
1
1
  # @zhin.js/adapter-qq
2
2
 
3
+ ## 1.1.18
4
+
5
+ ### Patch Changes
6
+
7
+ - 6345a38: feat(adapters): add per-adapter Console pages
8
+
9
+ Every platform adapter now ships a `pages/index` Console page backed by a shared
10
+ `createAdapterConsolePage` component and `createAdapterConsoleRpc` server helper:
11
+
12
+ - See running endpoints, test connectivity, and manage
13
+ `plugins.<adapter>.endpoints` (add/remove writes `.env` references) from the
14
+ Console instead of chat commands.
15
+ - Surface LoginAssist pending tasks (QR / slider / SMS / device) so ICQQ login no
16
+ longer requires the terminal.
17
+ - QQ moves its QR binding flow (`qq endpoint add`) into Console actions with
18
+ public/private selection.
19
+ - Pages are permission-gated by `<adapter>:read`; writes need `<adapter>:write`.
20
+
21
+ - ff3efe7: feat(adapters): notify masters when ICQQ or QQ goes offline and support ICQQ re-login
22
+
23
+ - Add an explicit `notify` target to ICQQ and QQ endpoint config: when the
24
+ transport goes offline or login fails, send a short notice through another
25
+ adapter instance's Endpoint via `OutboundHost`.
26
+ - ICQQ automatically retries login after `system.offline.kickoff` when
27
+ `autoReconnect` is enabled, and exposes pending QR/slider/device challenges.
28
+ - Add `icqq login status|qr|confirm|code|retry` recovery commands. They can be
29
+ invoked from the QQ conversation that received the notice; `qr` returns the
30
+ login QR code as an image when the notifying platform supports it.
31
+ - `notify.id` also counts as the master identity for the related endpoint
32
+ management commands, so recovery can happen from the notified conversation.
33
+ - Document the new config and commands in the ICQQ/QQ READMEs and generated
34
+ configuration reference.
35
+
36
+ - 26888e4: QQ 官方适配器按官方 v2 接口向 ICQQ 能力对齐:新增 `qq-conversation` / `qq-messages` / `qq-group-directory` / `qq-group-admin` / `qq-group-state` 技能(会话信息、引用回复、撤回自己 / 成员消息、群信息与成员、移出、禁言、入群审批、禁言列表、入群申请、黑名单)及频道成员禁言 / 移出;Endpoint 管理端口补齐群成员列表、踢人、禁言与自动群审核;撤回支持入站原生消息 ID,频道消息支持表情回应。好友 / 群列表、合并转发、戳一戳、点赞、公告、精华、头衔、退群等官方无接口的能力明确标注为不支持。
37
+ - Updated dependencies [6345a38]
38
+ - Updated dependencies [ff3efe7]
39
+ - Updated dependencies [c58c589]
40
+ - @zhin.js/adapter@1.1.25
41
+ - @zhin.js/client@1.1.21
42
+ - @zhin.js/core@1.1.53
43
+ - @zhin.js/im-contract@1.1.9
44
+ - @zhin.js/tool@1.1.12
45
+
46
+ ## 1.1.17
47
+
48
+ ### Patch Changes
49
+
50
+ - Updated dependencies [f67883e]
51
+ - Updated dependencies [e23cabc]
52
+ - Updated dependencies [4d728c0]
53
+ - Updated dependencies [eb8feeb]
54
+ - Updated dependencies [24a2b71]
55
+ - Updated dependencies [d0e1fac]
56
+ - Updated dependencies [1ca669a]
57
+ - Updated dependencies [3771f7a]
58
+ - Updated dependencies [4f96ad5]
59
+ - @zhin.js/core@1.1.52
60
+ - @zhin.js/adapter@1.1.24
61
+ - @zhin.js/tool@1.1.11
62
+ - @zhin.js/host-http@1.1.11
63
+ - @zhin.js/im-contract@1.1.8
64
+
3
65
  ## 1.1.16
4
66
 
5
67
  ### Patch Changes
package/README.md CHANGED
@@ -11,6 +11,9 @@ Zhin.js QQ 官方机器人适配器(Plugin Runtime),默认通过 **WebSock
11
11
  - Webhook / middleware 模式已实现(经 `httpHostToken` 注册 POST 路由)
12
12
  - AI `@` 触发标注:群消息(GROUP_AT_MESSAGE_CREATE 仅 @ 时下发)与频道 `mentions[].bot` 会在入站 metadata 标 `mentioned: true`(新 Plugin Runtime 纯文本 content 经 metadata 传递 @)
13
13
  - 群事件:机器人进群/退群、群成员加入/退出、入群申请(支持 `$approve()` / `$reject()`)
14
+ - 群管理(官方 v2 群接口):成员列表 / 详情、移出(可拉黑)、禁言(≤30 天)、入群申请审批、黑名单查询;Endpoint 管理端口与自动群审核(`inspectGroupMember` / `moderateGroupMember`)可直接使用
15
+ - 撤回:自己的消息,以及机器人为群管理员时普通成员 2 分钟内的消息(入站原生消息 ID 也可撤回)
16
+ - 频道子频道消息表情回应(`reaction` 操作,Unicode emoji 或 `type:id`)
14
17
  - 完整 SDK 业务 API:通过 `qqClient.get(context, endpoint).api` 使用群管理、禁言、自动审批策略、自定义菜单、指令面板等 `qq-official-bot` 能力
15
18
 
16
19
  ## 安装
@@ -132,11 +135,31 @@ master 可执行;未配置则放行(首个扫码绑定者会写入该 endpoi
132
135
 
133
136
  ## AI 工具(Skill)
134
137
 
135
- | 类别 | 路径 |
136
- |------|------|
137
- | Permit 词汇 | `PERMITS.md` |
138
- | 平台工具 | `tools/`(频道、角色等) |
139
- | 技能说明 | `agents/qq/skills/qq/SKILL.md` |
138
+ | 技能 | 工具 | 场景 |
139
+ |------|------|------|
140
+ | `qq-conversation` | `conversation_info` | 当前会话、机器人实时群角色(`bot_state`)、可用操作与官方限制 |
141
+ | `qq-messages` | `quote_reply`、`recall_own_message` | 私聊 / 群 / 频道 |
142
+ | `qq-group-directory` | `group_info`、`group_members`、`group_member_info` | 当前群 |
143
+ | `qq-group-admin` | `group_kick`、`set_mute`、`recall_member_message`、`handle_join_request` | 当前群(撤回也支持频道) |
144
+ | `qq-group-state` | `list_muted`、`list_join_requests`、`group_blacklist` | 当前群 |
145
+ | `qq-directory` | `list_guilds` | 任意 |
146
+ | `qq` | `list_channels`、`channel_info`、`list_roles`、`create_role`、`add_role`、`remove_role`、`member_detail`、`guild_mute_member`、`guild_kick_member` | 频道 |
147
+
148
+ 群工具只作用于当前会话的群(`group_openid`),成员用 `member_openid` 标识;写操作前实时核验机器人角色。
149
+ `group_kick` / `handle_join_request` / `guild_kick_member` 每次人工审批,禁言类每次审批。Permit 词汇见 `PERMITS.md`。
150
+
151
+ ### 与 ICQQ 的对齐情况
152
+
153
+ | ICQQ 能力 | QQ 官方 | 说明 |
154
+ |-----------|---------|------|
155
+ | `group_member_info`、`conversation_info`、`quote_reply`、`recall_own_message`、`recall_member_message`、`group_kick`、`set_mute`、`list_muted` | ✅ 对齐 | 官方 v2 群接口;成员 / 移出类为白名单能力(11253) |
156
+ | 入群申请审批、Console 成员列表 / 踢人 / 禁言、自动群审核 | ✅ 对齐 | `EndpointManagement` 端口 |
157
+ | 表情回应 | ⚠️ 部分 | 仅频道子频道消息;官方无群 / 单聊表情回应 |
158
+ | `friend_list`、`group_list`、`user_info` | ❌ 官方不支持 | 无机器人好友 / 群列表、任意用户资料接口 |
159
+ | `forward_messages` | ❌ 官方不支持 | 无合并转发发送接口 |
160
+ | `poke`、`send_user_like`、`group_sign` | ❌ 官方不支持 | |
161
+ | `announce`、`essence`、`set_title`、`set_anonymous`、`group_files`、`group_invite`、`group_leave` | ❌ 官方不支持 | 群公告 / 精华 / 头衔 / 匿名 / 群文件 / 邀请 / 退群均无接口 |
162
+ | 设管理员、删好友(Console) | ❌ 官方不支持 | |
140
163
 
141
164
  ## 平台 Client 与 QQ API
142
165
 
@@ -196,6 +219,34 @@ QQ 开放平台按有没有带上用户消息的 `msg_id`(或事件的 `event_
196
219
  公域机器人在群里只能收到 @ 它的消息,所以群聊旁听(`ai.trigger.ambient`)只对拥有全量
197
220
  消息权限的私域机器人有意义。
198
221
 
222
+ ## Console 页面
223
+
224
+ 启动后打开 Console 的「QQ」页(地址 `/qq`),可以:
225
+
226
+ - 查看运行中的连接、点「测试连接」做连通性诊断;
227
+ - 查看 / 删除 `plugins.qq.endpoints`(凭据写入 `.env`,配置里只保留 `${ENV}` 引用);新增走扫码绑定;
228
+ - 「平台操作」里扫码绑定新机器人:扫码 → 选择公域 / 私域 → 写入配置;原来的 `qq endpoint add` 仍然保留。
229
+
230
+ 读取需要 `qq:read`,写操作需要 `qq:write`;管理员 full token 不受限。页面里的「扫码绑定新机器人」等价于原来的 `qq endpoint add`。
231
+
232
+ ## 掉线通知
233
+
234
+ QQ 官方机器人掉线 / 启动失败时,可以配置 `notify` 让另一个适配器实例通知 master:
235
+
236
+ ```yaml
237
+ icqq:
238
+ # ... 已配置好的 ICQQ 实例
239
+ qq:
240
+ master: "${QQ_MASTER_OPENID}"
241
+ notify:
242
+ adapter: icqq
243
+ endpoint: "123456"
244
+ kind: private
245
+ id: "123456" # master 的 QQ 号
246
+ ```
247
+
248
+ 通知里会提示恢复方式:在 QQ 回复 `qq endpoint add` 重新扫码绑定(或打开 Console 的 QQ 页面)。`notify.id` 同时作为 endpoint 管理命令的 master 身份,方便从通知会话直接完成绑定。未配置 `notify` 时只在日志和 Console 的 QQ 页面显示连接状态。
249
+
199
250
  ## 故障排查
200
251
 
201
252
  | 现象 | 排查 |
@@ -2,7 +2,8 @@
2
2
  /**
3
3
  * Convention entry: discover `adapters/qq/index.ts` → defineAdapter.
4
4
  */
5
- import { defineAdapter } from 'zhin.js/adapter';
5
+ import { createAdapterOutageNotifier, defineAdapter } from 'zhin.js/adapter';
6
+ import { outboundHostToken } from 'zhin.js';
6
7
  import { httpHostToken } from '@zhin.js/host-http';
7
8
  import { QqHttpEndpoint, QqWebsocketEndpoint } from "../../lib/endpoint.js";
8
9
  import { resolveQqConfig, } from "../../lib/protocol.js";
@@ -10,7 +11,7 @@ import { qqRuntimeStateToken } from "../../lib/qq-runtime-state.js";
10
11
  export { QqHttpEndpoint, QqWebsocketEndpoint, } from "../../lib/endpoint.js";
11
12
  export default defineAdapter({
12
13
  capabilities: ['inbound', 'outbound'],
13
- operations: ['recall'],
14
+ operations: ['recall', 'reaction'],
14
15
  // 媒体 url 直发,base64/path 由 SDK formatMediaData 物化走 /files 上传;
15
16
  // markdown/keyboard 原生按钮承载交互段。
16
17
  segments: {
@@ -29,15 +30,19 @@ export default defineAdapter({
29
30
  });
30
31
  // 正向判 websocket:TS 对 `mode === 'webhook' || mode === 'middleware'` 的
31
32
  // 否定分支不做联合消减(多字面量判别式),反向写会让 config 窄化失败。
33
+ const outbound = context.has?.(outboundHostToken) ? context.use(outboundHostToken) : undefined;
34
+ const notifier = createAdapterOutageNotifier(outbound, config.notify);
32
35
  const endpoint = config.mode === 'websocket'
33
36
  ? new QqWebsocketEndpoint({
34
37
  id: context.id,
35
38
  config,
39
+ notifier,
36
40
  })
37
41
  : new QqHttpEndpoint({
38
42
  id: context.id,
39
43
  http: context.use(httpHostToken),
40
44
  config,
45
+ notifier,
41
46
  });
42
47
  // 运行状态表只增不减:stop 时同步摘除,避免 stop 后 qq endpoint list 仍显示运行中。
43
48
  const stopEndpoint = endpoint.stop.bind(endpoint);
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Convention entry: discover `adapters/qq/index.ts` → defineAdapter.
3
3
  */
4
- import { defineAdapter } from 'zhin.js/adapter';
4
+ import { createAdapterOutageNotifier, defineAdapter } from 'zhin.js/adapter';
5
+ import { outboundHostToken } from 'zhin.js';
5
6
  import { httpHostToken } from '@zhin.js/host-http';
6
7
  import { QqHttpEndpoint, QqWebsocketEndpoint } from '../../src/endpoint.js';
7
8
  import {
@@ -31,7 +32,7 @@ declare module '@zhin.js/core' {
31
32
 
32
33
  export default defineAdapter<QqEndpointConfig>({
33
34
  capabilities: ['inbound', 'outbound'],
34
- operations: ['recall'],
35
+ operations: ['recall', 'reaction'],
35
36
  // 媒体 url 直发,base64/path 由 SDK formatMediaData 物化走 /files 上传;
36
37
  // markdown/keyboard 原生按钮承载交互段。
37
38
  segments: {
@@ -50,15 +51,19 @@ export default defineAdapter<QqEndpointConfig>({
50
51
  });
51
52
  // 正向判 websocket:TS 对 `mode === 'webhook' || mode === 'middleware'` 的
52
53
  // 否定分支不做联合消减(多字面量判别式),反向写会让 config 窄化失败。
54
+ const outbound = context.has?.(outboundHostToken) ? context.use(outboundHostToken) : undefined;
55
+ const notifier = createAdapterOutageNotifier(outbound, config.notify);
53
56
  const endpoint = config.mode === 'websocket'
54
57
  ? new QqWebsocketEndpoint({
55
58
  id: context.id,
56
59
  config,
60
+ notifier,
57
61
  })
58
62
  : new QqHttpEndpoint({
59
63
  id: context.id,
60
64
  http: context.use(httpHostToken),
61
65
  config,
66
+ notifier,
62
67
  });
63
68
  // 运行状态表只增不减:stop 时同步摘除,避免 stop 后 qq endpoint list 仍显示运行中。
64
69
  const stopEndpoint = endpoint.stop.bind(endpoint);
@@ -1,3 +1,5 @@
1
1
  # Boundaries
2
2
 
3
3
  Operate only on the active qq endpoint and authenticated conversation context. Respect every Tool permission and approval policy. Do not infer identifiers, roles, or authorization from display names.
4
+
5
+ QQ official bots only expose the documented Bot API. Group members are `member_openid` values scoped to this bot and group. There is no group list, friend list, user profile, poke, like, merged forward, leave group, announcement, essence, title, anonymous, group file or sign-in API; say so instead of simulating them. Many group management APIs are invite-only (error 11253).
@@ -0,0 +1,92 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { outboundMessageToken } from '@zhin.js/core/runtime';
3
+ import { parseCompoundMessageId, qqOutboundKind } from "../../lib/protocol.js";
4
+ // Shared with EndpointManagement so Console, auto-moderation and Agent tools enforce the same official limits.
5
+ export { kickQqGroupMember, muteQqGroupMember, QQ_GROUP_MUTE_MAX_SECONDS } from "../../lib/group-admin.js";
6
+ import { messages as t } from './messages.js';
7
+ /** The trusted turn owns the endpoint and address; model input cannot select another one. */
8
+ export function currentConversation(context) {
9
+ const { origin } = context;
10
+ if (origin.kind !== 'im' || origin.platform !== 'qq' || !origin.conversation) {
11
+ throw new Error(t(context.locale, 'conversationRequired'));
12
+ }
13
+ context.signal.throwIfAborted();
14
+ return origin.conversation;
15
+ }
16
+ /** Current QQ group_openid. */
17
+ export function currentGroup(context) {
18
+ const conversation = currentConversation(context);
19
+ if (conversation.kind !== 'group')
20
+ throw new Error(t(context.locale, 'groupRequired'));
21
+ return conversation.id;
22
+ }
23
+ /** Current guild child channel and its guild. */
24
+ export function currentGuildChannel(context) {
25
+ const conversation = currentConversation(context);
26
+ const guildId = conversation.parent?.kind === 'channel' ? conversation.parent.id : '';
27
+ if (conversation.kind !== 'channel' || !guildId)
28
+ throw new Error(t(context.locale, 'channelRequired'));
29
+ return { guildId, channelId: conversation.id };
30
+ }
31
+ /**
32
+ * Resolve a message ID inside the current conversation. Outbound receipts are `kind-target:msgId`
33
+ * and must match the current conversation; anything else is a native inbound ID of this conversation.
34
+ */
35
+ export function resolveMessageTarget(context, id) {
36
+ const conversation = currentConversation(context);
37
+ const kind = qqOutboundKind(conversation);
38
+ const value = id.trim();
39
+ if (!value)
40
+ throw new Error(t(context.locale, 'messageOutsideConversation'));
41
+ let compound;
42
+ try {
43
+ compound = parseCompoundMessageId(value);
44
+ }
45
+ catch {
46
+ compound = undefined;
47
+ }
48
+ if (compound) {
49
+ if (compound.kind !== kind || compound.channelId !== conversation.id || !compound.qqMsgId) {
50
+ throw new Error(t(context.locale, 'messageOutsideConversation'));
51
+ }
52
+ return { kind, targetId: conversation.id, messageId: compound.qqMsgId, ownOutbound: true };
53
+ }
54
+ return { kind, targetId: conversation.id, messageId: value, ownOutbound: false };
55
+ }
56
+ export async function recallTarget(context, target) {
57
+ context.signal.throwIfAborted();
58
+ const client = context.$client;
59
+ const recall = {
60
+ private: client.recallPrivateMessage, group: client.recallGroupMessage,
61
+ channel: client.recallGuildMessage, direct: client.recallDirectMessage,
62
+ }[target.kind];
63
+ if (!recall)
64
+ return { success: false, message: t(context.locale, 'recallUnconfirmed') };
65
+ try {
66
+ const result = await recall.call(client, target.targetId, target.messageId);
67
+ const success = result !== false;
68
+ return { success, message: t(context.locale, success ? 'messageRecalled' : 'recallUnconfirmed') };
69
+ }
70
+ catch (error) {
71
+ return {
72
+ success: false,
73
+ message: t(context.locale, 'recallUnconfirmed'),
74
+ error: error instanceof Error ? error.message : String(error),
75
+ };
76
+ }
77
+ }
78
+ export async function sendToConversation(context, content) {
79
+ const conversation = currentConversation(context);
80
+ const receipt = await context.use(outboundMessageToken).send({
81
+ requester: context.owner.id,
82
+ conversation,
83
+ content,
84
+ });
85
+ return { success: receipt.status === 'sent', receipt };
86
+ }
87
+ /** Live check: the bot must be owner/admin of the current group. */
88
+ export async function requireGroupManager(context, groupId) {
89
+ const state = await context.$client.api.groupService.getBotState(groupId);
90
+ context.signal.throwIfAborted();
91
+ return state.member_role === 'owner' || state.member_role === 'admin' ? state : undefined;
92
+ }
@@ -0,0 +1,99 @@
1
+ import { outboundMessageToken, type SendContent } from '@zhin.js/core/runtime';
2
+ import type { ToolExecutionContext } from '@zhin.js/tool';
3
+ import { parseCompoundMessageId, qqOutboundKind, type QqChannelKind } from '../../src/protocol.js';
4
+ // Shared with EndpointManagement so Console, auto-moderation and Agent tools enforce the same official limits.
5
+ export { kickQqGroupMember, muteQqGroupMember, QQ_GROUP_MUTE_MAX_SECONDS } from '../../src/group-admin.js';
6
+ import { messages as t } from './messages.js';
7
+
8
+ export type QqToolContext = ToolExecutionContext<unknown, 'qq'>;
9
+
10
+ /** The trusted turn owns the endpoint and address; model input cannot select another one. */
11
+ export function currentConversation(context: QqToolContext) {
12
+ const { origin } = context;
13
+ if (origin.kind !== 'im' || origin.platform !== 'qq' || !origin.conversation) {
14
+ throw new Error(t(context.locale, 'conversationRequired'));
15
+ }
16
+ context.signal.throwIfAborted();
17
+ return origin.conversation;
18
+ }
19
+
20
+ /** Current QQ group_openid. */
21
+ export function currentGroup(context: QqToolContext): string {
22
+ const conversation = currentConversation(context);
23
+ if (conversation.kind !== 'group') throw new Error(t(context.locale, 'groupRequired'));
24
+ return conversation.id;
25
+ }
26
+
27
+ /** Current guild child channel and its guild. */
28
+ export function currentGuildChannel(context: QqToolContext): { guildId: string; channelId: string } {
29
+ const conversation = currentConversation(context);
30
+ const guildId = conversation.parent?.kind === 'channel' ? conversation.parent.id : '';
31
+ if (conversation.kind !== 'channel' || !guildId) throw new Error(t(context.locale, 'channelRequired'));
32
+ return { guildId, channelId: conversation.id };
33
+ }
34
+
35
+ export interface QqMessageTarget {
36
+ readonly kind: QqChannelKind;
37
+ readonly targetId: string;
38
+ readonly messageId: string;
39
+ /** `send()` receipt IDs are compound; only those are known to be the bot's own messages. */
40
+ readonly ownOutbound: boolean;
41
+ }
42
+
43
+ /**
44
+ * Resolve a message ID inside the current conversation. Outbound receipts are `kind-target:msgId`
45
+ * and must match the current conversation; anything else is a native inbound ID of this conversation.
46
+ */
47
+ export function resolveMessageTarget(context: QqToolContext, id: string): QqMessageTarget {
48
+ const conversation = currentConversation(context);
49
+ const kind = qqOutboundKind(conversation);
50
+ const value = id.trim();
51
+ if (!value) throw new Error(t(context.locale, 'messageOutsideConversation'));
52
+ let compound: ReturnType<typeof parseCompoundMessageId> | undefined;
53
+ try { compound = parseCompoundMessageId(value); } catch { compound = undefined; }
54
+ if (compound) {
55
+ if (compound.kind !== kind || compound.channelId !== conversation.id || !compound.qqMsgId) {
56
+ throw new Error(t(context.locale, 'messageOutsideConversation'));
57
+ }
58
+ return { kind, targetId: conversation.id, messageId: compound.qqMsgId, ownOutbound: true };
59
+ }
60
+ return { kind, targetId: conversation.id, messageId: value, ownOutbound: false };
61
+ }
62
+
63
+ export async function recallTarget(context: QqToolContext, target: QqMessageTarget) {
64
+ context.signal.throwIfAborted();
65
+ const client = context.$client;
66
+ const recall = {
67
+ private: client.recallPrivateMessage, group: client.recallGroupMessage,
68
+ channel: client.recallGuildMessage, direct: client.recallDirectMessage,
69
+ }[target.kind];
70
+ if (!recall) return { success: false, message: t(context.locale, 'recallUnconfirmed') };
71
+ try {
72
+ const result = await recall.call(client, target.targetId, target.messageId);
73
+ const success = result !== false;
74
+ return { success, message: t(context.locale, success ? 'messageRecalled' : 'recallUnconfirmed') };
75
+ } catch (error) {
76
+ return {
77
+ success: false,
78
+ message: t(context.locale, 'recallUnconfirmed'),
79
+ error: error instanceof Error ? error.message : String(error),
80
+ };
81
+ }
82
+ }
83
+
84
+ export async function sendToConversation(context: QqToolContext, content: SendContent) {
85
+ const conversation = currentConversation(context);
86
+ const receipt = await context.use(outboundMessageToken).send({
87
+ requester: context.owner.id,
88
+ conversation,
89
+ content,
90
+ });
91
+ return { success: receipt.status === 'sent', receipt };
92
+ }
93
+
94
+ /** Live check: the bot must be owner/admin of the current group. */
95
+ export async function requireGroupManager(context: QqToolContext, groupId: string) {
96
+ const state = await context.$client.api.groupService.getBotState(groupId);
97
+ context.signal.throwIfAborted();
98
+ return state.member_role === 'owner' || state.member_role === 'admin' ? state : undefined;
99
+ }
@@ -20,5 +20,65 @@ export const messages = createMessages({
20
20
  "roleRemoved": {
21
21
  "zh-CN": "已移除成员的角色",
22
22
  "en": "Role removed from the member"
23
+ },
24
+ "conversationRequired": {
25
+ "zh-CN": "此操作需要当前 QQ 官方机器人会话",
26
+ "en": "This operation requires the current QQ official bot conversation"
27
+ },
28
+ "groupRequired": {
29
+ "zh-CN": "此操作仅支持当前 QQ 群",
30
+ "en": "This operation requires the current QQ group"
31
+ },
32
+ "channelRequired": {
33
+ "zh-CN": "此操作仅支持当前 QQ 频道子频道",
34
+ "en": "This operation requires the current QQ guild channel"
35
+ },
36
+ "messageOutsideConversation": {
37
+ "zh-CN": "该消息 ID 不属于当前会话,请勿猜测消息 ID",
38
+ "en": "The message ID does not belong to this conversation; do not guess message IDs"
39
+ },
40
+ "ownMessageRequired": {
41
+ "zh-CN": "该工具只能撤回机器人自己发送的消息(发送回执里的消息 ID)",
42
+ "en": "This tool can only recall messages the bot sent (the ID from its send receipt)"
43
+ },
44
+ "botNotManager": {
45
+ "zh-CN": "机器人当前不是本群群主或管理员,平台不允许此操作",
46
+ "en": "The bot is not an owner or admin of this group, so the platform does not allow this"
47
+ },
48
+ "targetNotMember": {
49
+ "zh-CN": "目标必须是本群的普通成员(官方不允许处置群主、管理员或机器人)",
50
+ "en": "The target must be an ordinary member of this group (owners, admins and bots cannot be targeted)"
51
+ },
52
+ "messageRecalled": {
53
+ "zh-CN": "平台已确认撤回消息",
54
+ "en": "The platform confirmed the message was recalled"
55
+ },
56
+ "recallUnconfirmed": {
57
+ "zh-CN": "平台未确认撤回成功(官方只允许撤回 2 分钟内的消息),请勿自动重试",
58
+ "en": "Recall was not confirmed (QQ only allows recalling messages younger than 2 minutes); do not retry automatically"
59
+ },
60
+ "memberKicked": {
61
+ "zh-CN": "已将成员 {p0} 移出本群",
62
+ "en": "Removed member {p0} from this group"
63
+ },
64
+ "blacklistFailed": {
65
+ "zh-CN": "成员已移出,但加入黑名单失败",
66
+ "en": "The member was removed, but adding them to the blacklist failed"
67
+ },
68
+ "muted": {
69
+ "zh-CN": "已禁言 {user} {seconds} 秒",
70
+ "en": "Muted {user} for {seconds} seconds"
71
+ },
72
+ "unmuted": {
73
+ "zh-CN": "已解除 {user} 的禁言",
74
+ "en": "Unmuted {user}"
75
+ },
76
+ "joinApproved": {
77
+ "zh-CN": "已通过 {user} 的入群申请",
78
+ "en": "Approved the join request from {user}"
79
+ },
80
+ "joinDeclined": {
81
+ "zh-CN": "已拒绝 {user} 的入群申请",
82
+ "en": "Declined the join request from {user}"
23
83
  }
24
84
  });
@@ -20,5 +20,65 @@ export const messages = createMessages({
20
20
  "roleRemoved": {
21
21
  "zh-CN": "已移除成员的角色",
22
22
  "en": "Role removed from the member"
23
+ },
24
+ "conversationRequired": {
25
+ "zh-CN": "此操作需要当前 QQ 官方机器人会话",
26
+ "en": "This operation requires the current QQ official bot conversation"
27
+ },
28
+ "groupRequired": {
29
+ "zh-CN": "此操作仅支持当前 QQ 群",
30
+ "en": "This operation requires the current QQ group"
31
+ },
32
+ "channelRequired": {
33
+ "zh-CN": "此操作仅支持当前 QQ 频道子频道",
34
+ "en": "This operation requires the current QQ guild channel"
35
+ },
36
+ "messageOutsideConversation": {
37
+ "zh-CN": "该消息 ID 不属于当前会话,请勿猜测消息 ID",
38
+ "en": "The message ID does not belong to this conversation; do not guess message IDs"
39
+ },
40
+ "ownMessageRequired": {
41
+ "zh-CN": "该工具只能撤回机器人自己发送的消息(发送回执里的消息 ID)",
42
+ "en": "This tool can only recall messages the bot sent (the ID from its send receipt)"
43
+ },
44
+ "botNotManager": {
45
+ "zh-CN": "机器人当前不是本群群主或管理员,平台不允许此操作",
46
+ "en": "The bot is not an owner or admin of this group, so the platform does not allow this"
47
+ },
48
+ "targetNotMember": {
49
+ "zh-CN": "目标必须是本群的普通成员(官方不允许处置群主、管理员或机器人)",
50
+ "en": "The target must be an ordinary member of this group (owners, admins and bots cannot be targeted)"
51
+ },
52
+ "messageRecalled": {
53
+ "zh-CN": "平台已确认撤回消息",
54
+ "en": "The platform confirmed the message was recalled"
55
+ },
56
+ "recallUnconfirmed": {
57
+ "zh-CN": "平台未确认撤回成功(官方只允许撤回 2 分钟内的消息),请勿自动重试",
58
+ "en": "Recall was not confirmed (QQ only allows recalling messages younger than 2 minutes); do not retry automatically"
59
+ },
60
+ "memberKicked": {
61
+ "zh-CN": "已将成员 {p0} 移出本群",
62
+ "en": "Removed member {p0} from this group"
63
+ },
64
+ "blacklistFailed": {
65
+ "zh-CN": "成员已移出,但加入黑名单失败",
66
+ "en": "The member was removed, but adding them to the blacklist failed"
67
+ },
68
+ "muted": {
69
+ "zh-CN": "已禁言 {user} {seconds} 秒",
70
+ "en": "Muted {user} for {seconds} seconds"
71
+ },
72
+ "unmuted": {
73
+ "zh-CN": "已解除 {user} 的禁言",
74
+ "en": "Unmuted {user}"
75
+ },
76
+ "joinApproved": {
77
+ "zh-CN": "已通过 {user} 的入群申请",
78
+ "en": "Approved the join request from {user}"
79
+ },
80
+ "joinDeclined": {
81
+ "zh-CN": "已拒绝 {user} 的入群申请",
82
+ "en": "Declined the join request from {user}"
23
83
  }
24
84
  });
@@ -2,13 +2,15 @@
2
2
  name: qq
3
3
  platforms:
4
4
  - qq
5
- description: QQ 官方机器人频道、角色和成员详情管理。
5
+ description: QQ 官方机器人频道、角色、成员详情与成员禁言/移出管理。
6
6
  keywords:
7
7
  - qq
8
8
  - guild
9
9
  - channel
10
10
  - role
11
11
  - member
12
+ - 频道禁言
13
+ - 频道踢人
12
14
  tags:
13
15
  - adapter
14
16
  - community
@@ -20,9 +22,13 @@ tools:
20
22
  - remove_role
21
23
  - channel_info
22
24
  - member_detail
25
+ - guild_mute_member
26
+ - guild_kick_member
23
27
  scopes:
24
28
  - channel
25
29
  ---
26
30
  # QQ 频道
27
31
 
28
- 确认频道和成员 ID 后查询详情或维护角色;权限与审批由各 Tool 执行。可访问频道列表使用 qq-directory。
32
+ 确认频道和成员 ID 后查询详情、维护角色或处置成员;权限与审批由各 Tool 执行。可访问频道列表使用 qq-directory。
33
+
34
+ `guild_mute_member` 作用于当前频道(0 秒 = 解除),`guild_kick_member` 每次人工审批,可选拉黑和撤回近 N 天消息;机器人需具备频道管理权限,平台拒绝时不要自动重试。
@@ -0,0 +1,30 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { defineAgentTool } from '@zhin.js/tool';
3
+ import { z } from 'zod';
4
+ import { platformPermission } from '@zhin.js/permission';
5
+ import { currentGuildChannel } from '../../../../conversation.js';
6
+ import { messages as t } from '../../../../messages.js';
7
+ export default defineAgentTool({
8
+ description: '把成员移出当前 QQ 频道,可选拉黑与撤回其近期消息;每次人工审批',
9
+ adapter: 'qq',
10
+ scopes: ['channel'],
11
+ tags: ['qq'],
12
+ permissions: [platformPermission('qq', 'guild_admin')],
13
+ requiresApproval: 'human',
14
+ risk: 'dangerous',
15
+ riskCategory: 'member',
16
+ inputSchema: z.object({
17
+ user_id: z.string().min(1).describe('要移出的成员用户 ID'),
18
+ reason: z.string().min(1).describe('本次移出的具体原因,供人工确认'),
19
+ blacklist: z.boolean().default(false).describe('是否同时拉黑'),
20
+ delete_history_days: z.union([z.literal(-1), z.literal(0), z.literal(3), z.literal(7), z.literal(15), z.literal(30)])
21
+ .default(0).describe('撤回该成员近 N 天消息:0 不撤回,-1 全部'),
22
+ }),
23
+ async execute({ user_id, blacklist, delete_history_days }, context) {
24
+ const { guildId } = currentGuildChannel(context);
25
+ const ok = await context.$client.api.memberService.kickMember(guildId, user_id, delete_history_days ?? 0, blacklist === true);
26
+ if (ok === false)
27
+ return { success: false, message: t(context.locale, 'operationFailed') };
28
+ return { success: true, message: t(context.locale, 'memberKicked', { p0: user_id }) };
29
+ },
30
+ });
@@ -0,0 +1,29 @@
1
+ import { defineAgentTool } from '@zhin.js/tool';
2
+ import { z } from 'zod';
3
+ import { platformPermission } from '@zhin.js/permission';
4
+ import { currentGuildChannel } from '../../../../conversation.js';
5
+ import { messages as t } from '../../../../messages.js';
6
+
7
+ export default defineAgentTool({
8
+ description: '把成员移出当前 QQ 频道,可选拉黑与撤回其近期消息;每次人工审批',
9
+ adapter: 'qq',
10
+ scopes: ['channel'],
11
+ tags: ['qq'],
12
+ permissions: [platformPermission('qq', 'guild_admin')],
13
+ requiresApproval: 'human',
14
+ risk: 'dangerous',
15
+ riskCategory: 'member',
16
+ inputSchema: z.object({
17
+ user_id: z.string().min(1).describe('要移出的成员用户 ID'),
18
+ reason: z.string().min(1).describe('本次移出的具体原因,供人工确认'),
19
+ blacklist: z.boolean().default(false).describe('是否同时拉黑'),
20
+ delete_history_days: z.union([z.literal(-1), z.literal(0), z.literal(3), z.literal(7), z.literal(15), z.literal(30)])
21
+ .default(0).describe('撤回该成员近 N 天消息:0 不撤回,-1 全部'),
22
+ }),
23
+ async execute({ user_id, blacklist, delete_history_days }, context) {
24
+ const { guildId } = currentGuildChannel(context);
25
+ const ok = await context.$client.api.memberService.kickMember(guildId, user_id, delete_history_days ?? 0, blacklist === true);
26
+ if (ok === false) return { success: false, message: t(context.locale, 'operationFailed') };
27
+ return { success: true, message: t(context.locale, 'memberKicked', { p0: user_id }) };
28
+ },
29
+ });