Browse Source

feat(sdk-test): 测试面板重构为 Vue 3 + TDesign Chat 应用

- 新增 Vite 多页入口 sdk-test.html 与 SdkTestPanel.vue,用 TDesign Chat(ChatList/ChatSender/ChatActionbar)渲染对话演示
- 对话演示直调 GET /ai/chat/stream(OpenAI 格式),复用 readSSEStream 解析 delta.content,支持同步/流式、RAG 开关与来源展示
- SDK 集成验证区精简为 10 个核心用例,接口统一迁到新路径 /ai/chat*
- cp-to-static.mjs 构建后将 sdk-test.html 移动到 static/sdk/test.html
master
wanghanlin 6 days ago
parent
commit
8132f26891
  1. 12
      frontend/cp-to-static.mjs
  2. 12
      frontend/sdk-test.html
  3. 1478
      frontend/src/sdk-test/SdkTestPanel.vue
  4. 14
      frontend/src/sdk-test/main.ts
  5. 6
      frontend/vite.config.ts
  6. 715
      src/main/resources/static/sdk/test.html

12
frontend/cp-to-static.mjs

@ -2,7 +2,7 @@
* Vite build 后自动将产物复制到 Spring Boot 静态资源目录。 * Vite build 后自动将产物复制到 Spring Boot 静态资源目录。
* 由 npm postbuild 钩子触发。 * 由 npm postbuild 钩子触发。
*/ */
import { cpSync, existsSync, readdirSync, rmSync } from 'node:fs';
import { cpSync, existsSync, mkdirSync, readdirSync, rmSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
const DIST = 'dist'; const DIST = 'dist';
@ -25,3 +25,13 @@ if (existsSync(DIST)) {
console.error('✗ dist 目录不存在,请先执行 vite build'); console.error('✗ dist 目录不存在,请先执行 vite build');
process.exit(1); process.exit(1);
} }
// 3. 将 sdk-test 多页产物移动到 sdk/test.html(资源引用为绝对路径 /assets/...,移动后仍可解析)
const sdkTestHtml = join(TARGET, 'sdk-test.html');
if (existsSync(sdkTestHtml)) {
const sdkDir = join(TARGET, 'sdk');
if (!existsSync(sdkDir)) mkdirSync(sdkDir, { recursive: true });
cpSync(sdkTestHtml, join(sdkDir, 'test.html'));
rmSync(sdkTestHtml);
console.log('✓ 已移动 sdk-test.html → sdk/test.html');
}

12
frontend/sdk-test.html

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ChatbotSDK 测试面板</title>
</head>
<body>
<div id="sdk-test-app"></div>
<script type="module" src="/src/sdk-test/main.ts"></script>
</body>
</html>

1478
frontend/src/sdk-test/SdkTestPanel.vue
File diff suppressed because it is too large
View File

14
frontend/src/sdk-test/main.ts

@ -0,0 +1,14 @@
/**
* SDK
*
* main dist/sdk-test.html
* cp-to-static.mjs static/sdk/test.html
*/
import { createApp } from 'vue'
import TDesign from 'tdesign-vue-next'
import 'tdesign-vue-next/es/style/index.css'
import SdkTestPanel from './SdkTestPanel.vue'
const app = createApp(SdkTestPanel)
app.use(TDesign)
app.mount('#sdk-test-app')

6
frontend/vite.config.ts

