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.
- package/.claude-plugin/plugin.json +2 -2
- package/package.json +1 -1
- package/proto/lark.proto +27 -0
- package/skills/feishu-user-plugin/references/CLAUDE.md +33 -11
- package/src/client.js +81 -10
- package/src/index.js +30 -35
- package/src/official.js +231 -160
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "feishu-user-plugin",
|
|
3
|
-
"version": "1.2
|
|
4
|
-
"description": "All-in-one Feishu plugin for Claude Code — send messages as yourself, read chats, manage docs/tables/wiki.
|
|
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
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
|
-
-
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
317
|
-
cp
|
|
318
|
-
cp
|
|
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
|
-
|
|
347
|
-
#
|
|
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.
|
|
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
|
|
205
|
-
|
|
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
|
|
258
|
+
elementIds,
|
|
209
259
|
innerText: text,
|
|
210
|
-
elements: { dictionary
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
259
|
-
const
|
|
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 =
|
|
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
|
|
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
|
|
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).
|
|
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
|
-
|
|
1226
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1302
|
-
|
|
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.
|
|
375
|
-
|
|
376
|
-
'
|
|
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.
|
|
383
|
-
|
|
384
|
-
'
|
|
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.
|
|
391
|
-
|
|
392
|
-
'
|
|
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.
|
|
401
|
-
|
|
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.
|
|
412
|
-
|
|
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
|
-
|
|
423
|
-
|
|
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.
|
|
449
|
-
|
|
450
|
-
'
|
|
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.
|
|
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.
|
|
465
|
-
|
|
466
|
-
'
|
|
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.
|
|
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.
|
|
478
|
-
|
|
479
|
-
'
|
|
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.
|
|
486
|
-
|
|
487
|
-
'
|
|
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.
|
|
494
|
-
|
|
495
|
-
'
|
|
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
|
-
|
|
505
|
-
if (
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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.
|
|
515
|
-
|
|
516
|
-
'
|
|
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.
|
|
523
|
-
|
|
524
|
-
'
|
|
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.
|
|
531
|
-
|
|
532
|
-
'
|
|
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.
|
|
539
|
-
|
|
540
|
-
'
|
|
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.
|
|
547
|
-
|
|
548
|
-
'
|
|
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.
|
|
555
|
-
|
|
556
|
-
'
|
|
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.
|
|
563
|
-
|
|
564
|
-
'
|
|
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.
|
|
571
|
-
|
|
572
|
-
|
|
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.
|
|
579
|
-
|
|
580
|
-
'
|
|
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.
|
|
587
|
-
|
|
588
|
-
|
|
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.
|
|
595
|
-
|
|
596
|
-
'
|
|
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.
|
|
603
|
-
|
|
604
|
-
'
|
|
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.
|
|
611
|
-
|
|
612
|
-
'
|
|
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.
|
|
621
|
-
|
|
622
|
-
'
|
|
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
|
|
669
|
-
|
|
670
|
-
|
|
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
|
-
|
|
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) {
|