2 Commits
4 changed files with 92 additions and 11 deletions
+30
View File
@@ -269,6 +269,21 @@ def backup_db():
print(f"[数据库备份失败] {e}") print(f"[数据库备份失败] {e}")
def cleanup_uploads():
"""删除 uploads/ 里 7 天前的图片"""
try:
udir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "uploads")
if not os.path.isdir(udir):
return
cutoff = time.time() - 7 * 86400
for f in os.listdir(udir):
fp = os.path.join(udir, f)
if os.path.isfile(fp) and os.path.getmtime(fp) < cutoff:
os.remove(fp)
except Exception as e:
print(f"[清理上传文件失败] {e}")
def cleanup_old_conversations(): def cleanup_old_conversations():
"""自动清理 30 天前的旧对话,防止数据库无限膨胀""" """自动清理 30 天前的旧对话,防止数据库无限膨胀"""
try: try:
@@ -290,6 +305,7 @@ def cleanup_old_conversations():
backup_db() backup_db()
cleanup_uploads()
# 自动清理已关闭,不删旧对话 # 自动清理已关闭,不删旧对话
@@ -919,6 +935,9 @@ def stream_chat():
conv_id = request.args.get("conv_id", type=int) conv_id = request.args.get("conv_id", type=int)
if not msg: if not msg:
return Response(sse({"e": "问题不能为空"}), mimetype="text/event-stream") return Response(sse({"e": "问题不能为空"}), mimetype="text/event-stream")
# 纯图片消息:不调AI
if "[图片]" in msg and len(msg) < 300:
return Response(sse({"t": "收到您的图片,请描述一下您的问题。"}), mimetype="text/event-stream")
# 运单号:直接给轨迹卡片,不让模型编 # 运单号:直接给轨迹卡片,不让模型编
track_no = extract_tracking_no(msg) track_no = extract_tracking_no(msg)
@@ -1437,8 +1456,19 @@ def uploaded_file(name):
def index(): def index():
return render_template("index.html") return render_template("index.html")
@app.route("/admin/login", methods=["POST"])
def admin_login():
pwd = request.json.get("password", "")
if pwd == "123":
return jsonify({"ok": True})
return jsonify({"ok": False}), 401
@app.route("/admin") @app.route("/admin")
def admin(): def admin():
return render_template("admin_login.html")
@app.route("/admin/dashboard")
def admin_dash():
return render_template("admin.html") return render_template("admin.html")
+6 -1
View File
@@ -149,7 +149,12 @@ function switchSession(sid){
function appendMsg(m){ function appendMsg(m){
var div=document.createElement("div"); var div=document.createElement("div");
div.className="msg "+(m.side==="customer"?"cust":"agent"); div.className="msg "+(m.side==="customer"?"cust":"agent");
div.innerHTML='<div><div class="bubble">'+m.content.replace(/</g,"&lt;")+'</div><div class="meta">'+(m.side==="customer"?"客户":"客服")+'</div></div>'; var c = m.content.replace(/</g,"&lt;");
var imgMatch = c.match(/\[图片\](\/uploads\/\S+)/);
if(imgMatch){
c = c.replace(imgMatch[0], '<img src="'+imgMatch[1]+'" style="max-width:200px;border-radius:6px;display:block;">');
}
div.innerHTML='<div><div class="bubble">'+c+'</div><div class="meta">'+(m.side==="customer"?"客户":"客服")+'</div></div>';
document.getElementById("chatBox").appendChild(div); document.getElementById("chatBox").appendChild(div);
document.getElementById("chatBox").scrollTop=999999; document.getElementById("chatBox").scrollTop=999999;
} }
+31
View File
@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>登录 - 客服后台</title>
<style>
body{font-family:sans-serif;background:#f0f2f5;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;}
.box{background:#fff;padding:40px;border-radius:12px;box-shadow:0 2px 12px rgba(0,0,0,0.1);width:320px;}
h2{text-align:center;color:#18479c;margin-bottom:24px;}
input{width:100%;padding:10px;border:1px solid #ddd;border-radius:6px;margin-bottom:12px;font-size:14px;box-sizing:border-box;}
button{width:100%;padding:10px;background:#2978e8;color:#fff;border:none;border-radius:6px;font-size:14px;cursor:pointer;}
.err{color:#dc2626;font-size:13px;text-align:center;display:none;}
</style>
</head>
<body>
<div class="box">
<h2>🔒 后台登录</h2>
<input type="password" id="pwd" placeholder="请输入密码">
<p class="err" id="err">密码错误</p>
<button onclick="login()">登录</button>
</div>
<script>
function login(){
var p=document.getElementById("pwd").value;
fetch("/admin/login",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({password:p})})
.then(function(r){if(r.ok){location.href="/admin/dashboard"}else{document.getElementById("err").style.display="block"}});
}
document.getElementById("pwd").addEventListener("keydown",function(e){if(e.key==="Enter")login()});
</script>
</body>
</html>
+25 -10
View File
@@ -1321,9 +1321,15 @@ uploadInput.onchange = function(){
.then(data=>{ .then(data=>{
uploadBtn.textContent = "📎"; uploadBtn.textContent = "📎";
if(data.ok){ if(data.ok){
let text = "[附件] " + data.url; window.__pendingImg = data.url;
if(data.is_image) text = "[图片] " + data.url; var old = document.getElementById("imgPreview");
inputDom.value = text; if(old) old.remove();
var prev = document.createElement("div");
prev.id = "imgPreview";
prev.style.cssText="margin:0 0 8px 50px;";
prev.innerHTML = '<img src="'+data.url+'" style="max-height:60px;border-radius:6px;border:1px solid #ddd;">';
var area = document.querySelector(".input-area");
area.insertBefore(prev, area.firstChild);
inputDom.focus(); inputDom.focus();
}else{ }else{
alert(data.error || "上传失败"); alert(data.error || "上传失败");
@@ -1961,7 +1967,8 @@ function addMsg(content, isUser){
const userMsg = document.createElement('div'); const userMsg = document.createElement('div');
userMsg.className = "user-message"; userMsg.className = "user-message";
if(imgUrl){ if(imgUrl){
userMsg.innerHTML = '<img src="' + imgUrl + '" style="max-width:220px;border-radius:10px;display:block;padding:0;background:transparent;cursor:zoom-in;" onclick="window.open(this.src)">'; var txt = content.replace(/\[图片\]\/uploads\/\S+\.(?:jpg|jpeg|png|gif|webp)/i, "").trim();
userMsg.innerHTML = '<img src="' + imgUrl + '" style="max-width:220px;border-radius:10px;display:block;background:transparent;cursor:zoom-in;" onclick="window.open(this.src)">' + (txt ? '<div style="margin-top:6px;">'+txt+'</div>' : '');
}else{ }else{
userMsg.innerText = content; userMsg.innerText = content;
} }
@@ -2121,17 +2128,21 @@ function renderFollowups(bubble){
async function doSend(retryText){ async function doSend(retryText){
const isRetry = typeof retryText === "string"; const isRetry = typeof retryText === "string";
const sendWord = (isRetry ? retryText : inputDom.value).trim(); const sendWord = (isRetry ? retryText : inputDom.value).trim();
if(!sendWord){ var imgUrl = window.__pendingImg || "";
if(!sendWord && !imgUrl){
inputDom.focus(); inputDom.focus();
return; return;
} }
// 人工客服模式:直接发给客服,不走AI // 人工客服模式:直接发给客服,不走AI
if(window.humanSid){ if(window.humanSid){
if(!isRetry) addMsg(sendWord, true); var content = sendWord;
if(imgUrl) content += (content ? " " : "") + "[图片]" + imgUrl;
if(!isRetry) addMsg(content, true);
inputDom.value = ""; inputDom.value = "";
window.__pendingImg = "";
var p = document.getElementById("imgPreview"); if(p) p.remove();
inputDom.focus(); inputDom.focus();
fetch("/api/human/send",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sid:window.humanSid,side:"customer",content:sendWord})}); fetch("/api/human/send",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sid:window.humanSid,side:"customer",content:content})});
return; return;
} }
@@ -2139,8 +2150,12 @@ async function doSend(retryText){
if(cancelActive){ cancelActive(); cancelActive = null; } if(cancelActive){ cancelActive(); cancelActive = null; }
if(!isRetry){ if(!isRetry){
addMsg(sendWord, true); var fullContent = sendWord;
if(imgUrl) fullContent += (fullContent ? " " : "") + "[图片]" + imgUrl;
addMsg(fullContent, true);
inputDom.value = ""; inputDom.value = "";
window.__pendingImg = "";
var p = document.getElementById("imgPreview"); if(p) p.remove();
inputDom.focus(); inputDom.focus();
} }
@@ -2294,7 +2309,7 @@ async function doSend(retryText){
fetch("/stream_chat", { fetch("/stream_chat", {
method: "POST", method: "POST",
headers: {"Content-Type": "application/json"}, headers: {"Content-Type": "application/json"},
body: JSON.stringify({msg: sendWord, conv_id: currentConvId}), body: JSON.stringify({msg: fullContent, conv_id: currentConvId}),
signal: controller.signal signal: controller.signal
}).then(function(res){ }).then(function(res){
if(!res.ok){ throw new Error("HTTP " + res.status); } if(!res.ok){ throw new Error("HTTP " + res.status); }