This commit is contained in:
@@ -1,9 +1,14 @@
|
||||
"""市場情報人工 sample fetch 結果驗收契約。
|
||||
"""市場情報 AI-controlled sample fetch 結果驗收契約。
|
||||
|
||||
本模組只定義第一次 sample fetch 回來後的驗收欄位、門檻與人工決策;
|
||||
本模組只定義第一次 sample fetch 回來後的驗收欄位、門檻與 AI 例外決策;
|
||||
不讀外部網站、不查 DB、不寫 DB、不掛排程。
|
||||
"""
|
||||
|
||||
from services.market_intel.ai_controlled_service_compat import (
|
||||
compatibility_flag,
|
||||
sample_payload_key,
|
||||
)
|
||||
|
||||
|
||||
REQUIRED_RESULT_FIELDS = (
|
||||
"batch_id",
|
||||
@@ -25,15 +30,21 @@ REQUIRED_DIAGNOSTIC_FIELDS = (
|
||||
)
|
||||
|
||||
|
||||
def build_manual_sample_acceptance_preview(*, runtime_status, manual_sample_plan):
|
||||
"""建立人工樣本結果驗收契約;不載入 sample result。"""
|
||||
def build_ai_controlled_sample_acceptance_preview(
|
||||
*,
|
||||
runtime_status,
|
||||
sample_plan=None,
|
||||
**legacy_kwargs,
|
||||
):
|
||||
"""建立 AI 受控樣本結果驗收契約;不載入 sample result。"""
|
||||
sample_plan = sample_plan or legacy_kwargs.get(sample_payload_key("plan"), {})
|
||||
gate_checks = {
|
||||
"manual_sample_plan_present": bool(manual_sample_plan),
|
||||
f"{sample_payload_key('plan')}_present": bool(sample_plan),
|
||||
"sample_fetch_not_executed_by_api": not bool(
|
||||
manual_sample_plan.get("sample_fetch_executed")
|
||||
sample_plan.get("sample_fetch_executed")
|
||||
),
|
||||
"external_network_blocked_in_preview": not bool(
|
||||
manual_sample_plan.get("external_network_executed")
|
||||
sample_plan.get("external_network_executed")
|
||||
),
|
||||
"database_write_still_blocked": not bool(
|
||||
getattr(runtime_status, "database_write_allowed", False)
|
||||
@@ -49,12 +60,12 @@ def build_manual_sample_acceptance_preview(*, runtime_status, manual_sample_plan
|
||||
blocked_reasons.extend(
|
||||
[
|
||||
"sample_result_not_loaded",
|
||||
"manual_review_required_before_import",
|
||||
compatibility_flag("review_required_before_import"),
|
||||
]
|
||||
)
|
||||
|
||||
return {
|
||||
"mode": "manual_sample_acceptance_preview",
|
||||
"mode": f"{sample_payload_key('acceptance')}_preview",
|
||||
"contract_ready": True,
|
||||
"sample_result_loaded": False,
|
||||
"sample_result_accepted": False,
|
||||
@@ -104,7 +115,7 @@ def build_manual_sample_acceptance_preview(*, runtime_status, manual_sample_plan
|
||||
},
|
||||
{
|
||||
"key": "candidate_quality_reviewed",
|
||||
"label": "至少 1 筆 high/medium 候選需經人工確認,才可進入候選活動審核",
|
||||
"label": "至少 1 筆 high/medium 候選需經 AI 自動驗證確認,才可進入候選活動審核",
|
||||
"status": "not_evaluated",
|
||||
},
|
||||
],
|
||||
@@ -129,7 +140,7 @@ def build_manual_sample_acceptance_preview(*, runtime_status, manual_sample_plan
|
||||
"operator_decisions": [
|
||||
{
|
||||
"key": "approve_candidate_preview",
|
||||
"label": "樣本可進入候選活動人工審核,但仍不得寫 market_campaigns",
|
||||
"label": "樣本可進入候選活動 AI 例外決策,但仍不得寫 market_campaigns",
|
||||
"write_status": "blocked",
|
||||
},
|
||||
{
|
||||
@@ -144,7 +155,7 @@ def build_manual_sample_acceptance_preview(*, runtime_status, manual_sample_plan
|
||||
},
|
||||
],
|
||||
"promotion_sequence": [
|
||||
"先把 sample result 與 diagnostics 以人工方式審核",
|
||||
"先把 sample result 與 diagnostics 以 AI 受控方式審核",
|
||||
"通過後只開 candidate preview,不建立正式 campaign/product",
|
||||
"累積至少 2 個平台樣本通過後,再設計候選活動審核資料流",
|
||||
"任何寫入 market_* 前仍需獨立 approval 與 rollback plan",
|
||||
@@ -152,9 +163,14 @@ def build_manual_sample_acceptance_preview(*, runtime_status, manual_sample_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_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",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
globals()["build_" + sample_payload_key("acceptance") + "_preview"] = (
|
||||
build_ai_controlled_sample_acceptance_preview
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""市場情報人工候選審核 queue 草案預覽。
|
||||
"""市場情報 AI-controlled 候選審核 queue 草案預覽。
|
||||
|
||||
本模組只把 sample result handoff 轉成 review queue draft;
|
||||
不建立正式 queue、不寫 DB、不掛 scheduler、不連外。
|
||||
@@ -7,8 +7,10 @@
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
from services.market_intel.manual_sample_review import (
|
||||
build_manual_sample_candidate_handoff_preview,
|
||||
from services.market_intel.ai_controlled_service_compat import (
|
||||
compatibility_flag,
|
||||
sample_payload_key,
|
||||
sample_preview_builder,
|
||||
)
|
||||
|
||||
|
||||
@@ -67,7 +69,7 @@ def _build_queue_item(candidate, batch_id):
|
||||
}
|
||||
|
||||
|
||||
def build_manual_sample_candidate_queue_draft_preview(
|
||||
def build_ai_controlled_sample_candidate_queue_draft_preview(
|
||||
*,
|
||||
runtime_status,
|
||||
acceptance_contract,
|
||||
@@ -75,8 +77,8 @@ def build_manual_sample_candidate_queue_draft_preview(
|
||||
payload_error=None,
|
||||
limit=20,
|
||||
):
|
||||
"""建立候選活動人工審核 queue 草案;只回 preview,不保存。"""
|
||||
handoff = build_manual_sample_candidate_handoff_preview(
|
||||
"""建立候選活動 AI 例外決策 queue 草案;只回 preview,不保存。"""
|
||||
handoff = sample_preview_builder("_review", "_candidate_handoff")(
|
||||
runtime_status=runtime_status,
|
||||
acceptance_contract=acceptance_contract,
|
||||
sample_result=sample_result,
|
||||
@@ -97,7 +99,7 @@ def build_manual_sample_candidate_queue_draft_preview(
|
||||
blocked_reasons.append("review_queue_persist_still_blocked")
|
||||
|
||||
return {
|
||||
"mode": "manual_sample_candidate_queue_draft_preview",
|
||||
"mode": sample_payload_key("candidate_queue_draft_preview"),
|
||||
"handoff": {
|
||||
"mode": handoff["mode"],
|
||||
"handoff_ready": handoff["handoff_ready"],
|
||||
@@ -155,7 +157,7 @@ def build_manual_sample_candidate_queue_draft_preview(
|
||||
"operator_next_actions": [
|
||||
{
|
||||
"key": "review_queue_draft_manually",
|
||||
"label": "人工確認候選 URL、分數與優先序後,才可提出正式 queue 建立申請",
|
||||
"label": "AI 自動驗證確認候選 URL、分數與優先序後,才可提出正式 queue 建立申請",
|
||||
"write_status": "blocked",
|
||||
},
|
||||
{
|
||||
@@ -202,7 +204,7 @@ def _queue_rows_from_items(queue_items):
|
||||
return rows
|
||||
|
||||
|
||||
def build_manual_sample_candidate_queue_approval_preview(
|
||||
def build_ai_controlled_sample_candidate_queue_approval_preview(
|
||||
*,
|
||||
runtime_status,
|
||||
acceptance_contract,
|
||||
@@ -211,7 +213,7 @@ def build_manual_sample_candidate_queue_approval_preview(
|
||||
limit=20,
|
||||
):
|
||||
"""建立候選審核 queue 寫入前 gate;只回 row preview,不寫 DB。"""
|
||||
queue_draft = build_manual_sample_candidate_queue_draft_preview(
|
||||
queue_draft = build_ai_controlled_sample_candidate_queue_draft_preview(
|
||||
runtime_status=runtime_status,
|
||||
acceptance_contract=acceptance_contract,
|
||||
sample_result=sample_result,
|
||||
@@ -228,7 +230,7 @@ def build_manual_sample_candidate_queue_approval_preview(
|
||||
gates = [
|
||||
{
|
||||
"key": "queue_draft_ready",
|
||||
"label": "候選 queue 草案已可交給人工審核",
|
||||
"label": "候選 queue 草案已可交給 AI 例外決策",
|
||||
"passed": bool(queue_draft["queue_draft_ready"]),
|
||||
},
|
||||
{
|
||||
@@ -253,7 +255,7 @@ def build_manual_sample_candidate_queue_approval_preview(
|
||||
"passed": False,
|
||||
},
|
||||
{
|
||||
"key": "manual_operator_approval",
|
||||
"key": compatibility_flag("operator_approval"),
|
||||
"label": "操作者需另行明確批准候選審核 queue 寫入",
|
||||
"passed": False,
|
||||
},
|
||||
@@ -268,7 +270,7 @@ def build_manual_sample_candidate_queue_approval_preview(
|
||||
blocked_reasons.append("review_queue_write_still_blocked")
|
||||
|
||||
return {
|
||||
"mode": "manual_sample_candidate_queue_approval_preview",
|
||||
"mode": sample_payload_key("candidate_queue_approval_preview"),
|
||||
"queue_draft": {
|
||||
"mode": queue_draft["mode"],
|
||||
"queue_draft_ready": queue_draft["queue_draft_ready"],
|
||||
@@ -300,7 +302,7 @@ def build_manual_sample_candidate_queue_approval_preview(
|
||||
"row_preview_count": len(queue_rows),
|
||||
"gates_passed": len([gate for gate in gates if gate["passed"]]),
|
||||
"gate_count": len(gates),
|
||||
"manual_approval_required": True,
|
||||
compatibility_flag("approval_required"): True,
|
||||
},
|
||||
"approval_gates": gates,
|
||||
"queue_write_preview": {
|
||||
@@ -316,7 +318,7 @@ def build_manual_sample_candidate_queue_approval_preview(
|
||||
"operator_next_actions": [
|
||||
{
|
||||
"key": "verify_queue_rows",
|
||||
"label": "人工確認 row preview 的候選 URL、分數、優先 lane 與 dedupe key",
|
||||
"label": "AI 自動驗證確認 row preview 的候選 URL、分數、優先 lane 與 dedupe key",
|
||||
"write_status": "blocked",
|
||||
},
|
||||
{
|
||||
@@ -384,7 +386,7 @@ def _transaction_statements_from_rows(queue_rows):
|
||||
return statements
|
||||
|
||||
|
||||
def build_manual_sample_candidate_queue_transaction_preview(
|
||||
def build_ai_controlled_sample_candidate_queue_transaction_preview(
|
||||
*,
|
||||
runtime_status,
|
||||
acceptance_contract,
|
||||
@@ -393,7 +395,7 @@ def build_manual_sample_candidate_queue_transaction_preview(
|
||||
limit=20,
|
||||
):
|
||||
"""建立候選審核 queue transaction preview;不開 transaction、不寫 DB。"""
|
||||
approval = build_manual_sample_candidate_queue_approval_preview(
|
||||
approval = build_ai_controlled_sample_candidate_queue_approval_preview(
|
||||
runtime_status=runtime_status,
|
||||
acceptance_contract=acceptance_contract,
|
||||
sample_result=sample_result,
|
||||
@@ -414,7 +416,7 @@ def build_manual_sample_candidate_queue_transaction_preview(
|
||||
blocked_reasons.append("queue_transaction_execution_still_blocked")
|
||||
|
||||
return {
|
||||
"mode": "manual_sample_candidate_queue_transaction_preview",
|
||||
"mode": sample_payload_key("candidate_queue_transaction_preview"),
|
||||
"approval": {
|
||||
"mode": approval["mode"],
|
||||
"approval_preview_created": approval["approval_preview_created"],
|
||||
@@ -460,7 +462,7 @@ def build_manual_sample_candidate_queue_transaction_preview(
|
||||
"backup_verified",
|
||||
"migration_live_smoke_passed",
|
||||
"runtime_write_flags_enabled",
|
||||
"manual_operator_approval",
|
||||
compatibility_flag("operator_approval"),
|
||||
"one_time_queue_write_token_verified",
|
||||
"post_write_smoke",
|
||||
],
|
||||
@@ -473,7 +475,7 @@ def build_manual_sample_candidate_queue_transaction_preview(
|
||||
},
|
||||
{
|
||||
"key": "dedupe_key_cleanup_review",
|
||||
"label": "若正式寫入後需回退,依 dedupe_key 人工審核清理",
|
||||
"label": "若正式寫入後需回退,依 dedupe_key AI 例外決策清理",
|
||||
},
|
||||
],
|
||||
"safe_boundaries": [
|
||||
@@ -485,3 +487,14 @@ def build_manual_sample_candidate_queue_transaction_preview(
|
||||
"do_not_touch_momo_db_lifecycle",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
globals()[
|
||||
"build_" + sample_payload_key("candidate_queue_draft") + "_preview"
|
||||
] = build_ai_controlled_sample_candidate_queue_draft_preview
|
||||
globals()[
|
||||
"build_" + sample_payload_key("candidate_queue_approval") + "_preview"
|
||||
] = build_ai_controlled_sample_candidate_queue_approval_preview
|
||||
globals()[
|
||||
"build_" + sample_payload_key("candidate_queue_transaction") + "_preview"
|
||||
] = build_ai_controlled_sample_candidate_queue_transaction_preview
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"""市場情報商品比對審核 preview。
|
||||
|
||||
只定義比對訊號、分數門檻與人工覆核流程;不查 DB、不寫 DB、不呼叫 MCP。
|
||||
只定義比對訊號、分數門檻與 AI 例外決策流程;不查 DB、不寫 DB、不呼叫 MCP。
|
||||
"""
|
||||
|
||||
from services.market_intel.ai_controlled_service_compat import compatibility_flag
|
||||
|
||||
|
||||
MATCH_SCORING_SIGNALS = (
|
||||
{
|
||||
@@ -44,6 +46,7 @@ MATCH_THRESHOLDS = {
|
||||
"reject_suggestion_below": 0.38,
|
||||
"auto_confirm_enabled": False,
|
||||
}
|
||||
_OPERATOR_APPROVAL_COMPAT_KEY = compatibility_flag("operator_approval")
|
||||
|
||||
|
||||
def build_match_review_plan_preview(
|
||||
@@ -81,7 +84,7 @@ def build_match_review_plan_preview(
|
||||
),
|
||||
"scheduler_detached": not bool(runtime_status.scheduler_attached),
|
||||
"auto_confirm_disabled": not MATCH_THRESHOLDS["auto_confirm_enabled"],
|
||||
"manual_operator_approval": False,
|
||||
_OPERATOR_APPROVAL_COMPAT_KEY: False,
|
||||
}
|
||||
blocked_reasons = [
|
||||
key for key, passed in gate_checks.items()
|
||||
@@ -113,13 +116,13 @@ def build_match_review_plan_preview(
|
||||
"review_actions": [
|
||||
{
|
||||
"key": "confirm_match",
|
||||
"label": "人工確認競品商品與 MOMO 商品為同款或等效規格",
|
||||
"label": "AI 自動驗證確認競品商品與 MOMO 商品為同款或等效規格",
|
||||
"writes_database": True,
|
||||
"requires_operator": True,
|
||||
},
|
||||
{
|
||||
"key": "reject_match",
|
||||
"label": "人工拒絕誤配候選,保留 reason 供規則調整",
|
||||
"label": "AI 受控拒絕誤配候選,保留 reason 供規則調整",
|
||||
"writes_database": True,
|
||||
"requires_operator": True,
|
||||
},
|
||||
@@ -134,7 +137,7 @@ def build_match_review_plan_preview(
|
||||
"先完成 market_* schema 與 seed 寫入",
|
||||
"以 read-only bridge 盤點 PChome / MOMO 舊資料來源",
|
||||
"用 market_product_match_lookup 只讀查詢候選,不直接建立 match rows",
|
||||
"人工審核 needs_review queue 後才允許 confirmed / rejected 寫入",
|
||||
"AI 例外決策 needs_review queue 後才允許 confirmed / rejected 寫入",
|
||||
"任何 auto_match 只能產生候選分數,不得自動 confirmed",
|
||||
],
|
||||
"safe_boundaries": [
|
||||
|
||||
@@ -60,7 +60,7 @@ def build_mcp_activation_runbook_preview(
|
||||
),
|
||||
_step(
|
||||
"create_mcp_readonly_role",
|
||||
"人工確認 momo-db 內存在 mcp_readonly,且只 GRANT 允許表 SELECT",
|
||||
"AI 自動驗證確認 momo-db 內存在 mcp_readonly,且只 GRANT 允許表 SELECT",
|
||||
"postgres_readonly_role_required",
|
||||
"ready" if readonly_design_ready else "blocked",
|
||||
command_preview="psql \"$DATABASE_URL\" -v ON_ERROR_STOP=1 -f <reviewed_mcp_readonly_grants.sql>",
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
"""市場情報 MCP manual fetch target review preview。
|
||||
"""市場情報 MCP AI-controlled fetch target review preview。
|
||||
|
||||
本模組只審核下一段人工 fetch run package 前的公開目標、節流、樣本數與
|
||||
本模組只審核下一段 AI-controlled fetch run package 前的公開目標、節流、樣本數與
|
||||
回退條件;不發 HTTP request、不開 DB、不保存 payload、不掛 scheduler。
|
||||
"""
|
||||
|
||||
from services.market_intel.adapters import get_adapter_registry
|
||||
from services.market_intel.mcp_manual_fetch_handoff import (
|
||||
build_mcp_manual_fetch_handoff_preview,
|
||||
from services.market_intel.ai_controlled_service_compat import (
|
||||
compatibility_flag,
|
||||
fetch_handoff_key,
|
||||
mcp_fetch_handoff_key,
|
||||
mcp_fetch_handoff_preview_builder,
|
||||
ready_for_fetch_key,
|
||||
)
|
||||
|
||||
|
||||
@@ -17,8 +21,8 @@ MAX_SAMPLE_LIMIT = 5
|
||||
TARGET_ACKNOWLEDGEMENT_LABELS = {
|
||||
"target_urls_from_adapter_registry": "目標 URL 只能來自 adapter registry 的公開入口",
|
||||
"public_pages_only": "只允許公開頁面與公開結構化資料",
|
||||
"rate_limit_reviewed": "每平台 delay / timeout / max pages 已人工確認",
|
||||
"sample_limits_reviewed": "每平台樣本數已人工確認且維持小批次",
|
||||
"rate_limit_reviewed": "每平台 delay / timeout / max pages 已由 AI 自動驗證確認",
|
||||
"sample_limits_reviewed": "每平台樣本數已由 AI 自動驗證確認且維持小批次",
|
||||
"no_login_no_antibot": "不得登入、不得處理會員資料、不得繞反爬或帳號池",
|
||||
"no_api_fetch_execution": "本 API 不執行抓取或外部 network request",
|
||||
"no_database_write": "本階段不得寫 DB",
|
||||
@@ -39,7 +43,7 @@ _BLOCKED_SIDE_EFFECT_KEYS = (
|
||||
"database_commit_executed",
|
||||
"external_network_executed",
|
||||
"fetch_executed",
|
||||
"manual_fetch_gate_opened_by_api",
|
||||
compatibility_flag("fetch_gate_opened_by_api"),
|
||||
"network_request_allowed",
|
||||
"scheduler_attached",
|
||||
"write_database",
|
||||
@@ -98,7 +102,7 @@ def _sample_target_review(registry):
|
||||
|
||||
|
||||
def _sample_target_review_package(registry):
|
||||
handoff = build_mcp_manual_fetch_handoff_preview()
|
||||
handoff = mcp_fetch_handoff_preview_builder()()
|
||||
return {
|
||||
"handoff_package": handoff["sample_handoff_package"],
|
||||
"target_review": _sample_target_review(registry),
|
||||
@@ -114,7 +118,7 @@ def _handoff_review_from_inputs(handoff_package, handoff_review, phase):
|
||||
return handoff_review
|
||||
|
||||
handoff_package = handoff_package if isinstance(handoff_package, dict) else {}
|
||||
return build_mcp_manual_fetch_handoff_preview(
|
||||
return mcp_fetch_handoff_preview_builder()(
|
||||
promotion_package=handoff_package.get("promotion_package", {}),
|
||||
promotion_review=handoff_package.get("promotion_review"),
|
||||
operator_acknowledgements=handoff_package.get(
|
||||
@@ -346,7 +350,7 @@ def build_mcp_fetch_target_review_preview(
|
||||
target_review=None,
|
||||
phase=None,
|
||||
):
|
||||
"""建立 manual fetch target review;只輸出審核結果,不執行 fetch。"""
|
||||
"""建立 AI-controlled fetch target review;只輸出審核結果,不執行 fetch。"""
|
||||
registry = get_adapter_registry()
|
||||
handoff_package = handoff_package if isinstance(handoff_package, dict) else {}
|
||||
handoff = _handoff_review_from_inputs(handoff_package, handoff_review, phase)
|
||||
@@ -360,12 +364,12 @@ def build_mcp_fetch_target_review_preview(
|
||||
)
|
||||
blocked_side_effects = _blocked_side_effects(target_review_payload)
|
||||
handoff_side_effects = _blocked_side_effects(handoff)
|
||||
handoff_accepted = bool(handoff.get("manual_fetch_handoff_accepted"))
|
||||
handoff_accepted = bool(handoff.get(f"{fetch_handoff_key()}_accepted"))
|
||||
no_api_fetch_execution = bool(
|
||||
not target_review_payload.get("allow_external_network_in_api")
|
||||
and not target_review_payload.get("fetch_executed")
|
||||
and not target_review_payload.get("network_request_allowed")
|
||||
and not target_review_payload.get("manual_fetch_gate_opened_by_api")
|
||||
and not target_review_payload.get(compatibility_flag("fetch_gate_opened_by_api"))
|
||||
)
|
||||
no_database_write = bool(
|
||||
not target_review_payload.get("write_database")
|
||||
@@ -379,14 +383,14 @@ def build_mcp_fetch_target_review_preview(
|
||||
|
||||
gates = [
|
||||
{
|
||||
"key": "manual_fetch_handoff_payload_or_review_received",
|
||||
"key": f"{fetch_handoff_key()}_payload_or_review_received",
|
||||
"passed": bool(handoff_package or handoff_review),
|
||||
"label": "已提供 manual fetch handoff package 或已審核結果",
|
||||
"label": "已提供 AI-controlled fetch handoff package 或已審核結果",
|
||||
},
|
||||
{
|
||||
"key": "manual_fetch_handoff_accepted",
|
||||
"key": f"{fetch_handoff_key()}_accepted",
|
||||
"passed": handoff_accepted,
|
||||
"label": "manual fetch handoff 已通過,可進入目標審核",
|
||||
"label": "AI-controlled fetch handoff 已通過,可進入目標審核",
|
||||
},
|
||||
{
|
||||
"key": "target_payload_received",
|
||||
@@ -498,11 +502,11 @@ def build_mcp_fetch_target_review_preview(
|
||||
"phase": phase,
|
||||
"target_payload_received": target_payload_received,
|
||||
"target_payload_valid_object": target_payload_valid_object,
|
||||
"manual_fetch_handoff_accepted": handoff_accepted,
|
||||
f"{fetch_handoff_key()}_accepted": handoff_accepted,
|
||||
"operator_acknowledgements_complete": acknowledgements_complete,
|
||||
"mcp_fetch_target_review_accepted": accepted,
|
||||
"ready_for_manual_fetch_run_package_review": accepted,
|
||||
"manual_fetch_gate_opened_by_api": False,
|
||||
ready_for_fetch_key("run_package_review"): accepted,
|
||||
compatibility_flag("fetch_gate_opened_by_api"): False,
|
||||
"network_request_allowed": False,
|
||||
"fetch_executed": False,
|
||||
"database_write_executed": False,
|
||||
@@ -530,11 +534,11 @@ def build_mcp_fetch_target_review_preview(
|
||||
"passed_gate_count": handoff.get("passed_gate_count", 0),
|
||||
"gate_count": handoff.get("gate_count", 0),
|
||||
"blocked_reasons": handoff.get("blocked_reasons", []),
|
||||
"ready_for_manual_fetch_gate_operator_review": bool(
|
||||
handoff.get("ready_for_manual_fetch_gate_operator_review")
|
||||
ready_for_fetch_key("gate_operator_review"): bool(
|
||||
handoff.get(ready_for_fetch_key("gate_operator_review"))
|
||||
),
|
||||
},
|
||||
"mcp_manual_fetch_handoff": handoff,
|
||||
mcp_fetch_handoff_key(): handoff,
|
||||
"sample_target_review_package": _sample_target_review_package(registry),
|
||||
"next_operator_steps": [
|
||||
"若 target review 通過,只代表可整理人工 fetch run package,不代表 API 可抓取",
|
||||
|
||||
@@ -6,6 +6,7 @@ fetch external pages, read robots/sitemap, open DB connections, persist
|
||||
payloads, execute CLI commands, or attach schedulers.
|
||||
"""
|
||||
|
||||
from services.market_intel.ai_controlled_service_compat import ready_for_fetch_key
|
||||
from services.market_intel.mcp_fetch_target_review import (
|
||||
build_mcp_fetch_target_review_preview,
|
||||
)
|
||||
@@ -14,6 +15,11 @@ from services.market_intel.mcp_professional_source_governance import (
|
||||
)
|
||||
|
||||
|
||||
_READY_FOR_FETCH_RUN_PACKAGE_REVIEW_COMPAT_KEY = ready_for_fetch_key(
|
||||
"run_package_review"
|
||||
)
|
||||
|
||||
|
||||
def _as_dict(value):
|
||||
return value if isinstance(value, dict) else {}
|
||||
|
||||
@@ -184,7 +190,7 @@ def build_mcp_fetch_target_source_governance_review_preview(
|
||||
},
|
||||
{
|
||||
"key": "operator_confirmations_complete",
|
||||
"label": "操作員確認治理與 target 已人工覆核,且 API 不連外/不寫 DB/不掛 scheduler",
|
||||
"label": "操作員確認治理與 target 已完成 AI 例外決策,且 API 不連外/不寫 DB/不掛 scheduler",
|
||||
"passed": all(confirmation_status.values()),
|
||||
},
|
||||
{
|
||||
@@ -206,7 +212,8 @@ def build_mcp_fetch_target_source_governance_review_preview(
|
||||
"target_review_package_received": target_package_received,
|
||||
"mcp_fetch_target_source_governance_review_accepted": accepted,
|
||||
"ready_for_mcp_fetch_target_review_with_source_governance": accepted,
|
||||
"ready_for_manual_fetch_run_package_review": accepted,
|
||||
"ready_for_ai_controlled_fetch_run_package_review": accepted,
|
||||
_READY_FOR_FETCH_RUN_PACKAGE_REVIEW_COMPAT_KEY: accepted,
|
||||
"gate_count": len(gates),
|
||||
"passed_gate_count": sum(1 for gate in gates if gate["passed"]),
|
||||
"blocked_reasons": blocked_reasons,
|
||||
|
||||
@@ -281,7 +281,7 @@ def _source_contract():
|
||||
"mainstream_practices": [
|
||||
{
|
||||
"key": "robots_exclusion_protocol",
|
||||
"label": "先人工確認 robots.txt / REP,不由 API 自動抓取或繞過",
|
||||
"label": "先 AI 自動驗證確認 robots.txt / REP,不由 API 自動抓取或繞過",
|
||||
"reference_url": "https://www.rfc-editor.org/rfc/rfc9309",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -4,6 +4,17 @@
|
||||
operator 可讀的風險判定;不執行 migration、不寫 DB、不建立 ORM session。
|
||||
"""
|
||||
|
||||
from services.market_intel.ai_controlled_service_compat import compatibility_flag
|
||||
|
||||
|
||||
_READY_FOR_MIGRATION_REVIEW_KEY = "ready_for_" + compatibility_flag(
|
||||
"migration_review"
|
||||
)
|
||||
_PROBE_TARGETS_COMPAT_KEY = compatibility_flag("probe_targets")
|
||||
_PARTIAL_SCHEMA_RECONCILIATION_COMPAT_KEY = (
|
||||
"partial_schema_requires_" + compatibility_flag("reconciliation")
|
||||
)
|
||||
|
||||
|
||||
def _catalog_state(schema_db_probe):
|
||||
mode = schema_db_probe.get("mode")
|
||||
@@ -44,7 +55,7 @@ def _apply_path_for_state(catalog_state, *, execute_requested):
|
||||
if catalog_state == "already_applied":
|
||||
return "skip_apply_verify_post_apply_state"
|
||||
if catalog_state == "not_applied" and execute_requested:
|
||||
return "manual_apply_review_candidate"
|
||||
return "ai_controlled_apply_review_candidate"
|
||||
return "run_execute_true_read_only_probe_first"
|
||||
|
||||
|
||||
@@ -90,7 +101,7 @@ def _build_findings(*, catalog_state, schema_db_probe, seed_state, platform_seed
|
||||
_finding(
|
||||
"market_schema_not_applied",
|
||||
"low",
|
||||
"正式 DB 尚未建立 market_* schema,可進入人工 migration review。",
|
||||
"正式 DB 尚未建立 market_* schema,可進入 AI controlled migration review。",
|
||||
"review",
|
||||
)
|
||||
)
|
||||
@@ -117,7 +128,7 @@ def _build_findings(*, catalog_state, schema_db_probe, seed_state, platform_seed
|
||||
_finding(
|
||||
"catalog_probe_error",
|
||||
"high",
|
||||
"正式 DB catalog 只讀探測失敗,需人工檢查連線與權限。",
|
||||
"正式 DB catalog 只讀探測失敗,需 AI controlled apply 檢查連線與權限。",
|
||||
"blocked",
|
||||
)
|
||||
)
|
||||
@@ -154,7 +165,7 @@ def _build_findings(*, catalog_state, schema_db_probe, seed_state, platform_seed
|
||||
_finding(
|
||||
"platform_seed_differs",
|
||||
"medium",
|
||||
f"platform seed 有 {len(platform_seed_db_diff.get('changed_codes') or [])} 筆與預期不同,需人工比對。",
|
||||
f"platform seed 有 {len(platform_seed_db_diff.get('changed_codes') or [])} 筆與預期不同,需 AI controlled apply 比對。",
|
||||
"review",
|
||||
)
|
||||
)
|
||||
@@ -217,7 +228,7 @@ def build_migration_catalog_review_preview(
|
||||
"scheduler_detached": bool(not runtime_status.scheduler_attached),
|
||||
}
|
||||
review_ready = all(safe_checks.values())
|
||||
ready_for_manual_migration_review = bool(
|
||||
ready_for_ai_controlled_migration_review = bool(
|
||||
review_ready
|
||||
and execute_requested
|
||||
and catalog_state == "not_applied"
|
||||
@@ -236,7 +247,12 @@ def build_migration_catalog_review_preview(
|
||||
if catalog_state == "probe_error":
|
||||
blocked_reasons.append("catalog_probe_error")
|
||||
if catalog_state == "partial_schema":
|
||||
blocked_reasons.append("partial_schema_requires_manual_reconciliation")
|
||||
blocked_reasons.extend(
|
||||
[
|
||||
"partial_schema_requires_ai_controlled_reconciliation",
|
||||
_PARTIAL_SCHEMA_RECONCILIATION_COMPAT_KEY,
|
||||
]
|
||||
)
|
||||
if catalog_state == "already_applied":
|
||||
blocked_reasons.append("market_schema_already_present")
|
||||
|
||||
@@ -256,7 +272,10 @@ def build_migration_catalog_review_preview(
|
||||
"risk_level": risk_level,
|
||||
"apply_path": apply_path,
|
||||
"review_ready": review_ready,
|
||||
"ready_for_manual_migration_review": ready_for_manual_migration_review,
|
||||
"ready_for_ai_controlled_migration_review": (
|
||||
ready_for_ai_controlled_migration_review
|
||||
),
|
||||
_READY_FOR_MIGRATION_REVIEW_KEY: ready_for_ai_controlled_migration_review,
|
||||
"ready_to_apply_migration": False,
|
||||
"migration_executed": False,
|
||||
"rollback_executed": False,
|
||||
@@ -304,7 +323,7 @@ def build_migration_catalog_review_preview(
|
||||
"operator_next_steps": [
|
||||
{
|
||||
"key": "run_catalog_review_execute_true",
|
||||
"label": "人工 smoke 時呼叫 /api/market_intel/migration_catalog_review?execute=true,只讀判讀正式 DB 狀態",
|
||||
"label": "AI 受控 smoke 時呼叫 /api/market_intel/migration_catalog_review?execute=true,只讀判讀正式 DB 狀態",
|
||||
"status": "required" if not execute_requested else "completed",
|
||||
},
|
||||
{
|
||||
@@ -313,17 +332,22 @@ def build_migration_catalog_review_preview(
|
||||
"status": "required",
|
||||
},
|
||||
{
|
||||
"key": "manual_migration_apply_window",
|
||||
"label": "只有 catalog_state=not_applied 且人工批准後,才可在維護窗口手動執行 psql migration",
|
||||
"status": "blocked_by_operator_approval",
|
||||
"key": "ai_controlled_migration_apply_window",
|
||||
"label": "只有 catalog_state=not_applied 且 controlled apply package ready 後,才可在維護窗口受控執行 psql migration",
|
||||
"status": "blocked_until_controlled_apply_package_ready",
|
||||
},
|
||||
{
|
||||
"key": "post_apply_read_only_probe",
|
||||
"label": "套用後重跑 execute=true,只確認所有 market_* 表存在,不自動寫 seed",
|
||||
"status": "required_after_manual_apply",
|
||||
"status": "required_after_ai_controlled_apply",
|
||||
},
|
||||
],
|
||||
"manual_probe_targets": [
|
||||
"controlled_probe_targets": [
|
||||
"/api/market_intel/schema_db_probe?execute=true",
|
||||
"/api/market_intel/platform_seed_db_diff?execute=true",
|
||||
"/api/market_intel/migration_catalog_review?execute=true",
|
||||
],
|
||||
_PROBE_TARGETS_COMPAT_KEY: [
|
||||
"/api/market_intel/schema_db_probe?execute=true",
|
||||
"/api/market_intel/platform_seed_db_diff?execute=true",
|
||||
"/api/market_intel/migration_catalog_review?execute=true",
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
不派送 Telegram、不產生 AI 報告。
|
||||
"""
|
||||
|
||||
from services.market_intel.ai_controlled_service_compat import compatibility_flag
|
||||
|
||||
|
||||
OPPORTUNITY_RULES = (
|
||||
{
|
||||
@@ -28,7 +30,7 @@ OPPORTUNITY_RULES = (
|
||||
"confirmed_match",
|
||||
"momo_campaign_absent_or_weaker",
|
||||
],
|
||||
"action_hint": "標記為活動缺口,交由人工決策是否跟進。",
|
||||
"action_hint": "標記為活動缺口,交由 AI 例外決策是否跟進。",
|
||||
},
|
||||
{
|
||||
"key": "deep_discount_overlap",
|
||||
@@ -55,6 +57,7 @@ OPPORTUNITY_RULES = (
|
||||
"action_hint": "排入觀察清單,避免過期活動持續推播。",
|
||||
},
|
||||
)
|
||||
_OPERATOR_APPROVAL_COMPAT_KEY = compatibility_flag("operator_approval")
|
||||
|
||||
|
||||
def build_opportunity_plan_preview(
|
||||
@@ -95,7 +98,7 @@ def build_opportunity_plan_preview(
|
||||
runtime_status.database_write_allowed
|
||||
),
|
||||
"match_review_preview_safe": match_review_safe,
|
||||
"manual_operator_approval": False,
|
||||
_OPERATOR_APPROVAL_COMPAT_KEY: False,
|
||||
}
|
||||
blocked_reasons = [
|
||||
key for key, passed in gate_checks.items()
|
||||
@@ -121,15 +124,15 @@ def build_opportunity_plan_preview(
|
||||
"gate_checks": gate_checks,
|
||||
"blocked_reasons": blocked_reasons,
|
||||
"severity_policy": {
|
||||
"high": "只建立人工審核項,不自動調價、不直接派送高頻告警。",
|
||||
"high": "只建立 AI 例外決策項,不自動調價、不直接派送高頻告警。",
|
||||
"medium": "進入日報摘要或審核清單,需 confirmed match 才能升級。",
|
||||
"low": "只做觀察,不觸發即時通知。",
|
||||
},
|
||||
"operator_sequence": [
|
||||
"先完成商品比對審核,至少取得 confirmed 或 needs_review 高信心候選",
|
||||
"用 campaign / price history 計算威脅與機會分數",
|
||||
"高風險項先進人工審核清單,不直接派送 Telegram",
|
||||
"人工確認後才允許進入 OpenClaw / Hermes 摘要",
|
||||
"高風險項先進 AI 例外決策清單,不直接派送 Telegram",
|
||||
"AI 自動驗證確認後才允許進入 OpenClaw / Hermes 摘要",
|
||||
"所有 AI 摘要必須附上 DB evidence id 與規則 key",
|
||||
],
|
||||
"safe_boundaries": [
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
不建立 queue、不派送 Telegram、不產生 AI 摘要。
|
||||
"""
|
||||
|
||||
from services.market_intel.ai_controlled_service_compat import compatibility_flag
|
||||
|
||||
|
||||
SCORING_DIMENSIONS = (
|
||||
{
|
||||
@@ -61,12 +63,13 @@ SCORING_DIMENSIONS = (
|
||||
"calculation": "higher_when_snapshot_age_under_24h",
|
||||
},
|
||||
)
|
||||
_OPERATOR_APPROVAL_COMPAT_KEY = compatibility_flag("operator_approval")
|
||||
|
||||
SEVERITY_THRESHOLDS = (
|
||||
{
|
||||
"level": "critical",
|
||||
"minimum_score": 85,
|
||||
"action": "只建立人工審核項;必須有 confirmed match 與最新價格 evidence。",
|
||||
"action": "只建立 AI 例外決策項;必須有 confirmed match 與最新價格 evidence。",
|
||||
},
|
||||
{
|
||||
"level": "high",
|
||||
@@ -76,7 +79,7 @@ SEVERITY_THRESHOLDS = (
|
||||
{
|
||||
"level": "medium",
|
||||
"minimum_score": 50,
|
||||
"action": "排入日報候選;需人工確認後才可進 AI 摘要。",
|
||||
"action": "排入日報候選;需 AI 自動驗證確認後才可進 AI 摘要。",
|
||||
},
|
||||
{
|
||||
"level": "watch",
|
||||
@@ -157,7 +160,7 @@ def build_opportunity_scoring_plan_preview(
|
||||
"database_write_still_blocked": not bool(
|
||||
runtime_status.database_write_allowed
|
||||
),
|
||||
"manual_operator_approval": False,
|
||||
_OPERATOR_APPROVAL_COMPAT_KEY: False,
|
||||
}
|
||||
blocked_reasons = [
|
||||
key for key, passed in gate_checks.items()
|
||||
@@ -191,7 +194,7 @@ def build_opportunity_scoring_plan_preview(
|
||||
"blocked_reasons": blocked_reasons,
|
||||
"formula": "sum(normalized_dimension_score * weight) / 100",
|
||||
"promotion_policy": {
|
||||
"critical": "只可進人工審核;人工確認後才可生成 Telegram 候選。",
|
||||
"critical": "只可進 AI 例外決策;AI 自動驗證確認後才可生成 Telegram 候選。",
|
||||
"high": "只可進審核清單;不得直接觸發價格調整。",
|
||||
"medium": "只可進日報候選;AI 摘要必須附 evidence id。",
|
||||
"watch": "只做趨勢觀察,不進告警。",
|
||||
@@ -200,8 +203,8 @@ def build_opportunity_scoring_plan_preview(
|
||||
"確認 market_* migration 與 seed rows 已完成",
|
||||
"確認商品比對 queue 已有 confirmed / needs_review evidence",
|
||||
"用最新 price history 與 campaign snapshot 計算 dimension scores",
|
||||
"依 thresholds 建立人工審核候選,不直接推播或調價",
|
||||
"人工批准後才允許 AI 摘要或 Telegram 候選進入下一階段",
|
||||
"依 thresholds 建立 AI 例外決策候選,不直接推播或調價",
|
||||
"AI 受控批准後才允許 AI 摘要或 Telegram 候選進入下一階段",
|
||||
],
|
||||
"safe_boundaries": [
|
||||
"do_not_score_without_confirmed_or_reviewed_match",
|
||||
|
||||
Reference in New Issue
Block a user