diff --git a/add_agent.py b/add_agent.py new file mode 100644 index 0000000..bed5eb6 --- /dev/null +++ b/add_agent.py @@ -0,0 +1,13 @@ +import sqlite3 +c = sqlite3.connect('history.db') +c.execute('''CREATE TABLE IF NOT EXISTS agents ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE, + password TEXT, + name TEXT +)''') +c.execute("INSERT OR IGNORE INTO agents(username,password,name) VALUES('admin','123','管理员')") +c.execute("INSERT OR IGNORE INTO agents(username,password,name) VALUES('agent1','123','客服小王')") +c.execute("INSERT OR IGNORE INTO agents(username,password,name) VALUES('agent2','123','客服小李')") +c.commit() +print("ok") diff --git a/add_orders.py b/add_orders.py new file mode 100644 index 0000000..b860c93 --- /dev/null +++ b/add_orders.py @@ -0,0 +1,11 @@ +import sqlite3 +c = sqlite3.connect('history.db') +c.execute('''CREATE TABLE IF NOT EXISTS orders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + phone TEXT, + track_no TEXT, + status TEXT DEFAULT "运输中", + created TEXT +)''') +c.commit() +print("ok") diff --git a/add_test.py b/add_test.py new file mode 100644 index 0000000..6cb255a --- /dev/null +++ b/add_test.py @@ -0,0 +1,7 @@ +import sqlite3 +c = sqlite3.connect('history.db') +c.execute("INSERT INTO orders(phone, track_no, status, created) VALUES('13800138000','YT20261028001','运输中','2026-10-28')") +c.execute("INSERT INTO orders(phone, track_no, status, created) VALUES('13800138000','YT20261028002','派送中','2026-10-27')") +c.execute("INSERT INTO orders(phone, track_no, status, created) VALUES('13900139000','YT20261028003','已签收','2026-10-26')") +c.commit() +print("ok") diff --git a/add_ticket.py b/add_ticket.py new file mode 100644 index 0000000..9fee2e4 --- /dev/null +++ b/add_ticket.py @@ -0,0 +1,11 @@ +import sqlite3 +c = sqlite3.connect('history.db') +c.execute('''CREATE TABLE IF NOT EXISTS tickets ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + content TEXT, + phone TEXT, + ctime TEXT, + status TEXT DEFAULT "pending" +)''') +c.commit() +print("ok") diff --git a/am.py b/am.py index 7d6be69..0acbab0 100644 --- a/am.py +++ b/am.py @@ -227,6 +227,22 @@ def init_db(): if "read_at" not in hm_cols: cur.execute("ALTER TABLE human_messages ADD COLUMN read_at TEXT") + # 补 note 列(会话备注) + hs_cols = [r[1] for r in cur.execute("PRAGMA table_info(human_sessions)").fetchall()] + if "note" not in hs_cols: + cur.execute("ALTER TABLE human_sessions ADD COLUMN note TEXT DEFAULT ''") + + # 满意度评价表 + cur.execute(''' + CREATE TABLE IF NOT EXISTS ratings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + sid INTEGER NOT NULL, + stars INTEGER NOT NULL, + comment TEXT DEFAULT '', + ctime TEXT NOT NULL + ) + ''') + # 老数据只有标题,补一条 user 消息,点开还能接着聊 old = cur.execute( "SELECT id, title FROM chat_history " @@ -937,7 +953,10 @@ def stream_chat(): return Response(sse({"e": "问题不能为空"}), mimetype="text/event-stream") # 纯图片消息:不调AI if "[图片]" in msg and len(msg) < 300: - return Response(sse({"t": "收到您的图片,请描述一下您的问题。"}), mimetype="text/event-stream") + def _img_reply(): + yield sse({"t": "收到您的图片,请描述一下您的问题。"}) + yield sse({"d": 1}) + return Response(stream_with_context(_img_reply()), mimetype="text/event-stream") # 运单号:直接给轨迹卡片,不让模型编 track_no = extract_tracking_no(msg) @@ -1229,6 +1248,8 @@ def stream_chat(): headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"} ) +WELCOME_TEXT = "您好!欢迎咨询柏悦物流智能客服,请问有什么可以帮您?输入运单号可查轨迹,或点击下方快捷选项。" + @app.route("/api/new_chat", methods=["POST"]) def new_chat(): now = now_str() @@ -1237,9 +1258,11 @@ def new_chat(): cur.execute("INSERT INTO chat_history(title, ctime, utime) VALUES (?,?,?)", ("新对话", now, now)) conv_id = cur.lastrowid + cur.execute("INSERT INTO messages(conv_id, role, content, ctime) VALUES (?,?,?,?)", + (conv_id, "assistant", WELCOME_TEXT, now)) conn.commit() conn.close() - return jsonify({"id": conv_id}) + return jsonify({"id": conv_id, "welcome": WELCOME_TEXT}) @app.route("/api/get_history") @@ -1264,9 +1287,12 @@ def admin_stats(): bad = conn.execute("SELECT COUNT(*) FROM feedback WHERE vote=-1").fetchone()[0] today = datetime.now().strftime("%Y-%m-%d") today_conv = conn.execute("SELECT COUNT(*) FROM chat_history WHERE date(ctime)=date(?)", (today,)).fetchone()[0] + rating_avg = conn.execute("SELECT AVG(stars) FROM ratings").fetchone()[0] or 0 + rating_count = conn.execute("SELECT COUNT(*) FROM ratings").fetchone()[0] conn.close() return jsonify({"total_conv": total_conv, "total_msg": total_msg, - "good": good, "bad": bad, "today_conv": today_conv}) + "good": good, "bad": bad, "today_conv": today_conv, + "rating_avg": round(rating_avg, 1), "rating_count": rating_count}) # ===== 人工客服 ===== @@ -1275,6 +1301,11 @@ def human_request(): conv_id = request.json.get("conv_id") if not conv_id: return jsonify({"ok": False, "error": "no conv"}), 400 + # 工作时间 8:00-22:00,其他时间不接人工 + _h = datetime.now().hour + if _h < 8 or _h >= 22: + return jsonify({"ok": False, "closed": True, + "msg": "人工客服工作时间为每天8:00-22:00,当前为非工作时间,请留言"}), 200 now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") conn = sqlite3.connect(DB) row = conn.execute("SELECT id FROM human_sessions WHERE conv_id=? AND status!='closed' ORDER BY id DESC LIMIT 1", (conv_id,)).fetchone() @@ -1293,13 +1324,14 @@ def human_poll(): after = request.args.get("after", 0, type=int) conn = sqlite3.connect(DB) msgs = conn.execute("SELECT id, side, content, ctime, read_at FROM human_messages WHERE session_id=? AND id>? ORDER BY id", (sid, after)).fetchall() - st = conn.execute("SELECT status FROM human_sessions WHERE id=?", (sid,)).fetchone() + st = conn.execute("SELECT status, COALESCE(note,'') FROM human_sessions WHERE id=?", (sid,)).fetchone() conn.close() is_typing = False ts = typing_map.get(str(sid)) if ts and time.time() - ts < 5: is_typing = True return jsonify({"status": st[0] if st else "closed", + "note": st[1] if st else "", "typing": is_typing, "messages": [{"id": m[0], "side": m[1], "content": m[2], "ctime": m[3], "read": bool(m[4])} for m in msgs]}) @@ -1317,6 +1349,20 @@ def human_send(): conn.close() return jsonify({"ok": True}) +@app.route("/api/human/recall", methods=["POST"]) +def human_recall(): + data = request.json or {} + mid = data.get("mid") + conn = sqlite3.connect(DB) + row = conn.execute("SELECT session_id, side FROM human_messages WHERE id=?", (mid,)).fetchone() + if row: + sid, side = row + conn.execute("UPDATE human_messages SET content='', side='system' WHERE id=?", (mid,)) + conn.execute("INSERT INTO human_messages(session_id, side, content, ctime) VALUES(?,?,?,?)", + (sid, "system", "该消息已撤回", datetime.now().strftime("%Y-%m-%d %H:%M:%S"))) + conn.commit(); conn.close() + return jsonify({"ok": True}) + @app.route("/api/human/queue") def human_queue(): conn = sqlite3.connect(DB) @@ -1349,6 +1395,50 @@ def human_close(): conn.close() return jsonify({"ok": True}) +@app.route("/api/human/transfer", methods=["POST"]) +def human_transfer(): + sid = request.json.get("sid") + conn = sqlite3.connect(DB) + conn.execute("UPDATE human_sessions SET status='waiting' WHERE id=?", (sid,)) + conn.execute("INSERT INTO human_messages(session_id, side, content, ctime) VALUES(?,?,?,?)", + (sid, "system", "客服已转接,请其他客服接入", datetime.now().strftime("%Y-%m-%d %H:%M:%S"))) + conn.commit() + conn.close() + return jsonify({"ok": True}) + +@app.route("/api/agents/list") +def agents_list(): + conn = sqlite3.connect(DB) + rows = conn.execute("SELECT id, username, name FROM agents ORDER BY id").fetchall() + conn.close() + return jsonify([{"id": r[0], "username": r[1], "name": r[2]} for r in rows]) + +@app.route("/api/rate", methods=["POST"]) +def rate_chat(): + data = request.json or {} + sid = data.get("sid") + stars = data.get("stars", 0) + comment = data.get("comment", "") + if not sid or not isinstance(stars, int) or stars < 1 or stars > 5: + return jsonify({"ok": False}), 400 + conn = sqlite3.connect(DB) + conn.execute("INSERT INTO ratings(sid, stars, comment, ctime) VALUES(?,?,?,?)", + (sid, stars, comment, datetime.now().strftime("%Y-%m-%d %H:%M:%S"))) + conn.commit() + conn.close() + return jsonify({"ok": True}) + +@app.route("/api/human/note", methods=["POST"]) +def human_note(): + data = request.json or {} + sid = data.get("sid") + note = data.get("note", "") + conn = sqlite3.connect(DB) + conn.execute("UPDATE human_sessions SET note=? WHERE id=?", (note, sid)) + conn.commit() + conn.close() + return jsonify({"ok": True}) + # 客服正在输入(简单内存标记) typing_map = {} @app.route("/api/human/typing", methods=["POST"]) @@ -1499,17 +1589,43 @@ def export_chat(): conn = sqlite3.connect(DB) rows = conn.execute("SELECT h.title, m.role, m.content, m.ctime FROM messages m JOIN chat_history h ON m.conv_id=h.id ORDER BY m.id").fetchall() conn.close() - lines = ["标题,角色,内容,时间"] + from openpyxl import Workbook + from openpyxl.styles import Font, PatternFill, Alignment + wb = Workbook() + ws = wb.active + ws.title = "聊天记录" + headers = ["对话标题", "角色", "内容", "时间"] + ws.append(headers) + header_fill = PatternFill(start_color="2978e8", end_color="2978e8", fill_type="solid") + for cell in ws[1]: + cell.font = Font(bold=True, color="FFFFFF") + cell.fill = header_fill + cell.alignment = Alignment(horizontal="center") + role_map = {"user": "客户", "assistant": "客服", "system": "系统"} for r in rows: - safe = (r[2] or "").replace(",", ",").replace("\n", " ") - lines.append(f"{r[0]},{r[1]},{safe},{r[3]}") - import csv, io - buf = io.StringIO() - w = csv.writer(buf) - w.writerow(["标题","角色","内容","时间"]) - for r in rows: - w.writerow([r[0], r[1], (r[2] or ""), r[3]]) - return Response(buf.getvalue(), mimetype="text/csv", headers={"Content-Disposition":"attachment;filename=chat_export.csv"}) + ws.append([r[0], role_map.get(r[1], r[1]), (r[2] or ""), r[3]]) + ws.column_dimensions["A"].width = 20 + ws.column_dimensions["B"].width = 8 + ws.column_dimensions["C"].width = 80 + ws.column_dimensions["D"].width = 20 + import io + buf = io.BytesIO() + wb.save(buf) + buf.seek(0) + return Response(buf.getvalue(), mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + headers={"Content-Disposition": "attachment;filename=chat_export.xlsx"}) + +@app.route("/api/search_chat") +def search_chat(): + kw = request.args.get("q", "").strip() + if not kw: + return jsonify({"results": []}) + conn = sqlite3.connect(DB) + rows = conn.execute("""SELECT h.title, m.role, m.content, m.ctime FROM messages m + JOIN chat_history h ON m.conv_id=h.id + WHERE m.content LIKE ? ORDER BY m.id DESC LIMIT 50""", ("%"+kw+"%",)).fetchall() + conn.close() + return jsonify([{"title":r[0],"role":r[1],"content":r[2],"ctime":r[3]} for r in rows]) @app.route("/admin") def admin(): diff --git a/check.py b/check.py index d4106e5..48168a9 100644 --- a/check.py +++ b/check.py @@ -1,5 +1,4 @@ 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()) +print("messages:", [d[1] for d in c.execute("PRAGMA table_info(messages)").fetchall()]) +print("chat_history:", [d[1] for d in c.execute("PRAGMA table_info(chat_history)").fetchall()]) diff --git a/fix_user.py b/fix_user.py new file mode 100644 index 0000000..5c40284 --- /dev/null +++ b/fix_user.py @@ -0,0 +1,6 @@ +import sqlite3 +c = sqlite3.connect('history.db') +for u in ['123','456','789']: + c.execute("INSERT OR REPLACE INTO agents(username,password,name) VALUES(?,?,?)", (u,'123',u)) +c.commit() +print("ok") diff --git a/t.json b/t.json index 6347d41..db2be74 100644 --- a/t.json +++ b/t.json @@ -1 +1 @@ -{"conv_id":999} +{"msg":"[图片]/uploads/test.jpg","conv_id":1} diff --git a/templates/admin.html b/templates/admin.html index 69245b4..177ad55 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -64,12 +64,25 @@ body{font-family:-apple-system,"Microsoft YaHei",sans-serif;background:#f0f2f5;h 今日 - 👍 - 👎 - + ⭐ - + +