feishu-user-plugin 1.3.1 → 1.3.2

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,7 +1,7 @@
1
1
  {
2
2
  "name": "feishu-user-plugin",
3
- "version": "1.2.0",
4
- "description": "All-in-one Feishu plugin for Claude Code — send messages as yourself, read chats, manage docs/tables/wiki. 33 tools + 9 skills, 3 auth layers.",
3
+ "version": "1.3.2",
4
+ "description": "All-in-one Feishu plugin for Claude Code — send messages as yourself (incl. real @-mentions), read chats, manage docs/tables/wiki. 66 tools + 9 skills, 3 auth layers.",
5
5
  "author": {
6
6
  "name": "EthanQC"
7
7
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feishu-user-plugin",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
4
4
  "description": "All-in-one Feishu plugin for Claude Code & Codex — messaging, docs, bitable, wiki, drive. 66 tools + 9 skills, 3 auth layers.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/proto/lark.proto CHANGED
@@ -140,6 +140,19 @@ message RichText {
140
140
  repeated string elementIds = 1;
141
141
  optional string innerText = 2;
142
142
  optional RichTextElements elements = 3;
143
+ // Index fields: each "*Ids" is a list of elemIds (pointing into the dictionary)
144
+ // that the server needs to register as special-type elements. Discovered from
145
+ // Feishu Web bundle — atIds=6 is what makes @-mentions actually notify.
146
+ repeated string imageIds = 5;
147
+ repeated string atIds = 6;
148
+ repeated string anchorIds = 7;
149
+ repeated string i18nIds = 8;
150
+ repeated string mediaIds = 9;
151
+ repeated string docsIds = 10;
152
+ repeated string interactiveIds = 11;
153
+ repeated string mentionIds = 12;
154
+ optional int32 version = 13;
155
+ repeated string atUserGroupIds = 14;
143
156
  }
144
157
 
145
158
  message RichTextElements {
@@ -167,6 +180,20 @@ message TextProperty {
167
180
  optional string content = 1;
168
181
  }
169
182
 
183
+ // For AT (@-mention) elements. Both fields are required in the real schema;
184
+ // `content` is marked deprecated but still required on the wire.
185
+ message AtProperty {
186
+ optional string userId = 1;
187
+ optional string content = 2;
188
+ }
189
+
190
+ // For A (hyperlink) elements.
191
+ message AnchorProperty {
192
+ optional string href = 1;
193
+ optional string content = 2;
194
+ optional string textContent = 3;
195
+ }
196
+
170
197
  // --- Chat Operations ---
171
198
 
172
199
  // Create P2P chat (cmd=13)
@@ -14,7 +14,8 @@ All-in-one Feishu plugin for Claude Code with three auth layers:
14
14
  - `send_as_user` — Send text to any chat by ID, supports reply threading (root_id/parent_id)
15
15
  - `send_image_as_user` — Send image (requires image_key from `upload_image`)
16
16
  - `send_file_as_user` — Send file (requires file_key from `upload_file`)
17
- - `send_post_as_user` — Send rich text with title + formatted paragraphs
17
+ - `send_post_as_user` — Send rich text with title + formatted paragraphs. Elements: `{tag:"text"}`, `{tag:"a",href,text}`, `{tag:"at",userId,name}`. **@-mentions trigger real notifications** (fixed by registering AT element IDs in RichText.atIds field 6 — reverse-engineered from Feishu Web bundle's AtProperty + RichText schemas).
18
+ - `send_as_user` / `send_to_user` / `send_to_group` — plain text sends now accept optional `ats: [{userId, name}]`; the text must contain the `@<name>` marker for each entry. The marker is spliced into a real AT element so the mentioned user is notified. Identity is the cookie user (not bot).
18
19
  - `send_sticker_as_user` — Send sticker/emoji
19
20
  - `send_audio_as_user` — Send audio message
20
21
 
@@ -28,7 +29,7 @@ All-in-one Feishu plugin for Claude Code with three auth layers:
28
29
  ### User OAuth UAT Tools (P2P chat reading + user-identity creation)
29
30
  - `read_p2p_messages` — Read P2P (direct message) chat history. chat_id accepts both numeric IDs (from create_p2p_chat) and oc_xxx format. Returns newest messages first by default.
30
31
  - `list_user_chats` — List group chats the user is in. Note: API only returns groups, not P2P. For P2P, use: `search_contacts` → `create_p2p_chat` → `read_p2p_messages`.
31
- - `create_doc` / `create_bitable` / `create_folder` **UAT-first**: creates resources as the user (not the app) when UAT with write scopes is available. Falls back to app token.
32
+ - **All docx + bitable + drive create/read/write tools are UAT-first**: when UAT is configured, every operation (create/edit/delete doc blocks, bitable tables/fields/views/records, drive folders) tries the user's token first and falls back to app token on failure. This keeps resources consistently owned by the user and avoids 403 errors when the app can't access user-created resources. Read-only tools (e.g. `read_doc`, `get_doc_blocks`, `list_bitable_tables`) are also UAT-first so user-owned resources remain readable.
32
33
 
33
34
  ### Official API Tools (app credentials)
34
35
  - `list_chats` / `read_messages` — Chat history (read_messages accepts chat name, oc_ ID, or numeric ID; auto-resolves via bot's group list → im.chat.search → search_contacts). **Auto-falls back to UAT for external groups the bot cannot access.** Returns newest messages first by default. Messages include sender names.
@@ -59,7 +60,9 @@ All-in-one Feishu plugin for Claude Code with three auth layers:
59
60
  - Send text as yourself → `send_to_user` or `send_to_group`
60
61
  - Send image → `upload_image` → `send_image_as_user`
61
62
  - Send file → `upload_file` → `send_file_as_user`
62
- - Send rich content → `send_post_as_user` (formatted text with links, @mentions)
63
+ - Send rich content → `send_post_as_user` (formatted text + links + real @-mentions via `{tag:"at",userId,name}`)
64
+ - Send text with @-mentions (plain text) → `send_as_user` / `send_to_user` / `send_to_group` with `ats:[{userId,name}]` + text containing `@<name>` markers
65
+ - Bot-identity @-mention alternative → `send_message_as_bot` with `<at user_id="ou_xxx">Name</at>` inline in content text
63
66
  - Reply as user in thread → `send_as_user` with root_id
64
67
  - Reply as bot → `reply_message` (official API)
65
68
 
@@ -310,12 +313,29 @@ NPM_TOKEN is stored as a GitHub repo secret.
310
313
 
311
314
  ### Syncing to team-skills
312
315
 
313
- After publishing, sync plugin assets to team-skills:
316
+ **IMPORTANT: team-skills 仓库禁止直接推送 main。所有变更必须走 PR。**
314
317
 
318
+ team-skills 推送规范:
319
+ 1. **创建 feature branch**: `git checkout -b fix/feishu-xxx` 或 `sync/feishu-v1.x.x`
320
+ 2. **提交变更并推送 branch**: `git push -u origin <branch-name>`
321
+ 3. **创建 PR 并设置 auto-merge**: `gh pr create --title "..." --body "..."` 然后 `gh pr merge <number> --auto --merge`
322
+ 4. **CI 通过后自动合并**: validate workflow 检查三方版本一致性,通过即自动 merge,无需手动操作
323
+ 5. **如 CI 失败**: 修复后 push 到同一 branch,CI 会重跑,通过后自动合并
324
+
325
+ 三方版本一致性规则:
326
+ - `plugins/feishu-user-plugin/.claude-plugin/plugin.json` 的 `version`
327
+ - `plugins/feishu-user-plugin/skills/feishu-user-plugin/SKILL.md` frontmatter 的 `version`
328
+ - `plugins/feishu-user-plugin/README.md` 更新日志里第一个 `### vX.Y.Z` 标题
329
+ - 这三个版本号必须相同,否则 CI 会失败。每次 npm 发包后,team-skills 的版本号也要同步更新。
330
+
331
+ 同步内容(每次发版后执行):
315
332
  ```bash
316
- # From the feishu-user-plugin repo:
317
- cp -r skills/ /path/to/team-skills/plugins/feishu-user-plugin/skills/
318
- cp .claude-plugin/plugin.json /path/to/team-skills/plugins/feishu-user-plugin/.claude-plugin/
333
+ # 1. 同步 skills + plugin.json
334
+ cp CLAUDE.md skills/feishu-user-plugin/references/CLAUDE.md
335
+ cp -r skills/ /Users/abble/team-skills/plugins/feishu-user-plugin/skills/
336
+ cp .claude-plugin/plugin.json /Users/abble/team-skills/plugins/feishu-user-plugin/.claude-plugin/
337
+ # 2. 手动更新 team-skills 的 README.md(工具数、更新日志)和 SKILL.md(version + allowed-tools)
338
+ # 3. 走 PR 流程推送
319
339
  # Do NOT copy .mcp.json — team-skills plugin should not have one
320
340
  ```
321
341
 
@@ -340,11 +360,12 @@ When making ANY code change (new tools, bug fixes, features), update ALL of thes
340
360
 
341
361
  **同步命令(每次发版后执行):**
342
362
  ```bash
343
- # 1. 同步 skills
363
+ # 1. 同步 skills + plugin.json
344
364
  cp CLAUDE.md skills/feishu-user-plugin/references/CLAUDE.md
345
365
  cp -r skills/ /Users/abble/team-skills/plugins/feishu-user-plugin/skills/
346
- # 2. 手动更新 team-skills README(工具数、功能列表、更新日志)
347
- # 3. 提交并推送两个仓库
366
+ cp .claude-plugin/plugin.json /Users/abble/team-skills/plugins/feishu-user-plugin/.claude-plugin/
367
+ # 2. 手动更新 team-skills README(工具数、功能列表、更新日志)+ SKILL.md(version + allowed-tools)
368
+ # 3. 走 PR 流程推送 team-skills(禁止直接推 main)
348
369
  ```
349
370
 
350
371
  ### Keeping ROADMAP.md up to date
@@ -392,7 +413,8 @@ Steps:
392
413
  1. Copy CLAUDE.md to skill reference: `cp CLAUDE.md skills/feishu-user-plugin/references/CLAUDE.md`
393
414
  2. Sync to team-skills repo: `cp -r skills/ /Users/abble/team-skills/plugins/feishu-user-plugin/skills/`
394
415
  3. Also sync plugin.json: `cp .claude-plugin/plugin.json /Users/abble/team-skills/plugins/feishu-user-plugin/.claude-plugin/`
395
- 4. Commit and push both repos
416
+ 4. Update SKILL.md version + allowed-tools, README.md changelog + tool count
417
+ 5. **走 PR 流程**(创建 branch → push → PR → 等 CI 通过 → merge),禁止直接推 main
396
418
 
397
419
  ### Testing a tool
398
420
  - For Official API tools: can test directly via MCP tool call or standalone script using `readCredentials()` from `src/config.js`
package/src/client.js CHANGED
@@ -200,14 +200,65 @@ class LarkUserClient {
200
200
 
201
201
  // --- Send Text Message ---
202
202
 
203
+ // Supports inline @mentions via the `ats` param:
204
+ // ats: [{ userId: 'ou_xxx', name: 'Alice' }]
205
+ // The text should contain the mention markers (defaults to `@Alice` substrings,
206
+ // matched in order). If `text` already contains the @Name substrings, they're
207
+ // found in order and spliced into rich-text AT elements.
203
208
  async sendMessage(chatId, text, opts = {}) {
204
- const elemId = generateCid();
205
- const textPropBuf = this._encode('TextProperty', { content: text });
209
+ const { ats } = opts;
210
+ if (!Array.isArray(ats) || ats.length === 0) {
211
+ // Fast path: plain text, single TEXT element.
212
+ const elemId = generateCid();
213
+ const textPropBuf = this._encode('TextProperty', { content: text });
214
+ return this._sendMsg(MsgType.TEXT, chatId, {
215
+ richText: {
216
+ elementIds: [elemId],
217
+ innerText: text,
218
+ elements: { dictionary: { [elemId]: { tag: 1, property: textPropBuf } } },
219
+ },
220
+ }, opts);
221
+ }
222
+
223
+ // Build rich-text segments: split `text` by each at's display marker and
224
+ // weave AT elements in between text elements. Each `ats[i]` is consumed
225
+ // in order from the remaining text.
226
+ const elementIds = [];
227
+ const atIds = [];
228
+ const dictionary = {};
229
+ let remaining = text;
230
+ for (const at of ats) {
231
+ if (!at.userId) throw new Error('sendMessage: each at entry requires userId');
232
+ const display = at.marker || (at.name ? '@' + at.name : '@' + at.userId);
233
+ const idx = remaining.indexOf(display);
234
+ if (idx === -1) throw new Error(`sendMessage: marker "${display}" not found in text`);
235
+ const before = remaining.slice(0, idx);
236
+ if (before) {
237
+ const id = generateCid();
238
+ elementIds.push(id);
239
+ dictionary[id] = { tag: 1, property: this._encode('TextProperty', { content: before }) };
240
+ }
241
+ const atId = generateCid();
242
+ elementIds.push(atId);
243
+ atIds.push(atId);
244
+ dictionary[atId] = {
245
+ tag: 5,
246
+ property: this._encode('AtProperty', { userId: at.userId, content: display }),
247
+ };
248
+ remaining = remaining.slice(idx + display.length);
249
+ }
250
+ if (remaining) {
251
+ const id = generateCid();
252
+ elementIds.push(id);
253
+ dictionary[id] = { tag: 1, property: this._encode('TextProperty', { content: remaining }) };
254
+ }
255
+
206
256
  return this._sendMsg(MsgType.TEXT, chatId, {
207
257
  richText: {
208
- elementIds: [elemId],
258
+ elementIds,
209
259
  innerText: text,
210
- elements: { dictionary: { [elemId]: { tag: 1, property: textPropBuf } } },
260
+ elements: { dictionary },
261
+ atIds,
211
262
  },
212
263
  }, opts);
213
264
  }
@@ -240,26 +291,43 @@ class LarkUserClient {
240
291
 
241
292
  async sendPost(chatId, title, paragraphs, opts = {}) {
242
293
  const elementIds = [];
294
+ const atIds = [];
295
+ const anchorIds = [];
243
296
  const dictionary = {};
297
+ const paraTexts = [];
244
298
 
245
299
  for (let i = 0; i < paragraphs.length; i++) {
246
300
  const para = paragraphs[i];
301
+ const paraTextParts = [];
247
302
  for (const elem of para) {
248
303
  const elemId = generateCid();
249
304
  elementIds.push(elemId);
250
305
 
251
306
  if (elem.tag === 'text') {
252
- const propBuf = this._encode('TextProperty', { content: elem.text });
307
+ const t = elem.text || '';
308
+ const propBuf = this._encode('TextProperty', { content: t });
253
309
  dictionary[elemId] = { tag: 1, property: propBuf };
310
+ paraTextParts.push(t);
254
311
  } else if (elem.tag === 'at') {
255
- const propBuf = this._encode('TextProperty', { content: elem.userId });
312
+ if (!elem.userId) throw new Error('sendPost: {tag:"at"} requires userId');
313
+ const displayName = elem.name || elem.userName || elem.text || elem.userId;
314
+ const display = displayName.startsWith('@') ? displayName : `@${displayName}`;
315
+ const propBuf = this._encode('AtProperty', { userId: elem.userId, content: display });
256
316
  dictionary[elemId] = { tag: 5, property: propBuf };
317
+ atIds.push(elemId);
318
+ paraTextParts.push(display);
257
319
  } else if (elem.tag === 'a') {
258
- // Link element: content stores the URL, display text goes through innerText
259
- const propBuf = this._encode('TextProperty', { content: elem.href || elem.text || '' });
320
+ const href = elem.href || '';
321
+ const label = elem.text || href;
322
+ const propBuf = this._encode('AnchorProperty', { href, content: label, textContent: label });
260
323
  dictionary[elemId] = { tag: 6, property: propBuf };
324
+ anchorIds.push(elemId);
325
+ paraTextParts.push(label);
326
+ } else {
327
+ throw new Error(`sendPost: unknown element tag "${elem.tag}" (supported: text, at, a)`);
261
328
  }
262
329
  }
330
+ paraTexts.push(paraTextParts.join(''));
263
331
  // Insert newline element between paragraphs
264
332
  if (i < paragraphs.length - 1) {
265
333
  const nlId = generateCid();
@@ -269,10 +337,13 @@ class LarkUserClient {
269
337
  }
270
338
  }
271
339
 
272
- const innerText = paragraphs.map(p => p.map(e => e.text || '').join('')).join('\n');
340
+ const innerText = paraTexts.join('\n');
341
+ const richText = { elementIds, innerText, elements: { dictionary } };
342
+ if (atIds.length > 0) richText.atIds = atIds;
343
+ if (anchorIds.length > 0) richText.anchorIds = anchorIds;
273
344
  return this._sendMsg(MsgType.POST, chatId, {
274
345
  title: title || '',
275
- richText: { elementIds, innerText, elements: { dictionary } },
346
+ richText,
276
347
  }, opts);
277
348
  }
278
349
 
package/src/index.js CHANGED
@@ -144,12 +144,17 @@ const TOOLS = [
144
144
  // ========== User Identity — Send Messages ==========
145
145
  {
146
146
  name: 'send_as_user',
147
- description: '[User Identity] Send a text message as the logged-in Feishu user. Supports reply threading.',
147
+ description: '[User Identity] Send a text message as the logged-in Feishu user. Supports reply threading and real @-mentions (triggers push notifications).',
148
148
  inputSchema: {
149
149
  type: 'object',
150
150
  properties: {
151
151
  chat_id: { type: 'string', description: 'Target chat ID (numeric)' },
152
- text: { type: 'string', description: 'Message text' },
152
+ text: { type: 'string', description: 'Message text. If `ats` is provided, include the display marker for each @ in this text (default marker is `@<name>`).' },
153
+ ats: {
154
+ type: 'array',
155
+ description: 'Optional @-mentions. Each entry: {userId: "ou_xxx", name: "DisplayName"}. The text must contain each @<name> marker in order — it gets spliced into a real AT element so the mentioned user receives a notification.',
156
+ items: { type: 'object', properties: { userId: { type: 'string' }, name: { type: 'string' }, marker: { type: 'string' } } },
157
+ },
153
158
  root_id: { type: 'string', description: 'Thread root message ID (for reply, optional)' },
154
159
  parent_id: { type: 'string', description: 'Parent message ID (for nested reply, optional)' },
155
160
  },
@@ -164,6 +169,11 @@ const TOOLS = [
164
169
  properties: {
165
170
  user_name: { type: 'string', description: 'Recipient name (Chinese or English)' },
166
171
  text: { type: 'string', description: 'Message text' },
172
+ ats: {
173
+ type: 'array',
174
+ description: 'Optional @-mentions. Same format as send_as_user.ats: [{userId, name}]. Text must contain the `@<name>` marker for each entry.',
175
+ items: { type: 'object', properties: { userId: { type: 'string' }, name: { type: 'string' }, marker: { type: 'string' } } },
176
+ },
167
177
  },
168
178
  required: ['user_name', 'text'],
169
179
  },
@@ -176,6 +186,11 @@ const TOOLS = [
176
186
  properties: {
177
187
  group_name: { type: 'string', description: 'Group chat name' },
178
188
  text: { type: 'string', description: 'Message text' },
189
+ ats: {
190
+ type: 'array',
191
+ description: 'Optional @-mentions that trigger real notifications. Each entry: {userId, name}. Text must contain `@<name>` marker for each entry.',
192
+ items: { type: 'object', properties: { userId: { type: 'string' }, name: { type: 'string' }, marker: { type: 'string' } } },
193
+ },
179
194
  },
180
195
  required: ['group_name', 'text'],
181
196
  },
@@ -222,7 +237,7 @@ const TOOLS = [
222
237
  },
223
238
  {
224
239
  name: 'send_post_as_user',
225
- description: '[User Identity] Send a rich text (POST) message with title and formatted paragraphs.',
240
+ description: '[User Identity] Send a rich text (POST) message with title and formatted paragraphs. Supports real @-mentions that trigger notifications.',
226
241
  inputSchema: {
227
242
  type: 'object',
228
243
  properties: {
@@ -230,7 +245,7 @@ const TOOLS = [
230
245
  title: { type: 'string', description: 'Post title (optional)' },
231
246
  paragraphs: {
232
247
  type: 'array',
233
- description: 'Array of paragraphs. Each paragraph is an array of elements: {tag:"text",text:"..."} or {tag:"a",href:"...",text:"..."} or {tag:"at",userId:"..."}',
248
+ description: 'Array of paragraphs. Each paragraph is an array of elements:\n• {tag:"text",text:"..."} plain text\n• {tag:"a",href:"https://...",text:"display"} hyperlink\n• {tag:"at",userId:"ou_xxx",name:"Display Name"} — real @-mention (triggers notification)',
234
249
  items: { type: 'array', items: { type: 'object' } },
235
250
  },
236
251
  root_id: { type: 'string', description: 'Thread root message ID (optional)' },
@@ -674,13 +689,13 @@ const TOOLS = [
674
689
  // ========== IM — Bot Send / Edit / Delete ==========
675
690
  {
676
691
  name: 'send_message_as_bot',
677
- description: '[Official API] Send a message as the bot to any chat. Supports text, post, interactive, etc.',
692
+ description: '[Official API] Send a message as the bot to any chat. Supports text, post, interactive, etc. This is the reliable path for @-mentions: include `<at user_id="ou_xxx">Name</at>` inline in text content and Feishu resolves it to a real @-notification.',
678
693
  inputSchema: {
679
694
  type: 'object',
680
695
  properties: {
681
696
  chat_id: { type: 'string', description: 'Target chat_id (oc_xxx) or open_id' },
682
697
  msg_type: { type: 'string', description: 'Message type: text, post, image, interactive, etc.', enum: ['text', 'post', 'image', 'interactive', 'share_chat', 'share_user', 'audio', 'media', 'file', 'sticker'] },
683
- content: { description: 'Message content (string or object, auto-serialized). For text: {"text":"hello"}' },
698
+ content: { description: 'Message content (string or object, auto-serialized). Plain text: {"text":"hello"}. Text with @-mention: {"text":"<at user_id=\\"ou_xxx\\">Alice</at> hi"} — the inline tag becomes a real @-notification.' },
684
699
  },
685
700
  required: ['chat_id', 'msg_type', 'content'],
686
701
  },
@@ -1008,7 +1023,7 @@ async function handleTool(name, args) {
1008
1023
 
1009
1024
  case 'send_as_user': {
1010
1025
  const c = await getUserClient();
1011
- const r = await c.sendMessage(args.chat_id, args.text, { rootId: args.root_id, parentId: args.parent_id });
1026
+ const r = await c.sendMessage(args.chat_id, args.text, { rootId: args.root_id, parentId: args.parent_id, ats: args.ats });
1012
1027
  return sendResult(r, `Text sent as user to ${args.chat_id}`);
1013
1028
  }
1014
1029
  case 'send_to_user': {
@@ -1023,7 +1038,7 @@ async function handleTool(name, args) {
1023
1038
  const user = users[0];
1024
1039
  const chatId = await c.createChat(user.id);
1025
1040
  if (!chatId) return text(`Failed to create chat with ${user.title}`);
1026
- const r = await c.sendMessage(chatId, args.text);
1041
+ const r = await c.sendMessage(chatId, args.text, { ats: args.ats });
1027
1042
  return sendResult(r, `Text sent to ${user.title} (chat: ${chatId})`);
1028
1043
  }
1029
1044
  case 'send_to_group': {
@@ -1036,7 +1051,7 @@ async function handleTool(name, args) {
1036
1051
  return text(`Multiple groups match "${args.group_name}":\n${candidates}\nUse search_contacts to find the exact group, then send_as_user with the ID.`);
1037
1052
  }
1038
1053
  const group = groups[0];
1039
- const r = await c.sendMessage(group.id, args.text);
1054
+ const r = await c.sendMessage(group.id, args.text, { ats: args.ats });
1040
1055
  return sendResult(r, `Text sent to group "${group.title}" (${group.id})`);
1041
1056
  }
1042
1057
 
@@ -1222,31 +1237,17 @@ async function handleTool(name, args) {
1222
1237
  return json(await getOfficialClient().getDocBlocks(args.document_id));
1223
1238
  case 'create_doc': {
1224
1239
  const official = getOfficialClient();
1225
- if (official.hasUAT) {
1226
- try {
1227
- const result = await official.createDocAsUser(args.title, args.folder_id);
1228
- return text(`Document created (as user): ${result.documentId}`);
1229
- } catch (e) {
1230
- console.error(`[feishu-user-plugin] UAT createDoc failed, falling back to app: ${e.message}`);
1231
- }
1232
- }
1233
- return text(`Document created: ${(await official.createDoc(args.title, args.folder_id)).documentId}`);
1240
+ const ownership = official.hasUAT ? ' (as user)' : '';
1241
+ return text(`Document created${ownership}: ${(await official.createDoc(args.title, args.folder_id)).documentId}`);
1234
1242
  }
1235
1243
 
1236
1244
  // --- Official API: Bitable ---
1237
1245
 
1238
1246
  case 'create_bitable': {
1239
1247
  const official = getOfficialClient();
1240
- if (official.hasUAT) {
1241
- try {
1242
- const r = await official.createBitableAsUser(args.name, args.folder_id);
1243
- return text(`Bitable created (as user): ${r.appToken}\nURL: ${r.url || ''}`);
1244
- } catch (e) {
1245
- console.error(`[feishu-user-plugin] UAT createBitable failed, falling back to app: ${e.message}`);
1246
- }
1247
- }
1248
+ const ownership = official.hasUAT ? ' (as user)' : '';
1248
1249
  const r = await official.createBitable(args.name, args.folder_id);
1249
- return text(`Bitable created: ${r.appToken}\nURL: ${r.url || ''}`);
1250
+ return text(`Bitable created${ownership}: ${r.appToken}\nURL: ${r.url || ''}`);
1250
1251
  }
1251
1252
  case 'list_bitable_tables':
1252
1253
  return json(await getOfficialClient().listBitableTables(args.app_token));
@@ -1298,14 +1299,8 @@ async function handleTool(name, args) {
1298
1299
  return json(await getOfficialClient().listFiles(args.folder_token));
1299
1300
  case 'create_folder': {
1300
1301
  const official = getOfficialClient();
1301
- if (official.hasUAT) {
1302
- try {
1303
- return text(`Folder created (as user): ${(await official.createFolderAsUser(args.name, args.parent_token)).token}`);
1304
- } catch (e) {
1305
- console.error(`[feishu-user-plugin] UAT createFolder failed, falling back to app: ${e.message}`);
1306
- }
1307
- }
1308
- return text(`Folder created: ${(await official.createFolder(args.name, args.parent_token)).token}`);
1302
+ const ownership = official.hasUAT ? ' (as user)' : '';
1303
+ return text(`Folder created${ownership}: ${(await official.createFolder(args.name, args.parent_token)).token}`);
1309
1304
  }
1310
1305
 
1311
1306
  // --- Official API: Contact ---
package/src/official.js CHANGED
@@ -101,6 +101,37 @@ class LarkOfficialClient {
101
101
  return data;
102
102
  }
103
103
 
104
+ // Generic UAT REST helper. Returns parsed JSON ({code, msg, data}).
105
+ async _uatREST(method, path, { body, query } = {}) {
106
+ const qs = query ? '?' + new URLSearchParams(query).toString() : '';
107
+ const url = 'https://open.feishu.cn' + path + qs;
108
+ return this._withUAT(async (uat) => {
109
+ const headers = { 'Authorization': `Bearer ${uat}` };
110
+ const init = { method, headers };
111
+ if (body !== undefined) {
112
+ headers['content-type'] = 'application/json';
113
+ init.body = JSON.stringify(body);
114
+ }
115
+ const res = await fetch(url, init);
116
+ return res.json();
117
+ });
118
+ }
119
+
120
+ // Try UAT first (for resources likely owned by the user), fall back to app SDK on failure.
121
+ // Returns SDK-shaped {code, msg, data}. Both paths yield the same shape.
122
+ async _asUserOrApp({ uatPath, method = 'GET', body, query, sdkFn, label }) {
123
+ if (this.hasUAT) {
124
+ try {
125
+ const data = await this._uatREST(method, uatPath, { body, query });
126
+ if (data.code === 0) return data;
127
+ console.error(`[feishu-user-plugin] ${label} as user failed (${data.code}: ${data.msg}), retrying as app`);
128
+ } catch (err) {
129
+ console.error(`[feishu-user-plugin] ${label} as user threw (${err.message}), retrying as app`);
130
+ }
131
+ }
132
+ return this._safeSDKCall(sdkFn, label);
133
+ }
134
+
104
135
  async listChatsAsUser({ pageSize = 20, pageToken } = {}) {
105
136
  const params = new URLSearchParams({ page_size: String(pageSize) });
106
137
  if (pageToken) params.set('page_token', pageToken);
@@ -371,61 +402,77 @@ class LarkOfficialClient {
371
402
  }
372
403
 
373
404
  async readDoc(documentId) {
374
- const res = await this._safeSDKCall(
375
- () => this.client.docx.document.rawContent({ path: { document_id: documentId }, params: { lang: 0 } }),
376
- 'readDoc'
377
- );
405
+ const res = await this._asUserOrApp({
406
+ uatPath: `/open-apis/docx/v1/documents/${documentId}/raw_content`,
407
+ query: { lang: '0' },
408
+ sdkFn: () => this.client.docx.document.rawContent({ path: { document_id: documentId }, params: { lang: 0 } }),
409
+ label: 'readDoc',
410
+ });
378
411
  return { content: res.data.content };
379
412
  }
380
413
 
381
414
  async createDoc(title, folderId) {
382
- const res = await this._safeSDKCall(
383
- () => this.client.docx.document.create({ data: { title, folder_token: folderId || '' } }),
384
- 'createDoc'
385
- );
415
+ const res = await this._asUserOrApp({
416
+ uatPath: `/open-apis/docx/v1/documents`,
417
+ method: 'POST',
418
+ body: { title, folder_token: folderId || '' },
419
+ sdkFn: () => this.client.docx.document.create({ data: { title, folder_token: folderId || '' } }),
420
+ label: 'createDoc',
421
+ });
386
422
  return { documentId: res.data.document?.document_id };
387
423
  }
388
424
 
389
425
  async getDocBlocks(documentId) {
390
- const res = await this._safeSDKCall(
391
- () => this.client.docx.documentBlock.list({ path: { document_id: documentId }, params: { page_size: 500 } }),
392
- 'getDocBlocks'
393
- );
426
+ const res = await this._asUserOrApp({
427
+ uatPath: `/open-apis/docx/v1/documents/${documentId}/blocks`,
428
+ query: { page_size: '500' },
429
+ sdkFn: () => this.client.docx.documentBlock.list({ path: { document_id: documentId }, params: { page_size: 500 } }),
430
+ label: 'getDocBlocks',
431
+ });
394
432
  return { items: res.data.items || [] };
395
433
  }
396
434
 
397
435
  async createDocBlock(documentId, parentBlockId, children, index) {
398
436
  const data = { children };
399
437
  if (index !== undefined) data.index = index;
400
- const res = await this._safeSDKCall(
401
- () => this.client.docx.documentBlockChildren.create({
438
+ const res = await this._asUserOrApp({
439
+ uatPath: `/open-apis/docx/v1/documents/${documentId}/blocks/${parentBlockId}/children`,
440
+ method: 'POST',
441
+ body: data,
442
+ sdkFn: () => this.client.docx.documentBlockChildren.create({
402
443
  path: { document_id: documentId, block_id: parentBlockId },
403
444
  data,
404
445
  }),
405
- 'createDocBlock'
406
- );
446
+ label: 'createDocBlock',
447
+ });
407
448
  return { blocks: res.data.children || [] };
408
449
  }
409
450
 
410
451
  async updateDocBlock(documentId, blockId, updateBody) {
411
- const res = await this._safeSDKCall(
412
- () => this.client.docx.documentBlock.patch({
452
+ const res = await this._asUserOrApp({
453
+ uatPath: `/open-apis/docx/v1/documents/${documentId}/blocks/${blockId}`,
454
+ method: 'PATCH',
455
+ body: updateBody,
456
+ sdkFn: () => this.client.docx.documentBlock.patch({
413
457
  path: { document_id: documentId, block_id: blockId },
414
458
  data: updateBody,
415
459
  }),
416
- 'updateDocBlock'
417
- );
460
+ label: 'updateDocBlock',
461
+ });
418
462
  return { block: res.data.block };
419
463
  }
420
464
 
421
465
  async deleteDocBlocks(documentId, parentBlockId, startIndex, endIndex) {
422
- const res = await this._safeSDKCall(
423
- () => this.client.docx.documentBlockChildren.batchDelete({
466
+ await this._asUserOrApp({
467
+ uatPath: `/open-apis/docx/v1/documents/${documentId}/blocks/${parentBlockId}/children/batch_delete`,
468
+ method: 'DELETE',
469
+ body: { start_index: startIndex, end_index: endIndex },
470
+ sdkFn: () => this.client.docx.documentBlockChildren.batchDelete({
424
471
  path: { document_id: documentId, block_id: parentBlockId },
425
472
  data: { start_index: startIndex, end_index: endIndex },
426
473
  }),
427
- 'deleteDocBlocks'
428
- );
474
+ label: 'deleteDocBlocks',
475
+ });
429
476
  return { deleted: true };
430
477
  }
431
478
 
@@ -445,15 +492,22 @@ class LarkOfficialClient {
445
492
  const data = {};
446
493
  if (name) data.name = name;
447
494
  if (folderId) data.folder_token = folderId;
448
- const res = await this._safeSDKCall(
449
- () => this.client.bitable.app.create({ data }),
450
- 'createBitable'
451
- );
495
+ const res = await this._asUserOrApp({
496
+ uatPath: `/open-apis/bitable/v1/apps`,
497
+ method: 'POST',
498
+ body: data,
499
+ sdkFn: () => this.client.bitable.app.create({ data }),
500
+ label: 'createBitable',
501
+ });
452
502
  return { appToken: res.data.app?.app_token, name: res.data.app?.name, url: res.data.app?.url };
453
503
  }
454
504
 
455
505
  async listBitableTables(appToken) {
456
- const res = await this._safeSDKCall(() => this.client.bitable.appTable.list({ path: { app_token: appToken } }), 'listTables');
506
+ const res = await this._asUserOrApp({
507
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/tables`,
508
+ sdkFn: () => this.client.bitable.appTable.list({ path: { app_token: appToken } }),
509
+ label: 'listTables',
510
+ });
457
511
  return { items: res.data.items || [] };
458
512
  }
459
513
 
@@ -461,39 +515,54 @@ class LarkOfficialClient {
461
515
  const data = { table: { name } };
462
516
  if (fields && fields.length > 0) data.table.default_view_name = name;
463
517
  if (fields && fields.length > 0) data.table.fields = fields;
464
- const res = await this._safeSDKCall(
465
- () => this.client.bitable.appTable.create({ path: { app_token: appToken }, data }),
466
- 'createTable'
467
- );
518
+ const res = await this._asUserOrApp({
519
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/tables`,
520
+ method: 'POST',
521
+ body: data,
522
+ sdkFn: () => this.client.bitable.appTable.create({ path: { app_token: appToken }, data }),
523
+ label: 'createTable',
524
+ });
468
525
  return { tableId: res.data.table_id };
469
526
  }
470
527
 
471
528
  async listBitableFields(appToken, tableId) {
472
- const res = await this._safeSDKCall(() => this.client.bitable.appTableField.list({ path: { app_token: appToken, table_id: tableId } }), 'listFields');
529
+ const res = await this._asUserOrApp({
530
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/fields`,
531
+ sdkFn: () => this.client.bitable.appTableField.list({ path: { app_token: appToken, table_id: tableId } }),
532
+ label: 'listFields',
533
+ });
473
534
  return { items: res.data.items || [] };
474
535
  }
475
536
 
476
537
  async createBitableField(appToken, tableId, fieldConfig) {
477
- const res = await this._safeSDKCall(
478
- () => this.client.bitable.appTableField.create({ path: { app_token: appToken, table_id: tableId }, data: fieldConfig }),
479
- 'createField'
480
- );
538
+ const res = await this._asUserOrApp({
539
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/fields`,
540
+ method: 'POST',
541
+ body: fieldConfig,
542
+ sdkFn: () => this.client.bitable.appTableField.create({ path: { app_token: appToken, table_id: tableId }, data: fieldConfig }),
543
+ label: 'createField',
544
+ });
481
545
  return { field: res.data.field };
482
546
  }
483
547
 
484
548
  async updateBitableField(appToken, tableId, fieldId, fieldConfig) {
485
- const res = await this._safeSDKCall(
486
- () => this.client.bitable.appTableField.update({ path: { app_token: appToken, table_id: tableId, field_id: fieldId }, data: fieldConfig }),
487
- 'updateField'
488
- );
549
+ const res = await this._asUserOrApp({
550
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/fields/${fieldId}`,
551
+ method: 'PUT',
552
+ body: fieldConfig,
553
+ sdkFn: () => this.client.bitable.appTableField.update({ path: { app_token: appToken, table_id: tableId, field_id: fieldId }, data: fieldConfig }),
554
+ label: 'updateField',
555
+ });
489
556
  return { field: res.data.field };
490
557
  }
491
558
 
492
559
  async deleteBitableField(appToken, tableId, fieldId) {
493
- const res = await this._safeSDKCall(
494
- () => this.client.bitable.appTableField.delete({ path: { app_token: appToken, table_id: tableId, field_id: fieldId } }),
495
- 'deleteField'
496
- );
560
+ const res = await this._asUserOrApp({
561
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/fields/${fieldId}`,
562
+ method: 'DELETE',
563
+ sdkFn: () => this.client.bitable.appTableField.delete({ path: { app_token: appToken, table_id: tableId, field_id: fieldId } }),
564
+ label: 'deleteField',
565
+ });
497
566
  return { fieldId: res.data.field_id, deleted: res.data.deleted };
498
567
  }
499
568
 
@@ -501,126 +570,169 @@ class LarkOfficialClient {
501
570
  const data = {};
502
571
  if (filter) data.filter = filter;
503
572
  if (sort) data.sort = sort;
504
- if (pageSize) data.page_size = pageSize;
505
- if (pageToken) data.page_token = pageToken;
506
- const res = await this._safeSDKCall(
507
- () => this.client.bitable.appTableRecord.search({ path: { app_token: appToken, table_id: tableId }, data }),
508
- 'searchRecords'
509
- );
573
+ const query = {};
574
+ if (pageSize) query.page_size = String(pageSize);
575
+ if (pageToken) query.page_token = pageToken;
576
+ const res = await this._asUserOrApp({
577
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/records/search`,
578
+ method: 'POST',
579
+ body: data,
580
+ query,
581
+ sdkFn: () => this.client.bitable.appTableRecord.search({
582
+ path: { app_token: appToken, table_id: tableId },
583
+ params: { page_size: pageSize, ...(pageToken ? { page_token: pageToken } : {}) },
584
+ data,
585
+ }),
586
+ label: 'searchRecords',
587
+ });
510
588
  return { items: res.data.items || [], total: res.data.total, hasMore: res.data.has_more };
511
589
  }
512
590
 
513
591
  async createBitableRecord(appToken, tableId, fields) {
514
- const res = await this._safeSDKCall(
515
- () => this.client.bitable.appTableRecord.create({ path: { app_token: appToken, table_id: tableId }, data: { fields } }),
516
- 'createRecord'
517
- );
592
+ const res = await this._asUserOrApp({
593
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/records`,
594
+ method: 'POST',
595
+ body: { fields },
596
+ sdkFn: () => this.client.bitable.appTableRecord.create({ path: { app_token: appToken, table_id: tableId }, data: { fields } }),
597
+ label: 'createRecord',
598
+ });
518
599
  return { recordId: res.data.record?.record_id };
519
600
  }
520
601
 
521
602
  async updateBitableRecord(appToken, tableId, recordId, fields) {
522
- const res = await this._safeSDKCall(
523
- () => this.client.bitable.appTableRecord.update({ path: { app_token: appToken, table_id: tableId, record_id: recordId }, data: { fields } }),
524
- 'updateRecord'
525
- );
603
+ const res = await this._asUserOrApp({
604
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/records/${recordId}`,
605
+ method: 'PUT',
606
+ body: { fields },
607
+ sdkFn: () => this.client.bitable.appTableRecord.update({ path: { app_token: appToken, table_id: tableId, record_id: recordId }, data: { fields } }),
608
+ label: 'updateRecord',
609
+ });
526
610
  return { recordId: res.data.record?.record_id };
527
611
  }
528
612
 
529
613
  async deleteBitableRecord(appToken, tableId, recordId) {
530
- const res = await this._safeSDKCall(
531
- () => this.client.bitable.appTableRecord.delete({ path: { app_token: appToken, table_id: tableId, record_id: recordId } }),
532
- 'deleteRecord'
533
- );
614
+ const res = await this._asUserOrApp({
615
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/records/${recordId}`,
616
+ method: 'DELETE',
617
+ sdkFn: () => this.client.bitable.appTableRecord.delete({ path: { app_token: appToken, table_id: tableId, record_id: recordId } }),
618
+ label: 'deleteRecord',
619
+ });
534
620
  return { deleted: res.data.deleted };
535
621
  }
536
622
 
537
623
  async batchCreateBitableRecords(appToken, tableId, records) {
538
- const res = await this._safeSDKCall(
539
- () => this.client.bitable.appTableRecord.batchCreate({ path: { app_token: appToken, table_id: tableId }, data: { records } }),
540
- 'batchCreateRecords'
541
- );
624
+ const res = await this._asUserOrApp({
625
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/records/batch_create`,
626
+ method: 'POST',
627
+ body: { records },
628
+ sdkFn: () => this.client.bitable.appTableRecord.batchCreate({ path: { app_token: appToken, table_id: tableId }, data: { records } }),
629
+ label: 'batchCreateRecords',
630
+ });
542
631
  return { records: res.data.records || [] };
543
632
  }
544
633
 
545
634
  async batchUpdateBitableRecords(appToken, tableId, records) {
546
- const res = await this._safeSDKCall(
547
- () => this.client.bitable.appTableRecord.batchUpdate({ path: { app_token: appToken, table_id: tableId }, data: { records } }),
548
- 'batchUpdateRecords'
549
- );
635
+ const res = await this._asUserOrApp({
636
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/records/batch_update`,
637
+ method: 'POST',
638
+ body: { records },
639
+ sdkFn: () => this.client.bitable.appTableRecord.batchUpdate({ path: { app_token: appToken, table_id: tableId }, data: { records } }),
640
+ label: 'batchUpdateRecords',
641
+ });
550
642
  return { records: res.data.records || [] };
551
643
  }
552
644
 
553
645
  async batchDeleteBitableRecords(appToken, tableId, recordIds) {
554
- const res = await this._safeSDKCall(
555
- () => this.client.bitable.appTableRecord.batchDelete({ path: { app_token: appToken, table_id: tableId }, data: { records: recordIds } }),
556
- 'batchDeleteRecords'
557
- );
646
+ const res = await this._asUserOrApp({
647
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/records/batch_delete`,
648
+ method: 'POST',
649
+ body: { records: recordIds },
650
+ sdkFn: () => this.client.bitable.appTableRecord.batchDelete({ path: { app_token: appToken, table_id: tableId }, data: { records: recordIds } }),
651
+ label: 'batchDeleteRecords',
652
+ });
558
653
  return { records: res.data.records || [] };
559
654
  }
560
655
 
561
656
  async listBitableViews(appToken, tableId) {
562
- const res = await this._safeSDKCall(
563
- () => this.client.bitable.appTableView.list({ path: { app_token: appToken, table_id: tableId }, params: { page_size: 50 } }),
564
- 'listViews'
565
- );
657
+ const res = await this._asUserOrApp({
658
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/views`,
659
+ query: { page_size: '50' },
660
+ sdkFn: () => this.client.bitable.appTableView.list({ path: { app_token: appToken, table_id: tableId }, params: { page_size: 50 } }),
661
+ label: 'listViews',
662
+ });
566
663
  return { items: res.data.items || [] };
567
664
  }
568
665
 
569
666
  async getBitableRecord(appToken, tableId, recordId) {
570
- const res = await this._safeSDKCall(
571
- () => this.client.bitable.appTableRecord.get({ path: { app_token: appToken, table_id: tableId, record_id: recordId } }),
572
- 'getRecord'
573
- );
667
+ const res = await this._asUserOrApp({
668
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/records/${recordId}`,
669
+ sdkFn: () => this.client.bitable.appTableRecord.get({ path: { app_token: appToken, table_id: tableId, record_id: recordId } }),
670
+ label: 'getRecord',
671
+ });
574
672
  return { record: res.data.record };
575
673
  }
576
674
 
577
675
  async deleteBitableTable(appToken, tableId) {
578
- await this._safeSDKCall(
579
- () => this.client.bitable.appTable.delete({ path: { app_token: appToken, table_id: tableId } }),
580
- 'deleteTable'
581
- );
676
+ await this._asUserOrApp({
677
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}`,
678
+ method: 'DELETE',
679
+ sdkFn: () => this.client.bitable.appTable.delete({ path: { app_token: appToken, table_id: tableId } }),
680
+ label: 'deleteTable',
681
+ });
582
682
  return { deleted: true };
583
683
  }
584
684
 
585
685
  async getBitableMeta(appToken) {
586
- const res = await this._safeSDKCall(
587
- () => this.client.bitable.app.get({ path: { app_token: appToken } }),
588
- 'getBitableMeta'
589
- );
686
+ const res = await this._asUserOrApp({
687
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}`,
688
+ sdkFn: () => this.client.bitable.app.get({ path: { app_token: appToken } }),
689
+ label: 'getBitableMeta',
690
+ });
590
691
  return { app: res.data.app };
591
692
  }
592
693
 
593
694
  async updateBitableTable(appToken, tableId, name) {
594
- const res = await this._safeSDKCall(
595
- () => this.client.bitable.appTable.patch({ path: { app_token: appToken, table_id: tableId }, data: { name } }),
596
- 'updateTable'
597
- );
695
+ const res = await this._asUserOrApp({
696
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}`,
697
+ method: 'PATCH',
698
+ body: { name },
699
+ sdkFn: () => this.client.bitable.appTable.patch({ path: { app_token: appToken, table_id: tableId }, data: { name } }),
700
+ label: 'updateTable',
701
+ });
598
702
  return { name: res.data.name };
599
703
  }
600
704
 
601
705
  async createBitableView(appToken, tableId, viewName, viewType = 'grid') {
602
- const res = await this._safeSDKCall(
603
- () => this.client.bitable.appTableView.create({ path: { app_token: appToken, table_id: tableId }, data: { view_name: viewName, view_type: viewType } }),
604
- 'createView'
605
- );
706
+ const res = await this._asUserOrApp({
707
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/views`,
708
+ method: 'POST',
709
+ body: { view_name: viewName, view_type: viewType },
710
+ sdkFn: () => this.client.bitable.appTableView.create({ path: { app_token: appToken, table_id: tableId }, data: { view_name: viewName, view_type: viewType } }),
711
+ label: 'createView',
712
+ });
606
713
  return { view: res.data.view };
607
714
  }
608
715
 
609
716
  async deleteBitableView(appToken, tableId, viewId) {
610
- await this._safeSDKCall(
611
- () => this.client.bitable.appTableView.delete({ path: { app_token: appToken, table_id: tableId, view_id: viewId } }),
612
- 'deleteView'
613
- );
717
+ await this._asUserOrApp({
718
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/views/${viewId}`,
719
+ method: 'DELETE',
720
+ sdkFn: () => this.client.bitable.appTableView.delete({ path: { app_token: appToken, table_id: tableId, view_id: viewId } }),
721
+ label: 'deleteView',
722
+ });
614
723
  return { deleted: true };
615
724
  }
616
725
 
617
726
  async copyBitable(appToken, name, folderId) {
618
727
  const data = { name };
619
728
  if (folderId) data.folder_token = folderId;
620
- const res = await this._safeSDKCall(
621
- () => this.client.bitable.app.copy({ path: { app_token: appToken }, data }),
622
- 'copyBitable'
623
- );
729
+ const res = await this._asUserOrApp({
730
+ uatPath: `/open-apis/bitable/v1/apps/${appToken}/copy`,
731
+ method: 'POST',
732
+ body: data,
733
+ sdkFn: () => this.client.bitable.app.copy({ path: { app_token: appToken }, data }),
734
+ label: 'copyBitable',
735
+ });
624
736
  return { app: res.data.app };
625
737
  }
626
738
 
@@ -665,10 +777,14 @@ class LarkOfficialClient {
665
777
  }
666
778
 
667
779
  async createFolder(name, parentToken) {
668
- const res = await this._safeSDKCall(
669
- () => this.client.drive.file.createFolder({ data: { name, folder_token: parentToken || '' } }),
670
- 'createFolder'
671
- );
780
+ const body = { name, folder_token: parentToken || '' };
781
+ const res = await this._asUserOrApp({
782
+ uatPath: `/open-apis/drive/v1/files/create_folder`,
783
+ method: 'POST',
784
+ body,
785
+ sdkFn: () => this.client.drive.file.createFolder({ data: body }),
786
+ label: 'createFolder',
787
+ });
672
788
  return { token: res.data.token };
673
789
  }
674
790
 
@@ -731,53 +847,6 @@ class LarkOfficialClient {
731
847
  return allChats;
732
848
  }
733
849
 
734
- // --- UAT-based creation (resources owned by user, not app) ---
735
-
736
- async createDocAsUser(title, folderId) {
737
- const data = { title };
738
- if (folderId) data.folder_token = folderId;
739
- const result = await this._withUAT(async (uat) => {
740
- const res = await fetch('https://open.feishu.cn/open-apis/docx/v1/documents', {
741
- method: 'POST',
742
- headers: { 'Authorization': `Bearer ${uat}`, 'content-type': 'application/json' },
743
- body: JSON.stringify(data),
744
- });
745
- return res.json();
746
- });
747
- if (result.code !== 0) throw new Error(`createDocAsUser failed (${result.code}): ${result.msg}`);
748
- return { documentId: result.data.document?.document_id };
749
- }
750
-
751
- async createBitableAsUser(name, folderId) {
752
- const data = {};
753
- if (name) data.name = name;
754
- if (folderId) data.folder_token = folderId;
755
- const result = await this._withUAT(async (uat) => {
756
- const res = await fetch('https://open.feishu.cn/open-apis/bitable/v1/apps', {
757
- method: 'POST',
758
- headers: { 'Authorization': `Bearer ${uat}`, 'content-type': 'application/json' },
759
- body: JSON.stringify(data),
760
- });
761
- return res.json();
762
- });
763
- if (result.code !== 0) throw new Error(`createBitableAsUser failed (${result.code}): ${result.msg}`);
764
- return { appToken: result.data.app?.app_token, name: result.data.app?.name, url: result.data.app?.url };
765
- }
766
-
767
- async createFolderAsUser(name, parentToken) {
768
- const data = { name, folder_token: parentToken || '' };
769
- const result = await this._withUAT(async (uat) => {
770
- const res = await fetch('https://open.feishu.cn/open-apis/drive/v1/files/create_folder', {
771
- method: 'POST',
772
- headers: { 'Authorization': `Bearer ${uat}`, 'content-type': 'application/json' },
773
- body: JSON.stringify(data),
774
- });
775
- return res.json();
776
- });
777
- if (result.code !== 0) throw new Error(`createFolderAsUser failed (${result.code}): ${result.msg}`);
778
- return { token: result.data.token };
779
- }
780
-
781
850
  // --- Safe SDK Call (extracts real Feishu error from AxiosError) ---
782
851
 
783
852
  async _safeSDKCall(fn, label = 'API') {
@@ -863,7 +932,7 @@ class LarkOfficialClient {
863
932
  if (!m) return null;
864
933
  let body = m.body?.content || '';
865
934
  try { body = JSON.parse(body); } catch {}
866
- return {
935
+ const out = {
867
936
  messageId: m.message_id,
868
937
  chatId: m.chat_id,
869
938
  senderId: m.sender?.id,
@@ -873,6 +942,8 @@ class LarkOfficialClient {
873
942
  createTime: this._normalizeTimestamp(m.create_time),
874
943
  updateTime: this._normalizeTimestamp(m.update_time),
875
944
  };
945
+ if (Array.isArray(m.mentions) && m.mentions.length > 0) out.mentions = m.mentions;
946
+ return out;
876
947
  }
877
948
 
878
949
  _normalizeTimestamp(ts) {