Commit c0788f1cf7c6192d3012a82cb77838060eb44800

Authored by yibo
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>
.gitignore
... ... @@ -65,3 +65,6 @@ wechat_plain/scripts/lsposed-wechat-sendmsg/*.keystore
65 65 wechat_plain/scripts/platform-tools/
66 66 wechat_plain/scripts/archive/
67 67 *.idsig
  68 +
  69 +# WeChat DB 密钥:只留本地,不上传 GitLab(用 device/sync-device.sh 或 adb 从手机同步)
  70 +.wechat_key_*
... ...
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(&quot;wxid_or_chatroom_id&quot;, &quot;消息内容&quot;)
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(&quot;wxid_or_chatroom_id&quot;, &quot;消息内容&quot;)
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) -&gt; 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) -&gt; 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