@zhin.js/adapter-icqq 1.0.71 → 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 +1125 -52
  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,112 @@
1
+ import {
2
+ icqqElementsToSegments,
3
+ type IcqqMessageElement,
4
+ } from "./icqq-inbound.js";
5
+ import { toCanonicalSegments } from '@zhin.js/core';
6
+ import type { ForwardEntry, Segment } from '@zhin.js/im-contract';
7
+
8
+ const RESID_IN_XML_RE =
9
+ /(?:m_resid|resid|fileid|res_id)=["']?([A-Za-z0-9+/=_.-]{8,})/i;
10
+
11
+ function parseJsonPayload(raw: unknown): Record<string, unknown> | null {
12
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
13
+ return raw as Record<string, unknown>;
14
+ }
15
+ if (typeof raw !== "string" || !raw.trim()) return null;
16
+ try {
17
+ const parsed = JSON.parse(raw) as unknown;
18
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
19
+ return parsed as Record<string, unknown>;
20
+ }
21
+ } catch {
22
+ // ignore
23
+ }
24
+ return null;
25
+ }
26
+
27
+ /** 从 QQ 合并转发 json 元素提取 resid */
28
+ export function extractForwardResidFromJsonElement(
29
+ el: IcqqMessageElement,
30
+ ): string | undefined {
31
+ const record =
32
+ parseJsonPayload(el.data) ??
33
+ parseJsonPayload(el.text) ??
34
+ parseJsonPayload(el);
35
+ if (!record) {
36
+ const text = typeof el.text === "string" ? el.text : "";
37
+ const m = text.match(RESID_IN_XML_RE);
38
+ return m?.[1]?.trim();
39
+ }
40
+
41
+ const bytes = record.bytesData ?? record.bytes_data ?? record.bytes;
42
+ if (typeof bytes === "string" && bytes.trim()) {
43
+ const inner = parseJsonPayload(bytes);
44
+ if (inner) {
45
+ const nested = extractForwardResidFromJsonElement({
46
+ type: "json",
47
+ data: inner,
48
+ });
49
+ if (nested) return nested;
50
+ }
51
+ const m = bytes.match(RESID_IN_XML_RE);
52
+ if (m?.[1]) return m[1].trim();
53
+ }
54
+
55
+ const app = String(record.app ?? record.App ?? "");
56
+ if (app.includes("multimsg") || app.includes("MultiMsg")) {
57
+ const meta = record.meta as Record<string, unknown> | undefined;
58
+ const detail = meta?.detail as Record<string, unknown> | undefined;
59
+ const resid = detail?.resid ?? detail?.ResID ?? detail?.resId;
60
+ if (resid != null && String(resid).trim()) return String(resid).trim();
61
+ }
62
+
63
+ const prompt = record.prompt as string | undefined;
64
+ if (prompt) {
65
+ const m = prompt.match(RESID_IN_XML_RE);
66
+ if (m?.[1]) return m[1].trim();
67
+ }
68
+
69
+ const direct = record.resid ?? record.m_resid ?? record.fileid;
70
+ if (direct != null && String(direct).trim()) return String(direct).trim();
71
+
72
+ return undefined;
73
+ }
74
+
75
+ /** Canonical merged-forward entries. Speakers are data actors, never LLM roles. */
76
+ export function normalizeForwardMsgResponse(data: unknown): readonly ForwardEntry[] {
77
+ const root = data && typeof data === 'object' && !Array.isArray(data)
78
+ ? data as Record<string, unknown>
79
+ : undefined;
80
+ const list: unknown[] = Array.isArray(data)
81
+ ? data
82
+ : Array.isArray(root?.messages) ? root.messages as unknown[]
83
+ : Array.isArray(root?.msgList) ? root.msgList as unknown[]
84
+ : Array.isArray(root?.msg_list) ? root.msg_list as unknown[]
85
+ : Array.isArray(root?.message) ? root.message as unknown[]
86
+ : [];
87
+ return Object.freeze(list.flatMap((item): ForwardEntry[] => {
88
+ if (!item || typeof item !== 'object') return [];
89
+ const row = item as Record<string, unknown>;
90
+ const sender = row.sender && typeof row.sender === 'object'
91
+ ? row.sender as Record<string, unknown>
92
+ : row.user && typeof row.user === 'object'
93
+ ? row.user as Record<string, unknown>
94
+ : undefined;
95
+ const id = sender?.user_id ?? sender?.uin ?? sender?.id;
96
+ const displayName = sender?.nickname ?? sender?.card ?? sender?.name;
97
+ const body = row.message ?? row.content ?? row.elements ?? row.raw_message;
98
+ let segments: Segment[];
99
+ if (Array.isArray(body)) segments = toCanonicalSegments(icqqElementsToSegments(body as IcqqMessageElement[]) ?? body);
100
+ else if (typeof body === 'string' && body.trim()) segments = [{ type: 'text', data: { text: body.trim() } }];
101
+ else segments = [];
102
+ if (segments.length === 0) return [];
103
+ return [Object.freeze({
104
+ ...(id != null ? { actor: Object.freeze({
105
+ id: String(id),
106
+ ...(displayName != null && String(displayName).trim() ? { displayName: String(displayName) } : {}),
107
+ }) } : {}),
108
+ ...(typeof row.time === 'number' ? { timestamp: row.time < 1e12 ? row.time * 1000 : row.time } : {}),
109
+ segments: Object.freeze(segments),
110
+ })];
111
+ }));
112
+ }
@@ -0,0 +1,28 @@
1
+ import { addEndpointToConfig, createEndpointCommands } from 'zhin.js/adapter';
2
+ import { defineCommand } from 'zhin.js/command';
3
+ import { icqqRuntimeStateToken } from './icqq-runtime-state.js';
4
+
5
+ export const icqqEndpointCommands = createEndpointCommands({
6
+ adapterKey: 'icqq',
7
+ adapterDisplayName: 'ICQQ',
8
+ running: (use) => use(icqqRuntimeStateToken).endpoints.values(),
9
+ describeEntry: () => 'direct(直连 @icqqjs/icqq)',
10
+ addDescription: '登记 ICQQ endpoint(重启 zhin 生效)',
11
+ bindFlow: ({ id }) => {
12
+ if (!id) {
13
+ return '用法:icqq.endpoint add <uin>(uin 为纯数字 QQ 号)';
14
+ }
15
+ if (!/^\d+$/.test(id)) {
16
+ return 'icqq endpoint 名必须是纯数字 QQ 号(uin)';
17
+ }
18
+ try {
19
+ const filePath = addEndpointToConfig('icqq', { id });
20
+ return (
21
+ `✅ endpoint「${id}」已追加到 ${filePath} 的 plugins.icqq.endpoints。\n` +
22
+ `重启 zhin 后生效。`
23
+ );
24
+ } catch (error) {
25
+ return `添加失败:${error instanceof Error ? error.message : String(error)}`;
26
+ }
27
+ },
28
+ }, defineCommand);
@@ -0,0 +1,154 @@
1
+ /**
2
+ * ICQQ guild (QQ 频道) catalog sync, inbound normalize, Console channel list.
3
+ */
4
+ import { toCanonicalSegments, type MessageSegment } from "zhin.js";
5
+ import type { GuildMessageEventData } from "./protocol.js";
6
+ import { parseCqMessage } from "./cq-message.js";
7
+
8
+ export interface IcqqGuildClient {
9
+ getGuildList(): Array<{ guild_id: string; guild_name: string }>;
10
+ getChannelList(guildId: string): Array<{ guild_id: string; channel_id: string; channel_name: string }>;
11
+ }
12
+
13
+ export interface IcqqGuildChannelEntry {
14
+ channel_id: string;
15
+ channel_name: string;
16
+ guild_id: string;
17
+ guild_name: string;
18
+ }
19
+
20
+ export interface NormalizedIcqqGuildInbound {
21
+ messageId: string;
22
+ channelId: string;
23
+ guildId: string;
24
+ guildName: string;
25
+ channelName: string;
26
+ userId: string;
27
+ nickname: string;
28
+ content: MessageSegment[];
29
+ rawMessage: string;
30
+ timestampMs: number;
31
+ raw: GuildMessageEventData;
32
+ }
33
+
34
+ export class IcqqGuildCatalog {
35
+ private readonly channels = new Map<string, IcqqGuildChannelEntry>();
36
+ private readonly guilds = new Map<string, { guild_id: string; guild_name: string }>();
37
+
38
+ clear(): void {
39
+ this.channels.clear();
40
+ this.guilds.clear();
41
+ }
42
+
43
+ async syncAll(client: IcqqGuildClient): Promise<void> {
44
+ const guildRows = client.getGuildList();
45
+ for (const row of guildRows) {
46
+ const guild_id = String(row.guild_id ?? "");
47
+ if (!guild_id) continue;
48
+ const guild_name = String(row.guild_name ?? guild_id);
49
+ this.guilds.set(guild_id, { guild_id, guild_name });
50
+
51
+ const channels = client.getChannelList(guild_id);
52
+ for (const ch of channels) {
53
+ const channel_id = String(ch.channel_id ?? "");
54
+ if (!channel_id) continue;
55
+ const channel_name = String(ch.channel_name ?? channel_id);
56
+ this.channels.set(channel_id, {
57
+ channel_id,
58
+ channel_name,
59
+ guild_id,
60
+ guild_name,
61
+ });
62
+ }
63
+ }
64
+ }
65
+
66
+ upsertFromInbound(data: GuildMessageEventData): void {
67
+ const guild_id = String(data.guild_id ?? "");
68
+ const channel_id = String(data.channel_id ?? "");
69
+ if (!guild_id || !channel_id) return;
70
+ const guild_name = String(data.guild_name ?? guild_id);
71
+ const channel_name = String(data.channel_name ?? channel_id);
72
+ this.guilds.set(guild_id, { guild_id, guild_name });
73
+ this.channels.set(channel_id, {
74
+ channel_id,
75
+ channel_name,
76
+ guild_id,
77
+ guild_name,
78
+ });
79
+ }
80
+
81
+ getGuildChannelList(): Array<{
82
+ id: string;
83
+ name: string;
84
+ parent: { type: "guild"; id: string; name: string };
85
+ }> {
86
+ return Array.from(this.channels.values()).map((entry) => ({
87
+ id: entry.channel_id,
88
+ name: entry.channel_name,
89
+ parent: {
90
+ type: "guild" as const,
91
+ id: entry.guild_id,
92
+ name: entry.guild_name,
93
+ },
94
+ }));
95
+ }
96
+
97
+ resolveConsoleChannelNames(
98
+ channelId: string,
99
+ guildId?: string,
100
+ ): { channelName?: string; parentName?: string } {
101
+ const entry = this.channels.get(channelId);
102
+ if (entry) {
103
+ return { channelName: entry.channel_name, parentName: entry.guild_name };
104
+ }
105
+ if (guildId) {
106
+ const guild = this.guilds.get(guildId);
107
+ return guild ? { parentName: guild.guild_name } : {};
108
+ }
109
+ return {};
110
+ }
111
+ }
112
+
113
+ export function isIcqqGuildEvent(eventName: string | undefined): boolean {
114
+ return typeof eventName === "string" && eventName.startsWith("message.guild");
115
+ }
116
+
117
+ export function normalizeIcqqGuildInboundMessage(
118
+ data: GuildMessageEventData & Record<string, unknown>,
119
+ ): NormalizedIcqqGuildInbound | null {
120
+ if (data.type !== "guild") return null;
121
+ const guildId = String(data.guild_id ?? "");
122
+ const channelId = String(data.channel_id ?? "");
123
+ if (!guildId || !channelId) return null;
124
+
125
+ const userId = String(data.tiny_id ?? data.user_id ?? "");
126
+ const nickname = String(data.nickname ?? userId);
127
+ const rawMessage = String(data.raw_message ?? "");
128
+ const timestampMs =
129
+ typeof data.time === "number" ? data.time * 1000 : Date.now();
130
+ const messageId =
131
+ data.message_id != null && String(data.message_id) !== ""
132
+ ? String(data.message_id)
133
+ : data.seq != null
134
+ ? String(data.seq)
135
+ : `${data.time ?? 0}_${userId}_${channelId}`;
136
+
137
+ const parsed = parseCqMessage(rawMessage);
138
+ const canonical = toCanonicalSegments(parsed);
139
+ const content: MessageSegment[] = canonical.length > 0 ? canonical : [{ type: "text", data: { text: rawMessage } }];
140
+
141
+ return {
142
+ messageId,
143
+ channelId,
144
+ guildId,
145
+ guildName: String(data.guild_name ?? guildId),
146
+ channelName: String(data.channel_name ?? channelId),
147
+ userId,
148
+ nickname,
149
+ content,
150
+ rawMessage,
151
+ timestampMs,
152
+ raw: data,
153
+ };
154
+ }