"""世谱匹配引擎:关键词 / 正则 / selective / constant / 递归扫描。 纯逻辑,无 IO。扫描文本由调用方(集成层)从对话历史截取后传入。 """ from __future__ import annotations import re from app.modules.lorebook.types import LorebookBook, LorebookEntry, MatchedEntry _RECURSION_MAX_ITERATIONS = 5 def match_entries(book: LorebookBook, scan_text: str) -> list[MatchedEntry]: """返回命中的条目(含 constant 与关键词命中);递归扫描按 book 配置。""" matched: list[MatchedEntry] = [] for entry in book.entries: if not entry.enabled: continue if entry.constant: matched.append(MatchedEntry(entry=entry, reason="constant")) continue if _entry_matches(entry, scan_text): matched.append(MatchedEntry(entry=entry, reason="key")) if book.recursive_scanning: matched = _expand_recursive(matched, book, scan_text) return matched def _entry_matches(entry: LorebookEntry, scan_text: str) -> bool: if entry.selective and entry.secondary_keys: return _any_key_hit(entry.keys, entry, scan_text) and _any_key_hit( entry.secondary_keys, entry, scan_text ) return _any_key_hit(entry.keys, entry, scan_text) def _any_key_hit(keys: list[str], entry: LorebookEntry, scan_text: str) -> bool: for key in keys: if not key: continue if entry.use_regex: flags = 0 if entry.case_sensitive else re.IGNORECASE try: if re.search(key, scan_text, flags): return True except re.error: # 非法正则降级为字面量包含 pass else: hay = scan_text if entry.case_sensitive else scan_text.casefold() needle = key if entry.case_sensitive else key.casefold() if needle in hay: return True return False def _expand_recursive( matched: list[MatchedEntry], book: LorebookBook, original_text: str ) -> list[MatchedEntry]: """已命中条目的 content 追加进扫描文本,再扫未命中条目;限迭代防振荡。""" hit_ids = {m.entry.id for m in matched} pool = list(matched) extra_text = original_text + "\n" + "\n".join(m.entry.content for m in matched) for _ in range(_RECURSION_MAX_ITERATIONS): grew = False for entry in book.entries: if entry.id in hit_ids or not entry.enabled or entry.constant: continue if _entry_matches(entry, extra_text): pool.append(MatchedEntry(entry=entry, reason="recursive")) hit_ids.add(entry.id) extra_text += "\n" + entry.content grew = True if not grew: break return pool