⛔ 2026-10-28 用户决定:不需要这个功能。 曾按本方案实现过一版(协作式暂停挂在
wait.tick上,加pause.flag文件协议), 已整体回退,代码里不留痕迹。本文件仅作历史记录,不要再照着实现。目标(原):运行中的任务能暂停(脚本停手、不推进),之后继续(从暂停处往下跑), 而不是只能「停止」(不可恢复)。 技术约束仍然成立(供将来参考):AutoX 的
ScriptEngine只有forceStop(),没有 pause/resume, 所以任何暂停都只能是协作式的。
| 问题 | 回答 |
|---|---|
| 现在能暂停吗? | 不能。 当前只有「停止」(一次性、不可恢复,写 stop.flag → 引擎抛 StopSignal 退出) |
| AutoX.js 能做到吗? | 能做到,但没有原生 API。 设备端实测:引擎只有 forceStop(),没有 pause/resume;engines 也没有"暂停某个引擎"的入口。必须自己做协作式暂停 |
能用 Thread.suspend()/resume() 吗? |
不能。 方法确实存在(见下表),但会挂在任意指令处、不释放锁,且本项目的 UI 与脚本跑在同一套 looper 线程上(MainThreadProxy),挂起 = 界面卡死 |
| 推荐路径 | 协作式暂停:复用现有的"唯一等待出口 wait.tick()",加一个 control.json 通道。暂停点天然覆盖全流程,业务代码零改动 —— 与 stop 完全同构,只是可逆 |
org.autojs.autoxjs.v7,2026-10-13 反射探测)ScriptEngine 方法(匹配 pause|resume|suspend|interrupt|stop):forceStop/0 ← 只有它
ScriptEngine 类链:LoopBasedJavaScriptEngine ← RhinoJavaScriptEngine ← JavaScriptEngine ← ScriptEngine$AbstractScriptEngine
engines 方法:execScript, execScriptFile, execAutoFile, myEngine, all, stopAll, stopAllAndToast ← 无暂停入口
engines.myEngine().pause / resume / suspend / isPaused → 全部 undefined
threads.currentThread() 类:com.stardust.autojs.core.looper.MainThreadProxy
Thread 方法:interrupt, interrupted, isAlive, isInterrupted, resume, stop, suspend ← 底层 Thread 方法在,但 suspend/resume 不可用(见 §2)
一句话:AutoX 给的是"杀",不是"停"。暂停只能由脚本自己在安全点实现 —— 这也正是本项目的架构已经准备好的事。
tiktok-auto 有一条明确的设计主张(src/wait.js:1):
脚本里不允许出现裸 sleep 当作"等待";
tick= 轮询节拍,sleep= 盲等(禁止用于等待)。
于是全流程几十处等待全部经过唯一出口 wait.tick()(src/wait.js:33):
function tick(ms) {
if (TikTokAuto.bridge) { TikTokAuto.bridge.checkStop(); } // ← 停止检查就插在这一处
sleep(ms === undefined ? DEFAULT_TICK_MS : ms);
}
bridge.checkStop()(src/bridge.js:263)每 500ms 节流读一次 stop.flag,
内容等于本次 runId 就抛 StopSignal → 由 main 统一兜住 → 上报终态。
因为只有一个出口,所以只在那一处插检查,就获得了全流程可中断,业务代码一行没改。
暂停只需要在这同一个出口再加一个"闸门":命中暂停就停在 tick 里循环等待,恢复后原样继续往下跑。 这是本方案的全部核心。
| 路线 | 怎么做 | 可行性 | 结论 |
|---|---|---|---|
| A. 协作式暂停(推荐) | 引擎在 wait.tick() 里检查"暂停闸门",命中就阻塞在 tick 内循环等待恢复 |
✅ 与 stop 同构,改动小、可测 | 采用 |
| B. 挂起线程 | thread.suspend() / Process.sendSignal(SIGSTOP) |
❌ 三个致命问题:① 挂在任意指令处(可能停在一次 click/swipe 中间,恢复后页面处于半操作状态);② suspend 不释放锁 → 死锁风险;③ 本项目的 UI 与脚本共享 MainThreadProxy,挂起即界面无响应;④ SIGSTOP 冻结整个进程,连状态上报文件也不写,界面会以为引擎死了 |
否决 |
| C. 依赖运行时自带 pause | 找 ScriptEngine.pause() |
❌ 实测不存在(见 §0 证据) | 不可用 |
顺带说明为什么不给引擎加
threads.start的"暂停标志线程": 子线程只能改标志位,没法让正在等待的主流程停下来 —— 停下来的动作必须发生在主流程自己的代码路径上, 也就是wait.tick()。所以还是要走 A。
现有 stop.flag 是边沿触发(写一次 = 请求停止,用内容 = runId 避免误杀);
暂停/继续是电平触发("当前是不是暂停态"),语义不同,所以单独一个文件更清楚:
| 文件 | 方向 | 内容 | 触发方式 |
|---|---|---|---|
/sdcard/tiktok_app/control.json (新增) |
界面 → 引擎 | {"runId":"run-xxx","paused":true/false} |
电平(读当前状态) |
stop.flag(不动) |
界面 → 引擎 | 本次 runId | 边沿(请求停止) |
为什么要 json 而不是又一个 flag 文件:暂停需要一个明确的"继续"信号。
用文件存在/不存在表达时,"继续"就得删除文件(删除有竞态,且 files.remove 命中 MCP 危险调用黑名单 —— 这条坑在 stop.flag 上已经踩过,src/bridge.js:23 有记录)。
paused: false 是幂等的、不怕上一轮残留,也不会误杀本轮(runId 比对)。
| 文件 | 位置 | 改什么 |
|---|---|---|
tiktok-auto/src/bridge.js |
parseArgs(:102 附近) |
新增 --control 参数(默认 /sdcard/tiktok_app/control.json) |
| 同 | checkStop 旁(:263) |
新增 checkPause():与 checkStop 共用同一个 500ms 节流窗口(一次 tick 只读一个文件,不增加 IO);命中暂停 → report({status:'paused'}) → 进入等待循环 |
tiktok-auto/src/wait.js |
tick(:33) |
在 checkStop() 之后加一行 checkPause() —— 唯一的插入点 |
tiktok-auto/src/flow.js |
runSession(:263)上方 |
新增 relocate(config):恢复后重放"回到结果页"那几步(ensureFeed → openSearchPage → submitKeyword → waitForCards → selectResultTab,都是现成且幂等的函数) |
暂停循环的关键约束(三条,缺一条就出问题):
// bridge.checkPause() 内(示意,未实施)
report({ status: 'paused', phase: '已暂停' });
var since = Date.now();
for (;;) {
sleep(200); // 暂停期间极低开销
checkStop(); // ★ 必须继续检查停止:否则暂停中按「停止」毫无反应,用户以为卡死
if (!stillPaused()) { break; } // 读到 paused:false(且 runId 一致)→ 继续
}
report({ status: 'running', phase: '' });
paused:false);tick 上,所以生效延迟 = 当前这次动作的剩余时间(通常 <1s;最坏是一次触摸/等待超时)。| 文件 | 位置 | 改什么 |
|---|---|---|
tiktok-app/src/session.js |
字段区 / elapsedMs(:110) |
新增 paused / pausedMs / pauseStartedAt;elapsedMs() 扣除暂停时长(否则"耗时"里混进等待时间,D1/D3 的数字失真) |
| 同 | barText(:91) |
暂停态文案:{任务} · {脚本} · 已暂停 |
tiktok-app/src/main.js |
运行条渲染(:242 段内) |
暂停态色调用现成的 warn(琥珀);App.tickTimer 的 setTone 同步 |
| 同 | renderRunArea(:392) |
详情页 CTA 那一行加次级按钮「暂停 / 继续」(主 CTA 仍是「停止运行」) |
| 同 | pollEngine(:562) |
认 status === 'paused':置 Session.paused,不计为终态(Engine.isTerminalStatus 不动) |
| 同 | requestRun(校验链) |
暂停态视为仍占用:点别的任务的「运行」照旧弹 F3(Dialog.switchTask 文案改为「{X} 已暂停,开始 {Y} 将停止它」) |
按钮放哪(这一点需要你拍板):规格书 §2.4 明确写了 「运行条 = 状态与导航;页面底部 CTA = 启停控制。运行条上不放停止按钮」。 所以暂停按钮不建议塞进运行条,两种放法:
| 选项 | 交互 | 代价 |
|---|---|---|
| ① 放详情页 CTA 行(推荐) | CTA 行变成「暂停」次级按钮 + 「停止运行」主按钮;运行条只表达状态(琥珀 + 「已暂停」) | 需要切回该任务的详情页才能暂停(运行条点一下就到,路径很短) |
| ② 放运行条右侧 | 运行条右侧加一个小按钮 | 破坏规格书 §2.4 的分工,且运行条整条可点=跳任务页,会有点击冲突 |
暂停期间脚本停手了,但手机和 TikTok 不会停:视频继续播、可能自动滑到下一条、 可能弹"青少年模式/网络异常"弹窗、可能被系统回收,也可能息屏。
所以恢复不能假设"还在暂停时那一页"。方案:
needsRelocate = true;flow.js:302)检测该标志 → 调 relocate(config) 重放定位;已跳过:暂停期间页面漂移;skipped 单列,attemptedVideos 不虚增。这样做而不是"原地续":原地续需要保存"我在第几个卡片的第几秒",而暂停期间播放进度已变, 续上去必然对不齐 → 会变成一个"看起来在续、其实数据不对"的假功能。 重放定位 + 跳过被打断的那一个,语义诚实、实现简单、零归位成本。
备选:安全点暂停(配置项 pauseMode: "safePoint")
只在"视频与视频之间"的循环顶部接受暂停 —— 恢复时状态天然干净、无需归位,
代价是暂停最长要等当前视频处理完(一次 30–60s),用户会以为没生效。
默认建议 immediate(立刻暂停 + 重放定位),把 safePoint 留给"不能让抖音状态漂移"的保守场景。
| 项 | 处理 |
|---|---|
| 运行条计时 | 暂停期间停表:elapsedMs() 扣除 pausedMs(暂停中显示"已暂停"而不是继续跳秒) |
| 运行记录耗时 | 同上(Records.timeLine 的"耗时"因此是有效执行时长) |
| D3 执行日志 | 借用已有的日志通道(bridge.js:155 appendLog)写两行真实事件:已暂停 / 已继续(暂停 1 分 12 秒) |
| 结果分类 | 不新增分类:暂停不是终态。若暂停中卡密到期/权限被关/被停止,按现有的「手动停止 / 报错中断」归类 |
| 暂停超时 | 新增 pauseMaxMs(默认 10 分钟,可配):超时自动按「手动停止」结束本次运行并写记录,理由见 §4.3 —— 不能让它无限占着"全局单跑"的位 |
| # | 风险 | 应对 |
|---|---|---|
| 1 | 暂停 ≠ 手机停:TikTok 里的视频还在播,用户可能以为"全停了" | 暂停时 toast + 运行条文案明确「已暂停(TikTok 仍在播放)」;文案必须写清,否则是误导 |
| 2 | 恢复后页面漂移(见 §3.4) | 重放定位 + 跳过被打断的一次;日志如实记录 |
| 3 | 息屏/锁屏:暂停久了系统息屏,无障碍手势与触摸可能受限、后台可能被回收(项目里目前没有任何 keepScreenOn 调用,实测确认) |
① 暂停时调用保持屏幕常亮(device.keepScreenOn());② pauseMaxMs 到点自动停止 |
| 4 | 引擎被系统杀掉 | 界面已有兜底(main.js:562 段:「引擎消失且无终态 → failed」),暂停态同样生效 ✅ 无需改 |
| 5 | 暂停中卡密到期 | tickTimer 仍在跑 → 走现有 F4 路径(停引擎 + 弹窗 + 回登录)✅ 无需改 |
| 6 | 暂停中按「停止」 | 暂停循环内继续 checkStop()(§3.2 约束 1)→ 正常两步停止 |
| 7 | 「继续」信号丢失(界面崩了/被卸载) | pauseMaxMs 兜底 + 引擎在暂停循环里也读 stop.flag;最坏情况是超时自动停止 |
| 8 | 全局单跑被"暂停"长期占用 | 暂停态视同占用:别的任务点「运行」→ F3 确认(文案说明它已暂停) |
| 9 | 暂停点仍可能"半动作"(如刚点完一次 swipe 还没验证) | 恢复后第一步就是 relocate() 校验页面状态,不满足就归位;不信任暂停前的页面假设 |
Thread.suspend / SIGSTOP 式冻结(§2 已否);stop.flag 的既有语义与两步停止流程(已验证过的路径不重做);tools/smoke.js / 桩时钟)stop → 能中断、且上报终态是 stopped(不是 paused)control.json 里 runId 不匹配 → 不暂停(防上一轮残留)paused:false 幂等:连续写 10 次不会出问题elapsedMs() 扣除暂停时长(构造 60s 暂停,断言耗时少了 60s)running(青)→ paused(琥珀)→ finished(隐藏)Records 落盘里出现 已暂停 / 已继续 两行日志,且耗时是有效时长pauseMaxMs 到点 → 自动按「手动停止」写记录| 期 | 内容 | 涉及文件 | 验收 |
|---|---|---|---|
| 第 1 期(最小可用) | control.json 协议 + --control 参数 + checkPause + tick 插桩 + 详情页暂停按钮 + 停表 + 运行条琥珀态 + 日志两行 + 离线断言 |
bridge.js wait.js session.js main.js engine.js + smoke.js |
离线全绿;真机矩阵 1、5、6 通过 |
| 第 2 期(抗漂移) | relocate() 重放定位 + skipped 统计 + pauseMaxMs 超时自动停止 + 屏幕常亮 |
flow.js config.js + 离线断言 |
真机矩阵 2、3、4、7、8 通过 |
改动量预估:第 1 期约 5 个源文件 + 1 个测试文件,核心逻辑 <100 行(其中一半是注释); 第 2 期约 3 个源文件。不需要动规格书、不需要动 UI 骨架、不需要动既有 stop 链路。
tiktok_stats.js 那套 batchUrls + skipExisting 的思路,
任务级"跑一段、停、下次跳过已完成的"—— 语义上不如暂停,但对"跑一半要走开"的场景够用;1 ~ 5 已支持),单次运行短、好停。这两条都不需要新代码,先写在这里是为了让"要不要做暂停"这个决定有参照。