補 Gemini 出站守門測試與同款價差放行
All checks were successful
CD Pipeline / deploy (push) Successful in 1m17s

This commit is contained in:
OoO
2026-05-21 15:09:57 +08:00
committed by AiderHeal Bot
parent 1bdb0f2bd8
commit 1c4fcae5ca
5 changed files with 197 additions and 3 deletions

View File

@@ -325,7 +325,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
# ==========================================
# 系統版本與路徑
# ==========================================
SYSTEM_VERSION = "V10.368"
SYSTEM_VERSION = "V10.369"
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
public_url = PUBLIC_URL # 用於模板顯示

View File

@@ -13,6 +13,7 @@
## 📅 詳細更新日誌 (考古存檔)
### 2026-05-21瀏覽器測試守門與 PChome 熱路徑優化
- **V10.369 Gemini 防復發測試與極端價差同款放行**: 新增靜態測試禁止 production code 在 `services.gemini_guard` / `config.py` 之外直接讀 `GEMINI_API_KEY`,並要求所有 Gemini SDK/REST 出站點必須經 `get_gemini_api_key()`;比價 matcher 針對「同品牌 + 明確 identity anchor + 規格完全一致」但競品價格極端偏低的原生露/眉筆案例抑制價格懲罰,避免真同款因價格差被錯降級,同時補回既有 hard-veto 安全斷言。
- **V10.368 比價搜尋錨點強化**: marketplace matcher 補 LUDEYA 蜂王玫瑰外泌微臻霜、雅詩蘭黛微分子肌底原生露、Za / PERIPERA 眉筆眉彩等低信心邊界品牌的 identity anchor並把「兩入組 / 任選色號 / 多色可選 / 櫻花輕盈版」歸為搜尋噪音,讓 MOMO → PChome 搜尋詞更聚焦於同款身份與規格,不被包裝組合或色號選項帶偏。
- **V10.367 Gemini hard egress kill switch**: 新增 `GEMINI_API_HARD_DISABLED=true` 預設硬封鎖,中央 `services.gemini_guard` 會在 hard switch 未解鎖時拒絕 `GEMINI_API_KEY`,即使 `GEMINI_FALLBACK_ENABLED=true` 也不會初始化 SDK 或 REST 出站。Code Review/OpenClaw/MCP/通用 AI fallback 保留 emergency path但必須同時設 `GEMINI_API_HARD_DISABLED=false``GEMINI_FALLBACK_ENABLED=true`,必要時再用 `GEMINI_ALLOWED_CONTEXTS` 限定 caller。
- **V10.366 MCP runtime smoke receipt review**: 新增 `mcp_runtime_smoke_receipt` read-only builder、GET/POST endpoint、UI receipt JSON 審核面板與 deployment readiness smoke target讓操作員貼上 `/api/market_intel/mcp_readiness?execute=true&timeout=3` 的實際收據後,判斷 external/internal MCP runtime 是否可升級為已驗收。

View File

