文档

Desk

开源客服台:聊天窗口、帮助中心和团队后台,基于 ModelRiver 构建,运行在你自己的服务器上。

Desk 是一个可以自行运行的完整客服应用。访客在聊天窗口中提问;助手根据你的帮助中心作答并附上来源链接;当它无法回答,或访客要求人工服务时,对话会转交给你的团队,由团队在后台回复。一切都会实时更新。

它也是 ModelRiver 的参考应用:流式输出、对话记录、人工转接和实时更新都由 ModelRiver 提供,因此 Desk 本身非常精简。源码以 MIT 许可证发布在 github.com/modelriver/desk。

Desk 团队后台与访客聊天窗口显示同一条回复

包含哪些部分

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

聊天窗口中的机器人会展示它正在做什么:等待时为在线,访客输入时为倾听,撰写回答时为思考,回答完成后为已解决,把对话交给团队时为让位。

Desk 机器人的五种状态:在线、倾听、思考、已解决和让位


如何使用 ModelRiver

  1. 访客提问。Desk 在你的文章和私有笔记中搜索,挑选最相关的段落。
  2. 它把这些段落连同问题一起发送到 POST /v1/ai/async,并带上 stream: true 和访客的 session_id。浏览器通过 @modelriver/client 加入该请求的频道,边接收边显示回答。
  3. 对话保存在 ModelRiver 的会话中。Desk 不保存消息副本;它自己的 SQLite 文件只记录对话与会话的对应关系。
  4. 团队接管后,他们的回复和访客的后续消息通过 POST /v1/sessions/:id/messages 写入会话。AI 重新接手时会把这些内容视为对话的一部分。
  5. 聊天窗口和团队后台通过会话 API 的实时更新跟踪对话。Desk 自身的变化(接管、交还、归档)以信号的形式发出。

私有笔记会影响回答,但从不被引用、展示或链接。AI 无法访问 Desk 的数据库,也无法访问你连接的服务:由 Desk 的服务器决定查询什么,只把结果发送给模型。


快速开始

你需要 Node.js 26.12.1 或更高版本,以及一个 ModelRiver API 密钥。

Bash
git clone https://github.com/modelriver/desk.git
cd desk
nvm use
npm ci
cp .env.example .env # then set MODELRIVER_API_KEY
npm run create-admin
npm 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 中添加邮件服务、它的密钥和你的地址,然后重启:

Bash
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_URLhttps://api.modelriver.com/v1ModelRiver API 地址
MODELRIVER_WORKFLOWsupport_assistant负责回答问题的工作流(需开启会话)
DATA_DIR./examples/data你的文章和笔记(见下文)
DATABASE_PATH./var/support.db存放用户和对话映射的 SQLite 文件
APP_URL你的 Desk 地址,用于邮件中的链接和搜索引擎
EMAIL_PROVIDER推荐设置。resend、postmark、ses 或 log,用于工单、邮箱验证和团队通知(参见设置邮件)
EMAIL_FROM发件人,需使用邮件服务已验证的域名
SUPPORT_REPLY_TOEMAIL_FROM客户回复工单邮件时发往的地址:一个团队会查看的邮箱
ARCHIVE_RETENTION_HOURS24已归档对话在删除前保留的小时数(ModelRiver 中的记录也会删除)
WIDGET_FRAME_ANCESTORS允许嵌入聊天窗口的网站
GLOBAL_MESSAGES_PER_MINUTE、GLOBAL_MESSAGES_PER_DAY20、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 EncryptUbuntu 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 等无服务器函数在请求之间不保留任何内容。


下一步