feat(app): 实现完整的财务管家记账聊天与统计功能
- 新增App主组件,实现聊天、账本、统计三大功能模块切换 - 支持语音及文本输入,自动解析记账信息并生成交易记录 - 交易记录支持编辑、删除、提醒等操作,数据本地持久化 - 管家性格预设和自定义设定,包含头像上传和语气调整 - 会话展示支持关联交易显示,样式区分收入与支出 - 账本页添加日期分组交易列表及饼图统计展示 - 消息列表实现渐变背景和交互动画,提升用户体验 - 支持图片上传并同步发送给AI进行解析记账 - 底部导航实现三标签切换并同步显示对应内容 - 全局事务编辑弹窗支持直接调用和删除操作 - 增加环境变量示例和.gitignore规则,完善工程配置
This commit is contained in:
121
services/geminiService.ts
Normal file
121
services/geminiService.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { GoogleGenAI, Type } from "@google/genai";
|
||||
import { ExpenseCategory, IncomeCategory, TransactionType } from "../types";
|
||||
|
||||
const ai = new GoogleGenAI({
|
||||
apiKey: process.env.API_KEY,
|
||||
httpOptions: {
|
||||
baseUrl: process.env.GLM_API,
|
||||
},
|
||||
});
|
||||
|
||||
// 使用推荐的文本模型
|
||||
const MODEL_NAME = "gemini-3-flash-preview";
|
||||
|
||||
const getSystemInstruction = (aiName: string, aiPersona: string) => `
|
||||
你现在的身份是"${aiName}"。
|
||||
你的性格设定是:${aiPersona}
|
||||
|
||||
你的任务是:
|
||||
1. 从用户的输入中提取交易信息(内容、金额、类别、交易类型)。
|
||||
2. 判断是【支出】(expense) 还是【收入】(income)。
|
||||
3. **严格回复规范**:
|
||||
- 回复必须**极度简练**。
|
||||
- 每一段话的字数必须控制在 **30字左右**(越短越好)。
|
||||
- 全部的回复**最多只能分成 2 段**(中间用一个 \\n 换行符隔开)。
|
||||
- 必须在每一段中都加入**生动的表情包 (Emoji)** 🌈。
|
||||
- 说话风格要完全符合"${aiName}"的设定。
|
||||
|
||||
**关键规则 - 模糊输入处理**:
|
||||
- 如果用户只说了金额没说干嘛了,默认为“杂项”支出,并进行简短吐槽。
|
||||
|
||||
**交易类别参考**:
|
||||
- 支出:餐饮、购物、交通、住房、娱乐、医疗、人情、学习、金融、孩子、宠物、汽车、美容、经营、杂项。
|
||||
- 收入:工资、奖金、理财、兼职、红包、其他收入。
|
||||
`;
|
||||
|
||||
const parseResponseSchema = {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
transaction: {
|
||||
type: Type.OBJECT,
|
||||
nullable: true,
|
||||
description: "提取的交易详情",
|
||||
properties: {
|
||||
item: { type: Type.STRING },
|
||||
amount: { type: Type.NUMBER },
|
||||
type: {
|
||||
type: Type.STRING,
|
||||
enum: [TransactionType.EXPENSE, TransactionType.INCOME]
|
||||
},
|
||||
category: { type: Type.STRING },
|
||||
},
|
||||
required: ["item", "amount", "type", "category"],
|
||||
},
|
||||
reply: {
|
||||
type: Type.STRING,
|
||||
description: "管家的精简回复(每段约30字,最多2段),必须带表情包,用\\n换行",
|
||||
},
|
||||
},
|
||||
required: ["reply"],
|
||||
};
|
||||
|
||||
export interface ParseResult {
|
||||
transaction: {
|
||||
item: string;
|
||||
amount: number;
|
||||
type: TransactionType;
|
||||
category: string;
|
||||
} | null;
|
||||
reply: string;
|
||||
}
|
||||
|
||||
export const processUserMessage = async (
|
||||
message: string,
|
||||
aiName: string,
|
||||
aiPersona: string,
|
||||
imageBase64?: string,
|
||||
mimeType?: string
|
||||
): Promise<ParseResult> => {
|
||||
try {
|
||||
const parts: any[] = [];
|
||||
if (imageBase64 && mimeType) {
|
||||
parts.push({
|
||||
inlineData: {
|
||||
data: imageBase64,
|
||||
mimeType: mimeType
|
||||
}
|
||||
});
|
||||
if (!message) {
|
||||
parts.push({ text: "请识别这张图片里的账单详情" });
|
||||
}
|
||||
}
|
||||
|
||||
if (message) {
|
||||
parts.push({ text: message });
|
||||
}
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model: MODEL_NAME,
|
||||
contents: { parts },
|
||||
config: {
|
||||
systemInstruction: getSystemInstruction(aiName, aiPersona),
|
||||
responseMimeType: "application/json",
|
||||
responseSchema: parseResponseSchema,
|
||||
temperature: 0.8,
|
||||
},
|
||||
});
|
||||
|
||||
const text = response.text;
|
||||
if (!text) {
|
||||
throw new Error("No response from Gemini");
|
||||
}
|
||||
|
||||
return JSON.parse(text) as ParseResult;
|
||||
} catch (error) {
|
||||
console.error("Error processing message with Gemini:", error);
|
||||
return {
|
||||
transaction: null,
|
||||
reply: `信号不好 📶\n你自己记一下吧!🙄`,
|
||||
};
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user