@zhin.js/adapter-icqq 1.0.70 → 1.1.0

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.
Files changed (111) hide show
  1. package/CHANGELOG.md +1134 -51
  2. package/README.md +116 -431
  3. package/adapters/icqq.js +31 -0
  4. package/adapters/icqq.ts +42 -0
  5. package/agent/PERMITS.md +24 -0
  6. package/agent/prompt-sections/platform.ts +17 -0
  7. package/commands/endpoint/add/[[id]].js +3 -0
  8. package/commands/endpoint/add/[[id]].ts +3 -0
  9. package/commands/endpoint/list.js +3 -0
  10. package/commands/endpoint/list.ts +3 -0
  11. package/commands/endpoint/remove/[id].js +3 -0
  12. package/commands/endpoint/remove/[id].ts +3 -0
  13. package/lib/client.d.ts +19 -0
  14. package/lib/client.js +3 -0
  15. package/lib/cq-message.d.ts +14 -0
  16. package/lib/cq-message.js +106 -0
  17. package/lib/endpoint.d.ts +31 -0
  18. package/lib/endpoint.js +1046 -0
  19. package/lib/forward-msg.d.ts +6 -0
  20. package/lib/forward-msg.js +107 -0
  21. package/lib/icqq-endpoint-commands.d.ts +1 -0
  22. package/lib/icqq-endpoint-commands.js +26 -0
  23. package/lib/icqq-guild.d.ts +57 -0
  24. package/lib/icqq-guild.js +109 -0
  25. package/lib/icqq-inbound.d.ts +165 -0
  26. package/lib/icqq-inbound.js +651 -0
  27. package/lib/icqq-inbox.d.ts +25 -0
  28. package/lib/icqq-inbox.js +129 -0
  29. package/lib/icqq-login-assist.d.ts +27 -0
  30. package/lib/icqq-login-assist.js +109 -0
  31. package/lib/icqq-runtime-state.d.ts +1 -0
  32. package/lib/icqq-runtime-state.js +6 -0
  33. package/lib/index.d.ts +5 -18
  34. package/lib/index.js +5 -124
  35. package/lib/outbound-media.d.ts +14 -0
  36. package/lib/outbound-media.js +76 -0
  37. package/lib/protocol.d.ts +276 -0
  38. package/lib/protocol.js +425 -0
  39. package/lib/types.d.ts +37 -11
  40. package/lib/types.js +0 -1
  41. package/package.json +80 -32
  42. package/plugin.js +19 -0
  43. package/schema.json +162 -0
  44. package/src/client.ts +29 -0
  45. package/src/cq-message.ts +113 -0
  46. package/src/endpoint.ts +1138 -0
  47. package/src/forward-msg.ts +112 -0
  48. package/src/icqq-endpoint-commands.ts +28 -0
  49. package/src/icqq-guild.ts +154 -0
  50. package/src/icqq-inbound.ts +814 -0
  51. package/src/icqq-inbox.ts +147 -0
  52. package/src/icqq-login-assist.ts +132 -0
  53. package/src/icqq-runtime-state.ts +7 -0
  54. package/src/index.ts +30 -149
  55. package/src/outbound-media.ts +103 -0
  56. package/src/protocol.ts +575 -0
  57. package/src/types.ts +40 -11
  58. package/tools/announce.js +16 -0
  59. package/tools/announce.ts +15 -0
  60. package/tools/essence.js +22 -0
  61. package/tools/essence.ts +20 -0
  62. package/tools/friend_list.js +18 -0
  63. package/tools/friend_list.ts +18 -0
  64. package/tools/group_files.js +22 -0
  65. package/tools/group_files.ts +20 -0
  66. package/tools/group_invite.js +23 -0
  67. package/tools/group_invite.ts +20 -0
  68. package/tools/group_kick.js +26 -0
  69. package/tools/group_kick.ts +22 -0
  70. package/tools/group_list.js +19 -0
  71. package/tools/group_list.ts +19 -0
  72. package/tools/group_member_info.js +24 -0
  73. package/tools/group_member_info.ts +26 -0
  74. package/tools/group_sign.js +19 -0
  75. package/tools/group_sign.ts +17 -0
  76. package/tools/list_muted.js +17 -0
  77. package/tools/list_muted.ts +16 -0
  78. package/tools/poke.js +16 -0
  79. package/tools/poke.ts +15 -0
  80. package/tools/send_user_like.js +25 -0
  81. package/tools/send_user_like.ts +22 -0
  82. package/tools/set_anonymous.js +21 -0
  83. package/tools/set_anonymous.ts +19 -0
  84. package/tools/set_mute.js +24 -0
  85. package/tools/set_mute.ts +21 -0
  86. package/tools/set_title.js +25 -0
  87. package/tools/set_title.ts +22 -0
  88. package/tools/user_info.js +14 -0
  89. package/tools/user_info.ts +13 -0
  90. package/LICENSE +0 -21
  91. package/client/ICQQManagement.tsx +0 -139
  92. package/client/LoginAssistPanel.tsx +0 -293
  93. package/client/index.tsx +0 -11
  94. package/client/tsconfig.json +0 -7
  95. package/client/utils/api.ts +0 -20
  96. package/dist/index.js +0 -1
  97. package/lib/adapter.d.ts +0 -34
  98. package/lib/adapter.d.ts.map +0 -1
  99. package/lib/adapter.js +0 -396
  100. package/lib/adapter.js.map +0 -1
  101. package/lib/bot.d.ts +0 -46
  102. package/lib/bot.d.ts.map +0 -1
  103. package/lib/bot.js +0 -457
  104. package/lib/bot.js.map +0 -1
  105. package/lib/index.d.ts.map +0 -1
  106. package/lib/index.js.map +0 -1
  107. package/lib/types.d.ts.map +0 -1
  108. package/lib/types.js.map +0 -1
  109. package/skills/icqq/SKILL.md +0 -22
  110. package/src/adapter.ts +0 -448
  111. package/src/bot.ts +0 -521
