切換賬戶流程分析

返回:文件索引 / README

同步日期: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.jsonassets/images/;業務標籤由 src/data/FeatureList.py 宣告
GUI 框架 PySide6 + qfluentwidgets CustomTabConfigCardNavigationItemPosition
任務生命週期 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_1danger_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 次"]

五、關鍵設計決策

  1. 登入互動為什麼要自己寫 click_text() 而不是用框架的 find_feature + click
    因為登入介面的 UI 元素是文字("最近"、"登入"按鈕文字),不是預註冊的特徵圖片模板,所以只能用 OCR 識別文字位置再點選。

  2. 為什麼 click_text 不用框架的 self.click() 而用 pyautogui.click()
    self.click() 走的是 EfInteractionPostMessage 向視窗傳送訊息,但遊戲登入介面(使用者中心 WebView)可能不響應 PostMessage 滑鼠事件,需要用全域性級別滑鼠事件(pyautogui.click + SetCursorPos)。

  3. 為什麼 login_screenshot 不用框架的 self.executor.frame
    遊戲彈窗/登入介面可能無法被框架的 DesktopDuplication / BitBlt 捕獲(如 UWP 彈窗或某些渲染層覆蓋),pyautogui.screenshot() 是後備方案。

在 GitHub 查看來源 ↗ · 頁面產生時間: 2026年8月10日