Channel Configuration
How channel configuration metadata drives Console UI and runtime persistence
Channel Configuration
Each chat channel (Telegram / Feishu / QQ) exposes a runtime-readable configuration descriptor.
This descriptor directly controls:
- Which fields Console UI renders
- Which fields are editable
- Field types (
string/boolean/number/secret/enum) - Field source (
downcity.json)
Why this design
- Remove frontend hardcoding for per-channel fields
- Centralize validation in
chat.configure - Keep clear boundaries:
- Agent service (
downcity.json): channel binding + runtime toggles only - Global Channel Accounts (
downcity.db): all bot information (token/appId/appSecret/name/owner/creator)
- Agent service (
Agent Service Scope
Agent-side channel config only includes:
enabledchannelAccountId
Only fields with writable=true are editable in Console UI and persisted by chat.configure.
Runtime flow
1. Read status
chat.status returns, per channel:
detail.config: safe config summarydetail.configuration: full configuration descriptor
2. Render UI
Console UI renders forms dynamically from detail.configuration.fields:
- writable fields ->
enabled/channelAccountId
3. Save config
On save, Console UI sends only writable patch fields.
Backend then:
- Filters fields by channel descriptor whitelist
- Normalizes and validates by field type
- Updates in-memory runtime config
- Persists to project
downcity.json - Restarts/reloads channel if requested (default: restart)
Relationship with Channel Accounts
Recommended flow:
- Fill credentials in Global Channel Accounts and click
Test - Click
Confirmonly after test passes (bot base info is auto-discovered and saved) - Bind
channelAccountIdin channel configuration - Run
test/reconnect
This cleanly separates secret management and per-agent channel runtime config.
In multi-agent setups: what is shared and what stays agent-local
This boundary is important:
- Bot credentials behind
channelAccountIdare shared globally indowncity.db chatauthorization rules are also shared globally indowncity.db- agent-local config only decides which channels are enabled and which
channelAccountIdis bound
That means multiple agents can bind the same Telegram / Feishu / QQ bot account, and the same platform user's role stays consistent across those agents:
- default roles
- user role bindings
- who can use DMs
- who can trigger the agent from group chats
In practice:
- Channel Account decides which bot identity connects to the platform
- Chat Authorization decides whether that platform user may use chat access in this city
Example
For example:
agent-Aandagent-Bboth bind the samechannelAccountId- an admin runs
city chat auth set telegram:12345678and choosesadmin - that Telegram user enters authorization as
adminfor agents connected to Telegram
Result:
- the user uses the same city-level authorization in both
agent-Aandagent-B - switching agents or rebinding a bot account does not invalidate the user's role
So channel accounts and chat authorization are both city-level configuration. Agents only decide which channel account they connect to.
How bot info is fetched
You do not need to manually enter bot name, bot id, or identity.
The system fetches them automatically by channel:
- Telegram:
botToken->getMe - Feishu:
appId + appSecret-> access token -> bot info endpoint - QQ:
appId + appSecret-> gateway validation -> bot profile when available
Console UI keeps the flow minimal and focuses on test status + confirmation.
What you will use
Console UI
- Global -> Channel Accounts: manage global channel accounts
- Channel -> Configuration: bind
channelAccountId, toggleenabled
CLI
Inspect descriptor:
city service command chat configuration --channel qqUpdate and restart:
city service command chat configure \
--channel qq \
--config-json '{"channelAccountId":"qq-main","enabled":true}' \
--restartFAQ
Why no plaintext secrets in channel pages?
By design, channel pages expose safe summaries only.
Why are some fields not editable?
Channel pages handle binding/toggles only. Bot details are managed in Global Channel Accounts.
Why did new fields appear automatically in Console UI?
Because rendering is metadata-driven from detail.configuration, not hardcoded.