为规则、分类、标签管理页新增的「批量操作模式」与多选删除补充使用与开发文档,同时补上此前已完成但未同步的 AI 规则建议可编辑、编辑后实时重算去重等文档,并登记批量模式截图脚本。 变更内容: - guide/categories-tags.md:补充分类与标签的批量删除说明,并回填 tags-batch.png、categories-batch.png 配图。 - guide/rules.md:批量删除与应用匹配账单改为需先进入「批量操作」模式(默认不显示勾选框)。 - dev/api.md:新增 DELETE /api/tag/batch、/api/category/batch,并补齐规则命中账单预览/一键应用、AI 规则去重校验等接口。 - dev/data-model.md:说明标签删除清理交易标签与规则标签关联、分类删除保留逻辑引用(账单显示未分类)。 - dev/rule-engine.md、dev/ai.md、dev/preferences.md、guide/ai.md、guide/transactions.md:同步 AI 规则建议可编辑、编辑后实时重算去重、应用开关默认开启并持久化等说明。 - scripts/ts/capture-elements.ts:新增 captureBatchModes()(标签/分类批量模式截图)并纳入 main()。 说明: - 截图经测试环境(前端 5174 / 后端 8081,虚构数据)生成,已上传 OSS,正文以 OSS 长期地址引用;public/screenshots 目录不入库。
13 KiB
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),合并为一次视觉模型调用:
- 校验图片数量、单张与总大小、格式(JPEG/PNG/GIF/WebP,按魔数识别),并确认 AI 已启用且配置了 API Key。
- 逐张经
AiImageStorage落盘至data/ai-images/{userId}/{yyyyMM}/(经AppHome解析运行目录),每张写入一条t_ai_image(记录 MIME、尺寸、所用视觉模型等)。 - 以
ai-record-image模板构造提示词(system片段)(注入可用分类、标签、渠道与当前时间,要求模型对每笔输出imageIndex与rawData),调用chatWithImages使用视觉模型一次性识别。 - 复用
AiRecordJsonParser解析records数组(与文字记账共用:去除代码围栏、匹配分类/标签、校验渠道编码、回退时间),并回填摘要与各图记录数。 - 重复检测:
TransactionDedupUtils以「类型 + 金额 + 交易时间」为去重键,与已有记录及本批其它记录比对,命中则标记duplicate=true(前端默认不勾选)。 - 按模型标注的
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时后端再次校验并跳过。 - 编辑后重算去重:规则弹窗(两处入口共用的
AiRuleEditorTable.vue)支持逐字段编辑;任一参与签名的字段(类型/关键词/金额区间/分类/标签)变化后防抖调用POST /api/ai/rule/check-duplicates(AiRuleServiceImpl.checkDuplicates,纯本地复用签名逻辑)刷新duplicate,并自动增删勾选。 - 应用到交易:
POST /api/ai/rule/apply-transactions(applyToTransactions)按传入规则对指定交易做本地匹配(不调用大模型、不外发数据),命中时覆盖分类、追加标签,必要时新建分类/标签;用于收支记录页勾选「同时应用到账单」的场景,该开关默认开启并经ledgerly.transaction.aiRuleApplyToSelection持久化。
统计搜索
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用量。前端在账单关系编辑弹窗中用它覆盖名称(备注非空时覆盖备注)。
智能查数(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,而是由邮箱账单的发件人规则确定,结果确定、可预期。详见定时通知与邮箱账单。