Skip to content

feat(feishu): add rich markdown, table ops, image upload, and color text#26790

Closed
Elarwei001 wants to merge 22 commits intoopenclaw:mainfrom
Elarwei001:feat/feishu-rich-markdown
Closed

feat(feishu): add rich markdown, table ops, image upload, and color text#26790
Elarwei001 wants to merge 22 commits intoopenclaw:mainfrom
Elarwei001:feat/feishu-rich-markdown

Conversation

@Elarwei001
Copy link
Contributor

@Elarwei001 Elarwei001 commented Feb 25, 2026

Changes

Extends Feishu document operations with several new capabilities. All new actions are dispatched through the existing feishu_doc tool.

Core: Markdown → Descendant API

Replaced the old Children API path with Descendant API for all block insertions. The Convert API handles GFM tables natively (block types 31/32), so the flow is now:

Markdown → Convert API → cleanBlocksForDescendant() → Descendant API

Large documents (>1000 blocks) are automatically batched (docx-batch-insert.ts), preserving parent-child relationships.

Table column widths are calculated from cell content length (CJK chars weighted 2x) and proportionally distributed to fill page width.

New Actions

Action Description
upload_image Insert image from URL, data URI, local file path, or plain base64
color_text Apply color/bold to a text block using [red]text[/red] markup
insert_table_row / insert_table_column Insert row or column at index
delete_table_rows / delete_table_columns Delete rows or columns
merge_table_cells Merge a rectangular cell range

Image Upload

Three issues resolved for reliable image embedding:

  1. drive_route_token: When parent_node is a document block ID, the API requires passing extra: { drive_route_token: docToken }. Per the API docs, certain upload scenarios require providing the token of the cloud document that the block belongs to.
  2. Content-Length: Pass Buffer directly to the upload form, not Readable.from(buf). Streams have unknown length, causing a Content-Length mismatch that silently truncates uploads.
  3. Path detection: Distinguish file paths from base64 strings using path.isAbsolute() — standard base64 alphabet includes /, so checking !includes('/') breaks valid JPEG base64.

The same fixes apply to inline images processed during write/append.

File Structure

src/
├── docx.ts               # Tool registration + action dispatch
├── docx-batch-insert.ts  # Batch insertion for large docs (>1000 blocks)
├── docx-color-text.ts    # Color/style markup parser and updater
├── docx-table-ops.ts     # Column width calculation + table row/column ops
├── docx-picture-ops.ts   # Image upload, download, and inline processing
└── doc-schema.ts         # TypeBox schemas for all actions

Closes #26222

@openclaw-barnacle openclaw-barnacle bot added channel: feishu Channel integration: feishu size: XL labels Feb 25, 2026
@Elarwei001 Elarwei001 force-pushed the feat/feishu-rich-markdown branch from e9d15c9 to 0e19392 Compare February 25, 2026 18:51
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

@Elarwei001 Elarwei001 force-pushed the feat/feishu-rich-markdown branch from 0e19392 to e6b38d0 Compare February 25, 2026 18:57
@Elarwei001 Elarwei001 force-pushed the feat/feishu-rich-markdown branch from e6b38d0 to a4c7f32 Compare February 25, 2026 19:05
@Elarwei001 Elarwei001 force-pushed the feat/feishu-rich-markdown branch 7 times, most recently from 90d51d3 to b297e5b Compare February 25, 2026 19:25
@Elarwei001 Elarwei001 changed the title feat(feishu): rich markdown support via Descendant API [WIP]feat(feishu): rich markdown support via Descendant API Feb 25, 2026
@Elarwei001 Elarwei001 force-pushed the feat/feishu-rich-markdown branch 3 times, most recently from 3f737a5 to 385684b Compare February 26, 2026 00:17
@Elarwei001 Elarwei001 force-pushed the feat/feishu-rich-markdown branch 2 times, most recently from 9ad82f9 to 5671e34 Compare February 26, 2026 00:33
@Elarwei001
Copy link
Contributor Author

Hi @doodlewind 👋

I noticed you're the original author of the Feishu docx implementation. This PR replaces the Children API (documentBlockChildren.create) with the Descendant API (documentBlockDescendant.create) for document writes.

Reasoning:

  • Descendant API supports tables (block types 31, 32) which Children API doesn't
  • Higher batch limit (1000 vs 50 blocks per request)
  • Better nested structure support

Question:
Was there a specific reason for using Children API originally? I want to make sure this change doesn't break any edge cases I'm not aware of.

Happy to adjust the approach if needed. Thanks! 🙏

