Downcity
ServicesChat Service

Feishu

Connect Downcity to Feishu/Lark private chats and group chats

Feishu integration

Use Feishu (or Lark) to connect Downcity to:

  • direct messages
  • group chats
  • route binding and context persistence
  • agent reply delivery back to Feishu
  • file attachment delivery via <file type="document">path</file>
  • inbound image, video, file, and audio download with <file ...> injection into Agent execution
  • best-effort model file parts injection for inbound images and PDFs

Want the internal execution flow? See: /en/docs/services/chat/feishu-message-flow

Short version

The current Feishu channel in downcity uses:

  • a self-built Feishu app
  • bot + WebSocket long connection
  • server-side tenant_access_token

This is the most important thing to keep in mind:

  • if your manual API test works with a user_access_token, that does not mean the running bot can resolve the same user name
  • the running bot depends on tenant scopes, effective authorization, and contact visibility

Setup

1) Create the app

Create a self-built app in Feishu Open Platform and enable:

  1. bot capability
  2. message receiving
  3. WebSocket / long connection mode

The current runtime works best with long connection mode and does not require a public webhook URL first.

2) Subscribe to events

At minimum, enable:

  • im.message.receive_v1

Without this event the bot will start, but it will not receive chat messages.

3) Create a channel account

In Console UI, open Global / Channel Accounts and create a Feishu account such as feishu-main:

  1. channel = feishu
  2. fill appId
  3. fill appSecret
  4. for Lark, optionally set:
    • domain = https://open.larksuite.com

4) Bind it in downcity.json

{
  "services": {
    "chat": {
      "channels": {
        "feishu": {
          "enabled": true,
          "channelAccountId": "feishu-main"
        }
      }
    }
  }
}

5) Required permissions

Meaning:

  • role: a permission group such as default, member, or admin
  • permission: permissions owned by that group, such as chat.dm.use, chat.group.use, or agent.manage
  • user -> role: which role a Feishu user belongs to

The same config is now stored in the chat_auth_* structured tables in console ~/.downcity/downcity.db and managed through city chat or city chat auth set feishu:<userId>, instead of being written to project downcity.json in plaintext. New users fall back to default roles, so no pairing flow is required; group/channel usage depends only on the speaking user's role.

For the current runtime, tenant app scopes matter most.

Recommended minimum tenant scopes:

  • im:message:readonly
  • im:message:send_as_bot
  • im:chat:read
  • im:chat:readonly
  • im:chat.members:read
  • contact:user.basic_profile:readonly
  • contact:user.employee_id:readonly

In practice you will often also keep:

  • im:chat
  • im:chat.members:bot_access
  • contact:user.base:readonly
  • contact:user.id:readonly

6) Publish and re-authorize

Checking a scope in the console is not enough.

Make sure:

  1. the app version is published
  2. tenant admin authorization is refreshed
  3. the scopes are really enabled for the tenant side, not only user-side OAuth
  4. contact visibility includes the user you are testing with

Start runtime

city agent start

Expected logs:

  • Feishu channel enabled
  • 🤖 Starting Feishu Bot...
  • Feishu Bot started, using long connection mode

How to verify

In Console UI

Check:

  • Feishu channel is bound to the expected channel account
  • status shows link connected

Send a new Feishu message

Route metadata is refreshed on new incoming messages.

If you changed permissions but did not send a new message, old metadata will stay old.

Inspect route metadata

If name resolution works, the Feishu route in .downcity/channel/meta.json should contain:

  • actorName
  • chatTitle

If it only contains:

  • actorId
  • chatId

then the backend still did not resolve a display name.

Important behavior

Current Feishu channel behavior:

  • inbound transport is WebSocket
  • execution accepts text + rich post + image + video + file + audio inputs
  • text / links / mentions inside post are normalized into plain text, while post images / media are downloaded into .downcity/.cache/feishu/ and injected as <file ...>
  • inbound image, video, file, and audio messages are downloaded into .downcity/.cache/feishu/ and injected as <file ...>
  • inbound images and PDFs are also best-effort injected as model file parts for multimodal models
  • when a user replies to a Feishu message, runtime will best-effort fetch the parent message body from parent_id and inject it into the Agent input
  • outbound replies support <file ...> conversion and are delivered segment-by-segment in the actual text/attachment order from the source message
  • normal AI text replies are sent as plain messages by default and do not auto-reply to the latest message
  • a Feishu reply link is used only when reply mode is explicitly requested for a target message_id
  • sender name and chat title are resolved on a best-effort basis
  • if resolution fails, processing still continues with raw ids

So the channel may still work while the UI only shows chat_id / open_id. If a reply is structured as text -> attachment -> text, outbound delivery keeps that same order instead of forcing “text first, attachments later”.

tenant_access_token vs user_access_token

This is the most common source of confusion.

The running channel uses tenant_access_token

The runtime uses:

  • appId + appSecret
  • tenant_access_token

and then calls user/chat metadata APIs with that token.

So what matters is:

  • tenant scopes
  • contact visibility

Why manual user_access_token tests may still succeed

A user_access_token represents a specific authorized user.

That can return richer user-visible data, but the running bot does not currently use a user token for inbound chat metadata enrichment.

So this is completely possible:

  1. your manual user_access_token test returns a real name
  2. the running Feishu bot still shows unknown

Common issues

Messages arrive, but title / username is missing

Check:

  1. tenant scopes are really effective
  2. app version was published
  3. admin re-authorized the app
  4. contact visibility includes the user
  5. you sent a new Feishu message after changing permissions

I enabled user scopes, why does the bot still fail?

Because the runtime uses a tenant token, not your manual user OAuth token.

Group title or private chat name is still missing

Usually this means one of these is still not effective:

  • im:chat.members:read
  • contact:user.employee_id:readonly
  • contact field visibility for the current app