dsh-qq-link 1.4.6 → 1.4.7

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 CHANGED
@@ -1,3 +1,24 @@
1
+ ## 1.4.7 — 2026-11-01
2
+
3
+ **「只回 QQ 唤醒」开着时,QQ 叫醒的那一轮也发不出去(1.4.4 带进来的)**
4
+
5
+ - 🐞 现象:开着「只回 QQ 唤醒」,从 QQ 发一句,DSH 里正常回,**QQ 那边什么都没有**;
6
+ 把这个开关关掉就正常。手动点「试发」也正常 —— 说明发送通道没问题,是**判定把这一轮拦了**。
7
+ - 根因:1.4.4 为了修「自动触发的轮次也被挡」,把判据从「最近有没有 QQ 消息」改成「这一轮是谁起的头」,
8
+ 用一张 `轮来源` 表记 `source.kind`。但那行是 `轮来源.set(键, 类 || 'user')` —— **每来一条 user/message 就覆盖**。
9
+ 而同一轮里 DSH 还会再插一条**没有 `source.kind`** 的注入(每轮的 runtime context 就是独立的 user/message),
10
+ 它把 `qq` 覆盖成 `user`,于是这一轮被判成「你在窗口里自己打的」→ 整轮丢掉。
11
+ - ✅ 现在:**带 kind 才覆盖**;没有 kind 的只在「这一轮什么来源都还没记过」时才当成本地手打。
12
+ 另外单独记「这一轮里出现过 qq」,判定时只要出现过就算 QQ 唤醒,后来的注入盖不掉它。
13
+
14
+ **顺带:连着发的几条攒成一条**
15
+
16
+ - ✅ 默认值 **1 秒 → 3 秒**(界面文案一直写的是「默认 3」,代码里却是 1 —— 新装的人拿到 1 秒,
17
+ 人手打两条字常隔 2~4 秒,于是看起来就是"连着发不合并")。
18
+ - 🐞 窗口以前按 **QQ 消息自带的时间戳**算(`现在 - 条.time`):服务器时间只要比本机快一点,差值就是负的,
19
+ 代码直接不等 ⇒ **永远不合并**,窗口设多大都没用。现在按**本机第一次看到这条消息的时刻**算。
20
+ - ⚠ 水位(下次从哪条开始读)仍然用消息自己的时间,没有被上面的改动带偏。
21
+
1
22
  ## 1.4.6 — 2026-11-01
2
23
 
3
24
  **「检查更新」以前会报错版本,而且只给一条命令让你自己手动复制、然后手动装**
