'use strict'; const assert = require('assert/strict'); const fs = require('fs'); const path = require('path'); const packageRoot = path.resolve(__dirname, '..'); const dashboardRoot = path.join(packageRoot, 'mcp', 'src', 'dashboard'); const html = fs.readFileSync(path.join(dashboardRoot, 'index.html'), 'utf8'); const app = fs.readFileSync(path.join(dashboardRoot, 'app.js'), 'utf8'); const css = fs.readFileSync(path.join(dashboardRoot, 'styles.css'), 'utf8'); const server = fs.readFileSync(path.join(dashboardRoot, 'server.js'), 'utf8'); const agentService = fs.readFileSync(path.join(dashboardRoot, 'agent-service.js'), 'utf8'); const checks = []; function check(name, fn) { fn(); checks.push(name); } check('工作台资源带缓存版本', () => { assert.match(html, /styles\.css\?v=/); assert.match(html, /app\.js\?v=/); }); check('客服工作台可直接进入,不先被登录页挡住', () => { assert.match(app, /if \(!acc && !\[\'overview\', \'agent\', \'skills\', \'knowledge\', \'customer-ops\'\]\.includes\(page\)\)/); }); check('监听按钮跟 listener.running,不再跟 AI 模式绑死', () => { assert.match(app, /const listenerRunning = Boolean\(listener\.running\);/); assert.match(app, /data-agent-action="\$\{allowlistReady \? \(listenerRunning \? 'stop-listener' : 'start-listener'\) : 'manage-allowlist'\}"/); assert.doesNotMatch(app, /aiListenerActive/); }); check('离线账号和监听失败会直接显示在工作台', () => { assert.match(app, /账号离线,请先扫码登录/); assert.match(app, /登录后可启动监听/); assert.match(app, /listener\.lastError && !listenerRunning/); assert.match(app, /监听未运行<\/strong>/); }); check('启动监听前在点击处理里检查白名单,避免 allowlistReady 作用域错误', () => { assert.match(app, /if \(action === 'start-listener'\) \{\s*const allowlistReady = Number\(state\.agent\.status\?\.config\?\.allowedSenderCount \|\| 0\) > 0;/); }); check('删除账号会请求服务端清理本地凭据', () => { assert.match(app, /async function removeAccount\(id\)/); assert.match(app, /\/api\/accounts\/remove/); assert.match(server, /pathname === '\/api\/accounts\/remove'/); assert.match(agentService, /function removeActiveAccount/); }); check('席位占满时提供强制切换登录', () => { assert.match(app, /async function forceSwitchLogin/); assert.match(app, /已有登录流程进行中\|设备登录数量已达上限\|停用旧设备/); assert.match(app, /startLoginFlow\(\{ forceSwitch: true \}\)/); assert.match(app, /\/api\/logout/); assert.match(server, /pathname === '\/api\/logout'/); }); check('免扫码失败不再自动拉起二维码,避免占死登录通道', () => { assert.match(app, /不再自动发起登录流程/); assert.match(app, /toast\('免扫码恢复失败,请在登录页手动开始登录'/); assert.doesNotMatch(app, /setTimeout\(\(\) => startLoginFlow\(\), 100\)/); }); check('媒体不可用有独立气泡和重试按钮', () => { assert.match(app, /function agentMediaUnavailableHtml/); assert.match(app, /agent-message-media unavailable agent-media-unavailable/); assert.match(css, /\.agent-message-media \{/); assert.match(css, /\.agent-media-retry/); assert.match(css, /\.agent-drawer-backdrop \{/); assert.match(css, /\.content > \.agent-page > \* \{ animation: none; \}/); }); check('全自动接管和工作台会话入口仍在', () => { assert.match(app, /全自动接管/); assert.match(app, /AGENT_SNAPSHOT_KEY/); assert.match(app, /\/api\/accounts\/switch/); assert.match(app, /voicePreview/); assert.match(app, /openSessionConversation/); }); check('全局、会话和群聊模式切换不再弹二次确认', () => { assert.match(app, /const modeButton = event\.target\.closest\('\[data-agent-mode\]'\)/); assert.match(app, /const conversationModeButton = event\.target\.closest\('\[data-agent-conversation-mode\]'\)/); assert.match(app, /const groupModeButton = event\.target\.closest\('\[data-agent-group-mode\]'\)/); const modeHandler = app.slice(app.indexOf("const modeButton = event.target.closest('[data-agent-mode]')"), app.indexOf("const actionButton = event.target.closest('[data-agent-action]')")); assert.doesNotMatch(modeHandler, /confirmModal\(/); assert.match(app, /if \(action === 'group-approve'\)[\s\S]*confirmModal\(/); assert.match(app, /if \(action === 'approve'\)[\s\S]*confirmModal\(/); }); check('工作台语音可合成试听并真实发送', () => { assert.match(app, /data-agent-action="voice-preview"/); assert.match(app, /data-agent-action="voice-send"/); assert.match(app, /\/api\/agent\/voice\/preview/); assert.match(app, /\/api\/agent\/conversations\/\$\{id\}\/voice-send/); assert.match(app, /\/api\/agent\/messages\/\$\{encodeURIComponent\(message\.id\)\}\/voice-audio/); assert.match(server, /pathname === '\/api\/agent\/voice\/preview'/); assert.match(server, /voice-send/); assert.match(server, /serveVoicePreviewAudio/); assert.match(server, /serveSentVoiceAudio/); assert.match(agentService, /async function previewVoice/); assert.match(agentService, /async function sendClonedVoice/); assert.match(agentService, /confirmed: input.confirmed/); assert.match(css, /\.agent-voice-audio/); }); check('通用仓保留 token 表单,不迁入房产业务字段', () => { assert.match(app, /function ensureAuthTokenReady/); assert.match(app, /function renderAuthTokenForm/); assert.match(app, /match\.community \|\| match\.title/); assert.doesNotMatch(app, /match\.estateName/); assert.doesNotMatch(app, /match\.layoutName/); assert.doesNotMatch(app, /match\.buildingArea/); assert.doesNotMatch(app, /match\.featureTags/); }); console.log(JSON.stringify({ status: 'ok', checks: checks.length, coverage: checks }, null, 2));