feat: 仓库扫描#1950
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (59)
📝 WalkthroughWalkthrough新增完整的“仓库扫描”子系统:屏幕配置与图像分析管线、OCR 后台工作器、解析器、翻译/图标匹配服务、截图缓存与多个扫描应用(驱动盘/音擎/代理人)、GUI 集成及若干调试工具与配置类。 ChangesInventory Scan — end-to-end feature
序列图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: 完成通知
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: 扫描完成
估计代码审查工作量🎯 4 (复杂) | ⏱️ ~60 分钟 建议审查者
诗
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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.
Built for teams:
One agent for your entire SDLC. Right inside Slack. 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. Comment |
fa62de0 to
5b5493d
Compare
|
战术MARK, 请修复冲突, 这个我会重点关注。 看起来文件很多, 这个资源能否延迟加载? |
这个不需要资源 |
7ec97e9 to
8aa8404
Compare
There was a problem hiding this comment.
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.join、os.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
pathliblibrary for path handling instead ofos.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混淆,建议重命名为lum或luminance以提高可读性。♻️ 建议修改
- 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
📒 Files selected for processing (88)
assets/game_data/screen_info/_od_merged.ymlassets/game_data/screen_info/agent_info.ymlassets/game_data/screen_info/agent_list.ymlassets/game_data/screen_info/menu.ymlassets/game_data/screen_info/storage_drive_disc.ymlassets/image_analysis_pipelines/已选中的驱动盘.ymlassets/image_analysis_pipelines/驱动盘属性识别.ymlassets/image_analysis_pipelines/驱动盘方格.ymlassets/image_analysis_pipelines/驱动盘进度条检测.ymlassets/wiki_data/icons/IconRoleCircle01.webpassets/wiki_data/icons/IconRoleCircle05.webpassets/wiki_data/icons/IconRoleCircle09.webpassets/wiki_data/icons/IconRoleCircle10.webpassets/wiki_data/icons/IconRoleCircle11.webpassets/wiki_data/icons/IconRoleCircle12.webpassets/wiki_data/icons/IconRoleCircle13.webpassets/wiki_data/icons/IconRoleCircle14.webpassets/wiki_data/icons/IconRoleCircle15.webpassets/wiki_data/icons/IconRoleCircle16.webpassets/wiki_data/icons/IconRoleCircle17.webpassets/wiki_data/icons/IconRoleCircle18.webpassets/wiki_data/icons/IconRoleCircle20.webpassets/wiki_data/icons/IconRoleCircle21.webpassets/wiki_data/icons/IconRoleCircle22.webpassets/wiki_data/icons/IconRoleCircle23.webpassets/wiki_data/icons/IconRoleCircle24.webpassets/wiki_data/icons/IconRoleCircle25.webpassets/wiki_data/icons/IconRoleCircle26.webpassets/wiki_data/icons/IconRoleCircle27.webpassets/wiki_data/icons/IconRoleCircle28.webpassets/wiki_data/icons/IconRoleCircle29.webpassets/wiki_data/icons/IconRoleCircle30.webpassets/wiki_data/icons/IconRoleCircle31.webpassets/wiki_data/icons/IconRoleCircle32.webpassets/wiki_data/icons/IconRoleCircle35.webpassets/wiki_data/icons/IconRoleCircle36.webpassets/wiki_data/icons/IconRoleCircle37.webpassets/wiki_data/icons/IconRoleCircle38.webpassets/wiki_data/icons/IconRoleCircle39.webpassets/wiki_data/icons/IconRoleCircle40.webpassets/wiki_data/icons/IconRoleCircle41.webpassets/wiki_data/icons/IconRoleCircle42.webpassets/wiki_data/icons/IconRoleCircle43.webpassets/wiki_data/icons/IconRoleCircle44.webpassets/wiki_data/icons/IconRoleCircle45.webpassets/wiki_data/icons/IconRoleCircle46.webpassets/wiki_data/icons/IconRoleCircle47.webpassets/wiki_data/icons/IconRoleCircle48.webpassets/wiki_data/icons/IconRoleCircle49.webpassets/wiki_data/icons/IconRoleCircle50.webpassets/wiki_data/icons/IconRoleCircle51.webpassets/wiki_data/icons/IconRoleCircle52.webpassets/wiki_data/icons/IconRoleCircle53.webpassets/wiki_data/icons/IconRoleCircle54.webpassets/wiki_data/icons/IconRoleCircle55.webpassets/wiki_data/icons/IconRoleCircle56.webpassets/wiki_data/icons/IconRoleCircle57.webpassets/wiki_data/icons/IconRoleCircle58.webpassets/wiki_data/zzz_translation.jsonsrc/zzz_od/application/inventory_scan/__init__.pysrc/zzz_od/application/inventory_scan/agent/__init__.pysrc/zzz_od/application/inventory_scan/agent/agent_scan_app.pysrc/zzz_od/application/inventory_scan/drive_disk/__init__.pysrc/zzz_od/application/inventory_scan/drive_disk/drive_disk_scan_app.pysrc/zzz_od/application/inventory_scan/drive_disk/drive_disk_scan_config.pysrc/zzz_od/application/inventory_scan/drive_disk/drive_disk_scan_const.pysrc/zzz_od/application/inventory_scan/inventory_scan_app.pysrc/zzz_od/application/inventory_scan/inventory_scan_app_factory.pysrc/zzz_od/application/inventory_scan/inventory_scan_config.pysrc/zzz_od/application/inventory_scan/inventory_scan_const.pysrc/zzz_od/application/inventory_scan/ocr_worker.pysrc/zzz_od/application/inventory_scan/parser/__init__.pysrc/zzz_od/application/inventory_scan/parser/agent_parser.pysrc/zzz_od/application/inventory_scan/parser/drive_disk_parser.pysrc/zzz_od/application/inventory_scan/parser/wengine_parser.pysrc/zzz_od/application/inventory_scan/screenshot_cache.pysrc/zzz_od/application/inventory_scan/translation/__init__.pysrc/zzz_od/application/inventory_scan/translation/icon_downloader.pysrc/zzz_od/application/inventory_scan/translation/translation_service.pysrc/zzz_od/application/inventory_scan/translation/translation_updater.pysrc/zzz_od/application/inventory_scan/utils/__init__.pysrc/zzz_od/application/inventory_scan/utils/agent_icon_matcher.pysrc/zzz_od/application/inventory_scan/wengine/__init__.pysrc/zzz_od/application/inventory_scan/wengine/wengine_scan_app.pysrc/zzz_od/gui/view/game_assistant/game_assistant_interface.pysrc/zzz_od/gui/view/game_assistant/inventory_scan_interface.pytools/analyze_icon_crop.pytools/analyze_icon_crop_algos.py
| - 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: | ||
| - 代理人-信息 |
There was a problem hiding this comment.
给“代理人-列表”补一个唯一的 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.
| - 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.
| - 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: | ||
| - 仓库-驱动仓库-驱动盘拆解 |
There was a problem hiding this comment.
不要把内容区域也声明成“跳转到拆解”的入口。
驱动盘仓库、驱动盘属性、驱动盘进度条 这三个区域看起来是取图/识别区域,不是实际按钮。把它们也挂到同一个 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.
| - 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.
| - 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: [] |
There was a problem hiding this comment.
给代理人详情页补一个专属 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.
| - 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).
| - 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: | ||
| - 仓库-驱动仓库-驱动盘拆解 |
There was a problem hiding this comment.
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.
| # 检测街区按钮是否不全黑(返回成功) | ||
| if not self._is_area_black(screen, '代理人-信息', '按钮-街区'): | ||
| self._last_click_time = 0 # 成功后清空 | ||
| # 拼接并保存截图 | ||
| self._combine_and_save() | ||
| return self.round_success() |
There was a problem hiding this comment.
保存失败后仍然会继续翻到下一位角色。
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.
| import json | ||
| import os | ||
| import urllib.request | ||
| from datetime import datetime | ||
| from typing import Dict, Optional | ||
| from urllib.error import HTTPError, URLError |
There was a problem hiding this comment.
先校验下载结果,再原子替换正式翻译文件。
_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.
| 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 "" |
There was a problem hiding this comment.
缺少相似度阈值会把任何彩色块都硬匹配成某个角色。
当前只要 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.
| try: | ||
| storage_area = self.ctx.screen_loader.get_area('仓库-驱动仓库', '驱动盘属性') | ||
| cropped = cv2_utils.crop_image_only(screenshot, storage_area.rect) |
There was a problem hiding this comment.
不要复用驱动盘详情区去裁剪音擎截图。
这里从 仓库-驱动仓库/驱动盘属性 取裁剪框,但当前流程运行在音擎仓库。两个页面只要有轻微布局偏差,整批音擎 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.
| def _on_open_output_clicked(self): | ||
| """打开输出目录按钮点击事件""" | ||
| output_dir = os_utils.get_path_under_work_dir('.debug', 'inventory_exports') | ||
| os.startfile(output_dir) |
There was a problem hiding this comment.
先创建输出目录再打开。
第一次使用时 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 | |||
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
使用现代类型注解语法
根据编码规范,Python 3.11+ 应直接使用内置的 list 和 tuple 而非从 typing 导入 List 和 Tuple。
♻️ 建议修改
-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.
…with upstream/main
This reverts commit 041c303.
|
修了,就这么滴吧 |

Summary by CodeRabbit