Files
app_02_bill/services/geminiService.ts
karlcarlo c06e74bd01 feat(app): 实现完整的财务管家记账聊天与统计功能
- 新增App主组件,实现聊天、账本、统计三大功能模块切换
- 支持语音及文本输入,自动解析记账信息并生成交易记录
- 交易记录支持编辑、删除、提醒等操作,数据本地持久化
- 管家性格预设和自定义设定,包含头像上传和语气调整
- 会话展示支持关联交易显示,样式区分收入与支出
- 账本页添加日期分组交易列表及饼图统计展示
- 消息列表实现渐变背景和交互动画,提升用户体验
- 支持图片上传并同步发送给AI进行解析记账
- 底部导航实现三标签切换并同步显示对应内容
- 全局事务编辑弹窗支持直接调用和删除操作
- 增加环境变量示例和.gitignore规则,完善工程配置
2026-01-19 20:20:54 +08:00

121 lines
3.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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你自己记一下吧!🙄`,
};
}
};