fix(desktop): 隐藏Hand状态消息 + 过滤LLM工具调用叙述
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

1. 所有 role=hand 的消息不再显示 (不仅仅是 researcher)
   - "Hand: hand_researcher - running" 不再出现
   - Hand 错误 JSON 不再显示
   - 移除未使用的 PresentationContainer import

2. 添加 stripToolNarration() 过滤 LLM 推理文本
   - 英文: "Now let me...", "I need to...", "I keep getting..."
   - 中文: "让我执行...", "让我尝试使用...", "好的,让我为您..."
   - 保留实际有用内容,仅过滤工具调用叙述

验证: tsc --noEmit 零错误, vitest 343 pass (1 pre-existing fail)
This commit is contained in:
iven
2026-04-22 13:17:54 +08:00
parent 6d7457de56
commit 46fee4b2c8

View File

@@ -34,7 +34,6 @@ import { ModelSelector } from './ai/ModelSelector';
import { isTauriRuntime } from '../lib/tauri-gateway'; import { isTauriRuntime } from '../lib/tauri-gateway';
import { SuggestionChips } from './ai/SuggestionChips'; import { SuggestionChips } from './ai/SuggestionChips';
import { PipelineResultPreview } from './pipeline/PipelineResultPreview'; import { PipelineResultPreview } from './pipeline/PipelineResultPreview';
import { PresentationContainer } from './presentation/PresentationContainer';
// TokenMeter temporarily unused — using inline text counter instead // TokenMeter temporarily unused — using inline text counter instead
// Default heights for virtualized messages // Default heights for virtualized messages
@@ -637,12 +636,36 @@ export function ChatArea({ compact, onOpenDetail }: { compact?: boolean; onOpenD
); );
} }
/**
* Strip LLM tool-usage narration from response content.
* When the LLM calls tools (search, fetch, etc.), it often narrates its reasoning
* in English ("Now let me execute...", "I need to provide...", "I keep getting errors...")
* and Chinese ("让我执行...", "让我尝试..."). These are internal thoughts, not user-facing content.
*/
function stripToolNarration(content: string): string {
const sentences = content.split(/(?<=[。!?.!?])\s*/);
const filtered = sentences.filter(s => {
const t = s.trim();
if (!t) return false;
// English narration patterns
if (/^(?:Now )?[Ll]et me\s/i.test(t)) return false;
if (/^I\s+(?:need to|keep getting|should|will try|have to|can try|must)\s/i.test(t)) return false;
if (/^The hand_researcher\s/i.test(t)) return false;
// Chinese narration patterns
if (/^让我(?:执行|尝试|使用|进一步|调用|运行)/.test(t)) return false;
if (/^好的,让我为您/.test(t)) return false;
return true;
});
const result = filtered.join(' ').replace(/\s{2,}/g, ' ').trim();
return result || content; // Fallback: if everything was stripped, show original
}
function MessageBubble({ message, onRetry }: { message: Message; setInput?: (text: string) => void; onRetry?: () => void }) { function MessageBubble({ message, onRetry }: { message: Message; setInput?: (text: string) => void; onRetry?: () => void }) {
if (message.role === 'tool') { if (message.role === 'tool') {
return null; return null;
} }
// Researcher hand results are internal — search results are already in the LLM reply // Hand status/result messages are internal — search results are already in the LLM reply
if (message.role === 'hand' && message.handName === 'researcher') { if (message.role === 'hand') {
return null; return null;
} }
@@ -721,15 +744,15 @@ function MessageBubble({ message, onRetry }: { message: Message; setInput?: (tex
? (isUser ? (isUser
? message.content ? message.content
: <StreamingText : <StreamingText
content={message.content} content={stripToolNarration(message.content)}
isStreaming={!!message.streaming} isStreaming={!!message.streaming}
className="text-gray-700 dark:text-gray-200" className="text-gray-700 dark:text-gray-200"
/> />
) )
: '...'} : '...'}
</div> </div>
{/* Pipeline / Hand result presentation */} {/* Pipeline result presentation */}
{!isUser && (message.role === 'workflow' || message.role === 'hand') && message.workflowResult && typeof message.workflowResult === 'object' && message.workflowResult !== null && ( {!isUser && message.role === 'workflow' && message.workflowResult && typeof message.workflowResult === 'object' && message.workflowResult !== null && (
<div className="mt-3"> <div className="mt-3">
<PipelineResultPreview <PipelineResultPreview
outputs={message.workflowResult as Record<string, unknown>} outputs={message.workflowResult as Record<string, unknown>}
@@ -737,11 +760,6 @@ function MessageBubble({ message, onRetry }: { message: Message; setInput?: (tex
/> />
</div> </div>
)} )}
{!isUser && message.role === 'hand' && message.handResult && typeof message.handResult === 'object' && message.handResult !== null && !message.workflowResult && message.handName !== 'researcher' && (
<div className="mt-3">
<PresentationContainer data={message.handResult} />
</div>
)}
{message.error && ( {message.error && (
<div className="flex items-center gap-2 mt-2"> <div className="flex items-center gap-2 mt-2">
<p className="text-xs text-red-500">{message.error}</p> <p className="text-xs text-red-500">{message.error}</p>