Feishu Channel Deployment Guide

This document explains how to deploy the xiaoO Feishu channel in a way that is actually reproducible from scratch.

It covers:

  • how the connection is established from Feishu to xiaoO in both webhook and persistent-connection modes
  • which config files are used by the daemon
  • how to expose the webhook safely through nginx when you choose HTTP callbacks
  • how to configure Feishu persistent connection (websocket) when you do not want a public callback
  • how to verify URL challenge, message delivery, and reply behavior

The examples below match the currently deployed xiaoo-rebuild service layout on the server, but the same structure can be adapted to any host.

1. End-to-end Request Flow

xiaoO now supports two Feishu inbound connection modes:

  1. Webhook push
  2. Persistent connection (websocket / long connection)

1.1 Webhook flow

Feishu user sends a message
  -> Feishu platform sends HTTP POST to the public callback URL
  -> nginx receives the request on port 80/443
  -> nginx forwards the request to xiaoO on 127.0.0.1:18080
  -> xiaoO handles /api/v1/channels/feishu/events
  -> xiaoO calls Feishu OpenAPI to add reaction / update cards / send reply

In the current webhook deployment, the public callback path and the internal application path are not the same:

  • Public callback URL configured in Feishu:
    • http://<your-domain-or-ip>/api/v1/channels/xiaoo/events
  • Internal route handled by the xiaoO app:
    • POST /api/v1/channels/feishu/events

nginx bridges the two.

This distinction is important. If you configure Feishu with the internal route directly, but your reverse proxy only exposes the public route, URL verification will fail.

In other words, in the standard webhook deployment:

  • Feishu should call:
    • http://<your-domain-or-ip>/api/v1/channels/xiaoo/events
  • Feishu should not call:
    • http://<your-domain-or-ip>/api/v1/channels/feishu/events

unless you intentionally exposed /api/v1/channels/feishu/events as the public route.

1.2 Persistent connection flow

xiaoO daemon starts
  -> xiaoO calls Feishu callback/ws/endpoint
  -> Feishu returns a websocket endpoint
  -> xiaoO establishes an outbound long connection to Feishu
  -> Feishu pushes event frames through that connection
  -> xiaoO processes messages and calls Feishu OpenAPI to add reaction / update cards / send reply

In persistent-connection mode:

  • Feishu does not call your public callback URL for message delivery
  • xiaoO must maintain outbound connectivity to https://open.feishu.cn
  • nginx is not required for message ingress
  • URL challenge is not part of the message ingress path

2. Prerequisites

  • A Feishu tenant with admin access
  • A custom Feishu app with the Bot capability enabled
  • A public domain or public server IP reachable by Feishu
  • A Linux host where you can install and run xiaoo-app daemon
  • Rust toolchain and Cargo available on that host
  • systemd available to manage the daemon process
  • nginx or another reverse proxy available for public ingress
  • Outbound network access from xiaoO to:
    • https://open.feishu.cn
    • your model provider (for example OpenRouter)

3. Deployment Modes

There are three practical deployment modes for Feishu integration:

Mode A: Local deployment with public callback relay

This mode is useful during development.

In this setup:

  • xiaoO runs on your local machine
  • Feishu still needs a publicly reachable callback URL
  • you expose the local service through one of:
    • a reverse proxy on a public server
    • a tunnel service
    • a public test machine that forwards traffic back to your local instance

Typical flow:

Feishu
  -> public callback URL
  -> relay / reverse proxy / tunnel
  -> local xiaoO daemon

Important:

  • Feishu cannot call 127.0.0.1, localhost, or a private LAN address directly
  • even for local testing, you still need a public callback address

Optional implementation patterns for local deployment

If you are deploying locally for the first time, the hardest part is usually not xiaoO itself but choosing how Feishu reaches your local daemon.

The three most practical patterns are:

Pattern A1: Public server reverse proxy in front of your local machine

This is the most stable development setup if you already have a public Linux server.

Flow:

Feishu
  -> http://<public-domain-or-ip>/api/v1/channels/xiaoo/events
  -> nginx on a public server
  -> relay/tunnel from that server
  -> local xiaoO daemon on 127.0.0.1:18080

Typical responsibilities:

  • your local machine runs xiaoo-app daemon
  • a public server exposes the callback route
  • that public server forwards traffic back to your local machine through a secure relay or tunnel

