auto backup 2026-06-26 08:10:49

This commit is contained in:
ai_member_only 2026-06-26 08:10:49 +08:00
parent c81e1b532f
commit 6f80ba99e8
6 changed files with 135 additions and 30 deletions

View File

@ -43,3 +43,4 @@ lark-workflow-meeting-summary 7479f4b43979627fde453c48b5b50970a81030d4c74d62185f
feishu-group-monitor 8c3a459ae1383d51bc43e75e9ec39c910eb14406bda4d8c7875270058f1d3d15 feishu-group-monitor 8c3a459ae1383d51bc43e75e9ec39c910eb14406bda4d8c7875270058f1d3d15
lark-send-message-as-bot 5502e41e032a1b86adc91ebe42285735b12d17b9a6be8f5acb922a8a4fa6dad6 lark-send-message-as-bot 5502e41e032a1b86adc91ebe42285735b12d17b9a6be8f5acb922a8a4fa6dad6
gpt-image-2-generator e1842d74b29c63ba218764e8156a50a47aa0f3d7be540af54d6d2db9d59c7382 gpt-image-2-generator e1842d74b29c63ba218764e8156a50a47aa0f3d7be540af54d6d2db9d59c7382
vala-data-export 10b848c9c91084e4a8aab3c3c917225a07ceeaa5510537b40d0f7b5d2a40412f

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -56,7 +56,8 @@ with pg_conn.cursor(cursor_factory=RealDictCursor) as cur:
""", (user_ids,)) """, (user_ids,))
review_rows = cur.fetchall() review_rows = cur.fetchall()
# Parse question_list JSON for readable summary # Parse question_list JSON — explode each question into its own row
expanded_rows = [] # one row per question
for row in review_rows: for row in review_rows:
ql = row["question_list"] ql = row["question_list"]
if isinstance(ql, str): if isinstance(ql, str):
@ -64,17 +65,34 @@ for row in review_rows:
ql = json.loads(ql) ql = json.loads(ql)
except: except:
pass pass
questions = [] base = {
"角色ID": row["user_id"],
"Level": row["level"],
"Story ID": row["story_id"],
"Chapter ID": row["chapter_id"],
"Unique ID": row["unique_id"],
"更新时间": str(row["updated_at"]),
}
if isinstance(ql, list): if isinstance(ql, list):
for item in ql: for item in ql:
if isinstance(item, dict): if isinstance(item, dict):
q = item.get("question", {}) q = item.get("question", {})
qtype = q.get("type", "") qtype = q.get("type", "")
qtitle = q.get("title", "") qtitle = q.get("title", "")
user_answer = item.get("userAnswer", "") is_right = item.get("isRight")
score = item.get("score", "") status = "正确" if is_right is True else "错误"
questions.append(f"[{qtype}] {qtitle} | 回答: {user_answer} | 得分: {score}") audio_url = item.get("userAudio", "")
row["question_summary"] = "\n".join(questions) user_answer = item.get("userAnswer", "") or item.get("userAnswers", "")
if isinstance(user_answer, list):
user_answer = ", ".join(str(a) for a in user_answer)
expanded_rows.append({
**base,
"题目类型": qtype,
"题目内容": qtitle,
"题目对错": status,
"音频URL": audio_url,
"用户答案": user_answer,
})
row["question_count"] = len(ql) if isinstance(ql, list) else 0 row["question_count"] = len(ql) if isinstance(ql, list) else 0
pg_conn.close() pg_conn.close()
@ -149,26 +167,25 @@ from openpyxl.styles import Font, Alignment, PatternFill
wb = Workbook() wb = Workbook()
# Sheet 1: 课程巩固记录 # Sheet 1: 课程巩固记录(每题一行)
ws1 = wb.active ws1 = wb.active
ws1.title = "课程巩固记录" ws1.title = "课程巩固记录"
# Build DataFrame from expanded_rows
review_data = [] review_data = []
for row in review_rows: for row in expanded_rows:
review_data.append({ review_data.append({
"角色ID": row["user_id"], "角色ID": row["角色ID"],
"Level": row["level"], "Level": row["Level"],
"Story ID": row["story_id"], "Story ID": row["Story ID"],
"Chapter ID": row["chapter_id"], "Chapter ID": row["Chapter ID"],
"Unique ID": row["unique_id"], "Unique ID": row["Unique ID"],
"得分": row["score"], "题目类型": row["题目类型"],
"评级": row["score_text"], "题目内容": row["题目内容"],
"SP值": row["sp_value"], "题目对错": row["题目对错"],
"经验值": row["exp"], "音频URL": row["音频URL"],
"题目数": row["question_count"], "用户答案": row["用户答案"],
"耗时(秒)": row["play_time"], "更新时间": row["更新时间"],
"题目详情": row["question_summary"],
"更新时间": str(row["updated_at"]),
"创建时间": str(row["created_at"]),
}) })
df1 = pd.DataFrame(review_data) df1 = pd.DataFrame(review_data)
@ -186,8 +203,16 @@ for cell in ws1[1]:
# Column widths # Column widths
ws1.column_dimensions["A"].width = 10 ws1.column_dimensions["A"].width = 10
ws1.column_dimensions["L"].width = 12 ws1.column_dimensions["B"].width = 8
ws1.column_dimensions["M"].width = 60 ws1.column_dimensions["C"].width = 12
ws1.column_dimensions["D"].width = 12
ws1.column_dimensions["E"].width = 36
ws1.column_dimensions["F"].width = 24
ws1.column_dimensions["G"].width = 30
ws1.column_dimensions["H"].width = 10
ws1.column_dimensions["I"].width = 55
ws1.column_dimensions["J"].width = 30
ws1.column_dimensions["K"].width = 22
# Sheet 2: 音频数据 # Sheet 2: 音频数据
ws2 = wb.create_sheet("音频数据") ws2 = wb.create_sheet("音频数据")
@ -244,13 +269,15 @@ ws3["A3"] = "导出时间"
ws3["B3"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") ws3["B3"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
ws3["A4"] = "角色ID" ws3["A4"] = "角色ID"
ws3["B4"] = ", ".join(str(u) for u in user_ids) ws3["B4"] = ", ".join(str(u) for u in user_ids)
ws3["A5"] = "课程巩固记录数" ws3["A5"] = "课程巩固记录数(题目拆分行)"
ws3["B5"] = len(review_rows) ws3["B5"] = len(expanded_rows)
ws3["A6"] = "音频记录数" ws3["A6"] = "原始巩固回合数"
ws3["B6"] = len(audio_rows) ws3["B6"] = len(review_rows)
ws3["A7"] = "音频记录数"
ws3["B7"] = len(audio_rows)
# Per-user breakdown # Per-user breakdown
row_offset = 8 row_offset = 10
ws3[f"A{row_offset}"] = "按角色统计" ws3[f"A{row_offset}"] = "按角色统计"
ws3[f"A{row_offset}"].font = Font(bold=True) ws3[f"A{row_offset}"].font = Font(bold=True)
row_offset += 1 row_offset += 1
@ -284,6 +311,83 @@ ws3.column_dimensions["D"].width = 28
wb.save(output_path) wb.save(output_path)
print(f"\n✅ 导出完成: {output_path}") print(f"\n✅ 导出完成: {output_path}")
print(f" Sheet 1 — 课程巩固记录: {len(review_rows)}") print(f" Sheet 1 — 课程巩固记录(每题一行): {len(expanded_rows)}")
print(f" Sheet 2 — 音频数据: {len(audio_rows)}") print(f" Sheet 2 — 音频数据: {len(audio_rows)}")
print(f" Sheet 3 — 汇总") print(f" Sheet 3 — 汇总")
# ── 4. 自动通过飞书发送文件 ──────────────────────────
# 默认发送给李若松 (user_id: 4aagb443)
# 可通过环境变量 FEISHU_SEND_TO 覆盖目标 user_id
SEND_TO = os.environ.get("FEISHU_SEND_TO", "4aagb443")
SEND_ENABLED = os.environ.get("FEISHU_SEND_ENABLED", "1")
if SEND_ENABLED != "1":
print("\n⏭️ 飞书自动发送已禁用 (FEISHU_SEND_ENABLED != 1)")
sys.exit(0)
print(f"\n[4/4] 通过飞书发送文件到 user_id={SEND_TO}...")
FEISHU_APP_ID = g("FEISHU_APP_ID")
FEISHU_APP_SECRET = g("FEISHU_APP_SECRET")
if not FEISHU_APP_ID or not FEISHU_APP_SECRET:
print(" ⚠️ 未找到 FEISHU_APP_ID/FEISHU_APP_SECRET跳过发送")
sys.exit(0)
def feishu_api(method, path, **kwargs):
"""调用飞书 Open API返回解析后的 JSON"""
cmd = ["curl", "-sk", "--connect-timeout", "10", "--max-time", "60"]
if method == "POST":
cmd += ["-X", "POST"]
cmd += ["-H", f"Authorization: Bearer {kwargs.get('token', '')}"]
if "json_data" in kwargs:
cmd += ["-H", "Content-Type: application/json", "-d", json.dumps(kwargs["json_data"])]
if "form_fields" in kwargs:
for key, val in kwargs["form_fields"].items():
cmd += ["-F", f"{key}={val}"]
cmd.append(f"https://open.feishu.cn{path}")
r = subprocess.run(cmd, capture_output=True, text=True, timeout=65)
try:
return json.loads(r.stdout)
except:
print(f" ❌ API 返回解析失败: {r.stdout[:200]}")
return {"code": -1, "msg": r.stdout[:200]}
# Step 1: 获取 tenant_access_token
print(" → 获取 tenant_access_token...")
token_resp = feishu_api("POST", "/open-apis/auth/v3/tenant_access_token/internal",
json_data={"app_id": FEISHU_APP_ID, "app_secret": FEISHU_APP_SECRET})
token = token_resp.get("tenant_access_token")
if not token:
print(f" ❌ 获取 token 失败: {token_resp}")
sys.exit(1)
print(f" → Token 获取成功")
# Step 2: 上传文件
file_name = os.path.basename(output_path)
file_ext = os.path.splitext(output_path)[1].lower()
file_type_map = {".xlsx": "xls", ".xls": "xls", ".pdf": "pdf", ".docx": "doc", ".pptx": "ppt"}
file_type = file_type_map.get(file_ext, "stream")
print(f" → 上传文件: {file_name} (type={file_type})...")
upload_resp = feishu_api("POST", "/open-apis/im/v1/files", token=token,
form_fields={"file_type": file_type, "file_name": file_name, "file": f"@{output_path}"})
file_key = upload_resp.get("data", {}).get("file_key")
if not file_key:
print(f" ❌ 文件上传失败: {upload_resp}")
sys.exit(1)
print(f" → 上传成功: {file_key}")
# Step 3: 发送文件消息
print(f" → 发送文件到 user_id={SEND_TO}...")
send_resp = feishu_api("POST", "/open-apis/im/v1/messages?receive_id_type=user_id", token=token,
json_data={
"receive_id": SEND_TO,
"msg_type": "file",
"content": json.dumps({"file_key": file_key})
})
msg_id = send_resp.get("data", {}).get("message_id")
if msg_id:
print(f" ✅ 文件已发送message_id={msg_id}")
else:
print(f" ⚠️ 发送可能失败: {send_resp}")