OCR 模型按需下载(P8 瘦身): - 移除 plugins/ppocr/assets/ 内置模型(det 9.8MB + rec 21MB + dict),APK 减包 ~31MB - 新增 modelDownloader.ts:优先从 HuggingFace/CDN 下载,兜底从 APK assets 拷贝 - OcrModule.kt 新增 setModelDir,支持从 filesystem 加载模型,回退 assets 兼容旧用户 - settingsStore 持久化 ocrModelVersion / ocrModelDir - 自动化页集成模型状态检查与一键下载 UI OCR 三层独立控制: - OcrProcessorConfig 从单一 aiVisionEnabled 拆分为 layer1/2/3 三个独立开关 - 设置页可按层启停(L1 正则规则 / L2 本地 OCR / L3 AI Vision) - OCR 处理增加耗时与字符数日志 日志系统升级: - logger.ts 新增 LogFileBackend 抽象,支持磁盘持久化(按日期 app-YYYY-MM-DD.log) - 新增 logBackend.ts(ExpoLogFileBackend)+ 日志中心页 settings/logs.tsx - 日志中心:实时缓冲 + 历史文件、4 级过滤、Tag/关键词搜索、JSON 展开、分享导出、7 天过期清理 - _layout.tsx 启动时初始化文件后端 + 日志脱敏(验证码/卡号) 原生浮层 UI 主题同步(P6): - 新增 floatingUiConfig.ts:JS 侧从 theme tokens + i18n 构建 FloatingUiConfig 推送原生 - 新增 FloatingUiConfigStore.kt:SharedPreferences 存储,三浮层组件读取 - FloatingBillView 重设计:颜色/文案走配置、新增币种 chip、金额校验改 BigDecimal - FloatingHelper / FloatingTip 同步适配 - _layout.tsx 新增 FloatingUiConfigSyncer,主题/语言切换自动推送 UI 与组件增强: - FormModal 新增 select/dropdown 控件、行内布局(row/flex)、联动回调 onValuesChange - 新增 Touchable 通用触摸组件、AccountCreateModal 快速建账弹窗 - 信用卡页展示账单周期/到期还款日/本期应还/剩余可用额度,关联账户改下拉选择 - AI 设置页重做:OpenAI/Gemini/DeepSeek 预设 + 默认 URL/模型 - 引导页新增 Android 权限检查步骤(无障碍/通知/短信/存储/悬浮窗) 去重优化: - 对手方匹配改为模糊包含(includes),双方均无对手方时判定低置信度重复 - DedupResult 新增 matchedItem 返回匹配对比项 文档重构: - README.md 重写为入口索引(品牌更新 + 模块概览 + 文档导航表) - 新增 AGENTS.md(AI 助手贡献指南)、docs/architecture.md(Mermaid 数据流/分层/OCR 级联图) - 新增 docs/development.md(环境/命令/编码规范/测试/提交规范)、plugins/README.md - UI 重设计文档(design spec + p1-p8)移入 docs/design/ 其他: - i18n 新增权限/信用卡详情/日志中心/AI 设置等翻译键 - ppocr Config Plugin 修复 import 注入去重;size-optimization 增强 - 新增测试:logger.test.ts、floating-ui-config.test.ts
14 KiB
P8:自动记账可配置化 + 模型按需下载 Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax. 不执行任何 git 操作。不创建额外任务清单。
Goal: automation 页统一管控所有自动记账能力(规则/OCR/AI Vision 三层独立开关 + 模型下载管理 + AI 配置整合),settings/ai.tsx 删除并重定向;OCR 模型从 APK assets 改为应用内下载(filesystem 路径加载 ONNX)。
背景决策:
- 三层开关默认值:L1 规则
true、L2 OCRtrue、L3 AI Visionfalse(需用户配 API Key 后才可开启) - 模型下载:首次启用 L2 时检查模型是否存在,无则弹出下载提示
settings/ai.tsx删除,原入口重定向到 automation 页- 去重/转账识别两个 setting 字段不动(它们不是「层」而是管道的独立步骤),但在 automation 页加开关
- 模型从 assets 复制到 filesystem:预构建时仍打 APK assets 以兼容旧用户,应用首次启动将模型文件从 assets 拷贝到 documentDirectory,OcrModule 从 filesystem 加载。这样之后卸载 assets 打包就变成了纯删除(未来版本切到纯下载只需移除 assets + app.plugin.js 禁拷贝)。
Task 1: settingsStore 新增字段 + 迁移
Files: src/store/settingsStore.ts
- Step 1: 接口 + DEFAULTS + 持久化 + setter
在 SettingsState 接口加 5 个新字段:
/** 自动记账 L1:正则规则匹配 */
layer1RuleEnabled: boolean;
/** 自动记账 L2:本地 OCR 识别 */
layer2OcrEnabled: boolean;
/** 自动记账 L3:云端 AI Vision */
layer3AiEnabled: boolean;
/** OCR 模型版本(已下载),空字符串表示未安装 */
ocrModelVersion: string;
/** OCR 模型下载路径 */
ocrModelDir: string;
DEFAULTS:
layer1RuleEnabled: truelayer2OcrEnabled: truelayer3AiEnabled: falseocrModelVersion: ''ocrModelDir: ''
PersistableSettings 加对应 5 字段;toPersistable 加 5 行;新增 5 个 setter 函数。现有 aiEnabled 保留作为 LLM 的总开关语义(layer3AiEnabled 是在 automation 页独立控制,「启用」需同时满足 aiEnabled && aiApiKey)。
注意:isNotificationListenerEnabled 这类 bridge 方法在 setFloatingBallEnabled 附近——新字段与它们无关,不加 bridge 方法。
- Step 2: 验证
npm run typecheck → 0 错误;npm test → 全绿
Task 2: OcrProcessor 逐级回退逻辑
Files: src/domain/ocrProcessor.ts、src/services/ocrBridge.ts、src/services/automationPipeline.ts
- Step 1: OcrProcessorConfig 扩展
OcrProcessorConfig 接口加:
layer1Enabled: boolean; // 默认 true
layer2Enabled: boolean; // 默认 true
layer3Enabled: boolean; // 默认 false
- Step 2: doProcess() 逐级跳过
在 OcrProcessor.doProcess() 中修改流程(约 :103-162):
// 1. 横屏 / 图片去重 守卫不变
// 2. L1+L2 需要 OCR 文本
if (config.layer1Enabled || config.layer2Enabled) {
if (!ocrEngine) 返回 none(引擎不可用)
ocrText = await ocrEngine.recognizeText(imageBase64)
if (!ocrText) → 如果 L3 启用则 goto L3,否则返回 none
} else {
ocrText = ''
}
// 3. Layer 1(仅当启用)
if (config.layer1Enabled && ocrText && !isDetailPage) {
result = matchOcrRule(ocrText, packageName)
if (result) return { layer: 'layer1-rule', ... }
}
// 4. Layer 2(仅当启用)
if (config.layer2Enabled && ocrText) {
result = parseOcrBill(ocrText, packageName)
if (result) return { layer: 'layer2-ocr', ... }
}
// 5. Layer 3(仅当启用)
if (config.layer3Enabled) {
return tryLayer3(imageBase64, ocrText)
}
// 6. 全部未命中或全部关闭
return { event: null, layer: 'none', skipped: 'non-bill' }
- Step 3: automationPipeline.ts 的 getOcrProcessor 改读新字段
getOcrProcessor() 约 :116-147 行:从 settingsStore 读取 layer1RuleEnabled / layer2OcrEnabled / layer3AiEnabled 传入 OcrProcessorConfig。
- Step 4: 验证
npm run typecheck → 0 错误;npm test → 全绿
Task 3: 模型文件从 assets 拷贝到 filesystem + OcrModule 改路径
Files: plugins/ppocr/android/OcrModule.kt、plugins/ppocr/app.plugin.js、plugins/accessibility/android/AccessibilityBridgeModule.kt(新 bridge 方法)
- Step 1: AccessibilityBridgeModule 加 copyModelFromAssets 方法
@ReactMethod
fun copyOcrModelsFromAssets(promise: Promise) {
try {
val assetManager = reactContext.assets
val destDir = java.io.File(reactContext.filesDir, "ocr_models_v6")
if (!destDir.exists()) destDir.mkdirs()
val files = listOf("ppocrv6_det.onnx", "ppocrv6_rec.onnx", "ppocrv6_dict.txt")
for (filename in files) {
val destFile = java.io.File(destDir, filename)
if (destFile.exists()) continue // skip existing
val input = assetManager.open(filename)
val output = java.io.FileOutputStream(destFile)
val buffer = ByteArray(8192)
var bytesRead: Int
while (input.read(buffer).also { bytesRead = it } != -1) {
output.write(buffer, 0, bytesRead)
}
input.close()
output.close()
}
promise.resolve(destDir.absolutePath)
} catch (e: Exception) {
promise.reject("COPY_MODEL_FAIL", e.message)
}
}
- Step 2: OcrModule.kt 改为 filesystem 路径加载
读 OcrModule.kt 中 createSession 调用的位置,改从传入路径加载而非 asset 文件名。当前构造可能接受 asset 路径——改为接收 filesystem 绝对路径。新增一个 @ReactMethod setModelDir(dir: String) 存储路径,或直接在 initialize(assetManager: ...) 的参数中改为 initialize(modelDir: String, ...)。
注意:ONNX Runtime Android 的 OrtEnvironment.createSession(filePath) 接受文件系统路径(String),不需要 asset 特殊处理——只需确认 createSession 的参数类支持文件路径,否则改用 OrtEnvironment.createSession(byte[] modelBytes) 从内存加载。最终方案:保持 OrtEnvironment.createSession(filePath) ,将 modelDir + "/ppocrv6_det.onnx" 作为绝对路径传入。
- Step 3: JS 侧 bridge 接口 + modelDownloader 服务
NativeAccessibilityBridge 加 copyOcrModelsFromAssets(): Promise<string> → 返回模型目录绝对路径。
新建 src/services/modelDownloader.ts:
export async function ensureOcrModels(): Promise<string> {
// 1. 如果 settingsStore.ocrModelDir 有效且文件存在 → 直接返回
// 2. 尝试从 assets 拷贝到 documentDirectory/ocr_models_v6
// 3. 返回 modelsDir 绝对路径,存储到 settingsStore.ocrModelDir + ocrModelVersion
}
ocrModelVersion 在从 assets 拷贝成功后写 'v6-assets'(区分下载版本和打包版本)。
- Step 4: automationPipeline.ts 的 OcrProcessor 从 filesystem 初始化
getNativeOcrBridge()或 getOcrProcessor() 中,在初始化 OCR 引擎前调用 ensureOcrModels() 获取模型路径,传入 NativeOcrBridge 构造(或 OcrModule initialize)。
- Step 5: 验证
cd android && ./gradlew :app:compileDebugKotlin → BUILD SUCCESSFUL
Task 4: automation 页 UI —— 三层开关 + 模型管理 + AI 配置整合
Files: src/app/automation/index.tsx、src/i18n/zh.ts、src/i18n/en.ts
- Step 1: i18n 新键(zh/en 对等)
在 automation section 加:
autoBookkeeping 自动记账层级 / Auto Bookkeeping Layers
layer1Rule 规则匹配 / Rule Matching
layer1RuleDesc 基于正则规则的快速账单识别 / Fast regex-based bill recognition
layer2Ocr OCR 识别 / OCR Recognition
layer2OcrDesc 本地 PP-OCRv6 模型识别 / On-device PP-OCRv6 model
layer3Ai AI 视觉识别 / AI Vision
layer3AiDesc 云端多模态大模型识别 / Cloud multimodal LLM
layer3AiDisabled 请先配置 AI Key / Configure AI Key first
ocrModelTitle OCR 模型 / OCR Model
ocrModelNotInstalled 未安装 / Not Installed
ocrModelInstalled 已安装 ({version}) / Installed ({version})
ocrModelInstall 安装模型 / Install Model
ocrModelInstalling 安装中... / Installing...
ocrModelCopying 正在复制模型文件... / Copying model files...
dedupSwitch 多通道联合去重 / Cross-channel Dedup
transferSwitch 转账智能识别 / Transfer Recognition
- Step 2: automation 页 UI 重组织
在现有截图监控按钮 Card 之后加以下新 Card 区块:
Card A:「自动记账层级」
- 三行 Switch:L1 规则匹配(always enabled,不依赖外部条件)
- L2 OCR 识别(Switch;开启时检查模型→无模型则弹下载提示 Dialog)
- L3 AI 视觉(Switch;如果 aiEnabled=false 或 aiApiKey 为空则 disabled+提示文字;开启后展开 Provider/Key/Model 输入)
Card B:「OCR 模型」
- 状态行:版本号(未安装显示灰色「未安装」;已安装显示绿色「已安装 (v6-assets)」)
- 操作按钮:安装/重新安装(从 assets 拷贝 → 进度状态 → 完成)
Card C:「管道设置」(复用现有 Card B 和 C 部分)
-
dedupEnabledSwitch -
transferRecognitionEnabledSwitch -
Step 3: 操作逻辑
// 模型安装
const handleInstallModel = async () => {
setModelStatus('installing');
try {
const bridge = getAccessibilityBridge();
if (!bridge) throw new Error('Bridge unavailable');
const modelDir = await bridge.copyOcrModelsFromAssets();
setOcrModelVersion('v6-assets');
setOcrModelDir(modelDir);
setModelStatus('done');
} catch (e) {
setModelStatus('error');
Alert.alert('安装失败', String(e));
}
};
// L2 开关变化时检查模型
const handleL2Toggle = (val: boolean) => {
setLayer2OcrEnabled(val);
if (val && !ocrModelVersion) {
Alert.alert(t('automation.ocrModelTitle'), t('automation.ocrModelNotInstalled'), [
{ text: t('common.cancel'), onPress: () => setLayer2OcrEnabled(false) },
{ text: t('automation.ocrModelInstall'), onPress: handleInstallModel },
]);
}
};
// L3 开关变化时检查 AI Key
const handleL3Toggle = (val: boolean) => {
if (val && (!aiEnabled || !aiApiKey)) {
Alert.alert(t('automation.layer3Ai'), t('automation.layer3AiDisabled'));
return;
}
setLayer3AiEnabled(val);
};
- Step 4: AI 配置内联
L3 启用时展开的子区域:
-
aiProviderId:三选一 chip(openai/gemini/deepseek) -
aiApiKey:输入框(secureTextEntry) -
aiBaseUrl:输入框(默认值 placeholder) -
aiModel:输入框(默认值 placeholder) -
aiEnabled:Switch(总开关,控制 L3 的实际可用性) -
Step 5: 验证
npm run typecheck → 0 错误;npm test → 全绿
Task 5: AI 设置页清理 + settings 导航更新
Files: src/app/settings/ai.tsx(重写为跳转)、src/app/(tabs)/settings.tsx(更新入口文案)
- Step 1: settings/ai.tsx 改为 redirect
不删除文件(保留路由),但内容改为:useEffect(() => { router.replace('/automation'); }, []) 跳转到 automation 页,期间显示 loading。
- Step 2: settings.tsx 入口文案
src/app/(tabs)/settings.tsx 中「智能记账 & AI」入口的文案保持不变,但实际点击跳转已由 expo-router 自动处理(ai.tsx → redirect → automation),不需要改路由注册。
- Step 3: 验证
npm run typecheck → 0 错误;npm test → 全绿
grep -rn "settings/ai" src/app/(tabs)/settings.tsx → 确认无需改动即可跳转
Task 6: 编译 + 测试 + 终审
- Step 1: Kotlin 编译
cd android && ./gradlew :app:compileDebugKotlin → BUILD SUCCESSFUL
- Step 2: 全量测试
npm test → 全绿;npm run typecheck → 0 错误
- Step 3: 审计
grep -rn "#[0-9A-Fa-f]\{6\}" src/ --include="*.ts" --include="*.tsx" | grep -v "theme/presets.ts\|theme/palette.ts" # 无输出
grep -rn "fontFamily" src/app/automation/index.tsx # 无输出
grep -rn "📊|💰|💸|📝" src/app/automation/ # 无输出
- Step 4: 手工走查清单
- settings 页点「智能记账」→ 跳转到 automation 页
- automation 页看到三个层级开关(L1 默认开 / L2 默认开 / L3 默认关灰)
- 关闭 L1 → 仍可记账(走 L2 OCR)
- 关闭 L2 → L1 失败不再调用 OCR
- 开启 L3 → 提示配 AI Key → 配置后可用
- OCR 模型未安装 → 点安装 → 从 assets 拷贝 → 显示绿色已安装
- L2 开关关闭再打开 → 已有模型直接可用,不再提示
- AI Provider 切换 openai/gemini/deepseek → 对应的 baseUrl 默认值变化
终审修订(已实施,以实际代码为准)
日期:2026-07-22,审计 598 测试全绿、typecheck 干净、Kotlin BUILD SUCCESSFUL。
| # | 级别 | 问题 | 处理 |
|---|---|---|---|
| — | — | 无 Critical / Important 发现 | — |
偏差说明:
- 管道设置 Card 复用现有
settings.dedupLabel/settings.transferLabeli18n 键(已在 settings/ai.tsx 中使用),未新键dedupSwitch/transferSwitch——避免重复键。 - OcrModule.kt 从 assets 加载路径改为
modelDir字段 +setModelDir方法——保留原 assets 加载作为兜底(modelDir 为空时走原路径),保证向后兼容。 ensureOcrModels()在_layout.tsxready 前调用一次(异步非阻塞),getOcrProcessor()中也传ocrModelDir作为安全网。