Files
xianyan/lib/core/services/catcher2_config_service.dart
Developer a9499d7219 refactor: 重构项目目录结构与路径引用
1.  调整工具类、平台相关代码的目录组织,将原有根目录下的工具类迁移到`data/`和`platform/`子目录
2.  统一修复全项目的文件导入路径,匹配新的目录结构
3.  新增Web端平台适配的Stub实现,包括Isolate、path_provider、platform_io等
4.  删除旧的单文件平台适配实现,替换为分平台的目录结构实现
5.  移除旧的iOS Widget入口文件,新增Widget Extension的权限配置
6.  调整部分组件的目录位置,统一widget的分类组织
7.  修复部分硬编码文本和废弃的正则表达式逻辑
2026-05-23 05:16:31 +08:00

284 lines
8.2 KiB
Dart

/// ============================================================
/// 闲言APP — Catcher2 配置服务
/// 创建时间: 2026-05-21
/// 更新时间: 2026-05-21
/// 作用: 统一管理 Catcher2 异常捕获开关与动态配置更新
/// 上次更新: 自定义 CopyableDialogReportMode 支持复制/标识/控制台输出
/// ============================================================
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 '../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;
}
void init({required void Function() runAppFunction}) {
final enabled = isEnabled;
final debugConfig = enabled
? Catcher2Options(
CopyableDialogReportMode(),
[_ConsoleLogHandler()],
localizationOptions: [
LocalizationOptions.buildDefaultChineseOptions(),
],
)
: Catcher2Options(SilentReportMode(), []);
final releaseConfig = enabled
? Catcher2Options(SilentReportMode(), [_ConsoleLogHandler()])
: Catcher2Options(SilentReportMode(), []);
_catcher2 = Catcher2(
runAppFunction: runAppFunction,
debugConfig: debugConfig,
releaseConfig: releaseConfig,
profileConfig: Catcher2Options(SilentReportMode(), []),
);
_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 {
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) {
Clipboard.setData(ClipboardData(text: _errorText));
HapticFeedback.lightImpact();
showCupertinoDialog<void>(
context: context,
barrierDismissible: true,
builder: (ctx) => CupertinoAlertDialog(
content: Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Text(
'✅ 已复制到剪贴板\n标识: $_errorId',
style: const TextStyle(fontSize: 14),
),
),
actions: [
CupertinoDialogAction(
onPressed: () => Navigator.of(ctx).pop(),
child: const Text('好的'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final errorStr = report.error.toString();
final displayError = errorStr.length > 300
? '${errorStr.substring(0, 300)}...'
: errorStr;
return CupertinoAlertDialog(
title: Column(
children: [
const Text('⚠️ 应用异常'),
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(
'时间: ${report.dateTime.toIso8601String()}',
style: TextStyle(
fontSize: 11,
color: CupertinoColors.secondaryLabel.resolveFrom(context),
),
),
],
),
actions: [
CupertinoDialogAction(
onPressed: () => _copyToClipboard(context),
child: const Text('📋 复制详情'),
),
CupertinoDialogAction(
isDestructiveAction: true,
onPressed: onReject,
child: const Text('忽略'),
),
CupertinoDialogAction(
isDefaultAction: true,
onPressed: onAccept,
child: const Text('确认'),
),
],
);
}
}
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;
}