feat(desktop): Premium LLM SSE 스트리밍 — 토큰 단위 실시간 응답
- sse-parser.ts: Anthropic SSE 파서 (content_block_delta.text 추출) - CloudSyncService.invokeFunctionStream(): raw fetch + SSE ReadableStream - PremiumLLMService.chatStream(): stream=true, 토큰 단위 yield - SSE 실패 시 자동 비스트리밍 fallback - AbortController 연결 (취소 지원)
This commit is contained in:
parent
667c09242b
commit
7467bd6ce9
3 changed files with 172 additions and 11 deletions
|
|
@ -425,6 +425,62 @@ class CloudSyncService extends EventEmitter {
|
|||
return { data, error: null }
|
||||
}
|
||||
|
||||
/**
|
||||
* Edge Function 스트리밍 호출 — SSE 응답의 ReadableStream 반환.
|
||||
* Supabase JS의 functions.invoke()는 JSON 자동 파싱하므로 SSE에 부적합.
|
||||
* raw fetch + auth 토큰으로 직접 호출.
|
||||
*/
|
||||
async invokeFunctionStream(
|
||||
name: string,
|
||||
body: Record<string, unknown>,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{ stream: ReadableStream<Uint8Array> | null; error: { message: string } | null }> {
|
||||
if (!this._client) {
|
||||
return { stream: null, error: { message: 'Supabase client not initialized' } }
|
||||
}
|
||||
|
||||
const { data: sessionData } = await this._client.auth.getSession()
|
||||
const token = sessionData.session?.access_token
|
||||
if (!token) {
|
||||
return { stream: null, error: { message: 'No active session — 로그인 필요' } }
|
||||
}
|
||||
|
||||
const url = configGet('supabaseUrl') as string | undefined
|
||||
const anonKey = configGet('supabaseAnonKey') as string | undefined
|
||||
if (!url || !anonKey) {
|
||||
return { stream: null, error: { message: 'Supabase URL/key not configured' } }
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${url}/functions/v1/${name}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
apikey: anonKey,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text()
|
||||
return { stream: null, error: { message: `${response.status}: ${text}` } }
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
return { stream: null, error: { message: 'No response body' } }
|
||||
}
|
||||
|
||||
return { stream: response.body, error: null }
|
||||
} catch (err) {
|
||||
if ((err as Error).name === 'AbortError') {
|
||||
return { stream: null, error: { message: 'Request aborted' } }
|
||||
}
|
||||
return { stream: null, error: { message: err instanceof Error ? err.message : String(err) } }
|
||||
}
|
||||
}
|
||||
|
||||
getState(): CloudSyncState {
|
||||
return {
|
||||
authenticated: this.isAuthenticated(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue