#!/usr/bin/env node /** * T18 调试期局域网模型服务器(零依赖,Node 18+)。 * * 用法:node scripts/serve-models.mjs [端口=8080] * 手机与电脑连同一 Wi-Fi,App 内模型地址填 http://<电脑局域网IP>:<端口> * * 服务内容:models/ 目录下的 manifest.json 与模型文件。 * 调试阶段一次性整包下载即可;断点续传(Range)留待商用阶段接 CDN 时再做。 */ import { createServer } from 'node:http'; import { createReadStream, existsSync, statSync } from 'node:fs'; import { networkInterfaces } from 'node:os'; import { extname, join, normalize } from 'node:path'; import { fileURLToPath } from 'node:url'; const root = normalize(join(fileURLToPath(import.meta.url), '..', '..', 'models')); const port = Number(process.argv[2] ?? 8080); const MIME = { '.json': 'application/json; charset=utf-8', '.txt': 'text/plain; charset=utf-8', '.mnn': 'application/octet-stream', '.weight': 'application/octet-stream', }; const server = createServer((req, res) => { const urlPath = decodeURIComponent((req.url ?? '/').split('?')[0]); // 防目录穿越:规范化后必须仍落在 root 内 const filePath = normalize(join(root, urlPath)); if (!filePath.startsWith(root) || !existsSync(filePath) || !statSync(filePath).isFile()) { res.writeHead(404).end('not found'); return; } res.writeHead(200, { 'Content-Type': MIME[extname(filePath)] ?? 'application/octet-stream', 'Content-Length': statSync(filePath).size, }); createReadStream(filePath).pipe(res); }); server.listen(port, '0.0.0.0', () => { console.log(`serving ${root}`); for (const [name, addrs] of Object.entries(networkInterfaces())) { for (const a of addrs ?? []) { if (a.family === 'IPv4' && !a.internal) { console.log(` http://${a.address}:${port}/ (${name})`); } } } });