refactor(service): unify submit bootstrap target resolution

Use page context, deterministic plans, and direct-skill metadata as the service-owned bootstrap target precedence so callback-host startup no longer relies on line-loss text matching or the old request-url helper.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
木炎
2026-04-14 20:23:24 +08:00
parent 044d38003d
commit bd83d92480
8 changed files with 498 additions and 49 deletions

View File

@@ -1,7 +1,9 @@
use std::fs;
use std::path::Path;
use std::sync::Arc;
use reqwest::Url;
use serde::Deserialize;
use serde_json::{Map, Value};
use zeroclaw::skills::{load_skills_from_directory, SkillTool};
@@ -12,6 +14,12 @@ use crate::compat::runtime::CompatTaskContext;
use crate::config::SgClawSettings;
use crate::pipe::{BrowserPipeTool, PipeError, Transport};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct DirectSubmitBootstrapMetadata {
pub bootstrap_url: String,
pub expected_domain: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DirectSubmitOutcome {
pub success: bool,
@@ -111,6 +119,32 @@ pub fn execute_browser_script_skill_raw_output_with_browser_backend(
)
}
pub(crate) fn resolve_direct_submit_bootstrap_metadata(
configured_tool: &str,
workspace_root: &Path,
settings: &SgClawSettings,
) -> Result<Option<DirectSubmitBootstrapMetadata>, PipeError> {
let (tool, skill_root) = resolve_browser_script_skill(configured_tool, workspace_root, settings)?;
let manifest_path = skill_root.join("SKILL.toml");
let Ok(manifest) = fs::read_to_string(&manifest_path) else {
return Ok(None);
};
let Ok(parsed) = toml::from_str::<DirectSubmitSkillManifest>(&manifest) else {
return Ok(None);
};
let metadata = parsed
.tools
.into_iter()
.find(|candidate| candidate.name == tool.name)
.and_then(|candidate| candidate.metadata)
.and_then(|metadata| {
normalize_bootstrap_metadata(metadata.bootstrap_url, metadata.expected_domain)
});
Ok(metadata)
}
fn resolve_browser_script_skill(
configured_tool: &str,
workspace_root: &Path,
@@ -306,6 +340,50 @@ fn count_summary_rows(counts: Option<&Value>, sections: Option<&Value>) -> usize
.unwrap_or(0)
}
#[derive(Debug, Deserialize)]
struct DirectSubmitSkillManifest {
#[serde(default)]
tools: Vec<DirectSubmitSkillManifestTool>,
}
#[derive(Debug, Deserialize)]
struct DirectSubmitSkillManifestTool {
name: String,
#[serde(default)]
metadata: Option<DirectSubmitToolMetadata>,
}
#[derive(Debug, Deserialize)]
struct DirectSubmitToolMetadata {
#[serde(default)]
bootstrap_url: Option<String>,
#[serde(default)]
expected_domain: Option<String>,
}
fn normalize_bootstrap_metadata(
bootstrap_url: Option<String>,
expected_domain: Option<String>,
) -> Option<DirectSubmitBootstrapMetadata> {
let bootstrap_url = bootstrap_url
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())?;
let parsed = Url::parse(bootstrap_url).ok()?;
if parsed.scheme().is_empty() || parsed.host_str().is_none() {
return None;
}
Some(DirectSubmitBootstrapMetadata {
bootstrap_url: parsed.to_string(),
expected_domain: expected_domain
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToString::to_string),
})
}
fn parse_configured_tool_name(configured_tool: &str) -> Result<(&str, &str), PipeError> {
let (skill_name, tool_name) = configured_tool.split_once('.').ok_or_else(|| {
PipeError::Protocol(format!(