feat: restore dispatch confirmation flows
This commit is contained in:
@@ -90,6 +90,23 @@ public class BossApiClient {
|
||||
return requestWithRestore("GET", "/api/v1/projects/" + encode(projectId), null);
|
||||
}
|
||||
|
||||
public ApiResponse getDispatchPlans(String projectId) throws IOException, JSONException {
|
||||
return requestWithRestore("GET", "/api/v1/projects/" + encode(projectId) + "/dispatch-plans", null);
|
||||
}
|
||||
|
||||
public ApiResponse confirmDispatchPlan(String projectId, String planId, JSONArray approvedTargetProjectIds) throws IOException, JSONException {
|
||||
JSONObject payload = new JSONObject();
|
||||
payload.put(
|
||||
"approvedTargetProjectIds",
|
||||
approvedTargetProjectIds == null ? new JSONArray() : approvedTargetProjectIds
|
||||
);
|
||||
return requestWithRestore(
|
||||
"POST",
|
||||
"/api/v1/projects/" + encode(projectId) + "/dispatch-plans/" + encode(planId) + "/confirm",
|
||||
payload
|
||||
);
|
||||
}
|
||||
|
||||
public ApiResponse renameConversation(String projectId, String name, boolean group) throws IOException, JSONException {
|
||||
JSONObject payload = new JSONObject();
|
||||
payload.put("name", name);
|
||||
|
||||
@@ -2,6 +2,9 @@ package com.hyzq.boss;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
@@ -215,6 +218,73 @@ public final class ProjectChatUiState {
|
||||
return "文件";
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static JSONObject latestPendingDispatchPlan(@Nullable JSONArray plans) {
|
||||
if (plans == null || plans.length() == 0) {
|
||||
return null;
|
||||
}
|
||||
for (int i = 0; i < plans.length(); i++) {
|
||||
JSONObject plan = plans.optJSONObject(i);
|
||||
if (plan == null) {
|
||||
continue;
|
||||
}
|
||||
if ("pending_user_confirmation".equals(plan.optString("status", ""))) {
|
||||
return plan;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static List<String> dispatchPlanApprovedTargetIds(@Nullable JSONObject plan) {
|
||||
ArrayList<String> approved = new ArrayList<>();
|
||||
if (plan == null) {
|
||||
return approved;
|
||||
}
|
||||
JSONArray targets = plan.optJSONArray("targets");
|
||||
if (targets == null) {
|
||||
return approved;
|
||||
}
|
||||
for (int i = 0; i < targets.length(); i++) {
|
||||
JSONObject target = targets.optJSONObject(i);
|
||||
if (target == null) {
|
||||
continue;
|
||||
}
|
||||
String projectId = target.optString("projectId", "").trim();
|
||||
if (!projectId.isEmpty()) {
|
||||
approved.add(projectId);
|
||||
}
|
||||
}
|
||||
return approved;
|
||||
}
|
||||
|
||||
public static String summarizeDispatchPlan(@Nullable JSONObject plan) {
|
||||
if (plan == null) {
|
||||
return "主 Agent 暂未生成推荐线程。";
|
||||
}
|
||||
String summary = plan.optString("summary", "").trim();
|
||||
List<String> targetTitles = new ArrayList<>();
|
||||
JSONArray targets = plan.optJSONArray("targets");
|
||||
if (targets != null) {
|
||||
for (int i = 0; i < targets.length(); i++) {
|
||||
JSONObject target = targets.optJSONObject(i);
|
||||
if (target == null) {
|
||||
continue;
|
||||
}
|
||||
String title = target.optString("threadDisplayName", "").trim();
|
||||
if (!title.isEmpty()) {
|
||||
targetTitles.add(title);
|
||||
}
|
||||
}
|
||||
}
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append(isBlank(summary) ? "主 Agent 已生成推荐线程。" : summary);
|
||||
if (!targetTitles.isEmpty()) {
|
||||
builder.append("\n推荐线程:");
|
||||
builder.append(String.join("、", targetTitles));
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public static String formatAttachmentSize(long fileSizeBytes) {
|
||||
if (fileSizeBytes >= 1024L * 1024L) {
|
||||
return String.format(java.util.Locale.US, "%.1f MB", fileSizeBytes / (1024f * 1024f));
|
||||
|
||||
@@ -59,6 +59,9 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
private boolean conversationInfoReady;
|
||||
private String currentScreenTitle;
|
||||
private String currentScreenSubtitle;
|
||||
private String projectCollaborationMode = "development";
|
||||
private String projectApprovalState = "not_required";
|
||||
private @Nullable JSONObject currentPendingDispatchPlan;
|
||||
private ProjectChatUiState.SelectionState selectionState = ProjectChatUiState.emptySelection();
|
||||
private ActivityResultLauncher<Intent> conversationInfoLauncher;
|
||||
private ActivityResultLauncher<Intent> forwardTargetLauncher;
|
||||
@@ -216,7 +219,20 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
if (!response.ok()) {
|
||||
throw new IllegalStateException(response.message());
|
||||
}
|
||||
runOnUiThread(() -> renderProject(response.json));
|
||||
JSONObject project = response.json.optJSONObject("project");
|
||||
JSONArray dispatchPlans = null;
|
||||
if (project != null && project.optBoolean("isGroup", false)) {
|
||||
try {
|
||||
BossApiClient.ApiResponse dispatchPlansResponse = apiClient.getDispatchPlans(projectId);
|
||||
if (dispatchPlansResponse.ok()) {
|
||||
dispatchPlans = dispatchPlansResponse.json.optJSONArray("plans");
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
dispatchPlans = null;
|
||||
}
|
||||
}
|
||||
JSONArray finalDispatchPlans = dispatchPlans;
|
||||
runOnUiThread(() -> renderProject(response.json, finalDispatchPlans));
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
setRefreshing(false);
|
||||
@@ -245,7 +261,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
updateSelectionUi();
|
||||
}
|
||||
|
||||
private void renderProject(JSONObject payload) {
|
||||
private void renderProject(JSONObject payload, @Nullable JSONArray dispatchPlans) {
|
||||
JSONObject project = payload.optJSONObject("project");
|
||||
JSONArray devices = payload.optJSONArray("devices");
|
||||
JSONObject threadMeta = project == null ? null : project.optJSONObject("threadMeta");
|
||||
@@ -254,12 +270,18 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
initialProjectName = title;
|
||||
projectIsGroup = project != null && project.optBoolean("isGroup", false);
|
||||
projectFolderName = threadMeta == null ? "" : threadMeta.optString("folderName", "");
|
||||
projectCollaborationMode = project == null ? "development" : project.optString("collaborationMode", "development");
|
||||
projectApprovalState = project == null ? "not_required" : project.optString("approvalState", "not_required");
|
||||
currentPendingDispatchPlan = ProjectChatUiState.latestPendingDispatchPlan(dispatchPlans);
|
||||
conversationInfoReady = project != null;
|
||||
updateProjectHeader(title, buildProjectSubtitle(projectFolderName, devices));
|
||||
|
||||
renderQuickActions();
|
||||
replaceContent();
|
||||
pendingOutgoingBubble = null;
|
||||
if (currentPendingDispatchPlan != null) {
|
||||
appendContent(buildPendingDispatchPlanView(currentPendingDispatchPlan));
|
||||
}
|
||||
|
||||
JSONArray messages = project == null ? null : project.optJSONArray("messages");
|
||||
selectionState = ProjectChatUiState.reconcileSelection(selectionState, collectMessageIds(messages));
|
||||
@@ -437,11 +459,29 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
if (!response.ok()) {
|
||||
throw new IllegalStateException(response.message());
|
||||
}
|
||||
JSONObject dispatchPlan = response.json.optJSONObject("dispatchPlan");
|
||||
JSONObject collaborationGate = response.json.optJSONObject("collaborationGate");
|
||||
runOnUiThread(() -> {
|
||||
composerSending = false;
|
||||
composerInput.setText("");
|
||||
showMessage("消息已发送");
|
||||
if (collaborationGate != null) {
|
||||
projectCollaborationMode = collaborationGate.optString("collaborationMode", projectCollaborationMode);
|
||||
projectApprovalState = collaborationGate.optString("approvalState", projectApprovalState);
|
||||
}
|
||||
currentPendingDispatchPlan = dispatchPlan;
|
||||
if (dispatchPlan != null) {
|
||||
showMessage(
|
||||
"approval_required".equals(projectCollaborationMode)
|
||||
? "消息已发送,等待你批准主 Agent 下发。"
|
||||
: "消息已发送,主 Agent 已给出推荐线程。"
|
||||
);
|
||||
} else {
|
||||
showMessage("消息已发送");
|
||||
}
|
||||
reload(true);
|
||||
if (dispatchPlan != null) {
|
||||
showDispatchPlanConfirmation(dispatchPlan);
|
||||
}
|
||||
});
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
@@ -530,6 +570,74 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
conversationInfoLauncher.launch(intent);
|
||||
}
|
||||
|
||||
private View buildPendingDispatchPlanView(JSONObject dispatchPlan) {
|
||||
LinearLayout container = new LinearLayout(this);
|
||||
container.setOrientation(LinearLayout.VERTICAL);
|
||||
container.addView(BossUi.buildCard(
|
||||
this,
|
||||
"approval_required".equals(projectCollaborationMode) ? "等待你批准主 Agent 下发" : "主 Agent 推荐下发",
|
||||
ProjectChatUiState.summarizeDispatchPlan(dispatchPlan),
|
||||
"当前确认状态:" + projectApprovalState
|
||||
));
|
||||
Button confirmButton = BossUi.buildMiniActionButton(this, "确认下发", true);
|
||||
confirmButton.setOnClickListener(v -> showDispatchPlanConfirmation(dispatchPlan));
|
||||
container.addView(BossUi.buildInlineActionRow(this, confirmButton));
|
||||
return container;
|
||||
}
|
||||
|
||||
private void showDispatchPlanConfirmation(JSONObject dispatchPlan) {
|
||||
String title = "approval_required".equals(projectCollaborationMode)
|
||||
? "批准主 Agent 下发"
|
||||
: "确认主 Agent 推荐";
|
||||
String message = ProjectChatUiState.summarizeDispatchPlan(dispatchPlan)
|
||||
+ "\n\n确认后会把任务下发到推荐线程,并把线程原始回复与主 Agent 汇总一起回到群聊。";
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(title)
|
||||
.setMessage(message)
|
||||
.setNegativeButton("稍后", null)
|
||||
.setPositiveButton("确认下发", (dialog, which) -> confirmDispatchPlan(dispatchPlan))
|
||||
.show();
|
||||
}
|
||||
|
||||
private void confirmDispatchPlan(JSONObject dispatchPlan) {
|
||||
String planId = dispatchPlan.optString("planId", "").trim();
|
||||
if (planId.isEmpty()) {
|
||||
showMessage("缺少调度方案 ID");
|
||||
return;
|
||||
}
|
||||
List<String> approvedTargetProjectIds = ProjectChatUiState.dispatchPlanApprovedTargetIds(dispatchPlan);
|
||||
if (approvedTargetProjectIds.isEmpty()) {
|
||||
showMessage("当前没有可下发的目标线程");
|
||||
return;
|
||||
}
|
||||
setRefreshing(true);
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
JSONArray approved = new JSONArray();
|
||||
for (String approvedTargetProjectId : approvedTargetProjectIds) {
|
||||
approved.put(approvedTargetProjectId);
|
||||
}
|
||||
BossApiClient.ApiResponse response = apiClient.confirmDispatchPlan(projectId, planId, approved);
|
||||
if (!response.ok()) {
|
||||
throw new IllegalStateException(response.message());
|
||||
}
|
||||
JSONArray executions = response.json.optJSONArray("executions");
|
||||
int executionCount = executions == null ? approvedTargetProjectIds.size() : executions.length();
|
||||
runOnUiThread(() -> {
|
||||
currentPendingDispatchPlan = null;
|
||||
projectApprovalState = "approval_required".equals(projectCollaborationMode) ? "approved" : "not_required";
|
||||
showMessage("已确认下发到 " + executionCount + " 个线程");
|
||||
reload(true);
|
||||
});
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
setRefreshing(false);
|
||||
showMessage("确认下发失败:" + error.getMessage());
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private View buildMessageView(JSONObject message) {
|
||||
String messageId = message.optString("id", "");
|
||||
String senderLabel = message.optString("senderLabel", "消息");
|
||||
|
||||
Reference in New Issue
Block a user