feat: add generated scene skill platform hardening

This commit is contained in:
木炎
2026-04-21 23:19:06 +08:00
parent 118fc77935
commit 956f0c2b68
439 changed files with 61974 additions and 3645 deletions

View File

@@ -56,7 +56,11 @@ fn write_config(
if let Some(direct_submit_skill) = direct_submit_skill {
payload["directSubmitSkill"] = json!(direct_submit_skill);
}
fs::write(&config_path, serde_json::to_string_pretty(&payload).unwrap()).unwrap();
fs::write(
&config_path,
serde_json::to_string_pretty(&payload).unwrap(),
)
.unwrap();
config_path
}
@@ -92,9 +96,9 @@ fn start_browser_ws_server() -> (String, Arc<Mutex<Vec<String>>>, thread::JoinHa
Ok(message) => message,
Err(tungstenite::Error::ConnectionClosed)
| Err(tungstenite::Error::AlreadyClosed)
| Err(tungstenite::Error::Protocol(
ProtocolError::ResetWithoutClosingHandshake,
)) => break,
| Err(tungstenite::Error::Protocol(ProtocolError::ResetWithoutClosingHandshake)) => {
break
}
Err(err) => panic!("browser ws test server read failed: {err}"),
};
let payload = match message {
@@ -113,7 +117,9 @@ fn start_browser_ws_server() -> (String, Arc<Mutex<Vec<String>>>, thread::JoinHa
continue;
}
let values = parsed.as_array().expect("browser action frame should be an array");
let values = parsed
.as_array()
.expect("browser action frame should be an array");
let request_url = values[0].as_str().expect("request_url should be a string");
let action = values[1].as_str().expect("action should be a string");
action_count += 1;
@@ -129,7 +135,9 @@ fn start_browser_ws_server() -> (String, Arc<Mutex<Vec<String>>>, thread::JoinHa
let callback_frame = match action {
"sgHideBrowserCallAfterLoaded" => {
let target_url = values[2].as_str().expect("navigate target_url should be a string");
let target_url = values[2]
.as_str()
.expect("navigate target_url should be a string");
json!([
request_url,
"callBackJsToCpp",
@@ -139,7 +147,9 @@ fn start_browser_ws_server() -> (String, Arc<Mutex<Vec<String>>>, thread::JoinHa
])
}
"sgBrowserExcuteJsCodeByArea" => {
let target_url = values[2].as_str().expect("script target_url should be a string");
let target_url = values[2]
.as_str()
.expect("script target_url should be a string");
let response_text = if action_count == 2 {
"知乎热榜\n1 问题一 344万热度\n2 问题二 266万热度".to_string()
} else {
@@ -250,7 +260,10 @@ return {
root
}
fn write_direct_submit_config(workspace_root: &std::path::Path, skill_root: &std::path::Path) -> PathBuf {
fn write_direct_submit_config(
workspace_root: &std::path::Path,
skill_root: &std::path::Path,
) -> PathBuf {
let config_path = workspace_root.join("sgclaw_config.json");
fs::write(
&config_path,
@@ -266,10 +279,8 @@ fn write_direct_submit_config(workspace_root: &std::path::Path, skill_root: &std
}
fn direct_submit_runtime_context(skill_root: &std::path::Path) -> AgentRuntimeContext {
let workspace_root = std::env::temp_dir().join(format!(
"sgclaw-agent-runtime-workspace-{}",
Uuid::new_v4()
));
let workspace_root =
std::env::temp_dir().join(format!("sgclaw-agent-runtime-workspace-{}", Uuid::new_v4()));
fs::create_dir_all(&workspace_root).unwrap();
let config_path = write_direct_submit_config(&workspace_root, skill_root);
AgentRuntimeContext::new(Some(config_path), workspace_root)
@@ -295,6 +306,90 @@ fn submit_zhihu_hotlist_export_message() -> BrowserMessage {
}
}
fn build_manifest_scene_skill_root() -> PathBuf {
let root = std::env::temp_dir().join(format!(
"sgclaw-agent-runtime-scene-skill-root-{}",
Uuid::new_v4()
));
let skill_dir = root.join("manifest-scene-report");
let script_dir = skill_dir.join("scripts");
fs::create_dir_all(&script_dir).unwrap();
fs::write(
skill_dir.join("SKILL.toml"),
r#"
[skill]
name = "manifest-scene-report"
description = "Collect manifest scene report data."
version = "0.1.0"
[[tools]]
name = "collect_manifest_scene"
description = "Collect manifest scene report rows."
kind = "browser_script"
command = "scripts/collect_manifest_scene.js"
"#,
)
.unwrap();
fs::write(
skill_dir.join("scene.toml"),
r#"
[scene]
id = "manifest-scene-report"
skill = "manifest-scene-report"
tool = "collect_manifest_scene"
kind = "browser_script"
version = "0.1.0"
category = "report_collection"
[manifest]
schema_version = "1"
[bootstrap]
expected_domain = "manifest.example.test"
target_url = "https://manifest.example.test/report"
page_title_keywords = []
requires_target_page = true
[deterministic]
suffix = "。。。"
include_keywords = ["自定义场景报表"]
exclude_keywords = ["知乎"]
[[params]]
name = "period"
resolver = "literal_passthrough"
required = false
prompt_missing = "missing"
prompt_ambiguous = "ambiguous"
[params.resolver_config]
output_field = "period_value"
value = "2026-03"
[artifact]
type = "report-artifact"
success_status = ["ok", "partial", "empty"]
failure_status = ["blocked", "error"]
"#,
)
.unwrap();
fs::write(
script_dir.join("collect_manifest_scene.js"),
r#"
return {
type: "report-artifact",
report_name: "manifest-scene-report",
status: "ok",
columns: ["period_value"],
rows: [{ period_value: args.period_value }],
counts: { rows: 1 }
};
"#,
)
.unwrap();
root
}
fn direct_submit_mode_logs(sent: &[AgentMessage]) -> Vec<String> {
sent.iter()
.filter_map(|message| match message {
@@ -470,7 +565,10 @@ fn submit_task_uses_direct_skill_mode_without_llm_configuration() {
let sent = transport.sent_messages();
let completion = direct_submit_completion(&sent).expect("task completion");
assert!(completion.0, "expected direct submit task to succeed: {sent:?}");
assert!(
completion.0,
"expected direct submit task to succeed: {sent:?}"
);
assert!(
completion.1.contains("report artifact payload"),
"expected report artifact payload in summary: {}",
@@ -531,7 +629,9 @@ fn submit_task_rejects_invalid_direct_submit_skill_config_before_routing() {
if !success && summary.contains("skill.tool")
));
assert!(direct_submit_mode_logs(&sent).is_empty());
assert!(!sent.iter().any(|message| matches!(message, AgentMessage::Command { .. })));
assert!(!sent
.iter()
.any(|message| matches!(message, AgentMessage::Command { .. })));
}
#[test]
@@ -567,7 +667,10 @@ fn submit_task_treats_partial_report_artifact_as_success_with_warning_summary()
let sent = transport.sent_messages();
let completion = direct_submit_completion(&sent).expect("task completion");
assert!(completion.0, "expected partial artifact to succeed: {sent:?}");
assert!(
completion.0,
"expected partial artifact to succeed: {sent:?}"
);
assert!(completion.1.contains("fault-details-report"));
assert!(completion.1.contains("2026-03"));
assert!(completion.1.contains("status=partial"));
@@ -718,7 +821,10 @@ fn submit_task_routes_zhihu_hotlist_export_before_direct_submit() {
let mode_logs = direct_submit_mode_logs(&sent);
let completion = direct_submit_completion(&sent).expect("task completion");
assert_eq!(mode_logs, vec!["zeroclaw_process_message_primary".to_string()]);
assert_eq!(
mode_logs,
vec!["zeroclaw_process_message_primary".to_string()]
);
assert!(
!completion.0,
"expected zhihu export without page context to fail before browser actions: {sent:?}"
@@ -840,7 +946,10 @@ fn production_submit_task_with_ws_and_direct_submit_config_routes_zhihu_before_d
!mode_logs.iter().any(|mode| mode == "direct_skill_primary"),
"unexpected direct submit mode log for zhihu ws submit: {sent:?}"
);
assert!(completion.0, "expected zhihu ws submit to succeed: {sent:?}");
assert!(
completion.0,
"expected zhihu ws submit to succeed: {sent:?}"
);
assert!(
!completion
.1
@@ -851,6 +960,76 @@ fn production_submit_task_with_ws_and_direct_submit_config_routes_zhihu_before_d
std::env::remove_var("SGCLAW_DISABLE_POST_EXPORT_OPEN");
}
#[test]
fn submit_task_routes_configured_manifest_scene_before_llm_provider() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
std::env::remove_var("DEEPSEEK_API_KEY");
std::env::remove_var("DEEPSEEK_BASE_URL");
std::env::remove_var("DEEPSEEK_MODEL");
let skill_root = build_manifest_scene_skill_root();
let workspace_root = temp_workspace_root();
let config_path = write_config(
&workspace_root,
"deepseek-test-key",
"http://127.0.0.1:9",
"deepseek-chat",
Some(skill_root.to_str().unwrap()),
None,
None,
);
let transport = Arc::new(MockTransport::new(vec![success_browser_response(
1,
serde_json::json!({
"text": {
"type": "report-artifact",
"report_name": "manifest-scene-report",
"status": "ok",
"columns": ["period_value"],
"rows": [{"period_value": "2026-03"}],
"counts": {"rows": 1}
}
}),
)]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
policy_for_domains(&["manifest.example.test"]),
vec![1, 2, 3, 4, 5, 6, 7, 8],
)
.with_response_timeout(Duration::from_secs(1));
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root);
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: String::new(),
page_title: String::new(),
},
)
.unwrap();
let sent = transport.sent_messages();
let completion = direct_submit_completion(&sent).expect("task completion");
assert!(completion.0, "expected manifest scene success: {sent:?}");
assert!(completion.1.contains("manifest-scene-report"));
assert!(completion.1.contains("detail_rows=1"));
assert!(sent.iter().any(|message| matches!(
message,
AgentMessage::LogEntry { level, message }
if level == "mode" && message == "direct_skill_primary"
)));
assert!(!sent.iter().any(|message| matches!(
message,
AgentMessage::LogEntry { level, message }
if level == "mode" && message == "compat_llm_primary"
)));
}
#[test]
fn production_submit_task_routes_zhihu_through_ws_backend_without_helper_bootstrap() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
@@ -939,7 +1118,9 @@ fn production_submit_task_routes_zhihu_through_ws_backend_without_helper_bootstr
)
}));
std::env::remove_var("SGCLAW_DISABLE_POST_EXPORT_OPEN");
assert!(!sent.iter().any(|message| matches!(message, AgentMessage::Command { .. })));
assert!(!sent
.iter()
.any(|message| matches!(message, AgentMessage::Command { .. })));
}
#[test]
@@ -952,8 +1133,12 @@ fn lifecycle_messages_emit_status_events_without_browser_commands() {
)
.with_response_timeout(Duration::from_secs(1));
sgclaw::agent::handle_browser_message(transport.as_ref(), &browser_tool, BrowserMessage::Connect)
.unwrap();
sgclaw::agent::handle_browser_message(
transport.as_ref(),
&browser_tool,
BrowserMessage::Connect,
)
.unwrap();
sgclaw::agent::handle_browser_message(transport.as_ref(), &browser_tool, BrowserMessage::Start)
.unwrap();
sgclaw::agent::handle_browser_message(transport.as_ref(), &browser_tool, BrowserMessage::Stop)