Skill: siyiding/feishu-sheet (version 1.2.0)

--- name: feishu-sheet description: | 飞书电子表格（Sheets）完整操作。当需要创建、读取或编辑飞书电子表格时激活此 skill。 支持：创建表格、读写单元格、追加数据、图片插入、样式设置、合并单元格、行列操作、查找替换。 需要飞书应用凭证：channels.feishu.appId 和 channels.feishu.appSecret（配置在 ~/.openclaw/openclaw.json 中）。 飞书应用需开通 sheets:spreadsheet 权限。建议使用仅含表格权限的最小化飞书应用。 metadata: version: 1.2.0 author: siyiding homepage: "https://clawhub.ai/skills/feishu-sheet" tags: - feishu - spreadsheet - sheets requires: - curl - python3 - bash credentials: - name: channels.feishu.appId description: "Feishu app ID (from Feishu Open Platform). Configured in ~/.openclaw/openclaw.json at channels.feishu.appId" required: true - name: channels.feishu.appSecret description: "Feishu app secret (from Feishu Open Platform). Configured in ~/.openclaw/openclaw.json at channels.feishu.appSecret" required: true security: configFile: "~/.openclaw/openclaw.json (override with OPENCLAW_CONFIG env var)" credentialPath: "channels.feishu" networkAccess: "open.feishu.cn (Feishu Open API only)" fileAccess: "Reads local image files only when explicitly passed to insert_image/float_image commands" tokenCache: "Tenant access token cached in $TMPDIR with per-user isolation (uid suffix)" note: "Credentials are validated against ^[A-Za-z0-9_-]+$ before use. Python inline script uses single-quoted strings to prevent shell injection. Recommend using a minimal-permission Feishu app." --- # 飞书电子表格 Skill 通过 `exec` 调用 `~/.openclaw/skills/feishu-sheet/scripts/feishu-sheet.sh` 脚本操作飞书电子表格。 ## 前提条件 1. **飞书应用凭证**：`~/.openclaw/openclaw.json` 中必须配置 `channels.feishu`，包含 `appId` 和 `appSecret` 2. **飞书应用权限**：应用需要开通「电子表格」相关权限（`sheets:spreadsheet`） 3. **系统依赖**：`curl`、`python3`、`bash` 凭证自动从 `openclaw.json` 读取，无需手动配置。可通过 `OPENCLAW_CONFIG` 环境变量指定配置文件路径。 ## Token 提取 从 URL `https://feishu.cn/sheets/shtcnABC123` → `spreadsheet_token` = `shtcnABC123` --- ## 📊 表格操作 ### 创建电子表格 ```bash exec command="~/.openclaw/skills/feishu-sheet/scripts/feishu-sheet.sh create '表格标题'" ``` 返回 `spreadsheet_token` 和 URL。可选第二参数 `folder_token`。 ### 获取元数据（必须先调这个拿 sheet_id） ```bash exec command="~/.openclaw/skills/feishu-sheet/scripts/feishu-sheet.sh meta shtcnABC123" ``` --- ## 📖 数据读写 ### 读取数据 ```bash exec command="~/.openclaw/skills/feishu-sheet/scripts/feishu-sheet.sh read TOKEN 'sheetId!A1:C10'" ``` ### 读取多个范围 ```bash exec command="~/.openclaw/skills/feishu-sheet/scripts/feishu-sheet.sh read_multi TOKEN 'sheetId!A1:C10' 'sheetId!E1:F5'" ``` ### 写入数据（覆盖指定范围） ```bash exec command="~/.openclaw/skills/feishu-sheet/scripts/feishu-sheet.sh write TOKEN 'sheetId!A1:C2' '[[\"表头1\",\"表头2\",\"表头3\"],[\"数据1\",100,true]]'" ``` values 是 JSON 二维数组。字符串用引号，数字和布尔值不用。 ### 追加数据（在已有数据后面添加行） ```bash exec command="~/.openclaw/skills/feishu-sheet/scripts/feishu-sheet.sh append TOKEN 'sheetId!A1:C1' '[[\"新行1\",\"新行2\",\"新行3\"]]'" ``` ### 前插数据 ```bash exec command="~/.openclaw/skills/feishu-sheet/scripts/feishu-sheet.sh prepend TOKEN 'sheetId!A1:C1' '[[\"插入行1\",\"插入行2\",\"插入行3\"]]'" ``` --- ## 🖼️ 图片操作 ### 插入图片到单元格（本地文件） ```bash exec command="~/.openclaw/skills/feishu-sheet/scripts/feishu-sheet.sh insert_image TOKEN 'sheetId!E1:E1' /path/to/image.png" ``` 图片会填充到指定单元格内。Range 必须是单个单元格。 ### 插入浮动图片（本地文件） ```bash exec command="~/.openclaw/skills/feishu-sheet/scripts/feishu-sheet.sh float_image TOKEN sheetId /path/to/image.png 'sheetId!F1:F1' 400 300" ``` 参数：token, sheet_id, 文件路径, 锚点单元格(可选), 宽度(可选), 高度(可选) ### 插入浮动图片（URL） ```bash exec command="~/.openclaw/skills/feishu-sheet/scripts/feishu-sheet.sh float_image_url TOKEN sheetId 'https://example.com/image.png' 'sheetId!F1:F1' 400 300" ``` --- ## 🎨 样式操作 ### 设置单元格样式 ```bash exec command="~/.openclaw/skills/feishu-sheet/scripts/feishu-sheet.sh style TOKEN 'sheetId!A1:D1' '{\"bold\":true,\"foreColor\":\"#FFFFFF\",\"backColor\":\"#4472C4\",\"fontSize\":14}'" ``` **支持的样式属性：** - `bold` (bool) — 加粗 - `italic` (bool) — 斜体 - `foreColor` (string) — 字体颜色，如 `#FF0000` - `backColor` (string) — 背景颜色，如 `#FFFF00` - `fontSize` (int) — 字号，如 `14` - `horizontalAlign` (int) — 水平对齐：0=左, 1=中, 2=右 - `verticalAlign` (int) — 垂直对齐：0=上, 1=中, 2=下 - `textDecoration` (int) — 0=无, 1=下划线, 2=删除线, 3=两者都有 ### 批量设置样式 ```bash exec command="~/.openclaw/skills/feishu-sheet/scripts/feishu-sheet.sh style_batch TOKEN '{\"data\":[{\"ranges\":\"sheetId!A1:D1\",\"style\":{\"bold\":true}},{\"ranges\":\"sheetId!A2:D10\",\"style\":{\"fontSize\":12}}]}'" ``` --- ## 🔗 合并单元格 ### 合并 ```bash exec command="~/.openclaw/skills/feishu-sheet/scripts/feishu-sheet.sh merge TOKEN 'sheetId!A1:D1' MERGE_ALL" ``` 合并类型：`MERGE_ALL`（全部合并）、`MERGE_ROWS`（按行合并）、`MERGE_COLUMNS`（按列合并） ### 拆分 ```bash exec command="~/.openclaw/skills/feishu-sheet/scripts/feishu-sheet.sh unmerge TOKEN 'sheetId!A1:D1'" ``` --- ## 📄 工作表操作 ```bash # 添加工作表 exec command="~/.openclaw/skills/feishu-sheet/scripts/feishu-sheet.sh add_sheet TOKEN '工作表名称'" # 删除工作表 exec command="~/.openclaw/skills/feishu-sheet/scripts/feishu-sheet.sh delete_sheet TOKEN sheetId" # 复制工作表 exec command="~/.openclaw/skills/feishu-sheet/scripts/feishu-sheet.sh copy_sheet TOKEN sourceSheetId '副本名称'" ``` --- ## ↕️ 行列操作 ```bash # 末尾加 10 行 exec command="~/.openclaw/skills/feishu-sheet/scripts/feishu-sheet.sh add_rows TOKEN sheetId 10" # 末尾加 5 列 exec command="~/.openclaw/skills/feishu-sheet/scripts/feishu-sheet.sh add_cols TOKEN sheetId 5" # 在第3行前插入到第5行（0-indexed） exec command="~/.openclaw/skills/feishu-sheet/scripts/feishu-sheet.sh insert_rows TOKEN sheetId 3 5" # 删除第3到第5行 exec command="~/.openclaw/skills/feishu-sheet/scripts/feishu-sheet.sh delete_rows TOKEN sheetId 3 5" # 删除第2到第4列 exec command="~/.openclaw/skills/feishu-sheet/scripts/feishu-sheet.sh delete_cols TOKEN sheetId 2 4" ``` --- ## 🔍 查找替换 ```bash # 查找 exec command="~/.openclaw/skills/feishu-sheet/scripts/feishu-sheet.sh find TOKEN sheetId '关键词'" # 替换 exec command="~/.openclaw/skills/feishu-sheet/scripts/feishu-sheet.sh replace TOKEN sheetId '旧文本' '新文本'" ``` --- ## 常见流程示例 ### 创建带格式的报表 ```bash # 1. 创建表格 exec command="~/.openclaw/skills/feishu-sheet/scripts/feishu-sheet.sh create '月度报表'" # → 得到 spreadsheet_token # 2. 获取 sheet_id exec command="~/.openclaw/skills/feishu-sheet/scripts/feishu-sheet.sh meta TOKEN" # → 得到 sheet_id # 3. 写入表头 exec command="~/.openclaw/skills/feishu-sheet/scripts/feishu-sheet.sh write TOKEN 'sheetId!A1:D1' '[[\"项目\",\"负责人\",\"进度\",\"备注\"]]'" # 4. 设置表头样式（加粗+蓝底白字） exec command="~/.openclaw/skills/feishu-sheet/scripts/feishu-sheet.sh style TOKEN 'sheetId!A1:D1' '{\"bold\":true,\"backColor\":\"#4472C4\",\"foreColor\":\"#FFFFFF\",\"fontSize\":13}'" # 5. 写入数据 exec command="~/.openclaw/skills/feishu-sheet/scripts/feishu-sheet.sh write TOKEN 'sheetId!A2:D4' '[[\"功能A\",\"张三\",\"80%\",\"进行中\"],[\"功能B\",\"李四\",\"100%\",\"已完成\"],[\"功能C\",\"王五\",\"30%\",\"延期\"]]'" # 6. 插入图片 exec command="~/.openclaw/skills/feishu-sheet/scripts/feishu-sheet.sh float_image_url TOKEN sheetId 'https://example.com/chart.png' 'sheetId!F1:F1' 500 300" ``` ## 注意事项 - **Range 格式**：必须带 `sheetId` 前缀，用 `meta` 命令获取 - **单元格图片**：`insert_image` 的 range 必须是单个单元格（如 `A1:A1`） - **浮动图片**：支持本地文件和 URL，会自动上传到飞书 - **values 格式**：JSON 二维数组，注意 shell 中引号转义 - **权限**：表格需要对机器人开放编辑权限，或由机器人创建 - **凭证**：自动从 `~/.openclaw/openclaw.json` 的 `channels.feishu` 读取，无需手动配置

