最初版本

This commit is contained in:
sa19840531
2026-08-15 14:51:51 +08:00
parent 4e01df4dce
commit ea47ea4449
176 changed files with 21009 additions and 0 deletions
+650
View File
@@ -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)