feishu-user-plugin 1.3.15 → 1.3.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feishu-user-plugin",
3
- "version": "1.3.15",
3
+ "version": "1.3.17",
4
4
  "description": "All-in-one Feishu MCP server + CLI tool for Claude Code — send messages as yourself, read chats (auto-expanded merge_forward), manage docs / bitable / wiki (full CRUD) / drive / OKR (with progress writes) / calendar (read+write) / Tasks v2 / multi-profile auto-switch / real-time WS events. 85 tools + 9 prompts, 3 auth layers.",
5
5
  "author": {
6
6
  "name": "EthanQC"
@@ -2,7 +2,7 @@
2
2
  "name": "feishu-user-plugin",
3
3
  "displayName": "Feishu MCP for Claude Code & Codex",
4
4
  "description": "All-in-one Feishu MCP server + CLI tool for Claude Code / Codex / Cursor / scripts — 85 tools across 3 auth layers (cookie / app / OAuth). Send as you, read groups, manage docs / bitable / wiki / drive / calendar / tasks / OKR.",
5
- "version": "1.3.15",
5
+ "version": "1.3.17",
6
6
  "author": {
7
7
  "name": "EthanQC"
8
8
  },
@@ -2,7 +2,7 @@
2
2
  "manifest_version": "0.3",
3
3
  "name": "feishu-user-plugin",
4
4
  "display_name": "Feishu MCP for Claude Code & Codex",
5
- "version": "1.3.15",
5
+ "version": "1.3.17",
6
6
  "description": "All-in-one Feishu MCP server + CLI tool for Claude Code / Codex / Cursor / scripts — 85 tools across 3 auth layers (cookie / app / OAuth). Send as you, read groups, manage docs / bitable / wiki / drive / calendar / tasks / OKR.",
7
7
  "long_description": "feishu-user-plugin is a local stdio MCP server (and shell CLI tool) that bridges Feishu / Lark and any MCP client (Claude Code, Codex, Cursor, Windsurf, OpenClaw, Claude Desktop). It exposes 85 tools across three auth layers: cookie + protobuf for sending messages as the real user (a capability not available through the official bot API), Feishu Open Platform app credentials for groups / docs / bitable / wiki / drive / calendar / tasks / OKR, and user OAuth (UAT) for P2P chat reading and user-owned resource creation.",
8
8
  "author": {
package/CHANGELOG.md CHANGED
@@ -4,6 +4,57 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [1.3.17] - 2026-06-08
8
+
9
+ 本版围绕读路径完整性做一轮系统性收口:大文档与大列表不再静默截断、批量写的部分失败不再被读作全成功、文档表格 / 媒体块建失败可定位可修复。85 工具数不变,`get_doc_blocks` / `manage_doc_block` / `read_messages` / `read_p2p_messages` / `list_wiki_nodes` / `manage_bitable_record` 等 schema 新增分页或上报字段,无 breaking API。升级后重启 Claude Code / Codex 自动拉 v1.3.17。
10
+
11
+ ### Added
12
+
13
+ - **get_doc_blocks / read_doc_markdown 分页拉全量**:跟进 `page_token` 拉完整块树,`hasMore:false` 才代表拉全;此前单页静默截断在 500 块,大文档(一次报障是 280+ 块 / ~300KB)尾部"消失"且无任何标志,调用方误以为那部分块没建成功。`get_doc_blocks` 新增 `max_blocks` 限定单次返回 + `nextPageToken` 续拉,被限定的返回带 `truncated:true`;`read_doc_markdown` 块树不完整时末尾追加 `[output truncated]` 注记。(src/clients/official/docs.js)
14
+ - **read_messages / read_p2p_messages / list_wiki_nodes / manage_bitable_record(search) 分页续拉**:四条读路径新增 `page_token` 入参,`hasMore:true` 时把返回的 `pageToken` 回填即可翻页;此前 client 已返回游标但工具层丢弃 / schema 无入参,超出单页窗口(消息默认 20 上限 50、wiki 节点 50、bitable 默认 20)的内容拿不到也续不了,digest / 全表扫描类任务据此得出残缺结论。
15
+ - **manage_members(add) 部分失败上报**:新增 `notExistedIds`(用户不存在)/ `pendingApprovalIds`(卡入群审批、尚未进群),任一非空时响应顶部 ⚠ 逐一点名;此前只透 `invalidIds`,后两类被静默吞掉,半失败读作全员入群(卡审批的"成员"后续 @ 不到)。字段名对照飞书 OpenAPI 生成的 SDK 类型核验。(src/clients/official/groups.js)
16
+
17
+ ### Fixed
18
+
19
+ - **manage_doc_block 建表填格部分失败可恢复**:mode F 填格遇瞬态错误(`code=2200` scope-check 抖动 / 限频 / 5xx)自动退避重试;重试后仍失败的格子记录 `failedCells:[{row,col,cellId,textBlockId,reason,skipped}]`(row/col 0 起算)随成功结果返回、不再整体抛错,连续 3 格失败止损并标 `skipped`。逐格 `action=update`(block_id 传 textBlockId)补内容即可,不必重建表。一次报障是 7×3 表第 6 行起填格失败、整表残缺且拿不到已填清单。(src/clients/official/docs.js)
20
+ - **list_wiki_spaces 拉全量**:内部跟进 `page_token` 拉完整空间列表;此前单页截断在 50,第 51+ 个空间的 `space_id` 无法发现,后续 `list_wiki_nodes` / `create_wiki_node` 选不到 parent。(src/clients/official/wiki.js)
21
+ - **图片 / 文件块三步建块瞬态重试 + 孤儿可定位**:`createDocBlockWithImage` / `createDocBlockWithFile`(建占位块 → 上传 → PATCH)的上传与 PATCH 步骤自动重试瞬态错误;持续失败时错误携带占位块 `blockId`(上传已成功时附媒体 token + 含 `document_id` 的修复指引),不必全文找空块。(src/clients/official/docs.js)
22
+ - **空页不再当分页终止信号**:飞书因权限过滤可能返回空页 + `has_more:true` 但后面还有数据(其 `spaceNode.list` API 文档明确写"可以继续分页请求");`getDocBlocks` / `listWikiSpaces` 内部循环不再在空页停(停滞保护改由 token 守卫 + 页数 backstop),空页 + 前进游标继续翻。`list_wiki_nodes` schema 同步提醒 agent 空页 + hasMore 要续翻。(src/clients/official/docs.js、wiki.js)
23
+ - **error-codes:2200 归类 retry**:docx 的 "check incr user_access_token scope fail" 是 scope-check 服务的瞬态抖动(同一 UAT 前序调用均成功、scope 本已授权),归为可重试。(src/error-codes.js)
24
+ - **server.json 目录描述词边界截断**:registry catalog 描述改为词边界截断 + 省略号,不再切在半个词(32 个长描述受益);运行时 MCP client 经 `tools/list` 拿的仍是完整描述。(scripts/sync-server-json.js)
25
+
26
+ ### Changed
27
+
28
+ - **update_text_elements 整段替换语义强调**:`manage_doc_block(action=update)` 的 `update_text_elements` 全量覆盖该块的 elements(**非** patch / append),漏传的 element(加粗前缀、链接等)永久丢失;改局部应先 `get_doc_blocks` 读原块、整组传回。schema 描述 + docs/TOOLS.md + CLAUDE.md + skill reference 四处加粗。
29
+
30
+ ### Test scenarios
31
+
32
+ - 280+ 块大文档调 `get_doc_blocks` 应返回全部块、`hasMore:false`;`read_doc_markdown` 渲染到末段不截断
33
+ - `read_messages` 对消息密集的群 `hasMore:true` 时回填 `page_token` 应拿到更早的消息
34
+ - `manage_doc_block(action=create, table=7×3)` 21 格应全部填充;瞬态失败时返回 `failedCells` 而非整体报错
35
+ - `list_wiki_spaces` 在加入 >50 个空间的账号应返回全部、`hasMore:false`
36
+
37
+ ## [1.3.16] - 2026-06-06
38
+
39
+ 修掉发现类读路径的身份盲区:上传到个人空间的文件此前找不到、也因此删不掉。`list_files` / `search_docs` / `search_wiki` / `get_wiki_node` 四条读路径改为 UAT 优先(bot fallback 保留)。85 工具数不变,list_files / search_docs / search_wiki 三个 schema 新增分页参数,无 breaking API。
40
+
41
+ ### Added
42
+
43
+ - **list_files 看得见你的个人空间了(用户报障修复)**:此前 `list_files` 走纯 app token,bot 对个人空间("我的空间")文件夹 403,导致 `upload_drive_file` 走 UAT 传上去的文件**不可发现、也不可删除**(`manage_drive_file(action=delete)` 需要的 file_token 拿不到)。现在 UAT 优先、bot fallback:配置 UAT 后空 `folder_token` 列你自己的"我的空间"根目录。新增 `page_size` / `page_token` 入参与 `nextPageToken` 返回;root 空结果且走 bot 路径时附 `scopeHint` 解释 bot root ≠ 我的空间。(`src/clients/official/drive.js`)
44
+ - **search_docs / search_wiki 分页游标**:新增 `page_size` / `offset` 入参,`hasMore` 时返回 `nextOffset` 直接回填即可翻页;此前只有 `hasMore` 没有可用游标,截断的尾部恰好可能藏着要找的个人空间文档。异常的 `has_more:true` 空页不发 cursor,防止翻页死循环。坏参数(NaN / 负数)收敛为非负整数后才发给飞书。
45
+
46
+ ### Changed
47
+
48
+ - **search_docs / search_wiki / get_wiki_node 改 UAT-first**:suite 搜索 API 只索引调用身份可见的内容,app 身份搜不到个人空间文档(报障里上传的 PDF 就是这样消失的)。三条路径与 `list_files` 一并走 `_asUserOrApp`(UAT 优先、bot fallback,被迫走 bot 时返回 ⚠ fallbackWarning),响应统一带 `viaUser` 标明视角归属。`get_wiki_node` 保持裸 node 返回形状(resolver 兼容),additive 附加 `viaUser` / `fallbackWarning`;obj_token 合成正则不受新错误形状影响(953001 与 live 实测 131005 双分支测试钉死)。
49
+ - **依赖升级**:protobufjs 7.5.6 → 8.6.0(cookie protobuf 发送层经真实发送探针 + 读回验证);`@larksuiteoapi/node-sdk` 1.63.1 → 1.66.0(official API 读路径实测)。
50
+ - **MCP Registry namespace** 指向 `io.github.zhuzhen-team`(仓库迁移收尾)。
51
+
52
+ ### Test scenarios
53
+
54
+ - 配置 UAT 后调 `list_files`(空参)应列出你"我的空间"根目录且 `viaUser:true`
55
+ - `upload_drive_file` 上传 → `list_files` 拿 file_token → `manage_drive_file(action=delete)` 删除 → 再 `list_files` 确认消失
56
+ - `search_docs` 搜个人空间上传的 PDF 标题应能命中,`page_size`+`offset` 翻页两页无重叠
57
+
7
58
  ## [1.3.15] - 2026-05-31
8
59
 
9
60
  两条增强:文档建表格不再让 agent 猜 block_type;UAT 频繁重新授权的根因(良性 refresh_token 轮换竞态被误判为撤销)修掉。无 schema 变化、无新工具(仍 85)、无 breaking API。升级后重启 Claude Code / Codex 自动拉 v1.3.15。
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "feishu-user-plugin",
3
- "mcpName": "io.github.EthanQC/feishu-user-plugin",
4
- "version": "1.3.15",
3
+ "mcpName": "io.github.zhuzhen-team/feishu-user-plugin",
4
+ "version": "1.3.17",
5
5
  "description": "All-in-one Feishu MCP server + CLI tool for Claude Code / Codex / Cursor / scripts — 85 tools across 3 auth layers (cookie / app / OAuth). Send as you, read groups, manage docs / bitable / wiki / drive / calendar / tasks / OKR.",
6
6
  "main": "src/index.js",
7
7
  "bin": {
@@ -64,7 +64,7 @@
64
64
  "@modelcontextprotocol/sdk": "^1.29.0",
65
65
  "dotenv": "^16.4.7",
66
66
  "feishu-docx": "^0.7.0",
67
- "protobufjs": "^7.5.6"
67
+ "protobufjs": "^8.6.0"
68
68
  },
69
69
  "devDependencies": {
70
70
  "husky": "^9.1.7"
@@ -91,9 +91,8 @@ const BLOCK_LABELS = {
91
91
  if (process.env.LARK_USER_ACCESS_TOKEN) c.loadUAT();
92
92
 
93
93
  // --- Fetch blocks ---
94
- // NOTE: getDocBlocks fetches up to 500 blocks (no pagination). Docs longer
95
- // than that produce a truncated fixture / undercount. Out of scope for this
96
- // one-shot probe.
94
+ // getDocBlocks follows page_token pagination to completion (v1.3.17), so
95
+ // the fixture covers the whole document regardless of size.
97
96
  let blocks;
98
97
  try {
99
98
  const result = await c.getDocBlocks(rawDocId);
@@ -17,10 +17,23 @@ const SERVER_JSON = path.join(ROOT, 'server.json');
17
17
  const PKG = require(path.join(ROOT, 'package.json'));
18
18
  const { TOOLS } = require(path.join(ROOT, 'src', 'server'));
19
19
 
20
+ // Truncate a description to at most `limit` chars for the registry catalog.
21
+ // Cuts at the last word boundary (when one exists in the tail) and appends an
22
+ // ellipsis, so server.json never ends a description mid-word — the full text
23
+ // still reaches MCP clients at runtime via tools/list (this is catalog
24
+ // metadata only). PR #121 review.
25
+ function truncateForCatalog(s, limit = 200) {
26
+ if (s.length <= limit) return s;
27
+ const slice = s.slice(0, limit - 1); // leave room for the ellipsis
28
+ const lastSpace = slice.lastIndexOf(' ');
29
+ const cut = lastSpace > limit * 0.6 ? slice.slice(0, lastSpace) : slice;
30
+ return cut.replace(/[\s,;.:—-]+$/, '') + '…';
31
+ }
32
+
20
33
  function deriveToolEntry(t) {
21
34
  // Strip the "[Plugin]"/"[Cookie]"/etc category prefix from descriptions for compactness.
22
35
  const desc = (t.description || '').replace(/^\[[^\]]+\]\s*/, '');
23
- return { name: t.name, description: desc.split('\n')[0].slice(0, 200) };
36
+ return { name: t.name, description: truncateForCatalog(desc.split('\n')[0], 200) };
24
37
  }
25
38
 
26
39
  const existing = JSON.parse(fs.readFileSync(SERVER_JSON, 'utf8'));
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: feishu-user-plugin
3
- version: "1.3.15"
4
- description: "All-in-one Feishu MCP server + CLI tool — send messages as yourself (incl. batch_send), read group/P2P chats (auto-expands merge_forward), manage docs/tables/wiki (full CRUD)/drive, OKR (with progress writes), calendar (read+write), Tasks v2, multi-profile auto-switch, real-time WS events. v1.3.15: manage_doc_block gains a table create mode agents stop guessing the table block_type (tables are 31, not 40); the plugin builds the block_type=31 table, fills each cell by updating its auto-created text block (no stray empty blocks), resolves cells scoped (no whole-doc 500-block cap) and fails loud rather than silently dropping content. UAT refresh now self-heals a benign refresh_token rotation race: on invalid_grant it re-reads disk and adopts a peer's freshly-persisted valid token (snapshotting the sent token pre-await for the in-process hot-reload case) instead of false-flipping to UAT_REVOKED and pushing the user through a needless oauth re-consent."
3
+ version: "1.3.17"
4
+ description: "All-in-one Feishu MCP server + CLI tool — send messages as yourself (incl. batch_send), read group/P2P chats (auto-expands merge_forward), manage docs/tables/wiki (full CRUD)/drive, OKR (with progress writes), calendar (read+write), Tasks v2, multi-profile auto-switch, real-time WS events. v1.3.17: read-path completeness get_doc_blocks / read_doc_markdown now paginate past the old silent 500-block cap (hasMore / truncated / page_token / max_blocks); read_messages / read_p2p_messages / list_wiki_nodes / manage_bitable_record(search) gain page_token cursors and list_wiki_spaces fetches all spaces (was capped at 50); empty pages with has_more no longer stop pagination early. manage_doc_block table fills retry transient errors (code=2200) and report failedCells instead of aborting; manage_members(add) surfaces not-existed / pending-approval ids; image/file block creation retries and names orphaned placeholders on failure. update_text_elements full-replace semantics emphasized. 85 tools + 9 prompts unchanged."
5
5
  allowed-tools: send_to_user, send_to_group, send_as_user, send_image_as_user, send_file_as_user, send_post_as_user, batch_send, send_card_as_user, search_contacts, create_p2p_chat, get_chat_info, get_user_info, get_login_status, list_profiles, switch_profile, manage_profile_hints, read_p2p_messages, list_user_chats, list_chats, read_messages, search_messages, send_message_as_bot, reply_message, forward_message, delete_message, update_message, add_reaction, delete_reaction, pin_message, create_group, update_group, list_members, manage_members, search_docs, read_doc, get_doc_blocks, create_doc, manage_doc_block, read_doc_markdown, manage_bitable_app, manage_bitable_table, manage_bitable_field, manage_bitable_view, manage_bitable_record, upload_bitable_attachment, list_wiki_spaces, search_wiki, list_wiki_nodes, get_wiki_node, create_wiki_node, update_wiki_node, move_wiki_node, copy_wiki_node, delete_wiki_node, list_files, create_folder, upload_drive_file, manage_drive_file, upload_image, upload_file, download_message_resource, download_doc_image, list_user_okrs, get_okrs, list_okr_periods, create_okr_progress_record, list_okr_progress_records, delete_okr_progress_record, list_calendars, list_calendar_events, get_calendar_event, create_calendar_event, update_calendar_event, delete_calendar_event, respond_calendar_event, get_freebusy, list_tasks, get_task, create_task, update_task, complete_task, delete_task, manage_task_members, get_new_events, manage_ws_status
6
6
  user_invocable: true
7
7
  ---
@@ -23,6 +23,7 @@
23
23
  - `table` 形如 `{"rows":2,"columns":2,"cells":[["姓名","角色"],["Ann","PM"]]}`:`cells` 行优先,可省略或留空字符串表示空格
24
24
  - 插件内部建 `block_type=31` 表格、由飞书自动生成单元格(`block_type=32`)、逐格填内容——**不要自己用 `children` 拼 table block 或猜 block_type**(表格是 31 不是 40,猜错会报 `invalid_param`)
25
25
  2. 返回 `tableBlockId` + 行优先的 `cells` 单元格 ID 网格
26
+ 3. 若个别单元格填充失败(瞬态错误已自动重试),返回里会带 `failedCells:[{row,col,cellId,textBlockId?,reason}]`(row/col 0 起算)——逐格用 `manage_doc_block(action=update, block_id=<textBlockId>)` 补内容即可,**不必删表重建**
26
27
 
27
28
  ### 写决策树等纯文本结构
28
29
  表格只用于真正的二维数据;决策树、流程、缩进结构用 `children` 里的文本块(`block_type=2`)+ 代码块(`block_type=14`)表达即可,不依赖表格线。
@@ -37,3 +38,5 @@
37
38
  - 文档操作使用 Official API(需要 LARK_APP_ID)
38
39
  - 搜索结果受机器人权限范围限制
39
40
  - 建表格走 `manage_doc_block` 的 `table` 模式,不要手拼 block——表格 `block_type=31`、单元格 `32`
41
+ - `manage_doc_block(action=update)` 的 `update_text_elements` 是**整段替换**(非 patch / append):漏传的 element(加粗前缀、链接等)会永久丢失,改局部先 `get_doc_blocks` 读出原 elements、改完整组传回
42
+ - 读大文档:`get_doc_blocks` / `read_doc_markdown` 自动分页拉全量,`hasMore:false` 才代表拿到了完整块树
@@ -7,18 +7,24 @@
7
7
 
8
8
  ### 列出文件
9
9
  1. 用 `list_files` 列出文件夹内容
10
- - 不传 folder_token 则列出根目录
10
+ - 不传 folder_token 则列出根目录(配置了 UAT 时是**你的**"我的空间"根目录)
11
11
  - 传入 folder_token 则列出指定文件夹
12
+ - 结果多时用 `page_token`(来自上一页的 `nextPageToken`)翻页
12
13
 
13
14
  ### 创建文件夹
14
15
  1. 用 `create_folder` 创建新文件夹
15
16
  - 传入 name 和可选的 parent_token
16
17
 
18
+ ### 删除 / 移动 / 复制文件
19
+ 1. 用 `list_files` 找到目标的 token
20
+ 2. 用 `manage_drive_file` 操作(action=delete/move/copy,必传 type)
21
+
17
22
  ## 示例
18
23
  - `/drive list` — 列出根目录文件
19
24
  - `/drive list folderXxx` — 列出指定文件夹
20
25
  - `/drive create 项目资料` — 在根目录创建文件夹
26
+ - `/drive 删掉根目录里的 xxx.pdf` — list_files 找 token 后 manage_drive_file 删除
21
27
 
22
28
  ## 注意
23
29
  - 使用 Official API,需要 LARK_APP_ID
24
- - 文件列表受机器人权限范围限制
30
+ - `list_files` UAT-first(v1.3.16+):配置了 UAT 就以你的身份列文件(个人空间可见);否则以 bot 身份,只能看到共享给 bot 的文件夹。返回的 `viaUser` 标明视角归属
@@ -13,7 +13,7 @@
13
13
  2. 找到节点后可用 `read_doc` 读取其文档内容
14
14
 
15
15
  ### 浏览节点
16
- 1. 用 `list_wiki_nodes` 列出指定空间的节点树
16
+ 1. 用 `list_wiki_nodes` 列出指定空间的节点树(每页 50 个;`hasMore:true` 时把返回的 `pageToken` 传回 `page_token` 翻页)
17
17
  2. 可传 `parent_node_token` 浏览子节点
18
18
 
19
19
  ## 示例
@@ -116,7 +116,12 @@ module.exports = {
116
116
  }),
117
117
  label: 'searchRecords',
118
118
  });
119
- return { items: res.data.items || [], total: res.data.total, hasMore: res.data.has_more };
119
+ // pageToken accompanies hasMore (2026-06-07 audit) hasMore + total
120
+ // without the resume cursor stranded callers at the first page of a
121
+ // potentially thousands-row table.
122
+ const out = { items: res.data.items || [], total: res.data.total, hasMore: res.data.has_more };
123
+ if (res.data.page_token) out.pageToken = res.data.page_token;
124
+ return out;
120
125
  },
121
126
 
122
127
  async createBitableRecord(appToken, tableId, fields) {