Files
aiagent/docs/android-app-design.md
renjianbo a7512a5423 docs: update 3 reference docs + add Android design, Feishu bot config, productization plan
- Rewrite api-reference.md: 245 endpoints across 38 modules, correct auth paths and response format
- Rewrite 内置工具列表.md: all 56 real tools in 11 categories
- Fix quickstart.md: local dev ports (3001/8038) vs Docker (8037/8038), --port 8038
- Add android-app-design.md: Kotlin/Compose/MVVM design with SSE, FCM, voice
- Add 飞书智能体配置手册.md: all 6 bots config, capabilities, memory architecture
- Add 产品化落地方案.md: PWA/voice/push/Flutter productization roadmap

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-13 21:58:10 +08:00

25 KiB
Raw Blame History

Android App 设计文档

天工智能体平台 Android 客户端,提供 AI 对话、语音交互、推送通知等核心移动端能力。


一、技术选型

层面 选择 理由
语言 Kotlin Android 官方首选,协程天然适配 SSE 流式
UI 框架 Jetpack Compose 声明式 UI与消息列表/流式更新天然契合
架构 MVVM + Repository ViewModel 管理 UI 状态Repository 统一数据源
网络 OkHttp 4 + Retrofit 2 Retrofit 处理 RESTOkHttp 拦截器处理 JWT/重登录
SSE 自定义 OkHttp EventSource Retrofit 不原生支持 SSE需基于 OkHttp 手动解析
本地存储 Room + DataStore Room 存消息历史DataStore 存 Token/偏好
图片加载 Coil Kotlin 原生Compose 集成好
音频录制 MediaRecorder 系统原生 API无额外依赖
音频播放 Media3 ExoPlayer Google 官方推荐
推送 Firebase Cloud Messaging Google 官方,免费

二、项目结构

android/
├── app/
│   ├── build.gradle.kts
│   └── src/main/
│       ├── AndroidManifest.xml
│       ├── java/com/tiangong/aiagent/
│       │   ├── TiangongApp.kt              # Application 类
│       │   ├── MainActivity.kt             # 单 Activity 入口
│       │   │
│       │   ├── data/
│       │   │   ├── remote/
│       │   │   │   ├── ApiService.kt       # Retrofit 接口定义
│       │   │   │   ├── SseClient.kt        # SSE 流式解析器
│       │   │   │   └── AuthInterceptor.kt  # JWT 注入 + 401 自动重登录
│       │   │   ├── local/
│       │   │   │   ├── AppDatabase.kt      # Room 数据库
│       │   │   │   ├── MessageDao.kt       # 消息 DAO
│       │   │   │   └── ConversationDao.kt  # 会话 DAO
│       │   │   └── repository/
│       │   │       ├── AuthRepository.kt   # 登录/Token 管理
│       │   │       ├── ChatRepository.kt   # 对话/SSE 流式
│       │   │       ├── AgentRepository.kt  # Agent 列表/详情
│       │   │       └── NotificationRepository.kt  # 通知轮询
│       │   │
│       │   ├── domain/
│       │   │   └── model/
│       │   │       ├── Agent.kt            # 智能体
│       │   │       ├── Message.kt          # 消息user/assistant/tool/system
│       │   │       ├── Conversation.kt     # 会话
│       │   │       ├── SseEvent.kt         # SSE 事件密封类
│       │   │       ├── TokenUsage.kt       # Token 用量
│       │   │       └── Notification.kt     # 通知
│       │   │
│       │   ├── ui/
│       │   │   ├── navigation/
│       │   │   │   └── NavGraph.kt         # 路由导航图
│       │   │   ├── theme/
│       │   │   │   └── Theme.kt            # Material3 主题
│       │   │   ├── login/
│       │   │   │   ├── LoginScreen.kt
│       │   │   │   └── LoginViewModel.kt
│       │   │   ├── chat/
│       │   │   │   ├── ChatScreen.kt       # 对话主界面
│       │   │   │   ├── ChatViewModel.kt    # SSE 流式消费 + 消息管理
│       │   │   │   └── components/
│       │   │   │       ├── MessageBubble.kt      # 气泡(支持 Markdown
│       │   │   │       ├── VoiceInputButton.kt   # 语音录制按钮
│       │   │   │       ├── ToolCallCard.kt       # 工具调用卡片
│       │   │   │       └── StreamingText.kt      # 打字机流式文本
│       │   │   ├── agents/
│       │   │   │   ├── AgentListScreen.kt
│       │   │   │   └── AgentListViewModel.kt
│       │   │   └── settings/
│       │   │       └── SettingsScreen.kt
│       │   │
│       │   ├── util/
│       │   │   ├── AudioRecorder.kt        # 录音工具MediaRecorder
│       │   │   ├── AudioPlayer.kt          # TTS 播放器ExoPlayer
│       │   │   ├── MarkdownRenderer.kt     # Markdown 渲染
│       │   │   └── FcmTokenManager.kt      # FCM Token 注册/同步
│       │   │
│       │   └── di/
│       │       └── AppModule.kt            # Hilt 依赖注入
│       │
│       └── res/
│           ├── values/strings.xml
│           └── drawable/                   # 图标资源
│
├── build.gradle.kts                        # 根构建文件
├── settings.gradle.kts
└── gradle.properties

