Skip to content

Commit 2e51597

Browse files
author
James
committed
feat(import-markdown): add dedup, BOM/CRLF fixes, bullet formats, config options + tests
## 實作改善(相對於原本的 PR CortexReach#426) ### 新增 CLI 選項 - --dedup:啟用 scope-aware exact match 去重(避免重複匯入) - --min-text-length <n>:設定最短文字長度門檻(預設 5) - --importance <n>:設定匯入記憶的 importance 值(預設 0.7) ### Bug 修復 - UTF-8 BOM 處理:讀檔後主動移除 \ufeFF prefix - CRLF 正規化:改用 split(/\r?\n/) 同時支援 CRLF 和 LF - Bullet 格式擴展:從只支援 '- ' 擴展到支援 '- '、'* '、'+ ' 三種 ### 新增測試 - test/import-markdown/import-markdown.test.mjs:完整單元測試 - BOM handling - CRLF normalization - Extended bullet formats (dash/star/plus) - minTextLength 參數 - importance 參數 - Dedup logic(scope-aware exact match) - Dry-run mode - Continue on error ### 分析文件 - test/import-markdown/ANALYSIS.md:完整分析報告 - 效益分析(真實檔案 655 筆記錄實測) - 3 個程式碼缺口分析 - 建議的 5 個新 config 欄位 - 功能條列式說明 - test/import-markdown/recall-benchmark.py:實際 LanceDB 查詢對比腳本 - 實測結果:7/8 個關鍵字在 Markdown 有但 LanceDB 找不到 - 證明 import-markdown 的實際價值 ## 實測效果(真實記憶檔案) - James 的 workspace:MEMORY.md(20 筆)+ 30 個 daily notes(633 筆)= 653 筆記錄 - 無 dedup:每次執行浪費 50%(重複匯入) - 有 dedup:第二次執行 100% skip,節省 644 次 embedder API 呼叫 - 關鍵字對比:7/8 個測試關鍵字在 Markdown 有、LanceDB 無 ## 建議新增的 Config(共 5 項,預設值 = 現在行為,向下相容) - importMarkdown.dedup: boolean = false - importMarkdown.defaultScope: string = global - importMarkdown.minTextLength: number = 5 - importMarkdown.importanceDefault: number = 0.7 - importMarkdown.workspaceFilter: string[] = [] Closes: PR CortexReach#426 (CortexReach/memory-lancedb-pro)
1 parent ab501f5 commit 2e51597

4 files changed

Lines changed: 885 additions & 7 deletions

File tree

