import { dialog, shell } from 'electron' import fs from 'fs' import log from 'electron-log' import { CS_FAIL, CS_OK } from '../constants' import { assertNotBusy, assertReady, mainAppState } from '../services/app-state' import { configStore } from '../services/config-store' import { startUsbPoll, startJobPoll, stopAllPolls, stopJobPoll, stopUsbPoll, stopCardPositionPoll, startCardPositionPoll, getPollMainWindow, isJobPollActive } from '../services/poll-manager' import { parsePrinterInfoFromDll, type PrinterStatusSnapshot } from '@shared/printer-info' import { cleanPathPattern, getDirectorySizeBytes } from '../utils/dir-size' import { getDesignAppPath } from '../services/app-config' import { openDesignApp } from '../services/open-design-app' import { writeJobCsv, type JobCsvRow } from '../utils/job-csv' import { parseSoonTemplate } from '../utils/parse-soon' import { stageJobPayloadJson } from '../utils/stage-job-payload' import { ensureDllInitialized, isDllInitAttempted } from '../services/dll-bootstrap' import { loadDllModule } from '../services/dll-loader' import { tracedHandle } from './traced-handler' function ok(data?: T) { return { ok: true as const, code: CS_OK, data } } function fail(code: number, message: string) { return { ok: false as const, code, message } } function summarizeStagedPayload(json: string): Record { try { const p = JSON.parse(json) as Record return { task_id: p.task_id, has_copy_task: p.has_copy_task, has_print_task: p.has_print_task, path_file_count: Array.isArray(p.path_file) ? p.path_file.length : 0, json_file: p.json_file, udf_file: p.udf_file } } catch { return { parseError: true } } } function parseBool(v: unknown): boolean { return v === true || v === 'true' || v === 1 || v === '1' || String(v).toLowerCase() === 'true' } export function registerIpcHandlers(): void { tracedHandle('dll:init', async (_e, params) => { stopAllPolls() try { const r = await ensureDllInitialized(params) return ok({ code: r.code, warning: r.warning }) } catch (err) { return fail(CS_FAIL, String(err)) } }) tracedHandle('dll:printer-info', async () => { try { assertReady() const dll = await loadDllModule() const r = dll.dllGetPrinterInfo() if (!r.json) { const cached = configStore.get('lastPrinterStatus') if (cached) { return ok({ ...cached, fromCache: true, liveError: r.code <= 0 ? '未连接打印机' : `GetPrinterInfoEx code=${r.code}` }) } return fail(0, '未连接打印机') } const snapshot = parsePrinterInfoFromDll(r.json) configStore.set('lastPrinterStatus', snapshot) return ok({ ...r.json, snapshot }) } catch (err) { const cached = configStore.get('lastPrinterStatus') if (cached) { return ok({ ...cached, fromCache: true, liveError: String(err) }) } return fail(CS_FAIL, String(err)) } }) tracedHandle('dll:printer-reset', async () => { try { assertReady() const dll = await loadDllModule() const code = dll.dllPrinterReset() return code === CS_OK ? ok() : fail(code, '重置失败') } catch (err) { return fail(CS_FAIL, String(err)) } }) tracedHandle('dll:printer-reject', async () => { try { assertReady() const dll = await loadDllModule() if (!dll.isRejectApiAvailable()) return fail(CS_FAIL, 'REJECT_API_UNAVAILABLE') const code = dll.dllPrinterReject() return code === CS_OK ? ok() : fail(code, '废卡失败') } catch (err) { return fail(CS_FAIL, String(err)) } }) tracedHandle('dll:printer-error-str', async (_e, errorNo?: number) => { if (!mainAppState.initialized) return ok({ text: '' }) try { const dll = await loadDllModule() return ok({ text: dll.dllGetPrinterErrorStr(errorNo ?? -1) }) } catch (err) { log.warn('dll:printer-error-str', err) return ok({ text: '' }) } }) tracedHandle('dll:job-create', async (_e, json: string, opts?: { resubmit?: boolean }) => { try { assertReady() if (opts?.resubmit) { if (mainAppState.mode !== 'distributing') { return fail(CS_FAIL, '当前不在分发任务会话中') } stopCardPositionPoll() } else { assertNotBusy() } const dll = await loadDllModule() const sharedDir = configStore.get('sharedDir') as string let staged: { json: string; taskDir: string } try { staged = stageJobPayloadJson(json, sharedDir, dll) } catch (err) { const msg = err instanceof Error ? err.message : String(err) return fail(CS_FAIL, msg) } log.info('RestJobEx staging', { taskDir: staged.taskDir, summary: summarizeStagedPayload(staged.json) }) const r = dll.dllRestJobEx(staged.json) if (r.code !== CS_OK) { log.warn('RestJobEx rejected', { code: r.code, json: staged.json.slice(0, 800) }) const errText = dll.dllGetPrinterErrorStr(r.code) let detail = errText ? `${errText} (code=${r.code})` : `RestJobEx 失败 (code=${r.code})` if (r.code === -1 && !errText) { detail += ':请确认模板路径、变量 CSV(udf_file)及拷贝路径有效,且打印机/任务目录已就绪' } return fail(r.code, detail) } if (!r.jobId?.trim()) { return fail(CS_FAIL, 'RestJobEx 未返回 jobId') } mainAppState.mode = 'distributing' mainAppState.activeJobId = r.jobId return ok({ jobId: r.jobId }) } catch (err) { const msg = String(err) if (msg.includes('BUSY')) return fail(CS_FAIL, '已有任务在执行') if (msg.includes('NOT_INITIALIZED')) { return fail(CS_FAIL, '系统未初始化,请重启应用') } return fail(CS_FAIL, msg) } }) tracedHandle('dll:job-cancel', async (_e, jobId: string) => { const id = String(jobId || mainAppState.activeJobId || '').trim() stopJobPoll(true) mainAppState.mode = 'ready' mainAppState.activeJobId = '' if (!id) return ok() if (!mainAppState.initialized) return ok() try { const dll = await loadDllModule() if (!dll.isCancelApiAvailable()) return ok() const code = dll.dllAdminJobCancel(id) return code === CS_OK ? ok() : fail(code, '取消失败') } catch (err) { log.warn('dll:job-cancel', err) return ok() } }) tracedHandle('dll:usb-copy', async (_e, req: { destFolder: string; cardOutput: number; resubmit?: boolean }) => { try { assertReady() if (req.resubmit) { if (mainAppState.mode !== 'usbCopying') { return fail(CS_FAIL, '当前不在数据收集会话中') } stopCardPositionPoll() } else { assertNotBusy() } const destFolder = String(req.destFolder || '').trim() if (!destFolder) return fail(CS_FAIL, '请先选择数据导入目录') fs.mkdirSync(destFolder, { recursive: true }) const dll = await loadDllModule() if (dll.isUsbReaderApiAvailable()) { const moveCode = dll.dllPrinterMoveToUsbReader() if (moveCode !== CS_OK) { log.warn('MoveToUsbReader before copy', { moveCode }) } } const code = dll.dllCopyFromUsb(destFolder, req.cardOutput) if (code !== CS_OK) { const errText = dll.dllGetPrinterErrorStr(code) return fail(code, errText || '启动 USB 收集失败') } mainAppState.mode = 'usbCopying' startUsbPoll() return ok() } catch (err) { if (String(err).includes('BUSY')) return fail(CS_FAIL, '已有任务在执行') return fail(CS_FAIL, String(err)) } }) tracedHandle('poll:job-start', (_e, jobId: string) => { startJobPoll(jobId) return ok() }) tracedHandle('poll:job-stop', (_e, opts?: { resetMode?: boolean }) => { stopJobPoll(opts?.resetMode !== false) return ok() }) tracedHandle('poll:usb-start', () => { startUsbPoll() return ok() }) tracedHandle('poll:usb-stop', (_e, opts?: { resetMode?: boolean }) => { stopUsbPoll(opts?.resetMode !== false) return ok() }) tracedHandle('poll:card-position-start', async () => { try { assertReady() const dll = await loadDllModule() if (!dll.isCardPositionApiAvailable()) { return fail(CS_FAIL, '当前 DLL 不支持卡位查询,无法自动续做') } startCardPositionPoll() return ok() } catch (err) { return fail(CS_FAIL, String(err)) } }) tracedHandle('poll:card-position-stop', () => { stopCardPositionPoll() return ok() }) tracedHandle('dialog:open-directory', async () => { const win = getPollMainWindow() const r = await dialog.showOpenDialog(win ?? undefined, { properties: ['openDirectory', 'multiSelections'] }) if (r.canceled || !r.filePaths.length) return ok({ paths: [] as string[] }) return ok({ paths: r.filePaths }) }) tracedHandle('dialog:open-file', async (_e, filters?: { name: string; extensions: string[] }[]) => { const win = getPollMainWindow() const r = await dialog.showOpenDialog(win ?? undefined, { properties: ['openFile'], filters: filters ?? [{ name: 'Soon', extensions: ['soon'] }] }) if (r.canceled || !r.filePaths[0]) return ok({ path: '' }) return ok({ path: r.filePaths[0] }) }) tracedHandle('fs:path-exists', (_e, paths: string[]) => { const missing = paths .map((raw) => ({ raw, dir: cleanPathPattern(raw) })) .filter(({ dir }) => !dir || !fs.existsSync(dir)) .map(({ dir, raw }) => dir || raw) return ok({ missing }) }) tracedHandle('fs:dir-size', (_e, paths: string[]) => { const items = paths.map((raw) => { const dir = cleanPathPattern(raw) if (!fs.existsSync(dir)) return { path: raw, bytes: 0, missing: true as const } try { const st = fs.statSync(dir) if (!st.isDirectory()) return { path: raw, bytes: st.size } return { path: raw, bytes: getDirectorySizeBytes(dir) } } catch { return { path: raw, bytes: 0, missing: true as const } } }) return ok({ items }) }) tracedHandle( 'fs:write-job-csv', (_e, payload: { taskId: string; rows: JobCsvRow[] }) => { try { const sharedDir = configStore.get('sharedDir') as string const csvPath = writeJobCsv(sharedDir, payload.taskId, payload.rows || []) return ok({ path: csvPath }) } catch (err) { return fail(CS_FAIL, err instanceof Error ? err.message : String(err)) } } ) tracedHandle('fs:parse-soon', (_e, filePath: string) => { try { const soonPath = String(filePath || '').trim() if (!soonPath) return fail(CS_FAIL, '模板路径为空') if (!fs.existsSync(soonPath)) return fail(CS_FAIL, '模板文件不存在') const raw = JSON.parse(fs.readFileSync(soonPath, 'utf8')) as Record return ok(parseSoonTemplate(soonPath, raw)) } catch (err) { log.error('fs:parse-soon', err) return fail(CS_FAIL, err instanceof Error ? err.message : String(err)) } }) tracedHandle('config:get', () => { const payload: { sharedDir: string templateDir: string traceEnabled: boolean lastPrinterStatus?: PrinterStatusSnapshot dllInitialized: boolean } = { sharedDir: configStore.get('sharedDir'), templateDir: configStore.get('templateDir'), traceEnabled: configStore.get('traceEnabled', true), lastPrinterStatus: configStore.get('lastPrinterStatus'), dllInitialized: mainAppState.initialized } return ok(payload) }) tracedHandle('config:set', (_e, patch: Record) => { Object.entries(patch).forEach(([k, v]) => { if (k === 'traceEnabled' || k === 'dllTraceEnabled') { configStore.set('traceEnabled', parseBool(v)) return } configStore.set(k, v as string) }) return ok(configStore.store) }) tracedHandle('shell:open-path', (_e, target: string) => { const dir = target || (configStore.get('templateDir') as string) fs.mkdirSync(dir, { recursive: true }) shell.openPath(dir) return ok() }) tracedHandle('design:open', async () => { const r = await openDesignApp(getDesignAppPath()) if (!r.ok) return fail(CS_FAIL, r.message) return ok() }) tracedHandle('dll:reject-available', async () => { const dll = await loadDllModule() return ok({ available: dll.isRejectApiAvailable() }) }) } export async function handleBeforeQuit(): Promise { const dll = isDllInitAttempted() ? await loadDllModule().catch(() => null) : null const shouldCancel = mainAppState.mode === 'distributing' && !!mainAppState.activeJobId && isJobPollActive() && !!dll?.isCancelApiAvailable() const cancelJobId = mainAppState.activeJobId stopAllPolls() if (shouldCancel && cancelJobId && dll) { try { dll.dllAdminJobCancel(cancelJobId) } catch (e) { log.warn('before-quit cancel', e) } } mainAppState.mode = 'ready' mainAppState.activeJobId = '' }