Zorv AI 终端:基于 proot 沙箱的 Android 终端模拟器架构全解析

25 阅读15分钟

1. 引言

Zorv AI 终端是一个完整的 Android 终端模拟器,集成在 Zorv AI 应用中。它不仅仅是一个简单的命令行界面,而是一个具备以下核心能力的完整终端系统:

  • 真实 Linux 用户空间:基于 proot + Ubuntu 24.04 ARM64,提供完整的 Linux 工具链(Python、apt、bash 等)
  • PTY 伪终端:标准的 /dev/ptmx 伪终端实现,支持交互式 shell 会话
  • 前台服务保活:通过 Android Foreground Service 让终端会话脱离 UI 生命周期,息屏/切换应用不被杀死
  • ACI 跨进程调用:12 个标准化能力,支持其他应用通过 AIDL/HTTP/MCP 调用终端
  • 多种 IPC 接入:ContentProvider、Deep Link、Intent、BroadcastReceiver 四种标准 Android IPC 方式
  • 多会话管理:支持同时运行多个独立的终端会话
  • 开机自启动:设备重启后自动恢复终端保活

开源地址:github.com/Quor-a/Zorv…


2. 架构设计

2.1 整体架构图

┌─────────────────────────────────────────────────────────────────────┐
│                          终端 UI 层                                  │
│  ChatScreen 输入框「+」→ 终端  /  AI 调用 ui_open_terminal           │
└───────────────────────────┬─────────────────────────────────────────┘
                            │
┌───────────────────────────▼─────────────────────────────────────────┐
│                      QuroTerminalController                         │
│  会话管理 · 命令路由 · proot/设备shell 自动选择 · 超时控制            │
└───┬──────────────────────┬──────────────────────┬───────────────────┘
    │                      │                      │
┌───▼───────────┐  ┌───────▼────────┐  ┌─────────▼──────────────────┐
│QuroShellSession│  │QuroLinuxEnv    │  │QuroTerminalSessionManager  │
│PTY 会话载体    │  │proot+Ubuntu    │  │多会话管理·跨进程访问        │
│/dev/ptmx      │  │24.04 ARM64     │  │默认/额外/UI/历史会话        │
│fork/exec      │  │rootfs下载/解压  │  │会话隔离·状态同步            │
└───┬───────────┘  └───────┬────────┘  └─────────┬──────────────────┘
    │                      │                      │
┌───▼──────────────────────▼──────────────────────▼───────────────────┐
│                       前台服务层(保活)                               │
│   QuroTerminalKeepAliveService · QuroTerminalAciService             │
│   specialUse 前台服务 · shell 子进程归属服务进程 · 15 秒巡检          │
└───┬──────────────────────┬──────────────────────┬───────────────────┘
    │                      │                      │
┌───▼───────────┐  ┌───────▼────────┐  ┌─────────▼──────────────────┐
│ACI 跨进程     │  │Intent/Provider │  │BroadcastReceiver/DeepLink  │
│12 个能力      │  │ContentProvider │  │6 个广播 Action              │
│AIDL 绑定     │  │content:// URI  │  │quro://terminal/...         │
└───────────────┘  └────────────────┘  └────────────────────────────┘

2.2 设计原则

原则说明
进程归属shell 子进程必须归属前台服务进程,服务存活 = 终端存活
环境自动选择优先使用 proot Linux 环境,不可用时自动降级到设备 shell
多会话隔离每个终端会话独立运行,互不干扰
跨进程标准化通过 ACI 协议和标准 Android IPC 暴露能力
Android 版本兼容支持 Android 8.0(API 26)到 Android 15+

2.3 技术选型

技术选择原因
Linux 用户空间proot + Ubuntu 24.04 ARM64无需 ROOT,完整 Ubuntu 工具链
伪终端/dev/ptmx PTY标准 Linux 伪终端,支持交互式 shell
前台服务specialUse 类型Android 14+ 兼容,无需真实数据同步活动
跨进程通信ACI AIDL + HTTP + MCP多种调用方式,适配不同场景
IPC 接入Provider + DeepLink + Intent + Broadcast标准 Android IPC,无需特殊权限
会话管理SessionManager 单例统一管理所有会话,支持跨进程访问

