Files
aiagent/android/architecture_context.md
renjianbo beff3fac8d fix: delete agent 500 error + dynamic personality + deployment guide
- Fix delete agent 500: clean up FK records (agent_llm_logs, permissions,
  schedules, executions, team_members) and unbind goals/tasks before delete
- Remove hardcoded personality templates in Android, replace with dynamic
  system prompt generation from name + description
- Set promptSectionsEnabled=false to bypass PromptComposer for personality
- Add Tencent Cloud Linux deployment guide (Docker Compose)
- Accumulated backend service updates, frontend UI fixes, Android app changes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-29 01:17:21 +08:00

25 KiB
Raw Blame History

天工智能体 Android 客户端 — 架构上下文文档

文档版本: 2.0 | 生成日期: 2026-06-27 | 分析源码文件数: 14


1. 技术栈全景

类别 技术 版本 证据来源
语言 Kotlin 2.0.10 gradle/libs.versions.toml:3
编译 SDK Android API 34 app/build.gradle.kts:9
最低 SDK Android API 26 app/build.gradle.kts:13
Java 目标 JVM 17 - app/build.gradle.kts:34-35
UI 框架 Jetpack Compose (BOM) 2024.06.00 gradle/libs.versions.toml:4
Material Design Material3 BOM 托管 gradle/libs.versions.toml:26
导航 Navigation Compose 2.7.7 gradle/libs.versions.toml:6
DI Hilt (Dagger) 2.51.1 gradle/libs.versions.toml:11
网络层 Retrofit + OkHttp 2.11.0 / 4.12.0 gradle/libs.versions.toml:8-9
JSON Gson 2.11.0 gradle/libs.versions.toml:14
SSE 流式 OkHttp 底层流式读取 - SseClient.kt:95-120
本地数据库 Room 2.6.1 gradle/libs.versions.toml:10
键值存储 DataStore Preferences 1.1.1 gradle/libs.versions.toml:10
图片加载 Coil 2.6.0 gradle/libs.versions.toml:14
音频播放 Media3 ExoPlayer 1.3.1 gradle/libs.versions.toml:14
Markdown 渲染 Markwon 4.6.2 gradle/libs.versions.toml:15
安全加密 Security Crypto 1.1.0-alpha06 gradle/libs.versions.toml:15
启动画面 SplashScreen API 1.0.1 app/build.gradle.kts:75
构建工具 AGP 8.5.2 gradle/libs.versions.toml:2
架构模式 MVVM + Repository + UDF (单向数据流) - 所有 ViewModel 均采用 StateFlow<UiState>

2. 目录结构与模块组织

app/src/main/java/com/tiangong/aiagent/
├── MainActivity.kt                            # 入口 Activity
├── TiangongApplication.kt                     # Hilt Application
│
├── navigation/                                # 导航图
│
├── di/                                         # Hilt DI 模块
│
├── data/
│   ├── local/
│   │   ├── TokenDataStore.kt                  # DataStore 键值存储Token/偏好)
│   │   ├── CredentialStore.kt                 # 加密凭据存储
│   │   ├── AppDatabase.kt                     # Room 数据库定义
│   │   ├── ConversationEntity.kt              # 对话表实体
│   │   ├── MessageEntity.kt                   # 消息表实体
│   │   ├── ConversationDao.kt                 # 对话 DAO
│   │   └── MessageDao.kt                      # 消息 DAO
│   ├── remote/
│   │   ├── ApiService.kt                      # Retrofit API 接口
│   │   ├── SseClient.kt                       # SSE 流式客户端OkHttp
│   │   ├── AuthInterceptor.kt                 # Token 注入拦截器
│   │   └── dto/                                # 请求/响应 DTO
│   └── repository/
│       ├── AuthRepository.kt                  # 认证仓库
│       ├── ChatRepository.kt                  # 对话仓库
│       ├── AgentRepository.kt                 # 智能体仓库
│       └── FeedbackRepository.kt              # 反馈仓库
│
├── domain/
│   └── model/
│       ├── Agent.kt                           # 智能体模型
│       ├── Message.kt                         # 消息模型 + Role 枚举
│       ├── Conversation.kt                    # 对话模型
│       ├── SseEvent.kt                        # SSE 事件 sealed class
│       └── TokenUsage.kt                      # Token 用量模型
│
├── ui/
│   ├── login/       LoginScreen + LoginViewModel
│   ├── register/    RegisterScreen + RegisterViewModel
│   ├── chat/
│   │   ├── ChatScreen.kt                      # 主聊天界面
│   │   ├── ChatViewModel.kt                   # 聊天 ViewModel核心
│   │   └── components/                        # MessageBubble, StreamingText, ToolCallCard, VoiceInputButton
│   ├── agents/      AgentListScreen + AgentListViewModel
│   ├── history/     ConversationListScreen + ConversationListViewModel
│   ├── settings/    SettingsScreen
│   └── common/      SkeletonAgentList, SkeletonChat, SkeletonConversationList
│
└── util/
    ├── AudioPlayer.kt                         # TTS 音频播放器Media3
    └── AudioRecorder.kt                       # 语音录制器

