simulator.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598
  1. /**
  2. * BRAIN Alpha 模拟器 - 前端 JavaScript
  3. * 处理模拟器的用户界面,包括参数输入和日志监控
  4. */
  5. let currentLogFile = null;
  6. let logPollingInterval = null;
  7. let isSimulationRunning = false;
  8. let simulationAbortController = null;
  9. let userSelectedLogFile = false; // 跟踪用户是否手动选择了日志文件
  10. // 当 DOM 加载完成时初始化页面
  11. document.addEventListener('DOMContentLoaded', function() {
  12. refreshLogFiles();
  13. setupFormValidation();
  14. loadSimulatorDefaults();
  15. });
  16. /**
  17. * 设置表单验证和变更处理程序
  18. */
  19. function setupFormValidation() {
  20. const startPosition = document.getElementById('startPosition');
  21. const randomShuffle = document.getElementById('randomShuffle');
  22. const jsonFile = document.getElementById('jsonFile');
  23. // 当文件可能被覆盖时显示警告
  24. function checkOverwriteWarning() {
  25. const warning = document.getElementById('overwriteWarning');
  26. const showWarning = parseInt(startPosition.value) > 0 || randomShuffle.checked;
  27. warning.style.display = showWarning ? 'block' : 'none';
  28. }
  29. startPosition.addEventListener('input', checkOverwriteWarning);
  30. randomShuffle.addEventListener('change', checkOverwriteWarning);
  31. // 处理 JSON 文件选择
  32. jsonFile.addEventListener('change', function(e) {
  33. const file = e.target.files[0];
  34. const info = document.getElementById('jsonFileInfo');
  35. if (file) {
  36. info.innerHTML = `
  37. <strong>已选择:</strong> ${file.name}<br>
  38. <strong>大小:</strong> ${(file.size / 1024).toFixed(1)} KB<br>
  39. <strong>修改时间:</strong> ${new Date(file.lastModified).toLocaleString()}
  40. `;
  41. info.style.display = 'block';
  42. // 尝试读取并验证 JSON
  43. const reader = new FileReader();
  44. reader.onload = function(e) {
  45. try {
  46. const data = JSON.parse(e.target.result);
  47. if (Array.isArray(data)) {
  48. const maxStart = Math.max(0, data.length - 1);
  49. startPosition.max = maxStart;
  50. info.innerHTML += `<br><strong>表达式数量:</strong> 找到 ${data.length} 个`;
  51. } else {
  52. info.innerHTML += '<br><span style="color: #721c24;">⚠️ 警告: 不是数组格式</span>';
  53. }
  54. } catch (err) {
  55. info.innerHTML += '<br><span style="color: #721c24;">❌ JSON 格式无效</span>';
  56. }
  57. };
  58. reader.readAsText(file);
  59. } else {
  60. info.style.display = 'none';
  61. }
  62. });
  63. }
  64. /**
  65. * 从 localStorage 加载默认值(如果可用)
  66. */
  67. function loadSimulatorDefaults() {
  68. const username = localStorage.getItem('simulator_username');
  69. if (username) {
  70. document.getElementById('username').value = username;
  71. }
  72. const concurrentCount = localStorage.getItem('simulator_concurrent');
  73. if (concurrentCount) {
  74. document.getElementById('concurrentCount').value = concurrentCount;
  75. }
  76. }
  77. /**
  78. * 将当前表单值保存到 localStorage
  79. */
  80. function saveSimulatorDefaults() {
  81. localStorage.setItem('simulator_username', document.getElementById('username').value);
  82. localStorage.setItem('simulator_concurrent', document.getElementById('concurrentCount').value);
  83. }
  84. /**
  85. * 切换密码可见性
  86. */
  87. function togglePassword() {
  88. const passwordInput = document.getElementById('password');
  89. const isPassword = passwordInput.type === 'password';
  90. passwordInput.type = isPassword ? 'text' : 'password';
  91. const toggleBtn = document.querySelector('.password-toggle');
  92. toggleBtn.textContent = isPassword ? '🙈' : '👁️';
  93. }
  94. /**
  95. * 切换多模拟选项
  96. */
  97. function toggleMultiSimOptions() {
  98. const checkbox = document.getElementById('useMultiSim');
  99. const options = document.getElementById('multiSimOptions');
  100. options.style.display = checkbox.checked ? 'block' : 'none';
  101. }
  102. /**
  103. * 刷新可用的日志文件
  104. */
  105. async function refreshLogFiles() {
  106. try {
  107. const response = await fetch('/api/simulator/logs');
  108. const data = await response.json();
  109. const selector = document.getElementById('logSelector');
  110. selector.innerHTML = '<option value="">选择日志文件...</option>';
  111. if (data.logs && data.logs.length > 0) {
  112. data.logs.forEach(log => {
  113. const option = document.createElement('option');
  114. option.value = log.filename;
  115. option.textContent = `${log.filename} (${log.size}, ${log.modified})`;
  116. selector.appendChild(option);
  117. });
  118. // 只有当用户没有手动选择时才自动选择最新的日志文件
  119. if (data.latest && !userSelectedLogFile) {
  120. selector.value = data.latest;
  121. currentLogFile = data.latest;
  122. // 更新 UI 显示自动监控
  123. document.getElementById('currentLogFile').innerHTML = `
  124. <strong>🔄 自动监控:</strong> ${data.latest}<br>
  125. <small>当出现新日志文件时,将自动选择最新的。</small>
  126. `;
  127. loadSelectedLog();
  128. // 确保对自动选择的文件也启用轮询
  129. ensureLogPollingActive();
  130. }
  131. }
  132. } catch (error) {
  133. console.error('刷新日志文件时出错:', error);
  134. updateStatus('加载日志文件时出错', 'error');
  135. }
  136. // 确保刷新后轮询继续
  137. ensureLogPollingActive();
  138. }
  139. /**
  140. * 加载选定的日志文件内容
  141. */
  142. async function loadSelectedLog() {
  143. const selector = document.getElementById('logSelector');
  144. const selectedLog = selector.value;
  145. if (!selectedLog) {
  146. // 如果用户取消选择,则重置
  147. userSelectedLogFile = false;
  148. currentLogFile = null;
  149. document.getElementById('currentLogFile').innerHTML = `
  150. <strong>🔄 自动模式已启用:</strong> 将在可用时监控最新日志<br>
  151. <small>系统将自动选择并监控最新的日志文件。</small>
  152. `;
  153. // 尝试再次自动选择最新的
  154. refreshLogFiles();
  155. return;
  156. }
  157. // 标记用户已手动选择日志文件
  158. userSelectedLogFile = true;
  159. currentLogFile = selectedLog;
  160. // 如果尚未运行,则开始轮询以监控选定的文件
  161. ensureLogPollingActive();
  162. try {
  163. const response = await fetch(`/api/simulator/logs/${encodeURIComponent(selectedLog)}`);
  164. const data = await response.json();
  165. if (data.content !== undefined) {
  166. const logViewer = document.getElementById('logViewer');
  167. // 使用 innerHTML 正确处理中文字符编码
  168. const content = data.content || '日志文件为空。';
  169. logViewer.textContent = content;
  170. logViewer.scrollTop = logViewer.scrollHeight;
  171. // 仅当用户手动选择时(非自动选择)更新状态
  172. if (userSelectedLogFile) {
  173. document.getElementById('currentLogFile').innerHTML = `
  174. <strong>📌 手动选择:</strong> ${selectedLog}<br>
  175. <small>已禁用自动切换到最新日志。选择"选择日志文件..."以重新启用。</small>
  176. `;
  177. }
  178. }
  179. } catch (error) {
  180. console.error('加载日志文件时出错:', error);
  181. updateStatus('加载日志内容时出错', 'error');
  182. }
  183. }
  184. /**
  185. * 测试连接到 BRAIN API
  186. */
  187. async function testConnection() {
  188. const username = document.getElementById('username').value;
  189. const password = document.getElementById('password').value;
  190. if (!username || !password) {
  191. updateStatus('请先输入用户名和密码', 'error');
  192. return;
  193. }
  194. const testBtn = document.getElementById('testBtn');
  195. testBtn.disabled = true;
  196. testBtn.textContent = '🔄 测试中...';
  197. updateStatus('正在测试 BRAIN API 连接...', 'running');
  198. try {
  199. const response = await fetch('/api/simulator/test-connection', {
  200. method: 'POST',
  201. headers: {
  202. 'Content-Type': 'application/json'
  203. },
  204. body: JSON.stringify({
  205. username: username,
  206. password: password
  207. })
  208. });
  209. const data = await response.json();
  210. if (data.success) {
  211. updateStatus('✅ 连接成功!准备运行模拟。', 'success');
  212. saveSimulatorDefaults();
  213. } else {
  214. updateStatus(`❌ 连接失败: ${data.error}`, 'error');
  215. }
  216. } catch (error) {
  217. updateStatus(`❌ 连接错误: ${error.message}`, 'error');
  218. } finally {
  219. testBtn.disabled = false;
  220. testBtn.textContent = '🔗 测试连接';
  221. }
  222. }
  223. /**
  224. * 使用用户参数运行模拟器
  225. */
  226. async function runSimulator() {
  227. if (isSimulationRunning) {
  228. updateStatus('模拟已在运行中', 'error');
  229. return;
  230. }
  231. // 验证表单
  232. const form = document.getElementById('simulatorForm');
  233. if (!form.checkValidity()) {
  234. form.reportValidity();
  235. return;
  236. }
  237. const jsonFile = document.getElementById('jsonFile').files[0];
  238. if (!jsonFile) {
  239. updateStatus('请选择 JSON 文件', 'error');
  240. return;
  241. }
  242. // 准备表单数据
  243. const formData = new FormData();
  244. formData.append('jsonFile', jsonFile);
  245. formData.append('username', document.getElementById('username').value);
  246. formData.append('password', document.getElementById('password').value);
  247. formData.append('startPosition', document.getElementById('startPosition').value);
  248. formData.append('concurrentCount', document.getElementById('concurrentCount').value);
  249. formData.append('randomShuffle', document.getElementById('randomShuffle').checked);
  250. formData.append('useMultiSim', document.getElementById('useMultiSim').checked);
  251. formData.append('alphaCountPerSlot', document.getElementById('alphaCountPerSlot').value);
  252. // UI 更新
  253. isSimulationRunning = true;
  254. const runBtn = document.getElementById('runSimulator');
  255. const stopBtn = document.getElementById('stopBtn');
  256. runBtn.disabled = true;
  257. runBtn.textContent = '🔄 运行中...';
  258. stopBtn.style.display = 'inline-block';
  259. updateStatus('正在启动模拟...', 'running');
  260. showProgress(true);
  261. // 创建中止控制器用于停止模拟
  262. simulationAbortController = new AbortController();
  263. try {
  264. saveSimulatorDefaults();
  265. const response = await fetch('/api/simulator/run', {
  266. method: 'POST',
  267. body: formData,
  268. signal: simulationAbortController.signal
  269. });
  270. const data = await response.json();
  271. if (data.success) {
  272. updateStatus('✅ 模拟器已在新的终端窗口中启动!请查看终端窗口了解进度。', 'success');
  273. // 显示启动信息
  274. if (data.parameters) {
  275. showLaunchInfo(data.parameters);
  276. }
  277. // 由于模拟正在运行,开始更频繁地监控日志文件
  278. startLogPolling();
  279. // 刷新日志文件以获取最新的模拟日志
  280. setTimeout(() => {
  281. refreshLogFiles();
  282. }, 3000);
  283. } else {
  284. updateStatus(`❌ 启动模拟器失败: ${data.error}`, 'error');
  285. }
  286. } catch (error) {
  287. if (error.name === 'AbortError') {
  288. updateStatus('⏹️ 模拟已被用户停止', 'idle');
  289. } else {
  290. updateStatus(`❌ 模拟错误: ${error.message}`, 'error');
  291. }
  292. } finally {
  293. isSimulationRunning = false;
  294. runBtn.disabled = false;
  295. runBtn.textContent = '🚀 开始模拟';
  296. stopBtn.style.display = 'none';
  297. simulationAbortController = null;
  298. showProgress(false);
  299. }
  300. }
  301. /**
  302. * 停止正在运行的模拟
  303. */
  304. async function stopSimulation() {
  305. if (simulationAbortController) {
  306. simulationAbortController.abort();
  307. }
  308. try {
  309. await fetch('/api/simulator/stop', { method: 'POST' });
  310. } catch (error) {
  311. console.error('停止模拟时出错:', error);
  312. }
  313. updateStatus('正在停止模拟...', 'idle');
  314. }
  315. /**
  316. * 确保日志轮询在需要监控日志文件时处于活动状态
  317. */
  318. function ensureLogPollingActive() {
  319. if (currentLogFile && !logPollingInterval) {
  320. console.log('开始对文件进行日志轮询:', currentLogFile);
  321. startLogPolling();
  322. // 添加轮询活动状态的视觉指示器
  323. const currentLogFileDiv = document.getElementById('currentLogFile');
  324. if (userSelectedLogFile) {
  325. currentLogFileDiv.innerHTML = `
  326. <strong>📌 手动选择:</strong> ${currentLogFile} <span style="color: #28a745;">●</span><br>
  327. <small>已禁用自动切换到最新日志。选择"选择日志文件..."以重新启用。</small>
  328. `;
  329. } else {
  330. currentLogFileDiv.innerHTML = `
  331. <strong>🔄 自动监控:</strong> ${currentLogFile} <span style="color: #28a745;">●</span><br>
  332. <small>当出现新日志文件时,将自动选择最新的。</small>
  333. `;
  334. }
  335. } else if (currentLogFile && logPollingInterval) {
  336. console.log('日志轮询已对以下文件处于活动状态:', currentLogFile);
  337. }
  338. }
  339. /**
  340. * 开始轮询日志更新
  341. */
  342. function startLogPolling() {
  343. if (logPollingInterval) {
  344. clearInterval(logPollingInterval);
  345. }
  346. // 当模拟在终端中运行时,开始更频繁的轮询
  347. logPollingInterval = setInterval(async () => {
  348. try {
  349. // 仅当用户未手动选择文件时刷新日志文件列表
  350. // 这允许系统检测新日志文件,但不会干扰用户的选择
  351. if (!userSelectedLogFile) {
  352. await refreshLogFiles();
  353. }
  354. // 始终刷新当前监控的日志文件内容
  355. if (currentLogFile) {
  356. console.log('轮询日志文件:', currentLogFile, '用户选择:', userSelectedLogFile);
  357. const response = await fetch(`/api/simulator/logs/${encodeURIComponent(currentLogFile)}`);
  358. const data = await response.json();
  359. if (data.content !== undefined) {
  360. const logViewer = document.getElementById('logViewer');
  361. logViewer.textContent = data.content;
  362. logViewer.scrollTop = logViewer.scrollHeight;
  363. }
  364. }
  365. } catch (error) {
  366. console.error('轮询日志时出错:', error);
  367. }
  368. }, 3000); // 在终端中运行时每 3 秒轮询一次
  369. // 15 分钟后自动停止轮询,防止服务器负载过高
  370. setTimeout(() => {
  371. if (logPollingInterval) {
  372. clearInterval(logPollingInterval);
  373. logPollingInterval = null;
  374. console.log('15 分钟后自动停止日志轮询');
  375. }
  376. }, 900000); // 15 分钟
  377. }
  378. /**
  379. * 停止日志轮询
  380. */
  381. function stopLogPolling() {
  382. if (logPollingInterval) {
  383. clearInterval(logPollingInterval);
  384. logPollingInterval = null;
  385. }
  386. }
  387. /**
  388. * 更新状态指示器
  389. */
  390. function updateStatus(message, type = 'idle') {
  391. const statusEl = document.getElementById('simulatorStatus');
  392. statusEl.textContent = message;
  393. statusEl.className = `status-indicator status-${type}`;
  394. }
  395. /**
  396. * 显示/隐藏进度条
  397. */
  398. function showProgress(show) {
  399. const progressDiv = document.getElementById('simulationProgress');
  400. progressDiv.style.display = show ? 'block' : 'none';
  401. if (!show) {
  402. updateProgress(0, 0);
  403. }
  404. }
  405. /**
  406. * 更新进度条
  407. */
  408. function updateProgress(current, total) {
  409. const progressText = document.getElementById('progressText');
  410. const progressBar = document.getElementById('progressBar');
  411. progressText.textContent = `${current}/${total}`;
  412. if (total > 0) {
  413. const percentage = (current / total) * 100;
  414. progressBar.style.width = `${percentage}%`;
  415. } else {
  416. progressBar.style.width = '0%';
  417. }
  418. }
  419. /**
  420. * 当模拟器在终端中启动时显示启动信息
  421. */
  422. function showLaunchInfo(parameters) {
  423. const resultsPanel = document.getElementById('resultsPanel');
  424. const resultsDiv = document.getElementById('simulationResults');
  425. let html = '<div class="launch-info">';
  426. html += '<h4>🚀 模拟器启动成功</h4>';
  427. html += '<p>模拟器正在单独的终端窗口中运行。您可以在那里监控进度。</p>';
  428. html += '<div class="parameters-summary">';
  429. html += '<h5>📋 配置摘要:</h5>';
  430. html += `<p><strong>总表达式数:</strong> ${parameters.expressions_count}</p>`;
  431. html += `<p><strong>并发模拟数:</strong> ${parameters.concurrent_count}</p>`;
  432. if (parameters.use_multi_sim) {
  433. html += `<p><strong>多模拟模式:</strong> 是 (每个插槽 ${parameters.alpha_count_per_slot} 个 alpha)</p>`;
  434. html += `<p><strong>预计总 Alpha 数:</strong> ${parameters.expressions_count * parameters.alpha_count_per_slot}</p>`;
  435. } else {
  436. html += `<p><strong>多模拟模式:</strong> 否</p>`;
  437. }
  438. html += '</div>';
  439. html += '<div class="monitoring-info" style="margin-top: 15px; padding: 10px; background: #e7f3ff; border-radius: 4px;">';
  440. html += '<p><strong>💡 监控提示:</strong></p>';
  441. html += '<ul style="margin: 5px 0; padding-left: 20px;">';
  442. html += '<li>观察终端窗口以获取实时进度</li>';
  443. html += '<li>日志文件将在下方自动更新</li>';
  444. html += '<li>模拟结果将在完成后显示在终端中</li>';
  445. html += '<li>您可以使用刷新按钮手动刷新日志文件</li>';
  446. html += '</ul>';
  447. html += '</div>';
  448. html += '</div>';
  449. resultsDiv.innerHTML = html;
  450. resultsPanel.style.display = 'block';
  451. // 滚动到结果区域
  452. resultsPanel.scrollIntoView({ behavior: 'smooth' });
  453. }
  454. /**
  455. * 显示模拟结果
  456. */
  457. function showResults(results) {
  458. const resultsPanel = document.getElementById('resultsPanel');
  459. const resultsDiv = document.getElementById('simulationResults');
  460. let html = '<div class="results-summary">';
  461. html += `<p><strong>总模拟数:</strong> ${results.total || 0}</p>`;
  462. html += `<p><strong>成功:</strong> ${results.successful || 0}</p>`;
  463. html += `<p><strong>失败:</strong> ${results.failed || 0}</p>`;
  464. // 如果适用,添加多模拟信息
  465. if (results.use_multi_sim && results.alpha_count_per_slot) {
  466. html += `<div class="info-box" style="margin: 10px 0;">`;
  467. html += `<strong>📌 多模拟模式:</strong><br>`;
  468. html += `每个模拟插槽包含 ${results.alpha_count_per_slot} 个 alpha。<br>`;
  469. html += `处理的单个 alpha 总数: <strong>${results.successful * results.alpha_count_per_slot}</strong>`;
  470. html += `</div>`;
  471. }
  472. html += '</div>';
  473. if (results.alphaIds && results.alphaIds.length > 0) {
  474. html += '<h4>生成的 Alpha ID:</h4>';
  475. html += '<div class="alpha-ids" style="max-height: 200px; overflow-y: auto; background: #f8f9fa; padding: 10px; border-radius: 4px; font-family: monospace; font-size: 12px;">';
  476. results.alphaIds.forEach((alphaId, index) => {
  477. html += `<div>${index + 1}. ${alphaId}</div>`;
  478. });
  479. html += '</div>';
  480. // 为 Alpha ID 添加复制按钮
  481. html += '<div style="margin-top: 10px;">';
  482. html += '<button class="btn btn-outline btn-small" onclick="copyAlphaIds()">📋 复制所有 Alpha ID</button>';
  483. html += '</div>';
  484. }
  485. resultsDiv.innerHTML = html;
  486. resultsPanel.style.display = 'block';
  487. // 存储结果以供复制
  488. window.lastSimulationResults = results;
  489. // 滚动到结果区域
  490. resultsPanel.scrollIntoView({ behavior: 'smooth' });
  491. }
  492. /**
  493. * 将所有 Alpha ID 复制到剪贴板
  494. */
  495. function copyAlphaIds() {
  496. if (window.lastSimulationResults && window.lastSimulationResults.alphaIds) {
  497. const alphaIds = window.lastSimulationResults.alphaIds.join('\n');
  498. navigator.clipboard.writeText(alphaIds).then(() => {
  499. updateStatus('✅ Alpha ID 已复制到剪贴板!', 'success');
  500. }).catch(err => {
  501. console.error('复制失败: ', err);
  502. updateStatus('❌ 复制 Alpha ID 失败', 'error');
  503. });
  504. }
  505. }
  506. /**
  507. * 处理页面卸载 - 清理轮询
  508. */
  509. window.addEventListener('beforeunload', function() {
  510. stopLogPolling();
  511. if (simulationAbortController) {
  512. simulationAbortController.abort();
  513. }
  514. });