@zhin.js/adapter-icqq 1.1.20 → 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 (48) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/PERMITS.md +1 -0
  3. package/README.md +11 -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-conversation/SKILL.md +2 -2
  9. package/agents/icqq/skills/icqq-conversation/tools/conversation_info/index.js +1 -1
  10. package/agents/icqq/skills/icqq-conversation/tools/conversation_info/index.ts +1 -1
  11. package/agents/icqq/skills/icqq-group-admin/SKILL.md +8 -3
  12. package/agents/icqq/skills/icqq-group-admin/tools/announce/index.js +2 -0
  13. package/agents/icqq/skills/icqq-group-admin/tools/announce/index.ts +2 -0
  14. package/agents/icqq/skills/icqq-group-admin/tools/essence/index.js +2 -0
  15. package/agents/icqq/skills/icqq-group-admin/tools/essence/index.ts +2 -0
  16. package/agents/icqq/skills/icqq-group-admin/tools/group_kick/index.js +2 -0
  17. package/agents/icqq/skills/icqq-group-admin/tools/group_kick/index.ts +2 -0
  18. package/agents/icqq/skills/icqq-group-admin/tools/group_leave/index.js +1 -1
  19. package/agents/icqq/skills/icqq-group-admin/tools/group_leave/index.ts +1 -1
  20. package/agents/icqq/skills/icqq-group-admin/tools/recall_member_message/index.js +3 -1
  21. package/agents/icqq/skills/icqq-group-admin/tools/recall_member_message/index.ts +3 -1
  22. package/agents/icqq/skills/icqq-group-admin/tools/set_group_admin/index.js +43 -0
  23. package/agents/icqq/skills/icqq-group-admin/tools/set_group_admin/index.ts +36 -0
  24. package/agents/icqq/skills/icqq-group-admin/tools/set_mute/index.js +2 -0
  25. package/agents/icqq/skills/icqq-group-admin/tools/set_mute/index.ts +2 -0
  26. package/agents/icqq/skills/icqq-group-admin/tools/set_title/index.js +2 -0
  27. package/agents/icqq/skills/icqq-group-admin/tools/set_title/index.ts +2 -0
  28. package/agents/icqq/skills/icqq-messages/SKILL.md +1 -1
  29. package/agents/icqq/skills/icqq-messages/tools/recall_own_message/index.js +1 -1
  30. package/agents/icqq/skills/icqq-messages/tools/recall_own_message/index.ts +1 -1
  31. package/lib/endpoint.js +6 -2
  32. package/lib/group-actions.d.ts +10 -0
  33. package/lib/group-actions.js +94 -0
  34. package/lib/protocol.d.ts +2 -1
  35. package/lib/self-profile.d.ts +6 -0
  36. package/lib/self-profile.js +56 -0
  37. package/lib/sensitive-commands.d.ts +8 -0
  38. package/lib/sensitive-commands.js +16 -0
  39. package/package.json +26 -20
  40. package/plugin.js +8 -1
  41. package/prompt-sections/platform/index.js +1 -1
  42. package/prompt-sections/platform/index.ts +1 -1
  43. package/schema.json +106 -1
  44. package/src/endpoint.ts +6 -2
  45. package/src/group-actions.ts +105 -0
  46. package/src/protocol.ts +2 -1
  47. package/src/self-profile.ts +55 -0
  48. package/src/sensitive-commands.ts +19 -0
