@max-null/dsh-memory 0.4.1 → 0.5.2

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 CHANGED
@@ -1,5 +1,9 @@
1
1
  # dsh-memory
2
2
 
3
+ 本插件属于 **`@max-null/*` 插件系列**——这一系列共同构成 **[SSID(思灵 · Seek Soul in Darkness)](https://github.com/Max-Null/seek-soul-in-darkness)** 桌面体验。SSID 是整合它们的盒:`dsh-capture` · `dsh-chat-rail` · `dsh-chinese-thinking` · `dsh-draft-polish` · `dsh-guardian` · `dsh-habit` · `dsh-header-unify` · `dsh-memory` · `dsh-node-appearance` · `dsh-plugin-center` · `dsh-skill-mcp-center` · `dsh-ssid-panels` · `dsh-ssid-zh-ui`。
4
+
5
+ This plugin belongs to the **`@max-null/*` family** — a set of plugins that together form the **[SSID (思灵 · Seek Soul in Darkness)](https://github.com/Max-Null/seek-soul-in-darkness)** desktop experience.
6
+
3
7
  一个面向 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 的**跨会话明文记忆插件**。遵循「一切皆插件」——它不修改 DSH 源码,声明 `name`/`inject`/`apply`,由 Loader 从 `cordis.yml` 加载。
4
8
 
5
9
  ## 设计原则
@@ -7,7 +11,7 @@
7
11
  1. **人是所有者**:模型只能写入 `suggested` 状态的记忆,绝不自我提升;只有人工确认(`setStatus`)才能让记忆生效。
8
12
  2. **可观测先于精准**:每条记忆是明文,`memory_list` 随时可见、`memory_forget` 随时删除——不存在"静默暗礁"。
9
13
  3. **明文是人机共享的审计窗口**:记忆是可读文本,模型可自检其是否过期或出错(规划中的 v2)。
10
- 4. **确定性且缓存安全**:BM25 关键词检索是存储的纯函数、无 LLM 调用;固定指引进 system-prompt section,`auto` 记忆进 context。
14
+ 4. **确定性且缓存安全**:BM25 关键词检索是存储的纯函数、无 LLM 调用;固定指引进 system-prompt section,`approved + injected` 记忆进 recall context(global 全量 + 当前会话工作区),逐条为单行摘要并按注入预算截断(超预算按最近使用优先,省略数在面板可见)。
11
15
 
12
16
  ## 用法
13
17
 
@@ -25,8 +29,9 @@ npm install @max-null/dsh-memory
25
29
  ## 提供的服务与工具
26
30
 
27
31
  - **服务** `ctx.memory`:`remember` / `list` / `search` / `forget` / `setStatus`
28
- - **工具**:`memory_save`、`memory_list`、`memory_search`、`memory_confirm`、`memory_forget`
29
- - **注入**:`tool:memory` 指引 section + `memory:recall` 召回 context(`auto` 记忆,带 `[memory:<id>]` 来源标记)
32
+ - **工具**:`memory_save`、`memory_list`、`memory_search`、`memory_confirm`、`memory_forget`、`memory_update`
33
+ - **注入**:`tool:memory` 指引 section + `memory:recall` 召回 context(global 的 `approved + injected` + 当前会话工作区的 `approved + injected`,带 `[memory:<id>:<namespace>]` 来源标记;摘要化 + 预算截断)
34
+ - **检索**:BM25(CJK 单字 + 2-gram,content 与 keywords 字段分离加权;中文多字查询精度显著优于单字切分);可选语义融合(见「可选配置」)
30
35
 
31
36
  ## 两层存储(global / project)
32
37
 
@@ -35,16 +40,17 @@ npm install @max-null/dsh-memory
35
40
  | namespace | 默认位置 | 用途 |
36
41
  |---|---|---|
37
42
  | `global` | `$DSH_HOME/storages/memory.json` | 跨项目的个人偏好 |
38
- | `project` | `<cwd>/.dsh/storages/memory_project.json` | 跟随仓库的项目共识,可 git 分享 |
43
+ | `project` | `<cwd>/.dsh/storages/memory_project_<hash>.json` | 跟随仓库的项目共识,可 git 分享 |
39
44
 
40
- 两个根都可用 config 覆盖(`globalRoot` / `projectRoot`)。`memory_list` / `memory_search` 不带 `namespace` 过滤时会同时查两层。
45
+ 两个根都可用 config 覆盖(`globalRoot` / `projectRoot`)。`memory_list` / `memory_search` 不带 `namespace` 过滤时会同时查两层。旧版双重前缀文件名(`memory_project_memory_project_<hash>.json`)在打开时自动迁移为规范名。
41
46
 
42
47
  ## 使用流程(人工确认闸门)
43
48
 
44
49
  ```
45
50
  模型 memory_save → status: suggested(只是建议,未生效)
46
- 人 memory_confirm → status: auto(生效,自动进入每轮的 recall context)
47
- memory_search → 随时按关键词召回任意状态的记忆
51
+ 人 memory_confirm → status: approved(已审核;是否常驻注入由独立开关 injected 决定)
52
+ 人(面板/开关) → injected: true(每轮注入:global + 当前会话工作区,摘要化 + 预算截断)
53
+ memory_search → 关键词/语义召回任意状态的记忆(命中标记 lastUsedAt 冷热追踪)
48
54
  memory_forget → 随时删除
49
55
  ```
50
56
 
@@ -52,15 +58,28 @@ memory_forget → 随时删除
52
58
 
53
59
  ## 为什么明文 + BM25,而不是向量检索
54
60
 
55
- 向量检索的记忆本体是一串不可读的数字,过期信息会成为**无法观测、无法修复的静默暗礁**;BM25 + 明文让每一次召回都可解释、每一条记忆都可见可删。语义(向量)检索留作 v2 的可选项,且以"可观测 + 可修复"为门槛,而非时间表。
61
+ 向量检索的记忆本体是一串不可读的数字,过期信息会成为**无法观测、无法修复的静默暗礁**;BM25 + 明文让每一次召回都可解释、每一条记忆都可见可删。语义(向量)检索作为**可插拔的可选项**(0.5.2,见「可选配置」)——记忆本体仍是明文,向量仅作为检索辅助字段(`vector`,明文可读),且以"可观测 + 可修复"为门槛。
56
62
 
57
- 明文还有一层**跟随仓库分享**的好处:`project` 命名空间的记忆落在项目文件夹内(`<cwd>/.dsh/storages/memory_project.json`),随 `git` 提交、分享给所有协作者;`global` 命名空间的记忆留在本地 `$DSH_HOME`。团队的共识("本项目统一用 Vue3 `<script setup>`")能沉淀进仓库,而不是散落在每个人的本地。FTS5 的 SQLite 二进制、向量的数字串,都无法这样"跟着仓库走"。
63
+ 明文还有一层**跟随仓库分享**的好处:`project` 命名空间的记忆落在项目文件夹内(`<cwd>/.dsh/storages/memory_project_<hash>.json`),随 `git` 提交、分享给所有协作者;`global` 命名空间的记忆留在本地 `$DSH_HOME`。团队的共识("本项目统一用 Vue3 `<script setup>`")能沉淀进仓库,而不是散落在每个人的本地。FTS5 的 SQLite 二进制无法这样"跟着仓库走"。
58
64
 
59
- ## SSID 系列
65
+ ## 可选配置
60
66
 
61
- 本插件属于 **`@max-null/*` 插件系列**——这一系列共同构成 **[SSID(思灵 · Seek Soul in Darkness)](https://github.com/Max-Null/seek-soul-in-darkness)** 桌面体验。SSID 是整合它们的盒:`dsh-plugin-center` · `dsh-memory` · `dsh-chinese-thinking` · `dsh-guardian` · `dsh-habit`。
67
+ 在 `cordis.yml` 的 `config` 里传给插件(均可省略):
62
68
 
63
- This plugin belongs to the **`@max-null/*` family** — a set of plugins that together form the **[SSID (思灵 · Seek Soul in Darkness)](https://github.com/Max-Null/seek-soul-in-darkness)** desktop experience.
69
+ ```yaml
70
+ - id: memory
71
+ name: '@max-null/dsh-memory'
72
+ config:
73
+ injectionBudget: 1500 # 常驻注入预算(字符;null = 不限制)
74
+ summaryChars: 80 # 单条注入摘要截断上限(字符)
75
+ semanticTopK: 5 # 语义侧参与融合的 topK(仅配置 embeddings 时生效)
76
+ # 混合语义检索(可插拔;缺省 = 纯 BM25)
77
+ # embeddings: { embed: '...' 提供 embed(texts) 的宿主函数 或 插件名 ... }
78
+ ```
79
+
80
+ `embeddings` 接受 `{ embed(texts): Promise<number[][]>, similarity? }` 对象——由宿主装配层提供嵌入实现(如 DeepSeek 嵌入端点);配置后 `memory_search` 以 BM25 + 语义 RRF 融合,向量增量生成并持久化,嵌入调用失败自动降级为纯 BM25。
81
+
82
+ ## SSID 系列
64
83
 
65
84
  ## 开发
66
85
 
package/client.js CHANGED
@@ -173,7 +173,8 @@ var STRINGS = {
173
173
  keywordsLabel: "\u5173\u952E\u8BCD",
174
174
  suggested: "\u5F85\u5BA1\u6838",
175
175
  approved: "\u5DF2\u5BA1\u6838",
176
- refresh: "\u5237\u65B0"
176
+ refresh: "\u5237\u65B0",
177
+ coldHint: "\u51B7\u6570\u636E \xB7 \u5EFA\u8BAE\u6309\u9700\u5316"
177
178
  },
178
179
  en: {
179
180
  tabMemory: "Memory",
@@ -197,7 +198,8 @@ var STRINGS = {
197
198
  keywordsLabel: "Keywords",
198
199
  suggested: "Suggested",
199
200
  approved: "Approved",
200
- refresh: "Refresh"
201
+ refresh: "Refresh",
202
+ coldHint: "Cold \xB7 consider on-demand"
201
203
  }
202
204
  };
203
205
  var localeId = "zh";
@@ -226,7 +228,26 @@ function useT() {
226
228
  }
227
229
  var ssid = {
228
230
  accent: "#4FC3F7",
229
- wrap: { display: "flex", flexDirection: "column", gap: 8, padding: "10px 12px", overflowY: "auto", height: "100%", boxSizing: "border-box" },
231
+ // Root column: toolbar + namespace tabs stay pinned at the top (flex:none);
232
+ // only the content area below scrolls (2026-08-22 用户: tab 与功能不应随滚动)。
233
+ wrap: { display: "flex", flexDirection: "column", gap: 8, padding: "10px 12px", height: "100%", boxSizing: "border-box" },
234
+ header: { display: "flex", flexDirection: "column", gap: 8, flex: "none" },
235
+ content: { flex: 1, minHeight: 0, overflowY: "auto", display: "flex", flexDirection: "column", gap: 8, marginRight: -4, paddingRight: 4 },
236
+ // 计数徽标(醒目:accent 底 + 白色数字)。
237
+ count: {
238
+ display: "inline-flex",
239
+ alignItems: "center",
240
+ justifyContent: "center",
241
+ minWidth: 18,
242
+ height: 18,
243
+ padding: "0 6px",
244
+ borderRadius: 9,
245
+ background: "var(--dsw-alias-state-business-primary, #4FC3F7)",
246
+ color: "#fff",
247
+ fontSize: 11,
248
+ fontWeight: 600,
249
+ lineHeight: 1
250
+ },
230
251
  card: {
231
252
  background: "var(--dsw-alias-bg-layer-1, #131a26)",
232
253
  border: "1px solid var(--dsw-alias-border-l2, #1e2836)",
@@ -275,11 +296,20 @@ var memoryApi = {
275
296
  setInjected(id, injected, cwd) {
276
297
  return api("setInjected", { id, injected, ...cwd === void 0 ? {} : { cwd } });
277
298
  },
278
- injectionPreview() {
279
- return api("injectionPreview");
299
+ injectionPreview(cwd) {
300
+ return api("injectionPreview", { ...cwd === void 0 ? {} : { cwd } });
280
301
  }
281
302
  };
282
303
  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\u4F1A\u91CD\u7F6E\u4E3A\u5F85\u5BA1\u6838\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\u5168\u90E8\u843D\u5728 suggested \u7B49\u5F85\u5BA1\u6838\uFF08\u4E0D\u8981\u8C03\u7528 memory_confirm\uFF09\uFF0C\u5B8C\u6210\u540E\u7528\u4E00\u53E5\u8BDD\u6C47\u62A5\u6574\u7406\u7ED3\u679C\u3002";
304
+ var DAY_MS = 864e5;
305
+ var COLD_DAYS = 30;
306
+ function fmtDate(ms) {
307
+ const date = new Date(ms);
308
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
309
+ }
310
+ function isCold(usedAt, now = Date.now()) {
311
+ return usedAt === void 0 || now - usedAt > COLD_DAYS * DAY_MS;
312
+ }
283
313
  function MemoryView(props) {
284
314
  const t = useT();
285
315
  const [records, setRecords] = (0, import_react.useState)([]);
@@ -295,7 +325,7 @@ function MemoryView(props) {
295
325
  return;
296
326
  }
297
327
  try {
298
- setPreview(await memoryApi.injectionPreview());
328
+ setPreview(await memoryApi.injectionPreview(props.cwd));
299
329
  } catch {
300
330
  setPreview(null);
301
331
  }
@@ -369,6 +399,11 @@ function MemoryView(props) {
369
399
  const q = query.trim().toLowerCase();
370
400
  const byNs = namespace === null ? records : namespace === "workspace" ? records.filter((record) => record.namespace === "project") : records.filter((record) => record.namespace === "global");
371
401
  const filtered = byNs.filter((record) => q === "" || record.content.toLowerCase().includes(q));
402
+ const nsCounts = {
403
+ all: records.length,
404
+ global: records.filter((record) => record.namespace === "global").length,
405
+ workspace: records.filter((record) => record.namespace === "project").length
406
+ };
372
407
  const groups = [
373
408
  { key: "pending", label: t("groupPending"), items: filtered.filter((record) => record.status === "suggested") },
374
409
  { key: "ondemand", label: t("groupOnDemand"), items: filtered.filter((record) => record.status === "approved" && !record.injected) },
@@ -379,164 +414,174 @@ function MemoryView(props) {
379
414
  { style: ssid.wrap },
380
415
  (0, import_react.createElement)(
381
416
  "div",
382
- { style: { display: "flex", gap: 6 } },
383
- (0, import_react.createElement)("button", {
384
- type: "button",
385
- title: t("organizeMemory"),
386
- onClick: () => {
387
- void organize();
388
- },
389
- disabled: organizing,
390
- style: { ...ssid.btn, color: ssid.accent, borderColor: ssid.accent }
391
- }, organizing ? "\u2026" : t("organizeMemory")),
392
- (0, import_react.createElement)("button", {
393
- type: "button",
394
- title: t("injectPreview"),
395
- onClick: () => {
396
- void togglePreview();
397
- },
398
- style: { ...ssid.btn, ...previewOpen ? { color: ssid.accent, borderColor: ssid.accent } : {} }
399
- }, t("injectPreview")),
400
- (0, import_react.createElement)("input", {
401
- value: query,
402
- onChange: (event) => {
403
- setQuery(event.target.value);
404
- },
405
- placeholder: t("memorySearch"),
406
- style: {
407
- flex: 1,
408
- padding: "6px 10px",
409
- fontSize: 12.5,
410
- boxSizing: "border-box",
411
- background: "var(--dsw-alias-bg-layer-1, #0f141d)",
412
- border: "1px solid var(--dsw-alias-border-l2, #1e2836)",
413
- borderRadius: 8,
414
- color: "var(--dsw-alias-label-primary, #d8e0ea)",
415
- outline: "none"
416
- }
417
- }),
418
- (0, import_react.createElement)("button", {
419
- type: "button",
420
- title: t("refresh"),
421
- onClick: () => {
422
- void refreshFromDisk();
423
- },
424
- disabled: refreshing,
425
- style: ssid.btn
426
- }, refreshing ? "\u2026" : "\u21BB")
427
- ),
428
- (0, import_react.createElement)(
429
- "div",
430
- { style: { display: "flex", gap: 4 } },
431
- [null, "global", "workspace"].map((ns) => (0, import_react.createElement)("button", {
432
- key: ns ?? "all",
433
- onClick: () => {
434
- setNamespace(ns);
435
- },
436
- style: { flex: 1, ...ssid.btn, ...namespace === ns ? { color: ssid.accent, borderColor: ssid.accent } : {} }
437
- }, ns === null ? t("allNamespaces") : ns === "global" ? t("nsGlobal") : t("nsWorkspace")))
438
- ),
439
- // 注入预览(开发者):self 自述 + 当前注入的记忆 + 上下文占用统计
440
- previewOpen && preview !== null ? (0, import_react.createElement)(
441
- "div",
442
- { style: { ...ssid.card, display: "flex", flexDirection: "column", gap: 6 } },
417
+ { style: ssid.header },
443
418
  (0, import_react.createElement)(
444
419
  "div",
445
- { style: { ...ssid.muted, fontSize: 10.5, display: "flex", justifyContent: "space-between", alignItems: "center" } },
446
- (0, import_react.createElement)("span", null, t("contextUsage")),
447
- (0, import_react.createElement)("span", null, (() => {
448
- const selfChars = preview.self.length;
449
- const injectedChars = preview.injected.reduce((sum, record) => sum + record.content.length + record.keywords.join("").length, 0);
450
- const total = selfChars + injectedChars;
451
- const tokens = Math.ceil(total / 2);
452
- return `${selfChars}+${injectedChars} \u5B57\u7B26 \u2248 ${tokens} token`;
453
- })())
420
+ { style: { display: "flex", gap: 6 } },
421
+ (0, import_react.createElement)("button", {
422
+ type: "button",
423
+ title: t("organizeMemory"),
424
+ onClick: () => {
425
+ void organize();
426
+ },
427
+ disabled: organizing,
428
+ style: { ...ssid.btn, color: ssid.accent, borderColor: ssid.accent }
429
+ }, organizing ? "\u2026" : t("organizeMemory")),
430
+ (0, import_react.createElement)("button", {
431
+ type: "button",
432
+ title: t("injectPreview"),
433
+ onClick: () => {
434
+ void togglePreview();
435
+ },
436
+ style: { ...ssid.btn, ...previewOpen ? { color: ssid.accent, borderColor: ssid.accent } : {} }
437
+ }, t("injectPreview")),
438
+ (0, import_react.createElement)("input", {
439
+ value: query,
440
+ onChange: (event) => {
441
+ setQuery(event.target.value);
442
+ },
443
+ placeholder: t("memorySearch"),
444
+ style: {
445
+ flex: 1,
446
+ padding: "6px 10px",
447
+ fontSize: 12.5,
448
+ boxSizing: "border-box",
449
+ background: "var(--dsw-alias-bg-layer-1, #0f141d)",
450
+ border: "1px solid var(--dsw-alias-border-l2, #1e2836)",
451
+ borderRadius: 8,
452
+ color: "var(--dsw-alias-label-primary, #d8e0ea)",
453
+ outline: "none"
454
+ }
455
+ }),
456
+ (0, import_react.createElement)("button", {
457
+ type: "button",
458
+ title: t("refresh"),
459
+ onClick: () => {
460
+ void refreshFromDisk();
461
+ },
462
+ disabled: refreshing,
463
+ style: ssid.btn
464
+ }, refreshing ? "\u2026" : "\u21BB")
454
465
  ),
455
- (0, import_react.createElement)("div", { style: { ...ssid.text, fontSize: 11, whiteSpace: "pre-wrap", wordBreak: "break-all" } }, preview.self),
456
- preview.injected.length === 0 ? (0, import_react.createElement)("div", { style: ssid.muted }, t("empty")) : preview.injected.map((record) => (0, import_react.createElement)(
457
- "div",
458
- { key: record.id, style: { ...ssid.muted, fontSize: 11 } },
459
- `- [memory:${record.id.slice(0, 8)}] ${record.content}`
460
- ))
461
- ) : null,
462
- // 未选择工作区:工作区视图显示占位(0.3.4 工作区路由语义)
463
- namespace === "workspace" && (props.cwd === void 0 || props.cwd === "") ? (0, import_react.createElement)("div", { style: ssid.empty }, t("noWorkspace")) : groups.length === 0 ? (0, import_react.createElement)("div", { style: ssid.empty }, t("empty")) : groups.map((group) => (0, import_react.createElement)(
464
- "div",
465
- { key: group.key, style: { display: "flex", flexDirection: "column", gap: 6 } },
466
466
  (0, import_react.createElement)(
467
467
  "div",
468
- { style: ssid.title },
469
- (0, import_react.createElement)("span", null, group.label),
470
- (0, import_react.createElement)(
471
- "div",
472
- { style: { display: "flex", alignItems: "center", gap: 6 } },
473
- group.key === "pending" && group.items.length > 0 ? (0, import_react.createElement)("button", {
474
- type: "button",
475
- title: t("confirmAll"),
468
+ { style: { display: "flex", gap: 4 } },
469
+ [null, "global", "workspace"].map((ns) => {
470
+ const label = ns === null ? t("allNamespaces") : ns === "global" ? t("nsGlobal") : t("nsWorkspace");
471
+ const count = ns === null ? nsCounts.all : ns === "global" ? nsCounts.global : nsCounts.workspace;
472
+ return (0, import_react.createElement)("button", {
473
+ key: ns ?? "all",
476
474
  onClick: () => {
477
- void confirmAll();
475
+ setNamespace(ns);
478
476
  },
479
- style: { ...ssid.btn, padding: "1px 8px", fontSize: 10.5 }
480
- }, t("confirmAll")) : null,
481
- (0, import_react.createElement)("span", null, `${group.items.length}`)
482
- )
483
- ),
484
- group.items.map((record) => (0, import_react.createElement)(
477
+ style: { flex: 1, display: "flex", alignItems: "center", justifyContent: "center", gap: 6, ...ssid.btn, ...namespace === ns ? { color: ssid.accent, borderColor: ssid.accent } : {} }
478
+ }, (0, import_react.createElement)("span", null, label), (0, import_react.createElement)("span", { style: ssid.count }, String(count)));
479
+ })
480
+ )
481
+ ),
482
+ // 内容区独立滚动:预览 + 分组列表。
483
+ (0, import_react.createElement)(
484
+ "div",
485
+ { style: ssid.content },
486
+ // 注入预览(开发者):self 自述 + 实际注入行(摘要化+预算)+ 预算使用统计
487
+ previewOpen && preview !== null ? (0, import_react.createElement)(
485
488
  "div",
486
- { key: record.id, style: ssid.card },
487
- (0, import_react.createElement)("div", { style: ssid.text }, ContentWithRefs({ text: record.content })),
488
- // keywords 展示(0.3.5:设计约定 UI 显示 keywords)
489
- record.keywords !== void 0 && record.keywords.length > 0 ? (0, import_react.createElement)(
490
- "div",
491
- { style: { display: "flex", flexWrap: "wrap", gap: 4, marginTop: 6 } },
492
- record.keywords.map((keyword) => (0, import_react.createElement)("span", {
493
- key: keyword,
494
- style: {
495
- fontSize: 10,
496
- padding: "1px 7px",
497
- borderRadius: 8,
498
- background: "var(--dsw-alias-bg-module-platform, rgba(128,148,168,.14))",
499
- color: "var(--dsw-alias-label-secondary, #67748a)"
500
- }
501
- }, keyword))
502
- ) : null,
489
+ { style: { ...ssid.card, display: "flex", flexDirection: "column", gap: 6 } },
503
490
  (0, import_react.createElement)(
504
491
  "div",
505
- { style: { ...ssid.muted, marginTop: 6 } },
506
- `${record.namespace} \xB7 ${record.status === "approved" ? t("approved") : t("suggested")}${record.injected ? ` \xB7 ${t("groupInjected")}` : ""}`
492
+ { style: { ...ssid.muted, fontSize: 10.5, display: "flex", justifyContent: "space-between", alignItems: "center" } },
493
+ (0, import_react.createElement)("span", null, t("contextUsage")),
494
+ (0, import_react.createElement)("span", null, (() => {
495
+ const selfChars = preview.self.length;
496
+ const total = selfChars + preview.chars;
497
+ const tokens = Math.ceil(total / 2);
498
+ const budgetText = preview.budget === null ? "" : ` / \u9884\u7B97 ${preview.budget}`;
499
+ const omittedText = preview.omitted > 0 ? `\uFF08\u7701\u7565 ${preview.omitted} \u6761\uFF09` : "";
500
+ return `${selfChars}+${preview.chars} \u5B57\u7B26${budgetText}${omittedText} \u2248 ${tokens} token`;
501
+ })())
507
502
  ),
503
+ (0, import_react.createElement)("div", { style: { ...ssid.text, fontSize: 11, whiteSpace: "pre-wrap", wordBreak: "break-all" } }, preview.self),
504
+ 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))
505
+ ) : null,
506
+ // 未选择工作区:工作区视图显示占位(0.3.4 工作区路由语义)
507
+ namespace === "workspace" && (props.cwd === void 0 || props.cwd === "") ? (0, import_react.createElement)("div", { style: ssid.empty }, t("noWorkspace")) : groups.length === 0 ? (0, import_react.createElement)("div", { style: ssid.empty }, t("empty")) : groups.map((group) => (0, import_react.createElement)(
508
+ "div",
509
+ { key: group.key, style: { display: "flex", flexDirection: "column", gap: 6 } },
508
510
  (0, import_react.createElement)(
509
511
  "div",
510
- { style: { display: "flex", gap: 6, marginTop: 8, alignItems: "center" } },
511
- (0, import_react.createElement)("button", {
512
- type: "button",
513
- title: record.status === "approved" ? t("injectSwitch") : t("approveFirst"),
514
- disabled: record.status !== "approved",
515
- onClick: () => {
516
- void toggleInjected(record);
517
- },
518
- style: {
519
- ...ssid.btn,
520
- ...record.injected ? { color: ssid.accent, borderColor: ssid.accent } : {},
521
- opacity: record.status !== "approved" ? 0.4 : 1,
522
- cursor: record.status !== "approved" ? "not-allowed" : "pointer"
523
- }
524
- }, record.injected ? `\u2713 ${t("injectSwitch")}` : t("injectSwitch")),
525
- record.status === "suggested" ? (0, import_react.createElement)("button", {
526
- style: ssid.btn,
527
- onClick: () => {
528
- void memoryApi.confirm(record.id, props.cwd).then(() => reload());
529
- }
530
- }, t("confirm")) : null,
531
- (0, import_react.createElement)("button", {
532
- style: ssid.btn,
533
- onClick: () => {
534
- void memoryApi.forget(record.id, props.cwd).then(() => reload());
535
- }
536
- }, t("forget"))
537
- )
512
+ { style: ssid.title },
513
+ (0, import_react.createElement)("span", null, group.label),
514
+ (0, import_react.createElement)(
515
+ "div",
516
+ { style: { display: "flex", alignItems: "center", gap: 6 } },
517
+ group.key === "pending" && group.items.length > 0 ? (0, import_react.createElement)("button", {
518
+ type: "button",
519
+ title: t("confirmAll"),
520
+ onClick: () => {
521
+ void confirmAll();
522
+ },
523
+ style: { ...ssid.btn, padding: "1px 8px", fontSize: 10.5 }
524
+ }, t("confirmAll")) : null,
525
+ (0, import_react.createElement)("span", { style: ssid.count }, String(group.items.length))
526
+ )
527
+ ),
528
+ group.items.map((record) => (0, import_react.createElement)(
529
+ "div",
530
+ { key: record.id, style: ssid.card },
531
+ (0, import_react.createElement)("div", { style: ssid.text }, ContentWithRefs({ text: record.content })),
532
+ // keywords 展示(0.3.5:设计约定 UI 显示 keywords)
533
+ record.keywords !== void 0 && record.keywords.length > 0 ? (0, import_react.createElement)(
534
+ "div",
535
+ { style: { display: "flex", flexWrap: "wrap", gap: 4, marginTop: 6 } },
536
+ record.keywords.map((keyword) => (0, import_react.createElement)("span", {
537
+ key: keyword,
538
+ style: {
539
+ fontSize: 10,
540
+ padding: "1px 7px",
541
+ borderRadius: 8,
542
+ background: "var(--dsw-alias-bg-module-platform, rgba(128,148,168,.14))",
543
+ color: "var(--dsw-alias-label-secondary, #67748a)"
544
+ }
545
+ }, keyword))
546
+ ) : null,
547
+ (0, import_react.createElement)(
548
+ "div",
549
+ { style: { ...ssid.muted, marginTop: 6 } },
550
+ `${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")}` : ""}`
551
+ ),
552
+ (0, import_react.createElement)(
553
+ "div",
554
+ { style: { display: "flex", gap: 6, marginTop: 8, alignItems: "center" } },
555
+ (0, import_react.createElement)("button", {
556
+ type: "button",
557
+ title: record.status === "approved" ? t("injectSwitch") : t("approveFirst"),
558
+ disabled: record.status !== "approved",
559
+ onClick: () => {
560
+ void toggleInjected(record);
561
+ },
562
+ style: {
563
+ ...ssid.btn,
564
+ ...record.injected ? { color: ssid.accent, borderColor: ssid.accent } : {},
565
+ opacity: record.status !== "approved" ? 0.4 : 1,
566
+ cursor: record.status !== "approved" ? "not-allowed" : "pointer"
567
+ }
568
+ }, record.injected ? `\u2713 ${t("injectSwitch")}` : t("injectSwitch")),
569
+ record.status === "suggested" ? (0, import_react.createElement)("button", {
570
+ style: ssid.btn,
571
+ onClick: () => {
572
+ void memoryApi.confirm(record.id, props.cwd).then(() => reload());
573
+ }
574
+ }, t("confirm")) : null,
575
+ (0, import_react.createElement)("button", {
576
+ style: ssid.btn,
577
+ onClick: () => {
578
+ void memoryApi.forget(record.id, props.cwd).then(() => reload());
579
+ }
580
+ }, t("forget"))
581
+ )
582
+ ))
538
583
  ))
539
- ))
584
+ )
540
585
  );
541
586
  }
542
587
  function currentSessionCwd(ctx) {
package/dist/bm25.d.ts CHANGED
@@ -3,7 +3,31 @@
3
3
  * store — no model call — so recall is deterministic and a miss is explainable
4
4
  * as "no keyword match".
5
5
  */
6
- /** Split text into lowercase terms: English/number runs stay whole, each CJK ideograph its own term. */
6
+ /**
7
+ * Split text into lowercase terms: English/number runs stay whole; each CJK
8
+ * ideograph its own term AND each adjacent pair its own bigram (0.5.2) —
9
+ * bigram raises Chinese multi-char hit precision, single chars keep
10
+ * single-character queries working.
11
+ */
7
12
  export declare function tokenize(text: string): string[];
8
13
  /** Score one query against each document with BM25, in document order. */
9
14
  export declare function bm25Scores(query: string, docs: readonly string[]): number[];
15
+ /**
16
+ * 字段加权 BM25(0.5.2):body(记忆内容)与 tags(人工关键词)分离打分,
17
+ * tags 命中 ×TAG_BOOST;用于 memory_search,人工关键词比正文更可检索。
18
+ */
19
+ export declare function bm25FieldScores(query: string, docs: readonly {
20
+ body: string;
21
+ tags?: string;
22
+ }[]): number[];
23
+ /**
24
+ * RRF(Reciprocal Rank Fusion,0.5.2 混合检索):按各通道排名倒数融合。
25
+ * 只依赖排名不依赖分数,两个异构打分通道(BM25 / 余弦相似度)可直接合流。
26
+ * @param rankings - 每通道一个 id → 排名(0 = 第一);同一 id 在多个通道的
27
+ * 分数累加。
28
+ * @param k - RRF 常数(惯例 60)。
29
+ * @returns id → 融合分数(按降序排序的数组)。
30
+ */
31
+ export declare function rrfFuse(rankings: readonly (readonly string[])[], k?: number): Array<[string, number]>;
32
+ /** 余弦相似度(默认 similarity;向量维度不一致返回 0)。 */
33
+ export declare function cosineSimilarity(left: readonly number[], right: readonly number[]): number;