调整客服自动接待和手动排队规则

This commit is contained in:
songxinnuo
2026-11-04 13:37:26 +08:00
parent efb2e5b4b4
commit ba8a7104b3
22 changed files with 1086 additions and 21 deletions
+40 -4
View File
@@ -1,6 +1,7 @@
var curSid = 0;
var sessions = {};
var drafts = {};
var reassignedNotes = {};
var queueItems = [];
var queueOverdueNotified = new Set();
var accepting = new Set();
@@ -15,6 +16,7 @@ var staffExpired = false;
var staffLoggingOut = false;
var staffPollTimers = [];
var staffCsrf = document.querySelector('meta[name="staff-csrf-token"]').content;
var staffUsername = document.querySelector('meta[name="staff-username"]').content;
var replyInput = document.getElementById("replyText");
SupportUI.initAttachments(document.getElementById('chatBox'), '图片暂时无法访问,请客户重新发送附件。');
@@ -153,7 +155,7 @@ function renderQueue(){
const wait = item.overdue
? '<span class="queue-overdue">排队超时 '+escapeText(item.overdue_minutes)+' 分钟</span>'
: '<span class="queue-wait">已等待 '+escapeText(age)+' 分钟</span>';
return '<div class="queue-item'+(item.overdue ? ' is-overdue' : '')+'"><div class="qt">对话 #'+sid+wait+'<small>'+escapeText(item.ctime)+'</small></div><button type="button" onclick="accept('+sid+')">接入会话 →</button></div>';
return '<div class="queue-item'+(item.overdue ? ' is-overdue' : '')+'"><div class="qt">对话 #'+sid+wait+'<small>'+escapeText(item.group_name || '综合客服')+' · '+escapeText(item.ctime)+'</small></div><button type="button" onclick="accept('+sid+')">接入会话 →</button></div>';
}).join("")
: '<div class="list-empty">'+(term ? "没有匹配的排队会话" : "当前没有排队会话")+'</div>';
}
@@ -198,7 +200,7 @@ async function accept(sid){
loadQueue();
poll();
}catch(error){
document.getElementById("queue").innerHTML = '<div class="list-empty">接入失败,请稍后重试</div>';
document.getElementById("queue").innerHTML = '<div class="list-empty">'+escapeText(error.userMessage || '接入失败,请稍后重试')+'</div>';
}finally{
accepting.delete(sid);
}
@@ -213,7 +215,7 @@ function switchSession(sid){
document.getElementById("chatContent").style.display = "flex";
setChatView(true);
document.getElementById("chatTitle").textContent = "客户 #" + sid;
document.getElementById("chat-subtitle").textContent = "正在接待 · 人工会话";
document.getElementById("chat-subtitle").textContent = "我负责 · "+(sessions[sid].group_name || "综合客服");
document.getElementById("cInfo").textContent = "会话编号:#" + sid;
document.getElementById("chatBox").innerHTML = "";
sessions[sid].msgs.forEach(appendMsg);
@@ -316,11 +318,23 @@ async function poll(){
const list = await adminRequest("/api/human/active");
list.forEach(function(session){
if(!sessions[String(session.id)]) sessions[String(session.id)] = {lastId:0, msgs:[], unread:0};
Object.assign(sessions[String(session.id)], {group_name:session.group_name, owner_username:session.owner_username});
});
await Promise.all(Object.keys(sessions).map(async function(sid){
const session = sessions[sid];
const state = await adminRequest("/api/human/poll?sid="+Number(sid)+"&after="+session.lastId);
if(sessions[sid] !== session) return;
if(state.status === 'waiting' || (state.owner_username && state.owner_username !== staffUsername)){
if(!Object.prototype.hasOwnProperty.call(drafts,sid)) drafts[sid] = '';
if(sid === curSid){
drafts[sid] = replyInput.value;
reassignedNotes[sid] = document.getElementById('noteInput').value;
}else if(session.noteDraft){ reassignedNotes[sid] = session.noteDraft; }
delete sessions[sid];
if(sid === curSid) showEmptySession();
renderRecoveredDrafts();
return;
}
if(state.status !== "active"){
delete sessions[sid];
delete drafts[sid];
@@ -339,6 +353,7 @@ async function poll(){
}));
document.getElementById("sync-status").hidden = true;
renderList();
renderRecoveredDrafts();
}catch(error){
document.getElementById("sync-status").hidden = false;
}finally{
@@ -346,6 +361,15 @@ async function poll(){
}
}
function renderRecoveredDrafts(){
const ids = Object.keys(drafts).filter(function(sid){ return !sessions[sid] && (drafts[sid] || reassignedNotes[sid]); });
document.getElementById('routing-draft-recovery').hidden = ids.length === 0;
document.getElementById('routing-retired-drafts').innerHTML = ids.map(function(sid){
return '<label>会话 #'+Number(sid)+' · 未发送草稿<textarea readonly rows="3">'+escapeText(drafts[sid] || '')+'</textarea></label>'+
(reassignedNotes[sid] ? '<label>备注草稿<textarea readonly rows="3">'+escapeText(reassignedNotes[sid])+'</textarea></label>' : '');
}).join('');
}
async function sendReply(){
if(!curSid || sendingSid || closingSid) return;
const sid = curSid;
@@ -454,7 +478,7 @@ document.getElementById("tools-from-list").onclick = function(){ toolsOpener = "
document.getElementById("tools-close").onclick = function(){
setToolsOpen(false);
const fromList = toolsOpener === "tools-from-list" && !document.body.classList.contains("chat-view") && window.matchMedia("(max-width: 720px)").matches;
document.getElementById(["ticket-inbox-toggle", "ticket-overdue-toggle"].includes(toolsOpener) ? toolsOpener : fromList ? "tools-from-list" : "tools-toggle").focus();
document.getElementById(["ticket-inbox-toggle", "ticket-overdue-toggle", "routing-open"].includes(toolsOpener) ? toolsOpener : fromList ? "tools-from-list" : "tools-toggle").focus();
};
document.getElementById("tools-backdrop").onclick = document.getElementById("tools-close").onclick;
document.addEventListener("keydown", function(event){
@@ -494,3 +518,15 @@ var ticketDesk = window.AdminTickets.create({request:adminRequest, escape:escape
loadTickets();
poll();
staffPollTimers = [setInterval(loadQueue, 3000), setInterval(loadTickets, 5000), setInterval(poll, 1000)];
if(window.AdminRouting){
var routingDesk = window.AdminRouting.create({username:staffUsername,request:adminRequest,
changed:function(){ loadQueue(); poll(); }});
routingDesk.refresh();
staffPollTimers.push(setInterval(routingDesk.heartbeat, 20000));
document.getElementById('routing-open').onclick = function(){
toolsOpener = 'routing-open'; setToolsOpen(true);
document.getElementById('routing-title').scrollIntoView({block:'start'});
document.getElementById('routing-refresh').focus({preventScroll:true});
routingDesk.refresh();
};
}