236 lines
8.1 KiB
Python
236 lines
8.1 KiB
Python
"""SQLite schema, history persistence and backups; initialized explicitly by create_app."""
|
|
|
|
import os
|
|
import re
|
|
import shutil
|
|
import sqlite3
|
|
from datetime import datetime, timedelta
|
|
from pathlib import Path
|
|
|
|
from .config import get_setting
|
|
from .tracking import card_to_text
|
|
|
|
|
|
def connect():
|
|
connection = sqlite3.connect(get_setting('DATABASE'))
|
|
try:
|
|
from flask import g, has_app_context
|
|
except ImportError:
|
|
return connection
|
|
if has_app_context():
|
|
g.setdefault('_database_connections', []).append(connection)
|
|
return connection
|
|
|
|
|
|
def close_connections(error=None):
|
|
from flask import g
|
|
for connection in g.pop('_database_connections', []):
|
|
connection.close()
|
|
|
|
|
|
|
|
def now_str():
|
|
return datetime.now().strftime('%Y-%m-%d %H:%M')
|
|
|
|
|
|
def init_db():
|
|
Path(get_setting('DATABASE')).parent.mkdir(parents=True, exist_ok=True)
|
|
conn = connect()
|
|
cur = conn.cursor()
|
|
cur.execute('''
|
|
CREATE TABLE IF NOT EXISTS chat_history (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
title TEXT NOT NULL,
|
|
ctime TEXT NOT NULL
|
|
)
|
|
''')
|
|
cols = [r[1] for r in cur.execute('PRAGMA table_info(chat_history)').fetchall()]
|
|
if 'utime' not in cols:
|
|
cur.execute('ALTER TABLE chat_history ADD COLUMN utime TEXT')
|
|
cur.execute('UPDATE chat_history SET utime = ctime WHERE utime IS NULL')
|
|
cur.execute('''
|
|
CREATE TABLE IF NOT EXISTS messages (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
conv_id INTEGER NOT NULL,
|
|
role TEXT NOT NULL,
|
|
content TEXT NOT NULL,
|
|
ctime TEXT NOT NULL
|
|
)
|
|
''')
|
|
cur.execute('CREATE INDEX IF NOT EXISTS idx_msg_conv ON messages(conv_id, id)')
|
|
cur.execute('''
|
|
CREATE TABLE IF NOT EXISTS feedback (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
question TEXT,
|
|
answer TEXT,
|
|
vote TEXT,
|
|
ctime TEXT
|
|
)
|
|
''')
|
|
cur.execute('''
|
|
CREATE TABLE IF NOT EXISTS human_sessions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
conv_id INTEGER NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'waiting',
|
|
ctime TEXT NOT NULL
|
|
)
|
|
''')
|
|
cur.execute('''
|
|
CREATE TABLE IF NOT EXISTS human_messages (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
session_id INTEGER NOT NULL,
|
|
side TEXT NOT NULL,
|
|
content TEXT NOT NULL,
|
|
ctime TEXT NOT NULL
|
|
)
|
|
''')
|
|
hm_cols = [r[1] for r in cur.execute('PRAGMA table_info(human_messages)').fetchall()]
|
|
if 'read_at' not in hm_cols:
|
|
cur.execute('ALTER TABLE human_messages ADD COLUMN read_at TEXT')
|
|
if 'recalled_id' not in hm_cols:
|
|
cur.execute('ALTER TABLE human_messages ADD COLUMN recalled_id INTEGER')
|
|
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
|
|
)
|
|
''')
|
|
cur.execute('''
|
|
CREATE TABLE IF NOT EXISTS agents (
|
|
username TEXT PRIMARY KEY,
|
|
password TEXT NOT NULL,
|
|
name TEXT NOT NULL
|
|
)
|
|
''')
|
|
cur.execute('''
|
|
CREATE TABLE IF NOT EXISTS orders (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
phone TEXT,
|
|
track_no TEXT,
|
|
status TEXT,
|
|
created TEXT
|
|
)
|
|
''')
|
|
cur.execute('''
|
|
CREATE TABLE IF NOT EXISTS tickets (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
content TEXT,
|
|
phone TEXT,
|
|
ctime TEXT,
|
|
status TEXT
|
|
)
|
|
''')
|
|
old = cur.execute('SELECT id, title FROM chat_history WHERE id NOT IN (SELECT DISTINCT conv_id FROM messages)').fetchall()
|
|
for cid, title in old:
|
|
cur.execute('INSERT INTO messages(conv_id, role, content, ctime) VALUES (?,?,?,?)', (cid, 'user', title, now_str()))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
|
|
def backup_db():
|
|
"""启动时把数据库备份到 backups/,文件名带日期,只保留最近 7 天"""
|
|
try:
|
|
bdir = get_setting('BACKUP_DIR')
|
|
os.makedirs(bdir, exist_ok=True)
|
|
today = datetime.now().strftime('%Y%m%d')
|
|
dst = os.path.join(bdir, f'history_{today}.db')
|
|
if not os.path.exists(dst):
|
|
shutil.copy2(get_setting('DATABASE'), dst)
|
|
cutoff = datetime.now() - timedelta(days=7)
|
|
for f in os.listdir(bdir):
|
|
m = re.match('history_(\\d{8})\\.db$', f)
|
|
if not m:
|
|
continue
|
|
try:
|
|
if datetime.strptime(m.group(1), '%Y%m%d') < cutoff:
|
|
os.remove(os.path.join(bdir, f))
|
|
except Exception:
|
|
pass
|
|
except Exception as e:
|
|
print(f'[数据库备份失败] {e}')
|
|
|
|
|
|
def cleanup_old_conversations():
|
|
"""自动清理 30 天前的旧对话,防止数据库无限膨胀"""
|
|
try:
|
|
conn = connect()
|
|
c = conn.cursor()
|
|
cutoff = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d %H:%M:%S')
|
|
rows = c.execute('SELECT id FROM chat_history WHERE utime < ?', (cutoff,)).fetchall()
|
|
ids = [r[0] for r in rows]
|
|
if not ids:
|
|
return
|
|
for cid in ids:
|
|
c.execute('DELETE FROM messages WHERE conv_id=?', (cid,))
|
|
c.execute(f"DELETE FROM chat_history WHERE id IN ({','.join('?' * len(ids))})", ids)
|
|
conn.commit()
|
|
conn.close()
|
|
print(f'[自动清理] 已删除 {len(ids)} 条 30 天前的旧对话')
|
|
except Exception as e:
|
|
print(f'[自动清理失败] {e}')
|
|
|
|
|
|
def make_title(content):
|
|
"""生成会话标题:优先用文字内容,只有附件时显示附件数量"""
|
|
lines = [l for l in content.split('\n') if not l.strip().startswith('[附件]')]
|
|
text = ' '.join(lines).strip()
|
|
if text and text != '(发送了附件)' and (text != '请帮我看看这个附件'):
|
|
return text[:24]
|
|
n = len([l for l in content.split('\n') if l.strip().startswith('[附件]')])
|
|
if n == 1:
|
|
return '📎 已上传附件'
|
|
if n > 1:
|
|
return f'📎 {n}个附件'
|
|
return content[:24]
|
|
|
|
|
|
def save_message(conv_id, role, content):
|
|
"""把一条消息写进会话;用户消息做幂等处理,重试不会重复入库。"""
|
|
if not conv_id or not content:
|
|
return
|
|
conn = connect()
|
|
cur = conn.cursor()
|
|
if role == 'user':
|
|
last = cur.execute('SELECT role, content FROM messages WHERE conv_id=? ORDER BY id DESC LIMIT 1', (conv_id,)).fetchone()
|
|
if last and last[0] == 'user' and (last[1] == content):
|
|
conn.close()
|
|
return 'duplicate'
|
|
cur.execute('INSERT INTO messages(conv_id, role, content, ctime) VALUES (?,?,?,?)', (conv_id, role, content, now_str()))
|
|
if role == 'user':
|
|
row = cur.execute('SELECT title FROM chat_history WHERE id=?', (conv_id,)).fetchone()
|
|
if row and (not row[0] or row[0] == '新对话'):
|
|
cur.execute('UPDATE chat_history SET title=? WHERE id=?', (make_title(content), conv_id))
|
|
cur.execute('UPDATE chat_history SET utime=? WHERE id=?', (now_str(), conv_id))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
|
|
def last_user_question(conv_id):
|
|
"""上一条用户提问(必须在当前这句入库之前调用)"""
|
|
if not conv_id:
|
|
return None
|
|
conn = connect()
|
|
row = conn.execute("SELECT content FROM messages WHERE conv_id=? AND role='user' ORDER BY id DESC LIMIT 1", (conv_id,)).fetchone()
|
|
conn.close()
|
|
return row[0] if row else None
|
|
|
|
|
|
def load_context(conv_id, msg):
|
|
"""取最近 MAX_CONTEXT 条消息,拼成模型要的 messages(不含 system)。"""
|
|
if not conv_id:
|
|
return [{'role': 'user', 'content': msg}]
|
|
conn = connect()
|
|
rows = conn.execute('SELECT role, content FROM messages WHERE conv_id=? ORDER BY id DESC LIMIT ?', (conv_id, get_setting('MAX_CONTEXT'))).fetchall()
|
|
conn.close()
|
|
rows.reverse()
|
|
history = [{'role': r, 'content': card_to_text(c)} for r, c in rows]
|
|
if not history or history[-1]['role'] != 'user' or history[-1]['content'] != msg:
|
|
history.append({'role': 'user', 'content': msg})
|
|
return history
|