@@ -52,6 +52,8 @@ pub struct AiTaskEntry {
5252#[ serde( rename_all = "camelCase" ) ]
5353pub struct AiRefineResult {
5454 pub title : String ,
55+ /// 规则基线(本地规则版——采纳落库时作为首快照,版本链 [rule, ai-refine])
56+ pub base_markdown : String ,
5557 pub refined_markdown : String ,
5658 /// 与规则版的段级 diff(本地版为基线,AI 变化点高亮)
5759 pub diff : Vec < DiffOp > ,
@@ -161,14 +163,14 @@ pub fn ai_refine_result(state: State<'_, AppState>, task_id: u64) -> Result<AiRe
161163 }
162164}
163165
164- /// 采纳落库(REQ-141:diff 预览后用户采纳 → 新笔记,source=classroom;
165- /// M4 版本管理上线后统一 versioned 写路径)。
166+ /// 采纳落库(REQ-141:diff 预览后用户采纳;v0.8.0 M4 版本化写路径——
167+ /// ① 以规则基线建笔记(首快照)→ ② 精修版 = 新版本(ai-refine,含成本
168+ /// meta)→ ③ 成本落库 note_ai_usage)。
166169#[ tauri:: command]
167170pub fn ai_refine_apply (
168171 state : State < ' _ , AppState > ,
169172 session_id : i64 ,
170- refined_markdown : String ,
171- title : String ,
173+ result : AiRefineResult ,
172174) -> Result < Note , String > {
173175 if session_id <= 0 {
174176 return Err ( "无效的会话 id" . to_string ( ) ) ;
@@ -179,16 +181,57 @@ pub fn ai_refine_apply(
179181 . map_err ( |e| e. to_string ( ) ) ?
180182 . ok_or_else ( || format ! ( "会话不存在: {}" , session_id) ) ?;
181183 let fallback = format ! ( "{}(AI 精修)" , session. title) ;
182- let title = crate :: commands:: normalize_title ( title, & fallback) ;
184+ let title = crate :: commands:: normalize_title ( result. title . clone ( ) , & fallback) ;
185+ // ① 规则基线建笔记(版本链首快照原料——可回溯精修前内容)
183186 let new = NewNote {
184- title,
185- content : refined_markdown ,
187+ title : title . clone ( ) ,
188+ content : result . base_markdown . clone ( ) ,
186189 source : "classroom" . to_string ( ) ,
187190 session_id : Some ( session_id) ,
188- rule_version : Some ( "ai-refine " . to_string ( ) ) ,
191+ rule_version : Some ( "rule " . to_string ( ) ) ,
189192 purify_stats : None ,
190193 } ;
191- state. db . create_note ( & new) . map_err ( |e| e. to_string ( ) )
194+ let note = state. db . create_note ( & new) . map_err ( |e| e. to_string ( ) ) ?;
195+ // ② 精修版落库(新版本 ai-refine + 成本 meta)
196+ let cost = crate :: ai_cost:: usage_cost (
197+ result. base_markdown . chars ( ) . count ( ) ,
198+ result. refined_markdown . chars ( ) . count ( ) ,
199+ ) ;
200+ let meta = crate :: note_version:: VersionMeta {
201+ cost_yuan : Some ( cost) ,
202+ model : Some ( result. model . clone ( ) ) ,
203+ slices : Some ( result. slices ) ,
204+ merged_from : None ,
205+ } ;
206+ state
207+ . db
208+ . versioned_save (
209+ note. id ,
210+ & result. refined_markdown ,
211+ crate :: note_version:: NoteVersionSource :: AiRefine ,
212+ & meta,
213+ )
214+ . map_err ( |e| e. to_string ( ) ) ?;
215+ // ③ 成本落库(token 估算与预估同口径——校准单价表数据源)
216+ state
217+ . db
218+ . record_ai_usage (
219+ note. id ,
220+ & crate :: db_ai_usage:: AiUsageInput {
221+ op_type : "refine" ,
222+ tokens_in : result. base_markdown . chars ( ) . count ( ) ,
223+ tokens_out : result. refined_markdown . chars ( ) . count ( ) ,
224+ cost_yuan : cost,
225+ model : result. model . clone ( ) ,
226+ slices : result. slices ,
227+ } ,
228+ )
229+ . map_err ( |e| e. to_string ( ) ) ?;
230+ state
231+ . db
232+ . get_note ( note. id )
233+ . map_err ( |e| e. to_string ( ) ) ?
234+ . ok_or_else ( || "笔记不存在" . to_string ( ) )
192235}
193236
194237// ────────────────────────────────────────────────────────────
@@ -275,6 +318,7 @@ fn run_refine_task(st: AppState, task_id: u64, session_id: i64, mock: bool) {
275318 let ( added, removed, _) = diff_stats ( & diff) ;
276319 Ok ( AiRefineResult {
277320 title : draft. title . clone ( ) ,
321+ base_markdown : draft. markdown . clone ( ) ,
278322 refined_markdown : refined,
279323 diff,
280324 added_lines : added,
0 commit comments