3. 核心组件详解

3.1 QuroTerminalController — 终端控制器

文件位置:app/src/main/java/com/ai/assistance/quro/core/terminal/QuroTerminalController.kt

职责:终端的核心控制器,负责命令路由、环境选择、超时控制。

核心能力:

// 命令执行入口
fun runCommand(command: String, timeout: Long = 14000): String {
    val env = QuroLinuxEnv.getInstance(context)
    return if (env.isReady()) {
        // Linux 环境可用 → 使用 proot
        runCommandInLinux(command, timeout)
    } else {
        // 回退到设备 shell
        runCommandInDeviceShell(command, timeout)
    }
}

关键特性:

  • 环境自动检测:检查 QuroLinuxEnv.isReady(),自动选择 proot 或设备 shell
  • 超时控制:默认 14 秒超时,防止命令挂起
  • 错误处理:捕获所有异常,返回可读错误信息
  • 进程管理:管理子进程生命周期,支持中断执行

3.2 QuroShellSession — PTY Shell 会话载体

文件位置:app/src/main/java/com/ai/assistance/quro/core/terminal/QuroShellSession.kt

职责:PTY 伪终端的完整实现,管理 shell 进程的创建、通信和销毁。

核心能力:

// 会话创建
companion object {
    suspend fun create(
        context: Context,
        env: Map<String, String>,
        name: String = "default",
        onOutput: (String) -> Unit
    ): QuroShellSession {
        // 1. 打开伪终端主设备
        val masterFd = Os.open("/dev/ptmx", O_RDWR or O_NOCTTY)
        // 2. 授权并解锁从设备
        Os.grantpt(masterFd)
        Os.unlockpt(masterFd)
        // 3. 获取从设备名
        val slaveName = Os.slavename(masterFd)
        // 4. 打开从设备
        val slaveFd = Os.open(slaveName, O_RDWR or O_NOCTTY)
        // 5. 设置窗口大小
        val winsize = Winsize(24, 80, 0, 0)
        Os.ioctl(masterFd, TIOCSWINSZ, winsize)
        // 6. fork 子进程
        val pid = fork()
        if (pid == 0) {
            // 子进程:重定向标准 I/O 到从设备
            Os.dup2(slaveFd, 0)
            Os.dup2(slaveFd, 1)
            Os.dup2(slaveFd, 2)
            Os.execve("/bin/sh", arrayOf("/bin/sh"), envp)
        }
        // 7. 父进程:创建会话对象
        return QuroShellSession(masterFd, pid, name, onOutput)
    }
}

关键特性:

  • PTY 伪终端:标准 /dev/ptmx 实现,支持交互式 shell
  • 进程控制:fork/exec 创建子进程,TIOCSWINSZ 设置窗口大小
  • 输出流读取:异步读取子进程输出,通过回调传递给 UI
  • 会话状态:跟踪进程 PID、运行状态、启动时间
  • 会话销毁:发送 SIGTERM/SIGKILL 信号,清理资源

3.3 QuroTerminalSessionManager — 多会话管理器

文件位置:app/src/main/java/com/ai/assistance/quro/core/terminal/QuroTerminalSessionManager.kt

职责:统一管理所有终端会话,支持多会话并发和跨进程访问。

核心能力:

// 会话管理器(单例)
object QuroTerminalSessionManager {
    // 会话存储
    private val sessions = mutableMapOf<String, QuroShellSession>()

    // 创建会话
    suspend fun createSession(
        context: Context,
        name: String = "session_${System.currentTimeMillis()}",
        installIfMissing: Boolean = false
    ): QuroShellSession {
        val env = QuroLinuxEnv.getInstance(context)
        if (installIfMissing && !env.isReady()) {
            env.ensureInstalled(context)
        }
        val session = QuroShellSession.create(context, env.getEnv(), name) { output ->
            // 输出回调
        }
        sessions[session.id] = session
        return session
    }

    // 获取会话
    fun getSession(sessionId: String): QuroShellSession? = sessions[sessionId]

    // 列出所有会话
    fun listSessions(): List<Map<String, Any>> {
        return sessions.map { (id, session) ->
            mapOf(
                "id" to id,
                "name" to session.name,
                "is_alive" to session.isAlive(),
                "pid" to session.pid,
                "uptime" to session.getUptime()
            )
        }
    }

