feat: 添加测试用户到种子数据, AI改写功能优化, 前端联调修复

This commit is contained in:
2026-03-09 23:00:15 +08:00
parent 5e931424ab
commit 9948ccba8f
12 changed files with 1082 additions and 151 deletions

View File

@@ -32,6 +32,24 @@ class CreateDraftRequest(BaseModel):
prompt: str
class CreateEndingDraftRequest(BaseModel):
"""结局改写请求"""
userId: int
storyId: int
endingName: str
endingContent: str
prompt: str
class ContinueEndingDraftRequest(BaseModel):
"""结局续写请求"""
userId: int
storyId: int
endingName: str
endingContent: str
prompt: str
class DraftResponse(BaseModel):
id: int
storyId: int
@@ -120,6 +138,174 @@ async def process_ai_rewrite(draft_id: int):
pass
async def process_ai_rewrite_ending(draft_id: int):
"""后台异步处理AI改写结局"""
from app.database import async_session_factory
from app.services.ai import ai_service
import json
import re
async with async_session_factory() as db:
try:
# 获取草稿
result = await db.execute(select(StoryDraft).where(StoryDraft.id == draft_id))
draft = result.scalar_one_or_none()
if not draft:
return
# 更新状态为处理中
draft.status = DraftStatus.processing
await db.commit()
# 获取故事信息
story_result = await db.execute(select(Story).where(Story.id == draft.story_id))
story = story_result.scalar_one_or_none()
if not story:
draft.status = DraftStatus.failed
draft.error_message = "故事不存在"
draft.completed_at = datetime.now()
await db.commit()
return
# 从 path_history 获取结局信息
ending_info = draft.path_history or {}
ending_name = ending_info.get("endingName", "未知结局")
ending_content = ending_info.get("endingContent", "")
# 调用AI服务改写结局
ai_result = await ai_service.rewrite_ending(
story_title=story.title,
story_category=story.category or "未知",
ending_name=ending_name,
ending_content=ending_content,
user_prompt=draft.user_prompt
)
if ai_result and ai_result.get("content"):
content = ai_result["content"]
new_ending_name = f"{ending_name}AI改写"
# 尝试解析 JSON 格式的返回
try:
json_match = re.search(r'\{[^{}]*"ending_name"[^{}]*"content"[^{}]*\}', content, re.DOTALL)
if json_match:
parsed = json.loads(json_match.group())
new_ending_name = parsed.get("ending_name", new_ending_name)
content = parsed.get("content", content)
else:
parsed = json.loads(content)
new_ending_name = parsed.get("ending_name", new_ending_name)
content = parsed.get("content", content)
except (json.JSONDecodeError, AttributeError):
pass
# 成功 - 存储为单节点结局格式
draft.status = DraftStatus.completed
draft.ai_nodes = [{
"nodeKey": "ending_rewrite",
"content": content,
"speaker": "旁白",
"isEnding": True,
"endingName": new_ending_name,
"endingType": "rewrite"
}]
draft.entry_node_key = "ending_rewrite"
draft.tokens_used = ai_result.get("tokens_used", 0)
draft.title = f"{story.title}-{new_ending_name}"
else:
draft.status = DraftStatus.failed
draft.error_message = "AI服务暂时不可用"
draft.completed_at = datetime.now()
await db.commit()
except Exception as e:
print(f"[process_ai_rewrite_ending] 异常: {e}")
import traceback
traceback.print_exc()
try:
draft.status = DraftStatus.failed
draft.error_message = str(e)[:500]
draft.completed_at = datetime.now()
await db.commit()
except:
pass
async def process_ai_continue_ending(draft_id: int):
"""后台异步处理AI续写结局"""
from app.database import async_session_factory
from app.services.ai import ai_service
async with async_session_factory() as db:
try:
# 获取草稿
result = await db.execute(select(StoryDraft).where(StoryDraft.id == draft_id))
draft = result.scalar_one_or_none()
if not draft:
return
# 更新状态为处理中
draft.status = DraftStatus.processing
await db.commit()
# 获取故事信息
story_result = await db.execute(select(Story).where(Story.id == draft.story_id))
story = story_result.scalar_one_or_none()
if not story:
draft.status = DraftStatus.failed
draft.error_message = "故事不存在"
draft.completed_at = datetime.now()
await db.commit()
return
# 从 path_history 获取结局信息
ending_info = draft.path_history or {}
ending_name = ending_info.get("endingName", "未知结局")
ending_content = ending_info.get("endingContent", "")
# 调用AI服务续写结局
ai_result = await ai_service.continue_ending(
story_title=story.title,
story_category=story.category or "未知",
ending_name=ending_name,
ending_content=ending_content,
user_prompt=draft.user_prompt
)
if ai_result and ai_result.get("nodes"):
# 成功 - 存储多节点分支格式
draft.status = DraftStatus.completed
draft.ai_nodes = ai_result["nodes"]
draft.entry_node_key = ai_result.get("entryNodeKey", "continue_1")
draft.tokens_used = ai_result.get("tokens_used", 0)
draft.title = f"{story.title}-{ending_name}续写"
else:
draft.status = DraftStatus.failed
draft.error_message = "AI服务暂时不可用"
draft.completed_at = datetime.now()
await db.commit()
except Exception as e:
print(f"[process_ai_continue_ending] 异常: {e}")
import traceback
traceback.print_exc()
try:
draft.status = DraftStatus.failed
draft.error_message = str(e)[:500]
draft.completed_at = datetime.now()
await db.commit()
except:
pass
# ============ API 路由 ============
@router.post("")
@@ -173,6 +359,96 @@ async def create_draft(
}
@router.post("/ending")
async def create_ending_draft(
request: CreateEndingDraftRequest,
background_tasks: BackgroundTasks,
db: AsyncSession = Depends(get_db)
):
"""提交AI改写结局任务异步处理"""
if not request.prompt:
raise HTTPException(status_code=400, detail="请输入改写指令")
# 获取故事信息
result = await db.execute(select(Story).where(Story.id == request.storyId))
story = result.scalar_one_or_none()
if not story:
raise HTTPException(status_code=404, detail="故事不存在")
# 创建草稿记录,将结局信息存在 path_history
draft = StoryDraft(
user_id=request.userId,
story_id=request.storyId,
title=f"{story.title}-结局改写",
path_history={"endingName": request.endingName, "endingContent": request.endingContent},
current_node_key="ending",
current_content=request.endingContent,
user_prompt=request.prompt,
status=DraftStatus.pending
)
db.add(draft)
await db.commit()
await db.refresh(draft)
# 添加后台任务
background_tasks.add_task(process_ai_rewrite_ending, draft.id)
return {
"code": 0,
"data": {
"draftId": draft.id,
"message": "已提交AI正在生成新结局..."
}
}
@router.post("/continue-ending")
async def create_continue_ending_draft(
request: ContinueEndingDraftRequest,
background_tasks: BackgroundTasks,
db: AsyncSession = Depends(get_db)
):
"""提交AI续写结局任务异步处理"""
if not request.prompt:
raise HTTPException(status_code=400, detail="请输入续写指令")
# 获取故事信息
result = await db.execute(select(Story).where(Story.id == request.storyId))
story = result.scalar_one_or_none()
if not story:
raise HTTPException(status_code=404, detail="故事不存在")
# 创建草稿记录,将结局信息存在 path_history
draft = StoryDraft(
user_id=request.userId,
story_id=request.storyId,
title=f"{story.title}-结局续写",
path_history={"endingName": request.endingName, "endingContent": request.endingContent},
current_node_key="ending",
current_content=request.endingContent,
user_prompt=request.prompt,
status=DraftStatus.pending
)
db.add(draft)
await db.commit()
await db.refresh(draft)
# 添加后台任务
background_tasks.add_task(process_ai_continue_ending, draft.id)
return {
"code": 0,
"data": {
"draftId": draft.id,
"message": "已提交AI正在续写故事..."
}
}
@router.get("")
async def get_drafts(
userId: int,

View File

@@ -274,6 +274,139 @@ class AIService:
traceback.print_exc()
return None
async def continue_ending(
self,
story_title: str,
story_category: str,
ending_name: str,
ending_content: str,
user_prompt: str
) -> Optional[Dict[str, Any]]:
"""
AI续写结局从结局开始续写新的剧情分支
"""
print(f"\n[continue_ending] ========== 开始调用 ==========")
print(f"[continue_ending] story_title={story_title}, category={story_category}")
print(f"[continue_ending] ending_name={ending_name}")
print(f"[continue_ending] user_prompt={user_prompt}")
print(f"[continue_ending] enabled={self.enabled}, api_key存在={bool(self.api_key)}")
if not self.enabled or not self.api_key:
print(f"[continue_ending] 服务未启用或API Key为空返回None")
return None
# 构建系统提示词
system_prompt = """你是一个专业的互动故事续写专家。用户已经到达故事的某个结局,现在想要从这个结局继续故事发展。
【任务】
请从这个结局开始,创作故事的延续。新剧情必须紧接结局内容,仿佛故事并没有在此结束,而是有了新的发展。
【写作要求】
1. 第一个节点必须紧密衔接原结局,像是结局之后自然发生的事
2. 生成 4-6 个新节点,形成有层次的剧情发展(起承转合)
3. 每个节点内容 150-300 字,要分 2-3 个自然段(用\\n\\n分隔包含场景描写、人物对话、心理活动
4. 每个非结局节点有 2 个选项,选项要有明显的剧情差异和后果
5. 严格符合用户的续写意图,围绕用户指令展开剧情
6. 保持原故事的人物性格、语言风格和世界观
7. 对话要自然生动,描写要有画面感
【关于新结局 - 极其重要!】
★★★ 每一条分支路径的尽头必须是新结局节点 ★★★
- 结局节点必须设置 "is_ending": true
- 结局内容要 200-400 字,分 2-3 段,有情感冲击力
- 结局名称 4-8 字,体现剧情走向
- 如果有2个选项分支最终必须有2个不同的结局
- 每个结局必须有 "ending_score" 评分0-100
【输出格式】严格JSON不要有任何额外文字
{
"nodes": {
"continue_1": {
"content": "续写剧情第一段150-300字...",
"speaker": "旁白",
"choices": [
{"text": "选项A5-15字", "nextNodeKey": "continue_2a"},
{"text": "选项B5-15字", "nextNodeKey": "continue_2b"}
]
},
"continue_2a": {
"content": "...",
"speaker": "旁白",
"choices": [
{"text": "选项C", "nextNodeKey": "continue_ending_good"},
{"text": "选项D", "nextNodeKey": "continue_ending_bad"}
]
},
"continue_ending_good": {
"content": "新好结局内容200-400字...\\n\\n【达成结局xxx】",
"speaker": "旁白",
"is_ending": true,
"ending_name": "新结局名称",
"ending_type": "good",
"ending_score": 90
},
"continue_ending_bad": {
"content": "新坏结局内容...\\n\\n【达成结局xxx】",
"speaker": "旁白",
"is_ending": true,
"ending_name": "新结局名称",
"ending_type": "bad",
"ending_score": 40
}
},
"entryNodeKey": "continue_1"
}"""
# 构建用户提示词
user_prompt_text = f"""【原故事信息】
故事标题:{story_title}
故事分类:{story_category}
【已达成的结局】
结局名称:{ending_name}
结局内容:{ending_content[:800]}
【用户续写指令】
{user_prompt}
请从这个结局开始续写新的剧情分支输出JSON格式"""
print(f"[continue_ending] 提示词构建完成开始调用AI...")
try:
result = None
if self.provider == "openai":
result = await self._call_openai_long(system_prompt, user_prompt_text)
elif self.provider == "claude":
result = await self._call_claude(f"{system_prompt}\n\n{user_prompt_text}")
elif self.provider == "qwen":
result = await self._call_qwen_long(system_prompt, user_prompt_text)
elif self.provider == "deepseek":
result = await self._call_deepseek_long(system_prompt, user_prompt_text)
print(f"[continue_ending] AI调用完成result存在={result is not None}")
if result and result.get("content"):
print(f"[continue_ending] AI返回内容长度={len(result.get('content', ''))}")
# 解析JSON响应复用 rewrite_branch 的解析方法)
parsed = self._parse_branch_json(result["content"])
print(f"[continue_ending] JSON解析结果: parsed存在={parsed is not None}")
if parsed:
parsed["tokens_used"] = result.get("tokens_used", 0)
print(f"[continue_ending] 成功! nodes数量={len(parsed.get('nodes', {}))}")
return parsed
else:
print(f"[continue_ending] JSON解析失败!")
return None
except Exception as e:
print(f"[continue_ending] 异常: {type(e).__name__}: {e}")
import traceback
traceback.print_exc()
return None
def _parse_branch_json(self, content: str) -> Optional[Dict]:
"""解析AI返回的分支JSON"""
print(f"[_parse_branch_json] 开始解析,内容长度={len(content)}")