a7cc69bb924bfe1396f9d492a2ae647967200db5
三层排查实锤(lldb 运行时取证): 1. 红点按钮真实 action 是 AppKit 私有 selector `_close:`(并非文档所称 performClose:),内部直接关窗,绕过 performClose/close 重写与 delegate 拦截 2. flutter create 模板的 AppDelegate 自带 applicationShouldTerminateAfterLastWindowClosed 返回 true——最后一个窗口 关闭即 NSApp.terminate,这是"点红点进程退出、托盘随进程消失"的根因 3. 修复:改为返回 false。窗口即便被真关闭,进程/HTTP 服务/托盘也保持运行, 托盘点击 makeKeyAndOrderFront 恢复窗口(releasedWhenClosed=NO,窗口对象在) 验证(红点 action 原样重放 `_close:`):窗口关闭后进程存活、服务在线、 托盘状态栏窗口存在、makeKeyAndOrderFront 可恢复窗口。 保留 MainFlutterWindow 的 performClose/close 重写(Cmd+W 等正常路径只隐藏 不关闭,体验更好)。
desktop-plugin(资安信插件助手)
资安信插件助手:桌面插件集成软件(目录名 desktop-plugin,Dart 包名 desktop_plugin,bundle id com.zax.desktop-plugin,App 显示名"资安信插件助手")。当前内置第一个插件"饭卡打印机",后续插件在 lib/ui/routes.dart 注册即自动出现在左侧菜单。关闭窗口 = 最小化到后台托盘,服务持续运行;退出入口在托盘菜单。
用 Flutter 重写打印站客户端,替代 C# 版 card-printer-agent。HTTP 协议完全不变,前端(biz-pc-cloud-zax/src/api/plugin/cardPrinter.ts)零改动,两版助手可并存灰度替换。
- 图标源:资安信 logo(
biz_zax_app_cloud/unpackage/res/icons/1024x1024.png深蓝盾牌+白色人形);dart run tool/gen_app_icons.dart <源图>重新生成 mac AppIcon/Windows ICO/托盘图标 - 总体计划见
../card-printer-agent/flutter-client-plan.md
协议(与 C# 版逐字段对齐)
监听 127.0.0.1:38200,被占用顺延 38201/38202;CORS *(含 OPTIONS 预检);响应统一 {code, msg, data},code=0 成功;JSON 字段一律驼峰。
| 端点 | 说明 |
|---|---|
GET /manifest |
{plugins:[…], agent:"desktop-plugin", backend:"pavo|cups", mock} |
GET /api/card-printer/status |
PrinterStatus(online/model/firmware/connection/ribbon/cardLow/cardLeft/status/statusText) |
POST /api/card-printer/print |
{copies, front:{color,k?}, back:{color,k?}} → {taskId} |
GET /api/card-printer/task/{taskId} |
{taskId, state:printing|done|error, status, statusText} |
任务串行执行(打印机物理一次一张),完成后保留 30 分钟。
目录结构
lib/
├── main.dart # Flutter 窗口入口(服务状态 + 打印机状态 + 运行日志)
├── agent.dart # 核心组合:参数解析 + 日志缓冲 + 后端/队列/服务装配(纯 Dart,无 Flutter 依赖;stop 幂等)
├── ui/
│ ├── routes.dart # 插件路由注册表(左侧菜单与内容页共用,加插件=加一项)
│ ├── app_shell.dart # 应用外壳:左侧插件菜单 + IndexedStack 内容区
│ ├── card_printer_page.dart # 饭卡打印机插件页:主状态一眼可懂 + 指标卡 + 技术详情折叠
│ └── app_tray.dart # 后台托盘:关窗隐藏(window_manager)/托盘菜单(tray_manager)/恢复窗口/退出
├── server/
│ ├── protocol.dart # 协议 DTO(toJson 手写驼峰,对齐 C# ApiModels.cs)
│ ├── http_server.dart # shelf 服务:端口顺延、CORS、四端点
│ └── print_queue.dart # 串行任务队列 + 30 分钟清理
├── print/
│ ├── backend.dart # PrintBackend 抽象 + OS 工厂
│ ├── mock_backend.dart # --mock 模拟(打印 2.5s 完成)
│ ├── cups_backend.dart # macOS:lpoptions 发现队列 + lp 提交(参数列表传参防注入)
│ └── pavo/ # Windows:HiTi PavoApi ffi 直打
│ ├── pavo_ffi.dart # 结构体/导出绑定(PavoApiW_x64.dll,W 后缀回退兼容)
│ ├── pavo_printer.dart # 枚举/状态/色带/单卡直打/轮询/内存管理(对齐 C# PavoPrinter.cs)
│ └── pavo_backend.dart # 后端实现(全部跑在 Isolate.run,主 isolate 不阻塞)
├── image/
│ └── card_image.dart # PNG data URL → 缩放铺白底 → BGR24/Gray8(对齐 C# CardImage.cs)
├── utils/
│ └── command_runner.dart # 外部命令执行(LC_ALL=C、超时杀进程、参数列表)
bin/agent.dart # headless 入口(开发联调,行为对齐 C# 控制台助手)
tool/make_test_card.dart # 生成 1014×642 测试卡(真实打印验收用)
常用命令
flutter pub get
flutter analyze
flutter test
# headless 联调(不需要窗口/真机)
dart run bin/agent.dart --mock # mock 模式,端口 38200-38202 顺延
dart run bin/agent.dart --port 38203 # 指定端口(本机 C# 助手占用时的开发姿势)
dart run bin/agent.dart # 真实模式(macOS 走 CUPS 队列)
# 测试卡 + 真实打印
dart run tool/make_test_card.dart
# 生成 data URL 后 POST /api/card-printer/print,观察 lpoptions 的 printer-state 4→3
# 构建
flutter build macos --release
本机开发注意(血泪坑,勿再踩)
- 同机 C# 助手占端口:测试前
launchctl unload ~/Library/LaunchAgents/com.zax.cardprinteragent.plist,测完launchctl load恢复;两个版本的 manifest.agent 字段可区分(card-printer-agent / desktop-plugin) - AMFI 卡死进程:早前测试遗留的 CardPrinterAgent 进程 STAT=UE/UNE,kill -9 无效,占用 38201 直到重启;Flutter 版会自动落到其他候选端口,前端自适应
- macOS 必须关 App Sandbox(
macos/Runner/*.entitlements已置空):沙箱拦截 127.0.0.1 端口绑定与 lp/lpoptions 子进程访问 CUPS - macos 平台目录生成前提:本机 flutter config 曾禁用
enable-macos-desktop且缺 CocoaPods,均已修复(brew install cocoapods;注意 brew 装 ca-certificates 链接冲突时先brew unlink再重装) - windows/ 目录无法在 Mac 生成:需在 Windows 实机上
flutter create --platforms windows .补齐(Phase 3)
进度(2026-09-21)
- Phase 1:协议服务器 + mock + 任务队列 + 端口顺延(单测全过,接口自测全过)
- Phase 2:macOS CUPS 后端(真机 status
HiTi CS-200e, 2.4.2 就绪;真实出卡一张,printer-state 4→3 约 24s) - Phase 3 代码:Windows PavoApi ffi 后端(analyze 零问题;windows/ 目录已从 SDK 模板手动实例化,vendor 收录 SDK DLL/bin 并在 CMake 构建后自动拷贝)——待 Windows 实机验收
- Phase 4 部分:插件化 UI(左侧菜单=路由注册表,主状态/指标/技术详情三层信息分级)+ 后台托盘(关窗不退出、托盘恢复窗口、退出仅托盘菜单)+ 资安信 logo 图标全家桶 + 改名"资安信插件助手"
- Phase 4 剩余:首次引导向导、URL scheme 唤起闭环(zaxprint scheme 已写入 Info.plist)、开机自启(LaunchAgent/HKCU Run)
- Phase 5:打包签名分发(mac ad-hoc 就地签名;参考 C# 版签名铁律)
已知坑(本项目实测)
- SliverList 里
Row(crossAxisAlignment: stretch)必须包IntrinsicHeight,否则子项收到 h=Infinity 直接布局断言(test/ui_smoke_test.dart 守护) - testWidgets 里
agent.start()/stop()是真实 socket I/O,必须tester.runAsync包裹,否则 fake async zone 永久挂起;且勿用 pumpAndSettle(页面含动画组件永不 settle) - image 包
Image()默认 3 通道不透明黑(透明图须 numChannels:4);compositeImage对全透明像素非 src-over 语义 - tray_manager 0.5.3:无双击事件(单击即 MouseDown);菜单须先 setContextMenu 再无参 popUpContextMenu
Windows 实机验收清单(Phase 3 收尾)
flutter create --platforms windows .对照生成的目录结构核对手动实例化的差异(或直接构建)flutter build windows --release编译过;确认 vendor 四文件拷到 exe 旁dart run bin/agent.dart(或直接跑 App)→ status 返回真机型号/固件/色带余量- 提交测试卡真实出卡(tool/make_test_card.dart 生成后走 print 接口)
- 顺带实测:不装 HiTi 官方驱动能否直打(PavoApiW_x64 疑似用户态直连 USB)
Languages
Dart
77.8%
C++
9.7%
CMake
5.8%
Inno Setup
1.8%
Swift
1.6%
Other
3.3%