---
name: code-exec-guide
description: "Operational guide for `code_exec` in the Docker sandbox. Use for non-trivial scripts, generated files, Office/PDF/HTML tasks, skill-bundled scripts, or cross-turn artifact reuse. Do not use for purely conversational tasks."
---

# Code Exec 沙箱完整指南

`code_exec` 是执行器，不是代码文件写入工具。先看 Quick Rules；需要具体示例时再读后面的 playbook。

## 0. Quick Rules

- `code` 和 `code_ref` 严格二选一：短脚本只传 `code`；长脚本先 `write_file(artifactType=CODE_SCRIPT)`，再只传 `code_ref`。
- 读取上一轮产物只用 `input_refs`。产物会挂到 `/inputs/`；不要假设上一轮 `/outputs/xxx` 还存在。
- `/inputs/manifest.json` 顶层是数组；路径永远取 `manifest[i].path`，不要手猜 `/inputs/00-xxx`。
- 读 manifest 的同一次 `code_exec` 必须带 `input_refs`。不要先跑一个“空 input_refs 读 manifest”的脚本。
- 禁止硬编码 `/inputs/<文件名>`（例如 `/inputs/00-张三.pdf`）；真实 staged 路径可能变成 `00-00-张三.pdf` 或被去重，必须从 manifest 取。
- 所有最终产物写入 `/outputs/`，否则不会被 harvest 成 Artifact。
- 凡是脚本里访问 `/skill/<id>/...`，同一次 `code_exec` 必须传 `activated_skill: "<id>"`；否则该目录不会挂载。
- Python 脚本注释必须用 `#`。不要把 JavaScript 的 `//` 注释放进 `language: "python"`；pptxgenjs/Node 脚本应设 `language: "javascript"`。
- Node/JavaScript 正文含中文引号时，优先用模板字符串或中文弯引号，避免 `"从"信息孤岛"走向"` 这种语法错误。

**验证上一轮产物**：只传新短脚本和 `input_refs`，不要同时带旧脚本的 `code_ref`。

```json
{
  "language": "javascript",
  "input_refs": ["art_output_docx"],
  "code": "const fs = require('fs');\nconst manifest = JSON.parse(fs.readFileSync('/inputs/manifest.json', 'utf8'));\nconsole.log(manifest[0].path);"
}
```

**重跑旧脚本**：只传旧脚本的 `code_ref`；需要的数据文件另放 `input_refs`。

```json
{
  "language": "python",
  "code_ref": "art_script",
  "input_refs": ["art_input_data"]
}
```

## 0.1 什么时候用 code_ref

| 参数 | 用途 | 尺寸 | 适合 |
|---|---|---|---|
| `code` (string) | 把脚本源码直接 inline 在 tool_call 里 | 建议少量几十行；硬上限 8000 字符 / 12KB | 小脚本；一次性执行 |
| `code_ref` (string) | 脚本存在 workspace artifact 里，传 artifactId / resourceId | 受 `sandboxCodeMaxBytes` 执行上限约束；不占 `code_exec` 参数体积 | 长脚本、要迭代改再跑、要让用户在前端工作区看到脚本 |

生成 PPT / Word / Excel / PDF / HTML 的完整脚本、需要复用/调试的脚本、超过几十行的脚本，都默认走 `write_file` + `code_ref`。`input_refs` 可与 `code` 或 `code_ref` 同用，但它只表示数据文件，不表示脚本来源。

### 什么情况下 code_ref 失败

- ref 不存在 / 不属于当前用户 / 跨 thread：sandbox.rejected，LLM 应换一个 ref
- ref 对应的 Artifact 内容超过 `sandboxCodeMaxBytes`（默认 64KB）：报错并建议拆分
- ref 内容不是 UTF-8 文本（比如是个 docx）：会被当成字节流 decode 成乱码，脚本执行失败——**只有纯文本脚本可以走 code_ref**

---

## 1. 环境拓扑

每次 `code_exec` 启动一个**独立 Docker 容器**：

