- 角色管理:CRUD API + 卡片网格UI,每个角色约200字描述 - 世界观设定:单篇长文编辑器,建议500字以上 - 章节管理:CRUD + 排序 API,左右分栏编辑器,单章5000字上限 - AI工具调用:支持工具审批流程和结构化工具调用 - 小说详情页新增章节、角色、世界观入口按钮 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
151 lines
4.0 KiB
TypeScript
151 lines
4.0 KiB
TypeScript
import { http } from './client'
|
|
import type { AIConfig, AIConfigSave, ChatMessageList, ToolCallEntry } 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 ToolResultInfo {
|
|
name: string
|
|
label: string
|
|
result: string
|
|
}
|
|
|
|
export interface PendingToolCallData {
|
|
id: string
|
|
name: string
|
|
label: string
|
|
arguments: Record<string, unknown>
|
|
}
|
|
|
|
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<void> {
|
|
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()
|
|
}
|