    // 销毁会话
    fun destroySession(sessionId: String): Boolean {
        val session = sessions.remove(sessionId) ?: return false
        session.destroy()
        return true
    }
}

会话类型:

类型说明生命周期
默认会话主终端界面使用的会话前台服务保活,最长生命周期
额外会话用户手动创建的会话跟随应用进程
UI 会话终端 UI 界面的会话跟随 UI 生命周期
历史会话已结束的会话记录仅保留状态信息

3.4 QuroLinuxEnv — Linux 环境后端

文件位置:app/src/main/java/com/ai/assistance/quro/core/linux/QuroLinuxEnv.kt

职责:管理 proot + Ubuntu 24.04 ARM64 用户空间,提供 Linux 环境的安装、检测和配置。

核心能力:

class QuroLinuxEnv(private val context: Context) {
    // 路径配置(全动态,无 hardcode)
    private val rootfsPath = File(context.filesDir, "linux-sandbox/rootfs")
    private val prootPath = "${context.applicationInfo.nativeLibraryDir}/libproot.so"
    private val homePath = context.getExternalFilesDir(null)

    // 检测 Linux 环境是否就绪
    fun isReady(): Boolean {
        return rootfsPath.exists() &&
               File(rootfsPath, "usr/bin/sh").exists() &&
               File(prootPath).exists()
    }

    // 安装 Linux 环境(下载 rootfs)
    suspend fun ensureInstalled(context: Context) {
        if (isReady()) return
        // 从 Ubuntu 官方镜像下载 rootfs
        downloadRootfs(context)
        // 解压 rootfs
        extractRootfs(context)
        // 配置 apt 源
        configureAptSources()
    }

    // 获取 proot 启动参数
    fun getProotArgs(): List<String> {
        return listOf(
            "-0", "root",
            "--link2symlink",
            "-w", "/root",
            "--bind=/proc",
            "--bind=/sys",
            "--bind=/dev",
            "--bind=/sdcard:/mnt/sdcard"
        )
    }

    // 获取环境变量
    fun getEnv(): Map<String, String> {
        return mapOf(
            "HOME" to "/root",
            "PATH" to "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
            "TERM" to "xterm-256color",
            "LANG" to "en_US.UTF-8",
            "TMPDIR" to "/tmp"
        )
    }
}

技术细节:

项目说明
rootfs 格式ubuntu-noble-aarch64-pd-v4.18.0.tar.xz(Ubuntu 24.04 Noble ARM64)
rootfs 大小约 80MB(压缩后),解压后约 300MB
下载源Ubuntu 官方镜像(aliyun / tuna / cdimage)
内置工具proot(.so 形式)、libbash、libbusybox
路径rootfsPath=File(context.filesDir,"linux-sandbox"),全动态无 hardcode

3.5 QuroTerminalKeepAliveService — 前台保活服务

文件位置:app/src/main/java/com/ai/assistance/quro/service/QuroTerminalKeepAliveService.kt

职责:以前台服务身份存活,让终端会话脱离 UI 生命周期,息屏/切 App 不被杀。

核心原理:

前台服务调 startForeground()
    → 系统不杀这个进程
    → 进程内 fork 的 shell 子进程也不会被杀
    → 息屏/切 App 不死

核心代码:

class QuroTerminalKeepAliveService : Service() {
    private var heldSession: QuroShellSession? = null  // 服务直接持有终端会话

    override fun onCreate() {
        // 启动前台服务(specialUse 类型)
        startForeground(NOTIF_ID, buildNotification("终端运行中…"),
            ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)

        // 启动巡检循环
        startLoop()
    }

    private fun ensureSessionSafe() {
        // 在服务进程内创建 shell 子进程
        heldSession = QuroShellSession.create(
            context = this,
            env = QuroLinuxEnv.getInstance(this).getEnv(),
            name = "keepalive"
        ) { output ->
            // 输出回调
        }
        // shell 子进程是服务进程的 fork → 服务存活 = shell 子进程存活
    }

