Skip to content

feat: 仓库扫描#1950

Open
kawayiYokami wants to merge 19 commits into
OneDragon-Anything:mainfrom
kawayiYokami:discs_scan
Open

feat: 仓库扫描#1950
kawayiYokami wants to merge 19 commits into
OneDragon-Anything:mainfrom
kawayiYokami:discs_scan

Conversation

@kawayiYokami
Copy link
Copy Markdown
Contributor

@kawayiYokami kawayiYokami commented Jan 19, 2026

Summary by CodeRabbit

  • 新功能
    • 新增库存扫描功能:支持驱动盘、音擎、代理人三类自动扫描与导出
    • 新增库存扫描界面,可选择扫描目标并启动/查看结果
    • 新增后台 OCR 工作者、截图缓存与多项解析器(驱动盘、音擎、代理人)以识别并结构化数据
    • 增强图像分析流水线、图标匹配与翻译服务以提升识别率
  • 界面与配置
    • 扩展游戏屏幕配置,新增相关导航入口与区域布局

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 19, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f3376b0d-a9ab-435c-91eb-363045e48ccc

📥 Commits

Reviewing files that changed from the base of the PR and between efa94ba and a2fd99d.

📒 Files selected for processing (59)
  • assets/game_data/screen_info/_od_merged.yml
  • assets/game_data/screen_info/agent_info.yml
  • assets/game_data/screen_info/agent_list.yml
  • assets/game_data/screen_info/menu.yml
  • assets/game_data/screen_info/storage_drive_disc.yml
  • assets/image_analysis_pipelines/已选中的驱动盘.yml
  • assets/image_analysis_pipelines/驱动盘属性识别.yml
  • assets/image_analysis_pipelines/驱动盘方格.yml
  • assets/image_analysis_pipelines/驱动盘进度条检测.yml
  • assets/wiki_data/icons/IconRoleCircle01.webp
  • assets/wiki_data/icons/IconRoleCircle05.webp
  • assets/wiki_data/icons/IconRoleCircle09.webp
  • assets/wiki_data/icons/IconRoleCircle10.webp
  • assets/wiki_data/icons/IconRoleCircle11.webp
  • assets/wiki_data/icons/IconRoleCircle12.webp
  • assets/wiki_data/icons/IconRoleCircle13.webp
  • assets/wiki_data/icons/IconRoleCircle14.webp
  • assets/wiki_data/icons/IconRoleCircle15.webp
  • assets/wiki_data/icons/IconRoleCircle16.webp
  • assets/wiki_data/icons/IconRoleCircle17.webp
  • assets/wiki_data/icons/IconRoleCircle18.webp
  • assets/wiki_data/icons/IconRoleCircle20.webp
  • assets/wiki_data/icons/IconRoleCircle21.webp
  • assets/wiki_data/icons/IconRoleCircle22.webp
  • assets/wiki_data/icons/IconRoleCircle23.webp
  • assets/wiki_data/icons/IconRoleCircle24.webp
  • assets/wiki_data/icons/IconRoleCircle25.webp
  • assets/wiki_data/icons/IconRoleCircle26.webp
  • assets/wiki_data/icons/IconRoleCircle27.webp
  • assets/wiki_data/icons/IconRoleCircle28.webp
  • assets/wiki_data/icons/IconRoleCircle29.webp
  • assets/wiki_data/icons/IconRoleCircle30.webp
  • assets/wiki_data/icons/IconRoleCircle31.webp
  • assets/wiki_data/icons/IconRoleCircle32.webp
  • assets/wiki_data/icons/IconRoleCircle35.webp
  • assets/wiki_data/icons/IconRoleCircle36.webp
  • assets/wiki_data/icons/IconRoleCircle37.webp
  • assets/wiki_data/icons/IconRoleCircle38.webp
  • assets/wiki_data/icons/IconRoleCircle39.webp
  • assets/wiki_data/icons/IconRoleCircle40.webp
  • assets/wiki_data/icons/IconRoleCircle41.webp
  • assets/wiki_data/icons/IconRoleCircle42.webp
  • assets/wiki_data/icons/IconRoleCircle43.webp
  • assets/wiki_data/icons/IconRoleCircle44.webp
  • assets/wiki_data/icons/IconRoleCircle45.webp
  • assets/wiki_data/icons/IconRoleCircle46.webp
  • assets/wiki_data/icons/IconRoleCircle47.webp
  • assets/wiki_data/icons/IconRoleCircle48.webp
  • assets/wiki_data/icons/IconRoleCircle49.webp
  • assets/wiki_data/icons/IconRoleCircle50.webp
  • assets/wiki_data/icons/IconRoleCircle51.webp
  • assets/wiki_data/icons/IconRoleCircle52.webp
  • assets/wiki_data/icons/IconRoleCircle53.webp
  • assets/wiki_data/icons/IconRoleCircle54.webp
  • assets/wiki_data/icons/IconRoleCircle55.webp
  • assets/wiki_data/icons/IconRoleCircle56.webp
  • assets/wiki_data/icons/IconRoleCircle57.webp
  • assets/wiki_data/icons/IconRoleCircle58.webp
  • assets/wiki_data/zzz_translation.json

📝 Walkthrough

Walkthrough

新增完整的“仓库扫描”子系统:屏幕配置与图像分析管线、OCR 后台工作器、解析器、翻译/图标匹配服务、截图缓存与多个扫描应用(驱动盘/音擎/代理人)、GUI 集成及若干调试工具与配置类。

Changes

Inventory Scan — end-to-end feature

Layer / File(s) Summary
Screen / Data Shape
assets/game_data/screen_info/_od_merged.yml, assets/game_data/screen_info/agent_info.yml, assets/game_data/screen_info/agent_list.yml, assets/game_data/screen_info/menu.yml, assets/game_data/screen_info/storage_drive_disc.yml
新增 agent_infoagent_list 屏幕定义,向 menu 与 storage 屏增加“底部-代理人”入口,重构 storage_drive_disc 中街区相关为 驱动盘仓库/驱动盘属性/驱动盘进度条 区域并更新导航目标。
Image Analysis Pipelines
assets/image_analysis_pipelines/已选中的驱动盘.yml, assets/image_analysis_pipelines/驱动盘属性识别.yml, assets/image_analysis_pipelines/驱动盘方格.yml, assets/image_analysis_pipelines/驱动盘进度条检测.yml
新增四个 YAML 管线文件,定义区域裁剪、HSV 过滤、轮廓查找、面积过滤与 OCR 步骤及其参数。
Core Parsers & OCR
src/zzz_od/application/inventory_scan/parser/drive_disk_parser.py, .../agent_parser.py, .../wengine_parser.py, src/zzz_od/application/inventory_scan/ocr_worker.py
新增 OCR 后台工作器(任务队列、暂停/恢复、结果收集)和三个解析器,实现 OCR->结构化数据转换、错误保存与 JSON 导出。
Utilities & Services
src/zzz_od/application/inventory_scan/screenshot_cache.py, .../translation/translation_service.py, .../translation/translation_updater.py, .../translation/icon_downloader.py, .../utils/agent_icon_matcher.py
新增截图缓存、多语言翻译服务与更新器、图标下载器(已弃用提示)和代理人头像匹配工具(边缘比对、debug 图像缓存)。
Scanner Apps & Configs
src/zzz_od/application/inventory_scan/agent/agent_scan_app.py, .../drive_disk/drive_disk_scan_app.py, .../wengine/wengine_scan_app.py, .../drive_disk/drive_disk_scan_config.py, .../drive_disk/drive_disk_scan_const.py
新增三个具体扫描应用,驱动盘实现网格检测/行列扫描逻辑;并添加相应常量与配置类。
Orchestrator & Factory
src/zzz_od/application/inventory_scan/inventory_scan_app.py, .../inventory_scan_app_factory.py, .../inventory_scan_config.py, .../inventory_scan_const.py
新增 InventoryScanApp 作为主协调器,管理截图缓存、OCR 工作线程、逐类扫描流程与数据导出;新增工厂与配置常量。
GUI Integration
src/zzz_od/gui/view/game_assistant/game_assistant_interface.py, src/zzz_od/gui/view/game_assistant/inventory_scan_interface.py
在游戏助手中注册新的 InventoryScanInterface,提供启动配置、目标选择与常用操作按钮(打开输出/错误目录、反馈、跳转分析站点)。
Parser Package Exports & Init
src/zzz_od/application/inventory_scan/__init__.py, .../parser/__init__.py, .../translation/__init__.py, .../utils/__init__.py
新增包初始文件与解析器/翻译模块的导出声明。
Tools / Analysis
tools/analyze_icon_crop.py, tools/analyze_icon_crop_algos.py
新增两个独立脚本用于图标裁剪比对与多种相似度算法的评估、排名与报告生成(用于调试/调参)。

