DriftLedger/docs/design/ui-redesign-p8-plan.md
fengmengqi bf04400852 feat: OCR 模型按需下载 + 三层独立开关 + 日志持久化 + 原生浮层主题同步 + 文档重构
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
2026-07-23 19:03:38 +08:00

14 KiB
Raw Blame History

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 OCR true、L3 AI Vision false(需用户配 API Key 后才可开启)
  • 模型下载:首次启用 L2 时检查模型是否存在,无则弹出下载提示
  • settings/ai.tsx 删除,原入口重定向到 automation 页
  • 去重/转账识别两个 setting 字段不动(它们不是「层」而是管道的独立步骤),但在 automation 页加开关
  • 模型从 assets 复制到 filesystem预构建时仍打 APK assets 以兼容旧用户,应用首次启动将模型文件从 assets 拷贝到 documentDirectoryOcrModule 从 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: true
  • layer2OcrEnabled: true
  • layer3AiEnabled: false
  • ocrModelVersion: ''
  • 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.tssrc/services/ocrBridge.tssrc/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.ktplugins/ppocr/app.plugin.jsplugins/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.ktcreateSession 调用的位置,改从传入路径加载而非 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 服务

NativeAccessibilityBridgecopyOcrModelsFromAssets(): 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.tsxsrc/i18n/zh.tssrc/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「自动记账层级」

  • 三行 SwitchL1 规则匹配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 部分)

  • dedupEnabled Switch

  • transferRecognitionEnabled Switch

  • 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:三选一 chipopenai/gemini/deepseek

  • aiApiKey输入框secureTextEntry

  • aiBaseUrl:输入框(默认值 placeholder

  • aiModel:输入框(默认值 placeholder

  • aiEnabledSwitch总开关控制 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: 手工走查清单
  1. settings 页点「智能记账」→ 跳转到 automation 页
  2. automation 页看到三个层级开关L1 默认开 / L2 默认开 / L3 默认关灰)
  3. 关闭 L1 → 仍可记账(走 L2 OCR
  4. 关闭 L2 → L1 失败不再调用 OCR
  5. 开启 L3 → 提示配 AI Key → 配置后可用
  6. OCR 模型未安装 → 点安装 → 从 assets 拷贝 → 显示绿色已安装
  7. L2 开关关闭再打开 → 已有模型直接可用,不再提示
  8. AI Provider 切换 openai/gemini/deepseek → 对应的 baseUrl 默认值变化

终审修订(已实施,以实际代码为准)

日期2026-07-22审计 598 测试全绿、typecheck 干净、Kotlin BUILD SUCCESSFUL。

# 级别 问题 处理
无 Critical / Important 发现

偏差说明

  1. 管道设置 Card 复用现有 settings.dedupLabel/settings.transferLabel i18n 键(已在 settings/ai.tsx 中使用),未新键 dedupSwitch/transferSwitch——避免重复键。
  2. OcrModule.kt 从 assets 加载路径改为 modelDir 字段 + setModelDir 方法——保留原 assets 加载作为兜底modelDir 为空时走原路径),保证向后兼容。
  3. ensureOcrModels()_layout.tsx ready 前调用一次(异步非阻塞),getOcrProcessor() 中也传 ocrModelDir 作为安全网。