diff --git a/zh-CN/guide/adapter/adapter.md b/zh-CN/guide/adapter/adapter.md
index b8b2655d1af0..6d20b017252c 100644
--- a/zh-CN/guide/adapter/adapter.md
+++ b/zh-CN/guide/adapter/adapter.md
@@ -166,11 +166,12 @@ for (const event of parsed.events) {
首先调整目录结构,在 `server.ts` 和 `polling.ts` 中分别完成两种通信方式的适配器开发:
-```text{5-6}
+```text{5D,6-7A}
adapter-telegram
├── src
│ ├── bot.ts
│ ├── index.ts
+│ ├── adapter.ts
│ ├── polling.ts
│ └── server.ts
└── package.json
diff --git a/zh-CN/guide/adapter/message.md b/zh-CN/guide/adapter/message.md
index a85728f375ff..6b669d36475e 100644
--- a/zh-CN/guide/adapter/message.md
+++ b/zh-CN/guide/adapter/message.md
@@ -44,3 +44,134 @@ session.elements = [
```ts
session.content = input.replace(/@(\d+)/g, '')
```
+
+## 发送消息
+
+### 兼容性原则
+
+在具体介绍消息发送之前,不知道你是否有这样的疑问:Koishi 提供了一整套标准的消息元素,但并非所有平台都支持这些元素。对于那些不支持的元素,应该如何处理呢?
+
+Koishi 的建议是**尽量兼容实现**。对于平台不支持的元素,可以根据元素的类型和用户的配置进行转化与回退。大致可以分为两种情况:
+
+- 修饰型的元素可以选择只渲染内部的元素,或以适当的方式进行文本修饰。
+
例如:在不支持粗体的平台上渲染 `` 时,可以改为只渲染粗体的内容。
+
例如:在不支持列表的平台上渲染 `` 时,可以在每个列表项前面渲染一个 `-`。
+
+- 占位型的元素尽量转换为可渲染的元素;如果实在无法渲染则抛出错误。
+
例如:如果平台不支持发送网络图片,可以先将图片下载到本地再发送。
+
例如:如果平台不支持发送语音,可以改为发送文件,或抛出错误。
+
+对于更加复杂的元素,适配器也可以发挥自主性,设计最适合的交互形式。例如,如果用户的需求是「从若干个选项中选择一个」,那么平台 A 可以渲染出多个按钮供用户点击;平台 B 则可以发送一条带有表态的消息,点击表态对应选择选项;实在不行,平台 C 也可以直接发送选项列表和文本提示语,并将用户的下一次输入作为选项。
+
+### 消息编码器
+
+之前介绍过的 REPL 适配器为了简化写法,并未包含消息的编码过程。对于一般的适配器,我们建议通过继承 `MessageEncoder` 类来实现消息的发送逻辑。
+
+这里我们以 Telegram 平台为例,首先在源码目录下创建 `message.ts`:
+
+```text{6A}
+adapter-example
+├── src
+│ ├── adapter.ts
+│ ├── bot.ts
+│ ├── index.ts
+│ └── message.ts
+└── package.json
+```
+
+在这个文件中我们定义 `TelegramMessageEncoder`:
+
+```ts title=message.ts
+class TelegramMessageEncoder extends MessageEncoder {
+ // 使用 payload 存储待发送的消息
+ private payload: Dict
+
+ constructor(bot: TelegramBot, channelId: string, guildId?: string, options?: SendOptions) {
+ super(bot, channelId, guildId, options)
+ const chat_id = guildId || channelId
+ this.payload = { chat_id, parse_mode: 'html', text: '' }
+ }
+
+ // 将发送好的消息添加到 results 中
+ async addResult(message: Telegram.Message) {
+ const session = this.bot.session()
+ await adaptMessage(message, session)
+ this.results.push(session)
+ session.app.emit(session, 'send', session)
+ }
+
+ // 发送缓冲区内的消息
+ async flush() {
+ if (this.payload.text) {
+ const message = await this.bot.internal.sendMessage(this.payload)
+ await this.addResult(message)
+ delete this.payload.reply_to_message_id
+ this.payload.text = ''
+ }
+ }
+
+ // 遍历消息元素
+ async visit(element: h) {
+ const { type, attrs, children } = element
+ if (type === 'text') {
+ this.payload.text += h.escape(attrs.content)
+ } else {
+ await this.render(children)
+ }
+ }
+}
+```
+
+一个 `MessageEncoder` 类需要提供 `flush` 和 `visit` 两个方法。前者用于发送缓冲区内的消息,后者用于遍历消息元素。消息发送完成后,还需要构造相应的 `Session`,用于触发 `send` 会话事件并存储于 `results` 数组中。
+
+与此同时,我们还需要修改 `TelegramBot` 类,为其添加静态属性。实现了 `MessageEncoder` 静态属性后,就无需手动实现 `bot.sendMessage()` 和 `bot.sendPrivateMessage()` 方法了:
+
+```ts title=bot.ts
+export class TelegramBot extends Bot {
+ static MessageEncoder = TelegramMessageEncoder
+}
+```
+
+### 行内元素
+
+上面的例子仅仅包含了消息编码器的基本结构,并未实现除了文本外的任何消息元素。对于任何非文本元素,上面的代码都会回退到其内部的文本。要添加更多消息元素的支持,只需在 `visit` 方法中添加更多的判断分支,就像这样:
+
+```ts
+if (type === 'text') {
+ this.payload.text += h.escape(attrs.content)
+} else if (['b', 'strong', 'i', 'em', 'u', 'ins', 's', 'del'].includes(type)) {
+ // 这些元素都是 Telegram 已经支持的,直接渲染成 HTML 即可
+ this.payload.text += `<${type}>`
+ await this.render(children)
+ this.payload.text += `${type}>`
+} else if (type === 'at') {
+ // 将 at 渲染为用户链接
+ this.payload.text += `@${attrs.name || attrs.id}`
+} else {
+ await this.render(children)
+}
+```
+
+### 消息分片
+
+在 Koishi 中,一次消息发送可能在目标平台产生多条独立的消息,称为消息分片。这也是为什么上面的 `results` 是一个数组。消息分片产生的原因是多样的:
+
+- 某些元素的语义就是发送独立的消息 (例如 ``)
+- 部分平台不支持某些消息元素的组合 (例如图文混合发送),此时必须对消息进行拆分
+- 待发送的消息长度超出平台限制,此时必须对消息进行拆分
+
+在需要对消息进行分片的场合,我们可以手动调用 `flush()` 方法。例如下面的代码展示了如何实现 `` 和 `` 元素:
+
+```ts
+// 忽略前面的部分
+} else if (type === 'quote') {
+ // 将引用消息的 ID 添加到缓冲区
+ this.payload.reply_to_message_id = attrs.id
+} else if (type === 'message') {
+ // 在解析内部元素之前先清空缓冲区
+ await this.flush()
+ await this.render(children)
+ await this.flush()
+} else ...
+```
+