Skip to content

Commit e962b22

Browse files
committed
refactor(rust): 拆 commands_ai_refine 步 4 — 采纳落库/历史域
1 parent 077b9ac commit e962b22

4 files changed

Lines changed: 155 additions & 140 deletions

File tree

‎app/src-tauri/src/app_commands.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -399,14 +399,14 @@ pub fn handle() -> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send + Sync
399399
crate::commands_ai_refine::session::ai_refine_strategy_meta,
400400
crate::commands_ai_refine::session::ai_refine_prompt_preview,
401401
crate::commands_ai_refine::session::ai_refine_result,
402-
crate::commands_ai_refine::ai_refine_apply,
402+
crate::commands_ai_refine::apply::ai_refine_apply,
403403
crate::commands_ai_refine::workbench::refine_workbench,
404404
// v0.17.0(REQ-246):笔记级 AI 精修——估计/启动/采纳(手写笔记)
405405
crate::commands_ai_note_refine::ai_note_refine_estimate,
406406
crate::commands_ai_note_refine::ai_note_refine_start,
407407
crate::commands_ai_note_refine::ai_note_refine_apply,
408408
// v0.8.0 F2(2026-08-21):任务中心——历史列表(面板数据源)
409-
crate::commands_ai_refine::ai_task_history,
409+
crate::commands_ai_refine::apply::ai_task_history,
410410
// v0.16.0(REQ-224/225/226/227/228/230):内嵌 AI 对话——
411411
// 纯聊天(会话 CRUD/流式发送/停止/重发)+ 任务对话视图(轨迹详情)
412412
crate::commands_ai_chat::chat_create_session,
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
//! 采纳落库 + 任务历史(批 0-C3 Task 4 拆分,原 commands_ai_refine.rs 332–445 行)。
2+
//!
3+
//! @ai-context: 采纳 = ① 以规则基线建笔记(首快照)→ ② 精修版按版本化写路径落库
4+
//! (ai-refine + 成本 meta)→ ③ 成本落 note_ai_usage → ④ 标记任务已采纳
5+
//! / 回填成本 → ⑤ 广播 notes 域。
6+
//! @ai-context: 顺序铁律(逐字搬运不得重排):**采纳守卫前置于任何写库**(2026-09-04
7+
//! 审查修复:后置会留孤儿半态且早退不播 Notes);守卫失败必须早退。
8+
//! @ai-context: 既有缺陷只搬不改:`mark_ai_task_adopted` / `update_ai_task_cost` 的
9+
//! 返回值被 `let _ =` 静默吞掉(分析 §R7);`ai_task_history` 的
10+
//! op_type 白名单与 `unwrap_or(50).min(200)` 两个魔法上界原样保留。
11+
12+
use tauri::State;
13+
14+
use crate::commands::AppState;
15+
use crate::types::{NewNote, Note};
16+
17+
use super::dto::AiRefineResult;
18+
19+
/// 任务历史(F2 任务中心:前端面板数据源——按类型列最近任务)。
20+
#[tauri::command]
21+
pub fn ai_task_history(
22+
state: State<'_, AppState>,
23+
op_type: String,
24+
limit: Option<usize>,
25+
) -> Result<Vec<crate::db_ai_tasks::AiTaskRecord>, String> {
26+
if op_type != "refine" && op_type != "enrich" {
27+
return Err("无效的任务类型(refine|enrich)".to_string());
28+
}
29+
state
30+
.db
31+
.list_ai_tasks(&op_type, limit.unwrap_or(50).min(200))
32+
.map_err(|e| e.to_string())
33+
}
34+
35+
/// 采纳落库(REQ-141:diff 预览后用户采纳;v0.8.0 M4 版本化写路径——
36+
/// ① 以规则基线建笔记(首快照)→ ② 精修版 = 新版本(ai-refine,含成本
37+
/// meta)→ ③ 成本落库 note_ai_usage)。
38+
/// @ai-context: F2(2026-08-21):task_id 可选——传入时标记任务已采纳
39+
/// (防重启后从任务中心重复采纳产生重复笔记)。
40+
#[tauri::command]
41+
pub fn ai_refine_apply(
42+
state: State<'_, AppState>,
43+
session_id: i64,
44+
result: AiRefineResult,
45+
task_id: Option<u64>,
46+
) -> Result<Note, String> {
47+
if session_id <= 0 {
48+
return Err("无效的会话 id".to_string());
49+
}
50+
let session = state
51+
.db
52+
.get_session(session_id)
53+
.map_err(|e| e.to_string())?
54+
.ok_or_else(|| format!("会话不存在: {}", session_id))?;
55+
let fallback = format!("{}(AI 精修)", session.title);
56+
let title = crate::commands::normalize_title(result.title.clone(), &fallback);
57+
// 审查修复(2026-09-04):采纳守卫**前置**于任何写库——F2 注释自称"服务端
58+
// 前置校验"但实为后置(先建基线笔记/写版本/记 usage 再拒,重复采纳会留
59+
// 孤儿半态且 Err 早退不播 Notes);双点/陈旧面板/多入口并发兜底依赖此处。
60+
if let Some(tid) = task_id {
61+
if state.db.is_ai_task_adopted(tid) {
62+
return Err("该任务结果已采纳落库——请勿重复采纳(可到笔记页查看)".to_string());
63+
}
64+
}
65+
// ① 规则基线建笔记(版本链首快照原料——可回溯精修前内容)
66+
let new = NewNote {
67+
title: title.clone(),
68+
content: result.base_markdown.clone(),
69+
source: "classroom".to_string(),
70+
session_id: Some(session_id),
71+
rule_version: Some("rule".to_string()),
72+
purify_stats: None,
73+
tags: None,
74+
properties: None,
75+
// v0.11.0:精修基线笔记继承会话组归属(组化接线在会话转笔记链路写入,
76+
// 精修新建基线同样归组——同会话同组,防组内资产漏登)
77+
group_id: crate::note_group_assign::group_of_session(&state.db, session_id).ok().flatten(),
78+
};
79+
let note = state.db.create_note(&new).map_err(|e| e.to_string())?;
80+
// ② 精修版落库(新版本 ai-refine + 成本 meta)
81+
// 审查修复(2026-08-21):落库成本用模型感知单价(与预估同口径——
82+
// 付费模型预估 ¥X 不再记 ¥0)
83+
let cost = crate::ai_cost::usage_cost_for_model(
84+
result.base_markdown.chars().count(),
85+
result.refined_markdown.chars().count(),
86+
&result.model,
87+
);
88+
let meta = crate::note_version::VersionMeta {
89+
cost_yuan: Some(cost),
90+
model: Some(result.model.clone()),
91+
slices: Some(result.slices),
92+
merged_from: None,
93+
};
94+
state
95+
.db
96+
.versioned_save(
97+
note.id,
98+
&result.refined_markdown,
99+
crate::note_version::NoteVersionSource::AiRefine,
100+
&meta,
101+
)
102+
.map_err(|e| e.to_string())?;
103+
// ③ 成本落库(token 估算与预估同口径——校准单价表数据源)
104+
state
105+
.db
106+
.record_ai_usage(
107+
note.id,
108+
&crate::db_ai_usage::AiUsageInput {
109+
op_type: "refine",
110+
tokens_in: result.base_markdown.chars().count(),
111+
tokens_out: result.refined_markdown.chars().count(),
112+
cost_yuan: cost,
113+
model: result.model.clone(),
114+
slices: result.slices,
115+
},
116+
)
117+
.map_err(|e| e.to_string())?;
118+
// F2 任务中心:标记采纳 + 成本回填(task_id 可选——旧前端调用不传则跳过;
119+
// 防重启后从任务中心重复采纳产生重复笔记——采纳守卫已前置于写库前)
120+
if let Some(tid) = task_id {
121+
let _ = state.db.mark_ai_task_adopted(tid);
122+
let _ = state.db.update_ai_task_cost(tid, cost);
123+
}
124+
let note_row = state
125+
.db
126+
.get_note(note.id)
127+
.map_err(|e| e.to_string())?
128+
.ok_or_else(|| "笔记不存在".to_string())?;
129+
// REQ-278:采纳落库 = 笔记内容变更 → 广播 notes 域(任务中心/会话页采纳后笔记页即时刷新)
130+
crate::notify::emit_changed(&state.app, crate::notify::DataDomain::Notes);
131+
Ok(note_row)
132+
}

