|
|
|
@ -788,15 +788,18 @@ var ChatbotSDK = (function () { |
|
|
|
} |
|
|
|
} |
|
|
|
/** |
|
|
|
* 获取会话列表 |
|
|
|
* 获取会话列表(SDK 安全端点) |
|
|
|
* 使用 /ai/sdk/conversation/list,强制按 accountId + roleId 过滤 |
|
|
|
*/ |
|
|
|
async function fetchConversationList(page = 1, size = 20, accountId, roleId) { |
|
|
|
let path = `/conversation/list?page=${page}&size=${size}`; |
|
|
|
const params = new URLSearchParams(); |
|
|
|
params.set('page', String(page)); |
|
|
|
params.set('size', String(size)); |
|
|
|
if (accountId) |
|
|
|
path += `&accountId=${encodeURIComponent(accountId)}`; |
|
|
|
params.set('accountId', accountId); |
|
|
|
if (roleId) |
|
|
|
path += `&roleId=${encodeURIComponent(roleId)}`; |
|
|
|
const url = buildUrl(path); |
|
|
|
params.set('roleId', roleId); |
|
|
|
const url = buildUrl(`/ai/sdk/conversation/list?${params.toString()}`); |
|
|
|
try { |
|
|
|
const response = await safeFetch(url); |
|
|
|
if (!response.ok) |
|
|
|
@ -814,10 +817,16 @@ var ChatbotSDK = (function () { |
|
|
|
} |
|
|
|
} |
|
|
|
/** |
|
|
|
* 获取会话消息 |
|
|
|
* 获取会话消息(SDK 安全端点) |
|
|
|
* 使用 /ai/sdk/conversation/{id}/messages,含账户归属校验 |
|
|
|
*/ |
|
|
|
async function fetchConversationMessages(conversationId) { |
|
|
|
const url = buildUrl(`/conversation/${conversationId}/messages`); |
|
|
|
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); |
|
|
|
const url = buildUrl(`/ai/sdk/conversation/${conversationId}/messages?${params.toString()}`); |
|
|
|
try { |
|
|
|
const response = await safeFetch(url); |
|
|
|
if (!response.ok) |
|
|
|
@ -834,10 +843,16 @@ var ChatbotSDK = (function () { |
|
|
|
} |
|
|
|
} |
|
|
|
/** |
|
|
|
* 删除会话 |
|
|
|
* 删除会话(SDK 安全端点) |
|
|
|
* 使用 DELETE /ai/sdk/conversation/{id},含账户归属校验 |
|
|
|
*/ |
|
|
|
async function deleteConversation(conversationId) { |
|
|
|
const url = buildUrl(`/conversation/${conversationId}`); |
|
|
|
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); |
|
|
|
const url = buildUrl(`/ai/sdk/conversation/${conversationId}?${params.toString()}`); |
|
|
|
try { |
|
|
|
const response = await safeFetch(url, { method: 'DELETE' }); |
|
|
|
if (!response.ok) |
|
|
|
@ -852,10 +867,16 @@ var ChatbotSDK = (function () { |
|
|
|
} |
|
|
|
} |
|
|
|
/** |
|
|
|
* 导出会话 URL |
|
|
|
* 导出会话 URL(SDK 安全端点) |
|
|
|
* 使用 /ai/sdk/conversation/{id}/export,含账户归属参数 |
|
|
|
*/ |
|
|
|
function getConversationExportUrl(conversationId) { |
|
|
|
return buildUrl(`/conversation/${conversationId}/export`); |
|
|
|
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); |
|
|
|
return buildUrl(`/ai/sdk/conversation/${conversationId}/export?${params.toString()}`); |
|
|
|
} |
|
|
|
// ==================== chatId 自动初始化 ====================
|
|
|
|
/** |
|
|
|
@ -863,7 +884,7 @@ var ChatbotSDK = (function () { |
|
|
|
* |
|
|
|
* 逻辑: |
|
|
|
* 1. 先查 localStorage 缓存的 chatId(同一 integrateId + userId 可能复用) |
|
|
|
* 2. 查 /conversation/list?accountId=X&roleId=Y 看是否有匹配的会话 |
|
|
|
* 2. 查 /ai/sdk/conversation/list?accountId=X&roleId=Y 看是否有匹配的会话 |
|
|
|
* 3. 有会话 → 使用最新会话的 conversationId 作为 chatId |
|
|
|
* 4. 无会话 → 自动生成 chatId(格式:sdk_timestamp_random) |
|
|
|
*/ |
|
|
|
@ -2126,6 +2147,25 @@ var ChatbotSDK = (function () { |
|
|
|
.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); } |
|
|
|
|
|
|
|
/* Launcher 拖拽态:微放大 + 阴影加深 */ |
|
|
|
.csk-launcher--dragging { |
|
|
|
cursor: grabbing !important; |
|
|
|
transform: scale(1.1) !important; |
|
|
|
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; |
|
|
|
} |
|
|
|
.csk-launcher--dragging::before, |
|
|
|
.csk-launcher--dragging::after { animation-play-state: paused !important; } |
|
|
|
/* 拖拽中隐藏未读徽章 */ |
|
|
|
.csk-launcher--dragging .csk-launcher__badge { display: none !important; } |
|
|
|
/* 吸附过渡动画(释放时的缓动) */ |
|
|
|
.csk-launcher--snap { |
|
|
|
transition: left 0.3s ease-out, right 0.3s ease-out, bottom 0.3s ease-out !important; |
|
|
|
} |
|
|
|
|
|
|
|
/* ========== Launcher 未读徽章 ========== */ |
|
|
|
.csk-launcher__badge { |
|
|
|
position: absolute; |
|
|
|
@ -2308,9 +2348,10 @@ var ChatbotSDK = (function () { |
|
|
|
border-radius: 0; |
|
|
|
} |
|
|
|
.csk-window--hidden { transform: translateY(100%); } |
|
|
|
.csk-launcher { bottom: 20px; } |
|
|
|
.csk-launcher--right { right: 20px; } |
|
|
|
.csk-launcher--left { left: 20px; } |
|
|
|
/* 移动端默认位置,仅在未被 JS 拖拽覆盖时生效 */ |
|
|
|
.csk-launcher:not([style*="bottom"]) { bottom: 20px; } |
|
|
|
.csk-launcher--right:not([style*="bottom"]) { right: 20px; } |
|
|
|
.csk-launcher--left:not([style*="bottom"]) { left: 20px; } |
|
|
|
} |
|
|
|
`;
|
|
|
|
} |
|
|
|
@ -2723,6 +2764,150 @@ var ChatbotSDK = (function () { |
|
|
|
document.removeEventListener('mouseup', onMouseUp); |
|
|
|
}; |
|
|
|
} |
|
|
|
/** Launcher 拖拽:垂直方向最小 bottom 值(px),避免贴底被系统导航栏遮挡 */ |
|
|
|
const LAUNCHER_MIN_BOTTOM = 16; |
|
|
|
/** Launcher 拖拽:计算当前视口下最大 bottom 值 */ |
|
|
|
function launcherMaxBottom() { |
|
|
|
return window.innerHeight - 76; // 按钮高度 60 + 16 间距
|
|
|
|
} |
|
|
|
/** |
|
|
|
* 启用 Launcher 按钮的拖拽功能(边缘吸附式) |
|
|
|
* |
|
|
|
* 设计要点: |
|
|
|
* - 仅允许垂直方向自由移动,水平方向释放时自动吸附到最近的左/右边缘 |
|
|
|
* - 移动距离 < 5px 视为点击(不干扰原有的 open/close 逻辑) |
|
|
|
* - 拖拽中按钮微放大 + 隐藏未读徽章,释放后带缓动吸附动画 |
|
|
|
* - 支持鼠标 + 触摸事件,触摸拖拽时阻止页面滚动 |
|
|
|
*/ |
|
|
|
function enableLauncherDrag(launcherEl, config, onDragEnd) { |
|
|
|
let dragging = false; |
|
|
|
let startX = 0; |
|
|
|
let startY = 0; |
|
|
|
let hasMoved = false; |
|
|
|
let startBottom = 0; |
|
|
|
let startRight = 0; |
|
|
|
let startLeft = 0; |
|
|
|
// 标记是否正在吸附过渡中,避免过渡期间的 mouseup 再次触发
|
|
|
|
let isSnapping = false; |
|
|
|
/** 获取当前 bottom 值(纯数值) */ |
|
|
|
function getCurrentBottom() { |
|
|
|
const val = parseFloat(getComputedStyle(launcherEl).bottom); |
|
|
|
return isNaN(val) ? 24 : val; |
|
|
|
} |
|
|
|
function onPointerDown(clientX, clientY) { |
|
|
|
if (isSnapping) |
|
|
|
return; |
|
|
|
dragging = true; |
|
|
|
hasMoved = false; |
|
|
|
startX = clientX; |
|
|
|
startY = clientY; |
|
|
|
startBottom = getCurrentBottom(); |
|
|
|
// 记录水平起始位置(用于判断当前在哪一侧)
|
|
|
|
const style = launcherEl.style; |
|
|
|
const cs = getComputedStyle(launcherEl); |
|
|
|
startRight = style.right ? parseFloat(style.right) : parseFloat(cs.right); |
|
|
|
startLeft = style.left ? parseFloat(style.left) : parseFloat(cs.left); |
|
|
|
} |
|
|
|
function onPointerMove(clientX, clientY, e) { |
|
|
|
if (!dragging) |
|
|
|
return; |
|
|
|
const dx = clientX - startX; |
|
|
|
const dy = clientY - startY; |
|
|
|
if (!hasMoved) { |
|
|
|
if (Math.abs(dx) < 5 && Math.abs(dy) < 5) |
|
|
|
return; |
|
|
|
hasMoved = true; |
|
|
|
// 进入拖拽态
|
|
|
|
launcherEl.classList.add('csk-launcher--dragging'); |
|
|
|
launcherEl.classList.remove('csk-launcher--right', 'csk-launcher--left'); |
|
|
|
launcherEl.style.transition = 'none'; |
|
|
|
// 同步当前水平位置(从吸附态的 right/left 转换为固定像素)
|
|
|
|
if (!isNaN(startRight) && startRight >= 0 && isNaN(startLeft)) { |
|
|
|
launcherEl.style.right = `${startRight}px`; |
|
|
|
launcherEl.style.left = 'auto'; |
|
|
|
} |
|
|
|
else if (!isNaN(startLeft) && startLeft >= 0) { |
|
|
|
launcherEl.style.left = `${startLeft}px`; |
|
|
|
launcherEl.style.right = 'auto'; |
|
|
|
} |
|
|
|
} |
|
|
|
if (hasMoved) { |
|
|
|
e.preventDefault(); // 阻止触摸滚动
|
|
|
|
const newBottom = Math.max(LAUNCHER_MIN_BOTTOM, Math.min(startBottom - dy, launcherMaxBottom())); |
|
|
|
launcherEl.style.bottom = `${newBottom}px`; |
|
|
|
} |
|
|
|
} |
|
|
|
function onPointerUp() { |
|
|
|
if (!dragging) |
|
|
|
return; |
|
|
|
dragging = false; |
|
|
|
if (!hasMoved) |
|
|
|
return; // 移动距离不足 5px,视为点击,不处理
|
|
|
|
// 隐藏拖拽态(保留当前 bottom)
|
|
|
|
const currentBottom = parseFloat(launcherEl.style.bottom) || getCurrentBottom(); |
|
|
|
const clampedBottom = Math.max(LAUNCHER_MIN_BOTTOM, Math.min(currentBottom, launcherMaxBottom())); |
|
|
|
// 判断吸附到哪一侧(取离哪边更近)
|
|
|
|
const rect = launcherEl.getBoundingClientRect(); |
|
|
|
const center = rect.left + rect.width / 2; |
|
|
|
const snapSide = center <= window.innerWidth / 2 ? 'left' : 'right'; |
|
|
|
// 切换到吸附过渡动画
|
|
|
|
launcherEl.classList.remove('csk-launcher--dragging'); |
|
|
|
launcherEl.classList.add(`csk-launcher--${snapSide}`, 'csk-launcher--snap'); |
|
|
|
launcherEl.style.left = ''; |
|
|
|
launcherEl.style.right = ''; |
|
|
|
launcherEl.style.bottom = `${clampedBottom}px`; |
|
|
|
// 过渡结束后清理(保留 bottom,否则 CSS 默认 24px 会覆盖拖拽位置)
|
|
|
|
isSnapping = true; |
|
|
|
const onTransitionEnd = () => { |
|
|
|
launcherEl.removeEventListener('transitionend', onTransitionEnd); |
|
|
|
launcherEl.classList.remove('csk-launcher--snap'); |
|
|
|
launcherEl.style.transition = ''; |
|
|
|
// 注意:不清除 style.bottom,保留用户拖拽的垂直位置
|
|
|
|
isSnapping = false; |
|
|
|
}; |
|
|
|
launcherEl.addEventListener('transitionend', onTransitionEnd); |
|
|
|
// 兜底:350ms 后强制清理(transitionend 可能不触发)
|
|
|
|
setTimeout(() => { |
|
|
|
if (isSnapping) |
|
|
|
onTransitionEnd(); |
|
|
|
}, 350); |
|
|
|
// 恢复未读徽章
|
|
|
|
const badge = launcherEl.querySelector('.csk-launcher__badge'); |
|
|
|
if (badge) |
|
|
|
badge.style.display = ''; |
|
|
|
if (onDragEnd) { |
|
|
|
onDragEnd({ side: snapSide, bottom: clampedBottom }); |
|
|
|
} |
|
|
|
} |
|
|
|
// --- 鼠标事件 ---
|
|
|
|
function onMouseDown(e) { onPointerDown(e.clientX, e.clientY); } |
|
|
|
function onMouseMove(e) { onPointerMove(e.clientX, e.clientY, e); } |
|
|
|
function onMouseUp() { onPointerUp(); } |
|
|
|
// --- 触摸事件 ---
|
|
|
|
function onTouchStart(e) { |
|
|
|
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(); } |
|
|
|
launcherEl.addEventListener('mousedown', onMouseDown); |
|
|
|
document.addEventListener('mousemove', onMouseMove); |
|
|
|
document.addEventListener('mouseup', onMouseUp); |
|
|
|
launcherEl.addEventListener('touchstart', onTouchStart, { passive: true }); |
|
|
|
document.addEventListener('touchmove', onTouchMove, { passive: false }); |
|
|
|
document.addEventListener('touchend', onTouchEnd); |
|
|
|
return () => { |
|
|
|
launcherEl.removeEventListener('mousedown', onMouseDown); |
|
|
|
document.removeEventListener('mousemove', onMouseMove); |
|
|
|
document.removeEventListener('mouseup', onMouseUp); |
|
|
|
launcherEl.removeEventListener('touchstart', onTouchStart); |
|
|
|
document.removeEventListener('touchmove', onTouchMove); |
|
|
|
document.removeEventListener('touchend', onTouchEnd); |
|
|
|
}; |
|
|
|
} |
|
|
|
// ==================== 消息渲染 ====================
|
|
|
|
/** 渲染用户消息气泡 */ |
|
|
|
function renderUserBubble(container, text, timestamp) { |
|
|
|
@ -4171,6 +4356,9 @@ var ChatbotSDK = (function () { |
|
|
|
let showLoadingFn = null; |
|
|
|
let hideLoadingFn = null; |
|
|
|
let dragCleanup = null; |
|
|
|
let launcherDragCleanup = null; |
|
|
|
/** Launcher 位置记忆状态(拖拽后更新) */ |
|
|
|
let launcherPos = null; |
|
|
|
/** 提示气泡元素 */ |
|
|
|
let teaserEl = null; |
|
|
|
/** 提示气泡定时器 */ |
|
|
|
@ -4206,6 +4394,15 @@ var ChatbotSDK = (function () { |
|
|
|
document.body.appendChild(launcherEl); |
|
|
|
// 获取未读徽章引用(由 createLauncher 创建)
|
|
|
|
badgeEl = launcherEl.querySelector('.csk-launcher__badge'); |
|
|
|
// 6.1 恢复 Launcher 位置(必须在挂载到 DOM 之后、拖拽绑定之前)
|
|
|
|
restoreLauncherPosition(config.integrateId, launcherEl); |
|
|
|
// 6.2 启用 Launcher 拖拽(边缘吸附式,支持触摸 + 鼠标)
|
|
|
|
launcherDragCleanup = enableLauncherDrag(launcherEl, config, (pos) => { |
|
|
|
launcherPos = pos; |
|
|
|
saveLauncherPosition(config.integrateId, pos); |
|
|
|
// Launcher 位置变更后,同步聊天弹窗对齐侧
|
|
|
|
syncWindowAlignment(pos.side); |
|
|
|
}); |
|
|
|
// 7. 创建聊天弹窗
|
|
|
|
const dom = createChatWindow(config); |
|
|
|
windowEl = dom.window; |
|
|
|
@ -4310,15 +4507,21 @@ var ChatbotSDK = (function () { |
|
|
|
function positionTeaser() { |
|
|
|
if (!teaserEl || !launcherEl) |
|
|
|
return; |
|
|
|
// launcher 被拖拽过则用实际位置,否则用 DOM 实时计算
|
|
|
|
const rect = launcherEl.getBoundingClientRect(); |
|
|
|
const isLeft = (config === null || config === void 0 ? void 0 : config.position) === 'left-bottom'; |
|
|
|
const isLeft = launcherPos ? launcherPos.side === 'left' : (config === null || config === void 0 ? void 0 : config.position) === 'left-bottom'; |
|
|
|
if (isLeft) { |
|
|
|
teaserEl.style.left = `${rect.left}px`; |
|
|
|
teaserEl.style.right = 'auto'; |
|
|
|
} |
|
|
|
else { |
|
|
|
teaserEl.style.right = `${window.innerWidth - rect.right}px`; |
|
|
|
teaserEl.style.left = 'auto'; |
|
|
|
} |
|
|
|
teaserEl.style.bottom = `${window.innerHeight - rect.top + 8}px`; |
|
|
|
// 同步 teaser 类名(箭头方向)
|
|
|
|
teaserEl.classList.toggle('csk-teaser--left', !!isLeft); |
|
|
|
teaserEl.classList.toggle('csk-teaser--right', !isLeft); |
|
|
|
} |
|
|
|
function showTeaser() { |
|
|
|
if (!teaserEl) |
|
|
|
@ -4439,6 +4642,11 @@ var ChatbotSDK = (function () { |
|
|
|
dragCleanup(); |
|
|
|
dragCleanup = null; |
|
|
|
} |
|
|
|
if (launcherDragCleanup) { |
|
|
|
launcherDragCleanup(); |
|
|
|
launcherDragCleanup = null; |
|
|
|
} |
|
|
|
launcherPos = null; |
|
|
|
removeStyles(); |
|
|
|
document.removeEventListener('keydown', onKeyDown); |
|
|
|
window.removeEventListener('resize', onResize); |
|
|
|
@ -4525,6 +4733,50 @@ var ChatbotSDK = (function () { |
|
|
|
} |
|
|
|
catch ( /* 忽略 */_a) { /* 忽略 */ } |
|
|
|
} |
|
|
|
// ==================== Launcher 位置记忆 ====================
|
|
|
|
/** localStorage key:Launcher 位置 */ |
|
|
|
function launcherPosKey(integrateId) { |
|
|
|
return `csk_launcher_pos_${integrateId}`; |
|
|
|
} |
|
|
|
/** 保存 Launcher 拖拽位置到 localStorage */ |
|
|
|
function saveLauncherPosition(integrateId, pos) { |
|
|
|
try { |
|
|
|
localStorage.setItem(launcherPosKey(integrateId), JSON.stringify(pos)); |
|
|
|
} |
|
|
|
catch ( /* 忽略 */_a) { /* 忽略 */ } |
|
|
|
} |
|
|
|
/** 恢复 Launcher 位置(初始化时调用) */ |
|
|
|
function restoreLauncherPosition(integrateId, el) { |
|
|
|
try { |
|
|
|
const raw = localStorage.getItem(launcherPosKey(integrateId)); |
|
|
|
if (!raw) |
|
|
|
return; |
|
|
|
const pos = JSON.parse(raw); |
|
|
|
if ((pos.side !== 'left' && pos.side !== 'right') || typeof pos.bottom !== 'number') |
|
|
|
return; |
|
|
|
const maxBottom = window.innerHeight - 76; |
|
|
|
const clampedBottom = Math.max(16, Math.min(pos.bottom, maxBottom)); |
|
|
|
// 覆盖默认对齐方向
|
|
|
|
el.classList.remove('csk-launcher--right', 'csk-launcher--left'); |
|
|
|
el.classList.add(`csk-launcher--${pos.side}`); |
|
|
|
el.style.bottom = `${clampedBottom}px`; |
|
|
|
// 保存到模块状态,供后续 positionTeaser / 弹窗对齐使用
|
|
|
|
launcherPos = { side: pos.side, bottom: clampedBottom }; |
|
|
|
} |
|
|
|
catch ( /* 忽略 */_a) { /* 忽略 */ } |
|
|
|
} |
|
|
|
/** 同步聊天弹窗对齐侧(Launcher 拖拽到左/右侧时跟随) */ |
|
|
|
function syncWindowAlignment(side) { |
|
|
|
if (!windowEl) |
|
|
|
return; |
|
|
|
windowEl.classList.remove('csk-window--right', 'csk-window--left'); |
|
|
|
windowEl.classList.add(`csk-window--${side}`); |
|
|
|
// 同步 teaser 箭头方向
|
|
|
|
if (teaserEl) { |
|
|
|
teaserEl.classList.remove('csk-teaser--right', 'csk-teaser--left'); |
|
|
|
teaserEl.classList.add(`csk-teaser--${side}`); |
|
|
|
} |
|
|
|
} |
|
|
|
// ==================== 提示音 + 桌面通知 ====================
|
|
|
|
/** 播放短促提示音(Web Audio API 合成,无需音频文件) */ |
|
|
|
function playNotificationSound() { |
|
|
|
|