feat: add preset aliyun qwen model switching

This commit is contained in:
kris
2026-04-01 08:04:31 +08:00
parent e52932e8ef
commit a4655439dd
7 changed files with 332 additions and 16 deletions

View File

@@ -1,8 +1,10 @@
package com.hyzq.boss;
import android.os.Bundle;
import android.view.View;
import android.text.InputType;
import android.content.Intent;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
@@ -21,6 +23,9 @@ public class AiAccountsActivity extends BossScreenActivity {
private static final String[] ROLE_LABELS = {"主 GPT", "备用 GPT", "API 容灾"};
private static final String[] PROVIDER_VALUES = {"master_codex_node", "openai_api", "aliyun_qwen_api"};
private static final String[] PROVIDER_LABELS = {"Master Codex Node", "OpenAI API", "阿里百炼 Qwen"};
private static final String[] ALIYUN_QWEN_MODEL_VALUES = {"qwen3.5-plus", "qwen3.5-flash", "__custom__"};
private static final String[] ALIYUN_QWEN_MODEL_LABELS = {"qwen3.5-plus", "qwen3.5-flash", "自定义模型"};
private static final String ALIYUN_QWEN_PROVIDER = "aliyun_qwen_api";
private boolean refreshOnResume;
@Override
@@ -345,17 +350,16 @@ public class AiAccountsActivity extends BossScreenActivity {
final EditText displayNameInput = BossUi.buildInput(this, "显示名称", false);
displayNameInput.setText("阿里百炼备用账号");
final EditText accountIdentifierInput = BossUi.buildInput(this, "账号标识 / 备注", false);
final EditText modelInput = BossUi.buildInput(this, "模型,例如 qwen3.5-plus", false);
modelInput.setText("qwen3.5-plus");
final EditText apiKeyInput = BossUi.buildInput(this, "阿里百炼 API Key", false);
apiKeyInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
final AliyunModelSelection modelSelection = buildAliyunQwenModelSelection("qwen3.5-plus");
LinearLayout form = new LinearLayout(this);
form.setOrientation(LinearLayout.VERTICAL);
form.addView(BossUi.buildFormCell(this, "标签", "建议使用 备用 GPT", labelInput));
form.addView(BossUi.buildFormCell(this, "显示名称", "会展示在账号列表中", displayNameInput));
form.addView(BossUi.buildFormCell(this, "账号标识", "可填账号名或自定义备注", accountIdentifierInput));
form.addView(BossUi.buildFormCell(this, "模型", "例如 qwen3.5-plus", modelInput));
form.addView(BossUi.buildFormCell(this, "模型", "预设 qwen3.5-plus / qwen3.5-flash不适用时切换自定义模型。", modelSelection.container));
form.addView(BossUi.buildFormCell(this, "API Key", "填写后会保存为备用链路,不会抢占当前主控", apiKeyInput));
new AlertDialog.Builder(this)
@@ -367,7 +371,7 @@ public class AiAccountsActivity extends BossScreenActivity {
labelInput.getText().toString().trim(),
displayNameInput.getText().toString().trim(),
accountIdentifierInput.getText().toString().trim(),
modelInput.getText().toString().trim(),
modelSelection.resolveModel(),
apiKeyInput.getText().toString().trim()
))
.show();
@@ -482,6 +486,10 @@ public class AiAccountsActivity extends BossScreenActivity {
showMessage("标签、显示名称和 API Key 不能为空");
return;
}
if (model.isEmpty()) {
showMessage("模型不能为空");
return;
}
setRefreshing(true);
executor.execute(() -> {
try {
@@ -532,12 +540,17 @@ public class AiAccountsActivity extends BossScreenActivity {
final android.widget.EditText nodeIdInput = BossUi.buildInput(this, "节点 ID", false);
final android.widget.EditText nodeLabelInput = BossUi.buildInput(this, "节点名称", false);
final android.widget.EditText modelInput = BossUi.buildInput(this, "模型,例如 gpt-5.4", false);
final AliyunModelSelection aliyunModelSelection = buildAliyunQwenModelSelection(
existing == null ? "qwen3.5-plus" : existing.optString("model", "")
);
final android.widget.EditText apiKeyInput = BossUi.buildInput(this, "API Key", false);
final android.widget.EditText loginStatusInput = BossUi.buildInput(this, "登录状态备注", true);
final Spinner roleSpinner = new Spinner(this);
roleSpinner.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, ROLE_LABELS));
final Spinner providerSpinner = new Spinner(this);
providerSpinner.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, PROVIDER_LABELS));
final LinearLayout modelFieldContainer = new LinearLayout(this);
modelFieldContainer.setOrientation(LinearLayout.VERTICAL);
final SwitchCompat enabledSwitch = new SwitchCompat(this);
enabledSwitch.setText("启用");
enabledSwitch.setChecked(existing == null || existing.optBoolean("enabled", true));
@@ -560,6 +573,27 @@ public class AiAccountsActivity extends BossScreenActivity {
apiKeyInput.setText(apiKeyHint);
}
final Runnable refreshModelField = () -> {
modelFieldContainer.removeAllViews();
if (ALIYUN_QWEN_PROVIDER.equals(PROVIDER_VALUES[providerSpinner.getSelectedItemPosition()])) {
modelFieldContainer.addView(aliyunModelSelection.container);
} else {
modelFieldContainer.addView(modelInput);
}
};
providerSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
refreshModelField.run();
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
refreshModelField.run();
}
});
refreshModelField.run();
LinearLayout form = new LinearLayout(this);
form.setOrientation(LinearLayout.VERTICAL);
form.addView(BossUi.buildFormCell(this, "标签", "例如 主 GPT", labelInput));
@@ -567,7 +601,7 @@ public class AiAccountsActivity extends BossScreenActivity {
form.addView(BossUi.buildFormCell(this, "账号标识", "邮箱、登录名或备注信息", accountIdentifierInput));
form.addView(BossUi.buildFormCell(this, "节点 ID", "Master Codex Node 的唯一标识", nodeIdInput));
form.addView(BossUi.buildFormCell(this, "节点名称", "用于快速识别节点", nodeLabelInput));
form.addView(BossUi.buildFormCell(this, "模型", "例如 gpt-5.4", modelInput));
form.addView(BossUi.buildFormCell(this, "模型", "OpenAI / Master Node 使用通用模型名,阿里百炼会自动切换到预设选择。", modelFieldContainer));
form.addView(BossUi.buildFormCell(this, "API Key", "OpenAI API / 阿里百炼 Qwen 模式需要", apiKeyInput));
form.addView(BossUi.buildFormCell(this, "登录状态备注", "可记录 Plus、有无风控等状态", loginStatusInput));
form.addView(BossUi.buildFormCell(this, "账号角色", null, roleSpinner));
@@ -586,7 +620,9 @@ public class AiAccountsActivity extends BossScreenActivity {
accountIdentifierInput.getText().toString().trim(),
nodeIdInput.getText().toString().trim(),
nodeLabelInput.getText().toString().trim(),
modelInput.getText().toString().trim(),
ALIYUN_QWEN_PROVIDER.equals(PROVIDER_VALUES[providerSpinner.getSelectedItemPosition()])
? aliyunModelSelection.resolveModel()
: modelInput.getText().toString().trim(),
apiKeyInput.getText().toString().trim(),
loginStatusInput.getText().toString().trim(),
enabledSwitch.isChecked(),
@@ -613,7 +649,11 @@ public class AiAccountsActivity extends BossScreenActivity {
String provider
) {
if (label.isEmpty() || displayName.isEmpty()) {
showMessage("标签和显示名称不能为空");
showMessage("标签和显示名称不能为空");
return;
}
if (model.isEmpty()) {
showMessage("模型不能为空");
return;
}
setRefreshing(true);
@@ -659,6 +699,15 @@ public class AiAccountsActivity extends BossScreenActivity {
return 0;
}
private int indexOfOrMinusOne(String[] values, String target) {
for (int i = 0; i < values.length; i++) {
if (values[i].equals(target)) {
return i;
}
}
return -1;
}
private void activateAccount(JSONObject account) {
setRefreshing(true);
executor.execute(() -> {
@@ -739,4 +788,59 @@ public class AiAccountsActivity extends BossScreenActivity {
}
});
}
private AliyunModelSelection buildAliyunQwenModelSelection(String initialModel) {
LinearLayout container = new LinearLayout(this);
container.setOrientation(LinearLayout.VERTICAL);
final Spinner presetSpinner = new Spinner(this);
presetSpinner.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, ALIYUN_QWEN_MODEL_LABELS));
final EditText customModelInput = BossUi.buildInput(this, "自定义模型名", false);
int presetIndex = indexOfOrMinusOne(ALIYUN_QWEN_MODEL_VALUES, initialModel);
if (presetIndex >= 0 && presetIndex < ALIYUN_QWEN_MODEL_VALUES.length - 1) {
presetSpinner.setSelection(presetIndex);
customModelInput.setVisibility(View.GONE);
} else {
presetSpinner.setSelection(ALIYUN_QWEN_MODEL_VALUES.length - 1);
customModelInput.setText(initialModel == null ? "" : initialModel);
customModelInput.setVisibility(View.VISIBLE);
}
presetSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
customModelInput.setVisibility(position == ALIYUN_QWEN_MODEL_VALUES.length - 1 ? View.VISIBLE : View.GONE);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
customModelInput.setVisibility(View.GONE);
}
});
container.addView(BossUi.buildFormCell(this, "预设模型", "建议选择 qwen3.5-plus 或 qwen3.5-flash。", presetSpinner));
container.addView(BossUi.buildFormCell(this, "自定义模型", "预设不适用时填写完整模型名。", customModelInput));
return new AliyunModelSelection(container, presetSpinner, customModelInput);
}
private static final class AliyunModelSelection {
private final LinearLayout container;
private final Spinner presetSpinner;
private final EditText customModelInput;
AliyunModelSelection(LinearLayout container, Spinner presetSpinner, EditText customModelInput) {
this.container = container;
this.presetSpinner = presetSpinner;
this.customModelInput = customModelInput;
}
String resolveModel() {
int selectedIndex = presetSpinner.getSelectedItemPosition();
if (selectedIndex >= 0 && selectedIndex < ALIYUN_QWEN_MODEL_VALUES.length - 1) {
return ALIYUN_QWEN_MODEL_VALUES[selectedIndex];
}
return customModelInput.getText().toString().trim();
}
}
}

