feat: add websocket browser service runtime
Wire the service/browser runtime onto the websocket-driven execution path and add the new browser/service modules needed for the submit flow and runtime integration. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -7,6 +8,7 @@ use regex::Regex;
|
||||
use serde_json::{json, Value};
|
||||
use zeroclaw::tools::Tool;
|
||||
|
||||
use crate::browser::{BrowserBackend, PipeBrowserBackend};
|
||||
use crate::compat::openxml_office_tool::OpenXmlOfficeTool;
|
||||
use crate::compat::runtime::CompatTaskContext;
|
||||
use crate::compat::screen_html_export_tool::ScreenHtmlExportTool;
|
||||
@@ -21,8 +23,13 @@ const ZHIHU_CREATOR_URL: &str = "https://www.zhihu.com/creator";
|
||||
const ZHIHU_EDITOR_URL: &str = "https://zhuanlan.zhihu.com/write";
|
||||
const HOTLIST_READY_POLL_ATTEMPTS: usize = 10;
|
||||
const HOTLIST_READY_POLL_INTERVAL: Duration = Duration::from_millis(500);
|
||||
// Simplified readiness pattern: only checks that *some* heat metric exists
|
||||
// (e.g. "3440万热度", "2.1亿"). The full rank-title-heat structure is validated
|
||||
// later by the extraction script. Using a simple pattern avoids problems with
|
||||
// the multi-line innerText format where rank, title, and heat are on separate
|
||||
// lines (`.` does not cross newlines by default).
|
||||
const HOTLIST_TEXT_READY_PATTERN: &str =
|
||||
r"(?:^|\n)\s*1(?:[.、]|\s)+.+\d+(?:\.\d+)?\s*(?:万|亿|k|K|m|M)(?:热度)?";
|
||||
r"\d+(?:\.\d+)?\s*(?:万|亿|k|K|m|M)\s*(?:热度)?";
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum WorkflowRoute {
|
||||
ZhihuHotlistExportXlsx,
|
||||
@@ -113,9 +120,9 @@ pub fn should_fallback_after_summary(summary: &str, route: &WorkflowRoute) -> bo
|
||||
)
|
||||
}
|
||||
|
||||
pub fn execute_route<T: Transport + 'static>(
|
||||
transport: &T,
|
||||
browser_tool: &BrowserPipeTool<T>,
|
||||
pub fn execute_route_with_browser_backend(
|
||||
transport: &dyn crate::agent::AgentEventSink,
|
||||
browser_backend: Arc<dyn BrowserBackend>,
|
||||
workspace_root: &Path,
|
||||
instruction: &str,
|
||||
task_context: &CompatTaskContext,
|
||||
@@ -124,37 +131,61 @@ pub fn execute_route<T: Transport + 'static>(
|
||||
match route {
|
||||
WorkflowRoute::ZhihuHotlistExportXlsx | WorkflowRoute::ZhihuHotlistScreen => {
|
||||
let top_n = extract_top_n(instruction);
|
||||
let items = collect_hotlist_items(transport, browser_tool, top_n, task_context)?;
|
||||
let items = collect_hotlist_items(transport, browser_backend.as_ref(), top_n, task_context)?;
|
||||
if items.is_empty() {
|
||||
return Err(PipeError::Protocol(
|
||||
"知乎热榜采集失败:未能从页面文本中解析到热榜条目".to_string(),
|
||||
));
|
||||
}
|
||||
match route {
|
||||
WorkflowRoute::ZhihuHotlistExportXlsx => {
|
||||
export_xlsx(transport, workspace_root, &items)
|
||||
}
|
||||
WorkflowRoute::ZhihuHotlistScreen => {
|
||||
export_screen(transport, workspace_root, &items)
|
||||
}
|
||||
WorkflowRoute::ZhihuHotlistExportXlsx => export_xlsx(transport, workspace_root, &items),
|
||||
WorkflowRoute::ZhihuHotlistScreen => export_screen(transport, workspace_root, &items),
|
||||
_ => unreachable!("handled by outer match"),
|
||||
}
|
||||
}
|
||||
WorkflowRoute::ZhihuArticleEntry => {
|
||||
execute_zhihu_article_entry_route(transport, browser_tool)
|
||||
}
|
||||
WorkflowRoute::ZhihuArticleDraft => {
|
||||
execute_zhihu_article_route(transport, browser_tool, instruction, task_context, false)
|
||||
}
|
||||
WorkflowRoute::ZhihuArticlePublish => {
|
||||
execute_zhihu_article_route(transport, browser_tool, instruction, task_context, true)
|
||||
execute_zhihu_article_entry_route(transport, browser_backend.as_ref())
|
||||
}
|
||||
WorkflowRoute::ZhihuArticleDraft => execute_zhihu_article_route(
|
||||
transport,
|
||||
browser_backend.as_ref(),
|
||||
instruction,
|
||||
task_context,
|
||||
false,
|
||||
),
|
||||
WorkflowRoute::ZhihuArticlePublish => execute_zhihu_article_route(
|
||||
transport,
|
||||
browser_backend.as_ref(),
|
||||
instruction,
|
||||
task_context,
|
||||
true,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_hotlist_items<T: Transport + 'static>(
|
||||
pub fn execute_route<T: Transport + 'static>(
|
||||
transport: &T,
|
||||
browser_tool: &BrowserPipeTool<T>,
|
||||
workspace_root: &Path,
|
||||
instruction: &str,
|
||||
task_context: &CompatTaskContext,
|
||||
route: WorkflowRoute,
|
||||
) -> Result<String, PipeError> {
|
||||
let browser_backend: Arc<dyn BrowserBackend> =
|
||||
Arc::new(PipeBrowserBackend::from_inner(browser_tool.clone()));
|
||||
execute_route_with_browser_backend(
|
||||
transport,
|
||||
browser_backend,
|
||||
workspace_root,
|
||||
instruction,
|
||||
task_context,
|
||||
route,
|
||||
)
|
||||
}
|
||||
|
||||
fn collect_hotlist_items(
|
||||
transport: &dyn crate::agent::AgentEventSink,
|
||||
browser_tool: &dyn BrowserBackend,
|
||||
top_n: usize,
|
||||
task_context: &CompatTaskContext,
|
||||
) -> Result<Vec<HotlistItem>, PipeError> {
|
||||
@@ -185,9 +216,9 @@ fn collect_hotlist_items<T: Transport + 'static>(
|
||||
parse_hotlist_items_payload(response.data.get("text").unwrap_or(&response.data))
|
||||
}
|
||||
|
||||
fn ensure_hotlist_page_ready<T: Transport + 'static>(
|
||||
transport: &T,
|
||||
browser_tool: &BrowserPipeTool<T>,
|
||||
fn ensure_hotlist_page_ready(
|
||||
transport: &dyn crate::agent::AgentEventSink,
|
||||
browser_tool: &dyn BrowserBackend,
|
||||
top_n: usize,
|
||||
task_context: &CompatTaskContext,
|
||||
) -> Result<Option<Vec<HotlistItem>>, PipeError> {
|
||||
@@ -227,9 +258,9 @@ fn ensure_hotlist_page_ready<T: Transport + 'static>(
|
||||
Err(last_error.unwrap_or_else(|| PipeError::Protocol("知乎热榜页面未就绪".to_string())))
|
||||
}
|
||||
|
||||
fn probe_hotlist_extractor<T: Transport + 'static>(
|
||||
transport: &T,
|
||||
browser_tool: &BrowserPipeTool<T>,
|
||||
fn probe_hotlist_extractor(
|
||||
transport: &dyn crate::agent::AgentEventSink,
|
||||
browser_tool: &dyn BrowserBackend,
|
||||
top_n: usize,
|
||||
) -> Result<Option<Vec<HotlistItem>>, PipeError> {
|
||||
transport.send(&AgentMessage::LogEntry {
|
||||
@@ -242,19 +273,32 @@ fn probe_hotlist_extractor<T: Transport + 'static>(
|
||||
ZHIHU_DOMAIN,
|
||||
)?;
|
||||
if !response.success {
|
||||
eprintln!("probe_hotlist_extractor: eval not successful data={}", response.data);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
match parse_hotlist_items_payload(response.data.get("text").unwrap_or(&response.data)) {
|
||||
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)),
|
||||
Ok(_) => Ok(None),
|
||||
Err(_) => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn navigate_hotlist_page<T: Transport + 'static>(
|
||||
transport: &T,
|
||||
browser_tool: &BrowserPipeTool<T>,
|
||||
fn navigate_hotlist_page(
|
||||
transport: &dyn crate::agent::AgentEventSink,
|
||||
browser_tool: &dyn BrowserBackend,
|
||||
) -> Result<(), PipeError> {
|
||||
transport.send(&AgentMessage::LogEntry {
|
||||
level: "info".to_string(),
|
||||
@@ -275,16 +319,34 @@ fn navigate_hotlist_page<T: Transport + 'static>(
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_for_hotlist_readiness<T: Transport + 'static>(
|
||||
browser_tool: &BrowserPipeTool<T>,
|
||||
) -> Result<bool, PipeError> {
|
||||
fn poll_for_hotlist_readiness(browser_tool: &dyn BrowserBackend) -> Result<bool, PipeError> {
|
||||
let ready_pattern =
|
||||
Regex::new(HOTLIST_TEXT_READY_PATTERN).expect("hotlist readiness regex must compile");
|
||||
for attempt in 0..HOTLIST_READY_POLL_ATTEMPTS {
|
||||
let response =
|
||||
browser_tool.invoke(Action::GetText, json!({ "selector": "body" }), ZHIHU_DOMAIN)?;
|
||||
// Tolerate individual GetText failures (e.g. callback timeout) – they
|
||||
// are expected while the page is still loading or the callback delivery
|
||||
// path is not yet established. Only a PipeClosed error is fatal.
|
||||
let response = match browser_tool.invoke(
|
||||
Action::GetText,
|
||||
json!({ "selector": "body" }),
|
||||
ZHIHU_DOMAIN,
|
||||
) {
|
||||
Ok(resp) => resp,
|
||||
Err(PipeError::PipeClosed) => return Err(PipeError::PipeClosed),
|
||||
Err(_) => {
|
||||
if attempt + 1 < HOTLIST_READY_POLL_ATTEMPTS {
|
||||
thread::sleep(HOTLIST_READY_POLL_INTERVAL);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
};
|
||||
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);
|
||||
}
|
||||
@@ -302,8 +364,8 @@ fn hotlist_text_looks_ready(payload: &Value, ready_pattern: &Regex) -> bool {
|
||||
text.contains("热榜") && ready_pattern.is_match(text)
|
||||
}
|
||||
|
||||
fn export_xlsx<T: Transport>(
|
||||
transport: &T,
|
||||
fn export_xlsx(
|
||||
transport: &dyn crate::agent::AgentEventSink,
|
||||
workspace_root: &Path,
|
||||
items: &[HotlistItem],
|
||||
) -> Result<String, PipeError> {
|
||||
@@ -341,8 +403,8 @@ fn export_xlsx<T: Transport>(
|
||||
Ok(format!("已导出知乎热榜 Excel {output_path}"))
|
||||
}
|
||||
|
||||
fn export_screen<T: Transport>(
|
||||
transport: &T,
|
||||
fn export_screen(
|
||||
transport: &dyn crate::agent::AgentEventSink,
|
||||
workspace_root: &Path,
|
||||
items: &[HotlistItem],
|
||||
) -> Result<String, PipeError> {
|
||||
@@ -376,9 +438,9 @@ fn export_screen<T: Transport>(
|
||||
Ok(format!("已生成知乎热榜大屏 {output_path}"))
|
||||
}
|
||||
|
||||
fn execute_zhihu_article_route<T: Transport + 'static>(
|
||||
transport: &T,
|
||||
browser_tool: &BrowserPipeTool<T>,
|
||||
fn execute_zhihu_article_route(
|
||||
transport: &dyn crate::agent::AgentEventSink,
|
||||
browser_tool: &dyn BrowserBackend,
|
||||
instruction: &str,
|
||||
task_context: &CompatTaskContext,
|
||||
publish_mode: bool,
|
||||
@@ -479,9 +541,9 @@ fn execute_zhihu_article_route<T: Transport + 'static>(
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_zhihu_article_entry_route<T: Transport + 'static>(
|
||||
transport: &T,
|
||||
browser_tool: &BrowserPipeTool<T>,
|
||||
fn execute_zhihu_article_entry_route(
|
||||
transport: &dyn crate::agent::AgentEventSink,
|
||||
browser_tool: &dyn BrowserBackend,
|
||||
) -> Result<String, PipeError> {
|
||||
navigate_zhihu_page(transport, browser_tool, ZHIHU_CREATOR_URL)?;
|
||||
transport.send(&AgentMessage::LogEntry {
|
||||
@@ -596,9 +658,9 @@ fn extract_top_n(instruction: &str) -> usize {
|
||||
.unwrap_or(10)
|
||||
}
|
||||
|
||||
fn navigate_zhihu_page<T: Transport + 'static>(
|
||||
transport: &T,
|
||||
browser_tool: &BrowserPipeTool<T>,
|
||||
fn navigate_zhihu_page(
|
||||
transport: &dyn crate::agent::AgentEventSink,
|
||||
browser_tool: &dyn BrowserBackend,
|
||||
url: &str,
|
||||
) -> Result<(), PipeError> {
|
||||
transport.send(&AgentMessage::LogEntry {
|
||||
@@ -616,8 +678,8 @@ fn navigate_zhihu_page<T: Transport + 'static>(
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_browser_skill_script<T: Transport + 'static>(
|
||||
browser_tool: &BrowserPipeTool<T>,
|
||||
fn execute_browser_skill_script(
|
||||
browser_tool: &dyn BrowserBackend,
|
||||
skill_name: &str,
|
||||
script_name: &str,
|
||||
args: Value,
|
||||
@@ -641,9 +703,9 @@ fn execute_browser_skill_script<T: Transport + 'static>(
|
||||
))
|
||||
}
|
||||
|
||||
fn navigate_to_editor_after_creator_entry<T: Transport + 'static>(
|
||||
transport: &T,
|
||||
browser_tool: &BrowserPipeTool<T>,
|
||||
fn navigate_to_editor_after_creator_entry(
|
||||
transport: &dyn crate::agent::AgentEventSink,
|
||||
browser_tool: &dyn BrowserBackend,
|
||||
creator_state: &Value,
|
||||
) -> Result<(), PipeError> {
|
||||
let status = payload_status(creator_state);
|
||||
@@ -679,7 +741,7 @@ mod tests {
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::pipe::{BrowserMessage, Timing};
|
||||
use crate::pipe::{BrowserMessage, CommandOutput, ExecutionSurfaceMetadata, Timing};
|
||||
use crate::security::MacPolicy;
|
||||
|
||||
struct MockWorkflowTransport {
|
||||
@@ -743,6 +805,245 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[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()),
|
||||
}
|
||||
}
|
||||
|
||||
fn invocations(&self) -> Vec<(Action, Value, String)> {
|
||||
self.invocations.lock().unwrap().clone()
|
||||
}
|
||||
}
|
||||
|
||||
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 execute_route_with_browser_backend_runs_direct_route_with_ws_style_backend() {
|
||||
let transport = Arc::new(MockWorkflowTransport::new(vec![]));
|
||||
let backend = Arc::new(FakeBrowserBackend::new(vec![
|
||||
Ok(CommandOutput {
|
||||
seq: 1,
|
||||
success: true,
|
||||
data: json!({}),
|
||||
aom_snapshot: vec![],
|
||||
timing: Timing {
|
||||
queue_ms: 1,
|
||||
exec_ms: 1,
|
||||
},
|
||||
}),
|
||||
Ok(CommandOutput {
|
||||
seq: 2,
|
||||
success: true,
|
||||
data: json!({"text": "已进入编辑器"}),
|
||||
aom_snapshot: vec![],
|
||||
timing: Timing {
|
||||
queue_ms: 1,
|
||||
exec_ms: 1,
|
||||
},
|
||||
}),
|
||||
Ok(CommandOutput {
|
||||
seq: 3,
|
||||
success: true,
|
||||
data: json!({
|
||||
"text": {
|
||||
"status": "editor_ready",
|
||||
"current_url": "https://zhuanlan.zhihu.com/write"
|
||||
}
|
||||
}),
|
||||
aom_snapshot: vec![],
|
||||
timing: Timing {
|
||||
queue_ms: 1,
|
||||
exec_ms: 1,
|
||||
},
|
||||
}),
|
||||
]));
|
||||
|
||||
let summary = execute_route_with_browser_backend(
|
||||
transport.as_ref(),
|
||||
backend.clone(),
|
||||
Path::new("."),
|
||||
"打开知乎写文章页面",
|
||||
&CompatTaskContext::default(),
|
||||
WorkflowRoute::ZhihuArticleEntry,
|
||||
)
|
||||
.expect("ws-style backend should satisfy direct route execution");
|
||||
|
||||
assert_eq!(summary, "已进入知乎文章编辑器。");
|
||||
assert_eq!(
|
||||
backend.invocations(),
|
||||
vec![
|
||||
(
|
||||
Action::Navigate,
|
||||
json!({ "url": ZHIHU_CREATOR_URL }),
|
||||
ZHIHU_DOMAIN.to_string(),
|
||||
),
|
||||
(
|
||||
Action::Eval,
|
||||
json!({
|
||||
"script": load_browser_skill_script(
|
||||
"zhihu-navigate",
|
||||
"open_creator_entry.js",
|
||||
json!({ "desired_target": "article_editor" })
|
||||
)
|
||||
.expect("zhihu navigate script should load")
|
||||
}),
|
||||
ZHIHU_DOMAIN.to_string(),
|
||||
),
|
||||
(
|
||||
Action::Eval,
|
||||
json!({
|
||||
"script": load_browser_skill_script(
|
||||
"zhihu-write",
|
||||
"prepare_article_editor.js",
|
||||
json!({ "desired_mode": "draft" })
|
||||
)
|
||||
.expect("zhihu write script should load")
|
||||
}),
|
||||
ZHIHU_EDITOR_DOMAIN.to_string(),
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execute_route_with_browser_backend_keeps_bridge_style_article_entry_direct_route() {
|
||||
let transport = Arc::new(MockWorkflowTransport::new(vec![]));
|
||||
let backend = Arc::new(FakeBrowserBackend::new(vec![
|
||||
Ok(CommandOutput {
|
||||
seq: 1,
|
||||
success: true,
|
||||
data: json!({}),
|
||||
aom_snapshot: vec![],
|
||||
timing: Timing {
|
||||
queue_ms: 1,
|
||||
exec_ms: 1,
|
||||
},
|
||||
}),
|
||||
Ok(CommandOutput {
|
||||
seq: 2,
|
||||
success: true,
|
||||
data: json!({
|
||||
"text": {
|
||||
"status": "creator_entry_clicked",
|
||||
"current_url": "https://www.zhihu.com/creator",
|
||||
"next_url": ZHIHU_EDITOR_URL,
|
||||
}
|
||||
}),
|
||||
aom_snapshot: vec![],
|
||||
timing: Timing {
|
||||
queue_ms: 1,
|
||||
exec_ms: 1,
|
||||
},
|
||||
}),
|
||||
Ok(CommandOutput {
|
||||
seq: 3,
|
||||
success: true,
|
||||
data: json!({}),
|
||||
aom_snapshot: vec![],
|
||||
timing: Timing {
|
||||
queue_ms: 1,
|
||||
exec_ms: 1,
|
||||
},
|
||||
}),
|
||||
Ok(CommandOutput {
|
||||
seq: 4,
|
||||
success: true,
|
||||
data: json!({
|
||||
"text": {
|
||||
"status": "editor_ready",
|
||||
"current_url": ZHIHU_EDITOR_URL,
|
||||
}
|
||||
}),
|
||||
aom_snapshot: vec![],
|
||||
timing: Timing {
|
||||
queue_ms: 1,
|
||||
exec_ms: 1,
|
||||
},
|
||||
}),
|
||||
]));
|
||||
|
||||
let summary = execute_route_with_browser_backend(
|
||||
transport.as_ref(),
|
||||
backend.clone(),
|
||||
Path::new("."),
|
||||
"打开知乎写文章页面",
|
||||
&CompatTaskContext::default(),
|
||||
WorkflowRoute::ZhihuArticleEntry,
|
||||
)
|
||||
.expect("bridge-style backend should satisfy direct route execution");
|
||||
|
||||
assert_eq!(summary, "已进入知乎文章编辑器。");
|
||||
assert_eq!(
|
||||
backend.invocations(),
|
||||
vec![
|
||||
(
|
||||
Action::Navigate,
|
||||
json!({ "url": ZHIHU_CREATOR_URL }),
|
||||
ZHIHU_DOMAIN.to_string(),
|
||||
),
|
||||
(
|
||||
Action::Eval,
|
||||
json!({
|
||||
"script": load_browser_skill_script(
|
||||
"zhihu-navigate",
|
||||
"open_creator_entry.js",
|
||||
json!({ "desired_target": "article_editor" })
|
||||
)
|
||||
.expect("zhihu navigate script should load")
|
||||
}),
|
||||
ZHIHU_DOMAIN.to_string(),
|
||||
),
|
||||
(
|
||||
Action::Navigate,
|
||||
json!({ "url": ZHIHU_EDITOR_URL }),
|
||||
ZHIHU_EDITOR_DOMAIN.to_string(),
|
||||
),
|
||||
(
|
||||
Action::Eval,
|
||||
json!({
|
||||
"script": load_browser_skill_script(
|
||||
"zhihu-write",
|
||||
"prepare_article_editor.js",
|
||||
json!({ "desired_mode": "draft" })
|
||||
)
|
||||
.expect("zhihu write script should load")
|
||||
}),
|
||||
ZHIHU_EDITOR_DOMAIN.to_string(),
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect_hotlist_items_skips_navigation_when_hot_page_is_already_readable() {
|
||||
let transport = Arc::new(MockWorkflowTransport::new(vec![
|
||||
@@ -771,7 +1072,8 @@ mod tests {
|
||||
..CompatTaskContext::default()
|
||||
};
|
||||
|
||||
let items = collect_hotlist_items(transport.as_ref(), &browser_tool, 10, &task_context)
|
||||
let browser_backend = PipeBrowserBackend::from_inner(browser_tool);
|
||||
let items = collect_hotlist_items(transport.as_ref(), &browser_backend, 10, &task_context)
|
||||
.expect("hotlist collection should succeed");
|
||||
|
||||
assert_eq!(items.len(), 2);
|
||||
@@ -824,7 +1126,8 @@ mod tests {
|
||||
..CompatTaskContext::default()
|
||||
};
|
||||
|
||||
let items = collect_hotlist_items(transport.as_ref(), &browser_tool, 10, &task_context)
|
||||
let browser_backend = PipeBrowserBackend::from_inner(browser_tool);
|
||||
let items = collect_hotlist_items(transport.as_ref(), &browser_backend, 10, &task_context)
|
||||
.expect("hotlist collection should succeed after readiness polling");
|
||||
|
||||
assert_eq!(items.len(), 1);
|
||||
@@ -892,7 +1195,8 @@ mod tests {
|
||||
..CompatTaskContext::default()
|
||||
};
|
||||
|
||||
let items = collect_hotlist_items(transport.as_ref(), &browser_tool, 10, &task_context)
|
||||
let browser_backend = PipeBrowserBackend::from_inner(browser_tool);
|
||||
let items = collect_hotlist_items(transport.as_ref(), &browser_backend, 10, &task_context)
|
||||
.expect("hotlist collection should succeed after one navigation retry");
|
||||
|
||||
assert_eq!(items.len(), 1);
|
||||
@@ -958,7 +1262,8 @@ mod tests {
|
||||
..CompatTaskContext::default()
|
||||
};
|
||||
|
||||
let items = collect_hotlist_items(transport.as_ref(), &browser_tool, 10, &task_context)
|
||||
let browser_backend = PipeBrowserBackend::from_inner(browser_tool);
|
||||
let items = collect_hotlist_items(transport.as_ref(), &browser_backend, 10, &task_context)
|
||||
.expect("hotlist collection should succeed via extractor probe");
|
||||
|
||||
assert_eq!(items.len(), 1);
|
||||
|
||||
Reference in New Issue
Block a user