diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index b659d29..9636d39 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -18,7 +18,6 @@
- [ ] **我确认此更改不会破坏任何原有功能** / 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 ecd8c37..be677dd 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -266,7 +266,7 @@ moe.ouom.wekit/
│ │ ├── moment/ # 朋友圈
│ │ ├── fix/ # 优化与修复
│ │ ├── dev/ # 开发者选项
-│ │ ├── func/ # 娱乐功能
+│ │ ├── fun/ # 娱乐功能
│ │ ├── script/ # 脚本管理
│ │ └── example/ # 示例代码
│ └── sdk/ # SDK 封装
@@ -420,14 +420,10 @@ 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)
@@ -2243,70 +2239,6 @@ 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 数据的核心工具类,提供以下关键方法:
@@ -3037,7 +2969,6 @@ 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
@@ -3095,28 +3026,6 @@ 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 a158f1a..5ad4df1 100644
--- a/SCRIPT_API_DOCUMENT.md
+++ b/SCRIPT_API_DOCUMENT.md
@@ -3,28 +3,14 @@
## Beta 状态警告
**重要提醒:此 API 目前处于 Beta 测试阶段,在后续更新中可能出现不兼容的更改,使用时请密切关注相关更新通知。**
-
-**目前暂无提供监听信息发送等的 API 的想法,请自行根据 cgiId 判断实现相关监听。**
-
## 目录
1. [钩子函数](#钩子函数)
- - [onRequest](#onrequest)
- - [onResponse](#onresponse)
- - [数据对象说明](#数据对象说明)
+ - [onRequest](#onrequest)
+ - [onResponse](#onresponse)
2. [WEKit 对象](#wekit-对象)
- - [概述](#概述)
- - [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. [后续更新计划](#后续更新计划)
+ - [概述](#概述)
+ - [WEKit Log 函数](#wekit-log-函数)
## 钩子函数
@@ -60,22 +46,16 @@ 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 对象
### 概述
@@ -104,636 +84,11 @@ 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 f1079b6..74bc943 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,16 +20,23 @@ private RuntimeConfig() {
// account info //
- private static SharedPreferences mmPrefs;
+ // 注意时效性,这里保存的登录信息是刚启动应用时的登录信息,而不是实时的登录信息
+ // TODO: 需要一个机制来更新这些信息
+
// 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"
@@ -78,6 +85,22 @@ 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;
}
@@ -86,23 +109,19 @@ 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 mmPrefs.getString("login_weixin_username", "");
+ return login_weixin_username;
}
public static String getLast_login_nick_name() {
- return mmPrefs.getString("last_login_nick_name", "");
+ return last_login_nick_name;
}
public static String getLogin_user_name() {
- return mmPrefs.getString("login_user_name", "");
+ return login_user_name;
}
public static String getLast_login_uin() {
- return mmPrefs.getString("last_login_uin", "0");
+ return last_login_uin;
}
}
\ 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 25d1ce6..c399858 100644
--- a/app/src/main/java/moe/ouom/wekit/constants/Constants.kt
+++ b/app/src/main/java/moe/ouom/wekit/constants/Constants.kt
@@ -18,7 +18,6 @@ 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 adc748b..cf53873 100644
--- a/app/src/main/java/moe/ouom/wekit/constants/MMVersion.kt
+++ b/app/src/main/java/moe/ouom/wekit/constants/MMVersion.kt
@@ -1,7 +1,5 @@
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
@@ -13,9 +11,8 @@ 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 = 2800
+ const val MM_8_0_56 = 2780
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"
@@ -29,8 +26,4 @@ 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 5230b03..db2fde1 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
deleted file mode 100644
index 5319dcf..0000000
--- a/app/src/main/java/moe/ouom/wekit/hooks/item/chat/msg/AntiFoldMsg.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-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
deleted file mode 100644
index 1f17f92..0000000
--- a/app/src/main/java/moe/ouom/wekit/hooks/item/chat/msg/ModifyMessageDisplayHook.kt
+++ /dev/null
@@ -1,80 +0,0 @@
-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
deleted file mode 100644
index 326789c..0000000
--- a/app/src/main/java/moe/ouom/wekit/hooks/item/chat/msg/MsgType.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-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 00160a8..2c36f90 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 class ConfigDialog(context: Context) : BaseRikkaDialog(context, "收银台余额配置") {
+ private inner 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 b9cb651..4104bff 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(), IDexFind, WeDatabaseListener.InsertListener {
+class WeRedPacketAuto : BaseClickableFunctionHookItem(), WeDatabaseListener.DatabaseInsertListener, IDexFind {
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 c46a0c3..0b67938 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,10 +1,28 @@
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
deleted file mode 100644
index fc33518..0000000
--- a/app/src/main/java/moe/ouom/wekit/hooks/item/contact/ShowWeChatIdHook.kt
+++ /dev/null
@@ -1,87 +0,0 @@
-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 f841526..c40673b 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,6 +5,8 @@ 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/func/WeProfileCleaner.kt b/app/src/main/java/moe/ouom/wekit/hooks/item/dev/WeProfileCleaner.kt
similarity index 96%
rename from app/src/main/java/moe/ouom/wekit/hooks/item/func/WeProfileCleaner.kt
rename to app/src/main/java/moe/ouom/wekit/hooks/item/dev/WeProfileCleaner.kt
index 576e0d8..17de24a 100644
--- a/app/src/main/java/moe/ouom/wekit/hooks/item/func/WeProfileCleaner.kt
+++ b/app/src/main/java/moe/ouom/wekit/hooks/item/dev/WeProfileCleaner.kt
@@ -1,8 +1,9 @@
-package moe.ouom.wekit.hooks.item.func
+package moe.ouom.wekit.hooks.item.dev
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/func/WeProfileNameSetter.kt b/app/src/main/java/moe/ouom/wekit/hooks/item/dev/WeProfileNameSetter.kt
similarity index 97%
rename from app/src/main/java/moe/ouom/wekit/hooks/item/func/WeProfileNameSetter.kt
rename to app/src/main/java/moe/ouom/wekit/hooks/item/dev/WeProfileNameSetter.kt
index 26519b3..0de93da 100644
--- a/app/src/main/java/moe/ouom/wekit/hooks/item/func/WeProfileNameSetter.kt
+++ b/app/src/main/java/moe/ouom/wekit/hooks/item/dev/WeProfileNameSetter.kt
@@ -1,8 +1,9 @@
-package moe.ouom.wekit.hooks.item.func
+package moe.ouom.wekit.hooks.item.dev
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/fix/ForceCameraScan.kt b/app/src/main/java/moe/ouom/wekit/hooks/item/fix/ForceCameraScan.kt
deleted file mode 100644
index 8976d15..0000000
--- a/app/src/main/java/moe/ouom/wekit/hooks/item/fix/ForceCameraScan.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-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
deleted file mode 100644
index 6c037ac..0000000
--- a/app/src/main/java/moe/ouom/wekit/hooks/item/fix/WeChatArticleAdRemover.kt
+++ /dev/null
@@ -1,90 +0,0 @@
-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
deleted file mode 100644
index 8f10762..0000000
--- a/app/src/main/java/moe/ouom/wekit/hooks/item/func/WeAvatarTransparent.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-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/moment/AntiSnsAd.kt b/app/src/main/java/moe/ouom/wekit/hooks/item/moment/AntiSnsAd.kt
deleted file mode 100644
index b4934b9..0000000
--- a/app/src/main/java/moe/ouom/wekit/hooks/item/moment/AntiSnsAd.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-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
deleted file mode 100644
index c60911c..0000000
--- a/app/src/main/java/moe/ouom/wekit/hooks/item/moment/AntiSnsDeleteHook.kt
+++ /dev/null
@@ -1,98 +0,0 @@
-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
deleted file mode 100644
index 5a89572..0000000
--- a/app/src/main/java/moe/ouom/wekit/hooks/item/moment/SnsContentType.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-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
deleted file mode 100644
index 62677ad..0000000
--- a/app/src/main/java/moe/ouom/wekit/hooks/item/moment/SnsLikeModifyHook.kt
+++ /dev/null
@@ -1,205 +0,0 @@
-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 0e2e93c..88672cf 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,37 +1,41 @@
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.creator.dialog.ScriptManagerDialog
+import moe.ouom.wekit.ui.CommonContextWrapper
+import moe.ouom.wekit.ui.creator.dialog.BaseSettingsDialog
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)
}
@@ -84,4 +88,715 @@ 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
deleted file mode 100644
index f127ebf..0000000
--- a/app/src/main/java/moe/ouom/wekit/hooks/item/script/WeApiUtils.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-@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
deleted file mode 100644
index 50c5a9e..0000000
--- a/app/src/main/java/moe/ouom/wekit/hooks/item/script/WeDataBaseUtils.kt
+++ /dev/null
@@ -1,160 +0,0 @@
-@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
deleted file mode 100644
index a380558..0000000
--- a/app/src/main/java/moe/ouom/wekit/hooks/item/script/WeMessageUtils.kt
+++ /dev/null
@@ -1,96 +0,0 @@
-@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
deleted file mode 100644
index 8e82d1f..0000000
--- a/app/src/main/java/moe/ouom/wekit/hooks/item/script/WeProtoUtils.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-@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 84c3d19..1e2d0f5 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,119 +45,6 @@ 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")
@@ -360,21 +247,66 @@ class WeDatabaseApi : ApiHookItem(), IDexFind {
* 返回所有人类账号(包含好友、陌生人、自己),但排除群和公众号
*/
fun getAllConnects(): List {
- return mapToContact(executeQuery(SQL.ALL_CONNECTS))
+ 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))
}
/**
* 获取【好友】
*/
fun getContactList(): List {
- return mapToContact(executeQuery(SQL.CONTACT_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))
}
/**
* 获取【群聊】
*/
fun getChatroomList(): List {
- return executeQuery(SQL.CHATROOM_LIST).map { row ->
+ 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 ->
WeGroup(
username = row.str("username"),
nickname = row.str("nickname"),
@@ -392,7 +324,7 @@ class WeDatabaseApi : ApiHookItem(), IDexFind {
fun getGroupMembers(chatroomId: String): List {
if (!chatroomId.endsWith("@chatroom")) return emptyList()
- val roomSql = SQL.CHATROOM_MEMBERS.format(chatroomId)
+ val roomSql = "SELECT memberlist FROM chatroom WHERE chatroomname = '$chatroomId'"
val roomResult = executeQuery(roomSql)
if (roomResult.isEmpty()) {
@@ -408,14 +340,32 @@ class WeDatabaseApi : ApiHookItem(), IDexFind {
val idsStr = members.joinToString(",") { "'$it'" }
- return mapToContact(executeQuery(SQL.groupMembers(idsStr)))
+ 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))
}
/**
* 获取【公众号】
*/
fun getOfficialAccountList(): List {
- return executeQuery(SQL.OFFICIAL_LIST).map { row ->
+ 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 ->
WeOfficial(
username = row.str("username"),
nickname = row.str("nickname"),
@@ -432,7 +382,9 @@ 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
- return executeQuery(SQL.messages(wxid, pageSize, offset)).map { row ->
+ 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 ->
WeMessage(
msgId = row.long("msgId"),
talker = row.str("talker"),
@@ -449,7 +401,8 @@ class WeDatabaseApi : ApiHookItem(), IDexFind {
*/
fun getAvatarUrl(wxid: String): String {
if (wxid.isEmpty()) return ""
- val result = executeQuery(SQL.avatar(wxid))
+ val sql = "SELECT i.reserved2 AS avatarUrl FROM img_flag i WHERE i.username = '$wxid'"
+ val result = executeQuery(sql)
return if (result.isNotEmpty()) {
result[0]["avatarUrl"] as? String ?: ""
} else {
@@ -489,4 +442,5 @@ 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 f72ec60..4a6a609 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,13 +2,12 @@ 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
@@ -16,108 +15,40 @@ import java.util.concurrent.CopyOnWriteArrayList
@SuppressLint("DiscouragedApi")
@HookItem(path = "API/数据库监听服务", desc = "为其他功能提供数据库写入监听能力")
class WeDatabaseListener : ApiHookItem() {
- // 定义独立的接口
- interface InsertListener {
- fun onInsert(table: String, values: ContentValues)
- }
- interface UpdateListener {
- fun onUpdate(table: String, values: ContentValues): Boolean
+ // 定义监听器接口
+ interface DatabaseInsertListener {
+ fun onInsert(table: String, values: ContentValues)
}
- interface QueryListener {
- fun onQuery(sql: String): String?
- }
companion object {
- private const val TAG = "WeDatabaseApi"
-
- private val insertListeners = CopyOnWriteArrayList()
- private val updateListeners = CopyOnWriteArrayList()
- private val queryListeners = CopyOnWriteArrayList()
- fun addListener(listener: Any) {
- val addedTypes = mutableListOf()
+ private val listeners = CopyOnWriteArrayList()
- 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 addListener(listener: DatabaseInsertListener) {
+ if (!listeners.contains(listener)) {
+ listeners.add(listener)
+ WeLogger.i("WeDatabaseApi: 监听器已添加,当前监听器数量: ${listeners.size}")
+ } else {
+ WeLogger.w("WeDatabaseApi: 监听器已存在,跳过添加")
}
}
- 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}")
- }
+ fun removeListener(listener: DatabaseInsertListener) {
+ val removed = listeners.remove(listener)
+ WeLogger.i("WeDatabaseApi: 监听器移除${if (removed) "成功" else "失败"},当前监听器数量: ${listeners.size}")
}
-
}
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 method = XposedHelpers.findMethodExact(
+
+ val mInsertWithOnConflict = XposedHelpers.findMethodExact(
clsSQLite,
"insertWithOnConflict",
String::class.java,
@@ -126,163 +57,40 @@ class WeDatabaseListener : ApiHookItem() {
Int::class.javaPrimitiveType
)
- hookAfter(method) { param ->
+ hookAfter(mInsertWithOnConflict) { 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)
- }
- }
- // ==================== 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()}")
+ 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) }
}
} catch (e: Throwable) {
- WeLogger.e(TAG, "Update dispatch failed", e)
+ WeLogger.e("WeDatabaseApi: Dispatch failed", e)
}
}
- WeLogger.i(TAG, "Update hook success")
+ WeLogger.i("WeDatabaseApi: Hook success")
} catch (e: Throwable) {
- WeLogger.e(TAG, "Hook update failed", e)
+ WeLogger.e("WeDatabaseApi: Hook database failed", e)
}
}
- // ==================== 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)
- }
- }
+ override fun unload(classLoader: ClassLoader) {
+ listeners.clear()
}
}
\ 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 c5067cb..29f6be5 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 selfWxAlias = getSelfAlias()
- if (selfWxAlias.isEmpty()) throw IllegalStateException("无法获取 WxAlias")
+ val selfWxid = getSelfAlias()
+ if (selfWxid.isEmpty()) throw IllegalStateException("无法获取 Wxid")
// 获取 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, selfWxAlias, "amr_") as? String ?: throw IllegalStateException("VoiceName Gen Failed")
+ val fileName = voiceNameGenMethod?.invoke(null, selfWxid, "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,6 +762,7 @@ 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 9ed4bbd..9d76d11 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,6 +15,14 @@ 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 a3107c0..fa26364 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,14 +11,10 @@ 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) {
@@ -44,27 +40,6 @@ 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
deleted file mode 100644
index 3f3b847..0000000
--- a/app/src/main/java/moe/ouom/wekit/hooks/sdk/ui/WeChatChatContextMenuApi.kt
+++ /dev/null
@@ -1,170 +0,0 @@
-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
deleted file mode 100644
index 6c1980a..0000000
--- a/app/src/main/java/moe/ouom/wekit/hooks/sdk/ui/WeChatContactInfoAdapterItemHook.kt
+++ /dev/null
@@ -1,184 +0,0 @@
-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
deleted file mode 100644
index 596f50b..0000000
--- a/app/src/main/java/moe/ouom/wekit/hooks/sdk/ui/WeChatSnsContextMenuApi.kt
+++ /dev/null
@@ -1,191 +0,0 @@
-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 3acef47..d6f4693 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 = moe.ouom.wekit.host.impl.HostInfo.isWeChat();
-
- public static boolean isGooglePlayVersion = moe.ouom.wekit.host.impl.HostInfo.isGooglePlayVersion();
+ public static boolean isWeChat() {
+ return moe.ouom.wekit.host.impl.HostInfo.isWeChat();
+ }
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 6332ff7..f39c403 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,12 +75,6 @@ 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 a7964f0..4a1c9a0 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,7 +118,11 @@ 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.setmmPrefs(sharedPreferences);
+
+ 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"));
}
});
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 5c0da4c..e778a2b 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,31 +12,42 @@
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
-import android.os.*;
+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.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.*;
+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.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 {
@@ -71,10 +82,7 @@ public static void initForStubActivity(Context ctx) {
mInstrumentation.setAccessible(true);
Instrumentation instrumentation = (Instrumentation) mInstrumentation.get(sCurrentActivityThread);
if (!(instrumentation instanceof ProxyInstrumentation)) {
- // 创建代理对象
- ProxyInstrumentation proxy = new ProxyInstrumentation(instrumentation);
- // 替换掉系统的实例
- mInstrumentation.set(sCurrentActivityThread, proxy);
+ mInstrumentation.set(sCurrentActivityThread, new ProxyInstrumentation(instrumentation));
}
// Hook Handler (mH)
@@ -111,7 +119,6 @@ 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);
@@ -126,8 +133,7 @@ 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) {
@@ -165,7 +171,6 @@ 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");
@@ -212,7 +217,8 @@ 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]);
@@ -423,7 +429,8 @@ private Intent tryRecoverIntent(Intent intent) {
*/
@SneakyThrows
@Override
- public Activity newActivity(ClassLoader cl, String className, Intent intent) {
+ public Activity newActivity(ClassLoader cl, String className, Intent intent)
+ throws InstantiationException, IllegalAccessException, ClassNotFoundException {
// 兜底:如果 intent 仍然是 stub 的 wrapper,尝试还原
Intent recovered = tryRecoverIntent(intent);
@@ -438,7 +445,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).getDeclaredConstructor().newInstance();
+ return (Activity) moduleCL.loadClass(className).newInstance();
}
throw e;
}
@@ -464,11 +471,10 @@ public void callActivityOnCreate(Activity activity, Bundle icicle) {
ClassLoader hybridCL = ParcelableFixer.getHybridClassLoader();
if (hybridCL != null) {
try {
- @SuppressWarnings("JavaReflectionMemberAccess") Field f = Activity.class.getDeclaredField("mClassLoader");
+ 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) {
@@ -756,7 +762,7 @@ public void callActivityOnSaveInstanceState(@NonNull Activity activity, @NonNull
}
@Override
- public void callActivityOnSaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState, @NonNull PersistableBundle outPersistentState) {
+ public void callActivityOnSaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState, PersistableBundle outPersistentState) {
mBase.callActivityOnSaveInstanceState(activity, outState, outPersistentState);
}
@@ -770,15 +776,11 @@ 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();
@@ -885,11 +887,7 @@ 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 de37de8..16cac78 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)
- dexKit.use { dexKit ->
+ try {
// 创建进度更新 Channel
val progressChannel = Channel(Channel.UNLIMITED)
@@ -187,6 +187,8 @@ 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
deleted file mode 100644
index e17e5cf..0000000
--- a/app/src/main/java/moe/ouom/wekit/ui/creator/dialog/ScriptManagerDialog.kt
+++ /dev/null
@@ -1,611 +0,0 @@
-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 ce0227f..5086ee5 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,14 +4,13 @@ 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.*
+import java.util.Locale
+import java.util.ResourceBundle
+import java.util.MissingResourceException
import javax.script.ScriptEngine
import javax.script.ScriptEngineManager
@@ -124,6 +123,8 @@ class JsExecutor private constructor() {
val rhinoEngine = engineManager.getEngineByName("rhino")
mScriptEngine = rhinoEngine
+ // 注入日志接口
+ injectLoggingInterface()
mInitialized = true
WeLogger.i("JsExecutor initialized with Rhino")
@@ -164,53 +165,17 @@ class JsExecutor private constructor() {
}
/**
- * 注入脚本接口
- * @param sendCgi CGI发送函数
- * @param protoUtils 协议工具对象
- * @param dataBaseUtils 数据库工具对象
- * @param messageUtils 消息工具对象
+ * 注入日志接口
*/
@Suppress("unused")
- fun injectScriptInterfaces(
- sendCgi: Any,
- weApiUtils: Any,
- protoUtils: Any,
- dataBaseUtils: Any,
- messageUtils: Any
- ) {
+ private fun injectLoggingInterface() {
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
deleted file mode 100644
index b253108..0000000
--- a/app/src/main/res/drawable/edit_24px.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/app/src/main/res/layout/dialog_script_edit.xml b/app/src/main/res/layout/dialog_script_edit.xml
deleted file mode 100644
index 022ba8c..0000000
--- a/app/src/main/res/layout/dialog_script_edit.xml
+++ /dev/null
@@ -1,93 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ 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
deleted file mode 100644
index 7d82462..0000000
--- a/app/src/main/res/layout/dialog_test_script.xml
+++ /dev/null
@@ -1,59 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ 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
deleted file mode 100644
index 2ee56b1..0000000
--- a/app/src/main/res/layout/script_item.xml
+++ /dev/null
@@ -1,92 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/generate_secrets_h.py b/generate_secrets_h.py
deleted file mode 100644
index eaf6083..0000000
--- a/generate_secrets_h.py
+++ /dev/null
@@ -1,76 +0,0 @@
-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()