開發指南(DEVELOPMENT)

架構

本專案基於 ok-script。應用配置集中在 src/config.py,任務與 Tab 均在此註冊。

Python 模組檔案統一用 snake_case.py,tests/ 中的測試檔案用 test_*.py;類名保持 PascalCase。

框架版本鎖定 ok-script==2.0.6(pyproject.toml + uv.lock 為唯一來源)。

目錄職責

目錄 職責
main.py / main_debug.py 入口(Release / Debug)。啟動前先裝 src/patches/
src/config.py ok-script 應用配置:視窗、OCR、模板匹配、任務與 Tab 註冊
src/core/base_game_task.py 任務基類:暫停感知計時、配置遷移、異常處理、配置分組
src/core/base_mixin/runtime_mixin.py 通用能力庫:解析度對映、點選驗證、YOLO 檢測、畫面穩定判定、鍵鼠
src/core/base_mixin/framework_override_mixin.py 用"同名覆寫 + super()"擴充套件框架方法(不復制框架程式碼)
src/core/detector/ 識別層:把四類識別源(模板 / YOLO / OCR / 按鈕)歸一成統一判據 Hit / Detector,供 Action 生命週期編排使用
src/core/config_migration.py 配置鍵遷移工具(改鍵名時使用,防丟使用者配置)
src/core/global_config_store.py 本專案自建的全域性配置(不走框架的 config['global_configs'])
src/interaction/ 視窗與鍵鼠:GameInteraction(自定義後臺輸入)、Mouse、ScreenPosition、KeyConfig
src/image/ 影像演算法:rotated_template(旋轉模板匹配)、stability(影像指紋)、frame_processes、hsv_config、button_detector(固定 Box 按鈕檢測,無 OCR)、glow_target_detector(可射擊光圈,HSV+圓弧擬合)、treasure_band_detector(開鎖顏色帶,HSV+連通域)、rhythm_detector(音遊音符及長條頭尾)、jenga_detector(祖贊卡之梯的懸掛圖騰與塔頂)
src/yolo/ YOLO 模型註冊(models.py)與 OpenVINO 推理
src/tasks/onetime/ 一次性任務
src/tasks/trigger/ 觸發式任務
src/tasks/test/ 除錯/測試任務
src/tasks/daily/ 多步驟任務的編排(四態統計、失敗標記、分賬號輪次、彙總報告)
src/tasks/account/ 賬號作用域配置儲存
src/gui/ 自定義 Tab(全域性配置頁、賬號配置頁)
src/patches/ 啟動補丁。是對框架內部實現的猴子補丁,升級 ok-script 前必須逐個複核
src/data/feature_list.py 模板名列舉(由標註自動生成,程式碼裡不要寫裸字串模板名)
src/data/lang/ assets/lang/*.json 的讀取器
assets/coco_annotations.json 模板標註(COCO 格式)
assets/lang/ OCR 匹配文本(不是 UI 文案)
i18n/ gettext 目錄(UI 文案)
scripts/ tools/ 語言同步、PO 修復、lang 型別樁生成等工具
tests/ unittest 測試

花瓣音遊:自動演奏

在觸發任務中啟用「自動音遊」,按 F 進入花瓣音遊後自動接管,結束後自動釋放按鍵。 藍色用 Q、紅色用 E、紫色同時按 Q/E;長條持續按住至尾部經過判定點。 遊戲需要保持前臺,程式和遊戲的許可權等級應一致。

  • src/image/rhythm_detector.py:三枚說明圖示作為介面閘門。遊戲 Canvas 保持等比縮放, 軌道居中、說明圖示靠右,不能按截圖的寬高分別拉伸。RhythmLayout 將實際 UI 的 縮放和偏移對映到 1920×1080 的邏輯座標;視窗邊框造成偏移時用任意兩枚圖示校準, 再校驗全部三枚。視窗尺寸變化時重新定位。 HSV + 連通域識別頭尾,用高亮圓形與本輪學習的高通紋理找回與場景粘連的頭部, 再檢查白色按鍵圖案與前景飽和度,排除背景光斑。長條結合橫向顏色支援與淡灰尾符紋理定位, 漸隱區域的顏色邊界只作為長度下界;完整軌道用於測尾部,清晰區用於建軌跡。 頭部被命中特效覆蓋時從右側身體繼續測尾部,圓角擬合拆開粘連的短音符。 只歸一化小 ROI,WGC 的 BGRA 截圖也無需先複製整張畫面。
  • src/tasks/trigger/auto_rhythm_task.py:在清晰區域建立軌跡,多幀直線擬合滾動速度,預測被 命中特效遮擋的音符。速度擬合至少需要三個觀測、80 ms 的時間跨度;未完成擬合時, 關聯範圍隨幀間隔擴大,適應低幀率。使用 perf_counter 和畫面生成時間補償截圖、 識別延遲;按下和長條鬆開使用 UI 配置「輸入延遲 (ms)」,預設提前 50 ms,範圍 0..150 ms。 所有音符共享本幀位移與滾動速度,遮擋軌跡也跟隨共同位移。定時回撥沒有新觀測時保持速度, 清理舊軌跡不能改變已經安排的截止時刻。下一次截圖可能跨過按鍵時刻時, 按實際截圖加識別耗時先服務定時輸入。 軌跡至少有三個觀測才參與按鍵/長按衝突排程,避免單幀身體碎片提前截斷長按。 每次只消耗實際選中的音符,同鍵待鬆開時優先服務松鍵,過期音符不阻塞後續截止時刻。 截圖短暫變慢時,已確認音符保留至預測判定時刻後 100 ms;按滾動速度換算位置容差, 避免固定畫素截止線隨速度改變有效時間窗。 遊戲在長條鬆開後才推進當前音符,因此長按結束前不輸入下一枚音符,無論它是什麼顏色; 根據已擬合的下一枚音符預留 45 ms 松鍵間隙。連續按下至少間隔 60 ms, 避免相鄰 Q/E 單鍵被遊戲合成為紫色雙鍵;重觸發時保留至少 15 ms 抬起態。 長度和短/長分類通過多幀確認,單幀背景粘連或身體缺失不能改寫已確認的長按。 演奏期間通過 self.loop 獨佔任務迴圈,定時等待可由停止事件打斷。
  • src/patches/capture_timestamp_patch.py:為 WGC 保留 SystemRelativeTime,100 ns 單位轉為 與 perf_counter 一致的 QPC 秒數。時間戳在取幀鎖內與返回畫面配對,按呼叫執行緒儲存, 避免截圖預覽覆蓋任務正在使用的觀測時間;取幀失敗時清除舊時間戳。 補丁針對鎖定的 ok-script 2.0.6,升級時複核截圖生命週期; 其它截圖方式使用截圖耗時的中點估計,無法保證相同的時序精度。
  • 停用、暫停、失焦、退出、截圖中斷和異常均清理持鍵;清理直接呼叫互動層,避免框架 check_enabled() 在停用後阻止 key-up。看門執行緒只負責終止輸入,不截圖也不按新鍵。
  • tests/test_auto_rhythm_task.py 覆蓋真實截圖、640×360 到 4K、超寬屏/豎屏/視窗比例、 BGRA、三色短音符、連續同色、遮擋、粘連長條、10..144 FPS、不同速度/處理延遲、 定時輸入與停止清理;test_capture_timestamp_patch.py 驗證 WGC 時間戳保留。 tests/fixtures/rhythm/other_pc_*.png 來自其他電腦的參考錄影,詳見同目錄 README。 截圖只保留識別區域,其餘畫面置黑;測試不連線遊戲,不傳送真實按鍵。

2026-09-29 授權本機實機驗證(1920×1080、ForegroundBitBlt)結果為 259 次 Perfect, Good / Normal / Miss / Wrong 全部為 0,總完成度 99.86%;另用 WGC 實機驗證, 同樣 259 次 Perfect、其餘判定為 0,總完成度 100.00%。參考錄影的 2107 幀音遊畫面 另作離線複核。447 項完整測試通過,其中包含解析度矩陣和時序、停止清理等迴歸; 未修改 src/config.py 的解析度設定。

祖贊卡之梯:自動放置圖騰

當前驗收目標為每輪兩分鐘內完成、零 Miss,允許 Good / Perfect。2026-09-26 的本機機器碼核對見 實際判定與運動公式:接觸面多邊形面積決定 Perfect, 懸掛軌跡為八字,釋放繼承橫向速度,下落使用帶限速的加速度。 2026-09-28 的較早候選已完成 88.3 秒、20/20、零 Miss 的實機投放;隨後仍發現 其他輪次的 Miss 和超時,不能將這一輪作為當前實現穩定通過的證明。 當前候選及不同解析度、截圖方式的整輪驗收記錄見上述文件。

在觸發任務中啟用「自動祖贊卡之梯」,手動進入玩法並保持遊戲前臺。任務根據畫面 自動對齊、放置。介面消失、暫停、失焦、視窗重建、截圖中斷或解析度改變時結束當前跟蹤會話, 保留觸發任務的啟用狀態;畫面恢復或重新進入玩法後重新取樣當前圖騰和塔頂。 每次接管與會話退出都清除舊塔頂高度、造型、運動軌跡和會話模板,支援完成後重開、 中途啟用及程式重啟後接管。手動停用或退出程式時結束任務,停用及異常均釋放按鍵; 視窗重連時仍通過原互動物件釋放已按下的鍵。 不負責從大世界尋找玩法入口、選擇模式或重開結算畫面。

  • src/image/jenga_detector.py:匹配頂部操作圖示,並檢查右下角動畫星星的顏色佔比,確認玩法介面。 用鳥與圖騰的空間關係確認懸掛狀態,用圖騰細節模板尋找最高承接面,座標歸一至 1920×1080。灰度高通過濾模糊背景,並比較同幀前景的紋理密度,防止模糊欄杆進入 模板快取。模板覆蓋尺度與小角度晃動;未知懸掛造型嘗試 從鳥下方清晰輪廓定位,在釋放後儲存有限數量的會話模板供後續塔頂識別。 對高通響應限幅,減少閃光特效的影響;重新定位使用較低解析度,連續跟蹤使用 較高解析度。塔頂搜尋覆蓋橫向 100..1820,避免高層擺動與累計偏移越出舊搜尋區。 鳥頸部在頂部提示條後分成兩段時保留下緣錨點;未知裁圖會定期核對強參考匹配, 已確認的懸掛造型保持至本次釋放。逆光時低對比輪廓補全與頭部位置一致的暗腿, 避免快取只含頭和軀幹的區域性裁圖。 黃色翅膀與鳥頸尺寸相近時,用下方完整圖騰消除歧義。方向光下的參考造型若未達到 單幀強匹配門檻,需三個較強的不同畫面,或五個較弱畫面跨過至少 50px 橫向弧段, 支援同一造型且明顯高於其他造型才確認身份; 固定畫面不增加票數,身份確認不修改已有定位邊界。小於 160px 的快取另核對完整 輪廓,位置一致且延長至少 12px 時恢復腳部,覆蓋舊 25px 延長門檻遺漏的裁短。 完整高度經不同位置的多幀確認後單獨儲存,快取改為跟蹤上部紋理; 同一靜止畫面不建立完整高度。已確認人形或貓頭鷹且上端緊鄰鳥頸時, 可使用本輪軌道測得的畫面比例和原生網格高度;未知造型不強行補齊。 快取雖相關度很高但上端偏離鳥頸時,先回退到完整參考,再嘗試輪廓; 不讓區域性軀幹持續成為新的完整上端。兔子的耳部另保留原有完整輪廓恢復路徑。 塔頂快取帶小角度轉動變體;快取失配時優先重查該造型,通過紋理、位置及置信度 檢查才接受,否則回退到全部造型,不把弱匹配當作有效跟蹤。
  • src/tasks/trigger/auto_jenga_task.py:JengaPlayer 從鳥的橫向軌跡恢復八字相位和 畫素/世界單位比例,完整圖騰底邊獨立測量高度;頂部提示條及光照造成的鳥頸 下緣變化不會進入相位或橫向速度。按原生 1/30 秒固定步先更新重力速度, 用重力 49.05、終端速度 10 及最後一步的實際接觸位置求解下落時間,保留 釋放瞬間的橫向速度。塔頂使用 4 秒週期擬合,並與獨立的幾何時鐘預測交叉核對。 覆蓋至少 2 秒時可擬合圖騰相對鳥的週期橫移,只有誤差更小且振幅合理才使用。 首塊至少收集 2 秒、12 個觀測。後續可保留已測橫向中心和半徑,重新擬合高度與 塔頂;短觀測需至少 0.6 秒、6 個樣本,並增加未來位置誤差及外推放大檢查。 未滿 1.2 秒的短弧另留 22px;在 1.2..2 秒之間連續淡出,並始終計入實際殘差和 外推放大,避免某一幀跨過時間閾值就突然失去保護。投放時刻比較整個時間誤差範圍內的落點, 在相同接觸餘量下允許選擇更穩妥的非零偏移 Good。 塔頂均值偏離已測軌道中心時,僅在完整預算合格的候選中優先向內修正, 小於 0.025 世界單位的差異不修正,超出部分乘以 0.65,單次修正不超過 0.055 世界單位;修正只改變完整接觸預算內的候選排序。 交接方向來自成功放置後新鳥早期的半程,避免依賴固定螢幕方向。 原生 BeginMovement 每次重置相位。塔身明顯擺動且觀測覆蓋兩秒時, 在當前安全候選之外預估下一塊的接觸機會:未知接觸面、三個高度假設、 原生換塊時序、短弧保護及 27 個時序組合參與比較。 下一塊換相位的時刻另覆蓋當前釋放、下落與換塊固定步的不確定性; 未知未來造型的保守預算不合格時,仍可優先選擇明顯改善下一塊接觸的交接。 若一圈內另一個當前安全交接預計能明顯縮短兩塊總等待,最多等待一次; 到達計劃時刻仍須用新觀測重新通過全部當前接觸檢查,計劃過期便丟棄。 等待期間也持續複核未來接觸預算;新觀測使計劃失效時,立即恢復當前安全候選, 避免放過合格的半程後才發現遠期計劃無法執行。 預判只選擇時機,不能擴大 Good 範圍或繞過識別、殘差、時效與按鍵截止檢查。 未可靠觀測早期方向時保持原有選擇。 避免允許的小偏移連續向同一側累積,令高層錯過許多擺動週期。 不足一個原生鳥軌道週期(2π / 1.6 秒)的遮擋保留記錄,並繼續通過當前幀、 擬合殘差與雙時鐘檢查;垂直鏡頭跳變重測高度。更長的識別間斷清除歷史時鐘,保留已測 橫向中心與半徑並用新短弧重新驗證;畫面凍結或場景退出清除全部運動先驗。 高度擬合要求至少 60% / 6 點落在 25px 內的同一簇,且當前幀屬於該簇。 在未來最多 250 ms 內求解相交時刻;沒有中心交點時也檢查最近落點。 Good 接觸範圍內才可釋放,按投放/接觸固定步時間誤差與較小接觸面的半面積 檢查 Good 餘量;未知造型使用保守尺寸。預測或實際輸入遲到時跳過按鍵。 下落時距的不確定性同時改變懸掛物的橫向漂移和承接面的預測時刻; 飛行區間從首次幾何接觸覆蓋到當前 1/30 秒物理步結束,使用半步中心及兩端, 另按衝擊速度加入可見網格/碰撞面邊緣餘量。 承接面另有獨立的接觸幀誤差。釋放、飛行和承接時刻的 27 個組合均參與預算。 每次釋放後從兩幅不同且有效的畫面確認 Perfect / Good / Miss。只有成功判定才將釋放造型 作為新塔頂;Miss 或判定缺失時保留已驗證的圖形模板,重新識別實際承接面並重新取樣運動, 清除未完成的釋放鎖定。連續識別缺失超過一個鳥軌道週期時清理跟蹤約束重新定位, 不因舊塔頂造型或高度約束一直等待。取幀異常不會停用觸發任務;無單輪超時或失敗鎖定。 已排定的按鍵在暫停恢復、停用再啟用或截圖/互動物件更換後取消,避免舊預測繼續投放。 成功放置後,新一塊需有消失重現、足夠橫移或雙幀成功判定的證據,且釋放後至少等待 0.8 秒, 避免重複按鍵。判定輪詢已消耗消失畫面時,不再要求新塊遠離舊釋放座標。 Miss / 判定缺失後的重新接管仍需至少兩秒的新軌跡,不能立即再次放置。
  • 實際截圖的操作提示為 F / 滑鼠左鍵,任務使用全域性 Interact Key 對 F 的對映。 不照搬資料包 demo 的空格鍵、十層目標或 88 px/m;截圖包含 0/20、2/20、 19/20,任務也不把這些分數當作固定層數或結束條件。
  • _input_lead_ms=45 保留原有影像運動時序補償的語義,包含輸入與剛體渲染插值的影響; 在中心值周圍至少保留半個固定步,較長的額外補償和實際相位抖動擴大範圍。 WGC 畫面生成時間另外補償截圖、識別耗時。 陳舊或重複的 WGC 時間戳不能作為新觀測;無時間戳時使用取幀起止的中點估計。 _fall_time_ms、_drift_time_ms 為相容使用者歷史資料保留,物理預測已不讀取這兩個 固定時距。實際畫素比例來自本輪擬合的軌跡橫向振幅 / 原生半徑 1.25。
  • assets/jenga/ 為參考截圖裁出的圖形模板;tests/fixtures/jenga/ 為遮去身份區域 的真實截圖。測試覆蓋多解析度、背景干擾、新造型回退、雙向塔身相位、按鍵清理、 丟幀和釋放去重,以及 Miss / 判定缺失後的連續放置、結算後重開、暫停恢復、 視窗與截圖重建、停用再啟用、高層舊快取清理和中途接管。

驗證方式:使用 Python 呼叫真實 AutoJengaTask.run()、框架前臺 BitBlt 截圖與 GameInteraction 按鍵,並記錄逐幀時間戳、釋放記錄和錄影。本次驗證使用 Python。 靜態迴歸包含晝夜光照、動畫星星和搖擺塔頂;時序迴歸包含低幀率、短暫遮擋、 雙向運動與釋放後的橫移。介面消失只表示暫停投放,不單獨作為挑戰成功證據。

2026-09-26 實機結果:放置達到 20/20,結算顯示 CHALLENGE COMPLETE、兩星; 任務在操作 HUD 消失後停止輸入,補錄結算確認通過。結算截圖儲存在 tests/fixtures/jenga/complete.png(身份區域已遮去)。此次驗證覆蓋晝夜變化、人形、 貓頭鷹、兔子,以及高層快速搖擺;兩星結果不代表全部放置均為完美判定。

寶箱開鎖:顏色帶檢測與開鎖任務

開鎖小遊戲右側軌道里的顏色帶是低飽和、高亮度、豎向細長的矩形塊,同一區域還有 兩類干擾:貼 ROI 邊緣的軌道邊框、矮胖的橫向鑰匙。前者靠「不貼邊」排除,後者靠長寬比 排除,都不是顏色問題,所以統一走連通域 + 形狀判據。

檢測器 src/image/treasure_band_detector.py

流程:Box → ROI → HSV inRange(S≤80, V≥190) → 3x3 开运算 → 连通域 → 形状过滤 → 按 Y 排序

預設判據(1920×1080 實測):area≥300、w≤30、h≥30、h/w≥2.0、不貼 ROI 左右邊緣。

from src.image.treasure_band_detector import TreasureBandDetector

detector = TreasureBandDetector()                 # 复用实例,不要逐帧新建
roi = self.box_of_screen(0.6797, 0.2324, 0.7021, 0.7778)

bands = detector.find(frame, roi)                 # 命中,按 Y 升序
results = detector.analyze(frame, roi)            # 全部连通域,含被过滤项与原因
detector.y_ranges(bands)                          # [(286, 391), (552, 605), (672, 725)]
TreasureBandDetector.hit_by_center_y(bands, 580)  # 钥匙 center_y 落在哪条带
成員 說明
find(frame, box) 命中的幀座標 Box 列表(可直接 click() / draw_boxes())
analyze(frame, box) 全部連通域的 BandDetection,failed 給出過濾原因,用於校準閾值
presence(frame, box) Box 內目標顏色畫素佔比 0~1,存在性判定專用
y_ranges(boxes) / hit_by_center_y(boxes, y) Y 區間 / 鑰匙命中

閾值集中在 BandThresholds,用 with_(...) 生成副本。尺寸類閾值不隨解析度自動縮放, 呼叫方按 resolution_scale() 自行換算(開鎖任務裡已做)。

存在性判定為什麼不用 find()

鑰匙是橫向結構,壓在條帶上會把它切成上下兩段,連通域各自變小後被形狀判據拒絕, 結果是「明明還在卻被判消失」。所以判斷條帶在不在只統計 bbox 內目標顏色畫素的 佔比(presence),不看形狀。真實截圖實測:條帶在 0.99,被抹掉後 0.00, 閾值 0.15 有充足餘量。

開鎖任務 src/tasks/trigger/treasure_unlock_task.py

觸發式任務,狀態機跨 run() 呼叫保持:

WAIT_TREASURE ──treasure_icon──> CALIBRATING_BANDS ──连续多帧稳定──> UNLOCKING
                                                                        │
                              钥匙 Y 命中缓存 bbox → 点击 → 稳定消失 → 移出 active
                                                                        ↓
   FINISHED <── 持续无条带 ── COMPLETION_CHECK <──── active 空 ─────────┘

要點:

  • 校準結果是後續唯一的定位基準。開鎖階段不再重建座標,只用快取 bbox 做 「鑰匙 Y 匹配」和「存在 / 消失判定」。
  • 穩定判定一律用連續狀態而非單幀:校準用「連續 N 幀佈局一致」,消失與完成確認用 wait_until(..., settle_time=...)(複用框架,就是「條件持續成立一段時間」)。
  • 點選失敗到上限後把該條帶移到隊尾,避免死磕一條;active 清空後還要做 完成確認(完成确认时长),期間條帶重現就回 UNLOCKING。
  • 寶箱 UI 只作為「已退出介面」的輔助訊號,不能反過來阻塞結束,否則圖示常駐時 會永遠卡在完成確認裡(有 2 倍時長上限兜底)。
  • treasure_key_icon 必須顯式傳搜尋 box:find_feature 不傳 box 時只搜 coco 標註位置 ±variance(約 4px),而鑰匙會沿軌道上下滑動,預設框根本搜不到。

除錯任務 src/tasks/test/test_treasure_band_task.py 會在覆蓋層即時畫框 (紅=命中 / 綠=ROI / 藍=被過濾),用來肉眼校準閾值。

固定 Box 按鈕檢測(中央文本帶,無 OCR)

部分按鈕(如「跳過劇情 / 確認」)底色為深灰 RGB(50,50,53),與遊戲背景幾乎一致, 模板匹配和「整塊顏色判定」都不穩定。這類按鈕的可靠特徵是按鈕中央的文字。

src/image/button_detector.py 檢測「Box 中央是否存在一條顏色落在指定 HSV 區間內的文本帶」, 只有兩個顏色引數:

  • text_hsv —— 文字(前景)顏色區間,主特徵;
  • backdrop_hsv —— 底色(背景)顏色區間,可選輔助特徵。

入口掛在 RuntimeMixin 上:

box = self.box_of_screen(0.575, 0.61, 0.64, 0.64)
if result := self.find_button(box):      # 默认 = 深灰底 + 亮色文字
    self.click(result)                   # 结果可直接点击

# 直接传颜色
self.find_button(box, text_hsv=((0, 0, 170), (180, 100, 255)))
self.find_button(box, text_hsv=((0, 0, 0), (180, 255, 90)),
                      backdrop_hsv=((0, 0, 170), (180, 80, 255)))

# 或封装成语义常量复用
from src.image.button_detector import ButtonThresholds
SKIP_BUTTON = ButtonThresholds.for_button(
    ((0, 0, 170), (180, 100, 255)),   # 文字:亮色
    ((0, 0, 30), (180, 80, 110)),     # 底色:深灰
    name="skip_button",
)
self.find_button(box, thresholds=SKIP_BUTTON)
方法 返回 說明
find_button(box, frame=None, thresholds=None, text_hsv=None, backdrop_hsv=None) Box | None 命中返回可直接 click() 的 Box
find_buttons(boxes, ...) Box | None 多個候選 Box,返回首個命中
wait_button(box, time_out=5, ...) Box | None 以視覺狀態等待,不用固定延時
analyze_button(box, ...) ButtonDetection 含中間指標與未命中原因,用於校準閾值
button_detector(thresholds=None) ButtonDetector 快取的檢測器例項

預設(ButtonThresholds 的類方法 / 常量):

預設 文字 HSV 底色 HSV 適用
DARK_BUTTON_THRESHOLDS / DEFAULT_BUTTON_THRESHOLDS(預設) (0,0,170)~(180,100,255) (0,0,30)~(180,80,110) 深灰 / 黑底 + 亮字
LIGHT_BUTTON_THRESHOLDS (0,0,0)~(180,255,90) (0,0,170)~(180,80,255) 亮 / 白底 + 深字(預設校驗底色)
for_button(text_hsv, backdrop_hsv, name=...) 任意 可選 封裝任意語義按鈕

要點:

  • 底色區間只在 Box 不夠貼合按鈕時才明顯生效:真實 1080P 截圖滑窗 884 個位置實測, 只用文字特徵誤報 17,加上底色區間後 0,命中率不變,單次 +0.07 ms。按需開啟。
  • 所有閾值集中在 ButtonThresholds,用 with_(...) 生成改過的副本,不修改預設值。
  • 傳入的 Box 要貼合按鈕;Box 遠大於按鈕時文本帶相對過薄,會被形狀判定拒絕。

以視覺狀態等待用 wait_button(box, time_out=5, ...),不要用固定延時。若還需要「等到 → 點選 → 驗證結果」的完整流程,見下一節的 wait_action_result。

Action 生命週期:等條件 → 動作 → 驗證

遊戲裡大量操作是同一個形狀:看到某個東西 → 動它 → 確認預期結果出現了。 這類「識別 + 動作 + 再識別」的流程收斂成兩個 API:

  • 識別層 src/core/detector/ —— 把模板 / YOLO / OCR / 按鈕四類識別源歸一成統一判據 Detector.detect(frame) -> Hit | None;
  • 編排層 RuntimeMixin.wait_action_result / wait_expectation —— 負責「何時執行動作、何時判定成功」。

兩層正交:識別層只管「這一幀有沒有」,編排層只管「什麼時候做、做幾次」。

from src.core.detector import TemplateDetector
from src.data.feature_list import FeatureList

# A≠C:看到宝箱图标 → 点击 → 等解锁界面出现
self.wait_action_result(
    condition=TemplateDetector(FeatureList.treasure_icon),   # A 前置条件
    action=lambda hit: self.click(hit.box),                  # 主 Action
    expect=TemplateDetector(FeatureList.unlock_ui),          # C 预期结果
    time_out=5, expect_time_out=1.5, max_attempts=2,
)
階段 觸發條件 行為
① 等條件 condition 不為 None 時 condition 在 time_out 內命中才繼續;為 None 時跳過等待直接進入階段 ②;未命中 → 返回 False(raise_if_not_found=True 則拋 WaitFailedException)
② 動作 + 驗證 條件命中或 condition 為 None action(hit) → expect 驗證;未通過則重試,最多 max_attempts 次
③ 持續階段 僅當給了 while_condition 且 repeat_action while_condition 持續命中就重複 repeat_action;未命中立即停止;每輪後繼續檢查 expect,命中即返回 True

只等一個結果(只用到 C)則走 wait_expectation:

hit = self.wait_expectation(TemplateDetector(FeatureList.main_ui), time_out=2.0)

三條最容易踩的契約:

  • expect=None 表示「動作成功即返回」 —— 呼叫方宣告該動作沒有可驗證的結果。
  • settle_time 預設 0(命中一次即可);非 0 的語義是「每次判定都要求連續成立夠時長」, 而不是「總共等這麼久」—— 會顯著增加耗時,只在「等 UI 穩定下來再確認」時開啟。
  • 條件命中與動作之間會重取一幀,防止會動的目標在 settle_time 期間位移。

📖 完整參考見 action_lifecycle.md —— 含全部識別器引數(pick 策略、 mask_function、use_find_one 等)、組合器語義、典型用法配方與真實落地樣例。

迴圈與重試規範(self.loop)

在任務或 Mixin 中進行帶超時的輪詢、重試或多幀檢測時,嚴禁裸寫 while self.active_time() - start < time_out:、while time.monotonic() < deadline: 並手動 self.next_frame(),必須統一使用 self.loop(定義於 BaseGameTask):

為什麼使用 self.loop

  1. 暫停感知(Pause-aware):內部基於 self.active_time() 計算活躍耗時,任務被使用者暫停期間計時自動凍結,避免無謂超時。
  2. 自動取幀驅動:yield_frame=True(預設)時每次迭代自動呼叫併產出下一幀(self.next_frame()),迴圈體內無需手動寫 self.next_frame()。若不需要幀(或動作內部自行取幀),傳 yield_frame=False(產出 None)。
  3. 超時行為可控:
    • raise_if_time_out=True(預設):迴圈超時未提前 break/return 時丟擲 TimeoutError('Loop time out.')。
    • raise_if_time_out=False:超時後正常退出迴圈,可在迴圈後執行兜底邏輯或丟擲業務異常(如 WaitFailedException)。
    • raise_if_time_out=ExceptionInstance 或 ExceptionClass:超時直接丟擲指定的自定義異常。

標準用法模式

  • 模式 A(最常用):預設取幀,找到目標即 break,超時自動拋 TimeoutError

    for frame in self.loop(time_out=10):
        if target := self.find_one(FeatureList.some_btn, frame=frame):
            self.click(target)
            break
    
  • 模式 B:超時後執行特定兜底或拋業務異常(WaitFailedException)

    for frame in self.loop(time_out=10, raise_if_time_out=False):
        if self._check_success(frame):
            return
    raise WaitFailedException("操作超时")
    
  • 模式 C:不需要自動取幀的時間控制迴圈

    for _ in self.loop(time_out=10, yield_frame=False, raise_if_time_out=False):
        if self.try_step():
            return
    
  • 模式 D:直接指定超時丟擲的異常型別或例項

    for frame in self.loop(time_out=5, raise_if_time_out=WaitFailedException("未找到确认按钮")):
        if confirm := self.find_confirm(frame=frame):
            self.click(confirm)
            break
    

輔助星結:可射擊光圈檢測與輔助任務

星結時(智慧種族與奇波通過星結卡締結契約),游標指向目標會在螢幕中心出現一道 彩色光圈作為可射擊標識:綠(100%)、金黃(48.3%)、橙紅(1.4%)對應不同命中機率。

檢測器 src/image/glow_target_detector.py

關鍵是這道光圈是同一圓上的一段弧,不是閉合圓環,所以不能靠「有沒有內孔」判斷。 1920×1080 三張樣本實測:圓心 ≈ 螢幕中心 (963, 541)、半徑 ≈ 52px、圓擬合殘差 < 1.5px、 弧宽 3~6px —— 三张是同一个圆。

流程:Box → ROI → HSV inRange(S≥110, V≥190) → 3x3 闭运算 → 连通域 → 最小二乘圆拟合 → 判据过滤

判据(默认,GlowThresholds):

判据 默认值 作用
残差 ≤ max_residual 且 ≤ max_residual_ratio × 半径 6px / 0.12 「是不是圆」的核心,噪声与色块拟合不出低残差小圆
内切厚度 ≤ max_thickness_ratio × 半径 0.35 排除实心圆盘(见下)
半径 ∈ [min_radius, max_radius] 30 ~ 90 直线会拟合出半径上千的圆,由上限挡掉
跨度(外接框对角线)≥ min_span 40 排除小碎点
圆心落在 ROI 内 开 避免误检画面别处的大圆弧
from src.image.glow_target_detector import GlowTargetDetector

detector = GlowTargetDetector()                    # 复用实例,不要逐帧新建
roi = self.box_of_screen(0.4724, 0.4426, 0.5365, 0.5611)

result = detector.analyze(frame, roi)              # 含残差 / 半径 / 厚度 / 未命中原因
box = detector.find(frame, roi)                    # 命中的帧坐标 Box,可直接 click()

两个容易踩的点:

  • 厚度判据不能省。 实心圆盘的外轮廓本身就是一个完美圆,只比残差会把圆盘判成环; 加上「内切厚度 ≤ 0.35×半径」才能区分(真实弧带 3px / 半径 52px ≈ 0.06,圆盘 ≈ 1.0)。
  • 厚度必须在连通域像素上算,不能用轮廓填充。 闭合光环一填充就变成实心盘, 厚度立刻从「半环宽」跳到「半径」,把真目标误杀。

点击位置取拟合出的圆心,不是外接框中心:弧只是圆的一段(往往只有右侧 90°~120°), 框中心会偏向弧那一侧,而圆心就是屏幕准心。GlowDetection.center 给出这个坐标。

require_arc=False 可退回「最大彩色团块」模式,用于提示本身是实心色块的画面。

触发式任务,run() 里三道闸门依次短路:

star_link_icon 存在? ──否──> 返回(不在星結介面,不干預)
        │ 是
        ↓
  圓弧擬合命中? ──否──> 清空連續命中計數,返回
        │ 是
        ↓
  連續命中幀數 / 點選冷卻 ──> 單擊左鍵(圓心)
  • 前置闸门 find_feature(feature_name=FeatureList.star_link_icon, frame=frame): 屏幕中心那片区域彩色元素不少(技能特效、场景光斑都可能拟合成圆弧),用「场合」 把检测限定在星结界面内。这里显式传 frame 复用同一帧,不用 find_one()(会再取一帧)。
  • 连续命中帧数过滤光圈淡入淡出的过渡帧;点击冷却防止一次提示被点成连击。
  • 尺寸类阈值(面积按平方、半径与跨度按线性)随 resolution_scale() 换算。

任务配置项的显示与隐藏

任务卡片上该出现哪些配置项,由两条 ok-script 机制决定。本项目按下面的分工使用:

配置项性质 处理方式 例子
用户该调的开关 普通键名,正常显示 检测绿色、记录点击日志
数值调优项 键名前加 _,留在 default_config 但不渲染 _连续命中帧数、_条带存在阈值
调试任务自己的阈值 正常显示(调阈值就是它的用途) V 下限、最大宽度

用 _ 前缀隐藏调优项

框架的渲染逻辑是 if not key.startswith('_')(ok/ui/qt/tasks/ConfigCard.py), 所以键名以 _ 开头就不会出现在 UI 上,但:

  • 键仍在 default_config 里 → 默认值保留,configs/*.json 里仍会写入,仍可从日志恢复
  • 代码里照常读:self.config.get("_连续命中帧数", 1)
  • ok/core/config_schema.py 也会跳过它们,不会漏进自动生成的 schema

改键名时记得同步 config_description 的键,否则说明会挂在一个不存在的键上。

调优项属于「不该让用户操心」的参数,隐藏它们不写迁移表 —— 旧键会作为孤儿留在 用户的 JSON 里(无害),值回落到新默认值。只有当某个参数需要保住用户已调过的值时, 才按下一节加迁移表。

调试任务只在 debug 模式露面

框架的三个任务列表(OneTimeTaskTab / TriggerTaskTab / ScheduleTaskTab)都会用 getattr(task, 'visible', True) 过滤,所以纯调试任务在 __init__ 里写:

self.visible = self.debug      # self.debug -> executor.debug,main_debug.py 下為 True

正式业务任务不要设 visible,任何模式下都应可见。当前按此约定归类的调试任务是 TestScreenshotTask、TestInteractionTask、TestTreasureBandTask、TestUINavigateTask。

两条契约都由 tests/test_task_config_visibility.py 固化,改配置项时会被它挡住。

配置键迁移

修改 default_config 键名时必须先添加迁移表(同一提交完成):

class MyTask(BaseGameTask):
    config_key_migrations = {"舊鍵": "新鍵"}

BaseGameTask.load_config 会沿 MRO 收集所有迁移表并执行,详见 src/core/config_migration.py。 完整的改键名流程(含 i18n 与文档同步)见 AGENTS.md。

i18n

本项目有两条互不混用的翻译链路:

  • UI 文案:代码用 self.tr("中文"),msgid 写入 i18n/*/LC_MESSAGES/ok.po, 再编译 .mo:

    python tools/task_i18n_helper.py compile --i18n i18n   # 編譯全部 .mo
    python tools/task_i18n_helper.py check --i18n i18n     # 查重複 msgid(有則退出碼 1)
    python scripts/validate_all.py                         # 編譯 + 查空 msgstr
    

    新增文案时,先用 scan 把该进 .po 的字符串列出来,避免漏翻:

    python tools/task_i18n_helper.py scan --task src/tasks/onetime/daily_task.py
    
  • OCR 匹配文本:放进 assets/lang/<模块>.json,每 key 下 6 种语言节点, 代码用 self.lang.<模块>.<key> 读取。

