Mote 是一款运行在 Android 设备上的 AI Agent 聊天客户端,面向 OpenAI 兼容的 Chat Completions API。它支持流式响应、工具调用、本地 Shell、思考过程展示、多对话历史、本地文件读取、联网搜索和原生 Markdown 渲染。
- OpenAI 兼容接口:支持自定义 API 地址、API Key、模型、轻量标题模型和上下文压缩模型。
- 流式聊天:支持
stream=true响应、增量内容展示、推理内容展示和 token usage 统计;仅输出前的暂时性失败自动重试,输出后失败保留已有内容与独立错误提示。 - 工具调用:内置文件读取、目录列出、网页抓取、WebView 抓取、联网搜索、Shell 执行、后台进程管理和等待工具。
- 本地 Shell:打包 BusyBox for Android NDK 1.36.1,支持多 ABI,在 Android 设备上提供更完整的命令行环境。
- 风险确认:对高风险 Shell 命令进行静态检测,必须经过用户确认后才会执行。
- 多对话历史:按会话独立 JSON 文件保存,支持标题生成、手动重命名、切换、删除、编辑、重试和停止生成;改名失败会提示且保留旧显示。会话阅读位置在当前进程内按消息锚点恢复,主动发送或回到底部继续跟随输出。
- 上下文压缩:在上下文过长时自动总结旧消息,降低长对话请求压力。
- Markdown 渲染:使用原生 View 渲染 Markdown,支持代码高亮、表格、LaTeX 公式和流式增量更新。
- 附件输入:支持添加图片和文件,文件内容过长时会截断后发送;图片按需读取,可配置附件存储额度,准备失败或超额拒绝保留草稿,不裁剪既有历史。
- 搜索扩展:可配置 SearXNG、Tavily 或 AnySearch,配置后向模型暴露
web_search工具;三个服务只能启用一个。
| 类别 | 技术 |
|---|---|
| 语言 | Kotlin + Java |
| Android | minSdk 26,targetSdk 36,compileSdk 36.1 |
| Java 语言级别 | Java 11 |
| 构建 | Gradle Kotlin DSL + Version Catalog |
| UI | 传统 View + ViewBinding |
| 架构 | MVVM,AndroidViewModel,LiveData,Coroutines |
| 网络 | OkHttp 4.12.0 |
| 序列化 | org.json |
| Markdown | 自研原生视图树 + prism4j + RaTeX |
app/src/main/java/com/mukapp/mote/
├── MainActivity.kt # DrawerLayout 主界面
├── SettingsActivity.kt # API 设置、权限管理
├── MyApplication.kt # Application、BusyBox 初始化
├── data/ # 设置、历史记录、数据模型
├── network/ # OpenAI 兼容请求和 SSE 解析
├── tools/ # AI 工具、BusyBox、Shell 风险检测和进程管理
├── ui/ # 聊天界面、ViewModel、Adapter、侧栏列表
├── ui/markdown/ # Markdown 解析、渲染、代码高亮、表格和公式
└── util/ # 通用工具
- Android Studio,建议使用支持当前 Android Gradle Plugin 的较新版本。
- Android SDK Platform 36,并安装对应构建工具。
- JDK 使用 Android Studio 自带运行时或与当前 AGP 兼容的版本。
- Windows PowerShell、macOS Terminal 或 Linux Shell。
克隆项目后,使用 Android Studio 打开仓库根目录,等待 Gradle 同步完成,然后直接运行 app 模块。
也可以在命令行构建:
.\gradlew.bat assembleDebug生成的 Debug APK 位于:
app/build/outputs/apk/debug/app-debug.apk
Release 构建:
.\gradlew.bat assembleRelease运行单元测试:
.\gradlew.bat testDebugUnitTest运行设备测试:
.\gradlew.bat connectedAndroidTest首次启动后进入设置页,至少填写以下内容:
| 配置项 | 说明 |
|---|---|
| API 地址 | OpenAI 兼容接口地址。地址末尾不是 /chat/completions 时会自动拼接。 |
| API Key | 接口密钥。本地模型服务通常可以留空。 |
| 模型 | 主聊天模型名称。 |
| 轻量模型 | 用于生成历史对话标题,留空时使用首条消息作为标题。 |
| 压缩模型 | 用于长上下文摘要,留空时使用主模型。 |
| 模型上下文长度 | 用于估算和限制请求上下文,空串或 0 表示不限制;非法、负数或超出整数范围的输入不会保存。输入预算先扣除实际请求的输出预留。 |
| 模型能力 | 工具调用、图片输入、用量统计分别可配置,旧配置及新模型默认全部支持;关闭后不发送对应的工具或 usage 参数,不会把图片静默改成纯文本。 |
| 压缩接管百分比 | 占有效输入额度(模型上下文长度减输出预留)的百分比,默认 80,填 0 关闭;对话模型未填上下文长度时不自动压缩。后台提前 10 个百分点开始(最低 1%,默认 70%),达到接管百分比后在下一次安全请求边界用摘要替换较早对话,保留近期完整轮次与期间新增消息;摘要未完成可继续对话,仅超过有效输入额度才等待。不删除聊天界面或完整历史,待接管摘要不保存。 |
| SearXNG 地址 | 配置后启用搜索工具,不可与 Tavily 或 AnySearch 同时配置。 |
| Tavily API Key | 配置后启用搜索工具,不可与 SearXNG 或 AnySearch 同时配置。 |
| AnySearch API Key | 配置后通过 AnySearch POST /v1/search 启用搜索工具,不可与 SearXNG 或 Tavily 同时配置。 |
| 工具权限 | Shell 新命令、文件工具、网页抓取、局域网抓取分别可关闭,默认允许;Shell 关闭后仍保留进程查询和停止工具,不自动终止后台进程。 |
| 图片附件额度 | 按全局图片 blob 的 Base64 UTF-8 磁盘字节计算,单位 MiB,0 表示不限额。仅限制新增图片,不删除旧数据或限制文本/工具结果;设置页可刷新历史与附件占用,不完整统计会明确标记。 |
如果需要让工具读取外部存储文件,需要在设置页授予文件管理权限。该权限由系统设置页手动授予,应用不会自动绕过 Android 的存储限制。
Mote 会根据设置和模型能力向 AI 暴露工具:
| 工具 | 用途 |
|---|---|
read_file |
读取文本文件内容。读取应用私有数据目录需用户确认。 |
list_path |
列出目录或文件信息。查看应用私有数据目录需用户确认。 |
get_current_time |
获取设备当前本地时间、时区和 UTC 偏移量。 |
fetch_url |
获取 HTTP(S) 网页内容。 |
fetch_webview |
使用隐藏 WebView 渲染动态网页并提取内容。 |
web_search |
通过 SearXNG、Tavily 或 AnySearch 搜索网络内容。 |
shell |
执行本地 Shell 命令,长任务可转为后台进程。 |
shell_status |
查询后台 Shell 进程状态。 |
shell_stop |
停止后台 Shell 进程。 |
wait |
等待指定秒数后继续工具循环。 |
Shell 工具具备风险检测机制。命中删除、覆盖、权限破坏等高风险命令时,应用会显示确认条,用户确认后才会执行。read_file/list_path 访问应用私有数据目录(如 shared_prefs、聊天历史)时同样需要经确认条确认。
Shell 使用应用权限执行,不是隔离沙箱。局域网开关仅限制网页抓取,不限制模型接口、搜索服务或 Shell 网络访问。仅公网抓取时,fetch_url 在每次真实连接的 DNS 地址集合和重定向目标上拒绝本机/局域网地址,不使用系统代理;fetch_webview 不暴露且直接调用也拒绝。允许局域网时保留动态网页抓取,并关闭 WebView 的文件与内容访问。
自动重试最多四次外层尝试,仅限输出前的 HTTP 408/429/500/502/503/504 或指定暂时性传输失败;遵守 Retry-After 与指数等待,超过 60 秒的服务端等待交给用户稍后重试,停止可取消等待。工具增量也视为输出;缺少 [DONE] 和 finish_reason 的截断流不会交付可执行工具参数。usage 参数兼容降级仅在 HTTP 400/422 明确不支持且尚无任何输出时重发一次。
Shell 的 stdout/stderr 分别按 4096 字符块读取,固定环形缓冲只保留各自最后 65536 个 UTF-16 字符;无换行输出在进程运行时也可查询,原始换行及未终止行不被改写。
- API 设置保存在应用私有存储中,接口密钥经 Android Keystore 加密后落盘。
- 对话文件保存在
chat_history/conversations/{conversationId}.json(schemaVersion=3)。 - 当前对话指针保存在
chat_history/index.json;对话列表摘要索引保存在chat_history/summaries.json(索引 schema v2,每条摘要绑定对话文件的 mtime/length;旧版本、损坏或指纹不匹配时重建)。 - 图片附件的 base64 数据外置到
chat_history/blobs/{conversationId}/目录,对话 JSON 保存base64Ref;v3 加载保持仅引用,缩略图与压缩后的实际请求在 IO 按需读取。旧版内联格式可直接读取,保存时自动升级;缺失 blob 不隔离整份历史,发送时明确提示重新选择图片。草稿、准备批次与排队保存的活跃引用受进程内 pin 保护,取消只回滚本批新文件,不误删复用附件。 - 损坏的对话 JSON 会隔离到
corrupted/目录。 - 删除以对话 JSON 文件移除(或确认不存在)为提交点;提交前失败保留当前消息、草稿及用户标题保护,提交后清理失败提示“对话已删除,部分历史数据清理未完成。”,不会恢复已删除记录。
- 工具结果会完整保存到历史记录中,发送给模型前会按上下文预算截断。工具卡片的参数/结果预览各最多 5000 字符,仅原参数不超过 5000 字符时尝试 JSON 美化;“查看全部”保留原始参数和结果,不美化或截断。详情控件在首次展开时按需创建;跨消息复用同类工具卡片时,重新绑定当前工具身份、内容和展开状态。
- 请求输入预算包含实际 system prompt、工具定义与每张图片的本地 1024 token 预留(不是提供商精确图像费率)。API usage 锚点同时匹配模型/请求包络、消息前缀及共同工具结果截断额度;不匹配时回退本地估算。输出预留复用请求实际
max_tokens,未设置时在已配置上下文内本地预留最多 1024 token,不改变 API 参数。
Mote 使用原生 Android View 渲染 Markdown,不依赖 WebView 展示聊天内容。当前支持常见块级和行内语法、代码块、行内代码、表格、链接、LaTeX 公式、流式未闭合公式回退显示,以及基于 prism4j 的代码高亮。流式 part 结构稳定时按对象引用推导 dirty 提示、复用未变状态;结构、链接定义、展开态或收尾变化仍刷新必要视图与交互。
自由复制页保留单个可选择 TextView:Markdown 解析、样式构建与文本预计算在后台完成后才绑定,表格保持等宽纯文本网格、公式保持原文;纯文本模式逐字符保留输入。排版异常会记录日志并显示完整原文,页面销毁会取消绑定;最终 TextView 布局仍有主线程成本。
| 权限 | 用途 |
|---|---|
INTERNET |
访问 API、网页抓取和搜索服务。 |
MANAGE_EXTERNAL_STORAGE |
在用户授权后允许工具读取系统允许访问的外部文件路径。 |
- UI 使用传统 View + ViewBinding,不使用 Compose。
- 聊天历史直接以 JSON 文件存储,不使用 Room。
- UI 消息、API 上下文和上下文摘要分离维护。
- 新增设置字段时,需要同步更新设置存储、设置页和布局。
- 新增 AI 工具时,需要同步工具定义、执行逻辑和中间步骤展示。
- 修改 Shell 风险规则时,需要同步更新
ShellRiskDetectorTest。 - 修改上下文压缩、摘要失效或 token 估算时,需要同步相关测试。
本项目基于 Apache License 2.0 发布,详见 LICENSE。
内置 BusyBox 二进制来自 Magisk-Modules-Repo/busybox-ndk,上游版本为 1.36.1(二进制版本标识为 BusyBox v1.36.1-osm0sis,module.prop 的 versionCode 为 13614)。本项目中的四个 ABI 文件分别对应上游 busybox-arm64、busybox-arm、busybox-x86 和 busybox-x86_64。