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