序列图

sequenceDiagram
    participant UI as 用户界面
    participant App as InventoryScanApp
    participant Scanner as 扫描器(驱动盘/音擎/代理)
    participant Capture as 截图/管线
    participant OCR as OcrWorker
    participant Parser as 解析器
    participant Export as 导出器

    UI->>App: 启动扫描
    App->>App: 初始化缓存、OCR、解析器、扫描器
    App->>Scanner: 进入目标屏幕
    Scanner->>Capture: 捕获并裁剪区域
    Capture->>OCR: 提交 OCR 任务
    OCR->>Parser: 转发 OCR 结果
    Parser->>OCR: 返回结构化数据
    OCR->>App: 累积扫描结果
    loop 重复直到全部项
        Scanner->>Scanner: 翻页/下一项
    end
    App->>OCR: 等待完成
    App->>Export: 生成 JSON 导出
    Export->>UI: 完成通知
Loading
sequenceDiagram
    participant Scanner as DriveDiskScanApp
    participant Grid as 网格检测
    participant Cache as ScreenshotCache
    participant OCR as OcrWorker

    Scanner->>Grid: initialize_align()
    Grid->>Grid: 检测网格点位并对齐
    loop 行级扫描
        Scanner->>Grid: detect_grid()
        loop 列级扫描
            Scanner->>Cache: 保存裁剪图像并索引
            Cache->>OCR: 提交 OCR 任务(可并行)
            Scanner->>Scanner: 点击下一格/处理进度条
        end
        Scanner->>Scanner: 换行或滚动判断
    end
    Scanner->>Scanner: 扫描完成
Loading

估计代码审查工作量

🎯 4 (复杂) | ⏱️ ~60 分钟

建议审查者

  • Usagi-wusaqi

🐰 兔子来贺新功能,
仓库扫描自动成行,
驱动、音擎与代理人,
截图 OCR 都不慌,
导出数据把梦圆 ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.66% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'feat: 仓库扫描' directly describes the main feature being added - inventory scanning functionality. The title is clear and specific about the primary change.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@kawayiYokami kawayiYokami marked this pull request as ready for review February 5, 2026 12:34
@kawayiYokami kawayiYokami marked this pull request as draft February 5, 2026 14:21
@idk500
Copy link
Copy Markdown
Collaborator

idk500 commented Mar 8, 2026

战术MARK, 请修复冲突, 这个我会重点关注。 看起来文件很多, 这个资源能否延迟加载?

@kawayiYokami
Copy link
Copy Markdown
Contributor Author

战术MARK, 请修复冲突, 这个我会重点关注。 看起来文件很多, 这个资源能否延迟加载?

这个不需要资源

@kawayiYokami kawayiYokami force-pushed the discs_scan branch 2 times, most recently from 7ec97e9 to 8aa8404 Compare March 8, 2026 15:41
@kawayiYokami kawayiYokami marked this pull request as ready for review March 8, 2026 15:44
@idk500
Copy link
Copy Markdown
Collaborator

idk500 commented Mar 8, 2026

e708f80b667038438e2adeffd87f3174 宣发物料预备

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 20

🧹 Nitpick comments (7)
src/zzz_od/application/inventory_scan/translation/icon_downloader.py (5)

119-119: 未使用的循环变量应以下划线前缀命名。

char_id 在循环体内未被使用,建议重命名为 _char_id_ 以表明意图。

♻️ 建议修改
-            for char_id, char_info in character_data.items():
+            for _char_id, char_info in character_data.items():

或使用 .values()

-            for char_id, char_info in character_data.items():
+            for char_info in character_data.values():
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/zzz_od/application/inventory_scan/translation/icon_downloader.py` at line
119, The for-loop over character_data uses an unused loop variable char_id;
rename it to _char_id (or simply _) or iterate over character_data.values() to
make intent clear and silence linters—update the loop signature in
icon_downloader.py where "for char_id, char_info in character_data.items():" is
defined (or change to "for _, char_info in ..." / "for char_info in
character_data.values():") and leave the loop body using char_info unchanged.

122-138: 重复的条件检查可简化。

两处 if "IconRole" in icon_code: 检查相同条件,可合并为一个代码块。

♻️ 建议简化
                 if icon_code:
-                    # 圆头 (IconRolexx -> IconRoleCirclexx)
                     if "IconRole" in icon_code:
+                        # 圆头 (IconRolexx -> IconRoleCirclexx)
                         circle_code = icon_code.replace("IconRole", "IconRoleCircle")
                         circle_filename = f"{circle_code}.webp"
                         circle_url = f"{self.BASE_URL}{circle_filename}"
                         circle_filepath = os.path.join(self.icons_dir, circle_filename)
                         if self._download_file(circle_url, circle_filepath):
                             count += 1
-                    
-                    # 半身 (IconRolexx -> IconRoleCropxx)
-                    if "IconRole" in icon_code:
+                        
+                        # 半身 (IconRolexx -> IconRoleCropxx)
                         crop_code = icon_code.replace("IconRole", "IconRoleCrop")
                         crop_filename = f"{crop_code}.webp"
                         crop_url = f"{self.BASE_URL}{crop_filename}"
                         crop_filepath = os.path.join(self.icons_dir, crop_filename)
                         if self._download_file(crop_url, crop_filepath):
                             count += 1
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/zzz_od/application/inventory_scan/translation/icon_downloader.py` around
lines 122 - 138, The two identical checks if "IconRole" in icon_code should be
merged: inside the single conditional for icon_code containing "IconRole" (in
the IconDownloader method where icon_code is processed), perform both
replacements (icon_code.replace("IconRole", "IconRoleCircle") and
icon_code.replace("IconRole", "IconRoleCrop")), build
circle_filename/crop_filename and their URLs/filepaths using self.BASE_URL and
self.icons_dir, call self._download_file for each and increment count when each
download succeeds; this removes the duplicate condition while keeping both
download attempts and count increments.

154-159: URL 方案验证缺失存在安全隐患。

