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
- 新增 66 个 @reserved 标注 (已有 22 个) - 覆盖: agent/butler/classroom/hand/mcp/pipeline/skill/trigger/viking/zclaw 等模块 - MCP 命令增加 @connected 注释说明前端接入路径 - @reserved 总数: 89 (含 identity_init)
302 lines
10 KiB
Rust
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())
|
|
}
|