feat: 新增 AI 服务模块,支持智能聊天、语音转文字、交易确认及财务建议等功能。
This commit is contained in:
@@ -313,6 +313,9 @@ export interface DailyInsightContext {
|
|||||||
/**
|
/**
|
||||||
* Get AI-powered daily insight
|
* Get AI-powered daily insight
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Get AI-powered daily insight via dedicated endpoint
|
||||||
|
*/
|
||||||
export async function getDailyInsight(context: DailyInsightContext): Promise<{ spending: string; budget: string }> {
|
export async function getDailyInsight(context: DailyInsightContext): Promise<{ spending: string; budget: string }> {
|
||||||
// Hash needs to include new fields
|
// Hash needs to include new fields
|
||||||
const currentHash = JSON.stringify(context);
|
const currentHash = JSON.stringify(context);
|
||||||
@@ -326,64 +329,32 @@ export async function getDailyInsight(context: DailyInsightContext): Promise<{ s
|
|||||||
return dailyInsightCache.data;
|
return dailyInsightCache.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. Prepare Context Data
|
||||||
const weekday = new Date().toLocaleDateString('zh-CN', { weekday: 'long' });
|
const weekday = new Date().toLocaleDateString('zh-CN', { weekday: 'long' });
|
||||||
const weekDiff = context.lastWeekSpend !== undefined ? (context.todaySpend - context.lastWeekSpend) : 0;
|
const weekDiff = context.lastWeekSpend !== undefined ? (context.todaySpend - context.lastWeekSpend) : 0;
|
||||||
|
|
||||||
const prompt = `System: 你是 Novault 首席财务AI,也是用户的贴身管家。你的点评需要非常有温度、有依据。
|
const contextData = {
|
||||||
Context:
|
weekday,
|
||||||
- 今天是: ${weekday}
|
streakDays: context.streakDays || 0,
|
||||||
- 连续记账: ${context.streakDays || 0} 天
|
todaySpend: context.todaySpend,
|
||||||
- 今日支出: ${context.todaySpend}
|
yesterdaySpend: context.yesterdaySpend,
|
||||||
- 对比昨日: ${context.yesterdaySpend}
|
lastWeekSpend: context.lastWeekSpend || 0,
|
||||||
- 对比上周同日: ${context.lastWeekSpend || '无数据'} (差额: ${weekDiff})
|
weekSpendDiff: weekDiff,
|
||||||
- 月预算: ${context.monthlyBudget} (已用: ${context.monthlySpent}, 进度: ${(context.monthlySpent / (context.monthlyBudget || 1) * 100).toFixed(0)}%)
|
monthlyBudget: context.monthlyBudget,
|
||||||
- 月时间进度: ${(context.monthProgress * 100).toFixed(0)}%
|
monthlySpent: context.monthlySpent,
|
||||||
- 今日支出之王: ${context.topCategory ? `【${context.topCategory.name}】 ¥${context.topCategory.amount}` : '无'}
|
monthProgress: context.monthProgress,
|
||||||
- 今日最大一笔: ${context.maxTransaction ? `${context.maxTransaction.note || '未备注'} (¥${context.maxTransaction.amount})` : '无'}
|
topCategory: context.topCategory ? `${context.topCategory.name} (¥${context.topCategory.amount})` : '无',
|
||||||
|
maxTransaction: context.maxTransaction ? `${context.maxTransaction.note || '未备注'} (¥${context.maxTransaction.amount})` : '无',
|
||||||
Task:
|
};
|
||||||
请输出 JSON 对象(无 markdown 标记):
|
|
||||||
1. "spending": 针对今日支出的点评(40字内)。
|
|
||||||
- 必须结合【${weekday}】的场景(如周五可以放松,周一要收心)。
|
|
||||||
- 如果有【连续记账】成就(>3天),请顺带夸奖。
|
|
||||||
- 如果对比上周同日波动大,请指出。
|
|
||||||
- 结合最大支出进行调侃。
|
|
||||||
|
|
||||||
2. "budget": 针对预算状况的建议(40字内)。
|
|
||||||
- 结合月度进度,给出具体行动指南。
|
|
||||||
`;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await sendChatMessage(prompt);
|
// 3. Call Dedicated Backend Endpoint
|
||||||
|
const response = await api.post<ApiResponse<{ spending: string; budget: string }>>('/ai/insight', {
|
||||||
|
context_data: contextData
|
||||||
|
});
|
||||||
|
|
||||||
if (response.message) {
|
if (response.success && response.data) {
|
||||||
let content = response.message.trim();
|
const result = response.data;
|
||||||
|
|
||||||
// Extract JSON object using regex to handle potential markdown or extra text
|
|
||||||
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
||||||
if (jsonMatch) {
|
|
||||||
content = jsonMatch[0];
|
|
||||||
} else {
|
|
||||||
// Fallback cleanup if regex fails but it might still be JSON-ish
|
|
||||||
content = content.replace(/^```json\s*/, '').replace(/\s*```$/, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
let parsed;
|
|
||||||
try {
|
|
||||||
parsed = JSON.parse(content);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('AI returned invalid JSON, falling back to raw text split or default', content);
|
|
||||||
// Fallback: simple split if possible or default
|
|
||||||
parsed = {
|
|
||||||
spending: content.slice(0, 50) + '...',
|
|
||||||
budget: 'AI 分析数据格式异常,请稍后再试。'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = {
|
|
||||||
spending: parsed.spending || '暂无点评',
|
|
||||||
budget: parsed.budget || '暂无建议'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update Cache
|
// Update Cache
|
||||||
dailyInsightCache = {
|
dailyInsightCache = {
|
||||||
@@ -394,10 +365,15 @@ Task:
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
throw new Error('No insight received');
|
|
||||||
|
throw new Error(response.error || 'Failed to generate insight');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get AI insight:', error);
|
console.error('Failed to get AI insight:', error);
|
||||||
throw error;
|
// Return fallback instead of throwing to prevent UI crash
|
||||||
|
return {
|
||||||
|
spending: '今日消费情况暂未分析完成',
|
||||||
|
budget: '暂无预算建议'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user