From 0a7bdd819b6a1a5e2bb7c0c5457bee1206905ba7 Mon Sep 17 00:00:00 2001 From: ogt Date: Wed, 1 Jul 2026 13:53:46 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B8=85=E9=99=A4=E5=B8=82=E5=A0=B4=E6=83=85?= =?UTF-8?q?=E5=A0=B1=E5=8F=97=E6=8E=A7=E5=A5=97=E7=94=A8=E5=89=A9=E9=A4=98?= =?UTF-8?q?=E4=BA=BA=E5=B7=A5=E8=AA=9E=E6=84=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../market_intel/manual_sample_acceptance.py | 42 ++++++++++----- .../manual_sample_candidate_queue.py | 53 ++++++++++++------- services/market_intel/match_review_plan.py | 13 +++-- .../market_intel/mcp_activation_runbook.py | 2 +- .../market_intel/mcp_fetch_target_review.py | 48 +++++++++-------- ...p_fetch_target_source_governance_review.py | 11 +++- .../mcp_professional_source_governance.py | 2 +- .../market_intel/migration_catalog_review.py | 50 ++++++++++++----- services/market_intel/opportunity_plan.py | 13 +++-- services/market_intel/opportunity_scoring.py | 15 +++--- 10 files changed, 161 insertions(+), 88 deletions(-) diff --git a/services/market_intel/manual_sample_acceptance.py b/services/market_intel/manual_sample_acceptance.py index bf319c3..bb897a0 100644 --- a/services/market_intel/manual_sample_acceptance.py +++ b/services/market_intel/manual_sample_acceptance.py @@ -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 +) diff --git a/services/market_intel/manual_sample_candidate_queue.py b/services/market_intel/manual_sample_candidate_queue.py index dbcd871..ea3a00c 100644 --- a/services/market_intel/manual_sample_candidate_queue.py +++ b/services/market_intel/manual_sample_candidate_queue.py @@ -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 diff --git a/services/market_intel/match_review_plan.py b/services/market_intel/match_review_plan.py index c99e044..f1bd863 100644 --- a/services/market_intel/match_review_plan.py +++ b/services/market_intel/match_review_plan.py @@ -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": [ diff --git a/services/market_intel/mcp_activation_runbook.py b/services/market_intel/mcp_activation_runbook.py index 22700b1..6790ab6 100644 --- a/services/market_intel/mcp_activation_runbook.py +++ b/services/market_intel/mcp_activation_runbook.py @@ -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 ", diff --git a/services/market_intel/mcp_fetch_target_review.py b/services/market_intel/mcp_fetch_target_review.py index e31c4fd..241de0b 100644 --- a/services/market_intel/mcp_fetch_target_review.py +++ b/services/market_intel/mcp_fetch_target_review.py @@ -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 可抓取", diff --git a/services/market_intel/mcp_fetch_target_source_governance_review.py b/services/market_intel/mcp_fetch_target_source_governance_review.py index e2098fa..1733e57 100644 --- a/services/market_intel/mcp_fetch_target_source_governance_review.py +++ b/services/market_intel/mcp_fetch_target_source_governance_review.py @@ -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, diff --git a/services/market_intel/mcp_professional_source_governance.py b/services/market_intel/mcp_professional_source_governance.py index 8bfd396..ee9d2e6 100644 --- a/services/market_intel/mcp_professional_source_governance.py +++ b/services/market_intel/mcp_professional_source_governance.py @@ -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", }, { diff --git a/services/market_intel/migration_catalog_review.py b/services/market_intel/migration_catalog_review.py index b852ad5..50d5584 100644 --- a/services/market_intel/migration_catalog_review.py +++ b/services/market_intel/migration_catalog_review.py @@ -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", diff --git a/services/market_intel/opportunity_plan.py b/services/market_intel/opportunity_plan.py index 13c38f9..522e77a 100644 --- a/services/market_intel/opportunity_plan.py +++ b/services/market_intel/opportunity_plan.py @@ -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": [ diff --git a/services/market_intel/opportunity_scoring.py b/services/market_intel/opportunity_scoring.py index 1299880..358c8e6 100644 --- a/services/market_intel/opportunity_scoring.py +++ b/services/market_intel/opportunity_scoring.py @@ -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",