diff --git a/CLAUDE.md b/CLAUDE.md index 11c1208..09aab59 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -174,6 +174,25 @@ catch (e) { toast('操作失败', 'error') } - 消息反馈: `/feedback/*`(`MessageFeedbackController`) - 敏感词管理: `/sensitive-word/*`(`SensitiveWordController`) - FAQ 管理: `/faq/*`(`FaqController`) +- SDK 认证: `/open-api/auth/*`(`AuthController`,Token 换取) +- API Key 角色绑定: `/api-key/{id}/roles`(`ApiKeyController`,admin 角色) + +### Filter 优先级 + +| 优先级 | Filter | 路径 | 说明 | +|--------|--------|------|------| +| `HIGHEST + 1` | `SdkAuthFilter` | `/ai/**` | SDK JWT 鉴权 | +| `HIGHEST + 2` | `ApiKeyAuthFilter` | `/open-api/**` | API Key 鉴权 | +| SecurityFilterChain 内 | `JwtAuthFilter` | 管理接口 | 管理后台 JWT | + +### SDK 鉴权架构 + +采用两段式鉴权:客户端后端用 API Key 换取短期 JWT Token → SDK 携带 Token 请求 `/ai/**` → SdkAuthFilter 校验放行。SDK JWT 密钥独立于管理后台 JWT(`jwt.sdk-secret`)。 + +- API Key 支持绑定客服角色列表(`role_ids` JSONB 字段),Token 换取时优先返回绑定的角色 +- 未绑定角色的 API Key 返回所有启用角色(向后兼容) +- JWT Token 中 `sub` 为 apiKeyId,`rids` 为允许的角色 ID 列表 +- 第三方系统接入指南详见 `SDK-INTEGRATION.md` ## P0 阶段新增功能 diff --git a/SDK-INTEGRATION.md b/SDK-INTEGRATION.md new file mode 100644 index 0000000..8243cf8 --- /dev/null +++ b/SDK-INTEGRATION.md @@ -0,0 +1,472 @@ +# Chat SDK 第三方系统集成指南 + +> 本文档说明第三方系统如何接入 AI 智能客服的 Chat SDK,完成从创建 API Key 到嵌入对话组件的全流程。 + +--- + +## 目录 + +1. [集成架构总览](#1-集成架构总览) +2. [第一步:创建 API Key](#2-第一步创建-api-key) +3. [第二步:绑定客服角色(可选)](#3-第二步绑定客服角色可选) +4. [第三步:后端换取 SDK Token](#4-第三步后端换取-sdk-token) +5. [第四步:前端嵌入 Chat SDK](#5-第四步前端嵌入-chat-sdk) + - [5.5 角色切换](#55-角色切换) +6. [API 接口参考](#6-api-接口参考) +7. [SDK 配置参数参考](#7-sdk-配置参数参考) +8. [错误码与排查](#8-错误码与排查) +9. [安全建议](#9-安全建议) + +--- + +## 1. 集成架构总览 + +``` +┌─────────────────┐ ┌──────────────────┐ ┌──────────────────┐ +│ 第三方前端 │ │ 第三方后端 │ │ AI 客服后端 │ +│ (嵌入 SDK) │ │ (你的服务) │ │ (本系统) │ +│ │ │ │ │ │ +│ Chat SDK ──────┼─────┼──────────────────┼─────┼─► /ai/** 对话 │ +│ ↑ token │ │ │ │ │ +│ │ │ ① 携带 API Key │ │ ② 返回 JWT Token│ +│ │ │ POST /open-api/ │─────┼─► /auth/token │ +│ │ │ auth/token │ │ │ +└─────────────────┘ └──────────────────┘ └──────────────────┘ +``` + +**两段式鉴权流程:** + +1. **管理后台**:管理员创建 API Key,可选绑定客服角色 +2. **第三方后端**:用 API Key 换取短期 JWT Token + 可用角色列表 +3. **第三方前端**:Chat SDK 携带 JWT Token 请求 `/ai/**` 接口进行对话 + +--- + +## 2. 第一步:创建 API Key + +在管理后台「系统设置 → API Key 管理」页面创建。 + +### 管理后台操作 + +1. 登录管理后台 `http://localhost:9090/index.html` +2. 进入「系统设置 → API Key 管理」 +3. 点击「+ 新建 API Key」 +4. 填写名称、描述、频率限制等参数 +5. **(可选)** 在创建弹窗中勾选需要绑定的客服角色 +6. 创建后**立即复制保存 Key**,关闭后无法再次查看 + +### API Key 格式 + +``` +sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx(sk_ 前缀 + 32 位 UUID) +``` + +--- + +## 3. 第二步:绑定客服角色(可选) + +客服角色决定 AI 的人设、知识库范围和 MCP 工具权限。 + +### 角色绑定规则 + +| role_ids 状态 | Token 换取行为 | +|---|---| +| 空数组 `[]`(默认) | 返回**所有**启用的客服角色(向后兼容) | +| 非空 `[1, 2, 3]` | 仅返回绑定的角色 | + +### 通过管理后台绑定 + +在 API Key 列表中点击「绑定角色」按钮,勾选需要的角色后保存。 + +### 通过 API 绑定 + +```bash +PUT /api-key/{keyId}/roles +Content-Type: application/json +Authorization: Bearer {管理后台 JWT Token} + +{ + "roleIds": [1, 2, 3] +} +``` + +响应: + +```json +{ + "success": true, + "message": "角色绑定更新成功" +} +``` + +--- + +## 4. 第三步:后端换取 SDK Token + +**⚠️ 重要:Token 换取必须在第三方后端完成,不可在前端暴露 API Key。** + +### 请求 + +```bash +POST /open-api/auth/token +Content-Type: application/json +X-API-Key: sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + +{ + "ttl": 7200 +} +``` + +| 参数 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `X-API-Key` | Header | 是 | 创建的 API Key | +| `ttl` | Body | 否 | Token 有效期(秒),默认 7200(2 小时),范围 [300, 86400] | + +### 成功响应 + +```json +{ + "success": true, + "token": "eyJhbGciOiJIUzI1NiJ9...", + "expiresIn": 7200, + "roles": [ + { "id": "1", "key": "general", "name": "通用客服" }, + { "id": "2", "key": "finance", "name": "财务顾问" } + ] +} +``` + +| 字段 | 说明 | +|---|---| +| `token` | SDK JWT Token,传给前端使用 | +| `expiresIn` | 过期时间(秒) | +| `roles` | 可用客服角色列表,前端用于展示角色选择器 | + +### 错误响应 + +| HTTP 状态码 | 说明 | +|---|---| +| 401 | API Key 无效或已禁用 | +| 403 | 无可用客服角色 | + +--- + +## 5. 第四步:前端嵌入 Chat SDK + +### 使用测试面板快速验证 + +访问 `http://localhost:9090/sdk/test.html` 打开测试面板: + +1. 在「🔐 SDK Token 鉴权」区域输入 API Key +2. 点击「获取 Token」按钮自动换取(或手动粘贴已有的 Token) +3. Token 获取成功后,integrateId 会自动填充为第一个可用角色 +4. 点击「🚀 初始化 SDK」即可测试对话功能 + +测试面板支持所有 P0/P1/P2 测试用例的自动化运行,包括 API 连通性验证。 + +### 5.1 引入 SDK + +```html + +``` + +### 5.2 初始化 + +```javascript +const chatbot = ChatbotSDK.init({ + // ========== 必填参数 ========== + integrateId: '1', // 客服角色 ID(从 Token 换取接口的 roles 中选择) + requestDomain: 'https://your-domain.com', // AI 客服后端域名 + + // ========== 鉴权参数(推荐) ========== + token: 'eyJhbGciOiJIUzI1NiJ9...', // 后端换取的 SDK Token + roles: [ // 可用角色列表(传多个时 SDK header 显示角色切换下拉框) + { id: '1', key: 'general', name: '通用客服' }, + { id: '2', key: 'finance', name: '财务顾问' } + ], + + // ========== 可选参数 ========== + userId: 'user_12345', // 用户标识(用于会话隔离) + title: 'AI 智能助手', // 对话窗口标题 + theme: 'light', // 主题:light / dark + primaryColor: '#4F46E5', // 主题色(十六进制) + streaming: true, // 启用流式回复(默认 true) + enableRag: true, // 启用 RAG 知识库检索(默认 true) + quickReplies: ['如何退款?', '联系人工客服'], // 快捷问题 + position: 'right-bottom', // 悬浮按钮位置:right-bottom / left-bottom + width: 380, // 窗口宽度(px) + height: 520, // 窗口高度(px) + debug: true, // 控制台调试日志 + + // ========== 回调函数 ========== + onReady: function() { console.log('SDK 就绪'); }, + onMessage: function(msg) { console.log('收到消息', msg); }, + onError: function(err) { console.error('SDK 错误', err); } +}); +``` + +### 5.3 SDK 方法 + +```javascript +// 销毁实例(移除 DOM 和事件监听) +chatbot.destroy(); +``` + +### 5.4 不使用 Token 的兼容模式 + +如果暂时不接入后端 Token 换取,可直接使用兼容模式(**不推荐用于生产环境**): + +```javascript +ChatbotSDK.init({ + integrateId: '1', // 直接指定角色 ID + requestDomain: 'https://your-domain.com' + // 不传 token 和 roles +}); +``` + +兼容模式下,`integrateId` 直接作为 `roleId` 传递给后端,无需 Token 换取步骤。由于不传 `roles`,SDK 不会显示角色选择器,用户只能使用初始指定的单一角色。 + +### 5.5 角色切换 + +当 `roles` 数组长度 > 1 时,SDK 会在聊天窗口头部自动显示角色选择下拉框,用户可随时切换当前使用的客服角色。 + +**触发条件:** +- `ChatbotSDK.init()` 时传入的 `roles` 数组包含 2 个及以上角色 + +**切换行为:** +1. 自动保存当前对话到 localStorage(与旧角色关联) +2. 清空当前消息列表 +3. 生成新的 `chatId`(与新角色关联) +4. 尝试从 localStorage 恢复新角色的缓存消息 +5. 异步从后端加载新角色的历史会话 + +**示例:** + +```javascript +// 1. 后端换取 Token,获取可用角色列表 +const data = await fetch('/api/chatbot/token').then(r => r.json()); + +// 2. 初始化 SDK,传入角色列表 +ChatbotSDK.init({ + integrateId: data.roles[0].id, // 默认使用第一个角色 + requestDomain: 'https://your-domain.com', + token: data.token, + roles: data.roles, // 传入多个角色 → header 显示角色选择器 + userId: 'current_user_id' +}); +``` + +**安全说明:** +- 角色切换由 SDK 前端发起,后端 `SdkAuthFilter` 会校验 JWT Token 中的 `rids` claim,确保用户只能切换到被授权的角色 +- 切换角色不需要重新换取 Token,JWT 中已包含允许的角色 ID 列表 + +--- + +## 6. API 接口参考 + +### 6.1 Token 换取 + +``` +POST /open-api/auth/token +``` + +详见 [第三步:后端换取 SDK Token](#4-第三步后端换取-sdk-token)。 + +### 6.2 对话接口(SDK 内部调用,无需手动对接) + +| 接口 | 方法 | 说明 | +|---|---|---| +| `/ai/assistant_app/chat/sync` | GET | 同步对话(返回完整文本) | +| `/ai/assistant_app/chat/sse` | GET | SSE 流式对话 | +| `/ai/assistant_app/chat/rag/sse` | GET | RAG 增强流式对话 | +| `/ai/assistant_app/rag/sources` | GET | 获取 RAG 引用来源 | +| `/ai/sdk/conversation/list` | GET | 会话列表 | +| `/ai/sdk/conversation/{id}/messages` | GET | 会话消息 | +| `/ai/sdk/conversation/{id}` | DELETE | 删除会话 | +| `/ai/sdk/conversation/{id}/export` | GET | 导出会话 | +| `/category/tree` | GET | 知识库分类树 | +| `/feedback` | POST | 消息反馈 | + +**通用请求参数(对话接口):** + +| 参数 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `message` | Query | 是 | 用户消息 | +| `chatId` | Query | 是 | 会话 ID(SDK 自动管理) | +| `roleId` | Query | 否 | 客服角色 ID | +| `accountId` | Query | 否 | 用户标识 | +| `categoryId` | Query | 否 | 知识库分类 ID | +| `rewriteStrategy` | Query | 否 | RAG 查询重写策略:REWRITE / TRANSLATION / COMPRESSION / MULTI_QUERY | + +**认证方式:** 所有 `/ai/**` 请求自动在 Header 中携带 `Authorization: Bearer {token}`。 + +### 6.3 管理接口(需要管理后台 JWT) + +| 接口 | 方法 | 说明 | +|---|---|---| +| `/api-key/list` | GET | API Key 列表(admin) | +| `/api-key` | POST | 创建 API Key(admin) | +| `/api-key/{id}/revoke` | PUT | 吊销 API Key(admin) | +| `/api-key/{id}/enable` | PUT | 启用 API Key(admin) | +| `/api-key/{id}` | DELETE | 删除 API Key(admin) | +| `/api-key/{id}/roles` | PUT | 更新角色绑定(admin) | +| `/role/list` | GET | 客服角色列表(admin) | +| `/role/all` | GET | 全部角色含停用(admin) | + +--- + +## 7. SDK 配置参数参考 + +| 参数 | 类型 | 默认值 | 说明 | +|---|---|---|---| +| `integrateId` | String/Number | **必填** | 客服角色 ID | +| `requestDomain` | String | **必填** | 后端域名 | +| `token` | String | - | SDK JWT Token(推荐) | +| `roles` | Array | - | 角色列表 `[{id, key, name}]`,长度 > 1 时 header 显示切换下拉框 | +| `userId` | String | - | 用户标识(会话隔离) | +| `categoryId` | String | - | 默认知识库分类 ID | +| `showCategorySwitch` | Boolean | false | 显示分类切换器 | +| `title` | String | 'AI 智能助手' | 窗口标题 | +| `width` | Number | 380 | 窗口宽度(px) | +| `height` | Number | 520 | 窗口高度(px,最小 300) | +| `position` | String | 'right-bottom' | 悬浮按钮位置 | +| `primaryColor` | String | '#4F46E5' | 主题色 | +| `launcherTheme` | String | - | 按钮主题:dream-purple / mint-tech / coral-peach / sky-blue | +| `launcherIcon` | String | (内置) | 自定义悬浮按钮 SVG | +| `theme` | String | 'light' | 界面主题:light / dark | +| `streaming` | Boolean | true | 流式回复 | +| `enableRag` | Boolean | true | RAG 知识库检索 | +| `rewriteStrategy` | String | 'REWRITE' | 查询重写策略 | +| `quickReplies` | String[] | [] | 快捷问题列表 | +| `showClear` | Boolean | true | 显示清空按钮 | +| `showAdminPanel` | Boolean | false | 显示管理入口 | +| `showTeaser` | Boolean | true | 显示提示气泡 | +| `teaserText` | String | '' | 提示气泡文本 | +| `sound` | Boolean | false | 消息提示音 | +| `notification` | Boolean | false | 浏览器通知 | +| `locale` | String | 'zh-CN' | 语言:zh-CN / en | +| `debug` | Boolean | true | 调试日志 | +| `onReady` | Function | - | SDK 就绪回调 | +| `onMessage` | Function | - | 消息回调 | +| `onError` | Function | - | 错误回调 | + +--- + +## 8. 错误码与排查 + +### Token 换取阶段 + +| 错误 | 原因 | 解决方案 | +|---|---|---| +| 401 `API Key 鉴权失败` | X-API-Key 无效或缺失 | 检查 Header 是否正确携带 | +| 401 `API Key 已被禁用` | Key 被吊销 | 在管理后台重新启用或创建新 Key | +| 401 `API Key 已过期` | Key 超过有效期 | 创建新的 API Key | +| 403 `无可用客服角色` | 未绑定角色且系统无启用角色 | 在管理后台创建并启用客服角色 | + +### SDK 对话阶段 + +| HTTP 状态码 | SDK 提示 | 原因 | +|---|---|---| +| 401 | 鉴权失败 | Token 过期或无效,需重新换取 | +| 403 | 无访问权限 | roleId 不在 Token 允许范围内 | +| 429 | 请求过于频繁 | 超过 API Key 频率限制 | +| 500 | 服务器异常 | 后端错误,查看服务端日志 | +| 502/503 | 服务暂不可用 | 后端未启动或正在部署 | + +### 常见问题 + +**Q: Token 过期后怎么办?** +A: Token 默认 2 小时过期。建议在第三方后端实现 Token 缓存和自动刷新逻辑:检测到 401 时重新调用 `/open-api/auth/token` 换取新 Token。 + +**Q: 如何让用户只能使用特定角色?** +A: 在 API Key 上绑定角色(`PUT /api-key/{id}/roles`),SDK 初始化时只传入绑定的角色 ID。 + +**Q: 多个第三方系统可以用同一个 API Key 吗?** +A: 可以,但建议每个系统使用独立的 API Key,便于独立管控频率限制和角色权限。 + +--- + +## 9. 安全建议 + +1. **API Key 仅在后端使用**:永远不要将 API Key 暴露到前端代码中 +2. **Token 短有效期**:生产环境建议 TTL 设为 1-2 小时,配合自动刷新 +3. **角色最小权限**:只绑定必要的客服角色,避免授予过多权限 +4. **频率限制**:根据业务量设置合理的 rateLimit(默认 60 次/分钟) +5. **HTTPS**:生产环境必须使用 HTTPS,防止 Token 被中间人截获 +6. **定期轮换 Key**:定期吊销旧 Key 并创建新 Key + +--- + +## 附录:完整对接示例(Node.js) + +```javascript +// ==================== 第三方后端示例 ==================== +const express = require('express'); +const app = express(); + +const AI_DOMAIN = 'https://your-ai-domain.com'; +const API_KEY = 'sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; + +// Token 缓存 +let cachedToken = null; +let tokenExpireAt = 0; + +// 换取 Token(带缓存) +async function getSdkToken() { + if (cachedToken && Date.now() < tokenExpireAt) { + return cachedToken; + } + + const res = await fetch(`${AI_DOMAIN}/open-api/auth/token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': API_KEY + }, + body: JSON.stringify({ ttl: 7200 }) + }); + + const data = await res.json(); + if (!data.success) throw new Error(data.message); + + cachedToken = data.token; + tokenExpireAt = Date.now() + (data.expiresIn - 300) * 1000; // 提前 5 分钟刷新 + return { token: data.token, roles: data.roles }; +} + +// 给前端提供 Token +app.get('/api/chatbot/token', async (req, res) => { + try { + const data = await getSdkToken(); + res.json({ success: true, ...data }); + } catch (e) { + res.status(500).json({ success: false, message: e.message }); + } +}); + +app.listen(3000); +``` + +```html + + + +``` diff --git a/client/README.md b/client/README.md index 2cb55be..06f65af 100644 --- a/client/README.md +++ b/client/README.md @@ -85,7 +85,7 @@ SDK 产物位于 `client/dist/` 目录: | `width` | `number` | ❌ | `380` | P0 | 弹窗宽度(px) | | `position` | `string` | ❌ | `"right-bottom"` | P0 | 悬浮按钮位置 | | `primaryColor` | `string` | ❌ | `"#4F46E5"` | P0 | 主色调 | -| `launcherIcon` | `string` | ❌ | SVG 图标 | P0 | 悬浮按钮图标 | +| `launcherIcon` | `string` | ❌ | 极光粒子图标 | P0 | 悬浮按钮图标(可传 URL 或 SVG 字符串),默认使用极光粒子动画图标 | | `showClear` | `boolean` | ❌ | `true` | P0 | 是否显示清空按钮 | | `quickReplies` | `string[]` | ❌ | `[]` | P1 | 欢迎态快捷问题芯片,点击即自动发送 | | `theme` | `string` | ❌ | `"light"` | P2 | 主题模式:`"light"` / `"dark"` | diff --git a/client/dist/chatbot-sdk.js b/client/dist/chatbot-sdk.js index 64a87ea..76466c6 100644 --- a/client/dist/chatbot-sdk.js +++ b/client/dist/chatbot-sdk.js @@ -96,97 +96,13 @@ var ChatbotSDK = (function () { }, }; - /** 默认悬浮按钮 SVG 图标(赛博机器人:对话气泡 + 机器人面孔 + 天线能量灯 + 眨眼动效) */ - const DEFAULT_LAUNCHER_ICON = ``; - // ==================== 马卡龙色系玻璃质感图标 ==================== - /** - * 生成玻璃质感机器人 SVG 图标 - * 包含:圆润机器人头部 + 眨眼动效 + 跳动气泡 + 高光反射层 - */ - function buildGlassIcon(eyeColor, smileColor, bubbleColor) { - return ``; - } - /** 梦幻紫粉主题图标 */ - const GLASS_ICON_DREAM_PURPLE = buildGlassIcon('rgba(139,92,246,0.6)', // 眼睛:紫色 - 'rgba(139,92,246,0.45)', // 微笑 - 'rgba(240,171,252,0.85)' // 气泡:粉紫 - ); - /** 薄荷青绿主题图标 */ - const GLASS_ICON_MINT_TECH = buildGlassIcon('rgba(16,185,129,0.6)', // 眼睛:翠绿 - 'rgba(16,185,129,0.45)', // 微笑 - 'rgba(110,231,183,0.85)' // 气泡:薄荷 - ); - /** 珊瑚蜜桃主题图标 */ - const GLASS_ICON_CORAL_PEACH = buildGlassIcon('rgba(244,63,94,0.55)', // 眼睛:珊瑚红 - 'rgba(244,63,94,0.4)', // 微笑 - 'rgba(253,186,116,0.85)' // 气泡:蜜桃 - ); - /** 天空蓝紫主题图标 */ - const GLASS_ICON_SKY_BLUE = buildGlassIcon('rgba(56,189,248,0.6)', // 眼睛:天蓝 - 'rgba(56,189,248,0.45)', // 微笑 - 'rgba(167,139,250,0.85)' // 气泡:淡紫 - ); - /** 主题 → 默认主色映射(当用户未指定 primaryColor 时自动使用) */ - const THEME_PRIMARY_COLORS = { - 'dream-purple': '#A78BFA', - 'mint-tech': '#34D399', - 'coral-peach': '#FB7185', - 'sky-blue': '#7DD3FC', - }; + /** 默认悬浮按钮图标 — E5 梦幻拖尾·全粒子:极光旋转渐变底 + 20 颗粒子系统 */ + const DEFAULT_LAUNCHER_ICON = ``; /** * 解析并校验用户传入的配置,填充默认值 */ function parseConfig(raw) { - var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l; + var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m; // 校验必传参数:integrateId(对应后端 roleId) if (!raw.integrateId || (typeof raw.integrateId !== 'string' && typeof raw.integrateId !== 'number') || (typeof raw.integrateId === 'string' && raw.integrateId.trim() === '')) { @@ -202,30 +118,15 @@ var ChatbotSDK = (function () { try { new URL(raw.requestDomain); } - catch (_m) { + catch (_o) { logger.error(`requestDomain 不是合法的 URL 格式:${raw.requestDomain}。请提供完整的域名,如 https://api.example.com`); return null; } // integrateId 统一转为字符串(后端 roleId 为 Long,但 query param 传字符串也可接收) const integrateIdStr = String(raw.integrateId).trim(); - // 解析 launcherTheme:根据主题自动选择图标和默认主色 - const launcherTheme = raw.launcherTheme || undefined; - let resolvedLauncherIcon = raw.launcherIcon || DEFAULT_LAUNCHER_ICON; - let resolvedPrimaryColor = raw.primaryColor || '#4F46E5'; - if (launcherTheme && !raw.launcherIcon) { - // 有主题且未自定义图标 → 使用主题专属玻璃质感图标 - const themeIconsMap = { - 'dream-purple': GLASS_ICON_DREAM_PURPLE, - 'mint-tech': GLASS_ICON_MINT_TECH, - 'coral-peach': GLASS_ICON_CORAL_PEACH, - 'sky-blue': GLASS_ICON_SKY_BLUE, - }; - resolvedLauncherIcon = themeIconsMap[launcherTheme] || DEFAULT_LAUNCHER_ICON; - // 若未自定义主色 → 使用主题默认色 - if (!raw.primaryColor) { - resolvedPrimaryColor = THEME_PRIMARY_COLORS[launcherTheme] || '#4F46E5'; - } - } + // 解析图标:用户传入优先,否则使用默认极光粒子图标 + const resolvedLauncherIcon = raw.launcherIcon || DEFAULT_LAUNCHER_ICON; + const resolvedPrimaryColor = raw.primaryColor || '#4F46E5'; // 填充默认值 const config = { integrateId: integrateIdStr, @@ -238,7 +139,6 @@ var ChatbotSDK = (function () { height: Math.max(300, (_c = raw.height) !== null && _c !== void 0 ? _c : 520), position: raw.position === 'left-bottom' ? 'left-bottom' : 'right-bottom', primaryColor: resolvedPrimaryColor, - launcherTheme: launcherTheme, launcherIcon: resolvedLauncherIcon, showClear: (_d = raw.showClear) !== null && _d !== void 0 ? _d : true, showAdminPanel: (_e = raw.showAdminPanel) !== null && _e !== void 0 ? _e : false, @@ -248,16 +148,19 @@ var ChatbotSDK = (function () { theme: raw.theme === 'dark' ? 'dark' : 'light', showTeaser: (_f = raw.showTeaser) !== null && _f !== void 0 ? _f : true, teaserText: (typeof raw.teaserText === 'string' && raw.teaserText.trim()) || '', - streaming: (_g = raw.streaming) !== null && _g !== void 0 ? _g : true, - enableRag: (_h = raw.enableRag) !== null && _h !== void 0 ? _h : true, + resizable: (_g = raw.resizable) !== null && _g !== void 0 ? _g : true, + streaming: (_h = raw.streaming) !== null && _h !== void 0 ? _h : true, + enableRag: (_j = raw.enableRag) !== null && _j !== void 0 ? _j : true, rewriteStrategy: raw.rewriteStrategy || 'REWRITE', locale: raw.locale || 'zh-CN', - debug: (_j = raw.debug) !== null && _j !== void 0 ? _j : true, - sound: (_k = raw.sound) !== null && _k !== void 0 ? _k : false, - notification: (_l = raw.notification) !== null && _l !== void 0 ? _l : false, + debug: (_k = raw.debug) !== null && _k !== void 0 ? _k : true, + sound: (_l = raw.sound) !== null && _l !== void 0 ? _l : false, + notification: (_m = raw.notification) !== null && _m !== void 0 ? _m : false, onError: typeof raw.onError === 'function' ? raw.onError : undefined, onReady: typeof raw.onReady === 'function' ? raw.onReady : undefined, onMessage: typeof raw.onMessage === 'function' ? raw.onMessage : undefined, + token: raw.token, + roles: raw.roles, chatId: '', // 初始为空,由 chatId 初始化流程填充 }; logger.info(`配置解析完成 integrateId(=roleId)=${config.integrateId} userId(=accountId)=${config.userId || '(未设置)'} requestDomain=${config.requestDomain}`); @@ -275,6 +178,7 @@ var ChatbotSDK = (function () { minimize: '最小化', close: '关闭', status_online: '在线', + role_selector_label: '服务角色', // 欢迎空状态 welcome_title: '你好,我是 AI 智能助手', welcome_desc: '有什么可以帮你的吗?在下方输入框开始提问吧~', @@ -320,6 +224,7 @@ var ChatbotSDK = (function () { // 提示气泡 teaser_text: '有什么可以帮你的吗?', new_msg_announce: '收到新消息', + resize: '拖拽缩放窗口', // 错误提示 error_network: '网络连接失败,请检查网络', error_timeout: '请求超时,请稍后重试', @@ -340,6 +245,7 @@ var ChatbotSDK = (function () { minimize: 'Minimize', close: 'Close', status_online: 'Online', + role_selector_label: 'Service', // Welcome welcome_title: 'Hi, I am your AI assistant', welcome_desc: 'How can I help you? Type your question below to get started.', @@ -385,6 +291,7 @@ var ChatbotSDK = (function () { // Teaser teaser_text: 'How can I help you?', new_msg_announce: 'New message received', + resize: 'Resize window', // Errors error_network: 'Network connection failed', error_timeout: 'Request timed out, please try again', @@ -439,10 +346,26 @@ var ChatbotSDK = (function () { /** 请求超时时间(毫秒) */ const REQUEST_TIMEOUT = 30000; let currentConfig = null; + /** 运行时动态设置的角色 ID(通过角色选择器切换),优先于 currentConfig.integrateId */ + let activeIntegrateId = null; /** 设置当前配置 */ function setApiConfig(config) { currentConfig = config; } + /** 清空当前配置(销毁 SDK 时调用) */ + function clearApiConfig() { + currentConfig = null; + activeIntegrateId = null; + } + /** 设置当前活跃角色 ID(角色切换时调用),影响后续所有 API 调用的 roleId 参数 */ + function setActiveRoleId(roleId) { + activeIntegrateId = roleId; + } + /** 获取当前有效的 integrateId(优先使用运行时设置,回退到配置默认值) */ + function getActiveIntegrateId() { + var _a; + return (_a = activeIntegrateId !== null && activeIntegrateId !== void 0 ? activeIntegrateId : currentConfig === null || currentConfig === void 0 ? void 0 : currentConfig.integrateId) !== null && _a !== void 0 ? _a : ''; + } /** 更新当前 chatId(对话 ID) */ function updateChatId(chatId) { if (currentConfig) { @@ -484,7 +407,7 @@ var ChatbotSDK = (function () { params.set('message', message); params.set('chatId', currentConfig.chatId); // integrateId 映射为 roleId - setIfPresent(params, 'roleId', currentConfig.integrateId); + setIfPresent(params, 'roleId', getActiveIntegrateId()); // userId 映射为 accountId setIfPresent(params, 'accountId', currentConfig.userId); return buildUrl(`/ai/assistant_app/chat/sync?${params.toString()}`); @@ -496,7 +419,7 @@ var ChatbotSDK = (function () { const params = new URLSearchParams(); params.set('message', message); params.set('chatId', currentConfig.chatId); - setIfPresent(params, 'roleId', currentConfig.integrateId); + setIfPresent(params, 'roleId', getActiveIntegrateId()); setIfPresent(params, 'accountId', currentConfig.userId); setIfPresent(params, 'categoryId', categoryId !== null && categoryId !== void 0 ? categoryId : currentConfig.categoryId); return buildUrl(`/ai/assistant_app/chat/sse?${params.toString()}`); @@ -509,7 +432,7 @@ var ChatbotSDK = (function () { params.set('message', message); params.set('chatId', currentConfig.chatId); params.set('rewriteStrategy', currentConfig.rewriteStrategy || 'REWRITE'); - setIfPresent(params, 'roleId', currentConfig.integrateId); + setIfPresent(params, 'roleId', getActiveIntegrateId()); setIfPresent(params, 'accountId', currentConfig.userId); setIfPresent(params, 'categoryId', categoryId !== null && categoryId !== void 0 ? categoryId : currentConfig.categoryId); return buildUrl(`/ai/assistant_app/chat/rag/sse?${params.toString()}`); @@ -522,7 +445,7 @@ var ChatbotSDK = (function () { params.set('message', message); params.set('chatId', currentConfig.chatId); params.set('rewriteStrategy', currentConfig.rewriteStrategy || 'REWRITE'); - setIfPresent(params, 'roleId', currentConfig.integrateId); + setIfPresent(params, 'roleId', getActiveIntegrateId()); setIfPresent(params, 'accountId', currentConfig.userId); setIfPresent(params, 'categoryId', categoryId !== null && categoryId !== void 0 ? categoryId : currentConfig.categoryId); return buildUrl(`/ai/assistant_app/rag/sources?${params.toString()}`); @@ -542,7 +465,26 @@ var ChatbotSDK = (function () { } } try { - const response = await fetch(url, Object.assign(Object.assign({}, options), { signal: controller.signal, mode: 'cors', credentials: 'include' })); + // 注入 SDK Token 认证头(仅对 /ai/ 路径注入,避免将 SDK Token 发送到管理接口) + const headers = {}; + if (options.headers) { + if (options.headers instanceof Headers) { + options.headers.forEach((value, key) => { headers[key] = value; }); + } + else if (Array.isArray(options.headers)) { + options.headers.forEach(([key, value]) => { headers[key] = value; }); + } + else { + Object.assign(headers, options.headers); + } + } + if ((currentConfig === null || currentConfig === void 0 ? void 0 : currentConfig.token) && url.includes('/ai/')) { + headers['Authorization'] = `Bearer ${currentConfig.token}`; + } + const response = await fetch(url, Object.assign(Object.assign({}, options), { headers, signal: controller.signal, mode: 'cors', credentials: 'include' })); + if (response.status === 401 && url.includes('/ai/')) { + logger.error('SDK 认证失败:Token 缺失或已过期,请重新调用 /open-api/auth/token 换取 Token'); + } return response; } catch (err) { @@ -589,22 +531,22 @@ var ChatbotSDK = (function () { */ async function chatRequest(message) { const url = buildChatUrl(message); - logger.lifecycleSend(currentConfig.integrateId, message.length); + logger.lifecycleSend(getActiveIntegrateId(), message.length); try { const response = await safeFetch(url); if (!response.ok) { const errorMsg = getHttpErrorMessage(response.status); - logger.lifecycleError(currentConfig.integrateId, String(response.status), errorMsg); + logger.lifecycleError(getActiveIntegrateId(), String(response.status), errorMsg); throw new CskError(errorMsg, `http_${response.status}`); } const text = await response.text(); - logger.lifecycleReply(currentConfig.integrateId, text.length); + logger.lifecycleReply(getActiveIntegrateId(), text.length); return text; } catch (err) { if (err instanceof CskError) throw err; - logger.lifecycleError(currentConfig.integrateId, 'unknown', String(err)); + logger.lifecycleError(getActiveIntegrateId(), 'unknown', String(err)); throw new CskError(t('error_unknown'), 'unknown'); } } @@ -619,12 +561,12 @@ var ChatbotSDK = (function () { ? buildChatRAGSSEUrl(message, categoryId) : buildChatSSEUrl(message, categoryId); let totalText = ''; - logger.lifecycleSend(currentConfig.integrateId, message.length); + logger.lifecycleSend(getActiveIntegrateId(), message.length); try { const response = await safeFetch(url, {}, REQUEST_TIMEOUT * 2, signal); if (!response.ok) { const errorMsg = getHttpErrorMessage(response.status); - logger.lifecycleError(currentConfig.integrateId, String(response.status), errorMsg); + logger.lifecycleError(getActiveIntegrateId(), String(response.status), errorMsg); onError(new CskError(errorMsg, `http_${response.status}`)); return; } @@ -693,7 +635,7 @@ var ChatbotSDK = (function () { finally { reader.releaseLock(); } - logger.lifecycleStreamDone(currentConfig.integrateId, totalText.length); + logger.lifecycleStreamDone(getActiveIntegrateId(), totalText.length); onDone(); } catch (err) { @@ -706,7 +648,7 @@ var ChatbotSDK = (function () { onError(err); } else { - logger.lifecycleError(currentConfig.integrateId, 'unknown', String(err)); + logger.lifecycleError(getActiveIntegrateId(), 'unknown', String(err)); onError(new CskError(t('error_network'), 'network')); } } @@ -824,8 +766,8 @@ var ChatbotSDK = (function () { const params = new URLSearchParams(); if (currentConfig === null || currentConfig === void 0 ? void 0 : currentConfig.userId) params.set('accountId', currentConfig.userId); - if (currentConfig === null || currentConfig === void 0 ? void 0 : currentConfig.integrateId) - params.set('roleId', currentConfig.integrateId); + if (getActiveIntegrateId()) + params.set('roleId', getActiveIntegrateId()); const url = buildUrl(`/ai/sdk/conversation/${conversationId}/messages?${params.toString()}`); try { const response = await safeFetch(url); @@ -850,8 +792,8 @@ var ChatbotSDK = (function () { const params = new URLSearchParams(); if (currentConfig === null || currentConfig === void 0 ? void 0 : currentConfig.userId) params.set('accountId', currentConfig.userId); - if (currentConfig === null || currentConfig === void 0 ? void 0 : currentConfig.integrateId) - params.set('roleId', currentConfig.integrateId); + if (getActiveIntegrateId()) + params.set('roleId', getActiveIntegrateId()); const url = buildUrl(`/ai/sdk/conversation/${conversationId}?${params.toString()}`); try { const response = await safeFetch(url, { method: 'DELETE' }); @@ -874,40 +816,39 @@ var ChatbotSDK = (function () { const params = new URLSearchParams(); if (currentConfig === null || currentConfig === void 0 ? void 0 : currentConfig.userId) params.set('accountId', currentConfig.userId); - if (currentConfig === null || currentConfig === void 0 ? void 0 : currentConfig.integrateId) - params.set('roleId', currentConfig.integrateId); + if (getActiveIntegrateId()) + params.set('roleId', getActiveIntegrateId()); return buildUrl(`/ai/sdk/conversation/${conversationId}/export?${params.toString()}`); } // ==================== chatId 自动初始化 ==================== /** * 初始化 chatId:查询后端已有会话,找到则复用,否则生成新的 * - * 逻辑: - * 1. 先查 localStorage 缓存的 chatId(同一 integrateId + userId 可能复用) - * 2. 查 /ai/sdk/conversation/list?accountId=X&roleId=Y 看是否有匹配的会话 - * 3. 有会话 → 使用最新会话的 conversationId 作为 chatId - * 4. 无会话 → 自动生成 chatId(格式:sdk_timestamp_random) + * @param forceRefresh 为 true 时跳过 localStorage 缓存,强制查询后端 */ - async function initChatId() { + async function initChatId(forceRefresh = false) { if (!currentConfig) return ''; - // 1. 先尝试从 localStorage 恢复 - const cachedChatId = loadCachedChatId(currentConfig.integrateId, currentConfig.userId); - if (cachedChatId) { - currentConfig.chatId = cachedChatId; - logger.info(`从缓存恢复 chatId=${cachedChatId}`); - return cachedChatId; + const activeId = getActiveIntegrateId(); + // 1. 先从 localStorage 恢复(非强制刷新时) + if (!forceRefresh) { + const cachedChatId = loadCachedChatId(activeId, currentConfig.userId); + if (cachedChatId) { + currentConfig.chatId = cachedChatId; + logger.info(`从缓存恢复 chatId=${cachedChatId}`); + return cachedChatId; + } } // 2. 查询后端会话列表 try { - const result = await fetchConversationList(1, 5, currentConfig.userId, currentConfig.integrateId); + const result = await fetchConversationList(1, 5, currentConfig.userId, activeId); if (result.list.length > 0) { // 使用最新会话的 conversationId 作为 chatId const latestConv = result.list[0]; const chatId = latestConv.conversationId || latestConv.chatId || ''; if (chatId) { currentConfig.chatId = chatId; - saveCachedChatId(currentConfig.integrateId, currentConfig.userId, chatId); + saveCachedChatId(activeId, currentConfig.userId, chatId); logger.info(`从后端恢复会话 chatId=${chatId} messageCount=${latestConv.messageCount}`); return chatId; } @@ -919,7 +860,7 @@ var ChatbotSDK = (function () { // 3. 生成新的 chatId const newChatId = generateChatId(); currentConfig.chatId = newChatId; - saveCachedChatId(currentConfig.integrateId, currentConfig.userId, newChatId); + saveCachedChatId(activeId, currentConfig.userId, newChatId); logger.info(`生成新 chatId=${newChatId}`); return newChatId; } @@ -971,14 +912,6 @@ var ChatbotSDK = (function () { const darker = adjustColor(config.primaryColor, -15); const lighter = adjustColor(config.primaryColor, 18); const rgb = hexToRgb(config.primaryColor); - // 马卡龙主题渐变色映射 - const themeGradients = { - 'dream-purple': { grad1: '#A78BFA', grad2: '#F0ABFC', glow: '167, 139, 250' }, - 'mint-tech': { grad1: '#6EE7B7', grad2: '#34D399', glow: '52, 211, 153' }, - 'coral-peach': { grad1: '#FB7185', grad2: '#FDBA74', glow: '251, 113, 133' }, - 'sky-blue': { grad1: '#7DD3FC', grad2: '#A78BFA', glow: '125, 211, 252' }, - }; - const themeColors = config.launcherTheme ? themeGradients[config.launcherTheme] : null; return ` --csk-primary: ${config.primaryColor}; --csk-primary-hover: ${darker}; @@ -995,11 +928,6 @@ var ChatbotSDK = (function () { --csk-shadow-bubble: 0 1px 2px rgba(15, 23, 42, 0.06); --csk-border: #ECEEF2; --csk-bg-app: #F6F7F9; - ${themeColors ? ` - --csk-launcher-grad-1: ${themeColors.grad1}; - --csk-launcher-grad-2: ${themeColors.grad2}; - --csk-launcher-glow: ${themeColors.glow}; - ` : ''} `; } /** 简单的颜色加深(按通道加减) */ @@ -1038,7 +966,8 @@ var ChatbotSDK = (function () { width: 60px; height: 60px; border-radius: 50%; - background: var(--csk-bg-user); + /* E5 梦幻拖尾 — 四段极光渐变慢旋 */ + background: conic-gradient(from 0deg, #ec4899 0deg, #8b5cf6 90deg, #3b82f6 180deg, #06b6d4 270deg, #ec4899 360deg); display: flex; align-items: center; justify-content: center; @@ -1052,61 +981,27 @@ var ChatbotSDK = (function () { box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06), 0 4px 16px rgba(0, 0, 0, 0.10), - 0 6px 24px rgba(var(--csk-primary-rgb), 0.28), - 0 0 48px rgba(var(--csk-primary-rgb), 0.12); + 0 6px 24px rgba(236, 72, 153, 0.28), + 0 0 48px rgba(236, 72, 153, 0.12); transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.25s ease, border-color 0.25s ease; - animation: csk-launcher-in 0.55s cubic-bezier(0.22, 1, 0.36, 1) both; + animation: + csk-aurora-spin 5s linear infinite, + csk-launcher-in 0.55s cubic-bezier(0.22, 1, 0.36, 1) both; } .csk-launcher--right { right: 24px; } .csk-launcher--left { left: 24px; } -/* 高光扫过动效 */ -.csk-launcher::before { - content: ''; - position: absolute; - top: -50%; - left: -50%; - width: 40%; - height: 200%; - border-radius: 50%; - background: linear-gradient( - 105deg, - transparent 30%, - rgba(255, 255, 255, 0.28) 48%, - rgba(255, 255, 255, 0.1) 52%, - transparent 70% - ); - transform: rotate(25deg); - animation: csk-shimmer 4s ease-in-out infinite; - pointer-events: none; -} -/* 呼吸色晕 — 配合接地阴影,强化白底上的立体存在感 */ -.csk-launcher::after { - content: ''; - position: absolute; - inset: -18px; - border-radius: 50%; - background: radial-gradient(circle, rgba(var(--csk-primary-rgb), 0.18) 0%, rgba(var(--csk-primary-rgb), 0.05) 45%, transparent 68%); - animation: csk-breathe 3.5s ease-in-out infinite; - pointer-events: none; -} .csk-launcher:hover { transform: translateY(-3px) scale(1.08); border-color: rgba(255, 255, 255, 0.75); box-shadow: 0 2px 6px rgba(0, 0, 0, 0.10), 0 8px 24px rgba(0, 0, 0, 0.14), - 0 10px 36px rgba(var(--csk-primary-rgb), 0.32), - 0 0 56px rgba(var(--csk-primary-rgb), 0.14); -} -.csk-launcher:hover::before { animation-play-state: paused; } -.csk-launcher:hover::after { - animation: none; - inset: -22px; - background: radial-gradient(circle, rgba(var(--csk-primary-rgb), 0.22) 0%, rgba(var(--csk-primary-rgb), 0.06) 45%, transparent 68%); + 0 10px 36px rgba(236, 72, 153, 0.32), + 0 0 56px rgba(236, 72, 153, 0.14); } .csk-launcher:active { transform: scale(0.93); } .csk-launcher svg { @@ -1122,107 +1017,149 @@ var ChatbotSDK = (function () { 60% { transform: scale(1.06) translateY(-2px); } 100% { opacity: 1; transform: scale(1) translateY(0); } } -@keyframes csk-shimmer { - 0%, 100% { left: -60%; opacity: 0; } - 20% { opacity: 1; } - 60% { opacity: 1; } - 80% { left: 120%; opacity: 0; } + +/* ========== E5 梦幻拖尾·全粒子动画系统 ========== */ +/* 极光旋转 */ +@keyframes csk-aurora-spin { + to { transform: rotate(360deg); } } -@keyframes csk-breathe { - 0%, 100% { transform: scale(1); opacity: 1; } - 50% { transform: scale(1.15); opacity: 0.35; } + +/* 粒子通用 */ +.csk-pt { + position: absolute; + border-radius: 50%; + background: #fff; + z-index: 4; + top: 50%; + left: 50%; + pointer-events: none; } -/* SVG 图标内部动效:眨眼 */ -.csk-launcher .csk-ico-eye { - animation: csk-eye-blink 4s ease-in-out infinite; +/* ── 爆散粒子(8颗,不同方向/距离/大小/延迟)── */ +.csk-pt-burst { animation: csk-burst 2.8s ease-in-out infinite; } +.csk-pt-burst:nth-child(1) { width:5px; height:5px; animation-delay:0s; } +.csk-pt-burst:nth-child(2) { width:4px; height:4px; animation-delay:0.35s; } +.csk-pt-burst:nth-child(3) { width:5px; height:5px; animation-delay:0.7s; } +.csk-pt-burst:nth-child(4) { width:3px; height:3px; animation-delay:1.05s; } +.csk-pt-burst:nth-child(5) { width:5px; height:5px; animation-delay:1.4s; } +.csk-pt-burst:nth-child(6) { width:4px; height:4px; animation-delay:1.75s; } +.csk-pt-burst:nth-child(7) { width:3px; height:3px; animation-delay:2.1s; } +.csk-pt-burst:nth-child(8) { width:5px; height:5px; animation-delay:2.45s; } + +@keyframes csk-burst { + 0% { transform: translate(-50%,-50%) scale(0); opacity: 0; } + 12% { transform: translate(-50%,-50%) scale(0); opacity: 0; } + 32% { transform: translate(calc(-50% + var(--dx)), calc(-50% + var(--dy))) scale(1.5); opacity: 1; } + 48% { transform: translate(calc(-50% + var(--dx)), calc(-50% + var(--dy))) scale(0.5); opacity: .55; } + 62% { transform: translate(-50%,-50%) scale(0); opacity: 0; } + 100% { transform: translate(-50%,-50%) scale(0); opacity: 0; } } -@keyframes csk-eye-blink { - 0%, 42%, 48%, 100% { opacity: 1; } - 45% { opacity: 0.1; } + +/* ── 浮游粒子(持续轨道运动,永不停歇)── */ +.csk-pt-float { + animation: csk-float-orbit 3s linear infinite; + box-shadow: 0 0 4px 1px rgba(255,255,255,.5); +} +.csk-pt-float:nth-child(9) { width:3px; height:3px; animation-duration:2.4s; animation-delay:0s; } +.csk-pt-float:nth-child(10) { width:4px; height:4px; animation-duration:3.2s; animation-delay:-0.6s; } +.csk-pt-float:nth-child(11) { width:3px; height:3px; animation-duration:2.8s; animation-delay:-1.2s; } +.csk-pt-float:nth-child(12) { width:4px; height:4px; animation-duration:3.6s; animation-delay:-1.8s; } + +@keyframes csk-float-orbit { + 0% { transform: translate(-50%,-50%) rotate(0deg) translateX(20px) rotate(0deg); } + 100% { transform: translate(-50%,-50%) rotate(360deg) translateX(20px) rotate(-360deg); } +} + +/* ── 心跳粒子(中心脉冲 + 呼吸缩放)── */ +.csk-pt-pulse { + animation: csk-heart-pulse 1.4s ease-in-out infinite; } -/* SVG 图标内部动效:天线能量闪烁 */ -.csk-launcher .csk-ico-antenna { - animation: csk-antenna-glow 2s ease-in-out infinite; +.csk-pt-pulse:nth-child(13) { + width: 8px; height: 8px; + box-shadow: 0 0 14px 4px rgba(255,255,255,.8), 0 0 28px 8px rgba(255,255,255,.3); + animation-delay: 0s; } -@keyframes csk-antenna-glow { - 0%, 100% { opacity: 0.5; } - 50% { opacity: 1; } +.csk-pt-pulse:nth-child(14) { + width: 4px; height: 4px; + box-shadow: 0 0 8px 2px rgba(255,255,255,.6); + animation-delay: 0.7s; } -/* ========== 磨砂玻璃质感悬浮按钮 ========== */ -.csk-launcher--glass { - background: linear-gradient( - 135deg, - var(--csk-launcher-grad-1, var(--csk-primary)) 0%, - var(--csk-launcher-grad-2, var(--csk-primary-light)) 100% - ) !important; - backdrop-filter: blur(12px) saturate(180%); - -webkit-backdrop-filter: blur(12px) saturate(180%); - border: 1.5px solid rgba(255, 255, 255, 0.55) !important; - /* 多层阴影:接地 + 彩色光晕 + 内阴影立体感 */ - box-shadow: - 0 2px 4px rgba(0, 0, 0, 0.04), - 0 6px 20px rgba(0, 0, 0, 0.08), - 0 8px 32px rgba(var(--csk-launcher-glow, var(--csk-primary-rgb)), 0.30), - inset 0 1px 1px rgba(255, 255, 255, 0.35), - inset 0 -1px 1px rgba(0, 0, 0, 0.04) !important; -} -.csk-launcher--glass:hover { - border-color: rgba(255, 255, 255, 0.75) !important; - box-shadow: - 0 4px 8px rgba(0, 0, 0, 0.06), - 0 10px 28px rgba(0, 0, 0, 0.10), - 0 12px 40px rgba(var(--csk-launcher-glow, var(--csk-primary-rgb)), 0.38), - inset 0 1px 2px rgba(255, 255, 255, 0.45), - inset 0 -1px 1px rgba(0, 0, 0, 0.04) !important; +@keyframes csk-heart-pulse { + 0%, 100% { transform: translate(-50%,-50%) scale(0.4); opacity: .3; } + 25% { transform: translate(-50%,-50%) scale(1.6); opacity: 1; } + 50% { transform: translate(-50%,-50%) scale(0.4); opacity: .3; } + 75% { transform: translate(-50%,-50%) scale(0.9); opacity: .6; } } -/* 玻璃按钮的呼吸色晕 — 更鲜艳、更醒目 */ -.csk-launcher--glass::after { - background: radial-gradient( - circle, - rgba(var(--csk-launcher-glow, var(--csk-primary-rgb)), 0.28) 0%, - rgba(var(--csk-launcher-glow, var(--csk-primary-rgb)), 0.10) 40%, - transparent 65% - ) !important; - animation: csk-breathe-glow 3.5s ease-in-out infinite; -} -@keyframes csk-breathe-glow { - 0%, 100% { transform: scale(1); opacity: 0.8; } - 50% { transform: scale(1.22); opacity: 0.25; } +/* ── 甩尾粒子(椭圆轨道 + 变速,近大远小)── */ +.csk-pt-elliptic { + animation: csk-elliptic-orbit 2.6s ease-in-out infinite; + box-shadow: 0 0 5px 1px rgba(255,255,255,.6); } +.csk-pt-elliptic:nth-child(15) { width:4px; height:4px; animation-delay:0s; } +.csk-pt-elliptic:nth-child(16) { width:4px; height:4px; animation-delay:-1.3s; } -/* 玻璃按钮的高光扫过 — 更柔和的玻璃质感 */ -.csk-launcher--glass::before { - background: linear-gradient( - 115deg, - transparent 25%, - rgba(255, 255, 255, 0.35) 45%, - rgba(255, 255, 255, 0.15) 55%, - transparent 75% - ) !important; - width: 50%; - animation: csk-shimmer-glass 5s ease-in-out infinite; -} -@keyframes csk-shimmer-glass { - 0%, 100% { left: -70%; opacity: 0; } - 15% { opacity: 1; } - 55% { opacity: 1; } - 70% { left: 130%; opacity: 0; } +@keyframes csk-elliptic-orbit { + 0% { transform: translate(-50%,-50%) rotate(0deg) translateX(18px) rotate(0deg) scaleY(1); } + 25% { transform: translate(-50%,-50%) rotate(90deg) translateX(18px) rotate(-90deg) scaleY(0.45); } + 50% { transform: translate(-50%,-50%) rotate(180deg) translateX(18px) rotate(-180deg) scaleY(1); } + 75% { transform: translate(-50%,-50%) rotate(270deg) translateX(18px) rotate(-270deg) scaleY(0.45); } + 100% { transform: translate(-50%,-50%) rotate(360deg) translateX(18px) rotate(-360deg) scaleY(1); } } -/* SVG 内部动效:跳动气泡(3 个小圆点交错弹跳) */ -.csk-launcher .csk-ico-bubble { - animation: csk-bubble-jump 1.4s ease-in-out infinite; -} -.csk-launcher .csk-ico-bubble--1 { animation-delay: 0s; } -.csk-launcher .csk-ico-bubble--2 { animation-delay: 0.18s; } -.csk-launcher .csk-ico-bubble--3 { animation-delay: 0.36s; } -@keyframes csk-bubble-jump { - 0%, 100% { transform: translateY(0); } - 40% { transform: translateY(-3.5px); } - 60% { transform: translateY(-1px); } +/* ── 环绕流光(外层慢速光环拖尾)── */ +.csk-pt-ring { + width: 3px; height: 3px; + background: rgba(255,255,255,.9); + box-shadow: 0 0 6px 2px rgba(255,255,255,.7); + animation: csk-ring-orbit 5s linear infinite; +} +.csk-pt-ring:nth-child(17) { animation-delay: 0s; } +.csk-pt-ring:nth-child(18) { animation-delay: -1.25s; } +.csk-pt-ring:nth-child(19) { animation-delay: -2.5s; } +.csk-pt-ring:nth-child(20) { animation-delay: -3.75s; } + +@keyframes csk-ring-orbit { + 0% { transform: translate(-50%,-50%) rotate(0deg) translateX(23px) rotate(0deg) scale(1); opacity: .8; } + 50% { transform: translate(-50%,-50%) rotate(180deg) translateX(23px) rotate(-180deg) scale(1.8); opacity: 1; } + 100% { transform: translate(-50%,-50%) rotate(360deg) translateX(23px) rotate(-360deg) scale(1); opacity: .8; } +} + +/* ── 极光扫光叠加层 ── */ +.csk-sheen { + position: absolute; + inset: 0; + border-radius: 50%; + pointer-events: none; + z-index: 3; + overflow: hidden; +} +.csk-sheen::after { + content: ''; + position: absolute; + top: -60%; left: -60%; + width: 35%; height: 220%; + background: rgba(255,255,255,.15); + transform: rotate(22deg); + animation: csk-shimmer-sweep 2.8s ease-in-out infinite; +} +@keyframes csk-shimmer-sweep { + 0% { left: -60%; } + 40% { left: 50%; } + 60% { left: 50%; } + 100% { left: 130%; } +} + +/* ── 双层极光圈(反向旋转,增加层次)── */ +.csk-dual { + position: absolute; + inset: 0; + border-radius: 50%; + pointer-events: none; + z-index: 2; + background: conic-gradient(from 180deg, transparent 0%, rgba(255,255,255,.08) 35%, transparent 65%); + animation: csk-aurora-spin 2s linear infinite reverse; } /* ========== 聊天弹窗 ========== */ @@ -1357,6 +1294,43 @@ var ChatbotSDK = (function () { .csk-header__btn:active, .csk-history-btn:active { transform: scale(0.92); } +/* ========== 角色选择器 ========== */ +.csk-role-select-wrap { + display: flex; + align-items: center; + flex-shrink: 0; + margin: 0 4px; +} +.csk-role-select { + appearance: none; + -webkit-appearance: none; + padding: 4px 24px 4px 10px; + border: 1px solid rgba(255, 255, 255, 0.35); + border-radius: 8px; + background: rgba(255, 255, 255, 0.15) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='white' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") no-repeat right 6px center; + color: #fff; + font-size: 12px; + font-family: inherit; + cursor: pointer; + outline: none; + transition: background 0.15s, border-color 0.15s; + max-width: 120px; + text-overflow: ellipsis; + white-space: nowrap; +} +.csk-role-select:hover { + background-color: rgba(255, 255, 255, 0.25); + border-color: rgba(255, 255, 255, 0.55); +} +.csk-role-select:focus { + border-color: rgba(255, 255, 255, 0.7); + box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.2); +} +.csk-role-select option { + color: #1F2937; + background: #fff; +} + /* ========== 消息区 ========== */ .csk-messages { flex: 1; @@ -1744,6 +1718,7 @@ var ChatbotSDK = (function () { .csk-msg--ai .csk-msg__bubble .csk-md-h4 { font-size: 14px; } .csk-md-code-block { + position: relative; background: #1E293B; color: #E2E8F0; padding: 12px 14px; @@ -1930,19 +1905,6 @@ var ChatbotSDK = (function () { color: #9CA3AF; font-size: 13px; } -.csk-history-panel__loadmore { - display: block; - width: 100%; - padding: 10px; - border: none; - background: #F9FAFB; - color: #6B7280; - font-size: 12px; - cursor: pointer; - text-align: center; - transition: background 0.15s; -} -.csk-history-panel__loadmore:hover { background: #F3F4F6; } /* ========== 快捷问题芯片 ========== */ .csk-quick-replies { @@ -2022,7 +1984,6 @@ var ChatbotSDK = (function () { } /* ========== 代码块复制按钮 ========== */ -.csk-md-code-block { position: relative; } .csk-md-code-copy { position: absolute; top: 8px; @@ -2080,6 +2041,94 @@ var ChatbotSDK = (function () { box-shadow: 0 5px 14px rgba(239, 68, 68, 0.45) !important; } +/* ========== 窗口缩放拖拽手柄 ========== */ +.csk-resize-handle { + position: absolute; + right: 4px; + bottom: 4px; + width: 20px; + height: 20px; + display: flex; + align-items: flex-end; + justify-content: flex-end; + cursor: nwse-resize; + z-index: 5; + user-select: none; + -webkit-user-select: none; + /* 圆形半透明背景,与整体圆润风格一致 */ + border-radius: 50%; + background: transparent; + transition: background 0.2s ease; +} +.csk-resize-handle:hover { + background: rgba(var(--csk-primary-rgb), 0.08); +} + +/* 三斜线抓手 — 纯 CSS 绘制,用两条间距微调的线,视觉上是三条 */ +.csk-resize-grip { + display: block; + width: 10px; + height: 10px; + background: + linear-gradient(135deg, + transparent 0%, transparent 38%, + #C8CCD2 38%, #C8CCD2 42%, + transparent 42%, transparent 54%, + #C8CCD2 54%, #C8CCD2 58%, + transparent 58%, transparent 70%, + #C8CCD2 70%, #C8CCD2 74%, + transparent 74% + ); + transition: background 0.2s ease; + border-radius: 2px; +} +/* hover 时线条变为主色 */ +.csk-resize-handle:hover .csk-resize-grip { + background: + linear-gradient(135deg, + transparent 0%, transparent 38%, + var(--csk-primary) 38%, var(--csk-primary) 42%, + transparent 42%, transparent 54%, + var(--csk-primary) 54%, var(--csk-primary) 58%, + transparent 58%, transparent 70%, + var(--csk-primary) 70%, var(--csk-primary) 74%, + transparent 74% + ); +} + +/* 暗色模式 */ +.csk-dark .csk-resize-grip { + background: + linear-gradient(135deg, + transparent 0%, transparent 38%, + #5B6070 38%, #5B6070 42%, + transparent 42%, transparent 54%, + #5B6070 54%, #5B6070 58%, + transparent 58%, transparent 70%, + #5B6070 70%, #5B6070 74%, + transparent 74% + ); +} +.csk-dark .csk-resize-handle:hover .csk-resize-grip { + background: + linear-gradient(135deg, + transparent 0%, transparent 38%, + var(--csk-primary-light) 38%, var(--csk-primary-light) 42%, + transparent 42%, transparent 54%, + var(--csk-primary-light) 54%, var(--csk-primary-light) 58%, + transparent 58%, transparent 70%, + var(--csk-primary-light) 70%, var(--csk-primary-light) 74%, + transparent 74% + ); +} + +/* 缩放拖拽中:禁用过渡动画 + 全局光标 */ +.csk-window--resizing { + transition: none !important; + user-select: none; +} +.csk-window--resizing * { cursor: nwse-resize !important; } + /* ========== 暗色模式 ========== */ .csk-root.csk-dark { --csk-bg-ai: #1E1E2E; @@ -2146,6 +2195,10 @@ var ChatbotSDK = (function () { .csk-dark .csk-launcher__badge { box-shadow: 0 0 0 2px #1A1A2E; } .csk-dark .csk-welcome__avatar { box-shadow: 0 6px 18px rgba(var(--csk-primary-rgb), 0.25); } .csk-dark .csk-header__avatar { box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.15); background: rgba(255, 255, 255, 0.1); } +.csk-dark .csk-role-select option { + background: #1A1A2E; + color: #E2E8F0; +} /* Launcher 拖拽态:微放大 + 阴影加深 */ .csk-launcher--dragging { @@ -2154,11 +2207,9 @@ var ChatbotSDK = (function () { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.14), 0 12px 32px rgba(0, 0, 0, 0.18), - 0 14px 40px rgba(var(--csk-primary-rgb), 0.36), - 0 0 56px rgba(var(--csk-primary-rgb), 0.18) !important; + 0 14px 40px rgba(236, 72, 153, 0.36), + 0 0 56px rgba(236, 72, 153, 0.18) !important; } -.csk-launcher--dragging::before, -.csk-launcher--dragging::after { animation-play-state: paused !important; } /* 拖拽中隐藏未读徽章 */ .csk-launcher--dragging .csk-launcher__badge { display: none !important; } /* 吸附过渡动画(释放时的缓动) */ @@ -2347,6 +2398,7 @@ var ChatbotSDK = (function () { left: 0 !important; border-radius: 0; } + .csk-resize-handle { display: none !important; } .csk-window--hidden { transform: translateY(100%); } /* 移动端默认位置,仅在未被 JS 拖拽覆盖时生效 */ .csk-launcher:not([style*="bottom"]) { bottom: 20px; } @@ -2382,7 +2434,6 @@ var ChatbotSDK = (function () { /** * 工具函数模块 */ - /** 生成简短 UUID(取 crypto.randomUUID 前 8 位) */ /** 生成完整 UUID */ function uuid() { if (typeof crypto !== 'undefined' && crypto.randomUUID) { @@ -2419,6 +2470,13 @@ var ChatbotSDK = (function () { }, delay); }; } + /** 格式化时间戳为 HH:mm */ + function formatTime(timestamp) { + const d = new Date(timestamp); + const hh = String(d.getHours()).padStart(2, '0'); + const mm = String(d.getMinutes()).padStart(2, '0'); + return `${hh}:${mm}`; + } /** 获取当前时间戳(毫秒) */ function now() { return Date.now(); @@ -2434,9 +2492,7 @@ var ChatbotSDK = (function () { function createLauncher(config, onClick) { const launcher = document.createElement('div'); launcher.id = 'csk-launcher'; - // 有玻璃主题时追加 --glass 类名,启用磨砂玻璃质感样式 - const glassClass = config.launcherTheme ? ' csk-launcher--glass' : ''; - launcher.className = `csk-launcher csk-launcher--${config.position === 'left-bottom' ? 'left' : 'right'}${glassClass}`; + launcher.className = `csk-launcher csk-launcher--${config.position === 'left-bottom' ? 'left' : 'right'}`; launcher.setAttribute('title', config.title); launcher.setAttribute('aria-label', config.title); launcher.setAttribute('role', 'button'); @@ -2489,6 +2545,39 @@ var ChatbotSDK = (function () { headerInfo.appendChild(statusEl); headerLeft.appendChild(headerAvatar); headerLeft.appendChild(headerInfo); + // === 角色选择器(仅当有多个角色时显示) === + let roleSelect = null; + const roles = config.roles; + if (roles && roles.length > 1) { + roleSelect = document.createElement('select'); + roleSelect.className = 'csk-role-select'; + roleSelect.setAttribute('aria-label', t('role_selector_label')); + for (const role of roles) { + const option = document.createElement('option'); + option.value = String(role.id); + option.textContent = role.name || String(role.id); + if (String(role.id) === String(config.integrateId)) { + option.selected = true; + } + roleSelect.appendChild(option); + } + // onChange 触发自定义事件 + roleSelect.addEventListener('change', () => { + const selectedValue = roleSelect.value; + windowEl.dispatchEvent(new CustomEvent('csk:roleChange', { + detail: { roleId: selectedValue } + })); + }); + // 包装后插入到 headerLeft 之后、actions 之前 + const roleWrap = document.createElement('div'); + roleWrap.className = 'csk-role-select-wrap'; + roleWrap.appendChild(roleSelect); + header.appendChild(headerLeft); + header.appendChild(roleWrap); + } + else { + header.appendChild(headerLeft); + } const actions = document.createElement('div'); actions.className = 'csk-header__actions'; // 历史会话按钮(P2) @@ -2515,7 +2604,6 @@ var ChatbotSDK = (function () { actions.appendChild(historyBtn); actions.appendChild(minimizeBtn); actions.appendChild(closeBtn); - header.appendChild(headerLeft); header.appendChild(actions); // === 消息区 === const messagesContainer = document.createElement('div'); @@ -2694,6 +2782,13 @@ var ChatbotSDK = (function () { ${config.teaserText || t('teaser_text')} `; + // === 缩放拖拽手柄(右下角) === + const resizeHandle = document.createElement('div'); + resizeHandle.className = 'csk-resize-handle'; + resizeHandle.setAttribute('aria-label', t('resize')); + // 纯 CSS 渲染三条斜线,无需 SVG + resizeHandle.innerHTML = ''; + windowEl.appendChild(resizeHandle); return { window: windowEl, messagesContainer, @@ -2705,8 +2800,10 @@ var ChatbotSDK = (function () { welcomeEl, newMsgBtn, teaserEl, + resizeHandle, searchInput, ariaLiveEl, + roleSelect, showLoading, hideLoading, }; @@ -2914,6 +3011,100 @@ var ChatbotSDK = (function () { document.removeEventListener('touchend', onTouchEnd); }; } + // ==================== 窗口缩放支持 ==================== + /** 窗口最小尺寸 */ + const MIN_WIDTH = 300; + const MIN_HEIGHT = 300; + /** + * 启用窗口右下角拖拽缩放 + * @param handleEl 缩放手柄元素 + * @param windowEl 聊天窗口元素 + * @param onResizeEnd 缩放结束回调,返回最终宽高用于持久化 + * @returns 清理函数 + */ + function enableResize(handleEl, windowEl, onResizeEnd) { + let resizing = false; + let startX = 0; + let startY = 0; + let startWidth = 0; + let startHeight = 0; + const onPointerDown = (clientX, clientY) => { + resizing = true; + startX = clientX; + startY = clientY; + const rect = windowEl.getBoundingClientRect(); + startWidth = rect.width; + startHeight = rect.height; + windowEl.classList.add('csk-window--resizing'); + document.addEventListener('mousemove', onMouseMove); + document.addEventListener('mouseup', onMouseUp); + document.addEventListener('touchmove', onTouchMove, { passive: false }); + document.addEventListener('touchend', onTouchEnd); + }; + const onPointerMove = (clientX, clientY, e) => { + if (!resizing) + return; + if (e) + e.preventDefault(); + const dx = clientX - startX; + const dy = clientY - startY; + const newWidth = Math.max(MIN_WIDTH, Math.min(startWidth + dx, window.innerWidth - 24)); + const newHeight = Math.max(MIN_HEIGHT, Math.min(startHeight + dy, window.innerHeight - 24)); + windowEl.style.width = `${newWidth}px`; + windowEl.style.height = `${newHeight}px`; + // 同步更新 CSS 变量(供内部布局使用) + windowEl.style.setProperty('--csk-window-width', `${newWidth}px`); + windowEl.style.setProperty('--csk-window-height', `${newHeight}px`); + // 拖拽缩放后清除 bottom/right 定位,避免与固定定位冲突 + windowEl.style.bottom = ''; + windowEl.style.right = ''; + }; + const onPointerUp = () => { + if (!resizing) + return; + resizing = false; + windowEl.classList.remove('csk-window--resizing'); + document.removeEventListener('mousemove', onMouseMove); + document.removeEventListener('mouseup', onMouseUp); + document.removeEventListener('touchmove', onTouchMove); + document.removeEventListener('touchend', onTouchEnd); + const rect = windowEl.getBoundingClientRect(); + if (onResizeEnd) { + onResizeEnd({ width: rect.width, height: rect.height }); + } + }; + // 鼠标事件(DOM 事件监听器包装) + function onMouseMove(e) { onPointerMove(e.clientX, e.clientY, e); } + function onMouseUp() { onPointerUp(); } + // 鼠标事件 + function onMouseDown(e) { + e.preventDefault(); + e.stopPropagation(); + onPointerDown(e.clientX, e.clientY); + } + // 触摸事件 + function onTouchStart(e) { + e.preventDefault(); + e.stopPropagation(); + if (e.touches.length === 1) + onPointerDown(e.touches[0].clientX, e.touches[0].clientY); + } + function onTouchMove(e) { + if (e.touches.length === 1) + onPointerMove(e.touches[0].clientX, e.touches[0].clientY, e); + } + function onTouchEnd() { onPointerUp(); } + handleEl.addEventListener('mousedown', onMouseDown); + handleEl.addEventListener('touchstart', onTouchStart, { passive: false }); + return () => { + handleEl.removeEventListener('mousedown', onMouseDown); + handleEl.removeEventListener('touchstart', onTouchStart); + document.removeEventListener('mousemove', onMouseMove); + document.removeEventListener('mouseup', onMouseUp); + document.removeEventListener('touchmove', onTouchMove); + document.removeEventListener('touchend', onTouchEnd); + }; + } // ==================== 消息渲染 ==================== /** 渲染用户消息气泡 */ function renderUserBubble(container, text, timestamp) { @@ -3309,6 +3500,8 @@ var ChatbotSDK = (function () { const metaEl = document.createElement('div'); metaEl.className = 'csk-history-item__meta'; const metaParts = []; + if (item.roleName) + metaParts.push(item.roleName); if (item.messageCount !== undefined) metaParts.push(`${item.messageCount} 条消息`); if (item.lastMessageTime) @@ -3354,13 +3547,6 @@ var ChatbotSDK = (function () { function scrollToBottom(container) { container.scrollTop = container.scrollHeight; } - /** 格式化时间戳 */ - function formatTime(timestamp) { - const d = new Date(timestamp); - const hh = String(d.getHours()).padStart(2, '0'); - const mm = String(d.getMinutes()).padStart(2, '0'); - return `${hh}:${mm}`; - } const STORAGE_PREFIX = 'csk_history_'; const MAX_MESSAGES = 200; @@ -3623,6 +3809,7 @@ var ChatbotSDK = (function () { let sendBtn$1 = null; let clearBtn$1 = null; let categorySelect$1 = null; + let roleSelect = null; let historyPanel$1 = null; let welcomeEl = null; let newMsgBtn = null; @@ -3654,6 +3841,7 @@ var ChatbotSDK = (function () { sendBtn$1 = dom.sendBtn; clearBtn$1 = dom.clearBtn; categorySelect$1 = dom.categorySelect; + roleSelect = dom.roleSelect; historyPanel$1 = dom.historyPanel; welcomeEl = dom.welcomeEl; newMsgBtn = dom.newMsgBtn; @@ -3717,8 +3905,8 @@ var ChatbotSDK = (function () { })); renderHistory(); logger.info(`从后端加载 ${messages.length} 条历史消息`); - // 同步到 localStorage - saveMessages(config$1.integrateId, messages); + // 同步到 localStorage(使用当前活跃角色 ID) + saveMessages(getActiveIntegrateId(), messages); } } catch (err) { @@ -4255,6 +4443,68 @@ var ChatbotSDK = (function () { useRag = (_a = config$1 === null || config$1 === void 0 ? void 0 : config$1.enableRag) !== null && _a !== void 0 ? _a : true; logger.lifecycleCategoryChange(categoryId !== null && categoryId !== void 0 ? categoryId : '全部'); } + // ==================== 角色切换 ==================== + /** + * 切换客服角色 + * - 清理当前对话 + 生成新 chatId + * - 更新 API 层的 roleId 参数 + * - 重新加载新角色的对话历史 + */ + async function switchRole(newRoleId) { + if (!config$1 || !messagesContainer$1) + return; + const oldRoleId = getActiveIntegrateId(); + // 1. 判断是否与当前角色相同(防重复切换) + if (newRoleId === oldRoleId) + return; + // 2. 保存当前消息到 localStorage(与旧 integrateId 关联) + if (messages.length > 0) { + saveMessages(oldRoleId, messages); + } + // 3. 中断正在进行的流式请求 + if (abortController) { + abortController.abort(); + abortController = null; + } + isSending = false; + setSendButtonMode('send'); + // 4. 清空消息数组和 DOM + messages = []; + const msgNodes = messagesContainer$1.querySelectorAll('.csk-msg, .csk-loading'); + msgNodes.forEach(el => el.remove()); + // 5. 清空新旧角色的 localStorage 缓存(消息 + chatId),必须在 setActiveRoleId 之前 + clearMessages(oldRoleId); + saveCachedChatId(oldRoleId, config$1.userId, undefined); + clearMessages(newRoleId); + saveCachedChatId(newRoleId, config$1.userId, undefined); + // 6. 更新 API 层的 integrateId(必须在 initChatId 之前) + setActiveRoleId(newRoleId); + // 7. 更新角色下拉框高亮项 + if (roleSelect) { + roleSelect.value = newRoleId; + } + // 8. 强制查询后端会话列表(跳过 localStorage 缓存) + const foundChatId = await initChatId(true); + logger.info(`角色切换 initChatId 完成 chatId=${foundChatId} roleId=${newRoleId}`); + // 9. 从后端加载新角色的对话历史 + if (foundChatId) { + try { + await loadHistoryFromBackend(); + } + catch (err) { + logger.warn(`切换角色后加载后端历史失败 roleId=${newRoleId}`, err); + } + } + // 10. 隐藏清空按钮 + 更新欢迎态 + if (clearBtn$1 && messages.length === 0) + clearBtn$1.style.display = 'none'; + updateEmptyState(); + // 11. 重置滚动状态 + isNearBottom = true; + if (messagesContainer$1) + scrollToBottom(messagesContainer$1); + logger.info(`角色切换完成 ${oldRoleId} -> ${newRoleId}`); + } // ==================== 会话管理面板 ==================== /** 加载会话列表并渲染 */ async function loadHistoryConversations() { @@ -4265,10 +4515,12 @@ var ChatbotSDK = (function () { return; listEl.innerHTML = `
${o}`),`${ae}${s}\0`});const r=[];t=t.replace(/`([^`\n]+)`/g,(e,n)=>{const t=r.length;return r.push(`${H(n)}`),`${ie}${t}\0`}),t=H(t),t=le(t,ae,n),t=le(t,ie,r);const s=t.split("\n"),o=[];let a=!1,i="",c=!1,l=[];for(let e=0;e')),o.push(`"),c=!1)}}function le(e,n,t){return e.replace(new RegExp(n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"(\\d+)\0","g"),(e,n)=>t[parseInt(n)]||"")}let de,pe=null,ue=[],ge=null,ke=null,me=null,he=null,be=null,fe=null,ye=null,xe=null,ve=null,we=null,_e=null,Ee=null,Ce=null,Ie=!1,$e=null,Le=!0,Ae=[],Se="",Ne=!1;function Fe(e,n){pe=e,ge=n.messagesContainer,ke=n.inputEl,me=n.inputEl.parentElement,he=n.sendBtn,be=n.clearBtn,fe=n.categorySelect,ye=n.historyPanel,xe=n.welcomeEl,ve=n.newMsgBtn,we=n.searchInput,_e=n.ariaLiveEl,Ee=n.showLoading,Ce=n.hideLoading,de=e.categoryId,Ne=e.enableRag,function(){if(!ke||!he)return;he.addEventListener("click",()=>{Ie&&$e?$e.abort():Re()}),ke.addEventListener("keydown",e=>{"Enter"!==e.key||e.shiftKey||e.isComposing||(e.preventDefault(),Re())}),ke.addEventListener("input",()=>{je(),function(){if(!ke)return;ke.style.height="auto",ke.style.height=`${Math.min(ke.scrollHeight,120)}px`}()}),ke.addEventListener("focus",()=>{me&&me.classList.add("csk-input-wrap--focus")}),ke.addEventListener("blur",()=>{me&&me.classList.remove("csk-input-wrap--focus")}),be&&be.addEventListener("click",()=>function(){if(!pe)return;if(!confirm(m("clear_confirm")))return;if(ue=[],ge){ge.querySelectorAll(".csk-msg, .csk-loading").forEach(e=>e.remove())}be&&(be.style.display="none");He(),oe(pe.integrateId);const e=function(){const e="undefined"!=typeof crypto&&crypto.randomUUID?crypto.randomUUID().substring(0,8):Math.random().toString(36).substring(2,10);return`sdk_${Date.now()}_${e}`}();b(e),M(pe.integrateId,pe.userId,e),o.lifecycleClear(pe.integrateId),o.info(`新 chatId=${e}`)}())}(),function(){if(!ge)return;ge.addEventListener("scroll",()=>{if(!ge)return;const{scrollTop:e,scrollHeight:n,clientHeight:t}=ge;Le=n-e-t<80,Le&&Te()}),ve&&ve.addEventListener("click",()=>{ge&&ne(ge),Le=!0,Te()})}(),function(){if(!we)return;we.addEventListener("input",()=>{Se=we.value.trim().toLowerCase(),qe()})}(),e.showCategorySwitch&&fe&&async function(){if(!fe)return;try{const e=await async function(){const e=y("/category/tree");try{const n=await v(e);if(!n.ok)throw new w(_(n.status),`http_${n.status}`);const t=await n.json();return t.success&&Array.isArray(t.data)?(o.info(`加载分类树成功 count=${t.data.length}`),t.data):[]}catch(e){return e instanceof w?o.error(`加载分类树失败: ${e.message}`):o.error("加载分类树失败",e),[]}}();if(0===e.length)return;fe.innerHTML=``;const n=(e,t=0)=>{for(const r of e){const e=document.createElement("option");e.value=String(r.id),e.textContent=`${" ".repeat(t)}${r.name}`,void 0!==de&&String(r.id)===String(de)&&(e.selected=!0),fe.appendChild(e),r.children&&r.children.length>0&&n(r.children,t+1)}};n(e),o.info(`知识库分类加载成功 count=${e.length}`)}catch(e){o.error(m("category_load_error"),e)}}()}async function Me(){if(pe&&ge&&(await N(),await async function(){if(!pe||!ge)return;const e=f();if(!e)return;try{const n=await L(e);n.messages.length>0&&(ue=n.messages.map((e,n)=>({id:z(),role:"USER"===e.messageType?"user":"ai",content:e.content,timestamp:new Date(e.createTime).getTime()})),Ue(),o.info(`从后端加载 ${ue.length} 条历史消息`),se(pe.integrateId,ue))}catch(e){o.warn("从后端加载历史消息失败",e)}}(),0===ue.length)){const e=function(e){try{const n=localStorage.getItem(re(e));if(!n)return[];const t=JSON.parse(n);return t&&Array.isArray(t.messages)?(o.info(`加载历史消息 integrateId=${e} count=${t.messages.length}`),t.messages):[]}catch(e){return o.warn("加载会话历史失败",e),[]}}(pe.integrateId);e.length>0&&(ue=e,Ue(),o.info(`从本地缓存恢复 ${e.length} 条消息`))}}function Te(){ve&&ve.classList.add("csk-newmsg--hidden")}function De(){ge&&(Le?ne(ge):ve&&ve.classList.remove("csk-newmsg--hidden"))}function Be(e){he&&("stop"===e?(he.classList.add("csk-send-btn--stop"),he.removeAttribute("disabled"),he.setAttribute("title",m("stop")),he.setAttribute("aria-label",m("stop")),he.innerHTML=''):(he.classList.remove("csk-send-btn--stop"),he.setAttribute("title",m("send")),he.setAttribute("aria-label",m("send")),he.innerHTML='',je()))}function qe(){const e=null==ye?void 0:ye.querySelector("#csk-history-list");if(!e||!pe)return;ee(e,Se?Ae.filter(e=>(e.lastMessagePreview||e.chatId||e.id||"").toLowerCase().includes(Se)):Ae,e=>{Ye(e)},e=>{window.open(S(e),"_blank")},async e=>{if(!confirm(m("history_delete_confirm")))return;await A(e)&&(e===f()&&(ue=[],ge&&ge.querySelectorAll(".csk-msg, .csk-loading").forEach(e=>e.remove()),be&&(be.style.display="none"),He()),Ae=Ae.filter(n=>(n.chatId||n.id)!==e),qe())},f())}function ze(e,n){if(!ge)return;const t=ue.find(n=>n.id===e&&"ai"===n.role);if(!t)return;const r=t.feedback===n?void 0:n;t.feedback=r;const s=ge.querySelector(`[data-csk-msg-id="${e}"]`);if(s&&X(s,r),pe&&se(pe.integrateId,ue),r){const n="up"===r?"THUMBS_UP":"THUMBS_DOWN";(async function(e,n){if(!h)return!1;const t=y("/feedback");try{const r=await v(t,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({messageId:String(e),conversationId:h.chatId,feedbackType:n})});return r.ok?(await r.json()).success||!1:(o.error(`反馈提交失败 status=${r.status}`),!1)}catch(e){return o.error("反馈提交异常",e),!1}})(String(e),n).then(n=>{n?o.info(`消息反馈已提交 msgId=${e} value=${r}`):o.warn(`消息反馈提交失败 msgId=${e}(本地状态已更新)`)})}else o.info(`消息反馈已取消 msgId=${e}`)}function He(){if(!xe)return;const e=ue.length>0||ge&&ge.querySelector(".csk-msg");xe.style.display=e?"none":""}function je(){if(!he||!ke)return;ke.value.trim().length>0&&!Ie?he.removeAttribute("disabled"):he.setAttribute("disabled","true")}async function Re(){if(!ke||!pe||Ie)return;const e=ke.value.trim();if(""===e)return;ke.value="",je(),ke.style.height="auto";const n=j(),t={id:z(),role:"user",content:e,timestamp:n};ge&&Y(ge,e,n),ue.push(t),He(),be&&ue.length>0&&(be.style.display="inline-flex"),ge&&De(),await Pe(e)}async function Pe(e){if(!pe||!ge)return;Ie=!0,Be("stop"),pe.chatId||await N();const n=j(),t=Ne;Ee&&Ee(),ge&&De();const r=z();let s="";try{pe.streaming?s=await async function(e,n,t,r){$e=new AbortController;const s=$e.signal;return new Promise((o,a)=>{let i=null,c=null,l="",d=!1;C(e,e=>{if(l+=e,!d&&ge){Ce&&Ce();const{wrapper:e,bubble:t}=function(e,n,t){const r=document.createElement("div");r.className="csk-msg csk-msg--ai csk-msg--streaming",t&&(r.dataset.cskMsgId=t);const s=document.createElement("div");s.className="csk-msg__avatar csk-msg__avatar--ai",s.innerHTML=R;const o=document.createElement("div");o.className="csk-msg__content";const a=document.createElement("div");a.className="csk-msg__bubble",a.innerHTML="";const i=document.createElement("div");return i.className="csk-msg__time",i.textContent=te(n),o.appendChild(a),o.appendChild(V(a,r)),o.appendChild(i),r.appendChild(s),r.appendChild(o),e.appendChild(r),{wrapper:r,bubble:a}}(ge,n,r);c=e,i=t,d=!0}i&&(i.textContent=l,function(e){if(e.querySelector(".csk-caret"))return;const n=document.createElement("span");n.className="csk-caret",n.setAttribute("aria-hidden","true"),e.appendChild(n)}(i)),ge&&De()},()=>{if(c&&i){if(!d&&""===l)return void E(e).then(o).catch(a);l&&(i.innerHTML=ce(l)),Q(c,i)}o(l)},e=>{l.length>0?(i&&(i.innerHTML=ce(l+"\n\n"+m("stream_interrupted")),c&&Q(c,i)),o(l)):a(e)},de,t,s)})}(e,n,t,r):(s=await E(e),Ce&&Ce(),ge&&O(ge,s,n,ce,r));const a={id:r,role:"ai",content:s,timestamp:n};ue.push(a),se(pe.integrateId,ue),ge&&De(),function(e){if(!_e)return;const n=e.length>120?e.substring(0,120)+"...":e;_e.textContent=m("new_msg_announce")+":"+n}(s),ge&&ge.dispatchEvent(new CustomEvent("csk:newMessage",{bubbles:!0,detail:{msg:a}})),t&&async function(e,n){try{const t=await I(e,de);if(t.length>0){const e=t.map(e=>{var n,t;return{documentId:e.documentId||"",title:e.title||"",sourceName:e.sourceName||"",chunkIndex:null!==(n=e.chunkIndex)&&void 0!==n?n:0,score:null!==(t=e.score)&&void 0!==t?t:0,snippet:e.snippet||""}});if(n.sources=e,ge){const n=ge.querySelector(".csk-msg--ai:last-of-type");n&&Z(n,e)}pe&&se(pe.integrateId,ue)}}catch(e){o.warn("获取引用来源失败",e)}}(e,a)}catch(e){Ce&&Ce();const n=e instanceof w?e.message:m("error_send");if(ge){const e=document.createElement("div");e.className="csk-msg csk-msg--ai";const t=document.createElement("div");t.className="csk-msg__bubble",t.style.color="#DC2626",t.textContent=`⚠ ${n}`,e.appendChild(t),ge.appendChild(e)}o.error(`发送失败 integrateId=${pe.integrateId}`,e)}finally{Ie=!1,$e=null,Be("send"),je()}}function Ue(){if(!ge)return;const e=ge.querySelector(".csk-history-panel");ge.querySelectorAll(".csk-msg, .csk-loading").forEach(e=>e.remove());for(const e of ue)if("user"===e.role)Y(ge,e.content,e.timestamp);else{const n=O(ge,e.content,e.timestamp,ce,e.id);e.sources&&e.sources.length>0&&Z(n,e.sources),e.feedback&&X(n,e.feedback)}Le=!0,ne(ge),be&&ue.length>0&&(be.style.display="inline-flex"),He(),e&&!ge.contains(e)&&ge.appendChild(e)}async function Ye(e){if(!pe||!ge)return;o.info(`切换到会话 conversationId=${e}`),b(e),M(pe.integrateId,pe.userId,e),ye&&ye.classList.add("csk-history-panel--hidden"),ue=[];ge.querySelectorAll(".csk-msg, .csk-loading").forEach(e=>e.remove());try{const n=await L(e);n.messages.length>0&&(ue=n.messages.map(e=>({id:z(),role:"USER"===e.messageType?"user":"ai",content:e.content,timestamp:new Date(e.createTime).getTime()})),Ue(),o.info(`加载会话 ${e} 的 ${ue.length} 条消息`),se(pe.integrateId,ue))}catch(n){o.warn(`加载会话消息失败 conversationId=${e}`,n)}be&&ue.length>0&&(be.style.display="inline-flex"),He()}let Oe=null,We=!1,Ge=null,Ve=null,Xe=null,Ke=null,Je=null,Qe=null,Ze=null,en=null,nn=null,tn=null,rn=null,sn=null,on=null,an=null,cn=null,ln=null,dn=null;function pn(){if(!an||!Ge)return;const e=Ge.getBoundingClientRect(),n=on?"left"===on.side:"left-bottom"===(null==Oe?void 0:Oe.position);n?(an.style.left=`${e.left}px`,an.style.right="auto"):(an.style.right=window.innerWidth-e.right+"px",an.style.left="auto"),an.style.bottom=window.innerHeight-e.top+8+"px",an.classList.toggle("csk-teaser--left",!!n),an.classList.toggle("csk-teaser--right",!n)}function un(){if(!an)return;an.classList.add("csk-teaser--hidden"),an.removeEventListener("click",gn);const e=an.querySelector(".csk-teaser__close");e&&e.removeEventListener("click",kn)}function gn(e){e.target.closest(".csk-teaser__close")||(un(),fn())}function kn(e){e.stopPropagation(),un()}function mn(){an&&!an.classList.contains("csk-teaser--hidden")&&pn()}const hn='button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';function bn(e){"Escape"===e.key&&Ve&&!Ve.classList.contains("csk-window--hidden")&&yn(),function(e){if(!Ve||Ve.classList.contains("csk-window--hidden"))return;if("Tab"!==e.key)return;const n=Array.from(Ve.querySelectorAll(hn));if(0===n.length)return;const t=n[0],r=n[n.length-1];e.shiftKey?document.activeElement===t&&(e.preventDefault(),r.focus()):document.activeElement===r&&(e.preventDefault(),t.focus())}(e)}function fn(){Ve&&(Ve.classList.remove("csk-window--hidden"),un(),ln&&ln.classList.add("csk-launcher__badge--hidden"),setTimeout(()=>{Ke&&Ke.focus()},80))}function yn(){Ve&&Ve.classList.add("csk-window--hidden")}function xn(){Ve&&(Ve.classList.contains("csk-window--hidden")?fn():yn())}function vn(e){return`csk_position_${e}`}function wn(e){return`csk_launcher_pos_${e}`}function _n(e){Ve&&(Ve.classList.remove("csk-window--right","csk-window--left"),Ve.classList.add(`csk-window--${e}`),an&&(an.classList.remove("csk-teaser--right","csk-teaser--left"),an.classList.add(`csk-teaser--${e}`)))}const En={init:function(e){if(We)return void o.warn("SDK 已初始化,请先调用 destroy() 再重新初始化");const t=function(e){var n,t,r,s,i,g,k,m,h,b,f;if(!e.integrateId||"string"!=typeof e.integrateId&&"number"!=typeof e.integrateId||"string"==typeof e.integrateId&&""===e.integrateId.trim())return o.error('integrateId 是必传参数(对应后端 roleId 客服角色 ID),请检查 init() 调用。示例:ChatbotSDK.init({ integrateId: 1, requestDomain: "https://api.example.com" })'),null;if(!e.requestDomain||"string"!=typeof e.requestDomain||""===e.requestDomain.trim())return o.error('requestDomain 是必传参数,请检查 init() 调用。示例:ChatbotSDK.init({ integrateId: 1, requestDomain: "https://api.example.com" })'),null;try{new URL(e.requestDomain)}catch(n){return o.error(`requestDomain 不是合法的 URL 格式:${e.requestDomain}。请提供完整的域名,如 https://api.example.com`),null}const y=String(e.integrateId).trim(),x=e.launcherTheme||void 0;let v=e.launcherIcon||a,w=e.primaryColor||"#4F46E5";x&&!e.launcherIcon&&(v={"dream-purple":c,"mint-tech":l,"coral-peach":d,"sky-blue":p}[x]||a,e.primaryColor||(w=u[x]||"#4F46E5"));const _={integrateId:y,requestDomain:e.requestDomain.replace(/\/+$/,""),userId:e.userId,categoryId:e.categoryId,showCategorySwitch:null!==(n=e.showCategorySwitch)&&void 0!==n&&n,title:e.title||"AI 智能助手",width:null!==(t=e.width)&&void 0!==t?t:380,height:Math.max(300,null!==(r=e.height)&&void 0!==r?r:520),position:"left-bottom"===e.position?"left-bottom":"right-bottom",primaryColor:w,launcherTheme:x,launcherIcon:v,showClear:null===(s=e.showClear)||void 0===s||s,showAdminPanel:null!==(i=e.showAdminPanel)&&void 0!==i&&i,quickReplies:Array.isArray(e.quickReplies)?e.quickReplies.map(e=>String(e).trim()).filter(Boolean):[],theme:"dark"===e.theme?"dark":"light",showTeaser:null===(g=e.showTeaser)||void 0===g||g,teaserText:"string"==typeof e.teaserText&&e.teaserText.trim()||"",streaming:null===(k=e.streaming)||void 0===k||k,enableRag:null===(m=e.enableRag)||void 0===m||m,rewriteStrategy:e.rewriteStrategy||"REWRITE",locale:e.locale||"zh-CN",debug:null===(h=e.debug)||void 0===h||h,sound:null!==(b=e.sound)&&void 0!==b&&b,notification:null!==(f=e.notification)&&void 0!==f&&f,onError:"function"==typeof e.onError?e.onError:void 0,onReady:"function"==typeof e.onReady?e.onReady:void 0,onMessage:"function"==typeof e.onMessage?e.onMessage:void 0,chatId:""};return o.info(`配置解析完成 integrateId(=roleId)=${_.integrateId} userId(=accountId)=${_.userId||"(未设置)"} requestDomain=${_.requestDomain}`),_}(e);if(!t)return;var s;Oe=t,function(e){if(g[e])k=e;else{const n=e.split("-")[0],t=Object.keys(g).find(e=>e.startsWith(n));t&&(k=t)}}(Oe.locale),s=Oe.debug,n=s,r(Oe.onError),function(e){h=e}(Oe),q(Oe),Ge=P(Oe,xn),document.body.appendChild(Ge),ln=Ge.querySelector(".csk-launcher__badge"),function(e,n){try{const t=localStorage.getItem(wn(e));if(!t)return;const r=JSON.parse(t);if("left"!==r.side&&"right"!==r.side||"number"!=typeof r.bottom)return;const s=window.innerHeight-76,o=Math.max(16,Math.min(r.bottom,s));n.classList.remove("csk-launcher--right","csk-launcher--left"),n.classList.add(`csk-launcher--${r.side}`),n.style.bottom=`${o}px`,on={side:r.side,bottom:o}}catch(e){}}(Oe.integrateId,Ge),sn=function(e,n,t){let r=!1,s=0,o=0,a=!1,i=0,c=0,l=0,d=!1;function p(){const n=parseFloat(getComputedStyle(e).bottom);return isNaN(n)?24:n}function u(n,t){if(d)return;r=!0,a=!1,s=n,o=t,i=p();const u=e.style,g=getComputedStyle(e);c=u.right?parseFloat(u.right):parseFloat(g.right),l=u.left?parseFloat(u.left):parseFloat(g.left)}function g(n,t,d){if(!r)return;const p=n-s,u=t-o;if(!a){if(Math.abs(p)<5&&Math.abs(u)<5)return;a=!0,e.classList.add("csk-launcher--dragging"),e.classList.remove("csk-launcher--right","csk-launcher--left"),e.style.transition="none",!isNaN(c)&&c>=0&&isNaN(l)?(e.style.right=`${c}px`,e.style.left="auto"):!isNaN(l)&&l>=0&&(e.style.left=`${l}px`,e.style.right="auto")}if(a){d.preventDefault();const n=Math.max(16,Math.min(i-u,U()));e.style.bottom=`${n}px`}}function k(){if(!r)return;if(r=!1,!a)return;const n=parseFloat(e.style.bottom)||p(),s=Math.max(16,Math.min(n,U())),o=e.getBoundingClientRect(),i=o.left+o.width/2<=window.innerWidth/2?"left":"right";e.classList.remove("csk-launcher--dragging"),e.classList.add(`csk-launcher--${i}`,"csk-launcher--snap"),e.style.left="",e.style.right="",e.style.bottom=`${s}px`,d=!0;const c=()=>{e.removeEventListener("transitionend",c),e.classList.remove("csk-launcher--snap"),e.style.transition="",d=!1};e.addEventListener("transitionend",c),setTimeout(()=>{d&&c()},350);const l=n=>{n.stopPropagation(),e.removeEventListener("click",l,!0)};e.addEventListener("click",l,!0);const u=e.querySelector(".csk-launcher__badge");u&&(u.style.display=""),t&&t({side:i,bottom:s})}function m(e){u(e.clientX,e.clientY)}function h(e){g(e.clientX,e.clientY,e)}function b(){k()}function f(e){1===e.touches.length&&u(e.touches[0].clientX,e.touches[0].clientY)}function y(e){1===e.touches.length&&g(e.touches[0].clientX,e.touches[0].clientY,e)}function x(){k()}return e.addEventListener("mousedown",m),document.addEventListener("mousemove",h),document.addEventListener("mouseup",b),e.addEventListener("touchstart",f,{passive:!0}),document.addEventListener("touchmove",y,{passive:!1}),document.addEventListener("touchend",x),()=>{e.removeEventListener("mousedown",m),document.removeEventListener("mousemove",h),document.removeEventListener("mouseup",b),e.removeEventListener("touchstart",f),document.removeEventListener("touchmove",y),document.removeEventListener("touchend",x)}}(Ge,0,e=>{on=e,function(e,n){try{localStorage.setItem(wn(e),JSON.stringify(n))}catch(e){}}(Oe.integrateId,e),function(){if(!Ve)return;Ve.style.left="",Ve.style.top="",Ve.style.right="",Ve.style.bottom=""}(),_n(e.side),an&&!an.classList.contains("csk-teaser--hidden")&&pn()}),on&&_n(on.side);const i=function(e){const n=document.createElement("div");n.id="csk-window",n.className=`csk-root csk-window csk-window--${"left-bottom"===e.position?"left":"right"} csk-window--hidden${"dark"===e.theme?" csk-dark":""}`,n.setAttribute("role","dialog"),n.setAttribute("aria-label",e.title),n.setAttribute("aria-modal","false");const t=document.createElement("div");t.className="csk-header";const r=document.createElement("div");r.className="csk-header__left";const s=document.createElement("div");s.className="csk-header__avatar",s.innerHTML=R;const o=document.createElement("div");o.className="csk-header__info";const a=document.createElement("span");a.className="csk-header__title",a.textContent=e.title;const i=document.createElement("span");i.className="csk-header__status",i.innerHTML=`${m("status_online")}`,o.appendChild(a),o.appendChild(i),r.appendChild(s),r.appendChild(o);const c=document.createElement("div");c.className="csk-header__actions";const l=document.createElement("button");l.className="csk-history-btn",l.setAttribute("title",m("history_title")),l.innerHTML='';const d=document.createElement("button");d.className="csk-header__btn csk-header__btn--minimize",d.setAttribute("title",m("minimize")),d.innerHTML='',d.addEventListener("click",()=>{n.classList.add("csk-window--hidden")});const p=document.createElement("button");p.className="csk-header__btn csk-header__btn--close",p.setAttribute("title",m("close")),p.innerHTML='',p.addEventListener("click",()=>{n.classList.add("csk-window--hidden")}),c.appendChild(l),c.appendChild(d),c.appendChild(p),t.appendChild(r),t.appendChild(c);const u=document.createElement("div");u.id="csk-messages",u.className="csk-messages";const g=document.createElement("div");if(g.className="csk-welcome",g.innerHTML=`\n${d(r[1])}
`);continue}c&&g();const k=n.match(/^[\-\*]\s+(.+)/);if(k){p(),g(),a&&"ul"===i||(u(),a=!0,i="ul",o.push('')),o.push(`
":""),a=!1,i="")}function g(){c&&(o.push("- ${d(k[1])}
`);continue}const m=n.match(/^\d+\.\s+(.+)/);m?(p(),g(),a&&"ol"===i||(u(),a=!0,i="ol",o.push('')),o.push(`
- ${d(m[1])}
`)):""!==n.trim()?/^(\*{3,}|-{3,}|_{3,})$/.test(n.trim())?(p(),u(),g(),o.push('
')):(u(),g(),l.push(d(n))):(p(),u())}return p(),u(),g(),o.join("\n");function d(e){return e=(e=(e=(e=(e=(e=e.replace(/\*\*(.+?)\*\*/g,"$1")).replace(/__(.+?)__/g,"$1")).replace(/\*(.+?)\*/g,"$1")).replace(/(?$1")).replace(/~~(.+?)~~/g,"$1")).replace(/\[([^\]]+)\]\(([^)]+)\)/g,(e,n,t)=>`${n}`)}function p(){l.length>0&&(o.push(`${l.join("
`),l=[])}function u(){a&&(o.push("ul"===i?"
")}
${o}`),`${te}${s}\0`});const r=[];t=t.replace(/`([^`\n]+)`/g,(e,n)=>{const t=r.length;return r.push(`${D(n)}`),`${re}${t}\0`}),t=D(t),t=oe(t,te,n),t=oe(t,re,r);const s=t.split("\n"),o=[];let a=!1,i="",c=!1,l=[];for(let e=0;e')),o.push(`"),c=!1)}}function oe(e,n,t){return e.replace(new RegExp(n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"(\\d+)\0","g"),(e,n)=>t[parseInt(n)]||"")}let ae,ie=null,ce=[],le=null,de=null,pe=null,ue=null,he=null,me=null,ke=null,ge=null,be=null,fe=null,ye=null,xe=null,ve=null,we=null,_e=!1,Ee=null,Ce=!0,Ie=[],Le="",$e=!1;function Ae(e,n){ie=e,le=n.messagesContainer,de=n.inputEl,pe=n.inputEl.parentElement,ue=n.sendBtn,he=n.clearBtn,me=n.categorySelect,ke=n.roleSelect,ge=n.historyPanel,be=n.welcomeEl,fe=n.newMsgBtn,ye=n.searchInput,xe=n.ariaLiveEl,ve=n.showLoading,we=n.hideLoading,ae=e.categoryId,$e=e.enableRag,function(){if(!de||!ue)return;ue.addEventListener("click",()=>{_e&&Ee?Ee.abort():He()}),de.addEventListener("keydown",e=>{"Enter"!==e.key||e.shiftKey||e.isComposing||(e.preventDefault(),He())}),de.addEventListener("input",()=>{qe(),function(){if(!de)return;de.style.height="auto",de.style.height=`${Math.min(de.scrollHeight,120)}px`}()}),de.addEventListener("focus",()=>{pe&&pe.classList.add("csk-input-wrap--focus")}),de.addEventListener("blur",()=>{pe&&pe.classList.remove("csk-input-wrap--focus")}),he&&he.addEventListener("click",()=>function(){if(!ie)return;if(!confirm(c("clear_confirm")))return;if(ce=[],le){le.querySelectorAll(".csk-msg, .csk-loading").forEach(e=>e.remove())}he&&(he.style.display="none");Be(),ne(ie.integrateId);const e=function(){const e="undefined"!=typeof crypto&&crypto.randomUUID?crypto.randomUUID().substring(0,8):Math.random().toString(36).substring(2,10);return`sdk_${Date.now()}_${e}`}();h(e),A(ie.integrateId,ie.userId,e),o.lifecycleClear(ie.integrateId),o.info(`新 chatId=${e}`)}())}(),function(){if(!le)return;le.addEventListener("scroll",()=>{if(!le)return;const{scrollTop:e,scrollHeight:n,clientHeight:t}=le;Ce=n-e-t<80,Ce&&Me()}),fe&&fe.addEventListener("click",()=>{le&&Q(le),Ce=!0,Me()})}(),function(){if(!ye)return;ye.addEventListener("input",()=>{Le=ye.value.trim().toLowerCase(),De()})}(),e.showCategorySwitch&&me&&async function(){if(!me)return;try{const e=await async function(){const e=k("/category/tree");try{const n=await b(e);if(!n.ok)throw new f(y(n.status),`http_${n.status}`);const t=await n.json();return t.success&&Array.isArray(t.data)?(o.info(`加载分类树成功 count=${t.data.length}`),t.data):[]}catch(e){return e instanceof f?o.error(`加载分类树失败: ${e.message}`):o.error("加载分类树失败",e),[]}}();if(0===e.length)return;me.innerHTML=``;const n=(e,t=0)=>{for(const r of e){const e=document.createElement("option");e.value=String(r.id),e.textContent=`${" ".repeat(t)}${r.name}`,void 0!==ae&&String(r.id)===String(ae)&&(e.selected=!0),me.appendChild(e),r.children&&r.children.length>0&&n(r.children,t+1)}};n(e),o.info(`知识库分类加载成功 count=${e.length}`)}catch(e){o.error(c("category_load_error"),e)}}()}async function Se(){if(ie&&le&&(await L(),await Ne(),0===ce.length)){const e=function(e){try{const n=localStorage.getItem(Z(e));if(!n)return[];const t=JSON.parse(n);return t&&Array.isArray(t.messages)?(o.info(`加载历史消息 integrateId=${e} count=${t.messages.length}`),t.messages):[]}catch(e){return o.warn("加载会话历史失败",e),[]}}(ie.integrateId);e.length>0&&(ce=e,Re(),o.info(`从本地缓存恢复 ${e.length} 条消息`))}}async function Ne(){if(!ie||!le)return;const e=m();if(e)try{const n=await E(e);n.messages.length>0&&(ce=n.messages.map((e,n)=>({id:z(),role:"USER"===e.messageType?"user":"ai",content:e.content,timestamp:new Date(e.createTime).getTime()})),Re(),o.info(`从后端加载 ${ce.length} 条历史消息`),ee(u(),ce))}catch(e){o.warn("从后端加载历史消息失败",e)}}function Me(){fe&&fe.classList.add("csk-newmsg--hidden")}function Fe(){le&&(Ce?Q(le):fe&&fe.classList.remove("csk-newmsg--hidden"))}function ze(e){ue&&("stop"===e?(ue.classList.add("csk-send-btn--stop"),ue.removeAttribute("disabled"),ue.setAttribute("title",c("stop")),ue.setAttribute("aria-label",c("stop")),ue.innerHTML=''):(ue.classList.remove("csk-send-btn--stop"),ue.setAttribute("title",c("send")),ue.setAttribute("aria-label",c("send")),ue.innerHTML='',qe()))}function De(){const e=null==ge?void 0:ge.querySelector("#csk-history-list");if(!e||!ie)return;G(e,Le?Ie.filter(e=>(e.lastMessagePreview||e.chatId||e.id||"").toLowerCase().includes(Le)):Ie,e=>{Pe(e)},e=>{window.open(I(e),"_blank")},async e=>{if(!confirm(c("history_delete_confirm")))return;await C(e)&&(e===m()&&(ce=[],le&&le.querySelectorAll(".csk-msg, .csk-loading").forEach(e=>e.remove()),he&&(he.style.display="none"),Be()),Ie=Ie.filter(n=>(n.chatId||n.id)!==e),De())},m())}function Te(e,n){if(!le)return;const t=ce.find(n=>n.id===e&&"ai"===n.role);if(!t)return;const r=t.feedback===n?void 0:n;t.feedback=r;const s=le.querySelector(`[data-csk-msg-id="${e}"]`);if(s&&X(s,r),ie&&ee(ie.integrateId,ce),r){const n="up"===r?"THUMBS_UP":"THUMBS_DOWN";(async function(e,n){if(!l)return!1;const t=k("/feedback");try{const r=await b(t,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({messageId:String(e),conversationId:l.chatId,feedbackType:n})});return r.ok?(await r.json()).success||!1:(o.error(`反馈提交失败 status=${r.status}`),!1)}catch(e){return o.error("反馈提交异常",e),!1}})(String(e),n).then(n=>{n?o.info(`消息反馈已提交 msgId=${e} value=${r}`):o.warn(`消息反馈提交失败 msgId=${e}(本地状态已更新)`)})}else o.info(`消息反馈已取消 msgId=${e}`)}function Be(){if(!be)return;const e=ce.length>0||le&&le.querySelector(".csk-msg");be.style.display=e?"none":""}function qe(){if(!ue||!de)return;de.value.trim().length>0&&!_e?ue.removeAttribute("disabled"):ue.setAttribute("disabled","true")}async function He(){if(!de||!ie||_e)return;const e=de.value.trim();if(""===e)return;de.value="",qe(),de.style.height="auto";const n=B(),t={id:z(),role:"user",content:e,timestamp:n};le&&R(le,e,n),ce.push(t),Be(),he&&ce.length>0&&(he.style.display="inline-flex"),le&&Fe(),await je(e)}async function je(e){if(!ie||!le)return;_e=!0,ze("stop"),ie.chatId||await L();const n=B(),t=$e;ve&&ve(),le&&Fe();const r=z();let s="";try{ie.streaming?s=await async function(e,n,t,r){Ee=new AbortController;const s=Ee.signal;return new Promise((o,a)=>{let i=null,l=null,d="",p=!1;v(e,e=>{if(d+=e,!p&&le){we&&we();const{wrapper:e,bubble:t}=function(e,n,t){const r=document.createElement("div");r.className="csk-msg csk-msg--ai csk-msg--streaming",t&&(r.dataset.cskMsgId=t);const s=document.createElement("div");s.className="csk-msg__avatar csk-msg__avatar--ai",s.innerHTML=q;const o=document.createElement("div");o.className="csk-msg__content";const a=document.createElement("div");a.className="csk-msg__bubble",a.innerHTML="";const i=document.createElement("div");return i.className="csk-msg__time",i.textContent=T(n),o.appendChild(a),o.appendChild(O(a,r)),o.appendChild(i),r.appendChild(s),r.appendChild(o),e.appendChild(r),{wrapper:r,bubble:a}}(le,n,r);l=e,i=t,p=!0}i&&(i.textContent=d,function(e){if(e.querySelector(".csk-caret"))return;const n=document.createElement("span");n.className="csk-caret",n.setAttribute("aria-hidden","true"),e.appendChild(n)}(i)),le&&Fe()},()=>{if(l&&i){if(!p&&""===d)return void x(e).then(o).catch(a);d&&(i.innerHTML=se(d)),J(l,i)}o(d)},e=>{d.length>0?(i&&(i.innerHTML=se(d+"\n\n"+c("stream_interrupted")),l&&J(l,i)),o(d)):a(e)},ae,t,s)})}(e,n,t,r):(s=await x(e),we&&we(),le&&P(le,s,n,se,r));const a={id:r,role:"ai",content:s,timestamp:n};ce.push(a),ee(ie.integrateId,ce),le&&Fe(),function(e){if(!xe)return;const n=e.length>120?e.substring(0,120)+"...":e;xe.textContent=c("new_msg_announce")+":"+n}(s),le&&le.dispatchEvent(new CustomEvent("csk:newMessage",{bubbles:!0,detail:{msg:a}})),t&&async function(e,n){try{const t=await w(e,ae);if(t.length>0){const e=t.map(e=>{var n,t;return{documentId:e.documentId||"",title:e.title||"",sourceName:e.sourceName||"",chunkIndex:null!==(n=e.chunkIndex)&&void 0!==n?n:0,score:null!==(t=e.score)&&void 0!==t?t:0,snippet:e.snippet||""}});if(n.sources=e,le){const n=le.querySelector(".csk-msg--ai:last-of-type");n&&V(n,e)}ie&&ee(ie.integrateId,ce)}}catch(e){o.warn("获取引用来源失败",e)}}(e,a)}catch(e){we&&we();const n=e instanceof f?e.message:c("error_send");if(le){const e=document.createElement("div");e.className="csk-msg csk-msg--ai";const t=document.createElement("div");t.className="csk-msg__bubble",t.style.color="#DC2626",t.textContent=`⚠ ${n}`,e.appendChild(t),le.appendChild(e)}o.error(`发送失败 integrateId=${ie.integrateId}`,e)}finally{_e=!1,Ee=null,ze("send"),qe()}}function Re(){if(!le)return;const e=le.querySelector(".csk-history-panel");le.querySelectorAll(".csk-msg, .csk-loading").forEach(e=>e.remove());for(const e of ce)if("user"===e.role)R(le,e.content,e.timestamp);else{const n=P(le,e.content,e.timestamp,se,e.id);e.sources&&e.sources.length>0&&V(n,e.sources),e.feedback&&X(n,e.feedback)}Ce=!0,Q(le),he&&ce.length>0&&(he.style.display="inline-flex"),Be(),e&&!le.contains(e)&&le.appendChild(e)}async function Pe(e){if(!ie||!le)return;const n=Ie.find(n=>(n.chatId||n.id)===e);if(n&&void 0!==n.roleId){const e=String(n.roleId),t=u();e&&e!==t&&(o.info(`会话角色不匹配,自动切换角色 ${t} -> ${e}`),p(e),ke&&(ke.value=e))}o.info(`切换到会话 conversationId=${e}`),h(e),A(u(),ie.userId,e),ge&&ge.classList.add("csk-history-panel--hidden"),ce=[];le.querySelectorAll(".csk-msg, .csk-loading").forEach(e=>e.remove());try{const n=await E(e);n.messages.length>0&&(ce=n.messages.map(e=>({id:z(),role:"USER"===e.messageType?"user":"ai",content:e.content,timestamp:new Date(e.createTime).getTime()})),Re(),o.info(`加载会话 ${e} 的 ${ce.length} 条消息`),ee(ie.integrateId,ce))}catch(n){o.warn(`加载会话消息失败 conversationId=${e}`,n)}he&&ce.length>0&&(he.style.display="inline-flex"),Be()}let Ye=null,Ue=!1,Oe=null,Xe=null,We=null,Ke=null,Je=null,Ve=null,Ge=null,Qe=null,Ze=null,en=null,nn=null,tn=null,rn=null,sn=null,on=null,an=null,cn=null,ln=null;function dn(){if(!on||!Oe)return;const e=Oe.getBoundingClientRect(),n=sn?"left"===sn.side:"left-bottom"===(null==Ye?void 0:Ye.position);n?(on.style.left=`${e.left}px`,on.style.right="auto"):(on.style.right=window.innerWidth-e.right+"px",on.style.left="auto"),on.style.bottom=window.innerHeight-e.top+8+"px",on.classList.toggle("csk-teaser--left",!!n),on.classList.toggle("csk-teaser--right",!n)}function pn(){if(!on)return;on.classList.add("csk-teaser--hidden"),on.removeEventListener("click",un);const e=on.querySelector(".csk-teaser__close");e&&e.removeEventListener("click",hn)}function un(e){e.target.closest(".csk-teaser__close")||(pn(),bn())}function hn(e){e.stopPropagation(),pn()}function mn(){on&&!on.classList.contains("csk-teaser--hidden")&&dn()}const kn='button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';function gn(e){"Escape"===e.key&&Xe&&!Xe.classList.contains("csk-window--hidden")&&fn(),function(e){if(!Xe||Xe.classList.contains("csk-window--hidden"))return;if("Tab"!==e.key)return;const n=Array.from(Xe.querySelectorAll(kn));if(0===n.length)return;const t=n[0],r=n[n.length-1];e.shiftKey?document.activeElement===t&&(e.preventDefault(),r.focus()):document.activeElement===r&&(e.preventDefault(),t.focus())}(e)}function bn(){Xe&&(Xe.classList.remove("csk-window--hidden"),pn(),cn&&cn.classList.add("csk-launcher__badge--hidden"),setTimeout(()=>{Ke&&Ke.focus()},80))}function fn(){Xe&&Xe.classList.add("csk-window--hidden")}function yn(){Xe&&(Xe.classList.contains("csk-window--hidden")?bn():fn())}function xn(e){return`csk_size_${e}`}function vn(e){return`csk_position_${e}`}function wn(e){return`csk_launcher_pos_${e}`}function _n(e){Xe&&(Xe.classList.remove("csk-window--right","csk-window--left"),Xe.classList.add(`csk-window--${e}`),on&&(on.classList.remove("csk-teaser--right","csk-teaser--left"),on.classList.add(`csk-teaser--${e}`)))}const En={init:function(e){if(Ue)return void o.warn("SDK 已初始化,请先调用 destroy() 再重新初始化");const t=function(e){var n,t,r,s,a,i,c,l,d,p,u,h;if(!e.integrateId||"string"!=typeof e.integrateId&&"number"!=typeof e.integrateId||"string"==typeof e.integrateId&&""===e.integrateId.trim())return o.error('integrateId 是必传参数(对应后端 roleId 客服角色 ID),请检查 init() 调用。示例:ChatbotSDK.init({ integrateId: 1, requestDomain: "https://api.example.com" })'),null;if(!e.requestDomain||"string"!=typeof e.requestDomain||""===e.requestDomain.trim())return o.error('requestDomain 是必传参数,请检查 init() 调用。示例:ChatbotSDK.init({ integrateId: 1, requestDomain: "https://api.example.com" })'),null;try{new URL(e.requestDomain)}catch(n){return o.error(`requestDomain 不是合法的 URL 格式:${e.requestDomain}。请提供完整的域名,如 https://api.example.com`),null}const m=String(e.integrateId).trim(),k=e.launcherIcon||'',g=e.primaryColor||"#4F46E5",b={integrateId:m,requestDomain:e.requestDomain.replace(/\/+$/,""),userId:e.userId,categoryId:e.categoryId,showCategorySwitch:null!==(n=e.showCategorySwitch)&&void 0!==n&&n,title:e.title||"AI 智能助手",width:null!==(t=e.width)&&void 0!==t?t:380,height:Math.max(300,null!==(r=e.height)&&void 0!==r?r:520),position:"left-bottom"===e.position?"left-bottom":"right-bottom",primaryColor:g,launcherIcon:k,showClear:null===(s=e.showClear)||void 0===s||s,showAdminPanel:null!==(a=e.showAdminPanel)&&void 0!==a&&a,quickReplies:Array.isArray(e.quickReplies)?e.quickReplies.map(e=>String(e).trim()).filter(Boolean):[],theme:"dark"===e.theme?"dark":"light",showTeaser:null===(i=e.showTeaser)||void 0===i||i,teaserText:"string"==typeof e.teaserText&&e.teaserText.trim()||"",resizable:null===(c=e.resizable)||void 0===c||c,streaming:null===(l=e.streaming)||void 0===l||l,enableRag:null===(d=e.enableRag)||void 0===d||d,rewriteStrategy:e.rewriteStrategy||"REWRITE",locale:e.locale||"zh-CN",debug:null===(p=e.debug)||void 0===p||p,sound:null!==(u=e.sound)&&void 0!==u&&u,notification:null!==(h=e.notification)&&void 0!==h&&h,onError:"function"==typeof e.onError?e.onError:void 0,onReady:"function"==typeof e.onReady?e.onReady:void 0,onMessage:"function"==typeof e.onMessage?e.onMessage:void 0,token:e.token,roles:e.roles,chatId:""};return o.info(`配置解析完成 integrateId(=roleId)=${b.integrateId} userId(=accountId)=${b.userId||"(未设置)"} requestDomain=${b.requestDomain}`),b}(e);if(!t)return;var s;Ye=t,function(e){if(a[e])i=e;else{const n=e.split("-")[0],t=Object.keys(a).find(e=>e.startsWith(n));t&&(i=t)}}(Ye.locale),s=Ye.debug,n=s,r(Ye.onError),function(e){l=e}(Ye),F(Ye),Oe=H(Ye,yn),document.body.appendChild(Oe),cn=Oe.querySelector(".csk-launcher__badge"),function(e,n){try{const t=localStorage.getItem(wn(e));if(!t)return;const r=JSON.parse(t);if("left"!==r.side&&"right"!==r.side||"number"!=typeof r.bottom)return;const s=window.innerHeight-76,o=Math.max(16,Math.min(r.bottom,s));n.classList.remove("csk-launcher--right","csk-launcher--left"),n.classList.add(`csk-launcher--${r.side}`),n.style.bottom=`${o}px`,sn={side:r.side,bottom:o}}catch(e){}}(Ye.integrateId,Oe),tn=function(e,n,t){let r=!1,s=0,o=0,a=!1,i=0,c=0,l=0,d=!1;function p(){const n=parseFloat(getComputedStyle(e).bottom);return isNaN(n)?24:n}function u(n,t){if(d)return;r=!0,a=!1,s=n,o=t,i=p();const u=e.style,h=getComputedStyle(e);c=u.right?parseFloat(u.right):parseFloat(h.right),l=u.left?parseFloat(u.left):parseFloat(h.left)}function h(n,t,d){if(!r)return;const p=n-s,u=t-o;if(!a){if(Math.abs(p)<5&&Math.abs(u)<5)return;a=!0,e.classList.add("csk-launcher--dragging"),e.classList.remove("csk-launcher--right","csk-launcher--left"),e.style.transition="none",!isNaN(c)&&c>=0&&isNaN(l)?(e.style.right=`${c}px`,e.style.left="auto"):!isNaN(l)&&l>=0&&(e.style.left=`${l}px`,e.style.right="auto")}if(a){d.preventDefault();const n=Math.max(16,Math.min(i-u,j()));e.style.bottom=`${n}px`}}function m(){if(!r)return;if(r=!1,!a)return;const n=parseFloat(e.style.bottom)||p(),s=Math.max(16,Math.min(n,j())),o=e.getBoundingClientRect(),i=o.left+o.width/2<=window.innerWidth/2?"left":"right";e.classList.remove("csk-launcher--dragging"),e.classList.add(`csk-launcher--${i}`,"csk-launcher--snap"),e.style.left="",e.style.right="",e.style.bottom=`${s}px`,d=!0;const c=()=>{e.removeEventListener("transitionend",c),e.classList.remove("csk-launcher--snap"),e.style.transition="",d=!1};e.addEventListener("transitionend",c),setTimeout(()=>{d&&c()},350);const l=n=>{n.stopPropagation(),e.removeEventListener("click",l,!0)};e.addEventListener("click",l,!0);const u=e.querySelector(".csk-launcher__badge");u&&(u.style.display=""),t&&t({side:i,bottom:s})}function k(e){u(e.clientX,e.clientY)}function g(e){h(e.clientX,e.clientY,e)}function b(){m()}function f(e){1===e.touches.length&&u(e.touches[0].clientX,e.touches[0].clientY)}function y(e){1===e.touches.length&&h(e.touches[0].clientX,e.touches[0].clientY,e)}function x(){m()}return e.addEventListener("mousedown",k),document.addEventListener("mousemove",g),document.addEventListener("mouseup",b),e.addEventListener("touchstart",f,{passive:!0}),document.addEventListener("touchmove",y,{passive:!1}),document.addEventListener("touchend",x),()=>{e.removeEventListener("mousedown",k),document.removeEventListener("mousemove",g),document.removeEventListener("mouseup",b),e.removeEventListener("touchstart",f),document.removeEventListener("touchmove",y),document.removeEventListener("touchend",x)}}(Oe,0,e=>{sn=e,function(e,n){try{localStorage.setItem(wn(e),JSON.stringify(n))}catch(e){}}(Ye.integrateId,e),function(){if(!Xe)return;Xe.style.left="",Xe.style.top="",Xe.style.right="",Xe.style.bottom=""}(),_n(e.side),on&&!on.classList.contains("csk-teaser--hidden")&&dn()}),sn&&_n(sn.side);const d=function(e){const n=document.createElement("div");n.id="csk-window",n.className=`csk-root csk-window csk-window--${"left-bottom"===e.position?"left":"right"} csk-window--hidden${"dark"===e.theme?" csk-dark":""}`,n.setAttribute("role","dialog"),n.setAttribute("aria-label",e.title),n.setAttribute("aria-modal","false");const t=document.createElement("div");t.className="csk-header";const r=document.createElement("div");r.className="csk-header__left";const s=document.createElement("div");s.className="csk-header__avatar",s.innerHTML=q;const o=document.createElement("div");o.className="csk-header__info";const a=document.createElement("span");a.className="csk-header__title",a.textContent=e.title;const i=document.createElement("span");i.className="csk-header__status",i.innerHTML=`${c("status_online")}`,o.appendChild(a),o.appendChild(i),r.appendChild(s),r.appendChild(o);let l=null;const d=e.roles;if(d&&d.length>1){l=document.createElement("select"),l.className="csk-role-select",l.setAttribute("aria-label",c("role_selector_label"));for(const n of d){const t=document.createElement("option");t.value=String(n.id),t.textContent=n.name||String(n.id),String(n.id)===String(e.integrateId)&&(t.selected=!0),l.appendChild(t)}l.addEventListener("change",()=>{const e=l.value;n.dispatchEvent(new CustomEvent("csk:roleChange",{detail:{roleId:e}}))});const s=document.createElement("div");s.className="csk-role-select-wrap",s.appendChild(l),t.appendChild(r),t.appendChild(s)}else t.appendChild(r);const p=document.createElement("div");p.className="csk-header__actions";const u=document.createElement("button");u.className="csk-history-btn",u.setAttribute("title",c("history_title")),u.innerHTML='';const h=document.createElement("button");h.className="csk-header__btn csk-header__btn--minimize",h.setAttribute("title",c("minimize")),h.innerHTML='',h.addEventListener("click",()=>{n.classList.add("csk-window--hidden")});const m=document.createElement("button");m.className="csk-header__btn csk-header__btn--close",m.setAttribute("title",c("close")),m.innerHTML='',m.addEventListener("click",()=>{n.classList.add("csk-window--hidden")}),p.appendChild(u),p.appendChild(h),p.appendChild(m),t.appendChild(p);const k=document.createElement("div");k.id="csk-messages",k.className="csk-messages";const g=document.createElement("div");if(g.className="csk-welcome",g.innerHTML=`\n${d(r[1])}
`);continue}c&&h();const m=n.match(/^[\-\*]\s+(.+)/);if(m){p(),h(),a&&"ul"===i||(u(),a=!0,i="ul",o.push('')),o.push(`
":""),a=!1,i="")}function h(){c&&(o.push("- ${d(m[1])}
`);continue}const k=n.match(/^\d+\.\s+(.+)/);k?(p(),h(),a&&"ol"===i||(u(),a=!0,i="ol",o.push('')),o.push(`
- ${d(k[1])}
`)):""!==n.trim()?/^(\*{3,}|-{3,}|_{3,})$/.test(n.trim())?(p(),u(),h(),o.push('
')):(u(),h(),l.push(d(n))):(p(),u())}return p(),u(),h(),o.join("\n");function d(e){return e=(e=(e=(e=(e=(e=e.replace(/\*\*(.+?)\*\*/g,"$1")).replace(/__(.+?)__/g,"$1")).replace(/\*(.+?)\*/g,"$1")).replace(/(?$1")).replace(/~~(.+?)~~/g,"$1")).replace(/\[([^\]]+)\]\(([^)]+)\)/g,(e,n,t)=>`${n}`)}function p(){l.length>0&&(o.push(`${l.join("
`),l=[])}function u(){a&&(o.push("ul"===i?"
")}