1139 lines
34 KiB
Python
1139 lines
34 KiB
Python
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 = "<quick_question>";
|
||
var encClose = "</" + "quick_question>";
|
||
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)
|