npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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.

Readme

@sochatlive/cs-widget

npm version npm downloads bundle size license

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-widgethttps://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 release

3. 部署

构建产物 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-*)显示中文,其他所有语言一律显示英文。

当前支持的 LocaleTypezh-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 keysochat_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.moduleResolutionbundler/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