diff --git a/.gitignore b/.gitignore index f5b17c6..bedf58c 100644 --- a/.gitignore +++ b/.gitignore @@ -21,8 +21,8 @@ venv/ env/ # 用户运行产物 / 临时文件 -# task 内产物(sections/ slides/ spec_lock.md/ *.docx/*.pptx)都在 workspace/tasks// 下, -# 由上面这条 workspace/ 一并忽略。repo 根级别的 sections/ / slides/ / spec_lock.md +# task 内产物(sections/ slides/ *.spec.md/ *.docx/*.pptx)都在 workspace/ 下, +# 由上面这条 workspace/ 一并忽略。repo 根级别的 sections/ / slides/ / *.spec.md # **故意不忽略**——如果 agent 又写错位置,要靠 git status 立刻暴露,不再用 .gitignore 兜底。 workspace/ *.log diff --git a/DESIGN.md b/DESIGN.md index 7440362..8ce1b48 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -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 看到的目录,无中间层翻译。 +**task 级「宪法」文件靠文件名隔离,不 cascade / 不入 DB / 不开物理子目录**(2026-05-20):同 working_dir 多 task **共享中间产物**(`source/` / `sections/` / `figures/`)是真实价值(素材跨多本子复用),但 spec 这种 task 1:1 宪法文件必须隔离(两本子 spec 直接撞)。文件名 `--..md`:`task_short_id`(`task_id.hex[:8]`,永不变)主锚,glob `*--*..md` 字典序最大 = current 版本;`` 让"重定调"写新文件而非 edit 覆盖,旧版自然成历史快照;`` 仅作"建时元数据 / 人类可读说明",改 name 不 cascade(由 short_id 兜底定位)。**反方案不选**:① cascade rename — in-flight run 期间文件丢 + 复杂度上升;② DB 化(spec 入 PG)— 架构最干净但工作量 5-10×,且失"用户直接编辑 markdown"能力,且 spec 字段还在演化没必要这么早 schema 化;③ 物理 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) diff --git a/PROGRESS.md b/PROGRESS.md index 55e78f9..1895852 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -2,7 +2,7 @@ > 配合 `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 +- **task 级「宪法」文件 (spec) 命名约定 + `spec_lock` → `spec` 简化**:同 working_dir 多 task 共享中间产物(`source/` / `sections/` / `figures/` 跨本子复用)是设计意图,但 spec 这种 task 1:1 宪法文件必须隔离 — 两本子 spec 直接撞。文件名约定 `--.spec.md`:`task_short_id`(`task_id.hex[:8]`,永不变)作主锚,glob `*--*.spec.md` 字典序最大 = current;`` 让"重定调"写新文件而非 edit 覆盖,旧版自然成历史快照;`` 写入作建时元数据,改 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 任务行 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 最左插 ``,点击 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 对应版(用户没要)。 diff --git a/core/agent_builder.py b/core/agent_builder.py index e3e554c..d31011c 100644 --- a/core/agent_builder.py +++ b/core/agent_builder.py @@ -139,25 +139,59 @@ def _build_system_prompt( tool_base: Path, working_dir: Path, user_id: UUID, + task_id: UUID, + task_name: str, ) -> str: - """拼 system prompt: 模板 + skill 列表 + memory + 工作目录段。 + """拼 system prompt: 模板 + skill 列表 + memory + 工作目录段 + task 上下文 + 命名约定。 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") if skills.skills: prompt += f"\n\n## 可用 skill (用 load_skill 加载完整指引)\n{skills.discovery_block()}" prompt += memory_block(workspace_dir, user_id) wd_abs = working_dir.resolve() + today = datetime.now().strftime("%Y-%m-%d") + tname = task_name or "<未指定>" + short_id = task_id.hex[:8] prompt += ( - f"\n\n## 工作目录\n" + f"\n\n## 工作目录与 task 上下文\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 文档里出现的 `` 占位符,一律指上面这个绝对路径。" - f"产物示例: `{wd_abs}/spec_lock.md`、" - f"`{wd_abs}/sections/01_summary.md`、" - f"`{wd_abs}/slides/`、最终 .docx/.pptx。\n" - f"⛔ 不要把产物写到 cwd / `skills/` / repo 根 —— 只写到 task_dir。" + f"普通产物(sections / slides / 终稿 .docx/.pptx)按 SKILL 文档落路径;" + f"「宪法」性文件(spec 等)按下面《task 级「宪法」文件命名约定》拼路径。\n" + f"⛔ 不要把产物写到 cwd / `skills/` / repo 根 —— 只写到 task_dir。\n" + f"\n## task 级「宪法」文件命名约定(跨 skill 通用)\n" + f"任何 skill 产物中,跟 task 1:1 强绑定、阶段二/后续步骤会**反复 read**" + f"的「宪法」性文件(如 proposal/ppt 的 spec、outline 等),**统一按下面格式命名**," + f"落在 task_dir 根下:\n\n" + f" --..md\n\n" + f"其中 `` = 本会话 today=`{today}`;" + f"`` = `{short_id}`(永不变,主锚);" + f"`` = `{tname}`(可变,人类可读说明,原样用 含 CJK / 空格);" + f"`` 由 skill 定义(如 proposal/ppt 的 `spec`)。\n\n" + f"**取 current 版本规则**:read 时 **按 task_short_id 锚定** glob " + f"`{wd_abs}/*-{short_id}-*..md` → 按文件名字典序排 → 取最大者" + f"(= 最新日期)。这样即使用户改了 task_name,旧文件仍能定位(`` " + f"那段视为「建时快照」,不强求同步)。这是「current 指针」的纯文件名实现," + f"agent 自己拼即可。\n\n" + f"**重定调场景**:用户阶段一已确认过的「宪法」文件,后续要推翻重写时," + f"以 today=`{today}` 为前缀写一份新的,**旧版自然保留为历史快照**(不要 edit " + f"覆盖旧文件)。同日多次重定调可在文件名末尾加 `-v2` / `-v3` 等递增后缀。\n\n" + f"**隔离逻辑**:同 working_dir 多 task → 由 `` 严格隔离" + f"(8 位 hex,撞概率近 0);同 task 多版本 → 由 `` 隔离。两层隔离" + f"都靠文件名,**无目录嵌套、无 DB 字段、无 cascade rename**。其余产物" + f"(`sections/` / `figures/` / `slides/` / 终稿 .docx/.pptx 等)按 SKILL " + f"文档保留扁平共享,LLM 自行通过 task_short_id / 命名前缀判断归属。" ) return prompt @@ -238,19 +272,45 @@ def build_agent( 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") # meta["working_dir"] 是 db 形态(相对 ROOT 或绝对);Session.append → ensure_local_task_row # 把它直接落 PG tasks.working_dir,所以这里就转好。文件系统操作仍用 working_dir_path(absolute)。 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 = { "id": sid, "created_at": now_iso, "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, "model": caps.model_id, "model_profile": model, @@ -261,28 +321,8 @@ def build_agent( if resume: 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: 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 = {} for cls in (ReadTool, WriteTool, EditTool, GlobTool, GrepTool, ShellTool): diff --git a/core/session.py b/core/session.py index cf3c830..b48de3c 100644 --- a/core/session.py +++ b/core/session.py @@ -35,7 +35,7 @@ def atomic_write_text(path: Path, text: str, encoding: str = "utf-8") -> None: """原子写: 先写到 path.tmp 再 os.replace 到 path。 防止写中途异常(磁盘满 / 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) tmp = path.with_suffix(path.suffix + ".tmp") diff --git a/skills/ppt/SKILL.md b/skills/ppt/SKILL.md index 7effffe..3c1787b 100644 --- a/skills/ppt/SKILL.md +++ b/skills/ppt/SKILL.md @@ -12,7 +12,7 @@ description: 生成 PowerPoint 演示文稿 (.pptx)。当用户要求做汇报 P - `references/layouts.md` —— 9 种版式的 python-pptx 起手代码 + 安全区/越界保护 + `apply_brand` 品牌条 - `references/icons.md` —— 业务图标两层:Iconify (在线/本地缓存) / unicode 字形兜底 - `assets/icons/` —— 本地图标缓存 (Iconify 拉过的图存这,见 `INDEX.md` 推荐清单) -- 素材摄取: 直接用 `markitdown` CLI (PDF/DOCX/PPTX/XLSX/HTML/URL → 干净 Markdown) +- 素材摄取: 用 `markitdown` CLI 把 PDF/DOCX/PPTX/XLSX/HTML/URL 转干净 Markdown,统一落到 `/source/.md`(同 working_dir 多 task 共享 source 池) - `scripts/fetch_icon.py` —— 从 Iconify CDN 拉 SVG/PNG (染主题色,缓存本地) - `scripts/render_icon.py` —— unicode 字形 → 透明 PNG (Iconify 没有时兜底) - `scripts/quality_check.py` —— 产物 .pptx 验收 (越界 / 文本溢出 / 颜色一致) @@ -21,7 +21,7 @@ description: 生成 PowerPoint 演示文稿 (.pptx)。当用户要求做汇报 P **主色 `#C00000` / 辅色 `#E15554` / 强调色 `#FFC107`。** -⛔ **不允许擅自换色**。除非满足以下任一条件,否则 spec_lock 必须填这套红色: +⛔ **不允许擅自换色**。除非满足以下任一条件,否则 spec 必须填这套红色: - 用户在请求里**明确**点名其它配色 (例:"做成蓝色"、"用我们公司的紫色") - 用户提供素材里有明确的 brand guideline / 配色卡 @@ -29,13 +29,29 @@ description: 生成 PowerPoint 演示文稿 (.pptx)。当用户要求做汇报 P - "这个场景蓝色更专业" / "学术汇报红色不合适" / "财务用蓝更稳重" - "我觉得 XX 主题更适合" -要换色,**先问用户**,不要在 spec_lock 里塞自己的偏好。其它备选见 `design_principles.md §2`。 +要换色,**先问用户**,不要在 spec 里塞自己的偏好。其它备选见 `design_principles.md §2`。 ## 两阶段工作流 ### 阶段一: 策略 (Strategist) — 八条对齐 -产物:`spec_lock.md` —— 整个 deck 的"宪法",阶段二每页前都要重读。 +产物:**task 级 spec 文件** —— 整个 deck 的"宪法",阶段二每页前都要重读。文件路径按 system prompt 的《task 级「宪法」文件命名约定》: + + /--.spec.md + +`` / `` / `` 用 system prompt 注入的实际值替换。 + +**0. 先检测已有 spec**: + +``` +glob /*--*.spec.md → 按文件名字典序排,取最大者作 current +``` + +(按 short_id 主锚,name 部分不参与匹配 — 用户改过 task name 时旧文件仍能定位) + +- 有 current(当前 task 已有 spec) → 展示给用户,问「**沿用进阶段二** / **重定调**(以 today 写新版,旧版保留)」,⛔ BLOCKING 等用户决定 +- 仅有其它 task 的(`*-<别的 short_id>-*.spec.md`)→ 不当 current 用,继续走下面流程 +- 完全没有 → 直接走下面流程 按下表**一次性给出推荐方案**,然后 ⛔ **BLOCKING:等用户确认/修改后才能进阶段二**。不要一条一条问。 @@ -50,14 +66,14 @@ description: 生成 PowerPoint 演示文稿 (.pptx)。当用户要求做汇报 P | 7 | 图标 | **Iconify `tabler` 集** (描边商务图标,主色染色;`fetch_icon.py` 拉到 `assets/icons/` 缓存) | | 8 | 图表 | 数据 ≥ 3 个点的页用 matplotlib 配图 | -把这 8 项写进 `spec_lock.md`,以表格形式给用户预览,问一句"按这个开干?"。**spec_lock 写定后不再改**,有冲突回头跟用户重新对齐。 +把这 8 项写进上面那个 task 级 spec 文件,以表格形式给用户预览,问一句"按这个开干?"。**spec 写定后不再改**(要改就走 §0 的「重定调」分支,以 today 为前缀写新版,旧版保留)。 ### 阶段二: 执行 (Executor) — 逐页生成 -每页前 **必须 read 一次 `spec_lock.md`**,只用里面定的颜色/字体/图标 —— **不允许凭记忆或临时发挥**。这条规则是为了对抗长 deck 中的上下文漂移。 +每页前 **必须 read 一次 current spec**(按 §0 的 glob 规则拿到的字典序最大那份),只用里面定的颜色/字体/图标 —— **不允许凭记忆或临时发挥**。这条规则是为了对抗长 deck 中的上下文漂移。 每页流程: -1. 读 `/spec_lock.md` (即使刚读过) +1. 读 current spec(即使刚读过) 2. **图标先于版式**: 这一页要用什么概念图标? 先 `glob /assets/icons/` 看本地有没有 (`` 是 `load_skill` 头里的绝对路径),没有就 `python /scripts/fetch_icon.py --set tabler --color C00000 --size 128 -o /assets/icons/...` 拉一个;`add_picture` 嵌入。**几何形状(圆点/徽章/装饰线)不算图标,走 layouts.md helper 即可** 3. 写一个 `run_python` block,用 python-pptx 添加这一页 (载入已有 .pptx → append slide → save) 4. 报这一页:版式、标题、要点条数、用了哪些图标 @@ -70,7 +86,7 @@ description: 生成 PowerPoint 演示文稿 (.pptx)。当用户要求做汇报 P ### 阶段三: 验收 ```bash -python /scripts/quality_check.py / --spec /spec_lock.md +python /scripts/quality_check.py / --spec /--.spec.md ``` 不通过的项,回头 edit 对应页。 @@ -94,11 +110,10 @@ python /scripts/quality_check.py / --spec / -├── source.md # markitdown 转出的素材 -├── spec_lock.md # 八条对齐落定 -├── slides/ -│ └── chart_p3.png # 各页用到的图片素材 -└── .pptx # 最终产物 (按主题命名) +├── source/ # markitdown 转出的素材(同 working_dir 多 task 共享;用 markitdown -o /source/.md) +├── --.spec.md # 八条对齐落定,task 级宪法;命名见 system prompt 约定;按 short_id 主锚,重定调时写新日期,旧版保留 +├── slides/ # 各页用到的图片素材 (chart_p3.png 等),多 task 时文件名前缀区分 +└── .pptx # 最终产物 (按主题命名,多 task 时主题必须不同) ``` ## 反模式 diff --git a/skills/ppt/references/design_principles.md b/skills/ppt/references/design_principles.md index 944869b..25fb81c 100644 --- a/skills/ppt/references/design_principles.md +++ b/skills/ppt/references/design_principles.md @@ -116,7 +116,7 @@ ## 7. 图表规则 (matplotlib) -- 颜色用 spec_lock 里定的主/辅/强调三色,**不要用 matplotlib 默认色板** +- 颜色用 spec 里定的主/辅/强调三色,**不要用 matplotlib 默认色板** - 字号: 标题 16,坐标轴 12,刻度 10 - 去掉上方和右方边框 (`ax.spines['top'/'right'].set_visible(False)`) - 数据标签直接标在柱子/点上,优先于看坐标 @@ -155,6 +155,6 @@ fig.savefig("chart.png", bbox_inches="tight", dpi=150) | 投影看不清 | 字号 < 18 | 加大字号或拆页 | | 颜色花 | 用了超过 5 种色 | 退回三色制 | | bullet 是完整段落 | 把演讲稿当 bullet 写 | 提炼关键词,完整句留给口述 | -| 图表默认配色 | 没改 matplotlib 色板 | 用 spec_lock 主色 | +| 图表默认配色 | 没改 matplotlib 色板 | 用 spec 主色 | | 图标/图片随意找的 | 没统一风格 | 同一来源 / 同一风格 | | 标题在每页位置都不一样 | 没用统一版式 | 见 layouts.md,固定模板 | diff --git a/skills/ppt/references/layouts.md b/skills/ppt/references/layouts.md index c8f8a07..40d2e6e 100644 --- a/skills/ppt/references/layouts.md +++ b/skills/ppt/references/layouts.md @@ -2,7 +2,7 @@ > **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 # ---- 配色 (商务红 — 硬约束默认) ---- -# ⛔ 不允许擅自换色:除非用户明确点名其它配色 (例:"做成蓝色") 或 spec_lock 已写其它 hex, +# ⛔ 不允许擅自换色:除非用户明确点名其它配色 (例:"做成蓝色") 或 spec 已写其它 hex, # 否则就是这套商务红。禁止以"这个场景蓝色更专业"这类自我合理化做替换。 PRIMARY = RGBColor(0xC0, 0x00, 0x00) # 深红 - 标题/强调/关键数据 SECONDARY = RGBColor(0xE1, 0x55, 0x54) # 砖红 - 次要图形 diff --git a/skills/ppt/scripts/quality_check.py b/skills/ppt/scripts/quality_check.py index f83efdb..11bd8e7 100644 --- a/skills/ppt/scripts/quality_check.py +++ b/skills/ppt/scripts/quality_check.py @@ -1,11 +1,11 @@ """quality_check.py: 验收 .pptx,产出问题清单。 用法: - python quality_check.py [--spec spec_lock.md] + python quality_check.py [--spec spec.md] 检查项: - 文件存在且 > 10KB - - 总页数与 spec 一致 (如提供 spec_lock.md) + - 总页数与 spec 一致 (如提供 spec.md) - 每页有标题 - 每页 bullet ≤ 5 条 - 文字字号 ≥ 14pt (除页脚) @@ -220,7 +220,7 @@ def check_pptx(path: Path, spec: dict) -> tuple[list, list]: unmatched = seen_colors - spec_colors if len(unmatched) > 3: warnings.append( - f"出现 {len(unmatched)} 个 spec_lock 之外的颜色,可能用了 matplotlib 默认色板" + f"出现 {len(unmatched)} 个 spec 之外的颜色,可能用了 matplotlib 默认色板" ) return errors, warnings @@ -230,7 +230,7 @@ def main(): ap = argparse.ArgumentParser() ap.add_argument("pptx", type=Path) ap.add_argument("--spec", type=Path, default=None, - help="spec_lock.md 路径") + help="spec.md 路径") args = ap.parse_args() spec = parse_spec(args.spec) if args.spec else {} diff --git a/skills/proposal/SKILL.md b/skills/proposal/SKILL.md index 6adaf82..1f52211 100644 --- a/skills/proposal/SKILL.md +++ b/skills/proposal/SKILL.md @@ -15,7 +15,7 @@ description: 撰写中国科研项目申报书 / 课题任务书 (国家重点 - `/references/review_redlines.md` —— 评审雷区与不可考核词清单 - `/references/citation_gbt7714.md` —— GB/T 7714 顺序编码制 + 文献真实性铁律 - `/references/budget_rules.md` —— 间接费用台阶 + B1-B4 表 -- `/templates/spec_lock.md` —— 阶段一八条对齐的固定字段模板 (复制到 `/spec_lock.md`) +- `/templates/spec.md` —— 阶段一八条对齐的固定字段模板 (复制到 task 级 spec 文件,文件名见下文 §阶段一) - `/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 - `/scripts/render_docx.py` —— md→docx,自动加目录 / 解析 `**bold**`/`*italic*`/`` `code` `` / 列表分行 / `![](path)` 居中插图 + 图题自动编号 / 识别 mermaid 块按 caption 查 `figures/fig_.png` - `/scripts/render_diagrams.py` —— sections/*.md 里的 ```mermaid``` 块预渲染成 `/figures/fig_.png`(caption 必填 + 全 task 唯一,优先 `mmdc`、回退 `mermaid.ink`) @@ -33,25 +33,41 @@ markitdown /budget.xlsx -o /source/budget.md markitdown https://example.com/x -o /source/policy.md ``` -转完后 spec_lock 阶段直接 `read /source/*.md` 拿事实,不要凭印象写。 +转完后 spec 阶段直接 `read /source/*.md` 拿事实,不要凭印象写。 ## 阶段一: 八条对齐 -产物 `/spec_lock.md` —— 申报书"宪法",阶段二每章前都要重读。 +产物:**task 级 spec 文件**(申报书"宪法",阶段二每章前都要重读)。文件路径按 system prompt 的《task 级「宪法」文件命名约定》: -1. **复制模板**: `read /templates/spec_lock.md` → `write /spec_lock.md` + /--.spec.md + +`` / `` / `` 用 system prompt 注入的实际值替换。 + +**0. 先检测已有 spec**(同 working_dir 可能已经有别的 task 的 spec,也可能本 task 之前定调过要重写): + +``` +glob /*--*.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 /templates/spec.md` → `write /--.spec.md` 2. **先读 `/references/fund_types.md` 选基金类型**(章节、字数、表格各不相同) -3. 按 spec_lock.md 字段填,给用户预览 +3. 按字段填,给用户预览 4. ⛔ **BLOCKING:用户确认后才进阶段二** -字段清单见 `/templates/spec_lock.md` (基金类型 / 指南方向 / 关键科学技术问题 / 创新点 / 研究内容骨架 / 团队 / 考核指标矩阵 / 经费预算 / TODO 列表)。 +字段清单见 `/templates/spec.md` (基金类型 / 指南方向 / 关键科学技术问题 / 创新点 / 研究内容骨架 / 团队 / 考核指标矩阵 / 经费预算 / TODO 列表)。 ## 阶段二: 逐章起草 每章两段式:**先列要点 → 用户确认 → 再起草 → 用户确认**。不要直接出正文。 **A. 起草前列要点** (改要点比改正文便宜): -1. 读 `/spec_lock.md` 与 `/references/fund_types.md`,拿本章字数预算与必填要素 +1. 读 **current spec**(按 §阶段一 §0 的 glob 规则拿到的字典序最大那份)与 `/references/fund_types.md`,拿本章字数预算与必填要素 2. 列出 3-6 条要点骨架: 本章打算覆盖的论点 / 数据 / 表格,每条贴上对齐的指南要素与预估字数 3. ⛔ **BLOCKING:用户确认要点 (改 / 加 / 删) 后才动正文** @@ -73,7 +89,7 @@ markitdown https://example.com/x -o /source/policy.md ```bash python /scripts/word_count.py /sections/ --fund-type key_rd -python /scripts/quality_check.py /sections/ --fund-type key_rd --spec /spec_lock.md +python /scripts/quality_check.py /sections/ --fund-type key_rd --spec /--.spec.md python /scripts/render_diagrams.py /sections/ # 章节有 ```mermaid 块就跑 python /scripts/render_docx.py /sections/ --fund-type key_rd -o /.docx ``` @@ -90,10 +106,10 @@ caption 必须写、必须全 task 唯一 —— render_diagrams / quality_check ``` / -├── source/ # 用户给的素材 (指南 PDF / 前期成果) -├── spec_lock.md # 阶段一定调 -├── sections/ # 阶段二逐章产物 (按 templates/.md 切的小节命名) -└── .docx # 最终产物 (按课题命名,不要 output.docx) +├── source/ # 用户给的素材 (指南 PDF / 前期成果),同 working_dir 多 task 共享 +├── --.spec.md # 阶段一定调,task 级宪法;命名见 system prompt 约定;按 short_id 主锚,重定调时写新日期,旧版保留 +├── sections/ # 阶段二逐章产物 (按 templates/.md 切的小节命名);多 task 同章节用 LLM 判断/前缀区分 +└── .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` - **自己造数据/指标/单位/经费** —— 不知道就 `` diff --git a/skills/proposal/references/fund_types.md b/skills/proposal/references/fund_types.md index b1cc7b9..ab8d191 100644 --- a/skills/proposal/references/fund_types.md +++ b/skills/proposal/references/fund_types.md @@ -1,6 +1,6 @@ # 基金类型 cheat sheet -每种基金的章节、字数、表格、特殊要求都不同。**写之前必须先确认类型**,然后照抄本文档对应小节的章节大纲到 `spec_lock.md`。字数预算来自 3 份真实模板 (重大专项任务书 2025 / NSFC 联合基金 2026 / 重点研发"区块链") + 当年指南文件 — 指南每年微调,**以当年指南为准**。 +每种基金的章节、字数、表格、特殊要求都不同。**写之前必须先确认类型**,然后照抄本文档对应小节的章节大纲到 task 级 spec 文件(命名见 SKILL.md §阶段一)。字数预算来自 3 份真实模板 (重大专项任务书 2025 / NSFC 联合基金 2026 / 重点研发"区块链") + 当年指南文件 — 指南每年微调,**以当年指南为准**。 --- diff --git a/skills/proposal/references/review_redlines.md b/skills/proposal/references/review_redlines.md index dd1a083..3b9c9f4 100644 --- a/skills/proposal/references/review_redlines.md +++ b/skills/proposal/references/review_redlines.md @@ -11,7 +11,7 @@ - 自行扩大或缩小指南范围 - 应用示范类项目没有示范单位 / 示范点 -**自查**: 把指南文本逐条贴进 spec_lock,每写完一节回去标"已覆盖"。 +**自查**: 把指南文本逐条贴进 spec,每写完一节回去标"已覆盖"。 ## 2. 假大空 (低分) diff --git a/skills/proposal/scripts/quality_check.py b/skills/proposal/scripts/quality_check.py index b715077..cf6c077 100644 --- a/skills/proposal/scripts/quality_check.py +++ b/skills/proposal/scripts/quality_check.py @@ -105,7 +105,7 @@ def check_placeholders(text: str, file_label: str) -> list[str]: def parse_spec_metrics(spec_path: Path) -> list[str]: - """从 spec_lock.md 的"7. 考核指标矩阵"段抽出"指南考核指标"那列。 + """从 spec.md 的"7. 考核指标矩阵"段抽出"指南考核指标"那列。 寻找形如 `| 1 | 指南指标 | ... |` 的表行(序号 = 数字),取第 2 列。 返回每条指南指标的关键短语列表 (用于在 sections 中模糊匹配)。 @@ -238,7 +238,7 @@ def main() -> None: ap.add_argument("sections_dir", type=Path) ap.add_argument("--fund-type", required=True, choices=list(REQUIRED_SECTIONS.keys())) ap.add_argument("--spec", type=Path, default=None, - help="spec_lock.md 路径; 提供后会做指南考核指标覆盖度检查") + help="spec.md 路径; 提供后会做指南考核指标覆盖度检查") ap.add_argument("--strict", action="store_true", help="严格模式: 任何检查项失败均退出 1") args = ap.parse_args() diff --git a/skills/proposal/templates/key_rd.md b/skills/proposal/templates/key_rd.md index 2a17afe..51fe80d 100644 --- a/skills/proposal/templates/key_rd.md +++ b/skills/proposal/templates/key_rd.md @@ -6,7 +6,7 @@ ## 00_basic_info.md — 项目基本信息表 -按 spec_lock 第 1+6 项填,共 35 行左右。字段: +按 spec 第 1+6 项填,共 35 行左右。字段: - 项目名称 / 所属专项 / 指南方向 (榜单任务) / 创新分类 / 项目遴选方式 / 项目实施模式 - 单位总数 / 课题数 / 经费预算 (总+中央+地方+自筹+其他) / 项目周期 (起始/结束/实施周期/中期时间点) - 申报单位 (名称/性质/主管部门/隶属/所属地区/通信地址/邮编/法定代表人/组织机构代码) diff --git a/skills/proposal/templates/spec_lock.md b/skills/proposal/templates/spec.md similarity index 99% rename from skills/proposal/templates/spec_lock.md rename to skills/proposal/templates/spec.md index a56a030..71c3664 100644 --- a/skills/proposal/templates/spec_lock.md +++ b/skills/proposal/templates/spec.md @@ -1,4 +1,4 @@ -# 申报书 spec_lock +# 申报书 spec > 阶段一产物。**写定后不再改**,阶段二每章前都要 read。`` 是占位符,需要用户明确填值;不要硬编。