feat: surface thread status summary in conversation info

This commit is contained in:
kris
2026-04-05 03:32:12 +08:00
parent 5a53b60f13
commit 2a34c19cc9
2 changed files with 128 additions and 9 deletions

View File

@@ -50,7 +50,17 @@ public class ConversationInfoActivity extends BossScreenActivity {
if (!detailResponse.ok()) throw new IllegalStateException(detailResponse.message()); if (!detailResponse.ok()) throw new IllegalStateException(detailResponse.message());
BossApiClient.ApiResponse participantsResponse = apiClient.getConversationParticipants(projectId); BossApiClient.ApiResponse participantsResponse = apiClient.getConversationParticipants(projectId);
if (!participantsResponse.ok()) throw new IllegalStateException(participantsResponse.message()); 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) { } catch (Exception error) {
runOnUiThread(() -> { runOnUiThread(() -> {
setRefreshing(false); 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(); replaceContent();
JSONObject project = detail.optJSONObject("project"); JSONObject project = detail.optJSONObject("project");
JSONArray participants = participantsPayload.optJSONArray("participants"); JSONArray participants = participantsPayload.optJSONArray("participants");
@@ -84,6 +94,8 @@ public class ConversationInfoActivity extends BossScreenActivity {
buildHeaderDetail(project, threadMeta, participantCount) buildHeaderDetail(project, threadMeta, participantCount)
)); ));
appendThreadStatusSummary(threadStatusPayload);
appendContent(BossUi.buildWechatMenuRow( appendContent(BossUi.buildWechatMenuRow(
this, this,
"发起群聊", "发起群聊",
@@ -140,6 +152,75 @@ public class ConversationInfoActivity extends BossScreenActivity {
setRefreshing(false); 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) { private LinearLayout buildParticipantRow(JSONObject participant) {
boolean sourceProject = participant.optBoolean("isSourceProject", false); boolean sourceProject = participant.optBoolean("isSourceProject", false);
String participantProjectId = participant.optString("projectId", ""); String participantProjectId = participant.optString("projectId", "");

View File

@@ -43,16 +43,20 @@ public class ConversationInfoActivityTest {
activity, activity,
"renderConversation", "renderConversation",
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildDetailPayload()), 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); LinearLayout content = activity.findViewById(R.id.screen_content);
assertTrue(viewTreeContainsText(content.getChildAt(0), "北区试产线回归")); assertTrue(viewTreeContainsText(content.getChildAt(0), "北区试产线回归"));
assertTrue(viewTreeContainsText(content.getChildAt(0), "单线程会话")); assertTrue(viewTreeContainsText(content.getChildAt(0), "单线程会话"));
assertTrue(viewTreeContainsText(content.getChildAt(1), "发起群聊")); assertTrue(viewTreeContainsText(content.getChildAt(1), "线程状态摘要"));
assertTrue(viewTreeContainsText(content.getChildAt(1), "选择其他线程加入新群")); assertTrue(viewTreeContainsTextFragment(content.getChildAt(1), "当前进度:已经记录最近 2 条进展"));
assertTrue(viewTreeContainsText(content.getChildAt(2), "线程详情")); assertTrue(viewTreeContainsTextFragment(content.getChildAt(1), "建议下一步:继续同步 Android 只读页"));
assertTrue(viewTreeContainsText(content.getChildAt(2), "查看当前线程聊天与项目")); 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, "参与线程"));
assertTrue(viewTreeContainsText(content, "硬件审计协作")); assertTrue(viewTreeContainsText(content, "硬件审计协作"));
assertFalse(viewTreeContainsText(content, "从当前会话选择其他线程,创建新的独立群聊")); assertFalse(viewTreeContainsText(content, "从当前会话选择其他线程,创建新的独立群聊"));
@@ -73,7 +77,8 @@ public class ConversationInfoActivityTest {
activity, activity,
"renderConversation", "renderConversation",
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildDetailPayload()), 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( View threadDetailRow = findClickableViewContainingText(
@@ -114,7 +119,8 @@ public class ConversationInfoActivityTest {
activity, activity,
"renderConversation", "renderConversation",
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildDetailPayload()), 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( View threadStatusRow = findClickableViewContainingText(
@@ -188,6 +194,19 @@ public class ConversationInfoActivityTest {
return new JSONObject().put("participants", participants); 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) { private static boolean viewTreeContainsText(View root, String expectedText) {
if (root instanceof TextView) { if (root instanceof TextView) {
CharSequence text = ((TextView) root).getText(); CharSequence text = ((TextView) root).getText();
@@ -207,6 +226,25 @@ public class ConversationInfoActivityTest {
return false; 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) { private static View findClickableViewContainingText(View root, String expectedText) {
if (root == null) { if (root == null) {
return null; return null;