This is usually the easiest pattern if:

  • you want a stable public callback URL
  • you do not want to expose your local machine directly
  • you already have SSH or relay access to a public server
Pattern A2: Direct public tunnel to the local daemon

This is the fastest way to test Feishu locally.

Flow:

Feishu
  -> public tunnel URL
  -> local 127.0.0.1:18080

In this pattern:

  • xiaoO still runs locally
  • a tunnel tool publishes your local daemon as a temporary public URL
  • Feishu points directly to that tunnel URL

This is usually the easiest pattern if:

  • you want to validate the Feishu flow quickly
  • you do not need a long-lived production callback address

Be careful with path mapping:

  • if the tunnel forwards the whole local HTTP server directly, Feishu can call:
    • http://<public-tunnel-domain>/api/v1/channels/feishu/events
  • if you want to keep the public route as:
    • http://<public-domain-or-ip>/api/v1/channels/xiaoo/events then you still need a reverse proxy layer in front of the local daemon
Pattern A3: Local daemon behind a team relay gateway

This is common in organizations that already have an internal development ingress layer.

Flow:

Feishu
  -> team-owned public gateway
  -> team relay rule for /api/v1/channels/xiaoo/events
  -> your local xiaoO daemon

This is usually the cleanest pattern if:

  • your team already has a shared public ingress
  • local developers are expected to register routes rather than expose machines directly

The important thing is not which relay product you choose, but that all four of these remain true:

  1. Feishu sends to a public URL.
  2. That public URL reaches your local daemon.
  3. The path mapping is correct.
  4. The local daemon can still call Feishu OpenAPI and the model provider outbound.

How to choose between the three local patterns

Use this quick rule:

  • choose Pattern A1 if you want a stable callback URL and already have a public server
  • choose Pattern A2 if you want the fastest one-person local test loop
  • choose Pattern A3 if your team already operates a shared ingress or relay platform

No matter which pattern you use, Feishu should still be configured against the public callback address, never your local loopback address.

Mode B: Server deployment

This is the recommended production mode.

In this setup:

  • xiaoO runs on a server
  • nginx or another reverse proxy exposes a public callback URL
  • Feishu calls the public route directly
  • nginx forwards the request to the local daemon port

Typical flow:

Feishu
  -> public server URL
  -> nginx
  -> xiaoO daemon on localhost

If you are deploying for long-term use, this is usually the cleaner option.

Mode C: Server or local deployment with persistent connection

This mode is useful when you want to avoid public HTTP callback setup entirely.

In this setup:

  • xiaoO runs locally or on a server
  • xiaoO opens an outbound websocket to Feishu
  • Feishu pushes events through that long connection
  • xiaoO still calls Feishu OpenAPI outbound for replies, cards, reactions, and member queries

Typical flow:

xiaoO daemon
  -> outbound HTTPS request to fetch websocket endpoint
  -> outbound websocket connection to Feishu
  -> Feishu pushes events
  -> xiaoO processes and replies

Choose this mode when:

  • you do not want to manage nginx just for Feishu ingress
  • you do not have a stable public callback URL
  • you are running locally or inside a private network but still have outbound internet access

Be aware:

  • the xiaoO daemon must stay running continuously
  • outbound connectivity failures will break message delivery
  • Feishu callback-page URL verification is not the setup path for this mode

3.1 Can someone run Feishu locally with websocket mode only?

Yes.

If someone only has the source code and Feishu app credentials, they can run xiaoO locally with persistent connection mode and receive real Feishu messages without exposing a public callback URL.

This works because:

  • xiaoO opens an outbound connection to Feishu
  • Feishu pushes events over that long connection
  • replies, reactions, and cards still go out through Feishu OpenAPI

For local websocket deployment, the minimum requirements are:

  • the local machine can reach https://open.feishu.cn outbound
  • the local machine can reach the configured model provider outbound
  • the Feishu app is configured for persistent connection
  • the xiaoO daemon stays running

What is not required for local websocket mode:

  • no public callback URL
  • no nginx
  • no URL challenge verification
  • no reverse proxy path mapping

This makes websocket mode the easiest way for a new user to test Feishu locally from source.

4. Prepare Code and Build the Binary

If you only have the source code and no existing deployment, start from the repository first.

Example:

git clone <your-repo-url> /opt/xiaoo/src
cd /opt/xiaoo/src
git checkout <your-branch>
cargo build -p xiaoo-app

