|
@@ -521,40 +521,54 @@ tiktok-app/
|
|
|
└─ 终态(finished / stopped / failed)→ 复位 + 切回前台 + toast
|
|
└─ 终态(finished / stopped / failed)→ 复位 + 切回前台 + toast
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-### 两侧怎么说话:四个文件
|
|
|
|
|
|
|
+### 两侧怎么说话:六个文件
|
|
|
|
|
|
|
|
界面与引擎是**两个独立的 ScriptEngine**:各有各的全局作用域,对象传不过去
|
|
界面与引擎是**两个独立的 ScriptEngine**:各有各的全局作用域,对象传不过去
|
|
|
(实测:父脚本读不到子引擎里的任何东西)。所以通道只能是文件。
|
|
(实测:父脚本读不到子引擎里的任何东西)。所以通道只能是文件。
|
|
|
|
|
|
|
|
| 文件(都在 `/sdcard/tiktok_app/`) | 方向 | 内容 |
|
|
| 文件(都在 `/sdcard/tiktok_app/`) | 方向 | 内容 |
|
|
|
|---|---|---|
|
|
|---|---|---|
|
|
|
-| `engine-config.json` | 界面 → 引擎 | 映射后的引擎配置(由 `--config` 交给 `config.resolve`) |
|
|
|
|
|
-| `run-state.json` | 引擎 → 界面 | `status` / `phase` / `done` / `total` / `message` / `summary` |
|
|
|
|
|
|
|
+| `engine-config.json` | 界面 → 引擎 | 映射后的引擎配置(由 `--config` 交给 `config.resolve`),含本轮 `runtime.runId` |
|
|
|
|
|
+| `run-state.json` | 引擎 → 界面 | `v` / `status` / `phase` / `done` / `total` / `message` / `summary` / `engine.capabilities` |
|
|
|
|
|
+| `engine-beat.json` | 引擎 → 界面 | 心跳:`beatAt` + `stopSeenAt`,**每 2 秒整份重写**,唯一写者是引擎的心跳线程 |
|
|
|
| `stop.flag` | 界面 → 引擎 | 停止请求,内容是**本次运行的 runId** |
|
|
| `stop.flag` | 界面 → 引擎 | 停止请求,内容是**本次运行的 runId** |
|
|
|
-| `run-config.json` | 界面 → 人 | 完整快照,供追溯 |
|
|
|
|
|
|
|
+| `run-log.jsonl` | 引擎 → 界面 | 逐行 JSON,D3 的「执行日志」读它 |
|
|
|
|
|
+| `run-config.json` | 界面 → 人 | 完整快照,供追溯(**没有程序读它**) |
|
|
|
|
|
|
|
|
-全是纯文本 JSON,出问题 `cat` 一下就能复盘。
|
|
|
|
|
|
|
+除日志是逐行 JSON 外都是整份 JSON,出问题 `cat` 一下就能复盘。
|
|
|
|
|
|
|
|
-### 停止:两步,且以引擎的回报为准
|
|
|
|
|
|
|
+**为什么心跳要单独一个文件**:`run-state` 由主流程按条目写(可能几十秒一次),
|
|
|
|
|
+心跳 2 秒一次。两个写者写同一个文件必然互相覆盖,而且频率差一个数量级。
|
|
|
|
|
+有了它,界面才能区分「活着但正卡在一个 25 秒的观看里」和「已经死了」——
|
|
|
|
|
+这是只靠状态文件永远做不到的事。
|
|
|
|
|
|
|
|
-点「停止运行」不是"一按就当停了":
|
|
|
|
|
|
|
+### 停止:三段,且以引擎的回报为准
|
|
|
|
|
|
|
|
-1. 界面写 `stop.flag`(内容 = runId),状态条立刻变 **「正在收尾」**(琥珀色);
|
|
|
|
|
-2. 引擎在**唯一的等待原语** `wait.tick()` 里轮询到它 → 抛 `StopSignal` →
|
|
|
|
|
- 退出当前等待 → 收尾 → 写 `status: "stopped"`;
|
|
|
|
|
-3. 界面读到终态 → 复位 + 提示。实测**从点击到复位约 1 秒**。
|
|
|
|
|
|
|
+点「停止运行」不是"一按就当停了",中间有两段"我说了 / 它听见了"要分清:
|
|
|
|
|
+
|
|
|
|
|
+1. 界面写 `stop.flag`(内容 = runId),状态条变 **「正在通知引擎」**(琥珀色)
|
|
|
|
|
+ —— 此刻只是"我发出了",引擎还没回话;
|
|
|
|
|
+2. 引擎承认收到 → 状态条变 **「正在收尾」**。承认有两条通道:
|
|
|
|
|
+ 心跳里的 `stopSeenAt`(快,独立线程,不受主流程阻塞影响),
|
|
|
|
|
+ 或它自己报了 `status: "stopping"`(走 `wait.tick()` → `checkStop`);
|
|
|
|
|
+3. 引擎写出终态 → 界面复位 + 提示。实测**从点击到复位约 1 秒**。
|
|
|
|
|
|
|
|
为什么不做成点一下就复位:引擎收到请求后还要退出当前等待、把 TikTok 带回信息流,
|
|
为什么不做成点一下就复位:引擎收到请求后还要退出当前等待、把 TikTok 带回信息流,
|
|
|
它确实还在动。这时界面说「已停止」,用户看到 TikTok 仍在操作,只能怀疑"到底停没停"。
|
|
它确实还在动。这时界面说「已停止」,用户看到 TikTok 仍在操作,只能怀疑"到底停没停"。
|
|
|
|
|
|
|
|
-两个细节:
|
|
|
|
|
|
|
+四个细节:
|
|
|
|
|
|
|
|
- **停止检查放在 `wait.tick()` 里**,那是全流程唯一的等待出口(等待应用启动、等待播放、
|
|
- **停止检查放在 `wait.tick()` 里**,那是全流程唯一的等待出口(等待应用启动、等待播放、
|
|
|
等待结果卡片……几十处都经过它)。于是"能被打断"这件事**只实现了一次**,
|
|
等待结果卡片……几十处都经过它)。于是"能被打断"这件事**只实现了一次**,
|
|
|
业务代码一行都没改。`bridge` 未启用时是空操作,独立运行零开销。
|
|
业务代码一行都没改。`bridge` 未启用时是空操作,独立运行零开销。
|
|
|
- **`stop.flag` 里存 runId 而不是空文件**:于是不必删除它(删除既有竞态,
|
|
- **`stop.flag` 里存 runId 而不是空文件**:于是不必删除它(删除既有竞态,
|
|
|
`files.remove` 又命中 MCP 危险调用黑名单),上一轮的残留标志也不会误杀本轮。
|
|
`files.remove` 又命中 MCP 危险调用黑名单),上一轮的残留标志也不会误杀本轮。
|
|
|
-- 界面还有**超时兜底**(8 秒):引擎不确认也不能一直卡在"正在收尾"上。
|
|
|
|
|
|
|
+- **超时兜底是两段**:ack 等 3 秒、收尾再等 10 秒,合计 13 秒;
|
|
|
|
|
+ 哪一段没等到,终态文案就如实说是哪一段(「引擎未确认收到请求」/「引擎未确认收尾」)。
|
|
|
|
|
+ 老引擎(没有心跳的 v1)沿用改造前的单段 8 秒。
|
|
|
|
|
+- **心跳 ack 是防御性保险,不是主路径**:业务代码里所有等待都走 `wait.tick()`
|
|
|
|
|
+ (tick 在 200~250ms 量级),所以主流程最长 ~750ms 就会自己发现停止请求,比心跳的 2 秒还快。
|
|
|
|
|
+ 这条通道只在主线程被系统层面卡住(GC、IO 抖动、页面假死)时才可能先到。
|
|
|
|
|
|
|
|
### 引擎侧的能力边界,如实告知
|
|
### 引擎侧的能力边界,如实告知
|
|
|
|
|
|
|
@@ -768,8 +782,9 @@ ID提取的四个数量门槛)。每敲一个数字都会跑一遍 `syncCrossR
|
|
|
| **真引擎端到端** | 启动 TikTok → 搜索 fashion → 切 Top → 打开视频 → 观看 2095ms → 返回;终态 `finished`,界面接住并提示 |
|
|
| **真引擎端到端** | 启动 TikTok → 搜索 fashion → 切 Top → 打开视频 → 观看 2095ms → 返回;终态 `finished`,界面接住并提示 |
|
|
|
| **纯浏览模式** | `comment: {ok:true, skipped:true, reason:"本次未启用评论动作"}`——没勾评论就真的没发 |
|
|
| **纯浏览模式** | `comment: {ok:true, skipped:true, reason:"本次未启用评论动作"}`——没勾评论就真的没发 |
|
|
|
| **跑完自动切回前台** | 界面自己回到前台,顶部显示「上次运行:完成:处理 1 个视频 / 发出 0 条评论」 |
|
|
| **跑完自动切回前台** | 界面自己回到前台,顶部显示「上次运行:完成:处理 1 个视频 / 发出 0 条评论」 |
|
|
|
-| **两步停止** | 状态条「正在收尾」(琥珀色)→ 约 1 秒后「已停止(假引擎)」;引擎日志 `收到停止请求(runId 匹配)` |
|
|
|
|
|
-| 未接入的脚本被拦下 | 在 UID私信页点「开始运行」→ 弹「…的执行引擎尚未接入」,引擎调用数为 0(ID提取已接入,改用它验这条闸) |
|
|
|
|
|
|
|
+| **三段停止** | 状态条「正在通知引擎」→(引擎承认)→「正在收尾」(琥珀色)→ 约 1 秒后「已停止(假引擎)」;引擎日志 `收到停止请求(runId 匹配)` |
|
|
|
|
|
+| **心跳** | 运行期间 `engine-beat.json` 的 `beatAt` 每 2 秒前进一次;引擎被强杀后 ≤15 秒界面报「引擎已退出(未收到结束信号)」 |
|
|
|
|
|
+| 未接入的脚本被拦下 | **已无天然样本**(四个任务都接了引擎)→ 临时摘掉某个任务卡的 `engine` 标记再点运行,应弹「…的执行引擎尚未接入」,且引擎调用数为 0 |
|
|
|
| **ID提取端到端** | 界面点运行 → 引擎读到 `task=id` + 完整取材清单 → 抓 3 个 uid → 产物文件 3 行 3 唯一 → 记录页「完成:提取 3 个 ID → 文件路径」 |
|
|
| **ID提取端到端** | 界面点运行 → 引擎读到 `task=id` + 完整取材清单 → 抓 3 个 uid → 产物文件 3 行 3 唯一 → 记录页「完成:提取 3 个 ID → 文件路径」 |
|
|
|
|
|
|
|
|
**联调时自己绊自己两次**,都记下来免得重演:
|
|
**联调时自己绊自己两次**,都记下来免得重演:
|