feat(pipeline): implement Pipeline DSL system for automated workflows
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
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
Add complete Pipeline DSL system including:
- Rust backend (zclaw-pipeline crate) with parser, executor, and state management
- Frontend components: PipelinesPanel, PipelineResultPreview, ClassroomPreviewer
- Pipeline recommender for Agent conversation integration
- 5 pipeline templates: education, marketing, legal, research, productivity
- Documentation for Pipeline DSL architecture
Pipeline DSL enables declarative workflow definitions with:
- YAML-based configuration
- Expression resolution (${inputs.topic}, ${steps.step1.output})
- LLM integration, parallel execution, file export
- Agent smart recommendations in conversations
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
161
crates/zclaw-pipeline/src/actions/export.rs
Normal file
161
crates/zclaw-pipeline/src/actions/export.rs
Normal file
@@ -0,0 +1,161 @@
|
||||
//! File export action
|
||||
|
||||
use std::path::PathBuf;
|
||||
use serde_json::Value;
|
||||
use tokio::fs;
|
||||
|
||||
use crate::types::ExportFormat;
|
||||
use super::ActionError;
|
||||
|
||||
/// Export files in specified formats
|
||||
pub async fn export_files(
|
||||
formats: &[ExportFormat],
|
||||
data: &Value,
|
||||
output_dir: Option<&str>,
|
||||
) -> Result<Value, ActionError> {
|
||||
let dir = output_dir
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| std::env::temp_dir());
|
||||
|
||||
// Ensure directory exists
|
||||
fs::create_dir_all(&dir).await
|
||||
.map_err(|e| ActionError::Export(format!("Failed to create directory: {}", e)))?;
|
||||
|
||||
let mut paths = Vec::new();
|
||||
let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
|
||||
|
||||
for format in formats {
|
||||
let filename = format!("output_{}.{}", timestamp, format.extension());
|
||||
let path = dir.join(&filename);
|
||||
|
||||
match format {
|
||||
ExportFormat::Json => {
|
||||
let content = serde_json::to_string_pretty(data)
|
||||
.map_err(|e| ActionError::Export(format!("JSON serialization error: {}", e)))?;
|
||||
fs::write(&path, content).await
|
||||
.map_err(|e| ActionError::Export(format!("Write error: {}", e)))?;
|
||||
}
|
||||
ExportFormat::Markdown => {
|
||||
let content = render_markdown(data);
|
||||
fs::write(&path, content).await
|
||||
.map_err(|e| ActionError::Export(format!("Write error: {}", e)))?;
|
||||
}
|
||||
ExportFormat::Html => {
|
||||
let content = render_html(data);
|
||||
fs::write(&path, content).await
|
||||
.map_err(|e| ActionError::Export(format!("Write error: {}", e)))?;
|
||||
}
|
||||
ExportFormat::Pptx => {
|
||||
// Will integrate with zclaw-kernel export
|
||||
return Err(ActionError::Export("PPTX export requires kernel integration".to_string()));
|
||||
}
|
||||
ExportFormat::Pdf => {
|
||||
return Err(ActionError::Export("PDF export not yet implemented".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
paths.push(serde_json::json!({
|
||||
"format": format.extension(),
|
||||
"path": path.to_string_lossy(),
|
||||
"filename": filename,
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(Value::Array(paths))
|
||||
}
|
||||
|
||||
/// Render data to markdown
|
||||
fn render_markdown(data: &Value) -> String {
|
||||
let mut md = String::new();
|
||||
|
||||
if let Some(title) = data.get("title").and_then(|v| v.as_str()) {
|
||||
md.push_str(&format!("# {}\n\n", title));
|
||||
}
|
||||
|
||||
if let Some(description) = data.get("description").and_then(|v| v.as_str()) {
|
||||
md.push_str(&format!("{}\n\n", description));
|
||||
}
|
||||
|
||||
if let Some(outline) = data.get("outline") {
|
||||
md.push_str("## 大纲\n\n");
|
||||
if let Some(items) = outline.get("items").and_then(|v| v.as_array()) {
|
||||
for (i, item) in items.iter().enumerate() {
|
||||
if let Some(text) = item.get("title").and_then(|v| v.as_str()) {
|
||||
md.push_str(&format!("{}. {}\n", i + 1, text));
|
||||
}
|
||||
}
|
||||
md.push_str("\n");
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(scenes) = data.get("scenes").and_then(|v| v.as_array()) {
|
||||
md.push_str("## 场景\n\n");
|
||||
for scene in scenes {
|
||||
if let Some(title) = scene.get("title").and_then(|v| v.as_str()) {
|
||||
md.push_str(&format!("### {}\n\n", title));
|
||||
}
|
||||
if let Some(content) = scene.get("content").and_then(|v| v.as_str()) {
|
||||
md.push_str(&format!("{}\n\n", content));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
md
|
||||
}
|
||||
|
||||
/// Render data to HTML
|
||||
fn render_html(data: &Value) -> String {
|
||||
let mut html = String::from(r#"<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Export</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
|
||||
h1 { color: #333; }
|
||||
h2 { color: #555; border-bottom: 1px solid #eee; padding-bottom: 10px; }
|
||||
h3 { color: #666; }
|
||||
.scene { margin: 20px 0; padding: 15px; background: #f9f9f9; border-radius: 8px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
"#);
|
||||
|
||||
if let Some(title) = data.get("title").and_then(|v| v.as_str()) {
|
||||
html.push_str(&format!("<h1>{}</h1>", title));
|
||||
}
|
||||
|
||||
if let Some(description) = data.get("description").and_then(|v| v.as_str()) {
|
||||
html.push_str(&format!("<p>{}</p>", description));
|
||||
}
|
||||
|
||||
if let Some(outline) = data.get("outline") {
|
||||
html.push_str("<h2>大纲</h2><ol>");
|
||||
if let Some(items) = outline.get("items").and_then(|v| v.as_array()) {
|
||||
for item in items {
|
||||
if let Some(text) = item.get("title").and_then(|v| v.as_str()) {
|
||||
html.push_str(&format!("<li>{}</li>", text));
|
||||
}
|
||||
}
|
||||
}
|
||||
html.push_str("</ol>");
|
||||
}
|
||||
|
||||
if let Some(scenes) = data.get("scenes").and_then(|v| v.as_array()) {
|
||||
html.push_str("<h2>场景</h2>");
|
||||
for scene in scenes {
|
||||
html.push_str("<div class=\"scene\">");
|
||||
if let Some(title) = scene.get("title").and_then(|v| v.as_str()) {
|
||||
html.push_str(&format!("<h3>{}</h3>", title));
|
||||
}
|
||||
if let Some(content) = scene.get("content").and_then(|v| v.as_str()) {
|
||||
html.push_str(&format!("<p>{}</p>", content));
|
||||
}
|
||||
html.push_str("</div>");
|
||||
}
|
||||
}
|
||||
|
||||
html.push_str("</body></html>");
|
||||
html
|
||||
}
|
||||
Reference in New Issue
Block a user