#!/usr/bin/env python3 """ LoreBible目录管理模块 负责管理LoreBible目录结构、文件操作和现有角色扫描 """ import os import json import re from pathlib import Path from typing import Dict, List, Optional, Set, Tuple import logging logger = logging.getLogger(__name__) class LoreBibleManager: """LoreBible目录管理器""" # 标准目录结构 DEFAULT_DIRECTORY_STRUCTURE = { "00_Prepare": "临时档案目录", "01_Research": "研究资料目录", "02_LoreBible": { "Characters": "角色档案目录", "Locations": "地点设定目录", "Organizations": "组织设定目录", "Timeline": "时间线目录" } } def __init__(self, workspace: str): """初始化LoreBible管理器 Args: workspace: 工作目录路径 """ self.workspace = Path(workspace).resolve() self.characters_dir = self.workspace / "02_LoreBible" / "Characters" self.prepare_dir = self.workspace / "00_Prepare" self._character_index = None # 角色信息索引缓存 def validate_directory_structure(self) -> Tuple[bool, List[str]]: """验证目录结构是否符合标准 Returns: (是否有效, 缺失目录列表) """ missing_dirs = [] # 检查必需目录 required_dirs = [ self.workspace, self.characters_dir, self.prepare_dir ] for dir_path in required_dirs: if not dir_path.exists(): missing_dirs.append(str(dir_path.relative_to(self.workspace))) return len(missing_dirs) == 0, missing_dirs def create_directory_structure(self) -> bool: """创建缺失的目录结构 Returns: 是否成功创建 """ try: # 创建必需目录 self.characters_dir.mkdir(parents=True, exist_ok=True) self.prepare_dir.mkdir(parents=True, exist_ok=True) # 创建可选目录 optional_dirs = [ self.workspace / "01_Research", self.workspace / "02_LoreBible" / "Locations", self.workspace / "02_LoreBible" / "Organizations", self.workspace / "02_LoreBible" / "Timeline" ] for dir_path in optional_dirs: dir_path.mkdir(parents=True, exist_ok=True) logger.info(f"目录结构已创建在: {self.workspace}") return True except Exception as e: logger.error(f"创建目录结构失败: {e}") return False def scan_existing_characters(self) -> List[Dict]: """扫描现有角色档案 Returns: 角色信息列表 """ if not self.characters_dir.exists(): return [] characters = [] pattern = re.compile(r'\.md$', re.IGNORECASE) for file_path in self.characters_dir.glob("*.md"): try: character_info = self._parse_character_file(file_path) if character_info: characters.append(character_info) except Exception as e: logger.warning(f"解析角色文件失败 {file_path}: {e}") return characters def _parse_character_file(self, file_path: Path) -> Optional[Dict]: """解析单个角色档案文件 Args: file_path: 角色档案文件路径 Returns: 角色信息字典,解析失败返回None """ try: content = file_path.read_text(encoding='utf-8', errors='ignore') # 提取基本信息 info = { "file_path": str(file_path), "file_name": file_path.name, "file_size": file_path.stat().st_size, "last_modified": file_path.stat().st_mtime } # 提取角色姓名(从标题行) name_match = re.search(r'^#\s+(.+?)\s+-', content, re.MULTILINE) if name_match: info["name"] = name_match.group(1).strip() else: # 尝试从文件名提取 info["name"] = file_path.stem.replace('character_profile_', '').replace('_', ' ') # 提取年龄 age_match = re.search(r'- \*\*年龄\*\*[::]\s*(.+?)(?:\n|$)', content) if age_match: info["age"] = age_match.group(1).strip() # 提取性别 gender_match = re.search(r'- \*\*性别\*\*[::]\s*(.+?)(?:\n|$)', content) if gender_match: info["gender"] = gender_match.group(1).strip() # 提取职业/身份 occupation_match = re.search(r'- \*\*职业/身份\*\*[::]\s*(.+?)(?:\n|$)', content) if occupation_match: info["occupation"] = occupation_match.group(1).strip() # 提取故事中的角色 role_match = re.search(r'- \*\*故事中的角色\*\*[::]\s*(.+?)(?:\n|$)', content) if role_match: info["role"] = role_match.group(1).strip() # 提取角色类型(从元数据) type_match = re.search(r'- \*\*角色类型\*\*[::]\s*(.+?)(?:\n|$)', content) if type_match: info["character_type"] = type_match.group(1).strip() # 提取创建时间 created_match = re.search(r'- \*\*创建时间\*\*[::]\s*(.+?)(?:\n|$)', content) if created_match: info["created"] = created_match.group(1).strip() # 提取状态 status_match = re.search(r'- \*\*状态\*\*[::]\s*(.+?)(?:\n|$)', content) if status_match: info["status"] = status_match.group(1).strip() # 提取关系信息(简单提取) relationships_section = self._extract_section(content, "人物关系") if relationships_section: info["has_relationships"] = True # 可以进一步解析具体关系 # 提取背景故事 background_section = self._extract_section(content, "背景故事") if background_section: info["has_background"] = True return info except Exception as e: logger.error(f"解析角色文件失败 {file_path}: {e}") return None def _extract_section(self, content: str, section_title: str) -> Optional[str]: """提取指定章节内容 Args: content: 文档内容 section_title: 章节标题 Returns: 章节内容,未找到返回None """ # 寻找章节标题 pattern = rf'##\s+{re.escape(section_title)}(.*?)(?=##\s+|---|\Z)' match = re.search(pattern, content, re.DOTALL | re.IGNORECASE) if match: return match.group(1).strip() return None def get_character_index(self, force_refresh: bool = False) -> Dict: """获取角色信息索引(带缓存) Args: force_refresh: 是否强制刷新缓存 Returns: 角色索引字典 """ if self._character_index is None or force_refresh: characters = self.scan_existing_characters() self._character_index = { "total_count": len(characters), "characters": characters, "name_map": {char.get("name", ""): char for char in characters if char.get("name")}, "by_type": self._group_by_type(characters), "last_updated": os.path.getmtime(self.characters_dir) if self.characters_dir.exists() else 0 } return self._character_index def _group_by_type(self, characters: List[Dict]) -> Dict: """按角色类型分组 Args: characters: 角色列表 Returns: 按类型分组的字典 """ grouped = {} for char in characters: char_type = char.get("character_type", "未知") if char_type not in grouped: grouped[char_type] = [] grouped[char_type].append(char) return grouped def save_temp_profile(self, content: str, character_name: str, session_id: str = None) -> Optional[Path]: """保存临时档案到00_Prepare目录 Args: content: 档案内容 character_name: 角色名称 session_id: 会话ID,为None时自动生成 Returns: 临时文件路径,失败返回None """ try: if not self.prepare_dir.exists(): self.prepare_dir.mkdir(parents=True, exist_ok=True) # 生成会话ID import uuid if session_id is None: session_id = str(uuid.uuid4())[:8] # 安全文件名 safe_name = re.sub(r'[^\w\s-]', '', character_name).strip() safe_name = re.sub(r'[-\s]+', '_', safe_name) # 临时文件名 filename = f"temp_{safe_name}_{session_id}.md" temp_path = self.prepare_dir / filename # 写入文件 temp_path.write_text(content, encoding='utf-8') logger.info(f"临时档案已保存: {temp_path}") return temp_path except Exception as e: logger.error(f"保存临时档案失败: {e}") return None def move_to_characters(self, temp_file_path: Path, character_name: str = None) -> Optional[Path]: """将临时档案移动到Characters目录 Args: temp_file_path: 临时文件路径 character_name: 角色名称,为None时从文件内容提取 Returns: 最终文件路径,失败返回None """ try: if not temp_file_path.exists(): logger.error(f"临时文件不存在: {temp_file_path}") return None # 确保目标目录存在 if not self.characters_dir.exists(): self.characters_dir.mkdir(parents=True, exist_ok=True) # 读取内容提取角色名 if character_name is None: content = temp_file_path.read_text(encoding='utf-8', errors='ignore') name_match = re.search(r'^#\s+(.+?)\s+-', content, re.MULTILINE) if name_match: character_name = name_match.group(1).strip() else: character_name = temp_file_path.stem.replace('temp_', '').split('_')[0] # 安全文件名 safe_name = re.sub(r'[^\w\s-]', '', character_name).strip() safe_name = re.sub(r'[-\s]+', '_', safe_name) # 最终文件名 import datetime timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S') filename = f"character_profile_{safe_name}_{timestamp}.md" final_path = self.characters_dir / filename # 移动文件 temp_file_path.rename(final_path) logger.info(f"档案已移动到: {final_path}") # 刷新缓存 self._character_index = None return final_path except Exception as e: logger.error(f"移动档案失败: {e}") return None def cleanup_old_temp_files(self, max_age_hours: int = 24) -> int: """清理旧的临时文件 Args: max_age_hours: 最大保留时间(小时) Returns: 清理的文件数量 """ if not self.prepare_dir.exists(): return 0 import time current_time = time.time() max_age_seconds = max_age_hours * 3600 cleaned_count = 0 for file_path in self.prepare_dir.glob("temp_*.md"): try: file_age = current_time - file_path.stat().st_mtime if file_age > max_age_seconds: file_path.unlink() cleaned_count += 1 logger.debug(f"清理临时文件: {file_path}") except Exception as e: logger.warning(f"清理文件失败 {file_path}: {e}") if cleaned_count > 0: logger.info(f"清理了 {cleaned_count} 个临时文件") return cleaned_count def get_workspace_info(self) -> Dict: """获取工作空间信息 Returns: 工作空间信息字典 """ info = { "workspace": str(self.workspace), "characters_dir": str(self.characters_dir), "prepare_dir": str(self.prepare_dir), "characters_count": 0, "temp_files_count": 0, "directory_exists": self.workspace.exists() } if self.characters_dir.exists(): info["characters_count"] = len(list(self.characters_dir.glob("*.md"))) if self.prepare_dir.exists(): info["temp_files_count"] = len(list(self.prepare_dir.glob("temp_*.md"))) return info def main(): """命令行测试""" import sys if len(sys.argv) < 2: print("用法: python lore_bible_manager.py <工作目录>") sys.exit(1) workspace = sys.argv[1] manager = LoreBibleManager(workspace) print(f"工作目录: {workspace}") # 验证目录结构 is_valid, missing_dirs = manager.validate_directory_structure() if is_valid: print("✓ 目录结构完整") else: print("✗ 缺失目录:") for dir_name in missing_dirs: print(f" - {dir_name}") # 询问是否创建 response = input("是否创建缺失目录? (y/n): ").strip().lower() if response == 'y': if manager.create_directory_structure(): print("✓ 目录结构已创建") else: print("✗ 创建目录失败") # 扫描现有角色 characters = manager.scan_existing_characters() print(f"现有角色数量: {len(characters)}") for char in characters[:5]: # 显示前5个 print(f" - {char.get('name', '未知')} ({char.get('character_type', '未知类型')})") if len(characters) > 5: print(f" ... 还有 {len(characters) - 5} 个角色") # 显示工作空间信息 info = manager.get_workspace_info() print(f"\n工作空间信息:") print(f" 角色档案数: {info['characters_count']}") print(f" 临时文件数: {info['temp_files_count']}") if __name__ == "__main__": main()