This commit is contained in:
owenchen committed 2026-10-29 16:44:09 +08:00
1 parent e9af73007e
commit 2909e9b0af
28 files changed
+17789

No files matched your search

+239
View File
@@ -0,0 +1,239 @@
#!/usr/bin/env node
/**
* Nox(夜神模拟器)运行/测试助手
*
* 背景:Nox 自带 adb(协议36) 与 HBuilderX 自带 adb(协议41) 会互相 kill server 造成设备掉线,
* 因此本脚本统一使用 HBuilderX 的 adb,并通过 TCP `connect 127.0.0.1:62001` 挂载 Nox。
*
* 两种在 Nox 上测试本项目的方式:
* 1) test :构建/复用 H5 产物 -> 局域网静态服务 -> adb 唤起 Nox 浏览器打开(零打包,当前即可用)
* 2) native:调用 HBuilderX CLI `launch app-android` 运行到安卓基座(需先在 HBuilderX 里以 uni-app 方式导入本项目)
*
* 用法:
* node scripts/nox.mjs connect
* node scripts/nox.mjs serve [--build] [--port 5199]
* node scripts/nox.mjs open [--url http://ip:port]
* node scripts/nox.mjs test [--build] [--port 5199] [--shot]
* node scripts/nox.mjs screenshot
* node scripts/nox.mjs native
*/
import { spawn, spawnSync } from "node:child_process";
import http from "node:http";
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, "..");
// ---- 可配置项(均可用环境变量覆盖)----
const HBX_HOME = process.env.HBX_HOME || "C:\\apps\\HBuilderX";
const ADB =
process.env.NOX_ADB ||
path.join(HBX_HOME, "plugins", "launcher-tools", "tools", "adbs", "adb.exe");
const CLI = process.env.HBX_CLI || path.join(HBX_HOME, "cli.exe");
const NOX_SERIAL = process.env.NOX_SERIAL || "127.0.0.1:62001";
const PROJECT_NAME = process.env.NOX_PROJECT || "security";
const H5_DIST = path.join(ROOT, "dist", "build", "h5");
const args = process.argv.slice(2);
const cmd = args[0] || "test";
const getFlag = (name, def) => {
const i = args.indexOf(`--${name}`);
if (i === -1) return def;
const next = args[i + 1];
return next && !next.startsWith("--") ? next : true;
};
const hasFlag = (name) => args.includes(`--${name}`);
function lanIp() {
const ifaces = os.networkInterfaces();
for (const name of Object.keys(ifaces)) {
for (const info of ifaces[name] || []) {
if (info.family === "IPv4" && !info.internal) return info.address;
}
}
return "127.0.0.1";
}
function adb(...adbArgs) {
return spawnSync(ADB, adbArgs, { encoding: "utf8" });
}
function connect() {
const r = adb("connect", NOX_SERIAL);
console.log((r.stdout || "") + (r.stderr || ""));
const list = adb("devices");
console.log(list.stdout || "");
if (!(list.stdout || "").includes(NOX_SERIAL)) {
console.error(`[nox] 未发现设备 ${NOX_SERIAL},请确认夜神模拟器已启动。`);
process.exit(1);
}
return true;
}
const MIME = {
".html": "text/html; charset=utf-8",
".js": "text/javascript; charset=utf-8",
".mjs": "text/javascript; charset=utf-8",
".css": "text/css; charset=utf-8",
".json": "application/json; charset=utf-8",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".svg": "image/svg+xml",
".ico": "image/x-icon",
".webp": "image/webp",
".woff": "font/woff",
".woff2": "font/woff2",
".ttf": "font/ttf",
".map": "application/json; charset=utf-8",
};
function serve(port) {
if (!fs.existsSync(path.join(H5_DIST, "index.html"))) {
console.error(
`[nox] 未找到 H5 产物:${H5_DIST},请先执行 build:h5 或加 --build。`,
);
process.exit(1);
}
const server = http.createServer((req, res) => {
const urlPath = decodeURIComponent((req.url || "/").split("?")[0]);
let filePath = path.join(H5_DIST, urlPath === "/" ? "index.html" : urlPath);
if (!filePath.startsWith(H5_DIST)) {
res.writeHead(403);
return res.end("forbidden");
}
if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
filePath = path.join(filePath, "index.html");
}
if (!fs.existsSync(filePath)) {
// SPA 兜底:未知路径回 index.html
filePath = path.join(H5_DIST, "index.html");
}
res.writeHead(200, {
"Content-Type":
MIME[path.extname(filePath).toLowerCase()] ||
"application/octet-stream",
});
fs.createReadStream(filePath).pipe(res);
});
server.listen(port, "0.0.0.0", () => {
console.log(
`[nox] 静态服务已启动:http://${lanIp()}:${port} (Ctrl+C 停止)`,
);
});
return server;
}
function openBrowser(url) {
const r = adb(
"-s",
NOX_SERIAL,
"shell",
"am",
"start",
"-a",
"android.intent.action.VIEW",
"-d",
url,
);
console.log((r.stdout || "") + (r.stderr || ""));
}
function screenshot() {
const out = path.join(ROOT, "unpackage", "nox-screenshot.png");
fs.mkdirSync(path.dirname(out), { recursive: true });
const r = spawnSync(ADB, ["-s", NOX_SERIAL, "exec-out", "screencap", "-p"], {
encoding: "buffer",
maxBuffer: 64 * 1024 * 1024,
});
if (r.error || !r.stdout || r.stdout.length === 0) {
console.error(
"[nox] 截图失败:设备可能离线,先执行 node scripts/nox.mjs connect 后重试",
);
return;
}
fs.writeFileSync(out, r.stdout);
console.log(`[nox] 截图已保存:${out}(${r.stdout.length} 字节)`);
}
function run(cmdName, cmdArgs) {
const r = spawnSync(cmdName, cmdArgs, { encoding: "utf8", shell: false });
console.log((r.stdout || "") + (r.stderr || ""));
return r;
}
function buildH5() {
console.log("[nox] 构建 H5 产物 ...");
const npmCmd = process.platform === "win32" ? "npm" : "npm";
const r = spawnSync(npmCmd, ["run", "build:h5"], {
cwd: ROOT,
stdio: "inherit",
shell: true,
});
if (r.status !== 0) {
console.error("[nox] 构建失败");
process.exit(1);
}
}
function native() {
console.log(
"[nox] 通过 HBuilderX CLI 运行到安卓基座(需项目已在 HBuilderX 中识别为 uni-app)...",
);
run(CLI, [
"launch",
"app-android",
"--project",
PROJECT_NAME,
"--deviceId",
NOX_SERIAL,
"--playground",
"standard",
]);
}
const port = Number(getFlag("port", 5199));
switch (cmd) {
case "connect":
connect();
break;
case "serve":
if (hasFlag("build")) buildH5();
connect();
serve(port);
break;
case "open":
connect();
openBrowser(getFlag("url", `http://${lanIp()}:${port}`));
break;
case "screenshot":
connect();
screenshot();
break;
case "native":
connect();
native();
break;
case "test":
default: {
if (hasFlag("build")) buildH5();
connect();
const server = serve(port);
const url = `http://${lanIp()}:${port}`;
setTimeout(() => {
openBrowser(url);
console.log(`[nox] 已在 Nox 浏览器打开:${url}`);
if (hasFlag("shot")) setTimeout(screenshot, 4000);
}, 800);
process.on("SIGINT", () => {
server.close();
process.exit(0);
});
break;
}
}