#!/usr/bin/env node /** * 导入默认模型:把 MMD 模型压缩包(zip 内含 .pmx)交给服务端转换并入库, * 转换成功后把它设为全局默认模型,并把身份(名称/格式/产物 sha256) * 写进源码 `src/config/default-model.ts`。 * * 为什么要有这个脚本:服务进程在 systemd 加固(ProtectSystem=strict)下**不能** * 改写程序目录,写源码这一步必须在沙箱外完成,因此由本脚本(以 root/运维身份运行) * 调 API 之后自己写文件。 * * 用法(在 web-server 目录下,需先 `npm run build`): * node scripts/import-default-model.mjs 模型.zip \ * --url http://127.0.0.1:3000 \ * --name "初音未来 Hatsune Miku" \ * --author "Boku Wa Luka" \ * --license "MMD 模型使用条款(见压缩包内说明)" \ * --source-name "模型_初音未来Hatsune Miku_by_Boku_Wa_Luka.zip" \ * --source-url "本地文件:模型_初音未来Hatsune Miku_by_Boku_Wa_Luka.zip" \ * --source-type other * * # 只根据已入库的模型补写源码(库里的默认模型已知时) * node scripts/import-default-model.mjs --write-source-only --model-id 16 * * # 只写源码文件、不调 API(配合 --write-source-only) * node scripts/import-default-model.mjs --write-source-only --model-id 16 --dry-run */ import { createRequire } from 'node:module'; import { readFileSync, writeFileSync, existsSync } from 'node:fs'; import { basename, join, resolve } from 'node:path'; const require = createRequire(import.meta.url); const args = process.argv.slice(2); const flag = (name, fallback = undefined) => { const index = args.indexOf(name); return index >= 0 && args[index + 1] && !args[index + 1].startsWith('--') ? args[index + 1] : index >= 0 ? true : fallback; }; const has = (name) => args.includes(name); const positional = args.find((a) => !a.startsWith('--') && args[args.indexOf(a) - 1]?.startsWith('--') === false); const file = positional; const url = String(flag('--url', 'http://127.0.0.1:3000')).replace(/\/$/, ''); const writeSourceOnly = has('--write-source-only'); const dryRun = has('--dry-run'); const sourceDir = resolve(process.cwd(), 'src', 'config'); const sourceFile = join(sourceDir, 'default-model.ts'); function loadRenderer() { const distPath = join(process.cwd(), 'dist', 'models', 'default-model-source.js'); if (!existsSync(distPath)) { console.error('未找到 dist/models/default-model-source.js,请先执行:npm run build'); process.exit(2); } return require(distPath); } async function api(path, options = {}) { const response = await fetch(`${url}${path}`, options); const text = await response.text(); let body = null; try { body = text ? JSON.parse(text) : null; } catch { body = text; } if (!response.ok) { const detail = body && typeof body === 'object' ? JSON.stringify(body) : String(body); throw new Error(`${options.method ?? 'GET'} ${path} → HTTP ${response.status}:${detail}`); } return body; } async function fetchModel(id) { const model = await api(`/api/models/${id}`); return model; } async function main() { const { renderDefaultModelSource, defaultModelSourcePath } = loadRenderer(); const target = defaultModelSourcePath(process.cwd()); let model; if (writeSourceOnly) { const id = Number(flag('--model-id', 0)); if (!Number.isInteger(id) || id <= 0) { console.error('--write-source-only 需要 --model-id <已入库的模型 id>'); process.exit(2); } model = await fetchModel(id); console.log(`读取模型 #${model.id}:${model.name}(${model.format})`); } else { if (!file) { console.error( '用法:node scripts/import-default-model.mjs <模型.zip> [--url ...] [--name ...] [--author ...] [--license ...]', ); process.exit(2); } const buffer = readFileSync(resolve(file)); const form = new FormData(); form.append('file', new Blob([buffer]), basename(file)); form.append('name', String(flag('--name', basename(file).replace(/\.[^.]+$/, '')))); form.append('sourceType', String(flag('--source-type', 'other'))); form.append('sourceName', String(flag('--source-name', basename(file)))); form.append('sourceUrl', String(flag('--source-url', `本地文件:${basename(file)}`))); form.append('license', String(flag('--license', '待补充:MMD 模型使用条款(见压缩包内说明)'))); form.append('author', String(flag('--author', '待补充'))); form.append('autoDefault', 'true'); console.log(`上传并转换:${basename(file)}(${(buffer.length / 1024 / 1024).toFixed(2)} MiB)→ ${url}`); model = await api('/api/models', { method: 'POST', body: form }); console.log(`入库成功:#${model.id} ${model.name}(格式 ${model.format})`); console.log(`转换说明:${model.conversionNote ?? '(无)'}`); } if (!model.fileSha256) { console.error('该模型没有 fileSha256(文件缺失?),无法写入默认模型种子'); process.exit(1); } const seed = { id: model.id, name: model.name, format: model.format, sha256: model.fileSha256, convertedFrom: (model.conversionNote ?? '').split(',')[0] || null, updatedAt: new Date().toISOString(), }; const content = renderDefaultModelSource(seed); if (dryRun) { console.log(`--dry-run:将写入 ${target}\n`); console.log(content); return; } writeFileSync(target, content, 'utf8'); console.log(`已把默认模型写进源码:${target}`); console.log('建议接着执行:npm run typecheck && git add src/config/default-model.ts'); } main().catch((error) => { console.error(`失败:${error instanceof Error ? error.message : String(error)}`); process.exit(1); });