# Bisheng XLSX

> 制作或修改 Excel 表格（.xlsx）时使用：从零搭建数据表、明细表、汇总表、预算表、财务模型、 测算表、报价单、台账、看板底表；把整理好的数据导出成带公式和格式的工作簿；读取、清洗、 改写用户上传的 .xlsx/.xls/.csv。当用户说到「Excel」「表格」「工作簿」「xlsx」「报表」「台账」 「预算表」「测算」「明细表」「数据透视」「导出成表」，或提出「把这些数据做成 Excel」 「按这个模板填一张表」「帮我算一下并出个表」时触发。本技能给出 BiSheng 代码执行器里 唯一可行的 openpyxl 路径（该环境没有 markitdown，公式必须用 LibreOffice 重算才有值）， 以及中文表格规范、审计配色、公式禁用清单、交付前的重算与体检脚本。 如果用户要的是图表图片、Word 文档或 PPT，不要用本技能。

- Skill: `gabrielmoreira/bisheng-xlsx` (Agent Skill)
- Install (CLI): `npx skillmds@latest add gabrielmoreira/bisheng-xlsx`
- Raw SKILL.md: https://api.skillmd.com/api/skills/gabrielmoreira/bisheng-xlsx/raw
- Safety review: pending (external: skill-scanner WARNING, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Data & Analytics
- Author: gabrielmoreira (https://skillmd.com/u/gabrielmoreira)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/gabrielmoreira/bisheng-xlsx

---


# 在 BiSheng 里做 XLSX

## 0. 开工纪律

**这一轮只读文档，不要在同一轮里并行调用别的工具。** 读完本文件（必要时再读 references）之后，
下一轮才开始动手。把「读 SKILL.md」和「产出交付物」放进同一轮并行调用，等于技能没读。

**本技能要求已勾选代码执行器（`bisheng_code_interpreter`）。** 没有它就无法生成 .xlsx ——
直接告诉用户「请在工具里勾选代码执行器后重试」，不要用 Markdown 表格顶替。

## 1. 环境事实（照做，不要试探）

| 项 | 事实 |
|---|---|
| **生成方式** | **只有 `openpyxl`**（后端 `pyproject.toml` 的正式依赖），`import openpyxl` 直接可用 |
| **公式重算** | 靠 LibreOffice（`soffice`）。**openpyxl 写出的公式没有值**，不重算等于交了张空表 |
| 不存在的东西 | `markitdown`、Node/npm、`defusedxml`、`pdftoppm`、`zip`/`unzip` 命令 |
| `xlsxwriter` | 在，但只能新建、不能读改已有文件，也不能和 openpyxl 混用 → **一律用 openpyxl** |
| 其它可用库 | pandas/numpy、Pillow、PyMuPDF(`fitz`)、matplotlib、python-docx、python-pptx、lxml |
| 禁止 | `pip install`、`npm install`、任何联网假设（生产多为离线内网） |
| 工作目录 | 执行器 cwd = 工作区根，**一律用相对路径** |
| `output/` | 唯一交付区。每次运行自动创建 |
| `scratch/` | 中间产物区，**不会交付**，需自己 `os.makedirs`；**跨轮追问时会丢** |
| `uploads/` | 用户上传的原件在这里。跨轮追问时和 `output/` 一起被带进新会话 |
| `skills/bisheng-xlsx/` | 本技能包，脚本和参考资料在这里，只读 |
| 绝对禁止 | 写 `/output/xxx.xlsx` 这种带前导斜杠的路径 —— 文件被静默丢弃，用户拿不到 |
| 单次执行上限 | 本地执行器 600 秒；**E2B 沙箱 300 秒**。构建、重算、体检分多次调用 |
| 日志规则 | 二选一：`returncode != 0` **只回 stderr**（stdout 整段丢弃）；`returncode == 0` **只回 stdout**（stderr 整段丢弃）→ 一切诊断用 `print()`，且子进程的 stdout 和 stderr 都要各自打印一遍 |
| 可见性 | `exitcode 0` 时执行器把本轮新建/修改的文件同步进工作区，之后 `ls`/`read_file` 一般能看到。但**判断是否产出以 `exitcode 0` + 日志为准**，不要因为一次 `ls` 没看到就重做 |
| 轮次 | 最后两轮工具收窄到 `write_file`/`edit_file`/导出，代码执行器会被拦掉 → 表要尽早产出，不要拖到收尾 |
| **E2B 沙箱** | 沙箱模式下 `skills/` **结构性不可见**（copy-in 快照早于技能物化）→ 调包内脚本必 `FileNotFoundError`。一旦报这个错，就改走纯 openpyxl 内联写法：不调本包脚本，公式改成 Python 算好写数值，并在收尾里说明没做重算与体检 |

### 1.1 先探一次环境（第一次执行代码时顺手做，只花一轮）

```python
import shutil
try:
    import openpyxl
    print("openpyxl OK", openpyxl.__version__)
except ImportError:
    print("openpyxl MISSING")
print("soffice:", shutil.which("soffice") or shutil.which("libreoffice") or "无 —— 公式无法重算，见 §4 降级方案")
```

- `openpyxl MISSING`：正常部署不会出现。**不要 `pip install`**（共享的离线环境，装了会污染所有租户）。
  如实告诉用户环境缺依赖，需要运维补装。
- `soffice` 没有、或重算时报「没有 Calc 组件」：见 §4 的降级方案，**不要假装重算过了**。

## 2. 选路线

| 情况 | 做法 |
|---|---|
| 用户给了数据（或让你先查再整理），要一张新表 | §3 从零构建 |
| 用户上传了 .xlsx 要改内容 / 填数 | §5 改已有文件（**先读懂它的约定，再动手**） |
| 用户上传了 .csv/.xls 要清洗成规范表 | pandas 读进来清洗 → 按 §3 写出 |
| 用户要的是图表图片、Word、PPT | 不属于本技能 |

## 3. 从零构建

**第 1 步 · 定结构**。先想清楚分几个表、每个表的列。复杂测算把假设单独放一个表。
草稿写 `scratch/`，**不要写进 `output/`** —— 交付物按类型排序，`.md`/`.docx`/`.pdf`/`.html` 排在
`.xlsx` 前面，所以 `output/` 里只要有一个 `.md`，它就**永远**压过你的表成为用户看到的头条交付物。
不是「同一轮才会」，是只要在就会。

**第 2 步 · 写构建脚本**。用 `write_file` 把完整脚本写到 `scratch/build_sheet.py`，
**不要把整段代码塞进代码执行器的参数里** —— 参数过长会被截断，导致反复重试却总差一截。
写法读 `/skills/bisheng-xlsx/references/openpyxl-cookbook.md`（表格、样式、数字格式、公式、
图表、批注都有可直接抄的片段），规范读 `/skills/bisheng-xlsx/references/design-zh.md`。

**第 3 步 · 执行**：

```python
import subprocess, sys
r = subprocess.run([sys.executable, "scratch/build_sheet.py"], capture_output=True, text=True)
print(r.stdout or "(no stdout)")
print(r.stderr[-2000:] if r.stderr else "(no stderr)")
```

> 为什么不直接写 `python scratch/build_sheet.py`：PATH 里的 `python` 未必是后端那个解释器，
> 用 `sys.executable` 才能保证跑在装了 openpyxl 的环境里。**下面所有脚本调用都用这个写法。**

**第 4 步 · 重算（有公式就必做）**，见 §4。
**第 5 步 · 体检并返修**，见 §6。返修用 `edit_file` 定点改 `scratch/build_sheet.py` 再重跑，
不要每次重写整份脚本。

## 4. 重算公式（有公式就必做）

openpyxl 写出的公式**只是字符串，没有结果**。不重算的话，pandas、`data_only=True`、
以及多数预览器读到的全是空 —— 用户打开看到的是一张有公式没数字的表。

```python
import subprocess, sys
r = subprocess.run([sys.executable, "skills/bisheng-xlsx/scripts/recalc_check.py", "output/xxx.xlsx"],
                   capture_output=True, text=True)
print(r.stdout or "(no stdout)")
print(r.stderr[-2000:] if r.stderr else "(no stderr)")
```

LibreOffice 会算完所有公式、**就地重写文件**，然后脚本回读并列出所有错误单元格。
一次重算连冷启动约 10–30 秒；超时就加 `--timeout 120`。
看结论：

- `重算完成` —— 公式都能算出结果。**这不是交付条件**，还要过 §6 的体检。
- `重算未通过` + 错误单元格清单 —— 逐个改完再重算。
- `[未重算]` —— 环境问题（没有 soffice、或只装了 writer 没装 calc、或超时）。**这时不要硬撑**：
  改成在 Python 里把数算好直接写数值，并在表里用一列文字说明计算口径，
  同时如实告诉用户「本环境无法重算公式，已改为写入计算结果，修改输入不会自动重算」。

> **能算 ≠ 算对。** 区间差一行、引用错行，照样是一张干净的错数字表。
> 铺开整张表之前，先写 2–3 个关键公式，重算一次，肉眼核对结果符不符合预期。

## 5. 改用户上传的表

```python
import subprocess, sys
r = subprocess.run([sys.executable, "skills/bisheng-xlsx/scripts/inspect_workbook.py",
                    "uploads/原表.xlsx", "--content-only"], capture_output=True, text=True)
print(r.stdout or "(no stdout)")
print(r.stderr[-2000:] if r.stderr else "(no stderr)")
```

它按单元格坐标打印内容和公式（这是 `markitdown` 在本环境的替代品，而且比它多给坐标，
所以可以据此规划改哪一格）。然后：

- **原表的约定压倒本技能的一切规范** —— 它用什么字体、什么数字格式、什么配色，就跟着它。
- **先找到它的输入格**（通常有独特的字色或填充），只在那里写值，**不要动任何已有公式**。
- 用默认方式 `load_workbook(path)` 打开（**不要加 `data_only=True`**，那样保存会把公式全变成字面量）。
- `.xlsm` 要 `keep_vba=True`，否则宏全丢。
- 改完另存到 `output/`，不要覆盖 `uploads/` 里的原件。
- **不要给别人的表加示例行。**

## 6. 交付前体检（必做）

```python
import subprocess, sys
r = subprocess.run([sys.executable, "skills/bisheng-xlsx/scripts/inspect_workbook.py", "output/xxx.xlsx"],
                   capture_output=True, text=True)
print(r.stdout or "(no stdout)")
print(r.stderr[-2000:] if r.stderr else "(no stderr)")
```

输出分两段：

- **内容**：逐表逐格打印，公式旁边跟着重算后的值。用它核对数字对不对、有没有缺漏。
- **体检**：ERROR 必须修完再交付；WARN 逐条复核；INFO 是建议。覆盖：禁用函数（含 `_xlfn.` 前缀伪装）、
  `_xlfn.` 缺前缀、跨表引用引号、百分比存成文本、百分比存整数、数字带单位存成文本、
  公式无缓存值、重算后仍是错误值、缺表头、列宽不足导致 `###`、合并区丢值、年份千分位、
  残留占位符、硬编码系数、除法未保护。
  同一条规则在同一列命中多次时只列 3 例 + 一句「同类还有 N 处」，按同样办法一次改完。

改完重新生成 → 重算 → 再体检，直到最后一行是 `结论: 通过`。
只有 ERROR 挡交付；`结论: 通过` 之后仍要把 WARN 逐条看一遍，确实无碍再交付。

## 7. 公式红线（体检会拦，但你一开始就别写）

- ❌ **`XLOOKUP` / `XMATCH` / `SORT` / `FILTER` / `UNIQUE` / `SEQUENCE` / `LET` / `LAMBDA`**
  —— openpyxl 写不出溢出元数据，LibreOffice 要么报 `#NAME?`，要么只填左上角一个值
  **而且重算报 0 错误**。查表用 `INDEX`/`MATCH`；排序、去重、筛选在 Python 里做完再写值。
  **加 `_xlfn.` 前缀救不了这一组**：`=_xlfn.SORT(...)` 实测照样 `#NAME?`，体检也照样拦。
- ⚠️ **`TEXTJOIN` / `CONCAT` / `IFS` / `SWITCH` / `MAXIFS` / `MINIFS` 必须带 `_xlfn.` 前缀**，
  裸写 → `#NAME?`。
- ⚠️ 表名含空格的跨表引用必须加单引号：`='假设 输入'!$B$5`。用 `sheet_ref()` 自动处理。
- ⚠️ 百分比存小数（0.15 = 15%）。用 `set_pct()`，它会在你传 15 时直接报错。

## 8. 交付纪律

- `output/` 里**只放最终的 `.xlsx`**。草稿、构建脚本、中间版本一律放 `scratch/`。
- 文件名用有意义的中文名，如 `output/2024年度经营分析.xlsx`。
- 收尾时如实说明做了什么、几个表、多少行、公式是否已重算。
  **不要声称生成了实际不存在的文件。**
- 如果因为环境限制改成了写死数值，必须在收尾里明确说出来。

## 9. 绝不要做的事

- ❌ 跑 `markitdown` 读表 —— 环境里没有，用 §5 的 `--content-only`。
- ❌ `pip install` 任何东西。
- ❌ 用绝对路径 `/output/...`。
- ❌ 有公式却不重算就交付。
- ❌ 把表降级成 Markdown 表格或 CSV 交付。用户要的是 .xlsx。
- ❌ `load_workbook(..., data_only=True)` 之后 `save()` —— 会永久毁掉所有公式。
- ❌ 因为一次 `ls` 没看到刚生成的文件就重做一遍 —— `exitcode 0` + 日志就是产出的凭据。
- ❌ 把百分比、金额写成 `"15%"`、`"1,250,000"` 这种字符串 —— 不参与求和，体检会拦。

