From 1c35b82e61a6d877ddb08dbdb6fbf60ee52a5238 Mon Sep 17 00:00:00 2001 From: xiamuceer Date: Fri, 14 Nov 2025 10:24:53 +0800 Subject: [PATCH] =?UTF-8?q?update:1.=E6=94=AF=E6=8C=81=E6=89=8B=E5=8A=A8?= =?UTF-8?q?=E5=88=9B=E5=BB=BA=E8=A7=92=E8=89=B2=20=E7=BB=84=E7=BB=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/characters.py | 290 ++++++++++++++++++- backend/app/api/organizations.py | 197 ++++++++++++- backend/app/schemas/character.py | 25 ++ frontend/src/pages/Chapters.tsx | 42 ++- frontend/src/pages/Characters.tsx | 398 +++++++++++++++++++++++++-- frontend/src/pages/Organizations.tsx | 79 +----- frontend/src/services/api.ts | 23 ++ frontend/src/store/hooks.ts | 27 +- 8 files changed, 968 insertions(+), 113 deletions(-) diff --git a/backend/app/api/characters.py b/backend/app/api/characters.py index 3880a5b..2253c14 100644 --- a/backend/app/api/characters.py +++ b/backend/app/api/characters.py @@ -3,13 +3,16 @@ from fastapi import APIRouter, Depends, HTTPException, Request from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func import json +from typing import AsyncGenerator from app.database import get_db +from app.utils.sse_response import SSEResponse, create_sse_response from app.models.character import Character from app.models.project import Project from app.models.generation_history import GenerationHistory from app.models.relationship import CharacterRelationship, Organization, OrganizationMember, RelationshipType from app.schemas.character import ( + CharacterCreate, CharacterUpdate, CharacterResponse, CharacterListResponse, @@ -276,6 +279,79 @@ async def delete_character( return {"message": "角色删除成功"} +@router.post("", response_model=CharacterResponse, summary="手动创建角色") +async def create_character( + character_data: CharacterCreate, + request: Request, + db: AsyncSession = Depends(get_db) +): + """ + 手动创建角色或组织 + + - 可以创建普通角色(is_organization=False) + - 也可以创建组织(is_organization=True) + - 如果创建组织且提供了组织额外字段,会自动创建Organization详情记录 + """ + # 验证用户权限 + user_id = getattr(request.state, 'user_id', None) + await verify_project_access(character_data.project_id, user_id, db) + + try: + # 创建角色 + character = Character( + project_id=character_data.project_id, + name=character_data.name, + age=character_data.age, + gender=character_data.gender, + is_organization=character_data.is_organization, + role_type=character_data.role_type or "supporting", + personality=character_data.personality, + background=character_data.background, + appearance=character_data.appearance, + relationships=character_data.relationships, + organization_type=character_data.organization_type, + organization_purpose=character_data.organization_purpose, + organization_members=character_data.organization_members, + traits=character_data.traits, + avatar_url=character_data.avatar_url + ) + db.add(character) + await db.flush() # 获取character.id + + logger.info(f"✅ 手动创建角色成功:{character.name} (ID: {character.id}, 是否组织: {character.is_organization})") + + # 如果是组织,且提供了组织额外字段,自动创建Organization详情记录 + if character.is_organization and ( + character_data.power_level is not None or + character_data.location or + character_data.motto or + character_data.color + ): + organization = Organization( + character_id=character.id, + project_id=character_data.project_id, + member_count=0, + power_level=character_data.power_level or 50, + location=character_data.location, + motto=character_data.motto, + color=character_data.color + ) + db.add(organization) + await db.flush() + logger.info(f"✅ 自动创建组织详情:{character.name} (Org ID: {organization.id})") + + await db.commit() + await db.refresh(character) + + logger.info(f"🎉 成功手动创建角色/组织: {character.name}") + + return character + + except Exception as e: + logger.error(f"手动创建角色失败: {str(e)}") + raise HTTPException(status_code=500, detail=f"创建角色失败: {str(e)}") + + @router.post("/generate", response_model=CharacterResponse, summary="AI生成角色") async def generate_character( request: CharacterGenerateRequest, @@ -649,4 +725,216 @@ async def generate_character( except Exception as e: logger.error(f"生成角色失败: {str(e)}") - raise HTTPException(status_code=500, detail=f"生成角色失败: {str(e)}") \ No newline at end of file + raise HTTPException(status_code=500, detail=f"生成角色失败: {str(e)}") + + +@router.post("/generate-stream", summary="AI生成角色(流式)") +async def generate_character_stream( + request: CharacterGenerateRequest, + http_request: Request, + db: AsyncSession = Depends(get_db), + user_ai_service: AIService = Depends(get_user_ai_service) +): + """ + 使用AI生成角色卡(支持SSE流式进度显示) + + 通过Server-Sent Events返回实时进度信息 + """ + async def generate() -> AsyncGenerator[str, None]: + try: + # 验证用户权限和项目是否存在 + user_id = getattr(http_request.state, 'user_id', None) + project = await verify_project_access(request.project_id, user_id, db) + + yield await SSEResponse.send_progress("开始生成角色...", 0) + + # 获取已存在的角色列表 + yield await SSEResponse.send_progress("获取项目上下文...", 10) + + existing_chars_result = await db.execute( + select(Character) + .where(Character.project_id == request.project_id) + .order_by(Character.created_at.desc()) + ) + existing_characters = existing_chars_result.scalars().all() + + # 构建现有角色信息摘要 + existing_chars_info = "" + character_list = [] + organization_list = [] + + if existing_characters: + for c in existing_characters[:10]: + if c.is_organization: + organization_list.append(f"- {c.name} [{c.organization_type or '组织'}]") + else: + character_list.append(f"- {c.name}({c.role_type or '未知'})") + + if character_list: + existing_chars_info += "\n已有角色:\n" + "\n".join(character_list) + if organization_list: + existing_chars_info += "\n\n已有组织:\n" + "\n".join(organization_list) + + # 构建项目上下文 + project_context = f""" +项目信息: +- 书名:{project.title} +- 主题:{project.theme or '未设定'} +- 类型:{project.genre or '未设定'} +- 时间背景:{project.world_time_period or '未设定'} +- 地理位置:{project.world_location or '未设定'} +- 氛围基调:{project.world_atmosphere or '未设定'} +- 世界规则:{project.world_rules or '未设定'} +{existing_chars_info} +""" + + user_input = f""" +用户要求: +- 角色名称:{request.name or '请AI生成'} +- 角色定位:{request.role_type or 'supporting'} +- 背景设定:{request.background or '无特殊要求'} +- 其他要求:{request.requirements or '无'} +""" + + yield await SSEResponse.send_progress("构建AI提示词...", 20) + + prompt = prompt_service.get_single_character_prompt( + project_context=project_context, + user_input=user_input + ) + + yield await SSEResponse.send_progress("调用AI服务生成角色...", 30) + logger.info(f"🎯 开始为项目 {request.project_id} 生成角色(SSE流式)") + + try: + result = await user_ai_service.generate_text_with_mcp( + prompt=prompt, + user_id=user_id, + db_session=db, + enable_mcp=True, + max_tool_rounds=2, + tool_choice="auto", + provider=None, + model=None + ) + + if isinstance(result, dict): + ai_response = result.get('content', '') + else: + ai_response = result + + except Exception as ai_error: + logger.error(f"❌ AI服务调用异常:{str(ai_error)}") + yield await SSEResponse.send_error(f"AI服务调用失败:{str(ai_error)}") + return + + if not ai_response or not ai_response.strip(): + yield await SSEResponse.send_error("AI服务返回空响应") + return + + yield await SSEResponse.send_progress("解析AI响应...", 60) + + # 清理AI响应 + cleaned_response = ai_response.strip() + if cleaned_response.startswith("```json"): + cleaned_response = cleaned_response[7:] + if cleaned_response.startswith("```"): + cleaned_response = cleaned_response[3:] + if cleaned_response.endswith("```"): + cleaned_response = cleaned_response[:-3] + cleaned_response = cleaned_response.strip() + + try: + character_data = json.loads(cleaned_response) + except json.JSONDecodeError as e: + yield await SSEResponse.send_error(f"AI返回的内容无法解析为JSON:{str(e)}") + return + + yield await SSEResponse.send_progress("创建角色记录...", 75) + + # 转换traits + traits_json = json.dumps(character_data.get("traits", []), ensure_ascii=False) if character_data.get("traits") else None + is_organization = character_data.get("is_organization", False) + + # 创建角色 + character = Character( + project_id=request.project_id, + name=character_data.get("name", request.name or "未命名角色"), + age=str(character_data.get("age", "")), + gender=character_data.get("gender"), + is_organization=is_organization, + role_type=request.role_type or "supporting", + personality=character_data.get("personality", ""), + background=character_data.get("background", ""), + appearance=character_data.get("appearance", ""), + relationships=character_data.get("relationships_text", character_data.get("relationships", "")), + organization_type=character_data.get("organization_type") if is_organization else None, + organization_purpose=character_data.get("organization_purpose") if is_organization else None, + organization_members=json.dumps(character_data.get("organization_members", []), ensure_ascii=False) if is_organization else None, + traits=traits_json + ) + db.add(character) + await db.flush() + + logger.info(f"✅ 角色创建成功:{character.name} (ID: {character.id})") + + # 如果是组织,创建Organization详情 + if is_organization: + yield await SSEResponse.send_progress("创建组织详情...", 85) + + org_check = await db.execute( + select(Organization).where(Organization.character_id == character.id) + ) + existing_org = org_check.scalar_one_or_none() + + if not existing_org: + organization = Organization( + character_id=character.id, + project_id=request.project_id, + member_count=0, + power_level=character_data.get("power_level", 50), + location=character_data.get("location"), + motto=character_data.get("motto"), + color=character_data.get("color") + ) + db.add(organization) + await db.flush() + + yield await SSEResponse.send_progress("保存生成历史...", 95) + + # 记录生成历史 + history = GenerationHistory( + project_id=request.project_id, + prompt=prompt, + generated_content=json.dumps(result, ensure_ascii=False) if isinstance(result, dict) else ai_response, + model=user_ai_service.default_model + ) + db.add(history) + + await db.commit() + await db.refresh(character) + + logger.info(f"🎉 成功生成角色: {character.name}") + + yield await SSEResponse.send_progress("角色生成完成!", 100, "success") + + # 发送结果数据 + yield await SSEResponse.send_result({ + "character": { + "id": character.id, + "name": character.name, + "role_type": character.role_type, + "is_organization": character.is_organization + } + }) + + yield await SSEResponse.send_done() + + except HTTPException as he: + logger.error(f"HTTP异常: {he.detail}") + yield await SSEResponse.send_error(he.detail, he.status_code) + except Exception as e: + logger.error(f"生成角色失败: {str(e)}") + yield await SSEResponse.send_error(f"生成角色失败: {str(e)}") + + return create_sse_response(generate()) diff --git a/backend/app/api/organizations.py b/backend/app/api/organizations.py index 287f663..4f86b59 100644 --- a/backend/app/api/organizations.py +++ b/backend/app/api/organizations.py @@ -2,11 +2,12 @@ from fastapi import APIRouter, Depends, HTTPException, Request from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, and_ -from typing import List, Optional +from typing import List, Optional, AsyncGenerator from pydantic import BaseModel, Field import json from app.database import get_db +from app.utils.sse_response import SSEResponse, create_sse_response from app.models.relationship import Organization, OrganizationMember from app.models.character import Character from app.models.project import Project @@ -129,7 +130,7 @@ async def get_organization( return org -@router.post("/", response_model=OrganizationResponse, summary="创建组织") +@router.post("", response_model=OrganizationResponse, summary="创建组织") async def create_organization( organization: OrganizationCreate, request: Request, @@ -616,3 +617,195 @@ async def generate_organization( except Exception as e: logger.error(f"生成组织失败: {str(e)}") raise HTTPException(status_code=500, detail=f"生成组织失败: {str(e)}") + + +@router.post("/generate-stream", summary="AI生成组织(流式)") +async def generate_organization_stream( + gen_request: OrganizationGenerateRequest, + http_request: Request, + db: AsyncSession = Depends(get_db), + user_ai_service: AIService = Depends(get_user_ai_service) +): + """ + 使用AI生成组织设定(支持SSE流式进度显示) + + 通过Server-Sent Events返回实时进度信息 + """ + async def generate() -> AsyncGenerator[str, None]: + try: + # 验证用户权限和项目是否存在 + user_id = getattr(http_request.state, 'user_id', None) + project = await verify_project_access(gen_request.project_id, user_id, db) + + yield await SSEResponse.send_progress("开始生成组织...", 0) + + # 获取已存在的角色和组织列表 + yield await SSEResponse.send_progress("获取项目上下文...", 10) + + existing_chars_result = await db.execute( + select(Character) + .where(Character.project_id == gen_request.project_id) + .order_by(Character.created_at.desc()) + ) + existing_characters = existing_chars_result.scalars().all() + + # 构建现有角色和组织信息摘要 + existing_info = "" + character_list = [] + organization_list = [] + + if existing_characters: + for c in existing_characters[:10]: + if c.is_organization: + organization_list.append(f"- {c.name} [{c.organization_type or '组织'}]") + else: + character_list.append(f"- {c.name}({c.role_type or '未知'})") + + if character_list: + existing_info += "\n已有角色:\n" + "\n".join(character_list) + if organization_list: + existing_info += "\n\n已有组织:\n" + "\n".join(organization_list) + + # 构建项目上下文 + project_context = f""" +项目信息: +- 书名:{project.title} +- 主题:{project.theme or '未设定'} +- 类型:{project.genre or '未设定'} +- 时间背景:{project.world_time_period or '未设定'} +- 地理位置:{project.world_location or '未设定'} +- 氛围基调:{project.world_atmosphere or '未设定'} +- 世界规则:{project.world_rules or '未设定'} +{existing_info} +""" + + user_input = f""" +用户要求: +- 组织名称:{gen_request.name or '请AI生成'} +- 组织类型:{gen_request.organization_type or '请AI根据世界观决定'} +- 背景设定:{gen_request.background or '无特殊要求'} +- 其他要求:{gen_request.requirements or '无'} +""" + + yield await SSEResponse.send_progress("构建AI提示词...", 20) + + prompt = prompt_service.get_single_organization_prompt( + project_context=project_context, + user_input=user_input + ) + + yield await SSEResponse.send_progress("调用AI服务生成组织...", 30) + logger.info(f"🎯 开始为项目 {gen_request.project_id} 生成组织(SSE流式)") + + try: + ai_response = await user_ai_service.generate_text(prompt=prompt) + ai_content = ai_response.get("content", "") if isinstance(ai_response, dict) else str(ai_response) + except Exception as ai_error: + logger.error(f"❌ AI服务调用异常:{str(ai_error)}") + yield await SSEResponse.send_error(f"AI服务调用失败:{str(ai_error)}") + return + + if not ai_content or not ai_content.strip(): + yield await SSEResponse.send_error("AI服务返回空响应") + return + + yield await SSEResponse.send_progress("解析AI响应...", 60) + + # 清理AI响应 + cleaned_response = ai_content.strip() + if cleaned_response.startswith("```json"): + cleaned_response = cleaned_response[7:] + if cleaned_response.startswith("```"): + cleaned_response = cleaned_response[3:] + if cleaned_response.endswith("```"): + cleaned_response = cleaned_response[:-3] + cleaned_response = cleaned_response.strip() + + try: + organization_data = json.loads(cleaned_response) + except json.JSONDecodeError as e: + yield await SSEResponse.send_error(f"AI返回的内容无法解析为JSON:{str(e)}") + return + + yield await SSEResponse.send_progress("创建组织记录...", 75) + + # 创建角色记录(组织也是角色的一种) + character = Character( + project_id=gen_request.project_id, + name=organization_data.get("name", gen_request.name or "未命名组织"), + is_organization=True, + role_type="supporting", + personality=organization_data.get("personality", ""), + background=organization_data.get("background", ""), + appearance=organization_data.get("appearance", ""), + organization_type=organization_data.get("organization_type"), + organization_purpose=organization_data.get("organization_purpose"), + organization_members=json.dumps( + organization_data.get("organization_members", []), + ensure_ascii=False + ), + traits=json.dumps( + organization_data.get("traits", []), + ensure_ascii=False + ) + ) + db.add(character) + await db.flush() + + logger.info(f"✅ 组织角色创建成功:{character.name} (ID: {character.id})") + + yield await SSEResponse.send_progress("创建组织详情...", 85) + + # 自动创建Organization详情记录 + organization = Organization( + character_id=character.id, + project_id=gen_request.project_id, + member_count=0, + power_level=organization_data.get("power_level", 50), + location=organization_data.get("location"), + motto=organization_data.get("motto"), + color=organization_data.get("color") + ) + db.add(organization) + await db.flush() + + logger.info(f"✅ 组织详情创建成功:{character.name} (Org ID: {organization.id})") + + yield await SSEResponse.send_progress("保存生成历史...", 95) + + # 记录生成历史 + history = GenerationHistory( + project_id=gen_request.project_id, + prompt=prompt, + generated_content=ai_content, + model=user_ai_service.default_model + ) + db.add(history) + + await db.commit() + await db.refresh(character) + + logger.info(f"🎉 成功生成组织: {character.name}") + + yield await SSEResponse.send_progress("组织生成完成!", 100, "success") + + # 发送结果数据 + yield await SSEResponse.send_result({ + "character": { + "id": character.id, + "name": character.name, + "organization_type": character.organization_type, + "is_organization": character.is_organization + } + }) + + yield await SSEResponse.send_done() + + except HTTPException as he: + logger.error(f"HTTP异常: {he.detail}") + yield await SSEResponse.send_error(he.detail, he.status_code) + except Exception as e: + logger.error(f"生成组织失败: {str(e)}") + yield await SSEResponse.send_error(f"生成组织失败: {str(e)}") + + return create_sse_response(generate()) diff --git a/backend/app/schemas/character.py b/backend/app/schemas/character.py index 498c811..369c7fd 100644 --- a/backend/app/schemas/character.py +++ b/backend/app/schemas/character.py @@ -21,6 +21,31 @@ class CharacterBase(BaseModel): traits: Optional[str] = Field(None, description="特征标签(JSON)") +class CharacterCreate(BaseModel): + """手动创建角色的请求模型""" + project_id: str = Field(..., description="项目ID") + name: str = Field(..., description="角色/组织姓名") + age: Optional[str] = Field(None, description="年龄") + gender: Optional[str] = Field(None, description="性别") + is_organization: bool = Field(False, description="是否为组织") + role_type: Optional[str] = Field("supporting", description="角色类型:protagonist/supporting/antagonist") + personality: Optional[str] = Field(None, description="性格特点/组织特性") + background: Optional[str] = Field(None, description="背景故事") + appearance: Optional[str] = Field(None, description="外貌特征") + relationships: Optional[str] = Field(None, description="人际关系(JSON)") + organization_type: Optional[str] = Field(None, description="组织类型") + organization_purpose: Optional[str] = Field(None, description="组织目的") + organization_members: Optional[str] = Field(None, description="组织成员(JSON)") + traits: Optional[str] = Field(None, description="特征标签(JSON)") + avatar_url: Optional[str] = Field(None, description="头像URL") + + # 组织额外字段 + power_level: Optional[int] = Field(None, description="组织势力等级(0-100)") + location: Optional[str] = Field(None, description="组织所在地") + motto: Optional[str] = Field(None, description="组织格言/口号") + color: Optional[str] = Field(None, description="组织代表颜色") + + class CharacterUpdate(BaseModel): """更新角色的请求模型""" name: Optional[str] = None diff --git a/frontend/src/pages/Chapters.tsx b/frontend/src/pages/Chapters.tsx index f4d8af0..c174b9d 100644 --- a/frontend/src/pages/Chapters.tsx +++ b/frontend/src/pages/Chapters.tsx @@ -7,6 +7,7 @@ import { projectApi, writingStyleApi } from '../services/api'; import type { Chapter, ChapterUpdate, ApiError, WritingStyle, AnalysisTask } from '../types'; import { cardStyles } from '../components/CardStyles'; import ChapterAnalysis from '../components/ChapterAnalysis'; +import { SSELoadingOverlay } from '../components/SSELoadingOverlay'; const { TextArea } = Input; @@ -30,6 +31,10 @@ export default function Chapters() { const [analysisTasksMap, setAnalysisTasksMap] = useState>({}); const pollingIntervalsRef = useRef>({}); + // 单章节生成进度状态 + const [singleChapterProgress, setSingleChapterProgress] = useState(0); + const [singleChapterProgressMessage, setSingleChapterProgressMessage] = useState(''); + // 批量生成相关状态 const [batchGenerateVisible, setBatchGenerateVisible] = useState(false); const [batchGenerating, setBatchGenerating] = useState(false); @@ -301,17 +306,29 @@ export default function Chapters() { try { setIsContinuing(true); setIsGenerating(true); + setSingleChapterProgress(0); + setSingleChapterProgressMessage('准备开始生成...'); - const result = await generateChapterContentStream(editingId, (content) => { - editorForm.setFieldsValue({ content }); - - if (contentTextAreaRef.current) { - const textArea = contentTextAreaRef.current.resizableTextArea?.textArea; - if (textArea) { - textArea.scrollTop = textArea.scrollHeight; + const result = await generateChapterContentStream( + editingId, + (content) => { + editorForm.setFieldsValue({ content }); + + if (contentTextAreaRef.current) { + const textArea = contentTextAreaRef.current.resizableTextArea?.textArea; + if (textArea) { + textArea.scrollTop = textArea.scrollHeight; + } } + }, + selectedStyleId, + targetWordCount, + (progressMsg, progressValue) => { + // 进度回调 + setSingleChapterProgress(progressValue); + setSingleChapterProgressMessage(progressMsg); } - }, selectedStyleId, targetWordCount); + ); message.success('AI创作成功,正在分析章节内容...'); @@ -338,6 +355,8 @@ export default function Chapters() { } finally { setIsContinuing(false); setIsGenerating(false); + setSingleChapterProgress(0); + setSingleChapterProgressMessage(''); } }; @@ -1378,6 +1397,13 @@ export default function Chapters() { )} + + {/* 单章节生成进度显示 */} + ); } \ No newline at end of file diff --git a/frontend/src/pages/Characters.tsx b/frontend/src/pages/Characters.tsx index 77a2bfb..7451280 100644 --- a/frontend/src/pages/Characters.tsx +++ b/frontend/src/pages/Characters.tsx @@ -1,12 +1,14 @@ import { useState, useEffect } from 'react'; -import { Button, Modal, Form, Input, Select, message, Row, Col, Empty, Tabs, Divider, Typography, Space } from 'antd'; -import { ThunderboltOutlined, UserOutlined, TeamOutlined } from '@ant-design/icons'; +import { Button, Modal, Form, Input, Select, message, Row, Col, Empty, Tabs, Divider, Typography, Space, InputNumber } from 'antd'; +import { ThunderboltOutlined, UserOutlined, TeamOutlined, PlusOutlined } from '@ant-design/icons'; import { useStore } from '../store'; import { useCharacterSync } from '../store/hooks'; import { characterGridConfig } from '../components/CardStyles'; import { CharacterCard } from '../components/CharacterCard'; +import { SSELoadingOverlay } from '../components/SSELoadingOverlay'; import type { Character, CharacterUpdate } from '../types'; import { characterApi } from '../services/api'; +import { SSEPostClient } from '../utils/sseClient'; const { Title } = Typography; @@ -15,16 +17,21 @@ const { TextArea } = Input; export default function Characters() { const { currentProject, characters } = useStore(); const [isGenerating, setIsGenerating] = useState(false); + const [progress, setProgress] = useState(0); + const [progressMessage, setProgressMessage] = useState(''); const [activeTab, setActiveTab] = useState<'all' | 'character' | 'organization'>('all'); const [generateForm] = Form.useForm(); + const [generateOrgForm] = Form.useForm(); + const [createForm] = Form.useForm(); const [editForm] = Form.useForm(); const [isEditModalOpen, setIsEditModalOpen] = useState(false); + const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); + const [createType, setCreateType] = useState<'character' | 'organization'>('character'); const [editingCharacter, setEditingCharacter] = useState(null); const { refreshCharacters, - deleteCharacter, - generateCharacter + deleteCharacter } = useCharacterSync(); useEffect(() => { @@ -48,18 +55,140 @@ export default function Characters() { const handleGenerate = async (values: { name?: string; role_type: string; background?: string }) => { try { setIsGenerating(true); - await generateCharacter({ - project_id: currentProject.id, - name: values.name, - role_type: values.role_type, - background: values.background, - }); + setProgress(0); + setProgressMessage('准备生成角色...'); + + const client = new SSEPostClient( + '/api/characters/generate-stream', + { + project_id: currentProject.id, + name: values.name, + role_type: values.role_type, + background: values.background, + }, + { + onProgress: (msg, prog) => { + setProgress(prog); + setProgressMessage(msg); + }, + onResult: (data) => { + console.log('角色生成完成:', data); + }, + onError: (error) => { + message.error(`生成失败: ${error}`); + }, + onComplete: () => { + setProgress(100); + setProgressMessage('生成完成!'); + } + } + ); + + await client.connect(); message.success('AI生成角色成功'); Modal.destroyAll(); - } catch { - message.error('AI生成失败'); + await refreshCharacters(); + } catch (error: any) { + message.error(error.message || 'AI生成失败'); } finally { - setIsGenerating(false); + setTimeout(() => { + setIsGenerating(false); + setProgress(0); + setProgressMessage(''); + }, 500); + } + }; + + const handleGenerateOrganization = async (values: { + name?: string; + organization_type?: string; + background?: string; + requirements?: string; + }) => { + try { + setIsGenerating(true); + setProgress(0); + setProgressMessage('准备生成组织...'); + + const client = new SSEPostClient( + '/api/organizations/generate-stream', + { + project_id: currentProject.id, + name: values.name, + organization_type: values.organization_type, + background: values.background, + requirements: values.requirements, + }, + { + onProgress: (msg, prog) => { + setProgress(prog); + setProgressMessage(msg); + }, + onResult: (data) => { + console.log('组织生成完成:', data); + }, + onError: (error) => { + message.error(`生成失败: ${error}`); + }, + onComplete: () => { + setProgress(100); + setProgressMessage('生成完成!'); + } + } + ); + + await client.connect(); + message.success('AI生成组织成功'); + Modal.destroyAll(); + await refreshCharacters(); + } catch (error: any) { + message.error(error.message || 'AI生成失败'); + } finally { + setTimeout(() => { + setIsGenerating(false); + setProgress(0); + setProgressMessage(''); + }, 500); + } + }; + + const handleCreateCharacter = async (values: any) => { + try { + const createData: any = { + project_id: currentProject.id, + name: values.name, + is_organization: createType === 'organization', + }; + + if (createType === 'character') { + // 角色字段 + createData.age = values.age; + createData.gender = values.gender; + createData.role_type = values.role_type || 'supporting'; + createData.personality = values.personality; + createData.appearance = values.appearance; + createData.relationships = values.relationships; + createData.background = values.background; + } else { + // 组织字段 + createData.organization_type = values.organization_type; + createData.organization_purpose = values.organization_purpose; + createData.organization_members = values.organization_members; + createData.background = values.background; + createData.power_level = values.power_level; + createData.location = values.location; + createData.motto = values.motto; + createData.color = values.color; + createData.role_type = 'supporting'; // 组织默认为配角 + } + + await characterApi.createCharacter(createData); + message.success(`${createType === 'character' ? '角色' : '组织'}创建成功`); + setIsCreateModalOpen(false); + createForm.resetFields(); + await refreshCharacters(); + } catch { + message.error('创建失败'); } }; @@ -126,6 +255,42 @@ export default function Characters() { }); }; + const showGenerateOrgModal = () => { + Modal.confirm({ + title: 'AI生成组织', + width: 600, + centered: true, + content: ( +
+ + + + + + + +