# 消息抓取架构与 Hook 扩展指南

本文说明 notiMessage 的双通道抓取机制、Telegram 的实现方式、**Xposed 与 LSPosed**，以及日后接入其他 App 的步骤。

> 手机部署步骤见 [`手机操作手册.md`](手机操作手册.md) · MariBank 载荷与风控见 [`MariBank风控与载荷说明.md`](MariBank风控与载荷说明.md)

---

## 0. Xposed 与 LSPosed

### 0.1 是什么关系

| | **Xposed（经典）** | **LSPosed** |
|--|-------------------|-------------|
| 性质 | Hook 框架概念 + 老实现（改 `/system`） | 现代实现，**不动 system** |
| 依赖 | 老 Root / Recovery | **Magisk + Zygisk** |
| 作用域 | 全局或 Installer 里选 | **按 App 勾选** |
| 模块 API | `IXposedHookLoadPackage` 等 | **同一套 API** |
| 本项目 | 源码在 `xposed-module/` | **手机上实际加载框架** |

日常说法：**「Xposed 模块」= APK**；**「启用 Xposed」= 在 LSPosed 里打开模块并勾选作用域**。

### 0.2 与本项目的关系

```
Magisk → Zygisk → LSPosed → com.miraclegarden.smsmessage.xposed
                              ├── MariBankRootBypassHook（银行）
                              ├── TelegramMessageHook（Telegram）
                              └── …
```

测银行 App 时通常还需 **Shamiko**（Hide Magisk），与 LSPosed 分工见 [`MariBank风控与载荷说明.md` §6](MariBank风控与载荷说明.md)。

### 0.3 与其他工具对比

| 工具 | 特点 |
|------|------|
| **LSPosed** | 常驻、开机自动，适合银行 bypass |
| **Frida** | 临时 attach，与 LSPosed 同时开易冲突 |
| **Magisk 模块** | 改系统属性（serial），不是 Hook Java |

---

## 1. 总体架构

notiMessage 使用 **两条独立通道** 抓取消息，互为补充：

```
┌─────────────────────────────────────────────────────────────────┐
│                        目标 App（如 Telegram）                    │
└───────────────┬─────────────────────────────┬───────────────────┘
                │                             │
     后台/锁屏弹系统通知              前台打开 App、消息入库/解码
                │                             │
                ▼                             ▼
   NotificationListenerService          Xposed Hook 模块
   (NotificationService)              (xposed-module)
                │                             │
                │                             │ Broadcast
                │                             ▼
                │                    HookMessageReceiver
                │                      │ goAsync()
                │                      ├─ MessageLogStore（立即写入）
                │                      ├─ DebugForwarder（直接转发 PC）
                │                      └─ submitFromHook（仅正式上传模式）
                │                             │
                └──────────────┬──────────────┘
                               ▼
              NotificationService.submitNotification()（通知通道）
                               │
              ┌────────────────┼────────────────┐
              ▼                ▼                ▼
      MessageLogStore    DebugForwarder    （可选）正式后端上传
      （App 本地日志）    （PC 调试台）      AppConfig.ENABLE_SERVER_UPLOAD
```

| 通道 | 触发条件 | 代码入口 | 日志前缀 |
|------|----------|----------|----------|
| **通知监听** | 目标 App 在后台且弹出系统通知 | `NotificationService.onNotificationPosted()` | 无前缀 |
| **Hook** | 目标 App 进程收到新消息（通常在前台） | `HookMessageReceiver` → 直接 `DebugForwarder` | `[Hook/xposed_telegram]` 等 |

**重要**：仅开通知监听时，目标 App 在前台通常 **不会** 产生系统通知，因此抓不到消息。要覆盖前台场景，必须启用 Xposed Hook。

---

## 2. Telegram 实现方式

### 2.1 包名与作用域

Pixel 6 等设备上 Telegram 常见包名：

| 安装来源 | 包名 |
|----------|------|
| Play / 官网 APK | `org.telegram.messenger` |
| 部分渠道 / Web 版 | `org.telegram.messenger.web` |

LSPosed 作用域需勾选 **目标 Telegram 包名** + **notiMessage 主 App**。

默认作用域见 `xposed-module/src/main/res/values/arrays.xml`。

### 2.2 为何不 Hook SQLite？

Telegram 消息存在 SQLite 的 **`data` 字段（加密二进制 TL 序列化）**，不是明文 `content`/`text`。

通用 `SqliteMessageHook` 只能读明文列，对 Telegram **无效**。因此单独实现 `TelegramMessageHook`。

### 2.3 Hook 点

**主路径**：Hook `NotificationCenter.postNotificationName(int, Object[])`

