sage-mem 0.9.13 → 0.9.15
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 +4 -3
- package/lib/client.js +155 -92
- package/lib/index.js +72 -6
- package/lib/memory/bm25.js +208 -0
- package/lib/memory/chunks.js +217 -0
- package/lib/memory/search.js +401 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -159,9 +159,10 @@ SAGE_MEM_DIR=/path/to/your/memory
|
|
|
159
159
|
|
|
160
160
|
透明的东西值得说清楚它怎么选料,因为它直接决定注入多少 token:
|
|
161
161
|
|
|
162
|
-
-
|
|
163
|
-
-
|
|
164
|
-
-
|
|
162
|
+
- **两段式检索:先摘要,再正文**。第一段用 `description`(「一句话说清这条是什么」)+ 文件名 + `aliases`(别称)打分,最多取 **10 条**候选;第二段对这些候选做**正文段落检索**(BM25,中文按双字词元),把最相关的那一段注入。旧版拿问题里的双字去全文里蒙,长记忆天生占便宜——实测长记忆在全文匹配下连寒暄问句都能注入上千 token。新版比的是**段落**,每份文件最多贡献一段,长文不再靠段数多占候选位
|
|
163
|
+
- **摘要没写到的,正文能找回来**。摘要是精准的一句话,代价是正文细节天生检索不到;摘要一条都没命中时改由正文检索兜底——此前这种情况直接等于「没有记忆可用」。正文检索只在问题**至少 4 个字符**时才跑:`继续` 这种两个字的问题拿去做正文匹配,命中的全是「继续」这个词本身
|
|
164
|
+
- **换个说法也能命中**:打分是**词面重合**,不是语义的——问「我有什么待办」而文件里写的是「代办清单」,差一个字就完全不命中。给这类记忆加一行 `aliases: [待办, todo]` 就能补上
|
|
165
|
+
- **注入的是命中段落,不是文件开头**。命中段落标成 `### 项目_部署.md:40-52`,agent 据此能直接跳到那几行核对来源;段落前补一句该记忆的 `description`,补回「这段是从哪条记忆切出来的」。`baseline` 与没命中正文的文件仍注入开头。单条正文超 1500 字符截断并标 `…(截断)`
|
|
165
166
|
- **同一个问题不会重复注入**(按会话记签名),问第二遍不会又追加一遍
|
|
166
167
|
- **单场会话累计注入到 60 KB 就停**(`MAX_SESSION_BYTES`)。条数上限管的是「一次注多少」,这条管「一场会话总共注多少」—— 长会话里问题一直在换、签名一直在变,累积本来没有上限。到顶之后新的记忆不再注入(这时该让 agent 直接读文件),**压缩或清空之后账面自动归零**,又能注进来
|
|
167
168
|
- **每条记忆的标题带新鲜度**:`(保存于 N 天前)`,**优先读 frontmatter 的 `updated:`,缺失才回落文件 mtime** —— 手写的时间戳是权威,文件系统时间会被复制 / 备份还原 / checkout 整批改掉。理由是陈旧记忆被当事实引用时,「引用」这个动作本身会让它显得更权威而不是更可疑,所以注入文本开头还带一节**引用纪律**(提到路径先确认存在、提到函数先 grep、要动手先核实)。它**独立成节**而不是一条 bullet:Claude Code 实测同一句话埋成 bullet 命中 0/3、独立成节 3/3
|
package/lib/client.js
CHANGED
|
@@ -112,13 +112,55 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
112
112
|
// 配色:五个色若取高饱和高亮度(#ffd27d / #6fe3ff / #b18cff / #7dffb0),
|
|
113
113
|
// 配上星芒与脉冲环,整体读起来像贴纸。现在统一降饱和、压亮度 ——
|
|
114
114
|
// 深空画布上够辨认,又不抢眼。
|
|
115
|
+
//
|
|
116
|
+
// 两档取值:`color` = 深色档(夜空画布),`light` = 浅色档(纸本设色)。
|
|
117
|
+
// 浅色档不用「深色档降亮度」那种做法 —— 高饱和冷色压在暖纸上会发脏;
|
|
118
|
+
// 换成明度接近的矿物颜料色(赭石/石青/紫墨/石绿/墨),色相错开才分得清类型。
|
|
115
119
|
const KIND_META = {
|
|
116
|
-
user: { label: "用户", color: "#e0c07a" },
|
|
117
|
-
feedback: { label: "反馈", color: "#7ec8da" },
|
|
118
|
-
project: { label: "项目", color: "#ac9edc" },
|
|
119
|
-
reference: { label: "参考", color: "#92ceaa" },
|
|
120
|
-
special: { label: "特殊", color: "#bcc3d0" },
|
|
120
|
+
user: { label: "用户", color: "#e0c07a", light: "#a4652c" },
|
|
121
|
+
feedback: { label: "反馈", color: "#7ec8da", light: "#3a6f8f" },
|
|
122
|
+
project: { label: "项目", color: "#ac9edc", light: "#6a5b8f" },
|
|
123
|
+
reference: { label: "参考", color: "#92ceaa", light: "#4a7c63" },
|
|
124
|
+
special: { label: "特殊", color: "#bcc3d0", light: "#5b5750" },
|
|
121
125
|
};
|
|
126
|
+
/**
|
|
127
|
+
* 宿主主题挂钩 —— 与 ui-theme 同一处:body[data-ds-dark-theme] 存在即深色档。
|
|
128
|
+
*/
|
|
129
|
+
function isDarkTheme() {
|
|
130
|
+
return typeof document !== "undefined" && !!document.body
|
|
131
|
+
&& document.body.hasAttribute("data-ds-dark-theme");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** 类型色按当前主题取(meta.color / meta.light)。 */
|
|
135
|
+
function kindColor(meta) {
|
|
136
|
+
const m = meta || KIND_META.special;
|
|
137
|
+
return (isDarkTheme() ? m.color : m.light) || m.color;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 画布调色板:深色档 = 夜空,浅色档 = 暖纸 + 晕染。
|
|
142
|
+
* 两套渲染器(WebGL / canvas2d)都从这里取色,别各写一份。
|
|
143
|
+
* bgGl 是 WebGL shader 用的 0-1 分量(与 bg2d 是同一色,写法不同而已)。
|
|
144
|
+
*/
|
|
145
|
+
function starmapPalette() {
|
|
146
|
+
if (isDarkTheme()) {
|
|
147
|
+
return {
|
|
148
|
+
bg2d: ["#0b1026", "#101731"],
|
|
149
|
+
bgGl: [[0.105, 0.117, 0.145], [0.043, 0.050, 0.065]],
|
|
150
|
+
dust: "#9fb3dd",
|
|
151
|
+
archive: [0.46, 0.50, 0.58],
|
|
152
|
+
vignette: null,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
bg2d: ["#faf6ed", "#e9e0cd"],
|
|
157
|
+
bgGl: [[0.980, 0.965, 0.929], [0.914, 0.878, 0.804]],
|
|
158
|
+
dust: "#c9bfa6",
|
|
159
|
+
archive: [0.62, 0.60, 0.55],
|
|
160
|
+
vignette: { color: "#7a6a48", a: 0.10 },
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
122
164
|
const KIND_ORDER = ["user", "feedback", "project", "reference", "special"];
|
|
123
165
|
const FRESH_MS = 7 * 86400000;
|
|
124
166
|
|
|
@@ -370,79 +412,71 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
370
412
|
// ── 样式(data-plugin-css 约定,幂等注入)──
|
|
371
413
|
const CSS = [
|
|
372
414
|
".smap-footer-btn{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--dsw-alias-border-l1,transparent);background:transparent;color:var(--dsw-alias-label-secondary,inherit);border-radius:8px;padding:4px 10px;font-size:12px;cursor:pointer;font-family:inherit;white-space:nowrap;}",
|
|
373
|
-
".smap-fab{position:fixed;right:0;top:15%;transform:translateY(-50%);z-index:900;width:36px;height:64px;border-radius:12px 0 0 12px;border:1px solid
|
|
374
|
-
".smap-fab:hover{opacity:1;width:46px;background:
|
|
415
|
+
".smap-fab{position:fixed;right:0;top:15%;transform:translateY(-50%);z-index:900;width:36px;height:64px;border-radius:12px 0 0 12px;border:1px solid var(--dsw-alias-border-l1);border-right:none;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-brand-primary);font-size:17px;cursor:pointer;display:flex;align-items:center;justify-content:center;opacity:.5;pointer-events:auto;transition:opacity .15s,background .15s,width .15s;box-shadow:-4px 0 18px rgba(0,0,0,.35);font-family:inherit;}",
|
|
416
|
+
".smap-fab:hover{opacity:1;width:46px;background:var(--dsw-alias-bg-layer-1);}",
|
|
375
417
|
".smap-footer-btn:hover{color:var(--dsw-alias-label-primary,inherit);border-color:var(--dsw-alias-border-l2,#8886);}",
|
|
376
418
|
".smap-footer-icon{font-size:13px;line-height:1;}",
|
|
377
|
-
".smap-veil{position:fixed;inset:0;z-index:1000;background:rgba(
|
|
378
|
-
".smap-modal{width:min(980px,94vw);height:min(680px,92vh);background:
|
|
419
|
+
".smap-veil{position:fixed;inset:0;z-index:1000;background:var(--dsw-alias-bg-mask-1,rgba(0,0,0,.45));backdrop-filter:blur(3px);display:flex;align-items:center;justify-content:center;pointer-events:auto;animation:smap-fade-in .18s ease-out;}",
|
|
420
|
+
".smap-modal{width:min(980px,94vw);height:min(680px,92vh);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:16px;box-shadow:0 24px 80px rgba(0,0,0,.55);padding:16px 18px;display:flex;flex-direction:column;overflow:hidden;pointer-events:auto;animation:smap-pop-in .22s cubic-bezier(.2,.9,.3,1.15);}",
|
|
379
421
|
"@keyframes smap-fade-in{from{opacity:0}to{opacity:1}}",
|
|
380
422
|
"@keyframes smap-pop-in{from{opacity:0;transform:scale(.96) translateY(10px)}to{opacity:1;transform:none}}",
|
|
381
|
-
|
|
382
|
-
//
|
|
383
|
-
// 不这么做的下场:宿主切到**浅色主题**后,这个组件里硬编码的
|
|
384
|
-
// 浅色文字(#dbe4f5 系)落在白色卡片上,标题、搜索框、图例几乎看不见 ——
|
|
385
|
-
// 一片「重影」。星图本来就不打算跟随宿主主题(它是一张夜空),
|
|
386
|
-
// 那就把边界画清楚:整块深色,而不是只有中间那半块。
|
|
387
|
-
".smap-card{background:#0c0e12;border-radius:10px;padding:12px 14px;color:#e3e7ef;display:flex;flex-direction:column;flex:1;min-height:0;}",
|
|
423
|
+
".smap-card{background:var(--dsw-alias-bg-layer-1);border-radius:10px;padding:12px 14px;color:var(--dsw-alias-label-primary);display:flex;flex-direction:column;flex:1;min-height:0;}",
|
|
388
424
|
".smap-head{display:flex;align-items:center;gap:10px;margin-bottom:10px;flex-wrap:wrap;}",
|
|
389
425
|
".smap-title{font-size:15px;font-weight:600;letter-spacing:.06em;}",
|
|
390
|
-
".smap-sub{font-size:11px;
|
|
426
|
+
".smap-sub{font-size:11px;color:var(--dsw-alias-label-secondary);}",
|
|
391
427
|
".smap-spacer{flex:1;}",
|
|
392
|
-
".smap-search{background:
|
|
393
|
-
".smap-search:focus{border-color:
|
|
394
|
-
".smap-search::placeholder{color:
|
|
395
|
-
".smap-btn{background:
|
|
396
|
-
".smap-btn:hover{background:
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
".smap-btn.on{border-color:rgba(150,170,230,.78);background:rgba(122,158,255,.20);color:#eef3ff;}",
|
|
400
|
-
'.smap-ar-toggle[aria-pressed="false"]{opacity:.60;}',
|
|
428
|
+
".smap-search{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;color:var(--dsw-alias-label-primary);font-size:12px;padding:4px 9px;outline:none;width:160px;font-family:inherit;}",
|
|
429
|
+
".smap-search:focus{border-color:var(--dsw-alias-border-l2);}",
|
|
430
|
+
".smap-search::placeholder{color:var(--dsw-alias-label-tertiary);}",
|
|
431
|
+
".smap-btn{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;color:var(--dsw-alias-label-primary);font-size:12px;padding:4px 10px;cursor:pointer;font-family:inherit;}",
|
|
432
|
+
".smap-btn:hover{background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-layer-2));}",
|
|
433
|
+
".smap-btn.on{border-color:var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-label-primary);}",
|
|
434
|
+
".smap-ar-toggle[aria-pressed=\"false\"]{opacity:.60;}",
|
|
401
435
|
".smap-btn:disabled{opacity:.5;cursor:default;}",
|
|
402
|
-
".smap-btn-primary{border-color:
|
|
436
|
+
".smap-btn-primary{border-color:var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-layer-2));}",
|
|
403
437
|
".smap-wrap{position:relative;flex:1;min-height:0;}",
|
|
404
438
|
".smap-canvas{position:absolute;inset:0;width:100%;height:100%;display:block;border-radius:10px;cursor:crosshair;}",
|
|
405
|
-
".smap-tip{position:absolute;z-index:5;pointer-events:none;max-width:280px;background:
|
|
439
|
+
".smap-tip{position:absolute;z-index:5;pointer-events:none;max-width:280px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:8px 10px;font-size:12px;line-height:1.55;box-shadow:0 6px 24px rgba(0,0,0,.45);}",
|
|
406
440
|
".smap-tip-title{font-weight:600;margin-bottom:2px;}",
|
|
407
|
-
".smap-tip-kind{
|
|
408
|
-
".smap-tip-desc{
|
|
409
|
-
".smap-panel{position:absolute;top:10px;right:10px;bottom:10px;width:min(400px,54%);background:
|
|
441
|
+
".smap-tip-kind{color:var(--dsw-alias-label-secondary);font-size:11px;margin-bottom:2px;}",
|
|
442
|
+
".smap-tip-desc{color:var(--dsw-alias-label-secondary);}",
|
|
443
|
+
".smap-panel{position:absolute;top:10px;right:10px;bottom:10px;width:min(400px,54%);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:12px 14px;overflow-y:auto;z-index:6;font-size:13px;line-height:1.7;box-shadow:-8px 0 30px rgba(0,0,0,.35);}",
|
|
410
444
|
".smap-panel-head{display:flex;align-items:flex-start;gap:8px;margin-bottom:4px;}",
|
|
411
445
|
".smap-panel-title{font-weight:600;font-size:14px;flex:1;}",
|
|
412
|
-
".smap-panel-meta{font-size:11px;
|
|
413
|
-
".smap-panel-arch{display:flex;flex-direction:column;gap:6px;border:1px solid
|
|
446
|
+
".smap-panel-meta{font-size:11px;color:var(--dsw-alias-label-secondary);margin-bottom:8px;}",
|
|
447
|
+
".smap-panel-arch{display:flex;flex-direction:column;gap:6px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:8px 10px;background:var(--dsw-alias-bg-layer-2);}",
|
|
414
448
|
".smap-meta-dot{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:4px;vertical-align:-1px;}",
|
|
415
449
|
".smap-legend{display:flex;flex-wrap:wrap;gap:14px;margin-top:10px;font-size:12px;align-items:center;}",
|
|
416
450
|
".smap-lg-item{cursor:pointer;user-select:none;}",
|
|
417
451
|
".smap-lg-item.off{opacity:.32;}",
|
|
418
452
|
".smap-dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px;vertical-align:-1px;}",
|
|
419
|
-
".smap-count{margin-left:auto;
|
|
420
|
-
".smap-status{padding:28px;text-align:center;
|
|
421
|
-
".smap-empty{position:absolute;left:0;right:0;top:46%;text-align:center;color:
|
|
422
|
-
".smap-replay-on{border-color:
|
|
423
|
-
".smap-replay-date{font-size:11px;
|
|
453
|
+
".smap-count{margin-left:auto;color:var(--dsw-alias-label-secondary);font-size:11px;}",
|
|
454
|
+
".smap-status{padding:28px;text-align:center;color:var(--dsw-alias-label-secondary);font-size:13px;}",
|
|
455
|
+
".smap-empty{position:absolute;left:0;right:0;top:46%;text-align:center;color:var(--dsw-alias-label-tertiary);font-size:13px;pointer-events:none;z-index:4;}",
|
|
456
|
+
".smap-replay-on{border-color:var(--dsw-alias-brand-primary)!important;color:var(--dsw-alias-brand-primary)!important;}",
|
|
457
|
+
".smap-replay-date{font-size:11px;color:var(--dsw-alias-brand-primary);font-variant-numeric:tabular-nums;}",
|
|
424
458
|
".smap-md-h{font-weight:600;margin:12px 0 4px;}",
|
|
425
459
|
".smap-md-h1{font-size:16px;}",
|
|
426
460
|
".smap-md-h2{font-size:14.5px;}",
|
|
427
461
|
".smap-md-h3,.smap-md-h4{font-size:13.5px;opacity:.92;}",
|
|
428
462
|
".smap-md-p{margin:4px 0;}",
|
|
429
463
|
".smap-md-list{margin:4px 0;padding-left:20px;}",
|
|
430
|
-
".smap-md-code{background:
|
|
431
|
-
".smap-md-pre{background:
|
|
432
|
-
".smap-md-quote{border-left:3px solid
|
|
433
|
-
".smap-md-wiki{color
|
|
434
|
-
".smap-md-hr{border:none;border-top:1px solid
|
|
435
|
-
".smap-viewtoggle{display:inline-flex;border:1px solid
|
|
436
|
-
".smap-vt-btn{background:transparent;border:none;color:
|
|
437
|
-
".smap-vt-btn.on{background:
|
|
464
|
+
".smap-md-code{background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-layer-2));padding:1px 5px;border-radius:4px;font-size:12px;}",
|
|
465
|
+
".smap-md-pre{background:var(--dsw-alias-bg-layer-2);padding:8px 10px;border-radius:8px;font-size:12px;overflow-x:auto;white-space:pre-wrap;margin:6px 0;}",
|
|
466
|
+
".smap-md-quote{border-left:3px solid var(--dsw-alias-border-l2);margin:6px 0;padding:2px 10px;color:var(--dsw-alias-label-secondary);}",
|
|
467
|
+
".smap-md-wiki{color:var(--dsw-alias-brand-primary);}",
|
|
468
|
+
".smap-md-hr{border:none;border-top:1px solid var(--dsw-alias-border-l1);margin:10px 0;}",
|
|
469
|
+
".smap-viewtoggle{display:inline-flex;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;overflow:hidden;}",
|
|
470
|
+
".smap-vt-btn{background:transparent;border:none;color:var(--dsw-alias-label-secondary);font-size:12px;padding:4px 10px;cursor:pointer;font-family:inherit;}",
|
|
471
|
+
".smap-vt-btn.on{background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-label-primary);}",
|
|
438
472
|
".smap-timeline{position:absolute;inset:0;overflow-y:auto;display:flex;flex-direction:column;gap:6px;padding:4px 6px 4px 2px;}",
|
|
439
|
-
".smap-tl-item{display:flex;align-items:baseline;gap:8px;background:
|
|
440
|
-
".smap-tl-item:hover{background:
|
|
473
|
+
".smap-tl-item{display:flex;align-items:baseline;gap:8px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:7px 10px;cursor:pointer;flex:none;}",
|
|
474
|
+
".smap-tl-item:hover{background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-layer-2));border-color:var(--dsw-alias-border-l1);}",
|
|
441
475
|
".smap-tl-item--arch{opacity:.75;}",
|
|
442
|
-
".smap-ar-badge{font-size:10.5px;
|
|
443
|
-
".smap-tl-date{font-size:11px;
|
|
476
|
+
".smap-ar-badge{font-size:10.5px;color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l1);border-radius:5px;padding:0 5px;flex:none;align-self:center;}",
|
|
477
|
+
".smap-tl-date{font-size:11px;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;flex:none;}",
|
|
444
478
|
".smap-tl-title{font-size:13px;font-weight:600;flex:none;max-width:40%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
|
|
445
|
-
".smap-tl-desc{font-size:12px;
|
|
479
|
+
".smap-tl-desc{font-size:12px;color:var(--dsw-alias-label-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;}",
|
|
446
480
|
].join("\n");
|
|
447
481
|
|
|
448
482
|
function injectCss() {
|
|
@@ -463,14 +497,15 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
463
497
|
}
|
|
464
498
|
|
|
465
499
|
// 归档星(暗星)的用色:把类型色往石板灰上压 —— 它得「还在图上、一眼数得出来」,
|
|
466
|
-
// 又要一眼看出不是活记忆。亮度靠 shader 那一档(0.52
|
|
467
|
-
|
|
500
|
+
// 又要一眼看出不是活记忆。亮度靠 shader 那一档(0.52),这里负责色相:
|
|
501
|
+
// 深色档是冷石板灰;浅色档换成暖灰,否则灰点落在纸上看不见。
|
|
468
502
|
function dimRgb(hex) {
|
|
469
503
|
const c = hexToRgb(hex);
|
|
504
|
+
const g = starmapPalette().archive;
|
|
470
505
|
return [
|
|
471
|
-
c[0] * 0.26 +
|
|
472
|
-
c[1] * 0.26 +
|
|
473
|
-
c[2] * 0.26 +
|
|
506
|
+
c[0] * 0.26 + g[0] * 0.74,
|
|
507
|
+
c[1] * 0.26 + g[1] * 0.74,
|
|
508
|
+
c[2] * 0.26 + g[2] * 0.74,
|
|
474
509
|
];
|
|
475
510
|
}
|
|
476
511
|
function dimCss(hex) {
|
|
@@ -483,17 +518,21 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
483
518
|
// 「会呼吸的彩色底」正是廉价感的一大来源,而且每帧都要重算噪声。
|
|
484
519
|
// 现在只剩一条静态径向渐变:中心略亮的中性深灰,边缘更暗。uTime 保留声明
|
|
485
520
|
// 只为让 uniform 列表不变(用不到时 getUniformLocation 返回 null,调用是安全空操作)。
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
521
|
+
// 底色由调色板给(深色档=夜空深灰,浅色档=暖纸),所以是函数不是常量。
|
|
522
|
+
function smapBgFs(inner, outer) {
|
|
523
|
+
const v = (c) => c.map((x) => x.toFixed(3)).join(", ");
|
|
524
|
+
return [
|
|
525
|
+
"precision highp float;",
|
|
526
|
+
"uniform vec2 uRes;",
|
|
527
|
+
"uniform float uTime;",
|
|
528
|
+
"void main(){",
|
|
529
|
+
" vec2 suv = gl_FragCoord.xy / uRes;",
|
|
530
|
+
" float r = length((suv - vec2(0.5, 0.44)) * vec2(1.0, 1.28));",
|
|
531
|
+
" vec3 col = mix(vec3(" + v(inner) + "), vec3(" + v(outer) + "), clamp(r * 1.30, 0.0, 1.0));",
|
|
532
|
+
" gl_FragColor = vec4(col, 1.0);",
|
|
533
|
+
"}",
|
|
534
|
+
].join("\n");
|
|
535
|
+
}
|
|
497
536
|
|
|
498
537
|
const SMAP_STAR_VS = [
|
|
499
538
|
"attribute vec2 aCorner;",
|
|
@@ -642,7 +681,8 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
642
681
|
canvasEl.width = W * 2;
|
|
643
682
|
canvasEl.height = H * 2;
|
|
644
683
|
gl.viewport(0, 0, canvasEl.width, canvasEl.height);
|
|
645
|
-
const
|
|
684
|
+
const pal = starmapPalette();
|
|
685
|
+
const progBg = glProgram(gl, SMAP_BG_VS, smapBgFs(pal.bgGl[0], pal.bgGl[1]), ["aPos"], ["uRes", "uTime"]);
|
|
646
686
|
const progStar = glProgram(gl, SMAP_STAR_VS, SMAP_STAR_FS, ["aCorner", "aMeta0", "aColor", "aMeta1", "aBirth"], ["uRes", "uTime", "uHoverIdx", "uRevealT"]);
|
|
647
687
|
const progLine = glProgram(gl, SMAP_LINE_VS, SMAP_LINE_FS, ["aPos", "aColor", "aBirth"], ["uRes", "uRevealT"]);
|
|
648
688
|
|
|
@@ -660,7 +700,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
660
700
|
const x = rand01(i * 97 + 13) * W;
|
|
661
701
|
const y = rand01(i * 31 + 7) * H;
|
|
662
702
|
const r = 2.4 + rand01(i * 17 + 5) * 2.0;
|
|
663
|
-
pushStarQuad(arr, x, y, r,
|
|
703
|
+
pushStarQuad(arr, x, y, r, hexToRgb(pal.dust), 0.10 + 0.30 * rand01(i * 7 + 3), rand01(i * 11 + 29), 0, -1, -1);
|
|
664
704
|
}
|
|
665
705
|
gl.bindBuffer(gl.ARRAY_BUFFER, dustBuf);
|
|
666
706
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(arr), gl.STATIC_DRAW);
|
|
@@ -691,10 +731,10 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
691
731
|
// 负责「深空底上一眼数得出来」——只画一个小点是数不出来的;
|
|
692
732
|
// ② 一颗小核心(走普通分支,fresh 传 -1 = 归档档 0.52),负责
|
|
693
733
|
// 「看得出是颗星」,并且 hover 时整颗点亮 + 出环。
|
|
694
|
-
pushStarQuad(arr, p.x, p.y, p.r * 6.0, dimRgb(meta
|
|
695
|
-
pushStarQuad(arr, p.x, p.y, p.r * 4.4, dimRgb(meta
|
|
734
|
+
pushStarQuad(arr, p.x, p.y, p.r * 6.0, dimRgb(kindColor(meta)), 0.26, p.tw, 0, -1, birth);
|
|
735
|
+
pushStarQuad(arr, p.x, p.y, p.r * 4.4, dimRgb(kindColor(meta)), 1, p.tw, -1, i, birth);
|
|
696
736
|
} else {
|
|
697
|
-
pushStarQuad(arr, p.x, p.y, p.r * 5.4, hexToRgb(meta
|
|
737
|
+
pushStarQuad(arr, p.x, p.y, p.r * 5.4, hexToRgb(kindColor(meta)), 1, p.tw, fresh, i, birth);
|
|
698
738
|
}
|
|
699
739
|
}
|
|
700
740
|
gl.bindBuffer(gl.ARRAY_BUFFER, starBuf);
|
|
@@ -709,7 +749,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
709
749
|
const ln = visBox.lines[li];
|
|
710
750
|
const a = layout.pts[ln[0]];
|
|
711
751
|
const b = layout.pts[ln[1]];
|
|
712
|
-
const c = hexToRgb((KIND_META[ln[2]] || KIND_META.special)
|
|
752
|
+
const c = hexToRgb(kindColor(KIND_META[ln[2]] || KIND_META.special));
|
|
713
753
|
const hot = hoverLineIdx >= 0 && (ln[0] === hoverLineIdx || ln[1] === hoverLineIdx);
|
|
714
754
|
const mul = hot ? 1 : 0.72;
|
|
715
755
|
const al = hot ? 0.6 : 0.22;
|
|
@@ -801,16 +841,25 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
801
841
|
const c2d = canvasEl.getContext("2d");
|
|
802
842
|
if (!c2d) return;
|
|
803
843
|
c2d.setTransform(SCALE, 0, 0, SCALE, 0, 0);
|
|
844
|
+
const pal = starmapPalette();
|
|
804
845
|
const bg = c2d.createLinearGradient(0, 0, 0, H);
|
|
805
|
-
bg.addColorStop(0,
|
|
806
|
-
bg.addColorStop(1,
|
|
846
|
+
bg.addColorStop(0, pal.bg2d[0]);
|
|
847
|
+
bg.addColorStop(1, pal.bg2d[1]);
|
|
807
848
|
c2d.fillStyle = bg;
|
|
808
849
|
c2d.fillRect(0, 0, W, H);
|
|
850
|
+
// 晕染:中心透、边缘略深(浅色档才要,深色档的夜空渐变已经够了)
|
|
851
|
+
if (pal.vignette) {
|
|
852
|
+
const vg = c2d.createRadialGradient(W * 0.5, H * 0.46, 0, W * 0.5, H * 0.46, Math.max(W, H) * 0.72);
|
|
853
|
+
vg.addColorStop(0, hexA(pal.vignette.color, 0));
|
|
854
|
+
vg.addColorStop(1, hexA(pal.vignette.color, pal.vignette.a));
|
|
855
|
+
c2d.fillStyle = vg;
|
|
856
|
+
c2d.fillRect(0, 0, W, H);
|
|
857
|
+
}
|
|
809
858
|
for (let i = 0; i < 150; i++) {
|
|
810
859
|
const x = rand01(i * 97 + 13) * W;
|
|
811
860
|
const y = rand01(i * 31 + 7) * H;
|
|
812
861
|
c2d.globalAlpha = 0.08 + 0.22 * rand01(i * 7 + 3);
|
|
813
|
-
c2d.fillStyle =
|
|
862
|
+
c2d.fillStyle = pal.dust;
|
|
814
863
|
c2d.fillRect(x, y, 1, 1);
|
|
815
864
|
}
|
|
816
865
|
c2d.globalAlpha = 1;
|
|
@@ -827,7 +876,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
827
876
|
lrv = lt * lt * (3 - 2 * lt);
|
|
828
877
|
if (lrv <= 0) continue;
|
|
829
878
|
}
|
|
830
|
-
c2d.strokeStyle = hexA(meta
|
|
879
|
+
c2d.strokeStyle = hexA(kindColor(meta), (hot ? 0.6 : 0.22) * lrv);
|
|
831
880
|
c2d.lineWidth = 1;
|
|
832
881
|
c2d.beginPath();
|
|
833
882
|
c2d.moveTo(a.x, a.y);
|
|
@@ -849,7 +898,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
849
898
|
// 暗星(canvas2d 兜底路径):与 webgl 同一档 —— 核心小而暗、外面一圈很淡的冷辉光。
|
|
850
899
|
// 有辉光才「数得出来」;hover 时整颗点亮 + 描边环,这是它能被发现的关键。
|
|
851
900
|
const hot = hoverBox.idx === i;
|
|
852
|
-
const dc = dimRgb(meta
|
|
901
|
+
const dc = dimRgb(kindColor(meta));
|
|
853
902
|
const rgba = (a) => "rgba(" + Math.round(dc[0] * 255) + "," + Math.round(dc[1] * 255) + "," + Math.round(dc[2] * 255) + "," + a.toFixed(3) + ")";
|
|
854
903
|
const haloR = p.r * 3.4;
|
|
855
904
|
const hg = c2d.createRadialGradient(p.x, p.y, 0, p.x, p.y, haloR);
|
|
@@ -860,13 +909,13 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
860
909
|
c2d.arc(p.x, p.y, haloR, 0, Math.PI * 2);
|
|
861
910
|
c2d.fill();
|
|
862
911
|
c2d.globalAlpha = (hot ? 0.95 : 0.52) * rv;
|
|
863
|
-
c2d.fillStyle = dimCss(meta
|
|
912
|
+
c2d.fillStyle = dimCss(kindColor(meta));
|
|
864
913
|
c2d.beginPath();
|
|
865
914
|
c2d.arc(p.x, p.y, Math.max(1.2, p.r * 0.7), 0, Math.PI * 2);
|
|
866
915
|
c2d.fill();
|
|
867
916
|
c2d.globalAlpha = 1;
|
|
868
917
|
if (hot) {
|
|
869
|
-
c2d.strokeStyle = dimCss(meta
|
|
918
|
+
c2d.strokeStyle = dimCss(kindColor(meta));
|
|
870
919
|
c2d.lineWidth = 1;
|
|
871
920
|
c2d.beginPath();
|
|
872
921
|
c2d.arc(p.x, p.y, p.r + 5, 0, Math.PI * 2);
|
|
@@ -876,7 +925,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
876
925
|
}
|
|
877
926
|
if (p.star.mtimeMs > 0 && nowMs - p.star.mtimeMs < FRESH_MS) {
|
|
878
927
|
const pr = p.r + 5 + 3 * (0.5 + 0.5 * Math.sin(phase * 1.4 + p.tw * 6.28));
|
|
879
|
-
c2d.strokeStyle = hexA(meta
|
|
928
|
+
c2d.strokeStyle = hexA(kindColor(meta), 0.4 * rv);
|
|
880
929
|
c2d.lineWidth = 1;
|
|
881
930
|
c2d.beginPath();
|
|
882
931
|
c2d.arc(p.x, p.y, pr, 0, Math.PI * 2);
|
|
@@ -884,20 +933,20 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
884
933
|
}
|
|
885
934
|
const glowR = p.r * 4.2;
|
|
886
935
|
const g = c2d.createRadialGradient(p.x, p.y, 0, p.x, p.y, glowR);
|
|
887
|
-
g.addColorStop(0, hexA(meta
|
|
888
|
-
g.addColorStop(1, hexA(meta
|
|
936
|
+
g.addColorStop(0, hexA(kindColor(meta), 0.5 * tw * rv));
|
|
937
|
+
g.addColorStop(1, hexA(kindColor(meta), 0));
|
|
889
938
|
c2d.fillStyle = g;
|
|
890
939
|
c2d.beginPath();
|
|
891
940
|
c2d.arc(p.x, p.y, glowR, 0, Math.PI * 2);
|
|
892
941
|
c2d.fill();
|
|
893
942
|
c2d.globalAlpha = (0.62 + 0.38 * tw) * rv;
|
|
894
|
-
c2d.fillStyle = meta
|
|
943
|
+
c2d.fillStyle = kindColor(meta);
|
|
895
944
|
c2d.beginPath();
|
|
896
945
|
c2d.arc(p.x, p.y, p.r, 0, Math.PI * 2);
|
|
897
946
|
c2d.fill();
|
|
898
947
|
c2d.globalAlpha = 1;
|
|
899
948
|
if (hoverBox.idx === i) {
|
|
900
|
-
c2d.strokeStyle = hexA(meta
|
|
949
|
+
c2d.strokeStyle = hexA(kindColor(meta), 0.9 * rv);
|
|
901
950
|
c2d.lineWidth = 1;
|
|
902
951
|
c2d.beginPath();
|
|
903
952
|
c2d.arc(p.x, p.y, p.r + 5, 0, Math.PI * 2);
|
|
@@ -978,6 +1027,20 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
978
1027
|
const drawBox = react.useRef({ renderer: null, t0: 0, markDirty: null }).current;
|
|
979
1028
|
const replayBox = react.useRef({ raf: 0, active: false }).current;
|
|
980
1029
|
|
|
1030
|
+
// 宿主主题:切主题要把画布重建一遍(底色/星点色都变了),所以它是个 state,
|
|
1031
|
+
// 并进渲染 effect 的依赖。挂 observer 而不是每次 render 读属性 —— 属性变了要主动重画。
|
|
1032
|
+
const themeKeyS = react.useState(isDarkTheme() ? "dark" : "light");
|
|
1033
|
+
const themeKey = themeKeyS[0];
|
|
1034
|
+
const setThemeKey = themeKeyS[1];
|
|
1035
|
+
react.useEffect(() => {
|
|
1036
|
+
if (typeof document === "undefined" || !document.body) return undefined;
|
|
1037
|
+
const sync = () => setThemeKey(isDarkTheme() ? "dark" : "light");
|
|
1038
|
+
sync();
|
|
1039
|
+
const mo = new MutationObserver(sync);
|
|
1040
|
+
mo.observe(document.body, { attributes: true, attributeFilter: ["data-ds-dark-theme"] });
|
|
1041
|
+
return () => mo.disconnect();
|
|
1042
|
+
}, []);
|
|
1043
|
+
|
|
981
1044
|
// Esc 分层:详情面板开着时先关面板(capture 阶段拦截),再按才落到 overlay 关星空。
|
|
982
1045
|
react.useEffect(() => {
|
|
983
1046
|
if (!selected) return undefined;
|
|
@@ -1077,7 +1140,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1077
1140
|
window.cancelAnimationFrame(replayBox.raf);
|
|
1078
1141
|
if (renderer.destroy) renderer.destroy();
|
|
1079
1142
|
};
|
|
1080
|
-
}, [data, layout, canvasEl]);
|
|
1143
|
+
}, [data, layout, canvasEl, themeKey]);
|
|
1081
1144
|
|
|
1082
1145
|
react.useEffect(() => {
|
|
1083
1146
|
if (!data || !layout || !kindsOn) return;
|
|
@@ -1285,7 +1348,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1285
1348
|
onClick: () => toggleKind(k),
|
|
1286
1349
|
title: "点击显示/隐藏这一类",
|
|
1287
1350
|
},
|
|
1288
|
-
h("span", { className: "smap-dot", style: { background: KIND_META[k]
|
|
1351
|
+
h("span", { className: "smap-dot", style: { background: kindColor(KIND_META[k]) } }),
|
|
1289
1352
|
KIND_META[k].label + " " + n));
|
|
1290
1353
|
}
|
|
1291
1354
|
let totalBytes = 0;
|
|
@@ -1327,7 +1390,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1327
1390
|
h("span", { className: "smap-panel-title" }, selected.star.title),
|
|
1328
1391
|
h("button", { className: "smap-btn", onClick: () => setSelected(null), title: "关闭" }, "×")),
|
|
1329
1392
|
h("div", { key: "meta", className: "smap-panel-meta" },
|
|
1330
|
-
h("span", { className: "smap-meta-dot", style: { background: meta
|
|
1393
|
+
h("span", { className: "smap-meta-dot", style: { background: kindColor(meta) } }),
|
|
1331
1394
|
meta.label + " · " + selected.star.file + " · " + fmtBytes(selected.star.bytes) +
|
|
1332
1395
|
// 归档星的 mtime 是「当年写下」的时间,跟「什么时候收起来的」不是一回事 ——
|
|
1333
1396
|
// 这里只留「已归档」,具体时间在下面的归档块里给。
|
|
@@ -1370,7 +1433,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1370
1433
|
onClick: () => openStar(s),
|
|
1371
1434
|
title: s.archived === true ? "归档案:点开看归档信息并恢复" : "展开全文",
|
|
1372
1435
|
},
|
|
1373
|
-
h("span", { className: "smap-meta-dot", style: { background: s.archived === true ? dimCss((KIND_META[s.kind] || KIND_META.special)
|
|
1436
|
+
h("span", { className: "smap-meta-dot", style: { background: s.archived === true ? dimCss(kindColor(KIND_META[s.kind] || KIND_META.special)) : kindColor(KIND_META[s.kind] || KIND_META.special), flex: "none", alignSelf: "center" } }),
|
|
1374
1437
|
s.archived === true ? h("span", { className: "smap-ar-badge" }, "已归档") : null,
|
|
1375
1438
|
h("span", { className: "smap-tl-date", title: s.mtimeMs ? fmtDate(s.mtimeMs) : "" }, s.mtimeMs ? fmtRel(s.mtimeMs) : "—"),
|
|
1376
1439
|
h("span", { className: "smap-tl-title" }, s.title),
|
|
@@ -2016,7 +2079,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2016
2079
|
// 而独立成屏后如果父容器不是 column flex,`align-items:stretch` 就不会发生。
|
|
2017
2080
|
".smem-tool-panel--full{width:100%;align-self:stretch;box-sizing:border-box;}",
|
|
2018
2081
|
// 已锁定的记忆:卡片上那个按钮要一眼看出「这条现在拒绝归档」,用警示色
|
|
2019
|
-
".smem-btn-locked{border-color:var(--dsw-alias-state-
|
|
2082
|
+
".smem-btn-locked{border-color:var(--dsw-alias-state-warn-primary,#d97706);color:var(--dsw-alias-state-warn-primary,#d97706);}",
|
|
2020
2083
|
// 「自动归档」页签里的天数输入框:窄一点,别让三个数字各占一整行
|
|
2021
2084
|
".smem-num{width:88px;}",
|
|
2022
2085
|
// 体检 / 候选是「翻清单」的面板:定高 + 内部滚动,列表不被顶出首屏。
|
|
@@ -2105,7 +2168,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2105
2168
|
".smem-ad-main{flex:1;min-width:140px;overflow-wrap:anywhere;}",
|
|
2106
2169
|
".smem-ad-sub{color:var(--dsw-alias-label-secondary);font-size:11.5px;overflow-wrap:anywhere;}",
|
|
2107
2170
|
".smem-ad-badge{display:inline-flex;align-items:center;font-size:11px;color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l1);border-radius:6px;padding:0 5px;white-space:nowrap;}",
|
|
2108
|
-
".smem-ad-badge--warn{color:var(--dsw-alias-state-
|
|
2171
|
+
".smem-ad-badge--warn{color:var(--dsw-alias-state-warn-primary,#d97706);border-color:var(--dsw-alias-state-warn-primary,#d97706);}",
|
|
2109
2172
|
".smem-ad-badge--legacy{opacity:.78;}",
|
|
2110
2173
|
".smem-ad-confirm{border:1px solid var(--dsw-alias-state-error-primary,#ef4444);border-radius:8px;padding:7px 9px;font-size:12px;line-height:1.55;display:flex;flex-direction:column;gap:6px;overflow-wrap:anywhere;}",
|
|
2111
2174
|
".smem-ad-rb{font-size:12px;line-height:1.5;overflow-wrap:anywhere;}",
|
|
@@ -2114,7 +2177,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2114
2177
|
'.smem-ad-run[data-on="1"]{border-color:var(--dsw-alias-brand-primary,#4d76e6);}',
|
|
2115
2178
|
".smem-ad-md{max-height:360px;overflow:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:10px 12px;background:var(--dsw-alias-bg-layer-2);font-size:12px;white-space:pre-wrap;overflow-wrap:anywhere;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;line-height:1.6;}",
|
|
2116
2179
|
// ── 注入参数 / 保留名名单(自动做梦 tab)──
|
|
2117
|
-
".smem-ad-warn{color:var(--dsw-alias-state-
|
|
2180
|
+
".smem-ad-warn{color:var(--dsw-alias-state-warn-primary,#d97706);}",
|
|
2118
2181
|
".smem-chip--fixed{opacity:.72;cursor:default;}",
|
|
2119
2182
|
".smem-chip-x{border:0;background:transparent;color:inherit;cursor:pointer;font-size:11px;font-family:inherit;padding:0 0 0 5px;}",
|
|
2120
2183
|
// ── 更新提示(有新版本才出现的一条窄横幅)──
|
package/lib/index.js
CHANGED
|
@@ -23,6 +23,7 @@ import { parseAliases, parseBaseline, parseBaselinePriority, parseFrontmatter, p
|
|
|
23
23
|
import { isReserved, nameKey, safeName } from './memory/naming.js'
|
|
24
24
|
import { ageDays, bigrams, extractText, score, selectBaseline, selectRelevant } from './memory/retrieval.js'
|
|
25
25
|
import { MEMORY_ARCHIVE_DIR, addArchiveMeta, extractTitle, inspectDir, inspectMemoryEntry, readArchivedMeta, scanArchived, scanMemoryFiles, scanStars, shadowsReserved } from './memory/scan.js'
|
|
26
|
+
import { rankChunks } from './memory/search.js'
|
|
26
27
|
import { archiveMemory, restoreMemory } from './memory/archive.js'
|
|
27
28
|
import { flushAccess, pickArchiveCandidates, readAccessLedger, recordAccess } from './memory/access.js'
|
|
28
29
|
import { warnLog, writeFileAtomic } from './memory/util.js'
|
|
@@ -48,6 +49,24 @@ function debugRecall(payload) {
|
|
|
48
49
|
console.log(`[sage-mem] recall ${JSON.stringify(payload)}`)
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
/**
|
|
53
|
+
* 按文件去重,保留每个文件**首次**(也就是得分最高)出现的那条正文命中。
|
|
54
|
+
*
|
|
55
|
+
* 正文检索是段落粒度的,同一份文件可能返回多个段落。注入链要的是「这份文件要不要端上来」,
|
|
56
|
+
* 所以在这里收成文件粒度 —— 而**不是**把同一文件的多个段落一起端上去:那正是
|
|
57
|
+
* `retrieval.js` 当初从「全文匹配」退回「只比摘要」要躲开的病(长文靠段数多占满候选位)。
|
|
58
|
+
*/
|
|
59
|
+
function dedupeByFile(hits) {
|
|
60
|
+
const seen = new Set()
|
|
61
|
+
const out = []
|
|
62
|
+
for (const h of hits) {
|
|
63
|
+
if (seen.has(h.file)) continue
|
|
64
|
+
seen.add(h.file)
|
|
65
|
+
out.push(h)
|
|
66
|
+
}
|
|
67
|
+
return out
|
|
68
|
+
}
|
|
69
|
+
|
|
51
70
|
/**
|
|
52
71
|
* 面板可改的 5 项注入上限 —— 与 `memory/config.js` 里的 `SAGE_MEM_*` 一一对应。
|
|
53
72
|
* 边界值照抄那边的 envInt 调用(lo/hi 不许在这里另立一套)。
|
|
@@ -280,11 +299,46 @@ export class MemoryGateway extends TypertRemoteService {
|
|
|
280
299
|
// 因签名变化而被重复注入。现在 baseline 由会话级标记保证只来一次,
|
|
281
300
|
// 检索由这份签名保证同一问题不重复追加,两条线互不干扰。
|
|
282
301
|
const relevant = text && text.length >= 2 ? selectRelevant(text, files) : []
|
|
283
|
-
|
|
302
|
+
|
|
303
|
+
// ── 正文检索 ──
|
|
304
|
+
// 摘要(description + aliases + 文件名)是最省的检索面,代价是**正文里的东西天生检索不到**。
|
|
305
|
+
// 这里用它做两件事:
|
|
306
|
+
// 1. 给候选文件定位「最相关的那一段」,注入时替代「文件开头 1500 字」——
|
|
307
|
+
// 旧写法对任何问题都端同一段开头,等于把「命中在哪」这件事扔掉了;
|
|
308
|
+
// 2. 摘要一条都没命中时,用正文兜一次底。此前这种情况直接等于「没有记忆可用」。
|
|
309
|
+
//
|
|
310
|
+
// query 短于 4 个字符不做正文匹配:`继续` 这种两个字的问题拿去匹配正文,
|
|
311
|
+
// 命中的全是「继续」这个词本身,与用户问什么毫无关系。
|
|
312
|
+
const bodyRanked = typeof text === 'string' && text.length >= 4
|
|
313
|
+
? rankChunks(text, files, { limit: MAX_RESULTS, perFile: 1 })
|
|
314
|
+
: []
|
|
315
|
+
const bestChunk = new Map()
|
|
316
|
+
for (const h of bodyRanked) if (!bestChunk.has(h.file)) bestChunk.set(h.file, h.chunk)
|
|
317
|
+
|
|
318
|
+
// 「摘要命中就按摘要排;一条都没有才让正文接手」这个**保守写法是错的**,
|
|
319
|
+
// 由评测数据推翻(scripts/eval-retrieval.mjs 的对照 + 注入链实测):
|
|
320
|
+
// 摘要只要命中**任何一条**(哪怕很弱、甚至命中错的文件),正文里那份真正相关的
|
|
321
|
+
// 就再也没有机会——应注入的文件级召回从 87.0% 掉到 60.9%。而噪音并没有因此更低
|
|
322
|
+
// (正文命中本来就要过相关性门槛,实测误召回 1/10,与保守写法持平)。
|
|
323
|
+
//
|
|
324
|
+
// 所以改成**并集**:摘要命中的在前(它经过人工写的 description,精确度高),
|
|
325
|
+
// 正文独有的在后(按 BM25 + 证据强度排序)。`take` 按文件去重,
|
|
326
|
+
// 同一份文件不会因为两条检索线都命中而占两个名额。
|
|
327
|
+
const byFile = new Map(files.map(f => [f.file, f]))
|
|
328
|
+
const selected = relevant.slice()
|
|
329
|
+
const taken = new Set(selected.map(f => f.file))
|
|
330
|
+
for (const h of dedupeByFile(bodyRanked)) {
|
|
331
|
+
if (taken.has(h.file)) continue
|
|
332
|
+
taken.add(h.file)
|
|
333
|
+
const f = byFile.get(h.file)
|
|
334
|
+
if (f) selected.push(f)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const signature = text + '\u0000' + selected.map(f => f.file).join(',')
|
|
284
338
|
if (recallCache.get(session.id) !== signature) {
|
|
285
339
|
recallCache.set(session.id, signature)
|
|
286
340
|
if (recallCache.size > 8) recallCache.delete(recallCache.keys().next().value)
|
|
287
|
-
|
|
341
|
+
selected.forEach(take)
|
|
288
342
|
}
|
|
289
343
|
|
|
290
344
|
if (picked.length === 0) {
|
|
@@ -305,12 +359,24 @@ export class MemoryGateway extends TypertRemoteService {
|
|
|
305
359
|
const injectedFiles = []
|
|
306
360
|
let skippedForBudget = 0
|
|
307
361
|
for (const f of picked) {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
362
|
+
// **baseline 不吃段落替换**:它的语义是「这条记忆每场会话都该完整在场」
|
|
363
|
+
// (你是谁、待办清单)。一旦用户的问句碰巧命中它,替换成单个段落就等于
|
|
364
|
+
// 把「完整在场」偷偷降级成「命中在场」—— 那正是 baseline 要防的事。
|
|
365
|
+
const chunk = f.baseline ? null : bestChunk.get(f.file)
|
|
366
|
+
// 命中段落优先;没有命中(baseline、或 query 太短没做正文检索)才回到「文件开头」。
|
|
367
|
+
// 旧写法对**任何**问题都端同一段开头,等于把「命中在哪」这个信息扔掉了。
|
|
368
|
+
const raw = chunk ? chunk.text : f.content
|
|
369
|
+
const body = raw.length > MAX_CHARS_PER_FILE
|
|
370
|
+
? raw.slice(0, MAX_CHARS_PER_FILE) + '\n…(截断)'
|
|
371
|
+
: raw
|
|
311
372
|
const days = ageDays(f, nowMs)
|
|
312
373
|
const stamp = days !== null && days >= STALE_DAYS ? `(保存于 ${days} 天前)` : ''
|
|
313
|
-
|
|
374
|
+
// 段落命中时补两样东西:
|
|
375
|
+
// - 行号:模型据此能直接用文件工具跳过去核对来源,「引用可核对」靠它成立;
|
|
376
|
+
// - 摘要:段落本身不含 frontmatter,补回「这一段是从哪条记忆里切出来的」。
|
|
377
|
+
const loc = chunk ? `:${chunk.startLine}-${chunk.endLine}` : ''
|
|
378
|
+
const brief = chunk && f.description ? ` · ${f.description.slice(0, 80)}` : ''
|
|
379
|
+
const part = `### ${f.file}${loc}${stamp}${brief}\n${body}`
|
|
314
380
|
const size = Buffer.byteLength(part, 'utf8')
|
|
315
381
|
if (usedBytes + size > MAX_SESSION_BYTES) { skippedForBudget++; continue }
|
|
316
382
|
usedBytes += size
|