@zaofan/dsh-qqbot 1.5.28 → 1.5.31
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/README_EN.md +24 -24
- package/client/qqbot-settings.js +142 -26
- 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-stick-1790865591295 +5033 -0
- package/dist/channel-tools.d.ts.map +1 -1
- package/dist/channel-tools.js +10 -0
- package/dist/channel-tools.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/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/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/docs/USER-GUIDE.md +118 -115
- package/package.json +1 -1
- package/settings-host.js +116 -16
package/README_EN.md
CHANGED
|
@@ -30,9 +30,9 @@ The little ball at the corner of the settings panel opens a whole control deck:
|
|
|
30
30
|
**🖥️ No config-file surgery** — reply pacing, sticker gates, scheduled wake-ups, outbound mode and per-bot personas are all in the panel; saving applies live (only adding/removing bots needs a restart). There is even a ✏️ persona editor to tweak her "personality file" right in the browser.
|
|
31
31
|
|
|
32
32
|
**📦 Clean & lean** — drive image sending / recall from plain text (`[MEDIA:image|path]` / `[RECALL]`); the repo contains no bot credentials or private data.
|
|
33
|
-
|
|
34
|
-
**📱 No PC needed — she runs 24/7 on your phone**
|
|
35
|
-
Pair her with the community [dsh-mobile-apk](https://github.com/kelai141/dsh-mobile-apk) (an **Android shell APK for dsh**: WebView UI + a bundled Termux runtime snapshot, unzip-and-run) and you get a QQ bot that stays online from your pocket — no dedicated computer to keep running. (Android usually has no ONNX runtime, so remember to tick **“Program fallback (no small model)”** in Settings — see the Android section below.)
|
|
33
|
+
|
|
34
|
+
**📱 No PC needed — she runs 24/7 on your phone**
|
|
35
|
+
Pair her with the community [dsh-mobile-apk](https://github.com/kelai141/dsh-mobile-apk) (an **Android shell APK for dsh**: WebView UI + a bundled Termux runtime snapshot, unzip-and-run) and you get a QQ bot that stays online from your pocket — no dedicated computer to keep running. (Android usually has no ONNX runtime, so remember to tick **“Program fallback (no small model)”** in Settings — see the Android section below.)
|
|
36
36
|
|
|
37
37
|
### 📸 Showcase
|
|
38
38
|
|
|
@@ -149,26 +149,26 @@ npm install --legacy-peer-deps # install-time resolution only; runtime behav
|
|
|
149
149
|
|
|
150
150
|
> Tracked: once upstream #37 is fixed and version lines converge, this section can be removed.
|
|
151
151
|
|
|
152
|
-
### 📱 Deploy on Android — the pocket 24/7 setup
|
|
153
|
-
|
|
154
|
-
[dsh-mobile-apk](https://github.com/kelai141/dsh-mobile-apk) is a community-built **Android shell APK for dsh**
|
|
155
|
-
(WebView UI + a bundled Termux runtime snapshot — unzip and run). Install dsh there, then install this plugin
|
|
156
|
-
into that dsh exactly as documented above, and you end up with a QQ bot that runs **24/7 from your phone**,
|
|
157
|
-
with no PC left on.
|
|
158
|
-
|
|
159
|
-
```
|
|
160
|
-
Download APK: https://github.com/kelai141/dsh-mobile-apk/releases
|
|
161
|
-
(pick arm64 / x86_64 to match your phone's CPU)
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
> ⚠️ **Two switches you must turn on for Android** (otherwise she silently stops replying):
|
|
165
|
-
>
|
|
166
|
-
> 1. **Settings → Smart reply → tick “Program fallback (no small model)”**
|
|
167
|
-
> Phones usually have no ONNX runtime, so the real embedding model fails to load. With the fallback on,
|
|
168
|
-
> scoring switches to **character n-gram lexical similarity** (zero dependencies, zero tokens) and works.
|
|
169
|
-
> 2. **System settings → Battery → allow background activity / add to the power-saving whitelist**
|
|
170
|
-
> Otherwise the OS kills the Termux process once the screen is off and the bot goes offline.
|
|
171
|
-
|
|
152
|
+
### 📱 Deploy on Android — the pocket 24/7 setup
|
|
153
|
+
|
|
154
|
+
[dsh-mobile-apk](https://github.com/kelai141/dsh-mobile-apk) is a community-built **Android shell APK for dsh**
|
|
155
|
+
(WebView UI + a bundled Termux runtime snapshot — unzip and run). Install dsh there, then install this plugin
|
|
156
|
+
into that dsh exactly as documented above, and you end up with a QQ bot that runs **24/7 from your phone**,
|
|
157
|
+
with no PC left on.
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
Download APK: https://github.com/kelai141/dsh-mobile-apk/releases
|
|
161
|
+
(pick arm64 / x86_64 to match your phone's CPU)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
> ⚠️ **Two switches you must turn on for Android** (otherwise she silently stops replying):
|
|
165
|
+
>
|
|
166
|
+
> 1. **Settings → Smart reply → tick “Program fallback (no small model)”**
|
|
167
|
+
> Phones usually have no ONNX runtime, so the real embedding model fails to load. With the fallback on,
|
|
168
|
+
> scoring switches to **character n-gram lexical similarity** (zero dependencies, zero tokens) and works.
|
|
169
|
+
> 2. **System settings → Battery → allow background activity / add to the power-saving whitelist**
|
|
170
|
+
> Otherwise the OS kills the Termux process once the screen is off and the bot goes offline.
|
|
171
|
+
|
|
172
172
|
### First launch & binding
|
|
173
173
|
|
|
174
174
|
Start `dsh web`. If credentials are missing, the **QR flow** starts automatically: a QR code is printed in the
|
|
@@ -366,7 +366,7 @@ Then run `/tools-reload` (or ask the AI to call the `tools_reload` tool). Comman
|
|
|
366
366
|
**For AI agents — you can write these yourself (no feature request needed):**
|
|
367
367
|
- Card bodies are **plain markdown you write** (`#`, bold, ``, fenced code). This plugin has **no template engine — the model is the engine**. Cards with buttons go through a botplay event or the dock card editor (button callbacks must be registered host-side).
|
|
368
368
|
- Extensions live in the **account data directory** (`.qqbot-extensions/`), never inside the plugin package → **upgrading or reinstalling the plugin never overwrites them**.
|
|
369
|
-
- `run(args, env)` receives `env = { cwd, manager, sender, replyTarget, exec }`. `sender` + `replyTarget` is the very same sender the built-in `send_media` tool uses, so a tool can **post markdown cards / images / voice / files by itself**: "fetch an API → build markdown → send" closes the loop inside one tool.
|
|
369
|
+
- `run(args, env)` receives `env = { cwd, manager, sender, replyTarget, exec, ctx, logger }`. `sender` + `replyTarget` is the very same sender the built-in `send_media` tool uses, so a tool can **post markdown cards / images / voice / files by itself**: "fetch an API → build markdown → send" closes the loop inside one tool. `ctx` is the **host plugin context** (same origin as the built-in tools, available since 1.5.30), so an extension can call host services on its own — e.g. `ctx.compaction.compactNow(agent, exec.signal, id)` to compact the session, or `ctx.get('service')` to probe an optional one; `logger` comes with it. ⚠️ Same privilege level as built-in tools — only for extensions **you wrote yourself** in your own data root.
|
|
370
370
|
- Tools reply `{ ok, msg }` (fed back to you as the tool result); commands return plain text.
|
|
371
371
|
- Hot reload: `/tools-reload` (or call `tools_reload`) picks up **new** tools; **editing a tool under the same name is skipped by the registry → rename it or restart the host**; commands always need a restart.
|
|
372
372
|
- Limitation: extensions **cannot register button callbacks** — those require a botplay event or the dock card editor. Extension-only flows are "card + user replies a number", with the AI acting as the state machine.
|
package/client/qqbot-settings.js
CHANGED
|
@@ -2641,7 +2641,10 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
|
|
|
2641
2641
|
}
|
|
2642
2642
|
function groupSelName() {
|
|
2643
2643
|
var g = null; state.groups.forEach(function (x) { if (x.gid === state.gid) g = x })
|
|
2644
|
-
|
|
2644
|
+
if (g) return g.name || state.gid.slice(0, 10)
|
|
2645
|
+
// 2026-10-01: gid 有值但没在台账里(群未登记/刚切实例还没拉到列表)时, 别硬显示"(未选群)"
|
|
2646
|
+
// —— 那会让人以为没选中目标; 退化成 gid 前段更好定位
|
|
2647
|
+
return state.gid ? state.gid.slice(0, 10) : '(未选群)'
|
|
2645
2648
|
}
|
|
2646
2649
|
function c2cSelName() {
|
|
2647
2650
|
if (state.sendName) return state.sendName
|
|
@@ -3970,6 +3973,20 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
|
|
|
3970
3973
|
if (state.tab !== 'chat' || !state.chatMore || state.chatBusy) return
|
|
3971
3974
|
if (cbox.scrollTop <= 4) loadChat(false) // 顶部 → 加载更早
|
|
3972
3975
|
}
|
|
3976
|
+
// 2026-10-01 主人要求:「滚轮向下滚一次就刷新一下」——**不管有没有滚动成功**
|
|
3977
|
+
// (已经停在底部也刷:方便随时确认有没有新消息,比每个来回点 🔄 刷新省事)。
|
|
3978
|
+
// 向上滚不拦(交给上面的"到顶部加载更早",两个功能不打架)。
|
|
3979
|
+
// ⚠️ 一次滚轮会连发多个 wheel 事件 → 按 400ms 时间窗合并成一次,避免刷爆;
|
|
3980
|
+
// ⚠️ 加载中(busy)直接跳过,别打断正在进行的请求。
|
|
3981
|
+
if (cbox) cbox.onwheel = function (e) {
|
|
3982
|
+
if (state.tab !== 'chat') return
|
|
3983
|
+
if (e && e.deltaY <= 0) return
|
|
3984
|
+
var now = Date.now()
|
|
3985
|
+
if (now - (state.chatWheelAt || 0) < 400) return
|
|
3986
|
+
state.chatWheelAt = now
|
|
3987
|
+
if (state.chatBusy) return
|
|
3988
|
+
loadChatNewer() // ★ 增量: 只把新消息接在下面, 不重绘不闪不跳位置
|
|
3989
|
+
}
|
|
3973
3990
|
var cinput = panel.querySelector('#dk-chat-input')
|
|
3974
3991
|
if (cinput) {
|
|
3975
3992
|
cinput.oninput = function (e) {
|
|
@@ -4310,6 +4327,11 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
|
|
|
4310
4327
|
state.chatMore = d.hasMore === true
|
|
4311
4328
|
state.chatErr = ''
|
|
4312
4329
|
state.chatOldest = state.chatItems.length ? state.chatItems[0].seq : 0
|
|
4330
|
+
// 2026-10-01: 记住"已加载到的**最新事件 seq**" —— 滚轮增量刷新(loadChatNewer)的起点。
|
|
4331
|
+
// ⚠️ 不能拿 d.tailSeq(= sess.seq = 下一个可用 seq): 正扫是 afterSeq+1 起, 记大会**漏一条/取不到**。
|
|
4332
|
+
var _maxSeq = 0
|
|
4333
|
+
state.chatItems.forEach(function (x) { var n = Number(x.seq); if (n > _maxSeq) _maxSeq = n })
|
|
4334
|
+
state.chatNewest = _maxSeq
|
|
4313
4335
|
renderChatList(mode === 'more' ? keep : null)
|
|
4314
4336
|
}).catch(function () {
|
|
4315
4337
|
state.chatBusy = ''
|
|
@@ -4357,26 +4379,21 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
|
|
|
4357
4379
|
try { dd = curDataDir() || '' } catch (e) { dd = '' }
|
|
4358
4380
|
return '/api/qqbot-settings/sticker-img?id=' + encodeURIComponent(idm[1]) + (dd ? '&dataDir=' + encodeURIComponent(dd) : '')
|
|
4359
4381
|
}
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
if (!box) return
|
|
4382
|
+
// 2026-10-01: 状态行单独成函数 —— 增量追加新消息后只更新这行, 不重绘整个列表
|
|
4383
|
+
function updateChatStatus() {
|
|
4363
4384
|
var st = document.getElementById('dk-chat-status')
|
|
4364
|
-
if (st)
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
}
|
|
4377
|
-
var html = ''
|
|
4378
|
-
if (state.chatMore) html += '<div class="dk-chat-top">↑ 上滑加载更早消息</div>'
|
|
4379
|
-
html += state.chatItems.map(function (it) {
|
|
4385
|
+
if (!st) return
|
|
4386
|
+
if (state.chatBusy === 'send') st.textContent = '发送中…'
|
|
4387
|
+
else if (chatFlash) st.textContent = chatFlash
|
|
4388
|
+
else if (state.chatBusy === 'new') st.textContent = '检查新消息…'
|
|
4389
|
+
else if (state.chatBusy) st.textContent = state.chatBusy === 'chat' ? '加载中…' : '加载更早…'
|
|
4390
|
+
else if (state.chatErr) st.textContent = state.chatErr
|
|
4391
|
+
else if (!state.chatItems.length) st.textContent = '暂无记录 —— 机器人和该目标聊过后会显示在这里'
|
|
4392
|
+
else st.textContent = '共 ' + state.chatItems.length + ' 条 · ' + chatPeerName() + (state.chatMore ? ' · 上滑加载更早' : ' · 已到最早')
|
|
4393
|
+
}
|
|
4394
|
+
// 2026-10-01: 单条气泡 HTML 抽成函数 —— renderChatList 与"增量追加"共用同一套渲染
|
|
4395
|
+
function chatRowHTML(it) {
|
|
4396
|
+
{
|
|
4380
4397
|
var isOut = it.dir === 'out'
|
|
4381
4398
|
var who = isOut ? '我' : (it.sender || (state.sendScope === 'c2c' ? c2cSelName() : '群友'))
|
|
4382
4399
|
var ava = isOut ? '🐳' : chatAvaOf(who)
|
|
@@ -4407,11 +4424,13 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
|
|
|
4407
4424
|
+ '<div class="dk-cbubble">' + imgs + txt + '</div>'
|
|
4408
4425
|
+ '</div>'
|
|
4409
4426
|
+ '</div>'
|
|
4410
|
-
}
|
|
4411
|
-
|
|
4412
|
-
|
|
4427
|
+
}
|
|
4428
|
+
}
|
|
4429
|
+
// 2026-10-01: 图片交互绑定抽成函数(增量追加的新节点也要绑; 属性赋值 → 幂等)
|
|
4430
|
+
function bindChatImgs(root) {
|
|
4431
|
+
if (!root) return
|
|
4413
4432
|
// 图片: 单击/右键 → 灯箱放大(右键不再弹浏览器菜单, 避免误触发 onerror 变加载失败)
|
|
4414
|
-
|
|
4433
|
+
root.querySelectorAll('img.dk-img').forEach(function (img) {
|
|
4415
4434
|
var lbOf = function () { return img.getAttribute('data-lb') || img.src }
|
|
4416
4435
|
img.onclick = function (e) { e.preventDefault(); e.stopPropagation(); openLightbox(lbOf()) }
|
|
4417
4436
|
img.oncontextmenu = function (e) { e.preventDefault(); e.stopPropagation(); openLightbox(lbOf()) }
|
|
@@ -4429,14 +4448,108 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
|
|
|
4429
4448
|
img.outerHTML = '<span style="color:#888">[图加载失败]</span>'
|
|
4430
4449
|
}
|
|
4431
4450
|
})
|
|
4451
|
+
}
|
|
4452
|
+
function renderChatList(keepScrollOffset) {
|
|
4453
|
+
var box = document.getElementById('dk-chat-box')
|
|
4454
|
+
if (!box) return
|
|
4455
|
+
updateChatStatus()
|
|
4456
|
+
if (!state.chatItems.length) {
|
|
4457
|
+
var emptyTxt = state.chatErr || (!chatPeerReady() ? '先选一个群/私聊目标' : '还没有聊天记录(该目标暂无活跃会话)')
|
|
4458
|
+
box.innerHTML = '<div class="dk-empty" style="padding:26px 0">' + esc(emptyTxt) + '</div>'
|
|
4459
|
+
return
|
|
4460
|
+
}
|
|
4461
|
+
var html = ''
|
|
4462
|
+
if (state.chatMore) html += '<div class="dk-chat-top">↑ 上滑加载更早消息</div>'
|
|
4463
|
+
html += state.chatItems.map(chatRowHTML).join('')
|
|
4464
|
+
html += '<div class="dk-chat-bottom">—— 会话尾部 · ' + esc(chatPeerName()) + ' ——</div>'
|
|
4465
|
+
box.innerHTML = html
|
|
4466
|
+
bindChatImgs(box)
|
|
4432
4467
|
// 滚动: 刷新/切目标 → 滚到底; 上滚加载更早 → 保持视口
|
|
4433
|
-
if (keepScrollOffset == null)
|
|
4468
|
+
if (keepScrollOffset == null) {
|
|
4469
|
+
box.scrollTop = box.scrollHeight
|
|
4470
|
+
// 2026-10-01 主人实测「打开 dock 显示的不是最新消息」:
|
|
4471
|
+
// 气泡里的图是 loading="lazy",刚渲染时高度还没撑开 → 首次贴底其实只贴到"半截",
|
|
4472
|
+
// 等图加载完内容变高,视口就停在中间了。这里随后再补贴几次;
|
|
4473
|
+
// 用户一旦手动滚(wheel)立刻停手,不跟他抢滚动条。
|
|
4474
|
+
var stickCancel = false
|
|
4475
|
+
try { box.addEventListener('wheel', function () { stickCancel = true }, { once: true, passive: true }) } catch (e) { /* 老浏览器忽略 */ }
|
|
4476
|
+
var stickBottom = function () { if (!stickCancel) box.scrollTop = box.scrollHeight }
|
|
4477
|
+
setTimeout(stickBottom, 80)
|
|
4478
|
+
setTimeout(stickBottom, 240)
|
|
4479
|
+
setTimeout(stickBottom, 600)
|
|
4480
|
+
setTimeout(stickBottom, 1200)
|
|
4481
|
+
}
|
|
4434
4482
|
else box.scrollTop = box.scrollHeight - keepScrollOffset
|
|
4435
4483
|
}
|
|
4484
|
+
// 2026-10-01 主人要求:滚轮向下 → **增量**追加新消息
|
|
4485
|
+
// · 只 append 新气泡(不 box.innerHTML 重绘 → **不闪**)
|
|
4486
|
+
// · **不动 scrollTop**(不跳位置;想看最新的自己继续滑)
|
|
4487
|
+
function appendChatRows(list) {
|
|
4488
|
+
var box = document.getElementById('dk-chat-box')
|
|
4489
|
+
if (!box || !Array.isArray(list) || !list.length) return
|
|
4490
|
+
var html = list.map(chatRowHTML).join('')
|
|
4491
|
+
var tail = box.querySelector('.dk-chat-bottom')
|
|
4492
|
+
if (tail) tail.insertAdjacentHTML('beforebegin', html)
|
|
4493
|
+
else box.insertAdjacentHTML('beforeend', html)
|
|
4494
|
+
bindChatImgs(box)
|
|
4495
|
+
}
|
|
4496
|
+
// 滚轮触发的"取更新的消息":afterSeq = 当前已加载的最新位置
|
|
4497
|
+
function loadChatNewer() {
|
|
4498
|
+
if (!state.ns || !chatPeerReady() || state.chatBusy) return
|
|
4499
|
+
var after = Number(state.chatNewest || 0)
|
|
4500
|
+
if (!(after > 0)) { loadChat(true); return } // 还没加载过 → 走整页首屏
|
|
4501
|
+
state.chatBusy = 'new'
|
|
4502
|
+
updateChatStatus()
|
|
4503
|
+
var peerId = state.sendScope === 'c2c' ? state.sendTo : state.gid
|
|
4504
|
+
var q = 'ns=' + encodeURIComponent(state.ns) + '&scope=' + state.sendScope
|
|
4505
|
+
+ '&peerId=' + encodeURIComponent(peerId) + '&afterSeq=' + after + '&limit=100'
|
|
4506
|
+
api('chat/history', q).then(function (d) {
|
|
4507
|
+
state.chatBusy = ''
|
|
4508
|
+
if (!d || !d.ok) { updateChatStatus(); return }
|
|
4509
|
+
var list = (Array.isArray(d.items) ? d.items : []).filter(function (it) {
|
|
4510
|
+
return it && ((typeof it.text === 'string' && it.text) || (Array.isArray(it.images) && it.images.length))
|
|
4511
|
+
})
|
|
4512
|
+
// 去重键含 seq+方向+正文前缀 —— 群历史打包块里多条共用同一 seq, 只按 seq 去重会误删
|
|
4513
|
+
var keyOf = function (x) { return String(x.seq) + '|' + String(x.dir) + '|' + String(x.text || '').slice(0, 24) }
|
|
4514
|
+
var have = {}
|
|
4515
|
+
state.chatItems.forEach(function (x) { have[keyOf(x)] = 1 })
|
|
4516
|
+
// ⚠️ 2026-10-01 主人实测「向下滚后最新消息后面冒出历史消息」:
|
|
4517
|
+
// 群打包块([历史]…[/历史])会被拆成多条挂在**同一个新 seq** 上, 内容却是**更早**的消息;
|
|
4518
|
+
// 不过滤就会被当新事件整块接到列表尾部。只收"时间不早于列表尾部那条"的条目。
|
|
4519
|
+
var _tailT = state.chatItems.length ? (Number(state.chatItems[state.chatItems.length - 1].time) || 0) : 0
|
|
4520
|
+
var fresh = list.filter(function (x) {
|
|
4521
|
+
return !have[keyOf(x)] && (Number(x.time) || 0) >= _tailT
|
|
4522
|
+
})
|
|
4523
|
+
if (fresh.length) {
|
|
4524
|
+
state.chatItems = state.chatItems.concat(fresh)
|
|
4525
|
+
appendChatRows(fresh) // ★ 增量追加: 不重绘、不闪、不跳位置
|
|
4526
|
+
}
|
|
4527
|
+
// 已加载位置 = 列表里最大的事件 seq(同 ①:不能用 tailSeq)
|
|
4528
|
+
state.chatItems.forEach(function (x) { var n = Number(x.seq); if (n > state.chatNewest) state.chatNewest = n })
|
|
4529
|
+
updateChatStatus()
|
|
4530
|
+
}).catch(function () { state.chatBusy = ''; updateChatStatus() })
|
|
4531
|
+
}
|
|
4436
4532
|
// 聊天文本渲染(2026-09-13 引用消息功能): 把引用块渲染成"引用样式"气泡(左边框+灰底), 其余正文照常。
|
|
4437
4533
|
// 2026-09-15 标记改短([引]…[/引][当前]) —— **新旧都认**, 否则老会话里的英文标记会显示成裸文本。
|
|
4438
4534
|
function chatRenderText(t) {
|
|
4439
|
-
var src = String(t || '')
|
|
4535
|
+
var src = String(t || '')
|
|
4536
|
+
.replace(/\[(?:当前|Current message)\]\s*/gi, '')
|
|
4537
|
+
// 2026-10-01: 历史块标记(新旧)一律剥掉 —— 后端已把历史段拆成独立气泡, 这里兜底老会话/异常残留
|
|
4538
|
+
.replace(/\[(?:\/?历史|\/?(?:Chat history begins|Chat history ends))\]/gi, '')
|
|
4539
|
+
// 后端把引用块折叠成 `[引用]` → 渲染成灰色提示文字, 别显示成裸方括号(也不该被当发送者)
|
|
4540
|
+
.replace(/\[引用\]/g, '↩ 引用上文')
|
|
4541
|
+
// 媒体标记残留兜底(后端已抽成缩略图/播放器): **只显文件名, 不把本机绝对路径暴露在气泡里**
|
|
4542
|
+
.replace(/\[(图片|附件|文件|语音|视频|音频|Image|Video|Voice|File|Attachment):\s*([^\]]*)\]/gi, function (all, tag, inner) {
|
|
4543
|
+
var s = String(inner || '').trim()
|
|
4544
|
+
if (!s) return ''
|
|
4545
|
+
var tg = String(tag).toLowerCase()
|
|
4546
|
+
var icon = (tg === '图片' || tg === 'image') ? '🖼'
|
|
4547
|
+
: (tg === '视频' || tg === 'video') ? '🎬'
|
|
4548
|
+
: (tg === '语音' || tg === '音频' || tg === 'voice') ? '🎵' : '📎'
|
|
4549
|
+
if (/^https?:/i.test(s)) return icon + ' 媒体'
|
|
4550
|
+
var nm = s.replace(/[?#].*$/, '').split(/[\\/]/).pop() || s
|
|
4551
|
+
return icon + ' ' + nm
|
|
4552
|
+
})
|
|
4440
4553
|
var re = /\[(?:引|Quoted message begins)\]([\s\S]*?)\[\/(?:引|Quoted message ends)\]/g
|
|
4441
4554
|
var parts = []
|
|
4442
4555
|
var m, last = 0
|
|
@@ -4454,6 +4567,9 @@ var QQS_CSS = ".qqs-btn{font:inherit;color:#333;background:linear-gradient(180de
|
|
|
4454
4567
|
function chatAvaOf(who) {
|
|
4455
4568
|
var s = String(who || '').trim()
|
|
4456
4569
|
if (!s) return '?'
|
|
4570
|
+
// 脏数据兜底(2026-10-01): 万一 sender 还是块标记/方括号壳, 别把 "[" 当头像字
|
|
4571
|
+
s = s.replace(/^[\[[((【]+|[\]]))】]+$/g, '').trim()
|
|
4572
|
+
if (!s || /^(?:历|历史|当前|引|引用|(?:Chat history|Quoted message|Current message)(?:\s+begins|\s+ends)?)$/i.test(s)) return '❔'
|
|
4457
4573
|
try { return Array.from(s)[0] } catch (e) { return s[0] || '?' }
|
|
4458
4574
|
}
|
|
4459
4575
|
function setChatStatus(msg) {
|