@zhin.js/adapter-icqq 1.1.21 → 1.1.22

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 (32) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/PERMITS.md +1 -0
  3. package/README.md +1 -1
  4. package/adapters/icqq/index.js +96 -25
  5. package/adapters/icqq/index.ts +97 -27
  6. package/agents/icqq/messages.js +20 -0
  7. package/agents/icqq/messages.ts +20 -0
  8. package/agents/icqq/skills/icqq-group-admin/SKILL.md +5 -0
  9. package/agents/icqq/skills/icqq-group-admin/tools/announce/index.js +2 -0
  10. package/agents/icqq/skills/icqq-group-admin/tools/announce/index.ts +2 -0
  11. package/agents/icqq/skills/icqq-group-admin/tools/essence/index.js +2 -0
  12. package/agents/icqq/skills/icqq-group-admin/tools/essence/index.ts +2 -0
  13. package/agents/icqq/skills/icqq-group-admin/tools/group_kick/index.js +2 -0
  14. package/agents/icqq/skills/icqq-group-admin/tools/group_kick/index.ts +2 -0
  15. package/agents/icqq/skills/icqq-group-admin/tools/recall_member_message/index.js +2 -0
  16. package/agents/icqq/skills/icqq-group-admin/tools/recall_member_message/index.ts +2 -0
  17. package/agents/icqq/skills/icqq-group-admin/tools/set_group_admin/index.js +43 -0
  18. package/agents/icqq/skills/icqq-group-admin/tools/set_group_admin/index.ts +36 -0
  19. package/agents/icqq/skills/icqq-group-admin/tools/set_mute/index.js +2 -0
  20. package/agents/icqq/skills/icqq-group-admin/tools/set_mute/index.ts +2 -0
  21. package/agents/icqq/skills/icqq-group-admin/tools/set_title/index.js +2 -0
  22. package/agents/icqq/skills/icqq-group-admin/tools/set_title/index.ts +2 -0
  23. package/lib/endpoint.js +4 -1
  24. package/lib/group-actions.d.ts +10 -0
  25. package/lib/group-actions.js +94 -0
  26. package/lib/sensitive-commands.d.ts +8 -0
  27. package/lib/sensitive-commands.js +16 -0
  28. package/package.json +24 -18
  29. package/plugin.js +8 -1
  30. package/src/endpoint.ts +4 -1
  31. package/src/group-actions.ts +105 -0
  32. package/src/sensitive-commands.ts +19 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # @zhin.js/adapter-icqq
2
2
 
3
+ ## 1.1.22
4
+
5
+ ### Patch Changes
6
+
7
+ - b249e5e: ADR 0044 C4:事件映射迁出 core。新增 `@zhin.js/adapter-onebot-events`,承载 OneBot v11 / NapCat / icqq 的原始通知与请求 → 标准事件映射(`receiveOneBotLikeSideEvent`、`mapNoticeParts`、`mapRequestParts`、`resolveSideEventDedupeKey` 及 OneBot 映射表,icqq 差异用覆盖表表达);KOOK、Slack 的映射迁入各自适配器;`@zhin.js/core` 只保留标准事件名、类型与 `buildNotice` / `buildRequest` / `buildSystem` / `senderFromId`,不再含任何平台名。core 不再导出上述映射函数与 `ONEBOT_/KOOK_/SLACK_NOTICE_PARTS_MAP`、`SideEventPlatform`、`OneBotLike*` 类型,适配器改从共享包导入。
8
+ - 41ff1a5: ADR 0044 C5: every official adapter now declares a capability `profile` (its `segments` moved into `profile.default.messaging.segments`). icqq declares the complete profile (per conversation kind: scene, all 14 actions with real limits, events, feedback, delivery) and implements the seven new management ports (`pokeUser`, `unmuteGroupMember`, `muteAllGroupMembers`, `setGroupCard`, `setGroupTitle`, `setGroupEssence`, `inviteGroupMember`) with fresh bot-role reads. qq official declares its passive reply windows; undeclared capabilities stay unsupported.
9
+ - cbd3f14: ADR 0044 integration and C6 guard. `delivery.spool` now reproduces the pre-0044 media rule (icqq `false`, every other official adapter `true`). icqq narrows `actions.recall.others` unless the Bot is admin or owner, and declares recall for groups. Endpoint kind capabilities merge the Endpoint's `operations` (Discord reactions keep working; Discord declares reaction feedback). Adapters in the `@zhin.js/` scope are trusted: their custom prompt text is not labelled `external_content`. Core no longer keeps a telegram/discord interactive-policy table (adapters declare `segments.interactive`). New `pnpm check:no-platform-branches` blocks platform-name branches in runtime code.
10
+ - 299863a: ADR 0044 C3: runtime consumers read capabilities instead of platform names. Scene facts (group info, member profile) are queried where the profile declares `scene.groupInfo` / `scene.memberProfile`; activity feedback (reaction, typing, status message, refresh interval, quote-only delivery) follows `feedback`, `actions.reaction` and `delivery.proactive` and the per-platform table is gone; large inline media is spooled only where `delivery.spool` is declared; the sandbox ingress and its console shell follow a new additive `runtime.sandbox` capability. `defineAgentTool` and Skill frontmatter accept `requiresCapabilities` (for example `actions.mute`), evaluated against the per-turn `TurnCapabilities`, so moderation tools disappear when the Bot is not an administrator and return when it is; the icqq/qq keyword boost in tool selection is removed and the icqq group-admin tools are tagged. The `icqq` command-line special case in the exec policy is now a generic `registerSensitiveExecCommand` rule registered by the icqq plugin. `EndpointCapabilities` gains a serializable per-kind `kinds` view. Adapters without a profile keep their previous behaviour (their derived profile also reads the management ports they implement).
11
+ - 4fe49b5: 支持配置按需生效:分离保存与有效版本,独立变更先应用,账号资料更新保留连接,模型按回合使用确定版本;Console 返回结构化应用结果。监听改为仅显式 --watch 启用,开发模板显式选择源码和配置监听。
12
+
13
+ 自变更消费配置应用回执,避免已处理配置被二次重载;持久化本次写入修订和应用结果,未知结果或修订冲突时停止自动续作并保留现场,恢复不会覆盖未经本次变更确认的配置。
14
+
15
+ - 2766d09: New `set_group_admin` Agent tool in `icqq-group-admin`: set (`enabled: true`) or remove (`enabled: false`) a QQ group administrator. It is injected only when the bot is the current group's owner (not when it is merely an admin), requires `role(master,admin,trusted,owner)` plus per-call human approval, and re-checks live roles before calling `setGroupAdmin` (bot still owner; promote only regular members; demote only current admins). An unconfirmed platform result is reported as such and never retried automatically.
16
+ - Updated dependencies [b249e5e]
17
+ - Updated dependencies [57661f6]
18
+ - Updated dependencies [cbd3f14]
19
+ - Updated dependencies [299863a]
20
+ - Updated dependencies [aaa93d7]
21
+ - Updated dependencies [4fe49b5]
22
+ - Updated dependencies [cce07db]
23
+ - Updated dependencies [b652db3]
24
+ - Updated dependencies [0315ba0]
25
+ - Updated dependencies [00be764]
26
+ - @zhin.js/adapter-onebot-events@0.0.1
27
+ - @zhin.js/core@1.1.56
28
+ - @zhin.js/adapter@1.1.28
29
+ - @zhin.js/im-contract@1.1.11
30
+ - @zhin.js/tool@1.1.15
31
+ - @zhin.js/skill@1.1.13
32
+ - @zhin.js/feature-kit@1.1.12
33
+ - @zhin.js/client@1.1.24
34
+ - @zhin.js/page@1.1.14
35
+ - @zhin.js/agent-feature@1.1.13
36
+ - @zhin.js/permission@1.1.12
37
+
3
38
  ## 1.1.21
4
39
 
5
40
  ### Patch Changes
package/PERMITS.md CHANGED
@@ -12,6 +12,7 @@
12
12
  | `icqq-group-admin` / `recall_member_message` | master/admin/trusted/owner | 无逐次审批;Bot 管理员可撤回普通成员,Bot 群主可撤回管理员或普通成员,执行前实时检查 |
13
13
  | `icqq-group-admin` / `group_leave` | 仅 master | 当前群,每次人工审批;Bot 为群主时拒绝,以免 SDK 解散群 |
