feishu-user-plugin 1.3.12 → 1.3.13

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.12",
3
+ "version": "1.3.13",
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.12",
5
+ "version": "1.3.13",
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.12",
5
+ "version": "1.3.13",
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.13] - 2026-05-16
8
+
9
+ 紧急 patch — v1.3.12 release 后 Codex + Copilot PR #103 review 发现 1 P1 + 2 P2 + 5 polish,followup 又跑 5-agent 全仓 audit 找出 2 P1 (security) + 多个 doc/compliance 漂移。本版集中修复全部 issue + 把 fixture-based unit tests 拉进 CI gate。
10
+
11
+ > **包含 v1.3.12 全部能力**(4 个 architectural root cause 收口 + `search_messages` UAT-only 工具 + CLI 工具模式 + SEO 改造 + 工程质量 + 战略性微调,85 工具)+ 以下修复。建议跳过 v1.3.12 直接升 v1.3.13。
12
+
13
+ ### Security
14
+
15
+ - **oauth.js token leak(P1)**:`exchangeCode()` 之前会 `console.log('Token exchange raw response:', raw.slice(0, 500))` —— 完整 access_token + refresh_token 进 stderr。`saveToken()` 失败 fallback 路径还会 `console.error(' ${k}=${v}')` 把整个 token 字符串 dump 出来。改成只 log HTTP status + body 长度;fallback 用 `slice(0,6)…(N chars)` redact pattern(同 credentials.js migrate 风格)。
16
+
17
+ ### Fixed
18
+
19
+ - **P1 — UAT-success 路径错标 viaUser:false(影响 v1.3.12 全部 UAT 写工具)**:`src/auth/identity-state.js` withIdentityFallback UAT 成功路径返回 shallow clone of response,但漏 set `_viaUser: true`。15+ `_asUserOrApp` callsites (calendar/docs/bitable/wiki/okr/tasks/drive) 读 `res._viaUser` 决定显示,没设的话全部 v1.3.12 UAT-owned 写显示 `viaUser:false` + 无 fallbackWarning,用户误以为 bot 创建。Fix:shallow-clone 时加 `_viaUser: true`,加 test-identity-state 断言 pin contract。
20
+
21
+ - **P2 — credentials hot-reload 启动期空窗**:server.js main() 现在启动时调一次 `credMonitor.sync()`(在 verifyApp() 拿 officialClient 之后)。Pre-fix 第一次 sync 永远 silent baselining;server boot 跟 first tool call 之间用户跑 oauth 的话,会被错认为初始 baseline,hook 不 fire。
22
+
23
+ - **P2 — cookie rotation 不 hot-reload**:server.js 现注册 `onCookieChange` hook 把 userClient 设 null。Pre-fix monitor detect LARK_COOKIE 变化但 server.js 没 hook,rotation 后 cookie-based 工具 (send_to_user / search_contacts / get_login_status / send_as_user / batch_send) 继续用 stale cookie 直到重启。
24
+
25
+ - **read_messages via_user=true 错标 via='bot'**:`readMessagesWithFallback` 的 skipBot 分支默认 `via='bot'`,Path B (cookie 解析) 标 `via='contacts'` + reason='contacts_resolved_external'。via_user=true 显式调用混进了 contacts_resolved_external reason。Fix:handler 显式 pass `via: 'user'`;readMessagesWithFallback skipBot 分支按 `via === 'contacts'` 显式判断。
26
+
27
+ ### Changed
28
+
29
+ - **observability — _populateSenderNames 加 unresolved id log**:getUserById / getAppName 失败时 return null 而 **不** reject,原 Promise.allSettled rejection log 漏掉这种 case。现在每个 batch 后单独 log 未解析 ids: `sender name unresolved (cached null) for N id(s): ou_xxx, ...`(与 v1.3.12 的 negative-cache sentinel 配合)。
30
+
31
+ ### CI / Process
32
+
33
+ - **validate.yml 加 `npm test` + `check-changelog.js`**:之前 14 个 fixture-based unit tests 不在 PR gate 里,任何破坏它们的 PR 都能进 main;CHANGELOG section 缺失也不挡。现在两者都是 PR check 的一部分。
34
+ - **test-lark-desktop.js 接入 npm test**:原是孤儿 standalone script,现在 export run() 被 test-all.js require。
35
+
36
+ ### Docs
37
+
38
+ - CLAUDE.md 删除 stale "未实现:search_messages"(v1.3.12 已实装),换成"已删除"段(md ↔ wiki 双向同步 + Mermaid → 画板 都已删)。
39
+ - CLAUDE.md 工具大类计数 reconcile 到 85:Drive 5 → 4,加 "跨域 Uploads (3)" 行,"插件层 4" → "多 profile 3" + 实时事件 2。
40
+ - docs/REFACTOR-NOTES.md tools/ 子树补 tasks.js + events.js(v1.3.7 / v1.3.9 加但 doc 一直漏);smoke 契约 "当前 84" → "当前 85";events/ 子树第 layout 段已在 v1.3.12 加入。
41
+ - docs/TOOLS.md IM section 工具列表补 `search_messages`;Drive section 拆成 Drive 4 + Uploads 3 跟实际 src/tools/ 一致。
42
+ - docs/COMPARISON.md / CONTRIBUTING.md / .github/pull_request_template.md:84 → 85;COMPARISON.md "本仓:最新 v1.3.11" → v1.3.12。
43
+
44
+ ### 其他 polish
45
+
46
+ - docs/CLIENT-COMPAT.md(v1.3.12 加的):标题 "5 客户端" → "7 客户端";Tools 列 ✓ 84 → ✓ 85;`feishu-user-plugin-1.3.11.mcpb` 改成 version-agnostic placeholder。
47
+ - scripts/verify-app-name.js:错误 URL 插入实际 appId(之前是 `<appId>` 字面)。
48
+ - src/test-lru-cache.js:fix stale header 引用 `src/utils/lru-cache.js` → `src/utils.js`。
49
+
50
+ ### Test scenarios
51
+
52
+ - `npm test`:14 个 fixture-based test 全 pass(包含 v1.3.13 加的 `test-lark-desktop` wiring + identity-state `_viaUser=true` 断言)
53
+ - `node scripts/verify-app-name.js`:当前 APP self_manage scope 已开 → 输出 `OK — app name resolves to "Claude聊天助手"`;错误路径打印的修复 URL 含实际 cli_xxx appId
54
+ - 重启 Claude Code / Codex 后跑任何 UAT-owned 写工具(如 `create_doc` / `create_bitable` / `create_calendar_event` / `update_task`)→ 响应里 `viaUser:true`(而非 v1.3.12 的 `viaUser:false`)
55
+ - 跑 `npx feishu-user-plugin oauth` 后**不重启**,下次 `get_login_status` 立即 Valid(hot-reload 启动期空窗已修)
56
+ - 改 credentials.json 里的 LARK_COOKIE 字段后**不重启**,下次 cookie 工具如 `send_to_user` 会用新 cookie(onCookieChange hook 已注册)
57
+
7
58
  ## [1.3.12] - 2026-05-15
