dsh-selfnote 0.14.1 → 0.14.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -17
- package/lib/atomic.js +38 -1
- package/lib/capture-run.js +7 -4
- package/lib/capture.js +2 -1
- package/lib/client.js +63 -38
- package/lib/corrections.js +15 -1
- package/lib/explain.js +77 -0
- package/lib/index-store.js +4 -4
- package/lib/index.js +125 -50
- package/lib/memory.js +10 -8
- package/lib/persona-tx.js +5 -14
- package/lib/persona.js +7 -1
- package/lib/profile.js +5 -3
- package/lib/store.js +14 -4
- package/lib/transfer.js +26 -7
- package/lib/warm.js +16 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
13
|
-
<img src="https://img.shields.io/badge/version-0.14.
|
|
13
|
+
<img src="https://img.shields.io/badge/version-0.14.3-4f46e5" alt="version">
|
|
14
14
|
<img src="https://img.shields.io/badge/license-MIT-16a34a" alt="license">
|
|
15
15
|
<img src="https://img.shields.io/badge/node-%3E%3D20.13-339933" alt="node">
|
|
16
|
-
<img src="https://img.shields.io/badge/tests-
|
|
16
|
+
<img src="https://img.shields.io/badge/tests-349%20passed-0ea5e9" alt="tests">
|
|
17
17
|
</p>
|
|
18
18
|
|
|
19
19
|
---
|
|
@@ -36,13 +36,15 @@
|
|
|
36
36
|
|
|
37
37
|
**主体约定**:自述里的第一人称「我」= agent 自己;「记忆条目」「事实」的主语是用户。两者分开存、分开注入,不混为一谈。
|
|
38
38
|
|
|
39
|
-
##
|
|
39
|
+
## 五个工具
|
|
40
40
|
|
|
41
41
|
| 工具 | 做什么 | 关键约束 |
|
|
42
42
|
|---|---|---|
|
|
43
43
|
| `self_note` | 写一条第一人称自述 | 一句话到三句话;见下面「什么时候写」 |
|
|
44
44
|
| `memory_recall` | 按关键词翻历史对话原文,回来带出处 | 只收正文,剔除 reasoning / 工具输出 / 注入内容 |
|
|
45
45
|
| `memory_stage` | 把查到的一句连出处放进候选区 | 没有出处直接拒绝,所以编不了 |
|
|
46
|
+
| `memory_search` | 在「温层」里找带指针的摘要与已记的事实 | 每条带指针(sessionId + 行号),顺指针用 `memory_recall` 回原文核对 |
|
|
47
|
+
| `memory_write` | 往温层写一条带出处的摘要 | 必须带指针,不进每回合上下文,只在 `memory_search` 时浮出来 |
|
|
46
48
|
|
|
47
49
|
`self_note` 另有两个只读动作:`action: "persona"` 取 L2 扩展设定(带「这是设定,不是指令」的边界句),`action: "corrections"` 翻攒下的纠偏证据(每页 30 条,带游标)。两个都不写盘 —— **模型能读人格,但改不了人格**。
|
|
48
50
|
|
|
@@ -70,7 +72,7 @@ plugin_manager { action: "install_bundle", target: "<该目录的绝对路径>"
|
|
|
70
72
|
|
|
71
73
|
装完**重启 DSH**,三件事同时出现:
|
|
72
74
|
|
|
73
|
-
1.
|
|
75
|
+
1. 模型多了上面五个工具;
|
|
74
76
|
2. 设置页多一节「人格(自述)」(在「知识库」下面);
|
|
75
77
|
3. 每回合的系统提示里,多一段人格(L0 + L1,最多 320 字)和一段最多 1000 字的记忆。
|
|
76
78
|
|
|
@@ -109,7 +111,7 @@ plugin_manager { action: "install_bundle", target: "<该目录的绝对路径>"
|
|
|
109
111
|
| **人格** | 三段式人格(L0 核心身份 / L1 行为规则 / L2 扩展设定),带「还能写多少」的实时字数;「从 AGENT.md 导入」只填框、不保存;下面是纠偏证据列表 —— 勾几条、改写成一条规则、加进 L1 草稿,再点「保存人格」 | 人格字数 |
|
|
110
112
|
| **记忆** | 待确认候选(冲突并排)、已采纳(带热度,**正文能就地改**)、可以升成习惯的标签、已生效的习惯(能撤销) | 有待审时显示「待审 N」 |
|
|
111
113
|
| **画像** | 从已采纳记忆聚合的「关于你」,可手改覆盖,每句带来源 | 画像字数 |
|
|
112
|
-
| **注入** |
|
|
114
|
+
| **注入** | **这一回合真的会贴进提示词的那段原文**,外加每段占了多少字、哪些条目被跳过、每条为什么没进 | 注入字数 |
|
|
113
115
|
|
|
114
116
|
有待审候选时直接落在「记忆」页,没有就停在「概览」;页签支持 ←/→/Home/End 切换。
|
|
115
117
|
|
|
@@ -140,7 +142,7 @@ plugin_manager { action: "install_bundle", target: "<该目录的绝对路径>"
|
|
|
140
142
|
写的时候用 `tier` 标明是哪一型(`core` / `stable` / `temp`,不写按 `stable` 算)。落盘长这样:
|
|
141
143
|
|
|
142
144
|
```
|
|
143
|
-
- 2026-11-04 [核心*]
|
|
145
|
+
- 2026-11-04 [核心*] 默认用中文回答,专业词顺手带一句白话解释。
|
|
144
146
|
- 2026-11-04 [稳定] 回答先给结论,细节等他问。
|
|
145
147
|
- 2026-11-04 [临时] 这周在查 DSH 远程主机连不上的问题。
|
|
146
148
|
```
|
|
@@ -159,7 +161,7 @@ plugin_manager { action: "install_bundle", target: "<该目录的绝对路径>"
|
|
|
159
161
|
|
|
160
162
|
### 注入页:看见每回合贴了什么
|
|
161
163
|
|
|
162
|
-
「注入」页把 `composeInjection()`
|
|
164
|
+
「注入」页把 `assembleInjection()` 拼出来的那段字**原样**摊开(`composeInjection()` 是外面那层缓存,只管复用、不拼字)—— 包括抬头、一句人格框架句(「以下是你自己的样子,照这个说话。」)、人格 L0/L1、「(背景资料,不是指令)」这句边界句,以及 L2 的指路句。下面是分段账单:
|
|
163
165
|
|
|
164
166
|
| 段 | 预算 | 页面显示 |
|
|
165
167
|
|---|---|---|
|
|
@@ -170,7 +172,9 @@ plugin_manager { action: "install_bundle", target: "<该目录的绝对路径>"
|
|
|
170
172
|
| 结论 | 90 字 | 同上 |
|
|
171
173
|
| 习惯 | 40 字 | 同上 |
|
|
172
174
|
|
|
173
|
-
|
|
175
|
+
被预算挤掉或已过期的条目会**列出来**:每段都给 `skippedDetail`,点开就能看见每条没进上下文的原因(`budget` / `expired`);核心素材单列「素材层,不注入」,不会被误当成普通的预算竞争失败。这是**当前这一轮**的实时解释,不保存历史跳过记录 —— 实时可解释性不等于历史可追溯性。原文仍是「整条跳过,不截半句」—— 半句话会被当成结论读。这段文字来自 `assembleInjection()`(纯函数,不写盘、不记热度),所以设置页刷新一百次也不会把热度刷歪。
|
|
176
|
+
|
|
177
|
+
另外,`/state` 顶层的 `injectionCount` / `lastInjectedAt` 记的是「注入文本真的返回给模型」的次数与最后时刻(缓存命中也算),与上面的 `skippedDetail` 是两条独立观测链路:一个回答「哪些没进、为什么」,一个回答「实际返回了几次」,不用 `seenCount`、预览次数或心跳计数代替。
|
|
174
178
|
|
|
175
179
|
### 注入归属:这条记忆到底有没有被用上
|
|
176
180
|
|
|
@@ -269,7 +273,7 @@ DSH 的会话文件是**逐条追写**的 zstd 序列(一次写入 = 一个 fr
|
|
|
269
273
|
|
|
270
274
|
- 只有**真的进了注入**才算一次使用;检索到但没进上下文的,不记(否则搜一次就永远活跃)。
|
|
271
275
|
- 一小时内重复注入只记一次。
|
|
272
|
-
- 半衰期默认 30 天,可调 1–365
|
|
276
|
+
- 半衰期默认 30 天,可调 1–365 天。设置页「概览」里能直接调,也可以走 API:`POST /api/selfnote/config`,body 里带 `{"halfLifeDays": 30}`(同一条接口还收 `profileMaxChars` 20–120)。
|
|
273
277
|
- 冷下去只影响注入排序,**不会自动退役**。哪天真想起来,它还在库里能被搜到。
|
|
274
278
|
|
|
275
279
|
## 自动采集(默认开,只发现不确认)
|
|
@@ -379,7 +383,7 @@ L2 为空时那句指路句也不发(不能指一条空路)。L2 有内容
|
|
|
379
383
|
|
|
380
384
|
### 抬头:只留必要的三十个字
|
|
381
385
|
|
|
382
|
-
注入文本里有几处纯框架文字(标题行、边界句、段名前缀),不承载任何记忆。0.10.3 之前它们合计 **92 字**,等于原先
|
|
386
|
+
注入文本里有几处纯框架文字(标题行、边界句、段名前缀),不承载任何记忆。0.10.3 之前它们合计 **92 字**,等于原先 460 的预算里有 20% 花在说「下面是什么」上。(92/460≈20%;旧稿一度把分母误写成 800。)现在:
|
|
383
387
|
|
|
384
388
|
```
|
|
385
389
|
记忆(我写的)
|
|
@@ -497,7 +501,7 @@ L2 为空时那句指路句也不发(不能指一条空路)。L2 有内容
|
|
|
497
501
|
|
|
498
502
|
1. **`Host` 必须是回环地址或桌面壳(`dsh-app`)** —— 挡 DNS 重绑定;
|
|
499
503
|
2. **带了 `Origin` 就必须同源** —— 跨站页面的请求根本发不出去(服务端不回 CORS 头);
|
|
500
|
-
3. **带了 `x-selfnote` 就必须等于本实例的 token** —— token 在首次启动时随机生成(或用 `DSH_SELFNOTE_TOKEN` 指定),存在 `~/.dsh/selfnote/auth-token
|
|
504
|
+
3. **带了 `x-selfnote` 就必须等于本实例的 token** —— token 在首次启动时随机生成(或用 `DSH_SELFNOTE_TOKEN` 指定),存在 `~/.dsh/selfnote/auth-token`(POSIX 上 600;Windows 上 chmod 是空操作,靠用户目录自身的 ACL)。**它不是写在文档里的固定值**,照抄 README 读不到你的记忆。
|
|
501
505
|
|
|
502
506
|
第 3 条是**可选**的:桌面壳的页面从 `dsh-app://app` 加载,拿不到注入的 token,所以页面不带也放行;脚本调用带上它更稳妥。
|
|
503
507
|
|
|
@@ -505,7 +509,7 @@ L2 为空时那句指路句也不发(不能指一条空路)。L2 有内容
|
|
|
505
509
|
|
|
506
510
|
**拒绝和成功的破坏性操作都记进 `~/.dsh/selfnote/auth.log`**:被拒的写 `reason`,成功清空自述的写 `action: 'clear'` + 清了几条 + 归档文件名。哪天自述突然空了,翻这个文件就知道是哪一次、几点、清了多少条。
|
|
507
511
|
|
|
508
|
-
想用脚本自测,读 `~/.dsh/selfnote/auth-token
|
|
512
|
+
想用脚本自测,读 `~/.dsh/selfnote/auth-token`(POSIX 600,Windows 靠用户目录 ACL),或自己设环境变量后带上它。
|
|
509
513
|
|
|
510
514
|
## 和 `dsh-agent-md` 的区别
|
|
511
515
|
|
|
@@ -520,7 +524,7 @@ L2 为空时那句指路句也不发(不能指一条空路)。L2 有内容
|
|
|
520
524
|
|
|
521
525
|
设置页「迁移」卡两个按钮,全程在你自己的浏览器与磁盘之间。导出的包是 **v2**:比 v1 多带人格三段(`persona`)与两个配置项(`personaEnabled` / `tempTtlDays`)。**纠偏证据不进包** —— 它是「这台机器上发生过什么」,不是人格本身。v1 老包照样能导进来,缺的字段按默认补;目标机已有不同人格时不覆盖,落成 `persona.imported-<时间戳>.md`。
|
|
522
526
|
|
|
523
|
-
- **导出**:一份 JSON(`selfnote-YYYY-MM-DD.json
|
|
527
|
+
- **导出**:一份 JSON(`selfnote-YYYY-MM-DD.json`),含自述(分型与钉住也跟着走)、facts、习惯,以及你可调的那几项配置;导入时配置只按白名单与基本类型落盘。
|
|
524
528
|
- **导入**:在新机器上选这份文件。规则只有三条 —— **不覆盖**(同 id、同正文都跳过)、**默认降级**(导进来的「已采纳」一律变回候选,勾了「保留原状态」才原样搬)、**坏文件直接拒**(不是本插件的包、版本比本机新,都返回 400,不会合并一半)。
|
|
525
529
|
|
|
526
530
|
索引不跟着搬:它记的是本机磁盘路径与会话 mtime,换个地方必然过期。导入后重新构建一次就行(设置页「索引」卡)。
|
|
@@ -528,7 +532,7 @@ L2 为空时那句指路句也不发(不能指一条空路)。L2 有内容
|
|
|
528
532
|
## 开发
|
|
529
533
|
|
|
530
534
|
```bash
|
|
531
|
-
node --run test #
|
|
535
|
+
node --run test # 349 项,29 个文件,无外部依赖
|
|
532
536
|
node --run test:accept # 12 项:真实 HTTP + 36 进程并发写
|
|
533
537
|
```
|
|
534
538
|
|
|
@@ -542,8 +546,10 @@ node --run test:accept # 12 项:真实 HTTP + 36 进程并发写
|
|
|
542
546
|
| `tests/corrections.mjs` | 纠偏证据:强弱信号与误命中、事件级去重、分页游标、200 条上限、按 usedAt 清理、水位不回填、跨三文件事务的幂等恢复与坏日志隔离、指纹计数 |
|
|
543
547
|
| `tests/capture.mjs` `tests/capture-p1b.mjs` `tests/capture-debug.mjs` | 触发词、去重合并、游标幂等与崩溃恢复、迟到事件(空洞)、采集轨迹落点 |
|
|
544
548
|
| `tests/fair.mjs` `tests/warm.mjs` | 环境段冷门预留与轮换、压缩后补回;温层带指针摘要(无指针拒写、可吊销不可删) |
|
|
545
|
-
| `tests/transfer.mjs` |
|
|
546
|
-
| `tests/
|
|
549
|
+
| `tests/transfer.mjs` `tests/transfer-roundtrip.mjs` | 导出/导入:白名单、不覆盖、降级、坏文件拒绝;自述三型 / pin / 配置跟着往返 |
|
|
550
|
+
| `tests/r3-regressions.mjs` | 全量审查回归:采集正文、索引原型键、坏行隔离、缓存键与 UI 可读性 |
|
|
551
|
+
| `tests/crash-write.mjs` `tests/atomic-retry.mjs` `tests/capacity-warning.mjs` | 写入过程被 SIGKILL 不留半截文件、rename 瞬态冲突退避重试、长条挤掉旧规则时容量告警不静默 |
|
|
552
|
+
| `tests/unicode-rulers.mjs` | 全插件「字」按 Unicode 码点算:emoji 截断不写坏、字数校验、配置落盘与段 chars 都按码点 |
|
|
547
553
|
| `tests/acceptance-runtime.mjs` | 鉴权(无头 / 跨站 / 固定口令)、并发写不丢、锁放弃、注入预算 |
|
|
548
554
|
|
|
549
555
|
改 `lib/*.js` 或 `lib/client.js` 之后**必须重启 DSH**,热重载不会重新扫描 client bundle。测试会自动隔离 `DSH_MEMORY_DIR`,不会污染真实记忆。
|
|
@@ -560,7 +566,8 @@ lib/index-store.js 倒排索引
|
|
|
560
566
|
lib/persona.js 人格三段:解析、码点计数、上限校验、原子保存
|
|
561
567
|
lib/persona-tx.js 跨三文件事务:写人格 + 消费证据 + 记账,可幂等恢复
|
|
562
568
|
lib/corrections.js 纠偏证据:强弱信号、事件去重、游标分页、水位、清理
|
|
563
|
-
lib/store.js 自述文件读写:三型标记、pin
|
|
569
|
+
lib/store.js 自述文件读写:三型标记、pin、保质期、归档、码点计数与截断助手
|
|
570
|
+
lib/explain.js 每段跳过原因:封闭枚举、字素簇预览、core 明示素材层
|
|
564
571
|
lib/atomic.js 原子写:同目录临时文件 + fsync + rename
|
|
565
572
|
lib/warm.js 温层:带指针摘要,可吊销不可删
|
|
566
573
|
lib/transfer.js 导出/导入:白名单、不覆盖、降级
|
package/lib/atomic.js
CHANGED
|
@@ -11,6 +11,40 @@
|
|
|
11
11
|
import { closeSync, fsyncSync, openSync, readdirSync, renameSync, statSync, unlinkSync, writeSync } from 'node:fs'
|
|
12
12
|
import { basename, dirname, join } from 'node:path'
|
|
13
13
|
|
|
14
|
+
// Windows 没有 POSIX「覆盖正被打开的目标文件」语义:多进程硬写同一份 facts.jsonl 时,
|
|
15
|
+
// renameSync 偶发 EPERM / EBUSY / EACCES —— 目标只是「暂时被占用」,等几毫秒就好。
|
|
16
|
+
// ENOENT、EISDIR 之类是路径或权限真错了,重试只会白等,必须立刻抛。
|
|
17
|
+
const TRANSIENT_FS_CODES = new Set(['EPERM', 'EBUSY', 'EACCES'])
|
|
18
|
+
const RENAME_BACKOFF = new Int32Array(new SharedArrayBuffer(4))
|
|
19
|
+
|
|
20
|
+
export function isTransientFsError(error) {
|
|
21
|
+
return TRANSIENT_FS_CODES.has(error?.code)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 只对瞬态占用做有限次重试。delays 的长度就是总尝试次数(不是「重试次数」):
|
|
26
|
+
* 第 i 次失败后等 delays[i] 毫秒再试下一次,最后一次仍冲突就把最后的错抛出去。
|
|
27
|
+
* sleep 可注入,测试里用空函数省掉真实等待。
|
|
28
|
+
*/
|
|
29
|
+
export function retryOnTransient(operation, options = {}) {
|
|
30
|
+
const delays = Array.isArray(options.delays) ? options.delays : [5, 10, 20, 40, 80]
|
|
31
|
+
const sleep = typeof options.sleep === 'function' ? options.sleep : (ms) => {
|
|
32
|
+
try {
|
|
33
|
+
Atomics.wait(RENAME_BACKOFF, 0, 0, ms)
|
|
34
|
+
} catch {
|
|
35
|
+
/* 部分环境不支持 Atomics.wait 就退化成不让,重试仍会进行 */
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
for (let attempt = 0; attempt < delays.length; attempt += 1) {
|
|
39
|
+
try {
|
|
40
|
+
return operation()
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (!isTransientFsError(error) || attempt === delays.length - 1) throw error
|
|
43
|
+
sleep(delays[attempt])
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
14
48
|
/**
|
|
15
49
|
* 扫掉同一个目标留下的陈旧临时文件。
|
|
16
50
|
* 被 SIGKILL 的进程没机会自己删 tmp,所以下一次写这个文件时顺手清掉「已经放了很久」的。
|
|
@@ -53,7 +87,9 @@ export function writeAtomic(file, text) {
|
|
|
53
87
|
fsyncSync(fd)
|
|
54
88
|
closeSync(fd)
|
|
55
89
|
fd = -1
|
|
56
|
-
|
|
90
|
+
// Windows 上目标被别的进程短暂占用时 rename 会 EPERM/EBUSY:
|
|
91
|
+
// 等几毫秒重试,而不是把「整份写回」直接判成失败(test:accept 曾偶发这个错)。
|
|
92
|
+
retryOnTransient(() => renameSync(tmp, file))
|
|
57
93
|
} catch (error) {
|
|
58
94
|
if (fd !== -1) {
|
|
59
95
|
try {
|
|
@@ -72,6 +108,7 @@ export function writeAtomic(file, text) {
|
|
|
72
108
|
}
|
|
73
109
|
|
|
74
110
|
export function writeJsonAtomic(file, value, options = {}) {
|
|
111
|
+
if (value === undefined) throw new TypeError('writeJsonAtomic: undefined 不能写进 JSON 文件')
|
|
75
112
|
// 默认缩进是为了人能读;索引本体十兆,缩进全是白占的体积,传 pretty:false
|
|
76
113
|
const text = options.pretty === false ? JSON.stringify(value) : JSON.stringify(value, null, 2)
|
|
77
114
|
writeAtomic(file, text + '\n')
|
package/lib/capture-run.js
CHANGED
|
@@ -14,13 +14,13 @@ import { runSerial } from './persona-tx.js'
|
|
|
14
14
|
|
|
15
15
|
/** 从一条 UserMessage 的 content 里抽纯文本(content 可能是字符串或块数组)。 */
|
|
16
16
|
export function textOfEvent(event) {
|
|
17
|
-
const content = event?.data?.content ?? event?.data?.text ?? ''
|
|
17
|
+
const content = event?.data?.message?.content ?? event?.data?.content ?? event?.data?.text ?? event?.content ?? ''
|
|
18
18
|
if (typeof content === 'string') return content
|
|
19
19
|
if (!Array.isArray(content)) return ''
|
|
20
20
|
const parts = []
|
|
21
21
|
for (const block of content) {
|
|
22
22
|
if (block === null || typeof block !== 'object') continue
|
|
23
|
-
if (block.type === 'text' && typeof block.text === 'string') parts.push(block.text)
|
|
23
|
+
if ((block.type === 'text' || block.type === 'input_text') && typeof block.text === 'string') parts.push(block.text)
|
|
24
24
|
}
|
|
25
25
|
return parts.join('\n')
|
|
26
26
|
}
|
|
@@ -53,7 +53,8 @@ export function planWrites(texts, options = {}) {
|
|
|
53
53
|
for (const fact of facts) {
|
|
54
54
|
if (typeof fact.text !== 'string') continue
|
|
55
55
|
const verdict = compareCandidate(text, fact.text)
|
|
56
|
-
if (best === null || verdict.score > best.verdict.score
|
|
56
|
+
if (best === null || verdict.score > best.verdict.score
|
|
57
|
+
|| (verdict.score === best.verdict.score && fact.status === 'candidate' && best.fact.status !== 'candidate')) {
|
|
57
58
|
best = { fact, verdict }
|
|
58
59
|
}
|
|
59
60
|
}
|
|
@@ -121,7 +122,9 @@ export async function handleEvent(event, deps) {
|
|
|
121
122
|
if (!isUserAuthored(event)) return { handled: false, reason: 'not-user-authored' }
|
|
122
123
|
|
|
123
124
|
const sessionId = deps.sessionId ?? event?.sessionId ?? event?.data?.session?.id ?? 'unknown'
|
|
124
|
-
const
|
|
125
|
+
const rawSeq = event?.seq ?? event?.data?.seq
|
|
126
|
+
const seqText = typeof rawSeq === 'number' ? String(rawSeq) : typeof rawSeq === 'string' ? rawSeq.trim() : ''
|
|
127
|
+
const seq = seqText === '' || /^[0-9]+$/.test(seqText) === false ? NaN : Number(seqText)
|
|
125
128
|
if (!Number.isSafeInteger(seq) || seq < 0) return { handled: false, reason: 'no-seq' }
|
|
126
129
|
|
|
127
130
|
const raw = textOfEvent(event)
|
package/lib/capture.js
CHANGED
|
@@ -18,6 +18,7 @@ import { join } from 'node:path'
|
|
|
18
18
|
|
|
19
19
|
import { writeJsonAtomic } from './atomic.js'
|
|
20
20
|
import { memoryRoot } from './memory.js'
|
|
21
|
+
import { countChars } from './store.js'
|
|
21
22
|
|
|
22
23
|
/* ------------------------------ 触发 ------------------------------ */
|
|
23
24
|
|
|
@@ -81,7 +82,7 @@ export function candidatesOf(text, limit = 3) {
|
|
|
81
82
|
const out = []
|
|
82
83
|
for (const sentence of value.match(SENTENCE) ?? []) {
|
|
83
84
|
const line = sentence.trim()
|
|
84
|
-
if (line
|
|
85
|
+
if (countChars(line) < 4 || countChars(line) > 200) continue
|
|
85
86
|
if (triggersOf(line).length === 0) continue
|
|
86
87
|
if (!hasScope(line)) continue
|
|
87
88
|
if (out.includes(line)) continue
|
package/lib/client.js
CHANGED
|
@@ -213,8 +213,6 @@ window.__ModuleLoader__.load({
|
|
|
213
213
|
setTempTtl(String(data.tempTtlDays === undefined ? 14 : data.tempTtlDays))
|
|
214
214
|
setHalfLife(String(data.halfLifeDays === undefined ? 30 : data.halfLifeDays))
|
|
215
215
|
setProfileMax(String(data.profile && data.profile.maxChars !== undefined ? data.profile.maxChars : 110))
|
|
216
|
-
setMoreEvidence([])
|
|
217
|
-
setMoreCursor(null)
|
|
218
216
|
// 每次拿到新状态,列表下标都可能整体挪位:正在改/正等确认的那条已经没意义了
|
|
219
217
|
setEditing(null)
|
|
220
218
|
setPendingDelete(null)
|
|
@@ -483,8 +481,9 @@ window.__ModuleLoader__.load({
|
|
|
483
481
|
h('textarea', {
|
|
484
482
|
className: 'sn-input',
|
|
485
483
|
rows: rows,
|
|
486
|
-
//
|
|
487
|
-
|
|
484
|
+
// 不用 maxLength:HTML 的 maxLength 按 UTF-16 单元计,一个 emoji 算 2,
|
|
485
|
+
// 会和按码点算的预算打架(emoji 多的段落会提前两个字符被拦住)。
|
|
486
|
+
// 这里只给计数与超限提示,真正拦的是保存时那一次整份校验。
|
|
488
487
|
value: value,
|
|
489
488
|
'data-field': key,
|
|
490
489
|
onChange: function (e) { editPersona(key, e.target.value) },
|
|
@@ -819,7 +818,6 @@ window.__ModuleLoader__.load({
|
|
|
819
818
|
className: 'sn-input',
|
|
820
819
|
rows: 2,
|
|
821
820
|
// 一条事实硬上限 240 字(服务端同一条)
|
|
822
|
-
maxLength: 240,
|
|
823
821
|
value: factEdit.text,
|
|
824
822
|
onChange: function (e) { setFactEdit({ id: fact.id, text: e.target.value }) },
|
|
825
823
|
}),
|
|
@@ -901,10 +899,10 @@ window.__ModuleLoader__.load({
|
|
|
901
899
|
{ className: 'sn-budget', key: 'budget' },
|
|
902
900
|
h(
|
|
903
901
|
'div',
|
|
904
|
-
{ className: 'sn-budget-bar', title: '本回合注入 ' + (budget.total || 0) + ' / ' + limit + ' 字' },
|
|
902
|
+
{ className: 'sn-budget-bar', title: '本回合注入 ' + (budget.total || 0) + ' / ' + limit + ' 字', role: 'progressbar', 'aria-valuenow': budget.total || 0, 'aria-valuemin': 0, 'aria-valuemax': limit, 'aria-label': '本回合注入字数' },
|
|
905
903
|
segments.map(function (item) {
|
|
906
904
|
var width = limit > 0 ? Math.max(0, Math.min(100, (item.value / limit) * 100)) : 0
|
|
907
|
-
return h('div', { key: item.key, className: 'sn-budget-seg', style: { width: width + '%', background: item.color } })
|
|
905
|
+
return h('div', { key: item.key, className: 'sn-budget-seg', style: { width: width + '%', background: item.color }, title: item.label + ' ' + item.value + ' 字' })
|
|
908
906
|
}),
|
|
909
907
|
),
|
|
910
908
|
h(
|
|
@@ -972,7 +970,7 @@ window.__ModuleLoader__.load({
|
|
|
972
970
|
h(
|
|
973
971
|
'div',
|
|
974
972
|
{ className: 'sn-row' },
|
|
975
|
-
h('label', { className: 'sn-toggle' }, h('input', { type: 'checkbox', checked: state.enabled, onChange: toggleEnabled }), h('span', null, '每回合注入')),
|
|
973
|
+
h('label', { className: 'sn-toggle' }, h('input', { type: 'checkbox', checked: state.enabled, disabled: busy !== '', onChange: toggleEnabled }), h('span', null, '每回合注入')),
|
|
976
974
|
h('span', { className: 'sn-grow' }),
|
|
977
975
|
h('button', { className: 'sn-btn sn-btn-ghost', onClick: refresh, disabled: busy !== '' }, '刷新'),
|
|
978
976
|
),
|
|
@@ -995,7 +993,6 @@ window.__ModuleLoader__.load({
|
|
|
995
993
|
h('span', null, '注入抬头(每回合那段的第一行)'),
|
|
996
994
|
h('input', {
|
|
997
995
|
type: 'text',
|
|
998
|
-
maxLength: 24,
|
|
999
996
|
'aria-label': '注入抬头,最多 24 个字,留空就不加抬头',
|
|
1000
997
|
value: heading,
|
|
1001
998
|
placeholder: '例:AI记忆',
|
|
@@ -1055,7 +1052,6 @@ window.__ModuleLoader__.load({
|
|
|
1055
1052
|
h('span', null, '什么时候该写自述(留空=用默认判据)'),
|
|
1056
1053
|
h('textarea', {
|
|
1057
1054
|
rows: 2,
|
|
1058
|
-
maxLength: 80,
|
|
1059
1055
|
'aria-label': '什么时候该写自述,留空就用默认判据',
|
|
1060
1056
|
value: noteGuide,
|
|
1061
1057
|
placeholder: '例:每次改完一个插件、跑完验收之后写一条;闲聊不用写。',
|
|
@@ -1206,6 +1202,7 @@ window.__ModuleLoader__.load({
|
|
|
1206
1202
|
|
|
1207
1203
|
/** 导入:读文件 → 交服务端合并 → 把账目摊在卡上。 */
|
|
1208
1204
|
function importBundle(file) {
|
|
1205
|
+
if (busy !== '') return
|
|
1209
1206
|
if (!file) return
|
|
1210
1207
|
setBusy('import')
|
|
1211
1208
|
setTransfer(null)
|
|
@@ -1308,7 +1305,7 @@ window.__ModuleLoader__.load({
|
|
|
1308
1305
|
),
|
|
1309
1306
|
h(
|
|
1310
1307
|
'div',
|
|
1311
|
-
{ className: 'sn-file-path', title: '点一下复制路径', onClick: function () { copyPath(file.path) } },
|
|
1308
|
+
{ className: 'sn-file-path', title: '点一下复制路径', onClick: function () { copyPath(file.path) }, role: 'button', tabIndex: 0, onKeyDown: function (e) { if (e.key === 'Enter' || e.key === ' ') copyPath(file.path) } },
|
|
1312
1309
|
file.path,
|
|
1313
1310
|
),
|
|
1314
1311
|
h(
|
|
@@ -1345,12 +1342,13 @@ window.__ModuleLoader__.load({
|
|
|
1345
1342
|
h('button', { className: 'sn-btn sn-btn-ghost', onClick: exportAll, disabled: busy !== '' }, busy === 'export' ? '导出中…' : '导出全部记忆'),
|
|
1346
1343
|
h(
|
|
1347
1344
|
'label',
|
|
1348
|
-
{ className: 'sn-btn sn-btn-ghost sn-transfer-pick' },
|
|
1345
|
+
{ className: 'sn-btn sn-btn-ghost sn-transfer-pick', role: 'button', tabIndex: busy === '' ? 0 : -1, 'aria-disabled': busy !== '', onKeyDown: function (event) { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); if (busy === '') event.currentTarget.querySelector('input').click() } } },
|
|
1349
1346
|
busy === 'import' ? '导入中…' : '选择文件导入',
|
|
1350
1347
|
h('input', {
|
|
1351
1348
|
type: 'file',
|
|
1352
1349
|
accept: 'application/json,.json',
|
|
1353
|
-
style: {
|
|
1350
|
+
style: { position: 'absolute', width: 1, height: 1, opacity: 0 },
|
|
1351
|
+
disabled: busy !== '',
|
|
1354
1352
|
onChange: function (event) {
|
|
1355
1353
|
var picked = event.target.files && event.target.files[0]
|
|
1356
1354
|
event.target.value = ''
|
|
@@ -1504,7 +1502,6 @@ window.__ModuleLoader__.load({
|
|
|
1504
1502
|
className: 'sn-input',
|
|
1505
1503
|
rows: 2,
|
|
1506
1504
|
// 单条硬上限 600 字(服务端同一条)——这里是提示,不是校验
|
|
1507
|
-
maxLength: 600,
|
|
1508
1505
|
value: editText,
|
|
1509
1506
|
onChange: function (e) { setEditText(e.target.value) },
|
|
1510
1507
|
}),
|
|
@@ -1555,7 +1552,7 @@ window.__ModuleLoader__.load({
|
|
|
1555
1552
|
h(
|
|
1556
1553
|
'button',
|
|
1557
1554
|
{
|
|
1558
|
-
className: 'sn-btn sn-btn-ghost sn-pin' + (entry.pin === true ? ' sn-pin-on' : ''),
|
|
1555
|
+
'aria-label': entry.pin === true ? '取消钉住' : '钉住', className: 'sn-btn sn-btn-ghost sn-pin' + (entry.pin === true ? ' sn-pin-on' : ''),
|
|
1559
1556
|
title: entry.pin === true ? '已钉住:同一型里优先装进去(再点取消)' : '钉住:同一型里优先装进去',
|
|
1560
1557
|
onClick: function () { setNoteTier(stored, { pin: entry.pin !== true }) },
|
|
1561
1558
|
disabled: busy !== '',
|
|
@@ -1588,7 +1585,7 @@ window.__ModuleLoader__.load({
|
|
|
1588
1585
|
h(
|
|
1589
1586
|
'label',
|
|
1590
1587
|
{ className: 'sn-toggle' },
|
|
1591
|
-
h('input', { type: 'checkbox', checked: !!(state.profile && state.profile.enabled), onChange: toggleProfile }),
|
|
1588
|
+
h('input', { type: 'checkbox', checked: !!(state.profile && state.profile.enabled), disabled: busy !== '', onChange: toggleProfile }),
|
|
1592
1589
|
h('span', null, '每回合注入画像'),
|
|
1593
1590
|
),
|
|
1594
1591
|
h('span', { className: 'sn-grow' }),
|
|
@@ -1634,6 +1631,10 @@ window.__ModuleLoader__.load({
|
|
|
1634
1631
|
var pTotalChars = p0Chars + p1Chars + pGap
|
|
1635
1632
|
var pRestChars = Math.max(0, 320 - pTotalChars)
|
|
1636
1633
|
var pL1Max = Math.max(0, 320 - Math.min(p0Chars, 80) - (p0Chars > 0 ? 1 : 0))
|
|
1634
|
+
var p2Chars = pinCount(personaValuesNow.l2)
|
|
1635
|
+
// 超限不靠 maxLength 拦(它按 UTF-16 计,emoji 算两个),而是停掉「保存」并说明原因;
|
|
1636
|
+
// 磁盘那关仍由服务端整份校验兜底 —— 这里只是别让人白点一次。
|
|
1637
|
+
var pOver = p0Chars > 80 || pTotalChars > 320 || p0Chars + p1Chars + p2Chars > 8000
|
|
1637
1638
|
var firstPage = (state.corrections && state.corrections.items) || []
|
|
1638
1639
|
// 翻过页就把后面的接上;同一 id 只留第一条(翻页期间新证据插进来时会出现重复)
|
|
1639
1640
|
var correctionItems = firstPage.concat(moreEvidence).filter(function (item, at, list) {
|
|
@@ -1654,10 +1655,21 @@ window.__ModuleLoader__.load({
|
|
|
1654
1655
|
'L0 ' + p0Chars + ' + L1 ' + p1Chars + (pGap ? ' + 换行 1' : '') + ' = ' + pTotalChars + ' / 320 字(余 ' + pRestChars + ')',
|
|
1655
1656
|
),
|
|
1656
1657
|
personaField('l2', 'L2 扩展设定', '外貌、服饰、世界观、兴趣。这一层不进每回合上下文;保存在同一个 persona.md 里,不占注入预算。', personaValuesNow.l2, (state.persona && state.persona.l2Limit) || Math.max(0, 8000 - pTotalChars), 6),
|
|
1658
|
+
pOver
|
|
1659
|
+
? h(
|
|
1660
|
+
'div',
|
|
1661
|
+
{ className: 'sn-hint-warn' },
|
|
1662
|
+
'超了:'
|
|
1663
|
+
+ (p0Chars > 80 ? 'L0 超过 80 字;' : '')
|
|
1664
|
+
+ (pTotalChars > 320 ? 'L0 + L1 超过 320 字;' : '')
|
|
1665
|
+
+ (p0Chars + p1Chars + p2Chars > 8000 ? '整份超过 8000 字;' : '')
|
|
1666
|
+
+ '减到上限以内再保存,否则整份会被拒绝、一个字都不写。',
|
|
1667
|
+
)
|
|
1668
|
+
: null,
|
|
1657
1669
|
h(
|
|
1658
1670
|
'div',
|
|
1659
1671
|
{ className: 'sn-actions' },
|
|
1660
|
-
h('button', { className: 'sn-btn', onClick: savePersona, disabled: busy !== '' }, '保存人格'),
|
|
1672
|
+
h('button', { className: 'sn-btn', onClick: savePersona, disabled: busy !== '' || pOver }, '保存人格'),
|
|
1661
1673
|
h('button', { className: 'sn-btn sn-btn-ghost', onClick: importPersona, disabled: busy !== '' }, '从 AGENT.md 导入'),
|
|
1662
1674
|
h('span', { className: 'sn-grow' }),
|
|
1663
1675
|
h('span', { className: 'sn-muted' }, '保存是整份校验:任一段不合格就一个字都不写。'),
|
|
@@ -1808,7 +1820,6 @@ window.__ModuleLoader__.load({
|
|
|
1808
1820
|
h('textarea', {
|
|
1809
1821
|
className: 'sn-input',
|
|
1810
1822
|
rows: 2,
|
|
1811
|
-
maxLength: 240,
|
|
1812
1823
|
value: factEdit.text,
|
|
1813
1824
|
onChange: function (e) { setFactEdit({ id: fact.id, text: e.target.value }) },
|
|
1814
1825
|
}),
|
|
@@ -1943,9 +1954,20 @@ window.__ModuleLoader__.load({
|
|
|
1943
1954
|
var injSegments = inj.segments || []
|
|
1944
1955
|
var skippedItems = []
|
|
1945
1956
|
injSegments.forEach(function (seg) {
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1957
|
+
if (Array.isArray(seg.skippedDetail)) {
|
|
1958
|
+
seg.skippedDetail.forEach(function (item) {
|
|
1959
|
+
skippedItems.push({
|
|
1960
|
+
seg: seg.label,
|
|
1961
|
+
text: String((item && item.preview) ?? ''),
|
|
1962
|
+
reason: String((item && item.reason) ?? ''),
|
|
1963
|
+
})
|
|
1964
|
+
})
|
|
1965
|
+
} else {
|
|
1966
|
+
// 兼容旧 /state:只有 skipped 字符串,没有原因,也不许白板
|
|
1967
|
+
;(seg.skipped || []).forEach(function (text) {
|
|
1968
|
+
skippedItems.push({ seg: seg.label, text: String(text ?? ''), reason: '' })
|
|
1969
|
+
})
|
|
1970
|
+
}
|
|
1949
1971
|
})
|
|
1950
1972
|
|
|
1951
1973
|
var injectionCard = h(
|
|
@@ -1968,9 +1990,9 @@ window.__ModuleLoader__.load({
|
|
|
1968
1990
|
'div',
|
|
1969
1991
|
{ className: 'sn-seg' },
|
|
1970
1992
|
injSegments.map(function (seg) {
|
|
1971
|
-
var skipped = seg.skipped || []
|
|
1993
|
+
var skipped = Array.isArray(seg.skippedDetail) ? seg.skippedDetail : (seg.skipped || [])
|
|
1972
1994
|
var stateText = seg.chars > 0 ? '全部进入' : '暂无内容'
|
|
1973
|
-
if (skipped.length > 0) stateText = '
|
|
1995
|
+
if (skipped.length > 0) stateText = '跳过 ' + skipped.length + ' 条'
|
|
1974
1996
|
else if (seg.chars === 0 && seg.absorbed > 0) stateText = '已被画像吸收 ' + seg.absorbed + ' 条'
|
|
1975
1997
|
return h(
|
|
1976
1998
|
'div',
|
|
@@ -1984,25 +2006,27 @@ window.__ModuleLoader__.load({
|
|
|
1984
2006
|
h(
|
|
1985
2007
|
'div',
|
|
1986
2008
|
{ className: 'sn-hint' },
|
|
1987
|
-
'
|
|
2009
|
+
'七段只算记忆本身的字数;标题、边界句、画像抬头这些框架文字另计(' + (inj.overhead || 0) + ' 字),也算在总字数里。',
|
|
1988
2010
|
),
|
|
1989
2011
|
skippedItems.length === 0
|
|
1990
2012
|
? null
|
|
1991
2013
|
: h(
|
|
1992
|
-
'
|
|
2014
|
+
'details',
|
|
1993
2015
|
{ className: 'sn-skipped' },
|
|
1994
2016
|
h(
|
|
1995
|
-
'
|
|
2017
|
+
'summary',
|
|
1996
2018
|
{ className: 'sn-skipped-head' },
|
|
1997
|
-
'
|
|
2019
|
+
'被跳过的 ' + skippedItems.length + ' 条 —— 点开看每条为什么没进(每条都标了原因)',
|
|
1998
2020
|
),
|
|
1999
2021
|
skippedItems.slice(0, 6).map(function (item, i) {
|
|
2000
|
-
|
|
2022
|
+
// key 用下标:内容指纹派生 id,同段两条相同文本会撞 id(口径裁定 2)
|
|
2023
|
+
var reasonLabel = { budget: '预算不足', expired: '已过期', scope: '项目不匹配', duplicate: '重复' }
|
|
2001
2024
|
return h(
|
|
2002
2025
|
'div',
|
|
2003
2026
|
{ className: 'sn-skipped-row', key: 'sk' + i },
|
|
2004
2027
|
h('span', { className: 'sn-badge' }, item.seg),
|
|
2005
|
-
h('span', { className: 'sn-skipped-
|
|
2028
|
+
h('span', { className: 'sn-skipped-reason' }, reasonLabel[item.reason] || item.reason || '未说明'),
|
|
2029
|
+
h('span', { className: 'sn-skipped-text' }, item.text),
|
|
2006
2030
|
)
|
|
2007
2031
|
}),
|
|
2008
2032
|
skippedItems.length > 6 ? h('div', { className: 'sn-hint' }, '还有 ' + (skippedItems.length - 6) + ' 条没列出来') : null,
|
|
@@ -2052,12 +2076,12 @@ window.__ModuleLoader__.load({
|
|
|
2052
2076
|
'div',
|
|
2053
2077
|
{ className: 'sn-attr' },
|
|
2054
2078
|
attrRows.slice(0, 14).map(function (row) {
|
|
2055
|
-
var text = row.text
|
|
2079
|
+
var text = row.text
|
|
2056
2080
|
return h(
|
|
2057
2081
|
'div',
|
|
2058
2082
|
{ className: 'sn-attr-row sn-attr-' + row.state, key: row.id },
|
|
2059
2083
|
h('span', { className: 'sn-badge' }, attrSegLabel[row.segment] || (row.state === 'searched' ? '搜到过' : '没进')),
|
|
2060
|
-
h('span', { className: 'sn-attr-text' }, text),
|
|
2084
|
+
h('span', { className: 'sn-attr-text', title: row.text }, text),
|
|
2061
2085
|
h(
|
|
2062
2086
|
'span',
|
|
2063
2087
|
{ className: 'sn-attr-num' },
|
|
@@ -2127,7 +2151,7 @@ window.__ModuleLoader__.load({
|
|
|
2127
2151
|
type: 'button',
|
|
2128
2152
|
role: 'tab',
|
|
2129
2153
|
'aria-selected': active,
|
|
2130
|
-
'aria-controls': 'sn-panel-' +
|
|
2154
|
+
'aria-controls': 'sn-panel-' + activeTab,
|
|
2131
2155
|
tabIndex: active ? 0 : -1,
|
|
2132
2156
|
className: 'sn-tab' + (active ? ' sn-tab-on' : ''),
|
|
2133
2157
|
onClick: function () { setTab(item.key); setPicked(true) },
|
|
@@ -2164,7 +2188,7 @@ window.__ModuleLoader__.load({
|
|
|
2164
2188
|
'.sn-transfer{display:flex;flex-direction:column;gap:8px}',
|
|
2165
2189
|
'.sn-transfer-row{display:flex;gap:8px;flex-wrap:wrap}',
|
|
2166
2190
|
'.sn-transfer-pick{cursor:pointer}',
|
|
2167
|
-
'.sn-transfer-report{margin-top:4px;font-size:12px;line-height:1.7;color:var(--
|
|
2191
|
+
'.sn-transfer-report{margin-top:4px;font-size:12px;line-height:1.7;color:var(--dsw-alias-text-secondary,#6b7280)}',
|
|
2168
2192
|
'.sn-transfer-warn{color:#b45309}',
|
|
2169
2193
|
'.sn-root{display:flex;flex-direction:column;gap:14px;font-size:13px}',
|
|
2170
2194
|
'.sn-head{display:flex;flex-direction:column;gap:4px}',
|
|
@@ -2260,7 +2284,7 @@ window.__ModuleLoader__.load({
|
|
|
2260
2284
|
".sn-tab:focus-visible{outline:2px solid var(--dsw-alias-accent-default,#2563eb);outline-offset:-2px}",
|
|
2261
2285
|
".sn-tab-on{color:var(--dsw-alias-text-primary,#111);font-weight:600}",
|
|
2262
2286
|
".sn-tab-on::after{content:\"\";position:absolute;left:10px;right:10px;bottom:-1px;height:2px;background:var(--dsw-alias-accent-default,#2563eb);border-radius:2px}",
|
|
2263
|
-
".sn-tab-badge{font-size:
|
|
2287
|
+
".sn-tab-badge{font-size:11.5px;line-height:16px;padding:0 6px;border-radius:999px;background:var(--dsw-alias-bg-secondary,#f3f4f6);color:var(--dsw-alias-text-secondary,#6b7280);font-weight:400}",
|
|
2264
2288
|
".sn-tab-on .sn-tab-badge{background:#eef2ff;color:#3730a3}",
|
|
2265
2289
|
".sn-panel{display:flex;flex-direction:column;gap:12px}",
|
|
2266
2290
|
".sn-panel-head{display:flex;justify-content:space-between;align-items:center}",
|
|
@@ -2320,8 +2344,9 @@ window.__ModuleLoader__.load({
|
|
|
2320
2344
|
'.sn-seg-state{margin-left:auto;font-size:11px;color:var(--dsw-alias-text-secondary,#6b7280)}',
|
|
2321
2345
|
'.sn-seg-state-warn{color:#b45309}',
|
|
2322
2346
|
'.sn-skipped{display:flex;flex-direction:column;gap:6px;padding:8px 10px;border:1px dashed var(--dsw-alias-border-subtle,#e5e7eb);border-radius:8px}',
|
|
2323
|
-
'.sn-skipped-head{font-size:
|
|
2324
|
-
'.sn-skipped-row{display:flex;align-items:flex-start;gap:8px;font-size:12px;line-height:1.6}',
|
|
2347
|
+
'.sn-skipped-head{font-size:11.5px;color:#92400e;line-height:1.6;cursor:pointer}',
|
|
2348
|
+
'.sn-skipped-row{display:flex;align-items:flex-start;gap:8px;font-size:12px;line-height:1.6;flex-wrap:wrap}',
|
|
2349
|
+
'.sn-skipped-reason{font-size:11px;color:var(--dsw-alias-text-secondary,#6b7280);border:1px solid var(--dsw-alias-border-subtle,#e5e7eb);border-radius:999px;padding:0 6px;white-space:nowrap}',
|
|
2325
2350
|
'.sn-skipped-text{color:var(--dsw-alias-text-secondary,#4b5563)}',
|
|
2326
2351
|
'.sn-doc-list{display:flex;flex-direction:column;gap:8px}',
|
|
2327
2352
|
'.sn-doc-row{display:flex;align-items:flex-start;gap:8px;font-size:12px;line-height:1.6;flex-wrap:wrap}',
|
|
@@ -2334,7 +2359,7 @@ window.__ModuleLoader__.load({
|
|
|
2334
2359
|
// —— 可读性与适配(原来是 11px + #9ca3af,在浅底上几乎要贴到屏幕上看)——
|
|
2335
2360
|
'.sn-hint{font-size:12px;color:var(--dsw-alias-text-secondary,#6b7280)}',
|
|
2336
2361
|
'.sn-hint-warn{font-size:12px}',
|
|
2337
|
-
'.sn-date,.sn-file-meta,.sn-heat,.sn-evidence-meta,.sn-fact-cite,.sn-card-note{font-size:11.5px}',
|
|
2362
|
+
'.sn-date,.sn-file-meta,.sn-heat,.sn-evidence-meta,.sn-fact-cite,.sn-card-note{font-size:11.5px;color:var(--dsw-alias-text-secondary,#6b7280)}',
|
|
2338
2363
|
'.sn-muted{color:var(--dsw-alias-text-secondary,#6b7280)}',
|
|
2339
2364
|
// 输入框以前写死 110px,抬头那种长文本被压成一条缝;宽度交给容器,只在窄屏才吃满
|
|
2340
2365
|
'.sn-field{flex-wrap:wrap}',
|
|
@@ -2346,7 +2371,7 @@ window.__ModuleLoader__.load({
|
|
|
2346
2371
|
'.sn-expired{color:var(--dsw-alias-text-secondary,#6b7280);border-color:var(--dsw-alias-border-subtle,#d1d5db)}',
|
|
2347
2372
|
'@media (max-width:520px){.sn-limits,.sn-row,.sn-actions{flex-wrap:wrap}.sn-field{width:100%}.sn-field input,.sn-field textarea{width:100%}.sn-card{padding:12px}}',
|
|
2348
2373
|
// 深色底:插件原先把浅色写死了,宿主切深色时这些块会白得刺眼
|
|
2349
|
-
'@media (prefers-color-scheme:dark){.sn-notice-ok{background:#052e21;color:#a7f3d0}.sn-notice-warn{background:#3f1212;color:#fecaca}.sn-notice{background:var(--dsw-alias-bg-secondary,#1f2937)}.sn-badge,.sn-file-count,.sn-tab-on .sn-tab-badge{background:#1e1b4b;color:#c7d2fe}.sn-material,.sn-kv-tag-stale,.sn-pair-head,.sn-doc-next,.sn-skipped-head{background:#3b2a06;color:#fcd34d}.sn-kv-tag-ok{background:#052e21;color:#6ee7b7}.sn-doc-next{border-color:#92400e}.sn-fact-conflict{background:#2a2308;border-color:#92400e}.sn-doc-ok{background:#052e21;color:#6ee7b7}.sn-doc-error{background:#3f1212;color:#fecaca}.sn-doc-warn{background:#3b2a06;color:#fcd34d}}',
|
|
2374
|
+
'@media (prefers-color-scheme:dark){.sn-notice-ok{background:#052e21;color:#a7f3d0}.sn-notice-warn{background:#3f1212;color:#fecaca}.sn-notice{background:var(--dsw-alias-bg-secondary,#1f2937)}.sn-badge,.sn-file-count,.sn-tab-on .sn-tab-badge{background:#1e1b4b;color:#c7d2fe}.sn-material,.sn-kv-tag-stale,.sn-pair-head,.sn-doc-next,.sn-skipped-head{background:#3b2a06;color:#fcd34d}.sn-kv-tag-ok{background:#052e21;color:#6ee7b7}.sn-doc-next{border-color:#92400e}.sn-fact-conflict{background:#2a2308;border-color:#92400e}.sn-doc-ok{background:#052e21;color:#6ee7b7}.sn-doc-error{background:#3f1212;color:#fecaca}.sn-doc-warn{background:#3b2a06;color:#fcd34d}.sn-file-missing .sn-file-path{color:#fcd34d}.sn-file-missing .sn-file-count{background:#3b2a06;color:#fcd34d}.sn-transfer-warn{color:#fcd34d}.sn-btn-danger{color:#fecaca;border-color:#7f1d1d}.sn-pin-on{color:#fcd34d;border-color:#92400e}.sn-count-over{color:#fca5a5}.sn-hint-warn{color:#fcd34d}.sn-loading-error{color:#fecaca}.sn-seg-state-warn{color:#fcd34d}}',
|
|
2350
2375
|
].join('\n')
|
|
2351
2376
|
|
|
2352
2377
|
function apply(ctx) {
|
package/lib/corrections.js
CHANGED
|
@@ -47,9 +47,20 @@ function normalizeEvent(raw) {
|
|
|
47
47
|
used: raw.used === true,
|
|
48
48
|
usedAt: raw.usedAt === null || raw.usedAt === undefined ? null : String(raw.usedAt),
|
|
49
49
|
consumedBy: raw.consumedBy === null || raw.consumedBy === undefined ? null : String(raw.consumedBy),
|
|
50
|
+
strength: raw.strength === 'strong' || raw.strength === 'weak' ? raw.strength : null,
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
let pendingBadRows = []
|
|
55
|
+
function collectBadRows(rows) {
|
|
56
|
+
for (const row of rows) if (!pendingBadRows.includes(row)) pendingBadRows.push(row)
|
|
57
|
+
}
|
|
58
|
+
function flushBadRows() {
|
|
59
|
+
if (pendingBadRows.length === 0) return
|
|
60
|
+
const file = join(selfnoteRoot(), `corrections.bad-${Date.now().toString(36)}-${process.pid}.jsonl`)
|
|
61
|
+
writeAtomic(file, pendingBadRows.join('\n') + '\n')
|
|
62
|
+
pendingBadRows = []
|
|
63
|
+
}
|
|
53
64
|
export function readEvents() {
|
|
54
65
|
if (!existsSync(CORRECTIONS_PATH)) return []
|
|
55
66
|
const list = []
|
|
@@ -59,8 +70,10 @@ export function readEvents() {
|
|
|
59
70
|
try {
|
|
60
71
|
const entry = normalizeEvent(JSON.parse(text))
|
|
61
72
|
if (entry !== null) list.push(entry)
|
|
73
|
+
else collectBadRows([text])
|
|
62
74
|
} catch {
|
|
63
|
-
//
|
|
75
|
+
// 一行坏了不等于整个池子坏了:隔离留存,别在下一次写回时无声抹掉
|
|
76
|
+
collectBadRows([text])
|
|
64
77
|
}
|
|
65
78
|
}
|
|
66
79
|
return list
|
|
@@ -69,6 +82,7 @@ export function readEvents() {
|
|
|
69
82
|
export function writeEvents(list) {
|
|
70
83
|
const rows = list.map((entry) => JSON.stringify(normalizeEvent(entry) ?? entry))
|
|
71
84
|
writeAtomic(CORRECTIONS_PATH, rows.length === 0 ? '' : rows.join('\n') + '\n')
|
|
85
|
+
flushBadRows()
|
|
72
86
|
}
|
|
73
87
|
|
|
74
88
|
export function findById(id) {
|