@@ -0,0 +1,575 @@
1
+ import { pickCredential } from 'zhin.js/adapter';
2
+ import type { MessageElem, Sendable } from '@icqqjs/icqq';
3
+ import type { ConversationRef } from '@zhin.js/im-contract';
4
+ import type { MessageSegment } from "zhin.js";
5
+ import { resolveIcqqMediaFile } from "./cq-message.js";
6
+
7
+ export type ActionResult = {
8
+ id: string;
9
+ ok: boolean;
10
+ data?: unknown;
11
+ error?: string;
12
+ };
13
+
14
+ export type { IcqqMessageEvent } from "./icqq-inbound.js";
15
+
16
+ export { resolveIcqqInboundMessageId } from "./icqq-inbound.js";
17
+
18
+ export interface GuildMessageEventData {
19
+ type: "guild";
20
+ guild_id: string;
21
+ guild_name: string;
22
+ channel_id: string;
23
+ channel_name: string;
24
+ nickname: string;
25
+ tiny_id: string;
26
+ raw_message: string;
27
+ time: number;
28
+ seq: number;
29
+ }
30
+
31
+ /**
32
+ * 所有操作常量(@icqqjs/icqq)
33
+ */
34
+ export const Actions = {
35
+ PING: "ping",
36
+ LOGOUT: "logout",
37
+
38
+ // ── 列表查询 ──
39
+ LIST_FRIENDS: "list_friends",
40
+ LIST_GROUPS: "list_groups",
41
+ LIST_GROUP_MEMBERS: "list_group_members",
42
+ LIST_BLACKLIST: "list_blacklist",
43
+ LIST_FRIEND_CLASSES: "list_friend_classes",
44
+
45
+ // ── 信息查询 ──
46
+ GET_FRIEND_INFO: "get_friend_info",
47
+ GET_GROUP_INFO: "get_group_info",
48
+ GET_GROUP_MEMBER_INFO: "get_group_member_info",
49
+ GET_STRANGER_INFO: "get_stranger_info",
50
+ LIST_STRANGERS: "list_strangers",
51
+ GET_PROFILE: "get_profile",
52
+ GET_STATUS: "get_status",
53
+ GET_ONLINE_STATUS: "get_online_status",
54
+ GET_SELF_PROFILE: "get_self_profile",
55
+
56
+ // ── 消息发送 ──
57
+ SEND_PRIVATE_MSG: "send_private_msg",
58
+ SEND_GROUP_MSG: "send_group_msg",
59
+
60
+ // ── 消息操作 ──
61
+ RECALL_MSG: "recall_msg",
62
+ GET_MSG: "get_msg",
63
+ HISTORY_PRIVATE: "history_private",
64
+ HISTORY_GROUP: "history_group",
65
+ HISTORY_BY_MSG_ID: "history_by_msg_id",
66
+ SEND_LONG_MSG: "send_long_msg",
67
+ MARK_READ: "mark_read",
68
+ DELETE_MSG: "delete_msg",
69
+
70
+ // ── 个人设置 ──
71
+ SET_NICKNAME: "set_nickname",
72
+ SET_GENDER: "set_gender",
73
+ SET_BIRTHDAY: "set_birthday",
74
+ SET_SIGNATURE: "set_signature",
75
+ SET_DESCRIPTION: "set_description",
76
+ SET_AVATAR: "set_avatar",
77
+ SET_ONLINE_STATUS: "set_online_status",
78
+
79
+ // ── 群设置 ──
80
+ SET_GROUP_NAME: "set_group_name",
81
+ SET_GROUP_AVATAR: "set_group_avatar",
82
+ SET_GROUP_CARD: "set_group_card",
83
+ SET_GROUP_TITLE: "set_group_title",
84
+ SET_GROUP_ADMIN: "set_group_admin",
85
+ SET_GROUP_REMARK: "set_group_remark",
86
+
87
+ // ── 群管理 ──
88
+ GROUP_MUTE: "group_mute",
89
+ GROUP_MUTE_ALL: "group_mute_all",
90
+ GROUP_KICK: "group_kick",
91
+ GROUP_QUIT: "group_quit",
92
+ GROUP_INVITE: "group_invite",
93
+ GROUP_POKE: "group_poke",
94
+ GROUP_ANNOUNCE: "group_announce",
95
+ GROUP_SIGN: "group_sign",
96
+ GROUP_ESSENCE_ADD: "group_essence_add",
97
+ GROUP_ESSENCE_REMOVE: "group_essence_remove",
98
+ GROUP_ALLOW_ANONY: "group_allow_anony",
99
+ GROUP_MUTED_LIST: "group_muted_list",
100
+ GROUP_AT_ALL_REMAIN: "group_at_all_remain",
101
+
102
+ // ── 好友操作 ──
103
+ FRIEND_POKE: "friend_poke",
104
+ FRIEND_LIKE: "friend_like",
105
+ FRIEND_DELETE: "friend_delete",
106
+ FRIEND_REMARK: "friend_remark",
107
+ FRIEND_CLASS: "friend_class",
108
+
109
+ // ── 系统消息/请求 ──
110
+ GET_SYSTEM_MSG: "get_system_msg",
111
+ HANDLE_FRIEND_REQUEST: "handle_friend_request",
112
+ HANDLE_GROUP_REQUEST: "handle_group_request",
113
+
114
+ // ── 好友分组 ──
115
+ ADD_FRIEND_CLASS: "add_friend_class",
116
+ DELETE_FRIEND_CLASS: "delete_friend_class",
117
+ RENAME_FRIEND_CLASS: "rename_friend_class",
118
+
119
+ // ── 群文件系统 ──
120
+ GFS_LIST: "gfs_list",
121
+ GFS_INFO: "gfs_info",
122
+ GFS_MKDIR: "gfs_mkdir",
123
+ GFS_DELETE: "gfs_delete",
124
+ GFS_RENAME: "gfs_rename",
125
+ GFS_STAT: "gfs_stat",
126
+ GFS_MOVE: "gfs_move",
127
+ GFS_DOWNLOAD: "gfs_download",
128
+
129
+ // ── 其他功能 ──
130
+ IMAGE_OCR: "image_ocr",
131
+ RELOAD_FRIEND_LIST: "reload_friend_list",
132
+ RELOAD_GROUP_LIST: "reload_group_list",
133
+ CLEAN_CACHE: "clean_cache",
134
+ GET_GROUP_SHARE: "get_group_share",
135
+
136
+ // ── 群管理扩展 ──
137
+ GROUP_SET_JOIN_TYPE: "group_set_join_type",
138
+ GROUP_SET_RATE_LIMIT: "group_set_rate_limit",
139
+ GROUP_MUTE_ANONY: "group_mute_anony",
140
+ GROUP_ANON_INFO: "group_anon_info",
141
+
142
+ // ── 好友操作扩展 ──
143
+ ADD_FRIEND: "add_friend",
144
+ SEND_TEMP_MSG: "send_temp_msg",
145
+
146
+ // ── 漫游表情 ──
147
+ GET_ROAMING_STAMP: "get_roaming_stamp",
148
+ DELETE_STAMP: "delete_stamp",
149
+
150
+ // ── 文件传输 ──
151
+ SEND_PRIVATE_FILE: "send_private_file",
152
+ SEND_GROUP_FILE: "send_group_file",
153
+ FRIEND_RECALL_FILE: "friend_recall_file",
154
+ FRIEND_FORWARD_FILE: "friend_forward_file",
155
+ SEARCH_SAME_GROUP: "search_same_group",
156
+ SEND_CONTACT_SHARE: "send_contact_share",
157
+ GFS_UPLOAD: "gfs_upload",
158
+
159
+ // ── 群消息表态 ──
160
+ GROUP_SET_REACTION: "group_set_reaction",
161
+ GROUP_DEL_REACTION: "group_del_reaction",
162
+
163
+ // ── 转发消息 ──
164
+ GET_FORWARD_MSG: "get_forward_msg",
165
+ MAKE_FORWARD_MSG: "make_forward_msg",
166
+
167
+ // ── 频道系统 ──
168
+ GUILD_LIST: "guild_list",
169
+ GUILD_INFO: "guild_info",
170
+ GUILD_CHANNELS: "guild_channels",
171
+ GET_CHANNEL_INFO: "get_channel_info",
172
+ GUILD_MEMBERS: "guild_members",
173
+ GUILD_SEND_MSG: "guild_send_msg",
174
+ GUILD_RECALL_MSG: "guild_recall_msg",
175
+
176
+ // ── 用户文件操作 ──
177
+ GET_FILE_INFO: "get_file_info",
178
+ GET_FILE_URL: "get_file_url",
179
+ GET_AVATAR_URL: "get_avatar_url",
180
+ GET_GROUP_AVATAR_URL: "get_group_avatar_url",
181
+
182
+ // ── 屏蔽群成员消息 ──
183
+ SET_SCREEN_MEMBER_MSG: "set_screen_member_msg",
184
+
185
+ // ── 群文件转发 ──
186
+ GFS_FORWARD: "gfs_forward",
187
+ GFS_FORWARD_OFFLINE: "gfs_forward_offline",
188
+
189
+ // ── 重载列表 ──
190
+ RELOAD_BLACKLIST: "reload_blacklist",
191
+ RELOAD_STRANGER_LIST: "reload_stranger_list",
192
+ RELOAD_GUILDS: "reload_guilds",
193
+
194
+ // ── 在线状态查询 ──
195
+ GET_STATUS_INFO: "get_status_info",
196
+
197
+ // ── 密钥/工具 ──
198
+ GET_CLIENT_KEY: "get_client_key",
199
+ GET_PSKEY: "get_pskey",
200
+ UID2UIN: "uid2uin",
201
+ UID2UINS: "uid2uins",
202
+ UIN2UID: "uin2uid",
203
+ UIN2UIDS: "uin2uids",
204
+ GET_COOKIES: "get_cookies",
205
+ GET_CSRF_TOKEN: "get_csrf_token",
206
+ REFRESH_NT_PIC_RKEY: "refresh_nt_pic_rkey",
207
+ SEND_DISCUSS_MSG: "send_discuss_msg",
208
+
209
+ // ── 视频/加好友设置 ──
210
+ GET_VIDEO_URL: "get_video_url",
211
+ GET_ADD_FRIEND_SETTING: "get_add_friend_setting",
212
+
213
+ // ── 频道扩展 ──
214
+ GET_FORUM_URL: "get_forum_url",
215
+ GUILD_CHANNEL_SHARE: "guild_channel_share",
216
+
217
+ // ── 获取图片/语音 URL ──
218
+ GET_PIC_URL: "get_pic_url",
219
+ GET_PTT_URL: "get_ptt_url",
220
+
221
+ // ── 消息订阅 ──
222
+ SUBSCRIBE: "subscribe",
223
+ UNSUBSCRIBE: "unsubscribe",
224
+
225
+ // ── Webhook ──
226
+ SET_WEBHOOK: "set_webhook",
227
+ GET_WEBHOOK: "get_webhook",
228
+
229
+ // ── 系统通知 ──
230
+ SET_NOTIFY: "set_notify",
231
+ GET_NOTIFY: "get_notify",
232
+ } as const;
233
+
234
+ /** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
235
+ export interface IcqqAdapterConfig {
236
+ readonly id?: string;
237
+ readonly autoReconnect?: boolean;
238
+ readonly outboundMedia?: 'file' | 'base64';
239
+ readonly password?: string;
240
+ readonly platform?: number;
241
+ readonly ver?: string;
242
+ readonly dataDir?: string;
243
+ readonly signApiAddr?: string;
244
+ readonly ignoreSelf?: boolean;
245
+ readonly resend?: boolean;
246
+ readonly cacheGroupMember?: boolean;
247
+ readonly autoServer?: boolean;
248
+ readonly qqnt?: boolean;
249
+ readonly ntLogin?: boolean;
250
+ /** Transitional: legacy root `endpoints[]` with `context: icqq`. */
251
+ readonly endpoints?: ReadonlyArray<{
252
+ readonly context?: string;
253
+ readonly id?: string;
254
+ readonly autoReconnect?: boolean;
255
+ readonly outboundMedia?: 'file' | 'base64';
256
+ readonly password?: string;
257
+ readonly platform?: number;
258
+ readonly ver?: string;
259
+ readonly dataDir?: string;
260
+ readonly signApiAddr?: string;
261
+ readonly ignoreSelf?: boolean;
262
+ readonly resend?: boolean;
263
+ readonly cacheGroupMember?: boolean;
264
+ readonly autoServer?: boolean;
265
+ readonly qqnt?: boolean;
266
+ readonly ntLogin?: boolean;
267
+ }>;
268
+ }
269
+
270
+ export interface ResolvedIcqqConfig {
271
+ readonly context: 'icqq';
272
+ readonly id: string;
273
+ readonly autoReconnect: boolean;
274
+ readonly outboundMedia?: 'file' | 'base64';
275
+ readonly password?: string;
276
+ readonly platform?: number;
277
+ readonly ver?: string;
278
+ readonly dataDir?: string;
279
+ readonly signApiAddr?: string;
280
+ readonly ignoreSelf?: boolean;
281
+ readonly resend?: boolean;
282
+ readonly cacheGroupMember?: boolean;
283
+ readonly autoServer?: boolean;
284
+ readonly qqnt?: boolean;
285
+ readonly ntLogin?: boolean;
286
+ }
287
+
288
+ export interface IcqqWireSegment {
289
+ readonly type: string;
290
+ readonly data?: Record<string, unknown>;
291
+ }
292
+
293
+ export interface IcqqInboundMessage {
294
+ readonly id: string;
295
+ readonly conversation: ConversationRef;
296
+ readonly content: string;
297
+ /**
298
+ * 结构化段视图(canonical Segment,经 toCanonicalSegments 归一)。
299
+ * 与 content(CQ 原文纯文本视图)同源,随 gateway.receive 上送。
300
+ */
301
+ readonly segments?: readonly MessageSegment[];
302
+ readonly sender: { readonly id: string; readonly name?: string };
303
+ readonly channelType: 'private' | 'group' | 'channel';
304
+ readonly metadata?: Record<string, unknown>;
305
+ readonly replyTo?: { readonly id: string };
306
+ }
307
+
308
+ export type ParsedIcqqSendTarget =
309
+ | { readonly kind: 'private'; readonly userId: number }
310
+ | { readonly kind: 'group'; readonly groupId: number }
311
+ | { readonly kind: 'temp'; readonly groupId: number; readonly userId: number }
312
+ | { readonly kind: 'channel'; readonly guildId: string; readonly channelId: string };
313
+
314
+ export function resolveIcqqConfig(config: IcqqAdapterConfig = {}): ResolvedIcqqConfig {
315
+ const entry = config.endpoints?.find((item) => item.context === 'icqq');
316
+ const id = pickCredential(config.id, entry?.id, process.env.ICQQ_ACCOUNT);
317
+ if (!/^\d+$/.test(id)) {
318
+ throw new TypeError(
319
+ 'ICQQ adapter requires numeric id (QQ uin) via plugins.<key>.id or ICQQ_ACCOUNT',
320
+ );
321
+ }
322
+ const autoReconnect = config.autoReconnect ?? entry?.autoReconnect ?? true;
323
+ const outboundMedia = config.outboundMedia ?? entry?.outboundMedia;
324
+ const password = config.password ?? entry?.password;
325
+ const platform = config.platform ?? entry?.platform ?? 2;
326
+ const ver = config.ver ?? entry?.ver ?? '9.1.70';
327
+ const dataDir = config.dataDir ?? entry?.dataDir ?? `data/icqq/${id}`;
328
+ const signApiAddr = config.signApiAddr ?? entry?.signApiAddr;
329
+ const ignoreSelf = config.ignoreSelf ?? entry?.ignoreSelf ?? true;
330
+ const resend = config.resend ?? entry?.resend;
331
+ const cacheGroupMember = config.cacheGroupMember ?? entry?.cacheGroupMember ?? true;
332
+ const autoServer = config.autoServer ?? entry?.autoServer ?? true;
333
+ const qqnt = config.qqnt ?? entry?.qqnt;
334
+ const ntLogin = config.ntLogin ?? entry?.ntLogin;
335
+ return {
336
+ context: 'icqq',
337
+ id,
338
+ autoReconnect,
339
+ ...(outboundMedia ? { outboundMedia } : {}),
340
+ ...(password ? { password } : {}),
341
+ ...(platform != null ? { platform: Number(platform) } : {}),
342
+ ...(ver ? { ver } : {}),
343
+ ...(dataDir ? { dataDir } : {}),
344
+ ...(signApiAddr ? { signApiAddr } : {}),
345
+ ...(ignoreSelf != null ? { ignoreSelf } : {}),
346
+ ...(resend != null ? { resend } : {}),
347
+ ...(cacheGroupMember != null ? { cacheGroupMember } : {}),
348
+ ...(autoServer != null ? { autoServer } : {}),
349
+ ...(qqnt != null ? { qqnt } : {}),
350
+ ...(ntLogin != null ? { ntLogin } : {}),
351
+ };
352
+ }
353
+
354
+ /**
355
+ * 入站归一化 → ConversationRef:群临时会话(temp)映射为群容器内的 private
356
+ * 会话(parent kind 'group');频道消息的所属 guild 进 `parent`(kind 'channel')。
357
+ */
358
+ export function icqqInboundConversation(endpointKey: string, input: {
359
+ readonly channelType: 'private' | 'group' | 'channel';
360
+ readonly channelId: string;
361
+ readonly channelParentGroupId?: string;
362
+ readonly guildId?: string;
363
+ }): ConversationRef {
364
+ const endpoint = { id: endpointKey, adapter: endpointKey.split('\0')[0] ?? endpointKey };
365
+ if (input.channelType === 'private' && input.channelParentGroupId) {
366
+ return {
367
+ endpoint,
368
+ kind: 'private',
369
+ id: input.channelId,
370
+ parent: { kind: 'group', id: input.channelParentGroupId },
371
+ };
372
+ }
373
+ if (input.channelType === 'channel' && input.guildId) {
374
+ return {
375
+ endpoint,
376
+ kind: 'channel',
377
+ id: input.channelId,
378
+ parent: { kind: 'channel', id: input.guildId },
379
+ };
380
+ }
381
+ return { endpoint, kind: input.channelType, id: input.channelId };
382
+ }
383
+
384
+ /**
385
+ * 出站:从 ConversationRef 派生 action 路由。
386
+ * 群容器(parent kind 'group')内的 private 会话即 ICQQ 群临时会话(temp)。
387
+ */
388
+ export function icqqOutboundTarget(conversation: ConversationRef): ParsedIcqqSendTarget {
389
+ if (conversation.kind === 'private' && conversation.parent?.kind === 'group') {
390
+ return {
391
+ kind: 'temp',
392
+ groupId: Number(conversation.parent.id),
393
+ userId: Number(conversation.id),
394
+ };
395
+ }
396
+ if (conversation.kind === 'private') {
397
+ return { kind: 'private', userId: Number(conversation.id) };
398
+ }
399
+ if (conversation.kind === 'group') {
400
+ return { kind: 'group', groupId: Number(conversation.id) };
401
+ }
402
+ const guildId = conversation.parent?.kind === 'channel' ? conversation.parent.id : undefined;
403
+ if (!guildId) {
404
+ throw new TypeError(`Invalid icqq channel conversation (missing guild parent): ${conversation.id}`);
405
+ }
406
+ return { kind: 'channel', guildId, channelId: conversation.id };
407
+ }
408
+
409
+ const ICQQ_STANDALONE_ELEMENTS = new Set([
410
+ 'record',
411
+ 'video',
412
+ 'file',
413
+ 'json',
414
+ 'long_msg',
415
+ ] as const);
416
+
417
+ /**
418
+ * Project canonical Zhin segments directly into the native ICQQ Sendable
419
+ * algebra. ICQQ's direct client does not parse Zhin's historical `[image:x]`
420
+ * notation; passing that notation would send it as literal text.
421
+ */
422
+ export function formatOutboundBody(payload: unknown): Sendable {
423
+ if (typeof payload === 'string') {
424
+ const text = payload.trim();
425
+ return text || '\u200b';
426
+ }
427
+ if (payload == null) return '\u200b';
428
+ if (!Array.isArray(payload)) {
429
+ // 单个段对象(非数组)按单元素数组处理,杜绝 String(object) → '[object Object]'
430
+ if (isWireSegment(payload)) {
431
+ return formatOutboundBody([payload]);
432
+ }
433
+ if (typeof payload === 'object' && payload !== null && 'text' in payload) {
434
+ const text = String((payload as { text?: unknown }).text ?? '').trim();
435
+ return text || '\u200b';
436
+ }
437
+ const text = String(payload).trim();
438
+ return text || '\u200b';
439
+ }
440
+ const elements = payload.flatMap<string | MessageElem>((seg) => {
441
+ if (typeof seg === 'string') return seg;
442
+ const item = seg as IcqqWireSegment;
443
+ switch (item.type) {
444
+ case 'text':
445
+ return String(item.data?.text ?? '');
446
+ case 'at':
447
+ case 'mention': {
448
+ const target = item.data?.qq ?? item.data?.id ?? item.data?.target;
449
+ if (target === 'all') return { type: 'at', qq: 'all' };
450
+ const qq = Number(target);
451
+ if (!Number.isSafeInteger(qq) || qq <= 0) {
452
+ throw new TypeError(`Invalid ICQQ mention target: ${String(target ?? '')}`);
453
+ }
454
+ return { type: 'at', qq };
455
+ }
456
+ case 'image': {
457
+ const arg = resolveIcqqMediaFile(item.data);
458
+ if (!arg) throw new TypeError('ICQQ image segment requires canonical data.media');
459
+ return { type: 'image', file: arg };
460
+ }
461
+ case 'face': {
462
+ const id = Number(item.data?.id);
463
+ if (!Number.isSafeInteger(id) || id < 0) {
464
+ throw new TypeError(`Invalid ICQQ face id: ${String(item.data?.id ?? '')}`);
465
+ }
466
+ return { type: 'face', id };
467
+ }
468
+ case 'reply': {
469
+ const id = String(item.data?.message_id ?? item.data?.id ?? '').trim();
470
+ if (!id) throw new TypeError('ICQQ reply segment requires message_id');
471
+ return { type: 'reply', id };
472
+ }
473
+ case 'record':
474
+ case 'audio': {
475
+ const arg = resolveIcqqMediaFile(item.data);
476
+ if (!arg) throw new TypeError('ICQQ audio segment requires canonical data.media');
477
+ return { type: 'record', file: arg };
478
+ }
479
+ case 'video': {
480
+ const arg = resolveIcqqMediaFile(item.data);
481
+ if (!arg) throw new TypeError('ICQQ video segment requires canonical data.media');
482
+ return { type: 'video', file: arg };
483
+ }
484
+ case 'file': {
485
+ const arg = resolveIcqqMediaFile(item.data);
486
+ if (!arg) throw new TypeError('ICQQ file segment requires canonical data.media');
487
+ const name = String(item.data?.name ?? '').trim();
488
+ return { type: 'file', file: arg, ...(name ? { name } : {}) };
489
+ }
490
+ case 'dice':
491
+ return { type: 'dice' };
492
+ case 'rps':
493
+ return { type: 'rps' };
494
+ case 'forward': {
495
+ const resid = String(item.data?.forward_id ?? item.data?.resid ?? item.data?.id ?? '').trim();
496
+ if (!resid) throw new TypeError('ICQQ forward segment requires forward_id');
497
+ return { type: 'long_msg', resid };
498
+ }
499
+ case 'share':
500
+ return formatMusicShareJson(item.data);
501
+ default: {
502
+ const text = item.data?.text;
503
+ if (typeof text === 'string' && text) return text;
504
+ throw new TypeError(`Unsupported ICQQ outbound segment: ${item.type}`);
505
+ }
506
+ }
507
+ }).filter((element) => typeof element !== 'string' || element.length > 0);
508
+
509
+ if (elements.length === 0) return '\u200b';
510
+ const standalone = elements.find(
511
+ (element): element is MessageElem =>
512
+ typeof element !== 'string' && ICQQ_STANDALONE_ELEMENTS.has(element.type as never),
513
+ );
514
+ if (standalone && elements.length !== 1) {
515
+ throw new TypeError(`ICQQ ${standalone.type} element must be sent as a standalone message`);
516
+ }
517
+ return elements.length === 1 ? elements[0]! : elements;
518
+ }
519
+
520
+ export function formatInboundContent(rawMessage: string): string {
521
+ return rawMessage;
522
+ }
523
+
524
+ /** 段对象判定:`{ type: string, data? }`(非数组 payload 的防御入口)。 */
525
+ function isWireSegment(value: unknown): value is IcqqWireSegment {
526
+ return typeof value === 'object'
527
+ && value !== null
528
+ && typeof (value as { type?: unknown }).type === 'string';
529
+ }
530
+
531
+ function formatMusicShareJson(data: Record<string, unknown> | undefined): MessageElem {
532
+ if (!data) throw new TypeError('ICQQ share segment requires data');
533
+ const config = data.config as {
534
+ appid?: number;
535
+ package?: string;
536
+ icon?: string;
537
+ sign?: string;
538
+ } | undefined;
539
+ const title = String(data.title ?? '');
540
+ const desc = String(data.artist ?? '');
541
+ const jumpUrl = String(data.url ?? '');
542
+ const musicUrl = String(data.audio ?? '');
543
+ const preview = String(data.image ?? '');
544
+ const tag = config?.appid === 100497308 ? 'QQ音乐'
545
+ : config?.appid === 100495085 ? '网易云音乐'
546
+ : config?.appid === 100243533 ? '酷我音乐'
547
+ : config?.appid === 205141 ? '酷狗音乐'
548
+ : '音乐';
549
+ const json = {
550
+ app: 'com.tencent.structmsg',
551
+ view: 'music',
552
+ ver: '0.0.0.1',
553
+ desc: 'music',
554
+ prompt: `[分享]${title}`,
555
+ meta: {
556
+ music: {
557
+ action: '',
558
+ android_pkg_name: config?.package ?? '',
559
+ app_type: 1,
560
+ appid: config?.appid ?? 0,
561
+ ctime: 0,
562
+ desc,
563
+ jumpUrl,
564
+ musicUrl,
565
+ preview,
566
+ sourceMsgId: '0',
567
+ source_icon: config?.icon ?? '',
568
+ source_url: '',
569
+ tag,
570
+ title,
571
+ },
572
+ },
573
+ };
574
+ return { type: 'json', data: json };
575
+ }
package/src/types.ts CHANGED
@@ -1,9 +1,4 @@
1
- /**
2
- * ICQQ 适配器类型与配置
3
- */
4
- import type { Config, GroupRole, MemberInfo } from "@icqqjs/icqq";
5
-
6
- export type { Config, GroupRole, MemberInfo };
1
+ export type GroupRole = "owner" | "admin" | "member";
7
2
 