其它相关工具:

python tools/gen_lang_stubs.py          # 重新生成 src/data/lang/_lang_typed.py 型別樁
python scripts/lang_fill_missing.py     # 補全 assets/lang/*.json 缺失語言(--dry-run 可預覽)
python tools/repair_po_locales.py --apply   # 修復 .po 中語言寫錯的條目

测试

  • 测试位于 tests/,使用 unittest。
  • 全部跑:uv run python -m unittest discover -s tests 或逐文件跑:./run_tests.ps1(CI 在打 tag 时也跑它)。

⚠️ 不要用 python -m ok.test.RunTests —— 它在跑完测试后会在 ok.quit() 处抛 AttributeError,退出码非 0。

写视觉/任务逻辑的测试时用框架的 TaskTestCase + set_image()(静态图驱动), 它可以断言"识别结果"与"决策方向",但不能断言真实输入效果。

运行

uv sync
uv run python main.py          # Release
uv run python main_debug.py    # Debug(更多日誌、overlay、熱過載)

命令行参数只有三个:-t <1 起的序号>、-e(跑完退出)、-h(headless)。 注意 -t 只接受序号,不接受任务名。

发布

  • 发版流程:.github/workflows/release.yml,支持手动触发 workflow_dispatch(提供主版本/次版本/补丁版本递增选项,自动给主分支 HEAD 打 tag),也支持直接推送 tag(v*)。 单个工作流全自动完成版本计算、打 tag、测试、同步更新仓库、用 pyappify 打包并发布 Release。

  • 更新仓库同步仍在 Windows 上运行:checkout 前为工作区及其父目录启用 NTFS 大小写敏感。checkout 后通过 GITHUB_ENV 为后续 Git 子进程强制 core.ignorecase=false,并设置 core.hooksPath。目标仓库 clone 完成时,scripts/release/hooks/post-checkout 会根据源工作区白名单文件的实际大小写,移除目标 Git 索引中已不存在的路径。这样即使目标目录未继承大小写敏感属性,git add . 仍能正确提交 Key.py → key.py,并清理旧仓库中的重复大小写条目。tests/test_release_workflow.py 用临时 Git 仓库和不区分大小写的目标目录验证这两种情况及推送结果。

  • 启动器图标统一使用 icons/icon.png:pyappify.yml 的 icon 用于启动器界面;发布工作流在调用 PyAppify Action 前,自动生成包含 16 / 24 / 32 / 48 / 64 / 128 / 256 像素尺寸的 icons/icon.ico,供 Windows 启动器 EXE 和安装包使用。更换图标时只需更新 PNG。

  • 发版串行执行,使用 queue: max 保留最多 100 个等待任务,后来的 tag 不会替换已有的排队任务。

  • 手动发版会在 tag 注释中记录 Release-Run(当前工作流 run 的 URL)。完整重跑时复用该 tag 和原提交,不再次递增版本或切换到最新主分支;重建时覆盖同名构建产物。

  • requirements.txt 是 pyproject.toml + uv.lock 的派生产物,不要手改。 依赖变更后重新生成:

    uv export --no-hashes --no-dev --output-file requirements.txt
    

    (CI 用 pip install -r requirements.txt 裝依賴,不一致會導致打包失敗。)

目錄約定

  • 新增任務類必須註冊進 src/config.py,否則 UI 中不可見。
  • 任務/配置相關文件與程式碼同步更新。
在 GitHub 查看來源 ↗ · 頁面產生時間: 2026年11月4日