style: 将Tooltip组件替换为原生title属性,统一提示样式

This commit is contained in:
xiamuceer
2026-01-01 17:32:15 +08:00
parent 0ffa0ec4b5
commit fe22881194
19 changed files with 993 additions and 431 deletions
+6 -10
View File
@@ -7,7 +7,7 @@ import json
from app.database import get_db
from app.services.ai_service import AIService
from app.api.settings import get_user_ai_service
from app.services.prompt_service import prompt_service, PromptService
from app.services.prompt_service import PromptService
from app.logger import get_logger
router = APIRouter(prefix="/inspiration", tags=["灵感模式"])
@@ -441,17 +441,13 @@ async def quick_generate(
existing_text = "\n".join(existing_info) if existing_info else "暂无信息"
# 获取自定义提示词模板
prompt_template_str = await PromptService.get_template("INSPIRATION_QUICK_COMPLETE", user_id, db)
system_template = await PromptService.get_template("INSPIRATION_QUICK_COMPLETE", user_id, db)
# 格式化提示词
try:
prompts = json.loads(prompt_template_str)
# 格式化参数
prompts["system"] = prompts["system"].replace("{existing}", existing_text)
prompts["user"] = prompts["user"].replace("{existing}", existing_text)
except (json.JSONDecodeError, KeyError):
# 降级使用原有方法
prompts = prompt_service.get_inspiration_quick_complete_prompt(existing=existing_text)
prompts = {
"system": PromptService.format_prompt(system_template, existing=existing_text),
"user": "请补全小说信息"
}
# 调用AI - 流式生成并累积文本
accumulated_text = ""
+202 -26
View File
@@ -1052,26 +1052,49 @@ async def _continue_outline(
mcp_references=mcp_reference_materials
)
# 调用AI生成当前批次
# 调用AI生成当前批次(带重试机制)
logger.info(f"正在调用AI流式生成第{batch_num + 1}批...")
accumulated_text = ""
chunk_count = 0
async for chunk in user_ai_service.generate_text_stream(
prompt=prompt,
provider=request.provider,
model=request.model
):
chunk_count += 1
accumulated_text += chunk
max_retries = 2
retry_count = 0
outline_data = None
while retry_count <= max_retries:
accumulated_text = ""
chunk_count = 0
# 这里是非SSE接口,不需要发送chunk
ai_content = accumulated_text
ai_response = {"content": ai_content}
# 解析响应
outline_data = _parse_ai_response(ai_content)
# 第一次使用原始prompt,重试时添加格式强调
current_prompt = prompt if retry_count == 0 else (
prompt + "\n\n【重要提醒】请确保返回完整的JSON数组,不要截断。每个章节对象必须包含完整的title、summary等字段。"
)
async for chunk in user_ai_service.generate_text_stream(
prompt=current_prompt,
provider=request.provider,
model=request.model
):
chunk_count += 1
accumulated_text += chunk
# 这里是非SSE接口,不需要发送chunk
ai_content = accumulated_text
ai_response = {"content": ai_content}
# 解析响应
try:
outline_data = _parse_ai_response(ai_content, raise_on_error=True)
break # 解析成功,跳出循环
except JSONParseError as e:
retry_count += 1
if retry_count > max_retries:
# 超过最大重试次数,使用fallback数据
logger.error(f"❌ 第{batch_num + 1}批解析失败,已达最大重试次数({max_retries}),使用fallback数据")
outline_data = _parse_ai_response(ai_content, raise_on_error=False)
break
logger.warning(f"⚠️ 第{batch_num + 1}批JSON解析失败(第{retry_count}次),正在重试...")
# 保存当前批次的大纲
batch_outlines = await _save_outlines(
@@ -1111,8 +1134,27 @@ async def _continue_outline(
return OutlineListResponse(total=len(all_outlines), items=all_outlines)
def _parse_ai_response(ai_response: str) -> list:
"""解析AI响应为章节数据列表(使用统一的JSON清洗方法)"""
class JSONParseError(Exception):
"""JSON解析失败异常,用于触发重试"""
def __init__(self, message: str, original_content: str = ""):
super().__init__(message)
self.original_content = original_content
def _parse_ai_response(ai_response: str, raise_on_error: bool = False) -> list:
"""
解析AI响应为章节数据列表(使用统一的JSON清洗方法)
Args:
ai_response: AI返回的原始文本
raise_on_error: 如果为True,解析失败时抛出异常而不是返回fallback数据
Returns:
解析后的章节数据列表
Raises:
JSONParseError: 当raise_on_error=True且解析失败时抛出
"""
try:
# 使用统一的JSON清洗方法(从AIService导入)
from app.services.ai_service import AIService
@@ -1129,19 +1171,49 @@ def _parse_ai_response(ai_response: str) -> list:
else:
outline_data = [outline_data]
logger.info(f"✅ 成功解析 {len(outline_data)} 个章节数据")
return outline_data
# 验证解析结果是否有效(至少有一个有效章节)
valid_chapters = [
ch for ch in outline_data
if isinstance(ch, dict) and (ch.get("title") or ch.get("summary") or ch.get("content"))
]
if not valid_chapters:
error_msg = "解析结果无效:未找到有效的章节数据"
logger.error(f"{error_msg}")
if raise_on_error:
raise JSONParseError(error_msg, ai_response)
return [{
"title": "AI生成的大纲",
"content": ai_response[:1000],
"summary": ai_response[:1000]
}]
logger.info(f"✅ 成功解析 {len(valid_chapters)} 个章节数据")
return valid_chapters
except json.JSONDecodeError as e:
error_msg = f"JSON解析失败: {e}"
logger.error(f"❌ AI响应解析失败: {e}")
if raise_on_error:
raise JSONParseError(error_msg, ai_response)
# 返回一个包含原始内容的章节
return [{
"title": "AI生成的大纲",
"content": ai_response[:1000],
"summary": ai_response[:1000]
}]
except JSONParseError:
# 重新抛出JSONParseError
raise
except Exception as e:
logger.error(f"解析异常: {str(e)}")
error_msg = f"解析异常: {str(e)}"
logger.error(f"{error_msg}")
if raise_on_error:
raise JSONParseError(error_msg, ai_response)
return [{
"title": "解析异常的大纲",
"content": "系统错误",
@@ -1389,8 +1461,60 @@ async def new_outline_generator(
ai_content = accumulated_text
ai_response = {"content": ai_content}
# 解析响应
outline_data = _parse_ai_response(ai_content)
# 解析响应(带重试机制)
max_retries = 2
retry_count = 0
outline_data = None
while retry_count <= max_retries:
try:
# 使用 raise_on_error=True,解析失败时抛出异常
outline_data = _parse_ai_response(ai_content, raise_on_error=True)
break # 解析成功,跳出循环
except JSONParseError as e:
retry_count += 1
if retry_count > max_retries:
# 超过最大重试次数,使用fallback数据
logger.error(f"❌ 大纲解析失败,已达最大重试次数({max_retries}),使用fallback数据")
yield await SSEResponse.send_progress(
f"⚠️ 解析失败,使用备用数据",
96.5
)
outline_data = _parse_ai_response(ai_content, raise_on_error=False)
break
logger.warning(f"⚠️ JSON解析失败(第{retry_count}次),正在重试...")
yield await SSEResponse.send_progress(
f"⚠️ 解析失败,正在重试({retry_count}/{max_retries})...",
96
)
# 重新调用AI生成
accumulated_text = ""
chunk_count = 0
# 在prompt中添加格式强调
retry_prompt = prompt + "\n\n【重要提醒】请确保返回完整的JSON数组,不要截断。每个章节对象必须包含完整的title、summary等字段。"
async for chunk in user_ai_service.generate_text_stream(
prompt=retry_prompt,
provider=provider_param,
model=model_param
):
chunk_count += 1
accumulated_text += chunk
# 发送内容块
yield await SSEResponse.send_chunk(chunk)
# 每20个块发送心跳
if chunk_count % 20 == 0:
yield await SSEResponse.send_heartbeat()
ai_content = accumulated_text
ai_response = {"content": ai_content}
logger.info(f"🔄 重试生成完成,累计{len(ai_content)}字符")
# 全新生成模式:删除旧大纲和关联的所有章节
yield await SSEResponse.send_progress("清理旧大纲和章节...", 97)
@@ -1919,8 +2043,60 @@ async def continue_outline_generator(
ai_content = accumulated_text
ai_response = {"content": ai_content}
# 解析响应
outline_data = _parse_ai_response(ai_content)
# 解析响应(带重试机制)
max_retries = 2
retry_count = 0
outline_data = None
while retry_count <= max_retries:
try:
# 使用 raise_on_error=True,解析失败时抛出异常
outline_data = _parse_ai_response(ai_content, raise_on_error=True)
break # 解析成功,跳出循环
except JSONParseError as e:
retry_count += 1
if retry_count > max_retries:
# 超过最大重试次数,使用fallback数据
logger.error(f"❌ 第{batch_num + 1}批解析失败,已达最大重试次数({max_retries}),使用fallback数据")
yield await SSEResponse.send_progress(
f"⚠️ 第{str(batch_num + 1)}批解析失败,使用备用数据",
batch_progress + 11
)
outline_data = _parse_ai_response(ai_content, raise_on_error=False)
break
logger.warning(f"⚠️ 第{batch_num + 1}批JSON解析失败(第{retry_count}次),正在重试...")
yield await SSEResponse.send_progress(
f"⚠️ 第{str(batch_num + 1)}批解析失败,正在重试({retry_count}/{max_retries})...",
batch_progress + 10.5
)
# 重新调用AI生成
accumulated_text = ""
chunk_count = 0
# 在prompt中添加格式强调
retry_prompt = prompt + "\n\n【重要提醒】请确保返回完整的JSON数组,不要截断。每个章节对象必须包含完整的title、summary等字段。"
async for chunk in user_ai_service.generate_text_stream(
prompt=retry_prompt,
provider=provider_param,
model=model_param
):
chunk_count += 1
accumulated_text += chunk
# 发送内容块
yield await SSEResponse.send_chunk(chunk)
# 每20个块发送心跳
if chunk_count % 20 == 0:
yield await SSEResponse.send_heartbeat()
ai_content = accumulated_text
ai_response = {"content": ai_content}
logger.info(f"🔄 第{batch_num + 1}批重试生成完成,累计{len(ai_content)}字符")
# 保存当前批次的大纲
batch_outlines = await _save_outlines(