148 lines
6.4 KiB
JavaScript
148 lines
6.4 KiB
JavaScript
#!/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);
|
||
});
|