8
59
 
9
60
  主线:4 个 architectural root cause(A scope drift / B silent fallback / C LLM-unfriendly 数据 / D hot-reload 缺失)一次性收口 + 1 个新工具 `search_messages`(B.5 Protobuf 阶段二)+ CLI 工具模式(`tool` 子命令,复用 85 工具)+ SEO 改造(README h1 + repo description + 4 GitHub topics)+ 5 项工程质量(gitleaks 防 secret 误提交 / CHANGELOG 回填 v1.3.0-v1.3.2 / 客户端兼容矩阵 / 战略性微调 ×2)。工具数 84 → 85。
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "feishu-user-plugin",
3
3
  "mcpName": "io.github.EthanQC/feishu-user-plugin",
4
- "version": "1.3.12",
4
+ "version": "1.3.13",
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": {
@@ -51,7 +51,7 @@ async function main() {
51
51
  if (info.code === 99991672) {
52
52
  console.error('FAIL — code 99991672. The tenant-side scope `application:application:self_manage` is not granted.');
53
53
  console.error('Fix:');
54
- console.error(' 1. Open https://open.feishu.cn/app/<appId>/safe — "应用身份" tab');
54
+ console.error(` 1. Open https://open.feishu.cn/app/${appId}/safe — "应用身份" tab`);
55
55
  console.error(' 2. Add scope `application:application:self_manage` (marked 免审权限 — no admin review needed)');
56
56
  console.error(' 3. Save; no re-publish required');
57
57
  console.error(' 4. Re-run this script to confirm');
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: feishu-user-plugin
3
- version: "1.3.12"
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.12: search_messages tool (Protobuf phase 2 B.5, UAT-only), CLI tool mode (`tool list` / `tool help <name>` / `tool <name> '<json>'`), IdentityState state machine + credentials hot-reload (no-restart UAT reload), displayLabel + sender semantics pack for LLM consumption, WS owner PID liveness check, gitleaks secret scan."
3
+ version: "1.3.13"
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.13: search_messages tool (Protobuf phase 2, UAT-only), CLI tool mode (tool list / help / dispatch), IdentityState state machine + credentials hot-reload (UAT viaUser flag preserved across the 15+ write tools, no-restart UAT/cookie reload, startup-blind-window closed), displayLabel + sender semantics pack, WS owner PID liveness, gitleaks secret scan, oauth.js token-leak hardening, fixture unit tests pulled into CI gate."
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
  ---
@@ -150,7 +150,12 @@ async function withIdentityFallback({ client, uatFn, botFn, label }) {
150
150
  uatErr = e;
151
151
  }
152
152
  if (uatResp && uatResp.code === 0) {
153
- const data = { ...uatResp };
153
+ // Preserve the legacy _viaUser marker that 15+ _asUserOrApp callers read
154
+ // via `res._viaUser`. Without this flag, calendar/docs/bitable/wiki/okr/
155
+ // tasks/drive write tools labelled UAT-owned resources as viaUser:false,
156
+ // making users believe a bot created them. Caught by Codex review on
157
+ // PR #103 (P1 — set _viaUser on successful UAT results).
158
+ const data = { ...uatResp, _viaUser: true };
154
159
  return { data, via: 'uat', identity };
155
160
  }
156
161
  const cls = _classifyUatFailure(uatResp, uatErr);
@@ -199,11 +199,36 @@ class LarkOfficialClient {
199
199
  // dispatching N redundant API calls per read_messages on hot chats.
200
200
  // has(id)==true / get(id)==null lets _computeDisplayLabel fall back to
201
201
  // "(open_id)" exactly the same way as before.
202
+ //
203
+ // PR #103 Copilot followup: getUserById / getAppName return null on
204
+ // non-zero Feishu codes (e.g. 99991672, scope missing) WITHOUT rejecting,
205
+ // so the per-batch Promise.allSettled rejection log misses these. Log
206
+ // the ids that ended without a name as a separate stderr line so failure
207
+ // shape is observable regardless of whether the underlying lookup
208
+ // returned null or rejected.
209
+ const unresolvedUserIds = [];
202
210
  for (const id of unknownUserIds) {
203
- if (!this._userNameCache.has(id)) this._userNameCache.set(id, null);
211
+ if (!this._userNameCache.has(id) || this._userNameCache.get(id) === null) {
212
+ this._userNameCache.set(id, null);
213
+ unresolvedUserIds.push(id);
214
+ }
215
+ }
216
+ if (unresolvedUserIds.length) {
217
+ const sample = unresolvedUserIds.slice(0, 5).join(', ');
218
+ const tail = unresolvedUserIds.length > 5 ? ` (+${unresolvedUserIds.length - 5} more)` : '';
219
+ console.error(`[feishu-user-plugin] sender name unresolved (cached null) for ${unresolvedUserIds.length} id(s): ${sample}${tail}`);
204
220
  }
221
+ const unresolvedAppIds = [];
205
222
  for (const id of unknownAppIds) {
206
- if (!this._appNameCache.has(id)) this._appNameCache.set(id, null);
223
+ if (!this._appNameCache.has(id) || this._appNameCache.get(id) === null) {
224
+ this._appNameCache.set(id, null);
225
+ unresolvedAppIds.push(id);
226
+ }
227
+ }
228
+ if (unresolvedAppIds.length) {
229
+ const sample = unresolvedAppIds.slice(0, 5).join(', ');
230
+ const tail = unresolvedAppIds.length > 5 ? ` (+${unresolvedAppIds.length - 5} more)` : '';
231
+ console.error(`[feishu-user-plugin] app name unresolved (cached null) for ${unresolvedAppIds.length} id(s): ${sample}${tail}`);
207
232
  }
208
233
 
209
234
  // Step 4: populate senderName, isExternal, displayLabel
package/src/oauth.js CHANGED
@@ -163,10 +163,16 @@ async function exchangeCode(code) {
163
163
  body: JSON.stringify(body),
164
164
  });
165
165
  const raw = await tokenRes.text();
166
- console.log('Token exchange raw response:', raw.slice(0, 500));
166
+ // v1.3.13 security followup: don't log the full raw body — it contains the
167
+ // bare access_token + refresh_token. Log only the http status and a hint of
168
+ // success/failure; the parsed token never leaves this function except via
169
+ // saveToken (which writes the file with 0600 perms).
170
+ console.log(`Token exchange HTTP ${tokenRes.status} (body ${raw.length} bytes)`);
167
171
  let tokenData;
168
172
  try { tokenData = JSON.parse(raw); } catch (e) {
169
- throw new Error(`Response not JSON: ${raw.slice(0, 200)}`);
173
+ // Parse error path: redact body in the thrown message so an upstream
174
+ // log line doesn't accidentally surface tokens.
175
+ throw new Error(`Response not JSON (HTTP ${tokenRes.status}, ${raw.length} bytes): ${raw.slice(0, 100).replace(/[A-Za-z0-9._-]{40,}/g, '<redacted>')}`);
170
176
  }
171
177
  if (tokenData.error) {
172
178
  throw new Error(`${tokenData.error}: ${tokenData.error_description}`);
@@ -199,8 +205,17 @@ function saveToken(tokenData) {
199
205
  if (ok) console.log(`Tokens written to ${profileLabel}`);
200
206
  }
201
207
  if (!ok) {
202
- console.error('WARNING: Tokens could not be saved. Copy them manually:');
203
- for (const [k, v] of Object.entries(updates)) console.error(` ${k}=${v}`);
208
+ // v1.3.13 security followup: never dump full token bytes to stderr.
209
+ // Caller can find them by re-running OAuth or reading the credentials
210
+ // file. Show only the field shape so user knows what fields exist.
211
+ console.error('WARNING: Tokens could not be saved automatically. Re-run `npx feishu-user-plugin oauth` after fixing the config path, or check that `~/.feishu-user-plugin/credentials.json` is writable.');
212
+ console.error('Fields that would have been written (values redacted):');
213
+ for (const [k, v] of Object.entries(updates)) {
214
+ const preview = typeof v === 'string' && v.length > 0
215
+ ? `${v.slice(0, 6)}…(${v.length} chars)`
216
+ : '<empty>';
217
+ console.error(` ${k}=${preview}`);
218
+ }
204
219
  }
205
220
  }
206
221
 
package/src/server.js CHANGED
@@ -332,6 +332,17 @@ credMonitor.onUatChange((env) => {
332
332
  console.error('[feishu-user-plugin] UAT reloaded from credentials.json (no restart needed)');
333
333
  });
334
334
 
335
+ credMonitor.onCookieChange(() => {
336
+ // Cookie rotation: null the LarkUserClient singleton so the next
337
+ // getUserClient() call rebuilds it with the fresh cookie from env.
338
+ // Without this, cookie-based tools (send_to_user / search_contacts /
339
+ // get_login_status / send_as_user / batch_send) keep using the stale
340
+ // cookie until restart. PR #103 Codex P2 followup.
341
+ if (!userClient) return;
342
+ userClient = null;
343
+ console.error('[feishu-user-plugin] cookie rotation detected — userClient nulled, rebuilds on next tool call');
344
+ });
345
+
335
346
  credMonitor.onCacheInvalidate(() => {
336
347
  if (officialClient) identityState.invalidateIdentity(officialClient);
337
348
  });
@@ -568,6 +579,11 @@ async function main() {
568
579
  }
569
580
  }
570
581
 
582
+ // Baseline credMonitor at startup so any credential changes between server
583
+ // boot and the first tool call fire hooks instead of being silently absorbed
584
+ // by the first sync()'s baselining branch. PR #103 Codex P2 followup.
585
+ credMonitor.sync();
586
+
571
587
  // --- Real-time events (v1.3.9 — owner-arbitrated) ---
572
588
  if (hasApp) {
573
589
  _claimAndStart().catch((e) => {
package/src/test-all.js CHANGED
@@ -363,5 +363,6 @@ main().catch(console.error).finally(() => {
363
363
  process.exitCode = 1;
364
364
  });
365
365
  require('./test-cli-tool').run();
366
+ require('./test-lark-desktop').run();
366
367
  require('./test-display-label'); // standalone — runs on require, exits non-zero on fail
367
368
  });
@@ -86,6 +86,11 @@ async function run() {
86
86
  assert.equal(r1.data.ok, undefined, 'should pass through fields, not double-wrap');
87
87
  assert.equal(r1.data.data.ok, true);
88
88
  assert.equal(r1.viaReason, undefined, 'no fallback → no via_reason');
89
+ // PR #103 Codex P1 followup: UAT success must set the legacy _viaUser=true
90
+ // marker so 15+ _asUserOrApp callsites (calendar/docs/bitable/wiki/okr/tasks
91
+ // /drive) report viaUser:true. Without this flag downstream code thinks the
92
+ // resource was created by the bot.
93
+ assert.equal(r1.data._viaUser, true, 'UAT success path must mark _viaUser=true on response');
89
94
 
90
95
  // --- 8. withIdentityFallback: UAT returns 20064 → bot fallback, identity refined ---
91
96
  let botRan = false;
@@ -298,3 +298,4 @@ function run() {
298
298
  if (require.main === module) {
299
299
  run();
300
300
  }
301
+ module.exports = { run };
@@ -1,4 +1,4 @@
1
- // src/test-lru-cache.js — unit test for src/utils/lru-cache.js.
1
+ // src/test-lru-cache.js — unit test for the LRUCache class exported by src/utils.js.
2
2
  //
3
3
  // Replaces the v1.3.12 `new Map()` _userNameCache / _appNameCache. Pre-fix the
4
4
  // caches grew unboundedly across the server's lifetime (one entry per unique