初始化

This commit is contained in:
24kycj
2026-05-22 19:56:22 +08:00
commit d0c3fd162a
77 changed files with 17058 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
export type MainWorkflowMode = 'ready' | 'distributing' | 'usbCopying'
export const mainAppState = {
initialized: false,
mode: 'ready' as MainWorkflowMode,
activeJobId: ''
}
export function assertReady(): void {
if (!mainAppState.initialized) throw new Error('NOT_INITIALIZED')
}
export function assertNotBusy(): void {
if (mainAppState.mode === 'distributing' || mainAppState.mode === 'usbCopying') {
throw new Error('BUSY')
}
}
+21
View File
@@ -0,0 +1,21 @@
import Store from 'electron-store'
import { app } from 'electron'
import path from 'path'
interface AppConfig {
sharedDir: string
templateDir: string
/** 开发默认 true:不调用 SAPI_Init,避免无打印机时 DLL 刷错 */
skipDllInit: boolean
}
const defaultShared = path.join(app.getPath('userData'), 'Cardsoon', 'tasks')
export const configStore = new Store<AppConfig>({
name: 'cardsoon-config',
defaults: {
sharedDir: defaultShared,
templateDir: path.join(app.getPath('userData'), 'Cardsoon', 'templates'),
skipDllInit: !app.isPackaged
}
})
+88
View File
@@ -0,0 +1,88 @@
import { app } from 'electron'
import path from 'path'
import fs from 'fs'
import log from 'electron-log'
const RUNTIME_CONFIG_FILES = ['CapSettings.json'] as const
const DEFAULT_PRINT_TASKS = path.join('C:', 'PrintTasks')
const DEFAULT_CAP_SETTINGS = `{
"printerList": [],
"Img": [],
"Text": []
}
`
export function getNativeDir(): string {
if (app.isPackaged) {
return path.join(process.resourcesPath, 'native')
}
return path.join(app.getAppPath(), 'resources', 'native')
}
export function getProcessExecDir(): string {
return path.dirname(process.execPath)
}
function ensureBundledConfig(nativeDir: string): void {
fs.mkdirSync(nativeDir, { recursive: true })
for (const name of RUNTIME_CONFIG_FILES) {
const filePath = path.join(nativeDir, name)
if (!fs.existsSync(filePath)) {
fs.writeFileSync(filePath, DEFAULT_CAP_SETTINGS, 'utf8')
log.info(`Created default native config: ${filePath}`)
}
}
}
function deployConfigFile(src: string, destDir: string): void {
const dest = path.join(destDir, path.basename(src))
try {
fs.mkdirSync(destDir, { recursive: true })
if (!fs.existsSync(dest)) {
fs.copyFileSync(src, dest)
log.debug(`Deployed ${path.basename(src)} -> ${dest}`)
return
}
const srcStat = fs.statSync(src)
const destStat = fs.statSync(dest)
if (srcStat.mtimeMs > destStat.mtimeMs) {
fs.copyFileSync(src, dest)
log.debug(`Updated ${path.basename(src)} -> ${dest}`)
}
} catch (e) {
log.warn(`Deploy ${path.basename(src)} to ${destDir} failed`, e)
}
}
function deployRuntimeConfigs(nativeDir: string): void {
const deployDirs = new Set<string>([
nativeDir,
getProcessExecDir(),
DEFAULT_PRINT_TASKS,
path.join(app.getPath('userData'), 'Cardsoon', 'runtime')
])
for (const name of RUNTIME_CONFIG_FILES) {
const src = path.join(nativeDir, name)
if (!fs.existsSync(src)) continue
deployDirs.forEach((dir) => deployConfigFile(src, dir))
}
try {
fs.mkdirSync(DEFAULT_PRINT_TASKS, { recursive: true })
} catch (e) {
log.warn(`Cannot create ${DEFAULT_PRINT_TASKS}`, e)
}
}
export function setupNativeWorkingDir(): void {
const nativeDir = getNativeDir()
if (!fs.existsSync(path.join(nativeDir, 'workDll.dll'))) {
throw new Error(`Native DLL directory not found or incomplete: ${nativeDir}`)
}
ensureBundledConfig(nativeDir)
deployRuntimeConfigs(nativeDir)
process.chdir(nativeDir)
log.debug(`Native working directory: ${nativeDir}`)
}
+106
View File
@@ -0,0 +1,106 @@
import { BrowserWindow } from 'electron'
import log from 'electron-log'
import { POLL_INTERVAL_MS } from '../constants'
import { mainAppState } from './app-state'
import { dllGetJobStateById, dllGetUsbCopyState } from './work-dll.service'
let jobTimer: ReturnType<typeof setInterval> | null = null
let usbTimer: ReturnType<typeof setInterval> | null = null
let jobId = ''
let mainWindow: BrowserWindow | null = null
export function setPollMainWindow(win: BrowserWindow): void {
mainWindow = win
}
function send(channel: string, payload: unknown): void {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send(channel, payload)
}
}
export function stopJobPoll(): void {
if (jobTimer) {
clearInterval(jobTimer)
jobTimer = null
}
}
export function stopUsbPoll(): void {
if (usbTimer) {
clearInterval(usbTimer)
usbTimer = null
}
}
export function stopAllPolls(): void {
stopJobPoll()
stopUsbPoll()
}
export function startJobPoll(id: string): void {
stopJobPoll()
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
send('job:poll-tick', {
jobId,
queryErrorCode: r.queryErrorCode,
jobState: r.jobState,
progress: r.progress,
terminal,
failed,
cancelled,
finished
})
if (r.queryErrorCode !== 0) {
log.warn('GetJobStateById query failed', r.queryErrorCode)
stopJobPoll()
return
}
if (failed || cancelled) {
stopJobPoll()
if (cancelled) mainAppState.mode = 'ready'
}
} catch (e) {
log.error('job poll error', e)
stopJobPoll()
}
}, POLL_INTERVAL_MS)
}
export function startUsbPoll(): void {
stopUsbPoll()
usbTimer = setInterval(() => {
try {
const r = dllGetUsbCopyState()
const failed = r.taskStatus === 3
const success = r.taskStatus === 2
const terminal = failed || success
send('usb:poll-tick', {
taskStatus: r.taskStatus,
progress: r.progress,
terminal,
failed,
success
})
if (terminal) {
stopUsbPoll()
mainAppState.mode = 'ready'
}
} catch (e) {
log.error('usb poll error', e)
stopUsbPoll()
mainAppState.mode = 'ready'
}
}, POLL_INTERVAL_MS)
}
export function getActiveJobId(): string {
return jobId
}
+182
View File
@@ -0,0 +1,182 @@
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 { getNativeDir } from './native-path'
export interface InitParams {
sharedDir: string
keepCombinedImage?: boolean
stopOnFailure?: boolean
cleanTaskFile?: boolean
autoRetryTimes?: number
rejectConfig?: boolean
logLevel?: number
outBack?: boolean
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let lib: any = null
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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_GetPrinterErrorStr: any = null
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let SAPI_RestJobEx: any = null
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let SAPI_GetJobStateById: any = null
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let SAPI_AdminJobCancel: any = null
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let SAPI_CopyFromUsb: any = null
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let SAPI_GetUsbCopyState: any = null
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let SAPI_PrinterResetprinter: any = null
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let SAPI_PrinterMovetoreject: any = null
let hasRejectApi = false
let hasCancelApi = false
let loggedCancelMissing = false
let loggedRejectMissing = false
function loadLibrary(): void {
if (lib) return
const dllPath = path.join(getNativeDir(), 'workDll.dll')
lib = koffi.load(dllPath)
SAPI_Init = lib.func('int __stdcall SAPI_Init(str, bool, bool, bool, int, bool, int, bool)')
SAPI_GetPrinterInfo = lib.func('int __stdcall SAPI_GetPrinterInfo(_Out_ void **)')
SAPI_GetPrinterErrorStr = lib.func('str __stdcall SAPI_GetPrinterErrorStr(int)')
SAPI_RestJobEx = lib.func('int __stdcall SAPI_RestJobEx(str, _Out_ char *, int)')
SAPI_GetJobStateById = lib.func(
'int __stdcall SAPI_GetJobStateById(str, _Out_ int *, _Out_ int *)'
)
SAPI_CopyFromUsb = lib.func('int __stdcall SAPI_CopyFromUsb(str, int)')
SAPI_GetUsbCopyState = lib.func('int __stdcall SAPI_GetUsbCopyState(_Out_ int *, _Out_ int *)')
SAPI_PrinterResetprinter = lib.func('int __stdcall SAPI_PrinterResetprinter()')
try {
SAPI_AdminJobCancel = lib.func('int __stdcall SAPI_AdminJobCancel(str)')
hasCancelApi = true
} catch {
SAPI_AdminJobCancel = null
hasCancelApi = false
if (!loggedCancelMissing) {
loggedCancelMissing = true
log.info('SAPI_AdminJobCancel not in workDll (optional); stop uses poll-stop only')
}
}
try {
SAPI_PrinterMovetoreject = lib.func('int __stdcall SAPI_PrinterMovetoreject()')
hasRejectApi = true
} catch {
hasRejectApi = false
if (!loggedRejectMissing) {
loggedRejectMissing = true
log.info('SAPI_PrinterMovetoreject not in workDll (optional); reject card disabled')
}
}
}
export function isRejectApiAvailable(): boolean {
loadLibrary()
return hasRejectApi
}
export function isCancelApiAvailable(): boolean {
loadLibrary()
return hasCancelApi
}
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 ?? true,
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)
}
}
export function dllGetPrinterErrorStr(errorNo = -1): string {
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 }
}
export function dllGetJobStateById(jobId: string): {
queryErrorCode: number
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]
}
}
export function dllAdminJobCancel(jobId: string): number {
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
}
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] }
}
export function dllPrinterReset(): number {
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
}