feat: streamline group dispatch reminders
This commit is contained in:
@@ -167,12 +167,18 @@ public class BossApiClient {
|
||||
return requestWithRestore("DELETE", "/api/v1/projects/" + encode(projectId) + "/memories/" + encode(memoryId), null);
|
||||
}
|
||||
|
||||
public ApiResponse confirmDispatchPlan(String projectId, String planId, JSONArray approvedTargetProjectIds) throws IOException, JSONException {
|
||||
public ApiResponse confirmDispatchPlan(
|
||||
String projectId,
|
||||
String planId,
|
||||
JSONArray approvedTargetProjectIds,
|
||||
boolean rememberLightReminder
|
||||
) throws IOException, JSONException {
|
||||
JSONObject payload = new JSONObject();
|
||||
payload.put(
|
||||
"approvedTargetProjectIds",
|
||||
approvedTargetProjectIds == null ? new JSONArray() : approvedTargetProjectIds
|
||||
);
|
||||
payload.put("rememberLightReminder", rememberLightReminder);
|
||||
return requestWithRestoreRaw(
|
||||
"POST",
|
||||
"/api/v1/projects/" + encode(projectId) + "/dispatch-plans/" + encode(planId) + "/confirm",
|
||||
@@ -182,6 +188,10 @@ public class BossApiClient {
|
||||
);
|
||||
}
|
||||
|
||||
public ApiResponse confirmDispatchPlan(String projectId, String planId, JSONArray approvedTargetProjectIds) throws IOException, JSONException {
|
||||
return confirmDispatchPlan(projectId, planId, approvedTargetProjectIds, false);
|
||||
}
|
||||
|
||||
public ApiResponse rejectDispatchPlan(String projectId, String planId) throws IOException, JSONException {
|
||||
return requestWithRestoreRaw(
|
||||
"POST",
|
||||
@@ -227,6 +237,12 @@ public class BossApiClient {
|
||||
return requestWithRestore("POST", "/api/v1/projects/" + encode(projectId) + "/participants", payload);
|
||||
}
|
||||
|
||||
public ApiResponse updateProjectDispatchReminder(String projectId, boolean lightDispatchReminderEnabled) throws IOException, JSONException {
|
||||
JSONObject payload = new JSONObject();
|
||||
payload.put("lightDispatchReminderEnabled", lightDispatchReminderEnabled);
|
||||
return requestWithRestore("PATCH", "/api/v1/projects/" + encode(projectId) + "/dispatch-reminder", payload);
|
||||
}
|
||||
|
||||
public ApiResponse sendProjectMessage(String projectId, String body, String kind) throws IOException, JSONException {
|
||||
JSONObject payload = new JSONObject();
|
||||
payload.put("body", body);
|
||||
|
||||
@@ -106,6 +106,7 @@ public class GroupInfoActivity extends BossScreenActivity {
|
||||
if (orchestrationBackendPayload != null) {
|
||||
appendContent(buildOrchestrationBackendRow(orchestrationBackendPayload));
|
||||
}
|
||||
appendContent(buildDispatchReminderRow(project));
|
||||
|
||||
if (repairRequired) {
|
||||
String meta = invalidParticipantCount > 0
|
||||
@@ -150,6 +151,18 @@ public class GroupInfoActivity extends BossScreenActivity {
|
||||
setRefreshing(false);
|
||||
}
|
||||
|
||||
private LinearLayout buildDispatchReminderRow(JSONObject project) {
|
||||
boolean enabled = project.optBoolean("lightDispatchReminderEnabled", false);
|
||||
return BossUi.buildWechatMenuRow(
|
||||
this,
|
||||
"推荐下发默认轻提醒",
|
||||
enabled ? "已开启" : "已关闭",
|
||||
enabled ? "后续推荐会保留轻状态卡,不再弹重确认提醒。" : "当前仍会显式提醒你确认主 Agent 推荐。",
|
||||
enabled ? "开启" : "关闭",
|
||||
v -> openDispatchReminderDialog(enabled)
|
||||
);
|
||||
}
|
||||
|
||||
private LinearLayout buildMemberRow(JSONObject participant) {
|
||||
boolean sourceProject = participant.optBoolean("isSourceProject", false);
|
||||
boolean canOpenProject = participant.optBoolean("canOpenProject", true);
|
||||
@@ -426,6 +439,39 @@ public class GroupInfoActivity extends BossScreenActivity {
|
||||
});
|
||||
}
|
||||
|
||||
private void openDispatchReminderDialog(boolean enabled) {
|
||||
CharSequence[] items = enabled
|
||||
? new CharSequence[]{"关闭默认轻提醒"}
|
||||
: new CharSequence[]{"开启默认轻提醒"};
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("推荐下发默认轻提醒")
|
||||
.setMessage(enabled
|
||||
? "关闭后,这个群会恢复成每次都显式确认的提醒方式。"
|
||||
: "开启后,这个群后续仍会显示一张轻状态卡,但不再出现重提醒。")
|
||||
.setItems(items, (dialog, which) -> saveDispatchReminderPreference(!enabled))
|
||||
.setNegativeButton("取消", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void saveDispatchReminderPreference(boolean enabled) {
|
||||
setRefreshing(true);
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
BossApiClient.ApiResponse response = apiClient.updateProjectDispatchReminder(projectId, enabled);
|
||||
if (!response.ok()) throw new IllegalStateException(response.message());
|
||||
runOnUiThread(() -> {
|
||||
showMessage(enabled ? "已开启默认轻提醒" : "已关闭默认轻提醒");
|
||||
reload();
|
||||
});
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
setRefreshing(false);
|
||||
showMessage("保存失败:" + error.getMessage());
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void showMoreMenu() {
|
||||
new AlertDialog.Builder(this)
|
||||
.setItems(new CharSequence[]{"改名", "刷新"}, (dialog, which) -> {
|
||||
|
||||
@@ -312,6 +312,50 @@ public final class ProjectChatUiState {
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public static String summarizeDispatchPlanCompact(@Nullable JSONObject plan) {
|
||||
if (plan == null) {
|
||||
return "主 Agent 暂未生成推荐线程。";
|
||||
}
|
||||
List<String> targetTitles = dispatchPlanTargetTitles(plan);
|
||||
String summary = plan.optString("summary", "").trim();
|
||||
if (targetTitles.isEmpty()) {
|
||||
return isBlank(summary) ? "主 Agent 已生成推荐线程。" : truncate(summary, 32);
|
||||
}
|
||||
if (isBlank(summary)) {
|
||||
return "推荐给:" + String.join("、", targetTitles);
|
||||
}
|
||||
return "推荐给:" + String.join("、", targetTitles) + "\n" + truncate(summary, 32);
|
||||
}
|
||||
|
||||
public static String summarizeDispatchPlanLight(@Nullable JSONObject plan) {
|
||||
int targetCount = dispatchPlanTargetTitles(plan).size();
|
||||
if (targetCount <= 0) {
|
||||
return "主 Agent 已推荐线程";
|
||||
}
|
||||
return "主 Agent 已推荐 " + targetCount + " 个线程";
|
||||
}
|
||||
|
||||
private static List<String> dispatchPlanTargetTitles(@Nullable JSONObject plan) {
|
||||
List<String> targetTitles = new ArrayList<>();
|
||||
if (plan == null) {
|
||||
return targetTitles;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
return targetTitles;
|
||||
}
|
||||
|
||||
public static String formatAttachmentSize(long fileSizeBytes) {
|
||||
if (fileSizeBytes >= 1024L * 1024L) {
|
||||
return String.format(java.util.Locale.US, "%.1f MB", fileSizeBytes / (1024f * 1024f));
|
||||
|
||||
@@ -70,6 +70,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
private String currentScreenSubtitle;
|
||||
private String projectCollaborationMode = "development";
|
||||
private String projectApprovalState = "not_required";
|
||||
private boolean lightDispatchReminderEnabled;
|
||||
private @Nullable JSONObject currentPendingDispatchPlan;
|
||||
private @Nullable JSONObject currentRejectedDispatchPlan;
|
||||
private ProjectChatUiState.SelectionState selectionState = ProjectChatUiState.emptySelection();
|
||||
@@ -310,6 +311,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
projectFolderName = threadMeta == null ? "" : threadMeta.optString("folderName", "");
|
||||
projectCollaborationMode = project == null ? "development" : project.optString("collaborationMode", "development");
|
||||
projectApprovalState = project == null ? "not_required" : project.optString("approvalState", "not_required");
|
||||
lightDispatchReminderEnabled = project != null && project.optBoolean("lightDispatchReminderEnabled", false);
|
||||
JSONObject agentControls = project == null ? null : project.optJSONObject("agentControls");
|
||||
currentAgentModelOverride = normalizeControlValue(agentControls == null ? null : agentControls.optString("modelOverride", null));
|
||||
currentReasoningEffortOverride = normalizeControlValue(agentControls == null ? null : agentControls.optString("reasoningEffortOverride", null));
|
||||
@@ -530,6 +532,8 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
if (collaborationGate != null) {
|
||||
projectCollaborationMode = collaborationGate.optString("collaborationMode", projectCollaborationMode);
|
||||
projectApprovalState = collaborationGate.optString("approvalState", projectApprovalState);
|
||||
lightDispatchReminderEnabled =
|
||||
collaborationGate.optBoolean("lightDispatchReminderEnabled", lightDispatchReminderEnabled);
|
||||
}
|
||||
currentPendingDispatchPlan = dispatchPlan;
|
||||
currentRejectedDispatchPlan = null;
|
||||
@@ -542,7 +546,6 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
: "消息已发送,主 Agent 已给出推荐线程。"
|
||||
);
|
||||
reload(true);
|
||||
showDispatchPlanConfirmation(dispatchPlan);
|
||||
return;
|
||||
}
|
||||
if (waitSpec.shouldWait) {
|
||||
@@ -852,12 +855,28 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
container.setOrientation(LinearLayout.VERTICAL);
|
||||
container.addView(BossUi.buildCard(
|
||||
this,
|
||||
"approval_required".equals(projectCollaborationMode) ? "等待你批准主 Agent 下发" : "主 Agent 推荐下发",
|
||||
ProjectChatUiState.summarizeDispatchPlan(dispatchPlan),
|
||||
"当前确认状态:" + describeDispatchPlanApprovalState(projectApprovalState)
|
||||
lightDispatchReminderEnabled
|
||||
? ProjectChatUiState.summarizeDispatchPlanLight(dispatchPlan)
|
||||
: ("approval_required".equals(projectCollaborationMode) ? "主 Agent 推荐下发" : "主 Agent 推荐"),
|
||||
lightDispatchReminderEnabled
|
||||
? ProjectChatUiState.summarizeDispatchPlanCompact(dispatchPlan)
|
||||
: ProjectChatUiState.summarizeDispatchPlanCompact(dispatchPlan),
|
||||
lightDispatchReminderEnabled ? "轻提醒已开启" : "当前状态:" + describeDispatchPlanApprovalState(projectApprovalState)
|
||||
));
|
||||
Button confirmButton = BossUi.buildMiniActionButton(this, "确认下发", true);
|
||||
confirmButton.setOnClickListener(v -> showDispatchPlanConfirmation(dispatchPlan));
|
||||
Button confirmButton = BossUi.buildMiniActionButton(
|
||||
this,
|
||||
lightDispatchReminderEnabled ? "继续下发" : "确认一下",
|
||||
true
|
||||
);
|
||||
confirmButton.setOnClickListener(v -> confirmDispatchPlan(dispatchPlan, false));
|
||||
if (!lightDispatchReminderEnabled) {
|
||||
Button rememberButton = BossUi.buildMiniActionButton(this, "确认并记住", false);
|
||||
rememberButton.setOnClickListener(v -> confirmDispatchPlan(dispatchPlan, true));
|
||||
Button rejectButton = BossUi.buildMiniActionButton(this, "拒绝", false);
|
||||
rejectButton.setOnClickListener(v -> rejectDispatchPlan(dispatchPlan));
|
||||
container.addView(BossUi.buildInlineActionRow(this, confirmButton, rememberButton, rejectButton));
|
||||
return container;
|
||||
}
|
||||
Button rejectButton = BossUi.buildMiniActionButton(this, "拒绝", false);
|
||||
rejectButton.setOnClickListener(v -> rejectDispatchPlan(dispatchPlan));
|
||||
container.addView(BossUi.buildInlineActionRow(this, confirmButton, rejectButton));
|
||||
@@ -899,22 +918,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
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("拒绝", (dialog, which) -> rejectDispatchPlan(dispatchPlan))
|
||||
.setNeutralButton("稍后", null)
|
||||
.setPositiveButton("确认下发", (dialog, which) -> confirmDispatchPlan(dispatchPlan))
|
||||
.show();
|
||||
}
|
||||
|
||||
private void confirmDispatchPlan(JSONObject dispatchPlan) {
|
||||
private void confirmDispatchPlan(JSONObject dispatchPlan, boolean rememberLightReminder) {
|
||||
String planId = dispatchPlan.optString("planId", "").trim();
|
||||
if (planId.isEmpty()) {
|
||||
showMessage("缺少调度方案 ID");
|
||||
@@ -932,7 +936,12 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
for (String approvedTargetProjectId : approvedTargetProjectIds) {
|
||||
approved.put(approvedTargetProjectId);
|
||||
}
|
||||
BossApiClient.ApiResponse response = apiClient.confirmDispatchPlan(projectId, planId, approved);
|
||||
BossApiClient.ApiResponse response = apiClient.confirmDispatchPlan(
|
||||
projectId,
|
||||
planId,
|
||||
approved,
|
||||
rememberLightReminder
|
||||
);
|
||||
if (!response.ok()) {
|
||||
throw new IllegalStateException(response.message());
|
||||
}
|
||||
@@ -942,6 +951,9 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
ProjectChatUiState.resolveReplyWaitAfterDispatchConfirm(response.json);
|
||||
runOnUiThread(() -> {
|
||||
applyDispatchPlanActionResponse(response.json);
|
||||
if (rememberLightReminder) {
|
||||
showMessage("已确认下发,并对这个群开启默认轻提醒");
|
||||
}
|
||||
if (waitSpec.shouldWait) {
|
||||
startReplyWait(
|
||||
waitSpec,
|
||||
@@ -1011,9 +1023,6 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
applyDispatchPlanActionResponse(response.json);
|
||||
showMessage("主 Agent 已重新生成推荐");
|
||||
reload(true);
|
||||
if (nextPlan != null) {
|
||||
showDispatchPlanConfirmation(nextPlan);
|
||||
}
|
||||
});
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
@@ -2103,6 +2112,8 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
if (collaborationGate != null) {
|
||||
projectCollaborationMode = collaborationGate.optString("collaborationMode", projectCollaborationMode);
|
||||
projectApprovalState = collaborationGate.optString("approvalState", projectApprovalState);
|
||||
lightDispatchReminderEnabled =
|
||||
collaborationGate.optBoolean("lightDispatchReminderEnabled", lightDispatchReminderEnabled);
|
||||
}
|
||||
JSONObject plan = response.optJSONObject("plan");
|
||||
if (plan != null) {
|
||||
|
||||
@@ -161,6 +161,29 @@ public class GroupInfoActivityTest {
|
||||
assertTrue(viewTreeContainsText(content, "OMX Team Runtime 当前可用,当前可切换到该后端。"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void renderGroupShowsLightReminderPreferenceRow() throws Exception {
|
||||
Intent intent = new Intent()
|
||||
.putExtra(GroupInfoActivity.EXTRA_PROJECT_ID, "group-1")
|
||||
.putExtra(GroupInfoActivity.EXTRA_PROJECT_NAME, "巡检协作群");
|
||||
TestGroupInfoActivity activity = Robolectric
|
||||
.buildActivity(TestGroupInfoActivity.class, intent)
|
||||
.setup()
|
||||
.get();
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"renderGroup",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildDetailPayload(true)),
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildParticipantsPayload()),
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildOrchestrationBackendPayload())
|
||||
);
|
||||
|
||||
LinearLayout content = activity.findViewById(R.id.screen_content);
|
||||
assertTrue(viewTreeContainsText(content, "推荐下发默认轻提醒"));
|
||||
assertTrue(viewTreeContainsText(content, "已开启"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void renderGroupShowsOmxFallbackHintWhenOmxRuntimeIsUnavailable() throws Exception {
|
||||
Intent intent = new Intent()
|
||||
@@ -236,7 +259,38 @@ public class GroupInfoActivityTest {
|
||||
assertEquals("{\"requestedBackendId\":\"omx-team\"}", connection.requestBody());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveDispatchReminderPreferenceUsesScopedEndpoint() throws Exception {
|
||||
Intent intent = new Intent()
|
||||
.putExtra(GroupInfoActivity.EXTRA_PROJECT_ID, "group-1")
|
||||
.putExtra(GroupInfoActivity.EXTRA_PROJECT_NAME, "巡检协作群");
|
||||
TestGroupInfoActivity activity = Robolectric
|
||||
.buildActivity(TestGroupInfoActivity.class, intent)
|
||||
.setup()
|
||||
.get();
|
||||
RecordingConnection connection = new RecordingConnection(
|
||||
new URL("https://boss.hyzq.net/api/v1/projects/group-1/dispatch-reminder")
|
||||
);
|
||||
ReflectionHelpers.setField(activity, "apiClient", new RecordingBossApiClient(connection));
|
||||
ReflectionHelpers.setField(activity, "executor", new DirectExecutorService());
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"saveDispatchReminderPreference",
|
||||
ReflectionHelpers.ClassParameter.from(boolean.class, true)
|
||||
);
|
||||
Shadows.shadowOf(Looper.getMainLooper()).idle();
|
||||
|
||||
assertEquals("/api/v1/projects/group-1/dispatch-reminder", connection.lastPath);
|
||||
assertEquals("PATCH", connection.requestMethodValue);
|
||||
assertEquals("{\"lightDispatchReminderEnabled\":true}", connection.requestBody());
|
||||
}
|
||||
|
||||
private static JSONObject buildDetailPayload() throws Exception {
|
||||
return buildDetailPayload(false);
|
||||
}
|
||||
|
||||
private static JSONObject buildDetailPayload(boolean lightReminderEnabled) throws Exception {
|
||||
JSONObject threadMeta = new JSONObject()
|
||||
.put("threadId", "group-thread-3")
|
||||
.put("folderName", "Boss");
|
||||
@@ -245,6 +299,7 @@ public class GroupInfoActivityTest {
|
||||
.put("name", "巡检协作群")
|
||||
.put("isGroup", true)
|
||||
.put("collaborationMode", "development")
|
||||
.put("lightDispatchReminderEnabled", lightReminderEnabled)
|
||||
.put("threadMeta", threadMeta);
|
||||
return new JSONObject().put("project", project);
|
||||
}
|
||||
|
||||
@@ -463,10 +463,45 @@ public class ProjectDetailActivityUiTest {
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, dispatchPlan)
|
||||
);
|
||||
|
||||
assertTrue(viewTreeContainsText(card, "确认下发"));
|
||||
assertTrue(viewTreeContainsText(card, "确认一下"));
|
||||
assertTrue(viewTreeContainsText(card, "确认并记住"));
|
||||
assertTrue(viewTreeContainsText(card, "拒绝"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pendingDispatchPlanViewUsesCompactCopyWhenLightReminderEnabled() throws Exception {
|
||||
Intent intent = new Intent()
|
||||
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, "project-group")
|
||||
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, "协作群");
|
||||
TestProjectDetailActivity activity = Robolectric
|
||||
.buildActivity(TestProjectDetailActivity.class, intent)
|
||||
.setup()
|
||||
.get();
|
||||
|
||||
ReflectionHelpers.setField(activity, "projectCollaborationMode", "approval_required");
|
||||
ReflectionHelpers.setField(activity, "projectApprovalState", "pending_user");
|
||||
ReflectionHelpers.setField(activity, "lightDispatchReminderEnabled", true);
|
||||
|
||||
JSONObject dispatchPlan = new JSONObject()
|
||||
.put("planId", "dispatch-plan-light")
|
||||
.put("summary", "建议先让 Boss 移动控制台同步树莓派二代状态。")
|
||||
.put("targets", new JSONArray().put(new JSONObject()
|
||||
.put("projectId", "thread-1")
|
||||
.put("threadDisplayName", "Boss 移动控制台")
|
||||
.put("reason", "最近活跃")));
|
||||
|
||||
View card = ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"buildPendingDispatchPlanView",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, dispatchPlan)
|
||||
);
|
||||
|
||||
assertTrue(viewTreeContainsText(card, "主 Agent 已推荐 1 个线程"));
|
||||
assertTrue(viewTreeContainsText(card, "继续下发"));
|
||||
assertFalse(viewTreeContainsText(card, "等待你批准主 Agent 下发"));
|
||||
assertFalse(viewTreeContainsText(card, "当前确认状态:"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void renderProjectShowsRepairEntryForDirtyGroupAndOpensGroupInfo() throws Exception {
|
||||
Intent intent = new Intent()
|
||||
|
||||
Reference in New Issue
Block a user