feat: add NovelPage component with bookshelf, open book, and reading views

This commit is contained in:
2026-06-02 08:19:38 +08:00
parent 54e17c9795
commit 81b0d0ea1f
39 changed files with 8057 additions and 64 deletions

View File

@@ -0,0 +1,285 @@
"""
爽文Fiction / Novel数据模型。
"""
from __future__ import annotations
from typing import Dict, List, Optional
from pydantic import BaseModel, Field
class EmotionFlowStep(BaseModel):
key: str
text: str
class EmotionFlow(BaseModel):
id: str
intro: str
tags: List[str] = Field(default_factory=list)
steps: List[EmotionFlowStep] = Field(default_factory=list)
class EmotionFlowCatalog(BaseModel):
flows: List[EmotionFlow] = Field(default_factory=list)
class GuideGlobalEntry(BaseModel):
layer: str
title: str
content: str
class GuideGlobalEntries(BaseModel):
entries: List[GuideGlobalEntry] = Field(default_factory=list)
class FictionGuideWorldbook(BaseModel):
"""Book-local guide 世界书:本书具体人设 / 爽点 / 用户体验 / 禁区。"""
persona: str = ""
highlight: str = ""
experience: str = ""
forbiddenZones: str = ""
class FictionBookMeta(BaseModel):
id: str
title: str
allowedFlowIds: List[str] = Field(default_factory=list)
createdAt: str = ""
updatedAt: str = ""
class FictionBookSummary(BaseModel):
id: str
title: str
allowedFlowIds: List[str] = Field(default_factory=list)
updatedAt: str = ""
class FictionPrompts(BaseModel):
openBook: str = ""
coarseOutline: str = ""
eventPlan: str = ""
chapter: str = ""
nudge: str = ""
class FictionReaderSettings(BaseModel):
contextWindowChars: int = 2000
prefetchRemainingWords: int = 300
class FictionPipelineSettings(BaseModel):
"""半自动流水线:各层 ON=自动OFF=需手动触发。"""
semiAuto: bool = False
autoCoarse: bool = True
autoEventPlan: bool = True
autoChapter: bool = True
class FictionBookSettings(BaseModel):
prompts: FictionPrompts = Field(default_factory=FictionPrompts)
reader: FictionReaderSettings = Field(default_factory=FictionReaderSettings)
pipeline: FictionPipelineSettings = Field(default_factory=FictionPipelineSettings)
class CreateFictionBookRequest(BaseModel):
title: str
inspiration: str = ""
guide: FictionGuideWorldbook
allowedFlowIds: List[str] = Field(default_factory=list)
class OpenBookRequest(BaseModel):
inspiration: str
profile_id: Optional[str] = None
api_config: Optional[Dict[str, str]] = None
class OpenBookResult(BaseModel):
title: str
optimizedIntro: str
guide: FictionGuideWorldbook
allowedFlowIds: List[str] = Field(default_factory=list)
class UpdateFictionBookSettingsRequest(BaseModel):
prompts: Optional[FictionPrompts] = None
reader: Optional[FictionReaderSettings] = None
pipeline: Optional[FictionPipelineSettings] = None
class FictionPipelineTickResult(BaseModel):
run: FictionRunState
started: bool = False
pendingStages: List[str] = Field(default_factory=list)
class VolumeOutline(BaseModel):
"""卷纲:当前 10~30 章的阶段规划,不是整本书全局大纲。"""
id: str
order: int = 1
title: str = ""
goal: str = ""
coreConflict: str = ""
powerProgression: str = ""
emotionalPromise: str = ""
endingHook: str = ""
targetChapterCount: int = 20
primaryEmotionFlowId: str = ""
status: str = "active"
class CoarseOutlineEvent(BaseModel):
"""兼容旧 coarseOutline.events同时作为新版事件串条目的轻量视图。"""
id: str
title: str
summary: str = ""
order: int = 1
volumeId: str = ""
class CoarseOutline(BaseModel):
events: List[CoarseOutlineEvent] = Field(default_factory=list)
version: int = 1
class EventChainItem(BaseModel):
"""事件串:卷纲 + 情感链在剧情层的落地。"""
id: str
volumeId: str = ""
order: int = 1
title: str = ""
summary: str = ""
purpose: str = ""
conflict: str = ""
turningPoint: str = ""
expectedPayoff: str = ""
targetChapterCount: int = 3
emotionFlowId: str = ""
emotionStepKey: str = ""
emotionStepText: str = ""
status: str = "planned"
class FlowStepsPlan(BaseModel):
: str = ""
: str = ""
: str = ""
: str = ""
class ChapterPlanItem(BaseModel):
seq: int
# 旧字段:保留以兼容现有 metadata.events[eventId].chapterPlan。
phaseKey: str = ""
phaseSlice: str = ""
brief: str = ""
# 新字段:章纲是写章前最具体的规划层。
eventId: str = ""
title: str = ""
goal: str = ""
opening: str = ""
mainConflict: str = ""
emotionalTurn: str = ""
emotionStepKey: str = ""
emotionGoal: str = ""
payoff: str = ""
endingHook: str = ""
forbidden: str = ""
targetWords: int = 2000
status: str = "planned"
class EventPlanEntry(BaseModel):
emotionFlowId: str = ""
flowStepsPlan: FlowStepsPlan = Field(default_factory=FlowStepsPlan)
chapterPlan: List[ChapterPlanItem] = Field(default_factory=list)
class FictionProgress(BaseModel):
currentChapterSeq: int = 0
charOffset: int = 0
ttsPaused: bool = False
genPaused: bool = False
class FictionChapter(BaseModel):
seq: int
title: str = ""
body: str = ""
charCount: int = 0
status: str = "written"
eventId: str = ""
phaseKey: str = ""
createdAt: str = ""
class FictionChapterSummary(BaseModel):
seq: int
title: str = ""
charCount: int = 0
eventId: str = ""
phaseKey: str = ""
class FictionBookMetadata(BaseModel):
# v2 新规划结构:不再保留旧 coarseOutline/events 作为持久化主结构。
version: int = 2
volumes: List[VolumeOutline] = Field(default_factory=list)
eventChains: Dict[str, List[EventChainItem]] = Field(default_factory=dict)
chapterPlans: Dict[str, List[ChapterPlanItem]] = Field(default_factory=dict)
progress: FictionProgress = Field(default_factory=FictionProgress)
class FictionRunState(BaseModel):
status: str = "idle"
pipelineStage: Optional[str] = None
stage: str = "idle"
message: Optional[str] = None
progress: Optional[Dict[str, int]] = None
updatedAt: str = ""
class FictionStartReadingResult(BaseModel):
run: FictionRunState
started: bool = False
class FictionGenerationRequest(BaseModel):
profile_id: Optional[str] = None
api_config: Optional[Dict[str, str]] = None
event_id: Optional[str] = None
seq: Optional[int] = None
stream: bool = False
class UpdateFictionProgressRequest(BaseModel):
currentChapterSeq: Optional[int] = None
charOffset: Optional[int] = None
ttsPaused: Optional[bool] = None
genPaused: Optional[bool] = None
class FictionPrefetchRequest(BaseModel):
profile_id: Optional[str] = None
api_config: Optional[Dict[str, str]] = None
currentChapterSeq: int
charOffset: int = 0
remainingWords: Optional[int] = None
class FictionPrefetchResult(BaseModel):
run: FictionRunState
started: bool = False
targetSeq: Optional[int] = None
skippedReason: Optional[str] = None