@sochatlive/cs-widget
v0.1.3
Published
Official browser SDK for SoChat customer-service widget — single <script> CDN drop-in or npm import, zero runtime deps, Shadow DOM isolated, ~12KB gzipped.
Maintainers
Readme
@sochatlive/cs-widget
SoChat 客服域平台 — 网页客服 Widget 官方 SDK。两种接入方式同源同代码:
- A. CDN
<script>注入 — 一行脚本零代码上线,gzipped ~12 KB。 - B.
npm install— ESM / CJS 双产物 + 完整 TypeScript 类型,便于打包到 Vue / React / Next / Nuxt 等任意框架。
零代码接入承诺:方式 A 仅需一个
<script>标签 +data-app-key属性即可完成嵌入;进阶能力(手动init/ 命令式 API / 事件钩子)作为可选项保留。详见 §3 与 §4。
技术栈为 纯 TypeScript + Shadow DOM(不引入 Vue / React),运行时零依赖。
| 形态 | 文件 | 大小 | 说明 |
|---|---|---|---|
| IIFE(CDN)| dist/widget.js | 37 KB / 12 KB gzip | 全局名 SoChatCS,自启动 |
| ESM | dist/index.js | 53 KB | tree-shake 友好,命名导出 + default |
| CJS | dist/index.cjs | 53 KB | Node 18+ / Webpack / Rollup |
| 类型 | dist/index.d.ts / .d.cts | 6 KB | 完整 TypeScript 公开 API |
参见仓库根目录 docs/客服域平台方案.md §23 章节。
目录
cs-widget/
├── README.md
├── package.json
├── tsconfig.json
├── vite.config.ts
├── demo/
│ └── index.html # 本地可运行 demo(含 mock 模式)
└── src/
├── index.ts # 自举入口 + 公开 SoChatCS API
├── config.ts # 配置合并(init / data-* / query string)
├── types.ts # 公开 TS 类型
├── api.ts # REST 客户端
├── realtime.ts # WebSocket 接入 + 断线退避 + 轮询降级
├── visitorId.ts # 三层身份识别(§9)
├── store.ts # 极简 reactive store
├── i18n/
│ ├── index.ts
│ ├── zh-CN.ts
│ └── en-US.ts
├── ui/
│ ├── index.ts # mountUi
│ ├── FloatButton.ts # 悬浮按钮 + 未读角标
│ ├── ChatPanel.ts # 聊天面板(header / body / input)
│ ├── styles.ts # Shadow DOM 内的全部 CSS
│ └── theme.ts
└── utils/
├── singleton.ts # 幂等保护
├── shadowRoot.ts # Shadow DOM 容器
├── eventBus.ts # 内部事件总线
├── deviceId.ts # localStorage 持久化的 visitorId
├── debounce.ts
└── sanitize.ts # XSS 转义 + 链接识别1. 安装
pnpm add @sochatlive/cs-widget
# 或
npm install @sochatlive/cs-widget也可直接用 CDN(推荐零代码场景):
<script async src="https://chat.sochatlive.com/cs/widget.js"
data-app-key="sk_xxxxxxxxxxxxxxxxx"
data-api-base="https://chat.sochatlive.com"></script>
data-api-base是 widget 的"目标 API 域",指向 SoChat 后端网关。不写时 widget 会用打包内默认值https://chat.sochatlive.com;强烈建议显式写出来,便于排错和私有化部署一键切换(改这一行即可)。
CDN 等价镜像:https://unpkg.com/@sochatlive/cs-widget 与 https://cdn.jsdelivr.net/npm/@sochatlive/cs-widget(指向 dist/widget.js)。
2. 本地开发
pnpm install
pnpm --dir cs-widget dev
# 浏览器访问 http://localhost:5180/demo/ (默认开启 mock 模式)构建(同时产出 IIFE + ESM + CJS + dts):
pnpm --dir cs-widget build
# dist/index.{js,cjs,d.ts,d.cts} ← npm 库
# dist/widget.js ← CDN IIFE只构建其中一种:
pnpm --dir cs-widget build:lib # 仅 ESM/CJS + dts(tsup)
pnpm --dir cs-widget build:iife # 仅 widget.js(vite)类型检查:
pnpm --dir cs-widget typecheck发布(仓库内 dryrun + 发布到 npm):
pnpm --filter @sochatlive/cs-widget run release:dry
pnpm --filter @sochatlive/cs-widget run release3. 部署
构建产物 dist/widget.js 通过 nginx 暴露到 https://chat.sochatlive.com/cs/widget.js。
更新流程统一接入仓库根目录的 update-production.sh,CI/CD 与其他子工程一致。
# https://chat.sochatlive.com/cs/widget.js
location = /cs/widget.js {
alias /var/www/cs-widget/dist/widget.js;
add_header Access-Control-Allow-Origin "*" always;
add_header Cache-Control "public, max-age=300, must-revalidate" always;
types { application/javascript js; }
default_type application/javascript;
}
# 资源版本目录(带哈希指纹),可设长缓存
location /cs-static/ {
alias /var/www/cs-widget/dist/;
add_header Access-Control-Allow-Origin "*" always;
add_header Cache-Control "public, max-age=31536000, immutable" always;
}4. 接入方式
四种方式同源同包;可自由叠加(如 CDN 自启动 + npm 写法补
identify()同步登录态)。
A. 单 <script> 标签(推荐零代码)
把下面这段脚本加到第三方网站任意页面 </body> 之前即可:
<script async
src="https://chat.sochatlive.com/cs/widget.js"
data-app-key="sk_xxxxxxxxxxxxxxxxx"
data-api-base="https://chat.sochatlive.com"
data-locale="zh-CN"
data-theme="auto"
data-position="br"></script>如果用户已在第三方业务里登录,建议把业务用户 ID 一并传过去(用于 §9 三层识别第一层身份):
<script async
src="https://chat.sochatlive.com/cs/widget.js"
data-app-key="sk_xxxxxxxxxxxxxxxxx"
data-api-base="https://chat.sochatlive.com"
data-external-user-id="u_42"
data-nickname="李雷"
data-email="[email protected]"></script>B. Query String
适合没有权限改 data-* 属性、但能改 URL 的接入场景:
<script async src="https://chat.sochatlive.com/cs/widget.js?appKey=sk_xxx&apiBase=https%3A%2F%2Fchat.sochatlive.com&locale=en-US"></script>C. 命令式 API(手动初始化 + 事件钩子)
<script async src="https://chat.sochatlive.com/cs/widget.js" data-defer-init></script>
<script>
// 脚本异步加载期间的命令会被排队,加载完成后自动重放
window.SoChatCS = window.SoChatCS || function () { (window.SoChatCS.q = window.SoChatCS.q || []).push(arguments); };
SoChatCS('init', {
appKey: 'sk_xxxxxxxxxxxxxxxxx',
apiBase: 'https://chat.sochatlive.com', // 私有化部署改成自家 API 域
externalUserId: 'u_42',
nickname: '李雷',
extra: { orderNo: 'O-2026-001' },
onReady: () => console.log('widget ready'),
onMessage: (m) => console.log('msg', m),
});
</script>加载完成后也可直接调用:
SoChatCS.identify({ externalUserId: 'u_42', nickname: '李雷' });
SoChatCS.open({ prefill: '我想咨询一下订单状态' });
SoChatCS.sendMessage('你好');
SoChatCS.shutdown();D. npm 引入(ESM / CJS / TypeScript)
适合 Vue / React / Next / Nuxt 等已有打包流程的工程:
// ESM / TypeScript
import SoChatCS from '@sochatlive/cs-widget';
await SoChatCS.init({
appKey: 'sk_xxxxxxxxxxxxxxxxx',
apiBase: 'https://chat.sochatlive.com', // 私有化部署改成自家 API 域
externalUserId: currentUser?.id,
nickname: currentUser?.name,
onReady: () => console.log('widget ready'),
onMessage: (m) => console.log('msg', m),
});
SoChatCS.open({ prefill: '我想咨询一下订单状态' });也可使用命名导出(tree-shake 更友好):
import { init, identify, open, on, version } from '@sochatlive/cs-widget';
await init({
appKey: 'sk_xxxxxxxxxxxxxxxxx',
apiBase: 'https://chat.sochatlive.com',
});
on('message', (m) => console.log(m));
console.log('widget version', version);CommonJS:
const SoChatCS = require('@sochatlive/cs-widget').default;
// 或
const { init, open } = require('@sochatlive/cs-widget');在 SSR 框架中使用
cs-widget 是浏览器组件,访问 window / document / localStorage,在 SSR 环境(Next/Nuxt)请仅在客户端加载:
<!-- Nuxt 3 -->
<script setup lang="ts">
import { onMounted } from 'vue'
onMounted(async () => {
const { default: SoChatCS } = await import('@sochatlive/cs-widget')
await SoChatCS.init({
appKey: import.meta.env.VITE_SOCHAT_APP_KEY,
apiBase: import.meta.env.VITE_SOCHAT_API_BASE ?? 'https://chat.sochatlive.com',
})
})
</script>// Next.js (App Router)
'use client'
import { useEffect } from 'react'
export function CSWidget() {
useEffect(() => {
import('@sochatlive/cs-widget').then(({ default: SoChatCS }) =>
SoChatCS.init({
appKey: process.env.NEXT_PUBLIC_SOCHAT_APP_KEY!,
apiBase: process.env.NEXT_PUBLIC_SOCHAT_API_BASE ?? 'https://chat.sochatlive.com',
}),
)
}, [])
return null
}npm 模式下不会自动注册
window.SoChatCS、不会读取<script data-* />。所有配置都通过init({...})显式传入;这与 IIFE 自启动的 CDN 模式互不影响,可在同一项目中并存。
5. 配置项与优先级
合并优先级(高 → 低):
SoChatCS.init() 参数 > <script data-*> 属性 > <script> query string > 远端 site 配置
| 字段 | data-* 属性 | 说明 |
| ------------------- | -------------------------- | --------------------------------------------- |
| appKey | data-app-key | 必填,站点 appKey |
| apiBase | data-api-base | 默认 https://chat.sochatlive.com |
| wsBase | data-ws-base | 默认从 apiBase 推导 |
| externalUserId | data-external-user-id | 业务方已登录用户 ID |
| nickname | data-nickname | |
| avatar | data-avatar | |
| email | data-email | |
| phone | data-phone | |
| extra | data-extra | JSON 字符串,业务上下文 |
| locale | data-locale | zh-CN / en-US |
| theme | data-theme | light / dark / auto |
| position | data-position | br / bl / tr / tl |
| defaultOpen | data-default-open | true 时加载完成后自动展开 |
| zIndex | data-z-index | 默认 2147483000 |
| mock | data-mock | true 时启用本地 mock,便于 demo 与离线开发 |
| — | data-defer-init | 存在该属性时关闭自动初始化(手动 init()) |
6. 公开 JS API
| 方法 | 说明 |
| --------------------------------- | --------------------------------------------------- |
| SoChatCS.version | 当前 widget 版本 |
| SoChatCS.init(cfg) | 手动初始化(必须在 data-defer-init 模式下使用) |
| SoChatCS.identify(profile) | 同步身份信息(登录、登出、用户切换时调用) |
| SoChatCS.setLocale(locale) | 切换语言并持久化到 localStorage(见 §5.1) |
| SoChatCS.open({ prefill? }) | 展开聊天面板 |
| SoChatCS.close() | 收起聊天面板 |
| SoChatCS.toggle() | 切换面板 |
| SoChatCS.sendMessage(text) | 在面板内追加并发送一条游客消息 |
| SoChatCS.on(event, handler) | 订阅事件 |
| SoChatCS.off(event, handler) | 取消订阅 |
| SoChatCS.shutdown() | 销毁 widget、释放资源(一般用于 SPA 退出登录场景) |
事件类型:ready / open / close / message / error / identify / shutdown。
6.1 国际化(i18n)
核心策略:中文(
zh-*)显示中文,其他所有语言一律显示英文。当前支持的
LocaleType:zh-CN/en-US(与 SoChat 平台 app / admin 完全一致;同源参考server/shared/src/lang/config.js)。
优先级(从高到低)
| 级别 | 来源 | 设置方 | 说明 |
| ---- | -------------------------------------------- | ---------------- | ------------------------------------------------- |
| L1 | SoChatCS.init({ locale }) 或 setLocale() | 第三方 JS | 显式编程式设置 |
| L2 | <script data-locale="..."> | 第三方 HTML | 静态属性 |
| L3 | <script src="...?locale=..."> | 第三方 URL | Query string |
| L4 | localStorage['sochat_locale'] | 第三方网站 | 用户偏好持久化(与 app/admin 共用同一个 key) |
| L5 | 远端 site config | 客服管理员 | admin 后台站点配置默认 locale |
| L6 | navigator.languages | 浏览器 | zh-* → zh-CN,其他一律 → en-US |
| L7 | 兜底 | — | en-US |
L4 与 L5 的区别:
- L4 由"第三方网站"主动写入
localStorage,代表"用户在你网站上选择的语言偏好",不会被远端覆盖。- L5 由 SoChat 客服管理员在 admin 后台为站点设定的默认语言,仅当 widget 处于自动探测状态(L6)时才会覆盖。
关于 storage key:
sochat_locale是 SoChat 平台前端(app / admin / cs-widget)共用的 key。第三方网站嵌入 cs-widget 时,由于localStorage按域隔离,与你网站自身业务 key 不会冲突。
请求头:所有 widget 发起的 REST 请求都会自动携带
X-Locale: <当前语言>,与server/shared/src/lang/middleware/i18nMiddleware.js检测顺序对齐,便于后端按游客语言返回错误码翻译。
推荐用法:跟随你网站的语言切换器
如果你的网站本身有"中文 / English"切换按钮,建议在用户切换时同步通知 widget:
// 用户点击 "English"
SoChatCS.setLocale('en-US');
// setLocale 会自动写入 localStorage['sochat_locale'],
// 即使刷新页面 widget 仍记忆该选择。直接读写 localStorage(无需调用 widget API)
localStorage 是 widget 与第三方网站约定的契约层,你也可以完全不依赖 widget 的 JS API,仅通过写入约定 key 控制语言:
// 网站初始化语言时,直接写入即可,widget 加载后会读到这个值
localStorage.setItem('sochat_locale', 'en-US');合法值:'zh-CN' 或 'en-US';其他值(如 'zh' / 'zh-Hans' / 'zh-TW' / 'en-GB' 等)会按 LOCALE_ALIASES + startsWith('zh'|'en') 规则归一化。
完整示例
| 你的网站行为 | widget 显示语言 |
| ------------------------------------------------------------------------- | --------------- |
| 什么都不设置,浏览器是中文(zh-CN / zh-TW / zh-HK 等) | 中文 |
| 什么都不设置,浏览器是非中文(en / fr / ja / es 等) | 英文 |
| localStorage['sochat_locale'] = 'zh-CN',浏览器是英文 | 中文 |
| localStorage['sochat_locale'] = 'en-US',浏览器是中文 | 英文 |
| <script data-locale="zh-CN">,无视 localStorage | 中文(强制) |
| SoChatCS.init({ locale: 'en-US' }),无视 data-locale 与 localStorage | 英文(强制) |
7. SPA / 路由切换兼容
- Widget 通过 Shadow DOM 挂载在
document.body,且使用MutationObserver自愈:宿主页通过路由切换误删容器后,会在下一帧重新挂载。 - SPA 中用户登录态变化时调用
SoChatCS.identify()即可,无需 shutdown 后再 init,避免连接抖动与未读消息丢失。
8. CSP 兼容性
不使用
eval/new Function()/unsafe-inline。不依赖
<style>注入到宿主页<head>,所有样式都隔离在 Shadow DOM 内。第三方站点最小 CSP 建议:
Content-Security-Policy: script-src 'self' https://chat.sochatlive.com; connect-src 'self' https://chat.sochatlive.com wss://chat.sochatlive.com; img-src 'self' data: https://chat.sochatlive.com;
9. 访客身份识别(v0.1.1+)
游客在打开 widget 的瞬间就需要被识别成一个稳定的"访客",以便:跨刷新保留对话历史、跨设备/浏览器尽可能合并身份、登录后能与会员账户绑定。
widget 内置三层识别策略,按优先级尝试匹配同一访客;任一层命中即视为同一人:
| 层级 | 字段 | 来源 | 稳定性 | 用途 |
| ---- | ----------------- | --------------------------------- | ------- | ------------------------------------------------- |
| L1 | externalUserId | 业务方传入(已登录用户 ID) | 最高 | 与你网站的会员体系直接对齐 |
| L2 | fingerprint | 浏览器低熵特征 hash(widget 算) | 较高 | 用户清缓存 / 切隐身后仍能 ~80% 命中同一访客 |
| L3 | visitorId | localStorage 持久化 UUID | 一般 | 同浏览器内最稳定;清缓存后会变 |
后端识别时按 L1 → L2 → L3 顺序匹配,匹配到的访客记录会增量补全其它字段(例如未登录访客先用 fingerprint+visitorId 创建,登录后传入 externalUserId 即自动合并到该记录上,不会新建一个游客)。
§9.2 数据合规:fingerprint 仅在客户端生成 hash 后上传,不上传任何裸的指纹原料;用户使用浏览器隐身模式或关闭 canvas readback 即可天然规避。游客身份不会进入主用户表(
users),仅写入独立的访客表(cs_visitors)。
启用 fingerprint 不需要任何额外配置,0.1.1+ 自动生效。如果你已经在用 externalUserId,fingerprint 仅作为兜底命中层,不影响你现有接入。
10. 排错速查
| 现象 | 排查方向 |
| --------------------------------------- | ------------------------------------------------------------------- |
| 页面没有出现悬浮按钮 | 检查 data-app-key 是否填写;控制台是否报 [SoChatCS] no appKey |
| 出现按钮但点不开 | 检查站点 CSP 是否拦截 connect-src chat.sochatlive.com |
| 样式被宿主页覆盖 | 不应该发生(Shadow DOM 已隔离);如出现,提交 issue |
| 路由切换后按钮消失 | 升级到 ≥ 0.1.x;MutationObserver 已修复 |
| WebSocket failed | 浏览器/代理拒绝 wss;插件会自动降级到 8s 短轮询,对用户透明 |
| 用户切换后仍是旧昵称 | 调用 SoChatCS.identify({ externalUserId, nickname }) |
| handshake 返回 available=false | 平台 feature.customer_service.enabled=false,需在 admin 后台开启 |
| Next/Nuxt SSR 报 window is not defined | npm 模式必须放在 onMounted / useEffect / 'use client' 中加载 |
| TypeScript 类型未提示 | 确认 tsconfig.compilerOptions.moduleResolution 为 bundler/node16/nodenext |
| import 体积过大 | 改用命名导出 + tree-shaking;或直接走 CDN <script>(仅 12 KB gzip) |
10. 与 botsdk 对比
@sochatlive/cs-widget 与 @sochatlive/bot-sdk 同源同发布形态,互为补充:
| | @sochatlive/cs-widget | @sochatlive/bot-sdk |
|---|---|---|
| 运行环境 | 浏览器 | Node.js ≥ 18 |
| 主要场景 | 第三方网站嵌入客服面板 | 服务端机器人 / Webhook 处理 |
| 接入方式 | <script> CDN + npm 双产物 | npm 库 |
| 体积 | gzip 12 KB | gzip ~5 KB |
| 鉴权 | 站点 appKey(公开)+ Visitor JWT | Bot Token (sbot_*) + Webhook secret |
同一 npm scope
@sochatlive/*,发布脚本一致:prepublishOnly钩子保证发版前自动 build。
License
MIT © SoChat