三、网络层设计

3.1 基础配置

// data/remote/ApiService.kt
interface ApiService {

    // --- 认证 ---
    @FormUrlEncoded
    @POST("api/v1/auth/login")
    suspend fun login(
        @Field("username") username: String,
        @Field("password") password: String
    ): LoginResponse

    @GET("api/v1/auth/me")
    suspend fun getCurrentUser(): UserResponse

    // --- Agent ---
    @GET("api/v1/agents")
    suspend fun getAgents(
        @Query("skip") skip: Int = 0,
        @Query("limit") limit: Int = 100
    ): Response<List<AgentResponse>>  // X-Total-Count 在 header

    @GET("api/v1/agents/{agentId}")
    suspend fun getAgent(@Path("agentId") agentId: String): AgentResponse

    // --- 对话(非流式) ---
    @POST("api/v1/agent-chat/{agentId}")
    suspend fun chat(
        @Path("agentId") agentId: String,
        @Body request: ChatRequest
    ): ChatResponse

    // --- 通知 ---
    @GET("api/v1/notifications/unread-count")
    suspend fun getUnreadCount(): UnreadCountResponse

    @GET("api/v1/notifications")
    suspend fun getNotifications(
        @Query("unread_only") unreadOnly: Boolean = false,
        @Query("limit") limit: Int = 50,
        @Query("offset") offset: Int = 0
    ): List<NotificationResponse>

    // --- FCM Token 注册(新增后端接口) ---
    @POST("api/v1/push/register")
    suspend fun registerFcmToken(@Body request: FcmRegisterRequest)
}

3.2 关键接口参数

登录form-encoded — 后端使用 OAuth2PasswordRequestForm不走 JSON

// 请求
data class LoginRequest(
    val username: String,   // Field: username
    val password: String    // Field: password
)
// 响应
data class LoginResponse(
    val access_token: String,
    val token_type: String  // "bearer"
)

对话请求/响应

data class ChatRequest(
    val message: String,
    @SerializedName("session_id") val sessionId: String? = null,
    val streamlined: Boolean = false
)

// 流式 SSE 事件密封类
sealed class SseEvent {
    data class Message(val content: String) : SseEvent()
    data class ToolCall(val toolName: String, val toolInput: String) : SseEvent()
    data class ToolResult(val toolName: String, val toolOutput: String, val success: Boolean) : SseEvent()
    data class Plan(val plan: PlanObject) : SseEvent()
    data class Done(
        val sessionId: String,
        val iterationsUsed: Int,
        val toolCallsMade: Int,
        val tokenUsage: TokenUsage
    ) : SseEvent()
    data class Error(val error: String) : SseEvent()
}

3.3 SSE 流式解析器

后端 SSE 格式为 event: type\ndata: json\n\n,需手动解析:

