@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
package/client/qqbot-settings.js
CHANGED
|
@@ -2047,6 +2047,13 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
|
|
|
2047
2047
|
if (m) appId = m[1]
|
|
2048
2048
|
} catch (e) {}
|
|
2049
2049
|
}
|
|
2050
|
+
// ★ 优先用「顶栏命中的当前会话」—— 群/私聊通用(2026-10-01 修:原来只支持群,私聊保存会报未命中)
|
|
2051
|
+
var hit = state.detectedHit
|
|
2052
|
+
if (hit && hit.peerId && appId) {
|
|
2053
|
+
var kind = (hit.scope === 'c2c') ? 'c2c' : 'group'
|
|
2054
|
+
return 'qqbot:' + appId + ':' + kind + ':' + String(hit.peerId)
|
|
2055
|
+
}
|
|
2056
|
+
// 回落:面板里选中的群
|
|
2050
2057
|
var gid = ''
|
|
2051
2058
|
try { gid = curHitGid() || '' } catch (e) { gid = '' }
|
|
2052
2059
|
if (!gid) gid = state.gid || ''
|
|
@@ -2060,12 +2067,21 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
|
|
|
2060
2067
|
var win = panel ? panel.querySelector('#dk-ctxless-win') : null
|
|
2061
2068
|
var hint = panel ? panel.querySelector('#dk-ctxless-hint') : null
|
|
2062
2069
|
var key = ctxlessKey()
|
|
2063
|
-
if (!key) { if (hint) hint.textContent = '
|
|
2070
|
+
if (!key) { if (hint) hint.textContent = '先选一个群/私聊'; return }
|
|
2064
2071
|
api('contextless?sessionKey=' + encodeURIComponent(key))
|
|
2065
2072
|
.then(function (d) {
|
|
2066
2073
|
state.ctxless = d || null
|
|
2067
2074
|
if (on) on.checked = !!(d && d.enabled)
|
|
2068
2075
|
if (win && d && typeof d.window === 'number') win.value = String(d.window)
|
|
2076
|
+
// 2026-10-01: 同步 radio(原来只更新了条数框 —— 面板先渲染后拉数据,radio 会停在默认的"按条数")
|
|
2077
|
+
try {
|
|
2078
|
+
var _isSmart = !!(d && d.smart === true)
|
|
2079
|
+
var _rs = document.querySelectorAll('input[name="dk-ctxless-mode"]')
|
|
2080
|
+
Array.prototype.forEach.call(_rs, function (r) {
|
|
2081
|
+
r.checked = (r.value === 'smart') ? _isSmart : !_isSmart
|
|
2082
|
+
})
|
|
2083
|
+
if (win) win.disabled = _isSmart
|
|
2084
|
+
} catch (e) { /* ignore */ }
|
|
2069
2085
|
if (hint) hint.textContent = (d && d.enabled) ? '已开启(本会话)' : ''
|
|
2070
2086
|
})
|
|
2071
2087
|
.catch(function () { if (hint) hint.textContent = '读取失败(面板需刷新/重启后重试)' })
|
|
@@ -2079,14 +2095,25 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
|
|
|
2079
2095
|
if (hint) hint.textContent = '读取当前会话…'
|
|
2080
2096
|
lookupCurrentSession(function () {
|
|
2081
2097
|
var key = ctxlessKey()
|
|
2082
|
-
if (!key) { if (hint) hint.textContent = '未命中 QQ 会话 ——
|
|
2083
|
-
|
|
2098
|
+
if (!key) { if (hint) hint.textContent = '未命中 QQ 会话 —— 先和 bot 在群里或私聊里聊一句(让它成为活跃会话),再回来保存'; return }
|
|
2099
|
+
// 2026-10-01: 模式单选 → smart;smart=true 时 window 不生效(互斥)
|
|
2100
|
+
var _modeEl = document.querySelector('input[name="dk-ctxless-mode"]:checked')
|
|
2101
|
+
var _mode = _modeEl ? String(_modeEl.value) : 'window'
|
|
2102
|
+
var body = { sessionKey: key, enabled: !!(on && on.checked), window: win ? (parseInt(win.value, 10) || 0) : 5, smart: _mode === 'smart' }
|
|
2103
|
+
// radio 切换:智能判断时条数框禁用(互斥的视觉提示)
|
|
2104
|
+
Array.prototype.forEach.call(document.querySelectorAll('input[name="dk-ctxless-mode"]'), function (r) {
|
|
2105
|
+
r.onchange = function () {
|
|
2106
|
+
var w = document.getElementById('dk-ctxless-win')
|
|
2107
|
+
if (w) w.disabled = (r.value === 'smart')
|
|
2108
|
+
}
|
|
2109
|
+
})
|
|
2084
2110
|
if (hint) hint.textContent = '保存中…'
|
|
2085
2111
|
apiPost('contextless', body).then(function (d) {
|
|
2086
2112
|
if (!d || !d.ok) { if (hint) hint.textContent = '保存失败: ' + JSON.stringify(d); return }
|
|
2087
2113
|
// ★ 保存后【回读】验证(这样"到底存进去没有"一眼可见,不用猜)
|
|
2088
2114
|
api('contextless?sessionKey=' + encodeURIComponent(key)).then(function (back) {
|
|
2089
2115
|
var same = back && back.enabled === body.enabled && Number(back.window) === Number(body.window)
|
|
2116
|
+
&& (back.smart === true) === (body.smart === true)
|
|
2090
2117
|
if (hint) {
|
|
2091
2118
|
hint.textContent = same
|
|
2092
2119
|
? ('✅ 已保存并回读确认: ' + (back.enabled ? '开启' : '关闭') + ' / 带 ' + back.window + ' 条')
|
|
@@ -3641,12 +3668,24 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
|
|
|
3641
3668
|
var ctxHit = curHitLabel()
|
|
3642
3669
|
body += '<div class="dk-msg" style="line-height:1.6">开启后, 本会话每轮<b>不继承历史对话</b>(系统规则照旧), 只带上「@ 之前 N 条群消息」—— 群聊省 token 的利器。</div>'
|
|
3643
3670
|
body += '<div class="dk-msg" style="color:#888;font-size:12px;margin:2px 0 6px">作用对象: <b>' + (ctxHit || '(当前会话不是 QQ 会话)') + '</b></div>'
|
|
3671
|
+
var _smartOn = !!(state.ctxless && state.ctxless.smart === true)
|
|
3672
|
+
var _winVal = (state.ctxless && typeof state.ctxless.window === 'number') ? state.ctxless.window : 5
|
|
3644
3673
|
body += '<div class="dk-row" style="gap:8px;flex-wrap:wrap;align-items:center">'
|
|
3645
3674
|
+ '<label style="display:inline-flex;align-items:center;gap:4px;cursor:pointer;font-size:13px"><input type="checkbox" id="dk-ctxless-on"' + (state.ctxless && state.ctxless.enabled ? ' checked' : '') + '>无上下文模式</label>'
|
|
3646
|
-
+ '<span style="font-size:12px;color:#666">带 @ 前 <input type="number" id="dk-ctxless-win" min="0" value="' + ((state.ctxless && typeof state.ctxless.window === 'number') ? state.ctxless.window : 5) + '" style="width:56px"> 条</span>'
|
|
3647
3675
|
+ '<button class="dk-btn ok" id="dk-ctxless-save" style="margin-left:auto">💾 保存(本会话)</button>'
|
|
3648
3676
|
+ '<span class="dk-msg" id="dk-ctxless-hint" style="color:#2f9e44"></span></div>'
|
|
3649
|
-
|
|
3677
|
+
// 2026-10-01: 两种模式**互斥**(单参数是死参数、智能判断交给 AI),所以做成单选
|
|
3678
|
+
body += '<div style="margin:4px 0 0 22px;display:flex;flex-direction:column;gap:8px">'
|
|
3679
|
+
+ '<label style="display:flex;align-items:flex-start;gap:6px;cursor:pointer;font-size:13px">'
|
|
3680
|
+
+ '<input type="radio" name="dk-ctxless-mode" value="window"' + (_smartOn ? '' : ' checked') + ' style="margin-top:3px">'
|
|
3681
|
+
+ '<span>按条数保留 <span style="font-size:12px;color:#666">带 @ 前 <input type="number" id="dk-ctxless-win" min="0" value="' + _winVal + '"' + (_smartOn ? ' disabled' : '') + ' style="width:56px"> 条</span>'
|
|
3682
|
+
+ '<div style="font-size:12px;color:#888;line-height:1.4">不管在聊什么,每轮都只带「@ 前 N 条」。简单、可预期;但<b>同一话题接着聊可能断片</b>,<b>换了话题又会多带</b>。</div></span></label>'
|
|
3683
|
+
+ '<label style="display:flex;align-items:flex-start;gap:6px;cursor:pointer;font-size:13px">'
|
|
3684
|
+
+ '<input type="radio" name="dk-ctxless-mode" value="smart"' + (_smartOn ? ' checked' : '') + ' style="margin-top:3px">'
|
|
3685
|
+
+ '<span>智能判断(推荐)'
|
|
3686
|
+
+ '<div style="font-size:12px;color:#888;line-height:1.4">由 AI <b>每轮自己判断话题是否结束</b>:结束了它就压缩旧历史(可留最近几条)或整段丢弃,<b>只留下自己总结的要点</b> —— 省 token 最狠,也不容易断片。要点存在独立文件里,<b>压缩永远碰不到</b>。</div></span></label>'
|
|
3687
|
+
+ '</div>'
|
|
3688
|
+
body += '<div class="dk-msg" style="line-height:1.5;color:#888">0 条 = 完全不带群消息(只剩系统规则 + 当前这句)。智能判断模式下条数不生效。开关存在插件数据目录, 重启不丢;<b>面板保存即时生效</b>,直接改文件则需重启。</div>'
|
|
3650
3689
|
} else if (st === 'blank') {
|
|
3651
3690
|
body += '<div style="border:1px dashed #cfc7ee;border-radius:8px;padding:20px 14px;margin:8px 0;text-align:center;line-height:2">'
|
|
3652
3691
|
+ '<div style="font-size:15px;font-weight:700">📄 空白样板</div>'
|
|
@@ -3973,6 +4012,20 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
|
|
|
3973
4012
|
if (state.tab !== 'chat' || !state.chatMore || state.chatBusy) return
|
|
3974
4013
|
if (cbox.scrollTop <= 4) loadChat(false) // 顶部 → 加载更早
|
|
3975
4014
|
}
|
|
4015
|
+
// 2026-10-01 主人要求:「滚轮向下滚一次就刷新一下」——**不管有没有滚动成功**
|
|
4016
|
+
// (已经停在底部也刷:方便随时确认有没有新消息,比每个来回点 🔄 刷新省事)。
|
|
4017
|
+
// 向上滚不拦(交给上面的"到顶部加载更早",两个功能不打架)。
|
|
4018
|
+
// ⚠️ 一次滚轮会连发多个 wheel 事件 → 按 400ms 时间窗合并成一次,避免刷爆;
|
|
4019
|
+
// ⚠️ 加载中(busy)直接跳过,别打断正在进行的请求。
|
|
4020
|
+
if (cbox) cbox.onwheel = function (e) {
|
|
4021
|
+
if (state.tab !== 'chat') return
|
|
4022
|
+
if (e && e.deltaY <= 0) return
|
|
4023
|
+
var now = Date.now()
|
|
4024
|
+
if (now - (state.chatWheelAt || 0) < 400) return
|
|
4025
|
+
state.chatWheelAt = now
|
|
4026
|
+
if (state.chatBusy) return
|
|
4027
|
+
loadChatNewer() // ★ 增量: 只把新消息接在下面, 不重绘不闪不跳位置
|
|
4028
|
+
}
|
|
3976
4029
|
var cinput = panel.querySelector('#dk-chat-input')
|
|
3977
4030
|
if (cinput) {
|
|
3978
4031
|
cinput.oninput = function (e) {
|
|
@@ -4313,6 +4366,11 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
|
|
|
4313
4366
|
state.chatMore = d.hasMore === true
|
|
4314
4367
|
state.chatErr = ''
|
|
4315
4368
|
state.chatOldest = state.chatItems.length ? state.chatItems[0].seq : 0
|
|
4369
|
+
// 2026-10-01: 记住"已加载到的**最新事件 seq**" —— 滚轮增量刷新(loadChatNewer)的起点。
|
|
4370
|
+
// ⚠️ 不能拿 d.tailSeq(= sess.seq = 下一个可用 seq): 正扫是 afterSeq+1 起, 记大会**漏一条/取不到**。
|
|
4371
|
+
var _maxSeq = 0
|
|
4372
|
+
state.chatItems.forEach(function (x) { var n = Number(x.seq); if (n > _maxSeq) _maxSeq = n })
|
|
4373
|
+
state.chatNewest = _maxSeq
|
|
4316
4374
|
renderChatList(mode === 'more' ? keep : null)
|
|
4317
4375
|
}).catch(function () {
|
|
4318
4376
|
state.chatBusy = ''
|
|
@@ -4360,26 +4418,21 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
|
|
|
4360
4418
|
try { dd = curDataDir() || '' } catch (e) { dd = '' }
|
|
4361
4419
|
return '/api/qqbot-settings/sticker-img?id=' + encodeURIComponent(idm[1]) + (dd ? '&dataDir=' + encodeURIComponent(dd) : '')
|
|
4362
4420
|
}
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
if (!box) return
|
|
4421
|
+
// 2026-10-01: 状态行单独成函数 —— 增量追加新消息后只更新这行, 不重绘整个列表
|
|
4422
|
+
function updateChatStatus() {
|
|
4366
4423
|
var st = document.getElementById('dk-chat-status')
|
|
4367
|
-
if (st)
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
}
|
|
4380
|
-
var html = ''
|
|
4381
|
-
if (state.chatMore) html += '<div class="dk-chat-top">↑ 上滑加载更早消息</div>'
|
|
4382
|
-
html += state.chatItems.map(function (it) {
|
|
4424
|
+
if (!st) return
|
|
4425
|
+
if (state.chatBusy === 'send') st.textContent = '发送中…'
|
|
4426
|
+
else if (chatFlash) st.textContent = chatFlash
|
|
4427
|
+
else if (state.chatBusy === 'new') st.textContent = '检查新消息…'
|
|
4428
|
+
else if (state.chatBusy) st.textContent = state.chatBusy === 'chat' ? '加载中…' : '加载更早…'
|
|
4429
|
+
else if (state.chatErr) st.textContent = state.chatErr
|
|
4430
|
+
else if (!state.chatItems.length) st.textContent = '暂无记录 —— 机器人和该目标聊过后会显示在这里'
|
|
4431
|
+
else st.textContent = '共 ' + state.chatItems.length + ' 条 · ' + chatPeerName() + (state.chatMore ? ' · 上滑加载更早' : ' · 已到最早')
|
|
4432
|
+
}
|
|
4433
|
+
// 2026-10-01: 单条气泡 HTML 抽成函数 —— renderChatList 与"增量追加"共用同一套渲染
|
|
4434
|
+
function chatRowHTML(it) {
|
|
4435
|
+
{
|
|
4383
4436
|
var isOut = it.dir === 'out'
|
|
4384
4437
|
var who = isOut ? '我' : (it.sender || (state.sendScope === 'c2c' ? c2cSelName() : '群友'))
|
|
4385
4438
|
var ava = isOut ? '🐳' : chatAvaOf(who)
|
|
@@ -4410,11 +4463,13 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
|
|
|
4410
4463
|
+ '<div class="dk-cbubble">' + imgs + txt + '</div>'
|
|
4411
4464
|
+ '</div>'
|
|
4412
4465
|
+ '</div>'
|
|
4413
|
-
}
|
|
4414
|
-
|
|
4415
|
-
|
|
4466
|
+
}
|
|
4467
|
+
}
|
|
4468
|
+
// 2026-10-01: 图片交互绑定抽成函数(增量追加的新节点也要绑; 属性赋值 → 幂等)
|
|
4469
|
+
function bindChatImgs(root) {
|
|
4470
|
+
if (!root) return
|
|
4416
4471
|
// 图片: 单击/右键 → 灯箱放大(右键不再弹浏览器菜单, 避免误触发 onerror 变加载失败)
|
|
4417
|
-
|
|
4472
|
+
root.querySelectorAll('img.dk-img').forEach(function (img) {
|
|
4418
4473
|
var lbOf = function () { return img.getAttribute('data-lb') || img.src }
|
|
4419
4474
|
img.onclick = function (e) { e.preventDefault(); e.stopPropagation(); openLightbox(lbOf()) }
|
|
4420
4475
|
img.oncontextmenu = function (e) { e.preventDefault(); e.stopPropagation(); openLightbox(lbOf()) }
|
|
@@ -4432,10 +4487,87 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
|
|
|
4432
4487
|
img.outerHTML = '<span style="color:#888">[图加载失败]</span>'
|
|
4433
4488
|
}
|
|
4434
4489
|
})
|
|
4490
|
+
}
|
|
4491
|
+
function renderChatList(keepScrollOffset) {
|
|
4492
|
+
var box = document.getElementById('dk-chat-box')
|
|
4493
|
+
if (!box) return
|
|
4494
|
+
updateChatStatus()
|
|
4495
|
+
if (!state.chatItems.length) {
|
|
4496
|
+
var emptyTxt = state.chatErr || (!chatPeerReady() ? '先选一个群/私聊目标' : '还没有聊天记录(该目标暂无活跃会话)')
|
|
4497
|
+
box.innerHTML = '<div class="dk-empty" style="padding:26px 0">' + esc(emptyTxt) + '</div>'
|
|
4498
|
+
return
|
|
4499
|
+
}
|
|
4500
|
+
var html = ''
|
|
4501
|
+
if (state.chatMore) html += '<div class="dk-chat-top">↑ 上滑加载更早消息</div>'
|
|
4502
|
+
html += state.chatItems.map(chatRowHTML).join('')
|
|
4503
|
+
html += '<div class="dk-chat-bottom">—— 会话尾部 · ' + esc(chatPeerName()) + ' ——</div>'
|
|
4504
|
+
box.innerHTML = html
|
|
4505
|
+
bindChatImgs(box)
|
|
4435
4506
|
// 滚动: 刷新/切目标 → 滚到底; 上滚加载更早 → 保持视口
|
|
4436
|
-
if (keepScrollOffset == null)
|
|
4507
|
+
if (keepScrollOffset == null) {
|
|
4508
|
+
box.scrollTop = box.scrollHeight
|
|
4509
|
+
// 2026-10-01 主人实测「打开 dock 显示的不是最新消息」:
|
|
4510
|
+
// 气泡里的图是 loading="lazy",刚渲染时高度还没撑开 → 首次贴底其实只贴到"半截",
|
|
4511
|
+
// 等图加载完内容变高,视口就停在中间了。这里随后再补贴几次;
|
|
4512
|
+
// 用户一旦手动滚(wheel)立刻停手,不跟他抢滚动条。
|
|
4513
|
+
var stickCancel = false
|
|
4514
|
+
try { box.addEventListener('wheel', function () { stickCancel = true }, { once: true, passive: true }) } catch (e) { /* 老浏览器忽略 */ }
|
|
4515
|
+
var stickBottom = function () { if (!stickCancel) box.scrollTop = box.scrollHeight }
|
|
4516
|
+
setTimeout(stickBottom, 80)
|
|
4517
|
+
setTimeout(stickBottom, 240)
|
|
4518
|
+
setTimeout(stickBottom, 600)
|
|
4519
|
+
setTimeout(stickBottom, 1200)
|
|
4520
|
+
}
|
|
4437
4521
|
else box.scrollTop = box.scrollHeight - keepScrollOffset
|
|
4438
4522
|
}
|
|
4523
|
+
// 2026-10-01 主人要求:滚轮向下 → **增量**追加新消息
|
|
4524
|
+
// · 只 append 新气泡(不 box.innerHTML 重绘 → **不闪**)
|
|
4525
|
+
// · **不动 scrollTop**(不跳位置;想看最新的自己继续滑)
|
|
4526
|
+
function appendChatRows(list) {
|
|
4527
|
+
var box = document.getElementById('dk-chat-box')
|
|
4528
|
+
if (!box || !Array.isArray(list) || !list.length) return
|
|
4529
|
+
var html = list.map(chatRowHTML).join('')
|
|
4530
|
+
var tail = box.querySelector('.dk-chat-bottom')
|
|
4531
|
+
if (tail) tail.insertAdjacentHTML('beforebegin', html)
|
|
4532
|
+
else box.insertAdjacentHTML('beforeend', html)
|
|
4533
|
+
bindChatImgs(box)
|
|
4534
|
+
}
|
|
4535
|
+
// 滚轮触发的"取更新的消息":afterSeq = 当前已加载的最新位置
|
|
4536
|
+
function loadChatNewer() {
|
|
4537
|
+
if (!state.ns || !chatPeerReady() || state.chatBusy) return
|
|
4538
|
+
var after = Number(state.chatNewest || 0)
|
|
4539
|
+
if (!(after > 0)) { loadChat(true); return } // 还没加载过 → 走整页首屏
|
|
4540
|
+
state.chatBusy = 'new'
|
|
4541
|
+
updateChatStatus()
|
|
4542
|
+
var peerId = state.sendScope === 'c2c' ? state.sendTo : state.gid
|
|
4543
|
+
var q = 'ns=' + encodeURIComponent(state.ns) + '&scope=' + state.sendScope
|
|
4544
|
+
+ '&peerId=' + encodeURIComponent(peerId) + '&afterSeq=' + after + '&limit=100'
|
|
4545
|
+
api('chat/history', q).then(function (d) {
|
|
4546
|
+
state.chatBusy = ''
|
|
4547
|
+
if (!d || !d.ok) { updateChatStatus(); return }
|
|
4548
|
+
var list = (Array.isArray(d.items) ? d.items : []).filter(function (it) {
|
|
4549
|
+
return it && ((typeof it.text === 'string' && it.text) || (Array.isArray(it.images) && it.images.length))
|
|
4550
|
+
})
|
|
4551
|
+
// 去重键含 seq+方向+正文前缀 —— 群历史打包块里多条共用同一 seq, 只按 seq 去重会误删
|
|
4552
|
+
var keyOf = function (x) { return String(x.seq) + '|' + String(x.dir) + '|' + String(x.text || '').slice(0, 24) }
|
|
4553
|
+
var have = {}
|
|
4554
|
+
state.chatItems.forEach(function (x) { have[keyOf(x)] = 1 })
|
|
4555
|
+
// ⚠️ 2026-10-01 主人实测「向下滚后最新消息后面冒出历史消息」:
|
|
4556
|
+
// 群打包块([历史]…[/历史])会被拆成多条挂在**同一个新 seq** 上, 内容却是**更早**的消息;
|
|
4557
|
+
// 不过滤就会被当新事件整块接到列表尾部。只收"时间不早于列表尾部那条"的条目。
|
|
4558
|
+
var _tailT = state.chatItems.length ? (Number(state.chatItems[state.chatItems.length - 1].time) || 0) : 0
|
|
4559
|
+
var fresh = list.filter(function (x) {
|
|
4560
|
+
return !have[keyOf(x)] && (Number(x.time) || 0) >= _tailT
|
|
4561
|
+
})
|
|
4562
|
+
if (fresh.length) {
|
|
4563
|
+
state.chatItems = state.chatItems.concat(fresh)
|
|
4564
|
+
appendChatRows(fresh) // ★ 增量追加: 不重绘、不闪、不跳位置
|
|
4565
|
+
}
|
|
4566
|
+
// 已加载位置 = 列表里最大的事件 seq(同 ①:不能用 tailSeq)
|
|
4567
|
+
state.chatItems.forEach(function (x) { var n = Number(x.seq); if (n > state.chatNewest) state.chatNewest = n })
|
|
4568
|
+
updateChatStatus()
|
|
4569
|
+
}).catch(function () { state.chatBusy = ''; updateChatStatus() })
|
|
4570
|
+
}
|
|
4439
4571
|
// 聊天文本渲染(2026-09-13 引用消息功能): 把引用块渲染成"引用样式"气泡(左边框+灰底), 其余正文照常。
|
|
4440
4572
|
// 2026-09-15 标记改短([引]…[/引][当前]) —— **新旧都认**, 否则老会话里的英文标记会显示成裸文本。
|
|
4441
4573
|
function chatRenderText(t) {
|