Skill: xing2xian/feishu-doc-extended (version 1.0.0)

--- name: feishu-doc-extended description: 飞书文档扩展工具，提供图片下载和 OCR 识别功能。需要配合内置 feishu 插件使用。 homepage: https://open.feishu.cn metadata: { "openclaw": { "emoji": "📄", "requires": { "bins": ["tesseract"], "plugins": ["feishu"] }, "install": [ { "id": "brew-tesseract", "kind": "brew", "formula": "tesseract", "bins": ["tesseract"], "label": "Install tesseract OCR" }, { "id": "brew-tesseract-lang", "kind": "brew", "formula": "tesseract-lang", "label": "Install tesseract language data" } ] } } --- # feishu-doc-extended 飞书文档扩展工具，提供图片下载和 OCR 识别功能。 ## 功能 | 功能 | 说明 | |------|------| | **get_image** | 获取飞书文档中图片的下载 URL | | **image_ocr** | 下载图片并进行 OCR 文字识别（需要 tesseract） | ## 依赖 - tesseract + 中文语言包（用于 OCR） - 飞书开放平台应用权限 - OpenClaw 内置 feishu 插件 ## 安装 ```bash # 安装 tesseract brew install tesseract # 安装中文语言包 brew install tesseract-lang ``` ## 修改内置插件 本技能需要修改 OpenClaw 内置的 feishu 插件： ### 1. 修改 doc-schema.ts 文件路径: `/usr/local/lib/node_modules/openclaw/extensions/feishu/src/doc-schema.ts` 在 `FeishuDocSchema` 的 Union 类型末尾添加： ```typescript // Image download Type.Object({ action: Type.Literal("get_image"), image_token: Type.String({ description: "Image token (from block image.token)" }), }), ``` ### 2. 修改 docx.ts 文件路径: `/usr/local/lib/node_modules/openclaw/extensions/feishu/src/docx.ts` 1. 在文件末尾（`uploadFileBlock` 函数后）添加： ```typescript async function getImage(client: Lark.Client, imageToken: string) { const domain = client.domain ?? "https://open.feishu.cn"; const token = await client.tokenManager.getTenantAccessToken(); const res = await client.httpInstance.get<{ code?: number; data?: { image_url?: string } }>( `${domain}/open-apis/image/v4/get`, { params: { image_token: imageToken }, headers: { Authorization: `Bearer ${token}` }, }, ); if (res.data?.code !== 0 && res.data?.code !== undefined) { throw new Error(`Failed to get image: ${res.data}`); } return { image_url: res.data?.data?.image_url, image_token: imageToken, }; } ``` 2. 在 switch 语句中添加 case： ```typescript case "get_image": return json(await getImage(client, p.image_token)); ``` ### 3. 重启 Gateway ```bash openclaw gateway restart ``` ## 使用方法 ### 1. 获取文档中的图片 token 使用 `feishu_doc` 工具的 `list_blocks` 获取文档中的图片 block： ```json { "action": "list_blocks", "doc_token": "文档Token" } ``` 从返回结果中获取图片的 token（在 `block.image.token` 中）。 ### 2. 获取图片下载 URL ```json { "action": "get_image", "image_token": "图片Token" } ``` 返回： ```json { "image_url": "https://xxx...", "image_token": "图片Token" } ``` ### 3. OCR 识别 获取图片 URL 后，可以用浏览器打开并截图，然后用 tesseract 识别： ```bash tesseract /path/to/screenshot.jpg - -l chi_sim ``` ## 工作流程 ``` 1. feishu_doc list_blocks → 获取图片 block 和 token 2. feishu_doc get_image → 获取图片下载 URL 3. 浏览器访问 URL → 截图 4. tesseract OCR → 识别文字 ``` ## 注意事项 - `get_image` 返回的 URL 是飞书临时 URL，有时效性 - 如果 URL 过期，需要重新调用 `get_image` - OCR 识别效果取决于图片清晰度 ## 更新日志 - 2026-03-12: 初始版本，添加 get_image 功能

path: README.md
content: # feishu-doc-extended

飞书文档扩展工具包，提供图片下载和 OCR 识别功能。

## 文件说明

