import { http } from './client' import type { AIConfig, AIConfigSave, ChatMessageList, ToolCallEntry } from '@/types/chat' // ── AI配置 ── export async function getAIConfig(): Promise { const { data } = await http.get('/ai/config') return data } export async function saveAIConfig(payload: AIConfigSave): Promise { const { data } = await http.put('/ai/config', payload) return data } export async function testAIConfig(payload: AIConfigSave): Promise<{ ok: boolean; message: string }> { const { data } = await http.post<{ ok: boolean; message: string }>('/ai/config/test', payload) return data } // ── 聊天消息 ── export async function fetchMessages(novelId?: number): Promise { const params: Record = {} if (novelId !== undefined) params.novel_id = novelId const { data } = await http.get('/chat/messages', { params }) return data } export async function clearMessages(novelId?: number): Promise { const params: Record = {} if (novelId !== undefined) params.novel_id = novelId await http.delete('/chat/messages', { params }) } // ── 流式聊天 ── export interface TokenUsage { prompt_tokens: number completion_tokens: number total_tokens: number } export interface ToolResultInfo { name: string label: string result: string } export interface PendingToolCallData { id: string name: string label: string arguments: Record } export interface StreamChatOptions { messages: { role: string; content: string }[] novelId?: number pageContext?: string toolsEnabled?: boolean // 工具审批续传 pendingToolCalls?: PendingToolCallData[] assistantText?: string // 回调 onChunk: (text: string) => void onError: (error: string) => void onDone: (usage?: TokenUsage, pending?: boolean) => void onToolCallsPending?: (calls: PendingToolCallData[], assistantText: string) => void onToolResult?: (info: ToolResultInfo) => void signal?: AbortSignal } export async function streamChat(options: StreamChatOptions): Promise { const { messages, novelId, pageContext, toolsEnabled, pendingToolCalls, assistantText, onChunk, onError, onDone, onToolCallsPending, onToolResult, signal, } = options const body = JSON.stringify({ messages, novel_id: novelId ?? null, page_context: pageContext ?? null, tools_enabled: toolsEnabled ?? true, pending_tool_calls: pendingToolCalls ?? null, assistant_text: assistantText ?? null, }) const resp = await fetch('/api/chat/stream', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body, signal, }) if (!resp.ok || !resp.body) { onError(`请求失败: ${resp.status}`) onDone() return } const reader = resp.body.getReader() const decoder = new TextDecoder() let buffer = '' try { 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) { if (!line.startsWith('data: ')) continue try { const event = JSON.parse(line.slice(6)) if (event.done) { onDone(event.usage ?? undefined, event.pending ?? false) return } if (event.error) { onError(event.error) onDone() return } if (event.content) { onChunk(event.content) } if (event.tool_calls_pending && onToolCallsPending) { onToolCallsPending(event.tool_calls_pending, event.assistant_text ?? '') } if (event.tool_result && onToolResult) { onToolResult(event.tool_result) } } catch { // 忽略解析错误 } } } } catch (e) { if ((e as Error).name !== 'AbortError') { onError((e as Error).message) } } onDone() }