    private fun startLoop() {
        // 每 15 秒巡检
        coroutineScope.launch {
            while (isActive) {
                ensureSessionSafe()  // 确保会话存活
                ensureAciService()  // 确保 ACI 服务运行
                updateNotification() // 更新通知
                delay(15_000)       // 15 秒间隔
            }
        }
    }
}

关键特性:

特性实现
前台服务类型specialUse(Android 14+ 兼容)
巡检间隔每 15 秒检查会话状态,死亡自动重建
通知栏常驻「Zorv AI 终端运行中」,点击跳转主界面
会话所有权shell 子进程归属服务进程,服务存活 = 终端存活
ACI 服务管理自动启动/重启 QuroTerminalAciService

3.6 QuroTerminalAciService — ACI 受控端服务

文件位置:app/src/main/java/com/ai/assistance/quro/service/QuroTerminalAciService.kt

职责:继承 BaseAidlAciService,暴露终端全部能力给外部应用调用。

12 个 ACI 能力:

能力入参返回说明
execcommand(必填) / timeout(可选) / session_id(可选)output / exit_code / error执行命令
create_sessionname(可选)session_id / name创建会话
destroy_sessionsession_id(必填)destroyed销毁会话
send_inputsession_id(必填) / input(必填)sent发送输入
get_session_statussession_id(必填)session_id / is_alive / pid / uptime会话状态
list_sessions—sessions (array)列出所有会话
set_session_envsession_id / key / valueset设置环境变量
get_session_envsession_id / keyvalue获取环境变量
list_capabilities—capabilities (array)列出能力
get_service_status—running / session_count / uptime服务状态
get_audit_loglimit(可选)logs (array)审计日志
help—help_text帮助信息

4. Linux 沙箱(proot + Ubuntu 24.04 ARM64)

4.1 架构原理

┌─────────────────────────────────────────┐
│           Android 应用进程                │
│                                         │
│  ┌─────────────────────────────────┐    │
│  │         proot 进程               │    │
│  │  ┌─────────────────────────┐    │    │
│  │  │    Ubuntu 24.04 ARM64   │    │    │
│  │  │    rootfs 用户空间       │    │    │
│  │  │                         │    │    │
│  │  │  /bin/sh  ← 终端 shell  │    │    │
│  │  │  /usr/bin/python3       │    │    │
│  │  │  /usr/bin/apt           │    │    │
│  │  │  /usr/bin/bash          │    │    │
│  │  └─────────────────────────┘    │    │
│  │                                 │    │
│  │  --bind=/proc  ← 挂载 proc     │    │
│  │  --bind=/sys   ← 挂载 sys      │    │
│  │  --bind=/dev   ← 挂载 dev      │    │
│  │  --link2symlink ← 符号链接兼容  │    │
│  └─────────────────────────────────┘    │
│                                         │
│  proot 路径: nativeLibraryDir/libproot.so │
│  rootfs: filesDir/linux-sandbox/rootfs  │
└─────────────────────────────────────────┘

4.2 rootfs 管理

  • 格式:ubuntu-noble-aarch64-pd-v4.18.0.tar.xz
  • 大小:约 80MB(压缩后),解压后约 300MB
  • 下载源:Ubuntu 官方镜像(aliyun / tuna / cdimage)
  • 路径:filesDir/linux-sandbox/rootfs,全动态无 hardcode

4.3 内置工具链

工具说明
proot以 .so 形式内置,通过 nativeLibraryDir 加载
libbashBash shell 支持
libbusybox精简 Unix 工具集
python3Python 3 运行时
apt包管理器,可安装更多工具

4.4 CMS 运行时集成

# 通过 bootstrap 脚本安装运行时
bootstrap.sh --install NODE    # Node.js
bootstrap.sh --install PYTHON  # Python
bootstrap.sh --install RUST    # Rust
bootstrap.sh --install GO      # Go
bootstrap.sh --install JAVA    # Java
bootstrap.sh --install SSH     # OpenSSH

运行时共享:所有终端会话共享同一套 CMS 运行时,无需重复安装。


5. 前台服务保活机制

5.1 为什么需要前台服务

Android 系统会在以下情况杀死后台进程:

  • 内存不足:系统会回收低优先级进程
  • 电量优化:Doze 模式限制后台活动
  • 用户手动清理:从最近任务列表滑动清除
  • 应用切换:切到其他应用后,原应用可能被杀死