@@ -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) {
@@ -23,6 +23,6 @@ Only use real message IDs from the current conversation. Do not invent IDs, sour
23
23
 
24
24
  `quote_reply` sends a text reply quoting one original; `forward_messages` sends a merged forward of the supplied originals. Use `inspect_conversation_reference` to read an incoming quote/forward when available; it does not send messages.
25
25
 
26
- `recall_own_message` needs no management role or per-call approval, including when the bot is an ordinary group member. It verifies the bot authored the message. The platform decides the recall time window; never promise success before receiving confirmation or retry an unconfirmed result automatically. To recall another member's message, load `icqq-group-admin` instead.
26
+ `recall_own_message` needs no management role or per-call approval, including when you are an ordinary group member. It verifies that you authored the message. The platform decides the recall time window; never promise success before receiving confirmation or retry an unconfirmed result automatically. To recall another member's message, load `icqq-group-admin` instead.
27
27
 
28
28
  These tools currently support QQ private/group conversations, not guild channels. A failed or suppressed outbound receipt is not a delivered message.
@@ -4,7 +4,7 @@ import { z } from 'zod';
4
4
  import { recallMessage, resolveMessage } from '../../../../conversation.js';
5
5
  import { messages as t } from '../../../../messages.js';
6
6
  export default defineAgentTool({
7
- description: '撤回机器人在当前 QQ 群或私聊中自己的发言;无需群管角色或逐次审批,时限由平台决定',
7
+ description: '撤回你在当前 QQ 群或私聊中自己的发言;无需群管角色或逐次审批,时限由平台决定',
8
8
  adapter: 'icqq',
9
9
  scopes: ['private', 'group'],
10
10
  requiresApproval: 'never',
@@ -4,7 +4,7 @@ import { recallMessage, resolveMessage } from '../../../../conversation.js';
4
4
  import { messages as t } from '../../../../messages.js';
5
5
 
6
6
  export default defineAgentTool({
7
- description: '撤回机器人在当前 QQ 群或私聊中自己的发言;无需群管角色或逐次审批,时限由平台决定',
7
+ description: '撤回你在当前 QQ 群或私聊中自己的发言;无需群管角色或逐次审批,时限由平台决定',
8
8
  adapter: 'icqq',
9
9
  scopes: ['private', 'group'],
10
10
  requiresApproval: 'never',
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,8 +14,10 @@ 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';
20
+ import { createIcqqSelfProfile } from './self-profile.js';
18
21
  const BOUND_EVENTS = [
19
22
  'message.private',
20
23
  'message.group',
@@ -107,6 +110,7 @@ export class IcqqEndpoint extends ManagedIcqqClient {
107
110
  throw new Error('QQ did not confirm the group notice');
108
111
  }
109
112
  },
113
+ ...createIcqqActionPorts(() => this),
110
114
  });
111
115
  #control = Object.freeze({
112
116
  recall: async ({ message, signal }) => {
@@ -163,7 +167,7 @@ export class IcqqEndpoint extends ManagedIcqqClient {
163
167
  start: signal => this.#startFramework(signal), open: () => this.#openFramework(), close: () => this.#closeFramework(), stop: () => this.#stopFramework(),
164
168
  send: request => this.#sendFramework(request), get name() { return name(); },
165
169
  get transportState() { return state(); },
166
- management: this.#management, control: this.#control,
170
+ management: this.#management, control: this.#control, profile: createIcqqSelfProfile(this),
167
171
  get content() { return content(); },
168
172
  });
169
173
  this.#contentResolver = new IcqqContentResolver(this);
@@ -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
+ }
package/lib/protocol.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { EndpointProfileConfiguration } from 'zhin.js/adapter';
1
2
  import type { Sendable } from '@icqqjs/icqq';
2
3
  import { type ConversationRef } from '@zhin.js/im-contract';
3
4
  import { type AdapterNotifyTarget, type ResolvedAdapterNotifyTarget } from 'zhin.js/adapter';
@@ -163,7 +164,7 @@ export declare const Actions: {
163
164
  readonly GET_NOTIFY: "get_notify";
164
165
  };
165
166
  /** One expanded ICQQ endpoint configuration produced by AdapterIndex. */
166
- export interface IcqqEndpointConfig {
167
+ export interface IcqqEndpointConfig extends EndpointProfileConfiguration {
167
168
  readonly id: string;
168
169
  readonly autoReconnect?: boolean;
169
170
  readonly outboundMedia?: 'file' | 'base64';
@@ -0,0 +1,6 @@
1
+ import type { Client } from '@icqqjs/icqq';
2
+ import { type EndpointProfile } from 'zhin.js/adapter';
3
+ type IcqqProfileClient = Pick<Client, 'uin' | 'nickname' | 'isOnline' | 'getProfile' | 'setNickname' | 'setGender' | 'setSignature' | 'setDescription' | 'setAvatar'>;
4
+ /** ICQQ owns the platform facts; config fallbacks and call leases belong to Adapter. */
5
+ export declare function createIcqqSelfProfile(client: IcqqProfileClient): EndpointProfile;
6
+ export {};
@@ -0,0 +1,56 @@
1
+ import { createEndpointOperation } from 'zhin.js/adapter';
2
+ /** ICQQ owns the platform facts; config fallbacks and call leases belong to Adapter. */
3
+ export function createIcqqSelfProfile(client) {
4
+ return Object.freeze({
5
+ writableFields: Object.freeze(['nickname', 'gender', 'signature', 'bio', 'avatar']),
6
+ get current() { return client.isOnline() ? { nickname: client.nickname } : {}; },
7
+ async read({ signal }) {
8
+ const raw = await createEndpointOperation({ signal }).prepare(() => client.getProfile(client.uin));
9
+ const profile = {
10
+ ...(typeof raw.nickname === 'string' ? { nickname: raw.nickname } : {}),
11
+ ...(raw.sex === 'male' || raw.sex === 'female' || raw.sex === 'unknown' ? { gender: raw.sex } : {}),
12
+ ...(typeof raw.age === 'number' && Number.isFinite(raw.age) ? { age: raw.age } : {}),
13
+ ...(typeof raw.signature === 'string' ? { signature: raw.signature } : {}),
14
+ ...(typeof raw.avatar?.url === 'string' ? { avatar: raw.avatar.url } : {}),
15
+ };
16
+ return profile;
17
+ },
18
+ async write({ change, signal }) {
19
+ const operation = createEndpointOperation({ signal });
20
+ let accepted;
21
+ switch (change.field) {
22
+ case 'nickname':
23
+ accepted = await operation.run(() => client.setNickname(change.value));
24
+ break;
25
+ case 'gender': {
26
+ if (change.value === 'other')
27
+ return { status: 'rejected', message: 'ICQQ supports male, female or unknown gender' };
28
+ const gender = change.value === 'male' ? 1 : change.value === 'female' ? 2 : 0;
29
+ accepted = await operation.run(() => client.setGender(gender));
30
+ break;
31
+ }
32
+ case 'signature':
33
+ accepted = await operation.run(() => client.setSignature(change.value));
34
+ break;
35
+ case 'bio':
36
+ accepted = await operation.run(() => client.setDescription(change.value));
37
+ break;
38
+ case 'avatar': {
39
+ if (change.value.kind === 'file') {
40
+ return { status: 'rejected', message: 'ICQQ avatar requires resolved image bytes, URL or local path' };
41
+ }
42
+ const image = change.value.kind === 'base64' ? Buffer.from(change.value.value, 'base64') : change.value.value;
43
+ await operation.run(() => client.setAvatar(image));
44
+ // The input is not the platform CDN URL; it must not become an observed avatar.
45
+ return { status: 'updated' };
46
+ }
47
+ default: return { status: 'rejected', message: `ICQQ cannot safely update ${change.field}` };
48
+ }
49
+ if (accepted && change.field === 'nickname')
50
+ client.nickname = change.value;
51
+ return accepted
52
+ ? { status: 'updated', profile: { [change.field]: change.value } }
53
+ : { status: 'rejected', message: `ICQQ rejected the ${change.field} change` };
54
+ },
55
+ });
56
+ }
@@ -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.20",
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.26",
37
- "@zhin.js/agent-feature": "1.1.11",
38
- "@zhin.js/client": "1.1.22",
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.54",
41
- "@zhin.js/feature-kit": "1.1.10",
42
- "@zhin.js/im-contract": "1.1.9",
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.12",
45
- "@zhin.js/permission": "1.1.10",
46
- "@zhin.js/skill": "1.1.11",
47
- "@zhin.js/tool": "1.1.13"
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.26",
53
- "@zhin.js/command": "^1.1.12",
54
- "@zhin.js/core": "^1.1.54",
55
- "@zhin.js/prompt-section": "^1.1.10",
56
- "zhin.js": "^1.1.19"
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.10",
82
- "zhin.js": "1.1.19"
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",
@@ -111,7 +117,7 @@
111
117
  "features": [
112
118
  {
113
119
  "package": "@zhin.js/adapter",
114
- "api": "^2.0.0"
120
+ "api": "^2.1.0"
115
121
  },
116
122
  {
117
123
  "package": "@zhin.js/command",
@@ -119,7 +125,7 @@
119
125
  },
120
126
  {
121
127
  "package": "@zhin.js/tool",
122
- "api": "^1.0.0"
128
+ "api": "^1.1.0"
123
129
  },
124
130
  {
125
131
  "package": "@zhin.js/skill",
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
  });
@@ -4,7 +4,7 @@ export default defineAgentPromptSection({
4
4
  title: 'ICQQ / QQ',
5
5
  content: [
6
6
  'Use the current Skill catalog; discover the intent and load the matching Skill before its private Tool.',
7
- 'The current IM operation selects the bot Client automatically; never guess user_id or group_id.',
7
+ 'The current IM operation selects your current endpoint Client automatically; never guess user_id or group_id.',
8
8
  ].join('\n'),
9
9
  layer: 'tools',
10
10
  order: 70,
@@ -4,7 +4,7 @@ export default defineAgentPromptSection({
4
4
  title: 'ICQQ / QQ',
5
5
  content: [
6
6
  'Use the current Skill catalog; discover the intent and load the matching Skill before its private Tool.',
7
- 'The current IM operation selects the bot Client automatically; never guess user_id or group_id.',
7
+ 'The current IM operation selects your current endpoint Client automatically; never guess user_id or group_id.',
8
8
  ].join('\n'),
9
9
  layer: 'tools',
10
10
  order: 70,