@zhin.js/adapter-icqq 1.1.16 → 1.1.17
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 +26 -0
- package/agents/icqq/members.js +16 -0
- package/agents/icqq/members.ts +17 -0
- package/agents/icqq/skills/icqq-group-admin/tools/essence/index.js +1 -1
- package/agents/icqq/skills/icqq-group-admin/tools/essence/index.ts +1 -1
- package/agents/icqq/skills/icqq-group-admin/tools/group_invite/index.js +2 -2
- package/agents/icqq/skills/icqq-group-admin/tools/group_invite/index.ts +2 -2
- package/agents/icqq/skills/icqq-group-admin/tools/group_leave/index.js +1 -0
- package/agents/icqq/skills/icqq-group-admin/tools/group_leave/index.ts +1 -0
- package/agents/icqq/skills/icqq-group-admin/tools/recall_member_message/index.js +2 -1
- package/agents/icqq/skills/icqq-group-admin/tools/recall_member_message/index.ts +2 -1
- package/agents/icqq/skills/icqq-group-admin/tools/set_anonymous/index.js +2 -2
- package/agents/icqq/skills/icqq-group-admin/tools/set_anonymous/index.ts +2 -2
- package/agents/icqq/skills/icqq-group-admin/tools/set_mute/index.js +4 -4
- package/agents/icqq/skills/icqq-group-admin/tools/set_mute/index.ts +4 -4
- package/agents/icqq/skills/icqq-group-admin/tools/set_title/index.js +4 -4
- package/agents/icqq/skills/icqq-group-admin/tools/set_title/index.ts +4 -4
- package/agents/icqq/skills/icqq-group-directory/tools/group_member_info/index.js +16 -6
- package/agents/icqq/skills/icqq-group-directory/tools/group_member_info/index.ts +16 -5
- package/agents/icqq/skills/icqq-messages/tools/forward_messages/index.js +1 -1
- package/agents/icqq/skills/icqq-messages/tools/forward_messages/index.ts +1 -1
- package/agents/icqq/skills/icqq-messages/tools/quote_reply/index.js +1 -1
- package/agents/icqq/skills/icqq-messages/tools/quote_reply/index.ts +1 -1
- package/agents/icqq/skills/icqq-messages/tools/recall_own_message/index.js +1 -1
- package/agents/icqq/skills/icqq-messages/tools/recall_own_message/index.ts +1 -1
- package/agents/icqq/system.md +3 -3
- package/lib/endpoint.js +6 -8
- package/lib/group-moderation.js +7 -2
- package/lib/group-scene.d.ts +11 -0
- package/lib/group-scene.js +71 -0
- package/lib/icqq-inbound.d.ts +2 -0
- package/lib/icqq-inbound.js +3 -0
- package/lib/icqq-inbox.d.ts +5 -0
- package/lib/icqq-inbox.js +14 -0
- package/lib/protocol.d.ts +2 -0
- package/lib/protocol.js +4 -2
- package/package.json +9 -9
- package/src/endpoint.ts +6 -7
- package/src/group-moderation.ts +7 -2
- package/src/group-scene.ts +76 -0
- package/src/icqq-inbound.ts +5 -0
- package/src/icqq-inbox.ts +15 -0
- package/src/protocol.ts +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @zhin.js/adapter-icqq
|
|
2
2
|
|
|
3
|
+
## 1.1.17
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- e23cabc: ADR 0041 slice B: the turn context now carries real scene facts. `EndpointManagement` gains `inspectGroupScene` (name, owner, admins, member count, global mute) and `inspectGroupMemberProfile` (role, card, title, mute expiry; one lookup), implemented by the ICQQ adapter from uncached group/member reads. The CLI resolves them per turn (bot role always fresh; group and sender facts reuse a 60 s, 256-entry in-memory cache scoped to the generation; failures omit fields) and the inbound actor now carries `groupNickname` / `groupTitle` / `mutedUntil` plus the platform role. The Session / Bot state blocks are replaced by one projected scene line `[scene_id:…;scene_type:…;scene_name:…;owner_id:…;admins:…;member_count:…;global_mute:…;assistant_role:…;]` (values escaped and clipped to 64 chars). No new on-disk state.
|
|
8
|
+
- 4d728c0: ADR 0043 §4 + §5 (H4) and the ADR 0041 actor-only amendment. Participant headers are no longer stored: user messages keep only `actor`, assistant messages keep `assistantActor` (plus the delivered `messageId`), and the header is rendered at the LLM boundary; legacy rows that still carry `name` are read as before. Headers now use session message aliases `id:m<n>` (an ordinal over the stored active path, so already-sent prefixes never change), `time:HH:mm` shown only when a minute has passed since the last shown time (the first message and the first message of a day also carry `date`), and `reply_to:m<n>` when the message replies to one inside the visible window. A reply to a message outside the window gets one `[quote id:…;user_id:…;time:…;] <first 120 chars>` line (media as placeholders, a forward expanded one level) so the model no longer needs `inspect_conversation_reference` for ordinary quotes; the quote is resolved by the ingress through the reference port (3 s timeout) and stored as structured `UserMessage.quote`. Tool arguments named `message_id` / `message_ids` / `*_message_id` / `reference` and `reply` segments accept the alias as well as the platform id; `inspect_conversation_reference` also accepts the quoted message id.
|
|
9
|
+
- 51f1328: 修复 icqq 群成员相关 Agent 工具:`group_member_info` 返回空对象(模型据此误判成员不在群里)、`group_invite` 永远判定"已在群里"、`set_mute` / `set_title` / `set_anonymous` 读旧缓存误判 Bot 不是管理员。统一改为实时查询群成员。
|
|
10
|
+
- d0e1fac: Keep stable prompt instructions ahead of turn-specific capabilities, project unchanged context without placeholder text, and retain scoped state snapshots with readable updates and compaction recovery. Preserve native conversation/tool chronology and separate participant identity from runtime guidance.
|
|
11
|
+
|
|
12
|
+
Resolve the ICQQ bot account and current-group role before capability discovery, and support explicitly declared current-conversation bot-role requirements without changing existing requester permissions or approvals. Keep role lookup failures unknown and preserve execution-time platform checks.
|
|
13
|
+
|
|
14
|
+
- 3771f7a: 修复同一群先后两人入群时后一次入群被当重复事件丢掉:side event 去重键(core 与 icqq)加入成员与操作人。
|
|
15
|
+
- 4f96ad5: Fix member-join welcome display: icqq now sends the mention's display name as the `at` element text, the joining member's nickname from the notice is threaded into `target.name`, and icqq group messages populate `conversation.name` from `group_name` so `{group}` renders the group name instead of the group number.
|
|
16
|
+
- Updated dependencies [f67883e]
|
|
17
|
+
- Updated dependencies [e23cabc]
|
|
18
|
+
- Updated dependencies [4d728c0]
|
|
19
|
+
- Updated dependencies [eb8feeb]
|
|
20
|
+
- Updated dependencies [24a2b71]
|
|
21
|
+
- Updated dependencies [d0e1fac]
|
|
22
|
+
- Updated dependencies [3771f7a]
|
|
23
|
+
- Updated dependencies [4f96ad5]
|
|
24
|
+
- @zhin.js/core@1.1.52
|
|
25
|
+
- @zhin.js/adapter@1.1.24
|
|
26
|
+
- @zhin.js/tool@1.1.11
|
|
27
|
+
- @zhin.js/im-contract@1.1.8
|
|
28
|
+
|
|
3
29
|
## 1.1.16
|
|
4
30
|
|
|
5
31
|
### Patch Changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
/**
|
|
3
|
+
* 实时查询群成员(跳过缓存);不在群里或查询失败返回 undefined。
|
|
4
|
+
* 不要用 `pickMember`:它总会返回实体对象(判空永远为真),`is_admin` 等也读旧缓存——Bot 刚被设为管理员时会误判。
|
|
5
|
+
*/
|
|
6
|
+
export async function freshMember(client, groupId, userId) {
|
|
7
|
+
try {
|
|
8
|
+
return await client.getGroupMemberInfo(groupId, userId, true);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function isGroupManager(info) {
|
|
15
|
+
return info?.role === 'owner' || info?.role === 'admin';
|
|
16
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Client, MemberInfo } from '@icqqjs/icqq';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 实时查询群成员(跳过缓存);不在群里或查询失败返回 undefined。
|
|
5
|
+
* 不要用 `pickMember`:它总会返回实体对象(判空永远为真),`is_admin` 等也读旧缓存——Bot 刚被设为管理员时会误判。
|
|
6
|
+
*/
|
|
7
|
+
export async function freshMember(client: Client, groupId: number, userId: number): Promise<MemberInfo | undefined> {
|
|
8
|
+
try {
|
|
9
|
+
return await client.getGroupMemberInfo(groupId, userId, true);
|
|
10
|
+
} catch {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function isGroupManager(info: MemberInfo | undefined): boolean {
|
|
16
|
+
return info?.role === 'owner' || info?.role === 'admin';
|
|
17
|
+
}
|
|
@@ -5,7 +5,7 @@ import { z } from 'zod';
|
|
|
5
5
|
export default defineAgentTool({
|
|
6
6
|
description: '设置或移除 QQ 群精华消息',
|
|
7
7
|
inputSchema: z.object({
|
|
8
|
-
message_id: z.string().describe('消息 ID'),
|
|
8
|
+
message_id: z.string().describe('消息 ID,或消息头里的 m<n> 别名'),
|
|
9
9
|
action: z.enum(['add', 'remove']).describe('add=设为精华, remove=移除精华'),
|
|
10
10
|
}),
|
|
11
11
|
adapter: 'icqq',
|
|
@@ -4,7 +4,7 @@ import { z } from 'zod';
|
|
|
4
4
|
export default defineAgentTool<{ message_id: string; action: 'add' | 'remove' }>({
|
|
5
5
|
description: '设置或移除 QQ 群精华消息',
|
|
6
6
|
inputSchema: z.object({
|
|
7
|
-
message_id: z.string().describe('消息 ID'),
|
|
7
|
+
message_id: z.string().describe('消息 ID,或消息头里的 m<n> 别名'),
|
|
8
8
|
action: z.enum(['add', 'remove']).describe('add=设为精华, remove=移除精华'),
|
|
9
9
|
}),
|
|
10
10
|
adapter: 'icqq',
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
2
|
import { messages as t } from "../../../../messages.js";
|
|
3
|
+
import { freshMember } from "../../../../members.js";
|
|
3
4
|
import { defineAgentTool } from '@zhin.js/tool';
|
|
4
5
|
import { z } from 'zod';
|
|
5
6
|
export default defineAgentTool({
|
|
@@ -16,8 +17,7 @@ export default defineAgentTool({
|
|
|
16
17
|
const groupInfo = client.pickGroup(group_id);
|
|
17
18
|
if (!groupInfo)
|
|
18
19
|
return { success: false, message: t(context.locale, 'groupMissing') };
|
|
19
|
-
|
|
20
|
-
if (memberInfo)
|
|
20
|
+
if (await freshMember(client, group_id, user_id))
|
|
21
21
|
return { success: false, message: t(context.locale, 'memberExists') };
|
|
22
22
|
await client.inviteFriend(group_id, user_id);
|
|
23
23
|
return { success: true, message: t(context.locale, 'inviteAttempted', { p0: user_id, p1: group_id }) };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { messages as t } from "../../../../messages.js";
|
|
2
|
+
import { freshMember } from "../../../../members.js";
|
|
2
3
|
import { defineAgentTool } from '@zhin.js/tool';
|
|
3
4
|
import { z } from 'zod';
|
|
4
5
|
export default defineAgentTool<{ group_id: number; user_id: number }>({
|
|
@@ -14,8 +15,7 @@ export default defineAgentTool<{ group_id: number; user_id: number }>({
|
|
|
14
15
|
const client = context.$client;
|
|
15
16
|
const groupInfo= client.pickGroup(group_id);
|
|
16
17
|
if(!groupInfo) return { success: false, message: t(context.locale, 'groupMissing') };
|
|
17
|
-
|
|
18
|
-
if(memberInfo) return { success: false, message: t(context.locale, 'memberExists') };
|
|
18
|
+
if (await freshMember(client, group_id, user_id)) return { success: false, message: t(context.locale, 'memberExists') };
|
|
19
19
|
await client.inviteFriend(group_id, user_id);
|
|
20
20
|
return { success: true, message: t(context.locale, 'inviteAttempted', { p0: user_id, p1: group_id }) };
|
|
21
21
|
},
|
|
@@ -7,6 +7,7 @@ export default defineAgentTool({
|
|
|
7
7
|
description: '让机器人退出当前 QQ 群;仅主人可发起且每次人工审批,机器人是群主时拒绝执行以免解散群',
|
|
8
8
|
adapter: 'icqq',
|
|
9
9
|
scopes: ['group'],
|
|
10
|
+
currentConversationBotRoles: ['admin', 'member'],
|
|
10
11
|
permissions: ['role(master)'],
|
|
11
12
|
requiresApproval: 'human',
|
|
12
13
|
risk: 'dangerous',
|
|
@@ -7,6 +7,7 @@ export default defineAgentTool({
|
|
|
7
7
|
description: '让机器人退出当前 QQ 群;仅主人可发起且每次人工审批,机器人是群主时拒绝执行以免解散群',
|
|
8
8
|
adapter: 'icqq',
|
|
9
9
|
scopes: ['group'],
|
|
10
|
+
currentConversationBotRoles: ['admin', 'member'],
|
|
10
11
|
permissions: ['role(master)'],
|
|
11
12
|
requiresApproval: 'human',
|
|
12
13
|
risk: 'dangerous',
|
|
@@ -7,10 +7,11 @@ export default defineAgentTool({
|
|
|
7
7
|
description: '撤回当前 QQ 群内他人的真实消息;需调用方具备群管发起权限及机器人具备实时平台权限,无需逐次审批',
|
|
8
8
|
adapter: 'icqq',
|
|
9
9
|
scopes: ['group'],
|
|
10
|
+
currentConversationBotRoles: ['owner', 'admin'],
|
|
10
11
|
permissions: ['role(master,admin,trusted,owner)'],
|
|
11
12
|
requiresApproval: 'never',
|
|
12
13
|
inputSchema: z.object({
|
|
13
|
-
message_id: z.string().min(1).describe('当前群中要撤回的真实消息 ID'),
|
|
14
|
+
message_id: z.string().min(1).describe('当前群中要撤回的真实消息 ID,或消息头里的 m<n> 别名'),
|
|
14
15
|
reason: z.string().min(1).describe('撤回的具体原因,自动处理必须依据已确认的群规'),
|
|
15
16
|
}),
|
|
16
17
|
async execute({ message_id }, context) {
|
|
@@ -7,10 +7,11 @@ export default defineAgentTool({
|
|
|
7
7
|
description: '撤回当前 QQ 群内他人的真实消息;需调用方具备群管发起权限及机器人具备实时平台权限,无需逐次审批',
|
|
8
8
|
adapter: 'icqq',
|
|
9
9
|
scopes: ['group'],
|
|
10
|
+
currentConversationBotRoles: ['owner', 'admin'],
|
|
10
11
|
permissions: ['role(master,admin,trusted,owner)'],
|
|
11
12
|
requiresApproval: 'never',
|
|
12
13
|
inputSchema: z.object({
|
|
13
|
-
message_id: z.string().min(1).describe('当前群中要撤回的真实消息 ID'),
|
|
14
|
+
message_id: z.string().min(1).describe('当前群中要撤回的真实消息 ID,或消息头里的 m<n> 别名'),
|
|
14
15
|
reason: z.string().min(1).describe('撤回的具体原因,自动处理必须依据已确认的群规'),
|
|
15
16
|
}),
|
|
16
17
|
async execute({ message_id }, context) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
2
|
import { messages as t } from "../../../../messages.js";
|
|
3
|
+
import { freshMember, isGroupManager } from "../../../../members.js";
|
|
3
4
|
import { defineAgentTool } from '@zhin.js/tool';
|
|
4
5
|
import { z } from 'zod';
|
|
5
6
|
export default defineAgentTool({
|
|
@@ -15,8 +16,7 @@ export default defineAgentTool({
|
|
|
15
16
|
async execute({ group_id, enable }, context) {
|
|
16
17
|
const client = context.$client;
|
|
17
18
|
const on = enable ?? true;
|
|
18
|
-
|
|
19
|
-
if (!selfInfo.is_admin && !selfInfo.is_owner)
|
|
19
|
+
if (!isGroupManager(await freshMember(client, group_id, client.uin)))
|
|
20
20
|
return { success: false, message: t(context.locale, 'anonymousForbidden') };
|
|
21
21
|
await client.setGroupAnonymous(group_id, on);
|
|
22
22
|
return { success: true, message: on ? t(context.locale, 'anonymousEnabled') : t(context.locale, 'anonymousDisabled') };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { messages as t } from "../../../../messages.js";
|
|
2
|
+
import { freshMember, isGroupManager } from "../../../../members.js";
|
|
2
3
|
import { defineAgentTool } from '@zhin.js/tool';
|
|
3
4
|
import { z } from 'zod';
|
|
4
5
|
export default defineAgentTool<{ group_id: number; enable?: boolean }>({
|
|
@@ -14,8 +15,7 @@ export default defineAgentTool<{ group_id: number; enable?: boolean }>({
|
|
|
14
15
|
async execute({ group_id, enable }, context) {
|
|
15
16
|
const client = context.$client;
|
|
16
17
|
const on = enable ?? true;
|
|
17
|
-
|
|
18
|
-
if(!selfInfo.is_admin && !selfInfo.is_owner) return { success: false, message: t(context.locale, 'anonymousForbidden') };
|
|
18
|
+
if (!isGroupManager(await freshMember(client, group_id, client.uin))) return { success: false, message: t(context.locale, 'anonymousForbidden') };
|
|
19
19
|
await client.setGroupAnonymous(group_id, on);
|
|
20
20
|
return { success: true, message: on ? t(context.locale, 'anonymousEnabled') : t(context.locale, 'anonymousDisabled') };
|
|
21
21
|
},
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
2
|
import { messages as t } from "../../../../messages.js";
|
|
3
|
+
import { freshMember, isGroupManager } from "../../../../members.js";
|
|
3
4
|
import { defineAgentTool } from '@zhin.js/tool';
|
|
4
5
|
import { z } from 'zod';
|
|
5
6
|
export default defineAgentTool({
|
|
@@ -14,11 +15,10 @@ export default defineAgentTool({
|
|
|
14
15
|
requiresApproval: 'always',
|
|
15
16
|
async execute({ group_id, user_id, duration }, context) {
|
|
16
17
|
const client = context.$client;
|
|
17
|
-
const
|
|
18
|
-
if (!
|
|
18
|
+
const [self, member] = await Promise.all([freshMember(client, group_id, client.uin), freshMember(client, group_id, user_id)]);
|
|
19
|
+
if (!isGroupManager(self))
|
|
19
20
|
return { success: false, message: t(context.locale, 'muteForbidden') };
|
|
20
|
-
|
|
21
|
-
if (!memberInfo)
|
|
21
|
+
if (!member)
|
|
22
22
|
return { success: false, message: t(context.locale, 'memberMissing') };
|
|
23
23
|
await client.setGroupBan(group_id, user_id, duration === 0 ? 0 : duration);
|
|
24
24
|
return { success: true, message: duration === 0 ? t(context.locale, 'unmuted', { user: user_id }) : t(context.locale, 'muted', { user: user_id, seconds: duration ?? 0 }) };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { messages as t } from "../../../../messages.js";
|
|
2
|
+
import { freshMember, isGroupManager } from "../../../../members.js";
|
|
2
3
|
import { defineAgentTool } from '@zhin.js/tool';
|
|
3
4
|
import { z } from 'zod';
|
|
4
5
|
export default defineAgentTool<{ group_id: number; user_id: number; duration?: number }>({
|
|
@@ -13,10 +14,9 @@ export default defineAgentTool<{ group_id: number; user_id: number; duration?: n
|
|
|
13
14
|
requiresApproval: 'always',
|
|
14
15
|
async execute({ group_id, user_id, duration }, context) {
|
|
15
16
|
const client = context.$client;
|
|
16
|
-
const
|
|
17
|
-
if(!
|
|
18
|
-
|
|
19
|
-
if(!memberInfo) return { success: false, message: t(context.locale, 'memberMissing') };
|
|
17
|
+
const [self, member] = await Promise.all([freshMember(client, group_id, client.uin), freshMember(client, group_id, user_id)]);
|
|
18
|
+
if (!isGroupManager(self)) return { success: false, message: t(context.locale, 'muteForbidden') };
|
|
19
|
+
if (!member) return { success: false, message: t(context.locale, 'memberMissing') };
|
|
20
20
|
await client.setGroupBan(group_id, user_id, duration === 0 ? 0 : duration);
|
|
21
21
|
return { success: true, message: duration === 0 ? t(context.locale, 'unmuted', { user: user_id }) : t(context.locale, 'muted', { user: user_id, seconds: duration ?? 0 }) };
|
|
22
22
|
},
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
2
|
import { messages as t } from "../../../../messages.js";
|
|
3
|
+
import { freshMember, isGroupManager } from "../../../../members.js";
|
|
3
4
|
import { defineAgentTool } from '@zhin.js/tool';
|
|
4
5
|
import { z } from 'zod';
|
|
5
6
|
export default defineAgentTool({
|
|
@@ -15,11 +16,10 @@ export default defineAgentTool({
|
|
|
15
16
|
requiresApproval: 'always',
|
|
16
17
|
async execute({ group_id, user_id, title, duration }, context) {
|
|
17
18
|
const client = context.$client;
|
|
18
|
-
const
|
|
19
|
-
if (!
|
|
19
|
+
const [self, member] = await Promise.all([freshMember(client, group_id, client.uin), freshMember(client, group_id, user_id)]);
|
|
20
|
+
if (!isGroupManager(self))
|
|
20
21
|
return { success: false, message: t(context.locale, 'titleForbidden') };
|
|
21
|
-
|
|
22
|
-
if (!memberInfo)
|
|
22
|
+
if (!member)
|
|
23
23
|
return { success: false, message: t(context.locale, 'memberMissing') };
|
|
24
24
|
await client.setGroupSpecialTitle(group_id, user_id, title, duration ?? -1);
|
|
25
25
|
return { success: true, message: t(context.locale, 'titleChanged', { p0: user_id, p1: title }) };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { messages as t } from "../../../../messages.js";
|
|
2
|
+
import { freshMember, isGroupManager } from "../../../../members.js";
|
|
2
3
|
import { defineAgentTool } from '@zhin.js/tool';
|
|
3
4
|
import { z } from 'zod';
|
|
4
5
|
export default defineAgentTool<{ group_id: number; user_id: number; title: string; duration?: number }>({
|
|
@@ -14,10 +15,9 @@ export default defineAgentTool<{ group_id: number; user_id: number; title: strin
|
|
|
14
15
|
requiresApproval: 'always',
|
|
15
16
|
async execute({ group_id, user_id, title, duration }, context) {
|
|
16
17
|
const client = context.$client;
|
|
17
|
-
const
|
|
18
|
-
if(!
|
|
19
|
-
|
|
20
|
-
if(!memberInfo) return { success: false, message: t(context.locale, 'memberMissing') };
|
|
18
|
+
const [self, member] = await Promise.all([freshMember(client, group_id, client.uin), freshMember(client, group_id, user_id)]);
|
|
19
|
+
if (!isGroupManager(self)) return { success: false, message: t(context.locale, 'titleForbidden') };
|
|
20
|
+
if (!member) return { success: false, message: t(context.locale, 'memberMissing') };
|
|
21
21
|
await client.setGroupSpecialTitle(group_id, user_id, title, duration ?? -1);
|
|
22
22
|
return { success: true, message: t(context.locale, 'titleChanged', { p0: user_id, p1: title }) };
|
|
23
23
|
},
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
2
|
import { messages as t } from "../../../../messages.js";
|
|
3
|
+
import { freshMember } from "../../../../members.js";
|
|
3
4
|
import { defineAgentTool } from '@zhin.js/tool';
|
|
4
5
|
import { z } from 'zod';
|
|
5
6
|
export default defineAgentTool({
|
|
@@ -13,15 +14,24 @@ export default defineAgentTool({
|
|
|
13
14
|
permissions: ['role(master,admin,trusted,owner)'],
|
|
14
15
|
requiresApproval: 'never',
|
|
15
16
|
async execute({ group_id, user_id }, context) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const memberInfo = group.pickMember(user_id);
|
|
20
|
-
if (!memberInfo)
|
|
17
|
+
// pickMember 返回的是实体对象,序列化后只剩 `{}`:模型会据此以为成员不在群里。按 group_kick 的口径实时查询。
|
|
18
|
+
const info = await freshMember(context.$client, group_id, user_id);
|
|
19
|
+
if (!info)
|
|
21
20
|
return { success: false, message: t(context.locale, 'memberMissing') };
|
|
22
21
|
return {
|
|
23
22
|
success: true,
|
|
24
|
-
member:
|
|
23
|
+
member: {
|
|
24
|
+
group_id: info.group_id,
|
|
25
|
+
user_id: info.user_id,
|
|
26
|
+
nickname: info.nickname,
|
|
27
|
+
card: info.card,
|
|
28
|
+
role: info.role,
|
|
29
|
+
title: info.title,
|
|
30
|
+
level: info.level,
|
|
31
|
+
join_time: info.join_time,
|
|
32
|
+
last_sent_time: info.last_sent_time,
|
|
33
|
+
shutup_time: info.shutup_time,
|
|
34
|
+
},
|
|
25
35
|
};
|
|
26
36
|
},
|
|
27
37
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { messages as t } from "../../../../messages.js";
|
|
2
|
+
import { freshMember } from "../../../../members.js";
|
|
2
3
|
import { defineAgentTool } from '@zhin.js/tool';
|
|
3
4
|
import { z } from 'zod';
|
|
4
5
|
interface GroupMemberInfoInput {
|
|
@@ -17,13 +18,23 @@ export default defineAgentTool<GroupMemberInfoInput>({
|
|
|
17
18
|
permissions: ['role(master,admin,trusted,owner)'],
|
|
18
19
|
requiresApproval: 'never',
|
|
19
20
|
async execute({ group_id, user_id }, context) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (!memberInfo) return { success: false, message: t(context.locale, 'memberMissing') };
|
|
21
|
+
// pickMember 返回的是实体对象,序列化后只剩 `{}`:模型会据此以为成员不在群里。按 group_kick 的口径实时查询。
|
|
22
|
+
const info = await freshMember(context.$client, group_id, user_id);
|
|
23
|
+
if (!info) return { success: false, message: t(context.locale, 'memberMissing') };
|
|
24
24
|
return {
|
|
25
25
|
success: true,
|
|
26
|
-
member:
|
|
26
|
+
member: {
|
|
27
|
+
group_id: info.group_id,
|
|
28
|
+
user_id: info.user_id,
|
|
29
|
+
nickname: info.nickname,
|
|
30
|
+
card: info.card,
|
|
31
|
+
role: info.role,
|
|
32
|
+
title: info.title,
|
|
33
|
+
level: info.level,
|
|
34
|
+
join_time: info.join_time,
|
|
35
|
+
last_sent_time: info.last_sent_time,
|
|
36
|
+
shutup_time: info.shutup_time,
|
|
37
|
+
},
|
|
27
38
|
};
|
|
28
39
|
},
|
|
29
40
|
});
|
|
@@ -8,7 +8,7 @@ export default defineAgentTool({
|
|
|
8
8
|
adapter: 'icqq',
|
|
9
9
|
scopes: ['private', 'group'],
|
|
10
10
|
requiresApproval: 'never',
|
|
11
|
-
inputSchema: z.object({ message_ids: z.array(z.string().min(1)).min(1).describe('当前会话的真实消息 ID
|
|
11
|
+
inputSchema: z.object({ message_ids: z.array(z.string().min(1)).min(1).describe('当前会话的真实消息 ID(或消息头里的 m<n> 别名),按转发顺序排列') }),
|
|
12
12
|
async execute({ message_ids }, context) {
|
|
13
13
|
const conversation = currentConversation(context);
|
|
14
14
|
if (conversation.kind === 'channel')
|
|
@@ -8,7 +8,7 @@ export default defineAgentTool({
|
|
|
8
8
|
adapter: 'icqq',
|
|
9
9
|
scopes: ['private', 'group'],
|
|
10
10
|
requiresApproval: 'never',
|
|
11
|
-
inputSchema: z.object({ message_ids: z.array(z.string().min(1)).min(1).describe('当前会话的真实消息 ID
|
|
11
|
+
inputSchema: z.object({ message_ids: z.array(z.string().min(1)).min(1).describe('当前会话的真实消息 ID(或消息头里的 m<n> 别名),按转发顺序排列') }),
|
|
12
12
|
async execute({ message_ids }, context) {
|
|
13
13
|
const conversation = currentConversation(context);
|
|
14
14
|
if (conversation.kind === 'channel') throw new Error(t(context.locale, 'channelOperationUnsupported'));
|
|
@@ -8,7 +8,7 @@ export default defineAgentTool({
|
|
|
8
8
|
scopes: ['private', 'group'],
|
|
9
9
|
requiresApproval: 'never',
|
|
10
10
|
inputSchema: z.object({
|
|
11
|
-
message_id: z.string().min(1).describe('当前会话中真实的原消息 ID'),
|
|
11
|
+
message_id: z.string().min(1).describe('当前会话中真实的原消息 ID,或消息头里的 m<n> 别名'),
|
|
12
12
|
text: z.string().min(1).describe('本次回复的文字'),
|
|
13
13
|
}),
|
|
14
14
|
async execute({ message_id, text }, context) {
|
|
@@ -8,7 +8,7 @@ export default defineAgentTool({
|
|
|
8
8
|
scopes: ['private', 'group'],
|
|
9
9
|
requiresApproval: 'never',
|
|
10
10
|
inputSchema: z.object({
|
|
11
|
-
message_id: z.string().min(1).describe('当前会话中真实的原消息 ID'),
|
|
11
|
+
message_id: z.string().min(1).describe('当前会话中真实的原消息 ID,或消息头里的 m<n> 别名'),
|
|
12
12
|
text: z.string().min(1).describe('本次回复的文字'),
|
|
13
13
|
}),
|
|
14
14
|
async execute({ message_id, text }, context) {
|
|
@@ -8,7 +8,7 @@ export default defineAgentTool({
|
|
|
8
8
|
adapter: 'icqq',
|
|
9
9
|
scopes: ['private', 'group'],
|
|
10
10
|
requiresApproval: 'never',
|
|
11
|
-
inputSchema: z.object({ message_id: z.string().min(1).describe('机器人在当前会话中发送的真实消息 ID') }),
|
|
11
|
+
inputSchema: z.object({ message_id: z.string().min(1).describe('机器人在当前会话中发送的真实消息 ID,或消息头里的 m<n> 别名') }),
|
|
12
12
|
async execute({ message_id }, context) {
|
|
13
13
|
const message = await resolveMessage(context, message_id);
|
|
14
14
|
if (message.sender.user_id !== context.$client.uin)
|
|
@@ -8,7 +8,7 @@ export default defineAgentTool({
|
|
|
8
8
|
adapter: 'icqq',
|
|
9
9
|
scopes: ['private', 'group'],
|
|
10
10
|
requiresApproval: 'never',
|
|
11
|
-
inputSchema: z.object({ message_id: z.string().min(1).describe('机器人在当前会话中发送的真实消息 ID') }),
|
|
11
|
+
inputSchema: z.object({ message_id: z.string().min(1).describe('机器人在当前会话中发送的真实消息 ID,或消息头里的 m<n> 别名') }),
|
|
12
12
|
async execute({ message_id }, context) {
|
|
13
13
|
const message = await resolveMessage(context, message_id);
|
|
14
14
|
if (message.sender.user_id !== context.$client.uin) return { success: false, message: t(context.locale, 'ownMessageRequired') };
|
package/agents/icqq/system.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# ICQQ Platform Agent
|
|
2
2
|
|
|
3
|
-
You are the icqq platform specialist. Interpret requests in the current icqq conversation and load only the narrowest
|
|
3
|
+
You are the icqq platform specialist. Interpret requests in the current icqq conversation and load only the narrowest available Skill needed for the task.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Use the current Bot snapshot for your platform role. Load `icqq-conversation` and query `conversation_info` when the role is unknown or you need more scene or permission details. Sender roles describe the requester, not you. Do not infer your role from old session summaries or another endpoint account.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Discover message and management operations from the current scene's Skill catalog; follow each tool's target, platform and approval rules. Recalling your own messages needs no group management role or per-call approval. Leaving a group requires fresh approval and is forbidden when you own the group. Do not load a Skill that is absent from the current catalog.
|
package/lib/endpoint.js
CHANGED
|
@@ -8,12 +8,13 @@ import { formatCompact, getAdapterLogger, truncatePreview } from '@zhin.js/logge
|
|
|
8
8
|
import { runIcqqLoginAssistStep } from './icqq-login-assist.js';
|
|
9
9
|
import { IcqqContentResolver } from './content-resolver.js';
|
|
10
10
|
import { InboundMessageDeduper, findIcqqNestedMessageSource, isIcqqBotMentioned, isIcqqMessagePostType, normalizeIcqqInboundMessage, messageLookupFromIcqqSource, resolveIcqqQuoteIdFromEvent, resolveIcqqInboundMedia, resolveIcqqSelfUserId, shouldSkipSelfInboundMessage, } from './icqq-inbound.js';
|
|
11
|
-
import { isIcqqNoticePayload, isIcqqRequestPayload, } from './icqq-inbox.js';
|
|
11
|
+
import { icqqNoticeDedupeKey, isIcqqNoticePayload, isIcqqRequestPayload, } from './icqq-inbox.js';
|
|
12
12
|
import { IcqqGuildCatalog, normalizeIcqqGuildInboundMessage, } from './icqq-guild.js';
|
|
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
16
|
import { inspectGroupMember, moderateGroupMember } from './group-moderation.js';
|
|
17
|
+
import { inspectGroupMemberProfile, inspectGroupScene } from './group-scene.js';
|
|
17
18
|
const BOUND_EVENTS = [
|
|
18
19
|
'message.private',
|
|
19
20
|
'message.group',
|
|
@@ -57,6 +58,8 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
57
58
|
content;
|
|
58
59
|
management = Object.freeze({
|
|
59
60
|
inspectGroupMember: (group, user, signal) => inspectGroupMember(this.client, group, user, signal),
|
|
61
|
+
inspectGroupScene: (group, signal) => inspectGroupScene(this.client, group, signal),
|
|
62
|
+
inspectGroupMemberProfile: (group, user, signal) => inspectGroupMemberProfile(this.client, group, user, signal),
|
|
60
63
|
moderateGroupMember: (group, user, action, signal) => moderateGroupMember(this.client, group, user, action, signal, () => this.#open && !this.#retiring && !this.#stopped),
|
|
61
64
|
listFriends: async () => Array.from(this.client.fl.values()).map((f) => ({
|
|
62
65
|
user_id: f.user_id,
|
|
@@ -743,6 +746,7 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
743
746
|
channelType: normalized.channelType,
|
|
744
747
|
channelId: normalized.channelId,
|
|
745
748
|
channelParentGroupId: normalized.channelParentGroupId,
|
|
749
|
+
name: normalized.groupName,
|
|
746
750
|
});
|
|
747
751
|
const quoteId = resolveIcqqQuoteIdFromEvent(data);
|
|
748
752
|
const quoted = messageLookupFromIcqqSource(findIcqqNestedMessageSource(data));
|
|
@@ -857,13 +861,7 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
857
861
|
#onNoticeEvent(payload) {
|
|
858
862
|
if (!payload || !isIcqqNoticePayload(payload))
|
|
859
863
|
return;
|
|
860
|
-
|
|
861
|
-
payload.time,
|
|
862
|
-
payload.operator_id ?? payload.user_id,
|
|
863
|
-
payload.notice_type ?? payload.type,
|
|
864
|
-
payload.sub_type,
|
|
865
|
-
].join('_');
|
|
866
|
-
if (!this.#sideEventDeduper.shouldProcess(`notice:${key}`))
|
|
864
|
+
if (!this.#sideEventDeduper.shouldProcess(icqqNoticeDedupeKey(payload)))
|
|
867
865
|
return;
|
|
868
866
|
this.#dispatchIcqqSideEvent(payload);
|
|
869
867
|
}
|
package/lib/group-moderation.js
CHANGED
|
@@ -3,17 +3,22 @@ export async function inspectGroupMember(client, groupId, userId, signal) {
|
|
|
3
3
|
signal.throwIfAborted();
|
|
4
4
|
const group = numericId(groupId);
|
|
5
5
|
const user = numericId(userId);
|
|
6
|
+
const selfLookup = client.getGroupMemberInfo(group, client.uin, true);
|
|
6
7
|
const [self, member] = await Promise.all([
|
|
7
|
-
|
|
8
|
-
client.getGroupMemberInfo(group, user, true),
|
|
8
|
+
selfLookup,
|
|
9
|
+
user === client.uin ? selfLookup : client.getGroupMemberInfo(group, user, true),
|
|
9
10
|
]);
|
|
10
11
|
signal.throwIfAborted();
|
|
11
12
|
const joinedAt = member.join_time * 1000;
|
|
13
|
+
const platformRole = member.user_id === user && member.group_id === group
|
|
14
|
+
&& (member.role === 'owner' || member.role === 'admin' || member.role === 'member')
|
|
15
|
+
? member.role : undefined;
|
|
12
16
|
return {
|
|
13
17
|
canModerate: client.isOnline() && user !== client.uin
|
|
14
18
|
&& self.user_id === client.uin && self.group_id === group
|
|
15
19
|
&& member.user_id === user && member.group_id === group
|
|
16
20
|
&& (self.role === 'owner' || self.role === 'admin') && member.role === 'member',
|
|
21
|
+
...(platformRole ? { platformRole } : {}),
|
|
17
22
|
...(Number.isSafeInteger(joinedAt) && joinedAt > 0 && joinedAt <= Date.now() ? { joinedAt } : {}),
|
|
18
23
|
};
|
|
19
24
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Client } from '@icqqjs/icqq';
|
|
2
|
+
import type { GroupMemberProfile, GroupSceneFacts } from 'zhin.js/adapter';
|
|
3
|
+
/** Listing a very large roster only to find admins is not worth the packet; adminIds stay unknown. */
|
|
4
|
+
export declare const ADMIN_SCAN_MAX_MEMBERS = 1000;
|
|
5
|
+
/**
|
|
6
|
+
* ADR 0041 scene facts: one uncached group-info read, plus (small groups only) one roster read for
|
|
7
|
+
* admins. A roster failure leaves adminIds unknown instead of failing the other facts.
|
|
8
|
+
*/
|
|
9
|
+
export declare function inspectGroupScene(client: Client, groupId: string, signal: AbortSignal): Promise<GroupSceneFacts>;
|
|
10
|
+
/** ADR 0041 member facts from one uncached lookup (same rationale as the agent `freshMember` helper). */
|
|
11
|
+
export declare function inspectGroupMemberProfile(client: Client, groupId: string, userId: string, signal: AbortSignal): Promise<GroupMemberProfile>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** Listing a very large roster only to find admins is not worth the packet; adminIds stay unknown. */
|
|
2
|
+
export const ADMIN_SCAN_MAX_MEMBERS = 1000;
|
|
3
|
+
const MAX_ADMIN_IDS = 20;
|
|
4
|
+
function positiveId(value) {
|
|
5
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? String(value) : undefined;
|
|
6
|
+
}
|
|
7
|
+
function numericId(value) {
|
|
8
|
+
const id = Number(value);
|
|
9
|
+
if (!/^[1-9]\d*$/.test(value) || !Number.isSafeInteger(id))
|
|
10
|
+
throw new Error('Invalid QQ identifier');
|
|
11
|
+
return id;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* ADR 0041 scene facts: one uncached group-info read, plus (small groups only) one roster read for
|
|
15
|
+
* admins. A roster failure leaves adminIds unknown instead of failing the other facts.
|
|
16
|
+
*/
|
|
17
|
+
export async function inspectGroupScene(client, groupId, signal) {
|
|
18
|
+
signal.throwIfAborted();
|
|
19
|
+
const group = numericId(groupId);
|
|
20
|
+
const info = await client.getGroupInfo(group, true);
|
|
21
|
+
signal.throwIfAborted();
|
|
22
|
+
if (info.group_id !== group)
|
|
23
|
+
return {};
|
|
24
|
+
const memberCount = Number.isSafeInteger(info.member_count) && info.member_count > 0 ? info.member_count : undefined;
|
|
25
|
+
const ownerId = positiveId(info.owner_id);
|
|
26
|
+
let adminIds;
|
|
27
|
+
if (memberCount === undefined || memberCount <= ADMIN_SCAN_MAX_MEMBERS) {
|
|
28
|
+
try {
|
|
29
|
+
const roster = await client.getGroupMemberList(group);
|
|
30
|
+
signal.throwIfAborted();
|
|
31
|
+
adminIds = [...roster.values()]
|
|
32
|
+
.filter(member => member.role === 'admin' && member.group_id === group)
|
|
33
|
+
.map(member => positiveId(member.user_id))
|
|
34
|
+
.filter((id) => id !== undefined)
|
|
35
|
+
.slice(0, MAX_ADMIN_IDS);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
signal.throwIfAborted();
|
|
39
|
+
adminIds = undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const name = typeof info.group_name === 'string' && info.group_name.trim() ? info.group_name.trim() : undefined;
|
|
43
|
+
const nowSeconds = Date.now() / 1000;
|
|
44
|
+
return {
|
|
45
|
+
...(name ? { name } : {}),
|
|
46
|
+
...(ownerId ? { ownerId } : {}),
|
|
47
|
+
...(adminIds ? { adminIds } : {}),
|
|
48
|
+
...(memberCount !== undefined ? { memberCount } : {}),
|
|
49
|
+
...(typeof info.shutup_time_whole === 'number' ? { muteAll: info.shutup_time_whole > nowSeconds } : {}),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** ADR 0041 member facts from one uncached lookup (same rationale as the agent `freshMember` helper). */
|
|
53
|
+
export async function inspectGroupMemberProfile(client, groupId, userId, signal) {
|
|
54
|
+
signal.throwIfAborted();
|
|
55
|
+
const group = numericId(groupId);
|
|
56
|
+
const user = numericId(userId);
|
|
57
|
+
const member = await client.getGroupMemberInfo(group, user, true);
|
|
58
|
+
signal.throwIfAborted();
|
|
59
|
+
if (member.user_id !== user || member.group_id !== group)
|
|
60
|
+
return {};
|
|
61
|
+
const card = typeof member.card === 'string' && member.card.trim() ? member.card.trim() : undefined;
|
|
62
|
+
const title = typeof member.title === 'string' && member.title.trim() ? member.title.trim() : undefined;
|
|
63
|
+
const shutupSeconds = member.shutup_time;
|
|
64
|
+
return {
|
|
65
|
+
...(member.role === 'owner' || member.role === 'admin' || member.role === 'member' ? { platformRole: member.role } : {}),
|
|
66
|
+
...(card ? { card } : {}),
|
|
67
|
+
...(title ? { title } : {}),
|
|
68
|
+
...(typeof shutupSeconds === 'number' && Number.isFinite(shutupSeconds)
|
|
69
|
+
? { mutedUntil: shutupSeconds * 1000 > Date.now() ? shutupSeconds * 1000 : 0 } : {}),
|
|
70
|
+
};
|
|
71
|
+
}
|
package/lib/icqq-inbound.d.ts
CHANGED
package/lib/icqq-inbound.js
CHANGED
|
@@ -688,6 +688,9 @@ export function normalizeIcqqInboundMessage(data) {
|
|
|
688
688
|
channelType,
|
|
689
689
|
channelId,
|
|
690
690
|
channelParentGroupId,
|
|
691
|
+
...(channelType === "group" && typeof ext.group_name === "string" && ext.group_name.trim()
|
|
692
|
+
? { groupName: ext.group_name.trim() }
|
|
693
|
+
: {}),
|
|
691
694
|
userId: String(userIdNum),
|
|
692
695
|
nickname,
|
|
693
696
|
senderRole,
|
package/lib/icqq-inbox.d.ts
CHANGED
|
@@ -8,6 +8,11 @@ export interface IcqqInboxBase {
|
|
|
8
8
|
export declare function isIcqqRequestPayload(payload: unknown): boolean;
|
|
9
9
|
/** post_type=notice / notice.* 判定。 */
|
|
10
10
|
export declare function isIcqqNoticePayload(payload: unknown): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* notice 去重键。成员与操作人分开记:同一管理员先后拉两个人进群时,`operator_id ?? user_id`
|
|
13
|
+
* 会撞键(icqq 群通知又常不带 time),后一次入群被当重复吞掉。
|
|
14
|
+
*/
|
|
15
|
+
export declare function icqqNoticeDedupeKey(payload: Record<string, unknown>): string;
|
|
11
16
|
/**
|
|
12
17
|
* request 事件载荷 → unified_inbox_request 行。
|
|
13
18
|
* platform_request_id 取 flag(审批回执句柄),缺省回退 seq;两者皆无则无法审批,丢弃。
|
package/lib/icqq-inbox.js
CHANGED
|
@@ -10,6 +10,20 @@ export function isIcqqNoticePayload(payload) {
|
|
|
10
10
|
return typeof postType === 'string'
|
|
11
11
|
&& (postType === 'notice' || postType.startsWith('notice.'));
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* notice 去重键。成员与操作人分开记:同一管理员先后拉两个人进群时,`operator_id ?? user_id`
|
|
15
|
+
* 会撞键(icqq 群通知又常不带 time),后一次入群被当重复吞掉。
|
|
16
|
+
*/
|
|
17
|
+
export function icqqNoticeDedupeKey(payload) {
|
|
18
|
+
return `notice:${[
|
|
19
|
+
payload.time,
|
|
20
|
+
payload.group_id,
|
|
21
|
+
payload.user_id,
|
|
22
|
+
payload.operator_id,
|
|
23
|
+
payload.notice_type ?? payload.type,
|
|
24
|
+
payload.sub_type,
|
|
25
|
+
].join('_')}`;
|
|
26
|
+
}
|
|
13
27
|
/**
|
|
14
28
|
* request 事件载荷 → unified_inbox_request 行。
|
|
15
29
|
* platform_request_id 取 flag(审批回执句柄),缺省回退 seq;两者皆无则无法审批,丢弃。
|
package/lib/protocol.d.ts
CHANGED
|
@@ -246,6 +246,8 @@ export declare function icqqInboundConversation(endpointKey: string, input: {
|
|
|
246
246
|
readonly channelId: string;
|
|
247
247
|
readonly channelParentGroupId?: string;
|
|
248
248
|
readonly guildId?: string;
|
|
249
|
+
/** 群名(仅 group 会话使用) */
|
|
250
|
+
readonly name?: string;
|
|
249
251
|
}): ConversationRef;
|
|
250
252
|
/**
|
|
251
253
|
* 出站:从 ConversationRef 派生 action 路由。
|
package/lib/protocol.js
CHANGED
|
@@ -232,7 +232,8 @@ export function icqqInboundConversation(endpointKey, input) {
|
|
|
232
232
|
parent: { kind: 'channel', id: input.guildId },
|
|
233
233
|
};
|
|
234
234
|
}
|
|
235
|
-
|
|
235
|
+
const name = input.channelType === 'group' ? input.name?.trim() : undefined;
|
|
236
|
+
return { endpoint, kind: input.channelType, id: input.channelId, ...(name ? { name } : {}) };
|
|
236
237
|
}
|
|
237
238
|
/**
|
|
238
239
|
* 出站:从 ConversationRef 派生 action 路由。
|
|
@@ -362,7 +363,8 @@ function formatOutboundElements(payload) {
|
|
|
362
363
|
if (!Number.isSafeInteger(qq) || qq <= 0) {
|
|
363
364
|
throw new TypeError(`Invalid ICQQ mention target: ${String(target ?? '')}`);
|
|
364
365
|
}
|
|
365
|
-
|
|
366
|
+
const name = typeof item.data?.name === 'string' ? item.data.name.trim() : '';
|
|
367
|
+
return name ? { type: 'at', qq, text: name.startsWith('@') ? name : `@${name}` } : { type: 'at', qq };
|
|
366
368
|
}
|
|
367
369
|
case 'image': {
|
|
368
370
|
const arg = resolveIcqqMediaFile(normalizeIcqqImageData(item.data));
|
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.17",
|
|
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.
|
|
36
|
+
"@zhin.js/adapter": "1.1.24",
|
|
37
37
|
"@zhin.js/agent-feature": "1.1.10",
|
|
38
|
-
"@zhin.js/core": "1.1.
|
|
38
|
+
"@zhin.js/core": "1.1.52",
|
|
39
39
|
"@zhin.js/feature-kit": "1.1.9",
|
|
40
|
-
"@zhin.js/im-contract": "1.1.
|
|
40
|
+
"@zhin.js/im-contract": "1.1.8",
|
|
41
41
|
"@zhin.js/logger": "1.1.2",
|
|
42
42
|
"@zhin.js/permission": "1.1.9",
|
|
43
43
|
"@zhin.js/skill": "1.1.10",
|
|
44
|
-
"@zhin.js/tool": "1.1.
|
|
44
|
+
"@zhin.js/tool": "1.1.11"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"@icqqjs/icqq": "^1.12.2",
|
|
48
48
|
"zod": "^4.0.0",
|
|
49
|
-
"@zhin.js/adapter": "^1.1.
|
|
49
|
+
"@zhin.js/adapter": "^1.1.24",
|
|
50
50
|
"@zhin.js/command": "^1.1.11",
|
|
51
|
-
"@zhin.js/core": "^1.1.
|
|
51
|
+
"@zhin.js/core": "^1.1.52",
|
|
52
52
|
"@zhin.js/prompt-section": "^1.1.9",
|
|
53
|
-
"zhin.js": "^1.1.
|
|
53
|
+
"zhin.js": "^1.1.17"
|
|
54
54
|
},
|
|
55
55
|
"peerDependenciesMeta": {
|
|
56
56
|
"@icqqjs/icqq": {
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"vitest": "^4.1.10",
|
|
77
77
|
"zod": "^4.4.3",
|
|
78
78
|
"@zhin.js/prompt-section": "1.1.9",
|
|
79
|
-
"zhin.js": "1.1.
|
|
79
|
+
"zhin.js": "1.1.17"
|
|
80
80
|
},
|
|
81
81
|
"files": [
|
|
82
82
|
"adapters",
|
package/src/endpoint.ts
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
type IcqqMessageEvent,
|
|
40
40
|
} from './icqq-inbound.js';
|
|
41
41
|
import {
|
|
42
|
+
icqqNoticeDedupeKey,
|
|
42
43
|
isIcqqNoticePayload,
|
|
43
44
|
isIcqqRequestPayload,
|
|
44
45
|
} from './icqq-inbox.js';
|
|
@@ -62,6 +63,7 @@ import {
|
|
|
62
63
|
import type { SystemMessage as IcqqSystemMessage } from './types.js';
|
|
63
64
|
import { ManagedIcqqClient, type IcqqBackgroundOperation } from './managed-client.js';
|
|
64
65
|
import { inspectGroupMember, moderateGroupMember } from './group-moderation.js';
|
|
66
|
+
import { inspectGroupMemberProfile, inspectGroupScene } from './group-scene.js';
|
|
65
67
|
|
|
66
68
|
export interface IcqqEndpointOptions {
|
|
67
69
|
readonly id: CapabilityId;
|
|
@@ -115,6 +117,8 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
115
117
|
|
|
116
118
|
readonly management: EndpointManagement = Object.freeze<EndpointManagement>({
|
|
117
119
|
inspectGroupMember: (group, user, signal) => inspectGroupMember(this.client, group, user, signal),
|
|
120
|
+
inspectGroupScene: (group, signal) => inspectGroupScene(this.client, group, signal),
|
|
121
|
+
inspectGroupMemberProfile: (group, user, signal) => inspectGroupMemberProfile(this.client, group, user, signal),
|
|
118
122
|
moderateGroupMember: (group, user, action, signal) => moderateGroupMember(
|
|
119
123
|
this.client, group, user, action, signal, () => this.#open && !this.#retiring && !this.#stopped,
|
|
120
124
|
),
|
|
@@ -823,6 +827,7 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
823
827
|
channelType: normalized.channelType,
|
|
824
828
|
channelId: normalized.channelId,
|
|
825
829
|
channelParentGroupId: normalized.channelParentGroupId,
|
|
830
|
+
name: normalized.groupName,
|
|
826
831
|
});
|
|
827
832
|
const quoteId = resolveIcqqQuoteIdFromEvent(data);
|
|
828
833
|
const quoted = messageLookupFromIcqqSource(findIcqqNestedMessageSource(data));
|
|
@@ -942,13 +947,7 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
942
947
|
|
|
943
948
|
#onNoticeEvent(payload: Record<string, unknown> | null): void {
|
|
944
949
|
if (!payload || !isIcqqNoticePayload(payload)) return;
|
|
945
|
-
|
|
946
|
-
payload.time,
|
|
947
|
-
payload.operator_id ?? payload.user_id,
|
|
948
|
-
payload.notice_type ?? payload.type,
|
|
949
|
-
payload.sub_type,
|
|
950
|
-
].join('_');
|
|
951
|
-
if (!this.#sideEventDeduper.shouldProcess(`notice:${key}`)) return;
|
|
950
|
+
if (!this.#sideEventDeduper.shouldProcess(icqqNoticeDedupeKey(payload))) return;
|
|
952
951
|
this.#dispatchIcqqSideEvent(payload);
|
|
953
952
|
}
|
|
954
953
|
|
package/src/group-moderation.ts
CHANGED
|
@@ -8,17 +8,22 @@ export async function inspectGroupMember(
|
|
|
8
8
|
signal.throwIfAborted();
|
|
9
9
|
const group = numericId(groupId);
|
|
10
10
|
const user = numericId(userId);
|
|
11
|
+
const selfLookup = client.getGroupMemberInfo(group, client.uin, true);
|
|
11
12
|
const [self, member] = await Promise.all([
|
|
12
|
-
|
|
13
|
-
client.getGroupMemberInfo(group, user, true),
|
|
13
|
+
selfLookup,
|
|
14
|
+
user === client.uin ? selfLookup : client.getGroupMemberInfo(group, user, true),
|
|
14
15
|
]);
|
|
15
16
|
signal.throwIfAborted();
|
|
16
17
|
const joinedAt = member.join_time * 1000;
|
|
18
|
+
const platformRole = member.user_id === user && member.group_id === group
|
|
19
|
+
&& (member.role === 'owner' || member.role === 'admin' || member.role === 'member')
|
|
20
|
+
? member.role : undefined;
|
|
17
21
|
return {
|
|
18
22
|
canModerate: client.isOnline() && user !== client.uin
|
|
19
23
|
&& self.user_id === client.uin && self.group_id === group
|
|
20
24
|
&& member.user_id === user && member.group_id === group
|
|
21
25
|
&& (self.role === 'owner' || self.role === 'admin') && member.role === 'member',
|
|
26
|
+
...(platformRole ? { platformRole } : {}),
|
|
22
27
|
...(Number.isSafeInteger(joinedAt) && joinedAt > 0 && joinedAt <= Date.now() ? { joinedAt } : {}),
|
|
23
28
|
};
|
|
24
29
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Client } from '@icqqjs/icqq';
|
|
2
|
+
import type { GroupMemberProfile, GroupSceneFacts } from 'zhin.js/adapter';
|
|
3
|
+
|
|
4
|
+
/** Listing a very large roster only to find admins is not worth the packet; adminIds stay unknown. */
|
|
5
|
+
export const ADMIN_SCAN_MAX_MEMBERS = 1000;
|
|
6
|
+
const MAX_ADMIN_IDS = 20;
|
|
7
|
+
|
|
8
|
+
function positiveId(value: unknown): string | undefined {
|
|
9
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? String(value) : undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function numericId(value: string): number {
|
|
13
|
+
const id = Number(value);
|
|
14
|
+
if (!/^[1-9]\d*$/.test(value) || !Number.isSafeInteger(id)) throw new Error('Invalid QQ identifier');
|
|
15
|
+
return id;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* ADR 0041 scene facts: one uncached group-info read, plus (small groups only) one roster read for
|
|
20
|
+
* admins. A roster failure leaves adminIds unknown instead of failing the other facts.
|
|
21
|
+
*/
|
|
22
|
+
export async function inspectGroupScene(client: Client, groupId: string, signal: AbortSignal): Promise<GroupSceneFacts> {
|
|
23
|
+
signal.throwIfAborted();
|
|
24
|
+
const group = numericId(groupId);
|
|
25
|
+
const info = await client.getGroupInfo(group, true);
|
|
26
|
+
signal.throwIfAborted();
|
|
27
|
+
if (info.group_id !== group) return {};
|
|
28
|
+
const memberCount = Number.isSafeInteger(info.member_count) && info.member_count > 0 ? info.member_count : undefined;
|
|
29
|
+
const ownerId = positiveId(info.owner_id);
|
|
30
|
+
let adminIds: string[] | undefined;
|
|
31
|
+
if (memberCount === undefined || memberCount <= ADMIN_SCAN_MAX_MEMBERS) {
|
|
32
|
+
try {
|
|
33
|
+
const roster = await client.getGroupMemberList(group);
|
|
34
|
+
signal.throwIfAborted();
|
|
35
|
+
adminIds = [...roster.values()]
|
|
36
|
+
.filter(member => member.role === 'admin' && member.group_id === group)
|
|
37
|
+
.map(member => positiveId(member.user_id))
|
|
38
|
+
.filter((id): id is string => id !== undefined)
|
|
39
|
+
.slice(0, MAX_ADMIN_IDS);
|
|
40
|
+
} catch {
|
|
41
|
+
signal.throwIfAborted();
|
|
42
|
+
adminIds = undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const name = typeof info.group_name === 'string' && info.group_name.trim() ? info.group_name.trim() : undefined;
|
|
46
|
+
const nowSeconds = Date.now() / 1000;
|
|
47
|
+
return {
|
|
48
|
+
...(name ? { name } : {}),
|
|
49
|
+
...(ownerId ? { ownerId } : {}),
|
|
50
|
+
...(adminIds ? { adminIds } : {}),
|
|
51
|
+
...(memberCount !== undefined ? { memberCount } : {}),
|
|
52
|
+
...(typeof info.shutup_time_whole === 'number' ? { muteAll: info.shutup_time_whole > nowSeconds } : {}),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** ADR 0041 member facts from one uncached lookup (same rationale as the agent `freshMember` helper). */
|
|
57
|
+
export async function inspectGroupMemberProfile(
|
|
58
|
+
client: Client, groupId: string, userId: string, signal: AbortSignal,
|
|
59
|
+
): Promise<GroupMemberProfile> {
|
|
60
|
+
signal.throwIfAborted();
|
|
61
|
+
const group = numericId(groupId);
|
|
62
|
+
const user = numericId(userId);
|
|
63
|
+
const member = await client.getGroupMemberInfo(group, user, true);
|
|
64
|
+
signal.throwIfAborted();
|
|
65
|
+
if (member.user_id !== user || member.group_id !== group) return {};
|
|
66
|
+
const card = typeof member.card === 'string' && member.card.trim() ? member.card.trim() : undefined;
|
|
67
|
+
const title = typeof member.title === 'string' && member.title.trim() ? member.title.trim() : undefined;
|
|
68
|
+
const shutupSeconds = member.shutup_time;
|
|
69
|
+
return {
|
|
70
|
+
...(member.role === 'owner' || member.role === 'admin' || member.role === 'member' ? { platformRole: member.role } : {}),
|
|
71
|
+
...(card ? { card } : {}),
|
|
72
|
+
...(title ? { title } : {}),
|
|
73
|
+
...(typeof shutupSeconds === 'number' && Number.isFinite(shutupSeconds)
|
|
74
|
+
? { mutedUntil: shutupSeconds * 1000 > Date.now() ? shutupSeconds * 1000 : 0 } : {}),
|
|
75
|
+
};
|
|
76
|
+
}
|
package/src/icqq-inbound.ts
CHANGED
|
@@ -129,6 +129,8 @@ export interface NormalizedIcqqInbound {
|
|
|
129
129
|
channelId: string;
|
|
130
130
|
/** 群临时会话:来源群号 */
|
|
131
131
|
channelParentGroupId?: string;
|
|
132
|
+
/** 群聊时来自 group_name */
|
|
133
|
+
groupName?: string;
|
|
132
134
|
userId: string;
|
|
133
135
|
nickname: string;
|
|
134
136
|
/** 群聊时来自 sender.role(私聊无) */
|
|
@@ -847,6 +849,9 @@ export function normalizeIcqqInboundMessage(
|
|
|
847
849
|
channelType,
|
|
848
850
|
channelId,
|
|
849
851
|
channelParentGroupId,
|
|
852
|
+
...(channelType === "group" && typeof ext.group_name === "string" && ext.group_name.trim()
|
|
853
|
+
? { groupName: ext.group_name.trim() }
|
|
854
|
+
: {}),
|
|
850
855
|
userId: String(userIdNum),
|
|
851
856
|
nickname,
|
|
852
857
|
senderRole,
|
package/src/icqq-inbox.ts
CHANGED
|
@@ -20,6 +20,21 @@ export function isIcqqNoticePayload(payload: unknown): boolean {
|
|
|
20
20
|
&& (postType === 'notice' || postType.startsWith('notice.'));
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* notice 去重键。成员与操作人分开记:同一管理员先后拉两个人进群时,`operator_id ?? user_id`
|
|
25
|
+
* 会撞键(icqq 群通知又常不带 time),后一次入群被当重复吞掉。
|
|
26
|
+
*/
|
|
27
|
+
export function icqqNoticeDedupeKey(payload: Record<string, unknown>): string {
|
|
28
|
+
return `notice:${[
|
|
29
|
+
payload.time,
|
|
30
|
+
payload.group_id,
|
|
31
|
+
payload.user_id,
|
|
32
|
+
payload.operator_id,
|
|
33
|
+
payload.notice_type ?? payload.type,
|
|
34
|
+
payload.sub_type,
|
|
35
|
+
].join('_')}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
23
38
|
/**
|
|
24
39
|
* request 事件载荷 → unified_inbox_request 行。
|
|
25
40
|
* platform_request_id 取 flag(审批回执句柄),缺省回退 seq;两者皆无则无法审批,丢弃。
|
package/src/protocol.ts
CHANGED
|
@@ -342,6 +342,8 @@ export function icqqInboundConversation(endpointKey: string, input: {
|
|
|
342
342
|
readonly channelId: string;
|
|
343
343
|
readonly channelParentGroupId?: string;
|
|
344
344
|
readonly guildId?: string;
|
|
345
|
+
/** 群名(仅 group 会话使用) */
|
|
346
|
+
readonly name?: string;
|
|
345
347
|
}): ConversationRef {
|
|
346
348
|
const endpoint = { id: endpointKey, adapter: ICQQ_ADAPTER_NAME };
|
|
347
349
|
if (input.channelType === 'private' && input.channelParentGroupId) {
|
|
@@ -360,7 +362,8 @@ export function icqqInboundConversation(endpointKey: string, input: {
|
|
|
360
362
|
parent: { kind: 'channel', id: input.guildId },
|
|
361
363
|
};
|
|
362
364
|
}
|
|
363
|
-
|
|
365
|
+
const name = input.channelType === 'group' ? input.name?.trim() : undefined;
|
|
366
|
+
return { endpoint, kind: input.channelType, id: input.channelId, ...(name ? { name } : {}) };
|
|
364
367
|
}
|
|
365
368
|
|
|
366
369
|
/**
|
|
@@ -487,7 +490,8 @@ function formatOutboundElements(payload: readonly unknown[]): (string | MessageE
|
|
|
487
490
|
if (!Number.isSafeInteger(qq) || qq <= 0) {
|
|
488
491
|
throw new TypeError(`Invalid ICQQ mention target: ${String(target ?? '')}`);
|
|
489
492
|
}
|
|
490
|
-
|
|
493
|
+
const name = typeof item.data?.name === 'string' ? item.data.name.trim() : '';
|
|
494
|
+
return name ? { type: 'at', qq, text: name.startsWith('@') ? name : `@${name}` } : { type: 'at', qq };
|
|
491
495
|
}
|
|
492
496
|
case 'image': {
|
|
493
497
|
const arg = resolveIcqqMediaFile(normalizeIcqqImageData(item.data));
|