Files
zclaw_openfang/crates/zclaw-pipeline/src/actions/export.rs
iven 0d4fa96b82
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
refactor: 统一项目名称从OpenFang到ZCLAW
重构所有代码和文档中的项目名称,将OpenFang统一更新为ZCLAW。包括:
- 配置文件中的项目名称
- 代码注释和文档引用
- 环境变量和路径
- 类型定义和接口名称
- 测试用例和模拟数据

同时优化部分代码结构,移除未使用的模块,并更新相关依赖项。
2026-03-27 07:36:03 +08:00

181 lines
6.2 KiB
Rust

//! 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 => {
return Err(ActionError::Export(
"PPTX 导出暂不可用。桌面端可通过 Pipeline 结果面板使用 JSON 格式导出后转换。".to_string(),
));
}
ExportFormat::Pdf => {
return Err(ActionError::Export(
"PDF 导出暂不可用。桌面端可通过 Pipeline 结果面板使用 HTML 格式导出后通过浏览器打印为 PDF。".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
}
/// Escape HTML special characters to prevent XSS
fn escape_html(s: &str) -> String {
let mut escaped = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
'<' => escaped.push_str("&lt;"),
'>' => escaped.push_str("&gt;"),
'&' => escaped.push_str("&amp;"),
'"' => escaped.push_str("&quot;"),
'\'' => escaped.push_str("&#39;"),
_ => escaped.push(ch),
}
}
escaped
}
/// 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>", escape_html(title)));
}
if let Some(description) = data.get("description").and_then(|v| v.as_str()) {
html.push_str(&format!("<p>{}</p>", escape_html(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>", escape_html(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>", escape_html(title)));
}
if let Some(content) = scene.get("content").and_then(|v| v.as_str()) {
html.push_str(&format!("<p>{}</p>", escape_html(content)));
}
html.push_str("</div>");
}
}
html.push_str("</body></html>");
html
}