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

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:
iven
2026-04-17 18:12:42 +08:00
parent 93df380ca8
commit 2cae822775
9 changed files with 126 additions and 78 deletions

View File

@@ -50,7 +50,7 @@ jobs:
- name: Rust Clippy - name: Rust Clippy
working-directory: . working-directory: .
run: cargo clippy --workspace -- -D warnings run: cargo clippy --workspace --exclude zclaw-saas -- -D warnings
- name: Install frontend dependencies - name: Install frontend dependencies
working-directory: desktop working-directory: desktop
@@ -94,7 +94,7 @@ jobs:
- name: Run Rust tests - name: Run Rust tests
working-directory: . working-directory: .
run: cargo test --workspace run: cargo test --workspace --exclude zclaw-saas
- name: Install frontend dependencies - name: Install frontend dependencies
working-directory: desktop working-directory: desktop
@@ -138,7 +138,7 @@ jobs:
- name: Rust release build - name: Rust release build
working-directory: . working-directory: .
run: cargo build --release --workspace run: cargo build --release --workspace --exclude zclaw-saas
- name: Install frontend dependencies - name: Install frontend dependencies
working-directory: desktop working-directory: desktop

View File

@@ -45,7 +45,7 @@ jobs:
- name: Run Rust tests - name: Run Rust tests
working-directory: . working-directory: .
run: cargo test --workspace run: cargo test --workspace --exclude zclaw-saas
- name: Install frontend dependencies - name: Install frontend dependencies
working-directory: desktop working-directory: desktop

View File

