/** * 假引擎:模拟「长时间运行 + 能被叫停 + 会跳心跳」,**完全不碰 TikTok**。 * * 用途:验证界面侧的运行态交互(启动握手、状态条、进度、两段停止、心跳判活)。 * 用真引擎验证这些时,它会把 TikTok 拉到前台,我就没法同时操作界面上的按钮了 —— * 而这个假引擎不切应用,界面始终留在前台,正好把 UI 交互测干净。 * * 协议与真引擎完全一致:读 --config/--state/--stop-flag/--run-id, * 写 run-state.json(带 v / engine.capabilities)与 engine-beat.json。 * * ★ 心跳不能省:界面在 v2 档位下是用心跳判活的。这个假引擎要是不跳, * 跑起来 15 秒就会被界面判成"引擎已退出" —— 最常用的联调手段反而成了误报源。 */ function argOf(argv, key) { for (var i = 0; argv && argv[i] !== undefined; i += 1) { if (String(argv[i]) === key) { return String(argv[i + 1]); } } return ""; } var argv = engines.myEngine().execArgv; var STATE = argOf(argv, "--state"); var STOP = argOf(argv, "--stop-flag"); var RUN_ID = argOf(argv, "--run-id"); /* 心跳文件跟 run-state 放同一目录(真引擎也是这么约定的), * 这样不必额外传一个 --beat 参数,命令行协议保持 5 个参数不变。 */ var slash = STATE.lastIndexOf("/"); var BEAT = (slash >= 0 ? STATE.substring(0, slash) : "") + "/engine-beat.json"; var seq = 0; var seenAt = 0; function report(payload) { payload.v = 2; payload.runId = RUN_ID; payload.engine = payload.engine || { task: "fake", capabilities: { heartbeat: true } }; payload.updatedAt = Date.now(); files.write(STATE, JSON.stringify(payload)); } /* 心跳必须是**独立线程**:主流程会 sleep(300) 轮询, * 若把心跳写在同一循环里,一旦某一步卡住心跳就跟着停 —— 那就失去意义了。 * (真引擎侧也是同样的理由,见 tiktok-auto/src/bridge.js 的 heartbeatStart) */ var beating = true; threads.start(function () { while (beating) { seq += 1; try { files.write(BEAT, JSON.stringify({ v: 2, runId: RUN_ID, seq: seq, beatAt: Date.now(), stopSeenAt: seenAt })); } catch (e) { } sleep(2000); } }); report({ status: "starting", phase: "假引擎启动中", done: 0, total: 3 }); sleep(600); report({ status: "running", phase: "假引擎运行中", done: 0, total: 3 }); var began = Date.now(); var tick = 0; var done = 0; while (Date.now() - began < 60000) { tick += 1; /* 每 3 秒推进一格进度:用来验证状态条会跟着引擎上报刷新 */ if (tick % 10 === 0 && done < 3) { done += 1; report({ status: "running", phase: "已处理 " + done + "/3", done: done, total: 3 }); } var content = ""; try { if (files.exists(STOP)) { content = String(files.read(STOP)).replace(/\s/g, ""); } } catch (e) { } if (content !== "" && content === RUN_ID) { /* 先承认收到(sticky 的 stopSeenAt,界面靠它把"正在通知引擎"换成"正在收尾"), * 再走一段"收尾中"的过场 —— 真实引擎也是这样,不会立刻消失。 */ seenAt = Date.now(); report({ status: "stopping", phase: "正在收尾", message: "收到停止请求(假引擎)" }); sleep(1200); beating = false; report({ status: "stopped", phase: "", message: "已停止(假引擎)" }); exit(); } sleep(300); } beating = false; report({ status: "finished", phase: "", done: 3, total: 3, message: "跑完了(假引擎)" });