feishu-user-plugin 1.3.6 → 1.3.8
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/CHANGELOG.md +71 -0
- package/README.md +72 -41
- package/package.json +10 -3
- package/scripts/capture-feishu-protobuf.js +86 -0
- package/scripts/check-changelog.js +31 -0
- package/scripts/check-docs-sync.js +41 -0
- package/scripts/check-tool-count.js +40 -0
- package/scripts/check-version.js +40 -0
- package/scripts/decode-feishu-protobuf.js +115 -0
- package/scripts/smoke.js +224 -0
- package/scripts/sync-claude-md.sh +12 -0
- package/scripts/sync-server-json.js +71 -0
- package/scripts/sync-team-skills.sh +22 -0
- package/scripts/test-all-tools.js +158 -0
- package/scripts/test-wiki-attach-fallback.js +71 -0
- package/scripts/test-ws-events.js +84 -0
- package/skills/feishu-user-plugin/SKILL.md +5 -5
- package/skills/feishu-user-plugin/references/CLAUDE.md +248 -318
- package/skills/feishu-user-plugin/references/table.md +18 -9
- package/src/auth/cookie.js +30 -0
- package/src/auth/credentials.js +399 -0
- package/src/auth/profile-router.js +248 -0
- package/src/auth/uat.js +231 -0
- package/src/cli.js +45 -13
- package/src/clients/official/base.js +188 -0
- package/src/clients/official/bitable.js +269 -0
- package/src/clients/official/calendar.js +176 -0
- package/src/clients/official/contacts.js +54 -0
- package/src/clients/official/docs.js +301 -0
- package/src/clients/official/drive.js +77 -0
- package/src/clients/official/groups.js +68 -0
- package/src/clients/official/im.js +414 -0
- package/src/clients/official/index.js +30 -0
- package/src/clients/official/okr.js +127 -0
- package/src/clients/official/tasks.js +142 -0
- package/src/clients/official/uploads.js +260 -0
- package/src/clients/official/wiki.js +207 -0
- package/src/{client.js → clients/user.js} +25 -33
- package/src/config.js +13 -8
- package/src/events/event-buffer.js +100 -0
- package/src/events/index.js +5 -0
- package/src/events/ws-server.js +86 -0
- package/src/index.js +4 -1977
- package/src/logger.js +20 -0
- package/src/oauth.js +5 -1
- package/src/official.js +5 -1944
- package/src/prompts/_registry.js +69 -0
- package/src/prompts/index.js +54 -0
- package/src/server.js +305 -0
- package/src/setup.js +16 -1
- package/src/test-all.js +2 -2
- package/src/test-comprehensive.js +3 -3
- package/src/test-send.js +1 -1
- package/src/tools/_registry.js +31 -0
- package/src/tools/bitable.js +246 -0
- package/src/tools/calendar.js +207 -0
- package/src/tools/contacts.js +66 -0
- package/src/tools/diagnostics.js +172 -0
- package/src/tools/docs.js +158 -0
- package/src/tools/drive.js +111 -0
- package/src/tools/events.js +64 -0
- package/src/tools/groups.js +81 -0
- package/src/tools/im-read.js +259 -0
- package/src/tools/messaging-bot.js +151 -0
- package/src/tools/messaging-user.js +292 -0
- package/src/tools/okr.js +159 -0
- package/src/tools/profile.js +74 -0
- package/src/tools/tasks.js +168 -0
- package/src/tools/uploads.js +63 -0
- package/src/tools/wiki.js +191 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// src/tools/im-read.js — IM read paths (chat list, message history, P2P).
|
|
2
|
+
|
|
3
|
+
const { text, json } = require('./_registry');
|
|
4
|
+
|
|
5
|
+
// ChatIdMapper — fuzzy chat-id resolver shared across im-read handlers.
|
|
6
|
+
// Moved from src/index.js in v1.3.7 phase A. Only read_messages uses it,
|
|
7
|
+
// so it lives here as a module-level singleton.
|
|
8
|
+
class ChatIdMapper {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.nameCache = new Map(); // oc_id → chat name
|
|
11
|
+
this.lastRefresh = 0;
|
|
12
|
+
this.TTL = 5 * 60 * 1000; // 5 min cache
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async _refresh(official) {
|
|
16
|
+
if (Date.now() - this.lastRefresh < this.TTL) return;
|
|
17
|
+
try {
|
|
18
|
+
const chats = await official.listAllChats();
|
|
19
|
+
this.nameCache.clear();
|
|
20
|
+
for (const chat of chats) {
|
|
21
|
+
this.nameCache.set(chat.chat_id, chat.name || '');
|
|
22
|
+
}
|
|
23
|
+
this.lastRefresh = Date.now();
|
|
24
|
+
} catch (e) {
|
|
25
|
+
console.error('[feishu-user-plugin] ChatIdMapper refresh failed:', e.message);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Case-insensitive name matching helper
|
|
30
|
+
static _nameMatch(haystack, needle, exact = false) {
|
|
31
|
+
if (!haystack || !needle) return false;
|
|
32
|
+
const h = haystack.toLowerCase(), n = needle.toLowerCase();
|
|
33
|
+
return exact ? h === n : h.includes(n);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async findByName(name, official) {
|
|
37
|
+
await this._refresh(official);
|
|
38
|
+
// Exact match first (case-insensitive)
|
|
39
|
+
for (const [ocId, chatName] of this.nameCache) {
|
|
40
|
+
if (ChatIdMapper._nameMatch(chatName, name, true)) return ocId;
|
|
41
|
+
}
|
|
42
|
+
// Partial match (case-insensitive)
|
|
43
|
+
for (const [ocId, chatName] of this.nameCache) {
|
|
44
|
+
if (ChatIdMapper._nameMatch(chatName, name)) return ocId;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async resolveToOcId(chatIdOrName, official) {
|
|
50
|
+
if (!chatIdOrName) return null;
|
|
51
|
+
if (chatIdOrName.startsWith('oc_')) return chatIdOrName;
|
|
52
|
+
// Also accept raw numeric IDs (from search_contacts)
|
|
53
|
+
if (/^\d+$/.test(chatIdOrName)) return chatIdOrName;
|
|
54
|
+
// Strategy 1: Search in bot's group list cache
|
|
55
|
+
const cached = await this.findByName(chatIdOrName, official);
|
|
56
|
+
if (cached) return cached;
|
|
57
|
+
// Strategy 2: Use im.v1.chat.search API (finds groups even if not in cache)
|
|
58
|
+
try {
|
|
59
|
+
const results = await official.chatSearch(chatIdOrName);
|
|
60
|
+
for (const chat of results) {
|
|
61
|
+
this.nameCache.set(chat.chat_id, chat.name || '');
|
|
62
|
+
if (ChatIdMapper._nameMatch(chat.name, chatIdOrName, true)) return chat.chat_id;
|
|
63
|
+
}
|
|
64
|
+
// Partial match on search results (case-insensitive)
|
|
65
|
+
for (const chat of results) {
|
|
66
|
+
if (ChatIdMapper._nameMatch(chat.name, chatIdOrName)) return chat.chat_id;
|
|
67
|
+
}
|
|
68
|
+
} catch (e) {
|
|
69
|
+
console.error('[feishu-user-plugin] chatSearch fallback failed:', e.message);
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Strategy 3: Use search_contacts (cookie-based) to find external groups by name
|
|
75
|
+
// Returns numeric chat_id that works with UAT readMessagesAsUser
|
|
76
|
+
async resolveViaContacts(chatName, userClient) {
|
|
77
|
+
if (!userClient) return null;
|
|
78
|
+
try {
|
|
79
|
+
const results = await userClient.search(chatName);
|
|
80
|
+
const groups = results.filter(r => r.type === 'group');
|
|
81
|
+
// Exact match first (case-insensitive)
|
|
82
|
+
for (const g of groups) {
|
|
83
|
+
if (ChatIdMapper._nameMatch(g.title, chatName, true)) return String(g.id);
|
|
84
|
+
}
|
|
85
|
+
// Partial match (case-insensitive)
|
|
86
|
+
for (const g of groups) {
|
|
87
|
+
if (ChatIdMapper._nameMatch(g.title, chatName)) return String(g.id);
|
|
88
|
+
}
|
|
89
|
+
} catch (e) {
|
|
90
|
+
console.error('[feishu-user-plugin] search_contacts fallback failed:', e.message);
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const chatIdMapper = new ChatIdMapper();
|
|
97
|
+
|
|
98
|
+
const schemas = [
|
|
99
|
+
{
|
|
100
|
+
name: 'get_chat_info',
|
|
101
|
+
description: '[Official API + User Identity fallback] Get chat details: name, description, member count, owner. Supports both oc_xxx and numeric chat_id.',
|
|
102
|
+
inputSchema: {
|
|
103
|
+
type: 'object',
|
|
104
|
+
properties: { chat_id: { type: 'string', description: 'Chat ID (oc_xxx or numeric)' } },
|
|
105
|
+
required: ['chat_id'],
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: 'read_p2p_messages',
|
|
110
|
+
description: '[User UAT] Read P2P (direct message) chat history using user_access_token. Works for chats the bot cannot access. Returns newest messages first by default. Auto-expands merge_forward messages into their child messages by default — disable with expand_merge_forward=false. Requires OAuth setup.',
|
|
111
|
+
inputSchema: {
|
|
112
|
+
type: 'object',
|
|
113
|
+
properties: {
|
|
114
|
+
chat_id: { type: 'string', description: 'Chat ID (numeric from create_p2p_chat, or oc_xxx from list_user_chats). Both formats work.' },
|
|
115
|
+
page_size: { type: 'number', description: 'Messages to fetch (default 20, max 50)' },
|
|
116
|
+
start_time: { type: 'string', description: 'Start timestamp in seconds (optional)' },
|
|
117
|
+
end_time: { type: 'string', description: 'End timestamp in seconds (optional)' },
|
|
118
|
+
sort_type: { type: 'string', enum: ['ByCreateTimeDesc', 'ByCreateTimeAsc'], description: 'Sort order (default: ByCreateTimeDesc = newest first)' },
|
|
119
|
+
expand_merge_forward: { type: 'boolean', description: 'Auto-expand merge_forward placeholders into their child messages (default true). Children carry parentMessageId; use that id (not the child id) with download_message_resource (kind=image or file).' },
|
|
120
|
+
},
|
|
121
|
+
required: ['chat_id'],
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: 'list_user_chats',
|
|
126
|
+
description: '[User UAT] List group chats the user is in. Note: only returns groups, not P2P. For P2P chats, use search_contacts → create_p2p_chat → read_p2p_messages. Requires OAuth setup.',
|
|
127
|
+
inputSchema: {
|
|
128
|
+
type: 'object',
|
|
129
|
+
properties: {
|
|
130
|
+
page_size: { type: 'number', description: 'Items per page (default 20)' },
|
|
131
|
+
page_token: { type: 'string', description: 'Pagination token' },
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: 'list_chats',
|
|
137
|
+
description: '[Official API] List all chats the bot has joined. Returns chat_id, name, type.',
|
|
138
|
+
inputSchema: {
|
|
139
|
+
type: 'object',
|
|
140
|
+
properties: {
|
|
141
|
+
page_size: { type: 'number', description: 'Items per page (default 20, max 100)' },
|
|
142
|
+
page_token: { type: 'string', description: 'Pagination token' },
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: 'read_messages',
|
|
148
|
+
description: '[Official API + UAT fallback] Read message history from any group. Accepts oc_xxx ID, numeric ID, or chat name (auto-searched). Auto-falls back to UAT for external groups the bot cannot access. Returns newest messages first by default, with sender names resolved. Auto-expands merge_forward messages into their child messages (with original sender / time / content preserved) by default — disable with expand_merge_forward=false. Text messages have URLs extracted into `urls`; Feishu doc links are additionally surfaced as `feishuDocs` so agents can feed them straight into read_doc / get_doc_blocks.',
|
|
149
|
+
inputSchema: {
|
|
150
|
+
type: 'object',
|
|
151
|
+
properties: {
|
|
152
|
+
chat_id: { type: 'string', description: 'Chat ID (oc_xxx), numeric ID, or chat name (auto-searched via bot groups, im.chat.search, and user contacts)' },
|
|
153
|
+
page_size: { type: 'number', description: 'Messages to fetch (default 20, max 50)' },
|
|
154
|
+
start_time: { type: 'string', description: 'Start timestamp in seconds (optional)' },
|
|
155
|
+
end_time: { type: 'string', description: 'End timestamp in seconds (optional)' },
|
|
156
|
+
sort_type: { type: 'string', enum: ['ByCreateTimeDesc', 'ByCreateTimeAsc'], description: 'Sort order (default: ByCreateTimeDesc = newest first)' },
|
|
157
|
+
expand_merge_forward: { type: 'boolean', description: 'Auto-expand merge_forward placeholders into their child messages (default true). Children carry parentMessageId; use that id (not the child id) with download_message_resource (kind=image or file).' },
|
|
158
|
+
},
|
|
159
|
+
required: ['chat_id'],
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
const handlers = {
|
|
165
|
+
async get_chat_info(args, ctx) {
|
|
166
|
+
// Strategy 1: Official API im.chat.get (supports oc_xxx format)
|
|
167
|
+
if (args.chat_id.startsWith('oc_')) {
|
|
168
|
+
try {
|
|
169
|
+
const info = await ctx.getOfficialClient().getChatInfo(args.chat_id);
|
|
170
|
+
return info ? json(info) : text(`No info for chat ${args.chat_id}`);
|
|
171
|
+
} catch (e) {
|
|
172
|
+
console.error(`[feishu-user-plugin] Official getChatInfo failed: ${e.message}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Strategy 2: Protobuf gateway (supports numeric chat_id)
|
|
176
|
+
try {
|
|
177
|
+
const c = await ctx.getUserClient();
|
|
178
|
+
const info = await c.getGroupInfo(args.chat_id);
|
|
179
|
+
if (info) return json(info);
|
|
180
|
+
} catch (e) {
|
|
181
|
+
console.error(`[feishu-user-plugin] Protobuf getChatInfo failed: ${e.message}`);
|
|
182
|
+
}
|
|
183
|
+
return text(`No info for chat ${args.chat_id}`);
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
async read_p2p_messages(args, ctx) {
|
|
187
|
+
const official = ctx.getOfficialClient();
|
|
188
|
+
let chatId = args.chat_id;
|
|
189
|
+
let uc = null;
|
|
190
|
+
let ucError = null;
|
|
191
|
+
try { uc = await ctx.getUserClient(); } catch (e) { ucError = e; }
|
|
192
|
+
// If chat_id is not numeric or oc_, try to resolve as user name → P2P chat
|
|
193
|
+
if (!/^\d+$/.test(chatId) && !chatId.startsWith('oc_')) {
|
|
194
|
+
if (uc) {
|
|
195
|
+
const results = await uc.search(chatId);
|
|
196
|
+
const user = results.find(r => r.type === 'user');
|
|
197
|
+
if (user) {
|
|
198
|
+
const pChatId = await uc.createChat(String(user.id));
|
|
199
|
+
if (pChatId) chatId = String(pChatId);
|
|
200
|
+
else return text(`Found user "${user.title}" but failed to create P2P chat.`);
|
|
201
|
+
} else {
|
|
202
|
+
// Maybe it's a group name
|
|
203
|
+
const group = results.find(r => r.type === 'group');
|
|
204
|
+
if (group) chatId = String(group.id);
|
|
205
|
+
else return text(`Cannot resolve "${args.chat_id}" to a chat. Use search_contacts to find the ID first.`);
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
const hint = ucError ? `Cookie auth failed: ${ucError.message}. Fix LARK_COOKIE first, or p` : 'P';
|
|
209
|
+
return text(`"${args.chat_id}" is not a valid chat ID. ${hint}rovide a numeric ID or oc_xxx format. Use search_contacts + create_p2p_chat to get the ID.`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return json(await official.readMessagesAsUser(chatId, {
|
|
213
|
+
pageSize: args.page_size, startTime: args.start_time, endTime: args.end_time,
|
|
214
|
+
sortType: args.sort_type,
|
|
215
|
+
expandMergeForward: args.expand_merge_forward !== false,
|
|
216
|
+
}, uc));
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
async list_user_chats(args, ctx) {
|
|
220
|
+
return json(await ctx.getOfficialClient().listChatsAsUser({ pageSize: args.page_size, pageToken: args.page_token }));
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
async list_chats(args, ctx) {
|
|
224
|
+
return json(await ctx.getOfficialClient().listChats({ pageSize: args.page_size, pageToken: args.page_token }));
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
async read_messages(args, ctx) {
|
|
228
|
+
const official = ctx.getOfficialClient();
|
|
229
|
+
const msgOpts = {
|
|
230
|
+
pageSize: args.page_size, startTime: args.start_time, endTime: args.end_time,
|
|
231
|
+
sortType: args.sort_type,
|
|
232
|
+
expandMergeForward: args.expand_merge_forward !== false,
|
|
233
|
+
};
|
|
234
|
+
// Get userClient for name resolution fallback (best-effort)
|
|
235
|
+
let uc = null;
|
|
236
|
+
try { uc = await ctx.getUserClient(); } catch (_) {}
|
|
237
|
+
|
|
238
|
+
// Path A — chat_id that resolves inside bot's / official search scope.
|
|
239
|
+
const resolvedChatId = await chatIdMapper.resolveToOcId(args.chat_id, official);
|
|
240
|
+
if (resolvedChatId) {
|
|
241
|
+
return json(await official.readMessagesWithFallback(resolvedChatId, msgOpts, uc));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Path B — external group discovered only via cookie search_contacts.
|
|
245
|
+
// When we got here the bot definitely can't see it, so skip bot entirely
|
|
246
|
+
// and go straight to UAT with a `contacts` via label.
|
|
247
|
+
if (official.hasUAT) {
|
|
248
|
+
if (!uc) try { uc = await ctx.getUserClient(); } catch (_) {}
|
|
249
|
+
const contactChatId = await chatIdMapper.resolveViaContacts(args.chat_id, uc);
|
|
250
|
+
if (contactChatId) {
|
|
251
|
+
return json(await official.readMessagesWithFallback(contactChatId, msgOpts, uc, { skipBot: true, via: 'contacts' }));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return text(`Cannot resolve "${args.chat_id}" to a chat ID.\nSearched: bot's group list, im.chat.search API, and user contacts (search_contacts).\nTry: provide the oc_xxx or numeric chat ID directly.`);
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
module.exports = { schemas, handlers };
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// src/tools/messaging-bot.js — Bot-identity messaging operations (send, reply,
|
|
2
|
+
// forward, delete, update, pin, reactions). send_card_as_user is intentionally
|
|
3
|
+
// kept inline in src/index.js until v1.3.7's user-identity card path lands; it
|
|
4
|
+
// will move to src/tools/messaging-user.js together with that work.
|
|
5
|
+
|
|
6
|
+
const { text, json } = require('./_registry');
|
|
7
|
+
|
|
8
|
+
const schemas = [
|
|
9
|
+
{
|
|
10
|
+
name: 'send_message_as_bot',
|
|
11
|
+
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.',
|
|
12
|
+
inputSchema: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
chat_id: { type: 'string', description: 'Target chat_id (oc_xxx) or open_id' },
|
|
16
|
+
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'] },
|
|
17
|
+
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.' },
|
|
18
|
+
},
|
|
19
|
+
required: ['chat_id', 'msg_type', 'content'],
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'reply_message',
|
|
24
|
+
description: '[Official API] Reply to a specific message by message_id (as bot). Only works for text messages; other types return error 230054.',
|
|
25
|
+
inputSchema: {
|
|
26
|
+
type: 'object',
|
|
27
|
+
properties: {
|
|
28
|
+
message_id: { type: 'string', description: 'Message ID to reply to (om_xxx)' },
|
|
29
|
+
text: { type: 'string', description: 'Reply text' },
|
|
30
|
+
},
|
|
31
|
+
required: ['message_id', 'text'],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'forward_message',
|
|
36
|
+
description: '[Official API] Forward a message to another chat or user. `receive_id` may be a group chat_id (oc_xxx), an open_id (ou_xxx), a union_id, a user_id, or an email — set `receive_id_type` to match (default: chat_id).',
|
|
37
|
+
inputSchema: {
|
|
38
|
+
type: 'object',
|
|
39
|
+
properties: {
|
|
40
|
+
message_id: { type: 'string', description: 'Message ID to forward (om_xxx)' },
|
|
41
|
+
receive_id: { type: 'string', description: 'Target chat_id (oc_xxx), open_id (ou_xxx), union_id, user_id, or email — set receive_id_type to match.' },
|
|
42
|
+
receive_id_type: { type: 'string', enum: ['chat_id', 'open_id', 'union_id', 'user_id', 'email'], description: 'Format of receive_id (default: chat_id). Set to "open_id" when forwarding to a user via their open_id.', default: 'chat_id' },
|
|
43
|
+
},
|
|
44
|
+
required: ['message_id', 'receive_id'],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'delete_message',
|
|
49
|
+
description: '[Official API] Recall/delete a message (bot can only delete its own messages).',
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: 'object',
|
|
52
|
+
properties: { message_id: { type: 'string', description: 'Message ID (om_xxx)' } },
|
|
53
|
+
required: ['message_id'],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'update_message',
|
|
58
|
+
description: '[Official API] Edit a sent message (bot can only edit its own messages). Feishu supports edit only for `text` and `interactive` (card) messages — other types (post, image, file, etc.) are rejected by the API.',
|
|
59
|
+
inputSchema: {
|
|
60
|
+
type: 'object',
|
|
61
|
+
properties: {
|
|
62
|
+
message_id: { type: 'string', description: 'Message ID (om_xxx)' },
|
|
63
|
+
msg_type: { type: 'string', enum: ['text', 'interactive'], description: 'Message type: text or interactive. Other types are not editable per Feishu API.' },
|
|
64
|
+
content: { description: 'New content. For text: {"text":"updated text"}. For interactive: full card JSON.' },
|
|
65
|
+
},
|
|
66
|
+
required: ['message_id', 'msg_type', 'content'],
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'pin_message',
|
|
71
|
+
description: '[Official API] Pin or unpin a message in a chat.',
|
|
72
|
+
inputSchema: {
|
|
73
|
+
type: 'object',
|
|
74
|
+
properties: {
|
|
75
|
+
message_id: { type: 'string', description: 'Message ID' },
|
|
76
|
+
pinned: { type: 'boolean', description: 'true to pin, false to unpin', default: true },
|
|
77
|
+
},
|
|
78
|
+
required: ['message_id'],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'add_reaction',
|
|
83
|
+
description: '[Official API] Add an emoji reaction to a message.',
|
|
84
|
+
inputSchema: {
|
|
85
|
+
type: 'object',
|
|
86
|
+
properties: {
|
|
87
|
+
message_id: { type: 'string', description: 'Message ID (om_xxx)' },
|
|
88
|
+
emoji_type: { type: 'string', description: 'Emoji type string, e.g. "THUMBSUP", "SMILE", "HEART"' },
|
|
89
|
+
},
|
|
90
|
+
required: ['message_id', 'emoji_type'],
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: 'delete_reaction',
|
|
95
|
+
description: '[Official API] Remove an emoji reaction from a message.',
|
|
96
|
+
inputSchema: {
|
|
97
|
+
type: 'object',
|
|
98
|
+
properties: {
|
|
99
|
+
message_id: { type: 'string', description: 'Message ID' },
|
|
100
|
+
reaction_id: { type: 'string', description: 'Reaction ID (from add_reaction response)' },
|
|
101
|
+
},
|
|
102
|
+
required: ['message_id', 'reaction_id'],
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
const handlers = {
|
|
108
|
+
async send_message_as_bot(args, ctx) {
|
|
109
|
+
const r = await ctx.getOfficialClient().sendMessageAsBot(args.chat_id, args.msg_type, args.content);
|
|
110
|
+
return text(`Message sent (bot): ${r.messageId}`);
|
|
111
|
+
},
|
|
112
|
+
async reply_message(args, ctx) {
|
|
113
|
+
return text(`Reply sent: ${(await ctx.getOfficialClient().replyMessage(args.message_id, args.text)).messageId}`);
|
|
114
|
+
},
|
|
115
|
+
async forward_message(args, ctx) {
|
|
116
|
+
// Auto-detect receive_id_type when not provided so callers can pass an open_id
|
|
117
|
+
// (ou_xxx) without having to set the type field — matches what
|
|
118
|
+
// send_to_user/send_to_group already do for chat resolution.
|
|
119
|
+
let receiveIdType = args.receive_id_type;
|
|
120
|
+
if (!receiveIdType) {
|
|
121
|
+
const id = args.receive_id || '';
|
|
122
|
+
if (id.startsWith('ou_')) receiveIdType = 'open_id';
|
|
123
|
+
else if (id.startsWith('on_')) receiveIdType = 'union_id';
|
|
124
|
+
else if (id.includes('@')) receiveIdType = 'email';
|
|
125
|
+
else receiveIdType = 'chat_id';
|
|
126
|
+
}
|
|
127
|
+
return text(`Forwarded: ${(await ctx.getOfficialClient().forwardMessage(args.message_id, args.receive_id, receiveIdType)).messageId}`);
|
|
128
|
+
},
|
|
129
|
+
async delete_message(args, ctx) {
|
|
130
|
+
return text(`Message deleted: ${(await ctx.getOfficialClient().deleteMessage(args.message_id)).deleted}`);
|
|
131
|
+
},
|
|
132
|
+
async update_message(args, ctx) {
|
|
133
|
+
// Feishu API limit: only text + interactive are editable. Reject early so
|
|
134
|
+
// the user sees a clear message instead of a 230053 from the API.
|
|
135
|
+
if (!['text', 'interactive'].includes(args.msg_type)) {
|
|
136
|
+
return text(`update_message only supports msg_type=text or interactive (Feishu API limit). Got: ${args.msg_type}`);
|
|
137
|
+
}
|
|
138
|
+
return text(`Message updated: ${(await ctx.getOfficialClient().updateMessage(args.message_id, args.msg_type, args.content)).messageId}`);
|
|
139
|
+
},
|
|
140
|
+
async pin_message(args, ctx) {
|
|
141
|
+
return json(await ctx.getOfficialClient().pinMessage(args.message_id, args.pinned !== false));
|
|
142
|
+
},
|
|
143
|
+
async add_reaction(args, ctx) {
|
|
144
|
+
return text(`Reaction added: ${(await ctx.getOfficialClient().addReaction(args.message_id, args.emoji_type)).reactionId}`);
|
|
145
|
+
},
|
|
146
|
+
async delete_reaction(args, ctx) {
|
|
147
|
+
return text(`Reaction removed: ${(await ctx.getOfficialClient().deleteReaction(args.message_id, args.reaction_id)).deleted}`);
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
module.exports = { schemas, handlers };
|