path: scripts/feishu-sheet.sh
content: #!/bin/bash
# feishu-sheet.sh — Feishu Spreadsheet (Sheets) API wrapper for OpenClaw
# Version: 1.2.0
# Usage: feishu-sheet.sh <action> [args...]
#
# Required credentials (auto-read from config):
#   channels.feishu.appId     - Feishu app ID
#   channels.feishu.appSecret - Feishu app secret
# Config location: ~/.openclaw/openclaw.json (override with OPENCLAW_CONFIG)
#
# Feishu app permissions needed: sheets:spreadsheet (spreadsheet read/write)
# Recommend using a minimal-permission Feishu app for this skill.

set -euo pipefail

FEISHU_BASE="https://open.feishu.cn/open-apis"
TOKEN_CACHE="${TMPDIR:-/tmp}/.feishu_tenant_token_$(id -u)"
TOKEN_EXPIRE="${TMPDIR:-/tmp}/.feishu_tenant_token_expire_$(id -u)"

resolve_credentials() {
  local config_path="${OPENCLAW_CONFIG:-$HOME/.openclaw/openclaw.json}"
  if [[ ! -f "$config_path" ]]; then
    echo '{"error":"openclaw.json not found. Set OPENCLAW_CONFIG or ensure ~/.openclaw/openclaw.json exists."}' >&2
    exit 1
  fi

  local creds rc
  creds=$(python3 -c '
import json, sys, re
try:
    with open(sys.argv[1]) as f:
        c = json.load(f)
    feishu = c.get("channels", {}).get("feishu", {})
    app_id = str(feishu.get("appId", ""))
    app_secret = str(feishu.get("appSecret", ""))
    if not app_id or not app_secret:
        print("feishu appId/appSecret not found in openclaw.json channels.feishu", file=sys.stderr)
        sys.exit(1)
    pat = re.compile(r"^[A-Za-z0-9_\-]+$")
    if not pat.match(app_id) or not pat.match(app_secret):
        print("appId/appSecret contain unexpected characters", file=sys.stderr)
        sys.exit(1)
    print(app_id)
    print(app_secret)
except Exception as e:
    print(str(e), file=sys.stderr)
    sys.exit(1)
' "$config_path" 2>/dev/null)
  rc=$?

  if [[ $rc -ne 0 || -z "$creds" ]]; then
    echo '{"error":"Failed to read feishu credentials from openclaw.json. Ensure channels.feishu.appId and channels.feishu.appSecret are configured."}' >&2
    exit 1
  fi

  FEISHU_APP_ID=$(echo "$creds" | head -1)
  FEISHU_APP_SECRET=$(echo "$creds" | tail -1)

  if [[ -z "$FEISHU_APP_ID" || -z "$FEISHU_APP_SECRET" ]]; then
    echo '{"error":"Failed to read feishu credentials from openclaw.json"}' >&2
    exit 1
  fi
}

resolve_credentials

get_tenant_token() {
  local now
  now=$(date +%s)
  if [[ -f "$TOKEN_CACHE" && -f "$TOKEN_EXPIRE" ]]; then
    local expire
    expire=$(cat "$TOKEN_EXPIRE")
    if (( now < expire )); then
      cat "$TOKEN_CACHE"
      return
    fi
  fi
  local resp
  resp=$(curl -s -X POST "$FEISHU_BASE/auth/v3/tenant_access_token/internal" \
    -H "Content-Type: application/json" \
    -d "{\"app_id\":\"$FEISHU_APP_ID\",\"app_secret\":\"$FEISHU_APP_SECRET\"}")
  local token
  [REDACTED_SECRET] "$resp" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tenant_access_token',''))" 2>/dev/null)
  local expire_in
  expire_in=$(echo "$resp" | python3 -c "import sys,json; print(json.load(sys.stdin).get('expire',7200))" 2>/dev/null)
  if [[ -z "$token" ]]; then
    echo "{\"error\":\"Failed to get tenant_access_token\"}" >&2
    exit 1
  fi
  echo "$token" > "$TOKEN_CACHE"
  echo $(( now + expire_in - 300 )) > "$TOKEN_EXPIRE"
  echo "$token"
}

api_call() {
  local method="$1" path="$2"
  shift 2
  local token
  [REDACTED_SECRET])
  curl -s -X "$method" "$FEISHU_BASE$path" \
    -H "Authorization: Bearer $token" \
    -H "Content-Type: application/json" \
    "$@"
}