3. 核心模块分析

3.1 认证模块 (Auth)

文件 职责
LoginViewModel.kt 处理登录表单状态,调用 AuthRepository.login()
RegisterViewModel.kt 客户端字段验证 + 调用 AuthRepository.register()
AuthRepository.kt 封装 API 调用,管理 Token 持久化和凭据存储
ApiService.kt:15-19 Retrofit @FormUrlEncoded @POST login / @POST register
TokenDataStore.kt DataStore 持久化 access_tokenserver_url
CredentialStore.kt 加密存储用户名/密码
AuthInterceptor.kt OkHttp 拦截器,自动注入 Authorization: Bearer $token

认证流程:

LoginScreen → LoginViewModel.login()
  → AuthRepository.login(username, password)
    → ApiService.login() → 返回 LoginResponse(accessToken)
    → tokenDataStore.saveToken()    // 持久化 Token
    → credentialStore.saveCredentials()  // 加密保存凭据
  → LoginViewModel._uiState.copy(isLoggedIn = true)
  → LoginScreen LaunchedEffect 跳转主页

⚠️ 风险点:

  • LoginViewModel.kt:2-3 — 默认用户名 "admin"、密码 "123456" 硬编码在 LoginUiState 中,这是明显的安全漏洞,可能是测试遗留代码
  • AuthRepository.kt:22login()catch 捕获所有异常不区分类型,网络异常和业务异常混杂
  • TokenDataStore.kt:86-88clearAll() 注释说 "Keep server URL and theme preferences across logouts",但实际执行的是 prefs.clear() 然后什么也不保留——注释与实现矛盾

3.2 聊天/对话模块 (Chat) — 核心模块

文件 行数(估) 职责
ChatViewModel.kt ~300+ 管理聊天状态、SSE 流式消费、语音/图片、反馈
ChatScreen.kt ~400+ Compose UI消息列表、输入框、语音按钮、反馈操作
ChatRepository.kt 186 对话 API + Room 本地存储
SseClient.kt 203 SSE 长连接 + 指数退避自动重连

ChatUiState 状态字段ChatViewModel.kt:

messages: List<UiMessage>       # 当前对话消息列表
currentAgent: Agent?            # 当前选中的智能体
availableAgents: List<Agent>   # 可用智能体列表
isLoading/isStreaming           # 加载/流式状态
streamingContent: String        # 流式接收中的文本
currentToolCall: Pair?          # 进行中的工具调用 (name, input)
sessionId: String?              # 当前会话 ID
isRecording/isTranscribing      # 语音录制/识别状态
reconnectionState               # SSE 重连状态Idle/Reconnecting/Failed
feedbackSubmittingMsgId         # 提交反馈中的消息 ID

ChatViewModel 依赖注入ChatViewModel.kt10 个依赖):

ChatRepository, AgentRepository, FeedbackRepository,
AudioRecorder, AudioPlayer, ApiService,
TokenDataStore, SavedStateHandle, Gson

⚠️ 高依赖耦合: ViewModel 直接依赖 ApiService(绕过 Repository 层),对于 speakText()TTStranscribeVoice()uploadImage() 这三个方法是直接在 ViewModel 中调用 ApiService + TokenDataStore,破坏了 Repository 模式的封装。

SSE 流式数据路径:

