问题类型 / Issue Type
一般 Bug / General Bug
问题描述及复现步骤 / Problem Description & Reproduction Steps
摘要:Recognizer::ocr 读取 batch OCR 缓存时只按 name 匹配,不校验该子识别是否已在 try_add_ocr_node 中被排除。结果是:一个因 only_rec: true 被正确排除出 batch 计划的内联子识别,因 sub_name 与同一 next 列表中另一个内联子识别重名,读到了对方写入的缓存,识别恒定失败。
下面把「已确认的事实」和「我的推测」分开写。
一、问题(已确认部分)
以下每一条都有日志或源码为证,不含猜测。
1. 复现结构
同一个 next 列表里有两个 And 节点,各自带一个内联 OCR 子识别,两者的名称都是 OCR:
- 节点 A
QuickHunt_NoAP_Rice:内联子识别未写 sub_name,取默认名 OCR;普通 det+rec;roi [667,536,78,39],expected ["\\d"]
- 节点 B
QuickHunt_QuickBattleButton:内联子识别显式写 "sub_name": "OCR";only_rec: true;roi [1029,632,89,33],expected ["快速狩猎"]
两者 OCR 参数完全不同,但共用同一个缓存 key OCR。
2. 实际发生的事
prepare_batch_ocr 的计划里只有 A 的 OCR(B 因 only_rec 被正确排除,没有进 entries):
[Recognizer.cpp][L710][prefetch_batch_ocr] prefetch_batch_ocr completed
[entries=[{"name":"OCR"},{"name":"QuickHunt_FastBattleMAXTouchAdventureRoute"}]]
[*ocr_batch_cache_={"OCR":[],"QuickHunt_FastBattleMAXTouchAdventureRoute":[...]}]
此处 key OCR 写入的是 A 的 roi 的 det 结果,本轮为空数组。
随后轮到 B 识别,它虽然不在计划内,却命中了这个缓存:
[Recognizer.cpp][L371][and_] And recognition [name=QuickHunt_QuickBattleButton] [param->all_of.size()=2]
[Recognizer.cpp][L393][and_] And: run inline sub recognition [inline_sub.type=OCR] [inline_sub.sub_name=OCR]
[Recognizer.cpp][L267][ocr] OCR using batch cache [name=OCR] [cached=[]]
[OCRer.cpp][L139][handle_cached] cache is empty
[OCRer.cpp][L90][analyze] OCR [cache_=[]] [all_results_=[]] [filtered_results_=[]] [best_result_=null]
[param_.only_rec=true] [param_.expected=["快速狩猎"]]
[Recognizer.cpp][L403][and_] And: sub recognition failed
[Recognizer.cpp][L422][and_] And recognition failed [name=QuickHunt_QuickBattleButton]
B 的目标文字在画面上清晰可见,其 roi [1029,632,89,33] 精确框住了目标(draw 图可另附)。这是纯假阴性。
3. 同一节点的对照组(同一份日志内)
| 时刻 |
缓存状态 |
结果 |
| 18:46:23.989 |
cache_=null(该轮 batch 未触发) |
快速狩猎 score 0.9998,成功 |
| 18:46:41.353 起,连续 21 次 |
batch cache [cached=[]] |
all_results_=[],全部失败 |
# 未走 batch,走 only_rec 正常路径 —— 成功
[OCRer.cpp][L90][analyze] OCR [cache_=null]
[all_results_=[{"box":[1029,632,89,33],"score":0.999784,"text":"快速狩猎"}]]
[best_result_={"box":[1029,632,89,33],"score":0.999784,"text":"快速狩猎"}]
[param_.only_rec=true] [param_.expected=["快速狩猎"]]
同一节点、同一 expected、同一 roi、同一屏画面,只因该轮 batch 是否触发而结论相反。
统计上是确定性的,非偶发:该日志中 cache is empty 共 21 次,全部是 name=OCR;缓存中 key OCR 为空 21 次,非空 0 次。之后流程在此处死循环。
4. 源码定位
写入侧 source/MaaFramework/Task/PipelineTask.cpp:380(v5.12.2)——已正确排除 only_rec:
if (param.only_rec) {
// 这玩意 Batch 出来结果顺序可能是乱的,不知道哪个是哪个
// 我猜的,没试过,后面有空再看看
return;
}
读取侧 source/MaaFramework/Task/Component/Recognizer.cpp:265(v5.12.2)——没有对应的排除:
if (ocr_batch_cache_ && ocr_batch_cache_->contains(name)) {
const auto& cached = ocr_batch_cache_->at(name);
LogDebug << "OCR using batch cache" << VAR(name) << VAR(cached);
return build_result(name, "OCR", OCRer(image_, rois, param, cached, resource()->ocr_res().recer(param.model), name));
}
这里只用 name 做查找,既不校验该子识别是否在本轮 batch 计划内,也不校验缓存条目的 roi/param 是否与当前子识别一致。于是被写入侧排除的节点,在读取侧被重新拖回 batch 路径,同时丢失了它自己 only_rec 的执行路径(cached 构造不传 deter,是另一条分支)。
同样的问题也适用于其它被 try_add_ocr_node 排除的情形(color_filter 非空、model 不匹配、PreTask ROI 依赖)——它们都可能因重名读到不属于自己的缓存。
5. key 的取值空间没有唯一性保证
collect_ocr_from_sub_recognitions(PipelineTask.cpp:433)中,两种子识别写法取的 key 来源不同:
if (auto* node_name = std::get_if<std::string>(&sub)) {
// 引用式:用被引用节点的真实节点名 —— 节点名唯一,同名必同参,安全
collect_ocr_from_reco(ctx, sub_opt->name, sub_opt->reco_type, sub_opt->reco_param);
}
else {
// 内联式:用 sub_name —— 自由文本,无任何唯一性约束
collect_ocr_from_reco(ctx, inline_sub.sub_name, inline_sub.type, inline_sub.param);
}
内联子识别的 sub_name 与流水线节点名共用一个扁平命名空间,且未写 sub_name 时取默认名 OCR —— 只要同一 next 列表里出现两个内联 OCR 子识别,就极易撞上。
6. 与既有 PR / Issue 的关系
我没有找到覆盖本例(读取侧不校验计划归属)的既有 issue,若有重复请直接关闭。
二、推测(未经验证,仅供参考)
以下是我的猜想,没有实测验证,请以维护者判断为准。
-
修复方向的猜测。 感觉读取侧需要知道「本轮 batch 计划包含哪些条目」。可能的做法:把 BatchOCRPlan(或其 node_names)传给 Recognizer,读取时先确认 name 确实在计划内再用缓存;或者在 OCRCache 的 value 里连带存一份写入时的 param 指纹,读取时比对不一致就回退真实识别。哪种更合适我判断不了。
-
关于「空缓存」的语义。 handle_cached 目前把「缓存存在但为空」直接当作「识别无结果」。我不确定这是否是有意设计——如果 batch 预取本身可能对某些 roi 漏检,那么「预取为空」和「确实没有文字」似乎不宜等同。但这一条可能只是我不了解设计意图。
-
可能不止 only_rec。 如第 4 节所述,color_filter / model 不匹配 / PreTask ROI 这几个排除分支在读取侧同样没有保护。我没有实际构造这几种场景验证,只是读代码推断。
-
关于命名约束。 我们这侧的直接触发条件是「A 未写 sub_name 取了默认名 OCR,B 显式写了 sub_name: "OCR"」。我们会自查并把内联 sub_name 改为唯一命名来规避。但即便使用方全部规范命名,只要两处恰好取了同一个名字且参数不同,问题依旧会复现,所以我倾向于认为框架侧仍需要一层保护——不过这属于设计取舍,可能维护者有不同看法。
MaaFramework 版本 / Version
v5.12.2(日志中框架自报 Version v5.12.2 / Built at Jul 19 2026 14:59:13)
另已核对:v5.9.2 的 Recognizer.cpp:265 与 v5.12.2 逐字相同,only_rec 排除也在同一位置,故该版本应同样受影响。batch OCR 自 v5.7.0 引入(v5.6.0 中 ocr_batch_cache_ 不存在)。
日志文件 / Log Files
关键片段已内联在上文第 2、3 节。完整 maafw.log(约 1.8 MB)与 draw 调试图可按需补充上传。
Pipeline JSON
{
"QuickHunt_CollectMapAdventureRouteDoubleCk1": {
"recognition": "TemplateMatch",
"roi": [413, 592, 159, 110],
"template": ["DoubleExp.png", "DoubleGold.png"],
"green_mask": true,
"threshold": 0.8,
"next": [
"QuickHunt_NoAP_Rice",
"QuickHunt_FastBattleMAXTouchAdventureRoute",
"[JumpBack]QuickHunt_QuickBattleButton"
]
},
"QuickHunt_NoAP_Rice": {
"recognition": "And",
"all_of": [
{
"recognition": "OCR",
"roi": [667, 536, 78, 39],
"expected": ["\\d"]
},
"Sub_Ocr_Red_Clr"
],
"next": ["QuickHunt_CollectMap_Initialize"]
},
"QuickHunt_FastBattleMAXTouchAdventureRoute": {
"recognition": "OCR",
"expected": ["MAX"]
},
"QuickHunt_QuickBattleButton": {
"desc": "本例的受害者:only_rec 已被 batch 正确排除,却因 sub_name 撞名读到了别人的空缓存",
"recognition": "And",
"all_of": [
{
"sub_name": "OCR",
"recognition": "OCR",
"roi": [1029, 632, 89, 33],
"expected": ["快速狩猎"],
"only_rec": true
},
"Sub_Ocr_Enable_Clr"
],
"action": "Click",
"post_wait_freezes": 500
},
"Sub_Ocr_Enable_Clr": {
"desc": "[检测]Ocr按钮背景/文字,亮色",
"recognition": "ColorMatch",
"roi": "OCR",
"method": 40,
"lower": [
0,
0,
215
],
"upper": [
180,
40,
255
],
"count": 40
}
}
其他信息 / Additional Information
我们这侧会先通过统一内联 sub_name 命名来规避,不影响提交本报告。
感谢维护者的工作。如需完整日志、draw 图或进一步的复现配合,请随时告知。
问题类型 / Issue Type
一般 Bug / General Bug
问题描述及复现步骤 / Problem Description & Reproduction Steps
摘要:
Recognizer::ocr读取 batch OCR 缓存时只按name匹配,不校验该子识别是否已在try_add_ocr_node中被排除。结果是:一个因only_rec: true被正确排除出 batch 计划的内联子识别,因sub_name与同一 next 列表中另一个内联子识别重名,读到了对方写入的缓存,识别恒定失败。下面把「已确认的事实」和「我的推测」分开写。
一、问题(已确认部分)
以下每一条都有日志或源码为证,不含猜测。
1. 复现结构
同一个 next 列表里有两个
And节点,各自带一个内联 OCR 子识别,两者的名称都是OCR:QuickHunt_NoAP_Rice:内联子识别未写sub_name,取默认名OCR;普通 det+rec;roi [667,536,78,39],expected ["\\d"]QuickHunt_QuickBattleButton:内联子识别显式写"sub_name": "OCR";only_rec: true;roi [1029,632,89,33],expected ["快速狩猎"]两者 OCR 参数完全不同,但共用同一个缓存 key
OCR。2. 实际发生的事
prepare_batch_ocr的计划里只有 A 的OCR(B 因only_rec被正确排除,没有进 entries):此处 key
OCR写入的是 A 的 roi 的 det 结果,本轮为空数组。随后轮到 B 识别,它虽然不在计划内,却命中了这个缓存:
B 的目标文字在画面上清晰可见,其
roi [1029,632,89,33]精确框住了目标(draw 图可另附)。这是纯假阴性。3. 同一节点的对照组(同一份日志内)
cache_=null(该轮 batch 未触发)快速狩猎score 0.9998,成功batch cache [cached=[]]all_results_=[],全部失败同一节点、同一
expected、同一roi、同一屏画面,只因该轮 batch 是否触发而结论相反。统计上是确定性的,非偶发:该日志中
cache is empty共 21 次,全部是name=OCR;缓存中 keyOCR为空 21 次,非空 0 次。之后流程在此处死循环。4. 源码定位
写入侧
source/MaaFramework/Task/PipelineTask.cpp:380(v5.12.2)——已正确排除only_rec:读取侧
source/MaaFramework/Task/Component/Recognizer.cpp:265(v5.12.2)——没有对应的排除:这里只用
name做查找,既不校验该子识别是否在本轮 batch 计划内,也不校验缓存条目的roi/param是否与当前子识别一致。于是被写入侧排除的节点,在读取侧被重新拖回 batch 路径,同时丢失了它自己only_rec的执行路径(cached 构造不传deter,是另一条分支)。同样的问题也适用于其它被
try_add_ocr_node排除的情形(color_filter非空、model不匹配、PreTaskROI 依赖)——它们都可能因重名读到不属于自己的缓存。5. key 的取值空间没有唯一性保证
collect_ocr_from_sub_recognitions(PipelineTask.cpp:433)中,两种子识别写法取的 key 来源不同:内联子识别的
sub_name与流水线节点名共用一个扁平命名空间,且未写sub_name时取默认名OCR—— 只要同一 next 列表里出现两个内联 OCR 子识别,就极易撞上。6. 与既有 PR / Issue 的关系
Recognizer.cpp:265的读取侧未改动,本例不受影响。node_names→owner_node_names),修的是 AND/OR 节点触发失效;与本例的读取侧撞 key 无关。按我的理解,它合入后会让此类And节点更容易触发 batch,本例只会更频繁出现。sub_name与流水线节点名冲突导致的崩溃,是同一命名空间问题的另一侧面。我没有找到覆盖本例(读取侧不校验计划归属)的既有 issue,若有重复请直接关闭。
二、推测(未经验证,仅供参考)
以下是我的猜想,没有实测验证,请以维护者判断为准。
修复方向的猜测。 感觉读取侧需要知道「本轮 batch 计划包含哪些条目」。可能的做法:把
BatchOCRPlan(或其node_names)传给Recognizer,读取时先确认name确实在计划内再用缓存;或者在OCRCache的 value 里连带存一份写入时的param指纹,读取时比对不一致就回退真实识别。哪种更合适我判断不了。关于「空缓存」的语义。
handle_cached目前把「缓存存在但为空」直接当作「识别无结果」。我不确定这是否是有意设计——如果 batch 预取本身可能对某些 roi 漏检,那么「预取为空」和「确实没有文字」似乎不宜等同。但这一条可能只是我不了解设计意图。可能不止
only_rec。 如第 4 节所述,color_filter/model不匹配 /PreTaskROI 这几个排除分支在读取侧同样没有保护。我没有实际构造这几种场景验证,只是读代码推断。关于命名约束。 我们这侧的直接触发条件是「A 未写
sub_name取了默认名OCR,B 显式写了sub_name: "OCR"」。我们会自查并把内联sub_name改为唯一命名来规避。但即便使用方全部规范命名,只要两处恰好取了同一个名字且参数不同,问题依旧会复现,所以我倾向于认为框架侧仍需要一层保护——不过这属于设计取舍,可能维护者有不同看法。MaaFramework 版本 / Version
v5.12.2(日志中框架自报
Version v5.12.2/Built at Jul 19 2026 14:59:13)另已核对:v5.9.2 的
Recognizer.cpp:265与 v5.12.2 逐字相同,only_rec排除也在同一位置,故该版本应同样受影响。batch OCR 自 v5.7.0 引入(v5.6.0 中ocr_batch_cache_不存在)。日志文件 / Log Files
关键片段已内联在上文第 2、3 节。完整
maafw.log(约 1.8 MB)与 draw 调试图可按需补充上传。Pipeline JSON
{ "QuickHunt_CollectMapAdventureRouteDoubleCk1": { "recognition": "TemplateMatch", "roi": [413, 592, 159, 110], "template": ["DoubleExp.png", "DoubleGold.png"], "green_mask": true, "threshold": 0.8, "next": [ "QuickHunt_NoAP_Rice", "QuickHunt_FastBattleMAXTouchAdventureRoute", "[JumpBack]QuickHunt_QuickBattleButton" ] }, "QuickHunt_NoAP_Rice": { "recognition": "And", "all_of": [ { "recognition": "OCR", "roi": [667, 536, 78, 39], "expected": ["\\d"] }, "Sub_Ocr_Red_Clr" ], "next": ["QuickHunt_CollectMap_Initialize"] }, "QuickHunt_FastBattleMAXTouchAdventureRoute": { "recognition": "OCR", "expected": ["MAX"] }, "QuickHunt_QuickBattleButton": { "desc": "本例的受害者:only_rec 已被 batch 正确排除,却因 sub_name 撞名读到了别人的空缓存", "recognition": "And", "all_of": [ { "sub_name": "OCR", "recognition": "OCR", "roi": [1029, 632, 89, 33], "expected": ["快速狩猎"], "only_rec": true }, "Sub_Ocr_Enable_Clr" ], "action": "Click", "post_wait_freezes": 500 }, "Sub_Ocr_Enable_Clr": { "desc": "[检测]Ocr按钮背景/文字,亮色", "recognition": "ColorMatch", "roi": "OCR", "method": 40, "lower": [ 0, 0, 215 ], "upper": [ 180, 40, 255 ], "count": 40 } }其他信息 / Additional Information
我们这侧会先通过统一内联
sub_name命名来规避,不影响提交本报告。感谢维护者的工作。如需完整日志、draw 图或进一步的复现配合,请随时告知。