// data/remote/SseClient.kt
class SseClient(private val okHttpClient: OkHttpClient) {

    fun connect(url: String, token: String, body: ChatRequest): Flow<SseEvent> = callbackFlow {
        val requestBody = body.toJson().toRequestBody("application/json".toMediaType())
        val request = Request.Builder()
            .url(url)
            .header("Authorization", "Bearer $token")
            .header("Accept", "text/event-stream")
            .post(requestBody)
            .build()

        val call = okHttpClient.newCall(request)
        val response = call.execute()

        if (!response.isSuccessful) {
            close(IOException("HTTP ${response.code}"))
            return@callbackFlow
        }

        val source = response.body?.source() ?: return@callbackFlow
        var eventType = ""
        var data = ""

        while (!source.exhausted()) {
            val line = source.readUtf8Line() ?: break

            when {
                line.startsWith("event: ") -> eventType = line.removePrefix("event: ")
                line.startsWith("data: ") -> data = line.removePrefix("data: ")
                line.isEmpty() -> {
                    // 空行 = 事件结束
                    if (data.isNotEmpty()) {
                        val event = parseEvent(eventType, data)
                        trySend(event)
                    }
                    eventType = ""
                    data = ""
                }
            }
        }
        close()
    }

    private fun parseEvent(type: String, json: String): SseEvent = when (type) {
        "message" -> SseEvent.Message(extractContent(json))
        "tool_call" -> {
            val obj = JsonParser.parseString(json).asJsonObject
            SseEvent.ToolCall(
                obj["tool_name"].asString,
                obj["tool_input"].asString
            )
        }
        "tool_result" -> {
            val obj = JsonParser.parseString(json).asJsonObject
            SseEvent.ToolResult(
                obj["tool_name"].asString,
                obj["tool_output"].asString,
                obj["success"].asBoolean
            )
        }
        "done" -> SseEvent.Done(/* parse full done payload */)
        "error" -> SseEvent.Error(extractError(json))
        else -> SseEvent.Message("")  // 未知类型忽略
    }
}

3.4 JWT 拦截器 + 自动重登录

Token 30 分钟过期,无 refresh 端点401 时自动重登录:

// data/remote/AuthInterceptor.kt
class AuthInterceptor(
    private val tokenStore: TokenDataStore,       // DataStore 存 token
    private val credentialStore: CredentialStore  // 加密存用户名密码
) : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {
        val token = runBlocking { tokenStore.getToken() }

        val request = if (token != null) {
            chain.request().newBuilder()
                .header("Authorization", "Bearer $token")
                .build()
        } else {
            chain.request()
        }

        val response = chain.proceed(request)

        // 401 自动重登录
        if (response.code == 401) {
            response.close()
            val credentials = runBlocking { credentialStore.getCredentials() }
            if (credentials != null) {
                val newToken = runBlocking { reLogin(credentials) }
                if (newToken != null) {
                    val retryRequest = request.newBuilder()
                        .header("Authorization", "Bearer $newToken")
                        .build()
                    return chain.proceed(retryRequest)
                }
            }
            // 重登录也失败,跳转登录页(通过 EventBus 或回调通知 UI
        }
        return response
    }

    private suspend fun reLogin(cred: Credentials): String? {
        // 使用单独的 OkHttp 客户端(不走本拦截器,避免死循环)
        val client = OkHttpClient()
        val body = FormBody.Builder()
            .add("username", cred.username)
            .add("password", cred.password)
            .build()
        val request = Request.Builder()
            .url("${BuildConfig.BASE_URL}/api/v1/auth/login")
            .post(body)
            .build()
        val response = client.newCall(request).execute()
        return if (response.isSuccessful) {
            val json = JsonParser.parseString(response.body?.string()!!).asJsonObject
            val newToken = json["access_token"].asString
            tokenStore.saveToken(newToken)
            newToken
        } else null
    }
}

四、屏幕设计

4.1 整体导航

