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