feat: 新增仓库材料数量识别任务 - #871
Conversation
- WarehouseInventoryScan custom action:扫描仓库素材页全部已配模板材料数量 - 三段往返扫描 + 回顶逻辑,多偏移 OCR + 众数纠错,防装饰条误读 - 输出 data/combat/warehouse_inventory.json 快照(gitignore) - 新增 34 个材料图标模板,46/46 全量覆盖 - 新增 pipeline、任务入口、schema 注册、8 个单元测试 - 新增中英双语文档 docs/*/protocol/warehouse-inventory.md
There was a problem hiding this comment.
Hey - 我发现了 4 个问题,并留下了一些整体反馈:
WarehouseInventoryScan中的扫描逻辑依赖多个硬编码的魔法数字(滑动坐标、屏幕高度 718、图标高度阈值 80、偏移量 82/85/90/95 等);建议将它们提取为具名常量或一个小的配置结构,以便之后调整 UI 时更容易、更不容易出错。- 数量解析逻辑(对
\d+的正则和max(groups, key=len))在_recognize_item和test_count_parsing_takes_longest_digit_group之间重复;建议把这部分逻辑抽到 action 模块中的一个小的辅助函数里,这样测试就能直接覆盖同一套实现,而不是重新实现一遍。
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- `WarehouseInventoryScan` 中的扫描逻辑依赖多个硬编码的魔法数字(滑动坐标、屏幕高度 718、图标高度阈值 80、偏移量 82/85/90/95 等);建议将它们提取为具名常量或一个小的配置结构,以便之后调整 UI 时更容易、更不容易出错。
- 数量解析逻辑(对 `\d+` 的正则和 `max(groups, key=len)`)在 `_recognize_item` 和 `test_count_parsing_takes_longest_digit_group` 之间重复;建议把这部分逻辑抽到 action 模块中的一个小的辅助函数里,这样测试就能直接覆盖同一套实现,而不是重新实现一遍。
## Individual Comments
### Comment 1
<location path="agent/custom/action/warehouse_inventory.py" line_range="227-231" />
<code_context>
+ # 多个偏移读数取众数(出现最多的),无众数时取第一位
+ from collections import Counter
+
+ counter = Counter(candidates)
+ most_common = counter.most_common()
+ if len(most_common) > 1 and most_common[0][1] > most_common[1][1]:
+ return True, most_common[0][0]
+ return True, candidates[0]
</code_context>
<issue_to_address>
**suggestion:** 计数候选值的最终决策依赖偏移量顺序,可以考虑让它更稳定一些。
当多个 `candidates` 具有相同频率时,回退到 `candidates[0]` 会让结果取决于偏移量的顺序(即哪个偏移最先产生非空的 OCR 结果),而不是取决于候选值本身。建议使用与 `_best_count` 一致的、确定性的“平局决胜”策略,比如选择数字位数最长的值(再按数值大小比较),或者对这些并列的 `candidates` 再调用一次 `_best_count`,从而避免对偏移顺序或轻微 OCR 波动过于敏感。
</issue_to_address>
### Comment 2
<location path="tests/test_warehouse_inventory.py" line_range="5" />
<code_context>
+import re
+from pathlib import Path
+
+from agent.custom.action.warehouse_inventory import WarehouseInventoryScan
+
+
</code_context>
<issue_to_address>
**suggestion (testing):** 缺少覆盖主 `run()` 流程及其成功/失败分支的测试
当前测试只覆盖了一些配置假设和辅助函数,但从未调用 `WarehouseInventoryScan.run()`,导致关键分支没有经过验证。请新增调用 `run()` 的测试,并使用一个模拟的 `Context`/控制器以及一小份合成的材料数据集来覆盖以下场景:
- 当 items.json 无法读取/解析时,`success=False`
- 当找不到任何模板时,`success=False`
- 当没有产生任何计数结果时,`success=False`
- 对可读/不可读/缺失材料时,`counts` 和 `skipped` 的正确划分
- 输出文件的创建以及其 JSON 结构是否符合预期(包括 `counts`、`skipped`、`materials`、timestamp 字段),并且键是排序的
这样可以验证整体的“串线”和控制流程,而不仅仅是辅助工具函数。
Suggested implementation:
```python
import json
import re
from pathlib import Path
from typing import Any, Dict, List
import pytest
from agent.custom.action.warehouse_inventory import WarehouseInventoryScan
def test_items_json_contains_all_rarity_groups() -> None:
"""items.json 材料表包含金/黄/紫/蓝/绿五档稀有度。"""
with open("data/combat/items.json", encoding="utf-8") as f:
items = json.load(f)
assert set(items.keys()) == {"gold", "yellow", "purple", "blue", "green"}
total = sum(len(v) for v in items.values())
assert total >= 46
class _DummyContext:
"""Minimal context object for exercising WarehouseInventoryScan.run()."""
def __init__(self, workdir: Path) -> None:
self.workdir = workdir
self.logs: List[str] = []
# Common logging APIs used by actions; no-op implementations for tests.
def info(self, msg: str) -> None: # pragma: no cover - trivial
self.logs.append(f"INFO: {msg}")
def warning(self, msg: str) -> None: # pragma: no cover - trivial
self.logs.append(f"WARNING: {msg}")
def error(self, msg: str) -> None: # pragma: no cover - trivial
self.logs.append(f"ERROR: {msg}")
def _configure_scan_paths(
scan: WarehouseInventoryScan, tmp_path: Path
) -> Dict[str, Path]:
"""Point the scan instance at test-local paths if it supports them.
Returns the effective paths so tests can easily write fixtures.
"""
items_path = tmp_path / "items.json"
templates_dir = tmp_path / "templates"
output_path = tmp_path / "warehouse_inventory.json"
# Prefer instance attributes if they exist; fall back to class-level constants.
if hasattr(scan, "items_path"):
scan.items_path = items_path # type: ignore[attr-defined]
elif hasattr(scan, "ITEMS_PATH"):
scan.ITEMS_PATH = items_path # type: ignore[attr-defined]
if hasattr(scan, "templates_dir"):
scan.templates_dir = templates_dir # type: ignore[attr-defined]
elif hasattr(scan, "TEMPLATES_DIR"):
scan.TEMPLATES_DIR = templates_dir # type: ignore[attr-defined]
if hasattr(scan, "output_path"):
scan.output_path = output_path # type: ignore[attr-defined]
elif hasattr(scan, "OUTPUT_PATH"):
scan.OUTPUT_PATH = output_path # type: ignore[attr-defined]
return {
"items_path": items_path,
"templates_dir": templates_dir,
"output_path": output_path,
}
def test_run_fails_when_items_json_unreadable(tmp_path: Path) -> None:
"""run() 返回 success=False 当 items.json 无法读取/解析时。"""
ctx = _DummyContext(tmp_path)
scan = WarehouseInventoryScan()
paths = _configure_scan_paths(scan, tmp_path)
# 写入一个非法 JSON 文件,触发解析失败分支。
paths["items_path"].write_text("{not valid json", encoding="utf-8")
result: Dict[str, Any] = scan.run(ctx) # type: ignore[call-arg]
assert isinstance(result, dict)
assert result.get("success") is False
def test_run_fails_when_no_templates_found(tmp_path: Path) -> None:
"""run() 返回 success=False 当没有任何模板文件时。"""
ctx = _DummyContext(tmp_path)
scan = WarehouseInventoryScan()
paths = _configure_scan_paths(scan, tmp_path)
# 合法 items.json,但不创建任何模板目录/文件。
items = {
"gold": [{"name": "Gold Item"}],
"yellow": [],
"purple": [],
"blue": [],
"green": [],
}
paths["items_path"].write_text(json.dumps(items, ensure_ascii=False), encoding="utf-8")
result: Dict[str, Any] = scan.run(ctx) # type: ignore[call-arg]
assert isinstance(result, dict)
assert result.get("success") is False
def test_run_fails_when_no_counts_produced(tmp_path: Path) -> None:
"""run() 返回 success=False 当没有任何计数结果时。"""
ctx = _DummyContext(tmp_path)
scan = WarehouseInventoryScan()
paths = _configure_scan_paths(scan, tmp_path)
# items.json 中定义材料,但模板设计为全部无法匹配/统计。
items = {
"gold": [{"name": "Unmatched Gold"}],
"yellow": [],
"purple": [],
"blue": [],
"green": [],
}
paths["items_path"].write_text(json.dumps(items, ensure_ascii=False), encoding="utf-8")
templates_dir = paths["templates_dir"]
templates_dir.mkdir(parents=True, exist_ok=True)
# 示例模板文件内容:根据具体实现可以是 JSON/YAML;这里写入一个简单占位。
(templates_dir / "template_unmatched.txt").write_text("no matching items", encoding="utf-8")
result: Dict[str, Any] = scan.run(ctx) # type: ignore[call-arg]
assert isinstance(result, dict)
assert result.get("success") is False
def test_run_separates_counts_and_skipped_and_writes_output(tmp_path: Path) -> None:
"""run() 正确区分 counts/skipped 并写出预期 JSON 输出文件。"""
ctx = _DummyContext(tmp_path)
scan = WarehouseInventoryScan()
paths = _configure_scan_paths(scan, tmp_path)
# 构造一个小型数据集:部分可读、部分应被跳过。
items = {
"gold": [{"id": 1, "name": "Readable Gold"}],
"yellow": [{"id": 2, "name": "Skipped Yellow"}],
"purple": [],
"blue": [],
"green": [],
}
paths["items_path"].write_text(json.dumps(items, ensure_ascii=False), encoding="utf-8")
templates_dir = paths["templates_dir"]
templates_dir.mkdir(parents=True, exist_ok=True)
# 简单模板文件名用于在实现中通过正则/字符串匹配。
# 使用 re 避免导入未使用。
template_name = "template_readable.txt"
assert re.match(r"template_.*\.txt", template_name)
(templates_dir / template_name).write_text("Readable Gold", encoding="utf-8")
result: Dict[str, Any] = scan.run(ctx) # type: ignore[call-arg]
assert isinstance(result, dict)
assert result.get("success") is True
# 验证输出文件存在且 JSON 结构包含期望字段。
output_path = paths["output_path"]
assert output_path.exists()
output = json.loads(output_path.read_text(encoding="utf-8"))
# 至少包含 counts, skipped, materials, timestamp 这些字段。
assert set(output.keys()) >= {"counts", "skipped", "materials", "timestamp"}
# counts 与 skipped 应该是列表/字典一类的可迭代结构。
counts = output["counts"]
skipped = output["skipped"]
assert isinstance(counts, (list, dict))
assert isinstance(skipped, (list, dict))
# 简单检查 counts 与 skipped 之间有区分(至少一侧非空)。
assert bool(counts) or bool(skipped)
# materials 应该反映输入材料表。
materials = output["materials"]
assert isinstance(materials, dict)
assert "gold" in materials
# timestamp 为字符串(具体格式交由实现决定)。
assert isinstance(output["timestamp"], str)
# 额外检查输出 JSON 的键顺序是有序的(实现通常使用 sort_keys=True)。
# 这里通过重新 dump 再比较键列表验证。
loaded_keys = list(output.keys())
sorted_keys = sorted(loaded_keys)
assert loaded_keys == sorted_keys
```
这些测试假设:
1. `WarehouseInventoryScan` 暴露 `items_path` / `templates_dir` / `output_path` 实例属性,或对应的类常量 `ITEMS_PATH` / `TEMPLATES_DIR` / `OUTPUT_PATH`,并在 `run()` 中使用这些路径。
2. `run(context)` 返回一个 `dict`,包含 `success` 布尔字段。
3. `run()` 在成功时会写出一个 JSON 文件,包含至少 `counts`, `skipped`, `materials`, `timestamp` 几个字段,并使用 `sort_keys=True`(或生成按键名排序的字典)写入。
如果当前实现与上述假设不同,请根据实际 API 做以下调整:
- 若路径配置方式不同(例如通过配置对象或 `context` 提供),将 `_configure_scan_paths()` 改为设置实际使用的配置字段,并在各测试中使用相同的辅助函数。
- 若 `run()` 返回值结构不同(例如返回一个自定义结果对象),将断言中对 `result["success"]` 的访问改为对应的属性/字段。
- 若输出文件名或位置不同,更新 `_configure_scan_paths()` 返回的 `output_path` 与 `test_run_separates_counts_and_skipped_and_writes_output` 中的路径断言,使其匹配当前实现。
- 若实现未保证 JSON 键排序,请在生产代码中使用 `json.dump(..., sort_keys=True)`,或相应方式确保键按字典序排序,以满足测试中对键顺序的断言。
</issue_to_address>
### Comment 3
<location path="tests/test_warehouse_inventory.py" line_range="42-51" />
<code_context>
+ assert not missing, f"缺少模板的材料: {missing}"
+
+
+def test_count_parsing_takes_longest_digit_group() -> None:
+ """数量解析取最长数字组,忽略噪声。"""
+ # 模拟 ocr_text 返回值(带噪声的情况)
+ cases = [
+ ("123", 123),
+ ("1,234", 1234),
+ ("12 个噪声 3456", 3456),
+ ("", None),
+ ("x", None),
+ ]
+ for text, expected in cases:
+ groups = re.findall(r"\d+", text.replace(",", ""))
+ if not groups:
+ assert expected is None
+ else:
+ assert int(max(groups, key=len)) == expected
+
+
</code_context>
<issue_to_address>
**suggestion (testing):** 数量解析的测试复用了逻辑,而不是直接覆盖 `_recognize_item` 或一个共享的辅助函数
`test_count_parsing_takes_longest_digit_group` 通过 `re.findall`/`max(groups, key=len)` 重新实现了解析逻辑,而不是调用生产代码,这样即使它和 `_recognize_item` 出现差异也不会导致测试失败。
建议:
- 把解析逻辑抽取到一个辅助函数(比如 `_parse_count_text(text: str) -> int | None`),让 `_recognize_item` 和测试都使用这个函数,或
- 重构 `_recognize_item`,使其可以在单元测试中通过一个伪造的 `context`/`img` 注入可控的 `ocr_text`。
这样可以让测试始终和真实实现保持一致。
Suggested implementation:
```python
from agent.custom.action.warehouse_inventory import WarehouseInventoryScan, _parse_count_text
```
```python
missing = [item_id for group in items.values() for item_id in group if not scan._has_template(item_id)]
assert not missing, f"缺少模板的材料: {missing}"
def test_count_parsing_takes_longest_digit_group() -> None:
"""数量解析取最长数字组,忽略噪声。"""
# 模拟 ocr_text 返回值(带噪声的情况)
cases = [
("123", 123),
("1,234", 1234),
("12 个噪声 3456", 3456),
("", None),
("x", None),
]
for text, expected in cases:
assert _parse_count_text(text) == expected
from agent.custom.action.warehouse_inventory import WarehouseInventoryScan, _parse_count_text
```
要完整实现这个建议,还需要更新 `agent/custom/action/warehouse_inventory.py`:
1. 将现有数量解析逻辑抽取为一个共享辅助函数:
```python
# module-level helper
def _parse_count_text(text: str) -> int | None:
"""
Parse OCR text for an item count.
Strategy:
- Remove commas
- Find all digit groups
- Use the longest digit group as the count
- Return None if no digits found
"""
text = text.replace(",", "")
groups = re.findall(r"\d+", text)
if not groups:
return None
return int(max(groups, key=len))
```
2. 重构 `_recognize_item`(或相关方法),使其使用该辅助函数而不是内联正则逻辑,例如:
```python
count = _parse_count_text(ocr_text) # or whatever variable holds the OCR string
```
3. 确保在 `warehouse_inventory.py` 中已导入 `re`,如果没有则补充。
这些改动可以让测试和生产实现保持对齐,同时避免重复实现解析逻辑。
</issue_to_address>
### Comment 4
<location path="tests/test_warehouse_inventory.py" line_range="75-79" />
<code_context>
+ assert scan._best_count([73, 3]) == 73
+
+
+def test_action_registered_in_action_modules() -> None:
+ """warehouse_inventory 模块在 ACTION_MODULES 注册表中。"""
+ from agent.custom.action import ACTION_MODULES
+
+ assert "warehouse_inventory" in ACTION_MODULES
</code_context>
<issue_to_address>
**suggestion (testing):** 建议同时断言已注册的动作名称和类型,以完整验证“串线”是否正确
目前测试只检查模块名是否在 `ACTION_MODULES` 中,并未确认导入该模块之后是否真的将 `WarehouseInventoryScan` 按预期动作名注册了。建议扩展测试,引入 `AgentServer`(或负责注册的模块),并断言注册表中存在名为 "WarehouseInventoryScan" 的条目且其值就是 `WarehouseInventoryScan` 本身,从而完整验证动作注册 wiring。
Suggested implementation:
```python
def test_action_registered_in_action_modules() -> None:
"""warehouse_inventory 模块在 ACTION_MODULES 注册表中,并且 WarehouseInventoryScan 正确注册。"""
# 导入 ACTION_MODULES 以确保模块注册
from agent.custom.action import ACTION_MODULES
# 导入 AgentServer(或等效入口),以触发所有默认 action wiring
from agent.server import AgentServer # noqa: F401
# 模块级注册:warehouse_inventory 模块应该在 ACTION_MODULES 中
assert "warehouse_inventory" in ACTION_MODULES
# 断言 WarehouseInventoryScan 已按预期名称注册到动作注册表中
# 注意:下面的访问路径和注册表名称需要与实际实现保持一致,
# 例如 ACTION_REGISTRY、REGISTERED_ACTIONS 等。
from agent.custom.action.warehouse_inventory import WarehouseInventoryScan
# 以下示例假设存在一个全局动作注册表 ACTION_REGISTRY,
# 并且键是动作名称,值是动作类。
from agent.custom.action import ACTION_REGISTRY # type: ignore[attr-defined]
assert "WarehouseInventoryScan" in ACTION_REGISTRY
assert ACTION_REGISTRY["WarehouseInventoryScan"] is WarehouseInventoryScan
```
1. 将 `from agent.server import AgentServer` 调整为实际触发 action 注册的入口模块路径(如果不叫 `agent.server.AgentServer`,请改为正确的模块和类)。
2. 将 `ACTION_REGISTRY` 替换为代码库中实际使用的动作注册表名称和导入路径(例如 `ACTIONS`, `REGISTERED_ACTIONS`,或 `ACTION_MODULES["warehouse_inventory"].actions` 等)。
3. 如果动作名称并非 `"WarehouseInventoryScan"`(比如使用 snake_case 或其他别名),请将 `"WarehouseInventoryScan"` 更新为真实的注册键。
4. 如果注册表结构不同(例如值是工厂函数或包含元数据的对象),请调整断言以匹配真实结构(例如 `ACTION_REGISTRY["WarehouseInventoryScan"].cls is WarehouseInventoryScan`)。
</issue_to_address>Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Original comment in English
Hey - I've found 4 issues, and left some high level feedback:
- The scan logic in
WarehouseInventoryScanrelies on several hardcoded magic numbers (swipe coordinates, screen height 718, icon height threshold 80, offsets 82/85/90/95, etc.); consider extracting these into named constants or a small config structure to make future UI adjustments easier and less error-prone. - The count-parsing logic (regex on
\d+andmax(groups, key=len)) is duplicated between_recognize_itemandtest_count_parsing_takes_longest_digit_group; consider factoring this into a small helper function in the action module so tests exercise the same implementation instead of reimplementing it.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The scan logic in `WarehouseInventoryScan` relies on several hardcoded magic numbers (swipe coordinates, screen height 718, icon height threshold 80, offsets 82/85/90/95, etc.); consider extracting these into named constants or a small config structure to make future UI adjustments easier and less error-prone.
- The count-parsing logic (regex on `\d+` and `max(groups, key=len)`) is duplicated between `_recognize_item` and `test_count_parsing_takes_longest_digit_group`; consider factoring this into a small helper function in the action module so tests exercise the same implementation instead of reimplementing it.
## Individual Comments
### Comment 1
<location path="agent/custom/action/warehouse_inventory.py" line_range="227-231" />
<code_context>
+ # 多个偏移读数取众数(出现最多的),无众数时取第一位
+ from collections import Counter
+
+ counter = Counter(candidates)
+ most_common = counter.most_common()
+ if len(most_common) > 1 and most_common[0][1] > most_common[1][1]:
+ return True, most_common[0][0]
+ return True, candidates[0]
</code_context>
<issue_to_address>
**suggestion:** Tie-breaking in count candidates is dependent on offset order and could be made more stable.
When multiple `candidates` share the same frequency, the fallback to `candidates[0]` makes the result depend on offset order (whichever offset produced the first non-empty OCR result) rather than on the values themselves. Consider a deterministic tie-break that’s consistent with `_best_count`, e.g. choosing the value with the longest digit length (then largest numeric value) or reusing `_best_count` on the tied `candidates`, to avoid sensitivity to offset ordering or minor OCR changes.
</issue_to_address>
### Comment 2
<location path="tests/test_warehouse_inventory.py" line_range="5" />
<code_context>
+import re
+from pathlib import Path
+
+from agent.custom.action.warehouse_inventory import WarehouseInventoryScan
+
+
</code_context>
<issue_to_address>
**suggestion (testing):** Missing tests that exercise the main `run()` flow and its success/failure branches
Current tests cover config assumptions and helpers, but never invoke `WarehouseInventoryScan.run()`, leaving key branches unverified. Please add tests that call `run()` with a mocked `Context`/controller and a small synthetic items dataset to cover:
- `success=False` when items.json cannot be read/parsed
- `success=False` when no templates are found
- `success=False` when no counts are produced
- Correct separation of `counts` vs `skipped` for readable/unreadable/missing items
- Creation of the output file with the expected JSON schema (including `counts`, `skipped`, `materials`, timestamp) and sorted keys
This will validate the overall wiring and control flow, not just helper utilities.
Suggested implementation:
```python
import json
import re
from pathlib import Path
from typing import Any, Dict, List
import pytest
from agent.custom.action.warehouse_inventory import WarehouseInventoryScan
def test_items_json_contains_all_rarity_groups() -> None:
"""items.json 材料表包含金/黄/紫/蓝/绿五档稀有度。"""
with open("data/combat/items.json", encoding="utf-8") as f:
items = json.load(f)
assert set(items.keys()) == {"gold", "yellow", "purple", "blue", "green"}
total = sum(len(v) for v in items.values())
assert total >= 46
class _DummyContext:
"""Minimal context object for exercising WarehouseInventoryScan.run()."""
def __init__(self, workdir: Path) -> None:
self.workdir = workdir
self.logs: List[str] = []
# Common logging APIs used by actions; no-op implementations for tests.
def info(self, msg: str) -> None: # pragma: no cover - trivial
self.logs.append(f"INFO: {msg}")
def warning(self, msg: str) -> None: # pragma: no cover - trivial
self.logs.append(f"WARNING: {msg}")
def error(self, msg: str) -> None: # pragma: no cover - trivial
self.logs.append(f"ERROR: {msg}")
def _configure_scan_paths(
scan: WarehouseInventoryScan, tmp_path: Path
) -> Dict[str, Path]:
"""Point the scan instance at test-local paths if it supports them.
Returns the effective paths so tests can easily write fixtures.
"""
items_path = tmp_path / "items.json"
templates_dir = tmp_path / "templates"
output_path = tmp_path / "warehouse_inventory.json"
# Prefer instance attributes if they exist; fall back to class-level constants.
if hasattr(scan, "items_path"):
scan.items_path = items_path # type: ignore[attr-defined]
elif hasattr(scan, "ITEMS_PATH"):
scan.ITEMS_PATH = items_path # type: ignore[attr-defined]
if hasattr(scan, "templates_dir"):
scan.templates_dir = templates_dir # type: ignore[attr-defined]
elif hasattr(scan, "TEMPLATES_DIR"):
scan.TEMPLATES_DIR = templates_dir # type: ignore[attr-defined]
if hasattr(scan, "output_path"):
scan.output_path = output_path # type: ignore[attr-defined]
elif hasattr(scan, "OUTPUT_PATH"):
scan.OUTPUT_PATH = output_path # type: ignore[attr-defined]
return {
"items_path": items_path,
"templates_dir": templates_dir,
"output_path": output_path,
}
def test_run_fails_when_items_json_unreadable(tmp_path: Path) -> None:
"""run() 返回 success=False 当 items.json 无法读取/解析时。"""
ctx = _DummyContext(tmp_path)
scan = WarehouseInventoryScan()
paths = _configure_scan_paths(scan, tmp_path)
# 写入一个非法 JSON 文件,触发解析失败分支。
paths["items_path"].write_text("{not valid json", encoding="utf-8")
result: Dict[str, Any] = scan.run(ctx) # type: ignore[call-arg]
assert isinstance(result, dict)
assert result.get("success") is False
def test_run_fails_when_no_templates_found(tmp_path: Path) -> None:
"""run() 返回 success=False 当没有任何模板文件时。"""
ctx = _DummyContext(tmp_path)
scan = WarehouseInventoryScan()
paths = _configure_scan_paths(scan, tmp_path)
# 合法 items.json,但不创建任何模板目录/文件。
items = {
"gold": [{"name": "Gold Item"}],
"yellow": [],
"purple": [],
"blue": [],
"green": [],
}
paths["items_path"].write_text(json.dumps(items, ensure_ascii=False), encoding="utf-8")
result: Dict[str, Any] = scan.run(ctx) # type: ignore[call-arg]
assert isinstance(result, dict)
assert result.get("success") is False
def test_run_fails_when_no_counts_produced(tmp_path: Path) -> None:
"""run() 返回 success=False 当没有任何计数结果时。"""
ctx = _DummyContext(tmp_path)
scan = WarehouseInventoryScan()
paths = _configure_scan_paths(scan, tmp_path)
# items.json 中定义材料,但模板设计为全部无法匹配/统计。
items = {
"gold": [{"name": "Unmatched Gold"}],
"yellow": [],
"purple": [],
"blue": [],
"green": [],
}
paths["items_path"].write_text(json.dumps(items, ensure_ascii=False), encoding="utf-8")
templates_dir = paths["templates_dir"]
templates_dir.mkdir(parents=True, exist_ok=True)
# 示例模板文件内容:根据具体实现可以是 JSON/YAML;这里写入一个简单占位。
(templates_dir / "template_unmatched.txt").write_text("no matching items", encoding="utf-8")
result: Dict[str, Any] = scan.run(ctx) # type: ignore[call-arg]
assert isinstance(result, dict)
assert result.get("success") is False
def test_run_separates_counts_and_skipped_and_writes_output(tmp_path: Path) -> None:
"""run() 正确区分 counts/skipped 并写出预期 JSON 输出文件。"""
ctx = _DummyContext(tmp_path)
scan = WarehouseInventoryScan()
paths = _configure_scan_paths(scan, tmp_path)
# 构造一个小型数据集:部分可读、部分应被跳过。
items = {
"gold": [{"id": 1, "name": "Readable Gold"}],
"yellow": [{"id": 2, "name": "Skipped Yellow"}],
"purple": [],
"blue": [],
"green": [],
}
paths["items_path"].write_text(json.dumps(items, ensure_ascii=False), encoding="utf-8")
templates_dir = paths["templates_dir"]
templates_dir.mkdir(parents=True, exist_ok=True)
# 简单模板文件名用于在实现中通过正则/字符串匹配。
# 使用 re 避免导入未使用。
template_name = "template_readable.txt"
assert re.match(r"template_.*\.txt", template_name)
(templates_dir / template_name).write_text("Readable Gold", encoding="utf-8")
result: Dict[str, Any] = scan.run(ctx) # type: ignore[call-arg]
assert isinstance(result, dict)
assert result.get("success") is True
# 验证输出文件存在且 JSON 结构包含期望字段。
output_path = paths["output_path"]
assert output_path.exists()
output = json.loads(output_path.read_text(encoding="utf-8"))
# 至少包含 counts, skipped, materials, timestamp 这些字段。
assert set(output.keys()) >= {"counts", "skipped", "materials", "timestamp"}
# counts 与 skipped 应该是列表/字典一类的可迭代结构。
counts = output["counts"]
skipped = output["skipped"]
assert isinstance(counts, (list, dict))
assert isinstance(skipped, (list, dict))
# 简单检查 counts 与 skipped 之间有区分(至少一侧非空)。
assert bool(counts) or bool(skipped)
# materials 应该反映输入材料表。
materials = output["materials"]
assert isinstance(materials, dict)
assert "gold" in materials
# timestamp 为字符串(具体格式交由实现决定)。
assert isinstance(output["timestamp"], str)
# 额外检查输出 JSON 的键顺序是有序的(实现通常使用 sort_keys=True)。
# 这里通过重新 dump 再比较键列表验证。
loaded_keys = list(output.keys())
sorted_keys = sorted(loaded_keys)
assert loaded_keys == sorted_keys
```
这些测试假设:
1. `WarehouseInventoryScan` 暴露 `items_path` / `templates_dir` / `output_path` 实例属性,或对应的类常量 `ITEMS_PATH` / `TEMPLATES_DIR` / `OUTPUT_PATH`,并在 `run()` 中使用这些路径。
2. `run(context)` 返回一个 `dict`,包含 `success` 布尔字段。
3. `run()` 在成功时会写出一个 JSON 文件,包含至少 `counts`, `skipped`, `materials`, `timestamp` 几个字段,并使用 `sort_keys=True`(或生成按键名排序的字典)写入。
如果当前实现与上述假设不同,请根据实际 API 做以下调整:
- 若路径配置方式不同(例如通过配置对象或 `context` 提供),将 `_configure_scan_paths()` 改为设置实际使用的配置字段,并在各测试中使用相同的辅助函数。
- 若 `run()` 返回值结构不同(例如返回一个自定义结果对象),将断言中对 `result["success"]` 的访问改为对应的属性/字段。
- 若输出文件名或位置不同,更新 `_configure_scan_paths()` 返回的 `output_path` 与 `test_run_separates_counts_and_skipped_and_writes_output` 中的路径断言,使其匹配当前实现。
- 若实现未保证 JSON 键排序,请在生产代码中使用 `json.dump(..., sort_keys=True)`,或相应方式确保键按字典序排序,以满足测试中对键顺序的断言。
</issue_to_address>
### Comment 3
<location path="tests/test_warehouse_inventory.py" line_range="42-51" />
<code_context>
+ assert not missing, f"缺少模板的材料: {missing}"
+
+
+def test_count_parsing_takes_longest_digit_group() -> None:
+ """数量解析取最长数字组,忽略噪声。"""
+ # 模拟 ocr_text 返回值(带噪声的情况)
+ cases = [
+ ("123", 123),
+ ("1,234", 1234),
+ ("12 个噪声 3456", 3456),
+ ("", None),
+ ("x", None),
+ ]
+ for text, expected in cases:
+ groups = re.findall(r"\d+", text.replace(",", ""))
+ if not groups:
+ assert expected is None
+ else:
+ assert int(max(groups, key=len)) == expected
+
+
</code_context>
<issue_to_address>
**suggestion (testing):** The count-parsing test duplicates logic instead of directly exercising `_recognize_item` or a shared helper
`test_count_parsing_takes_longest_digit_group` reimplements the parsing logic with `re.findall`/`max(groups, key=len)` instead of using the production code, so it can diverge from `_recognize_item` without failing.
Consider either:
- Extracting the parsing into a helper (e.g. `_parse_count_text(text: str) -> int | None`) used by both `_recognize_item` and the test, or
- Refactoring `_recognize_item` so it can be unit-tested with a fake `context`/`img` that provides controlled `ocr_text`.
This keeps the test aligned with the real implementation.
Suggested implementation:
```python
from agent.custom.action.warehouse_inventory import WarehouseInventoryScan, _parse_count_text
```
```python
missing = [item_id for group in items.values() for item_id in group if not scan._has_template(item_id)]
assert not missing, f"缺少模板的材料: {missing}"
def test_count_parsing_takes_longest_digit_group() -> None:
"""数量解析取最长数字组,忽略噪声。"""
# 模拟 ocr_text 返回值(带噪声的情况)
cases = [
("123", 123),
("1,234", 1234),
("12 个噪声 3456", 3456),
("", None),
("x", None),
]
for text, expected in cases:
assert _parse_count_text(text) == expected
from agent.custom.action.warehouse_inventory import WarehouseInventoryScan, _parse_count_text
```
To fully implement the suggestion, you also need to update `agent/custom/action/warehouse_inventory.py`:
1. Extract the existing count-parsing logic into a shared helper:
```python
# module-level helper
def _parse_count_text(text: str) -> int | None:
"""
Parse OCR text for an item count.
Strategy:
- Remove commas
- Find all digit groups
- Use the longest digit group as the count
- Return None if no digits found
"""
text = text.replace(",", "")
groups = re.findall(r"\d+", text)
if not groups:
return None
return int(max(groups, key=len))
```
2. Refactor `_recognize_item` (or the relevant method) to use this helper instead of inline regex logic, e.g.:
```python
count = _parse_count_text(ocr_text) # or whatever variable holds the OCR string
```
3. Ensure `re` is imported in `warehouse_inventory.py` if not already present.
These changes keep the test aligned with the production implementation while avoiding duplication of parsing logic.
</issue_to_address>
### Comment 4
<location path="tests/test_warehouse_inventory.py" line_range="75-79" />
<code_context>
+ assert scan._best_count([73, 3]) == 73
+
+
+def test_action_registered_in_action_modules() -> None:
+ """warehouse_inventory 模块在 ACTION_MODULES 注册表中。"""
+ from agent.custom.action import ACTION_MODULES
+
+ assert "warehouse_inventory" in ACTION_MODULES
</code_context>
<issue_to_address>
**suggestion (testing):** Consider also asserting the registered action name and type to fully prove wiring
Right now this only checks that the module name is in `ACTION_MODULES`. It doesn’t confirm that importing the module actually registers `WarehouseInventoryScan` under the expected action name. Please extend the test to import `AgentServer` (or the responsible module) and assert that the registry contains an entry for "WarehouseInventoryScan" and that it points to `WarehouseInventoryScan` itself, so we fully verify the wiring.
Suggested implementation:
```python
def test_action_registered_in_action_modules() -> None:
"""warehouse_inventory 模块在 ACTION_MODULES 注册表中,并且 WarehouseInventoryScan 正确注册。"""
# 导入 ACTION_MODULES 以确保模块注册
from agent.custom.action import ACTION_MODULES
# 导入 AgentServer(或等效入口),以触发所有默认 action wiring
from agent.server import AgentServer # noqa: F401
# 模块级注册:warehouse_inventory 模块应该在 ACTION_MODULES 中
assert "warehouse_inventory" in ACTION_MODULES
# 断言 WarehouseInventoryScan 已按预期名称注册到动作注册表中
# 注意:下面的访问路径和注册表名称需要与实际实现保持一致,
# 例如 ACTION_REGISTRY、REGISTERED_ACTIONS 等。
from agent.custom.action.warehouse_inventory import WarehouseInventoryScan
# 以下示例假设存在一个全局动作注册表 ACTION_REGISTRY,
# 并且键是动作名称,值是动作类。
from agent.custom.action import ACTION_REGISTRY # type: ignore[attr-defined]
assert "WarehouseInventoryScan" in ACTION_REGISTRY
assert ACTION_REGISTRY["WarehouseInventoryScan"] is WarehouseInventoryScan
```
1. 将 `from agent.server import AgentServer` 调整为实际触发 action 注册的入口模块路径(如果不叫 `agent.server.AgentServer`,请改为正确的模块和类)。
2. 将 `ACTION_REGISTRY` 替换为代码库中实际使用的动作注册表名称和导入路径(例如 `ACTIONS`, `REGISTERED_ACTIONS`,或 `ACTION_MODULES["warehouse_inventory"].actions` 等)。
3. 如果动作名称并非 `"WarehouseInventoryScan"`(比如使用 snake_case 或其他别名),请将 `"WarehouseInventoryScan"` 更新为真实的注册键。
4. 如果注册表结构不同(例如值是工厂函数或包含元数据的对象),请调整断言以匹配真实结构(例如 `ACTION_REGISTRY["WarehouseInventoryScan"].cls is WarehouseInventoryScan`)。
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthrough新增 Changes仓库库存功能
Estimated code review effort: 4 (复杂) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant WarehouseInventoryTask
participant WarehouseInventoryPipeline
participant WarehouseInventoryScan
participant WarehouseUI
participant OCR
participant InventoryJSON
WarehouseInventoryTask->>WarehouseInventoryPipeline: 启动每日任务
WarehouseInventoryPipeline->>WarehouseUI: 进入并验证仓库
WarehouseInventoryPipeline->>WarehouseInventoryScan: 执行扫描动作
WarehouseInventoryScan->>WarehouseUI: 执行往返滚动
WarehouseInventoryScan->>OCR: 识别材料数量
OCR-->>WarehouseInventoryScan: 返回多次读数
WarehouseInventoryScan->>InventoryJSON: 原子写入库存快照
WarehouseInventoryPipeline->>WarehouseUI: 退出仓库并验证返回主界面
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@agent/custom/action/warehouse_inventory.py`:
- Around line 110-112: The fallback branch in the inventory scan must not assign
counts[item_id] = 0 when _recognize_item() reports found=False. Mark unmatched
materials in skipped or fail the scan when coverage is incomplete, while
preserving zero only for confirmed readings; add a regression test covering a
material with no readings.
- Around line 133-136: Update the snapshot-writing block in the warehouse
inventory action to serialize JSON into a temporary file within the output
directory, then atomically replace the path in _OUTPUT_PATH only after writing
succeeds. Preserve the existing formatting and encoding, and ensure
temporary-file handling does not leave partial data at the live snapshot path.
In `@docs/en_us/protocol/warehouse-inventory.md`:
- Around line 28-36: Label both warehouse inventory flow-diagram fences as text:
update docs/en_us/protocol/warehouse-inventory.md lines 28-36 and
docs/zh_cn/protocol/warehouse-inventory.md lines 27-35 by changing each opening
fence to a text-labelled fence, with no other content changes.
- Around line 42-45: The material-selection documentation incorrectly states
that adding a template alone is sufficient. Update the listed English site in
docs/en_us/protocol/warehouse-inventory.md lines 42-45 and the corresponding
Chinese site in docs/zh_cn/protocol/warehouse-inventory.md lines 41-43 to
require both a data/combat/items.json entry and the warehouse template image;
clarify that IDs are enumerated from the data file before template validation.
- Around line 47-52: Update docs/en_us/protocol/warehouse-inventory.md lines
47-52 and docs/zh_cn/protocol/warehouse-inventory.md lines 45-49 to document the
fixed 12-page scan schedule: 4 pages down, 4 pages up, then 4 pages down. Remove
the 6→6→6 wording and all claims that scanning stops early.
- Around line 101-103: Update the persisted output semantics at
docs/en_us/protocol/warehouse-inventory.md lines 101-103 and
docs/zh_cn/protocol/warehouse-inventory.md lines 92-94: state that selected
materials missing from the warehouse are recorded as counts[id] = 0, while
materials whose icons are found but whose OCR fails are omitted from counts and
listed in skipped.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 12aae362-c4f6-41d2-8c8c-a2eae3fd9828
⛔ Files ignored due to path filters (34)
resource/base/image/Warehouse/Item-110101.pngis excluded by!**/*.pngresource/base/image/Warehouse/Item-110102.pngis excluded by!**/*.pngresource/base/image/Warehouse/Item-110104.pngis excluded by!**/*.pngresource/base/image/Warehouse/Item-110201.pngis excluded by!**/*.pngresource/base/image/Warehouse/Item-110202.pngis excluded by!**/*.pngresource/base/image/Warehouse/Item-110204.pngis excluded by!**/*.pngresource/base/image/Warehouse/Item-110301.pngis excluded by!**/*.pngresource/base/image/Warehouse/Item-110302.pngis excluded by!**/*.pngresource/base/image/Warehouse/Item-110304.pngis excluded by!**/*.pngresource/base/image/Warehouse/Item-110401.pngis excluded by!**/*.pngresource/base/image/Warehouse/Item-110402.pngis excluded by!**/*.pngresource/base/image/Warehouse/Item-110404.pngis excluded by!**/*.pngresource/base/image/Warehouse/Item-110501.pngis excluded by!**/*.pngresource/base/image/Warehouse/Item-110502.pngis excluded by!**/*.pngresource/base/image/Warehouse/Item-110504.pngis excluded by!**/*.pngresource/base/image/Warehouse/Item-110602.pngis excluded by!**/*.pngresource/base/image/Warehouse/Item-110604.pngis excluded by!**/*.pngresource/base/image/Warehouse/Item-110702.pngis excluded by!**/*.pngresource/base/image/Warehouse/Item-110704.pngis excluded by!**/*.pngresource/base/image/Warehouse/Item-110802.pngis excluded by!**/*.pngresource/base/image/Warehouse/Item-110804.pngis excluded by!**/*.pngresource/base/image/Warehouse/Item-110902.pngis excluded by!**/*.pngresource/base/image/Warehouse/Item-110904.pngis excluded by!**/*.pngresource/base/image/Warehouse/Item-111001.pngis excluded by!**/*.pngresource/base/image/Warehouse/Item-111003.pngis excluded by!**/*.pngresource/base/image/Warehouse/Item-111004.pngis excluded by!**/*.pngresource/base/image/Warehouse/Item-111005.pngis excluded by!**/*.pngresource/base/image/Warehouse/Item-111006.pngis excluded by!**/*.pngresource/base/image/Warehouse/Item-111007.pngis excluded by!**/*.pngresource/base/image/Warehouse/Item-111008.pngis excluded by!**/*.pngresource/base/image/Warehouse/Item-111009.pngis excluded by!**/*.pngresource/base/image/Warehouse/Item-111013.pngis excluded by!**/*.pngresource/base/image/Warehouse/Item-111102.pngis excluded by!**/*.pngresource/base/image/Warehouse/Item-111104.pngis excluded by!**/*.png
📒 Files selected for processing (10)
.gitignoreagent/custom/action/__init__.pyagent/custom/action/warehouse_inventory.pydocs/en_us/protocol/warehouse-inventory.mddocs/zh_cn/protocol/warehouse-inventory.mdinterface.jsonresource/base/pipeline/warehouse_inventory.jsontasks/WarehouseInventory.jsontests/test_warehouse_inventory.pytools/schema/custom.action.schema.json
- 提取 _write_snapshot():同目录临时文件 + os.replace 原子替换 - 失败时清理临时文件并原样抛出,避免留下损坏的部分 JSON - 新增 test_write_snapshot_atomic / test_write_snapshot_failure_cleans_tmp
- 新增材料需同时满足 items.json 条目与模板图(原误写只需模板) - 扫描计划改为固定 12 屏 4/4/4、无提前退出(与代码一致) - 输出语义:未找到记录 counts[id]=0;从未读到进 skipped 且不入 counts
There was a problem hiding this comment.
🧹 Nitpick comments (1)
tests/test_warehouse_inventory.py (1)
97-111: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win验证替换失败时保留已有快照。
当前测试只验证目标文件原本不存在的情况。先写入一个已有快照,再让
os.replace()抛出异常,并断言目标文件内容保持不变。这样可以验证原子写入的核心恢复语义。建议的测试修改
def test_write_snapshot_failure_cleans_tmp(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: scan = WarehouseInventoryScan.__new__(WarehouseInventoryScan) scan._OUTPUT_PATH = str(tmp_path / "out" / "snapshot.json") + target = tmp_path / "out" / "snapshot.json" + target.parent.mkdir(parents=True) + target.write_text('{"counts":{"old":1}}', encoding="utf-8") def boom(src: str, dst: str) -> None: raise OSError("disk full") monkeypatch.setattr("agent.custom.action.warehouse_inventory.os.replace", boom) with pytest.raises(OSError): scan._write_snapshot({"counts": {}}) - assert not (tmp_path / "out" / "snapshot.json").exists() + assert json.loads(target.read_text(encoding="utf-8")) == {"counts": {"old": 1}} assert list((tmp_path / "out").glob("*.tmp")) == []🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/test_warehouse_inventory.py` around lines 97 - 111, Extend test_write_snapshot_failure_cleans_tmp to create an existing snapshot at the target path before invoking _write_snapshot, then force os.replace to raise and assert the target file’s original contents remain unchanged. Preserve the existing assertions that temporary files are cleaned up and the replacement error is propagated.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@tests/test_warehouse_inventory.py`:
- Around line 97-111: Extend test_write_snapshot_failure_cleans_tmp to create an
existing snapshot at the target path before invoking _write_snapshot, then force
os.replace to raise and assert the target file’s original contents remain
unchanged. Preserve the existing assertions that temporary files are cleaned up
and the replacement error is propagated.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: f74cc31a-bb0e-4a37-8efb-fa064efb86ed
📒 Files selected for processing (4)
agent/custom/action/warehouse_inventory.pydocs/en_us/protocol/warehouse-inventory.mddocs/zh_cn/protocol/warehouse-inventory.mdtests/test_warehouse_inventory.py
There was a problem hiding this comment.
♻️ Duplicate comments (1)
agent/custom/action/warehouse_inventory.py (1)
109-114: 🗄️ Data Integrity & Integration | 🟠 Major不要把未观测到的材料写入为
0。当
_recognize_item()返回found=False时,unreadable不会更新。第 113-114 行随后写入counts[item_id] = 0,第 125-127 行再把这个未经确认的值写入快照。模板漏检会被后续计划作战逻辑当成真实缺货。请将该 ID 加入
skipped,或在覆盖不完整时终止扫描。只有 OCR 明确读到0时才写入0。这是上一轮评审中相同问题的重复报告,当前代码仍保留该路径。建议修复
else: - logger.warning(f"仓库中未找到材料 {with_template[item_id]['name']}({item_id}),按 0 计") - counts[item_id] = 0 + logger.warning( + f"仓库中未找到材料 {with_template[item_id]['name']}({item_id}),无法确认数量,跳过" + ) + skipped.append(item_id)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@agent/custom/action/warehouse_inventory.py` around lines 109 - 114, Update the unrecognized-item branch in the inventory scan so an item absent from the warehouse is added to skipped (or the scan is terminated when coverage is incomplete) instead of writing counts[item_id] = 0. Preserve zero counts only for items whose OCR result explicitly reports zero, and ensure the snapshot-writing path cannot persist unconfirmed values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Duplicate comments:
In `@agent/custom/action/warehouse_inventory.py`:
- Around line 109-114: Update the unrecognized-item branch in the inventory scan
so an item absent from the warehouse is added to skipped (or the scan is
terminated when coverage is incomplete) instead of writing counts[item_id] = 0.
Preserve zero counts only for items whose OCR result explicitly reports zero,
and ensure the snapshot-writing path cannot persist unconfirmed values.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: d08ce8ca-b833-4b56-bb0f-89fa8e7e368f
📒 Files selected for processing (1)
agent/custom/action/warehouse_inventory.py
- 魔法数字提取为具名常量(滑动坐标/ROI 偏移/图标高度阈值) - 数量解析抽为模块级 parse_count_from_text(),测试覆盖同一实现 - 多偏移平局时改用 _best_count 聚合,消除对偏移顺序的依赖 - _best_count 无众数时取最小值(装饰条并入是最常见的读大误读) - 新增 run() 主流程测试(失败路径 + counts/skipped 划分 + 输出结构)
功能
新增「仓库材料识别」任务:扫描仓库素材页全部已配模板的材料数量,落盘到
data/combat/warehouse_inventory.json,为未来的计划作战/材料缺口提示等打基础。实现
WarehouseInventoryScancustom action:三段往返扫描(12 屏)+ 扫描前回滚到列表顶部,每个材料读 2-3 次h≥80顶部偏移 /h<80底部偏移)、横向 ROI 收窄 50% 避开装饰竖线、多读数众数纠错data/combat/items.json(46 种材料),只扫有模板的材料——新增材料只需补模板图,无需改代码data/combat/warehouse_inventory.json(updated_at/counts/skipped/materials,已 gitignore)验证
pnpm check:py(ruff + pyright + pytest 67 passed)、pnpm check:schema通过Closes #637
Summary by Sourcery
添加一个仓库材料库存扫描任务,将游戏内仓库材料数量记录到 JSON 快照中,以供未来战斗规划功能使用。
New Features:
WarehouseInventoryScan自定义动作,根据物品模板扫描仓库材料数量,并持久化结构化的库存快照。WarehouseInventory任务条目和流水线定义,将库存扫描集成到现有任务系统中。Enhancements:
warehouse_inventory自定义动作模块,并扩展自定义动作 schema 以描述该新任务。Documentation:
Tests:
warehouse_inventory动作的注册。Chores:
Original summary in English
Summary by Sourcery
Add a warehouse materials inventory scanning task that records in-game warehouse material counts to a JSON snapshot for use by future combat planning features.
New Features:
Enhancements:
Documentation:
Tests:
Chores:
Summary by CodeRabbit
新功能
文档
测试