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:
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -862,7 +862,7 @@ export function AuditLogsPanel() {
|
||||
{filteredLogs.length === 0 ? (
|
||||
<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" />
|
||||
<p>No audit logs found</p>
|
||||
<p>暂无审计日志</p>
|
||||
{(searchTerm || Object.keys(filter).length > 0) && (
|
||||
<button
|
||||
onClick={handleResetFilters}
|
||||
|
||||
@@ -683,14 +683,14 @@ function MessageBubble({ message, onRetry }: { message: Message; setInput?: (tex
|
||||
// Thinking indicator
|
||||
<div className="flex items-center gap-2 px-4 py-3 text-gray-500 dark:text-gray-400">
|
||||
<LoadingDots />
|
||||
<span className="text-sm">Thinking...</span>
|
||||
<span className="text-sm">思考中...</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className={`p-4 shadow-sm ${isUser ? 'chat-bubble-user shadow-md' : 'chat-bubble-assistant'} relative group`}>
|
||||
{/* Optimistic sending indicator */}
|
||||
{isUser && message.optimistic && (
|
||||
<span className="text-xs text-blue-200 dark:text-blue-300 mb-1 block animate-pulse">
|
||||
Sending...
|
||||
发送中...
|
||||
</span>
|
||||
)}
|
||||
{/* Reasoning block for thinking content (DeerFlow-inspired) */}
|
||||
|
||||
@@ -543,7 +543,7 @@ export function CreateTriggerModal({ isOpen, onClose, onSuccess }: CreateTrigger
|
||||
{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">
|
||||
<CheckCircle className="w-5 h-5 flex-shrink-0" />
|
||||
<span className="text-sm">Trigger created successfully!</span>
|
||||
<span className="text-sm">触发器创建成功!</span>
|
||||
</div>
|
||||
)}
|
||||
{submitStatus === 'error' && (
|
||||
|
||||
@@ -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<string, unknown>): Ri
|
||||
function getExpectedImpact(handId: HandId, params: Record<string, unknown>): 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
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-gray-500 dark:text-gray-400 flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
Time Remaining
|
||||
剩余时间
|
||||
</span>
|
||||
<span
|
||||
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> }) {
|
||||
if (!params || Object.keys(params).length === 0) {
|
||||
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,
|
||||
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({
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Hand Approval Request
|
||||
Hand 审批请求
|
||||
</h2>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Review and approve Hand execution
|
||||
审核并批准 Hand 执行
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -408,7 +408,7 @@ export function HandApprovalModal({
|
||||
{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">
|
||||
<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>
|
||||
)}
|
||||
|
||||
@@ -439,7 +439,7 @@ export function HandApprovalModal({
|
||||
{/* Parameters */}
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Execution Parameters
|
||||
执行参数
|
||||
</label>
|
||||
<ParamsDisplay params={approvalData.params} />
|
||||
</div>
|
||||
@@ -449,7 +449,7 @@ export function HandApprovalModal({
|
||||
<div className="space-y-2">
|
||||
<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" />
|
||||
Expected Impact
|
||||
预期影响
|
||||
</label>
|
||||
<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}
|
||||
@@ -459,9 +459,9 @@ export function HandApprovalModal({
|
||||
|
||||
{/* 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">
|
||||
<p>Run ID: {approvalData.runId}</p>
|
||||
<p>运行 ID: {approvalData.runId}</p>
|
||||
<p>
|
||||
Requested: {new Date(approvalData.requestedAt).toLocaleString()}
|
||||
请求时间: {new Date(approvalData.requestedAt).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -469,12 +469,12 @@ export function HandApprovalModal({
|
||||
{showRejectInput && (
|
||||
<div className="space-y-2">
|
||||
<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>
|
||||
<textarea
|
||||
value={rejectReason}
|
||||
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"
|
||||
rows={3}
|
||||
autoFocus
|
||||
@@ -502,7 +502,7 @@ export function HandApprovalModal({
|
||||
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"
|
||||
>
|
||||
Cancel
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -513,12 +513,12 @@ export function HandApprovalModal({
|
||||
{isProcessing ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Rejecting...
|
||||
拒绝中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<XCircle className="w-4 h-4" />
|
||||
Confirm Rejection
|
||||
确认拒绝
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
@@ -531,7 +531,7 @@ export function HandApprovalModal({
|
||||
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"
|
||||
>
|
||||
Close
|
||||
关闭
|
||||
</button>
|
||||
<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"
|
||||
>
|
||||
<XCircle className="w-4 h-4" />
|
||||
Reject
|
||||
拒绝
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -551,12 +551,12 @@ export function HandApprovalModal({
|
||||
{isProcessing ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Approving...
|
||||
批准中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
Approve
|
||||
批准
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
@@ -368,7 +368,7 @@ function ArrayParamInput({ param, value, onChange, disabled, error }: ParamInput
|
||||
</div>
|
||||
|
||||
{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>
|
||||
);
|
||||
|
||||
@@ -428,10 +428,10 @@ export function MessageSearch({ onNavigateToMessage }: MessageSearchProps) {
|
||||
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"
|
||||
>
|
||||
<option value="all">All time</option>
|
||||
<option value="today">Today</option>
|
||||
<option value="week">This week</option>
|
||||
<option value="month">This month</option>
|
||||
<option value="all">全部时间</option>
|
||||
<option value="today">今天</option>
|
||||
<option value="week">本周</option>
|
||||
<option value="month">本月</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -442,7 +442,7 @@ export function MessageSearch({ onNavigateToMessage }: MessageSearchProps) {
|
||||
{/* Search history */}
|
||||
{!query && searchHistory.length > 0 && (
|
||||
<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">
|
||||
{searchHistory.slice(0, 5).map((item, index) => (
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user