/* 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'); 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 `