@Elarwei001 Elarwei001 force-pushed the feat/feishu-rich-markdown branch 3 times, most recently from be7c05e to 4fc3e79 Compare February 26, 2026 00:40
When columns hit MAX_COLUMN_WIDTH, the remaining space was not
being distributed. Now redistribute evenly across all columns
that haven't reached max, ensuring tables fill the page width.
Instead of hardcoding 730px, use the sum of original column_width
values from Convert API. This ensures tables match Feishu's
expected dimensions for any document layout.
- Add insertBlocksInBatches() for documents exceeding API limit
- Add collectDescendants() to properly split blocks with their children
- Show batch progress in logs: 'Inserting batch 1/3 (1000 blocks)...'
- Automatically choose single vs batched insert based on block count
Move large document handling (>1000 blocks) to batch-insert.ts:
- collectDescendants() - gather blocks with their children
- insertBlocksInBatches() - split and insert in batches

Keep docx.ts focused on the common case (<1000 blocks).
The API limits BOTH children_id AND descendants to 1000 each.
Previous logic batched by first-level IDs but descendants could
exceed 1000. Now we ensure each batch has ≤1000 total blocks.
@Elarwei001 Elarwei001 changed the title [WIP]feat(feishu): rich markdown support via Descendant API feat(feishu): rich markdown support via Descendant API Feb 26, 2026
@Elarwei001
Copy link
Contributor Author

@greptile-apps please review the latest changes:

New Features Added

  1. Adaptive Column Width (table-utils.ts)

    • Calculates column widths based on content length
    • CJK characters weighted 2x
    • Redistributes space to fill page width
  2. Progress Logging (docx.ts)

    • Shows: Converting → Inserting → Done
    • Includes block counts and batch progress
  3. Batch Insert for Large Documents (batch-insert.ts)

    • Handles documents >1000 blocks
    • Splits by total descendants (not just first-level IDs)
    • Preserves parent-child relationships

Files Changed

  • src/docx.ts - Main operations + logging
  • src/table-utils.ts - Adaptive width calculation (NEW)
  • src/batch-insert.ts - Large document handling (NEW)

Please review for code quality and potential issues.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 26, 2026

Greptile Summary

Adds rich markdown support to Feishu document creation, including native table rendering, adaptive column widths, and batch insertion for large documents (>1000 blocks).

Key Changes:

  • Switched from documentBlockChildren API to documentBlockDescendant API to support tables
  • Added table-utils.ts with adaptive column width calculation (CJK characters weighted 2x)
  • Added batch-insert.ts to handle documents exceeding the 1000 block API limit
  • Added progress logging throughout the document creation flow
  • Removed limitation "Markdown tables are NOT supported" from documentation

Architecture:
The implementation is well-structured with clear separation of concerns. The batch insertion logic recursively collects descendants and splits them into batches based on total block count. The table utilities calculate proportional column widths based on content length and redistribute remaining space.

Testing:
Tests updated to use the new documentBlockDescendant API. Manual testing shows successful handling of tables, nested lists, and documents with 1973 blocks split into 2 batches.

Confidence Score: 4/5

  • Safe to merge with one edge-case bug that needs attention
  • The implementation is solid and well-architected with proper error handling, logging, and documentation updates. The batching logic has one edge-case bug where a single first-level block with >1000 descendants would exceed the API limit, but this scenario is unlikely in practice. The feature has been tested with documents up to 1973 blocks and successfully handles typical use cases including tables, nested lists, and large documents.
  • extensions/feishu/src/batch-insert.ts - review the batching logic for handling extremely large single blocks

Last reviewed commit: 86e0e14

