opencode-haimati 1.0.2 → 1.0.3
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 +44 -16
- package/dist/index.js +132 -129
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,10 +7,12 @@
|
|
|
7
7
|
- 🧠 类海马体记忆机制:模拟人脑记忆系统的分类存储
|
|
8
8
|
- 📁 树形索引结构:通过 `.haimati/索引.md` 维护记忆索引
|
|
9
9
|
- 📚 分组书页存储:记忆内容按序号分组存储于 `.haimati/书页/` 目录
|
|
10
|
-
- 🔍
|
|
11
|
-
- ✏️ 完整 CRUD
|
|
10
|
+
- 🔍 多模式搜索:支持序号精确查询和关键字模糊搜索
|
|
11
|
+
- ✏️ 完整 CRUD:读取、写入、更新(部分替换)、删除、移动记忆
|
|
12
|
+
- 🔄 版本并发控制:基于 version 的乐观锁,防止多会话并发冲突
|
|
12
13
|
- 🔄 自动规则注入:新会话自动注入海马体使用原则
|
|
13
14
|
- 📋 日志追踪:完整的操作日志记录
|
|
15
|
+
- ⚡ 纯异步架构:所有文件操作非阻塞执行
|
|
14
16
|
|
|
15
17
|
## 安装
|
|
16
18
|
|
|
@@ -35,8 +37,10 @@ bun add -g opencode-haimati
|
|
|
35
37
|
|
|
36
38
|
```
|
|
37
39
|
.haimati/
|
|
38
|
-
├──
|
|
39
|
-
|
|
40
|
+
├── .lock # 序号分配锁
|
|
41
|
+
├── 索引.md # 树形结构索引
|
|
42
|
+
├── next-id.txt # 下一个可用序号
|
|
43
|
+
└── 书页/ # 存储各记忆内容(按序号分组)
|
|
40
44
|
├── 001-050/
|
|
41
45
|
│ ├── 001.md
|
|
42
46
|
│ └── 002.md
|
|
@@ -46,13 +50,15 @@ bun add -g opencode-haimati
|
|
|
46
50
|
|
|
47
51
|
## 可用工具
|
|
48
52
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
| 工具 | 说明 |
|
|
54
|
+
|------|------|
|
|
55
|
+
| `haimati_read` | 读取记忆内容(只支持序号查询,支持分页,返回带行号和版本号) |
|
|
56
|
+
| `haimati_write` | 写入新记忆(覆盖已有内容时版本+1) |
|
|
57
|
+
| `haimati_search` | 搜索记忆内容(标题、分类、序号、书页内容) |
|
|
58
|
+
| `haimati_edit` | 修改记忆内容(部分替换,支持版本并发控制) |
|
|
59
|
+
| `haimati_move` | 修改记忆的分类路径 |
|
|
60
|
+
| `haimati_delete` | 删除记忆 |
|
|
61
|
+
| `haimati_list` | 列出记忆索引 |
|
|
56
62
|
|
|
57
63
|
## 海马体三大原则
|
|
58
64
|
|
|
@@ -70,13 +76,35 @@ haimati_write({
|
|
|
70
76
|
content: "WebSocket通信采用base64编码..."
|
|
71
77
|
})
|
|
72
78
|
|
|
73
|
-
//
|
|
79
|
+
// 读取记忆(只支持序号查询)
|
|
74
80
|
haimati_read({ query: "086" })
|
|
75
|
-
// 或
|
|
76
|
-
haimati_read({ query: "标立方/签章/签章服务WS通信机制" })
|
|
77
81
|
|
|
78
82
|
// 搜索记忆
|
|
79
|
-
haimati_search({ keyword: "WebSocket", limit: 10 })
|
|
83
|
+
haimati_search({ keyword: "WebSocket", match: "or", limit: 10 })
|
|
84
|
+
|
|
85
|
+
// 部分修改记忆
|
|
86
|
+
haimati_edit({
|
|
87
|
+
query: "086",
|
|
88
|
+
offsetBegin: 3,
|
|
89
|
+
offsetEnd: 5,
|
|
90
|
+
content: "新的替换内容",
|
|
91
|
+
version: 1
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// 移动记忆到新分类
|
|
95
|
+
haimati_move({
|
|
96
|
+
query: "086",
|
|
97
|
+
newCategory: "标立方/客户端"
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
// 删除记忆
|
|
101
|
+
haimati_delete({ query: "086" })
|
|
102
|
+
|
|
103
|
+
// 列出所有记忆
|
|
104
|
+
haimati_list({})
|
|
105
|
+
|
|
106
|
+
// 列出指定分类下的记忆
|
|
107
|
+
haimati_list({ category: "标立方", recursive: true })
|
|
80
108
|
```
|
|
81
109
|
|
|
82
110
|
## 索引格式示例
|
|
@@ -97,4 +125,4 @@ haimati_search({ keyword: "WebSocket", limit: 10 })
|
|
|
97
125
|
|
|
98
126
|
## 许可证
|
|
99
127
|
|
|
100
|
-
MIT
|
|
128
|
+
MIT
|
package/dist/index.js
CHANGED
|
@@ -3,24 +3,30 @@ import fs from "fs";
|
|
|
3
3
|
import path from "path";
|
|
4
4
|
import os from "os";
|
|
5
5
|
import { tool } from "@opencode-ai/plugin";
|
|
6
|
+
import { promisify } from "util";
|
|
7
|
+
var fsReadFile = promisify(fs.readFile);
|
|
8
|
+
var fsWriteFile = promisify(fs.writeFile);
|
|
9
|
+
var fsUnlink = promisify(fs.unlink);
|
|
10
|
+
var fsMkdir = promisify(fs.mkdir);
|
|
11
|
+
var fsExists = promisify(fs.exists);
|
|
12
|
+
var fsStat = promisify(fs.stat);
|
|
6
13
|
var HAIMATI_DIR = ".haimati";
|
|
7
14
|
var INDEX_FILE = "\u7D22\u5F15.md";
|
|
8
15
|
var PAGES_DIR = "\u4E66\u9875";
|
|
9
16
|
var NEXT_ID_FILE = "next-id.txt";
|
|
10
17
|
var LOG_FILE = "haimati.log";
|
|
11
18
|
var LOCK_MAX_WAIT = 5e3;
|
|
12
|
-
var LOCK_RETRY_INTERVAL = 50;
|
|
13
19
|
var HAIMATI_CONFIG_FILE = "opencode-haimati.conf";
|
|
14
20
|
var HAIMATI_PATH_KEY = "haimati-path";
|
|
15
21
|
var VERSION_SEPARATOR = "\n---\nversion:";
|
|
16
22
|
var DEFAULT_VERSION = 1;
|
|
17
|
-
function getConfigHaimatiPath(directory) {
|
|
23
|
+
async function getConfigHaimatiPath(directory) {
|
|
18
24
|
const configPath = path.join(directory, HAIMATI_CONFIG_FILE);
|
|
19
|
-
if (!
|
|
25
|
+
if (!await fsExists(configPath)) {
|
|
20
26
|
return null;
|
|
21
27
|
}
|
|
22
28
|
try {
|
|
23
|
-
const content =
|
|
29
|
+
const content = await fsReadFile(configPath, "utf-8");
|
|
24
30
|
const lines = content.split("\n");
|
|
25
31
|
for (const line of lines) {
|
|
26
32
|
const trimmed = line.trim();
|
|
@@ -41,35 +47,36 @@ function getConfigHaimatiPath(directory) {
|
|
|
41
47
|
}
|
|
42
48
|
return null;
|
|
43
49
|
}
|
|
44
|
-
function getHaimatiDir(directory) {
|
|
45
|
-
const configPath = getConfigHaimatiPath(directory);
|
|
50
|
+
async function getHaimatiDir(directory) {
|
|
51
|
+
const configPath = await getConfigHaimatiPath(directory);
|
|
46
52
|
if (configPath) {
|
|
47
53
|
return configPath;
|
|
48
54
|
}
|
|
49
55
|
return path.join(directory, HAIMATI_DIR);
|
|
50
56
|
}
|
|
51
|
-
function getIndexPath(directory) {
|
|
52
|
-
return path.join(getHaimatiDir(directory), INDEX_FILE);
|
|
57
|
+
async function getIndexPath(directory) {
|
|
58
|
+
return path.join(await getHaimatiDir(directory), INDEX_FILE);
|
|
53
59
|
}
|
|
54
|
-
function getNextIdPath(directory) {
|
|
55
|
-
return path.join(getHaimatiDir(directory), NEXT_ID_FILE);
|
|
60
|
+
async function getNextIdPath(directory) {
|
|
61
|
+
return path.join(await getHaimatiDir(directory), NEXT_ID_FILE);
|
|
56
62
|
}
|
|
57
|
-
function getLockPath(directory) {
|
|
58
|
-
return path.join(getHaimatiDir(directory), ".lock");
|
|
63
|
+
async function getLockPath(directory) {
|
|
64
|
+
return path.join(await getHaimatiDir(directory), ".lock");
|
|
59
65
|
}
|
|
60
|
-
function getPagesDir(directory) {
|
|
61
|
-
return path.join(getHaimatiDir(directory), PAGES_DIR);
|
|
66
|
+
async function getPagesDir(directory) {
|
|
67
|
+
return path.join(await getHaimatiDir(directory), PAGES_DIR);
|
|
62
68
|
}
|
|
63
|
-
function getPageGroupDir(directory, id) {
|
|
69
|
+
async function getPageGroupDir(directory, id) {
|
|
64
70
|
const n = parseInt(id, 10);
|
|
65
71
|
const groupStart = Math.floor((n - 1) / 50) * 50 + 1;
|
|
66
72
|
const groupEnd = groupStart + 49;
|
|
67
73
|
const groupName = `${String(groupStart).padStart(3, "0")}-${String(groupEnd).padStart(3, "0")}`;
|
|
68
|
-
return path.join(getPagesDir(directory), groupName);
|
|
74
|
+
return path.join(await getPagesDir(directory), groupName);
|
|
69
75
|
}
|
|
70
|
-
function getPagePath(directory, id) {
|
|
71
|
-
return path.join(getPageGroupDir(directory, id), `${id}.md`);
|
|
76
|
+
async function getPagePath(directory, id) {
|
|
77
|
+
return path.join(await getPageGroupDir(directory, id), `${id}.md`);
|
|
72
78
|
}
|
|
79
|
+
var fsAppendFile = promisify(fs.appendFile);
|
|
73
80
|
function getLogFilePath() {
|
|
74
81
|
const osTmpDir = os.tmpdir();
|
|
75
82
|
return path.join(osTmpDir, "haimati_logs", LOG_FILE);
|
|
@@ -81,12 +88,12 @@ async function writeLogToFile(message) {
|
|
|
81
88
|
const timeStr = now.toISOString().replace("T", " ").split(".")[0];
|
|
82
89
|
const logPath = getLogFilePath();
|
|
83
90
|
const logDir = path.dirname(logPath);
|
|
84
|
-
if (!
|
|
85
|
-
|
|
91
|
+
if (!await fsExists(logDir)) {
|
|
92
|
+
await fsMkdir(logDir, { recursive: true });
|
|
86
93
|
}
|
|
87
94
|
let shouldOverwrite = false;
|
|
88
|
-
if (
|
|
89
|
-
const mtime =
|
|
95
|
+
if (await fsExists(logPath)) {
|
|
96
|
+
const mtime = (await fsStat(logPath)).mtime;
|
|
90
97
|
const fileDate = mtime.toISOString().split("T")[0];
|
|
91
98
|
if (fileDate !== dateStr) {
|
|
92
99
|
shouldOverwrite = true;
|
|
@@ -94,9 +101,9 @@ async function writeLogToFile(message) {
|
|
|
94
101
|
}
|
|
95
102
|
const logLine = `[${timeStr}] ${message}`;
|
|
96
103
|
if (shouldOverwrite) {
|
|
97
|
-
|
|
104
|
+
await fsWriteFile(logPath, logLine + "\n", "utf-8");
|
|
98
105
|
} else {
|
|
99
|
-
|
|
106
|
+
await fsAppendFile(logPath, logLine + "\n", "utf-8");
|
|
100
107
|
}
|
|
101
108
|
} catch (error) {
|
|
102
109
|
console.error(`[haimati] \u5199\u5165\u65E5\u5FD7\u6587\u4EF6\u5931\u8D25:`, error);
|
|
@@ -181,7 +188,7 @@ function findEntryById(entries, id) {
|
|
|
181
188
|
function findEntryByPath(entries, categoryPath, title) {
|
|
182
189
|
return entries.find((e) => e.category === categoryPath && e.title === title) || null;
|
|
183
190
|
}
|
|
184
|
-
async function
|
|
191
|
+
async function searchEntries(entries, keyword, matchMode, directory, searchContent = false) {
|
|
185
192
|
const keywords = keyword.toLowerCase().split(/\s+/).filter((k) => k.length > 0);
|
|
186
193
|
if (keywords.length === 0) return [];
|
|
187
194
|
const matchedWithScore = [];
|
|
@@ -197,7 +204,7 @@ async function searchEntriesAsync(entries, keyword, matchMode, directory, search
|
|
|
197
204
|
}
|
|
198
205
|
}
|
|
199
206
|
if (searchContent && directory) {
|
|
200
|
-
const page = await
|
|
207
|
+
const page = await readPage(directory, e.id);
|
|
201
208
|
if (page && page.content) {
|
|
202
209
|
const contentLower = page.content.toLowerCase();
|
|
203
210
|
for (const kw of keywords) {
|
|
@@ -221,12 +228,12 @@ async function searchEntriesAsync(entries, keyword, matchMode, directory, search
|
|
|
221
228
|
matchedWithScore.sort((a, b) => b.score - a.score);
|
|
222
229
|
return matchedWithScore.map((item) => item.entry);
|
|
223
230
|
}
|
|
224
|
-
async function
|
|
225
|
-
const nextIdPath = getNextIdPath(directory);
|
|
226
|
-
if (!
|
|
231
|
+
async function readNextIdFromFile(directory) {
|
|
232
|
+
const nextIdPath = await getNextIdPath(directory);
|
|
233
|
+
if (!await fsExists(nextIdPath)) {
|
|
227
234
|
return 1;
|
|
228
235
|
}
|
|
229
|
-
const content =
|
|
236
|
+
const content = await fsReadFile(nextIdPath, "utf-8");
|
|
230
237
|
const trimmed = content.trim();
|
|
231
238
|
const parsed = parseInt(trimmed, 10);
|
|
232
239
|
if (!isNaN(parsed) && parsed > 0) {
|
|
@@ -234,58 +241,55 @@ async function readNextIdFromFileAsync(directory) {
|
|
|
234
241
|
}
|
|
235
242
|
return 1;
|
|
236
243
|
}
|
|
237
|
-
async function
|
|
238
|
-
const nextIdPath = getNextIdPath(directory);
|
|
239
|
-
if (
|
|
244
|
+
async function initializeNextIdIfNeeded(directory) {
|
|
245
|
+
const nextIdPath = await getNextIdPath(directory);
|
|
246
|
+
if (await fsExists(nextIdPath)) {
|
|
240
247
|
return;
|
|
241
248
|
}
|
|
242
|
-
const entries = await
|
|
249
|
+
const entries = await readIndex(directory);
|
|
243
250
|
let maxId = 0;
|
|
244
251
|
for (const e of entries) {
|
|
245
252
|
const n = parseInt(e.id, 10);
|
|
246
253
|
if (n > maxId) maxId = n;
|
|
247
254
|
}
|
|
248
|
-
await
|
|
255
|
+
await writeNextIdToFile(directory, maxId + 1);
|
|
249
256
|
}
|
|
250
|
-
async function
|
|
251
|
-
const haimatiDir = getHaimatiDir(directory);
|
|
252
|
-
if (!
|
|
253
|
-
|
|
257
|
+
async function writeNextIdToFile(directory, nextId) {
|
|
258
|
+
const haimatiDir = await getHaimatiDir(directory);
|
|
259
|
+
if (!await fsExists(haimatiDir)) {
|
|
260
|
+
await fsMkdir(haimatiDir, { recursive: true });
|
|
254
261
|
}
|
|
255
|
-
const nextIdPath = getNextIdPath(directory);
|
|
256
|
-
|
|
262
|
+
const nextIdPath = await getNextIdPath(directory);
|
|
263
|
+
await fsWriteFile(nextIdPath, String(nextId), "utf-8");
|
|
257
264
|
}
|
|
258
|
-
function acquireLock(directory) {
|
|
259
|
-
const haimatiDir = getHaimatiDir(directory);
|
|
260
|
-
if (!
|
|
261
|
-
|
|
265
|
+
async function acquireLock(directory) {
|
|
266
|
+
const haimatiDir = await getHaimatiDir(directory);
|
|
267
|
+
if (!await fsExists(haimatiDir)) {
|
|
268
|
+
await fsMkdir(haimatiDir, { recursive: true });
|
|
262
269
|
}
|
|
263
|
-
const lockPath = getLockPath(directory);
|
|
270
|
+
const lockPath = await getLockPath(directory);
|
|
264
271
|
const startTime = Date.now();
|
|
265
|
-
while (
|
|
272
|
+
while (await fsExists(lockPath)) {
|
|
266
273
|
if (Date.now() - startTime > LOCK_MAX_WAIT) {
|
|
267
274
|
return false;
|
|
268
275
|
}
|
|
269
|
-
|
|
270
|
-
const end = Date.now() + sleep;
|
|
271
|
-
while (Date.now() < end) {
|
|
272
|
-
}
|
|
276
|
+
await new Promise((resolve) => setTimeout(resolve, 50 + Math.random() * 20));
|
|
273
277
|
}
|
|
274
|
-
|
|
278
|
+
await fsWriteFile(lockPath, String(process.pid), "utf-8");
|
|
275
279
|
return true;
|
|
276
280
|
}
|
|
277
|
-
function releaseLock(directory) {
|
|
278
|
-
const lockPath = getLockPath(directory);
|
|
279
|
-
if (
|
|
280
|
-
|
|
281
|
+
async function releaseLock(directory) {
|
|
282
|
+
const lockPath = await getLockPath(directory);
|
|
283
|
+
if (await fsExists(lockPath)) {
|
|
284
|
+
await fsUnlink(lockPath);
|
|
281
285
|
}
|
|
282
286
|
}
|
|
283
|
-
async function
|
|
284
|
-
const pagePath = getPagePath(directory, id);
|
|
285
|
-
if (!
|
|
287
|
+
async function readPage(directory, id) {
|
|
288
|
+
const pagePath = await getPagePath(directory, id);
|
|
289
|
+
if (!await fsExists(pagePath)) {
|
|
286
290
|
return null;
|
|
287
291
|
}
|
|
288
|
-
const content =
|
|
292
|
+
const content = await fsReadFile(pagePath, "utf-8");
|
|
289
293
|
const lines = content.split("\n");
|
|
290
294
|
if (lines.length < 2) {
|
|
291
295
|
return null;
|
|
@@ -305,25 +309,25 @@ async function readPageAsync(directory, id) {
|
|
|
305
309
|
version
|
|
306
310
|
};
|
|
307
311
|
}
|
|
308
|
-
async function
|
|
309
|
-
const pagesDir = getPagesDir(directory);
|
|
310
|
-
if (!
|
|
311
|
-
|
|
312
|
+
async function writePage(directory, id, title, createdAt, content, version = 1) {
|
|
313
|
+
const pagesDir = await getPagesDir(directory);
|
|
314
|
+
if (!await fsExists(pagesDir)) {
|
|
315
|
+
await fsMkdir(pagesDir, { recursive: true });
|
|
312
316
|
}
|
|
313
|
-
const pageGroupDir = getPageGroupDir(directory, id);
|
|
314
|
-
if (!
|
|
315
|
-
|
|
317
|
+
const pageGroupDir = await getPageGroupDir(directory, id);
|
|
318
|
+
if (!await fsExists(pageGroupDir)) {
|
|
319
|
+
await fsMkdir(pageGroupDir, { recursive: true });
|
|
316
320
|
}
|
|
317
|
-
const pagePath = getPagePath(directory, id);
|
|
321
|
+
const pagePath = await getPagePath(directory, id);
|
|
318
322
|
const fileContent = `${title}
|
|
319
323
|
${createdAt}
|
|
320
324
|
${content}${VERSION_SEPARATOR}${version}`;
|
|
321
|
-
|
|
325
|
+
await fsWriteFile(pagePath, fileContent, "utf-8");
|
|
322
326
|
}
|
|
323
|
-
async function
|
|
324
|
-
const pagePath = getPagePath(directory, id);
|
|
325
|
-
if (
|
|
326
|
-
|
|
327
|
+
async function deletePage(directory, id) {
|
|
328
|
+
const pagePath = await getPagePath(directory, id);
|
|
329
|
+
if (await fsExists(pagePath)) {
|
|
330
|
+
await fsUnlink(pagePath);
|
|
327
331
|
return true;
|
|
328
332
|
}
|
|
329
333
|
return false;
|
|
@@ -386,57 +390,57 @@ function buildIndexTree(entries) {
|
|
|
386
390
|
function removeIndexEntry(entries, id) {
|
|
387
391
|
return entries.filter((e) => e.id !== id);
|
|
388
392
|
}
|
|
389
|
-
async function
|
|
390
|
-
const indexPath = getIndexPath(directory);
|
|
391
|
-
if (!
|
|
393
|
+
async function readIndex(directory) {
|
|
394
|
+
const indexPath = await getIndexPath(directory);
|
|
395
|
+
if (!await fsExists(indexPath)) {
|
|
392
396
|
return [];
|
|
393
397
|
}
|
|
394
|
-
const content =
|
|
398
|
+
const content = await fsReadFile(indexPath, "utf-8");
|
|
395
399
|
return parseIndex(content);
|
|
396
400
|
}
|
|
397
|
-
async function
|
|
398
|
-
const haimatiDir = getHaimatiDir(directory);
|
|
399
|
-
if (!
|
|
400
|
-
|
|
401
|
+
async function writeIndex(directory, entries) {
|
|
402
|
+
const haimatiDir = await getHaimatiDir(directory);
|
|
403
|
+
if (!await fsExists(haimatiDir)) {
|
|
404
|
+
await fsMkdir(haimatiDir, { recursive: true });
|
|
401
405
|
}
|
|
402
406
|
const tree = buildIndexTree(entries);
|
|
403
|
-
const indexPath = getIndexPath(directory);
|
|
404
|
-
|
|
407
|
+
const indexPath = await getIndexPath(directory);
|
|
408
|
+
await fsWriteFile(indexPath, tree, "utf-8");
|
|
405
409
|
}
|
|
406
410
|
async function allocateIdWithLock(directory, category, title, now, content) {
|
|
407
|
-
if (!acquireLock(directory)) {
|
|
411
|
+
if (!await acquireLock(directory)) {
|
|
408
412
|
return null;
|
|
409
413
|
}
|
|
410
414
|
try {
|
|
411
|
-
await
|
|
412
|
-
const entries = await
|
|
415
|
+
await initializeNextIdIfNeeded(directory);
|
|
416
|
+
const entries = await readIndex(directory);
|
|
413
417
|
const existingEntry = entries.find((e) => e.category === category && e.title === title);
|
|
414
418
|
if (existingEntry) {
|
|
415
|
-
const page = await
|
|
419
|
+
const page = await readPage(directory, existingEntry.id);
|
|
416
420
|
const createdAt = page ? page.createdAt : now;
|
|
417
421
|
const newVersion = page ? page.version + 1 : 1;
|
|
418
|
-
await
|
|
422
|
+
await writePage(directory, existingEntry.id, title, createdAt, content, newVersion);
|
|
419
423
|
await log("info", `[allocateIdWithLock] \u66F4\u65B0\u5DF2\u6709\u6761\u76EE: #${existingEntry.id} ${category}/${title}, \u65B0\u7248\u672C: ${newVersion}`);
|
|
420
424
|
return null;
|
|
421
425
|
}
|
|
422
|
-
const currentNext = await
|
|
426
|
+
const currentNext = await readNextIdFromFile(directory);
|
|
423
427
|
const newId = String(currentNext).padStart(3, "0");
|
|
424
|
-
await
|
|
428
|
+
await writePage(directory, newId, title, now, content, 1);
|
|
425
429
|
const newEntry = {
|
|
426
430
|
id: newId,
|
|
427
431
|
category,
|
|
428
432
|
title
|
|
429
433
|
};
|
|
430
434
|
entries.push(newEntry);
|
|
431
|
-
await
|
|
432
|
-
await
|
|
435
|
+
await writeIndex(directory, entries);
|
|
436
|
+
await writeNextIdToFile(directory, currentNext + 1);
|
|
433
437
|
return newId;
|
|
434
438
|
} catch (error) {
|
|
435
439
|
const errMsg = error instanceof Error ? error.message : String(error);
|
|
436
440
|
await log("error", `[allocateIdWithLock] \u5206\u914D\u5E8F\u53F7\u5931\u8D25: ${errMsg}`);
|
|
437
441
|
return null;
|
|
438
442
|
} finally {
|
|
439
|
-
releaseLock(directory);
|
|
443
|
+
await releaseLock(directory);
|
|
440
444
|
}
|
|
441
445
|
}
|
|
442
446
|
var HaimatiPlugin = async (ctx) => {
|
|
@@ -466,16 +470,16 @@ var HaimatiPlugin = async (ctx) => {
|
|
|
466
470
|
*/
|
|
467
471
|
async execute(args, context) {
|
|
468
472
|
const { directory } = context;
|
|
469
|
-
const indexPath = getIndexPath(directory);
|
|
473
|
+
const indexPath = await getIndexPath(directory);
|
|
470
474
|
await log("info", `[haimati_read] \u5165\u53C2: query="${args.query}", offset=${args.offset || 1}, limit=${args.limit || 2e3}`);
|
|
471
|
-
if (!
|
|
475
|
+
if (!await fsExists(indexPath)) {
|
|
472
476
|
const result = `\u9519\u8BEF\uFF1A\u6D77\u9A6C\u4F53\u7D22\u5F15\u4E0D\u5B58\u5728\u4E8E ${indexPath}`;
|
|
473
477
|
await log("warn", `[haimati_read] \u51FA\u53C2:
|
|
474
478
|
${truncateForLog(result)}`);
|
|
475
479
|
return result;
|
|
476
480
|
}
|
|
477
481
|
try {
|
|
478
|
-
const entries = await
|
|
482
|
+
const entries = await readIndex(directory);
|
|
479
483
|
await log("debug", `[haimati_read] \u8BFB\u53D6\u7D22\u5F15: \u5171 ${entries.length} \u6761\u8BB0\u5F55`);
|
|
480
484
|
const query = args.query.trim();
|
|
481
485
|
if (!/^\d+$/.test(query)) {
|
|
@@ -492,14 +496,15 @@ ${truncateForLog(result2)}`);
|
|
|
492
496
|
${truncateForLog(result2)}`);
|
|
493
497
|
return result2;
|
|
494
498
|
}
|
|
495
|
-
const page = await
|
|
499
|
+
const page = await readPage(directory, entry.id);
|
|
496
500
|
if (!page) {
|
|
497
501
|
const result2 = `\u672A\u627E\u5230\u4E66\u9875: #${entry.id}`;
|
|
498
502
|
await log("error", `[haimati_read] \u51FA\u53C2:
|
|
499
503
|
${truncateForLog(result2)}`);
|
|
500
504
|
return result2;
|
|
501
505
|
}
|
|
502
|
-
|
|
506
|
+
const pageGroupDir = await getPageGroupDir(directory, entry.id);
|
|
507
|
+
await log("debug", `[haimati_read] \u8BFB\u53D6\u4E66\u9875: \u4E66\u9875/${pageGroupDir.split(/[\\/]/).pop()}/${entry.id}.md, version=${page.version}`);
|
|
503
508
|
const contentLines = page.content.split("\n");
|
|
504
509
|
const offset = args.offset || 1;
|
|
505
510
|
const limit = args.limit || 2e3;
|
|
@@ -550,8 +555,9 @@ ${truncateForLog(args.content)}
|
|
|
550
555
|
await log("warn", `[haimati_write] \u9501\u83B7\u53D6\u5931\u8D25\uFF0C\u91CD\u8BD5\u7B2C ${retry + 1} \u6B21`);
|
|
551
556
|
}
|
|
552
557
|
if (newId) {
|
|
558
|
+
const pageGroupDir = await getPageGroupDir(directory, newId);
|
|
553
559
|
await log("debug", `[haimati_write] \u5206\u914D\u65B0\u5E8F\u53F7: #${newId}`);
|
|
554
|
-
await log("debug", `[haimati_write] \u4E66\u9875\u548C\u7D22\u5F15\u5DF2\u66F4\u65B0: \u4E66\u9875/${
|
|
560
|
+
await log("debug", `[haimati_write] \u4E66\u9875\u548C\u7D22\u5F15\u5DF2\u66F4\u65B0: \u4E66\u9875/${pageGroupDir.split(/[\\/]/).pop()}/${newId}.md`);
|
|
555
561
|
const result2 = `\u5DF2\u5199\u5165\u6D77\u9A6C\u4F53 #${newId}
|
|
556
562
|
\u8DEF\u5F84: ${args.category}/${args.title}
|
|
557
563
|
\u7248\u672C: 1`;
|
|
@@ -559,10 +565,10 @@ ${truncateForLog(args.content)}
|
|
|
559
565
|
${truncateForLog(result2)}`);
|
|
560
566
|
return result2;
|
|
561
567
|
}
|
|
562
|
-
const entries = await
|
|
568
|
+
const entries = await readIndex(directory);
|
|
563
569
|
const existingEntry = findEntryByPath(entries, args.category, args.title);
|
|
564
570
|
if (existingEntry) {
|
|
565
|
-
const page = await
|
|
571
|
+
const page = await readPage(directory, existingEntry.id);
|
|
566
572
|
const version = page ? page.version : 1;
|
|
567
573
|
const result2 = `\u5DF2\u66F4\u65B0\u6D77\u9A6C\u4F53 #${existingEntry.id}
|
|
568
574
|
\u8DEF\u5F84: ${args.category}/${args.title}
|
|
@@ -599,21 +605,21 @@ ${truncateForLog(result)}`);
|
|
|
599
605
|
},
|
|
600
606
|
async execute(args, context) {
|
|
601
607
|
const { directory } = context;
|
|
602
|
-
const indexPath = getIndexPath(directory);
|
|
608
|
+
const indexPath = await getIndexPath(directory);
|
|
603
609
|
await log("info", `[haimati_search] \u5165\u53C2: keyword="${args.keyword}", match="${args.match}", limit=${args.limit || 10}, offset=${args.offset || 0}`);
|
|
604
|
-
if (!
|
|
610
|
+
if (!await fsExists(indexPath)) {
|
|
605
611
|
const result = `\u9519\u8BEF\uFF1A\u6D77\u9A6C\u4F53\u7D22\u5F15\u4E0D\u5B58\u5728`;
|
|
606
612
|
await log("warn", `[haimati_search] \u51FA\u53C2:
|
|
607
613
|
${truncateForLog(result)}`);
|
|
608
614
|
return result;
|
|
609
615
|
}
|
|
610
616
|
try {
|
|
611
|
-
const entries = await
|
|
617
|
+
const entries = await readIndex(directory);
|
|
612
618
|
const keyword = args.keyword.trim();
|
|
613
619
|
const match = args.match;
|
|
614
620
|
const limit = args.limit || 10;
|
|
615
621
|
const offset = args.offset || 0;
|
|
616
|
-
const allMatched = await
|
|
622
|
+
const allMatched = await searchEntries(entries, keyword, match, directory, true);
|
|
617
623
|
const totalCount = allMatched.length;
|
|
618
624
|
const matched = allMatched.slice(offset, offset + limit);
|
|
619
625
|
if (matched.length === 0) {
|
|
@@ -632,7 +638,7 @@ ${truncateForLog(result2)}`);
|
|
|
632
638
|
`);
|
|
633
639
|
}
|
|
634
640
|
for (const entry of matched) {
|
|
635
|
-
const page = await
|
|
641
|
+
const page = await readPage(directory, entry.id);
|
|
636
642
|
const fullPath = `${entry.category}/${entry.title}`;
|
|
637
643
|
let excerpt = "";
|
|
638
644
|
if (page) {
|
|
@@ -683,18 +689,18 @@ ${truncateForLog(result)}`);
|
|
|
683
689
|
},
|
|
684
690
|
async execute(args, context) {
|
|
685
691
|
const { directory } = context;
|
|
686
|
-
const indexPath = getIndexPath(directory);
|
|
692
|
+
const indexPath = await getIndexPath(directory);
|
|
687
693
|
await log("info", `[haimati_edit] \u5165\u53C2: query="${args.query}", offsetBegin=${args.offsetBegin}, offsetEnd=${args.offsetEnd}, content\u957F\u5EA6=${args.content.length}\u5B57\u7B26, version=${args.version}, content=
|
|
688
694
|
${truncateForLog(args.content)}
|
|
689
695
|
`);
|
|
690
|
-
if (!
|
|
696
|
+
if (!await fsExists(indexPath)) {
|
|
691
697
|
const result = `\u9519\u8BEF\uFF1A\u6D77\u9A6C\u4F53\u7D22\u5F15\u4E0D\u5B58\u5728`;
|
|
692
698
|
await log("warn", `[haimati_edit] \u51FA\u53C2:
|
|
693
699
|
${truncateForLog(result)}`);
|
|
694
700
|
return result;
|
|
695
701
|
}
|
|
696
702
|
try {
|
|
697
|
-
const entries = await
|
|
703
|
+
const entries = await readIndex(directory);
|
|
698
704
|
const query = args.query.trim();
|
|
699
705
|
if (!/^\d+$/.test(query)) {
|
|
700
706
|
const result2 = `\u9519\u8BEF\uFF1Aquery \u5FC5\u987B\u662F\u5E8F\u53F7\uFF0C\u53EA\u652F\u6301\u5E8F\u53F7\u67E5\u8BE2`;
|
|
@@ -709,7 +715,7 @@ ${truncateForLog(result2)}`);
|
|
|
709
715
|
${truncateForLog(result2)}`);
|
|
710
716
|
return result2;
|
|
711
717
|
}
|
|
712
|
-
const page = await
|
|
718
|
+
const page = await readPage(directory, entry.id);
|
|
713
719
|
if (!page) {
|
|
714
720
|
const result2 = `\u672A\u627E\u5230\u4E66\u9875: #${entry.id}`;
|
|
715
721
|
await log("error", `[haimati_edit] \u51FA\u53C2:
|
|
@@ -733,15 +739,12 @@ ${truncateForLog(result2)}`);
|
|
|
733
739
|
const contentLines = page.content.split("\n");
|
|
734
740
|
const newContentLines = [
|
|
735
741
|
...contentLines.slice(0, offsetBegin - 1),
|
|
736
|
-
// [0, offsetBegin-1) 保留
|
|
737
742
|
...args.content.split("\n"),
|
|
738
|
-
// 插入新内容
|
|
739
743
|
...contentLines.slice(offsetEnd - 1)
|
|
740
|
-
// [offsetEnd-1, end) 保留
|
|
741
744
|
];
|
|
742
745
|
const newContent = newContentLines.join("\n");
|
|
743
746
|
const newVersion = page.version + 1;
|
|
744
|
-
await
|
|
747
|
+
await writePage(directory, entry.id, entry.title, page.createdAt, newContent, newVersion);
|
|
745
748
|
const fullPath = `${entry.category}/${entry.title}`;
|
|
746
749
|
const result = `\u5DF2\u66F4\u65B0\u6D77\u9A6C\u4F53 #${entry.id}
|
|
747
750
|
\u8DEF\u5F84: ${fullPath}
|
|
@@ -768,16 +771,16 @@ ${truncateForLog(result)}`);
|
|
|
768
771
|
},
|
|
769
772
|
async execute(args, context) {
|
|
770
773
|
const { directory } = context;
|
|
771
|
-
const indexPath = getIndexPath(directory);
|
|
774
|
+
const indexPath = await getIndexPath(directory);
|
|
772
775
|
await log("info", `[haimati_delete] \u5165\u53C2: query="${args.query}"`);
|
|
773
|
-
if (!
|
|
776
|
+
if (!await fsExists(indexPath)) {
|
|
774
777
|
const result = `\u9519\u8BEF\uFF1A\u6D77\u9A6C\u4F53\u7D22\u5F15\u4E0D\u5B58\u5728`;
|
|
775
778
|
await log("warn", `[haimati_delete] \u51FA\u53C2:
|
|
776
779
|
${truncateForLog(result)}`);
|
|
777
780
|
return result;
|
|
778
781
|
}
|
|
779
782
|
try {
|
|
780
|
-
const entries = await
|
|
783
|
+
const entries = await readIndex(directory);
|
|
781
784
|
const query = args.query.trim();
|
|
782
785
|
let entry = null;
|
|
783
786
|
if (/^\d+$/.test(query)) {
|
|
@@ -789,7 +792,7 @@ ${truncateForLog(result)}`);
|
|
|
789
792
|
const category = parts.join("/");
|
|
790
793
|
entry = findEntryByPath(entries, category, title);
|
|
791
794
|
} else {
|
|
792
|
-
const results = await
|
|
795
|
+
const results = await searchEntries(entries, query, "or", directory);
|
|
793
796
|
if (results.length === 1) {
|
|
794
797
|
entry = results[0];
|
|
795
798
|
} else if (results.length > 1) {
|
|
@@ -805,9 +808,9 @@ ${list}`;
|
|
|
805
808
|
${truncateForLog(result2)}`);
|
|
806
809
|
return result2;
|
|
807
810
|
}
|
|
808
|
-
await
|
|
811
|
+
await deletePage(directory, entry.id);
|
|
809
812
|
const newEntries = removeIndexEntry(entries, entry.id);
|
|
810
|
-
await
|
|
813
|
+
await writeIndex(directory, newEntries);
|
|
811
814
|
const fullPath = `${entry.category}/${entry.title}`;
|
|
812
815
|
const result = `\u5DF2\u5220\u9664\u6D77\u9A6C\u4F53 #${entry.id}
|
|
813
816
|
\u8DEF\u5F84: ${fullPath}`;
|
|
@@ -834,16 +837,16 @@ ${truncateForLog(result)}`);
|
|
|
834
837
|
},
|
|
835
838
|
async execute(args, context) {
|
|
836
839
|
const { directory } = context;
|
|
837
|
-
const indexPath = getIndexPath(directory);
|
|
840
|
+
const indexPath = await getIndexPath(directory);
|
|
838
841
|
await log("info", `[haimati_move] \u5165\u53C2: query="${args.query}", newCategory="${args.newCategory}"`);
|
|
839
|
-
if (!
|
|
842
|
+
if (!await fsExists(indexPath)) {
|
|
840
843
|
const result = `\u9519\u8BEF\uFF1A\u6D77\u9A6C\u4F53\u7D22\u5F15\u4E0D\u5B58\u5728`;
|
|
841
844
|
await log("warn", `[haimati_move] \u51FA\u53C2:
|
|
842
845
|
${truncateForLog(result)}`);
|
|
843
846
|
return result;
|
|
844
847
|
}
|
|
845
848
|
try {
|
|
846
|
-
const entries = await
|
|
849
|
+
const entries = await readIndex(directory);
|
|
847
850
|
const query = args.query.trim();
|
|
848
851
|
let entry = null;
|
|
849
852
|
if (/^\d+$/.test(query)) {
|
|
@@ -855,7 +858,7 @@ ${truncateForLog(result)}`);
|
|
|
855
858
|
const category = parts.join("/");
|
|
856
859
|
entry = findEntryByPath(entries, category, title);
|
|
857
860
|
} else {
|
|
858
|
-
const results = await
|
|
861
|
+
const results = await searchEntries(entries, query, "or", directory);
|
|
859
862
|
if (results.length === 1) {
|
|
860
863
|
entry = results[0];
|
|
861
864
|
} else if (results.length > 1) {
|
|
@@ -881,7 +884,7 @@ ${truncateForLog(result2)}`);
|
|
|
881
884
|
}
|
|
882
885
|
return e;
|
|
883
886
|
});
|
|
884
|
-
await
|
|
887
|
+
await writeIndex(directory, newEntries);
|
|
885
888
|
const result = `\u5DF2\u79FB\u52A8\u6D77\u9A6C\u4F53 #${entry.id}
|
|
886
889
|
\u65E7\u8DEF\u5F84: ${oldPath}
|
|
887
890
|
\u65B0\u8DEF\u5F84: ${newPath}`;
|
|
@@ -908,18 +911,18 @@ ${truncateForLog(result)}`);
|
|
|
908
911
|
},
|
|
909
912
|
async execute(args, context) {
|
|
910
913
|
const { directory } = context;
|
|
911
|
-
const indexPath = getIndexPath(directory);
|
|
914
|
+
const indexPath = await getIndexPath(directory);
|
|
912
915
|
await log("info", `[haimati_list] \u5165\u53C2: category="${args.category || "(\u65E0)"}, recursive=${args.recursive}"`);
|
|
913
|
-
if (!
|
|
916
|
+
if (!await fsExists(indexPath)) {
|
|
914
917
|
const result = `\u9519\u8BEF\uFF1A\u6D77\u9A6C\u4F53\u7D22\u5F15\u4E0D\u5B58\u5728`;
|
|
915
918
|
await log("warn", `[haimati_list] \u51FA\u53C2:
|
|
916
919
|
${truncateForLog(result)}`);
|
|
917
920
|
return result;
|
|
918
921
|
}
|
|
919
922
|
try {
|
|
920
|
-
const entries = await
|
|
923
|
+
const entries = await readIndex(directory);
|
|
921
924
|
if (args.category) {
|
|
922
|
-
const indexContent =
|
|
925
|
+
const indexContent = await fsReadFile(await getIndexPath(directory), "utf-8");
|
|
923
926
|
const lines = indexContent.split("\n");
|
|
924
927
|
let targetDepth = 0;
|
|
925
928
|
let currentPath = [];
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-haimati",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "OpenCode plugin for a file-based memory system inspired by the hippocampus, providing long-term memory storage and retrieval across sessions.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"type": "module",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-haimati",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "OpenCode plugin for a file-based memory system inspired by the hippocampus, providing long-term memory storage and retrieval across sessions.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"type": "module",
|