```
/work/        (rw)  entry.py 或 entry.js —— 你提交的 code 被写在这里，作为入口
/inputs/      (ro)  input_refs 引用的文件被挂在这里（只读）
                    /inputs/manifest.json 列出每个文件的真实路径
/outputs/     (rw)  你写进去的文件会被 harvest 成 Artifact（持久化）后销毁
/skill/<id>/  (ro)  activated_skill 的 asset bundle（如 scripts/、templates/）
```

给用户的最终交付文件应默认使用简体中文业务文件名，并保留正确扩展名，例如 `/outputs/请假条示例.pdf`、`/outputs/统计简报.docx`。代码脚本、用户指定名称或技术约定必须英文时除外。

**容器生命周期**：

1. 拉起容器 + 挂载 → 2. 跑 entry.{py,js} → 3. 脚本退出 → 4. 扫描 `/outputs/` 所有文件产出 Artifact → 5. `docker rm -f` 整个容器

**含义**：
- `/outputs` 不在会话之间持久化，**下次调用 /outputs 就是空的**
- `/tmp` / `/work` 也一样，每次全新
- 只有 Artifact（和 ContentStore 里的 resource/oaFile）能跨轮次存在

## 2. 跨轮次读写同一个文件（最常见的坑）

### 场景：turn1 生成 docx → turn2 转 PDF

**反例（会 FileNotFoundError）**：

```python
# turn2 错误写法：假设上一轮的文件还在 /outputs
import subprocess
subprocess.run(['soffice', '--headless', '--convert-to', 'pdf',
                '--outdir', '/outputs', '/outputs/report.docx'])
# → FileNotFoundError: /outputs/report.docx
```

**正确做法**：

```python
# turn2 正确写法：把上一轮返回的 artifactId 放进 input_refs
# tool_call:
# {
#   "name": "code_exec",
#   "arguments": {
#     "language": "python",
#     "input_refs": ["art_ab12cdef"],   ← 上一轮 tool_result.produced[0].artifactId
#     "code": "..."
#   }
# }

import json, subprocess
with open('/inputs/manifest.json', encoding='utf-8') as f:
    manifest = json.load(f)
src = manifest[0]['path']        # e.g. '/inputs/00-report.docx'
subprocess.run(['soffice', '--headless', '--convert-to', 'pdf',
                '--outdir', '/outputs', src], check=True)
```

### 三种 ID 都走 input_refs

| ID 前缀 | 来源 | 用途 |
|---|---|---|
| `art_xxxxx` | 上一轮 `code_exec` 产出 | 跨轮次读回自己生成的文件 |
| （无前缀，纯 UUID）| 用户上传（resourceId） | 读用户通过附件上传的文件 |
| `oa_xxxxx` | OA 平台文件 | 读 OA 系统文件 |

一视同仁：都放 `input_refs`，都挂到 `/inputs/<idx>-<safeName>.<ext>`。

### manifest.json 格式

`/inputs/manifest.json` 的顶层是 **JSON 数组**，不是 `{"inputs": [...]}` 对象。

```json
[
  {
    "index": 0,
    "ref": "art_ab12cdef",
    "displayName": "report.docx",
    "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
    "size": 24580,
    "path": "/inputs/00-report.docx"
  }
]
```

最小读取模板：

```python
import json

with open('/inputs/manifest.json', encoding='utf-8') as f:
    manifest = json.load(f)

src = manifest[0]['path']
```

**永远通过 manifest 拿路径，不要用 displayName 猜**（同名文件会被 deduplicate，真正路径有索引前缀）。

## 3. 镜像预装清单

### Python 库（`pip list` 可见）

