Files
2026-06-19 18:45:55 +08:00

451 lines
14 KiB
JavaScript

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) }));