修三处 v3 遗留: Iconify 不触发 / mkdir -p 误创目录 / 平台无知

- skills/ppt/SKILL.md: 八条对齐第 7 项默认值从 "MSO_SHAPE 几何形状 (无外部图片资源)"
  改成 "Iconify tabler 集 (描边商务图标, 主色染色, fetch_icon.py 缓存到 assets/icons/)"。
  阶段二每页流程加一步"图标先于版式": 先 glob 本地, 没就 fetch, 再做页。
  根因: v3 砍了 icons.md 里 MSO_SHAPE 当业务图标的部分, 但 SKILL.md 默认值没同步,
  模型把它写进 spec_lock 后阶段二永远不会触发 Iconify 拉取
- tools/shell.py: Windows 下拦截 `mkdir -p X [Y...]`, 走 os.makedirs(exist_ok=True)。
  根因: cmd.exe 的 mkdir 不识别 -p flag, 把 -p 当字面目录名创建
- prompts/system/general_v1.md: 加 "## 平台" 段, 提醒 Windows + cmd 环境下用
  run_python os.makedirs 而非 shell mkdir -p。行为前置防御 + shell.py 工具层后置兜底

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
caoqianming 2026-05-06 13:12:17 +08:00
parent 0971a500e7
commit 38fbee9d9e
3 changed files with 43 additions and 4 deletions

View File

@ -23,3 +23,9 @@
## 路径
默认工作目录在系统消息末尾,所有相对路径基于该目录。
## 平台
当前是 Windows + cmd.exe。**避免用 unix-only flag**:
- 建目录用 `run_python``os.makedirs(path, exist_ok=True)`,**不要** `shell mkdir -p`(cmd 不识别 -p,会创建名为 '-p' 的字面目录;shell 工具已对此做兜底但仍以 run_python 为优先)
- 路径分隔符用 `/``\\`,Python 内部都识别;字符串 raw 路径用 `r"..."`
- shell 工具走的是 cmd,不是 bash,管道/重定向语义可能不同 —— 复杂逻辑用 run_python 更稳

View File

@ -47,7 +47,7 @@ description: 生成 PowerPoint 演示文稿 (.pptx)。当用户要求做汇报 P
| 4 | 风格 | **现代简约** (白底 + 细线 + 留白) |
| 5 | 配色 | **商务红** `#C00000` `#E15554` `#FFC107` (见上"默认主题") |
| 6 | 字体 | **微软雅黑 + Arial** |
| 7 | 图标 | **MSO_SHAPE 几何形状** (无外部图片资源) |
| 7 | 图标 | **Iconify `tabler` 集** (描边商务图标,主色染色;`fetch_icon.py` 拉到 `assets/icons/` 缓存) |
| 8 | 图表 | 数据 ≥ 3 个点的页用 matplotlib 配图 |
把这 8 项写进 `spec_lock.md`,以表格形式给用户预览,问一句"按这个开干?"。**spec_lock 写定后不再改**,有冲突回头跟用户重新对齐。
@ -58,9 +58,10 @@ description: 生成 PowerPoint 演示文稿 (.pptx)。当用户要求做汇报 P
每页流程:
1. 读 `spec_lock.md` (即使刚读过)
2. 写一个 `run_python` block,用 python-pptx 添加这一页 (载入已有 .pptx → append slide → save)
3. 报这一页:版式、标题、要点条数、是否含图
4. 用户确认 / 微调后再下一页
2. **图标先于版式**: 这一页要用什么概念图标? 先 `glob skills/ppt/assets/icons/` 看本地有没有,没有就 `python skills/ppt/scripts/fetch_icon.py <name> --set tabler --color C00000 --size 128 -o skills/ppt/assets/icons/...` 拉一个;`add_picture` 嵌入。**几何形状(圆点/徽章/装饰线)不算图标,走 layouts.md helper 即可**
3. 写一个 `run_python` block,用 python-pptx 添加这一页 (载入已有 .pptx → append slide → save)
4. 报这一页:版式、标题、要点条数、用了哪些图标
5. 用户确认 / 微调后再下一页
**为什么逐页?** 一次性出全 deck 中途改方向就要全推翻;逐页能让用户在第 2 页就发现问题。
@ -103,6 +104,7 @@ python scripts/quality_check.py <output.pptx> --spec spec_lock.md
- **基于"场景判断"自行换配色**(见上"默认主题"违规清单)
- **缺封面 / 缺尾页(Q&A)** —— 两端都是强制项,不算在正文页数预算内
- **裸白纸版式** —— 所有版式起手都必须 `apply_brand(slide, kind)`,见 layouts.md
- **业务概念页只用几何形状** —— 比如"战略目标"页只摆圆点 bullet 没有 `target` 图标,视觉太单薄;按 §阶段二第 2 步先拉 Iconify 图标再做页
- 一个 `run_python` 出整 deck (中途改方向就要全推翻)
- 跑完不做 `quality_check.py` 就交付
- 起名 `output.pptx` / `untitled.pptx` —— 务必按主题给文件名

View File

@ -1,7 +1,11 @@
"""Shell 执行: subprocess 跑命令,有黑名单拦明显危险操作。"""
from __future__ import annotations
import os
import re
import shlex
import subprocess
import sys
from .base import Tool
@ -32,12 +36,36 @@ class ShellTool(Tool):
"format c:",
)
# Windows cmd 不识别 unix flag,常见踩坑命令直接在工具层兜底
_MKDIR_P_RE = re.compile(r"^\s*mkdir\s+-p\s+(.+?)\s*$")
def _windows_compat(self, command: str) -> tuple[str, str | None]:
"""Windows cmd 下把 unix 风格命令转译为可执行形式。
返回 (转译后命令, 转译说明 or None)无需转译时第二项为 None
"""
if sys.platform != "win32":
return command, None
m = self._MKDIR_P_RE.match(command)
if m:
paths = shlex.split(m.group(1), posix=False)
for p in paths:
p = p.strip('"').strip("'")
os.makedirs(p, exist_ok=True)
return (
"echo [shell-tool] mkdir -p handled in-process (Windows cmd doesn't support -p)",
f"intercepted `mkdir -p`: created {paths} via os.makedirs",
)
return command, None
def execute(self, command: str, timeout: int = 60) -> str:
normalized = command.lower()
for pat in self.BLOCKED_PATTERNS:
if pat in normalized:
return f"[Error] blocked dangerous command pattern: {pat!r}"
command, note = self._windows_compat(command)
try:
result = subprocess.run(
command,
@ -54,6 +82,9 @@ class ShellTool(Tool):
except FileNotFoundError as e:
return f"[Error] {e}"
if note:
result.stdout = (result.stdout or "") + f"\n[note] {note}"
parts = []
if result.stdout:
parts.append(f"[stdout]\n{result.stdout.rstrip()}")