Files
SoonMachine/app/src/main/ipc/register-handlers.ts
T
24kycj 574cb353e8 完善分发与收集任务全流程
- RestJobEx 对齐 task_id,path_file 提交目录路径
- 运行页进度仅跟接口轮询,失败态停留 page3 UI
- 完成态卡位续做,USB/任务轮询生命周期优化
- 拆分 DLL 加载、任务 staging 与提交前校验
- 移除 mock 与冗余样式,补充 native 依赖

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 09:43:24 +08:00

399 lines
13 KiB
TypeScript

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<T>(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<string, unknown> {
try {
const p = JSON.parse(json) as Record<string, unknown>
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<string, unknown>
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<string, unknown>) => {
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<void> {
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 = ''
}