import { useCallback } from 'react' import { getAttachedFileAccountIds } from './fileAccountBinding' export function useChatStreamClient({ t, onMessage, model, message, effectiveKey, selectedAccount, streamingMode, attachedFiles, abortControllerRef, setLoading, setIsStreaming, setResponse, setStreamingContent, setStreamingThinking, }) { const stopGeneration = useCallback(() => { if (abortControllerRef.current) { abortControllerRef.current.abort() abortControllerRef.current = null } setLoading(false) setIsStreaming(false) }, [abortControllerRef, setIsStreaming, setLoading]) const extractErrorMessage = useCallback(async (res) => { let raw = '' try { raw = await res.text() } catch { return t('apiTester.requestFailed') } if (!raw) { return t('apiTester.requestFailed') } try { const data = JSON.parse(raw) const fromErrorObject = data?.error?.message const fromErrorString = typeof data?.error === 'string' ? data.error : '' const detail = typeof data?.detail === 'string' ? data.detail : '' const msg = typeof data?.message === 'string' ? data.message : '' return fromErrorObject || fromErrorString || detail || msg || t('apiTester.requestFailed') } catch { return raw.length > 240 ? `${raw.slice(0, 240)}...` : raw } }, [t]) const resolveAttachmentAccount = useCallback(() => { const ids = getAttachedFileAccountIds(attachedFiles) if (ids.length > 1) { return { accountId: '', error: t('apiTester.fileAccountConflict'), } } return { accountId: ids[0] || '', error: '', } }, [attachedFiles, t]) const extractStreamError = useCallback((json) => { const error = json?.error if (!error || typeof error !== 'object') { return null } const message = typeof error.message === 'string' && error.message.trim() ? error.message.trim() : t('apiTester.requestFailed') const rawStatus = Number(json?.status_code ?? error.status_code ?? error.http_status) const statusCode = Number.isFinite(rawStatus) && rawStatus > 0 ? rawStatus : (error.code === 'content_filter' ? 400 : 429) return { message, statusCode, code: typeof error.code === 'string' ? error.code : '', type: typeof error.type === 'string' ? error.type : '', } }, [t]) const runTest = useCallback(async () => { if (!effectiveKey) { onMessage('error', t('apiTester.missingApiKey')) return } const startedAt = Date.now() setLoading(true) setIsStreaming(true) setResponse(null) setStreamingContent('') setStreamingThinking('') abortControllerRef.current = new AbortController() try { const selectedAccountId = String(selectedAccount || '').trim() const attachmentBinding = resolveAttachmentAccount() if (attachmentBinding.error) { setResponse({ success: false, error: attachmentBinding.error }) onMessage('error', attachmentBinding.error) setLoading(false) setIsStreaming(false) return } if (attachmentBinding.accountId && selectedAccountId && selectedAccountId !== attachmentBinding.accountId) { const errorMsg = t('apiTester.fileAccountMismatch', { account: attachmentBinding.accountId }) setResponse({ success: false, error: errorMsg }) onMessage('error', errorMsg) setLoading(false) setIsStreaming(false) return } const requestAccount = selectedAccountId || attachmentBinding.accountId const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${effectiveKey}`, } if (requestAccount) { headers['X-Ds2-Target-Account'] = requestAccount } const body = { model, messages: [{ role: 'user', content: message }], stream: streamingMode, } if (attachedFiles && attachedFiles.length > 0) { body.file_ids = attachedFiles.map(f => f.id) } const endpoint = streamingMode ? '/v1/chat/completions' : '/v1/chat/completions?__go=1' const res = await fetch(endpoint, { method: 'POST', headers, body: JSON.stringify(body), signal: abortControllerRef.current.signal, }) if (!res.ok) { const errorMsg = await extractErrorMessage(res) setResponse({ success: false, error: errorMsg }) onMessage('error', errorMsg) setLoading(false) setIsStreaming(false) return } if (streamingMode) { setResponse({ success: true, status_code: res.status }) const reader = res.body.getReader() const decoder = new TextDecoder() let buffer = '' let accumulatedThinking = '' let accumulatedContent = '' let streamError = null streamLoop: while (true) { const { done, value } = await reader.read() if (done) break buffer += decoder.decode(value, { stream: true }) const lines = buffer.split('\n') buffer = lines.pop() || '' for (const line of lines) { const trimmed = line.trim() if (!trimmed || !trimmed.startsWith('data: ')) continue const dataStr = trimmed.slice(6) if (dataStr === '[DONE]') continue try { const json = JSON.parse(dataStr) const errorPayload = extractStreamError(json) if (errorPayload) { streamError = errorPayload break streamLoop } const choice = json.choices?.[0] if (choice?.delta) { const delta = choice.delta if (delta.reasoning_content) { accumulatedThinking += delta.reasoning_content setStreamingThinking(prev => prev + delta.reasoning_content) } if (delta.content) { accumulatedContent += delta.content setStreamingContent(prev => prev + delta.content) } } } catch (e) { console.error('Invalid JSON hunk:', dataStr, e) } } } if (streamError) { await reader.cancel().catch(() => {}) setStreamingContent('') setStreamingThinking('') setResponse({ success: false, status_code: streamError.statusCode, error: streamError.message, code: streamError.code, type: streamError.type, }) onMessage('error', streamError.message) setLoading(false) setIsStreaming(false) return } setResponse({ success: true, status_code: res.status, choices: [{ finish_reason: 'stop', index: 0, message: { role: 'assistant', content: accumulatedContent, reasoning_content: accumulatedThinking, }, }], }) onMessage('success', t('apiTester.requestSuccess', { account: requestAccount || selectedAccountId || 'Auto', time: Math.max(0, Date.now() - startedAt) })) } else { const data = await res.json() setResponse({ success: true, status_code: res.status, ...data }) const elapsed = Math.max(0, Date.now() - startedAt) onMessage('success', t('apiTester.requestSuccess', { account: requestAccount || 'Auto', time: elapsed })) } } catch (e) { if (e.name === 'AbortError') { onMessage('info', t('messages.generationStopped')) } else { onMessage('error', t('apiTester.networkError', { error: e.message })) setResponse({ error: e.message, success: false }) } } finally { setLoading(false) setIsStreaming(false) abortControllerRef.current = null } }, [ abortControllerRef, attachedFiles, effectiveKey, extractErrorMessage, extractStreamError, message, model, onMessage, resolveAttachmentAccount, selectedAccount, setIsStreaming, setLoading, setResponse, setStreamingContent, setStreamingThinking, streamingMode, t, ]) return { runTest, stopGeneration, } }