From a5c119280932a60e5a221ffa22ce3e8fdbfa020e Mon Sep 17 00:00:00 2001 From: xiamuceer Date: Fri, 21 Nov 2025 15:49:39 +0800 Subject: [PATCH] =?UTF-8?q?update:1.=E4=BC=98=E5=8C=96=E5=90=91=E5=AF=BC?= =?UTF-8?q?=E7=94=9F=E6=88=90=E9=80=BB=E8=BE=91=EF=BC=8C=E6=9C=80=E5=90=8E?= =?UTF-8?q?=E4=B8=80=E6=AD=A5=E4=B8=8D=E5=86=8D=E5=B1=95=E5=BC=80=E5=A4=A7?= =?UTF-8?q?=E7=BA=B2=EF=BC=8C=E9=81=BF=E5=85=8D=E7=AD=89=E5=BE=85=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E8=BF=87=E4=B9=85=202.=E6=96=B0=E5=A2=9E=E7=AB=A0?= =?UTF-8?q?=E8=8A=82=E8=B7=B3=E8=BD=AC=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/.env.example | 6 +- backend/app/api/wizard_stream.py | 73 ++---------- docker-compose.yml | 4 +- .../src/components/FloatingIndexPanel.tsx | 100 ++++++++++++++++ frontend/src/pages/Chapters.tsx | 111 ++++++++++++------ frontend/src/pages/ProjectWizardNew.tsx | 12 +- 6 files changed, 201 insertions(+), 105 deletions(-) create mode 100644 frontend/src/components/FloatingIndexPanel.tsx diff --git a/backend/.env.example b/backend/.env.example index 75e0e72..6a5c699 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -21,11 +21,11 @@ TZ=Asia/Shanghai # PostgreSQL 连接信息 POSTGRES_DB=mumuai_novel POSTGRES_USER=mumuai -POSTGRES_PASSWORD=your_secure_password_here +POSTGRES_PASSWORD=123456 POSTGRES_PORT=5432 # 数据库连接 URL(Docker 部署时自动生成) -DATABASE_URL=postgresql+asyncpg://mumuai:your_secure_password_here@localhost:5432/mumuai_novel +DATABASE_URL=postgresql+asyncpg://mumuai:123456@localhost:5432/mumuai_novel # PostgreSQL 连接池配置(优化后,支持80-150并发用户) DATABASE_POOL_SIZE=30 # 核心连接数(默认30,小团队可用20) @@ -101,7 +101,7 @@ FRONTEND_URL=http://localhost:8000 # ========================================== LOCAL_AUTH_ENABLED=true LOCAL_AUTH_USERNAME=admin -LOCAL_AUTH_PASSWORD=your_secure_password_here +LOCAL_AUTH_PASSWORD=admin123 LOCAL_AUTH_DISPLAY_NAME=本地管理员 # ========================================== diff --git a/backend/app/api/wizard_stream.py b/backend/app/api/wizard_stream.py index 40c7937..d6b0f55 100644 --- a/backend/app/api/wizard_stream.py +++ b/backend/app/api/wizard_stream.py @@ -876,16 +876,14 @@ async def outline_generator( db: AsyncSession, user_ai_service: AIService ) -> AsyncGenerator[str, None]: - """大纲生成流式生成器 - 向导生成3个大纲节点,每个展开为3章,共9章""" + """大纲生成流式生成器 - 向导仅生成大纲节点,不展开章节(避免等待过久)""" db_committed = False try: yield await SSEResponse.send_progress("开始生成大纲...", 5) project_id = data.get("project_id") - # 向导固定生成3个大纲节点 - outline_count = 3 - # 每个大纲展开为3章 - chapters_per_outline = 3 + # 向导固定生成3个大纲节点(不展开) + outline_count = data.get("chapter_count", 3) narrative_perspective = data.get("narrative_perspective") target_words = data.get("target_words", 100000) requirements = data.get("requirements", "") @@ -989,59 +987,12 @@ async def outline_generator( logger.info(f"✅ 成功创建{len(created_outlines)}个大纲节点") - # 第二阶段:使用PlotExpansionService将每个大纲展开为详细章节 - yield await SSEResponse.send_progress(f"开始将大纲展开为详细章节...", 50) - - expansion_service = PlotExpansionService(user_ai_service) - total_chapters_created = 0 - start_chapter_number = 1 - - for outline_idx, outline in enumerate(created_outlines, 1): - yield await SSEResponse.send_progress( - f"展开第{outline_idx}/{len(created_outlines)}个大纲节点...", - 50 + (outline_idx - 1) * 35 // len(created_outlines) - ) - - try: - # 分析大纲并生成章节规划 - chapter_plans = await expansion_service.analyze_outline_for_chapters( - outline=outline, - project=project, - db=db, - target_chapter_count=chapters_per_outline, - expansion_strategy="balanced", - enable_scene_analysis=False, - provider=provider, - model=model - ) - - logger.info(f"大纲 {outline.title} 生成了 {len(chapter_plans)} 个章节规划") - - # 创建章节记录 - chapters = await expansion_service.create_chapters_from_plans( - outline_id=outline.id, - chapter_plans=chapter_plans, - project_id=project_id, - db=db, - start_chapter_number=start_chapter_number - ) - - total_chapters_created += len(chapters) - start_chapter_number += len(chapters) - - logger.info(f"✅ 大纲 {outline.title} 创建了 {len(chapters)} 个章节记录") - - except Exception as e: - logger.error(f"❌ 展开大纲 {outline.title} 失败: {e}") - yield await SSEResponse.send_progress( - f"⚠️ 展开大纲{outline_idx}失败,跳过", - 50 + outline_idx * 35 // len(created_outlines), - "warning" - ) - continue + # 向导流程中不展开大纲,避免等待时间过长 + # 用户可以在大纲页面手动展开需要的大纲节点 + yield await SSEResponse.send_progress("跳过大纲展开,加快创建速度...", 85) # 更新项目信息 - project.chapter_count = total_chapters_created + project.chapter_count = 0 # 向导阶段不创建章节 project.narrative_perspective = narrative_perspective project.target_words = target_words project.status = "writing" @@ -1053,20 +1004,20 @@ async def outline_generator( logger.info(f"📊 向导大纲生成完成:") logger.info(f" - 创建大纲节点:{len(created_outlines)} 个") - logger.info(f" - 创建详细章节:{total_chapters_created} 个") - logger.info(f" - 平均每个大纲:{total_chapters_created / len(created_outlines):.1f} 章") + logger.info(f" - 提示:可在大纲页面手动展开为章节") # 发送结果 yield await SSEResponse.send_result({ - "message": f"成功生成{len(created_outlines)}个大纲节点,展开为{total_chapters_created}个详细章节", + "message": f"成功生成{len(created_outlines)}个大纲节点(未展开章节,可在大纲页面手动展开)", "outline_count": len(created_outlines), - "chapter_count": total_chapters_created, + "chapter_count": 0, "outlines": [ { "id": outline.id, "order_index": outline.order_index, "title": outline.title, - "content": outline.content[:100] + "..." if len(outline.content) > 100 else outline.content + "content": outline.content[:100] + "..." if len(outline.content) > 100 else outline.content, + "note": "可在大纲页面展开为章节" } for outline in created_outlines ] }) diff --git a/docker-compose.yml b/docker-compose.yml index b71edb2..9aab5f2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ services: environment: POSTGRES_DB: ${POSTGRES_DB:-mumuai_novel} POSTGRES_USER: ${POSTGRES_USER:-mumuai} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-mumuai_password_2024} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-123456} POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C" TZ: ${TZ:-Asia/Shanghai} volumes: @@ -72,7 +72,7 @@ services: - DEBUG=${DEBUG:-false} # 数据库配置 - - DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-mumuai}:${POSTGRES_PASSWORD:-mumuai_password_2024}@postgres:5432/${POSTGRES_DB:-mumuai_novel} + - DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-mumuai}:${POSTGRES_PASSWORD:-123456}@postgres:5432/${POSTGRES_DB:-mumuai_novel} # PostgreSQL 连接池配置 - DATABASE_POOL_SIZE=${DATABASE_POOL_SIZE:-30} diff --git a/frontend/src/components/FloatingIndexPanel.tsx b/frontend/src/components/FloatingIndexPanel.tsx new file mode 100644 index 0000000..89b2959 --- /dev/null +++ b/frontend/src/components/FloatingIndexPanel.tsx @@ -0,0 +1,100 @@ +import { useState, useMemo } from 'react'; +import { Drawer, Input, List, Typography, Empty, Tag } from 'antd'; +import { SearchOutlined } from '@ant-design/icons'; +import type { Chapter } from '../types'; + +const { Link } = Typography; + +interface GroupedChapters { + outlineId: string | null; + outlineTitle: string; + chapters: Chapter[]; +} + +interface FloatingIndexPanelProps { + visible: boolean; + onClose: () => void; + groupedChapters: GroupedChapters[]; + onChapterSelect: (chapterId: string) => void; +} + +export default function FloatingIndexPanel({ + visible, + onClose, + groupedChapters, + onChapterSelect, +}: FloatingIndexPanelProps) { + const [searchTerm, setSearchTerm] = useState(''); + + const filteredGroups = useMemo(() => { + if (!searchTerm) { + return groupedChapters; + } + return groupedChapters + .map(group => { + const filteredChapters = group.chapters.filter(chapter => + chapter.title.toLowerCase().includes(searchTerm.toLowerCase()) + ); + return { ...group, chapters: filteredChapters }; + }) + .filter(group => group.chapters.length > 0); + }, [searchTerm, groupedChapters]); + + const handleChapterClick = (chapterId: string) => { + onChapterSelect(chapterId); + onClose(); + }; + + return ( + +
+ } + value={searchTerm} + onChange={(e) => setSearchTerm(e.target.value)} + allowClear + /> +
+ + {filteredGroups.length > 0 ? ( + ( + +
+ + {group.outlineTitle} + +
+ ( + + handleChapterClick(chapter.id)}> + {`第${chapter.chapter_number}章: ${chapter.title}`} + + + )} + split={false} + /> +
+ )} + style={{ height: 'calc(100vh - 120px)', overflowY: 'auto' }} + /> + ) : ( + + )} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/Chapters.tsx b/frontend/src/pages/Chapters.tsx index 3f6589f..dcc74e8 100644 --- a/frontend/src/pages/Chapters.tsx +++ b/frontend/src/pages/Chapters.tsx @@ -1,12 +1,13 @@ import { useState, useEffect, useRef, useMemo } from 'react'; -import { List, Button, Modal, Form, Input, Select, message, Empty, Space, Badge, Tag, Card, Tooltip, InputNumber, Progress, Alert, Radio, Descriptions, Collapse, Popconfirm } from 'antd'; -import { EditOutlined, FileTextOutlined, ThunderboltOutlined, LockOutlined, DownloadOutlined, SettingOutlined, FundOutlined, SyncOutlined, CheckCircleOutlined, CloseCircleOutlined, RocketOutlined, StopOutlined, InfoCircleOutlined, CaretRightOutlined, DeleteOutlined } from '@ant-design/icons'; +import { List, Button, Modal, Form, Input, Select, message, Empty, Space, Badge, Tag, Card, Tooltip, InputNumber, Progress, Alert, Radio, Descriptions, Collapse, Popconfirm, FloatButton } from 'antd'; +import { EditOutlined, FileTextOutlined, ThunderboltOutlined, LockOutlined, DownloadOutlined, SettingOutlined, FundOutlined, SyncOutlined, CheckCircleOutlined, CloseCircleOutlined, RocketOutlined, StopOutlined, InfoCircleOutlined, CaretRightOutlined, DeleteOutlined, BookOutlined } from '@ant-design/icons'; import { useStore } from '../store'; import { useChapterSync } from '../store/hooks'; import { projectApi, writingStyleApi } from '../services/api'; import type { Chapter, ChapterUpdate, ApiError, WritingStyle, AnalysisTask, ExpansionPlanData } from '../types'; import ChapterAnalysis from '../components/ChapterAnalysis'; import { SSELoadingOverlay } from '../components/SSELoadingOverlay'; +import FloatingIndexPanel from '../components/FloatingIndexPanel'; const { TextArea } = Input; @@ -29,6 +30,7 @@ export default function Chapters() { // 分析任务状态管理 const [analysisTasksMap, setAnalysisTasksMap] = useState>({}); const pollingIntervalsRef = useRef>({}); + const [isIndexPanelVisible, setIsIndexPanelVisible] = useState(false); // 单章节生成进度状态 const [singleChapterProgress, setSingleChapterProgress] = useState(0); @@ -847,9 +849,22 @@ export default function Chapters() { message.error('删除章节失败:' + (error.message || '未知错误')); } }; - - return ( -
+ + const handleChapterSelect = (chapterId: string) => { + const element = document.getElementById(`chapter-item-${chapterId}`); + if (element) { + element.scrollIntoView({ behavior: 'smooth', block: 'center' }); + // Optional: add a visual highlight effect + element.style.transition = 'background-color 0.5s ease'; + element.style.backgroundColor = '#e6f7ff'; + setTimeout(() => { + element.style.backgroundColor = ''; + }, 1500); + } + }; + + return ( +
-
+
{chapters.length === 0 ? ( ) : ( @@ -933,6 +948,7 @@ export default function Chapters() { dataSource={group.chapters} renderItem={(item) => ( } title={ -
- +
+ 第{item.chapter_number}章:{item.title} - {getStatusText(item.status)} - - {renderAnalysisStatus(item.id)} - {item.expansion_plan && ( - - } color="blue"> - 已展开 - - - )} - {!canGenerateChapter(item) && ( - - } color="warning"> - 需前置章节 - - - )} - {item.expansion_plan && ( - - { - e.stopPropagation(); - showExpansionPlanModal(item); - }} - /> - - )} + + {getStatusText(item.status)} + + {renderAnalysisStatus(item.id)} + {item.expansion_plan && ( + + } color="blue"> + 已展开 + + + )} + {!canGenerateChapter(item) && ( + + } color="warning"> + 需前置章节 + + + )} + {item.expansion_plan && ( + + { + e.stopPropagation(); + showExpansionPlanModal(item); + }} + /> + + )} +
} description={ @@ -1630,6 +1654,21 @@ export default function Chapters() { progress={singleChapterProgress} message={singleChapterProgressMessage} /> + + } + type="primary" + tooltip="章节目录" + onClick={() => setIsIndexPanelVisible(true)} + style={{ right: isMobile ? 24 : 48, bottom: isMobile ? 80 : 48 }} + /> + + setIsIndexPanelVisible(false)} + groupedChapters={groupedChapters} + onChapterSelect={handleChapterSelect} + />
); } \ No newline at end of file diff --git a/frontend/src/pages/ProjectWizardNew.tsx b/frontend/src/pages/ProjectWizardNew.tsx index e0946a8..a986fac 100644 --- a/frontend/src/pages/ProjectWizardNew.tsx +++ b/frontend/src/pages/ProjectWizardNew.tsx @@ -140,7 +140,7 @@ export default function ProjectWizardNew() { await wizardStreamApi.generateCompleteOutlineStream( { project_id: createdProjectId, - chapter_count: 5, // 开局5章 + chapter_count: 3, // 生成3个大纲节点(不展开) narrative_perspective: values.narrative_perspective, target_words: values.target_words, }, @@ -190,7 +190,7 @@ export default function ProjectWizardNew() { 创建新项目 - 填写基本信息后,AI将自动为您生成世界观、角色和开局大纲 + 填写基本信息后,AI将自动为您生成世界观、角色和大纲节点(大纲可在项目内手动展开为章节)
- 《{projectTitle}》已成功创建,包含完整的世界观、角色和开局大纲 + 《{projectTitle}》已成功创建,包含完整的世界观、角色和大纲节点 + + + 💡 提示:进入项目后,可在"大纲"页面将大纲节点展开为详细章节