api_upload() {
  local path="$1"
  shift
  local token
  [REDACTED_SECRET])
  curl -s -X POST "$FEISHU_BASE$path" \
    -H "Authorization: Bearer $token" \
    "$@"
}

pj() { python3 -m json.tool 2>/dev/null || cat; }

# ========================= Spreadsheet =========================

action_create() {
  local title="${1:-新建电子表格}" folder="${2:-}"
  local body="{\"title\":\"$title\""
  [[ -n "$folder" ]] && body+=",\"folder_token\":\"$folder\""
  body+="}"
  api_call POST "/sheets/v3/spreadsheets" -d "$body" | python3 -c "
import sys,json
d = json.load(sys.stdin)
ss = d.get('data',{}).get('spreadsheet',{})
if ss:
    t = ss.get('spreadsheet_token','')
    print(json.dumps({'success':True,'spreadsheet_token':t,'title':ss.get('title',''),'url':ss.get('url',f'https://feishu.cn/sheets/{t}')}, ensure_ascii=False, indent=2))
else:
    print(json.dumps({'error':d.get('msg','unknown'),'code':d.get('code',0)}, ensure_ascii=False, indent=2))
"
}

action_meta() {
  api_call GET "/sheets/v2/spreadsheets/$1/metainfo" | python3 -c "
import sys,json
d = json.load(sys.stdin)
props = d.get('data',{}).get('properties',{})
sheets = d.get('data',{}).get('sheets',[])
print(json.dumps({
    'title': props.get('title',''),
    'sheet_count': len(sheets),
    'sheets': [{'sheet_id':s.get('sheetId',''),'title':s.get('title',''),'rows':s.get('rowCount',0),'cols':s.get('columnCount',0)} for s in sheets]
}, ensure_ascii=False, indent=2))
"
}