| 类 | 库 | 用途 |
|---|---|---|
| 文档 | `python-docx` | .docx 读写（**首选**，见 §6） |
| | `openpyxl` | .xlsx 读写 |
| | `python-pptx` | .pptx 读写 |
| PDF | `pypdf` | PDF 读写、合并、拆分、表单 |
| | `pdfplumber` | PDF 文本 / 表格提取 |
| | `pdf2image` | PDF → PNG/JPG（需 poppler） |
| 图像 | `Pillow` | 通用图像处理 |
| XML | `defusedxml` | 安全 XML 解析 |
| 数据分析 | `numpy` / `pandas` / `polars` | 数值计算、DataFrame 清洗、聚合 |
| | `pyarrow` | Parquet / Feather / Arrow 列式数据读写 |
| | `duckdb` | 本地 SQL 分析，适合 CSV/Parquet 多表查询 |
| | `xlsxwriter` | pandas/openpyxl 之外的 xlsx 写入与图表辅助 |
| | `tabulate` | DataFrame / 表格转 Markdown、纯文本表 |
| 统计 / 建模 | `scipy` / `statsmodels` | 统计检验、回归、时间序列等分析 |
| | `scikit-learn` | 轻量机器学习、特征处理、聚类、评估 |
| 图表 | `matplotlib` / `seaborn` | 静态 PNG/PDF/SVG 图表（首选稳定产物） |
| | `plotly` + `kaleido` | 交互 HTML 或静态图片导出 |
| | `pyecharts` | ECharts HTML 图表 |
| | `altair` + `vl-convert-python` | Vega-Lite 图表与静态导出 |

### Node 全局库（`require()` 可直接用）

| 库 | 用途 |
|---|---|
| `docx` (docx-js) | .docx 生成（**仅简单结构，见 §6 警告**）|
| `pptxgenjs` | .pptx 生成 |
| `parcel` + `@parcel/config-default` | Web artifact 打包（预构建模板在 /opt/） |
| `jszip` | 读写 zip / Office Open XML 包（需使用包含该依赖的新沙箱镜像） |
| `html-inline` | 把外链 JS/CSS/图片内联成单 HTML 文件 |

### 系统工具（shell 可调）

| 工具 | 用途 |
|---|---|
| `soffice` / `libreoffice` | docx/pptx/xlsx → PDF；中文字体已预装（含政务公文字体） |
| `pandoc` | 通用文档转换（md / docx / html / latex 等）|
| `pdftoppm` / `pdftocairo` | PDF → 图片（来自 poppler-utils） |
| `chromium` | Plotly/kaleido 等图表库导出静态图片时使用 |
| `node` / `pnpm` | Node 20，pnpm 9.15.0 |

### 离线包装脚本（`/usr/local/bin/` 已 PATH）

| 脚本 | 用途 |
|---|---|
| `web-artifacts-init <name>` | 秒级把预构建 React + Tailwind + shadcn/ui 项目模板复制到当前目录 |
| `web-artifacts-bundle` | `parcel build` + `html-inline` 产出单文件 bundle.html |

## 4. skill bundle 脚本调用（绝对路径规则）

容器 CWD 是 `/outputs`（不是 skill 根目录）。激活 skill 后想跑它 bundle 里的脚本**必须写绝对路径**：

```python
# ✅ 正确：同一次 code_exec 还必须传 activated_skill="sys_docx"
subprocess.run(['python', '/skill/sys_docx/scripts/office/unpack.py',
                '/inputs/00-template.docx', '/tmp/unpacked'])

# ❌ 错误（FileNotFoundError）
subprocess.run(['python', 'scripts/office/unpack.py',      # 相对路径从 /outputs 找
                '/inputs/00-template.docx', '/tmp/unpacked'])
```

**常见 skill 及其脚本路径前缀**：

| skill id | 脚本根目录 |
|---|---|
| `sys_docx` | `/skill/sys_docx/scripts/` |
| `sys_pptx` | `/skill/sys_pptx/scripts/` |
| `sys_xlsx` | `/skill/sys_xlsx/scripts/` |
| `sys_pdf` | `/skill/sys_pdf/scripts/` |
| `sys_web-artifacts-builder` | 用 `/usr/local/bin/web-artifacts-{init,bundle}` 封装脚本 |

SKILL.md 里的相对路径示例（如 `python scripts/office/unpack.py`）是 skill 作者按"skill 根为 CWD"写的 —— **不要照抄**。

## 5. 典型任务 playbook

### 5.1 生成 docx（用 python-docx）

