skills+core(命名约定): task 级宪法文件 <date>-<short_id>-<name>.spec.md + spec_lock → spec 简化

同 working_dir 多 task 共享中间产物是设计意图(素材跨本子复用),
但 spec 这种 task 1:1 宪法文件必须隔离 — 否则两本子 spec 直接撞。
文件名三段式:
- task_short_id (task_id.hex[:8],永不变) 主锚 → glob *-<short_id>-*.spec.md 字典序最大 = current
- date 让"重定调"写新文件而非 edit 覆盖,旧版自然成历史快照
- task_name 作建时元数据,改 task.name 不 cascade(由 short_id 兜底定位)

约定由 core/agent_builder.py::_build_system_prompt 单点注入
(task_id / today 实际值嵌入,所有 skill SKILL.md 引用同一份)。
proposal / ppt SKILL.md 阶段一加"glob 检测已有 spec → 询问沿用/重定调"分支。
模板 templates/spec_lock.md → spec.md (git mv 保历史),_lock 后缀无信息量去掉。

未动:DB schema / PATCH /v1/tasks/{id} 改 name 入口 / 其他中间产物扁平共享
/ quality_check.py (--spec 接路径)。反方案(cascade rename / spec 入 PG /
物理 task 子目录)及"何时升级到 DB 化"信号见 DESIGN §7.9。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
caoqianming 2026-05-20 14:03:21 +08:00
parent 775962d68a
commit c4fac2428b
15 changed files with 151 additions and 77 deletions

4
.gitignore vendored
View File

@ -21,8 +21,8 @@ venv/
env/ env/
# 用户运行产物 / 临时文件 # 用户运行产物 / 临时文件
# task 内产物(sections/ slides/ spec_lock.md/ *.docx/*.pptx)都在 workspace/tasks/<id>/ 下, # task 内产物(sections/ slides/ *.spec.md/ *.docx/*.pptx)都在 workspace/ 下,
# 由上面这条 workspace/ 一并忽略。repo 根级别的 sections/ / slides/ / spec_lock.md # 由上面这条 workspace/ 一并忽略。repo 根级别的 sections/ / slides/ / *.spec.md
# **故意不忽略**——如果 agent 又写错位置,要靠 git status 立刻暴露,不再用 .gitignore 兜底。 # **故意不忽略**——如果 agent 又写错位置,要靠 git status 立刻暴露,不再用 .gitignore 兜底。
workspace/ workspace/
*.log *.log

View File

@ -447,6 +447,8 @@ create index on usage_events (model_profile, created_at);
**Tasks/Messages 在 PG 但 skill 产物在 FS**:tasks / messages 需要查询、过滤、全文搜、跨 task 统计 — DB 强项;skill 产物(`*.pptx` / `*.docx` / `sections/*.md`)终用户拿走,期望文件管理器看到、Office 打开、邮件发出 — 进 DB 要做"导出"多余操作。**FS 是产物天然存储,DB 是元数据 / 状态 / 索引天然存储**。同理 §7.5 bind mount = user root,容器里 ≡ 用户在 Web UI 看到的目录,无中间层翻译。 **Tasks/Messages 在 PG 但 skill 产物在 FS**:tasks / messages 需要查询、过滤、全文搜、跨 task 统计 — DB 强项;skill 产物(`*.pptx` / `*.docx` / `sections/*.md`)终用户拿走,期望文件管理器看到、Office 打开、邮件发出 — 进 DB 要做"导出"多余操作。**FS 是产物天然存储,DB 是元数据 / 状态 / 索引天然存储**。同理 §7.5 bind mount = user root,容器里 ≡ 用户在 Web UI 看到的目录,无中间层翻译。
**task 级「宪法」文件靠文件名隔离,不 cascade / 不入 DB / 不开物理子目录**(2026-05-20):同 working_dir 多 task **共享中间产物**(`source/` / `sections/` / `figures/`)是真实价值(素材跨多本子复用),但 spec 这种 task 1:1 宪法文件必须隔离(两本子 spec 直接撞)。文件名 `<YYYY-MM-DD>-<task_short_id>-<task_name>.<base>.md`:`task_short_id`(`task_id.hex[:8]`,永不变)主锚,glob `*-<short_id>-*.<base>.md` 字典序最大 = current 版本;`<YYYY-MM-DD>` 让"重定调"写新文件而非 edit 覆盖,旧版自然成历史快照;`<task_name>` 仅作"建时元数据 / 人类可读说明",改 name 不 cascade(由 short_id 兜底定位)。**反方案不选**:① cascade rename — in-flight run 期间文件丢 + 复杂度上升;② DB 化(spec 入 PG)— 架构最干净但工作量 5-10×,且失"用户直接编辑 markdown"能力,且 spec 字段还在演化没必要这么早 schema 化;③ 物理 task 子目录(`<working_dir>/<task>/`)— 破坏 §7.4 中间产物扁平共享设计。**升级到 DB 化的信号**:dev SPA 想做结构化编辑视图 / 想跨 task 查询 spec 字段(基金类型 / 经费 / 考核指标)/ markdown 版本文件堆积乱。约定由 `core/agent_builder.py::_build_system_prompt` 单点注入(`task_id` / `today` 实际值嵌入),所有 skill SKILL.md 引用同一份(目前 proposal / ppt 的 `spec`,未来 `outline` 等同款)。
--- ---
## 附录:DeepSeek V4 关键事实(2026-04-24) ## 附录:DeepSeek V4 关键事实(2026-04-24)

View File

