#!/usr/bin/env node 'use strict'; /** * 端到端验证 widget JWT: * 站点身份 → bridge /identity 签 LibreDesk JWT → auth exchange → 带身份开聊 * → AI 把该联系人视为已登录(可查订单/建商机) * * 用法:node scripts/e2e-widget-jwt.js "Can you check the status of my order SO-1?" */ const fs = require('fs'); const path = require('path'); const ROOT = path.resolve(__dirname, '..'); const message = process.argv.slice(2).join(' ') || 'Can you check the status of my order SO-1?'; function loadEnv(file) { const out = {}; for (const line of fs.readFileSync(file, 'utf8').split('\n')) { const t = line.trim(); if (!t || t.startsWith('#')) continue; const eq = t.indexOf('='); if (eq < 0) continue; let v = t.slice(eq + 1).trim(); if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1); out[t.slice(0, eq).trim()] = v; } return out; } const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); async function main() { const env = loadEnv(path.join(ROOT, '.env')); const SITE_ORIGIN = process.env.SITE_ORIGIN || env.IDENTITY_CORS_ORIGIN || 'http://localhost:9000'; const lr = (env.LIBREDESK_BASE_URL || 'http://localhost:9000').replace(/\/+$/, ''); const bridge = (env.BRIDGE_BASE_URL || 'http://localhost:4100').replace(/\/+$/, ''); const inboxUUID = env.LIBREDESK_INBOX_UUID; if (!inboxUUID) throw new Error('缺少 LIBREDESK_INBOX_UUID'); // 1) bridge /identity: 站点身份 -> widget JWT(dev payload;生产用 token) const idRes = await fetch(`${bridge}/identity`, { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: SITE_ORIGIN }, body: JSON.stringify({ email: process.env.IDENTITY_EMAIL || 'buyer@test-customer.nl', externalUserId: process.env.IDENTITY_EXTERNAL_ID || 'cust-1001', firstName: process.env.IDENTITY_FIRST || 'Jan', lastName: process.env.IDENTITY_LAST || 'Jansen', }), }); const idJson = await idRes.json(); if (!idRes.ok) throw new Error(`identity failed: ${JSON.stringify(idJson).slice(0, 200)}`); console.log(`1) userJWT 签发成功 (external_user_id=${idJson.user.external_user_id})`); // 2) widget auth exchange const exRes = await fetch(`${lr}/api/v1/widget/chat/auth/exchange`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Libredesk-Inbox-ID': inboxUUID }, body: JSON.stringify({ jwt: idJson.userJWT }), }); const exJson = await exRes.json(); if (!exRes.ok) throw new Error(`auth exchange failed: ${JSON.stringify(exJson).slice(0, 300)}`); const sessionToken = exJson.data.session_token; console.log(`2) auth exchange 成功 (contact user_id=${exJson.data.user.user_id})`); // 3) 带 session 开聊 const initRes = await fetch(`${lr}/api/v1/widget/chat/conversations/init`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${sessionToken}`, 'X-Libredesk-Inbox-ID': inboxUUID }, body: JSON.stringify({ message }), }); const initJson = await initRes.json(); if (!initRes.ok) throw new Error(`chat init failed: ${JSON.stringify(initJson).slice(0, 300)}`); const convUUID = initJson.data.conversation.uuid; console.log(`3) 会话创建: ${convUUID}`); console.log(` visitor message: ${message}`); // 4) admin 轮询回复 const loginRes = await fetch(`${lr}/api/v1/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: env.LIBREDESK_SYSTEM_EMAIL || 'System', password: env.LIBREDESK_SYSTEM_USER_PASSWORD }), }); const adminCookie = loginRes.headers.getSetCookie().map((c) => c.split(';')[0]).join('; '); const deadline = Date.now() + 120000; let reply = null; let last = -1; while (Date.now() < deadline) { await sleep(3000); const res = await fetch(`${lr}/api/v1/conversations/${convUUID}/messages`, { headers: { Cookie: adminCookie } }); if (!res.ok) continue; const body = await res.json(); const msgs = ((body.data && (body.data.results || body.data.messages)) || []).slice().sort((a, b) => Number(a.id) - Number(b.id)); if (msgs.length !== last) { last = msgs.length; const convRes = await fetch(`${lr}/api/v1/conversations/${convUUID}`, { headers: { Cookie: adminCookie } }); const conv = (await convRes.json()).data || {}; console.log(` messages=${msgs.length} assigned_user_id=${conv.assigned_user_id} status=${conv.status}`); } const ai = msgs.filter((m) => m.type === 'outgoing' && m.sender_type === 'agent' && !m.private).slice(-1)[0]; if (ai) { reply = ai; break; } } if (!reply) throw new Error('120 秒内没有 AI 回复'); console.log('\n===== AI 回复 #1 ====='); console.log(reply.content); const followups = String(process.env.FOLLOWUPS || '').split('||').map((s) => s.trim()).filter(Boolean); let round = 1; for (const text of followups) { round += 1; console.log(`\n>> 发送 #${round}: ${text}`); const sendRes = await fetch(`${lr}/api/v1/widget/chat/conversations/${convUUID}/message`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${sessionToken}`, 'X-Libredesk-Inbox-ID': inboxUUID }, body: JSON.stringify({ message: text }), }); if (!sendRes.ok) throw new Error(`send follow-up failed: HTTP ${sendRes.status} ${(await sendRes.text()).slice(0, 200)}`); const before = last; let next = null; const deadline2 = Date.now() + 120000; while (Date.now() < deadline2) { await sleep(3000); const res = await fetch(`${lr}/api/v1/conversations/${convUUID}/messages`, { headers: { Cookie: adminCookie } }); if (!res.ok) continue; const body = await res.json(); const msgs = ((body.data && (body.data.results || body.data.messages)) || []).slice().sort((a, b) => Number(a.id) - Number(b.id)); const fresh = msgs.filter((m) => m.type === 'outgoing' && m.sender_type === 'agent' && !m.private); const latest = fresh.slice(-1)[0]; if (msgs.length > before && latest && latest.id !== reply.id) { next = latest; break; } } if (!next) { console.log(' (本轮无新回复)'); continue; } reply = next; console.log(`===== AI 回复 #${round} =====`); console.log(next.content); } console.log('\n提示:bridge 日志应出现 search_orders / create_opportunity 且 verified=true'); } main().catch((err) => { console.error('[e2e-widget-jwt]', err.message); process.exit(1); });