refactor: 后端代码重构,提取通用权限验证逻辑至common模块,减少代码冗余
This commit is contained in:
@@ -108,6 +108,9 @@ launcher.py
|
||||
launcher.spec
|
||||
mumuainovel.md
|
||||
logo.ico
|
||||
.embed_cache
|
||||
dist_embed/
|
||||
embed_build.py
|
||||
|
||||
|
||||
data/
|
||||
|
||||
@@ -27,31 +27,12 @@ from app.schemas.career import (
|
||||
from app.services.ai_service import AIService
|
||||
from app.logger import get_logger
|
||||
from app.api.settings import get_user_ai_service
|
||||
from app.api.common import verify_project_access
|
||||
|
||||
router = APIRouter(prefix="/careers", tags=["职业管理"])
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
async def verify_project_access(project_id: str, user_id: str, db: AsyncSession) -> Project:
|
||||
"""验证用户是否有权访问指定项目"""
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="未登录")
|
||||
|
||||
result = await db.execute(
|
||||
select(Project).where(
|
||||
Project.id == project_id,
|
||||
Project.user_id == user_id
|
||||
)
|
||||
)
|
||||
project = result.scalar_one_or_none()
|
||||
|
||||
if not project:
|
||||
logger.warning(f"项目访问被拒绝: project_id={project_id}, user_id={user_id}")
|
||||
raise HTTPException(status_code=404, detail="项目不存在或无权访问")
|
||||
|
||||
return project
|
||||
|
||||
|
||||
@router.get("", response_model=CareerListResponse, summary="获取职业列表")
|
||||
async def get_careers(
|
||||
project_id: str,
|
||||
|
||||
@@ -10,6 +10,7 @@ from datetime import datetime
|
||||
from asyncio import Queue, Lock
|
||||
|
||||
from app.database import get_db
|
||||
from app.api.common import verify_project_access
|
||||
from app.services.chapter_context_service import ChapterContextBuilder, FocusedMemoryRetriever
|
||||
from app.models.chapter import Chapter
|
||||
from app.models.project import Project
|
||||
@@ -54,39 +55,6 @@ logger = get_logger(__name__)
|
||||
db_write_locks: dict[str, Lock] = {}
|
||||
|
||||
|
||||
async def verify_project_access(project_id: str, user_id: str, db: AsyncSession) -> Project:
|
||||
"""
|
||||
验证用户是否有权访问指定项目
|
||||
|
||||
Args:
|
||||
project_id: 项目ID
|
||||
user_id: 用户ID
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
Project: 项目对象
|
||||
|
||||
Raises:
|
||||
HTTPException: 401 未登录,404 项目不存在或无权访问
|
||||
"""
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="未登录")
|
||||
|
||||
result = await db.execute(
|
||||
select(Project).where(
|
||||
Project.id == project_id,
|
||||
Project.user_id == user_id
|
||||
)
|
||||
)
|
||||
project = result.scalar_one_or_none()
|
||||
|
||||
if not project:
|
||||
logger.warning(f"项目访问被拒绝: project_id={project_id}, user_id={user_id}")
|
||||
raise HTTPException(status_code=404, detail="项目不存在或无权访问")
|
||||
|
||||
return project
|
||||
|
||||
|
||||
async def get_db_write_lock(user_id: str) -> Lock:
|
||||
"""获取或创建用户的数据库写入锁"""
|
||||
if user_id not in db_write_locks:
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
"""API 公共函数模块
|
||||
|
||||
包含跨 API 模块共享的通用函数和工具。
|
||||
"""
|
||||
from fastapi import HTTPException, Request
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from typing import Optional
|
||||
|
||||
from app.models.project import Project
|
||||
from app.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
async def verify_project_access(
|
||||
project_id: str,
|
||||
user_id: Optional[str],
|
||||
db: AsyncSession
|
||||
) -> Project:
|
||||
"""
|
||||
验证用户是否有权访问指定项目
|
||||
|
||||
统一的项目访问验证函数,确保:
|
||||
1. 用户已登录
|
||||
2. 项目存在
|
||||
3. 用户有权访问该项目
|
||||
|
||||
Args:
|
||||
project_id: 项目ID
|
||||
user_id: 用户ID(从 request.state.user_id 获取)
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
Project: 验证通过后返回项目对象
|
||||
|
||||
Raises:
|
||||
HTTPException:
|
||||
- 401: 用户未登录
|
||||
- 404: 项目不存在或用户无权访问
|
||||
"""
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="未登录")
|
||||
|
||||
result = await db.execute(
|
||||
select(Project).where(
|
||||
Project.id == project_id,
|
||||
Project.user_id == user_id
|
||||
)
|
||||
)
|
||||
project = result.scalar_one_or_none()
|
||||
|
||||
if not project:
|
||||
logger.warning(f"项目访问被拒绝: project_id={project_id}, user_id={user_id}")
|
||||
raise HTTPException(status_code=404, detail="项目不存在或无权访问")
|
||||
|
||||
return project
|
||||
|
||||
|
||||
def get_user_id(request: Request) -> Optional[str]:
|
||||
"""
|
||||
从请求中获取用户ID
|
||||
|
||||
这是一个便捷函数,用于从 request.state 中提取 user_id。
|
||||
|
||||
Args:
|
||||
request: FastAPI 请求对象
|
||||
|
||||
Returns:
|
||||
用户ID,如果未登录则返回 None
|
||||
"""
|
||||
return getattr(request.state, 'user_id', None)
|
||||
|
||||
|
||||
async def verify_project_access_from_request(
|
||||
project_id: str,
|
||||
request: Request,
|
||||
db: AsyncSession
|
||||
) -> Project:
|
||||
"""
|
||||
从请求中验证项目访问权限(便捷函数)
|
||||
|
||||
结合 get_user_id 和 verify_project_access,简化调用。
|
||||
|
||||
Args:
|
||||
project_id: 项目ID
|
||||
request: FastAPI 请求对象
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
Project: 验证通过后返回项目对象
|
||||
|
||||
Raises:
|
||||
HTTPException: 401/404
|
||||
|
||||
Usage:
|
||||
project = await verify_project_access_from_request(project_id, request, db)
|
||||
"""
|
||||
user_id = get_user_id(request)
|
||||
return await verify_project_access(project_id, user_id, db)
|
||||
@@ -12,32 +12,13 @@ from app.services.plot_analyzer import get_plot_analyzer
|
||||
from app.services.ai_service import create_user_ai_service
|
||||
from app.models.settings import Settings
|
||||
from app.logger import get_logger
|
||||
from app.api.common import verify_project_access
|
||||
import uuid
|
||||
|
||||
logger = get_logger(__name__)
|
||||
router = APIRouter(prefix="/api/memories", tags=["memories"])
|
||||
|
||||
|
||||
async def verify_project_access(project_id: str, user_id: str, db: AsyncSession) -> Project:
|
||||
"""验证用户是否有权访问指定项目"""
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="未登录")
|
||||
|
||||
result = await db.execute(
|
||||
select(Project).where(
|
||||
Project.id == project_id,
|
||||
Project.user_id == user_id
|
||||
)
|
||||
)
|
||||
project = result.scalar_one_or_none()
|
||||
|
||||
if not project:
|
||||
logger.warning(f"项目访问被拒绝: project_id={project_id}, user_id={user_id}")
|
||||
raise HTTPException(status_code=404, detail="项目不存在或无权访问")
|
||||
|
||||
return project
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/analyze-chapter/{chapter_id}")
|
||||
async def analyze_chapter(
|
||||
project_id: str,
|
||||
|
||||
@@ -27,31 +27,12 @@ from app.services.ai_service import AIService
|
||||
from app.services.prompt_service import prompt_service, PromptService
|
||||
from app.logger import get_logger
|
||||
from app.api.settings import get_user_ai_service
|
||||
from app.api.common import verify_project_access
|
||||
|
||||
router = APIRouter(prefix="/organizations", tags=["组织管理"])
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
async def verify_project_access(project_id: str, user_id: str, db: AsyncSession) -> Project:
|
||||
"""验证用户是否有权访问指定项目"""
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="未登录")
|
||||
|
||||
result = await db.execute(
|
||||
select(Project).where(
|
||||
Project.id == project_id,
|
||||
Project.user_id == user_id
|
||||
)
|
||||
)
|
||||
project = result.scalar_one_or_none()
|
||||
|
||||
if not project:
|
||||
logger.warning(f"项目访问被拒绝: project_id={project_id}, user_id={user_id}")
|
||||
raise HTTPException(status_code=404, detail="项目不存在或无权访问")
|
||||
|
||||
return project
|
||||
|
||||
|
||||
class OrganizationGenerateRequest(BaseModel):
|
||||
"""AI生成组织的请求模型"""
|
||||
project_id: str = Field(..., description="项目ID")
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import List, AsyncGenerator, Dict, Any
|
||||
import json
|
||||
|
||||
from app.database import get_db
|
||||
from app.api.common import verify_project_access
|
||||
from app.models.outline import Outline
|
||||
from app.models.project import Project
|
||||
from app.models.chapter import Chapter
|
||||
@@ -42,39 +43,6 @@ router = APIRouter(prefix="/outlines", tags=["大纲管理"])
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
async def verify_project_access(project_id: str, user_id: str, db: AsyncSession) -> Project:
|
||||
"""
|
||||
验证用户是否有权访问指定项目
|
||||
|
||||
Args:
|
||||
project_id: 项目ID
|
||||
user_id: 用户ID
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
Project: 项目对象
|
||||
|
||||
Raises:
|
||||
HTTPException: 401 未登录,404 项目不存在或无权访问
|
||||
"""
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="未登录")
|
||||
|
||||
result = await db.execute(
|
||||
select(Project).where(
|
||||
Project.id == project_id,
|
||||
Project.user_id == user_id
|
||||
)
|
||||
)
|
||||
project = result.scalar_one_or_none()
|
||||
|
||||
if not project:
|
||||
logger.warning(f"项目访问被拒绝: project_id={project_id}, user_id={user_id}")
|
||||
raise HTTPException(status_code=404, detail="项目不存在或无权访问")
|
||||
|
||||
return project
|
||||
|
||||
|
||||
def _build_chapters_brief(outlines: List[Outline], max_recent: int = 20) -> str:
|
||||
"""构建章节概览字符串"""
|
||||
target = outlines[-max_recent:] if len(outlines) > max_recent else outlines
|
||||
|
||||
@@ -23,31 +23,12 @@ from app.schemas.relationship import (
|
||||
RelationshipGraphLink
|
||||
)
|
||||
from app.logger import get_logger
|
||||
from app.api.common import verify_project_access
|
||||
|
||||
router = APIRouter(prefix="/relationships", tags=["关系管理"])
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
async def verify_project_access(project_id: str, user_id: str, db: AsyncSession) -> Project:
|
||||
"""验证用户是否有权访问指定项目"""
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="未登录")
|
||||
|
||||
result = await db.execute(
|
||||
select(Project).where(
|
||||
Project.id == project_id,
|
||||
Project.user_id == user_id
|
||||
)
|
||||
)
|
||||
project = result.scalar_one_or_none()
|
||||
|
||||
if not project:
|
||||
logger.warning(f"项目访问被拒绝: project_id={project_id}, user_id={user_id}")
|
||||
raise HTTPException(status_code=404, detail="项目不存在或无权访问")
|
||||
|
||||
return project
|
||||
|
||||
|
||||
@router.get("/types", response_model=List[RelationshipTypeResponse], summary="获取关系类型列表")
|
||||
async def get_relationship_types(db: AsyncSession = Depends(get_db)):
|
||||
"""获取所有预定义的关系类型"""
|
||||
|
||||
@@ -8,6 +8,7 @@ sqlalchemy==2.0.25
|
||||
asyncpg==0.29.0 # PostgreSQL异步驱动
|
||||
psycopg2-binary==2.9.9 # PostgreSQL同步驱动(用于迁移脚本)
|
||||
alembic==1.14.0 # 数据库迁移工具
|
||||
aiosqlite==0.22.1
|
||||
|
||||
# 数据验证
|
||||
pydantic==2.12.4
|
||||
@@ -20,6 +21,7 @@ anthropic==0.72.0
|
||||
# 工具库
|
||||
httpx==0.28.1
|
||||
python-dotenv==1.1.0
|
||||
psutil==6.1.1
|
||||
# MCP官方库(Model Context Protocol Python SDK)
|
||||
mcp==1.22.0
|
||||
|
||||
|
||||
+22
-1
@@ -326,8 +326,29 @@ body {
|
||||
font-size: 22px !important;
|
||||
}
|
||||
|
||||
/* 折叠状态下隐藏文字但保持点击区域 */
|
||||
.modern-sider.ant-layout-sider-collapsed .ant-menu-item .ant-menu-title-content {
|
||||
display: none !important;
|
||||
width: 0 !important;
|
||||
overflow: hidden !important;
|
||||
opacity: 0 !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* 确保折叠状态下的 Link 覆盖整个菜单项区域 */
|
||||
.modern-sider.ant-layout-sider-collapsed .ant-menu-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.modern-sider.ant-layout-sider-collapsed .ant-menu-item a {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 选中项左侧指示条 */
|
||||
|
||||
+116
-116
@@ -5,6 +5,7 @@ import { useStore } from '../store';
|
||||
import { useChapterSync } from '../store/hooks';
|
||||
import { projectApi, writingStyleApi, chapterApi } from '../services/api';
|
||||
import type { Chapter, ChapterUpdate, ApiError, WritingStyle, AnalysisTask, ExpansionPlanData } from '../types';
|
||||
import type { TextAreaRef } from 'antd/es/input/TextArea';
|
||||
import ChapterAnalysis from '../components/ChapterAnalysis';
|
||||
import ExpansionPlanEditor from '../components/ExpansionPlanEditor';
|
||||
import { SSELoadingOverlay } from '../components/SSELoadingOverlay';
|
||||
@@ -54,7 +55,7 @@ export default function Chapters() {
|
||||
const [form] = Form.useForm();
|
||||
const [editorForm] = Form.useForm();
|
||||
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
|
||||
const contentTextAreaRef = useRef<any>(null);
|
||||
const contentTextAreaRef = useRef<TextAreaRef>(null);
|
||||
const [writingStyles, setWritingStyles] = useState<WritingStyle[]>([]);
|
||||
const [selectedStyleId, setSelectedStyleId] = useState<number | undefined>();
|
||||
const [targetWordCount, setTargetWordCount] = useState<number>(getCachedWordCount);
|
||||
@@ -124,12 +125,14 @@ export default function Chapters() {
|
||||
|
||||
// 清理轮询定时器
|
||||
useEffect(() => {
|
||||
const pollingIntervals = pollingIntervalsRef.current;
|
||||
const batchPollingInterval = batchPollingIntervalRef.current;
|
||||
return () => {
|
||||
Object.values(pollingIntervalsRef.current).forEach(interval => {
|
||||
Object.values(pollingIntervals).forEach(interval => {
|
||||
clearInterval(interval);
|
||||
});
|
||||
if (batchPollingIntervalRef.current) {
|
||||
clearInterval(batchPollingIntervalRef.current);
|
||||
if (batchPollingInterval) {
|
||||
clearInterval(batchPollingInterval);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
@@ -156,7 +159,7 @@ export default function Chapters() {
|
||||
startPollingTask(chapter.id);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// 404或其他错误表示没有分析任务,忽略
|
||||
console.debug(`章节 ${chapter.id} 暂无分析任务`);
|
||||
}
|
||||
@@ -252,7 +255,7 @@ export default function Chapters() {
|
||||
return settings.llm_model; // 返回模型名称
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
console.log('获取模型列表失败,将使用默认模型');
|
||||
}
|
||||
}
|
||||
@@ -339,6 +342,38 @@ export default function Chapters() {
|
||||
}
|
||||
};
|
||||
|
||||
// 按章节号排序并按大纲分组章节 (必须在早返回之前调用,避免违反 Hooks 规则)
|
||||
const { sortedChapters, groupedChapters } = useMemo(() => {
|
||||
const sorted = [...chapters].sort((a, b) => a.chapter_number - b.chapter_number);
|
||||
|
||||
const groups: Record<string, {
|
||||
outlineId: string | null;
|
||||
outlineTitle: string;
|
||||
outlineOrder: number;
|
||||
chapters: Chapter[];
|
||||
}> = {};
|
||||
|
||||
sorted.forEach(chapter => {
|
||||
const key = chapter.outline_id || 'uncategorized';
|
||||
|
||||
if (!groups[key]) {
|
||||
groups[key] = {
|
||||
outlineId: chapter.outline_id || null,
|
||||
outlineTitle: chapter.outline_title || '未分类章节',
|
||||
outlineOrder: chapter.outline_order ?? 999,
|
||||
chapters: []
|
||||
};
|
||||
}
|
||||
|
||||
groups[key].chapters.push(chapter);
|
||||
});
|
||||
|
||||
// 转换为数组并按大纲顺序排序
|
||||
const grouped = Object.values(groups).sort((a, b) => a.outlineOrder - b.outlineOrder);
|
||||
|
||||
return { sortedChapters: sorted, groupedChapters: grouped };
|
||||
}, [chapters]);
|
||||
|
||||
if (!currentProject) return null;
|
||||
|
||||
// 获取人称的中文显示文本
|
||||
@@ -633,7 +668,7 @@ export default function Chapters() {
|
||||
}
|
||||
await handleGenerate();
|
||||
instance.destroy();
|
||||
} catch (error) {
|
||||
} catch {
|
||||
instance.update({
|
||||
okButtonProps: { danger: true, loading: false },
|
||||
cancelButtonProps: { disabled: false },
|
||||
@@ -670,36 +705,6 @@ export default function Chapters() {
|
||||
return texts[status] || status;
|
||||
};
|
||||
|
||||
const sortedChapters = [...chapters].sort((a, b) => a.chapter_number - b.chapter_number);
|
||||
|
||||
// 按大纲分组章节
|
||||
const groupedChapters = useMemo(() => {
|
||||
const groups: Record<string, {
|
||||
outlineId: string | null;
|
||||
outlineTitle: string;
|
||||
outlineOrder: number;
|
||||
chapters: Chapter[];
|
||||
}> = {};
|
||||
|
||||
sortedChapters.forEach(chapter => {
|
||||
const key = chapter.outline_id || 'uncategorized';
|
||||
|
||||
if (!groups[key]) {
|
||||
groups[key] = {
|
||||
outlineId: chapter.outline_id || null,
|
||||
outlineTitle: chapter.outline_title || '未分类章节',
|
||||
outlineOrder: chapter.outline_order ?? 999,
|
||||
chapters: []
|
||||
};
|
||||
}
|
||||
|
||||
groups[key].chapters.push(chapter);
|
||||
});
|
||||
|
||||
// 转换为数组并按大纲顺序排序
|
||||
return Object.values(groups).sort((a, b) => a.outlineOrder - b.outlineOrder);
|
||||
}, [sortedChapters]);
|
||||
|
||||
const handleExport = () => {
|
||||
if (chapters.length === 0) {
|
||||
message.warning('当前项目没有章节,无法导出');
|
||||
@@ -761,7 +766,14 @@ export default function Chapters() {
|
||||
setBatchGenerating(true);
|
||||
setBatchGenerateVisible(false); // 关闭配置对话框,避免遮挡进度弹窗
|
||||
|
||||
const requestBody: any = {
|
||||
const requestBody: {
|
||||
start_chapter_number: number;
|
||||
count: number;
|
||||
enable_analysis: boolean;
|
||||
style_id: number;
|
||||
target_word_count: number;
|
||||
model?: string;
|
||||
} = {
|
||||
start_chapter_number: values.startChapterNumber,
|
||||
count: values.count,
|
||||
enable_analysis: true,
|
||||
@@ -814,8 +826,9 @@ export default function Chapters() {
|
||||
// 开始轮询任务状态
|
||||
startBatchPolling(result.batch_id);
|
||||
|
||||
} catch (error: any) {
|
||||
message.error('创建批量生成任务失败:' + (error.message || '未知错误'));
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
message.error('创建批量生成任务失败:' + (err.message || '未知错误'));
|
||||
setBatchGenerating(false);
|
||||
setBatchGenerateVisible(false);
|
||||
}
|
||||
@@ -936,8 +949,9 @@ export default function Chapters() {
|
||||
const updatedProject = await projectApi.getProject(currentProject.id);
|
||||
setCurrentProject(updatedProject);
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error('取消失败:' + (error.message || '未知错误'));
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
message.error('取消失败:' + (err.message || '未知错误'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1129,8 +1143,9 @@ export default function Chapters() {
|
||||
setCurrentProject(updatedProject);
|
||||
|
||||
manualCreateForm.resetFields();
|
||||
} catch (error: any) {
|
||||
message.error('操作失败:' + (error.message || '未知错误'));
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
message.error('操作失败:' + (err.message || '未知错误'));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -1154,8 +1169,9 @@ export default function Chapters() {
|
||||
setCurrentProject(updatedProject);
|
||||
|
||||
manualCreateForm.resetFields();
|
||||
} catch (error: any) {
|
||||
message.error('创建失败:' + (error.message || '未知错误'));
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
message.error('创建失败:' + (err.message || '未知错误'));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -1440,8 +1456,9 @@ export default function Chapters() {
|
||||
}
|
||||
|
||||
message.success('章节删除成功');
|
||||
} catch (error: any) {
|
||||
message.error('删除章节失败:' + (error.message || '未知错误'));
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
message.error('删除章节失败:' + (err.message || '未知错误'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1478,8 +1495,9 @@ export default function Chapters() {
|
||||
// 关闭编辑器
|
||||
setPlanEditorVisible(false);
|
||||
setEditingPlanChapter(null);
|
||||
} catch (error: any) {
|
||||
message.error('保存规划失败:' + (error.message || '未知错误'));
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
message.error('保存规划失败:' + (err.message || '未知错误'));
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -2193,7 +2211,7 @@ export default function Chapters() {
|
||||
disabled={isGenerating}
|
||||
style={{ width: '100%' }}
|
||||
formatter={(value) => `${value} 字`}
|
||||
parser={(value) => value?.replace(' 字', '') as any}
|
||||
parser={(value) => parseInt(value?.replace(' 字', '') || '0', 10) as unknown as 500}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -2357,11 +2375,27 @@ export default function Chapters() {
|
||||
setBatchGenerateVisible(false);
|
||||
}
|
||||
}}
|
||||
footer={null}
|
||||
width={600}
|
||||
footer={!batchGenerating ? (
|
||||
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
|
||||
<Button onClick={() => setBatchGenerateVisible(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" icon={<RocketOutlined />} onClick={() => batchForm.submit()}>
|
||||
开始批量生成
|
||||
</Button>
|
||||
</Space>
|
||||
) : null}
|
||||
width={700}
|
||||
centered
|
||||
closable={!batchGenerating}
|
||||
maskClosable={!batchGenerating}
|
||||
styles={{
|
||||
body: {
|
||||
maxHeight: 'calc(100vh - 260px)',
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{!batchGenerating ? (
|
||||
<Form
|
||||
@@ -2371,32 +2405,28 @@ export default function Chapters() {
|
||||
initialValues={{
|
||||
startChapterNumber: sortedChapters.find(ch => !ch.content || ch.content.trim() === '')?.chapter_number || 1,
|
||||
count: 5,
|
||||
enableAnalysis: true, // 强制启用同步分析
|
||||
enableAnalysis: true,
|
||||
styleId: selectedStyleId,
|
||||
targetWordCount: getCachedWordCount(),
|
||||
model: selectedModel,
|
||||
}}
|
||||
>
|
||||
<Alert
|
||||
message="批量生成说明"
|
||||
description={
|
||||
<ul style={{ margin: '8px 0 0 0', paddingLeft: 20 }}>
|
||||
<li>严格按章节序号顺序生成,不可跳过</li>
|
||||
<li>所有章节使用相同的写作风格和目标字数</li>
|
||||
<li>任一章节失败则终止后续生成</li>
|
||||
</ul>
|
||||
}
|
||||
message="批量生成说明:严格按序生成 | 统一风格字数 | 任一失败则终止"
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
|
||||
{/* 第一行:起始章节 + 生成数量 */}
|
||||
<div style={{ display: 'flex', gap: 16 }}>
|
||||
<Form.Item
|
||||
label="起始章节"
|
||||
name="startChapterNumber"
|
||||
rules={[{ required: true, message: '请选择起始章节' }]}
|
||||
rules={[{ required: true, message: '请选择' }]}
|
||||
style={{ flex: 1, marginBottom: 12 }}
|
||||
>
|
||||
<Select placeholder="选择起始章节" size="large">
|
||||
<Select placeholder="选择起始章节">
|
||||
{sortedChapters
|
||||
.filter(ch => !ch.content || ch.content.trim() === '')
|
||||
.filter(ch => canGenerateChapter(ch))
|
||||
@@ -2411,33 +2441,30 @@ export default function Chapters() {
|
||||
<Form.Item
|
||||
label="生成数量"
|
||||
name="count"
|
||||
rules={[{ required: true, message: '请选择生成数量' }]}
|
||||
rules={[{ required: true, message: '请选择' }]}
|
||||
style={{ marginBottom: 12 }}
|
||||
>
|
||||
<Radio.Group buttonStyle="solid" size="large">
|
||||
<Radio.Group buttonStyle="solid">
|
||||
<Radio.Button value={5}>5章</Radio.Button>
|
||||
<Radio.Button value={10}>10章</Radio.Button>
|
||||
<Radio.Button value={15}>15章</Radio.Button>
|
||||
<Radio.Button value={20}>20章</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* 第二行:写作风格 + 目标字数 */}
|
||||
<div style={{ display: 'flex', gap: 16 }}>
|
||||
<Form.Item
|
||||
label="写作风格"
|
||||
name="styleId"
|
||||
rules={[{ required: true, message: '请选择写作风格' }]}
|
||||
tooltip="批量生成时所有章节使用相同的写作风格"
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择写作风格"
|
||||
size="large"
|
||||
showSearch
|
||||
optionFilterProp="children"
|
||||
rules={[{ required: true, message: '请选择' }]}
|
||||
style={{ flex: 1, marginBottom: 12 }}
|
||||
>
|
||||
<Select placeholder="请选择写作风格" showSearch optionFilterProp="children">
|
||||
{writingStyles.map(style => (
|
||||
<Select.Option key={style.id} value={style.id}>
|
||||
{style.name}
|
||||
{style.is_default && ' (默认)'}
|
||||
{style.description && ` - ${style.description}`}
|
||||
{style.name}{style.is_default && ' (默认)'}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
@@ -2445,21 +2472,18 @@ export default function Chapters() {
|
||||
|
||||
<Form.Item
|
||||
label="目标字数"
|
||||
tooltip="AI生成章节时的目标字数,实际生成字数可能略有偏差(修改后会自动记住)"
|
||||
>
|
||||
<Form.Item
|
||||
name="targetWordCount"
|
||||
rules={[{ required: true, message: '请设置目标字数' }]}
|
||||
noStyle
|
||||
rules={[{ required: true, message: '请设置' }]}
|
||||
tooltip="修改后自动记住"
|
||||
style={{ flex: 1, marginBottom: 12 }}
|
||||
>
|
||||
<InputNumber
|
||||
min={500}
|
||||
max={10000}
|
||||
step={100}
|
||||
size="large"
|
||||
style={{ width: '100%' }}
|
||||
formatter={(value) => `${value} 字`}
|
||||
parser={(value) => value?.replace(' 字', '') as any}
|
||||
parser={(value) => parseInt(value?.replace(' 字', '') || '0', 10) as unknown as 500}
|
||||
onChange={(value) => {
|
||||
if (value) {
|
||||
setCachedWordCount(value);
|
||||
@@ -2467,20 +2491,19 @@ export default function Chapters() {
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<div style={{ color: '#666', fontSize: 12, marginTop: 4 }}>
|
||||
建议范围:500-10000字(修改后自动记住)
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
{/* 第三行:AI模型 + 同步分析 */}
|
||||
<div style={{ display: 'flex', gap: 16 }}>
|
||||
<Form.Item
|
||||
label="AI模型"
|
||||
tooltip="批量生成时所有章节使用相同模型,不选择则使用默认模型"
|
||||
tooltip="不选则使用默认模型"
|
||||
style={{ flex: 1, marginBottom: 12 }}
|
||||
>
|
||||
<Select
|
||||
placeholder={batchSelectedModel ? `默认: ${availableModels.find(m => m.value === batchSelectedModel)?.label || batchSelectedModel}` : "使用默认模型"}
|
||||
value={batchSelectedModel}
|
||||
onChange={setBatchSelectedModel}
|
||||
size="large"
|
||||
allowClear
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
@@ -2488,47 +2511,24 @@ export default function Chapters() {
|
||||
{availableModels.map(model => (
|
||||
<Select.Option key={model.value} value={model.value} label={model.label}>
|
||||
{model.label}
|
||||
{model.value === batchSelectedModel && ' (默认)'}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<div style={{ color: '#666', fontSize: 12, marginTop: 4 }}>
|
||||
{batchSelectedModel ? `当前默认模型: ${availableModels.find(m => m.value === batchSelectedModel)?.label || batchSelectedModel}` : '加载模型列表中...'}
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="同步分析"
|
||||
name="enableAnalysis"
|
||||
tooltip="批量生成必须开启同步分析,确保角色职业信息和剧情状态的连贯性"
|
||||
tooltip="必须开启,确保剧情连贯"
|
||||
style={{ marginBottom: 12 }}
|
||||
>
|
||||
<Radio.Group disabled>
|
||||
<Radio value={true}>
|
||||
<Space direction="vertical" size={0}>
|
||||
<span style={{ fontSize: 12, color: '#52c41a' }}>
|
||||
✓ 确保职业信息自动更新
|
||||
</span>
|
||||
<span style={{ fontSize: 12, color: '#52c41a' }}>
|
||||
✓ 保证剧情状态连贯
|
||||
</span>
|
||||
<span style={{ fontSize: 12, color: '#ff9800' }}>
|
||||
⏱ 增加约50%耗时
|
||||
</span>
|
||||
</Space>
|
||||
<span style={{ fontSize: 12, color: '#52c41a' }}>✓ 自动更新角色状态</span>
|
||||
</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
|
||||
<Button onClick={() => setBatchGenerateVisible(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" htmlType="submit" icon={<RocketOutlined />}>
|
||||
开始批量生成
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Form>
|
||||
) : (
|
||||
<div>
|
||||
|
||||
+482
-338
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ import { cardStyles } from '../components/CardStyles';
|
||||
import { SSEPostClient } from '../utils/sseClient';
|
||||
import { SSEProgressModal } from '../components/SSEProgressModal';
|
||||
import { outlineApi, chapterApi, projectApi } from '../services/api';
|
||||
import type { OutlineExpansionResponse, BatchOutlineExpansionResponse } from '../types';
|
||||
import type { OutlineExpansionResponse, BatchOutlineExpansionResponse, ChapterPlanItem, ApiError } from '../types';
|
||||
|
||||
// 角色预测数据类型
|
||||
interface PredictedCharacter {
|
||||
@@ -64,6 +64,42 @@ interface OrganizationConfirmationData {
|
||||
chapter_range: string;
|
||||
}
|
||||
|
||||
// 大纲生成请求数据类型
|
||||
interface OutlineGenerateRequestData {
|
||||
project_id: string;
|
||||
genre: string;
|
||||
theme: string;
|
||||
chapter_count: number;
|
||||
narrative_perspective: string;
|
||||
target_words: number;
|
||||
requirements?: string;
|
||||
mode: 'auto' | 'new' | 'continue';
|
||||
story_direction?: string;
|
||||
plot_stage: 'development' | 'climax' | 'ending';
|
||||
enable_auto_characters: boolean;
|
||||
require_character_confirmation: boolean;
|
||||
enable_auto_organizations: boolean;
|
||||
require_organization_confirmation: boolean;
|
||||
model?: string;
|
||||
provider?: string;
|
||||
confirmed_characters?: PredictedCharacter[];
|
||||
confirmed_organizations?: PredictedOrganization[];
|
||||
}
|
||||
|
||||
// 跳过的大纲信息类型
|
||||
interface SkippedOutlineInfo {
|
||||
outline_id: string;
|
||||
outline_title: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
// 场景类型
|
||||
interface SceneInfo {
|
||||
location: string;
|
||||
characters: string[];
|
||||
purpose: string;
|
||||
}
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
export default function Outline() {
|
||||
@@ -84,7 +120,7 @@ export default function Outline() {
|
||||
// 角色确认相关状态
|
||||
const [characterConfirmData, setCharacterConfirmData] = useState<CharacterConfirmationData | null>(null);
|
||||
const [characterConfirmVisible, setCharacterConfirmVisible] = useState(false);
|
||||
const [pendingGenerateData, setPendingGenerateData] = useState<any>(null);
|
||||
const [pendingGenerateData, setPendingGenerateData] = useState<OutlineGenerateRequestData | null>(null);
|
||||
const [selectedCharacterIndices, setSelectedCharacterIndices] = useState<number[]>([]);
|
||||
|
||||
// 组织确认相关状态
|
||||
@@ -274,7 +310,7 @@ export default function Outline() {
|
||||
setSSEModalVisible(true);
|
||||
|
||||
// 准备请求数据
|
||||
const requestData: any = {
|
||||
const requestData: OutlineGenerateRequestData = {
|
||||
project_id: currentProject.id,
|
||||
genre: currentProject.genre || '通用',
|
||||
theme: values.theme || currentProject.theme || '',
|
||||
@@ -315,10 +351,10 @@ export default function Outline() {
|
||||
setSSEMessage(msg);
|
||||
setSSEProgress(progress);
|
||||
},
|
||||
onResult: (data: any) => {
|
||||
onResult: (data: unknown) => {
|
||||
console.log('生成完成,结果:', data);
|
||||
},
|
||||
onCharacterConfirmation: (data: any) => {
|
||||
onCharacterConfirmation: (data: CharacterConfirmationData) => {
|
||||
// ✨ 新增:处理角色确认事件
|
||||
console.log('收到角色确认请求:', data);
|
||||
// 关闭SSE进度Modal
|
||||
@@ -332,7 +368,7 @@ export default function Outline() {
|
||||
setCharacterConfirmData(data);
|
||||
setCharacterConfirmVisible(true);
|
||||
},
|
||||
onOrganizationConfirmation: (data: any) => {
|
||||
onOrganizationConfirmation: (data: OrganizationConfirmationData) => {
|
||||
// ✨ 新增:处理组织确认事件
|
||||
console.log('收到组织确认请求:', data);
|
||||
// 关闭SSE进度Modal
|
||||
@@ -396,7 +432,7 @@ export default function Outline() {
|
||||
defaultModel = settings.llm_model;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
console.log('获取模型列表失败,将使用默认模型');
|
||||
}
|
||||
}
|
||||
@@ -756,12 +792,13 @@ export default function Outline() {
|
||||
message.success('大纲创建成功');
|
||||
await refreshOutlines();
|
||||
manualCreateForm.resetFields();
|
||||
} catch (error: any) {
|
||||
if (error.message === '序号重复') {
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
if (err.message === '序号重复') {
|
||||
// 序号重复错误已经显示了Modal,不需要再显示message
|
||||
throw error;
|
||||
}
|
||||
message.error('创建失败:' + (error.message || '未知错误'));
|
||||
message.error('创建失败:' + (err.message || '未知错误'));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -970,8 +1007,9 @@ export default function Outline() {
|
||||
const updatedProject = await projectApi.getProject(currentProject.id);
|
||||
setCurrentProject(updatedProject);
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.response?.data?.detail || '删除章节失败');
|
||||
} catch (error: unknown) {
|
||||
const apiError = error as ApiError;
|
||||
message.error(apiError.response?.data?.detail || '删除章节失败');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1003,12 +1041,11 @@ export default function Outline() {
|
||||
title: (
|
||||
<Space style={{ flexWrap: 'wrap' }}>
|
||||
<CheckCircleOutlined style={{ color: 'var(--color-success)' }} />
|
||||
<span>已存在的展开章节</span>
|
||||
<span>《{outlineTitle}》展开信息</span>
|
||||
</Space>
|
||||
),
|
||||
width: isMobile ? '95%' : 900,
|
||||
centered: true,
|
||||
okText: '关闭',
|
||||
style: isMobile ? {
|
||||
top: 20,
|
||||
maxWidth: 'calc(100vw - 16px)',
|
||||
@@ -1016,11 +1053,12 @@ export default function Outline() {
|
||||
} : undefined,
|
||||
styles: {
|
||||
body: {
|
||||
maxHeight: isMobile ? 'calc(100vh - 150px)' : 'calc(80vh - 110px)',
|
||||
overflowY: 'auto'
|
||||
maxHeight: isMobile ? 'calc(100vh - 200px)' : 'calc(80vh - 60px)',
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden'
|
||||
}
|
||||
},
|
||||
footer: (_: any, { OkBtn }: any) => (
|
||||
footer: (
|
||||
<Space wrap style={{ width: '100%', justifyContent: isMobile ? 'center' : 'flex-end' }}>
|
||||
<Button
|
||||
danger
|
||||
@@ -1053,7 +1091,9 @@ export default function Outline() {
|
||||
>
|
||||
删除所有展开的章节 ({data.chapter_count}章)
|
||||
</Button>
|
||||
<OkBtn />
|
||||
<Button onClick={() => Modal.destroyAll()}>
|
||||
关闭
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
content: (
|
||||
@@ -1335,7 +1375,7 @@ export default function Outline() {
|
||||
// 确认创建章节 - 使用缓存的规划数据,避免重复AI调用
|
||||
const handleConfirmCreateChapters = async (
|
||||
outlineId: string,
|
||||
cachedPlans: any[]
|
||||
cachedPlans: ChapterPlanItem[]
|
||||
) => {
|
||||
try {
|
||||
setIsExpanding(true);
|
||||
@@ -1449,7 +1489,7 @@ export default function Outline() {
|
||||
setSSEMessage(msg);
|
||||
setSSEProgress(progress);
|
||||
},
|
||||
onResult: (data: any) => {
|
||||
onResult: (data: BatchOutlineExpansionResponse) => {
|
||||
console.log('批量展开完成,结果:', data);
|
||||
// 缓存AI生成的规划数据
|
||||
setCachedBatchExpansionResponse(data);
|
||||
@@ -1515,7 +1555,7 @@ export default function Outline() {
|
||||
⚠️ 以下大纲已展开过,已自动跳过:
|
||||
</div>
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
{batchPreviewData.skipped_outlines.map((skipped: any, idx: number) => (
|
||||
{batchPreviewData.skipped_outlines.map((skipped: SkippedOutlineInfo, idx: number) => (
|
||||
<div key={idx} style={{ fontSize: 13, color: '#666' }}>
|
||||
• {skipped.outline_title} <Tag color="default" style={{ fontSize: 11 }}>{skipped.reason}</Tag>
|
||||
</div>
|
||||
@@ -1581,7 +1621,7 @@ export default function Outline() {
|
||||
<List
|
||||
size="small"
|
||||
dataSource={batchPreviewData.expansion_results[selectedOutlineIdx].chapter_plans}
|
||||
renderItem={(plan: any, idx: number) => (
|
||||
renderItem={(plan: ChapterPlanItem, idx: number) => (
|
||||
<List.Item
|
||||
key={idx}
|
||||
onClick={() => setSelectedChapterIdx(idx)}
|
||||
@@ -1625,7 +1665,7 @@ export default function Outline() {
|
||||
|
||||
<Card size="small" title="关键事件" bordered={false}>
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
{batchPreviewData.expansion_results[selectedOutlineIdx].chapter_plans[selectedChapterIdx].key_events.map((event: string, eventIdx: number) => (
|
||||
{(batchPreviewData.expansion_results[selectedOutlineIdx].chapter_plans[selectedChapterIdx].key_events as string[]).map((event: string, eventIdx: number) => (
|
||||
<div key={eventIdx}>• {event}</div>
|
||||
))}
|
||||
</Space>
|
||||
@@ -1633,7 +1673,7 @@ export default function Outline() {
|
||||
|
||||
<Card size="small" title="涉及角色" bordered={false}>
|
||||
<Space wrap>
|
||||
{batchPreviewData.expansion_results[selectedOutlineIdx].chapter_plans[selectedChapterIdx].character_focus.map((char: string, charIdx: number) => (
|
||||
{(batchPreviewData.expansion_results[selectedOutlineIdx].chapter_plans[selectedChapterIdx].character_focus as string[]).map((char: string, charIdx: number) => (
|
||||
<Tag key={charIdx} color="purple">{char}</Tag>
|
||||
))}
|
||||
</Space>
|
||||
@@ -1642,7 +1682,7 @@ export default function Outline() {
|
||||
{batchPreviewData.expansion_results[selectedOutlineIdx].chapter_plans[selectedChapterIdx].scenes && batchPreviewData.expansion_results[selectedOutlineIdx].chapter_plans[selectedChapterIdx].scenes!.length > 0 && (
|
||||
<Card size="small" title="场景" bordered={false}>
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
{batchPreviewData.expansion_results[selectedOutlineIdx].chapter_plans[selectedChapterIdx].scenes!.map((scene: any, sceneIdx: number) => (
|
||||
{batchPreviewData.expansion_results[selectedOutlineIdx].chapter_plans[selectedChapterIdx].scenes!.map((scene: SceneInfo, sceneIdx: number) => (
|
||||
<Card key={sceneIdx} size="small" style={{ backgroundColor: '#fafafa' }}>
|
||||
<div><strong>地点:</strong>{scene.location}</div>
|
||||
<div><strong>角色:</strong>{scene.characters.join('、')}</div>
|
||||
@@ -1700,8 +1740,10 @@ export default function Outline() {
|
||||
result.chapter_plans
|
||||
);
|
||||
totalCreated += response.chapters_created;
|
||||
} catch (error: any) {
|
||||
const errorMsg = error.response?.data?.detail || error.message || '未知错误';
|
||||
} catch (error: unknown) {
|
||||
const apiError = error as ApiError;
|
||||
const err = error as Error;
|
||||
const errorMsg = apiError.response?.data?.detail || err.message || '未知错误';
|
||||
errors.push(`${result.outline_title}: ${errorMsg}`);
|
||||
console.error(`创建大纲 ${result.outline_title} 的章节失败:`, error);
|
||||
}
|
||||
@@ -1766,7 +1808,7 @@ export default function Outline() {
|
||||
setSSEMessage(msg);
|
||||
setSSEProgress(progress);
|
||||
},
|
||||
onResult: (data: any) => {
|
||||
onResult: (data: unknown) => {
|
||||
console.log('生成完成,结果:', data);
|
||||
},
|
||||
onError: (error: string) => {
|
||||
@@ -1784,7 +1826,7 @@ export default function Outline() {
|
||||
// 刷新大纲列表
|
||||
refreshOutlines();
|
||||
},
|
||||
onOrganizationConfirmation: (data: any) => {
|
||||
onOrganizationConfirmation: (data: OrganizationConfirmationData) => {
|
||||
// 处理可能的后续组织确认
|
||||
console.log('收到组织确认请求:', data);
|
||||
setSSEModalVisible(false);
|
||||
@@ -1836,10 +1878,10 @@ export default function Outline() {
|
||||
setSSEMessage(msg);
|
||||
setSSEProgress(progress);
|
||||
},
|
||||
onResult: (data: any) => {
|
||||
onResult: (data: unknown) => {
|
||||
console.log('生成完成,结果:', data);
|
||||
},
|
||||
onOrganizationConfirmation: (data: any) => {
|
||||
onOrganizationConfirmation: (data: OrganizationConfirmationData) => {
|
||||
// 处理可能的后续组织确认
|
||||
console.log('收到组织确认请求:', data);
|
||||
setSSEModalVisible(false);
|
||||
@@ -1893,7 +1935,8 @@ export default function Outline() {
|
||||
|
||||
// 准备请求数据,添加确认的组织
|
||||
// ⚠️ 移除 confirmed_characters,避免重复创建角色
|
||||
const { confirmed_characters, ...baseData } = pendingGenerateData;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { confirmed_characters: _unusedChars, ...baseData } = pendingGenerateData;
|
||||
const requestData = {
|
||||
...baseData,
|
||||
confirmed_organizations: selectedOrganizations
|
||||
@@ -1908,7 +1951,7 @@ export default function Outline() {
|
||||
setSSEMessage(msg);
|
||||
setSSEProgress(progress);
|
||||
},
|
||||
onResult: (data: any) => {
|
||||
onResult: (data: unknown) => {
|
||||
console.log('生成完成,结果:', data);
|
||||
},
|
||||
onError: (error: string) => {
|
||||
@@ -1955,7 +1998,8 @@ export default function Outline() {
|
||||
setSSEModalVisible(true);
|
||||
|
||||
// 准备请求数据,禁用自动组织引入
|
||||
const { confirmed_characters, ...baseData } = pendingGenerateData;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { confirmed_characters: _unusedChars, ...baseData } = pendingGenerateData;
|
||||
const requestData = {
|
||||
...baseData,
|
||||
enable_auto_organizations: false // 禁用自动组织引入
|
||||
@@ -1970,7 +2014,7 @@ export default function Outline() {
|
||||
setSSEMessage(msg);
|
||||
setSSEProgress(progress);
|
||||
},
|
||||
onResult: (data: any) => {
|
||||
onResult: (data: unknown) => {
|
||||
console.log('生成完成,结果:', data);
|
||||
},
|
||||
onError: (error: string) => {
|
||||
|
||||
@@ -21,6 +21,23 @@ export default defineConfig({
|
||||
build: {
|
||||
outDir: '../backend/static',
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
// 手动分割代码块,将大型依赖库分离
|
||||
manualChunks: {
|
||||
// React 核心库
|
||||
'vendor-react': ['react', 'react-dom', 'react-router-dom'],
|
||||
// Ant Design UI库(最大的依赖)
|
||||
'vendor-antd': ['antd', '@ant-design/icons'],
|
||||
// 其他工具库
|
||||
'vendor-utils': ['axios', 'dayjs', 'zustand'],
|
||||
// Diff查看器(较大的组件)
|
||||
'vendor-diff': ['react-diff-viewer-continued'],
|
||||
// 拖拽库
|
||||
'vendor-dnd': ['react-beautiful-dnd'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
|
||||
Reference in New Issue
Block a user