
Feishu Common
- 1 installs
- 20 repo stars
- Updated April 11, 2026
- autogame-17/feishu-skills
feishu-common is a Claude skill providing shared Feishu (Lark) authentication and API request helpers with tenant token caching, auto-refresh, and authenticated fetch wrappers.
About
feishu-common is the shared authentication and API request helper for all OpenClaw Feishu skills. It acquires and caches a tenant access token, refreshes it automatically on expiry, and provides authenticated fetch wrappers with retry and timeout handling. Developers install it first because every other feishu-* skill imports its helpers. It reads FEISHU_APP_ID and FEISHU_APP_SECRET from the environment.
- Shared Feishu auth helper every other feishu-* skill depends on
- Tenant access token acquisition with local caching and auto-refresh
- fetchWithAuth and fetchWithRetry wrappers with timeout and 401 handling
Feishu Common 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-common capabilities & compatibility
Free skill; requires a Feishu app credential pair (FEISHU_APP_ID / FEISHU_APP_SECRET).
- Capabilities
- feishu message · feishu doc
- Works with
- slack
What feishu-common says it does
Shared authentication and API helper for all OpenClaw Feishu skills. Install this skill first -- every other `feishu-*` skill depends on it.
export FEISHU_APP_ID=cli_xxxxx
npx skills add https://github.com/autogame-17/feishu-skills --skill feishu-commonAdd 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
Provide cached tenant-token auth and authenticated fetch wrappers that all other Feishu skills reuse.
Who is it for?
Supplying cached tenant-token auth and retrying authenticated requests to every dependent feishu-* skill.
Skip if: Sending messages or reading documents on its own; it is an auth helper, not an end-user action.
When should I use this skill?
Setting up any Feishu skill that needs a tenant access token or authenticated API calls.
What you get
Dependent skills get a cached, auto-refreshing tenant token and authenticated fetch wrappers.
- A cached tenant access token and authenticated fetch wrappers
By the numbers
- 3 exported helpers (getToken, fetchWithRetry, fetchWithAuth)
Files
feishu-common
Shared authentication and API helper for all OpenClaw Feishu skills. Install this skill first -- every other feishu-* skill depends on it.
Prerequisites
Set these environment variables before using any Feishu skill:
export FEISHU_APP_ID=cli_xxxxx
export FEISHU_APP_SECRET=xxxxxUsage
Import the shared helpers in any dependent skill:
const { getToken, fetchWithRetry, fetchWithAuth } = require("../feishu-common/index.js");Workflow
1. Authenticate -- getToken() acquires a tenant access token and caches it locally, refreshing automatically on expiry. 2. Make requests -- fetchWithAuth(url, options) adds the Authorization header and handles token refresh on 401 responses. 3. Handle failures -- fetchWithRetry(url, options) wraps fetch with configurable retry count and timeout.
Compatibility Alias
A legacy import path is available for backward compatibility:
const { getToken, fetchWithAuth } = require("../feishu-common/feishu-client.js");Files
index.js-- Main implementation (token cache, retry logic, authenticated fetch).feishu-client.js-- Compatibility alias that re-exports fromindex.js.
{
"ownerId": "kn7apafdj4thknczrgxdzfd2v1808svf",
"slug": "feishu-common",
"version": "1.0.0",
"publishedAt": 1770892359246
}{
"version": 1,
"registry": "https://clawhub.ai",
"slug": "feishu-common",
"installedVersion": "1.0.0",
"installedAt": 1770943930785
}
module.exports = require("./index.js");
const fs = require('fs');
const path = require('path');
// const https = require('https'); // Unused
require('dotenv').config({ path: 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');
// --- Upstream Logic Injection (Simplified) ---
// Upstream uses @larksuiteoapi/node-sdk but we are lightweight.
// We replicate the robustness, not the dependency.
/**
* Robust Fetch with Retry (Exponential Backoff)
*/
async function fetchWithRetry(url, options = {}, retries = 3) {
const timeoutMs = options.timeout || 15000;
for (let i = 0; i < retries; i++) {
let timeoutId;
try {
const controller = new AbortController();
timeoutId = setTimeout(() => controller.abort(), timeoutMs);
const fetchOptions = { ...options, signal: controller.signal };
delete fetchOptions.timeout;
const res = await fetch(url, fetchOptions);
clearTimeout(timeoutId);
if (!res.ok) {
// Rate Limiting (429)
if (res.status === 429) {
const retryAfter = res.headers.get('Retry-After');
let waitMs = 1000 * Math.pow(2, i);
if (retryAfter) waitMs = parseInt(retryAfter, 10) * 1000;
console.warn(`[FeishuClient] Rate limited. Waiting ${waitMs}ms...`);
await new Promise(r => setTimeout(r, waitMs));
continue;
}
// Do not retry 4xx errors (except 429), usually auth or param errors
if (res.status >= 400 && res.status < 500) {
const errBody = await res.text();
throw new Error(`HTTP ${res.status} [${url}]: ${errBody}`);
}
throw new Error(`HTTP ${res.status} ${res.statusText} [${url}]`);
}
return res;
} catch (e) {
if (timeoutId) clearTimeout(timeoutId);
if (e.name === 'AbortError') e.message = `Timeout (${timeoutMs}ms) [${url}]`;
// Don't retry if it's a permanent error
if (e.message.includes('HTTP 4') && !e.message.includes('429')) throw e;
if (i === retries - 1) throw e;
const delay = 1000 * Math.pow(2, i);
console.warn(`[FeishuClient] Fetch failed (${e.message}) [${url}]. Retrying in ${delay}ms...`);
await new Promise(r => setTimeout(r, delay));
}
}
}
/**
* Get Tenant Access Token (Cached)
*/
async function getToken(forceRefresh = false) {
const now = Math.floor(Date.now() / 1000);
if (!forceRefresh && fs.existsSync(TOKEN_CACHE_FILE)) {
try {
const cached = JSON.parse(fs.readFileSync(TOKEN_CACHE_FILE, 'utf8'));
if (cached.token && cached.expire > now + 60) return cached.token;
} catch (e) {}
}
try {
const res = await fetchWithRetry('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(`API Error: ${data.msg}`);
try {
const cacheData = { token: data.tenant_access_token, expire: now + data.expire };
const cacheDir = path.dirname(TOKEN_CACHE_FILE);
if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true });
fs.writeFileSync(TOKEN_CACHE_FILE, JSON.stringify(cacheData, null, 2));
} catch (e) {}
return data.tenant_access_token;
} catch (e) {
console.error('[FeishuClient] Failed to get token:', e.message);
throw e;
}
}
/**
* Authenticated Fetch with Auto-Refresh
*/
async function fetchWithAuth(url, options = {}) {
let token = await getToken();
let headers = { ...options.headers, 'Authorization': `Bearer ${token}` };
try {
let res = await fetchWithRetry(url, { ...options, headers });
// Handle JSON Logic Errors (200 OK but code != 0)
const clone = res.clone();
try {
const data = await clone.json();
// Codes for invalid token: 99991663, 99991664, 99991661, 99991668
if ([99991663, 99991664, 99991661, 99991668].includes(data.code)) {
throw new Error('TokenExpired');
}
} catch (jsonErr) {
// If response isn't JSON or TokenExpired, ignore here
if (jsonErr.message === 'TokenExpired') throw jsonErr;
}
return res;
} catch (e) {
if (e.message.includes('HTTP 401') || e.message === 'TokenExpired') {
console.warn(`[FeishuClient] Token expired. Refreshing...`);
token = await getToken(true);
headers = { ...options.headers, 'Authorization': `Bearer ${token}` };
return await fetchWithRetry(url, { ...options, headers });
}
throw e;
}
}
// --- Simplified Export Wrappers for specific formats ---
function resolveReceiveIdType(receiveId) {
if (typeof receiveId !== 'string') return 'open_id';
if (receiveId.startsWith('oc_')) return 'chat_id';
if (receiveId.startsWith('ou_')) return 'open_id';
if (receiveId.includes('@')) return 'email';
return 'open_id';
}
async function sendMessage(receive_id, msg_type, content) {
const idType = resolveReceiveIdType(receive_id);
const url = `https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=${idType}`;
const body = { receive_id, msg_type, content: typeof content === 'string' ? content : JSON.stringify(content) };
const res = await fetchWithAuth(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
return await res.json();
}
async function sendText(receive_id, text) {
return sendMessage(receive_id, 'text', JSON.stringify({ text }));
}
async function sendPost(receive_id, postContent) {
return sendMessage(receive_id, 'post', JSON.stringify(postContent));
}
async function sendCard(receive_id, cardContent) {
return sendMessage(receive_id, 'interactive', JSON.stringify(cardContent));
}
module.exports = { getToken, fetchWithRetry, fetchWithAuth, sendMessage, sendText, sendPost, sendCard, resolveReceiveIdType };
{
"name": "feishu-common",
"version": "1.0.0",
"description": "Common Feishu API client and authentication utilities for OpenClaw skills.",
"main": "index.js",
"scripts": {
"test": "echo \"No tests specified\" && exit 0"
},
"dependencies": {
"axios": "^1.6.0",
"dotenv": "^16.3.1"
},
"author": "OpenClaw Evolution",
"license": "MIT"
}
Related skills
FAQ
Which environment variables does it need?
FEISHU_APP_ID and FEISHU_APP_SECRET must be set before using any Feishu skill.
What helpers does it export?
getToken, fetchWithRetry, and fetchWithAuth from index.js, with a feishu-client.js compatibility alias.