dsh-qq-link 1.4.6 → 1.4.8
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 +46 -0
- package/lib/host.js +68 -11
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,49 @@
|
|
|
1
|
+
## 1.4.8 — 2026-11-01
|
|
2
|
+
|
|
3
|
+
**QQ 转进来的消息被判成「你在 DSH 窗口里自己打的」—— 「只回 QQ 唤醒」开着时,QQ 那边永远收不到回复**
|
|
4
|
+
|
|
5
|
+
- 🐞 现象:开着「只回 QQ 唤醒」,从 QQ 发一句,DSH 里正常回了(页面上看得见),**QQ 那边什么都没有**;
|
|
6
|
+
日志写的是「这一轮是你在 DSH 窗口里自己打的」。1.4.7 想修的正是这个,但那版**没生效**。
|
|
7
|
+
- 根因:读错了字段。`user/message` 这种事件的 `data` **就是消息本体**(`{ id, role, content, source }`),
|
|
8
|
+
`source` 在 `event.data.source`;只有 `assistant/message` 才包一层 `data.message`。
|
|
9
|
+
代码读的是 `event.data.message.source` —— 永远是 `undefined`,于是 `kind` 永远空、每一轮都当成"你手打的"。
|
|
10
|
+
- ✅ 两种形状都试(`data.source || data.message.source`),哪天真换了也不会立刻断。
|
|
11
|
+
- ⚠ 测试也跟着错了:用例里把假事件造成 `data.message.source`,所以一直"通过"。
|
|
12
|
+
现在用例改成真实形状,这条才算真的被盯住。
|
|
13
|
+
|
|
14
|
+
**群门槛判过「不接」之后,下一轮正常的话会被整轮吞掉**
|
|
15
|
+
|
|
16
|
+
- 🐞 现象:开着群里那个「回复门槛」,某轮判定「这条不是在跟我说话」之后,
|
|
17
|
+
**紧接着下一轮它好好回了一句,QQ 那边却什么都没出现**,日志里只有一句「这一轮按判定跳过发送」。
|
|
18
|
+
- 根因:门槛判「不接」时会挂一个「这轮别往 QQ 发」的静默标记,靠 `turn/end` 清掉。
|
|
19
|
+
但清理那段写在「这一轮真的说完话」的分支里,下面两种轮次**根本走不到**:
|
|
20
|
+
· 开了「真停轮」→ 这一轮是被 cancel 停的,`turn/end` 是 **aborted**;
|
|
21
|
+
· 这一轮一个字都没说出来(没有内容)。
|
|
22
|
+
标记于是漏到下一轮,下一轮正常结束时被当成「判定过的不接」整轮跳过。
|
|
23
|
+
- ✅ 现在静默标记**只对触发它的那一轮有效**:`turn/end` 一到就作废(不再看这一轮是怎么结束的);
|
|
24
|
+
另外 `turn/start` 也会清一次,兜住那些**没有 turn/end**的轮次(宿主被杀、会话被换掉、轮次被丢掉)。
|
|
25
|
+
|
|
26
|
+
## 1.4.7 — 2026-11-01
|
|
27
|
+
|
|
28
|
+
**「只回 QQ 唤醒」开着时,QQ 叫醒的那一轮也发不出去(1.4.4 带进来的)**
|
|
29
|
+
|
|
30
|
+
- 🐞 现象:开着「只回 QQ 唤醒」,从 QQ 发一句,DSH 里正常回,**QQ 那边什么都没有**;
|
|
31
|
+
把这个开关关掉就正常。手动点「试发」也正常 —— 说明发送通道没问题,是**判定把这一轮拦了**。
|
|
32
|
+
- 根因:1.4.4 为了修「自动触发的轮次也被挡」,把判据从「最近有没有 QQ 消息」改成「这一轮是谁起的头」,
|
|
33
|
+
用一张 `轮来源` 表记 `source.kind`。但那行是 `轮来源.set(键, 类 || 'user')` —— **每来一条 user/message 就覆盖**。
|
|
34
|
+
而同一轮里 DSH 还会再插一条**没有 `source.kind`** 的注入(每轮的 runtime context 就是独立的 user/message),
|
|
35
|
+
它把 `qq` 覆盖成 `user`,于是这一轮被判成「你在窗口里自己打的」→ 整轮丢掉。
|
|
36
|
+
- ✅ 现在:**带 kind 才覆盖**;没有 kind 的只在「这一轮什么来源都还没记过」时才当成本地手打。
|
|
37
|
+
另外单独记「这一轮里出现过 qq」,判定时只要出现过就算 QQ 唤醒,后来的注入盖不掉它。
|
|
38
|
+
|
|
39
|
+
**顺带:连着发的几条攒成一条**
|
|
40
|
+
|
|
41
|
+
- ✅ 默认值 **1 秒 → 3 秒**(界面文案一直写的是「默认 3」,代码里却是 1 —— 新装的人拿到 1 秒,
|
|
42
|
+
人手打两条字常隔 2~4 秒,于是看起来就是"连着发不合并")。
|
|
43
|
+
- 🐞 窗口以前按 **QQ 消息自带的时间戳**算(`现在 - 条.time`):服务器时间只要比本机快一点,差值就是负的,
|
|
44
|
+
代码直接不等 ⇒ **永远不合并**,窗口设多大都没用。现在按**本机第一次看到这条消息的时刻**算。
|
|
45
|
+
- ⚠ 水位(下次从哪条开始读)仍然用消息自己的时间,没有被上面的改动带偏。
|
|
46
|
+
|
|
1
47
|
## 1.4.6 — 2026-11-01
|
|
2
48
|
|
|
3
49
|
**「检查更新」以前会报错版本,而且只给一条命令让你自己手动复制、然后手动装**
|
package/lib/host.js
CHANGED
|
@@ -116,7 +116,7 @@ const 默认配置 = {
|
|
|
116
116
|
// · >0 = 同一个人在这段时间内连着发的,合成一条(私聊、群都管用)
|
|
117
117
|
// 实现上是「等一小会儿再收」:最后一条没过去这么久,就先不消费,下一跳连新的那条一起收。
|
|
118
118
|
// ⚠ 别开大:一轮拉历史里本来就能看到「已经发完的那一串」,这里只兜「正好撞上轮询」的那一下。
|
|
119
|
-
合并窗口秒:
|
|
119
|
+
合并窗口秒: 3,
|
|
120
120
|
// 最多攒多久(秒):有人一直发个不停,到点也得先转进来,不能无限等
|
|
121
121
|
合并最多秒: 20,
|
|
122
122
|
// ⭐ 1.1.0(别人要的):人设(选填)。**塞进每条转进去的消息最前面**,每轮都带。
|
|
@@ -1007,6 +1007,9 @@ export function apply(ctx) {
|
|
|
1007
1007
|
// ⭐ 0.6.0:每个「聊天」各自记「看到哪儿了」(键 = 类型:号码)—— 多群多私聊才不会互相串
|
|
1008
1008
|
const 上次时间 = new Map() // 水位必须先初始化,再恢复磁盘状态
|
|
1009
1009
|
const 已转消息 = new Set() // 已经转发进 DSH 的 QQ 消息 id(防重复转)
|
|
1010
|
+
// ⭐ 1.4.7:每条消息「本机第一次看到它」的时间(秒)—— 攒条窗口按它算,
|
|
1011
|
+
// 这样既不依赖 QQ 服务器的时间戳,也不会因为每一跳重读同一批而把窗口无限延长。
|
|
1012
|
+
const 见时 = new Map()
|
|
1010
1013
|
// ⚠️ 2026-10-25:
|
|
1011
1014
|
// 这两个东西以前**只存在内存里** —— 插件一重启(今天改了鱼的心情 / 换插件都要重启),
|
|
1012
1015
|
// 「看到哪儿了」就归零 ⇒ 最近那十几条历史消息会**被当成新消息重投一遍**,
|
|
@@ -1403,6 +1406,15 @@ export function apply(ctx) {
|
|
|
1403
1406
|
const 上一轮 = 上一轮们.get(键) || 空轮()
|
|
1404
1407
|
const 记上一轮 = (v) => 上一轮们.set(键, v)
|
|
1405
1408
|
|
|
1409
|
+
// ⭐ 1.4.8(issue #27):新一轮一开,上一轮的「静默标记」就必须作废。
|
|
1410
|
+
// 正常路径下面 turn/end 会清;这里兜住那些**没发 turn/end 就没了**的轮次
|
|
1411
|
+
// (宿主被杀、会话被换掉、插件在这一轮里被卸载重载),
|
|
1412
|
+
// 否则标记会一直挂着,把这个会话接下来的回复全吞掉(表现是「它明明回了,群里却没有」)。
|
|
1413
|
+
if (event && event.type === 'turn/start') {
|
|
1414
|
+
静默轮.delete(键)
|
|
1415
|
+
return
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1406
1418
|
if (event && event.type === 'assistant/message') {
|
|
1407
1419
|
const d = event.data || {}
|
|
1408
1420
|
if (d.interrupted) {
|
|
@@ -1420,17 +1432,40 @@ export function apply(ctx) {
|
|
|
1420
1432
|
// ⭐ 1.0.1:她自己在这间会话里打了字(source 不是 qq)→ 尾巴立刻清掉,
|
|
1421
1433
|
// 不然「QQ 叫醒一次」会把后面她自己聊的几轮也推去手机(老坑)。
|
|
1422
1434
|
if (event && event.type === 'user/message') {
|
|
1423
|
-
|
|
1435
|
+
// ⚠⚠⚠ 1.4.8:`source` 在**事件自己的 data 上** —— user/message 的 data 就是消息本体
|
|
1436
|
+
// (`{ id, role, content, source }`,实测她本机会话日志的 7388 条 user/message 全是这个形状)。
|
|
1437
|
+
// 只有 assistant/message 那种才包一层 `data.message`。
|
|
1438
|
+
// 1.4.4~1.4.7 读的是 `event.data.message.source` —— 永远是 undefined ⇒ `kind` 永远空
|
|
1439
|
+
// ⇒ 每一轮都被判成「你在 DSH 窗口里自己打的」⇒「只回 QQ 唤醒」开着时,
|
|
1440
|
+
// **QQ 转进来的消息回不出去**(日志写「这一轮是你在 DSH 窗口里自己打的」,明明是从 QQ 来的)。
|
|
1441
|
+
// 两种形状都试,哪天真换了也不会立刻断。
|
|
1442
|
+
const 源 = (event.data && (event.data.source || (event.data.message && event.data.message.source))) || null
|
|
1424
1443
|
const 类 = String((源 && 源.kind) || '')
|
|
1425
1444
|
if (!类 || 类 === 'user') 尾巴.delete(键)
|
|
1426
1445
|
// ⭐ 1.4.4:把「这一轮是谁起的头」记下来 —— turn/end 判定要不要推到手机时用
|
|
1427
|
-
|
|
1446
|
+
// ⚠⚠ 1.4.7:以前是 `轮来源.set(键, 类 || 'user')` —— **无条件覆盖**。而同一轮里 DSH 还会再插
|
|
1447
|
+
// 一条没有 source.kind 的注入(每轮的 runtime context 就是独立的一条 user/message),
|
|
1448
|
+
// 于是「QQ 叫醒的那一轮」被改判成「你在窗口里自己打的」,「只回 QQ 唤醒」开着时整轮被丢掉。
|
|
1449
|
+
// 现在:**带 kind 才覆盖**;没有 kind 的只在「这一轮什么来源都还没记过」时才当成本地手打。
|
|
1450
|
+
if (类) {
|
|
1451
|
+
轮来源.set(键, 类)
|
|
1452
|
+
if (类 === 'qq') 本轮有QQ.set(键, true)
|
|
1453
|
+
} else if (!轮来源.has(键)) {
|
|
1454
|
+
轮来源.set(键, 'user')
|
|
1455
|
+
}
|
|
1428
1456
|
return
|
|
1429
1457
|
}
|
|
1430
1458
|
|
|
1431
1459
|
if (event && event.type === 'turn/end') {
|
|
1432
1460
|
const d = event.data || {}
|
|
1433
1461
|
const 完成 = !!(d.reason && d.reason.kind === 'completed')
|
|
1462
|
+
// ⭐ 1.4.8(issue #27):静默标记只对**触发它的那一轮**有效,这一轮一结束就作废。
|
|
1463
|
+
// 以前只在「这一轮真的说完了话」那条路上清,下面两种情况都清不掉:
|
|
1464
|
+
// · 门槛判了不接 → 这一轮是被 cancel 停的,turn/end 是 aborted(完成 = false)
|
|
1465
|
+
// · 这一轮一个字都没说出来(没有内容)
|
|
1466
|
+
// 标记于是漏到下一轮,下一轮 completed 时被当成「判定过的不接」整轮跳过。
|
|
1467
|
+
const 这轮静默 = 静默轮.has(键)
|
|
1468
|
+
if (这轮静默) 静默轮.delete(键)
|
|
1434
1469
|
const 有东西 = 上一轮.文本 || (上一轮.图们 && 上一轮.图们.length)
|
|
1435
1470
|
// ⚠ 去重的键必须是「会话 + 轮号」:只按轮号的话,换过目标会话就会静默漏发
|
|
1436
1471
|
const 轮标 = 键 + ':' + String(d.turn)
|
|
@@ -1454,11 +1489,13 @@ export function apply(ctx) {
|
|
|
1454
1489
|
// ⚠ 只有**真的收到过 user/message 事件**才下结论;没记录就按老逻辑走(不能误挡)。
|
|
1455
1490
|
const 有来源 = 轮来源.has(键)
|
|
1456
1491
|
const 来源 = 轮来源.get(键) || ''
|
|
1492
|
+
// ⭐ 1.4.7:这一轮里只要**出现过** qq 来源,后面来的注入就盖不掉它
|
|
1493
|
+
const 这轮来过QQ = 本轮有QQ.get(键) === true
|
|
1457
1494
|
轮来源.delete(键)
|
|
1458
|
-
|
|
1495
|
+
本轮有QQ.delete(键)
|
|
1496
|
+
const 是本地手打 = !这轮来过QQ && 有来源 && (来源 === 'user' || 来源 === '')
|
|
1459
1497
|
// 门槛判了「不接」→ 这一轮一个字都不往外发(但会话里照常留着)
|
|
1460
|
-
if (
|
|
1461
|
-
静默轮.delete(键)
|
|
1498
|
+
if (这轮静默) {
|
|
1462
1499
|
记账('回复门槛:这一轮按判定跳过发送(' + 短id(键) + ')')
|
|
1463
1500
|
} else {
|
|
1464
1501
|
void 落地一发(配置, 上一轮.文本, 上一轮.图们, 方向, 是QQ唤醒, 是本地手打)
|
|
@@ -2375,6 +2412,7 @@ export function apply(ctx) {
|
|
|
2375
2412
|
// 'qq' = 桥转进来的 / 'speak-up' = 主动搭话 / 'user' 或空 = 你自己在 DSH 窗口里打的 / 别的 = 定时任务之类。
|
|
2376
2413
|
// 用途:「只回 QQ 唤醒」开着时,只有**你亲手打的**那几轮才留在本地,自动触发的照样推手机。
|
|
2377
2414
|
const 轮来源 = new Map() // 会话键 -> source.kind
|
|
2415
|
+
const 本轮有QQ = new Map() // 会话键 -> 这一轮里出现过 qq 来源(1.4.7:注入盖不掉它)
|
|
2378
2416
|
// ⭐ 1.4.1:哪个群现在「接着听」(键 = 类型:号码,比如 `群:456`)→ { 剩, 到 }
|
|
2379
2417
|
// ⚠ 按聊天分开记(跟上面那条尾巴一个道理):A 群被点了名,不能让 B 群也开始接听。
|
|
2380
2418
|
// 规则在纯函数 该收这条 里,这里只存状态。
|
|
@@ -3377,7 +3415,7 @@ export function apply(ctx) {
|
|
|
3377
3415
|
const 丢 = 攒
|
|
3378
3416
|
攒 = null
|
|
3379
3417
|
for (const id of 丢.ids) 已转消息.add(id)
|
|
3380
|
-
上次 = Math.max(上次, 丢.末 || 上次)
|
|
3418
|
+
上次 = Math.max(上次, 丢.末时 || 丢.末 || 上次)
|
|
3381
3419
|
记账('群里这 ' + 丢.条目.length + ' 条没掷中(回复概率 ' + Number(配置.回复概率) + '%),这次不接')
|
|
3382
3420
|
return true
|
|
3383
3421
|
}
|
|
@@ -3404,7 +3442,7 @@ export function apply(ctx) {
|
|
|
3404
3442
|
if (一批.ids.length > 1) 记账('连着发的 ' + 一批.ids.length + ' 条攒成一条转进去')
|
|
3405
3443
|
const 结果 = await 转入一条(文字, 一批.图们, 一批.ids[0], 行, 是群行 ? null : 一批.发送者, 一批.点名, 信号, () => {
|
|
3406
3444
|
for (const id of 一批.ids) 已转消息.add(id)
|
|
3407
|
-
上次 = Math.max(上次, 一批.末 || 上次)
|
|
3445
|
+
上次 = Math.max(上次, 一批.末时 || 一批.末 || 上次)
|
|
3408
3446
|
存水位({ 键: 键2, 时间: 上次, ids: 一批.ids })
|
|
3409
3447
|
if (是群行) 记一发(键2)
|
|
3410
3448
|
})
|
|
@@ -3490,11 +3528,31 @@ export function apply(ctx) {
|
|
|
3490
3528
|
if (信号.aborted) return
|
|
3491
3529
|
if (!好) break
|
|
3492
3530
|
}
|
|
3531
|
+
// ⭐ 1.4.7:窗口按**本机第一次看到这条消息的时刻**算。
|
|
3532
|
+
// 以前用 `条.time`(QQ 服务器时间戳):服务器比本机快一点,差就是负的,
|
|
3533
|
+
// 落袋里 `差 >= 0` 不成立 ⇒ 永远不合并。
|
|
3534
|
+
// ⚠ 但不能直接用 `Date.now()`:每一跳都会把这一批重新读一遍,
|
|
3535
|
+
// 那样"最后一条"永远是刚刚,窗口永远等不到头。所以要记「第一次见到它的时间」。
|
|
3536
|
+
const 收到秒 = Math.floor(Date.now() / 1000)
|
|
3537
|
+
// ⚠ 本来就已经是旧消息(服务器时间戳比现在早)→ 用它的时间,这种不该再等窗口;
|
|
3538
|
+
// 未来时间戳(服务器比本机快)取不到就退回本机现在,免得差值为负、永远不合并。
|
|
3539
|
+
const 条时 = Number(条.time) > 0 ? Number(条.time) : 收到秒
|
|
3540
|
+
const 初见估 = Math.min(收到秒, 条时)
|
|
3541
|
+
if (!见时.has(条.message_id)) 见时.set(条.message_id, 初见估)
|
|
3542
|
+
const 初见 = 见时.get(条.message_id) || 初见估
|
|
3543
|
+
if (见时.size > 500) { // 别无限长大:超了就丢最老的一半
|
|
3544
|
+
const 键们 = [...见时.keys()]
|
|
3545
|
+
for (const k of 键们.slice(0, 键们.length - 250)) 见时.delete(k)
|
|
3546
|
+
}
|
|
3493
3547
|
if (!攒) {
|
|
3494
3548
|
攒 = {
|
|
3495
3549
|
条目: [], 图们: [], ids: [], 点名: false, 发送者,
|
|
3496
|
-
起:
|
|
3550
|
+
起: 初见, 末: 初见, 末时: 条时, // 末 = 本机初见(算窗口);末时 = 消息自己的时间(算水位)
|
|
3497
3551
|
}
|
|
3552
|
+
} else {
|
|
3553
|
+
攒.起 = Math.min(攒.起, 初见)
|
|
3554
|
+
攒.末 = Math.max(攒.末, 初见)
|
|
3555
|
+
攒.末时 = Math.max(攒.末时 || 0, 条时)
|
|
3498
3556
|
}
|
|
3499
3557
|
// ⭐ 1.2.0:私聊默认不标来源;开了就给它一个能一眼认出的标记
|
|
3500
3558
|
let 这行文字 = 文字
|
|
@@ -3516,7 +3574,6 @@ export function apply(ctx) {
|
|
|
3516
3574
|
if (图们.length) 攒.图们.push(...图们)
|
|
3517
3575
|
攒.ids.push(条.message_id)
|
|
3518
3576
|
攒.点名 = 攒.点名 || Boolean(点名)
|
|
3519
|
-
攒.末 = 条.time || 攒.末
|
|
3520
3577
|
if (!(Number(配置.合并窗口秒) > 0)) {
|
|
3521
3578
|
const 好 = await 落袋()
|
|
3522
3579
|
if (信号.aborted) return
|
|
@@ -3641,7 +3698,7 @@ export function apply(ctx) {
|
|
|
3641
3698
|
记唤醒: (会话, 类型, 点名) => 唤醒时间.set(会话键(会话), { 时: Date.now(), 类型: String(类型 || '群'), 点名: Boolean(点名) }),
|
|
3642
3699
|
清唤醒: (会话) => 唤醒时间.delete(会话键(会话)),
|
|
3643
3700
|
清尾巴: (会话) => 尾巴.delete(会话键(会话)),
|
|
3644
|
-
清来源: (会话) => 轮来源.delete(会话键(会话)),
|
|
3701
|
+
清来源: (会话) => { 轮来源.delete(会话键(会话)); 本轮有QQ.delete(会话键(会话)) },
|
|
3645
3702
|
有尾巴: (会话) => Boolean((尾巴.get(会话键(会话)) || 0) > Date.now()),
|
|
3646
3703
|
接听: (群号) => 接听中.get('群:' + String(群号)) || null,
|
|
3647
3704
|
记己方: (id) => 记己方消息(id),
|
package/package.json
CHANGED