urllib.request.urlopen 可能被利用打开非预期的 URL 方案(如 file://)。虽然当前 URL 由 BASE_URL 拼接构成,但建议添加验证以增强安全性。

🛡️ 建议添加 URL 验证
     def _download_file(self, url: str, filepath: str) -> bool:
         """下载单个文件(带重试机制)"""
+        # 验证 URL 方案
+        if not url.startswith(('http://', 'https://')):
+            log.error(f"不支持的 URL 方案: {url}")
+            return False
+        
         # 文件已存在则跳过
         if os.path.exists(filepath):
             return True
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/zzz_od/application/inventory_scan/translation/icon_downloader.py` around
lines 154 - 159, The code opens arbitrary URL strings with
urllib.request.urlopen (variable url in icon_downloader.py) without validating
the scheme; restrict allowed schemes to only 'http' and 'https' before calling
urllib.request.urlopen by parsing the URL (e.g., via
urllib.parse.urlparse(url)), reject or log and return False for any other scheme
(such as 'file'); perform this validation inside the same try block (or before
it) so the subsequent Request/urllib.request.urlopen call only runs for safe
schemes and include a clear error/log message when rejecting an unsupported
scheme.

1-1: 应使用 pathlib 而非 os 模块进行路径操作。

根据编码规范,应使用 pathlib 库进行路径处理。整个文件中多处使用 os.path.joinos.path.exists 等方法,建议统一改用 pathlib.Path

♻️ 建议的导入修改
-import os
+from pathlib import Path
 import json
 import urllib.request
 import time

示例用法变更:

# Before
self.icons_dir = os.path.join(os_utils.get_path_under_work_dir('assets', 'wiki_data'), 'icons')
if os.path.exists(self.icons_dir):

# After
self.icons_dir = Path(os_utils.get_path_under_work_dir('assets', 'wiki_data')) / 'icons'
if self.icons_dir.exists():

As per coding guidelines: "Use pathlib library for path handling instead of os.path"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/zzz_od/application/inventory_scan/translation/icon_downloader.py` at line
1, Replace use of the os module for filesystem paths with pathlib.Path
throughout this file: import Path from pathlib instead of os, convert any
os.path.join(...) patterns (e.g., creation of self.icons_dir where code uses
os.path.join(os_utils.get_path_under_work_dir('assets','wiki_data'), 'icons'))
to Path(os_utils.get_path_under_work_dir(...)) / 'icons', replace os.path.exists
/ os.path.isfile / os.listdir checks with Path.exists(), Path.is_file(),
Path.iterdir() or list(Path.glob(...)), and replace os.makedirs(...) with
Path.mkdir(parents=True, exist_ok=True); update any file open calls to use
path_obj.open(...) or str(path_obj) where required and adjust code that iterates
or builds paths to use Path methods so all path operations in functions/classes
like the icon downloader use pathlib.Path consistently.

40-41: 未使用的类属性 _failed_this_run

该类属性在整个文件中从未被读取或修改,属于死代码,应移除。

🧹 建议移除
-    # 类级别标志:同一次运行中如果已经失败过,就不再尝试
-    _failed_this_run = False
-
     def __init__(self):
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/zzz_od/application/inventory_scan/translation/icon_downloader.py` around
lines 40 - 41, Remove the dead class-level attribute _failed_this_run: locate
the class where _failed_this_run is defined (the icon downloader class) and
delete the `_failed_this_run = False` line; ensure no other code references it
(if any references exist, either remove them or replace with a local/stateful
mechanism) and run tests/lint to confirm no unused symbol warnings remain.
tools/analyze_icon_crop.py (2)

97-132: score_icons 函数返回类型过于冗长

考虑使用 TypedDict 或命名元组来简化返回类型,提高代码可读性和可维护性。当前 7 个列表的返回值顺序容易混淆。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/analyze_icon_crop.py` around lines 97 - 132, The function score_icons
currently returns seven separate lists which is hard to follow; define a single
structured return type (e.g., a TypedDict or a NamedTuple/dataclass) named
something like IconScoreResults with fields ncc_scores, sqdiff_scores,
mse_scores, hist_scores, ahash_scores, ncc_norm_scores, and edge_ncc_scores,
update the score_icons signature to return IconScoreResults instead of the long
Tuple[...] annotation, construct and return an instance of IconScoreResults at
the end (populating each field with the corresponding local list), and update
any callers that unpack the tuple to access fields by name (e.g.,
results.ncc_scores) to keep usage clear and type-safe.

56-58: 变量名 l 不够清晰

变量名 l 容易与数字 1 混淆,建议重命名为 lumluminance 以提高可读性。

♻️ 建议修改
-    l, a, b = cv2.split(lab)
+    lum, a, b = cv2.split(lab)
     clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
-    l = clahe.apply(l)
-    merged = cv2.merge([l, a, b])
+    lum = clahe.apply(lum)
+    merged = cv2.merge([lum, a, b])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/analyze_icon_crop.py` around lines 56 - 58, 变量名 `l` 与数字 `1`
易混淆,请将其重命名为更具描述性的名字(例如 `lum` 或 `luminance`)—在使用 cv2.split(lab) 的赋值处将 `l` 改为
`lum`/`luminance`,并在随后对其调用 clahe.apply(...) 以及文件中所有后续引用(例如任何对该通道的处理或与其他通道 merge
的位置)同步替换,确保命名一致且未破坏变量作用域或导入引用(保留 cv2.split, createCLAHE, clahe.apply 等符号不变)。
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@assets/game_data/screen_info/_od_merged.yml`:
- Around line 408-437: The area entry with area_name "代理人-信息" (text "基础") should
be marked as a unique identifier so list-page recognition isn’t confused with
the common back button; change its id_mark from false to true on that YAML entry
(the block containing area_name "代理人-信息" and text "基础") so navigation uses this
distinctive marker instead of the shared "按钮-返回" id_mark.
- Around line 8338-8382: The three regions named "驱动盘仓库", "驱动盘属性", and "驱动盘进度条"
are image/recognition areas and should not have navigation entries; remove their
goto_list entries (or set them to an empty list/null) so they no longer point to
"仓库-驱动仓库-驱动盘拆解", leaving only the actual clickable "拆解" button region with that
goto_list; update the YAML entries for the area_name values above to remove the
goto_list mapping.

In `@assets/game_data/screen_info/agent_info.yml`:
- Around line 35-76: This screen lacks a unique id_mark for the agent detail
page; set id_mark: true on one stable tab entry (e.g., the area with area_name:
"按钮-代理人基础" or "按钮-代理人技能"/"按钮-代理人装备") so the recognizer has a page-specific
anchor; locate the entries for area_name "按钮-代理人基础", "按钮-代理人技能", and "按钮-代理人装备"
and change the chosen entry's id_mark from false to true (leave the others
false).

In `@assets/game_data/screen_info/storage_drive_disc.yml`:
- Around line 49-93: The three new areas have their text field incorrectly set
to "街区"; update the text fields for area_name "驱动盘仓库", "驱动盘属性", and "驱动盘进度条" to
either an empty string '' if no OCR/matching is required or to the correct
expected text for each area if OCR matching is intended—ensure the changes are
applied to the corresponding entries (area_name keys) so text aligns with their
actual purpose.

In `@src/zzz_od/application/inventory_scan/agent/agent_scan_app.py`:
- Around line 187-192: The save helper (_combine_and_save and its helper
_save_screenshot) currently swallow errors and never return failure, so callers
like the block using _is_area_black -> _combine_and_save() then return
round_success() will skip a character even if saving failed; change
_combine_and_save and _save_screenshot to return a boolean (True on success,
False on failure) and raise/log appropriately, then update callers (e.g., the
branch after _is_area_black and other similar spots in the 260-310 region) to
check the boolean: only set _last_click_time = 0 and call round_success() when
saving succeeded, otherwise do not advance to the next agent and trigger a retry
for the current agent (or requeue the save), ensuring failures are propagated
rather than silently skipped.

In `@src/zzz_od/application/inventory_scan/drive_disk/drive_disk_scan_app.py`:
- Around line 145-163: The current flow uses round_success() for failure cases
(ctx.error_str, empty contours/get_absolute_rects, or empty self._sort_grids
result) which lets the state machine proceed incorrectly; change those branches
to return a failure or retry (use round_fail() or round_retry()) and ensure you
clear self.grid_rows before returning. Specifically, in the checks around
ctx.error_str, the contours/absolute_rects check after ctx.get_absolute_rects(),
and after calling self._sort_grids(all_grids), replace round_success(...) with
an appropriate round_retry()/round_fail() call and set self.grid_rows = [] (or
None) so stale grids are not reused.

In `@src/zzz_od/application/inventory_scan/inventory_scan_app.py`:
- Around line 58-69: The OCR worker and screenshot cache are only cleaned on the
success path in prepare_screenshots_dir(); add a finally block in the main
orchestration method execute() to always stop/reset the OCR worker and cache so
they are cleaned on failures or exceptions. Specifically, ensure execute() calls
the same teardown used on success (e.g., invoke wait_ocr_complete() or call
ocr_worker.wait_complete()/ocr_worker.stop() and screenshot_cache.reset_all())
in a finally clause, and also ensure any per-scanner screenshots_dir assignments
(drive_disk_scanner, wengine_scanner, agent_scanner) remain consistent before
teardown so queued OCR tasks are deterministically drained or cancelled. Use the
existing symbols prepare_screenshots_dir, execute, wait_ocr_complete,
screenshot_cache.reset_all, ocr_worker.start, ocr_worker.stop, and
ocr_worker.wait_complete to locate and implement the fix.

