feat: add master-agent takeover controls

This commit is contained in:
kris
2026-04-05 08:45:07 +08:00
parent 52f7d08b9e
commit 2a5962f767
10 changed files with 437 additions and 46 deletions

View File

@@ -133,6 +133,28 @@ public class BossApiClient {
return requestWithRestore("POST", "/api/v1/projects/" + encode(projectId) + "/agent-controls", payload);
}
public ApiResponse updateProjectTakeoverSettings(
String projectId,
@Nullable Boolean takeoverEnabled,
@Nullable Boolean globalTakeoverEnabled
) throws IOException, JSONException {
JSONObject payload = new JSONObject();
if (!"master-agent".equals(projectId)) {
if (takeoverEnabled == null) {
payload.put("takeoverEnabled", JSONObject.NULL);
} else {
payload.put("takeoverEnabled", takeoverEnabled);
}
}
if (globalTakeoverEnabled != null || "master-agent".equals(projectId)) {
payload.put(
"globalTakeoverEnabled",
globalTakeoverEnabled == null ? JSONObject.NULL : globalTakeoverEnabled
);
}
return requestWithRestore("POST", "/api/v1/projects/" + encode(projectId) + "/agent-controls", payload);
}
public ApiResponse getProjectOrchestrationBackend(String projectId) throws IOException, JSONException {
return requestWithRestore("GET", "/api/v1/projects/" + encode(projectId) + "/orchestration-backend", null);
}

View File