ChatScreen 输入文字 → ChatViewModel.sendMessage()
  → ChatRepository.chatStream(agentId, message, sessionId)
    → SseClient.connect(agentId, token, request)
      → OkHttp POST 到 /api/v1/agent-chat/{agentId}/stream
      → 通过 callbackFlow 逐行解析 "event:" / "data:" 行
      → parseEvent() 转为 SseEvent sealed class:
          Plan / Think / ToolCall / ToolResult / Final / Message / Error / ConnectionError
    → 返回 Flow<SseEvent>
  → ChatViewModel 收集 Flow:
      on Plan → 不处理
      on Think → 不处理
      on ToolCall → 更新 currentToolCall
      on ToolResult → 清除 currentToolCall
      on Final → 追加 AI 消息到 messages停止 streaming
      on Message → 累积 streamingContent
      on ConnectionError → 更新 reconnectionState
      on Done → 停止 streaming
      on Error → 设置 error
  → 同时 ChatRepository.saveMessage() 写入 Room 数据库

SSE 重连策略SseClient.kt:28-32:

初始延迟: 1s  → 退避因子: 2.0  → 最大延迟: 30s  → 最大重试: 3 次
401: 不重试(认证过期)
5xx/IOException: 触发退避重试

3.3 数据持久化层

Room 数据库 — 包含两张表:

  • ConversationEntity — 字段: sessionId, agentId, agentName, title, lastMessage, lastMessageAt, messageCount
  • MessageEntity — 字段: id, conversationId, agentId, role, content, toolName, toolInput, toolOutput, tokenUsageJson, createdAt

DataStore (Preferences)TokenDataStore.kt — 8 个键:

access_token        # JWT Token
server_url          # 服务器地址(默认 BuildConfig.BASE_URL = http://192.168.31.150:8037/
current_agent_id    # 当前选中智能体
current_agent_name  # 当前智能体名称
tts_enabled         # TTS 开关
tts_voice           # TTS 语音 (默认 "alloy")
theme_mode          # 主题模式 (默认 "system")
last_session_id     # 上次会话 ID用于恢复

消息持久化时机ChatRepository.kt:

  • chat() (非流式): 调用 saveMessagesFromResponse() 批量保存 steps
  • chatStream() (流式): 不自动保存,由 ViewModel 在收到 SseEvent.Final 后调用 saveMessage() 逐条保存
  • saveMessage() 同时 upsert ConversationEntity

⚠️ 风险: 流式消息在 SseEvent.Final 之前如果 App 崩溃或网络断开,消息会丢失(未持久化到 Room

3.4 UI 层

骨架屏 (Skeleton Screens):

  • SkeletonAgentList — 智能体列表加载中6 个占位项)
  • SkeletonChat — 聊天界面加载中
  • SkeletonConversationList — 对话历史加载中5 个占位项)

ChatScreen 组件树ChatScreen.kt:

Scaffold
├── TopAppBar (标题 = currentAgent.name, 操作按钮: 历史/通知/设置)
├── LazyColumn (消息列表, reversed)
│   ├── SkeletonChat (首次加载时)
│   ├── MessageBubble (逐条消息气泡)
│   │   ├── Markdown 渲染正文
│   │   ├── ToolCallCard (工具调用卡片)
│   │   └── 操作栏: 复制 / 朗读 / 点赞 / 点踩
│   ├── ToolCallCard (进行中的工具调用)
│   └── StreamingText (流式输出文本)
└── BottomBar
    ├── IconButton (图片上传)
    ├── OutlinedTextField (文字输入)
    └── VoiceInputButton (语音输入)

⚠️ 注意: 预扫描中 ChatScreen.kt 被截断8KB 限制),部分实现细节未获取。MessageBubble, StreamingText, ToolCallCard, VoiceInputButton 的具体实现在 ui/chat/components/ 目录下的独立文件中,未读取。

3.5 反馈模块

ChatViewModel.kt 中:

  • submitFeedback(msgId, rating)FeedbackRepositoryApiService.submitFeedback()
  • 支持 LIKE / DISLIKE 评分 + 可选评论 (submitFeedbackComment)
  • 状态通过 feedbackSubmittingMsgId 跟踪提交中状态

4. 数据流路径汇总

