feat: 添加 AI 分类功能,支持交易自动分类与标签生成
This commit is contained in:
5
src/services/ai/aiProvider.js
Normal file
5
src/services/ai/aiProvider.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export class AiProvider {
|
||||
async enrichTransaction() {
|
||||
throw new Error('AI provider is not implemented')
|
||||
}
|
||||
}
|
||||
63
src/services/ai/aiService.js
Normal file
63
src/services/ai/aiService.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import { buildTransactionEnrichmentMessages } from '../../lib/aiPrompt.js'
|
||||
import { useSettingsStore } from '../../stores/settings.js'
|
||||
import {
|
||||
applyAiEnrichment,
|
||||
fetchRecentTransactionsForAi,
|
||||
markTransactionAiFailed,
|
||||
} from '../transactionService.js'
|
||||
import { DeepSeekProvider } from './deepseekProvider.js'
|
||||
|
||||
const clampThreshold = (value) => {
|
||||
const numeric = Number(value)
|
||||
if (!Number.isFinite(numeric)) return 0.9
|
||||
return Math.min(Math.max(numeric, 0), 1)
|
||||
}
|
||||
|
||||
const createProvider = (settingsStore) => {
|
||||
if (settingsStore.aiProvider !== 'deepseek') {
|
||||
throw new Error(`Unsupported AI provider: ${settingsStore.aiProvider}`)
|
||||
}
|
||||
|
||||
return new DeepSeekProvider({
|
||||
apiKey: settingsStore.aiApiKey,
|
||||
model: settingsStore.aiModel,
|
||||
})
|
||||
}
|
||||
|
||||
export const isAiReady = (settingsStore = useSettingsStore()) =>
|
||||
!!settingsStore.aiAutoCategoryEnabled && !!settingsStore.aiApiKey
|
||||
|
||||
export const maybeEnrichTransactionWithAi = async (transaction) => {
|
||||
if (!transaction?.id) return transaction
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
if (!isAiReady(settingsStore)) {
|
||||
return transaction
|
||||
}
|
||||
|
||||
try {
|
||||
const provider = createProvider(settingsStore)
|
||||
const similarTransactions = await fetchRecentTransactionsForAi(6, transaction.id)
|
||||
const messages = buildTransactionEnrichmentMessages({
|
||||
transaction,
|
||||
similarTransactions,
|
||||
})
|
||||
const result = await provider.enrichTransaction({ messages })
|
||||
const threshold = clampThreshold(settingsStore.aiAutoApplyThreshold)
|
||||
|
||||
const updated = await applyAiEnrichment(transaction.id, result.normalized, {
|
||||
applyCategory: result.normalized.confidence >= threshold,
|
||||
model: result.model,
|
||||
})
|
||||
|
||||
return updated || transaction
|
||||
} catch (error) {
|
||||
console.warn('[ai] transaction enrichment failed', error)
|
||||
const failed = await markTransactionAiFailed(
|
||||
transaction.id,
|
||||
error?.message || 'AI enrichment failed',
|
||||
settingsStore.aiModel,
|
||||
)
|
||||
return failed || transaction
|
||||
}
|
||||
}
|
||||
55
src/services/ai/deepseekProvider.js
Normal file
55
src/services/ai/deepseekProvider.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import { normalizeEnrichmentResult } from '../../lib/aiPrompt.js'
|
||||
import { AiProvider } from './aiProvider.js'
|
||||
|
||||
export class DeepSeekProvider extends AiProvider {
|
||||
constructor({ apiKey, baseUrl = 'https://api.deepseek.com', model = 'deepseek-chat' }) {
|
||||
super()
|
||||
this.apiKey = apiKey
|
||||
this.baseUrl = baseUrl.replace(/\/$/, '')
|
||||
this.model = model
|
||||
}
|
||||
|
||||
async enrichTransaction({ messages }) {
|
||||
if (!this.apiKey) {
|
||||
throw new Error('DeepSeek API key is required')
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: this.model,
|
||||
temperature: 0.2,
|
||||
response_format: { type: 'json_object' },
|
||||
messages,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const detail = await response.text().catch(() => '')
|
||||
throw new Error(`DeepSeek request failed: ${response.status} ${detail}`.trim())
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const content = data?.choices?.[0]?.message?.content
|
||||
if (!content) {
|
||||
throw new Error('DeepSeek returned an empty response')
|
||||
}
|
||||
|
||||
let parsed
|
||||
try {
|
||||
parsed = JSON.parse(content)
|
||||
} catch {
|
||||
throw new Error('DeepSeek returned invalid JSON')
|
||||
}
|
||||
|
||||
return {
|
||||
raw: parsed,
|
||||
normalized: normalizeEnrichmentResult(parsed),
|
||||
model: data?.model || this.model,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user