feat: daily collection 6/12 - new docs, industry report, .gitignore cleanup
- Collected 9 new messages from dc group and innovation group - Fetched 6 new Feishu docs (黄静雯/陈楚真/韦译/夏莲/莫润麟/张家振) - Downloaded 狱国争霸分析拆解.html file attachment - Saved 6/12 game industry daily report from 韩丹 - Updated .gitignore to exclude temp scripts, images, downloaded files - Added utility scripts (group members extraction, invitation generation)
This commit is contained in:
@@ -0,0 +1,373 @@
|
||||
"""批量发送邀请卡图片:上传到 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}
|
||||
|
||||
请妥善保管,勿轻示他人。
|
||||
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user