支持多个大模型配置,可添加、选择、切换模型

This commit is contained in:
xiaji
2026-04-05 23:09:18 +08:00
parent ea90850b02
commit e0e257e373
4 changed files with 281 additions and 97 deletions

View File

@@ -40,17 +40,20 @@ class MainActivity : AppCompatActivity() {
private lateinit var promptSelector: Spinner
private lateinit var promptNameText: TextView
private lateinit var promptContentText: TextView
private lateinit var headerModelSelector: Spinner
// Data classes matching SecondActivity
data class HeaderConfig(val key: String, val value: String)
data class PromptConfig(val id: String, val title: String, val content: String, val expanded: Boolean = false)
data class ButtonConfig(val id: String, val label: String, val action: String, val apiUrl: String? = null, val apiMethod: String? = null, val apiBodyTemplate: String? = null, val expanded: Boolean = false)
data class LLMConfig(val baseUrl: String, val apiKey: String, val model: String)
data class LLMConfig(val name: String, val baseUrl: String, val apiKey: String, val model: String)
data class SettingsData(
val llmConfig: LLMConfig?,
val llmConfigs: List<LLMConfig>?,
val selectedLlmIndex: Int?,
val headerConfigs: List<HeaderConfig>?,
val promptConfigs: List<PromptConfig>?,
val buttonConfigs: List<ButtonConfig>?
val buttonConfigs: List<ButtonConfig>?,
val llmConfig: LLMConfig? = null
)
@SuppressLint("MissingInflatedId", "CutPasteId", "SetTextI18n")
@@ -71,12 +74,11 @@ class MainActivity : AppCompatActivity() {
outputTextView = findViewById<EditText>(R.id.outputTextView)
val btnCopyResult = findViewById<Button>(R.id.btnCopyResult)
val headerTitle = findViewById<TextView>(R.id.headerTitle)
val headerModelName = findViewById<TextView>(R.id.headerModelName)
headerModelSelector = findViewById<Spinner>(R.id.headerModelSelector)
Log.d("MainActivity", "onCreate: Views initialized")
headerTitle.text = "AI优化"
val savedModel = loadModelFromConfig()
headerModelName.text = if (savedModel.isNotBlank()) savedModel else "GPT-4o"
loadModelsFromConfig()
// Initialize quick action buttons
initQuickButtons()
@@ -129,21 +131,10 @@ class MainActivity : AppCompatActivity() {
CoroutineScope(Dispatchers.IO).launch {
try {
val sharedPrefs = getSharedPreferences("APIConfigs", Context.MODE_PRIVATE)
val json = sharedPrefs.getString("configs", null)
var baseUrl = "https://open.bigmodel.cn/api/paas/v4"
var apiKey = ""
var model = "glm-4.7-flash"
if (json != null) {
val settings = Gson().fromJson(json, SettingsData::class.java)
settings.llmConfig?.let {
if (it.baseUrl.isNotBlank()) baseUrl = it.baseUrl
if (it.apiKey.isNotBlank()) apiKey = it.apiKey
if (it.model.isNotBlank()) model = it.model
}
}
val llmConfig = getSelectedModelConfig()
val baseUrl = llmConfig.baseUrl.ifEmpty { "https://open.bigmodel.cn/api/paas/v4" }
val apiKey = llmConfig.apiKey
val model = llmConfig.model.ifEmpty { "glm-4.7-flash" }
val messagesJson = JSONArray().apply {
put(JSONObject().apply {
@@ -261,22 +252,110 @@ class MainActivity : AppCompatActivity() {
}
}
private fun loadModelFromConfig(): String {
return try {
private fun loadModelsFromConfig() {
try {
val sharedPrefs = getSharedPreferences("APIConfigs", Context.MODE_PRIVATE)
val json = sharedPrefs.getString("configs", null)
var models = mutableListOf<LLMConfig>()
var selectedIndex = 0
if (json != null) {
val settings = Gson().fromJson(json, SettingsData::class.java)
settings.llmConfig?.model?.takeIf { it.isNotBlank() } ?: ""
} else {
""
try {
val settings = Gson().fromJson(json, SettingsData::class.java)
models = settings.llmConfigs?.toMutableList() ?: mutableListOf()
selectedIndex = settings.selectedLlmIndex ?: 0
if (models.isEmpty()) {
settings.llmConfig?.let { legacy ->
models.add(LLMConfig(
name = "默认配置",
baseUrl = legacy.baseUrl,
apiKey = legacy.apiKey,
model = legacy.model
))
}
}
} catch (e: Exception) {
Log.e("MainActivity", "Error parsing settings, using defaults", e)
}
}
if (models.isEmpty()) {
models.add(LLMConfig(
name = "默认配置",
baseUrl = "https://open.bigmodel.cn/api/paas/v4",
apiKey = "",
model = "glm-4.7-flash"
))
}
if (selectedIndex >= models.size) {
selectedIndex = 0
}
val names = models.map { it.name.ifEmpty { "未命名" } }
val adapter = ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, names)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
headerModelSelector.adapter = adapter
headerModelSelector.setSelection(selectedIndex)
headerModelSelector.onItemSelectedListener = object : android.widget.AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: android.widget.AdapterView<*>, view: android.view.View?, position: Int, id: Long) {
Log.d("MainActivity", "Model selected: ${models[position].name}")
}
override fun onNothingSelected(parent: android.widget.AdapterView<*>) {}
}
Log.d("MainActivity", "Loaded ${models.size} model configs")
} catch (e: Exception) {
Log.e("MainActivity", "Error loading model from config", e)
""
Log.e("MainActivity", "Error loading models from config", e)
}
}
private fun getSelectedModelConfig(): LLMConfig {
val sharedPrefs = getSharedPreferences("APIConfigs", Context.MODE_PRIVATE)
val json = sharedPrefs.getString("configs", null)
var models = mutableListOf<LLMConfig>()
var selectedIndex = headerModelSelector.selectedItemPosition
if (json != null) {
try {
val settings = Gson().fromJson(json, SettingsData::class.java)
models = settings.llmConfigs?.toMutableList() ?: mutableListOf()
if (models.isEmpty()) {
settings.llmConfig?.let { legacy ->
models.add(LLMConfig(
name = "默认配置",
baseUrl = legacy.baseUrl,
apiKey = legacy.apiKey,
model = legacy.model
))
}
}
} catch (e: Exception) {
Log.e("MainActivity", "Error parsing settings", e)
}
}
if (models.isEmpty()) {
return LLMConfig(
name = "默认配置",
baseUrl = "https://open.bigmodel.cn/api/paas/v4",
apiKey = "",
model = "glm-4.7-flash"
)
}
if (selectedIndex >= models.size) {
selectedIndex = 0
}
return models[selectedIndex]
}
private fun loadPromptsFromConfig() {
Log.d("MainActivity", "loadPromptsFromConfig: Starting")
try {

View File

@@ -46,10 +46,14 @@ data class ButtonConfig(val id: String, val label: String, val action: String, v
class SecondActivity : AppCompatActivity() {
// View references
private lateinit var spModelSelector: Spinner
private lateinit var llModelList: LinearLayout
private lateinit var btnAddModel: Button
private lateinit var etBaseUrl: EditText
private lateinit var etApiKey: EditText
private lateinit var btnToggleApiKey: ImageButton
private lateinit var etModel: EditText
private lateinit var etModelName: EditText
private lateinit var btnTestConnection: Button
private lateinit var tvTestStatus: TextView
private lateinit var llHeadersList: LinearLayout
@@ -78,6 +82,8 @@ class SecondActivity : AppCompatActivity() {
private lateinit var btnToggleNoteApiKey: ImageButton
// Data storage
private var llmConfigs = mutableListOf<LLMConfig>()
private var selectedLlmIndex = 0
private var headerConfigs = mutableListOf<HeaderConfig>()
private var promptConfigs = mutableListOf<PromptConfig>()
private var buttonConfigs = mutableListOf<ButtonConfig>()
@@ -119,9 +125,7 @@ class SecondActivity : AppCompatActivity() {
// Home button functionality
findViewById<Button>(R.id.btnHome).setOnClickListener {
Log.d("SecondActivity", "Home button clicked")
// Create intent to go back to MainActivity
val intent = Intent(this, MainActivity::class.java)
// Clear the activity stack to start fresh
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
finish()
@@ -153,6 +157,9 @@ class SecondActivity : AppCompatActivity() {
etApiKey = findViewById(R.id.etApiKey)
btnToggleApiKey = findViewById(R.id.btnToggleApiKey)
etModel = findViewById(R.id.etModel)
etModelName = findViewById(R.id.etModelName)
spModelSelector = findViewById(R.id.spModelSelector)
btnAddModel = findViewById(R.id.btnAddModel)
btnTestConnection = findViewById(R.id.btnTestConnection)
tvTestStatus = findViewById(R.id.tvTestStatus)
@@ -189,14 +196,12 @@ class SecondActivity : AppCompatActivity() {
Log.d("SecondActivity", "API key toggle clicked")
val isPassword = etApiKey.transformationMethod is PasswordTransformationMethod
etApiKey.transformationMethod = if (isPassword) null else PasswordTransformationMethod()
// Move cursor to end
etApiKey.setSelection(etApiKey.text.length)
// Update icon based on state
if (isPassword) {
btnToggleApiKey.setImageResource(android.R.drawable.ic_menu_view) // Show eye
btnToggleApiKey.setImageResource(android.R.drawable.ic_menu_view)
} else {
btnToggleApiKey.setImageResource(android.R.drawable.ic_lock_idle_lock) // Show lock
btnToggleApiKey.setImageResource(android.R.drawable.ic_lock_idle_lock)
}
}
@@ -206,10 +211,10 @@ class SecondActivity : AppCompatActivity() {
val isExpanded = layoutHeaderContent.visibility == View.VISIBLE
if (isExpanded) {
layoutHeaderContent.visibility = View.GONE
ivHeaderArrow.rotation = 0f // Point right
ivHeaderArrow.rotation = 0f
} else {
layoutHeaderContent.visibility = View.VISIBLE
ivHeaderArrow.rotation = 90f // Point down
ivHeaderArrow.rotation = 90f
}
}
@@ -219,10 +224,10 @@ class SecondActivity : AppCompatActivity() {
val isExpanded = layoutPromptContent.visibility == View.VISIBLE
if (isExpanded) {
layoutPromptContent.visibility = View.GONE
ivPromptArrow.rotation = 0f // Point right
ivPromptArrow.rotation = 0f
} else {
layoutPromptContent.visibility = View.VISIBLE
ivPromptArrow.rotation = 90f // Point down
ivPromptArrow.rotation = 90f
}
}
@@ -253,14 +258,12 @@ class SecondActivity : AppCompatActivity() {
private fun loadConfigurations() {
Log.d("SecondActivity", "loadConfigurations: Starting")
// Load shared preferences
val sharedPrefs = getSharedPreferences("APIConfigs", Context.MODE_PRIVATE)
val json = sharedPrefs.getString("configs", null)
Log.d("SecondActivity", "loadConfigurations: JSON loaded: ${json?.substring(0, minOf(json.length, 100))}")
if (json != null) {
try {
// Try to load as new format first
Log.d("SecondActivity", "loadConfigurations: Trying to parse as SettingsData")
val settings = Gson().fromJson(json, SettingsData::class.java)
Log.d("SecondActivity", "loadConfigurations: SettingsData parsed successfully")
@@ -268,12 +271,27 @@ class SecondActivity : AppCompatActivity() {
promptConfigs = settings.promptConfigs?.toMutableList() ?: mutableListOf()
buttonConfigs = settings.buttonConfigs?.toMutableList() ?: mutableListOf()
// Load LLM config
etBaseUrl.setText(settings.llmConfig?.baseUrl ?: "https://api.openai.com/v1")
etApiKey.setText(settings.llmConfig?.apiKey ?: "")
etModel.setText(settings.llmConfig?.model ?: "gpt-4o")
llmConfigs = settings.llmConfigs?.toMutableList() ?: mutableListOf()
selectedLlmIndex = settings.selectedLlmIndex ?: 0
if (llmConfigs.isEmpty()) {
val legacyBaseUrl = settings.llmConfig?.baseUrl ?: "https://api.openai.com/v1"
val legacyApiKey = settings.llmConfig?.apiKey ?: ""
val legacyModel = settings.llmConfig?.model ?: "gpt-4o"
llmConfigs.add(LLMConfig(
name = "默认配置",
baseUrl = legacyBaseUrl,
apiKey = legacyApiKey,
model = legacyModel
))
}
if (selectedLlmIndex >= llmConfigs.size) {
selectedLlmIndex = 0
}
loadSelectedModelToFields()
// Load Note API config
settings.noteApiConfig?.let { noteConfig ->
val apiTypes = listOf("Flomo", "Notion", "Joplin", "Custom")
val typeIndex = apiTypes.indexOf(noteConfig.apiType)
@@ -284,12 +302,10 @@ class SecondActivity : AppCompatActivity() {
etNoteApiKey.setText(noteConfig.apiKey)
}
// Update API key visibility based on whether it has text
updateApiKeyVisibility()
} catch (e: Exception) {
Log.e("SecondActivity", "loadConfigurations: Error parsing SettingsData", e)
// If new format fails, try to load old format for migration
try {
Log.d("SecondActivity", "loadConfigurations: Trying to parse as List<APIConfig>")
val type = object : TypeToken<List<APIConfig>>() {}.type
@@ -303,14 +319,12 @@ class SecondActivity : AppCompatActivity() {
}
} catch (e2: Exception) {
Log.e("SecondActivity", "loadConfigurations: Error parsing List<APIConfig>", e2)
// If both fail, use defaults
etBaseUrl.setText("https://api.openai.com/v1")
etModel.setText("gpt-4o")
updateApiKeyVisibility()
}
}
} else {
// No saved config, use defaults
Log.d("SecondActivity", "loadConfigurations: No saved config, using defaults")
etBaseUrl.setText("https://api.openai.com/v1")
etModel.setText("gpt-4o")
@@ -319,18 +333,59 @@ class SecondActivity : AppCompatActivity() {
Log.d("SecondActivity", "loadConfigurations: Completed")
}
private fun loadSelectedModelToFields() {
if (llmConfigs.isNotEmpty() && selectedLlmIndex < llmConfigs.size) {
val config = llmConfigs[selectedLlmIndex]
etBaseUrl.setText(config.baseUrl)
etApiKey.setText(config.apiKey)
etModel.setText(config.model)
etModelName.setText(config.name)
}
}
private fun updateApiKeyVisibility() {
val isEmpty = etApiKey.text.toString().isEmpty()
etApiKey.transformationMethod = if (isEmpty) null else PasswordTransformationMethod()
// Keep cursor at end
etApiKey.setSelection(etApiKey.text.length)
}
private fun refreshModelSelector() {
val names = llmConfigs.map { it.name.ifEmpty { "未命名" } }.toMutableList()
val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, names)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spModelSelector.adapter = adapter
}
private fun setupUI() {
refreshModelSelector()
spModelSelector.setOnItemSelectedListener(object : android.widget.AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: android.widget.AdapterView<*>, view: android.view.View?, position: Int, id: Long) {
selectedLlmIndex = position
loadSelectedModelToFields()
}
override fun onNothingSelected(parent: android.widget.AdapterView<*>) {}
})
btnAddModel.setOnClickListener {
val newName = "新配置 ${llmConfigs.size + 1}"
val newConfig = LLMConfig(
name = newName,
baseUrl = etBaseUrl.text.toString().ifEmpty { "https://api.openai.com/v1" },
apiKey = "",
model = "gpt-4o"
)
llmConfigs.add(newConfig)
selectedLlmIndex = llmConfigs.size - 1
loadSelectedModelToFields()
refreshModelSelector()
spModelSelector.setSelection(selectedLlmIndex)
}
// Setup headers
llHeadersList.removeAllViews()
if (headerConfigs.isEmpty()) {
addHeaderEntry() // Add one empty entry by default
addHeaderEntry()
} else {
for (header in headerConfigs) {
addHeaderEntry(header.key, header.value)
@@ -340,7 +395,7 @@ class SecondActivity : AppCompatActivity() {
// Setup prompts
llPromptList.removeAllViews()
if (promptConfigs.isEmpty()) {
addPromptEntry() // Add one empty entry by default
addPromptEntry()
} else {
for (prompt in promptConfigs) {
addPromptEntry(prompt.title, prompt.content)
@@ -358,10 +413,8 @@ class SecondActivity : AppCompatActivity() {
}
private fun setupTheme() {
// Get saved theme mode
val themeMode = ThemeManager.getThemeMode(this)
// Set the correct radio button
when (themeMode) {
ThemeManager.THEME_FOLLOW_SYSTEM -> rbThemeFollowSystem.isChecked = true
ThemeManager.THEME_LIGHT -> rbThemeLight.isChecked = true
@@ -369,7 +422,6 @@ class SecondActivity : AppCompatActivity() {
else -> rbThemeFollowSystem.isChecked = true
}
// Set up radio group listener
rgThemeMode.setOnCheckedChangeListener { _, checkedId ->
val newMode = when (checkedId) {
R.id.rbThemeFollowSystem -> ThemeManager.THEME_FOLLOW_SYSTEM
@@ -378,11 +430,8 @@ class SecondActivity : AppCompatActivity() {
else -> ThemeManager.THEME_FOLLOW_SYSTEM
}
// Save and apply the new theme
ThemeManager.setThemeMode(this, newMode)
Log.d("SecondActivity", "Theme mode changed to: ${ThemeManager.getThemeModeName(newMode)}")
// Recreate activity to apply theme changes
recreate()
}
}
@@ -418,10 +467,15 @@ class SecondActivity : AppCompatActivity() {
}
val client = OkHttpClient()
val request = Request.Builder()
val requestBuilder = Request.Builder()
.url("$baseUrl/chat/completions")
.addHeader("Content-Type", "application/json")
.addHeader("Authorization", "Bearer $apiKey")
if (apiKey.isNotBlank()) {
requestBuilder.addHeader("Authorization", "Bearer $apiKey")
}
val request = requestBuilder
.post(requestBody.toString().toRequestBody("application/json".toMediaType()))
.build()
@@ -496,7 +550,24 @@ class SecondActivity : AppCompatActivity() {
}
private fun saveConfigurations() {
// Update header configs from UI
if (llmConfigs.isNotEmpty() && selectedLlmIndex < llmConfigs.size) {
val currentConfig = llmConfigs[selectedLlmIndex]
llmConfigs[selectedLlmIndex] = currentConfig.copy(
name = etModelName.text.toString().ifEmpty { "未命名" },
baseUrl = etBaseUrl.text.toString(),
apiKey = etApiKey.text.toString(),
model = etModel.text.toString()
)
} else {
llmConfigs.add(LLMConfig(
name = etModelName.text.toString().ifEmpty { "默认配置" },
baseUrl = etBaseUrl.text.toString(),
apiKey = etApiKey.text.toString(),
model = etModel.text.toString()
))
selectedLlmIndex = 0
}
headerConfigs.clear()
for (i in 0 until llHeadersList.childCount) {
val view = llHeadersList.getChildAt(i)
@@ -507,7 +578,6 @@ class SecondActivity : AppCompatActivity() {
}
}
// Update prompt configs from UI
promptConfigs.clear()
for (i in 0 until llPromptList.childCount) {
val view = llPromptList.getChildAt(i)
@@ -518,27 +588,17 @@ class SecondActivity : AppCompatActivity() {
}
}
// Note: Buttons are removed from this UI as per new design.
// We keep the list empty for data compatibility.
buttonConfigs.clear()
// Save LLM config
val llmConfig = LLMConfig(
baseUrl = etBaseUrl.text.toString(),
apiKey = etApiKey.text.toString(),
model = etModel.text.toString()
)
// Save Note API config
val noteApiConfig = NoteApiConfig(
apiType = spNoteApiType.selectedItem.toString(),
apiUrl = etNoteApiUrl.text.toString(),
apiKey = etNoteApiKey.text.toString()
)
// Save everything
val settingsData = SettingsData(
llmConfig = llmConfig,
llmConfigs = llmConfigs,
selectedLlmIndex = selectedLlmIndex,
headerConfigs = headerConfigs,
promptConfigs = promptConfigs,
buttonConfigs = buttonConfigs,
@@ -549,7 +609,6 @@ class SecondActivity : AppCompatActivity() {
val sharedPrefs = getSharedPreferences("APIConfigs", Context.MODE_PRIVATE)
sharedPrefs.edit().putString("configs", json).apply()
// Also update the legacy APIConfig for backward compatibility
apiConfig = APIConfig(
System.currentTimeMillis(),
"llm-config",
@@ -572,6 +631,7 @@ class SecondActivity : AppCompatActivity() {
// New data classes for settings structure
data class LLMConfig(
val name: String,
val baseUrl: String,
val apiKey: String,
val model: String
@@ -584,10 +644,12 @@ class SecondActivity : AppCompatActivity() {
)
data class SettingsData(
val llmConfig: LLMConfig?,
val llmConfigs: List<LLMConfig>?,
val selectedLlmIndex: Int?,
val headerConfigs: List<HeaderConfig>?,
val promptConfigs: List<PromptConfig>?,
val buttonConfigs: List<ButtonConfig>?,
val noteApiConfig: NoteApiConfig?
val noteApiConfig: NoteApiConfig?,
val llmConfig: LLMConfig? = null
)
}

View File

@@ -32,27 +32,12 @@
android:textStyle="bold"
android:textColor="@color/text_primary"/>
<LinearLayout
<Spinner
android:id="@+id/headerModelSelector"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginTop="2dp">
<View
android:layout_width="6dp"
android:layout_height="6dp"
android:background="@drawable/indicator_dot"/>
<TextView
android:id="@+id/headerModelName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="GPT-4o"
android:textSize="11sp"
android:textColor="@color/primary"
android:layout_marginStart="5dp"/>
</LinearLayout>
android:layout_marginTop="2dp"
android:spinnerMode="dropdown"/>
</LinearLayout>
<Button

View File

@@ -126,6 +126,64 @@
android:textColor="@color/text_hint"
android:textSize="14sp" />
<!-- Base URL -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="配置名称"
android:textColor="@color/text_secondary"
android:textSize="14sp" />
<EditText
android:id="@+id/etModelName"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="8dp"
android:background="@drawable/edittext_border"
android:hint="默认配置"
android:inputType="text"
android:padding="12dp"
android:textColor="@color/text_secondary"
android:textColorHint="@color/text_hint"
android:textSize="16sp" />
</LinearLayout>
<!-- 模型选择器 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="horizontal"
android:gravity="center_vertical">
<Spinner
android:id="@+id/spModelSelector"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:background="@drawable/edittext_border"
android:spinnerMode="dropdown"
android:padding="12dp"/>
<Button
android:id="@+id/btnAddModel"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:layout_marginStart="8dp"
android:text="+ 添加"
android:textSize="14sp"
android:textColor="@color/white"
android:background="@drawable/button_primary_bg"
android:minWidth="0dp"
android:minHeight="0dp"/>
</LinearLayout>
<!-- Base URL -->
<LinearLayout
android:layout_width="match_parent"