feat: add omx orchestration backend selection

This commit is contained in:
kris
2026-04-03 03:17:12 +08:00
parent 60f5e2d7d6
commit ec45bed59f
18 changed files with 1993 additions and 20 deletions

View File

@@ -126,6 +126,16 @@ public class BossApiClient {
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);
}
public ApiResponse updateProjectOrchestrationBackend(String projectId, @Nullable String requestedBackendId) throws IOException, JSONException {
JSONObject payload = new JSONObject();
payload.put("requestedBackendId", requestedBackendId == null ? JSONObject.NULL : requestedBackendId);
return requestWithRestore("PATCH", "/api/v1/projects/" + encode(projectId) + "/orchestration-backend", payload);
}
public ApiResponse getMasterAgentPromptProfile(String projectId) throws IOException, JSONException {
return requestWithRestore("GET", "/api/v1/projects/" + encode(projectId) + "/prompt-profile", null);
}

View File

@@ -48,7 +48,11 @@ public class GroupInfoActivity extends BossScreenActivity {
if (!detailResponse.ok()) throw new IllegalStateException(detailResponse.message());
BossApiClient.ApiResponse participantsResponse = apiClient.getConversationParticipants(projectId);
if (!participantsResponse.ok()) throw new IllegalStateException(participantsResponse.message());
runOnUiThread(() -> renderGroup(detailResponse.json, participantsResponse.json));
BossApiClient.ApiResponse orchestrationResponse = apiClient.getProjectOrchestrationBackend(projectId);
JSONObject orchestrationBackend = orchestrationResponse.ok()
? orchestrationResponse.json
: buildFallbackOrchestrationBackendPayload(orchestrationResponse.message());
runOnUiThread(() -> renderGroup(detailResponse.json, participantsResponse.json, orchestrationBackend));
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
@@ -59,6 +63,10 @@ public class GroupInfoActivity extends BossScreenActivity {
}
private void renderGroup(JSONObject detail, JSONObject participantsPayload) {
renderGroup(detail, participantsPayload, null);
}
private void renderGroup(JSONObject detail, JSONObject participantsPayload, @Nullable JSONObject orchestrationBackendPayload) {
replaceContent();
JSONObject project = detail.optJSONObject("project");
JSONArray participants = participantsPayload.optJSONArray("participants");
@@ -94,6 +102,9 @@ public class GroupInfoActivity extends BossScreenActivity {
null,
v -> openProject(projectId, projectName)
));
if (orchestrationBackendPayload != null) {
appendContent(buildOrchestrationBackendRow(orchestrationBackendPayload));
}
if (repairRequired) {
String meta = invalidParticipantCount > 0
@@ -316,6 +327,171 @@ public class GroupInfoActivity extends BossScreenActivity {
});
}
private LinearLayout buildOrchestrationBackendRow(JSONObject backendPayload) {
String requestedBackendId = backendPayload.optString("requestedBackendId", "boss-native-orchestrator");
String currentBackendId = backendPayload.optString("currentBackendId", requestedBackendId);
JSONObject omxAvailability = backendPayload.optJSONObject("omxAvailability");
String currentLabel = resolveBackendLabel(backendPayload, currentBackendId);
String requestedLabel = resolveBackendLabel(backendPayload, requestedBackendId);
String subtitle = "当前:" + currentLabel;
if (!TextUtils.equals(currentBackendId, requestedBackendId)) {
subtitle += " · 请求:" + requestedLabel;
}
boolean omxSelectable = omxAvailability != null && omxAvailability.optBoolean("selectable", false);
boolean fallbackActive = !TextUtils.equals(currentBackendId, requestedBackendId);
if (omxAvailability != null) {
subtitle += omxSelectable ? " · OMX 可用" : " · OMX 受限";
}
String meta = omxAvailability == null
? "等待后端状态"
: buildOrchestrationBackendAvailabilitySummary(omxAvailability, fallbackActive);
String badge = fallbackActive ? "回退" : (omxSelectable ? "当前" : "受限");
return BossUi.buildWechatMenuRow(
this,
"编排后端",
subtitle,
meta,
badge,
v -> openOrchestrationBackendDialog(backendPayload)
);
}
private void openOrchestrationBackendDialog(JSONObject backendPayload) {
JSONArray availableChoices = backendPayload.optJSONArray("availableChoices");
if (availableChoices == null || availableChoices.length() == 0) {
showMessage("编排后端状态暂不可用");
return;
}
CharSequence[] items = new CharSequence[availableChoices.length()];
final String[] backendIds = new String[availableChoices.length()];
final boolean[] selectable = new boolean[availableChoices.length()];
final String omxReason = backendPayload.optJSONObject("omxAvailability") == null
? "OMX Team Runtime 当前不可用。"
: backendPayload.optJSONObject("omxAvailability").optString("reasonLabel", "OMX Team Runtime 当前不可用。");
final boolean omxSelectable = backendPayload.optJSONObject("omxAvailability") != null
&& backendPayload.optJSONObject("omxAvailability").optBoolean("selectable", false);
for (int i = 0; i < availableChoices.length(); i++) {
JSONObject choice = availableChoices.optJSONObject(i);
if (choice == null) {
items[i] = "未命名后端";
backendIds[i] = "";
selectable[i] = false;
continue;
}
backendIds[i] = choice.optString("backendId", "");
selectable[i] = choice.optBoolean("selectable", false);
String label = resolveBackendLabel(backendPayload, backendIds[i]);
items[i] = label + (selectable[i] ? "" : "(不可用)");
}
new AlertDialog.Builder(this)
.setTitle("选择编排后端")
.setMessage(omxSelectable
? "Boss Native Orchestrator 永远可用OMX Team Runtime 当前可直接切换。"
: "Boss Native Orchestrator 永远可用OMX Team Runtime 当前不可用,切换时会自动回退到 Boss Native Orchestrator。")
.setItems(items, (dialog, which) -> {
String selectedBackendId = backendIds[which];
if (TextUtils.isEmpty(selectedBackendId)) {
showMessage("编排后端选择无效");
return;
}
if (!selectable[which] && TextUtils.equals(selectedBackendId, "omx-team")) {
showMessage(omxReason);
return;
}
saveOrchestrationBackend(selectedBackendId);
})
.setNegativeButton("取消", null)
.show();
}
private void saveOrchestrationBackend(String requestedBackendId) {
setRefreshing(true);
executor.execute(() -> {
try {
BossApiClient.ApiResponse response = apiClient.updateProjectOrchestrationBackend(projectId, requestedBackendId);
if (!response.ok()) throw new IllegalStateException(response.message());
runOnUiThread(() -> {
showMessage("编排后端已更新为 " + resolveBackendLabelForId(requestedBackendId));
reload();
});
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
showMessage("保存失败:" + error.getMessage());
});
}
});
}
private String resolveBackendLabel(JSONObject backendPayload, String backendId) {
JSONArray availableChoices = backendPayload.optJSONArray("availableChoices");
if (availableChoices != null) {
for (int i = 0; i < availableChoices.length(); i++) {
JSONObject choice = availableChoices.optJSONObject(i);
if (choice == null) continue;
if (TextUtils.equals(choice.optString("backendId", ""), backendId)) {
return choice.optString("label", resolveBackendLabelForId(backendId));
}
}
}
return resolveBackendLabelForId(backendId);
}
private String resolveBackendLabelForId(String backendId) {
if (TextUtils.equals(backendId, "omx-team")) {
return "OMX Team Runtime";
}
return "Boss Native Orchestrator";
}
private String normalizeOrchestrationReasonLabel(String value) {
String trimmed = value == null ? "" : value.trim();
if (trimmed.endsWith("") || trimmed.endsWith(".")) {
return trimmed.substring(0, trimmed.length() - 1);
}
return trimmed;
}
private String buildOrchestrationBackendAvailabilitySummary(JSONObject omxAvailability, boolean fallbackActive) {
if (omxAvailability.optBoolean("selectable", false)) {
return "OMX Team Runtime 当前可用,当前可切换到该后端。";
}
String reasonLabel = normalizeOrchestrationReasonLabel(
omxAvailability.optString("reasonLabel", "OMX Team Runtime 当前不可用。")
);
return fallbackActive
? reasonLabel + ",当前已自动回退到 Boss Native Orchestrator。"
: reasonLabel + ",切换后会自动回退到 Boss Native Orchestrator。";
}
private JSONObject buildFallbackOrchestrationBackendPayload(String reason) {
try {
JSONArray availableChoices = new JSONArray()
.put(new JSONObject()
.put("backendId", "boss-native-orchestrator")
.put("label", "Boss Native Orchestrator")
.put("selectable", true)
.put("current", true))
.put(new JSONObject()
.put("backendId", "omx-team")
.put("label", "OMX Team Runtime")
.put("selectable", false)
.put("current", false));
return new JSONObject()
.put("currentBackendId", "boss-native-orchestrator")
.put("requestedBackendId", "boss-native-orchestrator")
.put("availableChoices", availableChoices)
.put("omxAvailability", new JSONObject()
.put("selectable", false)
.put("reason", "disabled")
.put("reasonLabel", TextUtils.isEmpty(reason) ? "OMX Team Runtime 当前不可用。" : reason));
} catch (Exception error) {
return new JSONObject();
}
}
private String buildSubtitle(String folderName, int count) {
String memberLabel = count <= 0 ? "暂无成员" : count + " 个成员";
if (folderName.isEmpty()) {