@max-null/dsh-memory 0.6.1 → 0.7.0
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 +25 -20
- package/client.js +221 -102
- package/dist/anchors.d.ts +45 -0
- package/dist/anchors.js +45 -0
- package/dist/engine.d.ts +189 -8
- package/dist/engine.js +445 -17
- package/dist/index.js +32 -8
- package/dist/quarantine.d.ts +24 -0
- package/dist/quarantine.js +43 -0
- package/dist/query-log.d.ts +155 -0
- package/dist/query-log.js +220 -0
- package/dist/routes.js +2 -0
- package/dist/self.d.ts +1 -0
- package/dist/self.js +14 -10
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -8,22 +8,22 @@ This plugin belongs to the **`@max-null/*` family** — a set of plugins that to
|
|
|
8
8
|
|
|
9
9
|
## 设计原则
|
|
10
10
|
|
|
11
|
-
1.
|
|
11
|
+
1. **写入即生效,人是例外干预者**:模型写入的记忆直接生效(`approved`),不再逐条等人放行;人保留随时查看、改写、删除、钉住或回滚的能力——人不在场不等于失控。
|
|
12
12
|
2. **可观测先于精准**:每条记忆是明文,`memory_list` 随时可见、`memory_forget` 随时删除——不存在"静默暗礁"。
|
|
13
|
-
3.
|
|
13
|
+
3. **明文是人机共享的审计窗口**:记忆是可读文本,模型可自检其是否过期(有效性锚点会在所绑环境值变化后标记 `stale`),人可随时查看与改写。
|
|
14
14
|
4. **确定性且缓存安全**:BM25 关键词检索是存储的纯函数、无 LLM 调用;固定指引进 system-prompt section,`approved + injected` 记忆进 recall context(global 全量 + 当前会话工作区),逐条为单行摘要并按注入预算截断(超预算按最近使用优先,省略数在面板可见)。
|
|
15
15
|
|
|
16
|
-
## 截图
|
|
17
|
-
|
|
18
|
-
装完后在设置里多出「记忆」一项,可查看与管理跨会话记忆:
|
|
19
|
-
|
|
20
|
-
**入口:** 设置 → 记忆
|
|
21
|
-
|
|
22
|
-
| 设置入口与面板 |
|
|
23
|
-
|---|
|
|
24
|
-
|  |
|
|
25
|
-
|
|
26
|
-
|
|
16
|
+
## 截图
|
|
17
|
+
|
|
18
|
+
装完后在设置里多出「记忆」一项,可查看与管理跨会话记忆:
|
|
19
|
+
|
|
20
|
+
**入口:** 设置 → 记忆
|
|
21
|
+
|
|
22
|
+
| 设置入口与面板 |
|
|
23
|
+
|---|
|
|
24
|
+
|  |
|
|
25
|
+
|
|
26
|
+
|
|
27
27
|
## 用法
|
|
28
28
|
|
|
29
29
|
```bash
|
|
@@ -55,7 +55,7 @@ npm install @max-null/dsh-memory
|
|
|
55
55
|
|
|
56
56
|
两个根都可用 config 覆盖(`globalRoot` / `projectRoot`)。`memory_list` / `memory_search` 不带 `namespace` 过滤时会同时查两层。旧版双重前缀文件名(`memory_project_memory_project_<hash>.json`)在打开时自动迁移为规范名。
|
|
57
57
|
|
|
58
|
-
##
|
|
58
|
+
## 使用流程(写入即生效,人为例外干预)
|
|
59
59
|
|
|
60
60
|
提示词模板库(0.6.0):`prompt_search / prompt_get / prompt_list / prompt_add` 四个工具管理**模板库**——
|
|
61
61
|
md 文件是唯一事实源(`~/.dsh/prompt-library/*.md` 为 global;`<workspace>/.dsh/prompt-library/` 随工作区分享),
|
|
@@ -63,14 +63,19 @@ md 文件是唯一事实源(`~/.dsh/prompt-library/*.md` 为 global;`<worksp
|
|
|
63
63
|
**永不注入 system prompt**。
|
|
64
64
|
|
|
65
65
|
```
|
|
66
|
-
模型 memory_save → status:
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
模型 memory_save → status: approved,立即生效;命中密钥/凭据规则则隔离(不进检索也不进注入)
|
|
67
|
+
memory_search → 关键词/语义召回(只搜记忆;模板走 prompt_search 通道,不会混进候选池)
|
|
68
|
+
命中累计 2 次 → injected: true(自动打开常驻:global + 当前会话工作区,摘要化 + 预算截断)
|
|
69
|
+
30 天未再命中 → 自动撤下常驻(只撤自动开的;人工动过的开关双向豁免)
|
|
70
|
+
人(面板 / 开关) → 钉住 / 删除 / 放行隔离记录 / 回滚
|
|
71
|
+
memory_forget → 随时删除(删除始终是人的动作)
|
|
71
72
|
```
|
|
72
73
|
|
|
73
|
-
|
|
74
|
+
两条自动规则撑起淘汰机制:**反复被检索命中**是它值得每轮付费的证据,**长期不再被命中**则自动退出常驻。两者都不删除任何内容——记忆不会无限累积,也不会被系统自行清空。
|
|
75
|
+
|
|
76
|
+
记忆还可声明**有效性锚点**(环境变量 / 工具清单 / 插件版本):所绑的值变化后,这条记忆被标记 `stale` 并撤下常驻,检索结果里也带出失效提示——环境变了,旧结论就不再被当成仍然正确。
|
|
77
|
+
|
|
78
|
+
模型写入不需要人点头,但**人始终能看见并推翻**:面板按「隔离区 / 常驻 / 冷数据」分组,被隔离的记录必须由人放行才会重新进入检索。
|
|
74
79
|
|
|
75
80
|
## 为什么明文 + BM25,而不是向量检索
|
|
76
81
|
|
package/client.js
CHANGED
|
@@ -175,26 +175,34 @@ var STRINGS = {
|
|
|
175
175
|
promptFilterAll: "\u5168\u90E8",
|
|
176
176
|
memorySearch: "\u641C\u7D22\u8BB0\u5FC6\u2026",
|
|
177
177
|
empty: "\u9ED1\u6697\u4E2D\u672A\u89C1\u7075\u5149",
|
|
178
|
-
confirm: "\
|
|
178
|
+
confirm: "\u653E\u884C",
|
|
179
179
|
forget: "\u5220\u9664",
|
|
180
|
-
|
|
181
|
-
|
|
180
|
+
groupQuarantine: "\u9694\u79BB\u533A",
|
|
181
|
+
groupQuarantineHint: "\u547D\u4E2D\u5371\u9669\u5185\u5BB9\u89C4\u5219\uFF08\u5BC6\u94A5/\u51ED\u636E\u7C7B\uFF09\u2192 \u4E0D\u8FDB\u6CE8\u5165\u3001\u4E0D\u8FDB\u68C0\u7D22\u3002\u653E\u884C\u540E\u6309\u666E\u901A\u8BB0\u5FC6\u5904\u7406\u3002",
|
|
182
|
+
quarantineReason: "\u89C4\u5219\uFF1A{reason}",
|
|
183
|
+
release: "\u653E\u884C",
|
|
184
|
+
blockedWhileQuarantined: "\u9694\u79BB\u4E2D \xB7 \u653E\u884C\u540E\u53EF\u5E38\u9A7B\u6CE8\u5165",
|
|
185
|
+
groupPending: "\u5F85\u653E\u884C",
|
|
186
|
+
groupOnDemand: "\u6309\u9700\u68C0\u7D22",
|
|
182
187
|
groupInjected: "\u5E38\u9A7B\u6CE8\u5165",
|
|
188
|
+
groupCold: "\u51B7\u6570\u636E\xB7\u6298\u53E0\uFF08{days} \u5929\u672A\u68C0\u7D22\uFF09",
|
|
183
189
|
injectSwitch: "\u5E38\u9A7B\u6CE8\u5165",
|
|
184
|
-
approveFirst: "\
|
|
190
|
+
approveFirst: "\u5148\u653E\u884C\u9694\u79BB\u624D\u80FD\u5E38\u9A7B\u6CE8\u5165",
|
|
185
191
|
allNamespaces: "\u5168\u90E8",
|
|
186
192
|
nsGlobal: "\u5168\u5C40",
|
|
187
193
|
nsWorkspace: "\u5DE5\u4F5C\u533A",
|
|
188
194
|
noWorkspace: "\u672A\u9009\u62E9\u5DE5\u4F5C\u533A",
|
|
189
195
|
organizeMemory: "\u6574\u7406\u8BB0\u5FC6",
|
|
190
|
-
confirmAll: "\u5168\u90E8\
|
|
196
|
+
confirmAll: "\u5168\u90E8\u653E\u884C",
|
|
191
197
|
injectPreview: "\u6CE8\u5165\u9884\u89C8",
|
|
192
198
|
contextUsage: "\u4E0A\u4E0B\u6587\u5360\u7528",
|
|
193
199
|
keywordsLabel: "\u5173\u952E\u8BCD",
|
|
194
|
-
suggested: "\
|
|
195
|
-
approved: "\u5DF2\
|
|
200
|
+
suggested: "\u9694\u79BB",
|
|
201
|
+
approved: "\u5DF2\u751F\u6548",
|
|
196
202
|
refresh: "\u5237\u65B0",
|
|
197
|
-
coldHint: "\u51B7\u6570\u636E
|
|
203
|
+
coldHint: "\u51B7\u6570\u636E",
|
|
204
|
+
lastUsedAt: "\u4E0A\u641C",
|
|
205
|
+
hitCount: "\u547D\u4E2D {count} \u6B21"
|
|
198
206
|
},
|
|
199
207
|
en: {
|
|
200
208
|
tabMemory: "Memory",
|
|
@@ -220,26 +228,34 @@ var STRINGS = {
|
|
|
220
228
|
promptFilterAll: "All",
|
|
221
229
|
memorySearch: "Search memory\u2026",
|
|
222
230
|
empty: "No spark in the dark",
|
|
223
|
-
confirm: "
|
|
231
|
+
confirm: "Release",
|
|
224
232
|
forget: "Forget",
|
|
225
|
-
|
|
226
|
-
|
|
233
|
+
groupQuarantine: "Quarantine",
|
|
234
|
+
groupQuarantineHint: "Flagged by a dangerous-content rule (secrets/credentials) \u2192 kept out of injection and recall. Releasing it makes it an ordinary memory.",
|
|
235
|
+
quarantineReason: "Rule: {reason}",
|
|
236
|
+
release: "Release",
|
|
237
|
+
blockedWhileQuarantined: "Quarantined \xB7 release to allow injection",
|
|
238
|
+
groupPending: "Pending release",
|
|
239
|
+
groupOnDemand: "On demand",
|
|
227
240
|
groupInjected: "Always injected",
|
|
241
|
+
groupCold: "Cold \xB7 collapsed ({days} days untouched)",
|
|
228
242
|
injectSwitch: "Inject every turn",
|
|
229
|
-
approveFirst: "
|
|
243
|
+
approveFirst: "Release the quarantine first",
|
|
230
244
|
allNamespaces: "All",
|
|
231
245
|
nsGlobal: "Global",
|
|
232
246
|
nsWorkspace: "Workspace",
|
|
233
247
|
noWorkspace: "No workspace selected",
|
|
234
248
|
organizeMemory: "Organize memory",
|
|
235
|
-
confirmAll: "
|
|
249
|
+
confirmAll: "Release all",
|
|
236
250
|
injectPreview: "Injection preview",
|
|
237
251
|
contextUsage: "Context usage",
|
|
238
252
|
keywordsLabel: "Keywords",
|
|
239
|
-
suggested: "
|
|
240
|
-
approved: "
|
|
253
|
+
suggested: "Quarantined",
|
|
254
|
+
approved: "Active",
|
|
241
255
|
refresh: "Refresh",
|
|
242
|
-
coldHint: "Cold
|
|
256
|
+
coldHint: "Cold",
|
|
257
|
+
lastUsedAt: "Recalled",
|
|
258
|
+
hitCount: "{count} hits"
|
|
243
259
|
}
|
|
244
260
|
};
|
|
245
261
|
var localeId = "zh";
|
|
@@ -356,7 +372,7 @@ var memoryApi = {
|
|
|
356
372
|
return api("prompt.refresh", { ...cwd === void 0 ? {} : { cwd } });
|
|
357
373
|
}
|
|
358
374
|
};
|
|
359
|
-
var ORGANIZE_PROMPT = "\u8BF7\u6574\u7406\u6211\u7684\u8BB0\u5FC6\u5E93\uFF1A\u7528 memory_list \u67E5\u770B\u5168\u90E8\u8BB0\u5FC6\u3002\u6574\u7406\u89C4\u5219\uFF08\u5FC5\u987B\u6267\u884C\uFF09\uFF1A\u2460\u540C\u7C7B\u6761\u76EE\u5408\u5E76\u2014\u2014\u5DE5\u4F5C\u4E60\u60EF/\u7EA6\u5B9A\u7C7B\u591A\u6761\u5408\u5E76\u4E3A\u4E00\u6761\uFF08\u5185\u5BB9\u7528 \u2460\u2461\u2462 \u5E76\u5217\uFF0C\u907F\u514D\u788E\u7247\u5316\uFF09\uFF1B\u2461\u4E0E\u300C[\u8BB0\u5FC6\u7CFB\u7EDF\u81EA\u8FF0]\u300D\uFF08\u4F60\u4E0A\u4E0B\u6587\u4E2D\u7684\u8BB0\u5FC6\u673A\u5236\u8BF4\u660E\uFF09\u5185\u5BB9\u91CD\u590D\u7684\u63D2\u4EF6\u4ECB\u7ECD\u6761\u76EE\uFF08\u5982 dsh-memory \u63D2\u4EF6\u53D1\u5E03\u4FE1\u606F\uFF09\u5E94\u5220\u9664\u2014\u2014\u673A\u5236\u8BF4\u660E\u5DF2\u7531\u7CFB\u7EDF\u5E38\u9A7B\u63D0\u4F9B\uFF0C\u65E0\u9700\u7528\u6237\u5B58\u50A8\uFF1B\u2462\u5BF9\u8FC7\u65F6\u3001\u9519\u8BEF\u6216\u5DF2\u53D8\u5316\u7684\u5185\u5BB9\u7528 memory_update \u4FEE\u6B63\uFF08\
|
|
375
|
+
var ORGANIZE_PROMPT = "\u8BF7\u6574\u7406\u6211\u7684\u8BB0\u5FC6\u5E93\uFF1A\u7528 memory_list \u67E5\u770B\u5168\u90E8\u8BB0\u5FC6\u3002\u6574\u7406\u89C4\u5219\uFF08\u5FC5\u987B\u6267\u884C\uFF09\uFF1A\u2460\u540C\u7C7B\u6761\u76EE\u5408\u5E76\u2014\u2014\u5DE5\u4F5C\u4E60\u60EF/\u7EA6\u5B9A\u7C7B\u591A\u6761\u5408\u5E76\u4E3A\u4E00\u6761\uFF08\u5185\u5BB9\u7528 \u2460\u2461\u2462 \u5E76\u5217\uFF0C\u907F\u514D\u788E\u7247\u5316\uFF09\uFF1B\u2461\u4E0E\u300C[\u8BB0\u5FC6\u7CFB\u7EDF\u81EA\u8FF0]\u300D\uFF08\u4F60\u4E0A\u4E0B\u6587\u4E2D\u7684\u8BB0\u5FC6\u673A\u5236\u8BF4\u660E\uFF09\u5185\u5BB9\u91CD\u590D\u7684\u63D2\u4EF6\u4ECB\u7ECD\u6761\u76EE\uFF08\u5982 dsh-memory \u63D2\u4EF6\u53D1\u5E03\u4FE1\u606F\uFF09\u5E94\u5220\u9664\u2014\u2014\u673A\u5236\u8BF4\u660E\u5DF2\u7531\u7CFB\u7EDF\u5E38\u9A7B\u63D0\u4F9B\uFF0C\u65E0\u9700\u7528\u6237\u5B58\u50A8\uFF1B\u2462\u5BF9\u8FC7\u65F6\u3001\u9519\u8BEF\u6216\u5DF2\u53D8\u5316\u7684\u5185\u5BB9\u7528 memory_update \u4FEE\u6B63\uFF08\u6539\u52A8\u751F\u6548\u3001\u6CE8\u5165\u5F00\u5173\u4FDD\u7559\uFF09\uFF1B\u2463\u7CBE\u7B80\u5197\u957F\u5185\u5BB9\uFF0C\u4E3A\u6BCF\u6761\u8865\u5145\u6216\u4FEE\u6B63 keywords\uFF1B\u2464\u9700\u8981\u5220\u9664\u7684\u7528 memory_forget\uFF0C\u9700\u8981\u65B0\u589E\u7684\u7528 memory_save\u3002\u5224\u65AD\u5185\u5BB9\u662F\u5426\u8FC7\u65F6\u7684\u65B9\u6CD5\uFF1A\u628A\u8BB0\u5FC6\u91CC\u63D0\u5230\u7684\u5DE5\u5177\u540D/\u6570\u91CF\u4E0E\u4F60\u5F53\u524D\u5B9E\u9645\u53EF\u7528\u7684\u8BB0\u5FC6\u5DE5\u5177\u5BF9\u7167\u2014\u2014\u4F60\u5F53\u524D\u53EF\u7528\uFF1Amemory_save / memory_list / memory_search / memory_confirm / memory_forget / memory_update\uFF08\u5171 6 \u4E2A\uFF09\uFF1B\u82E5\u8BB0\u5FC6\u4E2D\u7684\u5DE5\u5177\u5217\u8868\u3001\u6570\u91CF\u3001\u6D41\u7A0B\u4E0E\u6B64\u4E0D\u7B26\u5373\u4E3A\u8FC7\u65F6\uFF0C\u7528 memory_update \u4FEE\u6B63\u3002\u6539\u52A8\u76F4\u63A5\u751F\u6548\uFF08\u4E0D\u8981\u8C03\u7528 memory_confirm\uFF09\uFF0C\u5B8C\u6210\u540E\u7528\u4E00\u53E5\u8BDD\u6C47\u62A5\u6574\u7406\u7ED3\u679C\u3002";
|
|
360
376
|
function insertIntoInput(ctx, text) {
|
|
361
377
|
const sessions = ctx.get?.("sessions");
|
|
362
378
|
const conversation = ctx.get?.("conversation");
|
|
@@ -585,6 +601,9 @@ function MemoryView(props) {
|
|
|
585
601
|
const t = useT();
|
|
586
602
|
const [mode, setMode] = (0, import_react.useState)("mem");
|
|
587
603
|
const [records, setRecords] = (0, import_react.useState)([]);
|
|
604
|
+
const [quarantined, setQuarantined] = (0, import_react.useState)([]);
|
|
605
|
+
const [coldOpen, setColdOpen] = (0, import_react.useState)(false);
|
|
606
|
+
const [quarantineOpen, setQuarantineOpen] = (0, import_react.useState)(true);
|
|
588
607
|
const [query, setQuery] = (0, import_react.useState)("");
|
|
589
608
|
const [namespace, setNamespace] = (0, import_react.useState)(null);
|
|
590
609
|
const [refreshing, setRefreshing] = (0, import_react.useState)(false);
|
|
@@ -608,11 +627,16 @@ function MemoryView(props) {
|
|
|
608
627
|
setRecords(await memoryApi.list({}, props.cwd));
|
|
609
628
|
} catch {
|
|
610
629
|
}
|
|
630
|
+
try {
|
|
631
|
+
setQuarantined(await memoryApi.list({ quarantined: true }, props.cwd));
|
|
632
|
+
} catch {
|
|
633
|
+
}
|
|
611
634
|
};
|
|
612
635
|
const refreshFromDisk = async () => {
|
|
613
636
|
setRefreshing(true);
|
|
614
637
|
try {
|
|
615
638
|
setRecords(await memoryApi.reload(props.cwd));
|
|
639
|
+
setQuarantined(await memoryApi.list({ quarantined: true }, props.cwd));
|
|
616
640
|
} catch {
|
|
617
641
|
await reload();
|
|
618
642
|
} finally {
|
|
@@ -623,15 +647,22 @@ function MemoryView(props) {
|
|
|
623
647
|
if (props.visible) void reload();
|
|
624
648
|
}, [props.visible]);
|
|
625
649
|
const toggleInjected = async (record) => {
|
|
626
|
-
if (record.status !== "approved") return;
|
|
650
|
+
if (record.status !== "approved" || record.quarantined) return;
|
|
627
651
|
try {
|
|
628
652
|
await memoryApi.setInjected(record.id, !record.injected, props.cwd);
|
|
629
653
|
} catch {
|
|
630
654
|
}
|
|
631
655
|
await reload();
|
|
632
656
|
};
|
|
657
|
+
const release = async (record) => {
|
|
658
|
+
try {
|
|
659
|
+
await memoryApi.confirm(record.id, props.cwd);
|
|
660
|
+
} catch {
|
|
661
|
+
}
|
|
662
|
+
await reload();
|
|
663
|
+
};
|
|
633
664
|
const confirmAll = async () => {
|
|
634
|
-
const pending = records.filter((record) => record.status === "suggested");
|
|
665
|
+
const pending = records.filter((record) => record.status === "suggested" && !record.quarantined);
|
|
635
666
|
if (pending.length === 0) return;
|
|
636
667
|
await Promise.all(pending.map((record) => memoryApi.confirm(record.id, props.cwd).catch(() => null)));
|
|
637
668
|
await reload();
|
|
@@ -671,16 +702,180 @@ function MemoryView(props) {
|
|
|
671
702
|
const q = query.trim().toLowerCase();
|
|
672
703
|
const byNs = namespace === null ? records : namespace === "workspace" ? records.filter((record) => record.namespace === "project") : records.filter((record) => record.namespace === "global");
|
|
673
704
|
const filtered = byNs.filter((record) => q === "" || record.content.toLowerCase().includes(q));
|
|
705
|
+
const quarantinedScoped = (namespace === null ? quarantined : namespace === "workspace" ? quarantined.filter((record) => record.namespace === "project") : quarantined.filter((record) => record.namespace === "global")).filter((record) => q === "" || record.content.toLowerCase().includes(q));
|
|
674
706
|
const nsCounts = {
|
|
675
|
-
all: records.length,
|
|
676
|
-
global: records.filter((record) => record.namespace === "global").length,
|
|
677
|
-
workspace: records.filter((record) => record.namespace === "project").length
|
|
707
|
+
all: records.length + quarantined.length,
|
|
708
|
+
global: records.filter((record) => record.namespace === "global").length + quarantined.filter((record) => record.namespace === "global").length,
|
|
709
|
+
workspace: records.filter((record) => record.namespace === "project").length + quarantined.filter((record) => record.namespace === "project").length
|
|
678
710
|
};
|
|
711
|
+
const coldNow = Date.now();
|
|
712
|
+
const settled = (excludeCold) => excludeCold ? filtered.filter((record) => !isCold(record.lastUsedAt, coldNow)) : filtered;
|
|
679
713
|
const groups = [
|
|
680
|
-
{ key: "pending", label: t("groupPending"), items:
|
|
681
|
-
{ key: "
|
|
682
|
-
{ key: "
|
|
714
|
+
{ key: "pending", label: t("groupPending"), items: settled(false).filter((record) => record.status === "suggested") },
|
|
715
|
+
{ key: "injected", label: t("groupInjected"), items: settled(true).filter((record) => record.status === "approved" && record.injected) },
|
|
716
|
+
{ key: "ondemand", label: t("groupOnDemand"), items: settled(true).filter((record) => record.status === "approved" && !record.injected) }
|
|
683
717
|
].filter((group) => group.items.length > 0);
|
|
718
|
+
const coldItems = settled(false).filter((record) => record.status === "approved" && isCold(record.lastUsedAt, coldNow));
|
|
719
|
+
const memoryCard = (record, options = {}) => {
|
|
720
|
+
const isQuarantined = options.quarantined === true || record.quarantined;
|
|
721
|
+
const injectable = record.status === "approved" && !isQuarantined;
|
|
722
|
+
return (0, import_react.createElement)(
|
|
723
|
+
"div",
|
|
724
|
+
{
|
|
725
|
+
key: record.id,
|
|
726
|
+
style: {
|
|
727
|
+
...ssid.card,
|
|
728
|
+
// 隔离区显眼但不吓人:左边一条警示色,不换底色(安全网,不是错误)
|
|
729
|
+
...isQuarantined ? { borderLeft: "3px solid var(--dsw-alias-state-warning-primary, #f7c94f)" } : {}
|
|
730
|
+
}
|
|
731
|
+
},
|
|
732
|
+
(0, import_react.createElement)("div", { style: ssid.text }, ContentWithRefs({ text: record.content })),
|
|
733
|
+
// 隔离原因:命中的规则名(安全网,只陈述事实,不做道德评判)
|
|
734
|
+
isQuarantined ? (0, import_react.createElement)(
|
|
735
|
+
"div",
|
|
736
|
+
{ style: { ...ssid.muted, marginTop: 6, display: "flex", flexWrap: "wrap", gap: 4, alignItems: "center" } },
|
|
737
|
+
(0, import_react.createElement)("span", {
|
|
738
|
+
style: {
|
|
739
|
+
fontSize: 10,
|
|
740
|
+
padding: "1px 7px",
|
|
741
|
+
borderRadius: 8,
|
|
742
|
+
fontWeight: 600,
|
|
743
|
+
background: "var(--dsw-alias-state-warning-tertiary, rgba(247,201,79,.16))",
|
|
744
|
+
color: "var(--dsw-alias-state-warning-primary, #f7c94f)"
|
|
745
|
+
}
|
|
746
|
+
}, t("groupQuarantine")),
|
|
747
|
+
(0, import_react.createElement)("span", null, record.quarantineReason === void 0 || record.quarantineReason === "" ? t("quarantineReason", { reason: "\u2014" }) : t("quarantineReason", { reason: record.quarantineReason }))
|
|
748
|
+
) : null,
|
|
749
|
+
// keywords 展示(0.3.5:设计约定 UI 显示 keywords)
|
|
750
|
+
record.keywords !== void 0 && record.keywords.length > 0 ? (0, import_react.createElement)(
|
|
751
|
+
"div",
|
|
752
|
+
{ style: { display: "flex", flexWrap: "wrap", gap: 4, marginTop: 6 } },
|
|
753
|
+
record.keywords.map((keyword) => (0, import_react.createElement)("span", {
|
|
754
|
+
key: keyword,
|
|
755
|
+
style: {
|
|
756
|
+
fontSize: 10,
|
|
757
|
+
padding: "1px 7px",
|
|
758
|
+
borderRadius: 8,
|
|
759
|
+
background: "var(--dsw-alias-bg-module-platform, rgba(128,148,168,.14))",
|
|
760
|
+
color: "var(--dsw-alias-label-secondary, #67748a)"
|
|
761
|
+
}
|
|
762
|
+
}, keyword))
|
|
763
|
+
) : null,
|
|
764
|
+
(0, import_react.createElement)(
|
|
765
|
+
"div",
|
|
766
|
+
{ style: { ...ssid.muted, marginTop: 6 } },
|
|
767
|
+
`${record.namespace} \xB7 ${record.status === "approved" ? t("approved") : t("suggested")}${record.injected ? ` \xB7 ${t("groupInjected")}` : ""}${record.lastUsedAt !== void 0 ? ` \xB7 ${t("lastUsedAt")} ${fmtDate(record.lastUsedAt)}` : ""}${isCold(record.lastUsedAt, coldNow) ? ` \xB7 ${t("coldHint")}` : ""}${record.hitCount > 0 ? ` \xB7 ${t("hitCount", { count: record.hitCount })}` : ""}`
|
|
768
|
+
),
|
|
769
|
+
(0, import_react.createElement)(
|
|
770
|
+
"div",
|
|
771
|
+
{ style: { display: "flex", gap: 6, marginTop: 8, alignItems: "center" } },
|
|
772
|
+
(0, import_react.createElement)("button", {
|
|
773
|
+
type: "button",
|
|
774
|
+
title: isQuarantined ? t("blockedWhileQuarantined") : record.status === "approved" ? t("injectSwitch") : t("approveFirst"),
|
|
775
|
+
disabled: !injectable,
|
|
776
|
+
onClick: () => {
|
|
777
|
+
void toggleInjected(record);
|
|
778
|
+
},
|
|
779
|
+
style: {
|
|
780
|
+
...ssid.btn,
|
|
781
|
+
...record.injected ? { color: ssid.accent, borderColor: ssid.accent } : {},
|
|
782
|
+
opacity: injectable ? 1 : 0.4,
|
|
783
|
+
cursor: injectable ? "pointer" : "not-allowed"
|
|
784
|
+
}
|
|
785
|
+
}, record.injected ? `\u2713 ${t("injectSwitch")}` : t("injectSwitch")),
|
|
786
|
+
isQuarantined ? (0, import_react.createElement)("button", {
|
|
787
|
+
style: { ...ssid.btn, color: ssid.accent, borderColor: ssid.accent },
|
|
788
|
+
onClick: () => {
|
|
789
|
+
void release(record);
|
|
790
|
+
}
|
|
791
|
+
}, t("release")) : null,
|
|
792
|
+
record.status === "suggested" && !isQuarantined ? (0, import_react.createElement)("button", {
|
|
793
|
+
style: ssid.btn,
|
|
794
|
+
onClick: () => {
|
|
795
|
+
void memoryApi.confirm(record.id, props.cwd).then(() => reload());
|
|
796
|
+
}
|
|
797
|
+
}, t("confirm")) : null,
|
|
798
|
+
(0, import_react.createElement)("button", {
|
|
799
|
+
style: ssid.btn,
|
|
800
|
+
onClick: () => {
|
|
801
|
+
void memoryApi.forget(record.id, props.cwd).then(() => reload());
|
|
802
|
+
}
|
|
803
|
+
}, t("forget"))
|
|
804
|
+
)
|
|
805
|
+
);
|
|
806
|
+
};
|
|
807
|
+
const foldHeader = (label, count, open, toggle) => (0, import_react.createElement)(
|
|
808
|
+
"div",
|
|
809
|
+
{
|
|
810
|
+
style: { ...ssid.title, cursor: "pointer", marginBottom: 0 },
|
|
811
|
+
onClick: toggle,
|
|
812
|
+
onKeyDown: (event) => {
|
|
813
|
+
if (event.key === "Enter" || event.key === " ") toggle();
|
|
814
|
+
},
|
|
815
|
+
role: "button",
|
|
816
|
+
tabIndex: 0
|
|
817
|
+
},
|
|
818
|
+
(0, import_react.createElement)("span", null, `${open ? "\u25BE" : "\u25B8"} ${label}`),
|
|
819
|
+
(0, import_react.createElement)("span", { style: ssid.count }, String(count))
|
|
820
|
+
);
|
|
821
|
+
const quarantineSection = quarantinedScoped.length === 0 ? null : (0, import_react.createElement)(
|
|
822
|
+
"div",
|
|
823
|
+
{ key: "quarantine", style: { display: "flex", flexDirection: "column", gap: 6 } },
|
|
824
|
+
foldHeader(t("groupQuarantine"), quarantinedScoped.length, quarantineOpen, () => {
|
|
825
|
+
setQuarantineOpen((open) => !open);
|
|
826
|
+
}),
|
|
827
|
+
(0, import_react.createElement)("div", { style: { ...ssid.muted, fontSize: 10.5 } }, t("groupQuarantineHint")),
|
|
828
|
+
quarantineOpen ? (0, import_react.createElement)(
|
|
829
|
+
"div",
|
|
830
|
+
{ style: { display: "flex", flexDirection: "column", gap: 6 } },
|
|
831
|
+
quarantinedScoped.map((record) => memoryCard(record, { quarantined: true }))
|
|
832
|
+
) : null
|
|
833
|
+
);
|
|
834
|
+
const coldSection = coldItems.length === 0 ? null : (0, import_react.createElement)(
|
|
835
|
+
"div",
|
|
836
|
+
{ key: "cold", style: { display: "flex", flexDirection: "column", gap: 6 } },
|
|
837
|
+
foldHeader(t("groupCold", { days: COLD_DAYS }), coldItems.length, coldOpen, () => {
|
|
838
|
+
setColdOpen((open) => !open);
|
|
839
|
+
}),
|
|
840
|
+
coldOpen ? (0, import_react.createElement)(
|
|
841
|
+
"div",
|
|
842
|
+
{ style: { display: "flex", flexDirection: "column", gap: 6 } },
|
|
843
|
+
coldItems.map((record) => memoryCard(record))
|
|
844
|
+
) : null
|
|
845
|
+
);
|
|
846
|
+
const groupSections = groups.map((group) => (0, import_react.createElement)(
|
|
847
|
+
"div",
|
|
848
|
+
{
|
|
849
|
+
key: group.key,
|
|
850
|
+
style: { display: "flex", flexDirection: "column", gap: 6 }
|
|
851
|
+
},
|
|
852
|
+
(0, import_react.createElement)(
|
|
853
|
+
"div",
|
|
854
|
+
{ style: ssid.title },
|
|
855
|
+
(0, import_react.createElement)("span", null, group.label),
|
|
856
|
+
(0, import_react.createElement)(
|
|
857
|
+
"div",
|
|
858
|
+
{ style: { display: "flex", alignItems: "center", gap: 6 } },
|
|
859
|
+
group.key === "pending" && group.items.length > 0 ? (0, import_react.createElement)("button", {
|
|
860
|
+
type: "button",
|
|
861
|
+
title: t("confirmAll"),
|
|
862
|
+
onClick: () => {
|
|
863
|
+
void confirmAll();
|
|
864
|
+
},
|
|
865
|
+
style: { ...ssid.btn, padding: "1px 8px", fontSize: 10.5 }
|
|
866
|
+
}, t("confirmAll")) : null,
|
|
867
|
+
(0, import_react.createElement)("span", { style: ssid.count }, String(group.items.length))
|
|
868
|
+
)
|
|
869
|
+
),
|
|
870
|
+
group.items.map((record) => memoryCard(record))
|
|
871
|
+
));
|
|
872
|
+
const listSection = quarantineSection === null && groupSections.length === 0 && coldSection === null ? (0, import_react.createElement)("div", { key: "empty", style: ssid.empty }, t("empty")) : (0, import_react.createElement)(
|
|
873
|
+
"div",
|
|
874
|
+
{ key: "groups", style: { display: "flex", flexDirection: "column", gap: 12 } },
|
|
875
|
+
quarantineSection,
|
|
876
|
+
groupSections,
|
|
877
|
+
coldSection
|
|
878
|
+
);
|
|
684
879
|
const modeTabs = (0, import_react.createElement)(
|
|
685
880
|
"div",
|
|
686
881
|
{ style: { display: "flex", gap: 4 } },
|
|
@@ -799,83 +994,7 @@ function MemoryView(props) {
|
|
|
799
994
|
preview.lines.length === 0 ? (0, import_react.createElement)("div", { style: ssid.muted }, t("empty")) : preview.lines.map((line, index) => (0, import_react.createElement)("div", { key: index, style: { ...ssid.muted, fontSize: 11 } }, line))
|
|
800
995
|
) : null,
|
|
801
996
|
// 未选择工作区:工作区视图显示占位(0.3.4 工作区路由语义)
|
|
802
|
-
namespace === "workspace" && (props.cwd === void 0 || props.cwd === "") ? (0, import_react.createElement)("div", { style: ssid.empty }, t("noWorkspace")) :
|
|
803
|
-
"div",
|
|
804
|
-
{ key: group.key, style: { display: "flex", flexDirection: "column", gap: 6 } },
|
|
805
|
-
(0, import_react.createElement)(
|
|
806
|
-
"div",
|
|
807
|
-
{ style: ssid.title },
|
|
808
|
-
(0, import_react.createElement)("span", null, group.label),
|
|
809
|
-
(0, import_react.createElement)(
|
|
810
|
-
"div",
|
|
811
|
-
{ style: { display: "flex", alignItems: "center", gap: 6 } },
|
|
812
|
-
group.key === "pending" && group.items.length > 0 ? (0, import_react.createElement)("button", {
|
|
813
|
-
type: "button",
|
|
814
|
-
title: t("confirmAll"),
|
|
815
|
-
onClick: () => {
|
|
816
|
-
void confirmAll();
|
|
817
|
-
},
|
|
818
|
-
style: { ...ssid.btn, padding: "1px 8px", fontSize: 10.5 }
|
|
819
|
-
}, t("confirmAll")) : null,
|
|
820
|
-
(0, import_react.createElement)("span", { style: ssid.count }, String(group.items.length))
|
|
821
|
-
)
|
|
822
|
-
),
|
|
823
|
-
group.items.map((record) => (0, import_react.createElement)(
|
|
824
|
-
"div",
|
|
825
|
-
{ key: record.id, style: ssid.card },
|
|
826
|
-
(0, import_react.createElement)("div", { style: ssid.text }, ContentWithRefs({ text: record.content })),
|
|
827
|
-
// keywords 展示(0.3.5:设计约定 UI 显示 keywords)
|
|
828
|
-
record.keywords !== void 0 && record.keywords.length > 0 ? (0, import_react.createElement)(
|
|
829
|
-
"div",
|
|
830
|
-
{ style: { display: "flex", flexWrap: "wrap", gap: 4, marginTop: 6 } },
|
|
831
|
-
record.keywords.map((keyword) => (0, import_react.createElement)("span", {
|
|
832
|
-
key: keyword,
|
|
833
|
-
style: {
|
|
834
|
-
fontSize: 10,
|
|
835
|
-
padding: "1px 7px",
|
|
836
|
-
borderRadius: 8,
|
|
837
|
-
background: "var(--dsw-alias-bg-module-platform, rgba(128,148,168,.14))",
|
|
838
|
-
color: "var(--dsw-alias-label-secondary, #67748a)"
|
|
839
|
-
}
|
|
840
|
-
}, keyword))
|
|
841
|
-
) : null,
|
|
842
|
-
(0, import_react.createElement)(
|
|
843
|
-
"div",
|
|
844
|
-
{ style: { ...ssid.muted, marginTop: 6 } },
|
|
845
|
-
`${record.namespace} \xB7 ${record.status === "approved" ? t("approved") : t("suggested")}${record.injected ? ` \xB7 ${t("groupInjected")}` : ""}${record.lastUsedAt !== void 0 ? ` \xB7 \u4E0A\u641C ${fmtDate(record.lastUsedAt)}` : ""}${isCold(record.lastUsedAt) ? ` \xB7 ${t("coldHint")}` : ""}`
|
|
846
|
-
),
|
|
847
|
-
(0, import_react.createElement)(
|
|
848
|
-
"div",
|
|
849
|
-
{ style: { display: "flex", gap: 6, marginTop: 8, alignItems: "center" } },
|
|
850
|
-
(0, import_react.createElement)("button", {
|
|
851
|
-
type: "button",
|
|
852
|
-
title: record.status === "approved" ? t("injectSwitch") : t("approveFirst"),
|
|
853
|
-
disabled: record.status !== "approved",
|
|
854
|
-
onClick: () => {
|
|
855
|
-
void toggleInjected(record);
|
|
856
|
-
},
|
|
857
|
-
style: {
|
|
858
|
-
...ssid.btn,
|
|
859
|
-
...record.injected ? { color: ssid.accent, borderColor: ssid.accent } : {},
|
|
860
|
-
opacity: record.status !== "approved" ? 0.4 : 1,
|
|
861
|
-
cursor: record.status !== "approved" ? "not-allowed" : "pointer"
|
|
862
|
-
}
|
|
863
|
-
}, record.injected ? `\u2713 ${t("injectSwitch")}` : t("injectSwitch")),
|
|
864
|
-
record.status === "suggested" ? (0, import_react.createElement)("button", {
|
|
865
|
-
style: ssid.btn,
|
|
866
|
-
onClick: () => {
|
|
867
|
-
void memoryApi.confirm(record.id, props.cwd).then(() => reload());
|
|
868
|
-
}
|
|
869
|
-
}, t("confirm")) : null,
|
|
870
|
-
(0, import_react.createElement)("button", {
|
|
871
|
-
style: ssid.btn,
|
|
872
|
-
onClick: () => {
|
|
873
|
-
void memoryApi.forget(record.id, props.cwd).then(() => reload());
|
|
874
|
-
}
|
|
875
|
-
}, t("forget"))
|
|
876
|
-
)
|
|
877
|
-
))
|
|
878
|
-
))
|
|
997
|
+
namespace === "workspace" && (props.cwd === void 0 || props.cwd === "") ? (0, import_react.createElement)("div", { style: ssid.empty }, t("noWorkspace")) : listSection
|
|
879
998
|
)
|
|
880
999
|
);
|
|
881
1000
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 有效性锚点(2026-09-15 静默记忆机制):把一条记忆绑定到某个**可探测的环境值**上,
|
|
3
|
+
* 值变了就自动失效——解决「版本特性类记忆随环境升级静默变错」的问题。
|
|
4
|
+
*
|
|
5
|
+
* 第一版只做**进程内可探测**的类型:校验跑在会话启动的路径上,探测必须零 IO。
|
|
6
|
+
* 需要跑子进程才能拿到的(如 PowerShell 版本)留待后续——那要先解决「插件直接
|
|
7
|
+
* spawn 会绕过 DSH 的 shell 沙箱」的合规问题,不能顺手开这个口子。
|
|
8
|
+
*/
|
|
9
|
+
/** 受控词汇表:写入时只能从这三种里选,不能自创(自创的锚点无法机械校验)。 */
|
|
10
|
+
export type AnchorKind = 'env' | 'tool-list' | 'self-version';
|
|
11
|
+
/** 一条记忆声明的锚点。 */
|
|
12
|
+
export interface MemoryAnchor {
|
|
13
|
+
kind: AnchorKind;
|
|
14
|
+
/** `env` 类型时的变量名;其余类型省略。 */
|
|
15
|
+
name?: string;
|
|
16
|
+
/** 写入时探测到的值。 */
|
|
17
|
+
value: string;
|
|
18
|
+
}
|
|
19
|
+
/** 探测上下文:由调用方提供,便于测试注入与缓存。 */
|
|
20
|
+
export interface AnchorProbes {
|
|
21
|
+
/**
|
|
22
|
+
* 当前可用工具的清单;**拿不到时必须是 `undefined` 而不是空数组**——空数组会被判成
|
|
23
|
+
* 「工具全没了」,让所有 `tool-list` 锚点误失效。
|
|
24
|
+
*/
|
|
25
|
+
toolNames?: readonly string[];
|
|
26
|
+
/** dsh-memory 自身版本。 */
|
|
27
|
+
selfVersion: string;
|
|
28
|
+
/** 环境变量读取器;缺省 `process.env`。 */
|
|
29
|
+
env?: (name: string) => string | undefined;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 探测某个锚点当前的取值。
|
|
33
|
+
*
|
|
34
|
+
* @param anchor - 记忆声明的锚点。
|
|
35
|
+
* @param probes - 当前环境的探测上下文。
|
|
36
|
+
* @returns 当前值;**无法探测时返回 `undefined`**(调用方按「未校验」处理,不能判失效)。
|
|
37
|
+
*/
|
|
38
|
+
export declare function probeAnchor(anchor: MemoryAnchor, probes: AnchorProbes): string | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* 锚点是否仍然成立。
|
|
41
|
+
*
|
|
42
|
+
* @returns `true` 成立;`false` 失效;`undefined` **未校验**(探测不到,不判失效——探测
|
|
43
|
+
* 失败与「值变了」是两件事,混起来会误伤)。
|
|
44
|
+
*/
|
|
45
|
+
export declare function anchorHolds(anchor: MemoryAnchor, probes: AnchorProbes): boolean | undefined;
|
package/dist/anchors.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 有效性锚点(2026-09-15 静默记忆机制):把一条记忆绑定到某个**可探测的环境值**上,
|
|
3
|
+
* 值变了就自动失效——解决「版本特性类记忆随环境升级静默变错」的问题。
|
|
4
|
+
*
|
|
5
|
+
* 第一版只做**进程内可探测**的类型:校验跑在会话启动的路径上,探测必须零 IO。
|
|
6
|
+
* 需要跑子进程才能拿到的(如 PowerShell 版本)留待后续——那要先解决「插件直接
|
|
7
|
+
* spawn 会绕过 DSH 的 shell 沙箱」的合规问题,不能顺手开这个口子。
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* 探测某个锚点当前的取值。
|
|
11
|
+
*
|
|
12
|
+
* @param anchor - 记忆声明的锚点。
|
|
13
|
+
* @param probes - 当前环境的探测上下文。
|
|
14
|
+
* @returns 当前值;**无法探测时返回 `undefined`**(调用方按「未校验」处理,不能判失效)。
|
|
15
|
+
*/
|
|
16
|
+
export function probeAnchor(anchor, probes) {
|
|
17
|
+
switch (anchor.kind) {
|
|
18
|
+
case 'env': {
|
|
19
|
+
if (anchor.name === undefined || anchor.name === '')
|
|
20
|
+
return undefined;
|
|
21
|
+
const read = probes.env ?? ((name) => process.env[name]);
|
|
22
|
+
return read(anchor.name);
|
|
23
|
+
}
|
|
24
|
+
case 'tool-list':
|
|
25
|
+
if (probes.toolNames === undefined)
|
|
26
|
+
return undefined;
|
|
27
|
+
return [...probes.toolNames].sort().join(',');
|
|
28
|
+
case 'self-version':
|
|
29
|
+
return probes.selfVersion;
|
|
30
|
+
default:
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 锚点是否仍然成立。
|
|
36
|
+
*
|
|
37
|
+
* @returns `true` 成立;`false` 失效;`undefined` **未校验**(探测不到,不判失效——探测
|
|
38
|
+
* 失败与「值变了」是两件事,混起来会误伤)。
|
|
39
|
+
*/
|
|
40
|
+
export function anchorHolds(anchor, probes) {
|
|
41
|
+
const current = probeAnchor(anchor, probes);
|
|
42
|
+
if (current === undefined)
|
|
43
|
+
return undefined;
|
|
44
|
+
return current === anchor.value;
|
|
45
|
+
}
|