English | 中文
OpenClaw already ships with a bundled Feishu plugin. Use the bundled plugin if you only need standard message delivery, baseline routing, and the default Feishu tool surface. feishu-plus exists for deployments that outgrew that baseline.
Our actual deployment needed four things the bundled path did not give us cleanly enough: shared context across multiple agents inside the same group chat, group-based slash command permissions, much stronger long-markdown delivery for Feishu, and deeper spreadsheet / bitable workflows. Command permissions matter especially in enterprise multi-user deployments where different teams need different access levels — management and dev teams may switch models freely, creative teams get access to image-generation skills, while junior staff stays on the default model. This repository keeps the upstream Feishu foundation, backports newer bundled changes from openclaw/openclaw, and layers on the production behavior that motivated the fork in the first place.
Two additions define this fork:
GroupSense is a context-enhancement layer for multi-agent group chats. It lets you @ one agent and continue a discussion that another agent was already part of, without manually replaying the whole conversation. The plugin achieves that by combining milestone summaries with recent raw group history and injecting both back into later prompts.
Command Control is a group-based permission system for slash commands. In multi-user enterprise deployments, not everyone should be able to run every command — /model and /think should probably be off-limits for general staff, while admins and power users get full access. Command Control lets you define user groups that map directly to your org structure, assign each group an allowlist, denylist, or full access, and have the rules take effect on save without a gateway restart.
This repository is also derived from m1heng/clawdbot-feishu, which remains part of the project lineage.
This distribution is Feishu-only. Some inherited source files still use Lark naming because the official SDK package is @larksuiteoapi/node-sdk, but Lark is not a supported target here.
| Need | Bundled Feishu | feishu-plus |
|---|---|---|
| Standard Feishu channel integration | Yes | Yes |
| Multiple agents, each with its own Feishu app | No (single app) | Yes |
| One agent serving multiple Feishu orgs | No | Yes |
| Shared context across multiple agents in one group | No | Yes |
| Milestone summaries + recent group history injection | No | Yes |
| Long-markdown delivery tuned for Feishu-heavy usage | Baseline | Yes |
| Sheet workflow as a first-class tool surface | No | Yes |
| Expanded bitable field / record operations | Partial | Yes |
| Group-based slash command permissions | No | Yes |
- Multi-account architecture. The bundled Feishu plugin connects a single Feishu app to the entire OpenClaw instance.
feishu-plussupports two layers of multi-account: (1) each agent can have its own dedicated Feishu app with independent credentials, permissions, and behavior settings; (2) a single agent can be bound to multiple Feishu apps across different organizations, so the same agent serves users in your personal tenant and your corporate tenant simultaneously. Inbound messages route automatically; outbound tool calls use explicit account selection to prevent cross-org misrouting. - GroupSense. Multi-agent group chats become materially more natural because later prompts can see milestone summaries and recent raw group history, not just the latest
@mention. - Milestone-aware prompting. Group discussion windows are periodically summarized via LLM and stored under
~/.openclaw/shared-knowledge/feishu-group-milestones. Bot outbound messages are also recorded. Group sessions roll over after each dispatch so every turn uses only the refined GroupSense context. - Long-form reply delivery.
textChunkLimit, card rendering, and Feishu-specific reply behavior are tuned for large markdown outputs. - Dedicated sheet tooling. This fork ships a full sheet workflow instead of treating spreadsheets as a side case.
- Expanded bitable tooling. The bitable surface covers metadata, fields, records, and batch deletion in a way that fits real operations.
- Retained upstream tooling. Doc, wiki, drive, chat, app-scope diagnostics, and other bundled Feishu capabilities are still here.
- Group-based command control. Slash command permissions can be restricted per user group. Each group chooses an allowlist, denylist, or full access. Groups are user-defined and can reflect existing org structure.
- OpenClaw
>= 2026.3.13 - Node.js
>= 20 - One or more Feishu self-built apps
- If you enable
milestoneContext: OpenClaw>= 2026.3.24(for theagent-runtimesimple-completion API)
git clone https://github.com/hchen13/feishu-plus.git
cd feishu-plus
npm install
openclaw plugins install /absolute/path/to/feishu-plusopenclaw plugins install github:hchen13/feishu-plus{
"plugins": {
"allow": ["feishu-plus"],
"load": {
"paths": ["/absolute/path/to/feishu-plus"]
},
"entries": {
"feishu-plus": {
"enabled": true
}
}
}
}{
"channels": {
"feishu": {
"enabled": true,
"connectionMode": "websocket",
"renderMode": "auto",
"textChunkLimit": 30000,
"milestoneContext": {
"enabled": false,
"llmInputTrace": {
"enabled": false
}
},
"accounts": {
"assistant": {
"enabled": true,
"appId": "cli_xxxxx",
"appSecret": "xxxx",
"dmPolicy": "open",
"groupPolicy": "open",
"requireMention": true,
"chunkMode": "length",
"textChunkLimit": 30000,
"streaming": true
}
}
}
}
}Single-account deployments can often stop there. Multi-account deployments should also add explicit Feishu accountId bindings for each agent that will use Feishu tools.
For multi-account setups, bind each agent to its Feishu accountId(s) at the OpenClaw level:
{
"bindings": [
{
"agentId": "assistant",
"match": {
"channel": "feishu",
"accountId": "assistant"
}
},
{
"agentId": "advisor",
"match": {
"channel": "feishu",
"accountId": "advisor"
}
}
]
}An agent can be bound to multiple Feishu apps across different organizations. For example, the same agent serving both a personal tenant and a corporate tenant:
{
"bindings": [
{
"agentId": "assistant",
"match": { "channel": "feishu", "accountId": "assistant" }
},
{
"agentId": "assistant",
"match": { "channel": "feishu", "accountId": "assistant-corp" }
}
]
}How multi-binding routing works:
- Inbound message → reply: the plugin knows which Feishu app received the message and replies through the same app. Zero ambiguity.
- Agent-initiated tool calls: the agent must pass
accountId(orasAccountIdfor tools that use it) to specify which Feishu app to use. If the agent omits the parameter and multiple bindings exist, the tool raises an explicit routing error — no silent fallback, no cross-org misrouting. - All Feishu tools (
feishu_doc,feishu_wiki,feishu_drive,feishu_chat,feishu_perm,feishu_app_scopes,feishu_id,feishu_id_admin, bitable, sheet, task) support explicit account selection via theiraccountIdorasAccountIdparameter.
im:messageim:message.p2p_msg:readonlyim:message.group_at_msg:readonlyim:message:send_as_botim:resourcecontact:user.base:readonly
contact:user.base:readonly matters because resolveSenderNames is enabled by default. Without it, message delivery still works, but sender-name lookup may fail and the agent may surface a permission-grant notice back to the user.
Grant only what your deployment actually uses:
docx:documentordocx:document:readonlydocx:document.block:convertdrive:driveordrive:drive:readonlywiki:wikiorwiki:wiki:readonlybitable:apporbitable:app:readonlysheets:spreadsheetorsheets:spreadsheet:readonlytask:task:readortask:task:writedrive:permissionif you enablepermim:message:updateif you rely on message/card patch flows outside the normal reply pipeline
If you enable account-level streaming: true, also grant:
cardkit:card:write
This is the exact permission used by the streaming-card path. The plugin creates a Card Kit card entity through /cardkit/v1/cards and then updates that card as text arrives.
Important distinction:
- Regular interactive markdown cards can still be sent through the standard Feishu message API.
- Streaming cards are different. They use the Card Kit entity API, so
streaming: trueis not only a UI toggle. It is a separate permission path.
If the permission is missing, the typical runtime symptom is a log like:
Create card failed: Access denied ... [cardkit:card:write]
Minimum:
im.message.receive_v1im.chat.member.bot.added_v1im.chat.member.bot.deleted_v1
Enable these when you use the corresponding features:
im.message.reaction.created_v1ifreactionNotificationsis notoffcard.action.triggerif your deployment uses interactive card actions as input
Transport-specific setup:
connectionMode: "websocket": configure Long connectionconnectionMode: "webhook": configure Request URL,verificationToken, andencryptKey
The webhook path is hardened in code: signature validation, body limits, content-type checks, and rate limiting are all enforced.
API scopes alone are not enough. The bot must also be added to or shared on the actual resource:
- docs
- wiki spaces
- drive folders and files
- bitables
- sheets
If authentication succeeds but results are empty, or permission errors persist, check sharing before debugging plugin logic.
open_id is app-scoped in Feishu. The same human will have different open_id values under different Feishu apps. Do not reuse an open_id captured from app A when operating through app B.
GroupSense is the public name for this fork's context-enhancement layer for multi-agent group chats.
Operationally, it does five things:
- Records recent group turns per chat — including bot-initiated outbound messages (via the Message tool, etc.).
- Periodically summarizes a discussion window into milestones using an LLM call (via the plugin-sdk
agent-runtimeAPI). - Stores those milestones under
~/.openclaw/shared-knowledge/feishu-group-milestones. - Injects milestone summaries plus recent raw group history back into later prompts.
- By default, rolls over the group session after each dispatch (stateless mode), so every turn starts with only the refined GroupSense context — no LLM history accumulation, no wasted tokens.
When plugins.slots.contextEngine is set to "groupsense", the plugin skips that post-dispatch rollover and lets the context engine hide prior transcript turns from the model while keeping the physical session intact for Control UI.
For prompt-payload debugging, you can temporarily enable:
"milestoneContext": {
"enabled": true,
"llmInputTrace": {
"enabled": true
}
}That writes the real llm_input hook payload for Feishu group turns to ~/.openclaw/shared-knowledge/feishu-groupsense-context-engine/llm-input-traces, including systemPrompt, prompt, and historyMessages.
With trace dumping off, the gateway still emits lightweight feishu[groupsense] log lines for checkpoint capture, checkpoint hits, delegate fallback, and llm_input prompt markers so you can follow behavior without writing full payloads to disk.
The resulting user experience is straightforward: one agent can respond with immediate context about what another agent recently argued, decided, or promised, so the group feels less like isolated bots and more like a coherent team conversation.
GroupSense uses the OpenClaw plugin-sdk agent-runtime API (prepareSimpleCompletionModel + completeWithPreparedSimpleCompletionModel) to call an LLM for milestone extraction. No separate summarizer agent or gateway RPC is needed.
The model defaults to the global agents.defaults.model.primary in openclaw.json. You can override it per-account with the milestoneContext.model field:
"milestoneContext": {
"enabled": true,
"model": "zhipu-coding/GLM-5"
}Format is provider/modelId. If the LLM call fails, the plugin falls back to a regex-based summarizer. The feature still runs, but summary quality is materially worse.
-
textChunkLimitis the main knob for keeping a long markdown reply inside a single Feishu message or card. -
chunkModeonly matters after the chunk limit is exceeded. -
renderMode: "raw"keeps replies in the plain message path. -
renderMode: "auto"is intentionally conservative. In current code it mainly upgrades replies to cards when content clearly looks like code blocks or markdown tables. -
renderMode: "card"always uses interactive markdown cards and gives the clearest streaming-card behavior. -
streaming: trueenables the Feishu Card Kit streaming path for that account. It does not guarantee a smooth typewriter effect by itself; visible smoothness still depends on how often upstream partial text arrives. -
This fork explicitly sets
disableBlockStreaming: truefor Feishu replies so OpenClaw block flushes do not prematurely close the streaming card. -
Live reasoning display — when
streaming: true, the card includes a collapsible "Reasoning" panel that shows live model thinking, auto-collapsing the moment the model starts producing its answer. Two conditions must both hold for the panel to light up:- The agent's model must support reasoning (e.g. Claude Opus/Sonnet 4.x, gpt-5.x, GLM-5). Check
models.providers[*].models[*].reasoning: truein your config. - The agent must be configured with
reasoningDefault: "stream"inagents.list[]. This is a per-agent field — OpenClaw's current schema does not acceptagents.defaults.reasoningDefault, so there is no true global toggle; you have to set it on every agent you want it on.
Example (add one line per agent that should stream reasoning):
{ "agents": { "list": [ { "id": "main", "name": "…", "workspace": "…", "reasoningDefault": "stream" } ] } }Accepted values:
"off"— no reasoning requested. Default when unset."on"— reasoning requested from the model, returned as an inline reply item formattedReasoning:\n_line1_\n_line2_and appended to the normal reply stream. This mode is designed for channels without a separate reasoning lane (e.g. plain text output, Discord). In Feishu specifically, the streaming card's collapsible panel ignores this mode because the panel only listens to theonReasoningStreamcallback."stream"— reasoning streamed via the plugin'sonReasoningStreamcallback and rendered into the card's collapsible "Reasoning" panel in real-time. This is the mode you want for the Feishu card UX. The panel is inserted lazily on the first reasoning event, so users never see an empty "Reasoning" frame when the model produces no thinking tokens.
Heads-up for OpenAI reasoning models (
gpt-5.xviaopenai-codexprovider): OpenAI returns reasoning as an encrypted blob for server-to-server continuity, not as streamable plaintext thinking tokens. This means even withreasoningDefault: "stream"and a liveonReasoningStreamwire, the callback receives little or no content for OpenAI reasoning models — you will see the panel appear only briefly or not at all. For a visible streaming reasoning experience, use an Anthropic Claude model (Opus/Sonnet 4.x) with extended thinking enabled; Anthropic streams raw reasoning tokens viathinking_deltaevents, which map directly onto the card's reasoning lane.Tip: agents whose output never reaches a human reader (e.g. a
summarizerthat just writes to disk) gain nothing from"stream"and will burn extra tokens — leave those agents on the default.Reload: restart the gateway after editing the config (
openclaw gateway restart). The gateway readsagents.listat startup and does not hot-reload it. - The agent's model must support reasoning (e.g. Claude Opus/Sonnet 4.x, gpt-5.x, GLM-5). Check
Practical reading:
- If your priority is long markdown integrity, start with
textChunkLimit. - If your priority is visible streaming-card UX, tune
renderModeand upstream partial cadence first.
These options matter in real deployments and are worth understanding:
requireMention: whether group replies require an explicit@botgroupPolicy/groupAllowFrom: whether the group itself is allowedgroupSenderAllowFrom: optional sender-level allowlist inside allowed groupsgroupSessionScope: how group sessions are isolatedreplyInThread: whether replies should create or continue Feishu topic threadsreactionNotifications: whether reactions become synthetic inbound messagestypingIndicator: whether the bot adds a temporary typing reaction before replyingresolveSenderNames: whether the bot resolves display names through the Feishu contact APIdynamicAgentCreation: optional DM-only mode that auto-creates one agent per direct-message user
groupSessionScope supports:
group: one session per groupgroup_sender: one session per(group + sender)group_topic: one session per topic thread inside a groupgroup_topic_sender: one session per(group + topic + sender)
topicSessionMode still exists for backward compatibility, but groupSessionScope is the real knob now.
By default, all users can only run a small set of safe commands (/status, /new, /reset, /compact). To grant access to more commands — or to fine-tune permissions per user group — configure commandControl under channels.feishu.
Two fields work together:
userGroups— define named sets of Feishu user or department IDs once, then reference them by name anywherecommandControl.groups— ordered rules that map group membership to command permissions
When a slash command arrives, the sender is matched against the groups list top-to-bottom. The first matching group's rule applies. If no group matches, a built-in safe fallback applies: /status, /new, /reset, and /compact are permitted; all other commands are denied.
If commandControl is not configured, the safe defaults still apply: /status, /new, /reset, and /compact are available to all users; everything else is denied until a group rule explicitly permits it.
"channels": {
"feishu": {
"userGroups": {
"admin": ["admin_user_id_1", "admin_user_id_2"],
"tech": ["user_id_a", "user_id_b", "user_id_c"],
"design": ["user_id_x", "user_id_y"]
},
"commandControl": {
"blockMessage": "You don't have permission to use this command.",
"groups": [
{
"name": "admin",
"members": ["@admin"],
"commands": "*"
},
{
"name": "tech",
"members": ["@tech"],
"except": ["/dangerous-command"]
},
{
"name": "design",
"members": ["@design"],
"commands": ["/new", "/reset", "/help", "/status"]
},
{
"name": "default",
"members": ["*"],
"commands": ["/new", "/help", "/status"]
}
]
}
}
}Each group rule uses exactly one of:
| Field | Mode | Behavior |
|---|---|---|
commands: "*" |
allow-all | All commands are permitted, including ones added by future plugins |
commands: [...] |
allowlist | Only the listed commands are permitted; new commands are not automatically allowed |
except: [...] |
denylist | All commands are permitted except the listed ones; new commands are automatically allowed |
commands and except cannot both be set on the same group — the schema rejects that config at load time.
members accepts:
@groupName— reference a key inuserGroups; the group is expanded to its ID list at match time- A Feishu
user_id(tenant-scoped, consistent across all apps) — recommended ou_xxx— a raw Feishuopen_id(app-scoped, different per Feishu app) — avoid if possible"*"— matches any sender; use as the last entry for a catch-all default rule
Always prefer user_id over open_id. A person's user_id is the same across all Feishu apps in the same tenant, so one entry per person is enough. An open_id is different for each Feishu app — you would need N entries per person for N bots. See Feishu app permissions for how to enable user_id resolution.
IDs are normalized the same way as allowFrom entries. The feishu: prefix is stripped automatically if present.
If a sender is not covered by any group rule — or if commandControl is not configured at all — the fallback allows only /status, /new, /reset, and /compact. All other commands return the blockMessage.
This is the default for everyone. To give a user or group access to more commands, add an explicit group rule that covers them.
"Account" here refers to a Feishu bot account (a self-built app), not a human user's Feishu account. Each bot corresponds to one entry under channels.feishu.accounts.<accountId> and typically maps to one OpenClaw agent.
commandControl and userGroups can be set inside a specific bot account. When present, the bot-level config fully replaces the top-level config for that bot — there is no merging of rules.
"channels": {
"feishu": {
"userGroups": {
"admin": ["admin_user_id"]
},
"commandControl": {
"groups": [
{ "name": "admin", "members": ["@admin"], "commands": "*" },
{ "name": "default", "members": ["*"], "commands": ["/new", "/help", "/status", "/compact", "/reset"] }
]
},
"accounts": {
"sensitive-agent": {
"userGroups": {
"admin": ["admin_user_id"]
},
"commandControl": {
"groups": [
{ "name": "admin", "members": ["@admin"], "commands": "*" },
{ "name": "default", "members": ["*"], "commands": ["/status"] }
]
}
}
}
}
}In this example, sensitive-agent only allows /status for non-admins, while all other agents use the top-level rules that allow four commands.
Important: if an account defines its own commandControl but not its own userGroups, it still inherits the top-level userGroups — so @admin references continue to resolve correctly. If an account defines its own userGroups, the top-level userGroups are no longer visible to that account.
When a command is blocked, the denial message is sent as a direct message to the sender, not posted to the group. This avoids broadcasting that a user attempted a restricted command.
commandControl and userGroups take effect immediately on save — no gateway restart is needed.
For user_id-based matching to work, each Feishu self-built app (bot) must have these permissions enabled on the Feishu Open Platform under tenant_access_token (application identity, not user identity):
| Permission | Purpose |
|---|---|
contact:user.base:readonly |
Read user profile (name resolution) |
contact:user.employee_id:readonly |
Read the user's user_id field from the contact API |
After adding permissions, publish a new app version on the Feishu Open Platform for them to take effect.
Why both are needed: The Feishu webhook event only reliably includes open_id (app-scoped). The plugin calls the contact API to look up each sender's user_id (tenant-scoped). Without contact:user.employee_id:readonly, the API returns user_id as empty, and matching falls back to open_id only — which means you would need a separate open_id entry per person per app.
Also check: the app's contact scope (通讯录权限范围) must include the users you want to resolve. If it's set too narrowly (e.g., "current department only"), the API won't return data for users outside that scope.
Retained from the upstream Feishu foundation:
feishu_docfeishu_wikifeishu_drivefeishu_chatfeishu_app_scopes
Local additions or expansions in this fork:
feishu_sheetget_meta,read_range,write_range,append_rows,set_format,set_style,insert_image,insert_cell_image
- expanded
feishu_bitable_*- meta lookup from URL, field CRUD, record CRUD, batch delete
feishu_task_*- create, get, update, delete
feishu_id- resolve (ID conversion across open_id/union_id/user_id/chat_id), lookup (email/phone → IDs), whois (full profile), members (enriched with all ID types), my_chats, search_chats
feishu_id_admin- rebuild_index (scan session history for observed IDs), search_observed (zero-API local search), verify_matrix (cross-account visibility report), explain_visibility (single-target deep diagnosis)
feishu_get_message_file- downloads any image / file / audio / video attachment by
message_id+file_keyand writes the raw bytes to an agent-chosen path on disk. The plugin does not parse, extract, or interpret the file — that is the agent's job using its own file-reading tools (Read, read_file, etc.). See Reading files sent without @mention for the cross-message workflow this enables.
- downloads any image / file / audio / video attachment by
Optional sensitive tooling:
feishu_permis disabled by default because it modifies ACLs
Operational note:
- Tool execution resolves the effective Feishu identity from inbound message context first, then from agent/account bindings.
- Agents bound to multiple Feishu apps (cross-organization) must specify
accountIdorasAccountIdwhen initiating tool calls outside of a message-reply context (e.g., cron jobs, heartbeats). The plugin will not guess — ambiguous routing is an explicit error.
Usability note:
- When creating a document, pass the requester's
owner_open_idif you want the human requester to immediately receive access on the created doc instead of leaving it visible only to the bot app.
Feishu's mobile UI does not let you attach a file and @mention the bot in the same message — the file always goes out alone. The natural workflow is therefore:
- Drop a PDF (or image, doc, etc.) into the group with no
@bot. - Follow up with a second message:
@bot 帮我看看这个 PDF.
feishu-plus makes this work end-to-end through two coordinated changes, both of which are deliberately format-agnostic:
-
GroupSense captures structured attachment markers at every layer of the history lifecycle. Each attachment carried by a group message — top-level
file/image/audio/videoplus image/file keys embedded in rich-textpostmessages — is serialized as[feishu_attachment type=… message_id=… key=… name="…"]. The marker is the agent's only handle on the file; without it the agent only sees opaque{"file_key":"..."}JSON. Two layers preserve the marker:- Pending history (recent messages window). The marker is part of the body emitted by
formatGroupBody, so the agent's prompt context shows it directly. Lifetime: until the next milestone compression rolls the window forward. - Milestone summaries (compressed older history). Before the LLM summarizer runs, a deterministic regex (
\[feishu_attachment [^\]]+\]) extracts every marker from the source entries and stores them verbatim on theMilestoneRecord, alongside the LLM-generated summary, not inside it. WhenbuildMilestonePrefixrenders a milestone back into agent context, the markers come out in their own附件(原文标记)block. The LLM is free to compress, paraphrase, or drop the human-readable summary text however it likes — the attachment list is captured by code, not by an instruction the model could ignore. Filenames containing[or]are stripped at marker emission time so the regex can never terminate early. - Retention model. Attachments live on the
MilestoneRecorditself. When an old milestone is evicted viaclampArray(milestones, keep)(default 5), its attachment list evicts in lockstep. Very old files become unreachable to the agent — matching how a human would expect "remember that PDF from last month?" to NOT necessarily work, while "remember that PDF from this morning?" should.
Files exchanged purely between humans never trigger any download. The plugin does zero network calls until the agent explicitly invokes
feishu_get_message_file. - Pending history (recent messages window). The marker is part of the body emitted by
-
feishu_get_message_filedownloads the attachment on demand. When the agent decides it actually wants to read the file, it calls this tool with themessage_id,file_key, an absolutesave_topath, and (ideally) theoriginal_filenamefrom the marker. The plugin downloads the bytes via Feishu'sim.messageResource.getAPI and writes them to disk — that's it. No PDF extraction, no OCR, no text decoding, no MIME-specific branching. After the tool returns the path, the agent uses its own file-reading tool (Claude Code'sRead, codex'sread_file, OpenClaw's visionimagetool,execto ZIP-unpack a docx/pptx, etc.) to inspect the contents.
Why save-to-disk instead of inline parsing? Two reasons:
- Universal by construction. Whatever format the agent's own file tools can read (PDF, image, plain text, Jupyter notebook, …), this tool can deliver. New formats just work the moment the agent's tools learn them — no plugin update needed.
- Persistent and re-usable. Once a file is on disk, the agent can grep it, transform it, reference it later, or attach it to a downstream tool call. An inline-parsed text blob would be one-shot.
Behavior:
save_tomust be an absolute path. The tool decides directory-vs-file mode automatically: a trailing separator, an existing directory, or a basename with no.in it all mean "directory" (andoriginal_filenameis appended inside). Otherwisesave_tois used as the full file path. The chosen mode is reported in the tool result text so misclassification is debuggable. Parent directories are created automatically.- The tool routes through the same
withFeishuToolClientwrapper as every otherfeishu_*tool, so disabled accounts, missing credentials, andasAccountIdsupervisor delegation behave identically. - Hard cap: 100 MB per file. Larger downloads are rejected.
- For multi-account-bound agents, pass
asAccountIdto disambiguate which Feishu app to download through. - Caveat for posts: the
[feishu_attachment ...]marker is emitted for keys embedded in rich-text post messages too, butim.messageResource.getmay or may not accept those embedded keys depending on Feishu's API behavior — the agent will see the underlying error verbatim if a download fails.
This section explains the command permission system in full, because the layered design can be non-obvious.
Three separate concepts:
-
userGroups— named sets of Feishu IDs. No permissions here, just names for groups of people. These exist purely so you don't repeat ID lists everywhere. -
commandControl.groups— ordered permission rules. Each rule hasmembers(who this applies to) and a command policy (commands: "*",commands: [...], orexcept: [...]). Rules are checked top-to-bottom; the first matching rule wins. -
Feishu bot accounts — each Feishu self-built app (bot) corresponds to one OpenClaw agent. "Account" in this context always means a bot account, not a human user account.
userGroupsandcommandControllive at the channel level, not the agent level. There is no per-agent command permission — only per-bot-account (i.e., per-Feishu-app).
How a command is evaluated:
User sends /model to agent X
↓
Which Feishu bot account received this message?
↓
Does that bot account have its own commandControl?
├─ Yes → use the bot's commandControl + its userGroups (or inherited global userGroups if bot has none)
└─ No → use the top-level commandControl + top-level userGroups
↓
Walk the groups list top-to-bottom. Find the first group whose members match the sender.
├─ Match found → apply that group's command policy
└─ No match → apply built-in fallback: /status /new /reset /compact only
↓
Allowed → message reaches the agent
Denied → sender receives a private DM with the blockMessage; agent never sees the message
Per-bot override semantics:
Bot-level config does a full replacement, not a merge:
- If
accounts.X.commandControlis set, the top-levelcommandControlis completely ignored for bot X - If
accounts.X.commandControlis not set, bot X inherits the top-levelcommandControlentirely userGroupsfollow the same rule independently: bot-level replaces top-level if set, otherwise inherited- Consequence: if you set
accounts.X.commandControlbut notaccounts.X.userGroups, bot X's rules can still reference@groupNameentries from the top-leveluserGroups
Default behavior (no configuration):
Even with no commandControl configured at all, the default is restrictive: only /status, /new, /reset, and /compact are available to everyone. Any other command requires an explicit group rule. This is by design — deploy first, configure access second, without risk of accidental exposure.
User ID resolution:
userGroups should contain Feishu user_id values (tenant-scoped, consistent across all apps), not open_id values (app-scoped, different per app). The plugin resolves each sender's user_id by calling the Feishu contact API at message time. For this to work, every Feishu app must have contact:user.employee_id:readonly and contact:user.base:readonly permissions under tenant_access_token. If these permissions are missing, the plugin falls back to open_id matching only, which requires N entries per person for N bots.
If you are an AI agent helping a human operator deploy this plugin, use this order:
- Confirm that the host runs OpenClaw
>= 2026.3.13. - Decide whether the deployment is single-account or multi-account.
- Create the Feishu self-built app(s) and collect
appId,appSecret, and transport choice (websocketorwebhook). - If transport is
webhook, also collectverificationTokenandencryptKey. - Configure Feishu app permissions. Each self-built app needs these permissions on the Feishu Open Platform (all under tenant_access_token, not user_access_token):
contact:user.base:readonly— required for sender name resolutioncontact:user.employee_id:readonly— required foruser_idresolution (command control matching)cardkit:card:write— required only if the deployment wants streaming cards- After adding permissions, the human (or their Feishu admin) must publish a new app version for the permissions to take effect. Guide them to: Feishu Open Platform → app page → Version Management → Create Version → Publish.
- Also ensure the app's contact scope (通讯录权限范围) covers all users who will interact with the bot. By default it may be too narrow.
- Write the smallest working
channels.feishublock first. Add per-account overrides only after DM and group delivery work. - Add explicit Feishu
accountIdbindings for every agent that will use Feishu tools. - Configure command control. Collect user IDs from the Feishu admin panel (each employee's
user_idis visible under the admin console's member management). Useuser_idvalues — notopen_id— inuserGroupsso one entry per person covers all bots. If you cannot obtain a user'suser_id, have them send any message to one of the bots and read theuser_idfrom the gateway log. - If
milestoneContextis enabled, ensure the OpenClaw version is>= 2026.3.24so theagent-runtimesimple-completion API is available. Optionally setmilestoneContext.modelto control which model is used for summarization. - Verify event subscriptions before debugging message flow.
- Test at least these cases:
- DM round-trip
- group
@mention - one long markdown reply with headings, lists, and a table
- one streaming-card reply if
streaming: true - one doc, wiki, drive, bitable, or sheet operation against a resource already shared with the bot
- a restricted command (e.g.
/model) from both an admin and a non-admin user, to verify command control is working
- Derived from
m1heng/clawdbot-feishu - Includes backports from the bundled Feishu extension in
openclaw/openclaw - Distributed under the MIT license in LICENSE
- Additional provenance notes live in NOTICE