diff --git a/.gitignore b/.gitignore index 39c342b..764fa20 100644 --- a/.gitignore +++ b/.gitignore @@ -1,20 +1,27 @@ # 依赖 node_modules/ -# 大文件(视频/PPT/媒体) +# 大文件 videos/ dc_files/ -# 临时/中间文件 +# 临时文件 *.tmp *.bak *.log -# 原始消息数据(可通过脚本重新拉取) +# 原始消息数据 data/raw-messages/ -# 下载的飞书文档(可通过 lark-cli 重新下载) +# 飞书文档和图片 output/feishu-docs/ +output/feishu-images/ +output/downloaded-files/ + +# 生成文件 +output/invitations/ +output/reports/ +output/group_members_*.html # Python __pycache__/ @@ -29,8 +36,22 @@ venv/ # 系统文件 Thumbs.db Desktop.ini -.DS_Store +.DS_Store -# 下载的 CLI 工具(通过 setup_dws.py 自动下载) +# CLI工具 tools/ - + +# 临时脚本 +scripts/_*.py + +# 数据文件 +data/*.xlsx +data/*.ttf +data/bg*.png +data/bg*.txt +data/qiange_preview.json +data/send_*.json +data/today_bug_*.json +data/invitation_*.json +data/group_members_*.json +data/group_members_*.csv diff --git a/data/links/all_feishu_content.json b/data/links/all_feishu_content.json index 6f16818..dcf5d12 100644 --- a/data/links/all_feishu_content.json +++ b/data/links/all_feishu_content.json @@ -1,6 +1,6 @@ { "date": "2026-06-08", - "total": 179, + "total": 202, "documents": [ { "url": "https://fcnlycv6dd0w.feishu.cn/docx/KlGYddZoOo2UD9xIEVgcNqQ3n5c", @@ -2379,6 +2379,282 @@ "images_downloaded": 2, "fetched": true, "fetched_at": "2026-06-08T10:00:10.926087" + }, + { + "url": "https://dianchukeji.feishu.cn/docx/MHgSdgo1TonJDDx1leDct9kWnce", + "doc_id": "MHgSdgo1TonJDDx1leDct9kWnce", + "title": "MHgSdgo1TonJDDx1leDct9kWnce", + "sender": "黄静雯", + "group": "dc战略问题研究院", + "time": "2026-06-09 02:26:56", + "content_length": 5707, + "image_blocks": 0, + "images_downloaded": 0, + "fetched": true + }, + { + "url": "https://ocnmca6f1o0p.feishu.cn/wiki/BuTXwr0boidXf8kUmv4cYAYSnKf", + "doc_id": "BuTXwr0boidXf8kUmv4cYAYSnKf", + "title": "2026.6.8-日报-张家振", + "sender": "张家振", + "group": "dc战略问题研究院", + "time": "2026-06-09 01:17:19", + "content_length": 1325, + "image_blocks": 0, + "images_downloaded": 0, + "fetched": true + }, + { + "url": "https://fcnlycv6dd0w.feishu.cn/wiki/O68jwDBCCie0QCkRrDKcdyC6nTf", + "doc_id": "O68jwDBCCie0QCkRrDKcdyC6nTf", + "title": "2026.06.08-日报-莫润麟", + "sender": "莫润麟", + "group": "dc战略问题研究院", + "time": "2026-06-09 00:30:41", + "content_length": 3562, + "image_blocks": 2, + "images_downloaded": 2, + "fetched": true + }, + { + "url": "https://fcnlycv6dd0w.feishu.cn/docx/NwXIdPqYhooCKlxf14jc0M3DnOh", + "doc_id": "NwXIdPqYhooCKlxf14jc0M3DnOh", + "title": "NwXIdPqYhooCKlxf14jc0M3DnOh", + "sender": "夏莲", + "group": "dc战略问题研究院", + "time": "2026-06-09 00:30:40", + "content_length": 7810, + "image_blocks": 1, + "images_downloaded": 1, + "fetched": true + }, + { + "url": "https://dianchukeji.feishu.cn/wiki/L6K3wxI54iLqBEkyD5zcWNxFn1g", + "doc_id": "L6K3wxI54iLqBEkyD5zcWNxFn1g", + "title": "2026.6.8-日报-王雨默", + "sender": "王雨默", + "group": "创新组", + "time": "2026-06-09 00:04:40", + "content_length": 5257, + "image_blocks": 0, + "images_downloaded": 0, + "fetched": true + }, + { + "url": "https://dianchukeji.feishu.cn/wiki/Hy4Vw8qvtiNG2mkS0d5c5T06ntd", + "doc_id": "Hy4Vw8qvtiNG2mkS0d5c5T06ntd", + "title": "2026.06.08日报-刘鹏", + "sender": "刘鹏", + "group": "创新组", + "time": "2026-06-08 23:17:38", + "content_length": 3647, + "image_blocks": 6, + "images_downloaded": 6, + "fetched": true + }, + { + "url": "https://dianchukeji.feishu.cn/wiki/UOuzwnxCuiS06BkPaNIconpnnXj", + "doc_id": "UOuzwnxCuiS06BkPaNIconpnnXj", + "title": "260608日报-韦译", + "sender": "韦译", + "group": "创新组", + "time": "2026-06-08 23:06:55", + "content_length": 3485, + "image_blocks": 2, + "images_downloaded": 2, + "fetched": true + }, + { + "url": "https://dianchukeji.feishu.cn/docx/Bkiodh65eoWffGxiyGZcd3gfn1f", + "doc_id": "Bkiodh65eoWffGxiyGZcd3gfn1f", + "title": "一、工作内容概述", + "sender": "黄静雯", + "group": "dc战略问题研究院", + "time": "2026-06-10 09:26:20", + "content_length": 4211, + "image_blocks": 0, + "images_downloaded": 0, + "fetched": true + }, + { + "url": "https://dianchukeji.feishu.cn/docx/HrTqdLydWo4QJrxPb40c5EvUnbr", + "doc_id": "HrTqdLydWo4QJrxPb40c5EvUnbr", + "title": "一、工作内容概述", + "sender": "陈楚真", + "group": "dc战略问题研究院", + "time": "2026-06-10 00:41:07", + "content_length": 7825, + "image_blocks": 0, + "images_downloaded": 0, + "fetched": true + }, + { + "url": "https://fcnlycv6dd0w.feishu.cn/docx/GBpOdqv3FowObmxeNR0cJGlGnkb", + "doc_id": "GBpOdqv3FowObmxeNR0cJGlGnkb", + "title": "", + "sender": "夏莲", + "group": "dc战略问题研究院", + "time": "2026-06-10 00:01:04", + "content_length": 5122, + "image_blocks": 2, + "images_downloaded": 2, + "fetched": true + }, + { + "url": "https://ocnmca6f1o0p.feishu.cn/wiki/QlkHwnISsiFSLRk6Q2PcyIn3ntc", + "doc_id": "QlkHwnISsiFSLRk6Q2PcyIn3ntc", + "title": "2026.6.9-日报-张家振", + "sender": "张家振", + "group": "dc战略问题研究院", + "time": "2026-06-10 00:01:02", + "content_length": 1798, + "image_blocks": 0, + "images_downloaded": 0, + "fetched": true + }, + { + "url": "https://fcnlycv6dd0w.feishu.cn/wiki/FJCowQGEbibDkakfJOacwaLmnV3", + "doc_id": "FJCowQGEbibDkakfJOacwaLmnV3", + "title": "2026.06.09-日报-莫润麟", + "sender": "莫润麟", + "group": "dc战略问题研究院", + "time": "2026-06-10 00:00:57", + "content_length": 3359, + "image_blocks": 0, + "images_downloaded": 0, + "fetched": true + }, + { + "url": "https://dianchukeji.feishu.cn/wiki/NKBVwPk7MiPg5XksndtcIU7nnWb", + "doc_id": "NKBVwPk7MiPg5XksndtcIU7nnWb", + "title": "2026.6.9-日报-王雨默", + "sender": "王雨默", + "group": "创新组", + "time": "2026-06-10 02:13:17", + "content_length": 5765, + "image_blocks": 0, + "images_downloaded": 0, + "fetched": true + }, + { + "url": "https://dianchukeji.feishu.cn/wiki/NLKrw7OJsirClOkhlHFcVeUCn5c", + "doc_id": "NLKrw7OJsirClOkhlHFcVeUCn5c", + "title": "2026.06.09日报-刘鹏", + "sender": "刘鹏", + "group": "创新组", + "time": "2026-06-09 23:41:13", + "content_length": 3486, + "image_blocks": 4, + "images_downloaded": 4, + "fetched": true + }, + { + "url": "https://dianchukeji.feishu.cn/wiki/JirGw7bynivIo4k76EqcsHCUnqb", + "doc_id": "JirGw7bynivIo4k76EqcsHCUnqb", + "title": "260609日报-韦译", + "sender": "韦译", + "group": "创新组", + "time": "2026-06-09 23:15:41", + "content_length": 4749, + "image_blocks": 6, + "images_downloaded": 6, + "fetched": true + }, + { + "url": "https://ocnmca6f1o0p.feishu.cn/wiki/SxSfwITX3iXoeKknVxickYWUnEf", + "doc_id": "SxSfwITX3iXoeKknVxickYWUnEf", + "title": "2026.6.10-日报-张家振", + "sender": "张家振", + "group": "dc战略问题研究院", + "time": "2026-06-11 01:29:02", + "content_length": 2091, + "image_blocks": 0, + "images_downloaded": 0, + "fetched": true + }, + { + "url": "https://fcnlycv6dd0w.feishu.cn/wiki/RW59wv667iwqnyk7EBScwJKMn1c", + "doc_id": "RW59wv667iwqnyk7EBScwJKMn1c", + "title": "2026.06.10-日报-莫润麟", + "sender": "莫润麟", + "group": "dc战略问题研究院", + "time": "2026-06-11 00:31:32", + "content_length": 2367, + "image_blocks": 0, + "images_downloaded": 0, + "fetched": true + }, + { + "url": "https://fcnlycv6dd0w.feishu.cn/docx/NNcgdMsI9ohaDDxDD32cfw7onUc", + "doc_id": "NNcgdMsI9ohaDDxDD32cfw7onUc", + "title": "", + "sender": "夏莲", + "group": "dc战略问题研究院", + "time": "2026-06-11 00:31:29", + "content_length": 6718, + "image_blocks": 0, + "images_downloaded": 0, + "fetched": true + }, + { + "url": "https://dianchukeji.feishu.cn/docx/S4bSd1MU3ofarpxOuUGcot1vnXd", + "doc_id": "S4bSd1MU3ofarpxOuUGcot1vnXd", + "title": "一、工作内容概述", + "sender": "陈楚真", + "group": "dc战略问题研究院", + "time": "2026-06-11 00:29:51", + "content_length": 5879, + "image_blocks": 6, + "images_downloaded": 6, + "fetched": true + }, + { + "url": "https://dianchukeji.feishu.cn/docx/A9lFdJXz5olCDvxsAwHcgSLGnad", + "doc_id": "A9lFdJXz5olCDvxsAwHcgSLGnad", + "title": "一、工作内容概述", + "sender": "黄静雯", + "group": "dc战略问题研究院", + "time": "2026-06-11 00:29:25", + "content_length": 3115, + "image_blocks": 0, + "images_downloaded": 0, + "fetched": true + }, + { + "url": "https://dianchukeji.feishu.cn/wiki/UAicweFWFi4jK0kvTCzcowIynkh", + "doc_id": "UAicweFWFi4jK0kvTCzcowIynkh", + "title": "2026.6.10-日报-王雨默", + "sender": "王雨默", + "group": "创新组", + "time": "2026-06-11 01:36:00", + "content_length": 898, + "image_blocks": 0, + "images_downloaded": 0, + "fetched": true + }, + { + "url": "https://dianchukeji.feishu.cn/wiki/BKigwCCEvixk1vkftyHc4Stonxf", + "doc_id": "BKigwCCEvixk1vkftyHc4Stonxf", + "title": "2026.06.10日报-刘鹏", + "sender": "刘鹏", + "group": "创新组", + "time": "2026-06-10 23:02:07", + "content_length": 3487, + "image_blocks": 11, + "images_downloaded": 11, + "fetched": true + }, + { + "url": "https://dianchukeji.feishu.cn/wiki/WOTowPY5nibWUKkaEKecw1FCnBh", + "doc_id": "WOTowPY5nibWUKkaEKecw1FCnBh", + "title": "260610日报-韦译", + "sender": "韦译", + "group": "创新组", + "time": "2026-06-10 22:40:42", + "content_length": 4533, + "image_blocks": 15, + "images_downloaded": 15, + "fetched": true } ] } \ No newline at end of file diff --git a/data/links/all_feishu_links.json b/data/links/all_feishu_links.json index 3423255..f6cde42 100644 --- a/data/links/all_feishu_links.json +++ b/data/links/all_feishu_links.json @@ -1,6 +1,6 @@ { "date": "2026-06-06", - "total": 215, + "total": 238, "links": [ { "url": "https://fcnlycv6dd0w.feishu.cn/docx/ErwhdZhR3oJjoNxGB85cQBicnge", @@ -1721,6 +1721,190 @@ "sender": "夏莲", "time": "2026-06-06 23:28:33", "group": "dc战略问题研究院" + }, + { + "url": "https://dianchukeji.feishu.cn/docx/MHgSdgo1TonJDDx1leDct9kWnce", + "doc_id": "MHgSdgo1TonJDDx1leDct9kWnce", + "type": "docx", + "sender": "黄静雯", + "time": "2026-06-09 02:26:56", + "group": "dc战略问题研究院" + }, + { + "url": "https://ocnmca6f1o0p.feishu.cn/wiki/BuTXwr0boidXf8kUmv4cYAYSnKf", + "doc_id": "BuTXwr0boidXf8kUmv4cYAYSnKf", + "type": "wiki", + "sender": "张家振", + "time": "2026-06-09 01:17:19", + "group": "dc战略问题研究院" + }, + { + "url": "https://fcnlycv6dd0w.feishu.cn/wiki/O68jwDBCCie0QCkRrDKcdyC6nTf", + "doc_id": "O68jwDBCCie0QCkRrDKcdyC6nTf", + "type": "wiki", + "sender": "莫润麟", + "time": "2026-06-09 00:30:41", + "group": "dc战略问题研究院" + }, + { + "url": "https://fcnlycv6dd0w.feishu.cn/docx/NwXIdPqYhooCKlxf14jc0M3DnOh", + "doc_id": "NwXIdPqYhooCKlxf14jc0M3DnOh", + "type": "docx", + "sender": "夏莲", + "time": "2026-06-09 00:30:40", + "group": "dc战略问题研究院" + }, + { + "url": "https://dianchukeji.feishu.cn/wiki/L6K3wxI54iLqBEkyD5zcWNxFn1g", + "doc_id": "L6K3wxI54iLqBEkyD5zcWNxFn1g", + "type": "wiki", + "sender": "王雨默", + "time": "2026-06-09 00:04:40", + "group": "创新组" + }, + { + "url": "https://dianchukeji.feishu.cn/wiki/Hy4Vw8qvtiNG2mkS0d5c5T06ntd", + "doc_id": "Hy4Vw8qvtiNG2mkS0d5c5T06ntd", + "type": "wiki", + "sender": "刘鹏", + "time": "2026-06-08 23:17:38", + "group": "创新组" + }, + { + "url": "https://dianchukeji.feishu.cn/wiki/UOuzwnxCuiS06BkPaNIconpnnXj", + "doc_id": "UOuzwnxCuiS06BkPaNIconpnnXj", + "type": "wiki", + "sender": "韦译", + "time": "2026-06-08 23:06:55", + "group": "创新组" + }, + { + "url": "https://dianchukeji.feishu.cn/docx/Bkiodh65eoWffGxiyGZcd3gfn1f", + "doc_id": "Bkiodh65eoWffGxiyGZcd3gfn1f", + "type": "docx", + "sender": "黄静雯", + "time": "2026-06-10 09:26:20", + "group": "dc战略问题研究院" + }, + { + "url": "https://dianchukeji.feishu.cn/docx/HrTqdLydWo4QJrxPb40c5EvUnbr", + "doc_id": "HrTqdLydWo4QJrxPb40c5EvUnbr", + "type": "docx", + "sender": "陈楚真", + "time": "2026-06-10 00:41:07", + "group": "dc战略问题研究院" + }, + { + "url": "https://fcnlycv6dd0w.feishu.cn/docx/GBpOdqv3FowObmxeNR0cJGlGnkb", + "doc_id": "GBpOdqv3FowObmxeNR0cJGlGnkb", + "type": "docx", + "sender": "夏莲", + "time": "2026-06-10 00:01:04", + "group": "dc战略问题研究院" + }, + { + "url": "https://ocnmca6f1o0p.feishu.cn/wiki/QlkHwnISsiFSLRk6Q2PcyIn3ntc", + "doc_id": "QlkHwnISsiFSLRk6Q2PcyIn3ntc", + "type": "wiki", + "sender": "张家振", + "time": "2026-06-10 00:01:02", + "group": "dc战略问题研究院" + }, + { + "url": "https://fcnlycv6dd0w.feishu.cn/wiki/FJCowQGEbibDkakfJOacwaLmnV3", + "doc_id": "FJCowQGEbibDkakfJOacwaLmnV3", + "type": "wiki", + "sender": "莫润麟", + "time": "2026-06-10 00:00:57", + "group": "dc战略问题研究院" + }, + { + "url": "https://dianchukeji.feishu.cn/wiki/NKBVwPk7MiPg5XksndtcIU7nnWb", + "doc_id": "NKBVwPk7MiPg5XksndtcIU7nnWb", + "type": "wiki", + "sender": "王雨默", + "time": "2026-06-10 02:13:17", + "group": "创新组" + }, + { + "url": "https://dianchukeji.feishu.cn/wiki/NLKrw7OJsirClOkhlHFcVeUCn5c", + "doc_id": "NLKrw7OJsirClOkhlHFcVeUCn5c", + "type": "wiki", + "sender": "刘鹏", + "time": "2026-06-09 23:41:13", + "group": "创新组" + }, + { + "url": "https://dianchukeji.feishu.cn/wiki/JirGw7bynivIo4k76EqcsHCUnqb", + "doc_id": "JirGw7bynivIo4k76EqcsHCUnqb", + "type": "wiki", + "sender": "韦译", + "time": "2026-06-09 23:15:41", + "group": "创新组" + }, + { + "url": "https://ocnmca6f1o0p.feishu.cn/wiki/SxSfwITX3iXoeKknVxickYWUnEf", + "doc_id": "SxSfwITX3iXoeKknVxickYWUnEf", + "type": "wiki", + "sender": "张家振", + "time": "2026-06-11 01:29:02", + "group": "dc战略问题研究院" + }, + { + "url": "https://fcnlycv6dd0w.feishu.cn/wiki/RW59wv667iwqnyk7EBScwJKMn1c", + "doc_id": "RW59wv667iwqnyk7EBScwJKMn1c", + "type": "wiki", + "sender": "莫润麟", + "time": "2026-06-11 00:31:32", + "group": "dc战略问题研究院" + }, + { + "url": "https://fcnlycv6dd0w.feishu.cn/docx/NNcgdMsI9ohaDDxDD32cfw7onUc", + "doc_id": "NNcgdMsI9ohaDDxDD32cfw7onUc", + "type": "docx", + "sender": "夏莲", + "time": "2026-06-11 00:31:29", + "group": "dc战略问题研究院" + }, + { + "url": "https://dianchukeji.feishu.cn/docx/S4bSd1MU3ofarpxOuUGcot1vnXd", + "doc_id": "S4bSd1MU3ofarpxOuUGcot1vnXd", + "type": "docx", + "sender": "陈楚真", + "time": "2026-06-11 00:29:51", + "group": "dc战略问题研究院" + }, + { + "url": "https://dianchukeji.feishu.cn/docx/A9lFdJXz5olCDvxsAwHcgSLGnad", + "doc_id": "A9lFdJXz5olCDvxsAwHcgSLGnad", + "type": "docx", + "sender": "黄静雯", + "time": "2026-06-11 00:29:25", + "group": "dc战略问题研究院" + }, + { + "url": "https://dianchukeji.feishu.cn/wiki/UAicweFWFi4jK0kvTCzcowIynkh", + "doc_id": "UAicweFWFi4jK0kvTCzcowIynkh", + "type": "wiki", + "sender": "王雨默", + "time": "2026-06-11 01:36:00", + "group": "创新组" + }, + { + "url": "https://dianchukeji.feishu.cn/wiki/BKigwCCEvixk1vkftyHc4Stonxf", + "doc_id": "BKigwCCEvixk1vkftyHc4Stonxf", + "type": "wiki", + "sender": "刘鹏", + "time": "2026-06-10 23:02:07", + "group": "创新组" + }, + { + "url": "https://dianchukeji.feishu.cn/wiki/WOTowPY5nibWUKkaEKecw1FCnBh", + "doc_id": "WOTowPY5nibWUKkaEKecw1FCnBh", + "type": "wiki", + "sender": "韦译", + "time": "2026-06-10 22:40:42", + "group": "创新组" } ] } \ No newline at end of file diff --git a/scripts/batch_gen_invitations.py b/scripts/batch_gen_invitations.py new file mode 100644 index 0000000..e42d56e --- /dev/null +++ b/scripts/batch_gen_invitations.py @@ -0,0 +1,97 @@ +"""批量生成邀请函图片:读取 invitation_config.json 模板 + group_members_with_account.csv""" + +import csv +import json +import os +import sys + +from PIL import Image, ImageDraw, ImageFont + +DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data") +BG_PATH = os.path.join(DATA_DIR, "bg.png") +FONT_DIR = DATA_DIR +CONFIG_PATH = os.path.join(DATA_DIR, "invitation_config.json") +CSV_PATH = os.path.join(DATA_DIR, "group_members_with_account.csv") +OUTPUT_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "output", "invitations") + + +def hex_to_rgb(h): + h = h.lstrip('#') + return tuple(int(h[i:i+2], 16) for i in (0, 2, 4)) + + +def resolve_font(name): + local = os.path.join(FONT_DIR, name) + if os.path.isfile(local): + return local + sys_font = os.path.join(r"C:\Windows\Fonts", name) + if os.path.isfile(sys_font): + return sys_font + return os.path.join(FONT_DIR, "WenDaoGeTeSong.ttf") + + +def generate_image(cfg, name, account, password): + img = Image.open(BG_PATH).copy() + draw = ImageDraw.Draw(img) + font_path = resolve_font(cfg.get("font", "WenDaoGeTeSong.ttf")) + sw = cfg.get("stroke_width", 3) + sc = hex_to_rgb(cfg.get("stroke_color", "#000000")) + + size = cfg.get("size", 30) + font = ImageFont.truetype(font_path, size) + color = hex_to_rgb(cfg.get("color", "#ffffff")) + x = cfg.get("x", 510) + y = cfg.get("y", 1055) + + text = cfg.get("text", "") + text = text.replace("{name}", name).replace("{account}", account).replace("{password}", password) + + line_height = cfg.get("line_height", 1.5) + spacing = int(size * (line_height - 1.0)) + + align = cfg.get("align", "center") + anchor_map = {"left": "lt", "center": "mt", "right": "rt"} + anchor = anchor_map.get(align, "mt") + + lines = text.split("\n") + cur_y = y + for line in lines: + draw.text( + (x, cur_y), line, fill=color, font=font, anchor=anchor, + stroke_width=sw, stroke_fill=sc + ) + cur_y += size + spacing + return img + + +def main(): + with open(CONFIG_PATH, encoding="utf-8") as f: + cfg = json.load(f) + + with open(CSV_PATH, encoding="utf-8") as f: + rows = list(csv.DictReader(f)) + + os.makedirs(OUTPUT_DIR, exist_ok=True) + + total = 0 + skipped = 0 + for row in rows: + name = row.get("姓名", "").strip() + account = row.get("账号", "").strip() + password = row.get("密码", "").strip() + + if not account or not password: + skipped += 1 + continue + + img = generate_image(cfg, name, account, password) + output_path = os.path.join(OUTPUT_DIR, f"invite_{account}.jpg") + img.convert("RGB").save(output_path, format="JPEG", quality=75) + total += 1 + + print(f"Done. Generated {total} images, skipped {skipped} (missing account/password).") + print(f"Output: {OUTPUT_DIR}") + + +if __name__ == "__main__": + main() diff --git a/scripts/extract_group_members.py b/scripts/extract_group_members.py new file mode 100644 index 0000000..fe60b60 --- /dev/null +++ b/scripts/extract_group_members.py @@ -0,0 +1,167 @@ +"""从钉钉群拉取全部成员并查询工号/邮箱/手机等详细信息,输出 CSV""" + +import csv +import json +import os +import subprocess +import sys +import time + +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)))) +from config import DWS + +GROUP_ID = "cidrLgqKZUgbJ9PdVHgQQIS9g==" +OUTPUT_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data") +OUTPUT_CSV = os.path.join(OUTPUT_DIR, "group_members_detail.csv") +OUTPUT_JSON = os.path.join(OUTPUT_DIR, "group_members_all.json") + + +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 fetch_all_members(): + all_members = [] + cursor = "0" + page = 0 + while True: + page += 1 + args = ["chat", "group", "members", "list", "--id", GROUP_ID, "--cursor", cursor] + data = run_dws(args, timeout=60) + if not data or not data.get("success"): + print(f" page {page} failed: {data}") + break + result = data.get("result", {}) + members = result.get("list", []) + all_members.extend(members) + print(f" page {page}: {len(members)} members (total {len(all_members)})") + if not result.get("hasMore"): + break + cursor = str(result.get("nextCursor", "")) + if not cursor: + break + time.sleep(0.3) + return all_members + + +def search_user_id(name, open_dingtalk_id): + """通过姓名搜索获取 userId""" + args = ["contact", "user", "search", "--query", name] + data = run_dws(args, timeout=15) + if data and data.get("success"): + results = data.get("result", []) + for r in results: + if r.get("openDingTalkId") == open_dingtalk_id: + return r.get("userId", "") + if r.get("name") == name: + return r.get("userId", "") + if results: + return results[0].get("userId", "") + return "" + + +def batch_get_user_details(user_ids): + """批量获取用户详情""" + profiles = {} + batch_size = 20 + for i in range(0, len(user_ids), batch_size): + batch = [uid for uid in user_ids[i:i+batch_size] if uid] + if not batch: + continue + ids_str = ",".join(batch) + args = ["contact", "user", "get", "--ids", ids_str] + data = run_dws(args, timeout=30) + if data and data.get("success"): + result = data.get("result", []) + if isinstance(result, list): + for u in result: + model = u.get("orgEmployeeModel", {}) + uid = model.get("orgUserId", "") + if uid: + profiles[uid] = model + print(f" batch {i//batch_size+1}/{(len(user_ids)+batch_size-1)//batch_size}: got {len(profiles)} profiles") + time.sleep(0.3) + return profiles + + +def main(): + os.makedirs(OUTPUT_DIR, exist_ok=True) + + print("=" * 50) + print("Step 1: Fetch all group members") + print("=" * 50) + members = fetch_all_members() + print(f"\nTotal: {len(members)} members") + + with open(OUTPUT_JSON, "w", encoding="utf-8") as f: + json.dump(members, f, ensure_ascii=False, indent=2) + + print("\n" + "=" * 50) + print("Step 2: Resolve userId for each member via search") + print("=" * 50) + user_id_map = {} # openDingtalkId -> userId + for idx, m in enumerate(members, 1): + name = m.get("memberEmpName", "") + oid = m.get("openDingtalkId", "") + if name and oid: + uid = search_user_id(name, oid) + if uid: + user_id_map[oid] = uid + if idx % 50 == 0: + print(f" resolved {len(user_id_map)}/{idx} userIds") + time.sleep(0.15) + print(f" resolved {len(user_id_map)}/{len(members)} userIds total") + + print("\n" + "=" * 50) + print("Step 3: Batch query user profiles") + print("=" * 50) + all_user_ids = list(user_id_map.values()) + profiles = batch_get_user_details(all_user_ids) + + print("\n" + "=" * 50) + print("Step 4: Generate CSV") + print("=" * 50) + + fieldnames = ["序号", "姓名", "昵称", "工号", "企业邮箱", "职位", "部门", "userId", "角色", "openDingtalkId"] + rows = [] + for idx, m in enumerate(members, 1): + oid = m.get("openDingtalkId", "") + name = m.get("memberEmpName", "") + nick = m.get("memberNick", "") + role = m.get("memberRoleDesc", "") + + uid = user_id_map.get(oid, "") + p = profiles.get(uid, {}) + job_number = p.get("jobNumber", "") + email = p.get("orgAuthEmail", "") + title = p.get("orgTitle", "") + depts = p.get("depts", []) + dept_name = "/".join(d.get("deptName", "") for d in depts) if depts else "" + + rows.append({ + "序号": idx, "姓名": name, "昵称": nick, "工号": job_number, + "企业邮箱": email, "职位": title, "部门": dept_name, + "userId": uid, "角色": role, "openDingtalkId": oid, + }) + + with open(OUTPUT_CSV, "w", encoding="utf-8-sig", newline="") as f: + writer = csv.DictWriter(f, fieldnames=fieldnames) + writer.writeheader() + writer.writerows(rows) + + print(f"\nCSV saved: {OUTPUT_CSV}") + print(f"Total: {len(rows)} rows") + has_job = sum(1 for r in rows if r["工号"]) + has_email = sum(1 for r in rows if r["企业邮箱"]) + has_dept = sum(1 for r in rows if r["部门"]) + print(f"有工号: {has_job}, 有邮箱: {has_email}, 有部门: {has_dept}") + + +if __name__ == "__main__": + main() diff --git a/scripts/gen_account_html.py b/scripts/gen_account_html.py new file mode 100644 index 0000000..cd8b758 --- /dev/null +++ b/scripts/gen_account_html.py @@ -0,0 +1,55 @@ +import csv, os + +CSV_PATH = r"D:\desktop\feishu_docs_summary\data\group_members_with_account.csv" +HTML_PATH = r"D:\desktop\feishu_docs_summary\output\group_members_with_account.html" + +rows = [] +with open(CSV_PATH, encoding="utf-8-sig") as f: + for r in csv.DictReader(f): + rows.append(r) + +matched = sum(1 for r in rows if r.get("密码")) + +html = [] +html.append('') +html.append('厦门点触科技 - 群成员账号信息') +html.append("""""") +html.append('

