完善分发与收集任务全流程
- RestJobEx 对齐 task_id,path_file 提交目录路径 - 运行页进度仅跟接口轮询,失败态停留 page3 UI - 完成态卡位续做,USB/任务轮询生命周期优化 - 拆分 DLL 加载、任务 staging 与提交前校验 - 移除 mock 与冗余样式,补充 native 依赖 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+48
-18
@@ -4,14 +4,29 @@ import { join } from 'path'
|
||||
app.commandLine.appendSwitch('disable-gpu-shader-disk-cache')
|
||||
|
||||
import log from 'electron-log'
|
||||
|
||||
if (app.isPackaged) {
|
||||
app.disableHardwareAcceleration()
|
||||
}
|
||||
|
||||
const gotSingleInstanceLock = app.requestSingleInstanceLock()
|
||||
if (!gotSingleInstanceLock) {
|
||||
app.quit()
|
||||
}
|
||||
|
||||
import { suppressKnownDllStderr } from './utils/suppress-dll-stderr'
|
||||
import { loadAppFileConfig } from './services/app-config'
|
||||
import { migrateTraceConfig, setTraceWebContents } from './utils/trace-bridge'
|
||||
import { setupNativeWorkingDir } from './services/native-path'
|
||||
import { configStore } from './services/config-store'
|
||||
|
||||
suppressKnownDllStderr()
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
log.error('uncaughtException', err)
|
||||
dialog.showErrorBox('程序异常', err instanceof Error ? err.message : String(err))
|
||||
})
|
||||
|
||||
import { registerIpcHandlers, handleBeforeQuit } from './ipc/register-handlers'
|
||||
import { ensureDllInitialized } from './services/dll-bootstrap'
|
||||
import { setPollMainWindow } from './services/poll-manager'
|
||||
import { DESIGN_WIDTH, DESIGN_HEIGHT, contentHeightForWidth } from '@shared/viewport'
|
||||
|
||||
@@ -19,7 +34,13 @@ let mainWindow: BrowserWindow | null = null
|
||||
|
||||
const MIN_CONTENT_WIDTH = 960
|
||||
|
||||
/** 默认内容区:约 85% 工作区宽,高 720:360 */
|
||||
function focusMainWindow(): void {
|
||||
if (!mainWindow) return
|
||||
if (mainWindow.isMinimized()) mainWindow.restore()
|
||||
mainWindow.show()
|
||||
mainWindow.focus()
|
||||
}
|
||||
|
||||
function getDefaultWindowSize(): { width: number; height: number } {
|
||||
const { width: sw, height: sh } = screen.getPrimaryDisplay().workAreaSize
|
||||
let w = Math.max(1280, Math.min(Math.floor(sw * 0.85), 1600))
|
||||
@@ -71,7 +92,6 @@ function createWindow(): void {
|
||||
}
|
||||
})
|
||||
|
||||
// 内容区 720:360,与 useScale 一致
|
||||
mainWindow.on('resize', () => {
|
||||
if (!mainWindow) return
|
||||
const [cw, ch] = mainWindow.getContentSize()
|
||||
@@ -86,6 +106,14 @@ function createWindow(): void {
|
||||
mainWindow = null
|
||||
})
|
||||
|
||||
mainWindow.webContents.on('render-process-gone', (_event, details) => {
|
||||
log.error('render-process-gone', details)
|
||||
dialog.showErrorBox(
|
||||
'界面进程异常退出',
|
||||
`reason=${details.reason} exitCode=${details.exitCode}\n请查看 %APPDATA%\\cardsoon-machine\\logs\\main.log`
|
||||
)
|
||||
})
|
||||
|
||||
if (process.env.ELECTRON_RENDERER_URL) {
|
||||
mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL)
|
||||
} else {
|
||||
@@ -93,24 +121,26 @@ function createWindow(): void {
|
||||
}
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
try {
|
||||
if (app.isPackaged) {
|
||||
configStore.set('skipDllInit', false)
|
||||
} else {
|
||||
const withDll =
|
||||
process.argv.includes('--with-dll') ||
|
||||
process.argv.includes('--no-skip-dll-init')
|
||||
configStore.set('skipDllInit', !withDll)
|
||||
if (!withDll) {
|
||||
log.info('skipDllInit enabled (dev default); use npm run dev:dll to load workDll')
|
||||
}
|
||||
}
|
||||
if (gotSingleInstanceLock) {
|
||||
app.on('second-instance', () => {
|
||||
focusMainWindow()
|
||||
})
|
||||
}
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
if (!gotSingleInstanceLock) return
|
||||
|
||||
try {
|
||||
migrateTraceConfig()
|
||||
loadAppFileConfig()
|
||||
setupNativeWorkingDir()
|
||||
log.info('app startup', { packaged: app.isPackaged, execPath: process.execPath })
|
||||
registerIpcHandlers()
|
||||
try {
|
||||
const r = await ensureDllInitialized()
|
||||
if (r.warning) log.warn(r.warning)
|
||||
} catch (e) {
|
||||
log.error('startup DLL init failed', e)
|
||||
}
|
||||
createWindow()
|
||||
if (!app.isPackaged) {
|
||||
globalShortcut.register('CommandOrControl+Shift+I', () => {
|
||||
|
||||
@@ -1,34 +1,19 @@
|
||||
import { app, dialog, shell } from 'electron'
|
||||
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 {
|
||||
startJobPoll,
|
||||
startUsbPoll,
|
||||
stopAllPolls,
|
||||
stopJobPoll,
|
||||
stopUsbPoll,
|
||||
getPollMainWindow
|
||||
} from '../services/poll-manager'
|
||||
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 {
|
||||
dllAdminJobCancel,
|
||||
dllCopyFromUsb,
|
||||
dllGetPrinterErrorStr,
|
||||
dllGetPrinterInfo,
|
||||
dllInit,
|
||||
dllPrinterReject,
|
||||
dllPrinterReset,
|
||||
dllRestJobEx,
|
||||
isCancelApiAvailable,
|
||||
isRejectApiAvailable
|
||||
} from '../services/work-dll.service'
|
||||
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) {
|
||||
@@ -39,63 +24,49 @@ function fail(code: number, message: string) {
|
||||
return { ok: false as const, code, message }
|
||||
}
|
||||
|
||||
let dllInitAttempted = false
|
||||
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', (_e, params) => {
|
||||
if (dllInitAttempted) {
|
||||
return ok({
|
||||
skipped: true,
|
||||
printerReady: false,
|
||||
warning: '已初始化,跳过重复 Init'
|
||||
})
|
||||
}
|
||||
tracedHandle('dll:init', async (_e, params) => {
|
||||
stopAllPolls()
|
||||
try {
|
||||
const sharedDir = params?.sharedDir || (configStore.get('sharedDir') as string)
|
||||
fs.mkdirSync(sharedDir, { recursive: true })
|
||||
const code = dllInit({
|
||||
sharedDir,
|
||||
keepCombinedImage: params?.keepCombinedImage,
|
||||
stopOnFailure: params?.stopOnFailure,
|
||||
cleanTaskFile: params?.cleanTaskFile,
|
||||
autoRetryTimes: params?.autoRetryTimes,
|
||||
rejectConfig: params?.rejectConfig,
|
||||
logLevel: params?.logLevel,
|
||||
outBack: params?.outBack
|
||||
})
|
||||
dllInitAttempted = true
|
||||
mainAppState.initialized = true
|
||||
configStore.set('sharedDir', sharedDir)
|
||||
if (code === CS_OK) {
|
||||
return ok({ code, printerReady: true })
|
||||
}
|
||||
return ok({
|
||||
code,
|
||||
printerReady: false,
|
||||
warning: '打印机未连接或驱动未就绪,界面可浏览,接好设备后可在设置中重试 Init'
|
||||
})
|
||||
const r = await ensureDllInitialized(params)
|
||||
return ok({ code: r.code, warning: r.warning })
|
||||
} catch (err) {
|
||||
mainAppState.initialized = false
|
||||
return fail(CS_FAIL, String(err))
|
||||
}
|
||||
})
|
||||
|
||||
tracedHandle('dll:printer-info', () => {
|
||||
tracedHandle('dll:printer-info', async () => {
|
||||
try {
|
||||
assertReady()
|
||||
const r = dllGetPrinterInfo()
|
||||
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 ? '未连接打印机' : `GetPrinterInfo code=${r.code}`
|
||||
liveError: r.code <= 0 ? '未连接打印机' : `GetPrinterInfoEx code=${r.code}`
|
||||
})
|
||||
}
|
||||
return fail(0, '未连接打印机')
|
||||
@@ -112,77 +83,139 @@ export function registerIpcHandlers(): void {
|
||||
}
|
||||
})
|
||||
|
||||
tracedHandle('dll:printer-reset', () => {
|
||||
tracedHandle('dll:printer-reset', async () => {
|
||||
try {
|
||||
assertReady()
|
||||
const code = dllPrinterReset()
|
||||
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', () => {
|
||||
tracedHandle('dll:printer-reject', async () => {
|
||||
try {
|
||||
assertReady()
|
||||
if (!isRejectApiAvailable()) return fail(CS_FAIL, 'REJECT_API_UNAVAILABLE')
|
||||
const code = dllPrinterReject()
|
||||
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', (_e, errorNo?: number) => {
|
||||
tracedHandle('dll:printer-error-str', async (_e, errorNo?: number) => {
|
||||
if (!mainAppState.initialized) return ok({ text: '' })
|
||||
try {
|
||||
assertReady()
|
||||
return ok({ text: dllGetPrinterErrorStr(errorNo ?? -1) })
|
||||
const dll = await loadDllModule()
|
||||
return ok({ text: dll.dllGetPrinterErrorStr(errorNo ?? -1) })
|
||||
} catch (err) {
|
||||
return fail(CS_FAIL, String(err))
|
||||
log.warn('dll:printer-error-str', err)
|
||||
return ok({ text: '' })
|
||||
}
|
||||
})
|
||||
|
||||
tracedHandle('dll:job-create', (_e, json: string) => {
|
||||
tracedHandle('dll:job-create', async (_e, json: string, opts?: { resubmit?: boolean }) => {
|
||||
try {
|
||||
assertReady()
|
||||
assertNotBusy()
|
||||
const r = dllRestJobEx(json)
|
||||
if (r.code !== CS_OK) return fail(r.code, 'RestJobEx 失败')
|
||||
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) {
|
||||
if (String(err).includes('BUSY')) return fail(CS_FAIL, '已有任务在执行')
|
||||
return fail(CS_FAIL, String(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', (_e, jobId: string) => {
|
||||
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 {
|
||||
assertReady()
|
||||
const id = jobId || mainAppState.activeJobId
|
||||
stopJobPoll(true)
|
||||
let code = CS_OK
|
||||
if (isCancelApiAvailable()) {
|
||||
code = dllAdminJobCancel(id)
|
||||
}
|
||||
mainAppState.mode = 'ready'
|
||||
mainAppState.activeJobId = ''
|
||||
const dll = await loadDllModule()
|
||||
if (!dll.isCancelApiAvailable()) return ok()
|
||||
const code = dll.dllAdminJobCancel(id)
|
||||
return code === CS_OK ? ok() : fail(code, '取消失败')
|
||||
} catch (err) {
|
||||
return fail(CS_FAIL, String(err))
|
||||
log.warn('dll:job-cancel', err)
|
||||
return ok()
|
||||
}
|
||||
})
|
||||
|
||||
tracedHandle('dll:usb-copy', (_e, req: { destFolder: string; cardOutput: number }) => {
|
||||
tracedHandle('dll:usb-copy', async (_e, req: { destFolder: string; cardOutput: number; resubmit?: boolean }) => {
|
||||
try {
|
||||
assertReady()
|
||||
assertNotBusy()
|
||||
const code = dllCopyFromUsb(req.destFolder, req.cardOutput)
|
||||
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) {
|
||||
return fail(code, '可能已有任务在执行')
|
||||
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, '已有任务在执行')
|
||||
@@ -195,8 +228,8 @@ export function registerIpcHandlers(): void {
|
||||
return ok()
|
||||
})
|
||||
|
||||
tracedHandle('poll:job-stop', () => {
|
||||
stopJobPoll(true)
|
||||
tracedHandle('poll:job-stop', (_e, opts?: { resetMode?: boolean }) => {
|
||||
stopJobPoll(opts?.resetMode !== false)
|
||||
return ok()
|
||||
})
|
||||
|
||||
@@ -205,9 +238,27 @@ export function registerIpcHandlers(): void {
|
||||
return ok()
|
||||
})
|
||||
|
||||
tracedHandle('poll:usb-stop', () => {
|
||||
stopUsbPoll()
|
||||
mainAppState.mode = 'ready'
|
||||
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()
|
||||
})
|
||||
|
||||
@@ -253,6 +304,19 @@ export function registerIpcHandlers(): void {
|
||||
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()
|
||||
@@ -272,26 +336,19 @@ export function registerIpcHandlers(): void {
|
||||
templateDir: string
|
||||
traceEnabled: boolean
|
||||
lastPrinterStatus?: PrinterStatusSnapshot
|
||||
skipDllInit?: boolean
|
||||
dllInitialized: boolean
|
||||
} = {
|
||||
sharedDir: configStore.get('sharedDir'),
|
||||
templateDir: configStore.get('templateDir'),
|
||||
traceEnabled: configStore.get('traceEnabled', true),
|
||||
lastPrinterStatus: configStore.get('lastPrinterStatus')
|
||||
}
|
||||
if (!app.isPackaged) {
|
||||
payload.skipDllInit = configStore.get('skipDllInit', false)
|
||||
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 === 'skipDllInit') {
|
||||
if (app.isPackaged) return
|
||||
configStore.set(k, parseBool(v))
|
||||
return
|
||||
}
|
||||
if (k === 'traceEnabled' || k === 'dllTraceEnabled') {
|
||||
configStore.set('traceEnabled', parseBool(v))
|
||||
return
|
||||
@@ -314,19 +371,24 @@ export function registerIpcHandlers(): void {
|
||||
return ok()
|
||||
})
|
||||
|
||||
tracedHandle('dll:reject-available', () => ok({ available: isRejectApiAvailable() }))
|
||||
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 &&
|
||||
isCancelApiAvailable()
|
||||
isJobPollActive() &&
|
||||
!!dll?.isCancelApiAvailable()
|
||||
const cancelJobId = mainAppState.activeJobId
|
||||
stopAllPolls()
|
||||
if (shouldCancel && cancelJobId) {
|
||||
if (shouldCancel && cancelJobId && dll) {
|
||||
try {
|
||||
dllAdminJobCancel(cancelJobId)
|
||||
dll.dllAdminJobCancel(cancelJobId)
|
||||
} catch (e) {
|
||||
log.warn('before-quit cancel', e)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { getProcessExecDir } from './native-path'
|
||||
|
||||
export const APP_CONFIG_FILENAME = 'cardsoon.config.json'
|
||||
|
||||
/** 与 cardsoon.config.json 键名一致,后续配置在此扩展 */
|
||||
export interface AppFileConfig {
|
||||
designAppPath: string
|
||||
}
|
||||
@@ -16,7 +15,6 @@ const defaults: AppFileConfig = {
|
||||
}
|
||||
|
||||
let cached: AppFileConfig | null = null
|
||||
let loadedFrom = ''
|
||||
|
||||
function bundledConfigPath(): string {
|
||||
if (app.isPackaged) {
|
||||
@@ -46,7 +44,6 @@ export function loadAppFileConfig(): AppFileConfig {
|
||||
if (!fs.existsSync(filePath)) continue
|
||||
try {
|
||||
cached = parseConfigFile(filePath)
|
||||
loadedFrom = filePath
|
||||
log.info(`Loaded ${APP_CONFIG_FILENAME} from ${filePath}`)
|
||||
return cached
|
||||
} catch (e) {
|
||||
@@ -55,18 +52,12 @@ export function loadAppFileConfig(): AppFileConfig {
|
||||
}
|
||||
|
||||
cached = { ...defaults }
|
||||
loadedFrom = ''
|
||||
log.warn(
|
||||
`${APP_CONFIG_FILENAME} not found (checked: ${configSearchPaths().join(', ')}), using defaults`
|
||||
)
|
||||
return cached
|
||||
}
|
||||
|
||||
export function getAppConfigLoadedPath(): string {
|
||||
loadAppFileConfig()
|
||||
return loadedFrom
|
||||
}
|
||||
|
||||
export function getDesignAppPath(): string {
|
||||
return loadAppFileConfig().designAppPath
|
||||
}
|
||||
|
||||
@@ -6,23 +6,15 @@ import type { PrinterStatusSnapshot } from '@shared/printer-info'
|
||||
interface AppConfig {
|
||||
sharedDir: string
|
||||
templateDir: string
|
||||
/** G2 门禁 false:启动即 SAPI_Init;仅调试可改 true */
|
||||
skipDllInit: boolean
|
||||
/** true:IPC/DLL 等调用输出到 DevTools 控制台 */
|
||||
traceEnabled: boolean
|
||||
/** 上次成功的 GetPrinterInfo 解析结果,供离线/失败时展示 */
|
||||
lastPrinterStatus?: PrinterStatusSnapshot
|
||||
}
|
||||
|
||||
const defaultShared = path.join('C:', 'PrintTasks')
|
||||
|
||||
export const configStore = new Store<AppConfig>({
|
||||
name: 'cardsoon-config',
|
||||
defaults: {
|
||||
sharedDir: defaultShared,
|
||||
sharedDir: path.join('C:', 'PrintTasks'),
|
||||
templateDir: path.join(app.getPath('userData'), 'Cardsoon', 'templates'),
|
||||
// 正式版始终 Init;仅开发时可通过 --skip-dll-init 临时跳过
|
||||
skipDllInit: false,
|
||||
traceEnabled: true
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import fs from 'fs'
|
||||
import log from 'electron-log'
|
||||
import { CS_OK } from '../constants'
|
||||
import { mainAppState } from './app-state'
|
||||
import { configStore } from './config-store'
|
||||
import { loadDllModule } from './dll-loader'
|
||||
import type { InitParams } from './work-dll.service'
|
||||
|
||||
let dllInitAttempted = false
|
||||
|
||||
export function isDllInitAttempted(): boolean {
|
||||
return dllInitAttempted
|
||||
}
|
||||
|
||||
export async function ensureDllInitialized(
|
||||
params?: Partial<InitParams>
|
||||
): Promise<{ code: number; warning?: string }> {
|
||||
if (dllInitAttempted) {
|
||||
return { code: CS_OK }
|
||||
}
|
||||
const sharedDir =
|
||||
params?.sharedDir || (configStore.get('sharedDir') as string) || 'C:\\PrintTasks'
|
||||
try {
|
||||
const dll = await loadDllModule()
|
||||
fs.mkdirSync(sharedDir, { recursive: true })
|
||||
const code = dll.dllInit({
|
||||
sharedDir,
|
||||
keepCombinedImage: params?.keepCombinedImage,
|
||||
stopOnFailure: params?.stopOnFailure,
|
||||
cleanTaskFile: params?.cleanTaskFile,
|
||||
autoRetryTimes: params?.autoRetryTimes,
|
||||
rejectConfig: params?.rejectConfig,
|
||||
logLevel: params?.logLevel,
|
||||
outBack: params?.outBack
|
||||
})
|
||||
dllInitAttempted = true
|
||||
mainAppState.initialized = true
|
||||
configStore.set('sharedDir', sharedDir)
|
||||
log.info('DLL initialized', { sharedDir, code })
|
||||
if (code === CS_OK) return { code }
|
||||
return {
|
||||
code,
|
||||
warning: '打印机未连接或驱动未就绪,可继续配置任务,接好设备后重启应用'
|
||||
}
|
||||
} catch (err) {
|
||||
mainAppState.initialized = false
|
||||
dllInitAttempted = false
|
||||
log.error('DLL init failed', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { setupNativeWorkingDir } from './native-path'
|
||||
|
||||
type DllModule = typeof import('./work-dll.service')
|
||||
|
||||
let dllMod: DllModule | null = null
|
||||
let nativeReady = false
|
||||
|
||||
function ensureNativeEnv(): void {
|
||||
if (nativeReady) return
|
||||
setupNativeWorkingDir()
|
||||
nativeReady = true
|
||||
}
|
||||
|
||||
export async function loadDllModule(): Promise<DllModule> {
|
||||
ensureNativeEnv()
|
||||
if (!dllMod) {
|
||||
dllMod = await import('./work-dll.service')
|
||||
}
|
||||
return dllMod
|
||||
}
|
||||
@@ -88,5 +88,10 @@ export function setupNativeWorkingDir(): void {
|
||||
if (!process.env.PATH?.toLowerCase().includes(nativeDir.toLowerCase())) {
|
||||
process.env.PATH = `${pathHead}${path.delimiter}${process.env.PATH || ''}`
|
||||
}
|
||||
log.debug(`Native DLL search path: ${nativeDir}; cwd kept at ${process.cwd()}`)
|
||||
try {
|
||||
process.chdir(execDir)
|
||||
} catch (e) {
|
||||
log.warn(`chdir to ${execDir} failed`, e)
|
||||
}
|
||||
log.debug(`Native DLL search path: ${nativeDir}; cwd=${process.cwd()}`)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { shell } from 'electron'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
export function validateDesignAppPath(exePath: string): { ok: true } | { ok: false; message: string } {
|
||||
function validateDesignAppPath(exePath: string): { ok: true } | { ok: false; message: string } {
|
||||
const p = exePath.trim()
|
||||
if (!p) {
|
||||
return { ok: false, message: '请在 cardsoon.config.json 中配置 designAppPath' }
|
||||
@@ -14,7 +14,6 @@ export function validateDesignAppPath(exePath: string): { ok: true } | { ok: fal
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
/** 由系统启动外部程序;空字符串表示成功,非空为失败原因 */
|
||||
export async function openDesignApp(
|
||||
exePath: string
|
||||
): Promise<{ ok: true } | { ok: false; message: string }> {
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import { BrowserWindow } from 'electron'
|
||||
import log from 'electron-log'
|
||||
import { POLL_INTERVAL_MS } from '../constants'
|
||||
import { POLL_INTERVAL_MS, CS_OK } from '../constants'
|
||||
import {
|
||||
USB_TASK_COMPLETED,
|
||||
USB_TASK_FAILED,
|
||||
clampUsbCopyProgress,
|
||||
usbTaskStatusHint
|
||||
} from '@shared/usb-copy-state'
|
||||
import { mainAppState } from './app-state'
|
||||
import { emitTrace } from '../utils/trace-bridge'
|
||||
import { dllGetJobStateById, dllGetUsbCopyState } from './work-dll.service'
|
||||
import { loadDllModule } from './dll-loader'
|
||||
|
||||
let jobTimer: ReturnType<typeof setInterval> | null = null
|
||||
let usbTimer: ReturnType<typeof setInterval> | null = null
|
||||
let cardTimer: ReturnType<typeof setInterval> | null = null
|
||||
let usbPollGen = 0
|
||||
let jobId = ''
|
||||
let mainWindow: BrowserWindow | null = null
|
||||
|
||||
@@ -35,84 +43,145 @@ export function stopJobPoll(resetMode = false): void {
|
||||
}
|
||||
}
|
||||
|
||||
export function stopUsbPoll(): void {
|
||||
export function stopUsbPoll(resetMode = false): void {
|
||||
usbPollGen += 1
|
||||
if (usbTimer) {
|
||||
clearInterval(usbTimer)
|
||||
usbTimer = null
|
||||
}
|
||||
if (resetMode && mainAppState.mode === 'usbCopying') {
|
||||
mainAppState.mode = 'ready'
|
||||
}
|
||||
}
|
||||
|
||||
export function stopCardPositionPoll(): void {
|
||||
if (cardTimer) {
|
||||
clearInterval(cardTimer)
|
||||
cardTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
export function stopAllPolls(): void {
|
||||
stopJobPoll(true)
|
||||
stopUsbPoll()
|
||||
stopUsbPoll(true)
|
||||
stopCardPositionPoll()
|
||||
}
|
||||
|
||||
export function startJobPoll(id: string): void {
|
||||
stopJobPoll(false)
|
||||
jobId = id
|
||||
jobTimer = setInterval(() => {
|
||||
try {
|
||||
const r = dllGetJobStateById(jobId)
|
||||
const failed = r.jobState === 4
|
||||
const cancelled = r.jobState === 6
|
||||
const finished = r.jobState === 100
|
||||
const terminal = failed || cancelled
|
||||
const tick = {
|
||||
jobId,
|
||||
queryErrorCode: r.queryErrorCode,
|
||||
jobState: r.jobState,
|
||||
progress: r.progress,
|
||||
terminal,
|
||||
failed,
|
||||
cancelled,
|
||||
finished
|
||||
}
|
||||
emitTrace('[poll] job:poll-tick', tick)
|
||||
send('job:poll-tick', tick)
|
||||
if (r.queryErrorCode !== 0) {
|
||||
log.warn('GetJobStateById query failed', r.queryErrorCode)
|
||||
stopJobPoll(true)
|
||||
return
|
||||
}
|
||||
if (failed || cancelled) {
|
||||
void (async () => {
|
||||
try {
|
||||
const dll = await loadDllModule()
|
||||
const r = dll.dllGetJobStateById(jobId)
|
||||
const failed = r.jobState === 4
|
||||
const cancelled = r.jobState === 6
|
||||
const finished = r.jobState === 100
|
||||
const terminal = failed || cancelled
|
||||
const tick = {
|
||||
jobId,
|
||||
queryErrorCode: r.queryErrorCode,
|
||||
jobState: r.jobState,
|
||||
progress: r.progress,
|
||||
terminal,
|
||||
failed,
|
||||
cancelled,
|
||||
finished
|
||||
}
|
||||
emitTrace('[poll] job:poll-tick', tick)
|
||||
send('job:poll-tick', tick)
|
||||
if (r.queryErrorCode !== 0) {
|
||||
log.warn('GetJobStateById query failed', r.queryErrorCode)
|
||||
return
|
||||
}
|
||||
if (failed || cancelled) {
|
||||
stopJobPoll(true)
|
||||
} else if (finished) {
|
||||
stopJobPoll(false)
|
||||
}
|
||||
} catch (e) {
|
||||
log.error('job poll error', e)
|
||||
stopJobPoll(true)
|
||||
}
|
||||
} catch (e) {
|
||||
log.error('job poll error', e)
|
||||
stopJobPoll(true)
|
||||
}
|
||||
})()
|
||||
}, POLL_INTERVAL_MS)
|
||||
}
|
||||
|
||||
export function startUsbPoll(): void {
|
||||
stopUsbPoll()
|
||||
stopUsbPoll(false)
|
||||
const gen = usbPollGen
|
||||
void pollUsbOnce(gen).catch((e) => log.error('usb poll error', e))
|
||||
usbTimer = setInterval(() => {
|
||||
try {
|
||||
const r = dllGetUsbCopyState()
|
||||
const failed = r.taskStatus === 3
|
||||
const success = r.taskStatus === 2
|
||||
const terminal = failed || success
|
||||
const tick = {
|
||||
taskStatus: r.taskStatus,
|
||||
progress: r.progress,
|
||||
terminal,
|
||||
failed,
|
||||
success
|
||||
}
|
||||
emitTrace('[poll] usb:poll-tick', tick)
|
||||
send('usb:poll-tick', tick)
|
||||
if (terminal) {
|
||||
stopUsbPoll()
|
||||
mainAppState.mode = 'ready'
|
||||
}
|
||||
} catch (e) {
|
||||
void pollUsbOnce(gen).catch((e) => {
|
||||
log.error('usb poll error', e)
|
||||
stopUsbPoll()
|
||||
mainAppState.mode = 'ready'
|
||||
}
|
||||
stopUsbPoll(true)
|
||||
})
|
||||
}, POLL_INTERVAL_MS)
|
||||
}
|
||||
|
||||
async function pollUsbOnce(gen: number): Promise<void> {
|
||||
if (gen !== usbPollGen) return
|
||||
const dll = await loadDllModule()
|
||||
if (gen !== usbPollGen) return
|
||||
const r = dll.dllGetUsbCopyState()
|
||||
if (gen !== usbPollGen) return
|
||||
const copyProgress = clampUsbCopyProgress(r.progress)
|
||||
const failed = r.taskStatus === USB_TASK_FAILED
|
||||
const success = r.taskStatus === USB_TASK_COMPLETED
|
||||
const terminal = failed || success
|
||||
let errorMessage = ''
|
||||
if (failed) {
|
||||
const errText = dll.dllGetPrinterErrorStr(-1)
|
||||
errorMessage = errText || usbTaskStatusHint(USB_TASK_FAILED)
|
||||
}
|
||||
const tick = {
|
||||
queryCode: r.queryCode,
|
||||
taskStatus: r.taskStatus,
|
||||
progress: copyProgress,
|
||||
terminal,
|
||||
failed,
|
||||
success,
|
||||
errorMessage
|
||||
}
|
||||
emitTrace('[poll] usb:poll-tick', tick)
|
||||
if (gen !== usbPollGen) return
|
||||
send('usb:poll-tick', tick)
|
||||
if (r.queryCode !== CS_OK) {
|
||||
log.warn('GetUsbCopyState query failed', r.queryCode)
|
||||
return
|
||||
}
|
||||
if (terminal) {
|
||||
stopUsbPoll(false)
|
||||
}
|
||||
}
|
||||
|
||||
export function startCardPositionPoll(): void {
|
||||
stopCardPositionPoll()
|
||||
cardTimer = setInterval(() => {
|
||||
void (async () => {
|
||||
try {
|
||||
const dll = await loadDllModule()
|
||||
if (!dll.isCardPositionApiAvailable()) return
|
||||
const r = dll.dllGetPrinterCardPosition()
|
||||
const tick = { queryCode: r.queryCode, position: r.position }
|
||||
emitTrace('[poll] card:position-tick', tick)
|
||||
send('card:position-tick', tick)
|
||||
} catch (e) {
|
||||
log.error('card position poll error', e)
|
||||
}
|
||||
})()
|
||||
}, POLL_INTERVAL_MS)
|
||||
}
|
||||
|
||||
export function getActiveJobId(): string {
|
||||
return jobId
|
||||
}
|
||||
|
||||
export function isJobPollActive(): boolean {
|
||||
return jobTimer !== null
|
||||
}
|
||||
|
||||
export function isUsbPollActive(): boolean {
|
||||
return usbTimer !== null
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import path from 'path'
|
||||
import koffi from 'koffi'
|
||||
import log from 'electron-log'
|
||||
import { CS_OK, JOB_ID_BUF_SIZE, LOG_FATAL_FLAG } from '../constants'
|
||||
import { emitTrace, isTraceEnabled } from '../utils/trace-bridge'
|
||||
import { getNativeDir } from './native-path'
|
||||
@@ -22,6 +23,10 @@ let SAPI_Init: any = null
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let SAPI_GetPrinterInfo: any = null
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let SAPI_GetPrinterInfoEx: any = null
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let SAPI_FreePrinterInfo: any = null
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let SAPI_GetPrinterErrorStr: any = null
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let SAPI_RestJobEx: any = null
|
||||
@@ -37,8 +42,18 @@ let SAPI_GetUsbCopyState: any = null
|
||||
let SAPI_PrinterResetprinter: any = null
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let SAPI_PrinterMovetoreject: any = null
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let SAPI_PrinterMovetousbreader: any = null
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let SAPI_GetPrinterCardPosition: any = null
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let SAPI_UploadFile: any = null
|
||||
let hasRejectApi = false
|
||||
let hasCardPositionApi = false
|
||||
let hasCancelApi = false
|
||||
let hasUploadApi = false
|
||||
let hasPrinterInfoEx = false
|
||||
let hasUsbReaderApi = false
|
||||
let loggedCancelMissing = false
|
||||
let loggedRejectMissing = false
|
||||
|
||||
@@ -63,6 +78,31 @@ function traceCall<T>(name: string, args: Record<string, unknown> | undefined, f
|
||||
}
|
||||
}
|
||||
|
||||
function readPrinterJsonFromOutPtr(len: number, outPtr: Buffer): { code: number; json?: Record<string, unknown> } {
|
||||
if (len <= 0) return { code: len }
|
||||
const ptr = koffi.decode(outPtr, 0, 'void *') as number
|
||||
if (!ptr) return { code: len }
|
||||
try {
|
||||
const jsonStr = koffi.decode(ptr, 'char', len) as string
|
||||
if (!jsonStr?.trim()) return { code: len }
|
||||
try {
|
||||
return { code: len, json: JSON.parse(jsonStr) as Record<string, unknown> }
|
||||
} catch {
|
||||
return { code: len }
|
||||
}
|
||||
} finally {
|
||||
if (SAPI_FreePrinterInfo) {
|
||||
try {
|
||||
SAPI_FreePrinterInfo(ptr)
|
||||
} catch (e) {
|
||||
log.warn('SAPI_FreePrinterInfo', e)
|
||||
}
|
||||
} else {
|
||||
koffi.free(ptr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadLibrary(): void {
|
||||
if (lib) return
|
||||
const dllPath = path.join(getNativeDir(), 'workDll.dll')
|
||||
@@ -79,6 +119,25 @@ function loadLibrary(): void {
|
||||
SAPI_GetUsbCopyState = lib.func('int __stdcall SAPI_GetUsbCopyState(_Out_ int *, _Out_ int *)')
|
||||
SAPI_PrinterResetprinter = lib.func('int __stdcall SAPI_PrinterResetprinter()')
|
||||
|
||||
try {
|
||||
SAPI_GetPrinterInfoEx = lib.func('int __stdcall SAPI_GetPrinterInfoEx(_Out_ void **)')
|
||||
SAPI_FreePrinterInfo = lib.func('void __stdcall SAPI_FreePrinterInfo(void *)')
|
||||
hasPrinterInfoEx = true
|
||||
} catch {
|
||||
SAPI_GetPrinterInfoEx = null
|
||||
SAPI_FreePrinterInfo = null
|
||||
hasPrinterInfoEx = false
|
||||
}
|
||||
|
||||
try {
|
||||
SAPI_UploadFile = lib.func('int __stdcall SAPI_UploadFile(str, str, str)')
|
||||
hasUploadApi = true
|
||||
} catch {
|
||||
SAPI_UploadFile = null
|
||||
hasUploadApi = false
|
||||
log.warn('SAPI_UploadFile not in workDll')
|
||||
}
|
||||
|
||||
try {
|
||||
SAPI_AdminJobCancel = lib.func('int __stdcall SAPI_AdminJobCancel(str)')
|
||||
hasCancelApi = true
|
||||
@@ -101,6 +160,33 @@ function loadLibrary(): void {
|
||||
emitTrace('[dll] SAPI_PrinterMovetoreject not in workDll (optional)')
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
SAPI_PrinterMovetousbreader = lib.func('int __stdcall SAPI_PrinterMovetousbreader()')
|
||||
hasUsbReaderApi = true
|
||||
} catch {
|
||||
SAPI_PrinterMovetousbreader = null
|
||||
hasUsbReaderApi = false
|
||||
emitTrace('[dll] SAPI_PrinterMovetousbreader not in workDll (optional)')
|
||||
}
|
||||
|
||||
try {
|
||||
SAPI_GetPrinterCardPosition = lib.func('int __stdcall SAPI_GetPrinterCardPosition(_Out_ int *)')
|
||||
hasCardPositionApi = true
|
||||
} catch {
|
||||
SAPI_GetPrinterCardPosition = null
|
||||
hasCardPositionApi = false
|
||||
emitTrace('[dll] SAPI_GetPrinterCardPosition not in workDll (optional)')
|
||||
}
|
||||
|
||||
log.info('workDll loaded', {
|
||||
upload: hasUploadApi,
|
||||
printerInfoEx: hasPrinterInfoEx,
|
||||
cancel: hasCancelApi,
|
||||
reject: hasRejectApi,
|
||||
usbReader: hasUsbReaderApi,
|
||||
cardPosition: hasCardPositionApi
|
||||
})
|
||||
}
|
||||
|
||||
export function isRejectApiAvailable(): boolean {
|
||||
@@ -113,6 +199,21 @@ export function isCancelApiAvailable(): boolean {
|
||||
return hasCancelApi
|
||||
}
|
||||
|
||||
export function isUploadApiAvailable(): boolean {
|
||||
loadLibrary()
|
||||
return hasUploadApi
|
||||
}
|
||||
|
||||
export function isUsbReaderApiAvailable(): boolean {
|
||||
loadLibrary()
|
||||
return hasUsbReaderApi
|
||||
}
|
||||
|
||||
export function isCardPositionApiAvailable(): boolean {
|
||||
loadLibrary()
|
||||
return hasCardPositionApi
|
||||
}
|
||||
|
||||
export function dllInit(params: InitParams): number {
|
||||
return traceCall(
|
||||
'SAPI_Init',
|
||||
@@ -142,29 +243,33 @@ export function dllInit(params: InitParams): number {
|
||||
)
|
||||
}
|
||||
|
||||
export function dllGetPrinterInfo(): { code: number; json?: Record<string, unknown> } {
|
||||
return traceCall('SAPI_GetPrinterInfo', undefined, () => {
|
||||
function dllGetPrinterInfoInternal(
|
||||
apiName: 'SAPI_GetPrinterInfo' | 'SAPI_GetPrinterInfoEx',
|
||||
fn: (outPtr: Buffer) => number
|
||||
): { code: number; json?: Record<string, unknown> } {
|
||||
return traceCall(apiName, undefined, () => {
|
||||
loadLibrary()
|
||||
const outPtr = koffi.alloc('void *', 8)
|
||||
try {
|
||||
const len = SAPI_GetPrinterInfo!(outPtr) as number
|
||||
if (len <= 0) return { code: len }
|
||||
const ptr = koffi.decode(outPtr, 0, 'void *') as number
|
||||
if (!ptr) return { code: len }
|
||||
const jsonStr = koffi.decode(ptr, 'char', len) as string
|
||||
koffi.free(ptr)
|
||||
if (!jsonStr?.trim()) return { code: len }
|
||||
try {
|
||||
return { code: len, json: JSON.parse(jsonStr) as Record<string, unknown> }
|
||||
} catch {
|
||||
return { code: len }
|
||||
}
|
||||
const len = fn(outPtr) as number
|
||||
return readPrinterJsonFromOutPtr(len, outPtr)
|
||||
} finally {
|
||||
koffi.free(outPtr)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function dllGetPrinterInfo(): { code: number; json?: Record<string, unknown> } {
|
||||
loadLibrary()
|
||||
if (hasPrinterInfoEx && SAPI_GetPrinterInfoEx) {
|
||||
const ex = dllGetPrinterInfoInternal('SAPI_GetPrinterInfoEx', (p) => SAPI_GetPrinterInfoEx!(p))
|
||||
if (ex.json && Object.keys(ex.json).length > 0) {
|
||||
return ex
|
||||
}
|
||||
}
|
||||
return dllGetPrinterInfoInternal('SAPI_GetPrinterInfo', (p) => SAPI_GetPrinterInfo!(p))
|
||||
}
|
||||
|
||||
export function dllGetPrinterErrorStr(errorNo = -1): string {
|
||||
return traceCall('SAPI_GetPrinterErrorStr', { errorNo }, () => {
|
||||
loadLibrary()
|
||||
@@ -173,6 +278,18 @@ export function dllGetPrinterErrorStr(errorNo = -1): string {
|
||||
})
|
||||
}
|
||||
|
||||
export function dllUploadFile(userDir: string, fileName: string, fileText: string): number {
|
||||
return traceCall(
|
||||
'SAPI_UploadFile',
|
||||
{ userDir, fileName, bytes: Buffer.byteLength(fileText ?? '', 'utf8') },
|
||||
() => {
|
||||
loadLibrary()
|
||||
if (!SAPI_UploadFile) throw new Error('UPLOAD_API_UNAVAILABLE')
|
||||
return SAPI_UploadFile(userDir, fileName, fileText) as number
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function dllRestJobEx(json: string): { code: number; jobId: string } {
|
||||
return traceCall('SAPI_RestJobEx', { jsonBytes: Buffer.byteLength(json ?? '', 'utf8') }, () => {
|
||||
loadLibrary()
|
||||
@@ -216,13 +333,22 @@ export function dllCopyFromUsb(destFolder: string, cardOutput: number): number {
|
||||
})
|
||||
}
|
||||
|
||||
export function dllGetUsbCopyState(): { taskStatus: number; progress: number } {
|
||||
export function dllGetUsbCopyState(): {
|
||||
queryCode: number
|
||||
taskStatus: number
|
||||
/** copy_progress 0-100 */
|
||||
progress: number
|
||||
} {
|
||||
return traceCall('SAPI_GetUsbCopyState', undefined, () => {
|
||||
loadLibrary()
|
||||
const taskStatus = [0]
|
||||
const progress = [0]
|
||||
SAPI_GetUsbCopyState!(taskStatus, progress)
|
||||
return { taskStatus: taskStatus[0], progress: progress[0] }
|
||||
const copyProgress = [0]
|
||||
const queryCode = SAPI_GetUsbCopyState!(taskStatus, copyProgress) as number
|
||||
return {
|
||||
queryCode,
|
||||
taskStatus: taskStatus[0],
|
||||
progress: copyProgress[0]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -233,6 +359,14 @@ export function dllPrinterReset(): number {
|
||||
})
|
||||
}
|
||||
|
||||
export function dllPrinterMoveToUsbReader(): number {
|
||||
return traceCall('SAPI_PrinterMovetousbreader', undefined, () => {
|
||||
loadLibrary()
|
||||
if (!SAPI_PrinterMovetousbreader) throw new Error('USB_READER_API_UNAVAILABLE')
|
||||
return SAPI_PrinterMovetousbreader() as number
|
||||
})
|
||||
}
|
||||
|
||||
export function dllPrinterReject(): number {
|
||||
return traceCall('SAPI_PrinterMovetoreject', undefined, () => {
|
||||
loadLibrary()
|
||||
@@ -240,3 +374,13 @@ export function dllPrinterReject(): number {
|
||||
return SAPI_PrinterMovetoreject() as number
|
||||
})
|
||||
}
|
||||
|
||||
export function dllGetPrinterCardPosition(): { queryCode: number; position: number } {
|
||||
return traceCall('SAPI_GetPrinterCardPosition', undefined, () => {
|
||||
loadLibrary()
|
||||
if (!SAPI_GetPrinterCardPosition) throw new Error('CARD_POSITION_API_UNAVAILABLE')
|
||||
const position = [0]
|
||||
const queryCode = SAPI_GetPrinterCardPosition!(position) as number
|
||||
return { queryCode, position: position[0] }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
export interface JobCsvRow {
|
||||
originName: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export function buildSoonCsvText(rows: JobCsvRow[]): string {
|
||||
const line1 = rows.map((r) => r.originName).join(',')
|
||||
const line2 = rows.map((r) => r.value).join(',')
|
||||
return `${line1}\n${line2}`
|
||||
}
|
||||
|
||||
export function writeJobCsv(sharedDir: string, taskId: string, rows: JobCsvRow[]): string {
|
||||
const dir = path.join(sharedDir, taskId)
|
||||
fs.mkdirSync(dir, { recursive: true })
|
||||
const csvPath = path.join(dir, 'temp.csv')
|
||||
fs.writeFileSync(csvPath, buildSoonCsvText(rows), 'utf8')
|
||||
return csvPath
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { pathToFileURL } from 'url'
|
||||
export interface TemplateFieldRow {
|
||||
label: string
|
||||
value: string
|
||||
originName: string
|
||||
}
|
||||
|
||||
export interface ParsedSoonTemplate {
|
||||
@@ -12,6 +13,8 @@ export interface ParsedSoonTemplate {
|
||||
fields: TemplateFieldRow[]
|
||||
}
|
||||
|
||||
const SOON_FIELD_TYPES = new Set([1, 3, 4, 5])
|
||||
|
||||
function pickArray(obj: Record<string, unknown>, key: string): Record<string, unknown>[] {
|
||||
const entry = Object.entries(obj).find(([k]) => k.toLowerCase() === key.toLowerCase())
|
||||
if (!Array.isArray(entry?.[1])) return []
|
||||
@@ -38,18 +41,53 @@ function sideLabel(side: 'front' | 'back'): string {
|
||||
return side === 'front' ? '正面' : '背面'
|
||||
}
|
||||
|
||||
function resolveAssetPath(soonPath: string, ref: string): string {
|
||||
function toImageUrl(soonPath: string, ref: string): string {
|
||||
if (!ref) return ''
|
||||
if (/^(data:|https?:|file:)/i.test(ref)) return ref
|
||||
const clean = ref.replace(/^file:\/\//i, '')
|
||||
const abs = path.isAbsolute(clean) ? clean : path.join(path.dirname(soonPath), clean)
|
||||
return pathToFileURL(abs).href
|
||||
}
|
||||
|
||||
function toFieldLabel(name: string, side: 'front' | 'back'): string {
|
||||
return `${name} [${sideLabel(side)}]`
|
||||
function resolveAssetPath(soonPath: string, ref: string): string {
|
||||
return toImageUrl(soonPath, ref)
|
||||
}
|
||||
|
||||
export function parseSoonTemplate(soonPath: string, raw: Record<string, unknown>): ParsedSoonTemplate {
|
||||
function toFieldLabel(name: string, side: 'front' | 'back'): string {
|
||||
return `${name}[${sideLabel(side)}]`
|
||||
}
|
||||
|
||||
function parseSoonWorkerDisk(soonPath: string, raw: Record<string, unknown>): ParsedSoonTemplate {
|
||||
const fields: TemplateFieldRow[] = []
|
||||
|
||||
const appendSide = (arr: unknown, side: '正面' | '背面') => {
|
||||
if (!Array.isArray(arr)) return
|
||||
for (const item of arr) {
|
||||
if (!item || typeof item !== 'object') continue
|
||||
const o = item as Record<string, unknown>
|
||||
const type = Number(o.type)
|
||||
if (!SOON_FIELD_TYPES.has(type)) continue
|
||||
const name = String(o.name ?? '').trim()
|
||||
if (!name) continue
|
||||
const value = o.DefaultText == null ? '' : String(o.DefaultText)
|
||||
fields.push({ label: `${name}[${side}]`, value, originName: name })
|
||||
}
|
||||
}
|
||||
|
||||
appendSide(raw.frontData, '正面')
|
||||
appendSide(raw.backData, '背面')
|
||||
|
||||
const frontPic = String(raw.frontDisplayPic ?? '').trim()
|
||||
const backPic = String(raw.backDisplayPic ?? '').trim()
|
||||
|
||||
return {
|
||||
frontImageUrl: toImageUrl(soonPath, frontPic),
|
||||
backImageUrl: toImageUrl(soonPath, backPic),
|
||||
fields
|
||||
}
|
||||
}
|
||||
|
||||
function parseSoonLegacy(soonPath: string, raw: Record<string, unknown>): ParsedSoonTemplate {
|
||||
const imgs = pickArray(raw, 'Img')
|
||||
const texts = pickArray(raw, 'Text')
|
||||
|
||||
@@ -66,7 +104,7 @@ export function parseSoonTemplate(soonPath: string, raw: Record<string, unknown>
|
||||
if (side === 'front') {
|
||||
if (!frontImageUrl) frontImageUrl = url
|
||||
const name = pickStr(item, ['name', 'field', 'key']) || 'IMAGE'
|
||||
fields.push({ label: toFieldLabel(name, 'front'), value: fileRef })
|
||||
fields.push({ label: toFieldLabel(name, 'front'), value: fileRef, originName: name })
|
||||
} else if (!backImageUrl) {
|
||||
backImageUrl = url
|
||||
}
|
||||
@@ -78,8 +116,20 @@ export function parseSoonTemplate(soonPath: string, raw: Record<string, unknown>
|
||||
const value = pickStr(item, ['value', 'text', 'default', 'content', 'data'])
|
||||
let side = sideOf(item)
|
||||
if (!side) side = /image|img|front/i.test(name) ? 'front' : 'back'
|
||||
fields.push({ label: toFieldLabel(name, side), value })
|
||||
fields.push({ label: toFieldLabel(name, side), value, originName: name })
|
||||
})
|
||||
|
||||
return { frontImageUrl, backImageUrl, fields }
|
||||
}
|
||||
|
||||
export function parseSoonTemplate(soonPath: string, raw: Record<string, unknown>): ParsedSoonTemplate {
|
||||
if (
|
||||
Array.isArray(raw.frontData) ||
|
||||
Array.isArray(raw.backData) ||
|
||||
raw.frontDisplayPic != null ||
|
||||
raw.backDisplayPic != null
|
||||
) {
|
||||
return parseSoonWorkerDisk(soonPath, raw)
|
||||
}
|
||||
return parseSoonLegacy(soonPath, raw)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import log from 'electron-log'
|
||||
import { CS_OK } from '../constants'
|
||||
import { cleanPathPattern } from '@shared/path-pattern'
|
||||
import { getDirectorySizeBytes } from './dir-size'
|
||||
|
||||
export type JobStageDll = {
|
||||
dllUploadFile: (userDir: string, fileName: string, fileText: string) => number
|
||||
isUploadApiAvailable: () => boolean
|
||||
}
|
||||
|
||||
function copyIfExists(src: string, dest: string): void {
|
||||
if (!fs.existsSync(src)) return
|
||||
fs.mkdirSync(path.dirname(dest), { recursive: true })
|
||||
fs.copyFileSync(src, dest)
|
||||
}
|
||||
|
||||
function uploadText(
|
||||
dll: JobStageDll,
|
||||
userDir: string,
|
||||
fileName: string,
|
||||
text: string
|
||||
): boolean {
|
||||
if (!dll.isUploadApiAvailable()) return false
|
||||
const code = dll.dllUploadFile(userDir, fileName, fileText)
|
||||
if (code !== CS_OK) {
|
||||
log.warn('SAPI_UploadFile failed', { userDir, fileName, code })
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function stageSoonAssets(taskDir: string, soonSrc: string, soonDest: string): void {
|
||||
copyIfExists(soonSrc, soonDest)
|
||||
let raw: Record<string, unknown>
|
||||
try {
|
||||
raw = JSON.parse(fs.readFileSync(soonDest, 'utf8')) as Record<string, unknown>
|
||||
} catch (e) {
|
||||
log.warn('stageSoonAssets: parse soon failed', e)
|
||||
return
|
||||
}
|
||||
const soonDir = path.dirname(soonSrc)
|
||||
for (const key of ['frontDisplayPic', 'backDisplayPic']) {
|
||||
const ref = raw[key]
|
||||
if (typeof ref !== 'string' || !ref.trim()) continue
|
||||
const clean = ref.replace(/^file:\/\//i, '').trim()
|
||||
const assetSrc = path.isAbsolute(clean) ? clean : path.join(soonDir, clean)
|
||||
const assetDest = path.join(taskDir, path.basename(clean))
|
||||
copyIfExists(assetSrc, assetDest)
|
||||
}
|
||||
}
|
||||
|
||||
/** 保留目录级 path_file,不展开为单文件列表 */
|
||||
function normalizeCopyPaths(payload: Record<string, unknown>): void {
|
||||
if (!Array.isArray(payload.path_file)) {
|
||||
throw new Error('拷贝任务缺少 path_file')
|
||||
}
|
||||
const normalized: string[] = []
|
||||
for (const entry of payload.path_file) {
|
||||
if (typeof entry !== 'string' || !entry.trim()) continue
|
||||
const target = cleanPathPattern(entry)
|
||||
if (!target || !fs.existsSync(target)) {
|
||||
throw new Error(`拷贝路径不存在: ${entry}`)
|
||||
}
|
||||
let st: fs.Stats
|
||||
try {
|
||||
st = fs.statSync(target)
|
||||
} catch {
|
||||
throw new Error(`拷贝路径不可访问: ${target}`)
|
||||
}
|
||||
if (st.isFile()) {
|
||||
normalized.push(target)
|
||||
continue
|
||||
}
|
||||
if (!st.isDirectory()) {
|
||||
throw new Error(`拷贝路径无效: ${target}`)
|
||||
}
|
||||
if (getDirectorySizeBytes(target) <= 0) {
|
||||
throw new Error(`拷贝路径下没有可拷贝的文件: ${target}`)
|
||||
}
|
||||
normalized.push(target)
|
||||
}
|
||||
if (normalized.length === 0) {
|
||||
throw new Error('拷贝路径下没有可拷贝的文件')
|
||||
}
|
||||
payload.path_file = normalized
|
||||
}
|
||||
|
||||
/** staging 后按 has_* 裁剪字段,并校验 RestJobEx 必填项 */
|
||||
function finalizeRestJobPayload(payload: Record<string, unknown>): void {
|
||||
const hasCopy = payload.has_copy_task === true
|
||||
const hasPrint = payload.has_print_task === true
|
||||
if (!hasCopy && !hasPrint) {
|
||||
throw new Error('任务需包含拷贝或打印')
|
||||
}
|
||||
|
||||
if (hasPrint) {
|
||||
if (typeof payload.json_file !== 'string' || !payload.json_file.trim()) {
|
||||
throw new Error('打印任务缺少 json_file')
|
||||
}
|
||||
if (!payload.udf_file) delete payload.udf_file
|
||||
} else {
|
||||
delete payload.json_file
|
||||
delete payload.udf_file
|
||||
}
|
||||
|
||||
if (hasCopy) {
|
||||
const paths = payload.path_file
|
||||
if (
|
||||
!Array.isArray(paths) ||
|
||||
paths.length === 0 ||
|
||||
!paths.every((p) => typeof p === 'string' && p.trim())
|
||||
) {
|
||||
throw new Error('拷贝任务缺少 path_file')
|
||||
}
|
||||
} else {
|
||||
delete payload.path_file
|
||||
}
|
||||
}
|
||||
|
||||
/** 通过 SAPI_UploadFile + 本地落盘,准备 RestJobEx 所需路径 */
|
||||
export function stageJobPayloadJson(
|
||||
json: string,
|
||||
sharedDir: string,
|
||||
dll: JobStageDll
|
||||
): { json: string; taskDir: string } {
|
||||
const payload = JSON.parse(json) as Record<string, unknown>
|
||||
const taskId = String(payload.task_id || '').trim()
|
||||
if (!taskId) throw new Error('task_id 缺失')
|
||||
|
||||
const taskDir = path.join(sharedDir, taskId)
|
||||
fs.mkdirSync(taskDir, { recursive: true })
|
||||
const userDir = taskId
|
||||
|
||||
const udfFile = payload.udf_file
|
||||
if (typeof udfFile === 'string' && udfFile.trim() && fs.existsSync(udfFile.trim())) {
|
||||
const csvText = fs.readFileSync(udfFile.trim(), 'utf8')
|
||||
if (!uploadText(dll, userDir, 'temp.csv', csvText)) {
|
||||
copyIfExists(udfFile.trim(), path.join(taskDir, 'temp.csv'))
|
||||
}
|
||||
payload.udf_file = path.join(taskDir, 'temp.csv')
|
||||
}
|
||||
|
||||
const jsonFile = payload.json_file
|
||||
if (typeof jsonFile === 'string' && jsonFile.trim()) {
|
||||
const src = jsonFile.trim()
|
||||
const base = path.basename(src)
|
||||
const dest = path.join(taskDir, base)
|
||||
const soonText = fs.readFileSync(src, 'utf8')
|
||||
uploadText(dll, userDir, base, soonText)
|
||||
stageSoonAssets(taskDir, src, dest)
|
||||
payload.json_file = dest
|
||||
}
|
||||
|
||||
if (payload.has_copy_task === true) {
|
||||
normalizeCopyPaths(payload)
|
||||
}
|
||||
|
||||
finalizeRestJobPayload(payload)
|
||||
|
||||
return { json: JSON.stringify(payload), taskDir }
|
||||
}
|
||||
Reference in New Issue
Block a user