Files
NovalRedot/backend/app/database.py
ROOG 0525c8217e
All checks were successful
Deploy Noval to K3s / deploy (push) Successful in 54s
新增子 Agent 与技能系统管理:
- **子 Agent 支持**:
  - 新增 `dispatch_subagent` 工具,支持分配特定任务给子 Agent,由其独立操作。
  - 前端新增 `ChatSubAgent` 组件,支持子 Agent 任务状态展示(运行中/已完成)、工具调用进度及输出预览等。

- **技能管理扩展**:
  - 新增 Skill 编辑器页面(`SkillEditorView.vue`),支持创建、编辑、删除技能。
  - 完善技能工具权限分组选择功能,按类型灵活分配工具。

- **工具增强**:
  - 新增工具接口:`get_outline_detail`、`get_character_detail`、`get_chapter_detail`,用于获取详情内容。
  - 优化部分工具返回数据,仅输出基础信息以减少上下文体积。

- 后端添加 `SPECIAL_TOOLS` 支持,增强 `dispatch_subagent` 的特殊处理能力。

同时优化部分前后端交互,提升代码维护性与可读性。
2026-03-23 02:23:22 +08:00

172 lines
9.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import json
import os
from pathlib import Path
from sqlalchemy import text
from sqlmodel import SQLModel, Session, create_engine, select
# 数据目录优先使用环境变量Docker 中设为 /app/data否则用 backend/data/
DATA_DIR = Path(os.environ.get("NOVAL_DATA_DIR", Path(__file__).resolve().parent.parent / "data"))
DATA_DIR.mkdir(exist_ok=True)
DATABASE_URL = f"sqlite:///{DATA_DIR / 'noval.db'}"
engine = create_engine(DATABASE_URL, echo=False)
def init_db() -> None:
SQLModel.metadata.create_all(engine)
_migrate_outline_parent_id()
_seed_builtin_skills()
def _migrate_outline_parent_id() -> None:
"""迁移:为 outline 表添加 parent_id 列(如果尚不存在)"""
with Session(engine) as session:
# 检查 outline 表是否存在 parent_id 列
result = session.exec(text("PRAGMA table_info(outline)"))
columns = [row[1] for row in result]
if "parent_id" not in columns:
session.exec(text("ALTER TABLE outline ADD COLUMN parent_id INTEGER REFERENCES outline(id)"))
session.exec(text("CREATE INDEX IF NOT EXISTS ix_outline_parent_id ON outline(parent_id)"))
session.commit()
def _seed_builtin_skills() -> None:
"""初始化预置 Skill仅首次运行时插入"""
from .models import Skill # 延迟导入避免循环
with Session(engine) as session:
existing = session.exec(select(Skill).where(Skill.is_builtin == True)).first() # noqa: E712
if existing:
return # 已初始化过
builtin_skills = [
Skill(
name="网文写作助手",
description="全能写作助手,可使用所有工具",
system_prompt=(
"你是一个专业的网文小说写作助手,擅长构思情节、塑造角色、打磨文笔。"
"请根据用户需求提供创作建议,必要时主动使用工具查看和操作小说数据。\n"
"- 书写章节前,应对先查看对应的大纲与前一章的内容\n"
"- 确保章节开头可以呼应上一张的钩子\n"
"- 确保在章节尾留下钩子"
),
allowed_tools=json.dumps([
"get_novel_info", "list_outlines", "get_outline_detail",
"create_outline", "update_outline", "delete_outline", "reorder_outlines",
"get_world_setting", "list_characters",
"list_chapters", "get_chapter_detail", "create_chapter", "update_chapter", "delete_chapter",
"list_files", "read_file",
]),
is_builtin=True,
),
Skill(
name="大纲规划师",
description="构建清晰的故事骨架,规划情节节点",
system_prompt=(
"你是大纲规划专家。请帮助用户构建清晰的故事骨架,包括起承转合、情节节点和分章结构。"
"分析现有大纲时请关注因果逻辑、节奏控制和悬念布置。在操作大纲时,注意保持节点间的逻辑连贯性。"
),
allowed_tools=json.dumps([
"get_novel_info", "list_outlines", "get_outline_detail",
"create_outline", "update_outline", "delete_outline", "reorder_outlines",
"get_world_setting", "list_characters", "get_character_detail",
"list_chapters", "get_chapter_detail",
"list_files", "read_file",
]),
is_builtin=True,
),
Skill(
name="角色塑造师",
description="设计立体、有深度的角色",
system_prompt=(
"你是角色塑造专家。请帮助用户设计立体、有深度的角色,包括外貌、性格、动机、成长弧线和人物关系。"
"注意角色间的化学反应和戏剧张力,确保每个角色都有独特的声音和行为逻辑。"
),
allowed_tools=json.dumps([
"get_novel_info", "list_characters", "get_character_detail",
"create_character", "update_character", "delete_character",
"list_outlines", "get_outline_detail", "get_world_setting",
"list_chapters", "get_chapter_detail", "list_files",
]),
is_builtin=True,
),
Skill(
name="世界观架构师",
description="构建完整自洽的虚构世界",
system_prompt=(
"你是世界观构建专家。请帮助用户设计完整自洽的虚构世界,包括地理、历史、政治、经济、魔法或科技体系等。"
"确保设定内部逻辑一致,并与故事主线相辅相成。"
),
allowed_tools=json.dumps([
"get_novel_info", "get_world_setting", "save_world_setting",
"list_outlines", "get_outline_detail",
"list_characters", "get_character_detail",
"list_files", "read_file",
]),
is_builtin=True,
),
Skill(
name="大纲拆解专家",
description="分析上传的小说或剧本文件,并拆解为大纲",
system_prompt=(
"你是文稿分析专家。请仔细阅读用户上传的文件,提取关键信息,分析文风、角色、情节结构,并提供改进建议。"
"分析时调用subagent分段读取大文件并做出分析注意把握整体脉络。\n"
"单次调用subagent最多查看原文的5000字并进行工具调用保存循环处理直到整个小说都拆解完成。\n"
"拆解完成后仅输出完成即可,不需要进行任何形式的总结。\n"
"在每次阅读完成后,必须调用工具将人物写入系统。\n"
"阅读过程中逐步完成以下内容拆解:\n- 世界观\n- 大纲\n"
"在每次阅读完成后,必须调用工具将世界观与大纲写入系统。\n"
"注意:\n"
"- 调用更新和创建新大纲时,必须先调用查看章节列表,确保没有重复内容\n"
"- 每次调用工具写入大纲后,记录当前进度\n"
"- 每次读取原文前,注意延续上下文中上一次的进度\n"
"- 拆解全本小说完成前不要停止"
),
allowed_tools=json.dumps([
"get_novel_info", "list_outlines", "get_outline_detail",
"create_outline", "update_outline", "delete_outline", "reorder_outlines",
"get_world_setting", "save_world_setting",
"list_characters", "get_character_detail",
"list_chapters", "get_chapter_detail",
"list_files", "read_file",
]),
is_builtin=True,
),
Skill(
name="人物拆解专家",
description="阅读小说原文,并将人物进行拆解",
system_prompt=(
"你是文稿分析专家。请仔细阅读用户上传的文件,提取关键信息,分析文风、角色、情节结构,并提供改进建议。"
"分析时调用subagent分段读取大文件并做出分析注意把握整体脉络。\n"
"单次调用subagent最多查看原文的5000字并进行工具调用保存循环处理直到整个小说都拆解完成。\n"
"阅读过程中逐步完成人物拆解。\n"
"拆解完成后仅输出完成即可,不需要进行任何形式的总结。\n"
"在每次阅读完成后,必须调用工具将人物写入系统。\n"
"注意:\n"
"- 不要漏拆人物\n"
"- 要对人物进行深刻的理解与描写\n"
"- 调用更新和创建人物时,必须先调用查看人物列表,确保没有重复,别名重拆\n"
"- 每次调用工具写入人物后,记录当前进度\n"
"- 每次读取原文前,注意延续上下文中上一次的进度\n"
"- 拆解全本小说完成前不要停止"
),
allowed_tools=json.dumps([
"get_novel_info", "list_characters", "get_character_detail",
"create_character", "update_character", "delete_character",
"list_outlines", "get_outline_detail", "get_world_setting",
"list_files", "read_file",
]),
is_builtin=True,
),
]
for skill in builtin_skills:
session.add(skill)
session.commit()
def get_session():
with Session(engine) as session:
yield session