Commit c0788f1cf7c6192d3012a82cb77838060eb44800
1 parent
419270a7
chore: wechat_key_* 只留本地不入库;device/ 重新同步
- .gitignore 加 .wechat_key_*,从索引移除 wechat-viewer/.wechat_key_*(本地文件保留,与手机一致) - sync-device.sh 把密钥只拉到本地(device/wechat-viewer/,被忽略) - device/ 快照更新(日报链新增 digest_gap.py 等) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Showing
11 changed files
with
243 additions
and
48 deletions
.gitignore
device/.dsh/skills/wechat-group-digest/SKILL.md
| ... | ... | @@ -106,7 +106,7 @@ WeChat Server |
| 106 | 106 | |
| 107 | 107 | ### Path F: send_text_via_hook — VIABLE & DEPLOYED |
| 108 | 108 | |
| 109 | -`send_text_via_hook(wxid: str, text: str) → dict` in `~/pixelphone/event_bus/wechat_hook_send.py`. Sends text via WeChatHook's LSPosed internal broadcast `compixelphone.wechat.SEND_MSG_INTERNAL`. No ADBKeyboard dependency, no UI disruption, sub-second round-trip, CJK-capable. | |
| 109 | +`send_text_via_hook(wxid: str, text: str) → dict` in `~/pixelphone/event_bus/wechat_hook_send.py`. Sends text via WeChatHook's LSPosed internal broadcast `com.hermes.wechat.SEND_MSG_INTERNAL`. No ADBKeyboard dependency, no UI disruption, sub-second round-trip, CJK-capable. | |
| 110 | 110 | |
| 111 | 111 | ```python |
| 112 | 112 | import sys |
| ... | ... | @@ -433,7 +433,7 @@ result = send_text_via_hook("wxid_or_chatroom_id", "消息内容") |
| 433 | 433 | # → {"status": "sent", "to": "...", "method": "hook_broadcast", ...} |
| 434 | 434 | ``` |
| 435 | 435 | |
| 436 | -Works for both 1-on-1 and chatroom wxids. Uses `compixelphone.wechat.SEND_MSG_INTERNAL` broadcast. | |
| 436 | +Works for both 1-on-1 and chatroom wxids. Uses `com.hermes.wechat.SEND_MSG_INTERNAL` broadcast. | |
| 437 | 437 | |
| 438 | 438 | ### Fallback: ADBKeyboard Workflow (⚠️ LEGACY — 仅作备用) |
| 439 | 439 | ... | ... |
device/README.md
| ... | ... | @@ -11,4 +11,6 @@ |
| 11 | 11 | | `~/.termux/boot` | `.termux/` | Termux:Boot 脚本 | |
| 12 | 12 | | `crontab -l` | `crontab.txt` | 定时任务 | |
| 13 | 13 | |
| 14 | +不入库(本地同步):`wechat-viewer/.wechat_key_*`(已在 .gitignore,sync 时只拉到本地)。 | |
| 15 | + | |
| 14 | 16 | 不拉:`.db`、日志、`.env`、`.hermes_env.sh`、`.secrets.env`、`config.py`、`ocr_models`、备份。脚本末尾会扫一遍疑似密钥,命中即中止。 | ... | ... |
device/pixelphone/digest/wechat-group-digest/SKILL.md
| ... | ... | @@ -106,7 +106,7 @@ WeChat Server |
| 106 | 106 | |
| 107 | 107 | ### Path F: send_text_via_hook — VIABLE & DEPLOYED |
| 108 | 108 | |
| 109 | -`send_text_via_hook(wxid: str, text: str) → dict` in `~/pixelphone/event_bus/wechat_hook_send.py`. Sends text via WeChatHook's LSPosed internal broadcast `compixelphone.wechat.SEND_MSG_INTERNAL`. No ADBKeyboard dependency, no UI disruption, sub-second round-trip, CJK-capable. | |
| 109 | +`send_text_via_hook(wxid: str, text: str) → dict` in `~/pixelphone/event_bus/wechat_hook_send.py`. Sends text via WeChatHook's LSPosed internal broadcast `com.hermes.wechat.SEND_MSG_INTERNAL`. No ADBKeyboard dependency, no UI disruption, sub-second round-trip, CJK-capable. | |
| 110 | 110 | |
| 111 | 111 | ```python |
| 112 | 112 | import sys |
| ... | ... | @@ -433,7 +433,7 @@ result = send_text_via_hook("wxid_or_chatroom_id", "消息内容") |
| 433 | 433 | # → {"status": "sent", "to": "...", "method": "hook_broadcast", ...} |
| 434 | 434 | ``` |
| 435 | 435 | |
| 436 | -Works for both 1-on-1 and chatroom wxids. Uses `compixelphone.wechat.SEND_MSG_INTERNAL` broadcast. | |
| 436 | +Works for both 1-on-1 and chatroom wxids. Uses `com.hermes.wechat.SEND_MSG_INTERNAL` broadcast. | |
| 437 | 437 | |
| 438 | 438 | ### Fallback: ADBKeyboard Workflow (⚠️ LEGACY — 仅作备用) |
| 439 | 439 | ... | ... |
device/pixelphone/digest/wechat-group-digest/scripts/check_digest_completeness.sh
0 → 100755
| 1 | +#!/data/data/com.termux/files/usr/bin/sh | |
| 2 | +# 日报出图前摘要完整度闸口 | |
| 3 | +# 2026-10-03 火星人指令:出图前检查摘要完整程度,别再缺文章 | |
| 4 | +# 2026-10-04 修订(需求 reczz28JC9dkrLDn):区分 pending「未排上」与 failed「摘要失败」 | |
| 5 | +# - pending>0 ⇒ 阻塞(exit 1):摘要还没排上,先补摘要再出图 | |
| 6 | +# - failed 全为失败 ⇒ 放行(exit 0):摘要抓不出来不再拦日报(failed 文章已收录于正文「缺摘要文章」) | |
| 7 | +# - JSON 新增 failed 计数与 failed_titles;修掉末行未定义 $MISMATCH 与 su 下 curl/python3 找不到的问题 | |
| 8 | +# 用法: check_digest_completeness.sh <group_key> <date> | |
| 9 | +# 例: check_digest_completeness.sh tgo 2026-10-02 | |
| 10 | +# 退出码: 0=可出图 1=本日报窗口仍有 pending(未排上)摘要 2=前置产物缺失 | |
| 11 | +# 输出: 单行 JSON(调度器可读,人也可读) | |
| 12 | + | |
| 13 | +GROUP="${1:-tgo}" | |
| 14 | +DATE="$2" | |
| 15 | +if [ -z "$DATE" ]; then | |
| 16 | + echo '{"ok":false,"reason":"usage: check_digest_completeness.sh <group> <date>"}' | |
| 17 | + exit 2 | |
| 18 | +fi | |
| 19 | + | |
| 20 | +HOME_DIR=/data/data/com.termux/files/home | |
| 21 | +# su 环境下 termux bin 不在 PATH ⇒ 用全路径(2026-10-04 实测裸 curl/python3 "inaccessible or not found") | |
| 22 | +PY=/data/data/com.termux/files/usr/bin/python3 | |
| 23 | +CURL=/data/data/com.termux/files/usr/bin/curl | |
| 24 | +if [ "$GROUP" = "nmm" ]; then | |
| 25 | + MD="$HOME_DIR/daily_digest_nmm_${DATE}.md" | |
| 26 | +else | |
| 27 | + MD="$HOME_DIR/daily_digest_${GROUP}_all_${DATE}.md" | |
| 28 | +fi | |
| 29 | + | |
| 30 | +if [ ! -f "$MD" ]; then | |
| 31 | + echo "{\"ok\":false,\"reason\":\"md missing: $MD\",\"action\":\"run gen_agg_digest.py --group $GROUP --date $DATE first\"}" | |
| 32 | + exit 2 | |
| 33 | +fi | |
| 34 | + | |
| 35 | +# --- 判定1(阻塞): md 头部「内容未就绪 N 篇」= pending(未排上,需阻塞) --- | |
| 36 | +N_PENDING=0 | |
| 37 | +READY_LINE=$(grep "内容未就绪" "$MD" | tail -1) | |
| 38 | +if [ -n "$READY_LINE" ]; then | |
| 39 | + N_PENDING=$(echo "$READY_LINE" | sed 's/.*内容未就绪 \([0-9]*\) 篇.*/\1/') | |
| 40 | + case "$N_PENDING" in | |
| 41 | + ''|*[!0-9]*) N_PENDING=0 ;; # 解析失败按0,不误拦 | |
| 42 | + esac | |
| 43 | +fi | |
| 44 | + | |
| 45 | +# --- 判定1b(非阻塞): md 头部「缺摘要(失败)N 篇:标题...」= failed(摘要失败,不阻塞) --- | |
| 46 | +N_FAILED=0 | |
| 47 | +FAILED_TITLES="" | |
| 48 | +FAILED_LINE=$(grep "缺摘要(失败)" "$MD" | tail -1) | |
| 49 | +if [ -n "$FAILED_LINE" ]; then | |
| 50 | + N_FAILED=$(echo "$FAILED_LINE" | sed 's/.*缺摘要(失败)\([0-9]*\) 篇.*/\1/') | |
| 51 | + case "$N_FAILED" in | |
| 52 | + ''|*[!0-9]*) N_FAILED=0 ;; # 解析失败按0,不误拦 | |
| 53 | + esac | |
| 54 | + FAILED_TITLES=$(echo "$FAILED_LINE" | sed 's/.*://' | sed 's/"/\\"/g') | |
| 55 | +fi | |
| 56 | + | |
| 57 | +if [ "$N_PENDING" -gt 0 ]; then | |
| 58 | + echo "{\"ok\":false,\"not_ready_in_digest\":$N_PENDING,\"failed\":$N_FAILED,\"reason\":\"md 头部: 内容未就绪(pending) $N_PENDING 篇\",\"action\":\"补摘要(POST /api/articles/summarize, 间隔40-60s, 并发<=3) -> 重跑 gen_agg_digest --group $GROUP --date $DATE -> 再出图\"}" | |
| 59 | + exit 1 | |
| 60 | +fi | |
| 61 | + | |
| 62 | +# --- 判定2(非阻塞参考): viewer 全库未摘要数(可能含日报窗口外的新文章) --- | |
| 63 | +N_COMPLETE=$(grep -o "完整内容:[0-9]* 篇" "$MD" | grep -o "[0-9]*" | head -1) | |
| 64 | +[ -z "$N_COMPLETE" ] && N_COMPLETE=0 | |
| 65 | +UNSUM=$("$CURL" -s --max-time 5 "http://127.0.0.1:5001/api/articles/unsummarized?limit=200") | |
| 66 | +N_UNSUM=$(echo "$UNSUM" | "$PY" -c "import json,sys | |
| 67 | +try: print(len(json.load(sys.stdin).get('data') or [])) | |
| 68 | +except Exception: print(0)") | |
| 69 | +[ -z "$N_UNSUM" ] && N_UNSUM=0 | |
| 70 | +echo "{\"ok\":true,\"complete_in_digest\":$N_COMPLETE,\"unsummarized_total\":$N_UNSUM,\"pending\":$N_PENDING,\"failed\":$N_FAILED,\"failed_titles\":\"$FAILED_TITLES\"}" | |
| 71 | +exit 0 | ... | ... |
device/pixelphone/digest/wechat-group-digest/scripts/digest_gap.py
0 → 100644
| 1 | +#!/usr/bin/env python3 | |
| 2 | +"""核对「日报自报消息数 vs DB 现值」(只读)。用法: digest_gap.py <组> <日期>(wechat-viewer 需在线) | |
| 3 | +微信入库可滞后数小时:01:00 生成时拿到的消息可能比最终少。差>0 ⇒ 此刻图/HTML/发送都还没发生,重跑 gen 零副作用。 | |
| 4 | +退出码: 0=差0(收工) 3=DB 比日报多(该重跑 gen) 2=无法核对(缺组配置/缺日报,不当作有差) | |
| 5 | +""" | |
| 6 | +import json | |
| 7 | +import re | |
| 8 | +import sys | |
| 9 | +import urllib.parse | |
| 10 | +import urllib.request | |
| 11 | + | |
| 12 | +BASE = "http://127.0.0.1:5001" | |
| 13 | +HOME = "/data/data/com.termux/files/home" | |
| 14 | +group, date = sys.argv[1], sys.argv[2] | |
| 15 | + | |
| 16 | +rooms = list((((json.load(open(f"{HOME}/pixelphone/group_profiles.json")).get("groups") or {}).get(group) or {}).get("chatrooms")) or {}) | |
| 17 | +if not rooms: | |
| 18 | + print(f"group={group}: group_profiles.json 里没有 chatrooms,无法核对") | |
| 19 | + sys.exit(2) | |
| 20 | + | |
| 21 | +tot = 0 | |
| 22 | +for room in rooms: # 日报「消息总数」= 各群逐群累加(非去重),与这里同口径 | |
| 23 | + u = f"{BASE}/api/messages?talker={urllib.parse.quote(room)}&date={date}" | |
| 24 | + with urllib.request.urlopen(u, timeout=25) as r: | |
| 25 | + tot += len(json.load(r).get("data") or []) | |
| 26 | + | |
| 27 | +md = f"{HOME}/daily_digest_{'tgo_all' if group == 'tgo' else group}_{date}.md" | |
| 28 | +try: | |
| 29 | + m = re.search(r"消息总数:(\d+) 条", open(md).read()) | |
| 30 | + rep = int(m.group(1)) if m else -1 | |
| 31 | +except OSError: | |
| 32 | + rep = -1 | |
| 33 | +if rep < 0: | |
| 34 | + print(f"group={group} date={date}: 读不到日报自报条数 ({md})") | |
| 35 | + sys.exit(2) | |
| 36 | + | |
| 37 | +print(f"group={group} date={date} 日报自报={rep} DB现值={tot} 差={tot - rep}") | |
| 38 | +sys.exit(3 if tot > rep else 0) | ... | ... |
device/pixelphone/event_bus/wechat_hook_send.py
| ... | ... | @@ -24,9 +24,9 @@ from typing import Any |
| 24 | 24 | |
| 25 | 25 | from ui_control_tools import _run, _adb_prefix, _ON_DEVICE |
| 26 | 26 | |
| 27 | -SEND_ACTION = "compixelphone.wechat.SEND_MSG_INTERNAL" | |
| 28 | -SEND_IMAGE_ACTION = "compixelphone.wechat.SEND_IMAGE_INTERNAL" | |
| 29 | -SEND_VOICE_CGI_ACTION = "compixelphone.wechat.SEND_VOICE_CGI" | |
| 27 | +SEND_ACTION = "com.hermes.wechat.SEND_MSG_INTERNAL" | |
| 28 | +SEND_IMAGE_ACTION = "com.hermes.wechat.SEND_IMAGE_INTERNAL" | |
| 29 | +SEND_VOICE_CGI_ACTION = "com.hermes.wechat.SEND_VOICE_CGI" | |
| 30 | 30 | _WECHAT_PKG = "com.tencent.mm" |
| 31 | 31 | _WECHAT_MAIN = "com.tencent.mm/.ui.LauncherUI" |
| 32 | 32 | |
| ... | ... | @@ -441,7 +441,7 @@ def send_voice_via_hook(wxid: str, silk_path: str, duration_ms: int = 3000) -> d |
| 441 | 441 | |
| 442 | 442 | Protocol: |
| 443 | 443 | 1. Push SILK V3 file to WeChat's cache dir (root-writable). |
| 444 | - 2. Fire compixelphone.wechat.SEND_VOICE_INTERNAL broadcast (root-only, | |
| 444 | + 2. Fire com.hermes.wechat.SEND_VOICE_INTERNAL broadcast (root-only, | |
| 445 | 445 | RECEIVER_NOT_EXPORTED). The hook generates a random 26-char voiceId, |
| 446 | 446 | computes voice2/{md5(id)[0:2]}/{md5(id)[2:4]}/msg_{id}.amr path, |
| 447 | 447 | copies from cache, inserts the DB row, and calls H() for server sync. |
| ... | ... | @@ -479,7 +479,7 @@ def send_voice_via_hook(wxid: str, silk_path: str, duration_ms: int = 3000) -> d |
| 479 | 479 | |
| 480 | 480 | # Stage 2: fire the voice broadcast |
| 481 | 481 | cmd = ( |
| 482 | - f"am broadcast -a compixelphone.wechat.SEND_VOICE_INTERNAL " | |
| 482 | + f"am broadcast -a com.hermes.wechat.SEND_VOICE_INTERNAL " | |
| 483 | 483 | f"--es to {wxid} " |
| 484 | 484 | f"--es src_path {wechat_cache} " |
| 485 | 485 | f"--ei duration {duration_ms}" | ... | ... |
device/pixelphone/scripts/daily-digest.sh
| 1 | 1 | #!/data/data/com.termux/files/usr/bin/bash |
| 2 | -# TGO 8群聚合日报定时链(取代 Hermes「调度器」的日报三段)。用法: daily-digest.sh gen|img|send [YYYY-MM-DD] | |
| 3 | -# 日期默认昨天;标记 $TMPDIR/scheduler_daily_<stage>_<今日> 防重,失败不写标记(下个 cron 窗口重试)。 | |
| 2 | +# 日报定时链(tgo + nmm 两组;恢复旧 Hermes 调度器的全量功能)。用法: daily-digest.sh gen|img|send [YYYY-MM-DD] | |
| 3 | +# gen : 01 点 生成聚合日报 md 并注册 report | |
| 4 | +# img : 02 点 摘要完整度闸口 → 出图 → 叠二维码 → 媒体上传 → 生成 HTML(两组串行:WanJie 图片有并发限制) | |
| 5 | +# send: 03 点 发带码 2x 图(目标读 group_profiles.json:tgo→yibooo 私聊,nmm→room 群)→ 按 hook 日志对账 | |
| 6 | +# 日期默认昨天;标记 $TMPDIR/scheduler_daily_<阶段>_<组>_<今日> 防重,失败不写标记(窗口内 cron 每 10 分钟重试)。 | |
| 4 | 7 | export PATH=/data/data/com.termux/files/usr/bin:/system/bin:$PATH HOME=${HOME:-/data/data/com.termux/files/home} |
| 5 | 8 | export TMPDIR=${TMPDIR:-/data/data/com.termux/files/usr/tmp} |
| 6 | 9 | [ -f "$HOME/.hermes_env.sh" ] && . "$HOME/.hermes_env.sh" # WANJIE/OSS key(不回显) |
| 7 | -D=$HOME/pixelphone/digest | |
| 8 | -Y=${2:-$(date -d yesterday +%Y-%m-%d)} | |
| 9 | -M="$TMPDIR/scheduler_daily_${1}_$(date +%Y%m%d)" | |
| 10 | -RID=$HOME/daily_digest_tgo_all.report_id | |
| 11 | -MD=$HOME/daily_digest_tgo_all_$Y.md | |
| 12 | -PNG=$HOME/daily_digest_tgo_all_$Y.png | |
| 13 | -[ -s "$M" ] && { echo "$1 already done"; exit 0; } | |
| 14 | -case "$1" in | |
| 15 | - gen) | |
| 16 | - python3 $D/wechat-group-digest/scripts/gen_agg_digest.py --date "$Y" || exit 1 | |
| 17 | - [ -s "$RID" ] && echo "done $(date +%s)" > "$M" ;; | |
| 18 | - img) | |
| 19 | - [ -f "$MD" ] || { echo "no $MD, skip"; exit 1; } | |
| 20 | - python3 $D/gpt-image-gen/scripts/gen_daily_1tgo_image.py \ | |
| 21 | - --content-file "$MD" --report-id "$(cat $RID)" --output "$PNG" \ | |
| 22 | - --title "tgo和他的朋友们的 8 个群日报" --style watercolor || exit 1 | |
| 23 | - [ -s "$PNG" ] && echo "done $(date +%s)" > "$M" ;; | |
| 24 | - send) | |
| 25 | - [ -f "$PNG" ] || { echo "no $PNG, skip"; exit 1; } | |
| 26 | - cd $HOME/pixelphone/event_bus && python3 - "$PNG" "$M" <<'PY' | |
| 27 | -import sys, time, json | |
| 28 | -from wechat_hook_send import check_wechat_ready, send_image_via_hook | |
| 29 | -r = check_wechat_ready() | |
| 30 | -if not r.get("ready"): | |
| 31 | - print("wechat not ready:", json.dumps(r, ensure_ascii=False)); sys.exit(1) | |
| 32 | -r = send_image_via_hook("yibooo", sys.argv[1]) | |
| 33 | -print(json.dumps(r, ensure_ascii=False)) | |
| 34 | -if r.get("status") in ("ok", "sent", "success") or r.get("ok"): | |
| 35 | - open(sys.argv[2], "w").write(f"done {int(time.time())}\n") | |
| 36 | -else: | |
| 37 | - sys.exit(1) | |
| 38 | -PY | |
| 39 | - ;; | |
| 40 | - *) echo "usage: $0 gen|img|send [date]"; exit 2 ;; | |
| 41 | -esac | |
| 10 | +D=$HOME/pixelphone/digest; WG=$D/wechat-group-digest/scripts; IG=$D/gpt-image-gen/scripts | |
| 11 | +STAGE=$1; Y=${2:-$(date -d yesterday +%Y-%m-%d)}; TODAY=$(date +%Y%m%d) | |
| 12 | + | |
| 13 | +meta() { # $1=组 → P 产物前缀 / RID report_id 文件 / TITLE / STYLE / QR 叠码参数 / TARGET 发送目标(与 group_profiles.json 一致) | |
| 14 | + case "$1" in | |
| 15 | + tgo) P=$HOME/daily_digest_tgo_all; RID=$HOME/daily_digest_tgo_all.report_id; TITLE="tgo和他的朋友们的 8 个群日报" | |
| 16 | + STYLE=editorial; QR="--position top-right --size 150"; TARGET=yibooo ;; | |
| 17 | + nmm) P=$HOME/daily_digest_nmm; RID=$HOME/daily_digest_nmm.report_id; TITLE="柠萌萌AI日报" | |
| 18 | + STYLE=watercolor; QR=""; TARGET=34576206251@chatroom ;; | |
| 19 | + esac | |
| 20 | +} | |
| 21 | + | |
| 22 | +# 对账:send_digest_group.py 不看结果就写标记,所以以 LSPosed 日志里「oj() DONE to=<目标>」为准(T0 之后)。 | |
| 23 | +# ponytail: 只认目标+时间,不核对条数;上限=同一目标 T0 后重复发送也算成功。 | |
| 24 | +sent_ok() { # $1=目标 $2=T0(epoch) | |
| 25 | + # 日志会轮转(刚轮转时最新文件几乎是空的)⇒ 同时看最近两个文件 | |
| 26 | + local logs t0s; logs=$(su -c 'ls -t /data/adb/lspd/log/verbose_*.log | head -2') || return 1 | |
| 27 | + t0s=$(date -d "@$2" +%Y-%m-%dT%H:%M:%S) | |
| 28 | + # 日志刷得很快(几分钟就上千行),不能靠 tail 取尾部:直接 grep 目标再按时间过滤 | |
| 29 | + su -c "grep -h 'oj() DONE to=$1' $(echo $logs)" 2>/dev/null | awk -v t0="$t0s" \ | |
| 30 | + '{ if (match($0, /20[0-9][0-9]-[0-9-]+T[0-9:]+/) && substr($0, RSTART, 19) >= t0) f=1 } END { exit !f }' | |
| 31 | +} | |
| 32 | + | |
| 33 | +[ -n "$DIGEST_SOURCE_ONLY" ] && return 0 2>/dev/null # 仅供自检 source | |
| 34 | + | |
| 35 | +case "$STAGE" in gen|img|send) ;; *) echo "usage: $0 gen|img|send [date]"; exit 2 ;; esac | |
| 36 | +# 防并发:cron 每 10 分钟重试而出图要好几分钟;锁目录超过 40 分钟视为残留 | |
| 37 | +LOCK=$TMPDIR/daily-digest.$STAGE.lock | |
| 38 | +if ! mkdir "$LOCK" 2>/dev/null; then | |
| 39 | + [ -n "$(find "$LOCK" -maxdepth 0 -mmin +40 2>/dev/null)" ] && { rmdir "$LOCK"; mkdir "$LOCK"; } || { echo "$STAGE running, skip"; exit 0; } | |
| 40 | +fi | |
| 41 | +trap 'rmdir "$LOCK" 2>/dev/null' EXIT | |
| 42 | +# 原图保存(orig-save.sh)在用微信界面时,发送要等下一轮(发图需要一次前台切换 flush,会互相打断) | |
| 43 | +if [ "$STAGE" = send ] && [ -d "$TMPDIR/orig-save.lock" ]; then echo "orig-save 在跑,发送等下一轮"; exit 1; fi | |
| 44 | + | |
| 45 | +rc=0 | |
| 46 | +for G in tgo nmm; do | |
| 47 | + meta $G; M="$TMPDIR/scheduler_daily_${STAGE}_${G}_$TODAY" | |
| 48 | + [ -s "$M" ] && { echo "$G $STAGE already done"; continue; } | |
| 49 | + case "$STAGE" in | |
| 50 | + gen) | |
| 51 | + python3 $WG/gen_agg_digest.py --date "$Y" --group $G && [ -s "$RID" ] || { echo "$G gen failed"; rc=1; continue; } | |
| 52 | + # 漏收防线:微信入库可滞后数小时,日报自报 < DB 现值 ⇒ 此刻图/HTML/发送都还没发生,重跑 gen 零副作用 | |
| 53 | + # (重跑会再注册一条 report,删掉旧的,以最新 report_id 为准) | |
| 54 | + python3 $WG/digest_gap.py $G "$Y"; GAP=$? | |
| 55 | + if [ $GAP -eq 3 ]; then | |
| 56 | + OLD=$(cat $RID); echo "$G: DB 比日报多,重跑 gen" | |
| 57 | + if python3 $WG/gen_agg_digest.py --date "$Y" --group $G && [ "$(cat $RID)" != "$OLD" ]; then | |
| 58 | + curl -s -m 20 -X POST -H 'Content-Type: application/json' -d '{}' http://127.0.0.1:5001/api/report/$OLD/delete >/dev/null | |
| 59 | + fi | |
| 60 | + fi | |
| 61 | + echo "done $(date +%s)" > "$M" ;; | |
| 62 | + img) | |
| 63 | + [ -f "${P}_$Y.md" ] || { echo "$G: no md, skip"; rc=1; continue; } | |
| 64 | + bash $WG/check_digest_completeness.sh $G $Y || { echo "$G: 闸口未过(缺摘要/缺md),不出图,下个窗口重试"; rc=1; continue; } | |
| 65 | + # png 比 md 新 ⇒ 上轮已出图(只是后面 HTML 等失败):不重复花生图钱;叠码也跳过(绝不在已叠码图上再叠) | |
| 66 | + if [ ! "${P}_$Y.png" -nt "${P}_$Y.md" ]; then | |
| 67 | + python3 $IG/gen_daily_1tgo_image.py --content-file "${P}_$Y.md" --report-id "$(cat $RID)" --output "${P}_$Y.png" \ | |
| 68 | + --title "$TITLE" --style $STYLE || { echo "$G: 出图失败"; rc=1; continue; } | |
| 69 | + python3 $IG/add_qr_to_image.py --image "${P}_$Y.png" --date "$Y" $QR --enlarge-2x || echo "$G: 叠码失败(警告,不阻断)" | |
| 70 | + fi | |
| 71 | + python3 $IG/collect_media_oss.py --date "$Y" --group $G --upload || echo "$G: 媒体上传失败(不阻断,墙为空)" | |
| 72 | + python3 $WG/gen_digest_html.py --date "$Y" --group $G && [ -s "${P}_$Y.png" ] && echo "done $(date +%s)" > "$M" || { echo "$G: HTML 失败"; rc=1; } ;; | |
| 73 | + send) | |
| 74 | + [ -s "${P}_$Y.png" ] || { echo "$G: no png, skip"; rc=1; continue; } | |
| 75 | + T0=$(date +%s) | |
| 76 | + python3 $WG/send_digest_group.py --group $G --date "$Y" || { rm -f "$M"; rc=1; continue; } | |
| 77 | + sleep 20 | |
| 78 | + if sent_ok "$TARGET" "$T0"; then echo "$G sent+verified -> $TARGET"; echo "done $(date +%s)" > "$M" | |
| 79 | + else echo "$G: hook 日志里没有 oj() DONE,视为未送达,下个窗口重试"; rm -f "$M"; rc=1; fi ;; | |
| 80 | + esac | |
| 81 | +done | |
| 82 | +exit $rc | ... | ... |
device/pixelphone/scripts/orig-save.sh
0 → 100755
| 1 | +#!/data/data/com.termux/files/usr/bin/bash | |
| 2 | +# 原图保存 A+B(旧 Hermes 调度器移植;窗口 02/06/10/14/18/22 点,用户 2026-08-24 定频,crontab 放在每窗口 :15 避开 02:00 出图) | |
| 3 | +# A: 微信图片查看器长按「保存图片」把当日群图的明文原图存到相册(UI 自动化,约 30 分钟,接管屏幕;--date 取【当日】, | |
| 4 | +# 供次日 01:00 生成当日日报用) | |
| 5 | +# B: 明文匹配 + 上传 OSS,更新 media_digest/media_files(幂等;--date 取【昨天】,供 HTML 媒体墙) | |
| 6 | +# 前置:锁屏则整轮跳过;微信没起跳过;有残留任务跳过。测试用环境变量: ORIG_SAVE_ARGS="--group <room> --max-swipes 2" | |
| 7 | +export PATH=/data/data/com.termux/files/usr/bin:/system/bin:$PATH HOME=${HOME:-/data/data/com.termux/files/home} | |
| 8 | +export TMPDIR=${TMPDIR:-/data/data/com.termux/files/usr/tmp} | |
| 9 | +[ -f "$HOME/.hermes_env.sh" ] && . "$HOME/.hermes_env.sh" # OSS key(不回显) | |
| 10 | +IG=$HOME/pixelphone/digest/gpt-image-gen/scripts | |
| 11 | +TODAY=$(date +%Y-%m-%d); YDAY=$(date -d yesterday +%Y-%m-%d) | |
| 12 | +LOG=$HOME/tmp/orig_save_$(date +%Y%m%d_%H%M).log | |
| 13 | +mkdir -p "$HOME/tmp" | |
| 14 | +say() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG"; } | |
| 15 | + | |
| 16 | +LOCK=$TMPDIR/orig-save.lock # send 阶段看到它在就等下一轮(两边都要用微信界面) | |
| 17 | +if ! mkdir "$LOCK" 2>/dev/null; then | |
| 18 | + [ -n "$(find "$LOCK" -maxdepth 0 -mmin +90 2>/dev/null)" ] && { rmdir "$LOCK"; mkdir "$LOCK"; } || { say "上一轮还在跑,跳过"; exit 0; } | |
| 19 | +fi | |
| 20 | +trap 'rmdir "$LOCK" 2>/dev/null' EXIT | |
| 21 | + | |
| 22 | +[ "$(su -c 'dumpsys window 2>/dev/null | grep -c mDreamingLockscreen=true')" = "1" ] && { say "锁屏,跳过原图保存"; exit 0; } | |
| 23 | +[ -n "$(su -c 'pidof com.tencent.mm' 2>/dev/null)" ] || { say "微信没在运行,跳过"; exit 0; } | |
| 24 | +BUSY=$(ps -ef | grep -E 'rescue_originals|send_digest|gen_daily|gen_agg|collect_media|save_originals|upload_originals' | grep -v grep | grep -v "orig-save" | wc -l) | |
| 25 | +[ "$BUSY" -gt 0 ] && { say "有 $BUSY 个日报/原图任务在跑,跳过"; exit 0; } | |
| 26 | + | |
| 27 | +say "== A 保存当日原图 date=$TODAY ${ORIG_SAVE_ARGS:-}" | |
| 28 | +python3 "$IG/save_originals_batch.py" --date "$TODAY" --max-swipes 18 ${ORIG_SAVE_ARGS:-} >> "$LOG" 2>&1; A=$? | |
| 29 | +say "A_EXIT=$A" | |
| 30 | +say "== B 匹配并上传昨日原图 date=$YDAY" | |
| 31 | +python3 "$IG/upload_originals.py" --date "$YDAY" --upload >> "$LOG" 2>&1; B=$? | |
| 32 | +say "B_EXIT=$B" | |
| 33 | +for i in 1 2 3; do su -c "input keyevent 4" 2>/dev/null; sleep 1; done # 复位副作用:退回微信主界面,避免残页影响下一轮 | |
| 34 | +say "== END (A=$A B=$B) log=$LOG" | |
| 35 | +[ $A -eq 0 ] && [ $B -eq 0 ] | ... | ... |
device/sync-device.sh
| ... | ... | @@ -26,13 +26,19 @@ chmod -R a+r $REMOTE; wc -l $REMOTE/list.txt |
| 26 | 26 | rm -rf _extract && mkdir _extract |
| 27 | 27 | "$ADB" pull $REMOTE/device.tgz _extract/ >/dev/null |
| 28 | 28 | "$ADB" pull $REMOTE/crontab.txt crontab.txt >/dev/null |
| 29 | -"$ADB" shell "su -c 'rm -rf $REMOTE'" | |
| 30 | 29 | # 重新生成:先清掉旧快照再解开,避免已删文件残留 |
| 31 | 30 | find . -mindepth 1 -maxdepth 1 ! -name sync-device.sh ! -name _extract ! -name crontab.txt ! -name README.md -exec rm -rf {} + |
| 32 | 31 | tar xzf _extract/device.tgz -C . 2>/dev/null || true |
| 33 | 32 | # service.d 脚本带绝对路径解开在 data/adb/...,挪到 service.d/ |
| 34 | 33 | if [ -d data/adb/service.d ]; then mv data/adb/service.d service.d; rm -rf data; fi |
| 35 | 34 | rm -rf _extract |
| 35 | +# 微信库密钥:只同步到本地,不入库(.gitignore 的 .wechat_key_*) | |
| 36 | +"$ADB" shell "su -c 'mkdir -p $REMOTE; cp $H/wechat-viewer/.wechat_key_* $REMOTE/ && chmod -R a+r $REMOTE'" 2>/dev/null || true | |
| 37 | +mkdir -p wechat-viewer | |
| 38 | +for k in $("$ADB" shell "su -c 'ls -a $REMOTE'" 2>/dev/null | tr -d '\r' | grep '^\.wechat_key_'); do | |
| 39 | + "$ADB" pull "$REMOTE/$k" "wechat-viewer/$k" >/dev/null && chmod 600 "wechat-viewer/$k" | |
| 40 | +done | |
| 41 | +"$ADB" shell "su -c 'rm -rf $REMOTE'" | |
| 36 | 42 | # 兜底:扫一遍疑似密钥,命中就报警并中止(不留在工作区) |
| 37 | 43 | if grep -rIlE '(sk-[A-Za-z0-9]{20,}|LTAI[A-Za-z0-9]{10,}|AccessKeySecret|api[_-]?key["'"'"' ]*[:=]["'"'"' ]*[A-Za-z0-9]{24,})' . --exclude=sync-device.sh --exclude=README.md 2>/dev/null; then |
| 38 | 44 | echo "!! 上面文件疑似含密钥,请检查后再提交" >&2; exit 1 | ... | ... |
wechat-viewer/.wechat_key_ec4f423b316d41e9af5e8398155cf105 deleted
| 1 | -1986554 |