fix(writer): 防静默丢章告警 + 占位符正则宽容化(大小写/全角冒号)

This commit is contained in:
lhl
2026-08-23 14:47:14 +08:00
parent 48a9624723
commit da33df92e1
10 changed files with 119 additions and 9 deletions
+8 -4
View File
@@ -9,8 +9,12 @@ from docx.oxml.ns import qn
from genesis.data_models import ChapterMarker, ParsedTemplate
from genesis.parsers._word_common import heading_level
# 统一占位符正则:{{键名}} 或 {{键名:章节名}}spec §3.2
PLACEHOLDER_RE = re.compile(r"\{\{([a-z][a-z0-9_]*)(?::([a-z][a-z0-9_]*))?\}\}")
# 统一占位符正则:{{键名}} 或 {{键名:章节名}}spec §3.2
# 宽容:键名大小写不敏感、分隔符支持半角(:)/全角(:)冒号;解析时归一为小写键名 + 半角冒号。
PLACEHOLDER_RE = re.compile(
r"\{\{([A-Za-z][A-Za-z0-9_]*)(?:[:]([^}]+?))?\}\}",
re.IGNORECASE,
)
class WordTemplateParser:
@@ -37,9 +41,9 @@ class WordTemplateParser:
for m in PLACEHOLDER_RE.finditer(text):
if m.group(2):
key = f"{m.group(1)}:{m.group(2)}"
key = f"{m.group(1).lower()}:{m.group(2).strip()}"
else:
key = m.group(1)
key = m.group(1).lower()
placeholders[key] = text
sections.append(ChapterMarker(type="placeholder", name=key, level=0))
+2 -1
View File
@@ -11,7 +11,7 @@ from genesis.qa.validator import QAValidator
from genesis.writer.context_builder import build_contexts
from genesis.writer.docx_injector import Block, DocxInjector
from genesis.writer.models import ChapterContent
from genesis.writer.orchestrator import _section_id_of
from genesis.writer.orchestrator import _section_id_of, _warn_unanchored
from genesis.writer.renderer import render_chapter_blocks
from genesis.writer.writer_agent import WriterAgent
from genesis.writer.writer_state import WriterState
@@ -23,6 +23,7 @@ class QALoop:
def _build(self, structured_source, samples_dir, engine, prompt_registry, template_path, output_path, session_id, only_ids=None, prev=None):
ctxs = build_contexts(structured_source, samples_dir)
_warn_unanchored(ctxs)
state = WriterState([c.chapter_id for c in ctxs])
agent = WriterAgent(session_id=session_id, engine=engine, prompt_registry=prompt_registry, state=state)
contents_map = dict(prev) if prev else {}
+3 -2
View File
@@ -21,7 +21,8 @@ from docx.document import Document as DocxDocument
from docx.oxml.ns import qn
from docx.text.paragraph import Paragraph
_SECTION_RE = re.compile(r"\{\{section:([^}]+)\}\}")
# 宽容:docx 正文锚点可能写为 {{Section:id}} / {{sectionid}}(大小写/全角冒号)
_SECTION_RE = re.compile(r"\{\{\s*section\s*[:]\s*([^}]+?)\s*\}\}", re.IGNORECASE)
_INLINE_RE = re.compile(r"\{\{([^}]+)\}\}")
@@ -63,7 +64,7 @@ class DocxInjector:
m = _SECTION_RE.search(para.text)
if not m:
continue
section_id = m.group(1)
section_id = m.group(1).strip()
blocks = sections.get(section_id)
if blocks is None:
# 未提供该章节内容 → 保留占位符段落,交由残留检查报错
+18
View File
@@ -1,6 +1,7 @@
"""Writer 编排:上下文装配 → 逐章生成 → 渲染 → docx 注入(Phase 5 垂直切片)。"""
from __future__ import annotations
import logging
from pathlib import Path
from genesis.data_models import StructuredSource
@@ -13,6 +14,8 @@ from genesis.writer.renderer import render_chapter_blocks
from genesis.writer.writer_agent import WriterAgent
from genesis.writer.writer_state import WriterState
_LOGGER = logging.getLogger(__name__)
def _section_id_of(placeholder: str | None) -> str | None:
if not placeholder or not placeholder.startswith("section:"):
@@ -20,6 +23,20 @@ def _section_id_of(placeholder: str | None) -> str | None:
return placeholder[len("section:"):]
def _warn_unanchored(ctxs) -> None:
"""防静默丢章:对缺少 {{section:<id>}} 锚点的章节打显式告警。
管线会对模板中每个 Heading 都生成内容,但只有带锚点的章才会注入 docx;
无锚点章生成后会被丢弃。此函数将其从「静默丢弃」变为「可见告警」。
"""
unanchored = [ctx.title for ctx in ctxs if not ctx.template_marker.section_placeholder]
if unanchored:
_LOGGER.warning(
"章节已生成但模板缺少 {{section:<id>}} 锚点,内容未注入(静默丢弃):%s",
", ".join(unanchored),
)
class WriteOrchestrator:
def generate(
self,
@@ -34,6 +51,7 @@ class WriteOrchestrator:
engine = engine or build_inference_engine()
prompt_registry = prompt_registry or PromptRegistry()
ctxs = build_contexts(structured_source, samples_dir)
_warn_unanchored(ctxs)
state = WriterState([c.chapter_id for c in ctxs])
agent = WriterAgent(session_id=session_id, engine=engine, prompt_registry=prompt_registry, state=state)
+1 -1
View File
@@ -11,7 +11,7 @@ import re
from genesis.data_models import ParsedTemplate
from genesis.writer.models import ChapterSpec
_SECTION_RE = re.compile(r"^section:(.+)$")
_SECTION_RE = re.compile(r"^section:(.+)$", re.IGNORECASE)
def map_template(parsed: ParsedTemplate) -> list[ChapterSpec]: