本文深入剖析开源项目 Swees 的技术架构,探讨如何在无 root 的 Android 手机上构建一个真正能「动手」的 AI Agent。

一、引言:AI 只能说,不能做?

你大概有过这样的体验:在手机上问 AI「帮我写个 Python 脚本处理 CSV」,AI 给你一段漂亮的代码,然后呢?你得手动复制、打开电脑、装环境、跑代码、遇到报错再回去问 AI……

手机AI 停留在「说」的阶段,无法「做」。

桌面端的 Claude Code、Cursor、Devin 已经给出了答案——让 AI 直接操作终端。但在移动端,这个领域几乎是空白的。原因很简单:手机上没有可用的 Linux 环境,没有 root 权限,没有成熟的终端模拟方案。

Swees 就是为了填这个空白。

它是一个运行在 Android 手机上的开源 AI Agent 应用。通过 PRoot 技术在手机内嵌完整的 Ubuntu 24.04 环境,AI 可以直接执行终端命令、编辑代码、管理文件——无需 root,无需电脑,一切在手机上完成。

二、项目概览

一句话定位:Swees 是一个 Android 原生 AI Agent 应用,内置 Ubuntu 终端,让 AI 从「对话」走向「行动」。

核心卖点

特性 说明
真正的命令执行 AI 自主调用 Function Calling,在真实 Ubuntu 环境中执行 shell 命令
无需 root 基于 PRoot 技术,普通 Android 手机即可运行
多智能体协作 多个 Agent 各自独立配置,通过 IPC 跨话题通信
跨对话记忆 SQLite + FTS4 全文搜索,AI 记住你说过的一切
零框架前端 纯 HTML/CSS/JS,~12K 行代码实现完整 SPA

AI 编写五子棋 APP
AI 编写五子棋 APP

手机上编译
手机上编译

安装,全程在手机上完成
安装,全程在手机上完成
(甚至gif都是AI做的)


三、技术架构总览

Swees 采用经典的三层架构,每层职责清晰、边界明确:

┌──────────────────────────────────────────────────┐
│           WebView (HTML / CSS / Vanilla JS)        │  前端 UI · ~12K 行
│                    ↓ SweesBridge                   │
├──────────────────────────────────────────────────┤
│              Java Bridge Layer                     │  JS ↔ Java 桥接
│    ┌───────────┬───────────┬────────────────────┐ │
│    │  AI 引擎   │  PRoot    │    Storage         │ │
│    │  SSE 流式  │  Ubuntu   │    SQLite          │ │
│    │  OpenAI    │  Session  │    13 张表         │ │
│    │  兼容协议   │  Pool     │    + FTS4 全文索引  │ │
│    └───────────┴───────────┴────────────────────┘ │
│           Native Layer (C / .so)                   │  linkhook 等
├──────────────────────────────────────────────────┤
│              Android OS (arm64)                    │
└──────────────────────────────────────────────────┘

各层职责

  • 前端 UI 层:WebView 承载的单页应用,纯 HTML + CSS + 原生 JavaScript,无任何前端框架。负责聊天界面渲染、Markdown 解析、手势交互、Git 面板等全部 UI 逻辑。
  • Java 桥接层:核心是 SweesBridge,暴露 71 个 @JavascriptInterface 方法,是 JS 与 Java 之间唯一的通信通道。AI 引擎、PRoot 终端、SQLite 存储三大子系统在此交汇。
  • Native 层:C 语言编写的 linkhook.c,编译为 .so 动态库,解决 Android 14 SELinux 对硬链接的限制问题。

数据流的关键路径是:用户输入 → JS 调用 Bridge → Java 发起 HTTP 请求 → SSE 流式回调 → JS 实时渲染。当 AI 决定执行命令时,Java 层通过 PRoot 会话池执行命令,结果回传给 AI 进行下一轮推理。


四、关键技术深度解析

4.1 PRoot:无 Root 运行 Ubuntu 的原理与 Android 14 兼容

