/* Ai-DOP 第三方对接模拟器 · 本地控制台脚本 * 安全约束:不使用 localStorage/sessionStorage 保存任何认证信息;Secret 只在服务端。 */ 'use strict'; const $ = (id) => document.getElementById(id); async function api(path, options = {}) { const resp = await fetch(path, { headers: { 'Content-Type': 'application/json' }, ...options, }); const text = await resp.text(); let data; try { data = text ? JSON.parse(text) : {}; } catch { data = text; } if (!resp.ok) { const detail = (data && data.detail) ? data.detail : data; throw new Error(typeof detail === 'string' ? detail : JSON.stringify(detail, null, 2)); } return data; } function show(el, data) { $(el).textContent = typeof data === 'string' ? data : JSON.stringify(data, null, 2); } function setError(el, ex) { $(el).textContent = '❌ ' + (ex && ex.message ? ex.message : String(ex)); } /* ---------------- Tabs ---------------- */ document.querySelectorAll('nav button').forEach((btn) => { btn.addEventListener('click', () => { document.querySelectorAll('nav button').forEach((b) => b.classList.remove('active')); document.querySelectorAll('.tab').forEach((t) => t.classList.remove('active')); btn.classList.add('active'); $(btn.dataset.tab).classList.add('active'); }); }); /* ---------------- 全局状态 ---------------- */ let CATALOG = null; let SCENARIOS = null; function objects() { return (CATALOG && CATALOG.objects) || []; } function findObj(code) { return objects().find((o) => o.objectCode === code); } function channelSpec(obj, channel) { if (!obj) return null; return { DB_SYNC: obj.dbSync, API_PULL: obj.apiPull, API_INBOUND: obj.apiInbound }[channel] || null; } function fillObjectSelect(sel, filterFn) { sel.innerHTML = ''; objects().filter(filterFn || (() => true)).forEach((o) => { const opt = document.createElement('option'); opt.value = o.objectCode; opt.textContent = `${o.objectCode} · ${o.businessName}`; sel.appendChild(opt); }); } /* ---------------- 环境状态 ---------------- */ async function refreshStatus() { try { const s = await api('/api/status'); const miss = []; if (!s.inbound.accessKeyLoaded) miss.push('AIDOP_SIM_INBOUND_ACCESS_KEY'); if (!s.inbound.secretLoaded) miss.push('AIDOP_SIM_INBOUND_SECRET'); if (!s.mysql.passwordLoaded) miss.push('AIDOP_SIM_MYSQL_PASSWORD'); const banner = $('env-banner'); if (miss.length) { banner.className = 'banner warn'; banner.textContent = `⚠ 缺少环境变量:${miss.join('、')}(Secret 只从环境变量读取,页面不提供输入)`; } else { banner.className = 'banner ok'; banner.textContent = `✓ 凭据已加载(AccessKey ${s.inbound.accessKeyMasked})`; } $('mock-state').textContent = s.mockApi.running ? `运行中 :${s.mockApi.port}(${s.mockApi.authMode})` : '未运行'; $('mock-state').className = 'state ' + (s.mockApi.running ? 'ok' : 'warn'); // 异常注入状态同步回 Mock 页下拉框(fault 页预置 N2 场景后这里应显示对应状态码) const force = s.mockApi.forceStatus; $('mock-force').value = force ? String(force) : ''; show('security-output', s); const counts = s.objectCounts; $('run-object-info').dataset.counts = JSON.stringify(counts); } catch (ex) { $('env-banner').className = 'banner err'; $('env-banner').textContent = '❌ 控制服务不可用:' + ex.message; } } /* ---------------- 对象测试页 ---------------- */ function currentRunId() { return $('run-id').value.trim() || null; } function refreshChannelState() { const obj = findObj($('run-object').value); const channel = $('run-channel').value; const spec = channelSpec(obj, channel); const el = $('run-channel-state'); if (!obj) { el.textContent = ''; return; } if (!spec || !spec.supported) { el.className = 'state err'; el.textContent = `代码层 UNSUPPORTED:${(spec && spec.reason) || '该通道不支持'}`; $('btn-exec').disabled = true; } else { el.className = 'state ok'; const extra = spec.entityCode ? `(entityCode=${spec.entityCode},契约 ${spec.contractVersion || 'v1'})` : spec.table ? `(源表 ${spec.table})` : spec.path ? `(路径 ${spec.path})` : ''; el.textContent = `代码层 SUPPORTED${extra};配置层需预检实测`; $('btn-exec').disabled = false; } } async function loadSample() { try { const obj = $('run-object').value; const runId = currentRunId(); const qs = runId ? `?runId=${encodeURIComponent(runId)}` : ''; const data = await api(`/api/sample/${obj}${qs}`); $('run-id').value = data.runId; const channel = $('run-channel').value; let rows = []; let missing = []; if (channel === 'DB_SYNC' && data.dbSync) rows = data.dbSync; else if (channel === 'API_PULL' && data.apiPull) rows = data.apiPull; else if (channel === 'API_INBOUND' && data.apiInbound) { rows = data.apiInbound.rows; missing = data.apiInbound.missingRequired || []; } $('run-rows').value = JSON.stringify(rows, null, 2); $('run-missing').textContent = missing.length ? `⚠ 样例缺少契约必填字段:${missing.join('、')}(推数将被部分拒绝,请补全样例)` : ''; } catch (ex) { alert('加载样例失败:' + ex.message); } } function parseRows() { const text = $('run-rows').value.trim(); if (!text) return null; const rows = JSON.parse(text); if (!Array.isArray(rows)) throw new Error('样例必须是 JSON 数组'); return rows; } async function execRun() { const obj = findObj($('run-object').value); const channel = $('run-channel').value; const spec = channelSpec(obj, channel); if (!spec || !spec.supported) { alert('该对象不支持此通道(代码层 UNSUPPORTED)'); return; } try { const rows = parseRows(); const runId = currentRunId(); let result; if (channel === 'DB_SYNC') { await api('/api/db/ensure', { method: 'POST', body: JSON.stringify({ objectCode: obj.objectCode, runId, rows }) }); result = await api('/api/db/load', { method: 'POST', body: JSON.stringify({ objectCode: obj.objectCode, runId, rows }) }); result.hint = '模拟源库已装载;请在 Ai-DOP 配置页登记 SIM_MODE3_DB 来源后,由模块同步入口触发 MdpDbPullExecutor 真实拉取'; } else if (channel === 'API_PULL') { result = await api('/api/mock-api/control', { method: 'POST', body: JSON.stringify({ override_path: spec.path, override_rows: rows || [] }), }); result = { ok: true, overridePath: spec.path, rowCount: (rows || []).length, hint: '适配样例已挂载到 Mock API;请确认 Mock 已启动、Ai-DOP 已登记 SIM_MODE3_API 来源,由 MdpApiPullExecutor 真实拉取', layers: { code: 'SUPPORTED', config: 'READY(模拟端)', data: 'NOT_RUN' }, }; } else { result = await api('/api/inbound/exec', { method: 'POST', body: JSON.stringify({ objectCode: obj.objectCode, operation: 'push', case: 'happy', runId, rows }), }); } renderRunResult(result); } catch (ex) { $('run-layers').innerHTML = ''; setError('run-output', ex); } } function renderRunResult(result) { const layers = result.layers || {}; $('run-layers').innerHTML = [ `代码层:${layers.code || '—'}`, `配置层:${layers.config || '—'}`, `数据层:${layers.data || '—'}`, ].join(' '); show('run-output', result); show('run-sql', (result.followupSql || []).join('\n') || '—'); } async function precheckRun() { const obj = findObj($('run-object').value); const channel = $('run-channel').value; try { const base = await api('/api/precheck'); let out = { precheck: base }; if (channel === 'API_INBOUND' && (obj.apiInbound || {}).supported) { out.entityPrecheck = await api(`/api/precheck/inbound/${obj.apiInbound.entityCode}`); } out.aidopConfigSuggestion = await api(`/api/db/aidop-config/${obj.objectCode}`); show('run-output', out); const cfg = (out.entityPrecheck || {}).configState; $('run-layers').innerHTML = [ `代码层:SUPPORTED`, `配置层:${cfg || (base.aidop.reachable ? '待人工核对来源/实体登记' : 'SOURCE_UNREACHABLE')}`, `数据层:NOT_RUN`, ].join(' '); } catch (ex) { setError('run-output', ex); } } /* ---------------- 场景页 ---------------- */ function renderScenarioGraph() { const sc = (SCENARIOS.scenarios || []).find((s) => s.scenarioId === $('scenario-select').value); if (!sc) return; $('scenario-desc').textContent = sc.description; $('scenario-graph').innerHTML = sc.nodes.map((n) => { const obj = findObj(n.objectCode); const spec = channelSpec(obj, n.channel); const ok = spec && spec.supported; return `
${n.objectCode}
${n.channel}
${ok ? (n.relationKey || '') : '该通道不支持'}
`; }).join('→'); } async function runScenario() { try { const result = await api('/api/scenarios/run', { method: 'POST', body: JSON.stringify({ scenarioId: $('scenario-select').value, runId: currentRunId() }), }); show('scenario-output', result); } catch (ex) { setError('scenario-output', ex); } } /* ---------------- Mock API 页 ---------------- */ async function mockControl(payload) { try { show('mock-audit', await api('/api/mock-api/control', { method: 'POST', body: JSON.stringify(payload) })); refreshStatus(); } catch (ex) { setError('mock-audit', ex); } } /* ---------------- DB 页 ---------------- */ async function dbAction(path, payload, out) { try { show(out, await api(path, { method: 'POST', body: JSON.stringify(payload) })); } catch (ex) { setError(out, ex); } } /* ---------------- Inbound 页 ---------------- */ async function precheckAll() { try { show('inbound-precheck', await api('/api/precheck/inbound')); } catch (ex) { setError('inbound-precheck', ex); } } async function inboundExec() { const body = { entityCode: $('inbound-entity').value, operation: $('inbound-op').value, case: $('inbound-case').value, syncBatchId: $('inbound-batch').value.trim() || null, snapshotId: $('inbound-snapshot').value.trim() || null, bizDate: $('inbound-date').value.trim() || null, runId: currentRunId(), }; try { show('inbound-output', await api('/api/inbound/exec', { method: 'POST', body: JSON.stringify(body) })); } catch (ex) { setError('inbound-output', ex); } } /* ---------------- 异常测试(第二阶段 N1-N4) ---------------- */ let FAULT_CATALOG = null; let LAST_FAULT_REPORT_ID = null; const FAULT_GROUP_TITLES = { DB_SYNC: '方式甲 DB_SYNC(数据库同步)· 10 场景', API_PULL: '方式乙 API_PULL(接口拉取)· 16 场景', API_INBOUND: '方式丙 API_INBOUND(接口推送)· 17 场景', }; function faultScenarios() { return (FAULT_CATALOG && FAULT_CATALOG.scenarios) || []; } function esc(s) { return String(s).replace(/&/g, '&').replace(//g, '>'); } async function refreshFaultCatalog() { try { FAULT_CATALOG = await api('/api/fault/catalog'); renderFaultGroups(); fillFaultReportScenario(); } catch (ex) { setError('fault-groups', ex); } } function renderFaultGroups() { const container = $('fault-groups'); const byChannel = { DB_SYNC: [], API_PULL: [], API_INBOUND: [] }; faultScenarios().forEach((s) => (byChannel[s.channel] || []).push(s)); container.innerHTML = ['DB_SYNC', 'API_PULL', 'API_INBOUND'].map((ch) => { const items = (byChannel[ch] || []).map((s) => `
${s.id} ${esc(s.title)} ${s.kind === 'auto' ? 'auto · 一键预置' : 'guide · 人工操作'}
模拟方法:${esc(s.sim)}
预期观察:${esc(s.expect)}
恢复:${esc(s.recover)}
`).join(''); return `

${FAULT_GROUP_TITLES[ch]}

${items}`; }).join(''); container.querySelectorAll('.fault-item').forEach((item) => { const id = item.dataset.id; item.querySelector('button[data-act="apply"]').addEventListener('click', async () => { const out = item.querySelector('.fault-result'); out.textContent = '预置中…'; try { const r = await api('/api/fault/apply', { method: 'POST', body: JSON.stringify({ scenarioId: id }) }); const note = r.note || (r.result ? JSON.stringify(r.result).slice(0, 120) : ''); out.textContent = '✓ ' + (r.applied ? '已预置' : '请按指引操作') + (note ? ' · ' + note : ''); out.className = 'fault-result ok'; } catch (ex) { out.textContent = '❌ ' + ex.message; out.className = 'fault-result err'; } }); item.querySelector('button[data-act="recover"]').addEventListener('click', async () => { const out = item.querySelector('.fault-result'); const scenario = faultScenarios().find((s) => s.id === id); out.textContent = '恢复中…'; try { const r = await api('/api/fault/recover', { method: 'POST', body: JSON.stringify({ channel: scenario ? scenario.channel : undefined }), }); out.textContent = '✓ 已恢复:' + (r.notes || []).join(';'); out.className = 'fault-result ok'; } catch (ex) { out.textContent = '❌ ' + ex.message; out.className = 'fault-result err'; } }); }); } function fillFaultReportScenario() { const sel = $('fault-report-scenario'); sel.innerHTML = ''; faultScenarios().forEach((s) => { const opt = document.createElement('option'); opt.value = s.id; opt.textContent = `${s.id} · ${s.title}`; sel.appendChild(opt); }); syncFaultReportPrefill(); } function syncFaultReportPrefill() { const s = faultScenarios().find((x) => x.id === $('fault-report-scenario').value); if (!s) return; $('fault-report-sim').value = s.sim; $('fault-report-expect').value = s.expect; } async function refreshFaultReportList() { try { show('fault-report-list', await api('/api/fault-report/list')); } catch (ex) { setError('fault-report-list', ex); } } async function saveFaultReport() { const btn = $('btn-fault-report-save'); if (btn) btn.disabled = true; // 防重复点击(同场景二次保存也会被后端原位更新) const fields = { scenarioId: $('fault-report-scenario').value, simBehavior: $('fault-report-sim').value.trim(), expected: $('fault-report-expect').value.trim(), actualResponse: $('fault-report-actual').value.trim(), aidopFlowStatus: $('fault-report-flow').value.trim(), targetChanged: $('fault-report-target').value.trim(), pass: $('fault-report-pass').value, note: $('fault-report-note').value.trim(), }; try { const r = await api('/api/fault-report/save', { method: 'POST', body: JSON.stringify(fields) }); LAST_FAULT_REPORT_ID = r.report.id; await refreshFaultReportList(); alert(r.report.replaced ? `已更新场景 ${r.report.scenarioId} 的报告(同场景覆盖,不产生重复行)` : `已保存异常报告 ${r.report.id}(保存前已脱敏)`); } catch (ex) { setError('fault-report-list', ex); } finally { if (btn) btn.disabled = false; } } function exportFaultReport(id) { if (!id) { alert('请先「保存报告」,再导出 Markdown'); return; } const a = document.createElement('a'); a.href = '/api/fault-report/export/' + encodeURIComponent(id); a.download = `aidop-sim-fault-report-${id}.md`; document.body.appendChild(a); a.click(); a.remove(); } async function recoverAllFaults() { try { const r = await api('/api/fault/recover', { method: 'POST', body: JSON.stringify({}) }); alert('已恢复:\n' + (r.notes || []).join('\n')); refreshStatus(); } catch (ex) { alert('恢复失败:' + ex.message); } } /* ---------------- 全链路报告(F5) ---------------- */ let LAST_REPORT_ID = null; function reportFields() { return { runId: $('report-runid').value.trim(), channel: $('report-channel').value, environment: $('report-env').value.trim(), objectCode: $('report-object').value.trim(), simPrep: $('report-simprep').value.trim(), manualSteps: $('report-steps').value.trim(), simEvidence: $('report-simevidence').value.trim(), aidopConn: $('report-conn').value.trim(), flowIds: $('report-flowids').value.trim(), stagingCheck: $('report-staging').value.trim(), targetResult: $('report-target').value.trim(), conclusion: $('report-conclusion').value.trim(), gaps: $('report-gaps').value.trim(), cleanup: $('report-cleanup').value.trim(), }; } async function refreshReportList() { try { const data = await api('/api/report/list'); show('report-list-output', data); return data.reports || []; } catch (ex) { setError('report-list-output', ex); return []; } } async function saveReport() { const fields = reportFields(); if (!fields.runId || !fields.channel) { alert('请先填写运行编号 runId'); return; } try { const r = await api('/api/report/save', { method: 'POST', body: JSON.stringify(fields) }); LAST_REPORT_ID = r.report.id; $('report-runid').value = r.report.runId || fields.runId; await refreshReportList(); alert(`已保存报告 ${r.report.id}(保存前已脱敏;不保存密码、Secret、JWT、连接串)`); } catch (ex) { setError('report-list-output', ex); } } function exportReport(id) { if (!id) { alert('请先「保存报告」,再导出 Markdown'); return; } const a = document.createElement('a'); a.href = '/api/report/export/' + encodeURIComponent(id); a.download = `aidop-sim-report-${id}.md`; document.body.appendChild(a); a.click(); a.remove(); } async function importReportEvidence() { const channel = $('report-channel').value; try { const data = await api('/api/report/evidence/' + channel); const entries = data.entries || []; const lines = entries.length ? entries.map((e) => `[${e.time}] ${e.objectCode} · ${e.channel} · runId=${e.runId}\n` + JSON.stringify({ request: e.request, response: e.response, layers: e.layers }, null, 2)) .join('\n\n') : `(${channel} 暂无运行记录;先在对应页签执行一次操作)`; $('report-simevidence').value = lines; } catch (ex) { alert('导入失败:' + ex.message); } } /* ---------------- 初始化 ---------------- */ async function init() { CATALOG = await api('/api/catalog'); SCENARIOS = await api('/api/scenarios'); fillObjectSelect($('run-object')); fillObjectSelect($('db-object'), (o) => (o.dbSync || {}).supported); const inboundSel = $('inbound-entity'); inboundSel.innerHTML = ''; const codes = [...new Set(objects() .filter((o) => (o.apiInbound || {}).supported) .map((o) => o.apiInbound.entityCode))].sort(); codes.forEach((c) => { const opt = document.createElement('option'); opt.value = c; opt.textContent = c; inboundSel.appendChild(opt); }); const scSel = $('scenario-select'); (SCENARIOS.scenarios || []).forEach((s) => { const opt = document.createElement('option'); opt.value = s.scenarioId; opt.textContent = `${s.name}(${s.scenarioId})`; scSel.appendChild(opt); }); $('run-object').addEventListener('change', refreshChannelState); $('run-channel').addEventListener('change', refreshChannelState); $('btn-load-sample').addEventListener('click', loadSample); $('btn-exec').addEventListener('click', execRun); $('btn-precheck').addEventListener('click', precheckRun); $('scenario-select').addEventListener('change', renderScenarioGraph); $('btn-scenario-run').addEventListener('click', runScenario); $('btn-mock-start').addEventListener('click', async () => { try { show('mock-audit', await api('/api/mock-api/start', { method: 'POST' })); refreshStatus(); } catch (ex) { setError('mock-audit', ex); } }); $('btn-mock-stop').addEventListener('click', async () => { try { show('mock-audit', await api('/api/mock-api/stop', { method: 'POST' })); refreshStatus(); } catch (ex) { setError('mock-audit', ex); } }); $('btn-mock-apply').addEventListener('click', () => mockControl({ auth_mode: $('mock-auth').value, delay_seconds: parseFloat($('mock-delay').value || '0'), force_status: $('mock-force').value ? parseInt($('mock-force').value, 10) : null, response_path: $('mock-resp-path').value.trim() || 'data.list', })); $('btn-mock-clear-overrides').addEventListener('click', () => mockControl({ clear_overrides: true })); $('btn-mock-audit').addEventListener('click', async () => { try { show('mock-audit', await api('/api/mock-api/audit')); } catch (ex) { setError('mock-audit', ex); } }); const dbObj = () => $('db-object').value; $('btn-db-ensure').addEventListener('click', () => dbAction('/api/db/ensure', { objectCode: dbObj(), runId: currentRunId() }, 'db-output')); $('btn-db-load').addEventListener('click', () => dbAction('/api/db/load', { objectCode: dbObj(), runId: currentRunId() }, 'db-output')); $('btn-db-reset').addEventListener('click', () => dbAction('/api/db/reset', { objectCode: dbObj() }, 'db-output')); $('btn-db-stats').addEventListener('click', async () => { try { show('db-output', await api(`/api/db/stats/${dbObj()}`)); } catch (ex) { setError('db-output', ex); } }); $('btn-db-config').addEventListener('click', async () => { try { show('db-config-output', await api(`/api/db/aidop-config/${dbObj()}`)); } catch (ex) { setError('db-config-output', ex); } }); $('btn-precheck-all').addEventListener('click', precheckAll); $('btn-inbound-exec').addEventListener('click', inboundExec); $('btn-runs-refresh').addEventListener('click', async () => { try { show('runs-output', await api('/api/runs')); } catch (ex) { setError('runs-output', ex); } }); $('btn-report-save').addEventListener('click', saveReport); $('btn-report-export').addEventListener('click', () => exportReport(LAST_REPORT_ID)); $('btn-report-refresh').addEventListener('click', refreshReportList); $('btn-report-evidence').addEventListener('click', importReportEvidence); refreshReportList(); $('btn-fault-refresh').addEventListener('click', refreshFaultCatalog); $('btn-fault-recover-all').addEventListener('click', recoverAllFaults); $('fault-report-scenario').addEventListener('change', syncFaultReportPrefill); $('btn-fault-report-save').addEventListener('click', saveFaultReport); $('btn-fault-report-export').addEventListener('click', () => exportFaultReport(LAST_FAULT_REPORT_ID)); $('btn-fault-report-refresh').addEventListener('click', refreshFaultReportList); refreshFaultCatalog(); refreshChannelState(); renderScenarioGraph(); await refreshStatus(); setInterval(refreshStatus, 15000); } init().catch((ex) => { $('env-banner').className = 'banner err'; $('env-banner').textContent = '❌ 初始化失败:' + ex.message; });