| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257 |
- #!/usr/bin/env node
- /**
- * build.js — 构建流水线:src/ 多个小模块 → dist/ 一个可在 AutoX 里跑的单文件
- *
- * 与 tiktok-auto 的构建思路一致(Zakas ch13/15):开发时一个文件只做一件事,
- * 部署时是一个自包含文件——AutoX 拿到的是"一整段脚本",带不了一堆 require。
- *
- * 两道自检:
- * 1. 语法检查(new Function)——设备端没有 linter,PC 侧不查就是把语法错误发到手机上才发现;
- * 2. **"ui" 指令位置检查**——UI 模式要求 "ui"; 出现在脚本最前面,
- * 一旦被注释放到后面,界面会以非 UI 模式执行,报一堆不着边际的错。
- *
- * 用法:
- * node tools/build.js
- * node tools/build.js --version 2026.10.11
- */
- 'use strict';
- const fs = require('fs');
- const path = require('path');
- const ROOT = path.join(__dirname, '..');
- const PROJECT_ROOT = path.join(ROOT, '..'); /* AutoX 项目根目录(project.json 所在处) */
- const SRC_DIR = path.join(ROOT, 'src');
- const DIST_DIR = path.join(ROOT, 'dist');
- const OUT_FILE = path.join(DIST_DIR, 'tiktok-app.js');
- let VERSION = '2026.10.13';
- const INJECT_MARKER = '2026.10.13';
- /**
- * 模块顺序 = 依赖顺序。
- * theme 定义 U(几乎所有模块都用它),store 定义 Store(widgets/service 用),
- * records 定义运行记录(任务卡与记录页在**顶层**就要读它),
- * device/service 定义数据源,session 定义运行态(页面与 main 都用),
- * widgets 定义组件(页面用),pages 定义 Pages(main 用)——main 必须最后。
- */
- const MODULE_ORDER = [
- 'theme',
- 'store',
- 'records', // 运行记录:任务卡/记录页/结果文件都读它
- 'sources', // ID提取的取材清单:不定长列表,与 records 同样走独立 storage key
- 'device',
- 'service',
- 'fingerprint', // 配置指纹:与 tiktok-auto/src/fingerprint.js 同算法(断点续跑用)
- 'protocol', // 运行期协议:状态枚举/迁移表/阈值,引擎侧有一份同样的拷贝(见 tools/protocol-check.js)
- 'engine', // 执行引擎桥接:界面与 tiktok-auto 之间的文件通道
- 'session',
- 'resume', // 断点续跑:读断点/判断能否续/清空(只读引擎写的进度文件)
- 'widgets',
- 'dialog',
- 'floatball', // 全局悬浮球:用 floaty 开窗,只依赖 theme/store/device/session
- 'page-id',
- 'page-dm', // 同时含 UID私信页与养号页(养号只有一张卡,单开文件太碎)
- 'page-video',
- 'page-tasks', // 任务列表(一级页,四张任务卡)
- 'page-records', // 运行记录(一级页 + 运行详情二级页)
- 'page-login',
- 'page-mine',
- 'main'
- ];
- function readModule(name) {
- const file = path.join(SRC_DIR, `${name}.js`);
- if (!fs.existsSync(file)) {
- throw new Error(`缺少模块文件:${file}`);
- }
- return fs.readFileSync(file, 'utf8');
- }
- function moduleBlock(name) {
- return [
- '',
- `/* ============================== src/${name}.js ============================== */`,
- readModule(name)
- ].join('\n');
- }
- function build() {
- const header = [
- `/*! tiktok-app v${VERSION}`,
- ` * built ${new Date().toISOString()}`,
- ` * modules: ${MODULE_ORDER.join(', ')}`,
- ' * 由 tools/build.js 自动生成 —— 请不要直接编辑本文件,改 src/ 后重新构建。',
- ' */'
- ].join('\n');
- const body = MODULE_ORDER.map(moduleBlock).join('\n');
- const source = [
- '"ui";',
- header,
- '(function () {',
- body,
- '',
- '/* 入口 */',
- 'App.main();',
- '}());',
- ''
- ].join('\n');
- assertUiDirective(source);
- assertSyntax(source);
- assertNoLeftoverMarkers(source);
- assertProtocolPresent(source);
- return { source, modules: MODULE_ORDER };
- }
- /** "ui" 指令必须是第一条语句,否则脚本会以非 UI 模式执行。 */
- function assertUiDirective(source) {
- const firstLine = source.split('\n')[0].trim();
- if (firstLine !== '"ui";') {
- throw new Error(`"ui"; 指令必须位于第一行,当前第一行是:${firstLine}`);
- }
- }
- function assertSyntax(source) {
- try {
- // 只为语法检查,不执行
- new Function(source);
- } catch (error) {
- throw new Error(`构建产物语法检查失败:${error.message}`);
- }
- }
- /** 防止半成品被发到设备上:占位符和调试残留在这里挡住。 */
- function assertNoLeftoverMarkers(source) {
- const offenders = [];
- if (/\bTODO\b/.test(source)) { offenders.push('TODO'); }
- if (/\bFIXME\b/.test(source)) { offenders.push('FIXME'); }
- if (/debugger\s*;/.test(source)) { offenders.push('debugger'); }
- if (offenders.length > 0) {
- throw new Error('构建产物含未完成标记:' + offenders.join(', '));
- }
- }
- /**
- * 协议模块必须真的进了产物。
- *
- * ★ 这是补一个**静默失败**:`build()` 只遍历 `MODULE_ORDER`,
- * 新建 `src/protocol.js` 却忘了加进清单时,构建会**成功**,
- * 产物里根本没有它,直到运行期 `Protocol is not defined` 才炸。
- * 同理,忘了往 `__FEIZHOU__` 白名单里加,`smoke.js` 拿到的是 undefined,
- * 那一整批协议断言会被"跳过"而不是失败 —— 这里一起挡住。
- */
- function assertProtocolPresent(source) {
- /* 挂载行就是"这个文件被拼进来了"的证据:产物里没有它,
- * 说明 MODULE_ORDER 漏了 protocol,或者文件被改名了 —— 两种都要当场拦。 */
- if (source.indexOf('var Protocol = (function () {') < 0) {
- throw new Error(
- '构建产物里没有 Protocol 模块:src/protocol.js 没进 MODULE_ORDER?'
- );
- }
- if (source.indexOf('Protocol: Protocol') < 0) {
- throw new Error(
- '构建产物里 __FEIZHOU__ 没有暴露 Protocol:PC 自检将无法断言协议模块'
- );
- }
- }
- /**
- * 项目入口检查(本界面是**整个 AutoX 项目的启动入口**)。
- *
- * 为什么要在这里查:AutoX 的 UI 模式由**入口文件**的 `"ui";` 开启,
- * 而被 require 的模块里写 `"ui";` 是无效的(只是一句字符串语句)。
- * 也就是说,**入口文件的第一行写错,界面就会以非 UI 模式执行**,
- * 在设备上报一堆不着边际的错 —— 而那种错从日志上完全看不出根因。
- * 这里在 PC 侧就把它挡住。
- *
- * 检查三件事:project.json 的 main 指向的文件存在、第一行是 `"ui";`、
- * 并且它确实 require 了本界面产物。
- */
- function assertProjectEntry() {
- const projectPath = path.join(PROJECT_ROOT, 'project.json');
- const rootDir = PROJECT_ROOT;
- if (!fs.existsSync(projectPath)) {
- throw new Error(`找不到项目清单 ${projectPath}`);
- }
- const project = JSON.parse(fs.readFileSync(projectPath, 'utf8'));
- const entryRel = String(project.main || '').replace(/^\.\//, '');
- if (!entryRel) {
- throw new Error('project.json 里没有 "main" 字段,AutoX 不知道从哪启动');
- }
- const entryPath = path.join(rootDir, entryRel);
- if (!fs.existsSync(entryPath)) {
- throw new Error(`project.json 的 main 指向的文件不存在:${entryRel}`);
- }
- const entrySource = fs.readFileSync(entryPath, 'utf8');
- const firstLine = entrySource.split('\n')[0].trim();
- if (firstLine !== '"ui";') {
- throw new Error(
- `入口 ${entryRel} 的第一行必须是 "ui";(当前是 ${JSON.stringify(firstLine)})。` +
- 'UI 模式由入口文件开启,写在被 require 的模块里无效。');
- }
- if (entrySource.indexOf('tiktok-app/dist/tiktok-app.js') < 0) {
- throw new Error(`入口 ${entryRel} 看起来没有加载界面产物(未找到 tiktok-app/dist/tiktok-app.js)`);
- }
- return entryRel;
- }
- function writeDist(source) {
- fs.mkdirSync(DIST_DIR, { recursive: true });
- fs.writeFileSync(OUT_FILE, source, 'utf8');
- }
- function parseArgs(argv) {
- const options = { quiet: false };
- for (let i = 0; i < argv.length; i += 1) {
- if (argv[i] === '--version' && argv[i + 1]) {
- VERSION = argv[i + 1];
- i += 1;
- } else if (argv[i] === '--quiet') {
- options.quiet = true;
- }
- }
- return options;
- }
- function main() {
- const options = parseArgs(process.argv.slice(2));
- const { source, modules } = build();
- /* 把版本号注入到 Service.SCRIPT_VERSION,让界面显示的就是构建版本。
- * 注意判据必须是「正则能否匹配」,不能比较替换前后的字符串——
- * 当 --version 恰好等于源码里的占位值(首版就是这种情况)时,
- * 替换前后内容完全相同,用 !== 判定会误报注入失败。 */
- const versionPattern = new RegExp(`Service\\.SCRIPT_VERSION = "${INJECT_MARKER}"`);
- if (!versionPattern.test(source)) {
- throw new Error('版本注入失败:src/service.js 里的 SCRIPT_VERSION 占位符没找到');
- }
- const withVersion = source.replace(versionPattern, `Service.SCRIPT_VERSION = "${VERSION}"`);
- /* 入口检查放在写盘**之前**:检查不过就不该留下一个新产物 */
- const entryRel = assertProjectEntry();
- writeDist(withVersion);
- if (!options.quiet) {
- console.log(`已构建 ${OUT_FILE}`);
- console.log(` 版本 ${VERSION}`);
- console.log(` 模块 ${modules.length} 个:${modules.join(' → ')}`);
- console.log(` 体积 ${Buffer.byteLength(withVersion, 'utf8')} 字节`);
- console.log(' 检查:ui 指令位置通过;语法通过;无未完成标记');
- console.log(` 入口:project.json → ${entryRel}(第一行 "ui"; —— 界面为启动入口)`);
- }
- }
- module.exports = { build, writeDist, MODULE_ORDER, OUT_FILE, VERSION };
- if (require.main === module) {
- main();
- }
|