This commit is contained in:
@@ -19,6 +19,7 @@ def build_deployment_readiness_preview(
|
||||
mcp_fetch_gate = service.build_mcp_fetch_gate()
|
||||
scheduler_plan = service.build_scheduler_plan()
|
||||
manual_sample_plan = service.build_manual_sample_plan()
|
||||
manual_sample_acceptance = service.build_manual_sample_acceptance()
|
||||
match_review_plan = service.build_match_review_plan()
|
||||
opportunity_plan = service.build_opportunity_plan()
|
||||
opportunity_scoring_plan = service.build_opportunity_scoring_plan()
|
||||
@@ -86,6 +87,15 @@ def build_deployment_readiness_preview(
|
||||
and not manual_sample_plan["database_commit_executed"]
|
||||
and not manual_sample_plan["scheduler_attached"]
|
||||
),
|
||||
"manual_sample_acceptance_preview_safe": bool(
|
||||
manual_sample_acceptance["mode"] == "manual_sample_acceptance_preview"
|
||||
and not manual_sample_acceptance["sample_result_loaded"]
|
||||
and not manual_sample_acceptance["candidate_import_allowed"]
|
||||
and not manual_sample_acceptance["external_network_executed"]
|
||||
and not manual_sample_acceptance["database_write_executed"]
|
||||
and not manual_sample_acceptance["database_commit_executed"]
|
||||
and not manual_sample_acceptance["scheduler_attached"]
|
||||
),
|
||||
"match_review_plan_preview_safe": bool(
|
||||
match_review_plan["mode"] == "match_review_plan_preview"
|
||||
and not match_review_plan["review_queue_created"]
|
||||
@@ -289,6 +299,7 @@ def build_deployment_readiness_preview(
|
||||
"/api/market_intel/mcp_fetch_gate",
|
||||
"/api/market_intel/scheduler_plan",
|
||||
"/api/market_intel/manual_sample_plan",
|
||||
"/api/market_intel/manual_sample_acceptance",
|
||||
"/api/market_intel/match_review_plan",
|
||||
"/api/market_intel/opportunity_plan",
|
||||
"/api/market_intel/opportunity_scoring_plan",
|
||||
@@ -323,6 +334,7 @@ def build_deployment_readiness_preview(
|
||||
"mcp_fetch_gate": mcp_fetch_gate,
|
||||
"scheduler_plan": scheduler_plan,
|
||||
"manual_sample_plan": manual_sample_plan,
|
||||
"manual_sample_acceptance": manual_sample_acceptance,
|
||||
"match_review_plan": match_review_plan,
|
||||
"opportunity_plan": opportunity_plan,
|
||||
"opportunity_scoring_plan": opportunity_scoring_plan,
|
||||
|
||||
160
services/market_intel/manual_sample_acceptance.py
Normal file
160
services/market_intel/manual_sample_acceptance.py
Normal file
@@ -0,0 +1,160 @@
|
||||
"""市場情報人工 sample fetch 結果驗收契約。
|
||||
|
||||
本模組只定義第一次 sample fetch 回來後的驗收欄位、門檻與人工決策;
|
||||
不讀外部網站、不查 DB、不寫 DB、不掛排程。
|
||||
"""
|
||||
|
||||
|
||||
REQUIRED_RESULT_FIELDS = (
|
||||
"batch_id",
|
||||
"platform_code",
|
||||
"source_key",
|
||||
"source_url",
|
||||
"status",
|
||||
"status_code",
|
||||
"content_length",
|
||||
"page_hash",
|
||||
"title",
|
||||
"diagnostics",
|
||||
)
|
||||
|
||||
REQUIRED_DIAGNOSTIC_FIELDS = (
|
||||
"link_count",
|
||||
"same_host_link_count",
|
||||
"campaign_link_candidates",
|
||||
)
|
||||
|
||||
|
||||
def build_manual_sample_acceptance_preview(*, runtime_status, manual_sample_plan):
|
||||
"""建立人工樣本結果驗收契約;不載入 sample result。"""
|
||||
gate_checks = {
|
||||
"manual_sample_plan_present": bool(manual_sample_plan),
|
||||
"sample_fetch_not_executed_by_api": not bool(
|
||||
manual_sample_plan.get("sample_fetch_executed")
|
||||
),
|
||||
"external_network_blocked_in_preview": not bool(
|
||||
manual_sample_plan.get("external_network_executed")
|
||||
),
|
||||
"database_write_still_blocked": not bool(
|
||||
getattr(runtime_status, "database_write_allowed", False)
|
||||
),
|
||||
"scheduler_detached": not bool(
|
||||
getattr(runtime_status, "scheduler_attached", False)
|
||||
),
|
||||
}
|
||||
blocked_reasons = [
|
||||
key for key, passed in gate_checks.items()
|
||||
if not passed
|
||||
]
|
||||
blocked_reasons.extend(
|
||||
[
|
||||
"sample_result_not_loaded",
|
||||
"manual_review_required_before_import",
|
||||
]
|
||||
)
|
||||
|
||||
return {
|
||||
"mode": "manual_sample_acceptance_preview",
|
||||
"contract_ready": True,
|
||||
"sample_result_loaded": False,
|
||||
"sample_result_accepted": 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,
|
||||
"gate_checks": gate_checks,
|
||||
"blocked_reasons": blocked_reasons,
|
||||
"required_result_fields": list(REQUIRED_RESULT_FIELDS),
|
||||
"required_diagnostic_fields": list(REQUIRED_DIAGNOSTIC_FIELDS),
|
||||
"acceptance_thresholds": {
|
||||
"http_status_min": 200,
|
||||
"http_status_max": 299,
|
||||
"minimum_content_length": 500,
|
||||
"page_hash_length": 64,
|
||||
"minimum_title_length": 2,
|
||||
"minimum_link_count": 1,
|
||||
"minimum_campaign_candidates": 1,
|
||||
"accepted_candidate_bands": ["high", "medium"],
|
||||
},
|
||||
"acceptance_checks": [
|
||||
{
|
||||
"key": "http_status_ok",
|
||||
"label": "HTTP status 必須為 2xx,redirect / 403 / 429 / 5xx 都不得進入候選導入",
|
||||
"status": "not_evaluated",
|
||||
},
|
||||
{
|
||||
"key": "content_has_body",
|
||||
"label": "content_length 必須超過最低門檻,避免只拿到 bot challenge 或空頁",
|
||||
"status": "not_evaluated",
|
||||
},
|
||||
{
|
||||
"key": "page_fingerprint_present",
|
||||
"label": "page_hash 必須存在,後續才能追蹤同頁變化",
|
||||
"status": "not_evaluated",
|
||||
},
|
||||
{
|
||||
"key": "parser_diagnostics_present",
|
||||
"label": "diagnostics 必須包含 title、link_count 與 campaign_link_candidates",
|
||||
"status": "not_evaluated",
|
||||
},
|
||||
{
|
||||
"key": "candidate_quality_reviewed",
|
||||
"label": "至少 1 筆 high/medium 候選需經人工確認,才可進入候選活動審核",
|
||||
"status": "not_evaluated",
|
||||
},
|
||||
],
|
||||
"reject_conditions": [
|
||||
{
|
||||
"key": "anti_bot_or_login_wall",
|
||||
"label": "頁面出現登入牆、驗證碼、bot challenge、會員頁或購物車內容",
|
||||
},
|
||||
{
|
||||
"key": "unexpected_personal_data",
|
||||
"label": "頁面含非公開個資、帳號資料或訂單資訊",
|
||||
},
|
||||
{
|
||||
"key": "too_many_failures",
|
||||
"label": "同平台樣本連續失敗時,不做快速重試,先回頭審查 adapter 入口",
|
||||
},
|
||||
{
|
||||
"key": "no_campaign_candidates",
|
||||
"label": "沒有任何 high/medium campaign candidate 時,不建立活動候選",
|
||||
},
|
||||
],
|
||||
"operator_decisions": [
|
||||
{
|
||||
"key": "approve_candidate_preview",
|
||||
"label": "樣本可進入候選活動人工審核,但仍不得寫 market_campaigns",
|
||||
"write_status": "blocked",
|
||||
},
|
||||
{
|
||||
"key": "revise_adapter_source",
|
||||
"label": "入口不適合時,調整 adapter source 後重新跑單一樣本",
|
||||
"write_status": "blocked",
|
||||
},
|
||||
{
|
||||
"key": "reject_platform_for_now",
|
||||
"label": "平台風險或阻擋過高時,暫停該平台,只保留其他平台預覽",
|
||||
"write_status": "blocked",
|
||||
},
|
||||
],
|
||||
"promotion_sequence": [
|
||||
"先把 sample result 與 diagnostics 以人工方式審核",
|
||||
"通過後只開 candidate preview,不建立正式 campaign/product",
|
||||
"累積至少 2 個平台樣本通過後,再設計候選活動審核資料流",
|
||||
"任何寫入 market_* 前仍需獨立 approval 與 rollback plan",
|
||||
],
|
||||
"safe_boundaries": [
|
||||
"do_not_accept_login_or_member_pages",
|
||||
"do_not_accept_anti_bot_challenge_pages",
|
||||
"do_not_import_candidates_without_human_review",
|
||||
"do_not_write_market_tables_from_acceptance_preview",
|
||||
"do_not_attach_scheduler_from_sample_result",
|
||||
"do_not_touch_momo_db_lifecycle",
|
||||
],
|
||||
}
|
||||
@@ -33,6 +33,9 @@ from services.market_intel.mcp_readiness import build_mcp_readiness_plan
|
||||
from services.market_intel.manual_sample_plan import (
|
||||
build_manual_sample_fetch_plan_preview,
|
||||
)
|
||||
from services.market_intel.manual_sample_acceptance import (
|
||||
build_manual_sample_acceptance_preview,
|
||||
)
|
||||
from services.market_intel.migration_blueprint import build_migration_blueprint
|
||||
from services.market_intel.migration_catalog_review import (
|
||||
build_migration_catalog_review_preview,
|
||||
@@ -95,7 +98,7 @@ class MarketIntelRuntimeStatus:
|
||||
class MarketIntelService:
|
||||
"""市場情報入口服務,先集中 feature gate 與安全狀態。"""
|
||||
|
||||
phase = "phase_47_manual_sample_fetch_plan"
|
||||
phase = "phase_48_manual_sample_acceptance_contract"
|
||||
|
||||
def get_runtime_status(self) -> MarketIntelRuntimeStatus:
|
||||
return MarketIntelRuntimeStatus(
|
||||
@@ -435,6 +438,15 @@ class MarketIntelService:
|
||||
plan["phase"] = self.phase
|
||||
return plan
|
||||
|
||||
def build_manual_sample_acceptance(self):
|
||||
"""回報人工 sample result 驗收契約;不載入外部結果、不寫 DB。"""
|
||||
acceptance = build_manual_sample_acceptance_preview(
|
||||
runtime_status=self.get_runtime_status(),
|
||||
manual_sample_plan=self.build_manual_sample_plan(),
|
||||
)
|
||||
acceptance["phase"] = self.phase
|
||||
return acceptance
|
||||
|
||||
def build_match_review_plan(self):
|
||||
"""回報商品比對審核計畫;不建立 queue、不自動確認。"""
|
||||
schema_smoke = self.build_schema_smoke()
|
||||
|
||||
Reference in New Issue
Block a user