feat: initial commit
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
# AI IDE Configurators
|
||||
|
||||
ONW 5.0 组件:Cursor/Windsurf AI IDE 直接挂载适配器
|
||||
|
||||
本目录包含与主流 AI IDE 集成的适配器,实现:
|
||||
- 项目上下文自动注入
|
||||
- 代码生成规则同步
|
||||
- RAG 检索结果直接挂载
|
||||
- Genesis Contract IDE 层强制执行
|
||||
@@ -0,0 +1,98 @@
|
||||
"""
|
||||
Cursor IDE Adapter
|
||||
NovelMaster Core Engine - Configurators
|
||||
|
||||
为 Cursor IDE 生成 .cursorrules 和项目配置
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional
|
||||
import json
|
||||
|
||||
|
||||
def generate_cursorrules_content(project_config: Dict[str, Any]) -> str:
|
||||
"""生成 .cursorrules 文件内容"""
|
||||
|
||||
genesis = project_config.get("genesis_contract", {})
|
||||
|
||||
rules = f"""# NovelMaster Cursor Rules
|
||||
|
||||
## 项目概览
|
||||
- 项目名称: {project_config.get("project_name", "未命名")}
|
||||
- 类型: {project_config.get("genre", "网文")}
|
||||
- 目标字数: {project_config.get("target_word_count", "未知")} 字
|
||||
|
||||
## 核心设定 (Genesis Contract)
|
||||
### 核心欲望
|
||||
{_format_dict(genesis.get("core_desire", {}))}
|
||||
|
||||
### 伦理沙盒
|
||||
{_format_dict(genesis.get("ethical_inversion", {}))}
|
||||
|
||||
### 奇观日常化
|
||||
{_format_dict(genesis.get("core_spectacle", {}))}
|
||||
|
||||
## 写作规范
|
||||
- 使用中文思维写作
|
||||
- 章节字数: 2000-2500 字
|
||||
- 优先使用第三人称
|
||||
- 爽点密度: 每1000字至少1个情感释放点
|
||||
- 追读力 (Reading Power): 监控 Hook/Cool-point 平衡
|
||||
|
||||
## 状态管理
|
||||
- 数据目录: .noma/
|
||||
- 使用 handoff.py 进行帧交接
|
||||
- 使用 ledger.py 管理资产/负债
|
||||
- 使用 retcon_manager.py 处理设定修改
|
||||
|
||||
## 敏感内容
|
||||
- 禁止: 政治敏感、真实暴力、未成年人性内容
|
||||
- 伦理沙盒内创作自由
|
||||
"""
|
||||
|
||||
return rules
|
||||
|
||||
|
||||
def _format_dict(d: Dict[str, Any], indent: int = 2) -> str:
|
||||
if not d:
|
||||
return " (未设置)"
|
||||
lines = []
|
||||
for key, value in d.items():
|
||||
if isinstance(value, dict):
|
||||
lines.append(f" {key}:")
|
||||
lines.append(_format_dict(value, indent + 2))
|
||||
elif isinstance(value, list):
|
||||
lines.append(f" {key}: {', '.join(str(v) for v in value)}")
|
||||
else:
|
||||
lines.append(f" {key}: {value}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def generate_project_config(project_root: Path, config: Dict[str, Any]) -> Path:
|
||||
"""生成 Cursor 项目配置文件"""
|
||||
config_dir = project_root / ".cursor"
|
||||
config_dir.mkdir(exist_ok=True)
|
||||
|
||||
config_file = config_dir / "novelmaster.json"
|
||||
with open(config_file, "w", encoding="utf-8") as f:
|
||||
json.dump(config, f, ensure_ascii=False, indent=2)
|
||||
|
||||
rules_file = project_root / ".cursorrules"
|
||||
rules_content = generate_cursorrules_content(config)
|
||||
with open(rules_file, "w", encoding="utf-8") as f:
|
||||
f.write(rules_content)
|
||||
|
||||
return rules_file
|
||||
|
||||
|
||||
def detect_cursor_environment() -> bool:
|
||||
"""检测是否在 Cursor 环境中"""
|
||||
cursor_markers = [
|
||||
".cursor",
|
||||
".cursorrules",
|
||||
"cursor_rules",
|
||||
]
|
||||
for marker in cursor_markers:
|
||||
if Path(marker).exists():
|
||||
return True
|
||||
return False
|
||||
@@ -0,0 +1,101 @@
|
||||
"""
|
||||
Universal IDE Adapter
|
||||
NovelMaster Core Engine - Configurators
|
||||
|
||||
跨IDE统一接口 (Cursor, Windsurf, VS Code, JetBrains)
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional, List
|
||||
from enum import Enum
|
||||
|
||||
from .cursor_adapter import generate_project_config as cursor_generate
|
||||
from .windsurf_adapter import setup_windsurf
|
||||
|
||||
|
||||
class IDEType(Enum):
|
||||
CURSOR = "cursor"
|
||||
WINDSURF = "windsurf"
|
||||
VSCODE = "vscode"
|
||||
JETBRAINS = "jetbrains"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
def detect_ide() -> IDEType:
|
||||
"""检测当前 IDE 环境"""
|
||||
markers = Path.cwd()
|
||||
|
||||
cursor_markers = [".cursorrules", ".cursor"]
|
||||
windsurf_markers = [".windsurfrules", ".windsurf"]
|
||||
|
||||
for marker in cursor_markers:
|
||||
if (markers / marker).exists():
|
||||
return IDEType.CURSOR
|
||||
|
||||
for marker in windsurf_markers:
|
||||
if (markers / marker).exists():
|
||||
return IDEType.WINDSURF
|
||||
|
||||
if "cursor" in Path.cwd().parts or "cursor" in str(Path(__file__)):
|
||||
return IDEType.CURSOR
|
||||
elif "windsurf" in Path.cwd().parts or "windsurf" in str(Path(__file__)):
|
||||
return IDEType.WINDSURF
|
||||
|
||||
return IDEType.UNKNOWN
|
||||
|
||||
|
||||
def setup_ide(project_root: Path, config: Dict[str, Any]) -> bool:
|
||||
"""为检测到的 IDE 设置项目"""
|
||||
ide = detect_ide()
|
||||
|
||||
if ide == IDEType.CURSOR:
|
||||
cursor_generate(project_root, config)
|
||||
return True
|
||||
elif ide == IDEType.WINDSURF:
|
||||
setup_windsurf(project_root, config)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def generate_ide_rules(ide: IDEType, project_config: Dict[str, Any]) -> str:
|
||||
"""为指定 IDE 生成规则"""
|
||||
if ide == IDEType.CURSOR:
|
||||
from .cursor_adapter import generate_cursorrules_content
|
||||
return generate_cursorrules_content(project_config)
|
||||
elif ide == IDEType.WINDSURF:
|
||||
from .windsurf_adapter import generate_windsurfrules
|
||||
return generate_windsurfrules(project_config)
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
def get_supported_ides() -> List[str]:
|
||||
"""获取支持的 IDE 列表"""
|
||||
return [e.value for e in IDEType if e != IDEType.UNKNOWN]
|
||||
|
||||
|
||||
def auto_setup(project_root: Path, config: Dict[str, Any]) -> Dict[str, bool]:
|
||||
"""自动为所有支持的 IDE 生成配置"""
|
||||
results = {}
|
||||
|
||||
results["cursor"] = _setup_cursor(project_root, config)
|
||||
results["windsurf"] = _setup_windsurf(project_root, config)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def _setup_cursor(project_root: Path, config: Dict[str, Any]) -> bool:
|
||||
try:
|
||||
cursor_generate(project_root, config)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _setup_windsurf(project_root: Path, config: Dict[str, Any]) -> bool:
|
||||
try:
|
||||
setup_windsurf(project_root, config)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
@@ -0,0 +1,77 @@
|
||||
"""
|
||||
Windsurf IDE Adapter
|
||||
NovelMaster Core Engine - Configurators
|
||||
|
||||
为 Windsurf IDE 生成配置和 Cascade 上下文
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
import json
|
||||
|
||||
|
||||
def generate_windsurfrules(project_config: Dict[str, Any]) -> str:
|
||||
"""生成 .windsurfrules 文件内容"""
|
||||
|
||||
genesis = project_config.get("genesis_contract", {})
|
||||
|
||||
rules = f"""# NovelMaster Windsurf Rules
|
||||
|
||||
## 项目配置
|
||||
- 名称: {project_config.get("project_name", "未命名")}
|
||||
- 题材: {project_config.get("genre", "网文")}
|
||||
|
||||
## Genesis Contract
|
||||
{genesis.get("description", "")}
|
||||
|
||||
## Cascade 上下文
|
||||
使用以下上下文进行创作:
|
||||
1. 核心欲望: {genesis.get("core_desire", {}).get("primary", "未设置")}
|
||||
2. 伦理沙盒: {genesis.get("ethical_inversion", {}).get("inverted_norms", [])}
|
||||
3. 奇观设定: {genesis.get("core_spectacle", {}).get("ordinary_state", "未设置")}
|
||||
|
||||
## 写作工作流
|
||||
1. Planner Agent 调度 catharsis_model
|
||||
2. Writer Agent 生成章节
|
||||
3. Handoff 帧交接
|
||||
4. Dashboard 监控
|
||||
|
||||
## 状态文件
|
||||
- 状态文件: .noma/state.json
|
||||
- 账本: .noma/ledger.json
|
||||
- Hook池: .noma/hooks_pool.json
|
||||
"""
|
||||
|
||||
return rules
|
||||
|
||||
|
||||
def generate_cascade_context(project_root: Path) -> Dict[str, Any]:
|
||||
"""生成 Cascade 上下文"""
|
||||
state_file = project_root / ".noma" / "state.json"
|
||||
|
||||
context = {
|
||||
"project_root": str(project_root),
|
||||
"noma_dir": str(project_root / ".noma"),
|
||||
"genesis_contract": {},
|
||||
"current_chapter": 1,
|
||||
"hooks": [],
|
||||
"ledger": {}
|
||||
}
|
||||
|
||||
if state_file.exists():
|
||||
try:
|
||||
with open(state_file, "r", encoding="utf-8") as f:
|
||||
state = json.load(f)
|
||||
context.update(state)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return context
|
||||
|
||||
|
||||
def setup_windsurf(project_root: Path, config: Dict[str, Any]) -> None:
|
||||
"""设置 Windsurf 项目"""
|
||||
rules_file = project_root / ".windsurfrules"
|
||||
rules_content = generate_windsurfrules(config)
|
||||
with open(rules_file, "w", encoding="utf-8") as f:
|
||||
f.write(rules_content)
|
||||
@@ -0,0 +1,110 @@
|
||||
"""
|
||||
Context Cache (上下文静态缓存与Hash脏标记更新)
|
||||
NovelMaster Core Engine - Memory RAG
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional, Set
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
@dataclass
|
||||
class CacheEntry:
|
||||
key: str
|
||||
value: Any
|
||||
hash: str
|
||||
created_at: datetime
|
||||
last_accessed: datetime
|
||||
access_count: int = 0
|
||||
is_dirty: bool = False
|
||||
|
||||
def update_hash(self) -> str:
|
||||
content = json.dumps(self.value, sort_keys=True, ensure_ascii=False)
|
||||
self.hash = hashlib.sha256(content.encode('utf-8')).hexdigest()
|
||||
self.is_dirty = False
|
||||
return self.hash
|
||||
|
||||
|
||||
@dataclass
|
||||
class ContextCache:
|
||||
entries: Dict[str, CacheEntry] = field(default_factory=dict)
|
||||
max_size: int = 1000
|
||||
dirty_keys: Set[str] = field(default_factory=set)
|
||||
|
||||
def get(self, key: str) -> Optional[Any]:
|
||||
if key not in self.entries:
|
||||
return None
|
||||
entry = self.entries[key]
|
||||
entry.last_accessed = datetime.now()
|
||||
entry.access_count += 1
|
||||
return entry.value
|
||||
|
||||
def set(self, key: str, value: Any) -> None:
|
||||
content = json.dumps(value, sort_keys=True, ensure_ascii=False)
|
||||
hash_val = hashlib.sha256(content.encode('utf-8')).hexdigest()
|
||||
self.entries[key] = CacheEntry(
|
||||
key=key,
|
||||
value=value,
|
||||
hash=hash_val,
|
||||
created_at=datetime.now(),
|
||||
last_accessed=datetime.now()
|
||||
)
|
||||
if len(self.entries) > self.max_size:
|
||||
self._evict_lru()
|
||||
|
||||
def invalidate(self, key: str) -> None:
|
||||
if key in self.entries:
|
||||
self.entries[key].is_dirty = True
|
||||
self.dirty_keys.add(key)
|
||||
|
||||
def invalidate_pattern(self, pattern: str) -> None:
|
||||
for key in self.entries:
|
||||
if pattern in key:
|
||||
self.invalidate(key)
|
||||
|
||||
def get_dirty_keys(self) -> Set[str]:
|
||||
return self.dirty_keys.copy()
|
||||
|
||||
def clear_dirty(self, key: str) -> None:
|
||||
self.dirty_keys.discard(key)
|
||||
if key in self.entries:
|
||||
self.entries[key].is_dirty = False
|
||||
|
||||
def _evict_lru(self) -> None:
|
||||
if not self.entries:
|
||||
return
|
||||
sorted_entries = sorted(
|
||||
self.entries.items(),
|
||||
key=lambda x: (x[1].access_count, x[1].last_accessed)
|
||||
)
|
||||
evict_count = max(1, len(sorted_entries) // 10)
|
||||
for i in range(evict_count):
|
||||
del self.entries[sorted_entries[i][0]]
|
||||
|
||||
|
||||
class RetconHashListener:
|
||||
def __init__(self, cache: ContextCache):
|
||||
self.cache = cache
|
||||
self.subscribers: Dict[str, List[callable]] = {}
|
||||
|
||||
def subscribe(self, pattern: str, callback: callable) -> None:
|
||||
if pattern not in self.subscribers:
|
||||
self.subscribers[pattern] = []
|
||||
self.subscribers[pattern].append(callback)
|
||||
|
||||
def notify(self, changed_keys: Set[str]) -> None:
|
||||
for key in changed_keys:
|
||||
for pattern, callbacks in self.subscribers.items():
|
||||
if pattern in key:
|
||||
for callback in callbacks:
|
||||
callback(key)
|
||||
|
||||
def on_retcon(self, retcon_patch: dict) -> None:
|
||||
affected_elements = retcon_patch.get('affected_elements', [])
|
||||
for element in affected_elements:
|
||||
element_name = element.get('name', '')
|
||||
self.cache.invalidate_pattern(element_name)
|
||||
dirty_keys = self.cache.get_dirty_keys()
|
||||
self.notify(dirty_keys)
|
||||
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
Vector Store Utilities
|
||||
NovelMaster Core Engine - Memory RAG
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Any, Optional
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
|
||||
class VectorStoreUtils:
|
||||
"""向量存储工具类"""
|
||||
|
||||
def __init__(self, embedding_model: str = "default"):
|
||||
self.embedding_model = embedding_model
|
||||
self.collection_name = "novelmaster_context"
|
||||
self.dimension = 1536 # 默认维度
|
||||
|
||||
def compute_hash(self, content: str) -> str:
|
||||
"""计算内容的哈希值"""
|
||||
return hashlib.sha256(content.encode('utf-8')).hexdigest()
|
||||
|
||||
def chunk_text(self, text: str, chunk_size: int = 1000, overlap: int = 200) -> List[str]:
|
||||
"""将文本分块"""
|
||||
chunks = []
|
||||
for i in range(0, len(text), chunk_size - overlap):
|
||||
chunks.append(text[i:i + chunk_size])
|
||||
return chunks
|
||||
|
||||
def prepare_for_storage(self, text: str, metadata: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""准备存储格式"""
|
||||
return {
|
||||
"text": text,
|
||||
"hash": self.compute_hash(text),
|
||||
"metadata": metadata,
|
||||
"model": self.embedding_model
|
||||
}
|
||||
|
||||
def query_similar(self, query: str, top_k: int = 5, filters: Optional[Dict] = None) -> List[Dict]:
|
||||
"""查询相似内容"""
|
||||
return [] # 待实现
|
||||
|
||||
def upsert(self, documents: List[Dict[str, Any]]) -> bool:
|
||||
"""插入或更新文档"""
|
||||
return True
|
||||
|
||||
def delete_by_hash(self, hash: str) -> bool:
|
||||
"""通过哈希删除"""
|
||||
return True
|
||||
@@ -0,0 +1,194 @@
|
||||
"""
|
||||
Handoff Protocol (帧交接协议)
|
||||
NovelMaster Core Engine - 状态管理器
|
||||
|
||||
特性:
|
||||
- LOD (视锥剔除): 只精确交接在场人物
|
||||
- Tick (全局时钟): 多线程悬置动作记录
|
||||
- 休眠背景人物惰性更新
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional, Dict, Any
|
||||
from enum import Enum
|
||||
import time
|
||||
|
||||
|
||||
class CharacterStatus(Enum):
|
||||
ACTIVE = "active"
|
||||
DORMANT = "dormant"
|
||||
SUSPENDED = "suspended"
|
||||
|
||||
|
||||
class ActionType(Enum):
|
||||
DIALOGUE = "dialogue"
|
||||
MOVEMENT = "movement"
|
||||
COMBAT = "combat"
|
||||
SKILL = "skill"
|
||||
MENTAL = "mental"
|
||||
|
||||
|
||||
class Urgency(Enum):
|
||||
CRITICAL = "critical"
|
||||
NORMAL = "normal"
|
||||
LOW = "low"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Vector3D:
|
||||
"""3D坐标"""
|
||||
x: float
|
||||
y: float
|
||||
z: float
|
||||
|
||||
|
||||
@dataclass
|
||||
class Character:
|
||||
"""角色"""
|
||||
id: str
|
||||
name: str
|
||||
status: CharacterStatus = CharacterStatus.ACTIVE
|
||||
position: Optional[Vector3D] = None
|
||||
last_update_tick: int = 0
|
||||
pending_actions: List['ImminentAction'] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ImminentAction:
|
||||
"""悬置动作"""
|
||||
id: str
|
||||
character_id: str
|
||||
type: ActionType
|
||||
description: str
|
||||
urgency: Urgency = Urgency.NORMAL
|
||||
tick: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class WorldState:
|
||||
"""世界状态"""
|
||||
location: str
|
||||
time_of_day: str
|
||||
weather: Optional[str] = None
|
||||
tension_level: int = 50 # 0-100
|
||||
hook_pressure: int = 0 # 负债池压强
|
||||
|
||||
|
||||
@dataclass
|
||||
class HandoffMetadata:
|
||||
"""交接元数据"""
|
||||
chapter_number: int
|
||||
scene_number: int
|
||||
previous_chapter_summary: str
|
||||
next_chapter_hint: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class HandoffFrame:
|
||||
"""交接帧"""
|
||||
tick: int
|
||||
timestamp: float
|
||||
active_characters: List[Character]
|
||||
suspended_characters: List[Character]
|
||||
imminent_actions: List[ImminentAction]
|
||||
world_state: WorldState
|
||||
metadata: HandoffMetadata
|
||||
|
||||
|
||||
def cull_to_viewshed(characters: List[Character], view_center: Vector3D, radius: float) -> List[Character]:
|
||||
"""LOD视锥剔除 - 只保留视野内角色"""
|
||||
result = []
|
||||
for char in characters:
|
||||
if not char.position:
|
||||
continue
|
||||
distance = ((char.position.x - view_center.x) ** 2 +
|
||||
(char.position.y - view_center.y) ** 2 +
|
||||
(char.position.z - view_center.z) ** 2) ** 0.5
|
||||
if distance <= radius:
|
||||
result.append(char)
|
||||
return result
|
||||
|
||||
|
||||
def create_handoff_frame(
|
||||
current_tick: int,
|
||||
all_characters: List[Character],
|
||||
world_state: WorldState,
|
||||
metadata: HandoffMetadata
|
||||
) -> HandoffFrame:
|
||||
"""创建交接帧"""
|
||||
active_characters = [c for c in all_characters if c.status == CharacterStatus.ACTIVE]
|
||||
suspended_characters = [c for c in all_characters if c.status == CharacterStatus.SUSPENDED]
|
||||
|
||||
# 收集所有悬置动作
|
||||
imminent_actions = []
|
||||
for char in all_characters:
|
||||
imminent_actions.extend(char.pending_actions)
|
||||
|
||||
return HandoffFrame(
|
||||
tick=current_tick,
|
||||
timestamp=time.time(),
|
||||
active_characters=active_characters,
|
||||
suspended_characters=suspended_characters,
|
||||
imminent_actions=imminent_actions,
|
||||
world_state=world_state,
|
||||
metadata=metadata
|
||||
)
|
||||
|
||||
|
||||
def merge_handoff_frames(frames: List[HandoffFrame]) -> HandoffFrame:
|
||||
"""合并多个交接帧"""
|
||||
if not frames:
|
||||
raise ValueError("No frames to merge")
|
||||
|
||||
latest_frame = max(frames, key=lambda f: f.tick)
|
||||
|
||||
return HandoffFrame(
|
||||
tick=latest_frame.tick,
|
||||
timestamp=latest_frame.timestamp,
|
||||
active_characters=[c for f in frames for c in f.active_characters],
|
||||
suspended_characters=[c for f in frames for c in f.suspended_characters],
|
||||
imminent_actions=[a for f in frames for a in f.imminent_actions],
|
||||
world_state=latest_frame.world_state,
|
||||
metadata=latest_frame.metadata
|
||||
)
|
||||
|
||||
|
||||
def to_dict(frame: HandoffFrame) -> Dict[str, Any]:
|
||||
"""序列化为字典"""
|
||||
return {
|
||||
"tick": frame.tick,
|
||||
"timestamp": frame.timestamp,
|
||||
"active_characters": [
|
||||
{
|
||||
"id": c.id,
|
||||
"name": c.name,
|
||||
"status": c.status.value,
|
||||
"position": {"x": c.position.x, "y": c.position.y, "z": c.position.z} if c.position else None
|
||||
}
|
||||
for c in frame.active_characters
|
||||
],
|
||||
"suspended_characters": [c.id for c in frame.suspended_characters],
|
||||
"imminent_actions": [
|
||||
{
|
||||
"id": a.id,
|
||||
"character_id": a.character_id,
|
||||
"type": a.type.value,
|
||||
"description": a.description,
|
||||
"urgency": a.urgency.value
|
||||
}
|
||||
for a in frame.imminent_actions
|
||||
],
|
||||
"world_state": {
|
||||
"location": frame.world_state.location,
|
||||
"time_of_day": frame.world_state.time_of_day,
|
||||
"weather": frame.world_state.weather,
|
||||
"tension_level": frame.world_state.tension_level,
|
||||
"hook_pressure": frame.world_state.hook_pressure
|
||||
},
|
||||
"metadata": {
|
||||
"chapter_number": frame.metadata.chapter_number,
|
||||
"scene_number": frame.metadata.scene_number,
|
||||
"previous_chapter_summary": frame.metadata.previous_chapter_summary,
|
||||
"next_chapter_hint": frame.metadata.next_chapter_hint
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
"""
|
||||
Ledger (动态资产与负债账本)
|
||||
NovelMaster Core Engine - 状态管理器
|
||||
|
||||
管理角色资产、负债、Hook/Cool-point 追踪
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict, Set, Optional, Any
|
||||
from enum import Enum
|
||||
import time
|
||||
|
||||
|
||||
class AssetType(Enum):
|
||||
SKILL = "skill"
|
||||
ITEM = "item"
|
||||
RELATIONSHIP = "relationship"
|
||||
STATUS = "status"
|
||||
KNOWLEDGE = "knowledge"
|
||||
POWER = "power"
|
||||
|
||||
|
||||
class LiabilityType(Enum):
|
||||
DEBT = "debt"
|
||||
PROMISE = "promise"
|
||||
UNRESOLVED_CONFLICT = "unresolved_conflict"
|
||||
SECRET = "secret"
|
||||
WEAKNESS = "weakness"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Asset:
|
||||
"""资产"""
|
||||
id: str
|
||||
type: AssetType
|
||||
name: str
|
||||
description: str
|
||||
value: int # 1-100
|
||||
acquired_at_tick: int
|
||||
owner_id: str
|
||||
tags: List[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Liability:
|
||||
"""负债"""
|
||||
id: str
|
||||
type: LiabilityType
|
||||
name: str
|
||||
description: str
|
||||
severity: int # 1-100
|
||||
created_at_tick: int
|
||||
due_tick: Optional[int] = None
|
||||
creditor_id: Optional[str] = None
|
||||
tags: List[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class LedgerEntry:
|
||||
"""账本条目"""
|
||||
asset: Optional[Asset] = None
|
||||
liability: Optional[Liability] = None
|
||||
delta: float = 0
|
||||
reason: str = ""
|
||||
tick: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class CharacterLedger:
|
||||
"""角色账本"""
|
||||
character_id: str
|
||||
assets: Dict[str, Asset] = field(default_factory=dict)
|
||||
liabilities: Dict[str, Liability] = field(default_factory=dict)
|
||||
history: List[LedgerEntry] = field(default_factory=list)
|
||||
|
||||
|
||||
class HookType(Enum):
|
||||
SETUP = "setup"
|
||||
BUILDUP = "buildup"
|
||||
CLIFFHANGER = "cliffhanger"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Hook:
|
||||
"""钩子"""
|
||||
id: str
|
||||
type: HookType
|
||||
description: str
|
||||
chapter_introduced: int
|
||||
tension_weight: int # 1-100
|
||||
resolved: bool = False
|
||||
resolution_chapter: Optional[int] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class CoolPoint:
|
||||
"""爽点"""
|
||||
id: str
|
||||
type: str # payoff, subversion, escalation
|
||||
description: str
|
||||
chapter_delivered: int
|
||||
satisfaction_weight: int # 1-100
|
||||
|
||||
|
||||
def create_asset(
|
||||
asset_id: str,
|
||||
asset_type: AssetType,
|
||||
name: str,
|
||||
value: int,
|
||||
owner_id: str,
|
||||
description: str = ""
|
||||
) -> Asset:
|
||||
"""创建资产"""
|
||||
return Asset(
|
||||
id=asset_id,
|
||||
type=asset_type,
|
||||
name=name,
|
||||
description=description,
|
||||
value=max(1, min(100, value)),
|
||||
acquired_at_tick=0,
|
||||
owner_id=owner_id,
|
||||
tags=[]
|
||||
)
|
||||
|
||||
|
||||
def create_liability(
|
||||
liability_id: str,
|
||||
liability_type: LiabilityType,
|
||||
name: str,
|
||||
severity: int,
|
||||
description: str = ""
|
||||
) -> Liability:
|
||||
"""创建负债"""
|
||||
return Liability(
|
||||
id=liability_id,
|
||||
type=liability_type,
|
||||
name=name,
|
||||
description=description,
|
||||
severity=max(1, min(100, severity)),
|
||||
created_at_tick=0,
|
||||
tags=[]
|
||||
)
|
||||
|
||||
|
||||
def calculate_net_worth(ledger: CharacterLedger) -> int:
|
||||
"""计算角色净值"""
|
||||
asset_value = sum(asset.value for asset in ledger.assets.values())
|
||||
liability_value = sum(liab.severity for liab in ledger.liabilities.values())
|
||||
return asset_value - liability_value
|
||||
|
||||
|
||||
def add_hook(hooks: List[Hook], hook: Hook) -> List[Hook]:
|
||||
"""添加钩子"""
|
||||
return [*hooks, hook]
|
||||
|
||||
|
||||
def add_cool_point(cool_points: List[CoolPoint], cool_point: CoolPoint) -> List[CoolPoint]:
|
||||
"""添加爽点"""
|
||||
return [*cool_points, cool_point]
|
||||
|
||||
|
||||
def calculate_hook_pressure(hooks: List[Hook]) -> int:
|
||||
"""计算钩子压强"""
|
||||
unresolved = [h for h in hooks if not h.resolved]
|
||||
return sum(h.tension_weight for h in unresolved)
|
||||
|
||||
|
||||
def resolve_hook(hooks: List[Hook], hook_id: str, resolution_chapter: int) -> List[Hook]:
|
||||
"""解决钩子"""
|
||||
return [
|
||||
{**hook.__dict__, "resolved": True, "resolution_chapter": resolution_chapter}
|
||||
if hook.id == hook_id else hook
|
||||
for hook in hooks
|
||||
]
|
||||
|
||||
|
||||
def check_genesis_compatibility(asset: Asset, ethical_inversion: Dict[str, Any]) -> float:
|
||||
"""检查资产与创世契约的兼容性"""
|
||||
incompatible_norms = ethical_inversion.get("inverted_norms", [])
|
||||
|
||||
# 简单检查:资产名称是否与反转的道德规范冲突
|
||||
for norm in incompatible_norms:
|
||||
if norm.lower() in asset.name.lower():
|
||||
return 0.3 # 低兼容
|
||||
|
||||
return 1.0 # 完全兼容
|
||||
|
||||
|
||||
@dataclass
|
||||
class HooksPool:
|
||||
"""钩子池"""
|
||||
hooks: List[Hook] = field(default_factory=list)
|
||||
cool_points: List[CoolPoint] = field(default_factory=list)
|
||||
|
||||
def add_hook(self, hook: Hook) -> None:
|
||||
self.hooks.append(hook)
|
||||
|
||||
def resolve_hook(self, hook_id: str, chapter: int) -> bool:
|
||||
for h in self.hooks:
|
||||
if h.id == hook_id:
|
||||
h.resolved = True
|
||||
h.resolution_chapter = chapter
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def pressure(self) -> int:
|
||||
return calculate_hook_pressure(self.hooks)
|
||||
|
||||
@property
|
||||
def unresolved_count(self) -> int:
|
||||
return sum(1 for h in self.hooks if not h.resolved)
|
||||
@@ -0,0 +1,337 @@
|
||||
"""
|
||||
Retcon Manager (意图漂移管理与热补丁生成器)
|
||||
NovelMaster Core Engine - 状态管理器
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict, Optional, Any, Tuple
|
||||
from enum import Enum
|
||||
import hashlib
|
||||
import json
|
||||
import time
|
||||
|
||||
from .ledger import (
|
||||
CharacterLedger, Asset, Liability, Hook,
|
||||
AssetType, LiabilityType, HooksPool,
|
||||
check_genesis_compatibility
|
||||
)
|
||||
|
||||
|
||||
class RetconPriority(Enum):
|
||||
CRITICAL = "critical"
|
||||
HIGH = "high"
|
||||
MEDIUM = "medium"
|
||||
LOW = "low"
|
||||
|
||||
|
||||
class RetconOperationType(Enum):
|
||||
MODIFY = "modify"
|
||||
DELETE = "delete"
|
||||
CREATE = "create"
|
||||
RELABEL = "relabel"
|
||||
TRANSFER = "transfer"
|
||||
|
||||
|
||||
class AffectedElementType(Enum):
|
||||
ASSET = "asset"
|
||||
LIABILITY = "liability"
|
||||
CHARACTER_STATUS = "character_status"
|
||||
RELATIONSHIP = "relationship"
|
||||
WORLD_RULE = "world_rule"
|
||||
HOOK = "hook"
|
||||
|
||||
|
||||
@dataclass
|
||||
class RetconAffectedElement:
|
||||
type: AffectedElementType
|
||||
id: str
|
||||
name: str
|
||||
original_value: Any
|
||||
proposed_change: Any
|
||||
compatibility_score: float
|
||||
|
||||
|
||||
@dataclass
|
||||
class RetconOperation:
|
||||
type: RetconOperationType
|
||||
target_type: str
|
||||
target_id: str
|
||||
old_value: Any = None
|
||||
new_value: Any = None
|
||||
description: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class RetconRequest:
|
||||
author_intent: str
|
||||
affected_elements: List[RetconAffectedElement] = field(default_factory=list)
|
||||
timestamp: float = field(default_factory=time.time)
|
||||
priority: RetconPriority = RetconPriority.MEDIUM
|
||||
|
||||
|
||||
@dataclass
|
||||
class RetconPatch:
|
||||
id: str
|
||||
request: RetconRequest
|
||||
generated_at: float
|
||||
operations: List[RetconOperation] = field(default_factory=list)
|
||||
rollback_plan: List[RetconOperation] = field(default_factory=list)
|
||||
success: bool = True
|
||||
compatibility_issues: List[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class RetconResult:
|
||||
patch: RetconPatch
|
||||
compatibility_score: float
|
||||
warnings: List[str]
|
||||
approved: bool = False
|
||||
|
||||
|
||||
CONFLICT_TEMPLATES = [
|
||||
{
|
||||
"pattern": ["修仙", "正道", "浩然正气"],
|
||||
"conflicts_with": ["克苏鲁", "邪神", "混沌", "疯狂"],
|
||||
"resolution": "将正道之物转化为邪神相关设定"
|
||||
},
|
||||
{
|
||||
"pattern": ["科技", "理性", "逻辑"],
|
||||
"conflicts_with": ["魔法", "神秘", "超自然"],
|
||||
"resolution": "将科技产物与魔法融合或对立"
|
||||
},
|
||||
{
|
||||
"pattern": ["现实", "现代"],
|
||||
"conflicts_with": ["异世界", "穿越", "幻想"],
|
||||
"resolution": "引入平行世界设定"
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def detect_conflicts(new_setting: str, ledger: CharacterLedger, hooks_pool: HooksPool) -> List[Tuple[str, str]]:
|
||||
conflicts = []
|
||||
new_setting_lower = new_setting.lower()
|
||||
|
||||
for asset in ledger.assets.values():
|
||||
for template in CONFLICT_TEMPLATES:
|
||||
setting_matches = any(p.lower() in new_setting_lower for p in template["pattern"])
|
||||
asset_matches = any(p.lower() in asset.name.lower() for p in template["conflicts_with"])
|
||||
if setting_matches and asset_matches:
|
||||
conflicts.append((asset.id, template["resolution"]))
|
||||
|
||||
for liability in ledger.liabilities.values():
|
||||
for template in CONFLICT_TEMPLATES:
|
||||
setting_matches = any(p.lower() in new_setting_lower for p in template["pattern"])
|
||||
liability_matches = any(p.lower() in liability.name.lower() for p in template["conflicts_with"])
|
||||
if setting_matches and liability_matches:
|
||||
conflicts.append((liability.id, template["resolution"]))
|
||||
|
||||
for hook in hooks_pool.hooks:
|
||||
for template in CONFLICT_TEMPLATES:
|
||||
setting_matches = any(p.lower() in new_setting_lower for p in template["pattern"])
|
||||
hook_matches = any(p.lower() in hook.description.lower() for p in template["conflicts_with"])
|
||||
if setting_matches and hook_matches:
|
||||
conflicts.append((hook.id, template["resolution"]))
|
||||
|
||||
return conflicts
|
||||
|
||||
|
||||
def analyze_retcon_compatibility(
|
||||
request: RetconRequest,
|
||||
ledgers: Dict[str, CharacterLedger],
|
||||
hooks_pool: HooksPool,
|
||||
genesis_contract: Optional[Dict] = None
|
||||
) -> List[RetconAffectedElement]:
|
||||
affected = []
|
||||
conflicts = detect_conflicts(request.author_intent, list(ledgers.values())[0] if ledgers else CharacterLedger(""), hooks_pool)
|
||||
|
||||
for ledger in ledgers.values():
|
||||
for asset_id, asset in ledger.assets.items():
|
||||
compatibility = 100.0
|
||||
if genesis_contract:
|
||||
ethical_inv = genesis_contract.get("ethical_inversion", {})
|
||||
compatibility = check_genesis_compatibility(asset, ethical_inv) * 100
|
||||
|
||||
conflict_resolution = None
|
||||
for conflict_id, resolution in conflicts:
|
||||
if conflict_id == asset_id:
|
||||
conflict_resolution = resolution
|
||||
compatibility = min(compatibility, 30.0)
|
||||
|
||||
if compatibility < 100 or conflict_resolution:
|
||||
affected.append(RetconAffectedElement(
|
||||
type=AffectedElementType.ASSET,
|
||||
id=asset_id,
|
||||
name=asset.name,
|
||||
original_value=asset.__dict__,
|
||||
proposed_change={"compatibility": compatibility},
|
||||
compatibility_score=compatibility
|
||||
))
|
||||
|
||||
for liab_id, liability in ledger.liabilities.items():
|
||||
compatibility = 80.0 if liability.type == LiabilityType.DEBT else 100.0
|
||||
affected.append(RetconAffectedElement(
|
||||
type=AffectedElementType.LIABILITY,
|
||||
id=liab_id,
|
||||
name=liability.name,
|
||||
original_value=liability.__dict__,
|
||||
proposed_change={"compatibility": compatibility},
|
||||
compatibility_score=compatibility
|
||||
))
|
||||
|
||||
for hook in hooks_pool.hooks:
|
||||
if not hook.resolved:
|
||||
affected.append(RetconAffectedElement(
|
||||
type=AffectedElementType.HOOK,
|
||||
id=hook.id,
|
||||
name=hook.description,
|
||||
original_value=hook.__dict__,
|
||||
proposed_change={"needs_resolution": True},
|
||||
compatibility_score=50.0
|
||||
))
|
||||
|
||||
return affected
|
||||
|
||||
|
||||
def generate_retcon_patch(
|
||||
request: RetconRequest,
|
||||
ledgers: Dict[str, CharacterLedger],
|
||||
hooks_pool: HooksPool,
|
||||
genesis_contract: Optional[Dict] = None
|
||||
) -> RetconPatch:
|
||||
affected = analyze_retcon_compatibility(request, ledgers, hooks_pool, genesis_contract)
|
||||
operations = []
|
||||
compatibility_issues = []
|
||||
|
||||
for element in affected:
|
||||
if element.type == AffectedElementType.ASSET:
|
||||
if element.compatibility_score < 30:
|
||||
for ledger in ledgers.values():
|
||||
if element.id in ledger.assets:
|
||||
original_asset = ledger.assets[element.id]
|
||||
new_liability = Liability(
|
||||
id=f"retcon_{original_asset.id}",
|
||||
type=LiabilityType.UNRESOLVED_CONFLICT,
|
||||
name=f"[Retcon] {original_asset.name}",
|
||||
description=f"由资产 '{original_asset.name}' 转化,原值 {original_asset.value}",
|
||||
severity=original_asset.value,
|
||||
created_at_tick=0,
|
||||
tags=["retcon-converted", "auto-generated"]
|
||||
)
|
||||
operations.append(RetconOperation(
|
||||
type=RetconOperationType.DELETE,
|
||||
target_type="asset",
|
||||
target_id=element.id,
|
||||
old_value=original_asset.__dict__,
|
||||
new_value=new_liability.__dict__,
|
||||
description=f"将资产 '{element.name}' 转化为负债以适应新设定"
|
||||
))
|
||||
operations.append(RetconOperation(
|
||||
type=RetconOperationType.CREATE,
|
||||
target_type="liability",
|
||||
target_id=new_liability.id,
|
||||
old_value=None,
|
||||
new_value=new_liability.__dict__,
|
||||
description=f"创建转化负债 '{new_liability.name}'"
|
||||
))
|
||||
compatibility_issues.append(f"资产 '{element.name}' 被标记为不兼容,转化为对冲负债")
|
||||
break
|
||||
elif element.compatibility_score < 70:
|
||||
compatibility_issues.append(f"资产 '{element.name}' 兼容性问题需要关注")
|
||||
|
||||
elif element.type == AffectedElementType.HOOK:
|
||||
operations.append(RetconOperation(
|
||||
type=RetconOperationType.MODIFY,
|
||||
target_type="hook",
|
||||
target_id=element.id,
|
||||
old_value=element.original_value,
|
||||
new_value={"retcon_flag": True, "new_intent": request.author_intent},
|
||||
description=f"Hook '{element.name}' 被Retcon标记,需要重新评估"
|
||||
))
|
||||
|
||||
rollback_plan = [
|
||||
RetconOperation(
|
||||
type=op.type,
|
||||
target_type=op.target_type,
|
||||
target_id=op.target_id,
|
||||
old_value=op.new_value,
|
||||
new_value=op.old_value,
|
||||
description=f"回滚: {op.description}"
|
||||
)
|
||||
for op in operations
|
||||
]
|
||||
|
||||
return RetconPatch(
|
||||
id=f"retcon_{int(time.time() * 1000)}",
|
||||
request=request,
|
||||
generated_at=time.time(),
|
||||
operations=operations,
|
||||
rollback_plan=rollback_plan,
|
||||
success=len(compatibility_issues) == 0,
|
||||
compatibility_issues=compatibility_issues
|
||||
)
|
||||
|
||||
|
||||
def apply_retcon_patch(
|
||||
patch: RetconPatch,
|
||||
ledgers: Dict[str, CharacterLedger],
|
||||
hooks_pool: HooksPool
|
||||
) -> Dict[str, CharacterLedger]:
|
||||
new_ledgers = {}
|
||||
for ledger_id, ledger in ledgers.items():
|
||||
new_ledger = CharacterLedger(
|
||||
character_id=ledger.character_id,
|
||||
assets=dict(ledger.assets),
|
||||
liabilities=dict(ledger.liabilities),
|
||||
history=list(ledger.history)
|
||||
)
|
||||
for op in patch.operations:
|
||||
if op.target_type == "asset":
|
||||
if op.type == RetconOperationType.DELETE:
|
||||
if op.target_id in new_ledger.assets:
|
||||
del new_ledger.assets[op.target_id]
|
||||
elif op.type == RetconOperationType.CREATE:
|
||||
new_asset = Asset(**op.new_value)
|
||||
new_ledger.assets[new_asset.id] = new_asset
|
||||
elif op.target_type == "liability":
|
||||
if op.type == RetconOperationType.CREATE:
|
||||
new_liability = Liability(**op.new_value)
|
||||
new_ledger.liabilities[new_liability.id] = new_liability
|
||||
elif op.type == RetconOperationType.MODIFY:
|
||||
if op.target_id in new_ledger.liabilities:
|
||||
existing = new_ledger.liabilities[op.target_id]
|
||||
updated = existing.__dict__.copy()
|
||||
updated.update(op.new_value)
|
||||
new_ledger.liabilities[op.target_id] = Liability(**updated)
|
||||
new_ledgers[ledger_id] = new_ledger
|
||||
return new_ledgers
|
||||
|
||||
|
||||
def rollback_retcon_patch(
|
||||
patch: RetconPatch,
|
||||
ledgers: Dict[str, CharacterLedger]
|
||||
) -> Dict[str, CharacterLedger]:
|
||||
return apply_retcon_patch(
|
||||
RetconPatch(
|
||||
id=f"rollback_{patch.id}",
|
||||
request=patch.request,
|
||||
generated_at=time.time(),
|
||||
operations=patch.rollback_plan,
|
||||
rollback_plan=patch.operations,
|
||||
success=True,
|
||||
compatibility_issues=[]
|
||||
),
|
||||
ledgers,
|
||||
HooksPool()
|
||||
)
|
||||
|
||||
|
||||
def compute_retcon_hash(patch: RetconPatch) -> str:
|
||||
content = json.dumps({
|
||||
"id": patch.id,
|
||||
"operations": [
|
||||
{"type": op.type.value, "target_type": op.target_type, "target_id": op.target_id}
|
||||
for op in patch.operations
|
||||
],
|
||||
"timestamp": patch.generated_at
|
||||
}, sort_keys=True)
|
||||
return hashlib.sha256(content.encode('utf-8')).hexdigest()
|
||||
@@ -0,0 +1,31 @@
|
||||
"""
|
||||
Core Engine Validators
|
||||
|
||||
物理校验模块,处理元叙事开关和物理连贯性验证。
|
||||
"""
|
||||
|
||||
from .fourth_wall_validator import (
|
||||
FourthWallValidator,
|
||||
FourthWallState,
|
||||
ValidationMode,
|
||||
PhysicalRule,
|
||||
ValidationRule,
|
||||
ValidationIssue,
|
||||
get_validator,
|
||||
enable_meta_mode,
|
||||
disable_meta_mode,
|
||||
is_meta_mode,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"FourthWallValidator",
|
||||
"FourthWallState",
|
||||
"ValidationMode",
|
||||
"PhysicalRule",
|
||||
"ValidationRule",
|
||||
"ValidationIssue",
|
||||
"get_validator",
|
||||
"enable_meta_mode",
|
||||
"disable_meta_mode",
|
||||
"is_meta_mode",
|
||||
]
|
||||
@@ -0,0 +1,395 @@
|
||||
"""
|
||||
Fourth Wall Validator (第四面墙校验器)
|
||||
|
||||
当元叙事开关开启时,关闭物理连贯性校验,
|
||||
允许"反规则"写法:梦境、意识流、打破第四面墙等。
|
||||
|
||||
当开关关闭时,恢复标准物理校验。
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional, Dict, Any
|
||||
from enum import Enum
|
||||
import json
|
||||
|
||||
|
||||
class ValidationMode(Enum):
|
||||
NORMAL = "normal" # 标准物理校验
|
||||
META = "meta" # 元叙事模式,跳过物理校验
|
||||
|
||||
|
||||
class PhysicalRule(Enum):
|
||||
"""物理校验规则类型"""
|
||||
REALM_CONSISTENCY = "realm_consistency" # 境界一致性
|
||||
TIME_CONTINUITY = "time_continuity" # 时间连续性
|
||||
LOCATION_COHERENCE = "location_coherence" # 地点连贯性
|
||||
POWER_BALANCE = "power_balance" # 战力平衡
|
||||
CAUSALITY = "causality" # 因果关系
|
||||
|
||||
|
||||
@dataclass
|
||||
class ValidationRule:
|
||||
"""校验规则"""
|
||||
type: PhysicalRule
|
||||
enabled: bool
|
||||
description: str
|
||||
severity: str = "high" # critical/high/medium/low
|
||||
|
||||
|
||||
@dataclass
|
||||
class ValidationIssue:
|
||||
"""校验问题"""
|
||||
rule: PhysicalRule
|
||||
chapter: int
|
||||
description: str
|
||||
severity: str
|
||||
entity_id: Optional[str] = None
|
||||
suggested_fix: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class FourthWallState:
|
||||
"""第四面墙状态"""
|
||||
mode: ValidationMode = ValidationMode.NORMAL
|
||||
enabled_rules: List[PhysicalRule] = field(default_factory=list)
|
||||
bypassed_rules: List[PhysicalRule] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def is_meta_mode(self) -> bool:
|
||||
return self.mode == ValidationMode.META
|
||||
|
||||
|
||||
class FourthWallValidator:
|
||||
"""
|
||||
第四面墙校验器
|
||||
|
||||
用法:
|
||||
1. 当 Meta_Narrative_Panel.jsx 的 Toggle 开启时,调用 enable_meta_mode()
|
||||
2. 当 Toggle 关闭时,调用 disable_meta_mode()
|
||||
3. 验证时调用 validate() 方法
|
||||
"""
|
||||
|
||||
# 默认启用的物理校验规则
|
||||
DEFAULT_ENABLED_RULES = [
|
||||
PhysicalRule.REALM_CONSISTENCY,
|
||||
PhysicalRule.TIME_CONTINUITY,
|
||||
PhysicalRule.LOCATION_COHERENCE,
|
||||
PhysicalRule.POWER_BALANCE,
|
||||
PhysicalRule.CAUSALITY,
|
||||
]
|
||||
|
||||
# 元叙事模式下放行的规则(这些在元叙事模式下被跳过)
|
||||
META_BYPASSABLE_RULES = [
|
||||
PhysicalRule.TIME_CONTINUITY, # 允许时间跳跃
|
||||
PhysicalRule.LOCATION_COHERENCE, # 允许瞬间移动
|
||||
PhysicalRule.POWER_BALANCE, # 允许战力波动
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
self._state = FourthWallState(
|
||||
enabled_rules=list(self.DEFAULT_ENABLED_RULES),
|
||||
bypassed_rules=[]
|
||||
)
|
||||
self._listeners: List[callable] = []
|
||||
|
||||
@property
|
||||
def state(self) -> FourthWallState:
|
||||
return self._state
|
||||
|
||||
def enable_meta_mode(self) -> None:
|
||||
"""开启元叙事模式,关闭物理校验"""
|
||||
self._state.mode = ValidationMode.META
|
||||
self._state.bypassed_rules = list(self.META_BYPASSABLE_RULES)
|
||||
self._notify_listeners()
|
||||
print("[FourthWall] 元叙事模式已激活 - 物理校验已关闭")
|
||||
|
||||
def disable_meta_mode(self) -> None:
|
||||
"""关闭元叙事模式,恢复物理校验"""
|
||||
self._state.mode = ValidationMode.NORMAL
|
||||
self._state.bypassed_rules = []
|
||||
self._notify_listeners()
|
||||
print("[FourthWall] 标准模式已激活 - 物理校验已恢复")
|
||||
|
||||
def toggle(self) -> ValidationMode:
|
||||
"""切换模式"""
|
||||
if self._state.is_meta_mode:
|
||||
self.disable_meta_mode()
|
||||
else:
|
||||
self.enable_meta_mode()
|
||||
return self._state.mode
|
||||
|
||||
def validate(
|
||||
self,
|
||||
chapter: int,
|
||||
entities: List[Dict[str, Any]],
|
||||
world_state: Dict[str, Any],
|
||||
previous_state: Optional[Dict[str, Any]] = None
|
||||
) -> List[ValidationIssue]:
|
||||
"""
|
||||
验证章节物理连贯性
|
||||
|
||||
在元叙事模式下,返回空列表(跳过所有物理校验)
|
||||
"""
|
||||
issues: List[ValidationIssue] = []
|
||||
|
||||
# 元叙事模式:跳过所有物理校验
|
||||
if self._state.is_meta_mode:
|
||||
return issues
|
||||
|
||||
# 标准模式:执行校验
|
||||
for rule_type in self._state.enabled_rules:
|
||||
if rule_type not in self._state.bypassed_rules:
|
||||
rule_issues = self._validate_rule(
|
||||
rule_type, chapter, entities, world_state, previous_state
|
||||
)
|
||||
issues.extend(rule_issues)
|
||||
|
||||
return issues
|
||||
|
||||
def _validate_rule(
|
||||
self,
|
||||
rule: PhysicalRule,
|
||||
chapter: int,
|
||||
entities: List[Dict[str, Any]],
|
||||
world_state: Dict[str, Any],
|
||||
previous_state: Optional[Dict[str, Any]]
|
||||
) -> List[ValidationIssue]:
|
||||
"""根据规则类型执行校验"""
|
||||
validators = {
|
||||
PhysicalRule.REALM_CONSISTENCY: self._check_realm_consistency,
|
||||
PhysicalRule.TIME_CONTINUITY: self._check_time_continuity,
|
||||
PhysicalRule.LOCATION_COHERENCE: self._check_location_coherence,
|
||||
PhysicalRule.POWER_BALANCE: self._check_power_balance,
|
||||
PhysicalRule.CAUSALITY: self._check_causality,
|
||||
}
|
||||
|
||||
validator = validators.get(rule)
|
||||
if validator:
|
||||
return validator(chapter, entities, world_state, previous_state)
|
||||
return []
|
||||
|
||||
def _check_realm_consistency(
|
||||
self,
|
||||
chapter: int,
|
||||
entities: List[Dict[str, Any]],
|
||||
world_state: Dict[str, Any],
|
||||
previous_state: Optional[Dict[str, Any]]
|
||||
) -> List[ValidationIssue]:
|
||||
"""检查境界一致性"""
|
||||
issues: List[ValidationIssue] = []
|
||||
|
||||
# 检查主角境界是否合理
|
||||
protagonist = next((e for e in entities if e.get("is_protagonist")), None)
|
||||
if protagonist:
|
||||
current_realm = protagonist.get("realm")
|
||||
previous_realm = previous_state.get("protagonist", {}).get("realm") if previous_state else None
|
||||
|
||||
if previous_realm and current_realm != previous_realm:
|
||||
# 境界变化超过1级
|
||||
if not self._is_valid_realm_jump(previous_realm, current_realm):
|
||||
issues.append(ValidationIssue(
|
||||
rule=PhysicalRule.REALM_CONSISTENCY,
|
||||
chapter=chapter,
|
||||
description=f"境界跳跃不合理: {previous_realm} → {current_realm}",
|
||||
severity="high",
|
||||
entity_id=protagonist.get("id"),
|
||||
suggested_fix="补充修炼/机缘说明"
|
||||
))
|
||||
|
||||
return issues
|
||||
|
||||
def _check_time_continuity(
|
||||
self,
|
||||
chapter: int,
|
||||
entities: List[Dict[str, Any]],
|
||||
world_state: Dict[str, Any],
|
||||
previous_state: Optional[Dict[str, Any]]
|
||||
) -> List[ValidationIssue]:
|
||||
"""检查时间连续性"""
|
||||
issues: List[ValidationIssue] = []
|
||||
|
||||
current_time = world_state.get("time_of_day")
|
||||
previous_time = previous_state.get("world_state", {}).get("time_of_day") if previous_state else None
|
||||
|
||||
if previous_time and current_time:
|
||||
# 检查时间是否倒退
|
||||
time_order = {"早晨": 0, "上午": 1, "中午": 2, "下午": 3, "傍晚": 4, "夜晚": 5, "深夜": 6, "凌晨": 7}
|
||||
if time_order.get(previous_time, -1) > time_order.get(current_time, -1):
|
||||
issues.append(ValidationIssue(
|
||||
rule=PhysicalRule.TIME_CONTINUITY,
|
||||
chapter=chapter,
|
||||
description=f"时间倒退: {previous_time} → {current_time}",
|
||||
severity="medium",
|
||||
suggested_fix="添加时间跳跃说明"
|
||||
))
|
||||
|
||||
return issues
|
||||
|
||||
def _check_location_coherence(
|
||||
self,
|
||||
chapter: int,
|
||||
entities: List[Dict[str, Any]],
|
||||
world_state: Dict[str, Any],
|
||||
previous_state: Optional[Dict[str, Any]]
|
||||
) -> List[ValidationIssue]:
|
||||
"""检查地点连贯性"""
|
||||
issues: List[ValidationIssue] = []
|
||||
|
||||
current_loc = world_state.get("location")
|
||||
previous_loc = previous_state.get("world_state", {}).get("location") if previous_state else None
|
||||
|
||||
protagonist = next((e for e in entities if e.get("is_protagonist")), None)
|
||||
|
||||
if previous_loc and current_loc and protagonist:
|
||||
# 如果地点变化但没有过渡说明
|
||||
if current_loc != previous_loc:
|
||||
has_transition = self._check_transition_exists(chapter, previous_loc, current_loc)
|
||||
if not has_transition:
|
||||
issues.append(ValidationIssue(
|
||||
rule=PhysicalRule.LOCATION_COHERENCE,
|
||||
chapter=chapter,
|
||||
description=f"地点跳跃无过渡: {previous_loc} → {current_loc}",
|
||||
severity="low",
|
||||
entity_id=protagonist.get("id"),
|
||||
suggested_fix="添加移动过程描写"
|
||||
))
|
||||
|
||||
return issues
|
||||
|
||||
def _check_power_balance(
|
||||
self,
|
||||
chapter: int,
|
||||
entities: List[Dict[str, Any]],
|
||||
world_state: Dict[str, Any],
|
||||
previous_state: Optional[Dict[str, Any]]
|
||||
) -> List[ValidationIssue]:
|
||||
"""检查战力平衡"""
|
||||
issues: List[ValidationIssue] = []
|
||||
|
||||
# 检查是否有越太多级战斗
|
||||
protagonist = next((e for e in entities if e.get("is_protagonist")), None)
|
||||
opponents = [e for e in entities if e.get("is_opponent")]
|
||||
|
||||
if protagonist and opponents:
|
||||
p_realm = protagonist.get("realm_level", 0)
|
||||
for opp in opponents:
|
||||
o_realm = opp.get("realm_level", 0)
|
||||
if p_realm - o_realm > 2:
|
||||
issues.append(ValidationIssue(
|
||||
rule=PhysicalRule.POWER_BALANCE,
|
||||
chapter=chapter,
|
||||
description=f"战力差距过大: {p_realm} vs {o_realm}",
|
||||
severity="medium",
|
||||
entity_id=opp.get("id"),
|
||||
suggested_fix="增加战斗难度或金手指解释"
|
||||
))
|
||||
|
||||
return issues
|
||||
|
||||
def _check_causality(
|
||||
self,
|
||||
chapter: int,
|
||||
entities: List[Dict[str, Any]],
|
||||
world_state: Dict[str, Any],
|
||||
previous_state: Optional[Dict[str, Any]]
|
||||
) -> List[ValidationIssue]:
|
||||
"""检查因果关系"""
|
||||
# 简化实现:检查是否有突兀的力量获得
|
||||
issues: List[ValidationIssue] = []
|
||||
|
||||
new_skills = world_state.get("new_skills_gained", [])
|
||||
protagonist = next((e for e in entities if e.get("is_protagonist")), None)
|
||||
|
||||
if new_skills and protagonist:
|
||||
has_setup = self._check_setup_exists(chapter, new_skills)
|
||||
if not has_setup:
|
||||
issues.append(ValidationIssue(
|
||||
rule=PhysicalRule.CAUSALITY,
|
||||
chapter=chapter,
|
||||
description=f"突兀获得技能: {new_skills}",
|
||||
severity="high",
|
||||
entity_id=protagonist.get("id"),
|
||||
suggested_fix="补充技能来源铺垫"
|
||||
))
|
||||
|
||||
return issues
|
||||
|
||||
def _is_valid_realm_jump(self, from_realm: str, to_realm: str) -> bool:
|
||||
"""判断境界跳跃是否合理"""
|
||||
# 简化实现
|
||||
valid_jumps = {
|
||||
"炼气期一层": ["炼气期二层"],
|
||||
"炼气期二层": ["炼气期三层"],
|
||||
"筑基期一层": ["筑基期二层"],
|
||||
}
|
||||
return to_realm in valid_jumps.get(from_realm, [])
|
||||
|
||||
def _check_transition_exists(self, chapter: int, from_loc: str, to_loc: str) -> bool:
|
||||
"""检查过渡是否存在"""
|
||||
# 简化实现:应该读取章节正文检查
|
||||
return False
|
||||
|
||||
def _check_setup_exists(self, chapter: int, skills: List[str]) -> bool:
|
||||
"""检查铺垫是否存在"""
|
||||
# 简化实现:应该读取章节正文检查
|
||||
return False
|
||||
|
||||
def subscribe(self, callback: callable) -> None:
|
||||
"""订阅状态变化"""
|
||||
self._listeners.append(callback)
|
||||
|
||||
def unsubscribe(self, callback: callable) -> None:
|
||||
"""取消订阅"""
|
||||
if callback in self._listeners:
|
||||
self._listeners.remove(callback)
|
||||
|
||||
def _notify_listeners(self) -> None:
|
||||
"""通知所有监听器"""
|
||||
for callback in self._listeners:
|
||||
try:
|
||||
callback(self._state)
|
||||
except Exception as e:
|
||||
print(f"[FourthWall] 通知监听器失败: {e}")
|
||||
|
||||
def get_config(self) -> Dict[str, Any]:
|
||||
"""获取当前配置"""
|
||||
return {
|
||||
"mode": self._state.mode.value,
|
||||
"enabled_rules": [r.value for r in self._state.enabled_rules],
|
||||
"bypassed_rules": [r.value for r in self._state.bypassed_rules],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def from_config(config: Dict[str, Any]) -> "FourthWallValidator":
|
||||
"""从配置恢复"""
|
||||
validator = FourthWallValidator()
|
||||
if config.get("mode") == ValidationMode.META.value:
|
||||
validator.enable_meta_mode()
|
||||
return validator
|
||||
|
||||
|
||||
# 全局实例
|
||||
_validator_instance: Optional[FourthWallValidator] = None
|
||||
|
||||
|
||||
def get_validator() -> FourthWallValidator:
|
||||
"""获取全局校验器实例"""
|
||||
global _validator_instance
|
||||
if _validator_instance is None:
|
||||
_validator_instance = FourthWallValidator()
|
||||
return _validator_instance
|
||||
|
||||
|
||||
def enable_meta_mode() -> None:
|
||||
"""快捷函数:开启元叙事模式"""
|
||||
get_validator().enable_meta_mode()
|
||||
|
||||
|
||||
def disable_meta_mode() -> None:
|
||||
"""快捷函数:关闭元叙事模式"""
|
||||
get_validator().disable_meta_mode()
|
||||
|
||||
|
||||
def is_meta_mode() -> bool:
|
||||
"""快捷函数:检查是否在元叙事模式"""
|
||||
return get_validator().state.is_meta_mode
|
||||
Reference in New Issue
Block a user