feat: add NovelPage component with bookshelf, open book, and reading views
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
"""
|
||||
爽文开书优化(fiction.open_book)— LLM 调用逻辑。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from langchain_core.messages import HumanMessage, SystemMessage
|
||||
|
||||
from models.fiction_models import (
|
||||
EmotionFlow,
|
||||
FictionGuideWorldbook,
|
||||
OpenBookResult,
|
||||
)
|
||||
from services.fiction_prompt_utils import resolve_prompt
|
||||
from services.fiction_service import fiction_service
|
||||
from services.studio_step_respond import resolve_api_config
|
||||
from utils.llm_client import LLMClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_llm_client = LLMClient()
|
||||
_JSON_FENCE = re.compile(r"```(?:json)?\s*([\s\S]*?)```", re.IGNORECASE)
|
||||
|
||||
|
||||
def _extract_json(raw: str) -> Dict[str, Any]:
|
||||
text = (raw or "").strip()
|
||||
if not text:
|
||||
raise ValueError("模型返回为空")
|
||||
fence = _JSON_FENCE.search(text)
|
||||
if fence:
|
||||
text = fence.group(1).strip()
|
||||
try:
|
||||
return json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
start = text.find("{")
|
||||
end = text.rfind("}")
|
||||
if start >= 0 and end > start:
|
||||
return json.loads(text[start : end + 1])
|
||||
raise ValueError("无法解析模型返回的 JSON")
|
||||
|
||||
|
||||
def _validate_api_config(api_config: Dict[str, str]) -> None:
|
||||
if not api_config.get("api_key"):
|
||||
raise ValueError("API Key 未配置,请先在 API 配置页面保存密钥")
|
||||
if not api_config.get("api_url"):
|
||||
raise ValueError("API 地址未配置,请先在 API 配置页面保存 mainLLM")
|
||||
|
||||
|
||||
def _format_catalog_for_prompt(flows: List[EmotionFlow]) -> str:
|
||||
lines: List[str] = []
|
||||
for flow in flows:
|
||||
tags = "、".join(flow.tags or [])
|
||||
lines.append(f"- id: {flow.id}\n intro: {flow.intro}\n tags: {tags}")
|
||||
return "\n".join(lines) if lines else "(无可用情绪流)"
|
||||
|
||||
|
||||
def _format_guide_global_for_prompt() -> str:
|
||||
entries = fiction_service.get_guide_global_entries().entries
|
||||
lines: List[str] = []
|
||||
for entry in entries:
|
||||
lines.append(f"[{entry.layer}] {entry.title}\n{entry.content}")
|
||||
return "\n\n".join(lines) if lines else "(无全局指南)"
|
||||
|
||||
|
||||
def _build_open_book_messages(inspiration: str) -> List[Any]:
|
||||
default_settings = fiction_service.get_default_settings()
|
||||
system_prompt = resolve_prompt("openBook", default_settings.prompts.openBook)
|
||||
|
||||
catalog = fiction_service.get_emotion_catalog()
|
||||
catalog_text = _format_catalog_for_prompt(catalog.flows)
|
||||
guide_global_text = _format_guide_global_for_prompt()
|
||||
|
||||
user_content = f"""## 全局创作指南(L0–L3)
|
||||
{guide_global_text}
|
||||
|
||||
## 可选情绪流 catalog
|
||||
{catalog_text}
|
||||
|
||||
## 用户创作灵感
|
||||
{inspiration.strip()}
|
||||
|
||||
请根据以上信息优化开书方案。"""
|
||||
|
||||
return [
|
||||
SystemMessage(content=system_prompt),
|
||||
HumanMessage(content=user_content),
|
||||
]
|
||||
|
||||
|
||||
def _normalize_flow_ids(raw_ids: Any, valid_ids: set[str]) -> List[str]:
|
||||
if not isinstance(raw_ids, list):
|
||||
return []
|
||||
result: List[str] = []
|
||||
for item in raw_ids:
|
||||
fid = str(item).strip()
|
||||
if fid in valid_ids and fid not in result:
|
||||
result.append(fid)
|
||||
return result
|
||||
|
||||
|
||||
def _parse_open_book_response(
|
||||
data: Dict[str, Any], valid_flow_ids: set[str]
|
||||
) -> OpenBookResult:
|
||||
guide_raw = data.get("guide") or {}
|
||||
guide = FictionGuideWorldbook(
|
||||
persona=str(guide_raw.get("persona") or "").strip(),
|
||||
highlight=str(guide_raw.get("highlight") or "").strip(),
|
||||
experience=str(guide_raw.get("experience") or "").strip(),
|
||||
forbiddenZones=str(guide_raw.get("forbiddenZones") or "").strip(),
|
||||
)
|
||||
allowed = _normalize_flow_ids(data.get("allowedFlowIds"), valid_flow_ids)
|
||||
if not allowed and valid_flow_ids:
|
||||
allowed = [next(iter(valid_flow_ids))]
|
||||
|
||||
title = str(data.get("title") or "未命名作品").strip() or "未命名作品"
|
||||
optimized_intro = str(data.get("optimizedIntro") or "").strip()
|
||||
|
||||
return OpenBookResult(
|
||||
title=title,
|
||||
optimizedIntro=optimized_intro,
|
||||
guide=guide,
|
||||
allowedFlowIds=allowed,
|
||||
)
|
||||
|
||||
|
||||
async def run_open_book(
|
||||
inspiration: str,
|
||||
*,
|
||||
profile_id: Optional[str] = None,
|
||||
api_config: Optional[Dict[str, str]] = None,
|
||||
) -> OpenBookResult:
|
||||
inspiration = (inspiration or "").strip()
|
||||
if not inspiration:
|
||||
raise ValueError("创作灵感不能为空")
|
||||
|
||||
resolved = resolve_api_config(profile_id, api_config)
|
||||
_validate_api_config(resolved)
|
||||
|
||||
catalog = fiction_service.get_emotion_catalog()
|
||||
valid_flow_ids = {f.id for f in catalog.flows}
|
||||
|
||||
messages = _build_open_book_messages(inspiration)
|
||||
response = await _llm_client.chat_completion(
|
||||
messages=messages,
|
||||
api_url=resolved["api_url"],
|
||||
api_key=resolved["api_key"],
|
||||
model=resolved.get("model", "gpt-4o-mini"),
|
||||
temperature=0.7,
|
||||
max_tokens=8000,
|
||||
request_timeout=120,
|
||||
stream=False,
|
||||
)
|
||||
content = response["choices"][0]["message"]["content"]
|
||||
data = _extract_json(content)
|
||||
return _parse_open_book_response(data, valid_flow_ids)
|
||||
Reference in New Issue
Block a user