libredesk-sdk-js
v0.2.2
Published
Libredesk 客户侧 SDK
Downloads
91
Readme
libredesk-sdk-js
libredesk-sdk-js 是 Libredesk LiveChat 的客户侧 TypeScript SDK。它封装了客户 Widget 所需的 HTTP API 和 WebSocket:读取公开配置、匿名访客建会话、已验证客户登录、会话与消息管理、附件、CSAT 评价、输入状态、页面访问上报和实时消息接收。
SDK 只面向网站访客/客户的 LiveChat 收件箱,不包含管理员、坐席或其他服务端管理 API。
目录
运行要求
- Bun 或支持标准
fetch、FormData、Blob的现代浏览器运行时。 - 实时功能还需要运行时提供标准
WebSocket。在无全局WebSocket的非浏览器运行时,可通过webSocketFactory注入适配实现。 - Libredesk 服务端需要启用一个 LiveChat 收件箱。
安装
# Bun
bun add libredesk-sdk-js
# PNPM
pnpm add libredesk-sdk-js
# NPM
npm install libredesk-sdk-jsimport { ApiError, LiveChatClient } from 'libredesk-sdk-js';
import type { ChatMessage, LiveChatSettings } from 'libredesk-sdk-js';SDK 使用 ESM 和具名导出。浏览器使用时应由应用构建工具打包;不要把收件箱 secret、JWT 签名密钥或后台管理员凭据放入前端产物。
服务端准备
在创建客户端前,确认 Libredesk 中的 LiveChat 收件箱已启用,并准备以下两个公开信息:
| 配置 | 来源与用途 |
| --- | --- |
| baseURL | Libredesk 服务根地址,例如 https://support.example.com;不包含 /api 或 /widget/ws。 |
| inboxID | LiveChat 收件箱的 UUID。SDK 会将它写入 X-Libredesk-Inbox-ID。 |
SDK 调用的是服务端公开的 /api/v1/client/chat/* 路由;服务端会校验收件箱、启用状态和 IP 限制。Client API 已启用跨域访问;受信任域名仅用于服务端托管 Widget 页面的嵌入限制。
已验证客户还需要由宿主业务后端生成 JWT。服务端使用该收件箱的 secret 校验 JWT,前端只能接收已签发的 JWT,绝不能获得 secret。按照官方 LiveChat 配置文档,JWT 使用 HS256 签名,完整 payload 如下:
{
"external_user_id": "crm-user-42",
"email": "[email protected]",
"first_name": "张三",
"last_name": "",
"phone_number": "15911111111",
"phone_number_country_code": "CN",
"exp": 1780000000,
"contact_custom_attributes": {
"plan": "premium",
"company": "EEDATEK"
}
}| 字段名称 | 是否一定需要 | 字段描述 |
| --- | --- | --- |
| external_user_id | 是 | 宿主系统中稳定且唯一的用户 ID。Libredesk 使用它识别和匹配回访用户。 |
| email | 是 | 用户的邮箱地址。 |
| first_name | 是 | 用户名字。 |
| last_name | 否 | 用户姓氏。 |
| phone_number | 否 | 用户电话号码。 |
| phone_number_country_code | 否 | 电话号码所属国家或地区的 ISO 3166-1 alpha-2 代码,例如 CN、US。 |
| exp | 是 | JWT 的过期时间,使用秒级 Unix 时间戳。建议由后端签发短期 token。 |
| contact_custom_attributes | 否 | 对象形式的联系人自定义属性;身份验证成功时写入联系人记录。 |
external_user_id、email、first_name 和 exp 必填。JWT 必须使用该收件箱 secret 以 HS256 签名;secret 只能保存在宿主业务后端。
服务端签发 JWT
Node.js
const jwt = require('jsonwebtoken');
const token = jwt.sign({
external_user_id: 'your_app_user_123',
email: '[email protected]',
first_name: 'John',
exp: Math.floor(Date.now() / 1000) + 3600,
}, SECRET, { algorithm: 'HS256' });Go
import (
"time"
"github.com/golang-jwt/jwt/v5"
)
claims := jwt.MapClaims{
"external_user_id": "your_app_user_123",
"email": "[email protected]",
"first_name": "John",
"exp": time.Now().Add(time.Hour).Unix(),
}
token, _ := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).
SignedString([]byte(SECRET))快速开始
下例实现了一个匿名访客的最小闭环:读取配置、创建会话、保存 token、连接实时通道、接收坐席消息、发送后续消息,并在页面卸载时断开连接。
import { ApiError, LiveChatClient } from 'libredesk-sdk-js';
const inboxID = '4d2e63c4-91c1-46e7-b502-725dca2f1b2b';
const client = new LiveChatClient({
baseURL: 'https://support.example.com',
inboxID,
sessionToken: localStorage.getItem('libredesk_session_token'),
visitorToken: localStorage.getItem('libredesk_visitor_token'),
onSessionChange(token) {
if (token) {
localStorage.setItem('libredesk_session_token', token);
} else {
localStorage.removeItem('libredesk_session_token');
}
},
onVisitorTokenChange(token) {
if (token) {
localStorage.setItem('libredesk_visitor_token', token);
} else {
localStorage.removeItem('libredesk_visitor_token');
}
}
});
const settings = await client.getWidgetSettings();
console.log(settings.brand_name);
const created = await client.initConversation({
message: '你好,我想了解产品价格。',
formData: {
name: '张三',
email: '[email protected]',
phone_number: '15911111111'
}
});
const conversationUUID = created.conversation.uuid;
const stopNewMessage = client.on('new_message', (message) => {
if (message.conversation_uuid === conversationUUID) {
renderMessage(message);
}
});
client.connectWebSocket();
await client.sendMessage(conversationUUID, { message: '请问是否有企业版?' });
window.addEventListener('pagehide', () => {
stopNewMessage();
client.disconnectWebSocket();
});
function renderMessage(message: { author: { first_name: string }; text_content: string }) {
console.log(`${message.author.first_name}: ${message.text_content}`);
}initConversation() 在无 session 时创建匿名访客并返回 session_token。SDK 会同时更新 sessionToken 与 visitorToken,并调用两个持久化回调;之后的 REST 与 WebSocket 请求会自动携带会话信息。
客户端配置与会话持久化
const client = new LiveChatClient({
baseURL: 'https://support.example.com/',
inboxID: '4d2e63c4-91c1-46e7-b502-725dca2f1b2b',
sessionToken: null,
visitorToken: null,
timeout: 10_000,
onSessionChange: (token, user) => {
// token 为 null 表示会话已被清除;user 可能为 null。
},
onVisitorTokenChange: (token) => {
// token 为 null 表示访客 token 已失效或已被服务端合并。
},
webSocketFactory: undefined
});| 选项 | 必填 | 说明 |
| --- | --- | --- |
| baseURL | 是 | Libredesk 服务根地址。末尾 / 会被自动移除。HTTP 使用此地址,WebSocket 自动转换为 ws(s)://.../widget/ws。 |
| inboxID | 是 | LiveChat 收件箱 UUID。 |
| sessionToken | 否 | 已持久化的 session token。 |
| visitorToken | 否 | 匿名会话 token;客户完成身份验证后用于将匿名记录合并到联系人。 |
| timeout | 否 | REST 超时毫秒数,默认 10000。上传附件固定使用 30000。 |
| onSessionChange | 否 | SDK 更新或清空 session token 时调用,适合持久化 token 和用户摘要。 |
| onVisitorTokenChange | 否 | SDK 更新或清空 visitor token 时调用。 |
| webSocketFactory | 否 | 自定义 WebSocket 创建函数,供测试或非浏览器运行时使用。 |
可通过只读属性读取内存状态:client.sessionToken、client.visitorToken、client.sessionUser。也可在恢复状态或登出时手动设置:
client.setSessionToken(savedSessionToken, savedUser);
client.setVisitorToken(savedVisitorToken);
// 应用自己的登出流程中清除本地与 SDK 状态。
client.setSessionToken(null);
client.setVisitorToken(null);
client.disconnectWebSocket();初始构造参数不会触发持久化回调。setSessionToken() 会触发 onSessionChange,setVisitorToken() 会触发 onVisitorTokenChange。
身份与会话
匿名访客
匿名访客无需先认证,直接调用 initConversation()。服务端会创建访客、会话和第一条消息;返回的 token 必须被持久化,否则刷新后无法读取之前的会话。
const result = await client.initConversation({
message: '我需要帮助。',
formData: {
name: '王五',
email: '[email protected]',
phone_number: '15922222222'
}
});
console.log(result.conversation.uuid);
console.log(result.messages);formData 的 key 应匹配收件箱配置的预聊天表单字段;其中的联系人属性和会话属性由服务端根据配置保存。message 必填,最大长度为 10000 个字符。服务端的会话策略可能阻止重复创建会话或向已关闭会话回复,此时会以 ApiError 返回。
已验证客户
宿主应用用户登录后,请向自己的后端请求短期 JWT,再以它交换 Libredesk Widget session:
const jwt = await fetch('/api/customer-service/jwt').then((response) => response.text());
const session = await client.exchangeJWTForSession(jwt);
console.log(session.user.user_id, session.user.is_visitor); // is_visitor 为 false
console.log(client.sessionToken); // 已更新为已验证客户 tokenexchangeJWTForSession() 会自动更新 sessionToken 和 sessionUser,并触发 onSessionChange。若当前用户先以匿名访客身份发起过会话,SDK 会保留原有 visitorToken;后续任意需要认证的 Client 请求会自动携带 X-Libredesk-Visitor-Token,服务端完成访客记录合并后会返回清理标记,SDK 随即清空并回调 onVisitorTokenChange(null)。
身份从访客切换为已验证客户后,建议重新建立 WebSocket,使实时连接也使用新 session:
client.disconnectWebSocket();
await client.exchangeJWTForSession(jwt);
client.connectWebSocket();恢复当前用户
已持久化 session 后可通过 getAuthMe() 验证 token 并恢复用户元数据:
try {
const user = await client.getAuthMe();
console.log(user.first_name, user.is_visitor);
} catch (error) {
// 401 时 SDK 会自动清空当前 session;按错误处理章节清理界面状态。
}HTTP 用例
SDK 将服务端标准 envelope 响应解包,所有方法直接返回 data 字段,而非 { status, data, message } 外层对象。除 CSAT 外,Widget 请求会自动添加收件箱相关的请求头;有 sessionToken 时自动添加 Authorization: Bearer <token>。
读取公开配置
轻量 launcher 配置适合页面首次渲染聊天入口:
const launcher = await client.getLauncherSettings();
applyLauncher({
position: launcher.launcher.position,
color: launcher.colors.primary,
logoURL: launcher.launcher.logo_url
});完整 Widget 配置用于构建聊天界面:
const settings = await client.getWidgetSettings();
if (settings.features.file_upload) {
enableAttachmentButton();
}
if (settings.prechat_form.enabled) {
renderPrechatForm(settings.prechat_form.fields);
}LiveChatSettings 包含品牌、语言、颜色、首页背景、功能开关、预聊天表单、会话策略、办公时间、公告/外部链接卡片等公开配置。请以服务端返回值为准,而不是在前端硬编码开关。
创建会话和发送首条消息
const { conversation, messages, session_token, user } = await client.initConversation({
message: '我的订单还没有发货。',
formData: {
email: '[email protected]',
order_number: 'ORD-1001'
}
});
renderConversation(conversation, messages);
if (session_token && user) {
console.log(`已创建访客 ${user.user_id}`);
}已有有效 session 时,此方法仍会创建新会话,但响应不会新增 session_token。是否允许创建多个会话取决于服务端收件箱的客户/访客策略。
会话列表、详情与已读状态
const conversations = await client.listConversations();
for (const conversation of conversations) {
console.log(conversation.uuid, conversation.status, conversation.unread_message_count);
}
const detail = await client.getConversation(conversations[0].uuid);
renderConversation(detail.conversation, detail.messages);
await client.updateConversationLastSeen(detail.conversation.uuid);getConversation() 和 initConversation() 均返回 { conversation, messages, business_hours_id?, working_hours_utc_offset? }。会话摘要包含状态、最后一条消息、未读数和处理人;消息包含 author、text_content、attachments、status 等字段。
在以下时机调用 updateConversationLastSeen():用户进入某个会话、收到并展示坐席消息、或将聊天窗口重新置于前台。该操作只更新服务端已读时间,不返回消息列表。
在已有会话中发送文本
const message = await client.sendMessage(conversationUUID, {
message: '我补充一下,订单号是 ORD-1001。'
});
appendMessage(message);请求中的 message 为必填文本。成功后返回服务端已保存的 ChatMessage;不要仅依赖 WebSocket 回显来更新自己的已发送消息。
上传附件
uploadFile() 会以 multipart/form-data 上传一个文件并创建附件消息。浏览器中可直接传递 File,因为 File 是 Blob:
const input = document.querySelector<HTMLInputElement>('#attachment')!;
const file = input.files?.[0];
if (file) {
const message = await client.uploadFile(conversationUUID, file);
appendMessage(message);
}SDK 自动提交 conversation_uuid 和一个名为 files 的文件字段,并将超时提高为 30 秒。是否可上传、允许的 MIME 类型和大小限制由服务端收件箱配置决定;界面应同时检查 settings.features.file_upload 并处理服务器拒绝。
提交 CSAT 评价
当消息元数据提供 CSAT UUID 时,可提交评分与文字反馈:
const accepted = await client.submitCSATResponse('csat-uuid', {
rating: 5,
feedback: '响应很及时。'
});
if (accepted) {
showThankYou();
}rating 可为 0 至 5;0 表示只提交文字反馈。评分和反馈都可省略,但应至少传递用户实际填写的内容。CSAT 是公开链接式接口,SDK 不会附加收件箱或 session 相关的请求头。
HTTP 方法总览
| 方法 | 服务端路由 | session | 返回值 |
| --- | --- | --- | --- |
| getLauncherSettings() | GET /api/v1/client/chat/settings/launcher | 不需要 | LauncherSettings |
| getWidgetSettings() | GET /api/v1/client/chat/settings | 不需要 | LiveChatSettings |
| exchangeJWTForSession(jwt) | POST /api/v1/client/chat/auth/exchange | 不需要 | SessionResponse |
| getAuthMe() | GET /api/v1/client/chat/auth/me | 需要 | SessionUser |
| initConversation(request) | POST /api/v1/client/chat/conversations/init | 可选 | InitConversationResponse |
| listConversations() | GET /api/v1/client/chat/conversations | 需要 | ChatConversation[] |
| getConversation(uuid) | GET /api/v1/client/chat/conversations/{uuid} | 需要 | ConversationResponse |
| sendMessage(uuid, request) | POST /api/v1/client/chat/conversations/{uuid}/message | 需要 | ChatMessage |
| uploadFile(conversationUUID, file) | POST /api/v1/client/media/upload | 需要 | ChatMessage |
| updateConversationLastSeen(uuid) | POST /api/v1/client/chat/conversations/{uuid}/update-last-seen | 需要 | boolean |
| submitCSATResponse(csatUuid, request) | POST /api/v1/csat/{uuid}/response | 不需要 | boolean |
WebSocket 实时通信
连接、关闭与自动行为
先获取有效 sessionToken,再连接:
client.connectWebSocket();
// 组件卸载、用户主动关闭聊天或应用登出时调用。
client.disconnectWebSocket(1000, 'chat closed');connectWebSocket() 会连接到 ${baseURL}/widget/ws,并在打开后自动发送 join(包含 session token 与收件箱 UUID)。没有 session token 时会同步抛出错误。重复调用不会创建重复的已打开连接。
SDK 每 10 秒自动发送一次 ping,若连续 30 秒未收到 pong 会关闭连接并重连。非手动关闭时,重连延迟按 1s、2s、4s、8s、16s、20s 递增,并在 20 秒封顶;重连次数没有固定上限。disconnectWebSocket() 会停止心跳和自动重连。
独立使用 WebSocket 客户端
通常应通过 LiveChatClient 使用实时功能,使 HTTP session 与 WebSocket 共享同一份状态。只有当应用自行管理 session 或只需要实时协议时,才直接创建 LiveChatWebSocketClient:
import { LiveChatWebSocketClient } from 'libredesk-sdk-js';
let sessionToken: string | null = readSavedSessionToken();
const websocket = new LiveChatWebSocketClient({
baseURL: 'https://support.example.com',
inboxID: '4d2e63c4-91c1-46e7-b502-725dca2f1b2b',
getSessionToken: () => sessionToken
});
websocket.on('new_message', (message) => appendMessage(message));
websocket.connect();
// 登录、登出或 token 刷新后,更新宿主状态并用新 token 重建连接。
sessionToken = newSessionToken;
websocket.disconnect();
websocket.connect();该类提供 socket 只读属性、connect()、disconnect()、sendTyping()、sendPageVisit()、sendPing()、on() 与 off()。事件、心跳和重连行为与 LiveChatClient 的代理方法完全一致;直接使用时,调用方负责保证 getSessionToken() 始终返回当前有效 token。
监听事件
on() 返回取消监听函数;也可以使用 off() 移除原始处理函数。建议在界面销毁时取消所有监听。
const unsubscribe = client.on('new_message', (message) => {
appendMessage(message);
client.updateConversationLastSeen(message.conversation_uuid).catch(reportError);
});
client.on('typing', ({ conversation_uuid, is_typing }) => {
updateAgentTyping(conversation_uuid, is_typing);
});
client.on('conversation_update', (patch) => {
updateConversation(patch);
});
client.on('error', (event) => {
if (event.kind === 'server') {
console.error(event.data.message);
} else {
console.error('WebSocket 运行时错误', event.event);
}
});
// 取消监听 new_message 事件
unsubscribe();| 事件 | 载荷 | 时机 |
| --- | --- | --- |
| open | 原始 open 事件 | socket 已打开,随后 SDK 自动发送 join。 |
| joined | { message?: string } | 服务端确认已加入 inbox。 |
| new_message | ChatMessage | 收到新消息。 |
| typing | WidgetTypingData | 坐席输入状态变更,包含 conversation_uuid 与 is_typing。 |
| conversation_update | Partial<ChatConversation> | 当前会话的状态、处理人等局部字段更新。 |
| pong | undefined | 收到心跳响应。 |
| raw_message | WidgetMessage | 每条可解析的服务端原始协议消息。 |
| error | LiveChatErrorEvent | socket 异常,或服务端发送 error 协议消息。 |
| close | 原始 close 事件 | socket 关闭;非手动关闭时会排程重连。 |
| reconnecting | { attempt: number; delayMs: number } | 即将按指数退避重连。 |
| reconnected | { attempt: number } | 重连 socket 打开后。 |
上报输入状态、页面访问和心跳
const conversationUUID = 'conversation-uuid';
messageInput.addEventListener('input', () => {
client.sendTyping(conversationUUID, true);
});
messageInput.addEventListener('blur', () => {
client.sendTyping(conversationUUID, false);
});
client.sendPageVisit(location.href, document.title);
// SDK 内部已维护心跳;只有在有明确业务需要时才手动调用。
client.sendPing();这些方法仅在 WebSocket 已打开时发送,未连接时会静默忽略。服务端会限制高频 typing、page_visit 和 ping 帧;应按状态变化或页面路由变化触发,不能在每次渲染时无节制调用。页面访问 URL 必须是 http 或 https 地址。
错误处理
服务端返回非 2xx 状态时,SDK 抛出 ApiError。其中包含 HTTP 状态、服务端错误类型、错误数据和原始 Response:
import { ApiError, ERROR_TYPE, LiveChatClient } from 'libredesk-sdk-js';
try {
await client.sendMessage(conversationUUID, { message: text });
} catch (error) {
if (error instanceof ApiError) {
console.error(error.status, error.errorType, error.message, error.data);
if (error.errorType === ERROR_TYPE.UNAUTHORIZED || error.status === 401) {
// SDK 已清空失效的 sessionToken;同步清理当前聊天界面即可。
showSignInOrNewConversation();
} else if (error.errorType === ERROR_TYPE.RATE_LIMIT) {
showRetryLater();
} else if (error.errorType === ERROR_TYPE.INPUT) {
showValidationMessage(error.message);
}
} else {
// 网络失败、超时、JSON 解析之外的运行时异常。
reportError(error);
}
}当某个使用当前 token 的请求收到 401,SDK 会自动执行 setSessionToken(null) 并触发 onSessionChange(null, null)。若响应带有 X-Libredesk-Clear-Visitor,SDK 也会清空访客 token。应用仍应自行清理会话列表、当前会话、界面缓存和本地用户状态。
可使用的标准错误常量为:
import { ERROR_TYPE } from 'libredesk-sdk-js';
ERROR_TYPE.GENERAL;
ERROR_TYPE.PERMISSION;
ERROR_TYPE.INPUT;
ERROR_TYPE.DATA;
ERROR_TYPE.NETWORK;
ERROR_TYPE.NOT_FOUND;
ERROR_TYPE.CONFLICT;
ERROR_TYPE.UNAUTHORIZED;
ERROR_TYPE.RATE_LIMIT;默认 REST 超时为 10 秒,附件上传为 30 秒。对用户可重试的网络错误,请由宿主应用提供重试按钮,并避免对创建会话、发送消息等非幂等操作进行无提示自动重放。
数据模型与常量
常用类型可直接导入;公开导出还包括所有配置、会话、消息、办公时间、附件和 WebSocket 协议类型。
import type {
Attachment,
ChatConversation,
ChatMessage,
ConversationResponse,
InitConversationRequest,
LiveChatClientOptions,
LiveChatSettings,
SessionResponse,
SessionUser
} from 'libredesk-sdk-js';
import {
ATTACHMENT_DISPOSITION,
BUSINESS_HOURS_DAY,
CHAT_USER_TYPE,
MESSAGE_STATUS,
USER_AVAILABILITY_STATUS,
WIDGET_MESSAGE_TYPE
} from 'libredesk-sdk-js';| 常量 | 示例值 | 用途 |
| --- | --- | --- |
| MESSAGE_STATUS | pending、sent、failed、received | 判断消息持久化状态。 |
| CHAT_USER_TYPE | agent、contact、visitor | 判断消息作者或处理人类型。 |
| USER_AVAILABILITY_STATUS | online、away、offline | 展示坐席在线状态。 |
| ATTACHMENT_DISPOSITION | inline、attachment | 展示或下载附件。 |
| BUSINESS_HOURS_DAY | Monday 至 Sunday | 读取服务端营业时间表的 key。 |
| WIDGET_MESSAGE_TYPE | join、typing、ping、new_message 等 | 自定义协议适配或使用 raw_message 时使用。 |
时间字段均以服务端字符串返回,应由宿主应用按用户语言和时区格式化。会话状态为服务端字符串,UI 不应假设只有固定的少数枚举值。
安全与集成建议
- 将
sessionToken和visitorToken视为用户会话凭据;使用 HTTPS,在可信的浏览器存储策略下保存,并在登出时清除。 - JWT 仅由宿主后端生成,使用收件箱 secret 签名并设置短有效期;不要让浏览器参与签名。
- 使用
getWidgetSettings()的features、预聊天表单和会话策略驱动界面,避免绕过服务端限制。 - WebSocket 重连后应调用
listConversations(),并在当前会话打开时调用getConversation(),以补齐断线期间可能遗漏的消息。 new_message是实时增量,getConversation()是权威快照;对同一message.uuid做去重,避免重连同步时重复渲染。- 在组件卸载、聊天窗口关闭或身份切换时调用
disconnectWebSocket()并取消on()返回的监听函数,防止重复连接与内存泄漏。 - 不要把
baseURL误设为管理后台页面地址或 API 前缀。应传入 Libredesk 服务根地址,例如https://support.example.com。