# ========================= Data R/W =========================

action_read() {
  api_call GET "/sheets/v2/spreadsheets/$1/values/$2?valueRenderOption=ToString" | python3 -c "
import sys,json
d = json.load(sys.stdin)
vr = d.get('data',{}).get('valueRange',{})
print(json.dumps({'range':vr.get('range',''),'rows':len(vr.get('values',[])),'values':vr.get('values',[])}, ensure_ascii=False, indent=2))
"
}

action_read_multi() {
  local token="$1"
  shift
  local ranges_param=""
  for r in "$@"; do
    [[ -n "$ranges_param" ]] && ranges_param+="&"
    ranges_param+="ranges=$r"
  done
  api_call GET "/sheets/v2/spreadsheets/$token/values_batch_get?$ranges_param&valueRenderOption=ToString" | python3 -c "
import sys,json
d = json.load(sys.stdin)
vrs = d.get('data',{}).get('valueRanges',[])
result = []
for vr in vrs:
    result.append({'range':vr.get('range',''),'rows':len(vr.get('values',[])),'values':vr.get('values',[])})
print(json.dumps(result, ensure_ascii=False, indent=2))
"
}

action_write() {
  local body="{\"valueRange\":{\"range\":\"$2\",\"values\":$3}}"
  api_call PUT "/sheets/v2/spreadsheets/$1/values" -d "$body" | python3 -c "
import sys,json
d = json.load(sys.stdin)
da = d.get('data',{})
print(json.dumps({'success':d.get('code',1)==0,'updated_range':da.get('updatedRange',''),'updated_cells':da.get('updatedCells',0),'updated_rows':da.get('updatedRows',0),'updated_columns':da.get('updatedColumns',0)}, ensure_ascii=False, indent=2))
"
}