After a successful build, the binary will usually be created at:

target/debug/xiaoo-app

For a long-running service, copy or install that binary to a stable runtime path such as:

/opt/xiaoo/bin/xiaoo-app

Example:

mkdir -p /opt/xiaoo/bin
install -m 755 target/debug/xiaoo-app /opt/xiaoo/bin/xiaoo-app

If you prefer release builds, replace the build command with:

cargo build -p xiaoo-app --release

and install:

install -m 755 target/release/xiaoo-app /opt/xiaoo/bin/xiaoo-app

5. Prepare Runtime Directories

Before writing config or creating the service, create the runtime layout explicitly.

Example:

mkdir -p /opt/xiaoo/bin
mkdir -p /opt/xiaoo/config
mkdir -p /opt/xiaoo/app
mkdir -p /opt/xiaoo/adt/skills
mkdir -p /var/lib/xiaoo/agents/main

Recommended layout:

/opt/xiaoo/bin/xiaoo-app
/opt/xiaoo/config/config.toml
/opt/xiaoo/config/xiaoo.env
/opt/xiaoo/app
/opt/xiaoo/adt/skills
/var/lib/xiaoo/agents/main

You can adjust these paths, but the same values must be used consistently across:

  • config.toml
  • systemd
  • nginx
  • your deployment scripts

6. Create the Feishu App

  1. Go to Feishu Open Platform
  2. Create a Custom App
  3. Add the Bot capability
  4. In the app sidebar, confirm the bot is enabled for the scenarios you need:
    • direct message
    • group chat mention
  5. Save the basic app configuration before proceeding to callback setup

Record the following values from the app settings:

Field Used For
App ID channels.feishu.app_id in config.toml
App Secret environment variable referenced by channels.feishu.app_secret_env
Verification Token channels.feishu.verification_token in config.toml

Recommended mapping:

  • App ID -> channels.feishu.app_id
  • App Secret -> FEISHU_APP_SECRET in your environment file
  • Verification Token -> channels.feishu.verification_token

7. Required Permissions

At minimum, enable the permissions needed for message receive/reply:

Permission Purpose
im:message:send_as_bot send bot replies
im:message.p2p_msg:readonly receive direct messages
im:message.group_msg:readonly receive group messages
contact:user.base:readonly resolve sender identity

If you want group member directory prompts or richer group behavior, keep the contact/user-related read permissions enabled.

After changing permissions, Feishu may require you to create and publish a new app version before the changes take effect for real message traffic.

Example Feishu permission management page:

Feishu permission management example

8. Subscription Method and Event Subscription

In the Feishu Open Platform callback page, pay attention to the subscription method.

xiaoO supports two valid choices:

  • Subscription Method: Webhook
  • Subscription Method: Receive events/callbacks through persistent connection

Choose the one that matches your runtime deployment mode.

8.1 If you use Webhook mode

Use this model:

Feishu platform
  -> HTTP POST webhook
  -> public callback address
  -> xiaoO

In Events & Callbacks:

  1. Choose Webhook
  2. Confirm you are editing the correct app environment and not a stale draft app
  3. Set the request URL to the public xiaoO endpoint:
http://<your-domain-or-ip>/api/v1/channels/xiaoo/events

If your server is listening on the default HTTP port, writing :80 is usually unnecessary.

For example, these two URLs are equivalent in most deployments:

http://<your-domain-or-ip>/api/v1/channels/xiaoo/events
http://<your-domain-or-ip>:80/api/v1/channels/xiaoo/events

What matters is not whether :80 is written explicitly, but whether that public URL is actually reachable from the public internet.

This address can be either:

  • a public server address in production
  • or a public relay/tunnel address that ultimately forwards to your local xiaoO instance in development

Example callback configuration page with the public request URL redacted:

Feishu callback configuration example

  1. Add the message event:
im.message.receive_v1
  1. Save and verify the URL

Example event subscription list showing the message receive event:

Feishu event subscription example

Feishu will perform URL verification by sending a challenge request. xiaoO must respond with:

{"challenge":"<the same challenge string>"}

If this step fails, the webhook is not connected yet.

After verification succeeds:

  1. Make sure the event switch is actually enabled
  2. Publish a new app version if Feishu shows the callback or permission changes as draft-only

Without publishing, URL verification may pass, but real user messages may still never be delivered.

8.1.1 Common callback URL mistakes

The following mistakes are extremely common:

  1. Filling the internal route instead of the public route

