feat: AI改写面板样式优化、封面图片显示、max_tokens调整至8192
This commit is contained in:
@@ -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_score):good 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)}")
|
||||
|
||||
Reference in New Issue
Block a user