action_write_multi() {
  local token="$1" data="$2"
  api_call POST "/sheets/v2/spreadsheets/$token/values_batch_update" -d "$data" | python3 -c "
import sys,json
d = json.load(sys.stdin)
print(json.dumps({'success':d.get('code',1)==0,'responses':d.get('data',{}).get('responses',[])}, ensure_ascii=False, indent=2))
"
}

action_append() {
  local body="{\"valueRange\":{\"range\":\"$2\",\"values\":$3}}"
  api_call POST "/sheets/v2/spreadsheets/$1/values_append" -d "$body" | python3 -c "
import sys,json
d = json.load(sys.stdin)
da = d.get('data',{})
print(json.dumps({'success':d.get('code',1)==0,'updated_range':da.get('updatedRange',''),'updated_cells':da.get('updatedCells',0)}, ensure_ascii=False, indent=2))
"
}

action_prepend() {
  local body="{\"valueRange\":{\"range\":\"$2\",\"values\":$3}}"
  api_call POST "/sheets/v2/spreadsheets/$1/values_prepend" -d "$body" | python3 -c "
import sys,json
d = json.load(sys.stdin)
da = d.get('data',{})
print(json.dumps({'success':d.get('code',1)==0,'updated_range':da.get('updatedRange',''),'updated_cells':da.get('updatedCells',0)}, ensure_ascii=False, indent=2))
"
}

# ========================= Images =========================

