diff --git a/wechat_send_internal/BRANDSERVICE_PUBLISH_RESEARCH.md b/wechat_send_internal/BRANDSERVICE_PUBLISH_RESEARCH.md index 1d7cd24..3ccd605 100644 --- a/wechat_send_internal/BRANDSERVICE_PUBLISH_RESEARCH.md +++ b/wechat_send_internal/BRANDSERVICE_PUBLISH_RESEARCH.md @@ -299,6 +299,38 @@ cw3 { d=key, e=value } --- +## 五、改用 `am`+UI 自动化自主复现发布流程(2026-07-27 下午续) + +用户提出关键改进:与其每轮都让用户在手机上手动操作、我在旁边被动等日志,不如**我自己直接用 `adb input tap`/`screencap` 驱动 UI**,全程自主复现"我的作品→贴图号→发表"整个流程,缩短每轮迭代的等待成本,也不再消耗用户注意力。 + +### 5.1 UI 自动化关键坑:这台设备是可折叠机,`screencap` 默认拿错屏幕 + +Pixel 10 Pro Fold 有两块屏(outer/cover 屏 + inner 屏),`adb exec-out screencap -p` 不带 `-d` 会警告"multiple displays"并默认拿第一个找到的(`4619827677550801152`,这次测得是纯黑——大概率是没在用的那块屏,或者是折叠态下未激活的屏)。**必须显式指定 `-d `**:`dumpsys SurfaceFlinger --display-id` 列出两个 ID,`dumpsys display | grep mPrimaryDisplayDevice` 能看到当前哪个是 "Outer Display";这次机器当时是以 outer/cover 屏(1080×2364,display id `4619827677550801153`)在用,`input tap` 坐标也要按这块屏的分辨率算(截图工具返回图默认会整体缩放显示,需要按标注的缩放比例换算成真实坐标再传给 `input tap`)。 + +### 5.2 自主复现全流程成功(无需用户参与),确认 `wxartrappsvr/route` 也不是提交点 + +自主完整跑通"我→作品→(贴图号)发表→选图→下一步→填标题/正文→发表"全程(用中性的 AI 日报图,不涉真人照片),全程无需用户操作。两轮独立发布均确认**服务器真的接受了新文章**(`js1.m.c()` 抓到的预览页 URL 里 `chksm=` 每轮都不同,且发布后能在"作品"列表里看到新帖子),排除了"根本没发出去,我们看错了"这种可能。 + +针对上一轮(§4)里最高频但内容读不出来的 `wxartrappsvr/route`(`r45.zt5`),本轮解开了它为什么读不出来的谜:**它的 payload 不是扁平字段,是一个 `LinkedList`**(`zt5.d` 字段),每个 `re7{d=type int, e=protobuf.g 装的 body 字节串, f=route/action 字符串}`——用之前"扫描字段 0-25 的 getString/getInteger/getLong"的通用逻辑天生看不到 List 类型字段,这是之前"内容都是空"的真正原因,不是"这条路真的没数据"。 + +给 `hookAnyCgiCapture` 加了 `r45.zt5` 专属 dump(读 `d` 字段的 LinkedList,逐个 `re7` 打印 `type(d)`/`route(f)`/`body(e.i())`),真机验证:**这条路的 payload 全是小程序 SDK 的埋点上报**——`type=1000`(页面性能指标:`costtime`/`staytime`/`targetAction`)、`type=1001`(`weapplog`,小程序 console.log 转发到日志系统)、`type=1005`(页面事件上报:`event_name`/`enter_scene`),`route(f)` 恒为 `wx0b2ec076ec39c786`(一个小程序 appid),`page_path` 恒为 `pages/image_editor/image_editor`。**这不是提交发布的调用,是小程序自身运行时的埋点上报,跟贴图号提交内容无关。** + +### 5.3 意外发现:图片编辑步骤本身是一个小程序(`appid=wx0b2ec076ec39c786`) + +这是本轮最有价值的新信息:从截图流程看,选完图后有一步"编辑"(`page_path: pages/image_editor/image_editor`)此前一直以为整条"编辑/填标题/发表"UI 都是纯 `TmplWebViewMMUI`(H5 网页),但埋点数据显示图片裁剪/编辑这一步实际上是**一个独立小程序**(appid `wx0b2ec076ec39c786`)渲染出来的,运行在 `appbrand*` 进程里(跟 §3 一开始的假设吻合,只是走了另一条我们没追的分支)。这意味着:贴图号发布流程可能是"原生 Flutter UI(选图) → 小程序(图片编辑) → H5 webview(标题/正文/发表)"三段拼接,每段用不同技术栈、不同网络通道,**"提交"这一步具体发生在哪一段,仍是未知数**——如果发生在小程序段,走的会是小程序专属的另一套 cgi 通道(`k2`/`y7`,回到最早 §一 反编译过的入口,但那条路当时已经确认在 appbrand 进程里挂了钩子却零触发,可能是挂错了具体哪个 appbrand 编号,或者小程序用的是另一套更底层的 native bridge,不经过 k2/y7)。 + +### 5.4 现状(本轮结束):三个候选全部证伪,第四个新线索(小程序 image_editor)尚未跟进 + +排除法进展更新: +- ❌ `js1.m.c()` / `bizh5webtransfer` —— 只读态 +- ❌ `m11.l0.d1()` (CDN 上传服务)—— 零触发 +- ❌ `wxartrappsvr/route`(`r45.zt5`/`re7`)—— 确认是小程序埋点上报,无关 +- 🆕 图片编辑步骤是小程序 `wx0b2ec076ec39c786`,可能是下一个该深挖的方向,但还没有具体行动 + +用户配合方式已改进(不再需要用户手动操作,可自主复现),今天已知的三条候选路径均已用真机数据证伪,核心提交调用仍未定位。建议下次接着来的方向:直接搜这个小程序 appid 相关的原生桥接代码(`k2`/`y7` 之外,可能还有别的小程序专属 jsapi 处理提交),或者接受当前"半自动"退路。 + +--- + ## 四、现场状态(写这份记录时) - 设备上所有 mitm/iptables/CA 改动均已清理(见上文事故记录),微信网络恢复正常。 diff --git a/xhs_lsposed_a16_fix/wechathook_src/WeChatHook.java b/xhs_lsposed_a16_fix/wechathook_src/WeChatHook.java index 92d0810..cff16db 100644 --- a/xhs_lsposed_a16_fix/wechathook_src/WeChatHook.java +++ b/xhs_lsposed_a16_fix/wechathook_src/WeChatHook.java @@ -1694,6 +1694,49 @@ public class WeChatHook implements IXposedHookLoadPackage { int idx = 0; for (Object req : pbs) { sb.append("\n -- pb#").append(idx++).append(" ").append(req.getClass().getName()).append(" --"); + // wxartrappsvr/route (r45.zt5) is a generic RPC multiplexer: field + // `d` is a LinkedList, each re7{d=type int, e=protobuf.g + // body bytes, f=route/action String}. Flat getString/getInteger + // scanning below never sees this (it's a List, not a scalar field), + // which is why earlier passes thought this call carried no payload. + // Dump it explicitly so we can see what's actually being routed. + if (req.getClass().getName().equals("r45.zt5")) { + try { + java.lang.reflect.Field lf = req.getClass().getDeclaredField("d"); + lf.setAccessible(true); + java.util.LinkedList items = (java.util.LinkedList) lf.get(req); + sb.append("\n zt5.d LinkedList size=").append(items.size()); + int ridx = 0; + for (Object item : items) { + String typeStr = "?", routeStr = "?", bodyStr = "?"; + try { + java.lang.reflect.Field df = item.getClass().getDeclaredField("d"); + df.setAccessible(true); + typeStr = String.valueOf(df.get(item)); + } catch (Throwable t) {} + try { + java.lang.reflect.Field ff = item.getClass().getDeclaredField("f"); + ff.setAccessible(true); + Object fv = ff.get(item); + routeStr = fv == null ? "null" : fv.toString(); + } catch (Throwable t) {} + try { + java.lang.reflect.Field ef = item.getClass().getDeclaredField("e"); + ef.setAccessible(true); + Object ev = ef.get(item); + if (ev != null) { + Object asStr = XposedHelpers.callMethod(ev, "i"); + bodyStr = asStr == null ? "null" : asStr.toString(); + if (bodyStr.length() > 500) bodyStr = bodyStr.substring(0, 500) + "…"; + } + } catch (Throwable t) { bodyStr = ""; } + sb.append("\n re7#").append(ridx++) + .append(" type(d)=").append(typeStr) + .append(" route(f)=").append(routeStr) + .append(" body(e.i())=").append(bodyStr); + } + } catch (Throwable t) { sb.append("\n zt5 dump err: ").append(t); } + } for (int i = 0; i <= 25; i++) { Object val = null; String kind = ""; try { Object c = XposedHelpers.callMethod(req, "getCustom", i);