@starim-io/cs-widget
v0.2.2
Published
Official browser SDK for StarIM customer-service widget — single <script> CDN drop-in or npm import, zero runtime deps, Shadow DOM isolated, ~12KB gzipped. v0.2 adds full appearance customization (primaryColor / panelSize / buttonShape / headerTitle / bac
Maintainers
Readme
@starim-io/cs-widget
StarIM 客服平台 — 网页客服 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 | 全局名 StarIMCS,自启动 |
| 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 |
目录
cs-widget/
├── README.md
├── package.json
├── tsconfig.json
├── vite.config.ts
├── demo/
│ └── index.html # 本地可运行 demo(含 mock 模式)
└── src/
├── index.ts # 自举入口 + 公开 StarIMCS API
├── config.ts # 配置合并(init / data-* / query string)
├── types.ts # 公开 TS 类型
├── api.ts # REST 客户端
├── realtime.ts # WebSocket 接入 + 断线退避 + 轮询降级
├── visitorId.ts # 三层身份识别
├── 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 @starim-io/cs-widget
# 或
npm install @starim-io/cs-widget也可直接用 CDN(推荐零代码场景):
<script async src="https://cs.starim.io/widget.js"
data-app-key="sk_xxxxxxxxxxxxxxxxx"
data-api-base="https://api.starim.io"></script>
data-api-base是 widget 的「目标 API 域」,指向 StarIM 后端网关。不写时 widget 会用打包内默认值https://api.starim.io;强烈建议显式写出来,便于排错和私有化部署一键切换(改这一行即可)。
CDN 等价镜像:https://unpkg.com/@starim-io/cs-widget 与 https://cdn.jsdelivr.net/npm/@starim-io/cs-widget(指向 dist/widget.js)。
2. 本地开发
pnpm install
pnpm --dir cs-widget dev
# 浏览器访问 http://localhost:5250/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 @starim-io/cs-widget run release:dry
pnpm --filter @starim-io/cs-widget run release3. 部署
构建产物 dist/widget.js 通过 nginx 暴露到 https://cs.starim.io/widget.js。
更新流程统一接入仓库根目录的 update-production.sh,CI/CD 与其他子工程一致。
# https://cs.starim.io/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 / B / C / D)与「链接跳转 E」并列存在,admin 后台对站点单独开关:嵌入式靠
originAllowList校验来源,链接跳转靠CsSite.linkEntry+ 可选 HMAC 签名校验。
A. 单 <script> 标签(推荐零代码)
把下面这段脚本加到第三方网站任意页面 </body> 之前即可:
<script async
src="https://cs.starim.io/widget.js"
data-app-key="sk_xxxxxxxxxxxxxxxxx"
data-api-base="https://api.starim.io"
data-locale="zh-CN"
data-theme="auto"
data-position="br"></script>如果用户已在第三方业务里登录,建议把业务用户 ID 一并传过去(用于三层识别第一层身份):
<script async
src="https://cs.starim.io/widget.js"
data-app-key="sk_xxxxxxxxxxxxxxxxx"
data-api-base="https://api.starim.io"
data-external-user-id="u_42"
data-nickname="李雷"
data-email="[email protected]"></script>B. Query String
适合没有权限改 data-* 属性、但能改 URL 的接入场景:
<script async src="https://cs.starim.io/widget.js?appKey=sk_xxx&apiBase=https%3A%2F%2Fapi.starim.io&locale=en-US"></script>C. 命令式 API(手动初始化 + 事件钩子)
<script async src="https://cs.starim.io/widget.js" data-defer-init></script>
<script>
// 脚本异步加载期间的命令会被排队,加载完成后自动重放
window.StarIMCS = window.StarIMCS || function () { (window.StarIMCS.q = window.StarIMCS.q || []).push(arguments); };
StarIMCS('init', {
appKey: 'sk_xxxxxxxxxxxxxxxxx',
apiBase: 'https://api.starim.io', // 私有化部署改成自家 API 域
externalUserId: 'u_42',
nickname: '李雷',
extra: { orderNo: 'O-2026-001' },
onReady: () => console.log('widget ready'),
onMessage: (m) => console.log('msg', m),
});
</script>加载完成后也可直接调用:
StarIMCS.identify({ externalUserId: 'u_42', nickname: '李雷' });
StarIMCS.open({ prefill: '我想咨询一下订单状态' });
StarIMCS.sendMessage('你好');
StarIMCS.shutdown();D. npm 引入(ESM / CJS / TypeScript)
适合 Vue / React / Next / Nuxt 等已有打包流程的工程:
// ESM / TypeScript
import StarIMCS from '@starim-io/cs-widget';
await StarIMCS.init({
appKey: 'sk_xxxxxxxxxxxxxxxxx',
apiBase: 'https://api.starim.io', // 私有化部署改成自家 API 域
externalUserId: currentUser?.id,
nickname: currentUser?.name,
onReady: () => console.log('widget ready'),
onMessage: (m) => console.log('msg', m),
});
StarIMCS.open({ prefill: '我想咨询一下订单状态' });也可使用命名导出(tree-shake 更友好):
import { init, identify, open, on, version } from '@starim-io/cs-widget';
await init({
appKey: 'sk_xxxxxxxxxxxxxxxxx',
apiBase: 'https://api.starim.io',
});
on('message', (m) => console.log(m));
console.log('widget version', version);CommonJS:
const StarIMCS = require('@starim-io/cs-widget').default;
// 或
const { init, open } = require('@starim-io/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: StarIMCS } = await import('@starim-io/cs-widget')
await StarIMCS.init({
appKey: import.meta.env.VITE_STARIM_APP_KEY,
apiBase: import.meta.env.VITE_STARIM_API_BASE ?? 'https://api.starim.io',
})
})
</script>// Next.js (App Router)
'use client'
import { useEffect } from 'react'
export function CSWidget() {
useEffect(() => {
import('@starim-io/cs-widget').then(({ default: StarIMCS }) =>
StarIMCS.init({
appKey: process.env.NEXT_PUBLIC_STARIM_APP_KEY!,
apiBase: process.env.NEXT_PUBLIC_STARIM_API_BASE ?? 'https://api.starim.io',
}),
)
}, [])
return null
}npm 模式下不会自动注册
window.StarIMCS、不会读取<script data-* />。所有配置都通过init({...})显式传入;这与 IIFE 自启动的 CDN 模式互不影响,可在同一项目中并存。
E. 链接跳转(standalone 落地页)
适合下列场景:第三方网站不方便注入 <script>、需要在客服外渠道(短信 / 邮件 / IM 消息 / 二维码 / 客服名片)一键发起会话。访客点开链接即进入独立的客服窗口,对方域名不需要白名单授权。
部署形态
- 后端:每个
CsSite都有linkEntry子配置(admin 后台「站点 → 链接入口」)。 - 前端:
dist/standalone.html与dist/widget.js同目录托管到https://chat.<your-im-domain>/c/,访问https://chat.<your-im-domain>/c/?k=sk_xxx&u=u_42即可自动启动。 - 落地页是 standalone 模式的 widget:没有悬浮按钮,ChatPanel 占满整个 viewport,与 iOS/Android 浏览器全屏体验一致。
URL 参数
| 短键 | 长键 | 必填 | 说明 |
|---|---|---|---|
| k | appKey | ✓ | 站点 appKey(admin 后台获取)|
| u | externalUserId | – | 三层身份识别 L1:业务方登录态用户 ID |
| nick | nickname | – | 显示昵称 |
| t | – | 见下 | 链接签名时间戳(秒级 epoch)|
| s | – | 见下 | 链接签名(HMAC-SHA256,hex)|
| – | apiBase | – | API 域,默认 = 落地页同源 |
| – | locale | – | zh-CN / en-US |
| – | theme | – | light / dark / auto |
| – | primaryColor | – | 品牌主色,覆盖站点配置 |
签名校验(推荐生产环境开启)
admin 后台 → 站点 → 链接入口 勾选「强制签名校验」后,链接必须携带 t / s,由站点签名密钥(仅 admin 可见,一次性返回明文)按下面算法计算:
HMAC_SHA256(signSecret, `${appKey}\n${t}\n${externalUserId || ''}\n${nickname || ''}`)未开启签名时携带 t/s 也无害;开启后没带或签名错 / 过期都会被网关 403。
服务端代签接口:POST /api/v1/admin/cs/sites/:siteId/link-entry/sign —— admin 后台直接点「生成签名链接」即可拿到现成的 URL,无需手动调签名。
典型链接
https://cs.starim.io/?k=sk_xxxxxxxxxxxxxxxxx
https://cs.starim.io/?k=sk_xxxxxxxxxxxxxxxxx&u=u_42&nick=%E6%9D%8E%E9%9B%B7
https://cs.starim.io/?k=sk_xxxxxxxxxxxxxxxxx&u=u_42&nick=%E6%9D%8E%E9%9B%B7&t=1716268800&s=8f0a...与嵌入式接入的差异
| 维度 | 嵌入式(A–D)| 链接跳转 E |
|---|---|---|
| 触发位置 | 第三方网站 <script> 注入 | 独立落地页 https://chat.<im>/c/ |
| 域名校验 | originAllowList | 不校验来源,靠 linkEntry.signRequired |
| UI 形态 | 悬浮按钮 + 380×580 弹窗 | 全屏 ChatPanel,无按钮 |
| 落地品牌 | 跟随站点 theme | 可在 admin 单独配置 landingBrand |
| 平台总开关 | feature.cs.enabled | 叠加 feature.cs.link_entry.enabled |
注:standalone 模式下
defaultOpen隐式为 true,handshake 自动携带X-CS-Entry: linkheader;服务端按链接入口分支处理(详见.cursor/skills/customer-service/SKILL.md)。
5. 配置项与优先级
合并优先级(高 → 低):
StarIMCS.init() 参数 > <script data-*> 属性 > <script> query string > 远端 site 配置
| 字段 | data-* 属性 | 说明 |
| ------------------- | -------------------------- | --------------------------------------------- |
| appKey | data-app-key | 必填,站点 appKey |
| apiBase | data-api-base | 默认 https://api.starim.io |
| 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
| 方法 | 说明 |
| --------------------------------- | --------------------------------------------------- |
| StarIMCS.version | 当前 widget 版本 |
| StarIMCS.init(cfg) | 手动初始化(必须在 data-defer-init 模式下使用) |
| StarIMCS.identify(profile) | 同步身份信息(登录、登出、用户切换时调用) |
| StarIMCS.setLocale(locale) | 切换语言并持久化到 localStorage(见 §6.1) |
| StarIMCS.open({ prefill? }) | 展开聊天面板 |
| StarIMCS.close() | 收起聊天面板 |
| StarIMCS.toggle() | 切换面板 |
| StarIMCS.sendMessage(text) | 在面板内追加并发送一条游客消息 |
| StarIMCS.on(event, handler) | 订阅事件 |
| StarIMCS.off(event, handler) | 取消订阅 |
| StarIMCS.shutdown() | 销毁 widget、释放资源(一般用于 SPA 退出登录场景) |
事件类型:ready / open / close / message / error / identify / shutdown。
6.1 国际化(i18n)
核心策略:中文(
zh-*)显示中文,其他所有语言一律显示英文。当前支持的
LocaleType:zh-CN/en-US(与 StarIM 平台 app / admin 完全一致)。
优先级(从高到低)
| 级别 | 来源 | 设置方 | 说明 |
| ---- | -------------------------------------------- | ---------------- | ------------------------------------------------- |
| L1 | StarIMCS.init({ locale }) 或 setLocale() | 第三方 JS | 显式编程式设置 |
| L2 | <script data-locale="..."> | 第三方 HTML | 静态属性 |
| L3 | <script src="...?locale=..."> | 第三方 URL | Query string |
| L4 | localStorage['starim_locale'] | 第三方网站 | 用户偏好持久化(与 app/admin 一致的命名) |
| L5 | 远端 site config | 客服管理员 | admin 后台站点配置默认 locale |
| L6 | navigator.languages | 浏览器 | zh-* → zh-CN,其他一律 → en-US |
| L7 | 兜底 | — | en-US |
L4 与 L5 的区别:
- L4 由「第三方网站」主动写入
localStorage,代表「用户在你网站上选择的语言偏好」,不会被远端覆盖。- L5 由 StarIM 客服管理员在 admin 后台为站点设定的默认语言,仅当 widget 处于自动探测状态(L6)时才会覆盖。
关于 storage key:
starim_locale是 widget 用于持久化语言偏好的 key,带品牌前缀避免与第三方网站自身业务 key 冲突。localStorage按域隔离,第三方网站嵌入 cs-widget 不会与你网站自身业务产生窜数据。
请求头:所有 widget 发起的 REST 请求都会自动携带
X-Locale: <当前语言>,便于后端按游客语言返回错误码翻译。
推荐用法:跟随你网站的语言切换器
如果你的网站本身有「中文 / English」切换按钮,建议在用户切换时同步通知 widget:
// 用户点击 "English"
StarIMCS.setLocale('en-US');
// setLocale 会自动写入 localStorage['starim_locale'],
// 即使刷新页面 widget 仍记忆该选择。直接读写 localStorage(无需调用 widget API)
localStorage 是 widget 与第三方网站约定的契约层,你也可以完全不依赖 widget 的 JS API,仅通过写入约定 key 控制语言:
// 网站初始化语言时,直接写入即可,widget 加载后会读到这个值
localStorage.setItem('starim_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['starim_locale'] = 'zh-CN',浏览器是英文 | 中文 |
| localStorage['starim_locale'] = 'en-US',浏览器是中文 | 英文 |
| <script data-locale="zh-CN">,无视 localStorage | 中文(强制) |
| StarIMCS.init({ locale: 'en-US' }),无视 data-locale 与 localStorage | 英文(强制) |
7. SPA / 路由切换兼容
- Widget 通过 Shadow DOM 挂载在
document.body,且使用MutationObserver自愈:宿主页通过路由切换误删容器后,会在下一帧重新挂载。 - SPA 中用户登录态变化时调用
StarIMCS.identify()即可,无需 shutdown 后再 init,避免连接抖动与未读消息丢失。
8. CSP 兼容性
不使用
eval/new Function()/unsafe-inline。不依赖
<style>注入到宿主页<head>,所有样式都隔离在 Shadow DOM 内。第三方站点最小 CSP 建议:
Content-Security-Policy: script-src 'self' https://api.starim.io; connect-src 'self' https://api.starim.io wss://api.starim.io; img-src 'self' data: https://api.starim.io;
9. 访客身份识别
游客在打开 widget 的瞬间就需要被识别成一个稳定的「访客」,以便:跨刷新保留对话历史、跨设备/浏览器尽可能合并身份、登录后能与会员账户绑定。
widget 内置三层识别策略,按优先级尝试匹配同一访客;任一层命中即视为同一人:
| 层级 | 字段 | 来源 | 稳定性 | 用途 |
| ---- | ----------------- | --------------------------------- | ------- | ------------------------------------------------- |
| L1 | externalUserId | 业务方传入(已登录用户 ID) | 最高 | 与你网站的会员体系直接对齐 |
| L2 | fingerprint | 浏览器低熵特征 hash(widget 算) | 较高 | 用户清缓存 / 切隐身后仍能 ~80% 命中同一访客 |
| L3 | visitorId | localStorage 持久化 UUID | 一般 | 同浏览器内最稳定;清缓存后会变 |
后端识别时按 L1 → L2 → L3 顺序匹配,匹配到的访客记录会增量补全其它字段(例如未登录访客先用 fingerprint+visitorId 创建,登录后传入 externalUserId 即自动合并到该记录上,不会新建一个游客)。
数据合规:fingerprint 仅在客户端生成 hash 后上传,不上传任何裸的指纹原料;用户使用浏览器隐身模式或关闭 canvas readback 即可天然规避。游客身份不会进入主用户表(
users),仅写入独立的访客表(cs_visitors)。
启用 fingerprint 不需要任何额外配置,自动生效。如果你已经在用 externalUserId,fingerprint 仅作为兜底命中层,不影响你现有接入。
10. 排错速查
| 现象 | 排查方向 |
| --------------------------------------- | ------------------------------------------------------------------- |
| 页面没有出现悬浮按钮 | 检查 data-app-key 是否填写;控制台是否报 [StarIMCS] no appKey |
| 出现按钮但点不开 | 检查站点 CSP 是否拦截 connect-src api.starim.io |
| 样式被宿主页覆盖 | 不应该发生(Shadow DOM 已隔离);如出现,提交 issue |
| 路由切换后按钮消失 | 升级到 ≥ 0.1.x;MutationObserver 已修复 |
| WebSocket failed | 浏览器/代理拒绝 wss;插件会自动降级到 8s 短轮询,对用户透明 |
| 用户切换后仍是旧昵称 | 调用 StarIMCS.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) |
11. 与 bot-sdk 对比
@starim-io/cs-widget 与 @starim-io/bot-sdk 同源同发布形态,互为补充:
| | @starim-io/cs-widget | @starim-io/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
@starim/*,发布脚本一致:prepublishOnly钩子保证发版前自动 build。
License
MIT © StarIM
