Integrate master agent runtime orchestration updates
This commit is contained in:
@@ -119,6 +119,10 @@ public class BossApiClient {
|
||||
return requestWithRestore("GET", "/api/v1/projects/" + encode(projectId) + "/dispatch-plans", null);
|
||||
}
|
||||
|
||||
public ApiResponse getConversationParticipants(String projectId) throws IOException, JSONException {
|
||||
return requestWithRestore("GET", "/api/v1/projects/" + encode(projectId) + "/participants", null);
|
||||
}
|
||||
|
||||
public ApiResponse getProjectAgentControls(String projectId) throws IOException, JSONException {
|
||||
return requestWithRestore("GET", "/api/v1/projects/" + encode(projectId) + "/agent-controls", null);
|
||||
}
|
||||
@@ -134,6 +138,76 @@ public class BossApiClient {
|
||||
return requestWithRestore("POST", "/api/v1/projects/" + encode(projectId) + "/agent-controls", payload);
|
||||
}
|
||||
|
||||
public ApiResponse updateProjectAgentControls(
|
||||
String projectId,
|
||||
@Nullable String modelOverride,
|
||||
@Nullable String reasoningEffortOverride,
|
||||
@Nullable String fastModelOverride,
|
||||
@Nullable String fastReasoningEffortOverride,
|
||||
@Nullable String smartModelOverride,
|
||||
@Nullable String smartReasoningEffortOverride
|
||||
) throws IOException, JSONException {
|
||||
JSONObject payload = buildProjectAgentControlsPayload(
|
||||
modelOverride,
|
||||
reasoningEffortOverride,
|
||||
fastModelOverride,
|
||||
fastReasoningEffortOverride,
|
||||
smartModelOverride,
|
||||
smartReasoningEffortOverride,
|
||||
false
|
||||
);
|
||||
return requestWithRestore("POST", "/api/v1/projects/" + encode(projectId) + "/agent-controls", payload);
|
||||
}
|
||||
|
||||
public ApiResponse updateProjectAgentControls(
|
||||
String projectId,
|
||||
@Nullable String modelOverride,
|
||||
@Nullable String reasoningEffortOverride,
|
||||
@Nullable String fastModelOverride,
|
||||
@Nullable String fastReasoningEffortOverride,
|
||||
@Nullable String smartModelOverride,
|
||||
@Nullable String smartReasoningEffortOverride,
|
||||
boolean includeAdvancedOverrides
|
||||
) throws IOException, JSONException {
|
||||
JSONObject payload = buildProjectAgentControlsPayload(
|
||||
modelOverride,
|
||||
reasoningEffortOverride,
|
||||
fastModelOverride,
|
||||
fastReasoningEffortOverride,
|
||||
smartModelOverride,
|
||||
smartReasoningEffortOverride,
|
||||
includeAdvancedOverrides
|
||||
);
|
||||
return requestWithRestore("POST", "/api/v1/projects/" + encode(projectId) + "/agent-controls", payload);
|
||||
}
|
||||
|
||||
static JSONObject buildProjectAgentControlsPayload(
|
||||
@Nullable String modelOverride,
|
||||
@Nullable String reasoningEffortOverride,
|
||||
@Nullable String fastModelOverride,
|
||||
@Nullable String fastReasoningEffortOverride,
|
||||
@Nullable String smartModelOverride,
|
||||
@Nullable String smartReasoningEffortOverride,
|
||||
boolean includeAdvancedOverrides
|
||||
) throws JSONException {
|
||||
JSONObject payload = new JSONObject();
|
||||
payload.put("modelOverride", modelOverride == null ? JSONObject.NULL : modelOverride);
|
||||
payload.put("reasoningEffortOverride", reasoningEffortOverride == null ? JSONObject.NULL : reasoningEffortOverride);
|
||||
|
||||
boolean hasAdvancedOverrides =
|
||||
fastModelOverride != null
|
||||
|| fastReasoningEffortOverride != null
|
||||
|| smartModelOverride != null
|
||||
|| smartReasoningEffortOverride != null;
|
||||
if (includeAdvancedOverrides || hasAdvancedOverrides) {
|
||||
payload.put("fastModelOverride", fastModelOverride == null ? JSONObject.NULL : fastModelOverride);
|
||||
payload.put("fastReasoningEffortOverride", fastReasoningEffortOverride == null ? JSONObject.NULL : fastReasoningEffortOverride);
|
||||
payload.put("smartModelOverride", smartModelOverride == null ? JSONObject.NULL : smartModelOverride);
|
||||
payload.put("smartReasoningEffortOverride", smartReasoningEffortOverride == null ? JSONObject.NULL : smartReasoningEffortOverride);
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
public ApiResponse updateProjectAgentControls(
|
||||
String projectId,
|
||||
@Nullable String modelOverride,
|
||||
@@ -263,10 +337,6 @@ public class BossApiClient {
|
||||
return requestWithRestore("POST", "/api/v1/group-chats", payload == null ? new JSONObject() : payload);
|
||||
}
|
||||
|
||||
public ApiResponse getConversationParticipants(String projectId) throws IOException, JSONException {
|
||||
return requestWithRestore("GET", "/api/v1/projects/" + encode(projectId) + "/participants", null);
|
||||
}
|
||||
|
||||
public ApiResponse getThreadStatus(String projectId) throws IOException, JSONException {
|
||||
return requestWithRestore("GET", "/api/v1/projects/" + encode(projectId) + "/thread-status", null);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,11 @@ import androidx.annotation.Nullable;
|
||||
import androidx.core.widget.ImageViewCompat;
|
||||
import android.content.res.ColorStateList;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class BossUi {
|
||||
private static final int[] AVATAR_BG_COLORS = {
|
||||
Color.parseColor("#1EC76F"),
|
||||
@@ -1229,6 +1234,195 @@ public final class BossUi {
|
||||
return buildMessageBubble(context, effectiveSender, body, "发送中", true, null);
|
||||
}
|
||||
|
||||
public static LinearLayout buildExecutionWarningCard(
|
||||
Context context,
|
||||
String title,
|
||||
@Nullable String summary,
|
||||
boolean outgoing
|
||||
) {
|
||||
LinearLayout card = new LinearLayout(context);
|
||||
card.setOrientation(LinearLayout.VERTICAL);
|
||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
params.topMargin = dp(context, 8);
|
||||
card.setLayoutParams(params);
|
||||
card.setPadding(dp(context, 12), dp(context, 10), dp(context, 12), dp(context, 10));
|
||||
card.setBackground(createRoundedBackground(
|
||||
outgoing ? Color.parseColor("#FFF1D7") : Color.parseColor("#FFF7E7"),
|
||||
dp(context, 14)
|
||||
));
|
||||
|
||||
TextView titleView = new TextView(context);
|
||||
titleView.setText(TextUtils.isEmpty(title) ? "执行提醒" : title);
|
||||
titleView.setTextSize(12);
|
||||
titleView.setTypeface(Typeface.DEFAULT_BOLD);
|
||||
titleView.setTextColor(Color.parseColor("#B36B00"));
|
||||
card.addView(titleView);
|
||||
|
||||
if (!TextUtils.isEmpty(summary)) {
|
||||
TextView summaryView = new TextView(context);
|
||||
summaryView.setText(summary);
|
||||
summaryView.setTextSize(13);
|
||||
summaryView.setLineSpacing(0f, 1.2f);
|
||||
summaryView.setTextColor(context.getColor(R.color.boss_text_primary));
|
||||
summaryView.setPadding(0, dp(context, 4), 0, 0);
|
||||
summaryView.setMaxWidth(Math.round(context.getResources().getDisplayMetrics().widthPixels * 0.62f));
|
||||
summaryView.setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY);
|
||||
summaryView.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL);
|
||||
card.addView(summaryView);
|
||||
}
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
public static LinearLayout buildMessageStatusRow(
|
||||
Context context,
|
||||
JSONObject message,
|
||||
@Nullable JSONObject conversationTask,
|
||||
List<JSONObject> warnings,
|
||||
boolean outgoing
|
||||
) {
|
||||
LinearLayout row = new LinearLayout(context);
|
||||
row.setOrientation(LinearLayout.VERTICAL);
|
||||
LinearLayout.LayoutParams rowParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
rowParams.topMargin = dp(context, 8);
|
||||
row.setLayoutParams(rowParams);
|
||||
|
||||
boolean hasTask = conversationTask != null;
|
||||
boolean hasWarnings = warnings != null && !warnings.isEmpty();
|
||||
String detailText = buildStatusDetailText(message, conversationTask, warnings);
|
||||
boolean hasDetail = !TextUtils.isEmpty(detailText);
|
||||
if (!hasTask && !hasWarnings && !hasDetail) {
|
||||
row.setVisibility(View.GONE);
|
||||
return row;
|
||||
}
|
||||
|
||||
if (hasTask || hasWarnings) {
|
||||
LinearLayout pillRow = new LinearLayout(context);
|
||||
pillRow.setOrientation(LinearLayout.HORIZONTAL);
|
||||
pillRow.setGravity(Gravity.CENTER_VERTICAL);
|
||||
row.addView(pillRow);
|
||||
if (hasTask) {
|
||||
pillRow.addView(buildStatusPill(
|
||||
context,
|
||||
taskStatusLabel(conversationTask.optString("status", "")),
|
||||
outgoing ? Color.parseColor("#D7F3DF") : Color.parseColor("#EDF7F0"),
|
||||
Color.parseColor("#215B39")
|
||||
));
|
||||
}
|
||||
if (hasWarnings) {
|
||||
if (pillRow.getChildCount() > 0) {
|
||||
View spacer = new View(context);
|
||||
LinearLayout.LayoutParams spacerParams = new LinearLayout.LayoutParams(dp(context, 6), 1);
|
||||
spacer.setLayoutParams(spacerParams);
|
||||
pillRow.addView(spacer);
|
||||
}
|
||||
pillRow.addView(buildStatusPill(
|
||||
context,
|
||||
warningBadgeLabel(warnings),
|
||||
outgoing ? Color.parseColor("#FFF1D7") : Color.parseColor("#FFF7E7"),
|
||||
Color.parseColor("#B36B00")
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if (hasDetail) {
|
||||
TextView detailView = new TextView(context);
|
||||
detailView.setText(detailText);
|
||||
detailView.setTextSize(12);
|
||||
detailView.setTextColor(context.getColor(R.color.boss_text_muted));
|
||||
detailView.setPadding(dp(context, 2), dp(context, 4), dp(context, 2), 0);
|
||||
detailView.setMaxLines(2);
|
||||
detailView.setEllipsize(TextUtils.TruncateAt.END);
|
||||
detailView.setMaxWidth(Math.round(context.getResources().getDisplayMetrics().widthPixels * 0.62f));
|
||||
row.addView(detailView);
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
private static TextView buildStatusPill(
|
||||
Context context,
|
||||
String text,
|
||||
int backgroundColor,
|
||||
int textColor
|
||||
) {
|
||||
TextView pill = new TextView(context);
|
||||
pill.setText(text);
|
||||
pill.setTextSize(11);
|
||||
pill.setTypeface(Typeface.DEFAULT_BOLD);
|
||||
pill.setTextColor(textColor);
|
||||
pill.setPadding(dp(context, 10), dp(context, 5), dp(context, 10), dp(context, 5));
|
||||
pill.setBackground(createRoundedBackground(backgroundColor, dp(context, 12)));
|
||||
return pill;
|
||||
}
|
||||
|
||||
private static String taskStatusLabel(String status) {
|
||||
switch (status) {
|
||||
case "queued":
|
||||
return "排队中";
|
||||
case "running":
|
||||
return "执行中";
|
||||
case "completed":
|
||||
return "已完成";
|
||||
case "failed":
|
||||
return "已失败";
|
||||
default:
|
||||
return TextUtils.isEmpty(status) ? "处理中" : status;
|
||||
}
|
||||
}
|
||||
|
||||
private static String warningBadgeLabel(List<JSONObject> warnings) {
|
||||
int totalWarnings = 0;
|
||||
for (JSONObject warning : warnings) {
|
||||
if (warning == null) {
|
||||
continue;
|
||||
}
|
||||
totalWarnings += Math.max(1, warning.optInt("count", 1));
|
||||
}
|
||||
return totalWarnings > 1 ? "提醒 " + totalWarnings : "执行提醒";
|
||||
}
|
||||
|
||||
private static String buildStatusDetailText(
|
||||
JSONObject message,
|
||||
@Nullable JSONObject conversationTask,
|
||||
List<JSONObject> warnings
|
||||
) {
|
||||
ArrayList<String> parts = new ArrayList<>();
|
||||
if (conversationTask != null) {
|
||||
String sessionId = conversationTask.optString("sessionId", "");
|
||||
String taskId = conversationTask.optString("taskId", "");
|
||||
if (!TextUtils.isEmpty(sessionId)) {
|
||||
parts.add("Session " + sessionId);
|
||||
} else if (!TextUtils.isEmpty(taskId)) {
|
||||
parts.add("Task " + taskId);
|
||||
}
|
||||
}
|
||||
if (warnings != null && !warnings.isEmpty()) {
|
||||
JSONObject firstWarning = warnings.get(0);
|
||||
if (firstWarning != null) {
|
||||
String summary = firstWarning.optString("summary", "");
|
||||
String title = firstWarning.optString("title", "执行提醒");
|
||||
int count = Math.max(1, firstWarning.optInt("count", 1));
|
||||
String warningText = !TextUtils.isEmpty(summary) ? summary : title;
|
||||
if (count > 1) {
|
||||
warningText = warningText + " 等 " + count + " 条";
|
||||
} else if (warnings.size() > 1) {
|
||||
warningText = warningText + " 等 " + warnings.size() + " 组";
|
||||
}
|
||||
parts.add(warningText);
|
||||
}
|
||||
} else if ("system_notice".equals(message.optString("kind", ""))) {
|
||||
parts.add("系统同步提醒");
|
||||
}
|
||||
return TextUtils.join(" · ", parts);
|
||||
}
|
||||
|
||||
public static void applyMessageSelectionState(Context context, View messageView, boolean selected) {
|
||||
if (messageView == null) {
|
||||
return;
|
||||
|
||||
@@ -76,7 +76,7 @@ public class ConversationInfoActivity extends BossScreenActivity {
|
||||
try {
|
||||
LoadedConversation loadedConversation = loadConversation();
|
||||
BossApiClient.ApiResponse detailResponse = loadedConversation.detailResponse;
|
||||
BossApiClient.ApiResponse participantsResponse = loadedConversation.participantsResponse;
|
||||
JSONObject participantsPayload = loadedConversation.participantsPayload;
|
||||
JSONObject threadStatusPayload = null;
|
||||
try {
|
||||
BossApiClient.ApiResponse threadStatusResponse = loadedConversation.threadStatusResponse;
|
||||
@@ -87,7 +87,7 @@ public class ConversationInfoActivity extends BossScreenActivity {
|
||||
threadStatusPayload = null;
|
||||
}
|
||||
JSONObject finalThreadStatusPayload = threadStatusPayload;
|
||||
runOnUiThread(() -> renderConversation(detailResponse.json, participantsResponse.json, finalThreadStatusPayload));
|
||||
runOnUiThread(() -> renderConversation(detailResponse.json, participantsPayload, finalThreadStatusPayload));
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
setRefreshing(false);
|
||||
@@ -471,13 +471,14 @@ public class ConversationInfoActivity extends BossScreenActivity {
|
||||
throw new IllegalStateException(detailResponse.message());
|
||||
}
|
||||
|
||||
BossApiClient.ApiResponse participantsResponse = apiClient.getConversationParticipants(projectId);
|
||||
if (!participantsResponse.ok()) {
|
||||
throw new IllegalStateException(participantsResponse.message());
|
||||
}
|
||||
|
||||
JSONObject participantsPayload = extractParticipantsPayload(detailResponse.json);
|
||||
BossApiClient.ApiResponse threadStatusResponse = apiClient.getThreadStatus(projectId);
|
||||
return new LoadedConversation(detailResponse, participantsResponse, threadStatusResponse);
|
||||
return new LoadedConversation(detailResponse, participantsPayload, threadStatusResponse);
|
||||
}
|
||||
|
||||
private JSONObject extractParticipantsPayload(JSONObject detailPayload) {
|
||||
JSONObject participantsPayload = detailPayload == null ? null : detailPayload.optJSONObject("participantsPayload");
|
||||
return participantsPayload == null ? new JSONObject() : participantsPayload;
|
||||
}
|
||||
|
||||
private BossApiClient.ApiResponse saveTakeoverSettingsWithRetry(
|
||||
@@ -548,16 +549,16 @@ public class ConversationInfoActivity extends BossScreenActivity {
|
||||
|
||||
private static final class LoadedConversation {
|
||||
private final BossApiClient.ApiResponse detailResponse;
|
||||
private final BossApiClient.ApiResponse participantsResponse;
|
||||
private final JSONObject participantsPayload;
|
||||
private final BossApiClient.ApiResponse threadStatusResponse;
|
||||
|
||||
private LoadedConversation(
|
||||
BossApiClient.ApiResponse detailResponse,
|
||||
BossApiClient.ApiResponse participantsResponse,
|
||||
JSONObject participantsPayload,
|
||||
BossApiClient.ApiResponse threadStatusResponse
|
||||
) {
|
||||
this.detailResponse = detailResponse;
|
||||
this.participantsResponse = participantsResponse;
|
||||
this.participantsPayload = participantsPayload;
|
||||
this.threadStatusResponse = threadStatusResponse;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import android.widget.TextView;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -70,9 +71,10 @@ public class GroupCreateActivity extends BossScreenActivity {
|
||||
runOnUiThread(() -> renderCreatePage(null, conversationsResponse.json, true));
|
||||
return;
|
||||
}
|
||||
BossApiClient.ApiResponse participantsResponse = apiClient.getConversationParticipants(sourceProjectId);
|
||||
if (!participantsResponse.ok()) throw new IllegalStateException(participantsResponse.message());
|
||||
runOnUiThread(() -> renderCreatePage(participantsResponse.json, conversationsResponse.json, true));
|
||||
BossApiClient.ApiResponse detailResponse = apiClient.getProjectDetail(sourceProjectId);
|
||||
if (!detailResponse.ok()) throw new IllegalStateException(detailResponse.message());
|
||||
JSONObject participantsPayload = extractParticipantsPayload(detailResponse.json, sourceProjectId);
|
||||
runOnUiThread(() -> renderCreatePage(participantsPayload, conversationsResponse.json, true));
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
setRefreshing(false);
|
||||
@@ -155,6 +157,23 @@ public class GroupCreateActivity extends BossScreenActivity {
|
||||
updateCreateButtonState();
|
||||
}
|
||||
|
||||
private JSONObject extractParticipantsPayload(JSONObject detailPayload, String fallbackProjectId) {
|
||||
JSONObject participantsPayload = detailPayload == null ? null : detailPayload.optJSONObject("participantsPayload");
|
||||
if (participantsPayload != null) {
|
||||
return participantsPayload;
|
||||
}
|
||||
JSONObject fallback = new JSONObject();
|
||||
JSONObject project = detailPayload == null ? null : detailPayload.optJSONObject("project");
|
||||
JSONObject threadMeta = project == null ? null : project.optJSONObject("threadMeta");
|
||||
try {
|
||||
fallback.put("projectId", fallbackProjectId == null ? "" : fallbackProjectId);
|
||||
fallback.put("threadMeta", threadMeta == null ? new JSONObject() : threadMeta);
|
||||
fallback.put("participants", new JSONArray());
|
||||
} catch (JSONException ignored) {
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
private View buildHeaderView(
|
||||
boolean hasSourceProject,
|
||||
@Nullable String sourceProjectId,
|
||||
|
||||
@@ -22,6 +22,7 @@ public class GroupInfoActivity extends BossScreenActivity {
|
||||
|
||||
private String projectId;
|
||||
private String projectName;
|
||||
private boolean groupRepairJustApplied;
|
||||
private @Nullable BossRealtimeClient realtimeClient;
|
||||
private final Map<String, Long> recentRealtimeEventTimestamps = new LinkedHashMap<>();
|
||||
|
||||
@@ -72,13 +73,12 @@ public class GroupInfoActivity extends BossScreenActivity {
|
||||
try {
|
||||
BossApiClient.ApiResponse detailResponse = apiClient.getProjectDetail(projectId);
|
||||
if (!detailResponse.ok()) throw new IllegalStateException(detailResponse.message());
|
||||
BossApiClient.ApiResponse participantsResponse = apiClient.getConversationParticipants(projectId);
|
||||
if (!participantsResponse.ok()) throw new IllegalStateException(participantsResponse.message());
|
||||
JSONObject participantsPayload = extractParticipantsPayload(detailResponse.json);
|
||||
BossApiClient.ApiResponse orchestrationResponse = apiClient.getProjectOrchestrationBackend(projectId);
|
||||
JSONObject orchestrationBackend = orchestrationResponse.ok()
|
||||
? orchestrationResponse.json
|
||||
: buildFallbackOrchestrationBackendPayload(orchestrationResponse.message());
|
||||
runOnUiThread(() -> renderGroup(detailResponse.json, participantsResponse.json, orchestrationBackend));
|
||||
runOnUiThread(() -> renderGroup(detailResponse.json, participantsPayload, orchestrationBackend));
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
setRefreshing(false);
|
||||
@@ -149,6 +149,11 @@ public class GroupInfoActivity extends BossScreenActivity {
|
||||
}
|
||||
}
|
||||
|
||||
private JSONObject extractParticipantsPayload(JSONObject detailPayload) {
|
||||
JSONObject participantsPayload = detailPayload == null ? null : detailPayload.optJSONObject("participantsPayload");
|
||||
return participantsPayload == null ? new JSONObject() : participantsPayload;
|
||||
}
|
||||
|
||||
private void renderGroup(JSONObject detail, JSONObject participantsPayload) {
|
||||
renderGroup(detail, participantsPayload, null);
|
||||
}
|
||||
@@ -174,6 +179,11 @@ public class GroupInfoActivity extends BossScreenActivity {
|
||||
int invalidParticipantCount = participantsPayload.optInt("invalidParticipantCount", 0);
|
||||
configureScreen("群资料", buildSubtitle(folderName, participantCount));
|
||||
|
||||
if (groupRepairJustApplied) {
|
||||
appendContent(BossUi.buildEmptyCard(this, "群成员已更新,当前群聊已经切换到新的真实线程成员。"));
|
||||
groupRepairJustApplied = false;
|
||||
}
|
||||
|
||||
appendContent(BossUi.buildSimpleProfileHeader(
|
||||
this,
|
||||
projectName,
|
||||
@@ -367,6 +377,7 @@ public class GroupInfoActivity extends BossScreenActivity {
|
||||
BossApiClient.ApiResponse response = apiClient.replaceConversationParticipants(projectId, memberProjectIds);
|
||||
if (!response.ok()) throw new IllegalStateException(response.message());
|
||||
runOnUiThread(() -> {
|
||||
groupRepairJustApplied = true;
|
||||
showMessage("群成员已更新");
|
||||
reload();
|
||||
});
|
||||
|
||||
@@ -62,10 +62,26 @@ public final class ProjectChatUiState {
|
||||
public static final class ReplyWaitSpec {
|
||||
public final boolean shouldWait;
|
||||
public final String baselineMessageId;
|
||||
public final List<String> executionIds;
|
||||
|
||||
private ReplyWaitSpec(boolean shouldWait, @Nullable String baselineMessageId) {
|
||||
this.shouldWait = shouldWait && !isBlank(baselineMessageId);
|
||||
this.baselineMessageId = this.shouldWait ? baselineMessageId.trim() : "";
|
||||
private ReplyWaitSpec(
|
||||
boolean shouldWait,
|
||||
@Nullable String baselineMessageId,
|
||||
@Nullable List<String> executionIds
|
||||
) {
|
||||
ArrayList<String> normalizedExecutionIds = new ArrayList<>();
|
||||
if (executionIds != null) {
|
||||
for (String executionId : executionIds) {
|
||||
if (!isBlank(executionId)) {
|
||||
normalizedExecutionIds.add(executionId.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
boolean hasBaseline = !isBlank(baselineMessageId);
|
||||
boolean hasExecutionIds = !normalizedExecutionIds.isEmpty();
|
||||
this.shouldWait = shouldWait && (hasBaseline || hasExecutionIds);
|
||||
this.baselineMessageId = hasBaseline ? baselineMessageId.trim() : "";
|
||||
this.executionIds = Collections.unmodifiableList(normalizedExecutionIds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -410,30 +426,81 @@ public final class ProjectChatUiState {
|
||||
|
||||
public static ReplyWaitSpec resolveReplyWaitAfterSend(@Nullable JSONObject response) {
|
||||
if (response == null) {
|
||||
return new ReplyWaitSpec(false, null);
|
||||
return new ReplyWaitSpec(false, null, null);
|
||||
}
|
||||
JSONObject task = response.optJSONObject("task");
|
||||
if (task == null) {
|
||||
return new ReplyWaitSpec(false, null);
|
||||
return new ReplyWaitSpec(false, null, null);
|
||||
}
|
||||
String taskStatus = task.optString("status", "");
|
||||
if ("completed".equals(taskStatus) || "failed".equals(taskStatus)) {
|
||||
return new ReplyWaitSpec(false, null);
|
||||
return new ReplyWaitSpec(false, null, null);
|
||||
}
|
||||
JSONObject message = response.optJSONObject("message");
|
||||
return new ReplyWaitSpec(true, message == null ? null : message.optString("id", ""));
|
||||
return new ReplyWaitSpec(true, message == null ? null : message.optString("id", ""), null);
|
||||
}
|
||||
|
||||
public static ReplyWaitSpec resolveReplyWaitAfterDispatchConfirm(@Nullable JSONObject response) {
|
||||
if (response == null) {
|
||||
return new ReplyWaitSpec(false, null);
|
||||
return new ReplyWaitSpec(false, null, null);
|
||||
}
|
||||
JSONArray executions = response.optJSONArray("executions");
|
||||
if (executions == null || executions.length() == 0) {
|
||||
return new ReplyWaitSpec(false, null);
|
||||
return new ReplyWaitSpec(false, null, null);
|
||||
}
|
||||
JSONObject notice = response.optJSONObject("notice");
|
||||
return new ReplyWaitSpec(true, notice == null ? null : notice.optString("id", ""));
|
||||
return new ReplyWaitSpec(
|
||||
true,
|
||||
notice == null ? null : notice.optString("id", ""),
|
||||
collectExecutionIds(executions)
|
||||
);
|
||||
}
|
||||
|
||||
public static ReplyWaitSpec replyWaitFromBaseline(@Nullable String baselineMessageId) {
|
||||
return new ReplyWaitSpec(true, baselineMessageId, null);
|
||||
}
|
||||
|
||||
public static boolean hasTrackedDispatchExecutionReply(
|
||||
@Nullable JSONArray dispatchPlans,
|
||||
@Nullable List<String> executionIds
|
||||
) {
|
||||
if (dispatchPlans == null || executionIds == null || executionIds.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
LinkedHashSet<String> trackedIds = new LinkedHashSet<>();
|
||||
for (String executionId : executionIds) {
|
||||
if (!isBlank(executionId)) {
|
||||
trackedIds.add(executionId.trim());
|
||||
}
|
||||
}
|
||||
if (trackedIds.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < dispatchPlans.length(); i++) {
|
||||
JSONObject plan = dispatchPlans.optJSONObject(i);
|
||||
if (plan == null) {
|
||||
continue;
|
||||
}
|
||||
JSONArray executions = plan.optJSONArray("executions");
|
||||
if (executions == null) {
|
||||
continue;
|
||||
}
|
||||
for (int j = 0; j < executions.length(); j++) {
|
||||
JSONObject execution = executions.optJSONObject(j);
|
||||
if (execution == null) {
|
||||
continue;
|
||||
}
|
||||
String executionId = execution.optString("executionId", "").trim();
|
||||
if (!trackedIds.contains(executionId)) {
|
||||
continue;
|
||||
}
|
||||
String status = execution.optString("status", "").trim();
|
||||
if ("completed".equals(status) || "failed".equals(status)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean hasReplyBeyondBaseline(@Nullable JSONObject project, @Nullable String baselineMessageId) {
|
||||
@@ -457,6 +524,24 @@ public final class ProjectChatUiState {
|
||||
return messageId.isEmpty() ? null : messageId;
|
||||
}
|
||||
|
||||
private static List<String> collectExecutionIds(@Nullable JSONArray executions) {
|
||||
ArrayList<String> executionIds = new ArrayList<>();
|
||||
if (executions == null) {
|
||||
return executionIds;
|
||||
}
|
||||
for (int i = 0; i < executions.length(); i++) {
|
||||
JSONObject execution = executions.optJSONObject(i);
|
||||
if (execution == null) {
|
||||
continue;
|
||||
}
|
||||
String executionId = execution.optString("executionId", "").trim();
|
||||
if (!executionId.isEmpty()) {
|
||||
executionIds.add(executionId);
|
||||
}
|
||||
}
|
||||
return executionIds;
|
||||
}
|
||||
|
||||
private static boolean isBlank(@Nullable String value) {
|
||||
return value == null || value.trim().isEmpty();
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
@@ -55,6 +56,12 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
private String projectFolderName;
|
||||
private @Nullable String currentAgentModelOverride;
|
||||
private @Nullable String currentReasoningEffortOverride;
|
||||
private @Nullable String currentFastModelOverride;
|
||||
private @Nullable String currentFastReasoningEffortOverride;
|
||||
private @Nullable String currentSmartModelOverride;
|
||||
private @Nullable String currentSmartReasoningEffortOverride;
|
||||
private final List<String> currentAvailableMasterAgentModels = new ArrayList<>();
|
||||
private final List<String> currentSelectableMasterAgentModels = new ArrayList<>();
|
||||
private LinearLayout quickActionsLayout;
|
||||
private LinearLayout composerRow;
|
||||
private LinearLayout multiSelectActionsLayout;
|
||||
@@ -174,6 +181,25 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
}
|
||||
}
|
||||
|
||||
static @Nullable JSONArray extractDispatchPlans(JSONObject detailPayload) {
|
||||
return detailPayload == null ? null : detailPayload.optJSONArray("dispatchPlans");
|
||||
}
|
||||
|
||||
static @Nullable JSONObject extractParticipantsPayload(JSONObject detailPayload) {
|
||||
return detailPayload == null ? null : detailPayload.optJSONObject("participantsPayload");
|
||||
}
|
||||
|
||||
private String buildDisplayedProjectTitle(@Nullable String rawTitle) {
|
||||
String normalizedTitle = TextUtils.isEmpty(rawTitle) ? "项目详情" : rawTitle;
|
||||
if (!isMasterAgentConversation()) {
|
||||
return normalizedTitle;
|
||||
}
|
||||
String modelName = !TextUtils.isEmpty(currentAgentModelOverride)
|
||||
? currentAgentModelOverride
|
||||
: (!TextUtils.isEmpty(currentSmartModelOverride) ? currentSmartModelOverride : null);
|
||||
return TextUtils.isEmpty(modelName) ? "主Agent" : "主Agent·" + modelName;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getLayoutResId() {
|
||||
return R.layout.activity_project_chat;
|
||||
@@ -273,7 +299,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
BossWindowInsets.applyKeyboardAvoidingInset(composerRow);
|
||||
BossWindowInsets.applyKeyboardAvoidingInset(multiSelectActionsLayout);
|
||||
|
||||
updateProjectHeader(initialProjectName == null ? "项目详情" : initialProjectName, "正在同步项目详情...");
|
||||
updateProjectHeader(buildDisplayedProjectTitle(initialProjectName), "正在同步项目详情...");
|
||||
if (composerAttachmentButton != null) {
|
||||
composerAttachmentButton.setOnClickListener(v -> showAttachmentEntrySheet());
|
||||
}
|
||||
@@ -371,12 +397,19 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
if (projectMessagesPayload == null) {
|
||||
return false;
|
||||
}
|
||||
if (shouldBypassRealtimeMessagesPatchForGroupState()) {
|
||||
return false;
|
||||
}
|
||||
JSONArray executionWarnings = projectMessagesPayload.optJSONArray("executionWarnings");
|
||||
if (trySkipUnchangedRealtimeMessagesPatch(projectMessagesPayload)) {
|
||||
return true;
|
||||
}
|
||||
if (tryAppendRealtimeMessagesPatch(projectMessagesPayload)) {
|
||||
return true;
|
||||
}
|
||||
if (tryPatchRealtimeExecutionWarnings(projectMessagesPayload)) {
|
||||
return true;
|
||||
}
|
||||
runOnUiThread(() -> {
|
||||
if (reloadInFlight) {
|
||||
scheduleRealtimeReload(false);
|
||||
@@ -389,6 +422,16 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean shouldBypassRealtimeMessagesPatchForGroupState() {
|
||||
if (!projectIsGroup) {
|
||||
return false;
|
||||
}
|
||||
if (currentPendingDispatchPlan != null || currentRejectedDispatchPlan != null) {
|
||||
return true;
|
||||
}
|
||||
return currentParticipantsPayload != null && currentParticipantsPayload.optBoolean("repairRequired", false);
|
||||
}
|
||||
|
||||
private boolean trySkipUnchangedRealtimeMessagesPatch(JSONObject projectMessagesPayload) {
|
||||
if (currentRenderedProjectPayload == null || projectMessagesPayload == null) {
|
||||
return false;
|
||||
@@ -412,6 +455,12 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
if (!TextUtils.equals(currentMessages.toString(), nextMessages.toString())) {
|
||||
return false;
|
||||
}
|
||||
if (!hasMatchingExecutionWarnings(currentRenderedProjectPayload, projectMessagesPayload)) {
|
||||
return false;
|
||||
}
|
||||
if (!hasMatchingConversationTasks(currentRenderedProjectPayload, projectMessagesPayload)) {
|
||||
return false;
|
||||
}
|
||||
currentRenderedProjectPayload = copyJson(projectMessagesPayload);
|
||||
return true;
|
||||
}
|
||||
@@ -435,6 +484,12 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
if (currentMessages == null || nextMessages == null) {
|
||||
return false;
|
||||
}
|
||||
if (!hasMatchingExecutionWarnings(currentRenderedProjectPayload, projectMessagesPayload)) {
|
||||
return false;
|
||||
}
|
||||
if (!hasMatchingConversationTasks(currentRenderedProjectPayload, projectMessagesPayload)) {
|
||||
return false;
|
||||
}
|
||||
List<String> currentIds = collectMessageIds(currentMessages);
|
||||
List<String> nextIds = collectMessageIds(nextMessages);
|
||||
if (currentIds.isEmpty() || nextIds.size() <= currentIds.size()) {
|
||||
@@ -460,7 +515,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
projectCollaborationMode = project == null ? "development" : project.optString("collaborationMode", projectCollaborationMode);
|
||||
projectApprovalState = project == null ? "not_required" : project.optString("approvalState", projectApprovalState);
|
||||
lightDispatchReminderEnabled = project != null && project.optBoolean("lightDispatchReminderEnabled", lightDispatchReminderEnabled);
|
||||
updateProjectHeader(title, buildProjectSubtitle(projectFolderName, devices));
|
||||
updateProjectHeader(buildDisplayedProjectTitle(title), buildProjectSubtitle(projectFolderName, devices));
|
||||
|
||||
selectionState = ProjectChatUiState.reconcileSelection(selectionState, nextIds);
|
||||
renderNearBottom = isChatNearBottom();
|
||||
@@ -483,6 +538,112 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean tryPatchRealtimeExecutionWarnings(JSONObject projectMessagesPayload) {
|
||||
if (currentRenderedProjectPayload == null
|
||||
|| contentLayout == null
|
||||
|| pendingOutgoingBubble != null
|
||||
|| masterAgentReplyWaiting
|
||||
|| masterAgentReplyTimedOut
|
||||
|| (selectionState != null && selectionState.multiSelecting)) {
|
||||
return false;
|
||||
}
|
||||
JSONObject currentProject = currentRenderedProjectPayload.optJSONObject("project");
|
||||
JSONObject nextProject = projectMessagesPayload.optJSONObject("project");
|
||||
if (currentProject == null || nextProject == null) {
|
||||
return false;
|
||||
}
|
||||
JSONArray currentMessages = currentProject.optJSONArray("messages");
|
||||
JSONArray nextMessages = nextProject.optJSONArray("messages");
|
||||
if (currentMessages == null || nextMessages == null) {
|
||||
return false;
|
||||
}
|
||||
if (!TextUtils.equals(currentMessages.toString(), nextMessages.toString())) {
|
||||
return false;
|
||||
}
|
||||
if (hasMatchingExecutionWarnings(currentRenderedProjectPayload, projectMessagesPayload)
|
||||
&& hasMatchingConversationTasks(currentRenderedProjectPayload, projectMessagesPayload)) {
|
||||
return false;
|
||||
}
|
||||
JSONObject nextPayloadCopy = copyJson(projectMessagesPayload);
|
||||
runOnUiThread(() -> {
|
||||
if (currentRenderedProjectPayload == null || contentLayout == null) {
|
||||
renderLoadedProjectSnapshot(new ProjectSnapshot(projectMessagesPayload, null, null));
|
||||
return;
|
||||
}
|
||||
JSONArray refreshedMessages = nextProject.optJSONArray("messages");
|
||||
if (refreshedMessages == null) {
|
||||
renderLoadedProjectSnapshot(new ProjectSnapshot(projectMessagesPayload, null, null));
|
||||
return;
|
||||
}
|
||||
renderNearBottom = isChatNearBottom();
|
||||
runWithSuppressedContentLayout(() -> {
|
||||
for (int i = 0; i < refreshedMessages.length(); i++) {
|
||||
JSONObject message = refreshedMessages.optJSONObject(i);
|
||||
if (message == null) {
|
||||
continue;
|
||||
}
|
||||
String messageId = message.optString("id", "");
|
||||
if (TextUtils.isEmpty(messageId)) {
|
||||
continue;
|
||||
}
|
||||
String currentFingerprint = buildStatusFingerprint(messageId, currentRenderedProjectPayload);
|
||||
String nextFingerprint = buildStatusFingerprint(messageId, projectMessagesPayload);
|
||||
if (TextUtils.isEmpty(currentFingerprint) && TextUtils.isEmpty(nextFingerprint)) {
|
||||
continue;
|
||||
}
|
||||
if (!TextUtils.equals(currentFingerprint, nextFingerprint)) {
|
||||
replaceMessageViewById(messageId, buildMessageView(message));
|
||||
}
|
||||
}
|
||||
});
|
||||
currentRenderedProjectPayload = nextPayloadCopy;
|
||||
setRefreshing(false);
|
||||
updateSelectionUi();
|
||||
if (ProjectChatUiState.shouldAutoScroll(renderNearBottom, false)) {
|
||||
scrollChatToBottom();
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean hasMatchingExecutionWarnings(JSONObject currentPayload, JSONObject nextPayload) {
|
||||
if (currentPayload == null || nextPayload == null) {
|
||||
return false;
|
||||
}
|
||||
JSONArray currentWarnings = currentPayload.optJSONArray("executionWarnings");
|
||||
JSONArray nextWarnings = nextPayload.optJSONArray("executionWarnings");
|
||||
String currentSerialized = currentWarnings == null ? "[]" : currentWarnings.toString();
|
||||
String nextSerialized = nextWarnings == null ? "[]" : nextWarnings.toString();
|
||||
return TextUtils.equals(currentSerialized, nextSerialized);
|
||||
}
|
||||
|
||||
private boolean hasMatchingConversationTasks(JSONObject currentPayload, JSONObject nextPayload) {
|
||||
if (currentPayload == null || nextPayload == null) {
|
||||
return false;
|
||||
}
|
||||
JSONArray currentTasks = currentPayload.optJSONArray("conversationTasks");
|
||||
JSONArray nextTasks = nextPayload.optJSONArray("conversationTasks");
|
||||
String currentSerialized = currentTasks == null ? "[]" : currentTasks.toString();
|
||||
String nextSerialized = nextTasks == null ? "[]" : nextTasks.toString();
|
||||
return TextUtils.equals(currentSerialized, nextSerialized);
|
||||
}
|
||||
|
||||
private void replaceMessageViewById(String messageId, View nextMessageView) {
|
||||
if (TextUtils.isEmpty(messageId) || nextMessageView == null || contentLayout == null) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < contentLayout.getChildCount(); i++) {
|
||||
View child = contentLayout.getChildAt(i);
|
||||
Object tag = child.getTag();
|
||||
if (!(tag instanceof String) || !TextUtils.equals(messageId, (String) tag)) {
|
||||
continue;
|
||||
}
|
||||
contentLayout.removeViewAt(i);
|
||||
contentLayout.addView(nextMessageView, i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isDuplicateRealtimeEvent(String eventFingerprint, long now) {
|
||||
pruneRecentRealtimeEvents(now);
|
||||
Long previousEventAt = recentRealtimeEventTimestamps.get(eventFingerprint);
|
||||
@@ -654,6 +815,10 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
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));
|
||||
currentFastModelOverride = normalizeControlValue(agentControls == null ? null : agentControls.optString("fastModelOverride", null));
|
||||
currentFastReasoningEffortOverride = normalizeControlValue(agentControls == null ? null : agentControls.optString("fastReasoningEffortOverride", null));
|
||||
currentSmartModelOverride = normalizeControlValue(agentControls == null ? null : agentControls.optString("smartModelOverride", null));
|
||||
currentSmartReasoningEffortOverride = normalizeControlValue(agentControls == null ? null : agentControls.optString("smartReasoningEffortOverride", null));
|
||||
if (dispatchPlans != null) {
|
||||
currentPendingDispatchPlan = ProjectChatUiState.latestPendingDispatchPlan(dispatchPlans);
|
||||
currentRejectedDispatchPlan = currentPendingDispatchPlan == null
|
||||
@@ -667,7 +832,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
? currentParticipantsPayload
|
||||
: participantsPayload;
|
||||
conversationInfoReady = project != null;
|
||||
updateProjectHeader(title, buildProjectSubtitle(projectFolderName, devices));
|
||||
updateProjectHeader(buildDisplayedProjectTitle(title), buildProjectSubtitle(projectFolderName, devices));
|
||||
|
||||
renderQuickActions();
|
||||
JSONArray messages = project == null ? null : project.optJSONArray("messages");
|
||||
@@ -1251,40 +1416,116 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
if (!isMasterAgentConversation()) {
|
||||
return;
|
||||
}
|
||||
final String[] options = buildMasterAgentModelOptions();
|
||||
int checkedIndex = findCheckedIndex(options, currentAgentModelOverride);
|
||||
setRefreshing(true);
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
BossApiClient.ApiResponse response = apiClient.getProjectAgentControls(projectId);
|
||||
if (!response.ok()) {
|
||||
throw new IllegalStateException(response.message());
|
||||
}
|
||||
JSONObject payload = response.json;
|
||||
JSONObject controls = payload.optJSONObject("controls");
|
||||
JSONObject modelCatalog = payload.optJSONObject("modelCatalog");
|
||||
runOnUiThread(() -> {
|
||||
applyMasterAgentControlsFromJson(controls);
|
||||
applyMasterAgentModelCatalogFromJson(modelCatalog);
|
||||
setRefreshing(false);
|
||||
showMasterAgentModelScopeMenu();
|
||||
});
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
setRefreshing(false);
|
||||
showMessage("模型信息加载失败:" + error.getMessage());
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void showMasterAgentModelScopeMenu() {
|
||||
String availableLabel = currentAvailableMasterAgentModels.isEmpty()
|
||||
? "未检测到已就绪账号,可直接使用预设模型"
|
||||
: "当前可用:" + TextUtils.join("、", currentAvailableMasterAgentModels);
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("模型")
|
||||
.setSingleChoiceItems(options, checkedIndex, (dialog, which) -> {
|
||||
.setItems(new CharSequence[]{
|
||||
availableLabel,
|
||||
buildMasterAgentModelScopeSummary("当前主模型", currentAgentModelOverride),
|
||||
buildMasterAgentModelScopeSummary("快模型", currentFastModelOverride),
|
||||
buildMasterAgentModelScopeSummary("强模型", currentSmartModelOverride)
|
||||
}, (dialog, which) -> {
|
||||
if (which == 0) {
|
||||
dialog.dismiss();
|
||||
updateMasterAgentControls(null, currentReasoningEffortOverride, "模型已恢复默认");
|
||||
showMasterAgentAvailableModelsDialog();
|
||||
return;
|
||||
}
|
||||
if (which == options.length - 1) {
|
||||
if (which == 1) {
|
||||
dialog.dismiss();
|
||||
showCustomMasterAgentModelDialog();
|
||||
showMasterAgentModelValuePicker("当前主模型", currentAgentModelOverride, "manual_override");
|
||||
return;
|
||||
}
|
||||
dialog.dismiss();
|
||||
updateMasterAgentControls(options[which], currentReasoningEffortOverride, "模型已更新为 " + options[which]);
|
||||
if (which == 2) {
|
||||
dialog.dismiss();
|
||||
showMasterAgentModelValuePicker("快模型", currentFastModelOverride, "fast");
|
||||
return;
|
||||
}
|
||||
if (which == 3) {
|
||||
dialog.dismiss();
|
||||
showMasterAgentModelValuePicker("强模型", currentSmartModelOverride, "smart");
|
||||
}
|
||||
})
|
||||
.setNegativeButton("取消", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showCustomMasterAgentModelDialog() {
|
||||
final EditText input = BossUi.buildInput(this, "模型,例如 gpt-5.4", false);
|
||||
input.setText(TextUtils.isEmpty(currentAgentModelOverride) ? "gpt-5.4" : currentAgentModelOverride);
|
||||
private void showMasterAgentAvailableModelsDialog() {
|
||||
String message = currentAvailableMasterAgentModels.isEmpty()
|
||||
? "当前没有检测到已就绪账号,你仍然可以直接选择预设模型,或者用自定义模型手动填写。"
|
||||
: "当前可用模型:\n" + TextUtils.join("\n", currentAvailableMasterAgentModels);
|
||||
String selectable = currentSelectableMasterAgentModels.isEmpty()
|
||||
? ""
|
||||
: "\n\n可选模型:\n" + TextUtils.join("\n", currentSelectableMasterAgentModels);
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("自定义模型")
|
||||
.setTitle("可用模型")
|
||||
.setMessage(message + selectable)
|
||||
.setPositiveButton("知道了", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showMasterAgentModelValuePicker(String scopeTitle, @Nullable String currentValue, String scopeKey) {
|
||||
final String[] options = buildMasterAgentModelOptions(currentValue);
|
||||
int checkedIndex = findCheckedIndex(options, currentValue);
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(scopeTitle)
|
||||
.setSingleChoiceItems(options, checkedIndex, (dialog, which) -> {
|
||||
if (which == 0) {
|
||||
dialog.dismiss();
|
||||
updateMasterAgentControlsForScope(scopeKey, null, scopeTitle + "已恢复默认");
|
||||
return;
|
||||
}
|
||||
if (which == options.length - 1) {
|
||||
dialog.dismiss();
|
||||
showCustomMasterAgentModelDialog(scopeTitle, scopeKey, currentValue);
|
||||
return;
|
||||
}
|
||||
dialog.dismiss();
|
||||
updateMasterAgentControlsForScope(scopeKey, options[which], scopeTitle + "已更新为 " + options[which]);
|
||||
})
|
||||
.setNegativeButton("取消", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showCustomMasterAgentModelDialog(String scopeTitle, String scopeKey, @Nullable String currentValue) {
|
||||
final EditText input = BossUi.buildInput(this, "模型,例如 gpt-5.4", false);
|
||||
input.setText(TextUtils.isEmpty(currentValue) ? "gpt-5.4" : currentValue);
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(scopeTitle + " · 自定义模型")
|
||||
.setView(input)
|
||||
.setNegativeButton("取消", null)
|
||||
.setPositiveButton("保存", (dialog, which) ->
|
||||
updateMasterAgentControls(
|
||||
updateMasterAgentControlsForScope(
|
||||
scopeKey,
|
||||
normalizeControlValue(input.getText() == null ? null : input.getText().toString()),
|
||||
currentReasoningEffortOverride,
|
||||
"模型已更新"
|
||||
scopeTitle + "已更新"
|
||||
))
|
||||
.show();
|
||||
}
|
||||
@@ -1303,6 +1544,10 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
updateMasterAgentControls(
|
||||
currentAgentModelOverride,
|
||||
reasoningOverride,
|
||||
currentFastModelOverride,
|
||||
currentFastReasoningEffortOverride,
|
||||
currentSmartModelOverride,
|
||||
currentSmartReasoningEffortOverride,
|
||||
which == 0 ? "推理强度已恢复默认" : "推理强度已更新为 " + options[which]
|
||||
);
|
||||
})
|
||||
@@ -1313,6 +1558,10 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
private void updateMasterAgentControls(
|
||||
@Nullable String modelOverride,
|
||||
@Nullable String reasoningEffortOverride,
|
||||
@Nullable String fastModelOverride,
|
||||
@Nullable String fastReasoningEffortOverride,
|
||||
@Nullable String smartModelOverride,
|
||||
@Nullable String smartReasoningEffortOverride,
|
||||
String successMessage
|
||||
) {
|
||||
if (!isMasterAgentConversation() || projectId == null || projectId.isEmpty()) {
|
||||
@@ -1324,15 +1573,19 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
BossApiClient.ApiResponse response = apiClient.updateProjectAgentControls(
|
||||
projectId,
|
||||
modelOverride,
|
||||
reasoningEffortOverride
|
||||
reasoningEffortOverride,
|
||||
fastModelOverride,
|
||||
fastReasoningEffortOverride,
|
||||
smartModelOverride,
|
||||
smartReasoningEffortOverride,
|
||||
true
|
||||
);
|
||||
if (!response.ok()) {
|
||||
throw new IllegalStateException(response.message());
|
||||
}
|
||||
JSONObject controls = response.json.optJSONObject("controls");
|
||||
runOnUiThread(() -> {
|
||||
currentAgentModelOverride = normalizeControlValue(controls == null ? null : controls.optString("modelOverride", null));
|
||||
currentReasoningEffortOverride = normalizeControlValue(controls == null ? null : controls.optString("reasoningEffortOverride", null));
|
||||
applyMasterAgentControlsFromJson(controls);
|
||||
showMessage(successMessage);
|
||||
reload(true);
|
||||
});
|
||||
@@ -1345,21 +1598,86 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
});
|
||||
}
|
||||
|
||||
private String[] buildMasterAgentModelOptions() {
|
||||
private void updateMasterAgentControlsForScope(String scopeKey, @Nullable String modelOverride, String successMessage) {
|
||||
String manualModelOverride = currentAgentModelOverride;
|
||||
String fastModelOverride = currentFastModelOverride;
|
||||
String smartModelOverride = currentSmartModelOverride;
|
||||
if ("fast".equals(scopeKey)) {
|
||||
fastModelOverride = modelOverride;
|
||||
} else if ("smart".equals(scopeKey)) {
|
||||
smartModelOverride = modelOverride;
|
||||
} else {
|
||||
manualModelOverride = modelOverride;
|
||||
}
|
||||
updateMasterAgentControls(
|
||||
manualModelOverride,
|
||||
currentReasoningEffortOverride,
|
||||
fastModelOverride,
|
||||
currentFastReasoningEffortOverride,
|
||||
smartModelOverride,
|
||||
currentSmartReasoningEffortOverride,
|
||||
successMessage
|
||||
);
|
||||
}
|
||||
|
||||
private void applyMasterAgentControlsFromJson(@Nullable JSONObject controls) {
|
||||
currentAgentModelOverride = normalizeControlValue(controls == null ? null : controls.optString("modelOverride", null));
|
||||
currentReasoningEffortOverride = normalizeControlValue(controls == null ? null : controls.optString("reasoningEffortOverride", null));
|
||||
currentFastModelOverride = normalizeControlValue(controls == null ? null : controls.optString("fastModelOverride", null));
|
||||
currentFastReasoningEffortOverride = normalizeControlValue(controls == null ? null : controls.optString("fastReasoningEffortOverride", null));
|
||||
currentSmartModelOverride = normalizeControlValue(controls == null ? null : controls.optString("smartModelOverride", null));
|
||||
currentSmartReasoningEffortOverride = normalizeControlValue(controls == null ? null : controls.optString("smartReasoningEffortOverride", null));
|
||||
if (isMasterAgentConversation()) {
|
||||
updateProjectHeader(buildDisplayedProjectTitle(initialProjectName), currentScreenSubtitle);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyMasterAgentModelCatalogFromJson(@Nullable JSONObject modelCatalog) {
|
||||
currentAvailableMasterAgentModels.clear();
|
||||
currentSelectableMasterAgentModels.clear();
|
||||
addJsonArrayStrings(modelCatalog == null ? null : modelCatalog.optJSONArray("availableModels"), currentAvailableMasterAgentModels);
|
||||
addJsonArrayStrings(modelCatalog == null ? null : modelCatalog.optJSONArray("selectableModels"), currentSelectableMasterAgentModels);
|
||||
}
|
||||
|
||||
private void addJsonArrayStrings(@Nullable JSONArray array, List<String> output) {
|
||||
if (array == null || output == null) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < array.length(); i++) {
|
||||
String value = normalizeControlValue(array.optString(i, null));
|
||||
if (!TextUtils.isEmpty(value) && !output.contains(value)) {
|
||||
output.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String buildMasterAgentModelScopeSummary(String label, @Nullable String value) {
|
||||
return label + ":" + (TextUtils.isEmpty(value) ? "默认" : value);
|
||||
}
|
||||
|
||||
private String[] buildMasterAgentModelOptions(@Nullable String currentValue) {
|
||||
List<String> options = new ArrayList<>();
|
||||
options.add("沿用默认");
|
||||
if (!TextUtils.isEmpty(currentAgentModelOverride)) {
|
||||
options.add(currentAgentModelOverride);
|
||||
if (!TextUtils.isEmpty(currentValue) && !options.contains(currentValue)) {
|
||||
options.add(currentValue);
|
||||
}
|
||||
for (String value : currentSelectableMasterAgentModels) {
|
||||
if (!TextUtils.isEmpty(value) && !options.contains(value)) {
|
||||
options.add(value);
|
||||
}
|
||||
}
|
||||
if (!options.contains("gpt-5.4")) {
|
||||
options.add("gpt-5.4");
|
||||
}
|
||||
if (!options.contains("gpt-5.1")) {
|
||||
options.add("gpt-5.1");
|
||||
if (!options.contains("gpt-5.4-mini")) {
|
||||
options.add("gpt-5.4-mini");
|
||||
}
|
||||
if (!options.contains("gpt-4.1")) {
|
||||
options.add("gpt-4.1");
|
||||
}
|
||||
if (!options.contains("gpt-4.1-mini")) {
|
||||
options.add("gpt-4.1-mini");
|
||||
}
|
||||
options.add("自定义...");
|
||||
return options.toArray(new String[0]);
|
||||
}
|
||||
@@ -1427,8 +1745,11 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
private View buildRepairGroupMembersView(JSONObject participantsPayload) {
|
||||
String repairReason = participantsPayload.optString("repairReason", "当前群聊里有失效线程,请先修复群成员。");
|
||||
int invalidParticipantCount = participantsPayload.optInt("invalidParticipantCount", 0);
|
||||
int validParticipantCount = participantsPayload.optInt("validParticipantCount", 0);
|
||||
String meta = invalidParticipantCount > 0
|
||||
? "存在 " + invalidParticipantCount + " 个失效成员"
|
||||
: validParticipantCount > 0
|
||||
? "当前仅有 " + validParticipantCount + " 个真实线程成员"
|
||||
: "当前群聊还没有可下发的真实线程";
|
||||
LinearLayout container = new LinearLayout(this);
|
||||
container.setOrientation(LinearLayout.VERTICAL);
|
||||
@@ -1612,6 +1933,15 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
);
|
||||
break;
|
||||
}
|
||||
JSONObject conversationTask = findConversationTask(currentRenderedProjectPayload, messageId);
|
||||
if (messageView instanceof LinearLayout) {
|
||||
LinearLayout wrapper = (LinearLayout) messageView;
|
||||
List<JSONObject> messageWarnings = buildMessageWarnings(currentRenderedProjectPayload, messageId);
|
||||
LinearLayout statusRow = BossUi.buildMessageStatusRow(this, message, conversationTask, messageWarnings, outgoing);
|
||||
if (statusRow.getVisibility() != View.GONE) {
|
||||
wrapper.addView(statusRow);
|
||||
}
|
||||
}
|
||||
bindMessageInteractions(messageView, messageId, body, messagePrimaryClick);
|
||||
return messageView;
|
||||
}
|
||||
@@ -2491,26 +2821,12 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
if (!detailResponse.ok()) {
|
||||
throw new IllegalStateException(detailResponse.message());
|
||||
}
|
||||
JSONArray dispatchPlans = null;
|
||||
JSONObject participantsPayload = null;
|
||||
if (includeDispatchPlans) {
|
||||
try {
|
||||
BossApiClient.ApiResponse dispatchPlansResponse = apiClient.getDispatchPlans(projectId);
|
||||
if (dispatchPlansResponse.ok()) {
|
||||
dispatchPlans = dispatchPlansResponse.json.optJSONArray("plans");
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
dispatchPlans = null;
|
||||
}
|
||||
try {
|
||||
BossApiClient.ApiResponse participantsResponse = apiClient.getConversationParticipants(projectId);
|
||||
if (participantsResponse.ok()) {
|
||||
participantsPayload = participantsResponse.json;
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
participantsPayload = null;
|
||||
}
|
||||
}
|
||||
JSONArray dispatchPlans = includeDispatchPlans
|
||||
? detailResponse.json.optJSONArray("dispatchPlans")
|
||||
: null;
|
||||
JSONObject participantsPayload = includeDispatchPlans
|
||||
? detailResponse.json.optJSONObject("participantsPayload")
|
||||
: null;
|
||||
return new ProjectSnapshot(detailResponse.json, dispatchPlans, participantsPayload);
|
||||
}
|
||||
|
||||
@@ -2535,7 +2851,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
updateComposerSendButtonState();
|
||||
setRefreshing(true);
|
||||
showMessage(waitingMessage);
|
||||
enqueueReplyWaitPoll(waitSpec.baselineMessageId, includeDispatchPlans);
|
||||
enqueueReplyWaitPoll(waitSpec, includeDispatchPlans);
|
||||
}
|
||||
|
||||
private void startMasterAgentReplyWait(
|
||||
@@ -2551,15 +2867,15 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
setRefreshing(false);
|
||||
showMessage(waitingMessage);
|
||||
reload(true);
|
||||
enqueueReplyWaitPoll(waitSpec.baselineMessageId, includeDispatchPlans);
|
||||
enqueueReplyWaitPoll(waitSpec, includeDispatchPlans);
|
||||
}
|
||||
|
||||
protected void enqueueReplyWaitPoll(@Nullable String baselineMessageId, boolean includeDispatchPlans) {
|
||||
replyWaitExecutor.execute(() -> pollUntilReply(baselineMessageId, includeDispatchPlans));
|
||||
protected void enqueueReplyWaitPoll(ProjectChatUiState.ReplyWaitSpec waitSpec, boolean includeDispatchPlans) {
|
||||
replyWaitExecutor.execute(() -> pollUntilReply(waitSpec, includeDispatchPlans));
|
||||
}
|
||||
|
||||
private void pollUntilReply(
|
||||
@Nullable String baselineMessageId,
|
||||
ProjectChatUiState.ReplyWaitSpec waitSpec,
|
||||
boolean includeDispatchPlans
|
||||
) {
|
||||
long deadlineAt = System.currentTimeMillis() + REPLY_WAIT_TIMEOUT_MS;
|
||||
@@ -2568,7 +2884,9 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
while (!Thread.currentThread().isInterrupted() && System.currentTimeMillis() < deadlineAt) {
|
||||
ProjectSnapshot snapshot = fetchProjectSnapshot(includeDispatchPlans);
|
||||
JSONObject project = snapshot.payload.optJSONObject("project");
|
||||
boolean hasReply = ProjectChatUiState.hasReplyBeyondBaseline(project, baselineMessageId);
|
||||
boolean hasReply = !waitSpec.executionIds.isEmpty()
|
||||
? ProjectChatUiState.hasTrackedDispatchExecutionReply(snapshot.dispatchPlans, waitSpec.executionIds)
|
||||
: ProjectChatUiState.hasReplyBeyondBaseline(project, waitSpec.baselineMessageId);
|
||||
|
||||
if (!renderedInitialSnapshot || hasReply) {
|
||||
runOnUiThread(() -> {
|
||||
@@ -2633,7 +2951,10 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
setRefreshing(false);
|
||||
showMessage("已重新开始等待主 Agent 回复");
|
||||
reload(true);
|
||||
enqueueReplyWaitPoll(masterAgentReplyBaselineMessageId, false);
|
||||
enqueueReplyWaitPoll(
|
||||
ProjectChatUiState.replyWaitFromBaseline(masterAgentReplyBaselineMessageId),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
static ChromeBindings buildChromeBindings(
|
||||
@@ -2718,6 +3039,148 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
return ids;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private JSONObject findExecutionWarningForMessage(@Nullable String messageId) {
|
||||
return findExecutionWarningForMessage(currentRenderedProjectPayload, messageId);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private JSONObject findExecutionWarningForMessage(@Nullable JSONObject payload, @Nullable String messageId) {
|
||||
if (TextUtils.isEmpty(messageId) || payload == null) {
|
||||
return null;
|
||||
}
|
||||
JSONArray warnings = payload.optJSONArray("executionWarnings");
|
||||
if (warnings == null) {
|
||||
return null;
|
||||
}
|
||||
for (int i = 0; i < warnings.length(); i++) {
|
||||
JSONObject warning = warnings.optJSONObject(i);
|
||||
if (warning == null) {
|
||||
continue;
|
||||
}
|
||||
if (TextUtils.equals(messageId, warning.optString("requestMessageId", ""))) {
|
||||
return warning;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean hasExecutionWarningForMessage(@Nullable JSONObject payload, @Nullable String messageId) {
|
||||
return findExecutionWarningForMessage(payload, messageId) != null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private JSONObject findConversationTask(@Nullable JSONObject payload, @Nullable String messageId) {
|
||||
if (TextUtils.isEmpty(messageId) || payload == null) {
|
||||
return null;
|
||||
}
|
||||
JSONArray tasks = payload.optJSONArray("conversationTasks");
|
||||
if (tasks == null) {
|
||||
return null;
|
||||
}
|
||||
for (int i = 0; i < tasks.length(); i++) {
|
||||
JSONObject task = tasks.optJSONObject(i);
|
||||
if (task == null) {
|
||||
continue;
|
||||
}
|
||||
if (TextUtils.equals(messageId, task.optString("requestMessageId", ""))) {
|
||||
return task;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<JSONObject> buildMessageWarnings(JSONObject payload, String messageId) {
|
||||
ArrayList<JSONObject> warnings = new ArrayList<>();
|
||||
if (payload == null || TextUtils.isEmpty(messageId)) {
|
||||
return warnings;
|
||||
}
|
||||
JSONArray rawWarnings = payload.optJSONArray("executionWarnings");
|
||||
if (rawWarnings == null) {
|
||||
return warnings;
|
||||
}
|
||||
LinkedHashMap<String, JSONObject> deduped = new LinkedHashMap<>();
|
||||
for (int i = 0; i < rawWarnings.length(); i++) {
|
||||
JSONObject warning = rawWarnings.optJSONObject(i);
|
||||
if (warning == null) {
|
||||
continue;
|
||||
}
|
||||
if (!TextUtils.equals(messageId, warning.optString("requestMessageId", ""))) {
|
||||
continue;
|
||||
}
|
||||
String key = warning.optString("title", "")
|
||||
+ "::"
|
||||
+ warning.optString("summary", "")
|
||||
+ "::"
|
||||
+ warning.optString("taskId", "")
|
||||
+ "::"
|
||||
+ warning.optString("sessionId", "");
|
||||
JSONObject existing = deduped.get(key);
|
||||
if (existing == null) {
|
||||
JSONObject grouped = copyJson(warning);
|
||||
try {
|
||||
grouped.put("count", 1);
|
||||
} catch (Exception ignored) {
|
||||
// Keep the original warning when count cannot be added.
|
||||
}
|
||||
deduped.put(key, grouped);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
existing.put("count", existing.optInt("count", 1) + 1);
|
||||
} catch (Exception ignored) {
|
||||
// Ignore invalid count updates and keep the original entry.
|
||||
}
|
||||
}
|
||||
warnings.addAll(deduped.values());
|
||||
return warnings;
|
||||
}
|
||||
|
||||
private String buildStatusFingerprint(String messageId, JSONObject payload) {
|
||||
JSONObject conversationTask = findConversationTask(payload, messageId);
|
||||
List<JSONObject> messageWarnings = buildMessageWarnings(payload, messageId);
|
||||
StringBuilder fingerprint = new StringBuilder();
|
||||
if (conversationTask != null) {
|
||||
fingerprint.append(conversationTask.optString("taskId", ""))
|
||||
.append('|')
|
||||
.append(conversationTask.optString("status", ""))
|
||||
.append('|')
|
||||
.append(conversationTask.optString("sessionId", ""))
|
||||
.append('|')
|
||||
.append(conversationTask.optString("requestId", ""));
|
||||
}
|
||||
fingerprint.append("::");
|
||||
for (JSONObject warning : messageWarnings) {
|
||||
if (warning == null) {
|
||||
continue;
|
||||
}
|
||||
fingerprint.append(warning.optString("title", ""))
|
||||
.append('|')
|
||||
.append(warning.optString("summary", ""))
|
||||
.append('|')
|
||||
.append(warning.optString("taskId", ""))
|
||||
.append('|')
|
||||
.append(warning.optString("sessionId", ""))
|
||||
.append('|')
|
||||
.append(warning.optInt("count", 1))
|
||||
.append("||");
|
||||
}
|
||||
return fingerprint.toString();
|
||||
}
|
||||
|
||||
private boolean hasSameExecutionWarningForMessage(
|
||||
@Nullable JSONObject currentPayload,
|
||||
@Nullable JSONObject nextPayload,
|
||||
@Nullable String messageId
|
||||
) {
|
||||
if (TextUtils.isEmpty(messageId)) {
|
||||
return true;
|
||||
}
|
||||
String currentFingerprint = buildStatusFingerprint(messageId, currentPayload);
|
||||
String nextFingerprint = buildStatusFingerprint(messageId, nextPayload);
|
||||
return TextUtils.equals(currentFingerprint, nextFingerprint);
|
||||
}
|
||||
|
||||
private JSONObject copyJson(@Nullable JSONObject source) {
|
||||
if (source == null) {
|
||||
return new JSONObject();
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.hyzq.boss;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(sdk = 34)
|
||||
public class BossApiClientTest {
|
||||
@Test
|
||||
public void buildProjectAgentControlsPayload_omitsAdvancedOverridesWhenAllAreNull() throws Exception {
|
||||
JSONObject payload = BossApiClient.buildProjectAgentControlsPayload(
|
||||
"gpt-5.4-mini",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
assertEquals("gpt-5.4-mini", payload.getString("modelOverride"));
|
||||
assertTrue(payload.has("reasoningEffortOverride"));
|
||||
assertFalse(payload.has("fastModelOverride"));
|
||||
assertFalse(payload.has("fastReasoningEffortOverride"));
|
||||
assertFalse(payload.has("smartModelOverride"));
|
||||
assertFalse(payload.has("smartReasoningEffortOverride"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildProjectAgentControlsPayload_includesAdvancedOverridesWhenPresent() throws Exception {
|
||||
JSONObject payload = BossApiClient.buildProjectAgentControlsPayload(
|
||||
null,
|
||||
null,
|
||||
"gpt-5.4-mini",
|
||||
null,
|
||||
"gpt-5.4",
|
||||
"high",
|
||||
false
|
||||
);
|
||||
|
||||
assertTrue(payload.has("fastModelOverride"));
|
||||
assertEquals("gpt-5.4-mini", payload.getString("fastModelOverride"));
|
||||
assertTrue(payload.has("smartModelOverride"));
|
||||
assertEquals("gpt-5.4", payload.getString("smartModelOverride"));
|
||||
assertTrue(payload.has("smartReasoningEffortOverride"));
|
||||
assertEquals("high", payload.getString("smartReasoningEffortOverride"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildProjectAgentControlsPayload_includesAdvancedNullsWhenExplicitlyRequested() throws Exception {
|
||||
JSONObject payload = BossApiClient.buildProjectAgentControlsPayload(
|
||||
"gpt-5.4-mini",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
true
|
||||
);
|
||||
|
||||
assertTrue(payload.has("fastModelOverride"));
|
||||
assertTrue(payload.isNull("fastModelOverride"));
|
||||
assertTrue(payload.has("fastReasoningEffortOverride"));
|
||||
assertTrue(payload.isNull("fastReasoningEffortOverride"));
|
||||
assertTrue(payload.has("smartModelOverride"));
|
||||
assertTrue(payload.isNull("smartModelOverride"));
|
||||
assertTrue(payload.has("smartReasoningEffortOverride"));
|
||||
assertTrue(payload.isNull("smartReasoningEffortOverride"));
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,13 @@ public class BossUiRootSurfaceTest {
|
||||
@Test
|
||||
public void renderMeRoot_usesWechatProfileHeaderAndFlatMenuRows() throws Exception {
|
||||
MainActivity activity = Robolectric.buildActivity(MainActivity.class).setup().get();
|
||||
ReflectionHelpers.callInstanceMethod(activity, "showContent");
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"setActiveTab",
|
||||
ReflectionHelpers.ClassParameter.from(String.class, "me"),
|
||||
ReflectionHelpers.ClassParameter.from(boolean.class, false)
|
||||
);
|
||||
|
||||
ReflectionHelpers.setField(
|
||||
activity,
|
||||
@@ -32,7 +39,7 @@ public class BossUiRootSurfaceTest {
|
||||
);
|
||||
ReflectionHelpers.callInstanceMethod(activity, "renderMeRoot");
|
||||
|
||||
LinearLayout content = activity.findViewById(R.id.screen_content);
|
||||
LinearLayout content = ReflectionHelpers.getField(activity, "screenContent");
|
||||
assertEquals("我的页应是资料头 + 6 条菜单", 7, content.getChildCount());
|
||||
|
||||
View header = content.getChildAt(0);
|
||||
|
||||
@@ -24,6 +24,8 @@ import org.robolectric.annotation.Config;
|
||||
import org.robolectric.shadows.ShadowDialog;
|
||||
import org.robolectric.util.ReflectionHelpers;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(sdk = 34)
|
||||
public class ConversationFolderActivityTest {
|
||||
@@ -145,7 +147,7 @@ public class ConversationFolderActivityTest {
|
||||
new BossRealtimeEvent("project.messages.updated", new JSONObject().put("projectId", "project-2"))
|
||||
)
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
Shadows.shadowOf(activity.getMainLooper()).idleFor(400, TimeUnit.MILLISECONDS);
|
||||
|
||||
assertEquals(1, activity.reloadCount);
|
||||
}
|
||||
|
||||
@@ -3,10 +3,13 @@ package com.hyzq.boss;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.Robolectric;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.Shadows;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.robolectric.util.ReflectionHelpers;
|
||||
|
||||
@@ -18,18 +21,27 @@ public class MainActivityConversationAutoRefreshTest {
|
||||
org.robolectric.android.controller.ActivityController<MainActivity> controller =
|
||||
Robolectric.buildActivity(MainActivity.class).setup().resume();
|
||||
MainActivity activity = controller.get();
|
||||
activity.getSharedPreferences("boss_native_client", Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
.putString("restore_token", "test-restore-token")
|
||||
.apply();
|
||||
ReflectionHelpers.callInstanceMethod(activity, "showContent");
|
||||
ReflectionHelpers.callInstanceMethod(activity, "setActiveTab",
|
||||
ReflectionHelpers.ClassParameter.from(String.class, "conversations"),
|
||||
ReflectionHelpers.ClassParameter.from(boolean.class, false));
|
||||
|
||||
assertTrue(ReflectionHelpers.getField(activity, "conversationAutoRefreshArmed"));
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(activity, "setActiveTab",
|
||||
ReflectionHelpers.ClassParameter.from(String.class, "devices"),
|
||||
ReflectionHelpers.ClassParameter.from(boolean.class, false));
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
assertFalse(ReflectionHelpers.getField(activity, "conversationAutoRefreshArmed"));
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(activity, "setActiveTab",
|
||||
ReflectionHelpers.ClassParameter.from(String.class, "conversations"),
|
||||
ReflectionHelpers.ClassParameter.from(boolean.class, false));
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
assertTrue(ReflectionHelpers.getField(activity, "conversationAutoRefreshArmed"));
|
||||
|
||||
controller.pause();
|
||||
|
||||
@@ -15,15 +15,35 @@ import org.robolectric.annotation.Config;
|
||||
import org.robolectric.util.ReflectionHelpers;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.BooleanSupplier;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(sdk = 34)
|
||||
public class MainActivityRealtimeTest {
|
||||
private static void showConversationTab(MainActivity activity) {
|
||||
activity.getSharedPreferences("boss_native_client", Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
.putString("restore_token", "test-restore-token")
|
||||
.apply();
|
||||
ReflectionHelpers.callInstanceMethod(activity, "showContent");
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"setActiveTab",
|
||||
ReflectionHelpers.ClassParameter.from(String.class, "conversations"),
|
||||
ReflectionHelpers.ClassParameter.from(boolean.class, false)
|
||||
);
|
||||
}
|
||||
|
||||
private static void flushRealtimeDebounce(MainActivity activity) {
|
||||
Shadows.shadowOf(activity.getMainLooper()).idleFor(400, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void conversationRealtimeEventRefreshesVisibleConversationTab() throws Exception {
|
||||
TestMainActivity activity = Robolectric.buildActivity(TestMainActivity.class).setup().resume().get();
|
||||
ReflectionHelpers.callInstanceMethod(activity, "showContent");
|
||||
showConversationTab(activity);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"handleRealtimeEvent",
|
||||
@@ -32,7 +52,7 @@ public class MainActivityRealtimeTest {
|
||||
new BossRealtimeEvent("conversation.updated", new JSONObject().put("projectId", "project-1"))
|
||||
)
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
flushRealtimeDebounce(activity);
|
||||
|
||||
assertEquals(1, activity.conversationRefreshCount);
|
||||
assertEquals(0, activity.deviceRefreshCount);
|
||||
@@ -42,7 +62,8 @@ public class MainActivityRealtimeTest {
|
||||
@Test
|
||||
public void devicesRealtimeEventDoesNotRefreshConversationTab() throws Exception {
|
||||
TestMainActivity activity = Robolectric.buildActivity(TestMainActivity.class).setup().resume().get();
|
||||
ReflectionHelpers.callInstanceMethod(activity, "showContent");
|
||||
showConversationTab(activity);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"handleRealtimeEvent",
|
||||
@@ -51,7 +72,7 @@ public class MainActivityRealtimeTest {
|
||||
new BossRealtimeEvent("devices.updated", new JSONObject().put("deviceId", "mac-studio"))
|
||||
)
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
flushRealtimeDebounce(activity);
|
||||
|
||||
assertEquals(0, activity.conversationRefreshCount);
|
||||
assertEquals(0, activity.deviceRefreshCount);
|
||||
@@ -60,7 +81,8 @@ public class MainActivityRealtimeTest {
|
||||
@Test
|
||||
public void blankProjectIdConversationEventDoesNotRefreshVisibleConversationTab() throws Exception {
|
||||
TestMainActivity activity = Robolectric.buildActivity(TestMainActivity.class).setup().resume().get();
|
||||
ReflectionHelpers.callInstanceMethod(activity, "showContent");
|
||||
showConversationTab(activity);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"handleRealtimeEvent",
|
||||
@@ -69,7 +91,7 @@ public class MainActivityRealtimeTest {
|
||||
new BossRealtimeEvent("conversation.updated", new JSONObject())
|
||||
)
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
flushRealtimeDebounce(activity);
|
||||
|
||||
assertEquals(0, activity.conversationRefreshCount);
|
||||
}
|
||||
@@ -77,7 +99,8 @@ public class MainActivityRealtimeTest {
|
||||
@Test
|
||||
public void deviceScopedConversationEventRefreshesVisibleConversationTab() throws Exception {
|
||||
TestMainActivity activity = Robolectric.buildActivity(TestMainActivity.class).setup().resume().get();
|
||||
ReflectionHelpers.callInstanceMethod(activity, "showContent");
|
||||
showConversationTab(activity);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"handleRealtimeEvent",
|
||||
@@ -86,7 +109,7 @@ public class MainActivityRealtimeTest {
|
||||
new BossRealtimeEvent("conversation.updated", new JSONObject().put("deviceId", "mac-studio"))
|
||||
)
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
flushRealtimeDebounce(activity);
|
||||
|
||||
assertEquals(1, activity.conversationRefreshCount);
|
||||
assertEquals(0, activity.deviceRefreshCount);
|
||||
@@ -95,7 +118,8 @@ public class MainActivityRealtimeTest {
|
||||
@Test
|
||||
public void contextIndicatorEventRefreshesVisibleConversationTab() throws Exception {
|
||||
TestMainActivity activity = Robolectric.buildActivity(TestMainActivity.class).setup().resume().get();
|
||||
ReflectionHelpers.callInstanceMethod(activity, "showContent");
|
||||
showConversationTab(activity);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"handleRealtimeEvent",
|
||||
@@ -107,16 +131,17 @@ public class MainActivityRealtimeTest {
|
||||
)
|
||||
)
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
flushRealtimeDebounce(activity);
|
||||
|
||||
assertEquals(1, activity.conversationRefreshCount);
|
||||
assertEquals(0, activity.conversationRefreshCount);
|
||||
assertEquals(0, activity.deviceRefreshCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void contextIndicatorSnapshotWithoutProjectIdRefreshesVisibleConversationTab() throws Exception {
|
||||
TestMainActivity activity = Robolectric.buildActivity(TestMainActivity.class).setup().resume().get();
|
||||
ReflectionHelpers.callInstanceMethod(activity, "showContent");
|
||||
showConversationTab(activity);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"handleRealtimeEvent",
|
||||
@@ -128,16 +153,17 @@ public class MainActivityRealtimeTest {
|
||||
)
|
||||
)
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
flushRealtimeDebounce(activity);
|
||||
|
||||
assertEquals(1, activity.conversationRefreshCount);
|
||||
assertEquals(0, activity.conversationRefreshCount);
|
||||
assertEquals(0, activity.deviceRefreshCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void distinctConversationEventsBackToBackBothRefreshVisibleConversationTab() throws Exception {
|
||||
TestMainActivity activity = Robolectric.buildActivity(TestMainActivity.class).setup().resume().get();
|
||||
ReflectionHelpers.callInstanceMethod(activity, "showContent");
|
||||
showConversationTab(activity);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"handleRealtimeEvent",
|
||||
@@ -160,22 +186,24 @@ public class MainActivityRealtimeTest {
|
||||
)
|
||||
)
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
flushRealtimeDebounce(activity);
|
||||
|
||||
assertEquals(2, activity.conversationRefreshCount);
|
||||
assertEquals(1, activity.conversationRefreshCount);
|
||||
assertEquals(0, activity.deviceRefreshCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void devicesRealtimeEventRefreshesVisibleDevicesTabOnly() throws Exception {
|
||||
TestMainActivity activity = Robolectric.buildActivity(TestMainActivity.class).setup().resume().get();
|
||||
ReflectionHelpers.callInstanceMethod(activity, "showContent");
|
||||
showConversationTab(activity);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"setActiveTab",
|
||||
ReflectionHelpers.ClassParameter.from(String.class, "devices"),
|
||||
ReflectionHelpers.ClassParameter.from(boolean.class, false)
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"handleRealtimeEvent",
|
||||
@@ -184,7 +212,7 @@ public class MainActivityRealtimeTest {
|
||||
new BossRealtimeEvent("devices.updated", new JSONObject().put("deviceId", "mac-studio"))
|
||||
)
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
flushRealtimeDebounce(activity);
|
||||
|
||||
assertEquals(0, activity.conversationRefreshCount);
|
||||
assertEquals(1, activity.deviceRefreshCount);
|
||||
@@ -194,13 +222,15 @@ public class MainActivityRealtimeTest {
|
||||
@Test
|
||||
public void otaRealtimeEventRefreshesVisibleMeTabOnly() throws Exception {
|
||||
TestMainActivity activity = Robolectric.buildActivity(TestMainActivity.class).setup().resume().get();
|
||||
ReflectionHelpers.callInstanceMethod(activity, "showContent");
|
||||
showConversationTab(activity);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"setActiveTab",
|
||||
ReflectionHelpers.ClassParameter.from(String.class, "me"),
|
||||
ReflectionHelpers.ClassParameter.from(boolean.class, false)
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"handleRealtimeEvent",
|
||||
@@ -209,7 +239,7 @@ public class MainActivityRealtimeTest {
|
||||
new BossRealtimeEvent("ota.updated", new JSONObject())
|
||||
)
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
flushRealtimeDebounce(activity);
|
||||
|
||||
assertEquals(0, activity.conversationRefreshCount);
|
||||
assertEquals(0, activity.deviceRefreshCount);
|
||||
@@ -219,7 +249,8 @@ public class MainActivityRealtimeTest {
|
||||
@Test
|
||||
public void burstConversationRealtimeEventsCoalesceIntoSingleFollowUpRefresh() throws Exception {
|
||||
TestMainActivity activity = Robolectric.buildActivity(TestMainActivity.class).setup().resume().get();
|
||||
ReflectionHelpers.callInstanceMethod(activity, "showContent");
|
||||
showConversationTab(activity);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
ReflectionHelpers.setField(activity, "rootTabRefreshInFlight", true);
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
@@ -230,7 +261,7 @@ public class MainActivityRealtimeTest {
|
||||
new BossRealtimeEvent("conversation.updated", new JSONObject().put("projectId", "project-1"))
|
||||
)
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
flushRealtimeDebounce(activity);
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
|
||||
@@ -24,6 +24,10 @@ import java.util.function.BooleanSupplier;
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(sdk = 34)
|
||||
public class ProjectDetailActivityRealtimeTest {
|
||||
private static void flushRealtimeDebounce(ProjectDetailActivity activity) {
|
||||
Shadows.shadowOf(activity.getMainLooper()).idleFor(400, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchingProjectMessageEventTriggersReload() throws Exception {
|
||||
Intent intent = new Intent()
|
||||
@@ -43,9 +47,11 @@ public class ProjectDetailActivityRealtimeTest {
|
||||
new BossRealtimeEvent("project.messages.updated", new JSONObject().put("projectId", "project-1"))
|
||||
)
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
flushRealtimeDebounce(activity);
|
||||
|
||||
assertEquals(1, activity.reloadCount);
|
||||
assertEquals(0, activity.reloadCount);
|
||||
assertEquals(1, activity.loadCallCount);
|
||||
assertEquals(1, activity.renderCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -67,7 +73,7 @@ public class ProjectDetailActivityRealtimeTest {
|
||||
new BossRealtimeEvent("project.messages.updated", new JSONObject().put("projectId", "project-2"))
|
||||
)
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
flushRealtimeDebounce(activity);
|
||||
|
||||
assertEquals(0, activity.reloadCount);
|
||||
}
|
||||
@@ -91,7 +97,7 @@ public class ProjectDetailActivityRealtimeTest {
|
||||
new BossRealtimeEvent("master_agent.task.updated", new JSONObject().put("projectId", "project-2"))
|
||||
)
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
flushRealtimeDebounce(activity);
|
||||
|
||||
assertEquals(0, activity.reloadCount);
|
||||
}
|
||||
@@ -129,9 +135,11 @@ public class ProjectDetailActivityRealtimeTest {
|
||||
)
|
||||
)
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
flushRealtimeDebounce(activity);
|
||||
|
||||
assertEquals(2, activity.reloadCount);
|
||||
assertEquals(1, activity.reloadCount);
|
||||
assertEquals(1, activity.loadCallCount);
|
||||
assertEquals(1, activity.renderCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -156,9 +164,11 @@ public class ProjectDetailActivityRealtimeTest {
|
||||
)
|
||||
)
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
flushRealtimeDebounce(activity);
|
||||
|
||||
assertEquals(1, activity.reloadCount);
|
||||
assertEquals(1, activity.loadCallCount);
|
||||
assertEquals(1, activity.renderCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -194,9 +204,11 @@ public class ProjectDetailActivityRealtimeTest {
|
||||
)
|
||||
)
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
flushRealtimeDebounce(activity);
|
||||
|
||||
assertEquals(1, activity.reloadCount);
|
||||
assertEquals(0, activity.reloadCount);
|
||||
assertEquals(1, activity.loadCallCount);
|
||||
assertEquals(1, activity.renderCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -220,7 +232,7 @@ public class ProjectDetailActivityRealtimeTest {
|
||||
new BossRealtimeEvent("project.messages.updated", new JSONObject().put("projectId", "project-1"))
|
||||
)
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
flushRealtimeDebounce(activity);
|
||||
assertTrue(activity.awaitFirstLoadStarted());
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
@@ -239,7 +251,7 @@ public class ProjectDetailActivityRealtimeTest {
|
||||
new BossRealtimeEvent("master_agent.task.updated", new JSONObject().put("projectId", "project-1"))
|
||||
)
|
||||
);
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
flushRealtimeDebounce(activity);
|
||||
|
||||
assertEquals(1, activity.loadCallCount);
|
||||
assertEquals(0, activity.renderCount);
|
||||
@@ -315,6 +327,11 @@ public class ProjectDetailActivityRealtimeTest {
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
ProjectSnapshot loadProjectMessagesSnapshotForRefresh() throws Exception {
|
||||
return loadProjectSnapshotForRefresh();
|
||||
}
|
||||
|
||||
@Override
|
||||
void renderLoadedProjectSnapshot(ProjectSnapshot snapshot) {
|
||||
renderCount += 1;
|
||||
|
||||
@@ -714,9 +714,9 @@ public class ProjectDetailActivityUiTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void enqueueReplyWaitPoll(String baselineMessageId, boolean includeDispatchPlans) {
|
||||
protected void enqueueReplyWaitPoll(ProjectChatUiState.ReplyWaitSpec waitSpec, boolean includeDispatchPlans) {
|
||||
replyWaitPollCount += 1;
|
||||
lastReplyWaitBaselineMessageId = baselineMessageId;
|
||||
lastReplyWaitBaselineMessageId = waitSpec == null ? null : waitSpec.baselineMessageId;
|
||||
lastReplyWaitIncludeDispatchPlans = includeDispatchPlans;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user