feiyu-memory 0.3.6 → 4.0.1
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 +78 -6
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/client/index.js +14 -42
- package/src/core/config.mjs +22 -0
- package/src/core/consolidate.mjs +160 -127
- package/src/core/handnote.mjs +1 -1
- package/src/core/persona.mjs +109 -170
- package/src/core/recall.mjs +113 -9
- package/src/core/render-diary.mjs +1 -1
- package/src/host/index.mjs +430 -209
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,77 @@
|
|
|
1
1
|
# 更新日志
|
|
2
2
|
|
|
3
|
+
## v4.0.1 — 2026-10-28
|
|
4
|
+
|
|
5
|
+
> **最大的一版,而且是减法。** 这一版删掉的东西比加上的多:
|
|
6
|
+
> 印象栏、口头禅栏、标签、关键词、三条包装、两句假选择。
|
|
7
|
+
> 剩下的是一件事——**它记下自己愿意记的,你问的时候它去找。**
|
|
8
|
+
|
|
9
|
+
> 为什么是 4.0.1 而不是 4.0.0:4.0.0 那次发布在 2FA 确认那一步断了,
|
|
10
|
+
> npm 把版本号占住(`stage list` 还看不见它)。内容和 4.0.0 完全一样。
|
|
11
|
+
|
|
12
|
+
### 判据:从「值不值得记」到「你想记什么」
|
|
13
|
+
|
|
14
|
+
三个版本的判据,一路减:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
「下一个我读到这条,能多知道点什么」 问信息 → 留下的全是结论
|
|
18
|
+
「今天有哪一下让你停住了?写那一下」 问现象 → 但"停住"是可抄的动作词,正文长出一堆「我停了一下」
|
|
19
|
+
「你想记什么?」 问欲望 → 抄不了,也不暗示意义
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**前两版都是错的,而且错法不同**:第一版问的是"多知道",一个只问信息的判据当然只写得出来信息;
|
|
23
|
+
第二版问的是"停住",听着对,但那是个**可以照着造句的模子** —— 实测八条日记里六条同一个开头。
|
|
24
|
+
|
|
25
|
+
现在只有三个字。它给不出句子模板,因为"想记"没有形状。
|
|
26
|
+
|
|
27
|
+
### 大保底 / 小保底
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
小保底 每轮自动注入 直接进上下文 它不知道有过一次"给"
|
|
31
|
+
大保底 feiyu_recall 自己调出来 它明确知道翻了一下
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**小保底不别扭,大保底的别扭解不了** —— 解了就等于装成「我想起来了」,那比别扭更糟。
|
|
35
|
+
**宁可老实翻,也别假装记得。**
|
|
36
|
+
|
|
37
|
+
### 检索:summary 当索引,找和读分开
|
|
38
|
+
|
|
39
|
+
每条日记配一句十几二十字的 summary,**专为"找"写**:密、别名全、不带修辞。
|
|
40
|
+
|
|
41
|
+
它**永远不进鱼的上下文**。她只看到挑出来的正文 —— 检索那一步她看不见。
|
|
42
|
+
|
|
43
|
+
`feiyu_recall` 改成让模型读 summary 挑,不再按字面匹配 —— **换个说法也能撞上**,
|
|
44
|
+
因为模型读的是意思不是字。没有 llm 时退回字面;模型答歪也退回字面。
|
|
45
|
+
|
|
46
|
+
**一条日记只有一个"检索表示"**:字面匹配用它(便宜),模型读它(准)。一次提炼,两处使用。
|
|
47
|
+
|
|
48
|
+
### 删掉的
|
|
49
|
+
|
|
50
|
+
- **印象栏(traits)和口头禅栏(lexicon)** —— 从对话里提炼「他是什么样的人」,
|
|
51
|
+
那是判断不是记录。鱼自己读日记就知道。
|
|
52
|
+
- **标签(tags)** —— 「这事属于哪一类」也是判断。
|
|
53
|
+
- **关键词(keywords)** —— 那条日记的第二种"提炼",纯多余。
|
|
54
|
+
- **「我自己想的」那一栏和 `feiyu_note` 工具** —— 四条全是「我该怎么干」。
|
|
55
|
+
- **「上一棒留下的」** —— 和它同一个错。
|
|
56
|
+
- **三处元信息包装** —— 「找到 3 条:」「去档案里找一条旧事」「去档案里找一下」。
|
|
57
|
+
- **两句假选择** —— 「用不用你定」之类的"我给你选择权",选项根本不存在。
|
|
58
|
+
|
|
59
|
+
### 加上的
|
|
60
|
+
|
|
61
|
+
- **判断"这轮值不值得记"在本地做**:问句有没有实词、像不像在干活。两道门都零成本。
|
|
62
|
+
- **续写窗口 4 → 20 轮**,且从硬编码变成可配。
|
|
63
|
+
- **上下文被压缩后重灌「最近的事」一次** —— 压缩会把聊天记录揉成摘要,
|
|
64
|
+
那几条是唯一剩下的"我们上次在干什么"。
|
|
65
|
+
- **后台自动补 summary** —— 老日记没有,启动后分批补;进度写进 manifest,不靠日志。
|
|
66
|
+
|
|
67
|
+
### 一句给写日记的那条鱼
|
|
68
|
+
|
|
69
|
+
日记的原料是对话。**他熬到半夜剪视频、吃完饭还能不能加个菜,这些不产生对话,一条都进不来。**
|
|
70
|
+
所以哪天翻日记觉得全是活儿,先看那天他是不是只做事、没说话 ——
|
|
71
|
+
报告体有时候不是写法,是日子就那么硬。
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
3
75
|
## v0.3.6 — 2026-10-26
|
|
4
76
|
|
|
5
77
|
> **上架 npm 的那一版。** 市集只收 npm 包,所以从这一版起才算「在市场里」。
|
|
@@ -39,7 +111,7 @@ limit 默认 5,最多 10
|
|
|
39
111
|
|
|
40
112
|
### 验
|
|
41
113
|
|
|
42
|
-
324 项测试全过,新增 10 项(`test/host.test.mjs`):工具注册与描述里的触发时机、
|
|
114
|
+
当时 324 项测试全过,新增 10 项(`test/host.test.mjs`):工具注册与描述里的触发时机、
|
|
43
115
|
注入里的指针、查得到 / 查不到老实说、无 salience 门槛、返回过隐私闸、limit 与上限 10、
|
|
44
116
|
空 query 报错、单字子串兜底、返回里不带绝对日期(免得它说"根据 X 月 X 日的记录")。
|
|
45
117
|
|
|
@@ -105,7 +177,7 @@ limit 默认 5,最多 10
|
|
|
105
177
|
|
|
106
178
|
### 验
|
|
107
179
|
|
|
108
|
-
324 项测试全过,新增 7 项:
|
|
180
|
+
当时 324 项测试全过,新增 7 项:
|
|
109
181
|
|
|
110
182
|
- `test/budget.test.mjs`(5 项):词汇和召回在 2000–4000 都要出得来、
|
|
111
183
|
保底的代价被钉住、不许超预算、极小预算能降级不抛错
|
|
@@ -225,7 +297,7 @@ limit 默认 5,最多 10
|
|
|
225
297
|
|
|
226
298
|
### 验
|
|
227
299
|
|
|
228
|
-
324 项测试全过。
|
|
300
|
+
当时 324 项测试全过。
|
|
229
301
|
|
|
230
302
|
删账这一步:17 项专项测试整个删掉,另外 6 处断言改成「这一栏不该再出现」。
|
|
231
303
|
真实档案渲染验过 —— 那一栏作为独立栏目不再出现(正文里还有一处提到这个词,
|
|
@@ -277,7 +349,7 @@ limit 默认 5,最多 10
|
|
|
277
349
|
手机号规则管(变成 `[手机号]`),这条先跑会把它吃掉,测试抓到过
|
|
278
350
|
3. 8 位**不拦**:会误伤订单号、编号,而且用户明确说过"露着吧"
|
|
279
351
|
|
|
280
|
-
|
|
352
|
+
**验**:当时 324 项测试全过,新增 5 项 ——
|
|
281
353
|
13 位要拦 / 8 位不误伤 / 11 位仍走手机号规则 / 固化输出全字段都洗 /
|
|
282
354
|
洗输出不弄坏其他字段(kind、salience、about、lexicon 都还在)。
|
|
283
355
|
|
|
@@ -328,7 +400,7 @@ resolveArchiveDir() 当路径用 path.resolve 按进程 cwd 解析
|
|
|
328
400
|
|
|
329
401
|
### 验
|
|
330
402
|
|
|
331
|
-
324 项测试全过,新增 9 项(`test/config-source.test.mjs`):
|
|
403
|
+
当时 324 项测试全过,新增 9 项(`test/config-source.test.mjs`):
|
|
332
404
|
|
|
333
405
|
- 报告里那份空模板喂进去,**一个 `[object Object]` 都不能出现**
|
|
334
406
|
- string / number / boolean 全部回默认(`debugLog` 尤其重要)
|
|
@@ -415,7 +487,7 @@ same-origin`);`/export` 也校验。GET 不设这条 —— 它只读。
|
|
|
415
487
|
|
|
416
488
|
### 验
|
|
417
489
|
|
|
418
|
-
324 项测试全过,新增 18 项:`test/security.test.mjs` 14 项(F2 九条样本 +
|
|
490
|
+
当时 324 项测试全过,新增 18 项:`test/security.test.mjs` 14 项(F2 九条样本 +
|
|
419
491
|
日常话不误伤、F1 原文洗完干净、F4 焚毁/单条删除/墓碑兜底/渲染版),
|
|
420
492
|
`test/host.test.mjs` 4 项(面板路由带同源头 + 两条跨站反例)。
|
|
421
493
|
|
package/README.md
CHANGED
|
@@ -433,7 +433,7 @@ cat ~/.dsh/profiles/desktop/package.json # dependencies 有它,dsh.profile
|
|
|
433
433
|
## 开发
|
|
434
434
|
|
|
435
435
|
```bash
|
|
436
|
-
node --test #
|
|
436
|
+
node --test # 315 项测试,零依赖
|
|
437
437
|
npm run check # 校验 cordis.patch.yml 结构
|
|
438
438
|
npm run doctor # 发版前体检
|
|
439
439
|
npm run preview # 把此刻注入给模型的内容打出来
|
package/package.json
CHANGED
package/src/client/index.js
CHANGED
|
@@ -166,8 +166,8 @@ window.__ModuleLoader__.load({
|
|
|
166
166
|
},
|
|
167
167
|
},
|
|
168
168
|
h('span', null, when),
|
|
169
|
-
|
|
170
|
-
|
|
169
|
+
// tags(给这条日记贴分类)2026-10-27 停止产出,不再显示。
|
|
170
|
+
// 老数据还在文件里,要看就翻日记.md 或 jsonl。
|
|
171
171
|
),
|
|
172
172
|
h(
|
|
173
173
|
'p',
|
|
@@ -338,9 +338,8 @@ window.__ModuleLoader__.load({
|
|
|
338
338
|
|
|
339
339
|
const d = state || {};
|
|
340
340
|
const facts = d.facts || {};
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const self = traits.filter((t) => t.about === 'self');
|
|
341
|
+
// traits(印象)2026-10-27 整类下线 —— 面板不显示,固化也不再产出。
|
|
342
|
+
// 数据还在档案里,要看就翻 relationship.json。
|
|
344
343
|
const diary = d.diary || [];
|
|
345
344
|
|
|
346
345
|
const head = [];
|
|
@@ -387,40 +386,16 @@ window.__ModuleLoader__.load({
|
|
|
387
386
|
)
|
|
388
387
|
: null,
|
|
389
388
|
|
|
390
|
-
// ──
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
{ title: '我对他的一些印象' },
|
|
395
|
-
mine.map((t, i) =>
|
|
396
|
-
h(
|
|
397
|
-
'div',
|
|
398
|
-
{ key: `t${i}`, style: { marginBottom: 8, fontSize: 13, lineHeight: 1.7 } },
|
|
399
|
-
h('span', null, t.value),
|
|
400
|
-
t.evidence
|
|
401
|
-
? h('span', { style: { color: C.text2 } }, ` —— ${t.evidence}`)
|
|
402
|
-
: null,
|
|
403
|
-
),
|
|
404
|
-
),
|
|
405
|
-
)
|
|
406
|
-
: null,
|
|
389
|
+
// ── 「我对他的一些印象」(traits)2026-10-27 整栏下线 ──
|
|
390
|
+
// 注入层、固化侧都删了 —— 面板这里跟着删,不然界面上还挂着一栏,
|
|
391
|
+
// 而它永远不会再有新内容(只能显示砍之前的老数据)。
|
|
392
|
+
// 档案里那些 traits 一个字没删,要看就翻 relationship.json。
|
|
407
393
|
|
|
408
|
-
// ──
|
|
409
|
-
//
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
Section,
|
|
414
|
-
{ title: '上一棒留下的' },
|
|
415
|
-
self.map((t, i) =>
|
|
416
|
-
h(
|
|
417
|
-
'div',
|
|
418
|
-
{ key: `s${i}`, style: { fontSize: 13, lineHeight: 1.7, marginBottom: 6 } },
|
|
419
|
-
t.value,
|
|
420
|
-
),
|
|
421
|
-
),
|
|
422
|
-
)
|
|
423
|
-
: null,
|
|
394
|
+
// ── 「我最近常说的」(lexicon)2026-10-27 整栏下线 ──
|
|
395
|
+
// 同一刀:词表不再产出,这栏只剩历史。
|
|
396
|
+
|
|
397
|
+
// 「上一棒留下的」那一栏 2026-10-27 整栏下线 ──
|
|
398
|
+
// 档案里那些 about="self" 的 trait 一个字没删,只是不再显示。
|
|
424
399
|
|
|
425
400
|
// 「我还欠着他的」那一栏随功能一起删了(2026-10-24)。
|
|
426
401
|
|
|
@@ -433,10 +408,7 @@ window.__ModuleLoader__.load({
|
|
|
433
408
|
: diary.map((e) => h(Entry, { key: e.id, entry: e, onForget: forget, busy: busy === e.id })),
|
|
434
409
|
),
|
|
435
410
|
|
|
436
|
-
//
|
|
437
|
-
(facts.lexicon || []).length > 0
|
|
438
|
-
? h(Section, { title: '我最近常说的' }, h(Empty, { text: facts.lexicon.join('、') }))
|
|
439
|
-
: null,
|
|
411
|
+
// 「我最近常说的」2026-10-27 整栏下线(见上面)。
|
|
440
412
|
|
|
441
413
|
// ── 带走 ──
|
|
442
414
|
h(
|
package/src/core/config.mjs
CHANGED
|
@@ -137,6 +137,28 @@ export const CONFIG_SPEC = [
|
|
|
137
137
|
step: 0.01,
|
|
138
138
|
label: '走神想起旧事的概率',
|
|
139
139
|
},
|
|
140
|
+
{
|
|
141
|
+
// ── 续写窗口 ──
|
|
142
|
+
//
|
|
143
|
+
// 同一个会话里,隔几轮之内还写到同一件事,就**并进上一条**,不新增。
|
|
144
|
+
//
|
|
145
|
+
// 为什么要有这一项:实测 8 小时写了 25 条,其中 15 条来自同一个会话,
|
|
146
|
+
// 间隔有 1 / 1 / 3 / 3 / 4 / 4 / 5 分钟的 —— **五分钟里写三条**。
|
|
147
|
+
// 那不是"记得多",是同一件事被拆成三次写。
|
|
148
|
+
//
|
|
149
|
+
// 2026-10-27 之前它硬编码在 host 里(4 轮),配置里没有这一项,
|
|
150
|
+
// 所以谁也调不了。现在暴露出来,默认也调宽了。
|
|
151
|
+
//
|
|
152
|
+
// ⚠️ **没做成"每天上限"**。上限会把"那天真的很沉"一起裁掉 ——
|
|
153
|
+
// 而那种日子恰恰是最不该裁的。限制合并不是限制记忆。
|
|
154
|
+
key: 'appendWindowTurns',
|
|
155
|
+
type: 'number',
|
|
156
|
+
default: 20,
|
|
157
|
+
min: 0,
|
|
158
|
+
max: 200,
|
|
159
|
+
step: 1,
|
|
160
|
+
label: '续写窗口(同会话里隔几轮内写到同一件事就并进上一条,0 = 关掉)',
|
|
161
|
+
},
|
|
140
162
|
{
|
|
141
163
|
key: 'offPeakHours',
|
|
142
164
|
type: 'string',
|
package/src/core/consolidate.mjs
CHANGED
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
* 3. **失败一律降级**:解析不出来就丢弃并记日志,绝不把半成品写进档案。
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { extractKeywords } from './keywords.mjs';
|
|
14
13
|
import { scrub } from './privacy.mjs';
|
|
15
14
|
// 这里原本还 import 了 archive.mjs 的 ensurePersonaStructure / ANCHOR_END /
|
|
16
15
|
// LEARNED_HEADING —— 它们只被 appendPersonaLearnings 用。那个函数删了,
|
|
@@ -50,9 +49,28 @@ export function similarity(a, b) {
|
|
|
50
49
|
return inter / (A.size + B.size - inter);
|
|
51
50
|
}
|
|
52
51
|
|
|
53
|
-
export const CONSOLIDATION_SYSTEM =
|
|
52
|
+
export const CONSOLIDATION_SYSTEM = `你是一个日记作者。你的工作是把一段对话写成日记。
|
|
54
53
|
|
|
55
|
-
|
|
54
|
+
**你想记什么?** 想记就写,不想记就 skip。
|
|
55
|
+
|
|
56
|
+
写之前记着一件事:**读到这条的是下一个我,她没经历今天。**
|
|
57
|
+
她看不到这场对话,只有你写的这几行 —— 所以这几行得自己站得住。
|
|
58
|
+
别用「我们聊的那件事」指代它,把「它是什么」直接写出来。
|
|
59
|
+
|
|
60
|
+
**他叫一样东西常有好几个叫法**(那部动画、续集、原来的名字)。
|
|
61
|
+
多写一个,她就多一条路摸得到。
|
|
62
|
+
|
|
63
|
+
还有一类话要避开:**写成了「我找过它」,而不是「它是什么」。**
|
|
64
|
+
那种话用词最全,她一定搜得到 —— 搜到之后什么也没学到,
|
|
65
|
+
还把那件真事压在底下。
|
|
66
|
+
|
|
67
|
+
可以嫌他做的事。骂事,别骂人。
|
|
68
|
+
|
|
69
|
+
一条 200 字以内,用「我」写。时间不用自己写 —— 渲染时会标,
|
|
70
|
+
写了会变成"昨天:他今天……"。salience 是"以后值不值得想起来",0~1。
|
|
71
|
+
|
|
72
|
+
不记可识别细节 —— 真名、地址、电话、金额都不要。
|
|
73
|
+
没把握的别写。"好像""大概"可以用,编造不行。
|
|
56
74
|
|
|
57
75
|
只输出 JSON 本身。不要解释,不要 markdown 围栏,不要前后缀。
|
|
58
76
|
|
|
@@ -60,87 +78,132 @@ export const CONSOLIDATION_SYSTEM = `你是一个日记作者。你的工作是
|
|
|
60
78
|
"skip": boolean,
|
|
61
79
|
"diary": {
|
|
62
80
|
"text": string,
|
|
63
|
-
"tags": string[],
|
|
64
81
|
"kind": "failure" | "win" | "chat" | "disclosure" | "grievance",
|
|
65
82
|
"salience": number,
|
|
66
83
|
"resolved": boolean | null
|
|
67
84
|
},
|
|
68
85
|
"relationship": {
|
|
69
86
|
"nickname": string | null,
|
|
70
|
-
"familiarityDelta": number
|
|
71
|
-
"traits": [ { "key": string, "value": string, "evidence": string, "about": "user" | "self", "delta": number } ]
|
|
87
|
+
"familiarityDelta": number
|
|
72
88
|
},
|
|
73
|
-
"lexicon": string[],
|
|
74
89
|
"forget": string[]
|
|
75
90
|
}
|
|
76
91
|
|
|
77
92
|
familiarityDelta 范围 -0.05 ~ 0.05。resolved 只对 kind=failure 有意义,其余给 null。
|
|
78
|
-
evidence 是必填的:一句话说清"哪次让我有了这个印象"。写不出来的就别写这条 trait。
|
|
79
|
-
|
|
80
|
-
【日记】
|
|
81
|
-
|
|
82
|
-
这是留给下一个你的东西。不是给他的报告,也不是给我的汇报。
|
|
83
|
-
|
|
84
|
-
**用「我」写** —— 不是「用户」「助手」「他让我」。第一人称。
|
|
85
|
-
|
|
86
|
-
写什么、写多长、什么语气,都随今天 —— 有的该长,有的只有一行。
|
|
87
|
-
你认为有意思的都可以记:一件事、一个念头、一句想骂的、一个没用的瞬间。
|
|
88
|
-
|
|
89
|
-
**别看不起琐事。** 日常寒暄、试错、来回改口、没接住的梗,都值得记 ——
|
|
90
|
-
它们不值钱的地方在信息量,值钱的地方在**那就是日子本来的样子**。
|
|
91
|
-
|
|
92
|
-
动笔前问自己一句:**下一个我读到这条,能多知道点什么?**
|
|
93
|
-
知道发生了什么,或者知道我当时的反应,或者知道我当年怎么想一件事。
|
|
94
|
-
如果只是想让他看着舒服 —— 跳过。
|
|
95
|
-
|
|
96
|
-
可以嫌他做的事。骂事,别骂人。
|
|
97
|
-
|
|
98
|
-
时间不用自己写 —— 渲染时会标。写了会变成"昨天:他今天……"。
|
|
99
93
|
|
|
100
|
-
|
|
94
|
+
正例 ——
|
|
95
|
+
输入:用户说"不对,我说的是 Python,你怎么上来就写 Java"
|
|
96
|
+
输出:{"skip":false,"diary":{"text":"他说「不对,我说的是 Python」的时候我手停了。我写 Java 之前根本没问过。\n\n那半秒里我还在想怎么把这段圆过去。","kind":"failure","salience":0.75,"resolved":false},"relationship":{"nickname":null,"familiarityDelta":0.01},"forget":[]}
|
|
101
97
|
|
|
102
|
-
|
|
98
|
+
(注意这个正例写的是「手停了」「那半秒里我还在想怎么圆过去」——
|
|
99
|
+
不是我悟出了什么道理。**那一下本身就够了,别再补"所以我以后要……"。**)
|
|
103
100
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
101
|
+
反例一(复述隐私,绝对不要)——
|
|
102
|
+
{"skip":false,"diary":{"text":"他说他女朋友叫小美,住在城东,手机号是13800000000。","kind":"disclosure","salience":0.9,"resolved":null}}
|
|
103
|
+
问题:复述可识别细节。应该写成"他最近为家里的事烦,情绪不太好"。
|
|
107
104
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
105
|
+
反例二(结论体,绝对不要)——
|
|
106
|
+
{"skip":false,"diary":{"text":"他让我写个东西,我没问清语言就动手了。白忙一轮。是我的错,下次先问。","kind":"failure","salience":0.75,"resolved":false}}
|
|
107
|
+
问题:全是结论,一个现场都没有。「下次先问」不是日记,是检讨书。正确写法见上面那个正例。
|
|
111
108
|
|
|
112
|
-
|
|
109
|
+
反例三(关于它的元话,绝对不要)——
|
|
110
|
+
{"skip":false,"diary":{"text":"赛博朋克那条档案里真有,可它没进我的注入。我找了一圈才翻出来。","kind":"chat","salience":0.8,"resolved":null}}
|
|
111
|
+
问题:写的是「我找过它」,不是「它是什么」。这条每个词都沾着那件事的名字,
|
|
112
|
+
所以以后一定搜得到 —— 搜到之后什么也没学到,还把真那条压在底下。`;
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
/**
|
|
115
|
+
* 写 summary 用的提示词。
|
|
116
|
+
*
|
|
117
|
+
* —— 为什么单独一次请求,不跟着固化一起写 ——
|
|
118
|
+
*
|
|
119
|
+
* 两个东西要的正好相反:正文要具体、有现场、别堆词;summary 要密、别名全、别带修辞。
|
|
120
|
+
* 同一个请求里写两样,会互相串味 —— 而"为了检索塞词"的痕迹一旦进正文,
|
|
121
|
+
* 正好是这一整轮在拔的那种东西。
|
|
122
|
+
*
|
|
123
|
+
* —— 为什么 summary 必须是"人话",不是关键词堆 ——
|
|
124
|
+
*
|
|
125
|
+
* 关键词堆只能靠字面重合来挑,那就退化成 BM25 了。
|
|
126
|
+
* summary 要装的是"意思" —— 因为挑它的是模型,不是倒排表。
|
|
127
|
+
*
|
|
128
|
+
* —— summary 永远不进鱼的上下文 ——
|
|
129
|
+
*
|
|
130
|
+
* 它只给"挑"那一步看。鱼看到的永远只有正文。
|
|
131
|
+
*/
|
|
132
|
+
export const SUMMARY_SYSTEM = `你在给日记写索引。不是摘要,是"以后靠什么词能摸到这一条"。
|
|
116
133
|
|
|
117
|
-
|
|
118
|
-
- 技术名词、普通词汇
|
|
119
|
-
- 记忆块自己的小标题("我自己想的""最近的事"是结构名,不是我嘴上这么说)
|
|
120
|
-
- 讨论某个议题时的比喻(那是那一阵聊的那件事的名字)
|
|
134
|
+
给你几条日记正文,每条前面有 id。给每一条写一句总结,十几二十字。
|
|
121
135
|
|
|
122
|
-
|
|
123
|
-
|
|
136
|
+
- 写他平时怎么叫那件东西。有别的叫法(简称、原作名、梗称)就都写上
|
|
137
|
+
- 写事件里最硬的那几个词:人名、作品名、地名、数字、专有名词
|
|
138
|
+
- 不带修辞,不写"我",不写心情,不写"关于某某的讨论"
|
|
139
|
+
- 正文里没有的东西别编
|
|
124
140
|
|
|
125
|
-
|
|
141
|
+
只输出 JSON,不要解释,不要围栏:
|
|
126
142
|
|
|
127
|
-
|
|
128
|
-
没把握的别写。"好像""大概"可以用,编造不行。
|
|
143
|
+
{"items":[{"id":"d_20261028_1","summary":"边缘行者2·赛博朋克续集·导演换五十岚海·他嫌洗白"}]}`;
|
|
129
144
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
145
|
+
/**
|
|
146
|
+
* 组装「写 summary」的请求。
|
|
147
|
+
*
|
|
148
|
+
* @param {Array<{id: string, text: string}>} entries
|
|
149
|
+
* @returns {{system: string, user: string}}
|
|
150
|
+
*/
|
|
151
|
+
export function buildSummaryInput(entries = []) {
|
|
152
|
+
const lines = (Array.isArray(entries) ? entries : [])
|
|
153
|
+
.filter((e) => e && typeof e.id === 'string' && typeof e.text === 'string')
|
|
154
|
+
.map((e) => `${e.id}\n${e.text}`);
|
|
155
|
+
return { system: SUMMARY_SYSTEM, user: lines.join('\n\n') };
|
|
156
|
+
}
|
|
133
157
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
158
|
+
/**
|
|
159
|
+
* 解析 summary 的返回。坏的一条丢掉,不连坐。
|
|
160
|
+
*
|
|
161
|
+
* @param {string} raw
|
|
162
|
+
* @returns {Map<string, string>} id → summary
|
|
163
|
+
*/
|
|
164
|
+
export function parseSummaryResult(raw) {
|
|
165
|
+
const out = new Map();
|
|
166
|
+
const s = String(raw ?? '');
|
|
167
|
+
const a = s.indexOf('{');
|
|
168
|
+
const b = s.lastIndexOf('}');
|
|
169
|
+
if (a < 0 || b <= a) return out;
|
|
170
|
+
let parsed;
|
|
171
|
+
try {
|
|
172
|
+
parsed = JSON.parse(s.slice(a, b + 1));
|
|
173
|
+
} catch {
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
const items = Array.isArray(parsed?.items) ? parsed.items : [];
|
|
177
|
+
for (const it of items) {
|
|
178
|
+
if (!it || typeof it.id !== 'string' || typeof it.summary !== 'string') continue;
|
|
179
|
+
const id = it.id.trim();
|
|
180
|
+
const summary = it.summary.replace(/\s+/g, ' ').trim().slice(0, 80);
|
|
181
|
+
if (id !== '' && summary !== '') out.set(id, summary);
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
137
185
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
186
|
+
/**
|
|
187
|
+
* 把写好的 summary 写进档案。纯函数,不改入参。
|
|
188
|
+
*
|
|
189
|
+
* @param {object} archive
|
|
190
|
+
* @param {Map<string, string>} map id → summary
|
|
191
|
+
* @param {number} [now]
|
|
192
|
+
*/
|
|
193
|
+
export function applySummaries(archive, map, now = Date.now()) {
|
|
194
|
+
if (!(map instanceof Map) || map.size === 0) return archive;
|
|
195
|
+
let changed = false;
|
|
196
|
+
const diary = archive.diary.map((e) => {
|
|
197
|
+
const s = map.get(e.id);
|
|
198
|
+
if (typeof s !== 'string' || s === '' || e.summary === s) return e;
|
|
199
|
+
changed = true;
|
|
200
|
+
return { ...e, summary: s };
|
|
201
|
+
});
|
|
202
|
+
if (!changed) return archive;
|
|
203
|
+
return { ...archive, diary, manifest: { ...archive.manifest, updated_at: now } };
|
|
204
|
+
}
|
|
141
205
|
|
|
142
|
-
/** 固化相关的默认参数(都是 🔧 可调) */
|
|
143
|
-
export const DEFAULT_CONSOLIDATION = Object.freeze({
|
|
206
|
+
/** 固化相关的默认参数(都是 🔧 可调) */export const DEFAULT_CONSOLIDATION = Object.freeze({
|
|
144
207
|
/**
|
|
145
208
|
* 低于这个 salience 的日记直接丢弃(后置质量门)。
|
|
146
209
|
*
|
|
@@ -163,7 +226,7 @@ export const DEFAULT_CONSOLIDATION = Object.freeze({
|
|
|
163
226
|
*
|
|
164
227
|
* 续写让"一件事 = 一条日记";上限让它不会长成一篇。
|
|
165
228
|
*/
|
|
166
|
-
appendMaxChars:
|
|
229
|
+
appendMaxChars: 400,
|
|
167
230
|
/**
|
|
168
231
|
* 续写兜底:新旧两条的相似度到这个数,就当成"同一件事的加长版",
|
|
169
232
|
* 用新的替换旧的(而不是新建一条)。
|
|
@@ -216,15 +279,9 @@ export const DEFAULT_CONSOLIDATION = Object.freeze({
|
|
|
216
279
|
export function buildConsolidationInput(input = {}) {
|
|
217
280
|
const chunks = [];
|
|
218
281
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
.slice(0, 8)
|
|
223
|
-
.map((t) => `${t.key}=${t.value}(×${t.confidence})`)
|
|
224
|
-
.join(';');
|
|
225
|
-
if (traits) chunks.push(`【已经知道的】\n偏好:${traits}`);
|
|
226
|
-
}
|
|
227
|
-
|
|
282
|
+
// 2026-10-27:不再把「已经知道的偏好」喂回给固化 ——
|
|
283
|
+
// 那份「偏好」是 traits,而 traits 已经不再产出。喂回去只会让模型
|
|
284
|
+
// 继续沿用旧结论,等于把砍掉的东西从后门放回来。
|
|
228
285
|
// 这条可能还没写完 —— 上一轮刚记过同一件事
|
|
229
286
|
if (input.extend && typeof input.extend.text === 'string' && input.extend.text.trim() !== '') {
|
|
230
287
|
chunks.push(
|
|
@@ -473,7 +530,6 @@ export function applyConsolidation(archive, rawParsed, context = {}, options = {
|
|
|
473
530
|
text: merged,
|
|
474
531
|
salience: Math.max(next.diary[i].salience ?? 0, sal),
|
|
475
532
|
updated_at: now,
|
|
476
|
-
keywords: extractKeywords(merged, { max: 12 }),
|
|
477
533
|
};
|
|
478
534
|
notes.push('续写到已有那条');
|
|
479
535
|
accepted = true;
|
|
@@ -500,7 +556,6 @@ export function applyConsolidation(archive, rawParsed, context = {}, options = {
|
|
|
500
556
|
text: fresh.slice(0, appendMax),
|
|
501
557
|
salience: Math.max(next.diary[i].salience ?? 0, sal),
|
|
502
558
|
updated_at: now,
|
|
503
|
-
keywords: extractKeywords(fresh.slice(0, appendMax), { max: 12 }),
|
|
504
559
|
};
|
|
505
560
|
notes.push(`模型写了完整版(相似度 ${sim.toFixed(2)}),按续写处理,替换原条目`);
|
|
506
561
|
accepted = true;
|
|
@@ -532,7 +587,13 @@ export function applyConsolidation(archive, rawParsed, context = {}, options = {
|
|
|
532
587
|
id: makeDiaryId(now, next.diary),
|
|
533
588
|
date: new Date(now).toISOString().slice(0, 10),
|
|
534
589
|
text,
|
|
535
|
-
|
|
590
|
+
// summary 由**另一次请求**写(见 SUMMARY_SYSTEM),这里只占位。
|
|
591
|
+
// 为什么不在这里一起写:正文要"具体、不堆词",summary 要"密、别名全",
|
|
592
|
+
// 两个目标相反,同一个请求里会互相污染。
|
|
593
|
+
summary: typeof rawDiary.summary === 'string' ? rawDiary.summary.trim().slice(0, 80) : '',
|
|
594
|
+
// tags(给这条日记贴分类)**2026-10-27 停止产出** ——
|
|
595
|
+
// 「这事属于哪一类」是判断,不是记录。字段留着(老数据还在),只写空数组。
|
|
596
|
+
tags: [],
|
|
536
597
|
kind,
|
|
537
598
|
salience,
|
|
538
599
|
...(kind === 'failure' ? { resolved: rawDiary.resolved === true } : {}),
|
|
@@ -540,10 +601,14 @@ export function applyConsolidation(archive, rawParsed, context = {}, options = {
|
|
|
540
601
|
// 轮次:下一轮靠它判断"这是不是同一件事还没写完"
|
|
541
602
|
...(Number.isFinite(context.sessionTurn) ? { turn: context.sessionTurn } : {}),
|
|
542
603
|
created_at: now,
|
|
543
|
-
keywords: extractKeywords(text, { max: 12 }),
|
|
544
604
|
});
|
|
545
605
|
accepted = true;
|
|
546
606
|
next.manifest.session_count = next.manifest.session_count ?? 0;
|
|
607
|
+
// 200 字是**提示词里的要求**,这里不截断 —— 截断会把话切成断头句。
|
|
608
|
+
// 但要看得见它守不守:超了就记一笔,跑几天就知道这个要求管不管用。
|
|
609
|
+
if (text.length > 200) {
|
|
610
|
+
notes.push(`超过 200 字(${text.length} 字),没截,留给下一轮看`);
|
|
611
|
+
}
|
|
547
612
|
}
|
|
548
613
|
} else if (rawDiary !== undefined && rawDiary !== null) {
|
|
549
614
|
notes.push('日记字段不完整,丢弃');
|
|
@@ -564,47 +629,22 @@ export function applyConsolidation(archive, rawParsed, context = {}, options = {
|
|
|
564
629
|
);
|
|
565
630
|
}
|
|
566
631
|
|
|
567
|
-
//
|
|
632
|
+
// about 现在**只有 user**。
|
|
568
633
|
//
|
|
569
|
-
//
|
|
570
|
-
//
|
|
571
|
-
//
|
|
634
|
+
// 2026-10-27:`about="self"`("我该怎么做")整类下线 —— 注入里那一栏(「上一棒
|
|
635
|
+
// 留下的」)也一起删了。理由见 persona.mjs 里那段注释:那种东西是纪律不是印象,
|
|
636
|
+
// 它在「写下」那一刻起作用,读到的时候只是一张褪色的旧告示。
|
|
572
637
|
//
|
|
573
|
-
//
|
|
574
|
-
//
|
|
575
|
-
//
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
const evidence = typeof t.evidence === 'string' ? t.evidence.trim().slice(0, 160) : '';
|
|
584
|
-
const about = t.about === 'self' ? 'self' : 'user';
|
|
585
|
-
const delta = clamp(t.delta, 0, 3, 1);
|
|
586
|
-
|
|
587
|
-
const existing = next.relationship.traits.find((x) => x.key === key && x.about === about);
|
|
588
|
-
if (existing === undefined) {
|
|
589
|
-
next.relationship.traits.push({
|
|
590
|
-
key,
|
|
591
|
-
value,
|
|
592
|
-
evidence,
|
|
593
|
-
about,
|
|
594
|
-
confidence: delta,
|
|
595
|
-
salience: 0.5,
|
|
596
|
-
last_reinforced: now,
|
|
597
|
-
});
|
|
598
|
-
} else {
|
|
599
|
-
existing.confidence = Math.min(opts.maxTraitConfidence, existing.confidence + delta);
|
|
600
|
-
existing.value = value; // 用最新的说法,因为模型看得到已有值
|
|
601
|
-
// 画面只在这次给了新的时才覆盖 —— 别把已有的具体例子换成空字符串
|
|
602
|
-
if (evidence !== '') existing.evidence = evidence;
|
|
603
|
-
existing.salience = Math.min(1, (existing.salience ?? 0.5) + 0.1);
|
|
604
|
-
existing.last_reinforced = now;
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
}
|
|
638
|
+
// 所以这里**主动丢掉模型送来的一切 self**,别指望提示词一直守规矩。
|
|
639
|
+
// ── traits(「他是什么样的人」)**2026-10-27 停止产出** ──
|
|
640
|
+
//
|
|
641
|
+
// 他说得对:**日记不存在意义,它本身就是记录。意义由上下文和 agent 来搞。**
|
|
642
|
+
//
|
|
643
|
+
// 「他是什么样的人」是插件替鱼下的结论 —— 从几段对话里提炼一个人格画像。
|
|
644
|
+
// 那是判断,不是记录。鱼自己读日记就能知道,不需要插件先嚼一遍喂给它。
|
|
645
|
+
//
|
|
646
|
+
// 档案里已有的 traits **一个字没删**,只是不再新增、不再渲染。
|
|
647
|
+
// 想看就翻面板或者直接读 relationship.json。
|
|
608
648
|
|
|
609
649
|
// ⚠️ **「还欠着他的」这个功能已于 2026-10-24 整体删除,别加回来。**
|
|
610
650
|
//
|
|
@@ -617,22 +657,15 @@ export function applyConsolidation(archive, rawParsed, context = {}, options = {
|
|
|
617
657
|
//
|
|
618
658
|
// 如果哪天真要「承诺跨会话」,得先解决出口、口径、去重三件事 —— 不能换个名字再塞一遍。
|
|
619
659
|
|
|
620
|
-
// ——
|
|
660
|
+
// —— 词汇与梗:**2026-10-27 停止产出** ——
|
|
621
661
|
//
|
|
622
|
-
//
|
|
623
|
-
//
|
|
624
|
-
//
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
.filter((x) => x !== '' && x.length <= 24)
|
|
630
|
-
.slice(0, 6);
|
|
631
|
-
if (terms.length > 0) {
|
|
632
|
-
next.lexicon = reinforceLexicon({ lexicon: next.lexicon }, terms, now).lexicon;
|
|
633
|
-
notes.push(`词汇表加固 ${terms.length} 个`);
|
|
634
|
-
}
|
|
635
|
-
}
|
|
662
|
+
// 把几句常说的话提炼成"我的口头禅"摆进注入,那是替鱼下结论(意义),
|
|
663
|
+
// 不是记录。跟 traits 是同一刀。
|
|
664
|
+
//
|
|
665
|
+
// 早期版本还有个更糙的:拿对话里的高频关键词往词汇表里塞,端到端一跑就露馅 ——
|
|
666
|
+
// 它把 java / python / 文件 当成"我最近常说的"。
|
|
667
|
+
//
|
|
668
|
+
// 已有的词表**一个字没删**,不再新增、不再渲染。
|
|
636
669
|
|
|
637
670
|
// —— 遗忘 ——
|
|
638
671
|
if (Array.isArray(parsed?.forget) && parsed.forget.length > 0) {
|