加字体大小设置,修复人工客服轮询和限流问题
This commit is contained in:
@@ -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 额度
|
||||||
|
|||||||
@@ -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())
|
||||||
@@ -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')
|
||||||
+76
-50
@@ -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
@@ -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){
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
Reference in New Issue
Block a user