最初版本
This commit is contained in:
@@ -0,0 +1,724 @@
|
||||
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[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;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
(function () {
|
||||
var PROMO_SRC = "/static/guest_promo.png?v=49";
|
||||
var GREET_MARK = "欢迎光临【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 || "";
|
||||
if (
|
||||
text.indexOf("guest_promo.png") === -1 &&
|
||||
!(text.indexOf("下載") !== -1 && text.indexOf(".png") !== -1)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
var box = el.closest(".file") || el.parentElement;
|
||||
if (box) {
|
||||
box.style.setProperty("display", "none", "important");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findGreetingNode() {
|
||||
var nodes = document.querySelectorAll("p, div, span");
|
||||
for (var i = 0; i < nodes.length; i += 1) {
|
||||
var el = nodes[i];
|
||||
if ((el.textContent || "").indexOf(GREET_MARK) === -1) {
|
||||
continue;
|
||||
}
|
||||
if (el.querySelector && el.querySelector("p, div")) {
|
||||
continue;
|
||||
}
|
||||
return el;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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 fillAndSend(text) {
|
||||
var ta = document.querySelector("textarea");
|
||||
if (!ta) {
|
||||
return;
|
||||
}
|
||||
var setter = Object.getOwnPropertyDescriptor(
|
||||
window.HTMLTextAreaElement.prototype,
|
||||
"value"
|
||||
);
|
||||
if (setter && setter.set) {
|
||||
setter.set.call(ta, text);
|
||||
} else {
|
||||
ta.value = text;
|
||||
}
|
||||
ta.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
ta.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { key: "Enter", code: "Enter", bubbles: 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() {
|
||||
var greet = findGreetingNode();
|
||||
if (!greet || !greet.parentNode) {
|
||||
return;
|
||||
}
|
||||
var panel = document.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 (greet.nextSibling !== panel) {
|
||||
greet.parentNode.insertBefore(panel, greet.nextSibling);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureComposer() {
|
||||
var ta = document.querySelector("textarea");
|
||||
if (!ta || !ta.parentNode) {
|
||||
return;
|
||||
}
|
||||
if (!ta.placeholder || ta.placeholder.indexOf("訊息") !== -1 || ta.placeholder.indexOf("请输入信息") !== -1) {
|
||||
ta.placeholder = "请输入您的问题...";
|
||||
}
|
||||
if (!document.querySelector("[data-yd-send]")) {
|
||||
var send = document.createElement("button");
|
||||
send.type = "button";
|
||||
send.setAttribute("data-yd-send", "1");
|
||||
send.setAttribute("aria-label", "发送");
|
||||
send.textContent = "➤";
|
||||
send.addEventListener("click", function () {
|
||||
fillAndSend(ta.value || "");
|
||||
});
|
||||
ta.parentNode.appendChild(send);
|
||||
}
|
||||
var footer = document.querySelector(".chat-message--input, .chat-footer, footer") || 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() {
|
||||
hideStartButtons();
|
||||
hideFileChrome();
|
||||
hideChatwootBrand();
|
||||
tuneHeader();
|
||||
paintTheme();
|
||||
if (document.querySelector("img[data-yd-promo]")) {
|
||||
return;
|
||||
}
|
||||
var greet = findGreetingNode();
|
||||
if (!greet || !greet.parentNode) {
|
||||
return;
|
||||
}
|
||||
var img = document.createElement("img");
|
||||
img.setAttribute("data-yd-promo", "1");
|
||||
img.src = PROMO_SRC;
|
||||
img.alt = "";
|
||||
img.style.cssText =
|
||||
"display:block;width:100%;max-width:280px;border-radius:12px;margin:0 0 8px;";
|
||||
greet.parentNode.insertBefore(img, greet);
|
||||
}
|
||||
|
||||
setInterval(paintPromo, 250);
|
||||
document.addEventListener("DOMContentLoaded", paintPromo);
|
||||
|
||||
// ActionCable 若仍失敗,輪詢訊息補上坐席回覆
|
||||
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) +
|
||||
"&cw_conversation=" +
|
||||
encodeURIComponent(cw),
|
||||
{ credentials: "same-origin" }
|
||||
)
|
||||
.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 && key !== lastMsgKey) {
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
lastMsgKey = key;
|
||||
})
|
||||
.catch(function () {});
|
||||
} catch (err) {}
|
||||
}
|
||||
setInterval(pollAgentMessages, 2500);
|
||||
setTimeout(pollAgentMessages, 1200);
|
||||
})();
|
||||
</script>
|
||||
"""
|
||||
|
||||
|
||||
def _cable_ws_origin() -> str:
|
||||
from urllib.parse import urlparse
|
||||
|
||||
parsed = urlparse(CONFIG.chatwoot_guest_proxy_target or "http://127.0.0.1:3000")
|
||||
scheme = "wss" if parsed.scheme == "https" else "ws"
|
||||
host = parsed.netloc or "127.0.0.1:3000"
|
||||
return f"{scheme}://{host}"
|
||||
|
||||
|
||||
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")
|
||||
|
||||
return Response(payload, status=status, headers=response_headers)
|
||||
Reference in New Issue
Block a user