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

445 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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

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