@@ -652,12 +652,14 @@ impl AgentLoop {
enhanced_prompt = mw_ctx.system_prompt; enhanced_prompt = mw_ctx.system_prompt;
} }
middleware::MiddlewareDecision::Stop(reason) => { middleware::MiddlewareDecision::Stop(reason) => {
let _ = tx.send(LoopEvent::Complete(AgentLoopResult { if let Err(e) = tx.send(LoopEvent::Complete(AgentLoopResult {
response: reason, response: reason,
input_tokens: 0, input_tokens: 0,
output_tokens: 0, output_tokens: 0,
iterations: 1, iterations: 1,
})).await; })).await {
tracing::warn!("[AgentLoop] Failed to send Complete event: {}", e);
}
return Ok(rx); return Ok(rx);
} }
} }
@@ -691,15 +693,19 @@ impl AgentLoop {
'outer: loop { 'outer: loop {
iteration += 1; iteration += 1;
if iteration > max_iterations { 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; break;
} }
// Notify iteration start // Notify iteration start
let _ = tx.send(LoopEvent::IterationStart { if let Err(e) = tx.send(LoopEvent::IterationStart {
iteration, iteration,
max_iterations, max_iterations,
}).await; }).await {
tracing::warn!("[AgentLoop] Failed to send IterationStart event: {}", e);
}
// Build completion request // Build completion request
let request = CompletionRequest { let request = CompletionRequest {
@@ -742,13 +748,17 @@ impl AgentLoop {
text_delta_count += 1; text_delta_count += 1;
tracing::debug!("[AgentLoop] TextDelta #{}: {} chars", text_delta_count, delta.len()); tracing::debug!("[AgentLoop] TextDelta #{}: {} chars", text_delta_count, delta.len());
iteration_text.push_str(delta); 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 } => { StreamChunk::ThinkingDelta { delta } => {
thinking_delta_count += 1; thinking_delta_count += 1;
tracing::debug!("[AgentLoop] ThinkingDelta #{}: {} chars", thinking_delta_count, delta.len()); tracing::debug!("[AgentLoop] ThinkingDelta #{}: {} chars", thinking_delta_count, delta.len());
reasoning_text.push_str(delta); 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 } => { StreamChunk::ToolUseStart { id, name } => {
tracing::debug!("[AgentLoop] ToolUseStart: id={}, name={}", 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 // Update with final parsed input and emit ToolStart event
if let Some(tool) = pending_tool_calls.iter_mut().find(|(tid, _, _)| tid == id) { if let Some(tool) = pending_tool_calls.iter_mut().find(|(tid, _, _)| tid == id) {
tool.2 = input.clone(); 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, .. } => { StreamChunk::Complete { input_tokens: it, output_tokens: ot, .. } => {
@@ -787,20 +799,26 @@ impl AgentLoop {
} }
StreamChunk::Error { message } => { StreamChunk::Error { message } => {
tracing::error!("[AgentLoop] Stream 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; stream_errored = true;
} }
} }
} }
Ok(Some(Err(e))) => { Ok(Some(Err(e))) => {
tracing::error!("[AgentLoop] Chunk error: {}", 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; stream_errored = true;
} }
Ok(None) => break, // Stream ended normally Ok(None) => break, // Stream ended normally
Err(_) => { Err(_) => {
tracing::error!("[AgentLoop] Stream chunk timeout ({}s)", chunk_timeout.as_secs()); 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; stream_errored = true;
} }
} }
@@ -820,7 +838,9 @@ impl AgentLoop {
if iteration_text.is_empty() && !reasoning_text.is_empty() { if iteration_text.is_empty() && !reasoning_text.is_empty() {
tracing::info!("[AgentLoop] Model generated {} chars of reasoning but no text — using reasoning as response", tracing::info!("[AgentLoop] Model generated {} chars of reasoning but no text — using reasoning as response",
reasoning_text.len()); 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(); iteration_text = reasoning_text.clone();
} else if iteration_text.is_empty() { } else if iteration_text.is_empty() {
tracing::warn!("[AgentLoop] No text content after {} chunks (thinking_delta={})", 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); 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(), response: iteration_text.clone(),
input_tokens: total_input_tokens, input_tokens: total_input_tokens,
output_tokens: total_output_tokens, output_tokens: total_output_tokens,
iterations: iteration, iterations: iteration,
})).await; })).await {
tracing::warn!("[AgentLoop] Failed to send Complete event: {}", e);
}
// Post-completion: middleware after_completion (memory extraction, etc.) // Post-completion: middleware after_completion (memory extraction, etc.)
if let Some(ref chain) = middleware_chain { if let Some(ref chain) = middleware_chain {
@@ -906,13 +928,17 @@ impl AgentLoop {
Ok(middleware::ToolCallDecision::Block(msg)) => { Ok(middleware::ToolCallDecision::Block(msg)) => {
tracing::warn!("[AgentLoop] Tool '{}' blocked by middleware: {}", name, msg); tracing::warn!("[AgentLoop] Tool '{}' blocked by middleware: {}", name, msg);
let error_output = serde_json::json!({ "error": 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)); messages.push(Message::tool_result(id, zclaw_types::ToolId::new(&name), error_output, true));
continue; continue;
} }
Ok(middleware::ToolCallDecision::AbortLoop(reason)) => { Ok(middleware::ToolCallDecision::AbortLoop(reason)) => {
tracing::warn!("[AgentLoop] Loop aborted by middleware: {}", 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; break 'outer;
} }
Ok(middleware::ToolCallDecision::ReplaceInput(new_input)) => { Ok(middleware::ToolCallDecision::ReplaceInput(new_input)) => {
@@ -936,18 +962,24 @@ impl AgentLoop {
let (result, is_error) = if let Some(tool) = tools.get(&name) { let (result, is_error) = if let Some(tool) = tools.get(&name) {
match tool.execute(new_input, &tool_context).await { match tool.execute(new_input, &tool_context).await {
Ok(output) => { 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) (output, false)
} }
Err(e) => { Err(e) => {
let error_output = serde_json::json!({ "error": e.to_string() }); 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) (error_output, true)
} }
} }
} else { } else {
let error_output = serde_json::json!({ "error": format!("Unknown tool: {}", 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) (error_output, true)
}; };
messages.push(Message::tool_result(id, zclaw_types::ToolId::new(&name), result, is_error)); messages.push(Message::tool_result(id, zclaw_types::ToolId::new(&name), result, is_error));
@@ -956,7 +988,9 @@ impl AgentLoop {
Err(e) => { Err(e) => {
tracing::error!("[AgentLoop] Middleware error for tool '{}': {}", name, e); tracing::error!("[AgentLoop] Middleware error for tool '{}': {}", name, e);
let error_output = serde_json::json!({ "error": e.to_string() }); 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)); messages.push(Message::tool_result(id, zclaw_types::ToolId::new(&name), error_output, true));
continue; continue;
} }
@@ -966,13 +1000,17 @@ impl AgentLoop {
let guard_result = loop_guard_clone.lock().unwrap_or_else(|e| e.into_inner()).check(&name, &input); let guard_result = loop_guard_clone.lock().unwrap_or_else(|e| e.into_inner()).check(&name, &input);
match guard_result { match guard_result {
LoopGuardResult::CircuitBreaker => { 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; break 'outer;
} }
LoopGuardResult::Blocked => { LoopGuardResult::Blocked => {
tracing::warn!("[AgentLoop] Tool '{}' blocked by loop guard", name); tracing::warn!("[AgentLoop] Tool '{}' blocked by loop guard", name);
let error_output = serde_json::json!({ "error": "工具调用被循环防护拦截" }); 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)); messages.push(Message::tool_result(id, zclaw_types::ToolId::new(&name), error_output, true));
continue; continue;
} }
@@ -1005,20 +1043,26 @@ impl AgentLoop {
match tool.execute(input.clone(), &tool_context).await { match tool.execute(input.clone(), &tool_context).await {
Ok(output) => { Ok(output) => {
tracing::debug!("[AgentLoop] Tool '{}' executed successfully: {:?}", name, 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) (output, false)
} }
Err(e) => { Err(e) => {
tracing::error!("[AgentLoop] Tool '{}' execution failed: {}", name, e); tracing::error!("[AgentLoop] Tool '{}' execution failed: {}", name, e);
let error_output = serde_json::json!({ "error": e.to_string() }); 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) (error_output, true)
} }
} }
} else { } else {
tracing::error!("[AgentLoop] Tool '{}' not found in registry", name); tracing::error!("[AgentLoop] Tool '{}' not found in registry", name);
let error_output = serde_json::json!({ "error": format!("Unknown tool: {}", 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) (error_output, true)
}; };
@@ -1038,13 +1082,17 @@ impl AgentLoop {
is_error, is_error,
)); ));
// Send the question as final delta so the user sees it // Send the question as final delta so the user sees it
let _ = tx.send(LoopEvent::Delta(question.clone())).await; if let Err(e) = tx.send(LoopEvent::Delta(question.clone())).await {
let _ = tx.send(LoopEvent::Complete(AgentLoopResult { tracing::warn!("[AgentLoop] Failed to send Delta event: {}", e);
}
if let Err(e) = tx.send(LoopEvent::Complete(AgentLoopResult {
response: question.clone(), response: question.clone(),
input_tokens: total_input_tokens, input_tokens: total_input_tokens,
output_tokens: total_output_tokens, output_tokens: total_output_tokens,
iterations: iteration, 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 { if let Err(e) = memory.append_message(&session_id_clone, &Message::assistant(&question)).await {
tracing::warn!("[AgentLoop] Failed to save clarification message: {}", e); tracing::warn!("[AgentLoop] Failed to save clarification message: {}", e);
} }

View File

@@ -862,7 +862,7 @@ export function AuditLogsPanel() {
{filteredLogs.length === 0 ? ( {filteredLogs.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-gray-500 dark:text-gray-400"> <div className="flex flex-col items-center justify-center h-full text-gray-500 dark:text-gray-400">
<AlertCircle className="w-8 h-8 mb-2" /> <AlertCircle className="w-8 h-8 mb-2" />
<p>No audit logs found</p> <p></p>
{(searchTerm || Object.keys(filter).length > 0) && ( {(searchTerm || Object.keys(filter).length > 0) && (
<button <button
onClick={handleResetFilters} onClick={handleResetFilters}

View File

@@ -683,14 +683,14 @@ function MessageBubble({ message, onRetry }: { message: Message; setInput?: (tex
// Thinking indicator // Thinking indicator
<div className="flex items-center gap-2 px-4 py-3 text-gray-500 dark:text-gray-400"> <div className="flex items-center gap-2 px-4 py-3 text-gray-500 dark:text-gray-400">
<LoadingDots /> <LoadingDots />
<span className="text-sm">Thinking...</span> <span className="text-sm">...</span>
</div> </div>
) : ( ) : (
<div className={`p-4 shadow-sm ${isUser ? 'chat-bubble-user shadow-md' : 'chat-bubble-assistant'} relative group`}> <div className={`p-4 shadow-sm ${isUser ? 'chat-bubble-user shadow-md' : 'chat-bubble-assistant'} relative group`}>
{/* Optimistic sending indicator */} {/* Optimistic sending indicator */}
{isUser && message.optimistic && ( {isUser && message.optimistic && (
<span className="text-xs text-blue-200 dark:text-blue-300 mb-1 block animate-pulse"> <span className="text-xs text-blue-200 dark:text-blue-300 mb-1 block animate-pulse">
Sending... ...
</span> </span>
)} )}
{/* Reasoning block for thinking content (DeerFlow-inspired) */} {/* Reasoning block for thinking content (DeerFlow-inspired) */}

View File

@@ -543,7 +543,7 @@ export function CreateTriggerModal({ isOpen, onClose, onSuccess }: CreateTrigger
{submitStatus === 'success' && ( {submitStatus === 'success' && (
<div className="flex items-center gap-2 p-3 bg-green-50 dark:bg-green-900/20 rounded-lg text-green-700 dark:text-green-400"> <div className="flex items-center gap-2 p-3 bg-green-50 dark:bg-green-900/20 rounded-lg text-green-700 dark:text-green-400">
<CheckCircle className="w-5 h-5 flex-shrink-0" /> <CheckCircle className="w-5 h-5 flex-shrink-0" />
<span className="text-sm">Trigger created successfully!</span> <span className="text-sm"></span>
</div> </div>
)} )}
{submitStatus === 'error' && ( {submitStatus === 'error' && (

View File

@@ -57,21 +57,21 @@ const RISK_CONFIG: Record<
{ label: string; color: string; bgColor: string; borderColor: string; icon: typeof AlertTriangle } { label: string; color: string; bgColor: string; borderColor: string; icon: typeof AlertTriangle }
> = { > = {
low: { low: {
label: 'Low Risk', label: '低风险',
color: 'text-green-600 dark:text-green-400', color: 'text-green-600 dark:text-green-400',
bgColor: 'bg-green-100 dark:bg-green-900/30', bgColor: 'bg-green-100 dark:bg-green-900/30',
borderColor: 'border-green-300 dark:border-green-700', borderColor: 'border-green-300 dark:border-green-700',
icon: CheckCircle, icon: CheckCircle,
}, },
medium: { medium: {
label: 'Medium Risk', label: '中风险',
color: 'text-yellow-600 dark:text-yellow-400', color: 'text-yellow-600 dark:text-yellow-400',
bgColor: 'bg-yellow-100 dark:bg-yellow-900/30', bgColor: 'bg-yellow-100 dark:bg-yellow-900/30',
borderColor: 'border-yellow-300 dark:border-yellow-700', borderColor: 'border-yellow-300 dark:border-yellow-700',
icon: AlertTriangle, icon: AlertTriangle,
}, },
high: { high: {
label: 'High Risk', label: '高风险',
color: 'text-red-600 dark:text-red-400', color: 'text-red-600 dark:text-red-400',
bgColor: 'bg-red-100 dark:bg-red-900/30', bgColor: 'bg-red-100 dark:bg-red-900/30',
borderColor: 'border-red-300 dark:border-red-700', borderColor: 'border-red-300 dark:border-red-700',
@@ -135,32 +135,32 @@ function calculateRiskLevel(handId: HandId, params: Record<string, unknown>): Ri
function getExpectedImpact(handId: HandId, params: Record<string, unknown>): string { function getExpectedImpact(handId: HandId, params: Record<string, unknown>): string {
switch (handId) { switch (handId) {
case 'browser': case 'browser':
return `Will perform browser automation on ${params.url || 'specified URL'}`; return `将在 ${params.url || '指定网址'} 执行浏览器自动化`;
case 'twitter': case 'twitter':
if (params.action === 'post') { if (params.action === 'post') {
return 'Will post content to Twitter/X publicly'; return '将公开发布内容到 Twitter/X';
} }
if (params.action === 'engage') { if (params.action === 'engage') {
return 'Will like/reply to tweets'; return '将点赞/回复推文';
} }
return 'Will perform Twitter/X operations'; return '将执行 Twitter/X 操作';
case 'collector': case 'collector':
return `Will collect data from ${params.targetUrl || 'specified source'}`; return `将从 ${params.targetUrl || '指定来源'} 收集数据`;
case 'lead': case 'lead':
return `Will search for leads from ${params.source || 'specified source'}`; return `将从 ${params.source || '指定来源'} 搜索线索`;
case 'clip': case 'clip':
return `Will process video: ${params.inputPath || 'specified input'}`; return `将处理视频: ${params.inputPath || '指定输入'}`;
case 'predictor': case 'predictor':
return `Will run prediction on ${params.dataSource || 'specified data'}`; return `将对 ${params.dataSource || '指定数据'} 运行预测`;
case 'researcher': case 'researcher':
return `Will conduct research on: ${params.topic || 'specified topic'}`; return `将研究: ${params.topic || '指定主题'}`;
default: default:
return 'Will execute Hand operation'; return '将执行 Hand 操作';
} }
} }
function formatTimeRemaining(seconds: number): string { function formatTimeRemaining(seconds: number): string {
if (seconds <= 0) return 'Expired'; if (seconds <= 0) return '已过期';
if (seconds < 60) return `${seconds}s`; if (seconds < 60) return `${seconds}s`;
const minutes = Math.floor(seconds / 60); const minutes = Math.floor(seconds / 60);
const secs = seconds % 60; const secs = seconds % 60;
@@ -218,7 +218,7 @@ function TimeoutProgress({ timeRemaining, totalSeconds }: { timeRemaining: numbe
<div className="flex items-center justify-between text-xs"> <div className="flex items-center justify-between text-xs">
<span className="text-gray-500 dark:text-gray-400 flex items-center gap-1"> <span className="text-gray-500 dark:text-gray-400 flex items-center gap-1">
<Clock className="w-3 h-3" /> <Clock className="w-3 h-3" />
Time Remaining
</span> </span>
<span <span
className={`font-medium ${isUrgent ? 'text-red-600 dark:text-red-400' : 'text-gray-700 dark:text-gray-300'}`} className={`font-medium ${isUrgent ? 'text-red-600 dark:text-red-400' : 'text-gray-700 dark:text-gray-300'}`}
@@ -241,7 +241,7 @@ function TimeoutProgress({ timeRemaining, totalSeconds }: { timeRemaining: numbe
function ParamsDisplay({ params }: { params: Record<string, unknown> }) { function ParamsDisplay({ params }: { params: Record<string, unknown> }) {
if (!params || Object.keys(params).length === 0) { if (!params || Object.keys(params).length === 0) {
return ( return (
<p className="text-sm text-gray-500 dark:text-gray-400 italic">No parameters provided</p> <p className="text-sm text-gray-500 dark:text-gray-400 italic"></p>
); );
} }
@@ -282,7 +282,7 @@ export function HandApprovalModal({
runId: handRun.runId, runId: handRun.runId,
handId, handId,
handName: handDef?.name || handId, handName: handDef?.name || handId,
description: handDef?.description || 'Hand execution request', description: handDef?.description || 'Hand 执行请求',
params, params,
riskLevel: calculateRiskLevel(handId, params), riskLevel: calculateRiskLevel(handId, params),
expectedImpact: getExpectedImpact(handId, params), expectedImpact: getExpectedImpact(handId, params),
@@ -329,7 +329,7 @@ export function HandApprovalModal({
await onApprove(approvalData.runId); await onApprove(approvalData.runId);
onClose(); onClose();
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Failed to approve'); setError(err instanceof Error ? err.message : '批准失败');
} finally { } finally {
setIsProcessing(false); setIsProcessing(false);
} }
@@ -344,7 +344,7 @@ export function HandApprovalModal({
} }
if (!rejectReason.trim()) { if (!rejectReason.trim()) {
setError('Please provide a reason for rejection'); setError('请提供拒绝原因');
return; return;
} }
@@ -355,7 +355,7 @@ export function HandApprovalModal({
await onReject(approvalData.runId, rejectReason.trim()); await onReject(approvalData.runId, rejectReason.trim());
onClose(); onClose();
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Failed to reject'); setError(err instanceof Error ? err.message : '拒绝失败');
} finally { } finally {
setIsProcessing(false); setIsProcessing(false);
} }
@@ -387,10 +387,10 @@ export function HandApprovalModal({
</div> </div>
<div> <div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white"> <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
Hand Approval Request Hand
</h2> </h2>
<p className="text-xs text-gray-500 dark:text-gray-400"> <p className="text-xs text-gray-500 dark:text-gray-400">
Review and approve Hand execution Hand
</p> </p>
</div> </div>
</div> </div>
@@ -408,7 +408,7 @@ export function HandApprovalModal({
{isExpired && ( {isExpired && (
<div className="flex items-center gap-2 p-3 bg-gray-100 dark:bg-gray-900 rounded-lg text-gray-600 dark:text-gray-400"> <div className="flex items-center gap-2 p-3 bg-gray-100 dark:bg-gray-900 rounded-lg text-gray-600 dark:text-gray-400">
<Clock className="w-5 h-5 flex-shrink-0" /> <Clock className="w-5 h-5 flex-shrink-0" />
<span className="text-sm">This approval request has expired</span> <span className="text-sm"></span>
</div> </div>
)} )}
@@ -439,7 +439,7 @@ export function HandApprovalModal({
{/* Parameters */} {/* Parameters */}
<div className="space-y-2"> <div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Execution Parameters
</label> </label>
<ParamsDisplay params={approvalData.params} /> <ParamsDisplay params={approvalData.params} />
</div> </div>
@@ -449,7 +449,7 @@ export function HandApprovalModal({
<div className="space-y-2"> <div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 flex items-center gap-1"> <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 flex items-center gap-1">
<Info className="w-3.5 h-3.5" /> <Info className="w-3.5 h-3.5" />
Expected Impact
</label> </label>
<p className="text-sm text-gray-600 dark:text-gray-400 bg-blue-50 dark:bg-blue-900/20 p-3 rounded-lg"> <p className="text-sm text-gray-600 dark:text-gray-400 bg-blue-50 dark:bg-blue-900/20 p-3 rounded-lg">
{approvalData.expectedImpact} {approvalData.expectedImpact}
@@ -459,9 +459,9 @@ export function HandApprovalModal({
{/* Request Info */} {/* Request Info */}
<div className="text-xs text-gray-500 dark:text-gray-400 space-y-1 pt-2 border-t border-gray-200 dark:border-gray-700"> <div className="text-xs text-gray-500 dark:text-gray-400 space-y-1 pt-2 border-t border-gray-200 dark:border-gray-700">
<p>Run ID: {approvalData.runId}</p> <p> ID: {approvalData.runId}</p>
<p> <p>
Requested: {new Date(approvalData.requestedAt).toLocaleString()} : {new Date(approvalData.requestedAt).toLocaleString()}
</p> </p>
</div> </div>
@@ -469,12 +469,12 @@ export function HandApprovalModal({
{showRejectInput && ( {showRejectInput && (
<div className="space-y-2"> <div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Rejection Reason <span className="text-red-500">*</span> <span className="text-red-500">*</span>
</label> </label>
<textarea <textarea
value={rejectReason} value={rejectReason}
onChange={(e) => setRejectReason(e.target.value)} onChange={(e) => setRejectReason(e.target.value)}
placeholder="Please provide a reason for rejecting this request..." placeholder="请提供拒绝此请求的原因..."
className="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-md bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-red-500" className="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-md bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-red-500"
rows={3} rows={3}
autoFocus autoFocus
@@ -502,7 +502,7 @@ export function HandApprovalModal({
disabled={isProcessing} disabled={isProcessing}
className="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors disabled:opacity-50" className="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors disabled:opacity-50"
> >
Cancel
</button> </button>
<button <button
type="button" type="button"
@@ -513,12 +513,12 @@ export function HandApprovalModal({
{isProcessing ? ( {isProcessing ? (
<> <>
<Loader2 className="w-4 h-4 animate-spin" /> <Loader2 className="w-4 h-4 animate-spin" />
Rejecting... ...
</> </>
) : ( ) : (
<> <>
<XCircle className="w-4 h-4" /> <XCircle className="w-4 h-4" />
Confirm Rejection
</> </>
)} )}
</button> </button>
@@ -531,7 +531,7 @@ export function HandApprovalModal({
disabled={isProcessing} disabled={isProcessing}
className="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors disabled:opacity-50" className="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors disabled:opacity-50"
> >
Close
</button> </button>
<button <button
type="button" type="button"
@@ -540,7 +540,7 @@ export function HandApprovalModal({
className="px-4 py-2 text-sm border border-red-200 dark:border-red-800 text-red-600 dark:text-red-400 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors disabled:opacity-50 flex items-center gap-2" className="px-4 py-2 text-sm border border-red-200 dark:border-red-800 text-red-600 dark:text-red-400 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors disabled:opacity-50 flex items-center gap-2"
> >
<XCircle className="w-4 h-4" /> <XCircle className="w-4 h-4" />
Reject
</button> </button>
<button <button
type="button" type="button"
@@ -551,12 +551,12 @@ export function HandApprovalModal({
{isProcessing ? ( {isProcessing ? (
<> <>
<Loader2 className="w-4 h-4 animate-spin" /> <Loader2 className="w-4 h-4 animate-spin" />
Approving... ...
</> </>
) : ( ) : (
<> <>
<CheckCircle className="w-4 h-4" /> <CheckCircle className="w-4 h-4" />
Approve
</> </>
)} )}
</button> </button>

View File

@@ -368,7 +368,7 @@ function ArrayParamInput({ param, value, onChange, disabled, error }: ParamInput
</div> </div>
{items.length === 0 && !newItem && ( {items.length === 0 && !newItem && (
<p className="text-xs text-gray-400 text-center">No items added yet</p> <p className="text-xs text-gray-400 text-center"></p>
)} )}
</div> </div>
); );

View File

@@ -428,10 +428,10 @@ export function MessageSearch({ onNavigateToMessage }: MessageSearchProps) {
onChange={(e) => setFilters((prev) => ({ ...prev, timeRange: e.target.value as SearchFilters['timeRange'] }))} onChange={(e) => setFilters((prev) => ({ ...prev, timeRange: e.target.value as SearchFilters['timeRange'] }))}
className="text-xs bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded px-2 py-1 focus:outline-none focus:ring-1 focus:ring-orange-500" className="text-xs bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded px-2 py-1 focus:outline-none focus:ring-1 focus:ring-orange-500"
> >
<option value="all">All time</option> <option value="all"></option>
<option value="today">Today</option> <option value="today"></option>
<option value="week">This week</option> <option value="week"></option>
<option value="month">This month</option> <option value="month"></option>
</select> </select>
</div> </div>
</div> </div>
@@ -442,7 +442,7 @@ export function MessageSearch({ onNavigateToMessage }: MessageSearchProps) {
{/* Search history */} {/* Search history */}
{!query && searchHistory.length > 0 && ( {!query && searchHistory.length > 0 && (
<div className="mt-2"> <div className="mt-2">
<div className="text-xs text-gray-400 dark:text-gray-500 mb-1">Recent searches:</div> <div className="text-xs text-gray-400 dark:text-gray-500 mb-1">:</div>
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{searchHistory.slice(0, 5).map((item, index) => ( {searchHistory.slice(0, 5).map((item, index) => (
<button <button