最初版本
This commit is contained in:
@@ -0,0 +1,650 @@
|
||||
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>
|
||||
/* 旧版深色外观(保留稳定功能,去掉 8/12 红头白底皮肤) */
|
||||
a[href*="chatwoot.com"],
|
||||
a[href*="Chatwoot"],
|
||||
.branding--container,
|
||||
.branding {
|
||||
display: none !important;
|
||||
}
|
||||
html, body, #app, .conversation-wrap, .messages-wrap {
|
||||
background: #090d1a !important;
|
||||
color: #f5f7ff !important;
|
||||
}
|
||||
header {
|
||||
background: #11182b !important;
|
||||
color: #f5f7ff !important;
|
||||
border: 0 !important;
|
||||
border-bottom: 1px solid rgba(255,255,255,.09) !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: #f5f7ff !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: #f5f7ff !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: #19233c !important;
|
||||
color: #f5f7ff !important;
|
||||
border-radius: 14px !important;
|
||||
border: 1px solid rgba(255,255,255,.08) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.user-message .chat-bubble,
|
||||
.user-message-wrap .chat-bubble {
|
||||
background: rgba(123,97,255,.28) !important;
|
||||
color: #f5f7ff !important;
|
||||
border-radius: 14px !important;
|
||||
border: 0 !important;
|
||||
}
|
||||
.chat-message--input,
|
||||
.chat-footer,
|
||||
footer {
|
||||
background: #11182b !important;
|
||||
border-top: 1px solid rgba(255,255,255,.09) !important;
|
||||
}
|
||||
textarea, input[type="text"] {
|
||||
background: #0c1222 !important;
|
||||
color: #f5f7ff !important;
|
||||
border-radius: 12px !important;
|
||||
border: 1px solid rgba(255,255,255,.09) !important;
|
||||
}
|
||||
[data-yd-panel],
|
||||
[data-yd-promo],
|
||||
img[data-yd-promo],
|
||||
[data-yd-powered] {
|
||||
display: none !important;
|
||||
}
|
||||
[data-yd-send] {
|
||||
display: inline-grid !important;
|
||||
place-items: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-left: 8px;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
color: #17120a;
|
||||
background: #ffcc4d;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</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() {
|
||||
// 只藏 branding,不要每次掃整頁 DOM(會卡死)
|
||||
var nodes = document.querySelectorAll(
|
||||
"a[href*='chatwoot'], .branding--container, .branding, .branding--text"
|
||||
);
|
||||
for (var i = 0; i < nodes.length; i += 1) {
|
||||
nodes[i].style.setProperty("display", "none", "important");
|
||||
}
|
||||
}
|
||||
|
||||
function hideStartButtons() {
|
||||
// 不再隱藏「开始会话/開始對話」:退出後要回到可點「开始会话」的首頁
|
||||
}
|
||||
|
||||
function isMessagesView() {
|
||||
var hash = String(window.location.hash || "");
|
||||
return hash.indexOf("messages") !== -1;
|
||||
}
|
||||
|
||||
function goToStartSessionScreen() {
|
||||
try {
|
||||
var url = new URL(window.location.href);
|
||||
url.searchParams.delete("cw_conversation");
|
||||
url.hash = "/";
|
||||
window.location.replace(url.toString());
|
||||
return;
|
||||
} catch (err) {}
|
||||
try {
|
||||
if (
|
||||
window.parent &&
|
||||
typeof window.parent.exitGuestChatToStartSession === "function"
|
||||
) {
|
||||
window.parent.exitGuestChatToStartSession();
|
||||
return;
|
||||
}
|
||||
} catch (err2) {}
|
||||
try {
|
||||
window.parent.postMessage(
|
||||
"chatwoot-widget:" +
|
||||
JSON.stringify({ event: "guestExitToStartSession" }),
|
||||
"*"
|
||||
);
|
||||
} catch (err3) {}
|
||||
}
|
||||
|
||||
function exitAndNewChat() {
|
||||
// 舊名保留:實際改為回到「开始会话」首頁,不再強制開新對話進聊天室
|
||||
goToStartSessionScreen();
|
||||
}
|
||||
|
||||
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) {}
|
||||
}
|
||||
|
||||
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", "#11182b", "important");
|
||||
header.style.setProperty("color", "#f5f7ff", "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:#f5f7ff;flex:1;";
|
||||
box.innerHTML =
|
||||
'<strong style="font-size:16px;color:#f5f7ff;">在线客服</strong>' +
|
||||
'<span style="font-size:11px;opacity:.92;color:#9aa7c5;">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:#f5f7ff;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();
|
||||
goToStartSessionScreen();
|
||||
});
|
||||
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 = "";
|
||||
powered.style.setProperty("display", "none", "important");
|
||||
}
|
||||
}
|
||||
|
||||
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", "rgba(123,97,255,.28)", "important");
|
||||
userBubbles[i].style.setProperty("color", "#f5f7ff", "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", "#19233c", "important");
|
||||
agentBubbles[j].style.setProperty("color", "#f5f7ff", "important");
|
||||
}
|
||||
ensureComposer();
|
||||
// 旧版:不插入快捷面板 / 猜你想问
|
||||
}
|
||||
|
||||
function paintPromo() {
|
||||
// 首頁(开始会话)不套聊天室皮膚,避免蓋住「开始会话」
|
||||
if (!isMessagesView()) {
|
||||
hideChatwootBrand();
|
||||
return;
|
||||
}
|
||||
hideStartButtons();
|
||||
hideFileChrome();
|
||||
hideChatwootBrand();
|
||||
tuneHeader();
|
||||
paintTheme();
|
||||
var promo = document.querySelector("img[data-yd-promo], [data-yd-panel]");
|
||||
if (promo) {
|
||||
promo.remove();
|
||||
}
|
||||
}
|
||||
|
||||
setInterval(paintPromo, 1500);
|
||||
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);
|
||||
// 禁止整頁 reload:訊息一變就刷新會造成聊天室卡死/閃爍
|
||||
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