- 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>
25 KiB
天工智能体 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_token、server_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:22—login()的catch捕获所有异常不区分类型,网络异常和业务异常混杂TokenDataStore.kt:86-88—clearAll()注释说 "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.kt(10 个依赖):
ChatRepository, AgentRepository, FeedbackRepository,
AudioRecorder, AudioPlayer, ApiService,
TokenDataStore, SavedStateHandle, Gson
⚠️ 高依赖耦合: ViewModel 直接依赖 ApiService(绕过 Repository 层),对于 speakText()(TTS)、transcribeVoice()、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, messageCountMessageEntity— 字段: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()批量保存 stepschatStream()(流式): 不自动保存,由 ViewModel 在收到SseEvent.Final后调用saveMessage()逐条保存saveMessage()同时 upsertConversationEntity
⚠️ 风险: 流式消息在 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)→FeedbackRepository→ApiService.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+TokenDataStoreTokenDataStore通过@ApplicationContext获取 Context 初始化 DataStore
6. 从源码中发现的实际代码质量问题
🔴 严重问题
| ID | 问题 | 位置 | 影响 |
|---|---|---|---|
| B1 | runBlocking 在 OkHttp 回调线程调用 |
SseClient.kt:66 — val baseUrl = runBlocking { tokenDataStore.serverUrl.first() } |
阻塞 OkHttp 网络线程,可能导致 ANR 或连接超时。runBlocking 在 callbackFlow {} builder 内运行在 OkHttp 的分发线程上 |
| B2 | OkHttp Callback 中 Thread.sleep() |
SseClient.kt:90,108,140 — 重连等待使用 Thread.sleep(delay) |
阻塞 OkHttp 线程池,多连接下可能耗尽 OkHttp dispatcher 线程 |
| B3 | 硬编码默认凭据 | LoginViewModel.kt:3 — val username: String = "admin", val password: String = "123456" |
安全漏洞,发布到生产环境会让攻击者轻松猜测默认凭据 |
| B4 | clearAll() 实现与注释矛盾 |
TokenDataStore.kt:85-88 — 注释说保留 server_url 和 theme,但实际执行 prefs.clear() |
用户退出登录后可能丢失服务器地址配置 |
🟡 中等问题
| ID | 问题 | 位置 | 影响 |
|---|---|---|---|
| M1 | ViewModel 绕过 Repository 直接调用 ApiService | ChatViewModel.kt — speakText(), transcribeVoice(), uploadImage() 方法内直接使用 apiService 和 tokenDataStore |
破坏架构分层,测试困难,无法 mock |
| M2 | 流式消息在 Final 事件前不持久化 | ChatViewModel.kt — 仅在 SseEvent.Final 后调用 ChatRepository.saveMessage() |
App 崩溃或断网时,已流式接收的消息全部丢失 |
| M3 | 缺少 Token 过期自动刷新机制 | SseClient.kt:95 — 401 直接 close(),AuthRepository 无 refreshToken 方法 |
Token 过期后用户必须手动重新登录 |
| M4 | parseEvent() 对未知 type 默认创建 SseEvent.Unknown |
SseClient.kt:178-181 — else → SseEvent.Unknown |
服务端新增事件类型可能导致前端默默忽略而非报错 |
| M5 | ChatViewModel 依赖过多(10 个注入参数) | ChatViewModel.kt 构造函数 |
违反单一职责原则,建议拆分为 AudioHandler、ImageHandler 等 |
🟢 轻微问题
| ID | 问题 | 位置 | 影响 |
|---|---|---|---|
| N1 | 搜索输入每次变化都触发网络请求 | AgentListViewModel.kt:42-44 — onSearchQueryChange 直接调用 loadAgents() |
快速输入时产生大量冗余 API 请求,无防抖(debounce) |
| N2 | BaseURL 硬编码局域网 IP | app/build.gradle.kts:23 — http://192.168.31.150:8037/ |
只能在该局域网使用,无法切换环境 |
| N3 | 异常吞没 | ChatViewModel.kt — speakText() 中 catch (_: Exception) { } 完全忽略异常 |
用户点击朗读无反应时无任何错误提示 |
| N4 | ConversationListViewModel.loadConversations() 每次加载取消上一个 Job |
ConversationListScreen.kt:51 — loadJob?.cancel() |
频繁切换筛选时可能丢失中间状态 |
| N5 | Room role 字段使用 String 值枚举 | ChatRepository.kt:147-151 — Message.Role.valueOf(entity.role.uppercase()) 失败时 default=SYSTEM |
类型不安全,数据库中存在非预期值时静默降级 |
| N6 | 临时文件未统一管理 | ChatScreen/ChatViewModel — 图片和音频临时文件写入 cacheDir,依赖 file.delete() |
异常时可能产生孤儿文件 |
| N7 | ProGuard 代码混淆未启用 | app/build.gradle.kts:27 — isMinifyEnabled = 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) | 聊天核心 ViewModel,SSE 消费,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*)