fix(growth,hands,kernel,desktop): Phase 1 用户可感知修复 — 6 项断链修复
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Phase 1 修复内容: 1. Hand 执行前端字段映射 — instance_id → runId,修复 Hand 状态追踪 2. Heartbeat 痛点感知 — PAIN_POINTS_CACHE + VikingStorage 持久化 + 未解决痛点检查 3. Browser Hand 委托消息 — pending_execution → delegated_to_frontend + 中文摘要 4. 跨会话记忆检索增强 — 扩展 IdentityRecall 模式 26→43 + 弱身份信号检测 + 低结果 fallback 5. Twitter Hand 凭据持久化 — SetCredentials action + 文件持久化 + 启动恢复 6. Browser 测试修复 — 适配新的 delegated_to_frontend 响应格式 验证: cargo check ✅ | cargo test 912 PASS ✅ | tsc --noEmit ✅
This commit is contained in:
@@ -117,6 +117,56 @@ pub enum BrowserAction {
|
||||
},
|
||||
}
|
||||
|
||||
impl BrowserAction {
|
||||
pub fn action_name(&self) -> &'static str {
|
||||
match self {
|
||||
BrowserAction::Navigate { .. } => "navigate",
|
||||
BrowserAction::Click { .. } => "click",
|
||||
BrowserAction::Type { .. } => "type",
|
||||
BrowserAction::Select { .. } => "select",
|
||||
BrowserAction::Scrape { .. } => "scrape",
|
||||
BrowserAction::Screenshot { .. } => "screenshot",
|
||||
BrowserAction::FillForm { .. } => "fill_form",
|
||||
BrowserAction::Wait { .. } => "wait",
|
||||
BrowserAction::Execute { .. } => "execute",
|
||||
BrowserAction::GetSource => "get_source",
|
||||
BrowserAction::GetUrl => "get_url",
|
||||
BrowserAction::GetTitle => "get_title",
|
||||
BrowserAction::Scroll { .. } => "scroll",
|
||||
BrowserAction::Back => "back",
|
||||
BrowserAction::Forward => "forward",
|
||||
BrowserAction::Refresh => "refresh",
|
||||
BrowserAction::Hover { .. } => "hover",
|
||||
BrowserAction::PressKey { .. } => "press_key",
|
||||
BrowserAction::Upload { .. } => "upload",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn summary(&self) -> String {
|
||||
match self {
|
||||
BrowserAction::Navigate { url, .. } => format!("导航到 {}", url),
|
||||
BrowserAction::Click { selector, .. } => format!("点击 {}", selector),
|
||||
BrowserAction::Type { selector, text, .. } => format!("在 {} 输入 {}", selector, text),
|
||||
BrowserAction::Select { selector, value } => format!("在 {} 选择 {}", selector, value),
|
||||
BrowserAction::Scrape { selectors, .. } => format!("抓取 {} 个选择器", selectors.len()),
|
||||
BrowserAction::Screenshot { .. } => "截图".to_string(),
|
||||
BrowserAction::FillForm { fields, .. } => format!("填写 {} 个字段", fields.len()),
|
||||
BrowserAction::Wait { selector, .. } => format!("等待 {}", selector),
|
||||
BrowserAction::Execute { .. } => "执行脚本".to_string(),
|
||||
BrowserAction::GetSource => "获取页面源码".to_string(),
|
||||
BrowserAction::GetUrl => "获取当前URL".to_string(),
|
||||
BrowserAction::GetTitle => "获取页面标题".to_string(),
|
||||
BrowserAction::Scroll { x, y, .. } => format!("滚动到 ({},{})", x, y),
|
||||
BrowserAction::Back => "后退".to_string(),
|
||||
BrowserAction::Forward => "前进".to_string(),
|
||||
BrowserAction::Refresh => "刷新".to_string(),
|
||||
BrowserAction::Hover { selector } => format!("悬停 {}", selector),
|
||||
BrowserAction::PressKey { key } => format!("按键 {}", key),
|
||||
BrowserAction::Upload { selector, .. } => format!("上传文件到 {}", selector),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Form field definition
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FormField {
|
||||
@@ -202,151 +252,18 @@ impl Hand for BrowserHand {
|
||||
Err(e) => return Ok(HandResult::error(format!("Invalid action: {}", e))),
|
||||
};
|
||||
|
||||
// Execute based on action type
|
||||
// Note: Actual browser operations are handled via Tauri commands
|
||||
// This Hand provides a structured interface for the runtime
|
||||
match action {
|
||||
BrowserAction::Navigate { url, wait_for } => {
|
||||
Ok(HandResult::success(serde_json::json!({
|
||||
"action": "navigate",
|
||||
"url": url,
|
||||
"wait_for": wait_for,
|
||||
"status": "pending_execution"
|
||||
})))
|
||||
}
|
||||
BrowserAction::Click { selector, wait_ms } => {
|
||||
Ok(HandResult::success(serde_json::json!({
|
||||
"action": "click",
|
||||
"selector": selector,
|
||||
"wait_ms": wait_ms,
|
||||
"status": "pending_execution"
|
||||
})))
|
||||
}
|
||||
BrowserAction::Type { selector, text, clear_first } => {
|
||||
Ok(HandResult::success(serde_json::json!({
|
||||
"action": "type",
|
||||
"selector": selector,
|
||||
"text": text,
|
||||
"clear_first": clear_first,
|
||||
"status": "pending_execution"
|
||||
})))
|
||||
}
|
||||
BrowserAction::Scrape { selectors, wait_for } => {
|
||||
Ok(HandResult::success(serde_json::json!({
|
||||
"action": "scrape",
|
||||
"selectors": selectors,
|
||||
"wait_for": wait_for,
|
||||
"status": "pending_execution"
|
||||
})))
|
||||
}
|
||||
BrowserAction::Screenshot { selector, full_page } => {
|
||||
Ok(HandResult::success(serde_json::json!({
|
||||
"action": "screenshot",
|
||||
"selector": selector,
|
||||
"full_page": full_page,
|
||||
"status": "pending_execution"
|
||||
})))
|
||||
}
|
||||
BrowserAction::FillForm { fields, submit_selector } => {
|
||||
Ok(HandResult::success(serde_json::json!({
|
||||
"action": "fill_form",
|
||||
"fields": fields,
|
||||
"submit_selector": submit_selector,
|
||||
"status": "pending_execution"
|
||||
})))
|
||||
}
|
||||
BrowserAction::Wait { selector, timeout_ms } => {
|
||||
Ok(HandResult::success(serde_json::json!({
|
||||
"action": "wait",
|
||||
"selector": selector,
|
||||
"timeout_ms": timeout_ms,
|
||||
"status": "pending_execution"
|
||||
})))
|
||||
}
|
||||
BrowserAction::Execute { script, args } => {
|
||||
Ok(HandResult::success(serde_json::json!({
|
||||
"action": "execute",
|
||||
"script": script,
|
||||
"args": args,
|
||||
"status": "pending_execution"
|
||||
})))
|
||||
}
|
||||
BrowserAction::GetSource => {
|
||||
Ok(HandResult::success(serde_json::json!({
|
||||
"action": "get_source",
|
||||
"status": "pending_execution"
|
||||
})))
|
||||
}
|
||||
BrowserAction::GetUrl => {
|
||||
Ok(HandResult::success(serde_json::json!({
|
||||
"action": "get_url",
|
||||
"status": "pending_execution"
|
||||
})))
|
||||
}
|
||||
BrowserAction::GetTitle => {
|
||||
Ok(HandResult::success(serde_json::json!({
|
||||
"action": "get_title",
|
||||
"status": "pending_execution"
|
||||
})))
|
||||
}
|
||||
BrowserAction::Scroll { x, y, selector } => {
|
||||
Ok(HandResult::success(serde_json::json!({
|
||||
"action": "scroll",
|
||||
"x": x,
|
||||
"y": y,
|
||||
"selector": selector,
|
||||
"status": "pending_execution"
|
||||
})))
|
||||
}
|
||||
BrowserAction::Back => {
|
||||
Ok(HandResult::success(serde_json::json!({
|
||||
"action": "back",
|
||||
"status": "pending_execution"
|
||||
})))
|
||||
}
|
||||
BrowserAction::Forward => {
|
||||
Ok(HandResult::success(serde_json::json!({
|
||||
"action": "forward",
|
||||
"status": "pending_execution"
|
||||
})))
|
||||
}
|
||||
BrowserAction::Refresh => {
|
||||
Ok(HandResult::success(serde_json::json!({
|
||||
"action": "refresh",
|
||||
"status": "pending_execution"
|
||||
})))
|
||||
}
|
||||
BrowserAction::Hover { selector } => {
|
||||
Ok(HandResult::success(serde_json::json!({
|
||||
"action": "hover",
|
||||
"selector": selector,
|
||||
"status": "pending_execution"
|
||||
})))
|
||||
}
|
||||
BrowserAction::PressKey { key } => {
|
||||
Ok(HandResult::success(serde_json::json!({
|
||||
"action": "press_key",
|
||||
"key": key,
|
||||
"status": "pending_execution"
|
||||
})))
|
||||
}
|
||||
BrowserAction::Upload { selector, file_path } => {
|
||||
Ok(HandResult::success(serde_json::json!({
|
||||
"action": "upload",
|
||||
"selector": selector,
|
||||
"file_path": file_path,
|
||||
"status": "pending_execution"
|
||||
})))
|
||||
}
|
||||
BrowserAction::Select { selector, value } => {
|
||||
Ok(HandResult::success(serde_json::json!({
|
||||
"action": "select",
|
||||
"selector": selector,
|
||||
"value": value,
|
||||
"status": "pending_execution"
|
||||
})))
|
||||
}
|
||||
}
|
||||
// Browser automation executes on the frontend via BrowserHandCard.
|
||||
// Return the parsed action with a clear message so the LLM can inform
|
||||
// the user and the frontend can pick it up via Tauri events.
|
||||
let action_type = action.action_name();
|
||||
let summary = action.summary();
|
||||
|
||||
Ok(HandResult::success(serde_json::json!({
|
||||
"action": action_type,
|
||||
"status": "delegated_to_frontend",
|
||||
"message": format!("浏览器操作「{}」已委托给前端执行。请在 HandsPanel 中查看执行结果。", summary),
|
||||
"details": format!("{} — 需要 WebDriver 会话,由前端 BrowserHandCard 管理。", summary),
|
||||
})))
|
||||
}
|
||||
|
||||
fn is_dependency_available(&self, dep: &str) -> bool {
|
||||
@@ -600,7 +517,7 @@ mod tests {
|
||||
let result = hand.execute(&ctx, action_json).await.expect("execute");
|
||||
assert!(result.success);
|
||||
assert_eq!(result.output["action"], "navigate");
|
||||
assert_eq!(result.output["url"], "https://example.com");
|
||||
assert_eq!(result.output["status"], "delegated_to_frontend");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in New Issue
Block a user