前台服务是 Android 提供的最高优先级服务类型,系统几乎不会杀死前台服务进程。因此,让终端会话归属于前台服务进程,就能保证息屏/切 App 不被杀。

5.2 specialUse 类型选择

Android 14+ 对前台服务类型有严格限制:

类型限制适用场景
dataSync必须有真实数据同步活动文件同步、云备份
mediaPlayback必须正在播放媒体音乐播放器
location必须正在获取位置导航应用
specialUse需要 <property> 标签说明用途终端保活(我们的选择)

选择 specialUse 的原因:

  1. 终端保活不属于 dataSync(没有真实数据同步)
  2. specialUse 需要 <property> 标签说明用途,Google Play 审核时会检查
  3. 添加 <property> 后,startForeground() 不会被系统静默拒绝

5.3 会话所有权模型

传统模型(不保活):
App 进程 → fork shell 子进程 → App 被杀 → 子进程也被杀

前台服务模型(保活):
App 进程 → 前台服务 → fork shell 子进程
    ↑
    系统不杀这个进程
    → shell 子进程也不会被杀
    → 息屏/切 App 不死

关键代码:

// QuroTerminalKeepAliveService.kt
private var heldSession: QuroShellSession? = null

private fun ensureSessionSafe() {
    // 在服务进程内创建 shell 子进程
    heldSession = QuroShellSession.create(
        context = this,
        env = QuroLinuxEnv.getInstance(this).getEnv(),
        name = "keepalive"
    ) { output ->
        // 输出回调
    }
    // shell 子进程是服务进程的 fork
    // 服务存活 = shell 子进程存活
}

5.4 巡检与自愈机制

private fun startLoop() {
    coroutineScope.launch {
        while (isActive) {
            // 1. 检查会话是否存活
            val session = heldSession
            if (session == null || !session.isAlive()) {
                Log.w(TAG, "会话已死亡,重建中…")
                ensureSessionSafe()
            }

            // 2. 检查 ACI 服务是否运行
            ensureAciService()

            // 3. 更新通知
            updateNotification()

            // 4. 等待 15 秒
            delay(15_000)
        }
    }
}

5.5 Android 14+ 兼容性

Manifest 配置:

<!-- 权限声明 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />

<!-- 服务声明 -->
<service
    android:name=".service.QuroTerminalKeepAliveService"
    android:exported="false"
    android:foregroundServiceType="specialUse">
    <property
        android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
        android:value="终端会话保活:保持终端会话在息屏/切换应用时不被杀死" />
</service>

启动代码:

override fun onCreate() {
    super.onCreate()
    try {
        startForeground(
            NOTIF_ID,
            buildNotification("终端运行中…"),
            ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
        )
    } catch (e: Throwable) {
        Log.e(TAG, "前台通知创建失败", e)
        stopSelf()
        return
    }
}

5.6 开机自启动

// QuroTerminalBootReceiver.kt
class QuroTerminalBootReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
            // 开机自启动前台服务
            val serviceIntent = Intent(context, QuroTerminalKeepAliveService::class.java)
            context.startForegroundService(serviceIntent)
        }
    }
}

Manifest 注册:

<receiver
    android:name=".service.QuroTerminalBootReceiver"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
    </intent-filter>
</receiver>

6. PTY 伪终端实现

6.1 伪终端工作原理

┌─────────────────────────────────────────────────┐
│                  终端 UI 进程                     │
│                                                 │
│  ┌─────────────────────────────────────────┐    │
│  │         master fd(主设备)               │    │
│  │  /dev/ptmx                              │    │
│  └──────────────────┬──────────────────────┘    │
│                     │                           │
│                     │ 内核伪终端驱动              │
│                     │                           │
│  ┌──────────────────▼──────────────────────┐    │
│  │         slave fd(从设备)                │    │
│  │  /dev/pts/N                             │    │
│  └──────────────────┬──────────────────────┘    │
│                     │                           │
│                     │ dup2 重定向                │
│                     │                           │
│  ┌──────────────────▼──────────────────────┐    │
│  │         shell 子进程                     │    │
│  │  /bin/sh                                │    │
│  │  stdin  ← slave fd                      │    │
│  │  stdout → slave fd                      │    │
│  │  stderr → slave fd                      │    │
│  └─────────────────────────────────────────┘    │
└─────────────────────────────────────────────────┘

