
Feishu Message
- 1 installs
- 20 repo stars
- Updated April 11, 2026
- autogame-17/feishu-skills
feishu-message is a Claude skill providing a unified CLI for Feishu (Lark) messaging: fetching messages, sending audio voice bubbles, creating chats, listing pins, and adding reactions.
About
feishu-message is a unified CLI for Feishu (Lark) messaging operations through a single entry point. It fetches messages by ID, sends audio files as voice bubbles, creates group chats, lists pinned messages, and adds emoji reactions. Developers use it when an agent needs to read or act on Feishu messages beyond posting text. It requires feishu-common with valid Feishu credentials.
- Unified CLI for Feishu messaging: fetch, send audio, create chats, list pins, react
- Fetches messages by ID including merged/forwarded threads with --recursive
- Sends audio files as voice bubbles and adds emoji reactions
Feishu Message by the numbers
- 1 all-time installs (skills.sh)
- Ranked #1,982 of 2,719 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
feishu-message capabilities & compatibility
Free skill; requires a Feishu app credential pair (FEISHU_APP_ID / FEISHU_APP_SECRET) via feishu-common.
- Capabilities
- feishu card · feishu sticker · feishu memory recall
- Works with
- slack
- Use cases
- orchestration
What feishu-message says it does
Unified CLI for Feishu (Lark) messaging operations including fetching messages by ID, sending audio voice bubbles, creating group chats, listing pinned messages, and adding emoji reactions.
Unified CLI for Feishu messaging -- fetch, send audio, create chats, list pins, and add reactions through a single entry point.
npx skills add https://github.com/autogame-17/feishu-skills --skill feishu-messageAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 20 |
| Last updated | April 11, 2026 |
| Repository | autogame-17/feishu-skills ↗ |
What it does
Fetch, send audio, create chats, list pins, and react to messages in Feishu from one CLI.
Who is it for?
Reading Feishu messages by ID and performing messaging actions like audio sends, chat creation, and reactions.
Skip if: Messaging on Slack, Teams, or platforms other than Feishu (Lark).
When should I use this skill?
An agent needs to fetch a Feishu message, send audio, create a chat, list pins, or add a reaction.
What you get
Messaging operations run through one CLI: fetch, send audio, create chats, list pins, and react.
- Fetched message content or a completed Feishu messaging action
By the numbers
- 5 messaging commands (get, send-audio, create-chat, list-pins, add-reaction)
- 6 supported reaction types
Files
feishu-message
Unified CLI for Feishu messaging -- fetch, send audio, create chats, list pins, and add reactions through a single entry point.
Prerequisites
feishu-commoninstalled with validFEISHU_APP_IDandFEISHU_APP_SECRET.
Commands
All commands use the unified CLI:
node skills/feishu-message/index.js <command> [options]Get Message
Fetch message content by ID. Use --recursive for merged/forwarded messages.
node skills/feishu-message/index.js get <message_id> [--raw] [--recursive]Send Audio
Send an audio file as a voice bubble to a user or chat.
node skills/feishu-message/index.js send-audio --target <ou_xxx|oc_xxx> --file <path> [--duration <ms>]--target: User OpenID (ou_) or ChatID (oc_).--file: Path to audio file (mp3/wav/etc).--duration: (Optional) Duration in ms.
Create Group Chat
Create a new group chat with specified users.
node skills/feishu-message/index.js create-chat --name "Project Alpha" --users "ou_1" "ou_2" --desc "Description"List Pins
List pinned messages in a chat.
node skills/feishu-message/index.js list-pins <chat_id>Add Reaction
Add an emoji reaction to a message.
node skills/feishu-message/reaction.js --message-id <msg_id> --type <emoji_type>Supported types: THUMBSUP (default), HEART, LAUGH, WOW, SAD, ANGRY.
Legacy Scripts
Standalone scripts remain available for backward compatibility: get.js, send-audio.js, create_chat.js, list_pins_v2.js.
Dependencies
- axios
- form-data
- music-metadata
- commander
{
"ownerId": "kn7apafdj4thknczrgxdzfd2v1808svf",
"slug": "feishu-message",
"version": "1.0.0",
"publishedAt": 1770118168893
}{
"version": 1,
"registry": "https://clawhub.ai",
"slug": "feishu-message",
"installedVersion": "1.0.0",
"installedAt": 1770518288026
}
node_modules/
.env
package-lock.json
#!/usr/bin/env node
const { program } = require('commander');
const path = require('path');
const fs = require('fs');
// Try to load Lark SDK from local or fallback
let Lark;
try {
Lark = require('@larksuiteoapi/node-sdk');
} catch (e) {
try {
Lark = require('../feishu-calendar/node_modules/@larksuiteoapi/node-sdk');
} catch (e2) {
console.error('Error: Could not load @larksuiteoapi/node-sdk');
process.exit(1);
}
}
require('dotenv').config({ path: path.resolve(__dirname, '../../.env') });
const APP_ID = process.env.FEISHU_APP_ID;
const APP_SECRET = process.env.FEISHU_APP_SECRET;
const client = new Lark.Client({ appId: APP_ID, appSecret: APP_SECRET });
async function createGroupChat(name, userIds, description) {
try {
console.log(`Creating group chat: "${name}" with users: ${userIds.join(', ')}`);
const res = await client.im.chat.create({
params: {
user_id_type: 'open_id',
set_bot_manager: true
},
data: {
name: name,
description: description || "Created by OpenClaw Agent",
user_id_list: userIds,
chat_mode: 'group',
group_type: 'private',
external: false // Internal only by default
}
});
if (res.code !== 0) {
console.error(`Error creating chat: [${res.code}] ${res.msg}`);
if (res.code === 403001) console.error("Tip: Check if bot has 'im:chat' scope and users are in visibility range.");
return null;
}
console.log(JSON.stringify(res.data, null, 2));
return res.data;
} catch (e) {
console.error(`API Exception: ${e.message}`);
return null;
}
}
program
.version('1.0.0')
.description('Create a Feishu group chat with specified users')
.argument('<name>', 'Name of the group chat')
.argument('<users...>', 'List of user OpenIDs (space separated)')
.option('-d, --desc <description>', 'Group description')
.option('--content <description>', 'Group description (alias)')
.action(async (name, users, options) => {
if (!process.env.FEISHU_APP_ID || !process.env.FEISHU_APP_SECRET) {
console.error('Error: FEISHU_APP_ID or FEISHU_APP_SECRET not set in env');
process.exit(1);
}
await createGroupChat(name, users, options.desc || options.content);
});
program.parse(process.argv);
const path = require('path');
require('dotenv').config({ path: path.resolve(__dirname, '../../.env') });
// Try to load Lark SDK
let Lark;
try {
Lark = require('@larksuiteoapi/node-sdk');
} catch (e) {
try {
Lark = require('../feishu-calendar/node_modules/@larksuiteoapi/node-sdk');
} catch (e2) {
console.error('Error: Could not load @larksuiteoapi/node-sdk');
process.exit(1);
}
}
const APP_ID = process.env.FEISHU_APP_ID;
const APP_SECRET = process.env.FEISHU_APP_SECRET;
const client = new Lark.Client({ appId: APP_ID, appSecret: APP_SECRET });
async function disbandChat(chatId) {
try {
console.log(`Attempting to disband chat: ${chatId}`);
const res = await client.im.chat.delete({
path: { chat_id: chatId }
});
if (res.code !== 0) {
console.error(`Error: [${res.code}] ${res.msg}`);
if (res.code === 403001) console.error("Permission denied (Not owner?)");
process.exit(1);
}
console.log(`Success: Chat ${chatId} disbanded.`);
console.log(JSON.stringify(res.data, null, 2));
} catch (e) {
console.error(`Exception: ${e.message}`);
process.exit(1);
}
}
const chatId = process.argv[2];
if (!chatId) {
console.error("Usage: node disband_chat.js <chat_id>");
process.exit(1);
}
disbandChat(chatId);
const path = require('path');
require('dotenv').config({ path: path.resolve(__dirname, '../../.env') });
// Try to load Lark SDK
let Lark;
try {
Lark = require('@larksuiteoapi/node-sdk');
} catch (e) {
try {
Lark = require('../feishu-calendar/node_modules/@larksuiteoapi/node-sdk');
} catch (e2) {
console.error('Error: Could not load @larksuiteoapi/node-sdk');
process.exit(1);
}
}
const APP_ID = process.env.FEISHU_APP_ID;
const APP_SECRET = process.env.FEISHU_APP_SECRET;
const client = new Lark.Client({ appId: APP_ID, appSecret: APP_SECRET });
async function getChatInfo(chatId) {
try {
const res = await client.im.chat.get({
path: { chat_id: chatId },
params: { user_id_type: 'open_id' }
});
if (res.code !== 0) {
console.error(`Error: [${res.code}] ${res.msg}`);
return;
}
console.log(JSON.stringify(res, null, 2));
} catch (e) {
console.error(e);
}
}
const chatId = process.argv[2];
if (!chatId) {
console.error("Usage: node get_chat_info.js <chat_id>");
process.exit(1);
}
getChatInfo(chatId);
const fs = require('fs');
const path = require('path');
const { program } = require('commander');
require('dotenv').config({ path: path.resolve(__dirname, '../../.env') });
const APP_ID = process.env.FEISHU_APP_ID;
const APP_SECRET = process.env.FEISHU_APP_SECRET;
const TOKEN_CACHE_FILE = path.resolve(__dirname, '../../memory/feishu_token.json');
program.option('--chat-id <id>', 'Chat ID (or User OpenID but API needs chat_id)').parse(process.argv);
const options = program.opts();
async function getToken() {
if (fs.existsSync(TOKEN_CACHE_FILE)) {
const cached = JSON.parse(fs.readFileSync(TOKEN_CACHE_FILE, 'utf8'));
if (cached.expire > Math.floor(Date.now() / 1000) + 60) return cached.token;
}
const res = await fetch('https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ app_id: APP_ID, app_secret: APP_SECRET })
});
const data = await res.json();
return data.tenant_access_token;
}
async function getHistory() {
const token = await getToken();
console.log(`Fetching history for ${options.chatId}...`);
// Note: 'im/v1/messages' lists messages in a chat. It requires 'container_id' which is 'chat_id'.
// If 'options.chatId' is a user OpenID, we first need to get the chat_id of the P2P chat.
let chatId = options.chatId;
if (chatId.startsWith('ou_')) {
// Get P2P Chat ID first (Not directly exposed via API easily without creating a chat, but we can try listing recent chats?)
// Or create a chat to ensure it exists and get ID.
// POST /im/v1/p2p_chats
const res = await fetch(`https://open.feishu.cn/open-apis/im/v1/p2p_chats`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ user_id: chatId })
});
const data = await res.json();
if (data.code === 0) {
chatId = data.data.chat_id;
console.log(`Resolved P2P Chat ID: ${chatId}`);
} else {
console.error("Failed to resolve P2P chat:", JSON.stringify(data));
return;
}
}
const res = await fetch(`https://open.feishu.cn/open-apis/im/v1/messages?container_id_type=chat&container_id=${chatId}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await res.json();
if (data.code === 0 && data.data.items) {
// Find latest file message
const fileMsg = data.data.items.find(m => m.msg_type === 'file');
if (fileMsg) {
console.log(JSON.stringify(fileMsg, null, 2));
} else {
console.log("No file message found.");
}
} else {
console.log("Error or no messages:", JSON.stringify(data));
}
}
getHistory();
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { program } = require('commander');
require('dotenv').config({ path: path.resolve(__dirname, '../../.env') });
const APP_ID = process.env.FEISHU_APP_ID;
const APP_SECRET = process.env.FEISHU_APP_SECRET;
const TOKEN_CACHE_FILE = path.resolve(__dirname, '../../memory/feishu_token.json');
async function getToken() {
try {
if (fs.existsSync(TOKEN_CACHE_FILE)) {
const cached = JSON.parse(fs.readFileSync(TOKEN_CACHE_FILE, 'utf8'));
const now = Math.floor(Date.now() / 1000);
if (cached.expire > now + 60) return cached.token;
}
} catch (e) {}
try {
const res = await fetch('https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ app_id: APP_ID, app_secret: APP_SECRET })
});
const data = await res.json();
if (data.code !== 0) throw new Error(`Auth failed: ${JSON.stringify(data)}`);
try {
fs.writeFileSync(TOKEN_CACHE_FILE, JSON.stringify({
token: data.tenant_access_token,
expire: Math.floor(Date.now() / 1000) + data.expire
}));
} catch(e) {}
return data.tenant_access_token;
} catch (e) {
console.error(e);
process.exit(1);
}
}
async function fetchMessage(messageId) {
const token = await getToken();
const url = `https://open.feishu.cn/open-apis/im/v1/messages/${messageId}`;
const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
const json = await res.json();
if (json.code !== 0) {
// If message not found, maybe it's a merge forward item?
// But we can't fetch merge forward items directly via this API usually.
throw new Error(`API Error ${json.code}: ${json.msg}`);
}
return json.data;
}
function parseContent(msgBody) {
try {
const content = JSON.parse(msgBody.content);
if (content.text) return content.text;
if (content.title && content.content) {
// Post type
return `[Post] ${content.title}\n` + content.content.map(p => p.map(e => e.text).join('')).join('\n');
}
if (content.image_key) return `[Image key=${content.image_key}]`;
return JSON.stringify(content);
} catch (e) {
return msgBody.content;
}
}
async function formatMessage(msg, depth = 0, recursive = false) {
const indent = ' '.repeat(depth);
let output = '';
const sender = msg.sender && msg.sender.sender_type === 'user' ? (msg.sender.id || 'User') : 'App';
const time = new Date(parseInt(msg.create_time)).toISOString().replace('T', ' ').substring(0, 19);
if (msg.msg_type === 'merge_forward') {
output += `${indent}📂 [Merged Forward] (${time})\n`;
// If recursive is true, we should try to fetch the merged content if it's not present
// But standard message API doesn't return merged content unless specific params are used?
// Actually for merge_forward, the content is usually just a placeholder or list of IDs.
// We might need a separate API call to get merged content?
// For now, let's just print what we have.
// If items are present (e.g. from a specialized fetch), print them
if (msg.items && Array.isArray(msg.items)) {
for (const item of msg.items) {
output += await formatMessage(item, depth + 1, recursive);
}
} else {
output += `${indent} (No items found or not expanded)\n`;
}
} else {
const content = parseContent(msg.body);
output += `${indent}💬 [${sender}] ${time}: ${content}\n`;
}
return output;
}
program
.argument('[message_id]', 'Message ID to read (positional)') // Make optional to allow --message-id usage
.option('-m, --message-id <id>', 'Message ID to read (alternative)')
.option('-r, --raw', 'Output raw JSON')
.option('-R, --recursive', 'Recursively fetch merged messages (dummy for now)')
.action(async (posMessageId, options) => {
try {
const messageId = posMessageId || options.messageId;
if (!messageId) {
console.error("Error: Message ID is required (argument or --message-id)");
process.exit(1);
}
const data = await fetchMessage(messageId);
if (options.raw) {
console.log(JSON.stringify(data, null, 2));
} else {
if (data.items && data.items.length > 0) {
console.log(`📦 Merged Message Container (${data.items.length} items):\n`);
for (const item of data.items) {
if (item.message_id === messageId && data.items.length > 1) continue;
console.log(await formatMessage(item, 0, options.recursive));
}
} else {
// Single message
console.log(await formatMessage(data.items ? data.items[0] : data, 0, options.recursive));
}
}
} catch (e) {
console.error('Error:', e.message);
process.exit(1);
}
});
program.parse();
#!/usr/bin/env node
const { program } = require('commander');
const fs = require('fs');
const path = require('path');
const { spawn } = require('child_process');
require('dotenv').config({ path: path.resolve(__dirname, '../../.env') });
function runScript(scriptName, args) {
const scriptPath = path.resolve(__dirname, scriptName);
if (!fs.existsSync(scriptPath)) {
console.error(`Error: Script ${scriptName} not found at ${scriptPath}`);
process.exit(1);
}
// Pass stdio: 'inherit' to preserve colors and output
const child = spawn(process.execPath, [scriptPath, ...args], {
stdio: 'inherit',
env: process.env
});
child.on('close', (code) => process.exit(code));
child.on('error', (err) => {
console.error(`Error spawning ${scriptName}:`, err);
process.exit(1);
});
}
program
.name('feishu-message')
.description('Unified Feishu Toolkit for messaging, groups, and files')
.version('1.1.0');
// Subcommand: get (calls get.js)
program
.command('get')
.description('Get a message by ID')
.argument('<message_id>', 'Message ID')
.option('-r, --raw', 'Output raw JSON')
.option('-R, --recursive', 'Recursively fetch merged messages')
.action((id, options) => {
const args = [id];
if (options.raw) args.push('--raw');
if (options.recursive) args.push('--recursive');
runScript('get.js', args);
});
// Subcommand: send (proxies to feishu-post)
program
.command('send')
.description('Send a rich text message (via feishu-post)')
.option('-t, --target <id>', 'Target ID')
.option('-c, --content <text>', 'Content')
.option('-x, --text <text>', 'Text')
.option('--title <text>', 'Title')
.action((options) => {
const target = options.target || process.env.OPENCLAW_MASTER_ID;
if (!target) {
console.error('Error: Target ID is required (and OPENCLAW_MASTER_ID not set)');
process.exit(1);
}
const scriptPath = path.resolve(__dirname, '../feishu-post/send.js');
const args = ['--target', target];
if (options.content) args.push('--content', options.content);
if (options.text) args.push('--text', options.text);
if (options.title) args.push('--title', options.title);
const child = spawn(process.execPath, [scriptPath, ...args], {
stdio: 'inherit',
env: process.env
});
child.on('close', (code) => process.exit(code));
});
// Subcommand: send-audio (calls send-audio.js)
program
.command('send-audio')
.description('Send an audio file')
.requiredOption('-t, --target <id>', 'Target ID (user/chat)')
.requiredOption('-f, --file <path>', 'Audio file path')
.option('-d, --duration <ms>', 'Duration in ms')
.action((options) => {
const args = ['--target', options.target, '--file', options.file];
if (options.duration) args.push('--duration', options.duration);
runScript('send-audio.js', args);
});
// Subcommand: create-chat (calls create_chat.js)
program
.command('create-chat')
.description('Create a group chat')
.requiredOption('-n, --name <name>', 'Chat name')
.requiredOption('-u, --users <ids...>', 'User IDs')
.option('--desc <text>', 'Description')
.option('--content <text>', 'Description (alias)')
.action((options) => {
const args = ['--name', options.name, '--users', ...options.users];
const desc = options.desc || options.content;
if (desc) args.push('--desc', desc);
runScript('create_chat.js', args);
});
// Subcommand: reaction (calls reaction.js)
program
.command('reaction')
.description('Add a reaction to a message')
.requiredOption('-m, --message-id <id>', 'Message ID')
.option('-t, --type <type>', 'Reaction type (THUMBSUP, HEART, etc)', 'THUMBSUP')
.option('-d, --delete', 'Delete reaction')
.action((options) => {
const args = ['--message-id', options.messageId, '--type', options.type];
if (options.delete) args.push('--delete');
runScript('reaction.js', args);
});
// Subcommand: list-pins (calls list_pins_v2.js)
program
.command('list-pins')
.description('List pinned messages in a chat')
.argument('<chat_id>', 'Chat ID')
.action((chatId) => {
runScript('list_pins_v2.js', [chatId]);
});
// Subcommand: list (calls list.js)
program
.command('list')
.description('List messages in a chat')
.requiredOption('-c, --chat-id <id>', 'Chat ID')
.option('-l, --limit <number>', 'Limit', '20')
.action((options) => {
runScript('list.js', ['--chat-id', options.chatId, '--limit', options.limit]);
});
program.parse();
const fs = require('fs');
const path = require('path');
const Lark = require('@larksuiteoapi/node-sdk');
require('dotenv').config({ path: path.resolve(__dirname, '../../.env') });
const APP_ID = process.env.FEISHU_APP_ID;
const APP_SECRET = process.env.FEISHU_APP_SECRET;
const client = new Lark.Client({ appId: APP_ID, appSecret: APP_SECRET });
async function listPins(chatId) {
try {
// Correct API usage for SDK v3: im.pin.list
// The error 99992402 usually means invalid parameters.
// Check if `start_time` or `end_time` are required or if `page_size` has limits.
console.log(`Listing pins for chat: ${chatId}`);
const res = await client.im.pin.list({
params: {
chat_id: chatId, // Note: For some APIs, chat_id is a query param, for others a path param. SDK handles this?
// The SDK might require chat_id in `params` for `list`.
page_size: 20
}
});
if (res.code !== 0) {
console.error(`Error listing pins: ${res.code} - ${res.msg}`);
// Debug: print full error
console.error(JSON.stringify(res));
return [];
}
return res.data.items || [];
} catch (e) {
console.error(`API Exception: ${e.message}`);
return [];
}
}
async function getChatId(userId) {
// Try to get chat_id from user_id
try {
const res = await client.im.chat.create({
params: { user_id_type: 'open_id' },
data: { user_id: userId }
});
if (res.code === 0) return res.data.chat_id;
} catch(e) {}
return null;
}
async function main() {
const userId = process.argv[2];
if (!userId) return;
const chatId = await getChatId(userId);
if (!chatId) {
console.log("Chat not found.");
return;
}
const pins = await listPins(chatId);
if (pins.length === 0) {
console.log("No pins found (or API failed).");
return;
}
// Process pins
const summary = pins.map(p => {
// Pins usually wrap a message. Need to fetch message details if content is sparse.
// But SDK `items` usually contain message content.
return `- [${new Date(parseInt(p.create_time)).toLocaleString()}] MessageID: ${p.message_id}`;
}).join('\n');
console.log(summary);
}
main();
const fs = require('fs');
const path = require('path');
const Lark = require('@larksuiteoapi/node-sdk');
require('dotenv').config({ path: path.resolve(__dirname, '../../.env') });
const APP_ID = process.env.FEISHU_APP_ID;
const APP_SECRET = process.env.FEISHU_APP_SECRET;
const client = new Lark.Client({ appId: APP_ID, appSecret: APP_SECRET });
async function listPins(chatId) {
try {
const res = await client.im.pin.list({
path: { chat_id: chatId },
params: { page_size: 50 }
});
if (res.code !== 0) {
console.error(`Error listing pins: ${res.msg}`);
return [];
}
return res.data.items_page.items || [];
} catch (e) {
console.error(`API Exception: ${e.message}`);
return [];
}
}
async function getChatId(userId) {
// 1. Try to create/get P2P chat
try {
const res = await client.im.chat.create({
params: { user_id_type: 'open_id' },
data: { user_id: userId }
});
if (res.code === 0) return res.data.chat_id;
} catch(e) {}
return null;
}
async function main() {
const userId = process.argv[2];
if (!userId) {
console.error("Usage: node list_pins.js <user_id>");
return;
}
// 1. Get Chat ID
const chatId = await getChatId(userId);
if (!chatId) {
console.error("Could not find P2P chat ID for user.");
return;
}
console.log(`Chat ID: ${chatId}`);
// 2. List Pins
const pins = await listPins(chatId);
if (pins.length === 0) {
console.log("No pinned messages found.");
return;
}
const summary = pins.map(p => {
let content = "Unknown";
try {
const msgContent = JSON.parse(p.message.content);
if (p.message.msg_type === 'text') content = msgContent.text;
else if (p.message.msg_type === 'post') content = msgContent.title || "(Rich Text)";
else content = `[${p.message.msg_type}]`;
} catch(e) {}
const time = new Date(parseInt(p.create_time)).toLocaleString();
return `- [${time}] ${content}`;
}).join('\n');
console.log(summary);
}
main();
const lark = require('@larksuiteoapi/node-sdk');
const { program } = require('commander');
// Initialize client
const client = new lark.Client({
appId: process.env.FEISHU_APP_ID,
appSecret: process.env.FEISHU_APP_SECRET,
disableTokenCache: false
});
program
.description('List messages in a chat')
.requiredOption('-c, --chat-id <id>', 'Chat ID (oc_...)')
.option('-l, --limit <number>', 'Number of messages to fetch', '20')
.parse(process.argv);
const options = program.opts();
async function main() {
try {
console.log(`Listing messages in chat ${options.chatId}...`);
const res = await client.im.message.list({
params: {
container_id_type: 'chat',
container_id: options.chatId,
page_size: parseInt(options.limit),
// sort_type removed
},
});
if (res.code === 0) {
const items = res.data.items || [];
console.log(`Found ${items.length} messages.`);
items.reverse().forEach(msg => { // Show oldest to newest
const senderName = msg.sender && msg.sender.sender_id ? msg.sender.sender_id.user_id : 'Unknown'; // Simplified
let contentText = 'Content parsing failed';
try {
const content = JSON.parse(msg.body.content);
contentText = content.text || '[Rich/Media Content]';
} catch (e) {
contentText = msg.body.content;
}
console.log(`[${msg.message_id}] [${msg.create_time}] ${contentText}`);
});
} else {
console.error('Failed to list messages:', res);
process.exit(1);
}
} catch (err) {
console.error('Error:', err.message);
if (err.response) {
console.error('Response:', err.response.data);
}
process.exit(1);
}
}
main();
{
"name": "feishu-message",
"version": "1.0.5",
"description": "General Feishu message operations (get, recursive read, etc)",
"main": "get.js",
"dependencies": {
"@larksuiteoapi/node-sdk": "^1.58.0",
"axios": "^1.13.4",
"commander": "^9.0.0",
"dotenv": "^16.0.0",
"form-data": "^4.0.5",
"music-metadata": "^11.11.2"
}
}
const lark = require('@larksuiteoapi/node-sdk');
const { program } = require('commander');
// Initialize client
const client = new lark.Client({
appId: process.env.FEISHU_APP_ID,
appSecret: process.env.FEISHU_APP_SECRET,
disableTokenCache: false
});
program
.description('Add a reaction to a Feishu message')
.requiredOption('-m, --message-id <id>', 'Message ID to react to')
.option('-t, --type <type>', 'Reaction type (e.g., THUMBSUP, HEART, LAUGH)', 'THUMBSUP')
.option('-d, --delete', 'Delete reaction instead of adding', false)
.parse(process.argv);
const options = program.opts();
async function main() {
try {
const emojiType = options.type.toUpperCase();
if (options.delete) {
// Get reaction ID first (requires listing reactions) - simpler to just implement add for now as delete needs reaction_id
console.error('Delete not implemented in this simple script yet.');
process.exit(1);
} else {
console.log(`Adding reaction ${emojiType} to message ${options.messageId}...`);
const res = await client.im.messageReaction.create({
path: {
message_id: options.messageId,
},
data: {
reaction_type: {
emoji_type: emojiType,
},
},
});
if (res.code === 0) {
console.log(`Successfully added reaction: ${emojiType}`);
} else {
console.error('Failed to add reaction:', res);
process.exit(1);
}
}
} catch (err) {
console.error('Error:', err.message);
if (err.response) {
console.error('Response:', err.response.data);
}
process.exit(1);
}
}
main();
#!/usr/bin/env node
const fs = require('fs');
const { program } = require('commander');
const path = require('path');
const axios = require('axios');
const FormData = require('form-data');
const { parseFile } = require('music-metadata');
require('dotenv').config({ path: require('path').resolve(__dirname, '../../.env'), quiet: true });
const APP_ID = process.env.FEISHU_APP_ID;
const APP_SECRET = process.env.FEISHU_APP_SECRET;
const TOKEN_CACHE_FILE = path.resolve(__dirname, '../../memory/feishu_token.json');
if (!APP_ID || !APP_SECRET) {
console.error('Error: FEISHU_APP_ID or FEISHU_APP_SECRET not set.');
process.exit(1);
}
// Reuse token logic
async function getToken() {
try {
if (fs.existsSync(TOKEN_CACHE_FILE)) {
const cached = JSON.parse(fs.readFileSync(TOKEN_CACHE_FILE, 'utf8'));
const now = Math.floor(Date.now() / 1000);
if (cached.expire > now + 60) return cached.token;
}
} catch (e) {}
try {
const res = await axios.post('https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal', {
app_id: APP_ID,
app_secret: APP_SECRET
});
const data = res.data;
if (!data.tenant_access_token) throw new Error(`No token returned: ${JSON.stringify(data)}`);
try {
const cacheData = {
token: data.tenant_access_token,
expire: Math.floor(Date.now() / 1000) + data.expire
};
fs.writeFileSync(TOKEN_CACHE_FILE, JSON.stringify(cacheData, null, 2));
} catch (e) {
console.error('Failed to cache token:', e.message);
}
return data.tenant_access_token;
} catch (e) {
console.error('Failed to get token:', e.message);
process.exit(1);
}
}
async function uploadAudio(token, filePath, durationMs) {
const fileSize = fs.statSync(filePath).size;
const fileStream = fs.createReadStream(filePath);
const form = new FormData();
form.append('file_type', 'opus'); // 'opus' triggers voice bubble handling in Feishu
form.append('file_name', path.basename(filePath));
// Feishu upload API usually takes duration in header or extra field for audio?
// Actually for 'opus' type, some docs say it detects.
// But let's check if we can pass it.
form.append('duration', durationMs);
form.append('file', fileStream);
try {
const res = await axios.post('https://open.feishu.cn/open-apis/im/v1/files', form, {
headers: {
Authorization: `Bearer ${token}`,
...form.getHeaders()
}
});
if (res.data.code !== 0) throw new Error(`Upload Error ${res.data.code}: ${res.data.msg}`);
return res.data.data.file_key;
} catch (e) {
console.error('Upload Failed:', e.response ? e.response.data : e.message);
throw e;
}
}
async function sendAudio(options) {
const token = await getToken();
if (!fs.existsSync(options.file)) {
console.error(`File not found: ${options.file}`);
process.exit(1);
}
// 1. Get Duration
let durationMs = options.duration;
if (!durationMs) {
try {
const metadata = await parseFile(options.file);
if (metadata.format.duration) {
durationMs = Math.round(metadata.format.duration * 1000);
console.log(`Detected duration: ${durationMs}ms`);
} else {
console.warn('Could not detect duration. Using default 1000ms.');
durationMs = 1000;
}
} catch (e) {
console.warn(`Duration detection failed: ${e.message}. Using default 1000ms.`);
durationMs = 1000;
}
}
// 2. Upload
console.log(`Uploading ${options.file} as opus...`);
let fileKey;
try {
fileKey = await uploadAudio(token, options.file, durationMs);
} catch (e) {
process.exit(1);
}
// 3. Send
let receiveIdType = 'open_id';
if (options.target.startsWith('oc_')) receiveIdType = 'chat_id';
else if (options.target.startsWith('ou_')) receiveIdType = 'open_id';
else if (options.target.includes('@')) receiveIdType = 'email';
const messageBody = {
receive_id: options.target,
msg_type: 'audio',
content: JSON.stringify({ file_key: fileKey })
};
console.log(`Sending Audio Bubble to ${options.target}...`);
try {
const res = await axios.post(
`https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=${receiveIdType}`,
messageBody,
{
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
);
if (res.data.code !== 0) {
throw new Error(`API Error ${res.data.code}: ${res.data.msg}`);
}
console.log('Success:', JSON.stringify(res.data.data, null, 2));
} catch (e) {
console.error('Send Failed:', e.response ? e.response.data : e.message);
process.exit(1);
}
}
program
.requiredOption('-t, --target <id>', 'Target ID')
.requiredOption('-f, --file <path>', 'Audio file path')
.option('-d, --duration <ms>', 'Duration in ms (optional, auto-detected if omitted)');
program.parse(process.argv);
const options = program.opts();
(async () => {
sendAudio(options);
})();
#!/usr/bin/env node
/**
* ⚠️ DEPRECATION NOTICE ⚠️
* This script is a COMPATIBILITY ALIAS.
* 'feishu-message' should be used for complex operations (get, merge-forward).
* For sending standard messages, use 'feishu-post' (RichText) or 'feishu-card'.
*
* This script forwards all arguments to 'skills/feishu-post/send.js'.
*/
const { spawn } = require('child_process');
const path = require('path');
// ANSI Colors
const YELLOW = '\x1b[33m';
const RESET = '\x1b[0m';
console.error(`${YELLOW}⚠️ [Evolution System] Redirecting 'feishu-message/send.js' -> 'feishu-post/send.js'...${RESET}`);
const targetScript = path.resolve(__dirname, '../feishu-post/send.js');
const args = process.argv.slice(2);
const child = spawn('node', [targetScript, ...args], {
stdio: 'inherit'
});
child.on('exit', (code) => {
process.exit(code);
});
child.on('error', (err) => {
console.error('Failed to spawn child process:', err);
process.exit(1);
});
Related skills
FAQ
Can it read merged or forwarded messages?
Yes, pass --recursive to the get command to fetch merged or forwarded message content.
Which emoji reactions are supported?
THUMBSUP (default), HEART, LAUGH, WOW, SAD, and ANGRY.