2026-10-22 04:18:27 -06:00

94 lines
3.9 KiB
Python

import json
import os
import sqlite3
from pathlib import Path
DEFAULT_DATA = Path(__file__).resolve().parent.parent / 'data'
def connect(root=None):
root = Path(root or os.environ.get('WECHAT_ANALYSIS_DATA', DEFAULT_DATA))
root.mkdir(parents=True, exist_ok=True)
try:
root.chmod(0o700)
except OSError:
pass
db = sqlite3.connect(root / 'workbench.sqlite3', timeout=30)
db.row_factory = sqlite3.Row
db.execute('PRAGMA foreign_keys=ON')
db.execute('PRAGMA journal_mode=WAL')
db.executescript('''
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY, fingerprint TEXT UNIQUE NOT NULL,
source_id TEXT NOT NULL, conversation TEXT NOT NULL,
conversation_id TEXT NOT NULL, kind TEXT NOT NULL,
sender TEXT NOT NULL, sender_id TEXT NOT NULL,
timestamp TEXT NOT NULL, content TEXT NOT NULL,
message_type TEXT NOT NULL, quote TEXT NOT NULL, source TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS msg_time ON messages(timestamp);
CREATE INDEX IF NOT EXISTS msg_conv ON messages(conversation_id, timestamp);
CREATE TABLE IF NOT EXISTS runs (
id TEXT PRIMARY KEY, created TEXT NOT NULL, mode TEXT NOT NULL,
filters TEXT NOT NULL, cards TEXT NOT NULL, coverage TEXT NOT NULL,
status TEXT NOT NULL, error TEXT NOT NULL DEFAULT ''
);
CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT NOT NULL);
''')
if 'events' not in {row[1] for row in db.execute('PRAGMA table_info(runs)')}:
db.execute("ALTER TABLE runs ADD COLUMN events TEXT NOT NULL DEFAULT '[]'")
db.commit()
return db
def setting(db, key, default=None):
row = db.execute('SELECT value FROM settings WHERE key=?', (key,)).fetchone()
return json.loads(row[0]) if row else default
def set_setting(db, key, value):
db.execute('INSERT OR REPLACE INTO settings VALUES (?,?)', (key, json.dumps(value, ensure_ascii=False)))
db.commit()
def where(filters):
clauses, args = ['1=1'], []
# Stored wall-clock times are normalized to the configured source UTC offset.
from .importer import parse_time
for key, op in [('start', '>='), ('end', '<=')]:
val = filters.get(key)
if val:
if len(val) == 10:
val += ' 00:00:00' if key == 'start' else ' 23:59:59'
clauses.append(f'timestamp {op} ?')
args.append(parse_time(val, filters.get('utc_offset', '+08:00')))
if filters.get('kind') in ('group', 'private'):
clauses.append('kind=?')
args.append(filters['kind'])
for key, negate in [('conversations', False), ('exclude', True)]:
values = filters.get(key) or []
if not isinstance(values, list) or len(values) > 500:
raise ValueError('会话筛选必须是数组,最多 500 项')
if values:
clauses.append('conversation_id ' + ('NOT IN' if negate else 'IN') + '(' + ','.join('?' for _ in values) + ')')
args.extend(values)
if filters.get('search'):
clauses.append('content LIKE ? ESCAPE \'\\\'')
term = filters['search'].replace('\\', '\\\\').replace('%', '\\%').replace('_', '\\_')
args.append('%' + term + '%')
return ' AND '.join(clauses), args
def messages(db, filters):
clause, args = where(filters)
return [dict(row) for row in db.execute('SELECT * FROM messages WHERE ' + clause + ' ORDER BY timestamp,id', args)]
def overview(db):
rows = db.execute('''SELECT conversation_id, MAX(conversation) AS conversation, kind,
COUNT(*) AS count, MIN(timestamp) AS first, MAX(timestamp) AS last,
SUM(CASE WHEN message_type!='text' THEN 1 ELSE 0 END) AS media
FROM messages GROUP BY conversation_id,kind ORDER BY count DESC''').fetchall()
total = db.execute('SELECT COUNT(*),MIN(timestamp),MAX(timestamp) FROM messages').fetchone()
return {'count': total[0], 'first': total[1], 'last': total[2], 'conversations': [dict(r) for r in rows]}