fix: Phase 0 阻碍项修复 — 流式事件错误处理 + CI 排除 + UI 中文化
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
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 ✓
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user