Adapter authoring
An adapter connects one chat platform to Zhin. You only need this page if the platform you want has no official adapter yet; supported platforms are listed under Adapters.
An adapter does three things:
- creates the platform SDK client;
- turns platform events into Zhin's unified messages;
- turns outgoing Zhin messages into platform API calls.
Hot reload, account identity, version switching and resource cleanup are handled by the framework.
Minimal implementation
Create adapters/<platform>/index.ts in your plugin and default-export defineAdapter():
// adapters/example/index.ts
import { defineAdapter } from 'zhin.js/adapter';
import { ExampleClient } from 'example-sdk';
export default defineAdapter<{ readonly token: string }>({
capabilities: ['inbound', 'outbound'],
create(context) {
const client = new ExampleClient(context.config.token);
return {
client,
async connect({ events, signal, onCleanup }) {
await client.login({ signal });
onCleanup(() => client.logout());
const unsubscribe = client.onMessage((message) => {
void events.message({
conversation: {
kind: message.groupId ? 'group' : 'private',
id: message.groupId ?? message.userId,
},
content: message.text,
sender: { id: message.userId, name: message.nickname },
});
});
onCleanup(unsubscribe);
},
send({ conversation, payload }) {
return client.sendMessage(conversation.id, payload);
},
};
},
});Piece by piece:
| Part | Purpose |
|---|---|
capabilities | Declares receiving (inbound), sending (outbound), or both — at least one |
create(context) | Called once per account; context.config is that account's config |
client | The platform SDK instance. Commands get it as context.$client, event handlers as event.client |
connect() | Connects to the platform and returns only once it is usable |
onCleanup(fn) | Register cleanup right after acquiring each resource. If later setup fails, the framework rolls back in reverse order |
events.message() | Reports an incoming message; the framework fills in the account identity |
send() | Sends a message and returns the platform message id |
With a single cleanup action you can also just return it from connect().
Common tasks
Receive this platform's messages in your bot
Mount the plugin containing the adapter on your bot and configure it in zhin.config.yml:
configVersion: 2
plugins:
example: '@example/adapter'
example:
token: ${EXAMPLE_TOKEN}After startup, check in Console's Chat accounts that the account is online and receiving. Mounting is covered in First plugin · Mount it on your bot.
Several accounts on one adapter
No code changes. Add an endpoints array in config; each entry creates a separate account connection, and create(context) receives the merged per-account config. Rules: Endpoint lifecycle · Several accounts on one adapter.
Listeners only the active version may hold
Some listeners or input streams may be held by only one version during a hot-reload switch. Implement a synchronous activate({ events }) returning a release function; the framework calls it on switch and rollback. Ordinary SDK listeners belong in connect().
WebSocket / SSE, heartbeats and reconnection
Don't write your own reconnect state machine — use createEndpointLifecycle.
Platform-specific message formats
Convert platform-specific structures such as share, audio or cards at the send() boundary. Plugins still send through Message.$reply or Adapter.sendMessage; never open a separate send path.
Manage accounts from chat
createEndpointCommands generates <adapter> endpoint list / add / remove commands; see Commands · Endpoint management commands for adapters.
When to extend Endpoint
defineAdapter as above covers most platforms. Only when a protocol needs a custom multi-stage connection flow should you extend the Endpoint class and implement start / open / close / stop yourself; see Endpoint lifecycle hooks.
Reference implementations
examples/minimal-bot/adapters/terminal/index.ts: treats the terminal as a chat platform — good to study.plugins/adapters/napcat: WebSocket / HTTP connection modes usingcreateEndpointLifecycle.
Next steps
- Endpoint lifecycle: long-lived connections, heartbeats, reconnection and multiple accounts.
- Message flow: the full path of a message through the framework.
- Complete plugin delivery: test the adapter with the adapter harness and publish it.
AI capabilities and scene guidance
The adapter owns platform capability declarations; the Agent should not maintain a second operation catalog:
- A Tool's
description,inputSchema,platforms / scopes / permissionsdescribe one operation. agents/<platform>/skills/<name>/SKILL.mdgroups operations by purpose and scene; instructions load on demand.prompt-sections/<name>/index.tscontains necessary shared conventions, restricted byplatformsandscopes, without repeating the Skill/Tool catalog.segments.supporteddeclares outbound types.segments.model[type]supplies extension fields, platformhintand optionalscopes. A scene mismatch omits both its model schema and hint. This grants no send permission and does not change manually authored message delivery.
This example offers model-authored share cards only in groups; other scenes retain plain text and existing fallbacks. Declarations must match actual platform delivery, not assumptions based on names.
segments: {
supported: ['text', 'share'],
model: {
share: {
scopes: ['group'],
hint: 'Use a real URL and title from the user or tool results.',
},
},
}text always remains a common fallback; scene declarations on it restrict only its extra fields and hints.