4.1 登录流程

LoginScreen
  → onUsernameChange / onPasswordChange → LoginViewModel._uiState.update
  → Button onClick → LoginViewModel.login()
    → AuthRepository.login(username, password)
      → ApiService.login(@Field username, @Field password)
      → 返回 LoginResponse(accessToken)
      → TokenDataStore.saveToken(accessToken)
      → CredentialStore.saveCredentials(username, password)
    → _uiState.copy(isLoggedIn = true)
  → LaunchedEffect(isLoggedIn) → onLoginSuccess() 导航

4.2 注册流程

RegisterScreen (5 个字段: username/displayName/email/password/confirmPassword)
  → 每个输入框 onValueChange → RegisterViewModel.onXxxChanged() → 实时客户端验证
  → Button onClick → RegisterViewModel.register()
    → 再次验证所有字段
    → AuthRepository.register(RegisterRequest)
      → ApiService.register(@Body)
      → 成功: isSuccess = true
      → HTTP 409: 解析 errorBody JSON 中的 detail 字段
      → HTTP 422: 同上
      → IOException: "网络连接失败"
  → RegisterScreen LaunchedEffect(isSuccess) → Snackbar + 跳转登录

4.3 SSE 流式对话

ChatScreen 输入 "你好"
  → ChatViewModel.sendMessage("你好")
    → 生成用户 UiMessage追加到 messages
    → ChatRepository.chatStream(agentId, "你好", sessionId)
      → TokenDataStore.getToken()
      → SseClient.connect(agentId, token, ChatRequest)
        → runBlocking { TokenDataStore.serverUrl.first() }  // ⚠️ 阻塞 OkHttp 线程
        → POST {baseUrl}api/v1/agent-chat/{agentId}/stream
        → callbackFlow 主循环:
            source.readUtf8Line() 逐行解析 SSE
            event: → 设置事件类型
            data: → 设置数据
            空行 → parseEvent(type, data) → trySend(event)
      返回 Flow<SseEvent>
    → ChatViewModel.collect:
        is ToolCall → currentToolCall = (name, input)
        is ToolResult → currentToolCall = null
        is Message → streamingContent += content
        is Final → 生成 AI UiMessage, isStreaming = false, 保存到 Room
        is ConnectionError → reconnectionState = Reconnecting(...)
        is Done → isStreaming = false
        is Error → error = ...
  → ChatScreen 自动滚动到最新消息

4.4 TTS 语音合成

ChatScreen 点击朗读按钮 → ChatViewModel.speakText(text)
  → TokenDataStore.ttsEnabled.first()     // 检查 TTS 开关
  → TokenDataStore.ttsVoice.first()       // 获取语音类型
  → ApiService.synthesizeSpeech(TtsRequest)
  → 拼接 audioUrl (相对路径补全 BaseURL)
  → AudioPlayer.play(audioUrl)            // Media3 ExoPlayer

4.5 语音识别 (ASR)

VoiceInputButton 录制完成
  → AudioRecorder 输出 File (AAC 格式)
  → ChatViewModel.transcribeVoice(file)
    → file.asRequestBody("audio/aac")
    → ApiService.transcribeAudio(MultipartBody.Part)
    → 返回 transcribedText
    → file.delete()  // finally 中删除临时文件

4.6 图片上传

ChatScreen 图片选择器 (ActivityResultContracts.GetContent)
  → ContentResolver 读取 Uri → 写入 cacheDir 临时文件
  → ChatViewModel.uploadImage(tempFile)
    → file.asRequestBody("image/*")
    → ApiService.uploadFile(MultipartBody.Part)
    → 拼接预览 URL: {baseUrl}api/v1/uploads/preview/file?file_path={relativePath}
    → 设置 transcribedText = "[图片](url)"  // Markdown 格式
    → file.delete()

