
Feishu Attendance
- 2 installs
- 20 repo stars
- Updated April 11, 2026
- autogame-17/feishu-skills
feishu-attendance is a Claude skill that monitors Feishu (Lark) attendance for late, early-leave, and absent employees and reports a summary to an admin.
About
This skill monitors daily Feishu (Lark) attendance records. It detects late arrivals, early leaves, and absences, notifies employees of abnormalities, and sends a rich interactive card summary to an admin. It is holiday-aware via the timor.tech API, caches the user list and holiday data for performance, and enters a safe mode that suppresses notifications if the holiday API fails to prevent spam.
- Monitors Feishu (Lark) attendance for late, early-leave, and absence, then reports a summary to admin
- Holiday-aware via the timor.tech API with a safe mode that suppresses notifications if the holiday API fails
Feishu Attendance by the numbers
- 2 all-time installs (skills.sh)
- Ranked #1,842 of 2,719 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
feishu-attendance capabilities & compatibility
- Capabilities
- feishu batch sender · feishu calendar
- Use cases
- project management
- Runs
- Runs locally
What feishu-attendance says it does
Monitor Feishu (Lark) attendance records. Check for late, early leave, or absent employees and report to admin.
npx skills add https://github.com/autogame-17/feishu-skills --skill feishu-attendanceAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| repo stars | ★ 20 |
| Last updated | April 11, 2026 |
| Repository | autogame-17/feishu-skills ↗ |
What it does
Monitor daily Feishu attendance for lateness, early leave, and absence and report a summary card to an admin.
When should I use this skill?
When a user wants to check or monitor daily Feishu attendance and report abnormalities to an admin.
What you get
- A rich interactive attendance summary card sent to admin
By the numbers
- 3 abnormality types detected (late, early leave, absence)
- user list cached with 24h TTL
- 3 Feishu permission scopes required
Files
Feishu Attendance Skill
Monitor daily attendance, notify employees of abnormalities, and report summary to admin.
Features
- Smart Checks: Detects Late, Early Leave, and Absence.
- Holiday Aware: Auto-detects holidays/weekends via
timor.techAPI. - Safe Mode: Disables user notifications if holiday API fails (prevents spam).
- Caching: Caches user list (24h TTL) and holiday data for performance.
- Reporting: Sends rich interactive cards to Admin.
Usage
# Check today's attendance (Default)
node index.js check
# Check specific date
node index.js check --date 2023-10-27
# Dry Run (Test mode, no messages sent)
node index.js check --dry-runPermissions Required
attendance:report:readonlycontact:user.employee:readonlyim:message:send_as_bot
{
"ownerId": "kn7apafdj4thknczrgxdzfd2v1808svf",
"slug": "feishu-attendance",
"version": "1.0.9",
"publishedAt": 1770118153420
}{
"version": 1,
"registry": "https://clawhub.ai",
"slug": "feishu-attendance",
"installedVersion": "1.0.9",
"installedAt": 1770639836290
}
const yargs = require('yargs/yargs');
const { hideBin } = require('yargs/helpers');
const { getAllUsers, getAttendance, sendMessage } = require('./lib/api');
const fs = require('fs');
const path = require('path');
const { getCST } = require('../time-helper');
// Allow overriding Admin ID via env var, fallback to Master's ID
const ADMIN_ID = process.env.FEISHU_ADMIN_ID;
// Cache Logic
const CACHE_DIR = path.resolve(__dirname, '../../memory/attendance_cache');
if (!fs.existsSync(CACHE_DIR)) fs.mkdirSync(CACHE_DIR, { recursive: true });
// --- Cache Helpers ---
const USER_CACHE_FILE = path.join(CACHE_DIR, 'users_cache.json');
async function getAllUsersCached() {
try {
if (fs.existsSync(USER_CACHE_FILE)) {
const stat = fs.statSync(USER_CACHE_FILE);
const age = Date.now() - stat.mtimeMs;
if (age < 24 * 60 * 60 * 1000) { // 24h TTL
console.log(`Using cached user list (${Math.floor(age/1000/60)} min old).`);
return JSON.parse(fs.readFileSync(USER_CACHE_FILE, 'utf8'));
}
}
} catch(e) {}
// Fetch fresh
console.log('Fetching fresh user list...');
const users = await getAllUsers();
// Write cache
try { fs.writeFileSync(USER_CACHE_FILE, JSON.stringify(users)); } catch(e) { console.warn('Failed to cache users:', e.message); }
return users;
}
function getHolidayCache(date) {
const file = path.join(CACHE_DIR, `holiday_${date}.json`);
if (fs.existsSync(file)) {
try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch (e) {}
}
return null;
}
function saveHolidayCache(date, data) {
const file = path.join(CACHE_DIR, `holiday_${date}.json`);
try { fs.writeFileSync(file, JSON.stringify(data)); } catch (e) {}
}
async function main() {
const argv = yargs(hideBin(process.argv))
.option('date', {
alias: 'd',
type: 'string',
description: 'Date to check (YYYY-MM-DD)',
default: getCST().toISOString().split('T')[0]
})
.option('notify', {
alias: 'n',
type: 'boolean',
description: 'Send DM notifications to users (Default: false)',
default: false
})
.option('dry-run', {
type: 'boolean',
description: 'Do not send any messages (even to Admin)',
default: false
})
.command('check', 'Check attendance and notify')
.help()
.argv;
if (!argv._.includes('check')) {
console.log('Use "check" command. Example: node index.js check --date 2023-10-27');
return;
}
let dateStr = argv.date;
// Smart date parsing: Handle "1.27", "01-27" -> "2026-01-27"
const currentYear = getCST().getFullYear();
const shortDateMatch = dateStr.match(/^(\d{1,2})[.-](\d{1,2})$/);
if (shortDateMatch) {
const month = shortDateMatch[1].padStart(2, '0');
const day = shortDateMatch[2].padStart(2, '0');
dateStr = `${currentYear}-${month}-${day}`;
console.log(`Auto-completed date input "${argv.date}" to: ${dateStr}`);
}
const dateInt = parseInt(dateStr.replace(/-/g, ''), 10);
console.log(`Checking attendance for ${dateStr} (${dateInt})...`);
// Holiday Check
let isWorkday = true;
let dayType = 'Workday';
let holidayCheckWarning = '';
try {
console.log('Checking holiday status...');
let holidayData = getHolidayCache(dateStr);
if (holidayData) {
console.log('Using cached holiday data.');
} else {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5s timeout
const holidayRes = await fetch(`https://timor.tech/api/holiday/info/${dateStr}`, { signal: controller.signal });
clearTimeout(timeoutId);
if (!holidayRes.ok) throw new Error(`HTTP ${holidayRes.status}`);
holidayData = await holidayRes.json();
saveHolidayCache(dateStr, holidayData);
}
// type: 0=workday, 1=weekend, 2=holiday, 3=makeup
if (holidayData && holidayData.type) {
const type = holidayData.type.type;
if (type === 1 || type === 2) {
isWorkday = false;
dayType = type === 1 ? 'Weekend' : 'Holiday';
} else {
dayType = type === 3 ? 'Makeup Workday' : 'Workday';
}
console.log(`Date ${dateStr} is a ${dayType} (type: ${type}).`);
}
} catch (e) {
console.error('Failed to check holiday API (using default: Workday):', e.message);
holidayCheckWarning = `⚠️ *Warning*: Holiday check failed (${e.message}). Assumed Workday.`;
}
// Safety: If holiday check failed, DO NOT spam users with DMs. Only report to admin.
const safeMode = !!holidayCheckWarning;
if (safeMode) {
console.log("SAFE MODE ACTIVE: User DMs disabled due to holiday API failure.");
}
try {
// 1. Get all users
const users = await getAllUsersCached();
console.log(`Found ${users.length} users.`);
const userMap = {};
users.forEach(u => userMap[u.open_id] = u); // Map open_id to user details
// Also map user_id if available, but response usually uses user_id or open_id depending on token type?
// user_task/query uses user_ids. contact/v3/users returns open_id and user_id.
// Let's create a map for both.
users.forEach(u => {
if(u.user_id) userMap[u.user_id] = u;
});
const userIds = users.map(u => u.user_id).filter(id => !!id); // Use user_id (employee_id) for querying
// Stability: Hybrid ID Support (user_id + open_id fallback)
const employeeIds = [];
const openIds = [];
users.forEach(u => {
if (u.user_id) employeeIds.push(u.user_id);
else if (u.open_id) openIds.push(u.open_id);
});
if (employeeIds.length === 0 && openIds.length === 0) {
console.log("No users found to check.");
return;
}
// 2. Get attendance
console.log('Fetching attendance records...');
let results = [];
if (employeeIds.length > 0) {
console.log(`Querying ${employeeIds.length} users via employee_id...`);
const r1 = await getAttendance(employeeIds, dateInt, 'employee_id');
results = results.concat(r1);
}
if (openIds.length > 0) {
console.log(`Querying ${openIds.length} users via open_id (fallback)...`);
const r2 = await getAttendance(openIds, dateInt, 'open_id');
results = results.concat(r2);
}
// 3. Analyze
const report = {
late: [],
early: [],
absent: [],
total: results.length
};
for (const res of results) {
const userId = res.user_id;
const userName = userMap[userId] ? userMap[userId].name : userId;
const records = res.records || [];
let isLate = false;
let isEarly = false;
let isAbsent = false;
if (records.length === 0) {
// No records usually means "No Shift" or "Rest Day" in Feishu.
// Do NOT mark as absent.
console.log(`[Info] ${userName}: No shift records found.`);
} else {
for (const record of records) {
// Check In Result
if (record.check_in_result === 'Late') isLate = true;
if (record.check_in_result === 'Lack') isAbsent = true; // Lack of check-in
// Check Out Result
if (record.check_out_result === 'Early') isEarly = true;
if (record.check_out_result === 'Lack') isAbsent = true; // Lack of check-out
}
}
// Prepare messages (Only send if enabled and safe)
// Helper to send safely without crashing the loop
const safeSend = async (uid, msg) => {
if (safeMode || !argv.notify || argv.dryRun) return;
try {
await sendMessage(uid, msg);
} catch (e) {
console.error(`[Error] Failed to send DM to ${userName} (${uid}): ${e.message}`);
}
};
if (isAbsent) {
report.absent.push(userName);
console.log(`[Absent] ${userName}`);
await safeSend(userId, `[Attendance Alert] You are marked as ABSENT for ${dateStr}. Please submit a request if this is an error.`);
} else {
if (isLate) {
report.late.push(userName);
console.log(`[Late] ${userName}`);
await safeSend(userId, `[Attendance Alert] You were LATE on ${dateStr}. Please be on time.`);
}
if (isEarly) {
report.early.push(userName);
console.log(`[Early] ${userName}`);
await safeSend(userId, `[Attendance Alert] You left EARLY on ${dateStr}.`);
}
}
}
// 4. Report to Admin (Card Construction)
const cardElements = [];
// Summary Element
const statusEmoji = (!report.late.length && !report.early.length && !report.absent.length) ? '✅' : '⚠️';
let summaryText = `**Type**: ${dayType} ${!isWorkday ? '(No Absent Checks)' : ''}\n**Total Checked**: ${report.total}`;
if (holidayCheckWarning) summaryText += `\n${holidayCheckWarning}`;
if (argv.dryRun) summaryText += `\n(DRY RUN: No messages sent)`;
cardElements.push({
tag: 'div',
text: { tag: 'lark_md', content: summaryText }
});
const cardColor = (report.late.length || report.early.length || report.absent.length) ? 'orange' : 'green';
if (report.late.length > 0) {
cardElements.push({ tag: 'hr' });
cardElements.push({
tag: 'div',
text: { tag: 'lark_md', content: `🔴 **Late**:\n${report.late.join(', ')}` }
});
}
if (report.early.length > 0) {
cardElements.push({ tag: 'hr' });
cardElements.push({
tag: 'div',
text: { tag: 'lark_md', content: `🟡 **Early Leave**:\n${report.early.join(', ')}` }
});
}
if (report.absent.length > 0) {
cardElements.push({ tag: 'hr' });
cardElements.push({
tag: 'div',
text: { tag: 'lark_md', content: `⚫ **Absent**:\n${report.absent.join(', ')}` }
});
}
if (report.late.length === 0 && report.early.length === 0 && report.absent.length === 0) {
cardElements.push({ tag: 'hr' });
cardElements.push({
tag: 'div',
text: { tag: 'lark_md', content: "✅ **All Good!** No anomalies detected today." }
});
}
// Detailed Logs
cardElements.push({ tag: 'hr' });
let details = "";
for (const res of results) {
const userId = res.user_id;
const userName = userMap[userId] ? userMap[userId].name : userId;
const records = res.records || [];
if (records.length > 0) {
const r = records[0];
details += `- ${userName}: In ${r.check_in_record_id ? '✅' : '❌'} | Out ${r.check_out_record_id ? '✅' : '❌'}\n`;
} else {
details += `- ${userName}: No shift\n`;
}
}
// Truncate
const MAX_CHARS = 4000;
if (details.length > MAX_CHARS) details = details.substring(0, MAX_CHARS) + "\n... (truncated)";
cardElements.push({
tag: 'note',
elements: [{ tag: 'plain_text', content: `Detailed Logs:\n${details}` }]
});
const card = {
header: {
title: { tag: 'plain_text', content: `📋 Attendance Report (${dateStr})` },
template: cardColor
},
elements: cardElements
};
console.log('Sending report to Admin (via Card)...');
if (!argv.dryRun) {
// sendMessage in lib/api.js handles objects as interactive cards
await sendMessage(ADMIN_ID, card);
console.log('Report sent.');
} else {
console.log('DRY RUN: Report NOT sent.');
}
console.log('Done.');
console.log(JSON.stringify(report, null, 2));
} catch (error) {
console.error('Error:', error);
}
}
main();
const fetch = require('node-fetch'); // Retain for specialized calls if needed, or refactor to use fetchWithAuth
const { getTenantAccessToken } = require('./auth');
const { fetchWithAuth, fetchWithRetry: fetchCommon } = require('../../feishu-common/index.js'); // Import new common
// Re-implement executeWithAuthRetry using common if possible, or adapt.
// Ideally, we replace manual token handling with fetchWithAuth where appropriate.
// But `fetchWithAuth` in feishu-common handles token refresh internally.
// So we can simplify `getAllUsers` and others to use `fetchWithAuth`.
async function getAllUsers() {
let users = [];
let pageToken = '';
do {
const url = `https://open.feishu.cn/open-apis/contact/v3/users?department_id_type=open_department_id&department_id=0&page_size=50${pageToken ? '&page_token=' + pageToken : ''}`;
// feishu-common's fetchWithAuth handles the Authorization header
const response = await fetchWithAuth(url);
const data = await response.json();
if (data.code !== 0) {
console.warn('Failed to fetch users:', data.msg);
break;
}
if (data.data && data.data.items) {
users = users.concat(data.data.items);
}
pageToken = data.data && data.data.page_token;
} while (pageToken);
return users;
}
async function getAttendance(userIds, dateInt, idType = 'employee_id') {
const url = `https://open.feishu.cn/open-apis/attendance/v1/user_tasks/query?employee_type=${idType}`;
const chunks = [];
for (let i = 0; i < userIds.length; i += 50) {
chunks.push(userIds.slice(i, i + 50));
}
let allTasks = [];
for (const chunk of chunks) {
const response = await fetchWithAuth(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
user_ids: chunk,
check_date_from: dateInt,
check_date_to: dateInt
})
});
const data = await response.json();
if (data.code === 0 && data.data && data.data.user_task_results) {
allTasks = allTasks.concat(data.data.user_task_results);
} else {
console.error(`Attendance query failed for chunk (${idType}):`, JSON.stringify(data));
}
}
return allTasks;
}
async function sendMessage(receiveId, content) {
let type = 'user_id';
if (receiveId.startsWith('ou_')) type = 'open_id';
else if (receiveId.startsWith('oc_')) type = 'chat_id';
let msgType = 'text';
let bodyContent = '';
if (typeof content === 'object') {
msgType = 'interactive';
bodyContent = JSON.stringify(content);
} else {
msgType = 'text';
bodyContent = JSON.stringify({ text: content });
}
const response = await fetchWithAuth(`https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=${type}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
receive_id: receiveId,
msg_type: msgType,
content: bodyContent
})
});
return await response.json();
}
module.exports = {
getAllUsers,
getAttendance,
sendMessage
};
const { getToken } = require('../../feishu-common/index.js');
async function getTenantAccessToken(forceRefresh = false) {
// Delegate to feishu-common
return await getToken(forceRefresh);
}
module.exports = {
getTenantAccessToken
};
{
"name": "feishu-attendance",
"version": "1.0.9",
"description": "Feishu Attendance Monitoring Skill",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"dotenv": "^16.3.1",
"node-fetch": "^2.7.0",
"yargs": "^17.7.2"
}
}
Related skills
FAQ
What does feishu-attendance detect?
Late arrivals, early leaves, and absences, using holiday awareness so it does not flag weekends or holidays.
How does it avoid spamming on holidays?
It has a safe mode that disables user notifications if the timor.tech holiday API fails.