Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 5 additions & 7 deletions outlook_web/services/verification_extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down
25 changes: 25 additions & 0 deletions tests/test_ai_fallback_trigger_condition.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""

Expand Down
45 changes: 45 additions & 0 deletions tests/test_verification_extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Loading