feat: 向导内置 HiTi Mac 驱动一键安装(补齐计划 §3.2"驱动 dmg 放包内"设计)

- 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);一键安装的提权弹窗待用户实测
This commit is contained in:
dzc committed 2026-09-21 14:04:55 +08:00
1 parent 3ecac9a302
commit d475cac6ab
5 files changed
+173 -4

No files matched your search

+1
View File
@@ -44,3 +44,4 @@ app.*.map.json
/android/app/profile
/android/app/release
/dist/
assets/drivers/
+78
View File
@@ -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<File>()
.where((f) => f.path.toLowerCase().endsWith('.dmg'))
.toList();
return dmgs.isEmpty ? null : dmgs.first.path;
}
/// 从内置 dmg 静默安装驱动(挂载 → 找 pkg → osascript 提权 installer → 卸载卷)。
/// 提权弹窗由系统显示(一次管理员密码),超时 3 分钟。
static Future<void> 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<File>()
.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 (_) {}
}
}
}
+84 -4
View File
@@ -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<SetupWizard> {
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<SetupWizard> {
// ---------------- Step 1:打印机检测 ----------------
/// 一键安装内置驱动(mac):装完自动重检(队列要等打印机连上/系统识别后才出现)。
Future<void> _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<SetupWizard> {
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<SetupWizard> {
label: const Text('重新检测'),
),
] else ...[
const SizedBox(height: 12),
Text('打印机已连接,可以正常接收管理端提交的卡面打印。',
style: Theme.of(context).textTheme.bodySmall),
],
+2
View File
@@ -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:
+8
View File
@@ -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 拒绝