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

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
def rate_limit():
p = request.path
if p.startswith("/static"):
if p.startswith("/static") or p.startswith("/api/human/"):
return None
ip = request.remote_addr or "unknown"
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")
def admin_stats():
conn = sqlite3.connect(DB)
@@ -1309,6 +1305,13 @@ def human_queue():
conn.close()
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"])
def human_accept():
sid = request.json.get("sid")
@@ -1434,6 +1437,11 @@ def uploaded_file(name):
def index():
return render_template("index.html")
@app.route("/admin")
def admin():
return render_template("admin.html")
if __name__ == "__main__":
# 0.0.0.0 = 接受局域网内所有设备访问(手机、其他电脑);
# 只在演示时开启,演示完建议改回 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}
+81 -55
View File
@@ -62,97 +62,123 @@ th,td{padding:6px 10px;border-bottom:1px solid #eee;text-align:left;}
<a href="/">← 返回客服页</a>
<script>
let curSid = 0;
let lastId = 0;
const sessions = {}; // sid -> {lastId, msgs:[]}
var curSid = 0;
var sessions = {};
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;
s3.textContent=d.today_conv; s4.textContent=d.good; s5.textContent=d.bad;
});
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
? 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]){
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){
fetch("/api/human/accept",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sid})}).then(()=>{
sessions[sid] = {lastId:0};
sid = String(sid);
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);
});
}
function switchSession(sid){
curSid = sid;
lastId = sessions[sid].lastId;
curSid = String(sid);
document.getElementById("chatCard").style.display="block";
document.getElementById("chatTitle").textContent="对话 #"+sid;
// 渲染已有消息
document.getElementById("chatBox").innerHTML = "";
sessions[sid].msgs.forEach(m=>{
const div=document.createElement("div");
div.className="msg "+(m.side==="customer"?"cust":"agent");
div.textContent=(m.side==="customer"?"客户: ":"客服: ")+m.content;
document.getElementById("chatBox").appendChild(div);
sessions[sid].msgs.forEach(function(m){
appendMsg(m);
});
sessions[sid].unread = 0;
renderSessionList();
}
function renderSessionList(){
const el=document.getElementById("sessionList");
el.innerHTML="";
Object.keys(sessions).forEach(sid=>{
const div=document.createElement("div");
div.className="session-item"+(sid==curSid?" active":"");
div.textContent="对话 #"+sid;
div.onclick=()=>switchSession(sid);
el.appendChild(div);
});
}
function poll(){
Object.keys(sessions).forEach(sid=>{
fetch("/api/human/poll?sid="+sid+"&after="+sessions[sid].lastId).then(r=>r.json()).then(d=>{
d.messages.forEach(m=>{
sessions[sid].lastId=m.id;
sessions[sid].msgs.push(m);
if(sid==curSid){
const div=document.createElement("div");
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(){
var el=document.getElementById("sessionList");
el.innerHTML="";
Object.keys(sessions).forEach(function(sid){
var unread = sessions[sid].unread || 0;
var div=document.createElement("div");
div.className="session-item"+(sid==curSid?" active":"");
div.innerHTML="对话 #"+sid+(unread>0?'<span class="badge">'+unread+'</span>':"");
div.onclick=function(){ switchSession(sid); };
el.appendChild(div);
});
});
});
setTimeout(poll, 2000);
}
poll();
function poll(){
// 先拉所有进行中的会话
fetch("/api/human/active").then(function(r){return r.json()}).then(function(list){
list.forEach(function(s){
if(!sessions[String(s.id)]){
sessions[String(s.id)] = {lastId:0, msgs:[], unread:0};
}
});
renderSessionList();
}).then(function(){
Object.keys(sessions).forEach(function(sid){
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(){});
});
});
}
setInterval(poll, 1000);
function sendReply(){
const t=document.getElementById("replyText").value.trim();
var t=document.getElementById("replyText").value.trim();
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").focus();
}
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];
curSid=0;
document.getElementById("chatCard").style.display="none";
@@ -163,10 +189,10 @@ document.getElementById("replyText").addEventListener("keydown", function(e){
if(e.key==="Enter"){ e.preventDefault(); sendReply(); }
});
fetch("/api/get_history").then(r=>r.json()).then(list=>{
const tb=document.getElementById("tbl");
list.slice(0,20).forEach(c=>{
tb.innerHTML+=`<tr><td>${c.id}</td><td>${c.title}</td><td>${c.utime||c.ctime}</td></tr>`;
fetch("/api/get_history").then(function(r){return r.json()}).then(function(list){
var tb=document.getElementById("tbl");
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>";
});
});
</script>
+44 -28
View File
@@ -1202,6 +1202,7 @@
<span><span class="ai-icon">🤖</span><span id="app-title">柏悦物流智能助手</span></span>
<span class="title-actions">
<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>
</span>
</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 = {
zh: {
@@ -1872,62 +1894,56 @@ window.showHuman = async function(){
addMsg("您已在人工客服会话中,请直接提问。", false);
return;
}
const now = new Date();
const day = now.getDay();
const h = now.getHours();
const work = (day >= 1 && day <= 5) && (h >= 9 && h < 18);
var now = new Date();
var day = now.getDay();
var h = now.getHours();
var work = (day >= 1 && day <= 5) && (h >= 9 && h < 19);
if(!work){
addMsg("人工客服工作时间:周一至周五 9:00-18:00\n当前非工作时间,留言后工作时间内会尽快回复。", false);
return;
}
addMsg("正在为您转接人工客服...", false);
if(!currentConvId) await ensureConv();
fetch("/api/human/request",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({conv_id:currentConvId})})
.then(r=>r.json()).then(d=>{
if(!currentConvId){
try{ await ensureConv(); }catch(e){}
}
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){
window.humanSid = d.session_id;
addMsg("您已加入人工排队,请稍候...", false);
startHumanPoll();
}else{
addMsg("转接失败,请稍后再试。", false);
}
})
.catch(function(){
addMsg("网络错误,请稍后再试。", false);
});
}
window.humanSid = 0;
function startHumanPoll(){
let last=0;
let typingShown=null;
const t=setInterval(()=>{
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"){
clearInterval(t);
window.humanSid=0;
window.__busy=false;
if(typeof setBusy==="function") setBusy(false);
addMsg("人工客服已结束,您可以继续向 AI 提问。", false);
inputDom.focus();
return;
}
if(typingShown){ typingShown.remove(); typingShown=null; }
if(d.typing){
typingShown = addMsg("对方正在输入...", false);
}
d.messages.forEach(m=>{
last=m.id;
if(m.side==="agent"){
addMsg(m.content, false);
}
if(m.side==="agent") addMsg(m.content, false);
});
// 已读回执:标记最后一条用户消息
if(d.messages.some(m=>m.side==="customer" && m.read)){
const rows=document.querySelectorAll(".msg-row.user-line");
if(rows.length){
const last=rows[rows.length-1];
const ts=last.querySelector(".msg-time");
if(ts && !ts.textContent.includes("已读")) ts.textContent += " ✓已读";
}
}
});
}, 2000);
})
.catch(()=>{});
}, 1000);
}
function addMsg(content, isUser){
+3 -5
View File
@@ -1,8 +1,6 @@
@echo off
cd /d D:\1\wuliu_chat
set DEEPSEEK_API_KEY=sk-529a6318230641a2b45bc28a716a1697
echo 柏悦物流客服系统已启动
echo 关闭这个黑窗口就停止服务
echo 浏览器打开 http://127.0.0.1:5000/
python am.py
pause
echo Starting...
echo Close this window to stop
start /wait python am.py