初始化提交
This commit is contained in:
commit
887c1f449e
963 files changed
+166869
No files matched your search
@@ -0,0 +1,15 @@
|
||||
"""符文引擎(Macros)—— ``{{...}}`` 占位符解析与替换。
|
||||
|
||||
纯逻辑模块(禁 IO),对齐酒馆核心宏子集 + 魂镜辰正时态扩展。
|
||||
设计见 docs/contracts/contract-glossary.md(§5 统一挂载架构、§8.4 宏依赖)。
|
||||
"""
|
||||
|
||||
from app.modules.macros.engine import MacroRegistry, MacroResolver, render
|
||||
from app.modules.macros.types import MacroContext
|
||||
|
||||
__all__ = [
|
||||
"MacroContext",
|
||||
"MacroRegistry",
|
||||
"MacroResolver",
|
||||
"render",
|
||||
]
|
||||
@@ -0,0 +1,92 @@
|
||||
"""内置符文集合。
|
||||
|
||||
对齐酒馆核心子集(``char`` / ``user`` / ``original`` / ``random`` / ``roll`` /
|
||||
``getvar``)+ 魂镜扩展(辰正时态宏,复用 ``time_sense.TemporalSnapshot``)。
|
||||
全部为纯函数解析器,不做 IO 或代码执行。
|
||||
|
||||
注:酒馆的 ``pick``(不重复随机)与 ``setvar``(写变量)需要跨调用状态,
|
||||
不在无状态渲染层实现;如需,由调用方在 ``MacroContext.variables`` 维护后注入。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
import re
|
||||
|
||||
from app.modules.macros.engine import MacroRegistry, MacroResolver
|
||||
from app.modules.macros.types import MacroContext
|
||||
|
||||
|
||||
def _char(_args: list[str], ctx: MacroContext) -> str:
|
||||
return ctx.char_name or ""
|
||||
|
||||
|
||||
def _user(_args: list[str], ctx: MacroContext) -> str:
|
||||
return ctx.user_name or ""
|
||||
|
||||
|
||||
def _original(_args: list[str], ctx: MacroContext) -> str:
|
||||
return ctx.original or ""
|
||||
|
||||
|
||||
def _random(args: list[str], _ctx: MacroContext) -> str:
|
||||
if not args:
|
||||
return ""
|
||||
return random.choice(args)
|
||||
|
||||
|
||||
def _roll(args: list[str], _ctx: MacroContext) -> str:
|
||||
"""``{{roll:NdM}}`` 或 ``{{roll:dM}}`` → N 个 M 面骰求和。非法规格原样返回。"""
|
||||
spec = (args[0] if args else "1d6").strip().casefold()
|
||||
match = re.fullmatch(r"(?P<n>\d*)d(?P<sides>\d+)", spec)
|
||||
if match is None:
|
||||
return spec
|
||||
count = int(match.group("n") or 1)
|
||||
sides = int(match.group("sides"))
|
||||
if count < 1 or sides < 1:
|
||||
return spec
|
||||
return str(sum(random.randint(1, sides) for _ in range(count)))
|
||||
|
||||
|
||||
def _getvar(args: list[str], ctx: MacroContext) -> str:
|
||||
return ctx.variables.get(args[0], "") if args else ""
|
||||
|
||||
|
||||
def _snapshot_field(field: str) -> MacroResolver:
|
||||
"""从辰正快照取字段;快照未注入时返回空串。"""
|
||||
|
||||
def _resolve(_args: list[str], ctx: MacroContext) -> str:
|
||||
snap = ctx.snapshot
|
||||
if snap is None:
|
||||
return ""
|
||||
value = getattr(snap, field, None)
|
||||
return "" if value is None else str(value)
|
||||
|
||||
return _resolve
|
||||
|
||||
|
||||
# 辰正时态宏名 → TemporalSnapshot 字段(魂镜扩展)
|
||||
_TIME_MACROS: tuple[tuple[str, str], ...] = (
|
||||
("date", "solar_date"),
|
||||
("time", "local_time"),
|
||||
("day_period", "day_period"),
|
||||
("shichen", "shichen"),
|
||||
("season", "season"),
|
||||
("solar_term", "solar_term"),
|
||||
("weekday", "weekday"),
|
||||
)
|
||||
|
||||
|
||||
def build_default_registry() -> MacroRegistry:
|
||||
"""构造默认符文注册表。"""
|
||||
reg = MacroRegistry()
|
||||
reg.register("char", _char)
|
||||
reg.register("bot", _char) # 酒馆别名
|
||||
reg.register("user", _user)
|
||||
reg.register("original", _original)
|
||||
reg.register("random", _random)
|
||||
reg.register("roll", _roll)
|
||||
reg.register("getvar", _getvar)
|
||||
for name, field in _TIME_MACROS:
|
||||
reg.register(name, _snapshot_field(field))
|
||||
return reg
|
||||
@@ -0,0 +1,96 @@
|
||||
"""符文引擎核心:解析 ``{{...}}`` 占位符并替换。
|
||||
|
||||
纯逻辑,无 IO、无代码执行——与酒馆可执行脚本的 macro 引擎划清边界:
|
||||
魂镜的符文只做文本替换,宏的解析器是受信任的 Python 函数,绝不 eval 用户输入。
|
||||
|
||||
规则:
|
||||
- 未知宏保留原 ``{{...}}`` 文本(不静默清空,避免误删用户文本)。
|
||||
- 解析器抛异常时保留原 ``{{...}}`` 文本,不阻断文本流。
|
||||
- 递归展开受限深度(``max_depth``),防止 ``{{a}}→{{b}}→{{a}}`` 振荡死循环。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
|
||||
from app.modules.macros.types import MacroContext
|
||||
|
||||
MacroResolver = Callable[[list[str], MacroContext], str]
|
||||
|
||||
_MACRO_PATTERN = re.compile(r"\{\{(.*?)\}\}")
|
||||
_DEFAULT_MAX_DEPTH = 5
|
||||
|
||||
|
||||
class MacroRegistry:
|
||||
"""符文注册表:名称(大小写不敏感)→ 解析器。"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._macros: dict[str, MacroResolver] = {}
|
||||
|
||||
def register(self, name: str, resolver: MacroResolver) -> None:
|
||||
self._macros[name.casefold()] = resolver
|
||||
|
||||
def get(self, name: str) -> MacroResolver | None:
|
||||
return self._macros.get(name.casefold())
|
||||
|
||||
def names(self) -> tuple[str, ...]:
|
||||
return tuple(self._macros)
|
||||
|
||||
|
||||
def render(
|
||||
text: str,
|
||||
ctx: MacroContext,
|
||||
*,
|
||||
registry: MacroRegistry | None = None,
|
||||
max_depth: int = _DEFAULT_MAX_DEPTH,
|
||||
) -> str:
|
||||
"""把 ``text`` 中的 ``{{...}}`` 占位符按 ``registry`` 替换。
|
||||
|
||||
递归展开(宏的返回值可含新的 ``{{...}}``),直到无占位符或达 ``max_depth``。
|
||||
未提供 ``registry`` 时使用内置默认符文集。
|
||||
"""
|
||||
reg = registry if registry is not None else _default_registry_lazy()
|
||||
current = text
|
||||
for _ in range(max_depth):
|
||||
if "{{" not in current:
|
||||
break
|
||||
nxt = _render_once(current, reg, ctx)
|
||||
if nxt == current:
|
||||
break
|
||||
current = nxt
|
||||
return current
|
||||
|
||||
|
||||
def _render_once(text: str, reg: MacroRegistry, ctx: MacroContext) -> str:
|
||||
def _replace(match: re.Match[str]) -> str:
|
||||
body = match.group(1).strip()
|
||||
if not body:
|
||||
return match.group(0)
|
||||
name, args = _split_macro(body)
|
||||
resolver = reg.get(name)
|
||||
if resolver is None:
|
||||
return match.group(0)
|
||||
try:
|
||||
return resolver(args, ctx)
|
||||
except Exception:
|
||||
return match.group(0)
|
||||
|
||||
return _MACRO_PATTERN.sub(_replace, text)
|
||||
|
||||
|
||||
def _split_macro(body: str) -> tuple[str, list[str]]:
|
||||
"""``name:arg1,arg2`` → ``("name", ["arg1", "arg2"])``;无参数时 args 为空。"""
|
||||
if ":" in body:
|
||||
name, rest = body.split(":", 1)
|
||||
args = [a.strip() for a in rest.split(",") if a.strip()] if rest.strip() else []
|
||||
else:
|
||||
name, args = body, []
|
||||
return name.strip(), args
|
||||
|
||||
|
||||
def _default_registry_lazy() -> MacroRegistry:
|
||||
"""延迟构造内置符文集,避免与 builtins 循环导入。"""
|
||||
from app.modules.macros.builtins import build_default_registry
|
||||
|
||||
return build_default_registry()
|
||||
@@ -0,0 +1,23 @@
|
||||
"""符文引擎数据结构。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from app.modules.time_sense.snapshot import TemporalSnapshot
|
||||
|
||||
|
||||
class MacroContext(BaseModel):
|
||||
"""符文渲染上下文:持有所有可被 ``{{...}}`` 占位符引用的值。
|
||||
|
||||
纯数据载体,无 IO。由运行时契约组装层填充(角色名 / 本相名 / 辰正快照等),
|
||||
交 ``render()`` 做文本替换。详见酒馆功能融合设计 §5(统一挂载架构)。
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
char_name: str | None = None
|
||||
user_name: str | None = None
|
||||
original: str | None = None
|
||||
snapshot: TemporalSnapshot | None = None
|
||||
variables: dict[str, str] = Field(default_factory=dict)
|
||||
Reference in new issue
Block a user