cli.ts

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1049,6 +1049,20 @@ export function registerMemoryCLI(program: Command, context: CLIContext): void {
10491049
"--openclaw-home <path>",
10501050
"OpenClaw home directory (default: ~/.openclaw)",
10511051
)
1052+
.option(
1053+
"--dedup",
1054+
"Skip entries already in store (scope-aware exact match, requires store.bm25Search)",
1055+
)
1056+
.option(
1057+
"--min-text-length <n>",
1058+
"Minimum text length to import (default: 5)",
1059+
"5",
1060+
)
1061+
.option(
1062+
"--importance <n>",
1063+
"Importance score for imported entries, 0.0-1.0 (default: 0.7)",
1064+
"0.7",
1065+
)
10521066
.action(async (workspaceGlob, options) => {
10531067
const openclawHome = options.openclawHome
10541068
? path.resolve(options.openclawHome)
@@ -1116,32 +1130,53 @@ export function registerMemoryCLI(program: Command, context: CLIContext): void {
11161130
}
11171131

11181132
const targetScope = options.scope || "global";
1133+
const minTextLength = parseInt(options.minTextLength ?? "5", 10);
1134+
const importanceDefault = parseFloat(options.importance ?? "0.7");
1135+
const dedupEnabled = !!options.dedup;
11191136

11201137
// Parse each file for memory entries (lines starting with "- ")
11211138
for (const { filePath, scope } of mdFiles) {
11221139
foundFiles++;
11231140
const { readFile } = await import("node:fs/promises");
1124-
const content = await readFile(filePath, "utf-8");
1125-
const lines = content.split("\n");
1141+
let content = await readFile(filePath, "utf-8");
1142+
// Strip UTF-8 BOM (e.g. from Windows Notepad-saved files)
1143+
content = content.replace(/^\uFEFF/, "");
1144+
// Normalize line endings: handle both CRLF (\r\n) and LF (\n)
1145+
const lines = content.split(/\r?\n/);
11261146

11271147
for (const line of lines) {
11281148
// Skip non-memory lines
1129-
if (!line.startsWith("- ")) continue;
1149+
// Supports: "- text", "* text", "+ text" (standard Markdown bullet formats)
1150+
if (!/^[-*+]\s/.test(line)) continue;
11301151
const text = line.slice(2).trim();
1131-
if (text.length < 5) { skipped++; continue; }
1152+
if (text.length < minTextLength) { skipped++; continue; }
11321153

11331154
if (options.dryRun) {
11341155
console.log(` [dry-run] would import: ${text.slice(0, 80)}...`);
11351156
imported++;
11361157
continue;
11371158
}
11381159

1160+
// ── Deduplication check (scope-aware exact match) ───────────────────
1161+
if (dedupEnabled) {
1162+
try {
1163+
const existing = await context.store.bm25Search(text, 1, [targetScope]);
1164+
if (existing.length > 0 && existing[0].entry.text === text) {
1165+
skipped++;
1166+
console.log(` [skip] already imported: ${text.slice(0, 60)}${text.length > 60 ? "..." : ""}`);
1167+
continue;
1168+
}
1169+
} catch {
1170+
// bm25Search not available on this store implementation; proceed with import
1171+
}
1172+
}
1173+
11391174
try {
11401175
const vector = await context.embedder!.embedQuery(text);
11411176
await context.store.store({
11421177
text,
11431178
vector,
1144-
importance: 0.7,
1179+
importance: importanceDefault,
11451180
category: "other",
11461181
scope: targetScope,
11471182
metadata: { importedFrom: filePath, sourceScope: scope },
@@ -1155,9 +1190,9 @@ export function registerMemoryCLI(program: Command, context: CLIContext): void {
11551190
}
11561191

11571192
if (options.dryRun) {
1158-
console.log(`\nDRY RUN — found ${foundFiles} files, ${imported} entries would be imported, ${skipped} skipped`);
1193+
console.log(`\nDRY RUN — found ${foundFiles} files, ${imported} entries would be imported, ${skipped} skipped${dedupEnabled ? " [dedup enabled]" : ""}`);
11591194
} else {
1160-
console.log(`\nImport complete: ${imported} imported, ${skipped} skipped (scanned ${foundFiles} files)`);
1195+
console.log(`\nImport complete: ${imported} imported, ${skipped} skipped (scanned ${foundFiles} files)${dedupEnabled ? " [dedup enabled]" : ""}`);
11611196
}
11621197
});
11631198

test/import-markdown/ANALYSIS.md

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
# PR #426 import-markdown 完整分析報告
2+
3+
> 日期:2026-03-31
4+
> 目標:PR #426(CortexReach/memory-lancedb-pro)
5+
> 狀態:分析完成,待 James 決定是否實作
6+
7+
---
8+
9+
## 一、PR 重要性
10+
11+
**解決 Issue #344:dual-memory 架構的根本矛盾**
12+
13+
memory-lancedb-pro 有兩層記憶,但長期是斷裂的:
14+
- **Markdown 層**`MEMORY.md``memory/`)→ 人類可讀,agent 持續寫入
15+
- **LanceDB 層**(向量資料庫)→ recall 只查這裡
16+
17+
結果:重要的記憶寫進 Markdown 了,但搜尋時根本找不到。
18+
19+
`import-markdown` 把兩層打通,讓所有歷史累積的 Markdown 記憶搬進 LanceDB,成為一套完整的 workflow。
20+
21+
---
22+
23+
## 二、PR 現況摘要
24+
25+
| 項目 | 內容 |
26+
|------|------|
27+
| PR 連結 | https://github.com/CortexReach/memory-lancedb-pro/pull/426 |
28+
| 標題 | `feat: add import-markdown CLI command` |
29+
| 狀態 | `OPEN`(等待 Codex/maintainer 審查) |
30+
| 作者 | `jlin53882`(James 的帳號)|
31+
| 主要實作 | `cli.ts` +125 行,`import-markdown` 子命令 |
32+
| 觸發來源 | Issue #344(dual-memory 混淆)|
33+
34+
---
35+
36+
## 三、實測測試結果
37+
38+
**測試封包位置:** `C:\Users\admin\Desktop\memory-lancedb-pro-import-markdown-test`
39+
**執行方式:** `npm test``tsx test-runner.ts`
40+
41+
### 3.1 全部測試結果(12 項,共 30 個 assert)
42+
43+
| # | 測試項目 | 結果 |
44+
|---|----------|------|
45+
| 1 | 檔案路徑解析(MEMORY.md + daily notes) ||
46+
| 2 | 錯誤處理(目錄不存在、無 embedder、空目錄) ||
47+
| 3 | 重複偵測(現狀 + Strategy B 驗證) ||
48+
| 4 | Scope 處理與 metadata.sourceScope ||
49+
| 5 | 批次處理(500 項目、OOM 測試) ||
50+
| 6 | Dry-run 日誌輸出 ||
51+
| 7 | Dry-run 與實際匯入一致性 ||
52+
| 8 | 測試覆蓋(跳過邏輯、importance/category 預設) ||
53+
| 9 | 其他 Markdown bullet 格式(`* ``+ `、數字列表) | ⚠️ 揭示缺口 |
54+
| 10 | UTF-8 BOM 處理 | ⚠️ 揭示缺口 |
55+
| 11 | 部分失敗 + continueOnError ||
56+
| 12 | 真實記憶檔案 + dedup 效益分析 ||
57+
58+
---
59+
60+
## 四、真實檔案效益分析
61+
62+
**測試資料:**
63+
- `~/.openclaw/workspace-dc-channel--1476866394556465252/`
64+
- MEMORY.md:20 筆記錄
65+
- memory/:30 個 daily notes,共 633 筆記錄
66+
- **合計:653 筆記錄**
67+
68+
### Scenario A:無 dedup(現在的行為)
69+
70+
```
71+
第一次匯入:644 筆記錄
72+
第二次匯入:+644 筆記錄(完全重複!)
73+
浪費比例:50%
74+
```
75+
76+
### Scenario B:有 dedup(加功能後的行為)
77+
78+
```
79+
第一次匯入:644 筆記錄
80+
第二次匯入:全部 skip → 節省 644 次 embedder API 呼叫
81+
節省比例:50% embedder API 費用
82+
```
83+
84+
**結論:** 每執行 2 次 import-markdown,可節省 644 次 embedder 呼叫。若每週執行一次,每月節省約 0.13 USD(視 embedder 定價)。
85+
86+
---
87+
88+
## 五、程式碼缺口分析(3 個真的問題)
89+
90+
### 缺口 1:其他 Markdown bullet 格式不支援
91+
92+
**根因:** 只檢查 `line.startsWith("- ")`
93+
94+
**修法:**
95+
```typescript
96+
// 現在(只認 - )
97+
if (!line.startsWith("- ")) continue;
98+
99+
// 改為(支援 - * +)
100+
if (!/^[-*+]\s/.test(line)) continue;
101+
// 數字列表再加:/^\d+\.\s/
102+
```
103+
104+
**嚴重程度:** 低(目前只處理 `- ` 是合理假設,但嚴格來說應支援 Obsidian/標準 Markdown 全格式)
105+
106+
---
107+
108+
### 缺口 2:UTF-8 BOM 破壞第一行解析
109+
110+
**根因:** Windows 編輯器(如記事本)產生的檔案帶 BOM (`\uFEFF`),讀取後未清除
111+
112+
**修法:**
113+
```typescript
114+
const content = await readFile(filePath, "utf-8");
115+
const normalized = content.replace(/^\uFEFF/, ""); // 加這行
116+
const lines = normalized.split(/\r?\n/);
117+
```
118+
119+
**嚴重程度:** 中(Windows 環境常見,會造成第一筆記錄被漏掉或誤判)
120+
121+
---
122+
123+
### 缺口 3:CRLF 行結尾 `\r` 殘留
124+
125+
**根因:** Windows 行結尾是 `\r\n``split("\n")` 後行尾留 `\r`,可能干擾 text 比對
126+
127+
**修法:**
128+
```typescript
129+
// 現在
130+
const lines = content.split("\n");
131+
132+
// 改為
133+
const lines = content.split(/\r?\n/);
134+
// 同時支援 CRLF (\r\n) 和 LF (\n)
135+
```
136+
137+
**嚴重程度:** 低(實際比對時 `\r` 在行尾,不影響內容主體,但精確比對時可能有問題)
138+
139+
---
140+
141+
## 六、建議新增的 Config 欄位(共 5 項)
142+
143+
> 所有預設值 = 現在的 hardcode 值,向下相容,舊用戶不受影響
144+
145+
| 設定 | 型別 | 預設值 | 說明 |
146+
|------|------|--------|------|
147+
| `importMarkdown.dedup` | boolean | `false` | 開啟 scope-aware exact match 去重 |
148+
| `importMarkdown.defaultScope` | string | `"global"` | 沒有 --scope 時的預設 scope |
149+
| `importMarkdown.minTextLength` | number | `5` | 最短文字長度門檻 |
150+
| `importMarkdown.importanceDefault` | number | `0.7` | 匯入記錄的預設 importance |
151+
| `importMarkdown.workspaceFilter` | string[] | `[]`(全部掃)| 只匯入指定的工作區名稱 |
152+
153+
### Config 片段建議
154+
155+
```yaml
156+
importMarkdown:
157+
dedup: false # 預設不開,保持舊行為相容
158+
dedupThreshold: 1.0 # 1.0 = exact match only
159+
defaultScope: "global"
160+
minTextLength: 5
161+
continueOnError: true # 預設為 true(現在已如此)
162+
importanceDefault: 0.7
163+
workspaceFilter: [] # 空 = 掃全部,非空 = 只掃指定名稱
164+
```
165+
166+
---
167+
168+
## 七、推薦實作的 --dedup 邏輯
169+
170+
```typescript
171+
// 在 importMarkdown() 內,store 前加這段
172+
if (options.dedup) {
173+
const existing = await context.store.bm25Search(text, 1, [targetScope]);
174+
if (existing.length > 0 && existing[0].entry.text === text) {
175+
skipped++;
176+
console.log(` [skip] already imported: ${text.slice(0, 60)}...`);
177+
continue; // 跳過,不 call embedder + store
178+
}
179+
}
180+
```
181+
182+
**代價:** 每筆多一次 BM25 查詢(~10-50ms),但節省了 embedder API 費用。
183+
184+
---
185+
186+
## 八、Dry-run 模式
187+
188+
目前已實作,完整對應真實匯入行為:
189+
- imported/skipped 數量與實際匯入完全一致
190+
- 不寫入任何 store 記錄
191+
- 適合用來預覽即將匯入的內容
192+
193+
---
194+
195+
## 九、功能條列式說明
196+
197+
```
198+
import-markdown CLI 功能規格
199+
200+
═══════════════════════════════════════════════
201+
202+
功能:import-markdown
203+
說明:將 Markdown 記憶(MENORY.md、memory/YYYY-MM-DD.md)遷移到 LanceDB
204+
205+
───────────────────────────────────────
206+
CLI 參數
207+
───────────────────────────────────────
208+
209+
--dry-run
210+
型別:flag
211+
說明:預覽模式,不實際寫入
212+
213+
--scope <scope>
214+
型別:string
215+
說明:指定匯入的目標 scope(預設:global)
216+
217+
--openclaw-home <path>
218+
型別:string
219+
說明:指定 OpenClaw home 目錄(預設:~/.openclaw)
220+
221+
<workspace-glob>
222+
型別:string
223+
說明:只掃特定名稱的 workspace(如 "dc-channel")
224+
225+
───────────────────────────────────────
226+
建議新增的 Config 欄位(共 5 項)
227+
───────────────────────────────────────
228+
229+
1. importMarkdown.dedup
230+
型別:boolean
231+
預設:false
232+
說明:匯入前檢查是否已有相同文字的記憶(scope-aware exact match)
233+
false = 不檢查,每次匯入都產生新 entry
234+
true = 先查同 scope 是否有相同文字,有則 skip
235+
236+
2. importMarkdown.defaultScope
237+
型別:string
238+
預設:global
239+
說明:沒有 --scope 參數時,匯入記憶的目標 scope
240+
指令列參數 --scope 的優先序高於此設定
241+
242+
3. importMarkdown.minTextLength
243+
型別:number
244+
預設:5
245+
說明:跳過短於此字數的記憶項目
246+
247+
4. importMarkdown.importanceDefault
248+
型別:number
249+
預設:0.7
250+
說明:匯入記憶的預設 importance 值(0.0 ~ 1.0)
251+
252+
5. importMarkdown.workspaceFilter
253+
型別:string[]
254+
預設:[](掃全部)
255+
說明:只匯入指定名稱的 workspace,空陣列 = 全部掃
256+
257+
═══════════════════════════════════════════════
258+
```
259+
260+
---
261+
262+
## 十、相關連結
263+
264+
- PR #426https://github.com/CortexReach/memory-lancedb-pro/pull/426
265+
- Issue #344https://github.com/CortexReach/memory-lancedb-pro/issues/344
266+
- PR #367https://github.com/CortexReach/memory-lancedb-pro/pull/367(已 merge,文件 + startup warning)
267+
- 測試封包:`C:\Users\admin\Desktop\memory-lancedb-pro-import-markdown-test`

0 commit comments

Comments
 (0)