[V10.358] add market intel MCP activation evidence review
All checks were successful
CD Pipeline / deploy (push) Successful in 1m7s
All checks were successful
CD Pipeline / deploy (push) Successful in 1m7s
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
================================================================================
|
||||
|
||||
【已完成】
|
||||
- V10.358 補市場情報 MCP 啟用證據審核:新增 `mcp_activation_evidence` read-only builder、GET/POST endpoint、UI redacted evidence 審核面板與 deployment readiness smoke target,讓操作員貼上 env/health/router/telemetry/fallback 證據後判斷能否補齊 external/internal MCP runtime 缺口;API/UI 不保存 payload、不打 health、不啟動 MCP、不執行 docker/SSH、不開 DB、不抓外站、不掛 scheduler,且會阻擋真實 secret 字串與任何 DB write/fetch/scheduler 證據。
|
||||
- V10.357 補市場情報 MCP 完整度稽核:新增 `mcp_completion_audit` read-only builder、GET endpoint、UI 面板與 deployment readiness smoke target,彙整外部 MCP design/runtime、內部 tool contract/runtime、activation runbook 與 fetch gate 狀態;API/UI 不啟動 MCP、不打 health、不執行 docker/SSH、不開 DB、不寫檔、不抓外站、不掛 scheduler。
|
||||
- V10.356 補市場情報 candidate queue review AI summary Telegram dispatch report catalog record final closeout gate:新增 read-only report catalog record final closeout builder、POST endpoint、UI 按鈕與 deployment readiness smoke target,在 archive summary gate 後覆核 catalog record identity、artifact traceability、sections、DB commit/post-write smoke、pipeline complete 與無後續 follow-up;API/UI 不讀 approval/Telegram token、不呼叫 LLM、不產報表、不派送 Telegram、不開 DB、不寫檔、不執行 CLI、不寫 catalog record、不 commit、不更新 review_state、不掛 scheduler。
|
||||
- V10.355 補市場情報 candidate queue review AI summary Telegram dispatch report catalog record archive summary gate:新增 read-only report catalog record archive summary builder、POST endpoint、UI 按鈕與 deployment readiness smoke target,在 archive gate 後整理 catalog record identity、artifact traceability、DB commit/post-write smoke、archive manifest/retention policy 與後續 final closeout separate gate;API/UI 不讀 approval/Telegram token、不呼叫 LLM、不產報表、不派送 Telegram、不開 DB、不寫檔、不執行 CLI、不寫 catalog record、不 commit、不更新 review_state、不掛 scheduler。
|
||||
|
||||
@@ -323,7 +323,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
|
||||
# ==========================================
|
||||
# 系統版本與路徑
|
||||
# ==========================================
|
||||
SYSTEM_VERSION = "V10.357"
|
||||
SYSTEM_VERSION = "V10.358"
|
||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||
public_url = PUBLIC_URL # 用於模板顯示
|
||||
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
|
||||
## 📅 詳細更新日誌 (考古存檔)
|
||||
|
||||
### 2026-05-21:市場情報 MCP 啟用證據審核
|
||||
- **V10.358 MCP activation evidence review**: 新增 `mcp_activation_evidence` read-only builder、GET/POST endpoint、UI redacted evidence 審核面板與 deployment readiness smoke target,讓操作員貼上 env/health/router/telemetry/fallback 證據後判斷能否補齊 external/internal MCP runtime 缺口。
|
||||
- **只讀安全邊界**: 本階段不保存 payload、不打 health、不啟動 MCP、不執行 docker/SSH、不開 DB、不抓外站、不掛 scheduler;payload 只允許 redacted/boolean,真實 secret 字串與任何 DB write/fetch/scheduler 證據會被阻擋。
|
||||
|
||||
### 2026-05-21:市場情報 MCP 完整度稽核
|
||||
- **V10.357 MCP completion audit**: 新增 `mcp_completion_audit` read-only builder、GET endpoint、UI 面板與 deployment readiness smoke target,彙整外部 MCP design/runtime、內部 tool contract/runtime、activation runbook 與 fetch gate 狀態。
|
||||
- **只讀安全邊界**: 本階段只做完整度稽核,不啟動 MCP、不打 health、不執行 docker/SSH、不開 DB、不寫檔、不抓外站、不掛 scheduler;外部 MCP runtime complete 仍需 operator 依 runbook 啟用與 health 驗證。
|
||||
|
||||
@@ -18,6 +18,7 @@ from services.market_intel.candidate_queue_writer_run_readiness import build_can
|
||||
from services.market_intel.candidate_queue_writer_run_receipt import build_candidate_queue_writer_run_receipt
|
||||
from services.market_intel.candidate_queue_writer_run_closeout import build_candidate_queue_writer_run_closeout
|
||||
from services.market_intel.candidate_queue_review_handoff import build_candidate_queue_review_handoff
|
||||
from services.market_intel.mcp_activation_evidence import build_mcp_activation_evidence_preview
|
||||
|
||||
|
||||
TAIPEI_TZ = timezone(timedelta(hours=8))
|
||||
@@ -166,6 +167,21 @@ def market_intel_mcp_completion_audit():
|
||||
return jsonify(_service().build_mcp_completion_audit())
|
||||
|
||||
|
||||
@market_intel_bp.route("/api/market_intel/mcp_activation_evidence", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def market_intel_mcp_activation_evidence():
|
||||
evidence = {}
|
||||
if request.method == "POST":
|
||||
payload = request.get_json(silent=True) or {}
|
||||
evidence = payload.get("evidence", payload)
|
||||
return jsonify(
|
||||
build_mcp_activation_evidence_preview(
|
||||
evidence=evidence,
|
||||
phase=_service().phase,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@market_intel_bp.route("/api/market_intel/scheduler_plan")
|
||||
@login_required
|
||||
def market_intel_scheduler_plan():
|
||||
|
||||
File diff suppressed because one or more lines are too long
269
services/market_intel/mcp_activation_evidence.py
Normal file
269
services/market_intel/mcp_activation_evidence.py
Normal file
@@ -0,0 +1,269 @@
|
||||
"""市場情報 MCP 啟用證據審核 preview。
|
||||
|
||||
操作員可貼上外部 MCP 啟用後的 redacted evidence;本模組只做結構化審核,
|
||||
不打 health endpoint、不開 DB、不寫入、不啟動 fetch。
|
||||
"""
|
||||
|
||||
|
||||
REQUIRED_ENV_VARS = ("MCP_POSTGRES_PASSWORD", "TAVILY_API_KEY", "EXA_API_KEY")
|
||||
EXPECTED_HEALTH_TARGETS = (
|
||||
"http://localhost:3001/health",
|
||||
"http://localhost:3002/health",
|
||||
"http://localhost:3003/health",
|
||||
"http://localhost:3004/health",
|
||||
)
|
||||
SAFE_LITERAL_VALUES = {"redacted", "***", "set", "present", "true", "yes", "ok"}
|
||||
PROHIBITED_EVIDENCE_FLAGS = (
|
||||
"database_write_executed",
|
||||
"database_commit_executed",
|
||||
"scheduler_attached",
|
||||
"crawler_job_started",
|
||||
"external_fetch_executed",
|
||||
"manual_fetch_executed",
|
||||
)
|
||||
|
||||
|
||||
def _truthy(value):
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
if isinstance(value, (int, float)):
|
||||
return bool(value)
|
||||
if isinstance(value, str):
|
||||
return value.strip().lower() in {"1", "true", "yes", "y", "ok", "pass", "passed", "healthy", "set", "present", "redacted", "***"}
|
||||
return False
|
||||
|
||||
|
||||
def _looks_like_secret_literal(value):
|
||||
if not isinstance(value, str):
|
||||
return False
|
||||
normalized = value.strip().lower()
|
||||
if not normalized or normalized in SAFE_LITERAL_VALUES:
|
||||
return False
|
||||
return len(value.strip()) > 2
|
||||
|
||||
|
||||
def _evidence_map(evidence, key):
|
||||
value = evidence.get(key)
|
||||
return value if isinstance(value, dict) else {}
|
||||
|
||||
|
||||
def _ack(evidence, key):
|
||||
acknowledgements = _evidence_map(evidence, "operator_acknowledgements")
|
||||
return _truthy(evidence.get(key)) or _truthy(acknowledgements.get(key))
|
||||
|
||||
|
||||
def _env_statuses(evidence):
|
||||
env = _evidence_map(evidence, "required_env_vars") or _evidence_map(evidence, "env")
|
||||
statuses = []
|
||||
for name in REQUIRED_ENV_VARS:
|
||||
value = env.get(name)
|
||||
statuses.append(
|
||||
{
|
||||
"name": name,
|
||||
"present": _truthy(value),
|
||||
"secret_literal_in_payload": _looks_like_secret_literal(value),
|
||||
"value_redacted": bool(value) and not _looks_like_secret_literal(value),
|
||||
}
|
||||
)
|
||||
return statuses
|
||||
|
||||
|
||||
def _health_item_ok(item):
|
||||
if isinstance(item, bool):
|
||||
return item
|
||||
if isinstance(item, (int, float)):
|
||||
return int(item) == 200
|
||||
if isinstance(item, str):
|
||||
return item.strip().lower() in {"200", "ok", "pass", "passed", "healthy"}
|
||||
if isinstance(item, dict):
|
||||
return (
|
||||
int(item.get("status_code") or item.get("code") or 0) == 200
|
||||
or _truthy(item.get("healthy"))
|
||||
or _truthy(item.get("passed"))
|
||||
or _truthy(item.get("status"))
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def _health_item_for_target(health, target):
|
||||
if isinstance(health, dict):
|
||||
if target in health:
|
||||
return health[target]
|
||||
port = target.rsplit(":", 1)[-1].split("/", 1)[0]
|
||||
return health.get(port) or health.get(f"localhost:{port}")
|
||||
if isinstance(health, list):
|
||||
for item in health:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
value = item.get("url") or item.get("target") or item.get("endpoint")
|
||||
port = str(item.get("port") or "")
|
||||
if value == target or (port and port in target):
|
||||
return item
|
||||
return None
|
||||
|
||||
|
||||
def _health_statuses(evidence):
|
||||
health = evidence.get("health_checks") or evidence.get("health_targets") or {}
|
||||
return [
|
||||
{
|
||||
"target": target,
|
||||
"passed": _health_item_ok(_health_item_for_target(health, target)),
|
||||
}
|
||||
for target in EXPECTED_HEALTH_TARGETS
|
||||
]
|
||||
|
||||
|
||||
def _sample_evidence_template():
|
||||
return {
|
||||
"required_env_vars": {
|
||||
"MCP_POSTGRES_PASSWORD": "redacted",
|
||||
"TAVILY_API_KEY": "redacted",
|
||||
"EXA_API_KEY": "redacted",
|
||||
},
|
||||
"health_checks": {
|
||||
"http://localhost:3001/health": 200,
|
||||
"http://localhost:3002/health": 200,
|
||||
"http://localhost:3003/health": 200,
|
||||
"http://localhost:3004/health": 200,
|
||||
},
|
||||
"mcp_router_enabled": True,
|
||||
"market_intel_readonly_smoke_passed": True,
|
||||
"telemetry": {
|
||||
"mcp_calls_table_exists": True,
|
||||
"read_only_query_executed": True,
|
||||
},
|
||||
"operator_acknowledgements": {
|
||||
"no_remove_orphans": True,
|
||||
"momo_db_preserved": True,
|
||||
"router_enabled_after_health": True,
|
||||
"rollback_plan_confirmed": True,
|
||||
"fetch_gate_left_closed": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def build_mcp_activation_evidence_preview(*, evidence=None, phase=None):
|
||||
"""審核 redacted MCP runtime evidence;不執行任何外部動作。"""
|
||||
evidence = evidence or {}
|
||||
payload_received = bool(evidence)
|
||||
env_statuses = _env_statuses(evidence)
|
||||
health_statuses = _health_statuses(evidence)
|
||||
telemetry = _evidence_map(evidence, "telemetry")
|
||||
prohibited_flags = [
|
||||
key for key in PROHIBITED_EVIDENCE_FLAGS
|
||||
if _truthy(evidence.get(key))
|
||||
]
|
||||
secret_literal_keys = [
|
||||
item["name"] for item in env_statuses
|
||||
if item["secret_literal_in_payload"]
|
||||
]
|
||||
gates = [
|
||||
{
|
||||
"key": "evidence_payload_received",
|
||||
"passed": payload_received,
|
||||
"label": "已提供 redacted MCP runtime evidence payload",
|
||||
},
|
||||
{
|
||||
"key": "no_secret_literals_in_payload",
|
||||
"passed": not secret_literal_keys,
|
||||
"label": "payload 只允許 redacted / boolean,不接受真實 secret 字串",
|
||||
},
|
||||
{
|
||||
"key": "required_env_vars_acknowledged",
|
||||
"passed": all(item["present"] for item in env_statuses),
|
||||
"label": "三個必要 env 已由操作員確認存在",
|
||||
},
|
||||
{
|
||||
"key": "all_health_targets_passed",
|
||||
"passed": all(item["passed"] for item in health_statuses),
|
||||
"label": "四個 localhost MCP health target 全部 200 / healthy",
|
||||
},
|
||||
{
|
||||
"key": "mcp_router_enabled_after_health",
|
||||
"passed": _truthy(evidence.get("mcp_router_enabled"))
|
||||
and _ack(evidence, "router_enabled_after_health"),
|
||||
"label": "router 僅在 health 全過後啟用",
|
||||
},
|
||||
{
|
||||
"key": "market_intel_readonly_smoke_passed",
|
||||
"passed": _truthy(evidence.get("market_intel_readonly_smoke_passed")),
|
||||
"label": "market_intel MCP read-only smoke 已通過",
|
||||
},
|
||||
{
|
||||
"key": "telemetry_runtime_confirmed",
|
||||
"passed": _truthy(telemetry.get("mcp_calls_table_exists"))
|
||||
and _truthy(telemetry.get("read_only_query_executed")),
|
||||
"label": "mcp_calls telemetry read-only 查詢鏈路已確認",
|
||||
},
|
||||
{
|
||||
"key": "operator_fallback_confirmed",
|
||||
"passed": _ack(evidence, "rollback_plan_confirmed"),
|
||||
"label": "router kill switch 與 stop MCP stack fallback 已確認",
|
||||
},
|
||||
{
|
||||
"key": "production_boundaries_acknowledged",
|
||||
"passed": _ack(evidence, "no_remove_orphans")
|
||||
and _ack(evidence, "momo_db_preserved"),
|
||||
"label": "已確認不使用 remove-orphans 且不動 momo-db lifecycle",
|
||||
},
|
||||
{
|
||||
"key": "manual_fetch_gate_left_closed",
|
||||
"passed": _ack(evidence, "fetch_gate_left_closed"),
|
||||
"label": "啟用 MCP 後仍未打開人工 fetch gate",
|
||||
},
|
||||
{
|
||||
"key": "no_write_or_scheduler_evidence",
|
||||
"passed": not prohibited_flags,
|
||||
"label": "證據未宣告 DB write、scheduler 或 fetch 已執行",
|
||||
},
|
||||
]
|
||||
blocked_reasons = [
|
||||
gate["key"] for gate in gates
|
||||
if not gate["passed"]
|
||||
]
|
||||
accepted = payload_received and not blocked_reasons
|
||||
|
||||
payload = {
|
||||
"mode": (
|
||||
"mcp_activation_evidence_review"
|
||||
if payload_received
|
||||
else "mcp_activation_evidence_preview"
|
||||
),
|
||||
"phase": phase,
|
||||
"evidence_payload_received": payload_received,
|
||||
"activation_evidence_accepted": accepted,
|
||||
"ready_for_runtime_promotion": accepted,
|
||||
"ready_for_fetch_gate_review": accepted,
|
||||
"external_mcp_runtime_evidence_complete": accepted,
|
||||
"internal_mcp_runtime_evidence_complete": accepted,
|
||||
"gate_count": len(gates),
|
||||
"passed_gate_count": sum(1 for gate in gates if gate["passed"]),
|
||||
"blocked_reasons": blocked_reasons,
|
||||
"gates": gates,
|
||||
"env_statuses": env_statuses,
|
||||
"health_statuses": health_statuses,
|
||||
"secret_literal_keys": secret_literal_keys,
|
||||
"prohibited_flags": prohibited_flags,
|
||||
"sample_evidence_template": _sample_evidence_template(),
|
||||
"next_operator_steps": [
|
||||
"若本審核通過,再跑 /api/market_intel/mcp_readiness?execute=true&timeout=3 做只讀 runtime smoke",
|
||||
"確認 completion audit 的 external/internal runtime 缺口被證據補齊",
|
||||
"人工 fetch gate 仍需另行審核,不可因 MCP runtime ready 自動抓外站",
|
||||
],
|
||||
"payload_persisted": False,
|
||||
"evidence_persisted": False,
|
||||
"api_executes_health_check": False,
|
||||
"api_executes_docker": False,
|
||||
"api_executes_ssh": False,
|
||||
"api_opens_database_connection": False,
|
||||
"api_writes_database": False,
|
||||
"api_uses_external_network": False,
|
||||
"database_session_created": False,
|
||||
"database_write_executed": False,
|
||||
"database_commit_executed": False,
|
||||
"external_network_executed": False,
|
||||
"scheduler_attached": False,
|
||||
"writes_executed": False,
|
||||
"would_write_database": False,
|
||||
}
|
||||
return payload
|
||||
@@ -1,3 +1,3 @@
|
||||
"""市場情報 rollout phase 單一來源。"""
|
||||
|
||||
MARKET_INTEL_PHASE = "phase_116_market_intel_mcp_completion_audit"
|
||||
MARKET_INTEL_PHASE = "phase_117_market_intel_mcp_activation_evidence"
|
||||
|
||||
@@ -552,6 +552,32 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="market-intel-panel" data-market-intel-mcp-activation-evidence>
|
||||
<div class="market-intel-preview-head">
|
||||
<div>
|
||||
<p class="market-intel-muted momo-mono mb-1">MCP / ACTIVATION EVIDENCE</p>
|
||||
<h2 class="market-intel-preview-title">MCP 啟用證據審核</h2>
|
||||
</div>
|
||||
<button class="market-intel-icon-button" type="button" title="重新整理 MCP 啟用證據審核" data-market-intel-mcp-activation-evidence-refresh>
|
||||
<i class="fas fa-rotate-right" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="market-intel-preview-meta" data-market-intel-mcp-activation-evidence-meta>
|
||||
<span class="market-intel-pill">loading</span>
|
||||
</div>
|
||||
<div data-market-intel-mcp-activation-evidence-body>
|
||||
<div class="market-intel-empty">讀取 MCP 啟用證據審核中...</div>
|
||||
</div>
|
||||
<div class="market-intel-control-row mt-3">
|
||||
<textarea class="market-intel-json-input" rows="7" spellcheck="false" data-market-intel-mcp-activation-evidence-input placeholder="redacted MCP activation evidence JSON"></textarea>
|
||||
<div class="market-intel-control-actions">
|
||||
<button class="market-intel-icon-button" type="button" title="審核 MCP 啟用證據 JSON" data-market-intel-mcp-activation-evidence-review>
|
||||
<i class="fas fa-check" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="market-intel-panel" data-market-intel-manual-sample>
|
||||
<div class="market-intel-preview-head">
|
||||
<div>
|
||||
@@ -1056,6 +1082,7 @@
|
||||
const mcpActivationRoot = document.querySelector('[data-market-intel-mcp-activation]');
|
||||
const mcpFetchGateRoot = document.querySelector('[data-market-intel-mcp-fetch-gate]');
|
||||
const mcpCompletionRoot = document.querySelector('[data-market-intel-mcp-completion]');
|
||||
const mcpActivationEvidenceRoot = document.querySelector('[data-market-intel-mcp-activation-evidence]');
|
||||
const manualSampleRoot = document.querySelector('[data-market-intel-manual-sample]');
|
||||
const sampleAcceptanceRoot = document.querySelector('[data-market-intel-sample-acceptance]');
|
||||
const sampleReviewRoot = document.querySelector('[data-market-intel-sample-review]');
|
||||
@@ -1072,7 +1099,7 @@
|
||||
const liveInventoryRoot = document.querySelector('[data-market-intel-live-inventory]');
|
||||
const approvalRoot = document.querySelector('[data-market-intel-approval]');
|
||||
const deployRoot = document.querySelector('[data-market-intel-deploy]');
|
||||
if (!root && !writerRoot && !cliRoot && !dbProbeRoot && !seedDiffRoot && !legacyBridgeRoot && !mcpReadinessRoot && !mcpPreflightRoot && !mcpActivationRoot && !mcpFetchGateRoot && !mcpCompletionRoot && !manualSampleRoot && !sampleAcceptanceRoot && !sampleReviewRoot && !schedulerRoot && !matchReviewRoot && !opportunityRoot && !opportunityScoringRoot && !opportunityEvidenceRoot && !opportunityAlertRoot && !migrationRoot && !migrationDrillRoot && !catalogReviewRoot && !liveSmokeRoot && !liveInventoryRoot && !approvalRoot && !deployRoot) return;
|
||||
if (!root && !writerRoot && !cliRoot && !dbProbeRoot && !seedDiffRoot && !legacyBridgeRoot && !mcpReadinessRoot && !mcpPreflightRoot && !mcpActivationRoot && !mcpFetchGateRoot && !mcpCompletionRoot && !mcpActivationEvidenceRoot && !manualSampleRoot && !sampleAcceptanceRoot && !sampleReviewRoot && !schedulerRoot && !matchReviewRoot && !opportunityRoot && !opportunityScoringRoot && !opportunityEvidenceRoot && !opportunityAlertRoot && !migrationRoot && !migrationDrillRoot && !catalogReviewRoot && !liveSmokeRoot && !liveInventoryRoot && !approvalRoot && !deployRoot) return;
|
||||
|
||||
const meta = root ? root.querySelector('[data-market-intel-preview-meta]') : null;
|
||||
const body = root ? root.querySelector('[data-market-intel-preview-body]') : null;
|
||||
@@ -1119,6 +1146,12 @@
|
||||
const mcpCompletionBody = mcpCompletionRoot ? mcpCompletionRoot.querySelector('[data-market-intel-mcp-completion-body]') : null;
|
||||
const mcpCompletionRefresh = mcpCompletionRoot ? mcpCompletionRoot.querySelector('[data-market-intel-mcp-completion-refresh]') : null;
|
||||
const mcpCompletionEndpoint = "{{ url_for('market_intel.market_intel_mcp_completion_audit') }}";
|
||||
const mcpActivationEvidenceMeta = mcpActivationEvidenceRoot ? mcpActivationEvidenceRoot.querySelector('[data-market-intel-mcp-activation-evidence-meta]') : null;
|
||||
const mcpActivationEvidenceBody = mcpActivationEvidenceRoot ? mcpActivationEvidenceRoot.querySelector('[data-market-intel-mcp-activation-evidence-body]') : null;
|
||||
const mcpActivationEvidenceInput = mcpActivationEvidenceRoot ? mcpActivationEvidenceRoot.querySelector('[data-market-intel-mcp-activation-evidence-input]') : null;
|
||||
const mcpActivationEvidenceReview = mcpActivationEvidenceRoot ? mcpActivationEvidenceRoot.querySelector('[data-market-intel-mcp-activation-evidence-review]') : null;
|
||||
const mcpActivationEvidenceRefresh = mcpActivationEvidenceRoot ? mcpActivationEvidenceRoot.querySelector('[data-market-intel-mcp-activation-evidence-refresh]') : null;
|
||||
const mcpActivationEvidenceEndpoint = "{{ url_for('market_intel.market_intel_mcp_activation_evidence') }}";
|
||||
const manualSampleMeta = manualSampleRoot ? manualSampleRoot.querySelector('[data-market-intel-manual-sample-meta]') : null;
|
||||
const manualSampleBody = manualSampleRoot ? manualSampleRoot.querySelector('[data-market-intel-manual-sample-body]') : null;
|
||||
const manualSampleRefresh = manualSampleRoot ? manualSampleRoot.querySelector('[data-market-intel-manual-sample-refresh]') : null;
|
||||
@@ -2106,6 +2139,116 @@
|
||||
}
|
||||
};
|
||||
|
||||
const renderMcpActivationEvidenceMeta = data => {
|
||||
mcpActivationEvidenceMeta.innerHTML = [
|
||||
`mode=${data.mode || 'unknown'}`,
|
||||
`accepted=${data.activation_evidence_accepted ? 'yes' : 'no'}`,
|
||||
`gates=${data.passed_gate_count || 0}/${data.gate_count || 0}`,
|
||||
`health=${(data.health_statuses || []).filter(item => item.passed).length}/4`,
|
||||
`persisted=${data.evidence_persisted ? 'yes' : 'no'}`
|
||||
].map(item => `<span class="market-intel-pill">${escapeHtml(item)}</span>`).join('');
|
||||
};
|
||||
|
||||
const renderMcpActivationEvidenceBody = data => {
|
||||
const blockers = (data.blocked_reasons || []).join(' / ');
|
||||
const gates = data.gates || [];
|
||||
const envs = data.env_statuses || [];
|
||||
const health = data.health_statuses || [];
|
||||
const steps = data.next_operator_steps || [];
|
||||
const renderCheck = (key, label, status) => `
|
||||
<div class="market-intel-check">
|
||||
<div>
|
||||
<strong>${escapeHtml(key)}</strong>
|
||||
<small>${escapeHtml(label || '')}</small>
|
||||
</div>
|
||||
<span>${escapeHtml(status)}</span>
|
||||
</div>
|
||||
`;
|
||||
mcpActivationEvidenceBody.innerHTML = `
|
||||
<div class="market-intel-empty mb-3">此審核只檢查操作員貼上的 redacted evidence;不打 health、不開 DB、不保存 payload、不啟動 fetch。${blockers ? `阻擋:${escapeHtml(blockers)}` : ''}</div>
|
||||
<div class="market-intel-deploy-grid">
|
||||
<div data-market-intel-mcp-activation-evidence-gates>
|
||||
<p class="market-intel-deploy-section-title">EVIDENCE GATES</p>
|
||||
<div class="market-intel-check-list">${
|
||||
gates.length
|
||||
? gates.map(item => renderCheck(item.key, item.label, item.passed ? 'PASS' : 'BLOCK')).join('')
|
||||
: '<div class="market-intel-empty">尚未提供 evidence gates。</div>'
|
||||
}</div>
|
||||
</div>
|
||||
<div data-market-intel-mcp-activation-evidence-env>
|
||||
<p class="market-intel-deploy-section-title">ENV / HEALTH</p>
|
||||
<div class="market-intel-check-list">
|
||||
${envs.map(item => renderCheck(
|
||||
item.name,
|
||||
item.secret_literal_in_payload ? 'secret literal blocked' : 'redacted or boolean only',
|
||||
item.present && !item.secret_literal_in_payload ? 'OK' : 'BLOCK'
|
||||
)).join('')}
|
||||
${health.map(item => renderCheck(
|
||||
item.target,
|
||||
'localhost health evidence',
|
||||
item.passed ? '200' : 'BLOCK'
|
||||
)).join('')}
|
||||
</div>
|
||||
</div>
|
||||
<div data-market-intel-mcp-activation-evidence-next>
|
||||
<p class="market-intel-deploy-section-title">NEXT</p>
|
||||
<div class="market-intel-check-list">${
|
||||
steps.map((item, index) => renderCheck(`step_${index + 1}`, item, 'MANUAL')).join('')
|
||||
}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
if (mcpActivationEvidenceInput && !mcpActivationEvidenceInput.value.trim() && data.sample_evidence_template) {
|
||||
mcpActivationEvidenceInput.value = JSON.stringify(data.sample_evidence_template, null, 2);
|
||||
}
|
||||
};
|
||||
|
||||
const loadMcpActivationEvidence = async () => {
|
||||
if (!mcpActivationEvidenceMeta || !mcpActivationEvidenceBody) return;
|
||||
mcpActivationEvidenceBody.innerHTML = '<div class="market-intel-empty">讀取 MCP 啟用證據審核中...</div>';
|
||||
try {
|
||||
const response = await fetch(mcpActivationEvidenceEndpoint, { credentials: 'same-origin' });
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const data = await response.json();
|
||||
renderMcpActivationEvidenceMeta(data);
|
||||
renderMcpActivationEvidenceBody(data);
|
||||
} catch (error) {
|
||||
mcpActivationEvidenceMeta.innerHTML = '<span class="market-intel-pill">error</span>';
|
||||
mcpActivationEvidenceBody.innerHTML = `<div class="market-intel-empty">MCP 啟用證據審核讀取失敗:${escapeHtml(error.message)}</div>`;
|
||||
}
|
||||
};
|
||||
|
||||
const reviewMcpActivationEvidence = async () => {
|
||||
if (!mcpActivationEvidenceMeta || !mcpActivationEvidenceBody || !mcpActivationEvidenceInput) return;
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(mcpActivationEvidenceInput.value || '{}');
|
||||
} catch (error) {
|
||||
mcpActivationEvidenceMeta.innerHTML = '<span class="market-intel-pill">json_error</span>';
|
||||
mcpActivationEvidenceBody.innerHTML = `<div class="market-intel-empty">JSON 格式錯誤:${escapeHtml(error.message)}</div>`;
|
||||
return;
|
||||
}
|
||||
mcpActivationEvidenceBody.innerHTML = '<div class="market-intel-empty">審核 MCP 啟用證據中...</div>';
|
||||
try {
|
||||
const response = await fetch(mcpActivationEvidenceEndpoint, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify({ evidence: parsed })
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok && !data.mode) throw new Error(`HTTP ${response.status}`);
|
||||
renderMcpActivationEvidenceMeta(data);
|
||||
renderMcpActivationEvidenceBody(data);
|
||||
} catch (error) {
|
||||
mcpActivationEvidenceMeta.innerHTML = '<span class="market-intel-pill">error</span>';
|
||||
mcpActivationEvidenceBody.innerHTML = `<div class="market-intel-empty">MCP 啟用證據審核失敗:${escapeHtml(error.message)}</div>`;
|
||||
}
|
||||
};
|
||||
|
||||
const renderManualSampleMeta = data => {
|
||||
manualSampleMeta.innerHTML = [
|
||||
`mode=${data.mode || 'unknown'}`,
|
||||
@@ -11509,6 +11652,12 @@
|
||||
if (mcpCompletionRefresh) {
|
||||
mcpCompletionRefresh.addEventListener('click', loadMcpCompletion);
|
||||
}
|
||||
if (mcpActivationEvidenceRefresh) {
|
||||
mcpActivationEvidenceRefresh.addEventListener('click', loadMcpActivationEvidence);
|
||||
}
|
||||
if (mcpActivationEvidenceReview) {
|
||||
mcpActivationEvidenceReview.addEventListener('click', reviewMcpActivationEvidence);
|
||||
}
|
||||
if (manualSampleRefresh) {
|
||||
manualSampleRefresh.addEventListener('click', loadManualSample);
|
||||
}
|
||||
@@ -11763,6 +11912,7 @@
|
||||
loadMcpActivation();
|
||||
loadMcpFetchGate();
|
||||
loadMcpCompletion();
|
||||
loadMcpActivationEvidence();
|
||||
loadManualSample();
|
||||
loadSampleAcceptance();
|
||||
loadSampleReview();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user