oh-memos-mcp 3.1.7 → 3.1.8
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 +53 -4
- package/dist/handlers/memory.js +7 -1
- package/dist/handlers/utils.js +11 -2
- package/dist/handlers/wiki-import.js +44 -17
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,55 @@ This project follows [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## [3.1.8] - 2026-08-27
|
|
10
|
+
|
|
11
|
+
仅 MCP server(npm `oh-memos-mcp`)。Python 包与容器镜像无改动,仍为 3.1.5。
|
|
12
|
+
|
|
13
|
+
### 🏷️ `memos_get` 报 root 无权限,而 root 从未被传入
|
|
14
|
+
|
|
15
|
+
`memos_get` 打自己项目的 cube 时回:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
❌ [API_ERROR] Get failed: User 'root' does not have access to cube
|
|
19
|
+
'jincaizhaopin_cube'. Please register the cube first or request access.
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
但 `MEMOS_USER=dev_user`,root 不在任何调用参数里。
|
|
23
|
+
|
|
24
|
+
root 来自后端的默认回退:`start_api.py` 的 `get_memory(mem_cube_id, memory_id,
|
|
25
|
+
user_id=None)` 把 `user_id` 声明为可选,缺失时 `core.py` 的 `get()` 回退到
|
|
26
|
+
`self.user_id`(MOS 实例自己的用户,即 root)。而 `ensureCubeRegistered` 注册的是
|
|
27
|
+
`MEMOS_USER`,于是 root 对该 cube 无授权,`_validate_cube_access` 抛 ValueError,
|
|
28
|
+
被 ValueError handler 映射成 400。
|
|
29
|
+
|
|
30
|
+
**`handleMemosGet` 是唯一漏传 `user_id` 的调用点** —— save / list / search /
|
|
31
|
+
stats / calendar / siblings 全都传了,admin 的 delete 路径也拼了查询串。
|
|
32
|
+
|
|
33
|
+
`apiCallWithRetry` 的 400 分支救不回来:它把 400 当「cube 未加载」,重注册后重试
|
|
34
|
+
—— 但重注册仍按 `MEMOS_USER` 注册,重试请求仍然不带 `user_id`,第二次照样落到 root。
|
|
35
|
+
|
|
36
|
+
实测对照(同一个不存在的 id,`jincaizhaopin_cube`):
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
不带 user_id -> User 'root' does not have access to cube 'jincaizhaopin_cube'
|
|
40
|
+
带 user_id=dev_user -> Memory with ID ... not found ← 正常语义
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
为什么以前没人踩到:cube 的**归属**决定是否触发。root 名下的 cube(`dev_cube`、
|
|
44
|
+
`claude_cube`、`memos_cube` 等)不受影响;只有 owner 是 `MEMOS_USER` 的项目 cube
|
|
45
|
+
才会报错 —— 也就是每一个用 `project_path` 自动派生出来的 cube。
|
|
46
|
+
|
|
47
|
+
同源缺陷也修了 `wiki-import.ts` 的 `getStoredMemory`。那里更隐蔽:该消息既不含
|
|
48
|
+
`not found` 也不是 404,会被当成真实故障,把整页 import 判成失败而不是「记忆不存在」。
|
|
49
|
+
|
|
50
|
+
`apiErrorResponse` 另加了对 `does not have access` 的识别。该消息同时含 `user` 和
|
|
51
|
+
cube 字样,却不匹配任何既有分支,此前只能给通用的「查 health / 看日志」。新提示先
|
|
52
|
+
指向 `user_id`,因为最常见的成因是调用方漏传它,而不是用户或 cube 不存在。
|
|
53
|
+
|
|
54
|
+
回归测试 `get-user-id-param.test.ts` 的 fetch stub **按 `user_id` 是否存在分流**
|
|
55
|
+
应答,复刻上面的实测对照。无条件回 not-found 的 stub 守不住这个缺陷 —— 变异验证
|
|
56
|
+
(把 params 改回 `{}`)时三条断言必须全红,写测试时实际拿它验过。
|
|
57
|
+
|
|
9
58
|
## [3.1.7] - 2026-08-27
|
|
10
59
|
|
|
11
60
|
仅 MCP server(npm `oh-memos-mcp`)。Python 包与容器镜像无改动,仍为 3.1.5。
|
|
@@ -50,7 +99,7 @@ export function canvasName(prefix: string, goal: string): string {
|
|
|
50
99
|
|
|
51
100
|
### 🧪 测试
|
|
52
101
|
|
|
53
|
-
canvas-format 单测 41 → 50,canvas e2e
|
|
102
|
+
canvas-format 单测 41 → 50,canvas e2e 34 → 40 项。
|
|
54
103
|
|
|
55
104
|
核心断言是**不动点**而不是长度:`slugify(canvasName(p, g)) === canvasName(p, g)`,
|
|
56
105
|
跨 3 个前缀 × 6 种 goal(超长、纯 CJK、尾部连字符诱饵、前 60 字符相同的两个变体)。
|
|
@@ -66,7 +115,7 @@ e2e 补的 5 项跨 open/list/show/delete 与磁盘:`open` 报的名字必须
|
|
|
66
115
|
它们守的是「如果有人改用『让 `canvasPath` 对有损名字抛错』来修这个缺陷,
|
|
67
116
|
这条回归会被抓到」,不是这次的不变量。
|
|
68
117
|
|
|
69
|
-
门禁:vitest 520 passed、tsc、canvas e2e
|
|
118
|
+
门禁:vitest 520 passed、tsc、canvas e2e 40 项、pack 契约 104 文件、
|
|
70
119
|
schema budget +0.8%、semantic 快照 17 工具、protocol legacy + v2、
|
|
71
120
|
lite smoke、host-env smoke、spread smoke。
|
|
72
121
|
|
|
@@ -137,7 +186,7 @@ commit message 或记忆正文引用的 `001-N3` 会变得指向两个不同画
|
|
|
137
186
|
|
|
138
187
|
### 🧪 测试
|
|
139
188
|
|
|
140
|
-
canvas 单测 41 → 45(format)、28 → 44(store),canvas e2e
|
|
189
|
+
canvas 单测 41 → 45(format)、28 → 44(store),canvas e2e 18 → 34 项。
|
|
141
190
|
两个修复都做了反向验证,还原实现后对应用例确实变红:转义 3 项、水位线 1 项(含 delete 侧那次抬升)。
|
|
142
191
|
|
|
143
192
|
e2e 补了「旧 cube(无水位线)删除后不复用前缀」这条真实升级路径 ——
|
|
@@ -150,7 +199,7 @@ e2e 补了「旧 cube(无水位线)删除后不复用前缀」这条真实
|
|
|
150
199
|
两处冻结的哈希同步重算:`schema-semantic-baseline.json` 与
|
|
151
200
|
`protocol-contract.test.ts` 的 `EXPECTED_SCHEMA_HASHES`。
|
|
152
201
|
|
|
153
|
-
门禁:vitest 511 passed、tsc、canvas e2e
|
|
202
|
+
门禁:vitest 511 passed、tsc、canvas e2e 34 项、pack 契约 104 文件、
|
|
154
203
|
schema budget +0.8%(限 5%)、semantic 快照 17 工具、protocol v2、lite smoke。
|
|
155
204
|
|
|
156
205
|
---
|
package/dist/handlers/memory.js
CHANGED
|
@@ -286,7 +286,13 @@ export async function handleMemosGet(arguments_) {
|
|
|
286
286
|
const [regSuccess, regError] = await ensureCubeRegistered(cubeId);
|
|
287
287
|
if (!regSuccess)
|
|
288
288
|
return cubeRegistrationError(cubeId, regError);
|
|
289
|
-
const result = await apiCallWithRetry("GET", `${MEMOS_URL}/memories/${cubeId}/${memoryId}`, cubeId,
|
|
289
|
+
const result = await apiCallWithRetry("GET", `${MEMOS_URL}/memories/${cubeId}/${memoryId}`, cubeId,
|
|
290
|
+
// user_id 必须显式传:后端 `get_memory` 的 user_id 是可选参数,缺失时回退到
|
|
291
|
+
// MOS 实例自己的 user_id(root)。而 ensureCubeRegistered 注册的是 MEMOS_USER
|
|
292
|
+
// (dev_user),于是 root 对该 cube 无授权,校验抛 ValueError → 400
|
|
293
|
+
// "User 'root' does not have access to cube '...'"。重注册重试也救不回来:
|
|
294
|
+
// 它仍然按 MEMOS_USER 注册,重试请求仍然不带 user_id,第二次照样落到 root。
|
|
295
|
+
{ params: { user_id: MEMOS_USER } }, ensureCubeRegistered);
|
|
290
296
|
if (result.success && result.data) {
|
|
291
297
|
const resultData = result.data
|
|
292
298
|
.data;
|
package/dist/handlers/utils.js
CHANGED
|
@@ -53,10 +53,19 @@ export function apiErrorResponse(operation, statusOrMsg) {
|
|
|
53
53
|
// Turn known API errors into actionable next steps on the hot paths
|
|
54
54
|
// (save/search/list hit these far more often than register_cube does).
|
|
55
55
|
const suggestions = [];
|
|
56
|
-
|
|
56
|
+
// 「does not have access to cube」要在 user-missing 分支之前判:它同时含 "user"
|
|
57
|
+
// 和 cube 字样,但成因完全不同 —— 用户和 cube 都存在,只是该用户没被授权。
|
|
58
|
+
// 实测最常见的触发方式是调用方漏传 user_id,后端回退到 root 去校验(root 往往
|
|
59
|
+
// 没有项目 cube 的授权),所以提示要先指向 user_id,而不是让人去建用户。
|
|
60
|
+
if (msg.includes("does not have access")) {
|
|
61
|
+
suggestions.push('Cube exists but this user is not authorized — most often the caller omitted `user_id` and the API fell back to its own default user. Verify `MEMOS_USER` matches the cube owner, then re-register: `memos_admin(action="register_cube", cube_id="...")`');
|
|
62
|
+
}
|
|
63
|
+
else if (msg.includes("user") &&
|
|
64
|
+
(msg.includes("not exist") || msg.includes("not found"))) {
|
|
57
65
|
suggestions.push('User missing — run `memos_admin(action="create_user", user_id="dev_user")`, then retry');
|
|
58
66
|
}
|
|
59
|
-
else if (msg.includes("not loaded") ||
|
|
67
|
+
else if (msg.includes("not loaded") ||
|
|
68
|
+
(msg.includes("cube") && msg.includes("not found"))) {
|
|
60
69
|
suggestions.push('Cube not registered — run `memos_admin(action="register_cube", cube_id="...")` (find ids via `memos_admin(action="list_cubes")`), then retry');
|
|
61
70
|
}
|
|
62
71
|
suggestions.push("Check API health: `curl http://localhost:18000/health/detail`", "Check API logs for details");
|
|
@@ -21,8 +21,8 @@ import { MEMOS_URL, MEMOS_USER, logger } from "../config.js";
|
|
|
21
21
|
import { apiCallWithRetry } from "../api-client.js";
|
|
22
22
|
import { ensureCubeRegistered } from "../cube-manager.js";
|
|
23
23
|
import { parseMemoryWriteResponse } from "../memory-write-response.js";
|
|
24
|
-
import { inspectWikiPages, normalizeLedger } from "../wiki-import-safety.js";
|
|
25
|
-
import { buildFileBaseIndex } from "../wiki-relations.js";
|
|
24
|
+
import { inspectWikiPages, normalizeLedger, } from "../wiki-import-safety.js";
|
|
25
|
+
import { buildFileBaseIndex, } from "../wiki-relations.js";
|
|
26
26
|
import { writeRelationsForPage } from "./wiki-import-relations.js";
|
|
27
27
|
import { buildMemoryContent, parseWikiPage, stripTypePrefix, } from "../wiki-import-format.js";
|
|
28
28
|
import { ERR_OPERATION_FAILED, ERR_PARAM_MISSING, cubeRegistrationError, errorResponse, getCubeIdFromArgs, } from "./utils.js";
|
|
@@ -55,7 +55,10 @@ function collectMarkdownFiles(pagesDir) {
|
|
|
55
55
|
return files;
|
|
56
56
|
}
|
|
57
57
|
function contentHash(page) {
|
|
58
|
-
return crypto
|
|
58
|
+
return crypto
|
|
59
|
+
.createHash("sha256")
|
|
60
|
+
.update(`${page.type}\n${page.content}`)
|
|
61
|
+
.digest("hex");
|
|
59
62
|
}
|
|
60
63
|
function loadLedger(wikiDir) {
|
|
61
64
|
const ledgerPath = path.join(wikiDir, LEDGER_FILE);
|
|
@@ -89,7 +92,9 @@ function saveLedger(wikiDir, ledger) {
|
|
|
89
92
|
try {
|
|
90
93
|
fs.unlinkSync(temp);
|
|
91
94
|
}
|
|
92
|
-
catch {
|
|
95
|
+
catch {
|
|
96
|
+
/* best effort */
|
|
97
|
+
}
|
|
93
98
|
return false;
|
|
94
99
|
}
|
|
95
100
|
}
|
|
@@ -109,7 +114,10 @@ function acquireImportLock(wikiDir) {
|
|
|
109
114
|
}
|
|
110
115
|
}
|
|
111
116
|
const fd = fs.openSync(lockPath, "wx");
|
|
112
|
-
fs.writeFileSync(fd, JSON.stringify({
|
|
117
|
+
fs.writeFileSync(fd, JSON.stringify({
|
|
118
|
+
pid: process.pid,
|
|
119
|
+
started_at: new Date(now).toISOString(),
|
|
120
|
+
}));
|
|
113
121
|
fs.closeSync(fd);
|
|
114
122
|
return null;
|
|
115
123
|
}
|
|
@@ -121,13 +129,20 @@ function releaseImportLock(wikiDir) {
|
|
|
121
129
|
try {
|
|
122
130
|
fs.unlinkSync(path.join(wikiDir, LOCK_FILE));
|
|
123
131
|
}
|
|
124
|
-
catch {
|
|
132
|
+
catch {
|
|
133
|
+
/* another process may own it */
|
|
134
|
+
}
|
|
125
135
|
}
|
|
126
136
|
/** Returns the stored node, null when the memory does not exist, or an error string. */
|
|
127
137
|
async function getStoredMemory(cubeId, memoryId) {
|
|
128
|
-
const result = await apiCallWithRetry("GET", `${MEMOS_URL}/memories/${cubeId}/${memoryId}`, cubeId,
|
|
138
|
+
const result = await apiCallWithRetry("GET", `${MEMOS_URL}/memories/${cubeId}/${memoryId}`, cubeId,
|
|
139
|
+
// 同 handlers/memory.ts 的 handleMemosGet:缺 user_id 会让后端回退到 root,
|
|
140
|
+
// 对 MEMOS_USER 名下的 cube 报「does not have access」。这里更隐蔽 —— 该错误
|
|
141
|
+
// 既不含 "not found" 也不是 404,会被当成真实故障返回,把整页 import 判成失败。
|
|
142
|
+
{ params: { user_id: MEMOS_USER } }, ensureCubeRegistered);
|
|
129
143
|
if (result.success) {
|
|
130
|
-
return result.data
|
|
144
|
+
return (result.data
|
|
145
|
+
?.data ?? null);
|
|
131
146
|
}
|
|
132
147
|
const msg = String(result.data?.message ?? "");
|
|
133
148
|
if (msg.toLowerCase().includes("not found") || result.status === 404)
|
|
@@ -152,7 +167,8 @@ async function savePageAsMemory(cubeId, page) {
|
|
|
152
167
|
const result = await apiCallWithRetry("POST", `${MEMOS_URL}/memories`, cubeId, { body }, ensureCubeRegistered);
|
|
153
168
|
if (!result.success) {
|
|
154
169
|
return {
|
|
155
|
-
error: String(result.data?.message ??
|
|
170
|
+
error: String(result.data?.message ??
|
|
171
|
+
`HTTP ${result.status}`),
|
|
156
172
|
memoryIds: [],
|
|
157
173
|
warnings: [],
|
|
158
174
|
uncertain: false,
|
|
@@ -174,12 +190,12 @@ export async function handleMemosImportWiki(arguments_) {
|
|
|
174
190
|
const projectPath = arguments_.project_path;
|
|
175
191
|
const dryRun = arguments_.dry_run === true;
|
|
176
192
|
const onEdit = arguments_.on_edit === "version" ? "version" : "skip";
|
|
177
|
-
const wikiDir = arguments_.wiki_dir
|
|
178
|
-
|
|
193
|
+
const wikiDir = arguments_.wiki_dir ??
|
|
194
|
+
(projectPath ? path.join(projectPath, "docs", "memory-wiki") : undefined);
|
|
179
195
|
if (!wikiDir) {
|
|
180
196
|
return errorResponse("Need project_path (default wiki: <project_path>/docs/memory-wiki) or an explicit wiki_dir", ERR_PARAM_MISSING, [
|
|
181
197
|
'`memos_import_wiki(project_path="/mnt/g/my/project")`',
|
|
182
|
-
|
|
198
|
+
"Run `memos_export_wiki` first, edit pages, then import",
|
|
183
199
|
]);
|
|
184
200
|
}
|
|
185
201
|
const resolvedDir = path.resolve(wikiDir);
|
|
@@ -189,7 +205,9 @@ export async function handleMemosImportWiki(arguments_) {
|
|
|
189
205
|
return errorResponse(lockError, ERR_OPERATION_FAILED);
|
|
190
206
|
try {
|
|
191
207
|
if (!fs.existsSync(pagesDir)) {
|
|
192
|
-
return errorResponse(`No pages directory under '${resolvedDir}' — export a wiki first`, ERR_OPERATION_FAILED, [
|
|
208
|
+
return errorResponse(`No pages directory under '${resolvedDir}' — export a wiki first`, ERR_OPERATION_FAILED, [
|
|
209
|
+
`memos_export_wiki(project_path="...") writes <project_path>/docs/memory-wiki/pages/`,
|
|
210
|
+
]);
|
|
193
211
|
}
|
|
194
212
|
const [regSuccess, regError] = await ensureCubeRegistered(cubeId);
|
|
195
213
|
if (!regSuccess)
|
|
@@ -209,7 +227,10 @@ export async function handleMemosImportWiki(arguments_) {
|
|
|
209
227
|
}
|
|
210
228
|
const result = parseWikiPage(raw);
|
|
211
229
|
if (result.ok)
|
|
212
|
-
parsed.push({
|
|
230
|
+
parsed.push({
|
|
231
|
+
page: result.page,
|
|
232
|
+
relPath: path.relative(resolvedDir, file),
|
|
233
|
+
});
|
|
213
234
|
else if (result.foreign)
|
|
214
235
|
foreign += 1;
|
|
215
236
|
else
|
|
@@ -252,7 +273,9 @@ export async function handleMemosImportWiki(arguments_) {
|
|
|
252
273
|
archived += 1;
|
|
253
274
|
continue;
|
|
254
275
|
}
|
|
255
|
-
if (ledgerEntry &&
|
|
276
|
+
if (ledgerEntry &&
|
|
277
|
+
typeof ledgerEntry !== "string" &&
|
|
278
|
+
ledgerEntry.new_memory_ids.length > 0) {
|
|
256
279
|
// The old page has already produced a new version; compare the edited
|
|
257
280
|
// content hash before doing another API lookup/version.
|
|
258
281
|
const mappedHash = ledgerEntry.content_hash;
|
|
@@ -306,7 +329,9 @@ export async function handleMemosImportWiki(arguments_) {
|
|
|
306
329
|
unchanged += 1;
|
|
307
330
|
continue;
|
|
308
331
|
}
|
|
309
|
-
const ledgerHash = typeof ledgerEntry === "string"
|
|
332
|
+
const ledgerHash = typeof ledgerEntry === "string"
|
|
333
|
+
? ledgerEntry
|
|
334
|
+
: ledgerEntry?.content_hash;
|
|
310
335
|
if (ledgerHash === contentHash(page)) {
|
|
311
336
|
alreadyVersioned += 1;
|
|
312
337
|
continue;
|
|
@@ -370,7 +395,9 @@ export async function handleMemosImportWiki(arguments_) {
|
|
|
370
395
|
lines.push("", `❌ 失败明细(前 ${REPORT_LIST_LIMIT} 条):`, ...failures.slice(0, REPORT_LIST_LIMIT).map((s) => `- ${s}`));
|
|
371
396
|
}
|
|
372
397
|
if (apiWarnings.length > 0) {
|
|
373
|
-
lines.push("", "⚠️ API warnings:", ...apiWarnings
|
|
398
|
+
lines.push("", "⚠️ API warnings:", ...apiWarnings
|
|
399
|
+
.slice(0, REPORT_LIST_LIMIT)
|
|
400
|
+
.map((warning) => `- ${warning}`));
|
|
374
401
|
}
|
|
375
402
|
lines.push("", "ℹ️ 导入恢复 type/tags/confidence/status/时间字段;关联 wikilinks 写入 Neo4j 关系边;写入内容经过服务端凭据脱敏。");
|
|
376
403
|
return [{ type: "text", text: lines.join("\n") }];
|