oh-memos-mcp 3.1.7 → 3.1.9
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 +69 -5
- package/README.md +31 -4
- package/dist/api-client.d.ts +13 -0
- package/dist/api-client.js +295 -12
- package/dist/cube-manager.js +6 -6
- package/dist/handlers/admin.js +8 -8
- package/dist/handlers/calendar.js +4 -4
- package/dist/handlers/graph.js +6 -6
- package/dist/handlers/index.d.ts +1 -0
- package/dist/handlers/index.js +10 -3
- package/dist/handlers/memory.js +24 -10
- package/dist/handlers/search.js +6 -6
- package/dist/handlers/think.js +3 -3
- package/dist/handlers/utils.js +14 -4
- package/dist/handlers/wiki-export.js +3 -3
- package/dist/handlers/wiki-import-relations.js +3 -3
- package/dist/handlers/wiki-import.js +53 -20
- package/dist/memory-write-response.d.ts +8 -1
- package/dist/memory-write-response.js +14 -1
- package/dist/models.js +3 -0
- package/dist/server.js +11 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,71 @@ All notable changes to `oh-memos-mcp` are documented here.
|
|
|
4
4
|
|
|
5
5
|
This project follows [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
-
---
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## [3.1.9] - 2026-09-16
|
|
10
|
+
|
|
11
|
+
配套 API / Docker 镜像版本为 3.1.9。保存先确认原文与向量已持久化,再返回记忆 ID;
|
|
12
|
+
LLM 解析由 API 在后台自动完成,即使模型调用耗时数分钟,也不阻塞 MCP 保存回执。
|
|
13
|
+
|
|
14
|
+
- 识别 `vector_saved` 与 `enrichment_status`,明确显示向量已保存和后台解析状态,兼容旧 API。
|
|
15
|
+
- 未确认向量保存或缺少实际 ID 时返回 MCP `isError: true`,且不写入去重缓存。
|
|
16
|
+
- 请求和响应正文超时归类为 `API_TIMEOUT`,不再误报 API 离线;结果未知的 POST 不自动重放。
|
|
17
|
+
- 显式传递 `memory_type`;Wiki 导入同样拒绝未确认的向量写入。
|
|
18
|
+
- 完整详情使用原始创建时间,避免将后台解析的更新时间误标为创建时间。
|
|
19
|
+
|
|
20
|
+
验证:582 项 Node 测试、旧版及 v2 MCP 协议检查通过。配套 API 的 118 项 Python 回归通过。
|
|
21
|
+
真实 MCP 首次保存 596ms 返回,LLM 约 225 秒后补齐同一 ID 的元数据;重复保存 362ms,
|
|
22
|
+
原文、ID 和已有解析结果均保留。
|
|
23
|
+
|
|
24
|
+
## [3.1.8] - 2026-08-27
|
|
25
|
+
|
|
26
|
+
仅 MCP server(npm `oh-memos-mcp`)。Python 包与容器镜像无改动,仍为 3.1.5。
|
|
27
|
+
|
|
28
|
+
### 🏷️ `memos_get` 报 root 无权限,而 root 从未被传入
|
|
29
|
+
|
|
30
|
+
`memos_get` 打自己项目的 cube 时回:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
❌ [API_ERROR] Get failed: User 'root' does not have access to cube
|
|
34
|
+
'jincaizhaopin_cube'. Please register the cube first or request access.
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
但 `MEMOS_USER=dev_user`,root 不在任何调用参数里。
|
|
38
|
+
|
|
39
|
+
root 来自后端的默认回退:`start_api.py` 的 `get_memory(mem_cube_id, memory_id,
|
|
40
|
+
user_id=None)` 把 `user_id` 声明为可选,缺失时 `core.py` 的 `get()` 回退到
|
|
41
|
+
`self.user_id`(MOS 实例自己的用户,即 root)。而 `ensureCubeRegistered` 注册的是
|
|
42
|
+
`MEMOS_USER`,于是 root 对该 cube 无授权,`_validate_cube_access` 抛 ValueError,
|
|
43
|
+
被 ValueError handler 映射成 400。
|
|
44
|
+
|
|
45
|
+
**`handleMemosGet` 是唯一漏传 `user_id` 的调用点** —— save / list / search /
|
|
46
|
+
stats / calendar / siblings 全都传了,admin 的 delete 路径也拼了查询串。
|
|
47
|
+
|
|
48
|
+
`apiCallWithRetry` 的 400 分支救不回来:它把 400 当「cube 未加载」,重注册后重试
|
|
49
|
+
—— 但重注册仍按 `MEMOS_USER` 注册,重试请求仍然不带 `user_id`,第二次照样落到 root。
|
|
50
|
+
|
|
51
|
+
实测对照(同一个不存在的 id,`jincaizhaopin_cube`):
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
不带 user_id -> User 'root' does not have access to cube 'jincaizhaopin_cube'
|
|
55
|
+
带 user_id=dev_user -> Memory with ID ... not found ← 正常语义
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
为什么以前没人踩到:cube 的**归属**决定是否触发。root 名下的 cube(`dev_cube`、
|
|
59
|
+
`claude_cube`、`memos_cube` 等)不受影响;只有 owner 是 `MEMOS_USER` 的项目 cube
|
|
60
|
+
才会报错 —— 也就是每一个用 `project_path` 自动派生出来的 cube。
|
|
61
|
+
|
|
62
|
+
同源缺陷也修了 `wiki-import.ts` 的 `getStoredMemory`。那里更隐蔽:该消息既不含
|
|
63
|
+
`not found` 也不是 404,会被当成真实故障,把整页 import 判成失败而不是「记忆不存在」。
|
|
64
|
+
|
|
65
|
+
`apiErrorResponse` 另加了对 `does not have access` 的识别。该消息同时含 `user` 和
|
|
66
|
+
cube 字样,却不匹配任何既有分支,此前只能给通用的「查 health / 看日志」。新提示先
|
|
67
|
+
指向 `user_id`,因为最常见的成因是调用方漏传它,而不是用户或 cube 不存在。
|
|
68
|
+
|
|
69
|
+
回归测试 `get-user-id-param.test.ts` 的 fetch stub **按 `user_id` 是否存在分流**
|
|
70
|
+
应答,复刻上面的实测对照。无条件回 not-found 的 stub 守不住这个缺陷 —— 变异验证
|
|
71
|
+
(把 params 改回 `{}`)时三条断言必须全红,写测试时实际拿它验过。
|
|
8
72
|
|
|
9
73
|
## [3.1.7] - 2026-08-27
|
|
10
74
|
|
|
@@ -50,7 +114,7 @@ export function canvasName(prefix: string, goal: string): string {
|
|
|
50
114
|
|
|
51
115
|
### 🧪 测试
|
|
52
116
|
|
|
53
|
-
canvas-format 单测 41 → 50,canvas e2e
|
|
117
|
+
canvas-format 单测 41 → 50,canvas e2e 34 → 40 项。
|
|
54
118
|
|
|
55
119
|
核心断言是**不动点**而不是长度:`slugify(canvasName(p, g)) === canvasName(p, g)`,
|
|
56
120
|
跨 3 个前缀 × 6 种 goal(超长、纯 CJK、尾部连字符诱饵、前 60 字符相同的两个变体)。
|
|
@@ -66,7 +130,7 @@ e2e 补的 5 项跨 open/list/show/delete 与磁盘:`open` 报的名字必须
|
|
|
66
130
|
它们守的是「如果有人改用『让 `canvasPath` 对有损名字抛错』来修这个缺陷,
|
|
67
131
|
这条回归会被抓到」,不是这次的不变量。
|
|
68
132
|
|
|
69
|
-
门禁:vitest 520 passed、tsc、canvas e2e
|
|
133
|
+
门禁:vitest 520 passed、tsc、canvas e2e 40 项、pack 契约 104 文件、
|
|
70
134
|
schema budget +0.8%、semantic 快照 17 工具、protocol legacy + v2、
|
|
71
135
|
lite smoke、host-env smoke、spread smoke。
|
|
72
136
|
|
|
@@ -137,7 +201,7 @@ commit message 或记忆正文引用的 `001-N3` 会变得指向两个不同画
|
|
|
137
201
|
|
|
138
202
|
### 🧪 测试
|
|
139
203
|
|
|
140
|
-
canvas 单测 41 → 45(format)、28 → 44(store),canvas e2e
|
|
204
|
+
canvas 单测 41 → 45(format)、28 → 44(store),canvas e2e 18 → 34 项。
|
|
141
205
|
两个修复都做了反向验证,还原实现后对应用例确实变红:转义 3 项、水位线 1 项(含 delete 侧那次抬升)。
|
|
142
206
|
|
|
143
207
|
e2e 补了「旧 cube(无水位线)删除后不复用前缀」这条真实升级路径 ——
|
|
@@ -150,7 +214,7 @@ e2e 补了「旧 cube(无水位线)删除后不复用前缀」这条真实
|
|
|
150
214
|
两处冻结的哈希同步重算:`schema-semantic-baseline.json` 与
|
|
151
215
|
`protocol-contract.test.ts` 的 `EXPECTED_SCHEMA_HASHES`。
|
|
152
216
|
|
|
153
|
-
门禁:vitest 511 passed、tsc、canvas e2e
|
|
217
|
+
门禁:vitest 511 passed、tsc、canvas e2e 34 项、pack 契约 104 文件、
|
|
154
218
|
schema budget +0.8%(限 5%)、semantic 快照 17 工具、protocol v2、lite smoke。
|
|
155
219
|
|
|
156
220
|
---
|
package/README.md
CHANGED
|
@@ -240,9 +240,36 @@ be deleted without a prompt; add it only if you have decided you want that.
|
|
|
240
240
|
| `MEMOS_PROVIDER` | No | `local` when `MEMOS_MODE=lite`, else `api` | Storage backend. Set explicitly only to override the mode-derived default |
|
|
241
241
|
| `MEMOS_LOG_LEVEL` | No | `info` | `debug` / `info` / `warning` / `error`. Logs go to stderr so they never corrupt the stdio JSON-RPC stream |
|
|
242
242
|
|
|
243
|
-
`MEMOS_URL` is required for Full mode and unused in Lite.
|
|
244
|
-
|
|
245
|
-
###
|
|
243
|
+
`MEMOS_URL` is required for Full mode and unused in Lite.
|
|
244
|
+
|
|
245
|
+
### Save acknowledgement and background parsing
|
|
246
|
+
|
|
247
|
+
In Full mode with the `tree_text` backend, `memos_save` returns after the
|
|
248
|
+
original text and its vector have been confirmed in storage. LLM extraction
|
|
249
|
+
continues in the background, even when a model call takes several minutes.
|
|
250
|
+
The response contains the stored ID; the API adds `vector_saved: true`,
|
|
251
|
+
`enrichment_status: "pending"`, and `queued: false`. Here `queued` describes
|
|
252
|
+
the memory write, which has already completed.
|
|
253
|
+
|
|
254
|
+
Parsing adds the key, tags, background, and extracted facts to that same record.
|
|
255
|
+
It preserves the original text, vector, ID, creation time, and explicit source
|
|
256
|
+
metadata. Graph conflict handling links these originals instead of merging or
|
|
257
|
+
deleting them. Identical retries target the same ID.
|
|
258
|
+
|
|
259
|
+
Two background consumers process persisted tasks. A failed parse leaves the
|
|
260
|
+
saved memory available and retries after 30 and 60 seconds; three completed
|
|
261
|
+
failed attempts stop automatic retries. Interrupted attempts can resume after
|
|
262
|
+
restart, including local projects restored from the cube registry. Parse status
|
|
263
|
+
and the exception type are stored as `enrichment_*` metadata.
|
|
264
|
+
|
|
265
|
+
This API path does not depend on `MOS_TYPED_SAVE_FAST` or the memory scheduler.
|
|
266
|
+
Existing library, chat/document ingestion, and Lite behavior remain available.
|
|
267
|
+
Upgrade/restart the API and reconnect MCP clients after rebuilding the Node
|
|
268
|
+
server. Older APIs without the additive fields remain compatible. An explicit
|
|
269
|
+
unconfirmed vector result is rejected; response timeouts report `API_TIMEOUT`
|
|
270
|
+
and do not automatically replay writes.
|
|
271
|
+
|
|
272
|
+
### Retrieval behaviour (3.1.x)
|
|
246
273
|
|
|
247
274
|
These change what search returns. All are off or conservative by default, so
|
|
248
275
|
upgrading never silently changes ranking.
|
|
@@ -250,7 +277,7 @@ upgrading never silently changes ranking.
|
|
|
250
277
|
| Variable | Required | Default | Description |
|
|
251
278
|
|----------|----------|---------|-------------|
|
|
252
279
|
| `MEMOS_SPREAD_ACTIVATION` | No | `false` | One-hop graph spreading activation. When on, a search also returns memories reachable from its direct hits over `CAUSE` / `CONDITION` / `RELATE` edges, annotated ` · via CAUSE from <first 8 chars of the source id>`. Full mode only — it queries Neo4j directly, so `NEO4J_HTTP_URL`, `NEO4J_USER`, and `NEO4J_PASSWORD` must all resolve |
|
|
253
|
-
| `MEMOS_SHOW_WORKING_MEMORY` | No | `false` | Show the scheduler's `WorkingMemory` tier.
|
|
280
|
+
| `MEMOS_SHOW_WORKING_MEMORY` | No | `false` | Show the scheduler's `WorkingMemory` tier. Chat/document and legacy ingestion can create short-term copies alongside long-term memories; hiding this tier avoids displaying those pairs. Vector-first content saves create one original record. Debugging only |
|
|
254
281
|
| `MEMOS_AUTO_CAPTURE` | No | `false` | Accept auto-captured memories. Auto-captured records are also ranked below explicit saves |
|
|
255
282
|
|
|
256
283
|
> **Spreading activation degrades silently without Neo4j credentials.** Retrieval
|
package/dist/api-client.d.ts
CHANGED
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
*
|
|
4
4
|
* HTTP fetch wrapper with timeout, retry, and health check.
|
|
5
5
|
*/
|
|
6
|
+
/** A response deadline does not prove the API is offline or a write failed. */
|
|
7
|
+
export declare function isApiTimeoutError(error: unknown): boolean;
|
|
8
|
+
/** Return true when an exception means the HTTP peer could not be reached. */
|
|
9
|
+
export declare function isApiUnreachableError(error: unknown): boolean;
|
|
10
|
+
/** Return true only when no request body could have reached the peer. */
|
|
11
|
+
export declare function isSafeApiAliasFallbackError(error: unknown): boolean;
|
|
12
|
+
/** Add the IPv4 loopback alias for clients whose resolver prefers IPv6. */
|
|
13
|
+
export declare function apiUrlCandidates(url: string): string[];
|
|
14
|
+
/** Join an API path to the configured base without losing its prefix/query. */
|
|
15
|
+
export declare function apiUrl(path?: string, base?: string): string;
|
|
16
|
+
export declare function apiHealthUrl(base?: string, detail?: boolean): string;
|
|
17
|
+
/** Remove credentials and query values before an endpoint is shown to a client. */
|
|
18
|
+
export declare function apiUrlForDisplay(url?: string): string;
|
|
6
19
|
export declare function fetchWithTimeout(url: string, options?: RequestInit & {
|
|
7
20
|
timeoutMs?: number;
|
|
8
21
|
}): Promise<Response>;
|
package/dist/api-client.js
CHANGED
|
@@ -3,25 +3,296 @@
|
|
|
3
3
|
*
|
|
4
4
|
* HTTP fetch wrapper with timeout, retry, and health check.
|
|
5
5
|
*/
|
|
6
|
+
import { execFileSync } from "node:child_process";
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { release as osRelease } from "node:os";
|
|
6
9
|
import { MEMOS_URL, MEMOS_API_WAIT_MAX, MEMOS_TIMEOUT_HEALTH, MEMOS_TIMEOUT_TOOL, logger, registeredCubes, } from "./config.js";
|
|
7
10
|
// ============================================================================
|
|
8
11
|
// Fetch with Timeout
|
|
9
12
|
// ============================================================================
|
|
13
|
+
const API_NETWORK_ERROR_CODES = [
|
|
14
|
+
"ECONNREFUSED",
|
|
15
|
+
"ECONNABORTED",
|
|
16
|
+
"ECONNRESET",
|
|
17
|
+
"ETIMEDOUT",
|
|
18
|
+
"ENETDOWN",
|
|
19
|
+
"ENETUNREACH",
|
|
20
|
+
"EHOSTDOWN",
|
|
21
|
+
"EHOSTUNREACH",
|
|
22
|
+
"ENOTFOUND",
|
|
23
|
+
"EAI_AGAIN",
|
|
24
|
+
"EADDRNOTAVAIL",
|
|
25
|
+
"UND_ERR_SOCKET",
|
|
26
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
27
|
+
];
|
|
28
|
+
// These failures happen before an HTTP request can be delivered. They are the
|
|
29
|
+
// only network errors safe to replay for a non-idempotent method.
|
|
30
|
+
const API_ALIAS_SAFE_ERROR_CODES = [
|
|
31
|
+
"ECONNREFUSED",
|
|
32
|
+
"ENETUNREACH",
|
|
33
|
+
"EHOSTUNREACH",
|
|
34
|
+
"ENOTFOUND",
|
|
35
|
+
"EAI_AGAIN",
|
|
36
|
+
"EADDRNOTAVAIL",
|
|
37
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
38
|
+
];
|
|
39
|
+
let discoveredWslHosts;
|
|
40
|
+
function isWslRuntime() {
|
|
41
|
+
if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP)
|
|
42
|
+
return true;
|
|
43
|
+
if (process.platform !== "linux")
|
|
44
|
+
return false;
|
|
45
|
+
const kernel = osRelease().toLowerCase();
|
|
46
|
+
return kernel.includes("microsoft") || kernel.includes("wsl");
|
|
47
|
+
}
|
|
48
|
+
function normalizeDiscoveredHost(value) {
|
|
49
|
+
const host = value.trim().replace(/^\[|\]$/g, "").replace(/%[^/]+$/, "");
|
|
50
|
+
if (!host ||
|
|
51
|
+
host === "localhost" ||
|
|
52
|
+
host === "::1" ||
|
|
53
|
+
host === "0.0.0.0" ||
|
|
54
|
+
/^127(?:\.[0-9]+){3}$/.test(host)) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
return host.includes(":") ? `[${host}]` : host;
|
|
58
|
+
}
|
|
59
|
+
function discoverWslHosts() {
|
|
60
|
+
if (discoveredWslHosts)
|
|
61
|
+
return discoveredWslHosts;
|
|
62
|
+
const hosts = new Set(["host.docker.internal"]);
|
|
63
|
+
try {
|
|
64
|
+
const resolvConf = readFileSync("/etc/resolv.conf", "utf8");
|
|
65
|
+
for (const line of resolvConf.split(/\r?\n/)) {
|
|
66
|
+
const match = /^\s*nameserver\s+(\S+)/.exec(line);
|
|
67
|
+
const host = match ? normalizeDiscoveredHost(match[1]) : undefined;
|
|
68
|
+
if (host)
|
|
69
|
+
hosts.add(host);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// WSL integrations may not expose resolv.conf; keep the other candidates.
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const routeOutput = String(execFileSync("ip", ["route", "show", "default"], {
|
|
77
|
+
encoding: "utf8",
|
|
78
|
+
timeout: 500,
|
|
79
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
80
|
+
}));
|
|
81
|
+
const match = /(?:^|\s)default\s+via\s+(\S+)/.exec(routeOutput);
|
|
82
|
+
const host = match ? normalizeDiscoveredHost(match[1]) : undefined;
|
|
83
|
+
if (host)
|
|
84
|
+
hosts.add(host);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// The ip utility is optional; resolv.conf and host.docker.internal suffice.
|
|
88
|
+
}
|
|
89
|
+
discoveredWslHosts = [...hosts];
|
|
90
|
+
return discoveredWslHosts;
|
|
91
|
+
}
|
|
92
|
+
function replaceUrlHost(url, host) {
|
|
93
|
+
try {
|
|
94
|
+
const parsed = new URL(url);
|
|
95
|
+
parsed.hostname = host;
|
|
96
|
+
return parsed.toString();
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** A response deadline does not prove the API is offline or a write failed. */
|
|
103
|
+
export function isApiTimeoutError(error) {
|
|
104
|
+
if (typeof error !== "object" || error === null)
|
|
105
|
+
return false;
|
|
106
|
+
const details = error;
|
|
107
|
+
if (details.name === "AbortError" || details.name === "TimeoutError")
|
|
108
|
+
return true;
|
|
109
|
+
if (details.code === "UND_ERR_HEADERS_TIMEOUT" || details.code === "UND_ERR_BODY_TIMEOUT")
|
|
110
|
+
return true;
|
|
111
|
+
return details.cause !== undefined && details.cause !== error && isApiTimeoutError(details.cause);
|
|
112
|
+
}
|
|
113
|
+
/** Return true when an exception means the HTTP peer could not be reached. */
|
|
114
|
+
export function isApiUnreachableError(error) {
|
|
115
|
+
if (isApiTimeoutError(error))
|
|
116
|
+
return false;
|
|
117
|
+
if (typeof error === "object" && error !== null) {
|
|
118
|
+
const details = error;
|
|
119
|
+
if (typeof details.code === "string" &&
|
|
120
|
+
API_NETWORK_ERROR_CODES.includes(details.code)) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
if (details.cause !== undefined && isApiUnreachableError(details.cause)) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const message = String(error).toLowerCase();
|
|
128
|
+
return (message.includes("fetch failed") ||
|
|
129
|
+
message.includes("network error") ||
|
|
130
|
+
message.includes("socket hang up") ||
|
|
131
|
+
message.includes("connect timeout") ||
|
|
132
|
+
API_NETWORK_ERROR_CODES.some((code) => message.includes(code.toLowerCase())));
|
|
133
|
+
}
|
|
134
|
+
/** Return true only when no request body could have reached the peer. */
|
|
135
|
+
export function isSafeApiAliasFallbackError(error) {
|
|
136
|
+
if (typeof error === "object" && error !== null) {
|
|
137
|
+
const details = error;
|
|
138
|
+
if (typeof details.code === "string" &&
|
|
139
|
+
API_ALIAS_SAFE_ERROR_CODES.includes(details.code)) {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
if (details.cause !== undefined && isSafeApiAliasFallbackError(details.cause)) {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const message = String(error).toLowerCase();
|
|
147
|
+
return API_ALIAS_SAFE_ERROR_CODES.some((code) => message.includes(code.toLowerCase()));
|
|
148
|
+
}
|
|
149
|
+
/** Add the IPv4 loopback alias for clients whose resolver prefers IPv6. */
|
|
150
|
+
export function apiUrlCandidates(url) {
|
|
151
|
+
let parsed;
|
|
152
|
+
try {
|
|
153
|
+
parsed = new URL(url);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return [url];
|
|
157
|
+
}
|
|
158
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
159
|
+
return [url];
|
|
160
|
+
const host = parsed.hostname.toLowerCase();
|
|
161
|
+
if (host !== "localhost" && host !== "127.0.0.1")
|
|
162
|
+
return [url];
|
|
163
|
+
const alias = new URL(url);
|
|
164
|
+
alias.hostname = host === "localhost" ? "127.0.0.1" : "localhost";
|
|
165
|
+
const candidates = [url, alias.toString()];
|
|
166
|
+
if (isWslRuntime()) {
|
|
167
|
+
for (const discoveredHost of discoverWslHosts()) {
|
|
168
|
+
const candidate = replaceUrlHost(url, discoveredHost);
|
|
169
|
+
if (candidate && !candidates.includes(candidate))
|
|
170
|
+
candidates.push(candidate);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return candidates;
|
|
174
|
+
}
|
|
175
|
+
const preferredLoopbackHosts = new Map();
|
|
176
|
+
function orderedApiUrlCandidates(url) {
|
|
177
|
+
const candidates = apiUrlCandidates(url);
|
|
178
|
+
if (candidates.length < 2)
|
|
179
|
+
return candidates;
|
|
180
|
+
try {
|
|
181
|
+
const parsed = new URL(url);
|
|
182
|
+
const preferredHost = preferredLoopbackHosts.get(parsed.origin);
|
|
183
|
+
if (!preferredHost)
|
|
184
|
+
return candidates;
|
|
185
|
+
const preferred = new URL(url);
|
|
186
|
+
preferred.hostname = preferredHost;
|
|
187
|
+
const preferredUrl = preferred.toString();
|
|
188
|
+
return [preferredUrl, ...candidates.filter((candidate) => candidate !== preferredUrl)];
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return candidates;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/** Join an API path to the configured base without losing its prefix/query. */
|
|
195
|
+
export function apiUrl(path = "", base = MEMOS_URL) {
|
|
196
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
197
|
+
try {
|
|
198
|
+
const parsedBase = new URL(base);
|
|
199
|
+
const parsedPath = new URL(normalizedPath, "http://memos.local");
|
|
200
|
+
const basePath = parsedBase.pathname.replace(/\/+$/, "").replace(/^\/+/, "");
|
|
201
|
+
const childPath = parsedPath.pathname.replace(/^\/+/, "");
|
|
202
|
+
const joinedPath = [basePath, childPath].filter(Boolean).join("/");
|
|
203
|
+
parsedBase.pathname = joinedPath ? `/${joinedPath}` : "/";
|
|
204
|
+
// Base query values (for example a gateway token) apply to every endpoint;
|
|
205
|
+
// a path-specific value wins when both use the same key.
|
|
206
|
+
const query = new URLSearchParams(parsedBase.search);
|
|
207
|
+
for (const [key, value] of parsedPath.searchParams)
|
|
208
|
+
query.set(key, value);
|
|
209
|
+
parsedBase.search = query.toString();
|
|
210
|
+
parsedBase.hash = "";
|
|
211
|
+
return parsedBase.toString();
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// Keep the old string behavior for a malformed base; the first fetch then
|
|
215
|
+
// reports the useful URL error to the caller.
|
|
216
|
+
const cleanBase = base.replace(/[?#].*$/, "").replace(/\/+$/, "");
|
|
217
|
+
const cleanPath = normalizedPath.replace(/^\/+/, "");
|
|
218
|
+
return `${cleanBase}/${cleanPath}`;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
export function apiHealthUrl(base = MEMOS_URL, detail = false) {
|
|
222
|
+
const suffix = detail ? "/health/detail" : "/health";
|
|
223
|
+
return apiUrl(suffix, base);
|
|
224
|
+
}
|
|
225
|
+
/** Remove credentials and query values before an endpoint is shown to a client. */
|
|
226
|
+
export function apiUrlForDisplay(url = MEMOS_URL) {
|
|
227
|
+
try {
|
|
228
|
+
const parsed = new URL(url);
|
|
229
|
+
parsed.username = "";
|
|
230
|
+
parsed.password = "";
|
|
231
|
+
parsed.search = "";
|
|
232
|
+
parsed.hash = "";
|
|
233
|
+
const displayed = parsed.toString();
|
|
234
|
+
return parsed.pathname === "/" && displayed.endsWith("/")
|
|
235
|
+
? displayed.slice(0, -1)
|
|
236
|
+
: displayed;
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return url
|
|
240
|
+
.replace(/[?#].*$/, "")
|
|
241
|
+
.replace(/:\/\/[^/@\s]+@/, "://");
|
|
242
|
+
}
|
|
243
|
+
}
|
|
10
244
|
export async function fetchWithTimeout(url, options = {}) {
|
|
11
245
|
const timeoutMs = (options.timeoutMs ?? MEMOS_TIMEOUT_TOOL) * 1000;
|
|
12
246
|
const { timeoutMs: _, ...fetchOptions } = options;
|
|
13
|
-
const
|
|
14
|
-
const
|
|
247
|
+
const method = String(fetchOptions.method ?? "GET").toUpperCase();
|
|
248
|
+
const candidates = orderedApiUrlCandidates(url);
|
|
249
|
+
const isIdempotent = method === "GET" || method === "HEAD";
|
|
250
|
+
let originalOrigin;
|
|
15
251
|
try {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return response;
|
|
252
|
+
originalOrigin = new URL(url).origin;
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
// The first fetch will report the malformed URL.
|
|
21
256
|
}
|
|
22
|
-
|
|
23
|
-
|
|
257
|
+
let lastError;
|
|
258
|
+
for (const candidate of candidates) {
|
|
259
|
+
const controller = new AbortController();
|
|
260
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
261
|
+
try {
|
|
262
|
+
const response = await fetch(candidate, {
|
|
263
|
+
...fetchOptions,
|
|
264
|
+
signal: controller.signal,
|
|
265
|
+
});
|
|
266
|
+
if (originalOrigin) {
|
|
267
|
+
const selectedOrigin = new URL(candidate).origin;
|
|
268
|
+
if (selectedOrigin === originalOrigin) {
|
|
269
|
+
preferredLoopbackHosts.delete(originalOrigin);
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
preferredLoopbackHosts.set(originalOrigin, new URL(candidate).hostname);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return response;
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
lastError = error;
|
|
279
|
+
const canTryAlias = candidate !== candidates.at(-1) &&
|
|
280
|
+
(isIdempotent
|
|
281
|
+
? isApiUnreachableError(error)
|
|
282
|
+
: isSafeApiAliasFallbackError(error));
|
|
283
|
+
if (!canTryAlias) {
|
|
284
|
+
if (originalOrigin) {
|
|
285
|
+
preferredLoopbackHosts.delete(originalOrigin);
|
|
286
|
+
}
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
logger.debug("MemOS API endpoint failed, trying loopback alias");
|
|
290
|
+
}
|
|
291
|
+
finally {
|
|
292
|
+
clearTimeout(timer);
|
|
293
|
+
}
|
|
24
294
|
}
|
|
295
|
+
throw lastError;
|
|
25
296
|
}
|
|
26
297
|
// ============================================================================
|
|
27
298
|
// API Readiness Check
|
|
@@ -32,7 +303,7 @@ export async function waitForApiReady(maxWait, interval = 2.0) {
|
|
|
32
303
|
const start = Date.now();
|
|
33
304
|
while (Date.now() - start < maxWaitMs) {
|
|
34
305
|
try {
|
|
35
|
-
const response = await fetchWithTimeout(
|
|
306
|
+
const response = await fetchWithTimeout(apiUrl("/users"), {
|
|
36
307
|
timeoutMs: MEMOS_TIMEOUT_HEALTH,
|
|
37
308
|
});
|
|
38
309
|
if (response.ok) {
|
|
@@ -57,7 +328,15 @@ function buildUrl(base, params) {
|
|
|
57
328
|
urlParams.append(k, String(v));
|
|
58
329
|
}
|
|
59
330
|
const qs = urlParams.toString();
|
|
60
|
-
|
|
331
|
+
if (!qs)
|
|
332
|
+
return base;
|
|
333
|
+
const hashIndex = base.indexOf("#");
|
|
334
|
+
const beforeHash = hashIndex === -1 ? base : base.slice(0, hashIndex);
|
|
335
|
+
const hash = hashIndex === -1 ? "" : base.slice(hashIndex);
|
|
336
|
+
const separator = beforeHash.includes("?")
|
|
337
|
+
? (beforeHash.endsWith("?") || beforeHash.endsWith("&") ? "" : "&")
|
|
338
|
+
: "?";
|
|
339
|
+
return `${beforeHash}${separator}${qs}${hash}`;
|
|
61
340
|
}
|
|
62
341
|
async function doFetch(method, url, options) {
|
|
63
342
|
const finalUrl = buildUrl(url, options.params);
|
|
@@ -80,7 +359,11 @@ async function doFetch(method, url, options) {
|
|
|
80
359
|
const data = (await response.json());
|
|
81
360
|
return { status: response.status, data };
|
|
82
361
|
}
|
|
83
|
-
catch {
|
|
362
|
+
catch (error) {
|
|
363
|
+
// Body reads can fail after response headers arrive. Preserve deadlines
|
|
364
|
+
// as exceptions so an ambiguous POST is not replayed or reported offline.
|
|
365
|
+
if (isApiTimeoutError(error))
|
|
366
|
+
throw error;
|
|
84
367
|
return { status: response.status, data: null };
|
|
85
368
|
}
|
|
86
369
|
}
|
package/dist/cube-manager.js
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import * as fs from "fs";
|
|
7
7
|
import * as path from "path";
|
|
8
|
-
import {
|
|
8
|
+
import { MEMOS_USER, MEMOS_DEFAULT_CUBE, MEMOS_CUBES_DIR, MEMOS_TIMEOUT_STARTUP, REGISTRATION_RETRY_INTERVAL, registeredCubes, lastRegistrationAttempt, logger, isDefaultCubeFromEnv, } from "./config.js";
|
|
9
9
|
import { detectCubeFromPath } from "./keyword-enhancer.js";
|
|
10
|
-
import { fetchWithTimeout } from "./api-client.js";
|
|
10
|
+
import { apiUrl, apiUrlForDisplay, fetchWithTimeout, isApiUnreachableError, } from "./api-client.js";
|
|
11
11
|
// ============================================================================
|
|
12
12
|
// Windows/WSL Path Conversion
|
|
13
13
|
// ============================================================================
|
|
@@ -368,7 +368,7 @@ export function ensureCubeDirectory(cubeId) {
|
|
|
368
368
|
// ============================================================================
|
|
369
369
|
export async function verifyCubeLoaded(cubeId) {
|
|
370
370
|
try {
|
|
371
|
-
const response = await fetchWithTimeout(
|
|
371
|
+
const response = await fetchWithTimeout(apiUrl(`/memories?user_id=${encodeURIComponent(MEMOS_USER)}&mem_cube_id=${encodeURIComponent(cubeId)}&limit=1`), { method: "GET", timeoutMs: 5 });
|
|
372
372
|
if (response.ok) {
|
|
373
373
|
const data = await response.json();
|
|
374
374
|
return data.code === 200;
|
|
@@ -412,7 +412,7 @@ export async function ensureCubeRegistered(cubeId, force = false) {
|
|
|
412
412
|
}
|
|
413
413
|
}
|
|
414
414
|
// Register with API
|
|
415
|
-
const response = await fetchWithTimeout(
|
|
415
|
+
const response = await fetchWithTimeout(apiUrl("/mem_cubes"), {
|
|
416
416
|
method: "POST",
|
|
417
417
|
headers: { "Content-Type": "application/json" },
|
|
418
418
|
body: JSON.stringify({
|
|
@@ -441,8 +441,8 @@ export async function ensureCubeRegistered(cubeId, force = false) {
|
|
|
441
441
|
}
|
|
442
442
|
catch (err) {
|
|
443
443
|
const msg = String(err);
|
|
444
|
-
if (
|
|
445
|
-
return [false, `Cannot connect to MemOS API at ${
|
|
444
|
+
if (isApiUnreachableError(err)) {
|
|
445
|
+
return [false, `Cannot connect to MemOS API at ${apiUrlForDisplay()}. Is the server running?`];
|
|
446
446
|
}
|
|
447
447
|
return [false, `Failed to register cube '${cubeId}': ${err}`];
|
|
448
448
|
}
|
package/dist/handlers/admin.js
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from "fs";
|
|
8
8
|
import * as path from "path";
|
|
9
|
-
import {
|
|
10
|
-
import { fetchWithTimeout } from "../api-client.js";
|
|
9
|
+
import { MEMOS_USER, MEMOS_DEFAULT_CUBE, MEMOS_CUBES_DIR, MEMOS_ENABLE_DELETE, MEMOS_TIMEOUT_TOOL, registeredCubes, } from "../config.js";
|
|
10
|
+
import { apiUrl, apiUrlForDisplay, fetchWithTimeout } from "../api-client.js";
|
|
11
11
|
import { ensureCubeDirectory, ensureCubeRegistered, getCubePath, getCubesBaseDir, getLocalCubesBaseDir, listAvailableCubes, validateAndFixCubeConfig, verifyCubeLoaded, } from "../cube-manager.js";
|
|
12
12
|
import { ERR_CUBE_NOT_FOUND, ERR_DELETE_DISABLED, ERR_PARAM_MISSING, apiErrorResponse, cubeRegistrationError, errorResponse, getCubeIdFromArgs, } from "./utils.js";
|
|
13
13
|
// ============================================================================
|
|
@@ -43,7 +43,7 @@ export async function handleMemosListCubes(arguments_) {
|
|
|
43
43
|
"",
|
|
44
44
|
`Cubes directory: \`${cubesDir}\``,
|
|
45
45
|
"",
|
|
46
|
-
`To create a new cube, use the MemOS web interface at ${
|
|
46
|
+
`To create a new cube, use the MemOS web interface at ${apiUrlForDisplay(apiUrl("/docs"))}`,
|
|
47
47
|
"or create a cube directory with a config.json file.",
|
|
48
48
|
].join("\n") }];
|
|
49
49
|
}
|
|
@@ -107,7 +107,7 @@ export async function handleMemosRegisterCube(arguments_) {
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
try {
|
|
110
|
-
const response = await fetchWithTimeout(
|
|
110
|
+
const response = await fetchWithTimeout(apiUrl("/mem_cubes"), {
|
|
111
111
|
method: "POST",
|
|
112
112
|
headers: { "Content-Type": "application/json" },
|
|
113
113
|
body: JSON.stringify({
|
|
@@ -157,7 +157,7 @@ export async function handleMemosCreateUser(arguments_) {
|
|
|
157
157
|
const userId = String(arguments_.user_id ?? MEMOS_USER);
|
|
158
158
|
const userName = String(arguments_.user_name ?? userId);
|
|
159
159
|
try {
|
|
160
|
-
const response = await fetchWithTimeout(
|
|
160
|
+
const response = await fetchWithTimeout(apiUrl("/users"), {
|
|
161
161
|
method: "POST",
|
|
162
162
|
headers: { "Content-Type": "application/json" },
|
|
163
163
|
body: JSON.stringify({
|
|
@@ -292,7 +292,7 @@ export async function handleMemosDelete(arguments_) {
|
|
|
292
292
|
if (!regSuccess)
|
|
293
293
|
return cubeRegistrationError(cubeId, regError);
|
|
294
294
|
if (deleteAll) {
|
|
295
|
-
const response = await fetchWithTimeout(
|
|
295
|
+
const response = await fetchWithTimeout(apiUrl(`/memories/${cubeId}?user_id=${encodeURIComponent(MEMOS_USER)}`), { method: "DELETE" });
|
|
296
296
|
if (response.ok) {
|
|
297
297
|
const data = await response.json();
|
|
298
298
|
if (data.code === 200) {
|
|
@@ -321,7 +321,7 @@ export async function handleMemosDelete(arguments_) {
|
|
|
321
321
|
for (const mid of idsToDelete) {
|
|
322
322
|
let memContent = "*(Unknown content)*";
|
|
323
323
|
try {
|
|
324
|
-
const getResp = await fetchWithTimeout(
|
|
324
|
+
const getResp = await fetchWithTimeout(apiUrl(`/memories/${cubeId}/${mid}?user_id=${encodeURIComponent(MEMOS_USER)}`), { method: "GET" });
|
|
325
325
|
if (getResp.ok) {
|
|
326
326
|
const gData = await getResp.json();
|
|
327
327
|
if (gData.code === 200 && gData.data) {
|
|
@@ -335,7 +335,7 @@ export async function handleMemosDelete(arguments_) {
|
|
|
335
335
|
catch {
|
|
336
336
|
memContent = "*(Fetch failed)*";
|
|
337
337
|
}
|
|
338
|
-
const deleteResp = await fetchWithTimeout(
|
|
338
|
+
const deleteResp = await fetchWithTimeout(apiUrl(`/memories/${cubeId}/${mid}?user_id=${encodeURIComponent(MEMOS_USER)}`), { method: "DELETE" });
|
|
339
339
|
if (deleteResp.ok) {
|
|
340
340
|
const data = await deleteResp.json();
|
|
341
341
|
if (data.code === 200) {
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* memos_admin(action="calendar") - project mode and student mode
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
7
|
-
import { apiCallWithRetry } from "../api-client.js";
|
|
6
|
+
import { MEMOS_USER } from "../config.js";
|
|
7
|
+
import { apiCallWithRetry, apiUrl } from "../api-client.js";
|
|
8
8
|
import { ensureCubeRegistered } from "../cube-manager.js";
|
|
9
9
|
import { apiErrorResponse, cubeRegistrationError, getCubeIdFromArgs } from "./utils.js";
|
|
10
10
|
// ============================================================================
|
|
@@ -201,7 +201,7 @@ export async function handleMemosCalendar(arguments_) {
|
|
|
201
201
|
if (!regSuccess)
|
|
202
202
|
return cubeRegistrationError(cubeId, regError);
|
|
203
203
|
if (mode === "project") {
|
|
204
|
-
const result = await apiCallWithRetry("GET",
|
|
204
|
+
const result = await apiCallWithRetry("GET", apiUrl("/memories"), cubeId, { params: { user_id: MEMOS_USER, mem_cube_id: cubeId, limit: 200 } }, ensureCubeRegistered);
|
|
205
205
|
if (result.success && result.data) {
|
|
206
206
|
const resultData = result.data.data ?? {};
|
|
207
207
|
const memories = [];
|
|
@@ -229,7 +229,7 @@ export async function handleMemosCalendar(arguments_) {
|
|
|
229
229
|
if (week !== undefined) {
|
|
230
230
|
[startDate, endDate] = getWeekDates(startDate, week);
|
|
231
231
|
}
|
|
232
|
-
const result = await apiCallWithRetry("GET",
|
|
232
|
+
const result = await apiCallWithRetry("GET", apiUrl("/memories"), cubeId, { params: { user_id: MEMOS_USER, mem_cube_id: cubeId, limit: 100 } }, ensureCubeRegistered);
|
|
233
233
|
if (result.success && result.data) {
|
|
234
234
|
const resultData = result.data.data ?? {};
|
|
235
235
|
let memories = [];
|
package/dist/handlers/graph.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* memos_graph(mode="path"), memos_graph(mode="related"), memos_graph(mode="schema"), memos_graph(mode="impact")
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
7
|
-
import { fetchWithTimeout } from "../api-client.js";
|
|
6
|
+
import { MEMOS_USER, NEO4J_HTTP_URL, NEO4J_USER, NEO4J_PASSWORD, logger, registeredCubes, } from "../config.js";
|
|
7
|
+
import { apiUrl, fetchWithTimeout } from "../api-client.js";
|
|
8
8
|
import { ensureCubeRegistered } from "../cube-manager.js";
|
|
9
9
|
import { formatProvenance } from "../graph-provenance.js";
|
|
10
10
|
import { buildGraphImportPlan, renderGraphImportPlan, } from "../graphify-import.js";
|
|
@@ -99,7 +99,7 @@ export async function handleMemosTracePath(arguments_) {
|
|
|
99
99
|
if (!regSuccess)
|
|
100
100
|
return cubeRegistrationError(cubeId, regError);
|
|
101
101
|
try {
|
|
102
|
-
const response = await fetchWithTimeout(
|
|
102
|
+
const response = await fetchWithTimeout(apiUrl("/product/graph/trace_path"), {
|
|
103
103
|
method: "POST",
|
|
104
104
|
headers: { "Content-Type": "application/json" },
|
|
105
105
|
body: JSON.stringify({
|
|
@@ -243,7 +243,7 @@ export async function handleMemosGetGraph(arguments_) {
|
|
|
243
243
|
// Search for relevant memories first
|
|
244
244
|
let memories = [];
|
|
245
245
|
try {
|
|
246
|
-
const searchResponse = await fetchWithTimeout(
|
|
246
|
+
const searchResponse = await fetchWithTimeout(apiUrl("/search"), {
|
|
247
247
|
method: "POST",
|
|
248
248
|
headers: { "Content-Type": "application/json" },
|
|
249
249
|
body: JSON.stringify({
|
|
@@ -262,7 +262,7 @@ export async function handleMemosGetGraph(arguments_) {
|
|
|
262
262
|
registeredCubes.delete(cubeId);
|
|
263
263
|
const [retrySuccess] = await ensureCubeRegistered(cubeId, true);
|
|
264
264
|
if (retrySuccess) {
|
|
265
|
-
const retrySearch = await fetchWithTimeout(
|
|
265
|
+
const retrySearch = await fetchWithTimeout(apiUrl("/search"), {
|
|
266
266
|
method: "POST",
|
|
267
267
|
headers: { "Content-Type": "application/json" },
|
|
268
268
|
body: JSON.stringify({
|
|
@@ -499,7 +499,7 @@ export async function handleMemosExportSchema(arguments_) {
|
|
|
499
499
|
if (!regSuccess)
|
|
500
500
|
return cubeRegistrationError(cubeId, regError);
|
|
501
501
|
try {
|
|
502
|
-
const response = await fetchWithTimeout(
|
|
502
|
+
const response = await fetchWithTimeout(apiUrl("/graph/schema"), {
|
|
503
503
|
method: "POST",
|
|
504
504
|
headers: { "Content-Type": "application/json" },
|
|
505
505
|
body: JSON.stringify({
|
package/dist/handlers/index.d.ts
CHANGED
|
@@ -7,4 +7,5 @@
|
|
|
7
7
|
import type { TextContent } from "../types.js";
|
|
8
8
|
export declare function dispatchTool(name: string, arguments_: Record<string, unknown>): Promise<TextContent[]>;
|
|
9
9
|
export declare function handleApiUnreachable(): Promise<TextContent[]>;
|
|
10
|
+
export declare function handleApiTimeout(): Promise<TextContent[]>;
|
|
10
11
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/handlers/index.js
CHANGED
|
@@ -14,7 +14,8 @@ import { handleMemosImportWiki } from "./wiki-import.js";
|
|
|
14
14
|
import { handleMemosCanvas } from "./canvas.js";
|
|
15
15
|
import { handleMemosDistillSkill, handleMemosListSkillCandidates, handleMemosReviewSkillCandidate, handleMemosInstallSkillCandidate } from "./skill.js";
|
|
16
16
|
import { handleMemosListCubes, handleMemosRegisterCube, handleMemosCreateUser, handleMemosValidateCubes, handleMemosDelete, handleMemosCapabilities, } from "./admin.js";
|
|
17
|
-
import {
|
|
17
|
+
import { MEMOS_PROVIDER } from "../config.js";
|
|
18
|
+
import { apiHealthUrl, apiUrlForDisplay } from "../api-client.js";
|
|
18
19
|
import { errorResponse } from "./utils.js";
|
|
19
20
|
export async function dispatchTool(name, arguments_) {
|
|
20
21
|
if (MEMOS_PROVIDER === "local" && ["memos_think", "memos_graph", "memos_export_wiki", "memos_import_wiki", "memos_admin", "memos_delete"].includes(name)) {
|
|
@@ -99,13 +100,19 @@ export async function handleApiUnreachable() {
|
|
|
99
100
|
return [{
|
|
100
101
|
type: "text",
|
|
101
102
|
text: [
|
|
102
|
-
`❌ [API_UNREACHABLE] Cannot connect to MemOS API at ${
|
|
103
|
+
`❌ [API_UNREACHABLE] Cannot connect to MemOS API at ${apiUrlForDisplay()}`,
|
|
103
104
|
"",
|
|
104
105
|
"💡 Suggestions:",
|
|
105
|
-
|
|
106
|
+
`- Check if MemOS API is running: \`curl ${apiUrlForDisplay(apiHealthUrl())}\``,
|
|
106
107
|
"- Start with: `scripts/local/start.bat`",
|
|
107
108
|
"- Check port availability",
|
|
108
109
|
].join("\n"),
|
|
109
110
|
}];
|
|
110
111
|
}
|
|
112
|
+
export async function handleApiTimeout() {
|
|
113
|
+
return errorResponse(`MemOS API at ${apiUrlForDisplay()} did not respond before the request deadline.`, "API_TIMEOUT", [
|
|
114
|
+
"A write may still have completed. Check memos_search or memos_list_v2 before retrying.",
|
|
115
|
+
"Check API logs for storage delays; LLM parsing runs in background after vector confirmation.",
|
|
116
|
+
]);
|
|
117
|
+
}
|
|
111
118
|
//# sourceMappingURL=index.js.map
|
package/dist/handlers/memory.js
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
* memos_save, memos_list_v2, memos_get, memos_admin(action="stats")
|
|
5
5
|
*/
|
|
6
6
|
import * as crypto from "crypto";
|
|
7
|
-
import {
|
|
7
|
+
import { MEMOS_USER, MEMOS_CUBES_DIR, logger } from "../config.js";
|
|
8
8
|
import { getMemoryProvider } from "../providers/provider-factory.js";
|
|
9
|
-
import { apiCallWithRetry } from "../api-client.js";
|
|
9
|
+
import { apiCallWithRetry, apiUrl } from "../api-client.js";
|
|
10
10
|
import { ensureCubeRegistered } from "../cube-manager.js";
|
|
11
|
-
import { parseMemoryWriteResponse } from "../memory-write-response.js";
|
|
11
|
+
import { parseMemoryWriteResponse, UnconfirmedMemoryWriteError } from "../memory-write-response.js";
|
|
12
12
|
import { recordAccess } from "../access-tracker.js";
|
|
13
13
|
import { filterEphemeralTier } from "../memory-tier.js";
|
|
14
14
|
import { findSiblings, renderVerbatimSections, verbatimOf, } from "../verbatim-source.js";
|
|
@@ -95,24 +95,32 @@ export async function handleMemosSave(arguments_) {
|
|
|
95
95
|
const [regSuccess, regError] = await ensureCubeRegistered(cubeId);
|
|
96
96
|
if (!regSuccess)
|
|
97
97
|
return cubeRegistrationError(cubeId, regError);
|
|
98
|
-
const result = await apiCallWithRetry("POST",
|
|
98
|
+
const result = await apiCallWithRetry("POST", apiUrl("/memories"), cubeId, {
|
|
99
99
|
body: {
|
|
100
100
|
user_id: MEMOS_USER,
|
|
101
101
|
mem_cube_id: cubeId,
|
|
102
102
|
memory_content: content,
|
|
103
|
+
memory_type: memoryType,
|
|
103
104
|
},
|
|
104
105
|
}, ensureCubeRegistered);
|
|
105
106
|
if (result.success) {
|
|
106
|
-
markSaved(content, cubeId);
|
|
107
107
|
const write = parseMemoryWriteResponse(result.data);
|
|
108
|
+
if (write.vectorSaved === false || (write.vectorSaved === true && write.memoryIds.length === 0)) {
|
|
109
|
+
throw new UnconfirmedMemoryWriteError(errorResponse("Vector storage was not confirmed; the save cannot be acknowledged.", "VECTOR_SAVE_UNCONFIRMED", ["Check API storage logs and search for the memory before retrying."]));
|
|
110
|
+
}
|
|
111
|
+
markSaved(content, cubeId);
|
|
108
112
|
const idText = write.memoryIds.length > 0 ? ` · IDs: ${write.memoryIds.join(", ")}` : "";
|
|
109
113
|
const warningText = write.warnings.length > 0
|
|
110
114
|
? ` · warnings: ${write.warnings.join("; ")}`
|
|
111
115
|
: "";
|
|
116
|
+
const vectorText = write.vectorSaved === true ? " · vector saved" : "";
|
|
117
|
+
const enrichmentText = write.enrichmentStatus === "pending"
|
|
118
|
+
? " · LLM parsing continues in background"
|
|
119
|
+
: write.enrichmentStatus === "failed" ? " · background parsing failed; saved memory is retained" : "";
|
|
112
120
|
return [
|
|
113
121
|
{
|
|
114
122
|
type: "text",
|
|
115
|
-
text: `Memory saved as [${memoryType}] in cube '${cubeId}'${idText}${warningText}`,
|
|
123
|
+
text: `Memory saved as [${memoryType}] in cube '${cubeId}'${idText}${vectorText}${enrichmentText}${warningText}`,
|
|
116
124
|
},
|
|
117
125
|
];
|
|
118
126
|
}
|
|
@@ -177,7 +185,7 @@ export async function handleMemosList(arguments_) {
|
|
|
177
185
|
// (the API filters by internal type, not MCP type), but never the whole cube —
|
|
178
186
|
// pulling everything blew a limit=5 request up to ~1.8M tokens on large cubes.
|
|
179
187
|
params.limit = memoryType ? Math.max(limit * 20, 200) : limit;
|
|
180
|
-
const result = await apiCallWithRetry("GET",
|
|
188
|
+
const result = await apiCallWithRetry("GET", apiUrl("/memories"), cubeId, { params }, ensureCubeRegistered);
|
|
181
189
|
if (result.success && result.data) {
|
|
182
190
|
const resultData = result.data.data ?? {};
|
|
183
191
|
const allMemories = prepareListMemories(resultData, memoryType, limit);
|
|
@@ -233,7 +241,7 @@ export async function handleMemosList(arguments_) {
|
|
|
233
241
|
*/
|
|
234
242
|
async function fetchSiblings(cubeId, target) {
|
|
235
243
|
try {
|
|
236
|
-
const result = await apiCallWithRetry("GET",
|
|
244
|
+
const result = await apiCallWithRetry("GET", apiUrl("/memories"), cubeId, { params: { user_id: MEMOS_USER, mem_cube_id: cubeId, limit: 200 } }, ensureCubeRegistered);
|
|
237
245
|
if (!result.success || !result.data)
|
|
238
246
|
return [];
|
|
239
247
|
const data = result.data.data;
|
|
@@ -286,7 +294,13 @@ export async function handleMemosGet(arguments_) {
|
|
|
286
294
|
const [regSuccess, regError] = await ensureCubeRegistered(cubeId);
|
|
287
295
|
if (!regSuccess)
|
|
288
296
|
return cubeRegistrationError(cubeId, regError);
|
|
289
|
-
const result = await apiCallWithRetry("GET",
|
|
297
|
+
const result = await apiCallWithRetry("GET", apiUrl(`/memories/${cubeId}/${memoryId}`), cubeId,
|
|
298
|
+
// user_id 必须显式传:后端 `get_memory` 的 user_id 是可选参数,缺失时回退到
|
|
299
|
+
// MOS 实例自己的 user_id(root)。而 ensureCubeRegistered 注册的是 MEMOS_USER
|
|
300
|
+
// (dev_user),于是 root 对该 cube 无授权,校验抛 ValueError → 400
|
|
301
|
+
// "User 'root' does not have access to cube '...'"。重注册重试也救不回来:
|
|
302
|
+
// 它仍然按 MEMOS_USER 注册,重试请求仍然不带 user_id,第二次照样落到 root。
|
|
303
|
+
{ params: { user_id: MEMOS_USER } }, ensureCubeRegistered);
|
|
290
304
|
if (result.success && result.data) {
|
|
291
305
|
const resultData = result.data
|
|
292
306
|
.data;
|
|
@@ -379,7 +393,7 @@ export async function handleMemosGetStats(arguments_) {
|
|
|
379
393
|
const [regSuccess, regError] = await ensureCubeRegistered(cubeId);
|
|
380
394
|
if (!regSuccess)
|
|
381
395
|
return cubeRegistrationError(cubeId, regError);
|
|
382
|
-
const result = await apiCallWithRetry("GET",
|
|
396
|
+
const result = await apiCallWithRetry("GET", apiUrl("/memories"), cubeId, { params: { user_id: MEMOS_USER, mem_cube_id: cubeId } }, ensureCubeRegistered);
|
|
383
397
|
if (result.success && result.data) {
|
|
384
398
|
const [stats, total] = computeMemoryStats(result.data.data ?? {});
|
|
385
399
|
if (total === 0) {
|
package/dist/handlers/search.js
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* memos_search, memos_search, memos_suggest, memos_context_resume
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { MEMOS_USER, NEO4J_HTTP_URL, NEO4J_USER, NEO4J_PASSWORD, MEMOS_MODE, MEMOS_CUBES_DIR, logger, } from "../config.js";
|
|
7
7
|
import { getMemoryProvider } from "../providers/provider-factory.js";
|
|
8
|
-
import { apiCallWithRetry, fetchWithTimeout } from "../api-client.js";
|
|
8
|
+
import { apiCallWithRetry, apiUrl, fetchWithTimeout } from "../api-client.js";
|
|
9
9
|
import { ensureCubeRegistered } from "../cube-manager.js";
|
|
10
10
|
import { formatMemoriesForDisplay } from "../formatters.js";
|
|
11
11
|
import { PREVIEW_COUNT, shouldCompact, compactedResultToText, toMinimal, } from "../models.js";
|
|
@@ -307,7 +307,7 @@ export async function handleMemosSearch(arguments_) {
|
|
|
307
307
|
const [regSuccess, regError] = await ensureCubeRegistered(cubeId);
|
|
308
308
|
if (!regSuccess)
|
|
309
309
|
return cubeRegistrationError(cubeId, regError);
|
|
310
|
-
const apiResult = await apiCallWithRetry("POST",
|
|
310
|
+
const apiResult = await apiCallWithRetry("POST", apiUrl("/search"), cubeId, {
|
|
311
311
|
body: {
|
|
312
312
|
user_id: MEMOS_USER,
|
|
313
313
|
query,
|
|
@@ -387,7 +387,7 @@ export async function handleMemosSearchContext(arguments_) {
|
|
|
387
387
|
if (!regSuccess)
|
|
388
388
|
return cubeRegistrationError(cubeId, regError);
|
|
389
389
|
try {
|
|
390
|
-
const response = await fetchWithTimeout(
|
|
390
|
+
const response = await fetchWithTimeout(apiUrl("/search"), {
|
|
391
391
|
method: "POST",
|
|
392
392
|
headers: { "Content-Type": "application/json" },
|
|
393
393
|
body: JSON.stringify({
|
|
@@ -424,7 +424,7 @@ export async function handleMemosSearchContext(arguments_) {
|
|
|
424
424
|
}
|
|
425
425
|
else {
|
|
426
426
|
// Fallback to standard search
|
|
427
|
-
const fallbackResult = await apiCallWithRetry("POST",
|
|
427
|
+
const fallbackResult = await apiCallWithRetry("POST", apiUrl("/search"), cubeId, { body: { user_id: MEMOS_USER, query, install_cube_ids: [cubeId] } }, ensureCubeRegistered);
|
|
428
428
|
if (fallbackResult.success && fallbackResult.data) {
|
|
429
429
|
let resultData = fallbackResult.data
|
|
430
430
|
.data ?? {};
|
|
@@ -564,7 +564,7 @@ export async function handleMemosContextResume(arguments_) {
|
|
|
564
564
|
let recentMemories = await getTemporalMemories(cubeId, RESUME_LIMIT, 24);
|
|
565
565
|
// Fallback to API list
|
|
566
566
|
if (recentMemories.length === 0) {
|
|
567
|
-
const result = await apiCallWithRetry("GET",
|
|
567
|
+
const result = await apiCallWithRetry("GET", apiUrl("/memories"), cubeId, {
|
|
568
568
|
params: {
|
|
569
569
|
user_id: MEMOS_USER,
|
|
570
570
|
mem_cube_id: cubeId,
|
package/dist/handlers/think.js
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* of CONFLICT edges: those edges are only created by the reorganizer pipeline,
|
|
15
15
|
* which is disabled by default, so in practice they rarely exist.
|
|
16
16
|
*/
|
|
17
|
-
import {
|
|
18
|
-
import { apiCallWithRetry } from "../api-client.js";
|
|
17
|
+
import { MEMOS_USER } from "../config.js";
|
|
18
|
+
import { apiCallWithRetry, apiUrl } from "../api-client.js";
|
|
19
19
|
import { ensureCubeRegistered } from "../cube-manager.js";
|
|
20
20
|
import { extractKeywords, extractMcpType } from "../query-processing.js";
|
|
21
21
|
import { levenshteinDistance } from "../keyword-enhancer.js";
|
|
@@ -127,7 +127,7 @@ export async function handleMemosThink(arguments_) {
|
|
|
127
127
|
return cubeRegistrationError(cubeId, regError);
|
|
128
128
|
// Two recall paths in parallel: semantic search + recent temporal slice.
|
|
129
129
|
const [apiResult, temporal] = await Promise.all([
|
|
130
|
-
apiCallWithRetry("POST",
|
|
130
|
+
apiCallWithRetry("POST", apiUrl("/search"), cubeId, { body: { user_id: MEMOS_USER, query, install_cube_ids: [cubeId], top_k: topK } }, ensureCubeRegistered),
|
|
131
131
|
getTemporalMemories(cubeId, 5, 72).catch(() => []),
|
|
132
132
|
]);
|
|
133
133
|
if (!apiResult.success) {
|
package/dist/handlers/utils.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { detectCubeFromPath } from "../keyword-enhancer.js";
|
|
7
7
|
import { getDefaultCubeId } from "../cube-manager.js";
|
|
8
|
+
import { apiHealthUrl, apiUrlForDisplay } from "../api-client.js";
|
|
8
9
|
// ============================================================================
|
|
9
10
|
// Error Codes
|
|
10
11
|
// ============================================================================
|
|
@@ -43,7 +44,7 @@ export function successResponse(message) {
|
|
|
43
44
|
}
|
|
44
45
|
export function cubeRegistrationError(cubeId, detail) {
|
|
45
46
|
return errorResponse(`Cube '${cubeId}' registration failed: ${detail ?? "unknown error"}`, ERR_CUBE_REGISTRATION, [
|
|
46
|
-
`Check if MemOS API is running: \`curl
|
|
47
|
+
`Check if MemOS API is running: \`curl ${apiUrlForDisplay(apiHealthUrl())}\``,
|
|
47
48
|
`Verify cube exists: \`memos_admin(action="list_cubes", include_status=True)\``,
|
|
48
49
|
`Try manual registration: \`memos_admin(action="register_cube", cube_id="...")\``,
|
|
49
50
|
]);
|
|
@@ -53,13 +54,22 @@ export function apiErrorResponse(operation, statusOrMsg) {
|
|
|
53
54
|
// Turn known API errors into actionable next steps on the hot paths
|
|
54
55
|
// (save/search/list hit these far more often than register_cube does).
|
|
55
56
|
const suggestions = [];
|
|
56
|
-
|
|
57
|
+
// 「does not have access to cube」要在 user-missing 分支之前判:它同时含 "user"
|
|
58
|
+
// 和 cube 字样,但成因完全不同 —— 用户和 cube 都存在,只是该用户没被授权。
|
|
59
|
+
// 实测最常见的触发方式是调用方漏传 user_id,后端回退到 root 去校验(root 往往
|
|
60
|
+
// 没有项目 cube 的授权),所以提示要先指向 user_id,而不是让人去建用户。
|
|
61
|
+
if (msg.includes("does not have access")) {
|
|
62
|
+
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="...")`');
|
|
63
|
+
}
|
|
64
|
+
else if (msg.includes("user") &&
|
|
65
|
+
(msg.includes("not exist") || msg.includes("not found"))) {
|
|
57
66
|
suggestions.push('User missing — run `memos_admin(action="create_user", user_id="dev_user")`, then retry');
|
|
58
67
|
}
|
|
59
|
-
else if (msg.includes("not loaded") ||
|
|
68
|
+
else if (msg.includes("not loaded") ||
|
|
69
|
+
(msg.includes("cube") && msg.includes("not found"))) {
|
|
60
70
|
suggestions.push('Cube not registered — run `memos_admin(action="register_cube", cube_id="...")` (find ids via `memos_admin(action="list_cubes")`), then retry');
|
|
61
71
|
}
|
|
62
|
-
suggestions.push(
|
|
72
|
+
suggestions.push(`Check API health: \`curl ${apiUrlForDisplay(apiHealthUrl(undefined, true))}\``, "Check API logs for details");
|
|
63
73
|
return errorResponse(`${operation} failed: ${statusOrMsg}`, ERR_API_ERROR, suggestions);
|
|
64
74
|
}
|
|
65
75
|
// ============================================================================
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import * as fs from "fs";
|
|
17
17
|
import * as path from "path";
|
|
18
|
-
import {
|
|
19
|
-
import { fetchWithTimeout } from "../api-client.js";
|
|
18
|
+
import { logger } from "../config.js";
|
|
19
|
+
import { apiUrl, fetchWithTimeout } from "../api-client.js";
|
|
20
20
|
import { ensureCubeRegistered } from "../cube-manager.js";
|
|
21
21
|
import { extractMcpType } from "../query-processing.js";
|
|
22
22
|
import { EDGE_LABELS } from "../wiki-relations.js";
|
|
@@ -33,7 +33,7 @@ async function fetchAllGraphData(cubeId) {
|
|
|
33
33
|
let totalNodes = 0;
|
|
34
34
|
let totalEdges = 0;
|
|
35
35
|
for (let page = 1; page <= MAX_PAGES; page++) {
|
|
36
|
-
const response = await fetchWithTimeout(
|
|
36
|
+
const response = await fetchWithTimeout(apiUrl("/product/graph/data"), {
|
|
37
37
|
method: "POST",
|
|
38
38
|
headers: { "Content-Type": "application/json" },
|
|
39
39
|
// user_id doubles as the Neo4j user_name filter server-side, and cube
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { apiCallWithRetry } from "../api-client.js";
|
|
1
|
+
import { MEMOS_USER } from "../config.js";
|
|
2
|
+
import { apiCallWithRetry, apiUrl } from "../api-client.js";
|
|
3
3
|
import { ensureCubeRegistered } from "../cube-manager.js";
|
|
4
4
|
import { relationEdges } from "../wiki-relations.js";
|
|
5
5
|
async function writeRelationsForPage(cubeId, effectiveId, page, fileBaseIndex) {
|
|
@@ -7,7 +7,7 @@ async function writeRelationsForPage(cubeId, effectiveId, page, fileBaseIndex) {
|
|
|
7
7
|
const written = [];
|
|
8
8
|
const failed = [];
|
|
9
9
|
for (const edge of result.resolved) {
|
|
10
|
-
const relResult = await apiCallWithRetry("POST",
|
|
10
|
+
const relResult = await apiCallWithRetry("POST", apiUrl("/product/graph/relation"), cubeId, {
|
|
11
11
|
body: {
|
|
12
12
|
user_id: MEMOS_USER,
|
|
13
13
|
mem_cube_id: cubeId,
|
|
@@ -17,12 +17,12 @@
|
|
|
17
17
|
import * as crypto from "crypto";
|
|
18
18
|
import * as fs from "fs";
|
|
19
19
|
import * as path from "path";
|
|
20
|
-
import {
|
|
21
|
-
import { apiCallWithRetry } from "../api-client.js";
|
|
20
|
+
import { MEMOS_USER, logger } from "../config.js";
|
|
21
|
+
import { apiCallWithRetry, apiUrl } 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",
|
|
138
|
+
const result = await apiCallWithRetry("GET", apiUrl(`/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)
|
|
@@ -149,16 +164,23 @@ async function savePageAsMemory(cubeId, page) {
|
|
|
149
164
|
body.created_at = page.created;
|
|
150
165
|
if (page.updated)
|
|
151
166
|
body.updated_at = page.updated;
|
|
152
|
-
const result = await apiCallWithRetry("POST",
|
|
167
|
+
const result = await apiCallWithRetry("POST", apiUrl("/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,
|
|
159
175
|
};
|
|
160
176
|
}
|
|
161
177
|
const parsed = parseMemoryWriteResponse(result.data);
|
|
178
|
+
if (parsed.vectorSaved === false || (parsed.vectorSaved === true && parsed.memoryIds.length === 0)) {
|
|
179
|
+
return {
|
|
180
|
+
error: "Vector storage was not confirmed", memoryIds: [],
|
|
181
|
+
warnings: parsed.warnings, uncertain: true,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
162
184
|
return {
|
|
163
185
|
error: null,
|
|
164
186
|
memoryIds: parsed.memoryIds,
|
|
@@ -174,12 +196,12 @@ export async function handleMemosImportWiki(arguments_) {
|
|
|
174
196
|
const projectPath = arguments_.project_path;
|
|
175
197
|
const dryRun = arguments_.dry_run === true;
|
|
176
198
|
const onEdit = arguments_.on_edit === "version" ? "version" : "skip";
|
|
177
|
-
const wikiDir = arguments_.wiki_dir
|
|
178
|
-
|
|
199
|
+
const wikiDir = arguments_.wiki_dir ??
|
|
200
|
+
(projectPath ? path.join(projectPath, "docs", "memory-wiki") : undefined);
|
|
179
201
|
if (!wikiDir) {
|
|
180
202
|
return errorResponse("Need project_path (default wiki: <project_path>/docs/memory-wiki) or an explicit wiki_dir", ERR_PARAM_MISSING, [
|
|
181
203
|
'`memos_import_wiki(project_path="/mnt/g/my/project")`',
|
|
182
|
-
|
|
204
|
+
"Run `memos_export_wiki` first, edit pages, then import",
|
|
183
205
|
]);
|
|
184
206
|
}
|
|
185
207
|
const resolvedDir = path.resolve(wikiDir);
|
|
@@ -189,7 +211,9 @@ export async function handleMemosImportWiki(arguments_) {
|
|
|
189
211
|
return errorResponse(lockError, ERR_OPERATION_FAILED);
|
|
190
212
|
try {
|
|
191
213
|
if (!fs.existsSync(pagesDir)) {
|
|
192
|
-
return errorResponse(`No pages directory under '${resolvedDir}' — export a wiki first`, ERR_OPERATION_FAILED, [
|
|
214
|
+
return errorResponse(`No pages directory under '${resolvedDir}' — export a wiki first`, ERR_OPERATION_FAILED, [
|
|
215
|
+
`memos_export_wiki(project_path="...") writes <project_path>/docs/memory-wiki/pages/`,
|
|
216
|
+
]);
|
|
193
217
|
}
|
|
194
218
|
const [regSuccess, regError] = await ensureCubeRegistered(cubeId);
|
|
195
219
|
if (!regSuccess)
|
|
@@ -209,7 +233,10 @@ export async function handleMemosImportWiki(arguments_) {
|
|
|
209
233
|
}
|
|
210
234
|
const result = parseWikiPage(raw);
|
|
211
235
|
if (result.ok)
|
|
212
|
-
parsed.push({
|
|
236
|
+
parsed.push({
|
|
237
|
+
page: result.page,
|
|
238
|
+
relPath: path.relative(resolvedDir, file),
|
|
239
|
+
});
|
|
213
240
|
else if (result.foreign)
|
|
214
241
|
foreign += 1;
|
|
215
242
|
else
|
|
@@ -252,7 +279,9 @@ export async function handleMemosImportWiki(arguments_) {
|
|
|
252
279
|
archived += 1;
|
|
253
280
|
continue;
|
|
254
281
|
}
|
|
255
|
-
if (ledgerEntry &&
|
|
282
|
+
if (ledgerEntry &&
|
|
283
|
+
typeof ledgerEntry !== "string" &&
|
|
284
|
+
ledgerEntry.new_memory_ids.length > 0) {
|
|
256
285
|
// The old page has already produced a new version; compare the edited
|
|
257
286
|
// content hash before doing another API lookup/version.
|
|
258
287
|
const mappedHash = ledgerEntry.content_hash;
|
|
@@ -306,7 +335,9 @@ export async function handleMemosImportWiki(arguments_) {
|
|
|
306
335
|
unchanged += 1;
|
|
307
336
|
continue;
|
|
308
337
|
}
|
|
309
|
-
const ledgerHash = typeof ledgerEntry === "string"
|
|
338
|
+
const ledgerHash = typeof ledgerEntry === "string"
|
|
339
|
+
? ledgerEntry
|
|
340
|
+
: ledgerEntry?.content_hash;
|
|
310
341
|
if (ledgerHash === contentHash(page)) {
|
|
311
342
|
alreadyVersioned += 1;
|
|
312
343
|
continue;
|
|
@@ -370,7 +401,9 @@ export async function handleMemosImportWiki(arguments_) {
|
|
|
370
401
|
lines.push("", `❌ 失败明细(前 ${REPORT_LIST_LIMIT} 条):`, ...failures.slice(0, REPORT_LIST_LIMIT).map((s) => `- ${s}`));
|
|
371
402
|
}
|
|
372
403
|
if (apiWarnings.length > 0) {
|
|
373
|
-
lines.push("", "⚠️ API warnings:", ...apiWarnings
|
|
404
|
+
lines.push("", "⚠️ API warnings:", ...apiWarnings
|
|
405
|
+
.slice(0, REPORT_LIST_LIMIT)
|
|
406
|
+
.map((warning) => `- ${warning}`));
|
|
374
407
|
}
|
|
375
408
|
lines.push("", "ℹ️ 导入恢复 type/tags/confidence/status/时间字段;关联 wikilinks 写入 Neo4j 关系边;写入内容经过服务端凭据脱敏。");
|
|
376
409
|
return [{ type: "text", text: lines.join("\n") }];
|
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
import type { ApiResponse } from "./types.js";
|
|
1
|
+
import type { ApiResponse, TextContent } from "./types.js";
|
|
2
|
+
/** A rejected storage acknowledgement must be an MCP tool error, not success text. */
|
|
3
|
+
export declare class UnconfirmedMemoryWriteError extends Error {
|
|
4
|
+
readonly content: TextContent[];
|
|
5
|
+
constructor(content: TextContent[]);
|
|
6
|
+
}
|
|
2
7
|
export interface MemoryWriteResult {
|
|
3
8
|
memoryIds: string[];
|
|
4
9
|
warnings: string[];
|
|
10
|
+
vectorSaved?: boolean;
|
|
11
|
+
enrichmentStatus?: string;
|
|
5
12
|
}
|
|
6
13
|
/** Normalize the additive POST /memories response while accepting legacy APIs. */
|
|
7
14
|
export declare function parseMemoryWriteResponse(response: ApiResponse<unknown>): MemoryWriteResult;
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/** A rejected storage acknowledgement must be an MCP tool error, not success text. */
|
|
2
|
+
export class UnconfirmedMemoryWriteError extends Error {
|
|
3
|
+
content;
|
|
4
|
+
constructor(content) {
|
|
5
|
+
super(content.map((item) => item.text).join("\n"));
|
|
6
|
+
this.content = content;
|
|
7
|
+
this.name = "UnconfirmedMemoryWriteError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
1
10
|
/** Normalize the additive POST /memories response while accepting legacy APIs. */
|
|
2
11
|
export function parseMemoryWriteResponse(response) {
|
|
3
12
|
const data = response.data;
|
|
@@ -10,6 +19,10 @@ export function parseMemoryWriteResponse(response) {
|
|
|
10
19
|
const warnings = Array.isArray(payload.warnings)
|
|
11
20
|
? payload.warnings.filter((warning) => typeof warning === "string" && warning.length > 0)
|
|
12
21
|
: [];
|
|
13
|
-
return {
|
|
22
|
+
return {
|
|
23
|
+
memoryIds, warnings,
|
|
24
|
+
...(typeof payload.vector_saved === "boolean" ? { vectorSaved: payload.vector_saved } : {}),
|
|
25
|
+
...(typeof payload.enrichment_status === "string" ? { enrichmentStatus: payload.enrichment_status } : {}),
|
|
26
|
+
};
|
|
14
27
|
}
|
|
15
28
|
//# sourceMappingURL=memory-write-response.js.map
|
package/dist/models.js
CHANGED
|
@@ -76,6 +76,9 @@ export function toFull(memory, cubeId = "", userId = "") {
|
|
|
76
76
|
const cleanContent = content.replace(/^\[[A-Z_]+\]\s*/, "");
|
|
77
77
|
return {
|
|
78
78
|
...brief,
|
|
79
|
+
createdAt: memory.created_at ??
|
|
80
|
+
meta.created_at ??
|
|
81
|
+
brief.createdAt,
|
|
79
82
|
content: cleanContent,
|
|
80
83
|
background: memory.background ?? meta.background,
|
|
81
84
|
cubeId,
|
package/dist/server.js
CHANGED
|
@@ -7,10 +7,11 @@ import { readFileSync } from "node:fs";
|
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { dirname, join } from "node:path";
|
|
9
9
|
import { MEMOS_DEFAULT_CUBE, MEMOS_ENABLE_DELETE, MEMOS_PROVIDER, logger } from "./config.js";
|
|
10
|
-
import { waitForApiReady } from "./api-client.js";
|
|
10
|
+
import { apiHealthUrl, apiUrlForDisplay, isApiTimeoutError, isApiUnreachableError, waitForApiReady, } from "./api-client.js";
|
|
11
11
|
import { ensureCubeRegistered } from "./cube-manager.js";
|
|
12
12
|
import { toolSchemas, toolAnnotations } from "./tools-registry.js";
|
|
13
|
-
import { dispatchTool, handleApiUnreachable } from "./handlers/index.js";
|
|
13
|
+
import { dispatchTool, handleApiTimeout, handleApiUnreachable } from "./handlers/index.js";
|
|
14
|
+
import { UnconfirmedMemoryWriteError } from "./memory-write-response.js";
|
|
14
15
|
import { createOnceFireAndForget } from "./server-lifecycle.js";
|
|
15
16
|
import { NormalizingStdioTransport } from "./stdio-compat-transport.js";
|
|
16
17
|
// ============================================================================
|
|
@@ -41,8 +42,14 @@ function registerTools(server) {
|
|
|
41
42
|
};
|
|
42
43
|
}
|
|
43
44
|
catch (err) {
|
|
45
|
+
if (err instanceof UnconfirmedMemoryWriteError) {
|
|
46
|
+
return { content: err.content, isError: true };
|
|
47
|
+
}
|
|
44
48
|
const errStr = String(err);
|
|
45
|
-
if (
|
|
49
|
+
if (isApiTimeoutError(err)) {
|
|
50
|
+
return { content: await handleApiTimeout(), isError: true };
|
|
51
|
+
}
|
|
52
|
+
if (isApiUnreachableError(err)) {
|
|
46
53
|
const unreachable = await handleApiUnreachable();
|
|
47
54
|
return {
|
|
48
55
|
content: unreachable.map((r) => ({ type: "text", text: r.text })),
|
|
@@ -58,7 +65,7 @@ function registerTools(server) {
|
|
|
58
65
|
"",
|
|
59
66
|
"💡 Suggestions:",
|
|
60
67
|
"- Check MCP server logs for details",
|
|
61
|
-
|
|
68
|
+
`- Verify MemOS API is healthy: \`curl ${apiUrlForDisplay(apiHealthUrl(undefined, true))}\``,
|
|
62
69
|
].join("\n"),
|
|
63
70
|
},
|
|
64
71
|
],
|