diff --git a/PROGRESS.md b/PROGRESS.md index f80b2be..c13e537 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -86,3 +86,4 @@ Python 合计 ~1669 行 3. **小修打磨**(~半小时)—— `Session.save()` 改原子写(tmp + rename),防 surrogate 等异常 truncate 4. **Phase 7 Docker 沙盒**(~1 天)—— 替换 subprocess,run_python 安全升级 5. **Phase 7 更多 skill / 模型档案**(持续) +6. **Proposal mermaid 流程图预渲染**(~半天,看到第二张图再做)—— 现状是 ASCII 框图走 fenced code 透传 (新宋体 + Consolas + xml:space=preserve),中文与 box drawing 字符宽度对不齐时还是有错位。增强方案: ` ```mermaid ` 块在 `render_docx.py` 里调 `mmdc` (mermaid-cli) → PNG → `add_picture` 嵌入。依赖 Node.js + `npm i -g @mermaid-js/mermaid-cli`,首次配置略麻烦,所以等 ASCII 透传明显不够用再做 diff --git a/skills/proposal/SKILL.md b/skills/proposal/SKILL.md index 25391a9..eff7378 100644 --- a/skills/proposal/SKILL.md +++ b/skills/proposal/SKILL.md @@ -47,15 +47,23 @@ markitdown https://example.com/x -o /source/policy.md ## 阶段二: 逐章起草 -每章流程: -1. 读 `/spec_lock.md` 与 `/references/fund_types.md` 拿本章字数预算与必填要素 -2. 复制 `/templates/.md` 对应小节到 `/sections/NN_xxx.md`,填空 -3. 报告: 章节名 / 实际字数 / 字数预算 / 与指南对齐情况 -4. ⛔ **BLOCKING:停下来等用户明确反馈** ("OK"、"下一章"、"继续") 后才进下一章。"看起来不错"、沉默、追问都不算确认 —— 主动问"这一章可以了吗?要改哪里?" +每章两段式:**先列要点 → 用户确认 → 再起草 → 用户确认**。不要直接出正文。 -**为什么逐章 + 强等?** 申报书 1.5-3 万字,模型连续生成时容易自我加速、把错的方向推到底。每章一卡可以在第 2 章被用户拦下。 +**A. 起草前列要点** (改要点比改正文便宜): +1. 读 `/spec_lock.md` 与 `/references/fund_types.md`,拿本章字数预算与必填要素 +2. 列出 3-6 条要点骨架: 本章打算覆盖的论点 / 数据 / 表格,每条贴上对齐的指南要素与预估字数 +3. ⛔ **BLOCKING:用户确认要点 (改 / 加 / 删) 后才动正文** -**例外**: 用户**主动且明确**说"别问,直接全做"或"一气呵成" —— 才能一次跑完,跑完必须 `quality_check.py`。"逐章太慢"之类的抱怨**不算**例外指令,继续问。 +**B. 正文起草**: +4. 复制 `/templates/.md` 对应小节到 `/sections/NN_xxx.md`,按要点填 +5. **关键章节一段一卡** —— 立项依据 / 研究方案 / 技术路线 / 考核指标矩阵: 写一段 → 报字数 → 等用户确认 → 写下一段 + 普通章节一节一卡: 整节写完再报 +6. 报告: 章节名 / 实际字数 / 字数预算 / 与指南对齐情况 +7. ⛔ **BLOCKING:停下来等用户明确反馈** ("OK"、"下一章"、"继续") 后才进下一章。"看起来不错"、沉默、追问都不算确认 —— 主动问"这一章/这一段可以了吗?要改哪里?" + +**为什么两段式 + 强等?** 申报书 1.5-3 万字,模型连续生成容易自我加速、把错方向推到底。要点阶段拦得早,关键章节段段卡可以在第 2 段被用户拦下。 + +**例外**: 用户**主动且明确**说"别问,直接全做"或"一气呵成" —— 才能一次跑完,跑完必须 `quality_check.py`。"逐章太慢"/"段段太碎"之类的抱怨**不算**例外指令,继续问。 ## 阶段三: 验收 + 渲染 @@ -106,6 +114,8 @@ python /scripts/render_docx.py /sections/ --fund-type key - 未 spec_lock 就开始硬编正文 - 一次性出全文 (中途改向就全推翻) +- 跳过"列要点"直接写正文 —— 即使只有一章也要先列要点过一遍 +- 关键章节 (立项依据 / 研究方案 / 技术路线 / 考核指标) 整章一次出 —— 必须段段卡 - **基于"通用模板"自行套基金类型** —— 重大专项任务书与国自然申请书结构完全不同,**先查 `fund_types.md`** - **自己造数据/指标/单位/经费** —— 不知道就 ``,不要硬编 - 引文写"[Smith et al., 2023]" 但其实没这篇文献 diff --git a/skills/proposal/scripts/render_docx.py b/skills/proposal/scripts/render_docx.py index 9fcff60..45faee7 100644 --- a/skills/proposal/scripts/render_docx.py +++ b/skills/proposal/scripts/render_docx.py @@ -224,6 +224,7 @@ _HEADING_RE = re.compile(r"^(#{1,6})\s+(.+)$") _TABLE_LINE_RE = re.compile(r"^\s*\|.*\|\s*$") _BLOCKQUOTE_RE = re.compile(r"^\s*>\s?") _HR_RE = re.compile(r"^\s*-{3,}\s*$|^\s*={3,}\s*$|^\s*_{3,}\s*$") +_FENCE_RE = re.compile(r"^\s*(`{3,}|~{3,})\s*(\S*)\s*$") # 列表项 (各自独立成段, 不跟相邻行合并, 不缩进首行) _LIST_PATTERNS = [ @@ -259,6 +260,29 @@ def is_hr(line: str) -> bool: return bool(_HR_RE.match(line)) +# ───────────────────────── 代码块 / ASCII 图 ───────────────────────── + +def add_code_block(doc: Document, lines: list[str], lang: str = "") -> None: + """fenced ``` 块 / ASCII 流程图: 等宽字体 + 行距 1.0 + 不缩进 + 不解析内联 + 保留空格。 + + 中文用"新宋体"(NSimSun, Windows 自带等宽宋体), 西文用 Consolas, 这样 + `─ │ ┌ ┐` 这类 box drawing 字符与中文字符的视觉宽度更接近, ASCII 流程图不至于错位。 + """ + for ln in lines: + p = doc.add_paragraph() + pf = p.paragraph_format + pf.first_line_indent = None + pf.line_spacing = 1.0 + pf.space_before = Pt(0) + pf.space_after = Pt(0) + run = p.add_run(ln if ln else " ") # 空行也占一行高 + run.font.size = Pt(10.5) # 五号 + _set_run_fonts(run, cn_font="新宋体", en_font="Consolas") + # docx 默认会压缩连续空格 -> 显式 xml:space=preserve, 否则 ASCII 对齐会被破坏 + for t in run._element.iter(qn("w:t")): + t.set(qn("xml:space"), "preserve") + + # ───────────────────────── 表格 ───────────────────────── def _split_md_row(line: str) -> list[str]: @@ -322,6 +346,24 @@ def render_md_block(doc: Document, md_text: str) -> None: i += 1 continue + # fenced 代码块 / ASCII 流程图 (```...``` 或 ~~~...~~~) + m_fence = _FENCE_RE.match(line) + if m_fence: + fence = m_fence.group(1) + lang = m_fence.group(2) or "" + code: list[str] = [] + i += 1 + while i < n: + m_close = _FENCE_RE.match(lines[i]) + # 闭围栏: 同种符号 (` vs ~) 且长度 ≥ 开围栏 + if m_close and m_close.group(1)[0] == fence[0] and len(m_close.group(1)) >= len(fence): + i += 1 + break + code.append(lines[i]) # 不 rstrip, 保留原始空格 + i += 1 + add_code_block(doc, code, lang) + continue + # 表格 (连续若干行 | ... | 视为一张表) if is_table_line(line): block: list[str] = []