@cup319/mmpl 3.3.3 → 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.
|
|
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.
|
|
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";
|
|
@@ -66,6 +66,11 @@ function download(url, redirects = 5) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
async function main() {
|
|
69
|
+
// Windows: 控制台切 UTF-8 代码页,避免中文提示在默认 GBK 终端乱码
|
|
70
|
+
if (IS_WIN) {
|
|
71
|
+
try { execSync("chcp 65001", { stdio: "ignore", windowsHide: true }); } catch (_) {}
|
|
72
|
+
}
|
|
73
|
+
|
|
69
74
|
if (existsSync(BIN)) return;
|
|
70
75
|
|
|
71
76
|
if (existsSync(BUNDLED)) {
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cup319/mmpl",
|
|
3
|
-
"version": "3.3.
|
|
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.
|
|
18
|
-
"mmpl-v3.3.
|
|
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
|
@@ -1,16 +1,67 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/** bin
|
|
2
|
+
/** bin 入口:转发到下载好的二进制(双平台)*/
|
|
3
3
|
const { spawn } = require("child_process");
|
|
4
4
|
const path = require("path");
|
|
5
|
+
const fs = require("fs");
|
|
5
6
|
|
|
6
7
|
const BIN = path.join(__dirname, "bin", process.platform === "win32" ? "mmpl.exe" : "mmpl");
|
|
8
|
+
|
|
9
|
+
if (!fs.existsSync(BIN)) {
|
|
10
|
+
console.error("[mmpl] 二进制不存在 — 请运行 `npm rebuild @cup319/mmpl` 重新解包,或参考 README 手动安装");
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Windows: 控制台代码页切到 UTF-8(65001),否则中文日志在默认 GBK (936) 控制台会乱码
|
|
15
|
+
if (process.platform === "win32") {
|
|
16
|
+
try {
|
|
17
|
+
require("child_process").execSync("chcp 65001", { stdio: "ignore", windowsHide: true });
|
|
18
|
+
if (process.stdout.isTTY) process.stdout.write(""); // 触发句柄刷新
|
|
19
|
+
} catch (_) { /* chcp 失败不影响功能,仅日志可能乱码 */ }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 端口预检: 7680 被占大概率是旧 mmpl 还在跑(v3.3.3 起 Windows 为无窗口 GUI 子系统,进程不可见)
|
|
23
|
+
// 升级遇到 EBUSY 也是同一原因 — 先 taskkill 再 npm install
|
|
24
|
+
if (process.platform === "win32" && !process.env.PORT) {
|
|
25
|
+
const port = 7680;
|
|
26
|
+
const net = require("net");
|
|
27
|
+
const probe = net.connect({ port, host: "127.0.0.1", timeout: 400 });
|
|
28
|
+
probe.on("connect", () => {
|
|
29
|
+
probe.destroy();
|
|
30
|
+
console.error("");
|
|
31
|
+
console.error("[mmpl] 警告: 端口 7680 已被占用,很可能是之前启动的 mmpl 还在运行(无窗口模式看不到它)。");
|
|
32
|
+
console.error("[mmpl] 如需重启,请先执行: taskkill /F /IM mmpl.exe");
|
|
33
|
+
console.error("[mmpl] 或设置 PORT 环境变量使用其他端口。");
|
|
34
|
+
console.error("[mmpl] 升级 npm 包时遇到 EBUSY 报错,同样是旧进程锁定文件导致,先 taskkill 再安装。");
|
|
35
|
+
console.error("");
|
|
36
|
+
});
|
|
37
|
+
probe.on("error", () => { /* 端口空闲,正常 */ });
|
|
38
|
+
probe.on("timeout", () => probe.destroy());
|
|
39
|
+
}
|
|
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
|
+
|
|
7
58
|
const child = spawn(BIN, process.argv.slice(2), {
|
|
8
59
|
stdio: "inherit",
|
|
9
60
|
env: process.env,
|
|
10
61
|
windowsHide: true, // 不弹出新 cmd 窗口(配合 GUI 子系统 exe)
|
|
11
62
|
});
|
|
12
63
|
child.on("error", (e) => {
|
|
13
|
-
console.error("[mmpl]
|
|
64
|
+
console.error("[mmpl] 启动失败:", e.message);
|
|
14
65
|
process.exit(1);
|
|
15
66
|
});
|
|
16
67
|
child.on("exit", (code) => process.exit(code ?? 1));
|
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); });
|