1. 调整工具类、平台相关代码的目录组织,将原有根目录下的工具类迁移到`data/`和`platform/`子目录 2. 统一修复全项目的文件导入路径,匹配新的目录结构 3. 新增Web端平台适配的Stub实现,包括Isolate、path_provider、platform_io等 4. 删除旧的单文件平台适配实现,替换为分平台的目录结构实现 5. 移除旧的iOS Widget入口文件,新增Widget Extension的权限配置 6. 调整部分组件的目录位置,统一widget的分类组织 7. 修复部分硬编码文本和废弃的正则表达式逻辑
284 lines
8.2 KiB
Dart
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;
|
|
}
|