---
name: feishu-embedded-sheet
version: 1.0.0
description: |
  飞书云文档中内嵌电子表格（Sheet）的读取与写入技能。
  当飞书 docx 文档中包含 `<sheet token="xxx"/>` 内嵌表格时，使用本技能进行精准的行列级读写操作。
  
  **触发场景：**
  - 需要读取飞书文档中内嵌的 Excel/Sheet 表格数据
  - 需要向飞书文档中内嵌的 Sheet 写入/更新数据
  - 用户提到"文档里的表格"、"内嵌表格"、"剧本表格"等
  - 从文档 markdown 中看到 `<sheet token="xxx_yyy"/>` 标签
metadata:
  requires:
    permissions: ["sheets:spreadsheet:readonly", "sheets:spreadsheet"]
  identity: bot
---

# 飞书文档内嵌 Sheet 读写技能

## 核心概念

飞书 docx 文档可以内嵌电子表格（Sheet）。通过 `lark-cli docs +fetch` 获取文档 markdown 时，内嵌表格会渲染为：

```
<sheet token="WreDs73X1hrgdFtxdpJcItj2n5c_RuQopk"/>
```

**Token 结构解析：**
- 完整 token：`WreDs73X1hrgdFtxdpJcItj2n5c_RuQopk`
- `_` 前面部分 = `spreadsheet_token`：`WreDs73X1hrgdFtxdpJcItj2n5c`
- `_` 后面部分 = `sheet_id`：`RuQopk`

同一个文档内嵌的多个 Sheet 共享同一个 `spreadsheet_token`，通过不同的 `sheet_id` 区分。

## 前置步骤：获取 Bot Token

所有操作均使用 **Bot 身份**执行，禁止触发用户授权。

```bash
APP_ID="cli_a931175d41799cc7"
APP_SECRET=$(jq -r '.apps[0].appSecret' /root/.openclaw/credentials/xiaoyan/config.json)

TOKEN=$(curl -s -X POST "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal" \
  -H "Content-Type: application/json" \
  -d "{\"app_id\":\"$APP_ID\",\"app_secret\":\"$APP_SECRET\"}" \
  | jq -r '.tenant_access_token')
```

Token 有效期 2 小时，过期需重新获取。

## 完整执行链路

### 第一步：从文档中提取 Sheet Token

1. 先通过 `lark_wiki_operate_as_bot` 技能获取文档内容
2. 在返回的 markdown 中查找 `<sheet token="xxx"/>` 标签
3. 解析出 `spreadsheet_token` 和 `sheet_id`

```bash
# 获取文档内容（以 wiki 文档为例）
LARKSUITE_CLI_CONFIG_DIR=/root/.openclaw/credentials/xiaoyan \
  lark-cli wiki spaces get_node --params '{"token":"<wiki_token>"}' --as bot

LARKSUITE_CLI_CONFIG_DIR=/root/.openclaw/credentials/xiaoyan \
  lark-cli docs +fetch --doc <obj_token> --as bot
```

### 第二步：获取 Sheet 元数据（了解表结构）

```bash
curl -s -X GET \
  "https://open.feishu.cn/open-apis/sheets/v3/spreadsheets/${SPREADSHEET_TOKEN}/sheets/query" \
  -H "Authorization: Bearer $TOKEN" | jq '.'
```

返回信息包括：
- `sheet_id`：每个子表的 ID
- `title`：子表名称
- `grid_properties.row_count`：总行数
- `grid_properties.column_count`：总列数
- `grid_properties.frozen_row_count`：冻结行数（通常表头行）
- `merges`：合并单元格信息

### 第三步：读取数据

#### 读取指定范围

```bash
# 格式：{sheet_id}!{起始单元格}:{结束单元格}
curl -s -X GET \
  "https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/${SPREADSHEET_TOKEN}/values/${SHEET_ID}!A1:J196?valueRenderOption=ToString" \
  -H "Authorization: Bearer $TOKEN" | jq '.data.valueRange.values'
```

**参数说明：**
- `valueRenderOption=ToString`：所有值转为字符串返回（推荐，避免类型混乱）
- Range 格式：`{sheet_id}!{列字母}{行号}:{列字母}{行号}`
- 行号从 1 开始，列用字母表示（A=1, B=2, ..., Z=26, AA=27...）

#### 读取全表

先从元数据获取 `row_count` 和 `column_count`，然后构造完整 range：

```bash
# 例：10列196行
curl -s -X GET \
  "https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/${SPREADSHEET_TOKEN}/values/${SHEET_ID}!A1:J196?valueRenderOption=ToString" \
  -H "Authorization: Bearer $TOKEN"
```

#### 读取结果处理

返回的 `values` 是二维数组，每个元素是一行：
```json
{
  "data": {
    "valueRange": {
      "values": [
        ["类型", "组件配置", "ID", ...],   // 第1行（表头）
        ["TL\nxxx", "S1前置xxx", null, ...], // 第2行
        ...
      ]
    }
  }
}
```

