"""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