const STORAGE_KEY = 'oem-batch-builder-jobs-v3'; const LEGACY_KEYS = ['oem-batch-builder-jobs-v2', 'oem-batch-builder-jobs-v1']; const defaultApiBaseUrl = 'http://47.107.86.179:3002/api'; const defaultVersion = '4.5.34'; let jobs = []; let running = false; let statusPollTimer = null; let logLines = []; let logFlushScheduled = false; const MAX_LOG_LINES = 180; const MAX_LOG_MESSAGE_LENGTH = 1000; const FULL_PACKAGE_COMMAND = 'npm run build:win'; const grid = document.getElementById('jobGrid'); const template = document.getElementById('jobTemplate'); const logBox = document.getElementById('logBox'); const outputDirText = document.getElementById('outputDir'); const runBtn = document.getElementById('runBtn'); const cancelBtn = document.getElementById('cancelBtn'); function diagnostic(message, payload = {}) { try { if (window.oemTool && typeof window.oemTool.diagnostic === 'function') { window.oemTool.diagnostic(message, payload).catch(() => {}); } } catch { // ignore diagnostics failure } } window.addEventListener('error', event => { diagnostic('window error', { message: event.message, filename: event.filename, lineno: event.lineno, colno: event.colno, stack: event.error && event.error.stack ? event.error.stack : '', }); }); window.addEventListener('unhandledrejection', event => { const reason = event.reason; diagnostic('unhandled rejection', { message: reason && reason.message ? reason.message : String(reason), stack: reason && reason.stack ? reason.stack : '', }); }); function createJob(seed = {}) { const seedAssets = seed.assets || {}; const buildCommand = normalizeBuildCommand(seed.buildCommand); const isSmokeDraft = /^ui-smoke-|^ui-buildwin-/.test(String(seed.id || '')); return { id: isSmokeDraft || !seed.id ? `oem-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` : seed.id, enabled: seed.enabled !== false, brandName: seed.brandName || '', brandSlogan: seed.brandSlogan || 'One-stop AI digital human system', brandShortName: seed.brandShortName || '', brandDisplayName: seed.brandDisplayName || '', apiBaseUrl: seed.apiBaseUrl || defaultApiBaseUrl, version: seed.version || defaultVersion, buildCommand, assets: { qrcodePath: seedAssets.qrcodePath || '', logoPath: seedAssets.logoPath || '', iconPath: seedAssets.iconPath || '', loginBgPath: seedAssets.loginBgPath || '', }, status: isSmokeDraft ? 'idle' : (seed.status || 'idle'), message: isSmokeDraft ? '' : (seed.message || ''), }; } function normalizeBuildCommand(command) { const trimmed = String(command || '').trim(); if (!trimmed) return FULL_PACKAGE_COMMAND; // Older smoke tests wrote plain npm run build into the shared Electron profile. // The OEM tool is for producing packages, so repair that polluted draft value. if (/^npm(?:\.cmd)?\s+run\s+build$/i.test(trimmed)) { return FULL_PACKAGE_COMMAND; } return trimmed; } function saveDrafts() { localStorage.setItem(STORAGE_KEY, JSON.stringify(jobs)); } function loadDrafts() { try { let raw = localStorage.getItem(STORAGE_KEY); for (const key of LEGACY_KEYS) { if (!raw) raw = localStorage.getItem(key); } const parsed = raw ? JSON.parse(raw) : null; jobs = Array.isArray(parsed) && parsed.length > 0 ? parsed.map(createJob) : [createJob()]; jobs.forEach(repairJobPaths); saveDrafts(); } catch { jobs = [createJob()]; saveDrafts(); } } function fileName(filePath) { return String(filePath || '').split(/[\\/]/).pop() || ''; } function repairKnownMojibakePath(filePath) { return String(filePath || '') .replace(/cursor澶囦唤/g, 'cursor备份') .replace(/cursor锟斤拷锟斤拷/g, 'cursor备份'); } function repairJobPaths(job) { for (const key of ['qrcodePath', 'logoPath', 'iconPath', 'loginBgPath']) { job.assets[key] = repairKnownMojibakePath(job.assets[key]); } } function isSupportedImage(filePath) { return /\.(png|jpe?g|webp|ico|icns)$/i.test(String(filePath || '')); } function normalizeShortName(job) { if (!job.brandShortName.trim()) { job.brandShortName = (job.brandName || 'OEMApp') .replace(/\s+/g, '-') .replace(/[^\w.-]/g, '') .slice(0, 48) || 'OEMApp'; } if (!job.brandDisplayName.trim()) { job.brandDisplayName = job.brandName; } } function validateJob(job) { normalizeShortName(job); if (!job.brandName.trim()) return 'Software name is required'; if (!job.apiBaseUrl.trim()) return 'API base URL is required'; if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(job.version)) return 'Version must be x.y.z'; if (!job.assets.qrcodePath) return 'QR code image is missing'; if (!job.assets.logoPath) return 'Logo image is missing'; if (!job.assets.iconPath) return 'Icon image is missing'; if (!job.assets.loginBgPath) return 'Login background image is missing'; return ''; } function appendLog(payload) { const prefix = payload.jobId ? `[${payload.jobId}]` : '[batch]'; let message = String(payload.message || '').trimEnd(); if (!message) return; if (message.length > MAX_LOG_MESSAGE_LENGTH) { message = `${message.slice(0, MAX_LOG_MESSAGE_LENGTH)}\n... [log chunk truncated]`; } const stamp = new Date(payload.time || Date.now()).toLocaleTimeString(); for (const line of message.split(/\r?\n/)) { logLines.push(`${stamp} ${prefix} ${line}`); } if (logLines.length > MAX_LOG_LINES) { logLines = logLines.slice(-MAX_LOG_LINES); } if (logFlushScheduled) return; logFlushScheduled = true; requestAnimationFrame(() => { logFlushScheduled = false; logBox.textContent = `${logLines.join('\n')}\n`; logBox.scrollTop = logBox.scrollHeight; }); } function updateRunState() { const enabledCount = jobs.filter(job => job.enabled).length; runBtn.disabled = running; cancelBtn.disabled = !running; runBtn.textContent = running ? 'Packaging...' : `OEM + Package (${enabledCount})`; } function applyResults(result) { if (result && result.outputDir) outputDirText.textContent = result.outputDir; const resultMap = new Map((result && result.results || []).map(item => [item.id, item])); jobs.forEach(job => { const item = resultMap.get(job.id); if (!item) return; job.status = item.success ? 'success' : 'failed'; job.message = item.message || (item.success ? 'Done' : 'Failed'); }); saveDrafts(); render(); } function stopStatusPolling() { if (statusPollTimer) { clearInterval(statusPollTimer); statusPollTimer = null; } } function startStatusPolling() { stopStatusPolling(); statusPollTimer = setInterval(async () => { try { const status = await window.oemTool.getStatus(); if (status && status.outputDir) outputDirText.textContent = status.outputDir; running = Boolean(status && status.running); if (status && Array.isArray(status.results) && status.results.length > 0) { applyResults(status); } if (!running) stopStatusPolling(); updateRunState(); } catch (error) { appendLog({ message: error && error.message ? error.message : String(error) }); } }, 1200); } function setAsset(job, key, filePath) { if (!filePath) return; if (!isSupportedImage(filePath)) { appendLog({ jobId: job.id, message: `Unsupported image type: ${filePath}` }); return; } job.assets[key] = filePath; job.status = 'idle'; job.message = ''; saveDrafts(); render(); } function renderAssetButton(button, job, key) { const img = button.querySelector('img'); const small = button.querySelector('small'); const filePath = job.assets[key]; button.classList.toggle('filled', Boolean(filePath)); button.title = filePath || 'Click to select or drag an image here'; if (filePath) { img.src = window.oemTool.pathToFileUrl(filePath); img.style.display = 'block'; small.textContent = fileName(filePath); } else { img.removeAttribute('src'); img.style.display = 'none'; } } function render() { grid.innerHTML = ''; jobs.forEach((job, index) => { const node = template.content.firstElementChild.cloneNode(true); node.classList.add(job.status); node.querySelector('.index').textContent = `#${index + 1}`; node.querySelector('.status').textContent = job.status; node.querySelector('.message').textContent = job.message || ''; node.querySelectorAll('[data-field]').forEach(input => { const field = input.dataset.field; if (input.type === 'checkbox') { input.checked = Boolean(job[field]); } else { input.value = job[field] || ''; } input.addEventListener('change', () => { job[field] = input.type === 'checkbox' ? input.checked : input.value; if (job.status === 'success' || job.status === 'failed') job.status = 'idle'; saveDrafts(); render(); }); }); node.querySelectorAll('[data-asset]').forEach(button => { const key = button.dataset.asset; renderAssetButton(button, job, key); button.addEventListener('click', async () => { const filePath = await window.oemTool.pickImage(); setAsset(job, key, filePath); }); button.addEventListener('dragover', event => { event.preventDefault(); button.classList.add('dragging'); }); button.addEventListener('dragleave', () => { button.classList.remove('dragging'); }); button.addEventListener('drop', event => { event.preventDefault(); button.classList.remove('dragging'); const file = event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]; setAsset(job, key, file && file.path ? file.path : ''); }); }); node.querySelector('[data-action="copy"]').addEventListener('click', () => { jobs.push(createJob({ ...job, id: undefined, brandName: `${job.brandName || 'OEM'} Copy`, status: 'idle', message: '', })); saveDrafts(); render(); }); node.querySelector('[data-action="delete"]').addEventListener('click', () => { jobs = jobs.filter(item => item.id !== job.id); if (jobs.length === 0) jobs.push(createJob()); saveDrafts(); render(); }); grid.appendChild(node); }); updateRunState(); } async function runBatch() { diagnostic('runBatch clicked', { running, jobs: jobs.length, enabled: jobs.filter(job => job.enabled).length }); if (running) return; const activeJobs = jobs.filter(job => job.enabled); if (activeJobs.length === 0) { appendLog({ message: 'Select at least one OEM job' }); return; } for (const job of activeJobs) { const error = validateJob(job); if (error) { job.status = 'failed'; job.message = error; saveDrafts(); render(); appendLog({ jobId: job.id, message: `${job.brandName || job.id}: ${error}` }); return; } } logBox.textContent = ''; logLines = []; running = true; activeJobs.forEach(job => { job.status = 'running'; job.message = 'Starting background package task'; }); saveDrafts(); render(); try { diagnostic('runBatch invoking ipc', { activeJobs: activeJobs.length }); const result = await window.oemTool.runBatch(activeJobs.map(job => ({ id: job.id, brandName: job.brandName, brandSlogan: job.brandSlogan, brandShortName: job.brandShortName, brandDisplayName: job.brandDisplayName, apiBaseUrl: job.apiBaseUrl, version: job.version, assets: job.assets, buildCommand: normalizeBuildCommand(job.buildCommand), }))); diagnostic('runBatch ipc resolved', { accepted: Boolean(result && result.accepted), running: Boolean(result && result.running), message: result && result.message ? result.message : '', }); if (result && result.outputDir) outputDirText.textContent = result.outputDir; if (result && result.accepted) { appendLog({ message: result.message || 'Build started in background' }); activeJobs.forEach(job => { job.status = 'running'; job.message = 'Running in background'; }); saveDrafts(); render(); startStatusPolling(); return; } running = false; applyResults(result || {}); } catch (error) { diagnostic('runBatch ipc rejected', { message: error && error.message ? error.message : String(error), stack: error && error.stack ? error.stack : '', }); appendLog({ message: error && error.message ? error.message : String(error) }); activeJobs.forEach(job => { if (job.status === 'running') { job.status = 'failed'; job.message = 'Package failed'; } }); running = false; saveDrafts(); render(); } } document.addEventListener('dragover', event => event.preventDefault()); document.addEventListener('drop', event => event.preventDefault()); document.getElementById('addJobBtn').addEventListener('click', () => { jobs.push(createJob()); saveDrafts(); render(); }); document.getElementById('openOutputBtn').addEventListener('click', async () => { const result = await window.oemTool.openOutputDir(); if (result && result.outputDir) outputDirText.textContent = result.outputDir; }); cancelBtn.addEventListener('click', async () => { await window.oemTool.cancel(); running = false; stopStatusPolling(); updateRunState(); }); runBtn.addEventListener('click', runBatch); window.oemTool.onLog(appendLog); loadDrafts(); render(); window.oemTool.getStatus().then(status => { if (status && status.outputDir) outputDirText.textContent = status.outputDir; running = Boolean(status && status.running); if (running) { jobs.forEach(job => { if (job.enabled && job.status !== 'success' && job.status !== 'failed') { job.status = 'running'; job.message = 'Running in background'; } }); startStatusPolling(); } else if (status && Array.isArray(status.results) && status.results.length > 0) { applyResults(status); } updateRunState(); }).catch(error => appendLog({ message: error && error.message ? error.message : String(error) }));