In `@src/zzz_od/application/inventory_scan/ocr_worker.py`:
- Around line 36-88: Reset the worker lifecycle and queue state so paused/stale
tasks don't leak into the next run: in start(), explicitly clear _pause_event
(ensure processing isn't unintentionally left paused), and in stop() and
reset(), clear or drain the _queue so pending screenshots are removed; also
ensure reset() clears _pause_event and, if the worker is not running,
reinitialize _queue to a fresh Queue() so later start() gets a clean queue.
Update references to the symbols start, stop, reset, pause, _pause_event, and
_queue when applying these changes.

In `@src/zzz_od/application/inventory_scan/parser/agent_parser.py`:
- Around line 77-83: The deduplication flag
self.scanned_agent_keys.add(agent_name) is set before parsing, causing agents to
be permanently skipped if parsing later raises an exception; move the add call
so it only runs after successful parsing and all value conversions in the agent
parsing flow (i.e., after the ValueError-prone numeric/OCR parsing completes)
and mirror the same change for the other similar block covering the logic around
lines 85-126; ensure you still check "if agent_name in self.scanned_agent_keys"
early to skip true duplicates but only mark (add) the key after the parsing
succeeds.
- Around line 204-231: _agent_level currently only accepts two-digit numbers and
can be mis-parsed by _parse_cinema_level; change _parse_agent_level to accept
1–2 digits on both sides (use regex like r'等级(\d{1,2})/(\d{1,3})'), parse ints,
and treat a match as agent level only if the parsed max_level > 6 (otherwise
treat as possible cinema); keep the single-number fallback as r'等级(\d{1,2})'
returning f"{num}/60". In _parse_cinema_level tighten matching so it only
returns a cinema when the pattern is explicitly a small-range mindscape (require
second digit <=6 and the first digit 0–6, e.g. regex r'([0-6])/([0-6])', or
require contextual keywords like '命座'/'影画' if present), and always validate
numeric bounds before returning to avoid interpreting "1/60" or "60/60" as a
cinema.

In `@src/zzz_od/application/inventory_scan/parser/drive_disk_parser.py`:
- Around line 281-285: The current fallback that forces main_stat_key to
'hp'/'hp_' when main_stat_key is None hides OCR failures; instead, remove the
hardcoded default and either call self._save_error_data(...) with context (e.g.,
slot_key, the OCR text, and parsing state) and raise/return an error, or
explicitly return (None, None) so the caller skips the disk; update the block
that currently checks "if main_stat_key is None:" to perform this error-path
using the existing _save_error_data method or by returning None for
main_stat_key to propagate the failure rather than pretending it is 'hp'/'hp_'.

In `@src/zzz_od/application/inventory_scan/parser/wengine_parser.py`:
- Around line 151-179: The _parse_modification function samples five hard-coded
points (260 + i*30, 160) which can IndexError if the screenshot is smaller; fix
by validating coordinates against gray.shape (height, width) before accessing
gray[y, x], and if any sample is out-of-bounds return a conservative
modification value (e.g., 1) and emit a warning/log via the module logger;
alternatively convert the hard-coded points to relative coordinates based on
width/height and compute x,y accordingly so sampling always stays inside the
image; ensure the check is placed before building gray_values and catch
unexpected exceptions to log them and return the conservative value.

In `@src/zzz_od/application/inventory_scan/screenshot_cache.py`:
- Around line 177-192: reset_all() currently clears only in-memory caches and
resets indices but leaves old screenshot files on disk (save_dir), causing
get_all_indices() to pick up previous-run JPGs; update reset_all to also delete
disk-stored screenshots (e.g., remove all .jpg/.jpeg files or delete the per-run
save_dir) before resetting indices — use pathlib.Path to iterate and unlink
files or rmdir the directory safely, or alternatively switch to creating a
unique per-scan directory so old files are not reused; reference
methods/variables: reset_all, get_all_indices, and save_dir when making the
change.

In `@src/zzz_od/application/inventory_scan/translation/translation_service.py`:
- Around line 24-33: The OCR correction mappings in special_name_mapping are
never applied because _translate() only checks exact key equality; call
correct_text() (the OCR-normalizer) at the main translation entry (e.g., at the
start of _translate() or the public translation entry function that calls
_translate()) to normalize input first so substrings like "搖摆爵士" get corrected
to "摇摆爵士" before lookup; update any other callers that bypass this flow to
ensure they pass text through correct_text() prior to exact-key mapping (refer
to special_name_mapping, _translate(), and correct_text()).
- Around line 128-135: The current fuzzy-match threshold (confidence > 0.2) in
the translation flow is too permissive; update the logic in the block that calls
self._fuzzy_match(name, category_dict) so it requires a higher, calibrated
minimum confidence (e.g., change the numeric threshold to a safer value), and
add extra protections before accepting a fuzzy match: check relative length
difference between name and best_match (reject if difference exceeds a small
ratio), and require the gap to the second-best match (obtain from _fuzzy_match
or modify it to return second_best/confidence_gap) to exceed a minimum margin;
only call extract_translation(category_dict[best_match], target_lang, name) when
all conditions (new_confidence_threshold, length check, and second-best gap)
pass, otherwise fall back to keeping the original OCR text and log a warning as
currently done.

In `@src/zzz_od/application/inventory_scan/translation/translation_updater.py`:
- Around line 1-6: Validate the downloaded payload is a dict in
_download_json/_download_json() and refuse non-dict JSON (raise or return error)
instead of accepting any JSON; then change _save_dict/_save_dict() to write the
JSON to a temporary file (e.g., in the same dir with a unique suffix) and only
call os.replace(temp_path, "zzz_translation.json") after the write/flush/close
succeeds to perform an atomic swap so a partial or invalid file never replaces
the live zzz_translation.json read by TranslationService.

In `@src/zzz_od/application/inventory_scan/utils/agent_icon_matcher.py`:
- Around line 123-152: The current logic in agent_icon_matcher.py sorts scores
and always picks scores[0] without validating similarity; update the code around
scores, best_score, and best_icon_name to require a minimum similarity threshold
(e.g., min_score) and also check a top-1 vs top-2 margin (compare best_score
against second_score or 0 if absent) before accepting the match; if the checks
fail, return "" (and still call _cache_unique_crop_debug with agent_key="") so
false positives are avoided; keep using icon_name_match and
_find_agent_key_by_icon as now but only call them after the score/margin checks
and log appropriately when rejecting for low score or small margin.

In `@src/zzz_od/application/inventory_scan/wengine/wengine_scan_app.py`:
- Around line 30-32: The code is incorrectly reusing the "仓库-驱动仓库/驱动盘属性" crop
area for 音擎 screenshots which causes OCR to break when layouts differ; update
the logic in wengine_scan_app (the block using self.ctx.screen_loader.get_area,
storage_area and cv2_utils.crop_image_only) to fetch an independent area for the
音擎 screen (e.g., "仓库-音擎仓库" with a dedicated property region) and use that area's
rect when cropping the screenshot instead of storage_area.rect so 音擎 scans use
their own defined region.

In `@src/zzz_od/gui/view/game_assistant/inventory_scan_interface.py`:
- Around line 102-105: The _on_open_output_clicked handler currently calls
os.startfile(output_dir) without ensuring the directory exists, causing an
exception on first use; modify the _on_open_output_clicked method to create the
output_dir returned by os_utils.get_path_under_work_dir('.debug',
'inventory_exports') if it does not exist (e.g., os.makedirs(output_dir,
exist_ok=True) or using an existing os_utils helper) before calling
os.startfile(output_dir) so the folder is guaranteed to exist when opened.

In `@tools/analyze_icon_crop.py`:
- Line 3: Replace legacy typing imports and annotations: remove "from typing
import List, Tuple" and use built-in generics (list, tuple) in annotations;
update function signatures such as score_icons and show to use modern syntax
(e.g., score_icons(...) -> tuple[list[tuple[str, float]], ...] and show(title:
str, items: list[tuple[str, float]]) -> None) and adjust any other occurrences
of List/Tuple in this file to list/tuple. Ensure imports and any type comments
are cleaned up and that nullability/optional types (if present) use "Optional"
from typing or "str | None" where appropriate for Python 3.11 compatibility.

---

Nitpick comments:
In `@src/zzz_od/application/inventory_scan/translation/icon_downloader.py`:
- Line 119: The for-loop over character_data uses an unused loop variable
char_id; rename it to _char_id (or simply _) or iterate over
character_data.values() to make intent clear and silence linters—update the loop
signature in icon_downloader.py where "for char_id, char_info in
character_data.items():" is defined (or change to "for _, char_info in ..." /
"for char_info in character_data.values():") and leave the loop body using
char_info unchanged.
- Around line 122-138: The two identical checks if "IconRole" in icon_code
should be merged: inside the single conditional for icon_code containing
"IconRole" (in the IconDownloader method where icon_code is processed), perform
both replacements (icon_code.replace("IconRole", "IconRoleCircle") and
icon_code.replace("IconRole", "IconRoleCrop")), build
circle_filename/crop_filename and their URLs/filepaths using self.BASE_URL and
self.icons_dir, call self._download_file for each and increment count when each
download succeeds; this removes the duplicate condition while keeping both
download attempts and count increments.
- Around line 154-159: The code opens arbitrary URL strings with
urllib.request.urlopen (variable url in icon_downloader.py) without validating
the scheme; restrict allowed schemes to only 'http' and 'https' before calling
urllib.request.urlopen by parsing the URL (e.g., via
urllib.parse.urlparse(url)), reject or log and return False for any other scheme
(such as 'file'); perform this validation inside the same try block (or before
it) so the subsequent Request/urllib.request.urlopen call only runs for safe
schemes and include a clear error/log message when rejecting an unsupported
scheme.
- Line 1: Replace use of the os module for filesystem paths with pathlib.Path
throughout this file: import Path from pathlib instead of os, convert any
os.path.join(...) patterns (e.g., creation of self.icons_dir where code uses
os.path.join(os_utils.get_path_under_work_dir('assets','wiki_data'), 'icons'))
to Path(os_utils.get_path_under_work_dir(...)) / 'icons', replace os.path.exists
/ os.path.isfile / os.listdir checks with Path.exists(), Path.is_file(),
Path.iterdir() or list(Path.glob(...)), and replace os.makedirs(...) with
Path.mkdir(parents=True, exist_ok=True); update any file open calls to use
path_obj.open(...) or str(path_obj) where required and adjust code that iterates
or builds paths to use Path methods so all path operations in functions/classes
like the icon downloader use pathlib.Path consistently.
- Around line 40-41: Remove the dead class-level attribute _failed_this_run:
locate the class where _failed_this_run is defined (the icon downloader class)
and delete the `_failed_this_run = False` line; ensure no other code references
it (if any references exist, either remove them or replace with a local/stateful
mechanism) and run tests/lint to confirm no unused symbol warnings remain.

