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:
267
src/compat/artifact_open.rs
Normal file
267
src/compat/artifact_open.rs
Normal file
@@ -0,0 +1,267 @@
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::browser::BrowserBackend;
|
||||
use crate::pipe::{Action, CommandOutput};
|
||||
|
||||
pub const LOCAL_DASHBOARD_EXPECTED_DOMAIN: &str = "__sgclaw_local_dashboard__";
|
||||
pub const LOCAL_DASHBOARD_SOURCE: &str = "compat.workflow_executor";
|
||||
pub const LOCAL_DASHBOARD_KIND_ZHIHU_HOTLIST_SCREEN: &str = "zhihu_hotlist_screen";
|
||||
const DISABLE_POST_EXPORT_OPEN_ENV: &str = "SGCLAW_DISABLE_POST_EXPORT_OPEN";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum PostExportOpen {
|
||||
Opened,
|
||||
Failed(String),
|
||||
}
|
||||
|
||||
pub fn open_exported_xlsx(output_path: &Path) -> PostExportOpen {
|
||||
open_exported_xlsx_with(output_path, launch_with_default_xlsx_app)
|
||||
}
|
||||
|
||||
fn open_exported_xlsx_with<F>(output_path: &Path, opener: F) -> PostExportOpen
|
||||
where
|
||||
F: FnOnce(&Path) -> Result<(), String>,
|
||||
{
|
||||
if !output_path.exists() {
|
||||
return PostExportOpen::Failed(format!(
|
||||
"导出的 Excel 文件不存在:{}",
|
||||
output_path.display()
|
||||
));
|
||||
}
|
||||
|
||||
match opener(output_path) {
|
||||
Ok(()) => PostExportOpen::Opened,
|
||||
Err(reason) => PostExportOpen::Failed(reason),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_local_dashboard(
|
||||
browser_backend: &dyn BrowserBackend,
|
||||
output_path: &Path,
|
||||
presentation_url: &str,
|
||||
) -> PostExportOpen {
|
||||
if !output_path.exists() {
|
||||
return PostExportOpen::Failed(format!(
|
||||
"生成的大屏文件不存在:{}",
|
||||
output_path.display()
|
||||
));
|
||||
}
|
||||
if presentation_url.trim().is_empty() {
|
||||
return PostExportOpen::Failed("screen_html_export did not return presentation.url".to_string());
|
||||
}
|
||||
|
||||
let params = json!({
|
||||
"url": presentation_url,
|
||||
"sgclaw_local_dashboard_open": {
|
||||
"source": LOCAL_DASHBOARD_SOURCE,
|
||||
"kind": LOCAL_DASHBOARD_KIND_ZHIHU_HOTLIST_SCREEN,
|
||||
"output_path": output_path.to_string_lossy(),
|
||||
"presentation_url": presentation_url,
|
||||
}
|
||||
});
|
||||
|
||||
match browser_backend.invoke(Action::Navigate, params, LOCAL_DASHBOARD_EXPECTED_DOMAIN) {
|
||||
Ok(output) if output.success => PostExportOpen::Opened,
|
||||
Ok(output) => PostExportOpen::Failed(command_output_reason(&output)),
|
||||
Err(err) => PostExportOpen::Failed(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn launch_with_default_xlsx_app(output_path: &Path) -> Result<(), String> {
|
||||
if std::env::var_os(DISABLE_POST_EXPORT_OPEN_ENV).is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let output = Command::new("cmd")
|
||||
.args(["/C", "start", "", &output_path.display().to_string()])
|
||||
.output()
|
||||
.map_err(|err| format!("启动 Excel 默认程序失败:{err}"))?;
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||
if stderr.is_empty() {
|
||||
Err(format!(
|
||||
"启动 Excel 默认程序失败:exit status {}",
|
||||
output.status
|
||||
))
|
||||
} else {
|
||||
Err(format!("启动 Excel 默认程序失败:{stderr}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn launch_with_default_xlsx_app(output_path: &Path) -> Result<(), String> {
|
||||
if std::env::var_os(DISABLE_POST_EXPORT_OPEN_ENV).is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let status = Command::new("open")
|
||||
.arg(output_path)
|
||||
.status()
|
||||
.map_err(|err| format!("启动 Excel 默认程序失败:{err}"))?;
|
||||
if status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("启动 Excel 默认程序失败:exit status {status}"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
fn launch_with_default_xlsx_app(output_path: &Path) -> Result<(), String> {
|
||||
if std::env::var_os(DISABLE_POST_EXPORT_OPEN_ENV).is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let status = Command::new("xdg-open")
|
||||
.arg(output_path)
|
||||
.status()
|
||||
.map_err(|err| format!("启动 Excel 默认程序失败:{err}"))?;
|
||||
if status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("启动 Excel 默认程序失败:exit status {status}"))
|
||||
}
|
||||
}
|
||||
|
||||
fn command_output_reason(output: &CommandOutput) -> String {
|
||||
output
|
||||
.data
|
||||
.get("error")
|
||||
.and_then(Value::as_str)
|
||||
.or_else(|| output.data.get("message").and_then(Value::as_str))
|
||||
.map(str::to_string)
|
||||
.unwrap_or_else(|| output.data.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::collections::VecDeque;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use crate::pipe::{ExecutionSurfaceMetadata, PipeError, Timing};
|
||||
|
||||
fn temp_file_path(name: &str) -> PathBuf {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"sgclaw-artifact-open-{}-{}",
|
||||
std::process::id(),
|
||||
uuid::Uuid::new_v4()
|
||||
));
|
||||
std::fs::create_dir_all(&root).expect("temp root should exist");
|
||||
root.join(name)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_exported_xlsx_with_passes_generated_path_to_launcher() {
|
||||
let output_path = temp_file_path("zhihu-hotlist.xlsx");
|
||||
std::fs::write(&output_path, b"xlsx").expect("xlsx fixture should be writable");
|
||||
let seen = Mutex::new(None::<PathBuf>);
|
||||
|
||||
let result = open_exported_xlsx_with(&output_path, |path| {
|
||||
*seen.lock().unwrap() = Some(path.to_path_buf());
|
||||
Ok(())
|
||||
});
|
||||
|
||||
assert!(matches!(result, PostExportOpen::Opened));
|
||||
assert_eq!(seen.lock().unwrap().clone().unwrap(), output_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_exported_xlsx_with_reports_launcher_failure() {
|
||||
let output_path = temp_file_path("zhihu-hotlist.xlsx");
|
||||
std::fs::write(&output_path, b"xlsx").expect("xlsx fixture should be writable");
|
||||
|
||||
let result = open_exported_xlsx_with(&output_path, |_path| Err("launcher failed".to_string()));
|
||||
|
||||
assert!(matches!(result, PostExportOpen::Failed(reason) if reason.contains("launcher failed")));
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct FakeBrowserBackend {
|
||||
responses: Mutex<VecDeque<Result<CommandOutput, PipeError>>>,
|
||||
invocations: Mutex<Vec<(Action, Value, String)>>,
|
||||
}
|
||||
|
||||
impl FakeBrowserBackend {
|
||||
fn new(responses: Vec<Result<CommandOutput, PipeError>>) -> Self {
|
||||
Self {
|
||||
responses: Mutex::new(VecDeque::from(responses)),
|
||||
invocations: Mutex::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BrowserBackend for FakeBrowserBackend {
|
||||
fn invoke(
|
||||
&self,
|
||||
action: Action,
|
||||
params: Value,
|
||||
expected_domain: &str,
|
||||
) -> Result<CommandOutput, PipeError> {
|
||||
self.invocations
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push((action, params, expected_domain.to_string()));
|
||||
self.responses
|
||||
.lock()
|
||||
.unwrap()
|
||||
.pop_front()
|
||||
.unwrap_or_else(|| Err(PipeError::Timeout))
|
||||
}
|
||||
|
||||
fn surface_metadata(&self) -> ExecutionSurfaceMetadata {
|
||||
ExecutionSurfaceMetadata::privileged_browser_pipe("fake_backend")
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_local_dashboard_uses_exact_approved_marker_payload() {
|
||||
let output_path = temp_file_path("zhihu-hotlist-screen.html");
|
||||
std::fs::write(&output_path, "<html></html>").expect("dashboard fixture should be writable");
|
||||
let presentation_url = format!("file:///{}", output_path.display().to_string().replace('\\', "/"));
|
||||
let backend = FakeBrowserBackend::new(vec![Ok(CommandOutput {
|
||||
seq: 1,
|
||||
success: true,
|
||||
data: json!({ "navigated": true }),
|
||||
aom_snapshot: vec![],
|
||||
timing: Timing {
|
||||
queue_ms: 1,
|
||||
exec_ms: 1,
|
||||
},
|
||||
})]);
|
||||
|
||||
let result = open_local_dashboard(&backend, &output_path, &presentation_url);
|
||||
let invocations = backend.invocations.lock().unwrap().clone();
|
||||
|
||||
assert!(matches!(result, PostExportOpen::Opened));
|
||||
assert_eq!(invocations.len(), 1);
|
||||
assert_eq!(invocations[0].0, Action::Navigate);
|
||||
assert_eq!(invocations[0].2, LOCAL_DASHBOARD_EXPECTED_DOMAIN.to_string());
|
||||
assert_eq!(invocations[0].1["url"], json!(presentation_url));
|
||||
assert_eq!(
|
||||
invocations[0].1["sgclaw_local_dashboard_open"]["source"],
|
||||
json!(LOCAL_DASHBOARD_SOURCE)
|
||||
);
|
||||
assert_eq!(
|
||||
invocations[0].1["sgclaw_local_dashboard_open"]["kind"],
|
||||
json!(LOCAL_DASHBOARD_KIND_ZHIHU_HOTLIST_SCREEN)
|
||||
);
|
||||
assert_eq!(
|
||||
invocations[0].1["sgclaw_local_dashboard_open"]["output_path"],
|
||||
json!(output_path.to_string_lossy().to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
invocations[0].1["sgclaw_local_dashboard_open"]["presentation_url"],
|
||||
invocations[0].1["url"]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod artifact_open;
|
||||
pub mod browser_script_skill_tool;
|
||||
pub mod browser_tool_adapter;
|
||||
pub mod config_adapter;
|
||||
|
||||
@@ -151,22 +151,6 @@ pub async fn execute_task_with_provider(
|
||||
) -> Result<String, PipeError> {
|
||||
let engine = RuntimeEngine::new(settings.runtime_profile);
|
||||
let browser_surface_present = engine.browser_surface_enabled();
|
||||
if let Some(preview) = crate::agent::planner::build_execution_preview(
|
||||
settings.planner_mode,
|
||||
instruction,
|
||||
task_context.page_url.as_deref(),
|
||||
task_context.page_title.as_deref(),
|
||||
) {
|
||||
let mut message = preview.summary;
|
||||
if !preview.steps.is_empty() {
|
||||
message.push('\n');
|
||||
message.push_str(&preview.steps.join("\n"));
|
||||
}
|
||||
transport.send(&crate::pipe::AgentMessage::LogEntry {
|
||||
level: "plan".to_string(),
|
||||
message,
|
||||
})?;
|
||||
}
|
||||
let loaded_skills = engine.loaded_skills(&config, &skills_dir);
|
||||
let loaded_skill_versions = loaded_skills
|
||||
.iter()
|
||||
@@ -376,3 +360,22 @@ fn to_chat_message(message: &ConversationMessage) -> Option<ChatMessage> {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn compat_runtime_source_no_longer_references_legacy_planner_preview() {
|
||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let source = fs::read_to_string(manifest_dir.join("src/compat/runtime.rs")).unwrap();
|
||||
let preview_prefix = ["if let Some(preview) = crate::agent::", "planner::build_execution_preview("].concat();
|
||||
let plan_level_expr = ["level: ", "\"plan\".to_string(),"].concat();
|
||||
|
||||
assert!(!source
|
||||
.lines()
|
||||
.any(|line| line.trim_start().starts_with(&preview_prefix)));
|
||||
assert!(!source.lines().any(|line| line.trim() == plan_level_expr));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,11 +150,7 @@ impl Tool for ScreenHtmlExportTool {
|
||||
};
|
||||
|
||||
let rendered = render_template(&payload)?;
|
||||
let output_path = parsed
|
||||
.output_path
|
||||
.as_deref()
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| default_output_path(&self.workspace_root));
|
||||
let output_path = resolve_output_path(&self.workspace_root, parsed.output_path.as_deref());
|
||||
write_output_html(&output_path, &rendered)?;
|
||||
|
||||
let presentation_url = file_url_for_path(&output_path);
|
||||
@@ -375,6 +371,21 @@ fn default_output_path(workspace_root: &Path) -> PathBuf {
|
||||
.join(format!("zhihu-hotlist-screen-{nanos}.html"))
|
||||
}
|
||||
|
||||
fn resolve_output_path(workspace_root: &Path, output_path: Option<&str>) -> PathBuf {
|
||||
output_path
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(PathBuf::from)
|
||||
.map(|path| {
|
||||
if path.is_absolute() {
|
||||
path
|
||||
} else {
|
||||
workspace_root.join(path)
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| default_output_path(workspace_root))
|
||||
}
|
||||
|
||||
fn default_snapshot_id() -> String {
|
||||
format!("zhihu-hotlist-screen-{}", now_ms())
|
||||
}
|
||||
@@ -391,3 +402,67 @@ fn file_url_for_path(path: &Path) -> String {
|
||||
.map(|url| url.to_string())
|
||||
.unwrap_or_else(|_| format!("file://{}", path.display()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::pipe::Action;
|
||||
use crate::security::MacPolicy;
|
||||
|
||||
fn temp_workspace_root() -> PathBuf {
|
||||
let root = std::env::temp_dir().join(format!("sgclaw-screen-html-{}", now_ms()));
|
||||
std::fs::create_dir_all(&root).unwrap();
|
||||
root
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn screen_html_export_resolves_relative_output_path_to_absolute_file_url() {
|
||||
let workspace_root = temp_workspace_root();
|
||||
let tool = ScreenHtmlExportTool::new(workspace_root.clone());
|
||||
|
||||
let result = tool
|
||||
.execute(json!({
|
||||
"snapshot_id": "snapshot-relative-path",
|
||||
"generated_at_ms": 1774713600000u64,
|
||||
"rows": [
|
||||
[1, "问题一", "344万"],
|
||||
[2, "问题二", "266万"]
|
||||
],
|
||||
"output_path": "../out/zhihu-hotlist-screen-relative.html"
|
||||
}))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(result.success, "{result:?}");
|
||||
|
||||
let payload: Value = serde_json::from_str(&result.output).unwrap();
|
||||
let output_path = PathBuf::from(payload["output_path"].as_str().unwrap());
|
||||
let presentation_url = payload["presentation"]["url"].as_str().unwrap();
|
||||
let expected_output_path = workspace_root.join("../out/zhihu-hotlist-screen-relative.html");
|
||||
let expected_presentation_url = Url::from_file_path(&expected_output_path)
|
||||
.unwrap()
|
||||
.to_string();
|
||||
let policy = MacPolicy::load_from_path(
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("resources")
|
||||
.join("rules.json"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(output_path.is_absolute());
|
||||
assert_eq!(output_path, expected_output_path);
|
||||
assert!(output_path.exists());
|
||||
assert_eq!(presentation_url, expected_presentation_url);
|
||||
assert!(presentation_url.starts_with("file:///"));
|
||||
policy
|
||||
.validate_local_dashboard_presentation(
|
||||
&Action::Navigate,
|
||||
"__sgclaw_local_dashboard__",
|
||||
presentation_url,
|
||||
output_path.to_string_lossy().as_ref(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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