Desk 是一个可以自行运行的完整客服应用。访客在聊天窗口中提问;助手根据你的帮助中心作答并附上来源链接;当它无法回答,或访客要求人工服务时,对话会转交给你的团队,由团队在后台回复。一切都会实时更新。
它也是 ModelRiver 的参考应用:流式输出、对话记录、人工转接和实时更新都由 ModelRiver 提供,因此 Desk 本身非常精简。源码以 MIT 许可证发布在 github.com/modelriver/desk。


包含哪些部分
| 部分 | 作用 |
|---|---|
| 聊天窗口 | 根据帮助文章作答并附上引用,回复边生成边显示,需要时把对话转交给团队。可通过 /widget 嵌入任意网站。 |
| 帮助中心 | 把公开文章变成可搜索的网站,带有分区、目录、相关文章和“是否有帮助?”投票。 |
| 团队后台 | 查看所有对话、谁在处理、谁在等待。可以接管、回复、交还给 AI 或归档。 |
| 工单 | 团队通过邮件处理的请求,支持回复、内部备注、负责人、状态和 AI 起草的回复。客户可在客户门户中查看并回复自己的工单。参见工单。 |
| 知识库 | 在输入时即可排版 Markdown 的编辑器中撰写文章,用文件夹整理,并通过拖拽调整顺序。可以一次发布、取消发布或删除多篇。 |
| 洞察 | 帮助中心无法回答的问题,以及评分最低的文章,帮助你决定下一步写什么。 |
| 集成 | 可选的只读查询,连接 Shopify、Stripe、Cal.com 或你自己的 API,让助手根据已验证访客自己的订单、账单或预约作答。 |
聊天窗口中的机器人会展示它正在做什么:等待时为在线,访客输入时为倾听,撰写回答时为思考,回答完成后为已解决,把对话交给团队时为让位。


