Android Demo 架构设计¶
概述¶
ClawSeed Android Demo 是一个完整的端侧 AI Agent 应用,在 Android 设备上运行整个 Agent 栈。Rust 编译的 Gateway 二进制作为前台服务进程运行,Android 客户端通过 WebSocket 连接并注册设备端工具。
下载 Android 应用¶
在最新 Release 页面的 Assets 中选择 .apk 文件。历史版本及其更新说明可在
ClawSeed Releases 页面查看。
面向用户的 Agent 功能¶
- 分身系统为新会话绑定专注于特定任务的助手。每个分身可以覆盖 Soul、模型、思考模式、 记忆命名空间、可用工具,以及启用或禁用的技能。
- 用户画像以结构化条目保存长期偏好和上下文。画像既可以人工维护,也可以在成功回复后 自动推断;用户可以审核、拒绝不正确的推断,并随备份导入导出。
- 语音播报使用 Android Text-to-Speech 朗读助手回复。用户既可以全局启用自动朗读, 也可以在会话中播放或停止单条回复。
- 定时任务可以在后台执行提示,并保留自己的会话,因此执行期间仍可使用设备工具和 CETP 外部工具。
- 跨应用工具通过 CETP 自动发现并桥接到当前工具注册表。九财花联动 展示了使用真实投资数据完成工具调用闭环的方式。
整体架构¶
┌─────────────────────────────────────────────────────────┐
│ Android 设备 │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ ClawseedService (前台服务) │ │
│ │ │ │
│ │ ┌────────────────────────────────────────────┐ │ │
│ │ │ libclawseed.so (Rust Gateway 进程) │ │ │
│ │ │ - Axum HTTP/WS 服务器 (端口 42617) │ │ │
│ │ │ - Agent 循环 + LLM 调用 │ │ │
│ │ │ - 内置工具执行 │ │ │
│ │ │ - 远程工具桥接 │ │ │
│ │ └────────────────────────────────────────────┘ │ │
│ │ ↑ ProcessBuilder 启动 │ │
│ │ ↓ /health 轮询就绪 │ │
│ └──────────────────────────────────────────────────┘ │
│ ↕ WebSocket │
│ ┌──────────────────────────────────────────────────┐ │
│ │ MainActivity (Compose UI) │ │
│ │ ┌────────────────────────────────────────────┐ │ │
│ │ │ ClawseedClient (SDK 库) │ │ │
│ │ │ - OkHttp WebSocket 连接 │ │ │
│ │ │ - 工具注册 (device_info 等) │ │ │
│ │ │ - 工具调用处理 │ │ │
│ │ │ - 流式响应回调 │ │ │
│ │ └────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ↕ 网络 │
│ LLM Provider (Anthropic 等) │
└─────────────────────────────────────────────────────────┘
关键设计:整个 Agent 栈在设备端运行,LLM 推理通过网络调用云端 Provider。
用户画像备份¶
设置中的数据迁移页面可以将结构化用户画像导出到备份 ZIP 内的
user_profile/profile.json。导入支持合并、追加和替换策略,并通过已认证的 Gateway API
原子更新正在使用的 SQLite 数据库,无需重启应用。
模块结构¶
clients/android/
├── lib/ # ClawSeed SDK 库
│ ├── build.gradle.kts
│ └── src/main/kotlin/dev/clawseed/client/
│ ├── ClawseedClient.kt # WebSocket 客户端
│ └── ClawseedMessages.kt # 消息协议类型
├── app/ # Demo 应用
│ ├── build.gradle.kts
│ └── src/main/kotlin/dev/clawseed/demo/
│ ├── MainActivity.kt # Compose 主界面
│ └── ClawseedService.kt # 前台服务(Gateway 进程管理)
└── settings.gradle.kts
lib — SDK 库¶
| 类 | 职责 |
|---|---|
ClawseedClient |
WebSocket 连接管理、工具注册、消息收发 |
ToolSpec |
工具规格(名称、描述、JSON Schema 参数) |
ToolCallRequest |
服务端发来的工具调用请求 |
ToolCallResult |
工具调用结果(Success / Failure) |
IncomingMessage |
所有服务端消息的密封类 |
ToolCallHandler |
工具调用处理接口(函数式接口) |
app — Demo 应用¶
| 类 | 职责 |
|---|---|
MainActivity |
Compose UI,连接/消息/工具注册入口 |
ClawseedService |
前台服务,管理 Gateway 进程生命周期,定时任务执行 |
ClawseedClient — WebSocket 客户端¶
构建器模式¶
val client = ClawseedClient.builder("ws://127.0.0.1:42617/ws/chat")
.authToken("optional-token")
.registerTool(ToolSpec(
name = "device_info",
description = "获取Android设备信息",
parameters = """{"type":"object","properties":{},"required":[]}"""
))
.toolCallHandler { request ->
when (request.name) {
"device_info" -> ToolCallResult.Success(queryDeviceInfo())
else -> ToolCallResult.Failure("unknown tool")
}
}
.onConnected { /* 连接成功 */ }
.onDisconnected { /* 连接断开 */ }
.onChunk { text -> /* 流式文本块 */ }
.onThinking { text -> /* 思考过程 */ }
.onDone { finalText -> /* 回合完成 */ }
.onToolCall { id, name, args -> /* 工具调用通知 */ }
.onToolResult { id, name, output -> /* 工具结果通知 */ }
.onAborted { /* 回合中止 */ }
.onError { message -> /* 错误 */ }
.build()
client.connect() // 建立 WebSocket 连接
client.sendMessage("你好") // 发送用户消息
client.disconnect() // 断开连接
连接流程¶
- OkHttp 建立 WebSocket 连接(readTimeout=0,支持流式)
onOpen回调中自动发送register_tools消息- 服务端确认工具注册(
tools_registered) - 连接就绪,可发送消息
工具调用处理¶
// 收到 tool_call_request 消息
private fun dispatchToolCall(request: ToolCallRequest) {
val handler = toolCallHandler ?: run {
webSocket?.send(ToolCallResult.Failure("No handler").toJson(request.id).toString())
return
}
executor.execute {
val result = runCatching { handler.handleToolCall(request) }
.getOrElse { ToolCallResult.Failure(it.message ?: "Exception") }
webSocket?.send(result.toJson(request.id).toString())
}
}
要点:
- 使用单线程 executor 处理工具调用,避免竞态
- 异常被捕获并包装为 ToolCallResult.Failure
- 结果通过 WebSocket 立即发送回服务端
消息协议¶
客户端 → 服务端¶
| 类型 | 格式 | 说明 |
|---|---|---|
| 用户消息 | {"type":"message","content":"..."} |
发送聊天消息 |
| 工具注册 | {"type":"register_tools","tools":[...]} |
注册工具列表 |
| 工具结果 | {"type":"tool_result","id":"...","output":"...","success":true} |
返回成功结果 |
| 工具错误 | {"type":"tool_error","id":"...","error":"...","success":false} |
返回执行错误 |
| 重新生成 | {"type":"regenerate"} |
重新生成最后一条助手响应 |
服务端 → 客户端¶
| 类型 | 说明 |
|---|---|
session_start |
会话开始(sessionId, name, resumed, messageCount) |
connected |
连接确认 |
chunk |
流式文本块 |
thinking |
Agent 思考过程 |
done |
回合完成(full_response) |
tool_call |
工具调用通知(信息性) |
tool_result |
工具结果通知(信息性) |
tool_call_request |
请求客户端执行工具(需要响应) |
tools_registered |
工具注册确认(count, registered) |
result_acknowledged |
结果已确认 |
chunk_reset |
重置流式输出 |
aborted |
回合中止 |
error |
错误消息 |
完整交互流程示例¶
客户端 服务端
│ │
│ ──── WebSocket 连接 ──────────────→ │
│ ──── register_tools ──────────────→ │
│ ←─── tools_registered ──────────── │
│ │
│ ──── message: "告诉我设备信息" ────→ │
│ ←─── chunk: "让我查看" ─────────── │
│ ←─── tool_call_request ──────────── │
│ {id:"tc1", name:"device_info"} │
│ │
│ ──── tool_result ─────────────────→ │
│ {id:"tc1", output:"..."} │
│ │
│ ←─── chunk: "您的设备是..." ────── │
│ ←─── done ───────────────────────── │
ClawseedService — 前台服务¶
生命周期¶
onCreate()
├── 创建通知渠道
└── startForeground("启动 clawseed gateway...")
onStartCommand()
└── scope.launch { startGateway() }
├── 提取 libclawseed.so 二进制
├── ensureConfig() — 配置初始化
├── ProcessBuilder 启动 Gateway 进程
│ 环境: HOME, XDG_CONFIG_HOME, XDG_DATA_HOME
│ 参数: gateway --port 42617
│ API Key: 从 .clawseed/api_key 加载
└── waitUntilReady()
└── 轮询 http://127.0.0.1:42617/health
每 500ms,最多 40 次(20 秒)
onDestroy()
├── 取消协程
├── 销毁 Gateway 进程
└── 清理资源
二进制提取与执行¶
// APK 打包时 useLegacyPackaging = true
// libclawseed.so 被解压到 nativeLibraryDir
val binary = File(applicationInfo.nativeLibraryDir, "libclawseed.so")
// 作为子进程启动
process = ProcessBuilder(binary.absolutePath, "gateway", "--port", "42617")
.redirectErrorStream(true)
.also { pb ->
pb.environment()["HOME"] = filesDir.absolutePath
pb.environment()["CLAWSEED_API_KEY"] = apiKey
}
.start()
为什么命名为 .so:Android APK 只允许打包 .so 文件到 jniLibs/,但实际这是一个可执行的 Rust 二进制,通过 ProcessBuilder 执行而非 System.loadLibrary()。
配置管理¶
ensureConfig() 负责初始化和修补配置:
- 创建
~/.clawseed/和workspace/目录 - 若
clawseed.toml不存在,生成初始配置 - 若存在,修补缺失字段(workspace_dir、web 功能启用等)
- 自动启用
web_fetch、http_request、web_search - 为网络工具添加
allowed_domains = ["*"]
初始配置模板:
workspace_dir = "{WORKSPACE_DIR}"
[gateway]
[web_fetch]
enabled = true
allowed_domains = ["*"]
[http_request]
enabled = true
allowed_domains = ["*"]
[web_search]
enabled = true
provider = "duckduckgo"
就绪检测¶
private suspend fun waitUntilReady() {
val healthUrl = "http://127.0.0.1:42617/health"
repeat(MAX_HEALTH_ATTEMPTS) { // 40 次
val code = // HTTP GET healthUrl
if (code in 200..299) {
isReady = true
readyCallbacks.forEach { it() } // 通知 MainActivity
return
}
delay(500) // 500ms 间隔
}
// 超时则停止服务
stopSelf()
}
网络安全配置¶
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">127.0.0.1</domain>
<domain includeSubdomains="false">localhost</domain>
</domain-config>
</network-security-config>
仅允许 localhost 明文连接(Gateway 在本地 42617 端口)。
权限¶
| 权限 | 用途 |
|---|---|
INTERNET |
网络访问(LLM API、WebSocket) |
FOREGROUND_SERVICE |
前台服务运行 |
FOREGROUND_SERVICE_SPECIAL_USE |
Android 14+ 前台服务类型声明 |
POST_NOTIFICATIONS |
Android 13+ 通知权限 |
SCHEDULE_EXACT_ALARM |
精确定时闹钟权限,用于定时任务(Android 12+;未授予时回退到非精确闹钟) |
RECEIVE_BOOT_COMPLETED |
设备重启后重新调度已启用的任务 |
构建配置¶
| 项目 | 值 |
|---|---|
| minSdk | 26 (Android 8.0) |
| targetSdk / compileSdk | 36 (Android 15) |
| Java 版本 | 17 |
| Compose BOM | 2026.04.01 |
| OkHttp | 4.12.0 |
| Kotlin Coroutines | 1.9.0 |
| useLegacyPackaging | true(二进制提取) |
自定义 Demo 的步骤¶
- 添加工具:在
MainActivity.kt中定义新的ToolSpec和对应的处理逻辑 - 修改 UI:调整 Compose 布局
- 添加权限:如需设备能力(相机、位置等),在
AndroidManifest.xml中声明 - 配置 Gateway:修改
ClawseedService.INITIAL_CONFIG调整默认配置 - API Key:放置在
filesDir/.clawseed/api_key文件中
定时后台任务¶
任务列表分别显示下次计划执行时间与上次执行时间;点击任务或详情图标可查看完整提示词、已保存的执行结果和错误。失败详情提供设置入口,不会自动重跑任务。编辑任务时,时间选择器按需弹出,重复模式与指定星期保留在紧凑表单内。
应用支持基于 AlarmManager 的定时任务,可在指定时间唤醒设备、通过 WebSocket 执行 AI 提示,并通知用户结果。
架构¶
ScheduledTaskManager— 通过AlarmManager.setExactAndAllowWhileIdle()管理闹钟调度ScheduledTaskStore— DataStore 持久化,支持原子合并更新BootReceiver— 设备重启后重新调度所有已启用任务(RECEIVE_BOOT_COMPLETED)ClawseedService任务模式 — 通过独立 WebSocket 会话执行任务,避免与聊天 UI 冲突
任务模型¶
| 字段 | 类型 | 说明 |
|---|---|---|
id |
UUID | 唯一任务 ID |
name |
String | 显示名称 |
message |
String | 发送给 AI 的提示 |
hour / minute |
Int | 定时时间(24 小时制) |
repeat |
Enum | ONCE(一次)、DAILY(每天)、WEEKDAY(工作日)、CUSTOM(指定星期) |
repeatDays |
List |
CUSTOM 模式选中的星期,1 为周一,7 为周日 |
enabled |
Boolean | 启用/禁用(不删除) |
sessionId |
String? | 可选的目标会话 |
重复模式¶
- ONCE — 触发一次后自动禁用
- DAILY — 每天在指定时间触发
- WEEKDAY — 仅周一至周五触发
- CUSTOM:仅在选中的星期触发,编辑和重启后保留选择。
执行流程¶
AlarmManager在定时时间触发onTaskFired()通过Channel队列唤醒ClawseedService- 服务打开独立的 WebSocket 会话(非聊天 UI 的会话)
- 发送 AI 提示,可使用工具(device_info、get_location、CETP)
- 高优先级通知(带声音/振动)显示结果
- 点击通知跳转到任务对应会话的聊天界面
- 循环任务自动重新调度下一次闹钟
外观设置¶
聊天输入框仅在用户点击时获取焦点,打开历史会话不会自动弹出键盘。历史抽屉支持按标题、分身和会话 ID 搜索,按分身分组并支持折叠,每条会话显示本地活动日期。会话操作菜单支持置顶和取消置顶:所有分身的置顶会话统一放在列表最上方,只显示一次并标注所属分身。每个分身的菜单支持删除全部会话,包含已置顶、被搜索或折叠隐藏的会话;确认弹窗显示总数,按已确认的清单逐条删除,失败项保留并提示。即使全部会话已置顶,仍保留分身入口。置顶和折叠状态保存在本机,应用重启后保留;搜索会展开匹配分组,不改变已保存的折叠状态。加载失败与空列表分别展示。LLM 配置使用独立视图和固定保存栏,服务技术详情默认折叠。分身详情按类别折叠工具权限,仅改变展示,不改变授权规则。
应用支持浅色/深色/跟随系统主题选择,以及 OLED 模式选项:
配色集中在 ui/theme/AppColors.kt:中性灰背景搭配暖金色操作与选中态,成功使用绿色,错误和删除使用红色。聊天输入框默认显示中性细描边,聚焦时强调为金色。分身自定义色保留在头像与标签中,大面积背景仅作轻微着色;标签文字按应用当前主题调整对比度。
- 跟随系统 — 跟随 Android 系统设置(默认)
- 浅色 — 始终使用浅色主题
- 深色 — 始终使用深色主题
- OLED — 深色主题,使用纯黑背景适合 OLED 屏幕
Soul 自定义¶
设置界面包含专门的 Soul 编辑器,通过 /api/personality API 端点读写工作区人格文件(SOUL.md 等)。仅允许编辑白名单中的文件。保存后 Gateway 自动重启以应用人格变更。
画像与记忆管理¶
设置中的“助手记住的内容”进入统一页面,包含“关于我”和“记忆”两个标签页。“关于我”支持类别、推断来源和已拒绝状态筛选,以及多选删除/拒绝、删除全部推断项、限时变更预览、冲突刷新和应用后的单次撤销。Activity 重建后会保留待确认计划的标识,但绝不会自动应用计划。记忆条目显示类别与 namespace,使 Persona 作用域保持可见。
聊天中的画像工具结果使用相同的结构化预览和动作控件,不解析模型文本。破坏性操作必须确认。
画像和记忆均属于个人数据。启用隐私过滤的导出会同时排除 USER_PROFILE 与 MEMORY;完整归档可能以明文包含两者,必须按敏感数据存储和分享。应用覆盖升级不要求清除应用数据或从归档恢复。
在 Android 16 上,应用会先请求 NEARBY_WIFI_DEVICES,再启动嵌入式回环 Gateway。Android 16 启用本地网络保护试验开关时会使用该权限;缺少权限会导致本地监听以 EPERM 失败。
图片对话¶
配置 DeepSeek,模型选择 deepseek-v4-flash-vision-exp。连接会话后点击“添加图片”,从相册选取最多 4 张,检查缩略图、移除或重试上传;支持纯图片及图文发送。点击已发送图片放大并双指缩放。旧 Gateway 会明确提示不支持图片上传。
图片草稿副本保存在 App 文件目录,按 Gateway 地址和会话隔离。处理时校正方向,按需缩小并转成 PNG,尽量保留截图文字清晰度;GIF 使用静态帧。发送失败保留图片草稿,进程重启后也可重试。历史图片通过会话对应的 Gateway 认证读取,App 或 Gateway 重启后仍可恢复。旧图超出模型图片上下文时会提示重新附图,但历史仍可查看。
视觉能力配置¶
LLM 设置提供“自动 / 开启 / 关闭”。旧配置默认自动,继续识别原有 DeepSeek 视觉模型;其他未知模型需手动指定。本阶段尚未接入服务商元数据发现或测试图探测。
分身可以继承或独立设置视觉能力。继承仅用于同一模型,更换模型后按自动判断。
附件图标根据当前会话实际 Provider 的能力启用;未知或不支持时置灰且不能点击。
Gateway 在 session_start(包括恢复会话)返回这一判断,并在处理图片时再次校验。
分身修改在新建连接后生效。
文件附件协议、限额与客户端读取工具见 Android 文件附件。