@@ -7,6 +7,7 @@ import android.widget.LinearLayout;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.SwitchCompat;
import org.json.JSONArray;
import org.json.JSONObject;
@@ -19,6 +20,8 @@ public class ConversationInfoActivity extends BossScreenActivity {
private String projectName;
private String projectFolderName;
private int participantCount;
private boolean takeoverEnabled;
private boolean takeoverInheritedFromGlobal;
@Override
protected int getLayoutResId() {
@@ -82,9 +85,12 @@ public class ConversationInfoActivity extends BossScreenActivity {
}
projectName = project.optString("name", projectName == null ? "会话信息" : projectName);
JSONObject agentControls = detail.optJSONObject("agentControls");
JSONObject threadMeta = project.optJSONObject("threadMeta");
projectFolderName = threadMeta == null ? "" : threadMeta.optString("folderName", "");
participantCount = participants == null ? 0 : participants.length();
takeoverEnabled = agentControls != null && agentControls.optBoolean("effectiveTakeoverEnabled", false);
takeoverInheritedFromGlobal = agentControls != null && agentControls.optBoolean("takeoverInheritedFromGlobal", false);
configureScreen("会话信息", buildSubtitle(threadMeta, participantCount));
appendContent(BossUi.buildSimpleProfileHeader(
@@ -95,6 +101,7 @@ public class ConversationInfoActivity extends BossScreenActivity {
));
appendThreadStatusSummary(threadStatusPayload);
appendTakeoverControl();
appendContent(BossUi.buildWechatMenuRow(
this,
@@ -152,6 +159,21 @@ public class ConversationInfoActivity extends BossScreenActivity {
setRefreshing(false);
}
private void appendTakeoverControl() {
SwitchCompat takeoverSwitch = new SwitchCompat(this);
takeoverSwitch.setText("开启");
takeoverSwitch.setChecked(takeoverEnabled);
takeoverSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> saveTakeoverSetting(isChecked));
appendContent(BossUi.buildFormCell(
this,
"主 Agent 协同接管",
takeoverInheritedFromGlobal
? "当前跟随全局默认开启。主 Agent 会协同推进,但不会抢走你直接控制线程开发的能力。"
: "为这个线程单独开启主 Agent 协同推进。不会抢走你直接控制线程开发的能力。",
takeoverSwitch
));
}
private void appendThreadStatusSummary(@Nullable JSONObject threadStatusPayload) {
if (threadStatusPayload == null) {
return;
@@ -325,6 +347,32 @@ public class ConversationInfoActivity extends BossScreenActivity {
});
}
private void saveTakeoverSetting(boolean enabled) {
setRefreshing(true);
executor.execute(() -> {
try {
BossApiClient.ApiResponse response = apiClient.updateProjectTakeoverSettings(
projectId,
enabled,
null
);
if (!response.ok()) {
throw new IllegalStateException(response.message());
}
runOnUiThread(() -> {
showMessage(enabled ? "已开启主 Agent 协同接管" : "已关闭主 Agent 协同接管");
reload();
});
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
showMessage("保存失败:" + error.getMessage());
reload();
});
}
});
}
private String buildSubtitle(@Nullable JSONObject threadMeta, int count) {
String folder = threadMeta == null ? "" : threadMeta.optString("folderName", "");
String suffix = count <= 0 ? "暂无参与线程" : count + " 个参与线程";

View File

@@ -12,6 +12,7 @@ import android.widget.Spinner;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.SwitchCompat;
import org.json.JSONObject;
@@ -32,12 +33,14 @@ 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
@@ -91,6 +94,7 @@ 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", "");
@@ -159,9 +163,20 @@ public class MasterAgentPromptActivity extends BossScreenActivity {
"默认沿用 Boss 当前主链;需要时可显式切到 Claw Runtime。",
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,
this,
"Claw Runtime 当前不可用",
TextUtils.isEmpty(clawReasonLabel) ? "当前环境未满足 Claw Runtime 的启动条件。" : clawReasonLabel,
TextUtils.equals(backendOverrideText, "claw-runtime")
@@ -235,6 +250,12 @@ 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 "当前没有任何提示词内容。";
}
@@ -251,6 +272,7 @@ 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 {
@@ -262,6 +284,14 @@ 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

@@ -53,10 +53,11 @@ public class ConversationInfoActivityTest {
assertTrue(viewTreeContainsText(content.getChildAt(1), "线程状态摘要"));
assertTrue(viewTreeContainsTextFragment(content.getChildAt(1), "当前进度:已经记录最近 2 条进展"));
assertTrue(viewTreeContainsTextFragment(content.getChildAt(1), "建议下一步:继续同步 Android 只读页"));
assertTrue(viewTreeContainsText(content.getChildAt(2), "发起群聊"));
assertTrue(viewTreeContainsText(content.getChildAt(2), "选择其他线程加入新群"));
assertTrue(viewTreeContainsText(content.getChildAt(3), "线程详情"));
assertTrue(viewTreeContainsText(content.getChildAt(3), "查看当前线程聊天与项目"));
assertTrue(viewTreeContainsText(content.getChildAt(2), "主 Agent 协同接管"));
assertTrue(viewTreeContainsText(content.getChildAt(3), "发起群聊"));
assertTrue(viewTreeContainsText(content.getChildAt(3), "选择其他线程加入新群"));
assertTrue(viewTreeContainsText(content.getChildAt(4), "线程详情"));
assertTrue(viewTreeContainsText(content.getChildAt(4), "查看当前线程聊天与项目"));
assertTrue(viewTreeContainsText(content, "参与线程"));
assertTrue(viewTreeContainsText(content, "硬件审计协作"));
assertFalse(viewTreeContainsText(content, "从当前会话选择其他线程,创建新的独立群聊"));
@@ -173,7 +174,11 @@ public class ConversationInfoActivityTest {
.put("isGroup", false)
.put("deviceIds", new JSONArray().put("mac-studio").put("macbook"))
.put("threadMeta", threadMeta);
return new JSONObject().put("project", project);
return new JSONObject()
.put("project", project)
.put("agentControls", new JSONObject()
.put("effectiveTakeoverEnabled", true)
.put("takeoverInheritedFromGlobal", true));
}
private static JSONObject buildParticipantsPayload() throws Exception {

View File

@@ -54,7 +54,8 @@ public class MasterAgentPromptActivityTest {
.put("userPrompt", new JSONObject().put("content", "用户私有主提示词"))
.put("projectControls", new JSONObject()
.put("promptOverride", "当前对话提示词")
.put("backendOverride", "claw-runtime"));
.put("backendOverride", "claw-runtime")
.put("globalTakeoverEnabled", true));
ReflectionHelpers.callInstanceMethod(
activity,
@@ -68,6 +69,7 @@ public class MasterAgentPromptActivityTest {
assertTrue(viewTreeContainsText(content, "用户私有主提示词"));
assertTrue(viewTreeContainsText(content, "当前对话提示词"));
assertTrue(viewTreeContainsText(content, "执行后端"));
assertTrue(viewTreeContainsText(content, "全局主 Agent 协同接管"));
assertTrue(viewTreeContainsText(content, "合成预览"));
}
@@ -283,6 +285,18 @@ public class MasterAgentPromptActivityTest {
void rememberIdentity(JSONObject json) {
// 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 {