In `@tools/analyze_icon_crop.py`:
- Around line 97-132: The function score_icons currently returns seven separate
lists which is hard to follow; define a single structured return type (e.g., a
TypedDict or a NamedTuple/dataclass) named something like IconScoreResults with
fields ncc_scores, sqdiff_scores, mse_scores, hist_scores, ahash_scores,
ncc_norm_scores, and edge_ncc_scores, update the score_icons signature to return
IconScoreResults instead of the long Tuple[...] annotation, construct and return
an instance of IconScoreResults at the end (populating each field with the
corresponding local list), and update any callers that unpack the tuple to
access fields by name (e.g., results.ncc_scores) to keep usage clear and
type-safe.
- Around line 56-58: 变量名 `l` 与数字 `1` 易混淆,请将其重命名为更具描述性的名字(例如 `lum` 或
`luminance`)—在使用 cv2.split(lab) 的赋值处将 `l` 改为 `lum`/`luminance`,并在随后对其调用
clahe.apply(...) 以及文件中所有后续引用(例如任何对该通道的处理或与其他通道 merge
的位置)同步替换,确保命名一致且未破坏变量作用域或导入引用(保留 cv2.split, createCLAHE, clahe.apply 等符号不变)。

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2b2bebe3-bccd-4ee8-8246-1cb848c61df4

📥 Commits

Reviewing files that changed from the base of the PR and between e4bfc2a and efa94ba.

📒 Files selected for processing (88)
  • assets/game_data/screen_info/_od_merged.yml
  • assets/game_data/screen_info/agent_info.yml
  • assets/game_data/screen_info/agent_list.yml
  • assets/game_data/screen_info/menu.yml
  • assets/game_data/screen_info/storage_drive_disc.yml
  • assets/image_analysis_pipelines/已选中的驱动盘.yml
  • assets/image_analysis_pipelines/驱动盘属性识别.yml
  • assets/image_analysis_pipelines/驱动盘方格.yml
  • assets/image_analysis_pipelines/驱动盘进度条检测.yml
  • assets/wiki_data/icons/IconRoleCircle01.webp
  • assets/wiki_data/icons/IconRoleCircle05.webp
  • assets/wiki_data/icons/IconRoleCircle09.webp
  • assets/wiki_data/icons/IconRoleCircle10.webp
  • assets/wiki_data/icons/IconRoleCircle11.webp
  • assets/wiki_data/icons/IconRoleCircle12.webp
  • assets/wiki_data/icons/IconRoleCircle13.webp
  • assets/wiki_data/icons/IconRoleCircle14.webp
  • assets/wiki_data/icons/IconRoleCircle15.webp
  • assets/wiki_data/icons/IconRoleCircle16.webp
  • assets/wiki_data/icons/IconRoleCircle17.webp
  • assets/wiki_data/icons/IconRoleCircle18.webp
  • assets/wiki_data/icons/IconRoleCircle20.webp
  • assets/wiki_data/icons/IconRoleCircle21.webp
  • assets/wiki_data/icons/IconRoleCircle22.webp
  • assets/wiki_data/icons/IconRoleCircle23.webp
  • assets/wiki_data/icons/IconRoleCircle24.webp
  • assets/wiki_data/icons/IconRoleCircle25.webp
  • assets/wiki_data/icons/IconRoleCircle26.webp
  • assets/wiki_data/icons/IconRoleCircle27.webp
  • assets/wiki_data/icons/IconRoleCircle28.webp
  • assets/wiki_data/icons/IconRoleCircle29.webp
  • assets/wiki_data/icons/IconRoleCircle30.webp
  • assets/wiki_data/icons/IconRoleCircle31.webp
  • assets/wiki_data/icons/IconRoleCircle32.webp
  • assets/wiki_data/icons/IconRoleCircle35.webp
  • assets/wiki_data/icons/IconRoleCircle36.webp
  • assets/wiki_data/icons/IconRoleCircle37.webp
  • assets/wiki_data/icons/IconRoleCircle38.webp
  • assets/wiki_data/icons/IconRoleCircle39.webp
  • assets/wiki_data/icons/IconRoleCircle40.webp
  • assets/wiki_data/icons/IconRoleCircle41.webp
  • assets/wiki_data/icons/IconRoleCircle42.webp
  • assets/wiki_data/icons/IconRoleCircle43.webp
  • assets/wiki_data/icons/IconRoleCircle44.webp
  • assets/wiki_data/icons/IconRoleCircle45.webp
  • assets/wiki_data/icons/IconRoleCircle46.webp
  • assets/wiki_data/icons/IconRoleCircle47.webp
  • assets/wiki_data/icons/IconRoleCircle48.webp
  • assets/wiki_data/icons/IconRoleCircle49.webp
  • assets/wiki_data/icons/IconRoleCircle50.webp
  • assets/wiki_data/icons/IconRoleCircle51.webp
  • assets/wiki_data/icons/IconRoleCircle52.webp
  • assets/wiki_data/icons/IconRoleCircle53.webp
  • assets/wiki_data/icons/IconRoleCircle54.webp
  • assets/wiki_data/icons/IconRoleCircle55.webp
  • assets/wiki_data/icons/IconRoleCircle56.webp
  • assets/wiki_data/icons/IconRoleCircle57.webp
  • assets/wiki_data/icons/IconRoleCircle58.webp
  • assets/wiki_data/zzz_translation.json
  • src/zzz_od/application/inventory_scan/__init__.py
  • src/zzz_od/application/inventory_scan/agent/__init__.py
  • src/zzz_od/application/inventory_scan/agent/agent_scan_app.py
  • src/zzz_od/application/inventory_scan/drive_disk/__init__.py
  • src/zzz_od/application/inventory_scan/drive_disk/drive_disk_scan_app.py
  • src/zzz_od/application/inventory_scan/drive_disk/drive_disk_scan_config.py
  • src/zzz_od/application/inventory_scan/drive_disk/drive_disk_scan_const.py
  • src/zzz_od/application/inventory_scan/inventory_scan_app.py
  • src/zzz_od/application/inventory_scan/inventory_scan_app_factory.py
  • src/zzz_od/application/inventory_scan/inventory_scan_config.py
  • src/zzz_od/application/inventory_scan/inventory_scan_const.py
  • src/zzz_od/application/inventory_scan/ocr_worker.py
  • src/zzz_od/application/inventory_scan/parser/__init__.py
  • src/zzz_od/application/inventory_scan/parser/agent_parser.py
  • src/zzz_od/application/inventory_scan/parser/drive_disk_parser.py
  • src/zzz_od/application/inventory_scan/parser/wengine_parser.py
  • src/zzz_od/application/inventory_scan/screenshot_cache.py
  • src/zzz_od/application/inventory_scan/translation/__init__.py
  • src/zzz_od/application/inventory_scan/translation/icon_downloader.py
  • src/zzz_od/application/inventory_scan/translation/translation_service.py
  • src/zzz_od/application/inventory_scan/translation/translation_updater.py
  • src/zzz_od/application/inventory_scan/utils/__init__.py
  • src/zzz_od/application/inventory_scan/utils/agent_icon_matcher.py
  • src/zzz_od/application/inventory_scan/wengine/__init__.py
  • src/zzz_od/application/inventory_scan/wengine/wengine_scan_app.py
  • src/zzz_od/gui/view/game_assistant/game_assistant_interface.py
  • src/zzz_od/gui/view/game_assistant/inventory_scan_interface.py
  • tools/analyze_icon_crop.py
  • tools/analyze_icon_crop_algos.py

