feat(tng): 自动拉红包历史/详情,并展示发放与领取时间

从 ILoginStorage 与官方历史页缓存请求模板,级联拉详情;领取台列表用发放时间而非拉取时间。
This commit is contained in:
mars
2026-08-04 16:20:13 +08:00
parent 1e2021d8fd
commit 1517ff4671
4 changed files with 873 additions and 128 deletions

View File

@@ -2,6 +2,7 @@
"""notiMessage 本地调试转发服务 — 在 PC 浏览器查看手机抓取的消息。"""
import json
import re
import threading
import time
from collections import deque
@@ -178,6 +179,11 @@ def _parse_mmp_content(content):
if "total" in meta:
total_num = _normalize_money(meta["total"])
meta["total"] = ("{0:.2f}".format(total_num) if total_num is not None else meta["total"])
# 发放时间issued= / createTime= / issuedAt=
for key in ("issued", "createTime", "issuedAt", "create"):
if meta.get(key):
meta["issued"] = meta[key]
break
for line in lines[1:]:
line = line.strip()
if not line or "->" not in line:
@@ -212,22 +218,28 @@ def _claims_fingerprint(claims):
def _aggregate_claims(claims):
"""同一快照内同昵称合并;跨快照不应再混加(外层只取最新快照)"""
"""同一快照内同昵称只保留一笔(取较大金额),红包每人只领一次,禁止累加导致翻倍"""
buckets = {}
order = []
for c in claims:
nick = c.get("nickname") or "?"
try:
amt = float(c.get("amountValue") if c.get("amountValue") is not None
else _normalize_money(c.get("amount")) or 0.0)
except (TypeError, ValueError):
amt = 0.0
if nick not in buckets:
buckets[nick] = {"nickname": nick, "amount": 0.0, "count": 0, "claimTime": ""}
buckets[nick] = {"nickname": nick, "amount": amt, "count": 1,
"claimTime": c.get("claimTime") or ""}
order.append(nick)
continue
b = buckets[nick]
b["count"] += 1
try:
b["amount"] += float(c.get("amountValue") if c.get("amountValue") is not None
else _normalize_money(c.get("amount")) or 0.0)
except (TypeError, ValueError):
pass
if c.get("claimTime") and not b["claimTime"]:
if amt >= b["amount"]:
b["amount"] = amt
if c.get("claimTime"):
b["claimTime"] = c.get("claimTime") or b["claimTime"]
elif c.get("claimTime") and not b["claimTime"]:
b["claimTime"] = c.get("claimTime") or ""
result = []
for nick in order:
@@ -259,6 +271,7 @@ def _mmp_packets():
# 用领取名单指纹区分不同红包
packet_id = "红包-" + _claims_fingerprint(claims)[:48]
key = str(packet_id)
issued = meta.get("issued") or ""
item = {
"packetId": key,
"title": msg.get("title") or "TNG 红包",
@@ -266,7 +279,9 @@ def _mmp_packets():
"group": meta.get("group") or "",
"total": meta.get("total") or "",
"via": meta.get("via") or "",
"latestAt": msg.get("receivedAt") or "",
"issuedAt": issued,
"fetchedAt": msg.get("receivedAt") or "",
"latestAt": issued or (msg.get("receivedAt") or ""),
"snapshots": 1,
"claims": claims,
"rawMessages": [{
@@ -277,15 +292,27 @@ def _mmp_packets():
}],
}
existing = packets.get(key)
if existing is None or (item["latestAt"] or "") >= (existing.get("latestAt") or ""):
# 优先保留「有发放时间 + 领取更全」的快照;时间戳用发放时间排序
def _score(it):
return (
1 if it.get("issuedAt") else 0,
len(it.get("claims") or []),
it.get("fetchedAt") or "",
)
if existing is None or _score(item) >= _score(existing):
if existing is not None:
item["snapshots"] = int(existing.get("snapshots") or 1) + 1
# 保留历史原始报文,但排行只用最新 claims
item["rawMessages"] = existing.get("rawMessages", []) + item["rawMessages"]
if not item.get("issuedAt") and existing.get("issuedAt"):
item["issuedAt"] = existing["issuedAt"]
item["latestAt"] = item["issuedAt"] or item.get("fetchedAt") or ""
packets[key] = item
else:
existing["snapshots"] = int(existing.get("snapshots") or 1) + 1
existing["rawMessages"].append(item["rawMessages"][0])
if not existing.get("issuedAt") and issued:
existing["issuedAt"] = issued
existing["latestAt"] = issued
result = []
for p in packets.values():
ranked = _aggregate_claims(p.get("claims") or [])
@@ -305,10 +332,19 @@ def _mmp_packets():
p["total"] = "{0:.2f}".format(total_num)
del p["claims"]
result.append(p)
result.sort(key=lambda x: x.get("latestAt") or "", reverse=True)
result.sort(key=lambda x: _mmp_time_sort_key(x.get("issuedAt") or x.get("latestAt") or ""), reverse=True)
return result
def _mmp_time_sort_key(text):
"""把 13/07/2026 11:54:44 / ISO 等统一成可比较字符串。"""
s = (text or "").strip()
m = re.match(r"^(\d{2})/(\d{2})/(\d{4})(?:\s+(\d{2}:\d{2}(?::\d{2})?))?", s)
if m:
return "{0}-{1}-{2} {3}".format(m.group(3), m.group(2), m.group(1), m.group(4) or "")
return s
def _mmp_html_page():
html = """<!DOCTYPE html>
<html lang="zh-CN">
@@ -371,7 +407,7 @@ def _mmp_html_page():
<main class="main">
<div id="empty" class="empty">
暂无红包数据<br/>
<div class="hint">打开 Money Packet 历史页可自动拉取;若没有数据,先手动点开任意一条详情一次(缓存登录模板),再回历史页</div>
<div class="hint">登录 TNG 后停留任意页面即可自动拉历史并拉详情;也可打开 Money Packet 历史页加速。无需再手动点详情。</div>
</div>
<div id="panel" style="display:none">
<h2 class="title" id="title">-</h2>
@@ -420,7 +456,7 @@ def _mmp_html_page():
div.onclick = () => { activeId = p.packetId; loadData(); };
div.innerHTML = `
<div class="pkt-id">${esc(shortId(p.packetId))} · ${esc((p.sender || '红包') + ' RM' + (p.sumClaimed ?? p.total ?? 0))}</div>
<div class="pkt-meta">${p.claimantCount || 0} 人领取 · ${esc(p.latestAt || '')}</div>`;
<div class="pkt-meta">${p.claimantCount || 0} 人领取 · ${esc(p.issuedAt ? ('发放 ' + p.issuedAt) : '发放时间未知')}</div>`;
list.appendChild(div);
}
renderActive();
@@ -440,6 +476,7 @@ def _mmp_html_page():
const parts = [];
if (p.packetId) parts.push('activityId ' + p.packetId);
if (p.sender) parts.push('发送人 ' + p.sender);
if (p.issuedAt) parts.push('发放 ' + p.issuedAt);
if (p.group) parts.push('群组 ' + p.group);
if (p.via) parts.push('来源 ' + (p.via === 'http' ? '网络' : p.via === 'gson' ? '解析' : p.via === 'rpc' ? 'RPC' : p.via));
document.getElementById('sub').textContent = parts.join(' · ') || '单个红包领取排行';