Files
flomo-ai/docs/superpowers/plans/2026-05-08-multi-llm-plan.md
2026-05-08 22:03:03 +08:00

461 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 多模型并行调用功能实现计划
**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个水平排列的结果卡片
- [ ] 发送时并行调用所有已配置的模型
- [ ] 每个结果卡片独立显示各自的结果
- [ ] 每个结果卡片有独立的复制按钮
- [ ] 配置保存后重启应用能正确加载