@zhin.js/adapter-icqq 1.1.14 → 1.1.15
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.
- package/CHANGELOG.md +29 -0
- package/adapters/icqq/index.js +13 -0
- package/adapters/icqq/index.ts +13 -0
- package/agents/icqq/skills/icqq-directory/SKILL.md +9 -11
- package/agents/icqq/skills/icqq-group-admin/SKILL.md +21 -14
- package/agents/icqq/skills/icqq-group-admin/tools/group_invite/index.js +1 -0
- package/agents/icqq/skills/icqq-group-admin/tools/group_invite/index.ts +1 -0
- package/agents/icqq/skills/icqq-group-admin/tools/group_kick/index.js +12 -10
- package/agents/icqq/skills/icqq-group-admin/tools/group_kick/index.ts +11 -8
- package/agents/icqq/skills/icqq-group-directory/SKILL.md +16 -0
- package/agents/icqq/skills/icqq-group-interaction/SKILL.md +18 -0
- package/agents/icqq/skills/icqq-group-state/SKILL.md +7 -6
- package/agents/icqq/skills/icqq-interaction/SKILL.md +5 -10
- package/lib/endpoint.js +6 -1
- package/lib/group-moderation.d.ts +5 -0
- package/lib/group-moderation.js +46 -0
- package/lib/icqq-inbound.js +15 -0
- package/package.json +16 -16
- package/prompt-sections/platform/index.js +1 -2
- package/prompt-sections/platform/index.ts +1 -2
- package/src/endpoint.ts +7 -1
- package/src/group-moderation.ts +52 -0
- package/src/icqq-inbound.ts +14 -0
- /package/agents/icqq/skills/{icqq-directory → icqq-group-directory}/tools/group_member_info/index.js +0 -0
- /package/agents/icqq/skills/{icqq-directory → icqq-group-directory}/tools/group_member_info/index.ts +0 -0
- /package/agents/icqq/skills/{icqq-interaction → icqq-group-interaction}/tools/group_sign/index.js +0 -0
- /package/agents/icqq/skills/{icqq-interaction → icqq-group-interaction}/tools/group_sign/index.ts +0 -0
- /package/agents/icqq/skills/{icqq-interaction → icqq-group-interaction}/tools/poke/index.js +0 -0
- /package/agents/icqq/skills/{icqq-interaction → icqq-group-interaction}/tools/poke/index.ts +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# @zhin.js/adapter-icqq
|
|
2
2
|
|
|
3
|
+
## 1.1.15
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 7d68df6: 精简 Agent 内置固定提示词与消息说明,保留工具、审批和结构化回复能力。适配器 Skill 按会话场景拆分,群/频道管理仅向对应场景披露;提示段与模型消息段支持场景声明,沿用当前回合权限和 generation 投影。
|
|
8
|
+
- e91235e: Let the bot answer with a reaction, the way a person taps 👍 instead of typing. Adapters declare reactions with `segments.reaction` (values with meanings, optional scopes); the reply schema then has a `reaction` field limited to those values, the instruction says when a reaction alone is enough, and the reply port applies it to the message being answered before any words. icqq offers 21 QQ face reactions in groups.
|
|
9
|
+
- 3781f1c: 支持多模态感知备用模型和 ICQQ 原创表情识图;增强语音回复的使用指引与错误提示,让已启用的 TTS 回复无需逐次审批。ICQQ 踢人工具使用 human 策略,每次都要求独立人工审批,不能由自动审核或历史授权替代。
|
|
10
|
+
|
|
11
|
+
内容审核支持按账号和群显式开启广告、辱骂审核:提醒、警告、10 分钟禁言,通知通过 @ 对应成员说明处理结果,30 分钟无新违规重置;提醒/警告是否撤回原消息由模型结合上下文决定。入群 10 分钟内的明确广告可直接踢出,执行前刷新权限和入群时间;审核、通知或平台操作结果不确定时不盲目重试。
|
|
12
|
+
|
|
13
|
+
并发消息独立审核并按到达顺序保存上下文,仅处罚和通知使用成员预留;重复投递不覆盖正在等待的撤回决定。接受完整 JSON 代码块,记录无效审核结果。人工确认显示具体参数,并在等待确认时隔离参数,确保批准对象与执行对象一致。
|
|
14
|
+
|
|
15
|
+
- Updated dependencies [f98025b]
|
|
16
|
+
- Updated dependencies [66ec1a5]
|
|
17
|
+
- Updated dependencies [7d68df6]
|
|
18
|
+
- Updated dependencies [bf0fa9f]
|
|
19
|
+
- Updated dependencies [3326db3]
|
|
20
|
+
- Updated dependencies [e91235e]
|
|
21
|
+
- Updated dependencies [6da2bad]
|
|
22
|
+
- Updated dependencies [3781f1c]
|
|
23
|
+
- @zhin.js/core@1.1.50
|
|
24
|
+
- @zhin.js/adapter@1.1.22
|
|
25
|
+
- @zhin.js/im-contract@1.1.6
|
|
26
|
+
- @zhin.js/tool@1.1.9
|
|
27
|
+
- @zhin.js/agent-feature@1.1.9
|
|
28
|
+
- @zhin.js/feature-kit@1.1.8
|
|
29
|
+
- @zhin.js/permission@1.1.8
|
|
30
|
+
- @zhin.js/skill@1.1.9
|
|
31
|
+
|
|
3
32
|
## 1.1.14
|
|
4
33
|
|
|
5
34
|
### Patch Changes
|
package/adapters/icqq/index.js
CHANGED
|
@@ -16,6 +16,19 @@ export default defineAdapter({
|
|
|
16
16
|
supported: ['text', 'mention', 'image', 'audio', 'video', 'file', 'reply', 'forward', 'face', 'dice', 'rps', 'share'],
|
|
17
17
|
outboundMedia: ['base64', 'url', 'path'],
|
|
18
18
|
interactive: 'text',
|
|
19
|
+
// QQ group reactions take system face ids; private chats have no reactions.
|
|
20
|
+
reaction: {
|
|
21
|
+
scopes: ['group'],
|
|
22
|
+
values: [
|
|
23
|
+
{ value: '76', label: '赞' }, { value: '66', label: '爱心' }, { value: '124', label: 'OK' },
|
|
24
|
+
{ value: '99', label: '鼓掌' }, { value: '144', label: '喝彩' }, { value: '320', label: '庆祝' },
|
|
25
|
+
{ value: '319', label: '比心' }, { value: '299', label: '牛啊' }, { value: '297', label: '拜谢' },
|
|
26
|
+
{ value: '182', label: '笑哭' }, { value: '179', label: 'doge' }, { value: '264', label: '捂脸' },
|
|
27
|
+
{ value: '271', label: '吃瓜' }, { value: '269', label: '暗中观察' }, { value: '268', label: '问号脸' },
|
|
28
|
+
{ value: '270', label: 'emm' }, { value: '106', label: '委屈' }, { value: '285', label: '摸鱼' },
|
|
29
|
+
{ value: '104', label: '哈欠' }, { value: '267', label: '头秃' }, { value: '326', label: '生气' },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
19
32
|
model: {
|
|
20
33
|
share: {
|
|
21
34
|
fields: ['description', 'image', 'audio', 'platform'],
|
package/adapters/icqq/index.ts
CHANGED
|
@@ -27,6 +27,19 @@ export default defineAdapter<IcqqEndpointConfig>({
|
|
|
27
27
|
supported: ['text', 'mention', 'image', 'audio', 'video', 'file', 'reply', 'forward', 'face', 'dice', 'rps', 'share'],
|
|
28
28
|
outboundMedia: ['base64', 'url', 'path'],
|
|
29
29
|
interactive: 'text',
|
|
30
|
+
// QQ group reactions take system face ids; private chats have no reactions.
|
|
31
|
+
reaction: {
|
|
32
|
+
scopes: ['group'],
|
|
33
|
+
values: [
|
|
34
|
+
{ value: '76', label: '赞' }, { value: '66', label: '爱心' }, { value: '124', label: 'OK' },
|
|
35
|
+
{ value: '99', label: '鼓掌' }, { value: '144', label: '喝彩' }, { value: '320', label: '庆祝' },
|
|
36
|
+
{ value: '319', label: '比心' }, { value: '299', label: '牛啊' }, { value: '297', label: '拜谢' },
|
|
37
|
+
{ value: '182', label: '笑哭' }, { value: '179', label: 'doge' }, { value: '264', label: '捂脸' },
|
|
38
|
+
{ value: '271', label: '吃瓜' }, { value: '269', label: '暗中观察' }, { value: '268', label: '问号脸' },
|
|
39
|
+
{ value: '270', label: 'emm' }, { value: '106', label: '委屈' }, { value: '285', label: '摸鱼' },
|
|
40
|
+
{ value: '104', label: '哈欠' }, { value: '267', label: '头秃' }, { value: '326', label: '生气' },
|
|
41
|
+
],
|
|
42
|
+
},
|
|
30
43
|
model: {
|
|
31
44
|
share: {
|
|
32
45
|
fields: ['description', 'image', 'audio', 'platform'],
|
|
@@ -2,19 +2,17 @@
|
|
|
2
2
|
name: icqq-directory
|
|
3
3
|
platforms:
|
|
4
4
|
- icqq
|
|
5
|
-
description: ICQQ
|
|
5
|
+
description: ICQQ 好友列表、群列表和用户资料查询。
|
|
6
6
|
keywords:
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
7
|
+
- icqq
|
|
8
|
+
- 好友列表
|
|
9
|
+
- 群列表
|
|
10
|
+
- 用户资料
|
|
11
11
|
tools:
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
- "group_member_info"
|
|
12
|
+
- friend_list
|
|
13
|
+
- group_list
|
|
14
|
+
- user_info
|
|
16
15
|
---
|
|
17
|
-
|
|
18
16
|
# icqq-directory
|
|
19
17
|
|
|
20
|
-
|
|
18
|
+
先查询稳定 QQ 号或群号,不猜测昵称对应的 ID。目录信息只向群主、管理员、master 或 trusted 披露。
|
|
@@ -4,22 +4,29 @@ platforms:
|
|
|
4
4
|
- icqq
|
|
5
5
|
description: ICQQ 邀请、踢人、禁言、头衔、匿名、公告和精华管理能力。
|
|
6
6
|
keywords:
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
7
|
+
- icqq
|
|
8
|
+
- 群管理
|
|
9
|
+
- 禁言
|
|
10
|
+
- 踢人
|
|
11
|
+
- 公告
|
|
12
|
+
- 精华
|
|
13
13
|
tools:
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
14
|
+
- group_invite
|
|
15
|
+
- group_kick
|
|
16
|
+
- set_mute
|
|
17
|
+
- set_title
|
|
18
|
+
- set_anonymous
|
|
19
|
+
- announce
|
|
20
|
+
- essence
|
|
21
|
+
scopes:
|
|
22
|
+
- group
|
|
21
23
|
---
|
|
22
|
-
|
|
23
24
|
# icqq-group-admin
|
|
24
25
|
|
|
25
26
|
仅在目标群和目标成员明确时加载。所有写操作遵守 Tool 审批与角色权限;不可逆或影响成员关系的操作先复核参数。
|
|
27
|
+
|
|
28
|
+
自动管群必须先确认具体规则和目标群,不能仅凭权限许可自行制定触发条件。
|
|
29
|
+
|
|
30
|
+
本技能的踢人工具每次都需人工审批,必须展示本次目标群、目标成员和理由;上一笔操作的批准不能授权下一笔踢人。
|
|
31
|
+
|
|
32
|
+
已配置的自动审核流程可对入群 10 分钟内的明确广告直接踢出;该例外由审核流程核验,不授权本技能或普通对话绕过人工审批。
|
|
@@ -7,6 +7,7 @@ export default defineAgentTool<{ group_id: number; user_id: number }>({
|
|
|
7
7
|
user_id: z.number().describe('要邀请的 QQ号'),
|
|
8
8
|
}),
|
|
9
9
|
adapter: 'icqq',
|
|
10
|
+
scopes: ["group"],
|
|
10
11
|
requiresApproval: 'always',
|
|
11
12
|
async execute({ group_id, user_id }, context) {
|
|
12
13
|
const client = context.$client;
|
|
@@ -6,22 +6,24 @@ export default defineAgentTool({
|
|
|
6
6
|
inputSchema: z.object({
|
|
7
7
|
group_id: z.number().describe('目标群号'),
|
|
8
8
|
user_id: z.number().describe('要踢出的成员 QQ号'),
|
|
9
|
+
reason: z.string().min(1).describe('本次踢人的具体原因,随目标群和成员一起供人工确认'),
|
|
9
10
|
}),
|
|
10
11
|
adapter: 'icqq',
|
|
11
12
|
scopes: ['group'],
|
|
12
13
|
permissions: ['role(master,admin,trusted,owner)'],
|
|
13
|
-
requiresApproval: '
|
|
14
|
+
requiresApproval: 'human',
|
|
14
15
|
async execute({ group_id, user_id }, context) {
|
|
15
16
|
const client = context.$client;
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
await client.setGroupKick(group_id, user_id)
|
|
17
|
+
const [self, member] = await Promise.all([
|
|
18
|
+
client.getGroupMemberInfo(group_id, client.uin, true),
|
|
19
|
+
client.getGroupMemberInfo(group_id, user_id, true),
|
|
20
|
+
]);
|
|
21
|
+
if (user_id === client.uin || (self.role !== 'owner' && self.role !== 'admin')
|
|
22
|
+
|| member.role !== 'member')
|
|
23
|
+
return { success: false, message: '当前权限或成员状态不允许踢出' };
|
|
24
|
+
context.signal.throwIfAborted();
|
|
25
|
+
if (!await client.setGroupKick(group_id, user_id))
|
|
26
|
+
return { success: false, message: '平台未确认踢出成功,请勿自动重试' };
|
|
25
27
|
return { success: true, message: `已踢出 ${user_id} 成员` };
|
|
26
28
|
},
|
|
27
29
|
});
|
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
import { defineAgentTool } from '@zhin.js/tool';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
export default defineAgentTool<{ group_id: number; user_id: number }>({
|
|
3
|
+
export default defineAgentTool<{ group_id: number; user_id: number; reason: string }>({
|
|
4
4
|
description: '踢出 QQ 群中的某个成员',
|
|
5
5
|
inputSchema: z.object({
|
|
6
6
|
group_id: z.number().describe('目标群号'),
|
|
7
7
|
user_id: z.number().describe('要踢出的成员 QQ号'),
|
|
8
|
+
reason: z.string().min(1).describe('本次踢人的具体原因,随目标群和成员一起供人工确认'),
|
|
8
9
|
}),
|
|
9
10
|
adapter: 'icqq',
|
|
10
11
|
scopes:['group'],
|
|
11
12
|
permissions: ['role(master,admin,trusted,owner)'],
|
|
12
|
-
requiresApproval: '
|
|
13
|
+
requiresApproval: 'human',
|
|
13
14
|
async execute({ group_id, user_id }, context) {
|
|
14
15
|
const client = context.$client;
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if(
|
|
20
|
-
|
|
16
|
+
const [self, member] = await Promise.all([
|
|
17
|
+
client.getGroupMemberInfo(group_id, client.uin, true),
|
|
18
|
+
client.getGroupMemberInfo(group_id, user_id, true),
|
|
19
|
+
]);
|
|
20
|
+
if (user_id === client.uin || (self.role !== 'owner' && self.role !== 'admin')
|
|
21
|
+
|| member.role !== 'member') return { success: false, message: '当前权限或成员状态不允许踢出' };
|
|
22
|
+
context.signal.throwIfAborted();
|
|
23
|
+
if (!await client.setGroupKick(group_id, user_id)) return { success: false, message: '平台未确认踢出成功,请勿自动重试' };
|
|
21
24
|
return { success: true, message: `已踢出 ${user_id} 成员` };
|
|
22
25
|
},
|
|
23
26
|
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: icqq-group-interaction
|
|
3
|
+
platforms:
|
|
4
|
+
- icqq
|
|
5
|
+
scopes:
|
|
6
|
+
- group
|
|
7
|
+
description: ICQQ 群成员戳一戳与群签到。
|
|
8
|
+
keywords:
|
|
9
|
+
- icqq
|
|
10
|
+
- 戳一戳
|
|
11
|
+
- 签到
|
|
12
|
+
tools:
|
|
13
|
+
- poke
|
|
14
|
+
- group_sign
|
|
15
|
+
---
|
|
16
|
+
# icqq-group-interaction
|
|
17
|
+
|
|
18
|
+
确认当前群和目标成员后调用;点赞使用 icqq-interaction。遵守各 Tool 审批策略。
|
|
@@ -4,14 +4,15 @@ platforms:
|
|
|
4
4
|
- icqq
|
|
5
5
|
description: ICQQ 群文件与禁言状态查询能力。
|
|
6
6
|
keywords:
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
7
|
+
- icqq
|
|
8
|
+
- 群文件
|
|
9
|
+
- 禁言列表
|
|
10
10
|
tools:
|
|
11
|
-
-
|
|
12
|
-
-
|
|
11
|
+
- group_files
|
|
12
|
+
- list_muted
|
|
13
|
+
scopes:
|
|
14
|
+
- group
|
|
13
15
|
---
|
|
14
|
-
|
|
15
16
|
# icqq-group-state
|
|
16
17
|
|
|
17
18
|
用于读取群文件和禁言状态。确认群号后调用,不把列表之外的信息当作已验证事实。
|
|
@@ -2,18 +2,13 @@
|
|
|
2
2
|
name: icqq-interaction
|
|
3
3
|
platforms:
|
|
4
4
|
- icqq
|
|
5
|
-
description: ICQQ
|
|
5
|
+
description: ICQQ 用户点赞。
|
|
6
6
|
keywords:
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
- "戳一戳"
|
|
10
|
-
- "签到"
|
|
7
|
+
- icqq
|
|
8
|
+
- 点赞
|
|
11
9
|
tools:
|
|
12
|
-
-
|
|
13
|
-
- "send_user_like"
|
|
14
|
-
- "group_sign"
|
|
10
|
+
- send_user_like
|
|
15
11
|
---
|
|
16
|
-
|
|
17
12
|
# icqq-interaction
|
|
18
13
|
|
|
19
|
-
|
|
14
|
+
用于给用户点赞;先确认真实 QQ 号,遵守 Tool 审批策略。
|
package/lib/endpoint.js
CHANGED
|
@@ -13,6 +13,7 @@ import { IcqqGuildCatalog, normalizeIcqqGuildInboundMessage, } from './icqq-guil
|
|
|
13
13
|
import { prepareIcqqOutboundMedia, resolveIcqqOutboundMediaMode, } from './outbound-media.js';
|
|
14
14
|
import { formatInboundContent, formatOutboundMessages, normalizeIcqqImageMedia, icqqInboundConversation, icqqOutboundTarget, } from './protocol.js';
|
|
15
15
|
import { ManagedIcqqClient } from './managed-client.js';
|
|
16
|
+
import { inspectGroupMember, moderateGroupMember } from './group-moderation.js';
|
|
16
17
|
const BOUND_EVENTS = [
|
|
17
18
|
'message.private',
|
|
18
19
|
'message.group',
|
|
@@ -55,6 +56,8 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
55
56
|
#contentResolver;
|
|
56
57
|
content;
|
|
57
58
|
management = Object.freeze({
|
|
59
|
+
inspectGroupMember: (group, user, signal) => inspectGroupMember(this.client, group, user, signal),
|
|
60
|
+
moderateGroupMember: (group, user, action, signal) => moderateGroupMember(this.client, group, user, action, signal, () => this.#open && !this.#retiring && !this.#stopped),
|
|
58
61
|
listFriends: async () => Array.from(this.client.fl.values()).map((f) => ({
|
|
59
62
|
user_id: f.user_id,
|
|
60
63
|
nickname: f.nickname,
|
|
@@ -89,7 +92,8 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
89
92
|
recall: async (message) => {
|
|
90
93
|
if (!message.id || message.id.startsWith('outbound:'))
|
|
91
94
|
return;
|
|
92
|
-
await this.client.deleteMsg(message.id)
|
|
95
|
+
if (!await this.client.deleteMsg(message.id))
|
|
96
|
+
throw new Error('QQ did not confirm message recall');
|
|
93
97
|
},
|
|
94
98
|
addReaction: async (message, emoji) => {
|
|
95
99
|
const target = resolveIcqqGroupReactionTarget(message);
|
|
@@ -764,6 +768,7 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
764
768
|
metadata: {
|
|
765
769
|
nickname: normalized.nickname,
|
|
766
770
|
senderRole: normalized.senderRole,
|
|
771
|
+
platformSentAt: normalized.timestampMs,
|
|
767
772
|
},
|
|
768
773
|
}, admittedBeforeRetire);
|
|
769
774
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Client } from '@icqqjs/icqq';
|
|
2
|
+
import type { GroupModerationAction, GroupModerationMember } from 'zhin.js/adapter';
|
|
3
|
+
/** Native role/join facts stay in the adapter; the moderation plugin owns policy. */
|
|
4
|
+
export declare function inspectGroupMember(client: Client, groupId: string, userId: string, signal: AbortSignal): Promise<GroupModerationMember>;
|
|
5
|
+
export declare function moderateGroupMember(client: Client, groupId: string, userId: string, action: GroupModerationAction, signal: AbortSignal, isActive: () => boolean): Promise<void>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Native role/join facts stay in the adapter; the moderation plugin owns policy. */
|
|
2
|
+
export async function inspectGroupMember(client, groupId, userId, signal) {
|
|
3
|
+
signal.throwIfAborted();
|
|
4
|
+
const group = numericId(groupId);
|
|
5
|
+
const user = numericId(userId);
|
|
6
|
+
const [self, member] = await Promise.all([
|
|
7
|
+
client.getGroupMemberInfo(group, client.uin, true),
|
|
8
|
+
client.getGroupMemberInfo(group, user, true),
|
|
9
|
+
]);
|
|
10
|
+
signal.throwIfAborted();
|
|
11
|
+
const joinedAt = member.join_time * 1000;
|
|
12
|
+
return {
|
|
13
|
+
canModerate: client.isOnline() && user !== client.uin
|
|
14
|
+
&& self.user_id === client.uin && self.group_id === group
|
|
15
|
+
&& member.user_id === user && member.group_id === group
|
|
16
|
+
&& (self.role === 'owner' || self.role === 'admin') && member.role === 'member',
|
|
17
|
+
...(Number.isSafeInteger(joinedAt) && joinedAt > 0 && joinedAt <= Date.now() ? { joinedAt } : {}),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export async function moderateGroupMember(client, groupId, userId, action, signal, isActive) {
|
|
21
|
+
const member = await inspectGroupMember(client, groupId, userId, signal);
|
|
22
|
+
signal.throwIfAborted();
|
|
23
|
+
if (!isActive() || !member.canModerate)
|
|
24
|
+
throw new Error('Group moderation permission is unavailable');
|
|
25
|
+
if (action.type === 'kick') {
|
|
26
|
+
if (!Number.isSafeInteger(action.maxMembershipAgeMs) || action.maxMembershipAgeMs <= 0
|
|
27
|
+
|| member.joinedAt !== action.joinedAt || !member.joinedAt
|
|
28
|
+
|| Date.now() - member.joinedAt > action.maxMembershipAgeMs) {
|
|
29
|
+
throw new Error('Group membership no longer matches the approved newcomer window');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else if (!Number.isSafeInteger(action.durationSeconds) || action.durationSeconds <= 0) {
|
|
33
|
+
throw new Error('Invalid group mute duration');
|
|
34
|
+
}
|
|
35
|
+
const ok = action.type === 'kick'
|
|
36
|
+
? await client.setGroupKick(numericId(groupId), numericId(userId))
|
|
37
|
+
: await client.setGroupBan(numericId(groupId), numericId(userId), action.durationSeconds);
|
|
38
|
+
if (!ok)
|
|
39
|
+
throw new Error('Platform did not confirm the moderation action');
|
|
40
|
+
}
|
|
41
|
+
function numericId(value) {
|
|
42
|
+
const id = Number(value);
|
|
43
|
+
if (!/^[1-9]\d*$/.test(value) || !Number.isSafeInteger(id))
|
|
44
|
+
throw new Error('Invalid QQ identifier');
|
|
45
|
+
return id;
|
|
46
|
+
}
|
package/lib/icqq-inbound.js
CHANGED
|
@@ -248,6 +248,21 @@ export function icqqElementsToSegments(elements) {
|
|
|
248
248
|
case "face":
|
|
249
249
|
out.push({ type: "face", data: { id: Number(el.id) } });
|
|
250
250
|
break;
|
|
251
|
+
case "bface": {
|
|
252
|
+
// ICQQ 的原创表情 file 是两个 MD5 加包编号,不是本地文件路径。
|
|
253
|
+
const file = typeof el.file === "string" ? el.file : "";
|
|
254
|
+
if (/^[a-f\d]{64}\d+$/i.test(file)) {
|
|
255
|
+
const md5 = file.slice(0, 32).toLowerCase();
|
|
256
|
+
out.push({
|
|
257
|
+
type: "image",
|
|
258
|
+
data: { media: { kind: "url", value: `https://gxh.vip.qq.com/club/item/parcel/item/${md5.slice(0, 2)}/${md5}/raw300.gif`, mime_type: "image/gif" } },
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
// 保留标签:下载或识图失败时仍能读懂表情的文字含义。
|
|
262
|
+
if (el.text)
|
|
263
|
+
out.push({ type: "text", data: { text: String(el.text) } });
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
251
266
|
case "image": {
|
|
252
267
|
const value = String(el.url ?? el.file ?? "");
|
|
253
268
|
if (!value)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-icqq",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.15",
|
|
4
4
|
"description": "Zhin.js ICQQ adapter — direct connection to @icqqjs/icqq",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -33,24 +33,24 @@
|
|
|
33
33
|
"directory": "plugins/adapters/icqq"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@zhin.js/adapter": "1.1.
|
|
37
|
-
"@zhin.js/agent-feature": "1.1.
|
|
38
|
-
"@zhin.js/core": "1.1.
|
|
39
|
-
"@zhin.js/feature-kit": "1.1.
|
|
40
|
-
"@zhin.js/im-contract": "1.1.
|
|
36
|
+
"@zhin.js/adapter": "1.1.22",
|
|
37
|
+
"@zhin.js/agent-feature": "1.1.9",
|
|
38
|
+
"@zhin.js/core": "1.1.50",
|
|
39
|
+
"@zhin.js/feature-kit": "1.1.8",
|
|
40
|
+
"@zhin.js/im-contract": "1.1.6",
|
|
41
41
|
"@zhin.js/logger": "1.1.2",
|
|
42
|
-
"@zhin.js/permission": "1.1.
|
|
43
|
-
"@zhin.js/skill": "1.1.
|
|
44
|
-
"@zhin.js/tool": "1.1.
|
|
42
|
+
"@zhin.js/permission": "1.1.8",
|
|
43
|
+
"@zhin.js/skill": "1.1.9",
|
|
44
|
+
"@zhin.js/tool": "1.1.9"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"@icqqjs/icqq": "^1.12.2",
|
|
48
48
|
"zod": "^4.0.0",
|
|
49
|
-
"@zhin.js/adapter": "^1.1.
|
|
50
|
-
"@zhin.js/command": "^1.1.
|
|
51
|
-
"@zhin.js/core": "^1.1.
|
|
52
|
-
"@zhin.js/prompt-section": "^1.1.
|
|
53
|
-
"zhin.js": "^1.1.
|
|
49
|
+
"@zhin.js/adapter": "^1.1.22",
|
|
50
|
+
"@zhin.js/command": "^1.1.10",
|
|
51
|
+
"@zhin.js/core": "^1.1.50",
|
|
52
|
+
"@zhin.js/prompt-section": "^1.1.8",
|
|
53
|
+
"zhin.js": "^1.1.15"
|
|
54
54
|
},
|
|
55
55
|
"peerDependenciesMeta": {
|
|
56
56
|
"@icqqjs/icqq": {
|
|
@@ -75,8 +75,8 @@
|
|
|
75
75
|
"typescript": "^6.0.3",
|
|
76
76
|
"vitest": "^4.1.10",
|
|
77
77
|
"zod": "^4.4.3",
|
|
78
|
-
"@zhin.js/prompt-section": "1.1.
|
|
79
|
-
"zhin.js": "1.1.
|
|
78
|
+
"@zhin.js/prompt-section": "1.1.8",
|
|
79
|
+
"zhin.js": "1.1.15"
|
|
80
80
|
},
|
|
81
81
|
"files": [
|
|
82
82
|
"adapters",
|
|
@@ -3,8 +3,7 @@ import { defineAgentPromptSection } from '@zhin.js/prompt-section';
|
|
|
3
3
|
export default defineAgentPromptSection({
|
|
4
4
|
title: 'ICQQ / QQ',
|
|
5
5
|
content: [
|
|
6
|
-
'
|
|
7
|
-
'Use discover for the user intent, then load only the matching Skill before calling its private Tool.',
|
|
6
|
+
'Use the current Skill catalog; discover the intent and load the matching Skill before its private Tool.',
|
|
8
7
|
'The current IM operation selects the bot Client automatically; never guess user_id or group_id.',
|
|
9
8
|
].join('\n'),
|
|
10
9
|
layer: 'tools',
|
|
@@ -3,8 +3,7 @@ import { defineAgentPromptSection } from '@zhin.js/prompt-section';
|
|
|
3
3
|
export default defineAgentPromptSection({
|
|
4
4
|
title: 'ICQQ / QQ',
|
|
5
5
|
content: [
|
|
6
|
-
'
|
|
7
|
-
'Use discover for the user intent, then load only the matching Skill before calling its private Tool.',
|
|
6
|
+
'Use the current Skill catalog; discover the intent and load the matching Skill before its private Tool.',
|
|
8
7
|
'The current IM operation selects the bot Client automatically; never guess user_id or group_id.',
|
|
9
8
|
].join('\n'),
|
|
10
9
|
layer: 'tools',
|
package/src/endpoint.ts
CHANGED
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
} from './protocol.js';
|
|
62
62
|
import type { SystemMessage as IcqqSystemMessage } from './types.js';
|
|
63
63
|
import { ManagedIcqqClient, type IcqqBackgroundOperation } from './managed-client.js';
|
|
64
|
+
import { inspectGroupMember, moderateGroupMember } from './group-moderation.js';
|
|
64
65
|
|
|
65
66
|
export interface IcqqEndpointOptions {
|
|
66
67
|
readonly id: CapabilityId;
|
|
@@ -112,6 +113,10 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
112
113
|
readonly content: EndpointContentPort;
|
|
113
114
|
|
|
114
115
|
readonly management: EndpointManagement = Object.freeze<EndpointManagement>({
|
|
116
|
+
inspectGroupMember: (group, user, signal) => inspectGroupMember(this.client, group, user, signal),
|
|
117
|
+
moderateGroupMember: (group, user, action, signal) => moderateGroupMember(
|
|
118
|
+
this.client, group, user, action, signal, () => this.#open && !this.#retiring && !this.#stopped,
|
|
119
|
+
),
|
|
115
120
|
listFriends: async () =>
|
|
116
121
|
Array.from(this.client.fl.values()).map((f) => ({
|
|
117
122
|
user_id: f.user_id,
|
|
@@ -149,7 +154,7 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
149
154
|
readonly control: EndpointControl = Object.freeze({
|
|
150
155
|
recall: async (message: MessageRef) => {
|
|
151
156
|
if (!message.id || message.id.startsWith('outbound:')) return;
|
|
152
|
-
await this.client.deleteMsg(message.id);
|
|
157
|
+
if (!await this.client.deleteMsg(message.id)) throw new Error('QQ did not confirm message recall');
|
|
153
158
|
},
|
|
154
159
|
addReaction: async (message: MessageRef, emoji: string) => {
|
|
155
160
|
const target = resolveIcqqGroupReactionTarget(message);
|
|
@@ -844,6 +849,7 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
844
849
|
metadata: {
|
|
845
850
|
nickname: normalized.nickname,
|
|
846
851
|
senderRole: normalized.senderRole,
|
|
852
|
+
platformSentAt: normalized.timestampMs,
|
|
847
853
|
},
|
|
848
854
|
}, admittedBeforeRetire);
|
|
849
855
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Client } from '@icqqjs/icqq';
|
|
2
|
+
import type { GroupModerationAction, GroupModerationMember } from 'zhin.js/adapter';
|
|
3
|
+
|
|
4
|
+
/** Native role/join facts stay in the adapter; the moderation plugin owns policy. */
|
|
5
|
+
export async function inspectGroupMember(
|
|
6
|
+
client: Client, groupId: string, userId: string, signal: AbortSignal,
|
|
7
|
+
): Promise<GroupModerationMember> {
|
|
8
|
+
signal.throwIfAborted();
|
|
9
|
+
const group = numericId(groupId);
|
|
10
|
+
const user = numericId(userId);
|
|
11
|
+
const [self, member] = await Promise.all([
|
|
12
|
+
client.getGroupMemberInfo(group, client.uin, true),
|
|
13
|
+
client.getGroupMemberInfo(group, user, true),
|
|
14
|
+
]);
|
|
15
|
+
signal.throwIfAborted();
|
|
16
|
+
const joinedAt = member.join_time * 1000;
|
|
17
|
+
return {
|
|
18
|
+
canModerate: client.isOnline() && user !== client.uin
|
|
19
|
+
&& self.user_id === client.uin && self.group_id === group
|
|
20
|
+
&& member.user_id === user && member.group_id === group
|
|
21
|
+
&& (self.role === 'owner' || self.role === 'admin') && member.role === 'member',
|
|
22
|
+
...(Number.isSafeInteger(joinedAt) && joinedAt > 0 && joinedAt <= Date.now() ? { joinedAt } : {}),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function moderateGroupMember(
|
|
27
|
+
client: Client, groupId: string, userId: string, action: GroupModerationAction,
|
|
28
|
+
signal: AbortSignal, isActive: () => boolean,
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
const member = await inspectGroupMember(client, groupId, userId, signal);
|
|
31
|
+
signal.throwIfAborted();
|
|
32
|
+
if (!isActive() || !member.canModerate) throw new Error('Group moderation permission is unavailable');
|
|
33
|
+
if (action.type === 'kick') {
|
|
34
|
+
if (!Number.isSafeInteger(action.maxMembershipAgeMs) || action.maxMembershipAgeMs <= 0
|
|
35
|
+
|| member.joinedAt !== action.joinedAt || !member.joinedAt
|
|
36
|
+
|| Date.now() - member.joinedAt > action.maxMembershipAgeMs) {
|
|
37
|
+
throw new Error('Group membership no longer matches the approved newcomer window');
|
|
38
|
+
}
|
|
39
|
+
} else if (!Number.isSafeInteger(action.durationSeconds) || action.durationSeconds <= 0) {
|
|
40
|
+
throw new Error('Invalid group mute duration');
|
|
41
|
+
}
|
|
42
|
+
const ok = action.type === 'kick'
|
|
43
|
+
? await client.setGroupKick(numericId(groupId), numericId(userId))
|
|
44
|
+
: await client.setGroupBan(numericId(groupId), numericId(userId), action.durationSeconds);
|
|
45
|
+
if (!ok) throw new Error('Platform did not confirm the moderation action');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function numericId(value: string): number {
|
|
49
|
+
const id = Number(value);
|
|
50
|
+
if (!/^[1-9]\d*$/.test(value) || !Number.isSafeInteger(id)) throw new Error('Invalid QQ identifier');
|
|
51
|
+
return id;
|
|
52
|
+
}
|
package/src/icqq-inbound.ts
CHANGED
|
@@ -394,6 +394,20 @@ export function icqqElementsToSegments(
|
|
|
394
394
|
case "face":
|
|
395
395
|
out.push({ type: "face", data: { id: Number(el.id) } });
|
|
396
396
|
break;
|
|
397
|
+
case "bface": {
|
|
398
|
+
// ICQQ 的原创表情 file 是两个 MD5 加包编号,不是本地文件路径。
|
|
399
|
+
const file = typeof el.file === "string" ? el.file : "";
|
|
400
|
+
if (/^[a-f\d]{64}\d+$/i.test(file)) {
|
|
401
|
+
const md5 = file.slice(0, 32).toLowerCase();
|
|
402
|
+
out.push({
|
|
403
|
+
type: "image",
|
|
404
|
+
data: { media: { kind: "url", value: `https://gxh.vip.qq.com/club/item/parcel/item/${md5.slice(0, 2)}/${md5}/raw300.gif`, mime_type: "image/gif" } },
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
// 保留标签:下载或识图失败时仍能读懂表情的文字含义。
|
|
408
|
+
if (el.text) out.push({ type: "text", data: { text: String(el.text) } });
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
397
411
|
case "image": {
|
|
398
412
|
const value = String(el.url ?? el.file ?? "");
|
|
399
413
|
if (!value) break;
|
/package/agents/icqq/skills/{icqq-directory → icqq-group-directory}/tools/group_member_info/index.js
RENAMED
|
File without changes
|
/package/agents/icqq/skills/{icqq-directory → icqq-group-directory}/tools/group_member_info/index.ts
RENAMED
|
File without changes
|
/package/agents/icqq/skills/{icqq-interaction → icqq-group-interaction}/tools/group_sign/index.js
RENAMED
|
File without changes
|
/package/agents/icqq/skills/{icqq-interaction → icqq-group-interaction}/tools/group_sign/index.ts
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|