@ -39,6 +39,12 @@ export default defineConfig({
minify: 'esbuild', // esbuild 比 terser 快 20-40 倍 minify: 'esbuild', // esbuild 比 terser 快 20-40 倍
reportCompressedSize: false, // 跳过压缩体积报告,省 1~3s reportCompressedSize: false, // 跳过压缩体积报告,省 1~3s
rollupOptions: { rollupOptions: {
input: {
// 管理后台主入口
main: resolve(__dirname, 'index.html'),
// SDK 测试面板(多页入口,产物由 cp-to-static 移动到 static/sdk/test.html)
'sdk-test': resolve(__dirname, 'sdk-test.html'),
},
output: { output: {
// 拆出独立 chunk,浏览器可长期缓存 // 拆出独立 chunk,浏览器可长期缓存
manualChunks(id) { manualChunks(id) {

715
src/main/resources/static/sdk/test.html

@ -1,710 +1,21 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ChatbotSDK 测试面板</title> <title>ChatbotSDK 测试面板</title>
<style>
*{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: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}
.btn-outline{background:#fff;border:1px solid #D1D5DB;color:#374151}.btn-outline:hover{background:#F9FAFB}
.btn-sm{padding:5px 10px;font-size:12px}
.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 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}
.code-section{margin-top:12px;border:1px solid #E5E7EB;border-radius:8px;overflow:hidden}
.code-section__header{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background:#F9FAFB;border-bottom:1px solid #E5E7EB}
.code-section__title{font-size:12px;font-weight:600;color:#374151;display:flex;align-items:center;gap:4px}
.code-section__copy{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;border:1px solid #D1D5DB;border-radius:4px;background:#fff;color:#374151;font-size:11px;cursor:pointer;transition:all .15s;font-family:inherit}
.code-section__copy:hover{background:#F3F4F6;border-color:#9CA3AF}
.code-section__copy--ok{background:#D1FAE5;border-color:#6EE7B7;color:#065F46}
.code-section__body{padding:0;max-height:260px;overflow-y:auto}
.code-section__body::-webkit-scrollbar{width:4px}
.code-section__body::-webkit-scrollbar-thumb{background:#D1D5DB;border-radius:2px}
.code-section pre{margin:0;padding:12px;font-size:11.5px;line-height:1.6;font-family:'SF Mono','Consolas','Menlo',monospace;white-space:pre;overflow-x:auto;color:#1F2937;tab-size:2}
.code-section .cm{color:#6B7280} /* 注释 */
.code-section .kw{color:#7C3AED} /* 关键字/key */
.code-section .st{color:#059669} /* 字符串 */
.code-section .nu{color:#2563EB} /* 数字 */
.code-section .bl{color:#DC2626} /* 必传高亮 */
.code-tabs{display:flex;gap:4px}
.code-tab{padding:3px 10px;border:1px solid #E5E7EB;border-radius:4px;background:#fff;color:#6B7280;font-size:11px;cursor:pointer;transition:all .15s;font-family:inherit}
.code-tab--active{background:#4F46E5;color:#fff;border-color:#4F46E5}
.code-tab:hover:not(.code-tab--active){background:#F3F4F6}
</style>
<script type="module" crossorigin src="/assets/sdk-test-DxurNOkQ.js"></script>
<link rel="modulepreload" crossorigin href="/assets/tdesign-rhtNERDv.js">
<link rel="modulepreload" crossorigin href="/assets/tdesign-web-components-DVS5zdio.js">
<link rel="modulepreload" crossorigin href="/assets/tdesign-chat-CeGDR2d-.js">
<link rel="modulepreload" crossorigin href="/assets/markdown-DYDVv77n.js">
<link rel="modulepreload" crossorigin href="/assets/chatAdapter-iiOpcNx_.js">
<link rel="stylesheet" crossorigin href="/assets/tdesign-CY0HVqZ3.css">
<link rel="stylesheet" crossorigin href="/assets/tdesign-chat-Dj1Q23QO.css">
<link rel="stylesheet" crossorigin href="/assets/tdesign-web-components-B-ycfzW_.css">
<link rel="stylesheet" crossorigin href="/assets/sdk-test-CG92De7i.css">
</head> </head>
<body> <body>
<div class="page">
<div class="panel">
<div class="panel-header">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">
<a href="/index.html" style="font-size:12px;color:#4F46E5;text-decoration:none">← 返回管理后台</a>
<a href="/doc.html" target="_blank" style="font-size:12px;color:#6B7280;text-decoration:none">API 文档 ↗</a>
</div>
<h1>🧪 ChatbotSDK 测试面板</h1>
<p>P0 核心 · P1 体验增强 · P2 运营完善</p>
</div>
<div class="panel-body">
<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>
<!-- SDK Token 鉴权 -->
<div style="background:#F0FDF4;border:1px solid #BBF7D0;border-radius:8px;padding:12px;margin-bottom:14px;">
<div style="font-size:12px;font-weight:600;color:#166534;margin-bottom:8px;">🔐 SDK Token 鉴权</div>
<div class="fg" style="margin-bottom:8px">
<label>API Key(用于换取 Token)</label>
<div style="display:flex;gap:6px">
<input type="text" id="cfg-apikey" placeholder="sk_xxxx..." style="flex:1">
<button class="btn btn-primary btn-sm" onclick="fetchToken()" id="btn-fetch-token">获取 Token</button>
</div>
<div class="hint">调用 POST /open-api/auth/token 换取 SDK Token</div>
</div>
<div class="fg" style="margin-bottom:0">
<label>SDK Token(自动填充或手动粘贴)</label>
<input type="text" id="cfg-token" placeholder="eyJhbGciOi...(获取后自动填充)">
<div class="hint">Token 有效期默认 2 小时,过期需重新获取</div>
</div>
</div>
<div class="fg">
<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 → 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">
<label>title</label>
<input type="text" id="cfg-title" value="AI 助手" placeholder="弹窗标题">
</div>
<div class="fg">
<label>primaryColor</label>
<input type="color" id="cfg-color" value="#4F46E5" style="height:36px;padding:2px">
</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>position</label>
<select id="cfg-pos"><option value="right-bottom" selected>右下角</option><option value="left-bottom">左下角</option></select>
</div>
</div>
<div class="fg-row">
<div class="fg">
<label>width (px)</label>
<input type="number" id="cfg-width" value="380" min="300" max="1200">
</div>
<div class="fg">
<label>height (px)</label>
<input type="number" id="cfg-height" value="520" min="300" max="1200">
</div>
</div>
<div class="fg-row">
<div class="fg">
<label>resizable 可缩放</label>
<select id="cfg-resizable"><option value="1" selected>开启</option><option value="0">关闭</option></select>
</div>
<div class="fg">
<label>theme 主题模式</label>
<select id="cfg-theme-mode"><option value="light" selected>白天 light</option><option value="dark">暗夜 dark</option></select>
</div>
</div>
<div class="fg-row">
<div class="fg">
<label>streaming</label>
<select id="cfg-stream"><option value="1" selected>开启流式</option><option value="0">关闭(同步)</option></select>
</div>
<div class="fg">
<label>enableRag 知识库检索</label>
<select id=cfg-rag><option value=1 selected>开启</option><option value=0>关闭</option></select>
</div>
</div>
<div class="fg-row">
<div class="fg">
<label>locale</label>
<select id="cfg-locale"><option value="zh-CN" selected>中文</option><option value="en">English</option></select>
</div>
<div class="fg">
<label>debug</label>
<select id="cfg-debug"><option value="1" selected>开启日志</option><option value="0">关闭日志</option></select>
</div>
</div>
<div class="fg-row">
<div class="fg">
<label>showClear 清空按钮</label>
<select id="cfg-clear"><option value="1" selected>显示</option><option value="0">隐藏</option></select>
</div>
<div class="fg">
<label>watermark 水印文字</label>
<input type="text" id="cfg-watermark" value="" placeholder="水印格式:日期 + 此值">
<div class="hint">留空则不显示水印</div>
</div>
</div>
<div class="actions">
<button class="btn btn-primary btn-full" id="btn-init" onclick="doInit()">🚀 初始化 SDK</button>
</div>
<div class="actions">
<button class="btn btn-outline btn-sm" onclick="domAction('open')">👁 打开窗口</button>
<button class="btn btn-outline btn-sm" onclick="domAction('close')">🙈 关闭窗口</button>
<button class="btn btn-outline btn-sm" onclick="domAction('toggle')">🔄 切换</button>
</div>
<div class="actions">
<button class="btn btn-outline btn-sm" onclick="ChatbotSDK.init({})">⚡ 测试空配置</button>
<button class="btn btn-outline btn-sm" onclick="doClearHistory()">🗑 清空对话</button>
<button class="btn btn-danger btn-sm" onclick="doDestroy()">💥 销毁 SDK</button>
</div>
<hr class="divider">
<div style="font-size:12px;color:#6B7280;line-height:1.6">
<b>💡 操作提示</b><br>
1. integrateId 填写客服角色 ID(数字,对应后端 roleId)<br>
2. userId 填写外部用户ID(对应后端 accountId)<br>
3. chatId 自动管理:从 /conversation/list 获取或自动生成<br>
4. 拖拽窗口右下角手柄可缩放窗口,尺寸自动保存<br>
5. 对话历史从后端加载(点击头部时钟图标查看)<br>
6. AI 回复支持 Markdown 渲染<br>
7. SDK 默认启用 RAG 检索;开启 showCategorySwitch 可选择知识库分类
</div>
<hr class="divider">
<!-- 生成接入代码 -->
<div class="code-section">
<div class="code-section__header">
<span class="code-section__title">📋 接入代码</span>
<div style="display:flex;gap:6px;align-items:center">
<div class="code-tabs">
<button class="code-tab code-tab--active" id="tab-snippet" onclick="switchCodeTab('snippet',this)">Script 片段</button>
<button class="code-tab" id="tab-fullpage" onclick="switchCodeTab('fullpage',this)">完整页面</button>
</div>
<button class="code-section__copy" id="btn-copy" onclick="copyCode()">📋 复制代码</button>
</div>
</div>
<div class="code-section__body">
<pre id="code-output"></pre>
</div>
</div>
</div>
</div>
<div class="main">
<div class="toolbar">
<h2>📋 测试用例</h2>
<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">22</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="footer">
<span id="footer-info">ChatbotSDK v1.2.0 | 后端:检测中...</span>
<span id="footer-time"></span>
</div>
</div>
</div>
<script src="/sdk/chatbot-sdk.min.js?v=2"></script>
<script>
(function() {
'use strict';
let passCount = 0, failCount = 0, skipCount = 0, apiCalls = 0, apiDurations = [], currentFilter = 'all';
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('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, phase) {
const container = getEl('cases-container');
const caseEl = document.createElement('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 = 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();
}
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() {
const token = getEl('cfg-token').value.trim() || undefined;
console.log('[getCfg] token input value:', getEl('cfg-token').value, 'trimmed:', token);
const cfg = {
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,
height: parseInt(getEl('cfg-height').value) || 520,
resizable: getEl('cfg-resizable').value === '1',
streaming: getEl('cfg-stream').value === '1',
locale: getEl('cfg-locale').value,
showCategorySwitch: getEl('cfg-cat').value === '1',
enableRag: getEl('cfg-rag').value === '1',
debug: getEl('cfg-debug').value === '1',
theme: getEl('cfg-theme-mode').value,
showClear: getEl('cfg-clear').value === '1',
watermark: getEl('cfg-watermark').value.trim() || undefined,
};
// 有 Token 时传入鉴权参数
if (token) {
cfg.token = token;
// roles 从 Token 换取接口获取(缓存在 window.__sdkRoles)
if (window.__sdkRoles) cfg.roles = window.__sdkRoles;
}
console.log('[getCfg] final cfg:', cfg);
return cfg;
}
/**
* 根据 Token 输入框内容更新初始化按钮状态
*/
function updateInitButton() {
const btn = getEl('btn-init');
if (!btn) return;
const hasToken = !!getEl('cfg-token').value.trim();
btn.disabled = !hasToken;
btn.style.opacity = hasToken ? '1' : '0.6';
btn.title = hasToken ? '' : '请先获取 SDK Token';
}
/**
* 用 API Key 换取 SDK Token
*/
window.fetchToken = async function() {
const apiKey = getEl('cfg-apikey').value.trim();
if (!apiKey) { alert('请先输入 API Key'); return; }
const domain = getEl('cfg-domain').value || window.location.origin;
const btn = getEl('btn-fetch-token');
btn.textContent = '获取中...';
btn.disabled = true;
try {
const res = await fetch(domain + '/open-api/auth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': apiKey
},
body: JSON.stringify({ ttl: 7200 })
});
const data = await res.json();
if (data.success && data.token) {
getEl('cfg-token').value = data.token;
// 缓存 roles 供 SDK 使用
window.__sdkRoles = data.roles;
// 自动填充第一个角色的 ID
if (data.roles && data.roles.length > 0) {
getEl('cfg-iid').value = data.roles[0].id;
}
// 刷新接入代码
generateCode();
// 如果 SDK 已加载,自动用 Token 重新初始化
if (document.getElementById('csk-launcher')) {
ChatbotSDK.destroy();
setTimeout(() => ChatbotSDK.init(getCfg()), 100);
}
btn.textContent = '✅ 已获取';
btn.style.background = '#059669';
setTimeout(() => { btn.textContent = '获取 Token'; btn.style.background = ''; btn.disabled = false; }, 2000);
updateInitButton();
} else {
alert('换取失败: ' + (data.message || '未知错误'));
btn.textContent = '获取 Token';
btn.disabled = false;
updateInitButton();
}
} catch (e) {
alert('请求失败: ' + e.message);
btn.textContent = '获取 Token';
btn.disabled = false;
updateInitButton();
}
};
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;}
/**
* 带 SDK Token 的 fetch 封装(/ai/ 路径自动注入 Authorization)
*/
function authFetch(url, options) {
const token = getEl('cfg-token').value.trim();
const opts = Object.assign({}, options);
if (token && url.includes('/ai/')) {
opts.headers = Object.assign({}, opts.headers || {}, { 'Authorization': 'Bearer ' + token });
}
return fetch(url, opts);
}
// ==================== 接入代码生成 ====================
// 当前代码展示 Tab:'snippet'(Script 片段)| 'fullpage'(完整 HTML 页面)
let codeTab = 'snippet';
// SDK 可选参数的默认值(与 config.ts parseConfig 严格对齐),用于判断是否需要在生成代码中输出
const SDK_DEFAULTS = {
title: 'AI 智能助手',
width: 500,
height: 520,
position: 'right-bottom',
primaryColor: '#4F46E5',
streaming: true,
locale: 'zh-CN',
showCategorySwitch: false,
enableRag: true,
resizable: true,
theme: 'light',
showClear: true,
};
// 生成 Script 片段代码(纯文本)
function buildCodeSnippet() {
const cfg = getCfg();
const domain = cfg.requestDomain;
// integrateId:纯数字输出为数字字面量,否则输出字符串
const iid = String(cfg.integrateId);
const iidLit = /^\d+$/.test(iid) ? iid : JSON.stringify(iid);
const params = [];
// 必传参数
params.push(' integrateId: ' + iidLit + ', // 必传:客服角色 ID(对应后端 roleId)');
params.push(' requestDomain: ' + JSON.stringify(domain) + ', // 必传:后端 API 域名');
// Token 鉴权(推荐)
const token = getEl('cfg-token').value.trim();
if (token) {
params.push(' token: ' + JSON.stringify(token) + ', // SDK JWT Token(有效期约2小时,生产环境请从后端动态换取)');
if (window.__sdkRoles) {
params.push(' roles: ' + JSON.stringify(window.__sdkRoles) + ', // 可用客服角色列表');
}
} else {
params.push(' token: ' + "'<从后端获取的 SDK Token>', // 推荐:SDK JWT Token(POST /open-api/auth/token 换取)");
}
// 可选参数:仅输出与默认值不同的项
if (cfg.userId) params.push(' userId: ' + JSON.stringify(cfg.userId) + ', // 可选:外部用户ID(对应后端 accountId)');
if (cfg.title !== SDK_DEFAULTS.title) params.push(' title: ' + JSON.stringify(cfg.title) + ', // 弹窗标题');
if (cfg.primaryColor !== SDK_DEFAULTS.primaryColor) params.push(' primaryColor: ' + JSON.stringify(cfg.primaryColor) + ', // 主色调');
if (cfg.position !== SDK_DEFAULTS.position) params.push(' position: ' + JSON.stringify(cfg.position) + ', // 悬浮按钮位置');
if (cfg.width !== SDK_DEFAULTS.width) params.push(' width: ' + cfg.width + ', // 弹窗宽度(px)');
if (cfg.height !== SDK_DEFAULTS.height) params.push(' height: ' + cfg.height + ', // 弹窗高度(px),最小 300');
if (cfg.resizable !== SDK_DEFAULTS.resizable) params.push(' resizable: ' + cfg.resizable + ', // 允许拖拽缩放窗口');
if (cfg.theme !== SDK_DEFAULTS.theme) params.push(' theme: ' + JSON.stringify(cfg.theme) + ', // 主题模式');
if (cfg.streaming !== SDK_DEFAULTS.streaming) params.push(' streaming: ' + cfg.streaming + ', // 是否启用流式输出');
if (cfg.locale !== SDK_DEFAULTS.locale) params.push(' locale: ' + JSON.stringify(cfg.locale) + ', // 界面语言');
if (cfg.showCategorySwitch !== SDK_DEFAULTS.showCategorySwitch) params.push(' showCategorySwitch: ' + cfg.showCategorySwitch + ', // 是否显示知识库切换');
if (cfg.enableRag !== SDK_DEFAULTS.enableRag) params.push(' enableRag: ' + cfg.enableRag + ', // 启用 RAG 知识库检索');
if (cfg.showClear !== SDK_DEFAULTS.showClear) params.push(' showClear: ' + cfg.showClear + ', // 显示清空按钮');
const watermark = getEl('cfg-watermark').value.trim();
if (watermark) params.push(' watermark: ' + JSON.stringify(watermark) + ', // 聊天窗口水印文字');
// debug 强制 false(生产环境推荐),无论面板当前值
params.push(' debug: false // 调试日志,生产环境建议关闭');
const initBlock = 'ChatbotSDK.init({\n' + params.join('\n') + '\n});';
return '<!-- ChatbotSDK 智能客服 -->\n' +
'<script src="' + domain + '/sdk/chatbot-sdk.min.js"><\/script>\n' +
'<script>\n' + initBlock + '\n<\/script>';
}
// 生成完整 HTML 页面代码(纯文本)
function buildFullPage() {
const snippet = buildCodeSnippet();
const indented = snippet.split('\n').map(l => ' ' + l).join('\n');
return '<!DOCTYPE html>\n' +
'<html lang="zh-CN">\n' +
'<head>\n' +
' <meta charset="UTF-8">\n' +
' <meta name="viewport" content="width=device-width, initial-scale=1.0">\n' +
' <title>AI 智能助手</title>\n' +
'</head>\n' +
'<body>\n' + indented + '\n</body>\n' +
'</html>';
}
// 根据当前 Tab 返回纯文本代码
function getCurrentCode() {
return codeTab === 'fullpage' ? buildFullPage() : buildCodeSnippet();
}
// 语法高亮:先转义 HTML,再用单遍正则匹配 HTML 注释 / 字符串 / 行注释 / 数字 / 对象 key
function highlightCode(code) {
const esc = code.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
const re = /(&lt;!--[\s\S]*?--&gt;)|('[^']*'|"[^"]*")|(\/\/[^\n]*)|\b(\d+)\b|\b(integrateId|requestDomain|userId|categoryId|title|primaryColor|position|width|streaming|locale|showCategorySwitch|enableRag|rewriteStrategy|debug|showClear|watermark|showAdminPanel|launcherIcon|token|roles)(?=\s*:)/g;
return esc.replace(re, (m, htmlCmt, str, cmt, num, key) => {
if (htmlCmt) return '<span class="cm">' + htmlCmt + '</span>';
if (str) return '<span class="st">' + str + '</span>';
if (cmt) return '<span class="cm">' + cmt + '</span>';
if (num) return '<span class="nu">' + num + '</span>';
if (key) return '<span class="kw">' + key + '</span>';
return m;
});
}
// 刷新代码预览区
function generateCode() {
const out = getEl('code-output');
if (out) out.innerHTML = highlightCode(getCurrentCode());
}
// 复制代码到剪贴板(带降级方案)
function fallbackCopy(text, cb) {
const ta = document.createElement('textarea');
ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0';
document.body.appendChild(ta); ta.select();
try { document.execCommand('copy'); cb(); } catch (e) { /* 忽略 */ }
document.body.removeChild(ta);
}
window.copyCode = function() {
const code = getCurrentCode();
const btn = getEl('btn-copy');
const done = function() {
if (btn) {
btn.textContent = '✅ 已复制';
btn.classList.add('code-section__copy--ok');
setTimeout(function() {
btn.textContent = '📋 复制代码';
btn.classList.remove('code-section__copy--ok');
}, 1500);
}
};
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(code).then(done).catch(function() { fallbackCopy(code, done); });
} else {
fallbackCopy(code, done);
}
};
// 切换代码 Tab
window.switchCodeTab = function(tab, btn) {
codeTab = tab;
document.querySelectorAll('.code-tab').forEach(function(t) { t.classList.remove('code-tab--active'); });
if (btn) btn.classList.add('code-tab--active');
generateCode();
};
window.doInit=function(){
const token = getEl('cfg-token').value.trim();
if (!token) {
alert('请先点击「获取 Token」按钮,用 API Key 换取 SDK Token 后再初始化。');
return;
}
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';});};
// ==================== 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+' 个方法)');}
async function test2(log,m){ChatbotSDK.init({});await sleep(200);assert(!document.getElementById('csk-launcher'),'缺失 integrateId 时不应创建 DOM');log('✅ 缺失 integrateId 正确拦截','log-line--pass');m('pass','通过');}
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','通过');}
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','通过');}
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','通过');}
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','通过');}
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','通过');}
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','通过');}
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','通过');}
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','通过');}
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 未回复');}}
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 authFetch(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)');}
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 authFetch(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)');}
// ==================== 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','通过');}
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','通过');}
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 authFetch(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','通过');}
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 authFetch(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','通过');}
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','通过');}
// ==================== 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','通过');}
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','通过');}
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','通过');}
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');}}
// ========== 参数映射验证 ==========
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 authFetch(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','通过');}
window.runAll = async function() {
passCount=0;failCount=0;skipCount=0;apiCalls=0;apiDurations=[];
updateStats();ChatbotSDK.destroy();getEl('cases-container').innerHTML='';await sleep(300);
const tests = [
[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'],
];
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(){
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 已加载';}
// 监听配置表单变化,实时刷新接入代码
['cfg-iid','cfg-domain','cfg-uid','cfg-title','cfg-color','cfg-pos','cfg-width','cfg-height','cfg-resizable','cfg-stream','cfg-locale','cfg-cat','cfg-rag','cfg-clear','cfg-debug','cfg-theme-mode','cfg-apikey','cfg-token','cfg-watermark'].forEach(function(id){
const el=getEl(id);
if(!el)return;
el.addEventListener('input', generateCode);
el.addEventListener('change', generateCode);
if(id==='cfg-token'){
el.addEventListener('input', updateInitButton);
el.addEventListener('change', updateInitButton);
}
});
generateCode();
updateInitButton();
// 后端连通性探测(使用 Token 或直接请求)
const probeToken = getEl('cfg-token').value.trim();
const probeHeaders = probeToken ? { 'Authorization': 'Bearer ' + probeToken } : {};
fetch(window.location.origin+'/ai/assistant_app/chat/sync?message=test&chatId=__probe__&roleId=1',{signal:AbortSignal.timeout(5000),headers:probeHeaders}).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>
<div id="sdk-test-app"></div>
</body> </body>
</html> </html>
Loading…
Cancel
Save