@ -2,7 +2,7 @@
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 2-4 句:做了啥 + 关键判断 + 没动什么;细节查 `git log` / `git diff` > 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 2-4 句:做了啥 + 关键判断 + 没动什么;细节查 `git log` / `git diff`
最后更新:2026-05-20(dev SPA 左 pane 折叠改 40px rail 模式 + time-ago 锁宽让 N条/Ntok 跨行对齐 + 删 header 冗余按钮) 最后更新:2026-05-20(task 级「宪法」文件命名约定 + `spec_lock``spec` 简化 — 解决同 working_dir 多 task 的 spec 文件冲突)
--- ---
@ -23,6 +23,7 @@
### 2026-05-20 ### 2026-05-20
- **task 级「宪法」文件 (spec) 命名约定 + `spec_lock` → `spec` 简化**:同 working_dir 多 task 共享中间产物(`source/` / `sections/` / `figures/` 跨本子复用)是设计意图,但 spec 这种 task 1:1 宪法文件必须隔离 — 两本子 spec 直接撞。文件名约定 `<YYYY-MM-DD>-<task_short_id>-<task_name>.spec.md`:`task_short_id`(`task_id.hex[:8]`,永不变)作主锚,glob `*-<short_id>-*.spec.md` 字典序最大 = current;`<YYYY-MM-DD>` 让"重定调"写新文件而非 edit 覆盖,旧版自然成历史快照;`<task_name>` 写入作建时元数据,改 task.name 不 cascade(由 short_id 兜底定位)。`core/agent_builder.py::_build_system_prompt` 加 `task_id` / `today` 注入 + 命名约定段 — 所有 skill 共享一份约定文本,SKILL.md 不再重复;proposal / ppt SKILL.md 阶段一加"先 glob 检测已有 spec → 询问沿用/重定调"分支。`_lock` 后缀无信息量去掉(`templates/spec_lock.md` → `templates/spec.md` git mv 保历史)。**没动**:DB schema(无新字段)、`PATCH /v1/tasks/{id}` 改 name 入口(免 cascade)、其他中间产物扁平共享、quality_check.py(`--spec` 接路径,SKILL.md 拼对参数即可)。**反方案**(cascade rename / spec 入 PG / 物理 task 子目录)及"何时升级到 DB 化"信号见 DESIGN §7.9 取舍说明。
- **dev SPA 左 pane 折叠改 rail 模式 + 删 header 冗余按钮 + time-ago 锁宽完成跨行对齐**:用户反馈 ① "原来 zcbot 旁的折叠按钮不要了,没用处" + ② "数字对齐那块现在是不是每块内容左侧对齐?"(实际是右对齐但因 time-ago 宽度变化导致 N 条/N tok 右边界也跟着抖,跨行没真对齐)。两件套:① 折叠模式从「pane display:none」改 VS Code 范式 rail —— `body.left-collapsed #app.ready { grid-template-columns: 40px 1fr 320px }` + `#pane-left > * { display: none }`(藏全部直接子) + override 第一行 pane-head 重显且只留 `#pane-toggle-left`(`> *:not(#pane-toggle-left) { display: none }`,选择器特异性 2 ids 压 1 id);pane-head 第一行用 `position: static` 取消 sticky / `border-bottom: none` / `background: transparent` 看起来更像 rail 非"卡片"。按钮符号根据 `body.left-collapsed` 在 `applyLeftCollapsed` 里翻向(展开态 `` 折叠态 ``)。彻底删 `#hd-toggle-left` + `header .icon-btn` CSS 块,header 不再背 expand 入口的债。② time-ago 加 `flex-shrink: 0; text-align: right; min-width: 64px` 锁宽,**这才是真正解决跨行对齐的关键**:此前 `.num.right-group``margin-left: auto` 把 [N 条][N tok][time] 整组推右,但 time 自身宽度浮动 30~70px(刚刚 / 10 小时前 / 2025-12-05)→ time 左边界抖 → N tok 右边界抖 → N 条 右边界抖,逐级传染。锁 time 宽后整组位置稳定,槽内 `text-align: right` 才能让"条/tok"后缀跨行真正垂直对齐。删 `.badge .time-ago { flex-shrink: 0 }` 合并里的 time-ago(已独立给规则)。**没动**:fmtTokens / 桶分级 / tabular-nums / `.num min-width: 44px`(上一轮已正确)、右 pane / chat 中列。 - **dev SPA 左 pane 折叠改 rail 模式 + 删 header 冗余按钮 + time-ago 锁宽完成跨行对齐**:用户反馈 ① "原来 zcbot 旁的折叠按钮不要了,没用处" + ② "数字对齐那块现在是不是每块内容左侧对齐?"(实际是右对齐但因 time-ago 宽度变化导致 N 条/N tok 右边界也跟着抖,跨行没真对齐)。两件套:① 折叠模式从「pane display:none」改 VS Code 范式 rail —— `body.left-collapsed #app.ready { grid-template-columns: 40px 1fr 320px }` + `#pane-left > * { display: none }`(藏全部直接子) + override 第一行 pane-head 重显且只留 `#pane-toggle-left`(`> *:not(#pane-toggle-left) { display: none }`,选择器特异性 2 ids 压 1 id);pane-head 第一行用 `position: static` 取消 sticky / `border-bottom: none` / `background: transparent` 看起来更像 rail 非"卡片"。按钮符号根据 `body.left-collapsed` 在 `applyLeftCollapsed` 里翻向(展开态 `` 折叠态 ``)。彻底删 `#hd-toggle-left` + `header .icon-btn` CSS 块,header 不再背 expand 入口的债。② time-ago 加 `flex-shrink: 0; text-align: right; min-width: 64px` 锁宽,**这才是真正解决跨行对齐的关键**:此前 `.num.right-group``margin-left: auto` 把 [N 条][N tok][time] 整组推右,但 time 自身宽度浮动 30~70px(刚刚 / 10 小时前 / 2025-12-05)→ time 左边界抖 → N tok 右边界抖 → N 条 右边界抖,逐级传染。锁 time 宽后整组位置稳定,槽内 `text-align: right` 才能让"条/tok"后缀跨行真正垂直对齐。删 `.badge .time-ago { flex-shrink: 0 }` 合并里的 time-ago(已独立给规则)。**没动**:fmtTokens / 桶分级 / tabular-nums / `.num min-width: 44px`(上一轮已正确)、右 pane / chat 中列。
- **dev SPA 任务行 meta 数字槽位跨行对齐 + 折叠按钮位置调整**:用户报"N 条 / N tok 数字宽窄不一,看着不齐";又说"折叠按钮应该贴刷新按钮"。两件套:① meta CSS 加 `font-variant-numeric: tabular-nums` + `align-items: baseline`,新 `.num` 子选择器 `flex-shrink: 0; text-align: right; min-width: 44px`(右对齐让 `条` / `tok` 后缀跨行垂直对齐);N 条 span 戴 `right-group` 类拿 `margin-left: auto`,把 [N 条][N tok][time-ago] 整组挤右侧,左侧只剩 badge + skill;原 time-ago 上的 inline `margin-left:auto` 移除避免双 push 失效。新 `fmtTokens(n)` helper:<1k 原数 / <10k `1.2k` / <1M `123k` / >=1M `1.2M`,bound 槽位宽度;`title=` hover 出 `123,456 tokens` 完整值(`Number.toLocaleString()`)。② 折叠按钮拆双入口 — `#pane-toggle-left` 放第一行 pane-head 紧贴刷新按钮(展开态用,点击折叠);`#hd-toggle-left` 留 header 但 `style="display:none"` 默认隐藏,仅折叠态显示(用户路径:折叠后 pane display:none → 无法在 pane 内点展开 → 必须 header 保留 expand 入口)。`applyLeftCollapsed(collapsed)` 控制 hd 按钮 display,两按钮共享 `toggleLeftCollapsed()` 实现;每按钮符号固定(pane 内 `` 一直是折叠方向,header 内 `` 一直是展开方向),不再翻向(语义更清)。**没动**:右 pane / chat 列宽、`/v1/tasks` 后端、id8 仍在 row title hover(上次改的不动)、CSS `.small` 等。 - **dev SPA 任务行 meta 数字槽位跨行对齐 + 折叠按钮位置调整**:用户报"N 条 / N tok 数字宽窄不一,看着不齐";又说"折叠按钮应该贴刷新按钮"。两件套:① meta CSS 加 `font-variant-numeric: tabular-nums` + `align-items: baseline`,新 `.num` 子选择器 `flex-shrink: 0; text-align: right; min-width: 44px`(右对齐让 `条` / `tok` 后缀跨行垂直对齐);N 条 span 戴 `right-group` 类拿 `margin-left: auto`,把 [N 条][N tok][time-ago] 整组挤右侧,左侧只剩 badge + skill;原 time-ago 上的 inline `margin-left:auto` 移除避免双 push 失效。新 `fmtTokens(n)` helper:<1k 原数 / <10k `1.2k` / <1M `123k` / >=1M `1.2M`,bound 槽位宽度;`title=` hover 出 `123,456 tokens` 完整值(`Number.toLocaleString()`)。② 折叠按钮拆双入口 — `#pane-toggle-left` 放第一行 pane-head 紧贴刷新按钮(展开态用,点击折叠);`#hd-toggle-left` 留 header 但 `style="display:none"` 默认隐藏,仅折叠态显示(用户路径:折叠后 pane display:none → 无法在 pane 内点展开 → 必须 header 保留 expand 入口)。`applyLeftCollapsed(collapsed)` 控制 hd 按钮 display,两按钮共享 `toggleLeftCollapsed()` 实现;每按钮符号固定(pane 内 `` 一直是折叠方向,header 内 `` 一直是展开方向),不再翻向(语义更清)。**没动**:右 pane / chat 列宽、`/v1/tasks` 后端、id8 仍在 row title hover(上次改的不动)、CSS `.small` 等。
- **dev SPA 左 pane 调宽 280→320px + header 折叠 toggle + 任务行精简 meta**:用户报 280px 下底行(badge/skill/N条/Ntok/time/id8)被 flex shrink 后 CJK 字符断行(像"10 小时前"裂成两行)。三件套修:① `#app.ready grid-template-columns` `280px → 320px`(右 pane / chat 不动,从 chat 借 40px,任务名 / 描述 / wd 都更舒展);② header 最左插 `<button id="hd-toggle-left" class="icon-btn"></button>`,点击 toggle `body.left-collapsed` → CSS `grid-template-columns: 0 1fr 320px` + `#pane-left { display: none }`(列归零腾给 chat,折叠态 chevron 翻 ``);state 存 `localStorage zcbot.left-collapsed`,boot 即应用,刷新保持。IntersectionObserver 留着不重建(display:none 期间 sentinel 0 高度自然不触发,展开后重算 layout 若 sentinel 在视口自然续传);③ 任务行删 `id8` span(8 位 hex 调试时才用),挪到 row `title=` hover 出 `${name}\n${task_id}` 完整 id 仍可查;`.task-row .meta > *` 全加 `white-space: nowrap; overflow: hidden; text-overflow: ellipsis` 防内部 CJK 字符破断;badge + time-ago 加 `flex-shrink: 0` 保两端不缩;wd / desc 副行恢复 inline 三件套 `overflow:hidden;text-overflow:ellipsis;white-space:nowrap`(它们是单文本带不是 flex 子元素行,`> *` CSS 不命中文本节点)。**没动**:右 pane 320px 不变(文件预览常用)、chat 中列 1fr(自适应剩余);折叠按钮没做右 pane 对应版(用户没要)。 - **dev SPA 左 pane 调宽 280→320px + header 折叠 toggle + 任务行精简 meta**:用户报 280px 下底行(badge/skill/N条/Ntok/time/id8)被 flex shrink 后 CJK 字符断行(像"10 小时前"裂成两行)。三件套修:① `#app.ready grid-template-columns` `280px → 320px`(右 pane / chat 不动,从 chat 借 40px,任务名 / 描述 / wd 都更舒展);② header 最左插 `<button id="hd-toggle-left" class="icon-btn"></button>`,点击 toggle `body.left-collapsed` → CSS `grid-template-columns: 0 1fr 320px` + `#pane-left { display: none }`(列归零腾给 chat,折叠态 chevron 翻 ``);state 存 `localStorage zcbot.left-collapsed`,boot 即应用,刷新保持。IntersectionObserver 留着不重建(display:none 期间 sentinel 0 高度自然不触发,展开后重算 layout 若 sentinel 在视口自然续传);③ 任务行删 `id8` span(8 位 hex 调试时才用),挪到 row `title=` hover 出 `${name}\n${task_id}` 完整 id 仍可查;`.task-row .meta > *` 全加 `white-space: nowrap; overflow: hidden; text-overflow: ellipsis` 防内部 CJK 字符破断;badge + time-ago 加 `flex-shrink: 0` 保两端不缩;wd / desc 副行恢复 inline 三件套 `overflow:hidden;text-overflow:ellipsis;white-space:nowrap`(它们是单文本带不是 flex 子元素行,`> *` CSS 不命中文本节点)。**没动**:右 pane 320px 不变(文件预览常用)、chat 中列 1fr(自适应剩余);折叠按钮没做右 pane 对应版(用户没要)。

