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:
parent
775962d68a
commit
c4fac2428b
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 对应版(用户没要)。
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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 # 最终产物 (按主题命名)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 反模式
|
## 反模式
|
||||||
|
|
|
||||||
|
|
@ -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,固定模板 |
|
||||||
|
|
|
||||||
|
|
@ -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) # 砖红 - 次要图形
|
||||||
|
|
|
||||||
|
|
@ -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 {}
|
||||||
|
|
|
||||||
|
|
@ -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` `` / 列表分行 / `` 居中插图 + 图题自动编号 / 识别 mermaid 块按 caption 查 `figures/fig_<caption>.png`
|
- `<skill_dir>/scripts/render_docx.py` —— md→docx,自动加目录 / 解析 `**bold**`/`*italic*`/`` `code` `` / 列表分行 / `` 居中插图 + 图题自动编号 / 识别 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 待用户提供>`
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# 基金类型 cheat sheet
|
# 基金类型 cheat sheet
|
||||||
|
|
||||||
每种基金的章节、字数、表格、特殊要求都不同。**写之前必须先确认类型**,然后照抄本文档对应小节的章节大纲到 `spec_lock.md`。字数预算来自 3 份真实模板 (重大专项任务书 2025 / NSFC 联合基金 2026 / 重点研发"区块链") + 当年指南文件 — 指南每年微调,**以当年指南为准**。
|
每种基金的章节、字数、表格、特殊要求都不同。**写之前必须先确认类型**,然后照抄本文档对应小节的章节大纲到 task 级 spec 文件(命名见 SKILL.md §阶段一)。字数预算来自 3 份真实模板 (重大专项任务书 2025 / NSFC 联合基金 2026 / 重点研发"区块链") + 当年指南文件 — 指南每年微调,**以当年指南为准**。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
- 自行扩大或缩小指南范围
|
- 自行扩大或缩小指南范围
|
||||||
- 应用示范类项目没有示范单位 / 示范点
|
- 应用示范类项目没有示范单位 / 示范点
|
||||||
|
|
||||||
**自查**: 把指南文本逐条贴进 spec_lock,每写完一节回去标"已覆盖"。
|
**自查**: 把指南文本逐条贴进 spec,每写完一节回去标"已覆盖"。
|
||||||
|
|
||||||
## 2. 假大空 (低分)
|
## 2. 假大空 (低分)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
## 00_basic_info.md — 项目基本信息表
|
## 00_basic_info.md — 项目基本信息表
|
||||||
|
|
||||||
按 spec_lock 第 1+6 项填,共 35 行左右。字段:
|
按 spec 第 1+6 项填,共 35 行左右。字段:
|
||||||
- 项目名称 / 所属专项 / 指南方向 (榜单任务) / 创新分类 / 项目遴选方式 / 项目实施模式
|
- 项目名称 / 所属专项 / 指南方向 (榜单任务) / 创新分类 / 项目遴选方式 / 项目实施模式
|
||||||
- 单位总数 / 课题数 / 经费预算 (总+中央+地方+自筹+其他) / 项目周期 (起始/结束/实施周期/中期时间点)
|
- 单位总数 / 课题数 / 经费预算 (总+中央+地方+自筹+其他) / 项目周期 (起始/结束/实施周期/中期时间点)
|
||||||
- 申报单位 (名称/性质/主管部门/隶属/所属地区/通信地址/邮编/法定代表人/组织机构代码)
|
- 申报单位 (名称/性质/主管部门/隶属/所属地区/通信地址/邮编/法定代表人/组织机构代码)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# 申报书 spec_lock
|
# 申报书 spec
|
||||||
|
|
||||||
> 阶段一产物。**写定后不再改**,阶段二每章前都要 read。`<TODO>` 是占位符,需要用户明确填值;不要硬编。
|
> 阶段一产物。**写定后不再改**,阶段二每章前都要 read。`<TODO>` 是占位符,需要用户明确填值;不要硬编。
|
||||||
|
|
||||||
Loading…
Reference in New Issue