This commit is contained in:
24kycj
2026-05-28 08:41:10 +08:00
parent 22b17aed35
commit 505e72ce28
19 changed files with 589 additions and 134 deletions
+72
View File
@@ -0,0 +1,72 @@
import { app } from 'electron'
import fs from 'fs'
import path from 'path'
import log from 'electron-log'
import { getProcessExecDir } from './native-path'
export const APP_CONFIG_FILENAME = 'cardsoon.config.json'
/** 与 cardsoon.config.json 键名一致,后续配置在此扩展 */
export interface AppFileConfig {
designAppPath: string
}
const defaults: AppFileConfig = {
designAppPath: ''
}
let cached: AppFileConfig | null = null
let loadedFrom = ''
function bundledConfigPath(): string {
if (app.isPackaged) {
return path.join(process.resourcesPath, APP_CONFIG_FILENAME)
}
return path.join(app.getAppPath(), 'resources', APP_CONFIG_FILENAME)
}
function configSearchPaths(): string[] {
const besideExe = path.join(getProcessExecDir(), APP_CONFIG_FILENAME)
const bundled = bundledConfigPath()
if (besideExe === bundled) return [besideExe]
return [besideExe, bundled]
}
function parseConfigFile(filePath: string): AppFileConfig {
const raw = JSON.parse(fs.readFileSync(filePath, 'utf8')) as Record<string, unknown>
return {
designAppPath: String(raw.designAppPath ?? '').trim()
}
}
export function loadAppFileConfig(): AppFileConfig {
if (cached) return cached
for (const filePath of configSearchPaths()) {
if (!fs.existsSync(filePath)) continue
try {
cached = parseConfigFile(filePath)
loadedFrom = filePath
log.info(`Loaded ${APP_CONFIG_FILENAME} from ${filePath}`)
return cached
} catch (e) {
log.warn(`Skip invalid ${APP_CONFIG_FILENAME}: ${filePath}`, e)
}
}
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
}
+7 -1
View File
@@ -1,12 +1,17 @@
import Store from 'electron-store'
import { app } from 'electron'
import path from 'path'
import type { PrinterStatusSnapshot } from '@shared/printer-info'
interface AppConfig {
sharedDir: string
templateDir: string
/** G2 门禁 false:启动即 SAPI_Init;仅调试可改 true */
skipDllInit: boolean
/** trueIPC/DLL 等调用输出到 DevTools 控制台 */
traceEnabled: boolean
/** 上次成功的 GetPrinterInfo 解析结果,供离线/失败时展示 */
lastPrinterStatus?: PrinterStatusSnapshot
}
const defaultShared = path.join('C:', 'PrintTasks')
@@ -17,6 +22,7 @@ export const configStore = new Store<AppConfig>({
sharedDir: defaultShared,
templateDir: path.join(app.getPath('userData'), 'Cardsoon', 'templates'),
// 正式版始终 Init;仅开发时可通过 --skip-dll-init 临时跳过
skipDllInit: false
skipDllInit: false,
traceEnabled: true
}
})
+30
View File
@@ -0,0 +1,30 @@
import { shell } from 'electron'
import fs from 'fs'
import path from 'path'
export function validateDesignAppPath(exePath: string): { ok: true } | { ok: false; message: string } {
const p = exePath.trim()
if (!p) {
return { ok: false, message: '请在 cardsoon.config.json 中配置 designAppPath' }
}
const resolved = path.resolve(p)
if (!fs.existsSync(resolved)) {
return { ok: false, message: `设计软件不存在: ${resolved}` }
}
return { ok: true }
}
/** 由系统启动外部程序;空字符串表示成功,非空为失败原因 */
export async function openDesignApp(
exePath: string
): Promise<{ ok: true } | { ok: false; message: string }> {
const check = validateDesignAppPath(exePath)
if (!check.ok) return check
const target = path.resolve(exePath.trim())
const err = await shell.openPath(target)
if (err) {
return { ok: false, message: err }
}
return { ok: true }
}
+9 -4
View File
@@ -2,6 +2,7 @@ import { BrowserWindow } from 'electron'
import log from 'electron-log'
import { POLL_INTERVAL_MS } from '../constants'
import { mainAppState } from './app-state'
import { emitTrace } from '../utils/trace-bridge'
import { dllGetJobStateById, dllGetUsbCopyState } from './work-dll.service'
let jobTimer: ReturnType<typeof setInterval> | null = null
@@ -56,7 +57,7 @@ export function startJobPoll(id: string): void {
const cancelled = r.jobState === 6
const finished = r.jobState === 100
const terminal = failed || cancelled
send('job:poll-tick', {
const tick = {
jobId,
queryErrorCode: r.queryErrorCode,
jobState: r.jobState,
@@ -65,7 +66,9 @@ export function startJobPoll(id: string): void {
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)
@@ -89,13 +92,15 @@ export function startUsbPoll(): void {
const failed = r.taskStatus === 3
const success = r.taskStatus === 2
const terminal = failed || success
send('usb:poll-tick', {
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'
+118 -58
View File
@@ -1,7 +1,7 @@
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'
export interface InitParams {
@@ -42,6 +42,27 @@ let hasCancelApi = false
let loggedCancelMissing = false
let loggedRejectMissing = false
function tracePayload(r: unknown): Record<string, unknown> {
if (r === null || r === undefined) return {}
if (typeof r !== 'object') return { value: r }
return { ...(r as Record<string, unknown>) }
}
function traceCall<T>(name: string, args: Record<string, unknown> | undefined, fn: () => T): T {
if (!isTraceEnabled()) return fn()
const tag = `[dll] ${name}`
const start = Date.now()
emitTrace(`${tag}`, args)
try {
const r = fn()
emitTrace(`${tag}`, { ms: Date.now() - start, ...tracePayload(r) })
return r
} catch (e) {
emitTrace(`${tag}`, { ms: Date.now() - start, error: String(e) }, 'error')
throw e
}
}
function loadLibrary(): void {
if (lib) return
const dllPath = path.join(getNativeDir(), 'workDll.dll')
@@ -66,7 +87,7 @@ function loadLibrary(): void {
hasCancelApi = false
if (!loggedCancelMissing) {
loggedCancelMissing = true
log.info('SAPI_AdminJobCancel not in workDll (optional); stop uses poll-stop only')
emitTrace('[dll] SAPI_AdminJobCancel not in workDll (optional)')
}
}
@@ -77,7 +98,7 @@ function loadLibrary(): void {
hasRejectApi = false
if (!loggedRejectMissing) {
loggedRejectMissing = true
log.info('SAPI_PrinterMovetoreject not in workDll (optional); reject card disabled')
emitTrace('[dll] SAPI_PrinterMovetoreject not in workDll (optional)')
}
}
}
@@ -93,46 +114,73 @@ export function isCancelApiAvailable(): boolean {
}
export function dllInit(params: InitParams): number {
loadLibrary()
return SAPI_Init!(
params.sharedDir,
params.keepCombinedImage ?? true,
params.stopOnFailure ?? false,
params.cleanTaskFile ?? true,
params.autoRetryTimes ?? 0,
params.rejectConfig ?? false,
params.logLevel ?? LOG_FATAL_FLAG,
params.outBack ?? false
) as number
return traceCall(
'SAPI_Init',
{
sharedDir: params.sharedDir,
keepCombinedImage: params.keepCombinedImage ?? true,
stopOnFailure: params.stopOnFailure ?? false,
cleanTaskFile: params.cleanTaskFile ?? true,
autoRetryTimes: params.autoRetryTimes ?? 0,
rejectConfig: params.rejectConfig ?? false,
logLevel: params.logLevel ?? LOG_FATAL_FLAG,
outBack: params.outBack ?? false
},
() => {
loadLibrary()
return SAPI_Init!(
params.sharedDir,
params.keepCombinedImage ?? true,
params.stopOnFailure ?? false,
params.cleanTaskFile ?? true,
params.autoRetryTimes ?? 0,
params.rejectConfig ?? false,
params.logLevel ?? LOG_FATAL_FLAG,
params.outBack ?? false
) as number
}
)
}
export function dllGetPrinterInfo(): { code: number; json?: Record<string, unknown> } {
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
const jsonStr = koffi.decode(ptr, 'char', len) as string
koffi.free(ptr)
return { code: len, json: JSON.parse(jsonStr) as Record<string, unknown> }
} finally {
koffi.free(outPtr)
}
return traceCall('SAPI_GetPrinterInfo', 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 }
}
} finally {
koffi.free(outPtr)
}
})
}
export function dllGetPrinterErrorStr(errorNo = -1): string {
loadLibrary()
const s = SAPI_GetPrinterErrorStr!(errorNo) as string
return s || ''
return traceCall('SAPI_GetPrinterErrorStr', { errorNo }, () => {
loadLibrary()
const s = SAPI_GetPrinterErrorStr!(errorNo) as string
return s || ''
})
}
export function dllRestJobEx(json: string): { code: number; jobId: string } {
loadLibrary()
const buf = Buffer.alloc(JOB_ID_BUF_SIZE)
const code = SAPI_RestJobEx!(json, buf, JOB_ID_BUF_SIZE) as number
const jobId = buf.toString('utf8').replace(/\0.*$/, '').trim()
return { code, jobId }
return traceCall('SAPI_RestJobEx', { jsonBytes: Buffer.byteLength(json ?? '', 'utf8') }, () => {
loadLibrary()
const buf = Buffer.alloc(JOB_ID_BUF_SIZE)
const code = SAPI_RestJobEx!(json, buf, JOB_ID_BUF_SIZE) as number
const jobId = buf.toString('utf8').replace(/\0.*$/, '').trim()
return { code, jobId }
})
}
export function dllGetJobStateById(jobId: string): {
@@ -140,43 +188,55 @@ export function dllGetJobStateById(jobId: string): {
jobState: number
progress: number
} {
loadLibrary()
const jobState = [0]
const copyScheduler = [0]
const queryErrorCode = SAPI_GetJobStateById!(jobId, jobState, copyScheduler) as number
return {
queryErrorCode,
jobState: jobState[0],
progress: copyScheduler[0]
}
return traceCall('SAPI_GetJobStateById', { jobId }, () => {
loadLibrary()
const jobState = [0]
const copyScheduler = [0]
const queryErrorCode = SAPI_GetJobStateById!(jobId, jobState, copyScheduler) as number
return {
queryErrorCode,
jobState: jobState[0],
progress: copyScheduler[0]
}
})
}
export function dllAdminJobCancel(jobId: string): number {
loadLibrary()
if (!SAPI_AdminJobCancel) throw new Error('CANCEL_API_UNAVAILABLE')
return SAPI_AdminJobCancel(jobId) as number
return traceCall('SAPI_AdminJobCancel', { jobId }, () => {
loadLibrary()
if (!SAPI_AdminJobCancel) throw new Error('CANCEL_API_UNAVAILABLE')
return SAPI_AdminJobCancel(jobId) as number
})
}
export function dllCopyFromUsb(destFolder: string, cardOutput: number): number {
loadLibrary()
return SAPI_CopyFromUsb!(destFolder, cardOutput) as number
return traceCall('SAPI_CopyFromUsb', { destFolder, cardOutput }, () => {
loadLibrary()
return SAPI_CopyFromUsb!(destFolder, cardOutput) as number
})
}
export function dllGetUsbCopyState(): { taskStatus: number; progress: number } {
loadLibrary()
const taskStatus = [0]
const progress = [0]
SAPI_GetUsbCopyState!(taskStatus, progress)
return { taskStatus: taskStatus[0], progress: progress[0] }
return traceCall('SAPI_GetUsbCopyState', undefined, () => {
loadLibrary()
const taskStatus = [0]
const progress = [0]
SAPI_GetUsbCopyState!(taskStatus, progress)
return { taskStatus: taskStatus[0], progress: progress[0] }
})
}
export function dllPrinterReset(): number {
loadLibrary()
return SAPI_PrinterResetprinter!() as number
return traceCall('SAPI_PrinterResetprinter', undefined, () => {
loadLibrary()
return SAPI_PrinterResetprinter!() as number
})
}
export function dllPrinterReject(): number {
loadLibrary()
if (!SAPI_PrinterMovetoreject) throw new Error('REJECT_API_UNAVAILABLE')
return SAPI_PrinterMovetoreject() as number
return traceCall('SAPI_PrinterMovetoreject', undefined, () => {
loadLibrary()
if (!SAPI_PrinterMovetoreject) throw new Error('REJECT_API_UNAVAILABLE')
return SAPI_PrinterMovetoreject() as number
})
}