action_insert_image() {
  local token="$1" range="$2" image_path="$3"
  local filename
  filename=$(basename "$image_path")

  local image_array
  image_array=$(python3 -c "
with open('$image_path','rb') as f:
    print(list(f.read()))
")
  local body="{\"range\":\"$range\",\"image\":$image_array,\"name\":\"$filename\"}"
  api_call POST "/sheets/v2/spreadsheets/$token/values_image" -d "$body" | python3 -c "
import sys,json
d = json.load(sys.stdin)
if d.get('code',1)==0:
    print(json.dumps({'success':True,'range':'$range','image':'$filename'}, ensure_ascii=False, indent=2))
else:
    print(json.dumps({'error':d.get('msg','unknown'),'code':d.get('code',0)}, ensure_ascii=False, indent=2))
"
}

action_float_image() {
  local token="$1" sheet_id="$2" image_path="$3" range="${4:-${sheet_id}!A1:A1}"
  local width="${5:-400}" height="${6:-300}"
  local filename
  filename=$(basename "$image_path")
  local filesize
  filesize=$(stat -c%s "$image_path" 2>/dev/null || stat -f%z "$image_path" 2>/dev/null)

  local upload_resp
  upload_resp=$(api_upload "/drive/v1/medias/upload_all" \
    -F "file_name=$filename" \
    -F "parent_type=sheet_image" \
    -F "parent_node=$token" \
    -F "size=$filesize" \
    -F "file=@$image_path")

  local file_token
  file_token=$(echo "$upload_resp" | python3 -c "import sys,json; print(json.load(sys.stdin).get('data',{}).get('file_token',''))" 2>/dev/null)

  if [[ -z "$file_token" ]]; then
    echo "$upload_resp" | python3 -c "
import sys,json
d = json.load(sys.stdin)
print(json.dumps({'error':'upload failed','detail':d.get('msg',str(d))}, ensure_ascii=False, indent=2))
"
    return 1
  fi

  local body="{\"float_image_token\":\"$file_token\",\"range\":\"$range\",\"width\":$width,\"height\":$height}"
  api_call POST "/sheets/v3/spreadsheets/$token/sheets/$sheet_id/float_images" -d "$body" | python3 -c "
import sys,json
d = json.load(sys.stdin)
fi = d.get('data',{}).get('float_image',{})
if fi:
    print(json.dumps({'success':True,'float_image_id':fi.get('float_image_id',''),'range':fi.get('range',''),'width':fi.get('width',0),'height':fi.get('height',0)}, ensure_ascii=False, indent=2))
else:
    print(json.dumps({'error':d.get('msg','unknown'),'code':d.get('code',0)}, ensure_ascii=False, indent=2))
"
}

action_float_image_url() {
  local token="$1" sheet_id="$2" image_url="$3" range="${4:-${sheet_id}!A1:A1}"
  local width="${5:-400}" height="${6:-300}"

  local tmpfile
  tmpfile=$(mktemp /tmp/feishu_img_XXXXXX)
  local filename
  filename=$(basename "$image_url" | sed 's/\?.*//')
  [[ "$filename" != *.* ]] && filename="${filename}.png"

  curl -sL "$image_url" -o "$tmpfile"
  if [[ ! -s "$tmpfile" ]]; then
    rm -f "$tmpfile"
    echo '{"error":"Failed to download image from URL"}'
    return 1
  fi

  action_float_image "$token" "$sheet_id" "$tmpfile" "$range" "$width" "$height"
  rm -f "$tmpfile"
}

# ========================= Style =========================

action_style() {
  local token="$1" range="$2" style_json="$3"
  local body="{\"appendStyle\":{\"range\":\"$range\",\"style\":$style_json}}"
  api_call PUT "/sheets/v2/spreadsheets/$token/style" -d "$body" | python3 -c "
import sys,json
d = json.load(sys.stdin)
print(json.dumps({'success':d.get('code',1)==0,'msg':d.get('msg','')}, ensure_ascii=False, indent=2))
"
}

action_style_batch() {
  local token="$1" data="$2"
  api_call PUT "/sheets/v2/spreadsheets/$token/styles_batch_update" -d "$data" | python3 -c "
import sys,json
d = json.load(sys.stdin)
print(json.dumps({'success':d.get('code',1)==0,'msg':d.get('msg','')}, ensure_ascii=False, indent=2))
"
}

# ========================= Merge =========================

action_merge() {
  local token="$1" range="$2" merge_type="${3:-MERGE_ALL}"
  api_call POST "/sheets/v2/spreadsheets/$token/merge_cells" \
    -d "{\"range\":\"$range\",\"mergeType\":\"$merge_type\"}" | python3 -c "
import sys,json
d = json.load(sys.stdin)
print(json.dumps({'success':d.get('code',1)==0,'msg':d.get('msg','')}, ensure_ascii=False, indent=2))
"
}

action_unmerge() {
  local token="$1" range="$2"
  api_call POST "/sheets/v2/spreadsheets/$token/unmerge_cells" \
    -d "{\"range\":\"$range\"}" | python3 -c "
import sys,json
d = json.load(sys.stdin)
print(json.dumps({'success':d.get('code',1)==0,'msg':d.get('msg','')}, ensure_ascii=False, indent=2))
"
}

# ========================= Sheets =========================

action_add_sheet() {
  local body="{\"requests\":[{\"addSheet\":{\"properties\":{\"title\":\"$2\"}}}]}"
  api_call POST "/sheets/v2/spreadsheets/$1/sheets_batch_update" -d "$body" | python3 -c "
import sys,json
d = json.load(sys.stdin)
replies = d.get('data',{}).get('replies',[])
if replies:
    p = replies[0].get('addSheet',{}).get('properties',{})
    print(json.dumps({'success':True,'sheet_id':p.get('sheetId',''),'title':p.get('title','')}, ensure_ascii=False, indent=2))
else:
    print(json.dumps({'error':d.get('msg','unknown'),'code':d.get('code',0)}, ensure_ascii=False, indent=2))
"
}

action_delete_sheet() {
  local body="{\"requests\":[{\"deleteSheet\":{\"sheetId\":\"$2\"}}]}"
  api_call POST "/sheets/v2/spreadsheets/$1/sheets_batch_update" -d "$body" | python3 -c "
import sys,json
d = json.load(sys.stdin)
print(json.dumps({'success':d.get('code',1)==0,'msg':d.get('msg','')}, ensure_ascii=False, indent=2))
"
}

action_copy_sheet() {
  local body="{\"requests\":[{\"copySheet\":{\"source\":{\"sheetId\":\"$2\"},\"destination\":{\"title\":\"$3\"}}}]}"
  api_call POST "/sheets/v2/spreadsheets/$1/sheets_batch_update" -d "$body" | python3 -c "
import sys,json
d = json.load(sys.stdin)
replies = d.get('data',{}).get('replies',[])
if replies:
    p = replies[0].get('copySheet',{}).get('properties',{})
    print(json.dumps({'success':True,'sheet_id':p.get('sheetId',''),'title':p.get('title','')}, ensure_ascii=False, indent=2))
else:
    print(json.dumps({'error':d.get('msg','unknown'),'code':d.get('code',0)}, ensure_ascii=False, indent=2))
"
}

# ========================= Rows/Cols =========================

action_add_rows() {
  local body="{\"dimension\":{\"sheetId\":\"$2\",\"majorDimension\":\"ROWS\",\"length\":$3}}"
  api_call POST "/sheets/v2/spreadsheets/$1/dimension_range" -d "$body" | python3 -c "
import sys,json
d = json.load(sys.stdin)
print(json.dumps({'success':d.get('code',1)==0,'msg':d.get('msg','')}, ensure_ascii=False, indent=2))
"
}

action_add_cols() {
  local body="{\"dimension\":{\"sheetId\":\"$2\",\"majorDimension\":\"COLUMNS\",\"length\":$3}}"
  api_call POST "/sheets/v2/spreadsheets/$1/dimension_range" -d "$body" | python3 -c "
import sys,json
d = json.load(sys.stdin)
print(json.dumps({'success':d.get('code',1)==0,'msg':d.get('msg','')}, ensure_ascii=False, indent=2))
"
}

action_insert_rows() {
  local body="{\"dimension\":{\"sheetId\":\"$2\",\"majorDimension\":\"ROWS\",\"startIndex\":$3,\"endIndex\":$4},\"inheritStyle\":\"BEFORE\"}"
  api_call POST "/sheets/v2/spreadsheets/$1/insert_dimension_range" -d "$body" | python3 -c "
import sys,json
d = json.load(sys.stdin)
print(json.dumps({'success':d.get('code',1)==0,'msg':d.get('msg','')}, ensure_ascii=False, indent=2))
"
}

action_delete_rows() {
  local body="{\"dimension\":{\"sheetId\":\"$2\",\"majorDimension\":\"ROWS\",\"startIndex\":$3,\"endIndex\":$4}}"
  api_call DELETE "/sheets/v2/spreadsheets/$1/dimension_range" -d "$body" | python3 -c "
import sys,json
d = json.load(sys.stdin)
print(json.dumps({'success':d.get('code',1)==0,'msg':d.get('msg','')}, ensure_ascii=False, indent=2))
"
}

action_delete_cols() {
  local body="{\"dimension\":{\"sheetId\":\"$2\",\"majorDimension\":\"COLUMNS\",\"startIndex\":$3,\"endIndex\":$4}}"
  api_call DELETE "/sheets/v2/spreadsheets/$1/dimension_range" -d "$body" | python3 -c "
import sys,json
d = json.load(sys.stdin)
print(json.dumps({'success':d.get('code',1)==0,'msg':d.get('msg','')}, ensure_ascii=False, indent=2))
"
}

# ========================= Find/Replace =========================

action_find() {
  api_call POST "/sheets/v3/spreadsheets/$1/sheets/$2/find" \
    -d "{\"find\":\"$3\",\"find_condition\":{\"range\":\"$2\"}}" | python3 -c "
import sys,json
d = json.load(sys.stdin)
fr = d.get('data',{}).get('find_result',{})
cells = fr.get('matched_cells',[]) if fr else []
print(json.dumps({'success':d.get('code',1)==0,'matched':len(cells),'rows_count':fr.get('rows_count',0),'cells':cells[:50]}, ensure_ascii=False, indent=2))
"
}

action_replace() {
  api_call POST "/sheets/v3/spreadsheets/$1/sheets/$2/replace" \
    -d "{\"find\":\"$3\",\"replacement\":\"$4\",\"find_condition\":{\"range\":\"$2\"}}" | python3 -c "
import sys,json
d = json.load(sys.stdin)
rr = d.get('data',{}).get('replace_result',{})
print(json.dumps({'success':d.get('code',1)==0,'matched':rr.get('matched_cells_count',0),'replaced':rr.get('replaced_cells_count',0)}, ensure_ascii=False, indent=2))
"
}

# ========================= Main =========================

ACTION="${1:-help}"
shift || true

case "$ACTION" in
  create)       action_create "${1:-}" "${2:-}" ;;
  meta)         action_meta "$1" ;;
  read)         action_read "$1" "$2" ;;
  read_multi)   action_read_multi "$@" ;;
  write)        action_write "$1" "$2" "$3" ;;
  write_multi)  action_write_multi "$1" "$2" ;;
  append)       action_append "$1" "$2" "$3" ;;
  prepend)      action_prepend "$1" "$2" "$3" ;;
  insert_image)      action_insert_image "$1" "$2" "$3" ;;
  float_image)       action_float_image "$1" "$2" "$3" "${4:-}" "${5:-400}" "${6:-300}" ;;
  float_image_url)   action_float_image_url "$1" "$2" "$3" "${4:-}" "${5:-400}" "${6:-300}" ;;
  style)        action_style "$1" "$2" "$3" ;;
  style_batch)  action_style_batch "$1" "$2" ;;
  merge)        action_merge "$1" "$2" "${3:-MERGE_ALL}" ;;
  unmerge)      action_unmerge "$1" "$2" ;;
  add_sheet)    action_add_sheet "$1" "$2" ;;
  delete_sheet) action_delete_sheet "$1" "$2" ;;
  copy_sheet)   action_copy_sheet "$1" "$2" "$3" ;;
  add_rows)     action_add_rows "$1" "$2" "$3" ;;
  add_cols)     action_add_cols "$1" "$2" "$3" ;;
  insert_rows)  action_insert_rows "$1" "$2" "$3" "$4" ;;
  delete_rows)  action_delete_rows "$1" "$2" "$3" "$4" ;;
  delete_cols)  action_delete_cols "$1" "$2" "$3" "$4" ;;
  find)         action_find "$1" "$2" "$3" ;;
  replace)      action_replace "$1" "$2" "$3" "$4" ;;
  help|*)
    cat << 'HELPEOF'