6.2 核心代码实现

// QuroShellSession.kt 核心创建逻辑
val masterFd = Os.open("/dev/ptmx", O_RDWR or O_NOCTTY)
val slaveName = Os.slavename(masterFd)
Os.grantpt(masterFd)
Os.unlockpt(masterFd)
val slaveFd = Os.open(slaveName, O_RDWR or O_NOCTTY)

// 设置窗口大小
val winsize = Winsize(24, 80, 0, 0)
Os.ioctl(masterFd, TIOCSWINSZ, winsize)

// 创建新会话(必须)
Os.setsid()

// fork 子进程
val pid = Os.fork()
if (pid == 0) {
    // 子进程
    Os.dup2(slaveFd, 0)  // stdin
    Os.dup2(slaveFd, 1)  // stdout
    Os.dup2(slaveFd, 2)  // stderr
    Os.execve("/bin/sh", arrayOf("/bin/sh"), envp)
}

6.3 输出流处理

// 异步读取输出
private fun readOutputLoop() {
    val buffer = ByteArray(4096)
    while (isRunning) {
        val bytesRead = Os.read(masterFd, buffer)
        if (bytesRead > 0) {
            val output = String(buffer, 0, bytesRead)
            onOutput(output)  // 回调给 UI
        } else if (bytesRead == 0) {
            break  // EOF
        }
    }
}

7. 跨进程接入方式

7.1 ContentProvider(TerminalProvider)

Authority:content://com.ai.assistance.quro.terminal

支持的路径:

路径方法说明
/sessionsquery列出所有会话
/exec?cmd=...query执行命令并返回结果
/statusquery获取服务状态
/session/{id}query获取指定会话信息

使用示例:

// 列出会话
val cursor = contentResolver.query(
    Uri.parse("content://com.ai.assistance.quro.terminal/sessions"),
    null, null, null, null
)

// 执行命令
val cursor = contentResolver.query(
    Uri.parse("content://com.ai.assistance.quro.terminal/exec?cmd=uname -a"),
    null, null, null, null
)

7.2 Deep Link(TerminalDeepLinkHandler)

Scheme:quro://terminal/...

支持的路径:

路径说明
exec?cmd=...执行命令
sessions会话列表
create?name=...创建会话
status服务状态

使用示例:

// 执行命令
val intent = Intent(Intent.ACTION_VIEW,
    Uri.parse("quro://terminal/exec?cmd=python3 --version"))
startActivity(intent)

// 创建会话
val intent = Intent(Intent.ACTION_VIEW,
    Uri.parse("quro://terminal/create?name=my-session"))
startActivity(intent)

7.3 Intent Handler(TerminalIntentHandler)

支持的 Action:

ActionExtra说明
com.ai.assistance.quro.action.TERMINAL_EXECcommand执行命令
com.ai.assistance.quro.action.TERMINAL_STATUS—获取状态
com.ai.assistance.quro.action.TERMINAL_SESSIONS—列出会话
com.ai.assistance.quro.action.TERMINAL_CREATE_SESSIONname创建会话

使用示例:

// 执行命令
val intent = Intent("com.ai.assistance.quro.action.TERMINAL_EXEC")
intent.putExtra("command", "ls -la /home")
sendBroadcast(intent)

7.4 BroadcastReceiver(TerminalBroadcastReceiver)

支持的 Action(6 个):

ActionExtra返回
TERMINAL_EXECcommandoutput / exit_code / error
TERMINAL_STATUS—running / session_count
TERMINAL_SESSIONS—sessions (array)
TERMINAL_CREATE_SESSIONnamesession_id
TERMINAL_DESTROY_SESSIONsession_iddestroyed
TERMINAL_SEND_INPUTsession_id / inputsent

使用示例:

// 发送广播并接收结果
val receiver = object : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        val output = intent.getStringExtra("output")
        val exitCode = intent.getIntExtra("exit_code", -1)
        Log.d("Terminal", "Output: $output, Exit: $exitCode")
    }
}

