Initial clean project import
This commit is contained in:
@@ -0,0 +1,450 @@
|
||||
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) }));
|
||||
Reference in New Issue
Block a user