@zhin.js/adapter-icqq 6.0.1 → 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 +59 -0
- package/README.md +32 -437
- package/adapters/icqq.ts +30 -0
- package/agent/tools/announce.ts +21 -0
- package/agent/tools/essence.ts +22 -0
- package/agent/tools/friend_list.ts +19 -0
- package/agent/tools/group_files.ts +27 -0
- package/agent/tools/group_invite.ts +20 -0
- package/agent/tools/group_list.ts +20 -0
- package/agent/tools/group_sign.ts +19 -0
- package/agent/tools/list_muted.ts +22 -0
- package/agent/tools/poke.ts +20 -0
- package/agent/tools/send_user_like.ts +22 -0
- package/agent/tools/set_anonymous.ts +22 -0
- package/agent/tools/set_title.ts +25 -0
- package/lib/agent-prompt.d.ts +0 -1
- package/lib/agent-prompt.js +0 -1
- package/lib/cq-message.d.ts +0 -1
- package/lib/cq-message.js +0 -1
- package/lib/endpoint.d.ts +45 -97
- package/lib/endpoint.js +357 -628
- package/lib/forward-msg.d.ts +1 -2
- package/lib/forward-msg.js +4 -2
- package/lib/icqq-agent-deps.d.ts +25 -0
- package/lib/icqq-agent-deps.js +32 -0
- package/lib/icqq-guild.d.ts +1 -2
- package/lib/icqq-guild.js +4 -2
- package/lib/icqq-inbound.d.ts +10 -1
- package/lib/icqq-inbound.js +27 -3
- package/lib/index.d.ts +6 -18
- package/lib/index.js +6 -50
- package/lib/ipc-client.d.ts +0 -1
- package/lib/ipc-client.js +0 -1
- package/lib/outbound-media.d.ts +0 -1
- package/lib/outbound-media.js +0 -1
- package/lib/protocol.d.ts +73 -2
- package/lib/protocol.js +124 -2
- package/lib/types.d.ts +0 -1
- package/lib/types.js +0 -1
- package/package.json +51 -43
- package/plugin.ts +32 -0
- package/schema.json +35 -0
- package/src/agent-prompt.ts +1 -2
- package/src/endpoint.ts +399 -800
- package/src/forward-msg.ts +2 -2
- package/src/icqq-agent-deps.ts +57 -0
- 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/adapter.d.ts +0 -46
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js +0 -128
- package/lib/adapter.js.map +0 -1
- package/lib/agent-prompt.d.ts.map +0 -1
- package/lib/agent-prompt.js.map +0 -1
- package/lib/cq-message.d.ts.map +0 -1
- package/lib/cq-message.js.map +0 -1
- package/lib/endpoint.d.ts.map +0 -1
- package/lib/endpoint.js.map +0 -1
- package/lib/forward-msg.d.ts.map +0 -1
- package/lib/forward-msg.js.map +0 -1
- package/lib/get-msg.d.ts +0 -3
- package/lib/get-msg.d.ts.map +0 -1
- package/lib/get-msg.js +0 -46
- package/lib/get-msg.js.map +0 -1
- package/lib/icqq-guild.d.ts.map +0 -1
- package/lib/icqq-guild.js.map +0 -1
- package/lib/icqq-inbound.d.ts.map +0 -1
- package/lib/icqq-inbound.js.map +0 -1
- package/lib/icqq-login-assist-bridge.d.ts +0 -5
- package/lib/icqq-login-assist-bridge.d.ts.map +0 -1
- package/lib/icqq-login-assist-bridge.js +0 -148
- package/lib/icqq-login-assist-bridge.js.map +0 -1
- package/lib/icqq-side-events.d.ts +0 -34
- package/lib/icqq-side-events.d.ts.map +0 -1
- package/lib/icqq-side-events.js +0 -194
- package/lib/icqq-side-events.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/ipc-client.d.ts.map +0 -1
- package/lib/ipc-client.js.map +0 -1
- package/lib/login-assist-routes.d.ts +0 -7
- package/lib/login-assist-routes.d.ts.map +0 -1
- package/lib/login-assist-routes.js +0 -29
- package/lib/login-assist-routes.js.map +0 -1
- package/lib/login-ipc-contract.d.ts +0 -44
- package/lib/login-ipc-contract.d.ts.map +0 -1
- package/lib/login-ipc-contract.js +0 -35
- package/lib/login-ipc-contract.js.map +0 -1
- package/lib/outbound-media.d.ts.map +0 -1
- package/lib/outbound-media.js.map +0 -1
- package/lib/protocol.d.ts.map +0 -1
- package/lib/protocol.js.map +0 -1
- package/lib/routes.d.ts +0 -8
- package/lib/routes.d.ts.map +0 -1
- package/lib/routes.js +0 -67
- package/lib/routes.js.map +0 -1
- package/lib/segment-mapper.d.ts +0 -2
- package/lib/segment-mapper.d.ts.map +0 -1
- package/lib/segment-mapper.js +0 -2
- package/lib/segment-mapper.js.map +0 -1
- package/lib/tools/index.d.ts +0 -10
- package/lib/tools/index.d.ts.map +0 -1
- package/lib/tools/index.js +0 -341
- package/lib/tools/index.js.map +0 -1
- package/lib/types.d.ts.map +0 -1
- package/lib/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 -228
- 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 -412
- /package/{skills/icqq → agent}/PERMITS.md +0 -0
package/src/forward-msg.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 合并转发(聊天记录)解析:从 json/xml 提取 resid,经 IPC get_forward_msg 拉取正文。
|
|
3
3
|
*/
|
|
4
|
-
import type
|
|
5
|
-
|
|
4
|
+
import { segment, type MessageSegment, type QuotedMessagePayload } from 'zhin.js';
|
|
5
|
+
|
|
6
6
|
import { Actions } from "./protocol.js";
|
|
7
7
|
import type { IpcClient } from "./ipc-client.js";
|
|
8
8
|
import {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent tool deps for icqq.
|
|
3
|
+
* Endpoints register themselves on start; tools look up by config name / QQ uin.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { IpcClient } from './ipc-client.js';
|
|
7
|
+
import type { IpcFriendInfo, IpcGroupInfo } from './types.js';
|
|
8
|
+
import type { IpcResponse } from './protocol.js';
|
|
9
|
+
|
|
10
|
+
export interface IcqqAgentEndpoint {
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly ipc: {
|
|
13
|
+
request(action: string, params?: Record<string, unknown>): Promise<IpcResponse>;
|
|
14
|
+
};
|
|
15
|
+
readonly friends: Map<number, IpcFriendInfo>;
|
|
16
|
+
readonly groups: Map<number, IpcGroupInfo>;
|
|
17
|
+
request(action: string, params?: Record<string, unknown>): Promise<IpcResponse>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface IcqqAgentDeps {
|
|
21
|
+
getEndpoint: (endpointId: string) => IcqqAgentEndpoint;
|
|
22
|
+
getAdapter?: () => { getEndpoint: (id: string) => IcqqAgentEndpoint };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const endpoints = new Map<string, IcqqAgentEndpoint>();
|
|
26
|
+
let override: IcqqAgentDeps | null = null;
|
|
27
|
+
|
|
28
|
+
export function registerIcqqAgentEndpoint(
|
|
29
|
+
endpointId: string,
|
|
30
|
+
endpoint: IcqqAgentEndpoint,
|
|
31
|
+
): () => void {
|
|
32
|
+
endpoints.set(endpointId, endpoint);
|
|
33
|
+
return () => {
|
|
34
|
+
if (endpoints.get(endpointId) === endpoint) {
|
|
35
|
+
endpoints.delete(endpointId);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Optional override used by tests / transitional callers. Pass `null` to clear. */
|
|
41
|
+
export function setIcqqAgentDeps(deps: IcqqAgentDeps | null): void {
|
|
42
|
+
override = deps;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function lookup(endpointId: string): IcqqAgentEndpoint {
|
|
46
|
+
const registered = endpoints.get(endpointId);
|
|
47
|
+
if (!registered) throw new Error(`Endpoint ${endpointId} 不存在`);
|
|
48
|
+
return registered;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getIcqqAgentDeps(): IcqqAgentDeps {
|
|
52
|
+
if (override) return override;
|
|
53
|
+
return {
|
|
54
|
+
getEndpoint: lookup,
|
|
55
|
+
getAdapter: () => ({ getEndpoint: lookup }),
|
|
56
|
+
};
|
|
57
|
+
}
|
package/src/icqq-guild.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ICQQ guild (QQ 频道) catalog sync, inbound normalize, Console channel list.
|
|
3
3
|
*/
|
|
4
|
-
import type
|
|
4
|
+
import { toCanonicalSegments, type MessageSegment } from "zhin.js";
|
|
5
5
|
import { Actions, type IpcGuildMessageEventData } from "./protocol.js";
|
|
6
6
|
import type { IpcClient } from "./ipc-client.js";
|
|
7
7
|
import { parseCqMessage } from "./cq-message.js";
|
|
8
|
-
import { toCanonicalSegments } from "./segment-mapper.js";
|
|
9
8
|
|
|
10
9
|
export interface IcqqGuildChannelEntry {
|
|
11
10
|
channel_id: string;
|
package/src/icqq-inbound.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* icqq 守护进程 IPC 入站事件归一化(post_type / message_type / message 段)
|
|
3
3
|
*/
|
|
4
|
-
import { Message, type MessageSegment, type QuotedMessagePayload } from "zhin.js";
|
|
4
|
+
import { Message, toCanonicalSegments, type MessageSegment, type QuotedMessagePayload } from "zhin.js";
|
|
5
5
|
import { parseCqMessage } from "./cq-message.js";
|
|
6
|
-
import { toCanonicalSegments } from "./segment-mapper.js";
|
|
7
6
|
import { extractForwardResidFromJsonElement } from "./forward-msg.js";
|
|
8
7
|
import type { GroupRole } from "./types.js";
|
|
9
8
|
|
|
@@ -110,6 +109,36 @@ export function parseIcqqSenderGroupRole(
|
|
|
110
109
|
return ICQQ_GROUP_ROLES.has(role) ? role : undefined;
|
|
111
110
|
}
|
|
112
111
|
|
|
112
|
+
/**
|
|
113
|
+
* 判断入站消息是否 @ 了本机账号(uin = endpoint 配置 name,即 QQ 号)。
|
|
114
|
+
* 新 Plugin Runtime 的 Message.content 为纯文本,at 信息只能经 metadata 传递,
|
|
115
|
+
* endpoint 在 admit 时用本函数标注 `metadata.mentioned`(供 Agent Host 的 @ 触发)。
|
|
116
|
+
*/
|
|
117
|
+
export function isIcqqBotMentioned(input: {
|
|
118
|
+
readonly uin: string;
|
|
119
|
+
readonly content?: readonly MessageSegment[] | null;
|
|
120
|
+
readonly rawMessage?: string;
|
|
121
|
+
}): boolean {
|
|
122
|
+
const uin = String(input.uin ?? "").trim();
|
|
123
|
+
if (!uin) return false;
|
|
124
|
+
for (const seg of input.content ?? []) {
|
|
125
|
+
if (!seg || (seg.type !== "at" && seg.type !== "mention")) continue;
|
|
126
|
+
const data = (seg.data ?? {}) as Record<string, unknown>;
|
|
127
|
+
const target = String(
|
|
128
|
+
data.qq ?? data.user_id ?? data.id ?? data.target ?? "",
|
|
129
|
+
).trim();
|
|
130
|
+
if (target && target !== "all" && Number(target) === Number(uin)) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const raw = input.rawMessage ?? "";
|
|
135
|
+
if (!raw) return false;
|
|
136
|
+
const escaped = uin.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
137
|
+
// OneBot CQ 码 `[CQ:at,qq=uin]`;QQ 频道 markdown `<@!uin>`
|
|
138
|
+
return new RegExp(`\\[CQ:at,qq=${escaped}\\]`, "u").test(raw)
|
|
139
|
+
|| new RegExp(`<@!?${escaped}>`, "u").test(raw);
|
|
140
|
+
}
|
|
141
|
+
|
|
113
142
|
const MESSAGE_POST_TYPES = new Set(["message"]);
|
|
114
143
|
|
|
115
144
|
function isMessagePostType(postType: unknown): boolean {
|
package/src/index.ts
CHANGED
|
@@ -1,73 +1,39 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
type
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
1
|
+
export {
|
|
2
|
+
Actions,
|
|
3
|
+
formatInboundContent,
|
|
4
|
+
formatInboundTarget,
|
|
5
|
+
formatOutboundBody,
|
|
6
|
+
parseSendTarget,
|
|
7
|
+
resolveIcqqConfig,
|
|
8
|
+
type IcqqAdapterConfig,
|
|
9
|
+
type IcqqInboundMessage,
|
|
10
|
+
type IcqqWireSegment,
|
|
11
|
+
type IpcEvent,
|
|
12
|
+
type IpcMessage,
|
|
13
|
+
type IpcRequest,
|
|
14
|
+
type IpcResponse,
|
|
15
|
+
type ParsedIcqqSendTarget,
|
|
16
|
+
type ResolvedIcqqConfig,
|
|
17
|
+
} from './protocol.js';
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
interface Adapters {
|
|
27
|
-
icqq: IcqqAdapter;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
19
|
+
export {
|
|
20
|
+
IcqqIpcEndpoint,
|
|
21
|
+
type CreateIcqqIpc,
|
|
22
|
+
type IcqqEndpointOptions,
|
|
23
|
+
type IcqqIpcTransport,
|
|
24
|
+
} from './endpoint.js';
|
|
30
25
|
|
|
31
|
-
export * from
|
|
32
|
-
export { IcqqEndpoint } from "./endpoint.js";
|
|
33
|
-
export { IcqqAdapter } from "./adapter.js";
|
|
26
|
+
export * from './types.js';
|
|
34
27
|
|
|
35
|
-
|
|
36
|
-
|
|
28
|
+
export {
|
|
29
|
+
getIcqqAgentDeps,
|
|
30
|
+
registerIcqqAgentEndpoint,
|
|
31
|
+
setIcqqAgentDeps,
|
|
32
|
+
type IcqqAgentDeps,
|
|
33
|
+
type IcqqAgentEndpoint,
|
|
34
|
+
} from './icqq-agent-deps.js';
|
|
37
35
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
mounted: async (p: Plugin) => {
|
|
43
|
-
registerAgentPromptContributor(createIcqqAgentPromptContributor());
|
|
44
|
-
const adapter = new IcqqAdapter(p);
|
|
45
|
-
await adapter.start();
|
|
46
|
-
return adapter;
|
|
47
|
-
},
|
|
48
|
-
dispose: async (adapter: IcqqAdapter) => {
|
|
49
|
-
unregisterAgentPromptContributor("icqq");
|
|
50
|
-
await adapter.stop();
|
|
51
|
-
},
|
|
52
|
-
} as any);
|
|
53
|
-
|
|
54
|
-
// ── AI 工具注册 ────────────────────────────────────────────────────
|
|
55
|
-
useContext("tool", "icqq", (toolService: ToolFeature, icqq: IcqqAdapter) => {
|
|
56
|
-
return registerTools(toolService, icqq, plugin.name);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
// ── Web 控制台入口 ─────────────────────────────────────────────────
|
|
60
|
-
useContext("web", (pageManager) => {
|
|
61
|
-
pageManager.addEntry({
|
|
62
|
-
id: "icqq",
|
|
63
|
-
development: path.resolve(import.meta.dirname, "../client/index.tsx"),
|
|
64
|
-
production: path.resolve(import.meta.dirname, "../dist/index.js"),
|
|
65
|
-
meta: { name: "ICQQ" },
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
// ── HTTP 路由 ──────────────────────────────────────────────────────
|
|
70
|
-
useContext("router", "icqq", async (router: Router, icqq: IcqqAdapter) => {
|
|
71
|
-
registerRoutes(router, icqq, root);
|
|
72
|
-
registerLoginAssistRoutes(router, root);
|
|
73
|
-
});
|
|
36
|
+
export { IpcClient } from './ipc-client.js';
|
|
37
|
+
export {
|
|
38
|
+
resolveIcqqInboundMessageId,
|
|
39
|
+
} from './icqq-inbound.js';
|
package/src/protocol.ts
CHANGED
|
@@ -9,9 +9,11 @@
|
|
|
9
9
|
* 2. 认证通过后发送 IpcRequest → 等待 IpcResponse
|
|
10
10
|
* 3. 认证通过后自动接收 icqq 事件推送(IpcEvent),断开连接自动停止
|
|
11
11
|
*
|
|
12
|
-
* 登录相关:**推送**走 IpcEvent(`system.login
|
|
12
|
+
* 登录相关:**推送**走 IpcEvent(`system.login.*`)。
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { pickCredential } from '@zhin.js/adapter';
|
|
16
|
+
|
|
15
17
|
/** CLI → Daemon 请求 */
|
|
16
18
|
export type IpcRequest = {
|
|
17
19
|
id: string;
|
|
@@ -265,3 +267,191 @@ export const Actions = {
|
|
|
265
267
|
SET_NOTIFY: "set_notify",
|
|
266
268
|
GET_NOTIFY: "get_notify",
|
|
267
269
|
} as const;
|
|
270
|
+
|
|
271
|
+
/** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
|
|
272
|
+
export interface IcqqAdapterConfig {
|
|
273
|
+
readonly name?: string;
|
|
274
|
+
readonly autoReconnect?: boolean;
|
|
275
|
+
readonly outboundMedia?: 'file' | 'base64';
|
|
276
|
+
readonly rpc?: {
|
|
277
|
+
readonly host?: string;
|
|
278
|
+
readonly port?: number;
|
|
279
|
+
readonly token?: string;
|
|
280
|
+
};
|
|
281
|
+
/** Transitional: legacy root `endpoints[]` with `context: icqq`. */
|
|
282
|
+
readonly endpoints?: ReadonlyArray<{
|
|
283
|
+
readonly context?: string;
|
|
284
|
+
readonly name?: string;
|
|
285
|
+
readonly autoReconnect?: boolean;
|
|
286
|
+
readonly outboundMedia?: 'file' | 'base64';
|
|
287
|
+
readonly rpc?: {
|
|
288
|
+
readonly host?: string;
|
|
289
|
+
readonly port?: number;
|
|
290
|
+
readonly token?: string;
|
|
291
|
+
};
|
|
292
|
+
}>;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export interface ResolvedIcqqConfig {
|
|
296
|
+
readonly context: 'icqq';
|
|
297
|
+
readonly name: string;
|
|
298
|
+
readonly autoReconnect: boolean;
|
|
299
|
+
readonly outboundMedia?: 'file' | 'base64';
|
|
300
|
+
readonly rpc?: {
|
|
301
|
+
readonly host: string;
|
|
302
|
+
readonly port: number;
|
|
303
|
+
readonly token: string;
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export interface IcqqWireSegment {
|
|
308
|
+
readonly type: string;
|
|
309
|
+
readonly data?: Record<string, unknown>;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export interface IcqqInboundMessage {
|
|
313
|
+
readonly id: string;
|
|
314
|
+
readonly target: string;
|
|
315
|
+
readonly content: string;
|
|
316
|
+
readonly sender: string;
|
|
317
|
+
readonly channelType: 'private' | 'group' | 'channel';
|
|
318
|
+
readonly metadata?: Record<string, unknown>;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export type ParsedIcqqSendTarget =
|
|
322
|
+
| { readonly kind: 'private'; readonly userId: number }
|
|
323
|
+
| { readonly kind: 'group'; readonly groupId: number }
|
|
324
|
+
| { readonly kind: 'temp'; readonly groupId: number; readonly userId: number }
|
|
325
|
+
| { readonly kind: 'channel'; readonly guildId: string; readonly channelId: string };
|
|
326
|
+
|
|
327
|
+
export function resolveIcqqConfig(config: IcqqAdapterConfig = {}): ResolvedIcqqConfig {
|
|
328
|
+
const entry = config.endpoints?.find((item) => item.context === 'icqq');
|
|
329
|
+
const name = pickCredential(config.name, entry?.name, process.env.ICQQ_ACCOUNT);
|
|
330
|
+
if (!/^\d+$/.test(name)) {
|
|
331
|
+
throw new TypeError(
|
|
332
|
+
'ICQQ adapter requires numeric name (QQ uin) via plugins.<key>.name or ICQQ_ACCOUNT',
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
const autoReconnect = config.autoReconnect ?? entry?.autoReconnect ?? true;
|
|
336
|
+
const outboundMedia = config.outboundMedia ?? entry?.outboundMedia;
|
|
337
|
+
const rpcRaw = config.rpc ?? entry?.rpc;
|
|
338
|
+
const rpc = rpcRaw?.host && rpcRaw.port != null && rpcRaw.token
|
|
339
|
+
? { host: rpcRaw.host, port: Number(rpcRaw.port), token: rpcRaw.token }
|
|
340
|
+
: undefined;
|
|
341
|
+
return {
|
|
342
|
+
context: 'icqq',
|
|
343
|
+
name,
|
|
344
|
+
autoReconnect,
|
|
345
|
+
...(outboundMedia ? { outboundMedia } : {}),
|
|
346
|
+
...(rpc ? { rpc } : {}),
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Gateway reply target: `private:uid` / `group:gid` / `temp:gid:uid` / `channel:guild:channel`. */
|
|
351
|
+
export function formatInboundTarget(input: {
|
|
352
|
+
readonly channelType: 'private' | 'group' | 'channel';
|
|
353
|
+
readonly channelId: string;
|
|
354
|
+
readonly channelParentGroupId?: string;
|
|
355
|
+
readonly guildId?: string;
|
|
356
|
+
}): string {
|
|
357
|
+
if (input.channelType === 'private' && input.channelParentGroupId) {
|
|
358
|
+
return `temp:${input.channelParentGroupId}:${input.channelId}`;
|
|
359
|
+
}
|
|
360
|
+
if (input.channelType === 'channel' && input.guildId) {
|
|
361
|
+
return `channel:${input.guildId}:${input.channelId}`;
|
|
362
|
+
}
|
|
363
|
+
return `${input.channelType}:${input.channelId}`;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function parseSendTarget(target: string): ParsedIcqqSendTarget {
|
|
367
|
+
const trimmed = target.trim();
|
|
368
|
+
if (trimmed.startsWith('temp:')) {
|
|
369
|
+
const rest = trimmed.slice(5);
|
|
370
|
+
const [groupId, userId] = rest.split(':');
|
|
371
|
+
if (!groupId || !userId) throw new TypeError(`Invalid icqq temp target: ${target}`);
|
|
372
|
+
return { kind: 'temp', groupId: Number(groupId), userId: Number(userId) };
|
|
373
|
+
}
|
|
374
|
+
if (trimmed.startsWith('channel:')) {
|
|
375
|
+
const rest = trimmed.slice(8);
|
|
376
|
+
const idx = rest.indexOf(':');
|
|
377
|
+
if (idx <= 0) throw new TypeError(`Invalid icqq channel target: ${target}`);
|
|
378
|
+
return {
|
|
379
|
+
kind: 'channel',
|
|
380
|
+
guildId: rest.slice(0, idx),
|
|
381
|
+
channelId: rest.slice(idx + 1),
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
if (trimmed.startsWith('private:')) {
|
|
385
|
+
return { kind: 'private', userId: Number(trimmed.slice(8)) };
|
|
386
|
+
}
|
|
387
|
+
if (trimmed.startsWith('group:')) {
|
|
388
|
+
return { kind: 'group', groupId: Number(trimmed.slice(6)) };
|
|
389
|
+
}
|
|
390
|
+
// Bare numeric id → private (common reply path)
|
|
391
|
+
if (/^\d+$/.test(trimmed)) {
|
|
392
|
+
return { kind: 'private', userId: Number(trimmed) };
|
|
393
|
+
}
|
|
394
|
+
throw new TypeError(`Invalid icqq send target: ${target}`);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export function formatOutboundBody(payload: unknown): string {
|
|
398
|
+
if (typeof payload === 'string') {
|
|
399
|
+
const text = payload.trim();
|
|
400
|
+
return text || '\u200b';
|
|
401
|
+
}
|
|
402
|
+
if (payload == null) return '\u200b';
|
|
403
|
+
if (!Array.isArray(payload)) {
|
|
404
|
+
// 单个段对象(非数组)按单元素数组处理,杜绝 String(object) → '[object Object]'
|
|
405
|
+
if (isWireSegment(payload)) {
|
|
406
|
+
return formatOutboundBody([payload]);
|
|
407
|
+
}
|
|
408
|
+
if (typeof payload === 'object' && payload !== null && 'text' in payload) {
|
|
409
|
+
const text = String((payload as { text?: unknown }).text ?? '').trim();
|
|
410
|
+
return text || '\u200b';
|
|
411
|
+
}
|
|
412
|
+
const text = String(payload).trim();
|
|
413
|
+
return text || '\u200b';
|
|
414
|
+
}
|
|
415
|
+
const joined = payload.map((seg) => {
|
|
416
|
+
if (typeof seg === 'string') return seg;
|
|
417
|
+
const item = seg as IcqqWireSegment;
|
|
418
|
+
switch (item.type) {
|
|
419
|
+
case 'text':
|
|
420
|
+
return String(item.data?.text ?? '');
|
|
421
|
+
case 'at':
|
|
422
|
+
case 'mention':
|
|
423
|
+
return `[at:${item.data?.qq ?? item.data?.id ?? item.data?.target ?? ''}]`;
|
|
424
|
+
case 'image':
|
|
425
|
+
return `[image:${item.data?.file || item.data?.url || base64Media(item.data) || ''}]`;
|
|
426
|
+
case 'face':
|
|
427
|
+
return `[face:${item.data?.id ?? ''}]`;
|
|
428
|
+
case 'reply':
|
|
429
|
+
return `[reply:${item.data?.message_id ?? item.data?.id ?? ''}]`;
|
|
430
|
+
case 'record':
|
|
431
|
+
case 'audio':
|
|
432
|
+
return `[record:${item.data?.file || item.data?.url || base64Media(item.data) || ''}]`;
|
|
433
|
+
case 'video':
|
|
434
|
+
return `[video:${item.data?.file || item.data?.url || base64Media(item.data) || ''}]`;
|
|
435
|
+
default:
|
|
436
|
+
return String(item.data?.text ?? '');
|
|
437
|
+
}
|
|
438
|
+
}).join('').trim();
|
|
439
|
+
return joined || '\u200b';
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export function formatInboundContent(rawMessage: string): string {
|
|
443
|
+
return rawMessage;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** base64 媒体回退:CQ `base64://` 由守护进程解码(异机 RPC 场景)。 */
|
|
447
|
+
function base64Media(data: Record<string, unknown> | undefined): string | undefined {
|
|
448
|
+
const b64 = data?.base64;
|
|
449
|
+
return typeof b64 === 'string' && b64 ? `base64://${b64}` : undefined;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** 段对象判定:`{ type: string, data? }`(非数组 payload 的防御入口)。 */
|
|
453
|
+
function isWireSegment(value: unknown): value is IcqqWireSegment {
|
|
454
|
+
return typeof value === 'object'
|
|
455
|
+
&& value !== null
|
|
456
|
+
&& typeof (value as { type?: unknown }).type === 'string';
|
|
457
|
+
}
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useState } from "react";
|
|
2
|
-
import { Activity, Bot, Endpoint, LogIn, Users, RefreshCw, Wifi, WifiOff, Loader2 } from "lucide-react";
|
|
3
|
-
import LoginAssistPanel from "./LoginAssistPanel";
|
|
4
|
-
import { apiFetch } from "./utils/api";
|
|
5
|
-
|
|
6
|
-
interface EndpointRow {
|
|
7
|
-
name: string;
|
|
8
|
-
connected: boolean;
|
|
9
|
-
groupCount: number;
|
|
10
|
-
friendCount: number;
|
|
11
|
-
loginMode: string;
|
|
12
|
-
status: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
type Tab = "overview" | "login";
|
|
16
|
-
|
|
17
|
-
export default function ICQQManagement() {
|
|
18
|
-
const [tab, setTab] = useState<Tab>("overview");
|
|
19
|
-
const [endpoints, setEndpoints] = useState<EndpointRow[]>([]);
|
|
20
|
-
const [loading, setLoading] = useState(true);
|
|
21
|
-
const [err, setErr] = useState<string | null>(null);
|
|
22
|
-
|
|
23
|
-
const loadBots = useCallback(async () => {
|
|
24
|
-
setLoading(true);
|
|
25
|
-
setErr(null);
|
|
26
|
-
try {
|
|
27
|
-
const res = await apiFetch("/api/icqq/endpoints");
|
|
28
|
-
const json = (await res.json()) as { success?: boolean; data?: EndpointRow[]; message?: string };
|
|
29
|
-
if (!res.ok || !json.success) throw new Error(json.message || "加载失败");
|
|
30
|
-
setEndpoints(Array.isArray(json.data) ? json.data : []);
|
|
31
|
-
} catch (e) {
|
|
32
|
-
setErr((e as Error).message);
|
|
33
|
-
setEndpoints([]);
|
|
34
|
-
} finally {
|
|
35
|
-
setLoading(false);
|
|
36
|
-
}
|
|
37
|
-
}, []);
|
|
38
|
-
|
|
39
|
-
useEffect(() => {
|
|
40
|
-
if (tab === "overview") void loadBots();
|
|
41
|
-
}, [tab, loadBots]);
|
|
42
|
-
|
|
43
|
-
return (
|
|
44
|
-
<div className="p-6 max-w-5xl mx-auto space-y-5">
|
|
45
|
-
{/* Header */}
|
|
46
|
-
<div className="flex items-center justify-between">
|
|
47
|
-
<div>
|
|
48
|
-
<h1 className="text-2xl font-bold flex items-center gap-2">
|
|
49
|
-
<Bot className="w-6 h-6" /> ICQQ 管理
|
|
50
|
-
</h1>
|
|
51
|
-
<p className="text-sm text-muted-foreground mt-1">
|
|
52
|
-
机器人概览与登录辅助(扫码 / 验证码 / 滑块)
|
|
53
|
-
</p>
|
|
54
|
-
</div>
|
|
55
|
-
{tab === "overview" && (
|
|
56
|
-
<button
|
|
57
|
-
onClick={loadBots}
|
|
58
|
-
disabled={loading}
|
|
59
|
-
className="flex items-center gap-1 px-3 py-1.5 rounded bg-blue-500 text-white hover:bg-blue-600 disabled:opacity-50 text-sm"
|
|
60
|
-
>
|
|
61
|
-
<RefreshCw className={`w-4 h-4 ${loading ? "animate-spin" : ""}`} /> 刷新
|
|
62
|
-
</button>
|
|
63
|
-
)}
|
|
64
|
-
</div>
|
|
65
|
-
|
|
66
|
-
{/* Tabs */}
|
|
67
|
-
<div className="flex gap-1 border-b">
|
|
68
|
-
<button
|
|
69
|
-
onClick={() => setTab("overview")}
|
|
70
|
-
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
|
|
71
|
-
tab === "overview"
|
|
72
|
-
? "border-blue-500 text-blue-600"
|
|
73
|
-
: "border-transparent text-gray-500 hover:text-gray-700"
|
|
74
|
-
}`}
|
|
75
|
-
>
|
|
76
|
-
<span className="flex items-center gap-1.5">
|
|
77
|
-
<Bot className="w-4 h-4" /> 概览
|
|
78
|
-
</span>
|
|
79
|
-
</button>
|
|
80
|
-
<button
|
|
81
|
-
onClick={() => setTab("login")}
|
|
82
|
-
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
|
|
83
|
-
tab === "login"
|
|
84
|
-
? "border-blue-500 text-blue-600"
|
|
85
|
-
: "border-transparent text-gray-500 hover:text-gray-700"
|
|
86
|
-
}`}
|
|
87
|
-
>
|
|
88
|
-
<span className="flex items-center gap-1.5">
|
|
89
|
-
<LogIn className="w-4 h-4" /> 登录辅助
|
|
90
|
-
</span>
|
|
91
|
-
</button>
|
|
92
|
-
</div>
|
|
93
|
-
|
|
94
|
-
{/* Overview Tab */}
|
|
95
|
-
{tab === "overview" && (
|
|
96
|
-
<>
|
|
97
|
-
{err && (
|
|
98
|
-
<div className="p-3 bg-red-50 text-red-600 rounded border border-red-200 text-sm dark:bg-red-900/20 dark:text-red-400 dark:border-red-800">
|
|
99
|
-
{err}
|
|
100
|
-
</div>
|
|
101
|
-
)}
|
|
102
|
-
{loading ? (
|
|
103
|
-
<div className="flex justify-center py-12">
|
|
104
|
-
<Loader2 className="w-6 h-6 animate-spin text-blue-500" />
|
|
105
|
-
</div>
|
|
106
|
-
) : endpoints.length === 0 ? (
|
|
107
|
-
<div className="text-center text-muted-foreground py-12">
|
|
108
|
-
暂无 ICQQ Endpoint 实例
|
|
109
|
-
</div>
|
|
110
|
-
) : (
|
|
111
|
-
<div className="grid gap-4 md:grid-cols-2">
|
|
112
|
-
{endpoints.map((endpoint) => (
|
|
113
|
-
<div key={endpoint.name} className="border rounded-lg p-4 bg-card shadow-sm">
|
|
114
|
-
<div className="flex items-center justify-between mb-3">
|
|
115
|
-
<span className="font-medium text-lg">{endpoint.name}</span>
|
|
116
|
-
{endpoint.connected ? (
|
|
117
|
-
<span className="flex items-center gap-1 text-green-600 text-sm">
|
|
118
|
-
<Wifi className="w-4 h-4" /> 在线
|
|
119
|
-
</span>
|
|
120
|
-
) : (
|
|
121
|
-
<span className="flex items-center gap-1 text-gray-400 text-sm">
|
|
122
|
-
<WifiOff className="w-4 h-4" /> 离线
|
|
123
|
-
</span>
|
|
124
|
-
)}
|
|
125
|
-
</div>
|
|
126
|
-
<div className="space-y-2 text-sm text-muted-foreground">
|
|
127
|
-
<div className="flex items-center gap-1.5">
|
|
128
|
-
<Users className="w-3.5 h-3.5" />
|
|
129
|
-
群 {endpoint.groupCount} · 好友 {endpoint.friendCount}
|
|
130
|
-
</div>
|
|
131
|
-
<div className="flex items-center gap-1.5">
|
|
132
|
-
<Activity className="w-3.5 h-3.5" />
|
|
133
|
-
登录方式 {endpoint.loginMode} · {endpoint.status}
|
|
134
|
-
</div>
|
|
135
|
-
</div>
|
|
136
|
-
</div>
|
|
137
|
-
))}
|
|
138
|
-
</div>
|
|
139
|
-
)}
|
|
140
|
-
</>
|
|
141
|
-
)}
|
|
142
|
-
|
|
143
|
-
{/* Login Tab */}
|
|
144
|
-
{tab === "login" && <LoginAssistPanel />}
|
|
145
|
-
</div>
|
|
146
|
-
);
|
|
147
|
-
}
|