MainActivity
└── NavHost
    ├── LoginScreen           (startDestination)
    ├── ChatScreen            (需登录)
    ├── AgentListScreen       (需登录,从 Chat 顶部切出)
    ├── SettingsScreen        (需登录)
    └── AboutScreen

4.2 登录页 — LoginScreen

说明
元素 用户名输入框、密码输入框、登录按钮、服务器地址(可配置)
状态 空闲、加载中、错误提示
逻辑 登录成功 → 存储 Token + 凭据 → 跳转 ChatScreen
安全 凭据存 EncryptedSharedPreferencesAndroidX Security

4.3 对话页 — ChatScreen

核心页面,承载主要交互。

区域 说明
顶部栏 当前 Agent 名称点击切换、Agent 在线状态、通知铃铛(红点未读数)
消息列表 LazyColumn 倒序渲染,消息气泡(用户右对齐/助手左对齐),支持 Markdown 渲染
工具调用卡片 可折叠卡片,显示工具名/输入/输出,加载状态
流式文本 打字机效果,逐 token 显示,光标闪烁
底部输入栏 文本输入框 + 语音按钮 + 发送按钮 + 加号(图片/文件)
空状态 首次进入显示"你好,我是 <Agent名>" + 快捷提问建议

状态管理ChatViewModel

事件 处理
发送消息 创建 user Message → 添加到列表 → 创建 assistant 占位 Message → 开启 SSE 连接
SSE token 追加到当前 assistant Message.content
SSE tool_call 插入 ToolCallCard折叠状态
SSE tool_result 更新 ToolCallCard 为展开状态,显示结果
SSE done 停止流式,更新 token_usage保存消息到 Room
SSE error 显示错误提示,消息气泡变红色
切换 Agent 结束当前 SSE → 清空列表 → 加载新 Agent 信息
退后台/杀进程 保存 session_id下次恢复时带 session_id 继续对话

消息气泡样式

类型 对齐 样式
user 右对齐 主题色背景,白色字
assistant (Markdown) 左对齐 卡片背景Markdown 渲染正文
tool_call 左对齐 缩进卡片,灰色边框,"正在调用 xxx 工具..."
tool_result 左对齐 缩进卡片,绿色边框(成功)/红色(失败),显示摘要
system 居中 灰色小字,如"会话已创建"

4.4 Agent 列表页 — AgentListScreen

说明
触发 对话页顶部栏点击 Agent 名称
样式 BottomSheet 或 全屏页面
列表项 Agent 头像首字母、名称、描述、状态标签published/stopped
操作 点击切换当前对话 Agent自动创建新 session
搜索 顶部搜索栏,调用 GET /agents?search=

4.5 设置页 — SettingsScreen

