新增角色管理、世界观设定、章节管理和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:
33
frontend/src/api/chapters.ts
Normal file
33
frontend/src/api/chapters.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { http } from './client'
|
||||
import type { Chapter, ChapterCreate, ChapterUpdate, ChapterList } from '@/types/chapter'
|
||||
|
||||
const base = (novelId: number) => `/novels/${novelId}/chapters`
|
||||
|
||||
export async function fetchChapters(novelId: number): Promise<ChapterList> {
|
||||
const { data } = await http.get<ChapterList>(base(novelId))
|
||||
return data
|
||||
}
|
||||
|
||||
export async function fetchChapter(novelId: number, chapterId: number): Promise<Chapter> {
|
||||
const { data } = await http.get<Chapter>(`${base(novelId)}/${chapterId}`)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function createChapter(novelId: number, payload: ChapterCreate): Promise<Chapter> {
|
||||
const { data } = await http.post<Chapter>(base(novelId), payload)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function updateChapter(novelId: number, chapterId: number, payload: ChapterUpdate): Promise<Chapter> {
|
||||
const { data } = await http.patch<Chapter>(`${base(novelId)}/${chapterId}`, payload)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function deleteChapter(novelId: number, chapterId: number): Promise<void> {
|
||||
await http.delete(`${base(novelId)}/${chapterId}`)
|
||||
}
|
||||
|
||||
export async function reorderChapters(novelId: number, orderedIds: number[]): Promise<ChapterList> {
|
||||
const { data } = await http.put<ChapterList>(`${base(novelId)}/reorder`, { ordered_ids: orderedIds })
|
||||
return data
|
||||
}
|
||||
23
frontend/src/api/characters.ts
Normal file
23
frontend/src/api/characters.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { http } from './client'
|
||||
import type { Character, CharacterCreate, CharacterUpdate, CharacterList } from '@/types/character'
|
||||
|
||||
const base = (novelId: number) => `/novels/${novelId}/characters`
|
||||
|
||||
export async function fetchCharacters(novelId: number): Promise<CharacterList> {
|
||||
const { data } = await http.get<CharacterList>(base(novelId))
|
||||
return data
|
||||
}
|
||||
|
||||
export async function createCharacter(novelId: number, payload: CharacterCreate): Promise<Character> {
|
||||
const { data } = await http.post<Character>(base(novelId), payload)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function updateCharacter(novelId: number, characterId: number, payload: CharacterUpdate): Promise<Character> {
|
||||
const { data } = await http.patch<Character>(`${base(novelId)}/${characterId}`, payload)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function deleteCharacter(novelId: number, characterId: number): Promise<void> {
|
||||
await http.delete(`${base(novelId)}/${characterId}`)
|
||||
}
|
||||
@@ -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 {
|
||||
// 忽略解析错误
|
||||
}
|
||||
|
||||
14
frontend/src/api/worldSetting.ts
Normal file
14
frontend/src/api/worldSetting.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { http } from './client'
|
||||
import type { WorldSetting, WorldSettingUpdate } from '@/types/worldSetting'
|
||||
|
||||
const base = (novelId: number) => `/novels/${novelId}/world-setting`
|
||||
|
||||
export async function fetchWorldSetting(novelId: number): Promise<WorldSetting> {
|
||||
const { data } = await http.get<WorldSetting>(base(novelId))
|
||||
return data
|
||||
}
|
||||
|
||||
export async function saveWorldSetting(novelId: number, payload: WorldSettingUpdate): Promise<WorldSetting> {
|
||||
const { data } = await http.put<WorldSetting>(base(novelId), payload)
|
||||
return data
|
||||
}
|
||||
Reference in New Issue
Block a user