update:1.新增AI生成组织功能,扩展优化组织字段(所在地 代表颜色 格言/口号)

2.适配移动端项目管理-剧情分析UI页面
This commit is contained in:
xiamuceer
2025-11-05 16:22:14 +08:00
parent ff58548a79
commit 397ca30bcb
17 changed files with 1155 additions and 185 deletions
-3
View File
@@ -59,9 +59,6 @@ RUN mkdir -p /app/data /app/logs /app/embedding
# 这样可以避免首次运行时联网下载约420MB的模型文件 # 这样可以避免首次运行时联网下载约420MB的模型文件
COPY backend/embedding /app/embedding COPY backend/embedding /app/embedding
# 复制环境变量示例文件
COPY backend/.env.example ./.env.example
# 暴露端口 # 暴露端口
EXPOSE 8000 EXPOSE 8000
+82 -23
View File
@@ -553,36 +553,59 @@ async def analyze_chapter_background(
) )
logger.info(f"✅ 添加{added_count}条记忆到向量库") logger.info(f"✅ 添加{added_count}条记忆到向量库")
# 最终更新任务状态(写操作,需要锁) # 最终更新任务状态(写操作,需要锁)- 增加重试机制
async with write_lock: update_success = False
task.progress = 100 for retry in range(3):
task.status = 'completed' try:
task.completed_at = datetime.now() async with write_lock:
await db_session.commit() task.progress = 100
task.status = 'completed'
task.completed_at = datetime.now()
await db_session.commit()
update_success = True
logger.info(f"✅ 章节分析完成: {chapter_id}, 提取{len(memories)}条记忆")
break
except Exception as commit_error:
logger.error(f"❌ 提交任务完成状态失败(重试{retry+1}/3): {str(commit_error)}")
if retry < 2:
await asyncio.sleep(0.1)
else:
logger.error(f"❌ 无法更新任务为completed状态: {task_id}")
# 即使失败也不抛出异常,因为分析本身已经完成
logger.info(f"✅ 章节分析完成: {chapter_id}, 提取{len(memories)}条记忆") if not update_success:
logger.warning(f"⚠️ 章节分析完成但状态更新失败: {chapter_id}")
except Exception as e: except Exception as e:
logger.error(f"❌ 后台分析异常: {str(e)}", exc_info=True) logger.error(f"❌ 后台分析异常: {str(e)}", exc_info=True)
# 确保任务状态被更新为failed(写操作,需要锁) # 确保任务状态被更新为failed(写操作,需要锁)
if db_session: if db_session:
try: # 多次重试更新任务状态
async with write_lock: for retry in range(3):
task_result = await db_session.execute( try:
select(AnalysisTask).where(AnalysisTask.id == task_id) async with write_lock:
) # 重新获取任务(可能是旧会话导致的问题)
task = task_result.scalar_one_or_none() task_result = await db_session.execute(
if task: select(AnalysisTask).where(AnalysisTask.id == task_id)
task.status = 'failed' )
task.error_message = str(e)[:500] task = task_result.scalar_one_or_none()
task.completed_at = datetime.now() if task:
task.progress = 0 task.status = 'failed'
await db_session.commit() task.error_message = str(e)[:500]
logger.info(f"✅ 任务状态已更新为failed: {task_id}") task.completed_at = datetime.now()
task.progress = 0
await db_session.commit()
logger.info(f"✅ 任务状态已更新为failed: {task_id} (重试{retry+1}次)")
break
else:
logger.error(f"❌ 无法找到任务进行状态更新: {task_id}")
break
except Exception as update_error:
logger.error(f"❌ 更新任务状态失败(重试{retry+1}/3): {str(update_error)}")
if retry < 2:
await asyncio.sleep(0.1) # 短暂等待后重试
else: else:
logger.error(f"无法找到任务进行状态更新: {task_id}") logger.error(f"任务状态更新失败,已达到最大重试次数: {task_id}")
except Exception as update_error:
logger.error(f"❌ 更新任务状态失败: {str(update_error)}")
finally: finally:
if db_session: if db_session:
await db_session.close() await db_session.close()
@@ -956,14 +979,21 @@ async def get_analysis_task_status(
""" """
查询指定章节的最新分析任务状态 查询指定章节的最新分析任务状态
自动恢复机制:
- 如果任务状态为running且超过1分钟未更新,自动标记为failed
- 如果任务状态为pending且超过2分钟未启动,自动标记为failed
返回: 返回:
- task_id: 任务ID - task_id: 任务ID
- status: pending/running/completed/failed - status: pending/running/completed/failed
- progress: 0-100 - progress: 0-100
- error_message: 错误信息(如果失败) - error_message: 错误信息(如果失败)
- auto_recovered: 是否被自动恢复
- created_at: 创建时间 - created_at: 创建时间
- completed_at: 完成时间 - completed_at: 完成时间
""" """
from datetime import timedelta
# 获取该章节最新的分析任务 # 获取该章节最新的分析任务
result = await db.execute( result = await db.execute(
select(AnalysisTask) select(AnalysisTask)
@@ -976,12 +1006,41 @@ async def get_analysis_task_status(
if not task: if not task:
raise HTTPException(status_code=404, detail="未找到分析任务") raise HTTPException(status_code=404, detail="未找到分析任务")
auto_recovered = False
current_time = datetime.now()
# 自动恢复卡住的任务
if task.status == 'running':
# 如果任务在running状态超过1分钟,标记为失败
if task.started_at and (current_time - task.started_at) > timedelta(minutes=1):
task.status = 'failed'
task.error_message = '任务超时(超过1分钟未完成,已自动恢复)'
task.completed_at = current_time
task.progress = 0
auto_recovered = True
await db.commit()
await db.refresh(task)
logger.warning(f"🔄 自动恢复卡住的任务: {task.id}, 章节: {chapter_id}")
elif task.status == 'pending':
# 如果任务在pending状态超过2分钟仍未开始,标记为失败
if task.created_at and (current_time - task.created_at) > timedelta(minutes=2):
task.status = 'failed'
task.error_message = '任务启动超时(超过2分钟未启动,已自动恢复)'
task.completed_at = current_time
task.progress = 0
auto_recovered = True
await db.commit()
await db.refresh(task)
logger.warning(f"🔄 自动恢复未启动的任务: {task.id}, 章节: {chapter_id}")
return { return {
"task_id": task.id, "task_id": task.id,
"chapter_id": task.chapter_id, "chapter_id": task.chapter_id,
"status": task.status, "status": task.status,
"progress": task.progress, "progress": task.progress,
"error_message": task.error_message, "error_message": task.error_message,
"auto_recovered": auto_recovered,
"created_at": task.created_at.isoformat() if task.created_at else None, "created_at": task.created_at.isoformat() if task.created_at else None,
"started_at": task.started_at.isoformat() if task.started_at else None, "started_at": task.started_at.isoformat() if task.started_at else None,
"completed_at": task.completed_at.isoformat() if task.completed_at else None "completed_at": task.completed_at.isoformat() if task.completed_at else None
+94 -11
View File
@@ -44,7 +44,50 @@ async def get_characters(
) )
characters = result.scalars().all() characters = result.scalars().all()
return CharacterListResponse(total=total, items=characters) # 为组织类型的角色填充Organization表的额外字段
enriched_characters = []
for char in characters:
char_dict = {
"id": char.id,
"project_id": char.project_id,
"name": char.name,
"age": char.age,
"gender": char.gender,
"is_organization": char.is_organization,
"role_type": char.role_type,
"personality": char.personality,
"background": char.background,
"appearance": char.appearance,
"relationships": char.relationships,
"organization_type": char.organization_type,
"organization_purpose": char.organization_purpose,
"organization_members": char.organization_members,
"traits": char.traits,
"avatar_url": char.avatar_url,
"created_at": char.created_at,
"updated_at": char.updated_at,
"power_level": None,
"location": None,
"motto": None,
"color": None
}
if char.is_organization:
org_result = await db.execute(
select(Organization).where(Organization.character_id == char.id)
)
org = org_result.scalar_one_or_none()
if org:
char_dict.update({
"power_level": org.power_level,
"location": org.location,
"motto": org.motto,
"color": org.color
})
enriched_characters.append(char_dict)
return CharacterListResponse(total=total, items=enriched_characters)
@router.get("/project/{project_id}", response_model=CharacterListResponse, summary="获取项目的所有角色") @router.get("/project/{project_id}", response_model=CharacterListResponse, summary="获取项目的所有角色")
@@ -67,7 +110,50 @@ async def get_project_characters(
) )
characters = result.scalars().all() characters = result.scalars().all()
return CharacterListResponse(total=total, items=characters) # 为组织类型的角色填充Organization表的额外字段
enriched_characters = []
for char in characters:
char_dict = {
"id": char.id,
"project_id": char.project_id,
"name": char.name,
"age": char.age,
"gender": char.gender,
"is_organization": char.is_organization,
"role_type": char.role_type,
"personality": char.personality,
"background": char.background,
"appearance": char.appearance,
"relationships": char.relationships,
"organization_type": char.organization_type,
"organization_purpose": char.organization_purpose,
"organization_members": char.organization_members,
"traits": char.traits,
"avatar_url": char.avatar_url,
"created_at": char.created_at,
"updated_at": char.updated_at,
"power_level": None,
"location": None,
"motto": None,
"color": None
}
if char.is_organization:
org_result = await db.execute(
select(Organization).where(Organization.character_id == char.id)
)
org = org_result.scalar_one_or_none()
if org:
char_dict.update({
"power_level": org.power_level,
"location": org.location,
"motto": org.motto,
"color": org.color
})
enriched_characters.append(char_dict)
return CharacterListResponse(total=total, items=enriched_characters)
@router.get("/{character_id}", response_model=CharacterResponse, summary="获取角色详情") @router.get("/{character_id}", response_model=CharacterResponse, summary="获取角色详情")
@@ -213,16 +299,12 @@ async def generate_character(
logger.info(f" - 角色名:{request.name or 'AI生成'}") logger.info(f" - 角色名:{request.name or 'AI生成'}")
logger.info(f" - 角色定位:{request.role_type}") logger.info(f" - 角色定位:{request.role_type}")
logger.info(f" - 背景设定:{request.background or ''}") logger.info(f" - 背景设定:{request.background or ''}")
logger.info(f" - AI提供商:{request.provider or 'default'}") logger.info(f" - AI提供商:{user_ai_service.api_provider}")
logger.info(f" - AI模型:{request.model or 'default'}") logger.info(f" - AI模型:{user_ai_service.default_model}")
logger.info(f" - Prompt长度:{len(prompt)} 字符") logger.info(f" - Prompt长度:{len(prompt)} 字符")
try: try:
ai_response = await user_ai_service.generate_text( ai_response = await user_ai_service.generate_text(prompt=prompt)
prompt=prompt,
provider=request.provider,
model=request.model
)
logger.info(f"✅ AI响应接收完成,长度:{len(ai_response) if ai_response else 0} 字符") logger.info(f"✅ AI响应接收完成,长度:{len(ai_response) if ai_response else 0} 字符")
except Exception as ai_error: except Exception as ai_error:
logger.error(f"❌ AI服务调用异常:{str(ai_error)}") logger.error(f"❌ AI服务调用异常:{str(ai_error)}")
@@ -317,7 +399,8 @@ async def generate_character(
member_count=0, member_count=0,
power_level=character_data.get("power_level", 50), power_level=character_data.get("power_level", 50),
location=character_data.get("location"), location=character_data.get("location"),
motto=character_data.get("motto") motto=character_data.get("motto"),
color=character_data.get("color")
) )
db.add(organization) db.add(organization)
await db.flush() await db.flush()
@@ -477,7 +560,7 @@ async def generate_character(
project_id=request.project_id, project_id=request.project_id,
prompt=prompt, prompt=prompt,
generated_content=ai_response, generated_content=ai_response,
model=request.model or "default" model=user_ai_service.default_model
) )
db.add(history) db.add(history)
+208 -2
View File
@@ -2,11 +2,15 @@
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_ from sqlalchemy import select, and_
from typing import List from typing import List, Optional
from pydantic import BaseModel, Field
import json
from app.database import get_db from app.database import get_db
from app.models.relationship import Organization, OrganizationMember from app.models.relationship import Organization, OrganizationMember
from app.models.character import Character from app.models.character import Character
from app.models.project import Project
from app.models.generation_history import GenerationHistory
from app.schemas.relationship import ( from app.schemas.relationship import (
OrganizationCreate, OrganizationCreate,
OrganizationUpdate, OrganizationUpdate,
@@ -17,12 +21,25 @@ from app.schemas.relationship import (
OrganizationMemberResponse, OrganizationMemberResponse,
OrganizationMemberDetailResponse OrganizationMemberDetailResponse
) )
from app.schemas.character import CharacterResponse
from app.services.ai_service import AIService
from app.services.prompt_service import prompt_service
from app.logger import get_logger from app.logger import get_logger
from app.api.settings import get_user_ai_service
router = APIRouter(prefix="/organizations", tags=["组织管理"]) router = APIRouter(prefix="/organizations", tags=["组织管理"])
logger = get_logger(__name__) logger = get_logger(__name__)
class OrganizationGenerateRequest(BaseModel):
"""AI生成组织的请求模型"""
project_id: str = Field(..., description="项目ID")
name: Optional[str] = Field(None, description="组织名称")
organization_type: Optional[str] = Field(None, description="组织类型")
background: Optional[str] = Field(None, description="组织背景")
requirements: Optional[str] = Field(None, description="特殊要求")
@router.get("/project/{project_id}", response_model=List[OrganizationDetailResponse], summary="获取项目的所有组织") @router.get("/project/{project_id}", response_model=List[OrganizationDetailResponse], summary="获取项目的所有组织")
async def get_project_organizations( async def get_project_organizations(
project_id: str, project_id: str,
@@ -338,4 +355,193 @@ async def remove_organization_member(
await db.commit() await db.commit()
logger.info(f"移除成员成功:{member_id}") logger.info(f"移除成员成功:{member_id}")
return {"message": "成员移除成功", "id": member_id} return {"message": "成员移除成功", "id": member_id}
@router.post("/generate", response_model=CharacterResponse, summary="AI生成组织")
async def generate_organization(
request: OrganizationGenerateRequest,
db: AsyncSession = Depends(get_db),
user_ai_service: AIService = Depends(get_user_ai_service)
):
"""
使用AI生成组织设定
根据用户输入的信息,结合项目的世界观、主题等背景,
AI会生成一个完整、详细的组织设定。
生成内容包括:组织名称、类型、特性、背景、目的、势力等级等
"""
# 验证项目是否存在并获取项目信息
result = await db.execute(
select(Project).where(Project.id == request.project_id)
)
project = result.scalar_one_or_none()
if not project:
raise HTTPException(status_code=404, detail="项目不存在")
try:
# 获取已存在的角色和组织列表
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_info = ""
character_list = []
organization_list = []
if existing_characters:
for c in existing_characters[:10]: # 最多显示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"""
用户要求:
- 组织名称:{request.name or '请AI生成'}
- 组织类型:{request.organization_type or '请AI根据世界观决定'}
- 背景设定:{request.background or '无特殊要求'}
- 其他要求:{request.requirements or ''}
"""
# 使用统一的提示词服务
prompt = prompt_service.get_single_organization_prompt(
project_context=project_context,
user_input=user_input
)
# 调用AI生成组织
logger.info(f"🎯 开始为项目 {request.project_id} 生成组织")
logger.info(f" - 组织名:{request.name or 'AI生成'}")
logger.info(f" - 组织类型:{request.organization_type or 'AI决定'}")
logger.info(f" - 背景设定:{request.background or ''}")
logger.info(f" - AI提供商:{user_ai_service.api_provider}")
logger.info(f" - AI模型:{user_ai_service.default_model}")
logger.info(f" - Prompt长度:{len(prompt)} 字符")
try:
ai_response = await user_ai_service.generate_text(prompt=prompt)
logger.info(f"✅ AI响应接收完成,长度:{len(ai_response) if ai_response else 0} 字符")
except Exception as ai_error:
logger.error(f"❌ AI服务调用异常:{str(ai_error)}")
raise HTTPException(
status_code=500,
detail=f"AI服务调用失败:{str(ai_error)}"
)
# 检查AI响应
if not ai_response or not ai_response.strip():
logger.error("❌ AI返回了空响应")
raise HTTPException(
status_code=500,
detail="AI服务返回空响应。请检查AI配置和网络连接。"
)
logger.info(f"📝 开始清理AI响应")
# 清理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()
logger.info(f" - 清理后长度:{len(cleaned_response)}")
# 解析AI响应
logger.info(f"🔍 开始解析JSON")
try:
organization_data = json.loads(cleaned_response)
logger.info(f"✅ JSON解析成功")
logger.info(f" - 解析后的字段:{list(organization_data.keys())}")
except json.JSONDecodeError as e:
logger.error(f"❌ JSON解析失败:{str(e)}")
raise HTTPException(
status_code=500,
detail=f"AI返回的内容无法解析为JSON。错误:{str(e)}"
)
# 创建角色记录(组织也是角色的一种)
character = Character(
project_id=request.project_id,
name=organization_data.get("name", 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})")
# 自动创建Organization详情记录
organization = Organization(
character_id=character.id,
project_id=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})")
# 记录生成历史
history = GenerationHistory(
project_id=request.project_id,
prompt=prompt,
generated_content=ai_response,
model=user_ai_service.default_model
)
db.add(history)
await db.commit()
await db.refresh(character)
logger.info(f"🎉 成功为项目 {request.project_id} 生成组织: {character.name}")
return character
except Exception as e:
logger.error(f"生成组织失败: {str(e)}")
raise HTTPException(status_code=500, detail=f"生成组织失败: {str(e)}")
+21 -2
View File
@@ -1,5 +1,5 @@
"""项目管理API""" """项目管理API"""
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Request
from fastapi.responses import Response from fastapi.responses import Response
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, delete from sqlalchemy import select, func, delete
@@ -13,6 +13,7 @@ from app.models.outline import Outline
from app.models.chapter import Chapter from app.models.chapter import Chapter
from app.models.generation_history import GenerationHistory from app.models.generation_history import GenerationHistory
from app.models.relationship import CharacterRelationship, Organization, OrganizationMember from app.models.relationship import CharacterRelationship, Organization, OrganizationMember
from app.models.memory import StoryMemory, PlotAnalysis
from app.schemas.project import ( from app.schemas.project import (
ProjectCreate, ProjectCreate,
ProjectUpdate, ProjectUpdate,
@@ -25,6 +26,7 @@ from app.schemas.import_export import (
ImportResult ImportResult
) )
from app.services.import_export_service import ImportExportService from app.services.import_export_service import ImportExportService
from app.services.memory_service import memory_service
from app.logger import get_logger from app.logger import get_logger
from app.utils.data_consistency import ( from app.utils.data_consistency import (
run_full_data_consistency_check, run_full_data_consistency_check,
@@ -143,6 +145,7 @@ async def update_project(
@router.delete("/{project_id}", summary="删除项目") @router.delete("/{project_id}", summary="删除项目")
async def delete_project( async def delete_project(
project_id: str, project_id: str,
request: Request,
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db)
): ):
try: try:
@@ -158,6 +161,19 @@ async def delete_project(
project_title = project.title project_title = project.title
# 从认证中间件获取用户ID
user_id = getattr(request.state, 'user_id', None)
# 删除向量数据库中的记忆
if user_id:
try:
await memory_service.delete_project_memories(user_id, project_id)
logger.info(f"✅ 向量数据库清理成功")
except Exception as e:
logger.warning(f"⚠️ 向量数据库清理失败(继续删除其他数据): {str(e)}")
else:
logger.warning(f"⚠️ 未找到用户ID,跳过向量数据库清理")
relationships_result = await db.execute( relationships_result = await db.execute(
delete(CharacterRelationship).where(CharacterRelationship.project_id == project_id) delete(CharacterRelationship).where(CharacterRelationship.project_id == project_id)
) )
@@ -200,11 +216,14 @@ async def delete_project(
) )
logger.debug(f"删除角色数: {characters_result.rowcount}") logger.debug(f"删除角色数: {characters_result.rowcount}")
# 注意:StoryMemory和PlotAnalysis会通过数据库级联删除自动清理
# 但向量数据库已在上面手动清理
await db.delete(project) await db.delete(project)
await db.commit() await db.commit()
logger.info(f"项目删除成功: {project_title}") logger.info(f"项目删除成功: {project_title}")
return {"message": "项目及所有关联数据删除成功"} return {"message": "项目及所有关联数据(包括向量数据库)删除成功"}
except HTTPException: except HTTPException:
raise raise
except Exception as e: except Exception as e:
+13 -9
View File
@@ -452,21 +452,24 @@ async def characters_generator(
elif isinstance(char_data.get("relationships"), str): elif isinstance(char_data.get("relationships"), str):
relationships_text = char_data.get("relationships") relationships_text = char_data.get("relationships")
# 判断是否为组织
is_organization = char_data.get("is_organization", False)
character = Character( character = Character(
project_id=project_id, project_id=project_id,
name=char_data.get("name", "未命名角色"), name=char_data.get("name", "未命名角色"),
age=char_data.get("age"), age=str(char_data.get("age", "")) if not is_organization else None,
gender=char_data.get("gender"), gender=char_data.get("gender") if not is_organization else None,
is_organization=char_data.get("is_organization", False), is_organization=is_organization,
role_type=char_data.get("role_type", "supporting"), role_type=char_data.get("role_type", "supporting"),
personality=char_data.get("personality", ""), personality=char_data.get("personality", ""),
background=char_data.get("background", ""), background=char_data.get("background", ""),
appearance=char_data.get("appearance", ""), appearance=char_data.get("appearance", ""),
relationships=relationships_text, relationships=relationships_text,
organization_type=char_data.get("organization_type"), organization_type=char_data.get("organization_type") if is_organization else None,
organization_purpose=char_data.get("organization_purpose"), organization_purpose=char_data.get("organization_purpose") if is_organization else None,
organization_members=json.dumps(char_data.get("organization_members", []), ensure_ascii=False), organization_members=json.dumps(char_data.get("organization_members", []), ensure_ascii=False) if is_organization else None,
traits=json.dumps(char_data.get("traits", []), ensure_ascii=False) traits=json.dumps(char_data.get("traits", []), ensure_ascii=False) if char_data.get("traits") else None
) )
db.add(character) db.add(character)
created_characters.append((character, char_data)) created_characters.append((character, char_data))
@@ -497,9 +500,10 @@ async def characters_generator(
character_id=character.id, character_id=character.id,
project_id=project_id, project_id=project_id,
member_count=0, # 初始为0,后续添加成员时会更新 member_count=0, # 初始为0,后续添加成员时会更新
power_level=char_data.get("power_level", 5), power_level=char_data.get("power_level", 50),
location=char_data.get("location"), location=char_data.get("location"),
motto=char_data.get("motto") motto=char_data.get("motto"),
color=char_data.get("color")
) )
db.add(org) db.add(org)
logger.info(f"向导创建组织记录:{character.name}") logger.info(f"向导创建组织记录:{character.name}")
+6 -2
View File
@@ -46,6 +46,12 @@ class CharacterResponse(CharacterBase):
created_at: datetime created_at: datetime
updated_at: datetime updated_at: datetime
# 组织额外字段(从Organization表关联)
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 Config: class Config:
from_attributes = True from_attributes = True
@@ -57,8 +63,6 @@ class CharacterGenerateRequest(BaseModel):
role_type: Optional[str] = Field(None, description="角色类型") role_type: Optional[str] = Field(None, description="角色类型")
background: Optional[str] = Field(None, description="角色背景") background: Optional[str] = Field(None, description="角色背景")
requirements: Optional[str] = Field(None, description="特殊要求") requirements: Optional[str] = Field(None, description="特殊要求")
provider: Optional[str] = Field(None, description="AI提供商")
model: Optional[str] = Field(None, description="AI模型")
class CharacterListResponse(BaseModel): class CharacterListResponse(BaseModel):
+38
View File
@@ -659,6 +659,44 @@ class MemoryService:
logger.error(f"❌ 删除章节记忆失败: {str(e)}") logger.error(f"❌ 删除章节记忆失败: {str(e)}")
return False return False
async def delete_project_memories(
self,
user_id: str,
project_id: str
) -> bool:
"""
删除指定项目的所有记忆(包括向量数据库)
Args:
user_id: 用户ID
project_id: 项目ID
Returns:
是否删除成功
"""
try:
# 生成collection名称
user_hash = hashlib.sha256(user_id.encode()).hexdigest()[:8]
project_hash = hashlib.sha256(project_id.encode()).hexdigest()[:8]
collection_name = f"u_{user_hash}_p_{project_hash}"
# 删除整个collection(这会清理所有向量数据)
try:
self.client.delete_collection(name=collection_name)
logger.info(f"🗑️ 已删除项目{project_id[:8]}的向量数据库collection: {collection_name}")
return True
except Exception as e:
# 如果collection不存在,也算成功
if "does not exist" in str(e).lower():
logger.info(f"️ 项目{project_id[:8]}的collection不存在,无需删除")
return True
else:
raise
except Exception as e:
logger.error(f"❌ 删除项目记忆失败: {str(e)}")
return False
async def update_memory( async def update_memory(
self, self,
user_id: str, user_id: str,
+106 -1
View File
@@ -169,7 +169,7 @@ class PromptService:
- 至少1个主角protagonist - 至少1个主角protagonist
- 多个配角supporting - 多个配角supporting
- 可以包含反派antagonist - 可以包含反派antagonist
- 可以包含1-2重要组织 - 可以包含1-2**高影响力的重要组织**势力等级应在70-95之间
要求 要求
- 角色要符合世界观设定 - 角色要符合世界观设定
@@ -222,10 +222,21 @@ class PromptService:
"organization_type": "组织类型", "organization_type": "组织类型",
"organization_purpose": "组织目的", "organization_purpose": "组织目的",
"organization_members": ["成员1", "成员2"], "organization_members": ["成员1", "成员2"],
"power_level": 85,
"location": "组织所在地或主要活动区域",
"motto": "组织格言、口号或宗旨",
"color": "组织代表颜色(如:深红色、金色、黑色等)",
"traits": [] "traits": []
}} }}
] ]
**组织生成要求重要**
- 组织必须是对故事有重大影响的势力
- power_level应在70-95之间高影响力组织
- 不要生成无关紧要的小组织或普通社团
- 组织应该是推动剧情发展的关键力量
- 可以是正派势力中立势力或反派势力但一定要有存在感
**关系类型参考从中选择或自定义** **关系类型参考从中选择或自定义**
- 家族父亲母亲兄弟姐妹子女配偶恋人 - 家族父亲母亲兄弟姐妹子女配偶恋人
- 社交师父徒弟朋友同学同事邻居知己 - 社交师父徒弟朋友同学同事邻居知己
@@ -678,6 +689,91 @@ class PromptService:
- 配角要有独特性不能是工具人 - 配角要有独特性不能是工具人
- 所有设定要为故事服务 - 所有设定要为故事服务
再次强调
1. 只返回纯JSON对象不要有```json```这样的标记
2. 文本中不要使用中文引号""改用
3. 不要有任何额外的文字说明"""
# 单个组织生成提示词
SINGLE_ORGANIZATION_GENERATION = """你是一位专业的组织设定师。请根据以下信息创建一个完整的组织/势力设定。
{project_context}
{user_input}
请生成一个完整的组织设定包含以下所有信息
1. **基本信息**
- 组织名称如果用户未提供请生成一个符合世界观的名称
- 组织类型如帮派公司门派学院政府机构宗教组织等
- 成立时间具体时间或时间段
2. **组织特性**150-200
- 组织的核心理念和行事风格
- 组织文化和价值观
- 运作方式和管理模式
- 特殊传统或规矩
3. **组织背景**200-300
- 建立历史和起源
- 发展历程和重要事件
- 目前的地位和影响力
- 如何与项目主题关联
- 融入用户提供的背景设定
4. **外在表现**100-150
- 总部或主要据点位置
- 标志性建筑或场所
- 组织标志徽章制服等
- 可辨识的外在特征
5. **组织目的/宗旨**
- 明确的组织目标
- 长期愿景
- 行动准则
6. **势力等级**
- 在世界中的影响力0-100
- 综合实力评估
7. **所在地点**
- 主要活动区域
- 势力范围
**重要格式要求**
1. 只返回纯JSON格式不要包含任何markdown标记代码块标记或其他说明文字
2. 不要在JSON字符串值中使用中文引号""''改用
3. 文本描述中的专有名词使用标记
请严格按照以下JSON格式返回
{{
"name": "组织名称",
"is_organization": true,
"organization_type": "组织类型",
"personality": "组织特性(150-200字)",
"background": "组织背景(200-300字)",
"appearance": "外在表现(100-150字)",
"organization_purpose": "组织目的和宗旨",
"power_level": 75,
"location": "所在地点",
"motto": "组织格言或口号",
"traits": ["特征1", "特征2", "特征3"],
"color": "组织代表颜色(如:深红色、金色、黑色等)",
"organization_members": ["重要成员1", "重要成员2", "重要成员3"]
}}
**组织设定要求**
- 组织要符合项目的世界观和主题
- 目标和行动要合理不能过于理想化或脸谱化
- 要有存在的必要性能推动故事发展
- 内部要有层级和结构
- 与其他势力要有互动关系
**说明**
1. power_level是0-100的整数表示组织在世界中的影响力
2. organization_members是组织内重要成员的名字列表如果已有角色可以关联
3. 所有文本描述要详细具体避免空泛
再次强调 再次强调
1. 只返回纯JSON对象不要有```json```这样的标记 1. 只返回纯JSON对象不要有```json```这样的标记
2. 文本中不要使用中文引号""改用 2. 文本中不要使用中文引号""改用
@@ -938,6 +1034,15 @@ class PromptService:
project_context=project_context, project_context=project_context,
user_input=user_input user_input=user_input
) )
@classmethod
def get_single_organization_prompt(cls, project_context: str, user_input: str) -> str:
"""获取单个组织生成提示词"""
return cls.format_prompt(
cls.SINGLE_ORGANIZATION_GENERATION,
project_context=project_context,
user_input=user_input
)
# 创建全局提示词服务实例 # 创建全局提示词服务实例
@@ -32,6 +32,7 @@ interface AnnotatedTextProps {
onAnnotationClick?: (annotation: MemoryAnnotation) => void; onAnnotationClick?: (annotation: MemoryAnnotation) => void;
activeAnnotationId?: string; activeAnnotationId?: string;
scrollToAnnotation?: string; scrollToAnnotation?: string;
style?: React.CSSProperties;
} }
// 类型颜色映射 // 类型颜色映射
@@ -60,6 +61,7 @@ const AnnotatedText: React.FC<AnnotatedTextProps> = ({
onAnnotationClick, onAnnotationClick,
activeAnnotationId, activeAnnotationId,
scrollToAnnotation, scrollToAnnotation,
style,
}) => { }) => {
const annotationRefs = useRef<Record<string, HTMLSpanElement | null>>({}); const annotationRefs = useRef<Record<string, HTMLSpanElement | null>>({});
@@ -243,6 +245,7 @@ const AnnotatedText: React.FC<AnnotatedTextProps> = ({
fontSize: 16, fontSize: 16,
whiteSpace: 'pre-wrap', whiteSpace: 'pre-wrap',
wordBreak: 'break-word', wordBreak: 'break-word',
...style,
}} }}
> >
{segments.map((segment, index) => renderAnnotatedSegment(segment, index))} {segments.map((segment, index) => renderAnnotatedSegment(segment, index))}
+50 -30
View File
@@ -14,6 +14,9 @@ import {
} from '@ant-design/icons'; } from '@ant-design/icons';
import type { AnalysisTask, ChapterAnalysisResponse } from '../types'; import type { AnalysisTask, ChapterAnalysisResponse } from '../types';
// 判断是否为移动设备
const isMobileDevice = () => window.innerWidth < 768;
interface ChapterAnalysisProps { interface ChapterAnalysisProps {
chapterId: string; chapterId: string;
visible: boolean; visible: boolean;
@@ -25,14 +28,23 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
const [analysis, setAnalysis] = useState<ChapterAnalysisResponse | null>(null); const [analysis, setAnalysis] = useState<ChapterAnalysisResponse | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [isMobile, setIsMobile] = useState(isMobileDevice());
useEffect(() => { useEffect(() => {
if (visible && chapterId) { if (visible && chapterId) {
fetchAnalysisStatus(); fetchAnalysisStatus();
} }
// 监听窗口大小变化
const handleResize = () => {
setIsMobile(isMobileDevice());
};
window.addEventListener('resize', handleResize);
// 清理函数:组件卸载或关闭时清除轮询 // 清理函数:组件卸载或关闭时清除轮询
return () => { return () => {
window.removeEventListener('resize', handleResize);
// 清除可能存在的轮询 // 清除可能存在的轮询
}; };
}, [visible, chapterId]); }, [visible, chapterId]);
@@ -194,10 +206,10 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
label: '概览', label: '概览',
icon: <TrophyOutlined />, icon: <TrophyOutlined />,
children: ( children: (
<div style={{ height: 'calc(90vh - 220px)', overflowY: 'auto', paddingRight: '8px' }}> <div style={{ height: isMobile ? 'calc(80vh - 180px)' : 'calc(90vh - 220px)', overflowY: 'auto', paddingRight: '8px' }}>
<Card title="整体评分" style={{ marginBottom: 16 }}> <Card title="整体评分" style={{ marginBottom: 16 }} size={isMobile ? 'small' : 'default'}>
<Row gutter={16}> <Row gutter={isMobile ? 8 : 16}>
<Col span={6}> <Col span={isMobile ? 12 : 6}>
<Statistic <Statistic
title="整体质量" title="整体质量"
value={analysis_data.overall_quality_score || 0} value={analysis_data.overall_quality_score || 0}
@@ -205,21 +217,21 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
valueStyle={{ color: '#3f8600' }} valueStyle={{ color: '#3f8600' }}
/> />
</Col> </Col>
<Col span={6}> <Col span={isMobile ? 12 : 6}>
<Statistic <Statistic
title="节奏把控" title="节奏把控"
value={analysis_data.pacing_score || 0} value={analysis_data.pacing_score || 0}
suffix="/ 10" suffix="/ 10"
/> />
</Col> </Col>
<Col span={6}> <Col span={isMobile ? 12 : 6}>
<Statistic <Statistic
title="吸引力" title="吸引力"
value={analysis_data.engagement_score || 0} value={analysis_data.engagement_score || 0}
suffix="/ 10" suffix="/ 10"
/> />
</Col> </Col>
<Col span={6}> <Col span={isMobile ? 12 : 6}>
<Statistic <Statistic
title="连贯性" title="连贯性"
value={analysis_data.coherence_score || 0} value={analysis_data.coherence_score || 0}
@@ -230,15 +242,15 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
</Card> </Card>
{analysis_data.analysis_report && ( {analysis_data.analysis_report && (
<Card title="分析摘要" style={{ marginBottom: 16 }}> <Card title="分析摘要" style={{ marginBottom: 16 }} size={isMobile ? 'small' : 'default'}>
<pre style={{ whiteSpace: 'pre-wrap', fontFamily: 'inherit' }}> <pre style={{ whiteSpace: 'pre-wrap', fontFamily: 'inherit', fontSize: isMobile ? 13 : 14 }}>
{analysis_data.analysis_report} {analysis_data.analysis_report}
</pre> </pre>
</Card> </Card>
)} )}
{analysis_data.suggestions && analysis_data.suggestions.length > 0 && ( {analysis_data.suggestions && analysis_data.suggestions.length > 0 && (
<Card title={<><BulbOutlined /> </>}> <Card title={<><BulbOutlined /> </>} size={isMobile ? 'small' : 'default'}>
<List <List
dataSource={analysis_data.suggestions} dataSource={analysis_data.suggestions}
renderItem={(item, index) => ( renderItem={(item, index) => (
@@ -257,8 +269,8 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
label: `钩子 (${analysis_data.hooks?.length || 0})`, label: `钩子 (${analysis_data.hooks?.length || 0})`,
icon: <ThunderboltOutlined />, icon: <ThunderboltOutlined />,
children: ( children: (
<div style={{ height: 'calc(90vh - 220px)', overflowY: 'auto', paddingRight: '8px' }}> <div style={{ height: isMobile ? 'calc(80vh - 180px)' : 'calc(90vh - 220px)', overflowY: 'auto', paddingRight: '8px' }}>
<Card> <Card size={isMobile ? 'small' : 'default'}>
{analysis_data.hooks && analysis_data.hooks.length > 0 ? ( {analysis_data.hooks && analysis_data.hooks.length > 0 ? (
<List <List
dataSource={analysis_data.hooks} dataSource={analysis_data.hooks}
@@ -289,8 +301,8 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
label: `伏笔 (${analysis_data.foreshadows?.length || 0})`, label: `伏笔 (${analysis_data.foreshadows?.length || 0})`,
icon: <FireOutlined />, icon: <FireOutlined />,
children: ( children: (
<div style={{ height: 'calc(90vh - 220px)', overflowY: 'auto', paddingRight: '8px' }}> <div style={{ height: isMobile ? 'calc(80vh - 180px)' : 'calc(90vh - 220px)', overflowY: 'auto', paddingRight: '8px' }}>
<Card> <Card size={isMobile ? 'small' : 'default'}>
{analysis_data.foreshadows && analysis_data.foreshadows.length > 0 ? ( {analysis_data.foreshadows && analysis_data.foreshadows.length > 0 ? (
<List <List
dataSource={analysis_data.foreshadows} dataSource={analysis_data.foreshadows}
@@ -326,18 +338,18 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
label: '情感曲线', label: '情感曲线',
icon: <HeartOutlined />, icon: <HeartOutlined />,
children: ( children: (
<div style={{ height: 'calc(90vh - 220px)', overflowY: 'auto', paddingRight: '8px' }}> <div style={{ height: isMobile ? 'calc(80vh - 180px)' : 'calc(90vh - 220px)', overflowY: 'auto', paddingRight: '8px' }}>
<Card> <Card size={isMobile ? 'small' : 'default'}>
{analysis_data.emotional_tone ? ( {analysis_data.emotional_tone ? (
<div> <div>
<Row gutter={16} style={{ marginBottom: 24 }}> <Row gutter={isMobile ? 8 : 16} style={{ marginBottom: isMobile ? 16 : 24 }}>
<Col span={12}> <Col span={isMobile ? 24 : 12}>
<Statistic <Statistic
title="主导情绪" title="主导情绪"
value={analysis_data.emotional_tone} value={analysis_data.emotional_tone}
/> />
</Col> </Col>
<Col span={12}> <Col span={isMobile ? 24 : 12}>
<Statistic <Statistic
title="情感强度" title="情感强度"
value={(analysis_data.emotional_intensity * 10).toFixed(1)} value={(analysis_data.emotional_intensity * 10).toFixed(1)}
@@ -372,8 +384,8 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
label: `角色 (${analysis_data.character_states?.length || 0})`, label: `角色 (${analysis_data.character_states?.length || 0})`,
icon: <TeamOutlined />, icon: <TeamOutlined />,
children: ( children: (
<div style={{ height: 'calc(90vh - 220px)', overflowY: 'auto', paddingRight: '8px' }}> <div style={{ height: isMobile ? 'calc(80vh - 180px)' : 'calc(90vh - 220px)', overflowY: 'auto', paddingRight: '8px' }}>
<Card> <Card size={isMobile ? 'small' : 'default'}>
{analysis_data.character_states && analysis_data.character_states.length > 0 ? ( {analysis_data.character_states && analysis_data.character_states.length > 0 ? (
<List <List
dataSource={analysis_data.character_states} dataSource={analysis_data.character_states}
@@ -414,8 +426,8 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
label: `记忆 (${memories?.length || 0})`, label: `记忆 (${memories?.length || 0})`,
icon: <FireOutlined />, icon: <FireOutlined />,
children: ( children: (
<div style={{ height: 'calc(90vh - 220px)', overflowY: 'auto', paddingRight: '8px' }}> <div style={{ height: isMobile ? 'calc(80vh - 180px)' : 'calc(90vh - 220px)', overflowY: 'auto', paddingRight: '8px' }}>
<Card> <Card size={isMobile ? 'small' : 'default'}>
{memories && memories.length > 0 ? ( {memories && memories.length > 0 ? (
<List <List
dataSource={memories} dataSource={memories}
@@ -462,20 +474,25 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
title="章节分析" title="章节分析"
open={visible} open={visible}
onCancel={onClose} onCancel={onClose}
width="90%" width={isMobile ? '100%' : '90%'}
centered centered={!isMobile}
style={{ style={{
maxWidth: '1400px', maxWidth: isMobile ? '100%' : '1400px',
paddingBottom: 0 paddingBottom: 0,
top: isMobile ? 0 : undefined,
margin: isMobile ? 0 : undefined,
maxHeight: isMobile ? '100vh' : undefined
}} }}
styles={{ styles={{
body: { body: {
padding: '24px', padding: isMobile ? '12px' : '24px',
paddingBottom: 0 paddingBottom: 0,
maxHeight: isMobile ? 'calc(100vh - 110px)' : undefined,
overflowY: isMobile ? 'auto' : undefined
} }
}} }}
footer={[ footer={[
<Button key="close" onClick={onClose}> <Button key="close" onClick={onClose} size={isMobile ? 'small' : 'middle'}>
</Button>, </Button>,
!task && !loading && ( !task && !loading && (
@@ -485,6 +502,7 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
icon={<ReloadOutlined />} icon={<ReloadOutlined />}
onClick={triggerAnalysis} onClick={triggerAnalysis}
loading={loading} loading={loading}
size={isMobile ? 'small' : 'middle'}
> >
</Button> </Button>
@@ -497,6 +515,7 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
onClick={triggerAnalysis} onClick={triggerAnalysis}
loading={loading} loading={loading}
danger danger
size={isMobile ? 'small' : 'middle'}
> >
</Button> </Button>
@@ -508,6 +527,7 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
icon={<ReloadOutlined />} icon={<ReloadOutlined />}
onClick={triggerAnalysis} onClick={triggerAnalysis}
loading={loading} loading={loading}
size={isMobile ? 'small' : 'middle'}
> >
</Button> </Button>
+36
View File
@@ -122,6 +122,42 @@ export const CharacterCard: React.FC<CharacterCardProps> = ({ character, onEdit,
<Tag color="cyan">{character.organization_type}</Tag> <Tag color="cyan">{character.organization_type}</Tag>
</div> </div>
)} )}
{character.power_level !== undefined && character.power_level !== null && (
<div style={{ marginBottom: 8, display: 'flex', alignItems: 'center' }}>
<Text type="secondary" style={{ flexShrink: 0 }}></Text>
<Tag color={character.power_level >= 70 ? 'red' : character.power_level >= 50 ? 'orange' : 'default'}>
{character.power_level}
</Tag>
</div>
)}
{character.location && (
<div style={{ marginBottom: 8, display: 'flex', alignItems: 'flex-start' }}>
<Text type="secondary" style={{ flexShrink: 0 }}></Text>
<Text
style={{ flex: 1, minWidth: 0 }}
ellipsis={{ tooltip: character.location }}
>
{character.location}
</Text>
</div>
)}
{character.color && (
<div style={{ marginBottom: 8, display: 'flex', alignItems: 'flex-start' }}>
<Text type="secondary" style={{ flexShrink: 0 }}></Text>
<Text style={{ flex: 1, minWidth: 0 }}>{character.color}</Text>
</div>
)}
{character.motto && (
<div style={{ marginBottom: 8, display: 'flex', alignItems: 'flex-start' }}>
<Text type="secondary" style={{ flexShrink: 0 }}></Text>
<Text
style={{ flex: 1, minWidth: 0 }}
ellipsis={{ tooltip: character.motto }}
>
{character.motto}
</Text>
</div>
)}
{character.organization_purpose && ( {character.organization_purpose && (
<div style={{ marginBottom: 8, display: 'flex', alignItems: 'flex-start' }}> <div style={{ marginBottom: 8, display: 'flex', alignItems: 'flex-start' }}>
<Text type="secondary" style={{ flexShrink: 0 }}></Text> <Text type="secondary" style={{ flexShrink: 0 }}></Text>
+257 -93
View File
@@ -6,6 +6,7 @@ import {
MenuOutlined, MenuOutlined,
LeftOutlined, LeftOutlined,
RightOutlined, RightOutlined,
UnorderedListOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import api from '../services/api'; import api from '../services/api';
@@ -71,8 +72,20 @@ const ChapterAnalysis: React.FC = () => {
const [showAnnotations, setShowAnnotations] = useState(true); const [showAnnotations, setShowAnnotations] = useState(true);
const [activeAnnotationId, setActiveAnnotationId] = useState<string | undefined>(); const [activeAnnotationId, setActiveAnnotationId] = useState<string | undefined>();
const [sidebarVisible, setSidebarVisible] = useState(false); const [sidebarVisible, setSidebarVisible] = useState(false);
const [chapterListVisible, setChapterListVisible] = useState(false);
const [scrollToContentAnnotation, setScrollToContentAnnotation] = useState<string | undefined>(); const [scrollToContentAnnotation, setScrollToContentAnnotation] = useState<string | undefined>();
const [scrollToSidebarAnnotation, setScrollToSidebarAnnotation] = useState<string | undefined>(); const [scrollToSidebarAnnotation, setScrollToSidebarAnnotation] = useState<string | undefined>();
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
// 监听窗口大小变化
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth < 768);
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
// 加载章节列表 // 加载章节列表
useEffect(() => { useEffect(() => {
@@ -128,6 +141,9 @@ const ChapterAnalysis: React.FC = () => {
const handleChapterSelect = (chapterId: string) => { const handleChapterSelect = (chapterId: string) => {
loadChapterContent(chapterId); loadChapterContent(chapterId);
if (isMobile) {
setChapterListVisible(false);
}
}; };
const handlePreviousChapter = () => { const handlePreviousChapter = () => {
@@ -151,7 +167,7 @@ const ChapterAnalysis: React.FC = () => {
// 清除滚动状态 // 清除滚动状态
setTimeout(() => setScrollToSidebarAnnotation(undefined), 100); setTimeout(() => setScrollToSidebarAnnotation(undefined), 100);
if (window.innerWidth < 768) { if (isMobile) {
setSidebarVisible(true); setSidebarVisible(true);
} }
} else { } else {
@@ -173,48 +189,102 @@ const ChapterAnalysis: React.FC = () => {
} }
return ( return (
<div style={{ display: 'flex', height: '100%', gap: 16 }}> <div style={{
{/* 左侧章节列表 */} display: 'flex',
<Card height: '100%',
title="章节列表" gap: isMobile ? 0 : 16,
style={{ width: 280, height: '100%', overflow: 'hidden' }} flexDirection: isMobile ? 'column' : 'row'
bodyStyle={{ padding: 0, height: 'calc(100% - 57px)', overflow: 'auto' }} }}>
> {/* 左侧章节列表 - 桌面端 */}
{chapters.length === 0 ? ( {!isMobile && (
<Empty description="暂无章节" style={{ marginTop: 60 }} /> <Card
) : ( title="章节列表"
<List style={{ width: 280, height: '100%', overflow: 'hidden' }}
dataSource={chapters} bodyStyle={{ padding: 0, height: 'calc(100% - 57px)', overflow: 'auto' }}
renderItem={(chapter) => ( >
<List.Item {chapters.length === 0 ? (
key={chapter.id} <Empty description="暂无章节" style={{ marginTop: 60 }} />
onClick={() => handleChapterSelect(chapter.id)} ) : (
style={{ <List
cursor: 'pointer', dataSource={chapters}
padding: '12px 16px', renderItem={(chapter) => (
background: selectedChapter?.id === chapter.id ? '#e6f7ff' : 'transparent', <List.Item
borderLeft: selectedChapter?.id === chapter.id ? '3px solid #1890ff' : '3px solid transparent', key={chapter.id}
}} onClick={() => handleChapterSelect(chapter.id)}
> style={{
<List.Item.Meta cursor: 'pointer',
title={ padding: '12px 16px',
<span style={{ fontSize: 14, fontWeight: selectedChapter?.id === chapter.id ? 600 : 400 }}> background: selectedChapter?.id === chapter.id ? '#e6f7ff' : 'transparent',
{chapter.chapter_number}: {chapter.title} borderLeft: selectedChapter?.id === chapter.id ? '3px solid #1890ff' : '3px solid transparent',
</span> }}
} >
description={ <List.Item.Meta
<Space size={4}> title={
<Tag color={chapter.content && chapter.content.trim() !== '' ? 'success' : 'default'}> <span style={{ fontSize: 14, fontWeight: selectedChapter?.id === chapter.id ? 600 : 400 }}>
{chapter.word_count || 0} {chapter.chapter_number}: {chapter.title}
</Tag> </span>
</Space> }
} description={
/> <Space size={4}>
</List.Item> <Tag color={chapter.content && chapter.content.trim() !== '' ? 'success' : 'default'}>
)} {chapter.word_count || 0}
/> </Tag>
)} </Space>
</Card> }
/>
</List.Item>
)}
/>
)}
</Card>
)}
{/* 移动端章节列表抽屉 */}
{isMobile && (
<Drawer
title="章节列表"
placement="left"
onClose={() => setChapterListVisible(false)}
open={chapterListVisible}
width="85%"
styles={{ body: { padding: 0 } }}
>
{chapters.length === 0 ? (
<Empty description="暂无章节" style={{ marginTop: 60 }} />
) : (
<List
dataSource={chapters}
renderItem={(chapter) => (
<List.Item
key={chapter.id}
onClick={() => handleChapterSelect(chapter.id)}
style={{
cursor: 'pointer',
padding: '12px 16px',
background: selectedChapter?.id === chapter.id ? '#e6f7ff' : 'transparent',
borderLeft: selectedChapter?.id === chapter.id ? '3px solid #1890ff' : '3px solid transparent',
}}
>
<List.Item.Meta
title={
<span style={{ fontSize: 14, fontWeight: selectedChapter?.id === chapter.id ? 600 : 400 }}>
{chapter.chapter_number}: {chapter.title}
</span>
}
description={
<Space size={4}>
<Tag color={chapter.content && chapter.content.trim() !== '' ? 'success' : 'default'}>
{chapter.word_count || 0}
</Tag>
</Space>
}
/>
</List.Item>
)}
/>
)}
</Drawer>
)}
{/* 右侧内容区域 */} {/* 右侧内容区域 */}
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}> <div style={{ flex: 1, display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
@@ -225,54 +295,138 @@ const ChapterAnalysis: React.FC = () => {
) : ( ) : (
<> <>
{/* 工具栏 */} {/* 工具栏 */}
<Card size="small" style={{ marginBottom: 16 }}> <Card size="small" style={{ marginBottom: isMobile ? 8 : 16 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> {isMobile ? (
<Space> // 移动端布局:两行显示
<Button <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
icon={<LeftOutlined />} {/* 第一行:标题和翻页按钮 */}
onClick={handlePreviousChapter} <div style={{
disabled={!navigation?.previous} display: 'flex',
title={navigation?.previous ? `上一章: ${navigation.previous.title}` : '已是第一章'} justifyContent: 'space-between',
> alignItems: 'center',
gap: 8
</Button> }}>
<span style={{ fontSize: 16, fontWeight: 600 }}> <Button
{selectedChapter.chapter_number}: {selectedChapter.title} icon={<LeftOutlined />}
</span> onClick={handlePreviousChapter}
<Button disabled={!navigation?.previous}
icon={<RightOutlined />} title={navigation?.previous ? `上一章: ${navigation.previous.title}` : '已是第一章'}
onClick={handleNextChapter} size="small"
disabled={!navigation?.next} />
title={navigation?.next ? `下一章: ${navigation.next.title}` : '已是最后一章'} <span style={{
> fontSize: 14,
fontWeight: 600,
</Button> flex: 1,
</Space> textAlign: 'center',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
padding: '0 8px'
}}>
{selectedChapter.chapter_number}: {selectedChapter.title}
</span>
<Button
icon={<RightOutlined />}
onClick={handleNextChapter}
disabled={!navigation?.next}
title={navigation?.next ? `下一章: ${navigation.next.title}` : '已是最后一章'}
size="small"
/>
</div>
<Space> {/* 第二行:章节、开关、分析按钮 */}
{hasAnnotations && ( <div style={{
<> display: 'flex',
<Switch justifyContent: 'space-between',
checked={showAnnotations} alignItems: 'center',
onChange={setShowAnnotations} gap: 8
checkedChildren={<EyeOutlined />} }}>
unCheckedChildren={<EyeInvisibleOutlined />} <Button
/> icon={<UnorderedListOutlined />}
<span style={{ fontSize: 13, color: '#666' }}></span> onClick={() => setChapterListVisible(true)}
<Button size="small"
icon={<MenuOutlined />} >
onClick={() => setSidebarVisible(true)}
style={{ display: window.innerWidth < 768 ? 'inline-block' : 'none' }} </Button>
>
{hasAnnotations && (
</Button> <>
</> <Switch
)} checked={showAnnotations}
</Space> onChange={setShowAnnotations}
</div> checkedChildren={<EyeOutlined />}
unCheckedChildren={<EyeInvisibleOutlined />}
size="small"
style={{
flexShrink: 0,
height: 16,
minHeight: 16,
lineHeight: '16px'
}}
/>
<Button
icon={<MenuOutlined />}
onClick={() => setSidebarVisible(true)}
size="small"
>
</Button>
</>
)}
</div>
</div>
) : (
// 桌面端布局:保持原样
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<Space>
<Button
icon={<LeftOutlined />}
onClick={handlePreviousChapter}
disabled={!navigation?.previous}
title={navigation?.previous ? `上一章: ${navigation.previous.title}` : '已是第一章'}
>
</Button>
<span style={{ fontSize: 16, fontWeight: 600 }}>
{selectedChapter.chapter_number}: {selectedChapter.title}
</span>
<Button
icon={<RightOutlined />}
onClick={handleNextChapter}
disabled={!navigation?.next}
title={navigation?.next ? `下一章: ${navigation.next.title}` : '已是最后一章'}
>
</Button>
</Space>
<Space>
{hasAnnotations && (
<>
<Switch
checked={showAnnotations}
onChange={setShowAnnotations}
checkedChildren={<EyeOutlined />}
unCheckedChildren={<EyeInvisibleOutlined />}
/>
<span style={{ fontSize: 13, color: '#666' }}></span>
</>
)}
</Space>
</div>
)}
{hasAnnotations && annotationsData && ( {hasAnnotations && annotationsData && (
<div style={{ marginTop: 12, fontSize: 12, color: '#999' }}> <div style={{
marginTop: 12,
fontSize: isMobile ? 11 : 12,
color: '#999',
lineHeight: 1.5
}}>
{annotationsData.summary.total_annotations} {annotationsData.summary.total_annotations}
{annotationsData.summary.hooks > 0 && ` 🎣${annotationsData.summary.hooks}个钩子`} {annotationsData.summary.hooks > 0 && ` 🎣${annotationsData.summary.hooks}个钩子`}
{annotationsData.summary.foreshadows > 0 && {annotationsData.summary.foreshadows > 0 &&
@@ -286,10 +440,16 @@ const ChapterAnalysis: React.FC = () => {
</Card> </Card>
{/* 内容区域 */} {/* 内容区域 */}
<div style={{ flex: 1, display: 'flex', gap: 16, overflow: 'hidden' }}> <div style={{
flex: 1,
display: 'flex',
gap: isMobile ? 0 : 16,
overflow: 'hidden'
}}>
{/* 章节内容 */} {/* 章节内容 */}
<Card <Card
style={{ flex: 1, overflow: 'auto' }} style={{ flex: 1, overflow: 'auto' }}
bodyStyle={{ padding: isMobile ? '12px' : '24px' }}
loading={contentLoading} loading={contentLoading}
> >
{!contentLoading && ( {!contentLoading && (
@@ -311,12 +471,16 @@ const ChapterAnalysis: React.FC = () => {
onAnnotationClick={(annotation) => handleAnnotationClick(annotation, 'content')} onAnnotationClick={(annotation) => handleAnnotationClick(annotation, 'content')}
activeAnnotationId={activeAnnotationId} activeAnnotationId={activeAnnotationId}
scrollToAnnotation={scrollToContentAnnotation} scrollToAnnotation={scrollToContentAnnotation}
style={{
lineHeight: isMobile ? 1.8 : 2,
fontSize: isMobile ? 14 : 16,
}}
/> />
) : ( ) : (
<div <div
style={{ style={{
lineHeight: 2, lineHeight: isMobile ? 1.8 : 2,
fontSize: 16, fontSize: isMobile ? 14 : 16,
whiteSpace: 'pre-wrap', whiteSpace: 'pre-wrap',
wordBreak: 'break-word', wordBreak: 'break-word',
}} }}
@@ -329,7 +493,7 @@ const ChapterAnalysis: React.FC = () => {
</Card> </Card>
{/* 右侧记忆侧边栏(桌面端) */} {/* 右侧记忆侧边栏(桌面端) */}
{hasAnnotations && annotationsData && window.innerWidth >= 768 && ( {hasAnnotations && annotationsData && !isMobile && (
<Card <Card
style={{ width: 400, overflow: 'auto' }} style={{ width: 400, overflow: 'auto' }}
bodyStyle={{ padding: 0 }} bodyStyle={{ padding: 0 }}
@@ -351,7 +515,7 @@ const ChapterAnalysis: React.FC = () => {
placement="right" placement="right"
onClose={() => setSidebarVisible(false)} onClose={() => setSidebarVisible(false)}
open={sidebarVisible} open={sidebarVisible}
width="80%" width={isMobile ? '90%' : '80%'}
> >
<MemorySidebar <MemorySidebar
annotations={annotationsData.annotations} annotations={annotationsData.annotations}
+17
View File
@@ -425,6 +425,23 @@ export default function Characters() {
> >
<TextArea rows={2} placeholder="描述组织的宗旨和目标..." /> <TextArea rows={2} placeholder="描述组织的宗旨和目标..." />
</Form.Item> </Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item label="所在地" name="location">
<Input placeholder="组织的主要活动区域或总部位置" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="代表颜色" name="color">
<Input placeholder="如:深红色、金色、黑色等" />
</Form.Item>
</Col>
</Row>
<Form.Item label="格言/口号" name="motto">
<Input placeholder="组织的宗旨、格言或口号" />
</Form.Item>
</> </>
)} )}
+191 -6
View File
@@ -1,10 +1,12 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { Card, Table, Tag, Button, Space, message, Modal, Form, Select, InputNumber, Input, Descriptions } from 'antd'; import { Card, Table, Tag, Button, Space, message, Modal, Form, Select, InputNumber, Input, Descriptions } from 'antd';
import { PlusOutlined, TeamOutlined, UserOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'; import { PlusOutlined, TeamOutlined, UserOutlined, EditOutlined, DeleteOutlined, ThunderboltOutlined } from '@ant-design/icons';
import { useStore } from '../store'; import { useStore } from '../store';
import axios from 'axios'; import axios from 'axios';
const { TextArea } = Input;
interface Organization { interface Organization {
id: string; id: string;
character_id: string; character_id: string;
@@ -15,6 +17,7 @@ interface Organization {
power_level: number; power_level: number;
location?: string; location?: string;
motto?: string; motto?: string;
color?: string;
} }
interface OrganizationMember { interface OrganizationMember {
@@ -44,7 +47,11 @@ export default function Organizations() {
const [characters, setCharacters] = useState<Character[]>([]); const [characters, setCharacters] = useState<Character[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [isAddMemberModalOpen, setIsAddMemberModalOpen] = useState(false); const [isAddMemberModalOpen, setIsAddMemberModalOpen] = useState(false);
const [isEditOrgModalOpen, setIsEditOrgModalOpen] = useState(false);
const [isGenerating, setIsGenerating] = useState(false);
const [form] = Form.useForm(); const [form] = Form.useForm();
const [editOrgForm] = Form.useForm();
const [generateForm] = Form.useForm();
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768); const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
useEffect(() => { useEffect(() => {
@@ -144,6 +151,68 @@ export default function Organizations() {
}); });
}; };
const handleGenerateOrganization = async (values: {
name?: string;
organization_type?: string;
background?: string;
requirements?: string;
}) => {
try {
setIsGenerating(true);
await axios.post('/api/organizations/generate', {
project_id: projectId,
name: values.name,
organization_type: values.organization_type,
background: values.background,
requirements: values.requirements,
});
message.success('AI生成组织成功');
Modal.destroyAll();
generateForm.resetFields();
loadOrganizations();
} catch (error: any) {
message.error(error.response?.data?.detail || 'AI生成失败');
} finally {
setIsGenerating(false);
}
};
const showGenerateModal = () => {
Modal.confirm({
title: 'AI生成组织',
width: 600,
centered: !isMobile,
content: (
<Form form={generateForm} layout="vertical" style={{ marginTop: 16 }}>
<Form.Item
label="组织名称"
name="name"
>
<Input placeholder="如:天剑门、黑龙会(可选,AI会自动生成)" />
</Form.Item>
<Form.Item
label="组织类型"
name="organization_type"
>
<Input placeholder="如:门派、帮派、公司、学院(可选,AI会根据世界观生成)" />
</Form.Item>
<Form.Item label="背景设定" name="background">
<TextArea rows={3} placeholder="简要描述组织的背景和环境..." />
</Form.Item>
<Form.Item label="其他要求" name="requirements">
<TextArea rows={2} placeholder="其他特殊要求..." />
</Form.Item>
</Form>
),
okText: '生成',
cancelText: '取消',
onOk: async () => {
const values = await generateForm.validateFields();
await handleGenerateOrganization(values);
},
});
};
const getStatusColor = (status: string) => { const getStatusColor = (status: string) => {
const colors: Record<string, string> = { const colors: Record<string, string> = {
active: 'green', active: 'green',
@@ -263,6 +332,17 @@ export default function Organizations() {
{!isMobile && <Tag color="blue">{currentProject?.title}</Tag>} {!isMobile && <Tag color="blue">{currentProject?.title}</Tag>}
</Space> </Space>
} }
extra={
<Button
type="dashed"
icon={<ThunderboltOutlined />}
onClick={showGenerateModal}
loading={isGenerating}
size={isMobile ? 'small' : 'middle'}
>
AI生成组织
</Button>
}
> >
<div style={{ <div style={{
display: isMobile ? 'flex' : 'grid', display: isMobile ? 'flex' : 'grid',
@@ -308,19 +388,53 @@ export default function Organizations() {
<div style={{ minHeight: isMobile ? 'auto' : undefined }}> <div style={{ minHeight: isMobile ? 'auto' : undefined }}>
{selectedOrg ? ( {selectedOrg ? (
<Space direction="vertical" style={{ width: '100%' }} size="large"> <Space direction="vertical" style={{ width: '100%' }} size="large">
<Card title="组织详情" size="small"> <Card
title="组织详情"
size="small"
extra={
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={() => {
editOrgForm.setFieldsValue({
power_level: selectedOrg.power_level,
location: selectedOrg.location,
motto: selectedOrg.motto,
color: selectedOrg.color
});
setIsEditOrgModalOpen(true);
}}
>
</Button>
}
>
<Descriptions column={isMobile ? 1 : 2} size="small"> <Descriptions column={isMobile ? 1 : 2} size="small">
<Descriptions.Item label="组织名称">{selectedOrg.name}</Descriptions.Item> <Descriptions.Item label="组织名称">{selectedOrg.name}</Descriptions.Item>
<Descriptions.Item label="类型">{selectedOrg.type}</Descriptions.Item> <Descriptions.Item label="类型">{selectedOrg.type}</Descriptions.Item>
<Descriptions.Item label="成员数量">{selectedOrg.member_count}</Descriptions.Item> <Descriptions.Item label="成员数量">{selectedOrg.member_count}</Descriptions.Item>
<Descriptions.Item label="势力等级">{selectedOrg.power_level}</Descriptions.Item> <Descriptions.Item label="势力等级">
<Tag color={selectedOrg.power_level >= 70 ? 'red' : selectedOrg.power_level >= 50 ? 'orange' : 'default'}>
{selectedOrg.power_level}
</Tag>
</Descriptions.Item>
{selectedOrg.location && ( {selectedOrg.location && (
<Descriptions.Item label="所在地">{selectedOrg.location}</Descriptions.Item> <Descriptions.Item label="所在地" span={isMobile ? 1 : 2}>
{selectedOrg.location}
</Descriptions.Item>
)}
{selectedOrg.color && (
<Descriptions.Item label="代表颜色">
{selectedOrg.color}
</Descriptions.Item>
)} )}
{selectedOrg.motto && ( {selectedOrg.motto && (
<Descriptions.Item label="宗旨" span={2}>{selectedOrg.motto}</Descriptions.Item> <Descriptions.Item label="格言/口号" span={2}>
{selectedOrg.motto}
</Descriptions.Item>
)} )}
<Descriptions.Item label="目标/宗旨" span={2}> <Descriptions.Item label="组织目的" span={2}>
{selectedOrg.purpose} {selectedOrg.purpose}
</Descriptions.Item> </Descriptions.Item>
</Descriptions> </Descriptions>
@@ -445,6 +559,77 @@ export default function Organizations() {
</Form.Item> </Form.Item>
</Form> </Form>
</Modal> </Modal>
{/* 编辑组织模态框 */}
<Modal
title="编辑组织信息"
open={isEditOrgModalOpen}
onCancel={() => {
setIsEditOrgModalOpen(false);
editOrgForm.resetFields();
}}
footer={null}
centered={!isMobile}
width={isMobile ? '100%' : 500}
style={isMobile ? { top: 0, paddingBottom: 0, maxWidth: '100vw' } : undefined}
styles={isMobile ? { body: { maxHeight: 'calc(100vh - 110px)', overflowY: 'auto' } } : undefined}
>
<Form
form={editOrgForm}
layout="vertical"
onFinish={async (values) => {
if (!selectedOrg) return;
try {
await axios.put(`/api/organizations/${selectedOrg.id}`, values);
message.success('组织信息更新成功');
setIsEditOrgModalOpen(false);
loadOrganizations();
} catch (error) {
message.error('更新失败');
console.error(error);
}
}}
>
<Form.Item
name="power_level"
label="势力等级"
rules={[{ required: true, message: '请输入势力等级' }]}
tooltip="0-100的数值,表示组织的影响力"
>
<InputNumber min={0} max={100} style={{ width: '100%' }} />
</Form.Item>
<Form.Item
name="location"
label="所在地"
>
<Input placeholder="组织的主要活动区域或总部位置" />
</Form.Item>
<Form.Item
name="motto"
label="格言/口号"
>
<Input placeholder="组织的宗旨、格言或口号" />
</Form.Item>
<Form.Item
name="color"
label="代表颜色"
>
<Input placeholder="如:深红色、金色、黑色等" />
</Form.Item>
<Form.Item>
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
<Button onClick={() => setIsEditOrgModalOpen(false)}></Button>
<Button type="primary" htmlType="submit">
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
</div> </div>
); );
} }
+23 -3
View File
@@ -974,7 +974,6 @@ export default function ProjectWizardNew() {
styles={{ styles={{
body: { body: {
maxHeight: isMobile ? 'calc(100vh - 150px)' : 'calc(80vh - 110px)', maxHeight: isMobile ? 'calc(100vh - 150px)' : 'calc(80vh - 110px)',
overflowY: 'auto'
} }
}} }}
> >
@@ -1051,12 +1050,29 @@ export default function ProjectWizardNew() {
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={12}> <Col span={12}>
<Form.Item label="主要成员" name="organization_members"> <Form.Item label="势力等级" name="power_level">
<Input placeholder="如:张三、李四、王五" /> <InputNumber min={0} max={100} placeholder="0-100" style={{ width: '100%' }} />
</Form.Item> </Form.Item>
</Col> </Col>
</Row> </Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item label="所在地" name="location">
<Input placeholder="组织所在地" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="代表颜色" name="color">
<Input placeholder="如:深红色、金色" />
</Form.Item>
</Col>
</Row>
<Form.Item label="格言/口号" name="motto">
<Input placeholder="组织的格言或口号" />
</Form.Item>
<Form.Item <Form.Item
label="组织目的" label="组织目的"
name="organization_purpose" name="organization_purpose"
@@ -1064,6 +1080,10 @@ export default function ProjectWizardNew() {
> >
<TextArea rows={2} placeholder="描述组织的宗旨和目标..." /> <TextArea rows={2} placeholder="描述组织的宗旨和目标..." />
</Form.Item> </Form.Item>
<Form.Item label="主要成员" name="organization_members">
<Input placeholder="如:张三、李四、王五" />
</Form.Item>
</> </>
)} )}
+10
View File
@@ -172,6 +172,11 @@ export interface Character {
organization_members?: string; organization_members?: string;
traits?: string; traits?: string;
avatar_url?: string; avatar_url?: string;
// 组织扩展字段(从Organization表关联)
power_level?: number;
location?: string;
motto?: string;
color?: string;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
} }
@@ -190,6 +195,11 @@ export interface CharacterUpdate {
organization_purpose?: string; organization_purpose?: string;
organization_members?: string; organization_members?: string;
traits?: string; traits?: string;
// 组织扩展字段
power_level?: number;
location?: string;
motto?: string;
color?: string;
} }
// 章节类型定义 // 章节类型定义