feat: queue master-agent chat replies
This commit is contained in:
@@ -97,6 +97,21 @@ public class BossApiClient {
|
||||
return requestWithRestore("GET", "/api/v1/projects/" + encode(projectId) + "/dispatch-plans", null);
|
||||
}
|
||||
|
||||
public ApiResponse getProjectAgentControls(String projectId) throws IOException, JSONException {
|
||||
return requestWithRestore("GET", "/api/v1/projects/" + encode(projectId) + "/agent-controls", null);
|
||||
}
|
||||
|
||||
public ApiResponse updateProjectAgentControls(
|
||||
String projectId,
|
||||
@Nullable String modelOverride,
|
||||
@Nullable String reasoningEffortOverride
|
||||
) throws IOException, JSONException {
|
||||
JSONObject payload = new JSONObject();
|
||||
payload.put("modelOverride", modelOverride == null ? JSONObject.NULL : modelOverride);
|
||||
payload.put("reasoningEffortOverride", reasoningEffortOverride == null ? JSONObject.NULL : reasoningEffortOverride);
|
||||
return requestWithRestore("POST", "/api/v1/projects/" + encode(projectId) + "/agent-controls", payload);
|
||||
}
|
||||
|
||||
public ApiResponse confirmDispatchPlan(String projectId, String planId, JSONArray approvedTargetProjectIds) throws IOException, JSONException {
|
||||
JSONObject payload = new JSONObject();
|
||||
payload.put(
|
||||
|
||||
@@ -35,6 +35,8 @@ import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class ProjectDetailActivity extends BossScreenActivity {
|
||||
public static final String EXTRA_PROJECT_ID = "project_id";
|
||||
@@ -46,6 +48,8 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
private String initialProjectName;
|
||||
private boolean projectIsGroup;
|
||||
private String projectFolderName;
|
||||
private @Nullable String currentAgentModelOverride;
|
||||
private @Nullable String currentReasoningEffortOverride;
|
||||
private LinearLayout quickActionsLayout;
|
||||
private LinearLayout composerRow;
|
||||
private LinearLayout multiSelectActionsLayout;
|
||||
@@ -59,6 +63,8 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
private boolean renderNearBottom;
|
||||
private boolean renderForcedScrollToBottom;
|
||||
private boolean conversationInfoReady;
|
||||
private boolean masterAgentReplyWaiting;
|
||||
private @Nullable String masterAgentReplyBaselineMessageId;
|
||||
private String currentScreenTitle;
|
||||
private String currentScreenSubtitle;
|
||||
private String projectCollaborationMode = "development";
|
||||
@@ -70,6 +76,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
private ActivityResultLauncher<String> imagePickerLauncher;
|
||||
private ActivityResultLauncher<String> videoPickerLauncher;
|
||||
private ActivityResultLauncher<String> filePickerLauncher;
|
||||
private final ExecutorService replyWaitExecutor = Executors.newSingleThreadExecutor();
|
||||
|
||||
static final class ChromeBindings {
|
||||
final boolean multiSelecting;
|
||||
@@ -212,6 +219,12 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
replyWaitExecutor.shutdownNow();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
boolean shouldLoadOnCreate() {
|
||||
return true;
|
||||
}
|
||||
@@ -239,7 +252,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
setRefreshing(false);
|
||||
composerSending = false;
|
||||
updateComposerSendButtonState();
|
||||
if (pendingOutgoingBubble == null) {
|
||||
if (pendingOutgoingBubble == null && !masterAgentReplyWaiting) {
|
||||
replaceContent(BossUi.buildEmptyCard(this, "项目详情加载失败:" + error.getMessage()));
|
||||
} else {
|
||||
showMessage("项目详情刷新失败:" + error.getMessage());
|
||||
@@ -277,6 +290,9 @@ 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");
|
||||
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));
|
||||
currentPendingDispatchPlan = ProjectChatUiState.latestPendingDispatchPlan(dispatchPlans);
|
||||
conversationInfoReady = project != null;
|
||||
updateProjectHeader(title, buildProjectSubtitle(projectFolderName, devices));
|
||||
@@ -305,6 +321,17 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
appendContent(BossUi.buildMessagePlaceholder(this, "还没有项目消息,先发一条开始对话。"));
|
||||
}
|
||||
|
||||
boolean masterAgentStillWaiting = isMasterAgentConversation()
|
||||
&& masterAgentReplyWaiting
|
||||
&& !ProjectChatUiState.hasReplyBeyondBaseline(project, masterAgentReplyBaselineMessageId);
|
||||
if (isMasterAgentConversation() && masterAgentReplyWaiting && !masterAgentStillWaiting) {
|
||||
masterAgentReplyWaiting = false;
|
||||
masterAgentReplyBaselineMessageId = null;
|
||||
}
|
||||
if (masterAgentStillWaiting) {
|
||||
appendContent(buildMasterAgentThinkingPlaceholder());
|
||||
}
|
||||
|
||||
setRefreshing(false);
|
||||
updateSelectionUi();
|
||||
if (ProjectChatUiState.shouldAutoScroll(renderNearBottom, renderForcedScrollToBottom)) {
|
||||
@@ -491,7 +518,11 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
return;
|
||||
}
|
||||
if (waitSpec.shouldWait) {
|
||||
startReplyWait(waitSpec, false, "消息已发送,正在等待回复…");
|
||||
if (isMasterAgentConversation()) {
|
||||
startMasterAgentReplyWait(waitSpec, false, "消息已发送,主 Agent 思考中");
|
||||
} else {
|
||||
startReplyWait(waitSpec, false, "消息已发送,正在等待回复…");
|
||||
}
|
||||
return;
|
||||
}
|
||||
composerSending = false;
|
||||
@@ -586,6 +617,162 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
conversationInfoLauncher.launch(intent);
|
||||
}
|
||||
|
||||
private void showMasterAgentMoreMenu() {
|
||||
if (!isMasterAgentConversation()) {
|
||||
return;
|
||||
}
|
||||
new AlertDialog.Builder(this)
|
||||
.setItems(new CharSequence[]{"模型", "推理强度", "会话信息", "刷新"}, (dialog, which) -> {
|
||||
switch (which) {
|
||||
case 0:
|
||||
showMasterAgentModelPicker();
|
||||
break;
|
||||
case 1:
|
||||
showMasterAgentReasoningPicker();
|
||||
break;
|
||||
case 2:
|
||||
openConversationInfo();
|
||||
break;
|
||||
case 3:
|
||||
reload(true);
|
||||
break;
|
||||
default:
|
||||
dialog.dismiss();
|
||||
break;
|
||||
}
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showMasterAgentModelPicker() {
|
||||
if (!isMasterAgentConversation()) {
|
||||
return;
|
||||
}
|
||||
final String[] options = buildMasterAgentModelOptions();
|
||||
int checkedIndex = findCheckedIndex(options, currentAgentModelOverride);
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("模型")
|
||||
.setSingleChoiceItems(options, checkedIndex, (dialog, which) -> {
|
||||
if (which == 0) {
|
||||
dialog.dismiss();
|
||||
updateMasterAgentControls(null, currentReasoningEffortOverride, "模型已恢复默认");
|
||||
return;
|
||||
}
|
||||
if (which == options.length - 1) {
|
||||
dialog.dismiss();
|
||||
showCustomMasterAgentModelDialog();
|
||||
return;
|
||||
}
|
||||
dialog.dismiss();
|
||||
updateMasterAgentControls(options[which], currentReasoningEffortOverride, "模型已更新为 " + options[which]);
|
||||
})
|
||||
.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);
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("自定义模型")
|
||||
.setView(input)
|
||||
.setNegativeButton("取消", null)
|
||||
.setPositiveButton("保存", (dialog, which) ->
|
||||
updateMasterAgentControls(
|
||||
normalizeControlValue(input.getText() == null ? null : input.getText().toString()),
|
||||
currentReasoningEffortOverride,
|
||||
"模型已更新"
|
||||
))
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showMasterAgentReasoningPicker() {
|
||||
if (!isMasterAgentConversation()) {
|
||||
return;
|
||||
}
|
||||
final String[] options = new String[]{"沿用默认", "low", "medium", "high"};
|
||||
int checkedIndex = findCheckedIndex(options, currentReasoningEffortOverride);
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("推理强度")
|
||||
.setSingleChoiceItems(options, checkedIndex, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
String reasoningOverride = which == 0 ? null : options[which];
|
||||
updateMasterAgentControls(
|
||||
currentAgentModelOverride,
|
||||
reasoningOverride,
|
||||
which == 0 ? "推理强度已恢复默认" : "推理强度已更新为 " + options[which]
|
||||
);
|
||||
})
|
||||
.setNegativeButton("取消", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void updateMasterAgentControls(
|
||||
@Nullable String modelOverride,
|
||||
@Nullable String reasoningEffortOverride,
|
||||
String successMessage
|
||||
) {
|
||||
if (!isMasterAgentConversation() || projectId == null || projectId.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
setRefreshing(true);
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
BossApiClient.ApiResponse response = apiClient.updateProjectAgentControls(
|
||||
projectId,
|
||||
modelOverride,
|
||||
reasoningEffortOverride
|
||||
);
|
||||
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));
|
||||
showMessage(successMessage);
|
||||
reload(true);
|
||||
});
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
setRefreshing(false);
|
||||
showMessage("保存失败:" + error.getMessage());
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String[] buildMasterAgentModelOptions() {
|
||||
List<String> options = new ArrayList<>();
|
||||
options.add("沿用默认");
|
||||
if (!TextUtils.isEmpty(currentAgentModelOverride)) {
|
||||
options.add(currentAgentModelOverride);
|
||||
}
|
||||
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-4.1")) {
|
||||
options.add("gpt-4.1");
|
||||
}
|
||||
options.add("自定义...");
|
||||
return options.toArray(new String[0]);
|
||||
}
|
||||
|
||||
private int findCheckedIndex(String[] options, @Nullable String selectedValue) {
|
||||
if (TextUtils.isEmpty(selectedValue)) {
|
||||
return 0;
|
||||
}
|
||||
for (int i = 0; i < options.length; i++) {
|
||||
if (selectedValue.equals(options[i])) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private View buildPendingDispatchPlanView(JSONObject dispatchPlan) {
|
||||
LinearLayout container = new LinearLayout(this);
|
||||
container.setOrientation(LinearLayout.VERTICAL);
|
||||
@@ -1154,10 +1341,14 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
}
|
||||
finish();
|
||||
});
|
||||
refreshButton.setVisibility(bindings.showRefresh ? View.VISIBLE : View.GONE);
|
||||
refreshButton.setVisibility(bindings.showRefresh && !isMasterAgentConversation() ? View.VISIBLE : View.GONE);
|
||||
titleView.setText(bindings.title);
|
||||
subtitleView.setText(bindings.subtitle);
|
||||
if (bindings.showHeaderAction) {
|
||||
if (bindings.multiSelecting) {
|
||||
hideHeaderAction();
|
||||
} else if (isMasterAgentConversation()) {
|
||||
setHeaderAction("...", v -> showMasterAgentMoreMenu());
|
||||
} else if (bindings.showHeaderAction) {
|
||||
setHeaderAction(WechatSurfaceMapper.conversationInfoActionLabel(), v -> openConversationInfo());
|
||||
} else {
|
||||
hideHeaderAction();
|
||||
@@ -1214,6 +1405,22 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
return folderName + " · 设备:" + deviceLabel;
|
||||
}
|
||||
|
||||
private boolean isMasterAgentConversation() {
|
||||
return "master-agent".equals(projectId);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String normalizeControlValue(@Nullable String value) {
|
||||
if (TextUtils.isEmpty(value)) {
|
||||
return null;
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
private View buildMasterAgentThinkingPlaceholder() {
|
||||
return BossUi.buildHintPill(this, "主 Agent 思考中");
|
||||
}
|
||||
|
||||
private void scrollChatToBottom() {
|
||||
if (chatScrollView == null) {
|
||||
return;
|
||||
@@ -1597,6 +1804,10 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
boolean includeDispatchPlans,
|
||||
String waitingMessage
|
||||
) {
|
||||
if (isMasterAgentConversation()) {
|
||||
startMasterAgentReplyWait(waitSpec, includeDispatchPlans, waitingMessage);
|
||||
return;
|
||||
}
|
||||
composerSending = true;
|
||||
updateComposerSendButtonState();
|
||||
setRefreshing(true);
|
||||
@@ -1604,6 +1815,21 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
executor.execute(() -> pollUntilReply(waitSpec, includeDispatchPlans));
|
||||
}
|
||||
|
||||
private void startMasterAgentReplyWait(
|
||||
ProjectChatUiState.ReplyWaitSpec waitSpec,
|
||||
boolean includeDispatchPlans,
|
||||
String waitingMessage
|
||||
) {
|
||||
masterAgentReplyWaiting = true;
|
||||
masterAgentReplyBaselineMessageId = waitSpec.baselineMessageId;
|
||||
composerSending = false;
|
||||
updateComposerSendButtonState();
|
||||
setRefreshing(false);
|
||||
showMessage(waitingMessage);
|
||||
reload(true);
|
||||
replyWaitExecutor.execute(() -> pollUntilReply(waitSpec, includeDispatchPlans));
|
||||
}
|
||||
|
||||
private void pollUntilReply(
|
||||
ProjectChatUiState.ReplyWaitSpec waitSpec,
|
||||
boolean includeDispatchPlans
|
||||
@@ -1619,7 +1845,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
if (!renderedInitialSnapshot || hasReply) {
|
||||
runOnUiThread(() -> {
|
||||
renderProject(snapshot.payload, snapshot.dispatchPlans, snapshot.participantsPayload);
|
||||
if (!hasReply) {
|
||||
if (!hasReply && !isMasterAgentConversation()) {
|
||||
composerSending = true;
|
||||
updateComposerSendButtonState();
|
||||
setRefreshing(true);
|
||||
@@ -1630,6 +1856,10 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
|
||||
if (hasReply) {
|
||||
runOnUiThread(() -> {
|
||||
if (isMasterAgentConversation()) {
|
||||
masterAgentReplyWaiting = false;
|
||||
masterAgentReplyBaselineMessageId = null;
|
||||
}
|
||||
composerSending = false;
|
||||
updateComposerSendButtonState();
|
||||
setRefreshing(false);
|
||||
@@ -1642,6 +1872,10 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
}
|
||||
|
||||
runOnUiThread(() -> {
|
||||
if (isMasterAgentConversation()) {
|
||||
masterAgentReplyWaiting = false;
|
||||
masterAgentReplyBaselineMessageId = null;
|
||||
}
|
||||
composerSending = false;
|
||||
updateComposerSendButtonState();
|
||||
setRefreshing(false);
|
||||
@@ -1650,6 +1884,10 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
});
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
if (isMasterAgentConversation()) {
|
||||
masterAgentReplyWaiting = false;
|
||||
masterAgentReplyBaselineMessageId = null;
|
||||
}
|
||||
composerSending = false;
|
||||
updateComposerSendButtonState();
|
||||
setRefreshing(false);
|
||||
|
||||
@@ -69,6 +69,31 @@ public class BossApiClientDispatchPlansTest {
|
||||
assertEquals("{}", connection.requestBody());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getProjectAgentControlsUsesScopedEndpoint() throws Exception {
|
||||
RecordingConnection connection = new RecordingConnection(new URL("https://boss.hyzq.net/api/v1/projects/master-agent/agent-controls"));
|
||||
RecordingBossApiClient apiClient = new RecordingBossApiClient(connection);
|
||||
|
||||
BossApiClient.ApiResponse response = apiClient.getProjectAgentControls("master-agent");
|
||||
|
||||
assertEquals(200, response.statusCode);
|
||||
assertEquals("/api/v1/projects/master-agent/agent-controls", apiClient.lastPath);
|
||||
assertEquals("GET", connection.requestMethodValue);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateProjectAgentControlsWritesModelAndReasoningOverrides() throws Exception {
|
||||
RecordingConnection connection = new RecordingConnection(new URL("https://boss.hyzq.net/api/v1/projects/master-agent/agent-controls"));
|
||||
RecordingBossApiClient apiClient = new RecordingBossApiClient(connection);
|
||||
|
||||
BossApiClient.ApiResponse response = apiClient.updateProjectAgentControls("master-agent", "gpt-5.4", "high");
|
||||
|
||||
assertEquals(200, response.statusCode);
|
||||
assertEquals("/api/v1/projects/master-agent/agent-controls", apiClient.lastPath);
|
||||
assertEquals("POST", connection.requestMethodValue);
|
||||
assertEquals("{\"modelOverride\":\"gpt-5.4\",\"reasoningEffortOverride\":\"high\"}", connection.requestBody());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendProjectMessageUsesExtendedReadTimeoutForMasterAgent() throws Exception {
|
||||
RecordingConnection connection = new RecordingConnection(new URL("https://boss.hyzq.net/api/v1/projects/master-agent/messages"));
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
package com.hyzq.boss;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.Robolectric;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.robolectric.shadows.ShadowDialog;
|
||||
import org.robolectric.util.ReflectionHelpers;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(sdk = 34)
|
||||
public class ProjectDetailActivityMasterAgentMenuTest {
|
||||
@Test
|
||||
public void masterAgentMoreMenuShowsWechatActions() {
|
||||
Intent intent = new Intent()
|
||||
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, "master-agent")
|
||||
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, "主 Agent");
|
||||
ProjectDetailActivityUiTest.TestProjectDetailActivity activity = Robolectric
|
||||
.buildActivity(ProjectDetailActivityUiTest.TestProjectDetailActivity.class, intent)
|
||||
.setup()
|
||||
.get();
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(activity, "showMasterAgentMoreMenu");
|
||||
|
||||
android.app.Dialog latestDialog = ShadowDialog.getLatestDialog();
|
||||
assertTrue(latestDialog instanceof AlertDialog);
|
||||
AlertDialog actionDialog = (AlertDialog) latestDialog;
|
||||
ListView listView = actionDialog.getListView();
|
||||
|
||||
assertMenuItem(listView, 0, "模型");
|
||||
assertMenuItem(listView, 1, "推理强度");
|
||||
assertMenuItem(listView, 2, "会话信息");
|
||||
assertMenuItem(listView, 3, "刷新");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void masterAgentWaitingStateRendersThinkingPlaceholder() throws Exception {
|
||||
Intent intent = new Intent()
|
||||
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, "master-agent")
|
||||
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, "主 Agent");
|
||||
ProjectDetailActivityUiTest.TestProjectDetailActivity activity = Robolectric
|
||||
.buildActivity(ProjectDetailActivityUiTest.TestProjectDetailActivity.class, intent)
|
||||
.setup()
|
||||
.get();
|
||||
|
||||
ReflectionHelpers.setField(activity, "conversationInfoReady", true);
|
||||
ReflectionHelpers.setField(activity, "masterAgentReplyWaiting", true);
|
||||
ReflectionHelpers.setField(activity, "masterAgentReplyBaselineMessageId", "msg-user-1");
|
||||
|
||||
JSONObject project = new JSONObject()
|
||||
.put("id", "master-agent")
|
||||
.put("name", "主 Agent")
|
||||
.put("messages", new JSONArray()
|
||||
.put(new JSONObject()
|
||||
.put("id", "msg-user-1")
|
||||
.put("sender", "user")
|
||||
.put("senderLabel", "Boss 超级管理员")
|
||||
.put("body", "帮我检查当前主控")
|
||||
.put("kind", "text")));
|
||||
JSONObject payload = new JSONObject().put("project", project);
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"renderProject",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, payload),
|
||||
ReflectionHelpers.ClassParameter.from(JSONArray.class, null),
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, null)
|
||||
);
|
||||
|
||||
View contentRoot = activity.findViewById(R.id.screen_content);
|
||||
assertNotNull(contentRoot);
|
||||
assertTrue(viewTreeContainsText(contentRoot, "主 Agent 思考中"));
|
||||
}
|
||||
|
||||
private static void assertMenuItem(ListView listView, int index, String expectedText) {
|
||||
View item = listView.getAdapter().getView(index, null, listView);
|
||||
assertTrue(viewTreeContainsText(item, expectedText));
|
||||
}
|
||||
|
||||
private static boolean viewTreeContainsText(View root, String expectedText) {
|
||||
if (root instanceof TextView) {
|
||||
CharSequence text = ((TextView) root).getText();
|
||||
if (expectedText.contentEquals(text)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (!(root instanceof ViewGroup)) {
|
||||
return false;
|
||||
}
|
||||
ViewGroup group = (ViewGroup) root;
|
||||
for (int index = 0; index < group.getChildCount(); index += 1) {
|
||||
if (viewTreeContainsText(group.getChildAt(index), expectedText)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -232,6 +232,27 @@ public class ProjectDetailActivityUiTest {
|
||||
assertFalse(viewTreeContainsText(messageView, "Boss 超级管理员 · 10:26"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void masterAgentHeaderUsesWechatMoreMenuLabel() {
|
||||
Intent intent = new Intent()
|
||||
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, "master-agent")
|
||||
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, "主 Agent");
|
||||
TestProjectDetailActivity activity = Robolectric
|
||||
.buildActivity(TestProjectDetailActivity.class, intent)
|
||||
.setup()
|
||||
.get();
|
||||
|
||||
ReflectionHelpers.setField(activity, "conversationInfoReady", true);
|
||||
ReflectionHelpers.setField(activity, "currentScreenTitle", "主 Agent");
|
||||
ReflectionHelpers.setField(activity, "currentScreenSubtitle", "单聊会话");
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(activity, "updateSelectionUi");
|
||||
|
||||
Button headerAction = activity.findViewById(R.id.screen_header_action);
|
||||
assertEquals(View.VISIBLE, headerAction.getVisibility());
|
||||
assertEquals("...", headerAction.getText().toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void outgoingAttachmentMetaPrefersTimeOnly() throws Exception {
|
||||
Intent intent = new Intent()
|
||||
|
||||
Reference in New Issue
Block a user