feat: add preset aliyun qwen model switching
This commit is contained in:
@@ -113,7 +113,11 @@ Android APK:
|
||||
- 当前 `我的 > AI 账号` 已补 `登录 OpenAI 平台账号`、`接入阿里百炼备用账号` 与 `绑定 Master Codex Node` 三条显式入口;OpenAI API 登录成功后会立即设为当前主控,阿里百炼账号会作为备用链路保存
|
||||
- 当前 `登录 OpenAI 平台账号` 已升级成浏览器辅助登录流:会先进入原生引导页,再自动打开 `OpenAI Platform` 登录页;用户登录后可直接跳到 `API Keys` 页面,回 APP 粘贴 key 完成接入
|
||||
- 当前 `AI 账号` 页顶部会显式展示“当前主控身份”,并提供 `校验主控 / 测试主 Agent 对话` 两个动作,切换主控后可直接验证聊天通路
|
||||
- 当前阿里百炼备用链已完成一次真实线上闭环验证:手动切到 `aliyun-qwen-backup` 后,`POST /api/v1/projects/master-agent/messages` 会返回 `queued`,并已实际回流 `阿里备用链正常。` 到 `master-agent` 会话
|
||||
- 当前 `我的 > AI 账号` 已把阿里百炼备用模型切成预设选择:Web 和原生 Android 都支持直接切换 `qwen3.5-plus / qwen3.5-flash`,只有在预设不适用时才需要填自定义模型
|
||||
- 当前 `我的 > 主 Agent 提示词 / 记忆` 页面已补:管理员全局主提示词只读展示、用户主提示词、当前对话附加提示词,以及用户通用记忆 / 跨项目项目记忆的新增、编辑、删除接口;当前对话设置按登录账号隔离,管理员全局主提示词不可覆盖
|
||||
- 当前 Web 端 `master-agent` 会话页右上角也已补齐微信式三点菜单,支持直接进入 `提示词 / 模型 / 推理强度 / 记忆 / 刷新`
|
||||
- 当前 `approval_required` 群聊在 Web 端已统一用单一状态快照驱动:如果有新的待确认推荐,会自动折叠旧的拒绝态;如果上次推荐已拒绝,会明确展示“重新生成新的推荐”的恢复入口
|
||||
- 当前 `OpenAiOnboardingActivity` 在登录成功后会直接给出 `测试主 Agent 对话` 入口,可一键跳到 `master-agent` 聊天页
|
||||
- 当前主控若还是 `Master Codex Node`,但节点离线或执行立即失败,主 Agent 会优先尝试已配置的 `OpenAI API / 阿里百炼 Qwen` 备用账号,避免聊天直接掉成失败日志
|
||||
- 当前原生 Android 的聊天发送已改成更短的客户端等待窗口;`master-agent` 单聊依赖服务端快速入队和消息流里的“思考中 / 超时 / 重试等待”状态,不再要求客户端长时间同步阻塞
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,11 @@ cd /Users/kris/code/boss
|
||||
- 当前 `登录 OpenAI 平台账号` 已升级成浏览器辅助登录流:原生 Android 会先进入 `OpenAiOnboardingActivity`,自动打开 `OpenAI Platform` 登录页;用户登录后可直接跳到 `API Keys` 页面,回 APP 粘贴 key 完成接入
|
||||
- 当前 `OpenAiOnboardingActivity` 在登录成功后会直接弹出 `测试主 Agent 对话`,可一键进入 `master-agent` 聊天页验证主控链路
|
||||
- 当前 `AI 账号` 页顶部会直接展示“当前主控身份”,并提供 `校验主控 / 测试主 Agent 对话` 两个入口,切换主控后不必再手动退回会话页验证
|
||||
- 当前阿里百炼备用链已完成一次真实线上闭环验证:手动切到 `aliyun-qwen-backup` 后,`POST /api/v1/projects/master-agent/messages` 会返回 `queued`,并已实际回流 `阿里备用链正常。` 到 `master-agent` 会话
|
||||
- 当前 `我的 > AI 账号` 已把阿里百炼备用模型切成预设选择:Web 和原生 Android 都支持直接切换 `qwen3.5-plus / qwen3.5-flash`,只有预设不适用时才需要填写自定义模型
|
||||
- 当前 `我的 > 主 Agent 提示词 / 记忆` 页面已接通:管理员全局主提示词只读展示、用户主提示词、当前对话附加提示词,以及用户通用记忆 / 跨项目项目记忆都可以在 Web 端查看和编辑;当前对话设置按登录账号隔离,管理员全局主提示词不可覆盖
|
||||
- 当前 Web 端 `master-agent` 会话页右上角也已补齐微信式三点菜单,支持直接进入 `提示词 / 模型 / 推理强度 / 记忆 / 刷新`
|
||||
- 当前 `approval_required` 群聊在 Web 端已统一用单一状态快照驱动:如果存在新的待确认推荐,会自动折叠旧的拒绝态;如果上次推荐已拒绝,会明确展示“重新生成新的推荐”的恢复入口
|
||||
- 当前如果主控身份还是 `Master Codex Node`,但该节点离线或执行立即失败,主 Agent 会优先尝试已配置的 `OpenAI API / 阿里百炼 Qwen` 备用账号,不再把失败日志直接原样回给用户
|
||||
- 当前原生 Android 的聊天发送已收短客户端等待窗口;`master-agent` 单聊依赖服务端快速入队和消息流里的“主 Agent 思考中 / 回复超时 / 重试等待”状态,不再要求客户端长时间同步阻塞
|
||||
- 当前设备导入主链也已补上第一轮后端闭环:`heartbeat` 可上报真实项目候选,服务端会生成 `deviceImportDraft`;用户可提交勾选结果、生成导入决议,再把选中的线程真正落成聊天窗口
|
||||
|
||||
@@ -10,6 +10,12 @@ import type {
|
||||
AiProvider,
|
||||
MasterIdentitySummary,
|
||||
} from "@/lib/boss-data";
|
||||
import {
|
||||
ALIYUN_QWEN_CUSTOM_MODEL_VALUE,
|
||||
ALIYUN_QWEN_PRESET_MODELS,
|
||||
resolveAliyunQwenModelSelection,
|
||||
resolveAliyunQwenModelValue,
|
||||
} from "@/lib/ai-account-models";
|
||||
import { formatTimestampLabel } from "@/lib/boss-projections";
|
||||
|
||||
type AccountDraft = {
|
||||
@@ -185,6 +191,45 @@ function AccountField({
|
||||
);
|
||||
}
|
||||
|
||||
function AliyunQwenModelField({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}) {
|
||||
const selection = resolveAliyunQwenModelSelection(value);
|
||||
const isCustom = selection === ALIYUN_QWEN_CUSTOM_MODEL_VALUE;
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<label className="space-y-1">
|
||||
<div className="text-[12px] text-[#8C8C8C]">模型</div>
|
||||
<select
|
||||
value={selection}
|
||||
onChange={(event) => onChange(resolveAliyunQwenModelValue(event.target.value, value))}
|
||||
className="w-full rounded-xl border border-[#E5E5EA] bg-[#F7F8FA] px-3 py-2 text-[13px] text-[#111111] outline-none"
|
||||
>
|
||||
{ALIYUN_QWEN_PRESET_MODELS.map((model) => (
|
||||
<option key={model} value={model}>
|
||||
{model}
|
||||
</option>
|
||||
))}
|
||||
<option value={ALIYUN_QWEN_CUSTOM_MODEL_VALUE}>自定义模型</option>
|
||||
</select>
|
||||
</label>
|
||||
{isCustom ? (
|
||||
<AccountField
|
||||
label="自定义模型"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="例如:qwen-max"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AiAccountsClient({
|
||||
accounts,
|
||||
activeIdentity,
|
||||
@@ -617,12 +662,19 @@ export function AiAccountsClient({
|
||||
onChange={(value) => updateDraft(account.accountId, (current) => ({ ...current, nodeId: value }))}
|
||||
placeholder="例如:mac-studio"
|
||||
/>
|
||||
<AccountField
|
||||
label="模型"
|
||||
value={draft.model}
|
||||
onChange={(value) => updateDraft(account.accountId, (current) => ({ ...current, model: value }))}
|
||||
placeholder="例如:gpt-5.4"
|
||||
/>
|
||||
{draft.provider === "aliyun_qwen_api" ? (
|
||||
<AliyunQwenModelField
|
||||
value={draft.model}
|
||||
onChange={(value) => updateDraft(account.accountId, (current) => ({ ...current, model: value }))}
|
||||
/>
|
||||
) : (
|
||||
<AccountField
|
||||
label="模型"
|
||||
value={draft.model}
|
||||
onChange={(value) => updateDraft(account.accountId, (current) => ({ ...current, model: value }))}
|
||||
placeholder="例如:gpt-5.4"
|
||||
/>
|
||||
)}
|
||||
{draft.provider === "openai_api" || draft.provider === "aliyun_qwen_api" ? (
|
||||
<div className="col-span-2">
|
||||
<AccountField
|
||||
@@ -856,13 +908,11 @@ export function AiAccountsClient({
|
||||
}
|
||||
placeholder="例如:dashscope 账号备注"
|
||||
/>
|
||||
<AccountField
|
||||
label="模型"
|
||||
<AliyunQwenModelField
|
||||
value={aliyunQwenOnboardDraft.model}
|
||||
onChange={(value) =>
|
||||
setAliyunQwenOnboardDraft((current) => ({ ...current, model: value }))
|
||||
}
|
||||
placeholder="例如:qwen3.5-plus"
|
||||
/>
|
||||
<AccountField
|
||||
label="API Key"
|
||||
|
||||
19
src/lib/ai-account-models.ts
Normal file
19
src/lib/ai-account-models.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export const ALIYUN_QWEN_PRESET_MODELS = ["qwen3.5-plus", "qwen3.5-flash"] as const;
|
||||
|
||||
export const ALIYUN_QWEN_CUSTOM_MODEL_VALUE = "__custom__";
|
||||
|
||||
export function isAliyunQwenPresetModel(model: string | null | undefined) {
|
||||
const trimmed = model?.trim() ?? "";
|
||||
return ALIYUN_QWEN_PRESET_MODELS.includes(trimmed as (typeof ALIYUN_QWEN_PRESET_MODELS)[number]);
|
||||
}
|
||||
|
||||
export function resolveAliyunQwenModelSelection(model: string | null | undefined) {
|
||||
return isAliyunQwenPresetModel(model) ? (model?.trim() ?? "") : ALIYUN_QWEN_CUSTOM_MODEL_VALUE;
|
||||
}
|
||||
|
||||
export function resolveAliyunQwenModelValue(selection: string, customModel: string) {
|
||||
if (selection === ALIYUN_QWEN_CUSTOM_MODEL_VALUE) {
|
||||
return customModel.trim();
|
||||
}
|
||||
return selection.trim();
|
||||
}
|
||||
24
tests/ai-account-models.test.ts
Normal file
24
tests/ai-account-models.test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
ALIYUN_QWEN_CUSTOM_MODEL_VALUE,
|
||||
isAliyunQwenPresetModel,
|
||||
resolveAliyunQwenModelSelection,
|
||||
resolveAliyunQwenModelValue,
|
||||
} from "../src/lib/ai-account-models";
|
||||
|
||||
test("阿里百炼预设模型会被识别为预设", () => {
|
||||
assert.equal(isAliyunQwenPresetModel("qwen3.5-plus"), true);
|
||||
assert.equal(isAliyunQwenPresetModel("qwen3.5-flash"), true);
|
||||
assert.equal(isAliyunQwenPresetModel(" qwen3.5-plus "), true);
|
||||
});
|
||||
|
||||
test("阿里百炼非预设模型会落到自定义选项", () => {
|
||||
assert.equal(isAliyunQwenPresetModel("qwen-max"), false);
|
||||
assert.equal(resolveAliyunQwenModelSelection("qwen-max"), ALIYUN_QWEN_CUSTOM_MODEL_VALUE);
|
||||
});
|
||||
|
||||
test("阿里百炼模型值会根据当前选择输出预设或自定义值", () => {
|
||||
assert.equal(resolveAliyunQwenModelValue("qwen3.5-plus", "ignored"), "qwen3.5-plus");
|
||||
assert.equal(resolveAliyunQwenModelValue(ALIYUN_QWEN_CUSTOM_MODEL_VALUE, " qwen-max "), "qwen-max");
|
||||
});
|
||||
Reference in New Issue
Block a user