5. 关键类的职责与依赖关系

                        ┌───────────────────────┐
                        │    TiangongApplication │  (Hilt Entry Point)
                        └───────────┬───────────┘
                                    │ @HiltAndroidApp
                        ┌───────────▼───────────┐
                        │      MainActivity      │  (Single Activity)
                        └───────────┬───────────┘
                                    │ setContent { NavHost }
        ┌───────────────────────────┼───────────────────────────┐
        │                           │                           │
  LoginScreen              ChatScreen                  AgentListScreen
  RegisterScreen           ConversationListScreen      SettingsScreen
        │                           │                           │
  LoginViewModel             ChatViewModel             AgentListViewModel
  RegisterViewModel        ConversationListVM
        │                           │                           │
  AuthRepository          ChatRepository ◄──┐         AgentRepository
        │                  │         │      │              │
        │         ┌────────┼────┐    │      │              │
        │         │        │    │    │      │              │
   ApiService  SseClient  AppDatabase │      │        ApiService
        │         │        │    │     │      │
   TokenDataStore  OkHttpClient │    │      │
   CredentialStore        Room DAOs    │      │
                              │        │      │
                        MessageDao  ConversationDao

依赖注入关系说明:

  • 所有 ViewModel 通过 @HiltViewModel + @Inject constructor 获取依赖
  • 所有 Repository 通过 @Singleton + @Inject constructor 由 Hilt 管理
  • ApiService 是 Retrofit 接口,由 Hilt Module 提供实例
  • SseClient 直接注入 OkHttpClient + Gson + TokenDataStore
  • TokenDataStore 通过 @ApplicationContext 获取 Context 初始化 DataStore

6. 从源码中发现的实际代码质量问题

🔴 严重问题

ID 问题 位置 影响
B1 runBlocking 在 OkHttp 回调线程调用 SseClient.kt:66val baseUrl = runBlocking { tokenDataStore.serverUrl.first() } 阻塞 OkHttp 网络线程,可能导致 ANR 或连接超时。runBlockingcallbackFlow {} builder 内运行在 OkHttp 的分发线程上
B2 OkHttp Callback 中 Thread.sleep() SseClient.kt:90,108,140 — 重连等待使用 Thread.sleep(delay) 阻塞 OkHttp 线程池,多连接下可能耗尽 OkHttp dispatcher 线程
B3 硬编码默认凭据 LoginViewModel.kt:3val username: String = "admin", val password: String = "123456" 安全漏洞,发布到生产环境会让攻击者轻松猜测默认凭据
B4 clearAll() 实现与注释矛盾 TokenDataStore.kt:85-88 — 注释说保留 server_url 和 theme但实际执行 prefs.clear() 用户退出登录后可能丢失服务器地址配置

🟡 中等问题

ID 问题 位置 影响
M1 ViewModel 绕过 Repository 直接调用 ApiService ChatViewModel.ktspeakText(), transcribeVoice(), uploadImage() 方法内直接使用 apiServicetokenDataStore 破坏架构分层,测试困难,无法 mock
M2 流式消息在 Final 事件前不持久化 ChatViewModel.kt — 仅在 SseEvent.Final 后调用 ChatRepository.saveMessage() App 崩溃或断网时,已流式接收的消息全部丢失
M3 缺少 Token 过期自动刷新机制 SseClient.kt:95 — 401 直接 close()AuthRepositoryrefreshToken 方法 Token 过期后用户必须手动重新登录
M4 parseEvent() 对未知 type 默认创建 SseEvent.Unknown SseClient.kt:178-181else → SseEvent.Unknown 服务端新增事件类型可能导致前端默默忽略而非报错
M5 ChatViewModel 依赖过多10 个注入参数) ChatViewModel.kt 构造函数 违反单一职责原则,建议拆分为 AudioHandler、ImageHandler 等

🟢 轻微问题

ID 问题 位置 影响
N1 搜索输入每次变化都触发网络请求 AgentListViewModel.kt:42-44onSearchQueryChange 直接调用 loadAgents() 快速输入时产生大量冗余 API 请求,无防抖(debounce)
N2 BaseURL 硬编码局域网 IP app/build.gradle.kts:23http://192.168.31.150:8037/ 只能在该局域网使用,无法切换环境
N3 异常吞没 ChatViewModel.ktspeakText()catch (_: Exception) { } 完全忽略异常 用户点击朗读无反应时无任何错误提示
N4 ConversationListViewModel.loadConversations() 每次加载取消上一个 Job ConversationListScreen.kt:51loadJob?.cancel() 频繁切换筛选时可能丢失中间状态
N5 Room role 字段使用 String 值枚举 ChatRepository.kt:147-151Message.Role.valueOf(entity.role.uppercase()) 失败时 default=SYSTEM 类型不安全,数据库中存在非预期值时静默降级
N6 临时文件未统一管理 ChatScreen/ChatViewModel — 图片和音频临时文件写入 cacheDir,依赖 file.delete() 异常时可能产生孤儿文件
N7 ProGuard 代码混淆未启用 app/build.gradle.kts:27isMinifyEnabled = false Release 构建无混淆保护
N8 RegisterScreen 密码明文传输 RegisterRequest DTO — 密码字段未被加密 即使走 HTTPS也应在客户端做一次哈希后再发送