14
14
  | `icqq-group-admin` / `group_kick` | master/admin/trusted/owner | 每次人工审批;执行前检查 Bot 与目标成员角色 |
15
+ | `icqq-group-admin` / `set_group_admin` | master/admin/trusted/owner | 仅 Bot 为当前群群主时注入;每次人工审批;执行前实时检查 Bot 仍为群主、设管理员目标为普通成员、取消目标为现任管理员 |
15
16
 
16
17
  已配置的自动群审核独立按获准规则执行撤回;上述工具不新增审核触发条件、敏感类别、处罚或目标群。Bot 对自己的消息始终不需要群管权限;平台拒绝或未确认时不能宣称撤回成功,也不自动重试。
17
18
 
package/README.md CHANGED
@@ -171,7 +171,7 @@ IM turn 的场景,应通过 Host 提供的显式账号作用域操作,不能
171
171
 
172
172
  ICQQ Agent 可按需加载 `icqq-conversation` 查询当前群/频道和 Bot 自身角色。`icqq-messages` 提供当前会话的引用回复、真实消息合并转发和撤回自己发言;自己的消息撤回不要求群管身份或逐次审批,时限由平台决定。
173
173
 
174
- `icqq-group-admin` 新增撤回他人消息与退群:前者保留群管发起权限、实时平台角色检查,无需逐次审批;退群仅主人可发起、每次审批,Bot 为群主时拒绝执行。引用/转发走统一发送链路,不支持跨会话转发。当前 guild-channel 接入仅提供身份查询,不能使用这些群/私聊操作。详见 [权限与能力说明](./PERMITS.md)。
174
+ `icqq-group-admin` 新增撤回他人消息与退群:前者保留群管发起权限、实时平台角色检查,无需逐次审批;退群仅主人可发起、每次审批,Bot 为群主时拒绝执行。引用/转发走统一发送链路,不支持跨会话转发。`set_group_admin` 设置/取消管理员仅在 Bot 为当前群群主时注入,每次人工审批并实时复核角色。当前 guild-channel 接入仅提供身份查询,不能使用这些群/私聊操作。详见 [权限与能力说明](./PERMITS.md)。
175
175
 
176
176
  ## Console 页面
177
177
 
@@ -1,38 +1,109 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
- import { IcqqAdapter } from "../../lib/adapter.js";
3
2
  /**
4
3
  * Convention entry: discover `adapters/icqq/index.ts` → defineAdapter.
5
4
  */
6
- import { defineAdapter } from 'zhin.js/adapter';
7
- import {} from "../../lib/protocol.js";
5
+ import { defineAdapter, resolvePlatformRole } from 'zhin.js/adapter';
6
+ import { IcqqAdapter } from "../../lib/adapter.js";
8
7
  export { IcqqEndpoint } from "../../lib/endpoint.js";