为什么是 PRoot?

在 Android 上运行 Linux 环境通常有三条路径:

方案 需要 Root 原理 适合 Swees?
Termux 自带包管理器,但非标准 Linux 目录结构 ❌ 目录结构不兼容 apt/dpkg
Docker 容器化 ❌ 需要 root
PRoot 通过 ptrace 拦截系统调用,模拟 chroot + bind mount

PRoot 的核心原理是利用 Linux 的 ptrace() 系统调用拦截目标程序的所有系统调用。当程序尝试访问文件路径时,PRoot 拦截并重写路径——比如把 /usr/bin/python3 重定向到 /data/data/com.swees/files/rootfs/usr/bin/python3。这样,对程序而言,它就运行在一个「真实」的 Ubuntu 根文件系统中。

Swees 在首次使用时自动下载约 29MB 的 Ubuntu 24.04 arm64 rootfs,解压后在 PRoot 中启动完整的 bash 环境,预装 gitpython3sqlite3 等开发工具。

Android 14 兼容:linkhook 的故事

Android 14 收紧了 untrusted_app SELinux 域的安全策略,禁止普通应用调用 link() / linkat() 创建硬链接(返回 EACCES)。这直接导致 PRoot 内的 dpkg 在执行 apt install 时卡死——因为 dpkg 依赖硬链接来做 status 文件的原子备份。

Swees 的解决方案是一段精巧的 C 代码——linkhook.c

/* 拦截 link():先尝试真实硬链接,失败则降级 */
int link(const char *oldpath, const char *newpath) {
    if (real_link(oldpath, newpath) == 0) return 0;
    if (errno == EACCES || errno == EPERM) {
        return link_fallback(oldpath, newpath);  // 降级为 copy → symlink
    }
    return -1;
}

设计要点

  1. LD_PRELOAD 拦截:编译为 .so 后通过 LD_PRELOAD 注入,对 PRoot 内所有程序透明——dpkg、apt 完全无感知。
  2. 三级降级策略:先尝试真实 link() → 失败则 copy_file()(产生独立副本)→ copy 也失败则 symlink() 兜底。
  3. 零依赖:不使用 dlsym(RTLD_NEXT)(避免依赖 libdl),而是直接通过 syscall() 调用原始系统调用,库本身只依赖 libc。

这段代码不到 100 行,却解决了 Android 14 上 PRoot 生态最头疼的兼容性问题。

4.2 SSE 流式输出:从 SseParser 到前端实时渲染

AI 对话的体验好坏,很大程度上取决于流式输出的流畅度。Swees 实现了一套完整的 SSE(Server-Sent Events)解析与渲染链路。

Java 端:SseParser

SseParser 是整个流式输出的核心,负责解析 OpenAI 兼容协议的 SSE 流。它的设计有几个值得注意的工程细节:

public static ParseResult parse(InputStream is,
                                 DeltaConsumer onDelta,
                                 DeltaConsumer onReasoning,
                                 ToolCallDeltaConsumer onToolCallDelta) throws Exception {
    // ...
    while ((line = r.readLine()) != null) {
        if (!line.startsWith("data:")) continue;
        String data = line.substring(5).trim();
        if (data.equals("[DONE]")) break;

        JSONObject chunk = new JSONObject(data);
        JSONObject delta = choices.getJSONObject(0).optJSONObject("delta");
        if (delta != null) {
            // 1. 思维链增量(reasoning_content)
            String reasoning = delta.isNull("reasoning_content") ? "" 
                : delta.optString("reasoning_content", "");
            if (!reasoning.isEmpty()) onReasoning.onDelta(reasoning);

            // 2. 正文增量(content)
            String content = delta.isNull("content") ? "" 
                : delta.optString("content", "");
            if (!content.isEmpty()) { full.append(content); onDelta.onDelta(content); }

            // 3. tool_calls 流式分片(按 index 累积拼接)
            // ...
        }
    }
}

关键设计决策

