From 7b09c77f768b4b73f9d9d3e12fa9962370b6fe76 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Wed, 3 Jun 2026 16:33:59 +0800 Subject: [PATCH] =?UTF-8?q?fix(sandbox):=20docker=20=E4=B8=8B=20system=20p?= =?UTF-8?q?rompt=20/=20SKILL=20=E6=96=87=E6=A1=A3=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E7=94=A8=E5=AE=B9=E5=99=A8=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docker backend 下 shell/run_python/fs 工具全在容器里跑(/users/ bind 到 /workspace),但喂给 LLM 的路径多处仍是宿主绝对路径(容器内不存在), agent 据此 find 全空、瞎转到 pwd 才发现真身在 /workspace/。 - core/agent_builder.py::_build_system_prompt:docker 下 task_dir + 「宪法」 glob 范例换成容器路径 /workspace/,去掉容器里无意义的 cwd 行(cwd 恒等 task_dir);host 不变。修法同 LoadSkillTool 的 container_skills_dir 改写。 - skills 文档同类宿主路径残留清扫:patent 跨 skill 调 proposal 脚本由 /...(硬编码 D:/projects/zcbot)改兄弟相对 /../proposal/...; research fetch_pdf 范例硬编码 D:/... 改 占位;patent/proposal/ppt 的 举例的废弃旧布局 workspace/tasks/ 改 host/docker 双形态说明。 - ppt 图标缓存写进只读挂载修复(方案 A):种子图标库 /assets/icons/ 降为只读(glob 读),fetch_icon.py 新拉图标一律 -o /assets/icons/, 读路径改种子库 + 本 task 两处都 glob。docker 下 skills 是 :ro bind,旧写法必败; host 下还顺带污染仓库工作树。脚本已 out.parent.mkdir,无需改。 Co-Authored-By: Claude Opus 4.8 (1M context) --- PROGRESS.md | 3 +++ core/agent_builder.py | 32 +++++++++++++++++++++++++++++--- skills/patent/SKILL.md | 8 ++++---- skills/ppt/SKILL.md | 10 +++++----- skills/ppt/assets/icons/INDEX.md | 6 +++--- skills/ppt/references/icons.md | 19 +++++++++++-------- skills/proposal/SKILL.md | 2 +- skills/research/SKILL.md | 2 +- 8 files changed, 57 insertions(+), 25 deletions(-) diff --git a/PROGRESS.md b/PROGRESS.md index 53b514a..91e1349 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -23,6 +23,9 @@ ### 2026-06-03 +- **修 docker sandbox 下 system prompt 焊死宿主路径(agent 找不到自己的文件)**:`ZCBOT_SANDBOX_BACKEND=docker` 时 shell/run_python/fs 工具全在容器里跑(`/users/` bind 到 `/workspace`),但 `_build_system_prompt` 注入的 `task_dir` / cwd 是宿主绝对路径(容器内不存在),LLM 据此 `find /home/ubuntu/zcbot ...` 全空、瞎转到 `pwd` 才发现真身在 `/workspace/`。修法同 `LoadSkillTool` 的 `container_skills_dir` 改写:docker 下 `task_dir` + 「宪法」glob 范例换成容器路径 `/workspace/`,并去掉 docker 下无意义的 cwd 行(容器 cwd 恒等 task_dir);host 不变。 +- **顺扫清掉 SKILL.md 里残留的宿主路径假设(同 docker 隐患)**:`patent` 跨 skill 调 proposal 脚本原用 `/skills/proposal/...`(还举例硬编码 `D:/projects/zcbot`)→ 改兄弟 skill 相对 `/../proposal/scripts/...`(两后端都对);`research` 的 `fetch_pdf` 范例硬编码 `working_dir=r"D:/..."` → 改 `` 占位;`patent`/`proposal`/`ppt` 的 `` 举例用废弃旧布局 `…/workspace/tasks//` → 改 host/docker 双形态说明。 +- **修 ppt 图标缓存写进只读挂载(docker 拉新图标失败)**:`fetch_icon.py -o /assets/icons/` 在 docker 下 skills 是 `:ro` bind,写回必败;host 下还顺带污染仓库工作树。改为(方案 A,与"产物只写 task_dir"的全局规矩一致):种子图标库 `/assets/icons/` 降为**只读**(glob 读),`fetch_icon.py` 新拉的图标一律 `-o /assets/icons/`;读路径改两处都 glob(种子库 + 本 task)。涉 `ppt/SKILL.md`、`references/icons.md`、`assets/icons/INDEX.md`;脚本本身已 `out.parent.mkdir`,无需改。 - **默认镜像源改清华(pip+apt)/ 腾讯(npm)**:腾讯 PyPI 吐损坏 litellm wheel(index 声明 sha256 与文件实际字节不符,非篡改 = 镜像端文件损坏)。`deploy/update.sh` 三默认值改清华(境内稳 + 同步及时;npm 无清华源走腾讯);换默认让下次 build pip 层全量重跑一次。 - **回退 `ZCBOT_WORKSPACE_DIR` env 覆盖,workspace 落数据盘改用 bind mount**:env 覆盖与 `core/paths.py` 锚 ROOT 的相对存储冲突 —— env 指向 ROOT 外致文件面板 / agent resume / 新建 task 三家分叉。`resolve_workspace` 回退成 `arg > cfg > 默`(均 ROOT 内),数据盘改用 bind mount(`/data/zcbot/workspace` → `ROOT/workspace`,DB 不改、dev 不受影响)。 diff --git a/core/agent_builder.py b/core/agent_builder.py index 00a4e8e..ad7b508 100644 --- a/core/agent_builder.py +++ b/core/agent_builder.py @@ -212,7 +212,22 @@ def _build_system_prompt( if skills.skills: prompt += f"\n\n## 可用 skill (用 load_skill 加载完整指引)\n{skills.discovery_block()}" prompt += memory_block(workspace_dir, user_id) + # docker backend 下 shell/run_python/fs 工具全在容器里跑,容器把 + # `/users/` bind 到 `/workspace`、`--workdir /workspace/` + # (executor_docker.py:99-100)。此时 prompt 必须给**容器路径**,否则 LLM + # 拿着宿主绝对路径在沙盒里 find 不到任何东西(host 路径容器内根本不存在)。 + # host backend 不变,直接用宿主绝对路径。 wd_abs = working_dir.resolve() + is_docker = os.getenv("ZCBOT_SANDBOX_BACKEND", "host").lower() == "docker" + if is_docker: + try: + wd_rel = wd_abs.relative_to(user_root(workspace_dir, user_id)) + wd_path = "/workspace/" + wd_rel.as_posix() + except ValueError: + # working_dir 不在 user_root 下 —— 防御性兜底,正常路径不会到这里 + wd_path = "/workspace" + else: + wd_path = str(wd_abs) today = datetime.now().strftime("%Y-%m-%d") tname = task_name or "<未指定>" short_id = task_id.hex[:8] @@ -220,10 +235,21 @@ def _build_system_prompt( f"- **task 预选 skill**: `{task_skill}` — 用户创建时声明的主 skill\n" if task_skill else "" ) + # docker 下容器 cwd 恒等于 task_dir(fs 工具 base_dir = --workdir = task_dir), + # 不存在"另一个只读宿主 cwd",留 cwd 行只会误导 → 去掉,改提一句工具起步点。 + if is_docker: + loc_lines = ( + f"- **task_dir(工作目录,所有产物写这里;shell / run_python / 读写工具" + f"均从此目录起步,相对路径都相对它)**: `{wd_path}`\n" + ) + else: + loc_lines = ( + f"- cwd(用户启动时所在目录,只读用): `{tool_base}`\n" + f"- **task_dir(所有产物写到这里)**: `{wd_path}`\n" + ) prompt += ( f"\n\n## 工作目录与 task 上下文\n" - f"- cwd(用户启动时所在目录,只读用): `{tool_base}`\n" - f"- **task_dir(所有产物写到这里)**: `{wd_abs}`\n" + f"{loc_lines}" f"- **task_short_id**(永不变,「宪法」文件主锚): `{short_id}`\n" f"- **task_name**(可变,写进文件名作人类可读说明): `{tname}`\n" f"- **today**(当前日期,用于「宪法」文件命名): `{today}`\n" @@ -243,7 +269,7 @@ def _build_system_prompt( f"`` = `{tname}`(可变,人类可读说明,原样用 含 CJK / 空格);" f"`` 由 skill 定义(如 proposal/ppt 的 `spec`)。\n\n" f"**取 current 版本规则**:read 时 **按 task_short_id 锚定** glob " - f"`{wd_abs}/*-{short_id}-*..md` → 按文件名字典序排 → 取最大者" + f"`{wd_path}/*-{short_id}-*..md` → 按文件名字典序排 → 取最大者" f"(= 最新日期)。这样即使用户改了 task_name,旧文件仍能定位(`` " f"那段视为「建时快照」,不强求同步)。这是「current 指针」的纯文件名实现," f"agent 自己拼即可。\n\n" diff --git a/skills/patent/SKILL.md b/skills/patent/SKILL.md index c5ce959..51bc7d8 100644 --- a/skills/patent/SKILL.md +++ b/skills/patent/SKILL.md @@ -128,19 +128,19 @@ read /references/self_check.md # 模型按清单 6 类逐项过一遍 sections/*.md, 不过则回头 edit # 2. mermaid 附图预渲染 (章节有 ```mermaid``` 块就跑) -python /skills/proposal/scripts/render_diagrams.py /sections/ +python /../proposal/scripts/render_diagrams.py /sections/ # 3. 渲染 .docx (复用 proposal skill 的脚本,patent 不另写) -python /skills/proposal/scripts/render_docx.py /sections/ --fund-type key_rd -o /<案件名>_技术交底书.docx +python /../proposal/scripts/render_docx.py /sections/ --fund-type key_rd -o /<案件名>_技术交底书.docx ``` > `render_docx.py` 的 `--fund-type` 只影响目录页表头文案与封面,不影响章节解析 —— 交底书复用 `key_rd` 排版规范(国标黑体/宋体/1.5 倍行距)。封面页用户拿到后手动改成"技术交底书"标题,或在 sections/00_封面.md 自定义。 -`` 取自 `` 的父级再父级,e.g. `skill_dir = D:/projects/zcbot/skills/patent` → `repo_root = D:/projects/zcbot`。 +复用的 proposal 脚本是 patent 的**兄弟 skill**,用 `/../proposal/scripts/...` 定位(`` = load_skill 头部给的本 skill 绝对目录,host 下是宿主路径、docker 沙盒里是 `/sandbox/skills/patent`,`../proposal` 在两种后端都落到同级 proposal 目录)。**不要**拼 repo 根的宿主绝对路径 —— docker 容器里看不到宿主路径。 ## 工作目录 -`` = system prompt 给的**绝对路径**(`…/workspace/tasks//`)。**所有产物都写到 task_dir 下**,不要写到 cwd / `skills/` / repo 根。 +`` = system prompt 给的**绝对路径**(host 下形如 `…/workspace/users///`,docker 沙盒里是 `/workspace//`)。**所有产物都写到 task_dir 下**,不要写到 cwd / `skills/` / repo 根。 ``` / diff --git a/skills/ppt/SKILL.md b/skills/ppt/SKILL.md index ea43a57..9de498e 100644 --- a/skills/ppt/SKILL.md +++ b/skills/ppt/SKILL.md @@ -11,9 +11,9 @@ description: 生成 PowerPoint 演示文稿 (.pptx) 文件。✅ 触发:用户 - `references/design_principles.md` —— 画布尺寸 + 字号/配色/留白/字数预算等硬规则 - `references/layouts.md` —— 9 种版式的 python-pptx 起手代码 + 安全区/越界保护 + `apply_brand` 品牌条 - `references/icons.md` —— 业务图标两层:Iconify (在线/本地缓存) / unicode 字形兜底 -- `assets/icons/` —— 本地图标缓存 (Iconify 拉过的图存这,见 `INDEX.md` 推荐清单) +- `assets/icons/` —— **只读**种子图标库 (skill 自带的商务红 tabler 集,见 `INDEX.md`;docker 沙盒里 skills 是只读挂载。新拉的图标写 `/assets/icons/`) - 素材摄取: 用 `markitdown` CLI 把 PDF/DOCX/PPTX/XLSX/HTML/URL 转干净 Markdown,统一落到 `/source/.md`(同 working_dir 多 task 共享 source 池) -- `scripts/fetch_icon.py` —— 从 Iconify CDN 拉 SVG/PNG (染主题色,缓存本地) +- `scripts/fetch_icon.py` —— 从 Iconify CDN 拉 SVG/PNG (染主题色,缓存到 `/assets/icons/`) - `scripts/render_icon.py` —— unicode 字形 → 透明 PNG (Iconify 没有时兜底) - `scripts/quality_check.py` —— 产物 .pptx 验收 (越界 / 文本溢出 / 颜色一致) @@ -63,7 +63,7 @@ glob /*--*.spec.md → 按文件名字典序排,取最 | 4 | 风格 | **现代简约** (白底 + 细线 + 留白) | | 5 | 配色 | **商务红** `#C00000` `#E15554` `#FFC107` (见上"默认主题") | | 6 | 字体 | **微软雅黑 + Arial** | -| 7 | 图标 | **Iconify `tabler` 集** (描边商务图标,主色染色;`fetch_icon.py` 拉到 `assets/icons/` 缓存) | +| 7 | 图标 | **Iconify `tabler` 集** (描边商务图标,主色染色;`fetch_icon.py` 拉到 `/assets/icons/`) | | 8 | 图表 | 数据 ≥ 3 个点的页用 matplotlib 配图 | 把这 8 项写进上面那个 task 级 spec 文件,以表格形式给用户预览,问一句"按这个开干?"。**spec 写定后不再改**(要改就走 §0 的「重定调」分支,以 today 为前缀写新版,旧版保留)。 @@ -74,7 +74,7 @@ glob /*--*.spec.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 即可** +2. **图标先于版式**: 这一页要用什么概念图标? 先 `glob` 两处看有没有现成 —— 种子库 `/assets/icons/`(只读,`` 是 `load_skill` 头里的绝对路径)+ 本 task `/assets/icons/`;没有就 `python /scripts/fetch_icon.py --set tabler --color C00000 --size 128 -o /assets/icons/...` 拉一个(种子库只读,新图标落 task 目录);`add_picture` 嵌入。**几何形状(圆点/徽章/装饰线)不算图标,走 layouts.md helper 即可** 3. 写一个 `run_python` block,用 python-pptx 添加这一页 (载入已有 .pptx → append slide → save) 4. 报这一页:版式、标题、要点条数、用了哪些图标 5. 用户确认 / 微调后再下一页 @@ -107,7 +107,7 @@ python /scripts/quality_check.py / --spec ` = system prompt 里「task_dir」给的**绝对路径**(形如 `…/workspace/tasks//`)。**所有产物都写到 task_dir 下**,不要写到 cwd / `skills/` / repo 根;`assets/icons/` 是 skill 自带的图标缓存,继续走 `skills/ppt/assets/icons/`。 +下文 `` = system prompt 里「task_dir」给的**绝对路径**(host 下形如 `…/workspace/users///`,docker 沙盒里是 `/workspace//`)。**所有产物都写到 task_dir 下**,不要写到 cwd / `skills/` / repo 根;图标分两处:skill 自带的**只读种子库**走 `/assets/icons/`(docker 沙盒里 skills 只读,只读不写),`fetch_icon.py` 新拉的图标写 `/assets/icons/`(详见 references/icons.md §A)。 ``` / diff --git a/skills/ppt/assets/icons/INDEX.md b/skills/ppt/assets/icons/INDEX.md index 4c67d54..e35bcee 100644 --- a/skills/ppt/assets/icons/INDEX.md +++ b/skills/ppt/assets/icons/INDEX.md @@ -1,6 +1,6 @@ # 本地图标库 -> 这里缓存通过 `scripts/fetch_icon.py` 从 Iconify 拉下来的图标,**已入库一组商务红 tabler 种子集**(target / brain / chart-bar / users / trophy / alert-triangle / cpu / building-factory / cloud-network / database 等),覆盖大部分商务汇报场景。新场景模型按需 `fetch_icon.py` 拉,再次用时直接读本地不发请求。 +> 这里是 skill 自带的**只读种子图标库**,**已入库一组商务红 tabler 种子集**(target / brain / chart-bar / users / trophy / alert-triangle / cpu / building-factory / cloud-network / database 等),覆盖大部分商务汇报场景 —— 直接 `glob` 读用即可。docker 沙盒里 `skills/` 是只读挂载,**不能往这儿写**。新场景按需 `fetch_icon.py` 拉,落点是 `/assets/icons/`(可写),本 task 内再用直接读不发请求。 ## 缓存命名规约 @@ -13,10 +13,10 @@ ## 推荐图标清单 (按业务主题) -按下面 3 行命令拉取首批最常用 18 个,够覆盖 80% 商务汇报场景: +种子集已含下列大部分;若某个本 task 缺,按下面命令拉到 `/assets/icons/`(种子库只读,新图标进 task 目录): ```bash -ICONS_DIR=/assets/icons # 来自 load_skill 头 +ICONS_DIR=/assets/icons # 可写落点;/scripts 来自 load_skill 头(只读可执行) # 战略 / 目标 / 启动 for n in target rocket flag bulb; do diff --git a/skills/ppt/references/icons.md b/skills/ppt/references/icons.md index 81557a4..6d77b61 100644 --- a/skills/ppt/references/icons.md +++ b/skills/ppt/references/icons.md @@ -13,21 +13,24 @@ ## §A. Iconify 个性化图标 (本地缓存 + 网络拉取) -### A1. 本地库 -路径: `/assets/icons/`,详见 [INDEX.md](../assets/icons/INDEX.md)。 +### A1. 本地库 (两处:只读种子库 + 本 task 已拉) +- **种子库(只读)**: `/assets/icons/` —— skill 自带的商务红 tabler 种子集,详见 [INDEX.md](../assets/icons/INDEX.md)。docker 沙盒里 `skills/` 是只读挂载,**只能读、不能往这儿写**。 +- **本 task 已拉**: `/assets/icons/` —— A2 fetch 新图标的落点(可写)。 + 命名规约: `___.png`(如 `tabler_rocket_C00000_128.png`) -**用之前先 `glob` 检查本地有没有**,有就直接 `add_picture`,免去网络往返。 +**用之前先 `glob` 两处都查一遍**(种子库 `/assets/icons/` + 本 task `/assets/icons/`),有就直接 `add_picture`,免去网络往返。 ### A2. fetch_icon.py 拉新图标 +脚本在 `/scripts/`(只读可执行);拉下来的图标 `-o` **必须落 `/assets/icons/`**(种子库只读,新图标进 task 目录): ```bash # 主红色 128px PNG (推荐) python /scripts/fetch_icon.py rocket --set tabler --color C00000 \ - --size 128 -o /assets/icons/tabler_rocket_C00000_128.png + --size 128 -o /assets/icons/tabler_rocket_C00000_128.png # 强调色金黄 python /scripts/fetch_icon.py target --set tabler --color FFC107 \ - --size 128 -o /assets/icons/tabler_target_FFC107_128.png + --size 128 -o /assets/icons/tabler_target_FFC107_128.png ``` `--set` 默认 `tabler`(4500+ 商务图标,MIT)。其它选 `lucide / heroicons / material-symbols / carbon / fluent / mdi`。**整 deck 只用一个 set**。 @@ -37,7 +40,7 @@ PNG 转换需 `pip install cairosvg`(推荐)或 `pip install svglib`。没装也 ### A3. 嵌入幻灯片 ```python slide.shapes.add_picture( - "/assets/icons/tabler_rocket_C00000_128.png", + "/assets/icons/tabler_rocket_C00000_128.png", # 路径 = glob 命中的那处(种子库或 task) Inches(1.0), Inches(2.5), width=Inches(0.8), # 装饰图标 0.5-1.5 in;别超 2 in ) @@ -53,7 +56,7 @@ slide.shapes.add_picture( ```python from pptx.util import Inches, Pt stages = ["调研","设计","开发","测试","上线"] -icon_path = "/assets/icons/tabler_chevron-right_C00000_64.png" +icon_path = "/assets/icons/tabler_chevron-right_C00000_64.png" # 先 fetch_icon.py 拉到 task,种子库没有 chevron-right_64 for i, label in enumerate(stages): x = 0.7 + i * 2.4 add_textbox(slide, x, 3.7, 1.8, 0.5, label, 16, bold=True, @@ -90,7 +93,7 @@ python /scripts/render_icon.py "✓" --color "#C00000" --size 96 -o < 4. **不替表意** —— 一个 ★ 不能代替"重点"两字 5. **避免 emoji** —— 跨系统渲染差异大,且自带颜色冲突主题 6. **不要每页都堆** —— 装饰是配角,文字是主角 -7. **缓存复用** —— Iconify 拉的图标进 `assets/icons/` 缓存,下次直接用,不要重复请求 +7. **缓存复用** —— Iconify 拉的图标进 `/assets/icons/`,本 task 内再用直接读,不要重复请求(种子库 `/assets/icons/` 只读,新图标不往那写) ## §D. 不要把 layouts.md helper 当"图标" diff --git a/skills/proposal/SKILL.md b/skills/proposal/SKILL.md index 82b7569..829463d 100644 --- a/skills/proposal/SKILL.md +++ b/skills/proposal/SKILL.md @@ -103,7 +103,7 @@ caption 必须写、必须全 task 唯一 —— render_diagrams / quality_check ## 工作目录 -`` = system prompt 给的**绝对路径**(`…/workspace/tasks//`)。**所有产物都写到 task_dir 下**,不要写到 cwd / `skills/` / repo 根。 +`` = system prompt 给的**绝对路径**(host 下形如 `…/workspace/users///`,docker 沙盒里是 `/workspace//`)。**所有产物都写到 task_dir 下**,不要写到 cwd / `skills/` / repo 根。 ``` / diff --git a/skills/research/SKILL.md b/skills/research/SKILL.md index b87fe57..4893381 100644 --- a/skills/research/SKILL.md +++ b/skills/research/SKILL.md @@ -90,7 +90,7 @@ print(paper["abstract"]) `has_fulltext_pdf=False` 或 `pdf_url` 空(publication_date 缺失)→ 抛 `RuntimeError`。 ```python -rel = fetch_pdf("10.1016/j.cemconres.2020.106156", working_dir=r"D:/projects/zcbot/workspace/users//") +rel = fetch_pdf("10.1016/j.cemconres.2020.106156", working_dir="") # = system prompt 给的工作目录绝对路径(docker 沙盒里形如 /workspace/) # rel == "papers/10.1016_j.cemconres.2020.106156.pdf" ```