jianzhihuixiang/skills/character-profile-cn/scripts/lore_bible_manager.py

445 lines
15 KiB
Python
Raw Permalink Normal View History

#!/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()