Files
zclaw_openfang/desktop/src-tauri/src/kernel_commands/approval.rs
iven 0a04b260a4
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
refactor(desktop): ChatStore structured split + IDB persistence + stream cancel
Split monolithic chatStore.ts (908 lines) into 4 focused stores:
- chatStore.ts: facade layer, owns messages[], backward-compatible selectors
- conversationStore.ts: conversation CRUD, agent switching, IndexedDB persistence
- streamStore.ts: streaming orchestration, chat mode, suggestions
- messageStore.ts: token tracking

Key fixes from 3-round deep audit:
- C1: Fix Rust serde camelCase vs TS snake_case mismatch (toolStart/toolEnd/iterationStart)
- C2: Fix IDB async rehydration race with persist.hasHydrated() subscribe
- C3: Add sessionKey to partialize to survive page refresh
- H3: Fix IDB migration retry on failure (don't set migrated=true in catch)
- M3: Fix ToolCallStep deduplication (toolStart creates, toolEnd updates)
- M-NEW-2: Clear sessionKey on cancelStream

Also adds:
- Rust backend stream cancellation via AtomicBool + cancel_stream command
- IndexedDB storage adapter with one-time localStorage migration
- HMR cleanup for cross-store subscriptions
2026-04-03 00:24:16 +08:00

143 lines
5.5 KiB
Rust

//! Approval commands: list and respond
//!
//! When approved, kernel's `respond_to_approval` internally spawns the Hand execution
//! and emits `hand-execution-complete` events to the frontend.
use serde::{Deserialize, Serialize};
use serde_json;
use tauri::{AppHandle, Emitter, State};
use super::KernelState;
// ============================================================
// Approval Commands
// ============================================================
/// Approval response
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApprovalResponse {
pub id: String,
pub hand_id: String,
pub status: String,
pub created_at: String,
pub input: serde_json::Value,
}
/// List pending approvals
// @reserved: 暂无前端集成
#[tauri::command]
pub async fn approval_list(
state: State<'_, KernelState>,
) -> Result<Vec<ApprovalResponse>, String> {
let kernel_lock = state.lock().await;
let kernel = kernel_lock.as_ref()
.ok_or_else(|| "Kernel not initialized".to_string())?;
let approvals = kernel.list_approvals().await;
Ok(approvals.into_iter().map(|a| ApprovalResponse {
id: a.id,
hand_id: a.hand_id,
status: a.status,
created_at: a.created_at.to_rfc3339(),
input: a.input,
}).collect())
}
/// Respond to an approval
///
/// When approved, the kernel's `respond_to_approval` internally spawns the Hand
/// execution. We additionally emit Tauri events so the frontend can track when
/// the execution finishes, since the kernel layer has no access to the AppHandle.
// @connected
#[tauri::command]
pub async fn approval_respond(
app: AppHandle,
state: State<'_, KernelState>,
id: String,
approved: bool,
reason: Option<String>,
) -> Result<(), String> {
// Capture hand info before calling respond_to_approval (which mutates the approval)
let hand_id = {
let kernel_lock = state.lock().await;
let kernel = kernel_lock.as_ref()
.ok_or_else(|| "Kernel not initialized".to_string())?;
let approvals = kernel.list_approvals().await;
let entry = approvals.iter().find(|a| a.id == id && a.status == "pending")
.ok_or_else(|| format!("Approval not found or already resolved: {}", id))?;
entry.hand_id.clone()
};
// Call kernel respond_to_approval (this updates status and spawns Hand execution)
{
let kernel_lock = state.lock().await;
let kernel = kernel_lock.as_ref()
.ok_or_else(|| "Kernel not initialized".to_string())?;
kernel.respond_to_approval(&id, approved, reason).await
.map_err(|e| format!("Failed to respond to approval: {}", e))?;
}
// When approved, monitor the Hand execution and emit events to the frontend.
// The kernel's respond_to_approval changes status to "approved" immediately,
// then the spawned task sets it to "completed" or "failed" when done.
if approved {
let approval_id = id.clone();
let kernel_state: KernelState = (*state).clone();
tokio::spawn(async move {
let timeout = tokio::time::Duration::from_secs(300);
let poll_interval = tokio::time::Duration::from_millis(500);
let result = tokio::time::timeout(timeout, async {
loop {
tokio::time::sleep(poll_interval).await;
let kernel_lock = kernel_state.lock().await;
if let Some(kernel) = kernel_lock.as_ref() {
// Use get_approval to check any status (not just "pending")
if let Some(entry) = kernel.get_approval(&approval_id).await {
match entry.status.as_str() {
"completed" => {
tracing::info!("[approval_respond] Hand '{}' completed for approval {}", hand_id, approval_id);
return (true, None::<String>);
}
"failed" => {
let error_msg = entry.input.get("error")
.and_then(|v| v.as_str())
.unwrap_or("Unknown error")
.to_string();
tracing::warn!("[approval_respond] Hand '{}' failed for approval {}: {}", hand_id, approval_id, error_msg);
return (false, Some(error_msg));
}
_ => {} // "approved" = still running
}
} else {
// Entry disappeared entirely — kernel was likely restarted
return (false, Some("Approval entry disappeared".to_string()));
}
} else {
return (false, Some("Kernel not available".to_string()));
}
}
}).await;
let (success, error) = match result {
Ok((s, e)) => (s, e),
Err(_) => (false, Some("Hand execution timed out (5 minutes)".to_string())),
};
let _ = app.emit("hand-execution-complete", serde_json::json!({
"approvalId": approval_id,
"handId": hand_id,
"success": success,
"error": error,
}));
});
}
Ok(())
}