diff --git a/CHANGELOG.md b/CHANGELOG.md index a69da157..4a67b2a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to OutlookMail Plus are documented in this file. ### 修复 / Bug Fixes +- **验证码提取大小写被篡改**:验证码提取收口处(规则分支 `_smart_extract_code_by_keywords` / `_fallback_extract_code` 及历史函数 `smart_extract_verification_code` / `fallback_extract_verification_code`,以及 AI 回退 `enhance_verification_with_ai_fallback`)不再强制 `.upper()`,`verification_code` 与 `formatted` 保持邮件原文大小写(如 `ab12cd` 不再返回 `AB12CD`)。补充回归测试。 - **Issue #65 Watchtower 容器镜像过时**:`docker-compose.yml` 中固定 Watchtower 版本为 `containrrr/watchtower:1.7.1`,避免本地缓存的旧版镜像(内嵌 Docker 客户端 API 1.25)连接新版本 Docker Engine(要求 API 1.44+)时失败。README 新增故障排查指引。 ## [v2.7.0] - 2026-05-29 diff --git a/outlook_web/services/verification_extractor.py b/outlook_web/services/verification_extractor.py index 340df2c8..3d0d4a7b 100644 --- a/outlook_web/services/verification_extractor.py +++ b/outlook_web/services/verification_extractor.py @@ -154,7 +154,7 @@ def smart_extract_verification_code(email_content: str) -> Optional[str]: # 过滤掉纯字母的匹配(验证码通常包含数字) for match in matches: if any(c.isdigit() for c in match): - return match.upper() + return match return None @@ -183,8 +183,6 @@ def fallback_extract_verification_code(email_content: str) -> Optional[str]: # 过滤规则 filtered = [] for match in matches: - match_upper = match.upper() - # 必须包含至少一个数字 if not any(c.isdigit() for c in match): continue @@ -209,7 +207,7 @@ def fallback_extract_verification_code(email_content: str) -> Optional[str]: if 2020 <= num <= 2030: continue - filtered.append(match_upper) + filtered.append(match) return filtered[0] if filtered else None @@ -460,7 +458,7 @@ def _smart_extract_code_by_keywords(email_content: str, code_re: re.Pattern) -> for m in code_re.finditer(context): value = m.group(0) if value and any(c.isdigit() for c in value): - return value.upper() + return value return None @@ -492,7 +490,7 @@ def _fallback_extract_code(email_content: str, code_re: re.Pattern) -> Optional[ if 2020 <= num <= 2030: continue - candidates.append(value.upper()) + candidates.append(value) return candidates[0] if candidates else None @@ -1055,7 +1053,7 @@ def _apply_output_policy(payload: Dict[str, Any]) -> Dict[str, Any]: updated = False if ai_code: - result["verification_code"] = ai_code.upper() + result["verification_code"] = ai_code result["code_confidence"] = "high" if ai_confidence == "high" else "low" updated = True if ai_link: diff --git a/tests/test_ai_fallback_trigger_condition.py b/tests/test_ai_fallback_trigger_condition.py index e9966619..dc42c4d7 100644 --- a/tests/test_ai_fallback_trigger_condition.py +++ b/tests/test_ai_fallback_trigger_condition.py @@ -193,6 +193,31 @@ def test_code_high_link_low_enforces_mutual_exclusion(self, mock_config, mock_ai self.assertEqual(result["link_confidence"], "low") +class AiFallbackCasePreservationTests(unittest.TestCase): + """验证码大小写:AI fallback 返回的验证码必须保持原大小写""" + + @patch("outlook_web.services.verification_extractor._call_verification_ai") + @patch("outlook_web.services.verification_extractor.get_verification_ai_runtime_config") + def test_ai_code_case_preserved(self, mock_config, mock_ai_call): + """AI 返回小写 code → 结果保持小写,不转大写""" + mock_config.return_value = _AI_CONFIG + mock_ai_call.return_value = { + "schema_version": "verification_ai_v1", + "verification_code": "cbdfdb", + "verification_link": "", + "confidence": "high", + "reason": "test", + } + extracted = _make_extracted("low", "low") + + result = extractor.enhance_verification_with_ai_fallback(email=_EMAIL_OBJ, extracted=extracted) + + mock_ai_call.assert_called_once() + self.assertTrue(result.get("ai_used")) + self.assertEqual(result["verification_code"], "cbdfdb") + self.assertEqual(result["formatted"], "cbdfdb") + + class AiFallbackEdgeCaseTests(unittest.TestCase): """边界情况:缺失 confidence 字段时默认为 low""" diff --git a/tests/test_verification_extractor.py b/tests/test_verification_extractor.py index 4f435c4f..3909d980 100644 --- a/tests/test_verification_extractor.py +++ b/tests/test_verification_extractor.py @@ -413,6 +413,51 @@ def test_extract_email_text_body_content_type(self): text = extract_email_text(email) self.assertIn("555666", text) + # ==================== 大小写保持测试(回归) ==================== + + def test_smart_extract_preserves_lowercase(self): + """ + 测试用例:智能识别 - 保持小写验证码原样 + + 测试目的:验证智能识别不再私自转换大小写(含数字的小写字母验证码) + """ + content = "Your verification code is ab12cd. Please verify." + result = smart_extract_verification_code(content) + self.assertEqual(result, "ab12cd") + + def test_smart_extract_preserves_mixed_case(self): + """ + 测试用例:智能识别 - 保持大小写混合验证码原样 + """ + content = "您的验证码是 Ab12Cd,请尽快使用。" + result = smart_extract_verification_code(content) + self.assertEqual(result, "Ab12Cd") + + def test_fallback_extract_preserves_lowercase(self): + """ + 测试用例:保底提取 - 保持小写验证码原样 + """ + content = "Please use ab12cd to complete your registration." + result = fallback_extract_verification_code(content) + self.assertEqual(result, "ab12cd") + + def test_fallback_extract_preserves_mixed_case(self): + """ + 测试用例:保底提取 - 保持大小写混合验证码原样 + """ + content = "Please use Ab12Cd to complete your registration." + result = fallback_extract_verification_code(content) + self.assertEqual(result, "Ab12Cd") + + def test_extract_verification_info_preserves_case(self): + """ + 测试用例:完整流程 - verification_code 与 formatted 均保持原大小写 + """ + email = {"body": "Your verification code is ab12cd."} + result = extract_verification_info_from_text(email["body"]) + self.assertEqual(result["verification_code"], "ab12cd") + self.assertEqual(result["formatted"], "ab12cd") + if __name__ == "__main__": unittest.main()