@zhin.js/adapter-icqq 3.0.4 → 3.0.5
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 +17 -0
- package/README.md +1 -1
- package/dist/index.js +1 -1
- package/lib/bot.d.ts +56 -12
- package/lib/bot.d.ts.map +1 -1
- package/lib/bot.js +416 -136
- package/lib/bot.js.map +1 -1
- package/lib/cq-message.d.ts +10 -0
- package/lib/cq-message.d.ts.map +1 -0
- package/lib/cq-message.js +119 -0
- package/lib/cq-message.js.map +1 -0
- package/lib/forward-msg.d.ts +27 -0
- package/lib/forward-msg.d.ts.map +1 -0
- package/lib/forward-msg.js +387 -0
- package/lib/forward-msg.js.map +1 -0
- package/lib/get-msg.d.ts +3 -0
- package/lib/get-msg.d.ts.map +1 -0
- package/lib/get-msg.js +46 -0
- package/lib/get-msg.js.map +1 -0
- package/lib/icqq-inbound.d.ts +114 -0
- package/lib/icqq-inbound.d.ts.map +1 -0
- package/lib/icqq-inbound.js +495 -0
- package/lib/icqq-inbound.js.map +1 -0
- package/lib/icqq-side-events.d.ts +34 -0
- package/lib/icqq-side-events.d.ts.map +1 -0
- package/lib/icqq-side-events.js +194 -0
- package/lib/icqq-side-events.js.map +1 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/ipc-client.d.ts +7 -2
- package/lib/ipc-client.d.ts.map +1 -1
- package/lib/ipc-client.js +74 -16
- package/lib/ipc-client.js.map +1 -1
- package/lib/protocol.d.ts +3 -10
- package/lib/protocol.d.ts.map +1 -1
- package/lib/protocol.js +2 -0
- package/lib/protocol.js.map +1 -1
- package/lib/types.d.ts +44 -0
- package/lib/types.d.ts.map +1 -1
- package/lib/typing-indicator-example.d.ts +108 -0
- package/lib/typing-indicator-example.d.ts.map +1 -0
- package/lib/typing-indicator-example.js +220 -0
- package/lib/typing-indicator-example.js.map +1 -0
- package/lib/typing-indicator.d.ts +89 -0
- package/lib/typing-indicator.d.ts.map +1 -0
- package/lib/typing-indicator.js +237 -0
- package/lib/typing-indicator.js.map +1 -0
- package/package.json +14 -10
- package/src/bot.ts +524 -149
- package/src/cq-message.ts +120 -0
- package/src/forward-msg.ts +433 -0
- package/src/get-msg.ts +56 -0
- package/src/icqq-inbound.ts +616 -0
- package/src/icqq-side-events.ts +228 -0
- package/src/index.ts +8 -0
- package/src/ipc-client.ts +76 -16
- package/src/protocol.ts +4 -10
- package/src/types.ts +45 -0
- package/src/typing-indicator-example.ts +269 -0
- package/src/typing-indicator.ts +334 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CQ 码与 MessageSegment 互转。
|
|
3
|
+
* 出站:@icqqjs/cli 要求 message 为非空字符串;引用段编码为 [reply:id](需 cli parse-message 支持 reply,见 scripts/patch-icqq-cli-reply.mjs)。
|
|
4
|
+
*/
|
|
5
|
+
import { MessageSegment, segment, SendContent } from "zhin.js";
|
|
6
|
+
|
|
7
|
+
const MAX_CQ_PARSE_LEN = 256_000;
|
|
8
|
+
|
|
9
|
+
function pushCqSegment(segments: MessageSegment[], type: string, arg: string): void {
|
|
10
|
+
switch (type) {
|
|
11
|
+
case "face":
|
|
12
|
+
segments.push({ type: "face", data: { id: Number(arg) } });
|
|
13
|
+
break;
|
|
14
|
+
case "image":
|
|
15
|
+
segments.push({ type: "image", data: { url: arg, file: arg } });
|
|
16
|
+
break;
|
|
17
|
+
case "at":
|
|
18
|
+
if (arg === "all") {
|
|
19
|
+
segments.push({ type: "at", data: { qq: "all" } });
|
|
20
|
+
} else {
|
|
21
|
+
segments.push({ type: "at", data: { qq: arg } });
|
|
22
|
+
}
|
|
23
|
+
break;
|
|
24
|
+
case "dice":
|
|
25
|
+
segments.push({ type: "dice", data: {} });
|
|
26
|
+
break;
|
|
27
|
+
case "rps":
|
|
28
|
+
segments.push({ type: "rps", data: {} });
|
|
29
|
+
break;
|
|
30
|
+
case "record":
|
|
31
|
+
case "audio":
|
|
32
|
+
segments.push({ type: "record", data: { file: arg } });
|
|
33
|
+
break;
|
|
34
|
+
case "video":
|
|
35
|
+
segments.push({ type: "video", data: { file: arg } });
|
|
36
|
+
break;
|
|
37
|
+
case "reply":
|
|
38
|
+
segments.push({ type: "reply", data: { id: arg } });
|
|
39
|
+
break;
|
|
40
|
+
default:
|
|
41
|
+
segments.push({ type, data: { text: `[${type}:${arg}]` } });
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function parseCqMessage(raw: string): MessageSegment[] {
|
|
47
|
+
const text = raw.length > MAX_CQ_PARSE_LEN ? raw.slice(0, MAX_CQ_PARSE_LEN) : raw;
|
|
48
|
+
const segments: MessageSegment[] = [];
|
|
49
|
+
let i = 0;
|
|
50
|
+
|
|
51
|
+
while (i < text.length) {
|
|
52
|
+
if (text[i] !== "[") {
|
|
53
|
+
const next = text.indexOf("[", i);
|
|
54
|
+
const chunk = next === -1 ? text.slice(i) : text.slice(i, next);
|
|
55
|
+
if (chunk) segments.push({ type: "text", data: { text: chunk } });
|
|
56
|
+
if (next === -1) break;
|
|
57
|
+
i = next;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const close = text.indexOf("]", i + 1);
|
|
62
|
+
if (close === -1) {
|
|
63
|
+
segments.push({ type: "text", data: { text: text.slice(i) } });
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const inner = text.slice(i + 1, close);
|
|
68
|
+
const colon = inner.indexOf(":");
|
|
69
|
+
const type = (colon === -1 ? inner : inner.slice(0, colon)).trim().toLowerCase();
|
|
70
|
+
const arg = colon === -1 ? "" : inner.slice(colon + 1);
|
|
71
|
+
if (/^[a-z_]+$/.test(type)) {
|
|
72
|
+
pushCqSegment(segments, type, arg);
|
|
73
|
+
} else {
|
|
74
|
+
segments.push({ type: "text", data: { text: text.slice(i, close + 1) } });
|
|
75
|
+
}
|
|
76
|
+
i = close + 1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return segments.length ? segments : [{ type: "text", data: { text: raw } }];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 构建 icqq IPC send_*_msg 的 message 字符串(非空) */
|
|
83
|
+
export function buildIcqqIpcMessage(content: SendContent): string {
|
|
84
|
+
let message = toCqString(content).trim();
|
|
85
|
+
if (!message) message = "\u200b";
|
|
86
|
+
return message;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function toCqString(content: SendContent): string {
|
|
90
|
+
if (!Array.isArray(content)) content = [content];
|
|
91
|
+
return content
|
|
92
|
+
.map((seg) => {
|
|
93
|
+
if (typeof seg === "string") return seg;
|
|
94
|
+
const { type, data } = seg as MessageSegment;
|
|
95
|
+
switch (type) {
|
|
96
|
+
case "text":
|
|
97
|
+
return data.text ?? "";
|
|
98
|
+
case "face":
|
|
99
|
+
return `[face:${data.id}]`;
|
|
100
|
+
case "image":
|
|
101
|
+
return `[image:${data.file || data.url || data.src}]`;
|
|
102
|
+
case "at":
|
|
103
|
+
return `[at:${data.qq ?? data.id}]`;
|
|
104
|
+
case "dice":
|
|
105
|
+
return "[dice]";
|
|
106
|
+
case "rps":
|
|
107
|
+
return "[rps]";
|
|
108
|
+
case "record":
|
|
109
|
+
case "audio":
|
|
110
|
+
return `[record:${data.file || data.url}]`;
|
|
111
|
+
case "video":
|
|
112
|
+
return `[video:${data.file || data.url}]`;
|
|
113
|
+
case "reply":
|
|
114
|
+
return `[reply:${data.id}]`;
|
|
115
|
+
default:
|
|
116
|
+
return segment.toString(seg);
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
.join("");
|
|
120
|
+
}
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 合并转发(聊天记录)解析:从 json/xml 提取 resid,经 IPC get_forward_msg 拉取正文。
|
|
3
|
+
*/
|
|
4
|
+
import type { MessageSegment, QuotedMessagePayload } from "zhin.js";
|
|
5
|
+
import { segment } from "zhin.js";
|
|
6
|
+
import { Actions } from "./protocol.js";
|
|
7
|
+
import type { IpcClient } from "./ipc-client.js";
|
|
8
|
+
import {
|
|
9
|
+
icqqElementsToSegments,
|
|
10
|
+
type IcqqMessageElement,
|
|
11
|
+
} from "./icqq-inbound.js";
|
|
12
|
+
import { parseCqMessage } from "./cq-message.js";
|
|
13
|
+
|
|
14
|
+
const RESID_IN_XML_RE =
|
|
15
|
+
/(?:m_resid|resid|fileid|res_id)=["']?([A-Za-z0-9+/=_.-]{8,})/i;
|
|
16
|
+
|
|
17
|
+
const FORWARD_PLACEHOLDER_RE =
|
|
18
|
+
/\[聊天记录\]|合并转发|转发消息|聊天记录/i;
|
|
19
|
+
|
|
20
|
+
function parseJsonPayload(raw: unknown): Record<string, unknown> | null {
|
|
21
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
22
|
+
return raw as Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
if (typeof raw !== "string" || !raw.trim()) return null;
|
|
25
|
+
try {
|
|
26
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
27
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
28
|
+
return parsed as Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
// ignore
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 从 QQ 合并转发 json 元素提取 resid */
|
|
37
|
+
export function extractForwardResidFromJsonElement(
|
|
38
|
+
el: IcqqMessageElement,
|
|
39
|
+
): string | undefined {
|
|
40
|
+
const record =
|
|
41
|
+
parseJsonPayload(el.data) ??
|
|
42
|
+
parseJsonPayload(el.text) ??
|
|
43
|
+
parseJsonPayload(el);
|
|
44
|
+
if (!record) {
|
|
45
|
+
const text = typeof el.text === "string" ? el.text : "";
|
|
46
|
+
const m = text.match(RESID_IN_XML_RE);
|
|
47
|
+
return m?.[1]?.trim();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const bytes = record.bytesData ?? record.bytes_data ?? record.bytes;
|
|
51
|
+
if (typeof bytes === "string" && bytes.trim()) {
|
|
52
|
+
const inner = parseJsonPayload(bytes);
|
|
53
|
+
if (inner) {
|
|
54
|
+
const nested = extractForwardResidFromJsonElement({
|
|
55
|
+
type: "json",
|
|
56
|
+
data: inner,
|
|
57
|
+
});
|
|
58
|
+
if (nested) return nested;
|
|
59
|
+
}
|
|
60
|
+
const m = bytes.match(RESID_IN_XML_RE);
|
|
61
|
+
if (m?.[1]) return m[1].trim();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const app = String(record.app ?? record.App ?? "");
|
|
65
|
+
if (app.includes("multimsg") || app.includes("MultiMsg")) {
|
|
66
|
+
const meta = record.meta as Record<string, unknown> | undefined;
|
|
67
|
+
const detail = meta?.detail as Record<string, unknown> | undefined;
|
|
68
|
+
const resid = detail?.resid ?? detail?.ResID ?? detail?.resId;
|
|
69
|
+
if (resid != null && String(resid).trim()) return String(resid).trim();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const prompt = record.prompt as string | undefined;
|
|
73
|
+
if (prompt) {
|
|
74
|
+
const m = prompt.match(RESID_IN_XML_RE);
|
|
75
|
+
if (m?.[1]) return m[1].trim();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const direct = record.resid ?? record.m_resid ?? record.fileid;
|
|
79
|
+
if (direct != null && String(direct).trim()) return String(direct).trim();
|
|
80
|
+
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 从 get_msg 的 raw_message 解析 multimsg resid(XML / JSON / CQ:json) */
|
|
85
|
+
export function extractForwardResidFromRawMessage(
|
|
86
|
+
raw: string,
|
|
87
|
+
): string | undefined {
|
|
88
|
+
const text = raw.trim();
|
|
89
|
+
if (!text) return undefined;
|
|
90
|
+
|
|
91
|
+
const xml = text.match(RESID_IN_XML_RE);
|
|
92
|
+
if (xml?.[1]) return xml[1].trim();
|
|
93
|
+
|
|
94
|
+
const asJson = parseJsonPayload(text);
|
|
95
|
+
if (asJson) {
|
|
96
|
+
const fromJson = extractForwardResidFromJsonElement({
|
|
97
|
+
type: "json",
|
|
98
|
+
data: asJson,
|
|
99
|
+
});
|
|
100
|
+
if (fromJson) return fromJson;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
for (const match of text.matchAll(/\[CQ:json(?:,([^\]]*))?\]/gi)) {
|
|
104
|
+
const arg = match[1] ?? "";
|
|
105
|
+
const dataMatch = arg.match(/(?:^|,)data=(.+)$/s);
|
|
106
|
+
if (!dataMatch?.[1]) continue;
|
|
107
|
+
const record = parseJsonPayload(dataMatch[1].trim());
|
|
108
|
+
if (!record) continue;
|
|
109
|
+
const fromCq = extractForwardResidFromJsonElement({
|
|
110
|
+
type: "json",
|
|
111
|
+
data: record,
|
|
112
|
+
});
|
|
113
|
+
if (fromCq) return fromCq;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const segs = parseCqMessage(text);
|
|
117
|
+
return extractForwardResidFromSegments(segs);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** 从 get_msg 的 message 元素数组解析 multimsg resid */
|
|
121
|
+
export function extractForwardResidFromGetMsgElements(
|
|
122
|
+
elements: IcqqMessageElement[] | undefined,
|
|
123
|
+
): string | undefined {
|
|
124
|
+
if (!elements?.length) return undefined;
|
|
125
|
+
for (const el of elements) {
|
|
126
|
+
if (!el || typeof el !== "object") continue;
|
|
127
|
+
if (el.type === "forward") {
|
|
128
|
+
const id = String(el.id ?? el.resid ?? el.res_id ?? el.file ?? "").trim();
|
|
129
|
+
if (id) return id;
|
|
130
|
+
}
|
|
131
|
+
const fromJson = extractForwardResidFromJsonElement(el);
|
|
132
|
+
if (fromJson) return fromJson;
|
|
133
|
+
}
|
|
134
|
+
const segs = icqqElementsToSegments(elements);
|
|
135
|
+
if (segs?.length) {
|
|
136
|
+
return extractForwardResidFromSegments(segs);
|
|
137
|
+
}
|
|
138
|
+
return extractForwardResidDeep(elements);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 从 get_msg 响应解析合并转发 resid(优先 message 数组,其次 raw_message)。
|
|
143
|
+
*/
|
|
144
|
+
export function extractForwardResidFromGetMsg(
|
|
145
|
+
data: unknown,
|
|
146
|
+
): string | undefined {
|
|
147
|
+
if (!data || typeof data !== "object") return undefined;
|
|
148
|
+
const record = data as Record<string, unknown>;
|
|
149
|
+
|
|
150
|
+
if (Array.isArray(record.message)) {
|
|
151
|
+
const fromElements = extractForwardResidFromGetMsgElements(
|
|
152
|
+
record.message as IcqqMessageElement[],
|
|
153
|
+
);
|
|
154
|
+
if (fromElements) return fromElements;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (typeof record.raw_message === "string" && record.raw_message.trim()) {
|
|
158
|
+
const fromRaw = extractForwardResidFromRawMessage(record.raw_message);
|
|
159
|
+
if (fromRaw) return fromRaw;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (typeof record.message === "string" && record.message.trim()) {
|
|
163
|
+
return extractForwardResidFromRawMessage(record.message);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function extractForwardResidFromSegments(
|
|
170
|
+
content: MessageSegment[],
|
|
171
|
+
): string | undefined {
|
|
172
|
+
for (const seg of content) {
|
|
173
|
+
if (seg.type === "forward") {
|
|
174
|
+
const id = seg.data?.id ?? seg.data?.resid;
|
|
175
|
+
if (id != null && String(id).trim()) return String(id).trim();
|
|
176
|
+
}
|
|
177
|
+
if (seg.type === "json" && seg.data) {
|
|
178
|
+
const resid = extractForwardResidFromJsonElement(
|
|
179
|
+
seg.data as IcqqMessageElement,
|
|
180
|
+
);
|
|
181
|
+
if (resid) return resid;
|
|
182
|
+
const raw = seg.data.data ?? seg.data.text;
|
|
183
|
+
const record = parseJsonPayload(raw);
|
|
184
|
+
if (record) {
|
|
185
|
+
const fromEl = extractForwardResidFromJsonElement({
|
|
186
|
+
type: "json",
|
|
187
|
+
data: record,
|
|
188
|
+
});
|
|
189
|
+
if (fromEl) return fromEl;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const raw = segment.raw(content);
|
|
194
|
+
const m = raw.match(RESID_IN_XML_RE);
|
|
195
|
+
return m?.[1]?.trim();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function extractForwardResidFromPayload(
|
|
199
|
+
payload: QuotedMessagePayload,
|
|
200
|
+
): string | undefined {
|
|
201
|
+
if (Array.isArray(payload.content)) {
|
|
202
|
+
const fromSeg = extractForwardResidFromSegments(
|
|
203
|
+
payload.content as MessageSegment[],
|
|
204
|
+
);
|
|
205
|
+
if (fromSeg) return fromSeg;
|
|
206
|
+
}
|
|
207
|
+
if (typeof payload.raw === "string") {
|
|
208
|
+
const m = payload.raw.match(RESID_IN_XML_RE);
|
|
209
|
+
if (m?.[1]) return m[1].trim();
|
|
210
|
+
const record = parseJsonPayload(payload.raw);
|
|
211
|
+
if (record) {
|
|
212
|
+
return extractForwardResidFromJsonElement({ type: "json", data: record });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** 在 get_msg / IPC 原始对象里深度搜索 resid */
|
|
219
|
+
export function extractForwardResidDeep(
|
|
220
|
+
root: unknown,
|
|
221
|
+
maxDepth = 14,
|
|
222
|
+
): string | undefined {
|
|
223
|
+
const seen = new Set<unknown>();
|
|
224
|
+
function walk(node: unknown, depth: number): string | undefined {
|
|
225
|
+
if (depth > maxDepth || node == null) return undefined;
|
|
226
|
+
if (typeof node === "string") {
|
|
227
|
+
const m = node.match(RESID_IN_XML_RE);
|
|
228
|
+
if (m?.[1]) return m[1].trim();
|
|
229
|
+
const record = parseJsonPayload(node);
|
|
230
|
+
if (record) {
|
|
231
|
+
const fromJson = extractForwardResidFromJsonElement({
|
|
232
|
+
type: "json",
|
|
233
|
+
data: record,
|
|
234
|
+
});
|
|
235
|
+
if (fromJson) return fromJson;
|
|
236
|
+
}
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
if (typeof node !== "object") return undefined;
|
|
240
|
+
if (seen.has(node)) return undefined;
|
|
241
|
+
seen.add(node);
|
|
242
|
+
|
|
243
|
+
if (Array.isArray(node)) {
|
|
244
|
+
for (const item of node) {
|
|
245
|
+
const hit = walk(item, depth + 1);
|
|
246
|
+
if (hit) return hit;
|
|
247
|
+
}
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const fromEl = extractForwardResidFromJsonElement({
|
|
252
|
+
type: "json",
|
|
253
|
+
data: node as IcqqMessageElement,
|
|
254
|
+
});
|
|
255
|
+
if (fromEl) return fromEl;
|
|
256
|
+
|
|
257
|
+
for (const value of Object.values(node as Record<string, unknown>)) {
|
|
258
|
+
const hit = walk(value, depth + 1);
|
|
259
|
+
if (hit) return hit;
|
|
260
|
+
}
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
return walk(root, 0);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function isForwardPlaceholderPayload(
|
|
267
|
+
payload: QuotedMessagePayload,
|
|
268
|
+
): boolean {
|
|
269
|
+
if (
|
|
270
|
+
Array.isArray(payload.content) &&
|
|
271
|
+
payload.content.some((s) => s.type === "forward")
|
|
272
|
+
) {
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
const raw = Array.isArray(payload.content)
|
|
276
|
+
? segment.raw(payload.content as MessageSegment[])
|
|
277
|
+
: String(payload.content ?? payload.raw ?? "");
|
|
278
|
+
return FORWARD_PLACEHOLDER_RE.test(raw);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function hasMergedForwardBlock(payload: QuotedMessagePayload): boolean {
|
|
282
|
+
const raw = Array.isArray(payload.content)
|
|
283
|
+
? segment.raw(payload.content as MessageSegment[])
|
|
284
|
+
: String(payload.content ?? "");
|
|
285
|
+
return raw.includes("[Merged chat history");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function senderLabel(sender: unknown): string {
|
|
289
|
+
if (!sender || typeof sender !== "object") return "unknown";
|
|
290
|
+
const s = sender as Record<string, unknown>;
|
|
291
|
+
const name = s.nickname ?? s.name ?? s.card;
|
|
292
|
+
const id = s.user_id ?? s.uin ?? s.uid ?? s.id;
|
|
293
|
+
if (name && id) return `${name} (${id})`;
|
|
294
|
+
if (name) return String(name);
|
|
295
|
+
if (id != null) return String(id);
|
|
296
|
+
return "unknown";
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function messageBodyToText(body: unknown): string {
|
|
300
|
+
if (body == null) return "";
|
|
301
|
+
if (typeof body === "string") return body.trim();
|
|
302
|
+
if (Array.isArray(body)) {
|
|
303
|
+
const segs = icqqElementsToSegments(body as IcqqMessageElement[]);
|
|
304
|
+
return segs?.length ? segment.raw(segs).trim() : segment.raw(body).trim();
|
|
305
|
+
}
|
|
306
|
+
if (typeof body === "object") {
|
|
307
|
+
const r = body as Record<string, unknown>;
|
|
308
|
+
if (typeof r.raw_message === "string" && r.raw_message.trim()) {
|
|
309
|
+
return r.raw_message.trim();
|
|
310
|
+
}
|
|
311
|
+
if (Array.isArray(r.message)) {
|
|
312
|
+
return messageBodyToText(r.message);
|
|
313
|
+
}
|
|
314
|
+
if (Array.isArray(r.elements)) {
|
|
315
|
+
return messageBodyToText(r.elements);
|
|
316
|
+
}
|
|
317
|
+
if (typeof r.content === "string") return r.content.trim();
|
|
318
|
+
}
|
|
319
|
+
return "";
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** 将 get_forward_msg 响应格式化为可读聊天记录 */
|
|
323
|
+
export function formatForwardMsgResponse(data: unknown): string {
|
|
324
|
+
if (!data) return "";
|
|
325
|
+
|
|
326
|
+
const root =
|
|
327
|
+
data && typeof data === "object" && !Array.isArray(data)
|
|
328
|
+
? (data as Record<string, unknown>)
|
|
329
|
+
: null;
|
|
330
|
+
|
|
331
|
+
const list: unknown[] = Array.isArray(data)
|
|
332
|
+
? data
|
|
333
|
+
: Array.isArray(root?.messages)
|
|
334
|
+
? (root!.messages as unknown[])
|
|
335
|
+
: Array.isArray(root?.msgList)
|
|
336
|
+
? (root!.msgList as unknown[])
|
|
337
|
+
: Array.isArray(root?.msg_list)
|
|
338
|
+
? (root!.msg_list as unknown[])
|
|
339
|
+
: Array.isArray(root?.message)
|
|
340
|
+
? (root!.message as unknown[])
|
|
341
|
+
: [];
|
|
342
|
+
|
|
343
|
+
if (!list.length) return "";
|
|
344
|
+
|
|
345
|
+
const lines: string[] = [];
|
|
346
|
+
let index = 0;
|
|
347
|
+
for (const item of list) {
|
|
348
|
+
if (!item || typeof item !== "object") continue;
|
|
349
|
+
const row = item as Record<string, unknown>;
|
|
350
|
+
index += 1;
|
|
351
|
+
const who = senderLabel(row.sender ?? row.user);
|
|
352
|
+
const time =
|
|
353
|
+
typeof row.time === "number"
|
|
354
|
+
? new Date(row.time * (row.time > 1e12 ? 1 : 1000)).toISOString()
|
|
355
|
+
: "";
|
|
356
|
+
const text = messageBodyToText(
|
|
357
|
+
row.message ??
|
|
358
|
+
row.content ??
|
|
359
|
+
row.raw_message ??
|
|
360
|
+
row.elements ??
|
|
361
|
+
row,
|
|
362
|
+
);
|
|
363
|
+
const head = time ? `${index}. ${who} @ ${time}` : `${index}. ${who}`;
|
|
364
|
+
lines.push(text ? `${head}\n${text}` : `${head}\n(非文本或无法解析的内容)`);
|
|
365
|
+
}
|
|
366
|
+
return lines.join("\n\n");
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export async function fetchForwardMsgText(
|
|
370
|
+
ipc: IpcClient,
|
|
371
|
+
id: string,
|
|
372
|
+
): Promise<string> {
|
|
373
|
+
const attempts: Record<string, unknown>[] = [
|
|
374
|
+
{ message_id: id },
|
|
375
|
+
{ id },
|
|
376
|
+
{ resid: id },
|
|
377
|
+
{ res_id: id },
|
|
378
|
+
{ msg_id: id },
|
|
379
|
+
];
|
|
380
|
+
for (const params of attempts) {
|
|
381
|
+
const resp = await ipc.request(Actions.GET_FORWARD_MSG, params);
|
|
382
|
+
if (!resp.ok) continue;
|
|
383
|
+
const text = formatForwardMsgResponse(resp.data);
|
|
384
|
+
if (text.trim()) return text.trim();
|
|
385
|
+
}
|
|
386
|
+
return "";
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function appendForwardBlock(
|
|
390
|
+
payload: QuotedMessagePayload,
|
|
391
|
+
block: string,
|
|
392
|
+
): QuotedMessagePayload {
|
|
393
|
+
if (Array.isArray(payload.content)) {
|
|
394
|
+
return {
|
|
395
|
+
...payload,
|
|
396
|
+
content: [...payload.content, { type: "text", data: { text: block } }],
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
const prev =
|
|
400
|
+
typeof payload.content === "string" ? payload.content.trim() : "";
|
|
401
|
+
return {
|
|
402
|
+
...payload,
|
|
403
|
+
content: prev ? `${prev}\n\n${block}` : block,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** 若 payload 含合并转发,拉取并追加到 content */
|
|
408
|
+
export async function enrichQuotedPayloadWithForward(
|
|
409
|
+
ipc: IpcClient | null | undefined,
|
|
410
|
+
payload: QuotedMessagePayload,
|
|
411
|
+
ipcRaw?: unknown,
|
|
412
|
+
): Promise<QuotedMessagePayload> {
|
|
413
|
+
if (!ipc || hasMergedForwardBlock(payload)) return payload;
|
|
414
|
+
|
|
415
|
+
const resid =
|
|
416
|
+
extractForwardResidFromPayload(payload) ??
|
|
417
|
+
(ipcRaw ? extractForwardResidFromGetMsg(ipcRaw) : undefined) ??
|
|
418
|
+
(ipcRaw ? extractForwardResidDeep(ipcRaw) : undefined);
|
|
419
|
+
|
|
420
|
+
const fetchIds = [
|
|
421
|
+
resid,
|
|
422
|
+
isForwardPlaceholderPayload(payload) ? payload.messageId : undefined,
|
|
423
|
+
].filter((v, i, a): v is string => !!v && a.indexOf(v) === i);
|
|
424
|
+
|
|
425
|
+
for (const fetchId of fetchIds) {
|
|
426
|
+
const forwardText = await fetchForwardMsgText(ipc, fetchId);
|
|
427
|
+
if (!forwardText.trim()) continue;
|
|
428
|
+
const block = `[Merged chat history — id ${fetchId}]\n${forwardText.trim()}`;
|
|
429
|
+
return appendForwardBlock(payload, block);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return payload;
|
|
433
|
+
}
|
package/src/get-msg.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { QuotedMessagePayload } from "zhin.js";
|
|
2
|
+
import { parseCqMessage } from "./cq-message.js";
|
|
3
|
+
import { extractForwardResidFromGetMsg } from "./forward-msg.js";
|
|
4
|
+
import {
|
|
5
|
+
icqqElementsToSegments,
|
|
6
|
+
type IcqqMessageElement,
|
|
7
|
+
} from "./icqq-inbound.js";
|
|
8
|
+
|
|
9
|
+
export function parseIcqqGetMsgResponse(
|
|
10
|
+
messageId: string,
|
|
11
|
+
data: unknown,
|
|
12
|
+
): QuotedMessagePayload {
|
|
13
|
+
const record =
|
|
14
|
+
data && typeof data === "object"
|
|
15
|
+
? (data as Record<string, unknown>)
|
|
16
|
+
: {};
|
|
17
|
+
let content: QuotedMessagePayload["content"] = [];
|
|
18
|
+
if (Array.isArray(record.message)) {
|
|
19
|
+
content =
|
|
20
|
+
icqqElementsToSegments(record.message as IcqqMessageElement[]) ?? [];
|
|
21
|
+
} else if (typeof record.raw_message === "string" && record.raw_message) {
|
|
22
|
+
content = parseCqMessage(record.raw_message);
|
|
23
|
+
} else if (typeof record.message === "string" && record.message) {
|
|
24
|
+
content = parseCqMessage(record.message);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const forwardResid = extractForwardResidFromGetMsg(record);
|
|
28
|
+
if (forwardResid && Array.isArray(content)) {
|
|
29
|
+
const hasForward = content.some((s) => s.type === "forward");
|
|
30
|
+
if (!hasForward) {
|
|
31
|
+
content = [
|
|
32
|
+
{ type: "forward", data: { id: forwardResid, resid: forwardResid } },
|
|
33
|
+
...content,
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const senderRaw = record.sender;
|
|
39
|
+
let sender: QuotedMessagePayload["sender"];
|
|
40
|
+
if (senderRaw && typeof senderRaw === "object") {
|
|
41
|
+
const s = senderRaw as Record<string, unknown>;
|
|
42
|
+
sender = {
|
|
43
|
+
id: String(s.user_id ?? s.uid ?? s.uin ?? ""),
|
|
44
|
+
name: String(s.nickname ?? s.name ?? ""),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
messageId,
|
|
50
|
+
sender,
|
|
51
|
+
content,
|
|
52
|
+
raw:
|
|
53
|
+
typeof record.raw_message === "string" ? record.raw_message : undefined,
|
|
54
|
+
time: typeof record.time === "number" ? record.time : undefined,
|
|
55
|
+
};
|
|
56
|
+
}
|