add implementation plan

This commit is contained in:
xiaji
2026-05-08 22:03:03 +08:00
parent 49fdb06753
commit dd40597355

View File

@@ -0,0 +1,461 @@
# 多模型并行调用功能实现计划
**Goal:** 支持同时配置3个LLM模型主页面同时显示3个结果框发送时并行调用所有已配置的模型
**Architecture:** 配置页面同时显示3组模型配置主页结果显示区改为水平布局3个卡片发送时并行调用所有模型
**Tech Stack:** Kotlin, Android, OkHttp, Coroutine, SharedPreferences
---
## 任务1: 修改 SecondActivity.kt 数据结构
**Files:**
- Modify: `flomo-ai/app/src/main/java/com/example/flomo_ai/SecondActivity.kt`
- [ ] **Step 1: 修改 LLMConfig 类,添加 enabled 字段**
在第650行 `data class LLMConfig` 中添加:
```kotlin
data class LLMConfig(
val name: String,
val baseUrl: String,
val apiKey: String,
val model: String,
val enabled: Boolean = true // 新增:是否启用
)
```
- [ ] **Step 2: 修改 SettingsData 类**
在第663行 `data class SettingsData` 中,将 `llmConfigs` 类型改为固定3个元素的列表:
```kotlin
data class SettingsData(
val llmConfigs: List<LLMConfig>,
val selectedLlmIndex: Int?,
val headerConfigs: List<HeaderConfig>?,
val promptConfigs: List<PromptConfig>?,
val buttonConfigs: List<ButtonConfig>?,
val noteApiConfig: NoteApiConfig?,
val llmConfig: LLMConfig? = null
)
```
- [ ] **Step 3: 提交**
```bash
git add flomo-ai/app/src/main/java/com/example/flomo_ai/SecondActivity.kt
git commit -m "feat: add enabled field to LLMConfig"
```
---
## 任务2: 修改 activity_second.xml 布局
**Files:**
- Modify: `flomo-ai/app/src/main/res/layout/activity_second.xml`
- [ ] **Step 1: 将现有卡片内容包装为可复用的 LinearLayout**
保留第一组配置作为模板。新增2组相同的结构。
每组配置需要唯一ID
- 第一组: etModelName1, etBaseUrl1, etApiKey1, btnToggleApiKey1, etModel1, btnTestConnection1, tvTestStatus1
- 第二组: etModelName2, etBaseUrl2, etApiKey2, btnToggleApiKey2, etModel2, btnTestConnection2, tvTestStatus2
- 第三组: etModelName3, etBaseUrl3, etApiKey3, btnToggleApiKey3, etModel3, btnTestConnection3, tvTestStatus3
布局结构:
```xml
<!-- 第一组模型配置 -->
<androidx.cardview.widget.CardView>
<LinearLayout>
<TextView android:text="模型1"/>
<!-- 配置名称、Base URL、API Key、Model、测试按钮 -->
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- 第二组模型配置 -->
<androidx.cardview.widget.CardView>
<!-- 同上结构ID带2 -->
</androidx.cardview.widget.CardView>
<!-- 第三组模型配置 -->
<androidx.cardview.widget.CardView>
<!-- 同上结构ID带3 -->
</androidx.cardview.widget.CardView>
```
- [ ] **Step 2: 提交**
```bash
git add flomo-ai/app/src/main/res/layout/activity_second.xml
git commit -m "feat: add 3 model config cards layout"
```
---
## 任务3: 修改 SecondActivity.kt 逻辑
**Files:**
- Modify: `flomo-ai/app/src/main/java/com/example/flomo_ai/SecondActivity.kt:152-700`
- [ ] **Step 1: 添加3组视图引用**
在第46行后添加3组EditText和Button引用:
```kotlin
private lateinit var etBaseUrl1: EditText
private lateinit var etApiKey1: EditText
private lateinit var etModel1: EditText
private lateinit var etModelName1: EditText
private lateinit var btnTestConnection1: Button
private lateinit var tvTestStatus1: TextView
private lateinit var etBaseUrl2: EditText
private lateinit var etApiKey2: EditText
private lateinit var etModel2: EditText
private lateinit var etModelName2: EditText
private lateinit var btnTestConnection2: Button
private lateinit var tvTestStatus2: TextView
private lateinit var etBaseUrl3: EditText
private lateinit var etApiKey3: EditText
private lateinit var etModel3: EditText
private lateinit var etModelName3: EditText
private lateinit var btnTestConnection3: Button
private lateinit var tvTestStatus3: TextView
```
- [ ] **Step 2: 修改 initViews() 方法,初始化所有视图**
```kotlin
etBaseUrl1 = findViewById(R.id.etBaseUrl1)
// ... 其他第一组视图
etBaseUrl2 = findViewById(R.id.etBaseUrl2)
// ... 其他第二组视图
etBaseUrl3 = findViewById(R.id.etBaseUrl3)
// ... 其他第三组视图
```
- [ ] **Step 3: 修改 loadConfigurations() 方法加载3个配置**
从 SharedPreferences 加载3个配置填充到对应视图。如果配置不足3个补齐空配置。
- [ ] **Step 4: 修改 saveConfigurations() 方法保存3个配置**
从3个视图组读取数据保存为包含3个元素的列表。
- [ ] **Step 5: 添加3个独立的测试方法**
```kotlin
private fun testConnection1() { testConnection(etBaseUrl1, etApiKey1, etModel1, tvTestStatus1, btnTestConnection1) }
private fun testConnection2() { testConnection(etBaseUrl2, etApiKey2, etModel2, tvTestStatus2, btnTestConnection2) }
private fun testConnection3() { testConnection(etBaseUrl3, etApiKey3, etModel3, tvTestStatus3, btnTestConnection3) }
```
- [ ] **Step 6: 设置3个测试按钮的点击事件**
```kotlin
btnTestConnection1.setOnClickListener { testConnection1() }
btnTestConnection2.setOnClickListener { testConnection2() }
btnTestConnection3.setOnClickListener { testConnection3() }
```
- [ ] **Step 7: 提交**
```bash
git add flomo-ai/app/src/main/java/com/example/flomo_ai/SecondActivity.kt
git commit -m "feat: support 3 model configs with individual test"
```
---
## 任务4: 修改 activity_main.xml 布局
**Files:**
- Modify: `flomo-ai/app/src/main/res/layout/activity_main.xml`
- [ ] **Step 1: 修改结果显示区为水平布局**
将原来的单个结果卡片改为水平 ScrollView + LinearLayout包含3个结果卡片。
```xml
<!-- 结果显示区标题 -->
<TextView android:text="大模型返回结果"/>
<!-- 水平滚动容器 -->
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<!-- 结果卡片1: 300dp宽 -->
<include layout="@layout/result_card" android:id="@+id/resultCard1"/>
<!-- 结果卡片2: 300dp宽 -->
<include layout="@layout/result_card" android:id="@+id/resultCard2"/>
<!-- 结果卡片3: 300dp宽 -->
<include layout="@layout/result_card" android:id="@+id/resultCard3"/>
</LinearLayout>
</HorizontalScrollView>
```
- [ ] **Step 2: 创建 result_card.xml 布局**
创建 `flomo-ai/app/src/main/res/layout/result_card.xml`:
```xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/result_card_bg"
android:padding="16dp"
android:layout_marginEnd="12dp">
<TextView
android:id="@+id/tvModelName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textColor="@color/text_secondary"
android:textStyle="bold"/>
<View
android:layout_width="match_parent"
android:layout_height="2dp"
android:background="@color/primary"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/outputStatusLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="12sp"
android:textColor="@color/primary"/>
<EditText
android:id="@+id/outputTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minLines="3"
android:textSize="14sp"
android:editable="true"
android:gravity="top"
android:background="@android:drawable/edit_text"/>
<Button
android:id="@+id/btnCopyResult"
android:layout_width="match_parent"
android:layout_height="32dp"
android:layout_marginTop="8dp"
android:text="复制结果"/>
</LinearLayout>
```
- [ ] **Step 3: 提交**
```bash
git add flomo-ai/app/src/main/res/layout/activity_main.xml flomo-ai/app/src/main/res/layout/result_card.xml
git commit -m "feat: add horizontal result cards layout"
```
---
## 任务5: 修改 MainActivity.kt 逻辑
**Files:**
- Modify: `flomo-ai/app/src/main/java/com/example/flomo_ai/MainActivity.kt`
- [ ] **Step 1: 添加3组视图引用**
```kotlin
private lateinit var outputTextView1: EditText
private lateinit var outputTextView2: EditText
private lateinit var outputTextView3: EditText
private lateinit var outputStatusLabel1: TextView
private lateinit var outputStatusLabel2: TextView
private lateinit var outputStatusLabel3: TextView
private lateinit var tvModelName1: TextView
private lateinit var tvModelName2: TextView
private lateinit var tvModelName3: TextView
private lateinit var btnCopyResult1: Button
private lateinit var btnCopyResult2: Button
private lateinit var btnCopyResult3: Button
```
- [ ] **Step 2: 在 onCreate() 中初始化视图**
```kotlin
outputTextView1 = findViewById(R.id.outputTextView1)
outputTextView2 = findViewById(R.id.outputTextView2)
outputTextView3 = findViewById(R.id.outputTextView3)
// ... 其他视图
```
- [ ] **Step 3: 修改 sendButton.setOnClickListener并行发送**
```kotlin
sendButton.setOnClickListener {
val inputText = inputEditText.text.toString()
if (inputText.isEmpty()) {
Toast.makeText(this, "请输入内容", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
val promptContent = promptContentText.text.toString()
val fullContent = if (promptContent.isNotEmpty() && promptContent != "无特殊指令") {
"$promptContent$inputText"
} else {
inputText
}
// 获取所有已启用的模型配置
val llmConfigs = loadLlmConfigs()
// 初始化所有状态
llmConfigs.forEachIndexed { index, config ->
when(index) {
0 -> { outputTextView1.setText(""); outputStatusLabel1.text = "等待中..." }
1 -> { outputTextView2.setText(""); outputStatusLabel2.text = "等待中..." }
2 -> { outputTextView3.setText(""); outputStatusLabel3.text = "等待中..." }
}
}
// 并行发送请求
llmConfigs.forEachIndexed { index, config ->
if (config.apiKey.isBlank() || config.baseUrl.isBlank()) {
// 跳过未配置的模型
when(index) {
0 -> outputStatusLabel1.text = "未配置"
1 -> outputStatusLabel2.text = "未配置"
2 -> outputStatusLabel3.text = "未配置"
}
return@forEachIndexed
}
CoroutineScope(Dispatchers.IO).launch {
try {
val result = callLlm(config, fullContent)
withContext(Dispatchers.Main) {
when(index) {
0 -> {
outputTextView1.setText(result)
outputStatusLabel1.text = if (result.startsWith("错误:")) "失败" else "完成"
}
1 -> {
outputTextView2.setText(result)
outputStatusLabel2.text = if (result.startsWith("错误:")) "失败" else "完成"
}
2 -> {
outputTextView3.setText(result)
outputStatusLabel3.text = if (result.startsWith("错误:")) "失败" else "完成"
}
}
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
when(index) {
0 -> { outputTextView1.setText("错误: ${e.message}"); outputStatusLabel1.text = "失败" }
1 -> { outputTextView2.setText("错误: ${e.message}"); outputStatusLabel2.text = "失败" }
2 -> { outputTextView3.setText("错误: ${e.message}"); outputStatusLabel3.text = "失败" }
}
}
}
}
}
}
```
- [ ] **Step 4: 添加 loadLlmConfigs() 和 callLlm() 方法**
```kotlin
private fun loadLlmConfigs(): List<LLMConfig> {
val sharedPrefs = getSharedPreferences("APIConfigs", Context.MODE_PRIVATE)
val json = sharedPrefs.getString("configs", null) ?: return emptyList()
return try {
val settings = Gson().fromJson(json, SettingsData::class.java)
settings.llmConfigs ?: emptyList()
} catch (e: Exception) {
emptyList()
}
}
private suspend fun callLlm(config: LLMConfig, content: String): String {
return withContext(Dispatchers.IO) {
val messagesJson = JSONArray().apply {
put(JSONObject().apply {
put("role", "user")
put("content", content)
})
}
val requestBody = JSONObject().apply {
put("model", config.model.ifEmpty { "gpt-4o" })
put("messages", messagesJson)
put("max_tokens", 65536)
}
val client = OkHttpClient()
val requestBuilder = Request.Builder()
.url("${config.baseUrl}/chat/completions")
.addHeader("Content-Type", "application/json")
if (config.apiKey.isNotBlank()) {
requestBuilder.addHeader("Authorization", "Bearer ${config.apiKey}")
}
val request = requestBuilder
.post(requestBody.toString().toRequestBody("application/json".toMediaType()))
.build()
val response = client.newCall(request).execute()
if (response.isSuccessful) {
val responseBody = response.body?.string()
val responseJson = JSONObject(responseBody ?: "")
val choices = responseJson.getJSONArray("choices")
if (choices.length() > 0) {
choices.getJSONObject(0).getJSONObject("message").getString("content")
} else {
"错误: API返回空结果"
}
} else {
"错误: ${response.code} ${response.message}"
}
}
}
```
- [ ] **Step 5: 添加3个复制按钮的点击事件**
```kotlin
btnCopyResult1.setOnClickListener { copyToClipboard(outputTextView1.text.toString()) }
btnCopyResult2.setOnClickListener { copyToClipboard(outputTextView2.text.toString()) }
btnCopyResult3.setOnClickListener { copyToClipboard(outputTextView3.text.toString()) }
```
- [ ] **Step 6: 提交**
```bash
git add flomo-ai/app/src/main/java/com/example/flomo_ai/MainActivity.kt
git commit -m "feat: parallel LLM calls with 3 result cards"
```
---
## 自检清单
- [ ] 所有3个模型配置都能独立测试
- [ ] 主页面显示3个水平排列的结果卡片
- [ ] 发送时并行调用所有已配置的模型
- [ ] 每个结果卡片独立显示各自的结果
- [ ] 每个结果卡片有独立的复制按钮
- [ ] 配置保存后重启应用能正确加载