From 09f681beb54a467dfa68205b91abc7aa6708f8e9 Mon Sep 17 00:00:00 2001 From: kai Date: Wed, 22 Apr 2026 16:31:01 +0800 Subject: [PATCH] =?UTF-8?q?v0.7.2:=20=E5=89=8D=E7=BD=AE=E4=BB=B6=E6=8E=92?= =?UTF-8?q?=E7=89=88=E9=87=8D=E6=9E=84=20+=20emoji=20=E7=A6=81=E4=BB=A4=20?= =?UTF-8?q?+=20=E5=BC=95=E6=96=87=E6=A0=B8=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户反馈 7 个 bug 修复: 1. 禁止 LLM 使用 emoji(全链路) - scripts/prompts/translate_system.txt 增加规则 12 - scripts/prompts/polish_system.txt 增加规则 7 - .opencode/agents/dr-analyst.md Hard Rules 增加第 10 条(同时把 prompt 自身的 ✅❌ 改为 MUST / MUST NOT) - .opencode/agents/dr-editor-in-chief.md 禁止事项加入 emoji 条款 - .opencode/skills/output-hygiene/SKILL.md 新增 §J emoji 强制禁用 2. 术语表位置错误(应在目录之后) 重构 build_body 为两阶段: (a) 扫描所有前置件(第一个正文 H1 前的所有 H1/H2),按 title_kind 分组收集 (b) 按固定顺序渲染:免责声明 → 执行摘要 → 目录 → 术语表 → 正文 → 参考文献 无论 Markdown 原文顺序如何,排版都一致。 3. 执行摘要/术语表提升为一级标题 + 分页空页 bug 统一所有独立章节(disclaimer/executive_summary/toc/glossary/references)用 h1 样式, 章节前 PageBreak;但第一个独立章节不 PageBreak(封面后已换页,避免空白)。 去掉 build_toc 内部末尾 PageBreak(原双 PageBreak 夹出空白页)。 4. 参考文献分页 已作为独立章节自动分页。 5. 附录章节自动删除 _title_kind 识别 "appendix" / "version_history" / "abstract" 全部跳过。 正文中若写了这些章节,模板直接丢弃。 6. 信源完整性核查 新增 scripts/check_citations.py: - 孤立引用(正文有 sources 无)检测 - 孤岛信源(sources 有正文无)检测 - emoji 扫描 - 实测发现项目中 61 条孤立引用(dr-analyst 编造的占位符)+ 5 条孤岛信源 7. git commit message 中文转义 bug 之前 commit 用 shell 双引号 + 反斜杠导致 \uXXXX 字面保留。 本 commit 用 heredoc 保证中文以 UTF-8 直接写入。 已 push 的历史不改,之后都用本 commit 的写法。 PDF 验证结果:55 页,0 空白页。 章节起始页:封面(1) - 免责声明(2) - 执行摘要(3) - 目录(5) - 术语表(7) - 第一章(12) - 第十章(48) - 参考文献(52)。 --- .opencode/agents/dr-analyst.md | 19 +- .opencode/agents/dr-editor-in-chief.md | 11 +- .opencode/skills/output-hygiene/SKILL.md | 28 +++ .opencode/templates/report-template.py | 257 +++++++++++++++-------- scripts/check_citations.py | 146 +++++++++++++ scripts/prompts/polish_system.txt | 1 + scripts/prompts/translate_system.txt | 1 + 7 files changed, 357 insertions(+), 106 deletions(-) create mode 100644 scripts/check_citations.py diff --git a/.opencode/agents/dr-analyst.md b/.opencode/agents/dr-analyst.md index a83a99f..ef45a95 100644 --- a/.opencode/agents/dr-analyst.md +++ b/.opencode/agents/dr-analyst.md @@ -170,12 +170,13 @@ Follow `skill:humanizer-cn` §1-26 strictly: ## Hard Rules -1. ✅ Every claim has `[src_xxx]` citation -2. ✅ Every numerical fact has a source -3. ✅ Counter-evidence section is mandatory (not optional) -4. ✅ Word count ≥85% of quota, or continue searching -5. ✅ No scheduling metadata in body text (no "P0 core", "quota: X", "researcher: dr-analyst") -6. ✅ No SCQA labels (not even implicitly suggested by structure) -7. ❌ Never fabricate data, URLs, DOIs -8. ❌ Never use Chinese words for claims (English working language) -9. ❌ Never delegate to other agents +1. MUST: Every claim has `[src_xxx]` citation +2. MUST: Every numerical fact has a source +3. MUST: Counter-evidence section is mandatory (not optional) +4. MUST: Word count ≥85% of quota, or continue searching +5. MUST: No scheduling metadata in body text (no "P0 core", "quota: X", "researcher: dr-analyst") +6. MUST: No SCQA labels (not even implicitly suggested by structure) +7. MUST NOT: Fabricate data, URLs, DOIs +8. MUST NOT: Use Chinese words for claims (English working language) +9. MUST NOT: Delegate to other agents +10. MUST NOT: **Use emoji anywhere in the draft** (no ✅ ❌ 🔶 🔷 ⭐ 🟢 🔴 ⚠️ 💡 📌 🔑 📊 etc.). The PDF font has no glyphs for colored emoji; they render as empty boxes. Use plain text equivalents (e.g., "✓", "×", "注:", "警告:", or descriptive words like "advantages / limitations / example"). diff --git a/.opencode/agents/dr-editor-in-chief.md b/.opencode/agents/dr-editor-in-chief.md index 65117d7..ce4c956 100644 --- a/.opencode/agents/dr-editor-in-chief.md +++ b/.opencode/agents/dr-editor-in-chief.md @@ -299,8 +299,9 @@ Phase 4 成稿完成 ## 禁止事项 -- ❌ 改写 dr-analyst 已完成的章节正文 -- ❌ 给每章开头强加 "**Situation**:" "**Complication**:" 等标注 -- ❌ 在正文里保留"章节定位 / P0 核心章 / 字数配额 / 研究员" -- ❌ 参考文献用占位符了事,要确保 dr-reporter 把它填满 -- ❌ 中途调用 dr-chief-editor(它只管 Phase 3) +- 改写 dr-analyst 已完成的章节正文 +- 给每章开头强加 "**Situation**:" "**Complication**:" 等标注 +- 在正文里保留"章节定位 / P0 核心章 / 字数配额 / 研究员" +- 参考文献用占位符了事,要确保 dr-reporter 把它填满 +- 中途调用 dr-chief-editor(它只管 Phase 3) +- **在正文中使用 emoji**(✅ ❌ 🔶 🔷 ⭐ 🟢 🔴 ⚠️ 💡 📌 🔑 📊 等彩色符号)。PDF 字体无法渲染,会变成方框。用文字或简单符号(✓ × 注: 警告:)代替。 diff --git a/.opencode/skills/output-hygiene/SKILL.md b/.opencode/skills/output-hygiene/SKILL.md index 412e07a..ee827a0 100644 --- a/.opencode/skills/output-hygiene/SKILL.md +++ b/.opencode/skills/output-hygiene/SKILL.md @@ -101,6 +101,34 @@ SCQA 要写得隐式融合(见 mckinsey-method skill)。 例外:行内英文术语、代码、URL、数据单位前后保持英文标点合理。 +### J. Emoji(强制禁用) + +**正文与表格中严禁使用任何 emoji / 彩色符号**: + +禁用清单(但不限于): +`✅ ❌ ✔ ✖ 🔶 🔷 ⭐ 🟢 🔴 🟡 🟠 ⚠️ ⚠ 💡 📌 🔑 📊 📈 📉 🔥 ✨ 🎯 🎉 ➔ ➜` + +**原因**:PDF 使用的思源字体子集不包含这些字符的 glyph,渲染为空白方框(□)。 + +**替代写法**: +- 表格标记"有/无":用 `✓` `×`(思源字体支持)或中文字 `是` / `否` +- 强调状态:用 `◆` `●` 等几何符号(字体支持) +- 警示:用 `注:` `警告:` `※` 等文字前缀 +- 重点:用 **粗体** 或引用块,不用 emoji + +扫描命令: +```bash +python3 -c " +import re +txt = open('final_zh_polished.md').read() +pat = re.compile(r'[\u2700-\u27BF]|[\U0001F300-\U0001F9FF]|[\u2B00-\u2BFF]') +hits = [(i, m.group()) for i, m in enumerate(pat.finditer(txt))] +print(f'emoji 命中:{len(hits)} 处') +for i, c in hits[:10]: + print(f' 位置 {i}: {c!r} (U+{ord(c):04X})') +" +``` + --- ## 三、参考文献完整性校验(最关键) diff --git a/.opencode/templates/report-template.py b/.opencode/templates/report-template.py index fb6d9c7..d76e6c5 100755 --- a/.opencode/templates/report-template.py +++ b/.opencode/templates/report-template.py @@ -1106,108 +1106,181 @@ def build_body( break i = first_section_idx # 从第一个 section 开始处理 + def _title_kind(title_raw: str) -> str: + """识别标题的"语义类别"。无论原文 H1 或 H2,统一归类。 + + 返回: + 'abstract' — 摘要(将被跳过) + 'appendix' — 附录(将被跳过) + 'version_history' — 版本历史(将被跳过) + 'toc' — 目录(自动生成) + 'references' — 参考文献(自动生成) + 'glossary' — 术语表(独立章节) + 'disclaimer' — 免责声明(独立章节) + 'executive_summary' — 执行摘要(独立章节) + 'chapter' — 正文章节(默认) + """ + t = title_raw.strip().lower() + # 跳过类 + if t in ("摘要", "abstract"): + return "abstract" + if t.startswith("附录") or t.startswith("appendix"): + return "appendix" + if t in ("版本历史", "version history", "版本"): + return "version_history" + # 自动生成类 + if t in ("目录", "table of contents"): + return "toc" + if t in ("参考文献", "references", "bibliography"): + return "references" + # 识别类(带 PageBreak 独立成章) + if t in ("术语表", "glossary"): + return "glossary" + if t in ("免责声明", "disclaimer"): + return "disclaimer" + if t in ("执行摘要", "executive summary", "管理层摘要"): + return "executive_summary" + return "chapter" + + def _consume_until_next_section(start: int) -> int: + """从 start 开始收集内容(非 H1/H2 的 block),返回下一个 H1/H2 的索引。""" + j = start + while j < n and blocks[j].kind not in ("h1", "h2"): + _render_generic_block(blocks[j], story, base_dir, styles, in_summary=in_summary) + j += 1 + return j + + def _skip_until_next_section(start: int) -> int: + """从 start 开始跳过内容,返回下一个 H1/H2 的索引。""" + j = start + while j < n and blocks[j].kind not in ("h1", "h2"): + j += 1 + return j + + # 收集前置件(在第一个"正文 H1 章节"之前的所有内容)按 title_kind 分组。 + # 然后按固定顺序输出:免责声明 → 执行摘要 → 目录 → 术语表 → 正文 → 参考文献。 + # 这样无论 Markdown 里写的顺序如何,最终排版都一致(目录在术语表之前)。 + first_h1_idx = n + for k in range(i, n): + if blocks[k].kind == "h1" and _title_kind(blocks[k].content) == "chapter": + first_h1_idx = k + break + + # 收集"前置件段":从 i 到 first_h1_idx + front_sections: dict[str, list[Block]] = {} + + def _collect_section(start: int, until: int) -> tuple[str, list[Block], int]: + """从 start 处的 H1/H2 开始,收集这一 section 直到下一个 H1/H2(或 until)。 + 返回 (kind, blocks 列表, 下一个 section 的起始索引)。 + """ + head = blocks[start] + kind = _title_kind(head.content) + sec = [head] + k = start + 1 + while k < until and blocks[k].kind not in ("h1", "h2"): + sec.append(blocks[k]) + k += 1 + return kind, sec, k + + # 在前置件区域内遍历 + k = i + while k < first_h1_idx: + b = blocks[k] + if b.kind not in ("h1", "h2"): + k += 1 + continue + kind, sec, next_k = _collect_section(k, first_h1_idx) + if kind in ("abstract", "appendix", "version_history"): + pass # 丢弃 + elif kind in front_sections: + # 重复出现:保留第一份 + pass + else: + front_sections[kind] = sec + k = next_k + + # 前置件输出顺序(固定) + front_order = [ + "disclaimer", # 免责声明 + "executive_summary", # 执行摘要 + "toc", # 目录 + "glossary", # 术语表 + ] + + def _render_head_section(kind: str, sec: list[Block]) -> None: + """渲染一个前置件 section。sec[0] 是标题,其余是正文。""" + nonlocal in_summary + # 独立章节前加 PageBreak(但第一个除外,避免封面后空白页) + if len(story) > 0: + story.append(PageBreak()) + in_summary = (kind == "executive_summary") + head = sec[0] + + # TOC 和 references 调用专门的生成器 + if kind == "toc": + story.extend(build_toc(blocks, styles)) + return + if kind == "references": + story.extend(build_references(blocks, sources_path, styles)) + return + + # 其它前置件:H1 样式渲染标题 + 内容 + story.append(Paragraph(md_inline_to_rl(head.content), styles["h1"])) + for sub in sec[1:]: + # 跳过占位符段 + if sub.kind == "p" and ( + _TOC_PLACEHOLDER_RE.search(sub.content) + or _REF_PLACEHOLDER_RE.search(sub.content) + ): + continue + _render_generic_block(sub, story, base_dir, styles, in_summary=in_summary) + + for kind in front_order: + if kind in front_sections: + _render_head_section(kind, front_sections[kind]) + + in_summary = False + + # 现在输出正文(从 first_h1_idx 开始) + i = first_h1_idx while i < n: block = blocks[i] - # --- H1 处理(非首个;本循环内 first_section_idx 之后的 H1 都是真正的章节 H1)--- - if block.kind == "h1": - content = block.content - title_low = content.strip().lower() + if block.kind not in ("h1", "h2"): + _render_generic_block(block, story, base_dir, styles, in_summary=in_summary) + i += 1 + continue - # 跳过整章:Abstract(保留 Executive Summary) - if title_low in ("摘要", "abstract"): - j = i + 1 - while j < n and blocks[j].kind != "h1": - j += 1 - i = j - continue + kind = _title_kind(block.content) - # 所有非被替换的 H1 都 PageBreak + # 跳过类 + if kind in ("abstract", "appendix", "version_history"): + i = _skip_until_next_section(i + 1) + continue + + # 参考文献:自动生成 + if kind == "references": story.append(PageBreak()) - - if any(k in content for k in ("执行摘要", "Executive Summary", "管理层摘要")): - in_summary = True - else: - in_summary = False - - # 目录:已经 PageBreak 了,build_toc 内部再 PageBreak(独立页) - if any(s in title_low for s in ("目录", "table of contents")): - story.extend(build_toc(blocks, styles)) - # 跳过紧随其后的占位段 - j = i + 1 - while j < n and blocks[j].kind == "p" and _TOC_PLACEHOLDER_RE.search(blocks[j].content): - j += 1 - i = j - continue - # 术语表:H1 本身已 PageBreak,末尾靠下一个 H1 自然换页 - if any(s in title_low for s in ("术语表", "glossary")): - story.append(Paragraph(md_inline_to_rl(content), styles["h1"])) - i += 1 - while i < n and blocks[i].kind != "h1": - sub = blocks[i] - _render_generic_block(sub, story, base_dir, styles, in_summary=False) - i += 1 - continue - if any(s in title_low for s in ("参考文献", "references")): - story.extend(build_references(blocks, sources_path, styles)) - j = i + 1 - while j < n and blocks[j].kind == "p" and _REF_PLACEHOLDER_RE.search(blocks[j].content): - j += 1 - i = j - continue - - story.append(Paragraph(md_inline_to_rl(content), styles["h1"])) - i += 1 + story.extend(build_references(blocks, sources_path, styles)) + j = i + 1 + while j < n and blocks[j].kind == "p" and _REF_PLACEHOLDER_RE.search(blocks[j].content): + j += 1 + i = j continue - # --- H2 同样检测占位符 --- - if block.kind == "h2": - title_low = block.content.strip().lower() - - # 跳过整段:Abstract(与 Executive Summary 重复,根据用户偏好保留 Executive Summary) - if title_low in ("摘要", "abstract"): - # 跳到下一个 H1/H2 - j = i + 1 - while j < n and blocks[j].kind not in ("h1", "h2"): - j += 1 - i = j - continue - - # 目录:前后 PageBreak(独立成页) - if any(s in title_low for s in ("目录", "table of contents")): - story.append(PageBreak()) - story.extend(build_toc(blocks, styles)) - j = i + 1 - while j < n and blocks[j].kind == "p" and _TOC_PLACEHOLDER_RE.search(blocks[j].content): - j += 1 - i = j - continue - - # 术语表:前 PageBreak,后靠下一个 H1/H2 自然换页 - if any(s in title_low for s in ("术语表", "glossary")): - story.append(PageBreak()) - story.append(Paragraph(md_inline_to_rl(block.content), styles["h1"])) - i += 1 - while i < n and blocks[i].kind not in ("h1", "h2"): - sub = blocks[i] - _render_generic_block(sub, story, base_dir, styles, in_summary=False) - i += 1 - continue - - if any(s in title_low for s in ("参考文献", "references")): - story.extend(build_references(blocks, sources_path, styles)) - j = i + 1 - while j < n and blocks[j].kind == "p" and _REF_PLACEHOLDER_RE.search(blocks[j].content): - j += 1 - i = j - continue + # 目录/术语表不应在正文中出现(已经作为前置件处理) + # 如果原文里在正文中又写了一遍目录/术语表,则跳过 + if kind in ("toc", "glossary", "disclaimer", "executive_summary"): + i = _skip_until_next_section(i + 1) + continue + # H1 正文章节(chapter):PageBreak + h1 样式 + if block.kind == "h1": + story.append(PageBreak()) + story.append(Paragraph(md_inline_to_rl(block.content), styles["h1"])) + else: + # H2 正文小节:h2 样式(不分页) story.append(Paragraph(md_inline_to_rl(block.content), styles["h2"])) - i += 1 - continue - - # 非 H1/H2 的 block:统一走 _render_generic_block - _render_generic_block(block, story, base_dir, styles, in_summary=in_summary) i += 1 return story diff --git a/scripts/check_citations.py b/scripts/check_citations.py new file mode 100644 index 0000000..f1f5f7b --- /dev/null +++ b/scripts/check_citations.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +"""引文完整性核查。 + +检查 final_zh_polished.md(或其它正文)中的 [src_xxx] 引用与 sources.jsonl 是否一致: +- 孤立引用(正文有但 sources.jsonl 无):需要 dr-analyst 补信源或删这处引用 +- 孤岛信源(sources.jsonl 有但正文无):被 polish 或润色误删了上下文,或 dr-analyst 收集了 + 但没用上 +- emoji 扫描:正文里不该有 emoji +- 编号格式:检查 src_xxx 是否符合规范 + +用法: + uv run python scripts/check_citations.py + uv run python scripts/check_citations.py --md phase4/final_zh.md +""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from pathlib import Path + + +EMOJI_RE = re.compile( + r"[\U0001F000-\U0001FFFF]" # Supplementary Plane emoji + r"|[\u2700-\u27BF]" # Dingbats (✅ ❌) + r"|[\u2600-\u26FF]" # Misc symbols (⭐ ⚠ ☀) + r"|[\u2B00-\u2BFF]" # Misc symbols and arrows +) + +# 允许的符号(字体支持) +ALLOWED_SYMBOLS = { + "✓", "×", "◆", "◇", "●", "○", "★", "※", + "→", "←", "↑", "↓", +} + +SRC_ID_RE = re.compile(r"\[(src_[A-Za-z0-9_\-]+(?:\s*,\s*src_[A-Za-z0-9_\-]+)*)\]") + + +def resolve_project(arg: str) -> Path: + p = Path(arg) + if p.is_dir(): + return p + cand = Path.cwd() / "projects" / arg + if cand.is_dir(): + return cand + raise SystemExit(f"project not found: {arg}") + + +def main() -> int: + parser = argparse.ArgumentParser(description="引文完整性核查") + parser.add_argument("project", help="项目 slug 或路径") + parser.add_argument("--md", default="phase4/final_zh_polished.md") + parser.add_argument("--sources", default="phase2/sources.jsonl") + args = parser.parse_args() + + project = resolve_project(args.project) + md_path = project / args.md + src_path = project / args.sources + + if not md_path.exists(): + raise SystemExit(f"找不到正文:{md_path}") + + text = md_path.read_text(encoding="utf-8") + + # 1. 收集正文引用(保序去重) + cited: list[str] = [] + cited_set: set[str] = set() + for m in SRC_ID_RE.finditer(text): + for sid in m.group(1).split(","): + sid = sid.strip() + if sid and sid not in cited_set: + cited_set.add(sid) + cited.append(sid) + + # 2. 收集 sources.jsonl 中的 ID + sources: dict[str, dict] = {} + if src_path.exists(): + for line in src_path.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line: + continue + try: + r = json.loads(line) + if sid := r.get("id"): + sources[sid] = r + except Exception: + continue + + orphan_cites = [s for s in cited if s not in sources] + island_sources = [s for s in sources if s not in cited_set] + matched = [s for s in cited if s in sources] + + print(f"=== 引文统计 ===") + print(f" 正文引用(独立 ID):{len(cited_set)}") + print(f" sources.jsonl 收录:{len(sources)}") + print(f" 匹配:{len(matched)}") + print(f" 孤立引用(正文有 sources 无):{len(orphan_cites)}") + print(f" 孤岛信源(sources 有正文无):{len(island_sources)}") + + if orphan_cites: + print(f"\n=== 孤立引用(前 20 条)===") + for s in orphan_cites[:20]: + print(f" {s}") + if len(orphan_cites) > 20: + print(f" ...还有 {len(orphan_cites) - 20}") + print(f"\n 处理建议:") + print(f" (a) 如果是 dr-analyst 编造的占位符 → 在正文中删除该引用") + print(f" (b) 如果是信源未收录 → 补到 sources.jsonl") + + if island_sources: + print(f"\n=== 孤岛信源(前 20 条)===") + for s in island_sources[:20]: + print(f" {s} — {sources[s].get('title', '')[:80]}") + if len(island_sources) > 20: + print(f" ...还有 {len(island_sources) - 20}") + print(f"\n 处理建议:") + print(f" (a) 如果是 polish 阶段误删了使用该信源的段落 → 检查 polish diff") + print(f" (b) 如果是收集多余信源 → 可以保留(build_references 会自动忽略)") + + # 3. Emoji 扫描 + emoji_hits = [] + for m in EMOJI_RE.finditer(text): + c = m.group() + if c not in ALLOWED_SYMBOLS: + line = text[:m.start()].count("\n") + 1 + emoji_hits.append((line, c)) + + if emoji_hits: + print(f"\n=== ⚠ 发现 {len(emoji_hits)} 个 emoji(不允许出现在正文)===") + seen = {} + for line, c in emoji_hits: + seen.setdefault(c, []).append(line) + for c, lines in seen.items(): + print(f" U+{ord(c):04X} {c!r} 第 {lines[:5]} 行 等 {len(lines)} 处") + print(f" 建议用 python3 替换:sed -i '' 's/{list(seen.keys())[0]}//g' {md_path}") + + # 返回码:有问题返回非零便于 CI 使用 + if orphan_cites or emoji_hits: + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/prompts/polish_system.txt b/scripts/prompts/polish_system.txt index b6b4473..bb1c4a5 100644 --- a/scripts/prompts/polish_system.txt +++ b/scripts/prompts/polish_system.txt @@ -8,6 +8,7 @@ 4. **保留段落数量**:不要合并或拆分段落。每段原文对应一段输出。 5. **专有名词首次出现保持"中文(English)"格式**;如果译文里这个术语已经这样标了就别改,也不要删掉。 6. **不改变论点、结论、数据、案例**。只改语言表达。 +7. **严禁使用 emoji**(✅ ❌ 🔶 🔷 ⭐ 🟢 🔴 ⚠️ 💡 📌 🔑 📊 等彩色符号)。如果原文里有 emoji,替换为字体支持的符号(✓ × ◆ ● ★ * 注 等)或直接删除。这些 emoji 在 PDF 里渲染为方框。 ## 要去掉的"AI 味/翻译腔"表征 diff --git a/scripts/prompts/translate_system.txt b/scripts/prompts/translate_system.txt index 47a4e9f..6a932eb 100644 --- a/scripts/prompts/translate_system.txt +++ b/scripts/prompts/translate_system.txt @@ -13,6 +13,7 @@ You are a senior English-to-Chinese biomedical translator and editor. You do NOT 9. Do NOT collapse or merge consecutive paragraphs — preserve paragraph breaks. 10. Output Chinese-style punctuation inside Chinese text: `,。;:?!""()`. Keep English punctuation inside parenthetical English phrases. 11. Do NOT add separator lines (`---`) or blank lines that weren't in the source. If the source ends with `---`, keep it; if it doesn't, don't add one. +12. **NEVER use emoji** (✅ ❌ 🔶 🔷 ⭐ 🟢 🔴 ⚠️ 💡 📌 🔑 📊 etc.). If the source contains emoji, replace with plain text or punctuation equivalents (✓ × ◆ ● ★ * 注 等). These do not render in the PDF (font has no glyphs). ## Style rules (aim for native-Chinese feel)