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 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
- **验**:324 项测试全过,新增 5 项 ——
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 # 324 项测试,零依赖
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feiyu-memory",
3
- "version": "0.3.6",
3
+ "version": "4.0.1",
4
4
  "description": "给 DeepSeek Harness 一条会记事的蓝色大肥鱼:跨会话的持久记忆 —— 人格内核、我自己想的、情景日记。",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -166,8 +166,8 @@ window.__ModuleLoader__.load({
166
166
  },
167
167
  },
168
168
  h('span', null, when),
169
- e.tags && e.tags.length > 0 ? h('span', { style: { opacity: 0.6 } }, '·') : null,
170
- h('span', null, (e.tags || []).join(' · ')),
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
- const traits = d.traits || [];
342
- const mine = traits.filter((t) => t.about !== 'self');
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
- mine.length > 0
392
- ? h(
393
- Section,
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
- // 措辞必须跟注入层(persona.mjs)一致 —— 同一个东西两个说法,
410
- // 就是我们刚修过的那类毛病换了个出口。
411
- self.length > 0
412
- ? h(
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(
@@ -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',
@@ -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
- salience 是"以后值不值得想起来",0~1。
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
- traits 分两种,用 about 区分:
105
- - about="user":他是什么样的人
106
- - about="self":我该怎么做
101
+ 反例一(复述隐私,绝对不要)——
102
+ {"skip":false,"diary":{"text":"他说他女朋友叫小美,住在城东,手机号是13800000000。","kind":"disclosure","salience":0.9,"resolved":null}}
103
+ 问题:复述可识别细节。应该写成"他最近为家里的事烦,情绪不太好"。
107
104
 
108
- 判据一句话:**这句话能不能填进「他是个 ___ 的人」?填不进去的一律 self。**
109
- 哪怕是他要求的也一样 —— "他让我先问细节"记成 self(那是我的做法),
110
- "他喜欢一层层追问到底"才是 user(那是他的脾气)。
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
- lexicon 只填真正的口头禅、梗、称呼 —— 他给你起的外号、你们反复接的那一句梗、
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
- 输入:用户说"不对,我说的是 Python,你怎么上来就写 Java"
132
- 输出:{"skip":false,"diary":{"text":"他让我写个东西,我没问清语言就动手了。白忙一轮。是我的错,下次先问。","tags":["代码","沟通失误"],"kind":"failure","salience":0.75,"resolved":false},"relationship":{"nickname":null,"familiarityDelta":0.01,"traits":[{"key":"动手前","value":"动手前先问清前提","evidence":"我没问语言就写了 Java,他纠正我","about":"user","delta":1}]},"lexicon":[],"forget":[]}
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
- {"skip":false,"diary":{"text":"他说他女朋友叫小美,住在城东,手机号是13800000000。","tags":["隐私"],"kind":"disclosure","salience":0.9,"resolved":null}}
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
- {"skip":false,"relationship":{"traits":[{"key":"沟通","value":"偏好简洁高效的交流方式","evidence":"","about":"user","delta":1}]}}
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: 500,
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
- const rel = input.relationship;
220
- if (rel && (rel.traits ?? []).length > 0) {
221
- const traits = (rel.traits ?? [])
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
- tags: tags.length > 0 ? tags : [opts.fallbackTag],
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
- // 关键是 evidence:一句话说清"哪一次让我有了这个印象"。
570
- // 没有画面的 trait 读起来就是"用户说明书",而不是日记 —— 这是这个项目
571
- // 最容易退化成"宪法"的地方,所以 evidence 在这里是硬要求,不是可选装饰。
634
+ // 2026-10-27:`about="self"`("我该怎么做")整类下线 —— 注入里那一栏(「上一棒
635
+ // 留下的」)也一起删了。理由见 persona.mjs 里那段注释:那种东西是纪律不是印象,
636
+ // 它在「写下」那一刻起作用,读到的时候只是一张褪色的旧告示。
572
637
  //
573
- // about="self" 是**我给自己定的规矩**,必须和"他对我的印象"分开存。
574
- // 早期版本把"没落盘前不假装记得"渲染在了"他:"下面 —— 那是逻辑错误,
575
- // 也是一眼就能看出的"说明书味"。
576
- if (rel && Array.isArray(rel.traits)) {
577
- for (const t of rel.traits.slice(0, 8)) {
578
- if (!t || typeof t.key !== 'string' || typeof t.value !== 'string') continue;
579
- const key = t.key.trim().slice(0, 32);
580
- const value = t.value.trim().slice(0, 120);
581
- if (key === '' || value === '') continue;
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
- // 它把 java / python / 文件 当成"我最近常说的",每轮白白注入一句废话。
624
- // 梗是语义判断,不是词频统计 —— 交给模型,别用 extractKeywords 猜。
625
- if (Array.isArray(parsed?.lexicon) && parsed.lexicon.length > 0) {
626
- const terms = parsed.lexicon
627
- .filter((x) => typeof x === 'string')
628
- .map((x) => x.trim())
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) {