'use client' /** * ThinkingStreamTest - Tracer Bullet 測試元件 (企業級強化版) * * 修復項目: * 1. Buffer 累積 - 防止 TCP 封包切斷 JSON * 2. AbortController - 防止 Memory Leak * 3. Zustand 整合 - 符合 ADR-004 * 4. Nothing.tech 美學 - 符合 ADR-002 */ import { useCallback, useRef, useEffect } from 'react' import { cn } from '@/lib/utils' import { useAgentStore } from '@/stores/agent.store' interface StreamThinkingStep { type: 'thinking' | 'result' content: string } const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/v1' export function ThinkingStreamTest() { // ADR-004: 使用 Zustand 全域狀態 const { status, thinkingStream, setStatus, appendThinking, clearThinking, } = useAgentStore() const isStreaming = status === 'thinking' // 防線 1: AbortController 用於中斷連線 const abortControllerRef = useRef(null) // 防線 2: 組件卸載時自動清理 useEffect(() => { return () => { abortControllerRef.current?.abort() } }, []) const startStream = useCallback(async () => { // 中斷前一次未完成的請求 abortControllerRef.current?.abort() abortControllerRef.current = new AbortController() // 重置狀態 (透過 Zustand) clearThinking() setStatus('thinking') try { const response = await fetch(`${API_BASE_URL}/agent/thinking`, { signal: abortControllerRef.current.signal, }) if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`) } const reader = response.body?.getReader() if (!reader) { throw new Error('無法建立串流通道') } const decoder = new TextDecoder() let buffer = '' // 防線 3: 字串緩衝區,防止 JSON 被切斷 // eslint-disable-next-line no-constant-condition -- SSE stream read loop while (true) { const { done, value } = await reader.read() if (done) break buffer += decoder.decode(value, { stream: true }) // SSE 規範: 事件之間以 \n\n 分隔 const events = buffer.split('\n\n') // 保留最後一個可能不完整的片段 buffer = events.pop() || '' for (const event of events) { if (event.startsWith('data: ')) { const data = event.slice(6).trim() // 結束標記 if (data === '[DONE]') { setStatus('idle') return } // 安全解析 JSON try { const step = JSON.parse(data) as StreamThinkingStep // 派發到 Zustand (ADR-004) appendThinking({ type: step.type === 'result' ? 'result' : 'thinking', content: step.content, timestamp: new Date(), }) } catch (e) { console.warn('JSON 解析錯誤,跳過片段:', data) } } } } } catch (err: unknown) { if (err instanceof Error && err.name === 'AbortError') { console.log('串流已手動中斷') } else { const message = err instanceof Error ? err.message : '未知錯誤' appendThinking({ type: 'error', content: message, timestamp: new Date(), }) } } finally { setStatus('idle') } }, [setStatus, appendThinking, clearThinking]) return (
{/* Header - Nothing.tech 風格 */}

AWOOOI Terminal

v0.1.0 | SSE
{/* Control Button */} {/* Terminal Output */}
{thinkingStream.length === 0 && !isStreaming && (
{'>'} Waiting for command...
)} {thinkingStream.map((step, index) => (
[{step.type.toUpperCase()}] {step.content}
))} {/* Cursor 動畫 */} {isStreaming && (
{'>'}
)}
{/* Connection Info */}

ENDPOINT: {API_BASE_URL}/agent/thinking

) }