refactor: remove ws-only scene routing remnants
Keep the ws branch focused on websocket and Zhihu behavior by dropping staged scene-routing artifacts and restoring single-path skills dir semantics. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -176,7 +176,7 @@ pub fn run_submit_task<T: Transport + 'static>(
|
||||
|
||||
let completion = match context.load_sgclaw_settings() {
|
||||
Ok(Some(settings)) => {
|
||||
let resolved_skills_dirs =
|
||||
let resolved_skills_dir =
|
||||
resolve_skills_dir_from_sgclaw_settings(&context.workspace_root, &settings);
|
||||
let _ = sink.send(&AgentMessage::LogEntry {
|
||||
level: "info".to_string(),
|
||||
@@ -189,7 +189,7 @@ pub fn run_submit_task<T: Transport + 'static>(
|
||||
});
|
||||
let _ = sink.send(&AgentMessage::LogEntry {
|
||||
level: "info".to_string(),
|
||||
message: format!("skills dirs resolved to [{}]", resolved_skills_dirs.iter().map(|d| d.display().to_string()).collect::<Vec<_>>().join(", ")),
|
||||
message: format!("skills dir resolved to {}", resolved_skills_dir.display()),
|
||||
});
|
||||
let _ = sink.send(&AgentMessage::LogEntry {
|
||||
level: "info".to_string(),
|
||||
@@ -310,7 +310,7 @@ pub fn run_submit_task_with_browser_backend<T: Transport + 'static>(
|
||||
|
||||
let completion = match context.load_sgclaw_settings() {
|
||||
Ok(Some(settings)) => {
|
||||
let resolved_skills_dirs =
|
||||
let resolved_skills_dir =
|
||||
resolve_skills_dir_from_sgclaw_settings(&context.workspace_root, &settings);
|
||||
let _ = sink.send(&AgentMessage::LogEntry {
|
||||
level: "info".to_string(),
|
||||
@@ -323,7 +323,7 @@ pub fn run_submit_task_with_browser_backend<T: Transport + 'static>(
|
||||
});
|
||||
let _ = sink.send(&AgentMessage::LogEntry {
|
||||
level: "info".to_string(),
|
||||
message: format!("skills dirs resolved to [{}]", resolved_skills_dirs.iter().map(|d| d.display().to_string()).collect::<Vec<_>>().join(", ")),
|
||||
message: format!("skills dir resolved to {}", resolved_skills_dir.display()),
|
||||
});
|
||||
let _ = sink.send(&AgentMessage::LogEntry {
|
||||
level: "info".to_string(),
|
||||
|
||||
@@ -12,7 +12,6 @@ use crate::runtime::RuntimeProfile;
|
||||
|
||||
const SGCLAW_ZEROCLAW_WORKSPACE_DIR: &str = ".sgclaw-zeroclaw-workspace";
|
||||
const SKILLS_DIR_NAME: &str = "skills";
|
||||
const STAGED_SKILLS_DIR_NAME: &str = "skill_staging";
|
||||
|
||||
pub fn build_zeroclaw_config(
|
||||
workspace_root: &Path,
|
||||
@@ -88,41 +87,23 @@ pub fn zeroclaw_default_skills_dir(workspace_root: &Path) -> PathBuf {
|
||||
zeroclaw_workspace_dir(workspace_root).join(SKILLS_DIR_NAME)
|
||||
}
|
||||
|
||||
pub fn resolve_skills_dir(workspace_root: &Path, settings: &DeepSeekSettings) -> Vec<PathBuf> {
|
||||
resolve_skills_dir_paths(workspace_root, &settings.skills_dir)
|
||||
pub fn resolve_skills_dir(workspace_root: &Path, settings: &DeepSeekSettings) -> PathBuf {
|
||||
settings
|
||||
.skills_dir
|
||||
.as_deref()
|
||||
.map(normalize_configured_skills_dir)
|
||||
.unwrap_or_else(|| zeroclaw_default_skills_dir(workspace_root))
|
||||
}
|
||||
|
||||
pub fn resolve_skills_dir_from_sgclaw_settings(
|
||||
workspace_root: &Path,
|
||||
settings: &SgClawSettings,
|
||||
) -> Vec<PathBuf> {
|
||||
resolve_skills_dir_paths(workspace_root, &settings.skills_dir)
|
||||
}
|
||||
|
||||
pub fn resolve_scene_skills_dir_from_sgclaw_settings(
|
||||
workspace_root: &Path,
|
||||
settings: &SgClawSettings,
|
||||
) -> Vec<PathBuf> {
|
||||
resolve_skills_dir_from_sgclaw_settings(workspace_root, settings)
|
||||
.into_iter()
|
||||
.flat_map(|dir| {
|
||||
let scene_dir = resolve_scene_skills_dir_path(dir.clone());
|
||||
if scene_dir != dir {
|
||||
vec![dir, scene_dir]
|
||||
} else {
|
||||
vec![dir]
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn resolve_scene_skills_dir_path(skills_dir: PathBuf) -> PathBuf {
|
||||
let staged_skills_dir = skills_dir.join(STAGED_SKILLS_DIR_NAME).join(SKILLS_DIR_NAME);
|
||||
if staged_skills_dir.is_dir() {
|
||||
staged_skills_dir
|
||||
} else {
|
||||
skills_dir
|
||||
}
|
||||
) -> PathBuf {
|
||||
settings
|
||||
.skills_dir
|
||||
.as_deref()
|
||||
.map(normalize_configured_skills_dir)
|
||||
.unwrap_or_else(|| zeroclaw_default_skills_dir(workspace_root))
|
||||
}
|
||||
|
||||
fn normalize_configured_skills_dir(configured_dir: &Path) -> PathBuf {
|
||||
@@ -138,13 +119,3 @@ fn normalize_configured_skills_dir(configured_dir: &Path) -> PathBuf {
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_skills_dir_paths(workspace_root: &Path, configured_dirs: &[PathBuf]) -> Vec<PathBuf> {
|
||||
if configured_dirs.is_empty() {
|
||||
vec![zeroclaw_default_skills_dir(workspace_root)]
|
||||
} else {
|
||||
configured_dirs
|
||||
.iter()
|
||||
.map(|d| normalize_configured_skills_dir(d))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,12 +146,12 @@ pub async fn execute_task_with_provider(
|
||||
instruction: &str,
|
||||
task_context: &CompatTaskContext,
|
||||
config: ZeroClawConfig,
|
||||
skills_dir: Vec<PathBuf>,
|
||||
skills_dir: PathBuf,
|
||||
settings: SgClawSettings,
|
||||
) -> Result<String, PipeError> {
|
||||
let engine = RuntimeEngine::new(settings.runtime_profile);
|
||||
let browser_surface_present = engine.browser_surface_enabled();
|
||||
let loaded_skills = engine.loaded_skills(&config, &skills_dir);
|
||||
let loaded_skills = engine.loaded_skills(&config, std::slice::from_ref(&skills_dir));
|
||||
let loaded_skill_versions = loaded_skills
|
||||
.iter()
|
||||
.map(|skill| (skill.name.clone(), skill.version.clone()))
|
||||
@@ -198,7 +198,7 @@ pub async fn execute_task_with_provider(
|
||||
let mut agent = engine.build_agent(
|
||||
provider,
|
||||
&config,
|
||||
&skills_dir,
|
||||
std::slice::from_ref(&skills_dir),
|
||||
tools,
|
||||
browser_surface_present,
|
||||
instruction,
|
||||
|
||||
@@ -5,15 +5,11 @@ use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use regex::Regex;
|
||||
use reqwest::Url;
|
||||
use serde_json::{json, Value};
|
||||
use zeroclaw::skills::load_skills_from_directory;
|
||||
use zeroclaw::tools::Tool;
|
||||
|
||||
use crate::browser::{BrowserBackend, PipeBrowserBackend};
|
||||
use crate::compat::artifact_open::{open_exported_xlsx, open_local_dashboard, PostExportOpen};
|
||||
use crate::compat::browser_script_skill_tool::execute_browser_script_tool;
|
||||
use crate::compat::config_adapter::resolve_scene_skills_dir_from_sgclaw_settings;
|
||||
use crate::compat::openxml_office_tool::OpenXmlOfficeTool;
|
||||
use crate::compat::runtime::CompatTaskContext;
|
||||
use crate::compat::screen_html_export_tool::ScreenHtmlExportTool;
|
||||
@@ -27,7 +23,6 @@ const ZHIHU_EDITOR_DOMAIN: &str = "zhuanlan.zhihu.com";
|
||||
const ZHIHU_HOT_URL: &str = "https://www.zhihu.com/hot";
|
||||
const ZHIHU_CREATOR_URL: &str = "https://www.zhihu.com/creator";
|
||||
const ZHIHU_EDITOR_URL: &str = "https://zhuanlan.zhihu.com/write";
|
||||
const FAULT_DETAILS_SCENE_ID: &str = "fault-details-report";
|
||||
const HOTLIST_READY_POLL_ATTEMPTS: usize = 10;
|
||||
const HOTLIST_READY_POLL_INTERVAL: Duration = Duration::from_millis(500);
|
||||
const EDITOR_READY_POLL_ATTEMPTS: usize = 12;
|
||||
@@ -39,7 +34,6 @@ const HOTLIST_TEXT_READY_PATTERN: &str =
|
||||
r"\d+(?:\.\d+)?\s*(?:万|亿|k|K|m|M)\s*热度";
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum WorkflowRoute {
|
||||
FaultDetailsReport,
|
||||
ZhihuHotlistExportXlsx,
|
||||
ZhihuHotlistScreen,
|
||||
ZhihuArticleEntry,
|
||||
@@ -66,13 +60,6 @@ pub fn detect_route(
|
||||
page_url: Option<&str>,
|
||||
page_title: Option<&str>,
|
||||
) -> Option<WorkflowRoute> {
|
||||
if let Some(scene) = crate::runtime::match_scene_instruction(instruction) {
|
||||
if scene.id == FAULT_DETAILS_SCENE_ID
|
||||
&& matches!(scene.dispatch_mode, crate::runtime::DispatchMode::DirectBrowser)
|
||||
{
|
||||
return Some(WorkflowRoute::FaultDetailsReport);
|
||||
}
|
||||
}
|
||||
if crate::runtime::is_zhihu_hotlist_task(instruction, page_url, page_title) {
|
||||
let normalized = instruction.to_ascii_lowercase();
|
||||
if normalized.contains("dashboard")
|
||||
@@ -106,8 +93,7 @@ pub fn detect_route(
|
||||
pub fn prefers_direct_execution(route: &WorkflowRoute) -> bool {
|
||||
matches!(
|
||||
route,
|
||||
WorkflowRoute::FaultDetailsReport
|
||||
| WorkflowRoute::ZhihuHotlistExportXlsx
|
||||
WorkflowRoute::ZhihuHotlistExportXlsx
|
||||
| WorkflowRoute::ZhihuHotlistScreen
|
||||
| WorkflowRoute::ZhihuArticleEntry
|
||||
| WorkflowRoute::ZhihuArticleDraft
|
||||
@@ -133,8 +119,7 @@ pub fn should_fallback_after_summary(summary: &str, route: &WorkflowRoute) -> bo
|
||||
looks_like_denial
|
||||
|| matches!(
|
||||
route,
|
||||
WorkflowRoute::FaultDetailsReport
|
||||
| WorkflowRoute::ZhihuHotlistExportXlsx
|
||||
WorkflowRoute::ZhihuHotlistExportXlsx
|
||||
| WorkflowRoute::ZhihuHotlistScreen
|
||||
| WorkflowRoute::ZhihuArticleEntry
|
||||
| WorkflowRoute::ZhihuArticleDraft
|
||||
@@ -153,13 +138,6 @@ pub fn execute_route_with_browser_backend(
|
||||
settings: &SgClawSettings,
|
||||
) -> Result<String, PipeError> {
|
||||
match route {
|
||||
WorkflowRoute::FaultDetailsReport => execute_fault_details_route(
|
||||
browser_backend.clone(),
|
||||
instruction,
|
||||
workspace_root,
|
||||
settings,
|
||||
task_context.page_url.as_deref(),
|
||||
),
|
||||
WorkflowRoute::ZhihuHotlistExportXlsx | WorkflowRoute::ZhihuHotlistScreen => {
|
||||
let top_n = extract_top_n(instruction);
|
||||
let items = collect_hotlist_items(transport, browser_backend.as_ref(), top_n, task_context)?;
|
||||
@@ -232,157 +210,6 @@ pub fn execute_route<T: Transport + 'static>(
|
||||
)
|
||||
}
|
||||
|
||||
fn execute_fault_details_route(
|
||||
browser_backend: Arc<dyn BrowserBackend>,
|
||||
instruction: &str,
|
||||
workspace_root: &Path,
|
||||
settings: &SgClawSettings,
|
||||
page_url: Option<&str>,
|
||||
) -> Result<String, PipeError> {
|
||||
let scene = crate::runtime::match_scene_instruction(instruction).ok_or_else(|| {
|
||||
PipeError::Protocol("故障明细直连路由失败:未找到场景元数据。".to_string())
|
||||
})?;
|
||||
if scene.id != FAULT_DETAILS_SCENE_ID {
|
||||
return Err(PipeError::Protocol(format!(
|
||||
"故障明细直连路由失败:场景不匹配,got {}",
|
||||
scene.id
|
||||
)));
|
||||
}
|
||||
|
||||
let period = derive_fault_details_period(instruction).ok_or_else(|| {
|
||||
PipeError::Protocol(
|
||||
"故障明细直连路由失败:无法从当前指令安全推导必填参数 period,请明确提供例如“导出 2026-04 故障明细”。"
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let skills_dirs = resolve_scene_skills_dir_from_sgclaw_settings(workspace_root, settings);
|
||||
let skill = skills_dirs
|
||||
.iter()
|
||||
.flat_map(|dir| load_skills_from_directory(dir, true))
|
||||
.find(|skill| skill.name == scene.skill_package)
|
||||
.ok_or_else(|| {
|
||||
PipeError::Protocol(format!(
|
||||
"故障明细直连路由失败:未找到技能包 {} in [{}]",
|
||||
scene.skill_package,
|
||||
skills_dirs.iter().map(|d| d.display().to_string()).collect::<Vec<_>>().join(", ")
|
||||
))
|
||||
})?;
|
||||
let skill_root = skill
|
||||
.location
|
||||
.as_deref()
|
||||
.and_then(Path::parent)
|
||||
.ok_or_else(|| {
|
||||
PipeError::Protocol(format!(
|
||||
"故障明细直连路由失败:技能包 {} 缺少有效位置元数据",
|
||||
scene.skill_package
|
||||
))
|
||||
})?;
|
||||
let tool = skill
|
||||
.tools
|
||||
.iter()
|
||||
.find(|tool| tool.name == scene.skill_tool)
|
||||
.ok_or_else(|| {
|
||||
PipeError::Protocol(format!(
|
||||
"故障明细直连路由失败:技能包 {} 缺少工具 {}",
|
||||
scene.skill_package, scene.skill_tool
|
||||
))
|
||||
})?;
|
||||
if tool.kind != "browser_script" {
|
||||
return Err(PipeError::Protocol(format!(
|
||||
"故障明细直连路由失败:工具 {} 必须是 browser_script,当前为 {}",
|
||||
scene.skill_tool, tool.kind
|
||||
)));
|
||||
}
|
||||
|
||||
let expected_domain = fault_details_expected_domain(page_url, &scene.expected_domain)
|
||||
.ok_or_else(|| {
|
||||
PipeError::Protocol(
|
||||
"故障明细直连路由失败:无法从当前页面上下文解析可用域名。".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let runtime = tokio::runtime::Runtime::new()
|
||||
.map_err(|err| PipeError::Protocol(format!("failed to create tokio runtime: {err}")))?;
|
||||
let result = runtime
|
||||
.block_on(execute_browser_script_tool(
|
||||
tool,
|
||||
skill_root,
|
||||
browser_backend,
|
||||
json!({
|
||||
"expected_domain": expected_domain,
|
||||
"period": period,
|
||||
}),
|
||||
))
|
||||
.map_err(|err| PipeError::Protocol(err.to_string()))?;
|
||||
if !result.success {
|
||||
return Err(PipeError::Protocol(
|
||||
result
|
||||
.error
|
||||
.unwrap_or_else(|| "fault-details-report browser script failed".to_string()),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(result.output)
|
||||
}
|
||||
|
||||
fn fault_details_expected_domain(page_url: Option<&str>, fallback: &str) -> Option<String> {
|
||||
page_url
|
||||
.and_then(host_from_url)
|
||||
.or_else(|| host_from_url(fallback))
|
||||
}
|
||||
|
||||
fn host_from_url(raw: &str) -> Option<String> {
|
||||
let trimmed = raw.trim();
|
||||
if trimmed.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Ok(url) = Url::parse(trimmed) {
|
||||
return url.host_str().map(|host| host.to_ascii_lowercase());
|
||||
}
|
||||
|
||||
let host = trimmed
|
||||
.trim_start_matches("https://")
|
||||
.trim_start_matches("http://")
|
||||
.split(['/', '?', '#'])
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.split(':')
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_ascii_lowercase();
|
||||
|
||||
(!host.is_empty()).then_some(host)
|
||||
}
|
||||
|
||||
fn derive_fault_details_period(instruction: &str) -> Option<String> {
|
||||
let month_re = Regex::new(r"(20\d{2})[-/年](0?[1-9]|1[0-2])").expect("valid fault details month regex");
|
||||
let derived = month_re.captures_iter(instruction).find_map(|capture| {
|
||||
let matched = capture.get(0)?;
|
||||
let before_is_digit = instruction[..matched.start()]
|
||||
.chars()
|
||||
.next_back()
|
||||
.is_some_and(|ch| ch.is_ascii_digit());
|
||||
let after_is_digit = instruction[matched.end()..]
|
||||
.chars()
|
||||
.next()
|
||||
.is_some_and(|ch| ch.is_ascii_digit());
|
||||
if before_is_digit || after_is_digit {
|
||||
return None;
|
||||
}
|
||||
|
||||
let year = capture.get(1).map(|m| m.as_str()).unwrap_or_default();
|
||||
let month = capture
|
||||
.get(2)
|
||||
.and_then(|m| m.as_str().parse::<u32>().ok())
|
||||
.unwrap_or(1);
|
||||
Some(format!("{year}-{month:02}"))
|
||||
});
|
||||
derived
|
||||
}
|
||||
|
||||
fn collect_hotlist_items(
|
||||
transport: &dyn crate::agent::AgentEventSink,
|
||||
browser_tool: &dyn BrowserBackend,
|
||||
@@ -1275,7 +1102,7 @@ mod tests {
|
||||
"test-key".to_string(),
|
||||
"http://127.0.0.1:9".to_string(),
|
||||
"deepseek-chat".to_string(),
|
||||
Vec::new(),
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::de;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::runtime::RuntimeProfile;
|
||||
@@ -106,7 +105,7 @@ pub struct DeepSeekSettings {
|
||||
pub api_key: String,
|
||||
pub base_url: String,
|
||||
pub model: String,
|
||||
pub skills_dir: Vec<PathBuf>,
|
||||
pub skills_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl DeepSeekSettings {
|
||||
@@ -125,7 +124,7 @@ pub struct SgClawSettings {
|
||||
pub provider_api_key: String,
|
||||
pub provider_base_url: String,
|
||||
pub provider_model: String,
|
||||
pub skills_dir: Vec<PathBuf>,
|
||||
pub skills_dir: Option<PathBuf>,
|
||||
pub skills_prompt_mode: SkillsPromptMode,
|
||||
pub runtime_profile: RuntimeProfile,
|
||||
pub planner_mode: PlannerMode,
|
||||
@@ -156,7 +155,7 @@ impl SgClawSettings {
|
||||
api_key: String,
|
||||
base_url: String,
|
||||
model: String,
|
||||
skills_dir: Vec<PathBuf>,
|
||||
skills_dir: Option<PathBuf>,
|
||||
) -> Result<Self, ConfigError> {
|
||||
Self::new(
|
||||
api_key,
|
||||
@@ -199,7 +198,7 @@ impl SgClawSettings {
|
||||
api_key,
|
||||
base_url,
|
||||
model,
|
||||
Vec::new(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
@@ -284,7 +283,7 @@ impl SgClawSettings {
|
||||
config.api_key,
|
||||
config.base_url,
|
||||
config.model,
|
||||
resolve_configured_skills_dirs(config.skills_dir, config_dir),
|
||||
resolve_configured_skills_dir(config.skills_dir, config_dir),
|
||||
skills_prompt_mode,
|
||||
runtime_profile,
|
||||
planner_mode,
|
||||
@@ -302,7 +301,7 @@ impl SgClawSettings {
|
||||
api_key: String,
|
||||
base_url: String,
|
||||
model: String,
|
||||
skills_dir: Vec<PathBuf>,
|
||||
skills_dir: Option<PathBuf>,
|
||||
skills_prompt_mode: Option<SkillsPromptMode>,
|
||||
runtime_profile: Option<RuntimeProfile>,
|
||||
planner_mode: Option<PlannerMode>,
|
||||
@@ -433,18 +432,11 @@ fn parse_office_backend(raw: &str) -> Result<OfficeBackend, String> {
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_configured_skills_dirs(raw: Vec<String>, config_dir: &Path) -> Vec<PathBuf> {
|
||||
raw.into_iter()
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.map(|s| {
|
||||
let path = PathBuf::from(s.trim());
|
||||
if path.is_absolute() {
|
||||
path
|
||||
} else {
|
||||
config_dir.join(path)
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
fn resolve_configured_skills_dir(raw: Option<String>, config_dir: &Path) -> Option<PathBuf> {
|
||||
raw.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(PathBuf::from)
|
||||
.map(|path| if path.is_absolute() { path } else { config_dir.join(path) })
|
||||
}
|
||||
|
||||
fn normalize_required_value(field: &'static str, raw: String) -> Result<String, ConfigError> {
|
||||
@@ -486,49 +478,6 @@ fn normalize_enum_token(raw: &str) -> String {
|
||||
.to_ascii_lowercase()
|
||||
}
|
||||
|
||||
fn deserialize_skills_dirs<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
|
||||
where
|
||||
D: de::Deserializer<'de>,
|
||||
{
|
||||
struct StringOrVec;
|
||||
|
||||
impl<'de> de::Visitor<'de> for StringOrVec {
|
||||
type Value = Vec<String>;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("a string or array of strings")
|
||||
}
|
||||
|
||||
fn visit_str<E: de::Error>(self, value: &str) -> Result<Vec<String>, E> {
|
||||
if value.trim().is_empty() {
|
||||
Ok(Vec::new())
|
||||
} else {
|
||||
Ok(vec![value.to_string()])
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Vec<String>, A::Error> {
|
||||
let mut dirs = Vec::new();
|
||||
while let Some(value) = seq.next_element::<String>()? {
|
||||
if !value.trim().is_empty() {
|
||||
dirs.push(value);
|
||||
}
|
||||
}
|
||||
Ok(dirs)
|
||||
}
|
||||
|
||||
fn visit_none<E: de::Error>(self) -> Result<Vec<String>, E> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
fn visit_unit<E: de::Error>(self) -> Result<Vec<String>, E> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(StringOrVec)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RawSgClawSettings {
|
||||
#[serde(rename = "apiKey", default)]
|
||||
@@ -537,8 +486,8 @@ struct RawSgClawSettings {
|
||||
base_url: String,
|
||||
#[serde(default)]
|
||||
model: String,
|
||||
#[serde(rename = "skillsDir", alias = "skills_dir", default, deserialize_with = "deserialize_skills_dirs")]
|
||||
skills_dir: Vec<String>,
|
||||
#[serde(rename = "skillsDir", alias = "skills_dir", default)]
|
||||
skills_dir: Option<String>,
|
||||
#[serde(rename = "skillsPromptMode", alias = "skills_prompt_mode", default)]
|
||||
skills_prompt_mode: Option<String>,
|
||||
#[serde(rename = "runtimeProfile", alias = "runtime_profile", default)]
|
||||
|
||||
@@ -12,9 +12,8 @@ use zeroclaw::tools::{self, ReadSkillTool};
|
||||
use zeroclaw::SecurityPolicy;
|
||||
|
||||
use crate::compat::memory_adapter::build_memory;
|
||||
use crate::compat::config_adapter::resolve_scene_skills_dir_path;
|
||||
use crate::pipe::PipeError;
|
||||
use crate::runtime::{match_scene_instruction, DispatchMode, RuntimeProfile, ToolPolicy};
|
||||
use crate::runtime::{RuntimeProfile, ToolPolicy};
|
||||
|
||||
const BROWSER_ACTION_TOOL_NAME: &str = "browser_action";
|
||||
const SUPERRPA_BROWSER_TOOL_NAME: &str = "superrpa_browser";
|
||||
@@ -26,7 +25,6 @@ const ZHIHU_HOTLIST_EXECUTION_PROMPT: &str = "Zhihu hotlist execution contract:\
|
||||
const OFFICE_EXPORT_COMPLETION_PROMPT: &str = "Export completion contract:\n- This task requires a real Excel export.\n- After the Zhihu rows are available, you must call openxml_office before finishing.\n- Never fabricate, simulate, or invent substitute hotlist data when a live collection/export task fails.\n- If live collection fails, report the failure concisely instead of producing fake rows.\n- Do not stop after describing how you will parse or export the data.\n- Do not repeat the same sentence or section in your final answer.\n- Your final answer must include the generated local .xlsx path.";
|
||||
const SCREEN_EXPORT_COMPLETION_PROMPT: &str = "Presentation completion contract:\n- This task requires a real dashboard artifact.\n- After the Zhihu rows are available, you must call screen_html_export before finishing.\n- Do not stop after describing how you will render or present the data.\n- Do not repeat the same sentence or section in your final answer.\n- Your final answer must include the local .html path and the presentation object.";
|
||||
const ZHIHU_WRITE_PUBLISH_PROMPT: &str = "Zhihu article publish contract:\n- This task may publish a Zhihu article.\n- You must not click publish without explicit human confirmation in the current session.\n- If the user asked to publish but no explicit confirmation phrase is present yet, ask for confirmation concisely and stop after the confirmation request.\n- Do not keep exploring tools after you have determined that publish confirmation is missing.\n- If the user only asked to write or draft, stay in draft mode and do not treat it as publish mode.\n- Do not repeat the same sentence or section in your final answer.";
|
||||
const REPAIR_CITY_DISPATCH_EXECUTION_PROMPT: &str = "95598 repair city dispatch execution contract:\n- Treat this as a browser workflow, not a text-only task.\n- You must call `95598-repair-city-dispatch.collect_repair_orders` first when the tool is available.\n- Use generic browser probing only after the scene-specific collection tool fails or is unavailable.\n- Collect the live repair order queue before summarizing or reporting status.";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct RuntimeEngine {
|
||||
@@ -153,9 +151,6 @@ impl RuntimeEngine {
|
||||
}
|
||||
|
||||
let mut sections = vec![BROWSER_TOOL_CONTRACT_PROMPT.to_string()];
|
||||
if let Some(scene_contract) = build_scene_execution_contract(trimmed_instruction) {
|
||||
sections.push(scene_contract);
|
||||
}
|
||||
if is_zhihu_hotlist_task(trimmed_instruction, page_url, page_title) {
|
||||
sections.push(ZHIHU_HOTLIST_EXECUTION_PROMPT.to_string());
|
||||
}
|
||||
@@ -276,17 +271,6 @@ fn task_needs_local_file_read(instruction: &str) -> bool {
|
||||
normalized.contains("/home/") || normalized.contains("./") || normalized.contains("../")
|
||||
}
|
||||
|
||||
fn build_scene_execution_contract(instruction: &str) -> Option<String> {
|
||||
let scene = match_scene_instruction(instruction)?;
|
||||
if scene.id == "95598-repair-city-dispatch"
|
||||
&& matches!(scene.dispatch_mode, DispatchMode::AgentBrowser)
|
||||
{
|
||||
Some(REPAIR_CITY_DISPATCH_EXECUTION_PROMPT.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_zhihu_hotlist_task(
|
||||
instruction: &str,
|
||||
page_url: Option<&str>,
|
||||
@@ -402,14 +386,6 @@ fn load_runtime_skills(config: &ZeroClawConfig, skills_dirs: &[PathBuf]) -> Vec<
|
||||
dir,
|
||||
config.skills.allow_scripts,
|
||||
));
|
||||
|
||||
let scene_skills_dir = resolve_scene_skills_dir_path(dir.clone());
|
||||
if scene_skills_dir != *dir {
|
||||
skills.extend(zeroclaw::skills::load_skills_from_directory(
|
||||
&scene_skills_dir,
|
||||
config.skills.allow_scripts,
|
||||
));
|
||||
}
|
||||
}
|
||||
skills
|
||||
}
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
mod engine;
|
||||
mod profile;
|
||||
mod scene_registry;
|
||||
mod tool_policy;
|
||||
|
||||
pub use engine::{
|
||||
is_zhihu_hotlist_task, is_zhihu_write_task, task_requests_zhihu_article_publish, RuntimeEngine,
|
||||
};
|
||||
pub use profile::RuntimeProfile;
|
||||
pub use scene_registry::{
|
||||
load_first_slice_scene_registry, load_scene_registry_from_root, match_scene_instruction,
|
||||
match_scene_instruction_in_registry, DispatchMode, SceneRegistryEntry,
|
||||
};
|
||||
pub use tool_policy::ToolPolicy;
|
||||
|
||||
@@ -1,242 +0,0 @@
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
const STAGED_SCENE_ROOT: &str = "D:/data/ideaSpace/rust/sgClaw/claw/claw/skills/skill_staging";
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum DispatchMode {
|
||||
DirectBrowser,
|
||||
AgentBrowser,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SceneRegistryEntry {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub summary: String,
|
||||
pub tags: Vec<String>,
|
||||
pub inputs: Vec<String>,
|
||||
pub outputs: Vec<String>,
|
||||
pub skill_package: String,
|
||||
pub skill_tool: String,
|
||||
pub skill_artifact_type: String,
|
||||
pub dispatch_mode: DispatchMode,
|
||||
pub expected_domain: String,
|
||||
pub aliases: Vec<String>,
|
||||
pub default_args: Map<String, Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct SceneMetadata {
|
||||
id: String,
|
||||
name: String,
|
||||
summary: String,
|
||||
#[serde(default)]
|
||||
tags: Vec<String>,
|
||||
#[serde(default)]
|
||||
inputs: Vec<String>,
|
||||
#[serde(default)]
|
||||
outputs: Vec<String>,
|
||||
skill: SceneSkillMetadata,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct SceneSkillMetadata {
|
||||
package: String,
|
||||
tool: String,
|
||||
artifact_type: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
struct SceneMatchScore {
|
||||
matched_terms: usize,
|
||||
longest_term: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
struct SceneMatchResult {
|
||||
score: SceneMatchScore,
|
||||
has_strong_phrase_hit: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct SceneRuntimePolicy {
|
||||
scene_id: &'static str,
|
||||
dispatch_mode: DispatchMode,
|
||||
expected_domain: &'static str,
|
||||
aliases: &'static [&'static str],
|
||||
}
|
||||
|
||||
const FIRST_SLICE_POLICIES: [SceneRuntimePolicy; 2] = [
|
||||
SceneRuntimePolicy {
|
||||
scene_id: "fault-details-report",
|
||||
dispatch_mode: DispatchMode::DirectBrowser,
|
||||
expected_domain: "sgcc.example.invalid",
|
||||
aliases: &["故障明细", "故障明细报表", "导出故障明细"],
|
||||
},
|
||||
SceneRuntimePolicy {
|
||||
scene_id: "95598-repair-city-dispatch",
|
||||
dispatch_mode: DispatchMode::AgentBrowser,
|
||||
expected_domain: "95598.example.invalid",
|
||||
aliases: &["95598抢修市指", "市指抢修监测", "95598抢修队列", "95598抢修市指监测"],
|
||||
},
|
||||
];
|
||||
|
||||
pub fn load_first_slice_scene_registry() -> Vec<SceneRegistryEntry> {
|
||||
load_scene_registry_from_root(Path::new(STAGED_SCENE_ROOT))
|
||||
}
|
||||
|
||||
pub fn load_scene_registry_from_root(root: &Path) -> Vec<SceneRegistryEntry> {
|
||||
let mut registry = Vec::new();
|
||||
for policy in FIRST_SLICE_POLICIES {
|
||||
if let Some(entry) = load_scene_entry(root, &policy) {
|
||||
registry.push(entry);
|
||||
}
|
||||
}
|
||||
registry
|
||||
}
|
||||
|
||||
pub fn match_scene_instruction(instruction: &str) -> Option<SceneRegistryEntry> {
|
||||
let registry = load_first_slice_scene_registry();
|
||||
match_scene_instruction_in_registry(®istry, instruction)
|
||||
}
|
||||
|
||||
pub fn match_scene_instruction_in_registry(
|
||||
registry: &[SceneRegistryEntry],
|
||||
instruction: &str,
|
||||
) -> Option<SceneRegistryEntry> {
|
||||
let normalized_instruction = normalize_for_match(instruction);
|
||||
if normalized_instruction.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut best_match: Option<(SceneMatchResult, &SceneRegistryEntry)> = None;
|
||||
let mut ambiguous = false;
|
||||
let mut strong_phrase_hits = 0;
|
||||
|
||||
for entry in registry {
|
||||
let Some(result) = score_scene_instruction(entry, &normalized_instruction) else {
|
||||
continue;
|
||||
};
|
||||
if result.has_strong_phrase_hit {
|
||||
strong_phrase_hits += 1;
|
||||
}
|
||||
|
||||
match &best_match {
|
||||
None => {
|
||||
best_match = Some((result, entry));
|
||||
ambiguous = false;
|
||||
}
|
||||
Some((current_result, _)) if result.score > current_result.score => {
|
||||
best_match = Some((result, entry));
|
||||
ambiguous = false;
|
||||
}
|
||||
Some((current_result, current_entry)) if result.score == current_result.score => {
|
||||
if result.has_strong_phrase_hit && current_result.has_strong_phrase_hit {
|
||||
if current_entry.id != entry.id {
|
||||
ambiguous = true;
|
||||
}
|
||||
} else if result.has_strong_phrase_hit && !current_result.has_strong_phrase_hit {
|
||||
best_match = Some((result, entry));
|
||||
ambiguous = false;
|
||||
} else if current_result.has_strong_phrase_hit && !result.has_strong_phrase_hit {
|
||||
ambiguous = false;
|
||||
} else if current_entry.id != entry.id {
|
||||
ambiguous = true;
|
||||
}
|
||||
}
|
||||
Some(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
if ambiguous || strong_phrase_hits > 1 {
|
||||
None
|
||||
} else {
|
||||
best_match.map(|(_, entry)| entry.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn load_scene_entry(root: &Path, policy: &SceneRuntimePolicy) -> Option<SceneRegistryEntry> {
|
||||
let scene_path = scene_json_path(root, policy.scene_id);
|
||||
let contents = fs::read_to_string(scene_path).ok()?;
|
||||
let metadata: SceneMetadata = serde_json::from_str(&contents).ok()?;
|
||||
if metadata.id != policy.scene_id {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(SceneRegistryEntry {
|
||||
id: policy.scene_id.to_string(),
|
||||
name: metadata.name,
|
||||
summary: metadata.summary,
|
||||
tags: metadata.tags,
|
||||
inputs: metadata.inputs,
|
||||
outputs: metadata.outputs,
|
||||
skill_package: metadata.skill.package,
|
||||
skill_tool: metadata.skill.tool,
|
||||
skill_artifact_type: metadata.skill.artifact_type,
|
||||
dispatch_mode: policy.dispatch_mode,
|
||||
expected_domain: policy.expected_domain.to_string(),
|
||||
aliases: policy.aliases.iter().map(|alias| (*alias).to_string()).collect(),
|
||||
default_args: Map::new(),
|
||||
})
|
||||
}
|
||||
|
||||
fn scene_json_path(root: &Path, scene_id: &str) -> PathBuf {
|
||||
root.join("scenes").join(scene_id).join("scene.json")
|
||||
}
|
||||
|
||||
fn score_scene_instruction(
|
||||
entry: &SceneRegistryEntry,
|
||||
normalized_instruction: &str,
|
||||
) -> Option<SceneMatchResult> {
|
||||
let mut matched_terms = 0;
|
||||
let mut longest_term = 0;
|
||||
let mut has_strong_phrase_hit = false;
|
||||
|
||||
for term in candidate_match_terms(entry) {
|
||||
let normalized_term = normalize_for_match(&term);
|
||||
if normalized_term.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
if normalized_instruction.contains(&normalized_term) {
|
||||
matched_terms += 1;
|
||||
longest_term = longest_term.max(normalized_term.len());
|
||||
if normalized_term.len() >= 6 {
|
||||
has_strong_phrase_hit = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if matched_terms == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(SceneMatchResult {
|
||||
score: SceneMatchScore {
|
||||
matched_terms,
|
||||
longest_term,
|
||||
},
|
||||
has_strong_phrase_hit,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn candidate_match_terms(entry: &SceneRegistryEntry) -> Vec<String> {
|
||||
let mut terms = Vec::new();
|
||||
terms.push(entry.id.clone());
|
||||
terms.push(entry.name.clone());
|
||||
terms.push(entry.summary.clone());
|
||||
terms.extend(entry.tags.iter().cloned());
|
||||
terms.extend(entry.aliases.iter().cloned());
|
||||
terms
|
||||
}
|
||||
|
||||
fn normalize_for_match(value: &str) -> String {
|
||||
value
|
||||
.chars()
|
||||
.filter(|ch| !ch.is_whitespace() && *ch != '-' && *ch != '_' && *ch != ':')
|
||||
.flat_map(|ch| ch.to_lowercase())
|
||||
.collect()
|
||||
}
|
||||
Reference in New Issue
Block a user