feat:新增ai分析,新增储值账户,优化通知规则

This commit is contained in:
2026-03-12 14:03:01 +08:00
parent 6a00875246
commit 6ca962a187
22 changed files with 1294 additions and 237 deletions

View File

@@ -1,4 +1,5 @@
import notificationRulesSource from '../config/notificationRules.js'
import { DEFAULT_LEDGER_BY_ENTRY_TYPE } from '../config/ledger.js'
import notificationRulesSource from '../config/notificationRules.js'
const fallbackRules = notificationRulesSource.map((rule) => ({
...rule,
@@ -10,13 +11,36 @@ const fallbackRules = notificationRulesSource.map((rule) => ({
const amountPatterns = [
/(?:¥|¥|RMB|CNY|人民币)\s*(-?\d+(?:\.\d{1,2})?)/i,
/(-?\d+(?:\.\d{1,2})?)\s*(?:元|块|块钱)/,
/(?:金额|实付|扣费|扣款|消费|支出|收入|收款|到账|入账|转入|转出|支付|票价|车费|本次乘车)\D{0,8}?(-?\d+(?:\.\d{1,2})?)/,
/(?:金额|实付|扣费|扣款|消费|支出|收入|收款|到账|入账|转入|转出|支付|票价|车费|本次乘车|充值)\D{0,8}?(-?\d+(?:\.\d{1,2})?)/,
/(-?\d+(?:\.\d{1,2})?)\s*(?:元|块|块钱)?\s*(?:已支付|已收款|到账|入账)/,
]
let cachedRules = [...fallbackRules]
let remoteRuleFetcher = async () => []
const buildLedgerPayload = (rule, notification, merchant) => {
const entryType = rule?.entryType || 'expense'
const defaults = DEFAULT_LEDGER_BY_ENTRY_TYPE[entryType] || DEFAULT_LEDGER_BY_ENTRY_TYPE.expense
const channelName = notification?.channel || ''
const resolveName = (field, fromChannelFlag) => {
if (rule?.[fromChannelFlag] && channelName) return channelName
return rule?.[field] || defaults[field] || ''
}
return {
entryType,
fundSourceType: rule?.fundSourceType || defaults.fundSourceType,
fundSourceName: resolveName('fundSourceName', 'fundSourceNameFromChannel'),
fundTargetType: rule?.fundTargetType || defaults.fundTargetType,
fundTargetName:
resolveName('fundTargetName', 'fundTargetNameFromChannel') ||
(entryType === 'expense' ? merchant : defaults.fundTargetName),
impactExpense: rule?.impactExpense ?? defaults.impactExpense,
impactIncome: rule?.impactIncome ?? defaults.impactIncome,
}
}
const buildEmptyTransaction = (notification, options = {}) => ({
id: options.id,
merchant: options.merchant || 'Unknown',
@@ -25,6 +49,7 @@ const buildEmptyTransaction = (notification, options = {}) => ({
date: new Date(options.date || notification?.createdAt || new Date().toISOString()).toISOString(),
note: notification?.text || '',
syncStatus: 'pending',
...DEFAULT_LEDGER_BY_ENTRY_TYPE.expense,
})
export const setRemoteRuleFetcher = (fetcher) => {
@@ -78,6 +103,13 @@ const looksLikeAmount = (raw = '') => {
return /^[-\d.,]+\s*(?:元|块|块钱|¥|¥|人民币)?$/i.test(value)
}
const hasTransitRouteAmountFallback = (rule, text = '') => {
if (rule?.id !== 'transit-card-expense') return false
return /[\u4e00-\u9fa5]{2,}\s*(?:-|->|→|至|到)\s*[\u4e00-\u9fa5]{2,}[:]?\s*\d+(?:\.\d{1,2})?\s*(?:元|块|块钱)/.test(
text,
)
}
const extractMerchant = (text, rule) => {
const content = text || ''
@@ -134,7 +166,8 @@ const matchRule = (notification, rule) => {
const keywordHit = !rule.keywords?.length || rule.keywords.some((word) => text.includes(word))
const requiredTextHit =
!rule.requiredTextPatterns?.length ||
rule.requiredTextPatterns.some((word) => text.includes(word))
rule.requiredTextPatterns.some((word) => text.includes(word)) ||
hasTransitRouteAmountFallback(rule, text)
return channelHit && keywordHit && requiredTextHit
}
@@ -161,8 +194,7 @@ export const transformNotificationToTransaction = (notification, options = {}) =
}
const direction = options.direction || rule.direction || 'expense'
const normalizedAmount =
direction === 'income' ? Math.abs(amountFromText) : -Math.abs(amountFromText)
const normalizedAmount = direction === 'income' ? Math.abs(amountFromText) : -Math.abs(amountFromText)
let merchant = options.merchant || extractMerchant(notification?.text || '', rule) || 'Unknown'
if (merchant === 'Unknown' && rule.defaultMerchant) {
@@ -170,6 +202,7 @@ export const transformNotificationToTransaction = (notification, options = {}) =
}
const date = options.date || notification?.createdAt || new Date().toISOString()
const ledger = buildLedgerPayload(rule, notification, merchant)
const transaction = {
id: options.id,
@@ -179,6 +212,7 @@ export const transformNotificationToTransaction = (notification, options = {}) =
date: new Date(date).toISOString(),
note: notification?.text || '',
syncStatus: 'pending',
...ledger,
}
const requiresConfirmation =