加字体大小设置,修复人工客服轮询和限流问题

This commit is contained in:
songxinnuo
2026-10-28 10:20:23 +08:00
parent 3a6eece72b
commit 7abd0e898c
9 changed files with 149 additions and 88 deletions
+13 -5
View File
@@ -44,7 +44,7 @@ _hits = defaultdict(list)
@app.before_request @app.before_request
def rate_limit(): def rate_limit():
p = request.path p = request.path
if p.startswith("/static"): if p.startswith("/static") or p.startswith("/api/human/"):
return None return None
ip = request.remote_addr or "unknown" ip = request.remote_addr or "unknown"
now = time.time() now = time.time()
@@ -1236,10 +1236,6 @@ def get_history():
# ===== 管理员后台 ===== # ===== 管理员后台 =====
@app.route("/admin")
def admin():
return render_template("admin.html")
@app.route("/api/admin/stats") @app.route("/api/admin/stats")
def admin_stats(): def admin_stats():
conn = sqlite3.connect(DB) conn = sqlite3.connect(DB)
@@ -1309,6 +1305,13 @@ def human_queue():
conn.close() conn.close()
return jsonify([{"id": r[0], "conv_id": r[1], "ctime": r[3]} for r in rows]) return jsonify([{"id": r[0], "conv_id": r[1], "ctime": r[3]} for r in rows])
@app.route("/api/human/active")
def human_active():
conn = sqlite3.connect(DB)
rows = conn.execute("SELECT id, conv_id FROM human_sessions WHERE status='active' ORDER BY id").fetchall()
conn.close()
return jsonify([{"id": r[0], "conv_id": r[1]} for r in rows])
@app.route("/api/human/accept", methods=["POST"]) @app.route("/api/human/accept", methods=["POST"])
def human_accept(): def human_accept():
sid = request.json.get("sid") sid = request.json.get("sid")
@@ -1434,6 +1437,11 @@ def uploaded_file(name):
def index(): def index():
return render_template("index.html") return render_template("index.html")
@app.route("/admin")
def admin():
return render_template("admin.html")
if __name__ == "__main__": if __name__ == "__main__":
# 0.0.0.0 = 接受局域网内所有设备访问(手机、其他电脑); # 0.0.0.0 = 接受局域网内所有设备访问(手机、其他电脑);
# 只在演示时开启,演示完建议改回 127.0.0.1,避免别人误用消耗 API 额度 # 只在演示时开启,演示完建议改回 127.0.0.1,避免别人误用消耗 API 额度
+1
View File
@@ -0,0 +1 @@
{"sid":24}
+5
View File
@@ -0,0 +1,5 @@
import sqlite3
c = sqlite3.connect('history.db')
print(c.execute('SELECT * FROM human_messages ORDER BY id DESC LIMIT 5').fetchall())
print('---')
print(c.execute("SELECT id, status FROM human_sessions WHERE status!='closed'").fetchall())
+5
View File
@@ -0,0 +1,5 @@
import sqlite3
c = sqlite3.connect('history.db')
c.execute("UPDATE human_sessions SET status='closed' WHERE status='active'")
c.commit()
print('ok')
+1
View File
@@ -0,0 +1 @@
{"sid":48,"side":"agent","content":"test123"}
+1
View File
@@ -0,0 +1 @@
{"conv_id":999}
+76 -50
View File
@@ -62,97 +62,123 @@ th,td{padding:6px 10px;border-bottom:1px solid #eee;text-align:left;}
<a href="/">← 返回客服页</a> <a href="/">← 返回客服页</a>
<script> <script>
let curSid = 0; var curSid = 0;
let lastId = 0; var sessions = {};
const sessions = {}; // sid -> {lastId, msgs:[]}
fetch("/api/admin/stats").then(r=>r.json()).then(d=>{ fetch("/api/admin/stats").then(function(r){return r.json()}).then(function(d){
s1.textContent=d.total_conv; s2.textContent=d.total_msg; s1.textContent=d.total_conv; s2.textContent=d.total_msg;
s3.textContent=d.today_conv; s4.textContent=d.good; s5.textContent=d.bad; s3.textContent=d.today_conv; s4.textContent=d.good; s5.textContent=d.bad;
}); });
function loadQueue(){ function loadQueue(){
fetch("/api/human/queue").then(r=>r.json()).then(list=>{ fetch("/api/human/queue").then(function(r){return r.json()}).then(function(list){
document.getElementById("queue").innerHTML = list.length document.getElementById("queue").innerHTML = list.length
? list.map(x=>`<button class="btn" onclick="accept(${x.id})">#${x.id} ${x.ctime}</button>`).join(" ") ? list.map(function(x){return '<button class="btn" onclick="accept('+x.id+')">#'+x.id+' '+x.ctime+'</button>'}).join(" ")
: "暂无"; : "暂无";
});
}
loadQueue(); setInterval(loadQueue, 3000);
// 自动接入等待的客户
setInterval(()=>{
fetch("/api/human/queue").then(r=>r.json()).then(list=>{
if(list.length && !sessions[list[0].id]){ if(list.length && !sessions[list[0].id]){
accept(list[0].id); accept(list[0].id);
} }
}); });
}, 3000); }
setInterval(loadQueue, 3000);
// 加载已有进行中的对话
fetch("/api/human/active").then(function(r){return r.json()}).then(function(list){
list.forEach(function(s){
sessions[String(s.id)] = {lastId:0, msgs:[], unread:0};
});
renderSessionList();
if(list.length && !curSid) switchSession(String(list[0].id));
});
function accept(sid){ function accept(sid){
fetch("/api/human/accept",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sid})}).then(()=>{ sid = String(sid);
sessions[sid] = {lastId:0}; if(sessions[sid]) return;
fetch("/api/human/accept",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sid:parseInt(sid)})}).then(function(){
sessions[sid] = {lastId:0, msgs:[], unread:0};
switchSession(sid); switchSession(sid);
}); });
} }
function switchSession(sid){ function switchSession(sid){
curSid = sid; curSid = String(sid);
lastId = sessions[sid].lastId;
document.getElementById("chatCard").style.display="block"; document.getElementById("chatCard").style.display="block";
document.getElementById("chatTitle").textContent="对话 #"+sid; document.getElementById("chatTitle").textContent="对话 #"+sid;
// 渲染已有消息
document.getElementById("chatBox").innerHTML = ""; document.getElementById("chatBox").innerHTML = "";
sessions[sid].msgs.forEach(m=>{ sessions[sid].msgs.forEach(function(m){
const div=document.createElement("div"); appendMsg(m);
div.className="msg "+(m.side==="customer"?"cust":"agent");
div.textContent=(m.side==="customer"?"客户: ":"客服: ")+m.content;
document.getElementById("chatBox").appendChild(div);
}); });
sessions[sid].unread = 0;
renderSessionList(); renderSessionList();
} }
function appendMsg(m){
var div=document.createElement("div");
div.className="msg "+(m.side==="customer"?"cust":"agent");
div.textContent=(m.side==="customer"?"客户: ":"客服: ")+m.content;
document.getElementById("chatBox").appendChild(div);
document.getElementById("chatBox").scrollTop=999999;
}
function renderSessionList(){ function renderSessionList(){
const el=document.getElementById("sessionList"); var el=document.getElementById("sessionList");
el.innerHTML=""; el.innerHTML="";
Object.keys(sessions).forEach(sid=>{ Object.keys(sessions).forEach(function(sid){
const div=document.createElement("div"); var unread = sessions[sid].unread || 0;
var div=document.createElement("div");
div.className="session-item"+(sid==curSid?" active":""); div.className="session-item"+(sid==curSid?" active":"");
div.textContent="对话 #"+sid; div.innerHTML="对话 #"+sid+(unread>0?'<span class="badge">'+unread+'</span>':"");
div.onclick=()=>switchSession(sid); div.onclick=function(){ switchSession(sid); };
el.appendChild(div); el.appendChild(div);
}); });
} }
function poll(){ function poll(){
Object.keys(sessions).forEach(sid=>{ // 先拉所有进行中的会话
fetch("/api/human/poll?sid="+sid+"&after="+sessions[sid].lastId).then(r=>r.json()).then(d=>{ fetch("/api/human/active").then(function(r){return r.json()}).then(function(list){
d.messages.forEach(m=>{ list.forEach(function(s){
sessions[sid].lastId=m.id; if(!sessions[String(s.id)]){
sessions[sid].msgs.push(m); sessions[String(s.id)] = {lastId:0, msgs:[], unread:0};
if(sid==curSid){ }
const div=document.createElement("div"); });
div.className="msg "+(m.side==="customer"?"cust":"agent"); renderSessionList();
div.textContent=(m.side==="customer"?"客户: ":"客服: ")+m.content; }).then(function(){
document.getElementById("chatBox").appendChild(div); Object.keys(sessions).forEach(function(sid){
document.getElementById("chatBox").scrollTop=999999; fetch("/api/human/poll?sid="+parseInt(sid)+"&after="+sessions[sid].lastId)
.then(function(r){return r.json()})
.then(function(d){
if(d.status==="closed"){
delete sessions[sid];
if(sid==curSid){ curSid=0; document.getElementById("chatCard").style.display="none"; }
renderSessionList();
return;
} }
}); d.messages.forEach(function(m){
sessions[sid].lastId=m.id;
sessions[sid].msgs.push(m);
if(m.side==="customer" && sid!=curSid){
sessions[sid].unread=(sessions[sid].unread||0)+1;
renderSessionList();
}
if(sid==curSid) appendMsg(m);
});
})
.catch(function(){});
}); });
}); });
setTimeout(poll, 2000);
} }
poll(); setInterval(poll, 1000);
function sendReply(){ function sendReply(){
const t=document.getElementById("replyText").value.trim(); var t=document.getElementById("replyText").value.trim();
if(!t || !curSid) return; if(!t || !curSid) return;
fetch("/api/human/send",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sid:curSid,side:"agent",content:t})}); fetch("/api/human/send",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sid:parseInt(curSid),side:"agent",content:t})});
document.getElementById("replyText").value=""; document.getElementById("replyText").value="";
document.getElementById("replyText").focus();
} }
function closeSession(){ function closeSession(){
fetch("/api/human/close",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sid:curSid})}); fetch("/api/human/close",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sid:parseInt(curSid)})});
delete sessions[curSid]; delete sessions[curSid];
curSid=0; curSid=0;
document.getElementById("chatCard").style.display="none"; document.getElementById("chatCard").style.display="none";
@@ -163,10 +189,10 @@ document.getElementById("replyText").addEventListener("keydown", function(e){
if(e.key==="Enter"){ e.preventDefault(); sendReply(); } if(e.key==="Enter"){ e.preventDefault(); sendReply(); }
}); });
fetch("/api/get_history").then(r=>r.json()).then(list=>{ fetch("/api/get_history").then(function(r){return r.json()}).then(function(list){
const tb=document.getElementById("tbl"); var tb=document.getElementById("tbl");
list.slice(0,20).forEach(c=>{ list.slice(0,20).forEach(function(c){
tb.innerHTML+=`<tr><td>${c.id}</td><td>${c.title}</td><td>${c.utime||c.ctime}</td></tr>`; tb.innerHTML+="<tr><td>"+c.id+"</td><td>"+c.title+"</td><td>"+(c.utime||c.ctime)+"</td></tr>";
}); });
}); });
</script> </script>
+44 -28
View File
@@ -1202,6 +1202,7 @@
<span><span class="ai-icon">🤖</span><span id="app-title">柏悦物流智能助手</span></span> <span><span class="ai-icon">🤖</span><span id="app-title">柏悦物流智能助手</span></span>
<span class="title-actions"> <span class="title-actions">
<button id="lang-toggle" class="theme-toggle lang-toggle" title="中英文切换(Ctrl+L)">中</button> <button id="lang-toggle" class="theme-toggle lang-toggle" title="中英文切换(Ctrl+L)">中</button>
<button id="font-toggle" class="theme-toggle" title="字体大小">A</button>
<button id="theme-toggle" class="theme-toggle" title="切换深色/浅色">🌙</button> <button id="theme-toggle" class="theme-toggle" title="切换深色/浅色">🌙</button>
</span> </span>
</div> </div>
@@ -1361,6 +1362,27 @@ window.__busy = false; // 生成中状态,用 window 属性避免作用域
} }
})(); })();
// ===== 字体大小切换 =====
(function(){
var KEY = "wuliu-fontsize";
var SIZES = [14, 16, 18, 20];
var idx = 1;
try{ var s = localStorage.getItem(KEY); if(s) idx = parseInt(s); }catch(e){}
var btn = document.getElementById("font-toggle");
function apply(){
document.body.style.fontSize = SIZES[idx] + "px";
if(btn) btn.textContent = SIZES[idx] + "px";
}
apply();
if(btn){
btn.onclick = function(){
idx = (idx + 1) % SIZES.length;
apply();
try{ localStorage.setItem(KEY, idx); }catch(e){}
};
}
})();
// ===== 中英文切换 ===== // ===== 中英文切换 =====
const I18N = { const I18N = {
zh: { zh: {
@@ -1872,62 +1894,56 @@ window.showHuman = async function(){
addMsg("您已在人工客服会话中,请直接提问。", false); addMsg("您已在人工客服会话中,请直接提问。", false);
return; return;
} }
const now = new Date(); var now = new Date();
const day = now.getDay(); var day = now.getDay();
const h = now.getHours(); var h = now.getHours();
const work = (day >= 1 && day <= 5) && (h >= 9 && h < 18); var work = (day >= 1 && day <= 5) && (h >= 9 && h < 19);
if(!work){ if(!work){
addMsg("人工客服工作时间:周一至周五 9:00-18:00\n当前非工作时间,留言后工作时间内会尽快回复。", false); addMsg("人工客服工作时间:周一至周五 9:00-18:00\n当前非工作时间,留言后工作时间内会尽快回复。", false);
return; return;
} }
addMsg("正在为您转接人工客服...", false); addMsg("正在为您转接人工客服...", false);
if(!currentConvId) await ensureConv(); if(!currentConvId){
fetch("/api/human/request",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({conv_id:currentConvId})}) try{ await ensureConv(); }catch(e){}
.then(r=>r.json()).then(d=>{ }
fetch("/api/human/request",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({conv_id:currentConvId||0})})
.then(function(r){return r.json()})
.then(function(d){
if(d.ok){ if(d.ok){
window.humanSid = d.session_id; window.humanSid = d.session_id;
addMsg("您已加入人工排队,请稍候...", false); addMsg("您已加入人工排队,请稍候...", false);
startHumanPoll(); startHumanPoll();
}else{
addMsg("转接失败,请稍后再试。", false);
} }
})
.catch(function(){
addMsg("网络错误,请稍后再试。", false);
}); });
} }
window.humanSid = 0; window.humanSid = 0;
function startHumanPoll(){ function startHumanPoll(){
let last=0; let last=0;
let typingShown=null;
const t=setInterval(()=>{ const t=setInterval(()=>{
if(!window.humanSid){ clearInterval(t); return; } if(!window.humanSid){ clearInterval(t); return; }
fetch("/api/human/poll?sid="+window.humanSid+"&after="+last).then(r=>r.json()).then(d=>{ fetch("/api/human/poll?sid="+window.humanSid+"&after="+last)
.then(r=>r.json())
.then(d=>{
if(d.status==="closed"){ if(d.status==="closed"){
clearInterval(t); clearInterval(t);
window.humanSid=0; window.humanSid=0;
window.__busy=false; window.__busy=false;
if(typeof setBusy==="function") setBusy(false); if(typeof setBusy==="function") setBusy(false);
addMsg("人工客服已结束,您可以继续向 AI 提问。", false); addMsg("人工客服已结束,您可以继续向 AI 提问。", false);
inputDom.focus();
return; return;
} }
if(typingShown){ typingShown.remove(); typingShown=null; }
if(d.typing){
typingShown = addMsg("对方正在输入...", false);
}
d.messages.forEach(m=>{ d.messages.forEach(m=>{
last=m.id; last=m.id;
if(m.side==="agent"){ if(m.side==="agent") addMsg(m.content, false);
addMsg(m.content, false);
}
}); });
// 已读回执:标记最后一条用户消息 })
if(d.messages.some(m=>m.side==="customer" && m.read)){ .catch(()=>{});
const rows=document.querySelectorAll(".msg-row.user-line"); }, 1000);
if(rows.length){
const last=rows[rows.length-1];
const ts=last.querySelector(".msg-time");
if(ts && !ts.textContent.includes("已读")) ts.textContent += " ✓已读";
}
}
});
}, 2000);
} }
function addMsg(content, isUser){ function addMsg(content, isUser){
+3 -5
View File
@@ -1,8 +1,6 @@
@echo off @echo off
cd /d D:\1\wuliu_chat cd /d D:\1\wuliu_chat
set DEEPSEEK_API_KEY=sk-529a6318230641a2b45bc28a716a1697 set DEEPSEEK_API_KEY=sk-529a6318230641a2b45bc28a716a1697
echo 柏悦物流客服系统已启动 echo Starting...
echo 关闭这个黑窗口就停止服务 echo Close this window to stop
echo 浏览器打开 http://127.0.0.1:5000/ start /wait python am.py
python am.py
pause