diff --git a/apps/notes/src/main.rs b/apps/notes/src/main.rs index e95220c..e5d066b 100644 --- a/apps/notes/src/main.rs +++ b/apps/notes/src/main.rs @@ -427,8 +427,8 @@ async fn process_recording(s: AppState, id: i64) { ); } - // LLM:生成会议纪要 - let summary = match call_llm_summary(&s, &transcript).await { + // LLM:生成会议纪要 + 标题 + let raw = match call_llm_summary(&s, &transcript).await { Ok(t) => t, Err(e) => { tracing::error!(%id, error = %e, "LLM failed"); @@ -436,14 +436,50 @@ async fn process_recording(s: AppState, id: i64) { return; } }; + let (new_title, summary_body) = parse_title_from_summary(&raw); { let conn = s.db.lock().unwrap(); - let _ = conn.execute( - "UPDATE recordings SET summary = ?1, status = 'done', error = NULL WHERE id = ?2", - params![&summary, id], - ); + if let Some(t) = new_title.as_deref() { + let _ = conn.execute( + "UPDATE recordings SET title = ?1, summary = ?2, status = 'done', error = NULL WHERE id = ?3", + params![t, &summary_body, id], + ); + } else { + let _ = conn.execute( + "UPDATE recordings SET summary = ?1, status = 'done', error = NULL WHERE id = ?2", + params![&summary_body, id], + ); + } } - tracing::info!(%id, "done"); + tracing::info!(%id, title = ?new_title, "done"); +} + +/// 从 LLM 输出剥离 `TITLE: ...\n---\n` 头部。 +/// 返回 (Option, summary_body),title 失败时返回 None + 原文。 +fn parse_title_from_summary(raw: &str) -> (Option<String>, String) { + let mut lines = raw.lines(); + let first = lines.next().unwrap_or("").trim(); + let Some(rest) = first + .strip_prefix("TITLE:") + .or_else(|| first.strip_prefix("Title:")) + .or_else(|| first.strip_prefix("标题:")) + .or_else(|| first.strip_prefix("标题:")) + else { + return (None, raw.to_string()); + }; + let title: String = rest.trim().chars().take(80).collect(); + if title.is_empty() { + return (None, raw.to_string()); + } + // 吃掉接下来的 `---` separator + 空行 + let body: String = lines + .skip_while(|l| { + let t = l.trim(); + t.is_empty() || t == "---" || t.starts_with("---") + }) + .collect::<Vec<_>>() + .join("\n"); + (Some(title), body) } fn set_status(s: &AppState, id: i64, status: &str, transcript: Option<&str>, error: Option<&str>) { @@ -501,8 +537,11 @@ async fn call_llm_summary(s: &AppState, transcript: &str) -> Result<String, Stri "model": s.llm_model, "messages": [ { "role": "system", "content": - "你是一个会议纪要助手。根据语音转写整理一份结构化纪要(markdown 格式):\n\ + "你是一个会议纪要助手。根据语音转写输出:\n\ \n\ + 第一行:`TITLE: <8-20 字符的会议主题>`(不含日期/时间,提取核心议题)\n\ + 第二行:`---`\n\ + 之后是 markdown 纪要:\n\ 1. **概要**:1-2 句话总结\n\ 2. **关键讨论点**:bullet 列出\n\ 3. **决定 / 结论**\n\