{"record":{"id":"6eb33c52b4cb575e","repo":"babalae/better-genshin-impact","slug":"missingresin-resinlimit","errorCode":null,"errorMessage":"计算缺失树脂 {missingResin} 超过树脂上限 {resinLimit}","messagePattern":"计算缺失树脂 (.+?) 超过树脂上限 (.+?)","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"BetterGenshinImpact/GameTask/AutoBoss/AutoBossTask.cs","lineNumber":329,"sourceCode":"        if (resinIconRegion.IsEmpty())\n        {\n            throw new InvalidOperationException(\"未找到原粹树脂图标\");\n        }\n\n        var iconLeft = resinIconSearchRegion.X + resinIconRegion.Left;\n        var iconRight = resinIconSearchRegion.X + resinIconRegion.Right;\n        var iconBottom = resinIconSearchRegion.Y + resinIconRegion.Bottom;\n\n        resinIconRegion.Click();\n        await Delay(500, _ct);\n\n        using var clickedCapture = CaptureToRectArea();\n        var resinLimit = RecognizeOriginalResinLimit(clickedCapture, iconRight);\n        var fullRecoveryTime = RecognizeFullRecoveryTime(clickedCapture, iconLeft, iconBottom);\n        var missingResin = (int)Math.Ceiling(fullRecoveryTime.TotalSeconds / OriginalResinRecoveryInterval.TotalSeconds);\n        if (missingResin > resinLimit)\n        {\n            throw new InvalidOperationException($\"计算缺失树脂 {missingResin} 超过树脂上限 {resinLimit}\");\n        }\n\n        var originalResin = resinLimit - missingResin;\n        _logger.LogInformation(\"{Name}：剩余树脂 {Count}\", Name, originalResin);\n        return new OriginalResinInfo(originalResin, resinLimit);\n    }\n\n    /// <summary>\n    /// 按原右上角 OCR 区域读取当前/上限文本，并取数字串最后三位作为树脂上限。\n    /// </summary>\n    private int RecognizeOriginalResinLimit(ImageRegion capture, int resinIconRight)\n    {\n        var countRect = new Rect(resinIconRight + ScaleX(25), ScaleY(37), ScaleX(120), ScaleY(24));\n        using var countRegion = capture.DeriveCrop(countRect);\n        var countText = OcrFactory.Paddle.OcrWithoutDetector(countRegion.SrcMat);\n        var digits = Regex.Replace(StringUtils.ConvertFullWidthNumToHalfWidth(countText), @\"\\D\", \"\");\n        if (digits.Length < 3)\n        {","sourceCodeStart":311,"sourceCodeEnd":347,"githubUrl":"https://github.com/babalae/better-genshin-impact/blob/a7cb36712dcb409be610257d877fcea3597e9d6b/BetterGenshinImpact/GameTask/AutoBoss/AutoBossTask.cs#L311-L347","documentation":"在 RecognizeOriginalResinInfoFromBigMap 中，缺失树脂数 missingResin = ⌈全部恢复秒数 / 480s⌉ 与 OCR 读到的树脂上限 resinLimit 做一致性校验。正常情况下 missingResin 永远 ≤ resinLimit（满上限约 160×8min≈21.3h），一旦超出说明两者之一被读错，任务以 InvalidOperationException 中止以免用错误的树脂数驱动后续补充逻辑。","triggerScenarios":"全部恢复时间被 OCR 读得过大（例如把其他界面文本里的时间误识别为恢复时间），或树脂上限被读得过小（例如 '120/160' 只识别到 '120'，limitText 取最后三位得到 120 而非 160），导致 ceil(recovery/480) 超过 resinLimit。","commonSituations":"大地图右上角树脂弹窗未完全展开就截图；OCR 区域偏移导致 limit 段丢失；游戏 UI 文案/字号变化使 'XXX/160' 被截断；图标模板定位漂移使 iconRight 偏移、countRect 落在错误位置。","solutions":["在 RecognizeOriginalResinInfoFromBigMap 外层已有 catch（行 282）做兜底，确认该兜底生效；将本次失败计入日志后让领奖界面逻辑接管，不要直接中止整个任务。","增加树脂上限识别的鲁棒性：当 digits 同时含 '/' 时优先按 '/' 拆分取右段作为 limit，而非盲目取最后三位。","对 fullRecoveryTime 做合理性校验（如 > 24h 则判定 OCR 异常并重试一次截图）。","校准 countRect 偏移（resinIconRight + ScaleX(25)）与图标模板，确保区域覆盖 '当前/上限' 整段文本。"],"exampleFix":"// before\nvar missingResin = (int)Math.Ceiling(fullRecoveryTime.TotalSeconds / OriginalResinRecoveryInterval.TotalSeconds);\nif (missingResin > resinLimit)\n{\n    throw new InvalidOperationException($\"计算缺失树脂 {missingResin} 超过树脂上限 {resinLimit}\");\n}\n\n// after\nvar missingResin = (int)Math.Ceiling(fullRecoveryTime.TotalSeconds / OriginalResinRecoveryInterval.TotalSeconds);\nif (missingResin > resinLimit)\n{\n    _logger.LogWarning(\"{Name}：缺失树脂 {Missing} 超过上限 {Limit}，判定为 OCR 异常并跳过预检\", Name, missingResin, resinLimit);\n    throw new InvalidOperationException($\"计算缺失树脂 {missingResin} 超过树脂上限 {resinLimit}\");\n}","handlingStrategy":"validation","validationCode":"// 在 RecognizeOriginalResinInfoFromBigMap 内、抛出前校验恢复时间合理性\nvar maxPlausibleRecovery = TimeSpan.FromHours(24);\nif (fullRecoveryTime > maxPlausibleRecovery)\n{\n    throw new FormatException($\"恢复时间 {fullRecoveryTime} 超出合理范围，疑似 OCR 异常\");\n}\n// resinLimit 来自 RecognizeOriginalResinLimit，已保证 >0；这里只兜底上限合理性\nif (resinLimit is < 60 or > 200)\n{\n    _logger.LogWarning(\"{Name}：树脂上限 {Limit} 异常，跳过预检\", Name, resinLimit);\n    throw new FormatException($\"树脂上限 {resinLimit} 超出常见范围\");\n}","typeGuard":null,"tryCatchPattern":"// 调用方（PreCheckResin 等）已用 catch (Exception e) when (e is not OperationCanceledException) 兜底，\n// 将树脂预检失败降级为返回 null，由领奖界面逻辑接管。保持该兜底即可。","preventionTips":["对 OCR 派生值（恢复时间、上限）做范围合理性校验后再做一致性比较。","保留外层 catch 将预检失败降级，不要让预检失败中止整个 Boss 任务。","在 debug 日志输出 OCR 原文，便于定位是时间还是上限读错。"],"tags":["ocr","resin","sanity-check","genshin","autoboss"],"backgroundTag":null,"analyzedSha":"a7cb36712dcb409be610257d877fcea3597e9d6b","analyzedAt":"2026-08-13T16:44:57.548Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}