8
3
  export interface IcqqSenderInfo {
9
4
  id: string;
@@ -16,13 +11,47 @@ export interface IcqqSenderInfo {
16
11
  title?: string;
17
12
  }
18
13
 
19
- export interface IcqqBotConfig extends Config {
14
+ export interface IcqqEndpointConfig {
20
15
  context: "icqq";
21
16
  name: `${number}`;
22
- password?: string;
23
- scope?: string;
17
+ autoReconnect?: boolean;
18
+ outboundMedia?: "file" | "base64";
19
+ }
20
+
21
+ export interface FriendInfo {
22
+ user_id: number;
23
+ nickname: string;
24
+ remark?: string;
25
+ class_id?: number;
26
+ }
27
+
28
+ export interface GroupInfo {
29
+ group_id: number;
30
+ group_name: string;
31
+ member_count: number;
32
+ max_member_count: number;
33
+ owner_id?: number;
34
+ }
35
+
36
+ export interface MemberInfo {
37
+ user_id: number;
38
+ nickname: string;
39
+ card: string;
40
+ role: GroupRole;
41
+ title: string;
42
+ join_time?: number;
43
+ last_sent_time?: number;
44
+ shutup_time?: number;
24
45
  }
25
46
 
26
- export interface IcqqBot {
27
- $config: IcqqBotConfig;
47
+ export interface SystemMessage {
48
+ type: string;
49
+ user_id?: number;
50
+ nickname?: string;
51
+ group_id?: number;
52
+ group_name?: string;
53
+ comment?: string;
54
+ flag?: string;
55
+ seq?: number;
56
+ time?: number;
28
57
  }
@@ -0,0 +1,16 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { defineAgentTool } from '@zhin.js/tool';
3
+ import { z } from 'zod';
4
+ export default defineAgentTool({
5
+ description: '发送 QQ 群公告(需要管理员权限)',
6
+ inputSchema: z.object({
7
+ group_id: z.number().describe('目标群号'),
8
+ content: z.string().describe('公告内容'),
9
+ }),
10
+ adapter: 'icqq',
11
+ permissions: ['role(master,admin,trusted,owner)'],
12
+ async execute({ group_id, content }, context) {
13
+ await context.$client.sendGroupNotice(group_id, content);
14
+ return { success: true, message: '群公告已发送' };
15
+ },
16
+ });
@@ -0,0 +1,15 @@
1
+ import { defineAgentTool } from '@zhin.js/tool';
2
+ import { z } from 'zod';
3
+ export default defineAgentTool<{ group_id: number; content: string }>({
4
+ description: '发送 QQ 群公告(需要管理员权限)',
5
+ inputSchema: z.object({
6
+ group_id: z.number().describe('目标群号'),
7
+ content: z.string().describe('公告内容'),
8
+ }),
9
+ adapter: 'icqq',
10
+ permissions: ['role(master,admin,trusted,owner)'],
11
+ async execute({ group_id, content }, context) {
12
+ await context.$client.sendGroupNotice(group_id, content);
13
+ return { success: true, message: '群公告已发送' };
14
+ },
15
+ });
@@ -0,0 +1,22 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { defineAgentTool } from '@zhin.js/tool';
3
+ import { z } from 'zod';
4
+ export default defineAgentTool({
5
+ description: '设置或移除 QQ 群精华消息',
6
+ inputSchema: z.object({
7
+ message_id: z.string().describe('消息 ID'),
8
+ action: z.enum(['add', 'remove']).describe('add=设为精华, remove=移除精华'),
9
+ }),
10
+ adapter: 'icqq',
11
+ approval: 'always',
12
+ async execute({ message_id, action }, context) {
13
+ const client = context.$client;
14
+ if (action === 'add') {
15
+ await client.setEssenceMessage(message_id);
16
+ }
17
+ else {
18
+ await client.removeEssenceMessage(message_id);
19
+ }
20
+ return { success: true, message: action === 'add' ? '已设为精华' : '已移除精华' };
21
+ },
22
+ });