Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions src-tauri/src/agent_fallback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,28 @@ fn rules_fallback(
}
}

/// Deterministic guidance for when openclaw is not installed locally.
/// This avoids sending the error to zeroclaw (which itself requires the binary)
/// and prevents hallucinated diagnoses like REGISTRY_CORRUPT.
fn local_openclaw_not_installed_guidance(operation: &str) -> GuidanceBody {
GuidanceBody {
summary: "本机未安装 OpenClaw CLI,无法执行本地实例操作。".to_string(),
actions: vec![
"前往 https://docs.openclaw.ai 按照文档安装 OpenClaw CLI。".to_string(),
"安装完成后重启 ClawPal 即可正常使用本地实例。".to_string(),
"如果只需连接远程服务器,可跳过本地安装,直接添加 SSH 远程实例。".to_string(),
],
structured_actions: vec![GuidanceAction {
label: "查看安装文档".to_string(),
action_type: "link".to_string(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use supported actionType for install-doc CTA

This guidance now emits action_type: "link", but the frontend only supports "inline_fix" | "doctor_handoff" for GuidanceAction (src/lib/types.ts:244) and GuidanceCard routes any non-inline_fix action to doctor handoff (src/components/GuidanceCard.tsx:64-77). In the local-openclaw-missing path, the button labeled “查看安装文档” will therefore open Doctor instead of the docs URL, so users still miss the intended installation guidance.

Useful? React with 👍 / 👎.

tool: None,
args: Some("https://docs.openclaw.ai".to_string()),
invoke_type: None,
context: Some(format!("本地 openclaw 未安装,操作 {} 失败", operation)),
}],
}
}

async fn probe_remote_openclaw(
pool: &SshConnectionPool,
instance_id: &str,
Expand Down Expand Up @@ -348,6 +370,23 @@ pub async fn explain_operation_error(
language: Option<String>,
) -> Result<ErrorGuidance, String> {
let lower_error = error.to_lowercase();

// Fast path: when openclaw is not installed locally, skip the LLM call
// entirely and return deterministic guidance. Without the binary, zeroclaw
// cannot run either, so the LLM would hallucinate a wrong diagnosis
// (e.g. REGISTRY_CORRUPT).
if transport != "remote_ssh" && looks_like_openclaw_binary_missing(&lower_error) {
let guidance = local_openclaw_not_installed_guidance(&operation);
let message = compose_message(&guidance.summary, &guidance.actions);
return Ok(ErrorGuidance {
message,
summary: guidance.summary,
actions: guidance.actions,
structured_actions: guidance.structured_actions,
source: "rules".to_string(),
});
}

let should_probe_openclaw =
transport == "remote_ssh" && looks_like_openclaw_binary_missing(&lower_error);
let probe = if should_probe_openclaw {
Expand Down Expand Up @@ -532,4 +571,15 @@ mod tests {
.iter()
.any(|a| a.action_type == "inline_fix" && a.tool.as_deref() == Some("clawpal")));
}

#[test]
fn local_not_installed_gives_install_guidance() {
let result = local_openclaw_not_installed_guidance("listAgents");
assert!(result.summary.contains("未安装"));
assert!(result
.actions
.iter()
.any(|a| a.contains("docs.openclaw.ai")));
assert!(result.actions.iter().any(|a| a.contains("远程")));
}
}
1 change: 1 addition & 0 deletions src/lib/guidance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export function isAlreadyExplainedGuidanceError(errorText: string): boolean {
|| text.includes("建议前往")
|| text.includes("建议打开")
|| text.includes("建议执行诊断命令")
|| text.includes("本机未安装 openclaw")
|| text.includes("recommend")
|| text.includes("next step")
|| text.includes("open doctor")
Expand Down