Files
xianyan/lib/core/services/catcher2_config_service.dart
Developer a4a7e10722 feat: 完成2026-06-09版本迭代更新
此版本包含:
1. 新增位置消息发送与展示功能
2. 完善多语言本地化文案
3. 新增安卓端管理空间Activity与图标背景
4. 优化摇一摇开关逻辑与深度链接配置
5. 新增信息流平台过滤与A/B测试后台功能
6. 更新签名配置与构建脚本
7. 修复若干已知问题与代码优化
2026-06-09 23:18:13 +08:00

298 lines
8.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// ============================================================
/// 闲言APP — Catcher2 配置服务
/// 创建时间: 2026-05-21
/// 更新时间: 2026-06-06
/// 作用: 统一管理 Catcher2 异常捕获开关与动态配置更新
/// 上次更新: 错误弹窗中文化改英文、复制显示字数上限提升至2000
/// ============================================================
import 'package:catcher_2/catcher_2.dart';
import 'package:catcher_2/model/platform_type.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' show SelectableText;
import 'package:flutter/services.dart';
import '../router/app_router.dart' show rootNavigatorKey;
import '../storage/kv_storage.dart';
import '../utils/logger.dart';
import 'crash_log_service.dart';
class Catcher2ConfigService {
Catcher2ConfigService._();
static final Catcher2ConfigService instance = Catcher2ConfigService._();
Catcher2? _catcher2;
bool _initialized = false;
bool get isInitialized => _initialized;
bool get isEnabled {
return KvStorage.getBool('general_catcher_enabled') ?? true;
}
/// 初始化 Catcher2使用 rootWidget 而非 runAppFunction避免 Zone mismatch
void init({required Widget rootWidget}) {
final enabled = isEnabled;
final debugConfig = enabled
? Catcher2Options(
CopyableDialogReportMode(),
[_ConsoleLogHandler()],
localizationOptions: [
LocalizationOptions.buildDefaultChineseOptions(),
],
)
: Catcher2Options(SilentReportMode(), []);
final releaseConfig = enabled
? Catcher2Options(SilentReportMode(), [_ConsoleLogHandler()])
: Catcher2Options(SilentReportMode(), []);
// 使用 rootWidget 而非 runAppFunctionCatcher2 会在内部调用 runApp
// 但不会创建新的 Zone避免 Zone mismatch 警告
_catcher2 = Catcher2(
rootWidget: rootWidget,
debugConfig: debugConfig,
releaseConfig: releaseConfig,
profileConfig: Catcher2Options(SilentReportMode(), []),
navigatorKey: rootNavigatorKey,
);
_initialized = true;
Log.i(
'Catcher2ConfigService: 初始化完成 (enabled=$enabled, mode=${kDebugMode ? "debug" : "release"})',
);
}
void updateFromSettings(bool enabled) {
if (_catcher2 == null) {
Log.w('Catcher2ConfigService: Catcher2 未初始化,跳过更新');
return;
}
final debugConfig = enabled
? Catcher2Options(
CopyableDialogReportMode(),
[_ConsoleLogHandler()],
localizationOptions: [
LocalizationOptions.buildDefaultChineseOptions(),
],
)
: Catcher2Options(SilentReportMode(), []);
final releaseConfig = enabled
? Catcher2Options(SilentReportMode(), [_ConsoleLogHandler()])
: Catcher2Options(SilentReportMode(), []);
_catcher2!.updateConfig(
debugConfig: debugConfig,
releaseConfig: releaseConfig,
);
Log.i('Catcher2ConfigService: 配置已更新 (enabled=$enabled)');
}
}
class CopyableDialogReportMode extends ReportMode {
@override
void requestAction(Report report, BuildContext? context) {
_showDialog(report, context);
}
Future<void> _showDialog(Report report, BuildContext? context) async {
// 布局溢出错误不弹窗
final errorStr = report.error.toString();
if (errorStr.contains('overflowed') || errorStr.contains('RenderFlex')) {
onActionConfirmed(report); // 自动确认,不弹窗
return;
}
await Future<void>.delayed(Duration.zero);
if (context != null && context.mounted) {
showCupertinoDialog<void>(
context: context,
builder: (ctx) => _CopyableErrorDialog(
report: report,
onAccept: () {
onActionConfirmed(report);
Navigator.of(ctx).pop();
},
onReject: () {
onActionRejected(report);
Navigator.of(ctx).pop();
},
),
);
}
}
@override
bool isContextRequired() => true;
@override
List<PlatformType> getSupportedPlatforms() => PlatformType.values.toList();
}
class _CopyableErrorDialog extends StatelessWidget {
const _CopyableErrorDialog({
required this.report,
required this.onAccept,
required this.onReject,
});
final Report report;
final VoidCallback onAccept;
final VoidCallback onReject;
String get _errorId {
final ts = report.dateTime.millisecondsSinceEpoch
.toRadixString(36)
.toUpperCase();
return 'ERR-$ts';
}
String get _errorText {
final buffer = StringBuffer();
buffer.writeln('[$_errorId] ${report.dateTime.toIso8601String()}');
buffer.writeln();
buffer.writeln('Error: ${report.error}');
buffer.writeln();
buffer.writeln('Stack Trace:');
buffer.writeln(report.stackTrace);
return buffer.toString();
}
void _copyToClipboard(BuildContext context) {
// 限制剪贴板内容不超过400字避免超长文本占用剪贴板
final text = _errorText;
final clipboardText = text.length > 400
? '${text.substring(0, 400)}...'
: text;
Clipboard.setData(ClipboardData(text: clipboardText));
HapticFeedback.lightImpact();
showCupertinoDialog<void>(
context: context,
barrierDismissible: true,
builder: (ctx) => CupertinoAlertDialog(
content: const Padding(
padding: EdgeInsets.symmetric(vertical: 12),
child: Text('已复制到剪贴板', style: TextStyle(fontSize: 14)),
),
actions: [
CupertinoDialogAction(
onPressed: () => Navigator.of(ctx).pop(),
child: const Text('OK'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final errorStr = report.error.toString();
final displayError = errorStr.length > 2000
? '${errorStr.substring(0, 2000)}...'
: errorStr;
return CupertinoAlertDialog(
title: Column(
children: [
const Text('App Error'),
const SizedBox(height: 4),
Text(
_errorId,
style: TextStyle(
fontSize: 11,
color: CupertinoColors.secondaryLabel.resolveFrom(context),
),
),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
Container(
constraints: const BoxConstraints(maxHeight: 200),
child: SingleChildScrollView(
child: SelectableText(
displayError,
style: TextStyle(
fontSize: 12,
color: CupertinoColors.systemRed.resolveFrom(context),
),
),
),
),
const SizedBox(height: 8),
Text(
'Time: ${report.dateTime.toIso8601String()}',
style: TextStyle(
fontSize: 11,
color: CupertinoColors.secondaryLabel.resolveFrom(context),
),
),
],
),
actions: [
CupertinoDialogAction(
onPressed: () => _copyToClipboard(context),
child: const Text('Copy Details'),
),
CupertinoDialogAction(
isDestructiveAction: true,
onPressed: onReject,
child: const Text('Dismiss'),
),
CupertinoDialogAction(
isDefaultAction: true,
onPressed: onAccept,
child: const Text('Confirm'),
),
],
);
}
}
class _ConsoleLogHandler extends ReportHandler {
DateTime? _lastLog;
static const _cooldown = Duration(seconds: 3);
@override
Future<bool> handle(Report report, BuildContext? context) async {
final now = DateTime.now();
final ts = report.dateTime.millisecondsSinceEpoch
.toRadixString(36)
.toUpperCase();
final errorId = 'ERR-$ts';
if (_lastLog == null || now.difference(_lastLog!) > _cooldown) {
_lastLog = now;
Log.e('[$errorId] ${report.error}');
if (kDebugMode) {
debugPrint('──────────────────────────────────────');
debugPrint('[$errorId] Catcher2 异常详情:');
debugPrint('Error: ${report.error}');
debugPrint('Stack: ${report.stackTrace}');
debugPrint('──────────────────────────────────────');
}
}
CrashLogService.instance.addLog(
error: report.error.toString(),
stackTrace: report.stackTrace.toString(),
);
return true;
}
@override
List<PlatformType> getSupportedPlatforms() => PlatformType.values.toList();
@override
bool isContextRequired() => false;
}