@@ -447,6 +447,16 @@ VARIANT_OPTION_COLOR_WORDS = {
"月光銀影",
}
VARIANT_DESCRIPTOR_NOISE_KEYWORDS = {
"平輸航空版",
"多色任選",
"色號任選",
"任選色號",
"極細筆頭",
"筆頭",
"官方直營",
}
SEARCH_AMBIGUOUS_PRODUCT_TERMS = {
"保護膜",
"保護貼",
@@ -478,6 +488,8 @@ BRAND_ALIAS_OVERRIDES = {
"febreze": ("febreze", "風倍清"),
"jo malone": ("jo malone",),
"prada": ("prada", "普拉達"),
"za": ("za",),
"xiaomi": ("小米有品", "小米", "xiaomi"),
}
PRODUCT_TYPES = {
@@ -1559,9 +1571,22 @@ def score_marketplace_match(
try:
if momo_price and competitor_price:
ratio = float(competitor_price) / max(float(momo_price), 1.0)
allow_price_penalty_suppression = (
shared_anchor
and len(shared_anchor.replace(" ", "")) >= 7
and brand_score >= 0.95
and not hard_veto
and type_score >= 0.55
and spec_score >= 0.99
and token_score >= 0.68
and sequence_score >= 0.72
)
if (ratio < 0.3 or ratio > 3.2) and token_score < 0.78:
price_penalty = 0.12
reasons.append("price_ratio_extreme")
if allow_price_penalty_suppression:
reasons.append("price_penalty_suppressed_exact_identity")
else:
price_penalty = 0.12
reasons.append("price_ratio_extreme")
elif (ratio < 0.48 or ratio > 2.2) and token_score < 0.68:
price_penalty = 0.06
reasons.append("price_ratio_wide")
@@ -1662,6 +1687,35 @@ def score_marketplace_match(
):
score += 0.02
reasons.append("shared_identity_anchor_core_line")
if (
shared_anchor
and len(shared_anchor.replace(" ", "")) >= 6
and brand_score >= 0.95
and not hard_veto
and price_penalty == 0
and type_score >= 0.55
and spec_score >= 0.45
and token_score >= 0.86
and sequence_score >= 0.75
and not variant_descriptor_conflict
):
score += 0.07
reasons.append("shared_identity_anchor_exact_line")
if (
shared_anchor
and len(shared_anchor.replace(" ", "")) >= 5
and brand_score >= 0.95
and not hard_veto
and price_penalty == 0
and type_score >= 0.55
and spec_score >= 0.45
and token_score >= 0.74
and sequence_score >= 0.60
and _shared_variant_descriptors(left, right)
and not variant_descriptor_conflict
):
score += 0.05
reasons.append("shared_variant_descriptor_alignment")
if (
shared_anchor
and len(shared_anchor.replace(" ", "")) >= 6
@@ -1870,12 +1924,30 @@ def _variant_descriptors(identity: ProductIdentity) -> set[str]:
continue
if compact in SEARCH_NOISE_TOKENS or compact in SEARCH_BROAD_ANCHORS:
continue
if any(keyword in compact for keyword in VARIANT_DESCRIPTOR_NOISE_KEYWORDS):
continue
if re.fullmatch(r"[a-z0-9-]+", compact):
continue
descriptors.add(compact.removesuffix(""))
return {token for token in descriptors if token}
def _shared_variant_descriptors(left: ProductIdentity, right: ProductIdentity) -> set[str]:
left_descriptors = _variant_descriptors(left)
right_descriptors = _variant_descriptors(right)
shared: set[str] = set()
for left_descriptor in left_descriptors:
for right_descriptor in right_descriptors:
if left_descriptor == right_descriptor:
shared.add(left_descriptor)
continue
if len(left_descriptor) >= 2 and left_descriptor in right_descriptor:
shared.add(left_descriptor)
elif len(right_descriptor) >= 2 and right_descriptor in left_descriptor:
shared.add(right_descriptor)
return shared
def _is_variant_sensitive_identity(
left: ProductIdentity,
right: ProductIdentity,

View File

@@ -2,9 +2,27 @@
# -*- coding: utf-8 -*-
"""Gemini fallback kill-switch contract."""
import re
from pathlib import Path
from services.ai_provider import AIProviderService, AIResponse
from services.gemini_service import GeminiService
ROOT = Path(__file__).resolve().parents[1]
def _production_python_files():
for folder in ("services", "routes"):
yield from sorted((ROOT / folder).rglob("*.py"))
for filename in ("scheduler.py", "config.py"):
path = ROOT / filename
if path.exists():
yield path
def _rel(path: Path) -> str:
return path.relative_to(ROOT).as_posix()
def test_gemini_guard_defaults_disabled(monkeypatch):
from services.gemini_guard import get_gemini_api_key, is_gemini_fallback_enabled
@@ -110,3 +128,50 @@ def test_openclaw_direct_gemini_call_is_blocked_by_default(monkeypatch):
monkeypatch.setenv("GEMINI_API_KEY", "test-key")
assert svc._call_gemini("system", "user", caller="openclaw_qa_gemini_fallback") is None
def test_no_direct_gemini_api_key_env_read_outside_guard_or_config():
allowed = {"config.py", "services/gemini_guard.py"}
offenders = []
pattern = re.compile(r"os\.getenv\(\s*['\"]GEMINI_API_KEY['\"]")
for path in _production_python_files():
if _rel(path) in allowed:
continue
if pattern.search(path.read_text(encoding="utf-8")):
offenders.append(_rel(path))
assert offenders == []
def test_gemini_outbound_files_are_guarded():
allowed = {
"routes/openclaw_bot_routes.py",
"services/code_review_pipeline_service.py",
"services/gemini_service.py",
"services/mcp_collector_service.py",
"services/openclaw_strategist_service.py",
}
outbound_markers = (
"google.generativeai",
"genai.configure",
"GenerativeModel",
"generate_content(",
"generateContent?key=",
)
offenders = []
unguarded = []
for path in _production_python_files():
text = path.read_text(encoding="utf-8")
has_outbound = any(marker in text for marker in outbound_markers)
if not has_outbound:
continue
rel = _rel(path)
if rel not in allowed:
offenders.append(rel)
if "get_gemini_api_key" not in text:
unguarded.append(rel)
assert offenders == []
assert unguarded == []

View File

@@ -386,6 +386,62 @@ def test_marketplace_matcher_promotes_precise_cosmetics_and_skincare_lines():
assert diagnostics.hard_veto is False
def test_marketplace_matcher_suppresses_price_penalty_for_exact_identity_toner():
from services.marketplace_product_matcher import score_marketplace_match
diagnostics = score_marketplace_match(
"【Estee Lauder 雅詩蘭黛】微分子肌底原生露/櫻花版200ml任選(新上市/化妝水/水精華/無酒精)",
"ESTEE LAUDER 雅詩蘭黛 微分子肌底原生露 200ml",
momo_price=4750,
competitor_price=999,
)
assert diagnostics.score >= 0.76
assert "price_penalty_suppressed_exact_identity" in diagnostics.reasons
def test_marketplace_matcher_promotes_exact_line_near_threshold_without_global_threshold_change():
from services.marketplace_product_matcher import score_marketplace_match
za = score_marketplace_match(
"【Za】官方直營 細芯睛彩雙頭眉筆(色號任選)",
"Za 細芯睛彩雙頭眉筆0.1g",
momo_price=158,
competitor_price=158,
)
assert za.score >= 0.76
assert "shared_identity_anchor_exact_line" in za.reasons
def test_marketplace_matcher_promotes_shared_variant_descriptor_alignment_for_shu():
from services.marketplace_product_matcher import score_marketplace_match
diagnostics = score_marketplace_match(
"【Shu uemura 植村秀】武士刀眉筆(平輸航空版/多色任選/橡棕.暗灰. 灰棕)",
"《Shu Uemura 植村秀》武士刀眉筆(H9) 4g -#橡棕06",
momo_price=699,
competitor_price=699,
)
assert diagnostics.score >= 0.76
assert "shared_variant_descriptor_alignment" in diagnostics.reasons
def test_marketplace_matcher_ignores_generic_variant_noise_for_peripera_brow_pencil():
from services.marketplace_product_matcher import score_marketplace_match
diagnostics = score_marketplace_match(
"【peripera官方直營】雙頭旋轉極細眉筆_多色任選(1.5mm極細筆頭)",
"PERIPERA 雙頭旋轉極細眉筆 09灰褐棕 0.05g",
momo_price=180,
competitor_price=180,
)
assert "variant_descriptor_conflict" not in diagnostics.reasons
assert diagnostics.score < 0.76
def test_marketplace_matcher_rejects_same_count_different_unit_family():
from services.marketplace_product_matcher import score_marketplace_match