Widget 集成
可嵌入 widget 是一个独立服务(packages/widget,端口 3211),在你的网站上提供 AI 聊天。架构:Express + Preact IIFE 运行在 sandboxed iframe 中,向服务器透传 SSE 代理,匿名会话、lead capture、按站点 branding。
可嵌入 widget 是一个独立服务(packages/widget,端口 3211),在你的网站上提供 AI 聊天。架构:Express + Preact IIFE 运行在 sandboxed iframe 中,向服务器透传 SSE 代理,匿名会话、lead capture、按站点 branding。
widget 是 Enterprise 功能(license widget_enabled);Community 默认有 1 个 widget,但创建需要该 flag 处于激活状态。如何嵌入(quick start)
- 创建 widget(admin UI → Settings → Widgets,或 API):
POST /api/widgets
Authorization: Bearer <admin JWT>
{ "name": "Sito Azienda", "allowedOrigins": ["https://www.azienda.it"],
"workspaceIds": ["<uuid>"], "primaryColor": "#4c6ef5", "botName": "Assistente" }响应:{ widget },包含 widget.id。
- Whitelist knowledge base - 用
PUT /api/widgets/:id/workspaces传入 widget 可访问其 RAG 的 workspace UUID(表WidgetWorkspace,复合 PK[widgetId, workspaceId])。
- 在宿主页面插入 snippet:
<div id="simmetric-widget" data-widget-id="<widget.id>" data-position="bottom-right"></div>
<script
src="https://widget.tuodominio.it/widget/<widget.id>.js"
integrity="sha384-<HASH>"
crossorigin="anonymous"
async
></script>安全:如果从第三方 CDN 提供 loader,请生成 bundle 的 SHA-384 hash 并添加integrity+crossorigin="anonymous"(Subresource Integrity)。如果 loader 与你的 widget service 同源,SRI 是可选的,但建议启用,以防御 CDN/asset 被篡改。
Loader(packages/widget/src/routes/loader.ts)读取 data-* 属性,创建一个 <iframe sandbox="allow-scripts allow-forms">(无 allow-same-origin、无 allow-top-navigation),加载 /widget/<id>(HTML),其中挂载 Preact bundle /widget/app.js。
Branding 与 trigger 配置
Widget 模型字段(由 shared 中的 widget.schema.ts 校验):
| 字段 | 默认值 | 描述 |
|---|---|---|
primaryColor | #4c6ef5 | 强调色(hex regex) |
botName | AI Assistant | bot 名称 |
logoUrl / avatarUrl | - | 图片 URL(http/https) |
welcomeMessage / fallbackMessage | - | 初始/fallback 消息(通过 localizedTexts 的 per-locale 文本) |
position | bottom-right | bottom-right/bottom-left |
allowedOrigins | - | URL 数组(JSON,最多 2000 字符)用于动态 CORS |
autoOpenDelay | - | 自动打开秒数(null = disabled,1–300) |
autoOpenUrlPatterns | - | 按 URL 自动打开的 glob pattern(**、*、?、{a,b}) |
exitIntentEnabled | false | exit-intent 打开 |
exitIntentCooldownMs | 1800000 | 60000–86400000 ms |
leadCaptureEnabled | false | Lead capture |
leadCapturePrompt | - | Lead prompt(最多 500) |
rateLimitPerMinute / sessionLimitPerDay | - | 按 widget 覆盖限制(null = 全局) |
responseProviderId / responseModel | - | Response model pin(v0.22,见下) |
suggestedQuestions / credits | - | per-locale 建议(最多 10)/ widget 额度 |
URL query param(?primaryColor=...&position=...&locale=...)在运行时覆盖。v0.22 附加字段:rateLimitPerMinute / sessionLimitPerDay(按 widget 覆盖全局限制;null = 全局默认)以及 responseProviderId/responseModel。
Response model pin(v0.22):每个 widget 的 admin 配置可以选择为该 widget 提供响应的 provider + LLM 模型(responseProviderId,provider UUID;responseModel,模型名)。pin 在每次聊天请求时由 DB 中的 widget 行在服务器端解析(绝不由客户端提供 - proxy 和访客 UI 不受影响),优先级高于 workspace 默认值,未设置 pin 的 widget 保持现有的 workspace/全局解析链。widget 聊天会记录实际服务的模型。
Trigger(widget/hooks/useTriggers.ts):监听来自 loader 的 postMessage - simmetric:urlChange(通过 matchUrlPattern/globToRegex 检查 autoOpenUrlPatterns:**、*、?、{a,b})、simmetric:exitIntent(带 cooldown 打开)。exit intent 在 loader 侧通过 mouseleave 且 e.clientY <= 10 检测。autoOpenDelay 以秒为单位(1–300,null = 禁用);exitIntentCooldownMs 60000–86400000 ms。
认证与 API key
- Widget ↔ Server:header
X-Api-Key: <WIDGET_API_KEY>发往/api/internal/widget/*。key 必须在packages/widget/.env和packages/server/.env之间匹配。 - 访客 ↔ Widget service:header
x-session-token(匿名会话,不是 JWT)。 - 动态 CORS(
widgetCors.ts):基于allowedOrigins的按 origin CORS header。DB 出错时 fail-closed(无 header = 浏览器阻止)。
匿名会话
POST /api/sessions(widget)→ 服务器创建 WidgetSession:256-bit hex token(crypto.randomBytes(32))、24h 过期、记录 IP。sessionMiddleware 在每次聊天请求时校验 token 并附加 req.widgetSession + req.widgetConfig。会话通过 postMessage 握手与 loader 一起持久化在宿主的 sessionStorage 中(simmetric:storage-*),并在 reload 时复用;过期 → 401 → Preact 显示 "Session expired",页面刷新后 widget 重新创建会话。
聊天流程(SSE proxy)
Visitatore → POST /api/chat/:widgetId/stream (x-session-token, body {message, chatId?})
→ widgetDailyMessageLimiter (per widget+IP, 5/24h prod, override sessionLimitPerDay)
→ widgetChatLimiter (per widgetId, 30/min prod, override rateLimitPerMinute)
→ sessionMiddleware valida sessione
→ check hourlyRemaining (DB-tracked, 20/h) → 429 se esaurito
→ increment messageCount (fire-and-forget)
→ pre-search RAG: POST /api/internal/widget/search {query, widgetId}
server risolve workspaceIds dalla whitelist WidgetWorkspace (IDOR prevention)
→ proxy a SERVER_URL/api/internal/widget/chat/stream
headers: X-Api-Key, X-Widget-Id, X-Widget-Session-Id, X-Accel-Buffering: no
→ relay byte SSE verbatim (responseType: "stream", filtra event: thinking)
→ Preact useWidgetChat parse: token | citations | done | errorproxy 是透明的:不 parse/过滤/转换 SSE 事件。如果 RAG pre-search 失败,会发出 status: rag-degraded,但聊天继续,只是没有上下文。
Lead capture
POST /api/lead/:widgetId(widget,sessionMiddleware):
- 检查
config.leadCaptureEnabled(关闭时 403)。 - Body
widgetLeadSubmitSchema:email(必填)、name(可选)、transcript[](role/content/timestamp)。 submitLead(widgetId, email, name, transcript, session.id)- 传递session.id(UUID FK)。- Limiter 每个 IP 3 leads/hour。
- Admin 获取:
GET /api/widgets/:id/leads,CSV 导出GET /api/widgets/:id/leads/export(带日期过滤 + 列选择;从 v0.22 起始终可用 - commodity flaglead_export已移除)。
Rate limit(双层)
| Limiter | Scope | 限制(prod) | Window |
|---|---|---|---|
| widgetDailyMessageLimiter | per widget+IP | 5/day(override sessionLimitPerDay) | 24h |
| widgetChatLimiter | per widgetId | 30/min(override rateLimitPerMinute) | 1 min |
| widgetSessionLimiter | per IP | 50/day(会话创建) | 24h |
| widgetLeadLimiter | per IP | 3/hour | 1h |
| server hourlyRemaining | per session | 20/h | 1h |
| server dailyRemaining | per session | 5/24h | 24h |
安全
- iframe sandbox
allow-scripts allow-forms(无 same-origin、无 top-navigation)。 - 为 embedding 放宽
helmet(无 CSP、无 frameguard、无 COOP/COEP/CORP)。 - 消息通过
markdown-it+dompurify渲染(anti-XSS)。 - 服务器端 DLP:每次运行的 DLP 匹配通过 SSE
done传输(dlp_matches,仅在 DLP 激活且无 bypass 角色时);访客的 PII 同意由PIIWarningPrompt管理(pre-chat gate,按 locale 的piiConsent文本)。 - 无 cookie、sandboxed iframe 中无 localStorage(storage 通过与 loader 的
postMessage握手,在 done/error/unmount 时批量处理)。 - loader 使用
getAttribute()(不用innerHTML)+encodeURIComponent;传入的simmetric:*消息用 guardevent.source === iframeEl.contentWindow校验。 - cache-bust endpoint(
POST /api/config/:widgetId/cache-bust)以常量时间比较X-Api-Key(crypto.timingSafeEqual带 length guard,unset 时 fail-closed)。
MCP(间接)
widget 不管理 MCP:安装/unpin 在 server admin 中进行。链接的 workspace 可能安装了 MCP tool → agent 在 widget 聊天期间自动使用它们(零 widget 代码)。SSE done 中的 mcpSources 指示哪些 MCP 做出了贡献。
扩展
- 新组件 →
packages/widget/src/widget/components/(Preact + Tailwind,不用 shadcn/ui)。 - 新 trigger → 扩展
useTriggers.ts+Widget字段 +internalWidget.tsroute。 - 新 rate limit →
src/middleware/rateLimit.ts+ 在index.ts中 mount。 - 可主题化 CSS →
src/widget/index.css中的 namespace--widget-*;静态样式用 Tailwind utility。
关键文件
| 文件 | 作用 |
|---|---|
packages/widget/src/index.ts | createApp() factory、mount routes |
src/routes/loader.ts | Loader JS + HTML iframe |
src/routes/chat.ts | SSE proxy + pre-search RAG |
src/routes/session.ts | 匿名会话 |
src/routes/config.ts | Config(缓存 5 min) |
src/routes/lead.ts | Lead capture |
src/middleware/session.ts | sessionMiddleware |
src/widget/App.tsx | Preact root、hook coord |
src/widget/hooks/{useWidgetChat,useWidgetConfig,useTriggers}.ts | State |
交叉链接
- widget endpoint:04 - API Reference
- RAG/agent 内部实现:05 - Feature Guide