Loading...
Loading...
Official Lark/Feishu plugin for OpenClaw that enables AI agents to interact with Lark workspaces including messages, docs, bases, calendars, and tasks
npx skill4agent add aradotso/hermes-skills openclaw-lark-integrationSkill by ara.so — Hermes Skills collection.
openclaw -vnpm install -g openclawnpm install -g @larksuite/openclaw-larkpnpm add -g @larksuite/openclaw-larkim:messageim:message:read_as_userim:chatdocx:documentdrive:drivebitable:appbitable:recordsheets:spreadsheetcalendar:calendarcalendar:eventtask:task// openclaw.config.ts
export default {
channels: {
lark: {
appId: process.env.LARK_APP_ID,
appSecret: process.env.LARK_APP_SECRET,
verificationToken: process.env.LARK_VERIFICATION_TOKEN,
encryptKey: process.env.LARK_ENCRYPT_KEY,
// Optional: Security policies
policies: {
allowPrivateChat: true,
allowGroupChat: false, // Disable by default for security
groupAllowlist: [], // Whitelist specific groups
},
// Optional: Default settings
enableInteractiveCards: true,
enableStreamingResponse: true,
}
}
}export LARK_APP_ID="your_app_id"
export LARK_APP_SECRET="your_app_secret"
export LARK_VERIFICATION_TOKEN="your_verification_token"
export LARK_ENCRYPT_KEY="your_encrypt_key"openclaw start --channel larkopenclaw start --config openclaw.config.ts# Start with Lark channel
openclaw start --channel lark
# Check version
openclaw -v
# View help
openclaw --help
# Stop OpenClaw
openclaw stop// Example skill that sends a Lark message
import { SkillContext } from 'openclaw';
export async function sendLarkMessage(context: SkillContext, message: string) {
// OpenClaw automatically routes to Lark channel
await context.channel.sendMessage({
text: message,
chatId: context.chatId
});
}export async function getRecentMessages(context: SkillContext, limit: number = 10) {
const messages = await context.channel.getMessages({
chatId: context.chatId,
limit: limit
});
return messages.map(msg => ({
sender: msg.sender,
content: msg.content,
timestamp: msg.timestamp
}));
}export async function createDocument(
context: SkillContext,
title: string,
content: string
) {
const doc = await context.channel.lark.createDoc({
title: title,
content: content,
folderToken: context.workspace.defaultFolder
});
return {
docId: doc.docToken,
url: doc.url
};
}export async function addBaseRecord(
context: SkillContext,
baseId: string,
tableId: string,
fields: Record<string, any>
) {
const record = await context.channel.lark.base.createRecord({
appToken: baseId,
tableId: tableId,
fields: fields
});
return record;
}
export async function queryBaseRecords(
context: SkillContext,
baseId: string,
tableId: string,
filter?: string
) {
const records = await context.channel.lark.base.listRecords({
appToken: baseId,
tableId: tableId,
filter: filter, // e.g., "AND(CurrentValue.[Status] = 'Active')"
pageSize: 100
});
return records.items;
}export async function createCalendarEvent(
context: SkillContext,
summary: string,
startTime: string,
endTime: string,
attendees?: string[]
) {
const event = await context.channel.lark.calendar.createEvent({
summary: summary,
startTime: { timestamp: startTime },
endTime: { timestamp: endTime },
attendees: attendees?.map(email => ({ email }))
});
return {
eventId: event.eventId,
htmlLink: event.htmlLink
};
}export async function createTask(
context: SkillContext,
summary: string,
description: string,
dueDate?: string
) {
const task = await context.channel.lark.task.createTask({
summary: summary,
description: description,
due: dueDate ? { date: dueDate } : undefined
});
return task;
}export default {
channels: {
lark: {
// ... credentials
policies: {
// Allow private chats (default: true)
allowPrivateChat: true,
// Disable all group chats (recommended for security)
allowGroupChat: false,
// Or allow specific groups only
allowGroupChat: true,
groupAllowlist: [
'oc_xxxxxxxxxxxxx', // Group chat ID
'oc_yyyyyyyyyyyyy'
],
// Require confirmation for sensitive operations
requireConfirmation: {
deleteDocument: true,
deleteBaseRecord: true,
sendMessageToGroup: true
}
}
}
}
}export default {
channels: {
lark: {
// ... credentials
groupSettings: {
'oc_xxxxxxxxxxxxx': {
enabled: true,
allowedSkills: ['search', 'summarize'], // Restrict skills
customSystemPrompt: 'You are a helpful assistant for the engineering team.',
maxTokens: 4000
}
}
}
}
}export default {
channels: {
lark: {
// ... credentials
enableStreamingResponse: true,
enableInteractiveCards: true
}
}
}export async function sendCardWithActions(context: SkillContext) {
await context.channel.sendCard({
header: {
title: 'Confirm Action',
template: 'blue'
},
elements: [
{
tag: 'div',
text: {
tag: 'plain_text',
content: 'Do you want to proceed with this operation?'
}
},
{
tag: 'action',
actions: [
{
tag: 'button',
text: { tag: 'plain_text', content: 'Confirm' },
type: 'primary',
value: { action: 'confirm' }
},
{
tag: 'button',
text: { tag: 'plain_text', content: 'Cancel' },
type: 'default',
value: { action: 'cancel' }
}
]
}
]
});
}import { Skill } from 'openclaw';
export const messageHandlerSkill: Skill = {
name: 'lark-message-handler',
description: 'Handle incoming Lark messages',
async execute(context) {
const { message, sender } = context;
// Process message
if (message.includes('help')) {
return await context.reply('How can I assist you?');
}
// Search message history
if (message.startsWith('search:')) {
const query = message.substring(7);
const results = await context.channel.searchMessages({ query });
return results;
}
// Default response
return await context.reply('Message received');
}
};export async function createWeeklyReport(context: SkillContext) {
const today = new Date();
const title = `Weekly Report - ${today.toISOString().split('T')[0]}`;
// Gather data from Base
const tasks = await context.channel.lark.base.listRecords({
appToken: process.env.LARK_TASK_BASE_ID!,
tableId: 'tblxxxxxxxx',
filter: "AND(CurrentValue.[CompletedAt] >= DATE_SUB(TODAY(), 7))"
});
// Create formatted document
const content = `
# ${title}
## Completed Tasks
${tasks.items.map(t => `- ${t.fields.Name}`).join('\n')}
## Summary
Total tasks completed: ${tasks.items.length}
`;
const doc = await context.channel.lark.createDoc({
title,
content
});
return doc.url;
}export async function batchUpdateRecords(
context: SkillContext,
baseId: string,
tableId: string,
updates: Array<{ recordId: string; fields: Record<string, any> }>
) {
// Lark API supports batch operations
const result = await context.channel.lark.base.batchUpdateRecords({
appToken: baseId,
tableId: tableId,
records: updates
});
return {
updated: result.records.length,
records: result.records
};
}openclaw statusallowGroupChatopenclaw logs --channel larkim:messageopenclaw start --config ./path/to/config.tsenableStreamingResponse: true