dsh-fish-buddy 0.8.1 → 0.8.3

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.
Files changed (2) hide show
  1. package/lib/host.js +121 -9
  2. package/package.json +6 -2
package/lib/host.js CHANGED
@@ -16,6 +16,7 @@ import { homedir } from 'node:os'
16
16
  import { join, dirname } from 'node:path'
17
17
  import { fileURLToPath } from 'node:url'
18
18
  import { randomUUID } from 'node:crypto'
19
+ import * as zlib from 'node:zlib'
19
20
 
20
21
  export const name = 'fish-buddy'
21
22
 
@@ -304,20 +305,113 @@ export function apply(ctx, inlineConfig) {
304
305
 
305
306
  // ---------- 会话清单(设置页的下拉用) ----------
306
307
  const 会话清单 = () => {
308
+ // ⚠ 2026-11-03:原来只列「活着的会话」(sessions.list) —— 窗口一关就选不到了,
309
+ // 未分组那批更是永远选不到。改成扫磁盘,跨所有工作区,按同一套规矩过滤。
307
310
  const 出 = []
308
311
  try {
309
- const 会话服务 = 取服务('sessions')
310
- const 标题服务 = 取服务('sessionTitle')
311
- if (会话服务 && typeof 会话服务.list === 'function') {
312
- for (const s of 会话服务.list()) {
313
- let 标题 = ''
312
+ const 根 = join(目录, 'sessions')
313
+ const 缓存 = join(目录, 'storages', 'session_projcache', 'sessions')
314
+ const 归档 = new Set()
315
+ try {
316
+ const wr = 取服务('workspaceRegistry')
317
+ const ids = wr && wr.archivedSessionIds
318
+ if (Array.isArray(ids)) for (const x of ids) 归档.add(String(x))
319
+ } catch (error) {}
320
+ try {
321
+ const w = JSON.parse(readFileSync(join(目录, 'storages', 'workspace.json'), 'utf8'))
322
+ for (const x of (w && w.global && w.global.archivedSessionIds) || []) 归档.add(String(x))
323
+ } catch (error) {}
324
+
325
+ let 一层们 = []
326
+ try {
327
+ 一层们 = readdirSync(根, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name)
328
+ } catch (error) {
329
+ return 出
330
+ }
331
+ const 位置 = []
332
+ for (const 一层 of 一层们) {
333
+ if (existsSync(join(根, 一层, 'session.v4.jsonl.zstd'))) {
334
+ 位置.push({ ws: '', sid: 一层 })
335
+ continue
336
+ }
337
+ let 子们 = []
338
+ try {
339
+ 子们 = readdirSync(join(根, 一层), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name)
340
+ } catch (error) {
341
+ continue
342
+ }
343
+ for (const sid of 子们) 位置.push({ ws: 一层, sid })
344
+ }
345
+
346
+ const 行们 = []
347
+ for (const { ws, sid } of 位置) {
348
+ const 日志 = join(根, ws, sid, 'session.v4.jsonl.zstd')
349
+ if (!existsSync(日志)) continue
350
+ if (归档.has(sid)) continue
351
+ let 大小 = 0
352
+ try {
353
+ 大小 = statSync(日志).size
354
+ } catch (error) {}
355
+ if (大小 < 1024) continue // 空会话
356
+ let 标题 = ''
357
+ let 空 = false
358
+ let 子代理 = false
359
+ let 时 = 0
360
+ try {
361
+ const r = JSON.parse(readFileSync(join(缓存, sid + '.json'), 'utf8'))?.record?.rows || {}
362
+ 标题 = String((r.title && r.title.val) || '')
363
+ const meta = (r.sessionListMetadata && r.sessionListMetadata.val) || {}
364
+ 时 = Number(meta.lastPromptAt || 0)
365
+ 空 = Boolean(meta.blank)
366
+ 子代理 = Boolean(r.subagent && r.subagent.val && r.subagent.val.identity)
367
+ } catch (error) {}
368
+ if (空) continue
369
+ if (!标题 || 子代理) {
370
+ // 没缓存或缓存没说清楚 → 看日志头:子代理/深度>0 直接排除,顺便捞第一句话当标题
314
371
  try {
315
- const 快照 = 标题服务 && typeof 标题服务.get === 'function' ? 标题服务.get(s) : undefined
316
- 标题 = 快照 && 快照.title ? String(快照.title) : ''
372
+ const 缓 = readFileSync(日志).subarray(0, 524288)
373
+ const 帧们 = []
374
+ for (let i = 0; i + 4 <= 缓.length; i++) {
375
+ if (缓[i] === 0x28 && 缓[i + 1] === 0xb5 && 缓[i + 2] === 0x2f && 缓[i + 3] === 0xfd) 帧们.push(i)
376
+ }
377
+ for (let k = 0; k < Math.min(帧们.length, 64); k++) {
378
+ const 段 = 缓.subarray(帧们[k], k + 1 < 帧们.length ? 帧们[k + 1] : 缓.length)
379
+ let 文 = ''
380
+ try {
381
+ 文 = zlib.zstdDecompressSync(段).toString('utf8')
382
+ } catch (error) {
383
+ continue
384
+ }
385
+ for (const 行 of 文.split(String.fromCharCode(10))) {
386
+ if (!行.trim()) continue
387
+ let e
388
+ try {
389
+ e = JSON.parse(行)
390
+ } catch (error) {
391
+ continue
392
+ }
393
+ if (e.type === 'session') {
394
+ if (e.origin === 'subagent' || Number(e.delegationDepth || 0) > 0) 子代理 = true
395
+ if (!时) 时 = Number(e.createdAt || 0)
396
+ continue
397
+ }
398
+ if (标题) continue
399
+ if (e.type !== 'user/message') continue
400
+ const 源 = (e.data && e.data.source && e.data.source.kind) || ''
401
+ if (源 !== 'user') continue
402
+ const c = e.data && e.data.content
403
+ const t = (Array.isArray(c) ? c : []).filter((x) => x && x.type === 'text').map((x) => x.text).join(' ').trim()
404
+ if (t) 标题 = t.slice(0, 40)
405
+ }
406
+ if (子代理 && 标题) break
407
+ }
317
408
  } catch (error) {}
318
- 出.push({ id: String(s.id), 标题 })
319
409
  }
410
+ if (子代理) continue
411
+ 行们.push({ id: sid, 标题: 标题 || ('未命名会话 · ' + (时 ? new Date(时).toLocaleDateString('zh-CN') : sid.slice(0, 8))), at: 时 })
320
412
  }
413
+ 行们.sort((a, b) => b.at - a.at)
414
+ for (const r of 行们) 出.push({ id: r.id, 标题: r.标题 })
321
415
  } catch (error) {
322
416
  记账('列会话出错:' + 说(error))
323
417
  }
@@ -737,11 +831,17 @@ export function apply(ctx, inlineConfig) {
737
831
  * ⚠ 只在 **截图送法 = 攒着** 时生效:每张都说那一档,截图本来就会叫醒她,
738
832
  * 再叠加搭话等于一轮变两轮、白花钱。
739
833
  */
834
+ // ⭐ 2026-11-04:最近一次往会话里塞图的时间 —— 给「游戏里自己搭话」让路(声明放前面,免得 TDZ)
835
+ let 上次塞图 = 0
740
836
  const 游戏搭话 = () => {
741
837
  try {
742
838
  if (!游戏模式.开) return false
743
839
  const 配置 = 读配置()
744
- if (String(配置.截图送法 || '每张') !== '攒着') return false // ← 只跟「攒着」配合
840
+ // ⚠⚠ 2026-11-04 修:这里原来判的是 配置.截图送法 —— 可全项目根本没有这个字段
841
+ // (「每张 / 攒着」是**挂件的设置**,插件这边只有每帧带的 存档/立即 标志)。
842
+ // 于是这一行永远为真 → 游戏搭话 永远 return false,攒着的图只能等她先开口。
843
+ // 现在改成「最近刚塞过图就让路」:每张都说那一档每隔几十秒就有图出去,
844
+ // 自然被挡掉;攒着那一档不塞图,所以能正常搭话。
745
845
  // ⭐ 1.7.1:优先用**挂件报上来的**那个数,拿不到才用自己配置里的默认值
746
846
  const 间隔 = 节奏('游戏搭话秒', Math.max(0, Number(配置.游戏搭话秒) || 0))
747
847
  if (间隔 <= 0) return false
@@ -749,6 +849,8 @@ export function apply(ctx, inlineConfig) {
749
849
  if (!键) return false
750
850
  // 刚进游戏模式先安静一会儿,别一进去就开口
751
851
  if (现在() - (游戏模式.时 || 0) < Math.min(间隔, 60) * 1000) return false
852
+ // 「每张都说」那档每隔几十秒刚发过图 → 让路,别一轮变两轮
853
+ if (上次塞图 && 现在() - 上次塞图 < 90000) return false
752
854
  if (!要搭话({
753
855
  间隔秒: 间隔,
754
856
  最多秒: 0,
@@ -826,7 +928,13 @@ export function apply(ctx, inlineConfig) {
826
928
  const 可能搭话 = () => {
827
929
  try {
828
930
  // ⭐ 0.7.7:在打游戏就别走「一起看片子」那条搭话 —— 两套提示词会打架
931
+ // ⚠⚠ 2026-11-04 修(用户反馈:游戏模式「攒着」永远不主动搭话):
932
+ // 这里以前是 **直接 return**,可游戏模式的主动搭话(游戏搭话)只有离看那条路
933
+ // 才会调 —— 于是「在打游戏」一旦命中,游戏搭话() 就永远走不到,
934
+ // 攒着的截图只能等她先开口才被带过去,跟"主动搭话"根本不是一回事。
935
+ // 现在改成:在打游戏 → 走游戏搭话;不在打游戏也没陪看 → 还是走游戏搭话(它自己会判条件)。
829
936
  if (游戏模式.开 && 现在() - (游戏模式.时 || 0) < 6 * 3600 * 1000) {
937
+ try { 游戏搭话() } catch (error) {}
830
938
  return
831
939
  }
832
940
  if (!陪看开着) {
@@ -942,6 +1050,7 @@ export function apply(ctx, inlineConfig) {
942
1050
  // ⭐ 0.6.1:挂件会告诉我们「现在在不在游戏模式」—— 游戏模式也要能主动搭话
943
1051
  const 游戏模式 = { 开: false, 时: 0 }
944
1052
  const 攒图 = new Map()
1053
+ // ⭐ 2026-11-04:最近一次往会话里塞图的时间 —— 用来给「游戏里自己搭话」让路
945
1054
  // ⭐ 0.7.2:**不限制张数**(以前的「最多 3 张、新的挤掉最旧的」是错的 ——
946
1055
  // 挤掉的图等于白截了)。攒着就一直留着,等她说话 / 她自己看一眼时**全部**带过去。
947
1056
  // ⚠ 只有个上限 60 张防跑飞(一小时 30 秒一张也就 120 张,实际轮不到)。
@@ -1520,6 +1629,7 @@ export function apply(ctx, inlineConfig) {
1520
1629
  source: { kind: 源 === '热键' ? 'user' : 'fishbuddy' },
1521
1630
  })
1522
1631
  状态.送出条数 += 1
1632
+ 上次塞图 = 现在() // ⭐ 2026-11-04:给「游戏里自己搭话」让路用
1523
1633
  return '发过去了'
1524
1634
  } catch (error) {
1525
1635
  return '发失败了:' + 说(error)
@@ -1585,6 +1695,8 @@ export function apply(ctx, inlineConfig) {
1585
1695
  source: { kind: 来源类别 },
1586
1696
  })
1587
1697
  状态.送出条数 += 1
1698
+ 上次塞图 = 现在() // 带图说话也算刚塞过
1699
+
1588
1700
  return '发过去了'
1589
1701
  } catch (error) {
1590
1702
  return '发失败了:' + 说(error)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-fish-buddy",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
4
4
  "description": "鱼搭子 · DSH 这半边:桌面小挂件说的话送进你绑定的会话,她的回话再递回挂件。挂件是独立程序,这里只负责接线。",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
@@ -40,5 +40,9 @@
40
40
  "react"
41
41
  ]
42
42
  }
43
+ },
44
+ "engines": {
45
+ "node": ">=22",
46
+ "dsh": ">=0.2.1-rc.2"
43
47
  }
44
- }
48
+ }