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
134 lines
4.7 KiB
TypeScript
134 lines
4.7 KiB
TypeScript
/**
|
||
* 自动记账状态管理(plan.md「3.x JS 层分层处理」+「决策 6 跨进程通信」)。
|
||
*
|
||
* 统一所有自动化入口(OCR/通知/短信/截图)的检测事件,
|
||
* 全部汇入 BillPipeline(转账识别→去重→分类→入库)。
|
||
*/
|
||
|
||
import { create } from 'zustand';
|
||
import type { ImportedEvent, LedgerIndex, Transaction } from '../domain/types';
|
||
import type { Category } from '../domain/categories';
|
||
import type { Rule } from '../domain/types';
|
||
import type { DedupConfig } from '../domain/dedup';
|
||
import type { TransferConfig } from '../domain/transferRecognizer';
|
||
import { sharedPipeline } from '../domain/pipelineSingleton';
|
||
import type { ProcessedDraft } from '../domain/billPipeline';
|
||
|
||
import { logger } from '../utils/logger';
|
||
|
||
export type AutomationSource = 'ocr' | 'notification' | 'sms' | 'screenshot' | 'manual';
|
||
|
||
export interface AutomationEvent {
|
||
id: string;
|
||
source: AutomationSource;
|
||
event: ImportedEvent;
|
||
detectedAt: string;
|
||
}
|
||
|
||
export interface AutomationState {
|
||
/** 待处理的检测事件(未入 pipeline 前)。 */
|
||
detected: AutomationEvent[];
|
||
/** pipeline 处理后的草稿。 */
|
||
drafts: ProcessedDraft[];
|
||
/** 是否正在处理。 */
|
||
isProcessing: boolean;
|
||
/** 处理出错时的错误信息。 */
|
||
error: string | null;
|
||
/** 各入口的统计。 */
|
||
stats: Record<AutomationSource, number>;
|
||
|
||
// actions
|
||
/** 添加检测事件(来自 OCR/通知/短信/截图)。 */
|
||
addDetected: (source: AutomationSource, event: ImportedEvent) => void;
|
||
/** 批量处理:把 detected 走 BillPipeline。 */
|
||
processAll: (
|
||
ledger: LedgerIndex,
|
||
rules: Rule[],
|
||
categories: Category[],
|
||
history: Transaction[],
|
||
dedupConfig?: DedupConfig,
|
||
transferConfig?: TransferConfig,
|
||
) => Promise<void>;
|
||
/** 确认单笔草稿。 */
|
||
confirmDraft: (index: number) => ProcessedDraft | null;
|
||
/** 拒绝单笔草稿。 */
|
||
rejectDraft: (index: number) => void;
|
||
/** 清空。 */
|
||
clear: () => void;
|
||
}
|
||
|
||
/** 使用共享 pipeline 单例,保证跨 store 的并发安全。 */
|
||
const pipeline = sharedPipeline;
|
||
|
||
export const useAutomationStore = create<AutomationState>((set, get) => ({
|
||
detected: [],
|
||
drafts: [],
|
||
isProcessing: false,
|
||
error: null,
|
||
stats: { ocr: 0, notification: 0, sms: 0, screenshot: 0, manual: 0 },
|
||
|
||
addDetected: (source, event) => {
|
||
logger.info('automationStore', `[收到自动化账单事件] 来源: ${source}, ID: ${event.id}, 时间: ${event.occurredAt}, 金额: ${event.amount} ${event.currency}, 对方: "${event.counterparty || '未知'}"`);
|
||
set(state => ({
|
||
detected: [...state.detected, {
|
||
id: `${source}:${event.id}`,
|
||
source,
|
||
event,
|
||
detectedAt: new Date().toISOString(),
|
||
}],
|
||
stats: { ...state.stats, [source]: state.stats[source] + 1 },
|
||
}));
|
||
},
|
||
|
||
processAll: async (ledger, rules, categories, history, dedupConfig, transferConfig) => {
|
||
const { detected } = get();
|
||
if (detected.length === 0) return;
|
||
const snapshotLength = detected.length;
|
||
set({ isProcessing: true, error: null });
|
||
try {
|
||
const events = detected.map(d => d.event);
|
||
const result = await pipeline.process(events, {
|
||
ledger, rules, categories, history, dedupConfig, transferConfig,
|
||
});
|
||
// 只清空已处理的事件,保留处理期间新增的
|
||
const { detected: currentDetected } = get();
|
||
set({
|
||
drafts: result.drafts,
|
||
detected: currentDetected.slice(snapshotLength),
|
||
isProcessing: false,
|
||
});
|
||
} catch (e) {
|
||
logger.error('automationStore', '处理自动化账单事件异常', e);
|
||
set({ isProcessing: false, error: e instanceof Error ? e.message : String(e) });
|
||
}
|
||
},
|
||
|
||
confirmDraft: (index) => {
|
||
const { drafts } = get();
|
||
const draft = drafts[index];
|
||
if (!draft) return null;
|
||
logger.info('automationStore', `[自动化草稿确认] 确认自动记账草稿 [日期: ${draft.draft.date}, 商户: "${draft.draft.payee ?? '未知'}", 叙述: "${draft.draft.narration}"]`);
|
||
set({ drafts: drafts.filter((_, i) => i !== index) });
|
||
return draft;
|
||
},
|
||
|
||
rejectDraft: (index) => {
|
||
const { drafts } = get();
|
||
const draft = drafts[index];
|
||
if (draft) {
|
||
logger.info('automationStore', `[自动化草稿拒绝] 丢弃自动记账草稿 [日期: ${draft.draft.date}, 商户: "${draft.draft.payee ?? '未知'}", 叙述: "${draft.draft.narration}"]`);
|
||
}
|
||
set({ drafts: drafts.filter((_, i) => i !== index) });
|
||
},
|
||
|
||
clear: () => {
|
||
logger.info('automationStore', '[自动化队列清空] 已清空自动化事件与待处理草稿');
|
||
set({
|
||
detected: [],
|
||
drafts: [],
|
||
isProcessing: false,
|
||
stats: { ocr: 0, notification: 0, sms: 0, screenshot: 0, manual: 0 },
|
||
});
|
||
},
|
||
}));
|