新增角色管理、世界观设定、章节管理和AI工具调用功能

- 角色管理:CRUD API + 卡片网格UI,每个角色约200字描述
- 世界观设定:单篇长文编辑器,建议500字以上
- 章节管理:CRUD + 排序 API,左右分栏编辑器,单章5000字上限
- AI工具调用:支持工具审批流程和结构化工具调用
- 小说详情页新增章节、角色、世界观入口按钮

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-21 13:58:17 +08:00
parent 580dc8be52
commit 20c759150a
34 changed files with 3495 additions and 127 deletions

View File

@@ -1,5 +1,5 @@
import { http } from './client'
import type { AIConfig, AIConfigSave, ChatMessageList } from '@/types/chat'
import type { AIConfig, AIConfigSave, ChatMessageList, ToolCallEntry } from '@/types/chat'
// ── AI配置 ──
@@ -41,27 +41,51 @@ export interface TokenUsage {
total_tokens: number
}
export interface ContextMessage {
role: string
content: string
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) => void
onContext?: (context: ContextMessage[]) => 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, onChunk, onError, onDone, onContext, signal } = options
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', {
@@ -95,7 +119,7 @@ export async function streamChat(options: StreamChatOptions): Promise<void> {
try {
const event = JSON.parse(line.slice(6))
if (event.done) {
onDone(event.usage ?? undefined)
onDone(event.usage ?? undefined, event.pending ?? false)
return
}
if (event.error) {
@@ -103,12 +127,15 @@ export async function streamChat(options: StreamChatOptions): Promise<void> {
onDone()
return
}
if (event.context && onContext) {
onContext(event.context)
}
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 {
// 忽略解析错误
}