说明
服务器地址 可编辑,默认 http://101.43.95.130:8038
语音 TTS 开关、音色选择alloy/echo/fable/onyx/nova/shimmer
推送 推送开关(注册/注销 FCM Token
主题 亮色/暗色/跟随系统
账户 当前用户信息、退出登录

五、语音交互

5.1 语音输入ASR

用户按住语音按钮
  -> MediaRecorder 录制 AAC/WebM
  -> 松开按钮,停止录制
  -> 上传到 POST /api/v1/voice/asr (multipart)
  -> 返回 { "text": "..." }
  -> 填入输入框(或直接发送)
实现
录制 MediaRecorder,输出格式 AAC_ADTSWEBM(兼容后端 Whisper
采样率 16000 HzWhisper 推荐)
按钮交互 按住录音、上滑取消、松开发送(类似微信)
振幅动画 录音时显示波形动画(AudioRecord.read() 获取 PCM 振幅)
// util/AudioRecorder.kt
class AudioRecorder(private val context: Context) {
    private var mediaRecorder: MediaRecorder? = null
    private var outputFile: File? = null

    fun startRecording(): File {
        outputFile = File(context.cacheDir, "voice_${System.currentTimeMillis()}.aac")
        mediaRecorder = MediaRecorder(context).apply {
            setAudioSource(MediaRecorder.AudioSource.MIC)
            setOutputFormat(MediaRecorder.OutputFormat.AAC_ADTS)
            setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
            setAudioSamplingRate(16000)
            setOutputFile(outputFile!!.absolutePath)
            prepare()
            start()
        }
        return outputFile!!
    }

    fun stopRecording() {
        mediaRecorder?.apply {
            stop()
            release()
        }
        mediaRecorder = null
    }
}

5.2 语音输出TTS

助手回复完成
  -> 调用 POST /api/v1/voice/tts (JSON: text + voice)
  -> 返回 { "audio_url": "/api/v1/uploads/tts/xxx.mp3" }
  -> ExoPlayer 播放
  -> 消息气泡右下角显示播放按钮
实现
播放器 ExoPlayer (Media3),支持缓存
触发 消息气泡旁播放按钮 / 设置中开启自动朗读
打断 用户开始录音时,停止当前 TTS

六、推送通知

6.1 架构

后端任务完成/告警
  -> PushService (新增)
  -> Firebase Cloud Messaging
  -> Android 设备
  -> 通知栏展示
  -> 点击跳转 App 对应页面

6.2 客户端实现

// 1. 获取 FCM Token
class TiangongApp : Application() {
    override fun onCreate() {
        super.onCreate()

        FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
            if (task.isSuccessful) {
                val token = task.result
                // 同步到后端
                CoroutineScope(Dispatchers.IO).launch {
                    apiService.registerFcmToken(FcmRegisterRequest(
                        token = token,
                        platform = "android"
                    ))
                }
            }
        }
    }
}

// 2. 前台消息处理 — 通知弹窗 + 红点更新
class TiangongMessagingService : FirebaseMessagingService() {
    override fun onMessageReceived(message: RemoteMessage) {
        val title = message.notification?.title ?: message.data["title"] ?: return
        val body = message.notification?.body ?: message.data["body"] ?: ""
        val url = message.data["url"] ?: ""

        showNotification(title, body, url)
    }

    private fun showNotification(title: String, body: String, deepLink: String) {
        val channelId = when (message.data["priority"]) {
            "urgent" -> "agent_urgent"
            else -> "agent_default"
        }
        // 创建通知渠道 + 显示通知
        // 点击通知 -> deepLink 跳转 (如打开对话页)
    }
}

6.3 通知渠道

渠道 ID 名称 级别 行为
agent_reply AI 回复 DEFAULT 声音 + 状态栏
agent_urgent 紧急通知 HIGH 声音 + 振动 + 悬浮
agent_alert 系统告警 MAX 全屏通知

6.4 后端需新增接口

方法 路径 说明
POST /api/v1/push/register 注册 FCM Token绑定 user_id
DELETE /api/v1/push/unregister 注销 FCM Token
POST /api/v1/push/send (内部) 发送推送 — 由 notify_user 工具/告警服务调用

需新增 push_tokens 表:

字段 类型 说明
id UUID 主键
user_id UUID 用户 ID
token VARCHAR(512) FCM token
platform VARCHAR(16) android / ios / web
created_at DATETIME 注册时间
last_used_at DATETIME 最后一次推送时间

七、数据层

7.1 Room 数据库

