dsh-memory-github 0.3.0 → 0.4.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/CHANGELOG.md +25 -0
- package/README.md +31 -8
- package/lib/index.js +71 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# 更新日志
|
|
2
2
|
|
|
3
|
+
## 0.4.0 — 2026-10-25
|
|
4
|
+
|
|
5
|
+
定位从「又一个跨会话记忆插件」改成「**多端共享记忆**」——本地记忆库的同类插件已有上百个,
|
|
6
|
+
多设备共用一份记忆才是这个项目的差异点。
|
|
7
|
+
|
|
8
|
+
### 新增
|
|
9
|
+
|
|
10
|
+
- **`autoRecall` 自动召回(默认开)**:每轮从当前用户消息抽检索词(英文整词、中文双字滑窗),
|
|
11
|
+
给归档打分(标题 ×3 / 标签 ×2 / 正文 ×1),把最相关的几条直接注入系统提示。
|
|
12
|
+
不需要模型"想起来去 `memory_search`"——这是竞品最影响体感的能力,用纯词袋实现,
|
|
13
|
+
不发 LLM 请求、不引入数据库。
|
|
14
|
+
- `autoRecall: false` 关闭
|
|
15
|
+
- `autoRecallLimit: 5` 最多注入几条
|
|
16
|
+
- `autoRecallMinScore: 4` 分数门槛(调高更保守,避免无关记忆干扰)
|
|
17
|
+
- 用户消息文本从 `user/message` 事件的 `content` 取(兼容字符串与 block 数组两种形态)。
|
|
18
|
+
|
|
19
|
+
### 文档
|
|
20
|
+
|
|
21
|
+
- README 重写开头:说明「为什么不跟本地库插件比功能」、适合与不适合的场景
|
|
22
|
+
- 特性列表、配置示例补上 `autoRecall` 三项
|
|
23
|
+
|
|
24
|
+
### 测试
|
|
25
|
+
|
|
26
|
+
- 冒烟测试 63 → **68 条**(覆盖:命中相关归档、不注入无关归档、无相关时不硬塞、关闭开关)
|
|
27
|
+
|
|
3
28
|
## 0.3.0 — 2026-10-25
|
|
4
29
|
|
|
5
30
|
参考同类插件(dsh-prime-memory、meow-memory、@try-works/dsh-memory、@modusensus/dsh-mneme 等
|
package/README.md
CHANGED
|
@@ -3,22 +3,39 @@
|
|
|
3
3
|
[](https://github.com/Administrator016/dsh-memory-github/actions/workflows/test.yml)
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
|
|
6
|
-
给 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)
|
|
6
|
+
给 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 用的**多端共享记忆插件**。
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
生态里同类插件有上百个(`dsh-prime-memory`、`meow-memory`、`dsh-mnemonic`…),绝大多数是
|
|
9
|
+
**本地库**:SQLite、向量索引、面板 UI,功能很强,但**只在那一台机器上**。
|
|
10
|
+
这个插件的取舍相反——不引入数据库、不调 embedding,只用一个目录 + 一个 git 仓库,
|
|
11
|
+
把「手机 / 电脑 / 移动硬盘」这些设备连成**同一份记忆**:
|
|
12
|
+
|
|
13
|
+
- 一台机当**整理端**(`role: primary`)负责写和推
|
|
14
|
+
- 其余机器当**子端**(`role: child`)只读,想留话就写 `inbox/`,整理端处理完归档
|
|
15
|
+
- 换机器:`git clone` 或拷贝目录即恢复;连不联网都能用
|
|
10
16
|
|
|
11
17
|
> 记忆分两层:**核心记忆**(`MEMORY.md`,每轮注入系统提示,硬上限可配)和**归档记忆**
|
|
12
|
-
> (`notes/*.md` + `index.json
|
|
18
|
+
> (`notes/*.md` + `index.json`,按关键词检索 + **自动召回**)。模型自己决定什么时候写、写什么。
|
|
13
19
|
|
|
14
20
|
## 特性
|
|
15
21
|
|
|
16
22
|
- 🧠 **两段式记忆**:核心记忆常驻注入,归档记忆按需检索(`memory_search` / `memory_read`)
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
23
|
+
- 🎯 **自动召回**:每轮按你的消息自动挑几条相关归档注入(无需模型想起来去 search)
|
|
24
|
+
- 🌐 **多端共享**:`primary` / `child` 角色分工,GitHub 做同步总线
|
|
25
|
+
- 💾 **零依赖**:一个目录就是记忆库(Markdown + JSON),可读、可审计、可 `git diff`
|
|
26
|
+
- ☁️ **可选 GitHub**:写入自动 commit + push,启动自动 pull,冲突自动 `rebase --abort`
|
|
27
|
+
- 🔒 **只读模式**:`readOnly: true` 只注册读工具,适合"手机写入、电脑只读"这类分工
|
|
20
28
|
- 🩹 **索引自愈**:`index.json` 丢了会从 `notes/` 重建,不会因为一个文件损坏就丢记忆
|
|
21
|
-
- 🧪
|
|
29
|
+
- 🧪 **自带测试**:`node test/smoke.mjs`(68 条断言)+ `test/git-backend.mjs`(12 条),不依赖 DSH 运行时
|
|
30
|
+
|
|
31
|
+
## 什么时候适合用它
|
|
32
|
+
|
|
33
|
+
✅ 你有多台设备(手机 + 电脑),想让它们记住同一批事
|
|
34
|
+
✅ 你想知道 AI 到底记住了什么(纯 Markdown,直接打开看)
|
|
35
|
+
✅ 记忆要能带走、能版本管理、能自己审查
|
|
36
|
+
|
|
37
|
+
❌ 你要的是单机最强检索(那去用带 BM25 / 向量库的插件)
|
|
38
|
+
❌ 你要的是聊天框里的记忆面板 UI(本项目没有客户端插件)
|
|
22
39
|
|
|
23
40
|
## 两种后端
|
|
24
41
|
|
|
@@ -29,6 +46,7 @@
|
|
|
29
46
|
| 多设备 | 各存各的 | 共享一份,自动 pull / push |
|
|
30
47
|
| 依赖 | 无 | `git` 可执行文件 |
|
|
31
48
|
|
|
49
|
+
|
|
32
50
|
## 安装
|
|
33
51
|
|
|
34
52
|
**方式一:npm(推荐)**
|
|
@@ -81,6 +99,11 @@ dsh plugin --profile desktop add link:/path/to/dsh-memory-github
|
|
|
81
99
|
pushOnTurnEnd: false # 每轮结束若有未提交改动就推送
|
|
82
100
|
remindEvery: 6 # 每 N 条用户消息没动记忆 → 注入提醒(0=关)
|
|
83
101
|
staleMinutes: 0 # 距上次同步超过 N 分钟 → 提示先 pull(0=关)
|
|
102
|
+
autoRecall: true # 按当前用户消息自动召回相关归档(默认开)
|
|
103
|
+
autoRecallLimit: 5 # 自动召回最多注入几条
|
|
104
|
+
autoRecallMinScore: 4 # 自动召回打分门槛,调高更保守
|
|
105
|
+
recentLimit: 0 # 额外注入最近 N 条归档标题(0=关)
|
|
106
|
+
dedupeHint: true # 写入时提示与已有归档高度相似
|
|
84
107
|
commitMessage: 'memory: {tool}' # {tool} 会替换成触发的工具名
|
|
85
108
|
gitBin: git # git 可执行文件(不在 PATH 就写绝对路径)
|
|
86
109
|
authorName: dsh-memory
|
package/lib/index.js
CHANGED
|
@@ -35,6 +35,9 @@ const DEFAULTS = {
|
|
|
35
35
|
remindEvery: 6, // 每 N 条用户消息、且期间没动过记忆 → 提醒(0=关)
|
|
36
36
|
staleMinutes: 0, // 距上次同步超过 N 分钟 → 提醒(0=关)
|
|
37
37
|
recentLimit: 0, // 每轮额外注入最近 N 条归档的标题与标签(0=关)
|
|
38
|
+
autoRecall: true, // 按当前用户消息自动召回相关归档并注入(不用模型主动 search)
|
|
39
|
+
autoRecallLimit: 5, // 自动召回最多注入几条
|
|
40
|
+
autoRecallMinScore: 4, // 自动召回打分门槛,越高越保守
|
|
38
41
|
dedupeHint: true, // 写入时检查是否与已有归档高度相似
|
|
39
42
|
commitMessage: 'memory: {tool}',
|
|
40
43
|
gitBin: 'git',
|
|
@@ -260,6 +263,69 @@ function recentSection(p, limit) {
|
|
|
260
263
|
return ['', `### 最近归档(${notes.length} 条,细节用 memory_search)`, ...lines].join('\n');
|
|
261
264
|
}
|
|
262
265
|
|
|
266
|
+
/**
|
|
267
|
+
* 从用户消息里抽检索词:英文/数字按整词,中文按双字滑窗(中文没有空格,整句当词反而匹配不上)。
|
|
268
|
+
*/
|
|
269
|
+
function recallTerms(text) {
|
|
270
|
+
const terms = new Set();
|
|
271
|
+
for (const w of String(text ?? '').toLowerCase().split(/[^\p{L}\p{N}]+/u)) {
|
|
272
|
+
if (!w) continue;
|
|
273
|
+
if (/^[a-z0-9]+$/.test(w)) {
|
|
274
|
+
if (w.length > 1) terms.add(w);
|
|
275
|
+
} else {
|
|
276
|
+
for (let i = 0; i < w.length - 1; i += 1) terms.add(w.slice(i, i + 2));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return [...terms].slice(0, 150);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* 自动召回:拿当前用户消息的词去给归档打分,注入最相关的几条。
|
|
284
|
+
* 竞品的卖点就是"不用问它就记得",这里用纯词袋实现,不发 LLM 请求、不需要数据库。
|
|
285
|
+
*/
|
|
286
|
+
function autoRecallSection(p, config, query) {
|
|
287
|
+
if (config.autoRecall === false) return '';
|
|
288
|
+
const limit = Number(config.autoRecallLimit);
|
|
289
|
+
if (!Number.isFinite(limit) || limit <= 0) return '';
|
|
290
|
+
const terms = recallTerms(query);
|
|
291
|
+
if (!terms.length) return '';
|
|
292
|
+
const minScore = Number.isFinite(Number(config.autoRecallMinScore)) ? Number(config.autoRecallMinScore) : 4;
|
|
293
|
+
|
|
294
|
+
const scored = [];
|
|
295
|
+
for (const n of loadIndex(p)) {
|
|
296
|
+
const parsed = parseNoteFile(readTextSafe(join(p.notes, `${n.id}.md`), ''));
|
|
297
|
+
const title = String(n.title ?? '').toLowerCase();
|
|
298
|
+
const tags = (n.tags ?? []).join(' ').toLowerCase();
|
|
299
|
+
const body = (parsed.content ?? '').toLowerCase();
|
|
300
|
+
let score = 0;
|
|
301
|
+
for (const t of terms) {
|
|
302
|
+
if (title.includes(t)) score += 3;
|
|
303
|
+
if (tags.includes(t)) score += 2;
|
|
304
|
+
if (body.includes(t)) score += 1;
|
|
305
|
+
}
|
|
306
|
+
if (score >= minScore) scored.push({ n, score });
|
|
307
|
+
}
|
|
308
|
+
if (!scored.length) return '';
|
|
309
|
+
scored.sort((a, b) => b.score - a.score);
|
|
310
|
+
const top = scored.slice(0, Math.floor(limit));
|
|
311
|
+
const lines = top.map(({ n }) => `- \`${n.id}\` ${n.title}${(n.tags ?? []).length ? ' #' + n.tags.join(' #') : ''}`);
|
|
312
|
+
return [
|
|
313
|
+
'',
|
|
314
|
+
`### 可能与当前任务相关的归档(自动召回 ${top.length} 条,细节用 memory_read / memory_search)`,
|
|
315
|
+
...lines,
|
|
316
|
+
].join('\n');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** 把一条 user/message 事件的 content 取成纯文本。 */
|
|
320
|
+
function messageText(data) {
|
|
321
|
+
const c = data?.content;
|
|
322
|
+
if (typeof c === 'string') return c;
|
|
323
|
+
if (Array.isArray(c)) {
|
|
324
|
+
return c.map((b) => (typeof b === 'string' ? b : (b?.text ?? ''))).join('\n');
|
|
325
|
+
}
|
|
326
|
+
return '';
|
|
327
|
+
}
|
|
328
|
+
|
|
263
329
|
// ---------------------------------------------------------------- core 记忆
|
|
264
330
|
|
|
265
331
|
function coreLimit(config) {
|
|
@@ -527,7 +593,7 @@ export function apply(ctx, config0) {
|
|
|
527
593
|
const readOnly = Boolean(config.readOnly) || config.role === 'child';
|
|
528
594
|
|
|
529
595
|
// 提醒状态:消息计数 / 最近一次记忆活动 / 最近一次成功同步
|
|
530
|
-
const syncState = { count: 0, lastActivity: activityStamp(p), lastSyncAt: Date.now() };
|
|
596
|
+
const syncState = { count: 0, lastActivity: activityStamp(p), lastSyncAt: Date.now(), lastUserText: '' };
|
|
531
597
|
|
|
532
598
|
const doPull = () => {
|
|
533
599
|
const r = gitPull(p, config);
|
|
@@ -576,7 +642,9 @@ export function apply(ctx, config0) {
|
|
|
576
642
|
name: 'dsh-memory-github:core',
|
|
577
643
|
order: 60,
|
|
578
644
|
text: () => {
|
|
579
|
-
const base = coreSection(readCore(p), coreLimit(config))
|
|
645
|
+
const base = coreSection(readCore(p), coreLimit(config))
|
|
646
|
+
+ recentSection(p, config.recentLimit)
|
|
647
|
+
+ autoRecallSection(p, config, syncState.lastUserText);
|
|
580
648
|
const stamp = activityStamp(p);
|
|
581
649
|
if (stamp > syncState.lastActivity) {
|
|
582
650
|
// 记忆被写过(本进程写的,或别的进程/设备同步下来的),计数归零
|
|
@@ -609,6 +677,7 @@ export function apply(ctx, config0) {
|
|
|
609
677
|
if (!event || typeof event.type !== 'string') return;
|
|
610
678
|
if (event.type === 'user/message') {
|
|
611
679
|
syncState.count += 1;
|
|
680
|
+
syncState.lastUserText = messageText(event.data);
|
|
612
681
|
} else if (event.type === 'turn/end' && githubMode && config.pushOnTurnEnd && !readOnly) {
|
|
613
682
|
enqueue(() => {
|
|
614
683
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-memory-github",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Cross-session memory for DeepSeek Harness. Local memory store by default, optional GitHub-backed sync (git), with primary/child roles for multi-device setups.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|