diff --git a/client/lib/bridge/log.dart b/client/lib/bridge/log.dart index bb835ca..b38452c 100644 --- a/client/lib/bridge/log.dart +++ b/client/lib/bridge/log.dart @@ -2,14 +2,61 @@ // 格式: `[时:分:秒.毫秒] [tag] msg`,全部走 logLine(),便于排查时序。 // 注:sing-box 自身时间戳已在下发配置里关闭(log.timestamp=false), // 避免与这里的前缀重复打印两个时间。 +// +// 除 stdout 外,桌面端还把日志落到固定文件(每次启动截断重写),便于在 GUI/管理员 +// 启动、无法用控制台 Tee 抓 stdout 时排障: +// Windows: %LOCALAPPDATA%\Pangolin\pangolin.log +// macOS/Linux: ~/.pangolin/pangolin.log // ignore_for_file: avoid_print +import 'dart:io'; + String _ts() { final n = DateTime.now(); String p(int v, [int w = 2]) => v.toString().padLeft(w, '0'); return '${p(n.hour)}:${p(n.minute)}:${p(n.second)}.${p(n.millisecond, 3)}'; } -/// 打印一行带时间戳与标签的桌面端日志。 -void logLine(String tag, String msg) => print('[${_ts()}] [$tag] $msg'); +File? _logFile; +bool _logInit = false; + +File? _openLogFile() { + try { + String? base; + String sep = '/'; + String sub = '.pangolin'; + if (Platform.isWindows) { + base = Platform.environment['LOCALAPPDATA']; + sep = '\\'; + sub = 'Pangolin'; + } else if (Platform.isMacOS || Platform.isLinux) { + base = Platform.environment['HOME']; + } else { + return null; // 移动端不落文件 + } + if (base == null || base.isEmpty) return null; + final dir = Directory('$base$sep$sub'); + if (!dir.existsSync()) dir.createSync(recursive: true); + final f = File('${dir.path}${sep}pangolin.log'); + f.writeAsStringSync('=== session ${DateTime.now()} ===\n'); // 每次启动截断重写 + return f; + } catch (_) { + return null; + } +} + +/// 打印一行带时间戳与标签的桌面端日志(stdout + 桌面端日志文件)。 +void logLine(String tag, String msg) { + final line = '[${_ts()}] [$tag] $msg'; + print(line); + try { + if (!_logInit) { + _logInit = true; + _logFile = _openLogFile(); + } + _logFile?.writeAsStringSync('$line\n', mode: FileMode.append); + } catch (_) { + // 落盘失败不影响运行。 + } +} diff --git a/client/lib/main.dart b/client/lib/main.dart index 1121e23..14d4d6c 100644 --- a/client/lib/main.dart +++ b/client/lib/main.dart @@ -129,13 +129,14 @@ class _RootFlowState extends ConsumerState { // 注意:不在此处早读 settingsProvider.autoConnect —— 其 _load 是异步(SharedPreferences), // 启动这一刻多半还没加载完,会读到默认 false 而永久跳过。改到「节点就绪」回调里再判: // 节点要走网络拉取,远慢于本地设置读取,那时 autoConnect 必已加载到位。 - // 必须标全泛型:裸 ProviderSubscription(=)会让 listenManual 推断成 dynamic, - // next 变 dynamic → valueOrNull(扩展 getter)在 dynamic 上不解析 → 运行期 NoSuchMethod。 + // 不用回调的 next(裸 ProviderSubscription 会让它退化成 dynamic,valueOrNull 这种 + // 扩展 getter 在 dynamic 上不解析 → 运行期 NoSuchMethod)。直接 ref.read 拿强类型值。 late final ProviderSubscription>> sub; sub = ref.listenManual(nodesProvider, (prev, next) { - final nodes = next.valueOrNull ?? const []; + final av = ref.read(nodesProvider); + final nodes = av.valueOrNull ?? const []; final ready = nodes.any((n) => n.uuid.isNotEmpty); - logLine('AutoConnect', 'nodes update: total=${nodes.length} ready=$ready loading=${next.isLoading} err=${next.hasError}'); + logLine('AutoConnect', 'nodes update: total=${nodes.length} ready=$ready loading=${av.isLoading} err=${av.hasError}'); if (!ready) return; sub.close(); final ac = ref.read(settingsProvider).autoConnect; diff --git a/client/pubspec.yaml b/client/pubspec.yaml index 90b2998..6d86c3e 100644 --- a/client/pubspec.yaml +++ b/client/pubspec.yaml @@ -1,7 +1,7 @@ name: pangolin_vpn description: 穿山甲 · Pangolin — 极简、稳定、跨平台网络加速客户端。 publish_to: "none" -version: 1.0.20+21 +version: 1.0.21+22 environment: sdk: ^3.5.0 diff --git a/client/windows/installer/pangolin.iss b/client/windows/installer/pangolin.iss index 354282c..9bff8b5 100644 --- a/client/windows/installer/pangolin.iss +++ b/client/windows/installer/pangolin.iss @@ -3,7 +3,7 @@ ; 前置: 先在 client/ 跑 `flutter build windows`(Release),产物在 ; ..\..\build\windows\x64\runner\Release #define MyAppName "穿山甲 Pangolin" -#define MyAppVersion "1.0.20" +#define MyAppVersion "1.0.21" #define MyAppPublisher "Pangolin" #define MyAppExeName "pangolin_vpn.exe" #define BuildDir "..\..\build\windows\x64\runner\Release"