registerReceiver(receiver, IntentFilter("com.ai.assistance.quro.action.TERMINAL_RESULT"))

val intent = Intent("com.ai.assistance.quro.action.TERMINAL_EXEC")
intent.putExtra("command", "echo hello")
sendBroadcast(intent)

7.5 对比与选型

方式适用场景优点缺点
ContentProvider数据查询、跨应用数据共享标准 Android API,支持 CRUD不适合长时间运行的命令
Deep Link用户点击链接触发操作直观,支持 URL 分享不适合后台调用
Intent应用间简单通信简单,支持 extras结果返回需要额外机制
BroadcastReceiver异步通知、事件驱动解耦,支持一对多结果返回需要额外注册
ACI复杂跨进程调用12 个标准化能力,支持 AIDL/HTTP/MCP需要绑定服务

8. 命令执行路由

8.1 环境自动检测

// QuroTerminalController.kt
fun runCommand(command: String, timeout: Long = 14000): String {
    val env = QuroLinuxEnv.getInstance(context)
    return if (env.isReady()) {
        // Linux 环境可用 → 使用 proot
        runCommandInLinux(command, timeout)
    } else {
        // 回退到设备 shell
        runCommandInDeviceShell(command, timeout)
    }
}

8.2 proot 命令构建

private fun runCommandInLinux(command: String, timeout: Long): String {
    val prootPath = "${applicationInfo.nativeLibraryDir}/libproot.so"
    val prootArgs = QuroLinuxEnv.getInstance(context).getProotArgs()
    // 直接使用 prootArgs + 命令,不重复添加参数
    val fullCommand = listOf(prootPath) + prootArgs + listOf("/bin/sh", "-c", command)

    val process = ProcessBuilder(fullCommand)
        .redirectErrorStream(true)
        .start()

    // 超时控制
    val completed = process.waitFor(timeout, TimeUnit.MILLISECONDS)
    if (!completed) {
        process.destroyForcibly()
        throw TimeoutException("命令执行超时: $command")
    }

    return process.inputStream.bufferedReader().readText()
}

8.3 超时与错误处理

// 超时控制
val completed = process.waitFor(timeout, TimeUnit.MILLISECONDS)
if (!completed) {
    process.destroyForcibly()
    throw TimeoutException("命令执行超时: $command")
}

// 错误处理
try {
    val result = runCommand(command, timeout)
    return mapOf(
        "output" to result,
        "exit_code" to 0,
        "error" to ""
    )
} catch (e: TimeoutException) {
    return mapOf(
        "output" to "",
        "exit_code" to -1,
        "error" to "命令执行超时"
    )
} catch (e: Exception) {
    return mapOf(
        "output" to "",
        "exit_code" to -1,
        "error" to e.message ?: "未知错误"
    )
}

9. 终端 UI 集成

9.1 入口方式

入口说明
对话框输入框「+」点击输入框左侧「+」按钮,选择「终端」
AI 调用 ui_open_terminalAI 在对话中主动打开终端界面
Deep Linkquro://terminal/exec?cmd=... 直接启动
ACI 跨进程调用其他应用通过 ACI 协议调用终端能力

9.2 会话切换

// QuroTerminalSessionManager.kt
// 创建新会话
val newSession = QuroTerminalSessionManager.createSession(context, "my-session")

// 切换到指定会话
val session = QuroTerminalSessionManager.getSession("session-id")

// 销毁会话
QuroTerminalSessionManager.destroySession("session-id")

9.3 输入输出渲染

终端 UI 使用 Jetpack Compose 实现:

@Composable
fun TerminalScreen() {
    val output = remember { mutableStateListOf<String>() }

    LaunchedEffect(Unit) {
        // 启动终端会话
        val session = QuroTerminalSessionManager.createSession(context) { line ->
            output.add(line)
        }
    }

    LazyColumn {
        items(output) { line ->
            Text(
                text = line,
                fontFamily = FontFamily.Monospace,
                fontSize = 12.sp
            )
        }
    }
}

10. 关键特性总结