Comment on lines +120 to +127
// If adding this first-level block would exceed limit, start new batch
if (
currentBatch.blocks.length + newBlocks.length > BATCH_SIZE &&
currentBatch.blocks.length > 0
) {
batches.push(currentBatch);
currentBatch = { firstLevelIds: [], blocks: [] };
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if a single first-level block has >1000 descendants, it will still be added to an empty batch, exceeding the API limit

the condition currentBatch.blocks.length > 0 prevents starting a new batch when the current batch is empty, but doesn't handle the case where newBlocks.length alone exceeds BATCH_SIZE

example scenario:

  • empty batch (length = 0)
  • single block with 1500 descendants
  • check: 0 + 1500 > 1000 && 0 > 0 → false
  • adds all 1500 blocks → exceeds limit
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/feishu/src/batch-insert.ts
Line: 120-127

Comment:
if a single first-level block has >1000 descendants, it will still be added to an empty batch, exceeding the API limit

the condition `currentBatch.blocks.length > 0` prevents starting a new batch when the current batch is empty, but doesn't handle the case where `newBlocks.length` alone exceeds `BATCH_SIZE`

example scenario:
- empty batch (length = 0)
- single block with 1500 descendants
- check: `0 + 1500 > 1000 && 0 > 0` → false
- adds all 1500 blocks → exceeds limit

How can I resolve this? If you propose a fix, please make it concise.

@Elarwei001
Copy link
Contributor Author

Elarwei001 commented Feb 26, 2026

Hi @m1heng, would appreciate your review when you have a moment.

This PR adds table rendering, image upload, table manipulation, and text color to the Feishu doc tool. These come up frequently when agents try to generate structured reports or insert diagrams — features that are hard to work around without native API support.

The PR description has the full details. Thanks!

New actions:
- insert_table_row: Insert a new row at specified index
- insert_table_column: Insert a new column at specified index
- delete_table_rows: Delete rows starting from index
- delete_table_columns: Delete columns starting from index
- merge_table_cells: Merge a range of cells
Move row/column manipulation functions to dedicated module:
- insertTableRow
- insertTableColumn
- deleteTableRows
- deleteTableColumns
- mergeTableCells
Supports a simple markup syntax for colored text:
  'Revenue [green]+15%[/green] YoY, Costs [red]-3%[/red]'

Tags: [red] [green] [blue] [orange] [yellow] [purple] [grey]
      [bold] [bg:color] (background color)

Use case: data reports with red/green highlighting for up/down trends.
Add usage examples for:
- insert_table_row / insert_table_column
- delete_table_rows / delete_table_columns
- merge_table_cells
- color_text with markup syntax and data report workflow
Supports uploading local images to Feishu documents:
- data URI: data:image/png;base64,...
- Plain base64 string
- Absolute file path

Flow follows official Feishu FAQ:
1. Create empty image block (Children API)
2. Upload image with parent_node = image block ID
3. Set image token via replace_image patch

Note: tested implementation matches official API docs.
JP-cluster cross-region issue observed in test env only.
Fixes image upload for JP cluster (and other non-default regions).
When parent_node is a doxjp... block ID, the drive service at the
global endpoint couldn't route to the JP datacenter, returning
'parent node not exist' (1061044).

Solution: pass extra={drive_route_token: docToken} to upload_all API.
This tells the drive service which document the block belongs to,
enabling correct datacenter routing.

Affects:
- processImages(): URL-based images in write/append actions
- uploadImageAction(): new upload_image action

Ref: Feishu docs - drive.media.uploadAll 'extra' param
….from()

Readable.from(buffer) creates a stream with unknown Content-Length,
causing form-data to declare wrong size in headers. Server rejects
with 'Error when parsing request' for images >~1KB.

Passing the Buffer directly lets form-data calculate Content-Length
correctly, fixing uploads for real-world image sizes.

Also removes unused Readable import from stream.
JPEG base64 strings start with '/9j/' (JPEG magic bytes in base64),
which was incorrectly matched as an absolute file path.

Fix: also require path length < 1024 chars. Real file paths are short;
base64-encoded images are thousands of characters long.

All 3 input formats now tested and passing:
- data URI:    data:image/jpeg;base64,...  ✅
- plain base64: /9j/4AAQSkZJRg...          ✅
- file path:   /Users/.../image.jpg        ✅
Allows inserting images from http/https URLs directly:
  upload_image { image: "https://example.com/photo.jpg" }

Also updates schema description to document all 4 input types:
- https/http URL (downloads via OpenClaw media fetcher)
- data URI (data:image/jpeg;base64,...)
- plain base64 string
- absolute file path

Passes mediaMaxBytes from account config for size limit enforcement.
- Rename batch-insert.ts → docx-batch-insert.ts
- Rename color-text.ts → docx-color-text.ts
- Merge table-ops.ts + table-utils.ts → docx-table-ops.ts
- Extract image operations from docx.ts → docx-picture-ops.ts
- Fix file path detection: use path.isAbsolute() + os.homedir()
  instead of checking for leading '/' characters (base64 also uses '/')
- Fix plain base64 detection: standard base64 alphabet includes '/'
  so the previous !includes('/') check broke valid JPEG base64 strings
@Elarwei001 Elarwei001 changed the title feat(feishu): rich markdown support via Descendant API feat(feishu): add rich markdown, table ops, image upload, and color text Feb 26, 2026
Adds action: 'insert' to feishu_doc tool.

- Resolves the target block's parent and index via documentBlock.get
  and documentBlockChildren.get
- Converts markdown via Convert API, then inserts using Descendant API
  with explicit index parameter (supports both small and large docs)
- Large documents (>1000 blocks) use batch insert with advancing index
- Also threads parent/index params through insertBlocksWithDescendant
  and insertBlocksInBatches for full position support
@Takhoffman
Copy link
Contributor

Closing as superseded by mainline Feishu doc/tooling work in commit 56fa058 (56fa05838). This landed rich document/table and upload capabilities via #20304.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: feishu Channel integration: feishu size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(feishu): support tables, table operations, image upload, and color text in doc tools

2 participants