7. 架构模式总结

┌─────────────────────────────────────────────────────┐
│                    架构模式: MVVM + UDF               │
│                                                      │
│  UI Layer (Compose)     ViewModel Layer    Data Layer│
│  ┌──────────┐        ┌──────────────┐   ┌─────────┐ │
│  │  Screen  │──State─│  ViewModel   │──▶│Repository│ │
│  │          │◀─Flow──│  (StateFlow) │◀──│         │ │
│  │  Events  │───────▶│              │   │  ┌─────┐│ │
│  │  (click) │        │ viewModelScope│  │  │ApiSvc││ │
│  └──────────┘        └──────────────┘   │  ├─────┤│ │
│                                          │  │Room  ││ │
│                                          │  ├─────┤│ │
│                                          │  │DataSt││ │
│                                          │  └─────┘│ │
│                                          └─────────┘ │
└─────────────────────────────────────────────────────┘

数据流向特点:

  • 单向数据流 (UDF): UI → Events → ViewModel → Repository → DataSource → State → UI
  • StateFlow 驱动: 所有 ViewModel 通过 MutableStateFlow<UiState> + asStateFlow() 暴露只读状态
  • viewModelScope 管理协程: 所有异步操作绑定到 ViewModel 生命周期
  • Flow 用于响应式数据: Room DAO 返回 Flow<List<T>>Repository 用 map 转换为 Domain Model
  • callbackFlow 用于 SSE: 将 OkHttp 回调风格的事件源桥接到 Kotlin Flow

8. 附录:分析引用的源文件清单

# 文件路径 读取状态 用途
1 app/build.gradle.kts 完整 构建配置、SDK 版本、依赖声明
2 gradle/libs.versions.toml 完整 全量版本号目录
3 ui/agents/AgentListViewModel.kt 完整 智能体列表 ViewModel
4 ui/chat/ChatViewModel.kt 截断(8KB) 聊天核心 ViewModelSSE 消费TTS/ASR/图片
5 ui/login/LoginViewModel.kt 完整 登录 ViewModel
6 ui/register/RegisterViewModel.kt 完整 注册 ViewModel + 客户端验证
7 ui/agents/AgentListScreen.kt 完整 智能体列表 Compose UI
8 ui/chat/ChatScreen.kt 截断(8KB) 聊天 Compose UI
9 ui/history/ConversationListScreen.kt 截断(8KB) 对话历史 Compose UI + ViewModel
10 ui/login/LoginScreen.kt 完整 登录 Compose UI
11 ui/register/RegisterScreen.kt 截断(8KB) 注册 Compose UI
12 data/repository/AuthRepository.kt 完整 认证仓库
13 data/remote/ApiService.kt 完整 Retrofit API 接口定义
14 data/repository/ChatRepository.kt 完整 聊天仓库 + Room 持久化
15 data/local/TokenDataStore.kt 完整 DataStore 键值存储
16 domain/model/Message.kt 完整 消息领域模型
17 data/remote/SseClient.kt 完整 SSE 流式客户端

未读取但推测存在的文件: AppDatabase.kt, ConversationEntity.kt, MessageEntity.kt, ConversationDao.kt, MessageDao.kt, CredentialStore.kt, AuthInterceptor.kt, AgentRepository.kt, FeedbackRepository.kt, Agent.kt, Conversation.kt, SseEvent.kt, MainActivity.kt, TiangongApplication.kt, 各 DTO 文件, DI 模块, 导航图, 各 UI 组件 (MessageBubble, StreamingText, ToolCallCard, VoiceInputButton, Skeleton*)