@zhin.js/adapter-icqq 1.1.0 → 1.1.2
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 +91 -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-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-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/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;
|
|
@@ -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",
|
package/lib/protocol.d.ts
CHANGED
|
@@ -161,9 +161,9 @@ export declare const Actions: {
|
|
|
161
161
|
readonly SET_NOTIFY: "set_notify";
|
|
162
162
|
readonly GET_NOTIFY: "get_notify";
|
|
163
163
|
};
|
|
164
|
-
/**
|
|
165
|
-
export interface
|
|
166
|
-
readonly id
|
|
164
|
+
/** One expanded ICQQ endpoint configuration produced by AdapterIndex. */
|
|
165
|
+
export interface IcqqEndpointConfig {
|
|
166
|
+
readonly id: string;
|
|
167
167
|
readonly autoReconnect?: boolean;
|
|
168
168
|
readonly outboundMedia?: 'file' | 'base64';
|
|
169
169
|
readonly password?: string;
|
|
@@ -177,24 +177,6 @@ export interface IcqqAdapterConfig {
|
|
|
177
177
|
readonly autoServer?: boolean;
|
|
178
178
|
readonly qqnt?: boolean;
|
|
179
179
|
readonly ntLogin?: boolean;
|
|
180
|
-
/** Transitional: legacy root `endpoints[]` with `context: icqq`. */
|
|
181
|
-
readonly endpoints?: ReadonlyArray<{
|
|
182
|
-
readonly context?: string;
|
|
183
|
-
readonly id?: string;
|
|
184
|
-
readonly autoReconnect?: boolean;
|
|
185
|
-
readonly outboundMedia?: 'file' | 'base64';
|
|
186
|
-
readonly password?: string;
|
|
187
|
-
readonly platform?: number;
|
|
188
|
-
readonly ver?: string;
|
|
189
|
-
readonly dataDir?: string;
|
|
190
|
-
readonly signApiAddr?: string;
|
|
191
|
-
readonly ignoreSelf?: boolean;
|
|
192
|
-
readonly resend?: boolean;
|
|
193
|
-
readonly cacheGroupMember?: boolean;
|
|
194
|
-
readonly autoServer?: boolean;
|
|
195
|
-
readonly qqnt?: boolean;
|
|
196
|
-
readonly ntLogin?: boolean;
|
|
197
|
-
}>;
|
|
198
180
|
}
|
|
199
181
|
export interface ResolvedIcqqConfig {
|
|
200
182
|
readonly context: 'icqq';
|
|
@@ -251,7 +233,7 @@ export type ParsedIcqqSendTarget = {
|
|
|
251
233
|
readonly guildId: string;
|
|
252
234
|
readonly channelId: string;
|
|
253
235
|
};
|
|
254
|
-
export declare function resolveIcqqConfig(config
|
|
236
|
+
export declare function resolveIcqqConfig(config: IcqqEndpointConfig): ResolvedIcqqConfig;
|
|
255
237
|
/**
|
|
256
238
|
* 入站归一化 → ConversationRef:群临时会话(temp)映射为群容器内的 private
|
|
257
239
|
* 会话(parent kind 'group');频道消息的所属 guild 进 `parent`(kind 'channel')。
|