| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257 |
- #!/usr/bin/env node
- /**
- * build.js — 构建流水线:src/ 多个小模块 → dist/ 一个可下发到 AutoX 的单文件脚本
- *
- * 为什么需要构建(Zakas《编写可维护的JavaScript》第 13/15 章):
- * 人维护代码时希望**一个文件只做一件事**(所以 src/ 拆成 11 个模块);
- * 但 AutoX 通过 MCP 下发的是"一整段脚本",没法带一堆 require 依赖。
- * 于是中间加一步 concat:开发时按模块读,部署时是一个自包含文件。
- * 两边都拿到自己最舒服的形态。
- *
- * 构建时会做两道自检:
- * 1. 语法检查(new Function)——避免把语法错误的脚本发到手机上才发现;
- * 2. **MCP 危险调用黑名单预检**——桥那边一旦命中就会要求人工确认,
- * 这属于"部署期的坑",应该在构建期就挡住,而不是每次运行都卡一下。
- *
- * 用法:
- * node tools/build.js # 构建 dist/tiktok-auto.js
- * node tools/build.js --self-test # 构建只跑自检的变体
- * node tools/build.js --inject '{"target":{"keyword":"cat"}}'
- */
- 'use strict';
- const fs = require('fs');
- const path = require('path');
- const ROOT = path.join(__dirname, '..');
- const SRC_DIR = path.join(ROOT, 'src');
- const DIST_DIR = path.join(ROOT, 'dist');
- const OUT_FILE = path.join(DIST_DIR, 'tiktok-auto.js');
- const VERSION = '1.0.0';
- /**
- * 模块顺序 = 依赖顺序。
- * bridge 不依赖任何模块(logger 是运行时取用),但它要被 wait 引用,所以排在 wait 之前;
- * tiktok.js 在**加载时**就读取 layout/logger,所以它必须排在它们之后;
- * 其余模块只在函数内部按名字取用,因此对顺序不敏感。
- */
- const MODULE_ORDER = [
- 'logger',
- /* 运行期协议:状态枚举 / 迁移表 / 阈值。
- * 与 tiktok-app/src/protocol.js 是同一份内容(只挂载点不同),
- * 由 tiktok-app/tools/protocol-check.js 逐字节比对。
- * 放在 bridge 之前:bridge 在**函数体内**才取 TikTokAuto.protocol,
- * 但让它先存在能省掉一类加载期顺序问题。 */
- 'protocol',
- 'bridge',
- 'config',
- 'wait',
- 'layout',
- 'ui',
- 'popups',
- 'tiktok',
- 'player',
- 'comment',
- 'interact',
- 'flow',
- /* 养号(task = 'nurture'):没有目标,在信息流里随机浏览 + 概率互动。
- * 放 flow 之后,因为它借用 flow 的 launchApp / ensureFeed(应用级动作)。 */
- 'nurture',
- /* —— ID 提取(task = 'id')——
- * 顺序按依赖走:parse 是纯函数地基;uidset/filters 只依赖 parse;
- * profile 调 tiktok 与 flow;idsource 调 profile;idflow 调全部。
- * 这些模块**都不在加载时取别的模块的引用**(一律在函数体内按名字取),
- * 所以这里的顺序主要是给人读的,不满足也不会炸 —— 但仍然按依赖排。 */
- 'parse',
- 'uidset',
- /* 断点续跑的地基:fingerprint 算配置指纹(纯函数)、checkpoint 管进度文件 */
- 'fingerprint',
- 'checkpoint',
- 'filters',
- 'profile',
- 'idsource',
- 'resolve',
- 'idflow',
- /* —— UID 私信(task = 'dm')——
- * dm 是"发一条私信"的能力(开面板/输入/点发送/验证),
- * dmflow 是编排(读 UID → 关注一批 → 私信另一批 → 逐个休息)。
- * 放在最后:dmflow 要用 profile / interact / dm / flow,它们都已定义。 */
- 'dm',
- 'dmflow'
- ];
- const ENTRY_MODULE = 'main';
- const SELFTEST_MODULE = '__selftest';
- const INJECT_MARKER = '/*TTA_INJECTED_CONFIG*/null';
- /**
- * 与 MCP 桥一致的"危险调用"规则。
- * 匹配前先按驼峰拆词,否则 smsManager 这类标识符会因为缺少词边界而漏报。
- * 正则**不能带 g** —— 带 g 的 RegExp.test 会保留 lastIndex,重复调用结果会交替翻转。
- */
- const DANGEROUS_RULES = [
- { name: 'sms', pattern: /\bsms\b/i },
- { name: 'telephony', pattern: /\btelephony\b/i },
- { name: 'call(', pattern: /\bcall\s*\(/i },
- { name: 'deleteFile', pattern: /\bdelete\s+file\b/i },
- { name: 'remove(', pattern: /\bremove\s*\(/i }
- ];
- function splitIdentifiers(code) {
- return code
- .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
- .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2');
- }
- 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 buildEntry(options) {
- if (options.customEntry) {
- return ['', '/* 自定义入口(调试/探针用) */', options.customEntry].join('\n');
- }
- if (options.selfTest) {
- return [
- '',
- '/* 自检模式:只跑纯函数断言,不启动 App、不发评论 */',
- 'var failedCount = TikTokAuto.selftest.report();',
- 'console.log(failedCount === 0 ? "[TTA]SELFTEST_ALL_PASS" : "[TTA]SELFTEST_HAS_FAILURE");',
- 'console.log("__END__");'
- ].join('\n');
- }
- return [
- '',
- '/* PC 侧下发时可替换下面这一行的 null 来注入配置覆盖项 */',
- `var INJECTED_CONFIG = ${INJECT_MARKER};`,
- '/* 被界面宿主唤起时(engines.execScriptFile 带 --config/--run-id 参数),',
- ' * fromExecArgv 会把参数翻译成「配置路径 + 桥的开关」;独立运行时是空操作。 */',
- 'TikTokAuto.main.run(TikTokAuto.bridge.fromExecArgv(INJECTED_CONFIG));'
- ].join('\n');
- }
- function build(options = {}) {
- const modules = options.selfTest
- ? MODULE_ORDER.concat([SELFTEST_MODULE])
- : MODULE_ORDER.concat([ENTRY_MODULE]);
- const header = [
- `/*! tiktok-auto v${VERSION}`,
- ` * built ${new Date().toISOString()}`,
- ` * modules: ${modules.join(', ')}`,
- ' * 由 tools/build.js 自动生成 —— 请不要直接编辑本文件,改 src/ 后重新构建。',
- ' */'
- ].join('\n');
- const body = modules.map(moduleBlock).join('\n');
- const source = [header, '(function () {', 'var TikTokAuto = {};', body, buildEntry(options), '}());', ''].join('\n');
- assertSyntax(source);
- assertNoDangerousCalls(source);
- assertProtocolPresent(source);
- return { source, modules };
- }
- /**
- * 协议模块必须真的进了产物。
- * ★ `build()` 只遍历 `MODULE_ORDER` —— 新建 src/protocol.js 却忘了加进清单时,
- * 构建会**成功**,产物里没有它,直到运行期取 `TikTokAuto.protocol` 才炸。
- */
- function assertProtocolPresent(source) {
- if (source.indexOf('TikTokAuto.protocol = (function') < 0) {
- throw new Error('构建产物里没有 TikTokAuto.protocol:src/protocol.js 没进 MODULE_ORDER?');
- }
- }
- function assertSyntax(source) {
- try {
- // 只为语法检查,不执行
- new Function(source);
- } catch (error) {
- throw new Error(`构建产物语法检查失败:${error.message}`);
- }
- }
- function assertNoDangerousCalls(source) {
- const flattened = splitIdentifiers(source);
- const hits = DANGEROUS_RULES.filter((rule) => rule.pattern.test(flattened));
- if (hits.length > 0) {
- throw new Error(
- '构建产物命中 MCP 危险调用黑名单:' + hits.map((h) => h.name).join(', ') +
- '。这会让每次下发都要求人工确认,请在 src/ 里改写这些调用。'
- );
- }
- }
- function writeDist(source) {
- fs.mkdirSync(DIST_DIR, { recursive: true });
- fs.writeFileSync(OUT_FILE, source, 'utf8');
- }
- function parseArgs(argv) {
- const options = { selfTest: false, inject: null, quiet: false };
- for (let i = 0; i < argv.length; i += 1) {
- const arg = argv[i];
- if (arg === '--self-test') {
- options.selfTest = true;
- } else if (arg === '--inject') {
- options.inject = JSON.parse(argv[i + 1]);
- i += 1;
- } else if (arg === '--quiet') {
- options.quiet = true;
- }
- }
- return options;
- }
- function main() {
- const options = parseArgs(process.argv.slice(2));
- const { source, modules } = build(options);
- if (options.inject) {
- const injected = JSON.stringify(options.inject);
- if (!source.includes(INJECT_MARKER)) {
- throw new Error('找不到注入占位符,构建产物异常');
- }
- writeDist(source.replace(INJECT_MARKER, `/*TTA_INJECTED_CONFIG*/${injected}`));
- } else {
- writeDist(source);
- }
- if (!options.quiet) {
- console.log(`已构建 ${OUT_FILE}`);
- console.log(` 模块 ${modules.length} 个:${modules.join(' → ')}`);
- console.log(` 体积 ${Buffer.byteLength(source, 'utf8')} 字节`);
- console.log(' 语法检查:通过;危险调用预检:通过');
- }
- }
- module.exports = {
- ROOT,
- OUT_FILE,
- MODULE_ORDER,
- INJECT_MARKER,
- build,
- writeDist,
- parseArgs,
- main
- };
- if (require.main === module) {
- main();
- }
|