feat: brand 白名单下发(站点覆盖/租户头像/CORS) + widget/chat 客户端 key 租户化 + 会话站点标
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
+11
-13
@@ -3,16 +3,16 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>HESUPIPE Support</title>
|
||||
<title>AI Support</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<script src="/static/js/theme.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="chat-container">
|
||||
<div class="chat-header">
|
||||
<div class="avatar"><span class="avatar-emoji">🤖</span><img class="avatar-img" alt="" src="/static/kefu-small.png"></div>
|
||||
<div class="avatar"><span class="avatar-emoji">🤖</span><img class="avatar-img" alt="" src="/api/public/avatar"></div>
|
||||
<div class="info">
|
||||
<h1>HESUPIPE Support</h1>
|
||||
<h1>AI Support</h1>
|
||||
<div class="availability">Online now - Usually replies in minutes</div>
|
||||
</div>
|
||||
<!-- <div class="actions">
|
||||
@@ -33,11 +33,13 @@
|
||||
<div class="help-description">Ask about pipe specifications, quotations, MOQ, shipping and delivery.</div>
|
||||
</div>
|
||||
<div class="help-actions">
|
||||
<a href="https://wa.me/+8615937108317" target="_blank" class="call-btn whatsapp-btn">
|
||||
<!-- Task 10: 联系按钮/标题/欢迎语/话题/隐私链接由 chat.js 从
|
||||
/api/public/brand 填充(值缺位隐藏),出厂不再内嵌租户品牌 -->
|
||||
<a target="_blank" rel="noopener" class="call-btn whatsapp-btn">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg>
|
||||
<span>WhatsApp</span>
|
||||
</a>
|
||||
<a href="mailto:hesu@hesuu.com" class="call-btn email-btn">
|
||||
<a class="call-btn email-btn">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>
|
||||
<span>Email</span>
|
||||
</a>
|
||||
@@ -48,7 +50,7 @@
|
||||
<div class="chat-separator"></div>
|
||||
<div class="chat-messages" id="messages">
|
||||
<div class="message assistant">
|
||||
<div class="bubble">What kind of piping project are you working on?</div>
|
||||
<div class="bubble"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -56,14 +58,10 @@
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
|
||||
<!-- POPULAR TOPICS 区块 -->
|
||||
<!-- POPULAR TOPICS 区块(按钮由 chat.js 按 brand.topics 生成) -->
|
||||
<div class="popular-topics-fixed" id="popular-topics">
|
||||
<div class="popular-topics-label">POPULAR TOPICS</div>
|
||||
<div class="popular-topics-grid">
|
||||
<button class="popular-topic-btn" data-topic="Get Quotation">Get Quotation</button>
|
||||
<button class="popular-topic-btn" data-topic="Pipe Specifications">Pipe Specifications</button>
|
||||
<button class="popular-topic-btn" data-topic="Delivery & Shipping">Delivery & Shipping</button>
|
||||
</div>
|
||||
<div class="popular-topics-grid"></div>
|
||||
</div>
|
||||
<div class="chat-separator-topics"></div>
|
||||
|
||||
@@ -76,7 +74,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-footer"><span class="footer-left">By chatting, you agree to our <a href="https://hesupipe.com/privacy-policy/" target="_blank" rel="noopener">privacy policy</a>.</span></div>
|
||||
<div class="chat-footer"><span class="footer-left">By chatting, you agree to our <a target="_blank" rel="noopener">privacy policy</a>.</span></div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/chat.js"></script>
|
||||
|
||||
+103
-25
@@ -1,20 +1,25 @@
|
||||
// ---- 动态主题色 bootstrap(spec §3):?color= → 派生 → :root 变量 ----
|
||||
// ---- Task 10: brand 拉取(同源 /api/public/brand,下方两个 IIFE 共用) ----
|
||||
// 失败/超时(3s) → resolve(null) → 中性兜底,页面照常可用不白屏。
|
||||
var chatBrandReady = (function () {
|
||||
try {
|
||||
var p = fetch("/api/public/brand", { cache: "no-store" }).then(function (r) {
|
||||
return r.ok ? r.json() : null;
|
||||
}).catch(function () {
|
||||
return null;
|
||||
});
|
||||
return Promise.race([p, new Promise(function (res) { setTimeout(function () { res(null); }, 3000); })]);
|
||||
} catch (e) {
|
||||
return Promise.resolve(null); // 无 fetch 的远古环境:中性兜底
|
||||
}
|
||||
})();
|
||||
|
||||
// ---- 动态主题色 bootstrap(spec §3):派生 → :root 变量 ----
|
||||
// Task 10 颜色优先级: ?color=(URL 显式覆盖,语义同挂件 data-*) >
|
||||
// brand.color(租户/站点门面) > DEFAULT_COLOR。
|
||||
(function () {
|
||||
if (typeof ChatTheme === "undefined") return; // theme.js 加载失败→style.css :root 品牌蓝兜底
|
||||
var m = /(?:^|[?&])color=([^&]*)/.exec(location.search);
|
||||
function safeDecode(s) { try { return decodeURIComponent(s); } catch (e) { return ""; } }
|
||||
var color = m ? safeDecode(m[1]).trim() : "";
|
||||
var palette = ChatTheme.derivePalette(color || ChatTheme.DEFAULT_COLOR);
|
||||
var root = document.documentElement.style;
|
||||
root.setProperty("--w-primary", palette.primary);
|
||||
root.setProperty("--w-primary-rgb", palette.primaryRgb);
|
||||
root.setProperty("--w-gradient-end", palette.gradientEnd);
|
||||
root.setProperty("--w-deep-text", palette.deepText);
|
||||
root.setProperty("--w-caption-text", palette.captionText);
|
||||
root.setProperty("--w-soft-bg", palette.softBg);
|
||||
root.setProperty("--w-border-soft", palette.borderSoft);
|
||||
|
||||
// 头像兜底(与挂件同机制)
|
||||
// 头像兜底(与挂件同机制)——与 brand 无关,立即挂
|
||||
Array.prototype.forEach.call(document.querySelectorAll(".avatar-img"), function (img) {
|
||||
function hideEmoji() {
|
||||
var s = img.parentElement && img.parentElement.querySelector(".avatar-emoji");
|
||||
@@ -24,18 +29,31 @@
|
||||
img.addEventListener("load", hideEmoji);
|
||||
img.addEventListener("error", function () { img.remove(); });
|
||||
});
|
||||
|
||||
chatBrandReady.then(function (b) {
|
||||
var m = /(?:^|[?&])color=([^&]*)/.exec(location.search);
|
||||
function safeDecode(s) { try { return decodeURIComponent(s); } catch (e) { return ""; } }
|
||||
var color = m ? safeDecode(m[1]).trim() : (((b && b.color) || "").trim());
|
||||
var palette = ChatTheme.derivePalette(color || ChatTheme.DEFAULT_COLOR);
|
||||
var root = document.documentElement.style;
|
||||
root.setProperty("--w-primary", palette.primary);
|
||||
root.setProperty("--w-primary-rgb", palette.primaryRgb);
|
||||
root.setProperty("--w-gradient-end", palette.gradientEnd);
|
||||
root.setProperty("--w-deep-text", palette.deepText);
|
||||
root.setProperty("--w-caption-text", palette.captionText);
|
||||
root.setProperty("--w-soft-bg", palette.softBg);
|
||||
root.setProperty("--w-border-soft", palette.borderSoft);
|
||||
});
|
||||
})();
|
||||
|
||||
(function () {
|
||||
// 联系方式配置(与 widget.js 的 data-* 机制一致)
|
||||
// 联系方式配置(与 widget.js 的 data-* 机制一致:属性显式覆盖 > brand)
|
||||
var chatScript = document.currentScript || document.querySelector('script[src*="chat.js"]');
|
||||
var whatsapp = (chatScript && chatScript.getAttribute("data-whatsapp")) || "https://wa.me/+8615937108317";
|
||||
var email = (chatScript && chatScript.getAttribute("data-email")) || "mailto:hesu@hesuu.com";
|
||||
var whatsappAttr = chatScript && chatScript.getAttribute("data-whatsapp");
|
||||
var emailAttr = chatScript && chatScript.getAttribute("data-email");
|
||||
var waBtn = document.querySelector('.whatsapp-btn');
|
||||
var emBtn = document.querySelector('.email-btn');
|
||||
if (waBtn) waBtn.href = whatsapp;
|
||||
if (emBtn) emBtn.href = email;
|
||||
var sessionId = localStorage.getItem("cc_session_id");
|
||||
var sessionId = null;
|
||||
var messagesEl = document.getElementById("messages");
|
||||
var inputEl = document.getElementById("input");
|
||||
var sendBtn = document.getElementById("sendBtn");
|
||||
@@ -49,13 +67,63 @@
|
||||
var streamBubble = null;
|
||||
var statusTimer = null;
|
||||
|
||||
function contactHref(value, kind) {
|
||||
// brand 存裸值(电话/邮箱),data-* 历史上传完整 URL——两者都接受
|
||||
if (!value) return "";
|
||||
if (/^(https?:|mailto:)/i.test(value)) return value;
|
||||
return kind === "wa" ? "https://wa.me/" + value : "mailto:" + value;
|
||||
}
|
||||
|
||||
function applyBrand(b) {
|
||||
b = (b && typeof b === "object") ? b : {};
|
||||
if (b.title) {
|
||||
document.title = b.title;
|
||||
var h1 = document.querySelector(".chat-header .info h1");
|
||||
if (h1) h1.textContent = b.title;
|
||||
}
|
||||
var wa = contactHref(whatsappAttr || b.whatsapp || "", "wa");
|
||||
if (waBtn) { if (wa) waBtn.href = wa; else waBtn.style.display = "none"; }
|
||||
var em = contactHref(emailAttr || b.email || "", "mail");
|
||||
if (emBtn) { if (em) emBtn.href = em; else emBtn.style.display = "none"; }
|
||||
if (b.placeholder) inputEl.placeholder = b.placeholder;
|
||||
if (b.welcome) {
|
||||
var bubble = document.querySelector("#messages .message .bubble");
|
||||
if (bubble) bubble.textContent = b.welcome;
|
||||
}
|
||||
if (Array.isArray(b.topics) && b.topics.length) {
|
||||
var grid = document.querySelector(".popular-topics-grid");
|
||||
if (grid) {
|
||||
grid.innerHTML = "";
|
||||
b.topics.forEach(function (t) {
|
||||
if (typeof t !== "string" || !t) return;
|
||||
var btn = document.createElement("button");
|
||||
btn.className = "popular-topic-btn";
|
||||
btn.setAttribute("data-topic", t);
|
||||
btn.textContent = t;
|
||||
btn.addEventListener("click", function () { sendTopicMessage(t); });
|
||||
grid.appendChild(btn);
|
||||
});
|
||||
}
|
||||
} else if (popularTopicsEl) {
|
||||
popularTopicsEl.style.display = "none"; // 无话题配置:整区隐藏
|
||||
var sep = document.querySelector(".chat-separator-topics");
|
||||
if (sep) sep.style.display = "none"; // 分隔线随区隐藏,不留孤线
|
||||
}
|
||||
var footer = document.querySelector(".chat-footer");
|
||||
if (footer) {
|
||||
var link = footer.querySelector("a");
|
||||
if (b.privacy_url && link) link.href = b.privacy_url;
|
||||
else footer.style.display = "none"; // GDPR 行:为空即隐藏
|
||||
}
|
||||
}
|
||||
|
||||
function getWsUrl() {
|
||||
var loc = window.location;
|
||||
var proto = loc.protocol === "https:" ? "wss:" : "ws:";
|
||||
return proto + "//" + loc.host + "/ws/chat";
|
||||
}
|
||||
|
||||
function connect() {
|
||||
function connect(sessionKey) {
|
||||
ws = new WebSocket(getWsUrl());
|
||||
|
||||
ws.onopen = function () {
|
||||
@@ -71,7 +139,7 @@
|
||||
|
||||
if (data.type === "init" || data.type === "resumed") {
|
||||
sessionId = data.session_id;
|
||||
localStorage.setItem("cc_session_id", sessionId);
|
||||
localStorage.setItem(sessionKey, sessionId);
|
||||
if (data.type === "resumed") {
|
||||
loadHistory();
|
||||
}
|
||||
@@ -113,7 +181,7 @@
|
||||
|
||||
ws.onclose = function () {
|
||||
if (!ended) {
|
||||
setTimeout(connect, 3000);
|
||||
setTimeout(function () { connect(sessionKey); }, 3000);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -218,7 +286,8 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Popular topics buttons
|
||||
// Popular topics buttons(出厂模板的兜底话题;brand 下发后由 applyBrand
|
||||
// 重建,这里的绑定覆盖"brand 失败"路径)
|
||||
var topicBtns = document.querySelectorAll(".popular-topic-btn");
|
||||
topicBtns.forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
@@ -227,5 +296,14 @@
|
||||
});
|
||||
});
|
||||
|
||||
connect();
|
||||
// Task 10: 会话 key 租户化(cc_session_id_<slug>)——brand 落定(成功或
|
||||
// 失败,slug 回退 "default")后才读 key/连 WS,保证同浏览器多租户互不串
|
||||
chatBrandReady.then(function (b) {
|
||||
applyBrand(b);
|
||||
var slug = String((b && b.slug) || "default").replace(/[^a-zA-Z0-9_-]/g, "") || "default";
|
||||
var sessionKey = "cc_session_id_" + slug;
|
||||
sessionId = null;
|
||||
try { sessionId = localStorage.getItem(sessionKey); } catch (e) { /* storage 不可用 */ }
|
||||
connect(sessionKey);
|
||||
});
|
||||
})();
|
||||
|
||||
+34
-7
@@ -83,7 +83,7 @@
|
||||
|
||||
<div class="info">
|
||||
<p><strong>Current Page:</strong> <span id="pathname"></span></p>
|
||||
<p><strong>Storage Key:</strong> widget_invite_dismissed_<span id="storage-key"></span></p>
|
||||
<p><strong>Storage Key:</strong> widget_invite_dismissed_<slug>_<span id="storage-key"></span> (Task 10: key 按租户 slug 命名空间化)</p>
|
||||
<p><button class="btn" onclick="clearStorage()">Clear Storage & Refresh</button></p>
|
||||
</div>
|
||||
|
||||
@@ -92,7 +92,12 @@
|
||||
document.getElementById('storage-key').textContent = window.location.pathname;
|
||||
|
||||
function clearStorage() {
|
||||
sessionStorage.removeItem('widget_invite_dismissed_' + window.location.pathname);
|
||||
// Task 10: key 已租户化(widget_invite_dismissed_<slug>_<path>)——按前缀清扫全部
|
||||
try {
|
||||
Object.keys(sessionStorage).forEach(function (k) {
|
||||
if (k.indexOf('widget_invite_dismissed_') === 0) sessionStorage.removeItem(k);
|
||||
});
|
||||
} catch (e) {}
|
||||
location.reload();
|
||||
}
|
||||
|
||||
@@ -107,22 +112,36 @@
|
||||
};
|
||||
|
||||
var rebuildLoading = false, rebuildPending = null;
|
||||
function removeWidgetHosts() {
|
||||
// Task 10: host id 已租户化(cc-widget-host-<slug>),按前缀清扫
|
||||
var hosts = document.querySelectorAll('[id^="cc-widget-host-"]');
|
||||
for (var i = 0; i < hosts.length; i++) hosts[i].remove();
|
||||
}
|
||||
function rebuildWidget(extra) {
|
||||
if (rebuildLoading) { rebuildPending = extra || {}; return; } // 快速连点:只记最新意图,当前加载完接力重建
|
||||
rebuildLoading = true;
|
||||
document.getElementById("assertions").innerHTML = "<div>…rebuilding…</div>";
|
||||
document.querySelectorAll("#cc-widget-host").forEach(function (h) { h.remove(); });
|
||||
removeWidgetHosts();
|
||||
document.querySelectorAll("script[src*='widget.js']").forEach(function (s) { s.remove(); });
|
||||
try { sessionStorage.removeItem("widget_invite_dismissed_" + window.location.pathname); } catch (e) {}
|
||||
try {
|
||||
Object.keys(sessionStorage).forEach(function (k) {
|
||||
if (k.indexOf("widget_invite_dismissed_") === 0) sessionStorage.removeItem(k);
|
||||
});
|
||||
} catch (e) {}
|
||||
var s = document.createElement("script");
|
||||
s.src = "/static/widget.js?" + Date.now();
|
||||
var attrs = Object.assign({}, BASE_ATTRS, extra || {});
|
||||
Object.keys(attrs).forEach(function (k) { s.setAttribute(k, attrs[k]); });
|
||||
s.onload = function () {
|
||||
rebuildLoading = false;
|
||||
var hosts = document.querySelectorAll("#cc-widget-host");
|
||||
for (var i = 0; i < hosts.length - 1; i++) hosts[i].remove(); // 竞态兜底:只保留最后挂载
|
||||
runAssertions();
|
||||
// Task 10: 挂件改等 brand 端点落定后才挂载——脚本 onload 早于挂载,
|
||||
// 轮询 __chatWidgetDebug(boot 里同步设置,观察到场即 init 已完成)
|
||||
// 就绪再清竞态残留并跑断言
|
||||
waitForWidget(function () {
|
||||
var hosts = document.querySelectorAll('[id^="cc-widget-host-"]');
|
||||
for (var i = 0; i < hosts.length - 1; i++) hosts[i].remove(); // 竞态兜底:只保留最后挂载
|
||||
runAssertions(); // 挂件未就绪(超时)时其内部 !d 分支自行报红
|
||||
});
|
||||
var next = rebuildPending; rebuildPending = null;
|
||||
if (next) rebuildWidget(next);
|
||||
};
|
||||
@@ -134,6 +153,14 @@
|
||||
};
|
||||
document.body.appendChild(s);
|
||||
}
|
||||
function waitForWidget(cb) {
|
||||
var started = Date.now();
|
||||
(function poll() {
|
||||
if (window.__chatWidgetDebug) return cb(true);
|
||||
if (Date.now() - started > 5000) return cb(false); // brand 挂起/失败兜底
|
||||
setTimeout(poll, 100);
|
||||
})();
|
||||
}
|
||||
function applyColor(c) {
|
||||
rebuildWidget({ "data-color": c || document.getElementById("colorPicker").value });
|
||||
}
|
||||
|
||||
+133
-43
@@ -5,12 +5,18 @@
|
||||
})();
|
||||
|
||||
// Save config immediately (before DOM ready)
|
||||
// Task 10 值管道: data-* 属性(嵌入方显式覆盖,最高优先) > brand 端点
|
||||
// (租户/站点门面) > 中性兜底。此处只同步读原始属性;有效值在 brand
|
||||
// 落定后由 boot() 推导(见文件尾),品牌文案不再硬编码于本文件。
|
||||
var position = script.getAttribute("data-position") || "bottom-right";
|
||||
var theme = script.getAttribute("data-theme") || "light";
|
||||
var title = script.getAttribute("data-title") || "HESUPIPE Support";
|
||||
var titleAttr = script.getAttribute("data-title");
|
||||
var autoOpen = script.getAttribute("data-auto-open") === "true";
|
||||
var whatsapp = script.getAttribute("data-whatsapp") || "https://wa.me/+8615937108317";
|
||||
var email = script.getAttribute("data-email") || "mailto:hesu@hesuu.com";
|
||||
var whatsappAttr = script.getAttribute("data-whatsapp");
|
||||
var emailAttr = script.getAttribute("data-email");
|
||||
var colorAttr = (script.getAttribute("data-color") || "").trim();
|
||||
var avatarAttr = (script.getAttribute("data-avatar") || "").trim();
|
||||
var siteAttr = (script.getAttribute("data-site") || "").trim();
|
||||
var autoInvite = script.getAttribute("data-auto-invite") !== "false";
|
||||
var isMobile = script.getAttribute("data-mobile") === "true";
|
||||
var wsBase = new URL(script.src).origin;
|
||||
@@ -109,14 +115,62 @@
|
||||
|
||||
/* ===== 主题派生单元结束 ===== */
|
||||
var DEFAULT_AVATAR_PATH = "/static/kefu-small.png";
|
||||
var avatarAttr = (script.getAttribute("data-avatar") || "").trim();
|
||||
var avatarUrl = avatarAttr
|
||||
? (/^https?:\/\//i.test(avatarAttr) ? avatarAttr : wsBase + "/" + avatarAttr.replace(/^\//, ""))
|
||||
: wsBase + DEFAULT_AVATAR_PATH;
|
||||
var colorAttr = (script.getAttribute("data-color") || "").trim();
|
||||
var palette = derivePalette(colorAttr || DEFAULT_COLOR);
|
||||
if (script.getAttribute("data-debug") === "true") {
|
||||
window.__chatWidgetDebug = { derivePalette: derivePalette, palette: palette };
|
||||
|
||||
// ---- Task 10: brand 端点取门面 + 有效值推导 ------------------------------
|
||||
// __brand = /api/public/brand 响应(null = 拉取失败/超时 → 中性兜底,不白屏)
|
||||
var __brand = null;
|
||||
var title = "", whatsapp = "", email = "", avatarUrl = "", privacyUrl = "";
|
||||
var welcome = "", placeholder = "Type a message...", topics = [];
|
||||
var palette = null;
|
||||
var brandSlug = "default";
|
||||
|
||||
function fetchBrand() {
|
||||
var url = wsBase + "/api/public/brand" + (siteAttr ? "?site=" + encodeURIComponent(siteAttr) : "");
|
||||
var p = fetch(url, { cache: "no-store" }).then(function (r) {
|
||||
return r.ok ? r.json() : null;
|
||||
}).catch(function () {
|
||||
return null; // fetch 失败 → data-*/中性兜底,挂件照常渲染
|
||||
});
|
||||
// 挂起保护: brand 端点 3s 无响应按失败处理,网络问题不挡挂件挂载
|
||||
return Promise.race([p, new Promise(function (res) { setTimeout(function () { res(null); }, 3000); })]);
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
return String(s == null ? "" : s).replace(/[&<>"']/g, function (ch) {
|
||||
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[ch];
|
||||
});
|
||||
}
|
||||
|
||||
function contactHref(value, kind) {
|
||||
// brand 存裸值(电话/邮箱),data-* 历史上传完整 URL——两者都接受
|
||||
if (!value) return "";
|
||||
if (/^(https?:|mailto:)/i.test(value)) return value;
|
||||
return kind === "wa" ? "https://wa.me/" + value : "mailto:" + value;
|
||||
}
|
||||
|
||||
// boot: brand 落定(成功或失败)后推导有效值再挂载。只改"默认值从哪来",
|
||||
// 不触碰上方主题派生单元(调色板算法零改动,只换输入)。
|
||||
function boot() {
|
||||
var b = (__brand && typeof __brand === "object") ? __brand : {};
|
||||
title = titleAttr || b.title || "Chat with us";
|
||||
whatsapp = contactHref(whatsappAttr || b.whatsapp || "", "wa");
|
||||
email = contactHref(emailAttr || b.email || "", "mail");
|
||||
palette = derivePalette(colorAttr || (b.color || "").trim() || DEFAULT_COLOR);
|
||||
avatarUrl = avatarAttr
|
||||
? (/^https?:\/\//i.test(avatarAttr) ? avatarAttr : wsBase + "/" + avatarAttr.replace(/^\//, ""))
|
||||
: (b.avatar ? wsBase + (b.avatar.charAt(0) === "/" ? b.avatar : "/" + b.avatar)
|
||||
: wsBase + DEFAULT_AVATAR_PATH);
|
||||
welcome = b.welcome || "";
|
||||
topics = Array.isArray(b.topics)
|
||||
? b.topics.filter(function (t) { return typeof t === "string" && t; })
|
||||
: [];
|
||||
placeholder = b.placeholder || "Type a message...";
|
||||
privacyUrl = b.privacy_url || "";
|
||||
brandSlug = String(b.slug || "default").replace(/[^a-zA-Z0-9_-]/g, "") || "default";
|
||||
if (script.getAttribute("data-debug") === "true") {
|
||||
window.__chatWidgetDebug = { derivePalette: derivePalette, palette: palette };
|
||||
}
|
||||
init();
|
||||
}
|
||||
|
||||
var positionStyles = {
|
||||
@@ -146,7 +200,7 @@
|
||||
|
||||
function init() {
|
||||
var host = document.createElement("div");
|
||||
host.id = "cc-widget-host";
|
||||
host.id = "cc-widget-host-" + brandSlug;
|
||||
document.body.appendChild(host);
|
||||
|
||||
var shadow = host.attachShadow({ mode: "open" });
|
||||
@@ -300,32 +354,61 @@
|
||||
var container = document.createElement("div");
|
||||
container.className = "widget-container pos-" + position + (isMobile ? " mobile-host" : "");
|
||||
|
||||
// Task 10: 品牌/联系/隐私/话题分块——值缺位(data-* 与 brand 都没给)则整块
|
||||
// 不渲染(隐私行隐藏、联系按钮隐藏、话题区隐藏),不再硬编码任何租户文案
|
||||
var waBtnHtml = whatsapp
|
||||
? '<a href="' + esc(whatsapp) + '" target="_blank" rel="noopener" class="call-btn whatsapp-btn"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg><span>WhatsApp</span></a>'
|
||||
: "";
|
||||
var emBtnHtml = email
|
||||
? '<a href="' + esc(email) + '" class="call-btn email-btn"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg><span>Email</span></a>'
|
||||
: "";
|
||||
var privacyLineHtml = privacyUrl
|
||||
? 'By chatting, you agree to our <a href="' + esc(privacyUrl) + '" target="_blank" rel="noopener">privacy policy</a>.'
|
||||
: "";
|
||||
var chatFooterHtml = privacyLineHtml
|
||||
? '<div class="chat-footer"><span class="footer-left">' + privacyLineHtml + '</span></div>'
|
||||
: "";
|
||||
var invitePrivacyHtml = privacyLineHtml
|
||||
? '<div class="invite-privacy">' + privacyLineHtml + '</div>'
|
||||
: "";
|
||||
var topicsBtnsHtml = topics.map(function (t) {
|
||||
return '<button class="popular-topic-btn" data-topic="' + esc(t) + '">' + esc(t) + '</button>';
|
||||
}).join("");
|
||||
var inviteTopicsHtml = topics.map(function (t) {
|
||||
return '<button class="invite-topic-btn" data-topic="' + esc(t) + '">' + esc(t) + '</button>';
|
||||
}).join("");
|
||||
var popularTopicsSectionHtml = topics.length
|
||||
? '<div class="popular-topics-fixed" id="w-popular-topics"><div class="popular-topics-label">POPULAR TOPICS</div><div class="popular-topics-grid">' + topicsBtnsHtml + '</div></div><div class="chat-separator-topics"></div>'
|
||||
: "";
|
||||
var welcomeMsgHtml = welcome
|
||||
? '<div class="msg-wrapper"><div class="msg ai">' + esc(welcome) + '</div></div>'
|
||||
: "";
|
||||
|
||||
var panel = document.createElement("div");
|
||||
panel.className = "chat-panel";
|
||||
panel.innerHTML = [
|
||||
'<div class="panel-header">',
|
||||
'<div class="avatar"><span class="avatar-emoji">🤖</span><img class="avatar-img" alt="" src="'+avatarUrl+'"></div>',
|
||||
'<div class="info"><h3>' + title + '</h3><div class="status">Online now · Usually replies in minutes</div></div>',
|
||||
'<div class="info"><h3>' + esc(title) + '</h3><div class="status">Online now · Usually replies in minutes</div></div>',
|
||||
'<div class="actions">',
|
||||
'<button class="action-btn" aria-label="Minimize">−</button>',
|
||||
// '<button class="close-btn action-btn" aria-label="Close">×</button>',
|
||||
'</div></div>',
|
||||
'<div class="ended" id="w-ended">Chat ended. We\'ll be in touch!</div>',
|
||||
'<div class="panel-top">',
|
||||
'<div class="help-block"><div style="display:flex;align-items:center;gap:14px;"><div class="help-content"><div class="help-title">How can we help?</div><div class="help-description">Ask about pipe specifications, quotations, MOQ, shipping and delivery.</div></div><div class="help-actions"><a href="' + whatsapp + '" target="_blank" class="call-btn whatsapp-btn"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg><span>WhatsApp</span></a><a href="' + email + '" class="call-btn email-btn"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg><span>Email</span></a></div></div></div>',
|
||||
'<div class="help-block"><div style="display:flex;align-items:center;gap:14px;"><div class="help-content"><div class="help-title">How can we help?</div><div class="help-description">Ask about pipe specifications, quotations, MOQ, shipping and delivery.</div></div><div class="help-actions">' + waBtnHtml + emBtnHtml + '</div></div></div>',
|
||||
'</div>',
|
||||
'<div class="chat-separator"></div>',
|
||||
'<div class="panel-messages" id="w-msgs">',
|
||||
'<div class="msg-wrapper"><div class="msg ai">What kind of piping project are you working on?</div></div>',
|
||||
welcomeMsgHtml,
|
||||
'</div>',
|
||||
'<div class="typing" id="w-typing"><span></span><span></span><span></span></div>',
|
||||
'<div class="popular-topics-fixed" id="w-popular-topics"><div class="popular-topics-label">POPULAR TOPICS</div><div class="popular-topics-grid"><button class="popular-topic-btn" data-topic="Get Quotation">Get Quotation</button><button class="popular-topic-btn" data-topic="Pipe Specifications">Pipe Specifications</button><button class="popular-topic-btn" data-topic="Delivery & Shipping">Delivery & Shipping</button></div></div>',
|
||||
'<div class="chat-separator-topics"></div>',
|
||||
popularTopicsSectionHtml,
|
||||
'<div class="input-container"><div class="input-wrapper">',
|
||||
'<input type="text" id="w-input" placeholder="Type a message...">',
|
||||
'<input type="text" id="w-input" placeholder="' + esc(placeholder) + '">',
|
||||
'<button id="w-send"><span style="font-size:16px;">➤</span><span>Send</span></button>',
|
||||
'</div></div>',
|
||||
'<div class="chat-footer"><span class="footer-left">By chatting, you agree to our <a href="https://hesupipe.com/privacy-policy/" target="_blank" rel="noopener">privacy policy</a>.</span></div>',
|
||||
chatFooterHtml,
|
||||
].join("");
|
||||
|
||||
// Invite card HTML template
|
||||
@@ -342,23 +425,23 @@
|
||||
'<button class="invite-close-btn" aria-label="Close">',
|
||||
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>',
|
||||
'</button>',
|
||||
'<a href="' + whatsapp + '" target="_blank" class="invite-action-btn whatsapp-btn">',
|
||||
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg>',
|
||||
'<span>WhatsApp</span>',
|
||||
'</a>',
|
||||
'<a href="' + email + '" class="invite-action-btn email-btn">',
|
||||
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>',
|
||||
'<span>Email</span>',
|
||||
'</a>',
|
||||
'</div>',
|
||||
'</div>',
|
||||
'<div class="invite-body">',
|
||||
'<div class="invite-topics">',
|
||||
'<button class="invite-topic-btn" data-topic="Get Quotation">Get Quotation</button>',
|
||||
'<button class="invite-topic-btn" data-topic="Pipe Specifications">Pipe Specifications</button>',
|
||||
'<button class="invite-topic-btn" data-topic="Delivery & Shipping">Delivery & Shipping</button>',
|
||||
(whatsapp
|
||||
? '<a href="' + esc(whatsapp) + '" target="_blank" rel="noopener" class="invite-action-btn whatsapp-btn">'
|
||||
+ '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg>'
|
||||
+ '<span>WhatsApp</span></a>'
|
||||
: ""),
|
||||
(email
|
||||
? '<a href="' + esc(email) + '" class="invite-action-btn email-btn">'
|
||||
+ '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>'
|
||||
+ '<span>Email</span></a>'
|
||||
: ""),
|
||||
'</div>',
|
||||
'</div>',
|
||||
(topics.length
|
||||
? '<div class="invite-body">'
|
||||
+ '<div class="invite-topics">' + inviteTopicsHtml + '</div>'
|
||||
+ '</div>'
|
||||
: ""),
|
||||
'<div class="chat-separator"></div>',
|
||||
'<div class="input-container">',
|
||||
'<div class="invite-actions">',
|
||||
@@ -367,7 +450,7 @@
|
||||
'</button>',
|
||||
'<button class="invite-dismiss-btn">No Thanks</button>',
|
||||
'</div>',
|
||||
'<div class="invite-privacy">By chatting, you agree to our <a href="https://hesupipe.com/privacy-policy/" target="_blank" rel="noopener">privacy policy</a>.</div>',
|
||||
invitePrivacyHtml,
|
||||
'</div>',
|
||||
'</div>'
|
||||
].join("");
|
||||
@@ -429,7 +512,8 @@
|
||||
var inviteTimer = null;
|
||||
var invitePanel = null;
|
||||
var inviteShown = false;
|
||||
var inviteStorageKey = "widget_invite_dismissed_" + window.location.pathname;
|
||||
// Task 10: key 租户化——同浏览器多租户挂件互不踩(不同租户 slug 隔离)
|
||||
var inviteStorageKey = "widget_invite_dismissed_" + brandSlug + "_" + window.location.pathname;
|
||||
var isPermanentlyClosed = false;
|
||||
|
||||
function checkInviteStorage() {
|
||||
@@ -597,10 +681,12 @@
|
||||
wWs = new WebSocket(wsHost + "/ws/chat");
|
||||
|
||||
wWs.onopen = function () {
|
||||
// Task 10: init 带 site(空域名站点如 es 靠 data-site 接入,服务端
|
||||
// 记入 client_info["site"] 作会话站点标;空值服务端忽略)
|
||||
if (wSessionId) {
|
||||
wWs.send(JSON.stringify({ session_id: wSessionId, page_token: pageToken }));
|
||||
wWs.send(JSON.stringify({ session_id: wSessionId, page_token: pageToken, site: siteAttr }));
|
||||
} else {
|
||||
wWs.send(JSON.stringify({ source: "widget", page_token: pageToken }));
|
||||
wWs.send(JSON.stringify({ source: "widget", page_token: pageToken, site: siteAttr }));
|
||||
}
|
||||
// Send any pending message after WebSocket opens
|
||||
if (pendingMessage) {
|
||||
@@ -696,10 +782,14 @@
|
||||
startInviteTimer();
|
||||
}
|
||||
|
||||
// Wait for body if script is in <head>
|
||||
if (document.body) {
|
||||
init();
|
||||
} else {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
// Wait for body if script is in <head>; Task 10: 同时等 brand 落定(成功或
|
||||
// 失败都继续)再挂载——两条件齐备才 init,任一失败路径都不白屏
|
||||
function domReady(fn) {
|
||||
if (document.body) fn();
|
||||
else document.addEventListener("DOMContentLoaded", fn);
|
||||
}
|
||||
fetchBrand().then(function (b) {
|
||||
__brand = b;
|
||||
domReady(boot);
|
||||
});
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user