add implementation plan
This commit is contained in:
461
docs/superpowers/plans/2026-05-08-multi-llm-plan.md
Normal file
461
docs/superpowers/plans/2026-05-08-multi-llm-plan.md
Normal 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个水平排列的结果卡片
|
||||
- [ ] 发送时并行调用所有已配置的模型
|
||||
- [ ] 每个结果卡片独立显示各自的结果
|
||||
- [ ] 每个结果卡片有独立的复制按钮
|
||||
- [ ] 配置保存后重启应用能正确加载
|
||||
Reference in New Issue
Block a user