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:
Evilom
2026-06-12 10:32:24 +08:00
parent 3b77605591
commit 64ac71a8ed
9 changed files with 1453 additions and 9 deletions
+373
View File
@@ -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}
请妥善保管,勿轻示他人。
![邀请函]({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()