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:
木炎
2026-04-10 22:35:43 +08:00
parent 81de162756
commit b454fa3f54
17 changed files with 107 additions and 2251 deletions

View File

@@ -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()
}
}

View File

@@ -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,

View File

@@ -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()
}