Files
crm-ai/scripts/e2e-widget-jwt.js
T

148 lines
6.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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);
});