完善会话草稿恢复与附件保留,统一人工服务时区

This commit is contained in:
2026-10-30 15:32:31 +08:00
parent 78797d8eca
commit b764fcb33c
20 changed files with 446 additions and 34 deletions
+15 -1
View File
@@ -2,21 +2,35 @@
import os
import secrets
import re
import time
from datetime import datetime
from urllib.parse import unquote
from .config import get_setting
def cleanup_uploads():
"""删除 uploads/ 里 7 天前的图片"""
"""Keep attachments referenced by chat history; expire unused uploads after 7 days."""
try:
udir = get_setting('UPLOAD_DIR')
if not os.path.isdir(udir):
return
from .db import connect
connection = connect()
try:
referenced = set()
rows = connection.execute("SELECT content FROM messages WHERE content LIKE '%/uploads/%' "
"UNION ALL SELECT content FROM human_messages WHERE content LIKE '%/uploads/%'")
for (content,) in rows:
referenced.update(unquote(name) for name in re.findall(r'/uploads/([A-Za-z0-9_.%-]+)', content or ''))
finally:
connection.close()
cutoff = time.time() - 7 * 86400
for f in os.listdir(udir):
if f in referenced:
continue
fp = os.path.join(udir, f)
if os.path.isfile(fp) and os.path.getmtime(fp) < cutoff:
os.remove(fp)
+7 -4
View File
@@ -1,18 +1,21 @@
"""Human conversation lifecycle; typing state is supplied by the app."""
import time
from datetime import datetime
from datetime import datetime, timedelta, timezone
from .db import connect
SERVICE_TIMEZONE = timezone(timedelta(hours=8))
def human_request(data):
conv_id = data.get('conv_id')
if not conv_id:
return ({'ok': False, 'error': 'no conv'}, 400)
if not 8 <= datetime.now().hour < 22:
return {'ok': False, 'closed': True, 'msg': '人工客服工作时间为每天8:00-22:00,当前为非工作时间,请留言'}
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
service_time = datetime.now(SERVICE_TIMEZONE)
if not 8 <= service_time.hour < 22:
return {'ok': False, 'closed': True, 'msg': '人工客服工作时间为每天北京时间8:00-22:00,当前为非工作时间,请留言'}
now = service_time.strftime('%Y-%m-%d %H:%M:%S')
conn = connect()
conn.execute('BEGIN IMMEDIATE')
if not conn.execute('SELECT 1 FROM chat_history WHERE id=?', (conv_id,)).fetchone():
+15 -4
View File
@@ -1,12 +1,17 @@
"""HTTP adapters for files; business logic lives in services."""
from flask import Blueprint, abort, request, send_from_directory
from flask import Blueprint, render_template, request, send_from_directory
from werkzeug.exceptions import NotFound
from .. import file_service, visitor_service
from ..config import get_setting
bp = Blueprint('files', __name__)
def unavailable(staff=False):
return render_template('attachment_unavailable.html', staff=staff), 404, {'Cache-Control':'private, no-store'}
@bp.route('/api/upload', methods=['POST'])
def upload_file():
return file_service.upload_file(request.files)
@@ -15,14 +20,20 @@ def upload_file():
@bp.route('/uploads/<path:name>')
def uploaded_file(name):
if not visitor_service.owns_upload(name):
abort(404)
response = send_from_directory(get_setting('UPLOAD_DIR'), name)
return unavailable()
try:
response = send_from_directory(get_setting('UPLOAD_DIR'), name)
except NotFound:
return unavailable()
response.headers['Cache-Control'] = 'private, no-store'
return response
@bp.route('/api/staff/uploads/<path:name>')
def staff_uploaded_file(name):
response = send_from_directory(get_setting('UPLOAD_DIR'), name)
try:
response = send_from_directory(get_setting('UPLOAD_DIR'), name)
except NotFound:
return unavailable(staff=True)
response.headers['Cache-Control'] = 'private, no-store'
return response