Add image decryption and inline preview for WeChat V2 format
Support all three .dat encryption formats:
- Old XOR format: single-byte XOR, auto-detect key from magic bytes
- V1 format: AES-ECB with fixed key (md5("0")[:16]) + XOR tail
- V2 format (2025-08+): AES-128-ECB + raw middle + XOR tail
New files:
- decode_image.py: unified image decryption module (XOR/V1/V2)
- find_image_key.py: extract AES key from WeChat process memory
- find_image_key_monitor.py: continuous monitoring version for key capture
monitor_web.py changes:
- Inline image preview in Web UI with async decryption
- MonitorDBCache for mtime-based DB decryption caching
- username-to-DB mapping for image resolution chain
- /img/ endpoint for serving decoded images
- SSE image_update events for real-time preview updates
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
368
monitor_web.py
368
monitor_web.py
@@ -6,13 +6,19 @@ http://localhost:5678
|
||||
- 检测到变化后:全量解密DB + 全量WAL patch
|
||||
- SSE 服务器推送
|
||||
"""
|
||||
import hashlib, struct, os, sys, json, time, sqlite3, io, threading, queue
|
||||
import hashlib, struct, os, sys, json, time, sqlite3, io, threading, queue, traceback
|
||||
import hmac as hmac_mod
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from datetime import datetime
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from socketserver import ThreadingMixIn
|
||||
from Crypto.Cipher import AES
|
||||
import urllib.parse
|
||||
import glob as glob_mod
|
||||
import zstandard as zstd
|
||||
from decode_image import extract_md5_from_packed_info, decrypt_dat_file, is_v2_format
|
||||
|
||||
_zstd_dctx = zstd.ZstdDecompressor()
|
||||
|
||||
PAGE_SZ = 4096
|
||||
KEY_SZ = 32
|
||||
@@ -28,6 +34,11 @@ DB_DIR = _cfg["db_dir"]
|
||||
KEYS_FILE = _cfg["keys_file"]
|
||||
CONTACT_CACHE = os.path.join(_cfg["decrypted_dir"], "contact", "contact.db")
|
||||
DECRYPTED_SESSION = os.path.join(_cfg["decrypted_dir"], "session", "session.db")
|
||||
DECODED_IMAGE_DIR = _cfg.get("decoded_image_dir", os.path.join(os.path.dirname(os.path.abspath(__file__)), "decoded_images"))
|
||||
MONITOR_CACHE_DIR = os.path.join(_cfg["decrypted_dir"], "_monitor_cache")
|
||||
WECHAT_BASE_DIR = _cfg.get("wechat_base_dir", "")
|
||||
IMAGE_AES_KEY = _cfg.get("image_aes_key") # V2 格式 AES key (从微信内存提取)
|
||||
IMAGE_XOR_KEY = _cfg.get("image_xor_key", 0x88) # XOR key
|
||||
|
||||
POLL_MS = 30 # 高频轮询WAL/DB的mtime,30ms一次
|
||||
PORT = 5678
|
||||
@@ -37,6 +48,98 @@ sse_lock = threading.Lock()
|
||||
messages_log = []
|
||||
messages_lock = threading.Lock()
|
||||
MAX_LOG = 500
|
||||
_img_executor = ThreadPoolExecutor(max_workers=2, thread_name_prefix='img')
|
||||
|
||||
|
||||
class MonitorDBCache:
|
||||
"""轻量 DB 缓存,mtime 检测变化时重新解密"""
|
||||
|
||||
def __init__(self, keys, tmp_dir):
|
||||
self.keys = keys
|
||||
self.tmp_dir = tmp_dir
|
||||
os.makedirs(tmp_dir, exist_ok=True)
|
||||
self._state = {} # rel_key → (db_mtime, wal_mtime)
|
||||
|
||||
def get(self, rel_key):
|
||||
"""返回解密后的临时文件路径,mtime 变化时自动重新解密"""
|
||||
if rel_key not in self.keys:
|
||||
return None
|
||||
|
||||
enc_key = bytes.fromhex(self.keys[rel_key]["enc_key"])
|
||||
rel_path = rel_key.replace('\\', os.sep)
|
||||
db_path = os.path.join(DB_DIR, rel_path)
|
||||
wal_path = db_path + "-wal"
|
||||
|
||||
if not os.path.exists(db_path):
|
||||
return None
|
||||
|
||||
try:
|
||||
db_mtime = os.path.getmtime(db_path)
|
||||
wal_mtime = os.path.getmtime(wal_path) if os.path.exists(wal_path) else 0
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
out_name = rel_key.replace('\\', '_')
|
||||
out_path = os.path.join(self.tmp_dir, out_name)
|
||||
|
||||
prev = self._state.get(rel_key)
|
||||
|
||||
if prev is None or db_mtime != prev[0]:
|
||||
t0 = time.perf_counter()
|
||||
full_decrypt(db_path, out_path, enc_key)
|
||||
if os.path.exists(wal_path):
|
||||
decrypt_wal_full(wal_path, out_path, enc_key)
|
||||
ms = (time.perf_counter() - t0) * 1000
|
||||
print(f" [cache] {rel_key} 全量解密 {ms:.0f}ms", flush=True)
|
||||
self._state[rel_key] = (db_mtime, wal_mtime)
|
||||
elif wal_mtime != prev[1]:
|
||||
t0 = time.perf_counter()
|
||||
decrypt_wal_full(wal_path, out_path, enc_key)
|
||||
ms = (time.perf_counter() - t0) * 1000
|
||||
print(f" [cache] {rel_key} WAL patch {ms:.0f}ms", flush=True)
|
||||
self._state[rel_key] = (db_mtime, wal_mtime)
|
||||
|
||||
return out_path
|
||||
|
||||
|
||||
def build_username_db_map():
|
||||
"""从已解密的 Name2Id 表构建 username → [db_keys] 映射
|
||||
|
||||
同一个 username 可能存在于多个 message_N.db 中,
|
||||
按 DB 文件修改时间倒序排列(最新的排前面)。
|
||||
"""
|
||||
# 先获取每个 DB 的 mtime 用于排序
|
||||
db_mtimes = {}
|
||||
for i in range(5):
|
||||
rel_key = f"message\\message_{i}.db"
|
||||
db_path = os.path.join(DB_DIR, "message", f"message_{i}.db")
|
||||
try:
|
||||
db_mtimes[rel_key] = os.path.getmtime(db_path)
|
||||
except OSError:
|
||||
db_mtimes[rel_key] = 0
|
||||
|
||||
mapping = {} # username → [db_keys], 最新的在前
|
||||
decrypted_msg_dir = os.path.join(_cfg["decrypted_dir"], "message")
|
||||
for i in range(5):
|
||||
db_path = os.path.join(decrypted_msg_dir, f"message_{i}.db")
|
||||
if not os.path.exists(db_path):
|
||||
continue
|
||||
rel_key = f"message\\message_{i}.db"
|
||||
try:
|
||||
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
|
||||
for row in conn.execute("SELECT user_name FROM Name2Id").fetchall():
|
||||
if row[0] not in mapping:
|
||||
mapping[row[0]] = []
|
||||
mapping[row[0]].append(rel_key)
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
print(f" [WARN] Name2Id message_{i}.db: {e}", flush=True)
|
||||
|
||||
# 对每个 username 的 db_keys 按 mtime 倒序(最新的优先)
|
||||
for username in mapping:
|
||||
mapping[username].sort(key=lambda k: db_mtimes.get(k, 0), reverse=True)
|
||||
|
||||
return mapping
|
||||
|
||||
|
||||
def decrypt_page(enc_key, page_data, pgno):
|
||||
@@ -156,7 +259,12 @@ def msg_type_icon(t):
|
||||
|
||||
|
||||
def broadcast_sse(msg_data):
|
||||
payload = f"data: {json.dumps(msg_data, ensure_ascii=False)}\n\n"
|
||||
event_type = msg_data.get('event', '')
|
||||
data_line = f"data: {json.dumps(msg_data, ensure_ascii=False)}\n"
|
||||
if event_type:
|
||||
payload = f"event: {event_type}\n{data_line}\n"
|
||||
else:
|
||||
payload = f"{data_line}\n"
|
||||
with sse_lock:
|
||||
dead = []
|
||||
for q in sse_clients:
|
||||
@@ -171,15 +279,180 @@ def broadcast_sse(msg_data):
|
||||
# ============ 监听器 ============
|
||||
|
||||
class SessionMonitor:
|
||||
def __init__(self, enc_key, session_db, contact_names):
|
||||
def __init__(self, enc_key, session_db, contact_names, db_cache=None, username_db_map=None):
|
||||
self.enc_key = enc_key
|
||||
self.session_db = session_db
|
||||
self.wal_path = session_db + "-wal"
|
||||
self.contact_names = contact_names
|
||||
self.db_cache = db_cache
|
||||
self.username_db_map = username_db_map or {}
|
||||
self.prev_state = {}
|
||||
self.decrypt_ms = 0
|
||||
self.patched_pages = 0
|
||||
|
||||
def resolve_image(self, username, timestamp):
|
||||
"""解密图片: username+timestamp → 解密后的图片文件名,失败返回 None"""
|
||||
if not self.db_cache or not self.username_db_map:
|
||||
return None
|
||||
|
||||
# 1. 找到 username 对应的所有 message_N.db(按 mtime 倒序)
|
||||
db_keys = self.username_db_map.get(username)
|
||||
if not db_keys:
|
||||
return None
|
||||
|
||||
# 2. 遍历候选 DB,找到包含该 timestamp 消息的那个
|
||||
table_name = f"Msg_{hashlib.md5(username.encode()).hexdigest()}"
|
||||
local_id = None
|
||||
for db_key in db_keys:
|
||||
msg_db_path = self.db_cache.get(db_key)
|
||||
if not msg_db_path:
|
||||
continue
|
||||
try:
|
||||
conn = sqlite3.connect(f"file:{msg_db_path}?mode=ro", uri=True)
|
||||
# 精确匹配 timestamp
|
||||
row = conn.execute(f"""
|
||||
SELECT local_id FROM [{table_name}]
|
||||
WHERE local_type = 3 AND create_time = ?
|
||||
""", (timestamp,)).fetchone()
|
||||
if not row:
|
||||
# 模糊匹配(±3秒内最近的图片消息)
|
||||
row = conn.execute(f"""
|
||||
SELECT local_id FROM [{table_name}]
|
||||
WHERE local_type = 3 AND ABS(create_time - ?) <= 3
|
||||
ORDER BY ABS(create_time - ?) LIMIT 1
|
||||
""", (timestamp, timestamp)).fetchone()
|
||||
conn.close()
|
||||
if row:
|
||||
local_id = row[0]
|
||||
break
|
||||
except Exception as e:
|
||||
print(f" [img] 查询 {db_key}/{table_name} 失败: {e}", flush=True)
|
||||
|
||||
if not local_id:
|
||||
print(f" [img] 未找到 local_id: {username} t={timestamp}", flush=True)
|
||||
return None
|
||||
|
||||
# 4. 查 message_resource.db 获取 MD5
|
||||
# local_id 不全局唯一,需要同时匹配 create_time
|
||||
res_path = self.db_cache.get("message\\message_resource.db")
|
||||
if not res_path:
|
||||
return None
|
||||
|
||||
file_md5 = None
|
||||
try:
|
||||
conn = sqlite3.connect(f"file:{res_path}?mode=ro", uri=True)
|
||||
row = conn.execute(
|
||||
"SELECT packed_info FROM MessageResourceInfo "
|
||||
"WHERE message_local_id = ? AND message_create_time = ? AND message_local_type = 3",
|
||||
(local_id, timestamp)
|
||||
).fetchone()
|
||||
if not row:
|
||||
# 降级: 只用 create_time + type
|
||||
row = conn.execute(
|
||||
"SELECT packed_info FROM MessageResourceInfo "
|
||||
"WHERE message_create_time = ? AND message_local_type = 3",
|
||||
(timestamp,)
|
||||
).fetchone()
|
||||
conn.close()
|
||||
if row and row[0]:
|
||||
file_md5 = extract_md5_from_packed_info(row[0])
|
||||
except Exception as e:
|
||||
print(f" [img] 查询 message_resource 失败: {e}", flush=True)
|
||||
return None
|
||||
|
||||
if not file_md5:
|
||||
print(f" [img] 未找到 MD5: local_id={local_id} t={timestamp}", flush=True)
|
||||
return None
|
||||
|
||||
# 5. 查找 .dat 文件
|
||||
attach_dir = os.path.join(WECHAT_BASE_DIR, "msg", "attach")
|
||||
username_hash = hashlib.md5(username.encode()).hexdigest()
|
||||
search_base = os.path.join(attach_dir, username_hash)
|
||||
|
||||
if not os.path.isdir(search_base):
|
||||
print(f" [img] attach 目录不存在: {search_base}", flush=True)
|
||||
return None
|
||||
|
||||
pattern = os.path.join(search_base, "*", "Img", f"{file_md5}*.dat")
|
||||
dat_files = sorted(glob_mod.glob(pattern))
|
||||
if not dat_files:
|
||||
print(f" [img] 未找到 .dat: MD5={file_md5}", flush=True)
|
||||
return None
|
||||
|
||||
# 优先原图,然后高清 _h,最后缩略图 _t
|
||||
selected = dat_files[0]
|
||||
for f in dat_files:
|
||||
fname = os.path.basename(f)
|
||||
if not fname.startswith(file_md5 + '_'):
|
||||
selected = f
|
||||
break
|
||||
for f in dat_files:
|
||||
if f.endswith('_h.dat'):
|
||||
selected = f
|
||||
break
|
||||
|
||||
# 6. 解密图片
|
||||
os.makedirs(DECODED_IMAGE_DIR, exist_ok=True)
|
||||
out_base = os.path.join(DECODED_IMAGE_DIR, file_md5)
|
||||
|
||||
# 已解密则跳过
|
||||
for ext in ('jpg', 'png', 'gif', 'webp', 'bmp', 'tif'):
|
||||
candidate = f"{out_base}.{ext}"
|
||||
if os.path.exists(candidate):
|
||||
return os.path.basename(candidate)
|
||||
|
||||
# V2 新格式需要 AES key
|
||||
if is_v2_format(selected) and not IMAGE_AES_KEY:
|
||||
print(f" [img] V2 格式缺少 AES key: {os.path.basename(selected)}", flush=True)
|
||||
print(f" [img] 请运行 find_image_key.py 提取密钥", flush=True)
|
||||
return '__v2_unsupported__'
|
||||
|
||||
result_path, fmt = decrypt_dat_file(selected, f"{out_base}.tmp", IMAGE_AES_KEY, IMAGE_XOR_KEY)
|
||||
if not result_path:
|
||||
print(f" [img] 解密失败: {selected}", flush=True)
|
||||
return None
|
||||
|
||||
final = f"{out_base}.{fmt}"
|
||||
if os.path.exists(final):
|
||||
os.unlink(final)
|
||||
os.rename(result_path, final)
|
||||
size_kb = os.path.getsize(final) / 1024
|
||||
print(f" [img] 解密成功: {os.path.basename(final)} ({size_kb:.0f}KB)", flush=True)
|
||||
return os.path.basename(final)
|
||||
|
||||
def _async_resolve_image(self, username, timestamp, msg_data):
|
||||
"""后台线程: 解密图片并通过 SSE 推送更新"""
|
||||
for attempt in range(3):
|
||||
try:
|
||||
img_name = self.resolve_image(username, timestamp)
|
||||
if img_name == '__v2_unsupported__':
|
||||
# V2 新加密格式,显示占位提示
|
||||
msg_data['content'] = '[图片 - 新加密格式暂不支持预览]'
|
||||
broadcast_sse({
|
||||
'event': 'image_update',
|
||||
'timestamp': timestamp,
|
||||
'username': username,
|
||||
'v2_unsupported': True,
|
||||
})
|
||||
return
|
||||
elif img_name:
|
||||
image_url = f'/img/{img_name}'
|
||||
msg_data['image_url'] = image_url
|
||||
broadcast_sse({
|
||||
'event': 'image_update',
|
||||
'timestamp': timestamp,
|
||||
'username': username,
|
||||
'image_url': image_url,
|
||||
})
|
||||
print(f" [img] 异步解密成功: {img_name}", flush=True)
|
||||
return
|
||||
elif attempt < 2:
|
||||
time.sleep(1.5)
|
||||
except Exception as e:
|
||||
print(f" [img] 异步解密失败(attempt={attempt}): {e}", flush=True)
|
||||
if attempt < 2:
|
||||
time.sleep(1.5)
|
||||
|
||||
def query_state(self):
|
||||
"""查询已解密副本的session状态"""
|
||||
conn = sqlite3.connect(f"file:{DECRYPTED_SESSION}?mode=ro", uri=True)
|
||||
@@ -237,10 +510,15 @@ class SessionMonitor:
|
||||
sender = self.contact_names.get(curr['sender'], curr['sender_name'] or curr['sender'])
|
||||
|
||||
summary = curr['summary']
|
||||
if isinstance(summary, bytes):
|
||||
try:
|
||||
summary = _zstd_dctx.decompress(summary).decode('utf-8', errors='replace')
|
||||
except Exception:
|
||||
summary = '(压缩内容)'
|
||||
if summary and ':\n' in summary:
|
||||
summary = summary.split(':\n', 1)[1]
|
||||
|
||||
new_msgs.append({
|
||||
msg_data = {
|
||||
'time': datetime.fromtimestamp(curr['timestamp']).strftime('%H:%M:%S'),
|
||||
'timestamp': curr['timestamp'],
|
||||
'chat': display,
|
||||
@@ -253,7 +531,16 @@ class SessionMonitor:
|
||||
'unread': curr['unread'],
|
||||
'decrypt_ms': round(self.decrypt_ms, 1),
|
||||
'pages': self.patched_pages,
|
||||
})
|
||||
}
|
||||
|
||||
new_msgs.append(msg_data)
|
||||
|
||||
# 图片消息: 后台异步解密(不阻塞轮询)
|
||||
if curr['msg_type'] == 3:
|
||||
_img_executor.submit(
|
||||
self._async_resolve_image,
|
||||
username, curr['timestamp'], msg_data
|
||||
)
|
||||
|
||||
# 按时间排序
|
||||
new_msgs.sort(key=lambda m: m['timestamp'])
|
||||
@@ -281,8 +568,8 @@ class SessionMonitor:
|
||||
|
||||
self.prev_state = curr_state
|
||||
|
||||
def monitor_thread(enc_key, session_db, contact_names):
|
||||
mon = SessionMonitor(enc_key, session_db, contact_names)
|
||||
def monitor_thread(enc_key, session_db, contact_names, db_cache=None, username_db_map=None):
|
||||
mon = SessionMonitor(enc_key, session_db, contact_names, db_cache, username_db_map)
|
||||
wal_path = mon.wal_path
|
||||
|
||||
# 初始全量解密
|
||||
@@ -372,6 +659,8 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;b
|
||||
.msg-unread{font-size:10px;padding:1px 6px;border-radius:8px;background:rgba(244,67,54,.2);color:#ef9a9a;font-weight:600}
|
||||
.msg-perf{font-size:9px;color:#333}
|
||||
.msg-content{font-size:13px;line-height:1.4;color:#bbb;word-break:break-all;padding-left:63px}
|
||||
.msg-img{max-width:300px;max-height:200px;border-radius:8px;cursor:pointer;margin-top:4px;transition:transform .2s}
|
||||
.msg-img:hover{transform:scale(1.02)}
|
||||
.empty{text-align:center;padding:80px 20px;color:#444}
|
||||
.empty .icon{font-size:48px;margin-bottom:12px}
|
||||
::-webkit-scrollbar{width:4px}
|
||||
@@ -415,7 +704,13 @@ function addMsg(m, animate){
|
||||
const ur=m.unread>0?`<span class="msg-unread">${m.unread}</span>`:'';
|
||||
const cc=m.is_group?'msg-chat grp':'msg-chat';
|
||||
|
||||
d.innerHTML=`<div class="msg-header"><span class="msg-time">${m.time}</span><span class="${cc}">${esc(m.chat)}</span>${sn}<div class="msg-r"><span class="msg-type">${m.type_icon} ${m.type}</span>${ur}</div></div><div class="msg-content">${esc(m.content||'')}</div>`;
|
||||
let contentHtml = esc(m.content||'');
|
||||
if(m.image_url){
|
||||
contentHtml = `<img class="msg-img" src="${m.image_url}" onclick="window.open('${m.image_url}','_blank')" onerror="this.style.display='none';this.nextElementSibling.style.display='inline'" /><span style="display:none">${esc(m.content||'')}</span>`;
|
||||
}
|
||||
|
||||
const dk=m.timestamp+'|'+(m.username||m.chat);
|
||||
d.innerHTML=`<div class="msg-header"><span class="msg-time">${m.time}</span><span class="${cc}">${esc(m.chat)}</span>${sn}<div class="msg-r"><span class="msg-type">${m.type_icon} ${m.type}</span>${ur}</div></div><div class="msg-content" data-key="${dk}">${contentHtml}</div>`;
|
||||
|
||||
M.insertBefore(d, M.firstChild);
|
||||
|
||||
@@ -438,6 +733,22 @@ function connectSSE(){
|
||||
es.onmessage=ev=>{
|
||||
addMsg(JSON.parse(ev.data), true); // 新消息有动画
|
||||
};
|
||||
es.addEventListener('image_update', ev=>{
|
||||
const d=JSON.parse(ev.data);
|
||||
const key=d.timestamp+'|'+(d.username||'');
|
||||
const msgs=M.querySelectorAll('.msg');
|
||||
for(const el of msgs){
|
||||
const ct=el.querySelector('.msg-content');
|
||||
if(ct && ct.dataset.key===key){
|
||||
if(d.v2_unsupported){
|
||||
ct.innerHTML='<span style="color:#999;font-style:italic">[图片 - 新加密格式暂不支持预览]</span>';
|
||||
} else if(d.image_url){
|
||||
ct.innerHTML=`<img class="msg-img" src="${d.image_url}" onclick="window.open('${d.image_url}','_blank')" onerror="this.style.display='none'" />`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
es.onerror=()=>{
|
||||
S.textContent='重连...';
|
||||
S.className='status err';
|
||||
@@ -481,6 +792,32 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(data, ensure_ascii=False).encode('utf-8'))
|
||||
|
||||
elif self.path.startswith('/img/'):
|
||||
filename = urllib.parse.unquote(self.path[5:])
|
||||
# 安全: 防目录穿越
|
||||
if '/' in filename or '\\' in filename or '..' in filename:
|
||||
self.send_error(403)
|
||||
return
|
||||
filepath = os.path.join(DECODED_IMAGE_DIR, filename)
|
||||
if not os.path.isfile(filepath):
|
||||
self.send_error(404)
|
||||
return
|
||||
ext = os.path.splitext(filename)[1].lower()
|
||||
ct = {
|
||||
'.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
||||
'.png': 'image/png', '.gif': 'image/gif',
|
||||
'.webp': 'image/webp', '.bmp': 'image/bmp',
|
||||
'.tif': 'image/tiff',
|
||||
}.get(ext, 'application/octet-stream')
|
||||
with open(filepath, 'rb') as f:
|
||||
data = f.read()
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', ct)
|
||||
self.send_header('Content-Length', str(len(data)))
|
||||
self.send_header('Cache-Control', 'public, max-age=86400')
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
|
||||
elif self.path == '/stream':
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'text/event-stream')
|
||||
@@ -529,7 +866,20 @@ def main():
|
||||
contact_names = load_contact_names()
|
||||
print(f"已加载 {len(contact_names)} 个联系人", flush=True)
|
||||
|
||||
t = threading.Thread(target=monitor_thread, args=(enc_key, session_db, contact_names), daemon=True)
|
||||
print("构建 username→DB 映射...", flush=True)
|
||||
username_db_map = build_username_db_map()
|
||||
print(f"已映射 {len(username_db_map)} 个用户名", flush=True)
|
||||
|
||||
db_cache = MonitorDBCache(keys, MONITOR_CACHE_DIR)
|
||||
|
||||
# 后台预热 message_resource.db(图片解密必需)
|
||||
def _warmup():
|
||||
t0 = time.perf_counter()
|
||||
db_cache.get("message\\message_resource.db")
|
||||
print(f"[warmup] message_resource.db 预热完成 {(time.perf_counter()-t0)*1000:.0f}ms", flush=True)
|
||||
threading.Thread(target=_warmup, daemon=True).start()
|
||||
|
||||
t = threading.Thread(target=monitor_thread, args=(enc_key, session_db, contact_names, db_cache, username_db_map), daemon=True)
|
||||
t.start()
|
||||
|
||||
server = ThreadedServer(('0.0.0.0', PORT), Handler)
|
||||
|
||||
Reference in New Issue
Block a user