① 三路并行回调SseParser 同时支持三种增量回调:正文 onDelta、思维链 onReasoning、工具调用 onToolCallDelta。这意味着用户在等待 AI 回复时,不仅能看到正文逐字输出,还能看到 AI 的「思考过程」和「正在准备执行的命令卡片」实时渲染。

② org.json 的 null 陷阱。代码中反复出现 delta.isNull("content") ? "" : delta.optString(...) 这种模式。这是因为 org.jsonoptString() 对 JSON null 值会返回字符串 "null" 而非空字符串——这是一个经典的 Android 开发坑。Swees 在每个字段解析处都做了显式判空。

③ tool_calls 流式分片累积。OpenAI 协议中,tool_callsarguments 字段是分片传输的——首个 chunk 携带 idname,后续 chunk 只携带 arguments 的片段。SseParser 使用 LinkedHashMap<Integer, ToolCall>index 累积拼接:

if (existing == null) {
    // 首个分片:建条目
    toolCallMap.put(idx, new ChatRequest.ToolCall(id, name, argsPart));
} else {
    // 后续分片:拼 arguments
    String mergedArgs = existing.arguments + argsPart;
    toolCallMap.put(idx, new ChatRequest.ToolCall(
        mergedId, mergedName, mergedArgs));
}

④ 容错设计。单行 SSE 解析失败不会中断整个流——catch (JSONException) 后跳过继续,但通过 Log.e 让问题可见。这在网络不稳定或服务商返回格式有微小偏差时至关重要。

前端:实时渲染链路

Java 解析出的增量通过 SweesBridge 回调到 JS 层,前端使用增量追加 + marked.js 实时 Markdown 渲染。整个链路是:

HTTP InputStream → SseParser.parse() → onDelta 回调
  → EventPusher.pushEvent("stream_delta", json)
    → WebView.evaluateJavascript("window.onStreamDelta(...)")
      → DOM 增量更新 + marked.js 渲染

4.3 Function Calling 安全设计:命令危险分级 + 三级权限 + 会话隔离

让 AI 执行命令,安全是第一要务。Swees 构建了一套多层安全体系。

命令危险分级(DangerLevel)

DangerLevel 是一个纯函数式的命令风险评估器,将每条命令分为 LOW(自动执行)和 HIGH(需用户确认)两级:

public enum Level {
    LOW,      // 自动执行
    HIGH      // 需用户确认
}

public static Level assess(String command) {
    String cmd = command.trim();
    if (matches(cmd,
        "rm ", "rmdir ", "dd if=", "mkfs", "shutdown", "reboot",
        "kill ", "sudo ", "chmod ", "chown ",
        "curl ", "wget ", "git push", "git reset", "git rebase",
        "drop table", "delete from", "mv ", "cp "))
        return Level.HIGH;

    // SQL UPDATE 需同时包含 update 和 set,避免误伤 apt update
    String lower = cmd.toLowerCase();
    if (lower.contains("update ") && lower.contains(" set "))
        return Level.HIGH;

    return Level.LOW;
}

设计亮点

  • 覆盖面广:涵盖文件删除、系统操作、权限变更、网络下载、Git 危险操作、数据库写操作等 7 大类高危命令。
  • 精准误伤控制:比如 SQL UPDATE 的判定要求同时包含 updateset,避免把 apt updatepip update 误判为高危。
  • Git 操作细分git pushgit resetgit rebase 等列为 HIGH,但 git statusgit loggit diff 等只读操作为 LOW,自动放行。
三级权限模式

每个 Agent 有独立的 trust_level 字段,默认为 RESTRICTED

级别 行为
RESTRICTED 所有 HIGH 级命令必须用户确认
TRUSTED 放宽部分限制,AI 有更大自主权
FULL 完全自主,无需确认(危险,谨慎使用)
会话隔离

每个 Agent 拥有独立的 PRoot 会话,通过 ProotSessionPool 管理:

