From 170e9ddc5ea7cd29c4f496388543f4dd0fbbeaef Mon Sep 17 00:00:00 2001 From: dzc <2388224167@qq.com> Date: Mon, 21 Sep 2026 16:38:11 +0800 Subject: [PATCH] =?UTF-8?q?feat(windows):=20=E5=90=91=E5=AF=BC=E5=86=85?= =?UTF-8?q?=E7=BD=AE=20HiTi=20Windows=20=E9=A9=B1=E5=8A=A8=E4=B8=80?= =?UTF-8?q?=E9=94=AE=E5=AE=89=E8=A3=85=EF=BC=88=E5=AF=B9=E9=BD=90=20mac=20?= =?UTF-8?q?=E7=9A=84"=E8=A3=85=20App=20=E5=8D=B3=E7=94=A8"=E4=BD=93?= =?UTF-8?q?=E9=AA=8C=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户实测定论:CS-2xx 的 USB 是自定义设备(驱动包内含 CS2xxhcd.inf 内核驱动), PavoApi 直打依赖驱动建立的 USB 通道——无驱动时枚举到设备但状态返回离线, "免驱动"假设不成立。产品要求两平台一致:驱动随 App 内置、向导一键安装。 - assets/drivers/CS2xx_Windows_Driver.zip:HiTi 官网 CS-2xx Windows 驱动 (2.6.25.48)整目录 zip(含 CS2xx/Setup64.exe),33MB,不入 git - DriverInstall Windows 分支:Expand-Archive 解压到临时目录 → 递归定位 Setup64.exe(兜底 setup/autorun)→ PowerShell Start-Process -Verb RunAs -Wait 提权安装(UAC 一次)→ 写安装标记(HKCU\Software\zax\desktop-plugin, 系统驱动安装状态无统一可查标志)→ finally 清理临时目录 - 向导第 2 步统一双平台状态机:驱动未装(异步查标记/目录)→"一键安装驱动 (内置安装包)";轮询 3 秒自动刷新安装/连接状态 - bundledInstallerPath 双平台资产路径推导(mac .dmg / win .zip); .gitignore 增 *.zip;assets/drivers/README 更新双平台放置约定 --- .gitignore | 2 + assets/drivers/README.md | 15 ++-- lib/setup/driver_install.dart | 153 ++++++++++++++++++++++++++-------- lib/ui/setup_wizard.dart | 39 ++++++--- 4 files changed, 156 insertions(+), 53 deletions(-) diff --git a/.gitignore b/.gitignore index c838b8d..bf7c75d 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,5 @@ app.*.map.json /android/app/release /dist/ assets/drivers/*.dmg +assets/drivers/*.exe +assets/drivers/*.zip diff --git a/assets/drivers/README.md b/assets/drivers/README.md index 8cd5006..a347c95 100644 --- a/assets/drivers/README.md +++ b/assets/drivers/README.md @@ -1,9 +1,10 @@ -# HiTi Mac 驱动安装包目录 +# HiTi 驱动安装包目录 -此目录用于打包时**内置 HiTi macOS 打印驱动**(向导"一键安装驱动"功能的数据源): +此目录用于打包时**内置 HiTi 打印驱动**(向导"一键安装驱动"功能的数据源): -- 构建 macOS 安装包前,把 `HiTiCardTypeMacDrivers_2.4.2.dmg`(约 25MB)放入本目录, - 然后执行 `bash tool/package_macos.sh`——驱动会随 dmg 安装包分发 -- **dmg 文件本身不入 git**(.gitignore 已忽略 `assets/drivers/*.dmg`) -- 缺少 dmg 时构建不受影响;向导会自动降级为"官网链接手动下载"模式 -- **Windows 不需要此目录放任何东西**(PavoApi 直打,驱动可选) +- **macOS**:放 `HiTiCardTypeMacDrivers_2.4.2.dmg`(约 25MB) +- **Windows**:放 `CS2xx_Windows_Driver.zip`(HiTi 官网 CS-2xx Windows 驱动整个文件夹打成 zip, + 内含 `CS2xx/Setup64.exe`;约 33MB)。mac 上打 zip 注意排除 `__MACOSX`: + `cd <驱动所在目录> && zip -r CS2xx_Windows_Driver.zip CS2_Driver_x.x.x.x -x "*.DS_Store"` +- 安装包文件**不入 git**(.gitignore 已忽略 `*.dmg/*.exe/*.zip`),构建安装包的机器放好后再打包 +- 缺失时构建不受影响;向导自动降级为"官网链接手动下载"模式 diff --git a/lib/setup/driver_install.dart b/lib/setup/driver_install.dart index a3c3a3b..232bd42 100644 --- a/lib/setup/driver_install.dart +++ b/lib/setup/driver_install.dart @@ -2,44 +2,85 @@ import 'dart:io'; import '../utils/command_runner.dart'; -/// HiTi Mac 驱动(CUPS 过滤器)内置安装:App 资源携带驱动 dmg, -/// 向导一键挂载静默安装(osascript 提权一次),已装判断 = /Library/Printers/HiTi 存在。 -/// Windows 不适用(PavoApi 直打,官方驱动可选)。 +/// HiTi 打印驱动内置安装:App 资源携带驱动安装包,向导一键安装。 +/// +/// - macOS:CUPS 过滤器 pkg(dmg 内置),已装判断 = /Library/Printers/HiTi 存在, +/// 安装 = 挂载 dmg → osascript 提权 installer → 卸载卷 +/// - Windows:HiTi 官方驱动 exe(assets/drivers/*.exe 内置),UAC 提权运行; +/// 已装判断 = 我们自己的注册表标记(HKCU\Software\zax\desktop-plugin, +/// 系统级驱动安装状态无统一可查标志,安装成功后写标记最可靠) class DriverInstall { static const installedMarker = '/Library/Printers/HiTi'; + static const _winMarkKey = r'HKCU\Software\zax\desktop-plugin'; + static const _winMarkValue = 'HiTiDriverInstalled'; - static bool get isSupported => Platform.isMacOS; + static bool get isSupported => Platform.isMacOS || Platform.isWindows; - /// 驱动是否已安装。 - static bool isInstalled() => Directory(installedMarker).existsSync(); - - /// 内置驱动 dmg 的文件系统路径;未打包携带(如开发运行/产物缺失)返回 null。 - /// - /// Flutter assets 在 .app 内的真实路径: - /// [app]/Contents/Frameworks/App.framework/Resources/flutter_assets/[asset] - /// 从可执行文件位置反推(exe 在 [app]/Contents/MacOS/)。 - static String? bundledDmgPath() { - if (!Platform.isMacOS) return null; - final exe = Platform.resolvedExecutable; - final exeDir = File(exe).parent.path; // .../Contents/MacOS - final driversDir = - '$exeDir/../Frameworks/App.framework/Resources/flutter_assets/assets/drivers'; - final dir = Directory(driversDir); - if (!dir.existsSync()) return null; - final dmgs = dir - .listSync() - .whereType() - .where((f) => f.path.toLowerCase().endsWith('.dmg')) - .toList(); - return dmgs.isEmpty ? null : dmgs.first.path; + /// 驱动是否已安装(macOS:驱动目录;Windows:安装标记,同步场景请用 queryInstalled)。 + static bool isInstalled() { + if (Platform.isMacOS) return Directory(installedMarker).existsSync(); + return false; // Windows 走异步 queryInstalled(注册表查询需要子进程) } - /// 从内置 dmg 静默安装驱动(挂载 → 找 pkg → osascript 提权 installer → 卸载卷)。 - /// 提权弹窗由系统显示(一次管理员密码),超时 3 分钟。 - static Future installFromDmg(String dmgPath, {void Function(String line)? onLog}) async { - final log = onLog ?? (_) {}; + /// Windows:异步查询安装标记。 + static Future queryInstalled() async { + if (!Platform.isWindows) return isInstalled(); + final out = await runCommand( + 'reg', + ['query', _winMarkKey, '/v', _winMarkValue], + tolerateExit: true, + ); + return out.contains('0x1'); + } - // 1. 只读挂载(挂载点带随机序号,必须从输出解析,不能拼名字) + /// 内置驱动安装包的文件系统路径;未打包携带返回 null。 + /// + /// Flutter assets 在包内的真实路径(两平台结构不同): + /// - macOS:[exe]/../Frameworks/App.framework/Resources/flutter_assets + /// - Windows:[exe]/data/flutter_assets + static String? bundledInstallerPath() { + if (Platform.isMacOS) { + final exeDir = File(Platform.resolvedExecutable).parent.path; + return _firstFile( + '$exeDir/../Frameworks/App.framework/Resources/flutter_assets/assets/drivers', + '.dmg'); + } + if (Platform.isWindows) { + final exeDir = File(Platform.resolvedExecutable).parent.path; + return _firstFile('$exeDir/data/flutter_assets/assets/drivers', '.zip'); + } + return null; + } + + static String? _firstFile(String dirPath, String ext) { + final dir = Directory(dirPath); + if (!dir.existsSync()) return null; + final files = dir + .listSync() + .whereType() + .where((f) => f.path.toLowerCase().endsWith(ext)) + .toList(); + return files.isEmpty ? null : files.first.path; + } + + /// 静默安装(macOS)/提权安装(Windows,UAC 确认一次)。 + static Future install(String installerPath, {void Function(String line)? onLog}) async { + final log = onLog ?? (_) {}; + if (Platform.isMacOS) { + await _installMac(installerPath, log); + return; + } + if (Platform.isWindows) { + await _installWindows(installerPath, log); + return; + } + throw UnsupportedError('当前平台不支持驱动安装'); + } + + // ---------------- macOS:dmg → pkg → osascript 提权 ---------------- + + static Future _installMac(String dmgPath, void Function(String) log) async { + // 挂载点带随机序号,必须从 attach 输出解析,不能拼名字 final attachOut = await runCommand('hdiutil', ['attach', '-nobrowse', '-readonly', dmgPath], timeoutMs: 30000); final match = RegExp(r'/Volumes/[^\t\n]+').allMatches(attachOut); @@ -50,7 +91,6 @@ class DriverInstall { log('驱动卷已挂载:$mountPoint'); try { - // 2. 找 pkg final pkg = Directory(mountPoint) .listSync() .whereType() @@ -61,7 +101,6 @@ class DriverInstall { } final pkgPath = pkg.first.path; - // 3. osascript 提权安装(AppleScript 字符串内嵌套引号用 \" 转义) final script = 'do shell script "installer -pkg \\"$pkgPath\\" -target /" ' 'with administrator privileges with prompt "安装 HiTi 打印机驱动"'; @@ -69,10 +108,56 @@ class DriverInstall { await runCommand('osascript', ['-e', script], timeoutMs: 180000); log('驱动安装完成'); } finally { - // 4. 卸载卷(失败不影响安装结果) try { await runCommand('hdiutil', ['detach', mountPoint, '-force'], tolerateExit: true); } catch (_) {} } } + + // ---------------- Windows:解压 zip → UAC 提权 Setup64 → 写安装标记 ---------------- + + static Future _installWindows(String zipPath, void Function(String) log) async { + // 1. 解压到临时目录(驱动是"安装器 + 数据文件"的多文件结构,内置为 zip) + final extractDir = + '${Directory.systemTemp.path}/zax-hiti-driver-${DateTime.now().millisecondsSinceEpoch}'; + log('解压驱动安装包…'); + await runCommand('powershell', [ + '-Command', + 'Expand-Archive -Path "$zipPath" -DestinationPath "$extractDir" -Force' + ], timeoutMs: 120000); + + try { + // 2. 递归找安装器(优先 64 位 Setup64.exe,兜底 autorun.exe) + final setup = _findFileDeep(extractDir, 'setup64.exe') ?? + _findFileDeep(extractDir, 'setup.exe') ?? + _findFileDeep(extractDir, 'autorun.exe'); + if (setup == null) { + throw StateError('驱动包内未找到安装程序(Setup64.exe)'); + } + log('开始安装 HiTi 打印机驱动(请在 UAC 弹窗中确认,安装向导可能需要点几次"下一步")…'); + // -Verb RunAs 触发 UAC;-Wait 等安装程序退出(用户取消 UAC 时 PowerShell 报错) + final script = 'Start-Process -FilePath "$setup" -Verb RunAs -Wait'; + await runCommand('powershell', ['-Command', script], timeoutMs: 600000); + log('驱动安装程序已退出'); + } finally { + // 3. 清理临时目录(失败不影响安装结果) + try { + await Directory(extractDir).delete(recursive: true); + } catch (_) {} + } + + // 4. 写安装标记(我们自己的注册表键;0x1 = 已装) + await runCommand('reg', + ['add', _winMarkKey, '/v', _winMarkValue, '/t', 'REG_DWORD', '/d', '1', '/f']); + log('已记录驱动安装状态'); + } + + static String? _findFileDeep(String root, String lowerName) { + final hits = Directory(root) + .listSync(recursive: true, followLinks: false) + .whereType() + .where((f) => f.uri.pathSegments.last.toLowerCase() == lowerName) + .toList(); + return hits.isEmpty ? null : hits.first.path; + } } diff --git a/lib/ui/setup_wizard.dart b/lib/ui/setup_wizard.dart index bf24281..8f86ea3 100644 --- a/lib/ui/setup_wizard.dart +++ b/lib/ui/setup_wizard.dart @@ -48,17 +48,22 @@ class _SetupWizardState extends State { String? _driverInstallError; PrinterStatus? _printerStatus; + /// 驱动是否已装(null=查询中;mac=驱动目录,win=注册表安装标记) + bool? _driverInstalled; + Agent get _agent => widget.agent; @override void initState() { super.initState(); _checkPrinter(); + _queryDriver(); // 第 2 步期间自动轮询:用户手动装驱动 / 插上打印机后页面自动刷新 // (驱动目录判断零成本;getStatus 走 lpoptions,3s 一次可接受) _pollTimer = Timer.periodic(const Duration(seconds: 3), (_) { if (_step == 1 && mounted && !_driverInstalling) { _checkPrinter(silent: true); + _queryDriver(); } }); } @@ -71,6 +76,11 @@ class _SetupWizardState extends State { super.dispose(); } + Future _queryDriver() async { + final installed = await DriverInstall.queryInstalled(); + if (mounted) setState(() => _driverInstalled = installed); + } + Future _checkPrinter({bool silent = false}) async { if (!silent) setState(() => _printerChecking = true); try { @@ -192,16 +202,17 @@ class _SetupWizardState extends State { // ---------------- Step 1:打印机检测 ---------------- - /// 一键安装内置驱动(mac):装完自动重检(队列要等打印机连上/系统识别后才出现)。 + /// 一键安装内置驱动:装完自动重检(队列要等打印机连上/系统识别后才出现)。 Future _installDriver() async { - final dmg = DriverInstall.bundledDmgPath(); - if (dmg == null) return; + final installer = DriverInstall.bundledInstallerPath(); + if (installer == null) return; setState(() { _driverInstalling = true; _driverInstallError = null; }); try { - await DriverInstall.installFromDmg(dmg, onLog: _agent.log.write); + await DriverInstall.install(installer, onLog: _agent.log.write); + await _queryDriver(); await _checkPrinter(); } catch (e) { if (mounted) setState(() => _driverInstallError = '$e'); @@ -213,8 +224,8 @@ class _SetupWizardState extends State { Widget _stepPrinter(ColorScheme scheme) { final s = _printerStatus; final ready = s != null && s.online; - final driverMissing = Platform.isMacOS && !DriverInstall.isInstalled(); - final bundledDmg = Platform.isMacOS ? DriverInstall.bundledDmgPath() : null; + final driverMissing = _driverInstalled == false; + final bundledInstaller = DriverInstall.bundledInstallerPath(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -227,17 +238,19 @@ class _SetupWizardState extends State { ), const SizedBox(height: 12), - // mac:驱动未装 → 首选一键安装内置驱动,其次官网链接 + // 驱动未装 → 首选一键安装内置驱动,其次官网链接(mac/win 通用) if (driverMissing) ...[ Text( - '本机尚未安装 HiTi 打印机驱动(mac 打印必需的系统组件)。', + Platform.isMacOS + ? '本机尚未安装 HiTi 打印机驱动(mac 打印必需的系统组件)。' + : '本机尚未安装 HiTi 打印机驱动。直打通道需要驱动建立的 USB 连接,装好即可自动识别打印机。', style: Theme.of(context).textTheme.bodySmall?.copyWith(height: 1.6), ), const SizedBox(height: 8), Row( children: [ FilledButton.tonalIcon( - onPressed: _driverInstalling || bundledDmg == null ? null : _installDriver, + onPressed: _driverInstalling || bundledInstaller == null ? null : _installDriver, icon: _driverInstalling ? const SizedBox( width: 14, @@ -246,7 +259,7 @@ class _SetupWizardState extends State { ) : const Icon(Icons.download, size: 16), label: Text(_driverInstalling - ? '正在安装…(弹窗中输入管理员密码)' + ? '正在安装…(请在系统弹窗中确认)' : '一键安装驱动(内置安装包)'), ), const SizedBox(width: 8), @@ -257,7 +270,7 @@ class _SetupWizardState extends State { ), ], ), - if (bundledDmg == null) + if (bundledInstaller == null) Padding( padding: const EdgeInsets.only(top: 4), child: Text( @@ -278,7 +291,9 @@ class _SetupWizardState extends State { ), const SizedBox(height: 8), Text( - '安装完成后页面会自动刷新(也可手动重新检测);随后 USB 连接打印机并开机,系统识别出现打印队列即完成。', + Platform.isMacOS + ? '安装完成后:USB 连接打印机并开机,系统识别出现打印队列即完成。' + : '安装完成后 USB 连接打印机并开机,回到这里重新检测即完成。', style: Theme.of(context).textTheme.bodySmall, ), const SizedBox(height: 4),