dsh-fish-memory 1.4.3 → 1.4.4
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.md +7 -7
- package/lib/index.js +141 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,15 +49,16 @@
|
|
|
49
49
|
在 DSH 里:**设置 → 插件 → 安装**,填下面这一行;或者命令行:
|
|
50
50
|
|
|
51
51
|
```
|
|
52
|
-
|
|
52
|
+
dsh-fish-memory
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
```sh
|
|
56
|
-
dsh plugin --profile web add
|
|
56
|
+
dsh plugin --profile web add dsh-fish-memory
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
>
|
|
60
|
-
|
|
59
|
+
> 也可以在 npm 上直接看到这个包(含历史版本):<https://www.npmjs.com/package/dsh-fish-memory>
|
|
60
|
+
|
|
61
|
+
> 上面那条要是碰到网络问题(npm 抽风、或者想固定某一次的代码快照),可以改用 tarball 直链,不用 git:
|
|
61
62
|
|
|
62
63
|
```bash
|
|
63
64
|
dsh plugin --profile web add https://codeload.github.com/Yuuyuko-uu/dsh-fish-memory/tar.gz/refs/heads/main
|
|
@@ -70,8 +71,7 @@ dsh plugin --profile web add https://codeload.github.com/Yuuyuko-uu/dsh-fish-mem
|
|
|
70
71
|
|
|
71
72
|
重启后,打开 **设置 → 鱼的记忆**。
|
|
72
73
|
|
|
73
|
-
>
|
|
74
|
-
> (以后要是上了 npm,`dsh plugin add dsh-fish-memory` 也能用。)
|
|
74
|
+
> 也可以直接装 npm 包:`dsh plugin add dsh-fish-memory`。
|
|
75
75
|
> ⭐ 最省事的一条:把仓库链接贴给你自己的 DSH,说「**帮我装这个插件**」。
|
|
76
76
|
|
|
77
77
|
### 装完起不来?先看这个
|
|
@@ -122,7 +122,7 @@ $p = "把上面 BOM=True 那一行的路径粘到这里"
|
|
|
122
122
|
|
|
123
123
|
```bash
|
|
124
124
|
dsh plugin --profile desktop remove dsh-fish-memory
|
|
125
|
-
dsh plugin --profile desktop add
|
|
125
|
+
dsh plugin --profile desktop add dsh-fish-memory
|
|
126
126
|
```
|
|
127
127
|
|
|
128
128
|
> ⚠️ **必须先卸再装**:直接装可能拿到缓存里的旧副本,装完还是老的。
|
package/lib/index.js
CHANGED
|
@@ -279,12 +279,21 @@ function loadStore(sid) {
|
|
|
279
279
|
return data
|
|
280
280
|
}
|
|
281
281
|
corruptIncidents.delete(path)
|
|
282
|
+
// ⭐ 1.4.4:老版本把「运行环境 / 记忆提醒列表项 / 技能目录」这些机器文本当成线索存了进来,
|
|
283
|
+
// 白占召回记录的 100 条上限。读的时候清一遍:净化之后什么都不剩的,那条记录本来就没有用。
|
|
284
|
+
// (只动 traces 这种诊断记录,记忆本身一条都不碰。)
|
|
285
|
+
const 旧记录 = Array.isArray(raw.traces) ? raw.traces : []
|
|
286
|
+
const traces = 旧记录.filter((t) => {
|
|
287
|
+
const q = typeof (t && t.query) === 'string' ? t.query : ''
|
|
288
|
+
if (!q) return true // 没有 query 的老记录不动
|
|
289
|
+
return !明显是机器写的(清掉注入上下文(q))
|
|
290
|
+
})
|
|
282
291
|
return {
|
|
283
292
|
version: 2,
|
|
284
293
|
purgeEpoch: purgeEpochOf(raw),
|
|
285
294
|
policy: 合并参数(raw.policy),
|
|
286
295
|
memories: Array.isArray(raw.memories) ? raw.memories : [],
|
|
287
|
-
traces
|
|
296
|
+
traces,
|
|
288
297
|
deleted: Array.isArray(raw.deleted) ? raw.deleted : [],
|
|
289
298
|
}
|
|
290
299
|
} catch (error) {
|
|
@@ -442,6 +451,15 @@ function saveStore(sid, data, 选项) {
|
|
|
442
451
|
if (!(选项 && 选项.raw)) {
|
|
443
452
|
if (盘 && Array.isArray(盘.memories)) data = 合并库(盘, data, 选项)
|
|
444
453
|
}
|
|
454
|
+
// ⭐ 1.4.4:合并会把盘上的旧 trace 捡回来,所以**这里也得清一遍**
|
|
455
|
+
// (只在加载时清没用:下一次保存又原样回来了)。
|
|
456
|
+
if (Array.isArray(data.traces) && data.traces.length) {
|
|
457
|
+
data.traces = data.traces.filter((t) => {
|
|
458
|
+
const q = typeof (t && t.query) === 'string' ? t.query : ''
|
|
459
|
+
if (!q) return true
|
|
460
|
+
return !明显是机器写的(清掉注入上下文(q))
|
|
461
|
+
})
|
|
462
|
+
}
|
|
445
463
|
// 与内容一起原子发布;其他实例后续读取时也能识别旧代际(并非跨进程事务)。
|
|
446
464
|
// 先构造待写副本;写失败时保留恢复候选的原代际,允许同对象安全重试。
|
|
447
465
|
data = { ...data, purgeEpoch: 选项 && 选项.raw ? randomUUID() : epoch }
|
|
@@ -1826,13 +1844,18 @@ export function 清掉注入上下文(文) {
|
|
|
1826
1844
|
// ③ 记忆提醒里的固定句子(下面是你们过去真实发生过的事 / (如果此刻你想起什么 …))。
|
|
1827
1845
|
let 跳到空行 = false
|
|
1828
1846
|
let 跳元信息 = false
|
|
1829
|
-
|
|
1847
|
+
// ⭐ 1.4.4:方块(【…】)跳过之后,**紧跟的列表项也算那块的一部分** ——
|
|
1848
|
+
// render() 写出来的是 `- 3 天前(10-27):…`、`- 一直:…`,以前那张白名单只认
|
|
1849
|
+
// 昨天/前天/今天/日期,这些全漏了出来,被当成「她这一轮说的话」去检索。
|
|
1850
|
+
let 方块之后 = false
|
|
1851
|
+
const 元信息行 = /^(Current runtime context|Current DSH|Current working directory|Current DeepSeek Harness|Browser time zone|Time sampled|Elapsed since|Approval (policy|prompts))/i
|
|
1830
1852
|
const 固定句 = /^(下面是你们过去真实发生过的事|(?如果此刻你想起什么|有时间的事会写在前面)/
|
|
1831
1853
|
for (const 行 of 行们) {
|
|
1832
1854
|
const 净 = 行.trim()
|
|
1833
1855
|
if (跳到空行) {
|
|
1834
1856
|
if (!净) {
|
|
1835
1857
|
跳到空行 = false
|
|
1858
|
+
方块之后 = true
|
|
1836
1859
|
留.push('')
|
|
1837
1860
|
}
|
|
1838
1861
|
continue
|
|
@@ -1850,6 +1873,11 @@ export function 清掉注入上下文(文) {
|
|
|
1850
1873
|
跳到空行 = true
|
|
1851
1874
|
continue
|
|
1852
1875
|
}
|
|
1876
|
+
// 刚跳过一块之后,跟着的列表项(不管前面写的是「3 天前」还是「一直」)都是那块的东西
|
|
1877
|
+
if (方块之后) {
|
|
1878
|
+
if (/^[-*]\s/.test(净)) continue
|
|
1879
|
+
方块之后 = false
|
|
1880
|
+
}
|
|
1853
1881
|
// 记忆提醒里的列表项(`- 前天 12:56:…`)也一起跳 —— 那是「提醒」,不是她这一轮说的话
|
|
1854
1882
|
if (/^-\s*(前天|昨天|今天|\d{1,2}-\d{1,2}|\d{4}-\d{2}-\d{2})/.test(净) && 净.includes(':')) {
|
|
1855
1883
|
continue
|
|
@@ -1867,6 +1895,29 @@ export function 清掉注入上下文(文) {
|
|
|
1867
1895
|
return 留.join(String.fromCharCode(10)).trim()
|
|
1868
1896
|
}
|
|
1869
1897
|
|
|
1898
|
+
/**
|
|
1899
|
+
* ⭐ 1.4.4:这一行**明显是机器写的**,不是人说的话。
|
|
1900
|
+
*
|
|
1901
|
+
* 只给「清老记录」用(召回记录里那些历史烂条目):trace 里只剩下净化后的残渣,
|
|
1902
|
+
* 没有上下文了,认不出「它原本跟着哪一块」,所以按长相判。
|
|
1903
|
+
* ⚠ 别拿它去决定「这一轮用哪句检索」—— 那个走 source.kind(见 不是人说的话)。
|
|
1904
|
+
*/
|
|
1905
|
+
export function 明显是机器写的(文) {
|
|
1906
|
+
const q = String(文 == null ? '' : 文).trim()
|
|
1907
|
+
if (!q) return true
|
|
1908
|
+
if (/^(Current runtime context|Current DSH|Current working directory|Current DeepSeek Harness|Browser time zone|Time sampled|Elapsed since|Approval (policy|prompts))/i.test(q)) return true
|
|
1909
|
+
if (/^<system-reminder>/i.test(q)) return true
|
|
1910
|
+
if (/^\[model changed/i.test(q)) return true
|
|
1911
|
+
if (/^This is an automatically generated checkpoint/i.test(q)) return true
|
|
1912
|
+
// 记忆提醒里的列表项:`- 昨天 16:10:…` / `- 3 天前(10-27):…` / `- 一直:…`
|
|
1913
|
+
// ⚠ 「N 天前」这种写法以前漏了,正是面板上一堆 `- 3 天前(10-27)` 的来源。
|
|
1914
|
+
if (/^[-*]\s*(一直|今天|昨天|前天|\d+\s*(天|个月|年)前|\d{1,2}-\d{1,2}|\d{4}-\d{2}-\d{2})/.test(q)) return true
|
|
1915
|
+
// 别的插件塞进来的提示(心情 / 陪看字幕 / 骰子喊她聊天 / 自动刷B站)——
|
|
1916
|
+
// 这些是「插件对模型说的话」,不是她说的,老记录里混进来的一样清掉。
|
|
1917
|
+
if (/^((此刻你|下面这句是|上面这些是|你俩正一起看着|这是我游戏里|到谷时)/.test(q)) return true
|
|
1918
|
+
return false
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1870
1921
|
/**
|
|
1871
1922
|
* ⭐ 1.4.2:线索太短就**借上文**。
|
|
1872
1923
|
*
|
|
@@ -1912,6 +1963,7 @@ function 最近几句(session, n = 3) {
|
|
|
1912
1963
|
const e = ev[i]
|
|
1913
1964
|
const type = e && (e.type || e.kind)
|
|
1914
1965
|
if (type !== 'user/message') continue
|
|
1966
|
+
if (不是人说的话(e)) continue // ⭐ 1.4.4:宿主塞的上下文整条跳过
|
|
1915
1967
|
const p = e.payload !== undefined ? e.payload : e.data
|
|
1916
1968
|
const txt = textOfMessage(p)
|
|
1917
1969
|
if (!txt) continue
|
|
@@ -1927,22 +1979,77 @@ function 最近几句(session, n = 3) {
|
|
|
1927
1979
|
return 出
|
|
1928
1980
|
}
|
|
1929
1981
|
|
|
1982
|
+
// ⭐ 1.4.4:哪些来源**根本不是人说的话** —— 直接跳过,别指望文本白名单兜得住。
|
|
1983
|
+
//
|
|
1984
|
+
// 坑的来历:以前只靠「净化后是不是空的」来判断,而净化是一张文本白名单
|
|
1985
|
+
// (`Current …` / `Approval prompts` / `- 昨天 …`)。可宿主每加一句新措辞就漏一条:
|
|
1986
|
+
// · 审批策略从 "never" 换成 "ask" 之后,那一行变成 `Approval policy: ask. …`,白名单没跟上;
|
|
1987
|
+
// · 插件自己 render 出来的列表项是 `- 3 天前(10-27):…` / `- 一直:…`,也不在白名单里。
|
|
1988
|
+
// 漏出来的那几行就成了「这一轮她说的那句话」,被拿去检索 —— 面板上一堆重复的英文/列表项。
|
|
1989
|
+
//
|
|
1990
|
+
// 源头上 DSH 每条 user/message 都带 `source.kind`,比猜文本可靠得多。
|
|
1991
|
+
// 这里列的是**宿主/插件塞进来的、不是人说的话**那些来源:
|
|
1992
|
+
// runtime-context / time-context / skill-catalog / model-selection / compact-checkpoint …
|
|
1993
|
+
// 是 DSH 自己往会话里补的上下文;speak-up / her-words(插件定时替人开口那种「该去找他聊天了」)、
|
|
1994
|
+
// fishbuddy-watch(片子字幕)、bili-duty-scan(自动刷B站)是插件替她开口或替她看的。
|
|
1995
|
+
// ⚠ **用黑名单不用白名单**:别的 IM 桥(im-connect 之类)用的是什么 kind 我们不知道,
|
|
1996
|
+
// 认成「不是人话」就等于让那些用户永远检索不到记忆 —— 比原来的毛病更糟。
|
|
1997
|
+
// 认不出来的来源**一律当人话**,靠上面的文本净化兜底。
|
|
1998
|
+
const 机器来源 = new Set([
|
|
1999
|
+
'runtime-context', 'time-context', 'skill-catalog', 'model-selection',
|
|
2000
|
+
'compact-checkpoint', 'repeat-tool-reminder', 'user-approval',
|
|
2001
|
+
'system-prompt', 'working-directory', 'ptc-mode', 'tool-registry',
|
|
2002
|
+
'agent-instructions', 'subagent-settled', 'team-message', 'agent-message',
|
|
2003
|
+
'speak-up', 'her-words', 'bili-duty-scan', 'fishbuddy-watch',
|
|
2004
|
+
])
|
|
2005
|
+
|
|
2006
|
+
function 来源种类(e) {
|
|
2007
|
+
try {
|
|
2008
|
+
const 源 = e && e.data && e.data.source
|
|
2009
|
+
return (源 && typeof 源.kind === 'string' && 源.kind) || ''
|
|
2010
|
+
} catch (error) {
|
|
2011
|
+
return ''
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
// 认不出来源的老会话 / 第三方渠道:**不拦**(交给文本净化兜底),免得把真话也丢了
|
|
2016
|
+
function 不是人说的话(e) {
|
|
2017
|
+
const 种 = 来源种类(e)
|
|
2018
|
+
if (!种) return false
|
|
2019
|
+
if (机器来源.has(种)) return true
|
|
2020
|
+
// `plugin:xxx` 那种是插件自报家门塞进来的(如 plugin:chat-import / plugin:meow-memory)
|
|
2021
|
+
return 种.startsWith('plugin:')
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
/**
|
|
2025
|
+
* ⭐ 1.4.4:从事件流里挑「这一轮她真正说的那句话」——抽成纯函数,好单测。
|
|
2026
|
+
* 返回原(未净化)文本;调用方再净化一次(跟以前一样,净化结果为空就说明这条不算)。
|
|
2027
|
+
* @param {Array} 事件们 session.snapshotEvents() 的结果
|
|
2028
|
+
* @returns {string} 原文本;找不到返回 ''
|
|
2029
|
+
*/
|
|
2030
|
+
export function 挑这一轮的真话(事件们) {
|
|
2031
|
+
const ev = Array.isArray(事件们) ? 事件们 : []
|
|
2032
|
+
for (let i = ev.length - 1; i >= 0; i--) {
|
|
2033
|
+
const e = ev[i]
|
|
2034
|
+
const type = e && (e.type || e.kind)
|
|
2035
|
+
if (type !== 'user/message') continue
|
|
2036
|
+
// ⭐ 1.4.4:宿主塞的上下文(运行环境 / 时间 / 技能目录 / 记忆提醒 / 压缩摘要 …)整条跳过
|
|
2037
|
+
if (不是人说的话(e)) continue
|
|
2038
|
+
const p = e.payload !== undefined ? e.payload : e.data
|
|
2039
|
+
const txt = textOfMessage(p)
|
|
2040
|
+
if (!txt) continue
|
|
2041
|
+
// ⭐ 1.4.1:宿主会把「运行环境」「【你记得的事】」这类东西跟真话夹在同一条里 ——
|
|
2042
|
+
// 净化之后是空的就**往前再找一条**,别拿它当线索。
|
|
2043
|
+
const 净 = 清掉注入上下文(txt)
|
|
2044
|
+
if (净 && 净.trim()) return txt
|
|
2045
|
+
}
|
|
2046
|
+
return ''
|
|
2047
|
+
}
|
|
2048
|
+
|
|
1930
2049
|
function lastUserTextOf(session) {
|
|
1931
2050
|
try {
|
|
1932
2051
|
if (!session || typeof session.snapshotEvents !== 'function') return ''
|
|
1933
|
-
|
|
1934
|
-
for (let i = ev.length - 1; i >= 0; i--) {
|
|
1935
|
-
const e = ev[i]
|
|
1936
|
-
const type = e && (e.type || e.kind)
|
|
1937
|
-
if (type !== 'user/message') continue
|
|
1938
|
-
const p = e.payload !== undefined ? e.payload : e.data
|
|
1939
|
-
const txt = textOfMessage(p)
|
|
1940
|
-
if (!txt) continue
|
|
1941
|
-
// ⭐ 1.4.1:宿主会把「运行环境」「【你记得的事】」这类东西当成 user 消息塞进来 ——
|
|
1942
|
-
// 有的轮次里整条就只剩这些。净化之后是空的就**往前再找一条**,别拿它当线索。
|
|
1943
|
-
const 净 = 清掉注入上下文(txt)
|
|
1944
|
-
if (净 && 净.trim()) return txt
|
|
1945
|
-
}
|
|
2052
|
+
return 挑这一轮的真话(session.snapshotEvents() || [])
|
|
1946
2053
|
} catch (error) {}
|
|
1947
2054
|
return ''
|
|
1948
2055
|
}
|
|
@@ -2533,6 +2640,9 @@ function 补历史尺(sid, session) {
|
|
|
2533
2640
|
for (let i = ev.length - 1; i >= 0 && 收 < 2000; i--) {
|
|
2534
2641
|
const e = ev[i]
|
|
2535
2642
|
if (!e || e.type !== 'user/message') continue
|
|
2643
|
+
// ⭐ 1.4.4:尺子是「她怎么说话」,喂进去的全得是人话;
|
|
2644
|
+
// 运行环境 / 片子字幕 / 骰子提示那些会把词频带偏。
|
|
2645
|
+
if (不是人说的话(e)) continue
|
|
2536
2646
|
const p = e.payload !== undefined ? e.payload : e.data && e.data.message !== undefined ? e.data.message : e.data
|
|
2537
2647
|
const txt = textOfMessage(p)
|
|
2538
2648
|
if (!txt) continue
|
|
@@ -3056,7 +3166,7 @@ function recall(data, query, now, sid) {
|
|
|
3056
3166
|
}
|
|
3057
3167
|
// ⭐⭐ 2026-10-27(issue #14):**常驻层** —— 长期事实不看这句话问的是什么,每轮都带上
|
|
3058
3168
|
//
|
|
3059
|
-
//
|
|
3169
|
+
// 当时的建议(转给用户那条):「把长期事实改为每轮注入,其他记忆打分淡忘」。
|
|
3060
3170
|
// 以前事实也走召回:这句话没撞上「我住在杭州」,这条就进不来 —— 可它明明是「要记一辈子」的事。
|
|
3061
3171
|
// 现在:
|
|
3062
3172
|
// · **长期事实**:每轮固定带 factsMax 条(按重要度、再按时间取前几名),
|
|
@@ -3812,6 +3922,9 @@ export function apply(ctx) {
|
|
|
3812
3922
|
try {
|
|
3813
3923
|
agent.ctx.on('agent/inbox/inserted', (payload) => {
|
|
3814
3924
|
const msg = payload && payload.message ? payload.message : payload
|
|
3925
|
+
// ⭐ 1.4.4:插件替人开口的那些(骰子喊人聊天 / 自动刷B站 …)不算人说的话,
|
|
3926
|
+
// 既不能当线索兜底,也不能喂进「她自己怎么说话」那把尺子。
|
|
3927
|
+
if (msg && 不是人说的话({ data: msg })) return
|
|
3815
3928
|
const txt = textOfMessage(msg)
|
|
3816
3929
|
if (txt) {
|
|
3817
3930
|
lastText = txt
|
|
@@ -3979,7 +4092,14 @@ export function apply(ctx) {
|
|
|
3979
4092
|
// 而每轮都要把整份读进来再整份写回去 → 每轮白白多花几十毫秒
|
|
3980
4093
|
// (每轮开销大就是这儿)
|
|
3981
4094
|
// scored 已经按 P 排好序了,所以前 24 条 = 挑中的 + 最接近的落选 ✓
|
|
3982
|
-
|
|
4095
|
+
// ⭐ 1.4.4:**同一轮别记两遍**。这段 text() 在一次请求里会被调用两次
|
|
4096
|
+
// (装配一次、真正发请求再一次),每次都 push 一条 trace —— 实测 222 条里
|
|
4097
|
+
// 有 108 条是「同一句、相隔 3 秒内」的重复,100 条上限白白占掉一半。
|
|
4098
|
+
// 判据:上一句跟这一句一模一样、而且才过了一小会儿 → 当作同一次,直接覆盖。
|
|
4099
|
+
const 上一条 = data.traces[data.traces.length - 1]
|
|
4100
|
+
const 同一次 = 上一条 && 上一条.query === query.slice(0, 200) &&
|
|
4101
|
+
Math.abs(now - Number(上一条.at || 0)) < 3000
|
|
4102
|
+
const 这条 = {
|
|
3983
4103
|
at: now,
|
|
3984
4104
|
session: sid,
|
|
3985
4105
|
query: query.slice(0, 200),
|
|
@@ -3996,7 +4116,9 @@ export function apply(ctx) {
|
|
|
3996
4116
|
// 进到提示词里的算 assembled;后面她回复里用上了就变 consumed ✓
|
|
3997
4117
|
status: r.omission ? null : 'assembled',
|
|
3998
4118
|
})),
|
|
3999
|
-
}
|
|
4119
|
+
}
|
|
4120
|
+
if (同一次) data.traces[data.traces.length - 1] = 这条
|
|
4121
|
+
else data.traces.push(这条)
|
|
4000
4122
|
// Trace persistence is auxiliary: an I/O failure must not hide a valid
|
|
4001
4123
|
// snapshot already read from disk. saveStore records the failure.
|
|
4002
4124
|
try { saveStore(sid, data) } catch (error) {}
|
|
@@ -6241,7 +6363,7 @@ mroute('/dsh-fish-memory/wash-keys/undo', async (req, res) => {
|
|
|
6241
6363
|
|
|
6242
6364
|
// 把记忆复制到**另一个会话** —— 她想在新对话里测(新对话没有上下文,最干净)✓
|
|
6243
6365
|
// { session: 从哪来, to: 到哪去 }
|
|
6244
|
-
// ⭐ 1.3.7
|
|
6366
|
+
// ⭐ 1.3.7(2026-10-28 报的):以前是**整份覆盖** —— 目标会话原来记的东西全没了。
|
|
6245
6367
|
// 现在改成**并进去**:那边原有的都留着,只补它还没有的(同一句话算重复,跳过)。
|
|
6246
6368
|
mroute('/dsh-fish-memory/copy-to', async (req, res) => {
|
|
6247
6369
|
let body = ''
|