├── .gitignore ├── LICENSE ├── README.md ├── app ├── __init__.py ├── core │ ├── __init__.py │ ├── context.py │ ├── generator.py │ └── summary.py ├── database │ ├── __init__.py │ └── sqlite.py ├── models │ ├── chapter.py │ ├── character.py │ └── novel.py └── ui │ ├── __init__.py │ ├── dialogs │ ├── character_editor.py │ ├── content_generator.py │ ├── database_manager_dialog.py │ └── summary_generator.py │ ├── widgets │ ├── chapter_list.py │ ├── character_list.py │ ├── editor.py │ └── outline_editor.py │ └── windows │ ├── __init__.py │ └── main_window.py ├── assistant_snippet_Hs4Wd2Aqxm.txt ├── main.py ├── requirements.txt └── test ├── test_chapter.py ├── test_character.py ├── test_core.py ├── test_models.py └── test_summary.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | build/ 8 | develop-eggs/ 9 | dist/ 10 | downloads/ 11 | eggs/ 12 | .eggs/ 13 | lib/ 14 | lib64/ 15 | parts/ 16 | sdist/ 17 | var/ 18 | wheels/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | 23 | # Virtual Environment 24 | venv/ 25 | ENV/ 26 | env/ 27 | 28 | # IDE 29 | .idea/ 30 | .vscode/ 31 | *.swp 32 | *.swo 33 | 34 | # Database 35 | *.db 36 | *.sqlite3 37 | 38 | # Environment variables 39 | .env 40 | 41 | # Logs 42 | *.log 43 | app.log 44 | 45 | # OS 46 | .DS_Store 47 | Thumbs.db -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Liufenyi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AI 小说生成器 (AI Novel Writer) 2 | 3 | 基于 Gemini API 开发的智能小说创作助手,支持文风模仿、智能写作和长篇小说创作。这是一个面向小说创作者的智能写作工具,添加了数据库进行长文记录。它能帮助作者更高效地进行创作,同时保持文章的连贯性和人物的一致性。 4 | 5 | ## 项目特点 6 | 7 | - 🤖 **AI 驱动**:基于 Google Gemini API,提供智能写作建议和内容生成 8 | - 📝 **专业编辑器**:集成现代化的文本编辑器,支持实时保存和版本控制 9 | - 👥 **角色管理**:智能角色提取和关系管理,自动维护角色信息的一致性 10 | - 📊 **数据可视化**:直观的数据管理界面,支持表格化展示和关系图可视化 11 | - 🔄 **版本控制**:内置版本控制系统,支持内容回滚和历史追踪 12 | - 🎯 **场景规划**:支持大纲创作和章节规划,帮助把控整体剧情走向 13 | - 💾 **数据安全**:本地数据存储,支持数据备份和恢复 14 | 15 | ## 技术栈 16 | 17 | - **前端框架**:PyQt6 18 | - **数据存储**:SQLite 19 | - **AI 模型**:Google Gemini API 20 | - **开发语言**:Python 3.8+ 21 | 22 | ## 快速开始 23 | 24 | ### 环境要求 25 | 26 | - Python 3.8 或更高版本 27 | - pip 包管理器 28 | - Google Gemini API 密钥 29 | 30 | ### 安装步骤 31 | 32 | 1. 克隆项目到本地: 33 | ```bash 34 | git clone https://github.com/yourusername/ai-novel-writer.git 35 | cd ai-novel-writer 36 | ``` 37 | 38 | 2. 安装依赖: 39 | ```bash 40 | pip install -r requirements.txt 41 | ``` 42 | 43 | 3. 配置环境变量: 44 | 创建 `.env` 文件并添加以下内容: 45 | ``` 46 | GEMINI_API_KEY=your_api_key_here 47 | ``` 48 | 49 | 4. 运行应用: 50 | ```bash 51 | python main.py 52 | ``` 53 | 54 | ## 功能特点 55 | 56 | ### 已实现功能 57 | - [x] 基础编辑器功能 58 | - 创建和管理小说 59 | - 章节管理(添加、删除、切换章节) 60 | - 自动保存(每30秒) 61 | - 内容修改状态跟踪 62 | - 版本历史记录 63 | 64 | - [x] AI 内容生成 65 | - 基于 Gemini API 的智能内容生成 66 | - 可自定义提示词 67 | - 内容预览和确认 68 | - 支持生成 2000 字的内容 69 | - 智能文风模仿 70 | - 上下文关联分析 71 | 72 | - [x] 数据管理 73 | - SQLite 数据库存储 74 | - 小说基本信息管理 75 | - 章节内容管理 76 | - 版本历史记录 77 | - 数据库管理界面 78 | - 表格化展示和编辑数据 79 | - 角色关系可视化 80 | - 预定义关系类型管理 81 | - 数据备份和恢复 82 | 83 | - [x] 大纲管理 84 | - 小说整体大纲 85 | - 章节大纲 86 | - 大纲智能生成 87 | - 情节线索追踪 88 | - 冲突设置建议 89 | 90 | - [x] 角色管理 91 | - 角色信息管理 92 | - 角色关系图 93 | - 角色特征追踪 94 | - 角色成长曲线 95 | - 性格特征分析 96 | 97 | - [x] 智能摘要系统 98 | - 章节自动摘要 99 | - 情节发展追踪 100 | - 上下文关联分析 101 | - 关键事件提取 102 | - 伏笔追踪系统 103 | 104 | ### 计划实现功能 105 | - [ ] 实现多模型生成 106 | 107 | 108 | 109 | - [ ] 写作辅助功能 110 | - 智能写作建议 111 | - 情节连贯性检查 112 | - 人物性格一致性检查 113 | - 文风分析与建议 114 | - 写作质量评估 115 | 116 | ## 使用方法 117 | 118 | ### 基本界面 119 | - 左侧面板:写作设置和角色管理 120 | - 中央区域:文本编辑 121 | - 右侧面板:大纲和摘要 122 | - 数据库管理:工具菜单 -> 数据库管理 123 | 124 | ### 常用操作 125 | - 新建文档:文件 -> 新建 126 | - 保存文档:文件 -> 保存 127 | - 生成内容:编辑 -> 生成 128 | - 查看历史:视图 -> 版本历史 129 | - 管理数据:工具 -> 数据库管理 130 | 131 | 132 | ## 开发说明 133 | 134 | ### 项目结构 135 | ``` 136 | novel-writer/ 137 | ├── app/ # 核心功能模块 138 | │ ├── core/ # 核心功能 139 | │ │ ├── generator.py # AI 生成器 140 | │ │ ├── context.py # 上下文管理 141 | │ │ └── summary.py # 摘要系统 142 | │ ├── models/ # 数据模型 143 | │ │ ├── novel.py # 小说模型 144 | │ │ ├── chapter.py # 章节模型 145 | │ │ └── character.py # 角色模型 146 | │ ├── database/ # 数据库管理 147 | │ │ ├── sqlite.py # SQLite 管理器 148 | │ │ └── queries.py # SQL 查询 149 | │ └── utils/ # 工具函数 150 | ├── ui/ # Qt界面模块 151 | │ ├── windows/ # 窗口类 152 | │ ├── widgets/ # 自定义组件 153 | │ └── resources/ # UI资源文件 154 | └── tests/ # 测试用例 155 | ``` 156 | 157 | ### 数据库结构 158 | 159 | #### 小说表 (novels) 160 | ```sql 161 | CREATE TABLE novels ( 162 | id INTEGER PRIMARY KEY AUTOINCREMENT, 163 | title TEXT NOT NULL, 164 | author TEXT, 165 | description TEXT, 166 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 167 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 168 | ); 169 | ``` 170 | 171 | #### 章节表 (chapters) 172 | ```sql 173 | CREATE TABLE chapters ( 174 | id INTEGER PRIMARY KEY AUTOINCREMENT, 175 | novel_id INTEGER NOT NULL, 176 | chapter_number INTEGER NOT NULL, 177 | title TEXT NOT NULL, 178 | content TEXT, 179 | summary TEXT, 180 | outline TEXT, 181 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 182 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 183 | FOREIGN KEY (novel_id) REFERENCES novels(id) ON DELETE CASCADE 184 | ); 185 | ``` 186 | 187 | #### 角色表 (characters) 188 | ```sql 189 | CREATE TABLE characters ( 190 | id INTEGER PRIMARY KEY AUTOINCREMENT, 191 | novel_id INTEGER NOT NULL, 192 | name TEXT NOT NULL, 193 | description TEXT, 194 | characteristics TEXT, 195 | role_type TEXT, 196 | first_appearance INTEGER, 197 | status TEXT DEFAULT '活跃', 198 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 199 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 200 | FOREIGN KEY (novel_id) REFERENCES novels(id) ON DELETE CASCADE, 201 | FOREIGN KEY (first_appearance) REFERENCES chapters(id) ON DELETE SET NULL 202 | ); 203 | ``` 204 | 205 | #### 角色关系表 (character_relationships) 206 | ```sql 207 | CREATE TABLE character_relationships ( 208 | id INTEGER PRIMARY KEY AUTOINCREMENT, 209 | novel_id INTEGER NOT NULL, 210 | character1_id INTEGER NOT NULL, 211 | character2_id INTEGER NOT NULL, 212 | relationship_type TEXT NOT NULL, 213 | description TEXT, 214 | start_chapter INTEGER, 215 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 216 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 217 | FOREIGN KEY (novel_id) REFERENCES novels(id) ON DELETE CASCADE, 218 | FOREIGN KEY (character1_id) REFERENCES characters(id) ON DELETE CASCADE, 219 | FOREIGN KEY (character2_id) REFERENCES characters(id) ON DELETE CASCADE, 220 | FOREIGN KEY (start_chapter) REFERENCES chapters(id) ON DELETE SET NULL 221 | ); 222 | ``` 223 | 224 | #### 关系类型表 (relationship_types) 225 | ```sql 226 | CREATE TABLE relationship_types ( 227 | id INTEGER PRIMARY KEY AUTOINCREMENT, 228 | category TEXT NOT NULL, 229 | type TEXT NOT NULL, 230 | description TEXT, 231 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 232 | ); 233 | ``` 234 | 235 | #### 章节版本表 (chapter_versions) 236 | ```sql 237 | CREATE TABLE chapter_versions ( 238 | id INTEGER PRIMARY KEY AUTOINCREMENT, 239 | chapter_id INTEGER, 240 | content TEXT, 241 | comment TEXT, 242 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 243 | FOREIGN KEY (chapter_id) REFERENCES chapters(id) 244 | ); 245 | ``` 246 | 247 | ### 模型功能说明 248 | 249 | #### Novel 模型 250 | - 小说基本信息管理 251 | - 章节管理 252 | - 版本控制 253 | - 导出功能 254 | - 数据备份 255 | 256 | #### Chapter 模型 257 | - 章节内容管理 258 | - 版本历史 259 | - 自动摘要生成 260 | - 大纲关联 261 | - 角色追踪 262 | 263 | #### Character 模型 264 | - 角色信息管理 265 | - 角色关系管理 266 | - 角色发展追踪 267 | - 状态管理 268 | - 自动角色提取 269 | - 关系类型预定义 270 | - 角色图谱生成 271 | 272 | ## 贡献指南 273 | 274 | 我们欢迎所有形式的贡献,包括但不限于: 275 | 276 | - 提交问题和建议 277 | - 改进文档 278 | - 修复 bug 279 | - 添加新功能 280 | - 优化代码 281 | 282 | ### 贡献步骤 283 | 284 | 1. Fork 项目 285 | 2. 创建特性分支 (`git checkout -b feature/AmazingFeature`) 286 | 3. 提交改动 (`git commit -m 'Add some AmazingFeature'`) 287 | 4. 推送到分支 (`git push origin feature/AmazingFeature`) 288 | 5. 创建 Pull Request 289 | 290 | ## 开源协议 291 | 292 | 本项目采用 MIT 协议开源,详见 [LICENSE](LICENSE) 文件。 293 | 294 | ## 联系方式 295 | 296 | - 项目作者:[Liufenyi] 297 | - 邮箱:[liufy696@gmail.com] 298 | - GitHub:[LIU666-sketch] 299 | 300 | ## 致谢 301 | 302 | 感谢所有为这个项目做出贡献的开发者们! 303 | 304 | - Google Gemini API 团队 305 | - PyQt 开发团队 306 | - 所有项目贡献者 307 | ``` 308 | 309 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | AI 小说生成器核心包 3 | """ -------------------------------------------------------------------------------- /app/core/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 核心功能模块 3 | """ -------------------------------------------------------------------------------- /app/core/context.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any, List, Optional 2 | from ..database.sqlite import DatabaseManager 3 | 4 | class ContextManager: 5 | def __init__(self, db_manager: DatabaseManager): 6 | """初始化上下文管理器 7 | 8 | Args: 9 | db_manager: 数据库管理器实例 10 | """ 11 | self.db = db_manager 12 | 13 | def get_novel_context(self, novel_id: int) -> Dict[str, Any]: 14 | """获取小说的完整上下文 15 | 16 | Args: 17 | novel_id: 小说ID 18 | 19 | Returns: 20 | 包含小说信息的上下文字典 21 | """ 22 | context = {} 23 | 24 | # 获取小说基本信息 25 | novel = self._get_novel_info(novel_id) 26 | if novel: 27 | context["outline"] = novel[2] # outline 28 | context["current_chapter"] = novel[3] # current_chapter 29 | 30 | # 获取上一章摘要 31 | if context.get("current_chapter", 0) > 1: 32 | previous_chapter = self._get_chapter_summary( 33 | novel_id, 34 | context["current_chapter"] - 1 35 | ) 36 | if previous_chapter: 37 | context["previous_summary"] = previous_chapter 38 | 39 | # 获取角色信息 40 | characters = self._get_characters(novel_id) 41 | if characters: 42 | context["characters"] = characters 43 | 44 | return context 45 | 46 | def _get_novel_info(self, novel_id: int) -> Optional[tuple]: 47 | """获取小说基本信息""" 48 | query = "SELECT * FROM novels WHERE id = ?" 49 | result = self.db.execute_query(query, (novel_id,)) 50 | return result[0] if result else None 51 | 52 | def _get_chapter_summary(self, novel_id: int, chapter_number: int) -> Optional[str]: 53 | """获取指定章节的摘要""" 54 | query = """ 55 | SELECT summary 56 | FROM chapters 57 | WHERE novel_id = ? AND chapter_number = ? 58 | """ 59 | result = self.db.execute_query(query, (novel_id, chapter_number)) 60 | return result[0][0] if result else None 61 | 62 | def _get_characters(self, novel_id: int) -> List[Dict[str, str]]: 63 | """获取小说中的所有角色信息""" 64 | query = "SELECT name, description, characteristics FROM characters WHERE novel_id = ?" 65 | result = self.db.execute_query(query, (novel_id,)) 66 | 67 | characters = [] 68 | if result: 69 | for row in result: 70 | characters.append({ 71 | "name": row[0], 72 | "description": row[1], 73 | "characteristics": row[2] 74 | }) 75 | return characters 76 | 77 | def update_chapter_summary(self, novel_id: int, chapter_number: int, summary: str): 78 | """更新章节摘要 79 | 80 | Args: 81 | novel_id: 小说ID 82 | chapter_number: 章节号 83 | summary: 摘要内容 84 | """ 85 | query = """ 86 | UPDATE chapters 87 | SET summary = ? 88 | WHERE novel_id = ? AND chapter_number = ? 89 | """ 90 | self.db.execute_query(query, (summary, novel_id, chapter_number)) 91 | 92 | def add_character(self, novel_id: int, name: str, description: str, 93 | characteristics: Optional[str] = None): 94 | """添加新角色 95 | 96 | Args: 97 | novel_id: 小说ID 98 | name: 角色名称 99 | description: 角色描述 100 | characteristics: 角色特征 101 | """ 102 | query = """ 103 | INSERT INTO characters (novel_id, name, description, characteristics) 104 | VALUES (?, ?, ?, ?) 105 | """ 106 | self.db.execute_query(query, (novel_id, name, description, characteristics)) 107 | 108 | def update_novel_progress(self, novel_id: int, current_chapter: int): 109 | """更新小说进度 110 | 111 | Args: 112 | novel_id: 小说ID 113 | current_chapter: 当前章节号 114 | """ 115 | query = "UPDATE novels SET current_chapter = ? WHERE id = ?" 116 | self.db.execute_query(query, (current_chapter, novel_id)) -------------------------------------------------------------------------------- /app/core/generator.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Optional, Dict, Any 3 | from google import genai 4 | from dotenv import load_dotenv 5 | import logging 6 | 7 | class NovelGenerator: 8 | def __init__(self): 9 | """初始化小说生成器""" 10 | load_dotenv() 11 | api_key = os.getenv("GEMINI_API_KEY") 12 | if not api_key: 13 | raise ValueError("未找到 GEMINI_API_KEY 环境变量") 14 | 15 | self.client = genai.Client(api_key=api_key) 16 | self.model = 'gemini-2.0-flash-exp' 17 | 18 | def generate_content(self, prompt: str, context: Optional[Dict[str, Any]] = None) -> str: 19 | """生成内容 20 | 21 | Args: 22 | prompt: 用户提示词 23 | context: 上下文信息,包含: 24 | - novel_outline: 小说大纲 25 | - current_chapter: 当前章节信息 26 | - previous_summaries: 之前章节的摘要列表 27 | - characters: 角色列表 28 | 29 | Returns: 30 | 生成的内容 31 | """ 32 | try: 33 | logging.info("开始生成内容...") 34 | logging.info(f"原始提示词: {prompt}") 35 | 36 | # 构建完整提示词 37 | base_prompt = """ 38 | 你是一个专业的小说创作助手。请基于以下信息创作故事情节: 39 | 40 | 1. 写作要求: 41 | - 保持情节连贯性和人物性格一致性 42 | - 细腻的描写和自然的对话 43 | - 符合小说整体风格和主题 44 | 45 | 2. 标记要求: 46 | - 新角色首次出场用【角色名:性格特征、外貌特征、身份背景】 47 | - 已有角色出场用【角色名】 48 | - 重要情节转折用《情节》标记 49 | 50 | 3. 上下文信息: 51 | """ 52 | 53 | full_prompt = self._build_prompt(base_prompt + "\n" + prompt, context) 54 | logging.info(f"完整提示词: {full_prompt}") 55 | 56 | # 调用 API 生成内容 57 | response = self.client.models.generate_content( 58 | model=self.model, 59 | contents=full_prompt 60 | ) 61 | 62 | generated_content = response.text 63 | logging.info(f"内容生成成功,长度: {len(generated_content)}") 64 | 65 | return generated_content 66 | 67 | except Exception as e: 68 | logging.error(f"内容生成失败: {str(e)}") 69 | raise 70 | 71 | def _build_prompt(self, prompt: str, context: Optional[Dict[str, Any]] = None) -> str: 72 | """构建完整的提示词 73 | 74 | Args: 75 | prompt: 基础提示词 76 | context: 上下文信息 77 | 78 | Returns: 79 | 完整的提示词 80 | """ 81 | if not context: 82 | logging.warning("没有提供上下文信息") 83 | return prompt 84 | 85 | # 构建上下文提示词 86 | context_prompt = [] 87 | 88 | # 添加小说大纲 89 | if "novel_outline" in context: 90 | context_prompt.append(f"小说大纲:\n{context['novel_outline']}") 91 | logging.info("已添加小说大纲到上下文") 92 | 93 | # 添加当前章节信息 94 | if "current_chapter" in context: 95 | chapter = context["current_chapter"] 96 | context_prompt.append( 97 | f"当前位置:第{chapter['chapter_number']}章 {chapter['title']}\n" 98 | f"章节大纲:\n{chapter.get('outline', '暂无大纲')}" 99 | ) 100 | logging.info(f"已添加当前章节信息:第{chapter['chapter_number']}章") 101 | 102 | # 添加之前章节的摘要 103 | if "previous_summaries" in context: 104 | summaries = context["previous_summaries"] 105 | if summaries: 106 | # 按章节号排序 107 | sorted_summaries = sorted(summaries, key=lambda x: x['chapter_number']) 108 | context_prompt.append("之前章节摘要:") 109 | for chapter_summary in sorted_summaries: 110 | context_prompt.append( 111 | f"第{chapter_summary['chapter_number']}章:{chapter_summary['summary']}" 112 | ) 113 | logging.info(f"已添加{len(summaries)}个之前章节的摘要") 114 | 115 | # 添加角色信息 116 | if "characters" in context: 117 | characters = context["characters"] 118 | if characters: 119 | context_prompt.append("已有角色:") 120 | for char in characters: 121 | desc = f"- {char['name']}: {char['description']}" 122 | if "characteristics" in char: 123 | desc += f" ({char['characteristics']})" 124 | context_prompt.append(desc) 125 | logging.info(f"已添加{len(characters)}个角色信息") 126 | 127 | # 组合所有提示词 128 | full_prompt = "\n\n".join(context_prompt + [prompt]) 129 | return full_prompt 130 | 131 | def generate_summary(self, content: str) -> str: 132 | """生成内容摘要 133 | 134 | Args: 135 | content: 需要总结的内容 136 | 137 | Returns: 138 | 生成的摘要 139 | """ 140 | prompt = f"""请以下内容生成一个简洁的摘要,突出关键情节: 141 | 142 | {content} 143 | 144 | 摘要:""" 145 | 146 | try: 147 | response = self.client.models.generate_content( 148 | model=self.model, 149 | contents=prompt 150 | ) 151 | return response.text 152 | except Exception as e: 153 | logging.error(f"摘要生成失败: {e}") 154 | raise 155 | 156 | def generate_outline(self, novel_title: str = None, chapter_content: str = None, is_chapter: bool = False) -> str: 157 | """生成大纲 158 | 159 | Args: 160 | novel_title: 小说标题(生成小说大纲时使用) 161 | chapter_content: 章节内容(生成章节大纲时使用) 162 | is_chapter: 是否为章节大纲 163 | 164 | Returns: 165 | 生成的大纲内容 166 | """ 167 | try: 168 | if is_chapter and not chapter_content: 169 | raise ValueError("生成章节大纲需要提供章节内容") 170 | 171 | if not is_chapter and not novel_title: 172 | raise ValueError("生成小说大纲需要提供小说标题") 173 | 174 | if is_chapter: 175 | prompt = f""" 176 | 请根据以下章节内容生成一个详细的章节大纲。大纲应包含: 177 | 1. 本章的主要内容和目标 178 | 2. 关键场景和对话 179 | 3. 与整体故事的关联 180 | 4. 需要重点描写的细节 181 | 182 | 章节内容: 183 | {chapter_content} 184 | """ 185 | else: 186 | prompt = f""" 187 | 请为小说《{novel_title}》生成一个详细的整体大纲。大纲应包含: 188 | 1. 小说的整体故事架构 189 | 2. 主要人物及其发展轨迹 190 | 3. 重要的情节转折点 191 | 4. 故事的主题和中心思想 192 | """ 193 | 194 | response = self.client.models.generate_content( 195 | model=self.model, 196 | contents=prompt 197 | ) 198 | return response.text 199 | 200 | except Exception as e: 201 | logging.error(f"生成大纲失败: {str(e)}") 202 | raise 203 | 204 | def extract_characters(self, content: str) -> list: 205 | """从内容中提取角色信息""" 206 | try: 207 | prompt = f""" 208 | 请分析以下故事中用【】标记的角色信息,并将其转换为严格的JSON格式。 209 | 210 | 要求: 211 | 1. 必须返回一个JSON数组 212 | 2. 每个角色必须包含以下字段: 213 | - name (字符串): 角色名称 214 | - description (字符串): 性格和外貌特征 215 | - characteristics (字符串): 身份背景 216 | - role_type (字符串): 必须是 "主角"、"配角" 或 "反派" 之一 217 | 218 | 返回格式必须严格遵循以下示例: 219 | [ 220 | {{ 221 | "name": "李明", 222 | "description": "性格开朗、正直,身材高大", 223 | "characteristics": "刚毕业的大学生", 224 | "role_type": "主角" 225 | }}, 226 | {{ 227 | "name": "王婆", 228 | "description": "头发花白、和蔼可亲", 229 | "characteristics": "退休老人", 230 | "role_type": "配角" 231 | }} 232 | ] 233 | 234 | 注意: 235 | 1. 返回的必须是可以直接解析的JSON格式 236 | 2. 不要添加任何额外的说明文字 237 | 3. 确保所有引号和逗号使用正确 238 | 239 | 故事内容: 240 | {content} 241 | 242 | 仅返回JSON数组: 243 | """ 244 | 245 | response = self.client.models.generate_content( 246 | model=self.model, 247 | contents=prompt 248 | ) 249 | 250 | # 清理响应文本,只保留JSON部分 251 | response_text = response.text.strip() 252 | if response_text.startswith('```json'): 253 | response_text = response_text[7:] 254 | if response_text.endswith('```'): 255 | response_text = response_text[:-3] 256 | response_text = response_text.strip() 257 | 258 | # 解析JSON响应 259 | import json 260 | try: 261 | characters = json.loads(response_text) 262 | if not isinstance(characters, list): 263 | logging.error("AI返回的不是JSON数组") 264 | return [] 265 | 266 | # 验证每个角色的字段 267 | valid_characters = [] 268 | for char in characters: 269 | if all(key in char for key in ['name', 'description', 'characteristics', 'role_type']): 270 | valid_characters.append(char) 271 | logging.info(f"成功提取角色: {char['name']} ({char['role_type']})") 272 | else: 273 | logging.warning(f"角色信息不完整: {char}") 274 | 275 | if valid_characters: 276 | logging.info(f"共提取到 {len(valid_characters)} 个角色") 277 | else: 278 | logging.warning("未提取到任何有效角色") 279 | 280 | return valid_characters 281 | 282 | except json.JSONDecodeError as e: 283 | logging.error(f"JSON解析失败: {str(e)}\n响应内容: {response_text}") 284 | return [] 285 | 286 | except Exception as e: 287 | logging.error(f"提取角色信息失败: {str(e)}") 288 | return [] 289 | 290 | if __name__ == "__main__": 291 | # 设置日志 292 | logging.basicConfig(level=logging.INFO) 293 | 294 | # 测试生成器 295 | generator = NovelGenerator() 296 | 297 | # 测试上下文生成 298 | context = { 299 | "outline": "这是一个关于冒险的故事", 300 | "current_chapter": "第一章", 301 | "characters": [ 302 | { 303 | "name": "张三", 304 | "description": "主角,勇敢的冒险家", 305 | "characteristics": "勇敢,正直" 306 | } 307 | ] 308 | } 309 | 310 | result = generator.generate_content("请继续写一个精彩的情节", context) 311 | print("生成结果:", result) -------------------------------------------------------------------------------- /app/core/summary.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict, Any, Optional 2 | from .generator import NovelGenerator 3 | from ..database.sqlite import DatabaseManager 4 | import logging 5 | 6 | class SummarySystem: 7 | def __init__(self, db_manager: DatabaseManager, generator: NovelGenerator): 8 | """初始化摘要系统 9 | 10 | Args: 11 | db_manager: 数据库管理器实例 12 | generator: AI生成器实例 13 | """ 14 | self.db = db_manager 15 | self.generator = generator 16 | 17 | def generate_chapter_summary(self, content: str) -> str: 18 | """生成章节摘要 19 | 20 | Args: 21 | content: 章节内容 22 | 23 | Returns: 24 | 章节摘要 25 | """ 26 | return self.generator.generate_summary(content) 27 | 28 | def update_novel_outline(self, novel_id: int) -> str: 29 | """更新小说大纲 30 | 31 | Args: 32 | novel_id: 小说ID 33 | 34 | Returns: 35 | 更新后的大纲 36 | """ 37 | try: 38 | # 获取所有章节摘要 39 | summaries = self._get_all_chapter_summaries(novel_id) 40 | if not summaries: 41 | return "" 42 | 43 | # 构建提示词 44 | prompt = self._build_outline_prompt(summaries) 45 | 46 | # 生成新大纲 47 | new_outline = self.generator.generate_content(prompt) 48 | 49 | # 更新数据库 50 | self._update_outline_in_db(novel_id, new_outline) 51 | 52 | return new_outline 53 | 54 | except Exception as e: 55 | logging.error(f"更新大纲失败: {e}") 56 | raise 57 | 58 | def extract_key_points(self, chapter_id: int) -> List[str]: 59 | """提取章节关键情节点 60 | 61 | Args: 62 | chapter_id: 章节ID 63 | 64 | Returns: 65 | 关键情节点列表 66 | """ 67 | try: 68 | # 获取章节内容 69 | content = self._get_chapter_content(chapter_id) 70 | if not content: 71 | return [] 72 | 73 | prompt = f"""请从以下内容中提取3-5个关键情节点,每个情节点用一句话描述: 74 | 75 | {content} 76 | 77 | 关键情节:""" 78 | 79 | result = self.generator.generate_content(prompt) 80 | # 将结果按行分割并清理 81 | key_points = [point.strip() for point in result.split('\n') if point.strip()] 82 | return key_points 83 | 84 | except Exception as e: 85 | logging.error(f"提取关键情节点失败: {e}") 86 | raise 87 | 88 | def _get_all_chapter_summaries(self, novel_id: int) -> List[Dict[str, Any]]: 89 | """获取小说所有章节的摘要""" 90 | query = """ 91 | SELECT chapter_number, summary 92 | FROM chapters 93 | WHERE novel_id = ? 94 | ORDER BY chapter_number 95 | """ 96 | result = self.db.execute_query(query, (novel_id,)) 97 | return [{"chapter": row[0], "summary": row[1]} for row in result] if result else [] 98 | 99 | def _build_outline_prompt(self, summaries: List[Dict[str, Any]]) -> str: 100 | """构建更新大纲的提示词""" 101 | summary_text = "\n".join([ 102 | f"第{s['chapter']}章:{s['summary']}" 103 | for s in summaries 104 | ]) 105 | 106 | return f"""基于以下各章节摘要,生成一个完整的小说大纲,需要: 107 | 1. 突出主要情节发展 108 | 2. 体现人物关系变化 109 | 3. 注意情节的连贯性 110 | 111 | 章节摘要: 112 | {summary_text} 113 | 114 | 请生成大纲:""" 115 | 116 | def _update_outline_in_db(self, novel_id: int, outline: str): 117 | """更新数据库中的小说大纲""" 118 | query = "UPDATE novels SET outline = ? WHERE id = ?" 119 | self.db.execute_query(query, (outline, novel_id)) 120 | 121 | def _get_chapter_content(self, chapter_id: int) -> Optional[str]: 122 | """获取章节内容""" 123 | query = "SELECT content FROM chapters WHERE id = ?" 124 | result = self.db.execute_query(query, (chapter_id,)) 125 | return result[0][0] if result else None -------------------------------------------------------------------------------- /app/database/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/database/sqlite.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import logging 3 | from typing import List, Dict, Any, Optional, Tuple 4 | from datetime import datetime 5 | 6 | # 配置日志 7 | logger = logging.getLogger(__name__) 8 | 9 | class DatabaseManager: 10 | def __init__(self, db_path: str): 11 | """初始化数据库管理器 12 | 13 | Args: 14 | db_path: 数据库文件路径 15 | """ 16 | self.db_path = db_path 17 | logger.info(f"数据库管理器初始化: {db_path}") 18 | 19 | def get_connection(self) -> sqlite3.Connection: 20 | """获取数据库连接""" 21 | try: 22 | conn = sqlite3.connect(self.db_path) 23 | conn.row_factory = sqlite3.Row 24 | return conn 25 | except Exception as e: 26 | logger.error(f"数据库连接失败: {e}") 27 | raise 28 | 29 | def init_database(self): 30 | """初始化数据库表结构""" 31 | try: 32 | logger.info("开始初始化数据库...") 33 | conn = self.get_connection() 34 | cursor = conn.cursor() 35 | 36 | # 创建小说表 37 | cursor.execute(""" 38 | CREATE TABLE IF NOT EXISTS novels ( 39 | id INTEGER PRIMARY KEY AUTOINCREMENT, 40 | title TEXT NOT NULL, 41 | author TEXT, 42 | description TEXT, 43 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 44 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 45 | ) 46 | """) 47 | 48 | # 创建章节表 49 | cursor.execute(""" 50 | CREATE TABLE IF NOT EXISTS chapters ( 51 | id INTEGER PRIMARY KEY AUTOINCREMENT, 52 | novel_id INTEGER NOT NULL, 53 | chapter_number INTEGER NOT NULL, 54 | title TEXT NOT NULL, 55 | content TEXT, 56 | summary TEXT, 57 | outline TEXT, 58 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 59 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 60 | FOREIGN KEY (novel_id) REFERENCES novels(id) ON DELETE CASCADE 61 | ) 62 | """) 63 | 64 | # 创建角色表 65 | cursor.execute(""" 66 | CREATE TABLE IF NOT EXISTS characters ( 67 | id INTEGER PRIMARY KEY AUTOINCREMENT, 68 | novel_id INTEGER NOT NULL, 69 | name TEXT NOT NULL, 70 | description TEXT, 71 | characteristics TEXT, 72 | role_type TEXT, 73 | first_appearance INTEGER, 74 | status TEXT DEFAULT '活跃', 75 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 76 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 77 | FOREIGN KEY (novel_id) REFERENCES novels(id) ON DELETE CASCADE, 78 | FOREIGN KEY (first_appearance) REFERENCES chapters(id) ON DELETE SET NULL 79 | ) 80 | """) 81 | 82 | # 创建角色关系表 83 | cursor.execute(""" 84 | CREATE TABLE IF NOT EXISTS character_relationships ( 85 | id INTEGER PRIMARY KEY AUTOINCREMENT, 86 | novel_id INTEGER NOT NULL, 87 | character1_id INTEGER NOT NULL, 88 | character2_id INTEGER NOT NULL, 89 | relationship_type TEXT NOT NULL, 90 | description TEXT, 91 | start_chapter INTEGER, 92 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 93 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 94 | FOREIGN KEY (novel_id) REFERENCES novels(id) ON DELETE CASCADE, 95 | FOREIGN KEY (character1_id) REFERENCES characters(id) ON DELETE CASCADE, 96 | FOREIGN KEY (character2_id) REFERENCES characters(id) ON DELETE CASCADE, 97 | FOREIGN KEY (start_chapter) REFERENCES chapters(id) ON DELETE SET NULL 98 | ) 99 | """) 100 | 101 | # 创建关系类型表 102 | cursor.execute(""" 103 | CREATE TABLE IF NOT EXISTS relationship_types ( 104 | id INTEGER PRIMARY KEY AUTOINCREMENT, 105 | category TEXT NOT NULL, 106 | type TEXT NOT NULL, 107 | description TEXT, 108 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 109 | ) 110 | """) 111 | 112 | # 插入预定义的关系类型 113 | predefined_types = [ 114 | ('家族', '父子', '父亲与儿子的关系'), 115 | ('家族', '母子', '母亲与儿子的关系'), 116 | ('家族', '父女', '父亲与女儿的关系'), 117 | ('家族', '母女', '母亲与女儿的关系'), 118 | ('家族', '兄弟', '兄弟关系'), 119 | ('家族', '姐妹', '姐妹关系'), 120 | ('家族', '夫妻', '已婚夫妻关系'), 121 | ('家族', '恋人', '恋爱关系'), 122 | ('社会', '朋友', '朋友关系'), 123 | ('社会', '敌人', '敌对关系'), 124 | ('社会', '竞争', '竞争关系'), 125 | ('社会', '合作', '合作关系'), 126 | ('师徒', '师傅', '师傅对徒弟的关系'), 127 | ('师徒', '徒弟', '徒弟对师傅的关系'), 128 | ('职场', '上级', '工作中的上级关系'), 129 | ('职场', '下级', '工作中的下级关系'), 130 | ('职场', '同事', '工作中的平级关系') 131 | ] 132 | 133 | cursor.executemany(""" 134 | INSERT OR IGNORE INTO relationship_types (category, type, description) 135 | VALUES (?, ?, ?) 136 | """, predefined_types) 137 | 138 | conn.commit() 139 | logger.info("数据库初始化成功") 140 | 141 | except Exception as e: 142 | logger.error(f"数据库初始化失败: {e}") 143 | raise 144 | finally: 145 | conn.close() 146 | 147 | def execute_query(self, query: str, params: tuple = ()) -> List[tuple]: 148 | """执行SQL查询 149 | 150 | Args: 151 | query: SQL查询语句 152 | params: 查询参数 153 | 154 | Returns: 155 | 查询结果列表 156 | """ 157 | try: 158 | conn = self.get_connection() 159 | cursor = conn.cursor() 160 | cursor.execute(query, params) 161 | 162 | if query.strip().upper().startswith(('INSERT', 'UPDATE', 'DELETE')): 163 | conn.commit() 164 | result = cursor.lastrowid if cursor.lastrowid else True 165 | else: 166 | result = cursor.fetchall() 167 | 168 | return result 169 | 170 | except Exception as e: 171 | logger.error(f"SQL执行失败: {query} - {e}") 172 | raise 173 | finally: 174 | conn.close() 175 | 176 | def get_table_structure(self, table_name: str) -> List[str]: 177 | """获取表结构 178 | 179 | Args: 180 | table_name: 表名 181 | 182 | Returns: 183 | 字段名列表 184 | """ 185 | try: 186 | conn = self.get_connection() 187 | cursor = conn.cursor() 188 | cursor.execute(f"PRAGMA table_info({table_name})") 189 | columns = [row[1] for row in cursor.fetchall()] 190 | logger.info(f"获取表结构成功: {table_name}") 191 | return columns 192 | 193 | except Exception as e: 194 | logger.error(f"获取表结构失败: {table_name} - {e}") 195 | raise 196 | finally: 197 | conn.close() 198 | 199 | def insert_record(self, table_name: str, data: Dict[str, Any]) -> int: 200 | """插入记录 201 | 202 | Args: 203 | table_name: 表名 204 | data: 要插入的数据字典 205 | 206 | Returns: 207 | 新记录的ID 208 | """ 209 | try: 210 | # 过滤掉值为None的字段 211 | filtered_data = {k: v for k, v in data.items() if v is not None} 212 | 213 | # 构建SQL语句 214 | columns = ', '.join(filtered_data.keys()) 215 | placeholders = ', '.join(['?' for _ in filtered_data]) 216 | query = f"INSERT INTO {table_name} ({columns}) VALUES ({placeholders})" 217 | 218 | # 执行插入 219 | result = self.execute_query(query, tuple(filtered_data.values())) 220 | logger.info(f"插入记录成功: {table_name}") 221 | return result 222 | 223 | except Exception as e: 224 | logger.error(f"插入记录失败: {table_name} - {e}") 225 | raise 226 | 227 | def update_record(self, table_name: str, record_id: int, data: Dict[str, Any]) -> bool: 228 | """更新记录 229 | 230 | Args: 231 | table_name: 表名 232 | record_id: 记录ID 233 | data: 要更新的数据字典 234 | 235 | Returns: 236 | 是否更新成功 237 | """ 238 | try: 239 | # 过滤掉值为None的字段 240 | filtered_data = {k: v for k, v in data.items() if v is not None} 241 | 242 | # 添加更新时间 243 | filtered_data['updated_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') 244 | 245 | # 构建SQL语句 246 | set_clause = ', '.join([f"{k} = ?" for k in filtered_data.keys()]) 247 | query = f"UPDATE {table_name} SET {set_clause} WHERE id = ?" 248 | 249 | # 执行更新 250 | values = list(filtered_data.values()) + [record_id] 251 | self.execute_query(query, tuple(values)) 252 | logger.info(f"更新记录成功: {table_name} ID={record_id}") 253 | return True 254 | 255 | except Exception as e: 256 | logger.error(f"更新记录失败: {table_name} ID={record_id} - {e}") 257 | raise 258 | 259 | def delete_record(self, table_name: str, record_id: int) -> bool: 260 | """删除记录 261 | 262 | Args: 263 | table_name: 表名 264 | record_id: 记录ID 265 | 266 | Returns: 267 | 是否删除成功 268 | """ 269 | try: 270 | query = f"DELETE FROM {table_name} WHERE id = ?" 271 | self.execute_query(query, (record_id,)) 272 | logger.info(f"删除记录成功: {table_name} ID={record_id}") 273 | return True 274 | 275 | except Exception as e: 276 | logger.error(f"删除记录失败: {table_name} ID={record_id} - {e}") 277 | raise -------------------------------------------------------------------------------- /app/models/chapter.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict, Optional 2 | from datetime import datetime 3 | from ..database.sqlite import DatabaseManager 4 | import json 5 | import logging 6 | 7 | class Chapter: 8 | def __init__(self, db_manager: DatabaseManager): 9 | """初始化章节模型 10 | 11 | Args: 12 | db_manager: 数据库管理器实例 13 | """ 14 | self.db = db_manager 15 | 16 | def get_by_novel(self, novel_id: int, limit: Optional[int] = None, before_chapter: Optional[int] = None) -> List[Dict]: 17 | """获取指定小说的章节 18 | 19 | Args: 20 | novel_id: 小说ID 21 | limit: 限制返回的章节数量 22 | before_chapter: 只返回此章节号之前的章节 23 | 24 | Returns: 25 | 章节列表 26 | """ 27 | try: 28 | query = """ 29 | SELECT id, novel_id, chapter_number, title, content, summary, created_at 30 | FROM chapters 31 | WHERE novel_id = ? 32 | """ 33 | params = [novel_id] 34 | 35 | if before_chapter is not None: 36 | query += " AND chapter_number < ?" 37 | params.append(before_chapter) 38 | 39 | query += " ORDER BY chapter_number DESC" 40 | 41 | if limit is not None: 42 | query += " LIMIT ?" 43 | params.append(limit) 44 | 45 | result = self.db.execute_query(query, tuple(params)) 46 | 47 | chapters = [{ 48 | "id": row[0], 49 | "novel_id": row[1], 50 | "chapter_number": row[2], 51 | "title": row[3], 52 | "content": row[4], 53 | "summary": row[5], 54 | "created_at": row[6] 55 | } for row in result] if result else [] 56 | 57 | logging.info(f"获取小说章节列表成功: novel_id={novel_id}, before_chapter={before_chapter}, limit={limit}, 共{len(chapters)}章") 58 | return chapters 59 | 60 | except Exception as e: 61 | logging.error(f"获取小说章节列表失败: {e}") 62 | raise 63 | 64 | def create(self, novel_id: int, chapter_number: int, title: str, 65 | content: str = "", summary: str = "") -> int: 66 | """创建新章节 67 | 68 | Args: 69 | novel_id: 小说ID 70 | chapter_number: 章节号 71 | title: 章节标题 72 | content: 章节内容 73 | summary: 章节摘要 74 | 75 | Returns: 76 | 新创建的章节ID 77 | """ 78 | try: 79 | # 检查章节号是否已存在 80 | if self._chapter_exists(novel_id, chapter_number): 81 | raise ValueError(f"章节号 {chapter_number} 已存在") 82 | 83 | # 检查小说是否存在 84 | novel_check = self.db.execute_query( 85 | "SELECT 1 FROM novels WHERE id = ?", 86 | (novel_id,) 87 | ) 88 | if not novel_check: 89 | raise ValueError(f"小说ID {novel_id} 不存在") 90 | 91 | query = """ 92 | INSERT INTO chapters (novel_id, chapter_number, title, content, summary) 93 | VALUES (?, ?, ?, ?, ?) 94 | """ 95 | result = self.db.execute_query( 96 | query, 97 | (novel_id, chapter_number, title, content, summary) 98 | ) 99 | 100 | if not result: 101 | raise ValueError("创建章节失败") 102 | 103 | chapter_id = result[0][0] 104 | logging.info(f"创建章节成功,ID: {chapter_id}") 105 | 106 | # 创建初始版本 107 | self._create_version(chapter_id, content, "初始版本") 108 | 109 | return chapter_id 110 | 111 | except Exception as e: 112 | logging.error(f"创建章节失败: {e}") 113 | raise 114 | 115 | def get(self, chapter_id: int) -> Optional[Dict]: 116 | """获取章节信息 117 | 118 | Args: 119 | chapter_id: 章节ID 120 | 121 | Returns: 122 | 章节信息字典 123 | """ 124 | try: 125 | query = """ 126 | SELECT id, novel_id, chapter_number, title, content, summary, created_at 127 | FROM chapters 128 | WHERE id = ? 129 | """ 130 | result = self.db.execute_query(query, (chapter_id,)) 131 | 132 | if not result: 133 | logging.warning(f"未找到章节: {chapter_id}") 134 | return None 135 | 136 | chapter_info = { 137 | "id": result[0][0], 138 | "novel_id": result[0][1], 139 | "chapter_number": result[0][2], 140 | "title": result[0][3], 141 | "content": result[0][4], 142 | "summary": result[0][5], 143 | "created_at": result[0][6] 144 | } 145 | logging.info(f"获取章节信息成功: {chapter_id}") 146 | return chapter_info 147 | 148 | except Exception as e: 149 | logging.error(f"获取章节信息失败: {e}") 150 | raise 151 | 152 | def update(self, chapter_id: int, **kwargs) -> bool: 153 | """更新章节信息 154 | 155 | Args: 156 | chapter_id: 章节ID 157 | **kwargs: 要更新的字段和值 158 | 159 | Returns: 160 | 是否更新成功 161 | """ 162 | try: 163 | # 检查章节是否存在 164 | chapter_info = self.get(chapter_id) 165 | if not chapter_info: 166 | raise ValueError(f"章节不存在: {chapter_id}") 167 | 168 | # 如果要更新内容,先创建新版本 169 | if "content" in kwargs: 170 | self._create_version(chapter_id, chapter_info["content"], "自动保存") 171 | 172 | # 构建更新语句 173 | fields = [] 174 | values = [] 175 | for key, value in kwargs.items(): 176 | if key in ["title", "content", "summary"]: 177 | fields.append(f"{key} = ?") 178 | values.append(value) 179 | 180 | if not fields: 181 | return False 182 | 183 | query = f""" 184 | UPDATE chapters 185 | SET {', '.join(fields)} 186 | WHERE id = ? 187 | """ 188 | values.append(chapter_id) 189 | 190 | self.db.execute_query(query, tuple(values)) 191 | logging.info(f"更新章节成功: {chapter_id}") 192 | return True 193 | 194 | except Exception as e: 195 | logging.error(f"更新章节失败: {e}") 196 | raise 197 | 198 | def delete(self, chapter_id: int) -> bool: 199 | """删除章节 200 | 201 | Args: 202 | chapter_id: 章节ID 203 | 204 | Returns: 205 | 是否删除成功 206 | """ 207 | try: 208 | # 检查章节是否存在 209 | if not self.get(chapter_id): 210 | raise ValueError(f"章节不存在: {chapter_id}") 211 | 212 | # 删除版本历史 213 | self.db.execute_query( 214 | "DELETE FROM chapter_versions WHERE chapter_id = ?", 215 | (chapter_id,) 216 | ) 217 | 218 | # 删除章节 219 | self.db.execute_query( 220 | "DELETE FROM chapters WHERE id = ?", 221 | (chapter_id,) 222 | ) 223 | logging.info(f"删除章节成功: {chapter_id}") 224 | return True 225 | 226 | except Exception as e: 227 | logging.error(f"删除章节失败: {e}") 228 | raise 229 | 230 | def get_versions(self, chapter_id: int) -> List[Dict]: 231 | """获取章节的版本历史 232 | 233 | Args: 234 | chapter_id: 章节ID 235 | 236 | Returns: 237 | 版本历史列表 238 | """ 239 | try: 240 | # 检查章节是否存在 241 | if not self.get(chapter_id): 242 | raise ValueError(f"章节不存在: {chapter_id}") 243 | 244 | query = """ 245 | SELECT id, content, comment, created_at 246 | FROM chapter_versions 247 | WHERE chapter_id = ? 248 | ORDER BY created_at DESC 249 | """ 250 | result = self.db.execute_query(query, (chapter_id,)) 251 | 252 | versions = [{ 253 | "id": row[0], 254 | "content": row[1], 255 | "comment": row[2], 256 | "created_at": row[3] 257 | } for row in result] if result else [] 258 | 259 | logging.info(f"获取版本历史成功: {chapter_id}, 共{len(versions)}个版本") 260 | return versions 261 | 262 | except Exception as e: 263 | logging.error(f"获取版本历史失败: {e}") 264 | raise 265 | 266 | def restore_version(self, version_id: int) -> bool: 267 | """恢复到指定版本 268 | 269 | Args: 270 | version_id: 版本ID 271 | 272 | Returns: 273 | 是否恢复成功 274 | """ 275 | try: 276 | # 获取版本信息 277 | query = """ 278 | SELECT chapter_id, content 279 | FROM chapter_versions 280 | WHERE id = ? 281 | """ 282 | result = self.db.execute_query(query, (version_id,)) 283 | 284 | if not result: 285 | raise ValueError(f"版本不存在: {version_id}") 286 | 287 | chapter_id, content = result[0] 288 | 289 | # 更新章节内容 290 | success = self.update(chapter_id, content=content) 291 | if success: 292 | logging.info(f"恢复版本成功: {version_id}") 293 | return success 294 | 295 | except Exception as e: 296 | logging.error(f"恢复版本失败: {e}") 297 | raise 298 | 299 | def _chapter_exists(self, novel_id: int, chapter_number: int) -> bool: 300 | """检查章节号是否已存在""" 301 | query = """ 302 | SELECT 1 FROM chapters 303 | WHERE novel_id = ? AND chapter_number = ? 304 | """ 305 | result = self.db.execute_query(query, (novel_id, chapter_number)) 306 | return bool(result) 307 | 308 | def _create_version(self, chapter_id: int, content: str, comment: str): 309 | """创建新版本""" 310 | query = """ 311 | INSERT INTO chapter_versions (chapter_id, content, comment) 312 | VALUES (?, ?, ?) 313 | """ 314 | self.db.execute_query(query, (chapter_id, content, comment)) 315 | logging.info(f"创建版本成功: {chapter_id}") -------------------------------------------------------------------------------- /app/models/character.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict, Optional, Tuple 2 | from datetime import datetime 3 | from ..database.sqlite import DatabaseManager 4 | import logging 5 | 6 | # 配置日志 7 | logger = logging.getLogger(__name__) 8 | 9 | class Character: 10 | def __init__(self, db_manager: DatabaseManager): 11 | """初始化角色模型 12 | 13 | Args: 14 | db_manager: 数据库管理器实例 15 | """ 16 | self.db = db_manager 17 | logger.info("角色模型初始化完成") 18 | 19 | def create(self, novel_id: int, name: str, description: str = None, 20 | characteristics: str = None, role_type: str = "配角", 21 | first_appearance: int = None, status: str = "活跃") -> int: 22 | """创建角色""" 23 | query = """ 24 | INSERT INTO characters ( 25 | novel_id, name, description, characteristics, 26 | role_type, first_appearance, status 27 | ) VALUES (?, ?, ?, ?, ?, ?, ?) 28 | """ 29 | result = self.db.execute_query( 30 | query, 31 | (novel_id, name, description, characteristics, 32 | role_type, first_appearance, status) 33 | ) 34 | return result[0][0] if result else None 35 | 36 | def extract_characters_from_content(self, generator, content: str) -> List[Dict]: 37 | """从内容中提取角色信息 38 | 39 | Args: 40 | generator: NovelGenerator实例 41 | content: 章节内容 42 | 43 | Returns: 44 | 角色信息列表,每个角色包含name和description 45 | """ 46 | return generator.extract_characters(content) 47 | 48 | def auto_update_characters(self, generator, novel_id: int, chapter_id: int, content: str): 49 | """自动更新角色信息 50 | 51 | Args: 52 | generator: NovelGenerator实例 53 | novel_id: 小说ID 54 | chapter_id: 章节ID 55 | content: 章节内容 56 | """ 57 | try: 58 | # 提取新角色 59 | new_characters = self.extract_characters_from_content(generator, content) 60 | logger.info(f"从内容中提取到 {len(new_characters)} 个角色") 61 | 62 | # 获取现有角色 63 | existing_characters = self.get_by_novel(novel_id) 64 | existing_names = {char['name'] for char in existing_characters} 65 | logger.info(f"当前小说已有 {len(existing_names)} 个角色") 66 | 67 | # 添加新角色 68 | added_count = 0 69 | for char in new_characters: 70 | if char['name'] not in existing_names: 71 | self.create( 72 | novel_id=novel_id, 73 | name=char['name'], 74 | description=char.get('description'), 75 | characteristics=char.get('characteristics'), 76 | role_type=char.get('role_type', '配角'), 77 | first_appearance=chapter_id, 78 | status='活跃' 79 | ) 80 | added_count += 1 81 | logger.info(f"添加新角色: {char['name']}") 82 | 83 | if added_count > 0: 84 | logger.info(f"成功添加 {added_count} 个新角色") 85 | else: 86 | logger.info("没有新角色需要添加") 87 | 88 | except Exception as e: 89 | logger.error(f"自动更新角色失败: {str(e)}") 90 | raise 91 | 92 | def get_character_by_name(self, novel_id: int, name: str) -> Optional[Dict]: 93 | """根据名称获取角色 94 | 95 | Args: 96 | novel_id: 小说ID 97 | name: 角色名称 98 | 99 | Returns: 100 | 角色信息字典 101 | """ 102 | query = "SELECT * FROM characters WHERE novel_id = ? AND name = ?" 103 | result = self.db.execute_query(query, (novel_id, name)) 104 | if result: 105 | row = result[0] 106 | return { 107 | 'id': row[0], 108 | 'novel_id': row[1], 109 | 'name': row[2], 110 | 'description': row[3], 111 | 'characteristics': row[4], 112 | 'role_type': row[5], 113 | 'status': row[6], 114 | 'first_appearance': row[7] 115 | } 116 | return None 117 | 118 | def get(self, character_id: int) -> Optional[Dict]: 119 | """获取角色信息 120 | 121 | Args: 122 | character_id: 角色ID 123 | 124 | Returns: 125 | 角色信息字典 126 | """ 127 | try: 128 | query = """ 129 | SELECT id, novel_id, name, description, characteristics, 130 | role_type, first_appearance, status 131 | FROM characters 132 | WHERE id = ? 133 | """ 134 | result = self.db.execute_query(query, (character_id,)) 135 | 136 | if not result: 137 | logger.warning(f"未找到角色: {character_id}") 138 | return None 139 | 140 | character_info = { 141 | "id": result[0][0], 142 | "novel_id": result[0][1], 143 | "name": result[0][2], 144 | "description": result[0][3], 145 | "characteristics": result[0][4], 146 | "role_type": result[0][5], 147 | "first_appearance": result[0][6], 148 | "status": result[0][7] 149 | } 150 | logger.info(f"获取角色信息成功: {character_id}") 151 | return character_info 152 | 153 | except Exception as e: 154 | logger.error(f"获取角色信息失败: {e}") 155 | raise 156 | 157 | def update(self, character_id: int, **kwargs) -> bool: 158 | """更新角色信息 159 | 160 | Args: 161 | character_id: 角色ID 162 | **kwargs: 要更新的字段和值 163 | 164 | Returns: 165 | 是否更新成功 166 | """ 167 | try: 168 | # 检查角色是否存在 169 | if not self.get(character_id): 170 | raise ValueError(f"角色不存在: {character_id}") 171 | 172 | # 构建更新语句 173 | fields = [] 174 | values = [] 175 | valid_fields = [ 176 | "name", "description", "characteristics", 177 | "role_type", "first_appearance", "status" 178 | ] 179 | 180 | for key, value in kwargs.items(): 181 | if key in valid_fields: 182 | fields.append(f"{key} = ?") 183 | values.append(value) 184 | 185 | if not fields: 186 | return False 187 | 188 | query = f""" 189 | UPDATE characters 190 | SET {', '.join(fields)} 191 | WHERE id = ? 192 | """ 193 | values.append(character_id) 194 | 195 | self.db.execute_query(query, tuple(values)) 196 | logger.info(f"更新角色成功: {character_id}") 197 | return True 198 | 199 | except Exception as e: 200 | logger.error(f"更新角色失败: {e}") 201 | raise 202 | 203 | def delete(self, character_id: int) -> bool: 204 | """删除角色 205 | 206 | Args: 207 | character_id: 角色ID 208 | 209 | Returns: 210 | 是否删除成功 211 | """ 212 | try: 213 | # 检查角色是否存在 214 | if not self.get(character_id): 215 | raise ValueError(f"角色不存在: {character_id}") 216 | 217 | # 删除角色关系 218 | self.db.execute_query( 219 | "DELETE FROM character_relationships WHERE character1_id = ? OR character2_id = ?", 220 | (character_id, character_id) 221 | ) 222 | 223 | # 删除角色 224 | self.db.execute_query( 225 | "DELETE FROM characters WHERE id = ?", 226 | (character_id,) 227 | ) 228 | logger.info(f"删除角色成功: {character_id}") 229 | return True 230 | 231 | except Exception as e: 232 | logger.error(f"删除角色失败: {e}") 233 | raise 234 | 235 | def add_relationship(self, novel_id: int, character1_id: int, character2_id: int, 236 | relationship_type: str, description: str = "", 237 | start_chapter: Optional[int] = None) -> int: 238 | """添加角色关系 239 | 240 | Args: 241 | novel_id: 小说ID 242 | character1_id: 角色1 ID 243 | character2_id: 角色2 ID 244 | relationship_type: 关系类型 245 | description: 关系描述 246 | start_chapter: 关系开始的章节ID 247 | 248 | Returns: 249 | 新创建的关系ID 250 | """ 251 | try: 252 | # 检查角色是否存在 253 | if not self.get(character1_id): 254 | raise ValueError(f"角色1不存在: {character1_id}") 255 | if not self.get(character2_id): 256 | raise ValueError(f"角色2不存在: {character2_id}") 257 | 258 | # 检查章节是否存在 259 | if start_chapter: 260 | chapter_check = self.db.execute_query( 261 | "SELECT 1 FROM chapters WHERE id = ?", 262 | (start_chapter,) 263 | ) 264 | if not chapter_check: 265 | raise ValueError(f"章节不存在: {start_chapter}") 266 | 267 | query = """ 268 | INSERT INTO character_relationships ( 269 | novel_id, character1_id, character2_id, 270 | relationship_type, description, start_chapter 271 | ) 272 | VALUES (?, ?, ?, ?, ?, ?) 273 | """ 274 | result = self.db.execute_query( 275 | query, 276 | (novel_id, character1_id, character2_id, 277 | relationship_type, description, start_chapter) 278 | ) 279 | 280 | if not result: 281 | raise ValueError("创建角色关系失败") 282 | 283 | relationship_id = result[0][0] 284 | logger.info(f"创建角色关系成功,ID: {relationship_id}") 285 | return relationship_id 286 | 287 | except Exception as e: 288 | logger.error(f"创建角色关系失败: {e}") 289 | raise 290 | 291 | def get_relationships(self, character_id: int) -> List[Dict]: 292 | """获取角色的所有关系 293 | 294 | Args: 295 | character_id: 角色ID 296 | 297 | Returns: 298 | 关系列表 299 | """ 300 | try: 301 | query = """ 302 | SELECT r.id, r.relationship_type, r.description, 303 | r.start_chapter, r.character1_id, r.character2_id, 304 | c1.name as character1_name, c2.name as character2_name 305 | FROM character_relationships r 306 | JOIN characters c1 ON r.character1_id = c1.id 307 | JOIN characters c2 ON r.character2_id = c2.id 308 | WHERE r.character1_id = ? OR r.character2_id = ? 309 | """ 310 | result = self.db.execute_query(query, (character_id, character_id)) 311 | 312 | relationships = [] 313 | if result: 314 | for row in result: 315 | # 确保当前角色始终是 character1 316 | if row[5] == character_id: # 如果当前角色是 character2 317 | char1_id, char2_id = row[5], row[4] 318 | char1_name, char2_name = row[7], row[6] 319 | else: 320 | char1_id, char2_id = row[4], row[5] 321 | char1_name, char2_name = row[6], row[7] 322 | 323 | relationships.append({ 324 | "id": row[0], 325 | "relationship_type": row[1], 326 | "description": row[2], 327 | "start_chapter": row[3], 328 | "character1_id": char1_id, 329 | "character2_id": char2_id, 330 | "character1_name": char1_name, 331 | "character2_name": char2_name 332 | }) 333 | 334 | logger.info(f"获取角色关系成功: {character_id}, 共{len(relationships)}个关系") 335 | return relationships 336 | 337 | except Exception as e: 338 | logger.error(f"获取角色关系失败: {e}") 339 | raise 340 | 341 | def update_relationship(self, relationship_id: int, **kwargs) -> bool: 342 | """更新角色关系 343 | 344 | Args: 345 | relationship_id: 关系ID 346 | **kwargs: 要更新的字段和值 347 | 348 | Returns: 349 | 是否更新成功 350 | """ 351 | try: 352 | # 检查关系是否存在 353 | query = "SELECT 1 FROM character_relationships WHERE id = ?" 354 | if not self.db.execute_query(query, (relationship_id,)): 355 | raise ValueError(f"关系不存在: {relationship_id}") 356 | 357 | # 构建更新语句 358 | fields = [] 359 | values = [] 360 | valid_fields = ["relationship_type", "description", "start_chapter"] 361 | 362 | for key, value in kwargs.items(): 363 | if key in valid_fields: 364 | fields.append(f"{key} = ?") 365 | values.append(value) 366 | 367 | if not fields: 368 | return False 369 | 370 | query = f""" 371 | UPDATE character_relationships 372 | SET {', '.join(fields)} 373 | WHERE id = ? 374 | """ 375 | values.append(relationship_id) 376 | 377 | self.db.execute_query(query, tuple(values)) 378 | logger.info(f"更新角色关系成功: {relationship_id}") 379 | return True 380 | 381 | except Exception as e: 382 | logger.error(f"更新角色关系失败: {e}") 383 | raise 384 | 385 | def delete_relationship(self, relationship_id: int) -> bool: 386 | """删除角色关系 387 | 388 | Args: 389 | relationship_id: 关系ID 390 | 391 | Returns: 392 | 是否删除成功 393 | """ 394 | try: 395 | query = "DELETE FROM character_relationships WHERE id = ?" 396 | self.db.execute_query(query, (relationship_id,)) 397 | logger.info(f"删除角色关系成功: {relationship_id}") 398 | return True 399 | 400 | except Exception as e: 401 | logger.error(f"删除角色关系失败: {e}") 402 | raise 403 | 404 | def get_by_novel(self, novel_id: int) -> list: 405 | """获取小说的所有角色 406 | 407 | Args: 408 | novel_id: 小说ID 409 | 410 | Returns: 411 | 角色列表 412 | """ 413 | query = "SELECT * FROM characters WHERE novel_id = ? ORDER BY id" 414 | result = self.db.execute_query(query, (novel_id,)) 415 | return [ 416 | { 417 | 'id': row[0], 418 | 'novel_id': row[1], 419 | 'name': row[2], 420 | 'description': row[3], 421 | 'characteristics': row[4], 422 | 'role_type': row[5], 423 | 'status': row[6], 424 | 'first_appearance': row[7] 425 | } 426 | for row in result 427 | ] if result else [] 428 | 429 | def get_character_relationships_for_novel(self, novel_id: int) -> List[Dict]: 430 | """获取小说中所有的角色关系信息""" 431 | try: 432 | logger.info(f"开始获取小说(ID={novel_id})的角色关系...") 433 | 434 | query = """ 435 | SELECT r.id, 436 | c1.name as character1_name, 437 | c2.name as character2_name, 438 | r.relationship_type, 439 | r.description, 440 | r.start_chapter, 441 | ch.chapter_number 442 | FROM character_relationships r 443 | JOIN characters c1 ON r.character1_id = c1.id 444 | JOIN characters c2 ON r.character2_id = c2.id 445 | LEFT JOIN chapters ch ON r.start_chapter = ch.id 446 | WHERE r.novel_id = ? 447 | ORDER BY r.id 448 | """ 449 | result = self.db.execute_query(query, (novel_id,)) 450 | 451 | relationships = [] 452 | if result: 453 | for row in result: 454 | chapter_info = f"第{row[6]}章" if row[6] else None 455 | relationships.append({ 456 | 'id': row[0], 457 | 'character1_name': row[1], 458 | 'character2_name': row[2], 459 | 'relationship_type': row[3], 460 | 'description': row[4], 461 | 'start_chapter': chapter_info 462 | }) 463 | 464 | # 按关系类型统计 465 | relation_types = {} 466 | for r in relationships: 467 | r_type = r['relationship_type'] 468 | relation_types[r_type] = relation_types.get(r_type, 0) + 1 469 | 470 | # 生成关系类型统计信息 471 | type_summary = ", ".join( 472 | f"{t}:{c}组" for t, c in sorted(relation_types.items()) 473 | ) 474 | 475 | logger.info(f"成功获取{len(relationships)}个角色关系") 476 | logger.info(f"关系类型统计: {type_summary}") 477 | 478 | # 输出每个关系的详细信息 479 | for relation in relationships: 480 | char1 = relation['character1_name'] 481 | char2 = relation['character2_name'] 482 | rel_type = relation['relationship_type'] 483 | desc = relation['description'] 484 | 485 | log_msg = f"- {char1} 与 {char2}: {rel_type}" 486 | if desc: 487 | log_msg += f" ({desc})" 488 | logger.info(log_msg) 489 | else: 490 | logger.warning("未找到任何角色关系") 491 | 492 | return relationships 493 | 494 | except Exception as e: 495 | error_msg = f"获取小说角色关系失败: {e}" 496 | logger.error(error_msg) 497 | raise ValueError(error_msg) 498 | 499 | def get_character_context(self, novel_id: int, character_name: str = None) -> Dict: 500 | """获取角色相关的上下文信息,用于AI生成内容 501 | 502 | Args: 503 | novel_id: 小说ID 504 | character_name: 角色名称(可选,如果提供则只返回该角色的信息) 505 | 506 | Returns: 507 | 角色上下文信息: 508 | { 509 | 'characters': [ 510 | { 511 | 'name': 角色名称, 512 | 'description': 角色描述, 513 | 'characteristics': 角色特征, 514 | 'role_type': 角色类型, 515 | 'status': 角色状态 516 | } 517 | ], 518 | 'relationships': [ 519 | { 520 | 'character1_name': 角色1名称, 521 | 'character2_name': 角色2名称, 522 | 'relationship_type': 关系类型, 523 | 'description': 关系描述 524 | } 525 | ] 526 | } 527 | """ 528 | try: 529 | # 获取角色信息 530 | if character_name: 531 | # 获取指定角色 532 | character = self.get_character_by_name(novel_id, character_name) 533 | characters = [character] if character else [] 534 | if characters: 535 | logger.info(f"已获取角色 '{character_name}' 的信息") 536 | else: 537 | logger.warning(f"未找到角色: {character_name}") 538 | else: 539 | # 获取所有角色 540 | characters = self.get_by_novel(novel_id) 541 | if characters: 542 | role_types = {} 543 | for char in characters: 544 | role_type = char['role_type'] 545 | role_types[role_type] = role_types.get(role_type, 0) + 1 546 | role_summary = ", ".join(f"{role}:{count}人" for role, count in role_types.items()) 547 | logger.info(f"已获取{len(characters)}个角色信息 ({role_summary})") 548 | else: 549 | logger.warning("未找到任何角色信息") 550 | 551 | # 获取角色关系 552 | relationships = self.get_character_relationships_for_novel(novel_id) 553 | 554 | # 如果指定了角色名称,只返回与该角色相关的关系 555 | if character_name: 556 | relationships = [ 557 | r for r in relationships 558 | if character_name in (r['character1_name'], r['character2_name']) 559 | ] 560 | if relationships: 561 | logger.info(f"已获取角色 '{character_name}' 的{len(relationships)}个关系") 562 | else: 563 | logger.info(f"角色 '{character_name}' 暂无任何关系") 564 | else: 565 | if relationships: 566 | # 统计关系类型 567 | relation_types = {} 568 | for r in relationships: 569 | r_type = r['relationship_type'] 570 | relation_types[r_type] = relation_types.get(r_type, 0) + 1 571 | type_summary = ", ".join(f"{t}:{c}组" for t, c in relation_types.items()) 572 | logger.info(f"已获取{len(relationships)}个角色关系 ({type_summary})") 573 | else: 574 | logger.info("暂无任何角色关系") 575 | 576 | # 构建上下文信息 577 | context = { 578 | 'characters': [ 579 | { 580 | 'name': char['name'], 581 | 'description': char['description'], 582 | 'characteristics': char['characteristics'], 583 | 'role_type': char['role_type'], 584 | 'status': char['status'] 585 | } 586 | for char in characters 587 | ], 588 | 'relationships': [ 589 | { 590 | 'character1_name': r['character1_name'], 591 | 'character2_name': r['character2_name'], 592 | 'relationship_type': r['relationship_type'], 593 | 'description': r['description'] 594 | } 595 | for r in relationships 596 | ] 597 | } 598 | 599 | return context 600 | 601 | except Exception as e: 602 | logger.error(f"获取角色上下文失败: {e}") 603 | raise 604 | -------------------------------------------------------------------------------- /app/models/novel.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict, Optional 2 | from datetime import datetime 3 | from ..database.sqlite import DatabaseManager 4 | import json 5 | import logging 6 | 7 | class Novel: 8 | def __init__(self, db_manager: DatabaseManager): 9 | """初始化小说模型 10 | 11 | Args: 12 | db_manager: 数据库管理器实例 13 | """ 14 | self.db = db_manager 15 | 16 | def create(self, title: str, outline: str = "") -> int: 17 | """创建新小说 18 | 19 | Args: 20 | title: 小说标题 21 | outline: 小说大纲(可选) 22 | 23 | Returns: 24 | 新创建的小说ID 25 | """ 26 | try: 27 | query = """ 28 | INSERT INTO novels (title, outline, current_chapter) 29 | VALUES (?, ?, ?) 30 | """ 31 | result = self.db.execute_query(query, (title, outline, 1)) 32 | 33 | if not result: 34 | raise ValueError("创建小说失败") 35 | 36 | novel_id = result[0][0] 37 | logging.info(f"创建小说成功,ID: {novel_id}") 38 | return novel_id 39 | 40 | except Exception as e: 41 | logging.error(f"创建小说失败: {e}") 42 | raise 43 | 44 | def get(self, novel_id: int) -> Optional[Dict]: 45 | """获取小说信息 46 | 47 | Args: 48 | novel_id: 小说ID 49 | 50 | Returns: 51 | 小说信息字典 52 | """ 53 | try: 54 | query = "SELECT * FROM novels WHERE id = ?" 55 | result = self.db.execute_query(query, (novel_id,)) 56 | 57 | if not result: 58 | logging.warning(f"未找到小说: {novel_id}") 59 | return None 60 | 61 | novel_info = { 62 | "id": result[0][0], 63 | "title": result[0][1], 64 | "outline": result[0][2], 65 | "current_chapter": result[0][3] 66 | } 67 | logging.info(f"获取小说信息成功: {novel_id}") 68 | return novel_info 69 | 70 | except Exception as e: 71 | logging.error(f"获取小说信息失败: {e}") 72 | raise 73 | 74 | def update(self, novel_id: int, **kwargs) -> bool: 75 | """更新小说信息 76 | 77 | Args: 78 | novel_id: 小说ID 79 | **kwargs: 要更新的字段和值 80 | 81 | Returns: 82 | 是否更新成功 83 | """ 84 | try: 85 | # 检查小说是否存在 86 | if not self.get(novel_id): 87 | raise ValueError(f"小说不存在: {novel_id}") 88 | 89 | # 构建更新语句 90 | fields = [] 91 | values = [] 92 | for key, value in kwargs.items(): 93 | if key in ["title", "outline", "current_chapter"]: 94 | fields.append(f"{key} = ?") 95 | values.append(value) 96 | 97 | if not fields: 98 | return False 99 | 100 | query = f""" 101 | UPDATE novels 102 | SET {', '.join(fields)} 103 | WHERE id = ? 104 | """ 105 | values.append(novel_id) 106 | 107 | self.db.execute_query(query, tuple(values)) 108 | logging.info(f"更新小说成功: {novel_id}") 109 | return True 110 | 111 | except Exception as e: 112 | logging.error(f"更新小说信息失败: {e}") 113 | raise 114 | 115 | def delete(self, novel_id: int) -> bool: 116 | """删除小说 117 | 118 | Args: 119 | novel_id: 小说ID 120 | 121 | Returns: 122 | 是否删除成功 123 | """ 124 | try: 125 | # 检查小说是否存在 126 | if not self.get(novel_id): 127 | raise ValueError(f"小说不存在: {novel_id}") 128 | 129 | # 首先删除相关的章节和角色 130 | self.db.execute_query("DELETE FROM chapters WHERE novel_id = ?", (novel_id,)) 131 | self.db.execute_query("DELETE FROM characters WHERE novel_id = ?", (novel_id,)) 132 | 133 | # 删除小说 134 | self.db.execute_query("DELETE FROM novels WHERE id = ?", (novel_id,)) 135 | logging.info(f"删除小说成功: {novel_id}") 136 | return True 137 | 138 | except Exception as e: 139 | logging.error(f"删除小说失败: {e}") 140 | raise 141 | 142 | def list_all(self) -> List[Dict]: 143 | """获取所有小说列表 144 | 145 | Returns: 146 | 小说信息列表 147 | """ 148 | try: 149 | query = "SELECT * FROM novels ORDER BY id DESC" 150 | result = self.db.execute_query(query) 151 | 152 | novels = [{ 153 | "id": row[0], 154 | "title": row[1], 155 | "outline": row[2], 156 | "current_chapter": row[3] 157 | } for row in result] if result else [] 158 | 159 | logging.info(f"获取小说列表成功,共{len(novels)}本") 160 | return novels 161 | 162 | except Exception as e: 163 | logging.error(f"获取小说列表失败: {e}") 164 | raise 165 | 166 | def get_chapters(self, novel_id: int) -> List[Dict]: 167 | """获取小说的所有章节 168 | 169 | Args: 170 | novel_id: 小说ID 171 | 172 | Returns: 173 | 章节信息列表 174 | """ 175 | try: 176 | # 检查小说是否存在 177 | if not self.get(novel_id): 178 | raise ValueError(f"小说不存在: {novel_id}") 179 | 180 | query = """ 181 | SELECT id, chapter_number, title, summary, created_at 182 | FROM chapters 183 | WHERE novel_id = ? 184 | ORDER BY chapter_number 185 | """ 186 | result = self.db.execute_query(query, (novel_id,)) 187 | 188 | chapters = [{ 189 | "id": row[0], 190 | "chapter_number": row[1], 191 | "title": row[2], 192 | "summary": row[3], 193 | "created_at": row[4] 194 | } for row in result] if result else [] 195 | 196 | logging.info(f"获取章节列表成功: {novel_id}, 共{len(chapters)}章") 197 | return chapters 198 | 199 | except Exception as e: 200 | logging.error(f"获取章节列表失败: {e}") 201 | raise 202 | 203 | def get_characters(self, novel_id: int) -> List[Dict]: 204 | """获取小说的所有角色 205 | 206 | Args: 207 | novel_id: 小说ID 208 | 209 | Returns: 210 | 角色信息列表 211 | """ 212 | try: 213 | # 检查小说是否存在 214 | if not self.get(novel_id): 215 | raise ValueError(f"小说不存在: {novel_id}") 216 | 217 | query = """ 218 | SELECT id, name, description, characteristics 219 | FROM characters 220 | WHERE novel_id = ? 221 | """ 222 | result = self.db.execute_query(query, (novel_id,)) 223 | 224 | characters = [{ 225 | "id": row[0], 226 | "name": row[1], 227 | "description": row[2], 228 | "characteristics": row[3] 229 | } for row in result] if result else [] 230 | 231 | logging.info(f"获取角色列表成功: {novel_id}, 共{len(characters)}个角色") 232 | return characters 233 | 234 | except Exception as e: 235 | logging.error(f"获取角色列表失败: {e}") 236 | raise 237 | 238 | def get_by_title(self, title: str) -> Optional[Dict]: 239 | """根据标题获取小说信息 240 | 241 | Args: 242 | title: 小说标题 243 | 244 | Returns: 245 | 小说信息字典,如果不存在返回 None 246 | """ 247 | try: 248 | query = """ 249 | SELECT id, title, outline, current_chapter 250 | FROM novels 251 | WHERE title = ? 252 | """ 253 | result = self.db.execute_query(query, (title,)) 254 | 255 | if not result: 256 | return None 257 | 258 | return { 259 | "id": result[0][0], 260 | "title": result[0][1], 261 | "outline": result[0][2], 262 | "current_chapter": result[0][3] 263 | } 264 | except Exception as e: 265 | logging.error(f"根据标题获取小说失败: {e}") 266 | raise -------------------------------------------------------------------------------- /app/ui/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | UI 模块 3 | 包含所有界面相关的代码 4 | """ -------------------------------------------------------------------------------- /app/ui/dialogs/character_editor.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtWidgets import ( 2 | QDialog, QVBoxLayout, QFormLayout, QLineEdit, 3 | QTextEdit, QComboBox, QPushButton, QDialogButtonBox 4 | ) 5 | 6 | class CharacterEditorDialog(QDialog): 7 | """角色编辑对话框""" 8 | 9 | def __init__(self, character=None, parent=None): 10 | """初始化对话框 11 | 12 | Args: 13 | character: 角色信息字典,如果为None则为新建角色 14 | parent: 父窗口 15 | """ 16 | super().__init__(parent) 17 | self.character = character 18 | self.init_ui() 19 | 20 | def init_ui(self): 21 | """初始化UI""" 22 | self.setWindowTitle("编辑角色" if self.character else "新建角色") 23 | self.setMinimumWidth(400) 24 | 25 | layout = QVBoxLayout(self) 26 | 27 | # 创建表单 28 | form_layout = QFormLayout() 29 | 30 | # 角色名称 31 | self.name_edit = QLineEdit() 32 | if self.character: 33 | self.name_edit.setText(self.character.get('name', '')) 34 | form_layout.addRow("角色名称:", self.name_edit) 35 | 36 | # 角色描述 37 | self.description_edit = QTextEdit() 38 | self.description_edit.setMaximumHeight(100) 39 | if self.character: 40 | self.description_edit.setText(self.character.get('description', '')) 41 | form_layout.addRow("角色描述:", self.description_edit) 42 | 43 | # 角色特征 44 | self.characteristics_edit = QTextEdit() 45 | self.characteristics_edit.setMaximumHeight(100) 46 | if self.character: 47 | self.characteristics_edit.setText(self.character.get('characteristics', '')) 48 | form_layout.addRow("角色特征:", self.characteristics_edit) 49 | 50 | # 角色类型 51 | self.role_type = QComboBox() 52 | self.role_type.addItems(["主角", "配角", "反派"]) 53 | if self.character: 54 | self.role_type.setCurrentText(self.character.get('role_type', '配角')) 55 | form_layout.addRow("角色类型:", self.role_type) 56 | 57 | # 角色状态 58 | self.status = QComboBox() 59 | self.status.addItems(["活跃", "已退场", "已死亡"]) 60 | if self.character: 61 | self.status.setCurrentText(self.character.get('status', '活跃')) 62 | form_layout.addRow("角色状态:", self.status) 63 | 64 | layout.addLayout(form_layout) 65 | 66 | # 添加按钮 67 | button_box = QDialogButtonBox( 68 | QDialogButtonBox.StandardButton.Ok | 69 | QDialogButtonBox.StandardButton.Cancel 70 | ) 71 | button_box.accepted.connect(self.accept) 72 | button_box.rejected.connect(self.reject) 73 | layout.addWidget(button_box) 74 | 75 | def get_character_data(self) -> dict: 76 | """获取角色数据 77 | 78 | Returns: 79 | 角色信息字典 80 | """ 81 | return { 82 | 'name': self.name_edit.text().strip(), 83 | 'description': self.description_edit.toPlainText().strip(), 84 | 'characteristics': self.characteristics_edit.toPlainText().strip(), 85 | 'role_type': self.role_type.currentText(), 86 | 'status': self.status.currentText() 87 | } -------------------------------------------------------------------------------- /app/ui/dialogs/content_generator.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtWidgets import ( 2 | QDialog, QVBoxLayout, QHBoxLayout, QTextEdit, 3 | QPushButton, QLabel, QProgressBar, QMessageBox, 4 | QSpinBox, QGroupBox 5 | ) 6 | from PyQt6.QtCore import Qt, pyqtSignal 7 | import logging 8 | 9 | class ContentGeneratorDialog(QDialog): 10 | """内容生成对话框""" 11 | 12 | # 自定义信号 13 | contentGenerated = pyqtSignal(str) # 内容生成完成信号 14 | 15 | def __init__(self, generator, parent=None, context=None): 16 | super().__init__(parent) 17 | self.generator = generator 18 | self.context = context or {} 19 | self.generated_content = "" 20 | self.init_ui() 21 | 22 | def init_ui(self): 23 | """初始化UI""" 24 | self.setWindowTitle("AI 内容生成") 25 | self.setMinimumSize(800, 600) 26 | 27 | layout = QVBoxLayout(self) 28 | 29 | # 提示词输入区域 30 | prompt_group = QGroupBox("提示词") 31 | prompt_layout = QVBoxLayout(prompt_group) 32 | 33 | # 提示词说明 34 | prompt_help = QLabel( 35 | "提示:\n" 36 | "1. 描述你想要生成的内容\n" 37 | "2. 指定写作风格和语气\n" 38 | "3. 设定情节发展方向\n" 39 | "4. 提供关键的背景信息\n" 40 | "\n" 41 | "系统已自动收集以下上下文信息:\n" 42 | f"- {'已有' if 'novel_outline' in self.context else '未有'}小说大纲\n" 43 | f"- {'已有' if 'current_chapter' in self.context else '未有'}当前章节信息\n" 44 | f"- {'已有' if 'previous_summaries' in self.context else '未有'}之前章节摘要\n" 45 | f"- {'已有' + str(len(self.context.get('characters', []))) + '个' if 'characters' in self.context else '未有'}角色信息" 46 | ) 47 | prompt_help.setWordWrap(True) 48 | prompt_layout.addWidget(prompt_help) 49 | 50 | # 提示词输入框 51 | self.prompt_edit = QTextEdit() 52 | self.prompt_edit.setPlaceholderText("在这里输入提示词...") 53 | self.prompt_edit.setMaximumHeight(100) 54 | prompt_layout.addWidget(self.prompt_edit) 55 | 56 | layout.addWidget(prompt_group) 57 | 58 | # 生成选项 59 | options_layout = QHBoxLayout() 60 | 61 | # 字数控制 62 | word_count_layout = QHBoxLayout() 63 | word_count_layout.addWidget(QLabel("生成字数:")) 64 | self.word_count_spin = QSpinBox() 65 | self.word_count_spin.setRange(100, 2000) 66 | self.word_count_spin.setValue(2000) 67 | self.word_count_spin.setSingleStep(100) 68 | word_count_layout.addWidget(self.word_count_spin) 69 | options_layout.addLayout(word_count_layout) 70 | 71 | options_layout.addStretch() 72 | 73 | # 生成按钮 74 | self.generate_button = QPushButton("开始生成") 75 | self.generate_button.clicked.connect(self._on_generate) 76 | options_layout.addWidget(self.generate_button) 77 | 78 | layout.addLayout(options_layout) 79 | 80 | # 进度条 81 | self.progress_bar = QProgressBar() 82 | self.progress_bar.setTextVisible(False) 83 | self.progress_bar.hide() 84 | layout.addWidget(self.progress_bar) 85 | 86 | # 预览区域 87 | preview_group = QGroupBox("内容预览") 88 | preview_layout = QVBoxLayout(preview_group) 89 | 90 | self.preview_edit = QTextEdit() 91 | self.preview_edit.setReadOnly(True) 92 | self.preview_edit.setPlaceholderText("生成的内容将在这里显示...") 93 | preview_layout.addWidget(self.preview_edit) 94 | 95 | layout.addWidget(preview_group) 96 | 97 | # 底部按钮 98 | button_layout = QHBoxLayout() 99 | 100 | self.apply_button = QPushButton("应用") 101 | self.apply_button.clicked.connect(self._on_apply) 102 | self.apply_button.setEnabled(False) 103 | button_layout.addWidget(self.apply_button) 104 | 105 | self.cancel_button = QPushButton("取消") 106 | self.cancel_button.clicked.connect(self.reject) 107 | button_layout.addWidget(self.cancel_button) 108 | 109 | layout.addLayout(button_layout) 110 | 111 | def _on_generate(self): 112 | """生成内容处理""" 113 | prompt = self.prompt_edit.toPlainText().strip() 114 | if not prompt: 115 | QMessageBox.warning(self, "提示", "请输入提示词") 116 | return 117 | 118 | try: 119 | # 显示进度条 120 | self.progress_bar.setRange(0, 0) # 显示忙碌状态 121 | self.progress_bar.show() 122 | self.generate_button.setEnabled(False) 123 | self.prompt_edit.setEnabled(False) 124 | self.word_count_spin.setEnabled(False) 125 | 126 | logging.info("开始生成内容...") 127 | 128 | # 调用生成器 129 | content = self.generator.generate_content( 130 | prompt=prompt, 131 | context=self.context 132 | ) 133 | 134 | # 显示生成的内容 135 | self.preview_edit.setPlainText(content) 136 | self.generated_content = content 137 | 138 | # 更新UI状态 139 | self.progress_bar.hide() 140 | self.apply_button.setEnabled(True) 141 | self.generate_button.setEnabled(True) 142 | self.prompt_edit.setEnabled(True) 143 | self.word_count_spin.setEnabled(True) 144 | 145 | logging.info("内容生成完成") 146 | 147 | except Exception as e: 148 | error_msg = f"生成内容失败:{str(e)}" 149 | logging.error(error_msg) 150 | QMessageBox.critical(self, "错误", error_msg) 151 | self.progress_bar.hide() 152 | self.generate_button.setEnabled(True) 153 | self.prompt_edit.setEnabled(True) 154 | self.word_count_spin.setEnabled(True) 155 | 156 | def _on_apply(self): 157 | """应用生成的内容""" 158 | if self.generated_content: 159 | self.contentGenerated.emit(self.generated_content) 160 | self.accept() 161 | 162 | @classmethod 163 | def generate_content(cls, generator, parent=None, context=None) -> str: 164 | """显示内容生成对话框 165 | 166 | Args: 167 | generator: 生成器实例 168 | parent: 父窗口 169 | context: 上下文信息 170 | 171 | Returns: 172 | 生成的内容,如果取消则返回空字符串 173 | """ 174 | dialog = cls(generator, parent, context) 175 | result = dialog.exec() 176 | 177 | if result == QDialog.DialogCode.Accepted: 178 | return dialog.generated_content 179 | return "" -------------------------------------------------------------------------------- /app/ui/dialogs/database_manager_dialog.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtWidgets import ( 2 | QDialog, QVBoxLayout, QHBoxLayout, QComboBox, 3 | QTableWidget, QTableWidgetItem, QPushButton, 4 | QMessageBox, QLabel, QHeaderView, QTabWidget, 5 | QWidget, QLineEdit, QGroupBox 6 | ) 7 | from PyQt6.QtCore import Qt 8 | import logging 9 | from ...models.character import Character 10 | 11 | class DatabaseManagerDialog(QDialog): 12 | """数据库管理器对话框""" 13 | 14 | def __init__(self, db_manager, parent=None): 15 | super().__init__(parent) 16 | self.db_manager = db_manager 17 | self.current_novel_id = None 18 | self.modified_data = {} # 记录修改的数据 19 | self.init_ui() 20 | 21 | def init_ui(self): 22 | """初始化UI""" 23 | self.setWindowTitle("小说数据管理器") 24 | self.setMinimumSize(1000, 700) 25 | 26 | layout = QVBoxLayout(self) 27 | 28 | # 顶部控制区 29 | top_layout = QHBoxLayout() 30 | 31 | # 小说选择 32 | novel_label = QLabel("选择小说:") 33 | self.novel_combo = QComboBox() 34 | self.refresh_novel_list() 35 | self.novel_combo.currentIndexChanged.connect(self.on_novel_selected) 36 | 37 | top_layout.addWidget(novel_label) 38 | top_layout.addWidget(self.novel_combo) 39 | 40 | # 新增小说按钮 41 | self.add_novel_button = QPushButton("新增小说") 42 | self.add_novel_button.clicked.connect(self.add_novel) 43 | top_layout.addWidget(self.add_novel_button) 44 | 45 | # 删除小说按钮 46 | self.delete_novel_button = QPushButton("删除小说") 47 | self.delete_novel_button.clicked.connect(self.delete_novel) 48 | top_layout.addWidget(self.delete_novel_button) 49 | 50 | top_layout.addStretch() 51 | 52 | layout.addLayout(top_layout) 53 | 54 | # 创建标签页 55 | self.tab_widget = QTabWidget() 56 | 57 | # 章节标签页 58 | self.chapters_tab = QWidget() 59 | self.setup_chapters_tab() 60 | self.tab_widget.addTab(self.chapters_tab, "章节管理") 61 | 62 | # 角色标签页 63 | self.characters_tab = QWidget() 64 | self.setup_characters_tab() 65 | self.tab_widget.addTab(self.characters_tab, "角色管理") 66 | 67 | # 角色关系标签页 68 | self.relationships_tab = QWidget() 69 | self.setup_relationships_tab() 70 | self.tab_widget.addTab(self.relationships_tab, "角色关系") 71 | 72 | layout.addWidget(self.tab_widget) 73 | 74 | # 底部按钮 75 | button_layout = QHBoxLayout() 76 | 77 | self.refresh_button = QPushButton("刷新") 78 | self.refresh_button.clicked.connect(self.refresh_all_data) 79 | button_layout.addWidget(self.refresh_button) 80 | 81 | layout.addLayout(button_layout) 82 | 83 | def setup_chapters_tab(self): 84 | """设置章节标签页""" 85 | layout = QVBoxLayout(self.chapters_tab) 86 | 87 | # 工具栏 88 | toolbar = QHBoxLayout() 89 | 90 | self.add_chapter_button = QPushButton("新增章节") 91 | self.add_chapter_button.clicked.connect(self.add_chapter) 92 | toolbar.addWidget(self.add_chapter_button) 93 | 94 | self.save_chapters_button = QPushButton("保存修改") 95 | self.save_chapters_button.clicked.connect(lambda: self.save_changes('chapters')) 96 | self.save_chapters_button.setEnabled(False) 97 | toolbar.addWidget(self.save_chapters_button) 98 | 99 | self.delete_chapter_button = QPushButton("删除章节") 100 | self.delete_chapter_button.clicked.connect(self.delete_chapter) 101 | toolbar.addWidget(self.delete_chapter_button) 102 | 103 | toolbar.addStretch() 104 | 105 | layout.addLayout(toolbar) 106 | 107 | # 章节表格 108 | self.chapters_table = QTableWidget() 109 | self.chapters_table.itemChanged.connect(lambda item: self.on_data_changed(item, 'chapters')) 110 | layout.addWidget(self.chapters_table) 111 | 112 | def setup_characters_tab(self): 113 | """设置角色标签页""" 114 | layout = QVBoxLayout(self.characters_tab) 115 | 116 | # 工具栏 117 | toolbar = QHBoxLayout() 118 | 119 | self.add_character_button = QPushButton("新增角色") 120 | self.add_character_button.clicked.connect(self.add_character) 121 | toolbar.addWidget(self.add_character_button) 122 | 123 | self.save_characters_button = QPushButton("保存修改") 124 | self.save_characters_button.clicked.connect(lambda: self.save_changes('characters')) 125 | self.save_characters_button.setEnabled(False) 126 | toolbar.addWidget(self.save_characters_button) 127 | 128 | self.delete_character_button = QPushButton("删除角色") 129 | self.delete_character_button.clicked.connect(self.delete_character) 130 | toolbar.addWidget(self.delete_character_button) 131 | 132 | toolbar.addStretch() 133 | 134 | layout.addLayout(toolbar) 135 | 136 | # 角色表格 137 | self.characters_table = QTableWidget() 138 | self.characters_table.itemChanged.connect(lambda item: self.on_data_changed(item, 'characters')) 139 | layout.addWidget(self.characters_table) 140 | 141 | def setup_relationships_tab(self): 142 | """设置角色关系���签页""" 143 | layout = QVBoxLayout(self.relationships_tab) 144 | 145 | # 工具栏 146 | toolbar = QHBoxLayout() 147 | 148 | self.add_relationship_button = QPushButton("新增关系") 149 | self.add_relationship_button.clicked.connect(self.add_relationship) 150 | toolbar.addWidget(self.add_relationship_button) 151 | 152 | self.save_character_relationships_button = QPushButton("保存修改") 153 | self.save_character_relationships_button.clicked.connect( 154 | lambda: self.save_changes('character_relationships') 155 | ) 156 | self.save_character_relationships_button.setEnabled(False) 157 | toolbar.addWidget(self.save_character_relationships_button) 158 | 159 | self.delete_relationship_button = QPushButton("删除关系") 160 | self.delete_relationship_button.clicked.connect(self.delete_relationship) 161 | toolbar.addWidget(self.delete_relationship_button) 162 | 163 | toolbar.addStretch() 164 | 165 | layout.addLayout(toolbar) 166 | 167 | # 关系表格 168 | self.character_relationships_table = QTableWidget() 169 | self.character_relationships_table.itemChanged.connect( 170 | lambda item: self.on_data_changed(item, 'character_relationships') 171 | ) 172 | layout.addWidget(self.character_relationships_table) 173 | 174 | def refresh_novel_list(self): 175 | """刷新小说列表""" 176 | try: 177 | # 获取所有小说 178 | query = "SELECT id, title FROM novels ORDER BY id" 179 | result = self.db_manager.execute_query(query) 180 | 181 | # 清空并重新填充下拉框 182 | self.novel_combo.clear() 183 | self.novel_combo.addItem("请选择小说...", None) 184 | 185 | for novel_id, title in result: 186 | self.novel_combo.addItem(title, novel_id) 187 | 188 | except Exception as e: 189 | error_msg = f"加载小说列表失败: {str(e)}" 190 | logging.error(error_msg) 191 | QMessageBox.critical(self, "错误", error_msg) 192 | 193 | def on_novel_selected(self, index): 194 | """小说选择变更处理""" 195 | self.current_novel_id = self.novel_combo.currentData() 196 | self.load_all_data() 197 | 198 | # 启用/禁用按钮 199 | has_novel = self.current_novel_id is not None 200 | self.delete_novel_button.setEnabled(has_novel) 201 | self.add_chapter_button.setEnabled(has_novel) 202 | self.add_character_button.setEnabled(has_novel) 203 | self.add_relationship_button.setEnabled(has_novel) 204 | 205 | def load_all_data(self): 206 | """加载所有数据""" 207 | logging.info("开始加载所有数据...") 208 | if self.current_novel_id is None: 209 | logging.info("当前未选择小说,清空所有表格") 210 | # 清空所有表格 211 | self.chapters_table.setRowCount(0) 212 | self.characters_table.setRowCount(0) 213 | self.character_relationships_table.setRowCount(0) 214 | return 215 | 216 | logging.info(f"当前选中小说ID: {self.current_novel_id}") 217 | self.load_chapters_data() 218 | self.load_characters_data() 219 | self.load_relationships_data() 220 | logging.info("所有数据加载完成") 221 | 222 | def load_chapters_data(self): 223 | """加载章节数据""" 224 | try: 225 | # 获取表结构 226 | columns = self.db_manager.get_table_structure('chapters') 227 | 228 | # 设置表格 229 | self.chapters_table.setColumnCount(len(columns)) 230 | self.chapters_table.setHorizontalHeaderLabels(columns) 231 | 232 | # 获取数据 233 | query = "SELECT * FROM chapters WHERE novel_id = ? ORDER BY chapter_number" 234 | result = self.db_manager.execute_query(query, (self.current_novel_id,)) 235 | 236 | # 填充数据 237 | self.chapters_table.setRowCount(0) 238 | for row_data in result: 239 | row = self.chapters_table.rowCount() 240 | self.chapters_table.insertRow(row) 241 | for col, value in enumerate(row_data): 242 | item = QTableWidgetItem(str(value) if value is not None else '') 243 | if col in [0, 1]: # ID和novel_id列不可编辑 244 | item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable) 245 | self.chapters_table.setItem(row, col, item) 246 | 247 | # 调整列宽 248 | self.chapters_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) 249 | 250 | except Exception as e: 251 | error_msg = f"加载章节数据失败: {str(e)}" 252 | logging.error(error_msg) 253 | QMessageBox.critical(self, "错误", error_msg) 254 | 255 | def load_characters_data(self): 256 | """加载角色数据""" 257 | try: 258 | # 获取表结构 259 | columns = self.db_manager.get_table_structure('characters') 260 | 261 | # 设置表格 262 | self.characters_table.setColumnCount(len(columns)) 263 | self.characters_table.setHorizontalHeaderLabels(columns) 264 | 265 | # 获取数据 266 | query = "SELECT * FROM characters WHERE novel_id = ? ORDER BY id" 267 | result = self.db_manager.execute_query(query, (self.current_novel_id,)) 268 | 269 | # 填充数据 270 | self.characters_table.setRowCount(0) 271 | for row_data in result: 272 | row = self.characters_table.rowCount() 273 | self.characters_table.insertRow(row) 274 | for col, value in enumerate(row_data): 275 | item = QTableWidgetItem(str(value) if value is not None else '') 276 | if col in [0, 1]: # ID和novel_id列不可编辑 277 | item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable) 278 | self.characters_table.setItem(row, col, item) 279 | 280 | # 调整列宽 281 | self.characters_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) 282 | 283 | except Exception as e: 284 | error_msg = f"加载角色数据失败: {str(e)}" 285 | logging.error(error_msg) 286 | QMessageBox.critical(self, "错误", error_msg) 287 | 288 | def load_relationships_data(self): 289 | """加载角色关系数据""" 290 | try: 291 | logging.info("开始加载角色关系数据...") 292 | if not self.current_novel_id: 293 | logging.info("当前未选择小说,清空关系表格") 294 | self.character_relationships_table.setRowCount(0) 295 | return 296 | 297 | logging.info(f"创建Character模型实例,小说ID={self.current_novel_id}") 298 | # 使用Character模型获取关系数据 299 | character_model = Character(self.db_manager) 300 | logging.info("开始调用get_character_relationships_for_novel方法") 301 | relationships = character_model.get_character_relationships_for_novel(self.current_novel_id) 302 | logging.info(f"获取到{len(relationships) if relationships else 0}个关系数据") 303 | 304 | # 设置表格列 305 | headers = ["ID", "角色1", "角色2", "关系类型", "描述", "起始章节"] 306 | self.character_relationships_table.setColumnCount(len(headers)) 307 | self.character_relationships_table.setHorizontalHeaderLabels(headers) 308 | 309 | # 填充数据 310 | self.character_relationships_table.setRowCount(0) 311 | if relationships: 312 | for relation in relationships: 313 | row = self.character_relationships_table.rowCount() 314 | self.character_relationships_table.insertRow(row) 315 | 316 | # 填充各列数据 317 | items = [ 318 | str(relation['id']), # ID 319 | relation['character1_name'], # 角色1名称 320 | relation['character2_name'], # 角色2名称 321 | relation['relationship_type'], # 关系类型 322 | relation['description'] or "", # 描述 323 | relation['start_chapter'] or "" # 起始章节 324 | ] 325 | 326 | for col, item_text in enumerate(items): 327 | item = QTableWidgetItem(str(item_text)) 328 | if col in [0, 1, 2]: # ID和角色名称列不可编辑 329 | item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable) 330 | self.character_relationships_table.setItem(row, col, item) 331 | 332 | logging.info("关系数据填充完成") 333 | else: 334 | logging.info("没有找到任何关系数据") 335 | 336 | # 调整列宽 337 | self.character_relationships_table.horizontalHeader().setSectionResizeMode( 338 | QHeaderView.ResizeMode.ResizeToContents 339 | ) 340 | logging.info("角色关系数据加载完成") 341 | 342 | except Exception as e: 343 | error_msg = f"加载角色关系数据失败: {str(e)}" 344 | logging.error(error_msg) 345 | logging.exception("详细错误信息:") 346 | QMessageBox.critical(self, "错误", error_msg) 347 | 348 | def on_data_changed(self, item, table_name): 349 | """数据修改处理""" 350 | if not item or not self.current_novel_id: 351 | return 352 | 353 | try: 354 | row = item.row() 355 | col = item.column() 356 | new_value = item.text() 357 | 358 | # 获取该行的ID(第一列) 359 | table_widget = None 360 | if table_name == 'chapters': 361 | table_widget = self.chapters_table 362 | elif table_name == 'characters': 363 | table_widget = self.characters_table 364 | elif table_name == 'character_relationships': 365 | table_widget = self.character_relationships_table 366 | else: 367 | return 368 | 369 | id_item = table_widget.item(row, 0) 370 | if not id_item: 371 | return 372 | 373 | record_id = int(id_item.text()) 374 | display_column_name = table_widget.horizontalHeaderItem(col).text() 375 | 376 | # 表头显示名称到数据库字段名的映射 377 | column_name_mapping = { 378 | # 章节表 379 | 'id': 'id', 380 | 'novel_id': 'novel_id', 381 | 'chapter_number': 'chapter_number', 382 | 'title': 'title', 383 | 'content': 'content', 384 | 'summary': 'summary', 385 | 'outline': 'outline', 386 | 'created_at': 'created_at', 387 | 388 | # 角色表 389 | 'name': 'name', 390 | 'description': 'description', 391 | 'characteristics': 'characteristics', 392 | 'role_type': 'role_type', 393 | 'first_appearance': 'first_appearance', 394 | 'status': 'status', 395 | 396 | # 角色关系表 397 | 'ID': 'id', 398 | '角色1': 'character1_id', 399 | '角色2': 'character2_id', 400 | '关系类型': 'relationship_type', 401 | '描述': 'description', 402 | '起始章节': 'start_chapter' 403 | } 404 | 405 | # 获取实际的数据库字段名 406 | column_name = column_name_mapping.get(display_column_name) 407 | if not column_name: 408 | logging.warning(f"未找到字段名映射: {display_column_name}") 409 | return 410 | 411 | # 对于角色关系表的特殊处理 412 | if table_name == 'character_relationships': 413 | # 只允许修改关系类型和描述 414 | if column_name not in ['relationship_type', 'description']: 415 | return 416 | 417 | # 记录修改 418 | if table_name not in self.modified_data: 419 | self.modified_data[table_name] = {} 420 | if record_id not in self.modified_data[table_name]: 421 | self.modified_data[table_name][record_id] = {} 422 | 423 | self.modified_data[table_name][record_id][column_name] = new_value 424 | 425 | # 启用相应的保存按钮 426 | if table_name == 'chapters': 427 | self.save_chapters_button.setEnabled(True) 428 | elif table_name == 'characters': 429 | self.save_characters_button.setEnabled(True) 430 | elif table_name == 'character_relationships': 431 | self.save_character_relationships_button.setEnabled(True) 432 | 433 | logging.info(f"数据已修改: 表={table_name}, ID={record_id}, {column_name}={new_value}") 434 | 435 | except Exception as e: 436 | error_msg = f"处理数据修改失败: {str(e)}" 437 | logging.error(error_msg) 438 | QMessageBox.critical(self, "错误", error_msg) 439 | 440 | def save_changes(self, table_name): 441 | """保存修改""" 442 | try: 443 | if table_name not in self.modified_data or not self.modified_data[table_name]: 444 | return 445 | 446 | # 确认保存 447 | reply = QMessageBox.question( 448 | self, 449 | "确认保存", 450 | f"确定要保存对{table_name}表的修改吗?", 451 | QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No 452 | ) 453 | 454 | if reply != QMessageBox.StandardButton.Yes: 455 | return 456 | 457 | # 保存修改 458 | for record_id, changes in self.modified_data[table_name].items(): 459 | self.db_manager.update_record(table_name, record_id, changes) 460 | 461 | # 清除修改记录 462 | self.modified_data[table_name].clear() 463 | 464 | # 禁用保存按钮 465 | if table_name == 'chapters': 466 | self.save_chapters_button.setEnabled(False) 467 | elif table_name == 'characters': 468 | self.save_characters_button.setEnabled(False) 469 | elif table_name == 'character_relationships': 470 | self.save_character_relationships_button.setEnabled(False) 471 | 472 | # 重新加载数据 473 | self.load_all_data() 474 | 475 | QMessageBox.information(self, "成功", "修改已保存") 476 | 477 | except Exception as e: 478 | error_msg = f"保存修改失败: {str(e)}" 479 | logging.error(error_msg) 480 | QMessageBox.critical(self, "错误", error_msg) 481 | 482 | def add_novel(self): 483 | """添加新小说""" 484 | try: 485 | title, ok = QMessageBox.getText( 486 | self, 487 | "新增小说", 488 | "请输入小说标题:" 489 | ) 490 | 491 | if ok and title: 492 | # 检查是否已存在同名小说 493 | query = "SELECT 1 FROM novels WHERE title = ?" 494 | if self.db_manager.execute_query(query, (title,)): 495 | raise ValueError("已存在同名小说") 496 | 497 | # 插入新小说 498 | query = "INSERT INTO novels (title) VALUES (?)" 499 | result = self.db_manager.execute_query(query, (title,)) 500 | 501 | if result: 502 | self.refresh_novel_list() 503 | QMessageBox.information(self, "成功", "小说创建成功") 504 | 505 | except Exception as e: 506 | error_msg = f"创建小说失败: {str(e)}" 507 | logging.error(error_msg) 508 | QMessageBox.critical(self, "错误", error_msg) 509 | 510 | def delete_novel(self): 511 | """删除当前小说""" 512 | try: 513 | if not self.current_novel_id: 514 | return 515 | 516 | # 确认删除 517 | reply = QMessageBox.question( 518 | self, 519 | "确认删除", 520 | "确定要删除当前小说吗?这将同时删除所有相关的章节、角色和关系数据!", 521 | QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No 522 | ) 523 | 524 | if reply != QMessageBox.StandardButton.Yes: 525 | return 526 | 527 | # 删除小说及相关数据 528 | self.db_manager.delete_record('novels', self.current_novel_id) 529 | 530 | # 刷新界面 531 | self.refresh_novel_list() 532 | self.current_novel_id = None 533 | self.load_all_data() 534 | 535 | QMessageBox.information(self, "成功", "小说已删除") 536 | 537 | except Exception as e: 538 | error_msg = f"删除小说失败: {str(e)}" 539 | logging.error(error_msg) 540 | QMessageBox.critical(self, "错误", error_msg) 541 | 542 | def add_chapter(self): 543 | """添加新章节""" 544 | try: 545 | if not self.current_novel_id: 546 | return 547 | 548 | # 获取当前最大章节号 549 | query = "SELECT MAX(chapter_number) FROM chapters WHERE novel_id = ?" 550 | result = self.db_manager.execute_query(query, (self.current_novel_id,)) 551 | max_chapter = result[0][0] if result and result[0][0] else 0 552 | 553 | # 创建新章节 554 | new_chapter = { 555 | 'novel_id': self.current_novel_id, 556 | 'chapter_number': max_chapter + 1, 557 | 'title': f'第{max_chapter + 1}章', 558 | 'content': '', 559 | 'summary': '' 560 | } 561 | 562 | self.db_manager.insert_record('chapters', new_chapter) 563 | 564 | # 刷新数据 565 | self.load_chapters_data() 566 | 567 | except Exception as e: 568 | error_msg = f"添加章节失败: {str(e)}" 569 | logging.error(error_msg) 570 | QMessageBox.critical(self, "错误", error_msg) 571 | 572 | def delete_chapter(self): 573 | """删除选中的章节""" 574 | self.delete_selected_rows('chapters', self.chapters_table) 575 | 576 | def add_character(self): 577 | """添加新角色""" 578 | try: 579 | if not self.current_novel_id: 580 | return 581 | 582 | # 创建新角色 583 | new_character = { 584 | 'novel_id': self.current_novel_id, 585 | 'name': '新角色', 586 | 'description': '', 587 | 'characteristics': '', 588 | 'role_type': '配角', 589 | 'status': '活跃' 590 | } 591 | 592 | self.db_manager.insert_record('characters', new_character) 593 | 594 | # 刷新数据 595 | self.load_characters_data() 596 | 597 | except Exception as e: 598 | error_msg = f"添加角色失败: {str(e)}" 599 | logging.error(error_msg) 600 | QMessageBox.critical(self, "错误", error_msg) 601 | 602 | def delete_character(self): 603 | """删除选中的角色""" 604 | self.delete_selected_rows('characters', self.characters_table) 605 | 606 | def add_relationship(self): 607 | """添加新角色关系""" 608 | try: 609 | if not self.current_novel_id: 610 | return 611 | 612 | # 获取当前小说的所有角色 613 | query = "SELECT id, name FROM characters WHERE novel_id = ? ORDER BY name" 614 | characters = self.db_manager.execute_query(query, (self.current_novel_id,)) 615 | 616 | if not characters or len(characters) < 2: 617 | QMessageBox.warning(self, "警告", "需要至少两个角色才能创建关系") 618 | return 619 | 620 | # 获取所有关系类型 621 | query = """ 622 | SELECT category, type, description 623 | FROM relationship_types 624 | ORDER BY category, type 625 | """ 626 | relationship_types = self.db_manager.execute_query(query) 627 | 628 | if not relationship_types: 629 | QMessageBox.warning(self, "警告", "未找到预定义的关系类型") 630 | return 631 | 632 | # 创建角色选择对话框 633 | dialog = QDialog(self) 634 | dialog.setWindowTitle("新增角色关系") 635 | dialog.setModal(True) 636 | dialog.setMinimumWidth(400) 637 | 638 | layout = QVBoxLayout(dialog) 639 | 640 | # 角色1选择 641 | char1_label = QLabel("选择角色1:") 642 | char1_combo = QComboBox() 643 | for char_id, char_name in characters: 644 | char1_combo.addItem(char_name, char_id) 645 | layout.addWidget(char1_label) 646 | layout.addWidget(char1_combo) 647 | 648 | # 角色2选择 649 | char2_label = QLabel("选择角色2:") 650 | char2_combo = QComboBox() 651 | for char_id, char_name in characters: 652 | char2_combo.addItem(char_name, char_id) 653 | layout.addWidget(char2_label) 654 | layout.addWidget(char2_combo) 655 | 656 | # 关系类型选择 657 | type_group = QGroupBox("关系类型") 658 | type_layout = QVBoxLayout() 659 | 660 | # 分类下拉框 661 | category_label = QLabel("选择分类:") 662 | category_combo = QComboBox() 663 | categories = sorted(set(t[0] for t in relationship_types)) 664 | for category in categories: 665 | category_combo.addItem(category) 666 | type_layout.addWidget(category_label) 667 | type_layout.addWidget(category_combo) 668 | 669 | # 关系类型下拉框 670 | type_label = QLabel("选择关系:") 671 | type_combo = QComboBox() 672 | type_combo.setMinimumWidth(200) 673 | 674 | # 关系描述标签 675 | desc_label = QLabel() 676 | desc_label.setWordWrap(True) 677 | desc_label.setStyleSheet("color: gray;") 678 | 679 | def update_types(): 680 | """更新关系类型下拉框""" 681 | type_combo.clear() 682 | current_category = category_combo.currentText() 683 | for cat, type_, desc in relationship_types: 684 | if cat == current_category: 685 | type_combo.addItem(type_) 686 | 687 | def update_description(): 688 | """更新关系描述""" 689 | current_type = type_combo.currentText() 690 | for cat, type_, desc in relationship_types: 691 | if type_ == current_type: 692 | desc_label.setText(desc) 693 | break 694 | 695 | category_combo.currentTextChanged.connect(update_types) 696 | type_combo.currentTextChanged.connect(update_description) 697 | 698 | type_layout.addWidget(type_label) 699 | type_layout.addWidget(type_combo) 700 | type_layout.addWidget(desc_label) 701 | 702 | type_group.setLayout(type_layout) 703 | layout.addWidget(type_group) 704 | 705 | # 自定义描述输入 706 | custom_desc_label = QLabel("补充描述:") 707 | custom_desc_edit = QLineEdit() 708 | layout.addWidget(custom_desc_label) 709 | layout.addWidget(custom_desc_edit) 710 | 711 | # 按钮 712 | button_box = QHBoxLayout() 713 | ok_button = QPushButton("确定") 714 | cancel_button = QPushButton("取消") 715 | button_box.addWidget(ok_button) 716 | button_box.addWidget(cancel_button) 717 | layout.addLayout(button_box) 718 | 719 | # 绑定按钮事件 720 | ok_button.clicked.connect(dialog.accept) 721 | cancel_button.clicked.connect(dialog.reject) 722 | 723 | # 初始化关系类型列表 724 | update_types() 725 | if type_combo.count() > 0: 726 | update_description() 727 | 728 | # 显示对话框 729 | if dialog.exec() == QDialog.DialogCode.Accepted: 730 | # 获取选择的值 731 | char1_id = char1_combo.currentData() 732 | char2_id = char2_combo.currentData() 733 | 734 | if char1_id == char2_id: 735 | QMessageBox.warning(self, "警告", "不能选择相同的角色") 736 | return 737 | 738 | # 创建新关系 739 | new_relationship = { 740 | 'novel_id': self.current_novel_id, 741 | 'character1_id': char1_id, 742 | 'character2_id': char2_id, 743 | 'relationship_type': type_combo.currentText(), 744 | 'description': custom_desc_edit.text(), 745 | 'start_chapter': None 746 | } 747 | 748 | self.db_manager.insert_record('character_relationships', new_relationship) 749 | 750 | # 刷新数据 751 | self.load_relationships_data() 752 | 753 | except Exception as e: 754 | error_msg = f"添加角色关系失败: {str(e)}" 755 | logging.error(error_msg) 756 | QMessageBox.critical(self, "错误", error_msg) 757 | 758 | def delete_relationship(self): 759 | """删除选中的角色关系""" 760 | try: 761 | selected_items = self.character_relationships_table.selectedItems() 762 | if not selected_items: 763 | return 764 | 765 | # 获取选中的行的ID 766 | selected_rows = set() 767 | for item in selected_items: 768 | row = item.row() 769 | id_item = self.character_relationships_table.item(row, 0) 770 | if id_item and id_item.text(): 771 | selected_rows.add((row, int(id_item.text()))) 772 | 773 | if not selected_rows: 774 | return 775 | 776 | # 确认删除 777 | reply = QMessageBox.question( 778 | self, 779 | "确认删除", 780 | f"确定要删除选中的 {len(selected_rows)} 条关系记录吗?", 781 | QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No 782 | ) 783 | 784 | if reply != QMessageBox.StandardButton.Yes: 785 | return 786 | 787 | # 删除记录 788 | for _, record_id in selected_rows: 789 | self.db_manager.delete_record('character_relationships', record_id) 790 | 791 | # 重新加载数据 792 | self.load_relationships_data() 793 | 794 | except Exception as e: 795 | error_msg = f"删除角色关系失败: {str(e)}" 796 | logging.error(error_msg) 797 | QMessageBox.critical(self, "错误", error_msg) 798 | 799 | def delete_selected_rows(self, table_name, table_widget): 800 | """删除选中的行""" 801 | try: 802 | selected_items = table_widget.selectedItems() 803 | if not selected_items: 804 | return 805 | 806 | # 获取选中的行的ID 807 | selected_rows = set() 808 | for item in selected_items: 809 | row = item.row() 810 | id_item = table_widget.item(row, 0) 811 | if id_item and id_item.text(): 812 | selected_rows.add((row, int(id_item.text()))) 813 | 814 | if not selected_rows: 815 | return 816 | 817 | # 确认删除 818 | reply = QMessageBox.question( 819 | self, 820 | "确认删除", 821 | f"确定要删除选中的 {len(selected_rows)} 条记录吗?", 822 | QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No 823 | ) 824 | 825 | if reply != QMessageBox.StandardButton.Yes: 826 | return 827 | 828 | # 删除记录 829 | for _, record_id in selected_rows: 830 | self.db_manager.delete_record(table_name, record_id) 831 | 832 | # 重新加载数据 833 | self.load_all_data() 834 | 835 | except Exception as e: 836 | error_msg = f"删除记录失败: {str(e)}" 837 | logging.error(error_msg) 838 | QMessageBox.critical(self, "错误", error_msg) 839 | 840 | def refresh_all_data(self): 841 | """刷新所有数据""" 842 | if any(self.modified_data.values()): 843 | reply = QMessageBox.question( 844 | self, 845 | "确认刷新", 846 | "有未保存的修改,确定要刷新吗?", 847 | QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No 848 | ) 849 | 850 | if reply != QMessageBox.StandardButton.Yes: 851 | return 852 | 853 | # 清除所有修改记录 854 | self.modified_data.clear() 855 | 856 | # 禁用所有保存按钮 857 | self.save_chapters_button.setEnabled(False) 858 | self.save_characters_button.setEnabled(False) 859 | self.save_character_relationships_button.setEnabled(False) 860 | 861 | # 刷新数据 862 | self.refresh_novel_list() 863 | self.load_all_data() 864 | 865 | @classmethod 866 | def show_dialog(cls, db_manager, parent=None): 867 | """显示数据库管理器对话框""" 868 | dialog = cls(db_manager, parent) 869 | dialog.exec() -------------------------------------------------------------------------------- /app/ui/dialogs/summary_generator.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtWidgets import ( 2 | QDialog, QVBoxLayout, QHBoxLayout, QTextEdit, 3 | QPushButton, QLabel, QProgressBar, QMessageBox, 4 | QCheckBox 5 | ) 6 | from PyQt6.QtCore import Qt, pyqtSignal 7 | import logging 8 | 9 | class SummaryGeneratorDialog(QDialog): 10 | """摘要生成对话框""" 11 | 12 | # 自定义信号 13 | summaryGenerated = pyqtSignal(str) # 摘要生成完成信号 14 | autoSummaryChanged = pyqtSignal(bool) # 自动摘要设置变更信号 15 | 16 | def __init__(self, summary_system, content: str, current_summary: str = "", parent=None): 17 | super().__init__(parent) 18 | self.summary_system = summary_system 19 | self.content = content 20 | self.generated_summary = "" 21 | self.init_ui() 22 | 23 | # 如果有现有摘要,显示在预览区域 24 | if current_summary: 25 | self.summary_preview.setPlainText(current_summary) 26 | self.generated_summary = current_summary 27 | self.apply_button.setEnabled(True) 28 | 29 | def init_ui(self): 30 | """初始化UI""" 31 | self.setWindowTitle("生成章节摘要") 32 | self.setMinimumSize(600, 400) 33 | 34 | layout = QVBoxLayout(self) 35 | 36 | # 原文预览区域 37 | content_label = QLabel("章节内容:") 38 | layout.addWidget(content_label) 39 | 40 | self.content_preview = QTextEdit() 41 | self.content_preview.setPlainText(self.content) 42 | self.content_preview.setReadOnly(True) 43 | self.content_preview.setMaximumHeight(150) 44 | layout.addWidget(self.content_preview) 45 | 46 | # 进度条 47 | self.progress_bar = QProgressBar() 48 | self.progress_bar.setTextVisible(False) 49 | self.progress_bar.hide() 50 | layout.addWidget(self.progress_bar) 51 | 52 | # 摘要预览区域 53 | summary_label = QLabel("摘要内容:") 54 | layout.addWidget(summary_label) 55 | 56 | self.summary_preview = QTextEdit() 57 | self.summary_preview.setPlaceholderText("在这里编辑或生成摘要...") 58 | layout.addWidget(self.summary_preview) 59 | 60 | # 自动生成选项 61 | self.auto_summary_check = QCheckBox("保存时自动生成摘要") 62 | self.auto_summary_check.stateChanged.connect(self._on_auto_summary_changed) 63 | layout.addWidget(self.auto_summary_check) 64 | 65 | # 按钮区域 66 | button_layout = QHBoxLayout() 67 | 68 | self.generate_button = QPushButton("生成摘要") 69 | self.generate_button.clicked.connect(self._on_generate) 70 | button_layout.addWidget(self.generate_button) 71 | 72 | self.apply_button = QPushButton("应用") 73 | self.apply_button.clicked.connect(self._on_apply) 74 | self.apply_button.setEnabled(False) 75 | button_layout.addWidget(self.apply_button) 76 | 77 | self.cancel_button = QPushButton("取消") 78 | self.cancel_button.clicked.connect(self.reject) 79 | button_layout.addWidget(self.cancel_button) 80 | 81 | layout.addLayout(button_layout) 82 | 83 | # 状态提示 84 | self.status_label = QLabel() 85 | layout.addWidget(self.status_label) 86 | 87 | # 连接摘要编辑信号 88 | self.summary_preview.textChanged.connect(self._on_summary_edited) 89 | 90 | def _on_generate(self): 91 | """生成摘要处理""" 92 | try: 93 | # 更新UI状态 94 | self.progress_bar.setRange(0, 0) 95 | self.progress_bar.show() 96 | self.generate_button.setEnabled(False) 97 | self.status_label.setText("正在分析章节内容...") 98 | 99 | # 生成摘要 100 | summary = self.summary_system.generate_chapter_summary(self.content) 101 | 102 | # 显示摘要 103 | self.summary_preview.setPlainText(summary) 104 | self.generated_summary = summary 105 | 106 | # 更新UI状态 107 | self.progress_bar.hide() 108 | self.apply_button.setEnabled(True) 109 | self.generate_button.setEnabled(True) 110 | self.status_label.setText("摘要生成完成") 111 | 112 | except Exception as e: 113 | QMessageBox.critical(self, "错误", f"生成摘要失败:{str(e)}") 114 | self.progress_bar.hide() 115 | self.generate_button.setEnabled(True) 116 | self.status_label.setText("生成失败") 117 | 118 | def _on_apply(self): 119 | """应用摘要""" 120 | summary = self.summary_preview.toPlainText().strip() 121 | if summary: 122 | self.generated_summary = summary 123 | self.summaryGenerated.emit(summary) 124 | self.accept() 125 | 126 | def _on_summary_edited(self): 127 | """摘要编辑处理""" 128 | # 只要有内容就启用应用按钮 129 | self.apply_button.setEnabled(bool(self.summary_preview.toPlainText().strip())) 130 | 131 | def _on_auto_summary_changed(self, state: int): 132 | """自动摘要设置变更处理""" 133 | self.autoSummaryChanged.emit(state == Qt.CheckState.Checked) 134 | 135 | @classmethod 136 | def generate_summary(cls, summary_system, content: str, current_summary: str = "", parent=None) -> str: 137 | """显示摘要生成对话框 138 | 139 | Args: 140 | summary_system: 摘要系统实例 141 | content: 章节内容 142 | current_summary: 当前摘要 143 | parent: 父窗口 144 | 145 | Returns: 146 | 生成的摘要,如果取消则返回空字符串 147 | """ 148 | dialog = cls(summary_system, content, current_summary, parent) 149 | result = dialog.exec() 150 | 151 | if result == QDialog.DialogCode.Accepted: 152 | return dialog.generated_summary 153 | return "" -------------------------------------------------------------------------------- /app/ui/widgets/chapter_list.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtWidgets import ( 2 | QWidget, QVBoxLayout, QListWidget, QListWidgetItem, 3 | QPushButton, QHBoxLayout, QInputDialog, QMessageBox, 4 | QMenu 5 | ) 6 | from PyQt6.QtCore import pyqtSignal, Qt 7 | 8 | class ChapterList(QWidget): 9 | """章节列表组件""" 10 | 11 | # 自定义信号 12 | chapterSelected = pyqtSignal(int) # 章节选择信号,参数为章节ID 13 | chapterCreated = pyqtSignal(int) # 章节创建信号,参数为章节ID 14 | chapterDeleted = pyqtSignal(int) # 章节删除信号,参数为章节ID 15 | chapterRenamed = pyqtSignal(int, str) # 章节重命名信号,参数为章节ID和新标题 16 | 17 | def __init__(self, parent=None): 18 | super().__init__(parent) 19 | self.current_novel_id = None 20 | self.init_ui() 21 | 22 | def init_ui(self): 23 | """初始化UI""" 24 | layout = QVBoxLayout(self) 25 | 26 | # 创建按钮布局 27 | button_layout = QHBoxLayout() 28 | 29 | # 添加章节按钮 30 | self.add_button = QPushButton("添加章节") 31 | self.add_button.clicked.connect(self._on_add_chapter) 32 | button_layout.addWidget(self.add_button) 33 | 34 | # 删除章节按钮 35 | self.delete_button = QPushButton("删除章节") 36 | self.delete_button.clicked.connect(self._on_delete_chapter) 37 | button_layout.addWidget(self.delete_button) 38 | 39 | layout.addLayout(button_layout) 40 | 41 | # 创建章节列表 42 | self.chapter_list = QListWidget() 43 | self.chapter_list.currentItemChanged.connect(self._on_chapter_selected) 44 | self.chapter_list.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) 45 | self.chapter_list.customContextMenuRequested.connect(self._show_context_menu) 46 | layout.addWidget(self.chapter_list) 47 | 48 | # 初始状态下禁用按钮 49 | self.add_button.setEnabled(False) 50 | self.delete_button.setEnabled(False) 51 | 52 | def set_novel(self, novel_id: int, chapters: list): 53 | """设置当前小说 54 | 55 | Args: 56 | novel_id: 小说ID 57 | chapters: 章节列表,每个元素应该包含 id 和 title 58 | """ 59 | self.current_novel_id = novel_id 60 | self.add_button.setEnabled(True) 61 | self.chapter_list.clear() 62 | 63 | for chapter in chapters: 64 | item = QListWidgetItem(f"{chapter['title']}") 65 | item.setData(Qt.ItemDataRole.UserRole, { 66 | 'id': chapter['id'], 67 | 'chapter_number': chapter['chapter_number'] 68 | }) 69 | self.chapter_list.addItem(item) 70 | 71 | def _show_context_menu(self, position): 72 | """显示上下文菜单""" 73 | item = self.chapter_list.itemAt(position) 74 | if not item: 75 | return 76 | 77 | menu = QMenu() 78 | rename_action = menu.addAction("重命名") 79 | action = menu.exec(self.chapter_list.mapToGlobal(position)) 80 | 81 | if action == rename_action: 82 | self._rename_chapter(item) 83 | 84 | def _rename_chapter(self, item: QListWidgetItem): 85 | """重命名章节""" 86 | chapter_data = item.data(Qt.ItemDataRole.UserRole) 87 | old_title = item.text() 88 | 89 | title, ok = QInputDialog.getText( 90 | self, 91 | "重命名章节", 92 | "请输入新的章节标题:", 93 | text=old_title 94 | ) 95 | 96 | if ok and title and title != old_title: 97 | # 发送重命名信号 98 | self.chapterRenamed.emit(chapter_data['id'], title) 99 | # 更新列表项 100 | item.setText(title) 101 | 102 | def clear_novel(self): 103 | """清除当前小说""" 104 | self.current_novel_id = None 105 | self.add_button.setEnabled(False) 106 | self.delete_button.setEnabled(False) 107 | self.chapter_list.clear() 108 | 109 | def _on_chapter_selected(self, current: QListWidgetItem, previous: QListWidgetItem): 110 | """章节选择处理""" 111 | self.delete_button.setEnabled(current is not None) 112 | if current: 113 | chapter_data = current.data(Qt.ItemDataRole.UserRole) 114 | self.chapterSelected.emit(chapter_data['id']) 115 | 116 | def _on_add_chapter(self): 117 | """添加章节处理""" 118 | if not self.current_novel_id: 119 | return 120 | 121 | title, ok = QInputDialog.getText( 122 | self, 123 | "添加章节", 124 | "请输入章节标题:" 125 | ) 126 | 127 | if ok and title: 128 | # 发送创建信号,让父组件处理创建逻辑 129 | self.chapterCreated.emit(self.current_novel_id) 130 | 131 | def _on_delete_chapter(self): 132 | """删除章节处理""" 133 | current_item = self.chapter_list.currentItem() 134 | if not current_item: 135 | return 136 | 137 | chapter_data = current_item.data(Qt.ItemDataRole.UserRole) 138 | reply = QMessageBox.question( 139 | self, 140 | "确认删除", 141 | f"确定要删除章节 '{current_item.text()}' 吗?", 142 | QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No 143 | ) 144 | 145 | if reply == QMessageBox.StandardButton.Yes: 146 | # 发送删除信号,让父组件处理删除逻辑 147 | self.chapterDeleted.emit(chapter_data['id']) 148 | 149 | def select_chapter(self, chapter_id: int): 150 | """选择指定章节 151 | 152 | Args: 153 | chapter_id: 要选择的章节ID 154 | """ 155 | for i in range(self.chapter_list.count()): 156 | item = self.chapter_list.item(i) 157 | chapter_data = item.data(Qt.ItemDataRole.UserRole) 158 | if chapter_data['id'] == chapter_id: 159 | self.chapter_list.setCurrentItem(item) 160 | break -------------------------------------------------------------------------------- /app/ui/widgets/character_list.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtWidgets import ( 2 | QWidget, QVBoxLayout, QHBoxLayout, QPushButton, 3 | QListWidget, QListWidgetItem, QMenu, QMessageBox, 4 | QInputDialog 5 | ) 6 | from PyQt6.QtCore import pyqtSignal, Qt 7 | 8 | class CharacterList(QWidget): 9 | """角色列表组件""" 10 | 11 | # 自定义信号 12 | characterSelected = pyqtSignal(int) # 角色选择信号 13 | characterCreated = pyqtSignal(int) # 角色创建信号 14 | characterDeleted = pyqtSignal(int) # 角色删除信号 15 | characterEdited = pyqtSignal(int) # 角色编辑信号 16 | 17 | def __init__(self, parent=None): 18 | super().__init__(parent) 19 | self.current_novel_id = None 20 | self.init_ui() 21 | 22 | def init_ui(self): 23 | """初始化UI""" 24 | layout = QVBoxLayout(self) 25 | 26 | # 创建工具栏 27 | toolbar = QHBoxLayout() 28 | 29 | # 添加角色按钮 30 | self.add_button = QPushButton("添加角色") 31 | self.add_button.clicked.connect(self._on_add_character) 32 | toolbar.addWidget(self.add_button) 33 | 34 | toolbar.addStretch() 35 | layout.addLayout(toolbar) 36 | 37 | # 创建角��列表 38 | self.list_widget = QListWidget() 39 | self.list_widget.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) 40 | self.list_widget.customContextMenuRequested.connect(self._show_context_menu) 41 | self.list_widget.itemClicked.connect(self._on_character_selected) 42 | layout.addWidget(self.list_widget) 43 | 44 | def set_novel(self, novel_id: int, characters: list): 45 | """设置当前小说和角色列表 46 | 47 | Args: 48 | novel_id: 小说ID 49 | characters: 角色列表 50 | """ 51 | self.current_novel_id = novel_id 52 | self.list_widget.clear() 53 | 54 | for char in characters: 55 | item = QListWidgetItem(char['name']) 56 | item.setData(Qt.ItemDataRole.UserRole, char['id']) 57 | self.list_widget.addItem(item) 58 | 59 | def clear_novel(self): 60 | """清空当前小说""" 61 | self.current_novel_id = None 62 | self.list_widget.clear() 63 | 64 | def _on_add_character(self): 65 | """添加角色处理""" 66 | if not self.current_novel_id: 67 | QMessageBox.warning(self, "警告", "请先创建或打开小说") 68 | return 69 | 70 | name, ok = QInputDialog.getText( 71 | self, 72 | "添加角色", 73 | "请输入角色名称:" 74 | ) 75 | 76 | if ok and name: 77 | self.characterCreated.emit(self.current_novel_id) 78 | 79 | def _show_context_menu(self, pos): 80 | """显示上下文菜单""" 81 | item = self.list_widget.itemAt(pos) 82 | if not item: 83 | return 84 | 85 | menu = QMenu(self) 86 | edit_action = menu.addAction("编辑") 87 | delete_action = menu.addAction("删除") 88 | 89 | action = menu.exec(self.list_widget.mapToGlobal(pos)) 90 | 91 | if action == edit_action: 92 | self._on_edit_character(item) 93 | elif action == delete_action: 94 | self._on_delete_character(item) 95 | 96 | def _on_edit_character(self, item): 97 | """编辑角色处理""" 98 | character_id = item.data(Qt.ItemDataRole.UserRole) 99 | self.characterEdited.emit(character_id) 100 | 101 | def _on_delete_character(self, item): 102 | """删除角色处理""" 103 | character_id = item.data(Qt.ItemDataRole.UserRole) 104 | reply = QMessageBox.question( 105 | self, 106 | "确认删除", 107 | f"确定要删除角色\"{item.text()}\"吗?", 108 | QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No 109 | ) 110 | 111 | if reply == QMessageBox.StandardButton.Yes: 112 | self.characterDeleted.emit(character_id) 113 | 114 | def _on_character_selected(self, item): 115 | """角色选择处理""" 116 | character_id = item.data(Qt.ItemDataRole.UserRole) 117 | self.characterSelected.emit(character_id) -------------------------------------------------------------------------------- /app/ui/widgets/editor.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtWidgets import QTextEdit 2 | from PyQt6.QtCore import QTimer, pyqtSignal 3 | import logging 4 | 5 | class Editor(QTextEdit): 6 | """自定义编辑器组件""" 7 | 8 | # 自定义信号 9 | contentChanged = pyqtSignal(str) # 内容变更信号 10 | saveRequested = pyqtSignal(str) # 保存请求信号 11 | 12 | def __init__(self, parent=None): 13 | super().__init__(parent) 14 | 15 | # 初始化自动保存定时器 16 | self._auto_save_timer = QTimer(self) 17 | self._auto_save_timer.setInterval(30000) # 30秒自动保存 18 | self._auto_save_timer.timeout.connect(self._auto_save) 19 | 20 | # 连接信号 21 | self.textChanged.connect(self._on_text_changed) 22 | 23 | # 设置占位符文本 24 | self.setPlaceholderText("在这里开始写作...") 25 | 26 | # 状态标记 27 | self._content_modified = False 28 | 29 | def _on_text_changed(self): 30 | """文本变更处理""" 31 | self._content_modified = True 32 | content = self.toPlainText() 33 | self.contentChanged.emit(content) 34 | 35 | # 重启自动保存定时器 36 | self._auto_save_timer.start() 37 | 38 | def _auto_save(self): 39 | """自动保存""" 40 | if self._content_modified: 41 | content = self.toPlainText() 42 | self.saveRequested.emit(content) 43 | self._content_modified = False 44 | logging.info("编辑器内容已自动保存") 45 | 46 | def load_content(self, content: str): 47 | """加载内容 48 | 49 | Args: 50 | content: 要加载的内容 51 | """ 52 | # 暂时断开信号连接,避免触发变更 53 | self.blockSignals(True) 54 | self.setPlainText(content) 55 | self.blockSignals(False) 56 | self._content_modified = False 57 | 58 | def save_content(self) -> str: 59 | """保存内容 60 | 61 | Returns: 62 | 当前内容 63 | """ 64 | content = self.toPlainText() 65 | self.saveRequested.emit(content) 66 | self._content_modified = False 67 | return content 68 | 69 | def is_modified(self) -> bool: 70 | """检查内容是否被修改 71 | 72 | Returns: 73 | 是否被修改 74 | """ 75 | return self._content_modified 76 | 77 | def clear_modified(self): 78 | """清除修改标记""" 79 | self._content_modified = False -------------------------------------------------------------------------------- /app/ui/widgets/outline_editor.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtWidgets import ( 2 | QWidget, QVBoxLayout, QTextEdit, QHBoxLayout, 3 | QPushButton, QLabel, QMessageBox, QComboBox 4 | ) 5 | from PyQt6.QtCore import pyqtSignal 6 | 7 | class OutlineEditor(QWidget): 8 | """大纲编辑器组件""" 9 | 10 | # 自定义信号 11 | outlineChanged = pyqtSignal(str) # 大纲内容变更信号 12 | generateRequested = pyqtSignal() # AI生成请求信号 13 | 14 | def __init__(self, parent=None): 15 | super().__init__(parent) 16 | self._modified = False 17 | self.init_ui() 18 | 19 | def init_ui(self): 20 | """初始化UI""" 21 | layout = QVBoxLayout(self) 22 | 23 | # 创建工具栏 24 | toolbar = QHBoxLayout() 25 | 26 | # 大纲类型选择 27 | self.outline_type = QComboBox() 28 | self.outline_type.addItems(["小说大纲", "章节大纲"]) 29 | toolbar.addWidget(self.outline_type) 30 | 31 | # 保存按钮 32 | self.save_button = QPushButton("保存大纲") 33 | self.save_button.clicked.connect(self._on_save) 34 | self.save_button.setEnabled(False) 35 | toolbar.addWidget(self.save_button) 36 | 37 | # AI生成按钮 38 | self.generate_button = QPushButton("AI生成") 39 | self.generate_button.clicked.connect(self._on_generate) 40 | toolbar.addWidget(self.generate_button) 41 | 42 | toolbar.addStretch() 43 | layout.addLayout(toolbar) 44 | 45 | # 创建编辑区域 46 | self.editor = QTextEdit() 47 | self.editor.setPlaceholderText("在这里编写大纲...") 48 | self.editor.textChanged.connect(self._on_text_changed) 49 | layout.addWidget(self.editor) 50 | 51 | # 创建提示标签 52 | self.hint_label = QLabel() 53 | self.hint_label.setStyleSheet("color: gray;") 54 | layout.addWidget(self.hint_label) 55 | 56 | # 连接大纲类型变更信号 57 | self.outline_type.currentIndexChanged.connect(self._update_hint) 58 | self._update_hint() 59 | 60 | def _update_hint(self): 61 | """更新提示信息""" 62 | if self.outline_type.currentText() == "小说大纲": 63 | self.hint_label.setText( 64 | "小说大纲提示:\n" 65 | "1. 描述小说的整体故事架构\n" 66 | "2. 设定主要人物及其发展轨迹\n" 67 | "3. 规划重要的情节转折点\n" 68 | "4. 确定故事的主题和中心思想" 69 | ) 70 | else: 71 | self.hint_label.setText( 72 | "章节大纲提示:\n" 73 | "1. 描述本章的主要内容和目标\n" 74 | "2. 列出关键场景和对话\n" 75 | "3. 说明与整体故事的关联\n" 76 | "4. 注明需要重点描写的细节" 77 | ) 78 | 79 | def _on_text_changed(self): 80 | """文本变更处理""" 81 | self._modified = True 82 | self.save_button.setEnabled(True) 83 | 84 | def _on_save(self): 85 | """保存处理""" 86 | content = self.editor.toPlainText().strip() 87 | self.outlineChanged.emit(content) 88 | self._modified = False 89 | self.save_button.setEnabled(False) 90 | 91 | def _on_generate(self): 92 | """AI生成处理""" 93 | self.generateRequested.emit() 94 | 95 | def set_content(self, content: str, is_chapter: bool = False): 96 | """设置内容 97 | 98 | Args: 99 | content: 大纲内容 100 | is_chapter: 是否为章节大纲 101 | """ 102 | self.outline_type.setCurrentText("章节大纲" if is_chapter else "小说大纲") 103 | self.editor.setPlainText(content or "") 104 | self._modified = False 105 | self.save_button.setEnabled(False) 106 | 107 | def get_content(self) -> str: 108 | """获取内容 109 | 110 | Returns: 111 | 当前大纲内容 112 | """ 113 | return self.editor.toPlainText().strip() 114 | 115 | def is_modified(self) -> bool: 116 | """是否已修改 117 | 118 | Returns: 119 | 是否已修改 120 | """ 121 | return self._modified 122 | 123 | def is_chapter_outline(self) -> bool: 124 | """是否为章节大纲 125 | 126 | Returns: 127 | 是否为章节大纲 128 | """ 129 | return self.outline_type.currentText() == "章节大纲" 130 | 131 | def clear(self): 132 | """清空内容""" 133 | self.editor.clear() 134 | self._modified = False 135 | self.save_button.setEnabled(False) -------------------------------------------------------------------------------- /app/ui/windows/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 窗口模块 3 | 包含所有窗口类 4 | """ -------------------------------------------------------------------------------- /app/ui/windows/main_window.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtWidgets import ( 2 | QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, 3 | QTreeView, QDockWidget, QMenuBar, QTextEdit, 4 | QStatusBar, QMessageBox, QFileDialog, QInputDialog 5 | ) 6 | from PyQt6.QtCore import Qt 7 | from app.models.novel import Novel 8 | from app.models.chapter import Chapter 9 | from app.core.generator import NovelGenerator 10 | from app.ui.widgets.editor import Editor 11 | from app.ui.widgets.chapter_list import ChapterList 12 | from app.ui.widgets.outline_editor import OutlineEditor 13 | from app.ui.widgets.character_list import CharacterList 14 | from app.ui.dialogs.content_generator import ContentGeneratorDialog 15 | from app.ui.dialogs.summary_generator import SummaryGeneratorDialog 16 | from app.core.summary import SummarySystem 17 | from app.ui.dialogs.character_editor import CharacterEditorDialog 18 | from app.models.character import Character 19 | from app.ui.dialogs.database_manager_dialog import DatabaseManagerDialog 20 | import logging 21 | 22 | class MainWindow(QMainWindow): 23 | def __init__(self, db_manager): 24 | super().__init__() 25 | self.db_manager = db_manager 26 | self.novel_model = Novel(db_manager) 27 | self.chapter_model = Chapter(db_manager) 28 | self.generator = NovelGenerator() 29 | self.summary_system = SummarySystem(db_manager, self.generator) 30 | self.character_model = Character(db_manager) 31 | 32 | self.current_novel_id = None 33 | self.current_chapter_id = None 34 | self.auto_summary = False # 自动摘要标志 35 | 36 | self.init_ui() 37 | 38 | def init_ui(self): 39 | """初始化UI""" 40 | # 设置窗口基本属性 41 | self.setWindowTitle('AI 小说生成器') 42 | self.setMinimumSize(1200, 800) 43 | 44 | # 创建中央部件 45 | central_widget = QWidget() 46 | self.setCentralWidget(central_widget) 47 | 48 | # 创建主布局 49 | layout = QHBoxLayout(central_widget) 50 | 51 | # 创建左侧面板(大纲和章节列表) 52 | self.create_left_panel() 53 | 54 | # 创建中央编辑区 55 | self.create_editor() 56 | layout.addWidget(self.editor) 57 | 58 | # 创建右侧面板(角色���摘要) 59 | self.create_right_panel() 60 | 61 | # 创建菜单栏 62 | self.create_menu_bar() 63 | 64 | # 创建状态栏 65 | self.statusBar = QStatusBar() 66 | self.setStatusBar(self.statusBar) 67 | self.statusBar.showMessage('就绪') 68 | 69 | def create_left_panel(self): 70 | """创建左侧面板""" 71 | # 创建大纲面板 72 | outline_dock = QDockWidget("小说大纲", self) 73 | outline_dock.setAllowedAreas(Qt.DockWidgetArea.LeftDockWidgetArea) 74 | 75 | # 创建大纲编辑器 76 | self.outline_editor = OutlineEditor() 77 | self.outline_editor.outlineChanged.connect(self._on_outline_changed) 78 | self.outline_editor.generateRequested.connect(self._on_generate_outline) 79 | outline_dock.setWidget(self.outline_editor) 80 | 81 | # 创建章节列表面板 82 | chapter_dock = QDockWidget("章节列表", self) 83 | chapter_dock.setAllowedAreas(Qt.DockWidgetArea.LeftDockWidgetArea) 84 | 85 | # 创建章节列表 86 | self.chapter_list = ChapterList() 87 | self.chapter_list.chapterSelected.connect(self._on_chapter_selected) 88 | self.chapter_list.chapterCreated.connect(self._on_chapter_created) 89 | self.chapter_list.chapterDeleted.connect(self._on_chapter_deleted) 90 | self.chapter_list.chapterRenamed.connect(self._on_chapter_renamed) 91 | chapter_dock.setWidget(self.chapter_list) 92 | 93 | # 添加到主窗口 94 | self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, outline_dock) 95 | self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, chapter_dock) 96 | 97 | def _on_outline_changed(self, content: str): 98 | """大纲内容变更处理""" 99 | try: 100 | if not self.current_novel_id: 101 | raise ValueError('请先创建或打开小说') 102 | 103 | if self.outline_editor.is_chapter_outline(): 104 | # 保存章节大纲 105 | if not self.current_chapter_id: 106 | raise ValueError('请先选择章节') 107 | self.chapter_model.update( 108 | self.current_chapter_id, 109 | outline=content 110 | ) 111 | self.statusBar.showMessage('章节大纲已保存') 112 | else: 113 | # 保存小说大纲 114 | self.novel_model.update( 115 | self.current_novel_id, 116 | outline=content 117 | ) 118 | self.statusBar.showMessage('小说大纲已保存') 119 | except Exception as e: 120 | QMessageBox.critical(self, '错误', f'保存大纲失败:{str(e)}') 121 | 122 | def _load_novel_outline(self): 123 | """加载小说大纲""" 124 | try: 125 | if self.current_novel_id: 126 | novel = self.novel_model.get(self.current_novel_id) 127 | if novel: 128 | self.outline_editor.set_content(novel.get('outline', ''), is_chapter=False) 129 | except Exception as e: 130 | QMessageBox.critical(self, '错误', f'加载大纲失败:{str(e)}') 131 | 132 | def _load_chapter_outline(self): 133 | """加载章节大纲""" 134 | try: 135 | if self.current_chapter_id: 136 | chapter = self.chapter_model.get(self.current_chapter_id) 137 | if chapter: 138 | self.outline_editor.set_content(chapter.get('outline', ''), is_chapter=True) 139 | except Exception as e: 140 | QMessageBox.critical(self, '错误', f'加载章节大纲失败:{str(e)}') 141 | 142 | def _on_chapter_selected(self, chapter_id: int): 143 | """章节选择处理""" 144 | try: 145 | # 如果当前有修改,先保存 146 | if self.editor.is_modified(): 147 | self.save_novel() 148 | 149 | # 加载选中的章节 150 | chapter = self.chapter_model.get(chapter_id) 151 | if chapter: 152 | self.current_chapter_id = chapter_id 153 | self.editor.load_content(chapter['content']) 154 | # 显示摘要 155 | self.summary_text.setPlainText(chapter['summary'] or "暂无摘要") 156 | # 加载章节大纲 157 | self._load_chapter_outline() 158 | self.statusBar.showMessage(f'当前章节:第{chapter["chapter_number"]}章 {chapter["title"]}') 159 | except Exception as e: 160 | QMessageBox.critical(self, '错误', f'加载章节失败:{str(e)}') 161 | 162 | def _on_chapter_created(self, novel_id: int): 163 | """章节创建处理""" 164 | try: 165 | # 获取当前章节数 166 | chapters = self.chapter_model.get_by_novel(novel_id) 167 | chapter_number = len(chapters) + 1 168 | 169 | # 创建新章节 170 | chapter_id = self.chapter_model.create( 171 | novel_id=novel_id, 172 | chapter_number=chapter_number, 173 | title=f'第{chapter_number}章', 174 | content='' 175 | ) 176 | 177 | # 刷新章节列表 178 | self._refresh_chapter_list() 179 | 180 | # 选择新创建的章节 181 | self.chapter_list.select_chapter(chapter_id) 182 | 183 | except Exception as e: 184 | QMessageBox.critical(self, '错误', f'创建章节失败:{str(e)}') 185 | 186 | def _on_chapter_deleted(self, chapter_id: int): 187 | """章节删除处理""" 188 | try: 189 | self.chapter_model.delete(chapter_id) 190 | self.current_chapter_id = None 191 | self.editor.clear() 192 | self.editor.clear_modified() 193 | 194 | # 刷新章节列表 195 | self._refresh_chapter_list() 196 | 197 | self.statusBar.showMessage('章节已删除') 198 | except Exception as e: 199 | QMessageBox.critical(self, '错误', f'删除章节失败:{str(e)}') 200 | 201 | def _refresh_chapter_list(self): 202 | """刷新章节列表""" 203 | if self.current_novel_id: 204 | chapters = self.chapter_model.get_by_novel(self.current_novel_id) 205 | self.chapter_list.set_novel(self.current_novel_id, chapters) 206 | 207 | def new_novel(self): 208 | """创建新小说""" 209 | try: 210 | title, ok = QInputDialog.getText( 211 | self, 212 | "新建小说", 213 | "请输入小说标题:" 214 | ) 215 | 216 | if ok and title: 217 | # 检查标题是否已存在 218 | existing_novel = self.novel_model.get_by_title(title) 219 | if existing_novel: 220 | raise ValueError(f'已存在同名小说:{title}') 221 | 222 | # 创建新小说 223 | novel_id = self.novel_model.create(title=title) 224 | self.current_novel_id = novel_id 225 | self.current_chapter_id = None 226 | 227 | # 清空编辑器 228 | self.editor.clear() 229 | self.editor.clear_modified() 230 | self.outline_editor.clear() 231 | 232 | # 刷新章节列表 233 | self._refresh_chapter_list() 234 | 235 | # 刷新角色列表 236 | self.character_list.clear_novel() 237 | 238 | self.statusBar.showMessage(f'创建新小说:{title}') 239 | 240 | except Exception as e: 241 | QMessageBox.critical(self, '错误', f'创建小说失败:{str(e)}') 242 | 243 | def open_novel(self): 244 | """打开小说""" 245 | try: 246 | # 获取所有小说列表 247 | novels = self.novel_model.list_all() 248 | if not novels: 249 | QMessageBox.information(self, "提示", "还没有创建任何小说") 250 | return 251 | 252 | # 创建选择话框 253 | items = [novel['title'] for novel in novels] 254 | title, ok = QInputDialog.getItem( 255 | self, 256 | "打开小说", 257 | "请选择要打开的小说:", 258 | items, 259 | 0, 260 | False 261 | ) 262 | 263 | if ok and title: 264 | # 获取小说信息 265 | novel = self.novel_model.get_by_title(title) 266 | if not novel: 267 | raise ValueError(f'找不到小说:{title}') 268 | 269 | self.current_novel_id = novel['id'] 270 | self.current_chapter_id = None 271 | 272 | # 清空编辑器 273 | self.editor.clear() 274 | self.editor.clear_modified() 275 | 276 | # 加载大纲 277 | self._load_novel_outline() 278 | 279 | # 刷新章节列表 280 | self._refresh_chapter_list() 281 | 282 | # 刷新角色列表 283 | self._refresh_character_list() 284 | 285 | self.statusBar.showMessage(f'打开小说:{title}') 286 | 287 | except Exception as e: 288 | QMessageBox.critical(self, '错误', f'打开小说失败:{str(e)}') 289 | 290 | def create_editor(self): 291 | """创建中央编辑器""" 292 | self.editor = Editor() 293 | # 连接编辑器信号 294 | self.editor.contentChanged.connect(self._on_editor_content_changed) 295 | self.editor.saveRequested.connect(self._on_editor_save_requested) 296 | 297 | def create_right_panel(self): 298 | """创建右侧面板""" 299 | # 创建角色面板 300 | character_dock = QDockWidget("角色", self) 301 | character_dock.setAllowedAreas(Qt.DockWidgetArea.RightDockWidgetArea) 302 | 303 | # 创建角色列表 304 | self.character_list = CharacterList() 305 | self.character_list.characterSelected.connect(self._on_character_selected) 306 | self.character_list.characterCreated.connect(self._on_character_created) 307 | self.character_list.characterDeleted.connect(self._on_character_deleted) 308 | self.character_list.characterEdited.connect(self._on_character_edited) 309 | character_dock.setWidget(self.character_list) 310 | 311 | # 创建摘要面板 312 | summary_dock = QDockWidget("摘要", self) 313 | summary_dock.setAllowedAreas(Qt.DockWidgetArea.RightDockWidgetArea) 314 | 315 | # 创建摘要显示 316 | self.summary_text = QTextEdit() 317 | self.summary_text.setReadOnly(True) 318 | summary_dock.setWidget(self.summary_text) 319 | 320 | # 添加到主窗口 321 | self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, character_dock) 322 | self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, summary_dock) 323 | 324 | def _refresh_character_list(self): 325 | """刷新角色列表""" 326 | if self.current_novel_id: 327 | characters = self.character_model.get_by_novel(self.current_novel_id) 328 | self.character_list.set_novel(self.current_novel_id, characters) 329 | 330 | def _on_character_selected(self, character_id: int): 331 | """角色选择处理""" 332 | try: 333 | character = self.character_model.get(character_id) 334 | if character: 335 | # TODO: 显示角色详细信息 336 | self.statusBar.showMessage(f'当前角色:{character["name"]}') 337 | except Exception as e: 338 | QMessageBox.critical(self, '错误', f'加载角色失败:{str(e)}') 339 | 340 | def _on_character_created(self, novel_id: int): 341 | """角色创建处理""" 342 | try: 343 | dialog = CharacterEditorDialog(parent=self) 344 | if dialog.exec(): 345 | character_data = dialog.get_character_data() 346 | character_data['novel_id'] = novel_id 347 | 348 | # 创建角色 349 | self.character_model.create(**character_data) 350 | 351 | # 刷新角色列表 352 | self._refresh_character_list() 353 | self.statusBar.showMessage('角色创建成功') 354 | 355 | except Exception as e: 356 | QMessageBox.critical(self, '错误', f'创建角色失败:{str(e)}') 357 | 358 | def _on_character_edited(self, character_id: int): 359 | """角色编辑处理""" 360 | try: 361 | character = self.character_model.get(character_id) 362 | if not character: 363 | raise ValueError('找不到角色') 364 | 365 | dialog = CharacterEditorDialog(character, parent=self) 366 | if dialog.exec(): 367 | character_data = dialog.get_character_data() 368 | 369 | # 更新角色 370 | self.character_model.update(character_id, **character_data) 371 | 372 | # 刷新角色列表 373 | self._refresh_character_list() 374 | self.statusBar.showMessage('角色更新成功') 375 | 376 | except Exception as e: 377 | QMessageBox.critical(self, '错误', f'编辑角色失败:{str(e)}') 378 | 379 | def _on_character_deleted(self, character_id: int): 380 | """角色删除处理""" 381 | try: 382 | self.character_model.delete(character_id) 383 | 384 | # 刷新角色列表 385 | self._refresh_character_list() 386 | self.statusBar.showMessage('角色已删除') 387 | 388 | except Exception as e: 389 | QMessageBox.critical(self, '错误', f'删除角色失败:{str(e)}') 390 | 391 | def create_menu_bar(self): 392 | """创建菜单栏""" 393 | menubar = self.menuBar() 394 | 395 | # 文件菜单 396 | file_menu = menubar.addMenu('文件') 397 | file_menu.addAction('新建', self.new_novel) 398 | file_menu.addAction('打开', self.open_novel) 399 | file_menu.addAction('保存', self.save_novel) 400 | file_menu.addSeparator() 401 | 402 | # 添加导出子菜单 403 | export_menu = file_menu.addMenu('导出') 404 | export_menu.addAction('导出当前章节', self.export_current_chapter) 405 | export_menu.addAction('导出整本小说', self.export_novel) 406 | export_menu.addAction('导出人物关系图', self.export_character_network) 407 | export_menu.addAction('导出章节大纲', self.export_outlines) 408 | 409 | file_menu.addSeparator() 410 | file_menu.addAction('退出', self.close) 411 | 412 | # 编辑菜单 413 | edit_menu = menubar.addMenu('编辑') 414 | edit_menu.addAction('生成内容', self.generate_content) 415 | edit_menu.addAction('更新摘要', self.update_summary) 416 | 417 | # 视图菜单 418 | view_menu = menubar.addMenu('视图') 419 | view_menu.addAction('版本历史', self.show_history) 420 | 421 | # 工具菜单 422 | tools_menu = menubar.addMenu('工具') 423 | tools_menu.addAction('数据库管理', self.show_database_manager) 424 | 425 | def show_database_manager(self): 426 | """显示数据库管理器""" 427 | try: 428 | DatabaseManagerDialog.show_dialog(self.db_manager, self) 429 | except Exception as e: 430 | error_msg = f"打开数据库管理器失败: {str(e)}" 431 | logging.error(error_msg) 432 | QMessageBox.critical(self, "错误", error_msg) 433 | 434 | def export_current_chapter(self): 435 | """导出当前章节""" 436 | try: 437 | if not self.current_chapter_id: 438 | raise ValueError('请先选择要导出的章节') 439 | 440 | chapter = self.chapter_model.get(self.current_chapter_id) 441 | if not chapter: 442 | raise ValueError('找不到章节信息') 443 | 444 | # 获取保存路径 445 | file_path, _ = QFileDialog.getSaveFileName( 446 | self, 447 | "导出章节", 448 | f"第{chapter['chapter_number']}章_{chapter['title']}.txt", 449 | "文本文件 (*.txt)" 450 | ) 451 | 452 | if file_path: 453 | # 构建导出内容 454 | content = [ 455 | f"第{chapter['chapter_number']}章 {chapter['title']}", 456 | "-" * 40, 457 | "章节大纲:", 458 | chapter.get('outline', '暂无大纲'), 459 | "-" * 40, 460 | "正文:", 461 | chapter['content'], 462 | "-" * 40, 463 | "摘要:", 464 | chapter.get('summary', '暂无摘要') 465 | ] 466 | 467 | # 写入文件 468 | with open(file_path, 'w', encoding='utf-8') as f: 469 | f.write('\n\n'.join(content)) 470 | 471 | self.statusBar.showMessage(f'章节已导出到:{file_path}') 472 | 473 | except Exception as e: 474 | QMessageBox.critical(self, '错误', f'导出章节失败:{str(e)}') 475 | 476 | def export_novel(self): 477 | """导出整本小说""" 478 | try: 479 | if not self.current_novel_id: 480 | raise ValueError('请先打开小说') 481 | 482 | novel = self.novel_model.get(self.current_novel_id) 483 | if not novel: 484 | raise ValueError('找不到小说信息') 485 | 486 | # 获取保存路径 487 | file_path, _ = QFileDialog.getSaveFileName( 488 | self, 489 | "导出小说", 490 | f"{novel['title']}.txt", 491 | "文本文件 (*.txt)" 492 | ) 493 | 494 | if file_path: 495 | # 获取所有章节 496 | chapters = self.chapter_model.get_by_novel(self.current_novel_id) 497 | 498 | # 构建导出内容 499 | content = [ 500 | novel['title'], 501 | "=" * 40, 502 | "小说大纲:", 503 | novel.get('outline', '暂无大纲'), 504 | "=" * 40, 505 | "" 506 | ] 507 | 508 | # 添加每个章节 509 | for chapter in chapters: 510 | content.extend([ 511 | f"第{chapter['chapter_number']}章 {chapter['title']}", 512 | "-" * 40, 513 | "章节大纲:", 514 | chapter.get('outline', '暂无大纲'), 515 | "-" * 40, 516 | chapter['content'], 517 | "\n" + "=" * 40 + "\n" 518 | ]) 519 | 520 | # 写入文件 521 | with open(file_path, 'w', encoding='utf-8') as f: 522 | f.write('\n\n'.join(content)) 523 | 524 | self.statusBar.showMessage(f'小说已导出到:{file_path}') 525 | 526 | except Exception as e: 527 | QMessageBox.critical(self, '错误', f'导出小说失败:{str(e)}') 528 | 529 | def export_character_network(self): 530 | """导出人物关系图""" 531 | try: 532 | if not self.current_novel_id: 533 | raise ValueError('请先打开小说') 534 | 535 | # 获取所有角色 536 | characters = self.character_model.get_by_novel(self.current_novel_id) 537 | if not characters: 538 | raise ValueError('当前小说没有角色信息') 539 | 540 | # 获取保存路径 541 | file_path, _ = QFileDialog.getSaveFileName( 542 | self, 543 | "导出人物关系图", 544 | "人物关系图.html", 545 | "HTML文件 (*.html)" 546 | ) 547 | 548 | if file_path: 549 | # 构建关系图数据 550 | nodes = [] 551 | for char in characters: 552 | nodes.append({ 553 | 'name': char['name'], 554 | 'role_type': char['role_type'], 555 | 'status': char['status'] 556 | }) 557 | 558 | # 使用 pyecharts 生成关系图 559 | from pyecharts import options as opts 560 | from pyecharts.charts import Graph 561 | 562 | # 创建图表 563 | graph = Graph() 564 | graph.add( 565 | "", 566 | [{'name': node['name'], 'symbolSize': 50} for node in nodes], 567 | [], # 暂时不添加关系连线 568 | categories=[ 569 | {'name': '主角'}, 570 | {'name': '配角'}, 571 | {'name': '反派'} 572 | ], 573 | layout='circular', 574 | is_rotate_label=True, 575 | label_opts=opts.LabelOpts(position='right'), 576 | ) 577 | graph.set_global_opts( 578 | title_opts=opts.TitleOpts(title="人物关系图") 579 | ) 580 | 581 | # 保存图表 582 | graph.render(file_path) 583 | self.statusBar.showMessage(f'人物关系图已导出到:{file_path}') 584 | 585 | except Exception as e: 586 | QMessageBox.critical(self, '错误', f'导出人物关系图失败:{str(e)}') 587 | 588 | def export_outlines(self): 589 | """导出章节大纲""" 590 | try: 591 | if not self.current_novel_id: 592 | raise ValueError('请先打开小说') 593 | 594 | novel = self.novel_model.get(self.current_novel_id) 595 | if not novel: 596 | raise ValueError('找不到小说信息') 597 | 598 | # 获取保存路径 599 | file_path, _ = QFileDialog.getSaveFileName( 600 | self, 601 | "导出大纲", 602 | f"{novel['title']}_大纲.txt", 603 | "文本文件 (*.txt)" 604 | ) 605 | 606 | if file_path: 607 | # 获取所有章节 608 | chapters = self.chapter_model.get_by_novel(self.current_novel_id) 609 | 610 | # 构建导出内容 611 | content = [ 612 | novel['title'] + " - 大纲", 613 | "=" * 40, 614 | "小说整体大纲:", 615 | novel.get('outline', '暂无大纲'), 616 | "=" * 40, 617 | "\n章节大纲:\n" 618 | ] 619 | 620 | # 添加每个章节的大纲 621 | for chapter in chapters: 622 | content.extend([ 623 | f"第{chapter['chapter_number']}章 {chapter['title']}", 624 | "-" * 40, 625 | chapter.get('outline', '暂无大纲'), 626 | "" 627 | ]) 628 | 629 | # 写入文件 630 | with open(file_path, 'w', encoding='utf-8') as f: 631 | f.write('\n'.join(content)) 632 | 633 | self.statusBar.showMessage(f'大纲已导出到:{file_path}') 634 | 635 | except Exception as e: 636 | QMessageBox.critical(self, '错误', f'导出大纲失败:{str(e)}') 637 | 638 | def _on_editor_content_changed(self, content: str): 639 | """编辑器内容变更处理""" 640 | if self.current_chapter_id: 641 | self.statusBar.showMessage('正在编辑...') 642 | 643 | def _on_editor_save_requested(self, content: str): 644 | """编辑器保存请求处理""" 645 | try: 646 | if self.current_chapter_id: 647 | # 保存内容 648 | self.chapter_model.update( 649 | self.current_chapter_id, 650 | content=content 651 | ) 652 | 653 | # 自动提取和更新角色 654 | self.character_model.auto_update_characters( 655 | self.generator, 656 | self.current_novel_id, 657 | self.current_chapter_id, 658 | content 659 | ) 660 | self._refresh_character_list() 661 | 662 | # 如果启用了自动摘要,生成并保存摘要 663 | if self.auto_summary: 664 | try: 665 | summary = self.summary_system.generate_chapter_summary(content) 666 | self.chapter_model.update( 667 | self.current_chapter_id, 668 | summary=summary 669 | ) 670 | self.summary_text.setPlainText(summary) 671 | self.statusBar.showMessage('内容、角色和摘要已自动保存') 672 | except Exception as e: 673 | logging.error(f"自动生成摘要失败: {e}") 674 | self.statusBar.showMessage('内容和角色已保存,但自动摘要失败') 675 | else: 676 | self.statusBar.showMessage('内容和角色已自动保存') 677 | 678 | logging.info(f"章节 {self.current_chapter_id} 已自动保存") 679 | except Exception as e: 680 | self.statusBar.showMessage('自动保存失败') 681 | logging.error(f"自动保存失败: {str(e)}") 682 | 683 | def save_novel(self): 684 | """保存小说""" 685 | try: 686 | if self.current_novel_id and self.current_chapter_id: 687 | content = self.editor.save_content() 688 | 689 | # 保存内容 690 | self.chapter_model.update( 691 | self.current_chapter_id, 692 | content=content 693 | ) 694 | 695 | # 如果启用了自动摘要,生成并保存摘要 696 | if self.auto_summary: 697 | try: 698 | summary = self.summary_system.generate_chapter_summary(content) 699 | self.chapter_model.update( 700 | self.current_chapter_id, 701 | summary=summary 702 | ) 703 | self.summary_text.setPlainText(summary) 704 | self.statusBar.showMessage('内容和摘要已保存') 705 | except Exception as e: 706 | logging.error(f"自动生成摘要失败: {e}") 707 | self.statusBar.showMessage('内容已保存,但自动摘要失败') 708 | else: 709 | self.statusBar.showMessage('保存成功') 710 | else: 711 | self.new_novel() 712 | except Exception as e: 713 | QMessageBox.critical(self, '错误', f'保存失败:{str(e)}') 714 | 715 | def generate_content(self): 716 | """生成内容""" 717 | try: 718 | if not self.current_novel_id: 719 | raise ValueError('请先创建或打开小说') 720 | 721 | # 收集上下文信息 722 | context = {} 723 | 724 | # 获取小说信息和大纲 725 | novel = self.novel_model.get(self.current_novel_id) 726 | if novel: 727 | context['novel_outline'] = novel.get('outline', '') 728 | logging.info(f"已获取小说《{novel['title']}》的大纲") 729 | 730 | # 获取当前章节信息 731 | if self.current_chapter_id: 732 | current_chapter = self.chapter_model.get(self.current_chapter_id) 733 | if current_chapter: 734 | context['current_chapter'] = current_chapter 735 | logging.info(f"已获取当前章节信息:第{current_chapter['chapter_number']}章") 736 | 737 | # 获取之前所有章节的摘要 738 | previous_chapters = self.chapter_model.get_by_novel( 739 | self.current_novel_id, 740 | before_chapter=current_chapter['chapter_number'] 741 | ) 742 | if previous_chapters: 743 | context['previous_summaries'] = [ 744 | { 745 | 'chapter_number': chapter['chapter_number'], 746 | 'summary': chapter.get('summary', '') 747 | } 748 | for chapter in previous_chapters 749 | ] 750 | logging.info(f"已获取{len(previous_chapters)}个之前章节的摘要") 751 | 752 | # 获取角色信息 753 | characters = self.character_model.get_by_novel(self.current_novel_id) 754 | if characters: 755 | context['characters'] = characters 756 | logging.info(f"已获取{len(characters)}个角色信息") 757 | 758 | logging.info("开始调用内容生成对话框...") 759 | 760 | # 显示内容生成对话框 761 | content = ContentGeneratorDialog.generate_content( 762 | self.generator, 763 | self, 764 | context 765 | ) 766 | 767 | # 如果生成了内容,插入到当前位置并提取角色 768 | if content: 769 | cursor = self.editor.textCursor() 770 | cursor.insertText(content) 771 | 772 | # 自动提取和更新角色 773 | self.character_model.auto_update_characters( 774 | self.generator, 775 | self.current_novel_id, 776 | self.current_chapter_id, 777 | content 778 | ) 779 | self._refresh_character_list() 780 | 781 | self.statusBar.showMessage('内容已插入,角色已更新') 782 | logging.info("内容生成和插入完成,角色已更新") 783 | 784 | except Exception as e: 785 | error_msg = f'生成内容失败:{str(e)}' 786 | logging.error(error_msg) 787 | QMessageBox.critical(self, '错误', error_msg) 788 | 789 | def update_summary(self): 790 | """更新摘要""" 791 | try: 792 | if not self.current_chapter_id: 793 | raise ValueError('请先选择章节') 794 | 795 | content = self.editor.toPlainText() 796 | if not content: 797 | raise ValueError('当前章节没有内容') 798 | 799 | # 获取当前摘要 800 | chapter = self.chapter_model.get(self.current_chapter_id) 801 | current_summary = chapter.get('summary', '') 802 | 803 | # 显示摘要生成对话框 804 | dialog = SummaryGeneratorDialog( 805 | self.summary_system, 806 | content, 807 | current_summary, 808 | self 809 | ) 810 | # 连接自动摘要信号 811 | dialog.autoSummaryChanged.connect(self._on_auto_summary_changed) 812 | 813 | summary = dialog.generate_summary( 814 | self.summary_system, 815 | content, 816 | current_summary 817 | ) 818 | 819 | # 如果生成了摘要,更新数据库 820 | if summary: 821 | self.chapter_model.update( 822 | self.current_chapter_id, 823 | summary=summary 824 | ) 825 | self.summary_text.setPlainText(summary) 826 | self.statusBar.showMessage('摘要已更新') 827 | 828 | except Exception as e: 829 | QMessageBox.critical(self, '错误', f'更新摘要失败:{str(e)}') 830 | 831 | def _on_auto_summary_changed(self, enabled: bool): 832 | """自动摘要设置变更处理""" 833 | self.auto_summary = enabled 834 | self.statusBar.showMessage(f'自动摘要已{"启用" if enabled else "禁用"}') 835 | 836 | def show_history(self): 837 | """显示版本历史""" 838 | try: 839 | if not self.current_chapter_id: 840 | raise ValueError('请先选择章节') 841 | 842 | # TODO: 实现显示版本历史功能 843 | self.statusBar.showMessage('正在加载版本历史...') 844 | except Exception as e: 845 | QMessageBox.critical(self, '错误', f'加载版本历史失败:{str(e)}') 846 | 847 | def closeEvent(self, event): 848 | """关闭窗口事件""" 849 | if self.editor.is_modified(): 850 | reply = QMessageBox.question( 851 | self, '确认退出', 852 | '是否保存当前更改?', 853 | QMessageBox.StandardButton.Save | 854 | QMessageBox.StandardButton.Discard | 855 | QMessageBox.StandardButton.Cancel 856 | ) 857 | 858 | if reply == QMessageBox.StandardButton.Save: 859 | self.save_novel() 860 | event.accept() 861 | elif reply == QMessageBox.StandardButton.Discard: 862 | event.accept() 863 | else: 864 | event.ignore() 865 | else: 866 | event.accept() 867 | 868 | def _on_chapter_renamed(self, chapter_id: int, new_title: str): 869 | """章节重命名处理""" 870 | try: 871 | # 更新章节标题 872 | self.chapter_model.update( 873 | chapter_id, 874 | title=new_title 875 | ) 876 | self.statusBar.showMessage(f'章节已重命名为:{new_title}') 877 | 878 | except Exception as e: 879 | QMessageBox.critical(self, '错误', f'重命名章节失败:{str(e)}') 880 | 881 | def _on_generate_outline(self): 882 | """AI生成大纲处理""" 883 | try: 884 | if not self.current_novel_id: 885 | raise ValueError('请先创建或打开小说') 886 | 887 | if self.outline_editor.is_chapter_outline(): 888 | # 生成章节大纲 889 | if not self.current_chapter_id: 890 | raise ValueError('请先选择章节') 891 | 892 | chapter = self.chapter_model.get(self.current_chapter_id) 893 | if not chapter['content']: 894 | raise ValueError('当前章节没有内容') 895 | 896 | self.statusBar.showMessage('正在生成章节大纲...') 897 | outline = self.generator.generate_outline( 898 | chapter_content=chapter['content'], 899 | is_chapter=True 900 | ) 901 | 902 | else: 903 | # 生成小说大纲 904 | novel = self.novel_model.get(self.current_novel_id) 905 | self.statusBar.showMessage('正在生成小说大纲...') 906 | outline = self.generator.generate_outline( 907 | novel_title=novel['title'], 908 | is_chapter=False 909 | ) 910 | 911 | # 更新编辑器内容 912 | self.outline_editor.set_content( 913 | outline, 914 | is_chapter=self.outline_editor.is_chapter_outline() 915 | ) 916 | self.statusBar.showMessage('大纲生成完成') 917 | 918 | except Exception as e: 919 | QMessageBox.critical(self, '错误', f'生成大纲失败:{str(e)}') -------------------------------------------------------------------------------- /assistant_snippet_Hs4Wd2Aqxm.txt: -------------------------------------------------------------------------------- 1 | 1|2024-01-10 10:30:15 - INFO - 日志系统初始化完成 2 | 2|2024-01-10 10:30:16 - INFO - 开始加载所有数据... 3 | 3|2024-01-10 10:30:16 - INFO - 当前选中小说ID: 1 4 | 4|2024-01-10 10:30:16 - INFO - 开始加载角色关系数据... 5 | 5|2024-01-10 10:30:16 - INFO - 创建Character模型实例,小说ID=1 6 | 6|2024-01-10 10:30:16 - INFO - 开始调用get_character_relationships_for_novel方法 7 | 7|2024-01-10 10:30:16 - INFO - 开始获取小说(ID=1)的角色关系... 8 | 8|2024-01-10 10:30:16 - INFO - 成功获取3个角色关系 9 | 9|2024-01-10 10:30:16 - INFO - 关系类型统计: 朋友:2组, 师徒:1组 10 | 10|2024-01-10 10:30:16 - INFO - 关系数据填充完成 11 | 11|2024-01-10 10:30:16 - INFO - 角色关系数据加载完成 -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import logging 3 | from PyQt6.QtWidgets import QApplication 4 | from app.ui.windows.main_window import MainWindow 5 | from app.database.sqlite import DatabaseManager 6 | 7 | def setup_logging(): 8 | """配置日志""" 9 | # 创建日志格式器 10 | formatter = logging.Formatter( 11 | '%(asctime)s - %(name)s - %(levelname)s - %(message)s', 12 | datefmt='%Y-%m-%d %H:%M:%S' 13 | ) 14 | 15 | # 配置控制台处理器 16 | console_handler = logging.StreamHandler() 17 | console_handler.setFormatter(formatter) 18 | 19 | # 配置文件处理器 20 | file_handler = logging.FileHandler('app.log', encoding='utf-8') 21 | file_handler.setFormatter(formatter) 22 | 23 | # 获取根日志记录器 24 | root_logger = logging.getLogger() 25 | root_logger.setLevel(logging.INFO) 26 | 27 | # 清除现有的处理器 28 | root_logger.handlers.clear() 29 | 30 | # 添加处理器 31 | root_logger.addHandler(console_handler) 32 | root_logger.addHandler(file_handler) 33 | 34 | # 设置其他模块的日志级别 35 | logging.getLogger('app').setLevel(logging.INFO) 36 | 37 | # 记录初始化完成 38 | root_logger.info("日志系统初始化完成") 39 | 40 | def main(): 41 | """主函数""" 42 | # 设置日志 43 | setup_logging() 44 | 45 | # 创建应用 46 | app = QApplication(sys.argv) 47 | 48 | # 初始化数据库 49 | db_manager = DatabaseManager("novels.db") 50 | db_manager.init_database() 51 | 52 | # 创建并显示主窗口 53 | window = MainWindow(db_manager) 54 | window.show() 55 | 56 | # 运行应用 57 | sys.exit(app.exec()) 58 | 59 | if __name__ == "__main__": 60 | main() -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyQt6>=6.4.0 2 | google-generativeai>=0.3.0 3 | python-dotenv>=0.19.0 4 | pyecharts>=2.0.0 -------------------------------------------------------------------------------- /test/test_chapter.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from app.database.sqlite import DatabaseManager 3 | from app.models.novel import Novel 4 | from app.models.chapter import Chapter 5 | 6 | def test_chapter_model(): 7 | # 设置日志 8 | logging.basicConfig(level=logging.INFO) 9 | 10 | try: 11 | # 1. 初始化数据库和模型 12 | db = DatabaseManager("test_models.db") 13 | db.init_database() 14 | novel_model = Novel(db) 15 | chapter_model = Chapter(db) 16 | 17 | # 2. 创建测试小说 18 | print("\n创建测试小说:") 19 | novel_id = novel_model.create( 20 | title="测试小说", 21 | outline="测试用小说大纲" 22 | ) 23 | print(f"小说ID: {novel_id}") 24 | 25 | # 3. 测试创建章节 26 | print("\n测试创建章节:") 27 | chapter_id = chapter_model.create( 28 | novel_id=novel_id, 29 | chapter_number=1, 30 | title="第一章 测试", 31 | content="这是第一章的内容", 32 | summary="第一章的摘要" 33 | ) 34 | print(f"章节ID: {chapter_id}") 35 | 36 | # 4. 测试获取章节信息 37 | print("\n测试获取章���信息:") 38 | chapter_info = chapter_model.get(chapter_id) 39 | print("章节信息:", chapter_info) 40 | 41 | # 5. 测试更新章节 42 | print("\n测试更新章节:") 43 | chapter_model.update( 44 | chapter_id, 45 | title="第一章 修改后的标题", 46 | content="这是更新后的内容" 47 | ) 48 | updated_info = chapter_model.get(chapter_id) 49 | print("更新后的信息:", updated_info) 50 | 51 | # 6. 测试获取版本历史 52 | print("\n测试获取版本历史:") 53 | versions = chapter_model.get_versions(chapter_id) 54 | print(f"版本历史(共{len(versions)}个版本):") 55 | for version in versions: 56 | print(f"- 版本ID: {version['id']}") 57 | print(f" 内容: {version['content']}") 58 | print(f" 备注: {version['comment']}") 59 | print(f" 创建时间: {version['created_at']}") 60 | 61 | # 7. 测试恢复版本 62 | if versions: 63 | print("\n测试恢复版本:") 64 | first_version_id = versions[-1]['id'] # 获取最早的版本 65 | chapter_model.restore_version(first_version_id) 66 | restored_info = chapter_model.get(chapter_id) 67 | print("恢复后的信息:", restored_info) 68 | 69 | # 8. 测试删��章节 70 | print("\n测试删除章节:") 71 | chapter_model.delete(chapter_id) 72 | deleted_info = chapter_model.get(chapter_id) 73 | print(f"删除后查询结果: {deleted_info}") 74 | 75 | print("\n测试完成!") 76 | 77 | except Exception as e: 78 | print(f"测试过程中出现错误: {e}") 79 | raise 80 | 81 | if __name__ == "__main__": 82 | test_chapter_model() -------------------------------------------------------------------------------- /test/test_character.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from app.database.sqlite import DatabaseManager 3 | from app.models.novel import Novel 4 | from app.models.chapter import Chapter 5 | from app.models.character import Character 6 | 7 | def test_character_model(): 8 | # 设置日志 9 | logging.basicConfig(level=logging.INFO) 10 | 11 | try: 12 | # 1. 初始化数据库和模型 13 | db = DatabaseManager("test_models.db") 14 | db.init_database() 15 | novel_model = Novel(db) 16 | chapter_model = Chapter(db) 17 | character_model = Character(db) 18 | 19 | # 2. 创建测试小说 20 | print("\n创建测试小说:") 21 | novel_id = novel_model.create( 22 | title="测试小说", 23 | outline="测试用小说大纲" 24 | ) 25 | print(f"小说ID: {novel_id}") 26 | 27 | # 3. 创建测试章节 28 | print("\n创建测试章节:") 29 | chapter_id = chapter_model.create( 30 | novel_id=novel_id, 31 | chapter_number=1, 32 | title="第一章", 33 | content="这是第一章的内容", 34 | summary="第一章的摘要" 35 | ) 36 | print(f"章节ID: {chapter_id}") 37 | 38 | # 4. 测试创建角色 39 | print("\n测试创建角色:") 40 | # 创建主角 41 | protagonist_id = character_model.create( 42 | novel_id=novel_id, 43 | name="张三", 44 | description="主角,一个普通人", 45 | characteristics="勇敢,正直", 46 | role_type="主角", 47 | first_appearance=chapter_id 48 | ) 49 | print(f"主角ID: {protagonist_id}") 50 | 51 | # 创建配角 52 | supporting_id = character_model.create( 53 | novel_id=novel_id, 54 | name="李四", 55 | description="主角的好友", 56 | characteristics="聪明,谨慎", 57 | role_type="配角", 58 | first_appearance=chapter_id 59 | ) 60 | print(f"配角ID: {supporting_id}") 61 | 62 | # 5. 测试获取角色信息 63 | print("\n测试获取角色信息:") 64 | protagonist_info = character_model.get(protagonist_id) 65 | print("主角信息:", protagonist_info) 66 | 67 | # 6. 测试更新角色 68 | print("\n测试更新角色:") 69 | character_model.update( 70 | protagonist_id, 71 | description="主角,一个觉醒了超能力的普通人", 72 | characteristics="勇敢,正直,善良" 73 | ) 74 | updated_info = character_model.get(protagonist_id) 75 | print("更新后的信息:", updated_info) 76 | 77 | # 7. 测试添加角色关系 78 | print("\n测试添加角色关系:") 79 | relationship_id = character_model.add_relationship( 80 | novel_id=novel_id, 81 | character1_id=protagonist_id, 82 | character2_id=supporting_id, 83 | relationship_type="挚友", 84 | description="青梅竹马的好友", 85 | start_chapter=chapter_id 86 | ) 87 | print(f"关系ID: {relationship_id}") 88 | 89 | # 8. 测试获取角色关系 90 | print("\n测试获取角色关系:") 91 | relationships = character_model.get_relationships(protagonist_id) 92 | print("角色关系:", relationships) 93 | 94 | # 9. 测试更新关系 95 | print("\n测试更新关系:") 96 | character_model.update_relationship( 97 | relationship_id, 98 | relationship_type="盟友", 99 | description="共同对抗邪恶势力的伙伴" 100 | ) 101 | updated_relationships = character_model.get_relationships(protagonist_id) 102 | print("更新后的关系:", updated_relationships) 103 | 104 | # 10. 测试删除关系 105 | print("\n测试删除关系:") 106 | character_model.delete_relationship(relationship_id) 107 | remaining_relationships = character_model.get_relationships(protagonist_id) 108 | print(f"剩余关系数量: {len(remaining_relationships)}") 109 | 110 | # 11. 测试删除角色 111 | print("\n测试删除角色:") 112 | character_model.delete(supporting_id) 113 | deleted_info = character_model.get(supporting_id) 114 | print(f"删除后查询结果: {deleted_info}") 115 | 116 | print("\n测试完成!") 117 | 118 | except Exception as e: 119 | print(f"测试过程中出现错误: {e}") 120 | raise 121 | 122 | if __name__ == "__main__": 123 | test_character_model() -------------------------------------------------------------------------------- /test/test_core.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from app.database.sqlite import DatabaseManager 3 | from app.core.generator import NovelGenerator 4 | from app.core.context import ContextManager 5 | 6 | def test_basic_workflow(): 7 | # 设置日志 8 | logging.basicConfig(level=logging.INFO) 9 | 10 | try: 11 | # 1. 初始化数据库 12 | db = DatabaseManager("test.db") 13 | db.init_database() 14 | 15 | # 2. 创建小说 16 | query = """ 17 | INSERT INTO novels (title, outline, current_chapter) 18 | VALUES (?, ?, ?) 19 | """ 20 | db.execute_query(query, ("测试小说", "这是一个测试用的武侠小说", 1)) 21 | 22 | # 获取新创建的小说ID 23 | result = db.execute_query("SELECT last_insert_rowid()") 24 | novel_id = result[0][0] 25 | 26 | # 3. 添加角色 27 | context_manager = ContextManager(db) 28 | context_manager.add_character( 29 | novel_id=novel_id, 30 | name="李白", 31 | description="主角,潇洒不羁的剑客", 32 | characteristics="豪放,善饮,武艺高超" 33 | ) 34 | 35 | # 4. 获取上下文 36 | context = context_manager.get_novel_context(novel_id) 37 | print("\n当前上下文:") 38 | print(context) 39 | 40 | # 5. 生成内容 41 | generator = NovelGenerator() 42 | prompt = "请以李白为主角,写一个江湖场景的开篇,要求有诗意,300字左右" 43 | content = generator.generate_content(prompt, context) 44 | print("\n生成的内容:") 45 | print(content) 46 | 47 | # 6. 生成摘要 48 | summary = generator.generate_summary(content) 49 | print("\n内容摘要:") 50 | print(summary) 51 | 52 | # 7. 保存章节 53 | query = """ 54 | INSERT INTO chapters (novel_id, chapter_number, title, content, summary) 55 | VALUES (?, ?, ?, ?, ?) 56 | """ 57 | db.execute_query(query, (novel_id, 1, "第一章", content, summary)) 58 | 59 | print("\n测试完成!") 60 | 61 | except Exception as e: 62 | print(f"测试过程中出现错误: {e}") 63 | raise 64 | 65 | if __name__ == "__main__": 66 | test_basic_workflow() -------------------------------------------------------------------------------- /test/test_models.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from app.database.sqlite import DatabaseManager 3 | from app.models.novel import Novel 4 | 5 | def test_novel_model(): 6 | # 设置日志 7 | logging.basicConfig(level=logging.INFO) 8 | 9 | try: 10 | # 1. 初始化数据库和模型 11 | db = DatabaseManager("test_models.db") 12 | db.init_database() 13 | novel_model = Novel(db) 14 | 15 | # 2. 测试创建小说 16 | print("\n测试创建小说:") 17 | novel_id = novel_model.create( 18 | title="测试小说标题", 19 | outline="这是一个测试用的小说大纲" 20 | ) 21 | print(f"创建的小说ID: {novel_id}") 22 | 23 | # 3. 测试获取小说信息 24 | print("\n测试获取小说信息:") 25 | novel_info = novel_model.get(novel_id) 26 | print("小说信息:", novel_info) 27 | 28 | # 4. 测试更新小说 29 | print("\n测试更新小说:") 30 | novel_model.update( 31 | novel_id, 32 | title="更新后的标题", 33 | outline="更新后的大纲" 34 | ) 35 | updated_info = novel_model.get(novel_id) 36 | print("更新后的信息:", updated_info) 37 | 38 | # 5. 测试获取小说列表 39 | print("\n测试获取小说列表:") 40 | novels = novel_model.list_all() 41 | print(f"小说列表(共{len(novels)}本):", novels) 42 | 43 | # 6. 测试添加章节 44 | print("\n测试添加章节:") 45 | query = """ 46 | INSERT INTO chapters (novel_id, chapter_number, title, content, summary) 47 | VALUES (?, ?, ?, ?, ?) 48 | """ 49 | db.execute_query(query, (novel_id, 1, "第一章", "章节内容", "章节摘要")) 50 | 51 | # 7. 测试获取章节 52 | print("\n测试获取章节:") 53 | chapters = novel_model.get_chapters(novel_id) 54 | print("章节列表:", chapters) 55 | 56 | # 8. 测试添加角色 57 | print("\n测试添加角色:") 58 | query = """ 59 | INSERT INTO characters (novel_id, name, description, characteristics) 60 | VALUES (?, ?, ?, ?) 61 | """ 62 | db.execute_query(query, (novel_id, "测试角色", "角色描述", "角色特征")) 63 | 64 | # 9. 测试获取角色 65 | print("\n测试获取角色:") 66 | characters = novel_model.get_characters(novel_id) 67 | print("角色列表:", characters) 68 | 69 | # 10. 测试删除小说 70 | print("\n测试删除小说:") 71 | novel_model.delete(novel_id) 72 | deleted_info = novel_model.get(novel_id) 73 | print(f"删除后查询结果: {deleted_info}") 74 | 75 | print("\n测试完成!") 76 | 77 | except Exception as e: 78 | print(f"测试过程中出现错误: {e}") 79 | raise 80 | 81 | if __name__ == "__main__": 82 | test_novel_model() -------------------------------------------------------------------------------- /test/test_summary.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from app.database.sqlite import DatabaseManager 3 | from app.core.generator import NovelGenerator 4 | from app.core.summary import SummarySystem 5 | 6 | def test_summary_system(): 7 | # 设置日志 8 | logging.basicConfig(level=logging.INFO) 9 | 10 | try: 11 | # 1. 初始化组件 12 | db = DatabaseManager("test.db") 13 | generator = NovelGenerator() 14 | summary_system = SummarySystem(db, generator) 15 | 16 | # 2. 创建测试小说和章节 17 | # 创建小说 18 | query = """ 19 | INSERT INTO novels (title, outline, current_chapter) 20 | VALUES (?, ?, ?) 21 | """ 22 | db.execute_query(query, ("测试小说", "初始大纲", 2)) 23 | 24 | # 获取小说ID 25 | result = db.execute_query("SELECT last_insert_rowid()") 26 | novel_id = result[0][0] 27 | 28 | # 创建两个测试章节 29 | chapters_data = [ 30 | (novel_id, 1, "第一章", "月光如水,李白独坐江畔,手中的酒壶映照着星光。远处传来阵阵琴声,一个蒙面剑客缓步走来...", "李白在江边遇到神秘剑客"), 31 | (novel_id, 2, "第二章", "剑光如虹,李白与神秘人的对决惊动了整个江湖。那人的剑法竟与传说中的荒古剑派如出一辙...", "李白与神秘剑客的惊天对决") 32 | ] 33 | 34 | for chapter in chapters_data: 35 | query = """ 36 | INSERT INTO chapters (novel_id, chapter_number, title, content, summary) 37 | VALUES (?, ?, ?, ?, ?) 38 | """ 39 | db.execute_query(query, chapter) 40 | 41 | # 3. 测试更新大纲 42 | print("\n测试更新大纲:") 43 | new_outline = summary_system.update_novel_outline(novel_id) 44 | print(f"新大纲:\n{new_outline}") 45 | 46 | # 4. 测试提取关键情节点 47 | print("\n测试提取关键情节点:") 48 | # 获取第二章的ID 49 | result = db.execute_query( 50 | "SELECT id FROM chapters WHERE novel_id = ? AND chapter_number = ?", 51 | (novel_id, 2) 52 | ) 53 | chapter_id = result[0][0] 54 | 55 | key_points = summary_system.extract_key_points(chapter_id) 56 | print("关键情节点:") 57 | for i, point in enumerate(key_points, 1): 58 | print(f"{i}. {point}") 59 | 60 | print("\n测试完成!") 61 | 62 | except Exception as e: 63 | print(f"测试过程中出现错误: {e}") 64 | raise 65 | 66 | if __name__ == "__main__": 67 | test_summary_system() --------------------------------------------------------------------------------