feat: 微信自动化客服(wechatauto-replica) 干净历史导入 - AI 自动回复/语音收发/朋友圈发布
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user