```python
from docx import Document
from docx.shared import Pt, Mm
from docx.enum.text import WD_ALIGN_PARAGRAPH

doc = Document()
# A4 页面 + 政务公文页边距（GB/T 9704-2012）
sec = doc.sections[0]
sec.page_width, sec.page_height = Mm(210), Mm(297)
sec.top_margin, sec.bottom_margin = Mm(37), Mm(35)
sec.left_margin, sec.right_margin = Mm(28), Mm(26)

# 正文默认：仿宋_GB2312 三号（镜像已预装政务字体）
style = doc.styles['Normal']
style.font.name = '仿宋_GB2312'
style.font.size = Pt(16)
style.element.rPr.rFonts.set(
    '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}eastAsia',
    '仿宋_GB2312'
)

# 标题
p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = p.add_run('关于XXX的通知')
run.font.name = '方正小标宋简体'
run.font.size = Pt(22)
run._element.rPr.rFonts.set(
    '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}eastAsia',
    '方正小标宋简体'
)

doc.add_paragraph('　　正文首行缩进 2 字符...')
doc.save('/outputs/通知.docx')
```

**激活 docx skill** 获取更多：`use_skill('sys_docx')` — 含 XML 编辑、tracked changes、表格双宽度等高级技巧。

### 5.2 docx → PDF（跨轮次或同轮次）

同轮次 **首选** 一次搞定，省一次 container cold start：

```python
from docx import Document
# ...生成 doc...
doc.save('/outputs/report.docx')

import subprocess
subprocess.run(['soffice', '--headless', '--convert-to', 'pdf',
                '--outdir', '/outputs', '/outputs/report.docx'], check=True)
# 结果：/outputs/report.docx + /outputs/report.pdf 都被 harvest
```

跨轮次（用户明确要求"先看 docx，再生成 PDF"）：见 §2 示例。

### 5.3 生成 xlsx（pandas 或 openpyxl）

```python
# 简单数据 → pandas
import pandas as pd
df = pd.DataFrame({'name': ['A','B'], 'qty': [10, 20]})
df.to_excel('/outputs/list.xlsx', index=False)

# 需要格式 / 公式 → openpyxl（详见 use_skill('sys_xlsx')）
```

### 5.4 数据分析 / 图表生成

```python
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

df = pd.DataFrame({
    'month': ['1月', '2月', '3月', '4月'],
    'value': [120, 180, 150, 220],
})

summary = df.describe(include='all')
summary.to_markdown('/outputs/summary.md')

plt.figure(figsize=(8, 4.5), dpi=160)
sns.barplot(data=df, x='month', y='value', color='#377eb8')
plt.title('月度数据概览')
plt.tight_layout()
plt.savefig('/outputs/chart.png')
df.to_excel('/outputs/data.xlsx', index=False)
```

**选型规则**：

| 需求 | 首选 |
|---|---|
| 常规清洗、汇总、Excel 输出 | `pandas` |
| 大 CSV/Parquet、本地 SQL | `duckdb` + `pyarrow` |
| 大表列式处理、性能敏感 | `polars` |
| 稳定图片产物 | `matplotlib` / `seaborn` → PNG/SVG/PDF |
| 交互图表网页 | `plotly` / `pyecharts` → HTML |
| Vega-Lite 规范或轻量图 | `altair` + `vl-convert-python` |

`plotly.write_image()` 依赖 `kaleido` + `chromium`，比 matplotlib 慢；只是给用户预览交互图时优先输出 HTML。

### 5.5 生成 pptx

```python
from pptx import Presentation
from pptx.util import Inches, Pt

pres = Presentation()
slide = pres.slides.add_slide(pres.slide_layouts[1])
slide.shapes.title.text = "2025 年工作报告"
pres.save('/outputs/report.pptx')
```

`use_skill('sys_pptx')` 获取模板、图表、政务 PPT 字体表。

### 5.6 生成单文件 HTML artifact（React + Tailwind + shadcn）

```bash
cd /work
web-artifacts-init my-app       # 秒级复制预构建项目模板（无需 pnpm install）
cd my-app
# 编辑 src/App.tsx... （在 code 里用 Python open + write 即可）
web-artifacts-bundle            # parcel + html-inline → bundle.html
cp bundle.html /outputs/dashboard.html
```

`use_skill('sys_web-artifacts-builder')` 获取 React/shadcn 最佳实践。

### 5.7 PDF 文本 / 表格提取

```python
import pdfplumber
with pdfplumber.open('/inputs/00-doc.pdf') as pdf:
    for page in pdf.pages:
        print(page.extract_text())
        for t in page.extract_tables():
            print(t)
```

