dsh-lite-memory 0.18.1 → 0.18.19
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 +13 -13
- package/TRIGGERS.md +1 -1
- package/client.js +184 -34
- package/docs/RELEASE-0.18.10.md +25 -0
- package/docs/RELEASE-0.18.11.md +48 -0
- package/docs/RELEASE-0.18.12.md +27 -0
- package/docs/RELEASE-0.18.13.md +13 -0
- package/docs/RELEASE-0.18.14.md +13 -0
- package/docs/RELEASE-0.18.15.md +5 -0
- package/docs/RELEASE-0.18.2.md +21 -0
- package/docs/RELEASE-0.18.3.md +16 -0
- package/docs/RELEASE-0.18.4.md +15 -0
- package/docs/RELEASE-0.18.5.md +21 -0
- package/docs/RELEASE-0.18.6.md +18 -0
- package/docs/RELEASE-0.18.7.md +23 -0
- package/docs/RELEASE-0.18.8.md +18 -0
- package/docs/RELEASE-0.18.9.md +24 -0
- package/index.js +347 -123
- package/learning.js +65 -6
- package/package.json +3 -3
- package/state.js +47 -6
- package/store.js +180 -18
- package/test/client.test.mjs +47 -25
- package/test/contract.test.mjs +221 -86
- package/test/control.test.mjs +2 -2
- package/test/eval-seed.test.mjs +113 -0
- package/test/evolution-read.test.mjs +10 -10
- package/test/ha.test.mjs +55 -5
- package/test/layer-read.test.mjs +293 -0
- package/test/learning.test.mjs +80 -0
- package/test/palace.test.mjs +2 -0
- package/test/recall.test.mjs +67 -9
- package/test/state.test.mjs +57 -1
- package/test/store.test.mjs +49 -5
- package/test/view.test.mjs +3 -0
- package/view.js +85 -12
package/README.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# dsh-lite-memory
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
**
|
|
3
|
+
**全库不常驻:没有命中的轮次不追加记忆内容,命中时最多三条卡片,正文按需读。**
|
|
4
|
+
**Nothing resident: a turn that misses adds no memory text; a hit adds at most three cards, bodies read on demand.**
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
## 它是什么 / What it is
|
|
9
9
|
|
|
10
|
-
给 DeepSeek Harness
|
|
10
|
+
给 DeepSeek Harness 的轻量记忆:全库不进常驻提示、不逐轮注入、不扫描文件。记忆只在两种时刻出现——你说「记一下」,或者这句话跟已有条目真有关(最多三条卡片:标题、摘要、前提,正文仍按需读)。
|
|
11
11
|
|
|
12
|
-
Lightweight memory for the DeepSeek Harness.
|
|
12
|
+
Lightweight memory for the DeepSeek Harness. Nothing resident: no standing prompt, no per-turn injection, no file scanning. Memory appears at exactly two moments: you ask it to remember, or the message genuinely overlaps an existing entry (at most three cards with title, summary and preconditions; bodies are read on demand).
|
|
13
13
|
|
|
14
14
|
## 触发方式 / Triggers
|
|
15
15
|
|
|
@@ -19,7 +19,7 @@ Lightweight memory for the DeepSeek Harness. No standing system prompt, nothing
|
|
|
19
19
|
| **L1** 它发现有跨会话价值的经验 | 一行灰字 `memory · suggested "X"` + 一个「保留」按钮;标题加一句话,看得懂才点,不点就永远不写,48 小时内不重复 |
|
|
20
20
|
| **L2** 常识、一次性参数、凭据的值 | 永不写入 |
|
|
21
21
|
|
|
22
|
-
这些行只属于产生它的那一轮:每轮按那轮自己的工具结果重建,重进会话还能看见,别的轮不会混进来。再记一次时插件自己判是更新还是新增:同层、同前置条件下,标题(或这条用过的旧标题)完全一致就更新原条目,换过说法而标题变了的旧标题记成别名以后也认得。像但不同名的不写:先在结果里回「相近:X
|
|
22
|
+
这些行只属于产生它的那一轮:每轮按那轮自己的工具结果重建,重进会话还能看见,别的轮不会混进来。再记一次时插件自己判是更新还是新增:同层、同前置条件下,标题(或这条用过的旧标题)完全一致就更新原条目,换过说法而标题变了的旧标题记成别名以后也认得。像但不同名的不写:先在结果里回「相近:X」,确认要更新就用那个标题重存,确认是另一条才加 `resolution="distinct"`。更新时没给的字段保持原值,正文整段替换——旧正文只作内部撤回依据,不展示、不参与检索;前置条件不同的同一主题是两条。
|
|
23
23
|
|
|
24
24
|
写入时分两层:默认只在这个项目生效;用户说「以后所有项目都这样」才进全局层(200 条起提示,硬上限 1000 条),检索时一起查。同一主题在不同前提下用 `when` 区分,各存一条。
|
|
25
25
|
|
|
@@ -35,7 +35,7 @@ Lightweight memory for the DeepSeek Harness. No standing system prompt, nothing
|
|
|
35
35
|
| **L1** it spots something worth keeping | one muted line `memory · suggested "X" · sug_x` with a single 「保留」 button — readable before you click, nothing written unless you click, at most once per two days |
|
|
36
36
|
| **L2** common sense, one-off parameters, credential values | never written |
|
|
37
37
|
|
|
38
|
-
Those lines belong to the turn that made them: each turn is rebuilt from its own tool results, so reopening the session shows them again and no other turn leaks in. Saving again updates an entry only when the title (or one of its earlier titles) matches exactly under the same precondition. A merely similar title writes nothing first: the result
|
|
38
|
+
Those lines belong to the turn that made them: each turn is rebuilt from its own tool results, so reopening the session shows them again and no other turn leaks in. Saving again updates an entry only when the title (or one of its earlier titles) matches exactly under the same precondition. A merely similar title writes nothing first: the result names the closest entry, so the caller either re-saves with that title or adds `resolution="distinct"`. An update keeps every field it was not given and replaces the body wholesale; the previous body is kept only for internal undo and never shown or searched. The same subject under different preconditions stays two entries.
|
|
39
39
|
|
|
40
40
|
Writes land in one of two layers: the current project by default, or the global layer (warns past 200, hard ceiling 1000) when the user says the rule holds for every project. Both layers are searched together, and a `when` field separates the same subject under different preconditions.
|
|
41
41
|
|
|
@@ -43,9 +43,9 @@ Before writing, distill: the title is the conclusion and the body keeps only the
|
|
|
43
43
|
|
|
44
44
|
## 点一下就存 / One click, no turn
|
|
45
45
|
|
|
46
|
-
「保留」是同源直写:不产生消息、不动输入框、不打断当前回复。点完按钮消失,行变成 `memory · kept`。真写过的行尾才有
|
|
46
|
+
「保留」是同源直写:不产生消息、不动输入框、不打断当前回复。点完按钮消失,行变成 `memory · kept`。真写过的行尾才有 `✕`,按标题撤销:改过的条目还原成上一版(显示 `restored`),刚建的条目移入归档(显示 `archived`);条目后来又改过就停下并说明已经变过,不覆盖后来的修改。没变、需要确认、保存失败的行没有撤销。
|
|
47
47
|
|
|
48
|
-
The 「保留」 button writes straight to disk from the page: no message, no extra model turn, the input box untouched. It disappears and the row becomes `memory · kept` — that is the whole interaction. Only a row that really changed gets the `✕`, and it undoes by
|
|
48
|
+
The 「保留」 button writes straight to disk from the page: no message, no extra model turn, the input box untouched. It disappears and the row becomes `memory · kept` — that is the whole interaction. Only a row that really changed gets the `✕`, and it undoes by title: an edited entry goes back to its previous body (`restored`), a freshly created one moves to the archive (`archived`); if the entry changed again it stops and says so instead of overwriting. Noop, needs-confirmation and failed saves get no undo.
|
|
49
49
|
|
|
50
50
|
## 每轮引用条 / Per-turn references
|
|
51
51
|
|
|
@@ -57,30 +57,30 @@ Each completed turn keeps a few lines under 「已编辑 N 个文件」, built f
|
|
|
57
57
|
|
|
58
58
|
会话头的大脑图标打开居中的记忆面板。顶部是「启用记忆」「自进化」两个开关,右上角「显示设置」里另有「启用图谱」「启用记忆宫殿」两个只影响展示的开关,默认关闭,按项目记住;内容分「记忆」「自进化」两个页签。默认看当前项目,也可以切到其他项目或全局层;切过去只读,不回写、不迁移旧格式。
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
「记忆」页签里,搜索框右侧可以在「列表」「图谱」「宫殿」之间切换;列表、图谱或宫殿占满内容宽度,选中记忆后右侧才出现正文与归属。当前项目里点「修改」可以就地改标题和正文,保存后版本加一;别的项目和全局层只读。图谱按真实数据画,点记忆节点读正文,点标签或主题节点转成搜索,右上角可缩放。宫殿按类型分四间:习惯与规范、方法与经验、资料与入口、其他记录;一次只展开一间,每间每页 24 条,点展品只读正文。宫殿只决定怎么陈列,不改记忆、学习状态和额度。「自进化」页签只作用于当前项目,按「全部 / 候选 / 试用 / 生效 / 暂停」筛选,也能切列表与图谱;详情给出当前经验、适用范围、当前状态、学习依据和变化记录。候选可采纳、修改、放弃,试用可采纳、修改、暂停,生效可修改、暂停,暂停可恢复或查看记录。
|
|
61
61
|
|
|
62
62
|
The brain icon in the session header opens a centered memory panel. Memory and Self-evolution sit at the top, and Display settings at the top right add two presentation-only switches, Graph and Memory palace, both off by default and remembered per project. Two tabs, Memories and Self-evolution, hold the content. It starts on the current project and can switch to another project or the global layer; switched scopes are read-only and never migrate or create state.
|
|
63
63
|
|
|
64
|
-
On the Memories tab, the search row switches between list, graph and palace; either view fills the content width, and the body pane only appears after a memory is selected. The graph is drawn from real data: a memory node opens its body, a tag or topic hub becomes a search, and the top-right controls zoom it. The palace sorts memories into four rooms by kind: Habits and conventions, Methods and experience, References and entries, and Other notes; one room opens at a time, each page holds 24 items, and an exhibit only opens its body. The palace changes presentation only, never memory, learning state or quota. The Self-evolution tab acts on the current project only, filters by All / Candidate / Trial / Active / Paused, and also offers list and graph views. It shows the current rule, scope, state, evidence and change history. Candidates can be adopted, edited or discarded; trials can be adopted, edited or paused; active rules can be edited or paused; paused rules can be resumed or inspected.
|
|
64
|
+
On the Memories tab, the search row switches between list, graph and palace; either view fills the content width, and the body pane only appears after a memory is selected. In the current project, Edit changes a memory's title and body in place and bumps its revision on save; other projects and the global layer stay read-only. The graph is drawn from real data: a memory node opens its body, a tag or topic hub becomes a search, and the top-right controls zoom it. The palace sorts memories into four rooms by kind: Habits and conventions, Methods and experience, References and entries, and Other notes; one room opens at a time, each page holds 24 items, and an exhibit only opens its body. The palace changes presentation only, never memory, learning state or quota. The Self-evolution tab acts on the current project only, filters by All / Candidate / Trial / Active / Paused, and also offers list and graph views. It shows the current rule, scope, state, evidence and change history. Candidates can be adopted, edited or discarded; trials can be adopted, edited or paused; active rules can be edited or paused; paused rules can be resumed or inspected.
|
|
65
65
|
|
|
66
66
|
- 关闭项目记忆后,这个项目不再使用任何记忆(包括全局层):自动注入、检索、读取、引用、保存全部停,服务端也拒绝记忆工具和面板修改动作。自进化同时关闭并禁用;重新打开记忆后自进化仍为关闭,要单独开启。
|
|
67
67
|
- 关闭只改权限,不删数据:候选、试用、生效、证据、整理额度和默认绑定都保留,计数照常显示。
|
|
68
68
|
- 记忆工具与面板管理动作都在服务端拒绝,不能靠改前端绕过;其他项目不受影响。
|
|
69
69
|
- 范围只来自服务端枚举的层;不接受客户端拼路径,也不能写别的项目。
|
|
70
|
-
-
|
|
70
|
+
- 图谱只按数据画:每条有效记忆一个节点,共享标签和主题做枢纽,显式关联画实线;自进化图谱只连共用条件。位置由条目自身算出来,同一批数据每次一样。
|
|
71
71
|
|
|
72
72
|
- Turning project memory off stops all memory use in that project, including the global layer: no injection, search, read, citation or save, and the server rejects both memory tools and panel mutations. Self-evolution turns off with it; after memory is reopened, self-evolution stays off until switched on.
|
|
73
73
|
- Closing is a permission change, not deletion: candidates, trials, active rules, evidence, organize quota and default bindings are kept, and counts stay visible.
|
|
74
74
|
- Memory tools and panel management actions are rejected server-side, so a modified client cannot bypass the switch; other projects are unaffected.
|
|
75
75
|
- Scopes come only from the server-side list; the page cannot assemble a path or write another project's notes.
|
|
76
|
-
- The graph is data only: one node per active memory, shared tags and topics as hubs, explicit links as solid edges; positions derive from
|
|
76
|
+
- The graph is data only: one node per active memory, shared tags and topics as hubs, explicit links as solid edges; positions derive from each entry, so the same data looks the same every time.
|
|
77
77
|
|
|
78
78
|
## 为什么轻 / Why it stays light
|
|
79
79
|
|
|
80
80
|
- **0 依赖**,存储层无定时器、无监听、无网络,只有同步单文件写;写入用一把有超时的锁,别的进程正在写就明确报「忙」,不抢别人的锁。
|
|
81
81
|
- 搜索最多 4 条、400 字;正文只在 `memory_read` 里出现,全文永不进上下文。
|
|
82
82
|
- 普通检索只列当前有效的记录;已经归档或被更新替换的旧说法不混进来,要查历史得在 `memory_search` 里显式要 `history`。
|
|
83
|
-
- 上限:单条正文 4096 字节、整库正文 1 MB、每层 1000 条、标题与摘要各 60 字;每层 200 条只当提示线。90
|
|
83
|
+
- 上限:单条正文 4096 字节、整库正文 1 MB、每层 1000 条、标题与摘要各 60 字;每层 200 条只当提示线。90 天未用只留标题与版本。到了硬上限不拒绝新记忆:把同层最久没被读的一条移进归档,用户固定过的规则不动,保存结果里写明是哪条(正文与完整记录都留着,可人工取回),单条本身超预算才报错;用户明确说「别记」也进归档。文件从不删除。
|
|
84
84
|
- 一个项目一个目录,项目之间互相看不见。
|
|
85
85
|
|
|
86
86
|
- **Zero dependencies.** The store has no timers, no listeners, no network — just synchronous single-file writes, guarded by a lock that gives up after 500 ms instead of stealing it.
|
package/TRIGGERS.md
CHANGED
package/client.js
CHANGED
|
@@ -82,16 +82,15 @@ window.__ModuleLoader__.load({
|
|
|
82
82
|
return meta;
|
|
83
83
|
};
|
|
84
84
|
|
|
85
|
-
const isMemoryId = (value) => typeof value === 'string' && /^mem_[a-z0-9]{6,}$/.test(value);
|
|
86
85
|
const isCandidateId = (value) => typeof value === 'string' && /^sug_[a-z0-9]{6,}$/.test(value);
|
|
87
86
|
|
|
88
|
-
/**
|
|
87
|
+
/** 一条引用的规范形态:只有标题、范围与版次;id 不进界面。 */
|
|
89
88
|
const citationOf = (meta, kind) => {
|
|
90
|
-
if (!meta || !
|
|
89
|
+
if (!meta || typeof meta.title !== 'string' || !meta.title.trim()) return null;
|
|
91
90
|
if (kind === 'memory-save' && meta.changed !== true) return null;
|
|
92
91
|
return {
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
title: meta.title,
|
|
93
|
+
key: `${meta.scope === 'global' ? 'global' : 'project'}\u0000${meta.title}`, // ui-copy-ignore
|
|
95
94
|
scope: meta.scope === 'global' ? 'global' : 'project',
|
|
96
95
|
revision: Number.isInteger(meta.revision) ? meta.revision : 1,
|
|
97
96
|
status: typeof meta.status === 'string' ? meta.status : 'ok',
|
|
@@ -158,7 +157,7 @@ window.__ModuleLoader__.load({
|
|
|
158
157
|
const seen = new Map();
|
|
159
158
|
const out = [];
|
|
160
159
|
for (const item of items) {
|
|
161
|
-
const key = `${item.scope}:${item.
|
|
160
|
+
const key = `${item.scope}:${item.key}:${item.exposure}`;
|
|
162
161
|
const at = seen.get(key);
|
|
163
162
|
if (at === undefined) {
|
|
164
163
|
seen.set(key, out.length);
|
|
@@ -258,9 +257,51 @@ window.__ModuleLoader__.load({
|
|
|
258
257
|
};
|
|
259
258
|
};
|
|
260
259
|
|
|
260
|
+
/** 一行展示的身份:曝光类型 + 文案 + 正文;拦截结果没有 id,只能按内容认。 */
|
|
261
|
+
const rowKeyOf = (toolName, block) => {
|
|
262
|
+
const text = settledText(block);
|
|
263
|
+
return text ? `${toolName}|${text}` : '';
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
/** 同一组行每次出现的去留:同键只留第一次。 */
|
|
267
|
+
const rowKeepFlags = (keys) => {
|
|
268
|
+
const seen = new Set();
|
|
269
|
+
return keys.map((key) => {
|
|
270
|
+
if (!key) return true;
|
|
271
|
+
if (seen.has(key)) return false;
|
|
272
|
+
seen.add(key);
|
|
273
|
+
return true;
|
|
274
|
+
});
|
|
275
|
+
};
|
|
276
|
+
|
|
261
277
|
const Row = (props) => {
|
|
262
278
|
const line = props.phase === 'preparing' ? null : firstLine(settledText(props.block));
|
|
263
|
-
|
|
279
|
+
const key = props.phase === 'preparing' ? '' : rowKeyOf(props.toolName, props.block);
|
|
280
|
+
const nodeRef = React.useRef(null);
|
|
281
|
+
const [dup, setDup] = React.useState(false);
|
|
282
|
+
const useLayout = React.useLayoutEffect || React.useEffect;
|
|
283
|
+
useLayout(() => {
|
|
284
|
+
const node = nodeRef.current;
|
|
285
|
+
if (!key || !node || typeof node.closest !== 'function') return;
|
|
286
|
+
const turnBox = node.closest('[data-chat-turn]');
|
|
287
|
+
const turn = turnBox ? turnBox.getAttribute('data-chat-turn') : null;
|
|
288
|
+
const rootScope = node.ownerDocument || node.parentElement;
|
|
289
|
+
if (!rootScope || typeof rootScope.querySelectorAll !== 'function') return;
|
|
290
|
+
const peers = Array.prototype.filter.call(rootScope.querySelectorAll('[data-memory-row]'), (peer) => {
|
|
291
|
+
if (!turn) return peer.parentElement === node.parentElement;
|
|
292
|
+
const box = peer.closest('[data-chat-turn]');
|
|
293
|
+
return box !== null && box.getAttribute('data-chat-turn') === turn;
|
|
294
|
+
});
|
|
295
|
+
const at = peers.indexOf(node);
|
|
296
|
+
if (at < 0) return;
|
|
297
|
+
setDup(!rowKeepFlags(peers.map((peer) => peer.getAttribute('data-memory-row') || ''))[at]);
|
|
298
|
+
}, [key]);
|
|
299
|
+
return h('div', {
|
|
300
|
+
ref: nodeRef,
|
|
301
|
+
'data-tool': props.toolName,
|
|
302
|
+
'data-memory-row': key || undefined,
|
|
303
|
+
style: dup ? { ...ROW, display: 'none' } : ROW,
|
|
304
|
+
}, h('span', { style: DOT }, '·'), h('span', { style: LABEL }, line ?? RUNNING[props.toolName] ?? 'memory'));
|
|
264
305
|
};
|
|
265
306
|
|
|
266
307
|
/** 建议行:候选 id 来自持久元数据,不读文案。 */
|
|
@@ -298,7 +339,7 @@ window.__ModuleLoader__.load({
|
|
|
298
339
|
const undoRefOf = (meta, cwd) => (meta
|
|
299
340
|
&& meta.kind === 'memory-save'
|
|
300
341
|
&& meta.changed === true
|
|
301
|
-
&&
|
|
342
|
+
&& typeof meta.title === 'string' && meta.title.trim().length > 0
|
|
302
343
|
&& Number.isInteger(meta.revision)
|
|
303
344
|
&& typeof cwd === 'string'
|
|
304
345
|
&& cwd.length > 0
|
|
@@ -317,7 +358,7 @@ window.__ModuleLoader__.load({
|
|
|
317
358
|
const drop = () => {
|
|
318
359
|
if (!canDrop) return;
|
|
319
360
|
setState('saving');
|
|
320
|
-
post('/lite-memory/undo', { op: 'undo',
|
|
361
|
+
post('/lite-memory/undo', { op: 'undo', title: changed.title, revision: changed.revision, cwd }).then((result) => {
|
|
321
362
|
if (succeeded(result)) {
|
|
322
363
|
setState('dropped');
|
|
323
364
|
setNote(result.body && result.body.mode === 'restored' ? 'memory · restored' : 'memory · archived');
|
|
@@ -557,16 +598,15 @@ window.__ModuleLoader__.load({
|
|
|
557
598
|
return '';
|
|
558
599
|
};
|
|
559
600
|
const refMetaOf = (item) => {
|
|
560
|
-
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
: [item.scope === 'global' ? '全局' : '项目', item.origin === 'project_evolution' ? '自进化' : '你记的'];
|
|
601
|
+
/** 会话里只标层与来源:类型留给面板和工具,不占引用行的宽度。 */
|
|
602
|
+
const layer = `${item.scope === 'global' ? '[全局]' : '[项目]'}${item.origin === 'project_evolution' ? '[自进化]' : ''}`;
|
|
603
|
+
const parts = [];
|
|
564
604
|
const ago = agoLabel(item.updated);
|
|
565
605
|
if (ago) parts.push(ago);
|
|
566
606
|
if (item.status === 'archived') parts.push('已归档');
|
|
567
607
|
const processing = PROCESSING_LABEL[item.processingStatus] || '';
|
|
568
608
|
if (processing) parts.push(processing);
|
|
569
|
-
return parts.join(' · ');
|
|
609
|
+
return [layer, ...parts].join(' · ');
|
|
570
610
|
};
|
|
571
611
|
|
|
572
612
|
const RefChevron = (props) => h('svg', {
|
|
@@ -634,28 +674,28 @@ window.__ModuleLoader__.load({
|
|
|
634
674
|
setOpen(false);
|
|
635
675
|
};
|
|
636
676
|
const show = (item) => {
|
|
637
|
-
if (shown && shown.
|
|
677
|
+
if (shown && shown.key === item.key) {
|
|
638
678
|
close();
|
|
639
679
|
return;
|
|
640
680
|
}
|
|
641
681
|
const request = generation.current + 1;
|
|
642
682
|
generation.current = request;
|
|
643
683
|
setOpen(true);
|
|
644
|
-
setShown({
|
|
684
|
+
setShown({ key: item.key, ref: item, loading: true });
|
|
645
685
|
setNote('');
|
|
646
|
-
post('/lite-memory/body', {
|
|
686
|
+
post('/lite-memory/body', { title: item.title, scope: item.scope, revision: item.revision, sessionId }).then((result) => {
|
|
647
687
|
if (!alive.current || generation.current !== request) return;
|
|
648
688
|
const payload = result.ok && result.body ? result.body : null;
|
|
649
689
|
if (!payload || payload.ok !== true) {
|
|
650
|
-
setShown({
|
|
690
|
+
setShown({ key: item.key, ref: item, invalid: true });
|
|
651
691
|
return;
|
|
652
692
|
}
|
|
653
693
|
if (payload.status === 'gone') {
|
|
654
|
-
setShown({
|
|
694
|
+
setShown({ key: item.key, ref: item, gone: true });
|
|
655
695
|
return;
|
|
656
696
|
}
|
|
657
697
|
setShown({
|
|
658
|
-
|
|
698
|
+
key: item.key,
|
|
659
699
|
ref: item,
|
|
660
700
|
title: typeof payload.title === 'string' ? payload.title : '',
|
|
661
701
|
when: typeof payload.when === 'string' ? payload.when : '',
|
|
@@ -671,11 +711,11 @@ window.__ModuleLoader__.load({
|
|
|
671
711
|
});
|
|
672
712
|
};
|
|
673
713
|
const rows = open ? items.map((item) => h(RefRow, {
|
|
674
|
-
key: `${item.
|
|
675
|
-
item, tone, active: Boolean(shown && shown.
|
|
714
|
+
key: `${item.key}|${item.revision}`, // ui-copy-ignore
|
|
715
|
+
item, tone, active: Boolean(shown && shown.key === item.key),
|
|
676
716
|
onShow: () => show(item),
|
|
677
717
|
})) : [];
|
|
678
|
-
const detail = shown && shown.ref && items.some((item) => item.
|
|
718
|
+
const detail = shown && shown.ref && items.some((item) => item.key === shown.key && item.revision === shown.ref.revision)
|
|
679
719
|
? h('div', { style: { ...REF_BODY, borderLeft: `2px solid ${tone}` } },
|
|
680
720
|
shown.loading ? h('span', { style: REF_META }, '读取中…') : null,
|
|
681
721
|
shown.gone ? h('span', { style: REF_META }, '这条记忆已删除') : null,
|
|
@@ -830,6 +870,24 @@ window.__ModuleLoader__.load({
|
|
|
830
870
|
explainBus.add(listener);
|
|
831
871
|
return () => explainBus.delete(listener);
|
|
832
872
|
};
|
|
873
|
+
/**
|
|
874
|
+
* 面板只说判断结果,不说内部原因码;条数来自上一次自动召回的回执。
|
|
875
|
+
*/
|
|
876
|
+
const recallLineOf = (receipt) => {
|
|
877
|
+
if (!receipt || typeof receipt !== 'object') return '';
|
|
878
|
+
const cards = Number.isInteger(receipt.cards) ? receipt.cards : 0;
|
|
879
|
+
const matched = Number.isInteger(receipt.matched) ? receipt.matched : 0;
|
|
880
|
+
const related = matched > 0 ? `${matched} 条相关记忆,` : '';
|
|
881
|
+
switch (receipt.reason) {
|
|
882
|
+
case 'hit': return cards > 0 ? `上次自动提醒 ${cards} 条` : '';
|
|
883
|
+
case 'budget': return `上次没提醒,${related}本轮自动提醒已用满`;
|
|
884
|
+
case 'needs_context': return `上次没提醒,${related}适用条件未确认`;
|
|
885
|
+
case 'no_overlap': return '上次没提醒,没有匹配的记忆';
|
|
886
|
+
case 'memory_off': return '上次没提醒,记忆已关闭';
|
|
887
|
+
case 'tools_only': return '上次没提醒,只在你问时检索';
|
|
888
|
+
default: return '';
|
|
889
|
+
}
|
|
890
|
+
};
|
|
833
891
|
/** 功能关闭时视图统一回列表,不保留一个画不出来的页签。 */
|
|
834
892
|
const graphViewOf = (enabled, view) => (enabled === true && view === 'graph' ? 'graph' : 'list');
|
|
835
893
|
const viewModeOf = (display, view) => {
|
|
@@ -1583,13 +1641,15 @@ window.__ModuleLoader__.load({
|
|
|
1583
1641
|
},
|
|
1584
1642
|
}));
|
|
1585
1643
|
|
|
1586
|
-
const STAGE = { pending: '候选', trial: '试用', active: '生效', paused: '暂停' };
|
|
1644
|
+
const STAGE = { pending: '候选', trial: '试用', active: '生效', paused: '暂停', legacy: '待复核' };
|
|
1587
1645
|
const STAGE_TONE = {
|
|
1588
1646
|
pending: 'var(--dsw-alias-state-warn-primary)',
|
|
1589
1647
|
trial: 'var(--dsw-alias-brand-primary)',
|
|
1590
1648
|
active: 'var(--dsw-alias-state-success-primary)',
|
|
1591
1649
|
paused: 'var(--dsw-alias-label-secondary)',
|
|
1650
|
+
legacy: 'var(--dsw-alias-state-warn-primary)',
|
|
1592
1651
|
};
|
|
1652
|
+
const stageOf = (rule) => (rule.reviewState === 'legacy_unreviewed' ? 'legacy' : rule.state);
|
|
1593
1653
|
const StagePill = (props) => h('span', {
|
|
1594
1654
|
style: {
|
|
1595
1655
|
flex: '0 0 auto', padding: '0 6px', border: '1px solid var(--dsw-alias-border-l1)',
|
|
@@ -1626,6 +1686,7 @@ window.__ModuleLoader__.load({
|
|
|
1626
1686
|
const usabilityOf = (rule, evolution, memoryOn) => {
|
|
1627
1687
|
if (!memoryOn) return '记忆已关闭,暂不参与回答。';
|
|
1628
1688
|
if (!evolution || evolution.enabled !== true || evolution.effectiveEnabled === false) return '自进化已关闭,暂不参与回答。';
|
|
1689
|
+
if (rule.reviewState === 'legacy_unreviewed') return '所属项目还没确认,暂不参与回答。';
|
|
1629
1690
|
if (rule.state === 'paused') return '已暂停,不参与回答。';
|
|
1630
1691
|
const unresolved = rule.applicability === 'unresolved'
|
|
1631
1692
|
|| (rule.applicability === 'conditional' && (!Array.isArray(rule.conditions) || rule.conditions.length === 0));
|
|
@@ -1709,7 +1770,7 @@ window.__ModuleLoader__.load({
|
|
|
1709
1770
|
},
|
|
1710
1771
|
h('span', { style: { fontSize: '12px', lineHeight: '18px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, rule.title),
|
|
1711
1772
|
h('span', { style: { ...PANEL_MUTED, display: 'flex', alignItems: 'center', gap: '6px', minWidth: 0, lineHeight: '16px' } },
|
|
1712
|
-
h(StagePill, { state: rule
|
|
1773
|
+
h(StagePill, { state: stageOf(rule) }),
|
|
1713
1774
|
h('span', { style: { marginLeft: 'auto', flex: '0 0 auto', fontVariantNumeric: 'tabular-nums' } }, EVO_DATE(rule.updated)),
|
|
1714
1775
|
));
|
|
1715
1776
|
};
|
|
@@ -1748,7 +1809,7 @@ window.__ModuleLoader__.load({
|
|
|
1748
1809
|
? h('div', { style: { display: 'flex', alignItems: 'center', gap: '12px', flexWrap: 'wrap', paddingTop: '3px' } },
|
|
1749
1810
|
rule.state === 'pending' || rule.state === 'trial'
|
|
1750
1811
|
? h(Action, { disabled: !canManage, onClick: () => act('adopt') }, '采纳') : null,
|
|
1751
|
-
rule.state === 'paused'
|
|
1812
|
+
rule.state === 'paused' && rule.reviewState !== 'legacy_unreviewed'
|
|
1752
1813
|
? h(Action, { disabled: !canManage, onClick: () => act('resume') }, '恢复')
|
|
1753
1814
|
: (rule.state === 'trial' || rule.state === 'active'
|
|
1754
1815
|
? h(Action, { disabled: !canManage, onClick: () => act('pause') }, '暂停')
|
|
@@ -1835,7 +1896,7 @@ window.__ModuleLoader__.load({
|
|
|
1835
1896
|
h('div', { style: { display: 'flex', flexDirection: 'column', gap: '4px' } },
|
|
1836
1897
|
h('div', { style: { display: 'flex', alignItems: 'center', gap: '6px' } },
|
|
1837
1898
|
h('span', { style: PANEL_LABEL }, '当前状态'),
|
|
1838
|
-
h(StagePill, { state: rule
|
|
1899
|
+
h(StagePill, { state: stageOf(rule) }),
|
|
1839
1900
|
),
|
|
1840
1901
|
h('div', { style: { fontSize: '12px', lineHeight: '1.55', opacity: 0.82 } }, usabilityOf(rule, props.evolution, props.memoryOn)),
|
|
1841
1902
|
failureState
|
|
@@ -2041,6 +2102,10 @@ window.__ModuleLoader__.load({
|
|
|
2041
2102
|
const [selectedRule, setSelectedRule] = React.useState('');
|
|
2042
2103
|
const [detail, setDetail] = React.useState(null);
|
|
2043
2104
|
const [detailNote, setDetailNote] = React.useState('');
|
|
2105
|
+
const [editingMemory, setEditingMemory] = React.useState(false);
|
|
2106
|
+
const [editTitle, setEditTitle] = React.useState('');
|
|
2107
|
+
const [editBody, setEditBody] = React.useState('');
|
|
2108
|
+
const [editNote, setEditNote] = React.useState('');
|
|
2044
2109
|
const [busy, setBusy] = React.useState(false);
|
|
2045
2110
|
const [showReason, setShowReason] = React.useState(false);
|
|
2046
2111
|
const [showSettings, setShowSettings] = React.useState(false);
|
|
@@ -2061,6 +2126,8 @@ window.__ModuleLoader__.load({
|
|
|
2061
2126
|
const load = (scopeKey = '', focusRule = '') => {
|
|
2062
2127
|
const request = generation.current + 1;
|
|
2063
2128
|
generation.current = request;
|
|
2129
|
+
/** 用户选的项目立刻算数:晚到的保存/开关回执不能再把界面拉回旧范围。 */
|
|
2130
|
+
if (scopeKey) scopeRef.current = scopeKey;
|
|
2064
2131
|
setLoading(true);
|
|
2065
2132
|
setFailed(false);
|
|
2066
2133
|
setNote('');
|
|
@@ -2118,6 +2185,8 @@ window.__ModuleLoader__.load({
|
|
|
2118
2185
|
}, []);
|
|
2119
2186
|
|
|
2120
2187
|
React.useEffect(() => {
|
|
2188
|
+
setEditingMemory(false);
|
|
2189
|
+
setEditNote('');
|
|
2121
2190
|
if (!selectedMemory || !data) { setDetail(null); setDetailNote(''); return undefined; }
|
|
2122
2191
|
let live = true;
|
|
2123
2192
|
setDetail(null);
|
|
@@ -2183,7 +2252,7 @@ window.__ModuleLoader__.load({
|
|
|
2183
2252
|
} else if (activityCode === 'scope_unconfirmed' || activityCode === 'unknown_scope') {
|
|
2184
2253
|
activityText = '暂未整理:适用范围待确认';
|
|
2185
2254
|
} else if (activityCode === 'quota' || activityCode === 'exhausted') {
|
|
2186
|
-
activityText = '
|
|
2255
|
+
activityText = '这轮先整理这些,下一条消息接着整理';
|
|
2187
2256
|
} else if (activityCode === 'failed' || activityCode === 'error') {
|
|
2188
2257
|
activityText = staleLearning ? '这轮整理没采用' : '最近整理失败,可查看原因';
|
|
2189
2258
|
} else if (activityCode === 'waiting' || activityCode === 'idle' || activityCode === 'empty') {
|
|
@@ -2263,18 +2332,64 @@ window.__ModuleLoader__.load({
|
|
|
2263
2332
|
};
|
|
2264
2333
|
if (learningAction === 'pause') payload.ids = [selectedRuleRow.id];
|
|
2265
2334
|
post('/lite-memory/evolution', payload).then((result) => {
|
|
2266
|
-
if (!alive.current) return;
|
|
2335
|
+
if (!alive.current || (selectedKey && scopeRef.current !== selectedKey)) return;
|
|
2267
2336
|
setBusy(false);
|
|
2268
2337
|
if (succeeded(result)) load(selectedKey, selectedRuleRow.id);
|
|
2269
2338
|
else setNote(failure(result, '这次没有改动'));
|
|
2270
2339
|
});
|
|
2271
2340
|
};
|
|
2272
2341
|
|
|
2342
|
+
/** 普通记忆的正文由人直接改:带读到的版本写,冲突就提示刷新,不覆盖别处的修改。 */
|
|
2343
|
+
const saveMemoryEdit = (record) => {
|
|
2344
|
+
if (busy || !canManage || !record || !record.id) return;
|
|
2345
|
+
const title = editTitle.trim();
|
|
2346
|
+
const text = editBody.trim();
|
|
2347
|
+
if (!title || !text) return;
|
|
2348
|
+
setBusy(true);
|
|
2349
|
+
setEditNote('');
|
|
2350
|
+
const payload = { sessionId, scopeKey: selectedKey, id: record.id, title, body: text, expectedRevision: record.revision };
|
|
2351
|
+
post('/lite-memory/entry', payload).then((result) => {
|
|
2352
|
+
if (!alive.current || (selectedKey && scopeRef.current !== selectedKey)) return;
|
|
2353
|
+
setBusy(false);
|
|
2354
|
+
if (succeeded(result)) {
|
|
2355
|
+
const nextTitle = result.body && typeof result.body.title === 'string' ? result.body.title : title;
|
|
2356
|
+
const nextRevision = result.body && Number.isInteger(result.body.revision) ? result.body.revision : record.revision;
|
|
2357
|
+
setEditingMemory(false);
|
|
2358
|
+
setDetail((prev) => (prev ? { ...prev, title: nextTitle, body: text, currentBody: text, revision: nextRevision } : prev));
|
|
2359
|
+
setData((prev) => {
|
|
2360
|
+
if (!prev || !prev.selected || !Array.isArray(prev.selected.records)) return prev;
|
|
2361
|
+
const records = prev.selected.records.map((row) => (row.id === record.id ? { ...row, title: nextTitle, revision: nextRevision } : row));
|
|
2362
|
+
return { ...prev, selected: { ...prev.selected, records } };
|
|
2363
|
+
});
|
|
2364
|
+
setEditNote('');
|
|
2365
|
+
} else {
|
|
2366
|
+
setEditNote(failure(result, '这次没有改动'));
|
|
2367
|
+
/**
|
|
2368
|
+
* 版本过期不是失败终点:把最新正文和版本取回来,草稿留着,
|
|
2369
|
+
* 人确认后再按一次保存就写在最新版本上。
|
|
2370
|
+
*/
|
|
2371
|
+
if (result.status === 409 && result.body && result.body.retry) {
|
|
2372
|
+
post('/lite-memory/body', { sessionId, id: record.id, scopeKey: selectedKey }).then((fresh) => {
|
|
2373
|
+
if (!alive.current || (selectedKey && scopeRef.current !== selectedKey)
|
|
2374
|
+
|| !fresh.ok || !fresh.body || !fresh.body.ok || fresh.body.status === 'gone') return;
|
|
2375
|
+
const revision = Number.isInteger(fresh.body.revision) ? fresh.body.revision : 0;
|
|
2376
|
+
setDetail(fresh.body);
|
|
2377
|
+
setData((prev) => {
|
|
2378
|
+
if (!prev || !prev.selected || !Array.isArray(prev.selected.records)) return prev;
|
|
2379
|
+
const records = prev.selected.records.map((row) => (row.id === record.id ? { ...row, revision: revision || row.revision } : row));
|
|
2380
|
+
return { ...prev, selected: { ...prev.selected, records } };
|
|
2381
|
+
});
|
|
2382
|
+
});
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
});
|
|
2386
|
+
};
|
|
2387
|
+
|
|
2273
2388
|
const toggleControl = () => {
|
|
2274
2389
|
if (busy || !canToggle) return;
|
|
2275
2390
|
setBusy(true);
|
|
2276
2391
|
post('/lite-memory/control', { sessionId, enabled: !memoryOn, operationId: newOperationId() }).then((result) => {
|
|
2277
|
-
if (!alive.current) return;
|
|
2392
|
+
if (!alive.current || (selectedKey && scopeRef.current !== selectedKey)) return;
|
|
2278
2393
|
setBusy(false);
|
|
2279
2394
|
if (succeeded(result)) load(selectedKey);
|
|
2280
2395
|
else setNote(failure(result, '开关没有改动'));
|
|
@@ -2285,7 +2400,7 @@ window.__ModuleLoader__.load({
|
|
|
2285
2400
|
if (busy || !evolution || !canToggle || !memoryOn) return;
|
|
2286
2401
|
setBusy(true);
|
|
2287
2402
|
post('/lite-memory/evolution', { sessionId, enabled: evolution.enabled !== true, operationId: newOperationId() }).then((result) => {
|
|
2288
|
-
if (!alive.current) return;
|
|
2403
|
+
if (!alive.current || (selectedKey && scopeRef.current !== selectedKey)) return;
|
|
2289
2404
|
setBusy(false);
|
|
2290
2405
|
if (succeeded(result)) load(selectedKey);
|
|
2291
2406
|
else setNote(failure(result, '开关没有改动'));
|
|
@@ -2354,6 +2469,7 @@ window.__ModuleLoader__.load({
|
|
|
2354
2469
|
),
|
|
2355
2470
|
hint ? h('span', { style: { ...PANEL_MUTED, lineHeight: '16px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, hint) : null);
|
|
2356
2471
|
const evolutionOn = Boolean(memoryOn && evolution && evolution.enabled === true);
|
|
2472
|
+
const recallLine = recallLineOf(current && current.recall);
|
|
2357
2473
|
const switchBar = h('div', {
|
|
2358
2474
|
style: { display: 'flex', flexDirection: 'column', gap: '3px', padding: '7px 18px 8px', borderBottom: '1px solid var(--dsw-alias-border-l1)', flex: '0 0 auto' },
|
|
2359
2475
|
},
|
|
@@ -2368,6 +2484,7 @@ window.__ModuleLoader__.load({
|
|
|
2368
2484
|
),
|
|
2369
2485
|
!globalView && !canToggle ? h('div', { style: { ...PANEL_MUTED, lineHeight: '18px' } }, '只读,切换回当前项目后可修改') : null,
|
|
2370
2486
|
note ? h('div', { style: { ...PANEL_MUTED, lineHeight: '18px', color: 'var(--dsw-alias-state-error-primary)' } }, note) : null,
|
|
2487
|
+
!globalView && recallLine ? h('div', { style: { ...PANEL_MUTED, lineHeight: '18px' } }, recallLine) : null,
|
|
2371
2488
|
);
|
|
2372
2489
|
|
|
2373
2490
|
const switchTab = (key) => {
|
|
@@ -2405,6 +2522,25 @@ window.__ModuleLoader__.load({
|
|
|
2405
2522
|
}));
|
|
2406
2523
|
|
|
2407
2524
|
const detailProcessing = detail ? processingStatusOf(detail) : '';
|
|
2525
|
+
const memoryEditor = editingMemory && detailRecord
|
|
2526
|
+
? h('div', { style: { display: 'flex', flexDirection: 'column', gap: '8px' } },
|
|
2527
|
+
h(Field, { label: '标题', value: editTitle, onChange: setEditTitle }),
|
|
2528
|
+
h('label', { style: { display: 'flex', flexDirection: 'column', gap: '4px' } },
|
|
2529
|
+
h('span', { style: PANEL_LABEL }, '经验'),
|
|
2530
|
+
h('textarea', {
|
|
2531
|
+
value: editBody, onChange: (event) => setEditBody(event.target.value), 'aria-label': '经验',
|
|
2532
|
+
rows: 8,
|
|
2533
|
+
style: { ...PANEL_INPUT, height: 'auto', minHeight: '150px', padding: '7px 9px', resize: 'vertical', lineHeight: '1.6' },
|
|
2534
|
+
}),
|
|
2535
|
+
),
|
|
2536
|
+
editNote ? h('div', { style: { ...PANEL_MUTED, color: 'var(--dsw-alias-label-error)' } }, editNote) : null,
|
|
2537
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: '12px', flexWrap: 'wrap' } },
|
|
2538
|
+
h(Action, { disabled: busy || !editTitle.trim() || !editBody.trim(), onClick: () => saveMemoryEdit(detailRecord) }, '保存'),
|
|
2539
|
+
h(Action, { disabled: busy, onClick: () => { setEditingMemory(false); setEditNote(''); } }, '取消'),
|
|
2540
|
+
),
|
|
2541
|
+
)
|
|
2542
|
+
: null;
|
|
2543
|
+
|
|
2408
2544
|
const memoryDetail = selectedMemory
|
|
2409
2545
|
? h('div', { style: { ...PANEL_CARD, minHeight: 0, overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '6px' } },
|
|
2410
2546
|
detail
|
|
@@ -2417,8 +2553,22 @@ window.__ModuleLoader__.load({
|
|
|
2417
2553
|
Array.isArray(detailRecord && detailRecord.tags) && detailRecord.tags.length
|
|
2418
2554
|
? h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '4px' } },
|
|
2419
2555
|
detailRecord.tags.slice(0, 6).map((tag) => h('span', { key: tag, style: { ...PANEL_MUTED, padding: '0 6px', border: '1px solid var(--dsw-alias-border-l1)', borderRadius: '999px' } }, `#${tag}`))) : null,
|
|
2420
|
-
|
|
2421
|
-
h(
|
|
2556
|
+
memoryEditor,
|
|
2557
|
+
editingMemory ? null : h('div', { style: { fontSize: '12px', lineHeight: '1.65', whiteSpace: 'pre-wrap', wordBreak: 'break-word' } }, currentBodyOf(detail) || '没有正文'),
|
|
2558
|
+
editingMemory ? null : h(DetailExtras, { payload: detail }),
|
|
2559
|
+
!editingMemory && detailRecord && canManage
|
|
2560
|
+
? h('div', { style: { display: 'flex', alignItems: 'center', gap: '12px', flexWrap: 'wrap', paddingTop: '2px' } },
|
|
2561
|
+
h(Action, {
|
|
2562
|
+
disabled: busy,
|
|
2563
|
+
onClick: () => {
|
|
2564
|
+
setEditTitle(detailRecord.title || detail.title || '');
|
|
2565
|
+
setEditBody(currentBodyOf(detail) || '');
|
|
2566
|
+
setEditNote('');
|
|
2567
|
+
setEditingMemory(true);
|
|
2568
|
+
},
|
|
2569
|
+
}, '修改'),
|
|
2570
|
+
)
|
|
2571
|
+
: null,
|
|
2422
2572
|
)
|
|
2423
2573
|
: h('div', { style: PANEL_MUTED }, detailNote || '读取中…'),
|
|
2424
2574
|
)
|
|
@@ -2700,7 +2850,7 @@ window.__ModuleLoader__.load({
|
|
|
2700
2850
|
|
|
2701
2851
|
// 测试缝:Node 里验引用投影,浏览器里没有任何副作用。
|
|
2702
2852
|
if (typeof globalThis.__LITE_MEMORY_TEST__ === 'function') {
|
|
2703
|
-
globalThis.__LITE_MEMORY_TEST__({ metaOf, citationOf, citationsOfMeta, citationsOfTurn, dedupe, undoRefOf, graphOf, ruleGraphOf, processingStatusOf, currentBodyOf, historyDiffsOf, MemoryPanel, MemoryGraph, MemoryPalace, TurnMemory, RefSection, displayKeyOf, readDisplaySettings, writeDisplaySettings, graphViewOf, viewModeOf, filterRecords, projectionOf, failureStateOf, evidenceTextOf, evidenceMetaOf, palaceRoomsOf, palaceRoomPageOf, palacePageWindow, palaceToggleOf, palaceMotionOf, palaceRoomIdOf, refMetaOf, explainKeyOf, explainModeOf, setExplainMode, agoLabel, checkupOf, titleCountsOf });
|
|
2853
|
+
globalThis.__LITE_MEMORY_TEST__({ metaOf, citationOf, citationsOfMeta, citationsOfTurn, dedupe, undoRefOf, graphOf, ruleGraphOf, processingStatusOf, currentBodyOf, historyDiffsOf, MemoryPanel, MemoryGraph, MemoryPalace, TurnMemory, RefSection, displayKeyOf, readDisplaySettings, writeDisplaySettings, graphViewOf, viewModeOf, filterRecords, projectionOf, failureStateOf, evidenceTextOf, evidenceMetaOf, palaceRoomsOf, palaceRoomPageOf, palacePageWindow, palaceToggleOf, palaceMotionOf, palaceRoomIdOf, refMetaOf, explainKeyOf, explainModeOf, setExplainMode, recallLineOf, agoLabel, checkupOf, titleCountsOf, rowKeyOf, rowKeepFlags, Row });
|
|
2704
2854
|
}
|
|
2705
2855
|
|
|
2706
2856
|
return {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# 0.18.10:编号不再出现在任何看得见的地方
|
|
2
|
+
|
|
3
|
+
## 编号只留作底层实现
|
|
4
|
+
|
|
5
|
+
0.18.9 把更新、关联、同主题的句柄换成了标题,但编号还会从别处漏出来:保存结果、搜索结构化结果、
|
|
6
|
+
召回卡片、面板引用、撤销回执、读正文回执。这一版把这些出口全部收口,模型和面板只看得到标题、
|
|
7
|
+
范围、版本号:
|
|
8
|
+
|
|
9
|
+
- 工具返回与结构化结果(`presentationMeta`、搜索 `refs`)不再带 `id`;`refs` 用标题、范围、类型表达。
|
|
10
|
+
- 召回卡片只带标题、范围、版本;面板引用按 `范围 + 标题 + 暴露位置` 去重,不再依赖编号。
|
|
11
|
+
- 撤销回执只回标题、版本、结果(`restored` / `archived`);撤销请求本身也只收标题 + 版本。
|
|
12
|
+
- 读正文回执不再带 `id`;面板按标题读正文,`sessionId` / `cwd` / `scopeKey` 的可见范围不变。
|
|
13
|
+
- 兼容:旧的 `args.id` 调用仍能解析(内部路径),但任何结果都不回显编号;参数表里没有编号字段。
|
|
14
|
+
|
|
15
|
+
## 为什么必须收干净
|
|
16
|
+
|
|
17
|
+
编号一旦在结果里出现,模型就会把它当句柄复用;参数表不再收编号时,它只能去翻 `state.json`,
|
|
18
|
+
既费 token 又容易读错层。把编号从「结果」和「参数」两侧同时拿掉,标题成为唯一对外句柄,
|
|
19
|
+
模型就没有理由再碰磁盘。
|
|
20
|
+
|
|
21
|
+
## 验证
|
|
22
|
+
|
|
23
|
+
- `npm test` 全绿:契约、召回、面板、跨层、记忆管理、安全与 HA 用例都按标题断言。
|
|
24
|
+
- 自动召回、手动搜索、读正文、关联读取共用同一套任务上下文、适用条件与词法规则;
|
|
25
|
+
条件未确认的条目仍不会被自动注入,但按标题显式读取会返回正文并标注 `needs_context`。
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# 0.18.11:普通记忆也能在面板里改
|
|
2
|
+
|
|
3
|
+
## 只开一条最小写入口
|
|
4
|
+
|
|
5
|
+
0.18.10 之前,面板只能改实验规则;普通记忆能读、能归档,正文写错了只能让模型再保存一次。
|
|
6
|
+
这一版在记忆面板的当前项目里加「修改」:就地改标题和正文,保存后版本加一,标签、类型、
|
|
7
|
+
适用条件、关联和学习状态都不动。
|
|
8
|
+
|
|
9
|
+
写入口只有一条:`POST /lite-memory/entry`,复用 `saveEntry` 的记录分支,不新增改记忆的函数。
|
|
10
|
+
|
|
11
|
+
- 必须带面板读到的 `expectedRevision`;写前在锁内再比一次,中间被别人改过就 409,
|
|
12
|
+
回包标 `retry: true`。面板把最新正文和版本取回来,草稿留在输入框里,确认后再存一次。
|
|
13
|
+
- 改名不能撞上另一条现行记忆:标题是唯一句柄,撞了以后按标题读会分不清,直接拒绝并指出同名条目。
|
|
14
|
+
- 只写当前会话确认过的项目层。别的项目、全局层一律 409;项目切换、目录未确认、记忆关闭都拒绝。
|
|
15
|
+
- 回执只给标题和版本,不给编号。
|
|
16
|
+
|
|
17
|
+
## 层与锁的边界收紧
|
|
18
|
+
|
|
19
|
+
- 显式 `scope="project"` / `scope="global"` 只解析和写那一层:读取、关联、归档、彻底删除都
|
|
20
|
+
不再回落到另一层。同名条目在两层都有时,必须点层,不能靠「先找到哪条」决定。
|
|
21
|
+
- 保存只锁真正要写的那一层(新增按归属路由,更新按条目所在层);两个项目各写自己的条目时
|
|
22
|
+
不再因为共享全局锁互相等待。全局写仍然锁全局层。
|
|
23
|
+
- 「以后所有口播」「以后一律」这类覆盖全部对象的说法,在项目层按 `all_in_scope` 收集;
|
|
24
|
+
「以后都」仍然不等于「所有项目」,不会自动升到全局层。
|
|
25
|
+
|
|
26
|
+
## 为什么不是「编辑一切」
|
|
27
|
+
|
|
28
|
+
跨项目、跨层的写权限不能从「面板能看见」推出来,也不能从模型的意图推出来;这次只放开
|
|
29
|
+
当前项目这一层,别层保持只读。改的是人明确选中的那一条,范围不重算、不迁移、不顺手改学习状态。
|
|
30
|
+
|
|
31
|
+
## 参数清单只做到「改正文」
|
|
32
|
+
|
|
33
|
+
标签、类型、适用条件、关联这些参数仍不在面板里改:它们影响适用性判断,逐项开放会把
|
|
34
|
+
「改一个标签」变成「改一条规则算不算适用」的权限问题。当前路径是:正文和标题可以就地
|
|
35
|
+
refine;要改参数,由模型按同标题再存一次,改动会升版本并留记录。把整条记忆转成「资料」
|
|
36
|
+
(保留原文、按需读取、不默认注入)需要独立的资料存储与删除/导出闭环,留到下一版,不在这
|
|
37
|
+
一版假装已经支持。
|
|
38
|
+
|
|
39
|
+
## 验证
|
|
40
|
+
|
|
41
|
+
- `npm test` 全绿。
|
|
42
|
+
- 契约用例覆盖:正常改正文并升版本、旧版本写入被拒、样式/标签保持、改名撞车被拒、
|
|
43
|
+
别项目与全局层被拒、空标题被拒;面板点开正文前后状态文件逐字节不变。
|
|
44
|
+
- 边界回归覆盖:被取代和已过期的条目不会进入自动注入,也不进默认检索;只有显式查历史才看得到。
|
|
45
|
+
- 宫殿回归覆盖:点展品只把条目交给详情入口,不改宫殿里的记录;浏览不写状态。
|
|
46
|
+
- 边界新增:显式项目层读不到、也彻底删不掉全局唯一条目,且全局条目原样保留;另一个项目
|
|
47
|
+
占着全局锁时,本项目保存自己的条目照常成功;刚存完立刻检索命中的是刚存的那一条。
|
|
48
|
+
- `ui-copy-lint` / `zh-lint` / `style-lint` 无新增错误。
|