feat: add NovelPage component with bookshelf, open book, and reading views
This commit is contained in:
178
backend/services/fiction_coarse_service.py
Normal file
178
backend/services/fiction_coarse_service.py
Normal file
@@ -0,0 +1,178 @@
|
||||
"""
|
||||
爽文粗纲生成(fiction.coarse)— 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 CoarseOutline, CoarseOutlineEvent, FictionBookMetadata
|
||||
from services.fiction_metadata_service import fiction_metadata_service
|
||||
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_guide_global_layers(layers: List[str]) -> str:
|
||||
entries = fiction_service.get_guide_global_entries().entries
|
||||
filtered = [e for e in entries if e.layer in layers]
|
||||
lines: List[str] = []
|
||||
for entry in filtered:
|
||||
lines.append(f"[{entry.layer}] {entry.title}\n{entry.content}")
|
||||
return "\n\n".join(lines) if lines else "(无全局指南)"
|
||||
|
||||
|
||||
def _format_book_guide(book_id: str) -> str:
|
||||
guide = fiction_service.get_book_guide(book_id)
|
||||
parts = [
|
||||
f"主角人设:{guide.persona}",
|
||||
f"核心爽点:{guide.highlight}",
|
||||
f"用户体验:{guide.experience}",
|
||||
f"创作禁区:{guide.forbiddenZones}",
|
||||
]
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def _build_coarse_messages(book_id: str) -> List[Any]:
|
||||
settings = fiction_service.get_book_settings(book_id)
|
||||
user_prompt = settings.prompts.coarseOutline or fiction_service.get_default_settings().prompts.coarseOutline
|
||||
system_prompt = resolve_prompt("coarseOutline", user_prompt)
|
||||
|
||||
guide_l1 = _format_guide_global_layers(["L1"])
|
||||
book_guide = _format_book_guide(book_id)
|
||||
meta = fiction_service.get_book_meta(book_id)
|
||||
|
||||
user_content = f"""## 全局创作指南(L1,仅用于粗纲)
|
||||
{guide_l1}
|
||||
|
||||
## 本书 Guide 世界书
|
||||
{book_guide}
|
||||
|
||||
## 书名
|
||||
{meta.title}
|
||||
|
||||
## 已选情绪流 ID
|
||||
{", ".join(meta.allowedFlowIds or []) or "(未配置)"}
|
||||
|
||||
请生成本书的粗纲事件链。"""
|
||||
|
||||
return [
|
||||
SystemMessage(content=system_prompt),
|
||||
HumanMessage(content=user_content),
|
||||
]
|
||||
|
||||
|
||||
def _normalize_coarse_events(raw_events: Any) -> List[CoarseOutlineEvent]:
|
||||
if not isinstance(raw_events, list):
|
||||
return []
|
||||
events: List[CoarseOutlineEvent] = []
|
||||
for idx, item in enumerate(raw_events):
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
evt_id = str(item.get("id") or f"evt-{idx + 1}").strip()
|
||||
title = str(item.get("title") or f"事件 {idx + 1}").strip()
|
||||
summary = str(item.get("summary") or "").strip()
|
||||
order = item.get("order")
|
||||
if not isinstance(order, int):
|
||||
order = idx + 1
|
||||
events.append(
|
||||
CoarseOutlineEvent(id=evt_id, title=title, summary=summary, order=order)
|
||||
)
|
||||
events.sort(key=lambda e: e.order)
|
||||
return events
|
||||
|
||||
|
||||
def _parse_coarse_response(data: Dict[str, Any]) -> CoarseOutline:
|
||||
events = _normalize_coarse_events(data.get("events"))
|
||||
if not events:
|
||||
raise ValueError("粗纲事件列表为空")
|
||||
version = data.get("version")
|
||||
if not isinstance(version, int):
|
||||
version = 1
|
||||
return CoarseOutline(events=events, version=version)
|
||||
|
||||
|
||||
async def run_coarse_outline(
|
||||
book_id: str,
|
||||
*,
|
||||
profile_id: Optional[str] = None,
|
||||
api_config: Optional[Dict[str, str]] = None,
|
||||
) -> FictionBookMetadata:
|
||||
existing = fiction_metadata_service.get_metadata(book_id)
|
||||
if existing.coarseOutline.events:
|
||||
logger.info("Coarse outline already exists for book %s, skipping generation", book_id)
|
||||
return existing
|
||||
|
||||
resolved = resolve_api_config(profile_id, api_config)
|
||||
_validate_api_config(resolved)
|
||||
|
||||
run = fiction_metadata_service.get_run(book_id)
|
||||
if run.status == "error":
|
||||
fiction_metadata_service.clear_pipeline_error(book_id)
|
||||
|
||||
fiction_metadata_service.set_pipeline_stage(
|
||||
book_id, status="running", pipeline_stage="coarse"
|
||||
)
|
||||
try:
|
||||
messages = _build_coarse_messages(book_id)
|
||||
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)
|
||||
coarse = _parse_coarse_response(data)
|
||||
|
||||
current = fiction_metadata_service.get_metadata(book_id)
|
||||
current.coarseOutline = coarse
|
||||
saved = fiction_metadata_service.save_metadata(book_id, current)
|
||||
|
||||
fiction_metadata_service.set_pipeline_stage(
|
||||
book_id, status="idle", pipeline_stage="coarse_done"
|
||||
)
|
||||
return saved
|
||||
except Exception:
|
||||
fiction_metadata_service.set_pipeline_stage(
|
||||
book_id, status="error", pipeline_stage="coarse"
|
||||
)
|
||||
raise
|
||||
Reference in New Issue
Block a user