Comment on lines +408 to +437
- area_name: 按钮-返回
id_mark: true
pc_rect:
- 82
- 13
- 150
- 90
text: ''
lcs_percent: 0.5
template_sub_dir: menu
template_id: back
template_match_threshold: 0.9
color_range: null
goto_list:
- 菜单
- area_name: 代理人-信息
id_mark: false
pc_rect:
- 1015
- 739
- 1350
- 1047
text: 基础
lcs_percent: 0.5
template_sub_dir: ''
template_id: ''
template_match_threshold: 0.7
color_range: null
goto_list:
- 代理人-信息
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

给“代理人-列表”补一个唯一的 id_mark

这里只有返回按钮是 id_mark,而返回按钮在很多页面都存在;真正能区分代理人列表的“基础”区域却被标成了普通区域。这样导航回列表页时,识屏很容易误判为成功。

可参考的最小修正
  - area_name: 代理人-信息
-    id_mark: false
+    id_mark: true
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- area_name: 按钮-返回
id_mark: true
pc_rect:
- 82
- 13
- 150
- 90
text: ''
lcs_percent: 0.5
template_sub_dir: menu
template_id: back
template_match_threshold: 0.9
color_range: null
goto_list:
- 菜单
- area_name: 代理人-信息
id_mark: false
pc_rect:
- 1015
- 739
- 1350
- 1047
text: 基础
lcs_percent: 0.5
template_sub_dir: ''
template_id: ''
template_match_threshold: 0.7
color_range: null
goto_list:
- 代理人-信息
- area_name: 按钮-返回
id_mark: true
pc_rect:
- 82
- 13
- 150
- 90
text: ''
lcs_percent: 0.5
template_sub_dir: menu
template_id: back
template_match_threshold: 0.9
color_range: null
goto_list:
- 菜单
- area_name: 代理人-信息
id_mark: true
pc_rect:
- 1015
- 739
- 1350
- 1047
text: 基础
lcs_percent: 0.5
template_sub_dir: ''
template_id: ''
template_match_threshold: 0.7
color_range: null
goto_list:
- 代理人-信息
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@assets/game_data/screen_info/_od_merged.yml` around lines 408 - 437, The area
entry with area_name "代理人-信息" (text "基础") should be marked as a unique
identifier so list-page recognition isn’t confused with the common back button;
change its id_mark from false to true on that YAML entry (the block containing
area_name "代理人-信息" and text "基础") so navigation uses this distinctive marker
instead of the shared "按钮-返回" id_mark.

Comment on lines +8338 to +8382
- area_name: 驱动盘仓库
id_mark: false
pc_rect:
- 93
- 202
- 1320
- 897
text: 街区
lcs_percent: 0.5
template_sub_dir: ''
template_id: ''
template_match_threshold: 0.7
color_range: null
goto_list:
- 仓库-驱动仓库-驱动盘拆解
- area_name: 驱动盘属性
id_mark: false
pc_rect:
- 1408
- 267
- 1838
- 768
text: 街区
lcs_percent: 0.5
template_sub_dir: ''
template_id: ''
template_match_threshold: 0.7
color_range: null
goto_list:
- 仓库-驱动仓库-驱动盘拆解
- area_name: 驱动盘进度条
id_mark: false
pc_rect:
- 1363
- 862
- 1367
- 863
text: 街区
lcs_percent: 0.5
template_sub_dir: ''
template_id: ''
template_match_threshold: 0.7
color_range: null
goto_list:
- 仓库-驱动仓库-驱动盘拆解
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

不要把内容区域也声明成“跳转到拆解”的入口。

驱动盘仓库驱动盘属性驱动盘进度条 这三个区域看起来是取图/识别区域,不是实际按钮。把它们也挂到同一个 goto_list 上,会让按 goto 反查点击位的逻辑命中大块内容区,而不是唯一可靠的“按钮-拆解”。

建议只保留真正可点击的入口
  - area_name: 驱动盘仓库
@@
-    goto_list:
-    - 仓库-驱动仓库-驱动盘拆解
+    goto_list: []
  - area_name: 驱动盘属性
@@
-    goto_list:
-    - 仓库-驱动仓库-驱动盘拆解
+    goto_list: []
  - area_name: 驱动盘进度条
@@
-    goto_list:
-    - 仓库-驱动仓库-驱动盘拆解
+    goto_list: []
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- area_name: 驱动盘仓库
id_mark: false
pc_rect:
- 93
- 202
- 1320
- 897
text: 街区
lcs_percent: 0.5
template_sub_dir: ''
template_id: ''
template_match_threshold: 0.7
color_range: null
goto_list:
- 仓库-驱动仓库-驱动盘拆解
- area_name: 驱动盘属性
id_mark: false
pc_rect:
- 1408
- 267
- 1838
- 768
text: 街区
lcs_percent: 0.5
template_sub_dir: ''
template_id: ''
template_match_threshold: 0.7
color_range: null
goto_list:
- 仓库-驱动仓库-驱动盘拆解
- area_name: 驱动盘进度条
id_mark: false
pc_rect:
- 1363
- 862
- 1367
- 863
text: 街区
lcs_percent: 0.5
template_sub_dir: ''
template_id: ''
template_match_threshold: 0.7
color_range: null
goto_list:
- 仓库-驱动仓库-驱动盘拆解
- area_name: 驱动盘仓库
id_mark: false
pc_rect:
- 93
- 202
- 1320
- 897
text: 街区
lcs_percent: 0.5
template_sub_dir: ''
template_id: ''
template_match_threshold: 0.7
color_range: null
goto_list: []
- area_name: 驱动盘属性
id_mark: false
pc_rect:
- 1408
- 267
- 1838
- 768
text: 街区
lcs_percent: 0.5
template_sub_dir: ''
template_id: ''
template_match_threshold: 0.7
color_range: null
goto_list: []
- area_name: 驱动盘进度条
id_mark: false
pc_rect:
- 1363
- 862
- 1367
- 863
text: 街区
lcs_percent: 0.5
template_sub_dir: ''
template_id: ''
template_match_threshold: 0.7
color_range: null
goto_list: []
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@assets/game_data/screen_info/_od_merged.yml` around lines 8338 - 8382, The
three regions named "驱动盘仓库", "驱动盘属性", and "驱动盘进度条" are image/recognition areas
and should not have navigation entries; remove their goto_list entries (or set
them to an empty list/null) so they no longer point to "仓库-驱动仓库-驱动盘拆解", leaving
only the actual clickable "拆解" button region with that goto_list; update the
YAML entries for the area_name values above to remove the goto_list mapping.

