diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 9636d39..b659d29 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -18,6 +18,7 @@
- [ ] **我确认此更改不会破坏任何原有功能** / I confirm this change does not break any existing features
- [ ] **我已进行多版本适配(如适用)** / I have used MMVersion for version compatibility (if applicable)
- [ ] **我已在多个微信版本上测试此更改(如适用)** / I have tested this change on multiple WeChat versions (if applicable)
+- [ ] **已在 Release 构建中完成测试**(含签名校验与 DEX 加密保护,未经测试请勿勾选;详见 `CONTRIBUTING.md` → 构建和发布 → 构建配置 → Release 构建) / Verified in Release build (with signature verification & DEX encryption protection; check only after testing per `CONTRIBUTING.md` → Build & Release → Build Configuration → Release Build)
## 其他信息 / Additional Information
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index be677dd..ecd8c37 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -266,7 +266,7 @@ moe.ouom.wekit/
│ │ ├── moment/ # 朋友圈
│ │ ├── fix/ # 优化与修复
│ │ ├── dev/ # 开发者选项
-│ │ ├── fun/ # 娱乐功能
+│ │ ├── func/ # 娱乐功能
│ │ ├── script/ # 脚本管理
│ │ └── example/ # 示例代码
│ └── sdk/ # SDK 封装
@@ -420,10 +420,14 @@ import moe.ouom.wekit.constants.MMVersion
import moe.ouom.wekit.host.HostInfo
override fun entry(classLoader: ClassLoader) {
+ val isGooglePlayVersion = HostInfo.isGooglePlayVersion
val currentVersion = HostInfo.getVersionCode()
// 根据版本选择不同的实现
when {
+ isGooglePlayVersion && currentVersion >= MMVersion.MM_8_0_48_Play ->
+ // Google Play 8.0.48+
+ hookForNewVersion(classLoader)
currentVersion >= MMVersion.MM_8_0_90 -> {
// 8.0.90 及以上版本的实现
hookForNewVersion(classLoader)
@@ -2239,6 +2243,70 @@ override fun unload(classLoader: ClassLoader) {
}
```
+### 数据库监听器 (WeDatabaseListener)
+
+`WeDatabaseListener` 提供监听和篡改微信数据库操作的能力。
+
+#### 适配器定义
+
+```kotlin
+// 重写需要的方法
+open class DatabaseListenerAdapter {
+ // 插入后执行
+ open fun onInsert(table: String, values: ContentValues) {}
+
+ // 更新前执行,返回true阻止更新
+ open fun onUpdate(table: String, values: ContentValues): Boolean = false
+
+ // 查询前执行,返回修改后的SQL,null表示不修改
+ open fun onQuery(sql: String): String? = sql
+}
+```
+
+#### 快速开始
+
+```kotlin
+// 1. 继承适配器
+class MyListener : DatabaseListenerAdapter() {
+ override fun onInsert(table: String, values: ContentValues) {
+ if (table == "message") {
+ values.put("time", System.currentTimeMillis())
+ }
+ }
+
+ override fun onUpdate(table: String, values: ContentValues): Boolean {
+ return table == "user_info" && values.containsKey("balance")
+ }
+
+ override fun onQuery(sql: String): String? {
+ return if (sql.contains("password")) null else sql
+ }
+}
+
+// 2. 注册/注销
+override fun entry(classLoader: ClassLoader) {
+ WeDatabaseListener.addListener(this)
+}
+
+override fun unload(classLoader: ClassLoader) {
+ WeDatabaseListener.removeListener(this)
+ super.unload(classLoader)
+}
+```
+
+#### 方法说明
+
+| 方法 | 时机 | 返回值 | 作用 |
+|------|------|--------|------|
+| `onInsert` | 插入后 | - | 监听/修改插入数据 |
+| `onUpdate` | 更新前 | `true`=阻止 | 监听/阻止更新 |
+| `onQuery` | 查询前 | 新SQL/null | 篡改查询语句 |
+
+#### 特性
+
+- ✅ **链式处理**:多个监听器按注册顺序执行
+- ✅ **直接修改**:`values` 对象可直接修改生效
+
#### 核心工具类:WeProtoData
`WeProtoData` 是处理 Protobuf 数据的核心工具类,提供以下关键方法:
@@ -2969,6 +3037,7 @@ close #1
- [ ] **我确认此更改不会破坏任何原有功能** / I confirm this change does not break any existing features
- [ ] **我已进行多版本适配(如适用)** / I have used MMVersion for version compatibility (if applicable)
- [ ] **我已在多个微信版本上测试此更改(如适用)** / I have tested this change on multiple WeChat versions (if applicable)
+- [ ] **已在 Release 构建中完成测试**(含签名校验与 DEX 加密保护,未经测试请勿勾选;详见 `CONTRIBUTING.md` → 构建和发布 → 构建配置 → Release 构建) / Verified in Release build (with signature verification & DEX encryption protection; check only after testing per `CONTRIBUTING.md` → Build & Release → Build Configuration → Release Build)
##### 其他信息 / Additional Information
@@ -3026,6 +3095,28 @@ close #1
输出位置:`app/build/outputs/apk/debug/app-debug.apk`
+#### Release 构建
+当构建 **Release 变体 APK** 时,软件启动阶段将执行双重签名校验。为进行本地测试,需**临时**修改以下两处配置:
+<64位SHA256签名> 通过 SignatureVerifier.getSignatureHash() 生成
+
+**Native 层(`secrets.h`)**:
+
+```bash
+python generate_secrets_h.py <64位SHA256签名>
+# 将输出内容临时覆盖 app/src/main/cpp/include/secrets.h
+```
+
+**Java 层(`SignatureVerifier.java`)**:
+
+```java
+private static final String[] VALID_SIGNATURE_HASHES = {
+ "<64位SHA256签名>" // 仅限本地测试
+};
+```
+
+> ⚠️ **关键要求**:
+> 以上修改**仅用于本地 Release 构建测试**,测试完成后 **`secrets.h` 和 `SignatureVerifier.java` 必须立即还原**至仓库原始版本,**严禁提交至仓库**,包含测试签名的 PR 将被拒绝合并
+
### 自定义构建任务
**代码保护机制说明**:
diff --git a/SCRIPT_API_DOCUMENT.md b/SCRIPT_API_DOCUMENT.md
index 5ad4df1..a158f1a 100644
--- a/SCRIPT_API_DOCUMENT.md
+++ b/SCRIPT_API_DOCUMENT.md
@@ -3,14 +3,28 @@
## Beta 状态警告
**重要提醒:此 API 目前处于 Beta 测试阶段,在后续更新中可能出现不兼容的更改,使用时请密切关注相关更新通知。**
+
+**目前暂无提供监听信息发送等的 API 的想法,请自行根据 cgiId 判断实现相关监听。**
+
## 目录
1. [钩子函数](#钩子函数)
- - [onRequest](#onrequest)
- - [onResponse](#onresponse)
+ - [onRequest](#onrequest)
+ - [onResponse](#onresponse)
+ - [数据对象说明](#数据对象说明)
2. [WEKit 对象](#wekit-对象)
- - [概述](#概述)
- - [WEKit Log 函数](#wekit-log-函数)
+ - [概述](#概述)
+ - [WEKit Log 函数](#wekit-log-函数)
+ - [WEKit isMMAtLeast 函数](#wekit-ismmatleast-函数)
+ - [WEKit isGooglePlayVersion 函数](#wekit-isgoogleplayversion-函数)
+ - [WEKit sendCgi 函数](#wekit-sendcgi-函数)
+ - [WEKit getSelfWxId 函数](#wekit-getselfwxid-函数)
+ - [WEKit getSelfAlias 函数](#wekit-getselfalias-函数)
+ - [WEKit proto 对象](#wekit-proto-对象)
+ - [WEKit database 对象](#wekit-database-对象)
+ - [WEKit message 对象](#wekit-message-对象)
+3. [注意事项](#注意事项)
+4. [后续更新计划](#后续更新计划)
## 钩子函数
@@ -46,16 +60,22 @@ function onResponse(data) {
}
```
-## 数据对象说明
+### 数据对象说明
每个钩子函数接收一个 `data` 参数,该参数是一个对象,包含以下字段:
-| 字段名 | 类型 | 描述 |
-|----------|--------|------------------------------|
-| uri | string | 请求的目标 URI 地址 |
+| 字段名 | 类型 | 描述 |
+| -------- | ------ | ------------------------------- |
+| uri | string | 请求的目标 URI 地址 |
| cgiId | string | 请求的 CGI ID,用于识别请求类型 |
| jsonData | object | 请求或响应的数据体(JSON 格式) |
+**重要说明:**
+
+- 如果想要修改响应或者发送请求,**请返回修改后的 JSON 对象**,而不是直接修改 `data.jsonData`
+- **不要直接修改传入的 data.jsonData 对象**
+- **如果未做任何修改,直接 return null 或不 return**,返回的 `jsonData` 将保持原始状态
+
## WEKit 对象
### 概述
@@ -84,11 +104,636 @@ function onRequest(data) {
}
```
-### 注意事项
+### WEKit isMMAtLeast 函数
+
+`wekit.isMMAtLeast` 是 `wekit` 对象提供的版本检查函数。
+
+#### 使用方法
+
+```javascript
+wekit.isMMAtLeast(field);
+```
+
+#### 参数说明
+
+| 参数名 | 类型 | 描述 |
+| ------ | ------ | -------------------------- |
+| field | string | `MMVersion` 提供的版本常量 |
+
+#### 返回值
+
+| 类型 | 描述 |
+| ------- | ----------------------------------------- |
+| boolean | 如果当前版本号大于等于指定版本则返回 true |
+
+#### 示例
+
+```javascript
+function onRequest(data) {
+ if (wekit.isMMAtLeast("MM_8_0_67")) {
+ wekit.log("当前版本高于或等于指定版本");
+ } else {
+ wekit.log("当前版本低于指定版本");
+ }
+}
+```
+
+### WEKit isGooglePlayVersion 函数
+
+`wekit.isGooglePlayVersion` 是 `wekit` 对象提供的 Google Play 版本判断函数。
+
+#### 使用方法
+
+```javascript
+wekit.isGooglePlayVersion();
+```
+
+#### 返回值
+
+| 类型 | 描述 |
+| ------- | ----------------------------------------- |
+| boolean | 如果是 Google Play 版本则返回 true,否则返回 false |
+
+#### 示例
+
+```javascript
+function onRequest(data) {
+ if (wekit.isGooglePlayVersion()) {
+ wekit.log("当前是 Google Play 版本");
+ // Google Play 版本的特定逻辑
+ } else {
+ wekit.log("当前是非 Google Play 版本");
+ // 普通版本的特定逻辑
+ }
+}
+```
+
+#### 结合版本判断使用
+
+```javascript
+function onRequest(data) {
+ if (wekit.isGooglePlayVersion()) {
+ // Google Play 版本逻辑
+ if (wekit.isMMAtLeast("MM_8_0_48_Play")) {
+ wekit.log("Google Play 8.0.48 及以上版本");
+ }
+ } else {
+ // 普通版本逻辑
+ if (wekit.isMMAtLeast("MM_8_0_90")) {
+ wekit.log("普通版 8.0.90 及以上版本");
+ }
+ }
+}
+```
+
+### WEKit sendCgi 函数
+
+`wekit.sendCgi` 是 `wekit` 对象提供的发送异步无返回值的 CGI 请求函数。
+
+#### 使用方法
+
+```javascript
+wekit.sendCgi(uri, cgiId, funcId, routeId, jsonPayload);
+```
+
+#### 参数说明
+
+| 参数名 | 类型 | 描述 |
+| ----------- | ------ | --------------------------- |
+| uri | string | 请求的目标 URI 地址 |
+| cgiId | number | 请求的 CGI ID |
+| funcId | number | 功能 ID |
+| routeId | number | 路由 ID |
+| jsonPayload | string | 请求的数据体(JSON 字符串) |
+
+#### 示例
+
+```javascript
+function onRequest(data) {
+ wekit.log('准备发送 CGI 请求');
+ wekit.sendCgi('/cgi-bin/micromsg-bin/newgetcontact', 12345, 1, 1, JSON.stringify(data.jsonData));
+}
+```
+
+#### wekit.getSelfWxId
+
+获取当前用户的微信id。
+
+```javascript
+wekit.getSelfWxId();
+```
+
+##### 返回值
+
+| 类型 | 描述 |
+| ------ | ---------------- |
+| string | 当前用户的微信id |
+
+##### 示例
+
+```javascript
+function onRequest(data) {
+ const wxid = wekit.getSelfWxId();
+ wekit.log('当前用户微信id:', wxid);
+}
+```
+
+#### wekit.getSelfAlias
+
+获取当前用户的微信号。
+
+```javascript
+wekit.getSelfAlias();
+```
+
+##### 返回值
+
+| 类型 | 描述 |
+| ------ | ---------------- |
+| string | 当前用户的微信号 |
+
+##### 示例
+
+```javascript
+function onRequest(data) {
+ const alias = wekit.message.getSelfAlias();
+ wekit.log('当前用户微信号:', alias);
+}
+```
+
+### WEKit proto 对象
+
+`wekit.proto` 是 `wekit` 对象提供的处理 JSON 数据的工具对象。
+
+#### wekit.proto.replaceUtf8ContainsInJson
+
+在 JSON 数据中替换包含指定字符串的内容。
+
+```javascript
+wekit.proto.replaceUtf8ContainsInJson(json, needle, replacement);
+```
+
+##### 参数说明
+
+| 参数名 | 类型 | 描述 |
+| ----------- | ------ | ------------------ |
+| json | object | 要处理的 JSON 对象 |
+| needle | string | 要查找的字符串 |
+| replacement | string | 替换字符串 |
+
+##### 返回值
+
+| 类型 | 描述 |
+| ------ | ------------------ |
+| object | 处理后的 JSON 对象 |
+
+##### 示例
+
+```javascript
+function onRequest(data) {
+ const modifiedJson = wekit.proto.replaceUtf8ContainsInJson(
+ data.jsonData,
+ "oldValue",
+ "newValue"
+ );
+ return modifiedJson;
+}
+```
+
+#### wekit.proto.replaceUtf8RegexInJson
+
+在 JSON 数据中使用正则表达式替换内容。
+
+```javascript
+wekit.proto.replaceUtf8RegexInJson(json, pattern, replacement);
+```
+
+##### 参数说明
+
+| 参数名 | 类型 | 描述 |
+| ----------- | ------ | ------------------ |
+| json | object | 要处理的 JSON 对象 |
+| pattern | string | 正则表达式字符串 |
+| replacement | string | 替换字符串 |
+
+##### 返回值
+
+| 类型 | 描述 |
+| ------ | ------------------ |
+| object | 处理后的 JSON 对象 |
+
+##### 示例
+
+```javascript
+function onResponse(data) {
+ const modifiedJson = wekit.proto.replaceUtf8RegexInJson(
+ data.jsonData,
+ "\\d+",
+ "REPLACED"
+ );
+ return modifiedJson;
+}
+```
+
+### WEKit database 对象
+
+`wekit.database` 是 `wekit` 对象提供的数据库操作工具对象。
+
+#### wekit.database.query
+
+执行 SQL 查询语句。
+
+```javascript
+wekit.database.query(sql);
+```
+
+##### 参数说明
+
+| 参数名 | 类型 | 描述 |
+| ------ | ------ | ------------ |
+| sql | string | SQL 查询语句 |
+
+##### 返回值
+
+| 类型 | 描述 |
+| ----- | -------------------- |
+| array | 查询结果的 JSON 数组 |
+
+##### 示例
+
+```javascript
+function onRequest(data) {
+ const result = wekit.database.query("SELECT * FROM contacts WHERE nickname LIKE '%张%'");
+ wekit.log('查询结果:', result);
+}
+```
+
+#### wekit.database.getAllContacts
+
+获取所有联系人列表。
+
+```javascript
+wekit.database.getAllContacts();
+```
+
+##### 返回值
+
+| 类型 | 描述 |
+| ----- | ---------------------- |
+| array | 所有联系人的 JSON 数组 |
+
+##### 示例
+
+```javascript
+function onRequest(data) {
+ const contacts = wekit.database.getAllContacts();
+ wekit.log('所有联系人:', contacts);
+}
+```
+
+#### wekit.database.getContactList
+
+获取好友列表。
+
+```javascript
+wekit.database.getContactList();
+```
+
+##### 返回值
+
+| 类型 | 描述 |
+| ----- | -------------------- |
+| array | 好友列表的 JSON 数组 |
+
+##### 示例
+
+```javascript
+function onRequest(data) {
+ const friends = wekit.database.getContactList();
+ wekit.log('好友列表:', friends);
+}
+```
+
+#### wekit.database.getChatrooms
+
+获取群聊列表。
+
+```javascript
+wekit.database.getChatrooms();
+```
+
+##### 返回值
+
+| 类型 | 描述 |
+| ----- | -------------------- |
+| array | 群聊列表的 JSON 数组 |
+
+##### 示例
+
+```javascript
+function onRequest(data) {
+ const chatrooms = wekit.database.getChatrooms();
+ wekit.log('群聊列表:', chatrooms);
+}
+```
+
+#### wekit.database.getOfficialAccounts
+
+获取公众号列表。
+
+```javascript
+wekit.database.getOfficialAccounts();
+```
+
+##### 返回值
+
+| 类型 | 描述 |
+| ----- | ---------------------- |
+| array | 公众号列表的 JSON 数组 |
+
+##### 示例
+
+```javascript
+function onRequest(data) {
+ const accounts = wekit.database.getOfficialAccounts();
+ wekit.log('公众号列表:', accounts);
+}
+```
+
+#### wekit.database.getMessages
+
+获取指定用户的聊天记录。
+
+```javascript
+wekit.database.getMessages(wxid, page, pageSize);
+```
+
+##### 参数说明
+
+| 参数名 | 类型 | 描述 |
+| -------- | ------ | --------------------------- |
+| wxid | string | 用户的微信 ID |
+| page | number | 页码(可选,默认为 1) |
+| pageSize | number | 每页大小(可选,默认为 20) |
+
+##### 返回值
+
+| 类型 | 描述 |
+| ----- | -------------------- |
+| array | 聊天记录的 JSON 数组 |
+
+##### 示例
+
+```javascript
+function onRequest(data) {
+ const messages = wekit.database.getMessages('wxid_123456', 1, 50);
+ wekit.log('聊天记录:', messages);
+}
+```
+
+#### wekit.database.getAvatarUrl
+
+获取用户的头像 URL。
+
+```javascript
+wekit.database.getAvatarUrl(wxid);
+```
+
+##### 参数说明
+
+| 参数名 | 类型 | 描述 |
+| ------ | ------ | ------------- |
+| wxid | string | 用户的微信 ID |
+
+##### 返回值
+
+| 类型 | 描述 |
+| ------ | -------------- |
+| string | 用户的头像 URL |
+
+##### 示例
+
+```javascript
+function onRequest(data) {
+ const avatarUrl = wekit.database.getAvatarUrl('wxid_123456');
+ wekit.log('用户头像:', avatarUrl);
+}
+```
+
+#### wekit.database.getGroupMembers
+
+获取群成员列表。
+
+```javascript
+wekit.database.getGroupMembers(chatroomId);
+```
+
+##### 参数说明
+
+| 参数名 | 类型 | 描述 |
+| ---------- | ------ | ------------- |
+| chatroomId | string | 群聊的微信 ID |
+
+##### 返回值
+
+| 类型 | 描述 |
+| ----- | ---------------------- |
+| array | 群成员列表的 JSON 数组 |
+
+##### 示例
+
+```javascript
+function onRequest(data) {
+ const members = wekit.database.getGroupMembers('group_123456@chatroom');
+ wekit.log('群成员列表:', members);
+}
+```
+
+### WEKit message 对象
+
+`wekit.message` 是 `wekit` 对象提供的微信消息发送工具对象。
+
+#### wekit.message.sendText
+
+发送文本消息。
+
+```javascript
+wekit.message.sendText(toUser, text);
+```
+
+##### 参数说明
+
+| 参数名 | 类型 | 描述 |
+| ------ | ------ | ---------- |
+| toUser | string | 目标用户ID |
+| text | string | 消息内容 |
+
+##### 返回值
+
+| 类型 | 描述 |
+| ------- | --------------------------------- |
+| boolean | 发送成功返回 true,否则返回 false |
+
+##### 示例
+
+```javascript
+function onRequest(data) {
+ const success = wekit.message.sendText('wxid_123456', 'Hello World!');
+ if (success) {
+ wekit.log('文本消息发送成功');
+ } else {
+ wekit.log('文本消息发送失败');
+ }
+}
+```
+
+#### wekit.message.sendImage
+
+发送图片消息。
+
+```javascript
+wekit.message.sendImage(toUser, imgPath);
+```
+
+##### 参数说明
+
+| 参数名 | 类型 | 描述 |
+| ------- | ------ | ---------- |
+| toUser | string | 目标用户ID |
+| imgPath | string | 图片路径 |
+
+##### 返回值
+
+| 类型 | 描述 |
+| ------- | --------------------------------- |
+| boolean | 发送成功返回 true,否则返回 false |
+
+##### 示例
+
+```javascript
+function onRequest(data) {
+ const success = wekit.message.sendImage('wxid_123456', '/sdcard/DCIM/screenshot.png');
+ if (success) {
+ wekit.log('图片消息发送成功');
+ } else {
+ wekit.log('图片消息发送失败');
+ }
+}
+```
+
+#### wekit.message.sendFile
+
+发送文件消息。
+
+```javascript
+wekit.message.sendFile(talker, filePath, title, appid);
+```
+
+##### 参数说明
+
+| 参数名 | 类型 | 描述 |
+| -------- | ------ | -------------- |
+| talker | string | 目标用户ID |
+| filePath | string | 文件路径 |
+| title | string | 文件标题 |
+| appid | string | 应用ID(可选) |
+
+##### 返回值
+
+| 类型 | 描述 |
+| ------- | --------------------------------- |
+| boolean | 发送成功返回 true,否则返回 false |
+
+##### 示例
+
+```javascript
+function onRequest(data) {
+ const success = wekit.message.sendFile('wxid_123456', '/sdcard/Documents/file.pdf', '文档文件', '');
+ if (success) {
+ wekit.log('文件消息发送成功');
+ } else {
+ wekit.log('文件消息发送失败');
+ }
+}
+```
+
+#### wekit.message.sendVoice
+
+发送语音消息。
+
+```javascript
+wekit.message.sendVoice(toUser, path, durationMs);
+```
+
+##### 参数说明
+
+| 参数名 | 类型 | 描述 |
+| ---------- | ------ | ---------------- |
+| toUser | string | 目标用户ID |
+| path | string | 语音文件路径 |
+| durationMs | number | 语音时长(毫秒) |
+
+##### 返回值
+
+| 类型 | 描述 |
+| ------- | --------------------------------- |
+| boolean | 发送成功返回 true,否则返回 false |
+
+##### 示例
+
+```javascript
+function onRequest(data) {
+ const success = wekit.message.sendVoice('wxid_123456', '/sdcard/Recordings/audio.amr', 5000);
+ if (success) {
+ wekit.log('语音消息发送成功');
+ } else {
+ wekit.log('语音消息发送失败');
+ }
+}
+```
+
+#### wekit.message.sendXmlAppMsg
+
+发送XML应用消息。
+
+```javascript
+wekit.message.sendXmlAppMsg(toUser, xmlContent);
+```
+
+##### 参数说明
+
+| 参数名 | 类型 | 描述 |
+| ---------- | ------ | ---------- |
+| toUser | string | 目标用户ID |
+| xmlContent | string | XML内容 |
+
+##### 返回值
+
+| 类型 | 描述 |
+| ------- | --------------------------------- |
+| boolean | 发送成功返回 true,否则返回 false |
+
+##### 示例
+
+```javascript
+function onRequest(data) {
+ const xmlContent = '分享链接';
+ const success = wekit.message.sendXmlAppMsg('wxid_123456', xmlContent);
+ if (success) {
+ wekit.log('XML应用消息发送成功');
+ } else {
+ wekit.log('XML应用消息发送失败');
+ }
+}
+```
+
+## 注意事项
1. 日志输出将显示在脚本日志查看器中
2. 当 API 发生变更时,请及时更新相关脚本代码
+3. 所有工具方法都应在脚本环境中可用
+4. 钩子函数中修改数据时,请遵循返回值规则,不要直接修改传入参数
-### 后续更新计划
+## 后续更新计划
我们可能在稳定版本中提供更加完善和一致的日志功能接口,届时会提供更详细的文档和更稳定的 API 设计。
\ No newline at end of file
diff --git a/app/src/main/java/moe/ouom/wekit/config/RuntimeConfig.java b/app/src/main/java/moe/ouom/wekit/config/RuntimeConfig.java
index 74bc943..f1079b6 100644
--- a/app/src/main/java/moe/ouom/wekit/config/RuntimeConfig.java
+++ b/app/src/main/java/moe/ouom/wekit/config/RuntimeConfig.java
@@ -1,7 +1,7 @@
package moe.ouom.wekit.config;
-import android.annotation.SuppressLint;
import android.app.Activity;
+import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import java.lang.ref.WeakReference;
@@ -20,23 +20,16 @@ private RuntimeConfig() {
// account info //
- // 注意时效性,这里保存的登录信息是刚启动应用时的登录信息,而不是实时的登录信息
- // TODO: 需要一个机制来更新这些信息
-
+ private static SharedPreferences mmPrefs;
// login_weixin_username: wxid_apfe8lfoeoad13
// last_login_nick_name: 帽子叔叔
// login_user_name: 15068586147
// last_login_uin: 1293948946
- public static String login_weixin_username;
- public static String last_login_nick_name;
- public static String login_user_name;
- public static String last_login_uin;
// ------- //
-
- // wechat app info //
+ // WeChat app info //
@Getter
private static String wechatVersionName; // "8.0.65"
@@ -85,22 +78,6 @@ public static void setHostApplicationInfo(ApplicationInfo appInfo) {
hostApplicationInfo = appInfo;
}
- public static void setLogin_weixin_username(String login_weixin_username) {
- RuntimeConfig.login_weixin_username = login_weixin_username;
- }
-
- public static void setLast_login_nick_name(String last_login_nick_name) {
- RuntimeConfig.last_login_nick_name = last_login_nick_name;
- }
-
- public static void setLogin_user_name(String login_user_name) {
- RuntimeConfig.login_user_name = login_user_name;
- }
-
- public static void setLast_login_uin(String last_login_uin) {
- RuntimeConfig.last_login_uin = last_login_uin;
- }
-
public static void setWechatVersionName(String wechatVersionName) {
RuntimeConfig.wechatVersionName = wechatVersionName;
}
@@ -109,19 +86,23 @@ public static void setWechatVersionCode(long wechatVersionCode) {
RuntimeConfig.wechatVersionCode = wechatVersionCode;
}
+ public static void setmmPrefs(SharedPreferences sharedPreferences) {
+ RuntimeConfig.mmPrefs = sharedPreferences;
+ }
+
public static String getLogin_weixin_username() {
- return login_weixin_username;
+ return mmPrefs.getString("login_weixin_username", "");
}
public static String getLast_login_nick_name() {
- return last_login_nick_name;
+ return mmPrefs.getString("last_login_nick_name", "");
}
public static String getLogin_user_name() {
- return login_user_name;
+ return mmPrefs.getString("login_user_name", "");
}
public static String getLast_login_uin() {
- return last_login_uin;
+ return mmPrefs.getString("last_login_uin", "0");
}
}
\ No newline at end of file
diff --git a/app/src/main/java/moe/ouom/wekit/constants/Constants.kt b/app/src/main/java/moe/ouom/wekit/constants/Constants.kt
index c399858..25d1ce6 100644
--- a/app/src/main/java/moe/ouom/wekit/constants/Constants.kt
+++ b/app/src/main/java/moe/ouom/wekit/constants/Constants.kt
@@ -18,6 +18,7 @@ class Constants private constructor() {
// 数据库类
const val CLAZZ_SQLITE_DATABASE = "com.tencent.wcdb.database.SQLiteDatabase"
+ const val CLAZZ_COMPAT_SQLITE_DATABASE = "com.tencent.wcdb.compat.SQLiteDatabase"
// 红包消息类型
const val TYPE_LUCKY_MONEY = 436207665 // 红包
diff --git a/app/src/main/java/moe/ouom/wekit/constants/MMVersion.kt b/app/src/main/java/moe/ouom/wekit/constants/MMVersion.kt
index cf53873..adc748b 100644
--- a/app/src/main/java/moe/ouom/wekit/constants/MMVersion.kt
+++ b/app/src/main/java/moe/ouom/wekit/constants/MMVersion.kt
@@ -1,5 +1,7 @@
package moe.ouom.wekit.constants
+import moe.ouom.wekit.host.HostInfo
+
object MMVersion {
const val MM_8_0_67 = 3000
const val MM_8_0_66 = 2980
@@ -11,8 +13,9 @@ object MMVersion {
const val MM_8_0_60 = 2860
const val MM_8_0_58 = 2840
const val MM_8_0_57 = 2820
- const val MM_8_0_56 = 2780
+ const val MM_8_0_56 = 2800
const val MM_8_0_49 = 2600
+ const val MM_8_0_43 = 2480
const val V_8_0_67 = "8.0.67"
const val V_8_0_66 = "8.0.66"
@@ -26,4 +29,8 @@ object MMVersion {
const val V_8_0_57 = "8.0.57"
const val V_8_0_56 = "8.0.56"
const val V_8_0_49 = "8.0.49"
+ const val V_8_0_43 = "8.0.43"
+
+ const val MM_8_0_48_Play = 2583
+ const val V_8_0_48_Play = "8.0.48"
}
diff --git a/app/src/main/java/moe/ouom/wekit/core/dsl/DexDelegate.kt b/app/src/main/java/moe/ouom/wekit/core/dsl/DexDelegate.kt
index db2fde1..5230b03 100644
--- a/app/src/main/java/moe/ouom/wekit/core/dsl/DexDelegate.kt
+++ b/app/src/main/java/moe/ouom/wekit/core/dsl/DexDelegate.kt
@@ -87,7 +87,7 @@ class DexClassDelegate internal constructor(
}
fun getSuperClass(dexKit: DexKitBridge): ClassData? {
- return getClassData(dexKit)?.superClass
+ return getClassData(dexKit).superClass
}
override fun getValue(thisRef: Any?, property: KProperty<*>): DexClassDelegate = this
diff --git a/app/src/main/java/moe/ouom/wekit/hooks/item/chat/msg/AntiFoldMsg.kt b/app/src/main/java/moe/ouom/wekit/hooks/item/chat/msg/AntiFoldMsg.kt
new file mode 100644
index 0000000..5319dcf
--- /dev/null
+++ b/app/src/main/java/moe/ouom/wekit/hooks/item/chat/msg/AntiFoldMsg.kt
@@ -0,0 +1,48 @@
+package moe.ouom.wekit.hooks.item.chat.msg
+
+import moe.ouom.wekit.core.dsl.dexMethod
+import moe.ouom.wekit.core.dsl.resultNull
+import moe.ouom.wekit.core.model.BaseSwitchFunctionHookItem
+import moe.ouom.wekit.dexkit.intf.IDexFind
+import moe.ouom.wekit.hooks.core.annotation.HookItem
+import moe.ouom.wekit.util.log.WeLogger
+import org.luckypray.dexkit.DexKitBridge
+
+@HookItem(path = "聊天与消息/防止消息折叠", desc = "阻止聊天消息被折叠")
+class AntiFoldMsg : BaseSwitchFunctionHookItem(), IDexFind {
+
+ private val TAG = "AntiFoldMsg"
+ private val methodFoldMsg by dexMethod()
+
+ override fun dexFind(dexKit: DexKitBridge): Map {
+ val descriptors = mutableMapOf()
+
+ methodFoldMsg.find(dexKit, descriptors = descriptors) {
+ matcher {
+ usingStrings(".msgsource.sec_msg_node.clip-len")
+ paramTypes(
+ Int::class.java,
+ CharSequence::class.java,
+ null,
+ Boolean::class.javaPrimitiveType,
+ null,
+ null
+ )
+ }
+ }
+
+ return descriptors
+ }
+
+ override fun entry(classLoader: ClassLoader) {
+ // Hook 折叠方法,使其无效
+ methodFoldMsg.toDexMethod {
+ hook {
+ beforeIfEnabled { param ->
+ WeLogger.i(TAG, "拦截到消息折叠方法")
+ param.resultNull()
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/moe/ouom/wekit/hooks/item/chat/msg/ModifyMessageDisplayHook.kt b/app/src/main/java/moe/ouom/wekit/hooks/item/chat/msg/ModifyMessageDisplayHook.kt
new file mode 100644
index 0000000..1f17f92
--- /dev/null
+++ b/app/src/main/java/moe/ouom/wekit/hooks/item/chat/msg/ModifyMessageDisplayHook.kt
@@ -0,0 +1,80 @@
+package moe.ouom.wekit.hooks.item.chat.msg
+
+import com.afollestad.materialdialogs.MaterialDialog
+import com.afollestad.materialdialogs.input.getInputField
+import com.afollestad.materialdialogs.input.input
+import moe.ouom.wekit.core.model.BaseSwitchFunctionHookItem
+import moe.ouom.wekit.hooks.core.annotation.HookItem
+import moe.ouom.wekit.hooks.sdk.ui.WeChatChatContextMenuApi
+import moe.ouom.wekit.ui.CommonContextWrapper
+import moe.ouom.wekit.util.common.ModuleRes
+import moe.ouom.wekit.util.log.WeLogger
+
+@HookItem(
+ path = "聊天与消息/修改消息显示",
+ desc = "修改本地消息显示内容"
+)
+class ModifyMessageDisplayHook : BaseSwitchFunctionHookItem() {
+
+ companion object {
+ private const val TAG = "ModifyMessageDisplayHook"
+ private const val PREF_ID = 322424
+ }
+
+ private val onCreateMenuCallback = WeChatChatContextMenuApi.OnCreateListener { messageInfo ->
+ val type = messageInfo["field_type"] as? Int ?: 0
+ if (!MsgType.isText(type)) {
+ return@OnCreateListener null
+ }
+
+ WeChatChatContextMenuApi.MenuInfoItem(
+ id = PREF_ID,
+ title = "修改信息",
+ iconDrawable = ModuleRes.getDrawable("edit_24px")
+ )
+ }
+
+ private val onSelectMenuCallback = WeChatChatContextMenuApi.OnSelectListener { id, messageInfo, view ->
+ if (id != PREF_ID) return@OnSelectListener false
+ val context = view.context ?: return@OnSelectListener false
+ val wrappedContext = CommonContextWrapper.createAppCompatContext(context)
+ MaterialDialog(wrappedContext).show {
+ title(text = "修改消息")
+ input(
+ hint = "输入要修改的消息,仅限娱乐。",
+ waitForPositiveButton = false,
+ )
+ positiveButton(text = "确定") { dialog ->
+ val inputText = dialog.getInputField().text?.toString() ?: ""
+ val setTextMethod = view.javaClass.declaredMethods.first {
+ it.parameterTypes.contentEquals(
+ arrayOf(
+ CharSequence::class.java,
+ )
+ )
+ }
+ setTextMethod.invoke(view, inputText)
+ dialog.dismiss()
+ }
+
+ negativeButton(text = "取消")
+ }
+ return@OnSelectListener true
+ }
+
+ override fun entry(classLoader: ClassLoader) {
+ try {
+ WeChatChatContextMenuApi.addOnCreateListener(onCreateMenuCallback)
+ WeChatChatContextMenuApi.addOnSelectListener(onSelectMenuCallback)
+ WeLogger.i(TAG, "修改消息显示 Hook 注册成功")
+ } catch (e: Exception) {
+ WeLogger.e(TAG, "注册失败: ${e.message}", e)
+ }
+ }
+
+ override fun unload(classLoader: ClassLoader) {
+ WeChatChatContextMenuApi.removeOnCreateListener(onCreateMenuCallback)
+ WeChatChatContextMenuApi.removeOnSelectListener(onSelectMenuCallback)
+ super.unload(classLoader)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/moe/ouom/wekit/hooks/item/chat/msg/MsgType.kt b/app/src/main/java/moe/ouom/wekit/hooks/item/chat/msg/MsgType.kt
new file mode 100644
index 0000000..326789c
--- /dev/null
+++ b/app/src/main/java/moe/ouom/wekit/hooks/item/chat/msg/MsgType.kt
@@ -0,0 +1,69 @@
+package moe.ouom.wekit.hooks.item.chat.msg
+
+enum class MsgType(val code: Int) {
+ MOMENTS(0),
+ TEXT(1),
+ IMAGE(3),
+ VOICE(34),
+ FRIEND_VERIFY(37),
+ CONTACT_RECOMMEND(40),
+ CARD(42),
+ VIDEO(43),
+ EMOJI(0x2F),
+ LOCATION(0x30),
+ APP(49),
+ VOIP(50),
+ STATUS(51),
+ VOIP_NOTIFY(52),
+ VOIP_INVITE(53),
+ MICRO_VIDEO(62),
+ SYSTEM_NOTICE(0x270F),
+ SYSTEM(10000),
+ SYSTEM_LOCATION(10002),
+ SO_GOU_EMOJI(0x100031),
+ LINK(0x1000031),
+ RECALL(0x10002710),
+ SERVICE(0x13000031),
+ TRANSFER(0x19000031),
+ RED_PACKET(0x1A000031),
+ YEAR_RED_PACKET(0x1C000031),
+ ACCOUNT_VIDEO(0x1D000031),
+ RED_PACKET_COVER(0x20010031),
+ VIDEO_ACCOUNT(0x2D000031),
+ VIDEO_ACCOUNT_CARD(0x2E000031),
+ GROUP_NOTE(0x30000031),
+ QUOTE(0x31000031),
+ PAT(0x37000031),
+ VIDEO_ACCOUNT_LIVE(0x3A000031),
+ PRODUCT(0x3A100031),
+ UNKNOWN(0x3A200031),
+ MUSIC(0x3E000031),
+ FILE(0x41000031);
+
+ companion object {
+ fun fromCode(code: Int): MsgType? = entries.find { it.code == code }
+
+ fun isType(code: Int, vararg types: MsgType): Boolean =
+ types.any { it.code == code }
+
+ fun isText(code: Int) = code == TEXT.code
+ fun isImage(code: Int) = code == IMAGE.code
+ fun isVoice(code: Int) = code == VOICE.code
+ fun isVideo(code: Int) = code == VIDEO.code
+ fun isFile(code: Int) = code == FILE.code
+ fun isApp(code: Int) = code == APP.code
+ fun isLink(code: Int) = code == LINK.code || code == MUSIC.code || code == PRODUCT.code
+ fun isRedPacket(code: Int) = code == RED_PACKET.code || code == YEAR_RED_PACKET.code
+ fun isSystem(code: Int) = code == SYSTEM.code || code == SYSTEM_NOTICE.code
+ fun isEmoji(code: Int) = code == EMOJI.code || code == SO_GOU_EMOJI.code
+ fun isLocation(code: Int) = code == LOCATION.code || code == SYSTEM_LOCATION.code
+ fun isQuote(code: Int) = code == QUOTE.code
+ fun isPat(code: Int) = code == PAT.code
+ fun isTransfer(code: Int) = code == TRANSFER.code
+ fun isGroupNote(code: Int) = code == GROUP_NOTE.code
+ fun isVideoAccount(code: Int) = code == VIDEO_ACCOUNT.code || code == VIDEO_ACCOUNT_CARD.code || code == VIDEO_ACCOUNT_LIVE.code
+ fun isCard(code: Int) = code == CARD.code
+ fun isMoments(code: Int) = code == MOMENTS.code
+ fun isVoip(code: Int) = code == VOIP.code || code == VOIP_NOTIFY.code || code == VOIP_INVITE.code
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/moe/ouom/wekit/hooks/item/chat/risk/HookQueryCashierPkg.kt b/app/src/main/java/moe/ouom/wekit/hooks/item/chat/risk/HookQueryCashierPkg.kt
index 2c36f90..00160a8 100644
--- a/app/src/main/java/moe/ouom/wekit/hooks/item/chat/risk/HookQueryCashierPkg.kt
+++ b/app/src/main/java/moe/ouom/wekit/hooks/item/chat/risk/HookQueryCashierPkg.kt
@@ -121,7 +121,7 @@ class HookQueryCashierPkg : BaseClickableFunctionHookItem(), IWePkgInterceptor {
}
}
- private inner class ConfigDialog(context: Context) : BaseRikkaDialog(context, "收银台余额配置") {
+ private class ConfigDialog(context: Context) : BaseRikkaDialog(context, "收银台余额配置") {
override fun initPreferences() {
addCategory("金额设置")
diff --git a/app/src/main/java/moe/ouom/wekit/hooks/item/chat/risk/WeRedPacketAuto.kt b/app/src/main/java/moe/ouom/wekit/hooks/item/chat/risk/WeRedPacketAuto.kt
index 4104bff..b9cb651 100644
--- a/app/src/main/java/moe/ouom/wekit/hooks/item/chat/risk/WeRedPacketAuto.kt
+++ b/app/src/main/java/moe/ouom/wekit/hooks/item/chat/risk/WeRedPacketAuto.kt
@@ -25,7 +25,7 @@ import kotlin.random.Random
@SuppressLint("DiscouragedApi")
@HookItem(path = "聊天与消息/自动抢红包", desc = "监听消息并自动拆开红包")
-class WeRedPacketAuto : BaseClickableFunctionHookItem(), WeDatabaseListener.DatabaseInsertListener, IDexFind {
+class WeRedPacketAuto : BaseClickableFunctionHookItem(), IDexFind, WeDatabaseListener.InsertListener {
private val dexClsReceiveLuckyMoney by dexClass()
private val dexClsOpenLuckyMoney by dexClass()
diff --git a/app/src/main/java/moe/ouom/wekit/hooks/item/chat/risk/WeSendXml.kt b/app/src/main/java/moe/ouom/wekit/hooks/item/chat/risk/WeSendXml.kt
index 0b67938..c46a0c3 100644
--- a/app/src/main/java/moe/ouom/wekit/hooks/item/chat/risk/WeSendXml.kt
+++ b/app/src/main/java/moe/ouom/wekit/hooks/item/chat/risk/WeSendXml.kt
@@ -1,28 +1,10 @@
package moe.ouom.wekit.hooks.item.chat.risk
import android.annotation.SuppressLint
-import android.content.ContentValues
import android.content.Context
-import androidx.core.net.toUri
import com.afollestad.materialdialogs.MaterialDialog
-import de.robv.android.xposed.XposedHelpers
-import moe.ouom.wekit.config.WeConfig
-import moe.ouom.wekit.constants.Constants.Companion.TYPE_LUCKY_MONEY
-import moe.ouom.wekit.constants.Constants.Companion.TYPE_LUCKY_MONEY_EXCLUSIVE
-import moe.ouom.wekit.core.dsl.dexClass
-import moe.ouom.wekit.core.dsl.dexMethod
-import moe.ouom.wekit.core.model.BaseClickableFunctionHookItem
import moe.ouom.wekit.core.model.BaseSwitchFunctionHookItem
-import moe.ouom.wekit.dexkit.intf.IDexFind
import moe.ouom.wekit.hooks.core.annotation.HookItem
-import moe.ouom.wekit.hooks.sdk.api.WeDatabaseListener
-import moe.ouom.wekit.hooks.sdk.api.WeNetworkApi
-import moe.ouom.wekit.ui.creator.dialog.item.chat.risk.WeRedPacketConfigDialog
-import moe.ouom.wekit.util.log.WeLogger
-import org.json.JSONObject
-import org.luckypray.dexkit.DexKitBridge
-import java.util.concurrent.ConcurrentHashMap
-import kotlin.random.Random
@SuppressLint("DiscouragedApi")
@HookItem(path = "聊天与消息/发送 AppMsg(XML)", desc = "长按'发送'按钮,自动发送卡片消息")
diff --git a/app/src/main/java/moe/ouom/wekit/hooks/item/contact/ShowWeChatIdHook.kt b/app/src/main/java/moe/ouom/wekit/hooks/item/contact/ShowWeChatIdHook.kt
new file mode 100644
index 0000000..fc33518
--- /dev/null
+++ b/app/src/main/java/moe/ouom/wekit/hooks/item/contact/ShowWeChatIdHook.kt
@@ -0,0 +1,87 @@
+package moe.ouom.wekit.hooks.item.contact
+
+import android.app.Activity
+import android.content.ClipData
+import android.content.Context
+import android.widget.Toast
+import moe.ouom.wekit.core.model.BaseSwitchFunctionHookItem
+import moe.ouom.wekit.hooks.core.annotation.HookItem
+import moe.ouom.wekit.hooks.sdk.ui.WeChatContactInfoAdapterItemHook
+import moe.ouom.wekit.hooks.sdk.ui.WeChatContactInfoAdapterItemHook.ContactInfoItem
+import moe.ouom.wekit.util.log.WeLogger
+
+@HookItem(
+ path = "联系人/显示微信ID",
+ desc = "在联系人页面显示微信ID"
+)
+class ShowWeChatIdHook : BaseSwitchFunctionHookItem() {
+
+ companion object {
+ private const val TAG = "ShowWeChatIdHook"
+ private const val PREF_KEY = "wechat_id_display"
+ }
+
+
+ // 创建初始化回调
+ private val initCallback = WeChatContactInfoAdapterItemHook.InitContactInfoViewCallback { activity ->
+ val wechatId = try {
+ "微信ID: ${activity.intent.getStringExtra("Contact_User") ?: "未知"}"
+ } catch (e: Exception) {
+ WeLogger.e(TAG, "获取微信ID失败", e)
+ "微信ID: 获取失败"
+ }
+ if (wechatId.contains("gh_")) {
+ WeLogger.d(TAG, "检测到公众号,不处理")
+ return@InitContactInfoViewCallback null
+ }
+
+ ContactInfoItem(
+ key = PREF_KEY,
+ title = wechatId,
+ position = 1
+ )
+ }
+
+ private val clickListener = WeChatContactInfoAdapterItemHook.OnContactInfoItemClickListener { activity, key ->
+ if (key == PREF_KEY) {
+ handleWeChatIdClick(activity)
+ true
+ } else {
+ false
+ }
+ }
+
+ override fun entry(classLoader: ClassLoader) {
+ try {
+ // 添加初始化回调
+ WeChatContactInfoAdapterItemHook.addInitCallback(initCallback)
+ // 添加点击监听器
+ WeChatContactInfoAdapterItemHook.addClickListener(clickListener)
+ WeLogger.i(TAG, "显示微信ID Hook 注册成功")
+ } catch (e: Exception) {
+ WeLogger.e(TAG, "注册失败: ${e.message}", e)
+ }
+ }
+
+ private fun handleWeChatIdClick(activity: Activity): Boolean {
+ try {
+ val contactUser = activity.intent.getStringExtra("Contact_User")
+ val clipboard = activity.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
+ val clip = ClipData.newPlainText("微信ID", contactUser)
+ clipboard.setPrimaryClip(clip)
+ Toast.makeText(activity, "已复制", Toast.LENGTH_SHORT).show()
+ WeLogger.d(TAG, "Contact User: $contactUser")
+ return true
+ } catch (e: Exception) {
+ WeLogger.e(TAG, "处理点击失败: ${e.message}", e)
+ return false
+ }
+ }
+
+
+ override fun unload(classLoader: ClassLoader) {
+ WeChatContactInfoAdapterItemHook.removeInitCallback(initCallback)
+ WeChatContactInfoAdapterItemHook.removeClickListener(clickListener)
+ WeLogger.i(TAG, "已移除显示微信ID Hook")
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/moe/ouom/wekit/hooks/item/dev/WePacketDebugger.kt b/app/src/main/java/moe/ouom/wekit/hooks/item/dev/WePacketDebugger.kt
index c40673b..f841526 100644
--- a/app/src/main/java/moe/ouom/wekit/hooks/item/dev/WePacketDebugger.kt
+++ b/app/src/main/java/moe/ouom/wekit/hooks/item/dev/WePacketDebugger.kt
@@ -5,8 +5,6 @@ import android.widget.EditText
import android.widget.LinearLayout
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.customview.customView
-import com.afollestad.materialdialogs.input.input
-import moe.ouom.wekit.config.WeConfig
import moe.ouom.wekit.core.model.BaseClickableFunctionHookItem
import moe.ouom.wekit.hooks.core.annotation.HookItem
import moe.ouom.wekit.hooks.sdk.protocol.WePkgHelper
diff --git a/app/src/main/java/moe/ouom/wekit/hooks/item/fix/ForceCameraScan.kt b/app/src/main/java/moe/ouom/wekit/hooks/item/fix/ForceCameraScan.kt
new file mode 100644
index 0000000..8976d15
--- /dev/null
+++ b/app/src/main/java/moe/ouom/wekit/hooks/item/fix/ForceCameraScan.kt
@@ -0,0 +1,61 @@
+package moe.ouom.wekit.hooks.item.fix
+
+import moe.ouom.wekit.core.dsl.dexMethod
+import moe.ouom.wekit.core.model.BaseSwitchFunctionHookItem
+import moe.ouom.wekit.dexkit.intf.IDexFind
+import moe.ouom.wekit.hooks.core.annotation.HookItem
+import moe.ouom.wekit.util.log.WeLogger
+import org.luckypray.dexkit.DexKitBridge
+
+@HookItem(path = "优化与修复/扫码增强", desc = "强制使用相机扫码方式处理所有扫码")
+class ForceCameraScan : BaseSwitchFunctionHookItem(), IDexFind {
+
+ private val methodHandleScan by dexMethod()
+
+ override fun dexFind(dexKit: DexKitBridge): Map {
+ val descriptors = mutableMapOf()
+
+ methodHandleScan.find(dexKit, descriptors = descriptors) {
+ matcher {
+ usingStrings(
+ "MicroMsg.QBarStringHandler",
+ "key_offline_scan_show_tips"
+ )
+ }
+ }
+ return descriptors
+ }
+
+ override fun entry(classLoader: ClassLoader) {
+ methodHandleScan.toDexMethod {
+ hook {
+ beforeIfEnabled { param ->
+ try {
+ // 确保参数足够
+ if (param.args.size < 4) return@beforeIfEnabled
+
+ val arg2 = param.args[2] as? Int ?: return@beforeIfEnabled
+ val arg3 = param.args[3] as? Int ?: return@beforeIfEnabled
+
+ // 相机扫码的值
+ val CAMERA_VALUE_1 = 0
+ val CAMERA_VALUE_2 = 4
+
+ val BLOCKED_PAIR_1 = Pair(1, 34) // 相册扫码
+ val BLOCKED_PAIR_2 = Pair(4, 37) // 长按扫码
+
+ val currentPair = Pair(arg2, arg3)
+
+ // 如果是相册或长按扫码,强制改成相机扫码的值
+ if (currentPair == BLOCKED_PAIR_1 || currentPair == BLOCKED_PAIR_2) {
+ param.args[2] = CAMERA_VALUE_1
+ param.args[3] = CAMERA_VALUE_2
+ }
+ } catch (_: Exception) {
+ // 忽略异常
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/moe/ouom/wekit/hooks/item/fix/WeChatArticleAdRemover.kt b/app/src/main/java/moe/ouom/wekit/hooks/item/fix/WeChatArticleAdRemover.kt
new file mode 100644
index 0000000..6c037ac
--- /dev/null
+++ b/app/src/main/java/moe/ouom/wekit/hooks/item/fix/WeChatArticleAdRemover.kt
@@ -0,0 +1,90 @@
+package moe.ouom.wekit.hooks.item.fix
+
+import moe.ouom.wekit.core.model.BaseSwitchFunctionHookItem
+import moe.ouom.wekit.hooks.core.annotation.HookItem
+import moe.ouom.wekit.hooks.sdk.protocol.WePkgManager
+import moe.ouom.wekit.hooks.sdk.protocol.intf.IWePkgInterceptor
+import moe.ouom.wekit.util.WeProtoData
+import moe.ouom.wekit.util.log.WeLogger
+import org.json.JSONArray
+import org.json.JSONObject
+
+@HookItem(path = "优化与修复/去除文章广告", desc = "清除文章中的广告数据")
+class WeChatArticleAdRemover : BaseSwitchFunctionHookItem(), IWePkgInterceptor {
+
+ override fun entry(classLoader: ClassLoader) {
+ WePkgManager.addInterceptor(this)
+ }
+
+ override fun onResponse(uri: String, cgiId: Int, respBytes: ByteArray): ByteArray? {
+ if (cgiId != 21909) return null
+
+ try {
+ val data = WeProtoData()
+ data.fromBytes(respBytes)
+ val json = data.toJSON()
+
+ // 获取字段2
+ val field2 = json.optJSONObject("2") ?: return null
+ // 获取字段3中的广告JSON字符串
+ val adJsonStr = field2.optString("3") ?: return null
+
+ // 解析广告JSON
+ val adJson = JSONObject(adJsonStr)
+ // 清空广告字段
+ var modified = false
+
+ // 清空广告数组
+ if (adJson.has("ad_slot_data")) {
+ adJson.put("ad_slot_data", JSONArray())
+ modified = true
+ }
+
+ if (adJson.has("advertisement_info")) {
+ adJson.put("advertisement_info", JSONArray())
+ modified = true
+ }
+
+ // 广告数量设置为0
+ if (adJson.has("advertisement_num")) {
+ adJson.put("advertisement_num", 0)
+ modified = true
+ }
+
+ // 清空广告曝光相关
+ if (adJson.has("no_ad_indicator_info")) {
+ adJson.put("no_ad_indicator_info", JSONArray())
+ modified = true
+ }
+
+ // 清空广告响应
+ if (adJson.has("check_ad_resp")) {
+ adJson.put("check_ad_resp", JSONObject().apply {
+ put("aid", "0")
+ put("del_aid", JSONArray())
+ put("offline_aid", JSONArray())
+ put("online_aid", JSONArray())
+ })
+ modified = true
+ }
+
+ if (modified) {
+ // 放回修改后的广告JSON
+ field2.put("3", adJson.toString())
+ data.applyViewJSON(json, true)
+ WeLogger.d("WeChatArticleAdRemover", "已清空所有广告相关数据")
+ return data.toPacketBytes()
+ }
+
+ } catch (e: Exception) {
+ WeLogger.e("WeChatArticleAdRemover", e)
+ }
+
+ return null
+ }
+
+ override fun unload(classLoader: ClassLoader) {
+ WePkgManager.removeInterceptor(this)
+ super.unload(classLoader)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/moe/ouom/wekit/hooks/item/func/WeAvatarTransparent.kt b/app/src/main/java/moe/ouom/wekit/hooks/item/func/WeAvatarTransparent.kt
new file mode 100644
index 0000000..8f10762
--- /dev/null
+++ b/app/src/main/java/moe/ouom/wekit/hooks/item/func/WeAvatarTransparent.kt
@@ -0,0 +1,50 @@
+package moe.ouom.wekit.hooks.item.func
+
+import android.graphics.Bitmap
+import moe.ouom.wekit.core.dsl.dexMethod
+import moe.ouom.wekit.core.model.BaseSwitchFunctionHookItem
+import moe.ouom.wekit.dexkit.intf.IDexFind
+import moe.ouom.wekit.hooks.core.annotation.HookItem
+import moe.ouom.wekit.util.log.WeLogger
+import org.luckypray.dexkit.DexKitBridge
+
+@HookItem(path = "娱乐功能/头像上传透明", desc = "头像上传时使用PNG格式保持透明")
+class AvatarTransparent : BaseSwitchFunctionHookItem(), IDexFind {
+
+ private val methodSaveBitmap by dexMethod()
+
+ override fun dexFind(dexKit: DexKitBridge): Map {
+ val descriptors = mutableMapOf()
+
+ methodSaveBitmap.find(dexKit, descriptors = descriptors) {
+ matcher {
+ usingStrings("saveBitmapToImage pathName null or nil", "MicroMsg.BitmapUtil")
+ }
+ }
+
+ return descriptors
+ }
+
+ override fun entry(classLoader: ClassLoader) {
+ methodSaveBitmap.toDexMethod {
+ hook {
+ beforeIfEnabled { param ->
+ try {
+ val args = param.args
+
+ val pathName = args[3] as? String
+ if (pathName != null &&
+ (pathName.contains("avatar") || pathName.contains("user_hd"))
+ ) {
+ WeLogger.i("检测到头像保存: $pathName")
+ args[2] = Bitmap.CompressFormat.PNG
+ WeLogger.i("已将头像格式修改为PNG,保留透明通道")
+ }
+ } catch (e: Exception) {
+ WeLogger.e("头像格式修改失败: ${e.message}")
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/moe/ouom/wekit/hooks/item/dev/WeProfileCleaner.kt b/app/src/main/java/moe/ouom/wekit/hooks/item/func/WeProfileCleaner.kt
similarity index 96%
rename from app/src/main/java/moe/ouom/wekit/hooks/item/dev/WeProfileCleaner.kt
rename to app/src/main/java/moe/ouom/wekit/hooks/item/func/WeProfileCleaner.kt
index 17de24a..576e0d8 100644
--- a/app/src/main/java/moe/ouom/wekit/hooks/item/dev/WeProfileCleaner.kt
+++ b/app/src/main/java/moe/ouom/wekit/hooks/item/func/WeProfileCleaner.kt
@@ -1,9 +1,8 @@
-package moe.ouom.wekit.hooks.item.dev
+package moe.ouom.wekit.hooks.item.func
import android.content.Context
import com.afollestad.materialdialogs.MaterialDialog
import moe.ouom.wekit.core.model.BaseClickableFunctionHookItem
-import moe.ouom.wekit.dexkit.cache.DexCacheManager
import moe.ouom.wekit.hooks.core.annotation.HookItem
import moe.ouom.wekit.hooks.sdk.protocol.WePkgHelper
import moe.ouom.wekit.util.log.WeLogger
diff --git a/app/src/main/java/moe/ouom/wekit/hooks/item/dev/WeProfileNameSetter.kt b/app/src/main/java/moe/ouom/wekit/hooks/item/func/WeProfileNameSetter.kt
similarity index 97%
rename from app/src/main/java/moe/ouom/wekit/hooks/item/dev/WeProfileNameSetter.kt
rename to app/src/main/java/moe/ouom/wekit/hooks/item/func/WeProfileNameSetter.kt
index 0de93da..26519b3 100644
--- a/app/src/main/java/moe/ouom/wekit/hooks/item/dev/WeProfileNameSetter.kt
+++ b/app/src/main/java/moe/ouom/wekit/hooks/item/func/WeProfileNameSetter.kt
@@ -1,9 +1,8 @@
-package moe.ouom.wekit.hooks.item.dev
+package moe.ouom.wekit.hooks.item.func
import android.content.Context
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.input.input
-import moe.ouom.wekit.config.WeConfig
import moe.ouom.wekit.core.model.BaseClickableFunctionHookItem
import moe.ouom.wekit.hooks.core.annotation.HookItem
import moe.ouom.wekit.hooks.sdk.protocol.WePkgHelper
diff --git a/app/src/main/java/moe/ouom/wekit/hooks/item/moment/AntiSnsAd.kt b/app/src/main/java/moe/ouom/wekit/hooks/item/moment/AntiSnsAd.kt
new file mode 100644
index 0000000..b4934b9
--- /dev/null
+++ b/app/src/main/java/moe/ouom/wekit/hooks/item/moment/AntiSnsAd.kt
@@ -0,0 +1,35 @@
+package moe.ouom.wekit.hooks.item.moment
+
+import de.robv.android.xposed.XC_MethodHook
+import de.robv.android.xposed.XposedHelpers
+import de.robv.android.xposed.callbacks.IXUnhook
+import moe.ouom.wekit.core.model.BaseSwitchFunctionHookItem
+import moe.ouom.wekit.hooks.core.annotation.HookItem
+import moe.ouom.wekit.util.Initiator.loadClass
+import moe.ouom.wekit.util.log.WeLogger
+
+@HookItem(path = "朋友圈/拦截广告", desc = "拦截朋友圈广告")
+class AntiSnsAd : BaseSwitchFunctionHookItem() {
+
+ private var unhook: IXUnhook<*>? = null
+ override fun entry(classLoader: ClassLoader) {
+ val adInfoClass = loadClass("\"com.tencent.mm.plugin.sns.storage.ADInfo\"")
+ unhook = XposedHelpers.findAndHookConstructor(
+ adInfoClass,
+ String::class.java,
+ object : XC_MethodHook() {
+ override fun beforeHookedMethod(param: MethodHookParam) {
+ if (param.args.isNotEmpty() && param.args[0] is String) {
+ param.args[0] = ""
+ WeLogger.i("拦截到ADInfo广告")
+ }
+ }
+ }
+ )
+ }
+
+ override fun unload(classLoader: ClassLoader) {
+ unhook?.unhook()
+ super.unload(classLoader)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/moe/ouom/wekit/hooks/item/moment/AntiSnsDeleteHook.kt b/app/src/main/java/moe/ouom/wekit/hooks/item/moment/AntiSnsDeleteHook.kt
new file mode 100644
index 0000000..c60911c
--- /dev/null
+++ b/app/src/main/java/moe/ouom/wekit/hooks/item/moment/AntiSnsDeleteHook.kt
@@ -0,0 +1,98 @@
+package moe.ouom.wekit.hooks.item.moment
+
+import android.content.ContentValues
+import moe.ouom.wekit.core.model.BaseSwitchFunctionHookItem
+import moe.ouom.wekit.hooks.core.annotation.HookItem
+import moe.ouom.wekit.hooks.sdk.api.WeDatabaseListener
+import moe.ouom.wekit.util.WeProtoData
+import moe.ouom.wekit.util.log.WeLogger
+
+@HookItem(
+ path = "朋友圈/拦截朋友圈删除",
+ desc = "移除删除标志并注入 '[拦截删除]' 标记"
+)
+class AntiSnsDeleteHook : BaseSwitchFunctionHookItem(), WeDatabaseListener.UpdateListener {
+
+ companion object {
+ private const val LOG_TAG = "MomentAntiDel"
+ private const val TBL_SNS_INFO = "SnsInfo"
+ private const val DEFAULT_WATERMARK = "[拦截删除]"
+ }
+
+ override fun onUpdate(table: String, values: ContentValues): Boolean {
+ if (!isEnabled) return false
+
+ try {
+ when (table) {
+ TBL_SNS_INFO -> handleSnsRecord(values)
+ }
+ } catch (ex: Throwable) {
+ WeLogger.e(LOG_TAG, "拦截处理异常", ex)
+ }
+ return false
+ }
+
+ override fun entry(classLoader: ClassLoader) {
+ WeDatabaseListener.addListener(this)
+ WeLogger.i(LOG_TAG, "服务已启动 | 标记文本:'$DEFAULT_WATERMARK'")
+ }
+
+ override fun unload(classLoader: ClassLoader) {
+ WeDatabaseListener.removeListener(this)
+ WeLogger.i(LOG_TAG, "服务已停止")
+ }
+
+ private fun handleSnsRecord(values: ContentValues) {
+ val typeVal = (values.get("type") as? Int) ?: return
+ val sourceVal = (values.get("sourceType") as? Int) ?: return
+
+ if (!SnsContentType.allTypeIds.contains(typeVal)) return
+ if (sourceVal != 0) return
+
+ val kindName = SnsContentType.fromId(typeVal)?.displayName ?: "Unknown[$typeVal]"
+ WeLogger.d(LOG_TAG, "捕获删除信号 -> $kindName ($typeVal)")
+
+ // 移除来源
+ values.remove("sourceType")
+
+ // 注入水印
+ val contentBytes = values.getAsByteArray("content")
+ if (contentBytes != null) {
+ try {
+ val proto = WeProtoData()
+ proto.fromMessageBytes(contentBytes)
+
+ if (appendWatermark(proto, 5)) {
+ values.put("content", proto.toMessageBytes())
+ WeLogger.i(LOG_TAG, ">> 拦截成功:[$kindName] 已注入标记")
+ }
+ } catch (e: Exception) {
+ WeLogger.e(LOG_TAG, "朋友圈 Protobuf 处理失败", e)
+ }
+ }
+ }
+
+ private fun appendWatermark(proto: WeProtoData, fieldNumber: Int): Boolean {
+ try {
+ val json = proto.toJSON()
+ val key = fieldNumber.toString()
+ WeLogger.d(LOG_TAG, json.toString())
+
+ if (!json.has(key)) return false
+
+ val currentVal = json.get(key)
+
+ if (currentVal is String) {
+ if (currentVal.contains(DEFAULT_WATERMARK)) {
+ return false
+ }
+ val newVal = "$DEFAULT_WATERMARK $currentVal "
+ proto.setLenUtf8(fieldNumber, 0, newVal)
+ return true
+ }
+ } catch (e: Exception) {
+ WeLogger.e(LOG_TAG, "注入标记失败", e)
+ }
+ return false
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/moe/ouom/wekit/hooks/item/moment/SnsContentType.kt b/app/src/main/java/moe/ouom/wekit/hooks/item/moment/SnsContentType.kt
new file mode 100644
index 0000000..5a89572
--- /dev/null
+++ b/app/src/main/java/moe/ouom/wekit/hooks/item/moment/SnsContentType.kt
@@ -0,0 +1,46 @@
+package moe.ouom.wekit.hooks.item.moment
+
+enum class SnsContentType(val typeId: Int, val displayName: String) {
+ IMG(1, "图片"),
+ TEXT(2, "文本"),
+ LINK(3, "链接"),
+ MUSIC(4, "音乐"),
+ VIDEO(5, "视频"),
+ COMMODITY(9, "商品"),
+ STICKER(10, "表情"),
+ COMMODITY_OLD(12, "商品 (旧)"),
+ COUPON(13, "卡券"),
+ TV_SHOW(14, "视频号/电视"),
+ LITTLE_VIDEO(15, "微视/短视频"),
+ STREAM_VIDEO(18, "直播流"),
+ ARTICLE_VIDEO(19, "文章视频"),
+ NOTE(26, "笔记"),
+ FINDER_VIDEO(28, "视频号视频"),
+ WE_APP(30, "小程序单页"),
+ LIVE(34, "直播"),
+ FINDER_LONG_VIDEO(36, "视频号长视频"),
+ LITE_APP(41, "轻应用"),
+ RICH_MUSIC(42, "富媒体音乐"),
+ TING_AUDIO(47, "听歌"),
+ LIVE_PHOTO(54, "动态照片");
+
+ companion object {
+ // 缓存所有有效的 Type ID,避免每次重复计算
+ private val validTypeSet by lazy { entries.map { it.typeId }.toHashSet() }
+
+ /**
+ * 解析整型 ID 为对应的枚举实例
+ * @param id 数据库中的 type 值
+ * @return 匹配成功返回枚举,否则返回 null
+ */
+ fun fromId(id: Int): SnsContentType? =
+ entries.firstOrNull { it.typeId == id }
+
+ /**
+ * 获取全量类型 ID 集合
+ * 用于快速判断某个 type 是否属于朋友圈已知内容范畴
+ */
+ val allTypeIds: Set
+ get() = validTypeSet
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/moe/ouom/wekit/hooks/item/moment/SnsLikeModifyHook.kt b/app/src/main/java/moe/ouom/wekit/hooks/item/moment/SnsLikeModifyHook.kt
new file mode 100644
index 0000000..62677ad
--- /dev/null
+++ b/app/src/main/java/moe/ouom/wekit/hooks/item/moment/SnsLikeModifyHook.kt
@@ -0,0 +1,205 @@
+package moe.ouom.wekit.hooks.item.moment
+
+import android.content.ContentValues
+import android.view.ContextMenu
+import com.afollestad.materialdialogs.MaterialDialog
+import com.afollestad.materialdialogs.list.listItemsMultiChoice
+import moe.ouom.wekit.core.model.BaseSwitchFunctionHookItem
+import moe.ouom.wekit.hooks.core.annotation.HookItem
+import moe.ouom.wekit.hooks.sdk.api.WeDatabaseApi
+import moe.ouom.wekit.hooks.sdk.api.WeDatabaseListener
+import moe.ouom.wekit.hooks.sdk.ui.WeChatSnsContextMenuApi
+import moe.ouom.wekit.ui.CommonContextWrapper
+import moe.ouom.wekit.util.Initiator.loadClass
+import moe.ouom.wekit.util.log.WeLogger
+import java.lang.reflect.Field
+import java.lang.reflect.Method
+import java.util.LinkedList
+
+@HookItem(
+ path = "朋友圈/伪点赞",
+ desc = "自定义修改朋友圈点赞用户列表(伪点赞)"
+)
+class FakeLikesHook : BaseSwitchFunctionHookItem(), WeDatabaseListener.UpdateListener {
+
+ companion object {
+ private const val TAG = "FakeLikesHook"
+ private const val MENU_ID_FAKE_LIKES = 20001
+ private const val TBL_SNS_INFO = "SnsInfo"
+
+ // 存储每个朋友圈动态的伪点赞用户配置 (snsId -> Set<微信id>)
+ private val fakeLikeWxids = mutableMapOf>()
+
+ private var snsObjectClass: Class<*>? = null
+ private var parseFromMethod: Method? = null
+ private var toByteArrayMethod: Method? = null
+ private var likeUserListField: Field? = null
+ private var likeUserListCountField: Field? = null
+ private var likeCountField: Field? = null
+ private var likeFlagField: Field? = null
+ private var snsUserProtobufClass: Class<*>? = null
+ }
+
+ private val onCreateListener = WeChatSnsContextMenuApi.OnCreateListener { menu ->
+ menu.add(ContextMenu.NONE, MENU_ID_FAKE_LIKES, 0, "设置伪点赞")
+ ?.setIcon(android.R.drawable.star_on)
+ }
+
+ private val onSelectListener = WeChatSnsContextMenuApi.OnSelectListener { context, itemId ->
+ if (itemId == MENU_ID_FAKE_LIKES) {
+ showFakeLikesDialog(context)
+ true
+ } else {
+ false
+ }
+ }
+
+ override fun entry(classLoader: ClassLoader) {
+ initReflection(classLoader)
+
+ WeChatSnsContextMenuApi.addOnCreateListener(onCreateListener)
+ WeChatSnsContextMenuApi.addOnSelectListener(onSelectListener)
+
+ WeDatabaseListener.addListener(this)
+ WeLogger.i(TAG, "伪点赞功能已启动")
+ }
+
+ override fun unload(classLoader: ClassLoader) {
+ WeDatabaseListener.removeListener(this)
+
+ WeChatSnsContextMenuApi.removeOnCreateListener(onCreateListener)
+ WeChatSnsContextMenuApi.removeOnSelectListener(onSelectListener)
+ WeLogger.i(TAG, "伪点赞功能已停止")
+ }
+
+ override fun onUpdate(table: String, values: ContentValues): Boolean {
+ try {
+ injectFakeLikes(table, values)
+ } catch (e: Throwable) {
+ WeLogger.e(TAG, "处理数据库更新异常", e)
+ }
+
+ return false // 返回false表示继续原有流程
+ }
+
+ private fun initReflection(classLoader: ClassLoader) {
+ try {
+ snsObjectClass = loadClass("com.tencent.mm.protocal.protobuf.SnsObject")
+
+ snsObjectClass?.let { clazz ->
+ parseFromMethod = clazz.getMethod("parseFrom", ByteArray::class.java)
+ toByteArrayMethod = clazz.getMethod("toByteArray")
+
+ listOf("LikeUserList", "LikeUserListCount", "LikeCount", "LikeFlag").forEach { name ->
+ clazz.getDeclaredField(name).also { field ->
+ field.isAccessible = true
+ when (name) {
+ "LikeUserList" -> likeUserListField = field
+ "LikeUserListCount" -> likeUserListCountField = field
+ "LikeCount" -> likeCountField = field
+ "LikeFlag" -> likeFlagField = field
+ }
+ }
+ }
+ }
+
+ snsUserProtobufClass = loadClass("com.tencent.mm.plugin.sns.ui.SnsCommentFooter")
+ .getMethod("getCommentInfo").returnType
+
+ WeLogger.d(TAG, "反射初始化成功")
+
+ } catch (e: Exception) {
+ WeLogger.e(TAG, "反射初始化失败", e)
+ }
+ }
+
+ private fun injectFakeLikes(tableName: String, values: ContentValues) = runCatching {
+ if (tableName != TBL_SNS_INFO) return@runCatching
+ val snsId = values.get("snsId") as? Long ?: return@runCatching
+ val fakeWxids = fakeLikeWxids[snsId] ?: emptySet()
+ if (fakeWxids.isEmpty() || snsObjectClass == null || snsUserProtobufClass == null) return@runCatching
+
+ val snsObj = snsObjectClass!!.getDeclaredConstructor().newInstance()
+ parseFromMethod?.invoke(snsObj, values.get("attrBuf") as? ByteArray ?: return@runCatching)
+
+ val fakeList = LinkedList().apply {
+ fakeWxids.forEach { wxid ->
+ snsUserProtobufClass!!.getDeclaredConstructor().newInstance().apply {
+ javaClass.getDeclaredField("d").apply { isAccessible = true }.set(this, wxid)
+ add(this)
+ }
+ }
+ }
+
+ likeUserListField?.set(snsObj, fakeList)
+ likeUserListCountField?.set(snsObj, fakeList.size)
+ likeCountField?.set(snsObj, fakeList.size)
+ likeFlagField?.set(snsObj, 1)
+
+ values.put("attrBuf", toByteArrayMethod?.invoke(snsObj) as? ByteArray ?: return@runCatching)
+ WeLogger.i(TAG, "成功为朋友圈 $snsId 注入 ${fakeList.size} 个伪点赞")
+ }.onFailure { WeLogger.e(TAG, "注入伪点赞失败", it) }
+
+ /**
+ * 显示伪点赞用户选择对话框
+ */
+ private fun showFakeLikesDialog(context: WeChatSnsContextMenuApi.SnsContext) {
+ try {
+ // 获取所有好友列表
+ val allFriends = WeDatabaseApi.INSTANCE?.getAllConnects() ?: return
+
+ val displayItems = allFriends.map { contact ->
+ buildString {
+ // 如果有备注,显示"备注(昵称)"
+ if (contact.conRemark.isNotBlank()) {
+ append(contact.conRemark)
+ if (contact.nickname.isNotBlank()) {
+ append(" (${contact.nickname})")
+ }
+ }
+ // 否则直接显示昵称
+ else if (contact.nickname.isNotBlank()) {
+ append(contact.nickname)
+ }
+ // 最后备选用wxid
+ else {
+ append(contact.username)
+ }
+ }
+ }
+
+ val snsInfo = context.snsInfo
+ val snsId = context.snsInfo!!.javaClass.superclass!!.getDeclaredField("field_snsId").apply { isAccessible = true }.get(snsInfo) as Long
+ val currentSelected = fakeLikeWxids[snsId] ?: emptySet()
+
+ val currentIndices = allFriends.mapIndexedNotNull { index, contact ->
+ if (currentSelected.contains(contact.username)) index else null
+ }.toIntArray()
+
+ val wrappedContext = CommonContextWrapper.createAppCompatContext(context.activity)
+
+ // 显示多选对话框
+ MaterialDialog(wrappedContext).show {
+ title(text = "选择伪点赞用户")
+ listItemsMultiChoice(
+ items = displayItems,
+ initialSelection = currentIndices
+ ) { dialog, indices, items ->
+ val selectedWxids = indices.map { allFriends[it].username }.toSet()
+
+ if (selectedWxids.isEmpty()) {
+ fakeLikeWxids.remove(snsId)
+ WeLogger.d(TAG, "已清除朋友圈 $snsId 的伪点赞配置")
+ } else {
+ fakeLikeWxids[snsId] = selectedWxids
+ WeLogger.d(TAG, "已设置朋友圈 $snsId 的伪点赞: $selectedWxids")
+ }
+ }
+ positiveButton(text = "确定")
+ negativeButton(text = "取消")
+ }
+ } catch (e: Exception) {
+ WeLogger.e(TAG, "显示选择对话框失败", e)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/moe/ouom/wekit/hooks/item/script/ScriptConfigHookItem.kt b/app/src/main/java/moe/ouom/wekit/hooks/item/script/ScriptConfigHookItem.kt
index 88672cf..0e2e93c 100644
--- a/app/src/main/java/moe/ouom/wekit/hooks/item/script/ScriptConfigHookItem.kt
+++ b/app/src/main/java/moe/ouom/wekit/hooks/item/script/ScriptConfigHookItem.kt
@@ -1,41 +1,37 @@
package moe.ouom.wekit.hooks.item.script
import android.content.Context
-import android.util.Base64
-import android.view.Gravity
-import android.widget.LinearLayout
-import android.widget.TextView
-import androidx.appcompat.widget.AppCompatEditText
-import androidx.core.content.ContextCompat
-import com.afollestad.materialdialogs.MaterialDialog
-import com.afollestad.materialdialogs.customview.customView
-import com.afollestad.materialdialogs.list.listItems
-import com.google.android.material.button.MaterialButton
import moe.ouom.wekit.core.model.BaseClickableFunctionHookItem
import moe.ouom.wekit.hooks.core.annotation.HookItem
+import moe.ouom.wekit.hooks.sdk.protocol.WePkgHelper
import moe.ouom.wekit.hooks.sdk.protocol.WePkgManager
import moe.ouom.wekit.hooks.sdk.protocol.intf.IWePkgInterceptor
-import moe.ouom.wekit.ui.CommonContextWrapper
-import moe.ouom.wekit.ui.creator.dialog.BaseSettingsDialog
+import moe.ouom.wekit.ui.creator.dialog.ScriptManagerDialog
import moe.ouom.wekit.util.WeProtoData
-import moe.ouom.wekit.util.common.Toasts.showToast
import moe.ouom.wekit.util.log.WeLogger
+import moe.ouom.wekit.util.script.JsExecutor
import moe.ouom.wekit.util.script.ScriptEvalManager
import moe.ouom.wekit.util.script.ScriptFileManager
-import org.json.JSONObject
-import java.text.SimpleDateFormat
-import java.util.*
/**
- * 脚本配置管理器Hook项(包含对话框)
+ * 脚本配置管理器Hook项
*/
@HookItem(
path = "脚本管理/脚本开关",
- desc = "管理JavaScript脚本配置"
+ desc = "点击管理JavaScript脚本配置"
)
class ScriptConfigHookItem : BaseClickableFunctionHookItem(), IWePkgInterceptor {
+ private fun sendCgi(uri: String, cgiId: Int, funcId: Int, routeId: Int, jsonPayload: String) {
+ WePkgHelper.INSTANCE?.sendCgi(uri, cgiId, funcId, routeId, jsonPayload) {
+ onSuccess { json, bytes -> WeLogger.e("异步CGI请求成功:回包: $json") }
+ onFail { type, code, msg -> WeLogger.e("异步CGI请求失败: $type, $code, $msg") }
+ }
+ }
+
override fun entry(classLoader: ClassLoader) {
+ // 注入脚本接口
+ JsExecutor.getInstance().injectScriptInterfaces(::sendCgi, WeApiUtils, WeProtoUtils, WeDataBaseUtils, WeMessageUtils)
// 注册拦截器
WePkgManager.addInterceptor(this)
}
@@ -88,715 +84,4 @@ class ScriptConfigHookItem : BaseClickableFunctionHookItem(), IWePkgInterceptor
ScriptManagerDialog(context, scriptManager, jsEvalManager).show()
}
- /**
- * 脚本管理器对话框
- */
- inner class ScriptManagerDialog(
- context: Context,
- private val scriptManager: ScriptFileManager,
- private val scriptEvalManager: ScriptEvalManager
- ) : BaseSettingsDialog(context, "脚本管理器") {
-
- private val scripts = mutableListOf()
- private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
-
- override fun initList() {
- contentContainer.removeAllViews()
- loadScripts()
- renderScriptList()
- }
-
- private fun loadScripts() {
- scripts.clear()
- scripts.addAll(scriptManager.getAllScripts())
- }
-
- private fun renderScriptList() {
- if (scripts.isEmpty()) {
- renderEmptyView()
- renderActionButtons()
- return
- }
-
- scripts.sortBy { it.order }
- scripts.forEachIndexed { index, script ->
- renderScriptItem(index, script)
- }
-
- renderActionButtons()
- }
-
- private fun renderEmptyView() {
- val emptyView = TextView(context).apply {
- text = "暂无脚本,点击下方按钮添加"
- gravity = Gravity.CENTER
- textSize = 16f
- setPadding(32, 64, 32, 64)
- setTextColor(ContextCompat.getColor(context, android.R.color.darker_gray))
- }
- contentContainer.addView(emptyView)
- }
-
- private fun renderScriptItem(index: Int, script: ScriptFileManager.ScriptConfig) {
- val itemLayout = LinearLayout(context).apply {
- orientation = LinearLayout.VERTICAL
- setPadding(32, 16, 32, 16)
- setBackgroundResource(android.R.color.transparent)
- }
-
- // 标题行
- val titleRow = LinearLayout(context).apply {
- orientation = LinearLayout.HORIZONTAL
- layoutParams = LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.MATCH_PARENT,
- LinearLayout.LayoutParams.WRAP_CONTENT
- )
- }
-
- val tvTitle = TextView(context).apply {
- text = "${index + 1}. ${script.name}"
- textSize = 16f
- layoutParams = LinearLayout.LayoutParams(
- 0,
- LinearLayout.LayoutParams.WRAP_CONTENT,
- 1f
- )
- }
-
- val cbEnabled = androidx.appcompat.widget.AppCompatCheckBox(context).apply {
- isChecked = script.enabled
- setOnCheckedChangeListener { _, isChecked ->
- script.enabled = isChecked
- scriptManager.saveScript(script)
- showToast(context, if (isChecked) "已启用" else "已禁用")
- }
- }
-
- titleRow.addView(tvTitle)
- titleRow.addView(cbEnabled)
-
- // 脚本信息行 (UUID和创建时间)
- val infoRow = LinearLayout(context).apply {
- orientation = LinearLayout.VERTICAL
- setPadding(0, 4, 0, 0)
- }
-
- // UUID显示
- val tvUuid = TextView(context).apply {
- text = "ID: ${script.id}..."
- textSize = 10f
- setTextColor(ContextCompat.getColor(context, android.R.color.darker_gray))
- }
-
- // 时间信息
- val tvTime = TextView(context).apply {
- val createdTime = dateFormat.format(Date(script.createdTime))
- val modifiedTime = dateFormat.format(Date(script.modifiedTime))
- text = "创建: $createdTime | 修改: $modifiedTime"
- textSize = 10f
- setTextColor(ContextCompat.getColor(context, android.R.color.darker_gray))
- }
-
- infoRow.addView(tvUuid)
- infoRow.addView(tvTime)
-
- // 描述和预览
- if (script.description.isNotEmpty()) {
- val tvDesc = TextView(context).apply {
- text = "描述: ${script.description}"
- textSize = 12f
- setTextColor(ContextCompat.getColor(context, android.R.color.darker_gray))
- setPadding(0, 4, 0, 0)
- }
- itemLayout.addView(tvDesc)
- }
-
- // 操作按钮行
- val buttonRow = LinearLayout(context).apply {
- orientation = LinearLayout.HORIZONTAL
- layoutParams = LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.MATCH_PARENT,
- LinearLayout.LayoutParams.WRAP_CONTENT
- ).apply {
- topMargin = 8
- }
- }
-
- val btnCopy = MaterialButton(context).apply {
- text = "复制"
- layoutParams = LinearLayout.LayoutParams(
- 0,
- LinearLayout.LayoutParams.WRAP_CONTENT,
- 1f
- ).apply {
- marginEnd = 8
- }
- setOnClickListener {
- copyFullScriptInfo(script)
- }
- }
-
- val btnActions = MaterialButton(context).apply {
- text = "操作"
- layoutParams = LinearLayout.LayoutParams(
- 0,
- LinearLayout.LayoutParams.WRAP_CONTENT,
- 1f
- ).apply {
- marginStart = 8
- }
- setOnClickListener {
- showActionMenu(index, script)
- }
- }
-
- buttonRow.addView(btnCopy)
- buttonRow.addView(btnActions)
-
- itemLayout.addView(titleRow)
- itemLayout.addView(infoRow)
- itemLayout.addView(buttonRow)
-
- contentContainer.addView(itemLayout)
-
- if (index < scripts.size - 1) {
- val divider = TextView(context).apply {
- setBackgroundColor(ContextCompat.getColor(context, android.R.color.darker_gray))
- layoutParams = LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.MATCH_PARENT,
- 1
- ).apply {
- setMargins(32, 16, 32, 16)
- }
- }
- contentContainer.addView(divider)
- }
- }
-
- /**
- * 显示操作菜单
- */
- private fun showActionMenu(index: Int, script: ScriptFileManager.ScriptConfig) {
- val wrappedContext = CommonContextWrapper.createAppCompatContext(context)
- val options = mutableListOf()
- val isTop = index == 0
- val isBottom = index == scripts.size - 1
-
- options.add("上移")
- options.add("下移")
- options.add("编辑")
- options.add("删除")
- options.add("测试")
-
- MaterialDialog(wrappedContext)
- .title(text = "脚本操作")
- .listItems(items = options) { dialog, optionIndex, _ ->
- when (optionIndex) {
- 0 -> {
- // 上移
- if (isTop) {
- showToast(context, "已在顶部,无法上移")
- return@listItems
- }
- moveScriptUp(index)
- }
-
- 1 -> {
- // 下移
- if (isBottom) {
- showToast(context, "已在底部,无法下移")
- return@listItems
- }
- moveScriptDown(index)
- }
-
- 2 -> showEditDialog(index)
- 3 -> confirmDeleteScript(index)
- 4 -> showTestDialog(index)
- }
-
- dialog.dismiss()
- }
- .negativeButton(text = "取消")
- .show()
- }
-
- /**
- * 复制完整的脚本信息
- */
- private fun copyFullScriptInfo(script: ScriptFileManager.ScriptConfig) {
- val contentEncoded = Base64.encodeToString(script.content.toByteArray(Charsets.UTF_8), Base64.DEFAULT)
- val jsonObject = JSONObject().apply {
- put("name", script.name)
- put("id", script.id)
- put("description", script.description)
- put("content", contentEncoded)
- }
-
- copyToClipboard("脚本JSON", jsonObject.toString(2))
- showToast(context, "已复制脚本JSON格式")
- }
-
- private fun renderActionButtons() {
- val buttonLayout = LinearLayout(context).apply {
- orientation = LinearLayout.HORIZONTAL
- gravity = Gravity.CENTER
- setPadding(32, 24, 32, 32)
- }
-
- val btnAdd = MaterialButton(context).apply {
- text = "添加脚本"
- layoutParams = LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.WRAP_CONTENT,
- LinearLayout.LayoutParams.WRAP_CONTENT
- ).apply {
- marginEnd = 16
- }
- setOnClickListener {
- showAddDialog()
- }
- }
-
- val btnImport = MaterialButton(context).apply {
- text = "导入"
- layoutParams = LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.WRAP_CONTENT,
- LinearLayout.LayoutParams.WRAP_CONTENT
- )
- setOnClickListener {
- showImportOptions()
- }
- }
-
- buttonLayout.addView(btnAdd)
- buttonLayout.addView(btnImport)
- contentContainer.addView(buttonLayout)
- }
-
- private fun showAddDialog() {
- showEditDialog(-1)
- }
-
- private fun showEditDialog(index: Int) {
- val isNew = index == -1
- val script = if (isNew) {
- val newId = UUID.randomUUID().toString()
- ScriptFileManager.ScriptConfig(
- id = newId,
- name = "",
- content = "",
- order = scripts.size
- )
- } else {
- scripts.getOrNull(index) ?: return
- }
-
- val wrappedContext = CommonContextWrapper.createAppCompatContext(context)
-
- val dialogView = LinearLayout(context).apply {
- orientation = LinearLayout.VERTICAL
- setPadding(32, 32, 32, 32)
- }
-
- // UUID显示
- val tvUuidLabel = TextView(context).apply {
- text = "脚本ID: ${script.id}"
- textSize = 12f
- setTextColor(ContextCompat.getColor(context, android.R.color.darker_gray))
- setPadding(0, 0, 0, 8)
- }
-
- val tvNameLabel = TextView(context).apply {
- text = "脚本名称:"
- textSize = 14f
- setPadding(0, 8, 0, 8)
- }
-
- val etName = AppCompatEditText(context).apply {
- hint = "输入脚本名称"
- setText(script.name)
- textSize = 14f
- }
-
- val tvDescLabel = TextView(context).apply {
- text = "描述(可选):"
- textSize = 14f
- setPadding(0, 16, 0, 8)
- }
-
- val etDesc = AppCompatEditText(context).apply {
- hint = "输入脚本描述"
- setText(script.description)
- textSize = 14f
- }
-
- val tvContentLabel = TextView(context).apply {
- text = "脚本内容 (JavaScript):"
- textSize = 14f
- setPadding(0, 16, 0, 8)
- }
-
- val etContent = AppCompatEditText(context).apply {
- hint = "输入JavaScript代码..."
- setText(script.content)
- minLines = 10
- maxLines = 20
- gravity = Gravity.START or Gravity.TOP
- }
-
- dialogView.addView(tvUuidLabel)
- dialogView.addView(tvNameLabel)
- dialogView.addView(etName)
- dialogView.addView(tvDescLabel)
- dialogView.addView(etDesc)
- dialogView.addView(tvContentLabel)
- dialogView.addView(etContent)
-
- MaterialDialog(wrappedContext)
- .customView(view = dialogView)
- .title(text = if (isNew) "添加脚本" else "编辑脚本")
- .positiveButton(text = "保存") {
- val name = etName.text.toString().trim()
- val description = etDesc.text.toString().trim()
- val content = etContent.text.toString().trim()
-
- if (name.isEmpty()) {
- showToast(context, "请输入脚本名称")
- return@positiveButton
- }
-
- if (content.isEmpty()) {
- showToast(context, "请输入脚本内容")
- return@positiveButton
- }
-
- saveScript(script, name, description, content, isNew)
- }
- .neutralButton(text = "复制脚本内容") {
- val content = etContent.text.toString().trim()
- if (content.isNotEmpty()) {
- copyToClipboard("脚本内容", content)
- showToast(context, "已复制脚本内容")
- } else {
- showToast(context, "脚本内容为空")
- }
- }
- .negativeButton(text = "取消")
- .show()
- }
-
- /**
- * 显示测试对话框
- */
- private fun showTestDialog(index: Int) {
- val script = scripts.getOrNull(index) ?: return
- val wrappedContext = CommonContextWrapper.createAppCompatContext(context)
-
- val dialogView = LinearLayout(context).apply {
- orientation = LinearLayout.VERTICAL
- setPadding(32, 32, 32, 32)
- }
-
- val tvScriptName = TextView(context).apply {
- text = "测试脚本: ${script.name}"
- textSize = 16f
- setPadding(0, 0, 0, 16)
- }
-
- val tvInputLabel = TextView(context).apply {
- text = "输入要执行的JavaScript代码:"
- textSize = 14f
- setPadding(0, 0, 0, 8)
- }
-
- val etInput = AppCompatEditText(context).apply {
- hint = "例如: wekit.log(onRequest({uri: 'uri',cgiId: 0,jsonData: {}}));"
- textSize = 14f
- minLines = 5
- maxLines = 10
- gravity = Gravity.START or Gravity.TOP
- }
-
- dialogView.addView(tvScriptName)
- dialogView.addView(tvInputLabel)
- dialogView.addView(etInput)
-
- MaterialDialog(wrappedContext)
- .customView(view = dialogView)
- .title(text = "测试脚本执行")
- .positiveButton(text = "执行") {
- val jsCode = etInput.text.toString().trim()
- if (jsCode.isNotEmpty()) {
- executeTestScript(script, jsCode)
- } else {
- showToast(context, "请输入要执行的JavaScript代码")
- }
- }
- .negativeButton(text = "取消")
- .show()
- }
-
- /**
- * 执行测试脚本
- */
- private fun executeTestScript(script: ScriptFileManager.ScriptConfig, jsCode: String) {
- val wrappedContext = CommonContextWrapper.createAppCompatContext(context)
-
- val result = scriptEvalManager.testExecuteCode(script.content, jsCode, "${script.name}+测试脚本")
- MaterialDialog(wrappedContext)
- .title(text = "执行结果")
- .message(text = result ?: "执行失败或无返回值")
- .positiveButton(text = "复制结果") {
- copyToClipboard("执行结果", result ?: "执行失败")
- }
- .negativeButton(text = "关闭")
- .show()
- }
-
- private fun saveScript(
- script: ScriptFileManager.ScriptConfig,
- name: String,
- description: String,
- content: String,
- isNew: Boolean
- ) {
- // 检测脚本是否正常
- val testResult = scriptEvalManager.testScriptMethods(content)
- if (!testResult.isPassed()) {
- val errorMsg = testResult.getSummary()
- showToast(context, "脚本验证失败,请修正后保存")
-
- // 显示详细错误信息
- val wrappedContext = CommonContextWrapper.createAppCompatContext(context)
- MaterialDialog(wrappedContext)
- .title(text = "脚本验证失败")
- .message(text = errorMsg)
- .positiveButton(text = "返回编辑") {
- showEditDialog(if (isNew) -1 else scripts.indexOf(script))
- }
- .negativeButton(text = "取消")
- .show()
- return
- }
-
- script.name = name
- script.description = description
- script.content = content
- script.modifiedTime = System.currentTimeMillis()
-
- if (isNew) {
- script.createdTime = System.currentTimeMillis()
- scripts.add(script)
- scriptManager.saveScript(script)
- showToast(context, "脚本已添加")
- } else {
- scriptManager.saveScript(script)
- showToast(context, "脚本已更新")
- }
-
- contentContainer.removeAllViews()
- renderScriptList()
- }
-
- private fun showImportOptions() {
- val wrappedContext = CommonContextWrapper.createAppCompatContext(context)
- val options = listOf("从剪贴板导入", "导入示例")
-
- MaterialDialog(wrappedContext)
- .title(text = "导入脚本")
- .listItems(items = options) { dialog, optionIndex, _ ->
- when (optionIndex) {
- 0 -> importFromClipboard()
- 1 -> importExample()
- }
- dialog.dismiss()
- }
- .negativeButton(text = "取消")
- .show()
- }
-
- private fun importFromClipboard() {
- try {
- val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? android.content.ClipboardManager
- val clipData = clipboard?.primaryClip
-
- if (clipData != null && clipData.itemCount > 0) {
- val clipItem = clipData.getItemAt(0)
- val text = clipItem.text?.toString()
-
- if (!text.isNullOrBlank()) {
- // 尝试解析为JSON格式
- val jsonObject = JSONObject(text)
-
- // 验证必需字段
- if (jsonObject.has("name") && jsonObject.has("id") && jsonObject.has("description") && jsonObject.has(
- "content"
- )
- ) {
- val newName = jsonObject.optString("name", "从剪贴板导入")
- val newId = jsonObject.optString("id", UUID.randomUUID().toString())
- val newDescription = jsonObject.optString("description", "")
- val contentEncoded = jsonObject.optString("content", "")
-
- // 解码内容
- val contentBytes = Base64.decode(contentEncoded, Base64.NO_WRAP)
- val newContent = String(contentBytes, Charsets.UTF_8)
-
- // 检查是否已存在相同ID的脚本
- val existingScript = scripts.find { it.id == newId }
- if (existingScript != null) {
- // 覆盖现有脚本
- existingScript.name = newName
- existingScript.content = newContent
- existingScript.description = newDescription
- existingScript.modifiedTime = System.currentTimeMillis()
-
- scriptManager.saveScript(existingScript)
- showToast(context, "已覆盖相同ID的脚本")
-
- contentContainer.removeAllViews()
- renderScriptList()
- return
- }
-
- val newScript = ScriptFileManager.ScriptConfig(
- id = newId,
- name = newName,
- content = newContent,
- description = newDescription,
- order = scripts.size
- )
-
- scriptManager.saveScript(newScript)
- scripts.add(newScript)
-
- contentContainer.removeAllViews()
- renderScriptList()
- showToast(context, "已从剪贴板导入脚本")
- } else {
- showToast(context, "剪贴板内容格式不正确,缺少必要字段(name, id, description, content)")
- }
- } else {
- showToast(context, "剪贴板内容为空")
- }
- } else {
- showToast(context, "剪贴板无内容")
- }
- } catch (e: Exception) {
- WeLogger.e("从剪贴板导入失败: ${e.message}")
- showToast(context, "导入失败,请确保剪贴板内容为有效的JSON格式")
- }
- }
-
- private fun importExample() {
- val exampleScripts = listOf(
- ScriptFileManager.ScriptConfig(
- id = UUID.randomUUID().toString(),
- name = "打印示例",
- content = """
- // 这是一个简单的示例脚本 将打印参数
- function onRequest(data) {
- const {uri,cgiId,jsonData} = data;
- wekit.log('拦截请求:', uri, cgiId, JSON.stringify(jsonData));
- return jsonData;
- }
-
- function onResponse(data) {
- const {uri,cgiId,jsonData} = data;
- wekit.log('拦截响应:', uri, cgiId, JSON.stringify(jsonData));
- return jsonData;
- }
- """.trimIndent(),
- description = "最简单的示例脚本,包含onRequest和onResponse方法",
- order = scripts.size
- )
- )
-
- exampleScripts.forEach { script ->
- scriptManager.saveScript(script)
- scripts.add(script)
- }
-
- contentContainer.removeAllViews()
- renderScriptList()
- showToast(context, "已导入示例脚本")
- }
-
- private fun confirmDeleteScript(index: Int) {
- val script = scripts.getOrNull(index) ?: return
- val wrappedContext = CommonContextWrapper.createAppCompatContext(context)
-
- MaterialDialog(wrappedContext)
- .title(text = "确认删除")
- .message(text = "确定要删除脚本「${script.name}」吗?\nID: ${script.id}")
- .positiveButton(text = "删除") {
- deleteScript(index)
- }
- .negativeButton(text = "取消")
- .show()
- }
-
- private fun deleteScript(index: Int) {
- val script = scripts.getOrNull(index) ?: return
-
- if (scriptManager.deleteScript(script.id)) {
- scripts.removeAt(index)
-
- scripts.forEachIndexed { i, s ->
- s.order = i
- scriptManager.saveScript(s)
- }
-
- contentContainer.removeAllViews()
- renderScriptList()
- showToast(context, "脚本已删除")
- } else {
- showToast(context, "删除失败")
- }
- }
-
- private fun moveScriptUp(index: Int) {
- if (index > 0) {
- val temp = scripts[index]
- scripts[index] = scripts[index - 1]
- scripts[index - 1] = temp
-
- scripts.forEachIndexed { i, s ->
- s.order = i
- scriptManager.saveScript(s)
- }
-
- contentContainer.removeAllViews()
- renderScriptList()
- }
- }
-
- private fun moveScriptDown(index: Int) {
- if (index < scripts.size - 1) {
- val temp = scripts[index]
- scripts[index] = scripts[index + 1]
- scripts[index + 1] = temp
-
- scripts.forEachIndexed { i, s ->
- s.order = i
- scriptManager.saveScript(s)
- }
-
- contentContainer.removeAllViews()
- renderScriptList()
- }
- }
-
- private fun copyToClipboard(label: String, text: String) {
- try {
- val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? android.content.ClipboardManager
- val clip = android.content.ClipData.newPlainText(label, text)
- clipboard?.setPrimaryClip(clip)
- } catch (e: Exception) {
- WeLogger.e("复制失败: ${e.message}")
- }
- }
-
- }
-
}
\ No newline at end of file
diff --git a/app/src/main/java/moe/ouom/wekit/hooks/item/script/WeApiUtils.kt b/app/src/main/java/moe/ouom/wekit/hooks/item/script/WeApiUtils.kt
new file mode 100644
index 0000000..f127ebf
--- /dev/null
+++ b/app/src/main/java/moe/ouom/wekit/hooks/item/script/WeApiUtils.kt
@@ -0,0 +1,28 @@
+@file:Suppress("unused")
+
+package moe.ouom.wekit.hooks.item.script
+
+import moe.ouom.wekit.hooks.sdk.protocol.WeApi
+import moe.ouom.wekit.util.log.WeLogger
+
+object WeApiUtils {
+ private const val TAG = "WeApiUtils"
+
+ fun getSelfWxId(): String {
+ return try {
+ WeApi.getSelfWxId()
+ } catch (e: Exception) {
+ WeLogger.e(TAG, "获取当前微信id失败: ${e.message}")
+ ""
+ }
+ }
+
+ fun getSelfAlias(): String {
+ return try {
+ WeApi.getSelfAlias()
+ } catch (e: Exception) {
+ WeLogger.e(TAG, "获取当前微信号失败: ${e.message}")
+ ""
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/moe/ouom/wekit/hooks/item/script/WeDataBaseUtils.kt b/app/src/main/java/moe/ouom/wekit/hooks/item/script/WeDataBaseUtils.kt
new file mode 100644
index 0000000..50c5a9e
--- /dev/null
+++ b/app/src/main/java/moe/ouom/wekit/hooks/item/script/WeDataBaseUtils.kt
@@ -0,0 +1,160 @@
+@file:Suppress("unused")
+
+package moe.ouom.wekit.hooks.item.script
+
+import moe.ouom.wekit.hooks.sdk.api.WeDatabaseApi
+import moe.ouom.wekit.util.log.WeLogger
+import org.json.JSONArray
+import org.json.JSONObject
+import kotlin.collections.iterator
+
+object WeDataBaseUtils {
+ private const val TAG = "WeDataBaseUtils"
+ private val instance: WeDatabaseApi? by lazy {
+ try {
+ WeDatabaseApi.INSTANCE
+ } catch (e: Exception) {
+ WeLogger.e(TAG, "初始化 WeDatabaseApi 失败: ${e.message}")
+ null
+ }
+ }
+
+ fun query(sql: String): Any {
+ return try {
+ instance?.executeQuery(sql)?.map { row ->
+ val jsonObject = JSONObject()
+ for ((key, value) in row) {
+ jsonObject.put(key, value)
+ }
+ jsonObject
+ }?.let { JSONArray(it) } ?: JSONArray()
+ } catch (e: Exception) {
+ WeLogger.e("WeDatabaseApi", "SQL执行异常: ${e.message}")
+ JSONArray()
+ }
+ }
+
+ fun getAllContacts(): Any {
+ return try {
+ instance?.getAllConnects()?.map { contact ->
+ val jsonObject = JSONObject()
+ jsonObject.put("username", contact.username)
+ jsonObject.put("nickname", contact.nickname)
+ jsonObject.put("alias", contact.alias)
+ jsonObject.put("conRemark", contact.conRemark)
+ jsonObject.put("pyInitial", contact.pyInitial)
+ jsonObject.put("quanPin", contact.quanPin)
+ jsonObject.put("avatarUrl", contact.avatarUrl)
+ jsonObject.put("encryptUserName", contact.encryptUserName)
+ jsonObject
+ }?.let { JSONArray(it) } ?: JSONArray()
+ } catch (e: Exception) {
+ WeLogger.e("WeDatabaseApi", "获取联系人异常: ${e.message}")
+ JSONArray()
+ }
+ }
+
+ fun getContactList(): Any {
+ return try {
+ instance?.getContactList()?.map { contact ->
+ val jsonObject = JSONObject()
+ jsonObject.put("username", contact.username)
+ jsonObject.put("nickname", contact.nickname)
+ jsonObject.put("alias", contact.alias)
+ jsonObject.put("conRemark", contact.conRemark)
+ jsonObject.put("pyInitial", contact.pyInitial)
+ jsonObject.put("quanPin", contact.quanPin)
+ jsonObject.put("avatarUrl", contact.avatarUrl)
+ jsonObject.put("encryptUserName", contact.encryptUserName)
+ jsonObject
+ }?.let { JSONArray(it) } ?: JSONArray()
+ } catch (e: Exception) {
+ WeLogger.e("WeDatabaseApi", "获取好友异常: ${e.message}")
+ JSONArray()
+ }
+ }
+
+ fun getChatrooms(): Any {
+ return try {
+ instance?.getChatroomList()?.map { group ->
+ val jsonObject = JSONObject()
+ jsonObject.put("username", group.username)
+ jsonObject.put("nickname", group.nickname)
+ jsonObject.put("pyInitial", group.pyInitial)
+ jsonObject.put("quanPin", group.quanPin)
+ jsonObject.put("avatarUrl", group.avatarUrl)
+ jsonObject
+ }?.let { JSONArray(it) } ?: JSONArray()
+ } catch (e: Exception) {
+ WeLogger.e("WeDatabaseApi", "获取群聊异常: ${e.message}")
+ JSONArray()
+ }
+ }
+
+ fun getOfficialAccounts(): Any {
+ return try {
+ instance?.getOfficialAccountList()?.map { account ->
+ val jsonObject = JSONObject()
+ jsonObject.put("username", account.username)
+ jsonObject.put("nickname", account.nickname)
+ jsonObject.put("alias", account.alias)
+ jsonObject.put("signature", account.signature)
+ jsonObject.put("avatarUrl", account.avatarUrl)
+ jsonObject
+ }?.let { JSONArray(it) } ?: JSONArray()
+ } catch (e: Exception) {
+ WeLogger.e("WeDatabaseApi", "获取公众号异常: ${e.message}")
+ JSONArray()
+ }
+ }
+
+ fun getMessages(wxid: String, page: Int = 1, pageSize: Int = 20): Any {
+ return try {
+ if (wxid.isEmpty()) return JSONArray()
+ instance?.getMessages(wxid, page, pageSize)?.map { message ->
+ val jsonObject = JSONObject()
+ jsonObject.put("msgId", message.msgId)
+ jsonObject.put("talker", message.talker)
+ jsonObject.put("content", message.content)
+ jsonObject.put("type", message.type)
+ jsonObject.put("createTime", message.createTime)
+ jsonObject.put("isSend", message.isSend)
+ jsonObject
+ }?.let { JSONArray(it) } ?: JSONArray()
+ } catch (e: Exception) {
+ WeLogger.e("WeDatabaseApi", "获取消息异常: ${e.message}")
+ JSONArray()
+ }
+ }
+
+ fun getAvatarUrl(wxid: String): String {
+ return try {
+ if (wxid.isEmpty()) return ""
+ instance?.getAvatarUrl(wxid) ?: ""
+ } catch (e: Exception) {
+ WeLogger.e("WeDatabaseApi", "获取头像异常: ${e.message}")
+ ""
+ }
+ }
+
+ fun getGroupMembers(chatroomId: String): Any {
+ return try {
+ if (!chatroomId.endsWith("@chatroom")) return JSONArray()
+ instance?.getGroupMembers(chatroomId)?.map { member ->
+ val jsonObject = JSONObject()
+ jsonObject.put("username", member.username)
+ jsonObject.put("nickname", member.nickname)
+ jsonObject.put("alias", member.alias)
+ jsonObject.put("conRemark", member.conRemark)
+ jsonObject.put("pyInitial", member.pyInitial)
+ jsonObject.put("quanPin", member.quanPin)
+ jsonObject.put("avatarUrl", member.avatarUrl)
+ jsonObject.put("encryptUserName", member.encryptUserName)
+ jsonObject
+ }?.let { JSONArray(it) } ?: JSONArray()
+ } catch (e: Exception) {
+ WeLogger.e("WeDatabaseApi", "获取群成员异常: ${e.message}")
+ JSONArray()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/moe/ouom/wekit/hooks/item/script/WeMessageUtils.kt b/app/src/main/java/moe/ouom/wekit/hooks/item/script/WeMessageUtils.kt
new file mode 100644
index 0000000..a380558
--- /dev/null
+++ b/app/src/main/java/moe/ouom/wekit/hooks/item/script/WeMessageUtils.kt
@@ -0,0 +1,96 @@
+@file:Suppress("unused")
+
+package moe.ouom.wekit.hooks.item.script
+
+import moe.ouom.wekit.hooks.sdk.api.WeMessageApi
+import moe.ouom.wekit.util.log.WeLogger
+
+object WeMessageUtils {
+ private const val TAG = "WeMessageUtils"
+ private val instance: WeMessageApi? by lazy {
+ try {
+ WeMessageApi.INSTANCE
+ } catch (e: Exception) {
+ WeLogger.e(TAG, "初始化 WeMessageApi 失败: ${e.message}")
+ null
+ }
+ }
+
+ /**
+ * 发送文本消息
+ * @param toUser 目标用户ID
+ * @param text 消息内容
+ * @return 是否发送成功
+ */
+ fun sendText(toUser: String, text: String): Boolean {
+ return try {
+ instance?.sendText(toUser, text) ?: false
+ } catch (e: Exception) {
+ WeLogger.e(TAG, "发送文本消息失败: ${e.message}")
+ false
+ }
+ }
+
+ /**
+ * 发送图片消息
+ * @param toUser 目标用户ID
+ * @param imgPath 图片路径
+ * @return 是否发送成功
+ */
+ fun sendImage(toUser: String, imgPath: String): Boolean {
+ return try {
+ instance?.sendImage(toUser, imgPath) ?: false
+ } catch (e: Exception) {
+ WeLogger.e(TAG, "发送图片消息失败: ${e.message}")
+ false
+ }
+ }
+
+ /**
+ * 发送文件消息
+ * @param talker 目标用户ID
+ * @param filePath 文件路径
+ * @param title 文件标题
+ * @param appid 应用ID(可选)
+ * @return 是否发送成功
+ */
+ fun sendFile(talker: String, filePath: String, title: String, appid: String? = null): Boolean {
+ return try {
+ instance?.sendFile(talker, filePath, title, appid) ?: false
+ } catch (e: Exception) {
+ WeLogger.e(TAG, "发送文件消息失败: ${e.message}")
+ false
+ }
+ }
+
+ /**
+ * 发送语音消息
+ * @param toUser 目标用户ID
+ * @param path 语音文件路径
+ * @param durationMs 语音时长(毫秒)
+ * @return 是否发送成功
+ */
+ fun sendVoice(toUser: String, path: String, durationMs: Int): Boolean {
+ return try {
+ instance?.sendVoice(toUser, path, durationMs) ?: false
+ } catch (e: Exception) {
+ WeLogger.e(TAG, "发送语音消息失败: ${e.message}")
+ false
+ }
+ }
+
+ /**
+ * 发送XML应用消息
+ * @param toUser 目标用户ID
+ * @param xmlContent XML内容
+ * @return 是否发送成功
+ */
+ fun sendXmlAppMsg(toUser: String, xmlContent: String): Boolean {
+ return try {
+ instance?.sendXmlAppMsg(toUser, xmlContent) ?: false
+ } catch (e: Exception) {
+ WeLogger.e(TAG, "发送XML应用消息失败: ${e.message}")
+ false
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/moe/ouom/wekit/hooks/item/script/WeProtoUtils.kt b/app/src/main/java/moe/ouom/wekit/hooks/item/script/WeProtoUtils.kt
new file mode 100644
index 0000000..8e82d1f
--- /dev/null
+++ b/app/src/main/java/moe/ouom/wekit/hooks/item/script/WeProtoUtils.kt
@@ -0,0 +1,35 @@
+@file:Suppress("unused")
+
+package moe.ouom.wekit.hooks.item.script
+
+import moe.ouom.wekit.util.WeProtoData
+import moe.ouom.wekit.util.log.WeLogger
+import org.json.JSONObject
+import java.util.regex.Pattern
+
+object WeProtoUtils {
+ fun replaceUtf8ContainsInJson(json: JSONObject, needle: String, replacement: String): JSONObject {
+ return try {
+ val protoData = WeProtoData()
+ protoData.fromJSON(json)
+ protoData.replaceUtf8Contains(needle, replacement)
+ protoData.toJSON()
+ } catch (e: Exception) {
+ WeLogger.e("Failed to replace in JSON: ${e.message}")
+ json
+ }
+ }
+
+ fun replaceUtf8RegexInJson(json: JSONObject, pattern: String, replacement: String): JSONObject {
+ return try {
+ val protoData = WeProtoData()
+ protoData.fromJSON(json)
+ val regex = Pattern.compile(pattern)
+ protoData.replaceUtf8Regex(regex, replacement)
+ protoData.toJSON()
+ } catch (e: Exception) {
+ WeLogger.e("Failed to regex replace in JSON: ${e.message}")
+ json
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/moe/ouom/wekit/hooks/sdk/api/WeDatabaseApi.kt b/app/src/main/java/moe/ouom/wekit/hooks/sdk/api/WeDatabaseApi.kt
index 1e2d0f5..84c3d19 100644
--- a/app/src/main/java/moe/ouom/wekit/hooks/sdk/api/WeDatabaseApi.kt
+++ b/app/src/main/java/moe/ouom/wekit/hooks/sdk/api/WeDatabaseApi.kt
@@ -45,6 +45,119 @@ class WeDatabaseApi : ApiHookItem(), IDexFind {
@SuppressLint("StaticFieldLeak")
var INSTANCE: WeDatabaseApi? = null
+
+ // =============================================================================
+ // SQL 语句集中管理
+ // =============================================================================
+ private object SQL {
+ // 基础字段 - 联系人查询常用字段
+ const val CONTACT_FIELDS = """
+ r.username, r.alias, r.conRemark, r.nickname,
+ r.pyInitial, r.quanPin, r.encryptUsername, i.reserved2 AS avatarUrl
+ """
+
+ // 基础字段 - 群聊查询常用字段
+ const val CHATROOM_FIELDS = "r.username, r.nickname, r.pyInitial, r.quanPin, i.reserved2 AS avatarUrl"
+
+ // 基础字段 - 公众号查询常用字段
+ const val OFFICIAL_FIELDS = "r.username, r.alias, r.nickname, i.reserved2 AS avatarUrl"
+
+ // 基础 JOIN 语句
+ const val LEFT_JOIN_IMG_FLAG = "LEFT JOIN img_flag i ON r.username = i.username"
+
+ // =========================================
+ // 联系人查询
+ // =========================================
+
+ /** 所有人类账号(排除群聊和公众号和系统账号) */
+ val ALL_CONNECTS = """
+ SELECT $CONTACT_FIELDS, r.type
+ FROM rcontact r
+ $LEFT_JOIN_IMG_FLAG
+ WHERE
+ r.username != 'filehelper'
+ AND r.verifyFlag = 0
+ AND (r.type & 1) != 0
+ AND (r.type & 8) = 0
+ AND (r.type & 32) = 0
+ """.trimIndent()
+
+ /** 好友列表(排除群聊和公众号和系统账号和自己和假好友) */
+ val CONTACT_LIST = """
+ SELECT $CONTACT_FIELDS, r.type
+ FROM rcontact r
+ $LEFT_JOIN_IMG_FLAG
+ WHERE
+ (
+ r.encryptUsername != '' -- 是真好友
+ OR
+ r.username = (SELECT value FROM userinfo WHERE id = 2) -- 是我自己
+ )
+ AND r.verifyFlag = 0
+ AND (r.type & 1) != 0
+ AND (r.type & 8) = 0
+ AND (r.type & 32) = 0
+ """.trimIndent()
+
+ // =========================================
+ // 群聊查询
+ // =========================================
+
+ /** 所有群聊 */
+ val CHATROOM_LIST = """
+ SELECT $CHATROOM_FIELDS
+ FROM rcontact r
+ $LEFT_JOIN_IMG_FLAG
+ WHERE r.username LIKE '%@chatroom'
+ """.trimIndent()
+
+ /** 获取群成员列表 */
+ fun groupMembers(idsStr: String) = """
+ SELECT $CONTACT_FIELDS
+ FROM rcontact r
+ $LEFT_JOIN_IMG_FLAG
+ WHERE r.username IN ($idsStr)
+ """.trimIndent()
+
+ // =========================================
+ // 公众号查询
+ // =========================================
+
+ /** 所有公众号 */
+ val OFFICIAL_LIST = """
+ SELECT $OFFICIAL_FIELDS
+ FROM rcontact r
+ $LEFT_JOIN_IMG_FLAG
+ WHERE r.username LIKE 'gh_%'
+ """.trimIndent()
+
+ // =========================================
+ // 消息查询
+ // =========================================
+
+ /** 分页获取消息 */
+ fun messages(wxid: String, limit: Int, offset: Int) = """
+ SELECT msgId, talker, content, type, createTime, isSend
+ FROM message
+ WHERE talker='$wxid'
+ ORDER BY createTime DESC
+ LIMIT $limit OFFSET $offset
+ """.trimIndent()
+
+ // =========================================
+ // 头像查询
+ // =========================================
+
+ /** 获取头像URL */
+ fun avatar(wxid: String) = """
+ SELECT i.reserved2 AS avatarUrl
+ FROM img_flag i
+ WHERE i.username = '$wxid'
+ """.trimIndent()
+
+ /** 获取群聊成员列表字符串 */
+ val CHATROOM_MEMBERS = "SELECT memberlist FROM chatroom WHERE chatroomname = '%s'"
+ }
}
@SuppressLint("NonUniqueDexKitData")
@@ -247,66 +360,21 @@ class WeDatabaseApi : ApiHookItem(), IDexFind {
* 返回所有人类账号(包含好友、陌生人、自己),但排除群和公众号
*/
fun getAllConnects(): List {
- val sql = """
- SELECT
- r.username, r.alias, r.conRemark, r.nickname, r.pyInitial, r.quanPin,
- r.encryptUsername, i.reserved2 AS avatarUrl
- FROM rcontact r
- LEFT JOIN img_flag i ON r.username = i.username
- WHERE
- r.username NOT LIKE '%@chatroom'
- AND r.username NOT LIKE 'gh_%'
- AND r.username != 'filehelper'
- AND r.verifyFlag = 0
- -- 移除了 type & 1 校验,允许返回非好友
- """.trimIndent()
- return mapToContact(executeQuery(sql))
+ return mapToContact(executeQuery(SQL.ALL_CONNECTS))
}
/**
* 获取【好友】
*/
fun getContactList(): List {
- val sql = """
- SELECT
- r.username, r.alias, r.conRemark, r.nickname, r.pyInitial, r.quanPin,
- r.encryptUsername, i.reserved2 AS avatarUrl
- FROM rcontact r
- LEFT JOIN img_flag i ON r.username = i.username
- WHERE
- r.username NOT LIKE '%@chatroom'
- AND r.username NOT LIKE 'gh_%'
- AND r.verifyFlag = 0
- AND (r.type & 1) != 0
- AND (
- r.encryptUsername != '' -- 是真好友
- OR
- r.username = (SELECT value FROM userinfo WHERE id = 2) -- 是我自己
- )
- AND r.username NOT IN (
- 'filehelper', 'qqmail', 'fmessage', 'tmessage', 'qmessage',
- 'floatbottle', 'lbsapp', 'shakeapp', 'medianote', 'qqfriend',
- 'newsapp', 'blogapp', 'facebookapp', 'masssendapp', 'feedsapp',
- 'voipapp', 'cardpackage', 'voicevoipapp', 'voiceinputapp',
- 'officialaccounts', 'linkedinplugin', 'notifymessage',
- 'appbrandcustomerservicemsg', 'appbrand_notify_message',
- 'downloaderapp', 'opencustomerservicemsg', 'weixin',
- 'weibo', 'pc_share', 'wxitil'
- )
- """.trimIndent()
- return mapToContact(executeQuery(sql))
+ return mapToContact(executeQuery(SQL.CONTACT_LIST))
}
/**
* 获取【群聊】
*/
fun getChatroomList(): List {
- val sql = """
- SELECT r.username, r.nickname, r.pyInitial, r.quanPin, i.reserved2 AS avatarUrl
- FROM rcontact r LEFT JOIN img_flag i ON r.username = i.username
- WHERE r.username LIKE '%@chatroom'
- """.trimIndent()
- return executeQuery(sql).map { row ->
+ return executeQuery(SQL.CHATROOM_LIST).map { row ->
WeGroup(
username = row.str("username"),
nickname = row.str("nickname"),
@@ -324,7 +392,7 @@ class WeDatabaseApi : ApiHookItem(), IDexFind {
fun getGroupMembers(chatroomId: String): List {
if (!chatroomId.endsWith("@chatroom")) return emptyList()
- val roomSql = "SELECT memberlist FROM chatroom WHERE chatroomname = '$chatroomId'"
+ val roomSql = SQL.CHATROOM_MEMBERS.format(chatroomId)
val roomResult = executeQuery(roomSql)
if (roomResult.isEmpty()) {
@@ -340,32 +408,14 @@ class WeDatabaseApi : ApiHookItem(), IDexFind {
val idsStr = members.joinToString(",") { "'$it'" }
- val sql = """
- SELECT
- r.username, r.alias, r.conRemark, r.nickname, r.pyInitial, r.quanPin,
- r.encryptUsername, i.reserved2 AS avatarUrl
- FROM rcontact r
- LEFT JOIN img_flag i ON r.username = i.username
- WHERE r.username IN ($idsStr)
- """.trimIndent()
-
- return mapToContact(executeQuery(sql))
+ return mapToContact(executeQuery(SQL.groupMembers(idsStr)))
}
/**
* 获取【公众号】
*/
fun getOfficialAccountList(): List {
- val sql = """
- SELECT
- r.username, r.alias, r.nickname,
- i.reserved2 AS avatarUrl
- FROM rcontact r
- LEFT JOIN img_flag i ON r.username = i.username
- WHERE r.username LIKE 'gh_%'
- """.trimIndent()
-
- return executeQuery(sql).map { row ->
+ return executeQuery(SQL.OFFICIAL_LIST).map { row ->
WeOfficial(
username = row.str("username"),
nickname = row.str("nickname"),
@@ -382,9 +432,7 @@ class WeDatabaseApi : ApiHookItem(), IDexFind {
fun getMessages(wxid: String, page: Int = 1, pageSize: Int = 20): List {
if (wxid.isEmpty()) return emptyList()
val offset = (page - 1) * pageSize
- val sql = "SELECT msgId, talker, content, type, createTime, isSend FROM message WHERE talker='$wxid' ORDER BY createTime DESC LIMIT $pageSize OFFSET $offset"
-
- return executeQuery(sql).map { row ->
+ return executeQuery(SQL.messages(wxid, pageSize, offset)).map { row ->
WeMessage(
msgId = row.long("msgId"),
talker = row.str("talker"),
@@ -401,8 +449,7 @@ class WeDatabaseApi : ApiHookItem(), IDexFind {
*/
fun getAvatarUrl(wxid: String): String {
if (wxid.isEmpty()) return ""
- val sql = "SELECT i.reserved2 AS avatarUrl FROM img_flag i WHERE i.username = '$wxid'"
- val result = executeQuery(sql)
+ val result = executeQuery(SQL.avatar(wxid))
return if (result.isNotEmpty()) {
result[0]["avatarUrl"] as? String ?: ""
} else {
@@ -442,5 +489,4 @@ class WeDatabaseApi : ApiHookItem(), IDexFind {
else -> 0
}
}
-
}
\ No newline at end of file
diff --git a/app/src/main/java/moe/ouom/wekit/hooks/sdk/api/WeDatabaseListener.kt b/app/src/main/java/moe/ouom/wekit/hooks/sdk/api/WeDatabaseListener.kt
index 4a6a609..f72ec60 100644
--- a/app/src/main/java/moe/ouom/wekit/hooks/sdk/api/WeDatabaseListener.kt
+++ b/app/src/main/java/moe/ouom/wekit/hooks/sdk/api/WeDatabaseListener.kt
@@ -2,12 +2,13 @@ package moe.ouom.wekit.hooks.sdk.api
import android.annotation.SuppressLint
import android.content.ContentValues
-import android.util.Log
import de.robv.android.xposed.XposedHelpers
import moe.ouom.wekit.config.WeConfig
import moe.ouom.wekit.constants.Constants
+import moe.ouom.wekit.constants.MMVersion
import moe.ouom.wekit.core.model.ApiHookItem
import moe.ouom.wekit.hooks.core.annotation.HookItem
+import moe.ouom.wekit.host.HostInfo
import moe.ouom.wekit.util.Initiator.loadClass
import moe.ouom.wekit.util.log.WeLogger
import java.util.concurrent.CopyOnWriteArrayList
@@ -15,40 +16,108 @@ import java.util.concurrent.CopyOnWriteArrayList
@SuppressLint("DiscouragedApi")
@HookItem(path = "API/数据库监听服务", desc = "为其他功能提供数据库写入监听能力")
class WeDatabaseListener : ApiHookItem() {
-
- // 定义监听器接口
- interface DatabaseInsertListener {
+ // 定义独立的接口
+ interface InsertListener {
fun onInsert(table: String, values: ContentValues)
}
+ interface UpdateListener {
+ fun onUpdate(table: String, values: ContentValues): Boolean
+ }
+
+ interface QueryListener {
+ fun onQuery(sql: String): String?
+ }
companion object {
- private val listeners = CopyOnWriteArrayList()
+ private const val TAG = "WeDatabaseApi"
- // 供其他模块注册监听
- fun addListener(listener: DatabaseInsertListener) {
- if (!listeners.contains(listener)) {
- listeners.add(listener)
- WeLogger.i("WeDatabaseApi: 监听器已添加,当前监听器数量: ${listeners.size}")
- } else {
- WeLogger.w("WeDatabaseApi: 监听器已存在,跳过添加")
+ private val insertListeners = CopyOnWriteArrayList()
+ private val updateListeners = CopyOnWriteArrayList()
+ private val queryListeners = CopyOnWriteArrayList()
+ fun addListener(listener: Any) {
+ val addedTypes = mutableListOf()
+
+ if (listener is InsertListener) {
+ insertListeners.add(listener)
+ addedTypes.add("INSERT")
+ }
+ if (listener is UpdateListener) {
+ updateListeners.add(listener)
+ addedTypes.add("UPDATE")
+ }
+ if (listener is QueryListener) {
+ queryListeners.add(listener)
+ addedTypes.add("QUERY")
+ }
+
+ // 只有实现了至少一个接口才打印日志
+ if (addedTypes.isNotEmpty()) {
+ WeLogger.i(TAG, "监听器已添加: ${listener.javaClass.simpleName} [${addedTypes.joinToString()}]")
}
}
- fun removeListener(listener: DatabaseInsertListener) {
- val removed = listeners.remove(listener)
- WeLogger.i("WeDatabaseApi: 监听器移除${if (removed) "成功" else "失败"},当前监听器数量: ${listeners.size}")
+ fun removeListener(listener: Any) {
+ var removed = false
+
+ if (listener is InsertListener) {
+ removed = insertListeners.remove(listener) || removed
+ }
+ if (listener is UpdateListener) {
+ removed = updateListeners.remove(listener) || removed
+ }
+ if (listener is QueryListener) {
+ removed = queryListeners.remove(listener) || removed
+ }
+
+ if (removed) {
+ WeLogger.i(TAG, "监听器已移除: ${listener.javaClass.simpleName}")
+ }
}
+
}
override fun entry(classLoader: ClassLoader) {
hookDatabaseInsert()
+ hookDatabaseUpdate()
+ hookDatabaseQuery()
+ }
+
+ override fun unload(classLoader: ClassLoader) {
+ insertListeners.clear()
+ updateListeners.clear()
+ queryListeners.clear()
+ }
+
+ // ==================== 私有辅助方法 ====================
+
+ private fun shouldLogDatabase(): Boolean {
+ val config = WeConfig.getDefaultConfig()
+ return config.getBooleanOrFalse(Constants.PrekVerboseLog) &&
+ config.getBooleanOrFalse(Constants.PrekDatabaseVerboseLog)
+ }
+
+ private fun formatArgs(args: Array): String {
+ return args.mapIndexed { index, arg ->
+ "arg[$index](${arg?.javaClass?.simpleName ?: "null"})=$arg"
+ }.joinToString(", ")
}
+ private fun logWithStack(methodName: String, table: String, args: Array, result: Any? = null) {
+ if (!shouldLogDatabase()) return
+
+ val argsInfo = formatArgs(args)
+ val resultStr = if (result != null) ", result=$result" else ""
+ val stackStr = ", stack=${WeLogger.getStackTraceString()}"
+
+ WeLogger.logChunkedD(TAG, "[$methodName] table=$table$resultStr, args=[$argsInfo]$stackStr")
+ }
+
+ // ==================== Insert Hook ====================
+
private fun hookDatabaseInsert() {
try {
val clsSQLite = loadClass(Constants.CLAZZ_SQLITE_DATABASE)
-
- val mInsertWithOnConflict = XposedHelpers.findMethodExact(
+ val method = XposedHelpers.findMethodExact(
clsSQLite,
"insertWithOnConflict",
String::class.java,
@@ -57,40 +126,163 @@ class WeDatabaseListener : ApiHookItem() {
Int::class.javaPrimitiveType
)
- hookAfter(mInsertWithOnConflict) { param ->
+ hookAfter(method) { param ->
try {
+ if (insertListeners.isEmpty()) return@hookAfter
+
val table = param.args[0] as String
val values = param.args[2] as ContentValues
+ val result = param.result
+
+ logWithStack("Insert", table, param.args, result)
+ insertListeners.forEach { it.onInsert(table, values) }
+ } catch (e: Throwable) {
+ WeLogger.e(TAG, "Insert dispatch failed", e)
+ }
+ }
+ WeLogger.i(TAG, "Insert hook success")
+ } catch (e: Throwable) {
+ WeLogger.e(TAG, "Hook insert failed", e)
+ }
+ }
- val config = WeConfig.getDefaultConfig()
- val verboseLog = config.getBooleanOrFalse(Constants.PrekVerboseLog)
- val dbVerboseLog = config.getBooleanOrFalse(Constants.PrekDatabaseVerboseLog)
-
- // 分发事件给所有监听者
- if (listeners.isNotEmpty()) {
- if (verboseLog) {
- if (dbVerboseLog) {
- val argsInfo = param.args.mapIndexed { index, arg ->
- "arg[$index](${arg?.javaClass?.simpleName ?: "null"})=$arg"
- }.joinToString(", ")
- val result = param.result
-
- WeLogger.logChunkedD("WeDatabaseApi","[Insert] table=$table, result=$result, args=[$argsInfo], stack=${WeLogger.getStackTraceString()}")
- }
- }
- listeners.forEach { it.onInsert(table, values) }
+ // ==================== Update Hook ====================
+
+ private fun hookDatabaseUpdate() {
+ try {
+ val isPlay = HostInfo.isGooglePlayVersion
+ val version = HostInfo.getVersionCode()
+ val isNewVersion = (!isPlay && version >= MMVersion.MM_8_0_43) ||
+ (isPlay && version >= MMVersion.MM_8_0_48_Play)
+
+ val clsName = if (isNewVersion) Constants.CLAZZ_COMPAT_SQLITE_DATABASE else Constants.CLAZZ_SQLITE_DATABASE
+ val clsSQLite = loadClass(clsName)
+
+ val method = XposedHelpers.findMethodExact(
+ clsSQLite,
+ "updateWithOnConflict",
+ String::class.java,
+ ContentValues::class.java,
+ String::class.java,
+ Array::class.java,
+ Int::class.javaPrimitiveType
+ )
+
+ hookBefore(method) { param ->
+ try {
+ if (updateListeners.isEmpty()) return@hookBefore
+
+ val table = param.args[0] as String
+ val values = param.args[1] as ContentValues
+ val whereClause = param.args[2] as? String
+ @Suppress("UNCHECKED_CAST") val whereArgs = param.args[3] as? Array
+
+ logWithStack("Update", table, param.args)
+
+ // 如果有任何一个监听器返回 true,则阻止更新
+ val shouldBlock = updateListeners.any { it.onUpdate(table, values) }
+
+ if (shouldBlock) {
+ param.result = 0 // 返回0表示没有行被更新
+ WeLogger.d(TAG, "[Update] 被监听器阻止, table=$table, stack=${WeLogger.getStackTraceString()}")
}
} catch (e: Throwable) {
- WeLogger.e("WeDatabaseApi: Dispatch failed", e)
+ WeLogger.e(TAG, "Update dispatch failed", e)
}
}
- WeLogger.i("WeDatabaseApi: Hook success")
+ WeLogger.i(TAG, "Update hook success")
} catch (e: Throwable) {
- WeLogger.e("WeDatabaseApi: Hook database failed", e)
+ WeLogger.e(TAG, "Hook update failed", e)
}
}
- override fun unload(classLoader: ClassLoader) {
- listeners.clear()
+ // ==================== Query Hook ====================
+
+ private fun hookDatabaseQuery() {
+ try {
+ val isPlay = HostInfo.isGooglePlayVersion
+ val version = HostInfo.getVersionCode()
+ val isNewVersion = (!isPlay && version >= MMVersion.MM_8_0_43) ||
+ (isPlay && version >= MMVersion.MM_8_0_48_Play)
+
+ if (isNewVersion) {
+ hookNewVersionQuery()
+ } else {
+ hookOldVersionQuery()
+ }
+ WeLogger.i(TAG, "Query hook success")
+ } catch (e: Throwable) {
+ WeLogger.e(TAG, "Hook query failed", e)
+ }
+ }
+
+ private fun hookNewVersionQuery() {
+ val clsSQLite = loadClass(Constants.CLAZZ_COMPAT_SQLITE_DATABASE)
+ val method = XposedHelpers.findMethodExact(
+ clsSQLite,
+ "rawQuery",
+ String::class.java,
+ Array::class.java,
+ )
+
+ hookBefore(method) { param ->
+ try {
+ if (queryListeners.isEmpty()) return@hookBefore
+
+ val sql = param.args[0] as? String ?: return@hookBefore
+ var currentSql = sql
+
+ logWithStack("rawQuery", "N/A", param.args)
+
+ queryListeners.forEach { listener ->
+ listener.onQuery(currentSql)?.let { currentSql = it }
+ }
+
+ if (currentSql != sql) {
+ param.args[0] = currentSql
+ WeLogger.d(TAG, "[rawQuery] SQL被修改: $sql -> $currentSql, stack=${WeLogger.getStackTraceString()}")
+ }
+ } catch (e: Throwable) {
+ WeLogger.e(TAG, "New version query dispatch failed", e)
+ }
+ }
+ }
+
+ private fun hookOldVersionQuery() {
+ val clsSQLite = loadClass(Constants.CLAZZ_SQLITE_DATABASE)
+ val cursorFactoryClass = loadClass("com.tencent.wcdb.database.SQLiteDatabase\$CursorFactory")
+ val cancellationSignalClass = loadClass("com.tencent.wcdb.support.CancellationSignal")
+
+ val method = XposedHelpers.findMethodExact(
+ clsSQLite,
+ "rawQueryWithFactory",
+ cursorFactoryClass,
+ String::class.java,
+ Array::class.java,
+ String::class.java,
+ cancellationSignalClass
+ )
+
+ hookBefore(method) { param ->
+ try {
+ if (queryListeners.isEmpty()) return@hookBefore
+
+ val sql = param.args[1] as? String ?: return@hookBefore
+ var currentSql = sql
+
+ logWithStack("rawQueryWithFactory", param.args[3] as? String ?: "N/A", param.args)
+
+ queryListeners.forEach { listener ->
+ listener.onQuery(currentSql)?.let { currentSql = it }
+ }
+
+ if (currentSql != sql) {
+ param.args[1] = currentSql
+ WeLogger.d(TAG, "[rawQueryWithFactory] SQL被修改: $sql -> $currentSql, stack=${WeLogger.getStackTraceString()}")
+ }
+ } catch (e: Throwable) {
+ WeLogger.e(TAG, "Old version query dispatch failed", e)
+ }
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/moe/ouom/wekit/hooks/sdk/api/WeMessageApi.kt b/app/src/main/java/moe/ouom/wekit/hooks/sdk/api/WeMessageApi.kt
index 29f6be5..c5067cb 100644
--- a/app/src/main/java/moe/ouom/wekit/hooks/sdk/api/WeMessageApi.kt
+++ b/app/src/main/java/moe/ouom/wekit/hooks/sdk/api/WeMessageApi.kt
@@ -661,8 +661,8 @@ class WeMessageApi : ApiHookItem(), IDexFind {
/** 发送私有路径下的语音文件 */
fun sendVoice(toUser: String, path: String, durationMs: Int): Boolean {
return try {
- val selfWxid = getSelfAlias()
- if (selfWxid.isEmpty()) throw IllegalStateException("无法获取 Wxid")
+ val selfWxAlias = getSelfAlias()
+ if (selfWxAlias.isEmpty()) throw IllegalStateException("无法获取 WxAlias")
// 获取 Service 实例
val serviceInterface = voiceServiceInterfaceClass ?: throw IllegalStateException("VoiceService interface not found")
@@ -692,7 +692,7 @@ class WeMessageApi : ApiHookItem(), IDexFind {
if (finalServiceObj == null) throw IllegalStateException("无法获取 VoiceService 实例")
// 准备文件
- val fileName = voiceNameGenMethod?.invoke(null, selfWxid, "amr_") as? String ?: throw IllegalStateException("VoiceName Gen Failed")
+ val fileName = voiceNameGenMethod?.invoke(null, selfWxAlias, "amr_") as? String ?: throw IllegalStateException("VoiceName Gen Failed")
val accPath = getAccPath()
val voice2Root = if (accPath.endsWith("/")) "${accPath}voice2/" else "$accPath/voice2/"
val destFullPath = pathGenMethod?.invoke(null, voice2Root, "msg_", fileName, ".amr", 2) as? String ?: throw IllegalStateException("Path Gen Failed")
@@ -762,7 +762,6 @@ class WeMessageApi : ApiHookItem(), IDexFind {
fun getSelfAlias(): String {
return getSelfAliasMethod?.invoke(null) as? String ?: ""
}
-
private fun bindServiceFramework() {
val smClazz = dexClassServiceManager.clazz
getServiceMethod = smClazz.declaredMethods.firstOrNull {
diff --git a/app/src/main/java/moe/ouom/wekit/hooks/sdk/protocol/WeApi.kt b/app/src/main/java/moe/ouom/wekit/hooks/sdk/protocol/WeApi.kt
index 9d76d11..9ed4bbd 100644
--- a/app/src/main/java/moe/ouom/wekit/hooks/sdk/protocol/WeApi.kt
+++ b/app/src/main/java/moe/ouom/wekit/hooks/sdk/protocol/WeApi.kt
@@ -15,14 +15,6 @@ object WeApi {
* 获取当前登录的微信ID
*/
fun getSelfWxId(): String {
- val sharedPreferences: SharedPreferences =
- HostInfo.getApplication().getSharedPreferences("com.tencent.mm_preferences", 0)
-
- RuntimeConfig.setLogin_weixin_username(sharedPreferences.getString("login_weixin_username", ""))
- RuntimeConfig.setLast_login_nick_name(sharedPreferences.getString("last_login_nick_name", ""))
- RuntimeConfig.setLogin_user_name(sharedPreferences.getString("login_user_name", ""))
- RuntimeConfig.setLast_login_uin(sharedPreferences.getString("last_login_uin", "0"))
-
return RuntimeConfig.getLogin_weixin_username()
}
diff --git a/app/src/main/java/moe/ouom/wekit/hooks/sdk/protocol/listener/WePkgDispatcher.kt b/app/src/main/java/moe/ouom/wekit/hooks/sdk/protocol/listener/WePkgDispatcher.kt
index fa26364..a3107c0 100644
--- a/app/src/main/java/moe/ouom/wekit/hooks/sdk/protocol/listener/WePkgDispatcher.kt
+++ b/app/src/main/java/moe/ouom/wekit/hooks/sdk/protocol/listener/WePkgDispatcher.kt
@@ -11,10 +11,14 @@ import moe.ouom.wekit.util.common.SyncUtils
import moe.ouom.wekit.util.log.WeLogger
import org.luckypray.dexkit.DexKitBridge
import java.lang.reflect.Proxy
+import java.util.concurrent.ConcurrentHashMap
+import kotlin.math.min
@HookItem(path = "protocol/wepkg_dispatcher", desc = "WePkg 请求/响应数据包拦截与篡改")
class WePkgDispatcher : ApiHookItem(), IDexFind {
private val dexClsOnGYNetEnd by dexClass()
+ // 缓存最近10条记录,避免因脚本引起的无限递归
+ private val recentRequests = ConcurrentHashMap()
override fun entry(classLoader: ClassLoader) {
SyncUtils.postDelayed(3000) {
@@ -40,6 +44,27 @@ class WePkgDispatcher : ApiHookItem(), IDexFind {
val reqPbObj = XposedHelpers.getObjectField(reqWrapper, "a") // m.a
val reqBytes = XposedHelpers.callMethod(reqPbObj, "toByteArray") as ByteArray
+ // 构造唯一标识符
+ val key = "$cgiId|$uri|${reqWrapper?.javaClass?.name}|${reqPbObj?.javaClass?.name}|${reqBytes.contentToString()}"
+ // 检查是否在缓存中且时间间隔小于500毫秒
+ val currentTime = System.currentTimeMillis()
+ val lastTime = recentRequests[key]
+ if (lastTime != null && currentTime - lastTime < 500) {
+ // 直接返回,不执行任何请求处理
+ WeLogger.i("PkgDispatcher", "Request skipped (duplicate): $uri")
+ return@hookBefore
+ }
+ // 更新缓存
+ recentRequests[key] = currentTime
+ // 限制缓存大小为10条
+ if (recentRequests.size > 10) {
+ // 移除最旧的条目
+ val oldestEntry = recentRequests.entries.firstOrNull()
+ oldestEntry?.let {
+ recentRequests.remove(it.key)
+ }
+ }
+
WePkgManager.handleRequestTamper(uri, cgiId, reqBytes)?.let { tampered ->
XposedHelpers.callMethod(reqPbObj, "parseFrom", tampered)
WeLogger.i("PkgDispatcher", "Request Tampered: $uri")
diff --git a/app/src/main/java/moe/ouom/wekit/hooks/sdk/ui/WeChatChatContextMenuApi.kt b/app/src/main/java/moe/ouom/wekit/hooks/sdk/ui/WeChatChatContextMenuApi.kt
new file mode 100644
index 0000000..3f3b847
--- /dev/null
+++ b/app/src/main/java/moe/ouom/wekit/hooks/sdk/ui/WeChatChatContextMenuApi.kt
@@ -0,0 +1,170 @@
+package moe.ouom.wekit.hooks.sdk.ui
+
+import android.annotation.SuppressLint
+import android.graphics.drawable.Drawable
+import android.view.MenuItem
+import android.view.View
+import de.robv.android.xposed.XC_MethodHook
+import moe.ouom.wekit.core.dsl.dexMethod
+import moe.ouom.wekit.core.model.ApiHookItem
+import moe.ouom.wekit.dexkit.intf.IDexFind
+import moe.ouom.wekit.hooks.core.annotation.HookItem
+import moe.ouom.wekit.util.log.WeLogger
+import org.luckypray.dexkit.DexKitBridge
+import org.luckypray.dexkit.query.enums.StringMatchType
+import java.util.concurrent.CopyOnWriteArrayList
+
+@HookItem(path = "API/聊天右键菜单增强")
+class WeChatChatContextMenuApi : ApiHookItem(), IDexFind {
+
+ private val dexMethodOnCreateMenu by dexMethod()
+ private val dexMethodOnItemSelected by dexMethod()
+
+ companion object {
+ private const val TAG = "ChatMenuApi"
+ private lateinit var thisView: View
+ private lateinit var rawMessage: Map
+ val onCreateCallbacks = CopyOnWriteArrayList()
+ val onSelectCallbacks = CopyOnWriteArrayList()
+
+ fun addOnCreateListener(listener: OnCreateListener) {
+ onCreateCallbacks.add(listener)
+ }
+
+ fun removeOnCreateListener(listener: OnCreateListener) {
+ onCreateCallbacks.remove(listener)
+ }
+
+ fun addOnSelectListener(listener: OnSelectListener) {
+ onSelectCallbacks.add(listener)
+ }
+
+ fun removeOnSelectListener(listener: OnSelectListener) {
+ onSelectCallbacks.remove(listener)
+ }
+ }
+
+ /**
+ * 接口:创建菜单后触发
+ */
+ fun interface OnCreateListener {
+ fun onCreated(messageInfo: Map): MenuInfoItem?
+ }
+
+ /**
+ * 接口:选中菜单时触发
+ */
+ fun interface OnSelectListener {
+ fun onSelected(id: Int, messageInfo: Map, view: View): Boolean
+ }
+
+ data class MenuInfoItem(val id: Int, val title: String, val iconDrawable: Drawable)
+
+ @SuppressLint("NonUniqueDexKitData")
+ override fun dexFind(dexKit: DexKitBridge): Map {
+ val descriptors = mutableMapOf()
+
+ dexMethodOnCreateMenu.find(dexKit, allowMultiple = false, descriptors = descriptors) {
+ searchPackages("com.tencent.mm.ui.chatting.viewitems")
+ matcher {
+ usingStrings(listOf("MicroMsg.ChattingItem", "msg is null!"), StringMatchType.Equals, false)
+ }
+ }
+
+ dexMethodOnItemSelected.find(dexKit, allowMultiple = false, descriptors = descriptors) {
+ searchPackages("com.tencent.mm.ui.chatting.viewitems")
+ matcher {
+ usingStrings(
+ "MicroMsg.ChattingItem", "context item select failed, null dataTag"
+ )
+ }
+ }
+
+ return descriptors
+ }
+
+ override fun entry(classLoader: ClassLoader) {
+ // Hook OnCreate
+ hookAfter(dexMethodOnCreateMenu.method) { param ->
+ handleCreateMenu(param)
+ }
+
+ // Hook OnSelected
+ hookAfter(dexMethodOnItemSelected.method) { param ->
+ handleSelectMenu(param)
+ }
+ }
+
+ private fun getRawMessage(item: Any): Map = runCatching {
+ // 可能会变更
+ val messageTagClass = item::class.java.superclass
+ val messageHolderClass = messageTagClass.superclass
+ val messageField = messageHolderClass.getField("a")
+ val messageObject = messageField.get(item)
+ val messageImplClass = messageObject::class.java
+ val messageWrapperClass = messageImplClass.superclass
+ val databaseMappingClass = messageWrapperClass.superclass
+ val databaseFields = databaseMappingClass.declaredFields.filter { field -> field.name.startsWith("field_") }
+ .onEach { field -> field.isAccessible = true }.associate { field ->
+ field.name to field.get(messageObject)
+ }
+
+ databaseFields
+ }.onFailure { WeLogger.e(TAG, "获取rawMessage失败: ${it.message}") }.getOrDefault(emptyMap())
+
+ private fun handleCreateMenu(param: XC_MethodHook.MethodHookParam) {
+ try {
+ val chatContextMenu = param.args[0]
+ val addMethod = chatContextMenu::class.java.declaredMethods.first {
+ it.parameterTypes.contentEquals(
+ arrayOf(
+ Int::class.java,
+ CharSequence::class.java,
+ Drawable::class.java
+ )
+ ) && it.returnType == MenuItem::class.java
+ }
+
+ thisView = param.args[1] as View
+ val item = thisView.tag
+ /*
+ val messageTagClass = item::class.java.superclass
+ val positionMethod = messageTagClass.declaredMethods.first {
+ it.returnType === Int::class.java
+ }
+ val position = positionMethod.invoke(item)
+ */
+
+ rawMessage = getRawMessage(item)
+ for (listener in onCreateCallbacks) {
+ val item = listener.onCreated(rawMessage)
+ try {
+ item?.let { addMethod.invoke(chatContextMenu, it.id, it.title, it.iconDrawable) }
+ } catch (e: Exception) {
+ WeLogger.e(TAG, "添加条目失败: ${e.message}")
+ }
+ }
+ } catch (e: Throwable) {
+ WeLogger.e(TAG, "handleCreateMenu 失败", e)
+ }
+ }
+
+ private fun handleSelectMenu(param: XC_MethodHook.MethodHookParam) {
+ try {
+ val menuItem = param.args[0] as MenuItem
+ val id = menuItem.itemId
+ for (listener in onSelectCallbacks) {
+ try {
+ val handled = listener.onSelected(id, rawMessage, thisView)
+ if (handled) {
+ WeLogger.d(TAG, "菜单项已被动态回调处理")
+ }
+ } catch (e: Throwable) {
+ WeLogger.e(TAG, "OnSelect 回调执行异常", e)
+ }
+ }
+ } catch (e: Throwable) {
+ WeLogger.e(TAG, "handleSelectMenu 失败", e)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/moe/ouom/wekit/hooks/sdk/ui/WeChatContactInfoAdapterItemHook.kt b/app/src/main/java/moe/ouom/wekit/hooks/sdk/ui/WeChatContactInfoAdapterItemHook.kt
new file mode 100644
index 0000000..6c1980a
--- /dev/null
+++ b/app/src/main/java/moe/ouom/wekit/hooks/sdk/ui/WeChatContactInfoAdapterItemHook.kt
@@ -0,0 +1,184 @@
+package moe.ouom.wekit.hooks.sdk.ui
+
+import android.app.Activity
+import android.content.Context
+import android.widget.BaseAdapter
+import de.robv.android.xposed.XC_MethodHook
+import de.robv.android.xposed.XposedBridge
+import de.robv.android.xposed.XposedHelpers
+import moe.ouom.wekit.core.model.ApiHookItem
+import moe.ouom.wekit.hooks.core.annotation.HookItem
+import moe.ouom.wekit.util.Initiator.loadClass
+import moe.ouom.wekit.util.log.WeLogger
+import java.lang.reflect.Constructor
+import java.lang.reflect.Field
+import java.lang.reflect.Method
+import java.lang.reflect.Modifier
+import java.util.concurrent.CopyOnWriteArrayList
+
+@HookItem(path = "API/用户联系页面扩展")
+class WeChatContactInfoAdapterItemHook : ApiHookItem() {
+
+ companion object {
+ private const val TAG = "ContactInfoAdapterItemHook"
+ private val initCallbacks = CopyOnWriteArrayList()
+ private val clickListeners = CopyOnWriteArrayList()
+
+ @Volatile
+ private var isRefInitialized = false
+ private lateinit var prefConstructor: Constructor<*>
+ private lateinit var prefKeyField: Field
+ private lateinit var adapterField: Field
+ private lateinit var onPreferenceTreeClickMethod: Method
+ private lateinit var addPreferenceMethod: Method
+ private lateinit var setKeyMethod: Method
+ private lateinit var setSummaryMethod: Method
+ private lateinit var setTitleMethod: Method
+
+
+ fun addInitCallback(callback: InitContactInfoViewCallback) {
+ initCallbacks.add(callback)
+ }
+
+ fun removeInitCallback(callback: InitContactInfoViewCallback) {
+ initCallbacks.remove(callback)
+ }
+
+ fun addClickListener(listener: OnContactInfoItemClickListener) {
+ clickListeners.add(listener)
+ }
+
+ fun removeClickListener(listener: OnContactInfoItemClickListener) {
+ clickListeners.remove(listener)
+ }
+
+ }
+
+ fun interface InitContactInfoViewCallback {
+ fun onInitContactInfoView(context: Activity): ContactInfoItem?
+ }
+
+ fun interface OnContactInfoItemClickListener {
+ fun onItemClick(activity: Activity, key: String): Boolean
+ }
+
+
+ data class ContactInfoItem(val key: String, val title: String, val summary: String? = null, val position: Int = -1)
+
+ override fun entry(classLoader: ClassLoader) {
+ initReflection()
+ hook(classLoader)
+ hookItemClick(classLoader)
+ }
+
+ private fun initReflection() {
+ if (isRefInitialized) return
+
+ synchronized(this) {
+ if (isRefInitialized) return
+
+ val prefClass = loadClass("com.tencent.mm.ui.base.preference.Preference")
+ prefConstructor = prefClass.getConstructor(Context::class.java)
+ prefKeyField = prefClass.declaredFields.first { field ->
+ field.type == String::class.java && !Modifier.isFinal(field.modifiers)
+ }
+
+ val contactInfoUIClass = loadClass("com.tencent.mm.plugin.profile.ui.ContactInfoUI")
+ adapterField = contactInfoUIClass.superclass.declaredFields.first {
+ BaseAdapter::class.java.isAssignableFrom(it.type)
+ }.apply { isAccessible = true }
+ onPreferenceTreeClickMethod = contactInfoUIClass.declaredMethods.first {
+ it.name == "onPreferenceTreeClick"
+ }
+
+ val adapterClass = adapterField.type
+ addPreferenceMethod = adapterClass.declaredMethods.first {
+ !Modifier.isFinal(it.modifiers)
+ && it.parameterCount == 2 &&
+ it.parameterTypes[0] == prefClass
+ && it.parameterTypes[1] == Int::class.java
+ }
+
+ setKeyMethod = prefClass.declaredMethods.first {
+ it.parameterCount == 1 && it.parameterTypes[0] == String::class.java
+ }
+
+ val charSeqMethods = prefClass.declaredMethods.filter {
+ it.parameterCount == 1 && it.parameterTypes[0] == CharSequence::class.java
+ }
+
+ // 可能需要之后维护 不稳定的方法
+ setSummaryMethod = charSeqMethods.getOrElse(0) {
+ throw RuntimeException("setTitle method not found")
+ }
+ setTitleMethod = charSeqMethods.getOrElse(1) {
+ throw RuntimeException("setSummary method not found")
+ }
+
+ isRefInitialized = true
+ WeLogger.i(
+ TAG, """
+ prefConstructor: $prefConstructor
+ prefKeyField: $prefKeyField
+ adapterField: $adapterField
+ onPreferenceTreeClickMethod: $onPreferenceTreeClickMethod
+ addPreferenceMethod: $addPreferenceMethod
+ setKeyMethod: $setKeyMethod
+ setSummaryMethod: $setSummaryMethod
+ setTitleMethod: $setTitleMethod
+ """.trimIndent()
+ )
+ WeLogger.i(TAG, "反射初始化完成")
+ }
+ }
+
+ fun hook(classLoader: ClassLoader) {
+ try {
+ XposedHelpers.findAndHookMethod(
+ "com.tencent.mm.plugin.profile.ui.ContactInfoUI",
+ classLoader,
+ "initView",
+ object : XC_MethodHook() {
+ override fun afterHookedMethod(param: MethodHookParam) {
+ val adapterInstance = adapterField.get(param.thisObject as Activity)
+ for (listener in initCallbacks) {
+ val item = listener.onInitContactInfoView(param.thisObject as Activity)
+ try {
+ val preference = prefConstructor.newInstance(param.thisObject as Context)
+ item?.let {
+ setKeyMethod.invoke(preference, it.key)
+ setTitleMethod.invoke(preference, it.title)
+ it.summary?.let { summary -> setSummaryMethod.invoke(preference, summary) }
+ addPreferenceMethod.invoke(adapterInstance, preference, it.position)
+ }
+ } catch (e: Exception) {
+ WeLogger.e(TAG, "添加条目失败: ${e.message}")
+ }
+ }
+ }
+ }
+ )
+
+ WeLogger.i(TAG, "Hook 注册成功")
+ } catch (e: Exception) {
+ WeLogger.e(TAG, "Hook 失败 - ${e.message}", e)
+ }
+ }
+
+ private fun hookItemClick(classLoader: ClassLoader) {
+ XposedBridge.hookMethod(onPreferenceTreeClickMethod, object : XC_MethodHook() {
+ override fun beforeHookedMethod(param: MethodHookParam) {
+ val preference = param.args[1] ?: return
+ val key = prefKeyField.get(preference) as? String
+ if (key != null) {
+ for (listener in clickListeners) {
+ if (listener.onItemClick(param.thisObject as Activity, key)) {
+ param.result = true
+ break
+ }
+ }
+ }
+ }
+ })
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/moe/ouom/wekit/hooks/sdk/ui/WeChatSnsContextMenuApi.kt b/app/src/main/java/moe/ouom/wekit/hooks/sdk/ui/WeChatSnsContextMenuApi.kt
new file mode 100644
index 0000000..596f50b
--- /dev/null
+++ b/app/src/main/java/moe/ouom/wekit/hooks/sdk/ui/WeChatSnsContextMenuApi.kt
@@ -0,0 +1,191 @@
+package moe.ouom.wekit.hooks.sdk.ui
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.view.ContextMenu
+import android.view.MenuItem
+import de.robv.android.xposed.XC_MethodHook
+import moe.ouom.wekit.core.dsl.dexMethod
+import moe.ouom.wekit.core.model.ApiHookItem
+import moe.ouom.wekit.dexkit.intf.IDexFind
+import moe.ouom.wekit.hooks.core.annotation.HookItem
+import moe.ouom.wekit.util.log.WeLogger
+import org.luckypray.dexkit.DexKitBridge
+import java.lang.reflect.Modifier
+import java.util.concurrent.CopyOnWriteArrayList
+
+@HookItem(path = "API/朋友圈右键菜单增强")
+class WeChatSnsContextMenuApi : ApiHookItem(), IDexFind {
+
+ private val dexMethodOnCreateMenu by dexMethod()
+ private val dexMethodOnItemSelected by dexMethod()
+ private val dexMethodSnsInfoStorage by dexMethod()
+ private val dexMethodGetSnsInfoStorage by dexMethod()
+
+ companion object {
+ private const val TAG = "SnsMenuApi"
+
+ val onCreateCallbacks = CopyOnWriteArrayList()
+ val onSelectCallbacks = CopyOnWriteArrayList()
+
+ fun addOnCreateListener(listener: OnCreateListener) {
+ onCreateCallbacks.add(listener)
+ }
+ fun removeOnCreateListener(listener: OnCreateListener) {
+ onCreateCallbacks.remove(listener)
+ }
+
+ fun addOnSelectListener(listener: OnSelectListener) {
+ onSelectCallbacks.add(listener)
+ }
+ fun removeOnSelectListener(listener: OnSelectListener) {
+ onSelectCallbacks.remove(listener)
+ }
+ }
+
+ /**
+ * 接口:创建菜单时触发
+ */
+ fun interface OnCreateListener {
+ fun onCreate(contextMenu: ContextMenu)
+ }
+
+ /**
+ * 接口:选中菜单时触发
+ */
+ fun interface OnSelectListener {
+ fun onSelected(context: SnsContext, itemId: Int): Boolean
+ }
+
+
+ data class SnsContext(
+ val activity: Activity,
+ val snsInfo: Any?,
+ val timeLineObject: Any?
+ )
+
+ @SuppressLint("NonUniqueDexKitData")
+ override fun dexFind(dexKit: DexKitBridge): Map {
+ val descriptors = mutableMapOf()
+
+ dexMethodOnCreateMenu.find(dexKit, allowMultiple = false, descriptors = descriptors) {
+ searchPackages("com.tencent.mm.plugin.sns.ui.listener")
+ matcher {
+ usingStrings("MicroMsg.TimelineOnCreateContextMenuListener", "onMMCreateContextMenu error")
+ }
+ }
+
+ dexMethodOnItemSelected.find(dexKit, allowMultiple = false, descriptors = descriptors) {
+ searchPackages("com.tencent.mm.plugin.sns.ui.listener")
+ matcher {
+ usingStrings(
+ "delete comment fail!!! snsInfo is null",
+ "send photo fail, mediaObj is null",
+ "mediaObj is null, send failed!"
+ )
+ }
+ }
+
+ dexMethodSnsInfoStorage.find(dexKit, allowMultiple = false, descriptors = descriptors) {
+ matcher {
+ paramCount(1)
+ paramTypes("java.lang.String")
+ usingStrings(
+ "getByLocalId",
+ "com.tencent.mm.plugin.sns.storage.SnsInfoStorage"
+ )
+ returnType("com.tencent.mm.plugin.sns.storage.SnsInfo")
+ }
+ }
+
+ dexMethodGetSnsInfoStorage.find(dexKit, allowMultiple = false, descriptors = descriptors) {
+ searchPackages("com.tencent.mm.plugin.sns.model")
+ matcher {
+ // 必须是静态方法
+ modifiers = Modifier.STATIC
+ returnType(dexMethodSnsInfoStorage.method.declaringClass)
+ // 无参数
+ paramCount(0)
+ // 同时包含两个特征字符串
+ usingStrings(
+ "com.tencent.mm.plugin.sns.model.SnsCore",
+ "getSnsInfoStorage"
+ )
+ }
+ }
+
+ return descriptors
+ }
+
+ override fun entry(classLoader: ClassLoader) {
+ // Hook OnCreate
+ hookAfter(dexMethodOnCreateMenu.method) { param ->
+ handleCreateMenu(param)
+ }
+
+ // Hook OnSelected
+ hookAfter(dexMethodOnItemSelected.method) { param ->
+ handleSelectMenu(param)
+ }
+ }
+
+
+ private fun handleCreateMenu(param: XC_MethodHook.MethodHookParam) {
+ try {
+ val contextMenu = param.args.getOrNull(0) as? ContextMenu ?: return
+
+ for (listener in onCreateCallbacks) {
+ try {
+ listener.onCreate(contextMenu)
+ } catch (e: Throwable) {
+ WeLogger.e(TAG, "OnCreate 回调执行异常", e)
+ }
+ }
+ } catch (e: Throwable) {
+ WeLogger.e(TAG, "handleCreateMenu 失败", e)
+ }
+ }
+
+ private fun handleSelectMenu(param: XC_MethodHook.MethodHookParam) {
+ try {
+ val menuItem = param.args.getOrNull(0) as? MenuItem ?: return
+ val hookedObject = param.thisObject
+ val fields = hookedObject.javaClass.declaredFields
+ fields.forEach { field ->
+ field.isAccessible = true
+ val value = field.get(hookedObject)
+ WeLogger.d(TAG, "字段: ${field.name} (${field.type.name}) = $value")
+ }
+
+ val activity = fields.firstOrNull { it.type == Activity::class.java }
+ ?.apply { isAccessible = true }?.get(hookedObject) as Activity
+
+ val timeLineObject = fields.firstOrNull {
+ it.type.name == "com.tencent.mm.protocal.protobuf.TimeLineObject"
+ }?.apply { isAccessible = true }?.get(hookedObject)
+
+ val snsID = fields.firstOrNull {
+ it.type == String::class.java && !Modifier.isFinal(it.modifiers)
+ }?.apply { isAccessible = true }?.get(hookedObject) as String
+ val targetMethod = dexMethodSnsInfoStorage.method
+ val instance = dexMethodGetSnsInfoStorage.method.invoke(null)
+ val snsInfo = targetMethod.invoke(instance, snsID)
+
+ val context = SnsContext(activity, snsInfo, timeLineObject)
+ val clickedId = menuItem.itemId
+
+ for (listener in onSelectCallbacks) {
+ try {
+ val handled = listener.onSelected(context, clickedId)
+ if (handled) {
+ WeLogger.d(TAG, "菜单项 $clickedId 已被动态回调处理")
+ }
+ } catch (e: Throwable) {
+ WeLogger.e(TAG, "OnSelect 回调执行异常", e)
+ }
+ }
+ } catch (e: Throwable) {
+ WeLogger.e(TAG, "handleSelectMenu 失败", e)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/moe/ouom/wekit/host/HostInfo.java b/app/src/main/java/moe/ouom/wekit/host/HostInfo.java
index d6f4693..3acef47 100644
--- a/app/src/main/java/moe/ouom/wekit/host/HostInfo.java
+++ b/app/src/main/java/moe/ouom/wekit/host/HostInfo.java
@@ -62,11 +62,11 @@ public static boolean isAndroidxFileProviderAvailable() {
return moe.ouom.wekit.host.impl.HostInfo.isAndroidxFileProviderAvailable();
}
- public static boolean isWeChat() {
- return moe.ouom.wekit.host.impl.HostInfo.isWeChat();
- }
+ public static boolean isWeChat = moe.ouom.wekit.host.impl.HostInfo.isWeChat();
+
+ public static boolean isGooglePlayVersion = moe.ouom.wekit.host.impl.HostInfo.isGooglePlayVersion();
public static boolean requireMinWeChatVersion(int versionCode) {
- return isWeChat() && getLongVersionCode() >= versionCode;
+ return isWeChat && getLongVersionCode() >= versionCode;
}
}
\ No newline at end of file
diff --git a/app/src/main/java/moe/ouom/wekit/host/impl/HostInfo.kt b/app/src/main/java/moe/ouom/wekit/host/impl/HostInfo.kt
index f39c403..6332ff7 100644
--- a/app/src/main/java/moe/ouom/wekit/host/impl/HostInfo.kt
+++ b/app/src/main/java/moe/ouom/wekit/host/impl/HostInfo.kt
@@ -75,6 +75,12 @@ val isAndroidxFileProviderAvailable: Boolean by lazy {
}
}
+val isGooglePlayVersion = runCatching {
+ Class.forName("com.tencent.mm.boot.BuildConfig")
+ .getField("BUILD_TAG")
+ .get(null) as? String
+}.getOrNull()?.contains("GP", true) ?: false
+
data class HostInfoImpl(
val application: Application,
val packageName: String,
diff --git a/app/src/main/java/moe/ouom/wekit/loader/core/WeLauncher.java b/app/src/main/java/moe/ouom/wekit/loader/core/WeLauncher.java
index 4a1c9a0..a7964f0 100644
--- a/app/src/main/java/moe/ouom/wekit/loader/core/WeLauncher.java
+++ b/app/src/main/java/moe/ouom/wekit/loader/core/WeLauncher.java
@@ -118,11 +118,7 @@ protected void afterHookedMethod(MethodHookParam param) throws Throwable {
Activity activity = (Activity) param.thisObject;
RuntimeConfig.setLauncherUIActivity(activity);
SharedPreferences sharedPreferences = activity.getSharedPreferences("com.tencent.mm_preferences", 0);
-
- RuntimeConfig.setLogin_weixin_username(sharedPreferences.getString("login_weixin_username", ""));
- RuntimeConfig.setLast_login_nick_name(sharedPreferences.getString("last_login_nick_name", ""));
- RuntimeConfig.setLogin_user_name(sharedPreferences.getString("login_user_name", ""));
- RuntimeConfig.setLast_login_uin(sharedPreferences.getString("last_login_uin", "0"));
+ RuntimeConfig.setmmPrefs(sharedPreferences);
}
});
diff --git a/app/src/main/java/moe/ouom/wekit/loader/core/hooks/ActivityProxyHooks.java b/app/src/main/java/moe/ouom/wekit/loader/core/hooks/ActivityProxyHooks.java
index e778a2b..5c0da4c 100644
--- a/app/src/main/java/moe/ouom/wekit/loader/core/hooks/ActivityProxyHooks.java
+++ b/app/src/main/java/moe/ouom/wekit/loader/core/hooks/ActivityProxyHooks.java
@@ -12,42 +12,31 @@
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.IBinder;
-import android.os.Looper;
-import android.os.Message;
-import android.os.PersistableBundle;
-import android.os.TestLooperManager;
+import android.os.*;
import android.view.KeyEvent;
import android.view.MotionEvent;
-
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import lombok.SneakyThrows;
+import moe.ouom.wekit.config.RuntimeConfig;
+import moe.ouom.wekit.constants.PackageConstants;
+import moe.ouom.wekit.util.common.ModuleRes;
+import moe.ouom.wekit.util.log.WeLogger;
-import java.lang.reflect.Field;
-import java.lang.reflect.InvocationHandler;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.lang.reflect.Proxy;
+import java.lang.reflect.*;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
-import lombok.SneakyThrows;
-import moe.ouom.wekit.config.RuntimeConfig;
-import moe.ouom.wekit.constants.PackageConstants;
-import moe.ouom.wekit.util.common.ModuleRes;
-import moe.ouom.wekit.util.log.WeLogger;
-
/**
* Activity 占位 Hook 实现
* 允许模块启动未在宿主 Manifest 中注册的 Activity
*/
public class ActivityProxyHooks {
+ private static final String TAG = "ActivityProxyHooks";
private static boolean __stub_hooked = false;
public static class ActProxyMgr {
@@ -82,7 +71,10 @@ public static void initForStubActivity(Context ctx) {
mInstrumentation.setAccessible(true);
Instrumentation instrumentation = (Instrumentation) mInstrumentation.get(sCurrentActivityThread);
if (!(instrumentation instanceof ProxyInstrumentation)) {
- mInstrumentation.set(sCurrentActivityThread, new ProxyInstrumentation(instrumentation));
+ // 创建代理对象
+ ProxyInstrumentation proxy = new ProxyInstrumentation(instrumentation);
+ // 替换掉系统的实例
+ mInstrumentation.set(sCurrentActivityThread, proxy);
}
// Hook Handler (mH)
@@ -119,6 +111,7 @@ private static void hookIActivityManager() throws Exception {
gDefaultField = activityManagerClass.getDeclaredField("gDefault");
} catch (Exception err1) {
activityManagerClass = Class.forName("android.app.ActivityManager");
+ //noinspection JavaReflectionMemberAccess
gDefaultField = activityManagerClass.getDeclaredField("IActivityManagerSingleton");
}
gDefaultField.setAccessible(true);
@@ -133,7 +126,8 @@ private static void hookIActivityManager() throws Exception {
Method getMethod = singletonClass.getDeclaredMethod("get");
getMethod.setAccessible(true);
getMethod.invoke(gDefault);
- } catch (Exception ignored) {}
+ } catch (Exception ignored) {
+ }
Object mInstance = mInstanceField.get(gDefault);
if (mInstance == null) {
@@ -171,6 +165,7 @@ private static void hookIActivityManager() throws Exception {
}
}
+ @SuppressLint("PrivateApi")
private static void hookPackageManager(Context ctx, Object sCurrentActivityThread, Class> clazz_ActivityThread) {
try {
Field sPackageManagerField = clazz_ActivityThread.getDeclaredField("sPackageManager");
@@ -217,8 +212,7 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl
if (shouldProxy(raw)) {
args[i] = createTokenWrapper(raw);
}
- }
- else if (args[i] instanceof Intent[] rawIntents) {
+ } else if (args[i] instanceof Intent[] rawIntents) {
for (int j = 0; j < rawIntents.length; j++) {
if (shouldProxy(rawIntents[j])) {
rawIntents[j] = createTokenWrapper(rawIntents[j]);
@@ -429,8 +423,7 @@ private Intent tryRecoverIntent(Intent intent) {
*/
@SneakyThrows
@Override
- public Activity newActivity(ClassLoader cl, String className, Intent intent)
- throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+ public Activity newActivity(ClassLoader cl, String className, Intent intent) {
// 兜底:如果 intent 仍然是 stub 的 wrapper,尝试还原
Intent recovered = tryRecoverIntent(intent);
@@ -445,7 +438,7 @@ public Activity newActivity(ClassLoader cl, String className, Intent intent)
} catch (ClassNotFoundException e) {
if (ActProxyMgr.isModuleProxyActivity(className)) {
ClassLoader moduleCL = Objects.requireNonNull(getClass().getClassLoader());
- return (Activity) moduleCL.loadClass(className).newInstance();
+ return (Activity) moduleCL.loadClass(className).getDeclaredConstructor().newInstance();
}
throw e;
}
@@ -471,10 +464,11 @@ public void callActivityOnCreate(Activity activity, Bundle icicle) {
ClassLoader hybridCL = ParcelableFixer.getHybridClassLoader();
if (hybridCL != null) {
try {
- Field f = Activity.class.getDeclaredField("mClassLoader");
+ @SuppressWarnings("JavaReflectionMemberAccess") Field f = Activity.class.getDeclaredField("mClassLoader");
f.setAccessible(true);
f.set(activity, hybridCL);
- } catch (Throwable ignored) {}
+ } catch (Throwable ignored) {
+ }
Intent intent = activity.getIntent();
if (intent != null) {
@@ -762,7 +756,7 @@ public void callActivityOnSaveInstanceState(@NonNull Activity activity, @NonNull
}
@Override
- public void callActivityOnSaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState, PersistableBundle outPersistentState) {
+ public void callActivityOnSaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState, @NonNull PersistableBundle outPersistentState) {
mBase.callActivityOnSaveInstanceState(activity, outState, outPersistentState);
}
@@ -776,11 +770,15 @@ public void callActivityOnUserLeaving(Activity activity) {
mBase.callActivityOnUserLeaving(activity);
}
+ @SuppressWarnings("deprecation")
+ @Deprecated
@Override
public void startAllocCounting() {
mBase.startAllocCounting();
}
+ @SuppressWarnings("deprecation")
+ @Deprecated
@Override
public void stopAllocCounting() {
mBase.stopAllocCounting();
@@ -887,7 +885,11 @@ private static class IntentTokenCache {
private static class Entry {
Intent intent;
long timestamp;
- Entry(Intent i) { intent = i; timestamp = System.currentTimeMillis(); }
+
+ Entry(Intent i) {
+ intent = i;
+ timestamp = System.currentTimeMillis();
+ }
}
private static final Map sCache = new ConcurrentHashMap<>();
diff --git a/app/src/main/java/moe/ouom/wekit/ui/creator/center/DexFinderDialog.kt b/app/src/main/java/moe/ouom/wekit/ui/creator/center/DexFinderDialog.kt
index 16cac78..de37de8 100644
--- a/app/src/main/java/moe/ouom/wekit/ui/creator/center/DexFinderDialog.kt
+++ b/app/src/main/java/moe/ouom/wekit/ui/creator/center/DexFinderDialog.kt
@@ -158,7 +158,7 @@ class DexFinderDialog(
private suspend fun performParallelScanning() = withContext(Dispatchers.IO) {
val dexKit = DexKitBridge.create(appInfo.sourceDir)
- try {
+ dexKit.use { dexKit ->
// 创建进度更新 Channel
val progressChannel = Channel(Channel.UNLIMITED)
@@ -187,8 +187,6 @@ class DexFinderDialog(
withContext(Dispatchers.Main) {
handleScanResults(results)
}
- } finally {
- dexKit.close()
}
}
diff --git a/app/src/main/java/moe/ouom/wekit/ui/creator/dialog/ScriptManagerDialog.kt b/app/src/main/java/moe/ouom/wekit/ui/creator/dialog/ScriptManagerDialog.kt
new file mode 100644
index 0000000..e17e5cf
--- /dev/null
+++ b/app/src/main/java/moe/ouom/wekit/ui/creator/dialog/ScriptManagerDialog.kt
@@ -0,0 +1,611 @@
+package moe.ouom.wekit.ui.creator.dialog
+
+import android.content.Context
+import android.util.Base64
+import android.view.Gravity
+import android.view.View
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.core.content.ContextCompat
+import com.afollestad.materialdialogs.MaterialDialog
+import com.afollestad.materialdialogs.customview.customView
+import com.afollestad.materialdialogs.list.listItems
+import com.google.android.material.button.MaterialButton
+import com.google.android.material.checkbox.MaterialCheckBox
+import moe.ouom.wekit.ui.CommonContextWrapper
+import moe.ouom.wekit.util.common.ModuleRes
+import moe.ouom.wekit.util.common.Toasts.showToast
+import moe.ouom.wekit.util.log.WeLogger
+import moe.ouom.wekit.util.script.ScriptEvalManager
+import moe.ouom.wekit.util.script.ScriptFileManager
+import org.json.JSONObject
+import java.text.SimpleDateFormat
+import java.util.*
+
+/**
+ * 脚本管理器对话框
+ */
+class ScriptManagerDialog(
+ context: Context,
+ private val scriptManager: ScriptFileManager,
+ private val scriptEvalManager: ScriptEvalManager
+) : BaseSettingsDialog(context, "脚本管理器") {
+
+ private val scripts = mutableListOf()
+ private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
+
+ override fun initList() {
+ contentContainer.removeAllViews()
+ loadScripts()
+ renderScriptList()
+ }
+
+ private fun loadScripts() {
+ scripts.clear()
+ scripts.addAll(scriptManager.getAllScripts())
+ }
+
+ private fun renderScriptList() {
+ if (scripts.isEmpty()) {
+ renderEmptyView()
+ renderActionButtons()
+ return
+ }
+
+ scripts.sortBy { it.order }
+ scripts.forEachIndexed { index, script ->
+ renderScriptItem(index, script)
+ }
+
+ renderActionButtons()
+ }
+
+ private fun renderEmptyView() {
+ val emptyView = TextView(context).apply {
+ text = "暂无脚本,点击下方按钮添加"
+ gravity = Gravity.CENTER
+ textSize = 16f
+ setPadding(32, 64, 32, 64)
+ setTextColor(ContextCompat.getColor(context, android.R.color.darker_gray))
+ }
+ contentContainer.addView(emptyView)
+ }
+
+ private fun renderScriptItem(index: Int, script: ScriptFileManager.ScriptConfig) {
+ // 使用 inflateItem 方式创建视图
+ val view = inflateItem("script_item", contentContainer) ?: return
+
+ // 获取视图组件
+ val tvTitle = view.findViewById(ModuleRes.getId("title", "id"))
+ val tvUuid = view.findViewById(ModuleRes.getId("uuid", "id"))
+ val tvTime = view.findViewById(ModuleRes.getId("time", "id"))
+ val tvDesc = view.findViewById(ModuleRes.getId("description", "id"))
+ val cbEnabled = view.findViewById(ModuleRes.getId("enabled_checkbox", "id"))
+ val btnCopy = view.findViewById(ModuleRes.getId("copy_button", "id"))
+ val btnActions = view.findViewById(ModuleRes.getId("actions_button", "id"))
+
+ // 设置标题
+ tvTitle.text = "${index + 1}. ${script.name}"
+
+ // 设置UUID
+ tvUuid.text = "ID: ${script.id}..."
+
+ // 设置时间信息
+ val createdTime = dateFormat.format(Date(script.createdTime))
+ val modifiedTime = dateFormat.format(Date(script.modifiedTime))
+ tvTime.text = "创建: $createdTime | 修改: $modifiedTime"
+
+ // 设置描述
+ if (script.description.isNotEmpty()) {
+ tvDesc.text = "描述: ${script.description}"
+ tvDesc.visibility = View.VISIBLE
+ } else {
+ tvDesc.visibility = View.GONE
+ }
+
+ // 设置启用状态
+ cbEnabled.isChecked = script.enabled
+
+ // 设置启用状态监听器
+ cbEnabled.setOnCheckedChangeListener { _, isChecked ->
+ script.enabled = isChecked
+ scriptManager.saveScript(script)
+ showToast(context, if (isChecked) "已启用" else "已禁用")
+ }
+
+ // 设置复制按钮监听器
+ btnCopy.setOnClickListener {
+ copyFullScriptInfo(script)
+ }
+
+ // 设置操作按钮监听器
+ btnActions.setOnClickListener {
+ showActionMenu(index, script)
+ }
+
+ // 设置整个视图的点击监听器,进入脚本编辑
+ view.setOnClickListener {
+ showEditDialog(index)
+ }
+
+ contentContainer.addView(view)
+
+ // 添加分隔线(如果不是最后一个)
+ if (index < scripts.size - 1) {
+ val divider = TextView(context).apply {
+ setBackgroundColor(ContextCompat.getColor(context, android.R.color.darker_gray))
+ layoutParams = LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ 1
+ ).apply {
+ setMargins(32, 16, 32, 16)
+ }
+ }
+ contentContainer.addView(divider)
+ }
+ }
+
+ /**
+ * 显示操作菜单
+ */
+ private fun showActionMenu(index: Int, script: ScriptFileManager.ScriptConfig) {
+ val wrappedContext = CommonContextWrapper.createAppCompatContext(context)
+ val options = mutableListOf()
+ val isTop = index == 0
+ val isBottom = index == scripts.size - 1
+
+ options.add("上移")
+ options.add("下移")
+ options.add("编辑")
+ options.add("删除")
+ options.add("测试")
+
+ MaterialDialog(wrappedContext)
+ .title(text = "脚本操作")
+ .listItems(items = options) { dialog, optionIndex, _ ->
+ when (optionIndex) {
+ 0 -> {
+ // 上移
+ if (isTop) {
+ showToast(context, "已在顶部,无法上移")
+ return@listItems
+ }
+ moveScriptUp(index)
+ }
+
+ 1 -> {
+ // 下移
+ if (isBottom) {
+ showToast(context, "已在底部,无法下移")
+ return@listItems
+ }
+ moveScriptDown(index)
+ }
+
+ 2 -> showEditDialog(index)
+ 3 -> confirmDeleteScript(index)
+ 4 -> showTestDialog(index)
+ }
+
+ dialog.dismiss()
+ }
+ .negativeButton(text = "取消")
+ .show()
+ }
+
+ /**
+ * 复制完整的脚本信息
+ */
+ private fun copyFullScriptInfo(script: ScriptFileManager.ScriptConfig) {
+ val contentEncoded = Base64.encodeToString(script.content.toByteArray(Charsets.UTF_8), Base64.DEFAULT)
+ val jsonObject = JSONObject().apply {
+ put("name", script.name)
+ put("id", script.id)
+ put("description", script.description)
+ put("content", contentEncoded)
+ }
+
+ copyToClipboard("脚本JSON", jsonObject.toString(2))
+ showToast(context, "已复制脚本JSON格式")
+ }
+
+ private fun renderActionButtons() {
+ val buttonLayout = LinearLayout(context).apply {
+ orientation = LinearLayout.HORIZONTAL
+ gravity = Gravity.CENTER
+ setPadding(32, 24, 32, 32)
+ }
+
+ val btnAdd = MaterialButton(context).apply {
+ text = "添加脚本"
+ layoutParams = LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.WRAP_CONTENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT
+ ).apply {
+ marginEnd = 16
+ }
+ setOnClickListener {
+ showAddDialog()
+ }
+ }
+
+ val btnImport = MaterialButton(context).apply {
+ text = "导入"
+ layoutParams = LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.WRAP_CONTENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT
+ )
+ setOnClickListener {
+ showImportOptions()
+ }
+ }
+
+ buttonLayout.addView(btnAdd)
+ buttonLayout.addView(btnImport)
+ contentContainer.addView(buttonLayout)
+ }
+
+ private fun showAddDialog() {
+ showEditDialog(-1)
+ }
+
+ private fun showEditDialog(index: Int) {
+ val isNew = index == -1
+ val script = if (isNew) {
+ val newId = UUID.randomUUID().toString()
+ ScriptFileManager.ScriptConfig(
+ id = newId,
+ name = "",
+ content = "",
+ order = scripts.size
+ )
+ } else {
+ scripts.getOrNull(index) ?: return
+ }
+
+ val wrappedContext = CommonContextWrapper.createAppCompatContext(context)
+
+ // 使用 ModuleRes 加载布局
+ val dialogView = ModuleRes.inflate("dialog_script_edit", null)
+
+ // 获取视图组件
+ val tvUuid = dialogView.findViewById(ModuleRes.getId("tv_uuid", "id"))
+ val etName = dialogView.findViewById(ModuleRes.getId("et_name", "id"))
+ val etDesc = dialogView.findViewById(ModuleRes.getId("et_desc", "id"))
+ val etContent = dialogView.findViewById(ModuleRes.getId("et_content", "id"))
+
+ // 设置UUID
+ tvUuid.text = "脚本ID: ${script.id}"
+
+ // 设置现有值
+ etName.text = script.name
+ etDesc.text = script.description ?: ""
+ etContent.text = script.content
+
+ MaterialDialog(wrappedContext)
+ .customView(view = dialogView)
+ .title(text = if (isNew) "添加脚本" else "编辑脚本")
+ .positiveButton(text = "保存") {
+ val name = etName.text.toString().trim()
+ val description = etDesc.text.toString().trim()
+ val content = etContent.text.toString().trim()
+
+ if (name.isEmpty()) {
+ showToast(context, "请输入脚本名称")
+ return@positiveButton
+ }
+
+ if (content.isEmpty()) {
+ showToast(context, "请输入脚本内容")
+ return@positiveButton
+ }
+
+ saveScript(script, name, description, content, isNew)
+ }
+ .neutralButton(text = "复制脚本内容") {
+ val content = etContent.text.toString().trim()
+ if (content.isNotEmpty()) {
+ copyToClipboard("脚本内容", content)
+ showToast(context, "已复制脚本内容")
+ } else {
+ showToast(context, "脚本内容为空")
+ }
+ }
+ .negativeButton(text = "取消")
+ .show()
+ }
+
+ /**
+ * 显示测试对话框
+ */
+ private fun showTestDialog(index: Int) {
+ val script = scripts.getOrNull(index) ?: return
+ val wrappedContext = CommonContextWrapper.createAppCompatContext(context)
+
+ // 使用 ModuleRes 加载布局
+ val dialogView = ModuleRes.inflate("dialog_test_script", null) ?: return
+
+ // 获取视图组件
+ val tvScriptName = dialogView.findViewById(ModuleRes.getId("tv_script_name", "id"))
+ val etInput = dialogView.findViewById(ModuleRes.getId("et_input", "id"))
+
+ // 设置脚本名称
+ tvScriptName.text = "测试脚本: ${script.name}"
+
+ MaterialDialog(wrappedContext)
+ .customView(view = dialogView)
+ .title(text = "测试脚本执行")
+ .positiveButton(text = "执行") {
+ val jsCode = etInput.text.toString().trim()
+ if (jsCode.isNotEmpty()) {
+ executeTestScript(script, jsCode)
+ } else {
+ showToast(context, "请输入要执行的JavaScript代码")
+ }
+ }
+ .negativeButton(text = "取消")
+ .show()
+ }
+
+ /**
+ * 执行测试脚本
+ */
+ private fun executeTestScript(script: ScriptFileManager.ScriptConfig, jsCode: String) {
+ val wrappedContext = CommonContextWrapper.createAppCompatContext(context)
+
+ val result = scriptEvalManager.testExecuteCode(script.content, jsCode, "${script.name}+测试脚本")
+ MaterialDialog(wrappedContext)
+ .title(text = "执行结果")
+ .message(text = result ?: "执行失败或无返回值")
+ .positiveButton(text = "复制结果") {
+ copyToClipboard("执行结果", result ?: "执行失败")
+ }
+ .negativeButton(text = "关闭")
+ .show()
+ }
+
+ private fun saveScript(
+ script: ScriptFileManager.ScriptConfig,
+ name: String,
+ description: String,
+ content: String,
+ isNew: Boolean
+ ) {
+ // 检测脚本是否正常
+ val testResult = scriptEvalManager.testScriptMethods(content)
+ if (!testResult.isPassed()) {
+ val errorMsg = testResult.getSummary()
+ showToast(context, "脚本验证失败,请修正后保存")
+
+ // 显示详细错误信息
+ val wrappedContext = CommonContextWrapper.createAppCompatContext(context)
+ MaterialDialog(wrappedContext)
+ .title(text = "脚本验证失败")
+ .message(text = errorMsg)
+ .positiveButton(text = "返回编辑") {
+ showEditDialog(if (isNew) -1 else scripts.indexOf(script))
+ }
+ .negativeButton(text = "取消")
+ .show()
+ return
+ }
+
+ script.name = name
+ script.description = description
+ script.content = content
+ script.modifiedTime = System.currentTimeMillis()
+
+ if (isNew) {
+ script.createdTime = System.currentTimeMillis()
+ scripts.add(script)
+ scriptManager.saveScript(script)
+ showToast(context, "脚本已添加")
+ } else {
+ scriptManager.saveScript(script)
+ showToast(context, "脚本已更新")
+ }
+
+ contentContainer.removeAllViews()
+ renderScriptList()
+ }
+
+ private fun showImportOptions() {
+ val wrappedContext = CommonContextWrapper.createAppCompatContext(context)
+ val options = listOf("从剪贴板导入", "导入示例")
+
+ MaterialDialog(wrappedContext)
+ .title(text = "导入脚本")
+ .listItems(items = options) { dialog, optionIndex, _ ->
+ when (optionIndex) {
+ 0 -> importFromClipboard()
+ 1 -> importExample()
+ }
+ dialog.dismiss()
+ }
+ .negativeButton(text = "取消")
+ .show()
+ }
+
+ private fun importFromClipboard() {
+ try {
+ val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? android.content.ClipboardManager
+ val clipData = clipboard?.primaryClip
+
+ if (clipData != null && clipData.itemCount > 0) {
+ val clipItem = clipData.getItemAt(0)
+ val text = clipItem.text?.toString()
+
+ if (!text.isNullOrBlank()) {
+ // 尝试解析为JSON格式
+ val jsonObject = JSONObject(text)
+
+ // 验证必需字段
+ if (jsonObject.has("name") && jsonObject.has("id") && jsonObject.has("description") && jsonObject.has(
+ "content"
+ )
+ ) {
+ val newName = jsonObject.optString("name", "从剪贴板导入")
+ val newId = jsonObject.optString("id", UUID.randomUUID().toString())
+ val newDescription = jsonObject.optString("description", "")
+ val contentEncoded = jsonObject.optString("content", "")
+
+ // 解码内容
+ val contentBytes = Base64.decode(contentEncoded, Base64.NO_WRAP)
+ val newContent = String(contentBytes, Charsets.UTF_8)
+
+ // 检查是否已存在相同ID的脚本
+ val existingScript = scripts.find { it.id == newId }
+ if (existingScript != null) {
+ // 覆盖现有脚本
+ existingScript.name = newName
+ existingScript.content = newContent
+ existingScript.description = newDescription
+ existingScript.modifiedTime = System.currentTimeMillis()
+
+ scriptManager.saveScript(existingScript)
+ showToast(context, "已覆盖相同ID的脚本")
+
+ contentContainer.removeAllViews()
+ renderScriptList()
+ return
+ }
+
+ val newScript = ScriptFileManager.ScriptConfig(
+ id = newId,
+ name = newName,
+ content = newContent,
+ description = newDescription,
+ order = scripts.size
+ )
+
+ scriptManager.saveScript(newScript)
+ scripts.add(newScript)
+
+ contentContainer.removeAllViews()
+ renderScriptList()
+ showToast(context, "已从剪贴板导入脚本")
+ } else {
+ showToast(context, "剪贴板内容格式不正确,缺少必要字段(name, id, description, content)")
+ }
+ } else {
+ showToast(context, "剪贴板内容为空")
+ }
+ } else {
+ showToast(context, "剪贴板无内容")
+ }
+ } catch (e: Exception) {
+ WeLogger.e("从剪贴板导入失败: ${e.message}")
+ showToast(context, "导入失败,请确保剪贴板内容为有效的JSON格式")
+ }
+ }
+
+ private fun importExample() {
+ val exampleScripts = listOf(
+ ScriptFileManager.ScriptConfig(
+ id = UUID.randomUUID().toString(),
+ name = "打印示例",
+ content = """
+ // 这是一个简单的示例脚本 将打印参数
+ function onRequest(data) {
+ const {uri,cgiId,jsonData} = data;
+ wekit.log('拦截请求:', uri, cgiId, JSON.stringify(jsonData));
+ return jsonData;
+ }
+
+ function onResponse(data) {
+ const {uri,cgiId,jsonData} = data;
+ wekit.log('拦截响应:', uri, cgiId, JSON.stringify(jsonData));
+ return jsonData;
+ }
+ """.trimIndent(),
+ description = "最简单的示例脚本,包含onRequest和onResponse方法",
+ order = scripts.size
+ )
+ )
+
+ exampleScripts.forEach { script ->
+ scriptManager.saveScript(script)
+ scripts.add(script)
+ }
+
+ contentContainer.removeAllViews()
+ renderScriptList()
+ showToast(context, "已导入示例脚本")
+ }
+
+ private fun confirmDeleteScript(index: Int) {
+ val script = scripts.getOrNull(index) ?: return
+ val wrappedContext = CommonContextWrapper.createAppCompatContext(context)
+
+ MaterialDialog(wrappedContext)
+ .title(text = "确认删除")
+ .message(text = "确定要删除脚本「${script.name}」吗?\nID: ${script.id}")
+ .positiveButton(text = "删除") {
+ deleteScript(index)
+ }
+ .negativeButton(text = "取消")
+ .show()
+ }
+
+ private fun deleteScript(index: Int) {
+ val script = scripts.getOrNull(index) ?: return
+
+ if (scriptManager.deleteScript(script.id)) {
+ scripts.removeAt(index)
+
+ scripts.forEachIndexed { i, s ->
+ s.order = i
+ scriptManager.saveScript(s)
+ }
+
+ contentContainer.removeAllViews()
+ renderScriptList()
+ showToast(context, "脚本已删除")
+ } else {
+ showToast(context, "删除失败")
+ }
+ }
+
+ private fun moveScriptUp(index: Int) {
+ if (index > 0) {
+ val temp = scripts[index]
+ scripts[index] = scripts[index - 1]
+ scripts[index - 1] = temp
+
+ scripts.forEachIndexed { i, s ->
+ s.order = i
+ scriptManager.saveScript(s)
+ }
+
+ contentContainer.removeAllViews()
+ renderScriptList()
+ }
+ }
+
+ private fun moveScriptDown(index: Int) {
+ if (index < scripts.size - 1) {
+ val temp = scripts[index]
+ scripts[index] = scripts[index + 1]
+ scripts[index + 1] = temp
+
+ scripts.forEachIndexed { i, s ->
+ s.order = i
+ scriptManager.saveScript(s)
+ }
+
+ contentContainer.removeAllViews()
+ renderScriptList()
+ }
+ }
+
+ private fun copyToClipboard(label: String, text: String) {
+ try {
+ val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? android.content.ClipboardManager
+ val clip = android.content.ClipData.newPlainText(label, text)
+ clipboard?.setPrimaryClip(clip)
+ } catch (e: Exception) {
+ WeLogger.e("复制失败: ${e.message}")
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/moe/ouom/wekit/util/script/JsExecutor.kt b/app/src/main/java/moe/ouom/wekit/util/script/JsExecutor.kt
index 5086ee5..ce0227f 100644
--- a/app/src/main/java/moe/ouom/wekit/util/script/JsExecutor.kt
+++ b/app/src/main/java/moe/ouom/wekit/util/script/JsExecutor.kt
@@ -4,13 +4,14 @@ import android.annotation.SuppressLint
import android.content.Context
import android.os.Handler
import android.os.Looper
+import moe.ouom.wekit.constants.MMVersion
+import moe.ouom.wekit.hooks.item.script.WeApiUtils
+import moe.ouom.wekit.host.HostInfo
import moe.ouom.wekit.util.log.WeLogger
import org.mozilla.javascript.ScriptRuntime
import org.mozilla.javascript.Scriptable
import java.text.MessageFormat
-import java.util.Locale
-import java.util.ResourceBundle
-import java.util.MissingResourceException
+import java.util.*
import javax.script.ScriptEngine
import javax.script.ScriptEngineManager
@@ -123,8 +124,6 @@ class JsExecutor private constructor() {
val rhinoEngine = engineManager.getEngineByName("rhino")
mScriptEngine = rhinoEngine
- // 注入日志接口
- injectLoggingInterface()
mInitialized = true
WeLogger.i("JsExecutor initialized with Rhino")
@@ -165,17 +164,53 @@ class JsExecutor private constructor() {
}
/**
- * 注入日志接口
+ * 注入脚本接口
+ * @param sendCgi CGI发送函数
+ * @param protoUtils 协议工具对象
+ * @param dataBaseUtils 数据库工具对象
+ * @param messageUtils 消息工具对象
*/
@Suppress("unused")
- private fun injectLoggingInterface() {
+ fun injectScriptInterfaces(
+ sendCgi: Any,
+ weApiUtils: Any,
+ protoUtils: Any,
+ dataBaseUtils: Any,
+ messageUtils: Any
+ ) {
try {
- // 为 ScriptEngine 添加日志接口
mScriptEngine?.put("wekit", object {
fun log(vararg args: Any?) {
val message = args.joinToString(" ") { it?.toString() ?: "null" }
ScriptLogger.getInstance().info(message)
}
+
+ fun isMMAtLeast(field: String) = runCatching {
+ HostInfo.getVersionCode() >= MMVersion::class.java.getField(field).getInt(null)
+ }.getOrDefault(false)
+
+ fun isGooglePlayVersion() = HostInfo.isGooglePlayVersion
+
+ fun sendCgi(uri: String, cgiId: Int, funcId: Int, routeId: Int, jsonPayload: String) {
+ sendCgi(uri, cgiId, funcId, routeId, jsonPayload)
+ }
+
+ fun getSelfWxId(): String {
+ return WeApiUtils.getSelfWxId()
+ }
+
+ fun getSelfAlias(): String {
+ return WeApiUtils.getSelfAlias()
+ }
+
+ @JvmField
+ val proto: Any = protoUtils
+
+ @JvmField
+ val database: Any = dataBaseUtils
+
+ @JvmField
+ val message: Any = messageUtils
})
WeLogger.i("JsExecutor: Injected Rhino logging interface")
diff --git a/app/src/main/res/drawable/edit_24px.xml b/app/src/main/res/drawable/edit_24px.xml
new file mode 100644
index 0000000..b253108
--- /dev/null
+++ b/app/src/main/res/drawable/edit_24px.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/layout/dialog_script_edit.xml b/app/src/main/res/layout/dialog_script_edit.xml
new file mode 100644
index 0000000..022ba8c
--- /dev/null
+++ b/app/src/main/res/layout/dialog_script_edit.xml
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_test_script.xml b/app/src/main/res/layout/dialog_test_script.xml
new file mode 100644
index 0000000..7d82462
--- /dev/null
+++ b/app/src/main/res/layout/dialog_test_script.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/script_item.xml b/app/src/main/res/layout/script_item.xml
new file mode 100644
index 0000000..2ee56b1
--- /dev/null
+++ b/app/src/main/res/layout/script_item.xml
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/generate_secrets_h.py b/generate_secrets_h.py
new file mode 100644
index 0000000..eaf6083
--- /dev/null
+++ b/generate_secrets_h.py
@@ -0,0 +1,76 @@
+import sys
+import random
+
+
+def generate_c_header(hex_string, keys=None):
+ """
+ 生成 secrets.h C头文件
+
+ Args:
+ hex_string: APK签名的SHA256哈希值(64位十六进制字符串)
+ keys: 4个密钥的列表,如果不提供则随机生成
+
+ Returns:
+ C头文件内容的字符串
+ """
+ # 验证输入:必须是64个十六进制字符(0-9, A-F)
+ hex_string = hex_string.strip().upper()
+ if len(hex_string) != 64 or not all(c in "0123456789ABCDEF" for c in hex_string):
+ raise ValueError("输入必须是64个十六进制字符(0-9, A-F)")
+
+ # 如果没有提供keys,随机生成4个密钥(0x00-0xFF)
+ if keys is None:
+ keys = [random.randint(0x00, 0xFF) for _ in range(4)]
+ elif len(keys) != 4:
+ raise ValueError("keys必须是包含4个密钥的列表")
+
+ # 将字符串转换为ASCII字节数组(64字节)
+ plaintext = [ord(c) for c in hex_string]
+
+ # 分段加密(每段16字节)
+ encrypted_segments = []
+ for i in range(4):
+ start = i * 16
+ segment = plaintext[start : start + 16]
+ key = keys[i]
+ encrypted = [b ^ key for b in segment]
+ encrypted_segments.append(encrypted)
+
+ # 生成C头文件
+ output = "#pragma once\n\n"
+ for i, (key, enc_data) in enumerate(zip(keys, encrypted_segments), 1):
+ output += f"static const unsigned char KEY{i} = 0x{key:02X};\n"
+ output += f"static const unsigned char ENC_PART{i}[] = {{\n"
+ # 格式化为每行8个字节
+ for j in range(0, 16, 8):
+ line_bytes = enc_data[j : j + 8]
+ line = ", ".join(f"0x{b:02X}" for b in line_bytes)
+ output += f" {line}"
+ if j < 8: # 第一行需要逗号
+ output += ","
+ output += "\n"
+ output += "};\n\n"
+
+ return output.strip()
+
+
+def main():
+ if len(sys.argv) != 2:
+ print("用法: python generate_secrets_h.py <64位SHA256签名>", file=sys.stderr)
+ print(
+ "示例: python generate_secrets_h.py 156B65C9CBE827BF0BB22F9E00BEEC3258319CE8A15D2A3729275CAF71CEDA21",
+ file=sys.stderr,
+ )
+ sys.exit(1)
+
+ try:
+ # 默认使用随机密钥生成
+ result = generate_c_header(sys.argv[1])
+ print(result)
+ except ValueError as e:
+ print(f"错误: {e}", file=sys.stderr)
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ main()