## 6. docx 生成：python-docx 首选 > docx-js

**问题**：镜像里 docx-js 的 `PageNumber.CURRENT` 页码字段在生成的 XML 里把 `<w:fldChar>` / `<w:instrText>` 直接挂到 `<w:p>` 下（不在 `<w:r>` 里），违反 OOXML schema：
- WPS / LibreOffice 宽容解析 → 能打开
- **MS Office 严格校验 → 拒绝打开**（"文件已损坏"）

**规则**：

| 场景 | 工具 |
|---|---|
| 一切正规文档（尤其涉及页码 / 页眉 / 页脚） | ✅ **python-docx** |
| 极简单无页码的小 docx（附件、测试用） | docx-js 可用 |
| 有中文字体 / 公文格式 | ✅ **python-docx**（配合 eastAsia 字段设中文字体） |

即使要写 JS，也优先考虑其它路径（如用 Node 调 pandoc 命令行）而不是 docx-js。

## 7. 常见错误 → 排查

| 症状 | 原因 | 排查 |
|---|---|---|
| `FileNotFoundError: /outputs/xxx` | 以为 /outputs 跨轮次持久化 | 见 §2，用 input_refs |
| `FileNotFoundError: scripts/office/xxx.py` | 用了相对路径调 skill 脚本 | 见 §4，改绝对路径 `/skill/sys_xxx/scripts/...` |
| MS Office 打不开 .docx，WPS 能开 | docx-js `PageNumber.CURRENT` bug | 见 §6，换 python-docx |
| `soffice` 卡住 / 无响应 | LibreOffice profile 冲突（多个实例） | 脚本本身没问题；首次调用会初始化 ~2s |
| `pnpm add xxx` 失败 | 沙箱无出网，pnpm 走 offline store | 用镜像已预装的；不要新增依赖 |
| `code_exec.invalid: code and code_ref are mutually exclusive` | 同一次调用同时传了脚本正文和脚本引用 | 删除其中一个；读旧产物只保留新 `code` + `input_refs` |
| `Cannot find module 'xxx'` | Node 依赖不在当前沙箱镜像，或镜像尚未重建 | 先看 §3；能用 Python 标准库就不要依赖 npm 包 |
| Python 包 `ModuleNotFoundError` | 不在镜像预装白名单 | 见 §3 清单；镜像外的包通过 skill bundle 或 workaround |
| 中文字体显示成方框（PDF） | 字体文件没加进容器 font cache | 镜像已装政务公文字体（方正小标宋/仿宋_GB2312/楷体_GB2312 等），用对 font family 名即可 |

## 8. 性能 / 限制

- **timeout**：默认 30s，可传 `timeout_seconds` 参数，硬上限 120s
- **内存**：512 MB（LobsterConfig.sandboxMemoryMb）
- **CPU**：0.5 核（sandboxCpus）
- **产物总量**：50 MB（单次 code_exec）
- **速率**：10 调用 / 分钟 / 用户（rateLimitPerMinute）
- **启动冷启动**：~500ms（docker create + start + tini + python 冷启动）

超时信号：tool_result.errorCategory = "timeout"，stdout/stderr 仍返回。

## 9. 调试模式

只想看中间结果而不生成文件？直接 `print()` 到 stdout，tool_result 里会回前 8000 字节。

```python
import sys
with open('/inputs/manifest.json') as f:
    print(f.read())  # 验证 input_refs 挂载情况
print('PATH:', __import__('os').environ.get('PATH'))
print('fonts:', __import__('subprocess').run(['fc-list',':lang=zh'], capture_output=True, text=True).stdout[:2000])
```

## 10. 不要做

- ❌ `pnpm install` / `pip install`（无出网，失败）
- ❌ 硬编码 `/outputs/xxx` 读上一轮产物（见 §2）
- ❌ 照抄 skill SKILL.md 里的相对路径（见 §4）
- ❌ 把敏感数据写到 stdout（会被前端看见 + 审计）
- ❌ 在 code 里 subprocess 拉外网资源（无出网，只会 hang 到 timeout）
- ❌ 用 docx-js 生成带页码的公文（MS Office 打不开）
