Files
ledgerly-docs/dev/ai.md
T
rosercode d94d4e1ed5 docs: 补充收支记录页 AI 生成规则、CORS/反向代理与邮箱检查进度时间戳
同步近期 ledgerly 功能与配置变更到文档站:新增收支记录页「AI 生成规则」入口的用户与开发说明、补充新接口与规则应用逻辑、修正 CORS/反向代理与运行环境编码说明,并更新邮箱检查进度时间戳描述。

变更内容:
- guide/transactions.md 批量操作区新增「AI 生成规则」按钮与「AI 生成规则(从勾选账单)」小节(含「同时应用到勾选账单」),引用新截图。
- guide/ai.md、guide/rules.md、guide/pages.md 补充两处生成入口与「同时应用」说明。
- dev/api.md 新增 POST /api/ai/rule/apply-transactions;dev/rule-engine.md、dev/ai.md 补充 applyToTransactions 与两个生成入口。
- dev/build-deploy.md 将 CORS 更新为放开全部来源并信任反向代理转发头,新增运行环境 UTF-8 locale 说明。
- guide/email-bills.md 补充「立即检查」进度每行带 HH:mm:ss 时间戳。
- scripts/ts/capture-elements.ts 新增 captureAiRuleFromTransactions 截图场景(交易页 AI 生成规则弹窗)。

说明:
- 新截图 elements/ai-rule-transactions.png 已上传 OSS,正文以 OSS 长期地址引用。
2026-10-13 22:39:40 +08:00

