feat: 本地日志文件系统 + 修复双击打不开/向导完成无反应

用户实测两个问题(先装好驱动的机器):
1. 双击 App 图标不一定能打开——根因 single_instance:唤醒端口 38210 被
   残留占用(上次异常退出/TIME_WAIT)时,connect 无应答仍返回 false →
   main 直接 exit(0)。修复:端口被占但无应答判定为残留占用,照常启动
   (仅唤醒功能降级,FileLog 留痕);有应答才作为第二实例退出。
2. 向导第三步点"完成"没反应——根因 _finish 只有 finally 没有 catch:
   Autostart/UrlScheme(reg 三连)/FirstRun 任一抛异常被静默吞,Navigator.pop
   永不执行=对话框锁死。修复:每步独立 try-catch + 分步日志,成败都关闭
   向导,失败汇总 SnackBar 提示"详见运行日志"。

本地日志文件系统(用户要求:仔细记录、可发回分析、定时删除):
- lib/utils/file_log.dart:按天滚动 app-YYYYMMDD.log、单文件 5MB 轮转加
  序号、启动清理 7 天前旧文件(按文件名日期判定);全同步追加写(低频
  日志,保证异常退出前落盘);目录 mac ~/Library/Logs/zax-desktop-plugin/
  win %APPDATA%\zax-desktop-plugin\logs\(不引第三方依赖)
- AgentLog.write 挂文件输出(一处改动覆盖全部 22 个现有调用点);
  main.dart FileLog.init 提到最前,bootWarning/startError 全部落日志,
  启动横幅记录平台/exe 路径/参数
- pavo isolate 的 [pavo-diag] 诊断日志传日志路径(纯数据,isolate 安全)
  直接写文件——GUI 模式下 isolate stdout 本来就丢
- 插件页技术详情加"打开日志文件夹"按钮(Explorer/Finder 直达,用户打包
  app-*.log 发回分析)

19 个测试全过、analyze 零问题。
This commit is contained in:
dzc committed 2026-09-22 15:09:24 +08:00
1 parent bf0de9b8b5
commit 726210edcb
9 files changed
+302 -27

No files matched your search

+37 -4
View File
@@ -1,9 +1,11 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import '../agent.dart';
import '../server/protocol.dart';
import '../utils/file_log.dart';
/// 饭卡打印机插件页:主状态一眼可懂,技术参数收进"技术详情"。
class CardPrinterPage extends StatefulWidget {
@@ -40,6 +42,21 @@ class _CardPrinterPageState extends State<CardPrinterPage> {
super.dispose();
}
/// 资源管理器/Finder 打开日志目录(用户取 app-*.log 发回分析)。
Future<void> _openLogFolder() async {
final dir = FileLog.dir;
if (dir == null) return;
try {
if (Platform.isWindows) {
await Process.start('explorer', [dir], mode: ProcessStartMode.detached);
} else if (Platform.isMacOS) {
await Process.start('open', [dir], mode: ProcessStartMode.detached);
}
} catch (e) {
_agent.log.write('打开日志文件夹失败:$e(目录:$dir)');
}
}
Future<void> _refreshStatus() async {
setState(() => _loading = true);
try {
@@ -418,10 +435,26 @@ class _CardPrinterPageState extends State<CardPrinterPage> {
('色带剩余张数', s != null && s.ribbon.remainCount >= 0 ? '${s.ribbon.remainCount} 张' : '—'),
]),
const SizedBox(height: 14),
Text('运行日志(最近 50 条)',
style: Theme.of(context).textTheme.labelMedium?.copyWith(
color: scheme.onSurfaceVariant,
)),
Row(
children: [
Expanded(
child: Text('运行日志(最近 50 条)',
style: Theme.of(context).textTheme.labelMedium?.copyWith(
color: scheme.onSurfaceVariant,
)),
),
if (FileLog.dir != null)
TextButton.icon(
onPressed: _openLogFolder,
icon: const Icon(Icons.folder_open, size: 14),
label: const Text('打开日志文件夹'),
style: TextButton.styleFrom(
textStyle: Theme.of(context).textTheme.bodySmall,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
),
),
],
),
const SizedBox(height: 6),
Container(
width: double.infinity,
+39 -6
View File
@@ -95,18 +95,51 @@ class _SetupWizardState extends State<SetupWizard> {
Future<void> _finish() async {
setState(() => _autostartBusy = true);
final failures = <String>[];
_agent.log.write('向导完成:开始收尾配置');
try {
if (_autostart && Autostart.isSupported) {
await Autostart.enable();
try {
_agent.log.write('向导完成:写入开机自启($_autostart)…');
await Autostart.enable();
_agent.log.write('向导完成:开机自启写入成功');
} catch (e) {
final msg = '开机自启设置失败:$e';
failures.add(msg);
_agent.log.write('向导完成:$msg');
}
}
try {
_agent.log.write('向导完成:注册 zaxprint:// 协议…');
await UrlScheme.ensureRegistered();
_agent.log.write('向导完成:协议注册成功');
} catch (e) {
final msg = '网页唤起协议注册失败:$e';
failures.add(msg);
_agent.log.write('向导完成:$msg');
}
try {
await FirstRun.complete();
_agent.log.write('向导完成:完成标记已写入');
} catch (e) {
final msg = '完成标记写入失败(下次启动会重新弹出向导):$e';
failures.add(msg);
_agent.log.write('向导完成:$msg');
}
await UrlScheme.ensureRegistered();
await FirstRun.complete();
} finally {
if (mounted) setState(() => _autostartBusy = false);
}
if (mounted) {
Navigator.of(context).pop();
widget.onFinished();
if (!mounted) return;
// 无论成败都关闭向导——完成动作失败不应把用户锁死在向导里
Navigator.of(context).pop();
widget.onFinished();
if (failures.isNotEmpty && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('部分配置未完成(详见运行日志):${failures.length} 项'),
duration: const Duration(seconds: 5),
),
);
}
}