fix: 落实测试反馈五项并优化分发配置
- 色带余量 RibbonAmount 独立展示与轮询刷新 - 加密狗授权码 UI、校验、auth_code 提交及 secrets 持久化 - 打印机状态全局轮询(前台 1.5s / 后台 8s) - 网络路径:列表仅映射目录,凭据单独配置,net_info 对齐 SoonWoker - 标签打印面:点击正/反面选择,print_flag 1/2/3 与模板校验 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -12,6 +12,8 @@ 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 { resolveDriveHostMap, resolveHostsFromPaths } from '../utils/network-drive'
|
||||
import { getSecrets, setSecrets, type SecretsPayload } from '../services/secrets-store'
|
||||
import { ensureDllInitialized, isDllInitAttempted } from '../services/dll-bootstrap'
|
||||
import { loadDllModule } from '../services/dll-loader'
|
||||
import { tracedHandle } from './traced-handler'
|
||||
@@ -103,6 +105,7 @@ export function registerIpcHandlers(): void {
|
||||
if (parsed.ok) {
|
||||
const snapshot: PrinterStatusSnapshot = {
|
||||
ribbonType: cached?.ribbonType ?? '—',
|
||||
ribbonAmount: cached?.ribbonAmount ?? '—',
|
||||
statusText: parsed.statusText,
|
||||
serialNo: cached?.serialNo ?? '—'
|
||||
}
|
||||
@@ -350,6 +353,29 @@ export function registerIpcHandlers(): void {
|
||||
return ok({ items })
|
||||
})
|
||||
|
||||
tracedHandle('fs:resolve-network-hosts', (_e, paths: string[]) => {
|
||||
const list = Array.isArray(paths) ? paths.map((p) => String(p || '')) : []
|
||||
const driveHostMap = resolveDriveHostMap(list)
|
||||
const hosts = resolveHostsFromPaths(list)
|
||||
return ok({ hosts, driveHostMap })
|
||||
})
|
||||
|
||||
tracedHandle('secrets:get', () => {
|
||||
return ok(getSecrets())
|
||||
})
|
||||
|
||||
tracedHandle('secrets:set', (_e, patch: Partial<SecretsPayload>) => {
|
||||
if (!patch || typeof patch !== 'object') return ok(getSecrets())
|
||||
const toMerge: Partial<SecretsPayload> = {}
|
||||
if (patch.networkCredentials !== undefined) {
|
||||
toMerge.networkCredentials = patch.networkCredentials
|
||||
}
|
||||
if (patch.dongleAuthCode !== undefined) {
|
||||
toMerge.dongleAuthCode = patch.dongleAuthCode
|
||||
}
|
||||
return ok(setSecrets(toMerge))
|
||||
})
|
||||
|
||||
tracedHandle(
|
||||
'fs:write-job-csv',
|
||||
(_e, payload: { taskId: string; rows: JobCsvRow[] }) => {
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { app, safeStorage } from 'electron'
|
||||
import log from 'electron-log'
|
||||
|
||||
export interface StoredNetworkCredential {
|
||||
userName: string
|
||||
password: string
|
||||
lastUsed: string
|
||||
}
|
||||
|
||||
export interface SecretsPayload {
|
||||
networkCredentials: Record<string, StoredNetworkCredential>
|
||||
dongleAuthCode: string
|
||||
}
|
||||
|
||||
const FILE_NAME = 'secrets.bin'
|
||||
|
||||
function secretsPath(): string {
|
||||
return path.join(app.getPath('userData'), FILE_NAME)
|
||||
}
|
||||
|
||||
function emptyPayload(): SecretsPayload {
|
||||
return { networkCredentials: {}, dongleAuthCode: '' }
|
||||
}
|
||||
|
||||
function canEncrypt(): boolean {
|
||||
try {
|
||||
return safeStorage.isEncryptionAvailable()
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function readRaw(): SecretsPayload {
|
||||
const file = secretsPath()
|
||||
if (!fs.existsSync(file)) return emptyPayload()
|
||||
try {
|
||||
const buf = fs.readFileSync(file)
|
||||
if (!buf.length) return emptyPayload()
|
||||
let text: string
|
||||
if (canEncrypt()) {
|
||||
text = safeStorage.decryptString(buf)
|
||||
} else {
|
||||
text = buf.toString('utf8')
|
||||
}
|
||||
const parsed = JSON.parse(text) as Partial<SecretsPayload>
|
||||
return {
|
||||
networkCredentials:
|
||||
parsed.networkCredentials && typeof parsed.networkCredentials === 'object'
|
||||
? parsed.networkCredentials
|
||||
: {},
|
||||
dongleAuthCode: typeof parsed.dongleAuthCode === 'string' ? parsed.dongleAuthCode : ''
|
||||
}
|
||||
} catch (e) {
|
||||
log.warn('[secrets-store] read failed', e)
|
||||
return emptyPayload()
|
||||
}
|
||||
}
|
||||
|
||||
function writeRaw(payload: SecretsPayload): void {
|
||||
const text = JSON.stringify(payload)
|
||||
try {
|
||||
const buf = canEncrypt() ? safeStorage.encryptString(text) : Buffer.from(text, 'utf8')
|
||||
fs.writeFileSync(secretsPath(), buf)
|
||||
} catch (e) {
|
||||
log.warn('[secrets-store] write failed', e)
|
||||
}
|
||||
}
|
||||
|
||||
let cache: SecretsPayload | null = null
|
||||
|
||||
export function getSecrets(): SecretsPayload {
|
||||
if (!cache) cache = readRaw()
|
||||
return {
|
||||
networkCredentials: { ...cache.networkCredentials },
|
||||
dongleAuthCode: cache.dongleAuthCode
|
||||
}
|
||||
}
|
||||
|
||||
export function setSecrets(patch: Partial<SecretsPayload>): SecretsPayload {
|
||||
const current = getSecrets()
|
||||
if (patch.networkCredentials !== undefined) {
|
||||
current.networkCredentials = { ...patch.networkCredentials }
|
||||
}
|
||||
if (patch.dongleAuthCode !== undefined) {
|
||||
current.dongleAuthCode = patch.dongleAuthCode
|
||||
}
|
||||
cache = current
|
||||
writeRaw(current)
|
||||
return getSecrets()
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { execSync } from 'child_process'
|
||||
import {
|
||||
extractDriveLetter,
|
||||
extractHostName,
|
||||
isNetworkPath
|
||||
} from '@shared/network-host'
|
||||
|
||||
function resolveUncForDrive(letter: string): string | null {
|
||||
const L = (letter || '').trim().toUpperCase()
|
||||
if (!L || L.length !== 1) return null
|
||||
if (process.platform !== 'win32') return null
|
||||
try {
|
||||
const out = execSync(`net use ${L}:`, { encoding: 'utf8', windowsHide: true })
|
||||
const m = /Remote\s+(\S+)/i.exec(out) || /远程\s+(\S+)/i.exec(out)
|
||||
const unc = m?.[1]?.trim()
|
||||
if (!unc || !unc.startsWith('\\\\')) return null
|
||||
return unc
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveHostsFromPaths(paths: string[]): string[] {
|
||||
const hosts = new Set<string>()
|
||||
for (const raw of paths) {
|
||||
const p = (raw || '').trim()
|
||||
if (!p) continue
|
||||
if (isNetworkPath(p)) {
|
||||
const h = extractHostName(p)
|
||||
if (h) hosts.add(h)
|
||||
continue
|
||||
}
|
||||
const letter = extractDriveLetter(p)
|
||||
if (!letter) continue
|
||||
const unc = resolveUncForDrive(letter)
|
||||
if (!unc) continue
|
||||
const h = extractHostName(unc)
|
||||
if (h) hosts.add(h)
|
||||
}
|
||||
return Array.from(hosts)
|
||||
}
|
||||
|
||||
export function resolveDriveHostMap(paths: string[]): Record<string, string> {
|
||||
const map: Record<string, string> = {}
|
||||
for (const raw of paths) {
|
||||
const p = (raw || '').trim()
|
||||
if (!p || isNetworkPath(p)) continue
|
||||
const letter = extractDriveLetter(p)
|
||||
if (!letter || map[letter]) continue
|
||||
const unc = resolveUncForDrive(letter)
|
||||
if (!unc) continue
|
||||
const h = extractHostName(unc)
|
||||
if (h) map[letter] = h
|
||||
}
|
||||
return map
|
||||
}
|
||||
@@ -13,18 +13,20 @@ export interface ParsedSoonTemplate {
|
||||
frontImageUrl: string
|
||||
backImageUrl: string
|
||||
fields: TemplateFieldRow[]
|
||||
printFlag: number
|
||||
/** soon 模板 flag:1 双面 / 2 正面 / 3 背面 */
|
||||
templateFlag: number
|
||||
}
|
||||
|
||||
const SOON_FIELD_TYPES = new Set([1, 3, 4, 5])
|
||||
|
||||
export function readSoonPrintFlag(raw: Record<string, unknown>): number {
|
||||
/** soon 文件元数据 flag,用于与任务 print_flag 校验 */
|
||||
export function readSoonTemplateFlag(raw: Record<string, unknown>): number {
|
||||
const flag = Number(raw.flag)
|
||||
if (flag === 1 || flag === 2) return flag
|
||||
if (flag === 1 || flag === 2 || flag === 3) return flag
|
||||
const hasBack =
|
||||
!!String(raw.backDisplayPic ?? '').trim() ||
|
||||
(Array.isArray(raw.backData) && raw.backData.length > 0)
|
||||
return hasBack ? 2 : 1
|
||||
return hasBack ? 1 : 2
|
||||
}
|
||||
|
||||
function pickArray(obj: Record<string, unknown>, key: string): Record<string, unknown>[] {
|
||||
@@ -96,7 +98,7 @@ function parseSoonWorkerDisk(soonPath: string, raw: Record<string, unknown>): Pa
|
||||
frontImageUrl: toImageUrl(soonPath, frontPic),
|
||||
backImageUrl: toImageUrl(soonPath, backPic),
|
||||
fields,
|
||||
printFlag: readSoonPrintFlag(raw)
|
||||
templateFlag: readSoonTemplateFlag(raw)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +134,12 @@ function parseSoonLegacy(soonPath: string, raw: Record<string, unknown>): Parsed
|
||||
fields.push({ label: toFieldLabel(name, side), value, originName: name, fieldType: 5 })
|
||||
})
|
||||
|
||||
return { frontImageUrl, backImageUrl, fields, printFlag: readSoonPrintFlag(raw) }
|
||||
return {
|
||||
frontImageUrl,
|
||||
backImageUrl,
|
||||
fields,
|
||||
templateFlag: readSoonTemplateFlag(raw)
|
||||
}
|
||||
}
|
||||
|
||||
export function parseSoonTemplate(soonPath: string, raw: Record<string, unknown>): ParsedSoonTemplate {
|
||||
|
||||
Reference in New Issue
Block a user