- `null` 表示空单元格
- 换行符在值中以 `\n` 表示
- 合并单元格：只有左上角单元格有值，其余为 `null`

### 第四步：写入数据

#### 写入/更新指定行

```bash
curl -s -X PUT \
  "https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/${SPREADSHEET_TOKEN}/values" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "valueRange": {
      "range": "'${SHEET_ID}'!A197:J197",
      "values": [
        ["对话测试", "S99主线\n自动\n对话测试", "9999901", "剧情描述", "User", "台词", "User", "润色台词", null, "知识点"]
      ]
    }
  }'
```

#### 批量写入多行

```bash
curl -s -X PUT \
  "https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/${SPREADSHEET_TOKEN}/values" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "valueRange": {
      "range": "'${SHEET_ID}'!A197:J199",
      "values": [
        ["行1列A", "行1列B", ...],
        ["行2列A", "行2列B", ...],
        ["行3列A", "行3列B", ...]
      ]
    }
  }'
```

#### 更新指定单元格

```bash
# 只更新单个单元格
curl -s -X PUT \
  "https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/${SPREADSHEET_TOKEN}/values" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "valueRange": {
      "range": "'${SHEET_ID}'!H50:H50",
      "values": [["更新后的台词润色内容"]]
    }
  }'
```

#### 清空单元格

写入空字符串 `""` 即可清空单元格内容。

### 第五步：写入后验证

**必须回读验证**，确保写入内容与预期一致：

```bash
curl -s -X GET \
  "https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/${SPREADSHEET_TOKEN}/values/${SHEET_ID}!A197:J197?valueRenderOption=ToString" \
  -H "Authorization: Bearer $TOKEN" | jq '.data.valueRange.values'
```

## 注意事项

1. **身份限制**：所有操作使用 Bot 身份，需要 Bot 应用对目标文档有编辑权限
2. **权限要求**：Bot 需在飞书开发者后台开通 `sheets:spreadsheet` 权限；目标文档/知识空间需将 Bot 添加为成员并授予编辑权限
3. **合并单元格**：写入合并单元格区域时，只写入左上角单元格即可，其余位置保持 null
4. **行数限制**：单次读取/写入建议不超过 500 行，超大表格请分批操作
5. **并发写入**：避免同时多次写入同一个 Sheet，可能导致数据覆盖
6. **数据类型**：写入时，数字直接用 number 类型，文本用 string，空值用 null
7. **换行符**：需要在单元格内换行时，在字符串中使用 `\n`

### ⚠️ 批量写入前必须验证行号连续性（强制执行）

**问题：** 飞书 Sheets API 的 range 写入是按连续区域覆盖的。如果 range 指定 `F4:F67`，API 会把数据依次写入 Row 4, 5, 6, ..., 67，**不管这些行是否对应正确的数据行**。

**场景：** 剧本表格中，E 列有内容的行号不连续（Row 4 有台词、Row 5 是剧情描述空行、Row 6 又有台词），如果按连续 range 批量写入，会导致 Row 6 的英文台词写到 Row 5 上，造成全列偏移。

**强制规则：**
1. 写入前，先统计目标行号列表，检查是否连续（`rows[i+1] - rows[i] == 1`）
2. **行号连续** → 可以用 range 批量写入（如 `F4:F67`）
3. **行号不连续** → 必须逐行写入（每个行号单独一个 range，如 `F4:F4`, `F6:F6`, ...），或先清空整列再逐行写入
4. 写入后必须抽样验证 E/F 列行号对齐（至少抽查前 5 行、中间 5 行、末尾 5 行）

```python
# 示例：检查行号连续性
rows = sorted(data.keys())
is_continuous = all(rows[i+1] - rows[i] == 1 for i in range(len(rows)-1))
if is_continuous:
    # 安全：批量 range 写入
    write_range(f"F{rows[0]}:F{rows[-1]}", values)
else:
    # 不安全：逐行写入
    for row in rows:
        write_range(f"F{row}:F{row}", [data[row]])
```

## 异常处理

| 错误码 | 含义 | 处理方式 |
|--------|------|---------|
| `1310251` | spreadsheet_token 格式错误 | 检查是否正确从 sheet token 中拆分出 spreadsheet_token |
| `1310201` | 无权限 | Bot 未被添加为文档/知识空间成员，需用户授权 |
| `1310212` | Range 格式错误 | 检查 sheet_id 和单元格范围格式 |
| `1310214` | 超出表格范围 | 检查行列号是否超出实际表格大小 |

## 与其他技能的配合

- **读取文档内容**：先用 `lark_wiki_operate_as_bot` 技能获取文档 markdown，从中提取 `<sheet>` 标签
- **多维表格操作**：如果目标是独立的多维表格（Bitable），应使用 `lark_bitable_operate_as_bot` 技能，而非本技能
- **文档写入**：如果需要修改文档中 Sheet 以外的内容（文本、表格等），使用 `feishu-update-doc` 技能
