Skip to content

Commit fabadde

Browse files
committed
feat(tasks): REQ-296/297 SOP 后端(模板引用/run 轨迹/修订聚合)
1 parent 8533ffe commit fabadde

5 files changed

Lines changed: 772 additions & 0 deletions

File tree

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

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
//! SOP 功能区命令面(v0.20.3 / REQ-296/297)。
2+
//!
3+
//! @ai-context: 只做校验与编排(AGENTS.md §6):模板=行范围引用(无双写——
4+
//! 编辑段落即编辑模板);run 启动校验行范围/步数,证据路径白名单
5+
//! `notes-images/`(G3:图片三入口落盘目录);结束写完成史
6+
//! (sop_run——完成即证据);修订建议 v1 纯本地聚合(步骤 N
7+
//! 失败 M 次提示——AI 拟稿后置,AiSettings 闸门默认关,G4)。
8+
9+
use tauri::State;
10+
11+
use crate::commands::AppState;
12+
use crate::db_sop::{
13+
lines_to_steps, EVIDENCE_PREFIX, MAX_SOP_STEPS, MODE_CONFIRM, MODE_READDO, RUN_ABORTED,
14+
RUN_DONE, SopRunDetail, SopTemplate,
15+
};
16+
17+
/// 建模板(编辑器工具栏「生成 SOP」入口:当前选中段落行范围)。
18+
#[tauri::command]
19+
pub fn sop_template_create(
20+
state: State<'_, AppState>,
21+
note_id: i64,
22+
name: String,
23+
start_line: i64,
24+
end_line: i64,
25+
mode: Option<String>,
26+
) -> Result<i64, String> {
27+
let name = name.trim();
28+
if name.is_empty() {
29+
return Err("模板名不能为空".to_string());
30+
}
31+
if name.chars().count() > 60 {
32+
return Err("模板名过长(≤60 字符)".to_string());
33+
}
34+
if start_line < 0 || end_line < start_line {
35+
return Err("行范围非法".to_string());
36+
}
37+
let note = state
38+
.db
39+
.get_note(note_id)
40+
.map_err(|e| e.to_string())?
41+
.ok_or_else(|| "笔记不存在".to_string())?;
42+
let steps = lines_to_steps(&note.content, start_line, end_line);
43+
if steps.is_empty() {
44+
return Err("所选段落无内容(无法生成空 SOP)".to_string());
45+
}
46+
if steps.len() > MAX_SOP_STEPS {
47+
return Err(format!("所选段落超过 {} 步上限——请缩小范围", MAX_SOP_STEPS));
48+
}
49+
let mode = mode.unwrap_or_else(|| MODE_READDO.to_string());
50+
state
51+
.db
52+
.create_sop_template(note_id, name, start_line, end_line, &mode)
53+
.map_err(|e| e.to_string())
54+
}
55+
56+
/// 模板列表(None=全部;行动中心「SOP 库」页签数据源)。
57+
#[tauri::command]
58+
pub fn sop_template_list(
59+
state: State<'_, AppState>,
60+
note_id: Option<i64>,
61+
) -> Result<Vec<SopTemplate>, String> {
62+
state.db.list_sop_templates(note_id).map_err(|e| e.to_string())
63+
}
64+
65+
/// 删除模板(只删引用不动正文)。
66+
#[tauri::command]
67+
pub fn sop_template_delete(state: State<'_, AppState>, template_id: i64) -> Result<(), String> {
68+
let ok = state
69+
.db
70+
.delete_sop_template(template_id)
71+
.map_err(|e| e.to_string())?;
72+
if !ok {
73+
return Err("模板不存在".to_string());
74+
}
75+
Ok(())
76+
}
77+
78+
/// 启动 run(快照当前行范围步骤;返回 run id)。
79+
#[tauri::command]
80+
pub fn sop_run_start(state: State<'_, AppState>, template_id: i64) -> Result<i64, String> {
81+
let tmpl = state
82+
.db
83+
.get_sop_template(template_id)
84+
.map_err(|e| e.to_string())?
85+
.ok_or_else(|| "模板不存在".to_string())?;
86+
// 有进行中的 run 拒绝并发执行(同一模板同时只跑一个——防双开重复证据)
87+
let running = state
88+
.db
89+
.list_sop_runs(template_id, 50)
90+
.map_err(|e| e.to_string())?
91+
.iter()
92+
.any(|r| r.status == "active");
93+
if running {
94+
return Err("该模板已有进行中的执行——请先结算或中止".to_string());
95+
}
96+
state.db.start_sop_run(&tmpl).map_err(|e| e.to_string())
97+
}
98+
99+
/// run 详情(执行面数据源:轨迹/步骤/保鲜 diff)。
100+
#[tauri::command]
101+
pub fn sop_run_detail(state: State<'_, AppState>, run_id: i64) -> Result<SopRunDetail, String> {
102+
state
103+
.db
104+
.sop_run_detail(run_id)
105+
.map_err(|e| e.to_string())?
106+
.ok_or_else(|| "run 不存在".to_string())
107+
}
108+
109+
/// 步更新(done|skipped|failed;证据路径白名单 notes-images/;failed 可留因)。
110+
#[tauri::command]
111+
pub fn sop_step_update(
112+
state: State<'_, AppState>,
113+
run_id: i64,
114+
step_no: i64,
115+
status: String,
116+
evidence_path: Option<String>,
117+
failure_note: Option<String>,
118+
) -> Result<(), String> {
119+
if !matches!(status.as_str(), "done" | "skipped" | "failed") {
120+
return Err("非法步状态(done/skipped/failed)".to_string());
121+
}
122+
if let Some(ev) = &evidence_path {
123+
if !ev.starts_with(EVIDENCE_PREFIX) {
124+
return Err(format!("证据路径须在 {} 下", EVIDENCE_PREFIX));
125+
}
126+
if ev.len() > 260 {
127+
return Err("证据路径过长".to_string());
128+
}
129+
}
130+
if let Some(n) = &failure_note {
131+
if n.chars().count() > 500 {
132+
return Err("失败原因过长(≤500 字)".to_string());
133+
}
134+
}
135+
if let Some(run) = state.db.get_sop_run(run_id).map_err(|e| e.to_string())? {
136+
if run.status != "active" {
137+
return Err("run 已结束,不能再更新步骤".to_string());
138+
}
139+
}
140+
let ok = state
141+
.db
142+
.update_sop_step(run_id, step_no, &status, evidence_path.as_deref(), failure_note.as_deref())
143+
.map_err(|e| e.to_string())?;
144+
if !ok {
145+
return Err("步骤不存在(run 已结束或步号越界)".to_string());
146+
}
147+
Ok(())
148+
}
149+
150+
/// 结算 run(done=全流程完成;aborted=中止;写完成史 + 保鲜 diff 回传)。
151+
#[tauri::command]
152+
pub fn sop_run_finish(
153+
state: State<'_, AppState>,
154+
run_id: i64,
155+
status: String,
156+
) -> Result<SopRunDetail, String> {
157+
let status = if status == RUN_ABORTED { RUN_ABORTED } else { RUN_DONE };
158+
state.db.finish_sop_run(run_id, status).map_err(|e| e.to_string())
159+
}
160+
161+
/// run 轨迹回看(模板维度)。
162+
#[tauri::command]
163+
pub fn sop_run_list(
164+
state: State<'_, AppState>,
165+
template_id: i64,
166+
limit: Option<usize>,
167+
) -> Result<Vec<crate::db_sop::SopRun>, String> {
168+
state
169+
.db
170+
.list_sop_runs(template_id, limit.unwrap_or(20))
171+
.map_err(|e| e.to_string())
172+
}
173+
174+
/// 修订建议(v1 纯本地:失败步聚合——"步骤 N 累计失败 M 次";AI 拟稿后置)。
175+
#[tauri::command]
176+
pub fn sop_revision_suggestions(
177+
state: State<'_, AppState>,
178+
template_id: i64,
179+
) -> Result<Vec<String>, String> {
180+
let agg = state
181+
.db
182+
.sop_failure_aggregate(template_id)
183+
.map_err(|e| e.to_string())?;
184+
Ok(agg
185+
.into_iter()
186+
.map(|(no, text, count)| {
187+
format!("步骤 {}({})累计失败 {} 次——建议复核该步做法或预期结果", no, truncate(&text, 20), count)
188+
})
189+
.collect())
190+
}
191+
192+
fn truncate(s: &str, n: usize) -> String {
193+
let t: String = s.chars().take(n).collect();
194+
if s.chars().count() > n { format!("{}…", t) } else { t }
195+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,8 @@ CREATE TABLE IF NOT EXISTS contracts (
507507
crate::db_task_index::init(conn)?;
508508
// v0.20.3(REQ-298):完成史统一事件表(周回顾原料/成长轨迹唯一数据源)
509509
crate::db_completion::init(conn)?;
510+
// v0.20.3(REQ-296/297):SOP 功能区三表(模板行范围引用/run/步骤轨迹)
511+
crate::db_sop::init(conn)?;
510512
Ok(())
511513
}
512514

0 commit comments

Comments
 (0)