‎app/src-tauri/src/commands_ai_refine/mod.rs‎

Lines changed: 2 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
1414
//! @ai-context: 目录模块(批 0-C3 Task 4 拆分,2026-09-11,原 751 行 → 外壳 ≤300)。
1515
//! 子模块职责(D6,按域划分,逐条列出):
16+
//! · apply.rs —— 采纳落库 + 任务历史
1617
//! · dto.rs —— IPC 契约类型(4 个 serde 结构体;字段序 = JSON 键序)
1718
//! · gate.rs —— 成本硬拦截(免费档/本地 Provider 跳过;查询失败宽容放行)
1819
//! · registry.rs —— 任务注册表 + id 序列 + 容量守卫 + 状态写入口
@@ -23,6 +24,7 @@
2324
//! #[tauri::command] 定义随域下沉后,注册路径同步改成
2425
//! crate::commands_ai_refine::<子模块>::<cmd>(IPC 名 = 路径末段,逐字不变)。
2526
27+
pub(crate) mod apply;
2628
mod dto;
2729
mod gate;
2830
mod registry;
@@ -33,123 +35,3 @@ pub use dto::{AiRefineResult, AiTaskHandle, RefineEstimateView, RefineStrategyIn
3335
pub use registry::{claim_task_id, task_registry, task_seq, task_seq_lower_bound, AiTaskEntry};
3436
pub(crate) use gate::ensure_balance_for;
3537
pub(crate) use registry::{set_task, trim_tasks};
36-
37-
use tauri::State;
38-
39-
use crate::commands::AppState;
40-
use crate::types::{NewNote, Note};
41-
42-
/// 任务历史(F2 任务中心:前端面板数据源——按类型列最近任务)。
43-
#[tauri::command]
44-
pub fn ai_task_history(
45-
state: State<'_, AppState>,
46-
op_type: String,
47-
limit: Option<usize>,
48-
) -> Result<Vec<crate::db_ai_tasks::AiTaskRecord>, String> {
49-
if op_type != "refine" && op_type != "enrich" {
50-
return Err("无效的任务类型(refine|enrich)".to_string());
51-
}
52-
state
53-
.db
54-
.list_ai_tasks(&op_type, limit.unwrap_or(50).min(200))
55-
.map_err(|e| e.to_string())
56-
}
57-
58-
/// 采纳落库(REQ-141:diff 预览后用户采纳;v0.8.0 M4 版本化写路径——
59-
/// ① 以规则基线建笔记(首快照)→ ② 精修版 = 新版本(ai-refine,含成本
60-
/// meta)→ ③ 成本落库 note_ai_usage)。
61-
/// @ai-context: F2(2026-08-21):task_id 可选——传入时标记任务已采纳
62-
/// (防重启后从任务中心重复采纳产生重复笔记)。
63-
#[tauri::command]
64-
pub fn ai_refine_apply(
65-
state: State<'_, AppState>,
66-
session_id: i64,
67-
result: AiRefineResult,
68-
task_id: Option<u64>,
69-
) -> Result<Note, String> {
70-
if session_id <= 0 {
71-
return Err("无效的会话 id".to_string());
72-
}
73-
let session = state
74-
.db
75-
.get_session(session_id)
76-
.map_err(|e| e.to_string())?
77-
.ok_or_else(|| format!("会话不存在: {}", session_id))?;
78-
let fallback = format!("{}(AI 精修)", session.title);
79-
let title = crate::commands::normalize_title(result.title.clone(), &fallback);
80-
// 审查修复(2026-09-04):采纳守卫**前置**于任何写库——F2 注释自称"服务端
81-
// 前置校验"但实为后置(先建基线笔记/写版本/记 usage 再拒,重复采纳会留
82-
// 孤儿半态且 Err 早退不播 Notes);双点/陈旧面板/多入口并发兜底依赖此处。
83-
if let Some(tid) = task_id {
84-
if state.db.is_ai_task_adopted(tid) {
85-
return Err("该任务结果已采纳落库——请勿重复采纳(可到笔记页查看)".to_string());
86-
}
87-
}
88-
// ① 规则基线建笔记(版本链首快照原料——可回溯精修前内容)
89-
let new = NewNote {
90-
title: title.clone(),
91-
content: result.base_markdown.clone(),
92-
source: "classroom".to_string(),
93-
session_id: Some(session_id),
94-
rule_version: Some("rule".to_string()),
95-
purify_stats: None,
96-
tags: None,
97-
properties: None,
98-
// v0.11.0:精修基线笔记继承会话组归属(组化接线在会话转笔记链路写入,
99-
// 精修新建基线同样归组——同会话同组,防组内资产漏登)
100-
group_id: crate::note_group_assign::group_of_session(&state.db, session_id).ok().flatten(),
101-
};
102-
let note = state.db.create_note(&new).map_err(|e| e.to_string())?;
103-
// ② 精修版落库(新版本 ai-refine + 成本 meta)
104-
// 审查修复(2026-08-21):落库成本用模型感知单价(与预估同口径——
105-
// 付费模型预估 ¥X 不再记 ¥0)
106-
let cost = crate::ai_cost::usage_cost_for_model(
107-
result.base_markdown.chars().count(),
108-
result.refined_markdown.chars().count(),
109-
&result.model,
110-
);
111-
let meta = crate::note_version::VersionMeta {
112-
cost_yuan: Some(cost),
113-
model: Some(result.model.clone()),
114-
slices: Some(result.slices),
115-
merged_from: None,
116-
};
117-
state
118-
.db
119-
.versioned_save(
120-
note.id,
121-
&result.refined_markdown,
122-
crate::note_version::NoteVersionSource::AiRefine,
123-
&meta,
124-
)
125-
.map_err(|e| e.to_string())?;
126-
// ③ 成本落库(token 估算与预估同口径——校准单价表数据源)
127-
state
128-
.db
129-
.record_ai_usage(
130-
note.id,
131-
&crate::db_ai_usage::AiUsageInput {
132-
op_type: "refine",
133-
tokens_in: result.base_markdown.chars().count(),
134-
tokens_out: result.refined_markdown.chars().count(),
135-
cost_yuan: cost,
136-
model: result.model.clone(),
137-
slices: result.slices,
138-
},
139-
)
140-
.map_err(|e| e.to_string())?;
141-
// F2 任务中心:标记采纳 + 成本回填(task_id 可选——旧前端调用不传则跳过;
142-
// 防重启后从任务中心重复采纳产生重复笔记——采纳守卫已前置于写库前)
143-
if let Some(tid) = task_id {
144-
let _ = state.db.mark_ai_task_adopted(tid);
145-
let _ = state.db.update_ai_task_cost(tid, cost);
146-
}
147-
let note_row = state
148-
.db
149-
.get_note(note.id)
150-
.map_err(|e| e.to_string())?
151-
.ok_or_else(|| "笔记不存在".to_string())?;
152-
// REQ-278:采纳落库 = 笔记内容变更 → 广播 notes 域(任务中心/会话页采纳后笔记页即时刷新)
153-
crate::notify::emit_changed(&state.app, crate::notify::DataDomain::Notes);
154-
Ok(note_row)
155-
}

0 commit comments

Comments
 (0)