@cup319/mmpl 3.3.4 → 3.3.6

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  MMPL (Memory Palace) — DuckDB 向量记忆 MCP Server(单二进制,Rust 实现)。
4
4
 
5
- 当前版本 **3.3.4**:版本链(`memory_manage action=history`)、长记忆切块嵌入、管道幂等去重、LongMemEval 检索评测。
5
+ 当前版本 **3.3.6**:版本链(`memory_manage action=history`)、长记忆切块嵌入、管道幂等去重、LongMemEval 检索评测。
6
6
 
7
7
  ```bash
8
8
  npm install -g @cup319/mmpl
package/install.js CHANGED
@@ -12,7 +12,7 @@ const { get } = require("https");
12
12
  const path = require("path");
13
13
  const { execSync } = require("child_process");
14
14
 
15
- const VERSION = "3.3.4";
15
+ const VERSION = "3.3.6";
16
16
  const IS_WIN = process.platform === "win32";
17
17
  const PLATFORM = IS_WIN ? "windows" : "linux";
18
18
  const ARCH = "x86_64";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cup319/mmpl",
3
- "version": "3.3.4",
3
+ "version": "3.3.6",
4
4
  "description": "MMPL (Memory Palace) — 单二进制 DuckDB 记忆 MCP Server (9 工具 + ONNX 语义向量 + 冲突双通道门控 + 场景构建 + 四层钻取) — Linux + Windows 双平台",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -8,14 +8,16 @@
8
8
  "registry": "https://registry.npmjs.org/"
9
9
  },
10
10
  "bin": {
11
- "mmpl": "run.js"
11
+ "mmpl": "run.js",
12
+ "mmpl-setup": "setup.js"
12
13
  },
13
14
  "files": [
14
15
  "install.js",
15
16
  "run.js",
17
+ "setup.js",
16
18
  "README.md",
17
- "mmpl-v3.3.4-linux-x86_64.tar.gz",
18
- "mmpl-v3.3.4-windows-x86_64.zip"
19
+ "mmpl-v3.3.6-linux-x86_64.tar.gz",
20
+ "mmpl-v3.3.6-windows-x86_64.zip"
19
21
  ],
20
22
  "scripts": {
21
23
  "postinstall": "node install.js"
package/run.js CHANGED
@@ -38,6 +38,23 @@ if (process.platform === "win32" && !process.env.PORT) {
38
38
  probe.on("timeout", () => probe.destroy());
39
39
  }
40
40
 
41
+ // CNB 云开发环境: 自动绑 0.0.0.0(CNB 端口代理拨容器 IP,127.0.0.1 会 connection refused)
42
+ // 同时提醒安全: 0.0.0.0 + 无 token = 任何拿到 URL 的人都能访问管理后台
43
+ const isCNB = !!process.env.CNB_REPO_NAME;
44
+ if (isCNB && !process.env.HOST) {
45
+ process.env.HOST = "0.0.0.0";
46
+ console.log("[mmpl] 检测到 CNB 云开发环境 → HOST=0.0.0.0(供端口代理访问)");
47
+ if (!process.env.MMPL_AUTH_TOKEN) {
48
+ const tok = require("crypto").randomBytes(16).toString("hex");
49
+ console.log("");
50
+ console.log("[mmpl] ⚠ 安全提示: 已监听 0.0.0.0,建议设置访问令牌。本次已自动生成:");
51
+ console.log(`[mmpl] MMPL_AUTH_TOKEN=${tok}`);
52
+ console.log(`[mmpl] 管理后台: https://<你的CNB端口域名>/admin?token=${tok}`);
53
+ console.log("[mmpl] (将该 token 写入环境变量重启后固定; 不设置则后台无认证暴露)");
54
+ console.log("");
55
+ }
56
+ }
57
+
41
58
  const child = spawn(BIN, process.argv.slice(2), {
42
59
  stdio: "inherit",
43
60
  env: process.env,
package/setup.js ADDED
@@ -0,0 +1,244 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * mmpl-setup — 语义向量模型预下载器(多线程分块 + 进度条 + 镜像回退)
4
+ *
5
+ * 用法:
6
+ * mmpl-setup 交互式(或 --yes 直接确认)
7
+ * mmpl-setup --model small 指定档位 small|base|m3|qwen3(默认 base)
8
+ * mmpl-setup --dir D:\mmpl 指定 DATA_DIR(默认读环境变量,再退 ./data)
9
+ *
10
+ * 下载内容: DATA_DIR/models/<model>.onnx + tokenizer.json
11
+ * 加速: HTTP Range 分块(hf-mirror 支持,302 到 CDN),8 并发;
12
+ * 镜像顺序 hf-mirror.com → huggingface.co(直连可用时)
13
+ */
14
+ const fs = require("fs");
15
+ const path = require("path");
16
+ const https = require("https");
17
+ const { execSync } = require("child_process");
18
+
19
+ // ---------- 参数 ----------
20
+ const argv = process.argv.slice(2);
21
+ const yes = argv.includes("--yes") || argv.includes("-y");
22
+ const modelKey = (argv.find((a, i) => argv[i - 1] === "--model") || "base").toLowerCase();
23
+ const dirArg = argv.find((a, i) => argv[i - 1] === "--dir");
24
+
25
+ const MODELS = {
26
+ small: { dim: 512, repo: "Xenova/bge-small-zh-v1.5", file: "bge-small-zh-v1.5-q.onnx", minSize: 25 * 1024 * 1024 },
27
+ base: { dim: 768, repo: "Xenova/bge-base-zh-v1.5", file: "bge-base-zh-v1.5-q.onnx", minSize: 90 * 1024 * 1024 },
28
+ m3: { dim: 1024, repo: "Xenova/bge-m3", file: "bge-m3-q.onnx", minSize: 200 * 1024 * 1024 },
29
+ qwen3: { dim: 1024, repo: "onnx-community/Qwen3-Embedding-0.6B-ONNX", file: "qwen3-embedding-0.6b-q.onnx", minSize: 300 * 1024 * 1024 },
30
+ };
31
+ const M = MODELS[modelKey];
32
+ if (!M) { console.error(`[mmpl-setup] 未知档位: ${modelKey}(可选 small|base|m3|qwen3)`); process.exit(1); }
33
+
34
+ const DATA_DIR = dirArg || process.env.DATA_DIR || path.join(process.cwd(), "data");
35
+ const MODELS_DIR = path.join(DATA_DIR, "models");
36
+ // 下载源顺序: HF_ENDPOINT 自定义 > 测速选快 > 默认国内镜像优先
37
+ const ALL_MIRRORS = ["https://hf-mirror.com", "https://huggingface.co"];
38
+ const CUSTOM = (process.env.HF_ENDPOINT || "").trim().replace(/\/+$/, "");
39
+ const MIRRORS = CUSTOM ? [CUSTOM, ...ALL_MIRRORS] : ALL_MIRRORS;
40
+ const CHUNK_WORKERS = 8;
41
+ let ranked = []; // 测速后的有序镜像(main() 里填充)
42
+
43
+ if (process.platform === "win32") {
44
+ try { execSync("chcp 65001", { stdio: "ignore", windowsHide: true }); } catch (_) {}
45
+ }
46
+
47
+ // ---------- 工具 ----------
48
+ const mb = (n) => (n / 1024 / 1024).toFixed(1);
49
+ function get(url, redirects = 5) {
50
+ return new Promise((resolve, reject) => {
51
+ https.get(url, { headers: { "User-Agent": "mmpl-setup" } }, (res) => {
52
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location && redirects > 0) {
53
+ res.resume();
54
+ const next = new URL(res.headers.location, url).toString();
55
+ return resolve(get(next, redirects - 1));
56
+ }
57
+ if (res.statusCode !== 200 && res.statusCode !== 206) {
58
+ res.resume();
59
+ return reject(new Error(`HTTP ${res.statusCode} @ ${url.slice(0, 80)}`));
60
+ }
61
+ resolve(res);
62
+ }).on("error", reject);
63
+ });
64
+ }
65
+ async function headSize(url) {
66
+ return new Promise((resolve, reject) => {
67
+ https.request(url, { method: "HEAD", headers: { "User-Agent": "mmpl-setup" } }, (res) => {
68
+ res.resume();
69
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
70
+ return resolve(headSize(new URL(res.headers.location, url).toString()));
71
+ }
72
+ const len = Number(res.headers["content-length"] || 0);
73
+ len > 0 ? resolve(len) : reject(new Error("no content-length"));
74
+ }).on("error", reject).end();
75
+ });
76
+ }
77
+ function fmtProgress(done, total, speed) {
78
+ const pct = total ? Math.floor((done / total) * 100) : 0;
79
+ const bar = "#".repeat(Math.floor(pct / 3)).padEnd(33, ".");
80
+ return `${bar} ${pct}% ${mb(done)}/${mb(total)}MB ${speed || ""}`;
81
+ }
82
+ /// 跟随 302 拿最终 CDN URL(签名 URL 才能带 Range 复用)
83
+ function resolveUrl(url, redirects = 5) {
84
+ return new Promise((resolve, reject) => {
85
+ https.request(url, { method: "GET", headers: { "User-Agent": "mmpl-setup", Range: "bytes=0-0" } }, (res) => {
86
+ res.resume();
87
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location && redirects > 0) {
88
+ return resolve(resolveUrl(new URL(res.headers.location, url).toString(), redirects - 1));
89
+ }
90
+ resolve(url); // 200/206 → 已是最终地址
91
+ }).on("error", reject).end();
92
+ });
93
+ }
94
+
95
+ // ---------- 分块并行下载 ----------
96
+ async function downloadFile(url, dest, label) {
97
+ const total = await headSize(url);
98
+ // 小文件或服务器不支持 Range 时直接单线程
99
+ const supportsRange = true; // hf-mirror 与 hf CDN 均支持;失败自动走单线程回退
100
+ if (!supportsRange || total < 4 * 1024 * 1024) return downloadSingle(url, dest, label);
101
+
102
+ const partSize = Math.ceil(total / CHUNK_WORKERS);
103
+ const parts = [];
104
+ for (let i = 0; i < CHUNK_WORKERS; i++) {
105
+ const start = i * partSize;
106
+ if (start >= total) break;
107
+ const end = Math.min(start + partSize - 1, total - 1);
108
+ parts.push({ i, start, end, path: `${dest}.part${i}` });
109
+ }
110
+ let done = parts.filter((p) => fs.existsSync(p.path)).reduce((s, p) => s + (fs.statSync(p.path).size), 0);
111
+ const t0 = Date.now();
112
+ const spinner = setInterval(() => {
113
+ done = parts.reduce((s, p) => s + (fs.existsSync(p.path) ? fs.statSync(p.path).size : 0), 0);
114
+ const secs = (Date.now() - t0) / 1000;
115
+ process.stdout.write(`\r ${label} ${fmtProgress(done, total, `${mb(done / secs)}MB/s`)}`.padEnd(100));
116
+ }, 300);
117
+
118
+ await Promise.all(parts.map((p) =>
119
+ (async () => {
120
+ const have = fs.existsSync(p.path) ? fs.statSync(p.path).size : 0;
121
+ const want = p.end - p.start + 1;
122
+ if (have === want) return; // 断点续传
123
+ const finalUrl = await resolveUrl(url); // 每分块解析一次最终 CDN URL(签名时效不同)
124
+ for (let attempt = 0; attempt < 3; attempt++) {
125
+ try {
126
+ // 带 Range 的原生请求
127
+ return await new Promise((resolve, reject) => {
128
+ const req = https.request(new URL(finalUrl), { headers: { "User-Agent": "mmpl-setup", Range: `bytes=${p.start + have}-${p.end}` } }, (res) => {
129
+ if (res.statusCode !== 206) { res.resume(); return reject(new Error(`part ${p.i}: HTTP ${res.statusCode}(镜像不支持断点)`)); }
130
+ const out = fs.createWriteStream(p.path, { flags: have ? "a" : "w" });
131
+ res.pipe(out);
132
+ out.on("finish", resolve).on("error", reject);
133
+ });
134
+ req.on("error", reject).end();
135
+ });
136
+ } catch (e) { if (attempt === 2) throw e; await new Promise((r) => setTimeout(r, 1000 * (attempt + 1))); }
137
+ }
138
+ })()
139
+ ));
140
+ clearInterval(spinner);
141
+ process.stdout.write("\n");
142
+ // 合并分块
143
+ const out = fs.createWriteStream(dest);
144
+ for (const p of parts) {
145
+ await new Promise((resolve, reject) => {
146
+ fs.createReadStream(p.path).on("data", (c) => out.write(c)).on("end", resolve).on("error", reject);
147
+ });
148
+ fs.unlinkSync(p.path);
149
+ }
150
+ await new Promise((r) => out.end(r));
151
+ if (fs.statSync(dest).size !== total) throw new Error(`大小不符: ${fs.statSync(dest).size} != ${total}`);
152
+ }
153
+
154
+ async function downloadSingle(url, dest, label) {
155
+ const res = await get(url);
156
+ const total = Number(res.headers["content-length"] || 0);
157
+ let done = 0; const t0 = Date.now();
158
+ const spinner = setInterval(() => {
159
+ const secs = (Date.now() - t0) / 1000;
160
+ process.stdout.write(`\r ${label} ${fmtProgress(done, total, `${mb(done / secs)}MB/s`)}`.padEnd(100));
161
+ }, 300);
162
+ await new Promise((resolve, reject) => {
163
+ const out = fs.createWriteStream(dest);
164
+ res.on("data", (c) => { done += c.length; out.write(c); });
165
+ res.on("end", () => out.end(resolve)).on("error", reject);
166
+ out.on("error", reject);
167
+ });
168
+ clearInterval(spinner);
169
+ process.stdout.write("\n");
170
+ }
171
+
172
+ /// 并行测速(拉 1MB 样本),返回按速度排序的镜像列表(国内外自动择优)
173
+ async function rankMirrors() {
174
+ const results = await Promise.all(MIRRORS.map(async (host) => {
175
+ const t0 = Date.now();
176
+ try {
177
+ // 用各仓库都存在的小文件测:tokenizer.json(~100-700KB)
178
+ const res = await get(`${host}/${M.repo}/resolve/main/tokenizer.json`);
179
+ const chunks = [];
180
+ for await (const c of res) chunks.push(c);
181
+ const bytes = chunks.reduce((s, c) => s + c.length, 0);
182
+ const secs = (Date.now() - t0) / 1000;
183
+ return { host, ok: true, speed: bytes / 1024 / 1024 / secs };
184
+ } catch { return { host, ok: false, speed: 0 }; }
185
+ }));
186
+ const ok = results.filter((r) => r.ok).sort((a, b) => b.speed - a.speed);
187
+ if (ok.length > 1) {
188
+ console.log(" 测速:");
189
+ for (const r of ok) console.log(` ${r.host} ${r.speed.toFixed(2)} MB/s`);
190
+ }
191
+ return ok.map((r) => r.host);
192
+ }
193
+
194
+ // ---------- 主流程 ----------
195
+ async function tryMirrors(buildUrl, dest, label) {
196
+ let lastErr;
197
+ for (const mirror of ranked) {
198
+ const url = buildUrl(mirror);
199
+ try {
200
+ await downloadFile(url, dest, label);
201
+ return mirror;
202
+ } catch (e) { lastErr = e; console.error(`\n [镜像失败] ${mirror}: ${e.message}`); }
203
+ }
204
+ throw lastErr;
205
+ }
206
+
207
+ async function main() {
208
+ const modelPath = path.join(MODELS_DIR, M.file);
209
+ const tokPath = path.join(MODELS_DIR, "tokenizer.json");
210
+
211
+ if (fs.existsSync(modelPath) && fs.statSync(modelPath).size > M.minSize / 2 && fs.existsSync(tokPath)) {
212
+ console.log(`[mmpl-setup] 模型已就绪: ${modelPath} (${mb(fs.statSync(modelPath).size)}MB)`);
213
+ console.log("[mmpl-setup] 无需下载,退出。");
214
+ return;
215
+ }
216
+
217
+ console.log("[mmpl-setup] MMPL 语义向量模型预下载");
218
+ console.log(` 档位: ${modelKey} (${M.dim} 维)`);
219
+ console.log(` 目标: ${modelPath}`);
220
+ console.log(` 体积: ~${Math.round(M.minSize / 1024 / 1024)}MB+,${CHUNK_WORKERS} 线程分块下载`);
221
+ console.log(` 源: ${CUSTOM ? `自定义 ${CUSTOM} + 回退` : "国内 hf-mirror.com / 海外 huggingface.co 自动择优"}`);
222
+ console.log(" 跳过则首次启动时由 mmpl 单线程下载(较慢)");
223
+ console.log("");
224
+ if (!yes && process.stdin.isTTY) {
225
+ process.stdout.write("现在下载? [Y/n] ");
226
+ const answer = await new Promise((r) => process.stdin.once("data", (d) => r(d.toString().trim().toLowerCase())));
227
+ if (answer === "n" || answer === "no") { console.log("已跳过。需要时运行: mmpl-setup --yes"); return; }
228
+ }
229
+
230
+ fs.mkdirSync(MODELS_DIR, { recursive: true });
231
+ ranked = await rankMirrors();
232
+ if (!ranked.length) { console.error("[mmpl-setup] 所有镜像均不可达,请检查网络或设置 HF_ENDPOINT"); process.exit(1); }
233
+ console.log(` 使用源: ${ranked[0]}${ranked.length > 1 ? `(回退: ${ranked.slice(1).join(", ")})` : ""}`);
234
+ const t0 = Date.now();
235
+ const m = await tryMirrors((host) => `${host}/${M.repo}/resolve/main/onnx/model_quantized.onnx`, modelPath, "模型");
236
+ console.log(` 模型完成(${m},${mb(fs.statSync(modelPath).size)}MB,${((Date.now() - t0) / 1000).toFixed(1)}s)`);
237
+ await tryMirrors((host) => `${host}/${M.repo}/resolve/main/tokenizer.json`, tokPath, "分词器");
238
+ console.log(` 分词器完成(${mb(fs.statSync(tokPath).size)}MB)`);
239
+ console.log("");
240
+ console.log("[mmpl-setup] 完成!mmpl 启动将直接使用本地模型。");
241
+ console.log(`[mmpl-setup] 提示: 环境变量 MMPL_MODEL=${modelKey}(默认 base)`);
242
+ }
243
+
244
+ main().catch((e) => { console.error(`[mmpl-setup] 下载失败: ${e.message}`); console.error("[mmpl-setup] 可稍后重试(支持断点续传),或检查网络代理设置"); process.exit(1); });