sgclaw: move runtime policy into config

This commit is contained in:
zyl
2026-03-29 22:38:20 +08:00
parent 54049a1e1e
commit 7d9036b2d4
9 changed files with 1095 additions and 93 deletions

View File

@@ -2,13 +2,19 @@ use reqwest::Url;
use serde_json::{json, Value};
use thiserror::Error;
use crate::config::PlannerMode;
use crate::pipe::Action;
/// Legacy deterministic planner kept for dev-only verification and fixture coverage.
/// Production browser submit flow no longer routes into this planner.
pub const LEGACY_DEV_ONLY: bool = true;
const BAIDU_URL: &str = "https://www.baidu.com";
const BAIDU_DOMAIN: &str = "www.baidu.com";
const BAIDU_INPUT_SELECTOR: &str = "#kw";
const BAIDU_SEARCH_BUTTON_SELECTOR: &str = "#su";
const ZHIHU_URL: &str = "https://www.zhihu.com/search";
const ZHIHU_HOME_URL: &str = "https://www.zhihu.com";
const ZHIHU_SEARCH_URL: &str = "https://www.zhihu.com/search";
const ZHIHU_DOMAIN: &str = "www.zhihu.com";
#[derive(Debug, Clone, PartialEq)]
@@ -25,6 +31,12 @@ pub struct TaskPlan {
pub steps: Vec<PlannedStep>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExecutionPreview {
pub summary: String,
pub steps: Vec<String>,
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum PlannerError {
#[error("unsupported instruction: {0}")]
@@ -35,10 +47,22 @@ pub enum PlannerError {
pub fn plan_instruction(instruction: &str) -> Result<TaskPlan, PlannerError> {
let trimmed = instruction.trim();
if matches_exact(trimmed, &["打开百度"]) {
return Ok(plan_homepage("已打开百度首页", BAIDU_URL, BAIDU_DOMAIN));
}
if let Some(query) = extract_query(trimmed, &["打开百度搜索", "打开百度并搜索"])? {
return Ok(plan_baidu_search(query));
}
if matches_exact(trimmed, &["打开知乎"]) {
return Ok(plan_homepage(
"已打开知乎首页",
ZHIHU_HOME_URL,
ZHIHU_DOMAIN,
));
}
if let Some(query) = extract_query(trimmed, &["打开知乎搜索", "打开知乎并搜索"])? {
return Ok(plan_zhihu_search(query));
}
@@ -46,6 +70,42 @@ pub fn plan_instruction(instruction: &str) -> Result<TaskPlan, PlannerError> {
Err(PlannerError::UnsupportedInstruction(trimmed.to_string()))
}
pub fn build_execution_preview(
mode: PlannerMode,
instruction: &str,
page_url: Option<&str>,
page_title: Option<&str>,
) -> Option<ExecutionPreview> {
if matches!(mode, PlannerMode::LegacyDeterministic) {
return None;
}
let trimmed = instruction.trim();
if crate::runtime::is_zhihu_hotlist_task(trimmed, page_url, page_title) {
return Some(build_zhihu_hotlist_preview(trimmed));
}
if let Ok(plan) = plan_instruction(trimmed) {
return Some(ExecutionPreview {
summary: format!("先规划再执行:{}", plan.summary),
steps: plan
.steps
.into_iter()
.map(|step| step.log_message)
.collect(),
});
}
Some(ExecutionPreview {
summary: "先规划再执行当前任务".to_string(),
steps: vec![
"inspect current browser context".to_string(),
"choose the required sgclaw runtime tools".to_string(),
"execute and return the concrete result".to_string(),
],
})
}
fn extract_query<'a>(
instruction: &'a str,
prefixes: &[&str],
@@ -65,6 +125,22 @@ fn extract_query<'a>(
Ok(Some(query))
}
fn matches_exact(instruction: &str, candidates: &[&str]) -> bool {
candidates.iter().any(|candidate| instruction == *candidate)
}
fn plan_homepage(summary: &str, url: &str, domain: &str) -> TaskPlan {
TaskPlan {
summary: summary.to_string(),
steps: vec![PlannedStep {
action: Action::Navigate,
params: json!({ "url": url }),
expected_domain: domain.to_string(),
log_message: format!("navigate {url}"),
}],
}
}
fn plan_baidu_search(query: &str) -> TaskPlan {
TaskPlan {
summary: format!("已在百度搜索{query}"),
@@ -96,7 +172,7 @@ fn plan_baidu_search(query: &str) -> TaskPlan {
}
fn plan_zhihu_search(query: &str) -> TaskPlan {
let url = Url::parse_with_params(ZHIHU_URL, &[("type", "content"), ("q", query)])
let url = Url::parse_with_params(ZHIHU_SEARCH_URL, &[("type", "content"), ("q", query)])
.expect("valid Zhihu search URL");
let url: String = url.into();
@@ -110,3 +186,28 @@ fn plan_zhihu_search(query: &str) -> TaskPlan {
}],
}
}
fn build_zhihu_hotlist_preview(instruction: &str) -> ExecutionPreview {
let normalized = instruction.to_ascii_lowercase();
if normalized.contains("dashboard") || instruction.contains("大屏") || instruction.contains("新标签页") {
return ExecutionPreview {
summary: "先规划再执行知乎热榜大屏生成".to_string(),
steps: vec![
"navigate https://www.zhihu.com/hot".to_string(),
"getText main".to_string(),
"call screen_html_export".to_string(),
"return generated local .html path".to_string(),
],
};
}
ExecutionPreview {
summary: "先规划再执行知乎热榜 Excel 导出".to_string(),
steps: vec![
"navigate https://www.zhihu.com/hot".to_string(),
"getText main".to_string(),
"call openxml_office".to_string(),
"return generated local .xlsx path".to_string(),
],
}
}