use crate::command::{CommandExecutor, EditorCommand}; use crate::error::{CoreError, CoreResult}; use crate::markdown::{import_markdown, import_plain_text}; use crate::model::{BlockType, DocumentBlock, DocumentModel}; use crate::projection::VisibilitySnapshot; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum EditorInputKind { PlainText, Markdown, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum EditorAiScenario { MeetingNotesToTodos, LongParagraphToTitle, PageReorder, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct EditorPipelineRequest { pub kind: EditorInputKind, pub scenario: EditorAiScenario, pub input: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct BlockSnapshot { pub block_id: String, pub block_type: String, pub text: String, pub parent_id: Option, pub indent: u16, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct CommandAuditRecord { pub command: String, pub target_block_id: Option, pub before: Option, pub after: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct EditorChangeReport { pub created_blocks: Vec, pub updated_blocks: Vec, pub moved_blocks: Vec, pub removed_blocks: Vec, pub notes: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct EditorPipelineResult { pub ok: bool, pub scenario: EditorAiScenario, pub input_kind: EditorInputKind, pub document: DocumentModel, pub audit: Vec, pub change_report: EditorChangeReport, pub visible_block_ids: Vec, pub outline_titles: Vec, pub markdown: String, } pub fn apply_ai_pipeline(request: EditorPipelineRequest) -> CoreResult { let mut document = match request.kind { EditorInputKind::PlainText => import_plain_text(&request.input)?, EditorInputKind::Markdown => import_markdown(&request.input)?, }; if document.blocks().is_empty() { document.push(DocumentBlock::new("block_1", BlockType::Paragraph).with_text("")); } let mut audit = Vec::new(); let mut change_report = EditorChangeReport { created_blocks: Vec::new(), updated_blocks: Vec::new(), moved_blocks: Vec::new(), removed_blocks: Vec::new(), notes: Vec::new(), }; match request.scenario { EditorAiScenario::MeetingNotesToTodos => { apply_meeting_notes_to_todos(&mut document, &mut audit, &mut change_report)? } EditorAiScenario::LongParagraphToTitle => { apply_long_paragraph_to_title(&mut document, &mut audit, &mut change_report)? } EditorAiScenario::PageReorder => { apply_page_reorder(&mut document, &mut audit, &mut change_report)? } } let projection = VisibilitySnapshot::derive(&document); let markdown = crate::markdown::export_markdown(&document); let outline_titles = projection .outline .iter() .map(|entry| entry.title.clone()) .collect(); Ok(EditorPipelineResult { ok: true, scenario: request.scenario, input_kind: request.kind, document, audit, change_report, visible_block_ids: projection.visible_block_ids, outline_titles, markdown, }) } fn apply_meeting_notes_to_todos( document: &mut DocumentModel, audit: &mut Vec, report: &mut EditorChangeReport, ) -> CoreResult<()> { for index in 0..document.blocks().len() { let is_note = document.blocks()[index].block_type == BlockType::Paragraph && document.blocks()[index].content.text.contains('待') && document.blocks()[index].content.text.contains('办'); if is_note { let block_id = document.blocks()[index].id.clone(); let before = snapshot_block(document.blocks()[index].clone()); CommandExecutor::apply( document, EditorCommand::SetBlockType { block_id: block_id.clone(), block_type: BlockType::Todo, }, )?; let block = document .block(&block_id) .ok_or_else(|| CoreError::BlockNotFound(block_id.clone()))?; let after = snapshot_block(block.clone()); audit.push(CommandAuditRecord { command: "set_block_type".into(), target_block_id: Some(block_id.clone()), before: Some(before), after: Some(after), }); report.updated_blocks.push(block_id); } } report.notes.push("已将疑似会议待办段落转为 todo".into()); Ok(()) } fn apply_long_paragraph_to_title( document: &mut DocumentModel, audit: &mut Vec, report: &mut EditorChangeReport, ) -> CoreResult<()> { let Some(first_id) = document.blocks().first().map(|block| block.id.clone()) else { return Ok(()); }; let before = snapshot_block( document .block(&first_id) .cloned() .ok_or_else(|| CoreError::BlockNotFound(first_id.clone()))?, ); let title = document .block(&first_id) .map(|block| { block .content .text .split_whitespace() .take(8) .collect::>() .join(" ") }) .unwrap_or_default(); CommandExecutor::apply( document, EditorCommand::SetBlockType { block_id: first_id.clone(), block_type: BlockType::Heading, }, )?; if let Some(block) = document.block_mut(&first_id) { block.heading_level = Some(1); block.content.text = if title.is_empty() { "提炼标题".into() } else { title }; } let after = snapshot_block( document .block(&first_id) .cloned() .ok_or_else(|| CoreError::BlockNotFound(first_id.clone()))?, ); audit.push(CommandAuditRecord { command: "set_block_type".into(), target_block_id: Some(first_id.clone()), before: Some(before), after: Some(after), }); report.updated_blocks.push(first_id); report.notes.push("已将长段落提炼为标题".into()); Ok(()) } fn apply_page_reorder( document: &mut DocumentModel, audit: &mut Vec, report: &mut EditorChangeReport, ) -> CoreResult<()> { if document.blocks().len() < 2 { report.notes.push("块数不足,跳过重排".into()); return Ok(()); } let first = document.blocks()[0].id.clone(); let second = document.blocks()[1].id.clone(); let before_first = snapshot_block( document .block(&first) .cloned() .ok_or_else(|| CoreError::BlockNotFound(first.clone()))?, ); CommandExecutor::apply( document, EditorCommand::MoveBlock { block_id: first.clone(), after_block_id: Some(second.clone()), }, )?; let after_first = snapshot_block( document .block(&first) .cloned() .ok_or_else(|| CoreError::BlockNotFound(first.clone()))?, ); audit.push(CommandAuditRecord { command: "move_block".into(), target_block_id: Some(first.clone()), before: Some(before_first), after: Some(after_first), }); report.moved_blocks.push(first); report.notes.push("已执行页面重排".into()); Ok(()) } fn snapshot_block(block: DocumentBlock) -> BlockSnapshot { BlockSnapshot { block_id: block.id, block_type: block.block_type.as_editor_label().into(), text: block.content.text, parent_id: block.parent_id, indent: block.indent, } }