厦门点触科技股份有限公司 - 群成员账号信息

') +html.append(f'

{len(rows)} 人 | 已匹配账号: {matched} | 未匹配: {len(rows)-matched}

') +html.append('
') +html.append('') +for i, h in enumerate(["序号","姓名","账号","密码","企业邮箱","部门","职位"]): + html.append(f'') +html.append('') + +for r in rows: + acct = r.get("账号","") + pwd = r.get("密码","") + pwd_d = str(pwd) if pwd else '-' + acct_d = acct if acct else '-' + html.append(f'') + +html.append("""
{h}
{r.get("序号","")}{r.get("姓名","")}{acct_d}{pwd_d}{r.get("企业邮箱","")}{r.get("部门","")}{r.get("职位","")}
+""") + +with open(HTML_PATH, "w", encoding="utf-8") as f: + f.write("\n".join(html)) +print(f"HTML saved: {HTML_PATH}") diff --git a/scripts/gen_invitation.py b/scripts/gen_invitation.py new file mode 100644 index 0000000..c027230 --- /dev/null +++ b/scripts/gen_invitation.py @@ -0,0 +1,62 @@ +"""根据 bg.png 模板生成带姓名/账号/密码的邀请函图片""" + +from PIL import Image, ImageDraw, ImageFont +import json +import os + +DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data") +BG_PATH = os.path.join(DATA_DIR, "bg.png") +FONT_PATH = os.path.join(DATA_DIR, "WenDaoGeTeSong.ttf") +OUTPUT_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "output", "invitations") + +# 文字位置配置(可通过 invitation_tuner.html 调整后粘贴回来) +CONFIG = { + "name": {"x": 582, "y": 1192, "size": 52, "color": "#ffd586"}, + "account": {"x": 572, "y": 1293, "size": 44, "color": "#ffffff"}, + "password": {"x": 575, "y": 1394, "size": 44, "color": "#ffffff"}, +} + + +def hex_to_rgb(hex_color): + hex_color = hex_color.lstrip('#') + return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) + + +def generate_invitation(name, account, password, output_path=None): + img = Image.open(BG_PATH).copy() + draw = ImageDraw.Draw(img) + + fields = { + "name": name, + "account": account, + "password": password, + } + + for field_key, text in fields.items(): + cfg = CONFIG[field_key] + font = ImageFont.truetype(FONT_PATH, cfg["size"]) + color = hex_to_rgb(cfg["color"]) + # textAlign center + textBaseline top 对应 Pillow 的 anchor='mt' + draw.text((cfg["x"], cfg["y"]), text, fill=color, font=font, anchor="mt") + + if not output_path: + os.makedirs(OUTPUT_DIR, exist_ok=True) + safe_name = name.replace("/", "_").replace("\\", "_") + output_path = os.path.join(OUTPUT_DIR, f"invite_{safe_name}.png") + + img.save(output_path, quality=95) + return output_path + + +if __name__ == "__main__": + import argparse + parser = argparse.ArgumentParser() + parser.add_argument("--name", default="测试用户") + parser.add_argument("--account", default="20250580") + parser.add_argument("--password", default="123456") + parser.add_argument("--output", default=None) + args = parser.parse_args() + + path = generate_invitation(args.name, args.account, args.password, args.output) + print(f"Generated: {path}") + print(f"Font: {FONT_PATH}") diff --git a/scripts/send_accounts.py b/scripts/send_accounts.py new file mode 100644 index 0000000..57c6600 --- /dev/null +++ b/scripts/send_accounts.py @@ -0,0 +1,209 @@ +"""根据群成员从账号数据库匹配账号密码,逐个私聊发送""" + +import csv +import json +import os +import subprocess +import sys +import time + +sys.path.insert(0, os.path.join(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") +ACCT_DB = os.path.join(DATA_DIR, "group_members_account.xlsx") +LOG_FILE = os.path.join(DATA_DIR, "send_account_log.json") +DEFAULT_GROUP_NAME = "账号发放测试" + + +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 load_account_db(): + import openpyxl + wb = openpyxl.load_workbook(ACCT_DB, read_only=True) + ws = wb["账号信息"] + acct_map = {} + for r in list(ws.iter_rows(values_only=True))[1:]: + t, acct, pwd = r[0], r[1], r[2] + if t is not None and acct is not None: + try: + acct_map[str(int(acct))] = str(int(pwd)) if pwd else "" + except: + acct_map[str(acct)] = str(pwd) + wb.close() + return acct_map + + +def fetch_all_members(group_id): + all_members = [] + cursor = "0" + while True: + args = ["chat", "group", "members", "list", "--id", group_id, "--cursor", cursor] + data = run_dws(args, 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 build_message(name, account, password): + return f"""## 隐秘之潮 - 试玩活动 + +{name},你好! + +项目试玩活动即将开启,以下是你的专属测试账号: + +| 项目 | 信息 | +|------|------| +| **账号** | {account} | +| **密码** | {password} | + +请妥善保管,勿转发给他人。 + +有问题欢迎在群里反馈。祝你玩得开心!""" + + +def send_message(open_dingtalk_id, title, text, dry_run=False): + if dry_run: + print(f" [DRY RUN] -> {open_dingtalk_id}") + return {"success": True, "dry_run": True} + args = [ + "chat", "message", "send", + "--open-dingtalk-id", open_dingtalk_id, + "--title", title, + "--text", text, + ] + return run_dws(args, timeout=15) + + +def main(): + import argparse + parser = argparse.ArgumentParser(description="群成员账号发放工具") + parser.add_argument("--group", type=str, help="群 openConversationId") + parser.add_argument("--group-name", type=str, default=DEFAULT_GROUP_NAME, help="群名 (默认: 账号发放测试)") + parser.add_argument("--dry-run", action="store_true", help="只预览不发送") + parser.add_argument("--limit", type=int, default=0, help="限制发送人数") + args = parser.parse_args() + + group_id = args.group + if not group_id and args.group_name: + print(f"搜索群: {args.group_name}") + data = run_dws(["chat", "search", "--query", args.group_name], timeout=15) + if data and data.get("success"): + groups = data["result"].get("groups", []) + if groups: + group_id = groups[0]["openConversationId"] + print(f" 找到: {groups[0]['title']} ({group_id})") + else: + print(" 未找到群") + return + if not group_id: + print("请指定 --group 或 --group-name <名称>") + return + + print("加载账号数据库...") + acct_db = load_account_db() + print(f" {len(acct_db)} 个账号") + + print("拉取群成员...") + members = fetch_all_members(group_id) + print(f" {len(members)} 人") + + print("解析成员工号...") + member_accounts = [] + no_match = [] + for idx, m in enumerate(members, 1): + name = m.get("memberEmpName", "") + oid = m.get("openDingtalkId", "") + nick = m.get("memberNick", "") + if not name or not oid: + no_match.append({"name": name, "reason": "无姓名或ID"}) + continue + + args_search = ["contact", "user", "search", "--query", name] + data = run_dws(args_search, timeout=15) + user_id = "" + if data and data.get("success"): + for r in data.get("result", []): + if r.get("openDingTalkId") == oid: + user_id = r.get("userId", "") + break + + job_number = "" + if user_id: + args_get = ["contact", "user", "get", "--ids", user_id] + data2 = run_dws(args_get, timeout=15) + if data2 and data2.get("success"): + for u in data2.get("result", []): + model = u.get("orgEmployeeModel", {}) + job_number = str(model.get("jobNumber", "")) + break + + if job_number and job_number in acct_db: + pwd = acct_db[job_number] + member_accounts.append({ + "name": name, "nick": nick, + "openDingtalkId": oid, "userId": user_id, + "account": job_number, "password": pwd, + }) + else: + no_match.append({"name": name, "job_number": job_number, "reason": "工号无匹配"}) + + if idx % 50 == 0: + print(f" {len(member_accounts)}/{idx} 已匹配") + time.sleep(0.15) + + print(f"\n匹配结果: {len(member_accounts)} 人有账号, {len(no_match)} 人无匹配") + + if args.limit > 0: + member_accounts = member_accounts[:args.limit] + print(f"限制发送: {args.limit} 人") + + title = "隐秘之潮 - 试玩活动" + success_count = 0 + fail_count = 0 + log = [] + + print(f"\n{'[DRY RUN] ' if args.dry_run else ''}开始发送...") + for idx, ma in enumerate(member_accounts, 1): + text = build_message(ma["name"], ma["account"], ma["password"]) + result = send_message(ma["openDingtalkId"], title, text, dry_run=args.dry_run) + status = "ok" if result and result.get("success") else "fail" + if status == "ok": + success_count += 1 + else: + fail_count += 1 + log.append({"name": ma["name"], "account": ma["account"], "status": status, "response": result}) + print(f" [{idx}/{len(member_accounts)}] {ma['name']} ({ma['account']}) -> {status}") + if not args.dry_run: + time.sleep(0.5) + + with open(LOG_FILE, "w", encoding="utf-8") as f: + json.dump(log, f, ensure_ascii=False, indent=2) + + print(f"\n完成! 成功: {success_count}, 失败: {fail_count}") + print(f"日志: {LOG_FILE}") + + if no_match: + print(f"\n未匹配人员 ({len(no_match)}):") + for nm in no_match[:10]: + print(f" {nm['name']}: {nm.get('reason','')}") + + +if __name__ == "__main__": + main() diff --git a/scripts/send_invitations.py b/scripts/send_invitations.py new file mode 100644 index 0000000..19fefa7 --- /dev/null +++ b/scripts/send_invitations.py @@ -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()