135 lines
12 KiB
Markdown
Raw 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.
# AI 集成
系统集成 **DeepSeek** 大模型,用于分类打标、自然语言查询与记账等场景,并完整记录调用用量。
## 客户端
`DeepSeekClient` 直接调用 DeepSeek 的 **OpenAI 兼容 Chat Completions 接口**(`{baseUrl}/chat/completions`),为减小可执行 jar 体积,使用 **Spring `RestClient` + JDK `HttpClient`** 而非第三方 SDK:
- 请求体包含 `model`、`messages`、`temperature = 0.2`、`response_format = {"type":"json_object"}`、`stream = false`。
- 文本请求超时 **120 秒**,视觉请求超时 **240 秒**(`chatWithImages` 单独放宽)。
- 解析 `choices[0].message.content` 与 `usage`(输入/输出/总 token;缓存命中 token 取 DeepSeek 的缓存字段,读不到时为 0)。
- 对外保持 `chat(baseUrl, apiKey, model, systemPrompt, userPrompt)` 与 `DeepSeekResponse` 不变,各 `Ai*ServiceImpl` 无需改动。
- 另有 `chatWithImages(baseUrl, apiKey, model, systemPrompt, userText, imageDataUrls)`:将一张或多张图片以 base64 data URL 作为 `user` 消息的内容块发送(DeepSeek 视觉接口要求图片只能出现在 user 消息中),供图片记账使用。
- 调用异常统一转 `BusinessException`;异常链含超时(`SocketTimeoutException` 等)时返回面向用户的友好提示。
配置存储于 `t_ai_config`(每用户一条):`enabled`、`api_key`、`base_url`(默认 `https://api.deepseek.com`)、`model`(默认 `deepseek-chat`)、`vision_model`(图片记账视觉模型,默认 `deepseek-flash`)。API Key 对外脱敏,支持连通性测试。
## 数据外发范围(隐私)
调用大模型时**仅发送功能所需的最小字段**,禁止外发敏感数据:
- **禁止**:`rawData`(账单原始 JSON)、金额、交易号、账户/卡号、姓名、地址、电话等。
- **允许**:收支类型、交易渠道、备注(必要时截断/脱敏)、分类与标签的名称/ID、用户主动输入的自然语言文本。
- **智能查数**:仅发送用户提问与可查询的**表结构说明**,不发送任何账单数据行;数据查询与隔离在本地由安全网关完成。
- **图片记账例外**:用户主动上传的账单/支付截图会**整张**发送给视觉模型(可能含金额、交易对方、订单号等),视为用户主动提供的输入;图片同时落盘于 `data/ai-images/`,仅本人可经 `GET /api/ai/image/{id}` 访问,禁止用于其他外发场景。
## 提示词模板
各功能的提示词统一抽取为 **Thymeleaf 文本模板**,存放于 `src/main/resources/prompts/`(`spring.thymeleaf` 配置 `prefix=classpath:/prompts/`、`suffix=.tpl`、`mode=TEXT`),由 `PromptTemplateService` 加载渲染。
- 每个功能一个模板文件,如 `ai-record.tpl`、`ai-channel.tpl`、`ai-relation.tpl`、`ai-sql.tpl` 等,共 14 个。
- **系统提示词与用户提示词合并为同一文件的不同片段**,由注入的 `__fragment` 变量经 `th:if` 选择:一般功能为 `system` / `user` 两段,建议功能(`ai-suggestion.tpl`)为 `strictSystem` / `newSystem` / `user` 三段;服务端调用 `render(template, fragment, vars)` 时传入片段名与变量。
## 调用类型
`AiCallType` 定义了 13 类调用,全部记录用量:
| 枚举值 | 含义 |
|---|---|
| `SUGGEST` | 交易分类/标签建议(批量 + 单条) |
| `TEST` | 连接测试 |
| `QUERY` | 自然语言查询解析为筛选条件 |
| `RECORD` | 自然语言记账解析为交易记录 |
| `RECORD_IMAGE` | 图片记账解析(视觉模型) |
| `DECORATE` | 分类 Emoji / 标签颜色建议 |
| `CATEGORY` | 自然语言生成分类 |
| `TAG` | 自然语言生成标签 |
| `CHANNEL` | 自然语言生成交易渠道 |
| `RULE` | 由已打标签记录生成分类与标签规则 |
| `STATISTICS` | 自然语言统计查询解析 |
| `RELATION` | 由关联账单生成关系名称与备注 |
| `SQL` | 自然语言转换为只读 SQL 查询(智能查数) |
## 分类/标签建议
`AiSuggestionServiceImpl`:
- 先按「类型 + 交易渠道 + 备注」聚合去重,仅将代表记录送模型;单批 ≤ 100 条、总量 ≤ 500 条,超出则分批调用。
- 支持严格模式(仅从已有分类/标签中选择)与增强模式(可返回新分类/新标签)。
- **单条建议**额外下发当前用户的渠道候选(内置取中文名、自定义取用户名称),模型可返回 `channel`,服务端仅接受候选中的编码;批量建议不下发渠道候选。
- 应用时分类覆盖写入、标签追加;新建分类按「类型 + 名称」去重,新建标签按名称去重并校验颜色格式。
## 自然语言查询
`AiQueryServiceImpl` 在提示词中注入当前分类、标签与当前日期,解析出:类型、交易渠道、分类名、标签名、日期范围预设(今天/昨天/本周/上周/本月/上月/近7天/近30天/今年/去年/自定义/无)、金额区间、备注、是否计入统计、是否未分类等,并在服务端把预设换算为绝对时间。
## 自然语言记账
`AiRecordServiceImpl` 解析类型、金额、分类、标签、交易渠道、交易时间、备注、计入统计等字段;金额无效直接报错,时间缺失回退当前时间。
## 生成交易渠道
`AiChannelServiceImpl` 根据用户对渠道用途的自然语言描述,使用 `ai-channel` 模板(`system` 片段)生成 2–6 字的渠道名称(返回 `name` 与 `reason`,名称长度上限 20),对应 `POST /api/ai/channel`。前端在新增/编辑记录的「交易渠道 · AI 新建」中使用,确认后经 `POST /api/channel` 创建并选中。
## 图片记账
`AiImageRecordServiceImpl` 处理截图上传到生成记录的完整链路,支持**一次上传多张图片**(最多 5 张,单张 ≤ 10MB、总 ≤ 30MB),合并为一次视觉模型调用:
1. 校验图片数量、单张与总大小、格式(JPEG/PNG/GIF/WebP,按魔数识别),并确认 AI 已启用且配置了 API Key。
2. 逐张经 `AiImageStorage` 落盘至 `data/ai-images/{userId}/{yyyyMM}/`(经 `AppHome` 解析运行目录),每张写入一条 `t_ai_image`(记录 MIME、尺寸、所用视觉模型等)。
3. 以 `ai-record-image` 模板构造提示词(`system` 片段)(注入可用分类、标签、渠道与当前时间,要求模型对每笔输出 `imageIndex` 与 `rawData`),调用 `chatWithImages` 使用**视觉模型**一次性识别。
4. 复用 `AiRecordJsonParser` 解析 `records` 数组(与文字记账共用:去除代码围栏、匹配分类/标签、校验渠道编码、回退时间),并回填摘要与各图记录数。
5. **重复检测**:`TransactionDedupUtils` 以「类型 + 金额 + 交易时间」为去重键,与已有记录及本批其它记录比对,命中则标记 `duplicate=true`(前端默认不勾选)。
6. 按模型标注的 `imageIndex` 将每笔记录关联到来源图片,前端确认后经 `POST /api/transaction/batch` 批量创建(同时写入 `rawData`、建立 `t_ai_image_transaction` 关联,并在导入阶段按同一去重键兜底跳过重复)。
**实时进度**:`AiImageRecordStreamService` 基于 SSE 将上述步骤的进度文本推送前端——`POST /api/ai/record/image/stream` 返回 `SseEmitter`,识别在固定线程池(8 线程)中执行,过程中逐条推送 `progress` 事件,结束推送 `done`(完整结果 JSON)或 `failed`(失败原因)。连接超时 **5 分钟**,并每 **15 秒**发送一次 SSE 注释行作为心跳保活(前端 SSE 客户端自动跳过注释行)。
**图片访问**:`GET /api/ai/image/{id}` 返回原图,`?thumbnail=true` 返回最长边 1280 的 JPEG 缩略图;仅图片所属用户可访问。收支记录「账单详情」弹窗中的来源图片默认加载缩略图,用户可按需切换原图。
## 规则生成
`AiRuleServiceImpl` 由用户勾选的记录反推分类/标签规则,有两个入口:规则管理页(依据**已打标签记录**)与收支记录页批量操作(依据**任意勾选账单**):
- 仅加载属于当前用户且被勾选的记录(单次 ≤ 500 条),附带其标签名称与分类名;提示词字段为 `type / remark / channel / category / tags`(不含金额与 `rawData`),并对相同「类型 + 备注 + 渠道 + 标签」组合去重。
- 解析并校验模型输出(分类 ID 归属与类型、标签归属、名称长度、颜色格式等)。
- **去重**:按「类型 + 归一化关键词 + 分类 + 排序标签集合」计算签名;与已有规则或本次建议内部重复的条目标记 `duplicate=true`(前端默认不勾选且禁止导入),`apply` 时后端再次校验并跳过。
- **应用到交易**:`POST /api/ai/rule/apply-transactions`(`applyToTransactions`)按传入规则对指定交易做**本地匹配**(不调用大模型、不外发数据),命中时覆盖分类、追加标签,必要时新建分类/标签;用于收支记录页勾选「同时应用到账单」的场景。
## 统计搜索
`AiStatisticsServiceImpl` 在提示词中注入分类、标签与当前日期,将自然语言解析为统计面板参数:时间范围(预设→绝对时间)、收支类型、分类、标签、交易渠道、金额区间、备注、统计口径(`statsScope`)与趋势粒度(`dimension`)。
## 生成账单关系名称
`AiRelationServiceImpl` 依据用户新建关系组时勾选的**关联账单**生成名称与备注,对应 `POST /api/ai/relation`。
- 请求体为 `AiRelationRequest`(`type` + `members`);每个成员仅携带白名单字段——**收支类型、交易渠道、分类名称与备注**(`AiRelationMemberRequest` 明确不含金额、交易号与 `rawData`)。
- 用户提示词由服务端集中拼接(关系类型中文名 + 成员字段,备注截断 30 字,最多 20 条);系统提示词模板 `ai-relation`(`system` 片段)要求输出 4–12 字名称、≤40 字备注,并说明借贷场景(借出/归还、分次归还)的判定,只输出 JSON。
- 解析后名称截断 ≤50 且必须非空、备注 ≤200;经 `AiUsageService` 记录 `AiCallType.RELATION` 用量。前端在[账单关系](/guide/relations)编辑弹窗中用它覆盖名称(备注非空时覆盖备注)。
## 智能查数(Text-to-SQL)
`AiSqlQueryService` 将自然语言交给大模型生成**只读 SQL** 并执行,配套独立的提问历史,对应 `POST /api/ai/sql`。
- 系统提示词模板 `ai-sql`(`system` 片段):按当前数据源方言(SQLite / MySQL)注入日期函数说明与**动态表结构**——`TextToSqlSchemaProvider` 从数据库元数据读取列(字段增删改自动同步),并剔除 `raw_data`、`external_no` 等敏感列;要求模型只输出 `{sql, explanation, chartType}`,且**不要自带 `user_id`**(由网关统一注入)。
- **安全网关** `cn.rosercode.ledgerly.util.sql.TextToSqlGuard` 是唯一执行通道:仅允许单条 `SELECT`(拒绝写操作/DDL/多语句/`SELECT INTO`/`FOR UPDATE`/危险函数);表白名单由 `TextToSqlTables` 静态登记(`t_transaction`、`t_category`、`t_tag`、`t_channel`、`t_transaction_group`、`t_rule` 及无 `user_id` 的关联表,新增可查表必须显式加入);对每张表强制注入 `user_id` 隔离条件(主表加 `WHERE`、JOIN 表加 `ON`、关联表用父表归属子查询);强制行数上限 `MAX_ROWS = 1000`。
- 结果 VO `AiSqlQueryResultVO` 含 `sql`、`explanation`、`chartType`(`SCALAR`/`TABLE`/`BAR`/`PIE`)、`columns`、`rows`、`rowCount`、`truncated`;列类型映射为 `NUMBER`/`STRING`/`DATE`/`DATETIME`/`BOOLEAN`。
- **提问历史**:`AiSqlHistoryService` 在每次成功后写入 `t_ai_sql_history`(按用户),提供 `GET /api/ai/sql/history`(limit 上限 200)与 `DELETE /api/ai/sql/history`。
- 用量记录 `AiCallType.SQL`。
## 用量记录
- 每次调用写入 `t_ai_usage`:调用类型、模型、分析条数、输入/输出/总/缓存 token、是否成功、错误信息、耗时。
- 记录失败仅告警,不影响主流程。
- `GET /api/ai/usage` 返回汇总与最近明细,对应「模型使用情况」页面。
## 说明
账单类型识别**不使用 AI**,而是由邮箱账单的**发件人规则**确定,结果确定、可预期。详见[定时通知](/dev/notifications)与[邮箱账单](/guide/email-bills)。
## 相关文档
- [AI 能力(使用说明)](/guide/ai)
- [整体架构](/dev/architecture)