From b8a230c99aa7f35de3e0ad61566ab922e5263d61 Mon Sep 17 00:00:00 2001 From: huajiqaq Date: Tue, 10 Feb 2026 17:44:33 +0800 Subject: [PATCH 1/4] Add native init state and null-safe URI handling Introduce native core initialization state (isNativeCoreInitialized / setNativeCoreInitialized) in NativeCoreBridge and use it when initializing MMKV to better track / log init status. Adjust initializeMmkvForPrimaryNativeLibrary to use the new API and set the flag via setter. Update LogUtils to consult the native init state before gating logs, preventing premature log suppression. Make WePkgDispatcher null-safe for getUri to avoid NPEs, and add a comment in MmkvConfigManagerImpl highlighting that MMKV must be initialized before use. --- .../wekit/config/MmkvConfigManagerImpl.java | 1 + .../sdk/protocol/listener/WePkgDispatcher.kt | 3 +- .../wekit/loader/core/NativeCoreBridge.java | 90 +++++++++---------- .../moe/ouom/wekit/util/log/LogUtils.java | 3 +- 4 files changed, 46 insertions(+), 51 deletions(-) diff --git a/app/src/main/java/moe/ouom/wekit/config/MmkvConfigManagerImpl.java b/app/src/main/java/moe/ouom/wekit/config/MmkvConfigManagerImpl.java index 7642600..287231f 100644 --- a/app/src/main/java/moe/ouom/wekit/config/MmkvConfigManagerImpl.java +++ b/app/src/main/java/moe/ouom/wekit/config/MmkvConfigManagerImpl.java @@ -368,6 +368,7 @@ public Set> entrySet() { protected MmkvConfigManagerImpl(@NonNull String name) { mmkvId = Objects.requireNonNull(name, "name"); + // 调用前需要等待模块mmkv初始化完毕 mmkv = MMKV.mmkvWithID(name, MMKV.MULTI_PROCESS_MODE); file = new File(MMKV.getRootDir(), name); } 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 c79a5fb..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 @@ -32,7 +32,8 @@ class WePkgDispatcher : ApiHookItem(), IDexFind { val v0Var = param.args[1] ?: return@hookBefore val originalCallback = param.args[2] ?: return@hookBefore - val uri = XposedHelpers.callMethod(v0Var, "getUri") as String + // 有时 getUri 返回 null + val uri = (XposedHelpers.callMethod(v0Var, "getUri") ?: "null") as String val cgiId = XposedHelpers.callMethod(v0Var, "getType") as Int try { val reqWrapper = XposedHelpers.callMethod(v0Var, "getReqObj") diff --git a/app/src/main/java/moe/ouom/wekit/loader/core/NativeCoreBridge.java b/app/src/main/java/moe/ouom/wekit/loader/core/NativeCoreBridge.java index 4b04803..6bae59c 100644 --- a/app/src/main/java/moe/ouom/wekit/loader/core/NativeCoreBridge.java +++ b/app/src/main/java/moe/ouom/wekit/loader/core/NativeCoreBridge.java @@ -1,22 +1,17 @@ package moe.ouom.wekit.loader.core; -import static moe.ouom.wekit.util.io.FileUtils.copyFile; - import android.annotation.SuppressLint; import android.content.Context; import androidx.annotation.NonNull; import com.tencent.mmkv.MMKV; import java.io.File; -import java.io.IOException; import moe.ouom.wekit.host.HostInfo; -import moe.ouom.wekit.util.io.FileUtils; import moe.ouom.wekit.util.log.WeLogger; public class NativeCoreBridge { static { - // it will be an UnsatisfiedLinkError if first load.. System.loadLibrary("dexkit"); System.loadLibrary("wekit"); } @@ -31,69 +26,66 @@ private NativeCoreBridge() { public static void initNativeCore() { Context context = HostInfo.getApplication(); - // init mmkv initializeMmkvForPrimaryNativeLibrary(context); } /** - * Load native library and initialize MMKV + * 检查本地核心库是否已初始化 + * @return true 如果已成功初始化,false 如果未初始化 + */ + public static boolean isNativeCoreInitialized() { + return sPrimaryNativeLibraryInitialized; + } + + /** + * 设置本地核心库初始化状态 + * @param initialized true表示已初始化,false表示未初始化 + */ + public static void setNativeCoreInitialized(boolean initialized) { + sPrimaryNativeLibraryInitialized = initialized; + if (initialized) { + WeLogger.i("Native core initialization status set to: initialized"); + } else { + WeLogger.w("Native core initialization status set to: not initialized"); + } + } + + /** + * 加载本地库并初始化MMKV * - * @param ctx Application context - * @throws LinkageError if failed to load native library + * @param ctx 应用上下文 */ @SuppressLint("SdCardPath") public static void initializeMmkvForPrimaryNativeLibrary(@NonNull Context ctx) { - if (sPrimaryNativeLibraryInitialized) { + if (isNativeCoreInitialized()) { return; } - File filesDir = null; - - File[] externalDirs = ctx.getExternalMediaDirs(); + // 获取微信的files目录 + File appFilesDir = ctx.getFilesDir(); + String packageName = ctx.getPackageName(); - if (externalDirs != null && externalDirs.length > 0) { - filesDir = externalDirs[0]; - } - - if (filesDir == null) { - filesDir = ctx.getFilesDir(); - } + WeLogger.i("Initializing NativeCoreBridge for package: " + packageName); - File mmkvDir = new File(filesDir, "wekit_mmkv"); + File mmkvDir = new File(appFilesDir, "mmkv"); + // 不存在就创建mmkv目录 if (!mmkvDir.exists()) { - mmkvDir.mkdirs(); + boolean created = mmkvDir.mkdirs(); + WeLogger.i("Created mmkv directory: " + created); } - // MMKV requires a ".tmp" cache directory, we have to create it manually - File cacheDir = new File(mmkvDir, ".tmp"); - if (!cacheDir.exists()) { - cacheDir.mkdir(); - } + // 初始化 MMKV + String mmkvRootPath = mmkvDir.getAbsolutePath(); + String initializedPath = MMKV.initialize(ctx, mmkvRootPath); - File oldDir = new File(ctx.getFilesDir(), "wekit_mmkv"); - if (oldDir.exists() && oldDir.isDirectory()) { - File[] files = oldDir.listFiles(); - if (files != null) { - for (File src : files) { - if (!src.isFile()) continue; - File dest = new File(mmkvDir, src.getName()); - if (!dest.exists()) { - try { - copyFile(src, dest); - WeLogger.i("Copy config file: " + src.getName()); - } catch (IOException e) { - WeLogger.e(e); - } - } - } - } - FileUtils.deleteFile(oldDir); - } - MMKV.initialize(ctx, "/data/data/com.tencent.mm/files/mmkv"); + WeLogger.i("MMKV initialized at: " + initializedPath); + + // 创建必要的 MMKV 实例 MMKV.mmkvWithID("global_config", MMKV.MULTI_PROCESS_MODE); MMKV.mmkvWithID("global_cache", MMKV.MULTI_PROCESS_MODE); - sPrimaryNativeLibraryInitialized = true; - } + setNativeCoreInitialized(true); + WeLogger.i("NativeCoreBridge initialization complete"); + } } diff --git a/app/src/main/java/moe/ouom/wekit/util/log/LogUtils.java b/app/src/main/java/moe/ouom/wekit/util/log/LogUtils.java index c4a3eda..a10c90d 100644 --- a/app/src/main/java/moe/ouom/wekit/util/log/LogUtils.java +++ b/app/src/main/java/moe/ouom/wekit/util/log/LogUtils.java @@ -10,6 +10,7 @@ import de.robv.android.xposed.XposedBridge; import moe.ouom.wekit.config.WeConfig; +import moe.ouom.wekit.loader.core.NativeCoreBridge; import moe.ouom.wekit.util.io.FileUtils; import moe.ouom.wekit.util.io.PathTool; @@ -99,7 +100,7 @@ public static void addError(String TAG, String Description, Throwable e) { private static void addLog(String fileName, String Description, Object content, boolean isError) { try { - if (!WeConfig.getDefaultConfig().getBooleanOrFalse(PrekEnableLog)){ + if (NativeCoreBridge.isNativeCoreInitialized() && !WeConfig.getDefaultConfig().getBooleanOrFalse(PrekEnableLog)){ return; } } catch (Exception e) { From 2c0a2ced4961cbd4ebba74b19083dd13bbe3fd94 Mon Sep 17 00:00:00 2001 From: huajiqaq Date: Thu, 12 Feb 2026 16:43:41 +0800 Subject: [PATCH 2/4] allow entry to be optional, enforce initOnce --- CONTRIBUTING.md | 8 +++++ .../moe/ouom/wekit/core/model/BaseHookItem.kt | 7 ++-- .../wekit/hooks/item/dev/DexCacheCleaner.kt | 7 ++-- .../wekit/hooks/item/dev/WePacketDebugger.kt | 5 +-- .../wekit/hooks/item/dev/WeProfileCleaner.kt | 5 +-- .../hooks/item/dev/WeProfileNameSetter.kt | 5 +-- .../hooks/item/dev/WeSplitChatroomMaker.kt | 6 +--- .../wekit/hooks/item/fix/CrashLogViewer.kt | 32 +++---------------- 8 files changed, 21 insertions(+), 54 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 623a395..be677dd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1182,9 +1182,12 @@ import moe.ouom.wekit.hooks.core.annotation.HookItem ) class DexCacheCleaner : BaseClickableFunctionHookItem() { + // 如果重写noSwitchWidget为true时将永远不会调用entry 此时可不重写entry方法来触发功能 通过onClick触发 + /* override fun entry(classLoader: ClassLoader) { // 可点击功能不需要 Hook,只需实现 onClick } + */ override fun onClick() { // 清理缓存 @@ -1195,6 +1198,8 @@ class DexCacheCleaner : BaseClickableFunctionHookItem() { WeLogger.i("DexCacheCleaner", "DEX 缓存已清理") } + + override fun noSwitchWidget(): Boolean = true } ``` @@ -1653,6 +1658,8 @@ class AutoGrabRedPacketConfigDialog(context: Context) : BaseRikkaDialog(context, | **点击处理** | 点击切换开关 | **`onClick(Context)` 必须重写** | | **主要用途** | 主要用于 Hook 功能 | 主要用于需要点击交互的功能 | +**如果重写noSwitchWidget为true将不会调用entry 请手动在onClick实现** + ### 功能放置位置 根据功能类型放置到对应的包中: @@ -2210,6 +2217,7 @@ class MyPacketInterceptor : IWePkgInterceptor { **步骤 2: 注册拦截器** +请确保该项目未重写noSwitchWidget为true 否则不会触发 `entry()` 方法 在 Hook 入口点(通常是 `entry()` 方法)中注册拦截器: ```kotlin diff --git a/app/src/main/java/moe/ouom/wekit/core/model/BaseHookItem.kt b/app/src/main/java/moe/ouom/wekit/core/model/BaseHookItem.kt index 85bee73..0c007d2 100644 --- a/app/src/main/java/moe/ouom/wekit/core/model/BaseHookItem.kt +++ b/app/src/main/java/moe/ouom/wekit/core/model/BaseHookItem.kt @@ -107,14 +107,13 @@ abstract class BaseHookItem { /** * 在 loadHook 前执行一次 * 返回 true 表示继续执行 loadHook - * 返回 false 表示由 initOnce 自行处理 loadHook 事件 + * 返回 false 表示不执行 entry 的事件 不可重写 */ - open fun initOnce(): Boolean = true - + fun initOnce(): Boolean = true /** * Hook 入口方法 */ - abstract fun entry(classLoader: ClassLoader) + open fun entry(classLoader: ClassLoader) {} /** * 卸载 Hook diff --git a/app/src/main/java/moe/ouom/wekit/hooks/item/dev/DexCacheCleaner.kt b/app/src/main/java/moe/ouom/wekit/hooks/item/dev/DexCacheCleaner.kt index 0497b39..c7dcddf 100644 --- a/app/src/main/java/moe/ouom/wekit/hooks/item/dev/DexCacheCleaner.kt +++ b/app/src/main/java/moe/ouom/wekit/hooks/item/dev/DexCacheCleaner.kt @@ -8,8 +8,7 @@ import moe.ouom.wekit.hooks.core.annotation.HookItem @HookItem(path = "开发者选项/清除适配信息", desc = "点击清除适配信息") class DexCacheCleaner : BaseClickableFunctionHookItem() { - override fun entry(classLoader: ClassLoader) {} - + override fun onClick(context: Context?) { context?.let { MaterialDialog(it) @@ -25,7 +24,5 @@ class DexCacheCleaner : BaseClickableFunctionHookItem() { } } - override fun noSwitchWidget(): Boolean { - return true - } + override fun noSwitchWidget(): Boolean = true } \ No newline at end of file diff --git a/app/src/main/java/moe/ouom/wekit/hooks/item/dev/WePacketDebugger.kt b/app/src/main/java/moe/ouom/wekit/hooks/item/dev/WePacketDebugger.kt index 39baca4..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 @@ -15,7 +15,6 @@ import moe.ouom.wekit.util.log.WeLogger @HookItem(path = "开发者选项/发包调试", desc = "发送自定义数据包到微信服务器") class WePacketDebugger : BaseClickableFunctionHookItem() { - override fun entry(classLoader: ClassLoader) {} override fun onClick(context: Context?) { context?.let { @@ -96,7 +95,5 @@ class WePacketDebugger : BaseClickableFunctionHookItem() { } } - override fun noSwitchWidget(): Boolean { - return true - } + override fun noSwitchWidget(): Boolean = true } \ No newline at end of file diff --git a/app/src/main/java/moe/ouom/wekit/hooks/item/dev/WeProfileCleaner.kt b/app/src/main/java/moe/ouom/wekit/hooks/item/dev/WeProfileCleaner.kt index 1e75ed3..17de24a 100644 --- a/app/src/main/java/moe/ouom/wekit/hooks/item/dev/WeProfileCleaner.kt +++ b/app/src/main/java/moe/ouom/wekit/hooks/item/dev/WeProfileCleaner.kt @@ -10,7 +10,6 @@ import moe.ouom.wekit.util.log.WeLogger @HookItem(path = "娱乐功能/清空资料信息", desc = "点击清空你之前所选择的微信地区和性别等资料信息") class WeProfileCleaner : BaseClickableFunctionHookItem() { - override fun entry(classLoader: ClassLoader) {} override fun onClick(context: Context?) { context?.let { @@ -54,7 +53,5 @@ class WeProfileCleaner : BaseClickableFunctionHookItem() { } } - override fun noSwitchWidget(): Boolean { - return true - } + override fun noSwitchWidget(): Boolean = true } \ No newline at end of file diff --git a/app/src/main/java/moe/ouom/wekit/hooks/item/dev/WeProfileNameSetter.kt b/app/src/main/java/moe/ouom/wekit/hooks/item/dev/WeProfileNameSetter.kt index dc97464..0de93da 100644 --- a/app/src/main/java/moe/ouom/wekit/hooks/item/dev/WeProfileNameSetter.kt +++ b/app/src/main/java/moe/ouom/wekit/hooks/item/dev/WeProfileNameSetter.kt @@ -11,7 +11,6 @@ import moe.ouom.wekit.util.log.WeLogger @HookItem(path = "娱乐功能/设置微信昵称", desc = "通过发包来更灵活的设置微信昵称") class WeProfileNameSetter : BaseClickableFunctionHookItem() { - override fun entry(classLoader: ClassLoader) {} override fun onClick(context: Context?) { context?.let { @@ -65,7 +64,5 @@ class WeProfileNameSetter : BaseClickableFunctionHookItem() { return input.replace("\"", "\\\"") } - override fun noSwitchWidget(): Boolean { - return true - } + override fun noSwitchWidget(): Boolean = true } \ No newline at end of file diff --git a/app/src/main/java/moe/ouom/wekit/hooks/item/dev/WeSplitChatroomMaker.kt b/app/src/main/java/moe/ouom/wekit/hooks/item/dev/WeSplitChatroomMaker.kt index 1a360ac..d327f8e 100644 --- a/app/src/main/java/moe/ouom/wekit/hooks/item/dev/WeSplitChatroomMaker.kt +++ b/app/src/main/java/moe/ouom/wekit/hooks/item/dev/WeSplitChatroomMaker.kt @@ -19,8 +19,6 @@ import moe.ouom.wekit.util.log.WeLogger @HookItem(path = "开发者选项/分裂群组", desc = "让群聊一分为二") class WeSplitChatroomMaker : BaseClickableFunctionHookItem() { - override fun entry(classLoader: ClassLoader) {} - override fun onClick(context: Context?) { context ?: return @@ -132,7 +130,5 @@ class WeSplitChatroomMaker : BaseClickableFunctionHookItem() { } } - override fun noSwitchWidget(): Boolean { - return true - } + override fun noSwitchWidget(): Boolean = true } \ No newline at end of file diff --git a/app/src/main/java/moe/ouom/wekit/hooks/item/fix/CrashLogViewer.kt b/app/src/main/java/moe/ouom/wekit/hooks/item/fix/CrashLogViewer.kt index 307a413..17378f2 100644 --- a/app/src/main/java/moe/ouom/wekit/hooks/item/fix/CrashLogViewer.kt +++ b/app/src/main/java/moe/ouom/wekit/hooks/item/fix/CrashLogViewer.kt @@ -9,7 +9,6 @@ import com.afollestad.materialdialogs.list.listItems import moe.ouom.wekit.core.model.BaseClickableFunctionHookItem import moe.ouom.wekit.hooks.core.annotation.HookItem import moe.ouom.wekit.ui.CommonContextWrapper -import moe.ouom.wekit.util.Initiator.loadClass import moe.ouom.wekit.util.common.Toasts.showToast import moe.ouom.wekit.util.common.Utils.formatFileSize import moe.ouom.wekit.util.crash.CrashLogManager @@ -34,32 +33,9 @@ import java.util.Locale class CrashLogViewer : BaseClickableFunctionHookItem() { private var crashLogManager: CrashLogManager? = null - private var appContext: Context? = null - - override fun entry(classLoader: ClassLoader) { - try { - // 获取 Application Context - val activityThreadClass = loadClass("android.app.ActivityThread") - val currentApplicationMethod = activityThreadClass.getMethod("currentApplication") - appContext = currentApplicationMethod.invoke(null) as? Context - - if (appContext == null) { - WeLogger.e("CrashLogViewer", "Failed to get application context") - return - } - - // 初始化崩溃日志管理器 - crashLogManager = CrashLogManager(appContext!!) - - WeLogger.i("CrashLogViewer", "Crash log viewer initialized") - } catch (e: Throwable) { - WeLogger.e("[CrashLogViewer] Failed to initialize crash log viewer", e) - } - } override fun onClick(context: Context?) { - val ctx = context ?: appContext - if (ctx == null) { + if (context == null) { WeLogger.e("CrashLogViewer", "Context is null") return } @@ -68,15 +44,15 @@ class CrashLogViewer : BaseClickableFunctionHookItem() { if (crashLogManager == null) { WeLogger.i("CrashLogViewer", "Lazy initializing CrashLogManager") try { - crashLogManager = CrashLogManager(ctx) + crashLogManager = CrashLogManager(context) } catch (e: Throwable) { WeLogger.e("[CrashLogViewer] Failed to initialize CrashLogManager", e) - showToast(ctx, "初始化失败: ${e.message}") + showToast(context, "初始化失败: ${e.message}") return } } - showCrashLogList(ctx) + showCrashLogList(context) } /** From 8fd36d0812aedb17a4e2bfcae7ceae7876ff2892 Mon Sep 17 00:00:00 2001 From: huajiqaq Date: Thu, 12 Feb 2026 16:46:52 +0800 Subject: [PATCH 3/4] introduce scripting capability via Rhino engine --- SCRIPT_API_DOCUMENT.md | 94 ++ app/build.gradle.kts | 4 + app/proguard-rules.pro | 8 + .../hooks/item/script/ScriptConfigHookItem.kt | 804 ++++++++++++++++++ .../hooks/item/script/ScriptDocViewer.kt | 41 + .../hooks/item/script/ScriptLogHookItem.kt | 264 ++++++ .../wekit/loader/core/NativeCoreBridge.java | 3 + .../moe/ouom/wekit/util/script/JsExecutor.kt | 224 +++++ .../wekit/util/script/ScriptEvalManager.kt | 313 +++++++ .../wekit/util/script/ScriptFileManager.kt | 248 ++++++ .../ouom/wekit/util/script/ScriptLogger.kt | 138 +++ 11 files changed, 2141 insertions(+) create mode 100644 SCRIPT_API_DOCUMENT.md create mode 100644 app/src/main/java/moe/ouom/wekit/hooks/item/script/ScriptConfigHookItem.kt create mode 100644 app/src/main/java/moe/ouom/wekit/hooks/item/script/ScriptDocViewer.kt create mode 100644 app/src/main/java/moe/ouom/wekit/hooks/item/script/ScriptLogHookItem.kt create mode 100644 app/src/main/java/moe/ouom/wekit/util/script/JsExecutor.kt create mode 100644 app/src/main/java/moe/ouom/wekit/util/script/ScriptEvalManager.kt create mode 100644 app/src/main/java/moe/ouom/wekit/util/script/ScriptFileManager.kt create mode 100644 app/src/main/java/moe/ouom/wekit/util/script/ScriptLogger.kt diff --git a/SCRIPT_API_DOCUMENT.md b/SCRIPT_API_DOCUMENT.md new file mode 100644 index 0000000..25e5793 --- /dev/null +++ b/SCRIPT_API_DOCUMENT.md @@ -0,0 +1,94 @@ +# 脚本 API 文档 + +## Beta 状态警告 + +**重要提醒:此 API 目前处于 Beta 测试阶段,在后续更新中可能出现不兼容的更改,使用时请密切关注相关更新通知。** +## 目录 + +1. [钩子函数](#钩子函数) + - [onRequest](#onrequest) + - [onResponse](#onresponse) +2. [WEKit 对象](#wekit-对象) + - [概述](#概述) + - [WEKit Log 函数](#wekit-log-函数) + +## 钩子函数 + +### onRequest + +当请求即将发出时触发此函数。 + +```javascript +function onRequest(data) { + // data 对象包含以下字段: + const {uri,cgiId,jsonData} = data; + + // 示例:修改请求数据 + if (data.cgiId === '111') { + data.jsonData.newField = 'newValue'; + } +} +``` + +### onResponse + +当请求收到响应时触发此函数。 + +```javascript +function onResponse(data) { + // data 对象包含以下字段: + const {uri,cgiId,jsonData} = data; + + // 示例:修改响应数据 + if (data.cgiId === 'some_cgi_id') { + data.jsonData.newField = 'newValue'; + } +} +``` + +## 数据对象说明 + +每个钩子函数接收一个 `data` 参数,该参数是一个对象,包含以下字段: + +| 字段名 | 类型 | 描述 | +|----------|--------|------------------------------| +| uri | string | 请求的目标 URI 地址 | +| cgiId | string | 请求的 CGI ID,用于识别请求类型 | +| jsonData | object | 请求或响应的数据体(JSON 格式) | + +## WEKit 对象 + +### 概述 + +`wekit` 是一个特殊的全局对象,是软件为脚本环境设计的api,提供了与底层系统交互的各种功能接口。该对象在脚本执行环境中自动可用,无需额外导入或初始化。 + +### WEKit Log 函数 + +`wekit.log` 是 `wekit` 对象提供的日志输出函数,专门用于在脚本执行过程中打印调试信息。所有通过此函数输出的日志都会被收集并可在脚本日志查看器中查看。 + +#### 使用方法 + +```javascript +wekit.log(message); +``` + +#### 示例 + +```javascript +function onRequest(data) { + wekit.log('请求发起:', data.uri); + wekit.log('CGI ID:', data.cgiId); + wekit.log('原始数据:', JSON.stringify(data.jsonData)); + + // 修改数据... +} +``` + +### 注意事项 + +1. 日志输出将显示在脚本日志查看器中 +2当 API 发生变更时,请及时更新相关脚本代码 + +### 后续更新计划 + +我们可能在稳定版本中提供更加完善和一致的日志功能接口,届时会提供更详细的文档和更稳定的 API 设计。 \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e348b03..8778e0b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -302,6 +302,7 @@ android { ) resources { merges += "META-INF/xposed/*" + merges += "org/mozilla/javascript/**" // 合并 Mozila Rhino 所有资源 excludes += "**" } } @@ -943,6 +944,9 @@ dependencies { implementation(libs.hutool.core) implementation(libs.nanohttpd) + // Mozila Rhino + implementation("io.apisense:rhino-android:1.3.0") + compileOnly(libs.lombok) annotationProcessor(libs.lombok) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 2371021..6086345 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -127,6 +127,14 @@ public static com.tencent.mmkv.MMKV defaultMMKV(); } +# ========================================================== +# Mozila Rhino +# ========================================================== +-keep class javax.script.** { *; } +-keep class com.sun.script.javascript.** { *; } +-keep class org.mozilla.javascript.** { *; } + + # ========================================================== # 忽略警告 # ========================================================== 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 new file mode 100644 index 0000000..d768221 --- /dev/null +++ b/app/src/main/java/moe/ouom/wekit/hooks/item/script/ScriptConfigHookItem.kt @@ -0,0 +1,804 @@ +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.WePkgManager +import moe.ouom.wekit.hooks.sdk.protocol.intf.IWePkgInterceptor +import moe.ouom.wekit.ui.CommonContextWrapper +import moe.ouom.wekit.ui.creator.dialog.BaseSettingsDialog +import moe.ouom.wekit.util.WeProtoData +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.* + +/** + * 脚本配置管理器Hook项(包含对话框) + */ +@HookItem( + path = "脚本管理/脚本开关", + desc = "管理JavaScript脚本配置" +) +class ScriptConfigHookItem : BaseClickableFunctionHookItem(), IWePkgInterceptor { + + override fun entry(classLoader: ClassLoader) { + // 注册拦截器 + WePkgManager.addInterceptor(this) + } + + override fun onRequest(uri: String, cgiId: Int, reqBytes: ByteArray): ByteArray? { + try { + // 解析 Protobuf 数据 + val data = WeProtoData() + data.fromBytes(reqBytes) + // 转换为 JSON 进行处理 + val json = data.toJSON() + // 应用脚本修改 + val modifiedJson = ScriptEvalManager.getInstance().executeOnRequest(uri, cgiId, json) + // 应用修改并转回字节数组 + data.applyViewJSON(modifiedJson, true) + return data.toPacketBytes() + } catch (e: Exception) { + WeLogger.e("ScriptConfig", e) + } + + return null + } + + override fun onResponse(uri: String, cgiId: Int, respBytes: ByteArray): ByteArray? { + try { + // 解析 Protobuf 数据 + val data = WeProtoData() + data.fromBytes(respBytes) + // 转换为 JSON 进行处理 + val json = data.toJSON() + // 应用脚本修改 + val modifiedJson = ScriptEvalManager.getInstance().executeOnResponse(uri, cgiId, json) + // 应用修改并转回字节数组 + data.applyViewJSON(modifiedJson, true) + // 应用修改并转回字节数组 + data.applyViewJSON(json, true) + return data.toPacketBytes() + } catch (e: Exception) { + WeLogger.e("ScriptConfig", e) + } + return null + } + + override fun unload(classLoader: ClassLoader) { + WePkgManager.removeInterceptor(this) + super.unload(classLoader) + } + + override fun onClick(context: Context) { + val scriptManager = ScriptFileManager.getInstance() + val jsEvalManager = ScriptEvalManager.getInstance() + 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/ScriptDocViewer.kt b/app/src/main/java/moe/ouom/wekit/hooks/item/script/ScriptDocViewer.kt new file mode 100644 index 0000000..7d9ab44 --- /dev/null +++ b/app/src/main/java/moe/ouom/wekit/hooks/item/script/ScriptDocViewer.kt @@ -0,0 +1,41 @@ +package moe.ouom.wekit.hooks.item.script + +import android.content.Context +import android.content.Intent +import android.net.Uri +import moe.ouom.wekit.core.model.BaseClickableFunctionHookItem +import moe.ouom.wekit.hooks.core.annotation.HookItem +import moe.ouom.wekit.util.common.Toasts.showToast +import moe.ouom.wekit.util.log.WeLogger + +/** + * 脚本文档查看器 + * 点击跳转到脚本文档网页 + */ +@HookItem( + path = "脚本管理/脚本文档", + desc = "查看脚本文档" +) +class ScriptDocViewer : BaseClickableFunctionHookItem() { + + override fun onClick(context: Context?) { + if (context == null) { + WeLogger.e("ScriptDocViewer", "Context is null") + return + } + + try { + // 跳转到脚本文档网页 + val url = "https://github.com/cwuom/WeKit/SCRIPT_API_DOCUMENT.md" + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + showToast(context, "正在打开脚本文档...") + } catch (e: Exception) { + WeLogger.e("ScriptDocViewer", "Failed to open script documentation", e) + showToast(context, "打开文档失败: ${e.message}") + } + } + + override fun noSwitchWidget(): Boolean = true +} \ No newline at end of file diff --git a/app/src/main/java/moe/ouom/wekit/hooks/item/script/ScriptLogHookItem.kt b/app/src/main/java/moe/ouom/wekit/hooks/item/script/ScriptLogHookItem.kt new file mode 100644 index 0000000..78d5ea2 --- /dev/null +++ b/app/src/main/java/moe/ouom/wekit/hooks/item/script/ScriptLogHookItem.kt @@ -0,0 +1,264 @@ +package moe.ouom.wekit.hooks.item.script + +import android.content.Context +import com.afollestad.materialdialogs.MaterialDialog +import com.afollestad.materialdialogs.list.listItems +import moe.ouom.wekit.core.model.BaseClickableFunctionHookItem +import moe.ouom.wekit.hooks.core.annotation.HookItem +import moe.ouom.wekit.ui.CommonContextWrapper +import moe.ouom.wekit.util.common.Toasts.showToast +import moe.ouom.wekit.util.log.WeLogger +import moe.ouom.wekit.util.script.ScriptLogger +import java.text.SimpleDateFormat +import java.util.* + +/** + * 脚本日志Hook项 + */ +@HookItem( + path = "脚本管理/脚本日志", + desc = "查看JavaScript脚本执行日志" +) +class ScriptLogHookItem : BaseClickableFunctionHookItem() { + + private var scriptLogger: ScriptLogger? = null + private val dateFormat = SimpleDateFormat("MM-dd HH:mm:ss", Locale.getDefault()) + + override fun onClick(context: Context) { + + if (scriptLogger == null) { + try { + scriptLogger = ScriptLogger.getInstance() + scriptLogger?.initialize() + } catch (e: Throwable) { + WeLogger.e("[ScriptLogHookItem] Failed to initialize ScriptLogger", e) + showToast(context, "初始化失败: ${e.message}") + return + } + } + + showLogViewer(context) + } + + override fun noSwitchWidget(): Boolean = true + + /** + * 显示日志查看器 + */ + private fun showLogViewer(context: Context) { + val wrappedContext = CommonContextWrapper.createAppCompatContext(context) + + val options = listOf( + "查看所有日志", + "按脚本查看", + "按级别查看", + "清除所有日志", + "导出日志" + ) + + MaterialDialog(wrappedContext) + .title(text = "脚本日志管理") + .listItems(items = options) { dialog, index, _ -> + dialog.dismiss() + when (index) { + 0 -> showAllLogs(context) + 1 -> showScriptFilter(context) + 2 -> showLevelFilter(context) + 3 -> confirmClearLogs(context) + 4 -> exportLogs(context) + } + } + .negativeButton(text = "关闭") + .show() + } + + /** + * 显示所有日志 + */ + private fun showAllLogs(context: Context) { + val wrappedContext = CommonContextWrapper.createAppCompatContext(context) + + val logs = scriptLogger?.getAllLogs() ?: emptyList() + if (logs.isEmpty()) { + showToast(context, "暂无日志") + return + } + + val logItems = logs.take(100).map { log -> + val time = dateFormat.format(Date(log.timestamp)) + "[${log.level}] $time - ${log.entryScriptName}\n${log.message.take(100)}" + } + + MaterialDialog(wrappedContext) + .title(text = "脚本日志 (共${logs.size}条)") + .listItems(items = logItems) { dialog, index, _ -> + dialog.dismiss() + showLogDetail(context, logs[index]) + } + .positiveButton(text = "清空") { + confirmClearLogs(context) + } + .negativeButton(text = "返回") { + showLogViewer(context) + } + .show() + } + + /** + * 显示日志详情 + */ + private fun showLogDetail(context: Context, logEntry: ScriptLogger.LogEntry) { + val wrappedContext = CommonContextWrapper.createAppCompatContext(context) + + val detail = buildString { + append("时间: ${dateFormat.format(Date(logEntry.timestamp))}\n") + append("脚本: ${logEntry.entryScriptName}\n") + append("级别: ${logEntry.level}\n") + append("\n消息:\n${logEntry.message}") + } + + MaterialDialog(wrappedContext) + .title(text = "日志详情") + .message(text = detail) + .positiveButton(text = "复制") { + copyToClipboard(context, detail) + showToast(context, "已复制到剪贴板") + } + .negativeButton(text = "返回") { + showAllLogs(context) + } + .show() + } + + /** + * 按脚本筛选 + */ + private fun showScriptFilter(context: Context) { + val wrappedContext = CommonContextWrapper.createAppCompatContext(context) + + val logs = scriptLogger?.getAllLogs() ?: emptyList() + val scripts = logs.map { it.entryScriptName }.distinct() + if (scripts.isEmpty()) { + showToast(context, "暂无日志") + return + } + + MaterialDialog(wrappedContext) + .title(text = "选择脚本") + .listItems(items = scripts) { dialog, index, _ -> + dialog.dismiss() + val scriptName = scripts[index] + val scriptLogs = scriptLogger?.getLogsByScript(scriptName) ?: emptyList() + showFilteredLogs(context, scriptLogs, "脚本: $scriptName") + } + .negativeButton(text = "返回") { + showLogViewer(context) + } + .show() + } + + /** + * 按级别筛选 + */ + private fun showLevelFilter(context: Context) { + val wrappedContext = CommonContextWrapper.createAppCompatContext(context) + + val logs = scriptLogger?.getAllLogs() ?: emptyList() + val levels = logs.map { it.level }.distinct() + if (levels.isEmpty()) { + showToast(context, "暂无日志") + return + } + + MaterialDialog(wrappedContext) + .title(text = "选择级别") + .listItems(items = levels) { dialog, index, _ -> + dialog.dismiss() + val level = levels[index] + val levelLogs = scriptLogger?.getLogsByLevel(level) ?: emptyList() + showFilteredLogs(context, levelLogs, "级别: $level") + } + .negativeButton(text = "返回") { + showLogViewer(context) + } + .show() + } + + /** + * 显示筛选后的日志 + */ + private fun showFilteredLogs(context: Context, logs: List, title: String) { + val wrappedContext = CommonContextWrapper.createAppCompatContext(context) + + if (logs.isEmpty()) { + showToast(context, "无相关日志") + return + } + + val logItems = logs.take(100).map { log -> + val time = dateFormat.format(Date(log.timestamp)) + "[${log.level}] $time - ${log.entryScriptName}\n${log.message.take(100)}" + } + + MaterialDialog(wrappedContext) + .title(text = "$title (共${logs.size}条)") + .listItems(items = logItems) { dialog, index, _ -> + dialog.dismiss() + showLogDetail(context, logs[index]) + } + .negativeButton(text = "返回") { + showLogViewer(context) + } + .show() + } + + /** + * 确认清除日志 + */ + private fun confirmClearLogs(context: Context) { + val wrappedContext = CommonContextWrapper.createAppCompatContext(context) + + MaterialDialog(wrappedContext) + .title(text = "确认清除") + .message(text = "确定要清除所有脚本日志吗?") + .positiveButton(text = "清除") { + scriptLogger?.clearAll() + showToast(context, "日志已清除") + showLogViewer(context) + } + .negativeButton(text = "取消") + .show() + } + + /** + * 导出日志 + */ + private fun exportLogs(context: Context) { + val logs = scriptLogger?.getAllLogs() ?: emptyList() + if (logs.isEmpty()) { + showToast(context, "暂无日志可导出") + return + } + + val exportContent = logs.joinToString("\n\n") { log -> + val time = dateFormat.format(Date(log.timestamp)) + "[${log.level}] $time - ${log.entryScriptName}\n${log.message}" + } + + copyToClipboard(context, exportContent) + showToast(context, "日志已导出到剪贴板") + } + + /** + * 复制到剪贴板 + */ + private fun copyToClipboard(context: Context, text: String) { + try { + val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? android.content.ClipboardManager + val clip = android.content.ClipData.newPlainText("Script Log", text) + clipboard?.setPrimaryClip(clip) + } catch (e: Exception) { + WeLogger.e("Failed to copy to clipboard", e) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/moe/ouom/wekit/loader/core/NativeCoreBridge.java b/app/src/main/java/moe/ouom/wekit/loader/core/NativeCoreBridge.java index 6bae59c..017ef6b 100644 --- a/app/src/main/java/moe/ouom/wekit/loader/core/NativeCoreBridge.java +++ b/app/src/main/java/moe/ouom/wekit/loader/core/NativeCoreBridge.java @@ -8,6 +8,7 @@ import moe.ouom.wekit.host.HostInfo; import moe.ouom.wekit.util.log.WeLogger; +import moe.ouom.wekit.util.script.JsExecutor; public class NativeCoreBridge { @@ -28,6 +29,8 @@ public static void initNativeCore() { Context context = HostInfo.getApplication(); // init mmkv initializeMmkvForPrimaryNativeLibrary(context); + // init JsExecutor + JsExecutor.getInstance().initialize(context); } /** 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 new file mode 100644 index 0000000..604fa8d --- /dev/null +++ b/app/src/main/java/moe/ouom/wekit/util/script/JsExecutor.kt @@ -0,0 +1,224 @@ +package moe.ouom.wekit.util.script + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Handler +import android.os.Looper +import moe.ouom.wekit.util.log.WeLogger +import org.mozilla.javascript.ScriptRuntime +import org.mozilla.javascript.Scriptable +import java.text.MessageFormat +import java.util.Locale +import java.util.ResourceBundle +import java.util.MissingResourceException +import javax.script.ScriptEngine +import javax.script.ScriptEngineManager + +/** + * Rhino JavaScript执行器 + */ +class JsExecutor private constructor() { + private var mRhinoContext: Context? = null + private var mScope: Scriptable? = null + private var mScriptEngine: ScriptEngine? = null + private val mMainHandler = Handler(Looper.getMainLooper()) + private var mInitialized = false + private var mAppContext: Context? = null + + companion object { + @Volatile + private var INSTANCE: JsExecutor? = null + + @JvmStatic + fun getInstance(): JsExecutor { + return INSTANCE ?: synchronized(this) { + INSTANCE ?: JsExecutor().also { INSTANCE = it } + } + } + + /** + * Rhino MessageProvider 降级实现 + */ + private class FallbackMessageProvider : ScriptRuntime.MessageProvider { + private val enBundle = try { + ResourceBundle.getBundle( + "org.mozilla.javascript.resources.Messages", + Locale.ENGLISH + ) + } catch (_: Exception) { + null + } + + // 始终使用英文资源 不尝试当前 locale + private fun getRawMessage(key: String): String { + // 只尝试英文消息 + enBundle?.let { + try { + return it.getString(key) + } catch (_: MissingResourceException) { + // 忽略 + } + } + // 如果英文资源也失败 返回 key 本身 + return key + } + + // 实现带参数格式化的方法 + override fun getMessage(messageId: String, arguments: Array?): String { + val pattern = getRawMessage(messageId) + return if (!arguments.isNullOrEmpty()) { + MessageFormat.format(pattern, *arguments) + } else { + pattern + } + } + } + + /** + * 初始化 Rhino MessageProvider + * 反射修改 一次生效 全局有效 + */ + private fun initRhinoMessageProvider() { + try { + val field = ScriptRuntime::class.java.getDeclaredField("messageProvider") + field.isAccessible = true + field.set(null, FallbackMessageProvider()) + WeLogger.i("Rhino MessageProvider initialized with fallback") + } catch (e: Exception) { + WeLogger.e("Failed to init Rhino MessageProvider", e) + } + } + } + + /** + * 初始化Rhino引擎 + * @param applicationContext Application级别的Context + */ + fun initialize(applicationContext: Context) { + if (mInitialized) { + WeLogger.w("JsExecutor already initialized") + return + } + + // 反射修改 MessageProvider(只需一次) + initRhinoMessageProvider() + // 保存 ApplicationContext 引用 + mAppContext = applicationContext.applicationContext + initializeInternal() + } + + @SuppressLint("SetJavaScriptEnabled") + private fun initializeInternal() { + if (mInitialized) { + return + } + + try { + val context = mAppContext ?: throw IllegalStateException("ApplicationContext not set") + // 获取模块 ClassLoader + val moduleClassLoader = this.javaClass.classLoader + // 使用模块 ClassLoader 加载 + val engineManager = ScriptEngineManager(moduleClassLoader) + + val rhinoEngine = engineManager.getEngineByName("rhino") + + mScriptEngine = rhinoEngine + // 注入日志接口 + injectLoggingInterface() + mInitialized = true + WeLogger.i("JsExecutor initialized with Rhino") + + // 初始化 ScriptFileManager 和 ScriptEvalManager + initRelatedManagers(context) + + } catch (e: Exception) { + WeLogger.e("Rhino init failed: ${e.message}") + mRhinoContext = null + mScope = null + mScriptEngine = null + mAppContext = null + } + } + + /** + * 初始化相关的管理器 + */ + private fun initRelatedManagers(context: Context) { + try { + // 初始化 ScriptFileManager + val scriptFileManager = ScriptFileManager.getInstance() + if (!scriptFileManager.isInitialized()) { + scriptFileManager.initialize(context) + WeLogger.i("JsExecutor: ScriptFileManager initialized") + } + + // 初始化 ScriptEvalManager + val scriptEvalManager = ScriptEvalManager.getInstance() + if (!scriptEvalManager.isInitialized()) { + scriptEvalManager.initialize(scriptFileManager) + WeLogger.i("JsExecutor: ScriptEvalManager initialized") + } + + } catch (e: Exception) { + WeLogger.e("Failed to init related managers: ${e.message}") + } + } + + /** + * 注入日志接口 + */ + @Suppress("unused") + 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) + } + }) + + WeLogger.i("JsExecutor: Injected Rhino logging interface") + + } catch (e: Exception) { + WeLogger.e("Failed to inject logging interface: ${e.message}") + } + } + + /** + * 执行 JavaScript 并返回结果(同步) + */ + fun executeJs(jsCode: String): String? { + if (!mInitialized) { + WeLogger.w("Rhino engine not ready") + return null + } + + return try { + val result = mScriptEngine?.eval(jsCode)?.toString() + result + } catch (e: Exception) { + WeLogger.e("JS exec error: ${e.message}", e) + e.toString() + } + } + + /** + * 检查Rhino引擎是否已初始化 + */ + fun isInitialized(): Boolean { + return mInitialized + } + + /** + * 关闭Rhino引擎 + */ + fun close() { + mMainHandler.post { + mRhinoContext = null + mScope = null + mScriptEngine = null + mInitialized = false + } + } +} \ No newline at end of file diff --git a/app/src/main/java/moe/ouom/wekit/util/script/ScriptEvalManager.kt b/app/src/main/java/moe/ouom/wekit/util/script/ScriptEvalManager.kt new file mode 100644 index 0000000..e35477c --- /dev/null +++ b/app/src/main/java/moe/ouom/wekit/util/script/ScriptEvalManager.kt @@ -0,0 +1,313 @@ +package moe.ouom.wekit.util.script + +import moe.ouom.wekit.util.log.WeLogger +import org.json.JSONObject + +/** + * JavaScript脚本执行管理器 + * 用于执行脚本的onRequest和onResponse方法 + */ +class ScriptEvalManager private constructor() { + + companion object { + @Volatile + private var instance: ScriptEvalManager? = null + + @JvmStatic + fun getInstance(): ScriptEvalManager { + return instance ?: synchronized(this) { + instance ?: ScriptEvalManager().also { instance = it } + } + } + } + + private lateinit var jsExecutor: JsExecutor + private lateinit var scriptFileManager: ScriptFileManager + private var isInitialized = false + + /** + * 初始化脚本执行管理器 + */ + fun initialize(scriptFileManager: ScriptFileManager) { + if (isInitialized) { + WeLogger.w("[ScriptEvalManager] 已经初始化过") + return + } + + try { + // 初始化JsExecutor + jsExecutor = JsExecutor.getInstance() + // 设置ScriptFileManager + this@ScriptEvalManager.scriptFileManager = scriptFileManager + isInitialized = true + WeLogger.i("[ScriptEvalManager] 初始化成功") + } catch (e: Exception) { + WeLogger.e("[ScriptEvalManager] 初始化失败", e) + throw IllegalStateException("ScriptEvalManager 初始化失败", e) + } + } + + /** + * 检查是否已初始化 + */ + fun isInitialized(): Boolean { + return isInitialized + } + + private fun checkInitialized() { + if (!isInitialized) { + throw IllegalStateException("ScriptEvalManager 未初始化,请先调用 initialize() 方法") + } + } + + /** + * 检查脚本是否定义了指定方法 + */ + fun hasMethod(scriptContent: String, methodName: String): Boolean { + checkInitialized() + + if (!jsExecutor.isInitialized()) { + WeLogger.w("[ScriptEvalManager] JsExecutor 未就绪") + return false + } + + val jsCode = """ + (function() { + try { + // 定义脚本内容 + $scriptContent + + // 检查方法是否存在且是函数类型 + return typeof $methodName === 'function'; + } catch (error) { + return false; + } + })(); + """.trimIndent() + + return try { + val result = jsExecutor.executeJs(jsCode) + result?.toBooleanStrictOrNull() ?: false + } catch (e: Exception) { + WeLogger.e("[ScriptEvalManager] 检查方法 $methodName 失败", e) + false + } + } + + /** + * 测试脚本方法定义 + */ + fun testScriptMethods(scriptContent: String): ScriptMethodTestResult { + checkInitialized() + + val hasOnRequest = hasMethod(scriptContent, "onRequest") + val hasOnResponse = hasMethod(scriptContent, "onResponse") + + // 额外检查语法错误 + val syntaxErrors = checkSyntaxError(scriptContent) + + return ScriptMethodTestResult( + hasOnRequest = hasOnRequest, + hasOnResponse = hasOnResponse, + errorMessages = syntaxErrors + ) + } + + /** + * 检查脚本语法错误 + */ + private fun checkSyntaxError(scriptContent: String): List { + checkInitialized() + + val jsCode = """ + (function() { + try { + // 尝试解析脚本内容 + eval($scriptContent); + return []; + } catch (error) { + return [error.message]; + } + })(); + """.trimIndent() + + return try { + val result = jsExecutor.executeJs(jsCode) + if (result.isNullOrEmpty() || result == "[]") { + emptyList() + } else { + listOf("语法错误: $result") + } + } catch (e: Exception) { + WeLogger.e("[ScriptEvalManager] 语法检查失败", e) + listOf("语法检查异常: ${e.message}") + } + } + + /** + * 执行所有启用脚本的onRequest方法 + */ + fun executeOnRequest(uri: String, cgiId: Int, requestJson: JSONObject): JSONObject? { + checkInitialized() + return executeAllScripts("onRequest", uri, cgiId, requestJson) + } + + /** + * 执行所有启用脚本的onResponse方法 + */ + fun executeOnResponse(uri: String, cgiId: Int, responseJson: JSONObject): JSONObject? { + checkInitialized() + return executeAllScripts("onResponse", uri, cgiId, responseJson) + } + + /** + * 执行所有脚本的指定方法 + */ + private fun executeAllScripts(methodName: String, uri: String, cgiId: Int, jsonData: JSONObject): JSONObject? { + checkInitialized() + + val enabledScripts = scriptFileManager.getEnabledScripts() + if (enabledScripts.isEmpty()) return null + + var currentData = jsonData + var modified = false + + enabledScripts.sortedBy { it.order }.forEach { script -> + if (hasMethod(script.content, methodName)) { + val result = executeScriptMethod(script, methodName, uri, cgiId, currentData) + if (result != null) { + try { + currentData = JSONObject(result) + modified = true + WeLogger.d("[ScriptEvalManager] 脚本 ${script.name}.$methodName 执行成功") + } catch (e: Exception) { + WeLogger.e("[ScriptEvalManager] 解析脚本 ${script.name}.$methodName 结果失败", e) + } + } + } + } + + return if (modified) currentData else null + } + + /** + * 执行单个脚本的方法 + */ + private fun executeScriptMethod( + script: ScriptFileManager.ScriptConfig, + methodName: String, + uri: String, + cgiId: Int, + jsonData: JSONObject + ): String? { + val scriptName = script.name + val scriptContent = script.content + + val jsCode = """ + (function() { + try { + // 执行脚本内容 + $scriptContent + + // 创建包含所有参数的对象 + const data = { + uri: '$uri', + cgiId: $cgiId, + jsonData: $jsonData + }; + + // 调用指定方法 + const result = $methodName(data); + + // 返回结果 + if (result === undefined || result === null) { + return null; + } + + if (typeof result === 'object') { + return JSON.stringify(result); + } else if (typeof result === 'string') { + try { + JSON.parse(result); + return result; + } catch(e) { + return JSON.stringify({value: result}); + } + } else { + return JSON.stringify({value: result}); + } + } catch(error) { + wekit.log('[Script:${scriptName} Error] ' + error.message); + return null; + } + })(); + """.trimIndent() + + return try { + ScriptLogger.getInstance().setScriptName(scriptName) + jsExecutor.executeJs(jsCode) + } catch (e: Exception) { + WeLogger.e("[ScriptEvalManager] 执行脚本 ${scriptName}.$methodName 失败", e) + null + } finally { + ScriptLogger.getInstance().resetScriptName() + } + } + + /** + * 测试执行指定的JavaScript代码片段 + */ + fun testExecuteCode(scriptContent: String, codeSnippet: String, scriptName: String = "测试脚本"): String? { + checkInitialized() + + val jsCode = """ + (function() { + try { + // 执行脚本内容 + $scriptContent + // 执行用户输入的代码 + const result = eval('$codeSnippet'); + // 返回结果 + return JSON.stringify(result); + } catch(error) { + return JSON.stringify({error: error.message}); + } + })(); + """.trimIndent() + + return try { + ScriptLogger.getInstance().setScriptName(scriptName) + val result = jsExecutor.executeJs(jsCode) + result + } catch (e: Exception) { + WeLogger.e("[ScriptEvalManager] 测试执行失败", e) + null + } finally { + ScriptLogger.getInstance().resetScriptName() + } + } + + data class ScriptMethodTestResult( + val hasOnRequest: Boolean, + val hasOnResponse: Boolean, + val errorMessages: List + ) { + fun isPassed(): Boolean { + return hasOnRequest || hasOnResponse + } + + fun getSummary(): String { + return buildString { + append("方法检查结果:\n") + append("• onRequest: ${if (hasOnRequest) "✓ 存在" else "✗ 不存在"}\n") + append("• onResponse: ${if (hasOnResponse) "✓ 存在" else "✗ 不存在"}") + + if (errorMessages.isNotEmpty()) { + append("\n\n错误:\n") + errorMessages.forEach { append("• $it\n") } + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/moe/ouom/wekit/util/script/ScriptFileManager.kt b/app/src/main/java/moe/ouom/wekit/util/script/ScriptFileManager.kt new file mode 100644 index 0000000..8fdb30c --- /dev/null +++ b/app/src/main/java/moe/ouom/wekit/util/script/ScriptFileManager.kt @@ -0,0 +1,248 @@ +package moe.ouom.wekit.util.script + +import android.content.Context +import moe.ouom.wekit.util.log.WeLogger +import org.json.JSONObject +import java.io.File +import java.io.FileWriter +import java.io.IOException + +/** + * 脚本文件管理器 + */ +class ScriptFileManager private constructor() { + + companion object { + private const val SCRIPT_DIR = "weikit_scripts" + private const val SCRIPT_SUFFIX = ".json" + + @Volatile + private var INSTANCE: ScriptFileManager? = null + + @JvmStatic + fun getInstance(): ScriptFileManager { + return INSTANCE ?: synchronized(this) { + INSTANCE ?: ScriptFileManager().also { INSTANCE = it } + } + } + } + + data class ScriptConfig( + val id: String, + var name: String, + var content: String, + var enabled: Boolean = true, + var order: Int = 0, + var createdTime: Long = System.currentTimeMillis(), + var modifiedTime: Long = System.currentTimeMillis(), + var description: String = "" + ) { + fun toJson(): JSONObject { + return JSONObject().apply { + put("id", id) + put("name", name) + put("content", content) + put("enabled", enabled) + put("order", order) + put("createdTime", createdTime) + put("modifiedTime", modifiedTime) + put("description", description) + } + } + + companion object { + fun fromJson(json: JSONObject): ScriptConfig { + return ScriptConfig( + id = json.optString("id", ""), + name = json.optString("name", "未命名脚本"), + content = json.optString("content", ""), + enabled = json.optBoolean("enabled", true), + order = json.optInt("order", 0), + createdTime = json.optLong("createdTime", System.currentTimeMillis()), + modifiedTime = json.optLong("modifiedTime", System.currentTimeMillis()), + description = json.optString("description", "") + ) + } + } + } + + private lateinit var scriptDir: File + private var isInitialized = false + + + /** + * 初始化脚本文件管理器 + * @param applicationContext Application级别的Context + */ + fun initialize(applicationContext: Context) { + if (isInitialized) { + WeLogger.w("[ScriptFileManager] 已经初始化过") + return + } + + try { + scriptDir = File(applicationContext.getFilesDir().parentFile, SCRIPT_DIR) + ensureScriptDirExists() + isInitialized = true + WeLogger.i("[ScriptFileManager] 初始化成功: ${scriptDir.absolutePath}") + } catch (e: Exception) { + WeLogger.e("[ScriptFileManager] 初始化失败", e) + throw IllegalStateException("ScriptFileManager 初始化失败", e) + } + } + + + /** + * 检查是否已初始化 + */ + fun isInitialized(): Boolean { + return isInitialized + } + + private fun ensureScriptDirExists() { + if (!scriptDir.exists()) { + if (scriptDir.mkdirs()) { + WeLogger.i("[ScriptFileManager] 脚本目录创建成功: ${scriptDir.absolutePath}") + } + } + } + + private fun checkInitialized() { + if (!isInitialized) { + throw IllegalStateException("ScriptFileManager 未初始化,请先调用 initialize() 方法") + } + } + + /** + * 保存脚本 + */ + fun saveScript(script: ScriptConfig): Boolean { + checkInitialized() + + return try { + ensureScriptDirExists() + + val scriptFile = File(scriptDir, "${script.id}$SCRIPT_SUFFIX") + val json = script.toJson() + + FileWriter(scriptFile).use { writer -> + writer.write(json.toString()) + writer.flush() + } + + WeLogger.d("[ScriptFileManager] 脚本已保存: ${script.name}") + true + } catch (e: IOException) { + WeLogger.e("[ScriptFileManager] 保存脚本失败", e) + false + } + } + + /** + * 获取所有脚本 + */ + fun getAllScripts(): List { + checkInitialized() + + ensureScriptDirExists() + + val scripts = mutableListOf() + + scriptDir.listFiles { _, name -> + name.endsWith(SCRIPT_SUFFIX) + }?.forEach { file -> + try { + val jsonString = file.readText() + val json = JSONObject(jsonString) + scripts.add(ScriptConfig.fromJson(json)) + } catch (e: Exception) { + WeLogger.e("[ScriptFileManager] 读取脚本文件失败: ${file.name}", e) + } + } + + return scripts.sortedBy { it.order } + } + + /** + * 根据ID获取脚本 + */ + fun getScriptById(id: String): ScriptConfig? { + checkInitialized() + + val scriptFile = File(scriptDir, "$id$SCRIPT_SUFFIX") + if (!scriptFile.exists()) return null + + return try { + val jsonString = scriptFile.readText() + val json = JSONObject(jsonString) + ScriptConfig.fromJson(json) + } catch (e: Exception) { + WeLogger.e("[ScriptFileManager] 读取脚本失败: $id", e) + null + } + } + + /** + * 删除脚本 + */ + fun deleteScript(id: String): Boolean { + checkInitialized() + + val scriptFile = File(scriptDir, "$id$SCRIPT_SUFFIX") + return if (scriptFile.exists()) { + val success = scriptFile.delete() + if (success) { + WeLogger.i("[ScriptFileManager] 脚本已删除: $id") + } + success + } else { + false + } + } + + /** + * 删除所有脚本 + */ + fun deleteAllScripts(): Int { + checkInitialized() + + var count = 0 + scriptDir.listFiles { _, name -> + name.endsWith(SCRIPT_SUFFIX) + }?.forEach { file -> + if (file.delete()) { + count++ + } + } + + WeLogger.i("[ScriptFileManager] 已删除 $count 个脚本") + return count + } + + /** + * 获取启用状态的脚本 + */ + fun getEnabledScripts(): List { + checkInitialized() + + return getAllScripts().filter { it.enabled } + } + + /** + * 获取脚本数量 + */ + fun getScriptCount(): Int { + checkInitialized() + + return getAllScripts().size + } + + /** + * 获取启用脚本数量 + */ + fun getEnabledScriptCount(): Int { + checkInitialized() + + return getEnabledScripts().size + } +} \ No newline at end of file diff --git a/app/src/main/java/moe/ouom/wekit/util/script/ScriptLogger.kt b/app/src/main/java/moe/ouom/wekit/util/script/ScriptLogger.kt new file mode 100644 index 0000000..83c8a44 --- /dev/null +++ b/app/src/main/java/moe/ouom/wekit/util/script/ScriptLogger.kt @@ -0,0 +1,138 @@ +package moe.ouom.wekit.util.script + +import moe.ouom.wekit.util.log.WeLogger +import java.util.* +import java.util.concurrent.CopyOnWriteArrayList + +/** + * 脚本日志记录器 + * 专门处理JavaScript脚本的日志记录 + */ +class ScriptLogger { + + companion object { + @Volatile + private var instance: ScriptLogger? = null + + @JvmStatic + fun getInstance(): ScriptLogger { + return instance ?: synchronized(this) { + instance ?: ScriptLogger().also { instance = it } + } + } + } + + private val defaultScriptName = "未知" + private var scriptName: String = defaultScriptName + // 日志条目 + data class LogEntry( + val id: String = UUID.randomUUID().toString(), + val timestamp: Long = System.currentTimeMillis(), + val level: String, // INFO, WARN, ERROR + val message: String, + val entryScriptName: String + ) + + // 配置 + data class LoggerConfig( + var maxEntries: Int = 1000, + var autoPrune: Boolean = true, + var enableConsoleOutput: Boolean = true, + var logLevels: Set = setOf("INFO", "WARN", "ERROR") + ) + + private val logEntries = CopyOnWriteArrayList() + private var config = LoggerConfig() + private var isInitialized = false + + fun isInitialized(): Boolean = isInitialized + + fun initialize() { + if (!isInitialized) { + isInitialized = true + WeLogger.i("ScriptLogger", "Script logger initialized") + } + } + + /** + * 设置脚本名称 + */ + fun setScriptName(scriptName: String) { + this.scriptName = scriptName + } + + /** + * 获取脚本名称 + */ + fun getScriptName(): String { + return this.scriptName + } + + /** + * 恢复默认脚本名称 + */ + fun resetScriptName() { + this.scriptName = defaultScriptName + } + + /** + * 添加日志 + */ + private fun addLogInternal(entry: LogEntry) { + if (!config.logLevels.contains(entry.level)) { + return + } + + // 添加到列表开头(最新的在前面) + logEntries.add(0, entry) + + // 自动清理 + if (config.autoPrune && logEntries.size > config.maxEntries) { + logEntries.removeAt(logEntries.size - 1) + } + + // 输出到系统日志 + if (config.enableConsoleOutput) { + when (entry.level) { + "ERROR" -> WeLogger.e("[Script:${entry.entryScriptName}] ${entry.message}") + "WARN" -> WeLogger.w("[Script:${entry.entryScriptName}] ${entry.message}") + else -> WeLogger.i("[Script:${entry.entryScriptName}] ${entry.message}") + } + } + } + + // 公共日志方法 + + fun info(message: String) { + if (!isInitialized) initialize() + addLogInternal(LogEntry(level = "INFO", message = message, entryScriptName = scriptName)) + } + + fun warn(message: String) { + if (!isInitialized) initialize() + addLogInternal(LogEntry(level = "WARN", message = message, entryScriptName = scriptName)) + } + + fun error(message: String) { + if (!isInitialized) initialize() + addLogInternal(LogEntry(level = "ERROR", message = message, entryScriptName = scriptName)) + } + + // 查询方法 + + fun getAllLogs(): List = logEntries.toList() + + fun getLogsByScript(scriptName: String): List { + return logEntries.filter { it.entryScriptName == scriptName } + } + + fun getLogsByLevel(level: String): List { + return logEntries.filter { it.level == level } + } + + fun clearAll() { + logEntries.clear() + WeLogger.i("ScriptLogger", "All logs cleared") + } + +} \ No newline at end of file From 10761915eb40c1b76d471966ccfed1d71555eecb Mon Sep 17 00:00:00 2001 From: huajiqaq Date: Thu, 12 Feb 2026 20:12:35 +0800 Subject: [PATCH 4/4] Fix Windows compilation failure due to command line length limits or R8 compilation issues --- .idea/.name | 1 + .idea/misc.xml | 71 ++++++++++--------- app/build.gradle.kts | 22 +++++- app/proguard-rules.pro | 2 + .../moe/ouom/wekit/util/script/JsExecutor.kt | 2 +- .../wekit/util/script/ScriptFileManager.kt | 2 +- build-logic/convention/build.gradle.kts | 5 +- 7 files changed, 67 insertions(+), 38 deletions(-) create mode 100644 .idea/.name diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..5856353 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +wekit \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 0c2c1a8..ca0ecae 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,3 +1,4 @@ + @@ -6,48 +7,54 @@ + + + diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8778e0b..ac8dbd4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -589,8 +589,26 @@ tasks.register("protectSensitiveCode") { val d8Name = if (System.getProperty("os.name").lowercase().contains("win")) "d8.bat" else "d8" val d8Path = "${android.sdkDirectory}/build-tools/${android.buildToolsVersion}/$d8Name" - project.exec { - commandLine(d8Path, "--release", "--min-api", "26", "--output", tempDir.absolutePath, *classFiles.map { it.absolutePath }.toTypedArray()) + // 生成参数文件 + val argsFile = tempDir.resolve("d8-args.txt") + // 写入并确保执行后删除 + try { + argsFile.printWriter().use { writer -> + writer.println("--release") + writer.println("--min-api") + writer.println("26") + writer.println("--output") + writer.println(tempDir.absolutePath) + classFiles.forEach { writer.println(it.absolutePath) } + } + + project.exec { + commandLine(d8Path, "@${argsFile.absolutePath}") + } + } finally { + if (!argsFile.delete() && argsFile.exists()) { + argsFile.deleteOnExit() + } } val dexFile = File(tempDir, "classes.dex") diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 6086345..a34af5e 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -133,6 +133,8 @@ -keep class javax.script.** { *; } -keep class com.sun.script.javascript.** { *; } -keep class org.mozilla.javascript.** { *; } +-dontwarn org.mozilla.javascript.** +-dontwarn sun.reflect.CallerSensitive # ========================================================== 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 604fa8d..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 @@ -199,7 +199,7 @@ class JsExecutor private constructor() { result } catch (e: Exception) { WeLogger.e("JS exec error: ${e.message}", e) - e.toString() + e.message } } diff --git a/app/src/main/java/moe/ouom/wekit/util/script/ScriptFileManager.kt b/app/src/main/java/moe/ouom/wekit/util/script/ScriptFileManager.kt index 8fdb30c..4cddda3 100644 --- a/app/src/main/java/moe/ouom/wekit/util/script/ScriptFileManager.kt +++ b/app/src/main/java/moe/ouom/wekit/util/script/ScriptFileManager.kt @@ -13,7 +13,7 @@ import java.io.IOException class ScriptFileManager private constructor() { companion object { - private const val SCRIPT_DIR = "weikit_scripts" + private const val SCRIPT_DIR = "wekit_scripts" private const val SCRIPT_SUFFIX = ".json" @Volatile diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index dbe874f..67e34ef 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -1,3 +1,4 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { @@ -24,7 +25,7 @@ java { } tasks.withType { - kotlinOptions { - jvmTarget = "11" + compilerOptions { + jvmTarget = JvmTarget.JVM_11 } }