feat: move global takeover into master-agent menu

This commit is contained in:
kris
2026-04-05 08:52:21 +08:00
parent 2a5962f767
commit 7cc33d391b
7 changed files with 228 additions and 51 deletions

View File

@@ -52,6 +52,7 @@
<activity android:name=".AiAccountsActivity" android:exported="false" android:screenOrientation="portrait" />
<activity android:name=".OpenAiOnboardingActivity" android:exported="false" android:screenOrientation="portrait" />
<activity android:name=".MasterAgentPromptActivity" android:exported="false" android:screenOrientation="portrait" />
<activity android:name=".MasterAgentTakeoverActivity" android:exported="false" android:screenOrientation="portrait" />
<activity android:name=".MasterAgentMemoryActivity" android:exported="false" android:screenOrientation="portrait" />
<activity android:name=".OpsCenterActivity" android:exported="false" android:screenOrientation="portrait" />
<activity android:name=".AboutActivity" android:exported="false" android:screenOrientation="portrait" />

View File

@@ -12,7 +12,6 @@ import android.widget.Spinner;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.SwitchCompat;
import org.json.JSONObject;
@@ -33,14 +32,12 @@ public class MasterAgentPromptActivity extends BossScreenActivity {
private @Nullable String userPromptText;
private @Nullable String projectPromptOverrideText;
private @Nullable String backendOverrideText;
private boolean globalTakeoverEnabled;
private boolean clawSelectable;
private @Nullable String clawReasonLabel;
private final List<String> backendOverrideValues = new ArrayList<>();
private EditText userPromptInput;
private EditText projectPromptInput;
private Spinner backendSpinner;
private SwitchCompat globalTakeoverSwitch;
private TextView previewTextView;
@Override
@@ -94,7 +91,6 @@ public class MasterAgentPromptActivity extends BossScreenActivity {
projectControls == null ? "" : projectControls.optString("promptOverride", "")
);
backendOverrideText = projectControls == null ? "" : projectControls.optString("backendOverride", "");
globalTakeoverEnabled = projectControls != null && projectControls.optBoolean("globalTakeoverEnabled", false);
clawSelectable = clawAvailability != null && clawAvailability.optBoolean("selectable", false);
clawReasonLabel = clawAvailability == null ? "" : clawAvailability.optString("reasonLabel", "");
@@ -164,16 +160,6 @@ public class MasterAgentPromptActivity extends BossScreenActivity {
backendSpinner
));
globalTakeoverSwitch = new SwitchCompat(this);
globalTakeoverSwitch.setText("开启");
globalTakeoverSwitch.setChecked(globalTakeoverEnabled);
globalTakeoverSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> refreshPreview());
appendContent(BossUi.buildFormCell(
this,
"全局主 Agent 协同接管",
"为线程会话默认开启主 Agent 协同推进。不会抢走你直接控制线程开发的能力。",
globalTakeoverSwitch
));
if (!clawSelectable) {
appendContent(BossUi.buildSoftPanel(
this,
@@ -250,12 +236,6 @@ public class MasterAgentPromptActivity extends BossScreenActivity {
} else if (TextUtils.equals(backendOverrideText, "claw-runtime") && !clawSelectable) {
builder.append("【执行后端】\n默认Claw Runtime 当前不可用,运行时会自动回退)\n\n");
}
boolean globalTakeover = globalTakeoverSwitch != null
? globalTakeoverSwitch.isChecked()
: globalTakeoverEnabled;
builder.append("【全局主 Agent 协同接管】\n")
.append(globalTakeover ? "已开启" : "已关闭")
.append("(不会抢走你直接控制线程开发)\n\n");
if (builder.length() == 0) {
return "当前没有任何提示词内容。";
}
@@ -272,7 +252,6 @@ public class MasterAgentPromptActivity extends BossScreenActivity {
final String backendOverride = backendSpinner == null
? ""
: backendOverrideValues.get(backendSpinner.getSelectedItemPosition());
final boolean globalTakeover = globalTakeoverSwitch != null && globalTakeoverSwitch.isChecked();
setRefreshing(true);
executor.execute(() -> {
try {
@@ -284,14 +263,6 @@ public class MasterAgentPromptActivity extends BossScreenActivity {
if (!response.ok()) {
throw new IllegalStateException(response.message());
}
BossApiClient.ApiResponse controlsResponse = apiClient.updateProjectTakeoverSettings(
projectId,
null,
globalTakeover
);
if (!controlsResponse.ok()) {
throw new IllegalStateException(controlsResponse.message());
}
runOnUiThread(() -> {
showMessage("提示词已保存");
setResult(RESULT_OK);

View File

@@ -0,0 +1,129 @@
package com.hyzq.boss;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.SwitchCompat;
import org.json.JSONObject;
public class MasterAgentTakeoverActivity extends BossScreenActivity {
public static final String EXTRA_PROJECT_ID = "project_id";
public static final String EXTRA_PROJECT_NAME = "project_name";
private String projectId;
private String projectName;
private boolean contentLoaded;
private boolean globalTakeoverEnabled;
private SwitchCompat globalTakeoverSwitch;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
projectId = getIntent().getStringExtra(EXTRA_PROJECT_ID);
projectName = getIntent().getStringExtra(EXTRA_PROJECT_NAME);
configureScreen("全局接管", projectName == null ? "主 Agent 协同推进" : projectName);
setHeaderAction("保存", v -> saveTakeoverSettings());
updateSaveAvailability();
reload();
}
@Override
protected void reload() {
if (projectId == null || projectId.isEmpty()) {
replaceContent(BossUi.buildEmptyCard(this, "缺少 projectId。"));
setRefreshing(false);
contentLoaded = false;
updateSaveAvailability();
return;
}
setRefreshing(true);
executor.execute(() -> {
try {
BossApiClient.ApiResponse response = apiClient.getProjectAgentControls(projectId);
if (!response.ok()) {
throw new IllegalStateException(response.message());
}
runOnUiThread(() -> renderTakeoverSettings(response.json));
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
contentLoaded = false;
updateSaveAvailability();
replaceContent(BossUi.buildEmptyCard(this, "全局接管加载失败:" + error.getMessage()));
});
}
});
}
private void renderTakeoverSettings(JSONObject payload) {
JSONObject controls = payload.optJSONObject("controls");
globalTakeoverEnabled = controls != null && controls.optBoolean("globalTakeoverEnabled", false);
replaceContent();
appendContent(BossUi.buildSimpleProfileHeader(
this,
projectName == null ? "主 Agent" : projectName,
"全局主 Agent 协同推进",
"为线程会话默认开启协同推进,不会抢走你继续直接控制线程开发的能力。"
));
globalTakeoverSwitch = new SwitchCompat(this);
globalTakeoverSwitch.setText("开启");
globalTakeoverSwitch.setChecked(globalTakeoverEnabled);
appendContent(BossUi.buildFormCell(
this,
"全局主 Agent 协同接管",
"开启后,线程会话默认跟随全局协同推进;线程会话仍可单独覆盖。",
globalTakeoverSwitch
));
appendContent(BossUi.buildSoftPanel(
this,
"说明",
"主 Agent 会理解项目状态、给建议、补调度方案,但不会因为介入就抢走你继续直接控制线程开发的能力。",
"线程级开关优先于这里的全局默认。"
));
contentLoaded = true;
updateSaveAvailability();
setRefreshing(false);
}
private void saveTakeoverSettings() {
if (!contentLoaded) {
showMessage("全局接管尚未加载完成,请先刷新成功后再保存。");
return;
}
final boolean enabled = globalTakeoverSwitch != null && globalTakeoverSwitch.isChecked();
setRefreshing(true);
executor.execute(() -> {
try {
BossApiClient.ApiResponse response = apiClient.updateProjectTakeoverSettings(
projectId,
null,
enabled
);
if (!response.ok()) {
throw new IllegalStateException(response.message());
}
runOnUiThread(() -> {
showMessage(enabled ? "已开启全局主 Agent 协同接管" : "已关闭全局主 Agent 协同接管");
setResult(RESULT_OK);
finish();
});
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
showMessage("保存失败:" + error.getMessage());
});
}
});
}
private void updateSaveAvailability() {
if (headerActionButton != null) {
headerActionButton.setEnabled(contentLoaded);
headerActionButton.setAlpha(contentLoaded ? 1f : 0.45f);
}
}
}

View File

@@ -77,6 +77,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
private ProjectChatUiState.SelectionState selectionState = ProjectChatUiState.emptySelection();
private ActivityResultLauncher<Intent> conversationInfoLauncher;
private ActivityResultLauncher<Intent> masterAgentPromptLauncher;
private ActivityResultLauncher<Intent> masterAgentTakeoverLauncher;
private ActivityResultLauncher<Intent> masterAgentMemoryLauncher;
private ActivityResultLauncher<Intent> forwardTargetLauncher;
private ActivityResultLauncher<String> imagePickerLauncher;
@@ -181,6 +182,14 @@ public class ProjectDetailActivity extends BossScreenActivity {
}
}
);
masterAgentTakeoverLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == RESULT_OK) {
reload(true);
}
}
);
masterAgentMemoryLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
@@ -790,12 +799,23 @@ public class ProjectDetailActivity extends BossScreenActivity {
masterAgentMemoryLauncher.launch(intent);
}
private void openMasterAgentTakeoverSettings() {
if (projectId == null || projectId.isEmpty()) {
showMessage("缺少 projectId");
return;
}
Intent intent = new Intent(this, MasterAgentTakeoverActivity.class);
intent.putExtra(MasterAgentTakeoverActivity.EXTRA_PROJECT_ID, projectId);
intent.putExtra(MasterAgentTakeoverActivity.EXTRA_PROJECT_NAME, initialProjectName);
masterAgentTakeoverLauncher.launch(intent);
}
private void showMasterAgentMoreMenu() {
if (!isMasterAgentConversation()) {
return;
}
new AlertDialog.Builder(this)
.setItems(new CharSequence[]{"模型", "推理强度", "提示词", "记忆", "会话信息", "刷新"}, (dialog, which) -> {
.setItems(new CharSequence[]{"模型", "推理强度", "全局接管", "提示词", "记忆", "会话信息", "刷新"}, (dialog, which) -> {
switch (which) {
case 0:
showMasterAgentModelPicker();
@@ -804,15 +824,18 @@ public class ProjectDetailActivity extends BossScreenActivity {
showMasterAgentReasoningPicker();
break;
case 2:
openMasterAgentPromptProfile();
openMasterAgentTakeoverSettings();
break;
case 3:
openMasterAgentMemories();
openMasterAgentPromptProfile();
break;
case 4:
openConversationInfo();
openMasterAgentMemories();
break;
case 5:
openConversationInfo();
break;
case 6:
reload(true);
break;
default:

View File

@@ -54,8 +54,7 @@ public class MasterAgentPromptActivityTest {
.put("userPrompt", new JSONObject().put("content", "用户私有主提示词"))
.put("projectControls", new JSONObject()
.put("promptOverride", "当前对话提示词")
.put("backendOverride", "claw-runtime")
.put("globalTakeoverEnabled", true));
.put("backendOverride", "claw-runtime"));
ReflectionHelpers.callInstanceMethod(
activity,
@@ -69,7 +68,6 @@ public class MasterAgentPromptActivityTest {
assertTrue(viewTreeContainsText(content, "用户私有主提示词"));
assertTrue(viewTreeContainsText(content, "当前对话提示词"));
assertTrue(viewTreeContainsText(content, "执行后端"));
assertTrue(viewTreeContainsText(content, "全局主 Agent 协同接管"));
assertTrue(viewTreeContainsText(content, "合成预览"));
}
@@ -286,17 +284,6 @@ public class MasterAgentPromptActivityTest {
// JVM 单测不需要落 Android 侧身份缓存。
}
@Override
public ApiResponse updateProjectTakeoverSettings(String projectId, Boolean takeoverEnabled, Boolean globalTakeoverEnabled) {
try {
return new ApiResponse(
200,
new JSONObject().put("ok", true)
);
} catch (Exception error) {
throw new IllegalStateException(error);
}
}
}
private static final class InMemorySharedPreferences implements android.content.SharedPreferences {

View File

@@ -0,0 +1,65 @@
package com.hyzq.boss;
import static org.junit.Assert.assertTrue;
import android.content.Intent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.json.JSONObject;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.util.ReflectionHelpers;
@RunWith(RobolectricTestRunner.class)
@Config(sdk = 34)
public class MasterAgentTakeoverActivityTest {
@Test
public void renderTakeoverSettingsShowsStandaloneGlobalTakeoverSection() throws Exception {
MasterAgentTakeoverActivity activity = Robolectric
.buildActivity(
MasterAgentTakeoverActivity.class,
new Intent()
.putExtra(MasterAgentTakeoverActivity.EXTRA_PROJECT_ID, "master-agent")
.putExtra(MasterAgentTakeoverActivity.EXTRA_PROJECT_NAME, "主 Agent")
)
.setup()
.get();
JSONObject payload = new JSONObject()
.put("controls", new JSONObject().put("globalTakeoverEnabled", true));
ReflectionHelpers.callInstanceMethod(
activity,
"renderTakeoverSettings",
ReflectionHelpers.ClassParameter.from(JSONObject.class, payload)
);
View content = activity.findViewById(R.id.screen_content);
assertTrue(viewTreeContainsText(content, "全局主 Agent 协同接管"));
assertTrue(viewTreeContainsText(content, "线程会话默认跟随全局协同推进"));
}
private static boolean viewTreeContainsText(View root, String expectedText) {
if (root instanceof TextView) {
CharSequence text = ((TextView) root).getText();
if (text != null && text.toString().contains(expectedText)) {
return true;
}
}
if (!(root instanceof ViewGroup)) {
return false;
}
ViewGroup group = (ViewGroup) root;
for (int index = 0; index < group.getChildCount(); index += 1) {
if (viewTreeContainsText(group.getChildAt(index), expectedText)) {
return true;
}
}
return false;
}
}

View File

@@ -44,10 +44,11 @@ public class ProjectDetailActivityMasterAgentMenuTest {
assertMenuItem(listView, 0, "模型");
assertMenuItem(listView, 1, "推理强度");
assertMenuItem(listView, 2, "提示词");
assertMenuItem(listView, 3, "记忆");
assertMenuItem(listView, 4, "会话信息");
assertMenuItem(listView, 5, "刷新");
assertMenuItem(listView, 2, "全局接管");
assertMenuItem(listView, 3, "提示词");
assertMenuItem(listView, 4, "记忆");
assertMenuItem(listView, 5, "会话信息");
assertMenuItem(listView, 6, "刷新");
}
@Test