     1|package com.ai.phoneagent.vdiso
     2|
     3|import android.graphics.Bitmap
     4|import android.os.Binder
     5|import android.os.Build
     6|import android.os.IInterface
     7|import android.os.SystemClock
     8|import android.util.Log
     9|import android.view.Display
    10|import android.view.Surface
    11|import com.ai.phoneagent.ShizukuBridge
    12|import java.lang.reflect.Proxy
    13|
    14|/**
    15| * Shizuku VirtualDisplay 引擎（虚拟隔离核心）。
    16| *
    17| * **用途**
    18| * - 通过 Shizuku 包装的系统服务 binder（见 [ShizukuServiceHub]）反射调用 `IDisplayManager`，创建 VirtualDisplay。
    19| * - 默认输出到内部的离屏帧接收链路（GL 分发 + ImageReader/纹理）以支持：
    20| * - 虚拟屏截图（给模型识别/决策）
    21| * - 在需要“0 bitmap 预览”时，支持把 VirtualDisplay 输出 surface 临时切换到 UI 的 `SurfaceView/TextureView`。
    22| * - 提供聚焦显示能力（通过 InputManager 的 `setFocusedDisplay` best-effort），减少黑屏/输入路由异常。
    23| *
    24| * **典型用法**
    25| * - 创建/复用虚拟屏：`ensureStarted(Args(...))`
    26| * - 切换输出：`setOutputSurface(surface)` / `restoreOutputSurfaceToImageReader()`
    27| * - 截图：`captureLatestBitmap()`（由上层转换成 base64）
    28| * - 聚焦：`ensureFocusedDisplay(displayId)`
    29| *
    30| * **引用路径（常见）**
    31| * - Kotlin：`VirtualDisplayController`（统一入口与状态管理）。
    32| * - Kotlin：`FloatingStatusService`（工具箱预览/切换输出 surface）。
    33| *
    34| * **使用注意事项**
    35| * - 强依赖 Shizuku 权限与系统服务反射：不同 ROM/Android 版本可能存在 API 差异，本实现大量采用候选方法匹配 + best-effort。
    36| * - 注意线程模型：启动/停止为同步方法，但内部图像与 GL 线程在后台运行，停止时要避免资源泄露。
    37| */
    38|object ShizukuVirtualDisplayEngine {
    39|
    40|    private const val TAG = "AriesVdIsoEngine"
    41|
    42|    data class Args(
    43|            val name: String = "AutoGLM-Virtual",
    44|            val width: Int = 1080,
    45|            val height: Int = 1920,
    46|            val dpi: Int = 142,
    47|            val refreshRate: Float = 0f,
    48|            val rotatesWithContent: Boolean = false,
    49|            val ownerPackage: String = "com.android.shell",
    50|    )
    51|
    52|    @Volatile private var displayId: Int? = null
    53|
    54|    @Volatile private var vdCallback: Any? = null
    55|
    56|    @Volatile private var currentOutputSurface: Surface? = null
    57|
    58|    @Volatile private var glDispatcher: VdGlFrameDispatcher? = null
    59|
    60|    @Volatile private var latestContentWidth: Int = 0
    61|
    62|    @Volatile private var latestContentHeight: Int = 0
    63|
    64|    private val frameLock = Any()
    65|
    66|    @Volatile private var stopping: Boolean = false
    67|
    68|    private fun setVirtualDisplaySurfaceBestEffort(callback: Any, surface: Surface): Result<Unit> {
    69|        // 通过 IDisplayManager.setVirtualDisplaySurface*(...) 反射切换 VirtualDisplay 的输出 Surface。
    70|        // 用途：
    71|        // - 工具箱展开（虚拟隔离模式）时把输出切到 SurfaceView.surface，实现 0 bitmap 的直出预览。
    72|        // - 工具箱收起/切换回主屏模式时把输出切回 ImageReader.surface，便于继续做虚拟屏截图/识别。
    73|        return runCatching {
    74|            val displayManager = ShizukuServiceHub.getDisplayManager()
    75|
    76|            val cbInterface =
    77|                    runCatching {
    78|                                Class.forName("android.hardware.display.IVirtualDisplayCallback")
    79|                            }
    80|                            .getOrNull()
    81|            val asBinder =
    82|                    callback.javaClass.methods.firstOrNull { m ->
    83|                        m.name == "asBinder" &&
    84|                                m.parameterTypes.isEmpty() &&
    85|                                m.returnType.name == "android.os.IBinder"
    86|                    }
    87|            val binder = asBinder?.invoke(callback)
    88|
    89|            val candidates =
    90|                    displayManager.javaClass.methods.filter { m ->
    91|                        m.name == "setVirtualDisplaySurface" ||
    92|                                m.name == "setVirtualDisplaySurfaceAsync"
    93|                    }
    94|
    95|            for (m in candidates) {
    96|                try {
    97|                    val p = m.parameterTypes
    98|                    when {
    99|                        p.size == 2 &&
   100|                                cbInterface != null &&
   101|                                p[0].isAssignableFrom(cbInterface) &&
   102|                                p[1] == Surface::class.java -> {
   103|                            m.invoke(displayManager, callback, surface)
   104|                            return@runCatching
   105|                        }
   106|                        p.size == 2 &&
   107|                                p[0].name == "android.hardware.display.IVirtualDisplayCallback" &&
   108|                                p[1] == Surface::class.java -> {
   109|                            m.invoke(displayManager, callback, surface)
   110|                            return@runCatching
   111|                        }
   112|                        p.size == 2 &&
   113|                                p[0].name == "android.os.IBinder" &&
   114|                                p[1] == Surface::class.java &&
   115|                                binder != null -> {
   116|                            m.invoke(displayManager, binder, surface)
   117|                            return@runCatching
   118|                        }
   119|                        p.size == 2 &&
   120|                                IInterface::class.java.isAssignableFrom(p[0]) &&
   121|                                p[1] == Surface::class.java -> {
   122|                            m.invoke(displayManager, callback, surface)
   123|                            return@runCatching
   124|                        }
   125|                    }
   126|                } catch (t: Throwable) {
   127|                    Log.w(TAG, "setVirtualDisplaySurface failed on ${m.name}", t)
   128|                }
   129|            }
   130|            throw IllegalStateException("No setVirtualDisplaySurface method matched")
   131|        }
   132|    }
   133|
   134|    @Synchronized
   135|    fun ensureStarted(args: Args = Args()): Result<Int> {
   136|        // 入口：确保虚拟屏已启动。
   137|        // 注意：VirtualDisplay 启动时默认输出到 ImageReader.surface，用于缓存帧（截图/识别）。
   138|        // 当需要 0 bitmap 预览时，外部会调用 setOutputSurface(surfaceViewSurface) 临时切换输出。
   139|        val existing = displayId
   140|        if (existing != null && glDispatcher != null && vdCallback != null) {
   141|            return Result.success(existing)
   142|        }
   143|        return start(args)
   144|    }
   145|
   146|    @Synchronized
   147|    fun start(args: Args = Args()): Result<Int> {
   148|        Log.i(
   149|                TAG,
   150|                "ShizukuVirtualDisplayEngine.start: width=${args.width}, height=${args.height}, dpi=${args.dpi}"
   151|        )
   152|
   153|        // 启动 Shizuku VirtualDisplay，并用 ImageReader 接收 RGBA 帧。
   154|        // 帧接收线程在 HandlerThread 上运行，避免阻塞主线程。
   155|        return runCatching {
   156|            stop()
   157|
   158|            stopping = false
   159|
   160|            Log.i(TAG, "Starting VdGlFrameDispatcher...")
   161|            val dispatcher = VdGlFrameDispatcher()
   162|            dispatcher.start(args.width, args.height)
   163|            glDispatcher = dispatcher
   164|
   165|            // 等待 GL 初始化完成，拿到 VirtualDisplay 的输入 Surface（SurfaceTexture）。
   166|            val deadline = SystemClock.uptimeMillis() + 1500L
   167|            var input: Surface? = null
   168|            while (SystemClock.uptimeMillis() <= deadline) {
   169|                input = dispatcher.getInputSurface()
   170|                if (input != null && input.isValid) break
   171|                try {
   172|                    Thread.sleep(10L)
   173|                } catch (_: InterruptedException) {
   174|                    break
   175|                }
   176|            }
   177|            val inputSurface =
   178|                    input
   179|                            ?: run {
   180|                                Log.e(TAG, "GL input surface not ready - input is null")
   181|                                throw IllegalStateException("GL input surface not ready")
   182|                            }
   183|            Log.i(TAG, "GL input surface ready: $inputSurface")
   184|            currentOutputSurface = inputSurface
   185|
   186|            // 在第一帧到达前，用创建时的目标分辨率作为内容尺寸兜底。
   187|            synchronized(frameLock) {
   188|                latestContentWidth = args.width
   189|                latestContentHeight = args.height
   190|            }
   191|
   192|            val created = createVirtualDisplay(args, inputSurface)
   193|            vdCallback = created.second
   194|            displayId = created.first
   195|
   196|            Log.i(TAG, "started: displayId=${displayId} size=${args.width}x${args.height}")
   197|            displayId ?: throw IllegalStateException("displayId is null")
   198|        }
   199|    }
   200|
   201|    fun getDisplayId(): Int? = displayId
   202|
   203|    fun isStarted(): Boolean = displayId != null && glDispatcher != null && vdCallback != null
   204|
   205|    fun getLatestFrameTimeMs(): Long = glDispatcher?.getLatestFrameTimeMs() ?: 0L
   206|
   207|    fun getLatestContentSize(): Pair<Int, Int> {
   208|        // latestBitmap 可能是带 rowStride padding 的“padded bitmap”。
   209|        // 这里返回的 content size 才是虚拟屏真实内容宽高，用于 UI 比例计算/裁剪绘制。
   210|        val d = glDispatcher
   211|        if (d != null) return d.getContentSize()
   212|        val (w, h) = synchronized(frameLock) { latestContentWidth to latestContentHeight }
   213|        return w to h
   214|    }
   215|
   216|    fun captureLatestBitmap(): Result<Bitmap> {
   217|        val d =
   218|                glDispatcher
   219|                        ?: return Result.failure(IllegalStateException("GL dispatcher not started"))
   220|        val bmp =
   221|                d.captureBitmapBlocking()
   222|                        ?: return Result.failure(IllegalStateException("No captured frame"))
   223|        return Result.success(bmp)
   224|    }
   225|
   226|    @Synchronized
   227|    fun stop() {
   228|        stopping = true
   229|
   230|        val dispatcher = glDispatcher
   231|        glDispatcher = null
   232|        runCatching { dispatcher?.stop() }
   233|
   234|        synchronized(frameLock) {
   235|            // reset timestamps/size hints
   236|            latestContentWidth = 0
   237|            latestContentHeight = 0
   238|        }
   239|
   240|        val cb = vdCallback
   241|        vdCallback = null
   242|        displayId = null
   243|
   244|        currentOutputSurface = null
   245|
   246|        if (cb != null) {
   247|            runCatching { releaseVirtualDisplayBestEffort(cb) }
   248|        }
   249|    }
   250|
   251|    @Synchronized
   252|    fun setOutputSurface(surface: Surface): Result<Unit> {
   253|        // OpenGL 分发架构下：VirtualDisplay 固定输出到中转 SurfaceTexture。
   254|        // 这里的 setOutputSurface 仅用于设置“预览输出目标”（悬浮窗 TextureView 的 Surface）。
   255|        val d =
   256|                glDispatcher
   257|                        ?: return Result.failure(IllegalStateException("GL dispatcher not started"))
   258|        if (stopping) return Result.failure(IllegalStateException("Engine stopping"))
   259|        d.setPreviewSurface(surface)
   260|        return Result.success(Unit)
   261|    }
   262|
   263|    @Synchronized
   264|    fun restoreOutputSurfaceToImageReader(): Result<Unit> {
   265|        // OpenGL 分发架构下：截图走 dispatcher 的离屏 ImageReader，不需要切换 VirtualDisplay 输出。
   266|        val d =
   267|                glDispatcher
   268|                        ?: return Result.failure(IllegalStateException("GL dispatcher not started"))
   269|        d.setPreviewSurface(null)
   270|        return Result.success(Unit)
   271|    }
   272|
   273|    fun ensureFocusedDisplay(targetDisplayId: Int): Result<Unit> {
   274|        return runCatching {
   275|            val inputManager = ShizukuServiceHub.getInputManager()
   276|            val cls = inputManager.javaClass
   277|
   278|            val getFocused =
   279|                    cls.methods.firstOrNull { m ->
   280|                        m.name == "getFocusedDisplayId" && m.parameterTypes.isEmpty()
   281|                    }
   282|            val setFocused =
   283|                    cls.methods.firstOrNull { m ->
   284|                        m.name == "setFocusedDisplay" &&
   285|                                m.parameterTypes.size == 1 &&
   286|                                m.parameterTypes[0] == Int::class.javaPrimitiveType
   287|                    }
   288|
   289|            if (setFocused == null) return@runCatching
   290|
   291|            if (getFocused != null) {
   292|                val current = runCatching { getFocused.invoke(inputManager) as? Int }.getOrNull()
   293|                if (current != null && current == targetDisplayId) return@runCatching
   294|            }
   295|
   296|            setFocused.invoke(inputManager, targetDisplayId)
   297|        }
   298|                .onFailure { t ->
   299|                    Log.w(TAG, "ensureFocusedDisplay failed: target=$targetDisplayId", t)
   300|                }
   301|    }
   302|
   303|    /** 将系统焦点显示恢复到默认主屏（displayId = 0）。 用于虚拟屏操作完成后，避免物理按键（如返回键）被路由到虚拟屏。 */
   304|    fun restoreFocusToDefaultDisplay(): Result<Unit> {
   305|        return runCatching {
   306|            val inputManager = ShizukuServiceHub.getInputManager()
   307|            val cls = inputManager.javaClass
   308|            val getFocused =
   309|                    cls.methods.firstOrNull { m ->
   310|                        m.name == "getFocusedDisplayId" && m.parameterTypes.isEmpty()
   311|                    }
   312|            val setFocused =
   313|                    cls.methods.firstOrNull { m ->
   314|                        m.name == "setFocusedDisplay" &&
   315|                                m.parameterTypes.size == 1 &&
   316|                                m.parameterTypes[0] == Int::class.javaPrimitiveType
   317|                    }
   318|                            ?: return@runCatching
   319|
   320|            // 仅在当前不是主屏时才切换，避免无效 IPC
   321|            if (getFocused != null) {
   322|                val current = runCatching { getFocused.invoke(inputManager) as? Int }.getOrNull()
   323|                if (current == 0) return@runCatching
   324|            }
   325|
   326|            setFocused.invoke(inputManager, 0) // Display.DEFAULT_DISPLAY = 0
   327|        }
   328|                .onFailure { t -> Log.w(TAG, "restoreFocusToDefaultDisplay failed", t) }
   329|    }
   330|
   331|    private fun createVirtualDisplay(args: Args, surface: Surface): Pair<Int, Any> {
   332|        val displayManager = ShizukuServiceHub.getDisplayManager()
   333|
   334|        val callback = createVirtualDisplayCallbackProxy()
   335|
   336|        // 打印所有 createVirtualDisplay 方法签名用于调试
   337|        val allCreateMethods =
   338|                displayManager.javaClass.methods.filter { it.name == "createVirtualDisplay" }
   339|        Log.i(TAG, "Found ${allCreateMethods.size} createVirtualDisplay methods:")
   340|        allCreateMethods.forEach { m ->
   341|            Log.i(TAG, "  - ${m.name}: ${m.parameterTypes.map { it.name }.joinToString()}")
   342|        }
   343|
   344|        // 尝试多种方法签名匹配
   345|        val createMethod =
   346|                allCreateMethods.firstOrNull { m ->
   347|                    val p = m.parameterTypes
   348|                    when (p.size) {
   349|                        4 -> {
   350|                            // 标准签名: VirtualDisplayConfig, IVirtualDisplayCallback,
   351|                            // IMediaProjection, String
   352|                            p[0].name.contains("VirtualDisplayConfig") &&
   353|                                    (p[1].name.contains("IVirtualDisplayCallback") ||
   354|                                            p[1].name.contains("IInterface")) &&
   355|                                    (p[2].name.contains("IMediaProjection") ||
   356|                                            p[2] == java.lang.Void.TYPE ||
   357|                                            p[2] == null) &&
   358|                                    p[3] == String::class.java
   359|                        }
   360|                        3 -> {
   361|                            // 某些版本可能是: VirtualDisplayConfig, IBinder, String
   362|                            p[0].name.contains("VirtualDisplayConfig") &&
   363|                                    p[1].name.contains("IBinder") &&
   364|                                    p[2] == String::class.java
   365|                        }
   366|                        else -> false
   367|                    }
   368|                }
   369|                        ?: run {
   370|                            // 如果没有找到4参数的方法，尝试找任何名为 createVirtualDisplay 的方法
   371|                            allCreateMethods.firstOrNull()
   372|                                    ?: throw NoSuchMethodException(
   373|                                            "No createVirtualDisplay method found"
   374|                                    )
   375|                        }
   376|
   377|        Log.i(
   378|                TAG,
   379|                "Using createVirtualDisplay: ${createMethod.name}: ${createMethod.parameterTypes.map { it.name }.joinToString()}"
   380|        )
   381|
   382|        val packageName = args.ownerPackage
   383|        val flags = buildFlags(rotatesWithContent = args.rotatesWithContent)
   384|        val trustedFlag =
   385|                getVirtualDisplayFlagBestEffort(
   386|                        "VIRTUAL_DISPLAY_FLAG_TRUSTED",
   387|                        fallback = (1 shl 10)
   388|                )
   389|        val flagsWithoutTrusted = if (trustedFlag != null) (flags and trustedFlag.inv()) else flags
   390|
   391|        val config = buildVirtualDisplayConfig(args, surface, flags)
   392|
   393|        // 根据参数数量调用
   394|        val id =
   395|                try {
   396|                    when (createMethod.parameterTypes.size) {
   397|                        4 ->
   398|                                createMethod.invoke(
   399|                                        displayManager,
   400|                                        config,
   401|                                        callback,
   402|                                        null,
   403|                                        packageName
   404|                                ) as
   405|                                        Int
   406|                        3 ->
   407|                                createMethod.invoke(
   408|                                        displayManager,
   409|                                        config,
   410|                                        callback,
   411|                                        packageName
   412|                                ) as
   413|                                        Int
   414|                        else ->
   415|                                throw IllegalStateException(
   416|                                        "Unsupported parameter count: ${createMethod.parameterTypes.size}"
   417|                                )
   418|                    }
   419|                } catch (t: Throwable) {
   420|                    val cause =
   421|                            (t as? java.lang.reflect.InvocationTargetException)?.targetException
   422|                                    ?: t
   423|                    if (cause is SecurityException &&
   424|                                    trustedFlag != null &&
   425|                                    (flags and trustedFlag) != 0
   426|                    ) {
   427|                        Log.w(
   428|                                TAG,
   429|                                "createVirtualDisplay with TRUSTED failed, retry without TRUSTED",
   430|                                cause
   431|                        )
   432|                        val configNoTrust =
   433|                                buildVirtualDisplayConfig(args, surface, flagsWithoutTrusted)
   434|                        when (createMethod.parameterTypes.size) {
   435|                            4 ->
   436|                                    createMethod.invoke(
   437|                                            displayManager,
   438|                                            configNoTrust,
   439|                                            callback,
   440|                                            null,
   441|                                            packageName
   442|                                    ) as
   443|                                            Int
   444|                            3 ->
   445|                                    createMethod.invoke(
   446|                                            displayManager,
   447|                                            configNoTrust,
   448|                                            callback,
   449|                                            packageName
   450|                                    ) as
   451|                                            Int
   452|                            else ->
   453|                                    throw IllegalStateException(
   454|                                            "Unsupported parameter count: ${createMethod.parameterTypes.size}"
   455|                                    )
   456|                        }
   457|                    } else {
   458|                        throw t
   459|                    }
   460|                }
   461|
   462|        Log.i(TAG, "createVirtualDisplay ok: displayId=$id flags=$flags")
   463|
   464|        applyImePolicyBestEffort(id)
   465|
   466|        return id to callback
   467|    }
   468|
   469|    private fun applyImePolicyBestEffort(displayId: Int) {
   470|        runCatching {
   471|            val wm = ShizukuServiceHub.getWindowManager()
   472|            val wmClass = wm.javaClass
   473|
   474|            // 完全隔离模式：虚拟屏禁用 IME 渲染
   475|            // setShouldShowIme(displayId, false) → 不在 VD 上显示键盘
   476|            val shouldShowIme =
   477|                    wmClass.methods.firstOrNull { m ->
   478|                        m.name == "setShouldShowIme" &&
   479|                                m.parameterTypes.size == 2 &&
   480|                                m.parameterTypes[0] == Int::class.javaPrimitiveType &&
   481|                                m.parameterTypes[1] == Boolean::class.javaPrimitiveType
   482|                    }
   483|            if (shouldShowIme != null) {
   484|                shouldShowIme.invoke(wm, displayId, false)
   485|            }
   486|
   487|            // DISPLAY_IME_POLICY_HIDE = 2 → 完全隐藏虚拟屏上的 IME
   488|            // 文本输入通过 clipboard+paste 或 input text --display 命令实现，不需要可视键盘
   489|            val DISPLAY_IME_POLICY_HIDE = 2
   490|            val DISPLAY_IME_POLICY_FALLBACK_DISPLAY = 1
   491|            val setImePolicy =
   492|                    wmClass.methods.firstOrNull { m ->
   493|                        m.name == "setDisplayImePolicy" &&
   494|                                m.parameterTypes.size == 2 &&
   495|                                m.parameterTypes[0] == Int::class.javaPrimitiveType &&
   496|                                m.parameterTypes[1] == Int::class.javaPrimitiveType
   497|                    }
   498|            if (setImePolicy != null) {
   499|                setImePolicy.invoke(
   500|                        wm,
   501|

Use offset=501 to continue reading (showing 1-500 of 705 lines)


     1|# Aries AI 技术文档
     2|
     3|> 本文档详细介绍 Aries AI 的核心技术实现与技术优势
     4|
     5|---
     6|
     7|## 📑 目录
     8|
     9|- [一、核心技术概览](#一核心技术概览)
    10|- [二、虚拟屏完全隔离技术](#二虚拟屏完全隔离技术)
    11|- [三、性能优化技术](#三性能优化技术)
    12|- [四、技术对比分析](#四技术对比分析)
    13|- [五、架构设计](#五架构设计)
    14|- [六、关键实现细节](#六关键实现细节)
    15|
    16|---
    17|
    18|## 一、核心技术概览
    19|
    20|Aries AI 采用多项创新技术，实现了业界领先的 Android UI 自动化能力。
    21|
    22|### 1.1 技术亮点
    23|
    24|| 技术 | 说明 | 优势 |
    25||------|------|------|
    26|| **虚拟屏完全隔离** | 100ms 高频焦点强制执行 | 物理按键永不误触，真正的后台自动化 |
    27|| **OpenGL 分发架构** | 中转 SurfaceTexture，GL 线程分发 | 同时支持截图和预览，无需切换 Surface |
    28|| **智能截图策略** | 自动等待非黑帧 | 避免截取空白画面，提升识别准确率 |
    29|| **IME 完全隔离** | 虚拟屏禁用 IME 渲染 | 防止焦点死锁，更稳定可靠 |
    30|| **多重降级兼容** | 方法签名匹配 + 自动降级 | 兼容各版本 Android 和 ROM |
    31|
    32|### 1.2 性能指标
    33|
    34|| 指标 | 数值 | 说明 |
    35||------|------|------|
    36|| 响应时间 | 1.8s | 从接收指令到执行动作的平均时间 |
    37|| 成功率 | 94% | 任务执行成功率 |
    38|| 截图大小 | 85KB | 平均截图文件大小 |
    39|| 焦点恢复速度 | 100ms | 焦点强制执行间隔 |
    40|| 截图压缩质量 | 85% | JPEG 压缩质量 |
    41|
    42|---
    43|
    44|## 二、虚拟屏完全隔离技术
    45|
    46|### 2.1 技术背景
    47|
    48|传统的 Android 自动化方案存在以下问题：
    49|- **焦点干扰**：虚拟屏操作会抢占主屏焦点，导致物理按键误触
    50|- **用户体验差**：主屏幕被自动化操作占用，无法正常使用
    51|- **稳定性差**：焦点切换频繁，容易出现异常
    52|
    53|Aries AI 通过完全隔离技术解决了这些问题。
    54|
    55|### 2.2 完全隔离的焦点管理
    56|
    57|#### 核心原理
    58|
    59|```kotlin
    60|// 100ms 高频焦点强制执行
    61|private const val FOCUS_ENFORCEMENT_INTERVAL_MS = 100L
    62|
    63|private val focusEnforcementRunnable = object : Runnable {
    64|    override fun run() {
    65|        if (activeDisplayId != null && activeDisplayId!! > 0) {
    66|            // 强制恢复焦点到主屏（Display 0）
    67|            ShizukuVirtualDisplayEngine.restoreFocusToDefaultDisplay()
    68|        }
    69|        if (isVirtualDisplayStarted()) {
    70|            focusHandler.postDelayed(this, FOCUS_ENFORCEMENT_INTERVAL_MS)
    71|        }
    72|    }
    73|}
    74|```
    75|
    76|#### 技术优势
    77|
    78|| 特性 | 传统方案 | Aries AI |
    79||------|---------|---------|
    80|| 焦点恢复间隔 | 2000ms（周期性检查） | 100ms（高频强制执行） |
    81|| 焦点隔离程度 | 部分隔离 | 完全隔离 |
    82|| 物理按键误触 | 可能发生 | 永不发生 |
    83|| 用户感知 | 有感知（卡顿） | 无感知 |
    84|
    85|#### 实现细节
    86|
    87|1. **焦点始终驻留主屏**：系统焦点始终在 Display 0（主屏）
    88|2. **定向注入**：虚拟屏操作通过 displayId 定向注入，不依赖焦点
    89|3. **高频执行**：100ms 间隔足够快，能打断系统的自动焦点切换
    90|4. **轻量级操作**：系统级的 `setFocusedDisplay` 操作很轻量，不影响性能
    91|
    92|### 2.3 OpenGL 分发架构
    93|
    94|#### 核心原理
    95|
    96|```kotlin
    97|// 虚拟屏输出固定到 SurfaceTexture
    98|val dispatcher = VdGlFrameDispatcher()
    99|dispatcher.start(width, height)
   100|
   101|// 通过 GL 线程同时分发到多个目标
   102|dispatcher.setPreviewSurface(textureViewSurface)  // 预览
   103|val bitmap = dispatcher.captureBitmapBlocking()    // 截图
   104|```
   105|
   106|#### 架构图
   107|
   108|```
   109|VirtualDisplay
   110|      ↓
   111|SurfaceTexture (中转)
   112|      ↓
   113|GL 线程 (分发)
   114|   ↙     ↘
   115|ImageReader  TextureView
   116|(截图)      (预览)
   117|```
   118|
   119|#### 技术优势
   120|
   121|| 特性 | 传统方案 | Aries AI |
   122||------|---------|---------|
   123|| Surface 切换 | 频繁切换 | 固定输出，无需切换 |
   124|| 同时输出 | 不支持 | 支持截图+预览同时进行 |
   125|| 性能开销 | 高（IPC 频繁） | 低（GL 线程内存拷贝） |
   126|| 预览延迟 | 高 | 低（0-bitmap 直出） |
   127|
   128|### 2.4 智能截图策略
   129|
   130|#### 核心原理
   131|
   132|```kotlin
   133|fun screenshotPngBase64NonBlack(
   134|    maxWaitMs: Long = 1500L,
   135|    pollIntervalMs: Long = 80L
   136|): String {
   137|    val deadline = SystemClock.uptimeMillis() + maxWaitMs
   138|    while (SystemClock.uptimeMillis() <= deadline) {
   139|        val bmp = ShizukuVirtualDisplayEngine.captureLatestBitmap().getOrNull()
   140|        if (bmp != null && !isLikelyBlackBitmap(bmp)) {
   141|            return encodeToPngBase64(bmp)
   142|        }
   143|        Thread.sleep(pollIntervalMs)
   144|    }
   145|    return ""
   146|}
   147|```
   148|
   149|#### 黑帧检测算法
   150|
   151|```kotlin
   152|private fun isLikelyBlackBitmap(bmp: Bitmap): Boolean {
   153|    // 采样 32x32 网格
   154|    val sampleX = 32
   155|    val sampleY = 32
   156|    val stepX = maxOf(1, bmp.width / sampleX)
   157|    val stepY = maxOf(1, bmp.height / sampleY)
   158|
   159|    var nonBlack = 0
   160|    for (y in 0 until bmp.height step stepY) {
   161|        for (x in 0 until bmp.width step stepX) {
   162|            val c = bmp.getPixel(x, y)
   163|            val r = (c shr 16) and 0xFF
   164|            val g = (c shr 8) and 0xFF
   165|            val b = c and 0xFF
   166|            if (r > 10 || g > 10 || b > 10) {
   167|                nonBlack++
   168|                if (nonBlack >= 20) return false
   169|            }
   170|        }
   171|    }
   172|    return true
   173|}
   174|```
   175|
   176|#### 技术优势
   177|
   178|- **自动等待**：最长等待 1.5s，避免截取黑屏
   179|- **快速轮询**：80ms 间隔，平衡速度与性能
   180|- **高效检测**：采样检测，避免全像素遍历
   181|- **不抢焦点**：截图过程不切换焦点，保持完全隔离
   182|
   183|### 2.5 IME 完全隔离
   184|
   185|#### 核心原理
   186|
   187|```kotlin
   188|// 虚拟屏禁用 IME 渲染
   189|setShouldShowIme(displayId, false)
   190|setDisplayImePolicy(displayId, DISPLAY_IME_POLICY_HIDE)
   191|
   192|// 文本输入通过剪贴板+粘贴实现
   193|fun injectPasteBestEffort(displayId: Int) {
   194|    asyncInputInjector.injectKeyComboAsync(
   195|        displayId,
   196|        KeyEvent.KEYCODE_V,
   197|        KeyEvent.META_CTRL_ON or KeyEvent.META_CTRL_LEFT_ON
   198|    )
   199|}
   200|```
   201|
   202|#### 技术优势
   203|
   204|| 特性 | 传统方案 | Aries AI |
   205||------|---------|---------|
   206|| IME 显示 | 在虚拟屏显示 | 完全禁用 |
   207|| 焦点死锁 | 可能发生 | 永不发生 |
   208|| 文本输入 | 依赖 IME | 剪贴板+粘贴 |
   209|| 稳定性 | 一般 | 优秀 |
   210|
   211|---
   212|
   213|## 三、性能优化技术
   214|
   215|### 3.1 输入优化
   216|
   217|#### 3.1.1 智能截图压缩
   218|
   219|```kotlin
   220|// 85% JPEG 质量，最大 150KB
   221|val quality = 85
   222|val maxSize = 150 * 1024
   223|
   224|// 16 像素对齐，优化编码效率
   225|val alignedWidth = (width + 15) and 15.inv()
   226|val alignedHeight = (height + 15) and 15.inv()
   227|```
   228|
   229|**优化效果**：
   230|- 截图大小：250KB → 85KB（减少 66%）
   231|- 视觉质量：几乎无损
   232|- 编码速度：提升 30%
   233|
   234|#### 3.1.2 UI 树智能截断
   235|
   236|```kotlin
   237|// 只保留关键属性
   238|val essentialAttrs = listOf(
   239|    "resource-id",
   240|    "text",
   241|    "class",
   242|    "clickable",
   243|    "bounds"
   244|)
   245|
   246|// 移除不可见节点
   247|if (!node.isVisibleToUser) {
   248|    return null
   249|}
   250|```
   251|
   252|**优化效果**：
   253|- UI 树大小：减少 40%
   254|- 解析速度：提升 25%
   255|
   256|#### 3.1.3 截图缓存与复用
   257|
   258|```kotlin
   259|// TTL 机制
   260|private const val CACHE_TTL_MS = 1500L
   261|
   262|// LRU 淘汰策略
   263|private val cache = LinkedHashMap<String, ScreenshotData>(
   264|    initialCapacity = 3,
   265|    loadFactor = 0.75f,
   266|    accessOrder = true
   267|)
   268|```
   269|
   270|**优化效果**：
   271|- 缓存命中率：35%
   272|- 响应时间：减少 20%
   273|
   274|### 3.2 推理加速
   275|
   276|#### 3.2.1 流式早停机制
   277|
   278|```kotlin
   279|// 边接收边解析
   280|streamingClient.stream(request) { chunk ->
   281|    val action = parseAction(chunk)
   282|    if (action != null) {
   283|        // 识别到动作立即执行，无需等待完整响应
   284|        executor.execute(action)
   285|        return@stream // 早停
   286|    }
   287|}
   288|```
   289|
   290|**优化效果**：
   291|- 响应时间：减少 30%
   292|- Token 消耗：减少 15%
   293|
   294|#### 3.2.2 并行状态采集
   295|
   296|```kotlin
   297|// 并行采集截图和 UI 树
   298|val screenshot = async { captureScreenshot() }
   299|val uiTree = async { getUiHierarchy() }
   300|
   301|val state = State(
   302|    screenshot = screenshot.await(),
   303|    uiTree = uiTree.await()
   304|)
   305|```
   306|
   307|**优化效果**：
   308|- 采集时间：减少 40%
   309|
   310|### 3.3 执行优化
   311|
   312|#### 3.3.1 Tap+Type 操作合并
   313|
   314|```kotlin
   315|// 合并点击和输入
   316|if (nextAction.type == "type" && currentAction.type == "tap") {
   317|    executor.tapAndType(
   318|        x = currentAction.x,
   319|        y = currentAction.y,
   320|        text = nextAction.text
   321|    )
   322|}
   323|```
   324|
   325|**优化效果**：
   326|- 操作步数：减少 20%
   327|- 执行时间：减少 15%
   328|
   329|#### 3.3.2 智能应用预启动
   330|
   331|```kotlin
   332|// 检测到应用名称时预启动
   333|if (task.contains("微信")) {
   334|    prelaunchApp("com.tencent.mm")
   335|}
   336|```
   337|
   338|**优化效果**：
   339|- 启动时间：减少 500ms
   340|
   341|#### 3.3.3 动作延迟动态调整
   342|
   343|```kotlin
   344|private fun getActionDelay(actionName: String): Long {
   345|    return when (actionName) {
   346|        "launch" -> 500L      // 应用启动需要较长时间
   347|        "tap" -> 100L         // 点击操作响应快
   348|        "type" -> 200L        // 输入操作需要等待
   349|        "swipe" -> 300L       // 滑动需要动画时间
   350|        else -> 200L
   351|    }
   352|}
   353|```
   354|
   355|**优化效果**：
   356|- 执行效率：提升 25%
   357|- 成功率：提升 12%
   358|
   359|---
   360|
   361|## 四、技术对比分析
   362|
   363|### 4.1 与同类产品对比
   364|
   365|| 技术特性 | 产品 A | 产品 B | Aries AI |
   366||---------|--------|--------|---------|
   367|| **焦点管理** | 周期性检查（2s） | 手动切换 | 高频强制执行（100ms） |
   368|| **截图方式** | 直接切换 Surface | MediaProjection | OpenGL 分发 |
   369|| **IME 处理** | 默认显示 | 手动隐藏 | 完全禁用 |
   370|| **兼容性** | 单一 API | 单一 API | 多重降级 |
   371|| **黑屏处理** | 手动重试 | 固定延迟 | 智能等待 |
   372|| **预览功能** | 需切换 Surface | 不支持 | 0-bitmap 直出 |
   373|
   374|### 4.2 性能对比
   375|
   376|| 指标 | 产品 A | 产品 B | Aries AI | 优势 |
   377||------|--------|--------|---------|------|
   378|| 响应时间 | 2.5s | 3.0s | 1.8s | 快 28-40% |
   379|| 成功率 | 88% | 85% | 94% | 高 6-9% |
   380|| 焦点隔离 | 80% | 70% | 100% | 完全隔离 |
   381|| 截图大小 | 200KB | 180KB | 85KB | 小 53-58% |
   382|
   383|### 4.3 技术优势总结
   384|
   385|**Aries AI 的核心优势**：
   386|
   387|1. **完全隔离**：100ms 高频焦点强制执行，比产品 A 快 20 倍
   388|2. **OpenGL 分发**：同时支持截图和预览，产品 B 不支持
   389|3. **智能截图**：自动等待非黑帧，产品 A 需要手动重试
   390|4. **IME 隔离**：完全禁用虚拟屏 IME，产品 A/B 可能出现焦点死锁
   391|5. **多重兼容**：支持各版本 Android 和 ROM，产品 A/B 兼容性一般
   392|
   393|---
   394|
   395|## 五、架构设计
   396|
   397|### 5.1 整体架构
   398|
   399|```
   400|┌─────────────────────────────────────────────────────────────────┐
   401|│                          Aries AI                                │
   402|├─────────────────────────────────────────────────────────────────┤
   403|│                                                                  │
   404|│  ┌──────────────────────────────────────────────────────┐      │
   405|│  │  UI 层 (AutomationActivity)                          │      │
   406|│  │  • 主界面交互  • 虚拟屏幕管理  • 实时预览            │      │
   407|│  └────────────────┬─────────────────────────────────────┘      │
   408|│                   │                                              │
   409|│  ┌────────────────▼─────────────────────────────────────┐      │
   410|│  │  控制层 (UiAutomationAgent)                          │      │
   411|│  │  • 任务调度  • 状态管理  • 错误处理                  │      │
   412|│  └────────────────┬─────────────────────────────────────┘      │
   413|│                   │                                              │
   414|│  ┌────────────────▼─────────────────────────────────────┐      │
   415|│  │  核心引擎层                                           │      │
   416|│  │  • 配置中心  • 动作解析器  • 动作执行器              │      │
   417|│  │  • 提示模板  • 截图管理器  • 缓存管理                │      │
   418|│  └────────────────┬─────────────────────────────────────┘      │
   419|│                   │                                              │
   420|│  ┌────────────────▼─────────────────────────────────────┐      │
   421|│  │  模型接入层 (AutoGlmClient)                          │      │
   422|│  │  • 兼容 OpenAI 接口  • 流式响应支持                  │      │
   423|│  └────────────────┬─────────────────────────────────────┘      │
   424|│                   │                                              │
   425|│  ┌────────────────▼─────────────────────────────────────┐      │
   426|│  │  虚拟屏隔离层 (核心技术) ⭐                          │      │
   427|│  │  ┌──────────────────────────────────────────────┐   │      │
   428|│  │  │ VirtualDisplayController (统一入口)          │   │      │
   429|│  │  │ • 虚拟屏生命周期管理                         │   │      │
   430|│  │  │ • 焦点完全隔离（100ms 强制执行）             │   │      │
   431|│  │  │ • 截图/输入注入门面                          │   │      │
   432|│  │  └──────────────┬───────────────────────────────┘   │      │
   433|│  │                 │                                     │      │
   434|│  │  ┌──────────────▼───────────────────────────────┐   │      │
   435|│  │  │ ShizukuVirtualDisplayEngine (核心引擎)       │   │      │
   436|│  │  │ • 通过 Shizuku 反射调用系统服务              │   │      │
   437|│  │  │ • 创建/销毁 VirtualDisplay                   │   │      │
   438|│  │  │ • IME 完全隔离策略                           │   │      │
   439|│  │  │ • 多重降级兼容策略                           │   │      │
   440|│  │  └──────────────┬───────────────────────────────┘   │      │
   441|│  │                 │                                     │      │
   442|│  │  ┌──────────────▼───────────────────────────────┐   │      │
   443|│  │  │ VdGlFrameDispatcher (OpenGL 分发)            │   │      │
   444|│  │  │ • GL 线程接收虚拟屏帧                        │   │      │
   445|│  │  │ • 同时分发到 ImageReader + TextureView      │   │      │
   446|│  │  │ • 智能非黑帧检测                             │   │      │
   447|│  │  │ • 0-bitmap 预览支持                          │   │      │
   448|│  │  └──────────────────────────────────────────────┘   │      │
   449|│  │                                                       │      │
   450|│  │  ┌──────────────────────────────────────────────┐   │      │
   451|│  │  │ InputHelper (输入注入)                       │   │      │
   452|│  │  │ • 异步输入队列                               │   │      │
   453|│  │  │ • displayId 定向注入                         │   │      │
   454|│  │  │ • 触摸/按键/文本输入                         │   │      │
   455|│  │  └──────────────────────────────────────────────┘   │      │
   456|│  └──────────────────────────────────────────────────────┘      │
   457|│                                                                  │
   458|│  ┌──────────────────────────────────────────────────────┐      │
   459|│  │  系统服务层                                           │      │
   460|│  │  • 无障碍服务  • Shizuku 权限管理  • 浮动窗口        │      │
   461|│  └──────────────────────────────────────────────────────┘      │
   462|│                                                                  │
   463|└─────────────────────────────────────────────────────────────────┘
   464|```
   465|
   466|### 5.2 虚拟屏隔离层详细设计
   467|
   468|#### 5.2.1 VirtualDisplayController
   469|
   470|**职责**：
   471|- 虚拟屏生命周期管理
   472|- 焦点完全隔离控制
   473|- 截图/输入注入门面
   474|
   475|**关键方法**：
   476|```kotlin
   477|// 准备虚拟屏
   478|fun prepareForTask(context: Context, adbExecPath: String): Int?
   479|
   480|// 截图（非黑帧）
   481|fun screenshotPngBase64NonBlack(): String
   482|
   483|// 注入点击
   484|fun injectTapBestEffort(displayId: Int, x: Int, y: Int)
   485|
   486|// 清理虚拟屏
   487|fun cleanup(context: Context)
   488|```
   489|
   490|#### 5.2.2 ShizukuVirtualDisplayEngine
   491|
   492|**职责**：
   493|- 通过 Shizuku 反射调用系统服务
   494|- 创建/销毁 VirtualDisplay
   495|- IME 完全隔离策略
   496|- 多重降级兼容策略
   497|
   498|**关键方法**：
   499|```kotlin
   500|// 启动虚拟屏
   501|

Use offset=501 to continue reading (showing 1-500 of 652 lines)


<思考>
这个虚拟屏技术文档非常详细！让我总结一下核心要点给小旺。
</思考>

学到了！这是 Aries-AI 虚拟屏的核心技术：

## 虚拟屏完全隔离技术

**核心原理**：创建一个独立的虚拟显示器（VirtualDisplay），自动化操作在虚拟屏上跑，用户的主屏完全不受影响。

### 关键技术点

1. **100ms 高频焦点强制**
   - 每 100ms 把系统焦点强制拉回主屏
   - 用户按物理按键永远作用在主屏，不会误触虚拟屏
   - 比传统方案（2秒检查一次）快 20 倍

2. **OpenGL 分发架构**
   ```
   VirtualDisplay → SurfaceTexture(中转) → GL线程
                                              ↙    ↘
                                        ImageReader  TextureView
                                          (截图)      (预览)
   ```
   - 截图和预览可以同时进行，不用切换

3. **IME 完全隔离**
   - 虚拟屏上禁用键盘显示
   - 文本输入用剪贴板 + Ctrl+V 粘贴
   - 避免焦点死锁

4. **智能截图**
   - 自动等待非黑帧（最长 1.5s）
   - 80ms 轮询检测
   - 采样 32x32 网格判断是否黑屏

### 依赖条件

- **Shizuku** — 需要 root 或 ADB 授权，用来反射调用系统服务
- 装在手机上的 App，不是远程 ADB

**对我们的意义**：这套技术需要 Shizuku 权限，是手机端 App 的方案。我们用 ADB 远程控制的话，暂时用不上虚拟屏，但思路可以借鉴——比如智能等待非黑帧、操作延迟动态调整这些。


[System note: Your previous turn was interrupted before you could process the last tool result(s). The conversation history contains tool outputs you haven't responded to yet. Please finish processing those results and summarize what was accomplished, then address the user's new message below.]

我现在启动了shiziku


刚才已经成功创建了虚拟屏 **Display 9**（1080x1920）！

现在 Shizuku 启动了，看看能不能玩更多花样：

Explanation:
用户说启动了 Shizuku。之前我用 overlay_display_devices 成功创建了一个虚拟屏（Display 9），现在让我继续探索如何利用这个虚拟屏。

让我先检查 Shizuku 服务状态，然后看看能不能在虚拟屏上运行应用。

terminal

# 检查 Shizuku 服务状态
adb -s localhost:15556 shell "ps -A | grep -i shizuku"


Output:
u0_a466      16963  1154    9306580 176024 0                   0 S moe.shizuku.privileged.api
shell        17994     1    7269204 181400 do_epoll_wait       0 S shizuku_server


Shizuku 跑起来了！试试在虚拟屏上启动应用：

# 在虚拟屏 (Display 9) 上启动 Spotify
adb -s localhost:15556 shell "am start --display 9 -a android.intent.action.VIEW -d 'spotify:search:周杰伦'"


Output: Starting: Intent { act=android.intent.action.VIEW dat=spotify: }


# 截取虚拟屏的画面
adb -s localhost:15556 shell "screencap -d 9 -p /sdcard/vd_screenshot.png" && \
adb -s localhost:15556 pull /sdcard/vd_screenshot.png /tmp/vd_screenshot.png 2>&1


Output:
Failed to take take screenshot. Display Id '9' is not valid.
Capturing failed.


截图不行，试试用 input 在虚拟屏上操作：

# 在虚拟屏上点击
adb -s localhost:15556 shell "input -d 9 tap 540 500"


# 检查虚拟屏上有什么 activity
adb -s localhost:15556 shell "dumpsys activity activities | grep -A3 'Display #9'"


Output:
Display #9 (activities from top to bottom):

  * Task{8fd6a8e #210 type=standard A=10375:com.spotify.music U=0 visible=true visibleRequested=true mode=fullscreen translucent=false sz=1}
    mLastNonFullscreenBounds=Rect(237, 420 - 844, 1500)


Spotify 确实在虚拟屏上跑着！问题是 screencap 不支持虚拟屏截图。

让我看看有没有其他方法：

# 试试用 uiautomator dump 指定 display
adb -s localhost:15556 shell "uiautomator dump --display 9 /sdcard/vd_ui.xml 2>&1"


Output: UI hierchary dumped to: /sdcard/vd_ui.xml


uiautomator 可以！看看虚拟屏上的 UI：

adb -s localhost:15556 shell "cat /sdcard/vd_ui.xml" | grep -oP 'text="[^"]*"' | head -20


Output:
text=""
text=""
text=""
text=""
text=""
text=""
text=""
text=""
text=""
text=""
text=""
text=""
text=""
text=""
text=""
text=""
text=""
text=""
text=""
text=""


# 看看有没有实际内容
adb -s localhost:15556 shell "cat /sdcard/vd_ui.xml" | grep -oP 'text="[^"]+"' | grep -v 'text=""' | head -20


Output:
text="Shizuku 跑起来了！试试在虚拟屏上启动应用："
text="截图不行，试试用 input 在虚拟屏上操作："
text="..."
text="Spotify 确实在虚拟屏上跑着！问题是 screencap 不支持虚拟屏截图。
让我看看有没有其他方法："
text="虾宝"
text="在线"
text="你操作成"
text="发送"


哈哈，dump 到的是主屏（QQ 聊天界面）不是虚拟屏。`--display` 参数可能没生效。

**总结一下目前的发现**：

| 功能 | 状态 | 说明 |
|------|------|------|
| 创建虚拟屏 | ✅ | `settings put global overlay_display_devices '1080x1920/320'` |
| 在虚拟屏启动 App | ✅ | `am start --display 9 ...` |
| 在虚拟屏点击 | ✅ | `input -d 9 tap x y` |
| 虚拟屏截图 | ❌ | screencap 不支持 |
| 虚拟屏 UI dump | ❓ | uiautomator 的 --display 参数可能没生效 |

**核心问题**：没法截图就没法让 AI 看到虚拟屏上的内容。

Aries-AI 用的是 Shizuku 反射调用 `IDisplayManager.createVirtualDisplay()`，然后用 OpenGL 接收帧。这需要在手机上跑一个 App，纯 ADB 做不到。

**两个选择**：
1. 装 Aries-AI 或类似 App，让它帮我们管理虚拟屏
2. 继续用主屏操作，但用 Shizuku 加速一些操作

你想走哪条路？


[System note: Your previous turn was interrupted before you could process the last tool result(s). The conversation history contains tool outputs you haven't responded to yet. Please finish processing those results and summarize what was accomplished, then address the user's new message below.]

那你现在可以实现了吗


Explanation: 用户在问我是否可以实现虚拟屏后台运行了。让我检查一下 scrcpy 是否安装成功，然后尝试使用它。


还没装好。换个方式，直接下载二进制：

# 下载最新 scrcpy
cd /tmp && wget -q https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-linux-x86_64-v3.2.tar.gz && \
tar xzf scrcpy-linux-x86_64-v3.2.tar.gz && \
ls scrcpy-linux-x86_64-v3.2/


Output: [Command timed out after 60s]


网络太慢了。试试国内镜像：

# 用 ghproxy 加速
cd /tmp && curl -L -o scrcpy.tar.gz "https://ghfast.top/https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-linux-x86_64-v3.2.tar.gz" --connect-timeout 10 -m 30 2>&1


Output:
% Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed

  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
  0     0    0     0    0     0      0      0 --:--:--  0:00:01 --:--:--     0
  1 12.3M    1  223k    0     0   111k      0  0:01:53  0:00:01  0:01:52  111k
 11 12.3M   11 1439k    0     0   496k      0  0:00:25  0:00:02  0:00:23  496k
 69 12.3M   69 8799k    0     0  2268k      0  0:00:05  0:00:03  0:00:02 2268k
100 12.3M  100 12.3M    0     0  2792k      0  0:00:04  0:00:04 --:--:-- 2792k


cd /tmp && tar xzf scrcpy.tar.gz && ls scrcpy-linux-x86_64-v3.2/ && ./scrcpy-linux-x86_64-v3.2/scrcpy --version


Output:
adb
icon.png
scrcpy
scrcpy.1
scrcpy-server
scrcpy 3.2 <https://github.com/Genymobile/scrcpy>

Dependencies (compiled / linked):
 - SDL: 2.32.2 / 2.32.2
 - libavcodec: 61.19.101 / 61.19.101
 - libavformat: 61.7.100 / 61.7.100
 - libavutil: 59.39.100 / 59.39.100
 - libavdevice: 61.3.100 / 61.3.100
 - libusb: - / 1.0.28


scrcpy 装好了！试试创建后台虚拟屏：

# 创建后台虚拟屏，不显示窗口（--no-window），录制到文件
cd /tmp/scrcpy-linux-x86_64-v3.2 && \
ADB=/tmp/scrcpy-linux-x86_64-v3.2/adb \
./scrcpy -s localhost:15556 --new-display=1080x1920/320 --no-window --record=/tmp/vd_record.mp4 &
sleep 3 && echo "scrcpy started"


Foreground command uses '&' backgrounding. Use terminal(background=true) for long-lived processes, then run health checks and tests in follow-up terminal calls.


# 用后台模式启动 scrcpy
cd /tmp/scrcpy-linux-x86_64-v3.2 && \
./scrcpy -s localhost:15556 --new-display=1080x1920/320 --no-window --print-fps 2>&1 &
SCRCPY_PID=$!
sleep 5
ps aux | grep scrcpy | grep -v grep


Output: Background process started


process

[1] 1881060
INFO: No video playback, no recording, no V4L2 sink: video disabled
INFO: No video mirroring, SDK mouse disabled
ERROR: --new-display is incompatible with --no-video
scrcpy 3.2 <https://github.com/Genymobile/scrcpy>


需要视频才能用 new-display。换个方式，截图到文件：

terminal

# 启动 scrcpy 创建虚拟屏，录制视频
cd /tmp/scrcpy-linux-x86_64-v3.2 && \
./scrcpy -s localhost:15556 --new-display=1080x1920/320 --no-playback --record=/tmp/vd.mp4 2>&1


Output: Background process started


process

ecord$Builder.build(AudioRecord.java:1103)
	at com.genymobile.scrcpy.FakeContext$$ExternalSyntheticApiModelOutline0.m(D8$$SyntheticClass:0)
	at com.genymobile.scrcpy.audio.AudioDirectCapture.createAudioRecord(AudioDirectCapture.java:53)
	at com.genymobile.scrcpy.audio.AudioDirectCapture.startRecording(AudioDirectCapture.java:96)
	at com.genymobile.scrcpy.audio.AudioDirectCapture.start(AudioDirectCapture.java:125)
	at com.genymobile.scrcpy.audio.AudioEncoder.encode(AudioEncoder.java:235)
	at com.genymobile.scrcpy.audio.AudioEncoder.lambda$start$0$com-genymobile-scrcpy-audio-AudioEncoder(AudioEncoder.java:158)
	at com.genymobile.scrcpy.audio.AudioEncoder$$ExternalSyntheticLambda4.run(D8$$SyntheticClass:0)
	at java.lang.Thread.run(Thread.java:1119)
[server] INFO: New display: 1080x1920/320 (id=10)
scrcpy 3.2 <https://github.com/Genymobile/scrcpy>
INFO: ADB device found:
INFO:     --> (tcpip)  localhost:15556                 device  RMX6688
INFO: Recording started to mp4 file: /tmp/vd.mp4


创建了 **Display 10**！但音频有问题导致退出了。禁用音频试试：

terminal

# 禁用音频，只要视频
cd /tmp/scrcpy-linux-x86_64-v3.2 && \
./scrcpy -s localhost:15556 --new-display=1080x1920/320 --no-playback --no-audio --record=/tmp/vd.mp4 2>&1


Output: Background process started


sleep 3 && adb -s localhost:15556 shell "dumpsys display | grep 'Display [0-9]:'"


Output: Display 0:


process

INFO: No video mirroring, SDK mouse disabled
/tmp/scrcpy-linux-x86_64-v3.2/scrcpy-server: 1 file pushed, 0 skipped. 101.5 MB/s (90888 bytes in 0.001s)
[server] INFO: Device: [realme] realme RMX6688 (Android 16)
[server] INFO: New display: 1080x1920/320 (id=11)
ERROR: Could not create renderer: Couldn't find matching render driver
ERROR: Recording failed to /tmp/vd.mp4
scrcpy 3.2 <https://github.com/Genymobile/scrcpy>
INFO: ADB device found:
INFO:     --> (tcpip)  localhost:15556                 device  RMX6688
INFO: Recording started to mp4 file: /tmp/vd.mp4


服务器没显卡，渲染失败。试试 v4l2 或者纯截图模式：

terminal

# 看看 scrcpy 有没有截图功能
cd /tmp/scrcpy-linux-x86_64-v3.2 && ./scrcpy --help 2>&1 | grep -iE "screenshot|capture|png|jpg"


Output:
Duplicate audio (capture and keep playing on the device).
         - "playback": captures the audio playback (Android apps can opt-out, so
        the whole output is not necessarily captured).
         - "mic": captures the microphone.
         - "mic-unprocessed": captures the microphone unprocessed (raw) sound.
         - "mic-camcorder": captures the microphone tuned for video recording,
         - "mic-voice-recognition": captures the microphone tuned for voice
         - "mic-voice-communication": captures the microphone tuned for voice
         - "voice-call": captures voice call.
         - "voice-call-uplink": captures voice call uplink only.
         - "voice-call-downlink": captures voice call downlink only.
         - "voice-performance": captures audio meant to be processed for live
        Specify the camera capture frame rate.
        Enable high-speed camera capture mode.
        Specify an explicit camera capture size.
    --capture-orientation=value
        Set the capture video orientation.
        If a leading '@' is passed (@90) for display capture, then the rotation
        List the valid camera capture sizes.
        Limit the frame rate of screen capture (officially supported since
        In "uhid" and "aoa" modes, the computer mouse is captured to control the
        LAlt, LSuper or RSuper toggle the capture mode, to give control of the
        LAlt, LSuper or RSuper toggle the mouse capture mode, to give control of
        By default, scrcpy mirrors only the video when audio capture fails on
        Reset video capture/encoding


scrcpy 主要是串流，不是截图。

**结论**：服务器没有 GPU/显示器，scrcpy 的录制功能跑不起来。

**可行方案**：
1. **用 Aries-AI App** — 它在手机端用 OpenGL 处理，不依赖服务器显卡
2. **用 X-OmniClaw App** — 同理
3. **继续用主屏** — 我们现在的方案其实够用，uiautomator dump 不需要截图也能操作

你要不要试试装 Aries-AI？它是开源的，可以直接用。或者我们就继续用现在的主屏方案，反正也能放歌 🎵


路圣：/sdcard/. xomniclaw/skills/你看看他的技能有没有好用的


Explanation: 用户让我查看 X-OmniClaw 在手机上的技能目录，看看有没有好用的技能可以学习。

adb -s localhost:15556 shell "ls -la /sdcard/.xomniclaw/skills/"


Output: adb: device 'localhost:15556' not found


ADB 断了，重连：

adb connect localhost:15556 && sleep 1 && adb -s localhost:15556 shell "ls -la /sdcard/.xomniclaw/skills/"


Output:
Connection refused
adb: device 'localhost:15556' not found


隧道断了，需要你在 Termux 重新建立：

```bash
ssh -R 15556:localhost:5555 ubuntu@100.81.160.90 -N
```


应该好了


adb connect localhost:15556 && adb -s localhost:15556 shell "ls -la /sdcard/.xomniclaw/skills/"


Output:
connected to localhost:15556
total 33
drwxrws--- 2 u0_a274 media_rw 3452 2026-05-15 03:36 app-search
drwxrws--- 2 u0_a274 media_rw 3452 2026-05-15 03:36 capcut-theme-video
drwxrws--- 2 u0_a274 media_rw 3452 2026-05-15 03:36 channel-config
drwxrws--- 2 u0_a274 media_rw 3452 2026-05-15 03:36 clipboard-to-shortcut
drwxrws--- 2 u0_a274 media_rw 3452 2026-05-15 03:36 gallery-memory
drwxrws--- 2 u0_a274 media_rw 3452 2026-05-15 03:36 gallery-qa
drwxrws--- 2 u0_a274 media_rw 3452 2026-05-15 03:36 memory-evolution
drwxrws--- 2 u0_a274 media_rw 3452 2026-05-15 03:36 model-config
drwxrws--- 2 u0_a274 media_rw 3452 2026-05-15 03:36 scheduled-automation
drwxrws--- 2 u0_a274 media_rw 3452 2026-05-15 03:36 skill-creator
drwxrws--- 2 u0_a274 media_rw 3452 2026-05-15 03:36 taobao-search


有料！看看每个技能：

for skill in app-search capcut-theme-video gallery-memory gallery-qa memory-evolution scheduled-automation skill-creator taobao-search; do
  echo "=== $skill ==="
  adb -s localhost:15556 shell "cat /sdcard/.xomniclaw/skills/$skill/SKILL.md" 2>/dev/null | head -50
  echo ""
done


Output:
=== app-search ===
---
name: app-search
description: |
  主流APP搜索。当用户想要在拼多多、美团、高德地图、抖音、快手、小红书、哔哩哔哩、知乎、百度、OPPO音乐、OPPO软件商店等APP中搜索时激活。覆盖电商购物、生活服务、短视频内容、知识搜索、音乐搜索、应用商店搜索等场景。
metadata:
  {
    "xomniclaw": {
      "always": false,
      "emoji": "🔍",
      "version": "1.0.0",
      "category": "search"
    }
  }
---

# App Search Tool

通过各主流 APP 的 Deep Link 直达搜索页面，浏览结果并为用户提供有价值的总结。

## 🎯 When to Use

Use this skill when user asks about:

### 电商购物
✅ **拼多多搜索** - "拼多多搜一下纸巾"、"拼多多有什么便宜的耳机"

### 生活服务
✅ **美团搜索** - "美团上搜搜附近火锅店"、"帮我在美团找酒店"
✅ **高德地图** - "高德上搜一下加油站"、"帮我找附近停车场"

### 短视频 / 内容
✅ **抖音搜索** - "抖音搜一下健身教程"、"在抖音看看旅行攻略"
✅ **快手搜索** - "快手搜一下美食做法"、"快手上找一下钓鱼视频"
✅ **小红书搜索** - "小红书搜一下穿搭"、"帮我在小红书找护肤攻略"
✅ **哔哩哔哩搜索** - "B站搜一下编程教程"、"在B站看看数码评测"

### 知识 / 搜索
✅ **知乎搜索** - "知乎上搜一下如何学Python"、"帮我在知乎找一下理财建议"
✅ **百度搜索** - "百度搜一下天气预报"、"帮我百度一下这个问题"

### 音乐
✅ **OPPO音乐搜索** - "OPPO音乐搜周杰伦"、"帮我在OPPO音乐搜一下晴天"

### 应用商店
✅ **OPPO软件商店搜索** - "OPPO软件商店搜红果短剧"、"帮我在软件商店找抖音"

## 🛑 工具选择（必读，最易踩坑）

跳转 APP 搜索页**只能且必须**调用 `device(action="open", uri="...")`：

=== capcut-theme-video ===
---
name: capcut-theme-video
description: CapCut (剪映) one-tap video creation on Android. Always clear A_latest first (copy_images_to_album clear-only), then retrieve by time (list_gallery_images) or content (image-memories). Copy to A_latest (clear_album_first), CapCut 照片视频 → A_latest → 全部, multi_tap, N=K.
metadata: { "xomniclaw": { "always": false, "emoji": "🎬" } }
---

# 剪映（CapCut）一键成片与相册选图

本 Skill 指导你如何获取目标照片、统一存入 `A_latest` 相册，并在剪映中完成「一键成片」。

## 何时使用

用户要：**打开剪映**、**一键成片**、**从相册选多张图（按时间或按主题）**、**去成片 / 导出视频** 等多步 UI 操作时加载本 Skill。

包名：`com.lemon.lv`

## 相册名 `A_latest`（硬规则）

- **本流程使用的相册名有且只有一个：`A_latest`**（与 `Pictures/A_latest/` 及剪映「照片视频」下拉显示一致）。
- **禁止**变体：`A_latest_风景`、`主题_A_latest`、`latest`、带空格等；主题/时间只体现在检索，**不要**写进相册名。
- **入库前必须先清相册**：最终写入时 `copy_images_to_album` 须 **`clear_album_first: true`**。
- **任意检索前也须先清空 `A_latest`**（路径 A、路径 B **相同**）：见下「步骤 0」。避免旧副本混入按时间的 `list_gallery_images` 结果，也避免按内容选图时相册里仍残留上一轮素材、与本次拷贝混淆。

---

## 核心策略：如何获取照片与目标张数 K

在进入剪映前，必须先确定要选哪些照片以及**目标张数 K**。不论是按时间还是按主题，**所有选出的照片都必须先复制到 `A_latest` 相册中**，然后在剪映里统一从 `A_latest` 相册中选取。

### 步骤 0：先清空 `A_latest`（路径 A 与路径 B 必做，先于一切检索）

在**任何** `list_gallery_images`（路径 A）或 `memory_search` / `memory_get`（路径 B）**之前**，先调用 **`copy_images_to_album`**：

- `album_name: "A_latest"`
- `clear_album_first: true`
- **不要传** `content_uris` / `media_ids` → 工具**只清空**该相册（空相册则删除 0 条）。

**原因简述**：

- **路径 A**：`A_latest` 里旧副本仍在 MediaStore 时，`date_preset` 等**未按相册排除**的检索会把它们和真实目标照片混在一起。
- **路径 B**：先清空可保证后续写入 `A_latest` 时相册状态干净，不与上一轮成片残留混淆。

**可选探针**（与直接先清空等价）：先 `list_gallery_images`，`album_filter: "A_latest"`，`limit: 1`；仅当 **`count` ≥ 1** 时再执行上述仅清空调用。

### 步骤 1：获取目标照片的 content_uris

完成步骤 0 后，按用户需求二选一：

#### 路径 A：基于时间或相册名（非主题语义）
**适用场景**：用户明确要求「今天拍的照片」「昨天的照片」「最近 24 小时」，或者指定了具体的系统相册名。

=== gallery-memory ===
---
name: gallery-memory
description: Sync gallery images into long-term memory files and rebuild the user profile from photo-derived memories.
metadata:
  {
    "xomniclaw": {
      "always": false,
      "emoji": "🖼",
      "skillKey": "gallery-memory",
      "version": "1.0.0",
      "category": "memory"
    }
  }
---

# Gallery Memory

Use this skill when the user wants to:

- scan new images from the phone gallery
- extract memories from screenshots or photos
- rebuild a user profile from image memories
- schedule periodic gallery memory updates

Do not use this skill as the first choice for questions like `今天我拍了什么照片` or `我最近拍过什么截图`.
For image-question answering or gallery-image operations based on existing `memory/IMAGE-MEMORY.md`, prefer `gallery-qa`.

This skill is responsible for **producing and maintaining** image memories.
It is not the primary consumer for gallery-image questions or operations once `memory/IMAGE-MEMORY.md` already exists.
When image memories are already available:

- `gallery-qa` should consume them
- the full `memory/IMAGE-MEMORY.md` file should be loaded into context
- later execution should rely on the filename stored in each entry, not wildcard filename guesses

## Preferred Tool

### `gallery_memory`

Use `gallery_memory` as the direct tool for this workflow.

Supported actions:

- `sync`: scan new gallery images, summarize them, and rewrite `memory/IMAGE-MEMORY.md` in compact format (filename, time, album, summary)
- `build_profile`: rebuild `memory/USER-PROFILE.md` from existing memories
- `status`: inspect cursor and output file availability
- `reset_cursor`: clear the incremental scan cursor
- `clear_image_memories`: reset `memory/IMAGE-MEMORY.md` to the initial template
- `clear_user_profile`: reset `memory/USER-PROFILE.md` to the initial template
- `reset_all`: clear both files and reset the scan cursor

=== gallery-qa ===
---
name: gallery-qa
description: Consume the full memory/IMAGE-MEMORY.md file for all gallery-image questions and operations instead of using retrieval-first search.
metadata:
  {
    "xomniclaw": {
      "always": false,
      "emoji": "🖼",
      "skillKey": "gallery-qa",
      "version": "1.0.0",
      "category": "memory"
    }
  }
---

# Gallery QA

Use this skill for all gallery-image consumption tasks, including:

- `今天我拍了什么照片`
- `我最近拍过什么`
- `我拍过和猫有关的照片吗`
- `帮我把所有截图整理出来`
- `把猫主题图片复制到某个文件夹`

This skill is the primary consumer of `memory/IMAGE-MEMORY.md`.
It does **not** maintain or rebuild image memories. If the user explicitly asks to scan, sync, refresh, or rebuild image memories, use `gallery-memory` instead.

## Source Of Truth

The source of truth is:

- `memory/IMAGE-MEMORY.md`

Each entry is compact and should be read as one whole item:

- title line: the image filename
- `time`: capture or creation time
- `album`: where the image lives in the gallery (MediaStore bucket / album display name)
- `summary`: short semantic description

## Core Workflow

When this skill applies, follow this workflow strictly:

1. Load the full `memory/IMAGE-MEMORY.md` file into context with `memory_get`.
2. Treat the entire file as the candidate set.
3. Read the entries directly from top to bottom and find the entries relevant to the user request.
4. For question answering, summarize the matched entries in natural language.
5. For operations such as copy / move / add to folder / add to album, use the filename in each matched entry for later execution. Use `album` when the user asks which album or folder the image came from.

=== memory-evolution ===
---
name: memory-evolution
description: Process pending X-OmniClaw task memories, update MEMORY.md, and rebuild the compact USER-PROFILE.md.
metadata:
  {
    "xomniclaw": {
      "always": false,
      "emoji": "🧠",
      "skillKey": "memory-evolution",
      "version": "1.0.0",
      "category": "memory"
    }
  }
---

# Memory Evolution

Use this skill when the user asks to:

- update global memory
- evolve task memory
- summarize task experience into the user profile
- inspect memory evolution status
- configure or verify the daily memory update mechanism

This skill maintains task-derived memories. Gallery-derived memories are maintained by `gallery-memory`.

## Memory Files

- `MEMORY.md`: task memory from X-OmniClaw usage, including habits, reusable workflows, failures, and project context.
- `memory/IMAGE-MEMORY.md`: gallery memory from photos and screenshots.
- `memory/USER-PROFILE.md`: compact user profile loaded by default. It should be rebuilt from both `MEMORY.md` and `IMAGE-MEMORY.md`.

## Preferred Tool

### `memory_evolution`

Use `memory_evolution` as the direct tool for this workflow.

Supported actions:

- `run`: process pending task memory events, update `MEMORY.md`, and rebuild `memory/USER-PROFILE.md`
- `status`: inspect the latest evolution run status and pending event count

## Recommended Calls

### Run evolution

```json
{

=== scheduled-automation ===
---
name: scheduled-automation
description: Schedule app automation tasks such as opening an app and performing actions at a specific time.
metadata:
  {
    "xomniclaw": {
      "always": false,
      "emoji": "⏰",
      "skillKey": "scheduled-automation",
      "version": "1.0.0",
      "category": "automation"
    }
  }
---

# Scheduled Automation

Use this skill when the user wants the phone to do something **later**, **every day**, **on specific weekdays**, **on workdays**, or **at a fixed minute interval**.

## Extraction Workflow

For app-centric natural-language requests, use this two-stage workflow:

1. LLM extraction:
   - extract `app_name`
   - extract `operation`
   - extract `schedule_phrase`
2. Rule parsing:
   - let `schedule_app_task` parse `schedule_phrase`
   - convert it into `repeat`, `daily_time`, `days_of_week`, or `interval_minutes`
3. Ambiguity check:
   - if `app_name`, `operation`, or `schedule_phrase` is still ambiguous
   - let the tool return clarification questions first instead of creating the wrong task

Important boundary:

- This skill is only for creating, listing, or cancelling future schedules.
- When an existing schedule fires, do **not** call `schedule_app_task` again.
- The saved execution instruction must be immediate and one-shot, e.g. `打开小红书，然后搜索新闻总结后发给我`.
- Never keep words such as `每天晚上8点`, `每周三`, `定时`, or `到点` inside `operation`; those belong only in `schedule_phrase`.

Example:

- user request: `每周三早上10点打开小红书搜新闻并总结`
- LLM extraction:
  - `app_name = 小红书`
  - `operation = 搜新闻并总结`
  - `schedule_phrase = 每周三早上10点`
- rule parsing result:
  - `repeat = weekly`

=== skill-creator ===
---
name: skill-creator
description: Create, edit, improve, or audit AgentSkills. Use when creating a new skill from scratch or when asked to improve, review, audit, tidy up, or clean up an existing skill or SKILL.md file. Also use when editing or restructuring a skill directory (moving files to references/ or scripts/, removing stale content, validating against the AgentSkills spec). Triggers on phrases like "create a skill", "author a skill", "tidy up a skill", "improve this skill", "review the skill", "clean up the skill", "audit the skill".
---

# Skill Creator

This skill provides guidance for creating effective skills.

## About Skills

Skills are modular, self-contained packages that extend Codex's capabilities by providing
specialized knowledge, workflows, and tools. Think of them as "onboarding guides" for specific
domains or tasks—they transform Codex from a general-purpose agent into a specialized agent
equipped with procedural knowledge that no model can fully possess.

### What Skills Provide

1. Specialized workflows - Multi-step procedures for specific domains
2. Tool integrations - Instructions for working with specific file formats or APIs
3. Domain expertise - Company-specific knowledge, schemas, business logic
4. Bundled resources - Scripts, references, and assets for complex and repetitive tasks

## Core Principles

### Concise is Key

The context window is a public good. Skills share the context window with everything else Codex needs: system prompt, conversation history, other Skills' metadata, and the actual user request.

**Default assumption: Codex is already very smart.** Only add context Codex doesn't already have. Challenge each piece of information: "Does Codex really need this explanation?" and "Does this paragraph justify its token cost?"

Prefer concise examples over verbose explanations.

### Set Appropriate Degrees of Freedom

Match the level of specificity to the task's fragility and variability:

**High freedom (text-based instructions)**: Use when multiple approaches are valid, decisions depend on context, or heuristics guide the approach.

**Medium freedom (pseudocode or scripts with parameters)**: Use when a preferred pattern exists, some variation is acceptable, or configuration affects behavior.

**Low freedom (specific scripts, few parameters)**: Use when operations are fragile and error-prone, consistency is critical, or a specific sequence must be followed.

Think of Codex as exploring a path: a narrow bridge with cliffs needs specific guardrails (low freedom), while an open field allows many routes (high freedom).

### Anatomy of a Skill

Every skill consists of a required SKILL.md file and optional bundled resources:

```

=== taobao-search ===
---
name: taobao-search
description: |
  淘宝商品搜索。当用户想要搜索商品、找某品牌产品、比价、查看新款等电商购物场景时激活。
metadata:
  {
    "xomniclaw": {
      "always": false,
      "emoji": "🛒",
      "version": "1.0.0",
      "category": "shopping"
    }
  }
---

# Taobao Search Tool

通过淘宝 Deep Link 直达搜索页面，浏览商品并为用户提供购物建议。

## 🎯 When to Use

Use this skill when user asks about:

✅ **商品搜索** - "帮我找阿迪达斯今年新款男鞋"
✅ **品牌查找** - "搜一下耐克跑步鞋"
✅ **比价购物** - "淘宝上看看有什么好的蓝牙耳机"
✅ **新品浏览** - "今年有什么新款连衣裙"
✅ **特定条件搜索** - "200元以内的双肩包"

## 📋 Workflow

### 标准搜索流程

```
1. 解析用户意图，提取搜索关键词
2. 通过 device(action="open", uri="taobao://...") 跳转淘宝搜索页
3. 等待淘宝 App 加载搜索结果：device(action="act", kind="wait", timeMs=1500)
4. 拉取页面 UI 树：device(action="snapshot", format="compact")
   —— 这是商品名/价格/店铺/销量等文字信息的**唯一来源**
5. 从 snapshot 返回的 text 节点里逐条提取商品信息（详见 Step 3）
6. 基于提取到的具体商品信息回答用户（200字以内，必须包含真实数字和细节）
```

> ⚠ **绝对禁止**: 不要用 `read_file` 读截图 PNG（如 `read_file("/sdcard/.xomniclaw/workspace/screenshots/*.png")`）。PNG 是二进制，read_file 会以 UTF-8 解码出大量乱码并撑爆上下文窗口，整个 session 会被 `context window exceeds limit` 终止。**只用 `device(action="snapshot")` 读页面文字**。

## 🔗 Deep Link 构造规则

淘宝搜索 Deep Link 格式：

```


好东西！让我看看还有两个配置类的：

adb -s localhost:15556 shell "cat /sdcard/.xomniclaw/skills/channel-config/SKILL.md" && echo "---" && adb -s localhost:15556 shell "cat /sdcard/.xomniclaw/skills/model-config/SKILL.md"


Output:
---
name: channel-config
description: Configure messaging channels in X-OmniClaw, including Feishu and Discord. Use when the user asks to configure, enable, disable, inspect, or fix channel settings such as tokens, appId/appSecret, dmPolicy, groupPolicy, requireMention, or connection status. For X-OmniClaw, these settings live in /sdcard/.xomniclaw/xomniclaw.json under channels.*.
---

# Channel Config

For X-OmniClaw, channel settings are stored in:

- `/sdcard/.xomniclaw/xomniclaw.json`

Relevant paths:

- `channels.feishu.enabled`
- `channels.feishu.appId`
- `channels.feishu.appSecret`
- `channels.feishu.domain`
- `channels.feishu.connectionMode`
- `channels.feishu.dmPolicy`
- `channels.feishu.groupPolicy`
- `channels.feishu.requireMention`
- `channels.discord.enabled`
- `channels.discord.token`
- `channels.discord.dmPolicy`
- `channels.discord.groupPolicy`
- `channels.discord.requireMention`
- `channels.discord.replyToMode`

## Workflow

1. Read `/sdcard/.xomniclaw/xomniclaw.json` first.
2. Update only the requested channel subtree under `channels.*`.
3. Preserve unrelated config.
4. If the user provides credentials, write them exactly.
5. Enable the channel when the user asks to configure it unless they explicitly say not to.
6. After writing config, tell the user what changed.
7. If asked to verify, check the app logs / connection status after config is saved.

## Notes

- Prefer structured config edits over unrelated file changes.
- Do not change model settings in this skill unless the user explicitly asks.
- For Feishu, common required fields are `enabled`, `appId`, `appSecret`.
- For Discord, common required fields are `enabled`, `token`.
---
---
name: model-config
description: Configure model providers and default model selection in X-OmniClaw. Use when the user asks to switch models, set a default model, or add or edit provider configs (including self-hosted OpenAI-compatible endpoints as a new `models.providers` key). For X-OmniClaw, model settings live in /sdcard/.xomniclaw/xomniclaw.json under agent.defaultModel and models.providers.
---

# Model Config

For X-OmniClaw, model configuration is stored in:

- `/sdcard/.xomniclaw/xomniclaw.json`

Relevant paths:

- `agent.defaultModel`
- `models.providers.<providerName>.baseUrl`
- `models.providers.<providerName>.apiKey`
- `models.providers.<providerName>.api`
- `models.providers.<providerName>.authHeader`
- `models.providers.<providerName>.models`

## Workflow

1. Read `/sdcard/.xomniclaw/xomniclaw.json` first.
2. Check current `agent.defaultModel` and existing providers under `models.providers`.
3. If the target provider already exists, update only the necessary fields.
4. If the target provider does not exist, create a provider entry under `models.providers`.
5. For self-hosted or other OpenAI-compatible endpoints, add a **new** `models.providers.<yourKey>` entry (any stable key name) with:
   - `api: "openai-completions"`
   - provider-specific `baseUrl`
   - `apiKey`
   - one or more model entries in `models`
6. Update `agent.defaultModel` to the requested model.
7. Preserve unrelated providers and settings.
8. If asked to verify, send a simple test prompt after config is saved.

## Notes

- Do not overwrite the full config if only one provider/model needs to change.
- If the user provides a compatible endpoint/key, add it as a new provider block under `models.providers` with `api: "openai-completions"` (or another supported API type per app).
- Keep model IDs exact.


**总结 X-OmniClaw 的技能**：

| 技能 | 用途 | 对我们有用吗 |
|------|------|-------------|
| **app-search** 🔍 | Deep Link 搜索（拼多多、美团、抖音、小红书、B站等） | ✅ 已经学过，整合到我们的技能了 |
| **taobao-search** 🛒 | 淘宝搜索 + 解析商品信息 | ✅ 可以学 |
| **capcut-theme-video** 🎬 | 剪映一键成片，从相册选图 | 🤔 需要相册操作工具 |
| **gallery-memory** 🖼 | 扫描相册照片，提取记忆 | 🤔 需要视觉模型 |
| **gallery-qa** 🖼 | 基于相册记忆回答问题 | 🤔 依赖 gallery-memory |
| **memory-evolution** 🧠 | 任务记忆演化，更新用户画像 | 🤔 X-OmniClaw 专用 |
| **scheduled-automation** ⏰ | 定时自动化任务 | ✅ 思路可以借鉴 |
| **skill-creator** | 创建/编辑技能 | 📝 参考价值 |
| **channel-config** | 配置飞书/Discord | ❌ X-OmniClaw 专用 |
| **model-config** | 配置模型 | ❌ X-OmniClaw 专用 |

**最有价值的**：
1. **app-search** — Deep Link 路由表（已学）
2. **taobao-search** — 淘宝搜索 + snapshot 解析商品
3. **scheduled-automation** — 定时任务的设计思路

要不要我把淘宝搜索的 Deep Link 也加到我们的技能里？