Wrong in the standard deployment:

http://<your-domain-or-ip>/api/v1/channels/feishu/events

Right in the standard deployment:

http://<your-domain-or-ip>/api/v1/channels/xiaoo/events
  1. Assuming nginx alone is enough

nginx can only proxy traffic if:

  • the xiaoO daemon is actually running
  • nginx has the correct location
  • nginx has been reloaded after config changes
  • port 80 or 443 is reachable from the public internet
  • the cloud security group / firewall allows inbound traffic
  1. Testing only from the server itself

If curl http://127.0.0.1:18080/api/v1/health works but curl http://<your-domain-or-ip>/api/v1/health from another machine times out, the problem is still in public ingress, not in Feishu.

8.2 If you use persistent-connection mode

Use this model:

xiaoO daemon
  -> outbound websocket connection
  -> Feishu pushes events

In Events & Callbacks:

  1. Choose Receive events/callbacks through persistent connection
  2. Confirm you are editing the correct app environment and not a stale draft app
  3. Add the message event:
im.message.receive_v1
  1. Save the event subscription
  2. Publish a new app version if Feishu shows the changes as draft-only

Important:

  • In persistent-connection mode you do not need a public callback URL for message delivery
  • URL challenge is not the main validation path
  • the actual validation is whether the daemon can fetch the websocket endpoint and stay connected

9. Why Reverse Proxy Is Needed for Webhook Mode

In webhook mode, xiaoO does not bind directly on the public interface.

Instead:

  • xiaoO listens on:
    • 127.0.0.1:18080
  • nginx listens publicly on:
    • 0.0.0.0:80 or 0.0.0.0:443

This is recommended because:

  • Feishu can reach your public address
  • xiaoO itself stays on localhost
  • you can host multiple bot instances behind different public paths
  • you can add TLS at the nginx layer

If you use persistent-connection mode, this section does not apply to Feishu message ingress.

9.1 nginx alone is not enough

This is the part that new deployments most often miss.

Even if nginx is installed and the config file looks correct, webhook mode still will not work unless all of the following are true:

  1. xiaoO daemon is running and healthy
  2. xiaoO is listening on 127.0.0.1:18080
  3. nginx has the correct location = /api/v1/channels/xiaoo/events
  4. nginx has been reloaded after editing the config
  5. the server's public 80 or 443 port is reachable from the internet
  6. cloud security groups / firewalls allow inbound traffic

If Feishu says the callback request timed out, the most common root causes are:

  • nginx never reloaded the new route
  • the public port is blocked
  • the daemon is down
  • the public route is wrong

10. Example nginx Routing

This is the key part many deployments miss.

In the current server layout, the public Feishu callback path for xiaoO is:

location = /api/v1/channels/xiaoo/events {
    proxy_pass http://127.0.0.1:18080/api/v1/channels/feishu/events;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

This means:

  • Feishu sends to /api/v1/channels/xiaoo/events
  • nginx forwards to xiaoO’s internal route /api/v1/channels/feishu/events

If you are hosting multiple bots on the same machine, give each one a dedicated public route.

For example:

  • xiaoO:
    • /api/v1/channels/xiaoo/events
  • another bot:
    • /api/v1/channels/eulerclaw/events

That avoids callback switching conflicts.

After editing nginx config, always run:

nginx -t
systemctl reload nginx

If nginx is not reloaded, Feishu may still be hitting an old route even though your config file looks correct on disk.

If you are creating nginx config from scratch rather than editing an existing server block, the minimum shape usually looks like:

server {
    listen 80;
    server_name <your-domain-or-ip>;

    location = /api/v1/channels/xiaoo/events {
        proxy_pass http://127.0.0.1:18080/api/v1/channels/feishu/events;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location = /api/v1/health {
        proxy_pass http://127.0.0.1:18080/api/v1/health;
    }
}

The exact server wrapper can vary across environments, but the important part is that the Feishu public callback route reaches 127.0.0.1:18080/api/v1/channels/feishu/events.

11. xiaoO Daemon Configuration

In the current deployment, the daemon is started by systemd and reads:

  • config file:
    • /opt/xiaoo/config/config.toml
  • environment file:
    • /opt/xiaoo/config/xiaoo.env

Example config.toml

Webhook mode

[llm]
provider = "openrouter"
api_base = "https://openrouter.ai/api/v1"
model = "z-ai/glm-5"
api_key_env = "OPENROUTER_API_KEY"
max_tokens = 8192

[channels.feishu]
enabled = true
transport = "webhook"
app_id = "cli_xxxxxxxxxxxxx"
app_secret_env = "FEISHU_APP_SECRET"
verification_token = "xxxxxxxxxxxxxxxx"
base_url = "https://open.feishu.cn"

[agents]
default_agent_id = "main"

[[agents.list]]
id = "main"
default = true
workspace = "/opt/xiaoo/app"
agent_dir = "/var/lib/xiaoo/agents/main"

[skills]
dirs = ["/opt/xiaoo/adt/skills"]

Persistent-connection mode

[llm]
provider = "openrouter"
api_base = "https://openrouter.ai/api/v1"
model = "z-ai/glm-5"
api_key_env = "OPENROUTER_API_KEY"
max_tokens = 8192

[channels.feishu]
enabled = true
transport = "websocket"
app_id = "cli_xxxxxxxxxxxxx"
app_secret_env = "FEISHU_APP_SECRET"
base_url = "https://open.feishu.cn"

[agents]
default_agent_id = "main"

[[agents.list]]
id = "main"
default = true
workspace = "/opt/xiaoo/app"
agent_dir = "/var/lib/xiaoo/agents/main"

[skills]
dirs = ["/opt/xiaoo/adt/skills"]

11.1 Minimal local websocket configuration

If someone wants to run xiaoO locally from source with Feishu websocket mode, the smallest practical config is:

[llm]
provider = "openrouter"
api_base = "https://openrouter.ai/api/v1"
model = "z-ai/glm-5"
api_key_env = "OPENROUTER_API_KEY"
max_tokens = 8192

[channels.feishu]
enabled = true
transport = "websocket"
app_id = "cli_xxxxxxxxxxxxx"
app_secret_env = "FEISHU_APP_SECRET"
base_url = "https://open.feishu.cn"

[agents]
default_agent_id = "main"

[[agents.list]]
id = "main"
default = true
workspace = "/path/to/your/local/workspace"

A matching local environment file can look like:

FEISHU_APP_SECRET=your-real-feishu-app-secret
OPENROUTER_API_KEY=your-real-model-key

Then start xiaoO locally:

cd /path/to/xiaoO
cargo run -p xiaoo-app -- daemon --config /path/to/config.toml --host 127.0.0.1 --port 18080

Notes:

  • --host 127.0.0.1 is fine for local websocket mode
  • Feishu message ingress does not depend on this port being public
  • the local port is still useful for:
    • /api/v1/health
    • local debugging
    • local test endpoints

If the daemon is healthy and the persistent connection is established, real Feishu messages should be delivered even though the daemon is only bound to localhost.

Example xiaoo.env

FEISHU_APP_SECRET=your-real-feishu-app-secret
OPENROUTER_API_KEY=your-real-model-key

Do not put the Feishu App Secret directly into config.toml. Keep it in the environment file and reference it through app_secret_env.

12. systemd Service

If you are creating the service from scratch, use a full unit file instead of only the ExecStart line.

[Unit]
Description=xiaoO rebuild daemon
After=network.target

[Service]
Type=simple
WorkingDirectory=/opt/xiaoo
EnvironmentFile=/opt/xiaoo/config/xiaoo.env
ExecStart=/opt/xiaoo/bin/xiaoo-app daemon --config /opt/xiaoo/config/config.toml --host 127.0.0.1 --port 18080
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

A few important details:

  • --host 127.0.0.1 means xiaoO is intentionally internal-only
  • nginx is responsible for public exposure in webhook mode
  • EnvironmentFile is where FEISHU_APP_SECRET and OPENROUTER_API_KEY are loaded from

After creating or editing the unit file:

systemctl daemon-reload
systemctl enable --now xiaoo-rebuild.service

After changing either the config file or env file, restart the service:

systemctl restart xiaoo-rebuild.service

If you changed the environment file, a restart is required. A plain nginx reload is not enough.

12.1 Local development without systemd

For local websocket-only development, systemd is optional.

You can run the daemon directly:

cd /path/to/xiaoO
export FEISHU_APP_SECRET=your-real-feishu-app-secret
export OPENROUTER_API_KEY=your-real-model-key
cargo run -p xiaoo-app -- daemon --config /path/to/config.toml --host 127.0.0.1 --port 18080

This is often the fastest way to validate that:

  • the Feishu app is correctly configured for persistent connection
  • the local machine can reach Feishu and the model provider
  • websocket message delivery works end to end

If you later move from local development to a server deployment, the same config can be reused inside systemd.

13. Connection Establishment Checklist

13.1 Webhook mode

If someone else wants to reproduce webhook deployment, these are the exact layers that must line up:

  1. Feishu app has webhook mode enabled
  2. Feishu callback URL points to the public endpoint:
    • /api/v1/channels/xiaoo/events
  3. Public DNS or IP resolves to your server
  4. Port 80 or 443 is reachable from the public internet
  5. nginx has a matching location = /api/v1/channels/xiaoo/events
  6. nginx proxies to:
    • http://127.0.0.1:18080/api/v1/channels/feishu/events
  7. xiaoO daemon is actually listening on:
    • 127.0.0.1:18080
  8. channels.feishu.enabled = true
  9. channels.feishu.transport = "webhook"
  10. app_id, verification_token, and app_secret_env are all configured
  11. the environment variable named by app_secret_env is actually present in the service environment
  12. the daemon can reach Feishu OpenAPI and the model provider over outbound network
  13. the Feishu app version has been published after adding callback/event changes

13.1.1 Minimum webhook smoke checklist for a borrowed server

If you are using a borrowed server for the first time, validate webhook mode in this exact order:

  1. Check local daemon health on the server:
curl http://127.0.0.1:18080/api/v1/health
  1. Check public health from another machine:
curl http://<your-domain-or-ip>/api/v1/health
  1. If step 2 times out, stop here and fix public ingress before touching Feishu:

    • nginx config
    • nginx reload
    • cloud security group
    • firewall
  2. Only after public health works, verify the Feishu challenge route:

curl -X POST http://<your-domain-or-ip>/api/v1/channels/xiaoo/events \
  -H 'Content-Type: application/json' \
  --data '{"type":"url_verification","token":"YOUR_VERIFICATION_TOKEN","challenge":"probe"}'
  1. Only after the challenge route works, configure the same public URL in the Feishu backend

If you skip this order and start from the Feishu console first, a plain timeout error usually gives too little information to know whether the problem is:

  • xiaoO daemon
  • nginx route
  • public port exposure
  • firewall
  • or the Feishu callback URL itself

13.2 Persistent-connection mode

If someone else wants to reproduce persistent-connection deployment, these are the exact layers that must line up:

  1. Feishu app has persistent-connection mode enabled
  2. channels.feishu.enabled = true
  3. channels.feishu.transport = "websocket"
  4. app_id and app_secret_env are configured
  5. the environment variable named by app_secret_env is actually present in the service environment
  6. xiaoO daemon can reach:
    • https://open.feishu.cn
    • your model provider
  7. the daemon stays running long enough to maintain the websocket connection
  8. the Feishu app version has been published after adding event changes

If any one of these is missing, message delivery will fail.

13.3 Local persistent-connection mode

For local-from-source websocket testing, these are the layers that must line up:

  1. Feishu app has persistent-connection mode enabled
  2. channels.feishu.transport = "websocket"
  3. app_id and app_secret_env are configured
  4. the local shell or process environment contains:
    • FEISHU_APP_SECRET
    • the model provider key referenced by api_key_env
  5. the local machine can reach:
    • https://open.feishu.cn
    • the configured model provider
  6. the daemon process remains running
  7. the Feishu app version has been published after enabling the event subscription

Unlike webhook mode, local websocket mode does not require:

  • a public callback URL
  • nginx
  • URL challenge verification

14. Manual Verification Commands

14.1 Check daemon health

curl http://127.0.0.1:18080/api/v1/health

Expected:

{"status":"ok","version":"0.1.0"}

14.2 Check public health

curl http://<your-domain-or-ip>/api/v1/health

If this request times out from another machine, Feishu webhook configuration will also time out.

Do not continue to the Feishu console until this request succeeds.

14.3 Check Feishu challenge response (Webhook mode only)

Replace the token with your real verification_token:

curl -X POST http://<your-domain-or-ip>/api/v1/channels/xiaoo/events \
  -H 'Content-Type: application/json' \
  --data '{"type":"url_verification","token":"YOUR_VERIFICATION_TOKEN","challenge":"probe"}'

Expected:

{"challenge":"probe"}

14.4 Check persistent-connection logs (Websocket mode)

journalctl -u xiaoo-rebuild.service -f | grep -i "feishu websocket"

What you want to see:

  • the daemon successfully fetches the websocket endpoint
  • the daemon establishes the websocket connection
  • message processing logs appear after a real Feishu message arrives

14.4.1 Check local websocket development directly

If you are running xiaoO locally from source, use the same idea but watch the local terminal output:

cd /path/to/xiaoO
export FEISHU_APP_SECRET=your-real-feishu-app-secret
export OPENROUTER_API_KEY=your-real-model-key
cargo run -p xiaoo-app -- daemon --config /path/to/config.toml --host 127.0.0.1 --port 18080

What you want to see:

  • the daemon starts successfully
  • no repeated websocket reconnect failures
  • event-processing logs appear after you send a real Feishu message

14.5 Check service logs

journalctl -u xiaoo-rebuild.service -f

14.6 Check nginx callback access (Webhook mode only)

grep "api/v1/channels/xiaoo/events" /var/log/nginx/access.log | tail -n 20

This is useful when Feishu claims the callback failed but the application logs show nothing.

14.7 Check nginx syntax before reload

nginx -t

14.8 Confirm the daemon process is actually running with the expected config

systemctl status xiaoo-rebuild.service

Look for:

  • EnvironmentFile=/opt/xiaoo/config/xiaoo.env
  • --config /opt/xiaoo/config/config.toml
  • --host 127.0.0.1 --port 18080

If you are deploying from source for the first time, one extra check is worth doing:

14.9 Confirm the binary you installed is the one you just built

ls -l /opt/xiaoo/bin/xiaoo-app

and compare the timestamp with your latest build result in target/debug/ or target/release/.

15. Message Handling Notes

xiaoO currently handles Feishu messages asynchronously in both ingress modes:

  • in webhook mode:
    • Feishu sends the callback request
    • xiaoO accepts the request quickly
    • xiaoO continues processing in the background
  • in persistent-connection mode:
    • Feishu pushes an event frame over the websocket
    • xiaoO acknowledges the frame quickly
    • xiaoO continues processing in the background

In both modes, reply text / reaction / progress card updates are sent through Feishu OpenAPI.

So the expected behavior is:

  1. ingress is acknowledged quickly
  2. xiaoO processes the task
  3. user later sees:
    • acknowledgement reaction
    • progress card updates
    • final text reply

In group chats, users normally need to @ the bot to make the intent explicit. In direct messages, plain text is usually enough.

16. Common Failure Modes

Symptom Likely Cause What to Check
Feishu says challenge failed public URL does not route correctly nginx path, daemon health, verification token
Feishu callback configuration times out public ingress is not reachable at all public /api/v1/health, nginx reload, security group, firewall
No request reaches xiaoO logs callback never entered app Feishu URL, public firewall, nginx access log
invalid verification token token mismatch channels.feishu.verification_token
local websocket mode starts but receives no messages app still uses webhook subscription mode Feishu subscription method, app publish status
persistent connection never receives messages wrong subscription mode or websocket not established Feishu subscription mode, daemon logs, outbound network
persistent connection repeatedly reconnects websocket endpoint cannot stay healthy outbound connectivity, service stability, Feishu app credentials
tenant token fetch fails wrong App Secret FEISHU_APP_SECRET in env file
xiaoO receives challenge but not message events event not subscribed or app not published im.message.receive_v1, app release status
service works locally but Feishu cannot reach it daemon bound only to localhost without proxy nginx/public route missing
bot replies fail after message is received OpenAPI call failure app permissions, App Secret, outbound network
Feishu says callback configured successfully but real messages still do not arrive draft config not published publish a new app version in Feishu Open Platform
challenge succeeds but browser access works while Feishu still fails firewall/security group issue check public ingress on port 80/443

16.1 Common Feishu Console Feedback and How to Fix It

When configuring Feishu for the first time, the console error message is often too short to directly tell you which layer is broken.

Use the table below as a practical decoding guide.

Feishu console feedback What it usually means Most likely layers to check first Recommended fix order
请求超时 / timeout while verifying callback URL Feishu could not reach your public callback endpoint at all within the allowed time public ingress, nginx route, daemon health, firewall, cloud security group 1. curl http://127.0.0.1:18080/api/v1/health on the server 2. curl http://<your-domain-or-ip>/api/v1/health from another machine 3. nginx -t and reload 4. verify inbound 80/443 is open 5. retry in Feishu console
challenge 校验失败 / challenge verification failed Feishu reached your service, but xiaoO did not return the expected challenge payload callback route, verification token, wrong public/internal path mapping 1. confirm Feishu uses /api/v1/channels/xiaoo/events 2. run manual challenge curl 3. verify channels.feishu.verification_token matches the app config
verification token invalid The token in Feishu console and the token in xiaoO config do not match config.toml, deployment config consistency 1. compare Feishu Verification Token with channels.feishu.verification_token 2. restart service after config change 3. retest challenge
callback URL configured successfully but real messages do not arrive URL challenge worked, but real event delivery is still not active Feishu event subscription, app publish status, wrong subscription mode 1. confirm im.message.receive_v1 is enabled 2. publish a new app version 3. verify webhook vs websocket mode matches daemon config
persistent connection mode enabled but the bot receives no messages The daemon did not establish or maintain the Feishu long connection, or Feishu app is still on webhook mode daemon logs, outbound network, Feishu subscription mode, app publish status 1. check daemon logs for websocket connection 2. verify outbound access to open.feishu.cn 3. confirm Feishu app is set to persistent connection 4. publish the app changes
bot receives messages but cannot reply Message ingress worked, but reply path failed when calling Feishu OpenAPI App Secret, bot permissions, outbound network 1. verify FEISHU_APP_SECRET 2. verify bot send/reply permissions 3. confirm outbound access to Feishu OpenAPI

16.1.1 The single most useful rule for webhook debugging

If this request still fails from outside the server:

curl http://<your-domain-or-ip>/api/v1/health

then Feishu webhook configuration is not ready yet.

Do not continue debugging the Feishu console until public health is reachable.

16.1.2 The single most useful rule for websocket debugging

If Feishu is configured for persistent connection, but xiaoO logs never show websocket session activity, the first three places to check are:

  1. Feishu app subscription mode is really set to persistent connection
  2. the app version has been published
  3. the daemon can reach https://open.feishu.cn outbound

For a clean production deployment, use this structure:

/opt/xiaoo/bin/xiaoo-app
/opt/xiaoo/config/config.toml
/opt/xiaoo/config/xiaoo.env
/opt/xiaoo/app
/etc/systemd/system/xiaoo-rebuild.service
/etc/nginx/conf.d/xiaoo.conf

And keep the responsibility split like this:

  • Feishu platform:
    • message source
  • nginx:
    • public ingress and path routing
  • xiaoO daemon:
    • webhook handling and runtime execution
  • Feishu OpenAPI:
    • outbound reactions, cards, and replies

18. Features Currently Available in Feishu

Feature Status Notes
Text messages Supported group and direct message
Reply by bot Supported sent via Feishu OpenAPI
Reactions Supported used for quick acknowledgement
Progress cards Supported runtime-driven card updates
Group member directory Supported used to enrich group context
Skill execution Supported requires valid runtime skill registry
File/message follow-up interaction Supported depends on normal session continuity

19. Final Deployment Checklist

Before handing the deployment to someone else, make sure they can answer yes to all of these:

  • Do you have the correct App ID, App Secret, and Verification Token?
  • Is the Feishu app published after adding the event subscription?
  • Is the callback URL the public route, not the internal route?
  • Can nginx forward that route to the xiaoO daemon?
  • Did you run nginx -t and reload nginx after updating the route?
  • Can the daemon read FEISHU_APP_SECRET and OPENROUTER_API_KEY?
  • Does curl challenge verification return the same challenge value?
  • Does journalctl show incoming webhook requests?
  • Can the daemon reach both Feishu OpenAPI and the model provider?

If all of the above are correct, the Feishu deployment should be reproducible.

For webhook mode specifically, one more practical rule is worth remembering:

  • If curl http://<your-domain-or-ip>/api/v1/health still times out from outside the server, Feishu callback configuration will not succeed yet.

For local websocket-only usage, the equivalent short checklist is:

  • Is the Feishu app set to persistent connection, not webhook?
  • Is channels.feishu.transport = "websocket"?
  • Are FEISHU_APP_SECRET and the model provider key exported locally?
  • Can the local machine reach Feishu and the model provider outbound?
  • Does the daemon stay running without repeated websocket reconnect errors?
  • After sending a real Feishu message, do local daemon logs show websocket event processing?