Browse Source
feat(sdk-test): 测试面板重构为 Vue 3 + TDesign Chat 应用
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.htmlmaster
6 changed files with 1539 additions and 708 deletions
-
12frontend/cp-to-static.mjs
-
12frontend/sdk-test.html
-
1478frontend/src/sdk-test/SdkTestPanel.vue
-
14frontend/src/sdk-test/main.ts
-
6frontend/vite.config.ts
-
725src/main/resources/static/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
File diff suppressed because it is too large
View File
@ -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') |
||||
@ -1,710 +1,21 @@ |
|||||
<!DOCTYPE html> |
<!DOCTYPE html> |
||||
<html lang="zh-CN"> |
<html lang="zh-CN"> |
||||
<head> |
|
||||
<meta charset="UTF-8"> |
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
||||
<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> |
|
||||
</head> |
|
||||
<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, '&').replace(/</g, '<').replace(/>/g, '>'); |
|
||||
const re = /(<!--[\s\S]*?-->)|('[^']*'|"[^"]*")|(\/\/[^\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> |
|
||||
</body> |
|
||||
|
<head> |
||||
|
<meta charset="UTF-8" /> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
||||
|
<title>ChatbotSDK 测试面板</title> |
||||
|
<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> |
||||
|
<body> |
||||
|
<div id="sdk-test-app"></div> |
||||
|
</body> |
||||
</html> |
</html> |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue