A CLI tool to query local WeChat data with 11 commands: sessions, history, search, contacts, members, stats, export, favorites, unread, new-messages, and init. Features: - Self-contained init with key extraction (no external deps) - On-the-fly SQLCipher decryption with caching - JSON output by default for LLM/AI tool integration - Message type filtering and chat statistics - Markdown/txt export for conversations - Cross-platform: macOS, Windows, Linux
143 lines
4.6 KiB
Python
143 lines
4.6 KiB
Python
"""favorites 命令 — 查看微信收藏"""
|
|
|
|
import os
|
|
import sqlite3
|
|
import xml.etree.ElementTree as ET
|
|
from contextlib import closing
|
|
from datetime import datetime
|
|
|
|
import click
|
|
|
|
from ..core.contacts import get_contact_names
|
|
from ..output.formatter import output
|
|
|
|
_FAV_TYPE_MAP = {
|
|
1: '文本', 2: '图片', 5: '文章', 19: '名片', 20: '视频号',
|
|
}
|
|
|
|
_FAV_TYPE_FILTERS = {
|
|
'text': 1, 'image': 2, 'article': 5, 'card': 19, 'video': 20,
|
|
}
|
|
|
|
|
|
def _parse_fav_content(content, fav_type):
|
|
"""从 XML content 提取摘要信息。"""
|
|
if not content:
|
|
return ''
|
|
try:
|
|
root = ET.fromstring(content)
|
|
except ET.ParseError:
|
|
return ''
|
|
item = root if root.tag == 'favitem' else root.find('.//favitem')
|
|
if item is None:
|
|
return ''
|
|
|
|
if fav_type == 1:
|
|
return (item.findtext('desc') or '').strip()
|
|
if fav_type == 2:
|
|
return '[图片收藏]'
|
|
if fav_type == 5:
|
|
title = (item.findtext('.//pagetitle') or '').strip()
|
|
desc = (item.findtext('.//pagedesc') or '').strip()
|
|
return f"{title} - {desc}" if desc else title
|
|
if fav_type == 19:
|
|
return (item.findtext('desc') or '').strip()
|
|
if fav_type == 20:
|
|
nickname = (item.findtext('.//nickname') or '').strip()
|
|
desc = (item.findtext('.//desc') or '').strip()
|
|
parts = [p for p in [nickname, desc] if p]
|
|
return ' '.join(parts) if parts else '[视频号]'
|
|
desc = (item.findtext('desc') or '').strip()
|
|
return desc if desc else '[收藏]'
|
|
|
|
|
|
@click.command("favorites")
|
|
@click.option("--limit", default=20, help="返回数量")
|
|
@click.option("--type", "fav_type", default=None,
|
|
type=click.Choice(list(_FAV_TYPE_FILTERS.keys())),
|
|
help="按类型过滤: text/image/article/card/video")
|
|
@click.option("--query", default=None, help="关键词搜索")
|
|
@click.option("--format", "fmt", default="json", type=click.Choice(["json", "text"]), help="输出格式")
|
|
@click.pass_context
|
|
def favorites(ctx, limit, fav_type, query, fmt):
|
|
"""查看微信收藏
|
|
|
|
\b
|
|
示例:
|
|
wechat-cli favorites # 最近收藏
|
|
wechat-cli favorites --type article # 只看文章
|
|
wechat-cli favorites --query "计算机网络" # 搜索收藏
|
|
wechat-cli favorites --limit 5 --format text
|
|
"""
|
|
app = ctx.obj
|
|
|
|
# 查找 favorite.db
|
|
fav_path = None
|
|
pre_decrypted = os.path.join(app.decrypted_dir, "favorite", "favorite.db")
|
|
if os.path.exists(pre_decrypted):
|
|
fav_path = pre_decrypted
|
|
else:
|
|
fav_path = app.cache.get(os.path.join("favorite", "favorite.db"))
|
|
if not fav_path:
|
|
click.echo("错误: 无法访问 favorite.db", err=True)
|
|
ctx.exit(3)
|
|
|
|
names = get_contact_names(app.cache, app.decrypted_dir)
|
|
|
|
with closing(sqlite3.connect(fav_path)) as conn:
|
|
where_parts = []
|
|
params = []
|
|
|
|
if fav_type:
|
|
where_parts.append('type = ?')
|
|
params.append(_FAV_TYPE_FILTERS[fav_type])
|
|
|
|
if query:
|
|
where_parts.append('content LIKE ?')
|
|
params.append(f'%{query}%')
|
|
|
|
where_sql = f"WHERE {' AND '.join(where_parts)}" if where_parts else ''
|
|
|
|
rows = conn.execute(f"""
|
|
SELECT local_id, type, update_time, content, fromusr, realchatname
|
|
FROM fav_db_item
|
|
{where_sql}
|
|
ORDER BY update_time DESC
|
|
LIMIT ?
|
|
""", (*params, limit)).fetchall()
|
|
|
|
results = []
|
|
for local_id, typ, ts, content, fromusr, realchat in rows:
|
|
from_display = names.get(fromusr, fromusr) if fromusr else ''
|
|
chat_display = names.get(realchat, realchat) if realchat else ''
|
|
|
|
summary = _parse_fav_content(content, typ)
|
|
|
|
results.append({
|
|
'id': local_id,
|
|
'type': _FAV_TYPE_MAP.get(typ, f'type={typ}'),
|
|
'time': datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M'),
|
|
'summary': summary,
|
|
'from': from_display,
|
|
'source_chat': chat_display,
|
|
})
|
|
|
|
if fmt == 'json':
|
|
output({
|
|
'count': len(results),
|
|
'favorites': results,
|
|
}, 'json')
|
|
else:
|
|
if not results:
|
|
output("没有找到收藏", 'text')
|
|
return
|
|
lines = []
|
|
for r in results:
|
|
entry = f"[{r['time']}] [{r['type']}] {r['summary']}"
|
|
if r['from']:
|
|
entry += f"\n 来自: {r['from']}"
|
|
if r['source_chat']:
|
|
entry += f" 聊天: {r['source_chat']}"
|
|
lines.append(entry)
|
|
output(f"收藏列表({len(results)} 条):\n\n" + "\n\n".join(lines), 'text')
|