sgclaw: snapshot today's runtime and skill updates

This commit is contained in:
zyl
2026-03-30 15:05:39 +08:00
parent c793bfc6a1
commit f51d6b7659
50 changed files with 3473 additions and 621 deletions

View File

@@ -11,15 +11,9 @@ use std::time::Duration;
use common::MockTransport;
use serde_json::{json, Value};
use sgclaw::agent::{
handle_browser_message,
handle_browser_message_with_context,
AgentRuntimeContext,
};
use sgclaw::compat::runtime::{
execute_task,
execute_task_with_sgclaw_settings,
CompatTaskContext,
handle_browser_message, handle_browser_message_with_context, AgentRuntimeContext,
};
use sgclaw::compat::runtime::{execute_task, execute_task_with_sgclaw_settings, CompatTaskContext};
use sgclaw::config::{DeepSeekSettings, SgClawSettings};
use sgclaw::pipe::{
Action, AgentMessage, BrowserMessage, BrowserPipeTool, ConversationMessage, Timing,
@@ -151,8 +145,8 @@ fn tool_message_content<'a>(request: &'a Value, tool_call_id: &str) -> Option<&'
messages.iter().find_map(|message| {
(message["role"].as_str() == Some("tool")
&& message["tool_call_id"].as_str() == Some(tool_call_id))
.then(|| message["content"].as_str())
.flatten()
.then(|| message["content"].as_str())
.flatten()
})
})
}
@@ -232,6 +226,23 @@ fn read_http_json_body(stream: &mut impl Read) -> Value {
serde_json::from_slice(&buffer[headers_end..headers_end + content_length]).unwrap()
}
fn task_complete_summary(sent: &[AgentMessage]) -> String {
sent.iter()
.find_map(|message| match message {
AgentMessage::TaskComplete { success, summary } if *success => Some(summary.clone()),
_ => None,
})
.expect("expected successful task completion")
}
fn extract_generated_artifact_path(summary: &str, extension: &str) -> PathBuf {
summary
.split_whitespace()
.find(|token| token.ends_with(extension))
.map(PathBuf::from)
.expect("expected artifact path in task summary")
}
#[test]
fn compat_runtime_uses_zeroclaw_provider_path_and_executes_browser_actions() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
@@ -386,7 +397,9 @@ fn compat_runtime_includes_default_workspace_skills_in_provider_request() {
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]);
let workspace_root = temp_workspace_root();
let default_skills_dir = workspace_root.join(".sgclaw-zeroclaw-workspace").join("skills");
let default_skills_dir = workspace_root
.join(".sgclaw-zeroclaw-workspace")
.join("skills");
write_skill_package(
&default_skills_dir,
"workspace-zhihu-skill",
@@ -422,7 +435,9 @@ fn compat_runtime_includes_default_workspace_skills_in_provider_request() {
assert_eq!(summary, "已识别默认 workspace skill");
assert_eq!(request_bodies.len(), 1);
assert!(request_bodies[0].to_string().contains("workspace-zhihu-skill"));
assert!(request_bodies[0]
.to_string()
.contains("workspace-zhihu-skill"));
}
#[test]
@@ -439,7 +454,9 @@ fn handle_browser_message_loads_skills_from_configured_skills_dir() {
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]);
let workspace_root = temp_workspace_root();
let default_skills_dir = workspace_root.join(".sgclaw-zeroclaw-workspace").join("skills");
let default_skills_dir = workspace_root
.join(".sgclaw-zeroclaw-workspace")
.join("skills");
write_skill_package(
&default_skills_dir,
"workspace-only-skill",
@@ -515,8 +532,7 @@ fn handle_browser_message_loads_skills_from_configured_skills_dir() {
}
#[test]
fn handle_browser_message_routes_supported_instruction_to_compat_runtime_when_llm_is_configured(
) {
fn handle_browser_message_routes_supported_instruction_to_compat_runtime_when_llm_is_configured() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let first_response = json!({
@@ -993,12 +1009,10 @@ fn compat_runtime_includes_prior_turns_in_follow_up_provider_request() {
assert_eq!(summary, "已在知乎搜索天气");
assert!(first_request_messages.iter().any(|message| {
message["role"] == json!("user")
&& message["content"] == json!("打开百度搜索天气")
message["role"] == json!("user") && message["content"] == json!("打开百度搜索天气")
}));
assert!(first_request_messages.iter().any(|message| {
message["role"] == json!("assistant")
&& message["content"] == json!("已在百度搜索天气")
message["role"] == json!("assistant") && message["content"] == json!("已在百度搜索天气")
}));
}
@@ -1224,9 +1238,9 @@ fn compat_runtime_can_complete_a_text_only_turn_without_browser_tool_calls() {
assert_eq!(summary, "这是纯文本回答");
assert!(!flattened.contains("Browser tool contract"));
assert!(!tool_names.contains(&"browser_action".to_string()));
assert!(!sent.iter().any(|message| {
matches!(message, AgentMessage::Command { .. })
}));
assert!(!sent
.iter()
.any(|message| { matches!(message, AgentMessage::Command { .. }) }));
}
#[test]
@@ -1243,7 +1257,9 @@ fn compat_runtime_allows_read_skill_under_compact_mode_policy() {
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]);
let workspace_root = temp_workspace_root();
let default_skills_dir = workspace_root.join(".sgclaw-zeroclaw-workspace").join("skills");
let default_skills_dir = workspace_root
.join(".sgclaw-zeroclaw-workspace")
.join("skills");
write_skill_package(
&default_skills_dir,
"workspace-zhihu-skill",
@@ -1307,7 +1323,9 @@ fn compat_runtime_exposes_browser_script_skill_tools_in_browser_attached_mode()
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![response]);
let workspace_root = temp_workspace_root();
let default_skills_dir = workspace_root.join(".sgclaw-zeroclaw-workspace").join("skills");
let default_skills_dir = workspace_root
.join(".sgclaw-zeroclaw-workspace")
.join("skills");
let skill_dir = write_skill_manifest_package(
&default_skills_dir,
"workspace-zhihu-skill",
@@ -1404,7 +1422,9 @@ fn compat_runtime_executes_browser_script_skill_via_eval_without_gettext_probing
start_fake_deepseek_server(vec![first_response, second_response]);
let workspace_root = temp_workspace_root();
let default_skills_dir = workspace_root.join(".sgclaw-zeroclaw-workspace").join("skills");
let default_skills_dir = workspace_root
.join(".sgclaw-zeroclaw-workspace")
.join("skills");
let skill_dir = write_skill_manifest_package(
&default_skills_dir,
"workspace-zhihu-skill",
@@ -1742,7 +1762,9 @@ fn compat_runtime_logs_read_skill_usage_with_skill_name() {
start_fake_deepseek_server(vec![first_response, second_response]);
let workspace_root = temp_workspace_root();
let default_skills_dir = workspace_root.join(".sgclaw-zeroclaw-workspace").join("skills");
let default_skills_dir = workspace_root
.join(".sgclaw-zeroclaw-workspace")
.join("skills");
write_skill_package(
&default_skills_dir,
"workspace-zhihu-skill",
@@ -2018,14 +2040,17 @@ fn handle_browser_message_executes_real_zhihu_hotlist_skill_flow() {
);
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, "热榜项目 1", "1707万"], [2, "热榜项目 2", "1150万"]]
}
}))]));
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, "热榜项目 1", "1707万"], [2, "热榜项目 2", "1150万"]]
}
}),
)]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
zhihu_test_policy(),
@@ -2136,11 +2161,8 @@ fn handle_browser_message_chains_hotlist_skill_into_office_export_tool() {
}
}]
});
let (base_url, _requests, server_handle) = start_fake_deepseek_server(vec![
first_response,
third_response,
fourth_response,
]);
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",
@@ -2150,16 +2172,17 @@ fn handle_browser_message_chains_hotlist_skill_into_office_export_tool() {
);
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
let transport = Arc::new(MockTransport::new(vec![
success_browser_response(1, json!({
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(),
@@ -2225,84 +2248,10 @@ fn handle_browser_message_chains_hotlist_skill_into_screen_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-screen.html");
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": "screen_html_export",
"arguments": serde_json::to_string(&json!({
"rows": [
[1, "问题一", "344万"],
[2, "问题二", "266万"]
],
"output_path": output_path_str
})).unwrap()
}
}]
}
}]
});
let fourth_response = json!({
"choices": [{
"message": {
"content": format!("已生成知乎热榜大屏 {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()),
);
@@ -2312,7 +2261,18 @@ fn handle_browser_message_chains_hotlist_skill_into_screen_export_tool() {
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(
@@ -2335,22 +2295,39 @@ fn handle_browser_message_chains_hotlist_skill_into_screen_export_tool() {
},
)
.unwrap();
server_handle.join().unwrap();
let sent = transport.sent_messages();
let summary = task_complete_summary(&sent);
let generated = extract_generated_artifact_path(&summary, ".html");
assert!(summary.contains("已生成知乎热榜大屏"));
assert!(summary.contains(".html"));
assert!(generated.exists());
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::TaskComplete { success, summary }
if *success && summary.contains("已生成知乎热榜大屏") && summary.contains(".html")
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 == "mode" && message == "zeroclaw_process_message_primary"
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 screen_html_export"
)
}));
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::Command { action, .. } if action == &Action::Eval
)
}));
assert!(!sent.iter().any(|message| {
@@ -2367,97 +2344,34 @@ fn handle_browser_message_runs_zhihu_hotlist_export_via_zeroclaw_primary_orchest
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-orchestrated.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万"],
[3, "问题三", "181万"]
],
"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()),
);
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(
1,
json!({ "text": "知乎热榜\n1 问题一 344万热度\n2 问题二 266万热度\n3 问题三 181万热度" }),
),
success_browser_response(
2,
json!({ "text": "知乎热榜\n1\n问题一\n344万热度\n2\n问题二\n266万热度\n3\n问题三\n181万热度" }),
json!({
"text": {
"source": "https://www.zhihu.com/hot",
"sheet_name": "知乎热榜",
"columns": ["rank", "title", "heat"],
"rows": [
[1, "问题一", "344万"],
[2, "问题二", "266万"],
[3, "问题三", "181万"]
]
}
}),
),
]));
let browser_tool = BrowserPipeTool::new(
@@ -2480,19 +2394,13 @@ fn handle_browser_message_runs_zhihu_hotlist_export_via_zeroclaw_primary_orchest
},
)
.unwrap();
server_handle.join().unwrap();
let sent = transport.sent_messages();
let summary = sent
.iter()
.find_map(|message| match message {
AgentMessage::TaskComplete { success, summary } if *success => Some(summary.clone()),
_ => None,
})
.expect("expected successful task completion");
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!(
@@ -2621,20 +2529,34 @@ fn browser_submit_path_prefers_zeroclaw_process_message_orchestrator_for_zhihu_p
#[test]
fn zhihu_publish_task_matches_primary_orchestration_gate() {
assert!(sgclaw::compat::orchestration::should_use_primary_orchestration(
"请直接发表这篇知乎文章,标题是测试标题,正文是第一段内容",
Some("https://www.zhihu.com/"),
Some("知乎"),
));
assert!(
sgclaw::compat::orchestration::should_use_primary_orchestration(
"请直接发表这篇知乎文章,标题是测试标题,正文是第一段内容",
Some("https://www.zhihu.com/"),
Some("知乎"),
)
);
}
#[test]
fn zhihu_article_entry_task_matches_primary_orchestration_gate() {
assert!(sgclaw::compat::orchestration::should_use_primary_orchestration(
"打开知乎发文章页面",
Some("https://www.zhihu.com/"),
Some("知乎"),
assert!(
sgclaw::compat::orchestration::should_use_primary_orchestration(
"打开知乎发文章页面",
Some("https://www.zhihu.com/"),
Some("知乎"),
)
);
}
#[test]
fn zhihu_hotlist_export_routes_prefer_direct_execution() {
use sgclaw::compat::workflow_executor::{prefers_direct_execution, WorkflowRoute};
assert!(prefers_direct_execution(
&WorkflowRoute::ZhihuHotlistExportXlsx
));
assert!(prefers_direct_execution(&WorkflowRoute::ZhihuHotlistScreen));
}
#[test]
@@ -2684,9 +2606,9 @@ fn zhihu_publish_without_article_inputs_returns_missing_fields_prompt() {
summary.contains("正文")
)
}));
assert!(!sent.iter().any(|message| {
matches!(message, AgentMessage::Command { .. })
}));
assert!(!sent
.iter()
.any(|message| { matches!(message, AgentMessage::Command { .. }) }));
}
#[test]
@@ -2748,7 +2670,8 @@ fn zhihu_publish_accepts_literal_backslash_n_between_title_and_body() {
&browser_tool,
&runtime_context,
BrowserMessage::SubmitTask {
instruction: "标题ai时代普通人如何自救 \\n正文第一段内容。 第二段内容。".to_string(),
instruction: "标题ai时代普通人如何自救 \\n正文第一段内容。 第二段内容。"
.to_string(),
conversation_id: String::new(),
messages: vec![],
page_url: "https://www.zhihu.com/creator".to_string(),
@@ -3055,9 +2978,9 @@ fn zhihu_publish_without_confirmation_returns_confirmation_before_any_browser_pr
if *success && summary.contains("确认发布")
)
}));
assert!(!sent.iter().any(|message| {
matches!(message, AgentMessage::Command { .. })
}));
assert!(!sent
.iter()
.any(|message| { matches!(message, AgentMessage::Command { .. }) }));
assert!(!sent.iter().any(|message| {
matches!(
message,
@@ -3086,7 +3009,10 @@ fn zhihu_publish_after_confirmation_reports_login_block_without_selector_probing
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, "url": "https://www.zhihu.com/signin?next=%2Fcreator" })),
success_browser_response(
1,
json!({ "navigated": true, "url": "https://www.zhihu.com/signin?next=%2Fcreator" }),
),
success_browser_response(
2,
json!({
@@ -3200,11 +3126,8 @@ fn browser_orchestration_registers_superrpa_tools_natively() {
}
}]
});
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![
first_response,
second_response,
third_response,
]);
let (base_url, requests, server_handle) =
start_fake_deepseek_server(vec![first_response, second_response, third_response]);
let workspace_root = temp_workspace_root();
let config_path = write_deepseek_config_with_skills_dir(
@@ -3216,9 +3139,10 @@ fn browser_orchestration_registers_superrpa_tools_natively() {
);
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\n问题一\n344万热度" }),
)]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
zhihu_test_policy(),
@@ -3513,11 +3437,8 @@ fn handle_browser_message_executes_real_zhihu_navigate_skill_flow() {
}
}]
});
let (base_url, requests, server_handle) = start_fake_deepseek_server(vec![
first_response,
second_response,
third_response,
]);
let (base_url, requests, server_handle) =
start_fake_deepseek_server(vec![first_response, second_response, third_response]);
let workspace_root = temp_workspace_root();
let skills_dir = real_skill_lib_root();
@@ -3709,9 +3630,14 @@ fn handle_browser_message_executes_real_zhihu_write_skill_flow() {
params["url"].as_str() == Some("https://zhuanlan.zhihu.com/write")
)
}));
assert!(sent.iter().filter(|message| {
matches!(message, AgentMessage::Command { action, .. } if action == &Action::Eval)
}).count() >= 2);
assert!(
sent.iter()
.filter(|message| {
matches!(message, AgentMessage::Command { action, .. } if action == &Action::Eval)
})
.count()
>= 2
);
assert!(!sent.iter().any(|message| {
matches!(
message,