diff --git a/android/app/src/main/java/com/hyzq/boss/ConversationInfoActivity.java b/android/app/src/main/java/com/hyzq/boss/ConversationInfoActivity.java index fc72a66..224e285 100644 --- a/android/app/src/main/java/com/hyzq/boss/ConversationInfoActivity.java +++ b/android/app/src/main/java/com/hyzq/boss/ConversationInfoActivity.java @@ -50,7 +50,17 @@ public class ConversationInfoActivity extends BossScreenActivity { if (!detailResponse.ok()) throw new IllegalStateException(detailResponse.message()); BossApiClient.ApiResponse participantsResponse = apiClient.getConversationParticipants(projectId); if (!participantsResponse.ok()) throw new IllegalStateException(participantsResponse.message()); - runOnUiThread(() -> renderConversation(detailResponse.json, participantsResponse.json)); + JSONObject threadStatusPayload = null; + try { + BossApiClient.ApiResponse threadStatusResponse = apiClient.getThreadStatus(projectId); + if (threadStatusResponse.ok()) { + threadStatusPayload = threadStatusResponse.json; + } + } catch (Exception ignored) { + threadStatusPayload = null; + } + JSONObject finalThreadStatusPayload = threadStatusPayload; + runOnUiThread(() -> renderConversation(detailResponse.json, participantsResponse.json, finalThreadStatusPayload)); } catch (Exception error) { runOnUiThread(() -> { setRefreshing(false); @@ -60,7 +70,7 @@ public class ConversationInfoActivity extends BossScreenActivity { }); } - private void renderConversation(JSONObject detail, JSONObject participantsPayload) { + private void renderConversation(JSONObject detail, JSONObject participantsPayload, @Nullable JSONObject threadStatusPayload) { replaceContent(); JSONObject project = detail.optJSONObject("project"); JSONArray participants = participantsPayload.optJSONArray("participants"); @@ -84,6 +94,8 @@ public class ConversationInfoActivity extends BossScreenActivity { buildHeaderDetail(project, threadMeta, participantCount) )); + appendThreadStatusSummary(threadStatusPayload); + appendContent(BossUi.buildWechatMenuRow( this, "发起群聊", @@ -140,6 +152,75 @@ public class ConversationInfoActivity extends BossScreenActivity { setRefreshing(false); } + private void appendThreadStatusSummary(@Nullable JSONObject threadStatusPayload) { + if (threadStatusPayload == null) { + return; + } + JSONObject document = threadStatusPayload.optJSONObject("threadStatusDocument"); + if (document == null) { + return; + } + JSONArray recentProgressEvents = threadStatusPayload.optJSONArray("recentProgressEvents"); + int eventCount = recentProgressEvents == null ? 0 : recentProgressEvents.length(); + String body = buildThreadStatusSummaryBody(document, eventCount); + String meta = buildThreadStatusSummaryMeta(document, eventCount); + appendContent(BossUi.buildCard(this, "线程状态摘要", body, meta)); + } + + private String buildThreadStatusSummaryBody(JSONObject document, int eventCount) { + return joinNonEmptyLines( + formatSummaryLine("当前目标", document.optString("projectGoal", "")), + formatSummaryLine("当前进度", document.optString("currentProgress", "")), + formatSummaryLine("当前阻塞", document.optString("currentBlockers", "")), + formatSummaryLine("建议下一步", document.optString("recommendedNextStep", "")), + eventCount > 0 ? "最近进展:" + eventCount + " 条" : "" + ); + } + + private String buildThreadStatusSummaryMeta(JSONObject document, int eventCount) { + return joinNonEmptyParts( + projectFolderName, + eventCount > 0 ? "最近 " + eventCount + " 条进展" : "暂无进展", + document.optString("updatedAt", "").isEmpty() ? "" : "更新于 " + document.optString("updatedAt", "") + ); + } + + private String formatSummaryLine(String label, String value) { + String trimmed = value == null ? "" : value.trim(); + if (trimmed.isEmpty()) { + return ""; + } + return label + ":" + trimmed; + } + + private String joinNonEmptyLines(String... values) { + StringBuilder builder = new StringBuilder(); + for (String value : values) { + if (value == null || value.trim().isEmpty()) { + continue; + } + if (builder.length() > 0) { + builder.append('\n'); + } + builder.append(value.trim()); + } + return builder.toString(); + } + + private String joinNonEmptyParts(String... values) { + StringBuilder builder = new StringBuilder(); + for (String value : values) { + if (value == null || value.trim().isEmpty()) { + continue; + } + if (builder.length() > 0) { + builder.append(" · "); + } + builder.append(value.trim()); + } + return builder.toString(); + } + private LinearLayout buildParticipantRow(JSONObject participant) { boolean sourceProject = participant.optBoolean("isSourceProject", false); String participantProjectId = participant.optString("projectId", ""); diff --git a/android/app/src/test/java/com/hyzq/boss/ConversationInfoActivityTest.java b/android/app/src/test/java/com/hyzq/boss/ConversationInfoActivityTest.java index df30585..08a0573 100644 --- a/android/app/src/test/java/com/hyzq/boss/ConversationInfoActivityTest.java +++ b/android/app/src/test/java/com/hyzq/boss/ConversationInfoActivityTest.java @@ -43,16 +43,20 @@ public class ConversationInfoActivityTest { activity, "renderConversation", ReflectionHelpers.ClassParameter.from(JSONObject.class, buildDetailPayload()), - ReflectionHelpers.ClassParameter.from(JSONObject.class, buildParticipantsPayload()) + ReflectionHelpers.ClassParameter.from(JSONObject.class, buildParticipantsPayload()), + ReflectionHelpers.ClassParameter.from(JSONObject.class, buildThreadStatusPayload()) ); LinearLayout content = activity.findViewById(R.id.screen_content); assertTrue(viewTreeContainsText(content.getChildAt(0), "北区试产线回归")); assertTrue(viewTreeContainsText(content.getChildAt(0), "单线程会话")); - assertTrue(viewTreeContainsText(content.getChildAt(1), "发起群聊")); - assertTrue(viewTreeContainsText(content.getChildAt(1), "选择其他线程加入新群")); - assertTrue(viewTreeContainsText(content.getChildAt(2), "线程详情")); - assertTrue(viewTreeContainsText(content.getChildAt(2), "查看当前线程聊天与项目")); + assertTrue(viewTreeContainsText(content.getChildAt(1), "线程状态摘要")); + assertTrue(viewTreeContainsTextFragment(content.getChildAt(1), "当前进度:已经记录最近 2 条进展")); + assertTrue(viewTreeContainsTextFragment(content.getChildAt(1), "建议下一步:继续同步 Android 只读页")); + assertTrue(viewTreeContainsText(content.getChildAt(2), "发起群聊")); + assertTrue(viewTreeContainsText(content.getChildAt(2), "选择其他线程加入新群")); + assertTrue(viewTreeContainsText(content.getChildAt(3), "线程详情")); + assertTrue(viewTreeContainsText(content.getChildAt(3), "查看当前线程聊天与项目")); assertTrue(viewTreeContainsText(content, "参与线程")); assertTrue(viewTreeContainsText(content, "硬件审计协作")); assertFalse(viewTreeContainsText(content, "从当前会话选择其他线程,创建新的独立群聊")); @@ -73,7 +77,8 @@ public class ConversationInfoActivityTest { activity, "renderConversation", ReflectionHelpers.ClassParameter.from(JSONObject.class, buildDetailPayload()), - ReflectionHelpers.ClassParameter.from(JSONObject.class, buildParticipantsPayload()) + ReflectionHelpers.ClassParameter.from(JSONObject.class, buildParticipantsPayload()), + ReflectionHelpers.ClassParameter.from(JSONObject.class, buildThreadStatusPayload()) ); View threadDetailRow = findClickableViewContainingText( @@ -114,7 +119,8 @@ public class ConversationInfoActivityTest { activity, "renderConversation", ReflectionHelpers.ClassParameter.from(JSONObject.class, buildDetailPayload()), - ReflectionHelpers.ClassParameter.from(JSONObject.class, buildParticipantsPayload()) + ReflectionHelpers.ClassParameter.from(JSONObject.class, buildParticipantsPayload()), + ReflectionHelpers.ClassParameter.from(JSONObject.class, buildThreadStatusPayload()) ); View threadStatusRow = findClickableViewContainingText( @@ -188,6 +194,19 @@ public class ConversationInfoActivityTest { return new JSONObject().put("participants", participants); } + private static JSONObject buildThreadStatusPayload() throws Exception { + return new JSONObject() + .put("threadStatusDocument", new JSONObject() + .put("projectGoal", "完成线程状态回归") + .put("currentProgress", "已经记录最近 2 条进展") + .put("currentBlockers", "暂无阻塞") + .put("recommendedNextStep", "继续同步 Android 只读页") + .put("updatedAt", "2026-04-04T18:00:00+08:00")) + .put("recentProgressEvents", new JSONArray() + .put(new JSONObject().put("summary", "事件 2")) + .put(new JSONObject().put("summary", "事件 1"))); + } + private static boolean viewTreeContainsText(View root, String expectedText) { if (root instanceof TextView) { CharSequence text = ((TextView) root).getText(); @@ -207,6 +226,25 @@ public class ConversationInfoActivityTest { return false; } + private static boolean viewTreeContainsTextFragment(View root, String expectedText) { + if (root instanceof TextView) { + CharSequence text = ((TextView) root).getText(); + if (text != null && text.toString().contains(expectedText)) { + return true; + } + } + if (!(root instanceof ViewGroup)) { + return false; + } + ViewGroup group = (ViewGroup) root; + for (int index = 0; index < group.getChildCount(); index += 1) { + if (viewTreeContainsTextFragment(group.getChildAt(index), expectedText)) { + return true; + } + } + return false; + } + private static View findClickableViewContainingText(View root, String expectedText) { if (root == null) { return null;