feat: add config-owned direct submit runtime
Keep browser-attached workflows on the configured direct-skill path and align the Zhihu export/browser regression contracts with the current ws merge state. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -132,6 +132,7 @@ pub fn execute_route_with_browser_backend(
|
||||
transport: &dyn crate::agent::AgentEventSink,
|
||||
browser_backend: Arc<dyn BrowserBackend>,
|
||||
workspace_root: &Path,
|
||||
skills_dir: &Path,
|
||||
instruction: &str,
|
||||
task_context: &CompatTaskContext,
|
||||
route: WorkflowRoute,
|
||||
@@ -140,7 +141,13 @@ pub fn execute_route_with_browser_backend(
|
||||
match route {
|
||||
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)?;
|
||||
let items = collect_hotlist_items(
|
||||
transport,
|
||||
browser_backend.as_ref(),
|
||||
skills_dir,
|
||||
top_n,
|
||||
task_context,
|
||||
)?;
|
||||
if items.is_empty() {
|
||||
return Err(PipeError::Protocol(
|
||||
"知乎热榜采集失败:未能从页面文本中解析到热榜条目".to_string(),
|
||||
@@ -155,11 +162,12 @@ pub fn execute_route_with_browser_backend(
|
||||
}
|
||||
}
|
||||
WorkflowRoute::ZhihuArticleEntry => {
|
||||
execute_zhihu_article_entry_route(transport, browser_backend.as_ref())
|
||||
execute_zhihu_article_entry_route(transport, browser_backend.as_ref(), skills_dir)
|
||||
}
|
||||
WorkflowRoute::ZhihuArticleDraft => execute_zhihu_article_route(
|
||||
transport,
|
||||
browser_backend.as_ref(),
|
||||
skills_dir,
|
||||
instruction,
|
||||
task_context,
|
||||
false,
|
||||
@@ -169,6 +177,7 @@ pub fn execute_route_with_browser_backend(
|
||||
WorkflowRoute::ZhihuArticlePublish => execute_zhihu_article_route(
|
||||
transport,
|
||||
browser_backend.as_ref(),
|
||||
skills_dir,
|
||||
instruction,
|
||||
task_context,
|
||||
true,
|
||||
@@ -179,6 +188,7 @@ pub fn execute_route_with_browser_backend(
|
||||
execute_generated_zhihu_article_publish_route(
|
||||
transport,
|
||||
browser_backend.as_ref(),
|
||||
skills_dir,
|
||||
instruction,
|
||||
task_context,
|
||||
workspace_root,
|
||||
@@ -192,6 +202,7 @@ pub fn execute_route<T: Transport + 'static>(
|
||||
transport: &T,
|
||||
browser_tool: &BrowserPipeTool<T>,
|
||||
workspace_root: &Path,
|
||||
skills_dir: &Path,
|
||||
instruction: &str,
|
||||
task_context: &CompatTaskContext,
|
||||
route: WorkflowRoute,
|
||||
@@ -203,6 +214,7 @@ pub fn execute_route<T: Transport + 'static>(
|
||||
transport,
|
||||
browser_backend,
|
||||
workspace_root,
|
||||
skills_dir,
|
||||
instruction,
|
||||
task_context,
|
||||
route,
|
||||
@@ -213,10 +225,13 @@ pub fn execute_route<T: Transport + 'static>(
|
||||
fn collect_hotlist_items(
|
||||
transport: &dyn crate::agent::AgentEventSink,
|
||||
browser_tool: &dyn BrowserBackend,
|
||||
skills_dir: &Path,
|
||||
top_n: usize,
|
||||
task_context: &CompatTaskContext,
|
||||
) -> Result<Vec<HotlistItem>, PipeError> {
|
||||
if let Some(items) = ensure_hotlist_page_ready(transport, browser_tool, top_n, task_context)? {
|
||||
if let Some(items) =
|
||||
ensure_hotlist_page_ready(transport, browser_tool, skills_dir, top_n, task_context)?
|
||||
{
|
||||
return Ok(items);
|
||||
}
|
||||
transport.send(&AgentMessage::LogEntry {
|
||||
@@ -225,7 +240,7 @@ fn collect_hotlist_items(
|
||||
})?;
|
||||
let response = browser_tool.invoke(
|
||||
Action::Eval,
|
||||
json!({ "script": load_hotlist_extractor_script(top_n)? }),
|
||||
json!({ "script": load_hotlist_extractor_script(skills_dir, top_n)? }),
|
||||
ZHIHU_DOMAIN,
|
||||
)?;
|
||||
if !response.success {
|
||||
@@ -246,6 +261,7 @@ fn collect_hotlist_items(
|
||||
fn ensure_hotlist_page_ready(
|
||||
transport: &dyn crate::agent::AgentEventSink,
|
||||
browser_tool: &dyn BrowserBackend,
|
||||
skills_dir: &Path,
|
||||
top_n: usize,
|
||||
task_context: &CompatTaskContext,
|
||||
) -> Result<Option<Vec<HotlistItem>>, PipeError> {
|
||||
@@ -268,7 +284,7 @@ fn ensure_hotlist_page_ready(
|
||||
// Best-effort wait for content to appear; ignore the boolean result –
|
||||
// we always follow up with the probe.
|
||||
let _ = poll_for_hotlist_readiness(browser_tool);
|
||||
if let Some(items) = probe_hotlist_extractor(transport, browser_tool, top_n)? {
|
||||
if let Some(items) = probe_hotlist_extractor(transport, browser_tool, skills_dir, top_n)? {
|
||||
return Ok(Some(items));
|
||||
}
|
||||
}
|
||||
@@ -277,7 +293,7 @@ fn ensure_hotlist_page_ready(
|
||||
for attempt in 0..2 {
|
||||
navigate_hotlist_page(transport, browser_tool)?;
|
||||
let _ = poll_for_hotlist_readiness(browser_tool);
|
||||
if let Some(items) = probe_hotlist_extractor(transport, browser_tool, top_n)? {
|
||||
if let Some(items) = probe_hotlist_extractor(transport, browser_tool, skills_dir, top_n)? {
|
||||
return Ok(Some(items));
|
||||
}
|
||||
last_error = Some(format!(
|
||||
@@ -304,6 +320,7 @@ fn ensure_hotlist_page_ready(
|
||||
/// reports "editor_unavailable".
|
||||
fn poll_for_editor_readiness(
|
||||
browser_tool: &dyn BrowserBackend,
|
||||
skills_dir: &Path,
|
||||
desired_mode: &str,
|
||||
) -> Result<Value, PipeError> {
|
||||
let args = json!({ "desired_mode": desired_mode });
|
||||
@@ -312,6 +329,7 @@ fn poll_for_editor_readiness(
|
||||
for attempt in 0..EDITOR_READY_POLL_ATTEMPTS {
|
||||
match execute_browser_skill_script(
|
||||
browser_tool,
|
||||
skills_dir,
|
||||
"zhihu-write",
|
||||
"prepare_article_editor.js",
|
||||
args.clone(),
|
||||
@@ -325,9 +343,7 @@ fn poll_for_editor_readiness(
|
||||
last_state = Some(state);
|
||||
}
|
||||
Err(PipeError::PipeClosed) => return Err(PipeError::PipeClosed),
|
||||
Err(_) => {
|
||||
// Script may fail while the page is still navigating; tolerate.
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
|
||||
if attempt + 1 < EDITOR_READY_POLL_ATTEMPTS {
|
||||
@@ -335,12 +351,11 @@ fn poll_for_editor_readiness(
|
||||
}
|
||||
}
|
||||
|
||||
// Return the last observed state so the caller can surface the
|
||||
// "editor_unavailable" message; or make one final attempt.
|
||||
match last_state {
|
||||
Some(state) => Ok(state),
|
||||
None => execute_browser_skill_script(
|
||||
browser_tool,
|
||||
skills_dir,
|
||||
"zhihu-write",
|
||||
"prepare_article_editor.js",
|
||||
args,
|
||||
@@ -352,6 +367,7 @@ fn poll_for_editor_readiness(
|
||||
fn probe_hotlist_extractor(
|
||||
transport: &dyn crate::agent::AgentEventSink,
|
||||
browser_tool: &dyn BrowserBackend,
|
||||
skills_dir: &Path,
|
||||
top_n: usize,
|
||||
) -> Result<Option<Vec<HotlistItem>>, PipeError> {
|
||||
transport.send(&AgentMessage::LogEntry {
|
||||
@@ -360,7 +376,7 @@ fn probe_hotlist_extractor(
|
||||
})?;
|
||||
let response = browser_tool.invoke(
|
||||
Action::Eval,
|
||||
json!({ "script": load_hotlist_extractor_script(top_n)? }),
|
||||
json!({ "script": load_hotlist_extractor_script(skills_dir, top_n)? }),
|
||||
ZHIHU_DOMAIN,
|
||||
)?;
|
||||
if !response.success {
|
||||
@@ -535,6 +551,7 @@ pub fn finalize_screen_export(
|
||||
fn execute_zhihu_article_route(
|
||||
transport: &dyn crate::agent::AgentEventSink,
|
||||
browser_tool: &dyn BrowserBackend,
|
||||
skills_dir: &Path,
|
||||
instruction: &str,
|
||||
task_context: &CompatTaskContext,
|
||||
publish_mode: bool,
|
||||
@@ -559,6 +576,7 @@ fn execute_zhihu_article_route(
|
||||
})?;
|
||||
let creator_state = execute_browser_skill_script(
|
||||
browser_tool,
|
||||
skills_dir,
|
||||
"zhihu-navigate",
|
||||
"open_creator_entry.js",
|
||||
json!({ "desired_target": "article_editor" }),
|
||||
@@ -582,6 +600,7 @@ fn execute_zhihu_article_route(
|
||||
})?;
|
||||
let editor_state = poll_for_editor_readiness(
|
||||
browser_tool,
|
||||
skills_dir,
|
||||
if publish_mode { "publish" } else { "draft" },
|
||||
)?;
|
||||
if is_login_required_payload(&editor_state) {
|
||||
@@ -600,10 +619,11 @@ fn execute_zhihu_article_route(
|
||||
message: "call zhihu-write.fill_article_draft".to_string(),
|
||||
})?;
|
||||
let fill_result = if browser_tool.supports_live_input() {
|
||||
execute_zhihu_fill_via_live_input(browser_tool, &article, publish_mode)?
|
||||
execute_zhihu_fill_via_live_input(browser_tool, skills_dir, &article, publish_mode)?
|
||||
} else {
|
||||
execute_browser_skill_script(
|
||||
browser_tool,
|
||||
skills_dir,
|
||||
"zhihu-write",
|
||||
"fill_article_draft.js",
|
||||
json!({
|
||||
@@ -641,6 +661,7 @@ fn execute_zhihu_article_route(
|
||||
fn execute_generated_zhihu_article_publish_route(
|
||||
transport: &dyn crate::agent::AgentEventSink,
|
||||
browser_tool: &dyn BrowserBackend,
|
||||
skills_dir: &Path,
|
||||
instruction: &str,
|
||||
task_context: &CompatTaskContext,
|
||||
workspace_root: &Path,
|
||||
@@ -661,6 +682,7 @@ fn execute_generated_zhihu_article_publish_route(
|
||||
execute_zhihu_article_route(
|
||||
transport,
|
||||
browser_tool,
|
||||
skills_dir,
|
||||
instruction,
|
||||
task_context,
|
||||
true,
|
||||
@@ -701,6 +723,7 @@ fn task_requests_zhihu_generated_article_publish(
|
||||
fn execute_zhihu_article_entry_route(
|
||||
transport: &dyn crate::agent::AgentEventSink,
|
||||
browser_tool: &dyn BrowserBackend,
|
||||
skills_dir: &Path,
|
||||
) -> Result<String, PipeError> {
|
||||
navigate_zhihu_page(transport, browser_tool, ZHIHU_CREATOR_URL)?;
|
||||
transport.send(&AgentMessage::LogEntry {
|
||||
@@ -709,6 +732,7 @@ fn execute_zhihu_article_entry_route(
|
||||
})?;
|
||||
let creator_state = execute_browser_skill_script(
|
||||
browser_tool,
|
||||
skills_dir,
|
||||
"zhihu-navigate",
|
||||
"open_creator_entry.js",
|
||||
json!({ "desired_target": "article_editor" }),
|
||||
@@ -730,10 +754,7 @@ fn execute_zhihu_article_entry_route(
|
||||
level: "info".to_string(),
|
||||
message: "call zhihu-write.prepare_article_editor".to_string(),
|
||||
})?;
|
||||
let editor_state = poll_for_editor_readiness(
|
||||
browser_tool,
|
||||
"draft",
|
||||
)?;
|
||||
let editor_state = poll_for_editor_readiness(browser_tool, skills_dir, "draft")?;
|
||||
if is_login_required_payload(&editor_state) {
|
||||
return Ok(build_login_block_message(payload_current_url(
|
||||
&editor_state,
|
||||
@@ -748,8 +769,9 @@ fn execute_zhihu_article_entry_route(
|
||||
)))
|
||||
}
|
||||
|
||||
fn load_hotlist_extractor_script(top_n: usize) -> Result<String, PipeError> {
|
||||
fn load_hotlist_extractor_script(skills_dir: &Path, top_n: usize) -> Result<String, PipeError> {
|
||||
load_browser_skill_script(
|
||||
skills_dir,
|
||||
"zhihu-hotlist",
|
||||
"extract_hotlist.js",
|
||||
json!({ "top_n": top_n.to_string() }),
|
||||
@@ -834,12 +856,14 @@ fn navigate_zhihu_page(
|
||||
|
||||
fn execute_browser_skill_script(
|
||||
browser_tool: &dyn BrowserBackend,
|
||||
skills_dir: &Path,
|
||||
skill_name: &str,
|
||||
script_name: &str,
|
||||
args: Value,
|
||||
expected_domain: &str,
|
||||
) -> Result<Value, PipeError> {
|
||||
let wrapped_script = load_browser_skill_script(skill_name, script_name, args)?;
|
||||
let wrapped_script =
|
||||
load_browser_skill_script(skills_dir, skill_name, script_name, args)?;
|
||||
let response = browser_tool.invoke(
|
||||
Action::Eval,
|
||||
json!({ "script": wrapped_script }),
|
||||
@@ -866,6 +890,7 @@ fn live_input_probe_script(selector_candidates: &[&str]) -> String {
|
||||
|
||||
fn execute_zhihu_fill_via_live_input(
|
||||
browser_tool: &dyn BrowserBackend,
|
||||
skills_dir: &Path,
|
||||
article: &ArticleDraft,
|
||||
publish_mode: bool,
|
||||
) -> Result<Value, PipeError> {
|
||||
@@ -1003,6 +1028,7 @@ return JSON.stringify({{status:'ok',chunks:chunks.length}});
|
||||
// enable the button after the content fill updates the editor state.
|
||||
let fill_result = execute_browser_skill_script(
|
||||
browser_tool,
|
||||
skills_dir,
|
||||
"zhihu-write",
|
||||
"fill_article_draft.js",
|
||||
json!({
|
||||
@@ -1107,6 +1133,10 @@ mod tests {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn test_skills_dir() -> &'static Path {
|
||||
Path::new("D:/data/ideaSpace/rust/sgClaw/claw/claw/skills")
|
||||
}
|
||||
|
||||
struct MockWorkflowTransport {
|
||||
sent: Mutex<Vec<AgentMessage>>,
|
||||
responses: Mutex<VecDeque<BrowserMessage>>,
|
||||
@@ -1266,6 +1296,7 @@ mod tests {
|
||||
transport.as_ref(),
|
||||
backend.clone(),
|
||||
Path::new("."),
|
||||
test_skills_dir(),
|
||||
"打开知乎写文章页面",
|
||||
&CompatTaskContext::default(),
|
||||
WorkflowRoute::ZhihuArticleEntry,
|
||||
@@ -1286,6 +1317,7 @@ mod tests {
|
||||
Action::Eval,
|
||||
json!({
|
||||
"script": load_browser_skill_script(
|
||||
test_skills_dir(),
|
||||
"zhihu-navigate",
|
||||
"open_creator_entry.js",
|
||||
json!({ "desired_target": "article_editor" })
|
||||
@@ -1298,6 +1330,7 @@ mod tests {
|
||||
Action::Eval,
|
||||
json!({
|
||||
"script": load_browser_skill_script(
|
||||
test_skills_dir(),
|
||||
"zhihu-write",
|
||||
"prepare_article_editor.js",
|
||||
json!({ "desired_mode": "draft" })
|
||||
@@ -1370,6 +1403,7 @@ mod tests {
|
||||
transport.as_ref(),
|
||||
backend.clone(),
|
||||
Path::new("."),
|
||||
test_skills_dir(),
|
||||
"打开知乎写文章页面",
|
||||
&CompatTaskContext::default(),
|
||||
WorkflowRoute::ZhihuArticleEntry,
|
||||
@@ -1390,6 +1424,7 @@ mod tests {
|
||||
Action::Eval,
|
||||
json!({
|
||||
"script": load_browser_skill_script(
|
||||
test_skills_dir(),
|
||||
"zhihu-navigate",
|
||||
"open_creator_entry.js",
|
||||
json!({ "desired_target": "article_editor" })
|
||||
@@ -1407,6 +1442,7 @@ mod tests {
|
||||
Action::Eval,
|
||||
json!({
|
||||
"script": load_browser_skill_script(
|
||||
test_skills_dir(),
|
||||
"zhihu-write",
|
||||
"prepare_article_editor.js",
|
||||
json!({ "desired_mode": "draft" })
|
||||
@@ -1495,6 +1531,7 @@ mod tests {
|
||||
let summary = execute_zhihu_article_route(
|
||||
transport.as_ref(),
|
||||
backend.as_ref(),
|
||||
test_skills_dir(),
|
||||
"标题:测试标题\n正文:第一段内容",
|
||||
&CompatTaskContext::default(),
|
||||
false,
|
||||
@@ -1625,6 +1662,7 @@ mod tests {
|
||||
let summary = execute_zhihu_article_route(
|
||||
transport.as_ref(),
|
||||
backend.as_ref(),
|
||||
test_skills_dir(),
|
||||
"标题:测试标题\n正文:第一段内容",
|
||||
&CompatTaskContext::default(),
|
||||
false,
|
||||
@@ -1655,6 +1693,7 @@ mod tests {
|
||||
assert_eq!(invocations[8].0, Action::Eval);
|
||||
assert_eq!(invocations[8].1["script"], json!(
|
||||
load_browser_skill_script(
|
||||
test_skills_dir(),
|
||||
"zhihu-write",
|
||||
"fill_article_draft.js",
|
||||
json!({
|
||||
@@ -1753,6 +1792,7 @@ mod tests {
|
||||
let _ = execute_zhihu_article_route(
|
||||
transport.as_ref(),
|
||||
backend.as_ref(),
|
||||
test_skills_dir(),
|
||||
"标题:测试标题\n正文:第一段内容\n第二段内容",
|
||||
&CompatTaskContext::default(),
|
||||
false,
|
||||
@@ -1771,6 +1811,7 @@ mod tests {
|
||||
#[test]
|
||||
fn zhihu_fill_script_checks_live_input_before_dom_fill_fallback() {
|
||||
let script = load_browser_skill_script(
|
||||
test_skills_dir(),
|
||||
"zhihu-write",
|
||||
"fill_article_draft.js",
|
||||
json!({
|
||||
@@ -1805,6 +1846,7 @@ mod tests {
|
||||
#[test]
|
||||
fn zhihu_fill_script_live_input_uses_editor_content_instead_of_whole_page_text() {
|
||||
let script = load_browser_skill_script(
|
||||
test_skills_dir(),
|
||||
"zhihu-write",
|
||||
"fill_article_draft.js",
|
||||
json!({
|
||||
@@ -1897,6 +1939,7 @@ mod tests {
|
||||
transport.as_ref(),
|
||||
backend.clone(),
|
||||
Path::new("."),
|
||||
test_skills_dir(),
|
||||
"打开知乎写文章页面",
|
||||
&CompatTaskContext::default(),
|
||||
WorkflowRoute::ZhihuArticleEntry,
|
||||
@@ -1917,6 +1960,7 @@ mod tests {
|
||||
Action::Eval,
|
||||
json!({
|
||||
"script": load_browser_skill_script(
|
||||
test_skills_dir(),
|
||||
"zhihu-navigate",
|
||||
"open_creator_entry.js",
|
||||
json!({ "desired_target": "article_editor" })
|
||||
@@ -1934,6 +1978,7 @@ mod tests {
|
||||
Action::Eval,
|
||||
json!({
|
||||
"script": load_browser_skill_script(
|
||||
test_skills_dir(),
|
||||
"zhihu-write",
|
||||
"prepare_article_editor.js",
|
||||
json!({ "desired_mode": "draft" })
|
||||
@@ -1975,7 +2020,13 @@ mod tests {
|
||||
};
|
||||
|
||||
let browser_backend = PipeBrowserBackend::from_inner(browser_tool);
|
||||
let items = collect_hotlist_items(transport.as_ref(), &browser_backend, 10, &task_context)
|
||||
let items = collect_hotlist_items(
|
||||
transport.as_ref(),
|
||||
&browser_backend,
|
||||
test_skills_dir(),
|
||||
10,
|
||||
&task_context,
|
||||
)
|
||||
.expect("hotlist collection should succeed");
|
||||
|
||||
assert_eq!(items.len(), 2);
|
||||
@@ -2029,7 +2080,13 @@ mod tests {
|
||||
};
|
||||
|
||||
let browser_backend = PipeBrowserBackend::from_inner(browser_tool);
|
||||
let items = collect_hotlist_items(transport.as_ref(), &browser_backend, 10, &task_context)
|
||||
let items = collect_hotlist_items(
|
||||
transport.as_ref(),
|
||||
&browser_backend,
|
||||
test_skills_dir(),
|
||||
10,
|
||||
&task_context,
|
||||
)
|
||||
.expect("hotlist collection should succeed after readiness polling");
|
||||
|
||||
assert_eq!(items.len(), 1);
|
||||
@@ -2098,7 +2155,13 @@ mod tests {
|
||||
};
|
||||
|
||||
let browser_backend = PipeBrowserBackend::from_inner(browser_tool);
|
||||
let items = collect_hotlist_items(transport.as_ref(), &browser_backend, 10, &task_context)
|
||||
let items = collect_hotlist_items(
|
||||
transport.as_ref(),
|
||||
&browser_backend,
|
||||
test_skills_dir(),
|
||||
10,
|
||||
&task_context,
|
||||
)
|
||||
.expect("hotlist collection should succeed after one navigation retry");
|
||||
|
||||
assert_eq!(items.len(), 1);
|
||||
@@ -2165,7 +2228,13 @@ mod tests {
|
||||
};
|
||||
|
||||
let browser_backend = PipeBrowserBackend::from_inner(browser_tool);
|
||||
let items = collect_hotlist_items(transport.as_ref(), &browser_backend, 10, &task_context)
|
||||
let items = collect_hotlist_items(
|
||||
transport.as_ref(),
|
||||
&browser_backend,
|
||||
test_skills_dir(),
|
||||
10,
|
||||
&task_context,
|
||||
)
|
||||
.expect("hotlist collection should succeed via extractor probe");
|
||||
|
||||
assert_eq!(items.len(), 1);
|
||||
@@ -2184,15 +2253,12 @@ mod tests {
|
||||
}
|
||||
|
||||
fn load_browser_skill_script(
|
||||
skills_dir: &Path,
|
||||
skill_name: &str,
|
||||
script_name: &str,
|
||||
args: Value,
|
||||
) -> Result<String, PipeError> {
|
||||
let script_path = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.unwrap_or_else(|| Path::new(env!("CARGO_MANIFEST_DIR")))
|
||||
.join("skill_lib")
|
||||
.join("skills")
|
||||
let script_path = skills_dir
|
||||
.join(skill_name)
|
||||
.join("scripts")
|
||||
.join(script_name);
|
||||
|
||||
Reference in New Issue
Block a user