@zhin.js/adapter-icqq 6.0.2 → 6.0.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 +40 -0
- package/README.md +32 -437
- package/adapters/icqq.ts +30 -0
- package/agent/tools/announce.ts +2 -2
- package/agent/tools/essence.ts +2 -2
- package/agent/tools/friend_list.ts +2 -2
- package/agent/tools/group_files.ts +2 -2
- package/agent/tools/group_invite.ts +2 -2
- package/agent/tools/group_list.ts +2 -2
- package/agent/tools/group_sign.ts +2 -2
- package/agent/tools/list_muted.ts +2 -2
- package/agent/tools/poke.ts +2 -2
- package/agent/tools/send_user_like.ts +2 -2
- package/agent/tools/set_anonymous.ts +2 -2
- package/agent/tools/set_title.ts +2 -2
- package/lib/agent-prompt.d.ts +2 -0
- package/lib/{src/agent-prompt.js → agent-prompt.js} +0 -1
- package/lib/cq-message.d.ts +9 -0
- package/lib/{src/cq-message.js → cq-message.js} +0 -1
- package/lib/endpoint.d.ts +51 -0
- package/lib/endpoint.js +483 -0
- package/lib/forward-msg.d.ts +26 -0
- package/lib/{src/forward-msg.js → forward-msg.js} +0 -1
- package/lib/icqq-agent-deps.d.ts +25 -0
- package/lib/icqq-agent-deps.js +32 -0
- package/lib/icqq-guild.d.ts +47 -0
- package/lib/{src/icqq-guild.js → icqq-guild.js} +4 -2
- package/lib/icqq-inbound.d.ts +139 -0
- package/lib/{src/icqq-inbound.js → icqq-inbound.js} +27 -3
- package/lib/index.d.ts +6 -0
- package/lib/index.js +6 -0
- package/lib/ipc-client.d.ts +73 -0
- package/lib/{src/ipc-client.js → ipc-client.js} +0 -1
- package/lib/outbound-media.d.ts +13 -0
- package/lib/{src/outbound-media.js → outbound-media.js} +0 -1
- package/lib/protocol.d.ts +269 -0
- package/lib/{src/protocol.js → protocol.js} +124 -2
- package/lib/types.d.ts +72 -0
- package/lib/{src/types.js → types.js} +0 -1
- package/package.json +49 -45
- package/plugin.ts +32 -0
- package/schema.json +35 -0
- package/src/endpoint.ts +407 -796
- package/src/icqq-agent-deps.ts +48 -9
- package/src/icqq-guild.ts +1 -2
- package/src/icqq-inbound.ts +31 -2
- package/src/index.ts +35 -69
- package/src/protocol.ts +191 -1
- package/client/ICQQManagement.tsx +0 -147
- package/client/LoginAssistPanel.tsx +0 -300
- package/client/index.tsx +0 -11
- package/client/tsconfig.json +0 -7
- package/client/utils/api.ts +0 -30
- package/dist/index.js +0 -34
- package/lib/agent/tools/announce.js +0 -21
- package/lib/agent/tools/announce.js.map +0 -1
- package/lib/agent/tools/essence.js +0 -22
- package/lib/agent/tools/essence.js.map +0 -1
- package/lib/agent/tools/friend_list.js +0 -19
- package/lib/agent/tools/friend_list.js.map +0 -1
- package/lib/agent/tools/group_files.js +0 -29
- package/lib/agent/tools/group_files.js.map +0 -1
- package/lib/agent/tools/group_invite.js +0 -20
- package/lib/agent/tools/group_invite.js.map +0 -1
- package/lib/agent/tools/group_list.js +0 -20
- package/lib/agent/tools/group_list.js.map +0 -1
- package/lib/agent/tools/group_sign.js +0 -19
- package/lib/agent/tools/group_sign.js.map +0 -1
- package/lib/agent/tools/list_muted.js +0 -23
- package/lib/agent/tools/list_muted.js.map +0 -1
- package/lib/agent/tools/poke.js +0 -20
- package/lib/agent/tools/poke.js.map +0 -1
- package/lib/agent/tools/send_user_like.js +0 -22
- package/lib/agent/tools/send_user_like.js.map +0 -1
- package/lib/agent/tools/set_anonymous.js +0 -22
- package/lib/agent/tools/set_anonymous.js.map +0 -1
- package/lib/agent/tools/set_title.js +0 -25
- package/lib/agent/tools/set_title.js.map +0 -1
- package/lib/src/adapter.js +0 -128
- package/lib/src/adapter.js.map +0 -1
- package/lib/src/agent-prompt.js.map +0 -1
- package/lib/src/cq-message.js.map +0 -1
- package/lib/src/endpoint.js +0 -755
- package/lib/src/endpoint.js.map +0 -1
- package/lib/src/forward-msg.js.map +0 -1
- package/lib/src/get-msg.js +0 -46
- package/lib/src/get-msg.js.map +0 -1
- package/lib/src/icqq-agent-deps.js +0 -10
- package/lib/src/icqq-agent-deps.js.map +0 -1
- package/lib/src/icqq-guild.js.map +0 -1
- package/lib/src/icqq-inbound.js.map +0 -1
- package/lib/src/icqq-login-assist-bridge.js +0 -148
- package/lib/src/icqq-login-assist-bridge.js.map +0 -1
- package/lib/src/icqq-side-events.js +0 -130
- package/lib/src/icqq-side-events.js.map +0 -1
- package/lib/src/index.js +0 -50
- package/lib/src/index.js.map +0 -1
- package/lib/src/ipc-client.js.map +0 -1
- package/lib/src/login-assist-routes.js +0 -29
- package/lib/src/login-assist-routes.js.map +0 -1
- package/lib/src/login-ipc-contract.js +0 -35
- package/lib/src/login-ipc-contract.js.map +0 -1
- package/lib/src/outbound-media.js.map +0 -1
- package/lib/src/protocol.js.map +0 -1
- package/lib/src/routes.js +0 -67
- package/lib/src/routes.js.map +0 -1
- package/lib/src/segment-mapper.js +0 -2
- package/lib/src/segment-mapper.js.map +0 -1
- package/lib/src/tools/index.js +0 -23
- package/lib/src/tools/index.js.map +0 -1
- package/lib/src/types.js.map +0 -1
- package/plugin.yml +0 -3
- package/src/adapter.ts +0 -151
- package/src/get-msg.ts +0 -56
- package/src/icqq-login-assist-bridge.ts +0 -196
- package/src/icqq-side-events.ts +0 -143
- package/src/login-assist-routes.ts +0 -39
- package/src/login-ipc-contract.ts +0 -52
- package/src/routes.ts +0 -83
- package/src/segment-mapper.ts +0 -1
- package/src/tools/index.ts +0 -31
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { defineAgentTool } from '@zhin.js/agent/tools';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { getIcqqAgentDeps } from '../../src/icqq-agent-deps.js';
|
|
4
|
-
export default
|
|
4
|
+
export default defineAgentTool<{ endpoint_id: string; user_id: number; times?: number }>({
|
|
5
5
|
description: '给用户点赞(竖大拇指),每人每天最多 20 次',
|
|
6
6
|
inputSchema: z.object({
|
|
7
7
|
endpoint_id: z.string().describe('Endpoint QQ号'),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { defineAgentTool } from '@zhin.js/agent/tools';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { getIcqqAgentDeps } from '../../src/icqq-agent-deps.js';
|
|
4
|
-
export default
|
|
4
|
+
export default defineAgentTool<{ endpoint_id: string; group_id: number; enable?: boolean }>({
|
|
5
5
|
description: '开启或关闭 QQ 群的匿名聊天功能',
|
|
6
6
|
inputSchema: z.object({
|
|
7
7
|
endpoint_id: z.string().describe('Endpoint QQ号'),
|
package/agent/tools/set_title.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { defineAgentTool } from '@zhin.js/agent/tools';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { getIcqqAgentDeps } from '../../src/icqq-agent-deps.js';
|
|
4
|
-
export default
|
|
4
|
+
export default defineAgentTool<{ endpoint_id: string; group_id: number; user_id: number; title: string; duration?: number }>({
|
|
5
5
|
description: '设置 QQ 群成员的专属头衔',
|
|
6
6
|
inputSchema: z.object({
|
|
7
7
|
endpoint_id: z.string().describe('Endpoint QQ号'),
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CQ 码与 MessageSegment 互转。
|
|
3
|
+
* 出站:@icqqjs/cli 要求 message 为非空字符串;引用段编码为 [reply:id](需 cli parse-message 支持 reply,见 scripts/patch-icqq-cli-reply.mjs)。
|
|
4
|
+
*/
|
|
5
|
+
import { MessageSegment, SendContent } from "zhin.js";
|
|
6
|
+
export declare function parseCqMessage(raw: string): MessageSegment[];
|
|
7
|
+
/** 构建 icqq IPC send_*_msg 的 message 字符串(非空) */
|
|
8
|
+
export declare function buildIcqqIpcMessage(content: SendContent): string;
|
|
9
|
+
export declare function toCqString(content: SendContent): string;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IcqqIpcEndpoint — lifecycle, outbound, IPC subscribe/admit for ICQQ daemon.
|
|
3
|
+
*/
|
|
4
|
+
import type { EndpointInstance } from '@zhin.js/adapter';
|
|
5
|
+
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
6
|
+
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
7
|
+
import { type IcqqInboundMessage, type IpcEvent, type IpcResponse, type ResolvedIcqqConfig } from './protocol.js';
|
|
8
|
+
import type { IpcFriendInfo, IpcGroupInfo } from './types.js';
|
|
9
|
+
/** Minimal IPC surface used by the endpoint (real IpcClient or test mock). */
|
|
10
|
+
export interface IcqqIpcTransport {
|
|
11
|
+
request(action: string, params?: Record<string, unknown>): Promise<IpcResponse>;
|
|
12
|
+
subscribe(action: string, params: Record<string, unknown>, handler: (event: IpcEvent) => void): {
|
|
13
|
+
unsubscribe: () => Promise<void>;
|
|
14
|
+
};
|
|
15
|
+
setOnRemoteDisconnect(handler: (() => void) | null): void;
|
|
16
|
+
close(): void;
|
|
17
|
+
}
|
|
18
|
+
export type CreateIcqqIpc = (config: ResolvedIcqqConfig) => Promise<IcqqIpcTransport>;
|
|
19
|
+
export interface IcqqEndpointOptions {
|
|
20
|
+
readonly id: CapabilityId;
|
|
21
|
+
readonly gateway: MessageGateway;
|
|
22
|
+
readonly config: ResolvedIcqqConfig;
|
|
23
|
+
readonly createIpc?: CreateIcqqIpc;
|
|
24
|
+
}
|
|
25
|
+
export declare class IcqqIpcEndpoint implements EndpointInstance {
|
|
26
|
+
#private;
|
|
27
|
+
readonly name: string;
|
|
28
|
+
/** Populated after start(); agent tools read this. */
|
|
29
|
+
ipc: IcqqIpcTransport;
|
|
30
|
+
readonly friends: Map<number, IpcFriendInfo>;
|
|
31
|
+
readonly groups: Map<number, IpcGroupInfo>;
|
|
32
|
+
constructor(options: IcqqEndpointOptions);
|
|
33
|
+
request(action: string, params?: Record<string, unknown>): Promise<IpcResponse>;
|
|
34
|
+
start(): Promise<void>;
|
|
35
|
+
open(): void;
|
|
36
|
+
close(): void;
|
|
37
|
+
stop(): Promise<void>;
|
|
38
|
+
send({ target, payload }: {
|
|
39
|
+
readonly target: string;
|
|
40
|
+
readonly payload: unknown;
|
|
41
|
+
}): Promise<string>;
|
|
42
|
+
private static readonly EMOJI_MAP;
|
|
43
|
+
private getEmojiId;
|
|
44
|
+
/** Activity-feedback / OutboundHost reaction surface. */
|
|
45
|
+
addReaction(messageId: string, emoji: string): Promise<string | null>;
|
|
46
|
+
removeReaction(messageId: string, reactionId: string): Promise<void>;
|
|
47
|
+
/** Activity-feedback autoRemove / Console recall. */
|
|
48
|
+
recallMessage(messageId: string): Promise<void>;
|
|
49
|
+
/** Test / internal: admit when open. */
|
|
50
|
+
admit(msg: IcqqInboundMessage): void;
|
|
51
|
+
}
|
package/lib/endpoint.js
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
2
|
+
import { registerIcqqAgentEndpoint } from './icqq-agent-deps.js';
|
|
3
|
+
import { InboundMessageDeduper, findIcqqNestedMessageSource, isIcqqBotMentioned, isIcqqMessagePostType, normalizeIcqqInboundMessage, quotedPayloadFromIcqqSource, resolveIcqqQuoteIdFromEvent, shouldSkipSelfInboundMessage, unwrapIcqqIpcEventPayload, } from './icqq-inbound.js';
|
|
4
|
+
import { isIcqqGuildIpcEvent, normalizeIcqqGuildInboundMessage, } from './icqq-guild.js';
|
|
5
|
+
import { IpcClient } from './ipc-client.js';
|
|
6
|
+
import { materializeOutboundBase64, resolveIcqqOutboundMediaMode, } from './outbound-media.js';
|
|
7
|
+
import { Actions, formatInboundContent, formatInboundTarget, formatOutboundBody, parseSendTarget, } from './protocol.js';
|
|
8
|
+
const logger = getLogger('icqq');
|
|
9
|
+
export class IcqqIpcEndpoint {
|
|
10
|
+
#options;
|
|
11
|
+
#createIpc;
|
|
12
|
+
name;
|
|
13
|
+
/** Populated after start(); agent tools read this. */
|
|
14
|
+
ipc;
|
|
15
|
+
friends = new Map();
|
|
16
|
+
groups = new Map();
|
|
17
|
+
#open = false;
|
|
18
|
+
#started = false;
|
|
19
|
+
#subscriptions = [];
|
|
20
|
+
#inboundDeduper = new InboundMessageDeduper();
|
|
21
|
+
#unregisterAgent;
|
|
22
|
+
/** 用户主动 stop 时为 true,阻止自动重连 */
|
|
23
|
+
#intentionalDisconnect = false;
|
|
24
|
+
/** 是否已有重连循环在跑(避免多次 schedule 叠套) */
|
|
25
|
+
#reconnectRunning = false;
|
|
26
|
+
constructor(options) {
|
|
27
|
+
this.#options = options;
|
|
28
|
+
this.name = options.config.name;
|
|
29
|
+
this.#createIpc = options.createIpc ?? defaultCreateIpc;
|
|
30
|
+
}
|
|
31
|
+
async request(action, params) {
|
|
32
|
+
if (!this.ipc) {
|
|
33
|
+
throw new Error(`icqq endpoint ${this.name} 未连接(IPC 未初始化或已停止),无法发起请求: ${action}`);
|
|
34
|
+
}
|
|
35
|
+
return this.ipc.request(action, params);
|
|
36
|
+
}
|
|
37
|
+
async start() {
|
|
38
|
+
if (this.#started)
|
|
39
|
+
return;
|
|
40
|
+
this.#started = true;
|
|
41
|
+
this.#intentionalDisconnect = false;
|
|
42
|
+
try {
|
|
43
|
+
await this.#bindIpcSession();
|
|
44
|
+
this.#unregisterAgent = registerIcqqAgentEndpoint(this.name, this);
|
|
45
|
+
logger.info(formatCompact({
|
|
46
|
+
op: 'connect',
|
|
47
|
+
endpoint: this.name,
|
|
48
|
+
mode: this.#options.config.rpc ? 'rpc' : 'ipc',
|
|
49
|
+
friends: this.friends.size,
|
|
50
|
+
groups: this.groups.size,
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
await this.stop();
|
|
55
|
+
// Startup connect failures are surfaced once (with stack) by AdapterIndex;
|
|
56
|
+
// keep this at debug to avoid a duplicate error-level log.
|
|
57
|
+
logger.debug(`Failed to connect ICQQ IPC: ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** 建立或恢复与守护进程的 IPC/RPC 会话(订阅、缓存列表) */
|
|
62
|
+
async #bindIpcSession() {
|
|
63
|
+
// 旧连接上的订阅句柄已失效,不再对死 socket 发 unsubscribe
|
|
64
|
+
this.#subscriptions = [];
|
|
65
|
+
this.ipc = await this.#createIpc(this.#options.config);
|
|
66
|
+
if (this.#options.config.autoReconnect !== false) {
|
|
67
|
+
this.ipc.setOnRemoteDisconnect(() => this.#scheduleReconnect());
|
|
68
|
+
}
|
|
69
|
+
await this.#refreshLists();
|
|
70
|
+
const sub = this.ipc.subscribe(Actions.SUBSCRIBE, {}, (event) => {
|
|
71
|
+
this.#handleEvent(event);
|
|
72
|
+
});
|
|
73
|
+
this.#subscriptions.push(sub);
|
|
74
|
+
}
|
|
75
|
+
/** IPC/RPC 意外断开时调度重连(指数退避) */
|
|
76
|
+
#scheduleReconnect() {
|
|
77
|
+
if (this.#options.config.autoReconnect === false)
|
|
78
|
+
return;
|
|
79
|
+
if (this.#intentionalDisconnect)
|
|
80
|
+
return;
|
|
81
|
+
if (this.#reconnectRunning)
|
|
82
|
+
return;
|
|
83
|
+
this.#reconnectRunning = true;
|
|
84
|
+
void this.#runReconnectLoop();
|
|
85
|
+
}
|
|
86
|
+
async #runReconnectLoop() {
|
|
87
|
+
try {
|
|
88
|
+
for (let attempt = 0; !this.#intentionalDisconnect; attempt++) {
|
|
89
|
+
const base = Math.min(30_000, 500 * 2 ** Math.min(attempt, 6));
|
|
90
|
+
const jitter = Math.floor(Math.random() * 400);
|
|
91
|
+
const delayMs = base + jitter;
|
|
92
|
+
// 首次断开 WARN,后续重试静默为 DEBUG,避免刷屏
|
|
93
|
+
const disconnectLog = attempt === 0 ? logger.warn.bind(logger) : logger.debug.bind(logger);
|
|
94
|
+
disconnectLog(formatCompact({
|
|
95
|
+
op: 'disconnect',
|
|
96
|
+
endpoint: this.name,
|
|
97
|
+
ok: false,
|
|
98
|
+
delay_ms: delayMs,
|
|
99
|
+
attempt: attempt + 1,
|
|
100
|
+
}));
|
|
101
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
102
|
+
if (this.#intentionalDisconnect)
|
|
103
|
+
break;
|
|
104
|
+
try {
|
|
105
|
+
await this.#bindIpcSession();
|
|
106
|
+
logger.info(formatCompact({
|
|
107
|
+
op: 'reconnect',
|
|
108
|
+
endpoint: this.name,
|
|
109
|
+
ok: true,
|
|
110
|
+
attempts: attempt + 1,
|
|
111
|
+
friends: this.friends.size,
|
|
112
|
+
groups: this.groups.size,
|
|
113
|
+
}));
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
const retryLog = attempt === 0 ? logger.warn.bind(logger) : logger.debug.bind(logger);
|
|
118
|
+
retryLog(formatCompact({
|
|
119
|
+
op: 'reconnect',
|
|
120
|
+
endpoint: this.name,
|
|
121
|
+
ok: false,
|
|
122
|
+
attempt: attempt + 1,
|
|
123
|
+
error: error instanceof Error ? error.message : String(error),
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
this.#reconnectRunning = false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
open() {
|
|
133
|
+
this.#open = true;
|
|
134
|
+
}
|
|
135
|
+
close() {
|
|
136
|
+
this.#open = false;
|
|
137
|
+
}
|
|
138
|
+
async stop() {
|
|
139
|
+
this.#intentionalDisconnect = true;
|
|
140
|
+
this.#open = false;
|
|
141
|
+
this.ipc?.setOnRemoteDisconnect(null);
|
|
142
|
+
for (const sub of this.#subscriptions.splice(0)) {
|
|
143
|
+
await sub.unsubscribe().catch(() => { });
|
|
144
|
+
}
|
|
145
|
+
this.#unregisterAgent?.();
|
|
146
|
+
this.#unregisterAgent = undefined;
|
|
147
|
+
this.#inboundDeduper.clear();
|
|
148
|
+
this.friends.clear();
|
|
149
|
+
this.groups.clear();
|
|
150
|
+
try {
|
|
151
|
+
this.ipc?.close();
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
/* ignore */
|
|
155
|
+
}
|
|
156
|
+
// 置空 ipc:start 前 / stop 后的 request() 走防御性报错而非 TypeError
|
|
157
|
+
this.ipc = undefined;
|
|
158
|
+
this.#started = false;
|
|
159
|
+
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.name }));
|
|
160
|
+
}
|
|
161
|
+
async send({ target, payload }) {
|
|
162
|
+
if (!this.ipc) {
|
|
163
|
+
throw new Error(`icqq endpoint ${this.name} 未连接(IPC 未初始化或已停止),无法发送消息`);
|
|
164
|
+
}
|
|
165
|
+
const mediaMode = resolveIcqqOutboundMediaMode(this.#options.config);
|
|
166
|
+
const content = Array.isArray(payload)
|
|
167
|
+
? materializeOutboundBase64(payload, mediaMode)
|
|
168
|
+
: payload;
|
|
169
|
+
const message = formatOutboundBody(content);
|
|
170
|
+
const parsed = parseSendTarget(target);
|
|
171
|
+
let action;
|
|
172
|
+
let params;
|
|
173
|
+
switch (parsed.kind) {
|
|
174
|
+
case 'private':
|
|
175
|
+
action = Actions.SEND_PRIVATE_MSG;
|
|
176
|
+
params = { user_id: parsed.userId, message };
|
|
177
|
+
break;
|
|
178
|
+
case 'group':
|
|
179
|
+
action = Actions.SEND_GROUP_MSG;
|
|
180
|
+
params = { group_id: parsed.groupId, message };
|
|
181
|
+
break;
|
|
182
|
+
case 'temp':
|
|
183
|
+
action = Actions.SEND_TEMP_MSG;
|
|
184
|
+
params = {
|
|
185
|
+
group_id: parsed.groupId,
|
|
186
|
+
user_id: parsed.userId,
|
|
187
|
+
message,
|
|
188
|
+
};
|
|
189
|
+
break;
|
|
190
|
+
case 'channel':
|
|
191
|
+
action = Actions.GUILD_SEND_MSG;
|
|
192
|
+
params = {
|
|
193
|
+
guild_id: parsed.guildId,
|
|
194
|
+
channel_id: parsed.channelId,
|
|
195
|
+
message,
|
|
196
|
+
};
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
const resp = await this.ipc.request(action, params);
|
|
200
|
+
if (!resp.ok) {
|
|
201
|
+
throw new Error(`发送消息失败: ${resp.error}`);
|
|
202
|
+
}
|
|
203
|
+
const messageId = String(resp.data?.message_id ?? `sent_${Date.now()}`);
|
|
204
|
+
logger.debug(formatCompact({
|
|
205
|
+
op: 'icqq_send',
|
|
206
|
+
endpoint: this.name,
|
|
207
|
+
target,
|
|
208
|
+
messageId,
|
|
209
|
+
}));
|
|
210
|
+
return messageId;
|
|
211
|
+
}
|
|
212
|
+
// ── 消息回应(activity-feedback / typing reaction)────────────────────
|
|
213
|
+
static EMOJI_MAP = {
|
|
214
|
+
'⏳': '1468368274',
|
|
215
|
+
'👍': '128077',
|
|
216
|
+
'❤️': '10084',
|
|
217
|
+
'😊': '128522',
|
|
218
|
+
'🎉': '127881',
|
|
219
|
+
'🔥': '128293',
|
|
220
|
+
'✅': '9989',
|
|
221
|
+
'❌': '10060',
|
|
222
|
+
'⭐': '11088',
|
|
223
|
+
'💯': '128175',
|
|
224
|
+
};
|
|
225
|
+
getEmojiId(emoji) {
|
|
226
|
+
if (/^\d+$/u.test(emoji))
|
|
227
|
+
return emoji;
|
|
228
|
+
return IcqqIpcEndpoint.EMOJI_MAP[emoji] ?? String(emoji.codePointAt(0) || 0);
|
|
229
|
+
}
|
|
230
|
+
/** Activity-feedback / OutboundHost reaction surface. */
|
|
231
|
+
async addReaction(messageId, emoji) {
|
|
232
|
+
try {
|
|
233
|
+
const emojiId = this.getEmojiId(emoji);
|
|
234
|
+
const resp = await this.request(Actions.GROUP_SET_REACTION, {
|
|
235
|
+
message_id: messageId,
|
|
236
|
+
id: emojiId,
|
|
237
|
+
});
|
|
238
|
+
if (!resp.ok) {
|
|
239
|
+
logger.warn(formatCompact({
|
|
240
|
+
op: 'add_reaction',
|
|
241
|
+
endpoint: this.name,
|
|
242
|
+
message_id: messageId,
|
|
243
|
+
emoji,
|
|
244
|
+
id: emojiId,
|
|
245
|
+
ok: false,
|
|
246
|
+
error: resp.error,
|
|
247
|
+
}));
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
logger.debug(formatCompact({
|
|
251
|
+
op: 'add_reaction',
|
|
252
|
+
endpoint: this.name,
|
|
253
|
+
message_id: messageId,
|
|
254
|
+
emoji,
|
|
255
|
+
id: emojiId,
|
|
256
|
+
ok: true,
|
|
257
|
+
}));
|
|
258
|
+
return emojiId;
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
logger.warn(formatCompact({
|
|
262
|
+
op: 'add_reaction',
|
|
263
|
+
endpoint: this.name,
|
|
264
|
+
message_id: messageId,
|
|
265
|
+
emoji,
|
|
266
|
+
ok: false,
|
|
267
|
+
error: error instanceof Error ? error.message : String(error),
|
|
268
|
+
}));
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
async removeReaction(messageId, reactionId) {
|
|
273
|
+
try {
|
|
274
|
+
const parts = reactionId.split('_');
|
|
275
|
+
const emojiId = parts.length >= 2 ? parts[1] : reactionId;
|
|
276
|
+
const resp = await this.request(Actions.GROUP_DEL_REACTION, {
|
|
277
|
+
message_id: messageId,
|
|
278
|
+
id: emojiId,
|
|
279
|
+
});
|
|
280
|
+
if (!resp.ok) {
|
|
281
|
+
logger.warn(formatCompact({
|
|
282
|
+
op: 'remove_reaction',
|
|
283
|
+
endpoint: this.name,
|
|
284
|
+
message_id: messageId,
|
|
285
|
+
reaction_id: reactionId,
|
|
286
|
+
id: emojiId,
|
|
287
|
+
ok: false,
|
|
288
|
+
error: resp.error,
|
|
289
|
+
}));
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
logger.debug(formatCompact({
|
|
293
|
+
op: 'remove_reaction',
|
|
294
|
+
endpoint: this.name,
|
|
295
|
+
message_id: messageId,
|
|
296
|
+
reaction_id: reactionId,
|
|
297
|
+
id: emojiId,
|
|
298
|
+
ok: true,
|
|
299
|
+
}));
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
logger.warn(formatCompact({
|
|
303
|
+
op: 'remove_reaction',
|
|
304
|
+
endpoint: this.name,
|
|
305
|
+
message_id: messageId,
|
|
306
|
+
reaction_id: reactionId,
|
|
307
|
+
ok: false,
|
|
308
|
+
error: error instanceof Error ? error.message : String(error),
|
|
309
|
+
}));
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/** Activity-feedback autoRemove / Console recall. */
|
|
313
|
+
async recallMessage(messageId) {
|
|
314
|
+
if (!messageId || messageId.startsWith('outbound:'))
|
|
315
|
+
return;
|
|
316
|
+
try {
|
|
317
|
+
const resp = await this.request(Actions.RECALL_MSG, { message_id: messageId });
|
|
318
|
+
if (!resp.ok) {
|
|
319
|
+
logger.warn(formatCompact({
|
|
320
|
+
op: 'recall_message',
|
|
321
|
+
endpoint: this.name,
|
|
322
|
+
message_id: messageId,
|
|
323
|
+
ok: false,
|
|
324
|
+
error: resp.error,
|
|
325
|
+
}));
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
logger.debug(formatCompact({
|
|
329
|
+
op: 'recall_message',
|
|
330
|
+
endpoint: this.name,
|
|
331
|
+
message_id: messageId,
|
|
332
|
+
ok: true,
|
|
333
|
+
}));
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
logger.warn(formatCompact({
|
|
337
|
+
op: 'recall_message',
|
|
338
|
+
endpoint: this.name,
|
|
339
|
+
message_id: messageId,
|
|
340
|
+
ok: false,
|
|
341
|
+
error: error instanceof Error ? error.message : String(error),
|
|
342
|
+
}));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/** Test / internal: admit when open. */
|
|
346
|
+
admit(msg) {
|
|
347
|
+
if (!this.#open)
|
|
348
|
+
return;
|
|
349
|
+
// 新 Runtime Message.content 为纯文本:@ 本机(uin = name)只能经 metadata 传递
|
|
350
|
+
const mentioned = isIcqqBotMentioned({ uin: this.name, rawMessage: msg.content });
|
|
351
|
+
void this.#options.gateway.receive({
|
|
352
|
+
adapter: this.#options.id,
|
|
353
|
+
target: msg.target,
|
|
354
|
+
content: msg.content,
|
|
355
|
+
sender: msg.sender,
|
|
356
|
+
id: msg.id,
|
|
357
|
+
metadata: Object.freeze({
|
|
358
|
+
endpoint: this.name,
|
|
359
|
+
channelType: msg.channelType,
|
|
360
|
+
...msg.metadata,
|
|
361
|
+
...(mentioned ? { mentioned: true } : {}),
|
|
362
|
+
}),
|
|
363
|
+
}).catch((err) => {
|
|
364
|
+
logger.warn(formatCompact({
|
|
365
|
+
op: 'icqq_gateway_receive_failed',
|
|
366
|
+
target: msg.target,
|
|
367
|
+
error: err instanceof Error ? err.message : String(err),
|
|
368
|
+
}));
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
async #refreshLists() {
|
|
372
|
+
const [flResp, glResp] = await Promise.all([
|
|
373
|
+
this.ipc.request(Actions.LIST_FRIENDS),
|
|
374
|
+
this.ipc.request(Actions.LIST_GROUPS),
|
|
375
|
+
]);
|
|
376
|
+
this.friends.clear();
|
|
377
|
+
if (flResp.ok && Array.isArray(flResp.data)) {
|
|
378
|
+
for (const f of flResp.data) {
|
|
379
|
+
this.friends.set(f.user_id, f);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
this.groups.clear();
|
|
383
|
+
if (glResp.ok && Array.isArray(glResp.data)) {
|
|
384
|
+
for (const g of glResp.data) {
|
|
385
|
+
this.groups.set(g.group_id, g);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
#handleEvent(event) {
|
|
390
|
+
if (isIcqqGuildIpcEvent(event.event)) {
|
|
391
|
+
this.#handleGuildEvent(event);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const payload = unwrapIcqqIpcEventPayload(event);
|
|
395
|
+
if (!payload || typeof payload !== 'object')
|
|
396
|
+
return;
|
|
397
|
+
if (!isIcqqMessagePostType(payload))
|
|
398
|
+
return;
|
|
399
|
+
const data = payload;
|
|
400
|
+
if (shouldSkipSelfInboundMessage(data))
|
|
401
|
+
return;
|
|
402
|
+
const normalized = normalizeIcqqInboundMessage(data);
|
|
403
|
+
if (!normalized)
|
|
404
|
+
return;
|
|
405
|
+
if (!this.#inboundDeduper.shouldProcess(normalized.messageId))
|
|
406
|
+
return;
|
|
407
|
+
this.admit({
|
|
408
|
+
id: normalized.messageId,
|
|
409
|
+
target: formatInboundTarget({
|
|
410
|
+
channelType: normalized.channelType,
|
|
411
|
+
channelId: normalized.channelId,
|
|
412
|
+
channelParentGroupId: normalized.channelParentGroupId,
|
|
413
|
+
}),
|
|
414
|
+
content: formatInboundContent(normalized.rawMessage),
|
|
415
|
+
sender: normalized.userId,
|
|
416
|
+
channelType: normalized.channelType,
|
|
417
|
+
metadata: buildIcqqQuoteMetadata(data, {
|
|
418
|
+
nickname: normalized.nickname,
|
|
419
|
+
senderRole: normalized.senderRole,
|
|
420
|
+
}),
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
#handleGuildEvent(event) {
|
|
424
|
+
const payload = event.data;
|
|
425
|
+
if (!payload || typeof payload !== 'object')
|
|
426
|
+
return;
|
|
427
|
+
const normalized = normalizeIcqqGuildInboundMessage(payload);
|
|
428
|
+
if (!normalized)
|
|
429
|
+
return;
|
|
430
|
+
if (!this.#inboundDeduper.shouldProcess(`guild:${normalized.messageId}`))
|
|
431
|
+
return;
|
|
432
|
+
this.admit({
|
|
433
|
+
id: normalized.messageId,
|
|
434
|
+
target: formatInboundTarget({
|
|
435
|
+
channelType: 'channel',
|
|
436
|
+
channelId: normalized.channelId,
|
|
437
|
+
guildId: normalized.guildId,
|
|
438
|
+
}),
|
|
439
|
+
content: formatInboundContent(normalized.rawMessage),
|
|
440
|
+
sender: normalized.userId,
|
|
441
|
+
channelType: 'channel',
|
|
442
|
+
metadata: {
|
|
443
|
+
guildId: normalized.guildId,
|
|
444
|
+
nickname: normalized.nickname,
|
|
445
|
+
},
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* 组装入站 metadata:在基础字段上补充 quote 链路信息。
|
|
451
|
+
* - quote_id:被引用消息 id(resolveIcqqQuoteIdFromEvent,无则不写)
|
|
452
|
+
* - quote_sender_id / quote_sender_name / quote_content:source 可用时的平铺摘要
|
|
453
|
+
*/
|
|
454
|
+
function buildIcqqQuoteMetadata(data, base) {
|
|
455
|
+
const metadata = { ...base };
|
|
456
|
+
const quoteId = resolveIcqqQuoteIdFromEvent(data);
|
|
457
|
+
if (quoteId)
|
|
458
|
+
metadata.quote_id = quoteId;
|
|
459
|
+
const quoted = quotedPayloadFromIcqqSource(findIcqqNestedMessageSource(data));
|
|
460
|
+
if (quoted?.sender?.id)
|
|
461
|
+
metadata.quote_sender_id = quoted.sender.id;
|
|
462
|
+
if (quoted?.sender?.name)
|
|
463
|
+
metadata.quote_sender_name = quoted.sender.name;
|
|
464
|
+
const quoteText = quoted
|
|
465
|
+
? (typeof quoted.content === 'string'
|
|
466
|
+
? quoted.content
|
|
467
|
+
: quoted.content
|
|
468
|
+
.map((seg) => seg && typeof seg === 'object' && seg.type === 'text'
|
|
469
|
+
? String(seg.data?.text ?? '')
|
|
470
|
+
: '')
|
|
471
|
+
.join('')).trim() || quoted.raw || ''
|
|
472
|
+
: '';
|
|
473
|
+
if (quoteText)
|
|
474
|
+
metadata.quote_content = quoteText;
|
|
475
|
+
return metadata;
|
|
476
|
+
}
|
|
477
|
+
async function defaultCreateIpc(config) {
|
|
478
|
+
const uin = Number(config.name);
|
|
479
|
+
if (config.rpc) {
|
|
480
|
+
return IpcClient.connectRpc(config.rpc);
|
|
481
|
+
}
|
|
482
|
+
return IpcClient.connect(uin);
|
|
483
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 合并转发(聊天记录)解析:从 json/xml 提取 resid,经 IPC get_forward_msg 拉取正文。
|
|
3
|
+
*/
|
|
4
|
+
import { type MessageSegment, type QuotedMessagePayload } from 'zhin.js';
|
|
5
|
+
import type { IpcClient } from "./ipc-client.js";
|
|
6
|
+
import { type IcqqMessageElement } from "./icqq-inbound.js";
|
|
7
|
+
/** 从 QQ 合并转发 json 元素提取 resid */
|
|
8
|
+
export declare function extractForwardResidFromJsonElement(el: IcqqMessageElement): string | undefined;
|
|
9
|
+
/** 从 get_msg 的 raw_message 解析 multimsg resid(XML / JSON / CQ:json) */
|
|
10
|
+
export declare function extractForwardResidFromRawMessage(raw: string): string | undefined;
|
|
11
|
+
/** 从 get_msg 的 message 元素数组解析 multimsg resid */
|
|
12
|
+
export declare function extractForwardResidFromGetMsgElements(elements: IcqqMessageElement[] | undefined): string | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* 从 get_msg 响应解析合并转发 resid(优先 message 数组,其次 raw_message)。
|
|
15
|
+
*/
|
|
16
|
+
export declare function extractForwardResidFromGetMsg(data: unknown): string | undefined;
|
|
17
|
+
export declare function extractForwardResidFromSegments(content: MessageSegment[]): string | undefined;
|
|
18
|
+
export declare function extractForwardResidFromPayload(payload: QuotedMessagePayload): string | undefined;
|
|
19
|
+
/** 在 get_msg / IPC 原始对象里深度搜索 resid */
|
|
20
|
+
export declare function extractForwardResidDeep(root: unknown, maxDepth?: number): string | undefined;
|
|
21
|
+
export declare function isForwardPlaceholderPayload(payload: QuotedMessagePayload): boolean;
|
|
22
|
+
/** 将 get_forward_msg 响应格式化为可读聊天记录 */
|
|
23
|
+
export declare function formatForwardMsgResponse(data: unknown): string;
|
|
24
|
+
export declare function fetchForwardMsgText(ipc: IpcClient, id: string): Promise<string>;
|
|
25
|
+
/** 若 payload 含合并转发,拉取并追加到 content */
|
|
26
|
+
export declare function enrichQuotedPayloadWithForward(ipc: IpcClient | null | undefined, payload: QuotedMessagePayload, ipcRaw?: unknown): Promise<QuotedMessagePayload>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent tool deps for icqq.
|
|
3
|
+
* Endpoints register themselves on start; tools look up by config name / QQ uin.
|
|
4
|
+
*/
|
|
5
|
+
import type { IpcFriendInfo, IpcGroupInfo } from './types.js';
|
|
6
|
+
import type { IpcResponse } from './protocol.js';
|
|
7
|
+
export interface IcqqAgentEndpoint {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly ipc: {
|
|
10
|
+
request(action: string, params?: Record<string, unknown>): Promise<IpcResponse>;
|
|
11
|
+
};
|
|
12
|
+
readonly friends: Map<number, IpcFriendInfo>;
|
|
13
|
+
readonly groups: Map<number, IpcGroupInfo>;
|
|
14
|
+
request(action: string, params?: Record<string, unknown>): Promise<IpcResponse>;
|
|
15
|
+
}
|
|
16
|
+
export interface IcqqAgentDeps {
|
|
17
|
+
getEndpoint: (endpointId: string) => IcqqAgentEndpoint;
|
|
18
|
+
getAdapter?: () => {
|
|
19
|
+
getEndpoint: (id: string) => IcqqAgentEndpoint;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export declare function registerIcqqAgentEndpoint(endpointId: string, endpoint: IcqqAgentEndpoint): () => void;
|
|
23
|
+
/** Optional override used by tests / transitional callers. Pass `null` to clear. */
|
|
24
|
+
export declare function setIcqqAgentDeps(deps: IcqqAgentDeps | null): void;
|
|
25
|
+
export declare function getIcqqAgentDeps(): IcqqAgentDeps;
|