From 46debab6241576802d8d46c7a4182ebec48ceb47 Mon Sep 17 00:00:00 2001 From: xiamuceer-j Date: Tue, 13 Jan 2026 16:45:58 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E5=90=8E=E7=AB=AF=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E9=87=8D=E6=9E=84=EF=BC=8C=E6=8F=90=E5=8F=96=E9=80=9A?= =?UTF-8?q?=E7=94=A8=E6=9D=83=E9=99=90=E9=AA=8C=E8=AF=81=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E8=87=B3common=E6=A8=A1=E5=9D=97=EF=BC=8C=E5=87=8F=E5=B0=91?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E5=86=97=E4=BD=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + backend/app/api/careers.py | 21 +- backend/app/api/chapters.py | 34 +- backend/app/api/common.py | 100 ++++ backend/app/api/memories.py | 21 +- backend/app/api/organizations.py | 21 +- backend/app/api/outlines.py | 34 +- backend/app/api/relationships.py | 21 +- backend/requirements.txt | 2 + frontend/src/index.css | 23 +- frontend/src/pages/Chapters.tsx | 356 ++++++------- frontend/src/pages/Characters.tsx | 856 +++++++++++++++++------------- frontend/src/pages/Outline.tsx | 114 ++-- frontend/vite.config.ts | 17 + 14 files changed, 907 insertions(+), 716 deletions(-) create mode 100644 backend/app/api/common.py diff --git a/.gitignore b/.gitignore index 0d9c146..4259454 100644 --- a/.gitignore +++ b/.gitignore @@ -108,6 +108,9 @@ launcher.py launcher.spec mumuainovel.md logo.ico +.embed_cache +dist_embed/ +embed_build.py data/ diff --git a/backend/app/api/careers.py b/backend/app/api/careers.py index 3293bc7..28aed82 100644 --- a/backend/app/api/careers.py +++ b/backend/app/api/careers.py @@ -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, diff --git a/backend/app/api/chapters.py b/backend/app/api/chapters.py index ec77aee..923e328 100644 --- a/backend/app/api/chapters.py +++ b/backend/app/api/chapters.py @@ -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: diff --git a/backend/app/api/common.py b/backend/app/api/common.py new file mode 100644 index 0000000..4a39ff4 --- /dev/null +++ b/backend/app/api/common.py @@ -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) \ No newline at end of file diff --git a/backend/app/api/memories.py b/backend/app/api/memories.py index 9e54566..b2b7ee1 100644 --- a/backend/app/api/memories.py +++ b/backend/app/api/memories.py @@ -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, diff --git a/backend/app/api/organizations.py b/backend/app/api/organizations.py index f988072..85e73a2 100644 --- a/backend/app/api/organizations.py +++ b/backend/app/api/organizations.py @@ -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") diff --git a/backend/app/api/outlines.py b/backend/app/api/outlines.py index 9c91b13..8c9bd8a 100644 --- a/backend/app/api/outlines.py +++ b/backend/app/api/outlines.py @@ -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 diff --git a/backend/app/api/relationships.py b/backend/app/api/relationships.py index ee8ed32..0e45fae 100644 --- a/backend/app/api/relationships.py +++ b/backend/app/api/relationships.py @@ -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)): """获取所有预定义的关系类型""" diff --git a/backend/requirements.txt b/backend/requirements.txt index 0e30257..fa6d9a2 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -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 diff --git a/frontend/src/index.css b/frontend/src/index.css index 7de084e..1ab1b28 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -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; } /* 选中项左侧指示条 */ diff --git a/frontend/src/pages/Chapters.tsx b/frontend/src/pages/Chapters.tsx index 4e4b5a2..f19a1e2 100644 --- a/frontend/src/pages/Chapters.tsx +++ b/frontend/src/pages/Chapters.tsx @@ -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(null); + const contentTextAreaRef = useRef(null); const [writingStyles, setWritingStyles] = useState([]); const [selectedStyleId, setSelectedStyleId] = useState(); const [targetWordCount, setTargetWordCount] = useState(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 = {}; + + 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 = {}; - - 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} /> @@ -2357,11 +2375,27 @@ export default function Chapters() { setBatchGenerateVisible(false); } }} - footer={null} - width={600} + footer={!batchGenerating ? ( + + + + + ) : null} + width={700} centered closable={!batchGenerating} maskClosable={!batchGenerating} + styles={{ + body: { + maxHeight: 'calc(100vh - 260px)', + overflowY: 'auto', + overflowX: 'hidden' + } + }} > {!batchGenerating ? (
!ch.content || ch.content.trim() === '')?.chapter_number || 1, count: 5, - enableAnalysis: true, // 强制启用同步分析 + enableAnalysis: true, styleId: selectedStyleId, targetWordCount: getCachedWordCount(), model: selectedModel, }} > -
  • 严格按章节序号顺序生成,不可跳过
  • -
  • 所有章节使用相同的写作风格和目标字数
  • -
  • 任一章节失败则终止后续生成
  • - - } + message="批量生成说明:严格按序生成 | 统一风格字数 | 任一失败则终止" type="info" showIcon style={{ marginBottom: 16 }} /> - - + {sortedChapters + .filter(ch => !ch.content || ch.content.trim() === '') + .filter(ch => canGenerateChapter(ch)) + .map(ch => ( + + 第{ch.chapter_number}章:{ch.title} + + ))} + + + + + + 5章 + 10章 + 15章 + 20章 + + + + + {/* 第二行:写作风格 + 目标字数 */} +
    + + - + + - - - 5章 - 10章 - 15章 - 20章 - - - - - - - - `${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,68 +2491,44 @@ export default function Chapters() { }} /> -
    - 建议范围:500-10000字(修改后自动记住) -
    -
    +
    - - -
    - {batchSelectedModel ? `当前默认模型: ${availableModels.find(m => m.value === batchSelectedModel)?.label || batchSelectedModel}` : '加载模型列表中...'} -
    -
    + + - - - - - - ✓ 确保职业信息自动更新 - - - ✓ 保证剧情状态连贯 - - - ⏱ 增加约50%耗时 - - - - - - - - - - - - + + + + ✓ 自动更新角色状态 + + + + ) : (
    diff --git a/frontend/src/pages/Characters.tsx b/frontend/src/pages/Characters.tsx index 46eb776..1f31c1b 100644 --- a/frontend/src/pages/Characters.tsx +++ b/frontend/src/pages/Characters.tsx @@ -6,7 +6,7 @@ import { useCharacterSync } from '../store/hooks'; import { characterGridConfig } from '../components/CardStyles'; import { CharacterCard } from '../components/CharacterCard'; import { SSELoadingOverlay } from '../components/SSELoadingOverlay'; -import type { Character } from '../types'; +import type { Character, ApiError } from '../types'; import { characterApi } from '../services/api'; import { SSEPostClient } from '../utils/sseClient'; import api from '../services/api'; @@ -21,6 +21,81 @@ interface Career { max_stage: number; } +// 副职业数据类型 +interface SubCareerData { + career_id: string; + stage: number; +} + +// 角色创建表单值类型 +interface CharacterFormValues { + name: string; + age?: string; + gender?: string; + role_type?: string; + personality?: string; + appearance?: string; + relationships?: string; + background?: string; + main_career_id?: string; + main_career_stage?: number; + sub_career_data?: SubCareerData[]; + // 组织字段 + organization_type?: string; + organization_purpose?: string; + organization_members?: string; + power_level?: number; + location?: string; + motto?: string; + color?: string; +} + +// 角色创建数据类型 +interface CharacterCreateData { + project_id: string; + name: string; + is_organization: boolean; + age?: string; + gender?: string; + role_type?: string; + personality?: string; + appearance?: string; + relationships?: string; + background?: string; + main_career_id?: string; + main_career_stage?: number; + sub_careers?: string; + organization_type?: string; + organization_purpose?: string; + organization_members?: string; + power_level?: number; + location?: string; + motto?: string; + color?: string; +} + +// 角色更新数据类型 +interface CharacterUpdateData { + name?: string; + age?: string; + gender?: string; + role_type?: string; + personality?: string; + appearance?: string; + relationships?: string; + background?: string; + main_career_id?: string; + main_career_stage?: number; + sub_careers?: string; + organization_type?: string; + organization_purpose?: string; + organization_members?: string; + power_level?: number; + location?: string; + motto?: string; + color?: string; +} + export default function Characters() { const { currentProject, characters } = useStore(); const [isGenerating, setIsGenerating] = useState(false); @@ -115,8 +190,9 @@ export default function Characters() { message.success('AI生成角色成功'); Modal.destroyAll(); await refreshCharacters(); - } catch (error: any) { - message.error(error.message || 'AI生成失败'); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : 'AI生成失败'; + message.error(errorMessage); } finally { setTimeout(() => { setIsGenerating(false); @@ -168,8 +244,9 @@ export default function Characters() { message.success('AI生成组织成功'); Modal.destroyAll(); await refreshCharacters(); - } catch (error: any) { - message.error(error.message || 'AI生成失败'); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : 'AI生成失败'; + message.error(errorMessage); } finally { setTimeout(() => { setIsGenerating(false); @@ -179,9 +256,9 @@ export default function Characters() { } }; - const handleCreateCharacter = async (values: any) => { + const handleCreateCharacter = async (values: CharacterFormValues) => { try { - const createData: any = { + const createData: CharacterCreateData = { project_id: currentProject.id, name: values.name, is_organization: createType === 'organization', @@ -234,7 +311,7 @@ export default function Characters() { setEditingCharacter(character); // 提取副职业数据(包含职业ID和阶段) - const subCareerData = character.sub_careers?.map((sc: any) => ({ + const subCareerData: SubCareerData[] = character.sub_careers?.map((sc) => ({ career_id: sc.career_id, stage: sc.stage || 1 })) || []; @@ -246,15 +323,13 @@ export default function Characters() { setIsEditModalOpen(true); }; - const handleUpdateCharacter = async (values: any) => { + const handleUpdateCharacter = async (values: CharacterFormValues) => { if (!editingCharacter) return; try { - const updateData: any = { ...values }; - - // 处理副职业数据 - const subCareerData = updateData.sub_career_data; - delete updateData.sub_career_data; + // 提取副职业数据,剩余的作为更新数据 + const { sub_career_data: subCareerData, ...restValues } = values; + const updateData: CharacterUpdateData = { ...restValues }; // 转换为sub_careers格式 if (subCareerData && Array.isArray(subCareerData) && subCareerData.length > 0) { @@ -433,14 +508,16 @@ export default function Characters() { } else { message.error(result.message || '导入失败'); } - } catch (error: any) { - message.error(error.response?.data?.detail || '导入失败'); + } catch (error: unknown) { + const apiError = error as ApiError; + message.error(apiError.response?.data?.detail || '导入失败'); console.error('导入错误:', error); } }, }); - } catch (error: any) { - message.error(error.response?.data?.detail || '文件验证失败'); + } catch (error: unknown) { + const apiError = error as ApiError; + message.error(apiError.response?.data?.detail || '文件验证失败'); console.error('验证错误:', error); } }; @@ -862,298 +939,63 @@ export default function Characters() { editForm.resetFields(); setEditingCharacter(null); }} - footer={null} - centered={!isMobile} - width={isMobile ? '100%' : 600} + footer={ + + + + + } + centered + width={isMobile ? '100%' : 700} style={isMobile ? { top: 0, paddingBottom: 0, maxWidth: '100vw' } : undefined} - styles={isMobile ? { body: { maxHeight: 'calc(100vh - 110px)', overflowY: 'auto' } } : undefined} - > -
    - - - - - - - - {!editingCharacter?.is_organization && ( - - - - - - )} - - - {!editingCharacter?.is_organization && ( - <> - - - - - - - - - - - - - - -