特性状态说明
真实用户空间✅Ubuntu 24.04 ARM64,完整的 Linux 工具链
PTY 伪终端✅/dev/ptmx + fork/exec + TIOCSWINSZ
前台服务保活✅specialUse 类型,息屏/切 App 不被杀
ACI 跨进程✅12 个能力,AIDL/HTTP/MCP 三种调用方式
Intent/Provider✅ContentProvider + Deep Link + Intent + BroadcastReceiver
多会话支持✅默认/额外/UI/历史会话,会话隔离
开机自启动✅BOOT_COMPLETED 广播接收器
Android 14+ 兼容✅specialUse + <property> 标签
proot 沙箱✅无需 ROOT,link2symlink 符号链接
CMS 运行时✅NODE/PYTHON/RUST/GO/JAVA 共享环境
超时控制✅默认 14 秒超时,防止命令挂起
错误处理✅完善的异常捕获和错误返回

11. 常见问题与故障排除

现象说明 / 处理
终端息屏/切 App 后被杀确认前台服务已启动:通知栏应显示「Zorv AI 终端运行中」。检查 AndroidManifest 中 foregroundServiceType="specialUse" 和 <property> 标签
终端 ACI 跨进程调用失败检查 QuroTerminalAciService 是否在 Manifest 中注册,权限 ai.aci.permission.CALL 是否声明
终端 Intent/Provider 不响应检查 TerminalProvider、TerminalBroadcastReceiver、TerminalDeepLinkHandler 是否在 Manifest 中注册
终端会话状态不一致QuroTerminalSessionManager 管理多会话,调用 listSessions() 获取真实状态
终端命令执行报 Illegal option -0proot 参数重复问题,更新到 v1.0.67+ 已修复
应用内 Linux(L5)无法运行首次进入终端会提示「安装 Linux 环境」,rootfs 需联网从 Ubuntu 官方镜像下载
命令执行超时默认 14 秒超时,可通过 timeout 参数调整
会话输出乱码检查 TERM 环境变量是否设置为 xterm-256color

12. 开发指南

12.1 环境准备

  • Android Studio:2024.1+ (Koala)
  • JDK:17+
  • Android SDK:compileSdk 36, minSdk 26, targetSdk 34
  • Gradle:8.13+
  • Kotlin:2.3+

12.2 从源码构建

# 克隆仓库
git clone https://github.com/Quor-a/ZorvAI.git
cd ZorvAI

# 构建 full release APK
./gradlew :app:assembleFullRelease

# 输出路径
# app/build/outputs/apk/full/release/app-full-release.apk

12.3 添加新的终端能力

步骤 1:在 QuroTerminalAciService.kt 的 onCreateCapabilities 中添加能力定义

override fun onCreateCapabilities(): List<AidlAciCapability> {
    val caps = super.onCreateCapabilities().toMutableList()
    caps.add(AidlAciCapability("my_new_capability", "我的新能力"))
    return caps
}

步骤 2:在 onCall 中添加能力处理逻辑

override fun onCall(request: AidlAciRequest): AidlAciResponse {
    return when (request.capability) {
        "my_new_capability" -> handleMyNewCapability(request.params)
        // ... 其他能力
    }
}

12.4 测试建议

功能测试:

  • 测试 exec 能力:执行简单命令、复杂命令、带超时的命令
  • 测试 list_sessions 能力:查看会话状态
  • 测试 help 能力:获取帮助信息

跨进程测试:

  • 测试其他应用通过 ACI 调用终端
  • 测试 Intent/Provider/BroadcastReceiver 接入

前台服务测试:

  • 测试息屏后服务是否持续运行
  • 测试切 App 后服务是否持续运行
  • 测试通知栏是否显示「终端运行中」

Linux 环境测试:

  • 测试 proot 环境是否正常工作
  • 测试设备 shell 回退机制
  • 测试 rootfs 下载和解压

13. 开源信息

项目信息
开源地址github.com/Quor-a/Zorv…
ACI 开发者手册docs/ACI_DEVELOPER_GUIDE.md
当前版本v1.0.67
许可证Apache License 2.0
技术栈Kotlin 2.3 + Jetpack Compose
最低支持Android 8.0 (API 26)
目标版本Android 14 (API 34)

本文档由 Zorv AI 开发团队维护。如有问题或建议,请在 GitHub Issues 反馈。