From c326053bebe9f4af49353f6d572ac286b406c4cb Mon Sep 17 00:00:00 2001 From: INTENTION Date: Sun, 22 Feb 2026 14:34:16 +0800 Subject: [PATCH 1/7] =?UTF-8?q?Revert=20"=E6=96=B0=E5=A2=9E=E9=83=A8?= =?UTF-8?q?=E5=88=86=E5=8A=9F=E8=83=BD"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 23ccf50e25572689370fff7146a8182a25949a0e. --- .../wekit/hooks/item/chat/msg/AntiFoldMsg.kt | 48 ---- .../item/chat/msg/ModifyMessageDisplayHook.kt | 80 ------- .../ouom/wekit/hooks/item/chat/msg/MsgType.kt | 69 ------ .../hooks/item/contact/ShowWeChatIdHook.kt | 87 -------- .../wekit/hooks/item/fix/ForceCameraScan.kt | 61 ------ .../hooks/item/fix/WeChatArticleAdRemover.kt | 90 -------- .../hooks/item/func/WeAvatarTransparent.kt | 50 ----- .../ouom/wekit/hooks/item/moment/AntiSnsAd.kt | 35 --- .../hooks/item/moment/AntiSnsDeleteHook.kt | 98 --------- .../wekit/hooks/item/moment/SnsContentType.kt | 46 ---- .../hooks/item/moment/SnsLikeModifyHook.kt | 205 ------------------ app/src/main/res/drawable/edit_24px.xml | 10 - 12 files changed, 879 deletions(-) delete mode 100644 app/src/main/java/moe/ouom/wekit/hooks/item/chat/msg/AntiFoldMsg.kt delete mode 100644 app/src/main/java/moe/ouom/wekit/hooks/item/chat/msg/ModifyMessageDisplayHook.kt delete mode 100644 app/src/main/java/moe/ouom/wekit/hooks/item/chat/msg/MsgType.kt delete mode 100644 app/src/main/java/moe/ouom/wekit/hooks/item/contact/ShowWeChatIdHook.kt delete mode 100644 app/src/main/java/moe/ouom/wekit/hooks/item/fix/ForceCameraScan.kt delete mode 100644 app/src/main/java/moe/ouom/wekit/hooks/item/fix/WeChatArticleAdRemover.kt delete mode 100644 app/src/main/java/moe/ouom/wekit/hooks/item/func/WeAvatarTransparent.kt delete mode 100644 app/src/main/java/moe/ouom/wekit/hooks/item/moment/AntiSnsAd.kt delete mode 100644 app/src/main/java/moe/ouom/wekit/hooks/item/moment/AntiSnsDeleteHook.kt delete mode 100644 app/src/main/java/moe/ouom/wekit/hooks/item/moment/SnsContentType.kt delete mode 100644 app/src/main/java/moe/ouom/wekit/hooks/item/moment/SnsLikeModifyHook.kt delete mode 100644 app/src/main/res/drawable/edit_24px.xml 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/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/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/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 @@ - - - From 33d98f8469a07861088f6b9d2d963e04ec6e93d7 Mon Sep 17 00:00:00 2001 From: INTENTION Date: Sun, 22 Feb 2026 14:34:16 +0800 Subject: [PATCH 2/7] =?UTF-8?q?Revert=20"=E6=96=B0=E5=A2=9E=E9=83=A8?= =?UTF-8?q?=E5=88=86ApiHookItem=20=E6=96=B0=E5=A2=9E=E9=83=A8=E5=88=86?= =?UTF-8?q?=E8=84=9A=E6=9C=AC=E6=8E=A5=E5=8F=A3"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit f838ef030ae4c214e8e60acd6e10434872293f5b. --- SCRIPT_API_DOCUMENT.md | 120 +++-------- .../hooks/item/script/ScriptConfigHookItem.kt | 2 +- .../wekit/hooks/item/script/WeApiUtils.kt | 28 --- .../wekit/hooks/item/script/WeMessageUtils.kt | 13 ++ .../hooks/sdk/ui/WeChatChatContextMenuApi.kt | 170 ---------------- .../ui/WeChatContactInfoAdapterItemHook.kt | 184 ----------------- .../hooks/sdk/ui/WeChatSnsContextMenuApi.kt | 191 ------------------ .../moe/ouom/wekit/util/script/JsExecutor.kt | 14 -- 8 files changed, 37 insertions(+), 685 deletions(-) delete mode 100644 app/src/main/java/moe/ouom/wekit/hooks/item/script/WeApiUtils.kt delete mode 100644 app/src/main/java/moe/ouom/wekit/hooks/sdk/ui/WeChatChatContextMenuApi.kt delete mode 100644 app/src/main/java/moe/ouom/wekit/hooks/sdk/ui/WeChatContactInfoAdapterItemHook.kt delete mode 100644 app/src/main/java/moe/ouom/wekit/hooks/sdk/ui/WeChatSnsContextMenuApi.kt diff --git a/SCRIPT_API_DOCUMENT.md b/SCRIPT_API_DOCUMENT.md index a158f1a..ef5889b 100644 --- a/SCRIPT_API_DOCUMENT.md +++ b/SCRIPT_API_DOCUMENT.md @@ -16,10 +16,7 @@ - [概述](#概述) - [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-对象) @@ -138,54 +135,6 @@ function onRequest(data) { } ``` -### 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 请求函数。 @@ -215,52 +164,6 @@ function onRequest(data) { } ``` -#### 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 数据的工具对象。 @@ -727,6 +630,29 @@ function onRequest(data) { } ``` +#### wekit.message.getSelfAlias + +获取当前用户的微信号。 + +```javascript +wekit.message.getSelfAlias(); +``` + +##### 返回值 + +| 类型 | 描述 | +| ------ | ---------------- | +| string | 当前用户的微信号 | + +##### 示例 + +```javascript +function onRequest(data) { + const alias = wekit.message.getSelfAlias(); + wekit.log('当前用户微信号:', alias); +} +``` + ## 注意事项 1. 日志输出将显示在脚本日志查看器中 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..c4b7e2b 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 @@ -31,7 +31,7 @@ class ScriptConfigHookItem : BaseClickableFunctionHookItem(), IWePkgInterceptor override fun entry(classLoader: ClassLoader) { // 注入脚本接口 - JsExecutor.getInstance().injectScriptInterfaces(::sendCgi, WeApiUtils, WeProtoUtils, WeDataBaseUtils, WeMessageUtils) + JsExecutor.getInstance().injectScriptInterfaces(::sendCgi, WeProtoUtils, WeDataBaseUtils, WeMessageUtils) // 注册拦截器 WePkgManager.addInterceptor(this) } 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/WeMessageUtils.kt b/app/src/main/java/moe/ouom/wekit/hooks/item/script/WeMessageUtils.kt index a380558..8904b2a 100644 --- 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 @@ -93,4 +93,17 @@ object WeMessageUtils { false } } + + /** + * 获取当前用户ID + * @return 当前用户ID + */ + fun getSelfAlias(): String { + return try { + instance?.getSelfAlias() ?: "" + } catch (e: Exception) { + WeLogger.e(TAG, "获取当前用户ID失败: ${e.message}") + "" + } + } } \ No newline at end of file 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/util/script/JsExecutor.kt b/app/src/main/java/moe/ouom/wekit/util/script/JsExecutor.kt index ce0227f..a247bc5 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 @@ -5,7 +5,6 @@ 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 @@ -173,7 +172,6 @@ class JsExecutor private constructor() { @Suppress("unused") fun injectScriptInterfaces( sendCgi: Any, - weApiUtils: Any, protoUtils: Any, dataBaseUtils: Any, messageUtils: Any @@ -189,26 +187,14 @@ class JsExecutor private constructor() { 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 }) From 3dd20fca576230c47bce1422569cba7ad9d2dfe6 Mon Sep 17 00:00:00 2001 From: INTENTION Date: Sun, 22 Feb 2026 14:34:16 +0800 Subject: [PATCH 3/7] =?UTF-8?q?Revert=20"=E4=BC=98=E5=8C=96=E9=83=A8?= =?UTF-8?q?=E5=88=86=E4=BB=A3=E7=A0=81=20=E5=B0=86=E5=A8=B1=E4=B9=90?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E4=BB=8Efun=E7=A7=BB=E5=8A=A8=E5=88=B0func?= =?UTF-8?q?=20=E6=96=B0=E5=A2=9E=E5=88=A4=E6=96=AD=E8=B0=B7=E6=AD=8C?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E7=9A=84=E6=96=B9=E6=B3=95"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 120687731832426d849a0703ab01d6eba753fe6c. --- CONTRIBUTING.md | 70 +---- .../moe/ouom/wekit/constants/Constants.kt | 1 - .../moe/ouom/wekit/core/dsl/DexDelegate.kt | 2 +- .../item/chat/risk/HookQueryCashierPkg.kt | 2 +- .../hooks/item/chat/risk/WeRedPacketAuto.kt | 2 +- .../wekit/hooks/item/chat/risk/WeSendXml.kt | 18 ++ .../wekit/hooks/item/dev/WePacketDebugger.kt | 2 + .../item/{func => dev}/WeProfileCleaner.kt | 3 +- .../item/{func => dev}/WeProfileNameSetter.kt | 3 +- .../ouom/wekit/hooks/sdk/api/WeDatabaseApi.kt | 196 +++++-------- .../wekit/hooks/sdk/api/WeDatabaseListener.kt | 272 +++--------------- .../ouom/wekit/hooks/sdk/api/WeMessageApi.kt | 7 +- .../sdk/protocol/listener/WePkgDispatcher.kt | 6 +- .../java/moe/ouom/wekit/host/HostInfo.java | 8 +- .../java/moe/ouom/wekit/host/impl/HostInfo.kt | 6 - .../loader/core/hooks/ActivityProxyHooks.java | 62 ++-- .../ui/creator/center/DexFinderDialog.kt | 4 +- 17 files changed, 187 insertions(+), 477 deletions(-) rename app/src/main/java/moe/ouom/wekit/hooks/item/{func => dev}/WeProfileCleaner.kt (96%) rename app/src/main/java/moe/ouom/wekit/hooks/item/{func => dev}/WeProfileNameSetter.kt (97%) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ecd8c37..c2e1595 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 数据的核心工具类,提供以下关键方法: 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/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/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/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/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/listener/WePkgDispatcher.kt b/app/src/main/java/moe/ouom/wekit/hooks/sdk/protocol/listener/WePkgDispatcher.kt index a3107c0..3df774a 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 @@ -17,7 +17,7 @@ import kotlin.math.min @HookItem(path = "protocol/wepkg_dispatcher", desc = "WePkg 请求/响应数据包拦截与篡改") class WePkgDispatcher : ApiHookItem(), IDexFind { private val dexClsOnGYNetEnd by dexClass() - // 缓存最近10条记录,避免因脚本引起的无限递归 + // 缓存最近10条记录,避免无限递归 private val recentRequests = ConcurrentHashMap() override fun entry(classLoader: ClassLoader) { @@ -46,10 +46,10 @@ class WePkgDispatcher : ApiHookItem(), IDexFind { // 构造唯一标识符 val key = "$cgiId|$uri|${reqWrapper?.javaClass?.name}|${reqPbObj?.javaClass?.name}|${reqBytes.contentToString()}" - // 检查是否在缓存中且时间间隔小于500毫秒 + // 检查是否在缓存中且时间间隔小于2秒 val currentTime = System.currentTimeMillis() val lastTime = recentRequests[key] - if (lastTime != null && currentTime - lastTime < 500) { + if (lastTime != null && currentTime - lastTime < 2000) { // 直接返回,不执行任何请求处理 WeLogger.i("PkgDispatcher", "Request skipped (duplicate): $uri") return@hookBefore 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/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() } } From 56c2d624c0351fdccf1816dea232de5963ed6063 Mon Sep 17 00:00:00 2001 From: INTENTION Date: Sun, 22 Feb 2026 14:34:16 +0800 Subject: [PATCH 4/7] =?UTF-8?q?Revert=20"=E6=9B=B4=E6=96=B0RuntimeConfig?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E5=8A=A8=E6=80=81=E4=BB=8Eprefs=E8=AF=BB?= =?UTF-8?q?=E5=8F=96"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 465c91b511981fb52d9ab3d724b80a266f2f2bc1. --- .../moe/ouom/wekit/config/RuntimeConfig.java | 41 ++++++++++++++----- .../moe/ouom/wekit/constants/MMVersion.kt | 9 +--- .../ouom/wekit/hooks/sdk/protocol/WeApi.kt | 8 ++++ .../ouom/wekit/loader/core/WeLauncher.java | 6 ++- 4 files changed, 44 insertions(+), 20 deletions(-) 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/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/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/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")); } }); From 3cc6dcd3a3115735bfc2e5ed65df80ae58eda877 Mon Sep 17 00:00:00 2001 From: INTENTION Date: Sun, 22 Feb 2026 14:34:16 +0800 Subject: [PATCH 5/7] Revert "Add cache mechanism to prevent interceptor infinite recursion" This reverts commit cefc2aab25d4005883df544b8b7c7fa028a478bc. --- .../sdk/protocol/listener/WePkgDispatcher.kt | 25 ------------------- 1 file changed, 25 deletions(-) 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 3df774a..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()}" - // 检查是否在缓存中且时间间隔小于2秒 - val currentTime = System.currentTimeMillis() - val lastTime = recentRequests[key] - if (lastTime != null && currentTime - lastTime < 2000) { - // 直接返回,不执行任何请求处理 - 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") From 7b56685f1ffc64f7f7775f7288d410f21ca8f670 Mon Sep 17 00:00:00 2001 From: INTENTION Date: Sun, 22 Feb 2026 14:34:16 +0800 Subject: [PATCH 6/7] Revert "Added Python script to generate signature header files" This reverts commit 21fcffceea9466fc4d3c7b14dffb0131e1e263d9. --- generate_secrets_h.py | 76 ------------------------------------------- 1 file changed, 76 deletions(-) delete mode 100644 generate_secrets_h.py 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() From 0f9799e6c633dcd444d64058b6930a7a349e16dd Mon Sep 17 00:00:00 2001 From: INTENTION Date: Sun, 22 Feb 2026 14:34:16 +0800 Subject: [PATCH 7/7] Revert "Fixed script manager crash on click, added more script APIs, and added release testing methods" This reverts commit 231f0e5bdbe972ae48ef44769c285c023a313563. --- .github/PULL_REQUEST_TEMPLATE.md | 1 - CONTRIBUTING.md | 23 - SCRIPT_API_DOCUMENT.md | 591 +------------- .../hooks/item/script/ScriptConfigHookItem.kt | 743 +++++++++++++++++- .../hooks/item/script/WeDataBaseUtils.kt | 160 ---- .../wekit/hooks/item/script/WeMessageUtils.kt | 109 --- .../wekit/hooks/item/script/WeProtoUtils.kt | 35 - .../ui/creator/dialog/ScriptManagerDialog.kt | 611 -------------- .../moe/ouom/wekit/util/script/JsExecutor.kt | 37 +- .../main/res/layout/dialog_script_edit.xml | 93 --- .../main/res/layout/dialog_test_script.xml | 59 -- app/src/main/res/layout/script_item.xml | 92 --- 12 files changed, 747 insertions(+), 1807 deletions(-) delete mode 100644 app/src/main/java/moe/ouom/wekit/hooks/item/script/WeDataBaseUtils.kt delete mode 100644 app/src/main/java/moe/ouom/wekit/hooks/item/script/WeMessageUtils.kt delete mode 100644 app/src/main/java/moe/ouom/wekit/hooks/item/script/WeProtoUtils.kt delete mode 100644 app/src/main/java/moe/ouom/wekit/ui/creator/dialog/ScriptManagerDialog.kt delete mode 100644 app/src/main/res/layout/dialog_script_edit.xml delete mode 100644 app/src/main/res/layout/dialog_test_script.xml delete mode 100644 app/src/main/res/layout/script_item.xml 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 c2e1595..be677dd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2969,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 @@ -3027,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 ef5889b..5ad4df1 100644 --- a/SCRIPT_API_DOCUMENT.md +++ b/SCRIPT_API_DOCUMENT.md @@ -3,25 +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 sendCgi 函数](#wekit-sendcgi-函数) - - [WEKit proto 对象](#wekit-proto-对象) - - [WEKit database 对象](#wekit-database-对象) - - [WEKit message 对象](#wekit-message-对象) -3. [注意事项](#注意事项) -4. [后续更新计划](#后续更新计划) + - [概述](#概述) + - [WEKit Log 函数](#wekit-log-函数) ## 钩子函数 @@ -57,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 对象 ### 概述 @@ -101,565 +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 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 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应用消息发送失败'); - } -} -``` - -#### wekit.message.getSelfAlias - -获取当前用户的微信号。 - -```javascript -wekit.message.getSelfAlias(); -``` - -##### 返回值 - -| 类型 | 描述 | -| ------ | ---------------- | -| string | 当前用户的微信号 | - -##### 示例 - -```javascript -function onRequest(data) { - const alias = wekit.message.getSelfAlias(); - wekit.log('当前用户微信号:', alias); -} -``` - -## 注意事项 +### 注意事项 1. 日志输出将显示在脚本日志查看器中 2. 当 API 发生变更时,请及时更新相关脚本代码 -3. 所有工具方法都应在脚本环境中可用 -4. 钩子函数中修改数据时,请遵循返回值规则,不要直接修改传入参数 -## 后续更新计划 +### 后续更新计划 我们可能在稳定版本中提供更加完善和一致的日志功能接口,届时会提供更详细的文档和更稳定的 API 设计。 \ 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 c4b7e2b..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, 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/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 8904b2a..0000000 --- a/app/src/main/java/moe/ouom/wekit/hooks/item/script/WeMessageUtils.kt +++ /dev/null @@ -1,109 +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 - } - } - - /** - * 获取当前用户ID - * @return 当前用户ID - */ - fun getSelfAlias(): String { - return try { - instance?.getSelfAlias() ?: "" - } catch (e: Exception) { - WeLogger.e(TAG, "获取当前用户ID失败: ${e.message}") - "" - } - } -} \ 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/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 a247bc5..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,13 +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.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 @@ -123,6 +123,8 @@ class JsExecutor private constructor() { val rhinoEngine = engineManager.getEngineByName("rhino") mScriptEngine = rhinoEngine + // 注入日志接口 + injectLoggingInterface() mInitialized = true WeLogger.i("JsExecutor initialized with Rhino") @@ -163,40 +165,17 @@ class JsExecutor private constructor() { } /** - * 注入脚本接口 - * @param sendCgi CGI发送函数 - * @param protoUtils 协议工具对象 - * @param dataBaseUtils 数据库工具对象 - * @param messageUtils 消息工具对象 + * 注入日志接口 */ @Suppress("unused") - fun injectScriptInterfaces( - sendCgi: 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 sendCgi(uri: String, cgiId: Int, funcId: Int, routeId: Int, jsonPayload: String) { - sendCgi(uri, cgiId, funcId, routeId, jsonPayload) - } - - @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/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