"""批量发送邀请卡图片:上传到 ImgBB 图床 → 私聊 markdown 图片消息发给群成员 用法: python send_invitations.py --upload-only # 预上传所有图片到图床,按工号缓存 URL python send_invitations.py # 发送(跳过已发放,自动检查 URL) python send_invitations.py --limit 2 --dry-run # 试运行 python send_invitations.py --status # 查看发放状态 python send_invitations.py --reset <工号> # 重置某人发放状态(允许重新发放) """ import base64 import csv import json import os import subprocess import sys import time from datetime import datetime import requests sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from config import DWS DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data") CSV_PATH = os.path.join(DATA_DIR, "group_members_with_account.csv") INVITATION_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "output", "invitations") LOG_FILE = os.path.join(DATA_DIR, "send_invitation_log.json") URL_CACHE_FILE = os.path.join(DATA_DIR, "invitation_urls.json") STATUS_FILE = os.path.join(DATA_DIR, "invitation_delivery_status.json") DEFAULT_GROUP_NAME = "账号发放测试" IMGBB_API_KEY = "3d6db51a136a8993c6d14e2c14ccf3bc" IMGBB_API_URL = "https://api.imgbb.com/1/upload" def run_dws(args, timeout=30): cmd = [DWS] + args + ["-f", "json"] result = subprocess.run(cmd, capture_output=True, timeout=timeout) out = result.stdout.decode("utf-8", errors="replace") for i, ch in enumerate(out): if ch == "{": return json.loads(out[i:]) return None def find_group(group_name): data = run_dws(["chat", "search", "--query", group_name], timeout=15) if data and data.get("success"): groups = data["result"].get("groups", []) if groups: return groups[0]["openConversationId"] return None def fetch_all_members(group_id): all_members = [] cursor = "0" while True: data = run_dws(["chat", "group", "members", "list", "--id", group_id, "--cursor", cursor], timeout=60) if not data or not data.get("success"): break result = data.get("result", {}) all_members.extend(result.get("list", [])) if not result.get("hasMore"): break cursor = str(result.get("nextCursor", "")) if not cursor: break time.sleep(0.3) return all_members def upload_to_imgbb(image_path): with open(image_path, "rb") as f: image_data = base64.b64encode(f.read()).decode("utf-8") payload = {"key": IMGBB_API_KEY, "image": image_data} for attempt in range(3): try: resp = requests.post(IMGBB_API_URL, data=payload, timeout=30, proxies={"http": None, "https": None}) result = resp.json() if result.get("success"): return result["data"]["url"] return None except Exception as e: if attempt < 2: time.sleep(2 * (attempt + 1)) else: print(f" 上传异常: {e}") return None def check_url(url): try: resp = requests.head(url, timeout=10, allow_redirects=True, proxies={"http": None, "https": None}) return resp.status_code == 200 and "image" in resp.headers.get("content-type", "") except Exception: return False def load_json(path, default=None): if default is None: default = {} if os.path.isfile(path): with open(path, encoding="utf-8") as f: return json.load(f) return default def save_json(path, data): with open(path, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) def load_url_cache(): return load_json(URL_CACHE_FILE) def save_url_cache(cache): save_json(URL_CACHE_FILE, cache) def load_status(): return load_json(STATUS_FILE) def save_status(status): save_json(STATUS_FILE, status) def send_invitation(open_dingtalk_id, image_url, name, account, password, dry_run=False): if dry_run: return {"success": True, "dry_run": True} title = "隐秘之潮 - 邀请函" text = f"""{name},你好! 今诚邀阁下步入《隐秘之潮》,赴此内部试玩之旅。 此函所附,为阁下本次试玩凭证: 测试账号:{account} 测试密码:{password} 请妥善保管,勿轻示他人。 ![邀请函]({image_url})""" return run_dws([ "chat", "message", "send", "--open-dingtalk-id", open_dingtalk_id, "--title", title, "--text", text, ], timeout=15) def find_image(account): for ext in ("jpg", "png"): p = os.path.join(INVITATION_DIR, f"invite_{account}.{ext}") if os.path.isfile(p): return p return None def load_csv_members(): with open(CSV_PATH, encoding="utf-8") as f: rows = list(csv.DictReader(f)) members = {} for row in rows: name = row.get("姓名", "").strip() account = row.get("账号", "").strip() password = row.get("密码", "").strip() if name and account and password: members[name] = (account, password) return members def do_upload_all(): """预上传所有图片到图床,按工号缓存 URL""" members = load_csv_members() cache = load_url_cache() total = 0 uploaded = 0 skipped = 0 no_img = 0 for name, (account, _) in members.items(): img_path = find_image(account) if not img_path: no_img += 1 continue total += 1 cached = cache.get(account, {}) url = cached.get("url") if url and check_url(url): skipped += 1 continue url = upload_to_imgbb(img_path) if url: cache[account] = {"name": name, "url": url, "path": img_path} uploaded += 1 print(f" [{uploaded}] {account} {name} -> {url}") else: print(f" [FAIL] {account} {name} -> 上传失败") time.sleep(0.3) save_url_cache(cache) print(f"\n预上传完成! 总计: {total}, 新上传: {uploaded}, 已有: {skipped}, 无图片: {no_img}") print(f"URL 缓存: {URL_CACHE_FILE}") def do_send(group_name, dry_run=False, limit=0, target_name=None): """发送模式:匹配群成员 → 跳过已发放 → 发送 → 标记""" cache = load_url_cache() status = load_status() member_info = load_csv_members() print(f"CSV 中有账号的成员: {len(member_info)} 人") print(f"搜索群: {group_name}") group_id = find_group(group_name) if not group_id: print("未找到群") return print(f" 群ID: {group_id}") print("拉取群成员...") members = fetch_all_members(group_id) print(f" {len(members)} 人") # 匹配 to_send = [] skipped_delivered = 0 for m in members: name = m.get("memberEmpName", "") oid = m.get("openDingtalkId", "") if not name or not oid or name not in member_info: continue account, password = member_info[name] if not find_image(account): continue # 跳过已发放 if status.get(account, {}).get("delivered"): skipped_delivered += 1 continue to_send.append({"name": name, "account": account, "password": password, "oid": oid}) print(f"匹配到 {len(to_send) + skipped_delivered} 人有邀请卡") if skipped_delivered > 0: print(f" 其中 {skipped_delivered} 人已发放,跳过") # 指定姓名过滤 if target_name: to_send = [t for t in to_send if t["name"] == target_name] if not to_send: print(f"未找到未发放的成员: {target_name}") return if limit > 0: to_send = to_send[:limit] if not to_send: print("无需发放,全部已完成") return # 发送 success = 0 fail = 0 log = [] print(f"\n{'[DRY RUN] ' if dry_run else ''}开始发送...") for idx, item in enumerate(to_send, 1): name = item["name"] account = item["account"] oid = item["oid"] # 按工号查 URL cached = cache.get(account, {}) image_url = cached.get("url") if not image_url or not check_url(image_url): img_path = find_image(account) image_url = upload_to_imgbb(img_path) if not image_url: print(f" [{idx}/{len(to_send)}] {name}({account}) -> 上传失败") log.append({"name": name, "account": account, "status": "upload_fail"}) fail += 1 continue cache[account] = {"name": name, "url": image_url, "path": img_path} save_url_cache(cache) result = send_invitation(oid, image_url, name, account, item["password"], dry_run=dry_run) ok = result and result.get("success") if ok: success += 1 # 标记已发放 if not dry_run: status[account] = { "name": name, "delivered": True, "time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), } save_status(status) else: fail += 1 log.append({"name": name, "account": account, "image_url": image_url, "status": "ok" if ok else "fail", "response": result}) print(f" [{idx}/{len(to_send)}] {name}({account}) -> {'ok' if ok else 'fail'}") if not dry_run: time.sleep(0.5) save_json(LOG_FILE, log) print(f"\n完成! 成功: {success}, 失败: {fail}") print(f"日志: {LOG_FILE}") def do_status(): """查看发放状态""" status = load_status() members = load_csv_members() total_with_account = len(members) delivered = sum(1 for v in status.values() if v.get("delivered")) not_delivered = total_with_account - delivered print(f"有账号的成员: {total_with_account} 人") print(f"已发放: {delivered} 人") print(f"未发放: {not_delivered} 人") if status: print(f"\n最近发放记录:") recent = sorted(status.items(), key=lambda x: x[1].get("time", ""), reverse=True) for account, info in recent[:10]: print(f" {account} {info.get('name', '')} -> {info.get('time', '')}") def do_reset(account): """重置某人发放状态""" status = load_status() if account in status: name = status[account].get("name", "") del status[account] save_status(status) print(f"已重置: {account} {name}") else: print(f"未找到工号 {account} 的发放记录") def main(): import argparse parser = argparse.ArgumentParser(description="邀请卡图片批量发放") parser.add_argument("--group-name", type=str, default=DEFAULT_GROUP_NAME) parser.add_argument("--dry-run", action="store_true") parser.add_argument("--limit", type=int, default=0) parser.add_argument("--upload-only", action="store_true", help="仅预上传所有图片到图床,不发送") parser.add_argument("--status", action="store_true", help="查看发放状态") parser.add_argument("--reset", type=str, metavar="工号", help="重置某人发放状态") parser.add_argument("--name", type=str, help="指定发送给某人(姓名)") args = parser.parse_args() if args.upload_only: do_upload_all() elif args.status: do_status() elif args.reset: do_reset(args.reset) else: do_send(args.group_name, dry_run=args.dry_run, limit=args.limit, target_name=args.name) if __name__ == "__main__": main()