Feishu Spreadsheet Tool v1.0.0

Usage: feishu-sheet.sh <action> [args...]

Credentials are auto-read from ~/.openclaw/openclaw.json (channels.feishu).
Set OPENCLAW_CONFIG to override the config path.

Spreadsheet:
  create [title] [folder_token]                         Create spreadsheet
  meta <token>                                          Get metadata (sheets list)

Data:
  read <token> <range>                                  Read cell range
  read_multi <token> <range1> <range2> ...              Read multiple ranges
  write <token> <range> <values_json>                   Write data (overwrite)
  write_multi <token> <json_body>                       Write to multiple ranges
  append <token> <range> <values_json>                  Append rows
  prepend <token> <range> <values_json>                 Prepend rows

Images:
  insert_image <token> <range> <file_path>              Insert image into cell
  float_image <token> <sheet_id> <path> [range] [w] [h] Insert floating image (local)
  float_image_url <token> <sheet_id> <url> [range] [w] [h] Insert floating image (URL)

Style:
  style <token> <range> <style_json>                    Set cell style
  style_batch <token> <json_body>                       Batch set styles

Merge:
  merge <token> <range> [MERGE_ALL|MERGE_ROWS|MERGE_COLUMNS]
  unmerge <token> <range>

Sheets:
  add_sheet <token> <title>                             Add worksheet
  delete_sheet <token> <sheet_id>                       Delete worksheet
  copy_sheet <token> <sheet_id> <new_title>             Copy worksheet

Rows/Columns:
  add_rows <token> <sheet_id> <count>
  add_cols <token> <sheet_id> <count>
  insert_rows <token> <sheet_id> <start> <end>          Insert rows (0-indexed)
  delete_rows <token> <sheet_id> <start> <end>
  delete_cols <token> <sheet_id> <start> <end>

Find/Replace:
  find <token> <sheet_id> <keyword>
  replace <token> <sheet_id> <find> <replacement>

Style JSON example: {"bold":true,"foreColor":"#FF0000","backColor":"#FFFF00","fontSize":14}
HELPEOF
    ;;
esac


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)