- 读取静态字段 `NotificationCenter.didReceiveNewMessages` 作为事件 ID
- 当 `id == didReceiveNewMessages` 时，从参数里的 `List<MessageObject>` 取新消息
- 只处理 **非 outgoing**（`messageOwner.out == false`）的消息

**兜底路径**：Hook `MessageObject` 全部构造函数（主路径失败时启用）

源码位置：`xposed-module/.../hook/TelegramMessageHook.java`

### 2.4 字段提取逻辑

| 字段 | 提取方式 | 用途 |
|------|----------|------|
| **群名/会话名** | `MessagesController.getPeerTitle(dialogId)` → `MessageObject.getName()` → `getChat().title` | 分组标题、转发 `group` |
| **发送者** | `MessageObject.getFromName()` | 群内消息前缀 `发送者: 内容` |
| **正文** | `messageText` → `messageOwner.message` → `caption` | 文本内容 |
| **图片+说明** | 识别 `[图片]` 等媒体标签 + 合并 `caption` | 避免只显示「图片」丢失说明 |
| **dialogId** | `MessageObject.getDialogId()` | 去重键 `dialogId:messageId` |

**注意**：不要把 `messageOwner.from_id`（`TLRPC$TL_peerUser` 对象）直接 `toString()` 当标题，会出现 `TLRPC$TL_peerUser@xxxx`。

### 2.5 转发到主 App

Hook 模块通过 `HookForwarder` 发送广播：

```
Action: com.miraclegarden.smsmessage.action.HOOK_MESSAGE
Package: com.miraclegarden.smsmessage
Extras: packageName, title, content, timestamp, source=xposed_telegram
```

主 App 的 `HookMessageReceiver` 接收后：

1. 检查该 `packageName` 是否在监听列表（`App.getMessageByNotiList`）
2. 写本地日志（`MessageLogStore`，立即持久化）
3. **直接**转发 PC（`DebugForwarder`，不依赖 `startService`，后台可用）
4. 若开启正式上传（`ENABLE_SERVER_UPLOAD`），再经 `submitFromHook` 交给 `NotificationService`

---

## 3. 主 App 相关模块

| 文件 | 职责 |
|------|------|
| `NotificationService.java` | 通知监听 + 统一提交入口 |
| `HookMessageReceiver.java` | 接收 Xposed 广播 |
| `MessageLogStore.java` | 日志持久化（后台也能保留历史） |
| `DebugForwarder.java` | POST 到 PC 调试服务 |
| `AppConfig.java` | `ENABLE_DEBUG_FORWARD`、`ENABLE_SERVER_UPLOAD` 开关 |

### PC 调试台

```powershell
# 启动本地服务（含 adb reverse）
powershell -ExecutionPolicy Bypass -File scripts\start-debug-server.ps1
# 浏览器打开 http://127.0.0.1:8765 ，按群/会话分组展示
```

---

## 4. 日后接入其他 App 的步骤

### 4.1 先判断用哪种 Hook 策略

```
目标 App 前台消息是否需要抓取？
├─ 否 → 仅通知监听即可，在 App「监听设置」里添加包名
└─ 是 → 需要 Xposed Hook
         │
         ├─ 消息以明文写入 SQLite（content/text/body 等）？
         │   └─ 是 → 可复用 SqliteMessageHook（默认兜底）
         │
         ├─ 微信？
         │   └─ 是 → 已有 WeChatMessageHook（Hook WCDB message 表）
         │
         └─ 其他（Telegram、WhatsApp、银行 App 等）
             └─ 需编写 **专用 Hook 类**
```

### 4.2 新增专用 Hook 的标准流程

以新 App `com.example.chat` 为例：

#### 步骤 1：分析目标 App

1. 用 `adb shell pm path <包名>` 拉出 APK
2. jadx / dexdump 找消息解码后的类（类似 Telegram 的 `MessageObject`）
3. 确认：消息明文出现在哪个字段、哪个时机（构造函数 / 事件总线 / 通知回调）

#### 步骤 2：编写 Hook 类

在 `xposed-module/src/main/java/.../hook/` 新建 `ExampleChatMessageHook.java`：

```java
public final class ExampleChatMessageHook {
    public static void install(XC_LoadPackage.LoadPackageParam lpparam) {
        // 1. findClass + findAndHookMethod / hookAllConstructors
        // 2. 提取 title（群名）、content（正文）、过滤 outgoing/系统消息
        // 3. 去重（messageId + dialogId）
        // 4. HookForwarder.forward(context, lpparam.packageName, title, content, "xposed_example");
    }
}
```

在 `HookBridge.java` 增加 source 常量，例如 `SOURCE_XPOSED_EXAMPLE = "xposed_example"`。

