切換賬戶流程分析
同步日期:2026-07-19 基於
ok-script框架(.venv/Lib/site-packages/ok)+ 本專案程式碼
一、整體架構
flowchart TD
A[AccountConfigTab] --> B[account_scope_store.py]
B --> C[configs/account_scoped_overrides.json]
C --> D[AccountMixin]
C --> E[AccountOverrideMixin]
D --> F[BaseEfTask.iter_multi_account_context]
E --> G[config.get 账号覆盖]
F --> H[login_flow]
H --> I[login_screenshot + login_ocr]
I --> J[click_text 切换账号]
┌─────────────────────────────────────────────────────────────┐
│ GUI 配置层 │
│ AccountConfigTab (PySide6 + qfluentwidgets) │
│ 账号列表管理 / 账号-任务覆盖编辑 │
└──────────────────┬──────────────────────────────────────────┘
│
┌──────────────────▼──────────────────────────────────────────┐
│ 数据持久化层 │
│ account_scope_store.py │
│ configs/account_scoped_overrides.json │
│ 账号ID注册表 / 覆盖字典 / 文件缓存 + 线程锁 │
└──────────────────┬──────────────────────────────────────────┘
│
┌──────────────────▼──────────────────────────────────────────┐
│ Mixin 层(运行时注入) │
│ AccountMixin (继承 LoginMixin) │
│ │ - 添加"多账户模式"、"多账户独立配置"配置项 │
│ │ - get_account_list() 解析任务页账号列表 │
│ AccountOverrideMixin │
│ - 首次绑定 config.get() → _config_get_with_account_override│
│ - 运行时按 current_account_id 读取覆盖 │
└──────────────────┬──────────────────────────────────────────┘
│
┌──────────────────▼──────────────────────────────────────────┐
│ 任务执行层(框架核心) │
│ BaseEfTask.iter_multi_account_context() │
│ │ - 读取"多账户模式"开关和账号列表 │
│ │ - 循环调用 set_current_account() + login_flow() │
│ DailyTaskRunner / DeliveryTask 业务循环 │
└──────────────────┬──────────────────────────────────────────┘
│
┌──────────────────▼──────────────────────────────────────────┐
│ 登录切换层(OCR + 鼠标点击) │
│ login_flow() │
│ 登出 → 选最近账号 → 点登录 → 确认 │
│ click_text() │
│ login_screenshot(自定义截图) + pyautogui.click(自定义点击) │
└─────────────────────────────────────────────────────────────┘
二、函式呼叫鏈逐級分析
flowchart TD
A[iter_multi_account_context] --> B[get_account_list]
B --> C[resolve_account_id]
C --> D[set_current_account]
D --> E[_bind_account_aware_config_get]
E --> F[login_flow]
F --> G[ensure_main / logout]
G --> H[click_text 最近账号]
H --> I[_confirm_logged_in]
2.1 入口:iter_multi_account_context() — 賬號迴圈排程
定義位置: src/core/BaseEfTask.py :: BaseEfTask.iter_multi_account_context
來源: ✅ 本專案實現(框架 BaseTask 無此概念)
iter_multi_account_context(repeat_times, empty_accounts_message, ...)
│
├─ self.config.get("多账户模式") → 框架 BaseTask.config 字典
│ [.venv/Lib/site-packages/ok/task/task.py]
│
├─ self.get_account_list() → 本项目
│ └─ [`AccountMixin.get_account_list`](../../src/tasks/account/account_mixin.py)
│ └─ resolve_account_id() → 本项目
│ └─ [`account_scope_store.resolve_account_id`](../../src/tasks/account/account_scope_store.py)
│
├─ self.set_current_account(username, account_id) → 本项目
│ └─ [`BaseEfTask.set_current_account`](../../src/core/BaseEfTask.py)
│ └─ _bind_account_aware_config_get() → 本项目
│ └─ [`AccountOverrideMixin._bind_account_aware_config_get`](../../src/core/base_mixin/account_override_mixin.py)
│
└─ self.login_flow(username) → 本项目
└─ [`LoginMixin.login_flow`](../../src/tasks/mixin/login_mixin.py)
2.2 login_flow() — 實際切換賬號流程
flowchart TD
A[login_flow username] --> B[确保主界面]
B --> C[打开登出入口]
C --> D[确认登出]
D --> E[点击最近账号]
E --> F[按用户名后四位匹配目标账号]
F --> G[点击登录]
G --> H{确认登录成功}
H -->|成功| I[返回任务流程]
H -->|失败| J[记录失败并重试或中止]
定義位置: src/tasks/mixin/login_mixin.py :: LoginMixin.login_flow
來源: ✅ 本專案完全自實現
login_flow(username)
│
│ 【阶段1:确保已登出】
├─ self.wait_ocr(ms, box=bottom_left) → 框架 OCR 引擎
├─ self.ensure_main() → 本项目封装 [`game_flow_mixin.py`](../../src/core/base_mixin/game_flow_mixin.py)
│ └─ 最终 → 框架 BaseTask
├─ self.find_one(fL.main_out) → 本项目封装 [`runtime_mixin.find_one`](../../src/core/base_mixin/runtime_mixin.py)
│ └─ 最终 → 框架 BaseTask.find_one()
├─ self.click(result) → 本项目封装 [`runtime_mixin.click`](../../src/core/base_mixin/runtime_mixin.py)
│ └─ → 框架 BaseTask.click()
│ └─ → [`EfInteraction.click`](../../src/interaction/EfInteraction.py#L39) ✅ 本项目重写
│
│ 【阶段2:找登出按钮并登出】
├─ self.find_feature(fL.logout) → 本项目封装 [`runtime_mixin.find_feature`](../../src/core/base_mixin/runtime_mixin.py)
│ └─ 最终 → 框架 BaseTask.find_feature()
├─ self.click(result[0]) → 同上
├─ self.wait_click_feature(fL.log_out_confirm) → 框架 BaseTask.wait_click_feature()
│
│ 【阶段3:切换到目标账号】
├─ self.click_text(re.compile("最近")) → ✅ 本项目实现 [`login_mixin.click_text`](../../src/tasks/mixin/login_mixin.py)
├─ self.click_text(re.compile(username[-4:])) → ✅ 同上(当前实现直接使用后四位)
├─ self.click_text("登录") → ✅ 同上
│
│ 【阶段4:确认登录成功】
└─ self._confirm_logged_in() → 本项目 [`login_mixin._confirm_logged_in`](../../src/tasks/mixin/login_mixin.py)
└─ self.find_feature(fL.logout) → 框架
2.3 click_text() — OCR 找字 + 點選(核心登入互動)
定義位置: src/tasks/mixin/login_mixin.py :: LoginMixin.click_text
來源: ✅ 本專案完全自實現,不經過框架點選鏈路
click_text(match, box, need_wait_disappear, success_match)
│
│ 【截图】
├─ self.login_ocr(match, box, need_active=False) → 本项目 [`game_flow_mixin.login_ocr`](../../src/core/base_mixin/game_flow_mixin.py)
│ │
│ ├─ self.login_screenshot() → ✅ 本项目 [`game_flow_mixin.login_screenshot`](../../src/core/base_mixin/game_flow_mixin.py)
│ │ └─ capture_window_by_screen(self.hwnd.hwnd) → ✅ 本项目 [`src/image/login_screenshot.py#L55`](../../src/image/login_screenshot.py#L55)
│ │ pyautogui.screenshot() + win32gui.ClientToScreen 裁剪
│ │ ❌ 完全绕过框架的 DesktopDuplication/BitBlt/WGC 截图体系
│ │
│ └─ super().ocr(frame=img, ...) → 框架 BaseTask.ocr()
│ ↑ 使用自定义截图帧,调用框架 OCR 接口;当前 `src/config.py` 配置为 `onnxocr`
│ ✅ 复用框架的文字识别引擎
│
│ 【点击】
└─ run_at_window_pos(self.hwnd.hwnd, pyautogui.click, x, y) → ✅ 本项目 [`Mouse.py#L318`](../../src/interaction/Mouse.py#L318)
│
├─ win32gui.ClientToScreen(hwnd, (x, y)) # 坐标转换
└─ pyautogui.click(screen_x, screen_y) # ❌ 直接 pyautogui
# 完全绕过框架点击系统
# 不走 self.click() / EfInteraction.click()
2.4 login_screenshot() — 自定義截圖
定義位置: src/core/base_mixin/game_flow_mixin.py :: GameFlowMixin.login_screenshot
來源: ✅ 本專案獨立實現
login_screenshot(need_active=True)
│
├─ self.active_and_send_mouse_delta() → 本项目 [`Mouse.py`](../../src/interaction/Mouse.py)
│
└─ capture_window_by_screen(self.hwnd.hwnd) → ✅ 本项目 [`src/image/login_screenshot.py#L55`](../../src/image/login_screenshot.py#L55)
│
├─ win32gui.ClientToScreen(hwnd, (0, 0)) # 获取客户区左上角屏幕坐标
├─ win32gui.GetClientRect(hwnd) # 获取客户区尺寸
├─ pyautogui.screenshot() # 全屏截图(PIL.Image)
└─ cropped = screen.crop((left, top, ...)) # 裁剪窗口客户区
❌ 完全不经过框架 capture 体系
(框架使用 DesktopDuplicationCaptureMethod / BitBltCaptureMethod / WGC)
2.5 login_ocr() — 登入場景 OCR
定義位置: src/core/base_mixin/game_flow_mixin.py :: GameFlowMixin.login_ocr
來源: 🔀 混合(自定義截圖 + 框架 OCR 引擎)
login_ocr(match, box, ...)
│
├─ self.login_screenshot() → ✅ 本项目 [`game_flow_mixin.login_screenshot`](../../src/core/base_mixin/game_flow_mixin.py)(自定义截图源)
│
└─ super().ocr(frame=img, ...) → 框架 BaseTask.ocr()
│ [.venv/Lib/site-packages/ok/task/task.py]
└─ self.ocr_fun(lib)()
└─ `src/config.py` 的 `ocr.lib = "onnxocr"` → 框架 `onnx_ocr`
2.6 find_feature() / find_one() — 模板匹配
定義位置: src/core/base_mixin/runtime_mixin.py :: RuntimeMixin.find_feature / RuntimeMixin.find_one
來源: 🔀 本專案封裝 + 框架引擎
find_feature(feature_name, ...)
│
├─ self.get_feature_by_resolution(name) → 本项目(分辨率映射)
│
└─ super().find_feature(...) → 框架 BaseTask.find_feature()
│ [.venv/Lib/site-packages/ok/task/task.py]
└─ self.executor.feature_set.find_feature(self.executor.frame, ...)
↑ 使用框架的 executor.frame(框架截图)做模板匹配
✅ 截图源和匹配引擎全部来自框架
find_one(...)
└─ self.find_feature(...) → 同上
2.7 click() — 通用點選(非 click_text)
定義位置: src/core/base_mixin/runtime_mixin.py :: RuntimeMixin.click
來源: 🔀 本專案封裝 + 本專案 EfInteraction
click(x, y, ...)
│
├─ self.find_danger() → 本项目 [`runtime_mixin.py`](../../src/core/base_mixin/runtime_mixin.py)(危险态检测)
│
└─ super().click(...) → 框架 BaseTask.click()
│ [.venv/Lib/site-packages/ok/task/task.py]
└─ self.executor.interaction.click(x, y, ...)
│
└─ [`EfInteraction.click`](../../src/interaction/EfInteraction.py#L39) → ✅ 本项目
│
├─ 继承框架 PostMessageInteraction
├─ 重写了 click() → 添加 try_activate() 窗口激活逻辑
└─ Win32 PostMessage(WM_LBUTTONDOWN / WM_LBUTTONUP) 发送鼠标事件
2.8 EfInteraction.click() — 框架級點選的最終執行者
定義位置: src/interaction/EfInteraction.py#L30 :: class EfInteraction / EfInteraction.py#L39 :: EfInteraction.click
來源: ✅ 本專案重寫(繼承框架 PostMessageInteraction)
EfInteraction.click(x, y, down_time, key, ...)
│
├─ try_activate() → 本项目(激活窗口)
│ ├─ 判断窗口是否为前台
│ ├─ 若非前台 → self.activate() → PostMessage(WM_ACTIVATE)
│ └─ 记录鼠标位置
│
├─ win32api.SetCursorPos((abs_x, abs_y)) # Win32 API 移动鼠标
│
└─ self.post(WM_LBUTTONDOWN, MK_LBUTTON, pos) → 继承框架 PostMessageInteraction
├─ self.post(WM_LBUTTONUP, 0, pos) Win32 PostMessage 到窗口
└─ SetCursorPos(self.cursor_position) # 移回原位
2.9 AccountOverrideMixin — 執行時覆蓋注入
定義位置: src/core/base_mixin/account_override_mixin.py :: _bind_account_aware_config_get
來源: ✅ 本專案完全自實現
_bind_account_aware_config_get()
│
└─ 首次调用时绑定(后续账号切换复用同一包装器):
config.get = _patched_get
config._raw_get = 原始 get # 保留原始方法
│
_patched_get(key, default):
│
├─ base = raw_get(key, default) # 读任务原始配置
│
├─ _is_account_override_enabled() # 检查"多账户独立配置"开关
│ └─ [`account_override_mixin.py`](../../src/core/base_mixin/account_override_mixin.py)
│
├─ account_overrides = get_account_task_overrides(
│ current_account_id, task_name) → 本项目 [`account_scope_store.py`](../../src/tasks/account/account_scope_store.py)
│
└─ if key in account_overrides:
return _coerce_override_value(base, overrides[key]) # 类型转换
2.10 account_scope_store — 整個持久化層
定義位置: src/tasks/account/account_scope_store.py
來源: ✅ 本專案完全自實現
| 函式 | 行號 | 職責 |
|---|---|---|
get_store_path() |
#L22 |
返回 configs/account_scoped_overrides.json 路徑 |
parse_account_list_text() |
#L70 |
解析賬號列表文本(相容 账号,密码 格式) |
_normalize() |
#L227 |
資料清洗:登錄檔、覆蓋和地圖 content 標準化,舊使用者名稱鍵遷移為 ID |
load_overrides() |
#L324 |
載入覆蓋檔案(帶 mtime 快取 + 執行緒鎖) |
save_overrides() |
#L352 |
儲存標準化覆蓋檔案 |
sync_account_list_text() |
#L397 |
同步賬號列表 → 登錄檔 ID 對映,並移除持久化密碼 |
resolve_account_id() |
#L412 |
使用者名稱 → account_id 解析,可選擇是否建立 |
get_account_task_overrides() |
#L435 |
按 account_id + task_name 讀取覆蓋,相容使用者名稱舊鍵 |
set_account_task_overrides() |
#L541 |
寫入完整任務覆蓋字典 |
_find_account_id_by_username() |
#L158 |
在登錄檔中按當前使用者名稱查詢 ID,可選匹配 aliases |
_generate_account_id() |
#L186 |
uuid4 生成 acc_xxxxxxxxxxxx 格式 ID |
執行時資料檔案: configs/account_scoped_overrides.json。該檔案由程式執行時生成,不是倉庫內保證存在的靜態文件資源,因此這裡不使用 Markdown 連結。
{
"account_list_text": "...",
"account_registry": {
"acc_bd19daf07452": { "username": "...", "aliases": ["..."] }
},
"accounts": {
"acc_781af5ebb0cd": {
"DailyTask": { "⭐送礼": true, "⭐刷体力": true, "其他配置": "..." }
}
}
}
三、使用框架 vs 本專案的精確邊界
3.1 ✅ 使用框架的部分
| 功能 | 框架類/方法 | 說明 |
|---|---|---|
| OCR 文字識別介面 | BaseTask.ocr() → ocr_fun(lib) |
本專案 login_ocr() 通過 super().ocr() 呼叫;當前 src/config.py 選擇 onnxocr 後端 |
| 模板匹配引擎 | BaseTask.find_feature() → FeatureSet.find_feature() |
find_feature() / find_one() 最終委託到此處 |
| 任務配置設施 | BaseTask.config + default_config + config_description + config_type |
框架初始化配置後設資料並由 GUI 渲染;本專案任務負責宣告具體配置項 |
| 特徵資源載入 | FeatureSet |
框架讀取本專案維護的 assets/coco_annotations.json 和 assets/images/;業務標籤由 src/data/FeatureList.py 宣告 |
| GUI 框架 | PySide6 + qfluentwidgets |
CustomTab、ConfigCard、NavigationItemPosition |
| 任務生命週期 | BaseTask + TriggerTask |
任務的 run() / on_destroy() / 觸發排程 |
| OCR 匹配後排序 | sort_boxes() |
框架工具函式 |
| 視窗訊息傳送基類 | PostMessageInteraction |
EfInteraction 繼承此基類的 post() 方法 |
| 訊號/通訊系統 | CommunicateHandler |
screenshot 儲存、draw_box 等 |
3.2 ❌ 本專案的部分(框架沒有提供任何能力)
| 功能 | 檔案 | 跳轉 | 說明 |
|---|---|---|---|
| 賬號 ID 登錄檔 | account_scope_store.py |
_find_account_id_by_username() / _generate_account_id() |
acc_uuid 生成、使用者名稱 ↔ ID 對映、aliases 管理 |
| 賬號覆蓋儲存 | account_scope_store.py |
load_overrides() / update_overrides() |
JSON 檔案讀寫、mtime 快取、執行緒鎖 |
| 執行時覆蓋注入 | account_override_mixin.py |
_bind_account_aware_config_get |
首次包裝 config.get(),讀取時按當前 context 解析覆蓋 |
| 多賬號配置項宣告 | account_mixin.py |
#L5 |
"多賬戶模式"、"多賬戶獨立配置"、"賬號列表" |
| 賬號列表解析 | account_mixin.py |
get_account_list() |
解析使用者名稱並非建立式呼叫 resolve_account_id() |
| 多賬號迴圈執行 | BaseEfTask.py |
iter_multi_account_context() |
設定/清理賬號 context 並生成執行輪次 |
| 登出→切賬號→登入 | login_mixin.py |
login_flow() |
整個登入切換流程 |
| OCR 文字點選 | login_mixin.py |
click_text() |
自定義截圖 + pyautogui 點選 |
| 自定義截圖 | login_screenshot.py |
#L55 |
capture_window_by_screen() — pyautogui + 裁剪 |
| 自定義滑鼠點選 | EfInteraction.py |
#L39 |
繼承框架,重寫 click() 新增視窗啟用邏輯 |
| 危險態檢測與處置 | runtime_mixin.py |
find_danger() / click() |
本專案在通用點選前匹配 danger_1~danger_5(按解析度對映到現有 _2k 資源),命中後結束遊戲並丟擲異常;框架只提供底層模板匹配能力 |
| GUI 覆蓋編輯器 | AccountConfigTab.py |
render_task_editor() / _save_pending_changes() |
以虛擬配置編輯並儲存當前賬號的完整任務配置快照,同時合併併發檔案變化 |
| 覆蓋編輯虛擬配置 | AccountConfigTab.py |
InMemoryConfig / _coerce_like() |
包裝配置控制元件並按基礎值轉換型別 |
| 任務編排器 | daily_task_runner.py |
DailyTaskRunner |
按賬號分組記錄失敗、輪次彙總 |
3.3 🔀 混合(框架 + 本專案各一部分)
| 功能 | 框架提供 | 本專案提供 |
|---|---|---|
login_ocr() |
super().ocr() — 呼叫框架 OCR 引擎 |
login_screenshot() — 自定義截圖源 |
find_feature() |
super().find_feature() — 框架模板匹配 |
get_feature_by_resolution() — 解析度資源對映;危險態檢測不在此方法中,而在本專案 RuntimeMixin.click() 的前置步驟中 |
EfInteraction.click() |
PostMessageInteraction.post() — 訊息傳送 |
重寫 click() — 視窗啟用、滑鼠移動邏輯 |
AccountConfigTab |
CustomTab 基類 + ConfigCard 渲染控制元件 |
覆蓋編輯資料流、_build_virtual_config()、CRUD |
RuntimeMixin.click() |
super().click() — 框架點選排程 |
find_danger() — 危險態檢測前置 |
四、流程圖
flowchart TD
A["用户开启多账户模式"] --> B["iter_multi_account_context()"]
B --> C{"多账户模式<br>config.get()"}
C -->|Yes| D["get_account_list()"]
D --> E{"账号列表非空?"}
E -->|Yes| F["for each account<br>set_current_account()"]
F --> G["login_flow(username)"]
G --> H["点击设置→退出登录"]
H --> I["find_feature(logout)"]
I --> J["wait_click_feature(log_out_confirm)"]
J --> K["click_text('最近')"]
K --> L["click_text(username[-4:])"]
L --> M["click_text('登录')"]
M --> N["_confirm_logged_in()"]
N --> O{"登录成功?"}
O -->|Yes| P["yield 执行业务逻辑<br>(子任务列表)"]
P --> Q["下一账号<br>循环"]
Q --> F
O -->|No| R["RuntimeError"]
C -->|No| S["单账号模式<br>yield 1 次"]
五、關鍵設計決策
登入互動為什麼要自己寫
click_text()而不是用框架的find_feature+click?
因為登入介面的 UI 元素是文字("最近"、"登入"按鈕文字),不是預註冊的特徵圖片模板,所以只能用 OCR 識別文字位置再點選。為什麼
click_text不用框架的self.click()而用pyautogui.click()?self.click()走的是EfInteraction→PostMessage向視窗傳送訊息,但遊戲登入介面(使用者中心 WebView)可能不響應PostMessage滑鼠事件,需要用全域性級別滑鼠事件(pyautogui.click+SetCursorPos)。為什麼
login_screenshot不用框架的self.executor.frame?
遊戲彈窗/登入介面可能無法被框架的DesktopDuplication/BitBlt捕獲(如 UWP 彈窗或某些渲染層覆蓋),pyautogui.screenshot()是後備方案。