feat: brand 白名单下发(站点覆盖/租户头像/CORS) + widget/chat 客户端 key 租户化 + 会话站点标
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
+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);
|
||||
});
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user