@zaofan/dsh-qqbot 1.5.30 → 1.6.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.
- package/client/qqbot-settings.js +161 -29
- package/client/qqbot-settings.js.bak-ctxkey-1790880133352 +5069 -0
- package/client/qqbot-settings.js.bak-inc-1790865292905 +4967 -0
- package/client/qqbot-settings.js.bak-inc-1790865309979 +4967 -0
- package/client/qqbot-settings.js.bak-newer-1790865544006 +5023 -0
- package/client/qqbot-settings.js.bak-radio-1790899148239 +5076 -0
- package/client/qqbot-settings.js.bak-stick-1790865591295 +5033 -0
- package/client/qqbot-settings.js.bak-ui4b-1790879732726 +5046 -0
- package/dist/channel-tools.d.ts +18 -0
- package/dist/channel-tools.d.ts.map +1 -1
- package/dist/channel-tools.js +272 -3
- package/dist/channel-tools.js.map +1 -1
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -0
- package/dist/config.js.map +1 -1
- package/dist/features/chat-ledger.d.ts +13 -0
- package/dist/features/chat-ledger.d.ts.map +1 -1
- package/dist/features/chat-ledger.js +52 -0
- package/dist/features/chat-ledger.js.map +1 -1
- package/dist/features/context-memo.d.ts +29 -0
- package/dist/features/context-memo.d.ts.map +1 -0
- package/dist/features/context-memo.js +173 -0
- package/dist/features/context-memo.js.map +1 -0
- package/dist/features/contextless-store.d.ts +16 -0
- package/dist/features/contextless-store.d.ts.map +1 -1
- package/dist/features/contextless-store.js +24 -1
- package/dist/features/contextless-store.js.map +1 -1
- package/dist/gateway/debounce.d.ts.map +1 -1
- package/dist/gateway/debounce.js +32 -2
- package/dist/gateway/debounce.js.map +1 -1
- package/dist/gateway/middleware-setup.d.ts.map +1 -1
- package/dist/gateway/middleware-setup.js +25 -5
- package/dist/gateway/middleware-setup.js.map +1 -1
- package/dist/middleware/media-history.d.ts +27 -0
- package/dist/middleware/media-history.d.ts.map +1 -1
- package/dist/middleware/media-history.js +19 -5
- package/dist/middleware/media-history.js.map +1 -1
- package/dist/session/session-manager.d.ts.map +1 -1
- package/dist/session/session-manager.js +198 -18
- package/dist/session/session-manager.js.map +1 -1
- package/dist/shared/path-utils.d.ts +6 -0
- package/dist/shared/path-utils.d.ts.map +1 -0
- package/dist/shared/path-utils.js +53 -0
- package/dist/shared/path-utils.js.map +1 -0
- package/dist/transport/inbound.d.ts +33 -0
- package/dist/transport/inbound.d.ts.map +1 -1
- package/dist/transport/inbound.js +232 -28
- package/dist/transport/inbound.js.map +1 -1
- package/dist/transport/msg-content-cache.d.ts +15 -0
- package/dist/transport/msg-content-cache.d.ts.map +1 -0
- package/dist/transport/msg-content-cache.js +105 -0
- package/dist/transport/msg-content-cache.js.map +1 -0
- package/dist/transport/quote-images.d.ts +17 -0
- package/dist/transport/quote-images.d.ts.map +1 -0
- package/dist/transport/quote-images.js +91 -0
- package/dist/transport/quote-images.js.map +1 -0
- package/dist/transport/quote-text.d.ts +30 -0
- package/dist/transport/quote-text.d.ts.map +1 -0
- package/dist/transport/quote-text.js +119 -0
- package/dist/transport/quote-text.js.map +1 -0
- package/package.json +1 -1
- package/settings-host.js +54 -6
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 引用文本清洗(2026-10-01 主人实测:「历史里包含引用的排版太乱了」+「引用的是表情包?」)。
|
|
3
|
+
*
|
|
4
|
+
* 两个数据源问题都在这里修:
|
|
5
|
+
* ① QQ 的引用在 `msg_elements` 里是一段**模板串**:
|
|
6
|
+
* `=== 消息 1 === [消息内容] A [消息类型] 引用消息 [关联消息] --- 第1条 --- [消息内容] B …`
|
|
7
|
+
* 嵌套几层就重复几段,夹着一堆纯噪声 —— 原样喂给 AI 又长又乱。
|
|
8
|
+
* ② 引用一条**表情消息**时,内容是一个 QQ 表情标签:
|
|
9
|
+
* `<faceType=6,faceId="0",ext="eyJ0ZXh0IjoiIn0=">`(ext 是 base64 的 `{"text":""}`)。
|
|
10
|
+
* 原样显示谁也看不懂 —— 这里转成 `【表情: 微笑】`;并顺手把 ext 里**可能藏着**的图片 URL
|
|
11
|
+
* (图片表情 / 收藏表情的其它端形态)挖出来当附件用。
|
|
12
|
+
*/
|
|
13
|
+
import { resolveFaceTags } from '../features/face-tags.js';
|
|
14
|
+
/** 判断是不是 QQ 的引用模板串 */
|
|
15
|
+
function looksLikeQqTemplate(s) {
|
|
16
|
+
return /===+\s*消息|\[消息内容\]|\[消息类型\]|\[关联消息\]|引用消息|第\s*\d+\s*条/.test(s);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 剥掉 QQ 引用模板的壳,返回各层正文(**最外层在前**,即"最近被引用的那句话"排第一)。
|
|
20
|
+
* 不是模板串时,按行返回原文(保留原样,只 trim)。
|
|
21
|
+
*/
|
|
22
|
+
export function stripQqQuoteTemplate(s) {
|
|
23
|
+
const raw = String(s ?? '');
|
|
24
|
+
if (!looksLikeQqTemplate(raw)) {
|
|
25
|
+
return raw.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
26
|
+
}
|
|
27
|
+
let t = raw;
|
|
28
|
+
// 每层的正文起点:`[消息内容]` 和 `--- 第N条 ---` 都切开
|
|
29
|
+
t = t.replace(/\[消息内容\]/g, '\n');
|
|
30
|
+
t = t.replace(/-{2,}\s*第\s*\d+\s*条\s*-{2,}/g, '\n');
|
|
31
|
+
// 剩余的壳当空白
|
|
32
|
+
t = t.replace(/\[(?:消息类型|关联消息|消息来源|消息序号)\]/g, ' ');
|
|
33
|
+
t = t.replace(/[=]{2,}\s*消息\s*\d+\s*[=]{2,}/g, ' ');
|
|
34
|
+
t = t.replace(/引用消息/g, ' ');
|
|
35
|
+
return t
|
|
36
|
+
.split(/\r?\n/)
|
|
37
|
+
.map((l) => l.replace(/\s+/g, ' ').trim())
|
|
38
|
+
.filter(Boolean);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 从 QQ 表情标签里挖**图片线索**。
|
|
42
|
+
*
|
|
43
|
+
* 背景(2026-10-01 主人实测:「图片收藏进了表情里,依然是图片」):
|
|
44
|
+
* QQ 的「图片表情 / 收藏表情」在消息里只给一个 `<faceType=…,ext="base64">` 标签。
|
|
45
|
+
* 实测那条 ext = `eyJ0ZXh0IjoiIn0=` → `{"text":""}` —— **里面没有图**(QQ 官方文档也没定义 faceType,
|
|
46
|
+
* SDK 同样只解 ext.text),所以光靠标签解析不出图;真正的出路是"那条消息入站时的附件"
|
|
47
|
+
* (走本地台账回查,见 msg-content-cache.ts / quote-images.ts 的预取)。
|
|
48
|
+
* 但不同端/版本的 ext 里**可能**带 url 之类字段 —— 这里全部扫一遍,挖到就当真图片附件用。
|
|
49
|
+
*/
|
|
50
|
+
export function extractFaceImageUrls(text) {
|
|
51
|
+
const src = String(text ?? '');
|
|
52
|
+
if (src.indexOf('<faceType=') < 0)
|
|
53
|
+
return [];
|
|
54
|
+
const out = [];
|
|
55
|
+
const re = /<faceType=\d+,faceId="[^"]*",ext="([^"]*)">/g;
|
|
56
|
+
let m;
|
|
57
|
+
while ((m = re.exec(src)) !== null) {
|
|
58
|
+
const b64 = m[1] ?? '';
|
|
59
|
+
if (!b64)
|
|
60
|
+
continue;
|
|
61
|
+
try {
|
|
62
|
+
const obj = JSON.parse(Buffer.from(b64, 'base64').toString('utf-8'));
|
|
63
|
+
for (const v of Object.values(obj ?? {})) {
|
|
64
|
+
if (typeof v === 'string' && /^https?:\/\//i.test(v) && !out.includes(v))
|
|
65
|
+
out.push(v);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch { /* ext 不是 JSON / 解不开 → 跳过 */ }
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 引用块正文瘦身:剥模板壳 → **只取最外层那一句** → 表情标签转可读 → 压平空白 → 限长。
|
|
74
|
+
*
|
|
75
|
+
* ⚠️ 只取最外层 —— 那才是"他引用的内容"。QQ 给的嵌套链(`[关联消息] --- 第1条 --- [消息内容] …`)
|
|
76
|
+
* 全是"那句话又引用了谁",又长又乱(主人:「不止是排版乱的问题,这是内容就乱了」)。
|
|
77
|
+
* @param max 最大字数(默认 160;传 0 = 不限长)
|
|
78
|
+
*/
|
|
79
|
+
export function slimQuoteText(s, max = 160) {
|
|
80
|
+
const lines = stripQqQuoteTemplate(s);
|
|
81
|
+
// 表情标签 → `【表情: 微笑】`(原来是 `<faceType=6,faceId="0",ext="…">` 这种谁也看不懂的串)
|
|
82
|
+
let out = resolveFaceTags(lines[0] ?? '').replace(/\s+/g, ' ').trim();
|
|
83
|
+
if (max > 0 && out.length > max)
|
|
84
|
+
out = out.slice(0, max) + '…';
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
/** 附件行(`📷 被引用的图片: <本地路径>` 等)—— 截断时必须豁免:那是"引用的是哪张图"的唯一线索
|
|
88
|
+
* ⚠️ 不能用字符类 `[📷🎵🎬📎]` —— emoji 是代理对,字符类里会被拆成单个 UTF-16 码元,**匹配不上**。 */
|
|
89
|
+
const ATT_LINE_RE = /^\s*(?:📷|🎵|🎬|📎)\s*被引用的/;
|
|
90
|
+
/**
|
|
91
|
+
* 把整个 `[引]…[/引]` 块压成**一行**并限长(历史行用)。
|
|
92
|
+
* 附件行不参与截断(否则路径被切掉 = 白记)。
|
|
93
|
+
*/
|
|
94
|
+
export function slimQuoteBlockOneLine(block, max = 140) {
|
|
95
|
+
const raw = String(block ?? '').trim();
|
|
96
|
+
if (!raw)
|
|
97
|
+
return '';
|
|
98
|
+
const lines = raw.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
99
|
+
const isBegin = (l) => /^\[(?:引|Quoted message begins)\]$/i.test(l);
|
|
100
|
+
const isEnd = (l) => /^\[\/(?:引|Quoted message ends)\]$/i.test(l);
|
|
101
|
+
const hasBegin = lines.some(isBegin);
|
|
102
|
+
const hasEnd = lines.some(isEnd);
|
|
103
|
+
// ⚠️ 2026-10-01 主人实测抓到:附件行**必须留在 `[引]…[/引]` 里面**。
|
|
104
|
+
// 原实现把「正文」和「附件」分成两段各自拼,附件被甩到 `[引] [/引]` **之后** →
|
|
105
|
+
// 引用块看着是空的、图却挂在块外(`[引] [/引] 📷 被引用的图片: …`)。
|
|
106
|
+
const att = lines.filter((l) => ATT_LINE_RE.test(l));
|
|
107
|
+
const body = lines
|
|
108
|
+
.filter((l) => !isBegin(l) && !isEnd(l) && !ATT_LINE_RE.test(l))
|
|
109
|
+
.join(' ')
|
|
110
|
+
.replace(/\s+/g, ' ')
|
|
111
|
+
.trim();
|
|
112
|
+
const clipped = max > 0 && body.length > max ? body.slice(0, max) + '…' : body;
|
|
113
|
+
const inner = [clipped, ...att].filter(Boolean).join(' ').replace(/\s+/g, ' ').trim();
|
|
114
|
+
if (!hasBegin && !hasEnd)
|
|
115
|
+
return inner;
|
|
116
|
+
// 老英文标记([Quoted message begins]…)也归一成短标记
|
|
117
|
+
return `[引] ${inner} [/引]`.replace(/\s+/g, ' ').trim();
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=quote-text.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quote-text.js","sourceRoot":"","sources":["../../src/transport/quote-text.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/settings-host.js
CHANGED
|
@@ -433,7 +433,8 @@ export function apply(ctx) {
|
|
|
433
433
|
if (!key) return writeJson(res, 400, { error: 'sessionKey required' });
|
|
434
434
|
const mod = await loadContextlessStore(); // 先 init 到同一个 dataRoot,避免两份模块实例各写一个文件
|
|
435
435
|
if (body.remove === true) { mod.clearContextless(key); return writeJson(res, 200, { ok: true, removed: true }); }
|
|
436
|
-
|
|
436
|
+
// 2026-10-01: smart = 无上下文模式·智能判断(与 window 互斥,面板单选取值)
|
|
437
|
+
const next = mod.setContextless(key, { enabled: body.enabled, window: body.window, smart: body.smart });
|
|
437
438
|
return writeJson(res, 200, { ok: true, value: next });
|
|
438
439
|
} catch (e) {
|
|
439
440
|
return writeJson(res, 500, { error: String((e && e.message) || e) });
|
|
@@ -3260,7 +3261,12 @@ export function apply(ctx) {
|
|
|
3260
3261
|
for (const line of String(body || '').split('\n')) {
|
|
3261
3262
|
const s = line.trim();
|
|
3262
3263
|
if (!s) continue;
|
|
3263
|
-
|
|
3264
|
+
// ⚠️ 2026-10-01 修(主人实测:"当前消息引用的消息显示给上一个人了"):
|
|
3265
|
+
// `[当前]` 是**分段点** —— 它之前属于历史(该收尾结算),之后的"引用块 + 正文"才是当前消息。
|
|
3266
|
+
// 原来这里直接 continue 不 flush,导致当前消息的 `[引]…[/引]` 被并进**上一条历史消息**里
|
|
3267
|
+
// (于是界面上引用块挂在别人气泡上)。
|
|
3268
|
+
if (RE_CURRENT_LINE.test(s)) { flush(); continue; }
|
|
3269
|
+
if (RE_HIST_BEGIN_LINE.test(s) || RE_HIST_END_LINE.test(s) || isTimeHeadLine(s)) continue;
|
|
3264
3270
|
if (/^\[系统提示\]/.test(s)) { flush(); return out; } // 系统注入段, 之后都不属于群聊内容
|
|
3265
3271
|
// 媒体元数据行: 暂存, 等归属给下面那条带昵称的消息
|
|
3266
3272
|
// (Layer 4 现输出 `- Image:/- Video:/- Voice:/- File:` 单数前缀, 此处一并覆盖)
|
|
@@ -3268,7 +3274,23 @@ export function apply(ctx) {
|
|
|
3268
3274
|
// 2026-09-12 适配(token 瘦身后历史行变成 "[昵称]"): id 段设为**可选**, 老格式 "[昵称 (openid)]" 仍认;
|
|
3269
3275
|
// 同时排除含冒号的方括号(如 "[图片: url]"), 免得把附件标记当成发送者。
|
|
3270
3276
|
const m = s.match(/^\[([^\]\n:]*?)(?:\s*\([A-Za-z0-9_-]{6,}\))?\](.*)$/);
|
|
3271
|
-
|
|
3277
|
+
// ⚠️ 2026-10-01 修(主人实测:dock 聊天界面冒出个叫「引」的人发消息):
|
|
3278
|
+
// `[引]` / `[/引]` / `[引用]` 是**块标记**而不是昵称壳 —— 命中它们时不能开新气泡,
|
|
3279
|
+
// 要当普通行并入当前段(真昵称在后面几行,交给 chatPolishOne 去找)。
|
|
3280
|
+
// 与 chatPolishOne 的 RE_MARKER_WORD 同款防御,此处之前漏了。
|
|
3281
|
+
const isNick = m && m[1].trim() && !RE_MARKER_WORD.test(m[1].trim());
|
|
3282
|
+
if (isNick) {
|
|
3283
|
+
// ⚠️ 2026-10-01:当前消息的引用块会先形成一个 sender 为空的段 —— 紧随的 [昵称] 行要**接管**它
|
|
3284
|
+
// (引用块 + 正文同属一条消息),否则引用块会独立成一个没有名字的气泡。
|
|
3285
|
+
if (cur && !cur.sender) {
|
|
3286
|
+
cur.sender = m[1].trim();
|
|
3287
|
+
cur.lines.push(...[m[2].trim(), ...pendingMeta].filter(Boolean));
|
|
3288
|
+
} else {
|
|
3289
|
+
flush();
|
|
3290
|
+
cur = { sender: m[1].trim(), lines: [m[2].trim(), ...pendingMeta].filter(Boolean) };
|
|
3291
|
+
}
|
|
3292
|
+
pendingMeta = [];
|
|
3293
|
+
}
|
|
3272
3294
|
else { if (cur) cur.lines.push(...pendingMeta, s); else { flush(); cur = { sender: '', lines: [...pendingMeta, s] }; } pendingMeta = []; }
|
|
3273
3295
|
}
|
|
3274
3296
|
flush();
|
|
@@ -3406,6 +3428,10 @@ export function apply(ctx) {
|
|
|
3406
3428
|
const limit = Math.max(1, Math.min(200, Math.round(Number(u.searchParams.get('limit'))) || 50));
|
|
3407
3429
|
const bRaw = Number(u.searchParams.get('beforeSeq'));
|
|
3408
3430
|
const beforeSeq = Number.isFinite(bRaw) && bRaw > 0 ? Math.floor(bRaw) : undefined;
|
|
3431
|
+
// 2026-10-01 新增 afterSeq:**往后取更新的消息**(dock 聊天页滚轮增量刷新用 ——
|
|
3432
|
+
// 只 append 新气泡,不重绘不闪、不跳滚动位置)。与 beforeSeq 互斥:给了 afterSeq 就正扫。
|
|
3433
|
+
const aRaw = Number(u.searchParams.get('afterSeq'));
|
|
3434
|
+
const afterSeq = Number.isFinite(aRaw) && aRaw >= 0 ? Math.floor(aRaw) : undefined;
|
|
3409
3435
|
const bot = nsBot(ns);
|
|
3410
3436
|
if (!bot) return writeJson(res, 400, { error: '找不到该账号实例(请先在账号页配置 appId/appSecret)' });
|
|
3411
3437
|
const reg = await import('./dist/features/session-registry.js');
|
|
@@ -3434,9 +3460,30 @@ export function apply(ctx) {
|
|
|
3434
3460
|
const fallbackSender = scope === 'c2c' ? ((ledger.get('c2c:' + peerId) || {}).name || '') : '';
|
|
3435
3461
|
// 尾部倒扫: 默认从最新一条事件 seq 往前; beforeSeq=加载更早(beforeSeq 之前的)
|
|
3436
3462
|
const items = [];
|
|
3437
|
-
let high = beforeSeq !== undefined ? Math.max(0, beforeSeq - 1) : Math.max(0, sess.seq - 1);
|
|
3438
|
-
let reachedEnd = false;
|
|
3439
3463
|
const STEP = 500;
|
|
3464
|
+
let reachedEnd = false;
|
|
3465
|
+
if (afterSeq !== undefined) {
|
|
3466
|
+
// ★ 正扫:从 afterSeq+1 扫到最新(升序)—— 只取"比前端已有的更新"的那批
|
|
3467
|
+
const top = Math.max(0, sess.seq - 1);
|
|
3468
|
+
let low = afterSeq + 1;
|
|
3469
|
+
while (low <= top && items.length < limit) {
|
|
3470
|
+
const hi = Math.min(top, low + STEP - 1);
|
|
3471
|
+
let evs = [];
|
|
3472
|
+
if (snap) { try { evs = snap(low, hi + 1) || []; } catch { evs = []; } }
|
|
3473
|
+
if (!evs.length) { const all = sess.events; if (Array.isArray(all) && all.length && all.length > low) evs = all.slice(low, hi + 1); }
|
|
3474
|
+
for (const ev of evs) {
|
|
3475
|
+
const got = chatDecodeEvent(ev, scope, nameByMid, fallbackSender);
|
|
3476
|
+
if (got) {
|
|
3477
|
+
if (Array.isArray(got)) { for (const g of got) items.push(g); }
|
|
3478
|
+
else items.push(got);
|
|
3479
|
+
}
|
|
3480
|
+
if (items.length >= limit) break;
|
|
3481
|
+
}
|
|
3482
|
+
low = hi + 1;
|
|
3483
|
+
}
|
|
3484
|
+
reachedEnd = low > top;
|
|
3485
|
+
} else {
|
|
3486
|
+
let high = beforeSeq !== undefined ? Math.max(0, beforeSeq - 1) : Math.max(0, sess.seq - 1);
|
|
3440
3487
|
while (high >= 0 && items.length < limit) {
|
|
3441
3488
|
const low = Math.max(0, high - STEP + 1);
|
|
3442
3489
|
let evs = [];
|
|
@@ -3454,12 +3501,13 @@ export function apply(ctx) {
|
|
|
3454
3501
|
if (low === 0) { reachedEnd = true; break; }
|
|
3455
3502
|
high = low - 1;
|
|
3456
3503
|
}
|
|
3504
|
+
}
|
|
3457
3505
|
// ⚠️ 2026-09-10 修复(主人实测"同一批次的历史消息会倒序"):
|
|
3458
3506
|
// 原为 items.reverse() —— 倒扫时同一打包块([Chat history begins]…[Chat history ends])
|
|
3459
3507
|
// 拆出的多条共用同一个 ev.seq, 块内本就是旧→新; 整体 reverse 会把"块内顺序"也翻反,
|
|
3460
3508
|
// 表现为"整批之间顺序对、批内倒序"。
|
|
3461
3509
|
// 改为: 按 seq 分块 → 块间倒序(旧→新), 块内保持原顺序。
|
|
3462
|
-
{
|
|
3510
|
+
if (afterSeq === undefined) {
|
|
3463
3511
|
const blocks = [];
|
|
3464
3512
|
for (const it of items) {
|
|
3465
3513
|
const last = blocks[blocks.length - 1];
|