#### 步骤 3：注册到 MainHook

编辑 `MainHook.java`：

```java
if ("com.example.chat".equals(lpparam.packageName)) {
    ExampleChatMessageHook.install(lpparam);
    return;
}
```

#### 步骤 4：更新 LSPosed 作用域

编辑 `xposed-module/src/main/res/values/arrays.xml`：

```xml
<item>com.example.chat</item>
```

#### 步骤 5：主 App 添加监听

在 notiMessage「监听设置」中添加该 App（或通过 `shared_prefs/server.xml` 的 `saveNotiList`）。

`bankInfoId` 本地调试可用 `local:<包名>`。

#### 步骤 6：构建、安装、验证

```powershell
# 完整安装（主 App + Xposed + LSPosed 作用域）
powershell -ExecutionPolicy Bypass -File scripts\install-full.ps1
```

验证清单：

- [ ] LSPosed 中模块已启用，作用域勾选 **目标 App + notiMessage**
- [ ] 强制停止并重启目标 App（让 Hook 重新加载）
- [ ] notiMessage 监听控制台「开始监听」
- [ ] **后台场景**：收消息 → 通知通道有日志
- [ ] **前台场景**：收消息 → `[Hook/xxx]` 日志 + PC 调试台有数据
- [ ] `adb logcat | grep notiMessageHook` 可见 `installed for <包名>`

### 4.3 复用通用 SqliteMessageHook 的条件

`SqliteMessageHook` 会在 `MainHook` 中作为 **默认兜底** 安装（非微信、非 Telegram 时）。

适用 App 特征：

- 使用标准 `SQLiteDatabase` / `FrameworkSQLiteDatabase`
- 表名含 `message` / `messages`
- `ContentValues` 中有明文列：`content`、`text`、`body`、`msg` 等

**不适用**：Telegram、WhatsApp、Signal 等端到端加密或二进制 `data` 列的 App。

### 4.4 已有专用 Hook 参考

| App | 类 | Hook 目标 |
|-----|-----|-----------|
| 微信 | `WeChatMessageHook` | `com.tencent.wcdb.database.SQLiteDatabase` insert，`message` 表 |
| Telegram | `TelegramMessageHook` | `NotificationCenter.didReceiveNewMessages` + `MessageObject` |
| 其他 | `SqliteMessageHook` | 通用 SQLite insert 兜底 |

---

## 5. 构建与安装

| 脚本 | 用途 |
|------|------|
| `scripts/build-debug.ps1` | 构建主 App + Xposed 双 APK |
| `scripts/install-debug.ps1` | 仅安装双 APK |
| `scripts/install-full.ps1` | **推荐**：构建 + 双 APK + LSPosed 作用域 + adb reverse + 电池白名单 |
| `scripts/start-debug-server.ps1` | 启动 PC 调试台 |

**注意**：Android Studio 点 Run 只安装 `:app` 主模块，**不会**安装 Xposed 模块。完整功能必须用 `install-full.ps1` 或手动安装两个 APK。

---

## 6. 常见问题

| 现象 | 原因 | 处理 |
|------|------|------|
| 前台有消息，App 无日志 | 仅通知通道，Hook 未生效 | 检查 LSPosed 作用域、重启目标 App |
| 标题显示 `TLRPC$...` | 误把对象当字符串 | 用 `getPeerTitle` 等 API，见 TelegramMessageHook |
| 图片消息只有「图片」 | 说明在 caption 字段 | 合并 `caption` + 媒体标签 |
| PC 有数据，App 无历史 | 旧版日志未持久化 | 已用 `MessageLogStore` 修复，更新主 App |
| Hook 日志前缀 `[Hook/...]` 无 | Xposed 未注入 | 确认 Magisk + LSPosed + 模块启用 |

---

## 7. 相关文件索引

```
xposed-module/
├── src/main/assets/xposed_init          # 入口 MainHook
├── src/main/java/.../MainHook.java      # 包名路由
├── src/main/java/.../HookForwarder.java # 广播转发
├── src/main/java/.../hook/
│   ├── TelegramMessageHook.java
│   ├── WeChatMessageHook.java
│   └── SqliteMessageHook.java
└── src/main/res/values/arrays.xml       # xposed_scope 默认作用域

app/
├── src/main/java/.../service/
│   ├── NotificationService.java
│   └── HookMessageReceiver.java
├── src/main/java/.../MessageLogStore.java
├── src/main/java/.../network/DebugForwarder.java
└── src/main/java/.../AppConfig.java

debug-server/server.py                   # PC 调试台
scripts/install-full.ps1                 # 一键完整部署
```