Comment on lines +35 to +76
- area_name: 按钮-代理人基础
id_mark: false
pc_rect:
- 1042
- 974
- 1251
- 1017
text: 基础
lcs_percent: 0.5
template_sub_dir: ''
template_id: ''
template_match_threshold: 0.7
color_range: null
goto_list: []
- area_name: 按钮-代理人技能
id_mark: false
pc_rect:
- 1290
- 974
- 1507
- 1013
text: 技能
lcs_percent: 0.5
template_sub_dir: ''
template_id: ''
template_match_threshold: 0.7
color_range: null
goto_list: []
- area_name: 按钮-代理人装备
id_mark: false
pc_rect:
- 1555
- 976
- 1760
- 1016
text: 装备
lcs_percent: 0.5
template_sub_dir: ''
template_id: ''
template_match_threshold: 0.7
color_range: null
goto_list: []
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

给代理人详情页补一个专属 id_mark

当前这个 screen 的 id_mark 只有“街区”和返回按钮,这两个锚点在很多页面都会同时出现。没有代理人页独有标识,识屏会和其他菜单页冲突;至少应把“基础/技能/装备”里一个稳定存在的页签设成 id_mark: true

可参考的最小修正
- - area_name: 按钮-代理人基础
-  id_mark: false
+ - area_name: 按钮-代理人基础
+  id_mark: true
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- area_name: 按钮-代理人基础
id_mark: false
pc_rect:
- 1042
- 974
- 1251
- 1017
text: 基础
lcs_percent: 0.5
template_sub_dir: ''
template_id: ''
template_match_threshold: 0.7
color_range: null
goto_list: []
- area_name: 按钮-代理人技能
id_mark: false
pc_rect:
- 1290
- 974
- 1507
- 1013
text: 技能
lcs_percent: 0.5
template_sub_dir: ''
template_id: ''
template_match_threshold: 0.7
color_range: null
goto_list: []
- area_name: 按钮-代理人装备
id_mark: false
pc_rect:
- 1555
- 976
- 1760
- 1016
text: 装备
lcs_percent: 0.5
template_sub_dir: ''
template_id: ''
template_match_threshold: 0.7
color_range: null
goto_list: []
- area_name: 按钮-代理人基础
id_mark: true
pc_rect:
- 1042
- 974
- 1251
- 1017
text: 基础
lcs_percent: 0.5
template_sub_dir: ''
template_id: ''
template_match_threshold: 0.7
color_range: null
goto_list: []
- area_name: 按钮-代理人技能
id_mark: false
pc_rect:
- 1290
- 974
- 1507
- 1013
text: 技能
lcs_percent: 0.5
template_sub_dir: ''
template_id: ''
template_match_threshold: 0.7
color_range: null
goto_list: []
- area_name: 按钮-代理人装备
id_mark: false
pc_rect:
- 1555
- 976
- 1760
- 1016
text: 装备
lcs_percent: 0.5
template_sub_dir: ''
template_id: ''
template_match_threshold: 0.7
color_range: null
goto_list: []
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@assets/game_data/screen_info/agent_info.yml` around lines 35 - 76, This
screen lacks a unique id_mark for the agent detail page; set id_mark: true on
one stable tab entry (e.g., the area with area_name: "按钮-代理人基础" or
"按钮-代理人技能"/"按钮-代理人装备") so the recognizer has a page-specific anchor; locate the
entries for area_name "按钮-代理人基础", "按钮-代理人技能", and "按钮-代理人装备" and change the
chosen entry's id_mark from false to true (leave the others false).

Comment on lines +49 to +93
- area_name: 驱动盘仓库
id_mark: false
pc_rect:
- 93
- 202
- 1320
- 897
text: 街区
lcs_percent: 0.5
template_sub_dir: ''
template_id: ''
template_match_threshold: 0.7
color_range: null
goto_list:
- 仓库-驱动仓库-驱动盘拆解
- area_name: 驱动盘属性
id_mark: false
pc_rect:
- 1408
- 267
- 1838
- 768
text: 街区
lcs_percent: 0.5
template_sub_dir: ''
template_id: ''
template_match_threshold: 0.7
color_range: null
goto_list:
- 仓库-驱动仓库-驱动盘拆解
- area_name: 驱动盘进度条
id_mark: false
pc_rect:
- 1363
- 862
- 1367
- 863
text: 街区
lcs_percent: 0.5
template_sub_dir: ''
template_id: ''
template_match_threshold: 0.7
color_range: null
goto_list:
- 仓库-驱动仓库-驱动盘拆解
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

text 字段疑似复制遗留

新增的三个区域(驱动盘仓库、驱动盘属性、驱动盘进度条)的 text 字段都设置为 "街区",这与它们的 area_name 不匹配。如果这些区域不需要文本匹配功能,建议将 text 设置为空字符串 '';如果需要文本匹配,应填写对应的实际文本。

🔧 建议修复
 - area_name: 驱动盘仓库
   id_mark: false
   pc_rect:
   - 93
   - 202
   - 1320
   - 897
-  text: 街区
+  text: ''
   lcs_percent: 0.5

驱动盘属性驱动盘进度条 同理处理。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@assets/game_data/screen_info/storage_drive_disc.yml` around lines 49 - 93,
The three new areas have their text field incorrectly set to "街区"; update the
text fields for area_name "驱动盘仓库", "驱动盘属性", and "驱动盘进度条" to either an empty
string '' if no OCR/matching is required or to the correct expected text for
each area if OCR matching is intended—ensure the changes are applied to the
corresponding entries (area_name keys) so text aligns with their actual purpose.

Comment on lines +187 to +192
# 检测街区按钮是否不全黑(返回成功)
if not self._is_area_black(screen, '代理人-信息', '按钮-街区'):
self._last_click_time = 0 # 成功后清空
# 拼接并保存截图
self._combine_and_save()
return self.round_success()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

保存失败后仍然会继续翻到下一位角色。

return_and_save()_combine_and_save() 之后无条件返回成功,而 _combine_and_save() / _save_screenshot() 只记日志,不把失败往上传。只要裁剪、缓存或 OCR 提交有一次异常,当前角色就会被静默跳过。建议让 helper 返回 bool,失败时 retry 当前角色,而不是直接进入 下一位代理人

Also applies to: 260-310

🧰 Tools
🪛 Ruff (0.15.4)

[warning] 187-187: Comment contains ambiguous (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF003)


