/** * 自动记账状态管理(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; // actions /** 添加检测事件(来自 OCR/通知/短信/截图)。 */ addDetected: (source: AutomationSource, event: ImportedEvent) => void; /** 批量处理:把 detected 走 BillPipeline。 */ processAll: ( ledger: LedgerIndex, rules: Rule[], categories: Category[], history: Transaction[], dedupConfig?: DedupConfig, transferConfig?: TransferConfig, ) => Promise; /** 确认单笔草稿。 */ confirmDraft: (index: number) => ProcessedDraft | null; /** 拒绝单笔草稿。 */ rejectDraft: (index: number) => void; /** 清空。 */ clear: () => void; } /** 使用共享 pipeline 单例,保证跨 store 的并发安全。 */ const pipeline = sharedPipeline; export const useAutomationStore = create((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 }, }); }, }));