如何使用 ModelRiver
- 访客提问。Desk 在你的文章和私有笔记中搜索,挑选最相关的段落。
- 它把这些段落连同问题一起发送到
POST /v1/ai/async,并带上stream: true和访客的session_id。浏览器通过@modelriver/client加入该请求的频道,边接收边显示回答。 - 对话保存在 ModelRiver 的会话中。Desk 不保存消息副本;它自己的 SQLite 文件只记录对话与会话的对应关系。
- 团队接管后,他们的回复和访客的后续消息通过
POST /v1/sessions/:id/messages写入会话。AI 重新接手时会把这些内容视为对话的一部分。 - 聊天窗口和团队后台通过会话 API 的实时更新跟踪对话。Desk 自身的变化(接管、交还、归档)以信号的形式发出。
私有笔记会影响回答,但从不被引用、展示或链接。AI 无法访问 Desk 的数据库,也无法访问你连接的服务:由 Desk 的服务器决定查询什么,只把结果发送给模型。
快速开始
你需要 Node.js 26.12.1 或更高版本,以及一个 ModelRiver API 密钥。
git clone https://github.com/modelriver/desk.gitcd desknvm usenpm cicp .env.example .env # then set MODELRIVER_API_KEYnpm run create-adminnpm run dev打开 http://localhost:3000 查看帮助中心和聊天窗口,打开 http://localhost:3000/dashboard 进入团队后台。首次登录时会引导你设置双重验证。
创建工作流
在 ModelRiver 控制台中,通过导入工作流导入 examples/support_assistant.workflow.json 并发布为 Live。它是一个名为 support_assistant 的聊天工作流,已开启会话,提示词来自 examples/workflow-system-prompt.md。它使用 Anthropic claude-haiku-5-5 作答,并以 OpenAI gpt-6-luna 作为备用模型,即使某个供应商宕机,助手也能继续回答。请选择你的项目已配置密钥的模型。如需使用其他名称,请设置 MODELRIVER_WORKFLOW。
设置邮件
推荐设置。没有邮件 Desk 也能运行,但以下功能需要邮件:
| 需要邮件的功能 | 没有邮件时 |
|---|---|
| 在聊天中提交工单 | 无法发送验证码确认访客的邮箱,工单没有邮箱,团队只能在聊天中回复 |
| 回复工单 | 只能添加内部备注 |
| 登录客户门户 | 客户收不到验证码,无法登录 |
| 账户查询 | 只有在你的网站上登录(带身份令牌)的访客才能查询 |
| 团队通知 | 除非有人盯着后台,否则没人知道有新工单或有人在等待 |
在 .env 中添加邮件服务、它的密钥和你的地址,然后重启:
EMAIL_PROVIDER=resend # 或 postmark(POSTMARK_SERVER_TOKEN),或 ses(AWS_REGION 和密钥)RESEND_API_KEY=re_…EMAIL_FROM="Acme Support <[email protected]>" # 使用邮件服务已验证的域名SUPPORT_REPLY_TO=support@acme.com # 团队会查看的邮箱:客户对工单的回复会发到这里APP_URL=https://support.acme.com # 用于邮件中的链接在 后台 → 设置 → Send a test email(发送测试邮件) 中检查设置是否正确。开发时,EMAIL_PROVIDER=log 会把每封邮件(包括验证码)打印到终端,而不会真正发送。
配置
Desk 从 .env 读取配置。主要配置项:
| 变量 | 默认值 | 用途 |
|---|---|---|
MODELRIVER_API_KEY | 项目 API 密钥,仅在服务器端使用 | |
MODELRIVER_API_URL | https://api.modelriver.com/v1 | ModelRiver API 地址 |
MODELRIVER_WORKFLOW | support_assistant | 负责回答问题的工作流(需开启会话) |
DATA_DIR | ./examples/data | 你的文章和笔记(见下文) |
DATABASE_PATH | ./var/support.db | 存放用户和对话映射的 SQLite 文件 |
APP_URL | 你的 Desk 地址,用于邮件中的链接和搜索引擎 | |
EMAIL_PROVIDER | 推荐设置。resend、postmark、ses 或 log,用于工单、邮箱验证和团队通知(参见设置邮件) | |
EMAIL_FROM | 发件人,需使用邮件服务已验证的域名 | |
SUPPORT_REPLY_TO | EMAIL_FROM | 客户回复工单邮件时发往的地址:一个团队会查看的邮箱 |
ARCHIVE_RETENTION_HOURS | 24 | 已归档对话在删除前保留的小时数(ModelRiver 中的记录也会删除) |
WIDGET_FRAME_ANCESTORS | 允许嵌入聊天窗口的网站 | |
GLOBAL_MESSAGES_PER_MINUTE、GLOBAL_MESSAGES_PER_DAY | 20、800 | 全站 AI 回答次数上限,按免费套餐设定。付费套餐可调高 |
外观的默认值来自 support.config.json;在后台 → 设置中修改后无需重启即可生效(参见你的品牌),也可以在那里开启或关闭聊天和转接人工(参见聊天开关)。
你的内容
DATA_DIR 下有两个文件夹:
public/:Markdown 文章。它们组成帮助中心,助手会引用它们。文件夹即分区,子文件夹即子分区;文章地址随之变化,例如/articles/billing/refunds。private/:内部笔记和处理手册(.md或.txt)。助手会参考它们,但从不展示、引用或链接。
所有内容都可以在后台 → 知识库中编辑,在这里修改无需重启即可生效。也可以直接修改文件夹,修改后需重启 Desk 才会生效。
文章或 Markdown 笔记可以设为草稿:保存草稿会让它不出现在帮助中心,也不会发送给 AI;发布会加入它;取消发布会把它移回草稿。在文件中,草稿的前置信息里写有 status: draft。
你的品牌
管理员可以在后台 → 设置中设置外观,以及帮助中心在搜索结果中的呈现方式。修改会立即生效,无需重启。
| 标签页 | 可以设置的内容 |
|---|---|
| Appearance(外观) | 页头标志,可另加深色模式版本(推荐 SVG,显示高度为 26 px)。有标志后,可以隐藏标志旁的名称。一种品牌色,用于浅色和深色模式下的按钮、链接和高亮;选择黑色、白色或灰色时,按钮在浅色模式下近乎黑色,在深色模式下近乎白色。从 Google Fonts 的精选列表中选择正文和标题字体,以及圆角样式。首页的标题、标题下方的说明文字,以及提问框中的示例问题 |
| SEO | 名称,会以"页面 — 名称"的形式加到每个页面的标题中。首页自己的标题和 meta 描述,并附搜索结果预览。网站图标(方形 SVG,或 512 × 512 的 PNG) |
这些设置保存在 Desk 的数据库中,叠加在 support.config.json 之上,后者仍提供默认值。留空某一项,或点击恢复默认值,就会回到配置文件中的值(首页文字则回到 Desk 自带的措辞)。系统字体以外的字体从 Google Fonts 加载。
聊天开关
后台 → 设置 → General(常规) 的 Chat assistant(聊天助手) 下有三个开关。它们都会立即生效,修改人和修改时间会显示在开关旁边,并记录在审计日志中。
| 开关 | 关闭后 |
|---|---|
| Visitors can chat(访客可以聊天) | 聊天按钮、首页的提问框和嵌入的聊天窗口都会消失,新的对话和消息会被拒绝。文章和搜索照常可用。适合在故障期间或重写文章时使用 |
| Visitors can talk with a person(访客可以联系人工) | 访客仍可以和助手聊天,但要求人工服务时不会转接。助手会告诉访客团队暂时无人可以接待,访客可以改为提交工单(参见无人可接待时)。没有人能及时回复时可以关闭它 |
| Customers can raise tickets(客户可以提交工单) | 不再接受来自聊天或客户门户的新工单。如果 Talk with a person(联系人工) 也已关闭,助手只会告诉访客暂时无人可以接待。客户仍可以查看并回复已有的工单,团队也仍可以在后台新建工单 |
转接人工
当访客要求人工服务(包括在助手给出不确定的回答后接受它的转接建议),或 AI 无法回复时,对话会转交给你的团队。有人处理对话时,AI 保持安静;任何成员都可以把对话交还给 AI。团队成员可以在账户设置中选择:每个新对话都收到邮件,或仅在访客要求人工服务时收到邮件。
团队的回复会在访客的聊天窗口中显示成员的名字,以及他们在 Account settings → Profile(账户设置 → 个人资料) 中设置的头像(未设置头像的成员在后台中显示为圆形的姓名缩写)。
无人可接待时
关闭 Visitors can talk with a person(访客可以联系人工) 后:
- 访客无论用什么措辞要求人工服务,都会收到一句简短的回复,说明团队暂时无人可以接待,同时聊天窗口中会打开 Raise a ticket(提交工单) 表单:填写需要帮助的内容和邮箱,并在提交前输入发送到该邮箱的 6 位验证码,这样就没人能冒用他人的邮箱提交工单。已在对话中验证过邮箱的访客无需再次验证。提交后,访客会看到工单的编号,例如 #K7F3Q9。聊天菜单中的 Talk to a person(联系人工) 也会换成这个表单。
- 你的团队会在 Tickets(工单) 中收到访客提交的每张工单,附有访客的邮箱(经过验证的会标为 Verified)和来源对话的链接。访客会看到自己的工单编号。后台会显示未处理工单的数量,开启了“有人要求人工服务”邮件的成员会收到每张工单的通知。
- **仍然可以接管。**这个开关只是不让访客主动要求人工:团队中的任何人仍然可以在后台打开对话并接管,已经在等待人工的对话也会继续等待。
- AI 无法回答时,如果这个开关已关闭,对话不会转交给团队:访客会被请求稍后重试或提交工单。
两种角色:管理员管理团队、知识库和设置;客服负责处理对话。
工单
后台 → Tickets(工单) 汇集团队通过邮件、按自己的节奏处理的请求。工单有三种来源:无人可接待时访客在聊天中提交的工单、团队成员把某个对话转成的工单(Create ticket),或为电话、邮件等手动新建的工单(New ticket)。
| 状态 | Open(待处理)、Waiting on customer(等待客户) 或 Solved(已解决),各有一个标签页,最早的待处理工单排在最前。可按 Me(我) 或 Nobody(未分配) 筛选 |
| 回复 | 通过邮件从 EMAIL_FROM 发出,署名为团队成员的名字,主题中带有工单编号,引用客户的原始消息,并附有前往客户门户的 View ticket and reply(查看工单并回复) 链接。客户通过邮件的回复会发到 SUPPORT_REPLY_TO。发送失败的回复会保留在工单上,并在约 16 小时内自动重试;点击 Send again(重新发送) 可立即发送 |
| 主题 | 管理员可以在 Tickets → Subjects(主题) 中列出客户常见的问题类型。客户在聊天或门户中提交工单时从中选择一个,也可以选择 Something else(其他) |
| 确认邮件 | 访客或客户用已验证的邮箱提交工单后,会收到工单编号和前往门户的链接。确认邮件和每封回复都带有相同的邮件会话标头,因此会归在同一个会话中 |
| 通知 | 每位团队成员可以在 Account settings(账户设置) 中选择:Tickets assigned to you(分配给你的工单)(默认开启:别人把工单分配给你,或客户回复了你的工单)和 Every new ticket(每张新工单)(新工单,以及无人负责的工单上的客户回复)。发送失败的邮件会像回复一样自动重试 |
| 内部备注 | 只有团队可见,且从不发送给 AI |
| AI 草稿 | Draft with AI 会根据你的文章和私有笔记起草回复,由团队成员修改后发送。不确定的地方会标为 [add details],而不是凭空猜测。每份草稿是对你工作流的一次请求,计入全站 AI 上限;ModelRiver 为它创建的会话会被 Desk 立即删除 |
| 历史记录 | 每条消息、回复、备注、状态和负责人的变更,以及操作人和时间 |
| 保留期限 | 已解决的工单会在解决 90 天后删除 |
客户在门户中的回复会添加到工单上,并重新打开工单。通过邮件的回复会进入你的 SUPPORT_REPLY_TO 邮箱,而不会回到 Desk 中;收到回复后,把工单改回 Open(待处理) 即可。
客户门户
客户可以在 /customer-portal 查看用自己邮箱提交的工单、阅读你的回复、作出回复,以及提交新工单。无需密码:客户用发送到邮箱的 6 位验证码登录。设置好邮件后,帮助中心的页头会显示 Your tickets(你的工单) 链接。
| 从回复邮件进入 | 点击 View ticket and reply(查看工单并回复) 会自动登录并打开该工单。链接 30 天内有效,且只对该工单的邮箱有效 |
| 其他方式 | 输入邮箱和验证码。无论该邮箱是否有工单,页面的回应都一样,因此无法借此查出谁是你的客户 |
| 能看到什么 | 只有自己的工单:主题、编号、状态、自己的消息和你的回复。永远看不到内部备注、负责人或其他人的工单 |
| 提交工单 | 验证码已证明邮箱属于本人,因此标为 Verified(已验证)。每个邮箱每天最多 5 张,门户和聊天合并计算 |
| 保持登录 | 14 天,保存在 HttpOnly Cookie 中。Sign out(退出) 也会在服务器端结束会话 |
登录验证码、工单和回复都按邮箱、IP 和全站分别限流,验证码输错 5 次会锁定该邮箱 15 分钟。
屏蔽客户
在工单上点击 Block this customer(屏蔽此客户),或在 Tickets → Blocked customers(已屏蔽客户) 中屏蔽任意邮箱,会立即停止该邮箱的一切操作:收不到登录或验证码,工单链接失效,无法在门户或聊天中提交新工单,也无法回复。已登录的门户会话会被结束。对方不会收到通知,你的团队仍然可以给他们发邮件。在同一页面点击 Unblock(取消屏蔽) 即可撤销;两种操作都会记入管理员审计日志。
集成
连接器让助手可以根据访客自己的记录回答“我的订单到哪了?”“我的套餐什么时候续费?”“我的预约是什么时候?”。管理员在后台 → 集成中粘贴密钥即可开启。
| 连接器 | 助手能看到的内容 | 密钥 |
|---|---|---|
| Shopify | 最近的订单:状态、商品、发货和物流追踪链接 | 只有 read_orders 权限的 Admin API 令牌 |
| Stripe | 订阅、套餐、续费日期和最近的发票链接 | 只读的受限密钥(rk_…) |
| Cal.com | 即将到来的预约,以及改期和取消链接 | API 密钥 |
| Custom HTTP | 你自己的 API 返回的最多 20 条信息 | 你的 URL 和签名密钥 |
- 访客先验证邮箱。 访客通过邮件收到的 6 位验证码完成验证;对于已在你网站登录的访客,也可以由你的网站传入签名的身份令牌。查询只使用这个已验证的邮箱,所以任何人都看不到别人的记录。验证在本次对话中持续有效,最长 24 小时(
VERIFIED_HOURS),或在 2 小时没有发送消息后失效,之后需要重新验证。 - 只读。 带写入权限的 Shopify 令牌或完整的 Stripe 密钥会被拒绝。退款和修改会给出链接,或转交给你的团队。
- 只取需要的内容。 每个连接器只保留一组固定的少量字段;地址、电话号码和银行卡信息绝不会进入对话。
- 密钥留在你的服务器上。 密钥用
DESK_SECRET_KEY加密,保存后不再显示。Desk 直接调用各项服务;ModelRiver 只会在该轮请求中看到简短的结果。 - 有据可查。 每次查询都会记录(邮箱已打码)及其结果,你的团队也能在对话中看到。
查询不会产生额外的 AI 请求。每个连接器的设置步骤、Custom HTTP 请求格式以及如何签名身份令牌,请参阅 README。
部署
Desk 只是一个 Node.js 进程加一个 SQLite 文件。它保存的所有内容(文章、笔记、数据库和品牌设置)都在同一个数据目录里,所以只要这个目录在重启后还在,Desk 就能运行。每种方式都有单独的指南:
| 方式 | 适合 | HTTPS | 需要 |
|---|---|---|---|
| Ubuntu 服务器 | 自己管理的 VPS,包括小内存机器(512 MB) | 自动配置 nginx 和 Let's Encrypt | Ubuntu 22.04 或更高版本,以及一个域名 |
| Docker Compose | 任何装有 Docker 的服务器 | Caddy 自动处理 | Docker 和 Compose 2.23 或更高版本,以及一个域名 |
| Fly.io | 用命令行完成的托管部署 | 由 Fly 提供 | Fly 账号和 flyctl |
| Render | 打开一个链接即可完成的托管部署 | 由 Render 提供 | 使用付费实例类型的 Render 账号 |
| Railway | 支持自动更新的托管部署 | 由 Railway 提供 | Railway 账号 |
| 手动部署 | 其他 Linux 发行版 | 你自己的反向代理 | Node.js 26.12.1 或更高版本 |
更新介绍了如何在以上每种方式上手动或定期更新到新版本。在 Ubuntu 服务器上只需运行 sudo desk-update:它会先备份,新版本无法启动时自动回到原来的版本。
Docker 镜像 ghcr.io/modelriver/desk 同时支持 amd64 和 arm64,Docker Compose、Fly.io、Render 和 Railway 运行的都是它。1 标签会跟随所有 1.x 版本;使用完整标签(如 1.0.1)可以固定在某个版本。
无论选哪种方式:
- 先导入工作流。 按上文「创建工作流」在 ModelRiver 控制台中导入工作流并发布为 Live,否则所有回答都会失败。
- 必须使用 HTTPS。 生产环境下 Desk 的登录 Cookie 只在 HTTPS 下发送,所以后台只能通过 HTTPS 使用。每种方式都会配置好 HTTPS。
- 两个密钥。
DESK_SECRET_KEY(32 字节,base64:openssl rand -base64 32)用于加密连接器密钥,DESK_IDENTITY_SECRET(openssl rand -base64 48)用于签名身份令牌。只生成一次并妥善保存:如果DESK_SECRET_KEY变了,集成页面上保存的密钥将无法读取。 - 只运行一个实例。 限流计数保存在内存中,数据库是一个文件,所以不要让两个实例使用同一份数据。
APP_URL。 知道访问地址后就把它设为 Desk 的地址,团队邮件中的链接和站点地图会用到它。- 邮件(推荐)。 设置邮件后,访客才能提交团队可以回复的工单、为账户查询验证邮箱,团队也能收到通知。每篇部署指南都说明了在哪里添加这些设置。
其他平台
任何能运行 Docker 镜像并提供持久磁盘的平台都可以运行 Desk:只运行一个实例,磁盘挂载到 /data,端口 3000,前面提供 HTTPS。还要告诉 Desk 访客地址从哪里来,限流才能正常工作:平台设置了专门的请求头时用 CLIENT_IP_HEADER,否则用 TRUST_PROXY_HOPS 指定会追加 X-Forwarded-For 的代理层数。
没有持久磁盘的平台不适合:Cloudflare Containers 会在容器休眠时重置磁盘,Vercel 等无服务器函数在请求之间不保留任何内容。
下一步
- GitHub 上的 Desk:源码、问题反馈和完整的 README
- 会话 API:读取、扩展并实时跟踪对话
- 客户端 SDK:在浏览器中实现流式输出和实时会话