feat: 微信自动化客服(wechatauto-replica) 干净历史导入 - AI 自动回复/语音收发/朋友圈发布

This commit is contained in:
2026-09-16 10:12:49 +08:00
commit c3b98f4c4e
69 changed files with 19828 additions and 0 deletions
+225
View File
@@ -0,0 +1,225 @@
# -*- coding: utf-8 -*-
"""wechatauto 演示:读取并下载会话中的媒体文件(图片/语音/视频/文件)。
从微信本地数据库直接取媒体消息并解密落地,无需 GUI 操作。
用法:
python demo_media.py [会话] [--out 目录] [--photos N]
[--limit N] [--ids 3,5,7] [--filter 图片,文件]
[--list] [--open]
默认行为(不带任何选项):
下载文件传输助手里最近 N 张真实照片(jpg/png,自动跳过表情)和所有视频。
参数:
会话 会话名(昵称/备注/username),默认「文件传输助手」
--photos 默认模式下下载照片张数(默认 3)
--limit 列出最近 N 条消息(默认 200)
--ids 仅下载指定 local_id(逗号分隔,此时忽略 --limit)
--filter 只处理指定类型,逗号分隔,可选 图片/语音/视频/文件
--out 下载保存目录(默认 ~/Documents/wechatauto_media
--list 仅列出媒体消息,不下载
--open 下载完成后用系统默认程序打开(仅单文件时有效)
例:
python demo_media.py # 最近 3 张照片+视频
python demo_media.py 兔仔仔 --photos 5 # 指定会话和数量
python demo_media.py 文件传输助手 --list --limit 30
python demo_media.py 我的群 --ids 105,107,109 --out D:\\media
python demo_media.py 文件传输助手 --filter 文件 --open
原理:
WeChatDB.get_messages 列出消息(local_type 识别媒体类型),
MediaDownloader.download_media 按类型自动分发:图片解密 .dat、
语音 SILK 提取、视频/文件从缓存目录复制。
"""
from __future__ import annotations
import argparse
import os
import sys
import time
try:
os.system("chcp 65001 >nul 2>&1")
except Exception:
pass
try:
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
except AttributeError:
pass
from wechatauto import WeChatDB, MediaDownloader
MEDIA_TYPES = {"图片": 3, "语音": 34, "视频": 43, "文件": 49}
def resolve_target(db: WeChatDB, raw: str) -> str:
hits = db.search_contact(raw)
return hits[0]["username"] if hits else raw
def fmt_size(n: int) -> str:
if n >= 1024 * 1024:
return f"{n / 1024 / 1024:.1f}MB"
if n >= 1024:
return f"{n / 1024:.1f}KB"
return f"{n}B"
def media_failure_reason(md, who: str, local_id: int) -> str:
"""下载失败时给出细分原因(表情容器 / 未落地 / 不支持)。
图片消息本地缓存的 .dat 解密后若为 wxgf(微信动画表情容器),
库会按设计不落盘;本函数复现该判定以区分「表情」与「真失败」。
"""
row = md.db.get_message_row(who, local_id)
if not row:
return "消息不存在"
t = row["local_type"]
if t == 3: # 图片
md5 = md._img_md5(row)
dat = md._find_dat(who, md5, row["create_time"]) if md5 else None
if not dat:
return "本地无 .dat 缓存(未在微信查看过/已清理)"
try:
data = md.decrypt_image(dat)
except Exception as e:
return f"解密失败:{str(e)[:40]}"
if data[:4] == b"wxgf":
return "微信动画表情容器(wxgf"
return "未知图片格式"
if t in (34, 43, 49):
return "本地缓存未落地或文件已清理"
return "类型不支持"
def main():
parser = argparse.ArgumentParser(
description="wechatauto 演示:读取/下载微信会话中的媒体文件",
add_help=False)
parser.add_argument("target", nargs="?", default="26级金高新生群1群",
help="会话名(昵称/备注/username),默认文件传输助手")
parser.add_argument("--limit", type=int, default=1000, help="列出最近 N 条(默认 200")
parser.add_argument("--photos", type=int, default=10, help="默认模式下载照片张数(默认 3")
parser.add_argument("--ids", default=None,
help="仅下载指定 local_id,逗号分隔,如 105,107")
parser.add_argument("--filter", default=None,
help="仅处理指定类型,逗号分隔:图片/语音/视频/文件")
parser.add_argument("--out", default=None, help="下载保存目录")
parser.add_argument("--image-key", default=None,
help="图片 AES 密钥(16 位,若内存扫描不可用)")
parser.add_argument("--list", action="store_true", help="仅列出媒体消息,不下载")
parser.add_argument("--open", action="store_true",
help="下载后用系统默认程序打开(仅单文件)")
parser.add_argument("-h", "--help", action="help")
args = parser.parse_args()
filt = None
if args.filter:
filt = set(t.strip() for t in args.filter.split(",") if t.strip())
unknown = [t for t in filt if t not in MEDIA_TYPES]
if unknown:
print(f"未知类型:{', '.join(unknown)}(可选:图片/语音/视频/文件)")
sys.exit(1)
print("=" * 60)
print("wechatauto 媒体读取演示")
print("=" * 60)
db = WeChatDB()
info = db.get_self_info()
print(f"账号:{info.get('nick_name') or info.get('username')}")
who = resolve_target(db, args.target)
print(f"会话:{args.target} -> {who}")
# ---- 指定 local_id:直接下载 ----
if args.ids:
ids = [int(x.strip()) for x in args.ids.split(",") if x.strip().isdigit()]
if not ids:
print("--ids 无效")
sys.exit(1)
md = MediaDownloader(db, save_dir=args.out, image_key=args.image_key)
for lid in ids:
row = db.get_message_row(who, lid)
if not row:
print(f" local_id={lid} 未找到,跳过")
continue
name = next((k for k, v in MEDIA_TYPES.items() if v == row["local_type"]), "")
t = time.strftime("%m-%d %H:%M", time.localtime(row["create_time"]))
label = name or ("类型%d" % row["local_type"])
print(f"\n[local_id={lid}] {t} {label}")
out = md.download_media(who, lid, args.out)
if out:
print(f" ✅ 已保存:{out} ({fmt_size(os.path.getsize(out))})")
if args.open:
os.startfile(out)
else:
print(f" ⚠️ 下载失败:{media_failure_reason(md, who, lid)}")
print("\n完成。")
sys.exit(0)
# ---- 列出最近消息并筛选媒体 ----
mode = "默认照片+视频" if filt is None else "+".join(sorted(filt))
msgs = db.get_messages(who, limit=args.limit)
media_msgs = []
print(f"\n最近 {len(msgs)} 条消息中的媒体项(模式:{mode}):")
for m in reversed(msgs):
name = MEDIA_TYPES.get(m.get("type"), "")
if not name:
continue
if filt and m.get("type") not in filt:
continue
if filt is None and m.get("type") not in ("图片", "视频"):
continue
t = time.strftime("%m-%d %H:%M", time.localtime(m["create_time"]))
sender = "" if m["sender_id"] == 2 else "对方"
media_msgs.append(m)
print(f" {m['local_id']:<6} {t} {sender} {m.get('type')} {m.get('content', '')[:40]}")
if args.list:
print(f"\n{len(media_msgs)} 条媒体消息(用 --ids 或 --filter 配合下载)。")
sys.exit(0)
if not media_msgs:
print("\n没有符合条件的媒体消息。")
sys.exit(0)
# ---- 下载筛选出的媒体 ----
md = MediaDownloader(db, save_dir=args.out, image_key=args.image_key)
ok = skip = fail = 0
photo_done = 0
for m in media_msgs:
t = time.strftime("%H:%M:%S", time.localtime(m["create_time"]))
print(f"\n[local_id={m['local_id']}] {t} {m.get('type')}")
is_photo_target = filt is None and m.get("type") == "图片"
if is_photo_target and photo_done >= args.photos:
print(f" 已达照片上限 {args.photos} 张,停止")
break
out = md.download_media(who, m["local_id"], args.out)
if out:
print(f"{fmt_size(os.path.getsize(out))} {out}")
ok += 1
if is_photo_target:
photo_done += 1
else:
reason = media_failure_reason(md, who, m["local_id"])
if "表情容器" in reason:
print(f" {reason}")
skip += 1
else:
print(f" ⚠️ 下载失败:{reason}")
fail += 1
print(f"\n完成:成功 {ok} 项,跳过 {skip} 项(表情),失败 {fail} 项。"
+ (f" 保存目录:{os.path.abspath(args.out or md.save_dir)}" if ok else ""))
if args.open and ok == 1:
out = md.download_media(who, media_msgs[0]["local_id"], args.out)
if out:
os.startfile(out)
sys.exit(0 if fail == 0 else 1)
if __name__ == "__main__":
main()