app.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  1. /* Ai-DOP 第三方对接模拟器 · 本地控制台脚本
  2. * 安全约束:不使用 localStorage/sessionStorage 保存任何认证信息;Secret 只在服务端。
  3. */
  4. 'use strict';
  5. const $ = (id) => document.getElementById(id);
  6. async function api(path, options = {}) {
  7. const resp = await fetch(path, {
  8. headers: { 'Content-Type': 'application/json' },
  9. ...options,
  10. });
  11. const text = await resp.text();
  12. let data;
  13. try { data = text ? JSON.parse(text) : {}; } catch { data = text; }
  14. if (!resp.ok) {
  15. const detail = (data && data.detail) ? data.detail : data;
  16. throw new Error(typeof detail === 'string' ? detail : JSON.stringify(detail, null, 2));
  17. }
  18. return data;
  19. }
  20. function show(el, data) {
  21. $(el).textContent = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
  22. }
  23. function setError(el, ex) {
  24. $(el).textContent = '❌ ' + (ex && ex.message ? ex.message : String(ex));
  25. }
  26. /* ---------------- Tabs ---------------- */
  27. document.querySelectorAll('nav button').forEach((btn) => {
  28. btn.addEventListener('click', () => {
  29. document.querySelectorAll('nav button').forEach((b) => b.classList.remove('active'));
  30. document.querySelectorAll('.tab').forEach((t) => t.classList.remove('active'));
  31. btn.classList.add('active');
  32. $(btn.dataset.tab).classList.add('active');
  33. });
  34. });
  35. /* ---------------- 全局状态 ---------------- */
  36. let CATALOG = null;
  37. let SCENARIOS = null;
  38. function objects() { return (CATALOG && CATALOG.objects) || []; }
  39. function findObj(code) { return objects().find((o) => o.objectCode === code); }
  40. function channelSpec(obj, channel) {
  41. if (!obj) return null;
  42. return { DB_SYNC: obj.dbSync, API_PULL: obj.apiPull, API_INBOUND: obj.apiInbound }[channel] || null;
  43. }
  44. function fillObjectSelect(sel, filterFn) {
  45. sel.innerHTML = '';
  46. objects().filter(filterFn || (() => true)).forEach((o) => {
  47. const opt = document.createElement('option');
  48. opt.value = o.objectCode;
  49. opt.textContent = `${o.objectCode} · ${o.businessName}`;
  50. sel.appendChild(opt);
  51. });
  52. }
  53. /* ---------------- 环境状态 ---------------- */
  54. async function refreshStatus() {
  55. try {
  56. const s = await api('/api/status');
  57. const miss = [];
  58. if (!s.inbound.accessKeyLoaded) miss.push('AIDOP_SIM_INBOUND_ACCESS_KEY');
  59. if (!s.inbound.secretLoaded) miss.push('AIDOP_SIM_INBOUND_SECRET');
  60. if (!s.mysql.passwordLoaded) miss.push('AIDOP_SIM_MYSQL_PASSWORD');
  61. const banner = $('env-banner');
  62. if (miss.length) {
  63. banner.className = 'banner warn';
  64. banner.textContent = `⚠ 缺少环境变量:${miss.join('、')}(Secret 只从环境变量读取,页面不提供输入)`;
  65. } else {
  66. banner.className = 'banner ok';
  67. banner.textContent = `✓ 凭据已加载(AccessKey ${s.inbound.accessKeyMasked})`;
  68. }
  69. $('mock-state').textContent = s.mockApi.running
  70. ? `运行中 :${s.mockApi.port}(${s.mockApi.authMode})` : '未运行';
  71. $('mock-state').className = 'state ' + (s.mockApi.running ? 'ok' : 'warn');
  72. // 异常注入状态同步回 Mock 页下拉框(fault 页预置 N2 场景后这里应显示对应状态码)
  73. const force = s.mockApi.forceStatus;
  74. $('mock-force').value = force ? String(force) : '';
  75. show('security-output', s);
  76. const counts = s.objectCounts;
  77. $('run-object-info').dataset.counts = JSON.stringify(counts);
  78. } catch (ex) {
  79. $('env-banner').className = 'banner err';
  80. $('env-banner').textContent = '❌ 控制服务不可用:' + ex.message;
  81. }
  82. }
  83. /* ---------------- 对象测试页 ---------------- */
  84. function currentRunId() {
  85. return $('run-id').value.trim() || null;
  86. }
  87. function refreshChannelState() {
  88. const obj = findObj($('run-object').value);
  89. const channel = $('run-channel').value;
  90. const spec = channelSpec(obj, channel);
  91. const el = $('run-channel-state');
  92. if (!obj) { el.textContent = ''; return; }
  93. if (!spec || !spec.supported) {
  94. el.className = 'state err';
  95. el.textContent = `代码层 UNSUPPORTED:${(spec && spec.reason) || '该通道不支持'}`;
  96. $('btn-exec').disabled = true;
  97. } else {
  98. el.className = 'state ok';
  99. const extra = spec.entityCode ? `(entityCode=${spec.entityCode},契约 ${spec.contractVersion || 'v1'})`
  100. : spec.table ? `(源表 ${spec.table})` : spec.path ? `(路径 ${spec.path})` : '';
  101. el.textContent = `代码层 SUPPORTED${extra};配置层需预检实测`;
  102. $('btn-exec').disabled = false;
  103. }
  104. }
  105. async function loadSample() {
  106. try {
  107. const obj = $('run-object').value;
  108. const runId = currentRunId();
  109. const qs = runId ? `?runId=${encodeURIComponent(runId)}` : '';
  110. const data = await api(`/api/sample/${obj}${qs}`);
  111. $('run-id').value = data.runId;
  112. const channel = $('run-channel').value;
  113. let rows = [];
  114. let missing = [];
  115. if (channel === 'DB_SYNC' && data.dbSync) rows = data.dbSync;
  116. else if (channel === 'API_PULL' && data.apiPull) rows = data.apiPull;
  117. else if (channel === 'API_INBOUND' && data.apiInbound) {
  118. rows = data.apiInbound.rows;
  119. missing = data.apiInbound.missingRequired || [];
  120. }
  121. $('run-rows').value = JSON.stringify(rows, null, 2);
  122. $('run-missing').textContent = missing.length
  123. ? `⚠ 样例缺少契约必填字段:${missing.join('、')}(推数将被部分拒绝,请补全样例)` : '';
  124. } catch (ex) { alert('加载样例失败:' + ex.message); }
  125. }
  126. function parseRows() {
  127. const text = $('run-rows').value.trim();
  128. if (!text) return null;
  129. const rows = JSON.parse(text);
  130. if (!Array.isArray(rows)) throw new Error('样例必须是 JSON 数组');
  131. return rows;
  132. }
  133. async function execRun() {
  134. const obj = findObj($('run-object').value);
  135. const channel = $('run-channel').value;
  136. const spec = channelSpec(obj, channel);
  137. if (!spec || !spec.supported) { alert('该对象不支持此通道(代码层 UNSUPPORTED)'); return; }
  138. try {
  139. const rows = parseRows();
  140. const runId = currentRunId();
  141. let result;
  142. if (channel === 'DB_SYNC') {
  143. await api('/api/db/ensure', { method: 'POST', body: JSON.stringify({ objectCode: obj.objectCode, runId, rows }) });
  144. result = await api('/api/db/load', { method: 'POST', body: JSON.stringify({ objectCode: obj.objectCode, runId, rows }) });
  145. result.hint = '模拟源库已装载;请在 Ai-DOP 配置页登记 SIM_MODE3_DB 来源后,由模块同步入口触发 MdpDbPullExecutor 真实拉取';
  146. } else if (channel === 'API_PULL') {
  147. result = await api('/api/mock-api/control', {
  148. method: 'POST',
  149. body: JSON.stringify({ override_path: spec.path, override_rows: rows || [] }),
  150. });
  151. result = {
  152. ok: true, overridePath: spec.path,
  153. rowCount: (rows || []).length,
  154. hint: '适配样例已挂载到 Mock API;请确认 Mock 已启动、Ai-DOP 已登记 SIM_MODE3_API 来源,由 MdpApiPullExecutor 真实拉取',
  155. layers: { code: 'SUPPORTED', config: 'READY(模拟端)', data: 'NOT_RUN' },
  156. };
  157. } else {
  158. result = await api('/api/inbound/exec', {
  159. method: 'POST',
  160. body: JSON.stringify({ objectCode: obj.objectCode, operation: 'push', case: 'happy', runId, rows }),
  161. });
  162. }
  163. renderRunResult(result);
  164. } catch (ex) {
  165. $('run-layers').innerHTML = '';
  166. setError('run-output', ex);
  167. }
  168. }
  169. function renderRunResult(result) {
  170. const layers = result.layers || {};
  171. $('run-layers').innerHTML = [
  172. `<span class="chip">代码层:${layers.code || '—'}</span>`,
  173. `<span class="chip">配置层:${layers.config || '—'}</span>`,
  174. `<span class="chip">数据层:${layers.data || '—'}</span>`,
  175. ].join(' ');
  176. show('run-output', result);
  177. show('run-sql', (result.followupSql || []).join('\n') || '—');
  178. }
  179. async function precheckRun() {
  180. const obj = findObj($('run-object').value);
  181. const channel = $('run-channel').value;
  182. try {
  183. const base = await api('/api/precheck');
  184. let out = { precheck: base };
  185. if (channel === 'API_INBOUND' && (obj.apiInbound || {}).supported) {
  186. out.entityPrecheck = await api(`/api/precheck/inbound/${obj.apiInbound.entityCode}`);
  187. }
  188. out.aidopConfigSuggestion = await api(`/api/db/aidop-config/${obj.objectCode}`);
  189. show('run-output', out);
  190. const cfg = (out.entityPrecheck || {}).configState;
  191. $('run-layers').innerHTML = [
  192. `<span class="chip">代码层:SUPPORTED</span>`,
  193. `<span class="chip">配置层:${cfg || (base.aidop.reachable ? '待人工核对来源/实体登记' : 'SOURCE_UNREACHABLE')}</span>`,
  194. `<span class="chip">数据层:NOT_RUN</span>`,
  195. ].join(' ');
  196. } catch (ex) { setError('run-output', ex); }
  197. }
  198. /* ---------------- 场景页 ---------------- */
  199. function renderScenarioGraph() {
  200. const sc = (SCENARIOS.scenarios || []).find((s) => s.scenarioId === $('scenario-select').value);
  201. if (!sc) return;
  202. $('scenario-desc').textContent = sc.description;
  203. $('scenario-graph').innerHTML = sc.nodes.map((n) => {
  204. const obj = findObj(n.objectCode);
  205. const spec = channelSpec(obj, n.channel);
  206. const ok = spec && spec.supported;
  207. return `<div class="node ${ok ? 'ok' : 'no'}">
  208. <b>${n.objectCode}</b><br/>
  209. <span>${n.channel}</span><br/>
  210. <small>${ok ? (n.relationKey || '') : '该通道不支持'}</small>
  211. </div>`;
  212. }).join('<span class="arrow">→</span>');
  213. }
  214. async function runScenario() {
  215. try {
  216. const result = await api('/api/scenarios/run', {
  217. method: 'POST',
  218. body: JSON.stringify({ scenarioId: $('scenario-select').value, runId: currentRunId() }),
  219. });
  220. show('scenario-output', result);
  221. } catch (ex) { setError('scenario-output', ex); }
  222. }
  223. /* ---------------- Mock API 页 ---------------- */
  224. async function mockControl(payload) {
  225. try { show('mock-audit', await api('/api/mock-api/control', { method: 'POST', body: JSON.stringify(payload) })); refreshStatus(); }
  226. catch (ex) { setError('mock-audit', ex); }
  227. }
  228. /* ---------------- DB 页 ---------------- */
  229. async function dbAction(path, payload, out) {
  230. try { show(out, await api(path, { method: 'POST', body: JSON.stringify(payload) })); }
  231. catch (ex) { setError(out, ex); }
  232. }
  233. /* ---------------- Inbound 页 ---------------- */
  234. async function precheckAll() {
  235. try { show('inbound-precheck', await api('/api/precheck/inbound')); }
  236. catch (ex) { setError('inbound-precheck', ex); }
  237. }
  238. async function inboundExec() {
  239. const body = {
  240. entityCode: $('inbound-entity').value,
  241. operation: $('inbound-op').value,
  242. case: $('inbound-case').value,
  243. syncBatchId: $('inbound-batch').value.trim() || null,
  244. snapshotId: $('inbound-snapshot').value.trim() || null,
  245. bizDate: $('inbound-date').value.trim() || null,
  246. runId: currentRunId(),
  247. };
  248. try { show('inbound-output', await api('/api/inbound/exec', { method: 'POST', body: JSON.stringify(body) })); }
  249. catch (ex) { setError('inbound-output', ex); }
  250. }
  251. /* ---------------- 异常测试(第二阶段 N1-N4) ---------------- */
  252. let FAULT_CATALOG = null;
  253. let LAST_FAULT_REPORT_ID = null;
  254. const FAULT_GROUP_TITLES = {
  255. DB_SYNC: '方式甲 DB_SYNC(数据库同步)· 10 场景',
  256. API_PULL: '方式乙 API_PULL(接口拉取)· 16 场景',
  257. API_INBOUND: '方式丙 API_INBOUND(接口推送)· 17 场景',
  258. };
  259. function faultScenarios() { return (FAULT_CATALOG && FAULT_CATALOG.scenarios) || []; }
  260. function esc(s) {
  261. return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
  262. }
  263. async function refreshFaultCatalog() {
  264. try {
  265. FAULT_CATALOG = await api('/api/fault/catalog');
  266. renderFaultGroups();
  267. fillFaultReportScenario();
  268. } catch (ex) { setError('fault-groups', ex); }
  269. }
  270. function renderFaultGroups() {
  271. const container = $('fault-groups');
  272. const byChannel = { DB_SYNC: [], API_PULL: [], API_INBOUND: [] };
  273. faultScenarios().forEach((s) => (byChannel[s.channel] || []).push(s));
  274. container.innerHTML = ['DB_SYNC', 'API_PULL', 'API_INBOUND'].map((ch) => {
  275. const items = (byChannel[ch] || []).map((s) => `
  276. <div class="fault-item" data-id="${s.id}">
  277. <div class="fault-head">
  278. <b>${s.id}</b> ${esc(s.title)}
  279. <span class="chip ${s.kind === 'auto' ? 'ok' : ''}">${s.kind === 'auto' ? 'auto · 一键预置' : 'guide · 人工操作'}</span>
  280. </div>
  281. <div class="muted">模拟方法:${esc(s.sim)}</div>
  282. <div class="muted">预期观察:${esc(s.expect)}</div>
  283. <div class="muted">恢复:${esc(s.recover)}</div>
  284. <div class="row">
  285. <button data-act="apply">${s.kind === 'auto' ? '预置此异常' : '查看指引'}</button>
  286. <button data-act="recover">恢复</button>
  287. <span class="fault-result muted"></span>
  288. </div>
  289. </div>`).join('');
  290. return `<h3>${FAULT_GROUP_TITLES[ch]}</h3>${items}`;
  291. }).join('');
  292. container.querySelectorAll('.fault-item').forEach((item) => {
  293. const id = item.dataset.id;
  294. item.querySelector('button[data-act="apply"]').addEventListener('click', async () => {
  295. const out = item.querySelector('.fault-result');
  296. out.textContent = '预置中…';
  297. try {
  298. const r = await api('/api/fault/apply', { method: 'POST', body: JSON.stringify({ scenarioId: id }) });
  299. const note = r.note || (r.result ? JSON.stringify(r.result).slice(0, 120) : '');
  300. out.textContent = '✓ ' + (r.applied ? '已预置' : '请按指引操作') + (note ? ' · ' + note : '');
  301. out.className = 'fault-result ok';
  302. } catch (ex) {
  303. out.textContent = '❌ ' + ex.message;
  304. out.className = 'fault-result err';
  305. }
  306. });
  307. item.querySelector('button[data-act="recover"]').addEventListener('click', async () => {
  308. const out = item.querySelector('.fault-result');
  309. const scenario = faultScenarios().find((s) => s.id === id);
  310. out.textContent = '恢复中…';
  311. try {
  312. const r = await api('/api/fault/recover', {
  313. method: 'POST', body: JSON.stringify({ channel: scenario ? scenario.channel : undefined }),
  314. });
  315. out.textContent = '✓ 已恢复:' + (r.notes || []).join(';');
  316. out.className = 'fault-result ok';
  317. } catch (ex) {
  318. out.textContent = '❌ ' + ex.message;
  319. out.className = 'fault-result err';
  320. }
  321. });
  322. });
  323. }
  324. function fillFaultReportScenario() {
  325. const sel = $('fault-report-scenario');
  326. sel.innerHTML = '';
  327. faultScenarios().forEach((s) => {
  328. const opt = document.createElement('option');
  329. opt.value = s.id;
  330. opt.textContent = `${s.id} · ${s.title}`;
  331. sel.appendChild(opt);
  332. });
  333. syncFaultReportPrefill();
  334. }
  335. function syncFaultReportPrefill() {
  336. const s = faultScenarios().find((x) => x.id === $('fault-report-scenario').value);
  337. if (!s) return;
  338. $('fault-report-sim').value = s.sim;
  339. $('fault-report-expect').value = s.expect;
  340. }
  341. async function refreshFaultReportList() {
  342. try { show('fault-report-list', await api('/api/fault-report/list')); }
  343. catch (ex) { setError('fault-report-list', ex); }
  344. }
  345. async function saveFaultReport() {
  346. const btn = $('btn-fault-report-save');
  347. if (btn) btn.disabled = true; // 防重复点击(同场景二次保存也会被后端原位更新)
  348. const fields = {
  349. scenarioId: $('fault-report-scenario').value,
  350. simBehavior: $('fault-report-sim').value.trim(),
  351. expected: $('fault-report-expect').value.trim(),
  352. actualResponse: $('fault-report-actual').value.trim(),
  353. aidopFlowStatus: $('fault-report-flow').value.trim(),
  354. targetChanged: $('fault-report-target').value.trim(),
  355. pass: $('fault-report-pass').value,
  356. note: $('fault-report-note').value.trim(),
  357. };
  358. try {
  359. const r = await api('/api/fault-report/save', { method: 'POST', body: JSON.stringify(fields) });
  360. LAST_FAULT_REPORT_ID = r.report.id;
  361. await refreshFaultReportList();
  362. alert(r.report.replaced
  363. ? `已更新场景 ${r.report.scenarioId} 的报告(同场景覆盖,不产生重复行)`
  364. : `已保存异常报告 ${r.report.id}(保存前已脱敏)`);
  365. } catch (ex) { setError('fault-report-list', ex); }
  366. finally { if (btn) btn.disabled = false; }
  367. }
  368. function exportFaultReport(id) {
  369. if (!id) { alert('请先「保存报告」,再导出 Markdown'); return; }
  370. const a = document.createElement('a');
  371. a.href = '/api/fault-report/export/' + encodeURIComponent(id);
  372. a.download = `aidop-sim-fault-report-${id}.md`;
  373. document.body.appendChild(a);
  374. a.click();
  375. a.remove();
  376. }
  377. async function recoverAllFaults() {
  378. try {
  379. const r = await api('/api/fault/recover', { method: 'POST', body: JSON.stringify({}) });
  380. alert('已恢复:\n' + (r.notes || []).join('\n'));
  381. refreshStatus();
  382. } catch (ex) { alert('恢复失败:' + ex.message); }
  383. }
  384. /* ---------------- 全链路报告(F5) ---------------- */
  385. let LAST_REPORT_ID = null;
  386. function reportFields() {
  387. return {
  388. runId: $('report-runid').value.trim(),
  389. channel: $('report-channel').value,
  390. environment: $('report-env').value.trim(),
  391. objectCode: $('report-object').value.trim(),
  392. simPrep: $('report-simprep').value.trim(),
  393. manualSteps: $('report-steps').value.trim(),
  394. simEvidence: $('report-simevidence').value.trim(),
  395. aidopConn: $('report-conn').value.trim(),
  396. flowIds: $('report-flowids').value.trim(),
  397. stagingCheck: $('report-staging').value.trim(),
  398. targetResult: $('report-target').value.trim(),
  399. conclusion: $('report-conclusion').value.trim(),
  400. gaps: $('report-gaps').value.trim(),
  401. cleanup: $('report-cleanup').value.trim(),
  402. };
  403. }
  404. async function refreshReportList() {
  405. try {
  406. const data = await api('/api/report/list');
  407. show('report-list-output', data);
  408. return data.reports || [];
  409. } catch (ex) { setError('report-list-output', ex); return []; }
  410. }
  411. async function saveReport() {
  412. const fields = reportFields();
  413. if (!fields.runId || !fields.channel) { alert('请先填写运行编号 runId'); return; }
  414. try {
  415. const r = await api('/api/report/save', { method: 'POST', body: JSON.stringify(fields) });
  416. LAST_REPORT_ID = r.report.id;
  417. $('report-runid').value = r.report.runId || fields.runId;
  418. await refreshReportList();
  419. alert(`已保存报告 ${r.report.id}(保存前已脱敏;不保存密码、Secret、JWT、连接串)`);
  420. } catch (ex) { setError('report-list-output', ex); }
  421. }
  422. function exportReport(id) {
  423. if (!id) { alert('请先「保存报告」,再导出 Markdown'); return; }
  424. const a = document.createElement('a');
  425. a.href = '/api/report/export/' + encodeURIComponent(id);
  426. a.download = `aidop-sim-report-${id}.md`;
  427. document.body.appendChild(a);
  428. a.click();
  429. a.remove();
  430. }
  431. async function importReportEvidence() {
  432. const channel = $('report-channel').value;
  433. try {
  434. const data = await api('/api/report/evidence/' + channel);
  435. const entries = data.entries || [];
  436. const lines = entries.length
  437. ? entries.map((e) => `[${e.time}] ${e.objectCode} · ${e.channel} · runId=${e.runId}\n`
  438. + JSON.stringify({ request: e.request, response: e.response, layers: e.layers }, null, 2))
  439. .join('\n\n')
  440. : `(${channel} 暂无运行记录;先在对应页签执行一次操作)`;
  441. $('report-simevidence').value = lines;
  442. } catch (ex) { alert('导入失败:' + ex.message); }
  443. }
  444. /* ---------------- 初始化 ---------------- */
  445. async function init() {
  446. CATALOG = await api('/api/catalog');
  447. SCENARIOS = await api('/api/scenarios');
  448. fillObjectSelect($('run-object'));
  449. fillObjectSelect($('db-object'), (o) => (o.dbSync || {}).supported);
  450. const inboundSel = $('inbound-entity');
  451. inboundSel.innerHTML = '';
  452. const codes = [...new Set(objects()
  453. .filter((o) => (o.apiInbound || {}).supported)
  454. .map((o) => o.apiInbound.entityCode))].sort();
  455. codes.forEach((c) => {
  456. const opt = document.createElement('option');
  457. opt.value = c; opt.textContent = c;
  458. inboundSel.appendChild(opt);
  459. });
  460. const scSel = $('scenario-select');
  461. (SCENARIOS.scenarios || []).forEach((s) => {
  462. const opt = document.createElement('option');
  463. opt.value = s.scenarioId; opt.textContent = `${s.name}(${s.scenarioId})`;
  464. scSel.appendChild(opt);
  465. });
  466. $('run-object').addEventListener('change', refreshChannelState);
  467. $('run-channel').addEventListener('change', refreshChannelState);
  468. $('btn-load-sample').addEventListener('click', loadSample);
  469. $('btn-exec').addEventListener('click', execRun);
  470. $('btn-precheck').addEventListener('click', precheckRun);
  471. $('scenario-select').addEventListener('change', renderScenarioGraph);
  472. $('btn-scenario-run').addEventListener('click', runScenario);
  473. $('btn-mock-start').addEventListener('click', async () => {
  474. try { show('mock-audit', await api('/api/mock-api/start', { method: 'POST' })); refreshStatus(); }
  475. catch (ex) { setError('mock-audit', ex); }
  476. });
  477. $('btn-mock-stop').addEventListener('click', async () => {
  478. try { show('mock-audit', await api('/api/mock-api/stop', { method: 'POST' })); refreshStatus(); }
  479. catch (ex) { setError('mock-audit', ex); }
  480. });
  481. $('btn-mock-apply').addEventListener('click', () => mockControl({
  482. auth_mode: $('mock-auth').value,
  483. delay_seconds: parseFloat($('mock-delay').value || '0'),
  484. force_status: $('mock-force').value ? parseInt($('mock-force').value, 10) : null,
  485. response_path: $('mock-resp-path').value.trim() || 'data.list',
  486. }));
  487. $('btn-mock-clear-overrides').addEventListener('click', () => mockControl({ clear_overrides: true }));
  488. $('btn-mock-audit').addEventListener('click', async () => {
  489. try { show('mock-audit', await api('/api/mock-api/audit')); } catch (ex) { setError('mock-audit', ex); }
  490. });
  491. const dbObj = () => $('db-object').value;
  492. $('btn-db-ensure').addEventListener('click', () => dbAction('/api/db/ensure', { objectCode: dbObj(), runId: currentRunId() }, 'db-output'));
  493. $('btn-db-load').addEventListener('click', () => dbAction('/api/db/load', { objectCode: dbObj(), runId: currentRunId() }, 'db-output'));
  494. $('btn-db-reset').addEventListener('click', () => dbAction('/api/db/reset', { objectCode: dbObj() }, 'db-output'));
  495. $('btn-db-stats').addEventListener('click', async () => {
  496. try { show('db-output', await api(`/api/db/stats/${dbObj()}`)); } catch (ex) { setError('db-output', ex); }
  497. });
  498. $('btn-db-config').addEventListener('click', async () => {
  499. try { show('db-config-output', await api(`/api/db/aidop-config/${dbObj()}`)); } catch (ex) { setError('db-config-output', ex); }
  500. });
  501. $('btn-precheck-all').addEventListener('click', precheckAll);
  502. $('btn-inbound-exec').addEventListener('click', inboundExec);
  503. $('btn-runs-refresh').addEventListener('click', async () => {
  504. try { show('runs-output', await api('/api/runs')); } catch (ex) { setError('runs-output', ex); }
  505. });
  506. $('btn-report-save').addEventListener('click', saveReport);
  507. $('btn-report-export').addEventListener('click', () => exportReport(LAST_REPORT_ID));
  508. $('btn-report-refresh').addEventListener('click', refreshReportList);
  509. $('btn-report-evidence').addEventListener('click', importReportEvidence);
  510. refreshReportList();
  511. $('btn-fault-refresh').addEventListener('click', refreshFaultCatalog);
  512. $('btn-fault-recover-all').addEventListener('click', recoverAllFaults);
  513. $('fault-report-scenario').addEventListener('change', syncFaultReportPrefill);
  514. $('btn-fault-report-save').addEventListener('click', saveFaultReport);
  515. $('btn-fault-report-export').addEventListener('click', () => exportFaultReport(LAST_FAULT_REPORT_ID));
  516. $('btn-fault-report-refresh').addEventListener('click', refreshFaultReportList);
  517. refreshFaultCatalog();
  518. refreshChannelState();
  519. renderScenarioGraph();
  520. await refreshStatus();
  521. setInterval(refreshStatus, 15000);
  522. }
  523. init().catch((ex) => {
  524. $('env-banner').className = 'banner err';
  525. $('env-banner').textContent = '❌ 初始化失败:' + ex.message;
  526. });