同步 ledgerly 近期开发改动(接口请求/响应体加解密、代码检查工具、安全与稳定性修复、AI 提示词模板抽取),补齐对应开发文档并修正过时表述,同时在快速开始补充小屏适配说明。 变更内容: - dev/api.md:新增「接口数据加密」小节,说明 RSA + AES-GCM 握手流程、信封格式、会话有效期与失效码、豁免范围及编译期开关。 - dev/architecture.md:请求处理流程补入加密会话拦截器与请求/响应体 Advice,关键设计亮点新增接口数据加密一条。 - dev/development.md:新增「代码检查」小节,登记 Spotless / Checkstyle / SpotBugs 与前端 ESLint / Prettier 配置及运行命令。 - dev/build-deploy.md:新增「安全配置」(JWT_SECRET 覆盖、CORS 放行来源),构建任务链补入前端 lint 校验说明。 - dev/ai.md:修正已重命名的提示词模板名,补充「提示词模板」片段机制与图片记账 SSE 超时、心跳说明。 - dev/project-structure.md:补充 config.crypto / util.crypto 包、prompts 资源目录、crypto-keys.json 运行时文件与交易服务拆分说明。 - guide/getting-started.md:新增「小屏与移动端适配」说明。 - guide/transactions.md:导出说明补充单次 10000 条上限。
134 lines
12 KiB
Markdown
134 lines
12 KiB
Markdown
# 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` 时后端再次校验并跳过。
|
||
|
||
## 统计搜索
|
||
|
||
`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)
|