Files
zclaw_openfang/desktop/src-tauri/src/classroom_commands/generate.rs
iven 5121a3c599
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
chore(desktop): Tauri 命令 @reserved 全量标注 — 88个无前端调用命令已标注
- 新增 66 个 @reserved 标注 (已有 22 个)
- 覆盖: agent/butler/classroom/hand/mcp/pipeline/skill/trigger/viking/zclaw 等模块
- MCP 命令增加 @connected 注释说明前端接入路径
- @reserved 总数: 89 (含 identity_init)
2026-04-15 02:05:58 +08:00

302 lines
10 KiB
Rust

//! Classroom generation commands
//!
//! - `classroom_generate` — start 4-stage pipeline, emit progress events
//! - `classroom_generation_progress` — query current progress
//! - `classroom_cancel_generation` — cancel active generation
//! - `classroom_get` — retrieve generated classroom data
//! - `classroom_list` — list all generated classrooms
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Emitter, State};
use zclaw_kernel::generation::{
Classroom, GenerationPipeline, GenerationRequest as KernelGenRequest, GenerationStage,
TeachingStyle, DifficultyLevel,
};
use super::{ClassroomStore, GenerationTasks};
use crate::kernel_commands::KernelState;
// ---------------------------------------------------------------------------
// Request / Response types
// ---------------------------------------------------------------------------
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClassroomGenerateRequest {
pub topic: String,
pub document: Option<String>,
pub style: Option<String>,
pub level: Option<String>,
pub target_duration_minutes: Option<u32>,
pub scene_count: Option<usize>,
pub custom_instructions: Option<String>,
pub language: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClassroomGenerateResponse {
pub classroom_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClassroomProgressResponse {
pub stage: String,
pub progress: u8,
pub activity: String,
pub items_progress: Option<(usize, usize)>,
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
fn parse_style(s: Option<&str>) -> TeachingStyle {
match s.unwrap_or("lecture") {
"discussion" => TeachingStyle::Discussion,
"pbl" => TeachingStyle::Pbl,
"flipped" => TeachingStyle::Flipped,
"socratic" => TeachingStyle::Socratic,
_ => TeachingStyle::Lecture,
}
}
fn parse_level(l: Option<&str>) -> DifficultyLevel {
match l.unwrap_or("intermediate") {
"beginner" => DifficultyLevel::Beginner,
"advanced" => DifficultyLevel::Advanced,
"expert" => DifficultyLevel::Expert,
_ => DifficultyLevel::Intermediate,
}
}
fn stage_name(stage: &GenerationStage) -> &'static str {
match stage {
GenerationStage::AgentProfiles => "agent_profiles",
GenerationStage::Outline => "outline",
GenerationStage::Scene => "scene",
GenerationStage::Complete => "complete",
}
}
// ---------------------------------------------------------------------------
// Commands
// ---------------------------------------------------------------------------
/// Start classroom generation (4-stage pipeline).
/// Progress events are emitted via `classroom:progress`.
/// Supports cancellation between stages by removing the task from GenerationTasks.
// @reserved: classroom generation
// @connected
#[tauri::command]
pub async fn classroom_generate(
app: AppHandle,
store: State<'_, ClassroomStore>,
tasks: State<'_, GenerationTasks>,
kernel_state: State<'_, KernelState>,
persistence: State<'_, crate::classroom_commands::persist::ClassroomPersistence>,
request: ClassroomGenerateRequest,
) -> Result<ClassroomGenerateResponse, String> {
if request.topic.trim().is_empty() {
return Err("Topic is required".to_string());
}
let topic_clone = request.topic.clone();
let kernel_request = KernelGenRequest {
topic: request.topic.clone(),
document: request.document.clone(),
style: parse_style(request.style.as_deref()),
level: parse_level(request.level.as_deref()),
target_duration_minutes: request.target_duration_minutes.unwrap_or(30),
scene_count: request.scene_count,
custom_instructions: request.custom_instructions.clone(),
language: request.language.clone().or_else(|| Some("zh-CN".to_string())),
};
// Register generation task so cancellation can check it
{
use zclaw_kernel::generation::GenerationProgress;
let mut t = tasks.lock().await;
t.insert(topic_clone.clone(), GenerationProgress {
stage: zclaw_kernel::generation::GenerationStage::AgentProfiles,
progress: 0,
activity: "Starting generation...".to_string(),
items_progress: None,
eta_seconds: None,
});
}
// Get LLM driver from kernel if available, otherwise use placeholder mode
let (pipeline, has_driver) = {
let ks = kernel_state.lock().await;
if let Some(kernel) = ks.as_ref() {
(GenerationPipeline::with_driver(kernel.driver(), kernel.config().model().to_string()), true)
} else {
(GenerationPipeline::new(), false)
}
};
// Helper: check if cancelled (try_lock avoids blocking the async runtime)
let is_cancelled = || {
match tasks.try_lock() {
Ok(t) => !t.contains_key(&topic_clone),
Err(_) => false, // Lock contested — treat as not cancelled
}
};
// Helper: emit progress event
let emit_progress = |stage: &str, progress: u8, activity: &str| {
let _ = app.emit("classroom:progress", serde_json::json!({
"topic": &topic_clone,
"stage": stage,
"progress": progress,
"activity": activity
}));
};
// ── Stage 0: Agent Profiles ──
emit_progress("agent_profiles", 5, "生成课堂角色...");
let agents = pipeline.generate_agent_profiles(&kernel_request).await;
emit_progress("agent_profiles", 25, "角色生成完成");
if is_cancelled() {
return Err("Generation cancelled".to_string());
}
// ── Stage 1: Outline ──
emit_progress("outline", 30, "分析主题,生成大纲...");
let outline = pipeline.generate_outline(&kernel_request).await
.map_err(|e| format!("Outline generation failed: {}", e))?;
emit_progress("outline", 50, &format!("大纲完成:{} 个场景", outline.len()));
if is_cancelled() {
return Err("Generation cancelled".to_string());
}
// ── Stage 2: Scenes (parallel) ──
emit_progress("scene", 55, &format!("并行生成 {} 个场景...", outline.len()));
let scenes = pipeline.generate_scenes(&outline).await
.map_err(|e| format!("Scene generation failed: {}", e))?;
if is_cancelled() {
return Err("Generation cancelled".to_string());
}
// ── Stage 3: Assemble ──
emit_progress("complete", 90, "组装课堂...");
// Build classroom directly (pipeline.build_classroom is private)
let total_duration: u32 = scenes.iter().map(|s| s.content.duration_seconds).sum();
let objectives = outline.iter()
.take(3)
.map(|item| format!("理解: {}", item.title))
.collect::<Vec<_>>();
let classroom_id = uuid::Uuid::new_v4().to_string();
let classroom = Classroom {
id: classroom_id.clone(),
title: format!("课堂: {}", kernel_request.topic),
description: format!("{:?} 风格课堂 — {}", kernel_request.style, kernel_request.topic),
topic: kernel_request.topic.clone(),
style: kernel_request.style,
level: kernel_request.level,
total_duration,
objectives,
scenes,
agents,
metadata: zclaw_kernel::generation::ClassroomMetadata {
generated_at: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as i64,
source_document: kernel_request.document.map(|_| "user_document".to_string()),
model: None,
version: "2.0.0".to_string(),
is_placeholder: !has_driver, // P2-10: true when no LLM driver available
custom: serde_json::Map::new(),
},
};
// Store classroom
{
let mut s = store.lock().await;
s.insert(classroom_id.clone(), classroom.clone());
// Persist to SQLite
if let Err(e) = persistence.save_classroom(&classroom).await {
tracing::warn!("[ClassroomGenerate] Failed to persist classroom {}: {}", classroom_id, e);
}
}
// Clear generation task
{
let mut t = tasks.lock().await;
t.remove(&topic_clone);
}
// Emit completion
emit_progress("complete", 100, "课堂生成完成");
Ok(ClassroomGenerateResponse {
classroom_id,
})
}
/// @reserved — no frontend UI yet
/// Get current generation progress for a topic
#[tauri::command]
pub async fn classroom_generation_progress(
tasks: State<'_, GenerationTasks>,
topic: String,
) -> Result<ClassroomProgressResponse, String> {
let t = tasks.lock().await;
let progress = t.get(&topic);
Ok(ClassroomProgressResponse {
stage: progress.map(|p| stage_name(&p.stage).to_string()).unwrap_or_else(|| "none".to_string()),
progress: progress.map(|p| p.progress).unwrap_or(0),
activity: progress.map(|p| p.activity.clone()).unwrap_or_default(),
items_progress: progress.and_then(|p| p.items_progress),
})
}
/// Cancel an active generation
// @connected
#[tauri::command]
pub async fn classroom_cancel_generation(
tasks: State<'_, GenerationTasks>,
topic: String,
) -> Result<(), String> {
let mut t = tasks.lock().await;
t.remove(&topic);
Ok(())
}
/// Retrieve a generated classroom by ID
// @reserved: classroom generation
// @connected
#[tauri::command]
pub async fn classroom_get(
store: State<'_, ClassroomStore>,
classroom_id: String,
) -> Result<Classroom, String> {
let s = store.lock().await;
s.get(&classroom_id)
.cloned()
.ok_or_else(|| format!("Classroom '{}' not found", classroom_id))
}
/// @reserved — no frontend UI yet
/// List all generated classrooms (id + title only)
#[tauri::command]
pub async fn classroom_list(
store: State<'_, ClassroomStore>,
) -> Result<Vec<serde_json::Value>, String> {
let s = store.lock().await;
Ok(s.values().map(|c| serde_json::json!({
"id": c.id,
"title": c.title,
"topic": c.topic,
"totalDuration": c.total_duration,
"sceneCount": c.scenes.len(),
})).collect())
}