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 CompressResult { compressed: boolean reason?: string error?: string removed_count?: number kept_count?: number summary_length?: number } export async function compressContext(novelId?: number): Promise { const params: Record = {} if (novelId !== undefined) params.novel_id = novelId const { data } = await http.post('/chat/compress', null, { params }) return data } // ── 流式聊天 ── 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 SubAgentStartInfo { skill_id: number skill_name: string task: string subagent_id: string } export interface SubAgentToolCallInfo { name: string label: string arguments: Record } export interface SubAgentToolResultInfo { name: string label: string result: string } export interface StreamChatOptions { messages: { role: string; content: string }[] novelId?: number pageContext?: string toolsEnabled?: boolean autoApproveTools?: boolean skillId?: number | null // 工具审批续传 pendingToolCalls?: PendingToolCallData[] assistantText?: string // 回调 onChunk: (text: string) => void onError: (error: string) => void onDone: (usage?: TokenUsage, pending?: boolean) => void onToolCallsPending?: (calls: PendingToolCallData[], assistantText: string) => void onToolCallsAuto?: (calls: PendingToolCallData[]) => void onToolResult?: (info: ToolResultInfo) => void // 子 Agent 回调(subagentId 用于区分并行的多个子 Agent) onSubAgentStart?: (info: SubAgentStartInfo) => void onSubAgentChunk?: (text: string, subagentId: string) => void onSubAgentToolCall?: (info: SubAgentToolCallInfo, subagentId: string) => void onSubAgentToolResult?: (info: SubAgentToolResultInfo, subagentId: string) => void onSubAgentDone?: (result: string, subagentId: string) => void signal?: AbortSignal } export async function streamChat(options: StreamChatOptions): Promise { const { messages, novelId, pageContext, toolsEnabled, autoApproveTools, skillId, pendingToolCalls, assistantText, onChunk, onError, onDone, onToolCallsPending, onToolCallsAuto, onToolResult, onSubAgentStart, onSubAgentChunk, onSubAgentToolCall, onSubAgentToolResult, onSubAgentDone, signal, } = options const body = JSON.stringify({ messages, novel_id: novelId ?? null, page_context: pageContext ?? null, tools_enabled: toolsEnabled ?? true, auto_approve_tools: autoApproveTools ?? false, skill_id: skillId ?? null, 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_calls_auto && onToolCallsAuto) { onToolCallsAuto(event.tool_calls_auto) } if (event.tool_result && onToolResult) { onToolResult(event.tool_result) } // 子 Agent 事件(所有事件携带 subagent_id 用于区分并行的子 Agent) if (event.subagent_start && onSubAgentStart) { onSubAgentStart(event.subagent_start) } if (event.subagent_chunk && onSubAgentChunk) { onSubAgentChunk(event.subagent_chunk, event.subagent_id ?? '') } if (event.subagent_tool_call && onSubAgentToolCall) { onSubAgentToolCall(event.subagent_tool_call, event.subagent_id ?? '') } if (event.subagent_tool_result && onSubAgentToolResult) { onSubAgentToolResult(event.subagent_tool_result, event.subagent_id ?? '') } if (event.subagent_done !== undefined && onSubAgentDone) { onSubAgentDone(event.subagent_done, event.subagent_id ?? '') } } catch { // 忽略解析错误 } } } } catch (e) { if ((e as Error).name !== 'AbortError') { onError((e as Error).message) } } onDone() }