```
feishu-doc-extended/
├── SKILL.md              # 技能定义文件
├── README.md             # 使用说明
├── src/
│   ├── doc-schema.ts     # 修改的 schema（新增 get_image）
│   └── docx.ts          # 修改的实现（新增 getImage 函数）
├── references/
│   └── CHANGELOG.md     # 更新日志
└── scripts/
    └── image-ocr.sh     # OCR 识别脚本（可选）
```

## 安装步骤

### 1. 安装依赖

```bash
# 安装 tesseract OCR
brew install tesseract
brew install tesseract-lang
```

### 2. 修改飞书插件

本技能需要修改 OpenClaw 内置的 feishu 插件：

**修改文件 1**: `/usr/local/lib/node_modules/openclaw/extensions/feishu/src/doc-schema.ts`

在 `FeishuDocSchema` 的 Union 类型末尾添加：

```typescript
// Image download
Type.Object({
  action: Type.Literal("get_image"),
  image_token: Type.String({ description: "Image token (from block image.token)" }),
}),
```

**修改文件 2**: `/usr/local/lib/node_modules/openclaw/extensions/feishu/src/docx.ts`

1. 在文件末尾（`uploadFileBlock` 函数后）添加：

```typescript
async function getImage(client: Lark.Client, imageToken: string) {
  const domain = client.domain ?? "https://open.feishu.cn";
  const token = await client.tokenManager.getTenantAccessToken();

  const res = await client.httpInstance.get<{ code?: number; data?: { image_url?: string } }>(
    `${domain}/open-apis/image/v4/get`,
    {
      params: { image_token: imageToken },
      headers: { Authorization: `Bearer ${token}` },
    },
  );

  if (res.data?.code !== 0 && res.data?.code !== undefined) {
    throw new Error(`Failed to get image: ${res.data}`);
  }

  return {
    image_url: res.data?.data?.image_url,
    image_token: imageToken,
  };
}
```

2. 在 `switch` 语句中添加 case：

```typescript
case "get_image":
  return json(await getImage(client, p.image_token));
```

### 3. 重启 Gateway

```bash
openclaw gateway restart
```

## 使用示例

### 获取文档中的图片

```javascript
// 1. 列出文档中的 blocks
feishu_doc({
  action: "list_blocks",
  doc_token: "Cxvsw9nGpiy1hsky1Agc7agdnlc"
})

// 返回包含 image blocks，每个有 token
// 例如: block.image.[REDACTED_SECRET]"

// 2. 获取图片下载 URL
feishu_doc({
  action: "get_image",
  image_token: "SJyGbgoTjomp0fxGI0Ccm4RInRf"
})

// 返回: { image_url: "https://xxx", image_token: "..." }
```

### OCR 识别

获取图片 URL 后，可以用浏览器打开并截图，然后用 tesseract 识别：

```bash
# 截图后识别
tesseract /path/to/screenshot.jpg - -l chi_sim
```

## 注意事项

1. **临时 URL**: `get_image` 返回的 URL 是飞书临时生成的，有时效性
2. **权限**: 需要飞书开放平台应用有 `drive:drive` 权限
3. **OCR 效果**: 取决于图片清晰度，建议使用高清图片

## 故障排除

### get_image 返回空 image_url

- 可能是 token 过期，尝试重新获取
- 检查飞书应用权限

### OCR 识别效果差

- 使用更高分辨率的图片
- 调整 tesseract 的预处理参数
- 考虑使用其他 OCR 服务

## 贡献

欢迎提交 Issue 和 Pull Request！

## 许可证

MIT


path: package.json
content: {
  "name": "feishu-doc-extended",
  "version": "1.0.0",
  "description": "飞书文档扩展工具 - 图片下载和 OCR 识别",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "feishu",
    "openclaw",
    "ocr",
    "document"
  ],
  "author": "",
  "license": "MIT",
  "dependencies": {},
  "devDependencies": {}
}


path: references/CHANGELOG.md
content: # Changelog

All notable changes to this project will be documented in this file.

## [1.0.0] - 2026-03-12

### Added
- `get_image` action for feishu_doc tool
- Ability to download images from Feishu documents
- Support for OCR integration with tesseract

### Modified
- `doc-schema.ts`: Added get_image schema definition
- `docx.ts`: Added getImage() function implementation

### Known Issues
- get_image returns temporary URL with limited validity
- OCR accuracy depends on image quality

## [0.0.1] - 2026-03-12

