新增前后端AI聊天模块,包括流式对话、配置管理和UI组件优化。

This commit is contained in:
2026-03-19 01:35:13 +08:00
commit ba6ceb94c8
66 changed files with 8631 additions and 0 deletions

123
frontend/src/api/chat.ts Normal file
View File

@@ -0,0 +1,123 @@
import { http } from './client'
import type { AIConfig, AIConfigSave, ChatMessageList } from '@/types/chat'
// ── AI配置 ──
export async function getAIConfig(): Promise<AIConfig> {
const { data } = await http.get<AIConfig>('/ai/config')
return data
}
export async function saveAIConfig(payload: AIConfigSave): Promise<AIConfig> {
const { data } = await http.put<AIConfig>('/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<ChatMessageList> {
const params: Record<string, unknown> = {}
if (novelId !== undefined) params.novel_id = novelId
const { data } = await http.get<ChatMessageList>('/chat/messages', { params })
return data
}
export async function clearMessages(novelId?: number): Promise<void> {
const params: Record<string, unknown> = {}
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 ContextMessage {
role: string
content: string
}
export interface StreamChatOptions {
messages: { role: string; content: string }[]
novelId?: number
onChunk: (text: string) => void
onError: (error: string) => void
onDone: (usage?: TokenUsage) => void
onContext?: (context: ContextMessage[]) => void
signal?: AbortSignal
}
export async function streamChat(options: StreamChatOptions): Promise<void> {
const { messages, novelId, onChunk, onError, onDone, onContext, signal } = options
const body = JSON.stringify({
messages,
novel_id: novelId ?? 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)
return
}
if (event.error) {
onError(event.error)
onDone()
return
}
if (event.context && onContext) {
onContext(event.context)
}
if (event.content) {
onChunk(event.content)
}
} catch {
// 忽略解析错误
}
}
}
} catch (e) {
if ((e as Error).name !== 'AbortError') {
onError((e as Error).message)
}
}
onDone()
}