feat: AI改写面板样式优化、封面图片显示、max_tokens调整至8192

This commit is contained in:
wangwuww111
2026-03-16 16:35:59 +08:00
parent 253bc4aed2
commit d111f1a2cf
5 changed files with 1352 additions and 61 deletions

View File

@@ -453,6 +453,356 @@ class AIService:
traceback.print_exc()
return None
async def create_story(
self,
genre: str,
keywords: str,
protagonist: str = None,
conflict: str = None,
user_id: int = None
) -> Optional[Dict[str, Any]]:
"""
AI创作全新故事
:return: 包含完整故事结构的字典,或 None
"""
print(f"\n[create_story] ========== 开始创作 ==========")
print(f"[create_story] genre={genre}, keywords={keywords}")
print(f"[create_story] protagonist={protagonist}, conflict={conflict}")
print(f"[create_story] enabled={self.enabled}, api_key存在={bool(self.api_key)}")
if not self.enabled or not self.api_key:
print(f"[create_story] 服务未启用或API Key为空返回None")
return None
# 构建系统提示词
system_prompt = """你是一个专业的互动故事创作专家。请根据用户提供的题材和关键词,创作一个完整的互动故事。
【故事结构要求】
1. 故事要有吸引人的标题10字以内和简介50-100字
2. 创建2-3个主要角色每个角色需要详细设定
3. 故事包含6-8个节点形成多分支结构
4. 必须有2-4个不同类型的结局good/bad/neutral/special
5. 每个非结局节点有2个选项选项要有明显的剧情差异
【角色设定要求】
每个角色需要:
- name: 角色名2-4字
- role_type: 角色类型protagonist/antagonist/supporting
- gender: 性别male/female
- age_range: 年龄段youth/adult/middle_aged/elderly
- appearance: 外貌描述50-100字包含发型、眼睛、身材、穿着等
- personality: 性格特点30-50字
【节点内容要求】
- 每个节点150-300字分2-3段\\n\\n分隔
- 包含场景描写、人物对话、心理活动
- 对话要自然生动,描写要有画面感
【结局要求】
- 结局内容200-400字有情感冲击力
- 结局名称4-8字体现剧情走向
- 结局需要评分ending_scoregood 80-100, bad 20-50, neutral 50-70, special 70-90
【输出格式】严格JSON不要有任何额外文字
{
"title": "故事标题",
"description": "故事简介50-100字",
"category": "题材分类",
"characters": [
{
"name": "角色名",
"role_type": "protagonist",
"gender": "male",
"age_range": "youth",
"appearance": "外貌描述...",
"personality": "性格特点..."
}
],
"nodes": {
"start": {
"content": "开篇内容...",
"speaker": "旁白",
"choices": [
{"text": "选项A", "nextNodeKey": "node_1a"},
{"text": "选项B", "nextNodeKey": "node_1b"}
]
},
"node_1a": {
"content": "...",
"speaker": "旁白",
"choices": [...]
},
"ending_good": {
"content": "好结局内容...\\n\\n【达成结局xxx】",
"speaker": "旁白",
"is_ending": true,
"ending_name": "结局名称",
"ending_type": "good",
"ending_score": 90
}
},
"startNodeKey": "start"
}"""
# 构建用户提示词
protagonist_text = f"\n主角设定:{protagonist}" if protagonist else ""
conflict_text = f"\n核心冲突:{conflict}" if conflict else ""
user_prompt_text = f"""请创作一个互动故事:
【题材】{genre}
【关键词】{keywords}{protagonist_text}{conflict_text}
请创作完整的故事输出JSON格式"""
print(f"[create_story] 提示词构建完成开始调用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"[create_story] AI调用完成result存在={result is not None}")
if result and result.get("content"):
print(f"[create_story] AI返回内容长度={len(result.get('content', ''))}")
# 解析JSON响应
parsed = self._parse_story_json(result["content"])
print(f"[create_story] JSON解析结果: parsed存在={parsed is not None}")
if parsed:
parsed["tokens_used"] = result.get("tokens_used", 0)
print(f"[create_story] 成功! title={parsed.get('title')}, nodes数量={len(parsed.get('nodes', {}))}")
return parsed
else:
print(f"[create_story] JSON解析失败!")
return None
except Exception as e:
print(f"[create_story] 异常: {type(e).__name__}: {e}")
import traceback
traceback.print_exc()
return None
def _parse_story_json(self, content: str) -> Optional[Dict]:
"""解析AI返回的故事JSON"""
print(f"[_parse_story_json] 开始解析,内容长度={len(content)}")
# 移除 markdown 代码块标记
clean_content = content.strip()
if clean_content.startswith('```'):
clean_content = re.sub(r'^```(?:json)?\s*', '', clean_content)
clean_content = re.sub(r'\s*```$', '', clean_content)
result = None
# 方法1: 直接解析
try:
result = json.loads(clean_content)
if all(k in result for k in ['title', 'nodes', 'startNodeKey']):
print(f"[_parse_story_json] 直接解析成功!")
except json.JSONDecodeError as e:
print(f"[_parse_story_json] 直接解析失败: {e}")
result = None
# 方法2: 提取JSON块
if not result:
try:
brace_match = re.search(r'\{[\s\S]*\}', clean_content)
if brace_match:
json_str = brace_match.group(0)
result = json.loads(json_str)
if all(k in result for k in ['title', 'nodes', 'startNodeKey']):
print(f"[_parse_story_json] 花括号块解析成功!")
else:
result = None
except json.JSONDecodeError as e:
print(f"[_parse_story_json] 花括号块解析失败: {e}")
# 尝试修复截断的JSON
try:
result = self._try_fix_story_json(json_str)
if result:
print(f"[_parse_story_json] JSON修复成功!")
except:
pass
except Exception as e:
print(f"[_parse_story_json] 提取解析失败: {e}")
if not result:
print(f"[_parse_story_json] 所有解析方法都失败了")
return None
# 验证并修复故事结构
result = self._validate_and_fix_story(result)
return result
def _validate_and_fix_story(self, story: Dict) -> Dict:
"""验证并修复故事结构,确保每个分支都有结局"""
nodes = story.get('nodes', {})
if not nodes:
return story
print(f"[_validate_and_fix_story] 开始验证,节点数={len(nodes)}")
# 1. 找出所有结局节点
ending_nodes = [k for k, v in nodes.items() if v.get('is_ending')]
print(f"[_validate_and_fix_story] 已有结局节点: {ending_nodes}")
# 2. 找出所有被引用的节点(作为 nextNodeKey
referenced_keys = set()
for node_key, node_data in nodes.items():
choices = node_data.get('choices', [])
if isinstance(choices, list):
for choice in choices:
if isinstance(choice, dict) and 'nextNodeKey' in choice:
referenced_keys.add(choice['nextNodeKey'])
# 3. 找出"叶子节点":没有 choices 或 choices 为空,且不是结局
leaf_nodes = []
broken_refs = [] # 引用了不存在节点的选项
for node_key, node_data in nodes.items():
choices = node_data.get('choices', [])
is_ending = node_data.get('is_ending', False)
# 检查 choices 中引用的节点是否存在
if isinstance(choices, list):
for choice in choices:
if isinstance(choice, dict):
next_key = choice.get('nextNodeKey')
if next_key and next_key not in nodes:
broken_refs.append((node_key, next_key))
# 没有有效选项且不是结局的节点
if not is_ending and (not choices or len(choices) == 0):
leaf_nodes.append(node_key)
print(f"[_validate_and_fix_story] 叶子节点(无选项非结局): {leaf_nodes}")
print(f"[_validate_and_fix_story] 断裂引用: {broken_refs}")
# 4. 修复:将叶子节点标记为结局
for node_key in leaf_nodes:
node = nodes[node_key]
print(f"[_validate_and_fix_story] 修复节点 {node_key} -> 标记为结局")
node['is_ending'] = True
if not node.get('ending_name'):
node['ending_name'] = '命运的转折'
if not node.get('ending_type'):
node['ending_type'] = 'neutral'
if not node.get('ending_score'):
node['ending_score'] = 60
# 5. 修复:处理断裂引用(选项指向不存在的节点)
for node_key, missing_key in broken_refs:
node = nodes[node_key]
choices = node.get('choices', [])
# 移除指向不存在节点的选项
valid_choices = [c for c in choices if c.get('nextNodeKey') in nodes]
if len(valid_choices) == 0:
# 没有有效选项了,标记为结局
print(f"[_validate_and_fix_story] 节点 {node_key} 所有选项失效 -> 标记为结局")
node['is_ending'] = True
node['choices'] = []
if not node.get('ending_name'):
node['ending_name'] = '未知结局'
if not node.get('ending_type'):
node['ending_type'] = 'neutral'
if not node.get('ending_score'):
node['ending_score'] = 50
else:
node['choices'] = valid_choices
# 6. 最终检查:确保至少有一个结局
ending_count = sum(1 for v in nodes.values() if v.get('is_ending'))
print(f"[_validate_and_fix_story] 修复后结局数: {ending_count}")
if ending_count == 0:
# 如果还是没有结局,找最后一个节点标记为结局
last_key = list(nodes.keys())[-1]
print(f"[_validate_and_fix_story] 强制将最后节点 {last_key} 标记为结局")
nodes[last_key]['is_ending'] = True
nodes[last_key]['ending_name'] = '故事的终点'
nodes[last_key]['ending_type'] = 'neutral'
nodes[last_key]['ending_score'] = 60
nodes[last_key]['choices'] = []
return story
def _try_fix_story_json(self, json_str: str) -> Optional[Dict]:
"""尝试修复不完整的故事JSON"""
try:
# 尝试找到最后一个完整的节点
# 查找 "nodes": { 的位置
nodes_match = re.search(r'"nodes"\s*:\s*\{', json_str)
if not nodes_match:
return None
# 找所有看起来完整的节点(有 "content" 字段的)
node_pattern = r'"(\w+)"\s*:\s*\{[^{}]*"content"[^{}]*(?:\{[^{}]*\}[^{}]*)*\}'
nodes = list(re.finditer(node_pattern, json_str[nodes_match.end():]))
if len(nodes) < 2:
return None
# 取到最后一个完整节点的位置
last_node_end = nodes_match.end() + nodes[-1].end()
# 尝试提取基本信息
title_match = re.search(r'"title"\s*:\s*"([^"]+)"', json_str)
desc_match = re.search(r'"description"\s*:\s*"([^"]+)"', json_str)
category_match = re.search(r'"category"\s*:\s*"([^"]+)"', json_str)
start_match = re.search(r'"startNodeKey"\s*:\s*"([^"]+)"', json_str)
title = title_match.group(1) if title_match else "AI创作故事"
description = desc_match.group(1) if desc_match else ""
category = category_match.group(1) if category_match else "都市言情"
startNodeKey = start_match.group(1) if start_match else "start"
# 提取角色
characters = []
char_match = re.search(r'"characters"\s*:\s*\[([\s\S]*?)\]', json_str)
if char_match:
try:
characters = json.loads('[' + char_match.group(1) + ']')
except:
pass
# 提取节点
nodes_content = json_str[nodes_match.start():last_node_end] + '}'
try:
nodes_obj = json.loads('{' + nodes_content + '}')
nodes_dict = nodes_obj.get('nodes', {})
except:
return None
if len(nodes_dict) < 2:
return None
result = {
"title": title,
"description": description,
"category": category,
"characters": characters,
"nodes": nodes_dict,
"startNodeKey": startNodeKey
}
print(f"[_try_fix_story_json] 修复成功! 节点数={len(nodes_dict)}")
return result
except Exception as e:
print(f"[_try_fix_story_json] 修复失败: {e}")
return None
def _parse_branch_json(self, content: str) -> Optional[Dict]:
"""解析AI返回的分支JSON"""
print(f"[_parse_branch_json] 开始解析,内容长度={len(content)}")
@@ -567,7 +917,7 @@ class AIService:
{"role": "user", "content": user_prompt}
],
"temperature": 0.85,
"max_tokens": 6000 # 增加输出长度确保JSON完整
"max_tokens": 8192 # DeepSeek 最大输出限制
}
print(f"[_call_deepseek_long] system_prompt长度={len(system_prompt)}")