chore: CRM-NEW baseline

This commit is contained in:
wangliang
2026-10-23 10:35:28 +08:00
commit aa499f6dbf
111 changed files with 15126 additions and 0 deletions
+147
View File
@@ -0,0 +1,147 @@
#!/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?';
const SITE_ORIGIN = 'http://192.168.31.61:9000';
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 lr = (env.LIBREDESK_BASE_URL || 'http://192.168.31.61:9000').replace(/\/+$/, '');
const bridge = (env.BRIDGE_BASE_URL || 'http://192.168.31.61: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);
});