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>
This commit is contained in:
466
android/architecture_context.md
Normal file
466
android/architecture_context.md
Normal file
@@ -0,0 +1,466 @@
|
||||
# 天工智能体 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, 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)` → `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` + `TokenDataStore`
|
||||
- `TokenDataStore` 通过 `@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*`)
|
||||
Reference in New Issue
Block a user