14 KiB
14 KiB
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,实现:
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)
@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
<service
android:name=".ocr.OcrAccessibilityService"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
android:exported="false">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_service_config" />
</service>
1.5 创建无障碍服务配置
android/app/src/main/res/xml/accessibility_service_config.xml:
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/ocr_service_description"
android:accessibilityEventTypes="typeWindowStateChanged"
android:accessibilityFeedbackType="feedbackGeneric"
android:notificationTimeout="100"
android:canTakeScreenshot="true"
android:canRetrieveWindowContent="false" />
阶段二:OCR 引擎集成(2 天)
2.1 添加 ML Kit 依赖
android/app/build.gradle:
dependencies {
// Google ML Kit OCR
implementation 'com.google.mlkit:text-recognition-chinese:16.0.0'
}
2.2 实现 OcrManager.kt
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 正则实现:
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:
import { NativeModules, Platform } from 'react-native';
const { OcrModule } = NativeModules;
export interface OcrResult {
text: string;
imagePath: string;
}
export class OcrBridge {
static async isAvailable(): Promise<boolean> {
if (Platform.OS !== 'android') return false;
return await OcrModule?.isServiceEnabled() ?? false;
}
static async startService(): Promise<void> {
await OcrModule?.startOcrService();
}
static async takeScreenshot(): Promise<string | null> {
return await OcrModule?.takeScreenshot();
}
static async getTopApp(): Promise<string | null> {
return await OcrModule?.getTopApp();
}
}
3.2 OCR 识别流程
src/ocr/OcrProcessor.ts:
import { OcrBridge } from './OcrBridge';
import { importStatement, type ImportedEvent } from '../domain';
export async function processOcrScreenshot(): Promise<ImportedEvent | null> {
// 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 入口:
const handleOcrImport = async () => {
const event = await processOcrScreenshot();
if (event) {
setEvents(current => [...current, event]);
setMessage('OCR 识别成功,请确认账单。');
} else {
setMessage('OCR 识别失败,请重试。');
}
};
阶段四:UI 适配(2 天)
4.1 添加 OCR 触发按钮
在导入标签页添加"屏幕识别"按钮:
{tab === '导入' && (
<>
<Card title="自动记账">
<Button label="屏幕识别(OCR)" onPress={handleOcrImport} />
<Button label="导入 CSV 文件" onPress={loadDemo} />
</Card>
{/* 现有的事件列表 */}
</>
)}
4.2 OCR 结果确认界面
展示识别结果,允许用户修正:
<Card title={`OCR 识别 · ${event.amount} ${event.currency}`}>
<Text>{event.counterparty} · {event.memo}</Text>
<Text style={styles.muted}>来源:{event.channel} · {event.occurredAt}</Text>
{/* 编辑按钮 */}
<Button label="确认入账" onPress={() => confirm(event)} />
</Card>
4.3 无障碍权限引导
首次使用时引导用户开启无障碍权限:
const enableOcr = async () => {
const available = await OcrBridge.isAvailable();
if (!available) {
// 打开系统无障碍设置
await OcrBridge.startService();
}
};
阶段五:测试与优化(2 天)
5.1 测试用例
| 测试场景 | 预期结果 |
|---|---|
| 支付宝付款成功页 OCR | 正确识别金额、商户、时间 |
| 微信支付凭证页 OCR | 正确识别金额、商户 |
| 银行卡扣款通知页 OCR | 正确识别金额、来源 |
| 识别失败(非支付页面) | 返回 null,提示重试 |
| 重复识别同一笔交易 | 去重,提示已存在 |
5.2 性能优化
- 截屏后立即回收 Bitmap,避免内存泄漏
- OCR 识别在后台线程执行
- 缓存最近识别结果,避免重复处理
5.3 兼容性处理
- Android 11 以下不支持
takeScreenshot(),降级为提示用户手动截图 - 不同支付 App 的页面布局差异,通过正则适配
文件清单
新增文件
android/app/src/main/java/com/beancount/mobile/ocr/
├── OcrModule.kt # React Native 原生模块
├── OcrAccessibilityService.kt # 无障碍服务
└── OcrManager.kt # OCR 处理管理器
android/app/src/main/res/xml/
└── accessibility_service_config.xml # 无障碍服务配置
src/ocr/
├── OcrBridge.ts # JS 层 Bridge
└── OcrProcessor.ts # OCR 处理逻辑
修改文件
android/app/build.gradle # 添加 ML Kit 依赖
android/app/src/main/AndroidManifest.xml # 注册无障碍服务
App.tsx # 添加 OCR 入口
package.json # 无变化(纯原生模块)
依赖清单
| 依赖 | 版本 | 用途 | 必需 |
|---|---|---|---|
| Google ML Kit Text Recognition | 16.0.0 | 中文 OCR | 是 |
| React Native | 0.81.0 | 框架 | 是 |
| Expo | ~54.0.0 | 开发框架 | 是 |
不需要的依赖:
- Shizuku SDK
- Xposed/LSPatch
- PP-OCR AAR
- QuickJS
风险与限制
| 风险 | 影响 | 缓解措施 |
|---|---|---|
| Android < 11 不支持截屏 API | 低版本设备无法使用 | 降级为手动截图导入 |
| 支付 App 页面更新 | OCR 正则失效 | 正则设计为宽松匹配 |
| 无障碍权限被系统回收 | 服务停止 | 前台通知保活 |
| OCR 识别准确率 | 可能识别错误 | 用户确认环节兜底 |
预估工期
| 阶段 | 工作量 | 产出 |
|---|---|---|
| 阶段一:原生模块 | 3 天 | 无障碍服务 + 截屏 |
| 阶段二:OCR 引擎 | 2 天 | ML Kit 集成 |
| 阶段三:JS 集成 | 2 天 | Bridge + 解析 |
| 阶段四:UI 适配 | 2 天 | 按钮 + 确认界面 |
| 阶段五:测试优化 | 2 天 | 测试用例 + 兼容性 |
| 总计 | 11 天 |
与 plan.md 的关系
本方案是 plan.md 的扩展,补充了 OCR 自动记账能力。plan.md 中明确"首版不做 OCR",本方案作为第二阶段实现。
两份文档的关系:
plan.md:核心架构 + CSV 导入 + 手工记账(首版)plan-ocr.md:OCR 屏幕识别 + 自动记账(第二阶段)
实现顺序:先完成 plan.md 的核心功能,再实现 plan-ocr.md 的 OCR 能力。