[warning] 187-187: Comment contains ambiguous (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF003)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/zzz_od/application/inventory_scan/agent/agent_scan_app.py` around lines
187 - 192, The save helper (_combine_and_save and its helper _save_screenshot)
currently swallow errors and never return failure, so callers like the block
using _is_area_black -> _combine_and_save() then return round_success() will
skip a character even if saving failed; change _combine_and_save and
_save_screenshot to return a boolean (True on success, False on failure) and
raise/log appropriately, then update callers (e.g., the branch after
_is_area_black and other similar spots in the 260-310 region) to check the
boolean: only set _last_click_time = 0 and call round_success() when saving
succeeded, otherwise do not advance to the next agent and trigger a retry for
the current agent (or requeue the save), ensuring failures are propagated rather
than silently skipped.

Comment on lines +1 to +6
import json
import os
import urllib.request
from datetime import datetime
from typing import Dict, Optional
from urllib.error import HTTPError, URLError
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

先校验下载结果,再原子替换正式翻译文件。

_download_json() 现在只判断“能不能解析成 JSON”,_save_dict() 又直接覆盖 zzz_translation.json。一旦远端返回非 dict 的 JSON,或者写文件过程中被打断,坏文件就会落到正式路径;后面的 TranslationService 读取后会在扫描流程里才报错/退化。建议至少拒绝非 dict payload,并改成临时文件写完后再 replace()

🛠️ 建议修改
+import tempfile
+from pathlib import Path
-from typing import Dict, Optional
+from typing import Any
...
-    def _download_json(self, url: str) -> Optional[Dict]:
+    def _download_json(self, url: str) -> dict[str, Any] | None:
         """下载JSON数据"""
         try:
             req = urllib.request.Request(
                 url,
                 headers={'User-Agent': 'Mozilla/5.0'}
             )
             with urllib.request.urlopen(req, timeout=30) as response:
                 data = response.read()
                 # 验证JSON有效性
                 try:
                     json_content = json.loads(data)
+                    if not isinstance(json_content, dict):
+                        log.error(f"下载的JSON结构无效: {url}")
+                        return None
                     return json_content
                 except json.JSONDecodeError:
                     log.error(f"下载的JSON格式无效: {url}")
                     return None
...
-    def _save_dict(self, translation_dict: Dict):
+    def _save_dict(self, translation_dict: dict[str, Any]) -> None:
         """保存翻译字典"""
-        # 确保目录存在
-        os.makedirs(os.path.dirname(self.dict_path), exist_ok=True)
-        # 保存完整JSON数据
-        with open(self.dict_path, 'w', encoding='utf-8') as f:
-            json.dump(translation_dict, f, ensure_ascii=False, indent=2)
+        dict_path = Path(self.dict_path)
+        dict_path.parent.mkdir(parents=True, exist_ok=True)
+
+        with tempfile.NamedTemporaryFile(
+            "w",
+            encoding="utf-8",
+            dir=dict_path.parent,
+            delete=False,
+        ) as f:
+            json.dump(translation_dict, f, ensure_ascii=False, indent=2)
+            temp_path = Path(f.name)
+
+        temp_path.replace(dict_path)

Also applies to: 119-131, 143-149

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/zzz_od/application/inventory_scan/translation/translation_updater.py`
around lines 1 - 6, Validate the downloaded payload is a dict in
_download_json/_download_json() and refuse non-dict JSON (raise or return error)
instead of accepting any JSON; then change _save_dict/_save_dict() to write the
JSON to a temporary file (e.g., in the same dir with a unique suffix) and only
call os.replace(temp_path, "zzz_translation.json") after the write/flush/close
succeeds to perform an atomic swap so a partial or invalid file never replaces
the live zzz_translation.json read by TranslationService.

Comment on lines +123 to +152
scores.sort(key=lambda x: x[1], reverse=True)

if not scores:
return ""

best_icon_name = scores[0][0]
best_score = float(scores[0][1])
icon_name_match = re.search(r'IconRoleCircle(\d+)', best_icon_name)
if not icon_name_match:
return ""

icon_number = icon_name_match.group(1)
icon_name = f"IconRole{icon_number}"

agent_key = self._find_agent_key_by_icon(icon_name)

if agent_key:
log.debug(f"头像匹配成功: {best_icon_name} -> {agent_key}")
else:
log.warning(f"未找到对应的代理人: {icon_name}")

self._cache_unique_crop_debug(
small_icon=small_icon,
best_icon_name=best_icon_name,
best_score=best_score,
agent_key=agent_key or "",
top_scores=scores[:5]
)

return agent_key or ""
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

缺少相似度阈值会把任何彩色块都硬匹配成某个角色。

当前只要 scores 非空就返回第一名,但没有校验 best_score 本身,也没有看与第二名的差距。模板稍微不准、头像被遮挡或裁剪偏一点时,也会把 location 写成错误角色,而不是留空。建议至少加一个最低分阈值,或结合 top-1/top-2 margin 再接受结果。

🧰 Tools
🪛 Ruff (0.15.4)

[warning] 152-152: Consider moving this statement to an else block

(TRY300)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/zzz_od/application/inventory_scan/utils/agent_icon_matcher.py` around
lines 123 - 152, The current logic in agent_icon_matcher.py sorts scores and
always picks scores[0] without validating similarity; update the code around
scores, best_score, and best_icon_name to require a minimum similarity threshold
(e.g., min_score) and also check a top-1 vs top-2 margin (compare best_score
against second_score or 0 if absent) before accepting the match; if the checks
fail, return "" (and still call _cache_unique_crop_debug with agent_key="") so
false positives are avoided; keep using icon_name_match and
_find_agent_key_by_icon as now but only call them after the score/margin checks
and log appropriately when rejecting for low score or small margin.

Comment on lines +30 to +32
try:
storage_area = self.ctx.screen_loader.get_area('仓库-驱动仓库', '驱动盘属性')
cropped = cv2_utils.crop_image_only(screenshot, storage_area.rect)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

不要复用驱动盘详情区去裁剪音擎截图。

这里从 仓库-驱动仓库/驱动盘属性 取裁剪框,但当前流程运行在音擎仓库。两个页面只要有轻微布局偏差,整批音擎 OCR 都会裁错;后续调整驱动盘界面时,也会无意中把音擎扫描一起带坏。建议给 仓库-音擎仓库 定义独立属性区,再从对应 screen 取 area。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/zzz_od/application/inventory_scan/wengine/wengine_scan_app.py` around
lines 30 - 32, The code is incorrectly reusing the "仓库-驱动仓库/驱动盘属性" crop area for
音擎 screenshots which causes OCR to break when layouts differ; update the logic
in wengine_scan_app (the block using self.ctx.screen_loader.get_area,
storage_area and cv2_utils.crop_image_only) to fetch an independent area for the
音擎 screen (e.g., "仓库-音擎仓库" with a dedicated property region) and use that area's
rect when cropping the screenshot instead of storage_area.rect so 音擎 scans use
their own defined region.

Comment on lines +102 to +105
def _on_open_output_clicked(self):
"""打开输出目录按钮点击事件"""
output_dir = os_utils.get_path_under_work_dir('.debug', 'inventory_exports')
os.startfile(output_dir)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

先创建输出目录再打开。

第一次使用时 inventory_exports 很可能还不存在,这里会直接抛异常,按钮本身就打不开。

可参考的最小修正
 def _on_open_output_clicked(self):
     """打开输出目录按钮点击事件"""
     output_dir = os_utils.get_path_under_work_dir('.debug', 'inventory_exports')
+    os.makedirs(output_dir, exist_ok=True)
     os.startfile(output_dir)
🧰 Tools
🪛 Ruff (0.15.4)

[error] 105-105: Starting a process without a shell

(S606)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/zzz_od/gui/view/game_assistant/inventory_scan_interface.py` around lines
102 - 105, The _on_open_output_clicked handler currently calls
os.startfile(output_dir) without ensuring the directory exists, causing an
exception on first use; modify the _on_open_output_clicked method to create the
output_dir returned by os_utils.get_path_under_work_dir('.debug',
'inventory_exports') if it does not exist (e.g., os.makedirs(output_dir,
exist_ok=True) or using an existing os_utils helper) before calling
os.startfile(output_dir) so the folder is guaranteed to exist when opened.

@@ -0,0 +1,185 @@
import argparse
from pathlib import Path
from typing import List, Tuple
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

使用现代类型注解语法

根据编码规范,Python 3.11+ 应直接使用内置的 listtuple 而非从 typing 导入 ListTuple

♻️ 建议修改
-from typing import List, Tuple

同时更新函数签名中的类型注解,例如第 97-100 行和第 157 行:

def score_icons(
    crop: np.ndarray,
    icon_paths: list[Path],
) -> tuple[list[tuple[str, float]], list[tuple[str, float]], list[tuple[str, float]], list[tuple[str, float]], list[tuple[str, int]], list[tuple[str, float]], list[tuple[str, float]]]:
def show(title: str, items: list[tuple[str, float]]) -> None:

As per coding guidelines: "Target Python 3.11+ and use modern syntax features such as list[str] instead of List[str]"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/analyze_icon_crop.py` at line 3, Replace legacy typing imports and
annotations: remove "from typing import List, Tuple" and use built-in generics
(list, tuple) in annotations; update function signatures such as score_icons and
show to use modern syntax (e.g., score_icons(...) -> tuple[list[tuple[str,
float]], ...] and show(title: str, items: list[tuple[str, float]]) -> None) and
adjust any other occurrences of List/Tuple in this file to list/tuple. Ensure
imports and any type comments are cleaned up and that nullability/optional types
(if present) use "Optional" from typing or "str | None" where appropriate for
Python 3.11 compatibility.

idk500 added a commit to kawayiYokami/ZenlessZoneZero-OneDragon that referenced this pull request May 6, 2026
@idk500
Copy link
Copy Markdown
Collaborator

idk500 commented May 6, 2026

修了,就这么滴吧

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants