fix(安全): 修复HTML导出中的XSS漏洞并清理调试日志
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(日志): 替换console.log为tracing日志系统
style(代码): 移除未使用的代码和依赖项

feat(测试): 添加端到端测试文档和CI工作流
docs(变更日志): 更新CHANGELOG.md记录0.1.0版本变更

perf(构建): 更新依赖版本并优化CI流程
This commit is contained in:
iven
2026-03-26 19:49:03 +08:00
parent b8d565a9eb
commit 978dc5cdd8
79 changed files with 3953 additions and 5724 deletions

View File

@@ -103,6 +103,22 @@ fn render_markdown(data: &Value) -> String {
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>
@@ -123,11 +139,11 @@ fn render_html(data: &Value) -> String {
"#);
if let Some(title) = data.get("title").and_then(|v| v.as_str()) {
html.push_str(&format!("<h1>{}</h1>", title));
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>", description));
html.push_str(&format!("<p>{}</p>", escape_html(description)));
}
if let Some(outline) = data.get("outline") {
@@ -135,7 +151,7 @@ fn render_html(data: &Value) -> String {
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(&format!("<li>{}</li>", escape_html(text)));
}
}
}
@@ -147,10 +163,10 @@ fn render_html(data: &Value) -> String {
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));
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>", content));
html.push_str(&format!("<p>{}</p>", escape_html(content)));
}
html.push_str("</div>");
}

View File

@@ -134,10 +134,10 @@ impl ActionRegistry {
max_tokens: Option<u32>,
json_mode: bool,
) -> Result<Value, ActionError> {
println!("[DEBUG execute_llm] Called with template length: {}", template.len());
println!("[DEBUG execute_llm] Input HashMap contents:");
tracing::debug!(target: "pipeline_actions", "execute_llm: Called with template length: {}", template.len());
tracing::debug!(target: "pipeline_actions", "execute_llm: Input HashMap contents:");
for (k, v) in &input {
println!(" {} => {:?}", k, v);
tracing::debug!(target: "pipeline_actions", " {} => {:?}", k, v);
}
if let Some(driver) = &self.llm_driver {
@@ -148,7 +148,7 @@ impl ActionRegistry {
template.to_string()
};
println!("[DEBUG execute_llm] Calling driver.generate with prompt length: {}", prompt.len());
tracing::debug!(target: "pipeline_actions", "execute_llm: Calling driver.generate with prompt length: {}", prompt.len());
driver.generate(prompt, input, model, temperature, max_tokens, json_mode)
.await
@@ -346,14 +346,14 @@ impl ActionRegistry {
let mut html = String::from("<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>Export</title></head><body>");
if let Some(title) = data.get("title").and_then(|v| v.as_str()) {
html.push_str(&format!("<h1>{}</h1>", title));
html.push_str(&format!("<h1>{}</h1>", escape_html(title)));
}
if let Some(items) = data.get("items").and_then(|v| v.as_array()) {
html.push_str("<ul>");
for item in items {
if let Some(text) = item.as_str() {
html.push_str(&format!("<li>{}</li>", text));
html.push_str(&format!("<li>{}</li>", escape_html(text)));
}
}
html.push_str("</ul>");
@@ -364,6 +364,22 @@ impl ActionRegistry {
}
}
/// 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
}
impl ExportFormat {
fn extension(&self) -> &'static str {
match self {

View File

@@ -185,22 +185,22 @@ impl PipelineExecutor {
async move {
match action {
Action::LlmGenerate { template, input, model, temperature, max_tokens, json_mode } => {
println!("[DEBUG executor] LlmGenerate action called");
println!("[DEBUG executor] Raw input map:");
tracing::debug!(target: "pipeline_executor", "LlmGenerate action called");
tracing::debug!(target: "pipeline_executor", "Raw input map:");
for (k, v) in input {
println!(" {} => {}", k, v);
tracing::debug!(target: "pipeline_executor", " {} => {}", k, v);
}
// First resolve the template itself (handles ${inputs.xxx}, ${item.xxx}, etc.)
let resolved_template = context.resolve(template)?;
let resolved_template_str = resolved_template.as_str().unwrap_or(template).to_string();
println!("[DEBUG executor] Resolved template (first 300 chars): {}",
tracing::debug!(target: "pipeline_executor", "Resolved template (first 300 chars): {}",
&resolved_template_str[..resolved_template_str.len().min(300)]);
let resolved_input = context.resolve_map(input)?;
println!("[DEBUG executor] Resolved input map:");
tracing::debug!(target: "pipeline_executor", "Resolved input map:");
for (k, v) in &resolved_input {
println!(" {} => {:?}", k, v);
tracing::debug!(target: "pipeline_executor", " {} => {:?}", k, v);
}
self.action_registry.execute_llm(
&resolved_template_str,