8
+ /** Group reactions take QQ system face ids. Private chats have none. */
9
+ const ICQQ_REACTIONS = [
10
+ { value: '76', label: '赞' }, { value: '66', label: '爱心' }, { value: '124', label: 'OK' },
11
+ { value: '99', label: '鼓掌' }, { value: '144', label: '喝彩' }, { value: '320', label: '庆祝' },
12
+ { value: '319', label: '比心' }, { value: '299', label: '牛啊' }, { value: '297', label: '拜谢' },
13
+ { value: '182', label: '笑哭' }, { value: '179', label: 'doge' }, { value: '264', label: '捂脸' },
14
+ { value: '271', label: '吃瓜' }, { value: '269', label: '暗中观察' }, { value: '268', label: '问号脸' },
15
+ { value: '270', label: 'emm' }, { value: '106', label: '委屈' }, { value: '285', label: '摸鱼' },
16
+ { value: '104', label: '哈欠' }, { value: '267', label: '头秃' }, { value: '326', label: '生气' },
17
+ ];
18
+ /** QQ group mute: 1 minute to 30 days (0 lifts it, which is the `unmute` action). */
19
+ const ICQQ_MUTE_SECONDS = { min: 60, max: 2592000 };
9
20
  export default defineAdapter({
10
21
  capabilities: ['inbound', 'outbound'],
11
- operations: ['recall', 'reaction'],
12
- segments: {
13
- // Canonical types formatOutboundBody encodes; markdown / keyboard degrade to text first.
14
- supported: ['text', 'mention', 'image', 'audio', 'video', 'file', 'reply', 'forward', 'face', 'dice', 'rps', 'share'],
15
- outboundMedia: ['base64', 'url', 'path'],
16
- interactive: 'text',
17
- // QQ group reactions take system face ids; private chats have no reactions.
18
- reaction: {
19
- scopes: ['group'],
20
- values: [
21
- { value: '76', label: '赞' }, { value: '66', label: '爱心' }, { value: '124', label: 'OK' },
22
- { value: '99', label: '鼓掌' }, { value: '144', label: '喝彩' }, { value: '320', label: '庆祝' },
23
- { value: '319', label: '比心' }, { value: '299', label: '牛啊' }, { value: '297', label: '拜谢' },
24
- { value: '182', label: '笑哭' }, { value: '179', label: 'doge' }, { value: '264', label: '捂脸' },
25
- { value: '271', label: '吃瓜' }, { value: '269', label: '暗中观察' }, { value: '268', label: '问号脸' },
26
- { value: '270', label: 'emm' }, { value: '106', label: '委屈' }, { value: '285', label: '摸鱼' },
27
- { value: '104', label: '哈欠' }, { value: '267', label: '头秃' }, { value: '326', label: '生气' },
28
- ],
22
+ profile: {
23
+ default: {
24
+ messaging: {
25
+ segments: {
26
+ // Canonical types formatOutboundBody encodes; markdown / keyboard degrade to text first.
27
+ supported: ['text', 'mention', 'image', 'audio', 'video', 'file', 'reply', 'forward', 'face', 'dice', 'rps', 'share'],
28
+ outboundMedia: ['base64', 'url', 'path'],
29
+ interactive: 'text',
30
+ reaction: { scopes: ['group'], values: ICQQ_REACTIONS },
31
+ model: {
32
+ share: {
33
+ fields: ['description', 'image', 'audio', 'platform'],
34
+ hint: 'renders as a QQ link card. For a song, set audio to the playable link, image to the cover, description to the artist and platform to its music service (qq, netease, kugou, kuwo or migu), copied from tool results; leave a field "" when you have no real value.',
35
+ },
36
+ },
37
+ },
38
+ mention: { user: true },
39
+ quote: true,
40
+ recall: { timeLimitSeconds: 120 },
41
+ forward: {},
42
+ },
43
+ scene: { roles: { owner: 'owner', admin: 'admin', member: 'member' } },
44
+ actions: {
45
+ recall: { scopes: ['private', 'group'], params: { timeLimitSeconds: 120 } },
46
+ quoteReply: { scopes: ['private', 'group'] },
47
+ forward: { scopes: ['private', 'group'] },
48
+ poke: { scopes: ['private', 'group'] },
49
+ },
50
+ delivery: { send: true, proactive: true, spool: false },
51
+ },
52
+ private: {
53
+ // A private chat has no reactions, so feedback is a short status message.
54
+ feedback: { mode: 'status-message', text: '⏳' },
55
+ events: { emit: ['message.recalled', 'poke'] },
29
56
  },
30
- model: {
31
- share: {
32
- fields: ['description', 'image', 'audio', 'platform'],
33
- hint: 'renders as a QQ link card. For a song, set audio to the playable link, image to the cover, description to the artist and platform to its music service (qq, netease, kugou, kuwo or migu), copied from tool results; leave a field "" when you have no real value.',
57
+ group: {
58
+ messaging: { reaction: { emojis: ICQQ_REACTIONS.map((item) => item.value) } },
59
+ scene: {
60
+ groupInfo: true,
61
+ memberProfile: true,
62
+ card: true,
63
+ title: true,
64
+ mute: { durationSeconds: ICQQ_MUTE_SECONDS, all: true },
65
+ },
66
+ actions: {
67
+ // Own messages need no role; recalling others' needs an admin (narrowed in resolveCapabilities).
68
+ recall: { scopes: ['private', 'group'], params: { timeLimitSeconds: 120, others: true } },
69
+ reaction: { scopes: ['group'], params: { emojis: ICQQ_REACTIONS.map((item) => item.value), own: true, others: true, cancellable: true } },
70
+ mute: { scopes: ['group'], requiresRole: 'admin', params: { durationSeconds: ICQQ_MUTE_SECONDS } },
71
+ unmute: { scopes: ['group'], requiresRole: 'admin' },
72
+ muteAll: { scopes: ['group'], requiresRole: 'admin' },
73
+ kick: { scopes: ['group'], requiresRole: 'admin' },
74
+ setCard: { scopes: ['group'], requiresRole: 'admin' },
75
+ setTitle: { scopes: ['group'], requiresRole: 'owner' },
76
+ setAdmin: { scopes: ['group'], requiresRole: 'owner' },
77
+ essence: { scopes: ['group'], requiresRole: 'admin' },
78
+ announce: { scopes: ['group'], requiresRole: 'admin' },
79
+ invite: { scopes: ['group'] },
80
+ },
81
+ events: {
82
+ emit: ['member.joined', 'member.left', 'member.role_changed', 'member.muted', 'message.recalled', 'poke'],
83
+ },
84
+ feedback: { mode: 'reaction', emoji: '⏳' },
85
+ prompt: {
86
+ text: '被禁言或不是管理员时不要尝试管理类操作;头衔和设/撤管理员只有群主能做;邀请只能邀请你的好友。',
34
87
  },
35
88
  },
89
+ channel: {
90
+ // Guild channels: text and media in and out; no recall, poke, forward, reaction or management actions.
91
+ actions: {
92
+ recall: false,
93
+ poke: false,
94
+ forward: false,
95
+ },
96
+ feedback: { mode: 'none' },
97
+ },
98
+ },
99
+ resolveCapabilities({ capabilities, facts }) {
100
+ if (!capabilities.actions?.recall)
101
+ return undefined;
102
+ const role = resolvePlatformRole(capabilities.scene, facts.bot?.platformRole);
103
+ // Unknown role fails closed, like every other role-gated action.
104
+ if (role === 'owner' || role === 'admin')
105
+ return undefined;
106
+ return { actions: { recall: { params: { others: false } } } };
36
107
  },
37
108
  createAdapter: context => new IcqqAdapter(context),
38
109
  });
@@ -1,42 +1,112 @@
1
- import { IcqqAdapter } from '../../src/adapter.js';
2
1
  /**
3
2
  * Convention entry: discover `adapters/icqq/index.ts` → defineAdapter.
4
3
  */
5
- import { defineAdapter } from 'zhin.js/adapter';
6
- import {
7
- type IcqqEndpointConfig,
8
- } from '../../src/protocol.js';
4
+ import { defineAdapter, resolvePlatformRole } from 'zhin.js/adapter';
5
+ import { IcqqAdapter } from '../../src/adapter.js';
6
+ import type { IcqqEndpointConfig } from '../../src/protocol.js';
9
7
 
10
8
  export { IcqqEndpoint } from '../../src/endpoint.js';
11
9
  export type { IcqqEndpointOptions } from '../../src/endpoint.js';
12
10
 
11
+ /** Group reactions take QQ system face ids. Private chats have none. */
12
+ const ICQQ_REACTIONS = [
13
+ { value: '76', label: '赞' }, { value: '66', label: '爱心' }, { value: '124', label: 'OK' },
14
+ { value: '99', label: '鼓掌' }, { value: '144', label: '喝彩' }, { value: '320', label: '庆祝' },
15
+ { value: '319', label: '比心' }, { value: '299', label: '牛啊' }, { value: '297', label: '拜谢' },
16
+ { value: '182', label: '笑哭' }, { value: '179', label: 'doge' }, { value: '264', label: '捂脸' },
17
+ { value: '271', label: '吃瓜' }, { value: '269', label: '暗中观察' }, { value: '268', label: '问号脸' },
18
+ { value: '270', label: 'emm' }, { value: '106', label: '委屈' }, { value: '285', label: '摸鱼' },
19
+ { value: '104', label: '哈欠' }, { value: '267', label: '头秃' }, { value: '326', label: '生气' },
20
+ ];
21
+
22
+ /** QQ group mute: 1 minute to 30 days (0 lifts it, which is the `unmute` action). */
23
+ const ICQQ_MUTE_SECONDS = { min: 60, max: 2592000 } as const;
24
+
13
25
  export default defineAdapter<IcqqEndpointConfig>({
14
26
  capabilities: ['inbound', 'outbound'],
15
- operations: ['recall', 'reaction'],
16
- segments: {
17
- // Canonical types formatOutboundBody encodes; markdown / keyboard degrade to text first.
18
- supported: ['text', 'mention', 'image', 'audio', 'video', 'file', 'reply', 'forward', 'face', 'dice', 'rps', 'share'],
19
- outboundMedia: ['base64', 'url', 'path'],
20
- interactive: 'text',
21
- // QQ group reactions take system face ids; private chats have no reactions.
22
- reaction: {
23
- scopes: ['group'],
24
- values: [
25
- { value: '76', label: '赞' }, { value: '66', label: '爱心' }, { value: '124', label: 'OK' },
26
- { value: '99', label: '鼓掌' }, { value: '144', label: '喝彩' }, { value: '320', label: '庆祝' },
27
- { value: '319', label: '比心' }, { value: '299', label: '牛啊' }, { value: '297', label: '拜谢' },
28
- { value: '182', label: '笑哭' }, { value: '179', label: 'doge' }, { value: '264', label: '捂脸' },
29
- { value: '271', label: '吃瓜' }, { value: '269', label: '暗中观察' }, { value: '268', label: '问号脸' },
30
- { value: '270', label: 'emm' }, { value: '106', label: '委屈' }, { value: '285', label: '摸鱼' },
31
- { value: '104', label: '哈欠' }, { value: '267', label: '头秃' }, { value: '326', label: '生气' },
32
- ],
27
+ profile: {
28
+ default: {
29
+ messaging: {
30
+ segments: {
31
+ // Canonical types formatOutboundBody encodes; markdown / keyboard degrade to text first.
32
+ supported: ['text', 'mention', 'image', 'audio', 'video', 'file', 'reply', 'forward', 'face', 'dice', 'rps', 'share'],
33
+ outboundMedia: ['base64', 'url', 'path'],
34
+ interactive: 'text',
35
+ reaction: { scopes: ['group'], values: ICQQ_REACTIONS },
36
+ model: {
37
+ share: {
38
+ fields: ['description', 'image', 'audio', 'platform'],
39
+ hint: 'renders as a QQ link card. For a song, set audio to the playable link, image to the cover, description to the artist and platform to its music service (qq, netease, kugou, kuwo or migu), copied from tool results; leave a field "" when you have no real value.',
40
+ },
41
+ },
42
+ },
43
+ mention: { user: true },
44
+ quote: true,
45
+ recall: { timeLimitSeconds: 120 },
46
+ forward: {},
47
+ },
48
+ scene: { roles: { owner: 'owner', admin: 'admin', member: 'member' } },
49
+ actions: {
50
+ recall: { scopes: ['private', 'group'], params: { timeLimitSeconds: 120 } },
51
+ quoteReply: { scopes: ['private', 'group'] },
52
+ forward: { scopes: ['private', 'group'] },
53
+ poke: { scopes: ['private', 'group'] },
54
+ },
55
+ delivery: { send: true, proactive: true, spool: false },
56
+ },
57
+ private: {
58
+ // A private chat has no reactions, so feedback is a short status message.
59
+ feedback: { mode: 'status-message', text: '⏳' },
60
+ events: { emit: ['message.recalled', 'poke'] },
33
61
  },
34
- model: {
35
- share: {
36
- fields: ['description', 'image', 'audio', 'platform'],
37
- hint: 'renders as a QQ link card. For a song, set audio to the playable link, image to the cover, description to the artist and platform to its music service (qq, netease, kugou, kuwo or migu), copied from tool results; leave a field "" when you have no real value.',
62
+ group: {
63
+ messaging: { reaction: { emojis: ICQQ_REACTIONS.map((item) => item.value) } },
64
+ scene: {
65
+ groupInfo: true,
66
+ memberProfile: true,
67
+ card: true,
68
+ title: true,
69
+ mute: { durationSeconds: ICQQ_MUTE_SECONDS, all: true },
70
+ },
71
+ actions: {
72
+ // Own messages need no role; recalling others' needs an admin (narrowed in resolveCapabilities).
73
+ recall: { scopes: ['private', 'group'], params: { timeLimitSeconds: 120, others: true } },
74
+ reaction: { scopes: ['group'], params: { emojis: ICQQ_REACTIONS.map((item) => item.value), own: true, others: true, cancellable: true } },
75
+ mute: { scopes: ['group'], requiresRole: 'admin', params: { durationSeconds: ICQQ_MUTE_SECONDS } },
76
+ unmute: { scopes: ['group'], requiresRole: 'admin' },
77
+ muteAll: { scopes: ['group'], requiresRole: 'admin' },
78
+ kick: { scopes: ['group'], requiresRole: 'admin' },
79
+ setCard: { scopes: ['group'], requiresRole: 'admin' },
80
+ setTitle: { scopes: ['group'], requiresRole: 'owner' },
81
+ setAdmin: { scopes: ['group'], requiresRole: 'owner' },
82
+ essence: { scopes: ['group'], requiresRole: 'admin' },
83
+ announce: { scopes: ['group'], requiresRole: 'admin' },
84
+ invite: { scopes: ['group'] },
85
+ },
86
+ events: {
87
+ emit: ['member.joined', 'member.left', 'member.role_changed', 'member.muted', 'message.recalled', 'poke'],
88
+ },
89
+ feedback: { mode: 'reaction', emoji: '⏳' },
90
+ prompt: {
91
+ text: '被禁言或不是管理员时不要尝试管理类操作;头衔和设/撤管理员只有群主能做;邀请只能邀请你的好友。',
38
92
  },
39
93
  },
94
+ channel: {
95
+ // Guild channels: text and media in and out; no recall, poke, forward, reaction or management actions.
96
+ actions: {
97
+ recall: false,
98
+ poke: false,
99
+ forward: false,
100
+ },
101
+ feedback: { mode: 'none' },
102
+ },
103
+ },
104
+ resolveCapabilities({ capabilities, facts }) {
105
+ if (!capabilities.actions?.recall) return undefined;
106
+ const role = resolvePlatformRole(capabilities.scene, facts.bot?.platformRole);
107
+ // Unknown role fails closed, like every other role-gated action.
108
+ if (role === 'owner' || role === 'admin') return undefined;
109
+ return { actions: { recall: { params: { others: false } } } };
40
110
  },
41
111
  createAdapter: context => new IcqqAdapter(context),
42
112
  });
@@ -85,6 +85,26 @@ export const messages = createMessages({
85
85
  "zh-CN": "当前权限或成员状态不允许踢出",
86
86
  "en": "Current permissions or member status do not allow removal"
87
87
  },
88
+ "adminSet": {
89
+ "zh-CN": "已将 {p0} 设为管理员",
90
+ "en": "Member {p0} is now an administrator"
91
+ },
92
+ "adminUnset": {
93
+ "zh-CN": "已取消 {p0} 的管理员",
94
+ "en": "Administrator role removed from {p0}"
95
+ },
96
+ "adminUnconfirmed": {
97
+ "zh-CN": "平台未确认管理员变更成功,请勿自动重试",
98
+ "en": "The platform did not confirm the administrator change; do not retry automatically"
99
+ },
100
+ "adminForbidden": {
101
+ "zh-CN": "机器人不是群主,无法设置或取消管理员",
102
+ "en": "The bot must be the group owner to set or remove administrators"
103
+ },
104
+ "adminTargetInvalid": {
105
+ "zh-CN": "目标成员身份不允许本次变更:设为管理员需是普通成员,取消管理员需是现任管理员",
106
+ "en": "The member's role does not allow this change: promotion requires a regular member, removal requires a current administrator"
107
+ },
88
108
  "anonymousDisabled": {
89
109
  "zh-CN": "已关闭匿名聊天",
90
110
  "en": "Anonymous chat disabled"
@@ -85,6 +85,26 @@ export const messages = createMessages({
85
85
  "zh-CN": "当前权限或成员状态不允许踢出",
86
86
  "en": "Current permissions or member status do not allow removal"
87
87
  },
88
+ "adminSet": {
89
+ "zh-CN": "已将 {p0} 设为管理员",
90
+ "en": "Member {p0} is now an administrator"
91
+ },
92
+ "adminUnset": {
93
+ "zh-CN": "已取消 {p0} 的管理员",
94
+ "en": "Administrator role removed from {p0}"
95
+ },
96
+ "adminUnconfirmed": {
97
+ "zh-CN": "平台未确认管理员变更成功,请勿自动重试",
98
+ "en": "The platform did not confirm the administrator change; do not retry automatically"
99
+ },
100
+ "adminForbidden": {
101
+ "zh-CN": "机器人不是群主,无法设置或取消管理员",
102
+ "en": "The bot must be the group owner to set or remove administrators"
103
+ },
104
+ "adminTargetInvalid": {
105
+ "zh-CN": "目标成员身份不允许本次变更:设为管理员需是普通成员,取消管理员需是现任管理员",
106
+ "en": "The member's role does not allow this change: promotion requires a regular member, removal requires a current administrator"
107
+ },
88
108
  "anonymousDisabled": {
89
109
  "zh-CN": "已关闭匿名聊天",
90
110
  "en": "Anonymous chat disabled"
@@ -11,10 +11,13 @@ keywords:
11
11
  - 公告
12
12
  - 精华
13
13
  - 退群
14
+ - 设管理员
15
+ - 取消管理员
14
16
  - 撤回他人消息
15
17
  tools:
16
18
  - group_invite
17
19
  - group_kick
20
+ - set_group_admin
18
21
  - set_mute
19
22
  - set_title
20
23
  - set_anonymous
@@ -35,6 +38,8 @@ scopes:
35
38
 
36
39
  已配置的自动审核流程可对入群 10 分钟内的明确广告直接踢出;该例外由审核流程核验,不授权本技能或普通对话绕过人工审批。
37
40
 
41
+ `set_group_admin` 仅在你是当前群群主时注入(你只是管理员时不可见),沿用 master/admin/trusted/owner 发起权限,每次人工审批并展示目标群、成员、设/撤方向和理由。执行前实时复核:你必须仍是群主;设为管理员只针对普通成员,取消只针对现任管理员;平台未确认时不宣称成功、不自动重试。
42
+
38
43
  `group_leave` 仅主人(master)可发起,每次人工审批,只能退出当前群。你为群主时拒绝执行,因为 SDK 的退群会解散群。
39
44
 
40
45
  `recall_member_message` 沿用 master/admin/trusted/owner 发起权限,无需逐次审批;目标固定在当前群中的真实消息。执行前实时查询你与目标成员的角色:管理员只能撤回普通成员,群主可撤回管理员或普通成员。平台时限与执行结果仍然有效,不自动重试未确认的结果。
@@ -9,6 +9,8 @@ export default defineAgentTool({
9
9
  content: z.string().describe('公告内容'),
10
10
  }),
11
11
  adapter: 'icqq',
12
+ // ADR 0044: offered only while the Bot can actually do this here (e.g. it is a group admin).
13
+ requiresCapabilities: ['actions.announce'],
12
14
  requiresApproval: 'once',
13
15
  scopes: ['group'],
14
16
  permissions: ['role(master,admin,trusted,owner)'],
@@ -8,6 +8,8 @@ export default defineAgentTool<{ group_id: number; content: string }>({
8
8
  content: z.string().describe('公告内容'),
9
9
  }),
10
10
  adapter: 'icqq',
11
+ // ADR 0044: offered only while the Bot can actually do this here (e.g. it is a group admin).
12
+ requiresCapabilities: ['actions.announce'],
11
13
  requiresApproval: 'once',
12
14
  scopes:['group'],
13
15
  permissions: ['role(master,admin,trusted,owner)'],
@@ -9,6 +9,8 @@ export default defineAgentTool({
9
9
  action: z.enum(['add', 'remove']).describe('add=设为精华, remove=移除精华'),
10
10
  }),
11
11
  adapter: 'icqq',
12
+ // ADR 0044: offered only while the Bot can actually do this here (e.g. it is a group admin).
13
+ requiresCapabilities: ['actions.essence'],
12
14
  requiresApproval: 'once',
13
15
  scopes: ['group'],
14
16
  permissions: ['role(master,admin,trusted,owner)'],
@@ -8,6 +8,8 @@ export default defineAgentTool<{ message_id: string; action: 'add' | 'remove' }>
8
8
  action: z.enum(['add', 'remove']).describe('add=设为精华, remove=移除精华'),
9
9
  }),
10
10
  adapter: 'icqq',
11
+ // ADR 0044: offered only while the Bot can actually do this here (e.g. it is a group admin).
12
+ requiresCapabilities: ['actions.essence'],
11
13
  requiresApproval: 'once',
12
14
  scopes:['group'],
13
15
  permissions: ['role(master,admin,trusted,owner)'],
@@ -10,6 +10,8 @@ export default defineAgentTool({
10
10
  reason: z.string().min(1).describe('本次踢人的具体原因,随目标群和成员一起供人工确认'),
11
11
  }),
12
12
  adapter: 'icqq',
13
+ // ADR 0044: offered only while the Bot can actually do this here (e.g. it is a group admin).
14
+ requiresCapabilities: ['actions.kick'],
13
15
  scopes: ['group'],
14
16
  permissions: ['role(master,admin,trusted,owner)'],
15
17
  requiresApproval: 'human',
@@ -9,6 +9,8 @@ export default defineAgentTool<{ group_id: number; user_id: number; reason: stri
9
9
  reason: z.string().min(1).describe('本次踢人的具体原因,随目标群和成员一起供人工确认'),
10
10
  }),
11
11
  adapter: 'icqq',
12
+ // ADR 0044: offered only while the Bot can actually do this here (e.g. it is a group admin).
13
+ requiresCapabilities: ['actions.kick'],
12
14
  scopes:['group'],
13
15
  permissions: ['role(master,admin,trusted,owner)'],
14
16
  requiresApproval: 'human',
@@ -6,6 +6,8 @@ import { messages as t } from '../../../../messages.js';
6
6
  export default defineAgentTool({
7
7
  description: '撤回当前 QQ 群内他人的真实消息;需调用方具备群管发起权限及你具备实时平台权限,无需逐次审批',
8
8
  adapter: 'icqq',
9
+ // ADR 0044: offered only while the Bot can actually do this here (e.g. it is a group admin).
10
+ requiresCapabilities: ['actions.recall.others'],
9
11
  scopes: ['group'],
10
12
  currentConversationBotRoles: ['owner', 'admin'],
11
13
  permissions: ['role(master,admin,trusted,owner)'],
@@ -6,6 +6,8 @@ import { messages as t } from '../../../../messages.js';
6
6
  export default defineAgentTool({
7
7
  description: '撤回当前 QQ 群内他人的真实消息;需调用方具备群管发起权限及你具备实时平台权限,无需逐次审批',
8
8
  adapter: 'icqq',
9
+ // ADR 0044: offered only while the Bot can actually do this here (e.g. it is a group admin).
10
+ requiresCapabilities: ['actions.recall.others'],
9
11
  scopes: ['group'],
10
12
  currentConversationBotRoles: ['owner', 'admin'],
11
13
  permissions: ['role(master,admin,trusted,owner)'],
@@ -0,0 +1,43 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { messages as t } from "../../../../messages.js";
3
+ import { freshMember } from "../../../../members.js";
4
+ import { defineAgentTool } from '@zhin.js/tool';
5
+ import { z } from 'zod';
6
+ export default defineAgentTool({
7
+ description: '设置或取消 QQ 群管理员(enabled=true 设为管理员,false 取消管理员);仅你是该群群主时可用,每次人工审批',
8
+ inputSchema: z.object({
9
+ group_id: z.number().describe('目标群号'),
10
+ user_id: z.number().describe('目标成员 QQ号'),
11
+ enabled: z.boolean().describe('true 设为管理员(目标须为普通成员);false 取消管理员(目标须为现任管理员)'),
12
+ reason: z.string().min(1).describe('本次设置或取消管理员的具体原因,随目标群和成员一起供人工确认'),
13
+ }),
14
+ adapter: 'icqq',
15
+ scopes: ['group'],
16
+ requiresCapabilities: ['actions.setAdmin'],
17
+ currentConversationBotRoles: ['owner'],
18
+ permissions: ['role(master,admin,trusted,owner)'],
19
+ requiresApproval: 'human',
20
+ async execute({ group_id, user_id, enabled }, context) {
21
+ const client = context.$client;
22
+ if (user_id === client.uin)
23
+ return { success: false, message: t(context.locale, 'adminForbidden') };
24
+ const [self, member] = await Promise.all([freshMember(client, group_id, client.uin), freshMember(client, group_id, user_id)]);
25
+ if (self?.role !== 'owner')
26
+ return { success: false, message: t(context.locale, 'adminForbidden') };
27
+ if (!member)
28
+ return { success: false, message: t(context.locale, 'memberMissing') };
29
+ if (member.role !== (enabled ? 'member' : 'admin'))
30
+ return { success: false, message: t(context.locale, 'adminTargetInvalid') };
31
+ context.signal.throwIfAborted();
32
+ let confirmed = false;
33
+ try {
34
+ confirmed = await client.setGroupAdmin(group_id, user_id, enabled);
35
+ }
36
+ catch {
37
+ confirmed = false;
38
+ }
39
+ if (!confirmed)
40
+ return { success: false, message: t(context.locale, 'adminUnconfirmed') };
41
+ return { success: true, message: t(context.locale, enabled ? 'adminSet' : 'adminUnset', { p0: user_id }) };
42
+ },
43
+ });
@@ -0,0 +1,36 @@
1
+ import { messages as t } from "../../../../messages.js";
2
+ import { freshMember } from "../../../../members.js";
3
+ import { defineAgentTool } from '@zhin.js/tool';
4
+ import { z } from 'zod';
5
+ export default defineAgentTool<{ group_id: number; user_id: number; enabled: boolean; reason: string }>({
6
+ description: '设置或取消 QQ 群管理员(enabled=true 设为管理员,false 取消管理员);仅你是该群群主时可用,每次人工审批',
7
+ inputSchema: z.object({
8
+ group_id: z.number().describe('目标群号'),
9
+ user_id: z.number().describe('目标成员 QQ号'),
10
+ enabled: z.boolean().describe('true 设为管理员(目标须为普通成员);false 取消管理员(目标须为现任管理员)'),
11
+ reason: z.string().min(1).describe('本次设置或取消管理员的具体原因,随目标群和成员一起供人工确认'),
12
+ }),
13
+ adapter: 'icqq',
14
+ scopes: ['group'],
15
+ requiresCapabilities: ['actions.setAdmin'],
16
+ currentConversationBotRoles: ['owner'],
17
+ permissions: ['role(master,admin,trusted,owner)'],
18
+ requiresApproval: 'human',
19
+ async execute({ group_id, user_id, enabled }, context) {
20
+ const client = context.$client;
21
+ if (user_id === client.uin) return { success: false, message: t(context.locale, 'adminForbidden') };
22
+ const [self, member] = await Promise.all([freshMember(client, group_id, client.uin), freshMember(client, group_id, user_id)]);
23
+ if (self?.role !== 'owner') return { success: false, message: t(context.locale, 'adminForbidden') };
24
+ if (!member) return { success: false, message: t(context.locale, 'memberMissing') };
25
+ if (member.role !== (enabled ? 'member' : 'admin')) return { success: false, message: t(context.locale, 'adminTargetInvalid') };
26
+ context.signal.throwIfAborted();
27
+ let confirmed = false;
28
+ try {
29
+ confirmed = await client.setGroupAdmin(group_id, user_id, enabled);
30
+ } catch {
31
+ confirmed = false;
32
+ }
33
+ if (!confirmed) return { success: false, message: t(context.locale, 'adminUnconfirmed') };
34
+ return { success: true, message: t(context.locale, enabled ? 'adminSet' : 'adminUnset', { p0: user_id }) };
35
+ },
36
+ });
@@ -11,6 +11,8 @@ export default defineAgentTool({
11
11
  duration: z.number().describe('禁言时间(秒),0=解除禁言').default(0),
12
12
  }),
13
13
  adapter: 'icqq',
14
+ // ADR 0044: offered only while the Bot can actually do this here (e.g. it is a group admin).
15
+ requiresCapabilities: ['actions.mute'],
14
16
  scopes: ['group'],
15
17
  requiresApproval: 'always',
16
18
  async execute({ group_id, user_id, duration }, context) {
@@ -10,6 +10,8 @@ export default defineAgentTool<{ group_id: number; user_id: number; duration?: n
10
10
  duration: z.number().describe('禁言时间(秒),0=解除禁言').default(0),
11
11
  }),
12
12
  adapter: 'icqq',
13
+ // ADR 0044: offered only while the Bot can actually do this here (e.g. it is a group admin).
14
+ requiresCapabilities: ['actions.mute'],
13
15
  scopes:['group'],
14
16
  requiresApproval: 'always',
15
17
  async execute({ group_id, user_id, duration }, context) {
@@ -12,6 +12,8 @@ export default defineAgentTool({
12
12
  duration: z.number().optional().describe('持续时间(秒),-1永久').default(-1),
13
13
  }),
14
14
  adapter: 'icqq',
15
+ // ADR 0044: offered only while the Bot can actually do this here (e.g. it is a group admin).
16
+ requiresCapabilities: ['actions.setTitle'],
15
17
  scopes: ['group'],
16
18
  requiresApproval: 'always',
17
19
  async execute({ group_id, user_id, title, duration }, context) {
@@ -11,6 +11,8 @@ export default defineAgentTool<{ group_id: number; user_id: number; title: strin
11
11
  duration: z.number().optional().describe('持续时间(秒),-1永久').default(-1),
12
12
  }),
13
13
  adapter: 'icqq',
14
+ // ADR 0044: offered only while the Bot can actually do this here (e.g. it is a group admin).
15
+ requiresCapabilities: ['actions.setTitle'],
14
16
  scopes:['group'],
15
17
  requiresApproval: 'always',
16
18
  async execute({ group_id, user_id, title, duration }, context) {
package/lib/endpoint.js CHANGED
@@ -2,7 +2,8 @@ import { parseGroupMessageId, } from '@icqqjs/icqq';
2
2
  import { inspect } from 'node:util';
3
3
  import { AsyncLocalStorage } from 'node:async_hooks';
4
4
  import { createEndpointSpecification, endpointSpec, createEndpointOperation, } from 'zhin.js/adapter';
5
- import { receiveOneBotLikeSideEvent, buildSystem, toCanonicalSegments } from '@zhin.js/core';
5
+ import { receiveOneBotLikeSideEvent } from '@zhin.js/adapter-onebot-events';
6
+ import { buildSystem, toCanonicalSegments } from '@zhin.js/core';
6
7
  import { EndpointDeliveryError } from '@zhin.js/im-contract';
7
8
  import { formatCompact, getAdapterLogger, truncatePreview } from '@zhin.js/logger';
8
9
  import { runIcqqLoginAssistStep } from './icqq-login-assist.js';
@@ -13,6 +14,7 @@ import { IcqqGuildCatalog, normalizeIcqqGuildInboundMessage, } from './icqq-guil
13
14
  import { prepareIcqqOutboundMedia, resolveIcqqOutboundMediaMode, } from './outbound-media.js';
14
15
  import { formatInboundContent, formatOutboundMessageParts, normalizeIcqqImageMedia, icqqInboundConversation, icqqOutboundTarget, } from './protocol.js';
15
16
  import { ManagedIcqqClient } from './managed-client.js';
17
+ import { createIcqqActionPorts } from './group-actions.js';
16
18
  import { inspectGroupMember, moderateGroupMember } from './group-moderation.js';
17
19
  import { inspectGroupMemberProfile, inspectGroupScene } from './group-scene.js';
18
20
  import { createIcqqSelfProfile } from './self-profile.js';
@@ -108,6 +110,7 @@ export class IcqqEndpoint extends ManagedIcqqClient {
108
110
  throw new Error('QQ did not confirm the group notice');
109
111
  }
110
112
  },
113
+ ...createIcqqActionPorts(() => this),
111
114
  });
112
115
  #control = Object.freeze({
113
116
  recall: async ({ message, signal }) => {
@@ -0,0 +1,10 @@
1
+ import type { Client } from '@icqqjs/icqq';
2
+ import { type EndpointManagement } from 'zhin.js/adapter';
3
+ type ActionPorts = Pick<EndpointManagement, 'pokeUser' | 'unmuteGroupMember' | 'muteAllGroupMembers' | 'setGroupCard' | 'setGroupTitle' | 'setGroupEssence' | 'inviteGroupMember'>;
4
+ /**
5
+ * ADR 0044 action ports of the icqq endpoint. Where the bot's role decides the outcome the role is
6
+ * read fresh from the platform first, so a stale cache can neither allow nor refuse the action.
7
+ * Each request carries its own lifetime: reads are `prepare`, the native effect is `run`.
8
+ */
9
+ export declare function createIcqqActionPorts(getClient: () => Client): ActionPorts;
10
+ export {};
@@ -0,0 +1,94 @@
1
+ import { createEndpointOperation } from 'zhin.js/adapter';
2
+ function numericId(value) {
3
+ const id = Number(value);
4
+ if (!/^[1-9]\d*$/.test(value) || !Number.isSafeInteger(id))
5
+ throw new Error('Invalid QQ identifier');
6
+ return id;
7
+ }
8
+ /** Uncached member lookup; `pickMember` and the cached roster would report stale roles. */
9
+ async function freshMember(operation, client, group, user) {
10
+ try {
11
+ const member = await operation.prepare(() => client.getGroupMemberInfo(group, user, true));
12
+ return member.user_id === user && member.group_id === group ? member : undefined;
13
+ }
14
+ catch {
15
+ return undefined;
16
+ }
17
+ }
18
+ async function requireBotRole(operation, client, group, roles, message) {
19
+ const self = await freshMember(operation, client, group, client.uin);
20
+ if (!self || !roles.includes(self.role))
21
+ throw new Error(message);
22
+ }
23
+ function confirmed(ok, what) {
24
+ if (ok === false)
25
+ throw new Error(`QQ did not confirm ${what}`);
26
+ }
27
+ /**
28
+ * ADR 0044 action ports of the icqq endpoint. Where the bot's role decides the outcome the role is
29
+ * read fresh from the platform first, so a stale cache can neither allow nor refuse the action.
30
+ * Each request carries its own lifetime: reads are `prepare`, the native effect is `run`.
31
+ */
32
+ export function createIcqqActionPorts(getClient) {
33
+ return {
34
+ async pokeUser({ userId, groupId, signal }) {
35
+ const client = getClient();
36
+ const operation = createEndpointOperation({ signal });
37
+ const user = numericId(userId);
38
+ confirmed(groupId === undefined
39
+ ? await operation.run(() => client.pickFriend(user).poke(false))
40
+ : await operation.run(() => client.sendGroupPoke(numericId(groupId), user)), 'poke');
41
+ },
42
+ async unmuteGroupMember({ groupId, userId, signal }) {
43
+ const client = getClient();
44
+ const operation = createEndpointOperation({ signal });
45
+ const group = numericId(groupId);
46
+ await requireBotRole(operation, client, group, ['owner', 'admin'], 'Bot is not a group admin');
47
+ confirmed(await operation.run(() => client.setGroupBan(group, numericId(userId), 0)), 'unmute');
48
+ },
49
+ async muteAllGroupMembers({ groupId, enabled, signal }) {
50
+ const client = getClient();
51
+ const operation = createEndpointOperation({ signal });
52
+ const group = numericId(groupId);
53
+ await requireBotRole(operation, client, group, ['owner', 'admin'], 'Bot is not a group admin');
54
+ confirmed(await operation.run(() => client.setGroupWholeBan(group, enabled)), 'whole-group mute');
55
+ },
56
+ async setGroupCard({ groupId, userId, card, signal }) {
57
+ const client = getClient();
58
+ const operation = createEndpointOperation({ signal });
59
+ const group = numericId(groupId);
60
+ const user = numericId(userId);
61
+ if (user !== client.uin)
62
+ await requireBotRole(operation, client, group, ['owner', 'admin'], 'Bot is not a group admin');
63
+ confirmed(await operation.run(() => client.setGroupCard(group, user, card)), 'card change');
64
+ },
65
+ async setGroupTitle({ groupId, userId, title, durationSeconds, signal }) {
66
+ const client = getClient();
67
+ const operation = createEndpointOperation({ signal });
68
+ const group = numericId(groupId);
69
+ const user = numericId(userId);
70
+ await requireBotRole(operation, client, group, ['owner'], 'Only the group owner can set special titles');
71
+ if (!await freshMember(operation, client, group, user))
72
+ throw new Error('Target is not a group member');
73
+ confirmed(await operation.run(() => client.setGroupSpecialTitle(group, user, title, durationSeconds ?? -1)), 'title change');
74
+ },
75
+ async setGroupEssence({ groupId, messageId, enabled, signal }) {
76
+ const client = getClient();
77
+ const operation = createEndpointOperation({ signal });
78
+ await requireBotRole(operation, client, numericId(groupId), ['owner', 'admin'], 'Bot is not a group admin');
79
+ if (enabled)
80
+ await operation.run(() => client.setEssenceMessage(messageId));
81
+ else
82
+ await operation.run(() => client.removeEssenceMessage(messageId));
83
+ },
84
+ async inviteGroupMember({ groupId, userId, signal }) {
85
+ const client = getClient();
86
+ const operation = createEndpointOperation({ signal });
87
+ const group = numericId(groupId);
88
+ const user = numericId(userId);
89
+ if (await freshMember(operation, client, group, user))
90
+ throw new Error('Target is already a group member');
91
+ confirmed(await operation.run(() => client.inviteFriend(group, user)), 'invitation');
92
+ },
93
+ };
94
+ }
@@ -0,0 +1,8 @@
1
+ import type { SensitiveExecCommandRule } from '@zhin.js/agent';
2
+ /**
3
+ * The `icqq` command-line client can kick members, move money and delete files. Under an `allowlist`
4
+ * exec policy its non-sensitive subcommands run freely, while these need owner approval (ADR 0044:
5
+ * this knowledge lives with the adapter, the exec policy only reads the registered rule).
6
+ */
7
+ export declare const ICQQ_SENSITIVE_SUBCOMMAND_REGEXES: readonly RegExp[];
8
+ export declare const icqqSensitiveExecRule: SensitiveExecCommandRule;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The `icqq` command-line client can kick members, move money and delete files. Under an `allowlist`
3
+ * exec policy its non-sensitive subcommands run freely, while these need owner approval (ADR 0044:
4
+ * this knowledge lives with the adapter, the exec policy only reads the registered rule).
5
+ */
6
+ export const ICQQ_SENSITIVE_SUBCOMMAND_REGEXES = Object.freeze([
7
+ /\bicqq\s+friend\s+(?:delete|block|move)\b/i,
8
+ /\bicqq\s+group\s+(?:kick|mute|set|admin|announce|transfer|quit|dissolve|invite)\b/i,
9
+ /\bicqq\s+(?:request|wallet|pay|recall)\b/i,
10
+ /\bicqq\s+file\s+(?:upload|delete)\b/i,
11
+ /\bicqq\s+gfs\b.*\b(?:rm|delete|del)\b/i,
12
+ ]);
13
+ export const icqqSensitiveExecRule = Object.freeze({
14
+ command: 'icqq',
15
+ sensitive: ICQQ_SENSITIVE_SUBCOMMAND_REGEXES,
16
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-icqq",
3
- "version": "1.1.21",
3
+ "version": "1.1.22",
4
4
  "description": "Zhin.js ICQQ adapter — direct connection to @icqqjs/icqq",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -33,32 +33,37 @@
33
33
  "directory": "plugins/adapters/icqq"
34
34
  },
35
35
  "dependencies": {
36
- "@zhin.js/adapter": "1.1.27",
37
- "@zhin.js/agent-feature": "1.1.12",
38
- "@zhin.js/client": "1.1.23",
36
+ "@zhin.js/adapter-onebot-events": "0.0.1",
37
+ "@zhin.js/adapter": "1.1.28",
38
+ "@zhin.js/agent-feature": "1.1.13",
39
+ "@zhin.js/client": "1.1.24",
39
40
  "@zhin.js/console-contract": "1.1.3",
40
- "@zhin.js/core": "1.1.55",
41
- "@zhin.js/feature-kit": "1.1.11",
42
- "@zhin.js/im-contract": "1.1.10",
41
+ "@zhin.js/core": "1.1.56",
42
+ "@zhin.js/feature-kit": "1.1.12",
43
+ "@zhin.js/im-contract": "1.1.11",
43
44
  "@zhin.js/logger": "1.1.2",
44
- "@zhin.js/page": "1.1.13",
45
- "@zhin.js/permission": "1.1.11",
46
- "@zhin.js/skill": "1.1.12",
47
- "@zhin.js/tool": "1.1.14"
45
+ "@zhin.js/page": "1.1.14",
46
+ "@zhin.js/permission": "1.1.12",
47
+ "@zhin.js/skill": "1.1.13",
48
+ "@zhin.js/tool": "1.1.15"
48
49
  },
49
50
  "peerDependencies": {
50
51
  "@icqqjs/icqq": "^1.12.2",
51
52
  "zod": "^4.0.0",
52
- "@zhin.js/adapter": "^1.1.27",
53
- "@zhin.js/command": "^1.1.13",
54
- "@zhin.js/core": "^1.1.55",
55
- "@zhin.js/prompt-section": "^1.1.11",
56
- "zhin.js": "^1.1.20"
53
+ "@zhin.js/adapter": "^1.1.28",
54
+ "@zhin.js/agent": "^1.1.47",
55
+ "@zhin.js/command": "^1.1.14",
56
+ "@zhin.js/core": "^1.1.56",
57
+ "@zhin.js/prompt-section": "^1.1.12",
58
+ "zhin.js": "^1.1.21"
57
59
  },
58
60
  "peerDependenciesMeta": {
59
61
  "@icqqjs/icqq": {
60
62
  "optional": true
61
63
  },
64
+ "@zhin.js/agent": {
65
+ "optional": true
66
+ },
62
67
  "@zhin.js/command": {
63
68
  "optional": true
64
69
  },
@@ -78,8 +83,9 @@
78
83
  "typescript": "^6.0.3",
79
84
  "vitest": "^4.1.10",
80
85
  "zod": "^4.4.3",
81
- "@zhin.js/prompt-section": "1.1.11",
82
- "zhin.js": "1.1.20"
86
+ "@zhin.js/agent": "1.1.47",
87
+ "@zhin.js/prompt-section": "1.1.12",
88
+ "zhin.js": "1.1.21"
83
89
  },
84
90
  "files": [
85
91
  "adapters",
package/plugin.js CHANGED
@@ -1,9 +1,12 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
2
  import { definePlugin, pluginConsoleRpcToken } from 'zhin.js';
3
3
  import { permissionHostToken, createSceneRolePlatformChecker } from '@zhin.js/permission';
4
+ import { registerSensitiveExecCommand } from '@zhin.js/agent';
4
5
  import { createIcqqConsoleRpc } from "./lib/console.js";
6
+ import { icqqSensitiveExecRule } from "./lib/sensitive-commands.js";
5
7
  export default definePlugin({
6
8
  name: 'icqq',
9
+ configUpdate: { featurePaths: [['nickname'], ['profile'], ['endpoints']] },
7
10
  metadata: {
8
11
  displayName: 'ICQQ Adapter',
9
12
  },
@@ -12,6 +15,10 @@ export default definePlugin({
12
15
  const disposePlatform = context.resources.has(permissionHostToken)
13
16
  ? context.resources.use(permissionHostToken).registerPlatform('icqq', createSceneRolePlatformChecker())
14
17
  : undefined;
15
- return disposePlatform;
18
+ const disposeExecRule = registerSensitiveExecCommand(icqqSensitiveExecRule);
19
+ return () => {
20
+ disposeExecRule();
21
+ disposePlatform?.();
22
+ };
16
23
  },
17
24
  });
package/src/endpoint.ts CHANGED
@@ -33,7 +33,8 @@ import {
33
33
  type EndpointGroupNoticeRequest,
34
34
  } from 'zhin.js/adapter';
35
35
  import type { EndpointContentPort } from '@zhin.js/adapter';
36
- import { receiveOneBotLikeSideEvent, buildSystem, toCanonicalSegments, type LoginAssist, type LoginAssistEndpointIdentity, type PendingLoginTask } from '@zhin.js/core';
36
+ import { receiveOneBotLikeSideEvent } from '@zhin.js/adapter-onebot-events';
37
+ import { buildSystem, toCanonicalSegments, type LoginAssist, type LoginAssistEndpointIdentity, type PendingLoginTask } from '@zhin.js/core';
37
38
  import { EndpointDeliveryError, type EndpointSendPart, type EndpointSendResult, type MessageRef } from '@zhin.js/im-contract';
38
39
  import { formatCompact, getAdapterLogger, truncatePreview } from '@zhin.js/logger';
39
40
  import type { CapabilityId } from 'zhin.js';
@@ -77,6 +78,7 @@ import {
77
78
  } from './protocol.js';
78
79
  import type { SystemMessage as IcqqSystemMessage } from './types.js';
79
80
  import { ManagedIcqqClient, type IcqqBackgroundOperation } from './managed-client.js';
81
+ import { createIcqqActionPorts } from './group-actions.js';
80
82
  import { inspectGroupMember, moderateGroupMember } from './group-moderation.js';
81
83
  import { inspectGroupMemberProfile, inspectGroupScene } from './group-scene.js';
82
84
  import { createIcqqSelfProfile } from './self-profile.js';
@@ -189,6 +191,7 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
189
191
  throw new Error('QQ did not confirm the group notice');
190
192
  }
191
193
  },
194
+ ...createIcqqActionPorts(() => this),
192
195
  });
193
196
 
194
197
  readonly #control: EndpointControl = Object.freeze<EndpointControl>({
@@ -0,0 +1,105 @@
1
+ import type { Client, MemberInfo } from '@icqqjs/icqq';
2
+ import { createEndpointOperation, type EndpointManagement, type EndpointOperation } from 'zhin.js/adapter';
3
+
4
+ type ActionPorts = Pick<
5
+ EndpointManagement,
6
+ | 'pokeUser'
7
+ | 'unmuteGroupMember'
8
+ | 'muteAllGroupMembers'
9
+ | 'setGroupCard'
10
+ | 'setGroupTitle'
11
+ | 'setGroupEssence'
12
+ | 'inviteGroupMember'
13
+ >;
14
+
15
+ function numericId(value: string): number {
16
+ const id = Number(value);
17
+ if (!/^[1-9]\d*$/.test(value) || !Number.isSafeInteger(id)) throw new Error('Invalid QQ identifier');
18
+ return id;
19
+ }
20
+
21
+ /** Uncached member lookup; `pickMember` and the cached roster would report stale roles. */
22
+ async function freshMember(operation: EndpointOperation, client: Client, group: number, user: number): Promise<MemberInfo | undefined> {
23
+ try {
24
+ const member = await operation.prepare(() => client.getGroupMemberInfo(group, user, true));
25
+ return member.user_id === user && member.group_id === group ? member : undefined;
26
+ } catch {
27
+ return undefined;
28
+ }
29
+ }
30
+
31
+ async function requireBotRole(operation: EndpointOperation, client: Client, group: number, roles: readonly string[], message: string): Promise<void> {
32
+ const self = await freshMember(operation, client, group, client.uin);
33
+ if (!self || !roles.includes(self.role)) throw new Error(message);
34
+ }
35
+
36
+ function confirmed(ok: unknown, what: string): void {
37
+ if (ok === false) throw new Error(`QQ did not confirm ${what}`);
38
+ }
39
+
40
+ /**
41
+ * ADR 0044 action ports of the icqq endpoint. Where the bot's role decides the outcome the role is
42
+ * read fresh from the platform first, so a stale cache can neither allow nor refuse the action.
43
+ * Each request carries its own lifetime: reads are `prepare`, the native effect is `run`.
44
+ */
45
+ export function createIcqqActionPorts(getClient: () => Client): ActionPorts {
46
+ return {
47
+ async pokeUser({ userId, groupId, signal }) {
48
+ const client = getClient();
49
+ const operation = createEndpointOperation({ signal });
50
+ const user = numericId(userId);
51
+ confirmed(
52
+ groupId === undefined
53
+ ? await operation.run(() => client.pickFriend(user).poke(false))
54
+ : await operation.run(() => client.sendGroupPoke(numericId(groupId), user)),
55
+ 'poke',
56
+ );
57
+ },
58
+ async unmuteGroupMember({ groupId, userId, signal }) {
59
+ const client = getClient();
60
+ const operation = createEndpointOperation({ signal });
61
+ const group = numericId(groupId);
62
+ await requireBotRole(operation, client, group, ['owner', 'admin'], 'Bot is not a group admin');
63
+ confirmed(await operation.run(() => client.setGroupBan(group, numericId(userId), 0)), 'unmute');
64
+ },
65
+ async muteAllGroupMembers({ groupId, enabled, signal }) {
66
+ const client = getClient();
67
+ const operation = createEndpointOperation({ signal });
68
+ const group = numericId(groupId);
69
+ await requireBotRole(operation, client, group, ['owner', 'admin'], 'Bot is not a group admin');
70
+ confirmed(await operation.run(() => client.setGroupWholeBan(group, enabled)), 'whole-group mute');
71
+ },
72
+ async setGroupCard({ groupId, userId, card, signal }) {
73
+ const client = getClient();
74
+ const operation = createEndpointOperation({ signal });
75
+ const group = numericId(groupId);
76
+ const user = numericId(userId);
77
+ if (user !== client.uin) await requireBotRole(operation, client, group, ['owner', 'admin'], 'Bot is not a group admin');
78
+ confirmed(await operation.run(() => client.setGroupCard(group, user, card)), 'card change');
79
+ },
80
+ async setGroupTitle({ groupId, userId, title, durationSeconds, signal }) {
81
+ const client = getClient();
82
+ const operation = createEndpointOperation({ signal });
83
+ const group = numericId(groupId);
84
+ const user = numericId(userId);
85
+ await requireBotRole(operation, client, group, ['owner'], 'Only the group owner can set special titles');
86
+ if (!await freshMember(operation, client, group, user)) throw new Error('Target is not a group member');
87
+ confirmed(await operation.run(() => client.setGroupSpecialTitle(group, user, title, durationSeconds ?? -1)), 'title change');
88
+ },
89
+ async setGroupEssence({ groupId, messageId, enabled, signal }) {
90
+ const client = getClient();
91
+ const operation = createEndpointOperation({ signal });
92
+ await requireBotRole(operation, client, numericId(groupId), ['owner', 'admin'], 'Bot is not a group admin');
93
+ if (enabled) await operation.run(() => client.setEssenceMessage(messageId));
94
+ else await operation.run(() => client.removeEssenceMessage(messageId));
95
+ },
96
+ async inviteGroupMember({ groupId, userId, signal }) {
97
+ const client = getClient();
98
+ const operation = createEndpointOperation({ signal });
99
+ const group = numericId(groupId);
100
+ const user = numericId(userId);
101
+ if (await freshMember(operation, client, group, user)) throw new Error('Target is already a group member');
102
+ confirmed(await operation.run(() => client.inviteFriend(group, user)), 'invitation');
103
+ },
104
+ };
105
+ }
@@ -0,0 +1,19 @@
1
+ import type { SensitiveExecCommandRule } from '@zhin.js/agent';
2
+
3
+ /**
4
+ * The `icqq` command-line client can kick members, move money and delete files. Under an `allowlist`
5
+ * exec policy its non-sensitive subcommands run freely, while these need owner approval (ADR 0044:
6
+ * this knowledge lives with the adapter, the exec policy only reads the registered rule).
7
+ */
8
+ export const ICQQ_SENSITIVE_SUBCOMMAND_REGEXES: readonly RegExp[] = Object.freeze([
9
+ /\bicqq\s+friend\s+(?:delete|block|move)\b/i,
10
+ /\bicqq\s+group\s+(?:kick|mute|set|admin|announce|transfer|quit|dissolve|invite)\b/i,
11
+ /\bicqq\s+(?:request|wallet|pay|recall)\b/i,
12
+ /\bicqq\s+file\s+(?:upload|delete)\b/i,
13
+ /\bicqq\s+gfs\b.*\b(?:rm|delete|del)\b/i,
14
+ ]);
15
+
16
+ export const icqqSensitiveExecRule: SensitiveExecCommandRule = Object.freeze({
17
+ command: 'icqq',
18
+ sensitive: ICQQ_SENSITIVE_SUBCOMMAND_REGEXES,
19
+ });