[V10.358] add market intel MCP activation evidence review
All checks were successful
CD Pipeline / deploy (push) Successful in 1m7s

This commit is contained in:
OoO
2026-05-21 11:31:28 +08:00
parent 8bffd0307d
commit f4ec50cd30
9 changed files with 679 additions and 113 deletions

View File

@@ -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-upAPI/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 gateAPI/UI 不讀 approval/Telegram token、不呼叫 LLM、不產報表、不派送 Telegram、不開 DB、不寫檔、不執行 CLI、不寫 catalog record、不 commit、不更新 review_state、不掛 scheduler。

View File

@@ -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 # 用於模板顯示

View File

@@ -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、不抓外站、不掛 schedulerpayload 只允許 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 驗證。

View File

@@ -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

View 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

View File

@@ -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"

View File

@@ -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