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:
- Webhook push
- 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
systemdavailable to manage the daemon processnginxor 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/eventsthen 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:
- Feishu sends to a public URL.
- That public URL reaches your local daemon.
- The path mapping is correct.
- 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.cnoutbound - 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.tomlsystemdnginx- your deployment scripts
6. Create the Feishu App
- Go to Feishu Open Platform
- Create a Custom App
- Add the Bot capability
- In the app sidebar, confirm the bot is enabled for the scenarios you need:
- direct message
- group chat mention
- 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_idApp Secret->FEISHU_APP_SECRETin your environment fileVerification 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:

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:
- Choose Webhook
- Confirm you are editing the correct app environment and not a stale draft app
- 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:

- Add the message event:
im.message.receive_v1
- Save and verify the URL
Example event subscription list showing the message receive event:

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:
- Make sure the event switch is actually enabled
- 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:
- 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
- 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
80or443is reachable from the public internet - the cloud security group / firewall allows inbound traffic
- 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:
- Choose Receive events/callbacks through persistent connection
- Confirm you are editing the correct app environment and not a stale draft app
- Add the message event:
im.message.receive_v1
- Save the event subscription
- 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:80or0.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:
- xiaoO daemon is running and healthy
- xiaoO is listening on
127.0.0.1:18080 - nginx has the correct
location = /api/v1/channels/xiaoo/events - nginx has been reloaded after editing the config
- the server's public
80or443port is reachable from the internet - 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.1is 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.1means xiaoO is intentionally internal-only- nginx is responsible for public exposure in webhook mode
EnvironmentFileis whereFEISHU_APP_SECRETandOPENROUTER_API_KEYare 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:
- Feishu app has webhook mode enabled
- Feishu callback URL points to the public endpoint:
/api/v1/channels/xiaoo/events
- Public DNS or IP resolves to your server
- Port
80or443is reachable from the public internet - nginx has a matching
location = /api/v1/channels/xiaoo/events - nginx proxies to:
http://127.0.0.1:18080/api/v1/channels/feishu/events
- xiaoO daemon is actually listening on:
127.0.0.1:18080
channels.feishu.enabled = truechannels.feishu.transport = "webhook"app_id,verification_token, andapp_secret_envare all configured- the environment variable named by
app_secret_envis actually present in the service environment - the daemon can reach Feishu OpenAPI and the model provider over outbound network
- 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:
- Check local daemon health on the server:
curl http://127.0.0.1:18080/api/v1/health
- Check public health from another machine:
curl http://<your-domain-or-ip>/api/v1/health
-
If step 2 times out, stop here and fix public ingress before touching Feishu:
- nginx config
- nginx reload
- cloud security group
- firewall
-
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"}'
- 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:
- Feishu app has persistent-connection mode enabled
channels.feishu.enabled = truechannels.feishu.transport = "websocket"app_idandapp_secret_envare configured- the environment variable named by
app_secret_envis actually present in the service environment - xiaoO daemon can reach:
https://open.feishu.cn- your model provider
- the daemon stays running long enough to maintain the websocket connection
- 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:
- Feishu app has persistent-connection mode enabled
channels.feishu.transport = "websocket"app_idandapp_secret_envare configured- the local shell or process environment contains:
FEISHU_APP_SECRET- the model provider key referenced by
api_key_env
- the local machine can reach:
https://open.feishu.cn- the configured model provider
- the daemon process remains running
- 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:
- ingress is acknowledged quickly
- xiaoO processes the task
- 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:
- Feishu app subscription mode is really set to persistent connection
- the app version has been published
- the daemon can reach
https://open.feishu.cnoutbound
17. Recommended Production Layout
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, andVerification 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 -tand reload nginx after updating the route? - Can the daemon read
FEISHU_APP_SECRETandOPENROUTER_API_KEY? - Does
curlchallenge verification return the same challenge value? - Does
journalctlshow 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/healthstill 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_SECRETand 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?