feat: add master-agent takeover controls
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 + " 个参与线程";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user