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:
@@ -13,6 +13,7 @@ use serde_json::{json, Value};
|
||||
use sgclaw::agent::{
|
||||
handle_browser_message, handle_browser_message_with_context, AgentRuntimeContext,
|
||||
};
|
||||
use sgclaw::compat::workflow_executor::finalize_screen_export;
|
||||
use sgclaw::compat::runtime::{execute_task, execute_task_with_sgclaw_settings, CompatTaskContext};
|
||||
use sgclaw::config::{DeepSeekSettings, SgClawSettings};
|
||||
use sgclaw::pipe::{
|
||||
@@ -176,6 +177,7 @@ fn start_fake_deepseek_server(
|
||||
Err(err) => panic!("failed to accept provider request: {err}"),
|
||||
}
|
||||
};
|
||||
stream.set_nonblocking(false).unwrap();
|
||||
let body = read_http_json_body(&mut stream);
|
||||
request_log.lock().unwrap().push(body);
|
||||
|
||||
@@ -1861,6 +1863,15 @@ fn handle_browser_message_exposes_real_zhihu_skill_lib_to_provider_request() {
|
||||
let request_bodies = requests.lock().unwrap().clone();
|
||||
let first_request = request_bodies[0].to_string();
|
||||
let tool_names = request_tool_names(&request_bodies[0]);
|
||||
let loaded_skills_message = sent
|
||||
.iter()
|
||||
.find_map(|message| match message {
|
||||
AgentMessage::LogEntry { level, message } if level == "info" && message.starts_with("loaded skills: ") => {
|
||||
Some(message.clone())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.expect("expected loaded skills log entry");
|
||||
|
||||
assert!(sent.iter().any(|message| {
|
||||
matches!(
|
||||
@@ -1869,15 +1880,11 @@ fn handle_browser_message_exposes_real_zhihu_skill_lib_to_provider_request() {
|
||||
if *success && summary == "已看到真实知乎 skill"
|
||||
)
|
||||
}));
|
||||
assert!(sent.iter().any(|message| {
|
||||
matches!(
|
||||
message,
|
||||
AgentMessage::LogEntry { level, message }
|
||||
if level == "info" &&
|
||||
message ==
|
||||
"loaded skills: office-export-xlsx@0.1.0, zhihu-hotlist@0.1.0, zhihu-hotlist-screen@0.1.0, zhihu-navigate@0.1.0, zhihu-write@0.1.0"
|
||||
)
|
||||
}));
|
||||
assert!(loaded_skills_message.contains("office-export-xlsx@0.1.0"));
|
||||
assert!(loaded_skills_message.contains("zhihu-hotlist@0.1.0"));
|
||||
assert!(loaded_skills_message.contains("zhihu-hotlist-screen@0.1.0"));
|
||||
assert!(loaded_skills_message.contains("zhihu-navigate@0.1.0"));
|
||||
assert!(loaded_skills_message.contains("zhihu-write@0.1.0"));
|
||||
assert_eq!(request_bodies.len(), 1);
|
||||
assert!(first_request.contains("office-export-xlsx"));
|
||||
assert!(first_request.contains("zhihu-hotlist"));
|
||||
@@ -2107,145 +2114,9 @@ fn handle_browser_message_executes_real_zhihu_hotlist_skill_flow() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handle_browser_message_chains_hotlist_skill_into_office_export_tool() {
|
||||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||||
|
||||
let workspace_root = temp_workspace_root();
|
||||
let output_path = workspace_root.join("out/zhihu-hotlist.xlsx");
|
||||
let output_path_str = output_path.to_string_lossy().to_string();
|
||||
let first_response = json!({
|
||||
"choices": [{
|
||||
"message": {
|
||||
"content": "",
|
||||
"tool_calls": [{
|
||||
"id": "call_1",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "zhihu-hotlist_extract_hotlist",
|
||||
"arguments": serde_json::to_string(&json!({
|
||||
"expected_domain": "www.zhihu.com",
|
||||
"top_n": "10"
|
||||
})).unwrap()
|
||||
}
|
||||
}]
|
||||
}
|
||||
}]
|
||||
});
|
||||
let third_response = json!({
|
||||
"choices": [{
|
||||
"message": {
|
||||
"content": "",
|
||||
"tool_calls": [{
|
||||
"id": "call_3",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "openxml_office",
|
||||
"arguments": serde_json::to_string(&json!({
|
||||
"sheet_name": "知乎热榜",
|
||||
"columns": ["rank", "title", "heat"],
|
||||
"rows": [
|
||||
[1, "问题一", "344万"],
|
||||
[2, "问题二", "266万"]
|
||||
],
|
||||
"output_path": output_path_str
|
||||
})).unwrap()
|
||||
}
|
||||
}]
|
||||
}
|
||||
}]
|
||||
});
|
||||
let fourth_response = json!({
|
||||
"choices": [{
|
||||
"message": {
|
||||
"content": format!("已导出知乎热榜 Excel {output_path_str}")
|
||||
}
|
||||
}]
|
||||
});
|
||||
let (base_url, _requests, server_handle) =
|
||||
start_fake_deepseek_server(vec![first_response, third_response, fourth_response]);
|
||||
let config_path = write_deepseek_config_with_skills_dir(
|
||||
&workspace_root,
|
||||
"deepseek-test-key",
|
||||
&base_url,
|
||||
"deepseek-chat",
|
||||
Some(real_skill_lib_root().to_str().unwrap()),
|
||||
);
|
||||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
||||
|
||||
let transport = Arc::new(MockTransport::new(vec![success_browser_response(
|
||||
1,
|
||||
json!({
|
||||
"text": {
|
||||
"source": "https://www.zhihu.com/hot",
|
||||
"sheet_name": "知乎热榜",
|
||||
"columns": ["rank", "title", "heat"],
|
||||
"rows": [[1, "问题一", "344万"], [2, "问题二", "266万"]]
|
||||
}
|
||||
}),
|
||||
)]));
|
||||
let browser_tool = BrowserPipeTool::new(
|
||||
transport.clone(),
|
||||
zhihu_test_policy(),
|
||||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||||
)
|
||||
.with_response_timeout(Duration::from_secs(1));
|
||||
|
||||
handle_browser_message_with_context(
|
||||
transport.as_ref(),
|
||||
&browser_tool,
|
||||
&runtime_context,
|
||||
BrowserMessage::SubmitTask {
|
||||
instruction: "读取知乎热榜数据,并导出 excel 文件".to_string(),
|
||||
conversation_id: String::new(),
|
||||
messages: vec![],
|
||||
page_url: "https://www.zhihu.com/".to_string(),
|
||||
page_title: "知乎".to_string(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
server_handle.join().unwrap();
|
||||
|
||||
let sent = transport.sent_messages();
|
||||
|
||||
assert!(sent.iter().any(|message| {
|
||||
matches!(
|
||||
message,
|
||||
AgentMessage::TaskComplete { success, summary }
|
||||
if *success && summary.contains("已导出知乎热榜 Excel") && summary.contains(".xlsx")
|
||||
)
|
||||
}));
|
||||
assert!(sent.iter().any(|message| {
|
||||
matches!(
|
||||
message,
|
||||
AgentMessage::LogEntry { level, message }
|
||||
if level == "mode" && message == "zeroclaw_process_message_primary"
|
||||
)
|
||||
}));
|
||||
assert!(sent.iter().any(|message| {
|
||||
matches!(
|
||||
message,
|
||||
AgentMessage::LogEntry { level, message }
|
||||
if level == "info" && message == "call zhihu-hotlist.extract_hotlist"
|
||||
)
|
||||
}));
|
||||
assert!(sent.iter().any(|message| {
|
||||
matches!(
|
||||
message,
|
||||
AgentMessage::Command { action, .. } if action == &Action::Eval
|
||||
)
|
||||
}));
|
||||
assert!(!sent.iter().any(|message| {
|
||||
matches!(
|
||||
message,
|
||||
AgentMessage::LogEntry { level, message }
|
||||
if level == "mode" && (message == "compat_llm_primary" || message == "compat_skill_runner_primary")
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handle_browser_message_chains_hotlist_skill_into_screen_export_tool() {
|
||||
fn handle_browser_message_chains_hotlist_skill_into_xlsx_export_and_auto_open() {
|
||||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||||
std::env::set_var("SGCLAW_DISABLE_POST_EXPORT_OPEN", "1");
|
||||
|
||||
let workspace_root = temp_workspace_root();
|
||||
let config_path = write_deepseek_config_with_skills_dir(
|
||||
@@ -2282,6 +2153,118 @@ fn handle_browser_message_chains_hotlist_skill_into_screen_export_tool() {
|
||||
)
|
||||
.with_response_timeout(Duration::from_secs(1));
|
||||
|
||||
handle_browser_message_with_context(
|
||||
transport.as_ref(),
|
||||
&browser_tool,
|
||||
&runtime_context,
|
||||
BrowserMessage::SubmitTask {
|
||||
instruction: "读取知乎热榜数据,并导出 excel 文件".to_string(),
|
||||
conversation_id: String::new(),
|
||||
messages: vec![],
|
||||
page_url: "https://www.zhihu.com/".to_string(),
|
||||
page_title: "知乎".to_string(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let sent = transport.sent_messages();
|
||||
let summary = task_complete_summary(&sent);
|
||||
let generated = extract_generated_artifact_path(&summary, ".xlsx");
|
||||
|
||||
assert!(summary.contains("已导出并打开知乎热榜 Excel"));
|
||||
assert!(summary.contains(".xlsx"));
|
||||
assert!(generated.exists());
|
||||
assert!(sent.iter().any(|message| {
|
||||
matches!(
|
||||
message,
|
||||
AgentMessage::TaskComplete { success, summary }
|
||||
if *success && summary.contains("已导出并打开知乎热榜 Excel") && summary.contains(".xlsx")
|
||||
)
|
||||
}));
|
||||
assert!(sent.iter().any(|message| {
|
||||
matches!(
|
||||
message,
|
||||
AgentMessage::LogEntry { level, message }
|
||||
if level == "mode" && message == "zeroclaw_process_message_primary"
|
||||
)
|
||||
}));
|
||||
assert!(sent.iter().any(|message| {
|
||||
matches!(
|
||||
message,
|
||||
AgentMessage::LogEntry { level, message }
|
||||
if level == "info" && message == "call zhihu-hotlist.extract_hotlist"
|
||||
)
|
||||
}));
|
||||
assert!(sent.iter().any(|message| {
|
||||
matches!(
|
||||
message,
|
||||
AgentMessage::LogEntry { level, message }
|
||||
if level == "info" && message == "call openxml_office"
|
||||
)
|
||||
}));
|
||||
assert!(sent.iter().any(|message| {
|
||||
matches!(
|
||||
message,
|
||||
AgentMessage::Command { action, .. } if action == &Action::Eval
|
||||
)
|
||||
}));
|
||||
assert!(!sent.iter().any(|message| {
|
||||
matches!(
|
||||
message,
|
||||
AgentMessage::Command { action, params, .. }
|
||||
if action == &Action::Navigate && params.get("sgclaw_local_dashboard_open").is_some()
|
||||
)
|
||||
}));
|
||||
assert!(!sent.iter().any(|message| {
|
||||
matches!(
|
||||
message,
|
||||
AgentMessage::LogEntry { level, message }
|
||||
if level == "mode" && (message == "compat_llm_primary" || message == "compat_skill_runner_primary")
|
||||
)
|
||||
}));
|
||||
std::env::remove_var("SGCLAW_DISABLE_POST_EXPORT_OPEN");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handle_browser_message_chains_hotlist_skill_into_screen_export_and_auto_open() {
|
||||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||||
|
||||
let workspace_root = temp_workspace_root();
|
||||
let config_path = write_deepseek_config_with_skills_dir(
|
||||
&workspace_root,
|
||||
"deepseek-test-key",
|
||||
"http://127.0.0.1:9",
|
||||
"deepseek-chat",
|
||||
Some(real_skill_lib_root().to_str().unwrap()),
|
||||
);
|
||||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
||||
|
||||
let transport = Arc::new(MockTransport::new(vec![
|
||||
success_browser_response(1, json!({ "navigated": true })),
|
||||
success_browser_response(
|
||||
2,
|
||||
json!({ "text": "知乎热榜\n1 问题一 344万热度\n2 问题二 266万热度" }),
|
||||
),
|
||||
success_browser_response(
|
||||
3,
|
||||
json!({
|
||||
"text": {
|
||||
"source": "https://www.zhihu.com/hot",
|
||||
"sheet_name": "知乎热榜",
|
||||
"columns": ["rank", "title", "heat"],
|
||||
"rows": [[1, "问题一", "344万"], [2, "问题二", "266万"]]
|
||||
}
|
||||
}),
|
||||
),
|
||||
success_browser_response(4, json!({ "navigated": true })),
|
||||
]));
|
||||
let browser_tool = BrowserPipeTool::new(
|
||||
transport.clone(),
|
||||
zhihu_test_policy(),
|
||||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||||
)
|
||||
.with_response_timeout(Duration::from_secs(1));
|
||||
|
||||
handle_browser_message_with_context(
|
||||
transport.as_ref(),
|
||||
&browser_tool,
|
||||
@@ -2299,10 +2282,43 @@ fn handle_browser_message_chains_hotlist_skill_into_screen_export_tool() {
|
||||
let sent = transport.sent_messages();
|
||||
let summary = task_complete_summary(&sent);
|
||||
let generated = extract_generated_artifact_path(&summary, ".html");
|
||||
let navigate = sent
|
||||
.iter()
|
||||
.find_map(|message| match message {
|
||||
AgentMessage::Command {
|
||||
action,
|
||||
params,
|
||||
security,
|
||||
..
|
||||
} if action == &Action::Navigate
|
||||
&& security.expected_domain == "__sgclaw_local_dashboard__" => Some((params, security)),
|
||||
_ => None,
|
||||
})
|
||||
.expect("dashboard route should emit local-dashboard navigate request");
|
||||
|
||||
assert!(summary.contains("已生成知乎热榜大屏"));
|
||||
assert!(summary.contains("已在浏览器中打开知乎热榜大屏"));
|
||||
assert!(summary.contains(".html"));
|
||||
assert!(generated.exists());
|
||||
assert_eq!(
|
||||
navigate.0["sgclaw_local_dashboard_open"]["output_path"].as_str(),
|
||||
generated.to_str()
|
||||
);
|
||||
assert!(navigate.0["url"]
|
||||
.as_str()
|
||||
.expect("dashboard open url should be present")
|
||||
.starts_with("file://"));
|
||||
assert_eq!(
|
||||
navigate.0["sgclaw_local_dashboard_open"]["source"],
|
||||
json!("compat.workflow_executor")
|
||||
);
|
||||
assert_eq!(
|
||||
navigate.0["sgclaw_local_dashboard_open"]["kind"],
|
||||
json!("zhihu_hotlist_screen")
|
||||
);
|
||||
assert_eq!(
|
||||
navigate.0["sgclaw_local_dashboard_open"]["presentation_url"],
|
||||
navigate.0["url"]
|
||||
);
|
||||
assert!(sent.iter().any(|message| {
|
||||
matches!(
|
||||
message,
|
||||
@@ -2330,6 +2346,13 @@ fn handle_browser_message_chains_hotlist_skill_into_screen_export_tool() {
|
||||
AgentMessage::Command { action, .. } if action == &Action::Eval
|
||||
)
|
||||
}));
|
||||
assert!(!sent.iter().any(|message| {
|
||||
matches!(
|
||||
message,
|
||||
AgentMessage::LogEntry { level, message }
|
||||
if level == "info" && message == "call openxml_office"
|
||||
)
|
||||
}));
|
||||
assert!(!sent.iter().any(|message| {
|
||||
matches!(
|
||||
message,
|
||||
@@ -2339,9 +2362,55 @@ fn handle_browser_message_chains_hotlist_skill_into_screen_export_tool() {
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handle_browser_message_reports_dashboard_auto_open_protocol_error_when_presentation_url_is_missing() {
|
||||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||||
|
||||
let transport = Arc::new(MockTransport::new(vec![]));
|
||||
let browser_tool = BrowserPipeTool::new(
|
||||
transport.clone(),
|
||||
zhihu_test_policy(),
|
||||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||||
)
|
||||
.with_response_timeout(Duration::from_secs(1));
|
||||
let browser_backend = sgclaw::browser::PipeBrowserBackend::from_inner(browser_tool);
|
||||
|
||||
let workspace_root = temp_workspace_root();
|
||||
let output_path = workspace_root.join("zhihu-hotlist-screen.html");
|
||||
fs::write(&output_path, "<html><body>fixture</body></html>").unwrap();
|
||||
let payload = json!({
|
||||
"title": "知乎热榜大屏",
|
||||
"output_path": output_path,
|
||||
"renderer": "screen_html_export",
|
||||
"row_count": 2,
|
||||
"snapshot_id": "snapshot-test",
|
||||
"presentation": {
|
||||
"mode": "new_tab",
|
||||
"title": "知乎热榜大屏",
|
||||
"open_in_new_tab": true
|
||||
}
|
||||
});
|
||||
|
||||
let summary = finalize_screen_export(&browser_backend, &payload.to_string()).unwrap();
|
||||
|
||||
assert!(summary.contains("已生成知乎热榜大屏"));
|
||||
assert!(summary.contains(output_path.to_string_lossy().as_ref()));
|
||||
assert!(summary.contains("但浏览器自动打开失败:screen_html_export did not return presentation.url"));
|
||||
|
||||
let sent = transport.sent_messages();
|
||||
assert!(!sent.iter().any(|message| {
|
||||
matches!(
|
||||
message,
|
||||
AgentMessage::Command { action, params, .. }
|
||||
if action == &Action::Navigate && params.get("sgclaw_local_dashboard_open").is_some()
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handle_browser_message_runs_zhihu_hotlist_export_via_zeroclaw_primary_orchestration() {
|
||||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||||
std::env::set_var("SGCLAW_DISABLE_POST_EXPORT_OPEN", "1");
|
||||
|
||||
let workspace_root = temp_workspace_root();
|
||||
let config_path = write_deepseek_config_with_skills_dir(
|
||||
@@ -2416,6 +2485,7 @@ fn handle_browser_message_runs_zhihu_hotlist_export_via_zeroclaw_primary_orchest
|
||||
if level == "mode" && (message == "compat_llm_primary" || message == "compat_skill_runner_primary")
|
||||
)
|
||||
}));
|
||||
std::env::remove_var("SGCLAW_DISABLE_POST_EXPORT_OPEN");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2527,6 +2597,143 @@ fn browser_submit_path_prefers_zeroclaw_process_message_orchestrator_for_zhihu_p
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zhihu_generated_auto_publish_matches_primary_orchestration_gate() {
|
||||
assert!(
|
||||
sgclaw::compat::orchestration::should_use_primary_orchestration(
|
||||
"在知乎自动发表一篇名称为人工智能技能大全",
|
||||
Some("https://www.zhihu.com/"),
|
||||
Some("知乎"),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zhihu_hotlist_export_route_stays_ahead_of_generated_article_publish() {
|
||||
use sgclaw::compat::workflow_executor::{detect_route, WorkflowRoute};
|
||||
|
||||
assert_eq!(
|
||||
detect_route(
|
||||
"打开知乎热榜,获取前10条数据,并导出 Excel",
|
||||
Some("https://www.zhihu.com/"),
|
||||
Some("知乎")
|
||||
),
|
||||
Some(WorkflowRoute::ZhihuHotlistExportXlsx)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zhihu_generated_auto_publish_uses_provider_and_submits_publish_without_confirmation() {
|
||||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||||
|
||||
let response = json!({
|
||||
"choices": [{
|
||||
"message": {
|
||||
"content": "标题:人工智能技能大全\n正文:第一段内容。\n\n第二段内容。"
|
||||
}
|
||||
}]
|
||||
});
|
||||
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]);
|
||||
|
||||
let workspace_root = temp_workspace_root();
|
||||
let config_path = write_deepseek_config_with_skills_dir(
|
||||
&workspace_root,
|
||||
"deepseek-test-key",
|
||||
&base_url,
|
||||
"deepseek-chat",
|
||||
Some(real_skill_lib_root().to_str().unwrap()),
|
||||
);
|
||||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
||||
|
||||
let transport = Arc::new(MockTransport::new(vec![
|
||||
success_browser_response(1, json!({ "navigated": true })),
|
||||
success_browser_response(
|
||||
2,
|
||||
json!({
|
||||
"text": {
|
||||
"status": "creator_entry_clicked",
|
||||
"current_url": "https://www.zhihu.com/creator",
|
||||
"next_url": "https://zhuanlan.zhihu.com/write"
|
||||
}
|
||||
}),
|
||||
),
|
||||
success_browser_response(3, json!({ "navigated": true })),
|
||||
success_browser_response(
|
||||
4,
|
||||
json!({
|
||||
"text": {
|
||||
"status": "editor_ready",
|
||||
"current_url": "https://zhuanlan.zhihu.com/write"
|
||||
}
|
||||
}),
|
||||
),
|
||||
success_browser_response(
|
||||
5,
|
||||
json!({
|
||||
"text": {
|
||||
"status": "publish_submitted",
|
||||
"current_url": "https://zhuanlan.zhihu.com/write",
|
||||
"title": "人工智能技能大全"
|
||||
}
|
||||
}),
|
||||
),
|
||||
]));
|
||||
let browser_tool = BrowserPipeTool::new(
|
||||
transport.clone(),
|
||||
zhihu_test_policy(),
|
||||
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||||
)
|
||||
.with_response_timeout(Duration::from_secs(1));
|
||||
|
||||
handle_browser_message_with_context(
|
||||
transport.as_ref(),
|
||||
&browser_tool,
|
||||
&runtime_context,
|
||||
BrowserMessage::SubmitTask {
|
||||
instruction: "在知乎自动发表一篇名称为人工智能技能大全".to_string(),
|
||||
conversation_id: String::new(),
|
||||
messages: vec![],
|
||||
page_url: "https://www.zhihu.com/".to_string(),
|
||||
page_title: "知乎".to_string(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
server_handle.join().unwrap();
|
||||
|
||||
let sent = transport.sent_messages();
|
||||
let request_bodies = requests.lock().unwrap().clone();
|
||||
|
||||
assert_eq!(request_bodies.len(), 1);
|
||||
assert!(request_bodies[0].to_string().contains("人工智能技能大全"));
|
||||
assert!(sent.iter().any(|message| {
|
||||
matches!(
|
||||
message,
|
||||
AgentMessage::TaskComplete { success, summary }
|
||||
if *success && summary == "已提交知乎文章发布流程《人工智能技能大全》"
|
||||
)
|
||||
}));
|
||||
assert!(sent.iter().any(|message| {
|
||||
matches!(
|
||||
message,
|
||||
AgentMessage::LogEntry { level, message }
|
||||
if level == "info" && message == "call zhihu-write.fill_article_draft"
|
||||
)
|
||||
}));
|
||||
assert!(sent.iter().any(|message| {
|
||||
matches!(
|
||||
message,
|
||||
AgentMessage::Command { action, .. } if action == &Action::Navigate
|
||||
)
|
||||
}));
|
||||
assert!(!sent.iter().any(|message| {
|
||||
matches!(
|
||||
message,
|
||||
AgentMessage::TaskComplete { success, summary }
|
||||
if *success && summary.contains("确认发布")
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zhihu_publish_task_matches_primary_orchestration_gate() {
|
||||
assert!(
|
||||
@@ -3078,71 +3285,37 @@ fn zhihu_publish_after_confirmation_reports_login_block_without_selector_probing
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn browser_orchestration_registers_superrpa_tools_natively() {
|
||||
fn browser_orchestration_executes_hotlist_export_natively_from_hotlist_page() {
|
||||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||||
|
||||
let first_response = json!({
|
||||
"choices": [{
|
||||
"message": {
|
||||
"content": "",
|
||||
"tool_calls": [{
|
||||
"id": "call_1",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "superrpa_browser",
|
||||
"arguments": serde_json::to_string(&json!({
|
||||
"action": "getText",
|
||||
"expected_domain": "www.zhihu.com",
|
||||
"selector": "main"
|
||||
})).unwrap()
|
||||
}
|
||||
}]
|
||||
}
|
||||
}]
|
||||
});
|
||||
let second_response = json!({
|
||||
"choices": [{
|
||||
"message": {
|
||||
"content": "",
|
||||
"tool_calls": [{
|
||||
"id": "call_2",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "openxml_office",
|
||||
"arguments": serde_json::to_string(&json!({
|
||||
"sheet_name": "知乎热榜",
|
||||
"columns": ["rank", "title", "heat"],
|
||||
"rows": [[1, "问题一", "344万"]]
|
||||
})).unwrap()
|
||||
}
|
||||
}]
|
||||
}
|
||||
}]
|
||||
});
|
||||
let third_response = json!({
|
||||
"choices": [{
|
||||
"message": {
|
||||
"content": "已导出知乎热榜 Excel"
|
||||
}
|
||||
}]
|
||||
});
|
||||
let (base_url, requests, server_handle) =
|
||||
start_fake_deepseek_server(vec![first_response, second_response, third_response]);
|
||||
std::env::set_var("SGCLAW_DISABLE_POST_EXPORT_OPEN", "1");
|
||||
|
||||
let workspace_root = temp_workspace_root();
|
||||
let config_path = write_deepseek_config_with_skills_dir(
|
||||
&workspace_root,
|
||||
"deepseek-test-key",
|
||||
&base_url,
|
||||
"http://127.0.0.1:9",
|
||||
"deepseek-chat",
|
||||
Some(real_skill_lib_root().to_str().unwrap()),
|
||||
);
|
||||
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
|
||||
|
||||
let transport = Arc::new(MockTransport::new(vec![success_browser_response(
|
||||
1,
|
||||
json!({ "text": "知乎热榜\n1\n问题一\n344万热度" }),
|
||||
)]));
|
||||
let transport = Arc::new(MockTransport::new(vec![
|
||||
success_browser_response(
|
||||
1,
|
||||
json!({ "text": "知乎热榜\n1 问题一 344万热度\n2 问题二 266万热度" }),
|
||||
),
|
||||
success_browser_response(
|
||||
2,
|
||||
json!({
|
||||
"text": {
|
||||
"source": "https://www.zhihu.com/hot",
|
||||
"sheet_name": "知乎热榜",
|
||||
"columns": ["rank", "title", "heat"],
|
||||
"rows": [[1, "问题一", "344万"], [2, "问题二", "266万"]]
|
||||
}
|
||||
}),
|
||||
),
|
||||
]));
|
||||
let browser_tool = BrowserPipeTool::new(
|
||||
transport.clone(),
|
||||
zhihu_test_policy(),
|
||||
@@ -3164,22 +3337,60 @@ fn browser_orchestration_registers_superrpa_tools_natively() {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let request_bodies = requests.lock().unwrap().clone();
|
||||
let sent = transport.sent_messages();
|
||||
assert!(
|
||||
!request_bodies.is_empty(),
|
||||
"expected provider request, sent messages were: {sent:?}"
|
||||
);
|
||||
server_handle.join().unwrap();
|
||||
let first_request = request_bodies
|
||||
.first()
|
||||
.expect("expected first provider request")
|
||||
.to_string();
|
||||
let tool_names = request_tool_names(&request_bodies[0]);
|
||||
let summary = task_complete_summary(&sent);
|
||||
let generated = extract_generated_artifact_path(&summary, ".xlsx");
|
||||
|
||||
assert!(first_request.contains("superrpa_browser"));
|
||||
assert!(tool_names.contains(&"superrpa_browser".to_string()));
|
||||
assert!(tool_names.contains(&"openxml_office".to_string()));
|
||||
assert!(summary.contains(".xlsx"));
|
||||
assert!(generated.exists());
|
||||
assert!(sent.iter().any(|message| {
|
||||
matches!(
|
||||
message,
|
||||
AgentMessage::LogEntry { level, message }
|
||||
if level == "mode" && message == "zeroclaw_process_message_primary"
|
||||
)
|
||||
}));
|
||||
assert!(sent.iter().any(|message| {
|
||||
matches!(
|
||||
message,
|
||||
AgentMessage::LogEntry { level, message }
|
||||
if level == "info" && message == "call zhihu-hotlist.extract_hotlist"
|
||||
)
|
||||
}));
|
||||
assert!(sent.iter().any(|message| {
|
||||
matches!(
|
||||
message,
|
||||
AgentMessage::LogEntry { level, message }
|
||||
if level == "info" && message == "call openxml_office"
|
||||
)
|
||||
}));
|
||||
assert!(sent.iter().any(|message| {
|
||||
matches!(
|
||||
message,
|
||||
AgentMessage::Command { action, .. } if action == &Action::GetText
|
||||
)
|
||||
}));
|
||||
assert!(sent.iter().any(|message| {
|
||||
matches!(
|
||||
message,
|
||||
AgentMessage::Command { action, .. } if action == &Action::Eval
|
||||
)
|
||||
}));
|
||||
assert!(!sent.iter().any(|message| {
|
||||
matches!(
|
||||
message,
|
||||
AgentMessage::Command { action, .. } if action == &Action::Navigate
|
||||
)
|
||||
}));
|
||||
assert!(!sent.iter().any(|message| {
|
||||
matches!(
|
||||
message,
|
||||
AgentMessage::LogEntry { level, message }
|
||||
if level == "mode" &&
|
||||
(message == "compat_llm_primary" || message == "compat_skill_runner_primary")
|
||||
)
|
||||
}));
|
||||
std::env::remove_var("SGCLAW_DISABLE_POST_EXPORT_OPEN");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3240,88 +3451,13 @@ fn zhihu_export_does_not_use_frontend_owned_mainline() {
|
||||
#[test]
|
||||
fn browser_skill_usage_is_execution_not_prompt_only() {
|
||||
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
|
||||
std::env::set_var("SGCLAW_DISABLE_POST_EXPORT_OPEN", "1");
|
||||
|
||||
let workspace_root = temp_workspace_root();
|
||||
let output_path = workspace_root.join("out/zhihu-hotlist-execution.xlsx");
|
||||
let output_path_str = output_path.to_string_lossy().to_string();
|
||||
let first_response = json!({
|
||||
"choices": [{
|
||||
"message": {
|
||||
"content": "",
|
||||
"tool_calls": [{
|
||||
"id": "call_1",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "superrpa_browser",
|
||||
"arguments": serde_json::to_string(&json!({
|
||||
"action": "navigate",
|
||||
"expected_domain": "www.zhihu.com",
|
||||
"url": "https://www.zhihu.com/hot"
|
||||
})).unwrap()
|
||||
}
|
||||
}]
|
||||
}
|
||||
}]
|
||||
});
|
||||
let second_response = json!({
|
||||
"choices": [{
|
||||
"message": {
|
||||
"content": "",
|
||||
"tool_calls": [{
|
||||
"id": "call_2",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "superrpa_browser",
|
||||
"arguments": serde_json::to_string(&json!({
|
||||
"action": "getText",
|
||||
"expected_domain": "www.zhihu.com",
|
||||
"selector": "main"
|
||||
})).unwrap()
|
||||
}
|
||||
}]
|
||||
}
|
||||
}]
|
||||
});
|
||||
let third_response = json!({
|
||||
"choices": [{
|
||||
"message": {
|
||||
"content": "",
|
||||
"tool_calls": [{
|
||||
"id": "call_3",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "openxml_office",
|
||||
"arguments": serde_json::to_string(&json!({
|
||||
"sheet_name": "知乎热榜",
|
||||
"columns": ["rank", "title", "heat"],
|
||||
"rows": [
|
||||
[1, "问题一", "344万"],
|
||||
[2, "问题二", "266万"]
|
||||
],
|
||||
"output_path": output_path_str
|
||||
})).unwrap()
|
||||
}
|
||||
}]
|
||||
}
|
||||
}]
|
||||
});
|
||||
let fourth_response = json!({
|
||||
"choices": [{
|
||||
"message": {
|
||||
"content": format!("已导出知乎热榜 Excel {output_path_str}")
|
||||
}
|
||||
}]
|
||||
});
|
||||
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![
|
||||
first_response,
|
||||
second_response,
|
||||
third_response,
|
||||
fourth_response,
|
||||
]);
|
||||
let config_path = write_deepseek_config_with_skills_dir(
|
||||
&workspace_root,
|
||||
"deepseek-test-key",
|
||||
&base_url,
|
||||
"http://127.0.0.1:9",
|
||||
"deepseek-chat",
|
||||
Some(real_skill_lib_root().to_str().unwrap()),
|
||||
);
|
||||
@@ -3331,7 +3467,18 @@ fn browser_skill_usage_is_execution_not_prompt_only() {
|
||||
success_browser_response(1, json!({ "navigated": true })),
|
||||
success_browser_response(
|
||||
2,
|
||||
json!({ "text": "知乎热榜\n1\n问题一\n344万热度\n2\n问题二\n266万热度" }),
|
||||
json!({ "text": "知乎热榜\n1 问题一 344万热度\n2 问题二 266万热度" }),
|
||||
),
|
||||
success_browser_response(
|
||||
3,
|
||||
json!({
|
||||
"text": {
|
||||
"source": "https://www.zhihu.com/hot",
|
||||
"sheet_name": "知乎热榜",
|
||||
"columns": ["rank", "title", "heat"],
|
||||
"rows": [[1, "问题一", "344万"], [2, "问题二", "266万"]]
|
||||
}
|
||||
}),
|
||||
),
|
||||
]));
|
||||
let browser_tool = BrowserPipeTool::new(
|
||||
@@ -3354,15 +3501,13 @@ fn browser_skill_usage_is_execution_not_prompt_only() {
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
server_handle.join().unwrap();
|
||||
|
||||
let request_bodies = requests.lock().unwrap().clone();
|
||||
let sent = transport.sent_messages();
|
||||
let first_request = request_bodies
|
||||
.first()
|
||||
.expect("expected first provider request")
|
||||
.to_string();
|
||||
let summary = task_complete_summary(&sent);
|
||||
let generated = extract_generated_artifact_path(&summary, ".xlsx");
|
||||
|
||||
assert!(summary.contains(".xlsx"));
|
||||
assert!(generated.exists());
|
||||
assert!(sent.iter().any(|message| {
|
||||
matches!(
|
||||
message,
|
||||
@@ -3370,6 +3515,29 @@ fn browser_skill_usage_is_execution_not_prompt_only() {
|
||||
if *success && summary.contains(".xlsx")
|
||||
)
|
||||
}));
|
||||
assert!(sent.iter().any(|message| {
|
||||
matches!(
|
||||
message,
|
||||
AgentMessage::LogEntry { level, message }
|
||||
if level == "mode" && message == "zeroclaw_process_message_primary"
|
||||
)
|
||||
}));
|
||||
assert!(sent.iter().any(|message| {
|
||||
matches!(
|
||||
message,
|
||||
AgentMessage::LogEntry { level, message }
|
||||
if level == "info" && message == "call openxml_office"
|
||||
)
|
||||
}));
|
||||
assert!(!sent.iter().any(|message| {
|
||||
matches!(
|
||||
message,
|
||||
AgentMessage::LogEntry { level, message }
|
||||
if level == "mode" &&
|
||||
(message == "compat_llm_primary" || message == "compat_skill_runner_primary")
|
||||
)
|
||||
}));
|
||||
std::env::remove_var("SGCLAW_DISABLE_POST_EXPORT_OPEN");
|
||||
assert!(!sent.iter().any(|message| {
|
||||
matches!(
|
||||
message,
|
||||
@@ -3387,7 +3555,6 @@ fn browser_skill_usage_is_execution_not_prompt_only() {
|
||||
message == "getText ol li")
|
||||
)
|
||||
}));
|
||||
assert!(!first_request.contains("Preloaded skill context:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user