diff --git a/README.md b/README.md index f6dcbf4..725fef5 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@
-![Version](https://img.shields.io/badge/version-1.0.10-blue.svg) +![Version](https://img.shields.io/badge/version-1.0.11-blue.svg) ![Python](https://img.shields.io/badge/python-3.11-blue.svg) ![FastAPI](https://img.shields.io/badge/FastAPI-0.109.0-green.svg) ![React](https://img.shields.io/badge/react-18.3.1-blue.svg) @@ -149,7 +149,7 @@ docker run -d --name postgres \ postgres:18-alpine # 启动后端 -python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload +python -m uvicorn app.main:app --host localhost --port 8000 --reload ``` #### 前端 diff --git a/backend/app/api/changelog.py b/backend/app/api/changelog.py new file mode 100644 index 0000000..86adb61 --- /dev/null +++ b/backend/app/api/changelog.py @@ -0,0 +1,233 @@ +""" +更新日志API +提供GitHub提交历史的缓存和代理服务 +""" +from fastapi import APIRouter, HTTPException, Query +from typing import List, Optional +import httpx +from datetime import datetime, timedelta +from pydantic import BaseModel +import logging + +logger = logging.getLogger(__name__) + +router = APIRouter() + +# GitHub API配置 +GITHUB_API_BASE = "https://api.github.com" +REPO_OWNER = "xiamuceer-j" +REPO_NAME = "MuMuAINovel" + +# 缓存配置 +_cache = { + "data": None, + "timestamp": None, + "ttl": timedelta(hours=1) # 缓存1小时 +} + + +class GitHubAuthor(BaseModel): + """GitHub作者信息""" + name: str + email: str + date: str + + +class GitHubCommitInfo(BaseModel): + """GitHub提交信息""" + author: GitHubAuthor + message: str + + +class GitHubUser(BaseModel): + """GitHub用户信息""" + login: str + avatar_url: str + + +class GitHubCommit(BaseModel): + """GitHub提交数据""" + sha: str + commit: GitHubCommitInfo + html_url: str + author: Optional[GitHubUser] = None + + +class ChangelogResponse(BaseModel): + """更新日志响应""" + commits: List[GitHubCommit] + cached: bool + cache_time: Optional[str] = None + + +def is_cache_valid() -> bool: + """检查缓存是否有效""" + if _cache["data"] is None or _cache["timestamp"] is None: + return False + + now = datetime.now() + cache_age = now - _cache["timestamp"] + + return cache_age < _cache["ttl"] + + +async def fetch_github_commits(page: int = 1, per_page: int = 30) -> List[dict]: + """从GitHub API获取提交历史""" + url = f"{GITHUB_API_BASE}/repos/{REPO_OWNER}/{REPO_NAME}/commits" + params = { + "author": REPO_OWNER, + "page": page, + "per_page": per_page + } + + headers = { + "Accept": "application/vnd.github.v3+json", + "User-Agent": "MuMuAINovel-App" + } + + try: + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.get(url, params=params, headers=headers) + response.raise_for_status() + return response.json() + except httpx.HTTPError as e: + logger.error(f"GitHub API请求失败: {str(e)}") + raise HTTPException( + status_code=502, + detail=f"获取GitHub提交历史失败: {str(e)}" + ) + + +@router.get("/changelog", response_model=ChangelogResponse) +async def get_changelog( + page: int = Query(1, ge=1, description="页码"), + per_page: int = Query(30, ge=1, le=100, description="每页数量") +): + """ + 获取更新日志 + + 从GitHub获取项目的提交历史,支持缓存以减少API调用 + + - **page**: 页码,从1开始 + - **per_page**: 每页返回的提交数量,最大100 + """ + try: + # 只缓存第一页 + if page == 1 and is_cache_valid(): + logger.info("使用缓存的更新日志") + return ChangelogResponse( + commits=_cache["data"], + cached=True, + cache_time=_cache["timestamp"].isoformat() + ) + + # 从GitHub获取数据 + logger.info(f"从GitHub获取更新日志 (page={page}, per_page={per_page})") + commits_data = await fetch_github_commits(page, per_page) + + # 解析数据 + commits = [] + for commit_data in commits_data: + try: + commit = GitHubCommit( + sha=commit_data["sha"], + commit=GitHubCommitInfo( + author=GitHubAuthor( + name=commit_data["commit"]["author"]["name"], + email=commit_data["commit"]["author"]["email"], + date=commit_data["commit"]["author"]["date"] + ), + message=commit_data["commit"]["message"] + ), + html_url=commit_data["html_url"], + author=GitHubUser( + login=commit_data["author"]["login"], + avatar_url=commit_data["author"]["avatar_url"] + ) if commit_data.get("author") else None + ) + commits.append(commit) + except (KeyError, TypeError) as e: + logger.warning(f"解析提交数据失败: {str(e)}") + continue + + # 缓存第一页数据 + if page == 1: + _cache["data"] = commits + _cache["timestamp"] = datetime.now() + logger.info("已缓存更新日志") + + return ChangelogResponse( + commits=commits, + cached=False, + cache_time=None + ) + + except HTTPException: + raise + except Exception as e: + logger.error(f"获取更新日志时发生错误: {str(e)}") + raise HTTPException( + status_code=500, + detail=f"获取更新日志失败: {str(e)}" + ) + + +@router.post("/changelog/refresh") +async def refresh_changelog(): + """ + 刷新更新日志缓存 + + 强制从GitHub重新获取最新的提交历史 + """ + try: + logger.info("刷新更新日志缓存") + + # 清除缓存 + _cache["data"] = None + _cache["timestamp"] = None + + # 重新获取 + commits_data = await fetch_github_commits(1, 30) + + # 解析数据 + commits = [] + for commit_data in commits_data: + try: + commit = GitHubCommit( + sha=commit_data["sha"], + commit=GitHubCommitInfo( + author=GitHubAuthor( + name=commit_data["commit"]["author"]["name"], + email=commit_data["commit"]["author"]["email"], + date=commit_data["commit"]["author"]["date"] + ), + message=commit_data["commit"]["message"] + ), + html_url=commit_data["html_url"], + author=GitHubUser( + login=commit_data["author"]["login"], + avatar_url=commit_data["author"]["avatar_url"] + ) if commit_data.get("author") else None + ) + commits.append(commit) + except (KeyError, TypeError) as e: + logger.warning(f"解析提交数据失败: {str(e)}") + continue + + # 更新缓存 + _cache["data"] = commits + _cache["timestamp"] = datetime.now() + + return { + "success": True, + "message": "缓存已刷新", + "commit_count": len(commits), + "cache_time": _cache["timestamp"].isoformat() + } + + except Exception as e: + logger.error(f"刷新缓存时发生错误: {str(e)}") + raise HTTPException( + status_code=500, + detail=f"刷新缓存失败: {str(e)}" + ) \ No newline at end of file diff --git a/backend/app/api/chapters.py b/backend/app/api/chapters.py index 6e90595..ccafe99 100644 --- a/backend/app/api/chapters.py +++ b/backend/app/api/chapters.py @@ -104,8 +104,8 @@ async def create_chapter( user_id = getattr(request.state, 'user_id', None) project = await verify_project_access(chapter.project_id, user_id, db) - # 计算字数 - word_count = len(chapter.content) + # 计算字数(处理content可能为None的情况) + word_count = len(chapter.content) if chapter.content else 0 db_chapter = Chapter( **chapter.model_dump(), @@ -300,9 +300,9 @@ async def update_chapter( for field, value in update_data.items(): setattr(chapter, field, value) - # 如果内容更新了,重新计算字数 - if "content" in update_data and chapter.content: - new_word_count = len(chapter.content) + # 如果内容更新了,重新计算字数(包括清空内容的情况) + if "content" in update_data: + new_word_count = len(chapter.content) if chapter.content else 0 chapter.word_count = new_word_count # 更新项目字数 @@ -312,6 +312,47 @@ async def update_chapter( project = result.scalar_one_or_none() if project: project.current_words = project.current_words - old_word_count + new_word_count + + # 如果内容被清空,清理相关数据 + if not chapter.content or chapter.content.strip() == "": + chapter.status = "draft" + + # 清理分析任务 + analysis_tasks_result = await db.execute( + select(AnalysisTask).where(AnalysisTask.chapter_id == chapter_id) + ) + analysis_tasks = analysis_tasks_result.scalars().all() + for task in analysis_tasks: + await db.delete(task) + + # 清理分析结果 + plot_analysis_result = await db.execute( + select(PlotAnalysis).where(PlotAnalysis.chapter_id == chapter_id) + ) + plot_analyses = plot_analysis_result.scalars().all() + for analysis in plot_analyses: + await db.delete(analysis) + + # 清理故事记忆(关系数据库) + story_memories_result = await db.execute( + select(StoryMemory).where(StoryMemory.chapter_id == chapter_id) + ) + story_memories = story_memories_result.scalars().all() + for memory in story_memories: + await db.delete(memory) + + # 清理向量数据库中的记忆数据 + try: + await memory_service.delete_chapter_memories( + user_id=user_id, + project_id=chapter.project_id, + chapter_id=chapter_id + ) + logger.info(f"✅ 已清理章节 {chapter_id[:8]} 的向量记忆数据") + except Exception as e: + logger.warning(f"⚠️ 清理向量记忆数据失败: {str(e)}") + + logger.info(f"🗑️ 章节 {chapter_id[:8]} 内容已清空,已清理分析和记忆数据") await db.commit() await db.refresh(chapter) @@ -954,6 +995,7 @@ async def generate_chapter_content_stream( target_word_count = generate_request.target_word_count or 3000 enable_mcp = generate_request.enable_mcp if hasattr(generate_request, 'enable_mcp') else True custom_model = generate_request.model if hasattr(generate_request, 'model') else None + temp_narrative_perspective = generate_request.narrative_perspective if hasattr(generate_request, 'narrative_perspective') else None # 预先验证章节存在性(使用临时会话) async for temp_db in get_db(request): try: @@ -1195,6 +1237,14 @@ async def generate_chapter_content_stream( logger.warning(f"⚠️ MCP工具调用失败,降级为基础模式: {str(e)}") yield f"data: {json.dumps({'type': 'progress', 'message': '⚠️ MCP工具暂时不可用,使用基础模式', 'progress': 32}, ensure_ascii=False)}\n\n" + # 🎭 确定使用的叙事人称(临时指定 > 项目默认 > 系统默认) + chapter_perspective = ( + temp_narrative_perspective or + project.narrative_perspective or + '第三人称' + ) + logger.info(f"📝 使用叙事人称: {chapter_perspective}") + # 📋 根据大纲模式构建差异化的章节大纲上下文 chapter_outline_content = "" if outline_mode == 'one-to-one': @@ -1245,7 +1295,7 @@ async def generate_chapter_content_stream( title=project.title, theme=project.theme or '', genre=project.genre or '', - narrative_perspective=project.narrative_perspective or '第三人称', + narrative_perspective=chapter_perspective, time_period=project.world_time_period or '未设定', location=project.world_location or '未设定', atmosphere=project.world_atmosphere or '未设定', @@ -1278,7 +1328,7 @@ async def generate_chapter_content_stream( title=project.title, theme=project.theme or '', genre=project.genre or '', - narrative_perspective=project.narrative_perspective or '第三人称', + narrative_perspective=chapter_perspective, time_period=project.world_time_period or '未设定', location=project.world_location or '未设定', atmosphere=project.world_atmosphere or '未设定', diff --git a/backend/app/api/outlines.py b/backend/app/api/outlines.py index db9ed7b..72609e5 100644 --- a/backend/app/api/outlines.py +++ b/backend/app/api/outlines.py @@ -75,14 +75,30 @@ async def create_outline( request: Request, db: AsyncSession = Depends(get_db) ): - """创建新的章节大纲(不自动创建章节,需通过展开功能生成章节)""" + """创建新的章节大纲(one-to-one模式会自动创建对应章节)""" # 验证用户权限 user_id = getattr(request.state, 'user_id', None) - await verify_project_access(outline.project_id, user_id, db) + project = await verify_project_access(outline.project_id, user_id, db) # 创建大纲 db_outline = Outline(**outline.model_dump()) db.add(db_outline) + await db.flush() # 确保大纲有ID + + # 如果是one-to-one模式,自动创建对应的章节 + if project.outline_mode == 'one-to-one': + chapter = Chapter( + project_id=outline.project_id, + title=db_outline.title, + summary=db_outline.content, + chapter_number=db_outline.order_index, + sub_index=1, + outline_id=None, # one-to-one模式不关联outline_id + status='pending', + content="" + ) + db.add(chapter) + logger.info(f"一对一模式:为手动创建的大纲 {db_outline.title} (序号{db_outline.order_index}) 自动创建了对应章节") await db.commit() await db.refresh(db_outline) diff --git a/backend/app/main.py b/backend/app/main.py index eed4af5..069195e 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -142,7 +142,8 @@ from app.api import ( projects, outlines, characters, chapters, wizard_stream, relationships, organizations, auth, users, settings, writing_styles, memories, - mcp_plugins, admin, inspiration, prompt_templates + mcp_plugins, admin, inspiration, prompt_templates, + changelog ) app.include_router(auth.router, prefix="/api") @@ -162,6 +163,7 @@ app.include_router(writing_styles.router, prefix="/api") app.include_router(memories.router) # 记忆管理API (已包含/api前缀) app.include_router(mcp_plugins.router, prefix="/api") # MCP插件管理API app.include_router(prompt_templates.router, prefix="/api") # 提示词模板管理API +app.include_router(changelog.router, prefix="/api") # 更新日志API static_dir = Path(__file__).parent.parent / "static" if static_dir.exists(): diff --git a/backend/app/schemas/chapter.py b/backend/app/schemas/chapter.py index 2427a10..fe72893 100644 --- a/backend/app/schemas/chapter.py +++ b/backend/app/schemas/chapter.py @@ -79,6 +79,7 @@ class ChapterGenerateRequest(BaseModel): ) enable_mcp: bool = Field(True, description="是否启用MCP工具增强(搜索参考资料)") model: Optional[str] = Field(None, description="指定使用的AI模型,不提供则使用用户默认模型") + narrative_perspective: Optional[str] = Field(None, description="临时人称视角:first_person/third_person/omniscient,不提供则使用项目默认") class BatchGenerateRequest(BaseModel): diff --git a/frontend/package.json b/frontend/package.json index a6a66e3..a2eec9c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "1.0.10", + "version": "1.0.11", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 59741ec..40f0af6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -23,6 +23,7 @@ import Login from './pages/Login'; import AuthCallback from './pages/AuthCallback'; import ProtectedRoute from './components/ProtectedRoute'; import AppFooter from './components/AppFooter'; +import ChangelogFloatingButton from './components/ChangelogFloatingButton'; import './App.css'; function App() { @@ -60,6 +61,7 @@ function App() { {/* } /> */} + ); diff --git a/frontend/src/components/AppFooter.tsx b/frontend/src/components/AppFooter.tsx index 636b818..97ddb6a 100644 --- a/frontend/src/components/AppFooter.tsx +++ b/frontend/src/components/AppFooter.tsx @@ -291,6 +291,7 @@ export default function AppFooter() { )}
+ ); } \ No newline at end of file diff --git a/frontend/src/components/ChangelogFloatingButton.tsx b/frontend/src/components/ChangelogFloatingButton.tsx new file mode 100644 index 0000000..0cd5445 --- /dev/null +++ b/frontend/src/components/ChangelogFloatingButton.tsx @@ -0,0 +1,28 @@ +import { useState } from 'react'; +import { FloatButton } from 'antd'; +import { FileTextOutlined } from '@ant-design/icons'; +import ChangelogModal from './ChangelogModal'; + +export default function ChangelogFloatingButton() { + const [showChangelog, setShowChangelog] = useState(false); + + return ( +
+ } + type="primary" + tooltip="查看更新日志" + style={{ + right: 24, + bottom: 100, + }} + onClick={() => setShowChangelog(true)} + /> + + setShowChangelog(false)} + /> +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/ChangelogModal.tsx b/frontend/src/components/ChangelogModal.tsx new file mode 100644 index 0000000..eec22dd --- /dev/null +++ b/frontend/src/components/ChangelogModal.tsx @@ -0,0 +1,325 @@ +import { Modal, Timeline, Tag, Avatar, Empty, Spin, Button, Space, Tooltip } from 'antd'; +import { useState, useEffect } from 'react'; +import { + BugOutlined, + StarOutlined, + FileTextOutlined, + BgColorsOutlined, + ThunderboltOutlined, + ExperimentOutlined, + ToolOutlined, + QuestionCircleOutlined, + GithubOutlined, + ReloadOutlined, + ClockCircleOutlined, +} from '@ant-design/icons'; +import { + fetchChangelog, + groupChangelogByDate, + getCachedChangelog, + cacheChangelog, + markChangelogFetched, + shouldFetchChangelog, + clearChangelogCache, + type ChangelogEntry, +} from '../services/changelogService'; + +interface ChangelogModalProps { + visible: boolean; + onClose: () => void; +} + +// 提交类型图标和颜色配置 +const typeConfig: Record = { + feature: { icon: , color: 'green', label: '新功能' }, + fix: { icon: , color: 'red', label: '修复' }, + docs: { icon: , color: 'blue', label: '文档' }, + style: { icon: , color: 'purple', label: '样式' }, + refactor: { icon: , color: 'orange', label: '重构' }, + perf: { icon: , color: 'gold', label: '性能' }, + test: { icon: , color: 'cyan', label: '测试' }, + chore: { icon: , color: 'default', label: '杂项' }, + other: { icon: , color: 'default', label: '其他' }, +}; + +export default function ChangelogModal({ visible, onClose }: ChangelogModalProps) { + const [changelog, setChangelog] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [page, setPage] = useState(1); + const [hasMore, setHasMore] = useState(true); + + // 加载更新日志 + const loadChangelog = async (pageNum: number = 1, append: boolean = false) => { + setLoading(true); + setError(null); + + try { + // 如果是第一页,先尝试使用缓存 + if (pageNum === 1 && !append) { + const cached = getCachedChangelog(); + if (cached && cached.length > 0) { + setChangelog(cached); + + // 后台刷新 + if (shouldFetchChangelog()) { + fetchChangelog(pageNum, 30) + .then(entries => { + setChangelog(entries); + cacheChangelog(entries); + markChangelogFetched(); + }) + .catch(console.error); + } + + setLoading(false); + return; + } + } + + const entries = await fetchChangelog(pageNum, 30); + + if (entries.length === 0) { + setHasMore(false); + } else { + if (append) { + setChangelog(prev => [...prev, ...entries]); + } else { + setChangelog(entries); + // 缓存第一页数据 + if (pageNum === 1) { + cacheChangelog(entries); + markChangelogFetched(); + } + } + } + } catch (err) { + setError(err instanceof Error ? err.message : '获取更新日志失败'); + } finally { + setLoading(false); + } + }; + + // 初始加载 + useEffect(() => { + if (visible) { + loadChangelog(1, false); + setPage(1); + setHasMore(true); + } + }, [visible]); + + // 加载更多 + const handleLoadMore = () => { + const nextPage = page + 1; + setPage(nextPage); + loadChangelog(nextPage, true); + }; + + // 刷新(清除缓存并重新加载) + const handleRefresh = () => { + clearChangelogCache(); + setPage(1); + setHasMore(true); + loadChangelog(1, false); + }; + + // 按日期分组 + const groupedChangelog = groupChangelogByDate(changelog); + const sortedDates = Array.from(groupedChangelog.keys()).sort((a, b) => b.localeCompare(a)); + + // 格式化日期 + const formatDate = (dateStr: string) => { + const date = new Date(dateStr); + const now = new Date(); + const diffDays = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24)); + + if (diffDays === 0) return '今天'; + if (diffDays === 1) return '昨天'; + if (diffDays < 7) return `${diffDays} 天前`; + + return date.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' }); + }; + + // 格式化时间 + const formatTime = (dateStr: string) => { + return new Date(dateStr).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }); + }; + + return ( + + + 更新日志 + + + + )} + + {!hasMore && changelog.length > 0 && ( +
+ 已显示所有更新日志 +
+ )} + + )} + +
+ 💡 提示:更新日志每小时自动刷新一次,数据来源于 GitHub 提交历史 +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/Chapters.tsx b/frontend/src/pages/Chapters.tsx index 67238dc..29b46da 100644 --- a/frontend/src/pages/Chapters.tsx +++ b/frontend/src/pages/Chapters.tsx @@ -1,9 +1,9 @@ import { useState, useEffect, useRef, useMemo } from 'react'; import { List, Button, Modal, Form, Input, Select, message, Empty, Space, Badge, Tag, Card, Tooltip, InputNumber, 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, FormOutlined } from '@ant-design/icons'; +import { EditOutlined, FileTextOutlined, ThunderboltOutlined, LockOutlined, DownloadOutlined, SettingOutlined, FundOutlined, SyncOutlined, CheckCircleOutlined, CloseCircleOutlined, RocketOutlined, StopOutlined, InfoCircleOutlined, CaretRightOutlined, DeleteOutlined, BookOutlined, FormOutlined, PlusOutlined } from '@ant-design/icons'; import { useStore } from '../store'; import { useChapterSync } from '../store/hooks'; -import { projectApi, writingStyleApi } from '../services/api'; +import { projectApi, writingStyleApi, chapterApi } from '../services/api'; import type { Chapter, ChapterUpdate, ApiError, WritingStyle, AnalysisTask, ExpansionPlanData } from '../types'; import ChapterAnalysis from '../components/ChapterAnalysis'; import ExpansionPlanEditor from '../components/ExpansionPlanEditor'; @@ -30,6 +30,7 @@ export default function Chapters() { const [availableModels, setAvailableModels] = useState>([]); const [selectedModel, setSelectedModel] = useState(); const [batchSelectedModel, setBatchSelectedModel] = useState(); // 批量生成的模型选择 + const [temporaryNarrativePerspective, setTemporaryNarrativePerspective] = useState(); // 临时人称选择 const [analysisVisible, setAnalysisVisible] = useState(false); const [analysisChapterId, setAnalysisChapterId] = useState(null); // 分析任务状态管理 @@ -50,6 +51,7 @@ export default function Chapters() { const [batchGenerating, setBatchGenerating] = useState(false); const [batchTaskId, setBatchTaskId] = useState(null); const [batchForm] = Form.useForm(); + const [manualCreateForm] = Form.useForm(); const [batchProgress, setBatchProgress] = useState<{ status: string; total: number; @@ -260,6 +262,16 @@ export default function Chapters() { if (!currentProject) return null; + // 获取人称的中文显示文本 + const getNarrativePerspectiveText = (perspective?: string): string => { + const texts: Record = { + 'first_person': '第一人称(我)', + 'third_person': '第三人称(他/她)', + 'omniscient': '全知视角', + }; + return texts[perspective || ''] || '第三人称(默认)'; + }; + const canGenerateChapter = (chapter: Chapter): boolean => { if (chapter.chapter_number === 1) { return true; @@ -328,6 +340,7 @@ export default function Chapters() { content: chapter.content, }); setEditingId(id); + setTemporaryNarrativePerspective(undefined); // 重置人称选择 setIsEditorOpen(true); // 打开编辑窗口时加载模型列表 loadAvailableModels(); @@ -379,7 +392,8 @@ export default function Chapters() { setSingleChapterProgress(progressValue); setSingleChapterProgressMessage(progressMsg); }, - selectedModel // 传递选中的模型 + selectedModel, // 传递选中的模型 + temporaryNarrativePerspective // 传递临时人称参数 ); message.success('AI创作成功,正在分析章节内容...'); @@ -692,6 +706,12 @@ export default function Chapters() { current_chapter_number: status.current_chapter_number, }); + // 每次轮询时刷新章节列表和分析状态,实时显示新生成的章节和分析进度 + if (status.completed > 0) { + refreshChapters(); + loadAnalysisTasks(); + } + // 任务完成或失败,停止轮询 if (status.status === 'completed' || status.status === 'failed' || status.status === 'cancelled') { if (batchPollingIntervalRef.current) { @@ -701,11 +721,12 @@ export default function Chapters() { setBatchGenerating(false); + // 立即刷新章节列表和分析任务状态(在显示消息前) + await refreshChapters(); + await loadAnalysisTasks(); + if (status.status === 'completed') { message.success(`批量生成完成!成功生成 ${status.completed} 章`); - // 刷新章节列表 - refreshChapters(); - loadAnalysisTasks(); } else if (status.status === 'failed') { message.error(`批量生成失败:${status.error_message || '未知错误'}`); } else if (status.status === 'cancelled') { @@ -745,6 +766,10 @@ export default function Chapters() { } message.success('批量生成已取消'); + + // 取消后立即刷新章节列表和分析任务,显示已生成的章节 + await refreshChapters(); + await loadAnalysisTasks(); } catch (error: any) { message.error('取消失败:' + (error.message || '未知错误')); } @@ -790,6 +815,200 @@ export default function Chapters() { setBatchGenerateVisible(true); }; + // 手动创建章节(仅one-to-many模式) + const showManualCreateChapterModal = () => { + // 计算下一个章节号 + const nextChapterNumber = chapters.length > 0 + ? Math.max(...chapters.map(c => c.chapter_number)) + 1 + : 1; + + Modal.confirm({ + title: '手动创建章节', + width: 600, + centered: true, + content: ( +
+ + + + + + + + + + + + + +