@zhin.js/adapter-icqq 1.1.0 → 1.1.3
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 +107 -0
- package/README.md +15 -6
- package/adapters/{icqq.js → icqq/index.js} +6 -6
- package/adapters/{icqq.ts → icqq/index.ts} +10 -10
- package/agents/icqq/agent.json +20 -0
- package/agents/icqq/boundaries.md +3 -0
- package/agents/icqq/conventions.md +3 -0
- package/agents/icqq/skills/icqq-directory/SKILL.md +20 -0
- package/{tools/friend_list.js → agents/icqq/skills/icqq-directory/tools/friend_list/index.js} +2 -1
- package/{tools/friend_list.ts → agents/icqq/skills/icqq-directory/tools/friend_list/index.ts} +2 -1
- package/{tools/group_list.js → agents/icqq/skills/icqq-directory/tools/group_list/index.js} +2 -1
- package/{tools/group_list.ts → agents/icqq/skills/icqq-directory/tools/group_list/index.ts} +2 -1
- package/{tools/group_member_info.js → agents/icqq/skills/icqq-directory/tools/group_member_info/index.js} +3 -1
- package/{tools/group_member_info.ts → agents/icqq/skills/icqq-directory/tools/group_member_info/index.ts} +3 -1
- package/{tools/user_info.js → agents/icqq/skills/icqq-directory/tools/user_info/index.js} +1 -0
- package/{tools/user_info.ts → agents/icqq/skills/icqq-directory/tools/user_info/index.ts} +1 -0
- package/agents/icqq/skills/icqq-group-admin/SKILL.md +25 -0
- package/{tools/announce.js → agents/icqq/skills/icqq-group-admin/tools/announce/index.js} +2 -0
- package/{tools/announce.ts → agents/icqq/skills/icqq-group-admin/tools/announce/index.ts} +2 -0
- package/{tools/essence.js → agents/icqq/skills/icqq-group-admin/tools/essence/index.js} +3 -1
- package/{tools/essence.ts → agents/icqq/skills/icqq-group-admin/tools/essence/index.ts} +3 -1
- package/{tools/group_invite.js → agents/icqq/skills/icqq-group-admin/tools/group_invite/index.js} +1 -1
- package/{tools/group_invite.ts → agents/icqq/skills/icqq-group-admin/tools/group_invite/index.ts} +1 -1
- package/{tools/group_kick.js → agents/icqq/skills/icqq-group-admin/tools/group_kick/index.js} +4 -3
- package/{tools/group_kick.ts → agents/icqq/skills/icqq-group-admin/tools/group_kick/index.ts} +4 -3
- package/{tools/set_anonymous.js → agents/icqq/skills/icqq-group-admin/tools/set_anonymous/index.js} +3 -1
- package/{tools/set_anonymous.ts → agents/icqq/skills/icqq-group-admin/tools/set_anonymous/index.ts} +3 -1
- package/{tools/set_mute.js → agents/icqq/skills/icqq-group-admin/tools/set_mute/index.js} +3 -2
- package/{tools/set_mute.ts → agents/icqq/skills/icqq-group-admin/tools/set_mute/index.ts} +3 -2
- package/{tools/set_title.js → agents/icqq/skills/icqq-group-admin/tools/set_title/index.js} +3 -2
- package/{tools/set_title.ts → agents/icqq/skills/icqq-group-admin/tools/set_title/index.ts} +3 -2
- package/agents/icqq/skills/icqq-group-state/SKILL.md +17 -0
- package/{tools/group_files.js → agents/icqq/skills/icqq-group-state/tools/group_files/index.js} +3 -1
- package/{tools/group_files.ts → agents/icqq/skills/icqq-group-state/tools/group_files/index.ts} +3 -1
- package/{tools/list_muted.js → agents/icqq/skills/icqq-group-state/tools/list_muted/index.js} +2 -1
- package/{tools/list_muted.ts → agents/icqq/skills/icqq-group-state/tools/list_muted/index.ts} +2 -1
- package/agents/icqq/skills/icqq-interaction/SKILL.md +19 -0
- package/{tools/group_sign.js → agents/icqq/skills/icqq-interaction/tools/group_sign/index.js} +2 -1
- package/{tools/group_sign.ts → agents/icqq/skills/icqq-interaction/tools/group_sign/index.ts} +2 -1
- package/{tools/poke.js → agents/icqq/skills/icqq-interaction/tools/poke/index.js} +2 -1
- package/{tools/poke.ts → agents/icqq/skills/icqq-interaction/tools/poke/index.ts} +2 -1
- package/{tools/send_user_like.js → agents/icqq/skills/icqq-interaction/tools/send_user_like/index.js} +4 -3
- package/{tools/send_user_like.ts → agents/icqq/skills/icqq-interaction/tools/send_user_like/index.ts} +3 -3
- package/agents/icqq/system.md +3 -0
- package/commands/{endpoint/add/[[id]].js → icqq/endpoint/add/[[id]]/index.js} +1 -1
- package/commands/icqq/endpoint/add/[[id]]/index.ts +3 -0
- package/{lib/icqq-endpoint-commands.js → commands/icqq/endpoint/definition.js} +10 -5
- package/{src/icqq-endpoint-commands.ts → commands/icqq/endpoint/definition.ts} +9 -5
- package/commands/{endpoint/list.js → icqq/endpoint/list/index.js} +1 -1
- package/commands/icqq/endpoint/list/index.ts +3 -0
- package/commands/{endpoint/remove/[id].js → icqq/endpoint/remove/[id]/index.js} +1 -1
- package/commands/icqq/endpoint/remove/[id]/index.ts +3 -0
- package/lib/content-resolver.d.ts +10 -0
- package/lib/content-resolver.js +98 -0
- package/lib/cq-message.d.ts +1 -1
- package/lib/cq-message.js +1 -1
- package/lib/endpoint.d.ts +1 -2
- package/lib/endpoint.js +34 -77
- package/lib/icqq-inbound.js +40 -0
- package/lib/icqq-runtime-state.js +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/outbound-media.d.ts +1 -1
- package/lib/outbound-media.js +1 -1
- package/lib/protocol.d.ts +4 -22
- package/lib/protocol.js +52 -55
- package/lib/types.d.ts +0 -6
- package/package.json +29 -23
- package/plugin.js +1 -1
- package/prompt-sections/platform/index.js +16 -0
- package/{agent/prompt-sections/platform.ts → prompt-sections/platform/index.ts} +3 -4
- package/schema.json +7 -1
- package/src/content-resolver.ts +131 -0
- package/src/cq-message.ts +1 -1
- package/src/endpoint.ts +43 -97
- package/src/icqq-inbound.ts +39 -0
- package/src/icqq-runtime-state.ts +1 -1
- package/src/index.ts +1 -1
- package/src/outbound-media.ts +2 -2
- package/src/protocol.ts +56 -80
- package/src/types.ts +0 -7
- package/commands/endpoint/add/[[id]].ts +0 -3
- package/commands/endpoint/list.ts +0 -3
- package/commands/endpoint/remove/[id].ts +0 -3
- package/lib/icqq-endpoint-commands.d.ts +0 -1
- /package/{agent/PERMITS.md → PERMITS.md} +0 -0
package/{tools/group_files.js → agents/icqq/skills/icqq-group-state/tools/group_files/index.js}
RENAMED
|
@@ -7,7 +7,9 @@ export default defineAgentTool({
|
|
|
7
7
|
group_id: z.number().describe('群号'),
|
|
8
8
|
}),
|
|
9
9
|
adapter: 'icqq',
|
|
10
|
-
|
|
10
|
+
requiresApproval: 'once',
|
|
11
|
+
scopes: ['group'],
|
|
12
|
+
permissions: ['role(master,admin,trusted,owner)'],
|
|
11
13
|
async execute({ group_id }, context) {
|
|
12
14
|
const files = await context.$client.acquireGfs(group_id).ls('/');
|
|
13
15
|
if (!files.length)
|
package/{tools/group_files.ts → agents/icqq/skills/icqq-group-state/tools/group_files/index.ts}
RENAMED
|
@@ -6,7 +6,9 @@ export default defineAgentTool<{ group_id: number }>({
|
|
|
6
6
|
group_id: z.number().describe('群号'),
|
|
7
7
|
}),
|
|
8
8
|
adapter: 'icqq',
|
|
9
|
-
|
|
9
|
+
requiresApproval: 'once',
|
|
10
|
+
scopes:['group'],
|
|
11
|
+
permissions: ['role(master,admin,trusted,owner)'],
|
|
10
12
|
async execute({ group_id }, context) {
|
|
11
13
|
const files = await context.$client.acquireGfs(group_id).ls('/');
|
|
12
14
|
if (!files.length) return { files: [], message: '群文件为空' };
|
package/{tools/list_muted.js → agents/icqq/skills/icqq-group-state/tools/list_muted/index.js}
RENAMED
|
@@ -7,7 +7,8 @@ export default defineAgentTool({
|
|
|
7
7
|
group_id: z.number().describe('目标群号'),
|
|
8
8
|
}),
|
|
9
9
|
adapter: 'icqq',
|
|
10
|
-
|
|
10
|
+
scopes: ['group'],
|
|
11
|
+
requiresApproval: 'never',
|
|
11
12
|
async execute({ group_id }, context) {
|
|
12
13
|
const members = await context.$client.getGroupMemberList(group_id);
|
|
13
14
|
const now = Math.floor(Date.now() / 1000);
|
package/{tools/list_muted.ts → agents/icqq/skills/icqq-group-state/tools/list_muted/index.ts}
RENAMED
|
@@ -6,7 +6,8 @@ export default defineAgentTool<{ group_id: number }>({
|
|
|
6
6
|
group_id: z.number().describe('目标群号'),
|
|
7
7
|
}),
|
|
8
8
|
adapter: 'icqq',
|
|
9
|
-
|
|
9
|
+
scopes:['group'],
|
|
10
|
+
requiresApproval: 'never',
|
|
10
11
|
async execute({ group_id }, context) {
|
|
11
12
|
const members = await context.$client.getGroupMemberList(group_id);
|
|
12
13
|
const now = Math.floor(Date.now() / 1000);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: icqq-interaction
|
|
3
|
+
platforms:
|
|
4
|
+
- icqq
|
|
5
|
+
description: ICQQ 点赞、戳一戳和群签到互动能力。
|
|
6
|
+
keywords:
|
|
7
|
+
- "icqq"
|
|
8
|
+
- "点赞"
|
|
9
|
+
- "戳一戳"
|
|
10
|
+
- "签到"
|
|
11
|
+
tools:
|
|
12
|
+
- "poke"
|
|
13
|
+
- "send_user_like"
|
|
14
|
+
- "group_sign"
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# icqq-interaction
|
|
18
|
+
|
|
19
|
+
用于低风险社交互动。确认目标用户或群后再调用,并遵守各 Tool 的审批策略。
|
package/{tools/group_sign.js → agents/icqq/skills/icqq-interaction/tools/group_sign/index.js}
RENAMED
|
@@ -7,7 +7,8 @@ export default defineAgentTool({
|
|
|
7
7
|
group_id: z.number().describe('目标群号'),
|
|
8
8
|
}),
|
|
9
9
|
adapter: 'icqq',
|
|
10
|
-
|
|
10
|
+
scopes: ['group'],
|
|
11
|
+
requiresApproval: 'never',
|
|
11
12
|
async execute({ group_id }, context) {
|
|
12
13
|
const client = context.$client;
|
|
13
14
|
const groupInfo = client.pickGroup(group_id);
|
package/{tools/group_sign.ts → agents/icqq/skills/icqq-interaction/tools/group_sign/index.ts}
RENAMED
|
@@ -6,7 +6,8 @@ export default defineAgentTool<{ group_id: number }>({
|
|
|
6
6
|
group_id: z.number().describe('目标群号'),
|
|
7
7
|
}),
|
|
8
8
|
adapter: 'icqq',
|
|
9
|
-
|
|
9
|
+
scopes:['group'],
|
|
10
|
+
requiresApproval: 'never',
|
|
10
11
|
async execute({ group_id }, context) {
|
|
11
12
|
const client = context.$client;
|
|
12
13
|
const groupInfo= client.pickGroup(group_id);
|
|
@@ -8,7 +8,8 @@ export default defineAgentTool({
|
|
|
8
8
|
user_id: z.number().describe('要戳的目标成员 QQ号'),
|
|
9
9
|
}),
|
|
10
10
|
adapter: 'icqq',
|
|
11
|
-
|
|
11
|
+
scopes: ['group'],
|
|
12
|
+
requiresApproval: 'never',
|
|
12
13
|
async execute({ group_id, user_id }, context) {
|
|
13
14
|
await context.$client.sendGroupPoke(group_id, user_id);
|
|
14
15
|
return { success: true, message: `已戳了戳 ${user_id}` };
|
|
@@ -7,7 +7,8 @@ export default defineAgentTool<{ group_id: number; user_id: number }>({
|
|
|
7
7
|
user_id: z.number().describe('要戳的目标成员 QQ号'),
|
|
8
8
|
}),
|
|
9
9
|
adapter: 'icqq',
|
|
10
|
-
|
|
10
|
+
scopes:['group'],
|
|
11
|
+
requiresApproval: 'never',
|
|
11
12
|
async execute({ group_id, user_id }, context) {
|
|
12
13
|
await context.$client.sendGroupPoke(group_id, user_id);
|
|
13
14
|
return { success: true, message: `已戳了戳 ${user_id}` };
|
|
@@ -7,13 +7,14 @@ export default defineAgentTool({
|
|
|
7
7
|
user_id: z.number().describe('要点赞的目标用户 QQ号'),
|
|
8
8
|
}),
|
|
9
9
|
adapter: 'icqq',
|
|
10
|
-
|
|
10
|
+
requiresApproval: 'never',
|
|
11
11
|
async execute({ user_id }, context) {
|
|
12
12
|
const successArray = [];
|
|
13
13
|
await Promise.all([20, 20, 10].map(async (times) => {
|
|
14
14
|
try {
|
|
15
|
-
await context.$client.sendLike(user_id, times);
|
|
16
|
-
|
|
15
|
+
const isSuccess = await context.$client.sendLike(user_id, times);
|
|
16
|
+
if (isSuccess)
|
|
17
|
+
successArray.push(times);
|
|
17
18
|
}
|
|
18
19
|
catch { /* ignore */ }
|
|
19
20
|
}));
|
|
@@ -6,13 +6,13 @@ export default defineAgentTool<{ user_id: number }>({
|
|
|
6
6
|
user_id: z.number().describe('要点赞的目标用户 QQ号'),
|
|
7
7
|
}),
|
|
8
8
|
adapter: 'icqq',
|
|
9
|
-
|
|
9
|
+
requiresApproval: 'never',
|
|
10
10
|
async execute({ user_id }, context) {
|
|
11
11
|
const successArray: number[] = [];
|
|
12
12
|
await Promise.all([20, 20, 10].map(async (times) => {
|
|
13
13
|
try {
|
|
14
|
-
await context.$client.sendLike(user_id, times);
|
|
15
|
-
successArray.push(times);
|
|
14
|
+
const isSuccess= await context.$client.sendLike(user_id, times);
|
|
15
|
+
if (isSuccess) successArray.push(times);
|
|
16
16
|
} catch { /* ignore */ }
|
|
17
17
|
}));
|
|
18
18
|
const successTimes = successArray.reduce((a, b) => a + b, 0);
|
|
@@ -1,21 +1,26 @@
|
|
|
1
|
-
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { createEndpointCommands, endpointConfigurationStoreToken } from 'zhin.js/adapter';
|
|
2
3
|
import { defineCommand } from 'zhin.js/command';
|
|
3
|
-
import { icqqRuntimeStateToken } from
|
|
4
|
+
import { icqqRuntimeStateToken } from "../../../lib/icqq-runtime-state.js";
|
|
4
5
|
export const icqqEndpointCommands = createEndpointCommands({
|
|
5
6
|
adapterKey: 'icqq',
|
|
6
7
|
adapterDisplayName: 'ICQQ',
|
|
7
8
|
running: (use) => use(icqqRuntimeStateToken).endpoints.values(),
|
|
8
9
|
describeEntry: () => 'direct(直连 @icqqjs/icqq)',
|
|
9
10
|
addDescription: '登记 ICQQ endpoint(重启 zhin 生效)',
|
|
10
|
-
bindFlow: ({ id }) => {
|
|
11
|
+
bindFlow: async ({ id, use }) => {
|
|
11
12
|
if (!id) {
|
|
12
|
-
return '用法:icqq
|
|
13
|
+
return '用法:icqq endpoint add <uin>(uin 为纯数字 QQ 号)';
|
|
13
14
|
}
|
|
14
15
|
if (!/^\d+$/.test(id)) {
|
|
15
16
|
return 'icqq endpoint 名必须是纯数字 QQ 号(uin)';
|
|
16
17
|
}
|
|
17
18
|
try {
|
|
18
|
-
const filePath =
|
|
19
|
+
const { filePath } = await use(endpointConfigurationStoreToken).add({
|
|
20
|
+
adapterKey: 'icqq',
|
|
21
|
+
entry: { id },
|
|
22
|
+
environment: {},
|
|
23
|
+
});
|
|
19
24
|
return (`✅ endpoint「${id}」已追加到 ${filePath} 的 plugins.icqq.endpoints。\n` +
|
|
20
25
|
`重启 zhin 后生效。`);
|
|
21
26
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createEndpointCommands, endpointConfigurationStoreToken } from 'zhin.js/adapter';
|
|
2
2
|
import { defineCommand } from 'zhin.js/command';
|
|
3
|
-
import { icqqRuntimeStateToken } from '
|
|
3
|
+
import { icqqRuntimeStateToken } from '../../../src/icqq-runtime-state.js';
|
|
4
4
|
|
|
5
5
|
export const icqqEndpointCommands = createEndpointCommands({
|
|
6
6
|
adapterKey: 'icqq',
|
|
@@ -8,15 +8,19 @@ export const icqqEndpointCommands = createEndpointCommands({
|
|
|
8
8
|
running: (use) => use(icqqRuntimeStateToken).endpoints.values(),
|
|
9
9
|
describeEntry: () => 'direct(直连 @icqqjs/icqq)',
|
|
10
10
|
addDescription: '登记 ICQQ endpoint(重启 zhin 生效)',
|
|
11
|
-
bindFlow: ({ id }) => {
|
|
11
|
+
bindFlow: async ({ id, use }) => {
|
|
12
12
|
if (!id) {
|
|
13
|
-
return '用法:icqq
|
|
13
|
+
return '用法:icqq endpoint add <uin>(uin 为纯数字 QQ 号)';
|
|
14
14
|
}
|
|
15
15
|
if (!/^\d+$/.test(id)) {
|
|
16
16
|
return 'icqq endpoint 名必须是纯数字 QQ 号(uin)';
|
|
17
17
|
}
|
|
18
18
|
try {
|
|
19
|
-
const filePath =
|
|
19
|
+
const { filePath } = await use(endpointConfigurationStoreToken).add({
|
|
20
|
+
adapterKey: 'icqq',
|
|
21
|
+
entry: { id },
|
|
22
|
+
environment: {},
|
|
23
|
+
});
|
|
20
24
|
return (
|
|
21
25
|
`✅ endpoint「${id}」已追加到 ${filePath} 的 plugins.icqq.endpoints。\n` +
|
|
22
26
|
`重启 zhin 后生效。`
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Client } from '@icqqjs/icqq';
|
|
2
|
+
import type { EndpointContentPort } from 'zhin.js/adapter';
|
|
3
|
+
import type { ConversationMessage } from '@zhin.js/im-contract';
|
|
4
|
+
/** Owns ICQQ-observed message content and recursive merged-forward expansion. */
|
|
5
|
+
export declare class IcqqContentResolver {
|
|
6
|
+
#private;
|
|
7
|
+
readonly port: EndpointContentPort;
|
|
8
|
+
constructor(client: Client);
|
|
9
|
+
remember(messageId: string, message: ConversationMessage): void;
|
|
10
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { normalizeForwardMsgResponse } from './forward-msg.js';
|
|
2
|
+
/** Owns ICQQ-observed message content and recursive merged-forward expansion. */
|
|
3
|
+
export class IcqqContentResolver {
|
|
4
|
+
#client;
|
|
5
|
+
#messages = new Map();
|
|
6
|
+
port = Object.freeze({
|
|
7
|
+
resolve: (reference, context) => this.#resolve(reference, context),
|
|
8
|
+
});
|
|
9
|
+
constructor(client) {
|
|
10
|
+
this.#client = client;
|
|
11
|
+
}
|
|
12
|
+
remember(messageId, message) {
|
|
13
|
+
this.#messages.set(messageId, message);
|
|
14
|
+
}
|
|
15
|
+
async #resolve(reference, context) {
|
|
16
|
+
context.signal.throwIfAborted();
|
|
17
|
+
if (reference.kind === 'message') {
|
|
18
|
+
const cached = this.#messages.get(reference.message.id);
|
|
19
|
+
return cached
|
|
20
|
+
? Object.freeze({ status: 'resolved', reference, value: cached })
|
|
21
|
+
: Object.freeze({ status: 'not_found', code: 'icqq_message_not_observed' });
|
|
22
|
+
}
|
|
23
|
+
if (reference.kind === 'forward') {
|
|
24
|
+
try {
|
|
25
|
+
const entries = await this.#resolveForwardEntries(reference.forwardId, context, { remainingEntries: context.maxEntries, path: new Set() }, 0);
|
|
26
|
+
if (entries.length === 0) {
|
|
27
|
+
return Object.freeze({ status: 'not_found', code: 'icqq_forward_not_found' });
|
|
28
|
+
}
|
|
29
|
+
return Object.freeze({
|
|
30
|
+
status: 'resolved',
|
|
31
|
+
reference,
|
|
32
|
+
value: Object.freeze(entries),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (context.signal.aborted)
|
|
37
|
+
throw error;
|
|
38
|
+
return Object.freeze({
|
|
39
|
+
status: 'failed',
|
|
40
|
+
code: 'icqq_forward_fetch_failed',
|
|
41
|
+
message: error instanceof Error ? error.message : String(error),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (reference.media.kind === 'url'
|
|
46
|
+
|| reference.media.kind === 'base64'
|
|
47
|
+
|| reference.media.kind === 'path') {
|
|
48
|
+
return Object.freeze({ status: 'resolved', reference, value: reference.media });
|
|
49
|
+
}
|
|
50
|
+
return Object.freeze({
|
|
51
|
+
status: 'unsupported',
|
|
52
|
+
code: 'icqq_media_reference_unsupported',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
async #resolveForwardEntries(forwardId, context, state, depth) {
|
|
56
|
+
context.signal.throwIfAborted();
|
|
57
|
+
if (state.remainingEntries <= 0 || state.path.has(forwardId))
|
|
58
|
+
return Object.freeze([]);
|
|
59
|
+
state.path.add(forwardId);
|
|
60
|
+
try {
|
|
61
|
+
// ICQQ has no cancellable getForwardMsg API. Keep the generation lease
|
|
62
|
+
// until the native operation settles, then honor cancellation.
|
|
63
|
+
const raw = await this.#client.getForwardMsg(forwardId);
|
|
64
|
+
context.signal.throwIfAborted();
|
|
65
|
+
const entries = normalizeForwardMsgResponse(raw).slice(0, state.remainingEntries);
|
|
66
|
+
state.remainingEntries -= entries.length;
|
|
67
|
+
const expanded = [];
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
context.signal.throwIfAborted();
|
|
70
|
+
const segments = [];
|
|
71
|
+
for (const segment of entry.segments) {
|
|
72
|
+
if (segment.type !== 'forward' || depth >= context.maxDepth) {
|
|
73
|
+
segments.push(segment);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const nestedId = String(segment.data.forward_id ?? '').trim();
|
|
77
|
+
if (!nestedId || state.path.has(nestedId) || state.remainingEntries <= 0) {
|
|
78
|
+
segments.push(segment);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const nested = await this.#resolveForwardEntries(nestedId, context, state, depth + 1);
|
|
82
|
+
segments.push(Object.freeze({
|
|
83
|
+
...segment,
|
|
84
|
+
data: Object.freeze({
|
|
85
|
+
...segment.data,
|
|
86
|
+
...(nested.length > 0 ? { entries: nested } : {}),
|
|
87
|
+
}),
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
expanded.push(Object.freeze({ ...entry, segments: Object.freeze(segments) }));
|
|
91
|
+
}
|
|
92
|
+
return Object.freeze(expanded);
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
state.path.delete(forwardId);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
package/lib/cq-message.d.ts
CHANGED
package/lib/cq-message.js
CHANGED
package/lib/endpoint.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Client } from '@icqqjs/icqq';
|
|
2
|
-
import { Endpoint } from 'zhin.js/adapter';
|
|
3
|
-
import type { EndpointControl, EndpointManagement, EndpointSendRequest } from 'zhin.js/adapter';
|
|
2
|
+
import { Endpoint, type EndpointControl, type EndpointManagement, type EndpointSendRequest } from 'zhin.js/adapter';
|
|
4
3
|
import type { EndpointContentPort } from '@zhin.js/adapter';
|
|
5
4
|
import { type LoginAssist } from '@zhin.js/core';
|
|
6
5
|
import type { CapabilityId } from 'zhin.js';
|
package/lib/endpoint.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { Client, parseGroupMessageId, } from '@icqqjs/icqq';
|
|
2
2
|
import { inspect } from 'node:util';
|
|
3
3
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
4
|
-
import { Endpoint } from 'zhin.js/adapter';
|
|
4
|
+
import { Endpoint, } from 'zhin.js/adapter';
|
|
5
5
|
import { receiveOneBotLikeSideEvent, SystemEvent, toCanonicalSegments } from '@zhin.js/core';
|
|
6
6
|
import { formatCompact, getAdapterLogger, truncatePreview } from '@zhin.js/logger';
|
|
7
7
|
import { runIcqqLoginAssistStep } from './icqq-login-assist.js';
|
|
8
|
+
import { IcqqContentResolver } from './content-resolver.js';
|
|
8
9
|
import { InboundMessageDeduper, findIcqqNestedMessageSource, isIcqqBotMentioned, isIcqqMessagePostType, normalizeIcqqInboundMessage, messageLookupFromIcqqSource, resolveIcqqQuoteIdFromEvent, resolveIcqqInboundMedia, shouldSkipSelfInboundMessage, } from './icqq-inbound.js';
|
|
9
10
|
import { isIcqqNoticePayload, isIcqqRequestPayload, } from './icqq-inbox.js';
|
|
10
11
|
import { IcqqGuildCatalog, normalizeIcqqGuildInboundMessage, } from './icqq-guild.js';
|
|
11
12
|
import { prepareIcqqOutboundMedia, resolveIcqqOutboundMediaMode, } from './outbound-media.js';
|
|
12
13
|
import { formatInboundContent, formatOutboundBody, icqqInboundConversation, icqqOutboundTarget, } from './protocol.js';
|
|
13
|
-
import { normalizeForwardMsgResponse } from './forward-msg.js';
|
|
14
14
|
const BOUND_EVENTS = [
|
|
15
15
|
'message.private',
|
|
16
16
|
'message.group',
|
|
@@ -56,80 +56,8 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
56
56
|
#heldInbound = [];
|
|
57
57
|
#inflightInbound = new Set();
|
|
58
58
|
#inboundOwner = new AsyncLocalStorage();
|
|
59
|
-
#
|
|
60
|
-
content
|
|
61
|
-
resolve: async (reference, context) => {
|
|
62
|
-
context.signal.throwIfAborted();
|
|
63
|
-
if (reference.kind === 'message') {
|
|
64
|
-
const cached = this.#messageContent.get(reference.message.id);
|
|
65
|
-
return cached
|
|
66
|
-
? Object.freeze({ status: 'resolved', reference, value: cached })
|
|
67
|
-
: Object.freeze({ status: 'not_found', code: 'icqq_message_not_observed' });
|
|
68
|
-
}
|
|
69
|
-
if (reference.kind === 'forward') {
|
|
70
|
-
try {
|
|
71
|
-
const entries = await this.#resolveForwardEntries(reference.forwardId, context, { remainingEntries: context.maxEntries, path: new Set() }, 0);
|
|
72
|
-
if (entries.length === 0)
|
|
73
|
-
return Object.freeze({ status: 'not_found', code: 'icqq_forward_not_found' });
|
|
74
|
-
return Object.freeze({ status: 'resolved', reference, value: Object.freeze(entries) });
|
|
75
|
-
}
|
|
76
|
-
catch (error) {
|
|
77
|
-
if (context.signal.aborted)
|
|
78
|
-
throw error;
|
|
79
|
-
return Object.freeze({
|
|
80
|
-
status: 'failed',
|
|
81
|
-
code: 'icqq_forward_fetch_failed',
|
|
82
|
-
message: error instanceof Error ? error.message : String(error),
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
if (reference.media.kind === 'url' || reference.media.kind === 'base64' || reference.media.kind === 'path') {
|
|
87
|
-
return Object.freeze({ status: 'resolved', reference, value: reference.media });
|
|
88
|
-
}
|
|
89
|
-
return Object.freeze({ status: 'unsupported', code: 'icqq_media_reference_unsupported' });
|
|
90
|
-
},
|
|
91
|
-
});
|
|
92
|
-
async #resolveForwardEntries(forwardId, context, state, depth) {
|
|
93
|
-
context.signal.throwIfAborted();
|
|
94
|
-
if (state.remainingEntries <= 0 || state.path.has(forwardId))
|
|
95
|
-
return Object.freeze([]);
|
|
96
|
-
state.path.add(forwardId);
|
|
97
|
-
try {
|
|
98
|
-
// ICQQ does not expose a cancellable getForwardMsg API. Keep the caller's
|
|
99
|
-
// generation lease until the native operation actually settles, then honor
|
|
100
|
-
// cancellation before publishing any resolved content.
|
|
101
|
-
const raw = await this.client.getForwardMsg(forwardId);
|
|
102
|
-
context.signal.throwIfAborted();
|
|
103
|
-
const entries = normalizeForwardMsgResponse(raw).slice(0, state.remainingEntries);
|
|
104
|
-
state.remainingEntries -= entries.length;
|
|
105
|
-
const expanded = [];
|
|
106
|
-
for (const entry of entries) {
|
|
107
|
-
context.signal.throwIfAborted();
|
|
108
|
-
const segments = [];
|
|
109
|
-
for (const segment of entry.segments) {
|
|
110
|
-
if (segment.type !== 'forward' || depth >= context.maxDepth) {
|
|
111
|
-
segments.push(segment);
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
const nestedId = String(segment.data.forward_id ?? '').trim();
|
|
115
|
-
if (!nestedId || state.path.has(nestedId) || state.remainingEntries <= 0) {
|
|
116
|
-
segments.push(segment);
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
const nested = await this.#resolveForwardEntries(nestedId, context, state, depth + 1);
|
|
120
|
-
segments.push(Object.freeze({
|
|
121
|
-
...segment,
|
|
122
|
-
data: Object.freeze({ ...segment.data, ...(nested.length > 0 ? { entries: nested } : {}) }),
|
|
123
|
-
}));
|
|
124
|
-
}
|
|
125
|
-
expanded.push(Object.freeze({ ...entry, segments: Object.freeze(segments) }));
|
|
126
|
-
}
|
|
127
|
-
return Object.freeze(expanded);
|
|
128
|
-
}
|
|
129
|
-
finally {
|
|
130
|
-
state.path.delete(forwardId);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
59
|
+
#contentResolver;
|
|
60
|
+
content;
|
|
133
61
|
management = Object.freeze({
|
|
134
62
|
listFriends: async () => Array.from(this.client.fl.values()).map((f) => ({
|
|
135
63
|
user_id: f.user_id,
|
|
@@ -202,6 +130,8 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
202
130
|
super();
|
|
203
131
|
const nativeConfig = resolveNativeClientConfig(options.config);
|
|
204
132
|
this.client = new ManagedIcqqClient(Number(options.config.id), nativeConfig);
|
|
133
|
+
this.#contentResolver = new IcqqContentResolver(this.client);
|
|
134
|
+
this.content = this.#contentResolver.port;
|
|
205
135
|
this.#logger = getAdapterLogger('icqq', options.config.id);
|
|
206
136
|
this.#options = options;
|
|
207
137
|
this.endpointName = options.config.id;
|
|
@@ -360,6 +290,27 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
360
290
|
}
|
|
361
291
|
}
|
|
362
292
|
async #sendNative(target, message) {
|
|
293
|
+
if (isIcqqShareElement(message)) {
|
|
294
|
+
switch (target.kind) {
|
|
295
|
+
case 'private':
|
|
296
|
+
await this.client.pickFriend(target.userId).share(message);
|
|
297
|
+
return {};
|
|
298
|
+
case 'group':
|
|
299
|
+
await this.client.pickGroup(target.groupId).share(message);
|
|
300
|
+
return {};
|
|
301
|
+
case 'temp':
|
|
302
|
+
await this.client.pickMember(target.groupId, target.userId).share(message);
|
|
303
|
+
return {};
|
|
304
|
+
case 'channel': {
|
|
305
|
+
const channel = this.client.pickGuild(target.guildId).channels.get(target.channelId);
|
|
306
|
+
if (!channel) {
|
|
307
|
+
throw new TypeError(`ICQQ guild channel is unavailable: ${target.guildId}/${target.channelId}`);
|
|
308
|
+
}
|
|
309
|
+
await channel.share(message);
|
|
310
|
+
return {};
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
363
314
|
if (isIcqqFileElement(message)) {
|
|
364
315
|
switch (target.kind) {
|
|
365
316
|
case 'private': {
|
|
@@ -708,7 +659,7 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
708
659
|
const quoteId = resolveIcqqQuoteIdFromEvent(data);
|
|
709
660
|
const quoted = messageLookupFromIcqqSource(findIcqqNestedMessageSource(data));
|
|
710
661
|
if (quoteId && quoted) {
|
|
711
|
-
this.#
|
|
662
|
+
this.#contentResolver.remember(quoteId, Object.freeze({
|
|
712
663
|
ref: Object.freeze({ conversation, id: quoteId }),
|
|
713
664
|
...(quoted.sender?.id ? { actor: Object.freeze({
|
|
714
665
|
id: quoted.sender.id,
|
|
@@ -919,6 +870,12 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
919
870
|
}
|
|
920
871
|
}
|
|
921
872
|
}
|
|
873
|
+
function isIcqqShareElement(message) {
|
|
874
|
+
return !Array.isArray(message)
|
|
875
|
+
&& typeof message === 'object'
|
|
876
|
+
&& message !== null
|
|
877
|
+
&& message.type === 'share';
|
|
878
|
+
}
|
|
922
879
|
function formatIcqqSendablePreview(message) {
|
|
923
880
|
if (typeof message === 'string')
|
|
924
881
|
return message;
|
package/lib/icqq-inbound.js
CHANGED
|
@@ -360,6 +360,46 @@ export function icqqElementsToSegments(elements) {
|
|
|
360
360
|
});
|
|
361
361
|
break;
|
|
362
362
|
}
|
|
363
|
+
case "share": {
|
|
364
|
+
const url = typeof el.url === "string" ? el.url.trim() : "";
|
|
365
|
+
const title = typeof el.title === "string" ? el.title.trim() : "";
|
|
366
|
+
if (!url || !title)
|
|
367
|
+
break;
|
|
368
|
+
const description = typeof el.summary === "string" && el.summary
|
|
369
|
+
? el.summary
|
|
370
|
+
: undefined;
|
|
371
|
+
const image = typeof el.image === "string" && el.image ? el.image : undefined;
|
|
372
|
+
const audio = typeof el.audio === "string" && el.audio ? el.audio : undefined;
|
|
373
|
+
const content = typeof el.content === "string" && el.content ? el.content : undefined;
|
|
374
|
+
const rawConfig = el.config && typeof el.config === "object" && !Array.isArray(el.config)
|
|
375
|
+
? el.config
|
|
376
|
+
: undefined;
|
|
377
|
+
const appid = Number(rawConfig?.appid);
|
|
378
|
+
const config = Number.isSafeInteger(appid) && appid > 0
|
|
379
|
+
? {
|
|
380
|
+
appid,
|
|
381
|
+
...(typeof rawConfig?.appname === "string" && rawConfig.appname
|
|
382
|
+
? { package: rawConfig.appname }
|
|
383
|
+
: {}),
|
|
384
|
+
...(typeof rawConfig?.appsign === "string" && rawConfig.appsign
|
|
385
|
+
? { sign: rawConfig.appsign }
|
|
386
|
+
: {}),
|
|
387
|
+
}
|
|
388
|
+
: undefined;
|
|
389
|
+
out.push({
|
|
390
|
+
type: "share",
|
|
391
|
+
data: {
|
|
392
|
+
url,
|
|
393
|
+
title,
|
|
394
|
+
...(description ? { description } : {}),
|
|
395
|
+
...(image ? { image } : {}),
|
|
396
|
+
...(audio ? { audio } : {}),
|
|
397
|
+
...(content ? { content } : {}),
|
|
398
|
+
...(config ? { config } : {}),
|
|
399
|
+
},
|
|
400
|
+
});
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
363
403
|
default:
|
|
364
404
|
if (el.text != null && el.text !== "") {
|
|
365
405
|
out.push({ type: "text", data: { text: String(el.text) } });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ICQQ 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
|
|
3
|
-
* 由 plugin.ts setup() provide,adapter create 与 `icqq
|
|
3
|
+
* 由 plugin.ts setup() provide,adapter create 与 `icqq endpoint` 命令共享(同一 owner generation)。
|
|
4
4
|
*/
|
|
5
5
|
import { defineEndpointRuntimeStateToken } from 'zhin.js/adapter';
|
|
6
6
|
export const icqqRuntimeStateToken = defineEndpointRuntimeStateToken('icqq');
|
package/lib/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { Actions, formatInboundContent, formatOutboundBody, icqqInboundConversation, icqqOutboundTarget, type ActionResult, type
|
|
1
|
+
export { Actions, formatInboundContent, formatOutboundBody, icqqInboundConversation, icqqOutboundTarget, type ActionResult, type IcqqEndpointConfig, type IcqqInboundMessage, type IcqqWireSegment, type ParsedIcqqSendTarget, type ResolvedIcqqConfig, } from './protocol.js';
|
|
2
2
|
export { IcqqEndpoint, type IcqqEndpointOptions, } from './endpoint.js';
|
|
3
3
|
export * from './types.js';
|
|
4
4
|
export { icqqClient, type IcqqClient, type IcqqClientEventMap, } from './client.js';
|
package/lib/outbound-media.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SendContent } from "zhin.js";
|
|
2
|
-
import type { IcqqEndpointConfig } from "./
|
|
2
|
+
import type { IcqqEndpointConfig } from "./protocol.js";
|
|
3
3
|
export type IcqqOutboundMediaMode = "file" | "base64";
|
|
4
4
|
export declare function resolveIcqqOutboundMediaMode(config: Pick<IcqqEndpointConfig, "outboundMedia">): IcqqOutboundMediaMode;
|
|
5
5
|
/**
|
package/lib/outbound-media.js
CHANGED
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import * as crypto from "node:crypto";
|
|
5
|
-
import { isMediaRef } from
|
|
5
|
+
import { isMediaRef } from '@zhin.js/im-contract';
|
|
6
6
|
function extForMime(mime, fallback) {
|
|
7
7
|
const map = {
|
|
8
8
|
"image/jpeg": ".jpg",
|