wechat_msg_crawler/wechat_cli/commands/search.py
canghe e64006bafe Initial release: wechat-cli v0.2.0
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
2026-04-04 11:10:10 +08:00

125 lines
4.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""search-messages 命令"""
import click
from ..core.contacts import get_contact_names
from ..core.messages import (
MSG_TYPE_FILTERS,
MSG_TYPE_NAMES,
collect_chat_search,
parse_time_range,
resolve_chat_context,
resolve_chat_contexts,
search_all_messages,
validate_pagination,
_candidate_page_size,
_page_ranked_entries,
)
from ..output.formatter import output
@click.command("search")
@click.argument("keyword")
@click.option("--chat", multiple=True, help="限定聊天对象(可多次指定)")
@click.option("--start-time", default="", help="起始时间")
@click.option("--end-time", default="", help="结束时间")
@click.option("--limit", default=20, help="返回数量最大500")
@click.option("--offset", default=0, help="分页偏移量")
@click.option("--format", "fmt", default="json", type=click.Choice(["json", "text"]), help="输出格式")
@click.option("--type", "msg_type", default=None, type=click.Choice(MSG_TYPE_NAMES), help="消息类型过滤")
@click.pass_context
def search(ctx, keyword, chat, start_time, end_time, limit, offset, fmt, msg_type):
"""搜索消息内容
\b
示例:
wechat-cli search "Claude" # 全局搜索
wechat-cli search "Claude" --chat "AI交流群" # 在指定群搜索
wechat-cli search "开会" --chat "群A" --chat "群B" # 同时搜多个群
wechat-cli search "你好" --start-time "2026-04-01" --limit 50
"""
app = ctx.obj
try:
validate_pagination(limit, offset)
start_ts, end_ts = parse_time_range(start_time, end_time)
except ValueError as e:
click.echo(f"错误: {e}", err=True)
ctx.exit(2)
names = get_contact_names(app.cache, app.decrypted_dir)
candidate_limit = _candidate_page_size(limit, offset)
chat_names = list(chat)
type_filter = MSG_TYPE_FILTERS[msg_type] if msg_type else None
if len(chat_names) == 1:
# 单聊搜索
chat_ctx = resolve_chat_context(chat_names[0], app.msg_db_keys, app.cache, app.decrypted_dir)
if not chat_ctx:
click.echo(f"找不到聊天对象: {chat_names[0]}", err=True)
ctx.exit(1)
if not chat_ctx['db_path']:
click.echo(f"找不到 {chat_ctx['display_name']} 的消息记录", err=True)
ctx.exit(1)
entries, failures = collect_chat_search(
chat_ctx, names, keyword, app.display_name_fn,
start_ts=start_ts, end_ts=end_ts, candidate_limit=candidate_limit,
msg_type_filter=type_filter,
)
scope = chat_ctx['display_name']
elif len(chat_names) > 1:
# 多聊搜索
resolved, unresolved, missing = resolve_chat_contexts(chat_names, app.msg_db_keys, app.cache, app.decrypted_dir)
if not resolved:
click.echo("错误: 没有可查询的聊天对象", err=True)
ctx.exit(1)
entries = []
failures = []
for rc in resolved:
e, f = collect_chat_search(
rc, names, keyword, app.display_name_fn,
start_ts=start_ts, end_ts=end_ts, candidate_limit=candidate_limit,
msg_type_filter=type_filter,
)
entries.extend(e)
failures.extend(f)
if unresolved:
failures.append("未找到: " + "".join(unresolved))
scope = f"{len(resolved)} 个聊天对象"
else:
# 全局搜索
entries, failures = search_all_messages(
app.msg_db_keys, app.cache, names, keyword, app.display_name_fn,
start_ts=start_ts, end_ts=end_ts, candidate_limit=candidate_limit,
msg_type_filter=type_filter,
)
scope = "全部消息"
paged = _page_ranked_entries(entries, limit, offset)
if fmt == 'json':
output({
'scope': scope,
'keyword': keyword,
'count': len(paged),
'offset': offset,
'limit': limit,
'start_time': start_time or None,
'end_time': end_time or None,
'type': msg_type or None,
'results': [item[1] for item in paged],
'failures': failures if failures else None,
}, 'json')
else:
if not paged:
output(f"{scope} 中未找到包含 \"{keyword}\" 的消息", 'text')
return
header = f"{scope} 中搜索 \"{keyword}\" 找到 {len(paged)} 条结果offset={offset}, limit={limit}"
if start_time or end_time:
header += f"\n时间范围: {start_time or '最早'} ~ {end_time or '最新'}"
if failures:
header += "\n查询失败: " + "".join(failures)
output(header + ":\n\n" + "\n\n".join(item[1] for item in paged), 'text')