@ -8,22 +8,19 @@
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans SC",sans-serif;background:#F0F2F5;min-height:100vh}
.page{display:flex;height:100vh}
/* 左侧面板 */
.panel{width:360px;min-width:360px;background:#fff;border-right:1px solid #E5E7EB;display:flex;flex-direction:column;overflow:hidden}
.panel{width:380px;min-width:380px;background:#fff;border-right:1px solid #E5E7EB;display:flex;flex-direction:column;overflow:hidden}
.panel-header{padding:20px;border-bottom:1px solid #F3F4F6}
.panel-header h1{font-size:20px;color:#111827;margin-bottom:4px}
.panel-header p{font-size:12px;color:#9CA3AF}
.panel-body{flex:1;overflow-y:auto;padding:20px}
.panel-body::-webkit-scrollbar{width:4px}
.panel-body::-webkit-scrollbar-thumb{background:#E5E7EB;border-radius:2px}
/* 表单 */
.fg{margin-bottom:14px}
.fg label{display:block;font-size:12px;font-weight:500;color:#374151;margin-bottom:4px}
.fg input,.fg select{width:100%;padding:8px 10px;border:1px solid #D1D5DB;border-radius:6px;font-size:13px;outline:none;transition:border-color .2s,box-shadow .2s;font-family:inherit}
.fg input:focus,.fg select:focus{border-color:#4F46E5;box-shadow:0 0 0 3px rgba(79,70,229,.1)}
.fg-row{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.fg .hint{font-size:11px;color:#9CA3AF;margin-top:3px}
/* 按钮 */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:10px 18px;border:none;border-radius:8px;font-size:13px;font-weight:500;cursor:pointer;transition:all .2s;font-family:inherit}
.btn-primary{background:#4F46E5;color:#fff}.btn-primary:hover{background:#4338CA}
.btn-danger{background:#FFF1F0;color:#CF1322}.btn-danger:hover{background:#FFE4E0}
@ -32,66 +29,78 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans S
.btn-full{width:100%}
.actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px}
.divider{border:none;border-top:1px solid #F3F4F6;margin:16px 0}
/* 右侧 */
.main{flex:1;display:flex;flex-direction:column;overflow:hidden}
.toolbar{display:flex;align-items:center;justify-content:space-between;padding:12px 20px;background:#fff;border-bottom:1px solid #E5E7EB}
.toolbar h2{font-size:15px;font-weight:600;color:#111827}
/* 用例区 */
.toolbar-tabs{display:flex;gap:4px}
.toolbar-tab{padding:6px 14px;border:1px solid #E5E7EB;border-radius:6px;background:#fff;color:#6B7280;font-size:12px;cursor:pointer;transition:all .2s}
.toolbar-tab--active{background:#4F46E5;color:#fff;border-color:#4F46E5}
.toolbar-tab:hover:not(.toolbar-tab--active){background:#F9FAFB}
.cases{flex:1;overflow-y:auto;padding:20px}
.cases::-webkit-scrollbar{width:4px}
.cases::-webkit-scrollbar-thumb{background:#E5E7EB;border-radius:2px}
.case{background:#fff;border:1px solid #E5E7EB;border-radius:10px;margin-bottom:14px;overflow:hidden}
.case-header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;background:#FAFBFC;border-bottom:1px solid #F3F4F6}
.case-title{font-size:13px;font-weight:600;color:#111827}
.case-badge{font-size:10px;font-weight:600;padding:2px 6px;border-radius:4px;margin-left:8px}
.badge-p0{background:#DBEAFE;color:#1D4ED8}
.badge-p1{background:#FEF3C7;color:#92400E}
.badge-p2{background:#F3E8FF;color:#7C3AED}
.case-body{padding:16px}
.case-desc{font-size:12px;color:#6B7280;margin-bottom:10px;line-height:1.5}
/* 日志 */
.logs{background:#1F2937;color:#D1D5DB;font-size:12px;line-height:1.7;padding:14px;border-radius:6px;max-height:260px;overflow-y:auto;white-space:pre-wrap;word-break:break-all;font-family:'SF Mono','Consolas','Menlo',monospace}
.logs::-webkit-scrollbar{width:4px}
.logs::-webkit-scrollbar-thumb{background:#4B5563;border-radius:2px}
.log-line--pass{color:#6EE7B7}.log-line--fail{color:#FCA5A5}.log-line--info{color:#93C5FD}.log-line--warn{color:#FCD34D}
/* 状态标签 */
.tag{display:inline-flex;align-items:center;gap:4px;font-size:11px;font-weight:600;padding:3px 10px;border-radius:12px}
.tag--idle{background:#F3F4F6;color:#6B7280}
.tag--running{background:#DBEAFE;color:#1D4ED8}
.tag--pass{background:#D1FAE5;color:#065F46}
.tag--fail{background:#FEE2E2;color:#991B1B}
/* 概览 */
.overview{display:grid;grid-template-columns:1fr 1fr 1fr 1fr 1fr;gap:10px;margin-bottom:16px}
.overview{display:grid;grid-template-columns:1fr 1fr 1fr 1fr 1fr 1fr;gap:10px;margin-bottom:16px}
.card-stat{background:#fff;border:1px solid #E5E7EB;border-radius:8px;padding:14px;text-align:center}
.card-stat .num{font-size:28px;font-weight:700}
.card-stat .lbl{font-size:11px;color:#6B7280;margin-top:2px}
.num--ok{color:#059669}.num--err{color:#DC2626}.num--total{color:#4F46E5}
/* 底部 */
.footer{padding:10px 20px;background:#fff;border-top:1px solid #E5E7EB;display:flex;align-items:center;justify-content:space-between;font-size:11px;color:#9CA3AF}
.mapping-note{background:#FFFBEB;border:1px solid #FDE68A;border-radius:8px;padding:10px 14px;margin-bottom:14px;font-size:11px;color:#92400E;line-height:1.6}
.mapping-note b{color:#78350F}
< / style >
< / head >
< body >
< div class = "page" >
<!-- ===== 左侧配置面板 ===== -->
< div class = "panel" >
< div class = "panel-header" >
< h1 > 🧪 ChatbotSDK 测试面板< / h1 >
< p > P0 核心链路 · 交互式验证 < / p >
< p > P0 核心 · P1 体验增强 · P2 运营完善 < / p >
< / div >
< div class = "panel-body" >
<!-- SDK 状态 -->
< div style = "margin-bottom:16px;display:flex;align-items:center;gap:8px" >
< span class = "tag tag--idle" id = "tag-sdk" > ⭕ SDK 未加载< / span >
< span class = "tag tag--idle" id = "tag-api" > ⭕ API 未测< / span >
< / div >
<!-- 参数映射说明 -->
< div class = "mapping-note" >
< b > 📋 参数映射(SDK → 后端)< / b > < br >
integrateId → < b > roleId< / b > (客服角色 ID)< br >
userId → < b > accountId< / b > (客户账号 ID)< br >
chatId → < b > 自动管理< / b > (从 /conversation/list 获取或自动生成)
< / div >
< div class = "fg" >
< label > integrateId(必传)< / label >
< input type = "text" id = "cfg-iid" value = "test-sdk-p0" placeholder = "集成标识" >
< label > integrateId → roleId(必传,客服角色 ID)< / label >
< input type = "number" id = "cfg-iid" value = "1" placeholder = "客服角色 ID(数字)" >
< div class = "hint" > 对应后端 AiController 的 roleId 参数< / div >
< / div >
< div class = "fg" >
< label > requestDomain(必传)< / label >
< input type = "text" id = "cfg-domain" value = "" placeholder = "http://localhost:9090" >
< / div >
< div class = "fg" >
< label > userId(可选)< / label >
< input type = "text" id = "cfg-uid" value = "" placeholder = "宿主用户标识(如 zhangsan)" >
< label > userId → accountId(可选,客户账号 ID)< / label >
< input type = "text" id = "cfg-uid" value = "" placeholder = "客户账号 ID(如 hanlin)" >
< div class = "hint" > 对应后端 AiController 的 accountId 参数< / div >
< / div >
< div class = "fg-row" >
< div class = "fg" >
@ -118,6 +127,16 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans S
< label > streaming< / label >
< select id = "cfg-stream" > < option value = "1" selected > 开启流式< / option > < option value = "0" > 关闭(同步)< / option > < / select >
< / div >
< div class = "fg" >
< label > locale< / label >
< select id = "cfg-locale" > < option value = "zh-CN" selected > 中文< / option > < option value = "en" > English< / option > < / select >
< / div >
< / div >
< div class = "fg-row" >
< div class = "fg" >
< label > showCategorySwitch< / label >
< select id = "cfg-cat" > < option value = "0" selected > 关闭< / option > < option value = "1" > 开启< / option > < / select >
< / div >
< div class = "fg" >
< label > debug< / label >
< select id = "cfg-debug" > < option value = "1" selected > 开启日志< / option > < option value = "0" > 关闭日志< / option > < / select >
@ -140,585 +159,199 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans S
< hr class = "divider" >
< div style = "font-size:12px;color:#6B7280;line-height:1.6" >
< b > 💡 操作提示< / b > < br >
1. 点击「初始化 SDK」→ 右下角出现悬浮按钮< br >
2. 点击悬浮按钮 → 打开聊天弹窗< br >
3. 在弹窗里输入问题 → 回车发送< br >
4. 观察 AI 回复(流式/同步)< br >
5. 等待 30s 后刷新页面 → 历史恢复
1. integrateId 填写客服角色 ID(数字,对应后端 roleId)< br >
2. userId 填写客户账号 ID(对应后端 accountId)< br >
3. chatId 自动管理:从 /conversation/list 获取或自动生成< br >
4. 对话历史从后端加载(点击头部时钟图标查看)< br >
5. AI 回复支持 Markdown 渲染< br >
6. 开启 showCategorySwitch 可选择知识库分类
< / div >
< / div >
< / div >
<!-- ===== 右侧测试区 ===== -->
< div class = "main" >
< div class = "toolbar" >
< h2 > 📋 测试用例< / h2 >
< button class = "btn btn-primary btn-sm" onclick = "runAll()" > ▶ 运行全部(约 10s)< / button >
< div style = "display:flex;gap:8px;align-items:center" >
< div class = "toolbar-tabs" >
< button class = "toolbar-tab toolbar-tab--active" onclick = "filterTests('all', this)" > 全部< / button >
< button class = "toolbar-tab" onclick = "filterTests('p0', this)" > P0< / button >
< button class = "toolbar-tab" onclick = "filterTests('p1', this)" > P1< / button >
< button class = "toolbar-tab" onclick = "filterTests('p2', this)" > P2< / button >
< / div >
< button class = "btn btn-primary btn-sm" onclick = "runAll()" > ▶ 运行全部< / button >
< / div >
< / div >
<!-- 概览 -->
< div style = "padding:20px 20px 0" >
< div class = "overview" >
< div class = "card-stat" > < div class = "num num--total" id = "total-tests" > 12< / div > < div class = "lbl" > 总用例数< / div > < / div >
< div class = "card-stat" > < div class = "num num--total" id = "total-tests" > 2 2< / div > < div class = "lbl" > 总用例数< / div > < / div >
< div class = "card-stat" > < div class = "num num--ok" id = "pass-count" > 0< / div > < div class = "lbl" > 通过< / div > < / div >
< div class = "card-stat" > < div class = "num num--err" id = "fail-count" > 0< / div > < div class = "lbl" > 失败< / div > < / div >
< div class = "card-stat" > < div class = "num" id = "api-count" style = "color:#2563EB" > 0< / div > < div class = "lbl" > API 调用< / div > < / div >
< div class = "card-stat" > < div class = "num" id = "speed-ms" style = "color:#7C3AED" > -< / div > < div class = "lbl" > 平均延迟< / div > < / div >
< div class = "card-stat" > < div class = "num" id = "skip-count" style = "color:#D97706" > 0< / div > < div class = "lbl" > 跳过< / div > < / div >
< / div >
< / div >
< div class = "cases" id = "cases-container" >
<!-- 动态生成 -->
< / div >
< div class = "cases" id = "cases-container" > < / div >
< div class = "footer" >
< span id = "footer-info" > ChatbotSDK v1.0.0-P 0 | 后端:检测中...< / span >
< span id = "footer-info" > ChatbotSDK v1.2.0 | 后端:检测中...< / span >
< span id = "footer-time" > < / span >
< / div >
< / div >
< / div >
<!-- 引入 SDK -->
< script src = "/sdk/chatbot-sdk.min.js" > < / script >
< script >
(function() {
'use strict';
let passCount = 0, failCount = 0, skipCount = 0, apiCalls = 0, apiDurations = [], currentFilter = 'all';
// ========== 全局状态 ==========
let passCount = 0;
let failCount = 0;
let apiCalls = 0;
let apiDurations = []; // 存储每次API调用的耗时
// ========== 日志工具 ==========
function getLog(id) { return document.getElementById(id); }
function setEl(id, text) { const e = getLog(id); if (e) e.textContent = text; }
function setHtml(id, html) { const e = getLog(id); if (e) e.innerHTML = html; }
function appendLog(id, text, cls) {
const el = getLog(id);
if (!el) return;
const span = document.createElement('span');
span.textContent = text + '\n';
span.className = cls || '';
el.appendChild(span);
el.scrollTop = el.scrollHeight;
}
function clearLog(id) {
const el = getLog(id);
if (el) el.innerHTML = '';
}
function getEl(id) { return document.getElementById(id); }
function setEl(id, text) { const e = getEl(id); if (e) e.textContent = text; }
function updateStats() {
setEl('pass-count', passCount);
setEl('fail-count', failCount);
setEl('api-count', apiCalls);
if (apiDurations.length > 0) {
const avg = Math.round(apiDurations.reduce((a, b) => a + b, 0) / apiDurations.length);
setEl('speed-ms', avg + 'ms');
}
setEl('pass-count', passCount); setEl('fail-count', failCount);
setEl('skip-count', skipCount); setEl('api-count', apiCalls);
if (apiDurations.length > 0) setEl('speed-ms', Math.round(apiDurations.reduce((a,b)=>a+b,0)/apiDurations.length)+'ms');
}
// ========== 测试框架 ==========
async function runTest(id, name, desc, fn) {
const container = getLog('cases-container');
async function runTest(id, name, desc, fn, phase) {
const container = getEl('cases-container');
const caseEl = document.createElement('div');
caseEl.className = 'case';
caseEl.id = 'case-' + id;
caseEl.innerHTML = `
< div class = "case-header" >
< span class = "case-title" > ${id}. ${name}< / span >
< span class = "tag tag--idle" id = "tag-${id}" > ⏳ 等待< / span >
< / div >
< div class = "case-body" >
< div class = "case-desc" > ${desc}< / div >
< div class = "logs" id = "log-${id}" > < / div >
< / div >
`;
caseEl.className = 'case'; caseEl.id = 'case-'+id; caseEl.dataset.phase = phase||'p0';
const badgeClass = phase==='p1'?'badge-p1':phase==='p2'?'badge-p2':'badge-p0';
caseEl.innerHTML = `< div class = "case-header" > < span class = "case-title" > ${id}. ${name}< span class = "case-badge ${badgeClass}" > ${(phase||'p0').toUpperCase()}< / span > < / span > < span class = "tag tag--idle" id = "tag-${id}" > ⏳ 等待< / span > < / div > < div class = "case-body" > < div class = "case-desc" > ${desc}< / div > < div class = "logs" id = "log-${id}" > < / div > < / div > `;
container.appendChild(caseEl);
const tagEl = getLog('tag-' + id);
const logId = 'log-' + id;
function mark(status, text) {
if (status === 'running') {
tagEl.className = 'tag tag--running';
tagEl.textContent = '🔄 ' + text;
} else if (status === 'pass') {
tagEl.className = 'tag tag--pass';
tagEl.textContent = '✅ ' + text;
passCount++;
} else {
tagEl.className = 'tag tag--fail';
tagEl.textContent = '❌ ' + text;
failCount++;
}
const tagEl = getEl('tag-'+id), logId = 'log-'+id;
function mark(s, t) {
if(s==='running'){tagEl.className='tag tag--running';tagEl.textContent='🔄 '+t;}
else if(s==='pass'){tagEl.className='tag tag--pass';tagEl.textContent='✅ '+t;passCount++;}
else if(s==='skip'){tagEl.className='tag tag--idle';tagEl.textContent='⏭ '+t;skipCount++;}
else{tagEl.className='tag tag--fail';tagEl.textContent='❌ '+t;failCount++;}
updateStats();
}
appendLog(logId, '开始执行...', 'log-line--info');
mark('running', '执行中');
try {
await fn(logId, mark);
// 如果 fn 没有主动 mark('pass'),这里自动标记
if (tagEl.className === 'tag tag--running') {
mark('pass', '通过');
}
} catch (e) {
appendLog(logId, '异常: ' + (e.message || e), 'log-line--fail');
mark('fail', e.message || '未知错误');
}
function log(t,c){const el=getEl(logId);if(!el)return;const s=document.createElement('span');s.textContent=t+'\n';s.className=c||'';el.appendChild(s);el.scrollTop=el.scrollHeight;}
log('开始执行...','log-line--info'); mark('running','执行中');
try { await fn(log, mark); if(tagEl.className==='tag tag--running') mark('pass','通过'); }
catch(e) { log('异常: '+(e.message||e),'log-line--fail'); mark('fail',e.message||'未知错误'); }
}
// ========== 辅助函数 ==========
function getCfg() {
return {
integrateId: getLog('cfg-iid').value,
requestDomain: getLog('cfg-domain').value || window.location.origin,
userId: getLog('cfg-uid').value || undefined,
title: getLog('cfg-title').value,
primaryColor: getLog('cfg-color').value,
position: getLog('cfg-pos').value,
width: parseInt(getLog('cfg-width').value) || 380,
streaming: getLog('cfg-stream').value === '1',
debug: getLog('cfg-debug').value === '1',
integrateId: getEl('cfg-iid').value, // 对应后端 roleId
requestDomain: getEl('cfg-domain').value || window.location.origin,
userId: getEl('cfg-uid').value || undefined, // 对应后端 accountId
title: getEl('cfg-title').value,
primaryColor: getEl('cfg-color').value,
position: getEl('cfg-pos').value,
width: parseInt(getEl('cfg-width').value) || 380,
streaming: getEl('cfg-stream').value === '1',
locale: getEl('cfg-locale').value,
showCategorySwitch: getEl('cfg-cat').value === '1',
debug: getEl('cfg-debug').value === '1',
showClear: true,
};
}
function sleep(ms){return new Promise(r=>setTimeout(r,ms));}
function assert(c,m){if(!c)throw new Error(m);}
function assertExists(s,m){const el=document.getElementById(s)||document.querySelector(s);assert(!!el,m||s+' 不存在');return el;}
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
function assert(cond, msg) { if (!cond) throw new Error(msg); }
function assertExists(sel, msg) {
const el = document.getElementById(sel) || document.querySelector(sel);
assert(!!el, msg || (sel + ' 不存在'));
return el;
}
// ========== 对外快捷操作 ==========
window.doInit = function() {
ChatbotSDK.destroy();
ChatbotSDK.init(getCfg());
const tag = getLog('tag-sdk');
if (document.getElementById('csk-launcher')) {
tag.className = 'tag tag--pass';
tag.textContent = '✅ SDK 就绪';
}
};
window.doInit=function(){ChatbotSDK.destroy();ChatbotSDK.init(getCfg());const tag=getEl('tag-sdk');if(document.getElementById('csk-launcher')){tag.className='tag tag--pass';tag.textContent='✅ SDK 就绪';}};
window.doDestroy=function(){ChatbotSDK.destroy();const tag=getEl('tag-sdk');tag.className='tag tag--idle';tag.textContent='⭕ SDK 未加载';};
window.doClearHistory=function(){ChatbotSDK.clearHistory();};
window.domAction=function(act){if(!document.getElementById('csk-launcher'))ChatbotSDK.init(getCfg());ChatbotSDK[act]();};
window.filterTests=function(phase,btn){currentFilter=phase;document.querySelectorAll('.toolbar-tab').forEach(t=>t.classList.remove('toolbar-tab--active'));btn.classList.add('toolbar-tab--active');document.querySelectorAll('.case').forEach(c=>{c.style.display=(phase==='all'||c.dataset.phase===phase)?'':'none';});};
window.doDestroy = function() {
ChatbotSDK.destroy();
const tag = getLog('tag-sdk');
tag.className = 'tag tag--idle';
tag.textContent = '⭕ SDK 未加载';
};
// ==================== P0 ====================
async function test1(log,m){log('检测 window.ChatbotSDK...','log-line--info');assert(typeof window.ChatbotSDK!=='undefined','window.ChatbotSDK 未定义');const ms=['init','destroy','open','close','toggle','clearHistory'];for(const x of ms){assert(typeof window.ChatbotSDK[x]==='function','ChatbotSDK.'+x+' 不是函数');log('✅ ChatbotSDK.'+x+'()','log-line--pass');}m('pass','通过 ('+ms.length+' 个方法)');}
window.doClearHistory = function() { ChatbotSDK.clearHistory(); };
async function test2(log,m){ChatbotSDK.init({});await sleep(200);assert(!document.getElementById('csk-launcher'),'缺失 integrateId 时不应创建 DOM');log('✅ 缺失 integrateId 正确拦截','log-line--pass');m('pass','通过');}
window.domAction = function(act) {
if (!document.getElementById('csk-launcher')) {
ChatbotSDK.init(getCfg());
}
ChatbotSDK[act]();
};
async function test3(log,m){ChatbotSDK.init({integrateId:'t'});await sleep(200);assert(!document.getElementById('csk-launcher'),'缺失 requestDomain 时不应创建 DOM');log('✅ 缺失 requestDomain 正确拦截','log-line--pass');m('pass','通过');}
// ========== P0 测试用例定义 ==========
async function test4(log,m){ChatbotSDK.init({integrateId:'t',requestDomain:'abc'});await sleep(200);assert(!document.getElementById('csk-launcher'),'非法 URL 时不应创建 DOM');log('✅ 非法 URL 正确拦截','log-line--pass');m('pass','通过');}
// T1: SDK 全局挂载检测
function test1_globalMount(logId, mark) {
appendLog(logId, '检测 window.ChatbotSDK...', 'log-line--info');
assert(typeof window.ChatbotSDK !== 'undefined', 'window.ChatbotSDK 未定义,script 引入失败');
appendLog(logId, '✅ window.ChatbotSDK 存在', 'log-line--pass');
async function test5(log,m){ChatbotSDK.destroy();await sleep(100);ChatbotSDK.init(getCfg());await sleep(500);assertExists('csk-launcher');log('✅ 悬浮按钮已创建','log-line--pass');assertExists('csk-window');log('✅ 弹窗已创建','log-line--pass');assertExists('#csk-messages');assertExists('#csk-input');assertExists('#csk-send-btn');log('✅ 消息区/输入框/发送按钮齐全','log-line--pass');assert(!!document.querySelector('style[data-csk-sdk]'),'CSS 未注入');log('✅ CSS 已注入','log-line--pass');m('pass','通过');}
const methods = ['init', 'destroy', 'open', 'close', 'toggle', 'clearHistory'];
for (const m of methods) {
assert(typeof window.ChatbotSDK[m] === 'function', `ChatbotSDK.${m} 不是函数`);
appendLog(logId, '✅ ChatbotSDK.' + m + '() 可用', 'log-line--pass');
}
mark('pass', '通过 (' + methods.length + ' 个方法)');
}
async function test6(log,m){ChatbotSDK.destroy();await sleep(100);ChatbotSDK.init(getCfg());await sleep(300);const w=document.getElementById('csk-window');ChatbotSDK.open();await sleep(100);assert(!w.classList.contains('csk-window--hidden'),'open() 失败');log('✅ open()','log-line--pass');ChatbotSDK.close();await sleep(100);assert(w.classList.contains('csk-window--hidden'),'close() 失败');log('✅ close()','log-line--pass');m('pass','通过');}
// T2: 配置校验 — 缺失 integrateId
async function test2_missingIntegrateId(logId, mark) {
appendLog(logId, '调用 ChatbotSDK.init({})', 'log-line--info');
ChatbotSDK.init({});
await sleep(200);
const launcher = document.getElementById('csk-launcher');
assert(!launcher, '缺失 integrateId 时不应创建 DOM');
appendLog(logId, '✅ 缺失 integrateId 正确拦截,无 DOM 产生', 'log-line--pass');
mark('pass', '通过');
}
async function test7(log,m){ChatbotSDK.destroy();await sleep(100);ChatbotSDK.init({integrateId:'1',requestDomain:getCfg().requestDomain});await sleep(300);const h=document.querySelector('#csk-window .csk-header__title');assert(h& & h.textContent==='AI 智能助手','默认标题错误');log('✅ 默认标题 "AI 智能助手"','log-line--pass');m('pass','通过');}
// T3: 配置校验 — 缺失 requestDomain
async function test3_missingDomain(logId, mark) {
appendLog(logId, '调用 ChatbotSDK.init({integrateId:"t"})', 'log-line--info');
ChatbotSDK.init({integrateId:'t'});
await sleep(200);
const launcher = document.getElementById('csk-launcher');
assert(!launcher, '缺失 requestDomain 时不应创建 DOM');
appendLog(logId, '✅ 缺失 requestDomain 正确拦截', 'log-line--pass');
mark('pass', '通过');
}
async function test8(log,m){ChatbotSDK.destroy();await sleep(100);ChatbotSDK.init({integrateId:'1',requestDomain:getCfg().requestDomain,primaryColor:'#DC2626',title:'测试'});await sleep(300);const s=document.querySelector('style[data-csk-sdk]');assert(s& & s.textContent.includes('#DC2626'),'CSS 应包含自定义主题色');log('✅ #DC2626 已注入','log-line--pass');m('pass','通过');}
// T4: 配置校验 — 非法 URL
async function test4_badUrl(logId, mark) {
appendLog(logId, '调用 init({integrateId:"t", requestDomain:"abc"})', 'log-line--info');
ChatbotSDK.init({integrateId:'t', requestDomain:'abc'});
await sleep(200);
const launcher = document.getElementById('csk-launcher');
assert(!launcher, '非法 URL 时不应创建 DOM');
appendLog(logId, '✅ 非法 URL 正确拦截', 'log-line--pass');
mark('pass', '通过');
}
async function test9(log,m){ChatbotSDK.destroy();await sleep(100);ChatbotSDK.init({integrateId:'1',requestDomain:getCfg().requestDomain,position:'left-bottom'});await sleep(300);const l=document.getElementById('csk-launcher');assert(l.classList.contains('csk-launcher--left'),'left 位置类');log('✅ position=left-bottom','log-line--pass');m('pass','通过');}
// T5: 正常初始化 — DOM 创建
async function test5_domCreate(logId, mark) {
ChatbotSDK.destroy();
await sleep(100);
ChatbotSDK.init(getCfg());
await sleep(300);
async function test10(log,m){ChatbotSDK.destroy();await sleep(100);ChatbotSDK.init(getCfg());await sleep(300);assertExists('csk-launcher');assertExists('csk-window');ChatbotSDK.destroy();await sleep(200);assert(!document.getElementById('csk-launcher'),'destroy 后按钮应移除');log('✅ 按钮已移除','log-line--pass');assert(!document.getElementById('csk-window'),'destroy 后弹窗应移除');log('✅ 弹窗已移除','log-line--pass');assert(document.querySelectorAll('style[data-csk-sdk]').length===0,'CSS 应移除');log('✅ CSS 已移除','log-line--pass');m('pass','通过');}
const launcher = assertExists('csk-launcher', '悬浮按钮未创建');
appendLog(logId, '✅ #csk-launcher 已创建', 'log-line--pass');
async function test11(log,m){ChatbotSDK.destroy();await sleep(100);ChatbotSDK.init(getCfg());await sleep(500);ChatbotSDK.open();await sleep(300);const input=getEl('csk-input');if(!input){log('⚠ 输入框不存在','log-line--warn');m('skip','输入框不存在');return;}input.value='你好';input.dispatchEvent(new Event('input'));await sleep(100);const sendBtn=getEl('csk-send-btn');if(sendBtn)sendBtn.click();log('✅ 点击发送按钮','log-line--info');await sleep(8000);const aiMsgs=document.querySelectorAll('.csk-msg--ai .csk-msg__bubble');if(aiMsgs.length>0){const lastAi=aiMsgs[aiMsgs.length-1];log('AI 回复: '+(lastAi.textContent||'').substring(0,100),'log-line--pass');m('pass','通过(有AI回复)');}else{log('⚠ 等待 AI 回复超时','log-line--warn');m('skip','AI 未回复');}}
const win = assertExists('csk-window', '聊天弹窗未创建');
appendLog(logId, '✅ #csk-window 已创建', 'log-line--pass');
async function test12(log,m){const cfg=getCfg();const url=cfg.requestDomain+'/ai/assistant_app/chat/sync?message='+encodeURIComponent('你好')+'&chatId=verify_test&roleId='+cfg.integrateId; apiCalls++;const t0=performance.now();let r;try{r=await fetch(url,{signal:AbortSignal.timeout(15000)});}catch(e){log('❌ fetch 失败','log-line--fail');throw new Error('后端未运行');}const el=Math.round(performance.now()-t0);apiDurations.push(el);updateStats();assert(r.ok,'HTTP '+r.status);const text=await r.text();assert(text.length>3,'回复过短');log('AI 回复: '+text.substring(0,100),'log-line--pass');log('参数: chatId=verify_test roleId='+cfg.integrateId,'log-line--info');m('pass','通过 ('+el+'ms)');}
assert(win.classList.contains('csk-window--hidden'), '初始状态应为隐藏');
appendLog(logId, '✅ 初始隐藏状态正确', 'log-line--pass');
async function test13(log,m){const cfg=getCfg();const url=cfg.requestDomain+'/ai/assistant_app/chat/sse?message='+encodeURIComponent('你好')+'&chatId=verify_sse&roleId='+cfg.integrateId; apiCalls++;const t0=performance.now();let r;try{r=await fetch(url,{signal:AbortSignal.timeout(20000)});}catch(e){log('❌ SSE 失败','log-line--fail');throw new Error('SSE 失败');}assert(r.ok,'HTTP '+r.status);const reader=r.body.getReader();const decoder=new TextDecoder();let chunks=0,total='';try{while(true){const x=await reader.read();if(x.done)break;total+=decoder.decode(x.value,{stream:true});chunks++;}}finally{reader.releaseLock();}const el=Math.round(performance.now()-t0);apiDurations.push(el);updateStats();assert(total.length>3,'SSE 回复过短');log('✅ SSE '+chunks+' chunks, '+total.length+' chars','log-line--pass');m('pass','通过 ('+el+'ms)');}
assertExists('#csk-messages', '消息区未创建');
assertExists('#csk-input', '输入框未创建');
assertExists('#csk-send-btn', '发送按钮未创建');
appendLog(logId, '✅ 消息区/输入框/发送按钮 齐全', 'log-line--pass');
// ==================== P1 ====================
async function test14(log,m){ChatbotSDK.destroy();await sleep(100);ChatbotSDK.init(getCfg());await sleep(300);const s=document.querySelector('style[data-csk-sdk]');const css=s?s.textContent:'';assert(css.includes('csk-md-code-block'),'代码块样式');log('✅ 代码块样式','log-line--pass');assert(css.includes('csk-md-link'),'链接样式');log('✅ 链接样式','log-line--pass');m('pass','通过');}
const style = document.querySelector('style[data-csk-sdk]');
assert(!!style, 'CSS 样式未注入');
appendLog(logId, '✅ CSS 样式已注入 (data-csk-sdk)', 'log-line--pass');
async function test15(log,m){ChatbotSDK.destroy();await sleep(100);ChatbotSDK.init(Object.assign(getCfg(),{showCategorySwitch:true}));await sleep(300);const sel=document.getElementById('csk-category-select');assert(!!sel,'分类下拉框未创建');log('✅ 分类下拉框已创建','log-line--pass');ChatbotSDK.destroy();await sleep(100);ChatbotSDK.init(Object.assign(getCfg(),{showCategorySwitch:false}));await sleep(300);assert(!document.getElementById('csk-category-select'),'关闭后不应创建');log('✅ showCategorySwitch=false','log-line--pass');m('pass','通过');}
mark('pass', '通过');
}
async function test16(log,m){const cfg=getCfg();const url=cfg.requestDomain+'/ai/assistant_app/chat/rag/sse?message='+encodeURIComponent('你好')+'&chatId=verify_rag&roleId='+cfg.integrateId+'&rewriteStrategy=REWRITE'; apiCalls++;log('请求 RAG SSE...','log-line--info');let r;try{r=await fetch(url,{signal:AbortSignal.timeout(20000)});}catch(e){log('⚠ RAG SSE 失败','log-line--warn');m('skip','后端不可用');return;}if(!r.ok){log('⚠ HTTP '+r.status,'log-line--warn');m('skip','接口异常');return;}const reader=r.body.getReader();const decoder=new TextDecoder();let total='';try{while(true){const x=await reader.read();if(x.done)break;total+=decoder.decode(x.value,{stream:true});}}finally{reader.releaseLock();}assert(total.length>3,'RAG 回复过短');log('✅ RAG SSE 回复 '+total.length+' chars','log-line--pass');m('pass','通过');}
// T6: open/close/toggle API
async function test6_openClose(logId, mark) {
ChatbotSDK.destroy();
await sleep(100);
ChatbotSDK.init(getCfg());
await sleep(300);
const win = document.getElementById('csk-window');
assert(win, '弹窗未初始化');
ChatbotSDK.open();
await sleep(100);
assert(!win.classList.contains('csk-window--hidden'), 'open() 失败');
appendLog(logId, '✅ open() 正确显示弹窗', 'log-line--pass');
ChatbotSDK.close();
await sleep(100);
assert(win.classList.contains('csk-window--hidden'), 'close() 失败');
appendLog(logId, '✅ close() 正确隐藏弹窗', 'log-line--pass');
ChatbotSDK.toggle();
await sleep(100);
assert(!win.classList.contains('csk-window--hidden'), 'toggle() 第一次失败');
ChatbotSDK.toggle();
await sleep(100);
assert(win.classList.contains('csk-window--hidden'), 'toggle() 第二次失败');
appendLog(logId, '✅ toggle() 正确切换显示/隐藏', 'log-line--pass');
mark('pass', '通过');
}
async function test17(log,m){const cfg=getCfg();const url=cfg.requestDomain+'/ai/assistant_app/rag/sources?message='+encodeURIComponent('请假')+'&chatId=verify_sources&roleId='+cfg.integrateId+'&rewriteStrategy=REWRITE'; apiCalls++;let r;try{r=await fetch(url,{signal:AbortSignal.timeout(15000)});}catch(e){log('⚠ 请求失败','log-line--warn');m('skip','后端不可用');return;}if(!r.ok){log('⚠ HTTP '+r.status,'log-line--warn');m('skip','接口异常');return;}const json=await r.json();log('返回 success='+json.success+' data='+(json.data?json.data.length:0)+' 条','log-line--info');assert(json.success!==undefined,'应返回 success 字段');log('✅ RAG 引用来源接口可用','log-line--pass');m('pass','通过');}
// T7: 配置默认值
async function test7_defaults(logId, mark) {
ChatbotSDK.destroy();
await sleep(100);
async function test18(log,m){const cfg=getCfg();const url=cfg.requestDomain+'/category/tree';apiCalls++;let r;try{r=await fetch(url,{signal:AbortSignal.timeout(10000)});}catch(e){log('⚠ 请求失败','log-line--warn');m('skip','后端不可用');return;}if(!r.ok){m('skip','HTTP '+r.status);return;}const json=await r.json();log('✅ 分类树 '+(json.data?json.data.length:0)+' 个根节点','log-line--pass');m('pass','通过');}
// 只传必传参数,验证默认值是否生效
ChatbotSDK.init({ integrateId: 'test-defaults', requestDomain: getCfg().requestDomain });
await sleep(300);
// ==================== P2 ====================
async function test19(log,m){ChatbotSDK.destroy();await sleep(100);ChatbotSDK.init(Object.assign(getCfg(),{locale:'zh-CN'}));await sleep(300);const ph=document.querySelector('#csk-input');assert(ph& & ph.placeholder==='输入您的问题...','中文 placeholder');log('✅ 中文界面','log-line--pass');ChatbotSDK.destroy();await sleep(100);ChatbotSDK.init(Object.assign(getCfg(),{locale:'en'}));await sleep(300);const phEn=document.querySelector('#csk-input');assert(phEn& & phEn.placeholder==='Type your question...','英文 placeholder');log('✅ 英文界面','log-line--pass');m('pass','通过');}
const header = document.querySelector('#csk-window .csk-header__title');
assert(header & & header.textContent === 'AI 智能助手', '默认标题应为 AI 智能助手');
appendLog(logId, '✅ 默认标题 "AI 智能助手"', 'log-line--pass');
async function test20(log,m){ChatbotSDK.destroy();await sleep(100);ChatbotSDK.init(getCfg());await sleep(300);const hb=document.querySelector('.csk-history-btn');assert(!!hb,'历史按钮');log('✅ 历史按钮已创建','log-line--pass');const hp=document.querySelector('.csk-history-panel');assert(!!hp,'历史面板');log('✅ 历史面板已创建','log-line--pass');assert(hp.classList.contains('csk-history-panel--hidden'),'初始隐藏');log('✅ 初始隐藏','log-line--pass');m('pass','通过');}
const launcher = document.getElementById('csk-launcher');
assert(launcher.classList.contains('csk-launcher--right'), '默认位置应为右侧');
appendLog(logId, '✅ 默认位置 right-bottom', 'log-line--pass');
async function test21(log,m){const cfg=getCfg();const url=cfg.requestDomain+'/conversation/list?page=1&size=10&roleId='+cfg.integrateId; apiCalls++;log('请求会话列表 (roleId='+cfg.integrateId+')...','log-line--info');let r;try{r=await fetch(url,{signal:AbortSignal.timeout(10000)});}catch(e){log('⚠ 请求失败','log-line--warn');m('skip','后端不可用');return;}if(!r.ok){m('skip','HTTP '+r.status);return;}const json=await r.json();log('✅ 会话列表 total='+json.total,'log-line--pass');m('pass','通过');}
const style = document.querySelector('style[data-csk-sdk]');
assert(style & & style.textContent.includes('#4F46E5'), '默认主题色应为 #4F46E5');
appendLog(logId, '✅ 默认主题色 #4F46E5', 'log-line--pass');
async function test22(log,m){const cfg=getCfg();if(cfg.userId){const url=cfg.requestDomain+'/conversation/list?page=1&size=10&accountId='+encodeURIComponent(cfg.userId)+'&roleId='+cfg.integrateId; apiCalls++;log('请求会话列表 (accountId='+cfg.userId+', roleId='+cfg.integrateId+')...','log-line--info');let r;try{r=await fetch(url,{signal:AbortSignal.timeout(10000)});}catch(e){log('⚠ 请求失败','log-line--warn');m('skip','后端不可用');return;}if(!r.ok){m('skip','HTTP '+r.status);return;}const json=await r.json();log('✅ 按账号+角色过滤 total='+json.total,'log-line--pass');m('pass','通过');}else{log('⚠ 未设置 userId,跳过','log-line--warn');m('skip','未设置 userId');}}
const clearBtn = document.querySelector('.csk-clear-btn');
assert(!!clearBtn, '默认应显示清空按钮');
appendLog(logId, '✅ showClear 默认 true', 'log-line--pass');
// ========== 参数映射验证 ==========
async function test23(log,m){log('验证参数映射:integrateId→roleId, userId→accountId','log-line--info');const cfg=getCfg();const msg='test_mapping';const url=cfg.requestDomain+'/ai/assistant_app/chat/sync?message='+encodeURIComponent(msg)+'&chatId=verify_mapping&roleId='+cfg.integrateId+(cfg.userId?'&accountId='+encodeURIComponent(cfg.userId):''); log('请求 URL: '+url.substring(0,120)+'...','log-line--info');apiCalls++;let r;try{r=await fetch(url,{signal:AbortSignal.timeout(15000)});}catch(e){log('⚠ fetch 失败','log-line--warn');m('skip','后端不可用');return;}if(!r.ok){log('⚠ HTTP '+r.status,'log-line--warn');m('skip','接口异常');return;}const text=await r.text();assert(text.length>0,'应有回复');log('✅ integrateId('+cfg.integrateId+')→roleId, userId('+(cfg.userId||'未设置')+')→accountId 映射正确','log-line--pass');log('AI 回复: '+text.substring(0,80),'log-line--info');m('pass','通过');}
mark('pass', '通过');
}
// T8: 主题色定制
async function test8_themeColor(logId, mark) {
ChatbotSDK.destroy();
await sleep(100);
ChatbotSDK.init({ integrateId:'t-color', requestDomain:getCfg().requestDomain, primaryColor:'#DC2626', title:'测试' });
await sleep(300);
const style = document.querySelector('style[data-csk-sdk]');
const css = style ? style.textContent : '';
assert(css.includes('#DC2626'), 'CSS 中应包含自定义主题色');
appendLog(logId, '✅ CSS 中已包含自定义主题色 #DC2626', 'log-line--pass');
const header = document.querySelector('#csk-window .csk-header');
assert(header, '头部不存在');
const headerBg = window.getComputedStyle(header).backgroundColor;
appendLog(logId, ' 头部 computed background: ' + headerBg, 'log-line--info');
mark('pass', '通过');
}
// T9: 位置切换(左下)
async function test9_position(logId, mark) {
ChatbotSDK.destroy();
await sleep(100);
ChatbotSDK.init({ integrateId:'t-pos', requestDomain:getCfg().requestDomain, position:'left-bottom' });
await sleep(300);
const launcher = document.getElementById('csk-launcher');
assert(launcher.classList.contains('csk-launcher--left'), '应包含 left 位置类');
assert(!launcher.classList.contains('csk-launcher--right'), '不应包含 right 位置类');
appendLog(logId, '✅ position=left-bottom 正确生效', 'log-line--pass');
const win = document.getElementById('csk-window');
assert(win.classList.contains('csk-window--left'), '弹窗也应左对齐');
appendLog(logId, '✅ 弹窗位置同步为左侧', 'log-line--pass');
mark('pass', '通过');
}
// T10: destroy 清理
async function test10_destroy(logId, mark) {
ChatbotSDK.destroy();
await sleep(100);
ChatbotSDK.init(getCfg());
await sleep(300);
assertExists('csk-launcher');
assertExists('csk-window');
ChatbotSDK.destroy();
await sleep(200);
const l = document.getElementById('csk-launcher');
const w = document.getElementById('csk-window');
assert(!l, 'destroy 后悬浮按钮应被移除');
appendLog(logId, '✅ 悬浮按钮已从 DOM 移除', 'log-line--pass');
assert(!w, 'destroy 后弹窗应被移除');
appendLog(logId, '✅ 聊天弹窗已从 DOM 移除', 'log-line--pass');
const styles = document.querySelectorAll('style[data-csk-sdk]');
assert(styles.length === 0, 'destroy 后样式应被移除(当前' + styles.length + '个)');
appendLog(logId, '✅ CSS 样式已移除', 'log-line--pass');
mark('pass', '通过');
}
// T11: localStorage 缓存
async function test11_storage(logId, mark) {
const key = 'csk_history_test-sdk-p0';
// 写入测试数据
const data = {
messages: [
{ id:'a', role:'user', content:'你好', timestamp: Date.now()-2000 },
{ id:'b', role:'ai', content:'您好!', timestamp: Date.now()-1000 },
],
updatedAt: Date.now()
};
localStorage.setItem(key, JSON.stringify(data));
appendLog(logId, '写入测试数据 key=' + key + ' messages=2', 'log-line--info');
// 通过 init 恢复(需要 SDK 内部调用 loadMessages)
ChatbotSDK.destroy();
await sleep(100);
ChatbotSDK.init(Object.assign(getCfg(), { integrateId:'test-sdk-p0' }));
await sleep(400);
// 验证缓存是否被 SDK 读取
const raw = localStorage.getItem(key);
const parsed = raw ? JSON.parse(raw) : null;
assert(parsed & & Array.isArray(parsed.messages) & & parsed.messages.length === 2,
'缓存应包含2条消息,实际: ' + (parsed ? JSON.stringify(parsed).substring(0, 80) : 'null'));
appendLog(logId, '✅ 缓存读取成功,共 ' + parsed.messages.length + ' 条消息', 'log-line--pass');
// 检查历史是否渲染
const messagesEl = document.getElementById('csk-messages');
const bubbles = messagesEl ? messagesEl.querySelectorAll('.csk-msg') : [];
appendLog(logId, '消息区渲染气泡数: ' + bubbles.length, 'log-line--info');
// 清理
localStorage.removeItem(key);
appendLog(logId, '✅ 缓存读写验证通过', 'log-line--pass');
mark('pass', '通过');
}
// T12: API 同步对话
async function test12_apiChat(logId, mark) {
const cfg = getCfg();
const url = cfg.requestDomain + '/ai/assistant_app/chat/sync?message=' + encodeURIComponent('你好,请用一句话介绍你自己') + '& chatId=' + cfg.integrateId;
appendLog(logId, '请求: GET ' + url.substring(0, 60) + '...', 'log-line--info');
apiCalls++;
const t0 = performance.now();
let resp;
try {
resp = await fetch(url, { signal: AbortSignal.timeout(15000) });
} catch(e) {
appendLog(logId, '❌ fetch 失败: ' + e.message, 'log-line--fail');
throw new Error('网络请求失败 — 后端可能未运行');
}
const elapsed = Math.round(performance.now() - t0);
apiDurations.push(elapsed);
updateStats();
appendLog(logId, 'HTTP ' + resp.status + ' 耗时 ' + elapsed + 'ms', 'log-line--info');
if (!resp.ok) {
appendLog(logId, '响应异常 status=' + resp.status, 'log-line--fail');
throw new Error('API 返回 ' + resp.status);
}
const text = await resp.text();
assert(text.length > 5, 'AI 回复过短: ' + text.substring(0, 30));
appendLog(logId, 'AI 回复: ' + text.substring(0, 120) + (text.length > 120 ? '...' : ''), 'log-line--pass');
appendLog(logId, '回复长度: ' + text.length + ' 字符', 'log-line--info');
mark('pass', '通过 (' + elapsed + 'ms)');
}
// T13: SSE 流式对话
async function test13_apiSSE(logId, mark) {
const cfg = getCfg();
const url = cfg.requestDomain + '/ai/assistant_app/chat/sse?message=' + encodeURIComponent('你好') + '& chatId=' + cfg.integrateId;
appendLog(logId, '请求 SSE: GET ' + url.substring(0, 60) + '...', 'log-line--info');
apiCalls++;
const t0 = performance.now();
let resp;
try {
resp = await fetch(url, { signal: AbortSignal.timeout(20000) });
} catch(e) {
appendLog(logId, '❌ fetch 失败: ' + e.message, 'log-line--fail');
throw new Error('SSE 连接失败 — 后端可能未运行');
}
assert(resp.ok, 'SSE 响应异常 HTTP ' + resp.status);
appendLog(logId, 'SSE 连接成功 HTTP ' + resp.status, 'log-line--pass');
const reader = resp.body.getReader();
const decoder = new TextDecoder('utf-8');
let chunks = 0;
let total = '';
try {
while (true) {
const result = await reader.read();
if (result.done) break;
const text = decoder.decode(result.value, { stream: true });
total += text;
chunks++;
if (chunks < = 3) {
appendLog(logId, 'chunk #' + chunks + ': ' + text.substring(0, 60), 'log-line--info');
}
}
} finally {
reader.releaseLock();
}
const elapsed = Math.round(performance.now() - t0);
apiDurations.push(elapsed);
updateStats();
appendLog(logId, '共收到 ' + chunks + ' 个数据块, ' + total.length + ' 字符', 'log-line--pass');
appendLog(logId, '完整回复: ' + total.substring(0, 200) + (total.length > 200 ? '...' : ''), 'log-line--pass');
assert(total.length > 3, 'SSE 流回复过短');
mark('pass', '通过 (' + elapsed + 'ms, ' + chunks + ' chunks)');
}
// ========== 运行全部 ==========
window.runAll = async function() {
// 重置
passCount = 0;
failCount = 0;
apiCalls = 0;
apiDurations = [];
updateStats();
ChatbotSDK.destroy();
document.getElementById('cases-container').innerHTML = '';
await sleep(300);
passCount=0;failCount=0;skipCount=0;apiCalls=0;apiDurations=[];
updateStats();ChatbotSDK.destroy();getEl('cases-container').innerHTML='';await sleep(300);
const tests = [
[test1_globalMount, 'T1', '全局挂载', '验证 window.ChatbotSDK 及其 6 个公开方法是否存在'],
[test2_missingIntegrateId, 'T2', '必传参数校验 — integrateId', '传入空对象 init({}),确认 SDK 静默拦截不崩溃'],
[test3_missingDomain, 'T3', '必传参数校验 — requestDomain', '传入 init({integrateId:"t"}),确认 SDK 拦截'],
[test4_badUrl, 'T4', '参数校验 — 非法 URL', '传入非法 requestDomain="abc",确认拦截'],
[test5_domCreate, 'T5', 'DOM 创建', '正常初始化后检查悬浮按钮+弹窗+输入框+样式全部创建'],
[test6_openClose, 'T6', 'open/close/toggle', '验证打开/关闭/切换 API 正常工作'],
[test7_defaults, 'T7', '配置默认值', '不传可选参数,验证标题/位置/主题色/清空按钮默认值'],
[test8_themeColor, 'T8', '主题色定制', '传入 primaryColor=#DC2626,验证 CSS 包含自定义颜色'],
[test9_position, 'T9', '位置切换', '传入 position=left-bottom,验证按钮和弹窗左对齐'],
[test10_destroy, 'T10', 'destroy 清理', '验证 destroy() 完全移除 DOM 和 CSS'],
[test11_storage, 'T11', 'localStorage 缓存', '写入缓存 → init 恢复 → 验证可读 → 清理'],
[test12_apiChat, 'T12', 'API 同步对话', '直接调用后端 /ai/assistant_app/chat/sync 验证连通性'],
[test13_apiSSE, 'T13', 'API SSE 流式', '直接调用后端 /ai/assistant_app/chat/sse 验证流式响应'],
[test1,'T1','全局挂载','验证 window.ChatbotSDK 方法','p0'],
[test2,'T2','必传参数 — integrateId','缺失时拦截','p0'],
[test3,'T3','必传参数 — requestDomain','缺失时拦截','p0'],
[test4,'T4','非法 URL','拦截','p0'],
[test5,'T5','DOM 创建','悬浮按钮+弹窗+样式','p0'],
[test6,'T6','open/close','窗口 API','p0'],
[test7,'T7','配置默认值','标题/位置默认','p0'],
[test8,'T8','主题色定制','primaryColor','p0'],
[test9,'T9','位置切换','left-bottom','p0'],
[test10,'T10','destroy 清理','移除 DOM+CSS','p0'],
[test11,'T11','实际对话','发送消息→AI回复','p0'],
[test12,'T12','API 同步对话','chatId+roleId 参数验证','p0'],
[test13,'T13','API SSE 流式','流式参数验证','p0'],
[test14,'T14','Markdown 渲染','CSS 样式验证','p1'],
[test15,'T15','知识库分类切换','下拉框 UI','p1'],
[test16,'T16','API RAG SSE','RAG 流式接口','p1'],
[test17,'T17','API RAG 引用来源','来源接口','p1'],
[test18,'T18','API 分类树','分类列表接口','p1'],
[test19,'T19','多语言国际化','zh-CN / en','p2'],
[test20,'T20','会话管理面板','UI + 样式','p2'],
[test21,'T21','API 会话列表','按 roleId 过滤','p2'],
[test22,'T22','API 会话按账号过滤','accountId+roleId','p2'],
[test23,'T23','参数映射验证','integrateId→roleId, userId→accountId','p0'],
];
for (const [fn, id, name, desc] of tests) {
await runTest(id, name, desc, fn);
await sleep(150); // 测试间隔
}
// 汇总
setEl('footer-info', 'ChatbotSDK v1.0.0-P0 | ' + passCount + ' 通过 / ' + failCount + ' 失败');
setEl('footer-time', new Date().toLocaleTimeString());
setEl('total-tests', tests.length);
for(const [fn,id,name,desc,phase] of tests){await runTest(id,name,desc,fn,phase);await sleep(150);}
setEl('footer-info','ChatbotSDK v1.2.0 | '+passCount+' 通过 / '+failCount+' 失败 / '+skipCount+' 跳过');
setEl('footer-time',new Date().toLocaleTimeString());
};
// ========== 启动自动检测 ==========
(function boot() {
getLog('cfg-domain').value = window.location.origin;
setEl('footer-time', new Date().toLocaleTimeString());
// 检测 SDK 加载状态
if (typeof window.ChatbotSDK !== 'undefined') {
const tag = getLog('tag-sdk');
tag.className = 'tag tag--pass';
tag.textContent = '✅ SDK 已加载';
}
// 检测后端是否可达
fetch(window.location.origin + '/ai/assistant_app/chat/sync?message=' + encodeURIComponent('test') + '& chatId=__probe__', {
signal: AbortSignal.timeout(5000)
}).then(r => {
const tag = getLog('tag-api');
if (r.ok || r.status < 500 ) {
tag.className = 'tag tag--pass';
tag.textContent = '✅ 后端连通';
setEl('footer-info', 'ChatbotSDK v1.0.0-P0 | 后端:在线 ✅');
}
}).catch(() => {
const tag = getLog('tag-api');
tag.className = 'tag tag--fail';
tag.textContent = '⚠ 后端离线';
});
(function boot(){
getEl('cfg-domain').value=window.location.origin;
setEl('footer-time',new Date().toLocaleTimeString());
if(typeof window.ChatbotSDK!=='undefined'){const tag=getEl('tag-sdk');tag.className='tag tag--pass';tag.textContent='✅ SDK 已加载';}
fetch(window.location.origin+'/ai/assistant_app/chat/sync?message=test& chatId=__probe__& roleId=1',{signal:AbortSignal.timeout(5000)}).then(r=>{const tag=getEl('tag-api');if(r.ok||r.status< 500 ) { tag . className = 'tag tag--pass' ; tag . textContent = '✅ 后端连通' ; setEl ( ' footer-info ' , ' ChatbotSDK v1 . 2 . 0 | 后端 : 在线 ✅ ' ) ; } } ) . catch ( ( ) = > {const tag=getEl('tag-api');tag.className='tag tag--fail';tag.textContent='⚠ 后端离线';});
})();
})();
< / script >