- 拆分应用工厂、业务服务与路由,保留原有 HTTP/SSE 和存储入口 - 分离页面资源,重排客服工作台并完善响应式导航、草稿和附件交互 - 整理维护脚本、请求示例和贡献指南,移除启动脚本中的密钥配置 - 上传文件改为仅本地保留,补充隔离的后端与前端回归验证 验证:46 项 Python 测试通过,含 14 个已有子测试;12 个 Node 前端场景通过。
103 lines
3.2 KiB
Python
103 lines
3.2 KiB
Python
"""Human conversation lifecycle; typing state is supplied by the app."""
|
|
|
|
import time
|
|
from datetime import datetime
|
|
|
|
from .db import connect
|
|
|
|
|
|
def human_request(data):
|
|
conv_id = data.get('conv_id')
|
|
if not conv_id:
|
|
return ({'ok': False, 'error': 'no conv'}, 400)
|
|
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
conn = connect()
|
|
row = conn.execute("SELECT id FROM human_sessions WHERE conv_id=? AND status!='closed' ORDER BY id DESC LIMIT 1", (conv_id,)).fetchone()
|
|
if row:
|
|
sid = row[0]
|
|
else:
|
|
cur = conn.execute('INSERT INTO human_sessions(conv_id, status, ctime) VALUES(?,?,?)', (conv_id, 'waiting', now))
|
|
sid = cur.lastrowid
|
|
conn.commit()
|
|
conn.close()
|
|
return {'ok': True, 'session_id': sid}
|
|
|
|
|
|
def human_poll(sid, after, typing_map):
|
|
conn = connect()
|
|
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()
|
|
conn.close()
|
|
is_typing = False
|
|
ts = typing_map.get(str(sid))
|
|
if ts and time.time() - ts < 5:
|
|
is_typing = True
|
|
return {'status': st[0] if st else 'closed', 'typing': is_typing, 'messages': [{'id': m[0], 'side': m[1], 'content': m[2], 'ctime': m[3], 'read': bool(m[4])} for m in msgs]}
|
|
|
|
|
|
def human_send(data):
|
|
sid = data.get('sid')
|
|
side = data.get('side', 'customer')
|
|
content = (data.get('content') or '').strip()
|
|
if not sid or not content:
|
|
return ({'ok': False}, 400)
|
|
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
conn = connect()
|
|
conn.execute('INSERT INTO human_messages(session_id, side, content, ctime) VALUES(?,?,?,?)', (sid, side, content, now))
|
|
conn.commit()
|
|
conn.close()
|
|
return {'ok': True}
|
|
|
|
|
|
def human_queue():
|
|
conn = connect()
|
|
rows = conn.execute("SELECT id, conv_id, status, ctime FROM human_sessions WHERE status='waiting' ORDER BY id").fetchall()
|
|
conn.close()
|
|
return [{'id': r[0], 'conv_id': r[1], 'ctime': r[3]} for r in rows]
|
|
|
|
|
|
def human_active():
|
|
conn = connect()
|
|
rows = conn.execute("SELECT id, conv_id FROM human_sessions WHERE status='active' ORDER BY id").fetchall()
|
|
conn.close()
|
|
return [{'id': r[0], 'conv_id': r[1]} for r in rows]
|
|
|
|
|
|
def human_accept(data):
|
|
sid = data.get('sid')
|
|
conn = connect()
|
|
conn.execute("UPDATE human_sessions SET status='active' WHERE id=?", (sid,))
|
|
conn.commit()
|
|
conn.close()
|
|
return {'ok': True}
|
|
|
|
|
|
def human_close(data):
|
|
sid = data.get('sid')
|
|
conn = connect()
|
|
conn.execute("UPDATE human_sessions SET status='closed' WHERE id=?", (sid,))
|
|
conn.commit()
|
|
conn.close()
|
|
return {'ok': True}
|
|
|
|
|
|
def human_typing(data, typing_map):
|
|
sid = data.get('sid')
|
|
typing_map[str(sid)] = time.time()
|
|
return {'ok': True}
|
|
|
|
|
|
def human_stop_typing(data, typing_map):
|
|
sid = data.get('sid')
|
|
typing_map.pop(str(sid), None)
|
|
return {'ok': True}
|
|
|
|
|
|
def human_read(data):
|
|
sid = data.get('sid')
|
|
conn = connect()
|
|
conn.execute("UPDATE human_messages SET read_at=? WHERE session_id=? AND side='customer' AND read_at IS NULL", (datetime.now().strftime('%Y-%m-%d %H:%M:%S'), sid))
|
|
conn.commit()
|
|
conn.close()
|
|
return {'ok': True}
|