View File

@@ -8,8 +8,13 @@ import android.content.SharedPreferences;
import android.os.Looper;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.SpinnerAdapter;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import org.json.JSONObject;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -18,6 +23,7 @@ import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.Shadows;
import org.robolectric.shadows.ShadowActivity;
import org.robolectric.shadows.ShadowDialog;
import org.robolectric.shadows.ShadowToast;
import org.robolectric.util.ReflectionHelpers;
@@ -133,6 +139,65 @@ public class AiAccountsActivityTest {
assertEquals("master-agent", nextIntent.getStringExtra(ProjectDetailActivity.EXTRA_PROJECT_ID));
}
@Test
public void openAliyunQwenOnboardingDialogUsesPresetModelsWithCustomFallback() throws Exception {
TestAiAccountsActivity activity = Robolectric.buildActivity(TestAiAccountsActivity.class).setup().get();
ReflectionHelpers.callInstanceMethod(activity, "openAliyunQwenOnboardingDialog");
Shadows.shadowOf(Looper.getMainLooper()).idle();
AlertDialog dialog = (AlertDialog) ShadowDialog.getLatestDialog();
assertNotNull(dialog);
View root = dialog.getWindow().getDecorView();
Spinner modelSpinner = findSpinnerContainingItem(root, "qwen3.5-plus");
assertNotNull(modelSpinner);
SpinnerAdapter adapter = modelSpinner.getAdapter();
assertNotNull(adapter);
assertEquals(3, adapter.getCount());
assertEquals("qwen3.5-plus", adapter.getItem(0).toString());
assertEquals("qwen3.5-flash", adapter.getItem(1).toString());
assertEquals("自定义模型", adapter.getItem(2).toString());
assertEquals("qwen3.5-plus", modelSpinner.getSelectedItem().toString());
EditText customModelInput = findEditTextWithHint(root, "自定义模型");
assertNotNull(customModelInput);
}
@Test
public void openAccountEditorShowsCustomFallbackForNonPresetAliyunModel() throws Exception {
TestAiAccountsActivity activity = Robolectric.buildActivity(TestAiAccountsActivity.class).setup().get();
JSONObject existing = new JSONObject()
.put("accountId", "acc-1")
.put("label", "备用 GPT")
.put("displayName", "阿里百炼备用账号")
.put("provider", "aliyun_qwen_api")
.put("model", "qwen-custom-x");
ReflectionHelpers.callInstanceMethod(
activity,
"openAccountEditor",
ReflectionHelpers.ClassParameter.from(JSONObject.class, existing),
ReflectionHelpers.ClassParameter.from(String.class, null)
);
Shadows.shadowOf(Looper.getMainLooper()).idle();
AlertDialog dialog = (AlertDialog) ShadowDialog.getLatestDialog();
assertNotNull(dialog);
View root = dialog.getWindow().getDecorView();
Spinner modelSpinner = findSpinnerContainingItem(root, "自定义模型");
assertNotNull(modelSpinner);
SpinnerAdapter adapter = modelSpinner.getAdapter();
assertNotNull(adapter);
assertEquals(3, adapter.getCount());
assertEquals("自定义模型", modelSpinner.getSelectedItem().toString());
EditText customModelInput = findEditTextWithHint(root, "自定义模型");
assertNotNull(customModelInput);
assertEquals("qwen-custom-x", customModelInput.getText().toString());
}
private static final class TestAiAccountsActivity extends AiAccountsActivity {
private int reloadCount = 0;
@@ -418,4 +483,50 @@ public class AiAccountsActivityTest {
}
return false;
}
private static Spinner findSpinnerContainingItem(View root, String expectedText) {
if (root instanceof Spinner) {
Spinner spinner = (Spinner) root;
SpinnerAdapter adapter = spinner.getAdapter();
if (adapter != null) {
for (int index = 0; index < adapter.getCount(); index += 1) {
Object item = adapter.getItem(index);
if (item != null && item.toString().contains(expectedText)) {
return spinner;
}
}
}
}
if (!(root instanceof ViewGroup)) {
return null;
}
ViewGroup group = (ViewGroup) root;
for (int index = 0; index < group.getChildCount(); index += 1) {
Spinner match = findSpinnerContainingItem(group.getChildAt(index), expectedText);
if (match != null) {
return match;
}
}
return null;
}
private static EditText findEditTextWithHint(View root, String expectedText) {
if (root instanceof EditText) {
CharSequence hint = ((EditText) root).getHint();
if (hint != null && hint.toString().contains(expectedText)) {
return (EditText) root;
}
}
if (!(root instanceof ViewGroup)) {
return null;
}
ViewGroup group = (ViewGroup) root;
for (int index = 0; index < group.getChildCount(); index += 1) {
EditText match = findEditTextWithHint(group.getChildAt(index), expectedText);
if (match != null) {
return match;
}
}
return null;
}
}