public ProotSession getOrCreate(String agentId, String workDir) throws Exception {
    ProotSession session = sessions.get(agentId);
    if (session != null && session.isAlive()) {
        lastActivity.put(agentId, System.currentTimeMillis());
        return session;  // 复用已有会话
    }
    session = new ProotSession(workDir);
    session.start(prootPath, rootfsPath, nativeLibDir, tmpDir);
    sessions.put(agentId, session);
    return session;
}

不同 Agent 的命令在各自的 Ubuntu 会话中执行,工作目录、环境变量、进程空间完全隔离。Agent A 的 cd /project-a 不会影响 Agent B。

4.4 数据层设计:13 张表 + 消息版本控制 + FTS4 记忆搜索

Swees 的数据层基于 SQLite,共 13 张表(含 FTS 虚拟表),覆盖对话、消息、智能体、设置、记忆、IPC 任务等全部业务域。

表结构总览
表名 职责 关键字段
conversations 对话主题 agent_id, starred, cache_hit/miss
messages 消息记录 role, content, tool_call, reasoning, prev_id, is_current, bubble_id
agents 智能体配置 system_prompt, model, provider, session_id, trust_level
settings 全局设置 key-value 结构
i18n 国际化文本 (key, lang) 联合主键
request_logs AI 请求日志 request_body(API Key 脱敏)
ipc_tasks IPC 任务队列 action, status, result
memories 跨对话记忆 content, keywords, category, importance
memories_fts 记忆全文索引 FTS4 虚拟表
消息版本控制:prev_id / is_current / bubble_id

这是 Swees 数据层最精巧的设计之一。在 AI 对话中,同一条用户消息可能触发多次 AI 回复(比如重新生成、Function Calling 多轮调用)。Swees 用三个字段实现了消息的版本树:

  • prev_id:指向父消息的 ID,构建消息版本链。
  • is_current:标记当前显示的版本(1=当前,0=历史)。
  • bubble_id:同一「气泡」(UI 上的一个消息块)的所有版本共享同一个 bubble_id,删除时级联清理。
用户消息 (bubble_id=B1)
├── AI回复v1 (prev_id=user_msg, is_current=0, bubble_id=B2)
└── AI回复v2 (prev_id=user_msg, is_current=1, bubble_id=B2)  ← 当前显示

这种设计让「重新生成」变得简单:新建一条 is_current=1 的消息,把旧版本标记为 is_current=0,UI 只渲染当前版本,但历史版本完整保留。

FTS4 全文搜索:让 AI 拥有记忆

memories 表配合 memories_fts 虚拟表,实现了跨对话的持久化记忆系统。Swees 使用 SQLite 的 FTS4 扩展(standalone 模式,自带数据副本),通过触发器自动同步:

-- 插入记忆时自动同步到 FTS 索引
CREATE TRIGGER memories_ai AFTER INSERT ON memories BEGIN
    INSERT INTO memories_fts(rowid, content, keywords)
    VALUES (new.id, new.content, new.keywords);
END;

-- 全文搜索
SELECT * FROM memories_fts WHERE memories_fts MATCH '用户偏好';

记忆表还设计了 importance(1-10 重要性评分)、access_count(命中次数)、last_accessed(最近命中时间)、expires_at(过期时间)等字段,支持记忆的优先级排序和生命周期管理。

请求日志自动裁剪

request_logs 表通过一个 SQLite 触发器实现自动裁剪,只保留最近 100 条记录:

CREATE TRIGGER trg_request_logs_trim AFTER INSERT ON request_logs BEGIN
    DELETE FROM request_logs WHERE id IN (
        SELECT id FROM request_logs
        ORDER BY timestamp DESC LIMIT -1 OFFSET 100
    );
END

这是一个优雅的设计——不需要 Java 层的定时清理任务,数据库自己搞定。


五、工程决策与思考

为什么不用前端框架?

