| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598 |
- /**
- * BRAIN Alpha 模拟器 - 前端 JavaScript
- * 处理模拟器的用户界面,包括参数输入和日志监控
- */
- let currentLogFile = null;
- let logPollingInterval = null;
- let isSimulationRunning = false;
- let simulationAbortController = null;
- let userSelectedLogFile = false; // 跟踪用户是否手动选择了日志文件
- // 当 DOM 加载完成时初始化页面
- document.addEventListener('DOMContentLoaded', function() {
- refreshLogFiles();
- setupFormValidation();
- loadSimulatorDefaults();
- });
- /**
- * 设置表单验证和变更处理程序
- */
- function setupFormValidation() {
- const startPosition = document.getElementById('startPosition');
- const randomShuffle = document.getElementById('randomShuffle');
- const jsonFile = document.getElementById('jsonFile');
- // 当文件可能被覆盖时显示警告
- function checkOverwriteWarning() {
- const warning = document.getElementById('overwriteWarning');
- const showWarning = parseInt(startPosition.value) > 0 || randomShuffle.checked;
- warning.style.display = showWarning ? 'block' : 'none';
- }
- startPosition.addEventListener('input', checkOverwriteWarning);
- randomShuffle.addEventListener('change', checkOverwriteWarning);
- // 处理 JSON 文件选择
- jsonFile.addEventListener('change', function(e) {
- const file = e.target.files[0];
- const info = document.getElementById('jsonFileInfo');
- if (file) {
- info.innerHTML = `
- <strong>已选择:</strong> ${file.name}<br>
- <strong>大小:</strong> ${(file.size / 1024).toFixed(1)} KB<br>
- <strong>修改时间:</strong> ${new Date(file.lastModified).toLocaleString()}
- `;
- info.style.display = 'block';
- // 尝试读取并验证 JSON
- const reader = new FileReader();
- reader.onload = function(e) {
- try {
- const data = JSON.parse(e.target.result);
- if (Array.isArray(data)) {
- const maxStart = Math.max(0, data.length - 1);
- startPosition.max = maxStart;
- info.innerHTML += `<br><strong>表达式数量:</strong> 找到 ${data.length} 个`;
- } else {
- info.innerHTML += '<br><span style="color: #721c24;">⚠️ 警告: 不是数组格式</span>';
- }
- } catch (err) {
- info.innerHTML += '<br><span style="color: #721c24;">❌ JSON 格式无效</span>';
- }
- };
- reader.readAsText(file);
- } else {
- info.style.display = 'none';
- }
- });
- }
- /**
- * 从 localStorage 加载默认值(如果可用)
- */
- function loadSimulatorDefaults() {
- const username = localStorage.getItem('simulator_username');
- if (username) {
- document.getElementById('username').value = username;
- }
- const concurrentCount = localStorage.getItem('simulator_concurrent');
- if (concurrentCount) {
- document.getElementById('concurrentCount').value = concurrentCount;
- }
- }
- /**
- * 将当前表单值保存到 localStorage
- */
- function saveSimulatorDefaults() {
- localStorage.setItem('simulator_username', document.getElementById('username').value);
- localStorage.setItem('simulator_concurrent', document.getElementById('concurrentCount').value);
- }
- /**
- * 切换密码可见性
- */
- function togglePassword() {
- const passwordInput = document.getElementById('password');
- const isPassword = passwordInput.type === 'password';
- passwordInput.type = isPassword ? 'text' : 'password';
- const toggleBtn = document.querySelector('.password-toggle');
- toggleBtn.textContent = isPassword ? '🙈' : '👁️';
- }
- /**
- * 切换多模拟选项
- */
- function toggleMultiSimOptions() {
- const checkbox = document.getElementById('useMultiSim');
- const options = document.getElementById('multiSimOptions');
- options.style.display = checkbox.checked ? 'block' : 'none';
- }
- /**
- * 刷新可用的日志文件
- */
- async function refreshLogFiles() {
- try {
- const response = await fetch('/api/simulator/logs');
- const data = await response.json();
- const selector = document.getElementById('logSelector');
- selector.innerHTML = '<option value="">选择日志文件...</option>';
- if (data.logs && data.logs.length > 0) {
- data.logs.forEach(log => {
- const option = document.createElement('option');
- option.value = log.filename;
- option.textContent = `${log.filename} (${log.size}, ${log.modified})`;
- selector.appendChild(option);
- });
- // 只有当用户没有手动选择时才自动选择最新的日志文件
- if (data.latest && !userSelectedLogFile) {
- selector.value = data.latest;
- currentLogFile = data.latest;
- // 更新 UI 显示自动监控
- document.getElementById('currentLogFile').innerHTML = `
- <strong>🔄 自动监控:</strong> ${data.latest}<br>
- <small>当出现新日志文件时,将自动选择最新的。</small>
- `;
- loadSelectedLog();
- // 确保对自动选择的文件也启用轮询
- ensureLogPollingActive();
- }
- }
- } catch (error) {
- console.error('刷新日志文件时出错:', error);
- updateStatus('加载日志文件时出错', 'error');
- }
- // 确保刷新后轮询继续
- ensureLogPollingActive();
- }
- /**
- * 加载选定的日志文件内容
- */
- async function loadSelectedLog() {
- const selector = document.getElementById('logSelector');
- const selectedLog = selector.value;
- if (!selectedLog) {
- // 如果用户取消选择,则重置
- userSelectedLogFile = false;
- currentLogFile = null;
- document.getElementById('currentLogFile').innerHTML = `
- <strong>🔄 自动模式已启用:</strong> 将在可用时监控最新日志<br>
- <small>系统将自动选择并监控最新的日志文件。</small>
- `;
- // 尝试再次自动选择最新的
- refreshLogFiles();
- return;
- }
- // 标记用户已手动选择日志文件
- userSelectedLogFile = true;
- currentLogFile = selectedLog;
- // 如果尚未运行,则开始轮询以监控选定的文件
- ensureLogPollingActive();
- try {
- const response = await fetch(`/api/simulator/logs/${encodeURIComponent(selectedLog)}`);
- const data = await response.json();
- if (data.content !== undefined) {
- const logViewer = document.getElementById('logViewer');
- // 使用 innerHTML 正确处理中文字符编码
- const content = data.content || '日志文件为空。';
- logViewer.textContent = content;
- logViewer.scrollTop = logViewer.scrollHeight;
- // 仅当用户手动选择时(非自动选择)更新状态
- if (userSelectedLogFile) {
- document.getElementById('currentLogFile').innerHTML = `
- <strong>📌 手动选择:</strong> ${selectedLog}<br>
- <small>已禁用自动切换到最新日志。选择"选择日志文件..."以重新启用。</small>
- `;
- }
- }
- } catch (error) {
- console.error('加载日志文件时出错:', error);
- updateStatus('加载日志内容时出错', 'error');
- }
- }
- /**
- * 测试连接到 BRAIN API
- */
- async function testConnection() {
- const username = document.getElementById('username').value;
- const password = document.getElementById('password').value;
- if (!username || !password) {
- updateStatus('请先输入用户名和密码', 'error');
- return;
- }
- const testBtn = document.getElementById('testBtn');
- testBtn.disabled = true;
- testBtn.textContent = '🔄 测试中...';
- updateStatus('正在测试 BRAIN API 连接...', 'running');
- try {
- const response = await fetch('/api/simulator/test-connection', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- username: username,
- password: password
- })
- });
- const data = await response.json();
- if (data.success) {
- updateStatus('✅ 连接成功!准备运行模拟。', 'success');
- saveSimulatorDefaults();
- } else {
- updateStatus(`❌ 连接失败: ${data.error}`, 'error');
- }
- } catch (error) {
- updateStatus(`❌ 连接错误: ${error.message}`, 'error');
- } finally {
- testBtn.disabled = false;
- testBtn.textContent = '🔗 测试连接';
- }
- }
- /**
- * 使用用户参数运行模拟器
- */
- async function runSimulator() {
- if (isSimulationRunning) {
- updateStatus('模拟已在运行中', 'error');
- return;
- }
- // 验证表单
- const form = document.getElementById('simulatorForm');
- if (!form.checkValidity()) {
- form.reportValidity();
- return;
- }
- const jsonFile = document.getElementById('jsonFile').files[0];
- if (!jsonFile) {
- updateStatus('请选择 JSON 文件', 'error');
- return;
- }
- // 准备表单数据
- const formData = new FormData();
- formData.append('jsonFile', jsonFile);
- formData.append('username', document.getElementById('username').value);
- formData.append('password', document.getElementById('password').value);
- formData.append('startPosition', document.getElementById('startPosition').value);
- formData.append('concurrentCount', document.getElementById('concurrentCount').value);
- formData.append('randomShuffle', document.getElementById('randomShuffle').checked);
- formData.append('useMultiSim', document.getElementById('useMultiSim').checked);
- formData.append('alphaCountPerSlot', document.getElementById('alphaCountPerSlot').value);
- // UI 更新
- isSimulationRunning = true;
- const runBtn = document.getElementById('runSimulator');
- const stopBtn = document.getElementById('stopBtn');
- runBtn.disabled = true;
- runBtn.textContent = '🔄 运行中...';
- stopBtn.style.display = 'inline-block';
- updateStatus('正在启动模拟...', 'running');
- showProgress(true);
- // 创建中止控制器用于停止模拟
- simulationAbortController = new AbortController();
- try {
- saveSimulatorDefaults();
- const response = await fetch('/api/simulator/run', {
- method: 'POST',
- body: formData,
- signal: simulationAbortController.signal
- });
- const data = await response.json();
- if (data.success) {
- updateStatus('✅ 模拟器已在新的终端窗口中启动!请查看终端窗口了解进度。', 'success');
- // 显示启动信息
- if (data.parameters) {
- showLaunchInfo(data.parameters);
- }
- // 由于模拟正在运行,开始更频繁地监控日志文件
- startLogPolling();
- // 刷新日志文件以获取最新的模拟日志
- setTimeout(() => {
- refreshLogFiles();
- }, 3000);
- } else {
- updateStatus(`❌ 启动模拟器失败: ${data.error}`, 'error');
- }
- } catch (error) {
- if (error.name === 'AbortError') {
- updateStatus('⏹️ 模拟已被用户停止', 'idle');
- } else {
- updateStatus(`❌ 模拟错误: ${error.message}`, 'error');
- }
- } finally {
- isSimulationRunning = false;
- runBtn.disabled = false;
- runBtn.textContent = '🚀 开始模拟';
- stopBtn.style.display = 'none';
- simulationAbortController = null;
- showProgress(false);
- }
- }
- /**
- * 停止正在运行的模拟
- */
- async function stopSimulation() {
- if (simulationAbortController) {
- simulationAbortController.abort();
- }
- try {
- await fetch('/api/simulator/stop', { method: 'POST' });
- } catch (error) {
- console.error('停止模拟时出错:', error);
- }
- updateStatus('正在停止模拟...', 'idle');
- }
- /**
- * 确保日志轮询在需要监控日志文件时处于活动状态
- */
- function ensureLogPollingActive() {
- if (currentLogFile && !logPollingInterval) {
- console.log('开始对文件进行日志轮询:', currentLogFile);
- startLogPolling();
- // 添加轮询活动状态的视觉指示器
- const currentLogFileDiv = document.getElementById('currentLogFile');
- if (userSelectedLogFile) {
- currentLogFileDiv.innerHTML = `
- <strong>📌 手动选择:</strong> ${currentLogFile} <span style="color: #28a745;">●</span><br>
- <small>已禁用自动切换到最新日志。选择"选择日志文件..."以重新启用。</small>
- `;
- } else {
- currentLogFileDiv.innerHTML = `
- <strong>🔄 自动监控:</strong> ${currentLogFile} <span style="color: #28a745;">●</span><br>
- <small>当出现新日志文件时,将自动选择最新的。</small>
- `;
- }
- } else if (currentLogFile && logPollingInterval) {
- console.log('日志轮询已对以下文件处于活动状态:', currentLogFile);
- }
- }
- /**
- * 开始轮询日志更新
- */
- function startLogPolling() {
- if (logPollingInterval) {
- clearInterval(logPollingInterval);
- }
- // 当模拟在终端中运行时,开始更频繁的轮询
- logPollingInterval = setInterval(async () => {
- try {
- // 仅当用户未手动选择文件时刷新日志文件列表
- // 这允许系统检测新日志文件,但不会干扰用户的选择
- if (!userSelectedLogFile) {
- await refreshLogFiles();
- }
- // 始终刷新当前监控的日志文件内容
- if (currentLogFile) {
- console.log('轮询日志文件:', currentLogFile, '用户选择:', userSelectedLogFile);
- const response = await fetch(`/api/simulator/logs/${encodeURIComponent(currentLogFile)}`);
- const data = await response.json();
- if (data.content !== undefined) {
- const logViewer = document.getElementById('logViewer');
- logViewer.textContent = data.content;
- logViewer.scrollTop = logViewer.scrollHeight;
- }
- }
- } catch (error) {
- console.error('轮询日志时出错:', error);
- }
- }, 3000); // 在终端中运行时每 3 秒轮询一次
- // 15 分钟后自动停止轮询,防止服务器负载过高
- setTimeout(() => {
- if (logPollingInterval) {
- clearInterval(logPollingInterval);
- logPollingInterval = null;
- console.log('15 分钟后自动停止日志轮询');
- }
- }, 900000); // 15 分钟
- }
- /**
- * 停止日志轮询
- */
- function stopLogPolling() {
- if (logPollingInterval) {
- clearInterval(logPollingInterval);
- logPollingInterval = null;
- }
- }
- /**
- * 更新状态指示器
- */
- function updateStatus(message, type = 'idle') {
- const statusEl = document.getElementById('simulatorStatus');
- statusEl.textContent = message;
- statusEl.className = `status-indicator status-${type}`;
- }
- /**
- * 显示/隐藏进度条
- */
- function showProgress(show) {
- const progressDiv = document.getElementById('simulationProgress');
- progressDiv.style.display = show ? 'block' : 'none';
- if (!show) {
- updateProgress(0, 0);
- }
- }
- /**
- * 更新进度条
- */
- function updateProgress(current, total) {
- const progressText = document.getElementById('progressText');
- const progressBar = document.getElementById('progressBar');
- progressText.textContent = `${current}/${total}`;
- if (total > 0) {
- const percentage = (current / total) * 100;
- progressBar.style.width = `${percentage}%`;
- } else {
- progressBar.style.width = '0%';
- }
- }
- /**
- * 当模拟器在终端中启动时显示启动信息
- */
- function showLaunchInfo(parameters) {
- const resultsPanel = document.getElementById('resultsPanel');
- const resultsDiv = document.getElementById('simulationResults');
- let html = '<div class="launch-info">';
- html += '<h4>🚀 模拟器启动成功</h4>';
- html += '<p>模拟器正在单独的终端窗口中运行。您可以在那里监控进度。</p>';
- html += '<div class="parameters-summary">';
- html += '<h5>📋 配置摘要:</h5>';
- html += `<p><strong>总表达式数:</strong> ${parameters.expressions_count}</p>`;
- html += `<p><strong>并发模拟数:</strong> ${parameters.concurrent_count}</p>`;
- if (parameters.use_multi_sim) {
- html += `<p><strong>多模拟模式:</strong> 是 (每个插槽 ${parameters.alpha_count_per_slot} 个 alpha)</p>`;
- html += `<p><strong>预计总 Alpha 数:</strong> ${parameters.expressions_count * parameters.alpha_count_per_slot}</p>`;
- } else {
- html += `<p><strong>多模拟模式:</strong> 否</p>`;
- }
- html += '</div>';
- html += '<div class="monitoring-info" style="margin-top: 15px; padding: 10px; background: #e7f3ff; border-radius: 4px;">';
- html += '<p><strong>💡 监控提示:</strong></p>';
- html += '<ul style="margin: 5px 0; padding-left: 20px;">';
- html += '<li>观察终端窗口以获取实时进度</li>';
- html += '<li>日志文件将在下方自动更新</li>';
- html += '<li>模拟结果将在完成后显示在终端中</li>';
- html += '<li>您可以使用刷新按钮手动刷新日志文件</li>';
- html += '</ul>';
- html += '</div>';
- html += '</div>';
- resultsDiv.innerHTML = html;
- resultsPanel.style.display = 'block';
- // 滚动到结果区域
- resultsPanel.scrollIntoView({ behavior: 'smooth' });
- }
- /**
- * 显示模拟结果
- */
- function showResults(results) {
- const resultsPanel = document.getElementById('resultsPanel');
- const resultsDiv = document.getElementById('simulationResults');
- let html = '<div class="results-summary">';
- html += `<p><strong>总模拟数:</strong> ${results.total || 0}</p>`;
- html += `<p><strong>成功:</strong> ${results.successful || 0}</p>`;
- html += `<p><strong>失败:</strong> ${results.failed || 0}</p>`;
- // 如果适用,添加多模拟信息
- if (results.use_multi_sim && results.alpha_count_per_slot) {
- html += `<div class="info-box" style="margin: 10px 0;">`;
- html += `<strong>📌 多模拟模式:</strong><br>`;
- html += `每个模拟插槽包含 ${results.alpha_count_per_slot} 个 alpha。<br>`;
- html += `处理的单个 alpha 总数: <strong>${results.successful * results.alpha_count_per_slot}</strong>`;
- html += `</div>`;
- }
- html += '</div>';
- if (results.alphaIds && results.alphaIds.length > 0) {
- html += '<h4>生成的 Alpha ID:</h4>';
- 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;">';
- results.alphaIds.forEach((alphaId, index) => {
- html += `<div>${index + 1}. ${alphaId}</div>`;
- });
- html += '</div>';
- // 为 Alpha ID 添加复制按钮
- html += '<div style="margin-top: 10px;">';
- html += '<button class="btn btn-outline btn-small" onclick="copyAlphaIds()">📋 复制所有 Alpha ID</button>';
- html += '</div>';
- }
- resultsDiv.innerHTML = html;
- resultsPanel.style.display = 'block';
- // 存储结果以供复制
- window.lastSimulationResults = results;
- // 滚动到结果区域
- resultsPanel.scrollIntoView({ behavior: 'smooth' });
- }
- /**
- * 将所有 Alpha ID 复制到剪贴板
- */
- function copyAlphaIds() {
- if (window.lastSimulationResults && window.lastSimulationResults.alphaIds) {
- const alphaIds = window.lastSimulationResults.alphaIds.join('\n');
- navigator.clipboard.writeText(alphaIds).then(() => {
- updateStatus('✅ Alpha ID 已复制到剪贴板!', 'success');
- }).catch(err => {
- console.error('复制失败: ', err);
- updateStatus('❌ 复制 Alpha ID 失败', 'error');
- });
- }
- }
- /**
- * 处理页面卸载 - 清理轮询
- */
- window.addEventListener('beforeunload', function() {
- stopLogPolling();
- if (simulationAbortController) {
- simulationAbortController.abort();
- }
- });
|