View File

@ -139,25 +139,59 @@ def _build_system_prompt(
tool_base: Path, tool_base: Path,
working_dir: Path, working_dir: Path,
user_id: UUID, user_id: UUID,
task_id: UUID,
task_name: str,
) -> str: ) -> str:
"""拼 system prompt: 模板 + skill 列表 + memory + 工作目录段。 """拼 system prompt: 模板 + skill 列表 + memory + 工作目录段 + task 上下文 + 命名约定
new task resume task 都走这里,memory 演化即时生效memory user_id 隔离 new task resume task 都走这里,memory 演化即时生效memory user_id 隔离
task_short_id (task_id.hex 8 ) 宪法文件主锚 task.name 可改,
task_id 永不变,glob short_id 找文件, cascade rename
task_name 仍写进文件名作"建时元数据 / 人类可读说明",改名后文件名里的旧 name
不强求同步( short_id 兜底定位)
today 当场算, prompt LLM 直接拼路径(避免 LLM 不知道当前日期)
""" """
prompt = (ROOT / cfg["system_prompt"]).read_text(encoding="utf-8") prompt = (ROOT / cfg["system_prompt"]).read_text(encoding="utf-8")
if skills.skills: if skills.skills:
prompt += f"\n\n## 可用 skill (用 load_skill 加载完整指引)\n{skills.discovery_block()}" prompt += f"\n\n## 可用 skill (用 load_skill 加载完整指引)\n{skills.discovery_block()}"
prompt += memory_block(workspace_dir, user_id) prompt += memory_block(workspace_dir, user_id)
wd_abs = working_dir.resolve() wd_abs = working_dir.resolve()
today = datetime.now().strftime("%Y-%m-%d")
tname = task_name or "<未指定>"
short_id = task_id.hex[:8]
prompt += ( prompt += (
f"\n\n## 工作目录\n" f"\n\n## 工作目录与 task 上下文\n"
f"- cwd(用户启动时所在目录,只读用): `{tool_base}`\n" f"- cwd(用户启动时所在目录,只读用): `{tool_base}`\n"
f"- **task_dir(所有产物写到这里)**: `{wd_abs}`\n\n" f"- **task_dir(所有产物写到这里)**: `{wd_abs}`\n"
f"- **task_short_id**(永不变,「宪法」文件主锚): `{short_id}`\n"
f"- **task_name**(可变,写进文件名作人类可读说明): `{tname}`\n"
f"- **today**(当前日期,用于「宪法」文件命名): `{today}`\n\n"
f"SKILL 文档里出现的 `<task_dir>` 占位符,一律指上面这个绝对路径。" f"SKILL 文档里出现的 `<task_dir>` 占位符,一律指上面这个绝对路径。"
f"产物示例: `{wd_abs}/spec_lock.md`、" f"普通产物(sections / slides / 终稿 .docx/.pptx)按 SKILL 文档落路径;"
f"`{wd_abs}/sections/01_summary.md`、" f"「宪法」性文件(spec 等)按下面《task 级「宪法」文件命名约定》拼路径。\n"
f"`{wd_abs}/slides/`、最终 .docx/.pptx。\n" f"⛔ 不要把产物写到 cwd / `skills/` / repo 根 —— 只写到 task_dir。\n"
f"⛔ 不要把产物写到 cwd / `skills/` / repo 根 —— 只写到 task_dir。" f"\n## task 级「宪法」文件命名约定(跨 skill 通用)\n"
f"任何 skill 产物中,跟 task 1:1 强绑定、阶段二/后续步骤会**反复 read**"
f"的「宪法」性文件(如 proposal/ppt 的 spec、outline 等),**统一按下面格式命名**,"
f"落在 task_dir 根下:\n\n"
f" <YYYY-MM-DD>-<task_short_id>-<task_name>.<base>.md\n\n"
f"其中 `<YYYY-MM-DD>` = 本会话 today=`{today}`;"
f"`<task_short_id>` = `{short_id}`(永不变,主锚);"
f"`<task_name>` = `{tname}`(可变,人类可读说明,原样用 含 CJK / 空格);"
f"`<base>` 由 skill 定义(如 proposal/ppt 的 `spec`)。\n\n"
f"**取 current 版本规则**:read 时 **按 task_short_id 锚定** glob "
f"`{wd_abs}/*-{short_id}-*.<base>.md` → 按文件名字典序排 → 取最大者"
f"(= 最新日期)。这样即使用户改了 task_name,旧文件仍能定位(`<task_name>` "
f"那段视为「建时快照」,不强求同步)。这是「current 指针」的纯文件名实现,"
f"agent 自己拼即可。\n\n"
f"**重定调场景**:用户阶段一已确认过的「宪法」文件,后续要推翻重写时,"
f"以 today=`{today}` 为前缀写一份新的,**旧版自然保留为历史快照**(不要 edit "
f"覆盖旧文件)。同日多次重定调可在文件名末尾加 `-v2` / `-v3` 等递增后缀。\n\n"
f"**隔离逻辑**:同 working_dir 多 task → 由 `<task_short_id>` 严格隔离"
f"(8 位 hex,撞概率近 0);同 task 多版本 → 由 `<YYYY-MM-DD>` 隔离。两层隔离"
f"都靠文件名,**无目录嵌套、无 DB 字段、无 cascade rename**。其余产物"
f"(`sections/` / `figures/` / `slides/` / 终稿 .docx/.pptx 等)按 SKILL "
f"文档保留扁平共享,LLM 自行通过 task_short_id / 命名前缀判断归属。"
) )
return prompt return prompt
@ -238,19 +272,45 @@ def build_agent(
skills = SkillRegistry(ROOT / cfg.get("skills_dir", "skills")) skills = SkillRegistry(ROOT / cfg.get("skills_dir", "skills"))
system_prompt = _build_system_prompt(
cfg, skills, workspace_dir, tool_base, working_dir_path, uid
)
now_iso = datetime.now().isoformat(timespec="seconds") now_iso = datetime.now().isoformat(timespec="seconds")
# meta["working_dir"] 是 db 形态(相对 ROOT 或绝对);Session.append → ensure_local_task_row # meta["working_dir"] 是 db 形态(相对 ROOT 或绝对);Session.append → ensure_local_task_row
# 把它直接落 PG tasks.working_dir,所以这里就转好。文件系统操作仍用 working_dir_path(absolute)。 # 把它直接落 PG tasks.working_dir,所以这里就转好。文件系统操作仍用 working_dir_path(absolute)。
wd_db = to_db_path(working_dir_path) wd_db = to_db_path(working_dir_path)
# task_state 先就位:resume 从 DB 拿真 name,new 直接用 task_name_safe。
# system_prompt 拼接需要 task.name 注入(「宪法」文件命名约定),所以拼 prompt
# 必须在 task_state 之后。
if resume:
task_state = TaskState.load(task_id)
if task_state is None:
# tasks 行不存在 —— 理论上 resolve_task_id 已经定位到 DB 行了,走到这里
# 说明被并发删了,兜底构造空 state(不主动 save,等下条 append / 命令)
task_state = TaskState(
task_id=sid, user_id=uid, name="", working_dir=wd_db,
skill=skill, description=description, status="active",
model=caps.model_id, model_profile=model,
)
else:
# 懒创建:TaskState 仅内存。tasks 行在首条 user 消息 append 时由
# ensure_local_task_row 占位 INSERT(name 已就位);首次 sync_task_tokens
# 或 /done /desc 走 upsert 覆盖完整字段。
task_state = TaskState(
task_id=sid, user_id=uid, name=task_name_safe, working_dir=wd_db,
skill=skill, description=description, status="active",
model=caps.model_id, model_profile=model,
reasoning_effort=caps.default_reasoning_effort or "",
)
system_prompt = _build_system_prompt(
cfg, skills, workspace_dir, tool_base, working_dir_path, uid,
task_id, task_state.name,
)
meta = { meta = {
"id": sid, "id": sid,
"created_at": now_iso, "created_at": now_iso,
"cwd": str(tool_base), "cwd": str(tool_base),
"name": task_name_safe, # resume 时空字符串(Session.load 会从 DB 拿不到 -- 不要紧,ensure 走 ON CONFLICT DO NOTHING) "name": task_state.name, # resume / new 都拿到真 name(空字符串只在并发删兜底分支)
"working_dir": wd_db, "working_dir": wd_db,
"model": caps.model_id, "model": caps.model_id,
"model_profile": model, "model_profile": model,
@ -261,28 +321,8 @@ def build_agent(
if resume: if resume:
session = Session.load(task_id, system_prompt=system_prompt, meta=meta) session = Session.load(task_id, system_prompt=system_prompt, meta=meta)
task_state = TaskState.load(task_id)
if task_state is None:
# tasks 行不存在 —— 理论上 resolve_task_id 已经定位到 DB 行了,走到这里
# 说明被并发删了,兜底构造空 state(不主动 save,等下条 append / 命令)
task_state = TaskState(
task_id=sid, user_id=uid, name="", working_dir=wd_db,
skill=skill, description=description, status="active",
model=caps.model_id, model_profile=model,
)
# resume 时 meta name 用 DB 里读出来的真值(给 Session.append → ensure 用,避免落空串)
meta["name"] = task_state.name
else: else:
session = Session(task_id=task_id, system_prompt=system_prompt, meta=meta) session = Session(task_id=task_id, system_prompt=system_prompt, meta=meta)
# 懒创建:TaskState 仅内存。tasks 行在首条 user 消息 append 时由
# ensure_local_task_row 占位 INSERT(name 已就位);首次 sync_task_tokens
# 或 /done /desc 走 upsert 覆盖完整字段。
task_state = TaskState(
task_id=sid, user_id=uid, name=task_name_safe, working_dir=wd_db,
skill=skill, description=description, status="active",
model=caps.model_id, model_profile=model,
reasoning_effort=caps.default_reasoning_effort or "",
)
tools = {} tools = {}
for cls in (ReadTool, WriteTool, EditTool, GlobTool, GrepTool, ShellTool): for cls in (ReadTool, WriteTool, EditTool, GlobTool, GrepTool, ShellTool):

View File

@ -35,7 +35,7 @@ def atomic_write_text(path: Path, text: str, encoding: str = "utf-8") -> None:
"""原子写: 先写到 path.tmp 再 os.replace 到 path。 """原子写: 先写到 path.tmp 再 os.replace 到 path。
防止写中途异常(磁盘满 / surrogate 编码错 / 进程被杀)留下 0 字节或半文件 防止写中途异常(磁盘满 / surrogate 编码错 / 进程被杀)留下 0 字节或半文件
skill 产物(spec_lock.md / sections/*.md )走这里,messages 已改走 PG skill 产物(*.spec.md / sections/*.md )走这里,messages 已改走 PG
""" """
path.parent.mkdir(parents=True, exist_ok=True) path.parent.mkdir(parents=True, exist_ok=True)
tmp = path.with_suffix(path.suffix + ".tmp") tmp = path.with_suffix(path.suffix + ".tmp")

View File

@ -12,7 +12,7 @@ description: 生成 PowerPoint 演示文稿 (.pptx)。当用户要求做汇报 P
- `references/layouts.md` —— 9 种版式的 python-pptx 起手代码 + 安全区/越界保护 + `apply_brand` 品牌条 - `references/layouts.md` —— 9 种版式的 python-pptx 起手代码 + 安全区/越界保护 + `apply_brand` 品牌条
- `references/icons.md` —— 业务图标两层:Iconify (在线/本地缓存) / unicode 字形兜底 - `references/icons.md` —— 业务图标两层:Iconify (在线/本地缓存) / unicode 字形兜底
- `assets/icons/` —— 本地图标缓存 (Iconify 拉过的图存这,见 `INDEX.md` 推荐清单) - `assets/icons/` —— 本地图标缓存 (Iconify 拉过的图存这,见 `INDEX.md` 推荐清单)
- 素材摄取: 直接用 `markitdown` CLI (PDF/DOCX/PPTX/XLSX/HTML/URL → 干净 Markdown) - 素材摄取: `markitdown` CLI 把 PDF/DOCX/PPTX/XLSX/HTML/URL 转干净 Markdown,统一落到 `<task_dir>/source/<name>.md`(同 working_dir 多 task 共享 source 池)
- `scripts/fetch_icon.py` —— 从 Iconify CDN 拉 SVG/PNG (染主题色,缓存本地) - `scripts/fetch_icon.py` —— 从 Iconify CDN 拉 SVG/PNG (染主题色,缓存本地)
- `scripts/render_icon.py` —— unicode 字形 → 透明 PNG (Iconify 没有时兜底) - `scripts/render_icon.py` —— unicode 字形 → 透明 PNG (Iconify 没有时兜底)
- `scripts/quality_check.py` —— 产物 .pptx 验收 (越界 / 文本溢出 / 颜色一致) - `scripts/quality_check.py` —— 产物 .pptx 验收 (越界 / 文本溢出 / 颜色一致)
@ -21,7 +21,7 @@ description: 生成 PowerPoint 演示文稿 (.pptx)。当用户要求做汇报 P
**主色 `#C00000` / 辅色 `#E15554` / 强调色 `#FFC107`。** **主色 `#C00000` / 辅色 `#E15554` / 强调色 `#FFC107`。**
**不允许擅自换色**。除非满足以下任一条件,否则 spec_lock 必须填这套红色: **不允许擅自换色**。除非满足以下任一条件,否则 spec 必须填这套红色:
- 用户在请求里**明确**点名其它配色 (例:"做成蓝色"、"用我们公司的紫色") - 用户在请求里**明确**点名其它配色 (例:"做成蓝色"、"用我们公司的紫色")
- 用户提供素材里有明确的 brand guideline / 配色卡 - 用户提供素材里有明确的 brand guideline / 配色卡
@ -29,13 +29,29 @@ description: 生成 PowerPoint 演示文稿 (.pptx)。当用户要求做汇报 P
- "这个场景蓝色更专业" / "学术汇报红色不合适" / "财务用蓝更稳重" - "这个场景蓝色更专业" / "学术汇报红色不合适" / "财务用蓝更稳重"
- "我觉得 XX 主题更适合" - "我觉得 XX 主题更适合"
要换色,**先问用户**,不要在 spec_lock 里塞自己的偏好。其它备选见 `design_principles.md §2` 要换色,**先问用户**,不要在 spec 里塞自己的偏好。其它备选见 `design_principles.md §2`
## 两阶段工作流 ## 两阶段工作流
### 阶段一: 策略 (Strategist) — 八条对齐 ### 阶段一: 策略 (Strategist) — 八条对齐
产物:`spec_lock.md` —— 整个 deck 的"宪法",阶段二每页前都要重读。 产物:**task 级 spec 文件** —— 整个 deck 的"宪法",阶段二每页前都要重读。文件路径按 system prompt 的《task 级「宪法」文件命名约定》:
<task_dir>/<today>-<task_short_id>-<task_name>.spec.md
`<today>` / `<task_short_id>` / `<task_name>` 用 system prompt 注入的实际值替换。
**0. 先检测已有 spec**:
```
glob <task_dir>/*-<task_short_id>-*.spec.md → 按文件名字典序排,取最大者作 current
```
(按 short_id 主锚,name 部分不参与匹配 — 用户改过 task name 时旧文件仍能定位)
- 有 current(当前 task 已有 spec) → 展示给用户,问「**沿用进阶段二** / **重定调**(以 today 写新版,旧版保留)」,⛔ BLOCKING 等用户决定
- 仅有其它 task 的(`*-<别的 short_id>-*.spec.md`)→ 不当 current 用,继续走下面流程
- 完全没有 → 直接走下面流程
按下表**一次性给出推荐方案**,然后 ⛔ **BLOCKING:等用户确认/修改后才能进阶段二**。不要一条一条问。 按下表**一次性给出推荐方案**,然后 ⛔ **BLOCKING:等用户确认/修改后才能进阶段二**。不要一条一条问。
@ -50,14 +66,14 @@ description: 生成 PowerPoint 演示文稿 (.pptx)。当用户要求做汇报 P
| 7 | 图标 | **Iconify `tabler` 集** (描边商务图标,主色染色;`fetch_icon.py` 拉到 `assets/icons/` 缓存) | | 7 | 图标 | **Iconify `tabler` 集** (描边商务图标,主色染色;`fetch_icon.py` 拉到 `assets/icons/` 缓存) |
| 8 | 图表 | 数据 ≥ 3 个点的页用 matplotlib 配图 | | 8 | 图表 | 数据 ≥ 3 个点的页用 matplotlib 配图 |
把这 8 项写进 `spec_lock.md`,以表格形式给用户预览,问一句"按这个开干?"。**spec_lock 写定后不再改**,有冲突回头跟用户重新对齐 把这 8 项写进上面那个 task 级 spec 文件,以表格形式给用户预览,问一句"按这个开干?"。**spec 写定后不再改**(要改就走 §0 的「重定调」分支,以 today 为前缀写新版,旧版保留)
### 阶段二: 执行 (Executor) — 逐页生成 ### 阶段二: 执行 (Executor) — 逐页生成
每页前 **必须 read 一次 `spec_lock.md`**,只用里面定的颜色/字体/图标 —— **不允许凭记忆或临时发挥**。这条规则是为了对抗长 deck 中的上下文漂移。 每页前 **必须 read 一次 current spec**(按 §0 的 glob 规则拿到的字典序最大那份),只用里面定的颜色/字体/图标 —— **不允许凭记忆或临时发挥**。这条规则是为了对抗长 deck 中的上下文漂移。
每页流程: 每页流程:
1. 读 `<task_dir>/spec_lock.md` (即使刚读过) 1. 读 current spec(即使刚读过)
2. **图标先于版式**: 这一页要用什么概念图标? 先 `glob <skill_dir>/assets/icons/` 看本地有没有 (`<skill_dir>` 是 `load_skill` 头里的绝对路径),没有就 `python <skill_dir>/scripts/fetch_icon.py <name> --set tabler --color C00000 --size 128 -o <skill_dir>/assets/icons/...` 拉一个;`add_picture` 嵌入。**几何形状(圆点/徽章/装饰线)不算图标,走 layouts.md helper 即可** 2. **图标先于版式**: 这一页要用什么概念图标? 先 `glob <skill_dir>/assets/icons/` 看本地有没有 (`<skill_dir>` 是 `load_skill` 头里的绝对路径),没有就 `python <skill_dir>/scripts/fetch_icon.py <name> --set tabler --color C00000 --size 128 -o <skill_dir>/assets/icons/...` 拉一个;`add_picture` 嵌入。**几何形状(圆点/徽章/装饰线)不算图标,走 layouts.md helper 即可**
3. 写一个 `run_python` block,用 python-pptx 添加这一页 (载入已有 .pptx → append slide → save) 3. 写一个 `run_python` block,用 python-pptx 添加这一页 (载入已有 .pptx → append slide → save)
4. 报这一页:版式、标题、要点条数、用了哪些图标 4. 报这一页:版式、标题、要点条数、用了哪些图标
@ -70,7 +86,7 @@ description: 生成 PowerPoint 演示文稿 (.pptx)。当用户要求做汇报 P
### 阶段三: 验收 ### 阶段三: 验收
```bash ```bash
python <skill_dir>/scripts/quality_check.py <task_dir>/<output.pptx> --spec <task_dir>/spec_lock.md python <skill_dir>/scripts/quality_check.py <task_dir>/<output.pptx> --spec <task_dir>/<today>-<task_short_id>-<task_name>.spec.md
``` ```
不通过的项,回头 edit 对应页。 不通过的项,回头 edit 对应页。
@ -94,11 +110,10 @@ python <skill_dir>/scripts/quality_check.py <task_dir>/<output.pptx> --spec <tas
``` ```
<task_dir>/ <task_dir>/
├── source.md # markitdown 转出的素材 ├── source/ # markitdown 转出的素材(同 working_dir 多 task 共享;用 markitdown -o <task_dir>/source/<name>.md)
├── spec_lock.md # 八条对齐落定 ├── <today>-<task_short_id>-<task_name>.spec.md # 八条对齐落定,task 级宪法;命名见 system prompt 约定;按 short_id 主锚,重定调时写新日期,旧版保留
├── slides/ ├── slides/ # 各页用到的图片素材 (chart_p3.png 等),多 task 时文件名前缀区分
│ └── chart_p3.png # 各页用到的图片素材 └── <topic>.pptx # 最终产物 (按主题命名,多 task 时主题必须不同)
└── <topic>.pptx # 最终产物 (按主题命名)
``` ```
## 反模式 ## 反模式

View File

@ -116,7 +116,7 @@
## 7. 图表规则 (matplotlib) ## 7. 图表规则 (matplotlib)
- 颜色用 spec_lock 里定的主/辅/强调三色,**不要用 matplotlib 默认色板** - 颜色用 spec 里定的主/辅/强调三色,**不要用 matplotlib 默认色板**
- 字号: 标题 16,坐标轴 12,刻度 10 - 字号: 标题 16,坐标轴 12,刻度 10
- 去掉上方和右方边框 (`ax.spines['top'/'right'].set_visible(False)`) - 去掉上方和右方边框 (`ax.spines['top'/'right'].set_visible(False)`)
- 数据标签直接标在柱子/点上,优先于看坐标 - 数据标签直接标在柱子/点上,优先于看坐标
@ -155,6 +155,6 @@ fig.savefig("chart.png", bbox_inches="tight", dpi=150)
| 投影看不清 | 字号 < 18 | 加大字号或拆页 | | 投影看不清 | 字号 < 18 | 加大字号或拆页 |
| 颜色花 | 用了超过 5 种色 | 退回三色制 | | 颜色花 | 用了超过 5 种色 | 退回三色制 |
| bullet 是完整段落 | 把演讲稿当 bullet 写 | 提炼关键词,完整句留给口述 | | bullet 是完整段落 | 把演讲稿当 bullet 写 | 提炼关键词,完整句留给口述 |
| 图表默认配色 | 没改 matplotlib 色板 | 用 spec_lock 主色 | | 图表默认配色 | 没改 matplotlib 色板 | 用 spec 主色 |
| 图标/图片随意找的 | 没统一风格 | 同一来源 / 同一风格 | | 图标/图片随意找的 | 没统一风格 | 同一来源 / 同一风格 |
| 标题在每页位置都不一样 | 没用统一版式 | 见 layouts.md,固定模板 | | 标题在每页位置都不一样 | 没用统一版式 | 见 layouts.md,固定模板 |

View File

@ -2,7 +2,7 @@
> **2.0 版本要点**:大幅减少满铺色块,引入 MSO_SHAPE 图标点缀,所有元素经 safe_area 校验不会越出画布。 > **2.0 版本要点**:大幅减少满铺色块,引入 MSO_SHAPE 图标点缀,所有元素经 safe_area 校验不会越出画布。
复制 → 改文案 → 跑。配色用 `spec_lock.md` 里的实际 hex 替换占位。 复制 → 改文案 → 跑。配色用 current spec(命名见 SKILL.md §阶段一)里的实际 hex 替换占位。
## 通用起手 + 安全辅助 ## 通用起手 + 安全辅助
@ -14,7 +14,7 @@ from pptx.enum.text import PP_ALIGN, MSO_ANCHOR, MSO_AUTO_SIZE
from pptx.enum.shapes import MSO_SHAPE from pptx.enum.shapes import MSO_SHAPE
# ---- 配色 (商务红 — 硬约束默认) ---- # ---- 配色 (商务红 — 硬约束默认) ----
# ⛔ 不允许擅自换色:除非用户明确点名其它配色 (例:"做成蓝色") 或 spec_lock 已写其它 hex, # ⛔ 不允许擅自换色:除非用户明确点名其它配色 (例:"做成蓝色") 或 spec 已写其它 hex,
# 否则就是这套商务红。禁止以"这个场景蓝色更专业"这类自我合理化做替换。 # 否则就是这套商务红。禁止以"这个场景蓝色更专业"这类自我合理化做替换。
PRIMARY = RGBColor(0xC0, 0x00, 0x00) # 深红 - 标题/强调/关键数据 PRIMARY = RGBColor(0xC0, 0x00, 0x00) # 深红 - 标题/强调/关键数据
SECONDARY = RGBColor(0xE1, 0x55, 0x54) # 砖红 - 次要图形 SECONDARY = RGBColor(0xE1, 0x55, 0x54) # 砖红 - 次要图形

View File

@ -1,11 +1,11 @@
"""quality_check.py: 验收 .pptx,产出问题清单。 """quality_check.py: 验收 .pptx,产出问题清单。
用法: 用法:
python quality_check.py <output.pptx> [--spec spec_lock.md] python quality_check.py <output.pptx> [--spec spec.md]
检查项: 检查项:
- 文件存在且 > 10KB - 文件存在且 > 10KB
- 总页数与 spec 一致 (如提供 spec_lock.md) - 总页数与 spec 一致 (如提供 spec.md)
- 每页有标题 - 每页有标题
- 每页 bullet 5 - 每页 bullet 5
- 文字字号 14pt (除页脚) - 文字字号 14pt (除页脚)
@ -220,7 +220,7 @@ def check_pptx(path: Path, spec: dict) -> tuple[list, list]:
unmatched = seen_colors - spec_colors unmatched = seen_colors - spec_colors
if len(unmatched) > 3: if len(unmatched) > 3:
warnings.append( warnings.append(
f"出现 {len(unmatched)} 个 spec_lock 之外的颜色,可能用了 matplotlib 默认色板" f"出现 {len(unmatched)} 个 spec 之外的颜色,可能用了 matplotlib 默认色板"
) )
return errors, warnings return errors, warnings
@ -230,7 +230,7 @@ def main():
ap = argparse.ArgumentParser() ap = argparse.ArgumentParser()
ap.add_argument("pptx", type=Path) ap.add_argument("pptx", type=Path)
ap.add_argument("--spec", type=Path, default=None, ap.add_argument("--spec", type=Path, default=None,
help="spec_lock.md 路径") help="spec.md 路径")
args = ap.parse_args() args = ap.parse_args()
spec = parse_spec(args.spec) if args.spec else {} spec = parse_spec(args.spec) if args.spec else {}

View File

@ -15,7 +15,7 @@ description: 撰写中国科研项目申报书 / 课题任务书 (国家重点
- `<skill_dir>/references/review_redlines.md` —— 评审雷区与不可考核词清单 - `<skill_dir>/references/review_redlines.md` —— 评审雷区与不可考核词清单
- `<skill_dir>/references/citation_gbt7714.md` —— GB/T 7714 顺序编码制 + 文献真实性铁律 - `<skill_dir>/references/citation_gbt7714.md` —— GB/T 7714 顺序编码制 + 文献真实性铁律
- `<skill_dir>/references/budget_rules.md` —— 间接费用台阶 + B1-B4 表 - `<skill_dir>/references/budget_rules.md` —— 间接费用台阶 + B1-B4 表
- `<skill_dir>/templates/spec_lock.md` —— 阶段一八条对齐的固定字段模板 (复制到 `<task_dir>/spec_lock.md`) - `<skill_dir>/templates/spec.md` —— 阶段一八条对齐的固定字段模板 (复制到 task 级 spec 文件,文件名见下文 §阶段一)
- `<skill_dir>/templates/{key_rd,major_project,nsfc_joint_fund}.md` —— **有完整章节模板**的 3 类基金;其它 4 类 (`nsfc_general` / `nsfc_youth` / `provincial` / `enterprise`) 复用 `nsfc_joint_fund``key_rd` 骨架,差异看 `fund_types.md` § 4-6 - `<skill_dir>/templates/{key_rd,major_project,nsfc_joint_fund}.md` —— **有完整章节模板**的 3 类基金;其它 4 类 (`nsfc_general` / `nsfc_youth` / `provincial` / `enterprise`) 复用 `nsfc_joint_fund``key_rd` 骨架,差异看 `fund_types.md` § 4-6
- `<skill_dir>/scripts/render_docx.py` —— md→docx,自动加目录 / 解析 `**bold**`/`*italic*`/`` `code` `` / 列表分行 / `![](path)` 居中插图 + 图题自动编号 / 识别 mermaid 块按 caption 查 `figures/fig_<caption>.png` - `<skill_dir>/scripts/render_docx.py` —— md→docx,自动加目录 / 解析 `**bold**`/`*italic*`/`` `code` `` / 列表分行 / `![](path)` 居中插图 + 图题自动编号 / 识别 mermaid 块按 caption 查 `figures/fig_<caption>.png`
- `<skill_dir>/scripts/render_diagrams.py` —— sections/*.md 里的 ```mermaid``` 块预渲染成 `<task_dir>/figures/fig_<caption>.png`(caption 必填 + 全 task 唯一,优先 `mmdc`、回退 `mermaid.ink`) - `<skill_dir>/scripts/render_diagrams.py` —— sections/*.md 里的 ```mermaid``` 块预渲染成 `<task_dir>/figures/fig_<caption>.png`(caption 必填 + 全 task 唯一,优先 `mmdc`、回退 `mermaid.ink`)
@ -33,25 +33,41 @@ markitdown <path>/budget.xlsx -o <task_dir>/source/budget.md
markitdown https://example.com/x -o <task_dir>/source/policy.md markitdown https://example.com/x -o <task_dir>/source/policy.md
``` ```
转完后 spec_lock 阶段直接 `read <task_dir>/source/*.md` 拿事实,不要凭印象写。 转完后 spec 阶段直接 `read <task_dir>/source/*.md` 拿事实,不要凭印象写。
## 阶段一: 八条对齐 ## 阶段一: 八条对齐
产物 `<task_dir>/spec_lock.md` —— 申报书"宪法",阶段二每章前都要重读。 产物:**task 级 spec 文件**(申报书"宪法",阶段二每章前都要重读)文件路径按 system prompt 的《task 级「宪法」文件命名约定》:
1. **复制模板**: `read <skill_dir>/templates/spec_lock.md``write <task_dir>/spec_lock.md` <task_dir>/<today>-<task_short_id>-<task_name>.spec.md
`<today>` / `<task_short_id>` / `<task_name>` 用 system prompt 注入的实际值替换。
**0. 先检测已有 spec**(同 working_dir 可能已经有别的 task 的 spec,也可能本 task 之前定调过要重写):
```
glob <task_dir>/*-<task_short_id>-*.spec.md → 按文件名字典序排,取最大者作 current
```
(按 short_id 主锚,name 部分不参与匹配 — 用户改过 task name 时旧文件仍能定位)
- 若已有 current(当前 task 的 spec) → 读出展示给用户,问「**沿用此 spec 进阶段二** / **重定调**(以 today 为前缀写新版,旧版保留为历史快照)」,⛔ BLOCKING 等用户决定
- 没有当前 task 的 spec,但 working_dir 下有其它 task 的(`*-<别的 short_id>-*.spec.md`) → 仅作参考,不当 current 用;继续走下面 1-4
- 完全没有 → 直接走 1-4
1. **复制模板**: `read <skill_dir>/templates/spec.md``write <task_dir>/<today>-<task_short_id>-<task_name>.spec.md`
2. **先读 `<skill_dir>/references/fund_types.md` 选基金类型**(章节、字数、表格各不相同) 2. **先读 `<skill_dir>/references/fund_types.md` 选基金类型**(章节、字数、表格各不相同)
3. 按 spec_lock.md 字段填,给用户预览 3. 按字段填,给用户预览
4. ⛔ **BLOCKING:用户确认后才进阶段二** 4. ⛔ **BLOCKING:用户确认后才进阶段二**
字段清单见 `<skill_dir>/templates/spec_lock.md` (基金类型 / 指南方向 / 关键科学技术问题 / 创新点 / 研究内容骨架 / 团队 / 考核指标矩阵 / 经费预算 / TODO 列表)。 字段清单见 `<skill_dir>/templates/spec.md` (基金类型 / 指南方向 / 关键科学技术问题 / 创新点 / 研究内容骨架 / 团队 / 考核指标矩阵 / 经费预算 / TODO 列表)。
## 阶段二: 逐章起草 ## 阶段二: 逐章起草
每章两段式:**先列要点 → 用户确认 → 再起草 → 用户确认**。不要直接出正文。 每章两段式:**先列要点 → 用户确认 → 再起草 → 用户确认**。不要直接出正文。
**A. 起草前列要点** (改要点比改正文便宜): **A. 起草前列要点** (改要点比改正文便宜):
1. 读 `<task_dir>/spec_lock.md``<skill_dir>/references/fund_types.md`,拿本章字数预算与必填要素 1. 读 **current spec**(按 §阶段一 §0 的 glob 规则拿到的字典序最大那份)`<skill_dir>/references/fund_types.md`,拿本章字数预算与必填要素
2. 列出 3-6 条要点骨架: 本章打算覆盖的论点 / 数据 / 表格,每条贴上对齐的指南要素与预估字数 2. 列出 3-6 条要点骨架: 本章打算覆盖的论点 / 数据 / 表格,每条贴上对齐的指南要素与预估字数
3. ⛔ **BLOCKING:用户确认要点 (改 / 加 / 删) 后才动正文** 3. ⛔ **BLOCKING:用户确认要点 (改 / 加 / 删) 后才动正文**
@ -73,7 +89,7 @@ markitdown https://example.com/x -o <task_dir>/source/policy.md
```bash ```bash
python <skill_dir>/scripts/word_count.py <task_dir>/sections/ --fund-type key_rd python <skill_dir>/scripts/word_count.py <task_dir>/sections/ --fund-type key_rd
python <skill_dir>/scripts/quality_check.py <task_dir>/sections/ --fund-type key_rd --spec <task_dir>/spec_lock.md python <skill_dir>/scripts/quality_check.py <task_dir>/sections/ --fund-type key_rd --spec <task_dir>/<today>-<task_short_id>-<task_name>.spec.md
python <skill_dir>/scripts/render_diagrams.py <task_dir>/sections/ # 章节有 ```mermaid 块就跑 python <skill_dir>/scripts/render_diagrams.py <task_dir>/sections/ # 章节有 ```mermaid 块就跑
python <skill_dir>/scripts/render_docx.py <task_dir>/sections/ --fund-type key_rd -o <task_dir>/<topic>.docx python <skill_dir>/scripts/render_docx.py <task_dir>/sections/ --fund-type key_rd -o <task_dir>/<topic>.docx
``` ```
@ -90,10 +106,10 @@ caption 必须写、必须全 task 唯一 —— render_diagrams / quality_check
``` ```
<task_dir>/ <task_dir>/
├── source/ # 用户给的素材 (指南 PDF / 前期成果) ├── source/ # 用户给的素材 (指南 PDF / 前期成果),同 working_dir 多 task 共享
├── spec_lock.md # 阶段一定调 ├── <today>-<task_short_id>-<task_name>.spec.md # 阶段一定调,task 级宪法;命名见 system prompt 约定;按 short_id 主锚,重定调时写新日期,旧版保留
├── sections/ # 阶段二逐章产物 (按 templates/<fund_type>.md 切的小节命名) ├── sections/ # 阶段二逐章产物 (按 templates/<fund_type>.md 切的小节命名);多 task 同章节用 LLM 判断/前缀区分
└── <topic>.docx # 最终产物 (按课题命名,不要 output.docx) └── <topic>.docx # 最终产物 (按课题命名,不要 output.docx;多 task 时主题必须不同)
``` ```
## 章节骨架速查 ## 章节骨架速查
@ -129,7 +145,7 @@ flowchart LR
``` ```
```` ````
matplotlib 注意:中文字体 `plt.rcParams['font.sans-serif']=['SimHei','Microsoft YaHei']` + `axes.unicode_minus=False`;`figsize=(10,4)` / `dpi=150` / `bbox_inches='tight'`;颜色用 spec_lock 主色而非默认色板;`render_docx` 自动限宽 15cm。 matplotlib 注意:中文字体 `plt.rcParams['font.sans-serif']=['SimHei','Microsoft YaHei']` + `axes.unicode_minus=False`;`figsize=(10,4)` / `dpi=150` / `bbox_inches='tight'`;颜色用 spec 主色而非默认色板;`render_docx` 自动限宽 15cm。
## 硬规则速查 (违反即扣分) ## 硬规则速查 (违反即扣分)
@ -144,7 +160,7 @@ matplotlib 注意:中文字体 `plt.rcParams['font.sans-serif']=['SimHei','Micro
## 反模式 ## 反模式
- 未 spec_lock 就硬编正文 / 一次性出全文 / 跳过"列要点"直接写正文 - 未 spec 就硬编正文 / 一次性出全文 / 跳过"列要点"直接写正文
- 关键章节(立项依据/研究方案/技术路线/考核指标)整章一次出 —— 必须段段卡 - 关键章节(立项依据/研究方案/技术路线/考核指标)整章一次出 —— 必须段段卡
- **基于"通用模板"自行套基金类型** —— 重大专项 vs 国自然结构完全不同,先查 `fund_types.md` - **基于"通用模板"自行套基金类型** —— 重大专项 vs 国自然结构完全不同,先查 `fund_types.md`
- **自己造数据/指标/单位/经费** —— 不知道就 `<TODO 待用户提供>` - **自己造数据/指标/单位/经费** —— 不知道就 `<TODO 待用户提供>`

View File

@ -1,6 +1,6 @@
# 基金类型 cheat sheet # 基金类型 cheat sheet
每种基金的章节、字数、表格、特殊要求都不同。**写之前必须先确认类型**,然后照抄本文档对应小节的章节大纲到 `spec_lock.md`。字数预算来自 3 份真实模板 (重大专项任务书 2025 / NSFC 联合基金 2026 / 重点研发"区块链") + 当年指南文件 — 指南每年微调,**以当年指南为准**。 每种基金的章节、字数、表格、特殊要求都不同。**写之前必须先确认类型**,然后照抄本文档对应小节的章节大纲到 task 级 spec 文件(命名见 SKILL.md §阶段一)。字数预算来自 3 份真实模板 (重大专项任务书 2025 / NSFC 联合基金 2026 / 重点研发"区块链") + 当年指南文件 — 指南每年微调,**以当年指南为准**。
--- ---

View File

@ -11,7 +11,7 @@
- 自行扩大或缩小指南范围 - 自行扩大或缩小指南范围
- 应用示范类项目没有示范单位 / 示范点 - 应用示范类项目没有示范单位 / 示范点
**自查**: 把指南文本逐条贴进 spec_lock,每写完一节回去标"已覆盖"。 **自查**: 把指南文本逐条贴进 spec,每写完一节回去标"已覆盖"。
## 2. 假大空 (低分) ## 2. 假大空 (低分)

View File

@ -105,7 +105,7 @@ def check_placeholders(text: str, file_label: str) -> list[str]:
def parse_spec_metrics(spec_path: Path) -> list[str]: def parse_spec_metrics(spec_path: Path) -> list[str]:
"""从 spec_lock.md 的"7. 考核指标矩阵"段抽出"指南考核指标"那列。 """从 spec.md 的"7. 考核指标矩阵"段抽出"指南考核指标"那列。
寻找形如 `| 1 | 指南指标 | ... |` 的表行(序号 = 数字),取第 2 寻找形如 `| 1 | 指南指标 | ... |` 的表行(序号 = 数字),取第 2
返回每条指南指标的关键短语列表 (用于在 sections 中模糊匹配) 返回每条指南指标的关键短语列表 (用于在 sections 中模糊匹配)
@ -238,7 +238,7 @@ def main() -> None:
ap.add_argument("sections_dir", type=Path) ap.add_argument("sections_dir", type=Path)
ap.add_argument("--fund-type", required=True, choices=list(REQUIRED_SECTIONS.keys())) ap.add_argument("--fund-type", required=True, choices=list(REQUIRED_SECTIONS.keys()))
ap.add_argument("--spec", type=Path, default=None, ap.add_argument("--spec", type=Path, default=None,
help="spec_lock.md 路径; 提供后会做指南考核指标覆盖度检查") help="spec.md 路径; 提供后会做指南考核指标覆盖度检查")
ap.add_argument("--strict", action="store_true", ap.add_argument("--strict", action="store_true",
help="严格模式: 任何检查项失败均退出 1") help="严格模式: 任何检查项失败均退出 1")
args = ap.parse_args() args = ap.parse_args()

View File

@ -6,7 +6,7 @@
## 00_basic_info.md — 项目基本信息表 ## 00_basic_info.md — 项目基本信息表
按 spec_lock 第 1+6 项填,共 35 行左右。字段: 按 spec 第 1+6 项填,共 35 行左右。字段:
- 项目名称 / 所属专项 / 指南方向 (榜单任务) / 创新分类 / 项目遴选方式 / 项目实施模式 - 项目名称 / 所属专项 / 指南方向 (榜单任务) / 创新分类 / 项目遴选方式 / 项目实施模式
- 单位总数 / 课题数 / 经费预算 (总+中央+地方+自筹+其他) / 项目周期 (起始/结束/实施周期/中期时间点) - 单位总数 / 课题数 / 经费预算 (总+中央+地方+自筹+其他) / 项目周期 (起始/结束/实施周期/中期时间点)
- 申报单位 (名称/性质/主管部门/隶属/所属地区/通信地址/邮编/法定代表人/组织机构代码) - 申报单位 (名称/性质/主管部门/隶属/所属地区/通信地址/邮编/法定代表人/组织机构代码)

View File

@ -1,4 +1,4 @@
# 申报书 spec_lock # 申报书 spec
> 阶段一产物。**写定后不再改**,阶段二每章前都要 read。`<TODO>` 是占位符,需要用户明确填值;不要硬编。 > 阶段一产物。**写定后不再改**,阶段二每章前都要 read。`<TODO>` 是占位符,需要用户明确填值;不要硬编。