### Initial release
- Concept version


path: src/doc-schema.ts
content: import { Type, type Static } from "@sinclair/typebox";

const tableCreationProperties = {
  doc_token: Type.String({ description: "Document token" }),
  parent_block_id: Type.Optional(
    Type.String({ description: "Parent block ID (default: document root)" }),
  ),
  row_size: Type.Integer({ description: "Table row count", minimum: 1 }),
  column_size: Type.Integer({ description: "Table column count", minimum: 1 }),
  column_width: Type.Optional(
    Type.Array(Type.Number({ minimum: 1 }), {
      description: "Column widths in px (length should match column_size)",
    }),
  ),
};

export const FeishuDocSchema = Type.Union([
  Type.Object({
    action: Type.Literal("read"),
    doc_token: Type.String({ description: "Document token (extract from URL /docx/XXX)" }),
  }),
  Type.Object({
    action: Type.Literal("write"),
    doc_token: Type.String({ description: "Document token" }),
    content: Type.String({
      description: "Markdown content to write (replaces entire document content)",
    }),
  }),
  Type.Object({
    action: Type.Literal("append"),
    doc_token: Type.String({ description: "Document token" }),
    content: Type.String({ description: "Markdown content to append to end of document" }),
  }),
  Type.Object({
    action: Type.Literal("insert"),
    doc_token: Type.String({ description: "Document token" }),
    content: Type.String({ description: "Markdown content to insert" }),
    after_block_id: Type.String({
      description: "Insert content after this block ID. Use list_blocks to find block IDs.",
    }),
  }),
  Type.Object({
    action: Type.Literal("create"),
    title: Type.String({ description: "Document title" }),
    folder_token: Type.Optional(Type.String({ description: "Target folder token (optional)" })),
    grant_to_requester: Type.Optional(
      Type.Boolean({
        description:
          "Grant edit permission to the trusted requesting Feishu user from runtime context (default: true).",
      }),
    ),
  }),
  Type.Object({
    action: Type.Literal("list_blocks"),
    doc_token: Type.String({ description: "Document token" }),
  }),
  Type.Object({
    action: Type.Literal("get_block"),
    doc_token: Type.String({ description: "Document token" }),
    block_id: Type.String({ description: "Block ID (from list_blocks)" }),
  }),
  Type.Object({
    action: Type.Literal("update_block"),
    doc_token: Type.String({ description: "Document token" }),
    block_id: Type.String({ description: "Block ID (from list_blocks)" }),
    content: Type.String({ description: "New text content" }),
  }),
  Type.Object({
    action: Type.Literal("delete_block"),
    doc_token: Type.String({ description: "Document token" }),
    block_id: Type.String({ description: "Block ID" }),
  }),
  // Table creation (explicit structure)
  Type.Object({
    action: Type.Literal("create_table"),
    ...tableCreationProperties,
  }),
  Type.Object({
    action: Type.Literal("write_table_cells"),
    doc_token: Type.String({ description: "Document token" }),
    table_block_id: Type.String({ description: "Table block ID" }),
    values: Type.Array(Type.Array(Type.String()), {
      description: "2D matrix values[row][col] to write into table cells",
      minItems: 1,
    }),
  }),
  Type.Object({
    action: Type.Literal("create_table_with_values"),
    ...tableCreationProperties,
    values: Type.Array(Type.Array(Type.String()), {
      description: "2D matrix values[row][col] to write into table cells",
      minItems: 1,
    }),
  }),
  // Table row/column manipulation
  Type.Object({
    action: Type.Literal("insert_table_row"),
    doc_token: Type.String({ description: "Document token" }),
    block_id: Type.String({ description: "Table block ID" }),
    row_index: Type.Optional(
      Type.Number({ description: "Row index to insert at (-1 for end, default: -1)" }),
    ),
  }),
  Type.Object({
    action: Type.Literal("insert_table_column"),
    doc_token: Type.String({ description: "Document token" }),
    block_id: Type.String({ description: "Table block ID" }),
    column_index: Type.Optional(
      Type.Number({ description: "Column index to insert at (-1 for end, default: -1)" }),
    ),
  }),
  Type.Object({
    action: Type.Literal("delete_table_rows"),
    doc_token: Type.String({ description: "Document token" }),
    block_id: Type.String({ description: "Table block ID" }),
    row_start: Type.Number({ description: "Start row index (0-based)" }),
    row_count: Type.Optional(Type.Number({ description: "Number of rows to delete (default: 1)" })),
  }),
  Type.Object({
    action: Type.Literal("delete_table_columns"),
    doc_token: Type.String({ description: "Document token" }),
    block_id: Type.String({ description: "Table block ID" }),
    column_start: Type.Number({ description: "Start column index (0-based)" }),
    column_count: Type.Optional(
      Type.Number({ description: "Number of columns to delete (default: 1)" }),
    ),
  }),
  Type.Object({
    action: Type.Literal("merge_table_cells"),
    doc_token: Type.String({ description: "Document token" }),
    block_id: Type.String({ description: "Table block ID" }),
    row_start: Type.Number({ description: "Start row index" }),
    row_end: Type.Number({ description: "End row index (exclusive)" }),
    column_start: Type.Number({ description: "Start column index" }),
    column_end: Type.Number({ description: "End column index (exclusive)" }),
  }),
  // Image / file upload
  Type.Object({
    action: Type.Literal("upload_image"),
    doc_token: Type.String({ description: "Document token" }),
    url: Type.Optional(Type.String({ description: "Remote image URL (http/https)" })),
    file_path: Type.Optional(Type.String({ description: "Local image file path" })),
    image: Type.Optional(
      Type.String({
        description:
          "Image as data URI (data:image/png;base64,...) or plain base64 string. Use instead of url/file_path for DALL-E outputs, canvas screenshots, etc.",
      }),
    ),
    parent_block_id: Type.Optional(
      Type.String({ description: "Parent block ID (default: document root)" }),
    ),
    filename: Type.Optional(Type.String({ description: "Optional filename override" })),
    index: Type.Optional(
      Type.Integer({
        minimum: 0,
        description: "Insert position (0-based index among siblings). Omit to append.",
      }),
    ),
  }),
  Type.Object({
    action: Type.Literal("upload_file"),
    doc_token: Type.String({ description: "Document token" }),
    url: Type.Optional(Type.String({ description: "Remote file URL (http/https)" })),
    file_path: Type.Optional(Type.String({ description: "Local file path" })),
    parent_block_id: Type.Optional(
      Type.String({ description: "Parent block ID (default: document root)" }),
    ),
    filename: Type.Optional(Type.String({ description: "Optional filename override" })),
  }),
  // Text color / style
  Type.Object({
    action: Type.Literal("color_text"),
    doc_token: Type.String({ description: "Document token" }),
    block_id: Type.String({ description: "Text block ID to update" }),
    content: Type.String({
      description:
        'Text with color markup. Tags: [red], [green], [blue], [orange], [yellow], [purple], [grey], [bold], [bg:yellow]. Example: "Revenue [green]+15%[/green] YoY"',
    }),
  }),
  // Image download
  Type.Object({
    action: Type.Literal("get_image"),
    image_token: Type.String({ description: "Image token (from block image.token)" }),
  }),
]);

export type FeishuDocParams = Static<typeof FeishuDocSchema>;


path: src/getImage.ts
content: async function getImage(client: Lark.Client, imageToken: string) {
  const domain = client.domain ?? "https://open.feishu.cn";
  const token = await client.tokenManager.getTenantAccessToken();

  const res = await client.httpInstance.get<{ code?: number; data?: { image_url?: string } }>(
    `${domain}/open-apis/image/v4/get`,
    {
      params: { image_token: imageToken },
      headers: { Authorization: `Bearer ${token}` },
    },
  );

  if (res.data?.code !== 0 && res.data?.code !== undefined) {
    throw new Error(`Failed to get image: ${res.data}`);
  }

  return {
    image_url: res.data?.data?.image_url,
    image_token: imageToken,
  };
}

// ============ Actions ============

const STRUCTURED_BLOCK_TYPES = new Set([14, 18, 21, 23, 27, 30, 31, 32]);

async function readDoc(client: Lark.Client, docToken: string) {
  const [contentRes, infoRes, blocksRes] = await Promise.all([
    client.docx.document.rawContent({ path: { document_id: docToken } }),
    client.docx.document.get({ path: { document_id: docToken } }),
    client.docx.documentBlock.list({ path: { document_id: docToken } }),


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