OneBot v11
- Adapters
- NapCat, Lagrange.Core, go-cqhttp
- Connection
- The adapter dials Diana's reverse WebSocket
- Address
ws://HOST:18080/onebot/v11/ws- Features
- Group lists, CQ codes, quotes, group files, local media
Configure
Establish a working model connection first, then assign model roles and platform channels, and finally narrow triggers, hours, users and tool boundaries per group.
Platforms
Every enabled bot profile runs in parallel, and replies always return to the channel they came from. Credentials are entered per platform on the Bots page; leaving a secret field blank keeps the stored value, and filling it in replaces it.
What actually shapes deployment is not the platform itself but how messages reach you. The first four connect outbound from Diana, so a home connection or an internal network works as-is. Feishu and WeCom can only POST events to you, which means a publicly reachable HTTPS address.
| Platform | How it connects | Public address |
|---|---|---|
| OneBot v11 | Reverse WebSocket, the adapter connects to Diana | No |
| Telegram | Bot API long polling, outbound from Diana | No |
| QQ official bot | Open Platform WebSocket gateway, outbound from Diana | No |
| DingTalk | Stream mode connection, outbound from Diana | No |
| Feishu / Lark | Event subscription callback, pushed to Diana | Yes |
| WeCom | Custom-app callback, pushed to Diana | Yes |
ws://HOST:18080/onebot/v11/wsopen.larksuite.comFeishu and WeCom need the addresses below entered in their own consoles. HOST must be reachable from the platform's servers, over HTTPS.
https://HOST/api/channels/feishu/callback
https://HOST/api/channels/wecom/callback
With more than one bot on the same platform, append the profile id to tell them apart, for example /api/channels/feishu/callback/<profile-id>. With a single bot the short address is enough.
Platform servers cannot carry your session, so each is verified by its own scheme instead: WeCom checks msg_signature, decrypts with WXBizMsgCrypt and confirms the corp id inside the payload; Feishu checks the Verification Token and, when an Encrypt Key is set, also verifies X-Lark-Signature before decrypting. The callback URL itself is public information and is not a credential, so do fill in the verification secrets.
/media/resolver on OneBot; Telegram cannot reach a local address, so files are uploaded directly as multipart instead.Each bot keeps its own platform, account, owner, persona and model assignments. Replies always go back to the source channel. Conversation context is always isolated by bot profile; matching group or user IDs never merge sessions. Message relays remain available. Cross-platform memory is a separate opt-in on both bots and only retrieves non-sensitive public group facts and summaries, not raw conversations or personal memories.
The legacy isolate_platform_contexts setting is ignored and its endpoint has been removed. Previously shared history is retained in its original namespace, without automatic reassignment to individual bots. New messages use each bot's isolated sessions.
Models
A provider holds the base URL, API key and protocol. The top of the page syncs the model catalogue from the server and the bottom picks a default; the bot configuration then binds each role to a specific model.
| Role | Used for | How to test it |
|---|---|---|
| Chat | Final replies and agent reasoning | Check phrasing, tool calls and long context. |
| Vision | Photos, screenshots, historical images | Send a real image and check small-text OCR, UI structure and multi-image relations. |
| Intent | Unprompted replies, suppression, relationship checks | Check latency, stable structured output and edge cases. |
| Image generation | Text-to-image and image editing | It must actually return an image — a text-only connectivity check is not enough. |
"Detect other bots and stay quiet" is enabled by default to stop bots talking to each other in a loop. Turn it off only when you have a concrete reason.
A profile's "Credential" setting can use OAuth instead of an API key. The console usually runs on a server and your browser is not necessarily on the same machine, so the callback does not have to land back locally: click "Sign in", finish the authorization in your own browser, then paste the whole callback URL from the address bar back in (just the code also works). The token is refreshed before it expires; if the profile also holds an API key, a failed refresh falls back to it rather than taking the whole profile down.
Providers are configuration, not code. OpenRouter ships built in; anything else goes under "Custom provider" with its authorize URL, token URL, client ID and scopes — suitable for a self-hosted gateway or any service not shipped here. Both URLs must be https; loopback addresses (127.0.0.1) may use http.
How the token is then presented to the model API is configuration too. The default Authorization: Bearer <token> suits almost every provider; not all of them, though — Anthropic answers an OAuth token in Authorization with a 401 "OAuth authentication is currently not supported." and wants the token in x-api-key with no prefix. So a custom provider can set the token header, the token prefix and a set of extra headers under "Advanced". When the token goes somewhere other than the default, the auth header the SDK writes itself is stripped so one request never carries two credentials.
The request body format of the token exchange is chosen under "Advanced" too. RFC 6749 §4.1.3 requires application/x-www-form-urlencoded at the token endpoint, so that is the default; JSON is a dialect some services speak instead, and the built-in OpenRouter only accepts JSON. Send the wrong one and the server usually answers with a bare "missing parameter" rather than saying the whole body failed to parse, which sends you looking at the client ID. Custom providers saved before this option existed keep sending JSON so a working setup does not suddenly stop exchanging tokens; newly added ones use the spec default.
That use misrepresents the client's identity to the authorization server and goes beyond what a consumer subscription itself licenses. Whether to do it is the operator's own call — it can be entered under "Custom provider".
llm:
context_window_tokens: 65536
max_context_tokens: 65536
max_output_tokens: 2048
timeout: 60000ms
Images consume vision tokens too. When vision quality is poor, first confirm that the role really is bound to a vision model, that the original image made it into the request, and that images were not dropped by count or token budget — before raising the detail level or the context ceiling.
Group policy
Group management syncs every group the bot has joined through the OneBot get_group_list call, not just the ones with local configuration. It falls back to the groups stored in SQLite only when the connection is unavailable.
Stopping one group does not affect other groups or direct messages.
Explicit mentions, quotes, trigger words and semantic routing are evaluated in layers; an explicit mention outranks a quote.
Same-day and overnight windows with IANA time zones. This is group policy and never enters the model prompt.
Blacklist (default) replies to everyone but the muted accounts; whitelist replies only to the listed accounts and sends nothing at all to the rest, not even the quiet-hours notice. The owner is exempt from the whitelist.
The group list and the global list are a union: adding someone here never drops the global mute list. Level and hours, by contrast, are taken over wholesale by "set reply rules for this group".
Group-level prompts, history depth and per-message reply limits can override bot defaults. The existing max_reply_chars field and default of 3500 remain, but now apply per message on every platform, not to the combined length of all parts. Generation prompts include the limit. When splitting is allowed, complete paragraphs, sentences or sections are grouped first; only parts that remain oversized are compressed, leaving other parts unchanged. At most two compression calls are allowed per reply. Failure sends no partial result, and content is never hard-truncated. An explicit single-message request instead compresses the whole reply. Model output token budgets and delivery pacing still apply.
Each bot owns its plugin switches, settings and credentials. There is no global-default or inheritance control. On upgrade, legacy shared values are copied once to existing bots without overwriting independent configurations; the migration marker is persisted and old shared values are removed. New bots use built-in defaults with empty credentials. Resetting settings affects only the selected bot. Group overrides belong to that bot; credentials are not group-editable. OpenAPI switches, rate limits and keys live in system settings; installation and dependencies remain process-wide. Group administration is bound to a specific OneBot profile. Subscriptions remain separate without automatic URL merging.
QQ/OneBot merge thresholds package the parts into a forward card without shortening the content. Telegram ignores those thresholds and never automatically concatenates separate messages. It sends normal parts or one message according to the user's choice and validates rendered platform capacity. Oversized parts are compressed first; content such as code that cannot be safely reduced still fails rather than being corrupted. Other platforms never attempt OneBot forwarding.
Portable personas
The persona library stores "who it is and how it talks" as named sets you can switch between. The whole library and a single persona export to the same file format, and both import back — moving machines, keeping a backup, or handing a persona you tuned to someone else is all this one file.
The library is a source to apply from, not a live binding: picking a set fills those fields into the bot form, and what runs afterwards is the form. Editing the library entry never silently changes a bot you already configured.
{
"version": 1,
"personas": [
{
"name": "Ranran",
"system_prompt": "You are one of the people in this group chat, not an assistant.",
"reply_style": "human",
"action_description_enabled": false,
"self_reference": "I",
"sentence_enders": ""
}
]
}
Import accepts three shapes so you never have to reshape a file by hand: the full {"personas": [...]}, a bare array [...], or a single persona {...}. The file carries no IDs or timestamps — those are local state and are reassigned on import.
| Field | Required | Notes |
|---|---|---|
name | Yes | Shown in the library, up to 40 characters. |
system_prompt | No | The persona body, up to 4000 characters. |
reply_style | No | Speaking style, see below. Empty means this persona does not pin a style. |
action_description_enabled | No | Whether to weave parenthesised actions around dialogue. Defaults to false. |
self_reference | No | How the bot refers to itself. |
sentence_enders | No | Comma-separated sentence-ending particles. Several values are candidates the model picks from by mood. |
| Value | Style |
|---|---|
human | A specific person talking to you — emotions out front, clingy, leaves hooks |
assistant | Default |
gentle | Gentle |
lively | Lively |
concise | Concise |
catgirl | Catgirl |
roleplay | Roleplay — exposed in the UI as the "action description" switch; imports as assistant plus action_description_enabled: true |
An unrecognised value does not fail the import: those personas come in as assistant, and the console names the value it could not resolve so a typo in a hand-written file is easy to find.
Reply mode (how readily it joins a chat), time-of-day tone, refusal wording and model assignments are not part of a persona file. Those describe how this bot runs in this group, not who it is — the same persona belongs at different chattiness in a work group and a hangout group.
Import also accepts SillyTavern character cards: V1/V2/V3 JSON files and PNGs with an embedded card (the chara/ccv3 tEXt chunk). The card's description, personality, scenario, example dialogue and greeting are composed into one persona prompt ({{char}}/{{user}} macros are expanded in place; past the 4000-character budget, later sections are dropped whole). An embedded character_book is merged into the world book along the way. Alternate greetings, creator notes and other fields with no counterpart are not imported.
The repository ships hand-written examples you can import directly: examples/personas/.
World and relationships
The world book is the lore book of the world your bot lives in, maintained under Bots → Persona → World book. Entries are organised as chapters on a tree, and the path itself becomes context ("Zhijiang / Harbor: …"). Each node declares how it is injected: always-on entries ride along on every turn and describe the skeleton of the world; entries with trigger keywords only enter the context when recent conversation touches them, so details never waste tokens. Title-only nodes act as folders and inject nothing; disabling a chapter disables everything under it.
There is one book shared by all bots; each bot's own switch decides whether it uses it (on by default — an empty book injects nothing anyway). The whole book exports to JSON and imports back; parent-child links are rebuilt from the references inside the file, and like persona files, IDs are machine-local state and are reassigned on import.
Import is also compatible with SillyTavern world books: a world info file's top-level entries, or the character_book embedded in a character card, can be selected directly. Fields map by meaning — blue-light constant becomes always-on, green-light keys become trigger keywords, comment becomes the title (falling back to the first key or the start of the content), disable becomes disabled — sorted by order and placed at the root. Secondary keys (keysecondary, AND ANY logic) carry over too: the entry then requires a primary hit plus any secondary hit. Other secondary logics (NOT ANY etc.) invert the meaning, so those entries fall back to primary keys only. Recursive scanning, probability and insertion position have no counterpart here and are ignored; injection follows the local rules.
AI romance is off by default and is enabled by the owner on the same page. With it on, when a user sincerely confesses, the bot decides based on favorability and time spent together — not enough and it declines gently, without quoting numbers. Romance is monogamous: one bot has at most one partner at a time, and while a partner exists every confession is declined (without revealing who the partner is) until that relationship ends. Once established it keeps anniversaries and speaks like a partner; if favorability drops too low the couple enters a cold war. On monthly and yearly anniversaries it also sends one proactive private greeting during the day (9:00–22:00), at most once per day, deduplicated across restarts. The user can break up at any time, and the owner can dissolve any pairing; breaking up clears neither favorability nor the portrait. Being partners changes tone and manner only — it unlocks no permissions, and the bot never courts users on its own. With the switch off the bot does not know the feature exists and responds to confessions as an ordinary acquaintance would.
Bots → Persona → Human-like adds three independent switches, all off by default:
Agent
The agent runner feeds structured tool results back to the model until it produces a final reply or hits the step limit. The tool registry narrows by platform, relationship permissions, group configuration and feature state.
Web search ships with Diana and cannot be installed or removed. The model is told to search first for time-sensitive questions, fact checks, unfamiliar entities and recommendations.
One-off and recurring tasks share the same persistence; the console shows the schedule, next run, source and status.
External extensions are discovered from configuration and constrained by working directory, allow lists, permissions and timeouts.
Recognition, generation and editing are authorised separately; image generation is health-checked with a real generation request.
diana.host_stats lets the bot answer "how much memory is in use", "is the CPU busy", "how hot is it right now": CPU model and load, load average, memory, disk, Diana's own usage, plus whatever temperature, power and battery readings the machine exposes. Read-only, no parameters, and it shares its collector with the console dashboard.
Temperatures come from /sys/class/hwmon, power from the RAPL energy counters, battery from /sys/class/power_supply — no dependency on lm-sensors or any other external command, and a container only needs /sys mounted. RAPL exposes cumulative energy rather than instantaneous power, so power needs the difference between two samples; the first call says so instead of inventing a number. On macOS powermetrics needs root and Windows has no general interface, so both report temperature and power as unavailable — a fabricated number is worse than no number. That holds for every field: anything that cannot be read is reported with the reason, never as a zero.
This tool is owner-only too: hostname, disk paths and hardware models should not be visible to everyone in a group.
Every local tool is confined to workspace under the data directory. The path is fixed and not configurable; absolute paths and ../ are rejected. The three tiers are independent:
| Tier | Tools | Default | Switch |
|---|---|---|---|
| Read | read_file, list_files, grep, find_files | on | follows the "built-in agent" switch |
| Write | write_file, edit_file | on for new profiles | "allow file writes" |
| Execute | run_command | read-only allowlist for new profiles | registered only when the command allowlist is non-empty |
A newly created bot works out of the box: writes are on (confined to workspace under the data directory, so config and the database stay out of reach) and the command allowlist is seeded with commands that only report state — uptime, free, df, uname, nproc, date, hostname, whoami.
Three conditions decide what goes in that default list, and all three must hold: it cannot read files at arbitrary paths, cannot reach the network, and cannot change anything. Without a working sandbox, an allowlisted binary runs with Diana's own process permissions — the allowlist governs what may run, never what it may touch. So cat, ls, find (arbitrary reads), curl, wget (network) and ps (other processes' full command lines, which can carry someone else's secrets) are deliberately left out; reading inside the working directory goes through read_file / grep / find_files, which stay inside workspace.
Upgrading an existing deployment never grants command execution or file writes it did not have: an allowlist left empty stays empty, a write switch left off stays off. A default is a convenience for new users, not a silent capability grant to deployments already running. An older deployment that wants these can use "Fill in recommended defaults" on the same form — it only edits the form, and nothing takes effect until you press Save, so the grant is yours.
Each edit_file replacement must match exactly one place in the original file; several matches or none rejects the whole batch rather than editing the wrong spot. All replacements are located against the original file, not applied one after another — applying them in sequence would let a later replacement match what an earlier one just wrote, and that edit is invisible in the caller's input.
These tiers are owner-only, but "the call happened in the owner's conversation" is not "the owner asked for this call": other people's messages share the context with the owner's, and the model decides to call a tool after reading them. The real boundary is these switches and the allowlist, not the identity check — open the tier you need and leave the rest closed.
Security
config.yaml, model API keys, bot tokens and platform cookies out of Git.