@max-null/dsh-memory 0.5.0 → 0.6.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 +36 -12
- package/client.js +318 -15
- package/dist/bm25.d.ts +25 -1
- package/dist/bm25.js +99 -20
- package/dist/engine.d.ts +115 -3
- package/dist/engine.js +388 -41
- package/dist/index.d.ts +1 -1
- package/dist/index.js +205 -12
- package/dist/injection.d.ts +34 -0
- package/dist/injection.js +52 -0
- package/dist/prompt-files.d.ts +77 -0
- package/dist/prompt-files.js +188 -0
- package/dist/remote.d.ts +9 -5
- package/dist/remote.js +11 -4
- package/dist/routes.js +90 -2
- package/dist/self.js +2 -2
- package/package.json +1 -1
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,`
|
|
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
|
|
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,22 @@ npm install @max-null/dsh-memory
|
|
|
35
40
|
| namespace | 默认位置 | 用途 |
|
|
36
41
|
|---|---|---|
|
|
37
42
|
| `global` | `$DSH_HOME/storages/memory.json` | 跨项目的个人偏好 |
|
|
38
|
-
| `project` | `<cwd>/.dsh/storages/
|
|
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
|
|
|
49
|
+
提示词模板库(0.6.0):`prompt_search / prompt_get / prompt_list / prompt_add` 四个工具管理**模板库**——
|
|
50
|
+
md 文件是唯一事实源(`~/.dsh/prompt-library/*.md` 为 global;`<workspace>/.dsh/prompt-library/` 随工作区分享),
|
|
51
|
+
前端(记忆面板「模板」tab / 模型工具)检索同一份索引;模板存在即生效(`source: agent` 角标标识模型新增),
|
|
52
|
+
**永不注入 system prompt**。
|
|
53
|
+
|
|
44
54
|
```
|
|
45
55
|
模型 memory_save → status: suggested(只是建议,未生效)
|
|
46
|
-
人 memory_confirm → status:
|
|
47
|
-
|
|
56
|
+
人 memory_confirm → status: approved(已审核;是否常驻注入由独立开关 injected 决定)
|
|
57
|
+
人(面板/开关) → injected: true(每轮注入:global + 当前会话工作区,摘要化 + 预算截断)
|
|
58
|
+
memory_search → 关键词/语义召回任意状态的记忆(命中标记 lastUsedAt 冷热追踪)
|
|
48
59
|
memory_forget → 随时删除
|
|
49
60
|
```
|
|
50
61
|
|
|
@@ -52,15 +63,28 @@ memory_forget → 随时删除
|
|
|
52
63
|
|
|
53
64
|
## 为什么明文 + BM25,而不是向量检索
|
|
54
65
|
|
|
55
|
-
向量检索的记忆本体是一串不可读的数字,过期信息会成为**无法观测、无法修复的静默暗礁**;BM25 +
|
|
66
|
+
向量检索的记忆本体是一串不可读的数字,过期信息会成为**无法观测、无法修复的静默暗礁**;BM25 + 明文让每一次召回都可解释、每一条记忆都可见可删。语义(向量)检索作为**可插拔的可选项**(0.5.2,见「可选配置」)——记忆本体仍是明文,向量仅作为检索辅助字段(`vector`,明文可读),且以"可观测 + 可修复"为门槛。
|
|
56
67
|
|
|
57
|
-
明文还有一层**跟随仓库分享**的好处:`project` 命名空间的记忆落在项目文件夹内(`<cwd>/.dsh/storages/
|
|
68
|
+
明文还有一层**跟随仓库分享**的好处:`project` 命名空间的记忆落在项目文件夹内(`<cwd>/.dsh/storages/memory_project_<hash>.json`),随 `git` 提交、分享给所有协作者;`global` 命名空间的记忆留在本地 `$DSH_HOME`。团队的共识("本项目统一用 Vue3 `<script setup>`")能沉淀进仓库,而不是散落在每个人的本地。FTS5 的 SQLite 二进制无法这样"跟着仓库走"。
|
|
58
69
|
|
|
59
|
-
##
|
|
70
|
+
## 可选配置
|
|
60
71
|
|
|
61
|
-
|
|
72
|
+
在 `cordis.yml` 的 `config` 里传给插件(均可省略):
|
|
62
73
|
|
|
63
|
-
|
|
74
|
+
```yaml
|
|
75
|
+
- id: memory
|
|
76
|
+
name: '@max-null/dsh-memory'
|
|
77
|
+
config:
|
|
78
|
+
injectionBudget: 1500 # 常驻注入预算(字符;null = 不限制)
|
|
79
|
+
summaryChars: 80 # 单条注入摘要截断上限(字符)
|
|
80
|
+
semanticTopK: 5 # 语义侧参与融合的 topK(仅配置 embeddings 时生效)
|
|
81
|
+
# 混合语义检索(可插拔;缺省 = 纯 BM25)
|
|
82
|
+
# embeddings: { embed: '...' 提供 embed(texts) 的宿主函数 或 插件名 ... }
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`embeddings` 接受 `{ embed(texts): Promise<number[][]>, similarity? }` 对象——由宿主装配层提供嵌入实现(如 DeepSeek 嵌入端点);配置后 `memory_search` 以 BM25 + 语义 RRF 融合,向量增量生成并持久化,嵌入调用失败自动降级为纯 BM25。
|
|
86
|
+
|
|
87
|
+
## SSID 系列
|
|
64
88
|
|
|
65
89
|
## 开发
|
|
66
90
|
|
package/client.js
CHANGED
|
@@ -153,6 +153,26 @@ function registerSettingsNavIcon(label) {
|
|
|
153
153
|
var STRINGS = {
|
|
154
154
|
zh: {
|
|
155
155
|
tabMemory: "\u8BB0\u5FC6",
|
|
156
|
+
tabPrompt: "\u6A21\u677F",
|
|
157
|
+
promptEmpty: "\u6682\u65E0\u6A21\u677F\uFF08\u70B9\u300C\u65B0\u589E\u6A21\u677F\u300D\u6216\u5F80 ~/.dsh/prompt-library \u653E md \u6587\u4EF6\uFF09",
|
|
158
|
+
promptRefresh: "\u5237\u65B0\u7D22\u5F15",
|
|
159
|
+
promptAdd: "\u65B0\u589E\u6A21\u677F",
|
|
160
|
+
promptInsert: "\u63D2\u5165\u8F93\u5165\u6846",
|
|
161
|
+
promptInserted: "\u5DF2\u63D2\u5165",
|
|
162
|
+
promptInsertUnavailable: "\u65E0\u6D3B\u52A8\u4F1A\u8BDD/\u8F93\u5165\u6846",
|
|
163
|
+
promptDelete: "\u5220\u9664",
|
|
164
|
+
promptDeleteConfirm: "\u5220\u9664\u6A21\u677F\uFF08md \u6587\u4EF6\u4E0E\u7D22\u5F15\u4E00\u5E76\u5220\u9664\uFF09\uFF1F",
|
|
165
|
+
promptFormName: "\u540D\u79F0",
|
|
166
|
+
promptFormContent: "\u6B63\u6587",
|
|
167
|
+
promptFormDimension: "\u7EF4\u5EA6\uFF08\u524D\u7AEF/\u540E\u7AEF/\u2026\uFF09",
|
|
168
|
+
promptFormDifficulty: "\u96BE\u5EA6\uFF08L1-L5/LX\uFF09",
|
|
169
|
+
promptFormTags: "\u6807\u7B7E\uFF08\u9017\u53F7\u5206\u9694\uFF09",
|
|
170
|
+
promptFormFallback: "\u5907\u7528\u63D0\u793A\u8BCD",
|
|
171
|
+
promptFormSubmit: "\u5199\u5165\u6A21\u677F\u5E93",
|
|
172
|
+
promptFormCancel: "\u53D6\u6D88",
|
|
173
|
+
promptSearch: "\u641C\u7D22\u6A21\u677F\u2026",
|
|
174
|
+
promptAgent: "agent",
|
|
175
|
+
promptFilterAll: "\u5168\u90E8",
|
|
156
176
|
memorySearch: "\u641C\u7D22\u8BB0\u5FC6\u2026",
|
|
157
177
|
empty: "\u9ED1\u6697\u4E2D\u672A\u89C1\u7075\u5149",
|
|
158
178
|
confirm: "\u786E\u8BA4",
|
|
@@ -173,10 +193,31 @@ var STRINGS = {
|
|
|
173
193
|
keywordsLabel: "\u5173\u952E\u8BCD",
|
|
174
194
|
suggested: "\u5F85\u5BA1\u6838",
|
|
175
195
|
approved: "\u5DF2\u5BA1\u6838",
|
|
176
|
-
refresh: "\u5237\u65B0"
|
|
196
|
+
refresh: "\u5237\u65B0",
|
|
197
|
+
coldHint: "\u51B7\u6570\u636E \xB7 \u5EFA\u8BAE\u6309\u9700\u5316"
|
|
177
198
|
},
|
|
178
199
|
en: {
|
|
179
200
|
tabMemory: "Memory",
|
|
201
|
+
tabPrompt: "Templates",
|
|
202
|
+
promptEmpty: 'No templates yet (use "Add template" or drop md files into ~/.dsh/prompt-library)',
|
|
203
|
+
promptRefresh: "Refresh index",
|
|
204
|
+
promptAdd: "Add template",
|
|
205
|
+
promptInsert: "Insert into input",
|
|
206
|
+
promptInserted: "Inserted",
|
|
207
|
+
promptInsertUnavailable: "No active session/input",
|
|
208
|
+
promptDelete: "Delete",
|
|
209
|
+
promptDeleteConfirm: "Delete this template (md file + index)?",
|
|
210
|
+
promptFormName: "Name",
|
|
211
|
+
promptFormContent: "Body",
|
|
212
|
+
promptFormDimension: "Dimension (\u524D\u7AEF/\u540E\u7AEF/\u2026)",
|
|
213
|
+
promptFormDifficulty: "Difficulty (L1-L5/LX)",
|
|
214
|
+
promptFormTags: "Tags (comma separated)",
|
|
215
|
+
promptFormFallback: "Fallback prompt",
|
|
216
|
+
promptFormSubmit: "Write to library",
|
|
217
|
+
promptFormCancel: "Cancel",
|
|
218
|
+
promptSearch: "Search templates\u2026",
|
|
219
|
+
promptAgent: "agent",
|
|
220
|
+
promptFilterAll: "All",
|
|
180
221
|
memorySearch: "Search memory\u2026",
|
|
181
222
|
empty: "No spark in the dark",
|
|
182
223
|
confirm: "Confirm",
|
|
@@ -197,7 +238,8 @@ var STRINGS = {
|
|
|
197
238
|
keywordsLabel: "Keywords",
|
|
198
239
|
suggested: "Suggested",
|
|
199
240
|
approved: "Approved",
|
|
200
|
-
refresh: "Refresh"
|
|
241
|
+
refresh: "Refresh",
|
|
242
|
+
coldHint: "Cold \xB7 consider on-demand"
|
|
201
243
|
}
|
|
202
244
|
};
|
|
203
245
|
var localeId = "zh";
|
|
@@ -294,13 +336,254 @@ var memoryApi = {
|
|
|
294
336
|
setInjected(id, injected, cwd) {
|
|
295
337
|
return api("setInjected", { id, injected, ...cwd === void 0 ? {} : { cwd } });
|
|
296
338
|
},
|
|
297
|
-
injectionPreview() {
|
|
298
|
-
return api("injectionPreview");
|
|
339
|
+
injectionPreview(cwd) {
|
|
340
|
+
return api("injectionPreview", { ...cwd === void 0 ? {} : { cwd } });
|
|
341
|
+
},
|
|
342
|
+
// ── 提示词模板库(0.6.0)──
|
|
343
|
+
promptList(cwd) {
|
|
344
|
+
return api("prompt.list", { ...cwd === void 0 ? {} : { cwd } });
|
|
345
|
+
},
|
|
346
|
+
promptGet(nameOrId, cwd) {
|
|
347
|
+
return api("prompt.get", { name: nameOrId, ...cwd === void 0 ? {} : { cwd } });
|
|
348
|
+
},
|
|
349
|
+
promptAdd(payload, cwd) {
|
|
350
|
+
return api("prompt.add", { ...payload, ...cwd === void 0 ? {} : { cwd } });
|
|
351
|
+
},
|
|
352
|
+
promptRemove(nameOrId, cwd) {
|
|
353
|
+
return api("prompt.remove", { name: nameOrId, ...cwd === void 0 ? {} : { cwd } });
|
|
354
|
+
},
|
|
355
|
+
promptRefresh(cwd) {
|
|
356
|
+
return api("prompt.refresh", { ...cwd === void 0 ? {} : { cwd } });
|
|
299
357
|
}
|
|
300
358
|
};
|
|
301
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\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";
|
|
360
|
+
function insertIntoInput(ctx, text) {
|
|
361
|
+
const sessions = ctx.get?.("sessions");
|
|
362
|
+
const conversation = ctx.get?.("conversation");
|
|
363
|
+
if (sessions === void 0 || conversation?.input?.for === void 0) return { ok: false, reason: "unavailable" };
|
|
364
|
+
const current = sessions.list?.getSnapshot?.()?.current;
|
|
365
|
+
if (current === void 0 || current === "") return { ok: false, reason: "no-active-session" };
|
|
366
|
+
const actx = sessions.scope(current);
|
|
367
|
+
if (actx === void 0) return { ok: false, reason: "no-scope" };
|
|
368
|
+
const input = conversation.input.for(actx);
|
|
369
|
+
if (input === void 0) return { ok: false, reason: "no-input" };
|
|
370
|
+
try {
|
|
371
|
+
input.setDraft(text);
|
|
372
|
+
return { ok: true };
|
|
373
|
+
} catch {
|
|
374
|
+
return { ok: false, reason: "setDraft-failed" };
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function PromptView(props) {
|
|
378
|
+
const t = useT();
|
|
379
|
+
const [items, setItems] = (0, import_react.useState)([]);
|
|
380
|
+
const [query, setQuery] = (0, import_react.useState)("");
|
|
381
|
+
const [expanded, setExpanded] = (0, import_react.useState)(null);
|
|
382
|
+
const [detail, setDetail] = (0, import_react.useState)(null);
|
|
383
|
+
const [note, setNote] = (0, import_react.useState)(null);
|
|
384
|
+
const [formOpen, setFormOpen] = (0, import_react.useState)(false);
|
|
385
|
+
const [form, setForm] = (0, import_react.useState)({});
|
|
386
|
+
const reload = async () => {
|
|
387
|
+
try {
|
|
388
|
+
setItems(await memoryApi.promptList(props.cwd));
|
|
389
|
+
} catch {
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
(0, import_react.useEffect)(() => {
|
|
393
|
+
if (props.visible) void reload();
|
|
394
|
+
}, [props.visible]);
|
|
395
|
+
const flash = (text) => {
|
|
396
|
+
setNote(text);
|
|
397
|
+
setTimeout(() => setNote(null), 2e3);
|
|
398
|
+
};
|
|
399
|
+
const q = query.trim().toLowerCase();
|
|
400
|
+
const filtered = items.filter((item) => {
|
|
401
|
+
if (q === "") return true;
|
|
402
|
+
const hay = [
|
|
403
|
+
String(item.name ?? ""),
|
|
404
|
+
String(item.dimension ?? ""),
|
|
405
|
+
String(item.difficulty ?? ""),
|
|
406
|
+
(item.tags ?? []).join(" "),
|
|
407
|
+
String(item.summary ?? "")
|
|
408
|
+
].join(" ").toLowerCase();
|
|
409
|
+
return hay.includes(q);
|
|
410
|
+
});
|
|
411
|
+
const openPrompt = async (item) => {
|
|
412
|
+
const key = String(item.id ?? item.name);
|
|
413
|
+
if (expanded === key) {
|
|
414
|
+
setExpanded(null);
|
|
415
|
+
setDetail(null);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
setExpanded(key);
|
|
419
|
+
try {
|
|
420
|
+
setDetail(await memoryApi.promptGet(String(item.name ?? item.id), props.cwd));
|
|
421
|
+
} catch {
|
|
422
|
+
setDetail(null);
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
const insert = (item) => {
|
|
426
|
+
const text = String(detail?.body ?? item.summary ?? "");
|
|
427
|
+
const result = insertIntoInput(props.ctx, text);
|
|
428
|
+
flash(result.ok ? t("promptInserted") : `${t("promptInsertUnavailable")} (${result.reason ?? "?"})`);
|
|
429
|
+
};
|
|
430
|
+
const remove = async (item) => {
|
|
431
|
+
if (!window.confirm(t("promptDeleteConfirm"))) return;
|
|
432
|
+
await memoryApi.promptRemove(String(item.name ?? item.id), props.cwd).catch(() => null);
|
|
433
|
+
await reload();
|
|
434
|
+
};
|
|
435
|
+
const submitForm = async () => {
|
|
436
|
+
if ((form.name ?? "").trim() === "" || (form.content ?? "").trim() === "") return;
|
|
437
|
+
await memoryApi.promptAdd({
|
|
438
|
+
name: form.name.trim(),
|
|
439
|
+
content: form.content,
|
|
440
|
+
...(form.dimension ?? "").trim() === "" ? {} : { dimension: form.dimension.trim() },
|
|
441
|
+
...(form.difficulty ?? "").trim() === "" ? {} : { difficulty: form.difficulty.trim() },
|
|
442
|
+
...(form.tags ?? "").trim() === "" ? {} : { tags: form.tags.split(",").map((tag) => tag.trim()).filter((tag) => tag !== "") },
|
|
443
|
+
...(form.fallback ?? "").trim() === "" ? {} : { fallback: form.fallback }
|
|
444
|
+
}, props.cwd);
|
|
445
|
+
setForm({});
|
|
446
|
+
setFormOpen(false);
|
|
447
|
+
await reload();
|
|
448
|
+
};
|
|
449
|
+
const field = (key, hint, multiline = false) => (0, import_react.createElement)(multiline ? "textarea" : "input", {
|
|
450
|
+
key,
|
|
451
|
+
value: form[key] ?? "",
|
|
452
|
+
placeholder: hint,
|
|
453
|
+
rows: multiline ? 4 : void 0,
|
|
454
|
+
onChange: (event) => {
|
|
455
|
+
setForm((prev) => ({ ...prev, [key]: event.target.value }));
|
|
456
|
+
},
|
|
457
|
+
style: {
|
|
458
|
+
width: "100%",
|
|
459
|
+
boxSizing: "border-box",
|
|
460
|
+
padding: "6px 10px",
|
|
461
|
+
fontSize: 12,
|
|
462
|
+
fontFamily: "inherit",
|
|
463
|
+
background: "var(--dsw-alias-bg-layer-1, #0f141d)",
|
|
464
|
+
border: "1px solid var(--dsw-alias-border-l2, #1e2836)",
|
|
465
|
+
borderRadius: 6,
|
|
466
|
+
color: "var(--dsw-alias-label-primary, #d8e0ea)",
|
|
467
|
+
outline: "none"
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
return (0, import_react.createElement)(
|
|
471
|
+
"div",
|
|
472
|
+
{ style: { display: "flex", flexDirection: "column", gap: 8, flex: 1, minHeight: 0, overflowY: "auto" } },
|
|
473
|
+
(0, import_react.createElement)(
|
|
474
|
+
"div",
|
|
475
|
+
{ style: { display: "flex", gap: 6 } },
|
|
476
|
+
(0, import_react.createElement)("button", { type: "button", style: { ...ssid.btn, color: ssid.accent, borderColor: ssid.accent }, onClick: () => {
|
|
477
|
+
setFormOpen((open) => !open);
|
|
478
|
+
} }, t("promptAdd")),
|
|
479
|
+
(0, import_react.createElement)("button", { type: "button", style: ssid.btn, onClick: () => {
|
|
480
|
+
void reload();
|
|
481
|
+
} }, t("promptRefresh")),
|
|
482
|
+
(0, import_react.createElement)("input", {
|
|
483
|
+
value: query,
|
|
484
|
+
onChange: (event) => {
|
|
485
|
+
setQuery(event.target.value);
|
|
486
|
+
},
|
|
487
|
+
placeholder: t("promptSearch"),
|
|
488
|
+
style: {
|
|
489
|
+
flex: 1,
|
|
490
|
+
padding: "6px 10px",
|
|
491
|
+
fontSize: 12.5,
|
|
492
|
+
boxSizing: "border-box",
|
|
493
|
+
background: "var(--dsw-alias-bg-layer-1, #0f141d)",
|
|
494
|
+
border: "1px solid var(--dsw-alias-border-l2, #1e2836)",
|
|
495
|
+
borderRadius: 8,
|
|
496
|
+
color: "var(--dsw-alias-label-primary, #d8e0ea)",
|
|
497
|
+
outline: "none"
|
|
498
|
+
}
|
|
499
|
+
}),
|
|
500
|
+
note === null ? null : (0, import_react.createElement)("span", { style: { ...ssid.muted, alignSelf: "center" } }, note)
|
|
501
|
+
),
|
|
502
|
+
formOpen ? (0, import_react.createElement)(
|
|
503
|
+
"div",
|
|
504
|
+
{ style: ssid.card },
|
|
505
|
+
(0, import_react.createElement)(
|
|
506
|
+
"div",
|
|
507
|
+
{ style: { display: "flex", flexDirection: "column", gap: 6 } },
|
|
508
|
+
field("name", t("promptFormName")),
|
|
509
|
+
field("dimension", t("promptFormDimension")),
|
|
510
|
+
field("difficulty", t("promptFormDifficulty")),
|
|
511
|
+
field("tags", t("promptFormTags")),
|
|
512
|
+
field("content", t("promptFormContent"), true),
|
|
513
|
+
field("fallback", t("promptFormFallback"), true),
|
|
514
|
+
(0, import_react.createElement)(
|
|
515
|
+
"div",
|
|
516
|
+
{ style: { display: "flex", gap: 6, justifyContent: "flex-end" } },
|
|
517
|
+
(0, import_react.createElement)("button", { type: "button", style: ssid.btn, onClick: () => {
|
|
518
|
+
setFormOpen(false);
|
|
519
|
+
setForm({});
|
|
520
|
+
} }, t("promptFormCancel")),
|
|
521
|
+
(0, import_react.createElement)("button", { type: "button", style: { ...ssid.btn, color: ssid.accent, borderColor: ssid.accent }, onClick: () => {
|
|
522
|
+
void submitForm();
|
|
523
|
+
} }, t("promptFormSubmit"))
|
|
524
|
+
)
|
|
525
|
+
)
|
|
526
|
+
) : null,
|
|
527
|
+
filtered.length === 0 ? (0, import_react.createElement)("div", { style: ssid.empty }, t("promptEmpty")) : filtered.map((item) => {
|
|
528
|
+
const key = String(item.id ?? item.name);
|
|
529
|
+
const open = expanded === key;
|
|
530
|
+
return (0, import_react.createElement)(
|
|
531
|
+
"div",
|
|
532
|
+
{ key, style: ssid.card },
|
|
533
|
+
(0, import_react.createElement)(
|
|
534
|
+
"div",
|
|
535
|
+
{ style: { cursor: "pointer", display: "flex", flexDirection: "column", gap: 4 }, onClick: () => {
|
|
536
|
+
void openPrompt(item);
|
|
537
|
+
} },
|
|
538
|
+
(0, import_react.createElement)(
|
|
539
|
+
"div",
|
|
540
|
+
{ style: { display: "flex", alignItems: "baseline", gap: 6 } },
|
|
541
|
+
(0, import_react.createElement)("span", { style: { ...ssid.text, fontWeight: 600 } }, String(item.name ?? "")),
|
|
542
|
+
item.source === "agent" ? (0, import_react.createElement)("span", { style: { fontSize: 10, color: "#f7c94f", fontWeight: 600 } }, t("promptAgent")) : null,
|
|
543
|
+
(0, import_react.createElement)(
|
|
544
|
+
"span",
|
|
545
|
+
{ style: { ...ssid.muted, fontSize: 10.5 } },
|
|
546
|
+
[String(item.dimension ?? ""), String(item.difficulty ?? "")].filter((part) => part !== "").join(" \xB7 ")
|
|
547
|
+
)
|
|
548
|
+
),
|
|
549
|
+
(item.tags ?? []).length > 0 ? (0, import_react.createElement)(
|
|
550
|
+
"div",
|
|
551
|
+
{ style: { display: "flex", gap: 4, flexWrap: "wrap" } },
|
|
552
|
+
item.tags.slice(0, 6).map((tag) => (0, import_react.createElement)("span", { key: tag, style: { fontSize: 10, padding: "1px 6px", borderRadius: 999, border: "1px solid var(--dsw-alias-border-l2, #1e2836)", color: "var(--dsw-alias-label-secondary, #67748a)" } }, tag))
|
|
553
|
+
) : null,
|
|
554
|
+
(0, import_react.createElement)("span", { style: ssid.muted }, String(item.summary ?? "").slice(0, 120))
|
|
555
|
+
),
|
|
556
|
+
open ? (0, import_react.createElement)(
|
|
557
|
+
"div",
|
|
558
|
+
{ style: { marginTop: 6 } },
|
|
559
|
+
detail === null ? (0, import_react.createElement)("div", { style: ssid.muted }, "\u2026") : (0, import_react.createElement)("pre", { style: { ...ssid.muted, whiteSpace: "pre-wrap", margin: 0, fontSize: 12, maxHeight: 180, overflowY: "auto" } }, String(detail.body ?? "")),
|
|
560
|
+
(0, import_react.createElement)(
|
|
561
|
+
"div",
|
|
562
|
+
{ style: { display: "flex", gap: 6, marginTop: 6, justifyContent: "flex-end" } },
|
|
563
|
+
(0, import_react.createElement)("button", { type: "button", style: { ...ssid.btn, color: ssid.accent, borderColor: ssid.accent }, onClick: () => {
|
|
564
|
+
insert(item);
|
|
565
|
+
} }, t("promptInsert")),
|
|
566
|
+
(0, import_react.createElement)("button", { type: "button", style: { ...ssid.btn, color: "#f76f4f", borderColor: "#f76f4f" }, onClick: () => {
|
|
567
|
+
void remove(item);
|
|
568
|
+
} }, t("promptDelete"))
|
|
569
|
+
)
|
|
570
|
+
) : null
|
|
571
|
+
);
|
|
572
|
+
})
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
var DAY_MS = 864e5;
|
|
576
|
+
var COLD_DAYS = 30;
|
|
577
|
+
function fmtDate(ms) {
|
|
578
|
+
const date = new Date(ms);
|
|
579
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
|
580
|
+
}
|
|
581
|
+
function isCold(usedAt, now = Date.now()) {
|
|
582
|
+
return usedAt === void 0 || now - usedAt > COLD_DAYS * DAY_MS;
|
|
583
|
+
}
|
|
302
584
|
function MemoryView(props) {
|
|
303
585
|
const t = useT();
|
|
586
|
+
const [mode, setMode] = (0, import_react.useState)("mem");
|
|
304
587
|
const [records, setRecords] = (0, import_react.useState)([]);
|
|
305
588
|
const [query, setQuery] = (0, import_react.useState)("");
|
|
306
589
|
const [namespace, setNamespace] = (0, import_react.useState)(null);
|
|
@@ -314,7 +597,7 @@ function MemoryView(props) {
|
|
|
314
597
|
return;
|
|
315
598
|
}
|
|
316
599
|
try {
|
|
317
|
-
setPreview(await memoryApi.injectionPreview());
|
|
600
|
+
setPreview(await memoryApi.injectionPreview(props.cwd));
|
|
318
601
|
} catch {
|
|
319
602
|
setPreview(null);
|
|
320
603
|
}
|
|
@@ -398,9 +681,32 @@ function MemoryView(props) {
|
|
|
398
681
|
{ key: "ondemand", label: t("groupOnDemand"), items: filtered.filter((record) => record.status === "approved" && !record.injected) },
|
|
399
682
|
{ key: "injected", label: t("groupInjected"), items: filtered.filter((record) => record.status === "approved" && record.injected) }
|
|
400
683
|
].filter((group) => group.items.length > 0);
|
|
684
|
+
const modeTabs = (0, import_react.createElement)(
|
|
685
|
+
"div",
|
|
686
|
+
{ style: { display: "flex", gap: 4 } },
|
|
687
|
+
["mem", "prompt"].map(
|
|
688
|
+
(m) => (0, import_react.createElement)("button", {
|
|
689
|
+
key: m,
|
|
690
|
+
type: "button",
|
|
691
|
+
onClick: () => {
|
|
692
|
+
setMode(m);
|
|
693
|
+
},
|
|
694
|
+
style: { flex: 1, ...ssid.btn, ...mode === m ? { color: ssid.accent, borderColor: ssid.accent } : {} }
|
|
695
|
+
}, m === "mem" ? t("tabMemory") : t("tabPrompt"))
|
|
696
|
+
)
|
|
697
|
+
);
|
|
698
|
+
if (mode === "prompt") {
|
|
699
|
+
return (0, import_react.createElement)(
|
|
700
|
+
"div",
|
|
701
|
+
{ style: ssid.wrap },
|
|
702
|
+
modeTabs,
|
|
703
|
+
(0, import_react.createElement)(PromptView, { visible: props.visible, cwd: props.cwd, ctx: props.ctx })
|
|
704
|
+
);
|
|
705
|
+
}
|
|
401
706
|
return (0, import_react.createElement)(
|
|
402
707
|
"div",
|
|
403
708
|
{ style: ssid.wrap },
|
|
709
|
+
modeTabs,
|
|
404
710
|
(0, import_react.createElement)(
|
|
405
711
|
"div",
|
|
406
712
|
{ style: ssid.header },
|
|
@@ -472,7 +778,7 @@ function MemoryView(props) {
|
|
|
472
778
|
(0, import_react.createElement)(
|
|
473
779
|
"div",
|
|
474
780
|
{ style: ssid.content },
|
|
475
|
-
// 注入预览(开发者):self 自述 +
|
|
781
|
+
// 注入预览(开发者):self 自述 + 实际注入行(摘要化+预算)+ 预算使用统计
|
|
476
782
|
previewOpen && preview !== null ? (0, import_react.createElement)(
|
|
477
783
|
"div",
|
|
478
784
|
{ style: { ...ssid.card, display: "flex", flexDirection: "column", gap: 6 } },
|
|
@@ -482,18 +788,15 @@ function MemoryView(props) {
|
|
|
482
788
|
(0, import_react.createElement)("span", null, t("contextUsage")),
|
|
483
789
|
(0, import_react.createElement)("span", null, (() => {
|
|
484
790
|
const selfChars = preview.self.length;
|
|
485
|
-
const
|
|
486
|
-
const total = selfChars + injectedChars;
|
|
791
|
+
const total = selfChars + preview.chars;
|
|
487
792
|
const tokens = Math.ceil(total / 2);
|
|
488
|
-
|
|
793
|
+
const budgetText = preview.budget === null ? "" : ` / \u9884\u7B97 ${preview.budget}`;
|
|
794
|
+
const omittedText = preview.omitted > 0 ? `\uFF08\u7701\u7565 ${preview.omitted} \u6761\uFF09` : "";
|
|
795
|
+
return `${selfChars}+${preview.chars} \u5B57\u7B26${budgetText}${omittedText} \u2248 ${tokens} token`;
|
|
489
796
|
})())
|
|
490
797
|
),
|
|
491
798
|
(0, import_react.createElement)("div", { style: { ...ssid.text, fontSize: 11, whiteSpace: "pre-wrap", wordBreak: "break-all" } }, preview.self),
|
|
492
|
-
preview.
|
|
493
|
-
"div",
|
|
494
|
-
{ key: record.id, style: { ...ssid.muted, fontSize: 11 } },
|
|
495
|
-
`- [memory:${record.id.slice(0, 8)}] ${record.content}`
|
|
496
|
-
))
|
|
799
|
+
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))
|
|
497
800
|
) : null,
|
|
498
801
|
// 未选择工作区:工作区视图显示占位(0.3.4 工作区路由语义)
|
|
499
802
|
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)(
|
|
@@ -539,7 +842,7 @@ function MemoryView(props) {
|
|
|
539
842
|
(0, import_react.createElement)(
|
|
540
843
|
"div",
|
|
541
844
|
{ style: { ...ssid.muted, marginTop: 6 } },
|
|
542
|
-
`${record.namespace} \xB7 ${record.status === "approved" ? t("approved") : t("suggested")}${record.injected ? ` \xB7 ${t("groupInjected")}` : ""}`
|
|
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")}` : ""}`
|
|
543
846
|
),
|
|
544
847
|
(0, import_react.createElement)(
|
|
545
848
|
"div",
|
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
|
-
/**
|
|
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;
|