feat: align browser callback runtime and export flows
Consolidate the browser task runtime around the callback path, add safer artifact opening for Zhihu exports, and cover the new service/browser flows with focused tests and supporting docs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ use serde_json::{json, Value};
|
||||
use zeroclaw::tools::Tool;
|
||||
|
||||
use crate::browser::{BrowserBackend, PipeBrowserBackend};
|
||||
use crate::compat::artifact_open::{open_exported_xlsx, open_local_dashboard, PostExportOpen};
|
||||
use crate::compat::openxml_office_tool::OpenXmlOfficeTool;
|
||||
use crate::compat::runtime::CompatTaskContext;
|
||||
use crate::compat::screen_html_export_tool::ScreenHtmlExportTool;
|
||||
@@ -147,7 +148,9 @@ pub fn execute_route_with_browser_backend(
|
||||
}
|
||||
match route {
|
||||
WorkflowRoute::ZhihuHotlistExportXlsx => export_xlsx(transport, workspace_root, &items),
|
||||
WorkflowRoute::ZhihuHotlistScreen => export_screen(transport, workspace_root, &items),
|
||||
WorkflowRoute::ZhihuHotlistScreen => {
|
||||
export_screen(transport, browser_backend.as_ref(), workspace_root, &items)
|
||||
}
|
||||
_ => unreachable!("handled by outer match"),
|
||||
}
|
||||
}
|
||||
@@ -297,21 +300,10 @@ fn probe_hotlist_extractor(
|
||||
ZHIHU_DOMAIN,
|
||||
)?;
|
||||
if !response.success {
|
||||
eprintln!("probe_hotlist_extractor: eval not successful data={}", response.data);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let eval_text = response.data.get("text").unwrap_or(&response.data);
|
||||
let eval_preview: String = eval_text
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.chars()
|
||||
.take(300)
|
||||
.collect();
|
||||
eprintln!(
|
||||
"probe_hotlist_extractor: eval_len={} preview={eval_preview:?}",
|
||||
eval_text.as_str().unwrap_or_default().len()
|
||||
);
|
||||
|
||||
match parse_hotlist_items_payload(eval_text) {
|
||||
Ok(items) if !items.is_empty() => Ok(Some(items)),
|
||||
@@ -366,11 +358,6 @@ fn poll_for_hotlist_readiness(browser_tool: &dyn BrowserBackend) -> Result<bool,
|
||||
};
|
||||
if response.success {
|
||||
let payload = response.data.get("text").unwrap_or(&response.data);
|
||||
let preview: String = payload.as_str().unwrap_or_default().chars().take(200).collect();
|
||||
eprintln!(
|
||||
"poll_hotlist_readiness[{attempt}]: text_len={} preview={preview:?}",
|
||||
payload.as_str().unwrap_or_default().len()
|
||||
);
|
||||
if hotlist_text_looks_ready(payload, &ready_pattern) {
|
||||
return Ok(true);
|
||||
}
|
||||
@@ -424,11 +411,17 @@ fn export_xlsx(
|
||||
let output_path = payload["output_path"].as_str().ok_or_else(|| {
|
||||
PipeError::Protocol("openxml_office did not return output_path".to_string())
|
||||
})?;
|
||||
Ok(format!("已导出知乎热榜 Excel {output_path}"))
|
||||
Ok(match open_exported_xlsx(Path::new(output_path)) {
|
||||
PostExportOpen::Opened => format!("已导出并打开知乎热榜 Excel {output_path}"),
|
||||
PostExportOpen::Failed(reason) => {
|
||||
format!("已导出知乎热榜 Excel {output_path},但自动打开失败:{reason}")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn export_screen(
|
||||
transport: &dyn crate::agent::AgentEventSink,
|
||||
browser_backend: &dyn BrowserBackend,
|
||||
workspace_root: &Path,
|
||||
items: &[HotlistItem],
|
||||
) -> Result<String, PipeError> {
|
||||
@@ -454,12 +447,25 @@ fn export_screen(
|
||||
));
|
||||
}
|
||||
|
||||
let payload: Value = serde_json::from_str(&result.output)
|
||||
finalize_screen_export(browser_backend, &result.output)
|
||||
}
|
||||
|
||||
pub fn finalize_screen_export(
|
||||
browser_backend: &dyn BrowserBackend,
|
||||
output: &str,
|
||||
) -> Result<String, PipeError> {
|
||||
let payload: Value = serde_json::from_str(output)
|
||||
.map_err(|err| PipeError::Protocol(format!("invalid screen_html_export output: {err}")))?;
|
||||
let output_path = payload["output_path"].as_str().ok_or_else(|| {
|
||||
PipeError::Protocol("screen_html_export did not return output_path".to_string())
|
||||
})?;
|
||||
Ok(format!("已生成知乎热榜大屏 {output_path}"))
|
||||
let presentation_url = payload["presentation"]["url"].as_str().unwrap_or_default();
|
||||
Ok(match open_local_dashboard(browser_backend, Path::new(output_path), presentation_url) {
|
||||
PostExportOpen::Opened => format!("已在浏览器中打开知乎热榜大屏 {output_path}"),
|
||||
PostExportOpen::Failed(reason) => {
|
||||
format!("已生成知乎热榜大屏 {output_path},但浏览器自动打开失败:{reason}")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn execute_zhihu_article_route(
|
||||
@@ -823,7 +829,6 @@ fn execute_zhihu_fill_via_live_input(
|
||||
]);
|
||||
|
||||
// ── Step 1: Click title field ──────────────────────────────
|
||||
eprintln!("live_input: step 1 — click title field");
|
||||
browser_tool.invoke(
|
||||
Action::Click,
|
||||
json!({
|
||||
@@ -871,7 +876,6 @@ return JSON.stringify({{status:'ok',chunks:chunks.length}});
|
||||
title_chunk = title_chunk,
|
||||
title_delay = title_delay,
|
||||
);
|
||||
eprintln!("live_input: step 2 — animated title typing ({title_chars} chars, ~{title_wait}ms)");
|
||||
browser_tool.invoke(
|
||||
Action::Eval,
|
||||
json!({ "script": title_script }),
|
||||
@@ -880,7 +884,6 @@ return JSON.stringify({{status:'ok',chunks:chunks.length}});
|
||||
std::thread::sleep(std::time::Duration::from_millis(title_wait));
|
||||
|
||||
// ── Step 3: Click body field ────────────────────────────────
|
||||
eprintln!("live_input: step 3 — click body field");
|
||||
browser_tool.invoke(
|
||||
Action::Click,
|
||||
json!({
|
||||
@@ -930,7 +933,6 @@ return JSON.stringify({{status:'ok',chunks:chunks.length}});
|
||||
body_chunk = body_chunk,
|
||||
body_delay = body_delay,
|
||||
);
|
||||
eprintln!("live_input: step 4 — animated body typing ({body_chars} chars, ~{body_wait}ms)");
|
||||
browser_tool.invoke(
|
||||
Action::Eval,
|
||||
json!({ "script": body_script }),
|
||||
@@ -941,7 +943,6 @@ return JSON.stringify({{status:'ok',chunks:chunks.length}});
|
||||
// Step 5: Fill content only. The publish-button click is split into a
|
||||
// separate eval (step 6) because React needs a full render cycle to
|
||||
// enable the button after the content fill updates the editor state.
|
||||
eprintln!("live_input: step 5 — eval fill_article_draft.js (fill only, publish_mode=false)");
|
||||
let fill_result = execute_browser_skill_script(
|
||||
browser_tool,
|
||||
"zhihu-write",
|
||||
@@ -960,7 +961,6 @@ return JSON.stringify({{status:'ok',chunks:chunks.length}});
|
||||
}
|
||||
|
||||
// Step 6: After React has rendered the enabled publish button, click it.
|
||||
eprintln!("live_input: step 6 — waiting 1.5s for React render, then clicking publish");
|
||||
std::thread::sleep(std::time::Duration::from_millis(1500));
|
||||
|
||||
let publish_script = r#"(function(){
|
||||
|
||||
Reference in New Issue
Block a user