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:
24kycj
2026-06-11 10:34:05 +08:00
parent f1b73ee3d3
commit 931fcf90a4
24 changed files with 840 additions and 126 deletions
+26
View File
@@ -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[] }) => {
+92
View File
@@ -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()
}
+56
View File
@@ -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 -6
View File
@@ -13,18 +13,20 @@ export interface ParsedSoonTemplate {
frontImageUrl: string
backImageUrl: string
fields: TemplateFieldRow[]
printFlag: number
/** soon 模板 flag1 双面 / 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 {