新增角色管理、世界观设定、章节管理和AI工具调用功能
- 角色管理:CRUD API + 卡片网格UI,每个角色约200字描述 - 世界观设定:单篇长文编辑器,建议500字以上 - 章节管理:CRUD + 排序 API,左右分栏编辑器,单章5000字上限 - AI工具调用:支持工具审批流程和结构化工具调用 - 小说详情页新增章节、角色、世界观入口按钮 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
205
backend/app/services/tools.py
Normal file
205
backend/app/services/tools.py
Normal file
@@ -0,0 +1,205 @@
|
||||
"""工具注册表和执行器 —— 大纲 & 世界观 CRUD"""
|
||||
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlmodel import Session, select, func
|
||||
|
||||
from ..database import engine
|
||||
from ..models import Novel, Outline, WorldSetting
|
||||
|
||||
|
||||
# ── 工具定义(provider 无关格式)──
|
||||
|
||||
TOOL_DEFINITIONS: list[dict] = [
|
||||
{
|
||||
"name": "list_outlines",
|
||||
"description": "列出当前小说的所有大纲节点,返回每个节点的 id、排序、摘要和详情",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "create_outline",
|
||||
"description": "为当前小说创建一个新的大纲节点",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"summary": {"type": "string", "description": "大纲摘要(简短标题,200字以内)"},
|
||||
"detail": {"type": "string", "description": "大纲详情(可选,1000字以内)"},
|
||||
},
|
||||
"required": ["summary"],
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "update_outline",
|
||||
"description": "更新指定大纲节点的摘要或详情",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"outline_id": {"type": "integer", "description": "要更新的大纲节点 ID"},
|
||||
"summary": {"type": "string", "description": "新的摘要(可选)"},
|
||||
"detail": {"type": "string", "description": "新的详情(可选)"},
|
||||
},
|
||||
"required": ["outline_id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "delete_outline",
|
||||
"description": "删除指定的大纲节点",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"outline_id": {"type": "integer", "description": "要删除的大纲节点 ID"},
|
||||
},
|
||||
"required": ["outline_id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "get_world_setting",
|
||||
"description": "获取当前小说的世界观设定内容",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "save_world_setting",
|
||||
"description": "保存/更新当前小说的世界观设定",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string", "description": "世界观设定内容(5000字以内)"},
|
||||
},
|
||||
"required": ["content"],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def get_openai_tools() -> list[dict]:
|
||||
"""转换为 OpenAI function calling 格式"""
|
||||
return [
|
||||
{"type": "function", "function": t}
|
||||
for t in TOOL_DEFINITIONS
|
||||
]
|
||||
|
||||
|
||||
def get_anthropic_tools() -> list[dict]:
|
||||
"""转换为 Anthropic tool_use 格式"""
|
||||
return [
|
||||
{"name": t["name"], "description": t["description"], "input_schema": t["parameters"]}
|
||||
for t in TOOL_DEFINITIONS
|
||||
]
|
||||
|
||||
|
||||
# ── 工具执行 ──
|
||||
|
||||
|
||||
def execute_tool(name: str, arguments: dict, novel_id: int) -> str:
|
||||
"""执行工具,返回结果字符串(JSON)"""
|
||||
with Session(engine) as session:
|
||||
handlers = {
|
||||
"list_outlines": _list_outlines,
|
||||
"create_outline": _create_outline,
|
||||
"update_outline": _update_outline,
|
||||
"delete_outline": _delete_outline,
|
||||
"get_world_setting": _get_world_setting,
|
||||
"save_world_setting": _save_world_setting,
|
||||
}
|
||||
handler = handlers.get(name)
|
||||
if not handler:
|
||||
return json.dumps({"error": f"未知工具: {name}"}, ensure_ascii=False)
|
||||
try:
|
||||
return handler(session, novel_id, arguments)
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)}, ensure_ascii=False)
|
||||
|
||||
|
||||
def _list_outlines(session: Session, novel_id: int, _args: dict) -> str:
|
||||
items = session.exec(
|
||||
select(Outline)
|
||||
.where(Outline.novel_id == novel_id)
|
||||
.order_by(Outline.sort_order)
|
||||
).all()
|
||||
result = [
|
||||
{"id": o.id, "sort_order": o.sort_order, "summary": o.summary, "detail": o.detail}
|
||||
for o in items
|
||||
]
|
||||
return json.dumps({"outlines": result, "total": len(result)}, ensure_ascii=False)
|
||||
|
||||
|
||||
def _create_outline(session: Session, novel_id: int, args: dict) -> str:
|
||||
max_order = session.exec(
|
||||
select(func.max(Outline.sort_order)).where(Outline.novel_id == novel_id)
|
||||
).one()
|
||||
next_order = (max_order or 0) + 1
|
||||
|
||||
outline = Outline(
|
||||
novel_id=novel_id,
|
||||
sort_order=next_order,
|
||||
summary=args["summary"],
|
||||
detail=args.get("detail", ""),
|
||||
)
|
||||
session.add(outline)
|
||||
session.commit()
|
||||
session.refresh(outline)
|
||||
return json.dumps(
|
||||
{"id": outline.id, "sort_order": outline.sort_order, "summary": outline.summary, "detail": outline.detail},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
|
||||
def _update_outline(session: Session, novel_id: int, args: dict) -> str:
|
||||
outline = session.get(Outline, args["outline_id"])
|
||||
if not outline or outline.novel_id != novel_id:
|
||||
return json.dumps({"error": "大纲不存在"}, ensure_ascii=False)
|
||||
|
||||
if "summary" in args:
|
||||
outline.summary = args["summary"]
|
||||
if "detail" in args:
|
||||
outline.detail = args["detail"]
|
||||
outline.updated_at = datetime.now(timezone.utc)
|
||||
session.add(outline)
|
||||
session.commit()
|
||||
session.refresh(outline)
|
||||
return json.dumps(
|
||||
{"id": outline.id, "summary": outline.summary, "detail": outline.detail},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
|
||||
def _delete_outline(session: Session, novel_id: int, args: dict) -> str:
|
||||
outline = session.get(Outline, args["outline_id"])
|
||||
if not outline or outline.novel_id != novel_id:
|
||||
return json.dumps({"error": "大纲不存在"}, ensure_ascii=False)
|
||||
|
||||
session.delete(outline)
|
||||
session.commit()
|
||||
return json.dumps({"deleted": True, "id": args["outline_id"]}, ensure_ascii=False)
|
||||
|
||||
|
||||
def _get_world_setting(session: Session, novel_id: int, _args: dict) -> str:
|
||||
ws = session.exec(
|
||||
select(WorldSetting).where(WorldSetting.novel_id == novel_id)
|
||||
).first()
|
||||
if not ws:
|
||||
return json.dumps({"content": "", "exists": False}, ensure_ascii=False)
|
||||
return json.dumps({"content": ws.content, "exists": True}, ensure_ascii=False)
|
||||
|
||||
|
||||
def _save_world_setting(session: Session, novel_id: int, args: dict) -> str:
|
||||
ws = session.exec(
|
||||
select(WorldSetting).where(WorldSetting.novel_id == novel_id)
|
||||
).first()
|
||||
now = datetime.now(timezone.utc)
|
||||
if ws:
|
||||
ws.content = args["content"]
|
||||
ws.updated_at = now
|
||||
else:
|
||||
ws = WorldSetting(novel_id=novel_id, content=args["content"])
|
||||
session.add(ws)
|
||||
session.commit()
|
||||
session.refresh(ws)
|
||||
return json.dumps({"saved": True, "content_length": len(ws.content)}, ensure_ascii=False)
|
||||
Reference in New Issue
Block a user