// data/local/AppDatabase.kt
@Database(entities = [MessageEntity::class, ConversationEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun messageDao(): MessageDao
    abstract fun conversationDao(): ConversationDao
}

// MessageEntity
@Entity(tableName = "messages")
data class MessageEntity(
    @PrimaryKey val id: String,          // UUID客户端生成
    val conversationId: String,          // 会话 ID (即 session_id)
    val agentId: String?,                // 来源 Agent
    val role: String,                    // user / assistant / tool / system
    val content: String,                 // 正文
    val toolName: String?,               // 工具名role=tool 时)
    val toolInput: String?,              // 工具输入
    val toolOutput: String?,             // 工具输出
    val tokenUsage: String?,             // JSON 序列化的 TokenUsage
    val createdAt: Long                  // 时间戳
)

// ConversationEntity
@Entity(tableName = "conversations")
data class ConversationEntity(
    @PrimaryKey val sessionId: String,   // = 后端 session_id
    val agentId: String?,
    val agentName: String?,
    val title: String?,                  // 首条消息截取
    val lastMessage: String?,
    val lastMessageAt: Long,
    val messageCount: Int
)

7.2 DataStore

类型 说明
access_token String JWT Token
server_url String 服务器地址
current_agent_id String 当前选中的 Agent ID
tts_enabled Boolean 是否自动朗读
tts_voice String 音色选择
push_enabled Boolean 推送开关
theme_mode String light / dark / system

八、构建配置

8.1 build.gradle.kts关键依赖

// app/build.gradle.kts
dependencies {
    // Compose BOM
    implementation(platform("androidx.compose:compose-bom:2024.06.00"))
    implementation("androidx.compose.material3:material3")
    implementation("androidx.compose.ui:ui-tooling-preview")

    // Navigation
    implementation("androidx.navigation:navigation-compose:2.7.7")

    // Lifecycle + ViewModel
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.0")
    implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.0")

    // Network
    implementation("com.squareup.retrofit2:retrofit:2.11.0")
    implementation("com.squareup.retrofit2:converter-gson:2.11.0")
    implementation("com.squareup.okhttp3:okhttp:4.12.0")
    implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")

    // Room
    implementation("androidx.room:room-runtime:2.6.1")
    implementation("androidx.room:room-ktx:2.6.1")
    kapt("androidx.room:room-compiler:2.6.1")

    // DataStore
    implementation("androidx.datastore:datastore-preferences:1.1.1")

    // DI
    implementation("com.google.dagger:hilt-android:2.51")
    kapt("com.google.dagger:hilt-compiler:2.51")

    // Firebase (推送)
    implementation(platform("com.google.firebase:firebase-bom:33.1.0"))
    implementation("com.google.firebase:firebase-messaging-ktx")

    // Media (TTS 播放)
    implementation("androidx.media3:media3-exoplayer:1.3.1")

    // Markdown 渲染
    implementation("io.noties.markwon:core:4.6.2")

    // Security (加密凭据)
    implementation("androidx.security:security-crypto:1.1.0-alpha06")
}

8.2 最低版本

minSdk 26 (Android 8.0)
targetSdk 34 (Android 14)
compileSdk 34
Kotlin 2.0
AGP 8.5

九、工作量估算

模块 内容 工作量
项目搭建 Gradle 配置、Hilt、主题、导航骨架 0.5天
网络层 Retrofit 接口 + OkHttp 拦截器 + SSE 解析器 1天
登录 登录页 + ViewModel + Token 持久化 + 自动重登录 0.5天
对话页 ChatScreen + ChatViewModel + SSE 流式消费 + 消息气泡 2天
Markdown 渲染 Markwon 集成 + 代码块 + 表格 + 图片 0.5天
工具调用 ToolCallCard + 折叠展开 + 实时更新 0.5天
Agent 列表 AgentListScreen + 切换 Agent 0.5天
语音输入 AudioRecorder + 语音按钮 + 上传 + ASR 调用 1天
语音输出 ExoPlayer + TTS 调用 + 播放按钮 0.5天
推送 FCM 集成 + 通知渠道 + 后端接口 1天
本地存储 Room 数据库 + DataStore + 消息持久化 1天
设置页 SettingsScreen + 主题切换 0.5天
后端推送 FCM 集成 + push_tokens 表 + 推送 API 1天
测试联调 端到端测试 + 多机型适配 2天
合计 12天

十、后端需配合改造

改动 说明 优先级
新增 /api/v1/voice/asr 接收音频文件,返回文本(已有 speech_to_text 工具,封装为 API P0
新增 /api/v1/voice/tts 接收文本,返回音频 URL已有 text_to_speech 工具,封装为 API P0
新增 /api/v1/push/register 接收 FCM Token绑定用户 P1
新增 push_tokens 存储设备推送 Token P1
FCM Server SDK 后端集成 firebase-admin,从 notify_user 触发推送 P1
Token 过期延长 30分钟 → 7天移动端场景或增加 refresh_token 机制 P1