fix: harden OriginOS recognition tracking

This commit is contained in:
2026-08-31 11:29:04 +08:00
parent 9ccbdfead4
commit 477f83c5b1
17 changed files with 980 additions and 108 deletions
@@ -49,6 +49,7 @@ class RecognitionDiagnosticDisplay {
'matched' || 'auto_ready' => '已识别',
'confirm' => '待确认',
'started' => '处理中',
'waiting' => '等待结果页',
'failed' => '失败',
_ => '未触发入账',
};
@@ -56,6 +57,7 @@ class RecognitionDiagnosticDisplay {
static String _summaryLabel(String result, String reason) {
if (reason == 'duplicate_result_surface') return '已合并';
if (result == 'waiting') return '等待结果页';
if (_captureFailureReasons.contains(reason)) return '截图失败';
if (_ocrNoResultReasons.contains(reason)) return 'OCR 无结果';
if (_ruleRejectedReasons.contains(reason) || result == 'rejected') {
@@ -65,6 +67,7 @@ class RecognitionDiagnosticDisplay {
'matched' || 'auto_ready' => '已识别',
'confirm' => '待确认',
'started' => '处理中',
'waiting' => '等待结果页',
'failed' => '失败',
_ => '没事件',
};
@@ -75,8 +78,9 @@ class RecognitionDiagnosticDisplay {
'history_page' => '当前是账单或交易历史页',
'blocked_status' => '当前状态为失败、处理中或已取消',
'no_text' => '截图中没有识别到文字',
'no_success_status' => '没有找到明确或弱完成状态,可开启诊断预览查看脱敏结果',
'payment_input_page' => '当前仍是付款输入或确认页面,已拒绝入账',
'no_success_status' => '暂未找到完成状态,正在按计划复核结果',
'payment_input_page' => '当前仍是付款输入或确认页面,等待结果页,不会提前入账',
'result_page_timeout' => '90 秒内未等到明确结果页,本次流程已停止追踪',
'direction_unknown' => '识别到完成状态,但无法确认收支方向',
'missing_amount' => '成功状态已识别,但没有找到金额',
'expected_amount_missing' => '结果页没有金额,且付款前金额不唯一或未捕获',
@@ -167,8 +171,7 @@ class RecognitionDiagnosticDisplay {
static const _ruleRejectedReasons = {
'history_page',
'blocked_status',
'no_success_status',
'payment_input_page',
'result_page_timeout',
'direction_unknown',
'missing_amount',
'expected_amount_missing',
@@ -95,6 +95,14 @@ class RecognitionStatus {
final DateTime? accessibilityLastConnectedAt;
final DateTime? accessibilityLastDisconnectedAt;
final DateTime? recognitionProcessStartedAt;
final bool keepAliveExpected;
final bool keepAliveRunning;
final String? keepAliveError;
final bool recentsProtectionExpected;
final bool recentsProtectionActive;
final DateTime? lastRecognitionExitAt;
final String? lastRecognitionExitReason;
final bool taskCleanerRecoveryNeeded;
final bool notificationAuthorized;
final bool notificationConnected;
final bool postNotificationsGranted;
@@ -116,6 +124,14 @@ class RecognitionStatus {
this.accessibilityLastConnectedAt,
this.accessibilityLastDisconnectedAt,
this.recognitionProcessStartedAt,
this.keepAliveExpected = false,
this.keepAliveRunning = false,
this.keepAliveError,
this.recentsProtectionExpected = false,
this.recentsProtectionActive = false,
this.lastRecognitionExitAt,
this.lastRecognitionExitReason,
this.taskCleanerRecoveryNeeded = false,
required this.notificationAuthorized,
required this.notificationConnected,
required this.postNotificationsGranted,
@@ -158,6 +174,17 @@ class RecognitionStatus {
'accessibilityLastDisconnectedAt',
),
recognitionProcessStartedAt: epochDate('recognitionProcessStartedAt'),
keepAliveExpected: value['keepAliveExpected'] as bool? ?? false,
keepAliveRunning: value['keepAliveRunning'] as bool? ?? false,
keepAliveError: value['keepAliveError']?.toString(),
recentsProtectionExpected:
value['recentsProtectionExpected'] as bool? ?? false,
recentsProtectionActive:
value['recentsProtectionActive'] as bool? ?? false,
lastRecognitionExitAt: epochDate('lastRecognitionExitAt'),
lastRecognitionExitReason: value['lastRecognitionExitReason']?.toString(),
taskCleanerRecoveryNeeded:
value['taskCleanerRecoveryNeeded'] as bool? ?? false,
notificationAuthorized: value['notificationAuthorized'] as bool? ?? false,
notificationConnected: value['notificationConnected'] as bool? ?? false,
postNotificationsGranted:
@@ -189,6 +216,8 @@ class RecognitionStatus {
!accessibilityConnected &&
accessibilityConnectionState == AccessibilityConnectionState.disconnected;
bool get keepAliveNeedsRecovery => keepAliveExpected && !keepAliveRunning;
String get accessibilityConnectionLabel =>
switch (accessibilityConnectionState) {
AccessibilityConnectionState.unauthorized => '未授权',
@@ -320,6 +349,7 @@ class SpeechEvent {
/// Android native capabilities for screenshots, AI progress and speech.
class ScreenshotChannel {
static const _channel = MethodChannel('com.miaoji/screenshot');
static Future<RecognitionStatus>? _activeAccessibilityConnectionWait;
static void Function(String path)? _screenshotReady;
static void Function(String error)? _screenshotError;
static void Function(SpeechEvent event)? _speechEvent;
@@ -472,13 +502,32 @@ class ScreenshotChannel {
static Future<RecognitionStatus> waitForAccessibilityConnection({
Duration timeout = const Duration(seconds: 5),
Duration interval = const Duration(milliseconds: 500),
}) {
final active = _activeAccessibilityConnectionWait;
if (active != null) return active;
final operation = _waitForAccessibilityConnection(
timeout: timeout,
interval: interval,
);
_activeAccessibilityConnectionWait = operation;
return operation.whenComplete(() {
if (identical(_activeAccessibilityConnectionWait, operation)) {
_activeAccessibilityConnectionWait = null;
}
});
}
static Future<RecognitionStatus> _waitForAccessibilityConnection({
required Duration timeout,
required Duration interval,
}) async {
var status = await recognitionStatus();
final recognitionEnabled =
status.accessibilityEvents || status.aiScreenshot;
if (!recognitionEnabled ||
!status.accessibilityAuthorized ||
status.accessibilityConnected) {
status.accessibilityConnected ||
status.taskCleanerRecoveryNeeded) {
return status;
}
final deadline = DateTime.now().add(timeout);
@@ -487,6 +536,7 @@ class ScreenshotChannel {
status = await recognitionStatus();
if (!status.accessibilityAuthorized ||
status.accessibilityConnected ||
status.taskCleanerRecoveryNeeded ||
!(status.accessibilityEvents || status.aiScreenshot)) {
return status;
}
@@ -494,6 +544,15 @@ class ScreenshotChannel {
return status;
}
static Future<bool> ensureRecognitionKeepAlive() async {
try {
return await _channel.invokeMethod<bool>('ensureRecognitionKeepAlive') ??
false;
} on MissingPluginException {
return false;
}
}
static Future<bool> clearRecognitionDiagnostic() async {
try {
return await _channel.invokeMethod<bool>('clearRecognitionDiagnostic') ??