新增市場情報 AI summary output receipt gate
All checks were successful
CD Pipeline / deploy (push) Successful in 1m3s
All checks were successful
CD Pipeline / deploy (push) Successful in 1m3s
This commit is contained in:
@@ -163,7 +163,7 @@
|
||||
- Phase 79 candidate queue review archive summary:新增 `services/market_intel/candidate_queue_review_archive_summary.py`、POST `/api/market_intel/manual_sample_review/candidate_queue_review_archive_summary` 與 UI summary 按鈕,在 review completion archive 後整理可供摘要/報表審核的結構化輸入;API/UI 不呼叫 LLM、不派送 Telegram、不寫檔、不讀 token、不執行 CLI、不更新 review_state、不寫 DB、不 commit、不掛 scheduler;版本同步至 V10.276。
|
||||
- Phase 80 candidate queue review AI summary preflight:新增 `services/market_intel/candidate_queue_review_ai_summary_preflight.py`、POST `/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_preflight` 與 UI preflight 按鈕,在 archive summary 後檢查 Ollama-first 三主機級聯與 Gemini 備援邊界;API/UI 不呼叫 LLM、不派送 Telegram、不寫檔、不讀 token、不執行 CLI、不更新 review_state、不寫 DB、不 commit、不掛 scheduler;版本同步至 V10.278。
|
||||
- Phase 81 candidate queue review AI summary run package:新增 `services/market_intel/candidate_queue_review_ai_summary_run_package.py`、POST `/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_run_package` 與 UI package 按鈕,在 AI summary preflight 後整理手動 Ollama 摘要任務包、prompt contract 與輸出 schema;API/UI 不呼叫 LLM、不派送 Telegram、不寫 run package、不讀 token、不執行 CLI、不更新 review_state、不寫 DB、不 commit、不掛 scheduler;版本同步至 V10.280。
|
||||
- Phase 82 candidate queue review AI summary output receipt:新增 `services/market_intel/candidate_queue_review_ai_summary_output_receipt.py`、POST `/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_output_receipt` 與 UI receipt 按鈕,在 run package 後驗收人工 Ollama 摘要輸出的 schema、evidence_refs 與 model_route;API/UI 不呼叫 LLM、不派送 Telegram、不寫 receipt、不讀 token、不執行 CLI、不更新 review_state、不寫 DB、不 commit、不掛 scheduler;版本同步至 V10.282。
|
||||
- Phase 82 candidate queue review AI summary output receipt:新增 `services/market_intel/candidate_queue_review_ai_summary_output_receipt.py`、POST `/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_output_receipt` 與 UI receipt 按鈕,在 run package 後驗收人工 Ollama 摘要輸出的 schema、evidence_refs 與 model_route;API/UI 不呼叫 LLM、不派送 Telegram、不寫 receipt、不讀 token、不執行 CLI、不更新 review_state、不寫 DB、不 commit、不掛 scheduler;版本同步至 V10.285。
|
||||
- V10.248 補市場情報 390px preview panel QA:sample review 工具列改為 textarea + 可換行 action rail,移除舊的硬編 8 欄 grid;`check_responsive_overflow` 新增 `--screenshot-all`,本機 390x844 `/market_intel` 真頁面 QA 通過且 overflow=0。
|
||||
- V10.250 補 Code Review Gemini 備援遙測護欄:Ollama 主路徑失敗時 `fallback_to` 明確指向 `code_review_openclaw_gemini`,測試鎖住「Gemini 不得記成 `code_review_openclaw` 主 caller」;AI Calls 觀測台會把 legacy `code_review_openclaw + gemini` 顯示成 Gemini 備援,避免誤判 Gemini-first。
|
||||
- Schema smoke:`tests/test_market_intel_skeleton.py` 檢查 `Base.metadata` 內含 ADR-035 八張 `market_*` tables。
|
||||
|
||||
@@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
|
||||
# ==========================================
|
||||
# 系統版本與路徑
|
||||
# ==========================================
|
||||
SYSTEM_VERSION = "V10.284"
|
||||
SYSTEM_VERSION = "V10.285"
|
||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||
public_url = PUBLIC_URL # 用於模板顯示
|
||||
|
||||
|
||||
@@ -207,6 +207,7 @@ EwoooC 目前已有 MOMO EDM / 節慶活動資料、`promo_products`、PChome
|
||||
- 2026-05-19 追加 candidate queue review archive summary:`services.market_intel.candidate_queue_review_archive_summary` 與 `/api/market_intel/manual_sample_review/candidate_queue_review_archive_summary` 在 review completion archive 後整理可供摘要/報表審核的結構化輸入。此 summary gate 不產生 AI 摘要;API/UI 不呼叫 LLM、不派送 Telegram、不寫檔、不讀 approval token、不執行 CLI、不更新 `review_state`、不寫 DB、不 commit、不掛 scheduler。
|
||||
- 2026-05-19 追加 candidate queue review AI summary preflight:`services.market_intel.candidate_queue_review_ai_summary_preflight` 與 `/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_preflight` 在 archive summary 後檢查未來 AI 摘要前置條件、Ollama-first 三主機級聯與 Gemini 備援邊界。此 preflight 不產生 AI 摘要;API/UI 不呼叫 LLM、不派送 Telegram、不寫檔、不讀 approval token、不執行 CLI、不更新 `review_state`、不寫 DB、不 commit、不掛 scheduler;188 不可作為 Ollama 節點。
|
||||
- 2026-05-19 追加 candidate queue review AI summary run package:`services.market_intel.candidate_queue_review_ai_summary_run_package` 與 `/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_run_package` 在 AI summary preflight 後整理手動 Ollama 摘要任務包、prompt contract、輸出 schema 與 artifact path contract。此 package gate 不產生 AI 摘要;API/UI 不呼叫 LLM、不派送 Telegram、不寫檔、不讀 approval token、不執行 CLI、不更新 `review_state`、不寫 DB、不 commit、不掛 scheduler;Gemini 僅能作為 Ollama cascade 全失敗後的備援。
|
||||
- 2026-05-19 追加 candidate queue review AI summary output receipt:`services.market_intel.candidate_queue_review_ai_summary_output_receipt` 與 `/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_output_receipt` 在 run package 後驗收人工 Ollama 摘要輸出的 schema、`evidence_refs` 與 `model_route`。此 receipt gate 不產生 AI 摘要;API/UI 不呼叫 LLM、不派送 Telegram、不寫檔、不讀 approval token、不執行 CLI、不更新 `review_state`、不寫 DB、不 commit、不掛 scheduler;摘要持久化與 Telegram 派送必須另開後續 gate。
|
||||
|
||||
### Phase 4:Coupang / Shopee Adapter
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
- 2026-05-19 追記:同步治理測試盤點,校正 `routes/admin_observability_routes.py` 行數;此處只更新 inventory,不變更觀測台功能。
|
||||
- 2026-05-19 追記:V10.229 之後 `services/ppt_vision_service.py` 進入 800 行治理清單;本次只補 inventory 讓守門測試反映現況,不變更 PPT 視覺 QA 功能。
|
||||
- 2026-05-19 追記:同步背景 V10.276 ElephantAlpha 更新後的 `services/elephant_alpha_autonomous_engine.py` 行數;此處只更新 inventory,不變更 AI engine 行為。
|
||||
- 2026-05-19 追記:同步背景 V10.281/V10.282 dashboard 與 Code Review pipeline 更新後的行數;此處只更新 inventory,不變更 dashboard 或 code review 行為。
|
||||
|
||||
## 達到或超過 800 行檔案清單
|
||||
|
||||
@@ -27,7 +28,7 @@
|
||||
| 3681 | `routes/admin_observability_routes.py` | P0 觀測台巨型 Blueprint | `services/observability_query_service.py` / `services/observability_action_service.py` / route glue |
|
||||
| 1796 | `routes/ai_routes.py` | P1 AI Blueprint | route glue / AI orchestration service / prompt builders |
|
||||
| 1721 | `services/nemoton_dispatcher_service.py` | P1 NemoTron service | NIM client / tool-call parser / action dispatcher |
|
||||
| 1507 | `routes/dashboard_routes.py` | P1 Dashboard Blueprint | competitor decision overview / dashboard query service;首頁資料整併需抽 service |
|
||||
| 1777 | `routes/dashboard_routes.py` | P1 Dashboard Blueprint | competitor decision overview / dashboard query service;首頁資料整併需抽 service |
|
||||
| 1485 | `routes/vendor_routes.py` | P1 Vendor Blueprint | route glue / stockout mutation/email;V2 page query、stockout list/batches API query、vendor list/detail query 已抽到 `services/vendor_stockout_query_service.py` |
|
||||
| 1390 | `services/telegram_bot_service.py` | P1 Telegram service | command handlers / message formatters / bot client |
|
||||
| 1237 | `app.py` | P1 bootstrap | 保持只做 app setup;繼續往 app_factory / extension setup 抽;Phase 42 只做 metadata table name 對齊 |
|
||||
@@ -41,7 +42,7 @@
|
||||
| 867 | `services/token_report_service.py` | P2 token report service | query / aggregation / chart payload / notification formatting |
|
||||
| 865 | `routes/daily_sales_routes.py` | P2 Daily Sales Blueprint | route glue / export helpers / daily query and formatting service |
|
||||
| 844 | `services/ollama_service.py` | P2 Ollama client | host health / request client / fallback policy / response parsing |
|
||||
| 837 | `services/code_review_pipeline_service.py` | P2 Code review pipeline service | scan orchestration / finding normalization / persistence adapter |
|
||||
| 1042 | `services/code_review_pipeline_service.py` | P2 Code review pipeline service | scan orchestration / finding normalization / persistence adapter |
|
||||
| 832 | `routes/export_routes.py` | P2 Export flow | export command/router glue / file path / download orchestration |
|
||||
| 816 | `services/ppt_vision_service.py` | P2 PPT vision QA service | runtime state / queue status / model probe / audit execution 分離 |
|
||||
| 904 | `services/competitor_price_feeder.py` | P2 competitor price feeder | crawler scheduling / price normalization / cache strategy |
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
| `edm_routes.py` | EDM 與節慶儀表板 | `/edm`, `/festival` |
|
||||
| `monthly_routes.py` | 月結分析 | `/monthly_summary_analysis`, `/api/monthly_summary_data` |
|
||||
| `daily_sales_routes.py` | 當日業績 | `/daily_sales`, `/daily_sales/export*` |
|
||||
| `market_intel_routes.py` | 市場情報 Phase 81 candidate queue review AI summary run package 主路由 | `/market_intel`, `/market_intel/*`, `/api/market_intel/status`, `/api/market_intel/schema`, `/api/market_intel/schema_smoke`, `/api/market_intel/schema_db_probe`, `/api/market_intel/platform_seed_db_diff`, `/api/market_intel/legacy_source_bridge`, `/api/market_intel/mcp_readiness`, `/api/market_intel/mcp_tool_contract`, `/api/market_intel/mcp_deploy_preflight`, `/api/market_intel/mcp_activation_runbook`, `/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/manual_sample_review`, `/api/market_intel/manual_sample_review/evaluate`, `/api/market_intel/manual_sample_review/candidate_handoff`, `/api/market_intel/manual_sample_review/candidate_queue_draft`, `/api/market_intel/manual_sample_review/candidate_queue_approval`, `/api/market_intel/manual_sample_review/candidate_queue_transaction`, `/api/market_intel/manual_sample_review/candidate_queue_writer_status`, `/api/market_intel/manual_sample_review/candidate_queue_writer_preflight`, `/api/market_intel/manual_sample_review/candidate_queue_writer_postwrite_smoke`, `/api/market_intel/manual_sample_review/candidate_queue_writer_operator_drill`, `/api/market_intel/manual_sample_review/candidate_queue_writer_run_package`, `/api/market_intel/manual_sample_review/candidate_queue_writer_run_readiness`, `/api/market_intel/manual_sample_review/candidate_queue_writer_run_receipt`, `/api/market_intel/manual_sample_review/candidate_queue_writer_run_closeout`, `/api/market_intel/manual_sample_review/candidate_queue_review_handoff`, `/api/market_intel/match_review_plan`, `/api/market_intel/opportunity_plan`, `/api/market_intel/opportunity_scoring_plan`, `/api/market_intel/opportunity_evidence_plan`, `/api/market_intel/opportunity_alert_plan`, `/api/market_intel/adapters`, `/api/market_intel/dry_run_plan`, `/api/market_intel/discovery_plan`, `/api/market_intel/manual_discovery`, `/api/market_intel/candidate_preview`, `/api/market_intel/platform_seed_plan`, `/api/market_intel/platform_seed_write_guard`, `/api/market_intel/platform_seed_writer_plan`, `/api/market_intel/migration_blueprint`, `/api/market_intel/migration_apply_drill`, `/api/market_intel/migration_catalog_review`, `/api/market_intel/migration_live_smoke`, `/api/market_intel/live_db_inventory`, `/api/market_intel/seed_writer_cli_status`, `/api/market_intel/write_approval_runbook`, `/api/market_intel/deployment_readiness` |
|
||||
| `market_intel_routes.py` | 市場情報 Phase 82 candidate queue review AI summary output receipt 主路由 | `/market_intel`, `/market_intel/*`, `/api/market_intel/status`, `/api/market_intel/schema`, `/api/market_intel/schema_smoke`, `/api/market_intel/schema_db_probe`, `/api/market_intel/platform_seed_db_diff`, `/api/market_intel/legacy_source_bridge`, `/api/market_intel/mcp_readiness`, `/api/market_intel/mcp_tool_contract`, `/api/market_intel/mcp_deploy_preflight`, `/api/market_intel/mcp_activation_runbook`, `/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/manual_sample_review`, `/api/market_intel/manual_sample_review/evaluate`, `/api/market_intel/manual_sample_review/candidate_handoff`, `/api/market_intel/manual_sample_review/candidate_queue_draft`, `/api/market_intel/manual_sample_review/candidate_queue_approval`, `/api/market_intel/manual_sample_review/candidate_queue_transaction`, `/api/market_intel/manual_sample_review/candidate_queue_writer_status`, `/api/market_intel/manual_sample_review/candidate_queue_writer_preflight`, `/api/market_intel/manual_sample_review/candidate_queue_writer_postwrite_smoke`, `/api/market_intel/manual_sample_review/candidate_queue_writer_operator_drill`, `/api/market_intel/manual_sample_review/candidate_queue_writer_run_package`, `/api/market_intel/manual_sample_review/candidate_queue_writer_run_readiness`, `/api/market_intel/manual_sample_review/candidate_queue_writer_run_receipt`, `/api/market_intel/manual_sample_review/candidate_queue_writer_run_closeout`, `/api/market_intel/manual_sample_review/candidate_queue_review_handoff`, `/api/market_intel/match_review_plan`, `/api/market_intel/opportunity_plan`, `/api/market_intel/opportunity_scoring_plan`, `/api/market_intel/opportunity_evidence_plan`, `/api/market_intel/opportunity_alert_plan`, `/api/market_intel/adapters`, `/api/market_intel/dry_run_plan`, `/api/market_intel/discovery_plan`, `/api/market_intel/manual_discovery`, `/api/market_intel/candidate_preview`, `/api/market_intel/platform_seed_plan`, `/api/market_intel/platform_seed_write_guard`, `/api/market_intel/platform_seed_writer_plan`, `/api/market_intel/migration_blueprint`, `/api/market_intel/migration_apply_drill`, `/api/market_intel/migration_catalog_review`, `/api/market_intel/migration_live_smoke`, `/api/market_intel/live_db_inventory`, `/api/market_intel/seed_writer_cli_status`, `/api/market_intel/write_approval_runbook`, `/api/market_intel/deployment_readiness` |
|
||||
| `market_intel_review_routes.py` | 市場情報人工 queue review 只讀延伸 API | `/api/market_intel/manual_sample_review/candidate_queue_review_inventory`, `/api/market_intel/manual_sample_review/candidate_queue_review_decision`, `/api/market_intel/manual_sample_review/candidate_queue_review_decision_approval`, `/api/market_intel/manual_sample_review/candidate_queue_review_decision_transaction`, `/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_status`, `/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_preflight`, `/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_postwrite_smoke`, `/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_operator_drill`, `/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_run_package`, `/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_run_readiness`, `/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_run_receipt`, `/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_run_closeout` |
|
||||
| `market_intel_review_post_routes.py` | 市場情報 review_state closeout 後只讀延伸 API(掛在 `market_intel_review_bp`) | `/api/market_intel/manual_sample_review/candidate_queue_review_decision_post_closeout_inventory`, `/api/market_intel/manual_sample_review/candidate_queue_review_completion_archive`, `/api/market_intel/manual_sample_review/candidate_queue_review_archive_summary`, `/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_preflight`, `/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_run_package` |
|
||||
| `market_intel_review_post_routes.py` | 市場情報 review_state closeout 後只讀延伸 API(掛在 `market_intel_review_bp`) | `/api/market_intel/manual_sample_review/candidate_queue_review_decision_post_closeout_inventory`, `/api/market_intel/manual_sample_review/candidate_queue_review_completion_archive`, `/api/market_intel/manual_sample_review/candidate_queue_review_archive_summary`, `/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_preflight`, `/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_run_package`, `/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_output_receipt` |
|
||||
| `api_routes.py` | 通用任務與查詢 API | `/api/run_task`, `/api/history/*` |
|
||||
| `export_routes.py` | 匯出功能 | `/api/export/*` |
|
||||
| `import_routes.py` | 匯入功能 | `/api/import_excel`, `/api/import/monthly_summary` |
|
||||
|
||||
@@ -14,6 +14,9 @@ from services.market_intel import MarketIntelService
|
||||
from services.market_intel.candidate_queue_review_ai_summary_preflight import (
|
||||
build_candidate_queue_review_ai_summary_preflight,
|
||||
)
|
||||
from services.market_intel.candidate_queue_review_ai_summary_output_receipt import (
|
||||
build_candidate_queue_review_ai_summary_output_receipt,
|
||||
)
|
||||
from services.market_intel.candidate_queue_review_ai_summary_run_package import (
|
||||
build_candidate_queue_review_ai_summary_run_package,
|
||||
)
|
||||
@@ -182,6 +185,53 @@ def _build_review_ai_summary_preflight_stack(
|
||||
)
|
||||
|
||||
|
||||
def _build_review_ai_summary_run_package_stack(
|
||||
*,
|
||||
service,
|
||||
sample_result,
|
||||
payload_error,
|
||||
operator_evidence,
|
||||
writer_output,
|
||||
smoke_result,
|
||||
limit,
|
||||
execute_requested,
|
||||
):
|
||||
(
|
||||
transaction,
|
||||
receipt,
|
||||
closeout,
|
||||
inventory,
|
||||
archive,
|
||||
archive_summary,
|
||||
ai_summary_preflight,
|
||||
) = _build_review_ai_summary_preflight_stack(
|
||||
service=service,
|
||||
sample_result=sample_result,
|
||||
payload_error=payload_error,
|
||||
operator_evidence=operator_evidence,
|
||||
writer_output=writer_output,
|
||||
smoke_result=smoke_result,
|
||||
limit=limit,
|
||||
execute_requested=execute_requested,
|
||||
)
|
||||
ai_summary_run_package = build_candidate_queue_review_ai_summary_run_package(
|
||||
archive_summary=archive_summary,
|
||||
ai_summary_preflight=ai_summary_preflight,
|
||||
operator_evidence=operator_evidence,
|
||||
execute_requested=execute_requested,
|
||||
)
|
||||
return (
|
||||
transaction,
|
||||
receipt,
|
||||
closeout,
|
||||
inventory,
|
||||
archive,
|
||||
archive_summary,
|
||||
ai_summary_preflight,
|
||||
ai_summary_run_package,
|
||||
)
|
||||
|
||||
|
||||
@market_intel_review_bp.route(
|
||||
"/api/market_intel/manual_sample_review/"
|
||||
"candidate_queue_review_decision_post_closeout_inventory",
|
||||
@@ -293,7 +343,8 @@ def market_intel_manual_sample_candidate_queue_review_ai_summary_run_package():
|
||||
archive,
|
||||
archive_summary,
|
||||
ai_summary_preflight,
|
||||
) = _build_review_ai_summary_preflight_stack(
|
||||
ai_summary_run_package,
|
||||
) = _build_review_ai_summary_run_package_stack(
|
||||
service=service,
|
||||
sample_result=sample_result,
|
||||
payload_error=payload_error,
|
||||
@@ -303,9 +354,44 @@ def market_intel_manual_sample_candidate_queue_review_ai_summary_run_package():
|
||||
limit=limit,
|
||||
execute_requested=execute_requested,
|
||||
)
|
||||
data = build_candidate_queue_review_ai_summary_run_package(
|
||||
archive_summary=archive_summary,
|
||||
ai_summary_preflight=ai_summary_preflight,
|
||||
data = ai_summary_run_package
|
||||
data["phase"] = service.phase
|
||||
return jsonify(data), 400 if payload_error else 200
|
||||
|
||||
|
||||
@market_intel_review_bp.route(
|
||||
"/api/market_intel/manual_sample_review/"
|
||||
"candidate_queue_review_ai_summary_output_receipt",
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
def market_intel_manual_sample_candidate_queue_review_ai_summary_output_receipt():
|
||||
service = MarketIntelService()
|
||||
execute_requested = request.args.get("execute", "false").lower() == "true"
|
||||
sample_result, operator_evidence, writer_output, smoke_result, payload_error, limit = (
|
||||
_extract_run_payload()
|
||||
)
|
||||
(
|
||||
transaction,
|
||||
receipt,
|
||||
closeout,
|
||||
inventory,
|
||||
archive,
|
||||
archive_summary,
|
||||
ai_summary_preflight,
|
||||
ai_summary_run_package,
|
||||
) = _build_review_ai_summary_run_package_stack(
|
||||
service=service,
|
||||
sample_result=sample_result,
|
||||
payload_error=payload_error,
|
||||
operator_evidence=operator_evidence,
|
||||
writer_output=writer_output,
|
||||
smoke_result=smoke_result,
|
||||
limit=limit,
|
||||
execute_requested=execute_requested,
|
||||
)
|
||||
data = build_candidate_queue_review_ai_summary_output_receipt(
|
||||
ai_summary_run_package=ai_summary_run_package,
|
||||
operator_evidence=operator_evidence,
|
||||
execute_requested=execute_requested,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,451 @@
|
||||
"""候選審核 queue AI summary output receipt 預覽。
|
||||
|
||||
本模組只驗收人工 Ollama 摘要輸出是否符合 run package schema 與 evidence contract;
|
||||
不呼叫 LLM、不派送 Telegram、不讀 approval token、不執行 CLI、不寫檔、
|
||||
不寫 DB、不更新 review_state、不 commit、不掛 scheduler。
|
||||
"""
|
||||
|
||||
|
||||
FORBIDDEN_TOKEN_KEYWORDS = (
|
||||
"approval_token",
|
||||
"approval-token",
|
||||
"market_intel_queue_write_approval",
|
||||
)
|
||||
SAFE_TOKEN_METADATA_KEYS = {
|
||||
"approval_token_present",
|
||||
"approval_token_valid",
|
||||
"approval_token_secret_configured",
|
||||
}
|
||||
SAFE_APPROVAL_ENV_VAR = "MARKET_INTEL_QUEUE_WRITE_APPROVAL"
|
||||
TARGET_TABLE = "market_alert_review_queue"
|
||||
|
||||
|
||||
def _as_dict(value):
|
||||
return value if isinstance(value, dict) else {}
|
||||
|
||||
|
||||
def _as_list(value):
|
||||
if value is None:
|
||||
return []
|
||||
if isinstance(value, (list, tuple, set)):
|
||||
return list(value)
|
||||
return [value]
|
||||
|
||||
|
||||
def _contains_forbidden_token_key(value):
|
||||
if isinstance(value, dict):
|
||||
for key, nested in value.items():
|
||||
normalized_key = str(key).lower()
|
||||
if normalized_key in SAFE_TOKEN_METADATA_KEYS and isinstance(nested, bool):
|
||||
continue
|
||||
if normalized_key == "approval_env_var" and nested == SAFE_APPROVAL_ENV_VAR:
|
||||
continue
|
||||
if any(token_key in normalized_key for token_key in FORBIDDEN_TOKEN_KEYWORDS):
|
||||
return True
|
||||
if _contains_forbidden_token_key(nested):
|
||||
return True
|
||||
elif isinstance(value, list):
|
||||
return any(_contains_forbidden_token_key(item) for item in value)
|
||||
return False
|
||||
|
||||
|
||||
def _safe_text(value, limit=300):
|
||||
if value is None:
|
||||
return None
|
||||
text = str(value).strip()
|
||||
return text[:limit] if text else None
|
||||
|
||||
|
||||
def _side_effects_clear(*payloads):
|
||||
blocked_keys = (
|
||||
"api_executes_cli",
|
||||
"api_executes_llm",
|
||||
"api_reads_approval_token",
|
||||
"api_writes_file",
|
||||
"api_writes_database",
|
||||
"api_updates_review_state",
|
||||
"run_package_file_written",
|
||||
"summary_file_written",
|
||||
"summary_record_written",
|
||||
"summary_manifest_written",
|
||||
"summary_receipt_file_written",
|
||||
"database_write_executed",
|
||||
"database_commit_executed",
|
||||
"review_state_update_executed",
|
||||
"llm_call_executed",
|
||||
"ollama_call_executed",
|
||||
"gemini_call_executed",
|
||||
"telegram_dispatched",
|
||||
"ai_summary_generated",
|
||||
"external_network_executed",
|
||||
"scheduler_attached",
|
||||
"writes_executed",
|
||||
"would_write_database",
|
||||
)
|
||||
return all(
|
||||
not _as_dict(payload).get(key)
|
||||
for payload in payloads
|
||||
for key in blocked_keys
|
||||
)
|
||||
|
||||
|
||||
def _operator_summary(operator_evidence):
|
||||
operator_evidence = _as_dict(operator_evidence)
|
||||
output = operator_evidence.get("ai_summary_output") or operator_evidence.get(
|
||||
"summary_output"
|
||||
)
|
||||
return {
|
||||
"provided_keys": sorted(operator_evidence.keys()),
|
||||
"operator_confirmed_ai_summary_output_receipt": bool(
|
||||
operator_evidence.get("operator_confirmed_ai_summary_output_receipt")
|
||||
or operator_evidence.get("operator_confirmed_summary_output_receipt")
|
||||
),
|
||||
"operator_confirmed_manual_output_only": bool(
|
||||
operator_evidence.get("operator_confirmed_manual_output_only")
|
||||
or operator_evidence.get("operator_confirmed_manual_ollama_summary_run")
|
||||
),
|
||||
"operator_confirmed_ollama_first": bool(
|
||||
operator_evidence.get("operator_confirmed_ollama_first")
|
||||
or operator_evidence.get("operator_confirmed_ollama_cascade")
|
||||
),
|
||||
"operator_confirmed_gemini_backup_only": bool(
|
||||
operator_evidence.get("operator_confirmed_gemini_backup_only")
|
||||
or operator_evidence.get("operator_confirmed_gemini_fallback_only")
|
||||
),
|
||||
"operator_confirmed_no_api_llm_call": bool(
|
||||
operator_evidence.get("operator_confirmed_no_api_llm_call")
|
||||
or operator_evidence.get("operator_confirmed_no_llm_call")
|
||||
),
|
||||
"operator_confirmed_no_telegram_dispatch": bool(
|
||||
operator_evidence.get("operator_confirmed_no_telegram_dispatch")
|
||||
),
|
||||
"operator_confirmed_no_scheduler_attach": bool(
|
||||
operator_evidence.get("operator_confirmed_no_scheduler_attach")
|
||||
),
|
||||
"operator_confirmed_no_api_db_write": bool(
|
||||
operator_evidence.get("operator_confirmed_no_api_db_write")
|
||||
),
|
||||
"ai_summary_output_artifact_path": _safe_text(
|
||||
operator_evidence.get("ai_summary_output_artifact_path")
|
||||
or operator_evidence.get("summary_output_artifact_path")
|
||||
),
|
||||
"ai_summary_receipt_artifact_path": _safe_text(
|
||||
operator_evidence.get("ai_summary_receipt_artifact_path")
|
||||
or operator_evidence.get("summary_receipt_artifact_path")
|
||||
),
|
||||
"manual_ai_summary_output_provided": isinstance(output, dict),
|
||||
"approval_token_submitted_to_api": _contains_forbidden_token_key(
|
||||
operator_evidence
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _run_package_payload(ai_summary_run_package):
|
||||
package = _as_dict(ai_summary_run_package)
|
||||
prompt = _as_dict(package.get("prompt_contract"))
|
||||
input_contract = _as_dict(prompt.get("input_contract"))
|
||||
output_schema = _as_dict(package.get("summary_output_schema"))
|
||||
return {
|
||||
"provided": bool(package),
|
||||
"mode": package.get("mode"),
|
||||
"ai_summary_run_package_ready": bool(
|
||||
package.get("ai_summary_run_package_ready")
|
||||
),
|
||||
"ready_for_manual_ollama_summary_run": bool(
|
||||
package.get("ready_for_manual_ollama_summary_run")
|
||||
),
|
||||
"model_route_policy": _as_dict(package.get("model_route_policy")),
|
||||
"prompt_contract_version": prompt.get("contract_version"),
|
||||
"summary_output_schema": output_schema,
|
||||
"required_fields": _as_list(output_schema.get("required_fields")),
|
||||
"expected_dedupe_keys": _as_list(package.get("expected_dedupe_keys")),
|
||||
"summary_section_keys": _as_list(package.get("summary_section_keys")),
|
||||
"artifact_paths": _as_dict(input_contract.get("artifact_paths")),
|
||||
"safe_boundaries": _as_list(package.get("safe_boundaries")),
|
||||
"blocked_reasons": _as_list(package.get("blocked_reasons")),
|
||||
"read_only_query_executed": bool(package.get("read_only_query_executed")),
|
||||
"database_connection_opened": bool(package.get("database_connection_opened")),
|
||||
"database_write_executed": bool(package.get("database_write_executed")),
|
||||
"database_commit_executed": bool(package.get("database_commit_executed")),
|
||||
"review_state_update_executed": bool(
|
||||
package.get("review_state_update_executed")
|
||||
),
|
||||
"llm_call_executed": bool(package.get("llm_call_executed")),
|
||||
"ollama_call_executed": bool(package.get("ollama_call_executed")),
|
||||
"gemini_call_executed": bool(package.get("gemini_call_executed")),
|
||||
"telegram_dispatched": bool(package.get("telegram_dispatched")),
|
||||
"ai_summary_generated": bool(package.get("ai_summary_generated")),
|
||||
"scheduler_attached": bool(package.get("scheduler_attached")),
|
||||
}
|
||||
|
||||
|
||||
def _summary_output(operator_evidence):
|
||||
output = _as_dict(operator_evidence).get("ai_summary_output") or _as_dict(
|
||||
operator_evidence
|
||||
).get("summary_output")
|
||||
return _as_dict(output)
|
||||
|
||||
|
||||
def _required_field_status(*, output, required_fields):
|
||||
missing = [field for field in required_fields if field not in output]
|
||||
empty_allowed = {"risk_flags"}
|
||||
empty = [
|
||||
field
|
||||
for field in required_fields
|
||||
if field in output
|
||||
and field not in empty_allowed
|
||||
and output.get(field) in (None, "", [], {})
|
||||
]
|
||||
return missing, empty
|
||||
|
||||
|
||||
def _output_shape_valid(output):
|
||||
if not output:
|
||||
return False
|
||||
if not isinstance(output.get("headline"), str):
|
||||
return False
|
||||
if not isinstance(output.get("executive_summary"), str):
|
||||
return False
|
||||
for key in ("key_findings", "risk_flags", "recommended_actions", "evidence_refs"):
|
||||
if not isinstance(output.get(key), list):
|
||||
return False
|
||||
return isinstance(output.get("model_route"), dict)
|
||||
|
||||
|
||||
def _allowed_evidence_refs(package):
|
||||
artifact_paths = package["artifact_paths"]
|
||||
return sorted(
|
||||
set(str(item) for item in package["expected_dedupe_keys"])
|
||||
| set(str(item) for item in package["summary_section_keys"])
|
||||
| set(str(key) for key in artifact_paths.keys())
|
||||
| set(str(value) for value in artifact_paths.values() if value)
|
||||
)
|
||||
|
||||
|
||||
def _evidence_refs_grounded(*, output, package):
|
||||
refs = [str(item) for item in _as_list(output.get("evidence_refs")) if item]
|
||||
allowed = set(_allowed_evidence_refs(package))
|
||||
return bool(refs) and all(ref in allowed for ref in refs), refs
|
||||
|
||||
|
||||
def _model_route_accepted(*, output, package):
|
||||
model_route = _as_dict(output.get("model_route"))
|
||||
route_policy = package["model_route_policy"]
|
||||
cascade = _as_list(route_policy.get("primary_cascade"))
|
||||
allowed_hosts = {str(_as_dict(node).get("host")) for node in cascade}
|
||||
provider = model_route.get("provider")
|
||||
host = model_route.get("host")
|
||||
fallback_reason = _safe_text(model_route.get("fallback_reason"))
|
||||
failed_hosts = set(str(host) for host in _as_list(model_route.get("ollama_failed_hosts")))
|
||||
if route_policy.get("primary_policy") != "ollama_first":
|
||||
return False
|
||||
if route_policy.get("app_host_forbidden_as_ollama_node") == host:
|
||||
return False
|
||||
if provider == "ollama":
|
||||
return bool(host in allowed_hosts)
|
||||
if provider == "gemini_fallback":
|
||||
return bool(
|
||||
route_policy.get("fallback_policy")
|
||||
== "gemini_backup_only_after_ollama_cascade_failure"
|
||||
and fallback_reason
|
||||
and len(failed_hosts & allowed_hosts) == len(allowed_hosts)
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def _receipt_gates(*, package, output, operator):
|
||||
missing_fields, empty_fields = _required_field_status(
|
||||
output=output,
|
||||
required_fields=package["required_fields"],
|
||||
)
|
||||
grounded, refs = _evidence_refs_grounded(output=output, package=package)
|
||||
route_accepted = _model_route_accepted(output=output, package=package)
|
||||
return [
|
||||
{
|
||||
"key": "ai_summary_run_package_ready",
|
||||
"label": "AI summary run package 必須已通過",
|
||||
"passed": bool(
|
||||
package["provided"]
|
||||
and package["mode"]
|
||||
== "candidate_queue_review_ai_summary_run_package_preview"
|
||||
and package["ai_summary_run_package_ready"]
|
||||
and package["ready_for_manual_ollama_summary_run"]
|
||||
),
|
||||
},
|
||||
{
|
||||
"key": "manual_ai_summary_output_provided",
|
||||
"label": "必須提供人工 Ollama 摘要輸出 JSON",
|
||||
"passed": bool(output and operator["manual_ai_summary_output_provided"]),
|
||||
},
|
||||
{
|
||||
"key": "summary_output_required_fields_present",
|
||||
"label": "摘要輸出必須包含 schema 必要欄位",
|
||||
"passed": bool(package["required_fields"] and not missing_fields and not empty_fields),
|
||||
},
|
||||
{
|
||||
"key": "summary_output_shape_valid",
|
||||
"label": "摘要輸出欄位型別必須符合 contract",
|
||||
"passed": _output_shape_valid(output),
|
||||
},
|
||||
{
|
||||
"key": "summary_output_evidence_refs_grounded",
|
||||
"label": "摘要 evidence_refs 必須對應 dedupe key、section 或 artifact",
|
||||
"passed": grounded,
|
||||
},
|
||||
{
|
||||
"key": "summary_output_model_route_accepted",
|
||||
"label": "摘要 model_route 必須符合 Ollama-first / Gemini fallback-only",
|
||||
"passed": route_accepted,
|
||||
},
|
||||
{
|
||||
"key": "operator_confirmed_ai_summary_output_receipt",
|
||||
"label": "操作員確認本步只驗收人工摘要輸出",
|
||||
"passed": bool(
|
||||
operator["operator_confirmed_ai_summary_output_receipt"]
|
||||
and operator["operator_confirmed_manual_output_only"]
|
||||
and operator["operator_confirmed_ollama_first"]
|
||||
and operator["operator_confirmed_gemini_backup_only"]
|
||||
and operator["operator_confirmed_no_api_llm_call"]
|
||||
and operator["operator_confirmed_no_telegram_dispatch"]
|
||||
and operator["operator_confirmed_no_api_db_write"]
|
||||
and operator["operator_confirmed_no_scheduler_attach"]
|
||||
),
|
||||
},
|
||||
{
|
||||
"key": "ai_summary_output_receipt_artifacts_recorded",
|
||||
"label": "summary output 與 receipt artifact path 必須由操作員提供",
|
||||
"passed": bool(
|
||||
operator["ai_summary_output_artifact_path"]
|
||||
and operator["ai_summary_receipt_artifact_path"]
|
||||
),
|
||||
},
|
||||
{
|
||||
"key": "ai_summary_output_receipt_no_approval_token_submitted_to_api",
|
||||
"label": "payload 不得包含一次性 approval token key",
|
||||
"passed": not operator["approval_token_submitted_to_api"],
|
||||
},
|
||||
{
|
||||
"key": "ai_summary_output_receipt_has_no_side_effects",
|
||||
"label": "receipt 不得呼叫 LLM、派送 Telegram、寫檔、寫 DB 或掛 scheduler",
|
||||
"passed": _side_effects_clear(package),
|
||||
},
|
||||
], {
|
||||
"missing_fields": missing_fields,
|
||||
"empty_fields": empty_fields,
|
||||
"evidence_refs": refs,
|
||||
"allowed_evidence_refs": _allowed_evidence_refs(package),
|
||||
"model_route_accepted": route_accepted,
|
||||
}
|
||||
|
||||
|
||||
def build_candidate_queue_review_ai_summary_output_receipt(
|
||||
*,
|
||||
ai_summary_run_package,
|
||||
operator_evidence=None,
|
||||
execute_requested=False,
|
||||
):
|
||||
"""建立人工 AI summary output receipt 預覽;不呼叫模型或執行副作用。"""
|
||||
package = _run_package_payload(ai_summary_run_package)
|
||||
operator = _operator_summary(operator_evidence)
|
||||
output = _summary_output(operator_evidence)
|
||||
gates, validation = _receipt_gates(
|
||||
package=package,
|
||||
output=output,
|
||||
operator=operator,
|
||||
)
|
||||
blocked_reasons = [gate["key"] for gate in gates if not gate["passed"]]
|
||||
receipt_ready = bool(not blocked_reasons)
|
||||
|
||||
return {
|
||||
"mode": "candidate_queue_review_ai_summary_output_receipt_preview",
|
||||
"target_table": TARGET_TABLE,
|
||||
"target_operation": "review_manual_ollama_ai_summary_output",
|
||||
"execute_requested": bool(execute_requested),
|
||||
"ai_summary_output_receipt_ready": receipt_ready,
|
||||
"ready_for_summary_persistence_review": receipt_ready,
|
||||
"ready_for_next_manual_phase": receipt_ready,
|
||||
"manual_ai_summary_output_provided": operator[
|
||||
"manual_ai_summary_output_provided"
|
||||
],
|
||||
"summary_output_schema_valid": bool(
|
||||
output and not validation["missing_fields"] and not validation["empty_fields"]
|
||||
),
|
||||
"summary_output_evidence_refs_grounded": bool(
|
||||
validation["evidence_refs"]
|
||||
and all(
|
||||
ref in set(validation["allowed_evidence_refs"])
|
||||
for ref in validation["evidence_refs"]
|
||||
)
|
||||
),
|
||||
"summary_output_model_route_accepted": validation["model_route_accepted"],
|
||||
"ready_for_ai_summary_generation": False,
|
||||
"ready_for_llm_call": False,
|
||||
"ready_for_telegram_dispatch": False,
|
||||
"ready_for_api_review_state_update": False,
|
||||
"ready_for_api_database_write": False,
|
||||
"ready_for_scheduler_attach": False,
|
||||
"api_executes_cli": False,
|
||||
"api_executes_llm": False,
|
||||
"api_reads_approval_token": False,
|
||||
"api_writes_file": False,
|
||||
"api_writes_database": False,
|
||||
"api_updates_review_state": False,
|
||||
"approval_record_written": False,
|
||||
"decision_record_written": False,
|
||||
"run_package_file_written": False,
|
||||
"summary_file_written": False,
|
||||
"summary_record_written": False,
|
||||
"summary_manifest_written": False,
|
||||
"summary_receipt_file_written": False,
|
||||
"ai_summary_generated": False,
|
||||
"llm_call_executed": False,
|
||||
"ollama_call_executed": False,
|
||||
"gemini_call_executed": False,
|
||||
"telegram_dispatched": False,
|
||||
"review_state_update_executed": False,
|
||||
"read_only_query_executed": package["read_only_query_executed"],
|
||||
"database_connection_opened": package["database_connection_opened"],
|
||||
"database_session_created": False,
|
||||
"explicit_transaction_opened": False,
|
||||
"transaction_opened": False,
|
||||
"transaction_committed": False,
|
||||
"database_write_executed": False,
|
||||
"database_commit_executed": False,
|
||||
"database_rollback_executed": False,
|
||||
"external_network_executed": False,
|
||||
"scheduler_attached": False,
|
||||
"writes_executed": False,
|
||||
"would_write_database": False,
|
||||
"expected_dedupe_keys": package["expected_dedupe_keys"],
|
||||
"summary_section_keys": package["summary_section_keys"],
|
||||
"summary_output_required_fields": package["required_fields"],
|
||||
"summary_output_validation": validation,
|
||||
"model_route_policy": package["model_route_policy"],
|
||||
"operator_ai_summary_output_receipt": operator,
|
||||
"blocked_reasons": blocked_reasons,
|
||||
"gates": gates,
|
||||
"next_operator_steps": [
|
||||
"確認人工摘要 output JSON 已符合 schema 與 evidence_refs",
|
||||
"若要持久化 summary 或派送 Telegram,必須另開 persistence/dispatch gate",
|
||||
"保留 Ollama host 或 Gemini fallback reason 供後續稽核",
|
||||
"本 receipt 不寫檔、不寫 DB、不呼叫模型、不發 Telegram",
|
||||
],
|
||||
"safe_boundaries": [
|
||||
"do_not_call_llm_from_ai_summary_output_receipt",
|
||||
"do_not_dispatch_telegram_from_ai_summary_output_receipt",
|
||||
"do_not_write_summary_receipt_file_from_api",
|
||||
"do_not_write_summary_file_from_api",
|
||||
"do_not_insert_summary_record_from_api",
|
||||
"do_not_update_review_state_from_ai_summary_output_receipt",
|
||||
"do_not_read_approval_token_from_ai_summary_output_receipt",
|
||||
"do_not_execute_cli_from_ai_summary_output_receipt",
|
||||
"do_not_attach_scheduler_from_ai_summary_output_receipt",
|
||||
"manual_ai_summary_output_only",
|
||||
"ollama_cascade_must_use_gcp_a_then_gcp_b_then_111",
|
||||
"gemini_backup_only_after_ollama_cascade_failure",
|
||||
"host_188_forbidden_as_ollama_node",
|
||||
"ai_summary_output_receipt_preview_only",
|
||||
"no_remove_orphans",
|
||||
"no_momo_db_lifecycle_change",
|
||||
],
|
||||
}
|
||||
@@ -28,6 +28,7 @@ from services.market_intel.candidate_queue_review_decision_post_closeout_invento
|
||||
from services.market_intel.candidate_queue_review_completion_archive import build_candidate_queue_review_completion_archive
|
||||
from services.market_intel.candidate_queue_review_archive_summary import build_candidate_queue_review_archive_summary
|
||||
from services.market_intel.candidate_queue_review_ai_summary_preflight import build_candidate_queue_review_ai_summary_preflight
|
||||
from services.market_intel.candidate_queue_review_ai_summary_output_receipt import build_candidate_queue_review_ai_summary_output_receipt
|
||||
from services.market_intel.candidate_queue_review_ai_summary_run_package import build_candidate_queue_review_ai_summary_run_package
|
||||
|
||||
|
||||
@@ -49,6 +50,7 @@ BLOCKED_RUN_REVIEW_KEYS = (
|
||||
"summary_file_written",
|
||||
"summary_record_written",
|
||||
"summary_manifest_written",
|
||||
"summary_receipt_file_written",
|
||||
"ai_summary_generated",
|
||||
"llm_call_executed",
|
||||
"ollama_call_executed",
|
||||
@@ -67,7 +69,7 @@ BLOCKED_RUN_REVIEW_KEYS = (
|
||||
"writes_executed",
|
||||
"would_write_database",
|
||||
)
|
||||
PRODUCTION_SMOKE_TARGETS = ("/health", "/market_intel", "/api/market_intel/status", "/api/market_intel/deployment_readiness", "/api/market_intel/schema_smoke", "/api/market_intel/schema_db_probe", "/api/market_intel/platform_seed_db_diff", "/api/market_intel/legacy_source_bridge", "/api/market_intel/mcp_readiness", "/api/market_intel/mcp_tool_contract", "/api/market_intel/mcp_deploy_preflight", "/api/market_intel/mcp_activation_runbook", "/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/manual_sample_review", "/api/market_intel/match_review_plan", "/api/market_intel/opportunity_plan", "/api/market_intel/opportunity_scoring_plan", "/api/market_intel/opportunity_evidence_plan", "/api/market_intel/opportunity_alert_plan", "/api/market_intel/migration_apply_drill", "/api/market_intel/migration_catalog_review", "/api/market_intel/migration_live_smoke", "/api/market_intel/live_db_inventory", "/api/market_intel/manual_sample_review/candidate_queue_writer_postwrite_smoke", "/api/market_intel/manual_sample_review/candidate_queue_writer_operator_drill", "/api/market_intel/manual_sample_review/candidate_queue_writer_run_package", "/api/market_intel/manual_sample_review/candidate_queue_writer_run_readiness", "/api/market_intel/manual_sample_review/candidate_queue_writer_run_receipt", "/api/market_intel/manual_sample_review/candidate_queue_writer_run_closeout", "/api/market_intel/manual_sample_review/candidate_queue_review_handoff", "/api/market_intel/manual_sample_review/candidate_queue_review_inventory", "/api/market_intel/manual_sample_review/candidate_queue_review_decision", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_approval", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_transaction", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_preflight", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_postwrite_smoke", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_operator_drill", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_run_package", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_run_readiness", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_run_receipt", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_run_closeout", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_post_closeout_inventory", "/api/market_intel/manual_sample_review/candidate_queue_review_completion_archive", "/api/market_intel/manual_sample_review/candidate_queue_review_archive_summary", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_preflight", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_run_package", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_status")
|
||||
PRODUCTION_SMOKE_TARGETS = ("/health", "/market_intel", "/api/market_intel/status", "/api/market_intel/deployment_readiness", "/api/market_intel/schema_smoke", "/api/market_intel/schema_db_probe", "/api/market_intel/platform_seed_db_diff", "/api/market_intel/legacy_source_bridge", "/api/market_intel/mcp_readiness", "/api/market_intel/mcp_tool_contract", "/api/market_intel/mcp_deploy_preflight", "/api/market_intel/mcp_activation_runbook", "/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/manual_sample_review", "/api/market_intel/match_review_plan", "/api/market_intel/opportunity_plan", "/api/market_intel/opportunity_scoring_plan", "/api/market_intel/opportunity_evidence_plan", "/api/market_intel/opportunity_alert_plan", "/api/market_intel/migration_apply_drill", "/api/market_intel/migration_catalog_review", "/api/market_intel/migration_live_smoke", "/api/market_intel/live_db_inventory", "/api/market_intel/manual_sample_review/candidate_queue_writer_postwrite_smoke", "/api/market_intel/manual_sample_review/candidate_queue_writer_operator_drill", "/api/market_intel/manual_sample_review/candidate_queue_writer_run_package", "/api/market_intel/manual_sample_review/candidate_queue_writer_run_readiness", "/api/market_intel/manual_sample_review/candidate_queue_writer_run_receipt", "/api/market_intel/manual_sample_review/candidate_queue_writer_run_closeout", "/api/market_intel/manual_sample_review/candidate_queue_review_handoff", "/api/market_intel/manual_sample_review/candidate_queue_review_inventory", "/api/market_intel/manual_sample_review/candidate_queue_review_decision", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_approval", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_transaction", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_preflight", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_postwrite_smoke", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_operator_drill", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_run_package", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_run_readiness", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_run_receipt", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_run_closeout", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_post_closeout_inventory", "/api/market_intel/manual_sample_review/candidate_queue_review_completion_archive", "/api/market_intel/manual_sample_review/candidate_queue_review_archive_summary", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_preflight", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_run_package", "/api/market_intel/manual_sample_review/candidate_queue_review_ai_summary_output_receipt", "/api/market_intel/manual_sample_review/candidate_queue_review_decision_writer_status")
|
||||
|
||||
|
||||
def _run_review_preview_safe(payload, mode):
|
||||
@@ -222,6 +224,9 @@ def build_deployment_readiness_preview(
|
||||
archive_summary=candidate_queue_review_archive_summary,
|
||||
ai_summary_preflight=candidate_queue_review_ai_summary_preflight,
|
||||
)
|
||||
candidate_queue_review_ai_summary_output_receipt = build_candidate_queue_review_ai_summary_output_receipt(
|
||||
ai_summary_run_package=candidate_queue_review_ai_summary_run_package,
|
||||
)
|
||||
checks = {
|
||||
"schema_smoke_passed": bool(schema_smoke["passed"]),
|
||||
"feature_flags_default_safe": bool(
|
||||
@@ -508,6 +513,10 @@ def build_deployment_readiness_preview(
|
||||
candidate_queue_review_ai_summary_run_package,
|
||||
"candidate_queue_review_ai_summary_run_package_preview",
|
||||
),
|
||||
"candidate_queue_review_ai_summary_output_receipt_preview_safe": _run_review_preview_safe(
|
||||
candidate_queue_review_ai_summary_output_receipt,
|
||||
"candidate_queue_review_ai_summary_output_receipt_preview",
|
||||
),
|
||||
"candidate_queue_review_decision_writer_cli_status_safe": _run_review_preview_safe(
|
||||
candidate_queue_review_decision_writer_status,
|
||||
"candidate_queue_review_decision_writer_cli_blocked",
|
||||
@@ -756,6 +765,7 @@ def build_deployment_readiness_preview(
|
||||
"candidate_queue_review_archive_summary": candidate_queue_review_archive_summary,
|
||||
"candidate_queue_review_ai_summary_preflight": candidate_queue_review_ai_summary_preflight,
|
||||
"candidate_queue_review_ai_summary_run_package": candidate_queue_review_ai_summary_run_package,
|
||||
"candidate_queue_review_ai_summary_output_receipt": candidate_queue_review_ai_summary_output_receipt,
|
||||
"candidate_queue_review_decision_writer_status": candidate_queue_review_decision_writer_status,
|
||||
"match_review_plan": match_review_plan,
|
||||
"opportunity_plan": opportunity_plan,
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""市場情報 rollout phase 單一來源。"""
|
||||
|
||||
MARKET_INTEL_PHASE = "phase_81_candidate_queue_review_ai_summary_run_package"
|
||||
MARKET_INTEL_PHASE = "phase_82_candidate_queue_review_ai_summary_output_receipt"
|
||||
|
||||
@@ -682,6 +682,9 @@
|
||||
<button class="market-intel-icon-button" type="button" title="產生 queue review AI summary run package" data-market-intel-sample-candidate-queue-review-ai-summary-run-package>
|
||||
<i class="fas fa-box-open" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button class="market-intel-icon-button" type="button" title="驗收 queue review AI summary output receipt" data-market-intel-sample-candidate-queue-review-ai-summary-output-receipt>
|
||||
<i class="fas fa-file-circle-check" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1037,6 +1040,7 @@
|
||||
const sampleCandidateQueueReviewArchiveSummary = sampleReviewRoot ? sampleReviewRoot.querySelector('[data-market-intel-sample-candidate-queue-review-archive-summary]') : null;
|
||||
const sampleCandidateQueueReviewAiSummaryPreflight = sampleReviewRoot ? sampleReviewRoot.querySelector('[data-market-intel-sample-candidate-queue-review-ai-summary-preflight]') : null;
|
||||
const sampleCandidateQueueReviewAiSummaryRunPackage = sampleReviewRoot ? sampleReviewRoot.querySelector('[data-market-intel-sample-candidate-queue-review-ai-summary-run-package]') : null;
|
||||
const sampleCandidateQueueReviewAiSummaryOutputReceipt = sampleReviewRoot ? sampleReviewRoot.querySelector('[data-market-intel-sample-candidate-queue-review-ai-summary-output-receipt]') : null;
|
||||
const sampleReviewEndpoint = "{{ url_for('market_intel.market_intel_manual_sample_review') }}";
|
||||
const sampleReviewEvaluateEndpoint = "{{ url_for('market_intel.market_intel_manual_sample_review_evaluate') }}";
|
||||
const sampleCandidateHandoffEndpoint = "{{ url_for('market_intel.market_intel_manual_sample_candidate_handoff') }}";
|
||||
@@ -1069,6 +1073,7 @@
|
||||
const sampleCandidateQueueReviewArchiveSummaryEndpoint = "{{ url_for('market_intel_review.market_intel_manual_sample_candidate_queue_review_archive_summary') }}";
|
||||
const sampleCandidateQueueReviewAiSummaryPreflightEndpoint = "{{ url_for('market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_preflight') }}";
|
||||
const sampleCandidateQueueReviewAiSummaryRunPackageEndpoint = "{{ url_for('market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_run_package') }}";
|
||||
const sampleCandidateQueueReviewAiSummaryOutputReceiptEndpoint = "{{ url_for('market_intel_review.market_intel_manual_sample_candidate_queue_review_ai_summary_output_receipt') }}";
|
||||
const schedulerMeta = schedulerRoot ? schedulerRoot.querySelector('[data-market-intel-scheduler-meta]') : null;
|
||||
const schedulerBody = schedulerRoot ? schedulerRoot.querySelector('[data-market-intel-scheduler-body]') : null;
|
||||
const schedulerRefresh = schedulerRoot ? schedulerRoot.querySelector('[data-market-intel-scheduler-refresh]') : null;
|
||||
@@ -4949,6 +4954,108 @@
|
||||
}
|
||||
};
|
||||
|
||||
const renderCandidateQueueReviewAiSummaryOutputReceipt = data => {
|
||||
const blockers = (data.blocked_reasons || []).join(' / ');
|
||||
const gates = data.gates || [];
|
||||
const validation = data.summary_output_validation || {};
|
||||
const routePolicy = data.model_route_policy || {};
|
||||
sampleReviewMeta.innerHTML = [
|
||||
`mode=${data.mode || 'unknown'}`,
|
||||
`receipt=${data.ai_summary_output_receipt_ready ? 'ready' : 'blocked'}`,
|
||||
`schema=${data.summary_output_schema_valid ? 'pass' : 'blocked'}`,
|
||||
`refs=${data.summary_output_evidence_refs_grounded ? 'pass' : 'blocked'}`,
|
||||
`llm=${data.llm_call_executed ? 'called' : 'blocked'}`
|
||||
].map(item => `<span class="market-intel-pill">${escapeHtml(item)}</span>`).join('');
|
||||
sampleReviewBody.innerHTML = `
|
||||
<div class="market-intel-empty mb-3">此卡只驗收人工 Ollama 摘要輸出;API/UI 不呼叫模型、不寫 receipt、不派送 Telegram、不更新 review_state、不寫 DB。${blockers ? `阻擋:${escapeHtml(blockers)}` : ''}</div>
|
||||
<div class="market-intel-deploy-grid">
|
||||
<div>
|
||||
<p class="market-intel-deploy-section-title">RECEIPT GATES</p>
|
||||
<div class="market-intel-check-list">${
|
||||
gates.map(gate => `
|
||||
<div class="market-intel-check">
|
||||
<div>
|
||||
<strong>${escapeHtml(gate.key)}</strong>
|
||||
<small>${escapeHtml(gate.label)}</small>
|
||||
</div>
|
||||
<span>${gate.passed ? 'PASS' : 'BLOCK'}</span>
|
||||
</div>
|
||||
`).join('') || '<div class="market-intel-empty">尚未提供 output receipt gate。</div>'
|
||||
}</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="market-intel-deploy-section-title">OUTPUT VALIDATION</p>
|
||||
<div class="market-intel-check-list">
|
||||
${[
|
||||
['missing_fields', (validation.missing_fields || []).join(', ') || 'none'],
|
||||
['empty_fields', (validation.empty_fields || []).join(', ') || 'none'],
|
||||
['evidence_refs', (validation.evidence_refs || []).join(', ') || 'none'],
|
||||
['route_accepted', validation.model_route_accepted],
|
||||
['primary_policy', routePolicy.primary_policy || 'missing']
|
||||
].map(([key, value]) => `
|
||||
<div class="market-intel-check">
|
||||
<div><strong>${escapeHtml(key)}</strong></div>
|
||||
<span>${escapeHtml(String(value))}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="market-intel-deploy-section-title">NO-SIDE-EFFECTS</p>
|
||||
<div class="market-intel-check-list">
|
||||
${[
|
||||
['api_executes_llm', data.api_executes_llm],
|
||||
['llm_call_executed', data.llm_call_executed],
|
||||
['ollama_call_executed', data.ollama_call_executed],
|
||||
['gemini_call_executed', data.gemini_call_executed],
|
||||
['telegram_dispatched', data.telegram_dispatched],
|
||||
['summary_receipt_file_written', data.summary_receipt_file_written],
|
||||
['summary_file_written', data.summary_file_written],
|
||||
['database_write_executed', data.database_write_executed],
|
||||
['scheduler_attached', data.scheduler_attached]
|
||||
].map(([key, value]) => `
|
||||
<div class="market-intel-check">
|
||||
<div><strong>${escapeHtml(key)}</strong></div>
|
||||
<span>${escapeHtml(String(value))}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
const loadCandidateQueueReviewAiSummaryOutputReceipt = async () => {
|
||||
if (!sampleReviewMeta || !sampleReviewBody || !sampleReviewInput) return;
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(sampleReviewInput.value || '{}');
|
||||
} catch (error) {
|
||||
sampleReviewMeta.innerHTML = '<span class="market-intel-pill">json_error</span>';
|
||||
sampleReviewBody.innerHTML = `<div class="market-intel-empty">JSON 格式錯誤:${escapeHtml(error.message)}</div>`;
|
||||
return;
|
||||
}
|
||||
const body = parsed && parsed.sample_result ? parsed : { sample_result: parsed };
|
||||
sampleReviewBody.innerHTML = '<div class="market-intel-empty">驗收 queue review AI summary output receipt 中...</div>';
|
||||
try {
|
||||
const response = await fetch(sampleCandidateQueueReviewAiSummaryOutputReceiptEndpoint, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok && !data.mode) throw new Error(`HTTP ${response.status}`);
|
||||
renderCandidateQueueReviewAiSummaryOutputReceipt(data);
|
||||
} catch (error) {
|
||||
sampleReviewMeta.innerHTML = '<span class="market-intel-pill">error</span>';
|
||||
sampleReviewBody.innerHTML = `<div class="market-intel-empty">queue review AI summary output receipt 失敗:${escapeHtml(error.message)}</div>`;
|
||||
}
|
||||
};
|
||||
|
||||
const renderCandidateQueueReviewDecisionWriter = data => {
|
||||
const blockers = (data.blocked_reasons || []).join(' / ');
|
||||
const summary = data.statement_summary || {};
|
||||
@@ -6646,6 +6753,9 @@
|
||||
if (sampleCandidateQueueReviewAiSummaryRunPackage) {
|
||||
sampleCandidateQueueReviewAiSummaryRunPackage.addEventListener('click', loadCandidateQueueReviewAiSummaryRunPackage);
|
||||
}
|
||||
if (sampleCandidateQueueReviewAiSummaryOutputReceipt) {
|
||||
sampleCandidateQueueReviewAiSummaryOutputReceipt.addEventListener('click', loadCandidateQueueReviewAiSummaryOutputReceipt);
|
||||
}
|
||||
if (schedulerRefresh) {
|
||||
schedulerRefresh.addEventListener('click', loadScheduler);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user