SceneFlow 作者指南

SceneFlow 是程式碼內的輕量 Pipeline。它只處理“當前允許識別什麼畫面, 成功後允許到哪裡去, 何時重試或恢復”。OCR、點選、按鍵、路線和長時間業務迴圈仍寫在任務 action 中。

一輪流程如何執行

flow.step(
    FishingStep.CAST,
    self.is_ready_to_cast,
    self._cast,
    next=(FishingStep.CAST, FishingStep.WAIT_BITE, FishingStep.RESULT),
    policy=StepPolicy(max_attempts=4, interval=2),
    on_failure=self._route_cast_failure,
)

其含義是:

  1. 只有 CAST 在當前候選集內, 且 is_ready_to_cast() 為真時, 才執行 _cast()。
  2. _cast() 沒有拋異常就視為完成。它的返回值會被忽略。
  3. 完成後只允許識別 CAST、WAIT_BITE、RESULT。next 不是立即呼叫下一 action, 而是下一輪的可識別範圍。
  4. CAST 寫在 next 中, 所以準備頁仍然可見時可以再次拋竿。沒有寫入自身的 action 不會被框架重放。
  5. 四次後仍停在 CAST, 則呼叫 _route_cast_failure() 決定去補貨還是結束當前輪。
flowchart LR
    A["候选: CAST"] --> B{"准备页?"}
    B -->|是| C["执行 _cast()"]
    C --> D["候选: CAST / WAIT_BITE / RESULT"]
    D --> E{"匹配其中一个画面"}
    E -->|CAST| C
    E -->|WAIT_BITE| F["执行 _wait_bite()"]
    E -->|RESULT| G["执行 _collect_result()"]

API

flow.step(key, detector, action, *, next, policy=None, transition=None, on_failure=None)
引數 含義
key 步驟 Enum。
detector 無副作用的畫面判斷函式。
action 業務動作。返回值不參與流程; 成功不拋異常, 失敗拋 WaitFailedException。
next action 成功後允許識別的後繼步驟, 必須非空。只有把自身寫入其中才允許再次執行 action。
policy 當前 action 的重複次數和最小間隔。
transition 已知頁面切換期間重複執行的輕量輸入, 例如 Escape。
on_failure 當前步驟已無法繼續時的區域性路由。

StepPolicy

StepPolicy(max_attempts=None, interval=0.0)
  • max_attempts: 同一個 action 最多執行幾次。None 表示不限制。
  • interval: 同一個 action 再次執行前的最小間隔。它不替代點選或按鍵自身的節流。

StepPolicy 不管理 action 的執行時長。長動作自己擁有領域 timeout, 例如釣魚控條的 CONTROL_TIMEOUT。Pipeline 在 action 完成後才繼續識別後繼步驟; 節點策略主要提供重複次數、最小重試間隔與錯誤路由, 而非統一接管業務 action timeout。

如果 action 丟擲 WaitFailedException:

  • 後繼畫面已經出現時, 直接進入後繼步驟;
  • 原畫面仍在且未達到 max_attempts 時, 等待 interval 後重試;
  • 否則進入 on_failure。

on_failure

def _route_cast_failure(self, failure: StepFailure) -> FishingStep | None:
    if self.config[self.CONF_AUTO_BUY_BAIT]:
        return FishingStep.OPEN_SELL
    self.add_failed("未检测到进入抛竿状态")
    return FishingStep.CAST

on_failure 只在框架確認當前步驟不能繼續時呼叫, 例如達到 max_attempts 或 action 丟擲不可重試的 WaitFailedException。

  • 返回已註冊的步驟: 立即切換為只識別該步驟;
  • 返回 None: 進入全域性 recovery();
  • 沒有 on_failure: 同樣進入 recovery()。

已知切換與未知恢復

兩者都用 interval 節流重複輸入, 但觸發條件不同。

return_to_ready = flow.transition(
    lambda: self.send_key("esc"),
    interval=2,
    timeout=60,
)

flow.step(
    FishingStep.RESULT,
    self.has_success_overlay,
    self._collect_result,
    next=(FishingStep.CAST,),
    transition=return_to_ready,
)
  • transition() 在 source action 成功完成後立刻執行一次, 並在等待 next 頁面時按 interval 重試。它的 timeout 從 action 返回後開始計算。
  • recovery() 只處理候選步驟都不可見, 或失敗路由返回 None 的未知畫面。它預設先等待 5 秒 grace, 之後才按 interval 執行恢復動作。
flow.recovery(
    self._recover_fishing_scene,
    grace=5,
    interval=2,
    max_attempts=180,
    timeout=360,
)

釣魚的 _recover_fishing_scene() 可在傳送 Escape 前釋放控條按鍵; 普通 transition 則直接寫 lambda: self.send_key("esc") 即可。

Guard 與中斷

  • guard() 優先於普通 step, 但不修改當前候選集。釣魚的 TEAM guard 重新進入釣魚點後, 會繼續等待原來的補貨或釣魚步驟。
  • interrupt() 優先於 guard。月卡中斷在 BaseNTETask 中統一註冊。
  • 普通 wait_until() 已自動檢查中斷。自定義高頻迴圈呼叫 self.scene_flow.safe_point(); 發現中斷會拋 SceneReplan 並重新分類。
  • 被 SceneReplan 中斷的 action 不計入 max_attempts。

最小檢查表

  1. detector 是否只讀畫面, 沒有輸入副作用?
  2. action 是否只完成一次業務提交? 失敗是否拋 WaitFailedException?
  3. 成功後真實可能出現的每個畫面是否都在 next?
  4. 是否真的要再次輸入? 需要才把當前 key 放入 next。
  5. 長動作是否在 action 內部設定自己的業務 timeout?
  6. 已知退出頁面是否使用 transition, 未知畫面是否交給 recovery?
在 GitHub 查看來源 ↗ · 頁面產生時間: 2026年9月24日