acceptance: stabilize zhihu hotlist excel flow
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
use async_trait::async_trait;
|
||||
use serde::Deserialize;
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -79,21 +80,35 @@ impl Tool for OpenXmlOfficeTool {
|
||||
.iter()
|
||||
.map(|value| value.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
if parsed.columns != expected_columns {
|
||||
return Ok(failed_tool_result(
|
||||
"unsupported columns: expected [rank, title, heat]".to_string(),
|
||||
));
|
||||
}
|
||||
let column_order = match resolve_column_order(&parsed.columns, &expected_columns) {
|
||||
Some(order) => order,
|
||||
None => {
|
||||
return Ok(failed_tool_result(
|
||||
"unsupported columns: expected [rank, title, heat]".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
if parsed.rows.is_empty() {
|
||||
return Ok(failed_tool_result("rows must not be empty".to_string()));
|
||||
}
|
||||
|
||||
if parsed.rows.iter().any(|row| row.len() != parsed.columns.len()) {
|
||||
return Ok(failed_tool_result(
|
||||
"each row must match the declared columns length".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if parsed.rows.iter().any(|row| row.len() != 3) {
|
||||
return Ok(failed_tool_result(
|
||||
"each row must contain exactly 3 values".to_string(),
|
||||
));
|
||||
}
|
||||
let normalized_rows = parsed
|
||||
.rows
|
||||
.iter()
|
||||
.map(|row| reorder_row(row, &column_order))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let job_root = create_job_root(&self.workspace_root)?;
|
||||
let template_path = job_root.join("zhihu_hotlist_template.xlsx");
|
||||
@@ -105,8 +120,8 @@ impl Tool for OpenXmlOfficeTool {
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| default_output_path(&self.workspace_root));
|
||||
|
||||
write_hotlist_template(&template_path, parsed.rows.len())?;
|
||||
write_payload_json(&payload_path, &parsed.rows)?;
|
||||
write_hotlist_template(&template_path, normalized_rows.len())?;
|
||||
write_payload_json(&payload_path, &normalized_rows)?;
|
||||
write_request_json(&request_path, &template_path, &payload_path, &output_path)?;
|
||||
|
||||
let rendered = run_openxml_cli(&request_path)?;
|
||||
@@ -120,7 +135,7 @@ impl Tool for OpenXmlOfficeTool {
|
||||
output: json!({
|
||||
"sheet_name": DEFAULT_SHEET_NAME,
|
||||
"output_path": artifact_path,
|
||||
"row_count": parsed.rows.len(),
|
||||
"row_count": normalized_rows.len(),
|
||||
"renderer": OPENXML_OFFICE_TOOL_NAME
|
||||
})
|
||||
.to_string(),
|
||||
@@ -156,6 +171,44 @@ fn default_output_path(workspace_root: &Path) -> PathBuf {
|
||||
.join(format!("zhihu-hotlist-{nanos}.xlsx"))
|
||||
}
|
||||
|
||||
fn resolve_column_order(
|
||||
provided_columns: &[String],
|
||||
expected_columns: &[String],
|
||||
) -> Option<Vec<usize>> {
|
||||
if provided_columns.len() != expected_columns.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let provided_set = provided_columns
|
||||
.iter()
|
||||
.map(|value| value.trim().to_string())
|
||||
.collect::<BTreeSet<_>>();
|
||||
let expected_set = expected_columns
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
if provided_set != expected_set {
|
||||
return None;
|
||||
}
|
||||
|
||||
expected_columns
|
||||
.iter()
|
||||
.map(|expected| {
|
||||
provided_columns
|
||||
.iter()
|
||||
.position(|provided| provided.trim() == expected)
|
||||
})
|
||||
.collect::<Option<Vec<_>>>()
|
||||
}
|
||||
|
||||
fn reorder_row(row: &[Value], column_order: &[usize]) -> Vec<Value> {
|
||||
column_order
|
||||
.iter()
|
||||
.map(|index| row[*index].clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn write_payload_json(path: &Path, rows: &[Vec<Value>]) -> anyhow::Result<()> {
|
||||
let mut variables = BTreeMap::new();
|
||||
for (idx, row) in rows.iter().enumerate() {
|
||||
|
||||
Reference in New Issue
Block a user