From d475cac6abf691d883fe654985edde3c66b49453 Mon Sep 17 00:00:00 2001 From: dzc <2388224167@qq.com> Date: Mon, 21 Sep 2026 14:04:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=90=91=E5=AF=BC=E5=86=85=E7=BD=AE=20?= =?UTF-8?q?HiTi=20Mac=20=E9=A9=B1=E5=8A=A8=E4=B8=80=E9=94=AE=E5=AE=89?= =?UTF-8?q?=E8=A3=85=EF=BC=88=E8=A1=A5=E9=BD=90=E8=AE=A1=E5=88=92=20=C2=A7?= =?UTF-8?q?3.2"=E9=A9=B1=E5=8A=A8=20dmg=20=E6=94=BE=E5=8C=85=E5=86=85"?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - setup/driver_install.dart:检测已装(/Library/Printers/HiTi)+ 内置 dmg 定位 (从 resolvedExecutable 反推 flutter_assets/assets/drivers/,缺失降级官网链接) + 静默安装(hdiutil 挂载→定位 pkg→osascript 提权 installer→finally detach; 挂载点名带随机序号必须从 attach 输出解析) - 向导第 2 步重做状态机:驱动未装 → "一键安装驱动(内置安装包)"按钮 (安装中提示输管理员密码,失败显示原因);驱动已装但无队列 → 连接/添加队列 引导;无内置 dmg 的构建回退官网链接 - assets/drivers/ 收驱动 dmg(.gitignore 不入库,打包脚本检查缺失并警告, 当前从 card-printer-agent/dist 拷入 2.4.2 版,25MB) - 实测:挂载/pkg 定位/detach 免密链路全通过;打包 47MB dmg 内含驱动, 冒烟启动正常(服务已占主端口 38200);一键安装的提权弹窗待用户实测 --- .gitignore | 1 + lib/setup/driver_install.dart | 78 +++++++++++++++++++++++++++++++ lib/ui/setup_wizard.dart | 88 +++++++++++++++++++++++++++++++++-- pubspec.yaml | 2 + tool/package_macos.sh | 8 ++++ 5 files changed, 173 insertions(+), 4 deletions(-) create mode 100644 lib/setup/driver_install.dart diff --git a/.gitignore b/.gitignore index 8f41729..1749f2c 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,4 @@ app.*.map.json /android/app/profile /android/app/release /dist/ +assets/drivers/ diff --git a/lib/setup/driver_install.dart b/lib/setup/driver_install.dart new file mode 100644 index 0000000..a3c3a3b --- /dev/null +++ b/lib/setup/driver_install.dart @@ -0,0 +1,78 @@ +import 'dart:io'; + +import '../utils/command_runner.dart'; + +/// HiTi Mac 驱动(CUPS 过滤器)内置安装:App 资源携带驱动 dmg, +/// 向导一键挂载静默安装(osascript 提权一次),已装判断 = /Library/Printers/HiTi 存在。 +/// Windows 不适用(PavoApi 直打,官方驱动可选)。 +class DriverInstall { + static const installedMarker = '/Library/Printers/HiTi'; + + static bool get isSupported => Platform.isMacOS; + + /// 驱动是否已安装。 + 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; + } + + /// 从内置 dmg 静默安装驱动(挂载 → 找 pkg → osascript 提权 installer → 卸载卷)。 + /// 提权弹窗由系统显示(一次管理员密码),超时 3 分钟。 + static Future installFromDmg(String dmgPath, {void Function(String line)? onLog}) async { + final log = onLog ?? (_) {}; + + // 1. 只读挂载(挂载点带随机序号,必须从输出解析,不能拼名字) + final attachOut = await runCommand('hdiutil', ['attach', '-nobrowse', '-readonly', dmgPath], + timeoutMs: 30000); + final match = RegExp(r'/Volumes/[^\t\n]+').allMatches(attachOut); + if (match.isEmpty) { + throw StateError('驱动 dmg 挂载失败'); + } + final mountPoint = match.last.group(0)!; + log('驱动卷已挂载:$mountPoint'); + + try { + // 2. 找 pkg + final pkg = Directory(mountPoint) + .listSync() + .whereType() + .where((f) => f.path.toLowerCase().endsWith('.pkg')) + .toList(); + if (pkg.isEmpty) { + throw StateError('驱动 dmg 内未找到 .pkg 安装包'); + } + final pkgPath = pkg.first.path; + + // 3. osascript 提权安装(AppleScript 字符串内嵌套引号用 \" 转义) + final script = + 'do shell script "installer -pkg \\"$pkgPath\\" -target /" ' + 'with administrator privileges with prompt "安装 HiTi 打印机驱动"'; + log('开始安装驱动(请在系统弹窗中输入管理员密码)…'); + await runCommand('osascript', ['-e', script], timeoutMs: 180000); + log('驱动安装完成'); + } finally { + // 4. 卸载卷(失败不影响安装结果) + try { + await runCommand('hdiutil', ['detach', mountPoint, '-force'], tolerateExit: true); + } catch (_) {} + } + } +} diff --git a/lib/ui/setup_wizard.dart b/lib/ui/setup_wizard.dart index ebfb01d..b2cac5b 100644 --- a/lib/ui/setup_wizard.dart +++ b/lib/ui/setup_wizard.dart @@ -6,6 +6,7 @@ import 'package:url_launcher/url_launcher.dart'; import '../agent.dart'; import '../server/protocol.dart'; import '../setup/autostart.dart'; +import '../setup/driver_install.dart'; import '../setup/first_run.dart'; import '../setup/url_scheme.dart'; @@ -42,6 +43,8 @@ class _SetupWizardState extends State { bool _autostart = true; bool _autostartBusy = false; bool _printerChecking = false; + bool _driverInstalling = false; + String? _driverInstallError; PrinterStatus? _printerStatus; Agent get _agent => widget.agent; @@ -173,9 +176,29 @@ class _SetupWizardState extends State { // ---------------- Step 1:打印机检测 ---------------- + /// 一键安装内置驱动(mac):装完自动重检(队列要等打印机连上/系统识别后才出现)。 + Future _installDriver() async { + final dmg = DriverInstall.bundledDmgPath(); + if (dmg == null) return; + setState(() { + _driverInstalling = true; + _driverInstallError = null; + }); + try { + await DriverInstall.installFromDmg(dmg, onLog: _agent.log.write); + await _checkPrinter(); + } catch (e) { + if (mounted) setState(() => _driverInstallError = '$e'); + } finally { + if (mounted) setState(() => _driverInstalling = false); + } + } + 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; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -186,11 +209,69 @@ class _SetupWizardState extends State { okText: '打印机就绪${s != null && s.model.isNotEmpty ? "(${s.model})" : ""}', badText: s == null ? '未检测到打印队列' : (s.statusText.isEmpty ? '打印机未就绪' : s.statusText), ), - if (!ready) ...[ - const SizedBox(height: 12), + const SizedBox(height: 12), + + // mac:驱动未装 → 首选一键安装内置驱动,其次官网链接 + if (driverMissing) ...[ + Text( + '本机尚未安装 HiTi 打印机驱动(mac 打印必需的系统组件)。', + style: Theme.of(context).textTheme.bodySmall?.copyWith(height: 1.6), + ), + const SizedBox(height: 8), + Row( + children: [ + FilledButton.tonalIcon( + onPressed: _driverInstalling || bundledDmg == null ? null : _installDriver, + icon: _driverInstalling + ? const SizedBox( + width: 14, + height: 14, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.download, size: 16), + label: Text(_driverInstalling + ? '正在安装…(弹窗中输入管理员密码)' + : '一键安装驱动(内置安装包)'), + ), + const SizedBox(width: 8), + TextButton.icon( + onPressed: () => launchUrl(Uri.parse(_driverUrl)), + icon: const Icon(Icons.open_in_new, size: 14), + label: const Text('改为手动下载'), + ), + ], + ), + if (bundledDmg == null) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Text( + '(当前构建未内置驱动安装包,请用官网链接手动安装)', + style: Theme.of(context).textTheme.bodySmall, + ), + ), + if (_driverInstallError != null) + Padding( + padding: const EdgeInsets.only(top: 6), + child: Text( + '安装失败:$_driverInstallError', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: scheme.error), + ), + ), + const SizedBox(height: 8), + Text( + '安装完成后:USB 连接打印机并开机,系统识别出现打印队列即完成。', + style: Theme.of(context).textTheme.bodySmall, + ), + ] + + // 驱动已装但打印机未就绪 → 引导连接/添加队列 + else if (!ready) ...[ Text( Platform.isMacOS - ? '安装步骤:① 打开 HiTi 官网下载 macOS 驱动(HiTiCardTypeMacDrivers dmg),双击安装里面的 pkg;② USB 连接打印机并开机;③ 系统设置 → 打印机与扫描仪 确认出现 HiTi 打印机;④ 回到这里点「重新检测」。' + ? '驱动已安装。请确认:① 打印机 USB 已连接并开机;② 系统设置 → 打印机与扫描仪 里出现 HiTi 打印机(没有的话点下方「+」添加,选 HiTi CS-200e);③ 回到这里重新检测。' : 'Windows 打印站:打开 HiTi 官网下载 CS-2xx Windows 驱动并安装,USB 连接打印机;本应用也支持免驱动的 SDK 直连模式,装完驱动或接好打印机后点「重新检测」。', style: Theme.of(context).textTheme.bodySmall?.copyWith(height: 1.6), ), @@ -210,7 +291,6 @@ class _SetupWizardState extends State { label: const Text('重新检测'), ), ] else ...[ - const SizedBox(height: 12), Text('打印机已连接,可以正常接收管理端提交的卡面打印。', style: Theme.of(context).textTheme.bodySmall), ], diff --git a/pubspec.yaml b/pubspec.yaml index 9b96b7b..c3ce963 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -75,6 +75,8 @@ flutter: - assets/tray_icon_mac.png - assets/tray_icon_win.png - assets/logo.png + # HiTi Mac 驱动安装包(不进 git:放 assets/drivers/*.dmg 后重新打包;缺失时向导降级为官网链接) + - assets/drivers/ # To add assets to your application, add an assets section to your flutter # configuration like this: diff --git a/tool/package_macos.sh b/tool/package_macos.sh index 2c1ae00..c7a461e 100755 --- a/tool/package_macos.sh +++ b/tool/package_macos.sh @@ -12,6 +12,14 @@ DMG="dist/${APP_NAME}-macOS.dmg" echo "==> flutter build macos --release" flutter build macos --release +echo "==> 检查内置驱动" +if ls assets/drivers/*.dmg >/dev/null 2>&1; then + echo "已内置:$(ls assets/drivers/*.dmg)" +else + echo "警告:assets/drivers/ 下没有驱动 dmg,产出的安装包在向导中将只提供官网链接(一键安装不可用)。" + echo " 将 HiTiCardTypeMacDrivers_2.4.2.dmg 放入 assets/drivers/ 后重新打包即可内置。" +fi + echo "==> ad-hoc 就地签名(含全部嵌入内容;构建产物位于最终位置,直接在此重签)" # 注意:ad-hoc 签名不要加 --options=runtime——hardened runtime 的库校验(library validation) # 要求所有嵌入库与主程序同 Team ID,ad-hoc 无 Team 会导致 framework 加载被 dyld 拒绝