Swees 的前端是 ~12K 行纯 HTML/CSS/JS,没有 React、Vue、也没有任何构建工具。这个决定并非偷懒,而是深思熟虑的结果:

  1. WebView 性能:移动端 WebView 加载 React/Vue 的 runtime 本身就有开销,而 Swees 的 UI 逻辑用原生 JS 完全可以胜任。
  2. 包体积:零依赖意味着 APK 更小,用户下载更快。
  3. 调试友好:没有构建步骤,改完代码刷新即生效,开发体验极佳。
  4. 可控性:每一行代码都是自己写的,不存在框架黑盒。

为什么选 PRoot 而非 Termux?

Termux 是 Android 上最成熟的 Linux 环境方案,但它有自己的包管理器和非标准目录结构(/data/data/com.termux/files/usr/)。这意味着 aptdpkg 等 Debian 系工具无法直接使用,很多依赖标准 Linux 目录结构的软件包会出问题。

PRoot + Ubuntu rootfs 的方案虽然性能略低(ptrace 拦截有开销),但提供了100% 兼容的 Ubuntu 环境——apt install 开箱即用,所有教程和 StackOverflow 答案都适用。对于 AI Agent 场景,兼容性远比性能重要。

为什么用 vanilla JS 而非 TypeScript?

在 WebView 环境中,TypeScript 需要额外的编译步骤。Swees 的前端代码通过 SweesBridge 直接与 Java 交互,类型安全由 Java 侧的 @JavascriptInterface 注解保证。引入 TypeScript 会增加构建复杂度,但收益有限。


六、性能与优化

SSE 解析效率

SseParser 采用逐行读取 + 即时回调的模式,不缓存整个响应体。每个 SSE chunk 解析后立即通过回调推送到前端,内存占用恒定(仅 StringBuilder full 累积完整文本)。对于长回复,这意味着内存不会随输出长度线性增长。

PRoot 会话池复用

ProotSessionPool 使用 ConcurrentHashMap 管理会话,按 agentId 复用:

private static final long IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 分钟空闲超时

public void reapIdle() {
    long now = System.currentTimeMillis();
    for (Map.Entry<String, Long> e : lastActivity.entrySet()) {
        if (now - e.getValue() > IDLE_TIMEOUT_MS) destroy(e.getKey());
    }
}

每次命令执行时更新 lastActivity 时间戳,空闲超过 5 分钟的会话自动回收。这避免了频繁创建/销毁 PRoot 进程的开销(每次启动 PRoot 需要初始化 bash 环境,约 1-2 秒),同时防止内存泄漏。

请求日志自动裁剪

前文提到的触发器自动裁剪机制,确保 request_logs 表不会无限增长。这是一个「设置即忘记」的设计——不需要 Java 层的 Handler.postDelayedWorkManager 定时任务,数据库触发器在每次插入时自动执行清理。


七、未来规划

Swees 仍在积极开发中,路线图包括:

  • 🌐 更多 AI 服务商:扩展对 Claude、Gemini、Qwen 等更多模型的支持
  • 🔌 插件/技能市场:社区驱动的技能生态,一键安装新能力
  • ☁️ 云端同步:对话和记忆的跨设备同步
  • 📱 平板适配:针对大屏设备的响应式布局
  • 🔐 端侧模型:探索在手机上运行小型 LLM 的可能性

八、结语

Swees 证明了一件事:在 2026 年,一部普通的 Android 手机,足以成为 AI Agent 的完整运行环境。

不需要 root,不需要电脑,不需要云服务器。PRoot 提供了 Linux 环境,SSE 实现了实时交互,Function Calling 赋予了行动能力,SQLite + FTS4 构建了持久记忆——所有这些技术组合在一起,让 AI 从「能说」变成了「能做」。

这是一个开源项目,基于 GPL v3 协议,代码完全公开。

🔗 GitHub: https://github.com/wildwalker2026/Swees

如果你觉得这个项目有意思,欢迎 Star ⭐ 支持。如果你是开发者,欢迎提 Issue、提 PR、写技能——每一份贡献都让 Swees 更好。

Swees — 让 AI 真正动手。

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