新增市場情報候選活動交接預覽
All checks were successful
CD Pipeline / deploy (push) Successful in 1m3s

This commit is contained in:
OoO
2026-05-19 00:54:41 +08:00
parent c08f76f315
commit 7a6afa3055
10 changed files with 473 additions and 39 deletions

View File

@@ -24,6 +24,9 @@ def build_deployment_readiness_preview(
manual_sample_review_evaluation = service.build_manual_sample_review_evaluation(
sample_result={}
)
manual_sample_candidate_handoff = service.build_manual_sample_candidate_handoff(
sample_result={}
)
match_review_plan = service.build_match_review_plan()
opportunity_plan = service.build_opportunity_plan()
opportunity_scoring_plan = service.build_opportunity_scoring_plan()
@@ -121,6 +124,18 @@ def build_deployment_readiness_preview(
and not manual_sample_review_evaluation["database_commit_executed"]
and not manual_sample_review_evaluation["scheduler_attached"]
),
"manual_sample_candidate_handoff_post_safe": bool(
manual_sample_candidate_handoff["mode"]
== "manual_sample_candidate_handoff_preview"
and manual_sample_candidate_handoff["payload_received"]
and not manual_sample_candidate_handoff["payload_persisted"]
and not manual_sample_candidate_handoff["candidate_handoff_persisted"]
and not manual_sample_candidate_handoff["candidate_import_allowed"]
and not manual_sample_candidate_handoff["external_network_executed"]
and not manual_sample_candidate_handoff["database_write_executed"]
and not manual_sample_candidate_handoff["database_commit_executed"]
and not manual_sample_candidate_handoff["scheduler_attached"]
),
"match_review_plan_preview_safe": bool(
match_review_plan["mode"] == "match_review_plan_preview"
and not match_review_plan["review_queue_created"]
@@ -363,6 +378,7 @@ def build_deployment_readiness_preview(
"manual_sample_acceptance": manual_sample_acceptance,
"manual_sample_review": manual_sample_review,
"manual_sample_review_evaluation": manual_sample_review_evaluation,
"manual_sample_candidate_handoff": manual_sample_candidate_handoff,
"match_review_plan": match_review_plan,
"opportunity_plan": opportunity_plan,
"opportunity_scoring_plan": opportunity_scoring_plan,

View File

@@ -345,3 +345,155 @@ def build_manual_sample_review_evaluation_preview(
"do_not_persist_posted_review_payload",
],
}
def _accepted_candidates_from_sample(sample_result, acceptance_contract, limit):
diagnostics = sample_result.get("diagnostics") if isinstance(sample_result, dict) else {}
diagnostics = diagnostics if isinstance(diagnostics, dict) else {}
raw_candidates = diagnostics.get("campaign_link_candidates")
raw_candidates = raw_candidates if isinstance(raw_candidates, list) else []
accepted_bands = set(_thresholds(acceptance_contract)["accepted_candidate_bands"])
normalized = [
_normalize_candidate(item)
for item in raw_candidates
]
return [
item for item in normalized
if item["confidence_band"] in accepted_bands
][:limit]
def build_manual_sample_candidate_handoff_preview(
*,
runtime_status,
acceptance_contract,
sample_result=None,
payload_error=None,
limit=20,
):
"""建立人工樣本候選活動 handoff只產生 preview payload不保存。"""
safe_limit = max(1, min(_as_int(limit, 20), 50))
review = build_manual_sample_review_evaluation_preview(
runtime_status=runtime_status,
acceptance_contract=acceptance_contract,
sample_result=sample_result,
payload_error=payload_error,
)
handoff_ready = bool(
review["payload_valid_json_object"]
and review["sample_result_accepted"]
and review["ready_for_candidate_preview"]
)
candidates = []
if handoff_ready:
platform_code = str(sample_result.get("platform_code") or "")
source_key = str(sample_result.get("source_key") or "")
source_url = str(sample_result.get("source_url") or "")
for index, candidate in enumerate(
_accepted_candidates_from_sample(
sample_result,
acceptance_contract,
safe_limit,
),
start=1,
):
candidates.append(
{
"candidate_key": (
f"{platform_code}:{source_key}:{index}:"
f"{candidate['confidence_band']}:{candidate['score']}"
),
"platform_code": platform_code,
"source_key": source_key,
"source_url": source_url,
"candidate_url": candidate["url"],
"candidate_text": candidate["text"],
"confidence_band": candidate["confidence_band"],
"score": candidate["score"],
"rank_position": index,
"review_status": "needs_operator_review",
"write_status": "blocked_preview_only",
"import_allowed": False,
}
)
blocked_reasons = list(review["blocked_reasons"])
if not handoff_ready:
blocked_reasons.append("candidate_handoff_not_ready")
blocked_reasons.append("candidate_handoff_persist_still_blocked")
return {
"mode": "manual_sample_candidate_handoff_preview",
"review": {
"mode": review["mode"],
"review_result": review["review_result"],
"sample_result_accepted": review["sample_result_accepted"],
"ready_for_candidate_preview": review["ready_for_candidate_preview"],
"review_findings": review["review_findings"],
},
"payload_received": review["payload_received"],
"payload_valid_json_object": review["payload_valid_json_object"],
"payload_error": review["payload_error"],
"payload_persisted": False,
"sample_result_persisted": False,
"handoff_ready": handoff_ready,
"candidate_handoff_created": bool(candidates),
"candidate_handoff_persisted": False,
"candidate_import_allowed": False,
"external_network_executed": False,
"database_connection_opened": False,
"database_session_created": False,
"database_write_executed": False,
"database_commit_executed": False,
"scheduler_attached": False,
"writes_executed": False,
"would_write_database": False,
"blocked_reasons": blocked_reasons,
"handoff_summary": {
"candidate_count": len(candidates),
"limit": safe_limit,
"review_status": "needs_operator_review" if candidates else "blocked",
"import_allowed": False,
},
"candidate_preview_contract": {
"required_fields": [
"candidate_key",
"platform_code",
"source_key",
"source_url",
"candidate_url",
"candidate_text",
"confidence_band",
"score",
"rank_position",
"review_status",
],
"forbidden_actions": [
"insert_market_campaigns",
"insert_market_campaign_products",
"create_crawler_run",
"auto_import_candidates",
],
},
"candidates": candidates,
"operator_next_actions": [
{
"key": "review_candidate_urls",
"label": "人工檢查候選活動 URL、文字與信心分級",
"write_status": "blocked",
},
{
"key": "promote_to_candidate_review_queue_later",
"label": "後續需另行批准才可建立候選審核 queue",
"write_status": "blocked",
},
],
"safe_boundaries": [
"do_not_fetch_external_pages_from_handoff_api",
"do_not_persist_candidate_handoff_payload",
"do_not_import_candidates_from_handoff_preview",
"do_not_write_market_tables_from_handoff_preview",
"do_not_attach_scheduler_from_handoff_preview",
"do_not_touch_momo_db_lifecycle",
],
}

View File

@@ -37,6 +37,7 @@ from services.market_intel.manual_sample_acceptance import (
build_manual_sample_acceptance_preview,
)
from services.market_intel.manual_sample_review import (
build_manual_sample_candidate_handoff_preview,
build_manual_sample_review_evaluation_preview,
build_manual_sample_review_preview,
)
@@ -102,7 +103,7 @@ class MarketIntelRuntimeStatus:
class MarketIntelService:
"""市場情報入口服務,先集中 feature gate 與安全狀態。"""
phase = "phase_50_manual_sample_review_evaluate"
phase = "phase_51_manual_sample_candidate_handoff"
def get_runtime_status(self) -> MarketIntelRuntimeStatus:
return MarketIntelRuntimeStatus(
@@ -476,6 +477,23 @@ class MarketIntelService:
review["phase"] = self.phase
return review
def build_manual_sample_candidate_handoff(
self,
sample_result=None,
payload_error=None,
limit=20,
):
"""回報人工樣本候選活動 handoff只產生 preview不寫 DB。"""
handoff = build_manual_sample_candidate_handoff_preview(
runtime_status=self.get_runtime_status(),
acceptance_contract=self.build_manual_sample_acceptance(),
sample_result=sample_result,
payload_error=payload_error,
limit=limit,
)
handoff["phase"] = self.phase
return handoff
def build_match_review_plan(self):
"""回報商品比對審核計畫;不建立 queue、不自動確認。"""
schema_smoke = self.build_schema_smoke()