Files
2026-08-15 14:51:51 +08:00

1139 lines
34 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode
from urllib.request import Request, urlopen
from flask import Response, request
from config import CONFIG
HOP_BY_HOP = {
"connection",
"keep-alive",
"proxy-authenticate",
"proxy-authorization",
"te",
"trailers",
"transfer-encoding",
"upgrade",
"host",
"content-length",
}
INJECT_SCRIPT = """
<script>
(function () {
var CABLE_ORIGIN = "__CABLE_WS_ORIGIN__";
var NativeWS = window.WebSocket;
if (!NativeWS || window.__ydCablePatched) {
return;
}
window.__ydCablePatched = true;
function WrappedWS(url, protocols) {
var next = String(url || "");
if (next.indexOf("/cable") !== -1 && next.indexOf(CABLE_ORIGIN) !== 0) {
try {
var parsed = new URL(next, window.location.href);
next = CABLE_ORIGIN + "/cable" + (parsed.search || "") + (parsed.hash || "");
} catch (err) {
next = CABLE_ORIGIN + "/cable";
}
}
if (protocols === undefined) {
return new NativeWS(next);
}
return new NativeWS(next, protocols);
}
WrappedWS.prototype = NativeWS.prototype;
WrappedWS.CONNECTING = NativeWS.CONNECTING;
WrappedWS.OPEN = NativeWS.OPEN;
WrappedWS.CLOSING = NativeWS.CLOSING;
WrappedWS.CLOSED = NativeWS.CLOSED;
window.WebSocket = WrappedWS;
})();
</script>
<style>
a[href*="chatwoot.com"],
a[href*="Chatwoot"],
.branding--container,
.branding {
display: none !important;
}
html, body, #app, .conversation-wrap, .messages-wrap {
background: #f5f6f8 !important;
}
header {
background: #e31c23 !important;
color: #fff !important;
border: 0 !important;
box-shadow: none !important;
min-height: 64px !important;
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
gap: 8px !important;
padding: 10px 12px !important;
}
header [data-yd-right] {
display: inline-flex !important;
align-items: center !important;
gap: 8px !important;
margin-left: auto !important;
}
header, header * {
color: #fff !important;
}
header button.close-button,
header button[data-yd-close],
header button:not([data-yd-exit]) {
display: none !important;
}
header button[data-yd-exit] {
display: inline-flex !important;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
padding: 0;
font-size: 26px;
line-height: 1;
color: #fff !important;
background: transparent !important;
border: 0;
cursor: pointer;
}
header button[data-yd-exit] svg {
width: 20px;
height: 20px;
display: block;
}
.agent-message .chat-bubble,
.agent-message-wrap .chat-bubble,
.prose-bubble {
background: #fff !important;
color: #222 !important;
border-radius: 14px !important;
border: 0 !important;
box-shadow: 0 1px 2px rgba(0,0,0,.06) !important;
}
.user-message .chat-bubble,
.user-message-wrap .chat-bubble {
background: #ffe5e5 !important;
color: #222 !important;
border-radius: 14px !important;
border: 0 !important;
}
.chat-message--input,
.chat-footer,
footer {
background: #fff !important;
border-top: 1px solid #eee !important;
}
textarea, input[type="text"] {
background: #f2f3f5 !important;
color: #222 !important;
border-radius: 999px !important;
border: 0 !important;
}
[data-yd-panel] {
margin: 10px 0 4px;
padding: 0;
font-size: 13px;
color: #333;
width: 100%;
max-width: 320px;
}
[data-yd-panel] .yd-hint {
margin: 0 0 10px;
color: #666;
}
[data-yd-panel] .yd-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 8px;
}
[data-yd-panel] .yd-grid button {
display: flex;
align-items: center;
gap: 6px;
min-height: 42px;
padding: 8px 10px;
border: 1px solid #e6e6e6;
border-radius: 10px;
background: #fff;
color: #333;
text-align: left;
cursor: pointer;
font-size: 12px;
}
[data-yd-panel] .yd-grid button span {
display: inline-grid;
place-items: center;
width: 22px;
height: 22px;
border-radius: 6px;
color: #fff;
font-size: 11px;
font-weight: 700;
}
[data-yd-panel] .yd-faq-title {
margin: 14px 0 6px;
font-size: 14px;
font-weight: 800;
}
[data-yd-panel] .yd-faq button {
display: flex;
width: 100%;
align-items: center;
justify-content: space-between;
padding: 12px 2px;
border: 0;
border-bottom: 1px solid #eee;
background: transparent;
color: #333;
text-align: left;
cursor: pointer;
}
[data-yd-send] {
display: inline-grid !important;
place-items: center;
width: 36px;
height: 36px;
margin-left: 8px;
border: 0;
border-radius: 999px;
color: #fff;
background: #e31c23;
font-size: 16px;
cursor: pointer;
}
[data-yd-powered] {
display: block;
padding: 4px 0 8px;
color: #b0b0b0;
font-size: 11px;
text-align: center;
}
/* 隱藏 Chatwoot 郵件收集提示與信箱輸入 */
.agent-message:has(input[type="email"]),
.agent-message-wrap:has(input[type="email"]),
form[data-testid="email-input"],
.email-input,
input[type="email"],
input[placeholder*="電子信箱"],
input[placeholder*="电子邮箱"],
input[placeholder*="email" i] {
display: none !important;
}
</style>
<script>
(function () {
var PROMO_SRC = "/static/guest_promo.png?v=49";
var GREET_MARK = "欢迎光临【49彩票集团】";
var GREET_MARKS = [
"欢迎光临【49彩票集团】",
"歡迎光臨【49彩票集团】",
"欢迎光临【49彩票集團】",
"歡迎光臨【49彩票集團】"
];
var QUICK = [
{ label: "充值问题", text: "我想咨询充值问题", color: "#3b82f6", icon: "充" },
{ label: "提款问题", text: "我想咨询提款问题", color: "#22c55e", icon: "提" },
{ label: "优惠活动", text: "我想咨询优惠活动", color: "#f59e0b", icon: "惠" },
{ label: "账户问题", text: "我想咨询账户问题", color: "#8b5cf6", icon: "户" },
{ label: "游戏问题", text: "我想咨询游戏问题", color: "#a855f7", icon: "游" },
{ label: "代理合作", text: "我想咨询代理合作", color: "#f97316", icon: "代" },
{ label: "常见问题", text: "常见问题", color: "#ef4444", icon: "?" },
{ label: "转人工客服", text: "转人工客服", color: "#e31c23", icon: "人" }
];
var FAQ = [
"提款未到账怎么办?",
"如何修改登录密码?",
"如何绑定银行卡?"
];
function norm(text) {
return String(text || "").replace(/\s+/g, "");
}
function hideChatwootBrand() {
var nodes = document.querySelectorAll("a, p, span, div, small");
for (var i = 0; i < nodes.length; i += 1) {
var el = nodes[i];
var text = norm(el.textContent);
if (
text.indexOf("Chatwoot") === -1 &&
text.indexOf("chatwoot") === -1 &&
text.indexOf("由Chatwoot支援") === -1
) {
continue;
}
if (text.length > 40) {
continue;
}
el.style.setProperty("display", "none", "important");
}
}
function hideStartButtons() {
var nodes = document.querySelectorAll("button, a, .button, [role='button']");
for (var i = 0; i < nodes.length; i += 1) {
var el = nodes[i];
if (
norm(el.textContent).indexOf("開始對話") === -1 &&
norm(el.textContent).indexOf("开始对话") === -1
) {
continue;
}
el.style.setProperty("display", "none", "important");
el.style.setProperty("visibility", "hidden", "important");
}
}
function hideFileChrome() {
var nodes = document.querySelectorAll("a, button, div, span, p");
for (var i = 0; i < nodes.length; i += 1) {
var el = nodes[i];
var text = el.textContent || "";
var compact = text.replace(/\s+/g, "");
var uuidDl = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}下載/i.test(compact);
if (
text.indexOf("guest_promo.png") === -1 &&
!(text.indexOf("下載") !== -1 && text.indexOf(".png") !== -1) &&
!uuidDl
) {
continue;
}
var box = el.closest(".file") || el.parentElement;
if (box) {
box.style.setProperty("display", "none", "important");
}
}
}
function findGreetingNode() {
var marks = GREET_MARKS.map(function (m) {
return norm(m);
});
var nodes = document.querySelectorAll("p, div, span, li");
var best = null;
var bestLen = 1e9;
for (var i = 0; i < nodes.length; i += 1) {
var el = nodes[i];
var text = norm(el.textContent);
if (!text || text.length > 240) {
continue;
}
var hit = false;
for (var m = 0; m < marks.length; m += 1) {
if (text.indexOf(marks[m]) !== -1) {
hit = true;
break;
}
}
if (!hit) {
continue;
}
if (text.length < bestLen) {
best = el;
bestLen = text.length;
}
}
return best;
}
function findGreetingHost() {
var greet = findGreetingNode();
if (!greet) {
return null;
}
return (
greet.closest(
".chat-bubble, .prose-bubble, .agent-message .message-content, .agent-message-wrap, .agent-message"
) || greet
);
}
function closeWidget() {
try {
window.parent.postMessage(
"chatwoot-widget:" + JSON.stringify({ event: "closeWindow" }),
"*"
);
} catch (err) {}
}
function exitAndNewChat() {
try {
if (
window.parent &&
typeof window.parent.exitGuestChatAndStartNew === "function"
) {
window.parent.exitGuestChatAndStartNew();
return;
}
if (
window.parent &&
typeof window.parent.openGuestChatNewConversation === "function"
) {
window.parent.openGuestChatNewConversation();
return;
}
} catch (err) {}
try {
window.parent.postMessage(
"chatwoot-widget:" +
JSON.stringify({ event: "guestExitNewChat" }),
"*"
);
} catch (err2) {}
}
var EXIT_ICON =
'<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">' +
'<path d="M10 4H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h4" stroke="#fff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>' +
'<path d="M14 12H21" stroke="#fff" stroke-width="1.8" stroke-linecap="round"/>' +
'<path d="M18 8l4 4-4 4" stroke="#fff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>' +
"</svg>";
function findVueStore() {
try {
var root = document.querySelector("#app");
if (!root) {
return null;
}
if (root.__vue__ && root.__vue__.$store) {
return root.__vue__.$store;
}
if (root.__vue__ && root.__vue__.$root && root.__vue__.$root.$store) {
return root.__vue__.$root.$store;
}
var app = root.__vue_app__;
if (
app &&
app._context &&
app._context.config &&
app._context.config.globalProperties &&
app._context.config.globalProperties.$store
) {
return app._context.config.globalProperties.$store;
}
var nodes = document.querySelectorAll("*");
for (var i = 0; i < nodes.length && i < 400; i += 1) {
var el = nodes[i];
if (el.__vue__ && el.__vue__.$store) {
return el.__vue__.$store;
}
if (
el.__vueParentComponent &&
el.__vueParentComponent.proxy &&
el.__vueParentComponent.proxy.$store
) {
return el.__vueParentComponent.proxy.$store;
}
}
} catch (err) {}
return null;
}
function clearComposer() {
try {
var ta = document.querySelector(
"#chat-input, textarea.user-message-input, textarea"
);
if (!ta) {
return;
}
var setter = Object.getOwnPropertyDescriptor(
window.HTMLTextAreaElement.prototype,
"value"
);
if (setter && setter.set) {
setter.set.call(ta, "");
} else {
ta.value = "";
}
ta.dispatchEvent(new Event("input", { bubbles: true }));
ta.dispatchEvent(
new InputEvent("input", { bubbles: true, composed: true, inputType: "deleteContent" })
);
// Vue ChatInputWrap.userInput
var comp =
(ta.__vueParentComponent && ta.__vueParentComponent.proxy) ||
(ta.__vue__ && ta.__vue__.$parent) ||
null;
if (comp && typeof comp.userInput === "string") {
comp.userInput = "";
}
} catch (err) {}
}
function fillAndSend(text) {
var content = String(text || "").trim();
if (!content) {
return;
}
// 優先走 Vuex,避免只改 DOM 導致 Chatwoot userInput 仍是空的
var store = findVueStore();
if (store && typeof store.dispatch === "function") {
try {
store.dispatch("conversation/sendMessage", { content: content });
clearComposer();
return;
} catch (err) {}
}
var ta = document.querySelector(
"#chat-input, textarea.user-message-input, textarea"
);
if (!ta) {
return;
}
var setter = Object.getOwnPropertyDescriptor(
window.HTMLTextAreaElement.prototype,
"value"
);
if (setter && setter.set) {
setter.set.call(ta, content);
} else {
ta.value = content;
}
ta.dispatchEvent(new Event("input", { bubbles: true }));
ta.dispatchEvent(
new InputEvent("input", {
bubbles: true,
composed: true,
data: content,
inputType: "insertText",
})
);
var nativeSend =
document.querySelector(".chat-message--input button[type='submit']") ||
document.querySelector("button[type='submit']") ||
null;
if (nativeSend && !nativeSend.disabled) {
nativeSend.click();
return;
}
// Chatwoot 監聽的是 document keypress + keyCode 13
document.dispatchEvent(
new KeyboardEvent("keypress", {
key: "Enter",
code: "Enter",
keyCode: 13,
which: 13,
bubbles: true,
cancelable: true,
})
);
}
function tuneHeader() {
var header = document.querySelector("header");
if (!header) {
return;
}
header.style.setProperty("background", "#e31c23", "important");
header.style.setProperty("color", "#fff", "important");
header.style.setProperty("display", "flex", "important");
header.style.setProperty("align-items", "center", "important");
header.style.setProperty("justify-content", "space-between", "important");
var oldTitles = header.querySelectorAll("h1, h2, p, span, strong, div");
for (var ot = 0; ot < oldTitles.length; ot += 1) {
var node = oldTitles[ot];
if (
node.closest("[data-yd-title]") ||
node.closest("[data-yd-online]") ||
node.closest("[data-yd-right]") ||
node.getAttribute("data-yd-title") ||
node.getAttribute("data-yd-online") ||
node.getAttribute("data-yd-right") ||
node.tagName === "BUTTON" ||
(node.querySelector &&
node.querySelector(
"button, img, svg, [data-yd-title], [data-yd-online], [data-yd-right]"
))
) {
continue;
}
var t = norm(node.textContent);
if (!t || t === "×" || t === "在线" || t.indexOf("7x24") !== -1 || t === "在线客服") {
continue;
}
if (t.length <= 30) {
node.style.setProperty("display", "none", "important");
}
}
if (!header.querySelector("[data-yd-title]")) {
var box = document.createElement("div");
box.setAttribute("data-yd-title", "1");
box.style.cssText =
"display:flex;flex-direction:column;gap:2px;line-height:1.2;color:#fff;flex:1;";
box.innerHTML =
'<strong style="font-size:16px;color:#fff;">在线客服</strong>' +
'<span style="font-size:11px;opacity:.92;color:#fff;">7x24小时为您服务</span>';
header.insertBefore(box, header.firstChild);
}
var right = header.querySelector("[data-yd-right]");
if (!right) {
right = document.createElement("div");
right.setAttribute("data-yd-right", "1");
right.style.cssText =
"display:inline-flex;align-items:center;gap:8px;margin-left:auto;";
header.appendChild(right);
}
var online = header.querySelector("[data-yd-online]");
if (!online) {
online = document.createElement("span");
online.setAttribute("data-yd-online", "1");
online.style.cssText =
"display:inline-flex;align-items:center;gap:5px;font-size:12px;color:#fff;white-space:nowrap;";
online.innerHTML =
'<i style="width:8px;height:8px;border-radius:999px;background:#22c55e;display:inline-block;"></i>在线';
}
if (online.parentNode !== right) {
right.appendChild(online);
}
// 隱藏原生關閉鈕,改用「退出並開新聊天」圖示
var buttons = header.querySelectorAll("button");
for (var i = 0; i < buttons.length; i += 1) {
var btn = buttons[i];
if (btn.getAttribute("data-yd-exit")) {
continue;
}
btn.style.setProperty("display", "none", "important");
}
var exitBtn = header.querySelector("button[data-yd-exit]");
if (!exitBtn) {
exitBtn = document.createElement("button");
exitBtn.type = "button";
exitBtn.setAttribute("data-yd-exit", "1");
exitBtn.setAttribute("aria-label", "退出并开启新对话");
exitBtn.title = "退出并开启新对话";
exitBtn.innerHTML = EXIT_ICON;
exitBtn.addEventListener("click", function (event) {
event.preventDefault();
event.stopPropagation();
exitAndNewChat();
});
right.appendChild(exitBtn);
}
exitBtn.style.setProperty("display", "inline-flex", "important");
if (exitBtn.parentNode !== right) {
right.appendChild(exitBtn);
} else if (online.nextSibling !== exitBtn) {
right.appendChild(exitBtn);
}
}
function ensureQuickPanel() {
// Vue 會清掉 chat-bubble 裡多插的兄弟節點;必須插進 .message-content 內部
var marks = GREET_MARKS.map(function (m) {
return norm(m);
});
var contents = document.querySelectorAll(
".message-content, .chat-bubble .text-block, .chat-bubble"
);
var target = null;
for (var i = 0; i < contents.length; i += 1) {
var el = contents[i];
var text = norm(el.textContent);
if (!text || text.length > 280) {
continue;
}
var hit = false;
for (var m = 0; m < marks.length; m += 1) {
if (text.indexOf(marks[m]) !== -1) {
hit = true;
break;
}
}
if (!hit) {
continue;
}
// 優先 message-content
if (
(el.className || "").indexOf("message-content") !== -1 ||
el.classList.contains("message-content")
) {
target = el;
break;
}
if (!target) {
target = el;
}
}
if (!target) {
return;
}
var panel = target.querySelector("[data-yd-panel]");
if (!panel) {
panel = document.createElement("div");
panel.setAttribute("data-yd-panel", "1");
var hint = document.createElement("p");
hint.className = "yd-hint";
hint.textContent = "您可以点击下方快捷入口获取帮助";
panel.appendChild(hint);
var grid = document.createElement("div");
grid.className = "yd-grid";
QUICK.forEach(function (item) {
var b = document.createElement("button");
b.type = "button";
b.innerHTML =
'<span style="background:' +
item.color +
'">' +
item.icon +
"</span>" +
item.label;
b.addEventListener("click", function () {
fillAndSend(item.text);
});
grid.appendChild(b);
});
panel.appendChild(grid);
var faqTitle = document.createElement("div");
faqTitle.className = "yd-faq-title";
faqTitle.textContent = "猜你想问";
panel.appendChild(faqTitle);
var faq = document.createElement("div");
faq.className = "yd-faq";
FAQ.forEach(function (q) {
var b = document.createElement("button");
b.type = "button";
b.innerHTML = "<span>" + q + "</span><span>›</span>";
b.addEventListener("click", function () {
fillAndSend(q);
});
faq.appendChild(b);
});
panel.appendChild(faq);
}
if (panel.parentNode !== target) {
target.appendChild(panel);
}
var promo = target.querySelector("img[data-yd-promo]");
if (!promo) {
promo = document.createElement("img");
promo.setAttribute("data-yd-promo", "1");
promo.src = PROMO_SRC;
promo.alt = "";
promo.style.cssText =
"display:block;width:100%;max-width:280px;border-radius:12px;margin:0 0 8px;";
}
if (promo.parentNode !== target || target.firstChild !== promo) {
target.insertBefore(promo, target.firstChild);
}
}
function ensureComposer() {
var ta = document.querySelector(
"#chat-input, textarea.user-message-input, textarea"
);
if (!ta) {
return;
}
if (
!ta.placeholder ||
ta.placeholder.indexOf("訊息") !== -1 ||
ta.placeholder.indexOf("请输入信息") !== -1 ||
ta.placeholder.indexOf("Type here") !== -1
) {
ta.placeholder = "请输入您的问题...";
}
// 移除自訂發送鈕,改用 Chatwoot 原生按鈕(自訂鈕未更新 Vue userInput,按了沒反應)
var fakeSends = document.querySelectorAll("[data-yd-send]");
for (var i = 0; i < fakeSends.length; i += 1) {
fakeSends[i].remove();
}
// 強化原生送出:若 Vue 尚未同步,點擊時改走 Vuex
var nativeSend = document.querySelector("button[type='submit']");
if (nativeSend && !nativeSend.getAttribute("data-yd-send-bound")) {
nativeSend.setAttribute("data-yd-send-bound", "1");
nativeSend.addEventListener(
"click",
function (event) {
try {
var current = String((ta && ta.value) || "").trim();
if (!current) {
return;
}
var store = findVueStore();
if (store && typeof store.dispatch === "function") {
var proxy =
(ta.__vueParentComponent && ta.__vueParentComponent.proxy) ||
null;
var vueInput =
proxy && typeof proxy.userInput === "string" ? proxy.userInput : null;
if (vueInput !== null && !String(vueInput).trim() && current) {
event.preventDefault();
event.stopPropagation();
store.dispatch("conversation/sendMessage", { content: current });
clearComposer();
}
}
} catch (err) {}
},
true
);
}
var footer =
document.querySelector(".chat-message--input, .chat-footer, footer") ||
(ta.parentNode && ta.parentNode.parentNode);
var powered = document.querySelector("[data-yd-powered]");
if (footer && !powered) {
powered = document.createElement("div");
powered.setAttribute("data-yd-powered", "1");
footer.appendChild(powered);
}
if (powered) {
powered.textContent = "49彩票AI智能客服";
}
}
function paintTheme() {
var userBubbles = document.querySelectorAll(
".user-message .chat-bubble, .user-message-wrap .chat-bubble"
);
for (var i = 0; i < userBubbles.length; i += 1) {
userBubbles[i].style.setProperty("background", "#ffe5e5", "important");
userBubbles[i].style.setProperty("color", "#222", "important");
}
var agentBubbles = document.querySelectorAll(
".agent-message .chat-bubble, .agent-message-wrap .chat-bubble"
);
for (var j = 0; j < agentBubbles.length; j += 1) {
agentBubbles[j].style.setProperty("background", "#fff", "important");
agentBubbles[j].style.setProperty("color", "#222", "important");
}
ensureComposer();
ensureQuickPanel();
}
function paintPromo() {
if (window.__ydPainting) {
return;
}
window.__ydPainting = true;
try {
hideStartButtons();
hideFileChrome();
hideChatwootBrand();
tuneHeader();
paintTheme();
ensureQuickPanel();
} catch (errPaint) {
} finally {
window.__ydPainting = false;
}
}
setInterval(paintPromo, 500);
document.addEventListener("DOMContentLoaded", paintPromo);
setTimeout(paintPromo, 200);
setTimeout(paintPromo, 800);
setTimeout(paintPromo, 2000);
setTimeout(paintPromo, 4000);
window.__ydEnsureGuestUI = paintPromo;
// 隱藏郵件收集氣泡(含「給個聯繫方式」「透過電子郵件」)
function hideEmailCollect() {
try {
var nodes = document.querySelectorAll(
".agent-message, .agent-message-wrap, .message, li"
);
for (var i = 0; i < nodes.length; i += 1) {
var el = nodes[i];
var text = (el.textContent || "").replace(/\s+/g, "");
var hasEmailInput =
!!el.querySelector('input[type="email"]') ||
text.indexOf("請輸入您的電子信箱") !== -1 ||
text.indexOf("请输入您的电子邮箱") !== -1;
var isCollectText =
text.indexOf("給個聯繫方式") !== -1 ||
text.indexOf("给个联系方式") !== -1 ||
text.indexOf("透過電子郵件得到通知") !== -1 ||
text.indexOf("通过电子邮件得到通知") !== -1;
if (hasEmailInput || isCollectText) {
el.style.setProperty("display", "none", "important");
}
}
} catch (err) {}
}
setInterval(hideEmailCollect, 600);
setTimeout(hideEmailCollect, 200);
// 隱藏/轉換 MaxKB 殘留的 quick_question 原文標籤
function polishQuickQuestions() {
try {
var bubbles = document.querySelectorAll(
".agent-message .chat-bubble, .agent-message-wrap .chat-bubble, .prose-bubble, .message-content, p"
);
var tagOpen = "<" + "quick_question>";
var tagClose = "</" + "quick_question>";
var encOpen = "&lt;quick_question&gt;";
var encClose = "&lt;/" + "quick_question&gt;";
for (var i = 0; i < bubbles.length; i += 1) {
var el = bubbles[i];
if (el.getAttribute("data-yd-qq-done") === "1") {
continue;
}
var html = el.innerHTML || "";
var text = el.textContent || "";
if (text.indexOf("quick_question") === -1 && html.indexOf("quick_question") === -1) {
continue;
}
var labels = [];
var cleaned = html;
function pullTags(openTok, closeTok) {
var guard = 0;
while (guard < 20) {
guard += 1;
var a = cleaned.indexOf(openTok);
if (a === -1) {
break;
}
var b = cleaned.indexOf(closeTok, a + openTok.length);
if (b === -1) {
break;
}
var label = cleaned
.slice(a + openTok.length, b)
.replace(/<[^>]+>/g, "")
.replace(/\s+/g, " ")
.trim();
if (label) {
labels.push(label);
}
cleaned = cleaned.slice(0, a) + cleaned.slice(b + closeTok.length);
}
}
pullTags(encOpen, encClose);
pullTags(tagOpen, tagClose);
cleaned = cleaned.trim();
el.innerHTML = cleaned;
el.setAttribute("data-yd-qq-done", "1");
if (labels.length && el.parentNode) {
var wrap = document.createElement("div");
wrap.setAttribute("data-yd-qq", "1");
wrap.style.cssText = "display:flex;flex-wrap:wrap;gap:8px;margin:8px 0 4px;";
labels.forEach(function (label) {
var btn = document.createElement("button");
btn.type = "button";
btn.textContent = label;
btn.style.cssText =
"border:1px solid #e31c23;color:#e31c23;background:#fff;border-radius:999px;padding:6px 12px;font-size:13px;cursor:pointer;";
btn.addEventListener("click", function () {
fillAndSend(label);
});
wrap.appendChild(btn);
});
if (el.nextSibling) {
el.parentNode.insertBefore(wrap, el.nextSibling);
} else {
el.parentNode.appendChild(wrap);
}
}
}
} catch (err) {}
}
setInterval(polishQuickQuestions, 700);
setTimeout(polishQuickQuestions, 300);
// ActionCable 失敗時:用 Vuex syncLatestMessages 補訊息,禁止 location.reload(會閃爍)
function softSyncMessages(list) {
var store = findVueStore();
if (!store || typeof store.dispatch !== "function") {
return false;
}
try {
// 只同步缺的訊息,避免整包 addOrUpdate 重繪把快捷入口清掉
store.dispatch("conversation/syncLatestMessages");
store.dispatch("conversation/toggleAgentTyping", { status: false });
return true;
} catch (err) {
try {
store.dispatch("conversation/fetchOldConversations", {});
return true;
} catch (err2) {
return false;
}
}
}
var lastMsgKey = "";
function pollAgentMessages() {
try {
var params = new URLSearchParams(window.location.search);
var token = params.get("website_token");
var cw = params.get("cw_conversation");
if (!token || !cw) {
return;
}
fetch(
"/api/v1/widget/messages?website_token=" +
encodeURIComponent(token),
{
credentials: "same-origin",
headers: { "X-Auth-Token": cw }
}
)
.then(function (response) {
return response.json();
})
.then(function (data) {
var list = [];
if (data && Array.isArray(data.payload)) {
list = data.payload;
} else if (data && data.payload && Array.isArray(data.payload.messages)) {
list = data.payload.messages;
} else if (Array.isArray(data)) {
list = data;
} else if (data && Array.isArray(data.messages)) {
list = data.messages;
}
if (!list.length) {
return;
}
var maxId = 0;
for (var i = 0; i < list.length; i += 1) {
var id = Number((list[i] && list[i].id) || 0);
if (id > maxId) {
maxId = id;
}
}
var key = String(maxId) + ":" + String(list.length);
if (!lastMsgKey) {
lastMsgKey = key;
return;
}
if (key !== lastMsgKey) {
lastMsgKey = key;
softSyncMessages(list);
hideEmailCollect();
setTimeout(paintPromo, 150);
}
})
.catch(function () {});
} catch (err) {}
}
setInterval(pollAgentMessages, 2500);
setTimeout(pollAgentMessages, 1200);
})();
</script>
"""
def _cable_ws_origin() -> str:
from urllib.parse import urlparse
from flask import has_request_context, request
# 浏览器 WebSocket 必须走对外 host + /cw,不能注入容器内 127.0.0.1:3000
if has_request_context() and getattr(request, "host", None):
scheme = "wss" if request.scheme == "https" else "ws"
return f"{scheme}://{request.host}/cw"
public_base = (
CONFIG.chatwoot_guest_base_url
or CONFIG.chatwoot_base_url
or "http://127.0.0.1:7788/cw"
)
parsed = urlparse(public_base)
scheme = "wss" if parsed.scheme == "https" else "ws"
host = parsed.netloc or "127.0.0.1:7788"
path = (parsed.path or "/cw").rstrip("/") or "/cw"
return f"{scheme}://{host}{path}"
def _inject_script() -> str:
return INJECT_SCRIPT.replace("__CABLE_WS_ORIGIN__", _cable_ws_origin())
def _forward_headers() -> dict[str, str]:
headers: dict[str, str] = {}
for key, value in request.headers.items():
if key.lower() in HOP_BY_HOP:
continue
headers[key] = value
return headers
def proxy_chatwoot(subpath: str) -> Response:
target = CONFIG.chatwoot_guest_proxy_target.rstrip("/")
query = urlencode(request.args, doseq=True)
url = f"{target}/{subpath.lstrip('/')}"
if query:
url = f"{url}?{query}"
body = request.get_data() if request.method not in {"GET", "HEAD"} else None
forwarded = Request(
url,
data=body,
headers=_forward_headers(),
method=request.method,
)
try:
with urlopen(forwarded, timeout=30) as upstream:
payload = upstream.read()
status = upstream.status
content_type = upstream.headers.get("Content-Type", "")
response_headers = [
(key, value)
for key, value in upstream.headers.items()
if key.lower() not in HOP_BY_HOP
]
except HTTPError as exc:
payload = exc.read()
status = exc.code
content_type = exc.headers.get("Content-Type", "") if exc.headers else ""
response_headers = []
except URLError as exc:
return Response(
f"无法连线本机 Chatwoot:{exc.reason}",
status=502,
mimetype="text/plain; charset=utf-8",
)
if "text/html" in content_type.lower() and payload:
html = payload.decode("utf-8", "replace")
inject = _inject_script()
if "</head>" in html:
html = html.replace("</head>", inject + "</head>", 1)
elif "</body>" in html:
html = html.replace("</body>", inject + "</body>", 1)
else:
html += inject
payload = html.encode("utf-8")
# 避免瀏覽器快取舊版 widget HTML(導致注入腳本不更新)
response_headers = [
(key, value)
for key, value in response_headers
if key.lower()
not in {
"content-length",
"content-encoding",
"etag",
"last-modified",
"cache-control",
"pragma",
"expires",
}
]
response_headers.extend(
[
("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0"),
("Pragma", "no-cache"),
("Expires", "0"),
]
)
return Response(payload, status=status, headers=response_headers)