Commit b47aed621223a535eaf4df5d21e292e7d77c1e95
1 parent
01a2cf55
feat(xhshook): 实时转发 edith 请求到 Hermes 本地 server
Hook.java v3:在现有 logcat 日志基础上,后台线程 POST 每条 edith.xiaohongshu.com 请求(method/url/全量 headers)到 Hermes event_server(127.0.0.1:8765/xhs_params)。 Hermes server 未运行时静默失败,logcat 捕获不受影响。 rebuild_xhshook.sh:d8 步骤改用 find 通配所有 .class 文件, 避免新增匿名类时漏掉(之前写死 Hook$1.class)。 update_xhshook_db.sh:两阶段更新策略 - Phase 1:Python in-place UPDATE + commit(不做 checkpoint, lspd 持有写锁时 checkpoint 会 OperationalError) - Phase 2:pull DB+WAL → Mac 本地 checkpoint → push 干净主文件 → 删设备端 WAL/SHM,确保重启后 DB 正确 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Showing
4 changed files
with
97 additions
and
15 deletions
xhs_lsposed_a16_fix/rebuild_xhshook.sh
| ... | ... | @@ -32,8 +32,7 @@ $D8 \ |
| 32 | 32 | --classpath "$LEGACY_JAR" \ |
| 33 | 33 | --min-api 27 \ |
| 34 | 34 | --output "$BUILD_DIR/dex" \ |
| 35 | - "$BUILD_DIR/compiled/com/hermes/xhshook/Hook.class" \ | |
| 36 | - "$BUILD_DIR/compiled/com/hermes/xhshook/Hook\$1.class" | |
| 35 | + $(find "$BUILD_DIR/compiled/com/hermes/xhshook" -name "*.class") | |
| 37 | 36 | |
| 38 | 37 | echo "== Packing APK ==" |
| 39 | 38 | cp "$SCRIPT_DIR/xhshook_src/AndroidManifest.xml" "$BUILD_DIR/pack/" 2>/dev/null || \ | ... | ... |
xhs_lsposed_a16_fix/update_xhshook_db.sh
| ... | ... | @@ -6,22 +6,42 @@ set -e |
| 6 | 6 | ADB="${ADB:-$(dirname "$0")/../platform-tools/adb}" |
| 7 | 7 | if [ ! -f "$ADB" ]; then ADB=adb; fi |
| 8 | 8 | |
| 9 | +TERMUX_PY=/data/data/com.termux/files/usr/bin/python3 | |
| 9 | 10 | NEW_PATH=$($ADB shell 'pm path com.hermes.xhshook' | tr -d '\r' | sed 's/package://') |
| 10 | 11 | echo "New APK path: $NEW_PATH" |
| 11 | 12 | |
| 12 | -$ADB shell 'su -c "cp /data/adb/lspd/config/modules_config.db /data/local/tmp/mc_upd.db && chmod 644 /data/local/tmp/mc_upd.db"' | |
| 13 | -$ADB pull /data/local/tmp/mc_upd.db /tmp/mc_upd.db | |
| 14 | -sqlite3 /tmp/mc_upd.db "UPDATE modules SET apk_path='${NEW_PATH}' WHERE module_pkg_name='com.hermes.xhshook';" | |
| 15 | -# Use Python in-place UPDATE to preserve inode → triggers lspd FileObserver. | |
| 16 | -# (adb push / cp replaces the file, changing inode, which FileObserver misses.) | |
| 17 | -TERMUX_PY=/data/data/com.termux/files/usr/bin/python3 | |
| 13 | +# --- Phase 1: Python in-place UPDATE (preserves inode, works while lspd is running) --- | |
| 14 | +# commit() writes to WAL so lspd can see it immediately via its DB connection. | |
| 15 | +# Checkpoint may fail if lspd holds a write lock — that's OK, we skip it. | |
| 18 | 16 | $ADB shell "su -c \"$TERMUX_PY -c \\\" |
| 19 | 17 | import sqlite3 |
| 20 | 18 | db = sqlite3.connect('/data/adb/lspd/config/modules_config.db') |
| 21 | 19 | db.execute(\\\\\\\"UPDATE modules SET apk_path='${NEW_PATH}' WHERE module_pkg_name='com.hermes.xhshook'\\\\\\\") |
| 22 | -db.execute('PRAGMA wal_checkpoint(TRUNCATE)') | |
| 23 | 20 | db.commit() |
| 21 | +try: | |
| 22 | + db.execute('PRAGMA wal_checkpoint(TRUNCATE)') | |
| 23 | +except Exception as e: | |
| 24 | + pass # lspd holds write lock; skip checkpoint, WAL commit is enough | |
| 24 | 25 | db.close() |
| 25 | -print('done') | |
| 26 | +print('phase1 done') | |
| 26 | 27 | \\\"\"" 2>&1 |
| 27 | -echo "DB updated in-place. Now: am force-stop com.xingin.xhs → open XHS manually." | |
| 28 | + | |
| 29 | +# --- Phase 2: pull + overwrite main file (ensures correct path survives reboot) --- | |
| 30 | +# After phase 1, WAL has the correct path. Pull the DB+WAL, checkpoint locally, | |
| 31 | +# then push clean main file back and remove WAL/SHM on device. | |
| 32 | +$ADB shell 'su -c "cp /data/adb/lspd/config/modules_config.db /data/local/tmp/mc_upd.db && cp /data/adb/lspd/config/modules_config.db-wal /data/local/tmp/mc_upd.db-wal 2>/dev/null; chmod 644 /data/local/tmp/mc_upd.db /data/local/tmp/mc_upd.db-wal 2>/dev/null; true"' | |
| 33 | +$ADB pull /data/local/tmp/mc_upd.db /tmp/mc_upd.db 2>/dev/null | |
| 34 | +$ADB pull /data/local/tmp/mc_upd.db-wal /tmp/mc_upd.db-wal 2>/dev/null || true | |
| 35 | + | |
| 36 | +# Checkpoint locally (Mac sqlite3 has no lock contention) | |
| 37 | +sqlite3 /tmp/mc_upd.db "PRAGMA wal_checkpoint(TRUNCATE);" 2>/dev/null || true | |
| 38 | +# Verify the path is there | |
| 39 | +ACTUAL=$(sqlite3 /tmp/mc_upd.db "SELECT apk_path FROM modules WHERE module_pkg_name='com.hermes.xhshook';") | |
| 40 | +echo "Verified path in DB: $ACTUAL" | |
| 41 | + | |
| 42 | +# Push clean DB and remove WAL/SHM on device | |
| 43 | +$ADB push /tmp/mc_upd.db /data/local/tmp/mc_upd_clean.db | |
| 44 | +$ADB shell 'su -c "cp /data/local/tmp/mc_upd_clean.db /data/adb/lspd/config/modules_config.db && rm -f /data/adb/lspd/config/modules_config.db-wal /data/adb/lspd/config/modules_config.db-shm && echo phase2 done"' | |
| 45 | + | |
| 46 | +echo "DB updated. Now: am force-stop com.xingin.xhs → open XHS manually." | |
| 47 | +echo "(If lspd cache doesn't auto-refresh, reboot is needed for changes to take effect.)" | ... | ... |
xhs_lsposed_a16_fix/xhshook_src/Hook.java
| ... | ... | @@ -6,7 +6,15 @@ import de.robv.android.xposed.XposedBridge; |
| 6 | 6 | import de.robv.android.xposed.XposedHelpers; |
| 7 | 7 | import de.robv.android.xposed.callbacks.XC_LoadPackage; |
| 8 | 8 | |
| 9 | +import java.io.OutputStream; | |
| 10 | +import java.net.HttpURLConnection; | |
| 11 | +import java.net.URL; | |
| 12 | +import java.nio.charset.StandardCharsets; | |
| 13 | + | |
| 9 | 14 | public class Hook implements IXposedHookLoadPackage { |
| 15 | + // Hermes event_server endpoint — POST xhs_params events here | |
| 16 | + private static final String HERMES_URL = "http://127.0.0.1:8765/xhs_params"; | |
| 17 | + | |
| 10 | 18 | @Override |
| 11 | 19 | public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable { |
| 12 | 20 | if (!lpparam.packageName.equals("com.xingin.xhs")) return; |
| ... | ... | @@ -22,22 +30,26 @@ public class Hook implements IXposedHookLoadPackage { |
| 22 | 30 | Object url = XposedHelpers.getObjectField(param.thisObject, "url"); |
| 23 | 31 | String urlStr = url != null ? url.toString() : "null"; |
| 24 | 32 | |
| 25 | - // Only log edith.xiaohongshu.com (the signed API endpoint) | |
| 26 | 33 | if (!urlStr.contains("edith.xiaohongshu.com")) return; |
| 27 | 34 | |
| 28 | 35 | Object method = XposedHelpers.getObjectField(param.thisObject, "method"); |
| 29 | 36 | String methodStr = method != null ? method.toString() : "GET"; |
| 30 | 37 | |
| 31 | - // headers field is Headers.Builder — call build() to get actual headers | |
| 32 | 38 | Object headersBuilder = XposedHelpers.getObjectField(param.thisObject, "headers"); |
| 33 | - String headersStr = "null"; | |
| 39 | + String headersStr = ""; | |
| 34 | 40 | if (headersBuilder != null) { |
| 35 | 41 | Object headers = XposedHelpers.callMethod(headersBuilder, "build"); |
| 36 | - headersStr = headers != null ? headers.toString() : "null"; | |
| 42 | + headersStr = headers != null ? headers.toString() : ""; | |
| 37 | 43 | } |
| 38 | 44 | |
| 39 | 45 | XposedBridge.log("[XHSHook] " + methodStr + " " + urlStr); |
| 40 | 46 | XposedBridge.log("[XHSHook] Headers:\n" + headersStr); |
| 47 | + | |
| 48 | + final String json = buildJson(methodStr, urlStr, headersStr); | |
| 49 | + new Thread(new Runnable() { | |
| 50 | + public void run() { postToHermes(json); } | |
| 51 | + }).start(); | |
| 52 | + | |
| 41 | 53 | } catch (Throwable t) { |
| 42 | 54 | XposedBridge.log("[XHSHook] Error in hook: " + t.getMessage()); |
| 43 | 55 | } |
| ... | ... | @@ -49,4 +61,55 @@ public class Hook implements IXposedHookLoadPackage { |
| 49 | 61 | XposedBridge.log(t); |
| 50 | 62 | } |
| 51 | 63 | } |
| 64 | + | |
| 65 | + private static String buildJson(String method, String url, String headersStr) { | |
| 66 | + StringBuilder sb = new StringBuilder(); | |
| 67 | + sb.append("{"); | |
| 68 | + sb.append("\"method\":").append(jsonStr(method)).append(","); | |
| 69 | + sb.append("\"url\":").append(jsonStr(url)).append(","); | |
| 70 | + sb.append("\"headers\":{"); | |
| 71 | + boolean first = true; | |
| 72 | + for (String line : headersStr.split("\n")) { | |
| 73 | + int colon = line.indexOf(": "); | |
| 74 | + if (colon < 0) continue; | |
| 75 | + String k = line.substring(0, colon).trim(); | |
| 76 | + String v = line.substring(colon + 2).trim(); | |
| 77 | + if (!first) sb.append(","); | |
| 78 | + sb.append(jsonStr(k)).append(":").append(jsonStr(v)); | |
| 79 | + first = false; | |
| 80 | + } | |
| 81 | + sb.append("}}"); | |
| 82 | + return sb.toString(); | |
| 83 | + } | |
| 84 | + | |
| 85 | + private static String jsonStr(String s) { | |
| 86 | + if (s == null) return "null"; | |
| 87 | + return "\"" + s.replace("\\", "\\\\") | |
| 88 | + .replace("\"", "\\\"") | |
| 89 | + .replace("\n", "\\n") | |
| 90 | + .replace("\r", "\\r") + "\""; | |
| 91 | + } | |
| 92 | + | |
| 93 | + private static void postToHermes(String json) { | |
| 94 | + try { | |
| 95 | + HttpURLConnection conn = (HttpURLConnection) new URL(HERMES_URL).openConnection(); | |
| 96 | + conn.setRequestMethod("POST"); | |
| 97 | + conn.setRequestProperty("Content-Type", "application/json"); | |
| 98 | + conn.setDoOutput(true); | |
| 99 | + conn.setConnectTimeout(1000); | |
| 100 | + conn.setReadTimeout(1000); | |
| 101 | + byte[] body = json.getBytes(StandardCharsets.UTF_8); | |
| 102 | + conn.setFixedLengthStreamingMode(body.length); | |
| 103 | + try (OutputStream os = conn.getOutputStream()) { | |
| 104 | + os.write(body); | |
| 105 | + } | |
| 106 | + int code = conn.getResponseCode(); | |
| 107 | + conn.disconnect(); | |
| 108 | + if (code != 200 && code != 201 && code != 204) { | |
| 109 | + XposedBridge.log("[XHSHook] Hermes POST " + code); | |
| 110 | + } | |
| 111 | + } catch (Throwable t) { | |
| 112 | + // Hermes server not running — silent fail, logcat capture still works | |
| 113 | + } | |
| 114 | + } | |
| 52 | 115 | } | ... | ... |
xhs_lsposed_a16_fix/xhshook_v2_signed.apk
0 → 100644
No preview for this file type