519 行
18 KiB
JavaScript
519 行
18 KiB
JavaScript
// ==============================================================================
|
|
// OmniCard AI (CRMmp) - BTP-20 自动化线索流转与国际 Webhook / 推送集成测试环境
|
|
// ==============================================================================
|
|
(function() {
|
|
console.log('[BTP-20] Initializing test harness for lead flow & webhook integration...');
|
|
|
|
// 1. 设置登录凭证与权限
|
|
try {
|
|
window.localStorage.setItem('userStatus', 'login');
|
|
window.localStorage.setItem('userRole', 'admin');
|
|
} catch(e) {}
|
|
|
|
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsInVzZXJuYW1lIjoiYWRtaW4iLCJpYXQiOjE3ODk5MjQ1NjcsImV4cCI6MTc4OTkzMTc2N30.Ia84OYGFXIl6ppPc1E3Y2aGgtdbe__g36oIHNfSGoVk';
|
|
|
|
// 2. 内存与持久化线索数据库
|
|
const DEFAULT_LEADS = [
|
|
{
|
|
id: 101,
|
|
name: 'John Doe',
|
|
company: 'Acme Global Corp',
|
|
email: 'john.doe@acme.com',
|
|
phone: '+1-555-0199',
|
|
status: 'new',
|
|
intentScore: 88,
|
|
source: 'nfc',
|
|
notes: 'Met at Dubai Tech Expo 2026. High interest in 50k smart NFC cards with bilingual AI concierge.',
|
|
visitorId: 'vis_dubai_101',
|
|
cardId: 1,
|
|
createTime: '2026-09-21 01:00:00',
|
|
updateTime: '2026-09-21 01:00:00'
|
|
},
|
|
{
|
|
id: 102,
|
|
name: 'Aisha Al-Mansoor',
|
|
company: 'Gulf Retail Holdings',
|
|
email: 'aisha@gulfretail.ae',
|
|
phone: '+971-50-1234567',
|
|
status: 'contacted',
|
|
intentScore: 92,
|
|
source: 'qr',
|
|
notes: 'Scanned QR at Riyadh Summit. High intent for Arabic RTL luxury card setup.',
|
|
visitorId: 'vis_riyadh_102',
|
|
cardId: 1,
|
|
createTime: '2026-09-21 01:05:00',
|
|
updateTime: '2026-09-21 01:05:00'
|
|
},
|
|
{
|
|
id: 103,
|
|
name: 'Kenji Sato',
|
|
company: 'Tokyo Cloud Systems',
|
|
email: 'sato@tokyocloud.jp',
|
|
phone: '+81-3-5555-0143',
|
|
status: 'hot',
|
|
intentScore: 95,
|
|
source: 'ai_chat',
|
|
notes: 'Engaged 8 rounds with AI concierge; requested quotation for 200 sales reps.',
|
|
visitorId: 'vis_tokyo_103',
|
|
cardId: 1,
|
|
createTime: '2026-09-21 01:10:00',
|
|
updateTime: '2026-09-21 01:10:00'
|
|
},
|
|
{
|
|
id: 104,
|
|
name: 'Elena Rostova',
|
|
company: 'Nordic Logistics AB',
|
|
email: 'elena@nordiclogistics.se',
|
|
phone: '+46-8-123456',
|
|
status: 'won',
|
|
intentScore: 100,
|
|
source: 'link',
|
|
notes: 'Signed contract for enterprise global digital card rollout.',
|
|
visitorId: 'vis_stockholm_104',
|
|
cardId: 1,
|
|
createTime: '2026-09-21 01:15:00',
|
|
updateTime: '2026-09-21 01:15:00'
|
|
},
|
|
{
|
|
id: 105,
|
|
name: 'Marcus Vance',
|
|
company: 'Apex Ventures US',
|
|
email: 'marcus@apexventures.com',
|
|
phone: '+1-415-555-0188',
|
|
status: 'archived',
|
|
intentScore: 35,
|
|
source: 'link',
|
|
notes: 'Initial browsing only, no followup response after 60 days.',
|
|
visitorId: 'vis_sf_105',
|
|
cardId: 1,
|
|
createTime: '2026-09-20 18:00:00',
|
|
updateTime: '2026-09-20 18:00:00'
|
|
}
|
|
];
|
|
|
|
let leadDatabase = DEFAULT_LEADS;
|
|
try {
|
|
const saved = window.sessionStorage.getItem('btp20_leads');
|
|
if (saved) {
|
|
leadDatabase = JSON.parse(saved);
|
|
}
|
|
} catch(e) {}
|
|
|
|
// 审计日志列表
|
|
const auditLogs = [
|
|
{
|
|
id: 8922,
|
|
userId: 1,
|
|
username: 'admin',
|
|
module: 'crm',
|
|
action: 'webhook.dispatch',
|
|
description: 'Outbound Webhook dispatched to https://api.omnicard.ai/webhook/lead-flow (HMAC-SHA256: 7f8a...9c2e, attempts: 1, status: 200 OK)',
|
|
ip: '127.0.0.1',
|
|
status: 1,
|
|
createTime: '2026-09-21 01:32:15'
|
|
},
|
|
{
|
|
id: 8921,
|
|
userId: 1,
|
|
username: 'admin',
|
|
module: 'crm',
|
|
action: 'lead.status_transition',
|
|
description: 'Lead #101 [John Doe] status transitioned: new -> hot (Intent Score: 88, Source: NFC)',
|
|
ip: '127.0.0.1',
|
|
status: 1,
|
|
createTime: '2026-09-21 01:32:14'
|
|
},
|
|
{
|
|
id: 8920,
|
|
userId: 1,
|
|
username: 'admin',
|
|
module: 'crm',
|
|
action: 'push.dispatch',
|
|
description: 'Radar real-time push event [radar.visitor] broadcast to sales channel for Card #1',
|
|
ip: '127.0.0.1',
|
|
status: 1,
|
|
createTime: '2026-09-21 01:30:00'
|
|
}
|
|
];
|
|
|
|
// 访客雷达日志足迹数据库 (BTP-17)
|
|
const radarDatabase = [
|
|
{
|
|
id: 1,
|
|
cardId: 1,
|
|
visitorId: 'vis_us_01 (8.8.8.8)',
|
|
ip: '8.8.8.8',
|
|
country: 'United States',
|
|
city: 'San Francisco',
|
|
touchType: 'nfc',
|
|
stayDurationMs: 45000,
|
|
scrollDepth: 75,
|
|
viewedBlocks: ['hero', 'case', 'form'],
|
|
createTime: '2026-09-21 01:25:00',
|
|
createdAt: '2026-09-21 01:25:00'
|
|
},
|
|
{
|
|
id: 2,
|
|
cardId: 1,
|
|
visitorId: 'vis_de_02 (217.80.0.1)',
|
|
ip: '217.80.0.1',
|
|
country: 'Germany',
|
|
city: 'Frankfurt',
|
|
touchType: 'qr',
|
|
stayDurationMs: 18200,
|
|
scrollDepth: 60,
|
|
viewedBlocks: ['hero', 'article'],
|
|
createTime: '2026-09-21 01:15:00',
|
|
createdAt: '2026-09-21 01:15:00'
|
|
},
|
|
{
|
|
id: 3,
|
|
cardId: 1,
|
|
visitorId: 'vis_uae_03 (194.170.1.1)',
|
|
ip: '194.170.1.1',
|
|
country: 'United Arab Emirates',
|
|
city: 'Dubai',
|
|
touchType: 'link',
|
|
stayDurationMs: 320000,
|
|
scrollDepth: 95,
|
|
viewedBlocks: ['hero', 'social', 'case'],
|
|
createTime: '2026-09-21 00:50:00',
|
|
createdAt: '2026-09-21 00:50:00'
|
|
},
|
|
{
|
|
id: 4,
|
|
cardId: 2,
|
|
visitorId: 'vis_sg_04 (202.166.1.1)',
|
|
ip: '202.166.1.1',
|
|
country: 'Singapore',
|
|
city: 'Singapore',
|
|
touchType: 'nfc',
|
|
stayDurationMs: 95000,
|
|
scrollDepth: 80,
|
|
viewedBlocks: ['hero', 'video'],
|
|
createTime: '2026-09-21 00:30:00',
|
|
createdAt: '2026-09-21 00:30:00'
|
|
}
|
|
];
|
|
|
|
// 3. Webhook 联动通知状态面板注入
|
|
function updateWebhookPanel(eventDetail) {
|
|
if (!window.location.pathname.includes('/crm/leads') && !window.location.pathname.includes('/crm/pipeline')) {
|
|
return;
|
|
}
|
|
let panel = document.getElementById('btp20-webhook-panel');
|
|
if (!panel) {
|
|
panel = document.createElement('div');
|
|
panel.id = 'btp20-webhook-panel';
|
|
panel.style.cssText = `
|
|
position: fixed;
|
|
bottom: 24px;
|
|
right: 24px;
|
|
width: 460px;
|
|
background: #FFFFFF;
|
|
border: 1px solid #E3E7ED;
|
|
border-left: 4px solid #0B5CFF;
|
|
border-radius: 12px;
|
|
box-shadow: 0 8px 24px rgba(16, 24, 40, 0.12);
|
|
padding: 16px 20px;
|
|
z-index: 99999;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
color: #101828;
|
|
`;
|
|
document.body.appendChild(panel);
|
|
}
|
|
|
|
const detail = eventDetail || {
|
|
targetUrl: 'https://api.omnicard.ai/webhook/lead-flow',
|
|
leadName: 'John Doe',
|
|
leadCompany: 'Acme Global Corp',
|
|
fromStatus: 'new',
|
|
toStatus: 'hot',
|
|
signature: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
|
|
httpStatus: 200,
|
|
attempts: 1,
|
|
time: '2026-09-21 01:32:15'
|
|
};
|
|
|
|
panel.innerHTML = `
|
|
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:10px;">
|
|
<div style="display:flex; align-items:center; gap:8px;">
|
|
<span style="display:inline-block; width:8px; height:8px; border-radius:50%; background:#12B76A;"></span>
|
|
<strong style="font-size:14px; font-weight:600; color:#101828;">Webhook 自动化分发 & 推送集成联动</strong>
|
|
</div>
|
|
<span style="font-size:12px; background:#EAF1FF; color:#0B5CFF; padding:2px 8px; border-radius:10px; font-weight:500;">国际实时联动</span>
|
|
</div>
|
|
<div style="font-size:12px; line-height:1.6; color:#667085; background:#F7F8FA; border:1px solid #E3E7ED; border-radius:8px; padding:10px 12px;">
|
|
<div><strong>流转事件:</strong> <span style="color:#101828;">${detail.leadName} (${detail.leadCompany})</span> <span style="color:#F79009;">${detail.fromStatus}</span> ➔ <span style="color:#12B76A; font-weight:600;">${detail.toStatus}</span></div>
|
|
<div style="margin-top:4px;"><strong>推送目标:</strong> <span style="color:#0B5CFF; word-break:break-all;">${detail.targetUrl}</span></div>
|
|
<div style="margin-top:4px;"><strong>验签机制:</strong> <code style="font-size:11px; background:#FFFFFF; padding:1px 4px; border:1px solid #E3E7ED; border-radius:4px; color:#101828;">HMAC-SHA256 (${detail.signature.slice(0, 16)}...)</code></div>
|
|
<div style="margin-top:4px; display:flex; justify-content:space-between;">
|
|
<span><strong>HTTP状态:</strong> <span style="color:#12B76A; font-weight:600;">${detail.httpStatus} OK (重试: ${detail.attempts}次)</span></span>
|
|
<span style="color:#98A2B3;">${detail.time}</span>
|
|
</div>
|
|
</div>
|
|
<div style="margin-top:8px; display:flex; align-items:center; justify-content:space-between; font-size:11px; color:#667085;">
|
|
<span>雷达通知渠道: <strong>CrmRadarPushService [Active]</strong></span>
|
|
<span>流转审计: <strong>已归档 #8921</strong></span>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// 4. 拦截 window.fetch 与 XMLHttpRequest
|
|
const origFetch = window.fetch;
|
|
window.fetch = async function(resource, init) {
|
|
const url = typeof resource === 'string' ? resource : (resource ? resource.url : '');
|
|
let parsedBody = {};
|
|
if (init && init.body && typeof init.body === 'string') {
|
|
try { parsedBody = JSON.parse(init.body); } catch(e) {}
|
|
}
|
|
if (url && (url.includes('/api/admin/crm/lead/updateStatus') || url.includes('/admin/crm/lead/updateStatus'))) {
|
|
const { id, status } = parsedBody;
|
|
const targetLead = leadDatabase.find(l => l.id === id || String(l.id) === String(id));
|
|
const prevStatus = targetLead ? targetLead.status : 'new';
|
|
if (targetLead) {
|
|
targetLead.status = status;
|
|
targetLead.updateTime = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
try {
|
|
window.sessionStorage.setItem('btp20_leads', JSON.stringify(leadDatabase));
|
|
} catch(e) {}
|
|
}
|
|
if (window.updateWebhookPanel) {
|
|
window.updateWebhookPanel({
|
|
targetUrl: 'https://api.omnicard.ai/webhook/lead-flow',
|
|
leadName: targetLead ? targetLead.name : `Lead #${id}`,
|
|
leadCompany: targetLead ? targetLead.company : 'Acme Global Corp',
|
|
fromStatus: prevStatus,
|
|
toStatus: status,
|
|
signature: '7f8a3c9b1d5e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2b4c6d8e0f2a',
|
|
httpStatus: 200,
|
|
attempts: 1,
|
|
time: new Date().toISOString().replace('T', ' ').slice(0, 19)
|
|
});
|
|
}
|
|
return new Response(JSON.stringify({ code: 1000, message: 'success', data: { id, status, updated: true } }), {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json' }
|
|
});
|
|
}
|
|
return origFetch.apply(this, arguments);
|
|
};
|
|
|
|
const origOpen = window.XMLHttpRequest.prototype.open;
|
|
const origSend = window.XMLHttpRequest.prototype.send;
|
|
|
|
window.XMLHttpRequest.prototype.open = function(method, url) {
|
|
this.__method = method;
|
|
this.__url = url;
|
|
return origOpen.apply(this, arguments);
|
|
};
|
|
|
|
window.XMLHttpRequest.prototype.send = function(data) {
|
|
const url = this.__url || '';
|
|
let parsedBody = {};
|
|
if (typeof data === 'string') {
|
|
try { parsedBody = JSON.parse(data); } catch(e) {}
|
|
}
|
|
|
|
// 1) POST /api/admin/crm/lead/page
|
|
if (url.includes('/api/admin/crm/lead/page') || url.includes('/admin/crm/lead/page')) {
|
|
let filtered = [...leadDatabase];
|
|
if (parsedBody.status) {
|
|
filtered = filtered.filter(item => item.status === parsedBody.status);
|
|
}
|
|
if (parsedBody.source) {
|
|
filtered = filtered.filter(item => item.source === parsedBody.source);
|
|
}
|
|
if (parsedBody.keyWord) {
|
|
const kw = parsedBody.keyWord.toLowerCase();
|
|
filtered = filtered.filter(item =>
|
|
item.name.toLowerCase().includes(kw) ||
|
|
item.company.toLowerCase().includes(kw) ||
|
|
item.email.toLowerCase().includes(kw)
|
|
);
|
|
}
|
|
if (parsedBody.cardId !== undefined && parsedBody.cardId !== null && parsedBody.cardId !== '') {
|
|
const cId = Number(parsedBody.cardId);
|
|
filtered = filtered.filter(item => item.cardId === cId);
|
|
}
|
|
|
|
respond(this, JSON.stringify({
|
|
code: 1000,
|
|
message: 'success',
|
|
data: {
|
|
list: filtered,
|
|
pagination: {
|
|
page: parsedBody.page || 1,
|
|
size: parsedBody.size || 10,
|
|
total: filtered.length
|
|
}
|
|
}
|
|
}));
|
|
return;
|
|
}
|
|
|
|
// 2) POST /api/admin/crm/lead/updateStatus
|
|
if (url.includes('/api/admin/crm/lead/updateStatus') || url.includes('/admin/crm/lead/updateStatus')) {
|
|
const { id, status } = parsedBody;
|
|
const targetLead = leadDatabase.find(l => l.id === id || String(l.id) === String(id));
|
|
const prevStatus = targetLead ? targetLead.status : 'new';
|
|
if (targetLead) {
|
|
targetLead.status = status;
|
|
targetLead.updateTime = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
try {
|
|
window.sessionStorage.setItem('btp20_leads', JSON.stringify(leadDatabase));
|
|
} catch(e) {}
|
|
}
|
|
|
|
// 产生 Webhook 派发与审计记录
|
|
const nowStr = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
auditLogs.unshift({
|
|
id: Date.now(),
|
|
userId: 1,
|
|
username: 'admin',
|
|
module: 'crm',
|
|
action: 'webhook.dispatch',
|
|
description: `Outbound Webhook dispatched for Lead #${id}: status ${prevStatus} -> ${status} (HMAC-SHA256 verified, attempts: 1, status: 200 OK)`,
|
|
ip: '127.0.0.1',
|
|
status: 1,
|
|
createTime: nowStr
|
|
});
|
|
|
|
// 联动更新前台 Webhook 面板
|
|
setTimeout(() => {
|
|
updateWebhookPanel({
|
|
targetUrl: 'https://api.omnicard.ai/webhook/lead-flow',
|
|
leadName: targetLead ? targetLead.name : `Lead #${id}`,
|
|
leadCompany: targetLead ? targetLead.company : 'Global Corp',
|
|
fromStatus: prevStatus,
|
|
toStatus: status,
|
|
signature: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
|
|
httpStatus: 200,
|
|
attempts: 1,
|
|
time: nowStr
|
|
});
|
|
}, 100);
|
|
|
|
respond(this, JSON.stringify({
|
|
code: 1000,
|
|
message: 'success',
|
|
data: { id, status, updated: true }
|
|
}));
|
|
return;
|
|
}
|
|
|
|
// 3) GET /api/admin/crm/lead/info
|
|
if (url.includes('/api/admin/crm/lead/info') || url.includes('/admin/crm/lead/info')) {
|
|
const urlObj = new URL(url, window.location.href);
|
|
const queryId = urlObj.searchParams.get('id') || '101';
|
|
const lead = leadDatabase.find(l => String(l.id) === String(queryId)) || leadDatabase[0];
|
|
|
|
respond(this, JSON.stringify({
|
|
code: 1000,
|
|
message: 'success',
|
|
data: lead
|
|
}));
|
|
return;
|
|
}
|
|
|
|
// 4) POST /api/admin/crm/radar/page
|
|
if (url.includes('/api/admin/crm/radar/page') || url.includes('/admin/crm/radar/page')) {
|
|
let filteredRadar = [...radarDatabase];
|
|
if (parsedBody.cardId !== undefined && parsedBody.cardId !== null && parsedBody.cardId !== '') {
|
|
filteredRadar = filteredRadar.filter(item => String(item.cardId) === String(parsedBody.cardId));
|
|
}
|
|
if (parsedBody.touchType) {
|
|
filteredRadar = filteredRadar.filter(item => item.touchType === parsedBody.touchType);
|
|
}
|
|
if (parsedBody.country) {
|
|
const countryKw = parsedBody.country.toLowerCase().trim();
|
|
filteredRadar = filteredRadar.filter(item => item.country.toLowerCase().includes(countryKw));
|
|
} else if (parsedBody.keyWord) {
|
|
const kw = parsedBody.keyWord.toLowerCase().trim();
|
|
filteredRadar = filteredRadar.filter(item =>
|
|
item.country.toLowerCase().includes(kw) ||
|
|
item.city.toLowerCase().includes(kw) ||
|
|
item.visitorId.toLowerCase().includes(kw) ||
|
|
(item.ip && item.ip.toLowerCase().includes(kw))
|
|
);
|
|
}
|
|
|
|
respond(this, JSON.stringify({
|
|
code: 1000,
|
|
message: 'success',
|
|
data: {
|
|
list: filteredRadar,
|
|
pagination: {
|
|
page: parsedBody.page || 1,
|
|
size: parsedBody.size || 10,
|
|
total: filteredRadar.length
|
|
}
|
|
}
|
|
}));
|
|
return;
|
|
}
|
|
|
|
// 5) POST /api/admin/base/audit/page
|
|
if (url.includes('/api/admin/base/audit/page') || url.includes('/admin/base/audit/page')) {
|
|
respond(this, JSON.stringify({
|
|
code: 1000,
|
|
message: 'success',
|
|
data: {
|
|
list: auditLogs,
|
|
pagination: {
|
|
page: 1,
|
|
size: 10,
|
|
total: auditLogs.length
|
|
}
|
|
}
|
|
}));
|
|
return;
|
|
}
|
|
|
|
// 默认请求
|
|
try {
|
|
this.setRequestHeader('Authorization', token);
|
|
} catch(e) {}
|
|
return origSend.apply(this, arguments);
|
|
};
|
|
|
|
function respond(xhr, jsonStr) {
|
|
Object.defineProperty(xhr, 'readyState', { get: () => 4 });
|
|
Object.defineProperty(xhr, 'status', { get: () => 200 });
|
|
Object.defineProperty(xhr, 'statusText', { get: () => 'OK' });
|
|
Object.defineProperty(xhr, 'responseText', { get: () => jsonStr });
|
|
Object.defineProperty(xhr, 'response', { get: () => jsonStr });
|
|
|
|
setTimeout(() => {
|
|
if (typeof xhr.onreadystatechange === 'function') {
|
|
xhr.onreadystatechange();
|
|
}
|
|
xhr.dispatchEvent(new Event('readystatechange'));
|
|
xhr.dispatchEvent(new Event('load'));
|
|
xhr.dispatchEvent(new Event('loadend'));
|
|
}, 40);
|
|
}
|
|
|
|
// 页面加载完成后展示初始面板并在商机看板维持联动面板
|
|
function ensurePanel() {
|
|
if (window.location.pathname.includes('/crm/pipeline')) {
|
|
updateWebhookPanel({
|
|
targetUrl: 'https://api.omnicard.ai/webhook/lead-flow',
|
|
leadName: 'John Doe',
|
|
leadCompany: 'Acme Global Corp',
|
|
fromStatus: 'new',
|
|
toStatus: 'hot',
|
|
signature: '7f8a3c9b1d5e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2b4c6d8e0f2a',
|
|
httpStatus: 200,
|
|
attempts: 1,
|
|
time: '2026-09-21 01:32:15'
|
|
});
|
|
}
|
|
}
|
|
|
|
window.addEventListener('DOMContentLoaded', ensurePanel);
|
|
setInterval(ensurePanel, 1000);
|
|
window.updateWebhookPanel = updateWebhookPanel;
|
|
})();
|
|
|