feat: add generated scene skill platform hardening
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user