# OCR 自动记账集成方案 ## 摘要 借鉴 AutoAccounting 项目的 OCR 模式,为 beancount-mobile 增加屏幕识别自动记账能力。用户在支付宝/微信/银行 App 付款后,通过无障碍服务截屏 → OCR 识别 → 解析账单 → 生成 Beancount 复式分录 → 用户确认入账。 **核心原则**:不修改任何应用、不需要 Root、不需要 Shizuku,仅依赖 Android 无障碍权限。 ## 技术方案 ### 架构概览 ``` 用户在支付 App 完成付款 ↓ Android 无障碍服务检测到页面变化 ↓ AccessibilityService.takeScreenshot() 截屏(API 30+) ↓ Google ML Kit OCR 识别文字 ↓ 正则 + 规则引擎解析为 ImportedEvent ↓ 复用现有 classify() → TransactionDraft ↓ 用户确认 → commitMobileTransaction() → mobile.bean ``` ### 依赖 AutoAccounting 的部分 | AutoAccounting 组件 | 用途 | beancount-mobile 替代方案 | | --------------------------- | ------------------------- | ---------------------------------- | | `OcrTools.kt` | 无障碍截屏 + 前台应用检测 | 原生模块重写,逻辑一致 | | `OcrProcessor.kt` | PP-OCRv5 文字识别 | Google ML Kit(免费,无需 AAR) | | `JsExecutor.kt` | QuickJS 规则引擎 | 复用现有`importStatement` + 规则 | | `BillService.kt` | 账单分析流程 | 复用现有`classify()` 流程 | | `PageSignatureManager.kt` | 页面特征匹配 | 可选,首版不做 | | `FlipDetector.kt` | 翻转触发 | 改为悬浮按钮触发 | ### 不依赖的部分 - Shizuku SDK(无障碍模式不需要) - Xposed/LSPatch 框架 - Ktor 嵌入式服务器 - Room 数据库 - MMKV 配置存储 - TapBack 双击背部模块 ## 实现计划 ### 阶段一:原生模块搭建(3 天) #### 1.1 创建 Android 原生模块目录结构 ``` android/app/src/main/java/com/beancount/mobile/ ├── ocr/ │ ├── OcrModule.kt # React Native 原生模块 │ ├── OcrAccessibilityService.kt # 无障碍服务 │ └── OcrManager.kt # OCR 处理管理器 ``` #### 1.2 实现 OcrAccessibilityService.kt 参考 AutoAccounting 的 `OcrTools.kt`,实现: ```kotlin class OcrAccessibilityService : AccessibilityService() { // 1. 监听窗口变化事件 override fun onAccessibilityEvent(event: AccessibilityEvent?) { // 检测前台应用变化 // 触发截屏回调 } // 2. 截屏功能(API 30+) fun takeScreenshot(callback: (Bitmap?) -> Unit) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { takeScreenshot( Display.DEFAULT_DISPLAY, mainExecutor, object : TakeScreenshotCallback { override fun onSuccess(result: ScreenshotResult) { val bitmap = Bitmap.wrapHardwareBuffer( result.hardwareBuffer, result.colorSpace ) callback(bitmap) result.hardwareBuffer.close() } override fun onFailure(errorCode: Int) { callback(null) } } ) } } // 3. 获取前台应用包名 fun getTopPackage(): String? { // 通过 rootInActiveWindow 获取 } } ``` #### 1.3 实现 OcrModule.kt(React Native Bridge) ```kotlin @ReactModule(name = "OcrModule") class OcrModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { @ReactMethod fun startOcrService(promise: Promise) { // 启动无障碍服务 } @ReactMethod fun stopOcrService(promise: Promise) { // 停止无障碍服务 } @ReactMethod fun takeScreenshot(promise: Promise) { // 调用无障碍服务截屏 // 返回 base64 编码的图片 } @ReactMethod fun getTopApp(promise: Promise) { // 返回前台应用包名 } @ReactMethod fun isServiceEnabled(promise: Promise) { // 检查无障碍服务是否已启用 } } ``` #### 1.4 配置 AndroidManifest.xml ```xml ``` #### 1.5 创建无障碍服务配置 `android/app/src/main/res/xml/accessibility_service_config.xml`: ```xml ``` ### 阶段二:OCR 引擎集成(2 天) #### 2.1 添加 ML Kit 依赖 `android/app/build.gradle`: ```gradle dependencies { // Google ML Kit OCR implementation 'com.google.mlkit:text-recognition-chinese:16.0.0' } ``` #### 2.2 实现 OcrManager.kt ```kotlin class OcrManager(private val context: Context) { private val recognizer = TextRecognition.getClient( ChineseTextRecognizerOptions.Builder().build() ) suspend fun recognizeText(bitmap: Bitmap): String { val image = InputImage.fromBitmap(bitmap, 0) val result = recognizer.process(image).await() return result.text } } ``` #### 2.3 金额/商户正则解析 参考 AutoAccounting 的 JS 规则,用 Kotlin 正则实现: ```kotlin object BillParser { // 金额匹配:¥100.00 / 100.00元 / -50.50 private val amountPattern = Regex("""[¥¥]?\s*(-?\d+\.?\d*)\s*元?""") // 时间匹配:2026-07-10 14:30:00 / 07-10 14:30 private val timePattern = Regex("""(\d{4}[-/]\d{2}[-/]\d{2}\s+\d{2}:\d{2}(?::\d{2})?)""") // 商户匹配:支付成功至 XXX / 商户名称:XXX private val merchantPattern = Regex("""(?:商户|商家|收款方)[::]\s*(.+?)(?:\s|$)""") fun parse(ocrText: String, appPackage: String): ImportedEvent? { val amount = amountPattern.find(ocrText)?.groupValues?.get(1) ?: return null val time = timePattern.find(ocrText)?.groupValues?.get(1) ?: return null val merchant = merchantPattern.find(ocrText)?.groupValues?.get(1) ?: "未知商户" // 根据 appPackage 判断渠道 val channel = when { appPackage.contains("alipay") -> "Alipay" appPackage.contains("wechat") -> "WeChat" else -> "Bank" } return ImportedEvent( id = "ocr-${System.currentTimeMillis()}", occurredAt = time.replace("/", "-").take(10), amount = amount, currency = "CNY", direction = if (amount.startsWith("-")) "expense" else "income", channel = channel, counterparty = merchant, memo = "OCR 自动识别", raw = mapOf("ocrText" to ocrText) ) } } ``` ### 阶段三:JS 层集成(2 天) #### 3.1 创建 OCR Bridge 模块 `src/ocr/OcrBridge.ts`: ```typescript import { NativeModules, Platform } from 'react-native'; const { OcrModule } = NativeModules; export interface OcrResult { text: string; imagePath: string; } export class OcrBridge { static async isAvailable(): Promise { if (Platform.OS !== 'android') return false; return await OcrModule?.isServiceEnabled() ?? false; } static async startService(): Promise { await OcrModule?.startOcrService(); } static async takeScreenshot(): Promise { return await OcrModule?.takeScreenshot(); } static async getTopApp(): Promise { return await OcrModule?.getTopApp(); } } ``` #### 3.2 OCR 识别流程 `src/ocr/OcrProcessor.ts`: ```typescript import { OcrBridge } from './OcrBridge'; import { importStatement, type ImportedEvent } from '../domain'; export async function processOcrScreenshot(): Promise { // 1. 截屏 const base64Image = await OcrBridge.takeScreenshot(); if (!base64Image) return null; // 2. 获取前台应用 const topApp = await OcrBridge.getTopApp(); if (!topApp) return null; // 3. OCR 识别(通过原生模块) const ocrText = await OcrModule.recognizeText(base64Image); if (!ocrText) return null; // 4. 解析为 ImportedEvent const event = parseOcrText(ocrText, topApp); return event; } ``` #### 3.3 集成到现有导入流程 修改 `App.tsx` 的导入标签页,添加 OCR 入口: ```typescript const handleOcrImport = async () => { const event = await processOcrScreenshot(); if (event) { setEvents(current => [...current, event]); setMessage('OCR 识别成功,请确认账单。'); } else { setMessage('OCR 识别失败,请重试。'); } }; ``` ### 阶段四:UI 适配(2 天) #### 4.1 添加 OCR 触发按钮 在导入标签页添加"屏幕识别"按钮: ```tsx {tab === '导入' && ( <>