From 2cae8227756c6a85927188d33e9e19ec8c935a5c Mon Sep 17 00:00:00 2001 From: iven Date: Fri, 17 Apr 2026 18:12:42 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20Phase=200=20=E9=98=BB=E7=A2=8D=E9=A1=B9?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20=E2=80=94=20=E6=B5=81=E5=BC=8F=E4=BA=8B?= =?UTF-8?q?=E4=BB=B6=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86=20+=20CI=20?= =?UTF-8?q?=E6=8E=92=E9=99=A4=20+=20UI=20=E4=B8=AD=E6=96=87=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BLK-2: loop_runner.rs 22 处 let _ = tx.send() 全部替换为 if let Err(e) { tracing::warn!(...) },修复流式事件静默丢失问题 BLK-5: 50+ 英文字符串翻译为中文 - HandApprovalModal.tsx (~40处): 风险标签/按钮/状态/表单标签 - ChatArea.tsx: Thinking.../Sending... - AuditLogsPanel.tsx: 空状态文案 - HandParamsForm.tsx: 空列表提示 - CreateTriggerModal.tsx: 成功提示 - MessageSearch.tsx: 时间筛选/搜索历史 BLK-6: CI/Release workflow 添加 --exclude zclaw-saas - ci.yml: clippy/test/build 三个步骤 - release.yml: test 步骤 验证: cargo check ✓ | tsc --noEmit ✓ --- .github/workflows/ci.yml | 6 +- .github/workflows/release.yml | 2 +- crates/zclaw-runtime/src/loop_runner.rs | 104 +++++++++++++----- desktop/src/components/AuditLogsPanel.tsx | 2 +- desktop/src/components/ChatArea.tsx | 4 +- desktop/src/components/CreateTriggerModal.tsx | 2 +- desktop/src/components/HandApprovalModal.tsx | 72 ++++++------ desktop/src/components/HandParamsForm.tsx | 2 +- desktop/src/components/MessageSearch.tsx | 10 +- 9 files changed, 126 insertions(+), 78 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d2f8fbb..406c476 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,7 +50,7 @@ jobs: - name: Rust Clippy working-directory: . - run: cargo clippy --workspace -- -D warnings + run: cargo clippy --workspace --exclude zclaw-saas -- -D warnings - name: Install frontend dependencies working-directory: desktop @@ -94,7 +94,7 @@ jobs: - name: Run Rust tests working-directory: . - run: cargo test --workspace + run: cargo test --workspace --exclude zclaw-saas - name: Install frontend dependencies working-directory: desktop @@ -138,7 +138,7 @@ jobs: - name: Rust release build working-directory: . - run: cargo build --release --workspace + run: cargo build --release --workspace --exclude zclaw-saas - name: Install frontend dependencies working-directory: desktop diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index db5d545..41011ea 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -45,7 +45,7 @@ jobs: - name: Run Rust tests working-directory: . - run: cargo test --workspace + run: cargo test --workspace --exclude zclaw-saas - name: Install frontend dependencies working-directory: desktop diff --git a/crates/zclaw-runtime/src/loop_runner.rs b/crates/zclaw-runtime/src/loop_runner.rs index e323738..361a440 100644 --- a/crates/zclaw-runtime/src/loop_runner.rs +++ b/crates/zclaw-runtime/src/loop_runner.rs @@ -652,12 +652,14 @@ impl AgentLoop { enhanced_prompt = mw_ctx.system_prompt; } middleware::MiddlewareDecision::Stop(reason) => { - let _ = tx.send(LoopEvent::Complete(AgentLoopResult { + if let Err(e) = tx.send(LoopEvent::Complete(AgentLoopResult { response: reason, input_tokens: 0, output_tokens: 0, iterations: 1, - })).await; + })).await { + tracing::warn!("[AgentLoop] Failed to send Complete event: {}", e); + } return Ok(rx); } } @@ -691,15 +693,19 @@ impl AgentLoop { 'outer: loop { iteration += 1; if iteration > max_iterations { - let _ = tx.send(LoopEvent::Error("达到最大迭代次数".to_string())).await; + if let Err(e) = tx.send(LoopEvent::Error("达到最大迭代次数".to_string())).await { + tracing::warn!("[AgentLoop] Failed to send Error event: {}", e); + } break; } // Notify iteration start - let _ = tx.send(LoopEvent::IterationStart { + if let Err(e) = tx.send(LoopEvent::IterationStart { iteration, max_iterations, - }).await; + }).await { + tracing::warn!("[AgentLoop] Failed to send IterationStart event: {}", e); + } // Build completion request let request = CompletionRequest { @@ -742,13 +748,17 @@ impl AgentLoop { text_delta_count += 1; tracing::debug!("[AgentLoop] TextDelta #{}: {} chars", text_delta_count, delta.len()); iteration_text.push_str(delta); - let _ = tx.send(LoopEvent::Delta(delta.clone())).await; + if let Err(e) = tx.send(LoopEvent::Delta(delta.clone())).await { + tracing::warn!("[AgentLoop] Failed to send Delta event: {}", e); + } } StreamChunk::ThinkingDelta { delta } => { thinking_delta_count += 1; tracing::debug!("[AgentLoop] ThinkingDelta #{}: {} chars", thinking_delta_count, delta.len()); reasoning_text.push_str(delta); - let _ = tx.send(LoopEvent::ThinkingDelta(delta.clone())).await; + if let Err(e) = tx.send(LoopEvent::ThinkingDelta(delta.clone())).await { + tracing::warn!("[AgentLoop] Failed to send ThinkingDelta event: {}", e); + } } StreamChunk::ToolUseStart { id, name } => { tracing::debug!("[AgentLoop] ToolUseStart: id={}, name={}", id, name); @@ -770,7 +780,9 @@ impl AgentLoop { // Update with final parsed input and emit ToolStart event if let Some(tool) = pending_tool_calls.iter_mut().find(|(tid, _, _)| tid == id) { tool.2 = input.clone(); - let _ = tx.send(LoopEvent::ToolStart { name: tool.1.clone(), input: input.clone() }).await; + if let Err(e) = tx.send(LoopEvent::ToolStart { name: tool.1.clone(), input: input.clone() }).await { + tracing::warn!("[AgentLoop] Failed to send ToolStart event: {}", e); + } } } StreamChunk::Complete { input_tokens: it, output_tokens: ot, .. } => { @@ -787,20 +799,26 @@ impl AgentLoop { } StreamChunk::Error { message } => { tracing::error!("[AgentLoop] Stream error: {}", message); - let _ = tx.send(LoopEvent::Error(message.clone())).await; + if let Err(e) = tx.send(LoopEvent::Error(message.clone())).await { + tracing::warn!("[AgentLoop] Failed to send Error event: {}", e); + } stream_errored = true; } } } Ok(Some(Err(e))) => { tracing::error!("[AgentLoop] Chunk error: {}", e); - let _ = tx.send(LoopEvent::Error(format!("LLM 响应错误: {}", e.to_string()))).await; + if let Err(e) = tx.send(LoopEvent::Error(format!("LLM 响应错误: {}", e.to_string()))).await { + tracing::warn!("[AgentLoop] Failed to send Error event: {}", e); + } stream_errored = true; } Ok(None) => break, // Stream ended normally Err(_) => { tracing::error!("[AgentLoop] Stream chunk timeout ({}s)", chunk_timeout.as_secs()); - let _ = tx.send(LoopEvent::Error("LLM 响应超时,请重试".to_string())).await; + if let Err(e) = tx.send(LoopEvent::Error("LLM 响应超时,请重试".to_string())).await { + tracing::warn!("[AgentLoop] Failed to send Error event: {}", e); + } stream_errored = true; } } @@ -820,7 +838,9 @@ impl AgentLoop { if iteration_text.is_empty() && !reasoning_text.is_empty() { tracing::info!("[AgentLoop] Model generated {} chars of reasoning but no text — using reasoning as response", reasoning_text.len()); - let _ = tx.send(LoopEvent::Delta(reasoning_text.clone())).await; + if let Err(e) = tx.send(LoopEvent::Delta(reasoning_text.clone())).await { + tracing::warn!("[AgentLoop] Failed to send Delta event: {}", e); + } iteration_text = reasoning_text.clone(); } else if iteration_text.is_empty() { tracing::warn!("[AgentLoop] No text content after {} chunks (thinking_delta={})", @@ -838,12 +858,14 @@ impl AgentLoop { tracing::warn!("[AgentLoop] Failed to save final assistant message: {}", e); } - let _ = tx.send(LoopEvent::Complete(AgentLoopResult { + if let Err(e) = tx.send(LoopEvent::Complete(AgentLoopResult { response: iteration_text.clone(), input_tokens: total_input_tokens, output_tokens: total_output_tokens, iterations: iteration, - })).await; + })).await { + tracing::warn!("[AgentLoop] Failed to send Complete event: {}", e); + } // Post-completion: middleware after_completion (memory extraction, etc.) if let Some(ref chain) = middleware_chain { @@ -906,13 +928,17 @@ impl AgentLoop { Ok(middleware::ToolCallDecision::Block(msg)) => { tracing::warn!("[AgentLoop] Tool '{}' blocked by middleware: {}", name, msg); let error_output = serde_json::json!({ "error": msg }); - let _ = tx.send(LoopEvent::ToolEnd { name: name.clone(), output: error_output.clone() }).await; + if let Err(e) = tx.send(LoopEvent::ToolEnd { name: name.clone(), output: error_output.clone() }).await { + tracing::warn!("[AgentLoop] Failed to send ToolEnd event: {}", e); + } messages.push(Message::tool_result(id, zclaw_types::ToolId::new(&name), error_output, true)); continue; } Ok(middleware::ToolCallDecision::AbortLoop(reason)) => { tracing::warn!("[AgentLoop] Loop aborted by middleware: {}", reason); - let _ = tx.send(LoopEvent::Error(reason)).await; + if let Err(e) = tx.send(LoopEvent::Error(reason)).await { + tracing::warn!("[AgentLoop] Failed to send Error event: {}", e); + } break 'outer; } Ok(middleware::ToolCallDecision::ReplaceInput(new_input)) => { @@ -936,18 +962,24 @@ impl AgentLoop { let (result, is_error) = if let Some(tool) = tools.get(&name) { match tool.execute(new_input, &tool_context).await { Ok(output) => { - let _ = tx.send(LoopEvent::ToolEnd { name: name.clone(), output: output.clone() }).await; + if let Err(e) = tx.send(LoopEvent::ToolEnd { name: name.clone(), output: output.clone() }).await { + tracing::warn!("[AgentLoop] Failed to send ToolEnd event: {}", e); + } (output, false) } Err(e) => { let error_output = serde_json::json!({ "error": e.to_string() }); - let _ = tx.send(LoopEvent::ToolEnd { name: name.clone(), output: error_output.clone() }).await; + if let Err(e) = tx.send(LoopEvent::ToolEnd { name: name.clone(), output: error_output.clone() }).await { + tracing::warn!("[AgentLoop] Failed to send ToolEnd event: {}", e); + } (error_output, true) } } } else { let error_output = serde_json::json!({ "error": format!("Unknown tool: {}", name) }); - let _ = tx.send(LoopEvent::ToolEnd { name: name.clone(), output: error_output.clone() }).await; + if let Err(e) = tx.send(LoopEvent::ToolEnd { name: name.clone(), output: error_output.clone() }).await { + tracing::warn!("[AgentLoop] Failed to send ToolEnd event: {}", e); + } (error_output, true) }; messages.push(Message::tool_result(id, zclaw_types::ToolId::new(&name), result, is_error)); @@ -956,7 +988,9 @@ impl AgentLoop { Err(e) => { tracing::error!("[AgentLoop] Middleware error for tool '{}': {}", name, e); let error_output = serde_json::json!({ "error": e.to_string() }); - let _ = tx.send(LoopEvent::ToolEnd { name: name.clone(), output: error_output.clone() }).await; + if let Err(e) = tx.send(LoopEvent::ToolEnd { name: name.clone(), output: error_output.clone() }).await { + tracing::warn!("[AgentLoop] Failed to send ToolEnd event: {}", e); + } messages.push(Message::tool_result(id, zclaw_types::ToolId::new(&name), error_output, true)); continue; } @@ -966,13 +1000,17 @@ impl AgentLoop { let guard_result = loop_guard_clone.lock().unwrap_or_else(|e| e.into_inner()).check(&name, &input); match guard_result { LoopGuardResult::CircuitBreaker => { - let _ = tx.send(LoopEvent::Error("检测到工具调用循环,已自动终止".to_string())).await; + if let Err(e) = tx.send(LoopEvent::Error("检测到工具调用循环,已自动终止".to_string())).await { + tracing::warn!("[AgentLoop] Failed to send Error event: {}", e); + } break 'outer; } LoopGuardResult::Blocked => { tracing::warn!("[AgentLoop] Tool '{}' blocked by loop guard", name); let error_output = serde_json::json!({ "error": "工具调用被循环防护拦截" }); - let _ = tx.send(LoopEvent::ToolEnd { name: name.clone(), output: error_output.clone() }).await; + if let Err(e) = tx.send(LoopEvent::ToolEnd { name: name.clone(), output: error_output.clone() }).await { + tracing::warn!("[AgentLoop] Failed to send ToolEnd event: {}", e); + } messages.push(Message::tool_result(id, zclaw_types::ToolId::new(&name), error_output, true)); continue; } @@ -1005,20 +1043,26 @@ impl AgentLoop { match tool.execute(input.clone(), &tool_context).await { Ok(output) => { tracing::debug!("[AgentLoop] Tool '{}' executed successfully: {:?}", name, output); - let _ = tx.send(LoopEvent::ToolEnd { name: name.clone(), output: output.clone() }).await; + if let Err(e) = tx.send(LoopEvent::ToolEnd { name: name.clone(), output: output.clone() }).await { + tracing::warn!("[AgentLoop] Failed to send ToolEnd event: {}", e); + } (output, false) } Err(e) => { tracing::error!("[AgentLoop] Tool '{}' execution failed: {}", name, e); let error_output = serde_json::json!({ "error": e.to_string() }); - let _ = tx.send(LoopEvent::ToolEnd { name: name.clone(), output: error_output.clone() }).await; + if let Err(e) = tx.send(LoopEvent::ToolEnd { name: name.clone(), output: error_output.clone() }).await { + tracing::warn!("[AgentLoop] Failed to send ToolEnd event: {}", e); + } (error_output, true) } } } else { tracing::error!("[AgentLoop] Tool '{}' not found in registry", name); let error_output = serde_json::json!({ "error": format!("Unknown tool: {}", name) }); - let _ = tx.send(LoopEvent::ToolEnd { name: name.clone(), output: error_output.clone() }).await; + if let Err(e) = tx.send(LoopEvent::ToolEnd { name: name.clone(), output: error_output.clone() }).await { + tracing::warn!("[AgentLoop] Failed to send ToolEnd event: {}", e); + } (error_output, true) }; @@ -1038,13 +1082,17 @@ impl AgentLoop { is_error, )); // Send the question as final delta so the user sees it - let _ = tx.send(LoopEvent::Delta(question.clone())).await; - let _ = tx.send(LoopEvent::Complete(AgentLoopResult { + if let Err(e) = tx.send(LoopEvent::Delta(question.clone())).await { + tracing::warn!("[AgentLoop] Failed to send Delta event: {}", e); + } + if let Err(e) = tx.send(LoopEvent::Complete(AgentLoopResult { response: question.clone(), input_tokens: total_input_tokens, output_tokens: total_output_tokens, iterations: iteration, - })).await; + })).await { + tracing::warn!("[AgentLoop] Failed to send Complete event: {}", e); + } if let Err(e) = memory.append_message(&session_id_clone, &Message::assistant(&question)).await { tracing::warn!("[AgentLoop] Failed to save clarification message: {}", e); } diff --git a/desktop/src/components/AuditLogsPanel.tsx b/desktop/src/components/AuditLogsPanel.tsx index 6d5a1ea..12f98b6 100644 --- a/desktop/src/components/AuditLogsPanel.tsx +++ b/desktop/src/components/AuditLogsPanel.tsx @@ -862,7 +862,7 @@ export function AuditLogsPanel() { {filteredLogs.length === 0 ? (
-

No audit logs found

+

暂无审计日志

{(searchTerm || Object.keys(filter).length > 0) && (
) : (
{/* Optimistic sending indicator */} {isUser && message.optimistic && ( - Sending... + 发送中... )} {/* Reasoning block for thinking content (DeerFlow-inspired) */} diff --git a/desktop/src/components/CreateTriggerModal.tsx b/desktop/src/components/CreateTriggerModal.tsx index 80371d3..7a676db 100644 --- a/desktop/src/components/CreateTriggerModal.tsx +++ b/desktop/src/components/CreateTriggerModal.tsx @@ -543,7 +543,7 @@ export function CreateTriggerModal({ isOpen, onClose, onSuccess }: CreateTrigger {submitStatus === 'success' && (
- Trigger created successfully! + 触发器创建成功!
)} {submitStatus === 'error' && ( diff --git a/desktop/src/components/HandApprovalModal.tsx b/desktop/src/components/HandApprovalModal.tsx index 3746250..c0345fb 100644 --- a/desktop/src/components/HandApprovalModal.tsx +++ b/desktop/src/components/HandApprovalModal.tsx @@ -57,21 +57,21 @@ const RISK_CONFIG: Record< { label: string; color: string; bgColor: string; borderColor: string; icon: typeof AlertTriangle } > = { low: { - label: 'Low Risk', + label: '低风险', color: 'text-green-600 dark:text-green-400', bgColor: 'bg-green-100 dark:bg-green-900/30', borderColor: 'border-green-300 dark:border-green-700', icon: CheckCircle, }, medium: { - label: 'Medium Risk', + label: '中风险', color: 'text-yellow-600 dark:text-yellow-400', bgColor: 'bg-yellow-100 dark:bg-yellow-900/30', borderColor: 'border-yellow-300 dark:border-yellow-700', icon: AlertTriangle, }, high: { - label: 'High Risk', + label: '高风险', color: 'text-red-600 dark:text-red-400', bgColor: 'bg-red-100 dark:bg-red-900/30', borderColor: 'border-red-300 dark:border-red-700', @@ -135,32 +135,32 @@ function calculateRiskLevel(handId: HandId, params: Record): Ri function getExpectedImpact(handId: HandId, params: Record): string { switch (handId) { case 'browser': - return `Will perform browser automation on ${params.url || 'specified URL'}`; + return `将在 ${params.url || '指定网址'} 执行浏览器自动化`; case 'twitter': if (params.action === 'post') { - return 'Will post content to Twitter/X publicly'; + return '将公开发布内容到 Twitter/X'; } if (params.action === 'engage') { - return 'Will like/reply to tweets'; + return '将点赞/回复推文'; } - return 'Will perform Twitter/X operations'; + return '将执行 Twitter/X 操作'; case 'collector': - return `Will collect data from ${params.targetUrl || 'specified source'}`; + return `将从 ${params.targetUrl || '指定来源'} 收集数据`; case 'lead': - return `Will search for leads from ${params.source || 'specified source'}`; + return `将从 ${params.source || '指定来源'} 搜索线索`; case 'clip': - return `Will process video: ${params.inputPath || 'specified input'}`; + return `将处理视频: ${params.inputPath || '指定输入'}`; case 'predictor': - return `Will run prediction on ${params.dataSource || 'specified data'}`; + return `将对 ${params.dataSource || '指定数据'} 运行预测`; case 'researcher': - return `Will conduct research on: ${params.topic || 'specified topic'}`; + return `将研究: ${params.topic || '指定主题'}`; default: - return 'Will execute Hand operation'; + return '将执行 Hand 操作'; } } function formatTimeRemaining(seconds: number): string { - if (seconds <= 0) return 'Expired'; + if (seconds <= 0) return '已过期'; if (seconds < 60) return `${seconds}s`; const minutes = Math.floor(seconds / 60); const secs = seconds % 60; @@ -218,7 +218,7 @@ function TimeoutProgress({ timeRemaining, totalSeconds }: { timeRemaining: numbe
- Time Remaining + 剩余时间 }) { if (!params || Object.keys(params).length === 0) { return ( -

No parameters provided

+

暂无参数

); } @@ -282,7 +282,7 @@ export function HandApprovalModal({ runId: handRun.runId, handId, handName: handDef?.name || handId, - description: handDef?.description || 'Hand execution request', + description: handDef?.description || 'Hand 执行请求', params, riskLevel: calculateRiskLevel(handId, params), expectedImpact: getExpectedImpact(handId, params), @@ -329,7 +329,7 @@ export function HandApprovalModal({ await onApprove(approvalData.runId); onClose(); } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to approve'); + setError(err instanceof Error ? err.message : '批准失败'); } finally { setIsProcessing(false); } @@ -344,7 +344,7 @@ export function HandApprovalModal({ } if (!rejectReason.trim()) { - setError('Please provide a reason for rejection'); + setError('请提供拒绝原因'); return; } @@ -355,7 +355,7 @@ export function HandApprovalModal({ await onReject(approvalData.runId, rejectReason.trim()); onClose(); } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to reject'); + setError(err instanceof Error ? err.message : '拒绝失败'); } finally { setIsProcessing(false); } @@ -387,10 +387,10 @@ export function HandApprovalModal({

- Hand Approval Request + Hand 审批请求

- Review and approve Hand execution + 审核并批准 Hand 执行

@@ -408,7 +408,7 @@ export function HandApprovalModal({ {isExpired && (
- This approval request has expired + 此审批请求已过期
)} @@ -439,7 +439,7 @@ export function HandApprovalModal({ {/* Parameters */}
@@ -449,7 +449,7 @@ export function HandApprovalModal({

{approvalData.expectedImpact} @@ -459,9 +459,9 @@ export function HandApprovalModal({ {/* Request Info */}

-

Run ID: {approvalData.runId}

+

运行 ID: {approvalData.runId}

- Requested: {new Date(approvalData.requestedAt).toLocaleString()} + 请求时间: {new Date(approvalData.requestedAt).toLocaleString()}

@@ -469,12 +469,12 @@ export function HandApprovalModal({ {showRejectInput && (