feat: initial commit

This commit is contained in:
2026-06-23 20:29:02 +08:00
commit ea8f6066c4
217 changed files with 60754 additions and 0 deletions
+194
View File
@@ -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
}
}
+212
View File
@@ -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()