package/lib/host.js CHANGED
@@ -116,7 +116,7 @@ const 默认配置 = {
116
116
  // · >0 = 同一个人在这段时间内连着发的,合成一条(私聊、群都管用)
117
117
  // 实现上是「等一小会儿再收」:最后一条没过去这么久,就先不消费,下一跳连新的那条一起收。
118
118
  // ⚠ 别开大:一轮拉历史里本来就能看到「已经发完的那一串」,这里只兜「正好撞上轮询」的那一下。
119
- 合并窗口秒: 1,
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
  // 「看到哪儿了」就归零 ⇒ 最近那十几条历史消息会**被当成新消息重投一遍**,
@@ -1424,7 +1427,16 @@ export function apply(ctx) {
1424
1427
  const 类 = String((源 && 源.kind) || '')
1425
1428
  if (!类 || 类 === 'user') 尾巴.delete(键)
1426
1429
  // ⭐ 1.4.4:把「这一轮是谁起的头」记下来 —— turn/end 判定要不要推到手机时用
1427
- 轮来源.set(键, 类 || 'user')
1430
+ // ⚠⚠ 1.4.7:以前是 `轮来源.set(键, 类 || 'user')` —— **无条件覆盖**。而同一轮里 DSH 还会再插
1431
+ // 一条没有 source.kind 的注入(每轮的 runtime context 就是独立的一条 user/message),
1432
+ // 于是「QQ 叫醒的那一轮」被改判成「你在窗口里自己打的」,「只回 QQ 唤醒」开着时整轮被丢掉。
1433
+ // 现在:**带 kind 才覆盖**;没有 kind 的只在「这一轮什么来源都还没记过」时才当成本地手打。
1434
+ if (类) {
1435
+ 轮来源.set(键, 类)
1436
+ if (类 === 'qq') 本轮有QQ.set(键, true)
1437
+ } else if (!轮来源.has(键)) {
1438
+ 轮来源.set(键, 'user')
1439
+ }
1428
1440
  return
1429
1441
  }
1430
1442
 
@@ -1454,8 +1466,11 @@ export function apply(ctx) {
1454
1466
  // ⚠ 只有**真的收到过 user/message 事件**才下结论;没记录就按老逻辑走(不能误挡)。
1455
1467
  const 有来源 = 轮来源.has(键)
1456
1468
  const 来源 = 轮来源.get(键) || ''
1469
+ // ⭐ 1.4.7:这一轮里只要**出现过** qq 来源,后面来的注入就盖不掉它
1470
+ const 这轮来过QQ = 本轮有QQ.get(键) === true
1457
1471
  轮来源.delete(键)
1458
- const 是本地手打 = 有来源 && (来源 === 'user' || 来源 === '')
1472
+ 本轮有QQ.delete(键)
1473
+ const 是本地手打 = !这轮来过QQ && 有来源 && (来源 === 'user' || 来源 === '')
1459
1474
  // 门槛判了「不接」→ 这一轮一个字都不往外发(但会话里照常留着)
1460
1475
  if (静默轮.has(键)) {
1461
1476
  静默轮.delete(键)
@@ -2375,6 +2390,7 @@ export function apply(ctx) {
2375
2390
  // 'qq' = 桥转进来的 / 'speak-up' = 主动搭话 / 'user' 或空 = 你自己在 DSH 窗口里打的 / 别的 = 定时任务之类。
2376
2391
  // 用途:「只回 QQ 唤醒」开着时,只有**你亲手打的**那几轮才留在本地,自动触发的照样推手机。
2377
2392
  const 轮来源 = new Map() // 会话键 -> source.kind
2393
+ const 本轮有QQ = new Map() // 会话键 -> 这一轮里出现过 qq 来源(1.4.7:注入盖不掉它)
2378
2394
  // ⭐ 1.4.1:哪个群现在「接着听」(键 = 类型:号码,比如 `群:456`)→ { 剩, 到 }
2379
2395
  // ⚠ 按聊天分开记(跟上面那条尾巴一个道理):A 群被点了名,不能让 B 群也开始接听。
2380
2396
  // 规则在纯函数 该收这条 里,这里只存状态。
@@ -3377,7 +3393,7 @@ export function apply(ctx) {
3377
3393
  const 丢 = 攒
3378
3394
  攒 = null
3379
3395
  for (const id of 丢.ids) 已转消息.add(id)
3380
- 上次 = Math.max(上次, 丢.末 || 上次)
3396
+ 上次 = Math.max(上次, 丢.末时 || 丢.末 || 上次)
3381
3397
  记账('群里这 ' + 丢.条目.length + ' 条没掷中(回复概率 ' + Number(配置.回复概率) + '%),这次不接')
3382
3398
  return true
3383
3399
  }
@@ -3404,7 +3420,7 @@ export function apply(ctx) {
3404
3420
  if (一批.ids.length > 1) 记账('连着发的 ' + 一批.ids.length + ' 条攒成一条转进去')
3405
3421
  const 结果 = await 转入一条(文字, 一批.图们, 一批.ids[0], 行, 是群行 ? null : 一批.发送者, 一批.点名, 信号, () => {
3406
3422
  for (const id of 一批.ids) 已转消息.add(id)
3407
- 上次 = Math.max(上次, 一批.末 || 上次)
3423
+ 上次 = Math.max(上次, 一批.末时 || 一批.末 || 上次)
3408
3424
  存水位({ 键: 键2, 时间: 上次, ids: 一批.ids })
3409
3425
  if (是群行) 记一发(键2)
3410
3426
  })
@@ -3490,11 +3506,31 @@ export function apply(ctx) {
3490
3506
  if (信号.aborted) return
3491
3507
  if (!好) break
3492
3508
  }
3509
+ // ⭐ 1.4.7:窗口按**本机第一次看到这条消息的时刻**算。
3510
+ // 以前用 `条.time`(QQ 服务器时间戳):服务器比本机快一点,差就是负的,
3511
+ // 落袋里 `差 >= 0` 不成立 ⇒ 永远不合并。
3512
+ // ⚠ 但不能直接用 `Date.now()`:每一跳都会把这一批重新读一遍,
3513
+ // 那样"最后一条"永远是刚刚,窗口永远等不到头。所以要记「第一次见到它的时间」。
3514
+ const 收到秒 = Math.floor(Date.now() / 1000)
3515
+ // ⚠ 本来就已经是旧消息(服务器时间戳比现在早)→ 用它的时间,这种不该再等窗口;
3516
+ // 未来时间戳(服务器比本机快)取不到就退回本机现在,免得差值为负、永远不合并。
3517
+ const 条时 = Number(条.time) > 0 ? Number(条.time) : 收到秒
3518
+ const 初见估 = Math.min(收到秒, 条时)
3519
+ if (!见时.has(条.message_id)) 见时.set(条.message_id, 初见估)
3520
+ const 初见 = 见时.get(条.message_id) || 初见估
3521
+ if (见时.size > 500) { // 别无限长大:超了就丢最老的一半
3522
+ const 键们 = [...见时.keys()]
3523
+ for (const k of 键们.slice(0, 键们.length - 250)) 见时.delete(k)
3524
+ }
3493
3525
  if (!攒) {
3494
3526
  攒 = {
3495
3527
  条目: [], 图们: [], ids: [], 点名: false, 发送者,
3496
- 起: 条.time || 0, 末: 条.time || 0,
3528
+ 起: 初见, 末: 初见, 末时: 条时, // 末 = 本机初见(算窗口);末时 = 消息自己的时间(算水位)
3497
3529
  }
3530
+ } else {
3531
+ 攒.起 = Math.min(攒.起, 初见)
3532
+ 攒.末 = Math.max(攒.末, 初见)
3533
+ 攒.末时 = Math.max(攒.末时 || 0, 条时)
3498
3534
  }
3499
3535
  // ⭐ 1.2.0:私聊默认不标来源;开了就给它一个能一眼认出的标记
3500
3536
  let 这行文字 = 文字
@@ -3516,7 +3552,6 @@ export function apply(ctx) {
3516
3552
  if (图们.length) 攒.图们.push(...图们)
3517
3553
  攒.ids.push(条.message_id)
3518
3554
  攒.点名 = 攒.点名 || Boolean(点名)
3519
- 攒.末 = 条.time || 攒.末
3520
3555
  if (!(Number(配置.合并窗口秒) > 0)) {
3521
3556
  const 好 = await 落袋()
3522
3557
  if (信号.aborted) return
@@ -3641,7 +3676,7 @@ export function apply(ctx) {
3641
3676
  记唤醒: (会话, 类型, 点名) => 唤醒时间.set(会话键(会话), { 时: Date.now(), 类型: String(类型 || '群'), 点名: Boolean(点名) }),
3642
3677
  清唤醒: (会话) => 唤醒时间.delete(会话键(会话)),
3643
3678
  清尾巴: (会话) => 尾巴.delete(会话键(会话)),
3644
- 清来源: (会话) => 轮来源.delete(会话键(会话)),
3679
+ 清来源: (会话) => { 轮来源.delete(会话键(会话)); 本轮有QQ.delete(会话键(会话)) },
3645
3680
  有尾巴: (会话) => Boolean((尾巴.get(会话键(会话)) || 0) > Date.now()),
3646
3681
  接听: (群号) => 接听中.get('群:' + String(群号)) || null,
3647
3682
  记己方: (id) => 记己方消息(id),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-qq-link",
3
- "version": "1.4.6",
3
+ "version": "1.4.7",
4
4
  "description": "通到 QQ:DSH 里的 AI 伙伴和 QQ 双向互通 —— 它说完话自动发到 QQ,你在 QQ 里说的话自动转进 DSH。走 OneBot 11 HTTP(SnowLuma / NapCat 都行)。",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "engines": {