feat: queue device import review tasks
This commit is contained in:
@@ -23,7 +23,9 @@ public class DeviceImportDraftActivity extends BossScreenActivity {
|
||||
private String deviceName;
|
||||
private @Nullable JSONObject currentDraft;
|
||||
private @Nullable JSONObject currentResolution;
|
||||
private @Nullable JSONObject currentReviewTask;
|
||||
private final LinkedHashSet<String> selectedCandidateIds = new LinkedHashSet<>();
|
||||
private final Runnable reviewPollRunnable = this::reload;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
@@ -48,7 +50,11 @@ public class DeviceImportDraftActivity extends BossScreenActivity {
|
||||
if (!response.ok()) {
|
||||
throw new IllegalStateException(response.message());
|
||||
}
|
||||
runOnUiThread(() -> applyPayload(response.json.optJSONObject("draft"), response.json.optJSONObject("resolution")));
|
||||
runOnUiThread(() -> applyPayload(
|
||||
response.json.optJSONObject("draft"),
|
||||
response.json.optJSONObject("resolution"),
|
||||
response.json.optJSONObject("reviewTask")
|
||||
));
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
setRefreshing(false);
|
||||
@@ -58,9 +64,10 @@ public class DeviceImportDraftActivity extends BossScreenActivity {
|
||||
});
|
||||
}
|
||||
|
||||
private void applyPayload(@Nullable JSONObject draft, @Nullable JSONObject resolution) {
|
||||
private void applyPayload(@Nullable JSONObject draft, @Nullable JSONObject resolution, @Nullable JSONObject reviewTask) {
|
||||
currentDraft = draft;
|
||||
currentResolution = resolution;
|
||||
currentReviewTask = reviewTask;
|
||||
selectedCandidateIds.clear();
|
||||
JSONArray selected = draft == null ? null : draft.optJSONArray("selectedCandidateIds");
|
||||
if (selected != null) {
|
||||
@@ -74,9 +81,31 @@ public class DeviceImportDraftActivity extends BossScreenActivity {
|
||||
renderCurrentState();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
contentLayout.removeCallbacks(reviewPollRunnable);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
private boolean isReviewPending(@Nullable JSONObject draft, @Nullable JSONObject resolution, @Nullable JSONObject reviewTask) {
|
||||
if (draft == null || resolution != null) {
|
||||
return false;
|
||||
}
|
||||
if (!"pending_resolution".equals(draft.optString("status", ""))) {
|
||||
return false;
|
||||
}
|
||||
if (reviewTask == null) {
|
||||
return false;
|
||||
}
|
||||
String taskStatus = reviewTask.optString("status", "");
|
||||
return "queued".equals(taskStatus) || "running".equals(taskStatus);
|
||||
}
|
||||
|
||||
private void renderCurrentState() {
|
||||
JSONObject draft = currentDraft;
|
||||
JSONObject resolution = currentResolution;
|
||||
JSONObject reviewTask = currentReviewTask;
|
||||
contentLayout.removeCallbacks(reviewPollRunnable);
|
||||
replaceContent();
|
||||
appendContent(BossUi.buildSoftPanel(
|
||||
this,
|
||||
@@ -110,7 +139,7 @@ public class DeviceImportDraftActivity extends BossScreenActivity {
|
||||
appendContent(BossUi.buildCard(
|
||||
this,
|
||||
resolveStatusTitle(draft),
|
||||
resolveStatusBody(draft, resolution),
|
||||
resolveStatusBody(draft, resolution, reviewTask),
|
||||
"候选 " + candidates.length()
|
||||
+ " · 已选 " + selectedCandidateIds.size()
|
||||
+ " · 推荐 " + recommendedCount
|
||||
@@ -183,6 +212,17 @@ public class DeviceImportDraftActivity extends BossScreenActivity {
|
||||
}
|
||||
}
|
||||
|
||||
if (reviewTask != null) {
|
||||
appendContent(BossUi.buildCard(
|
||||
this,
|
||||
"审核任务",
|
||||
"状态:" + reviewTask.optString("status", "unknown"),
|
||||
isReviewPending(draft, resolution, reviewTask)
|
||||
? "主 Agent 正在生成导入建议,页面会自动刷新。"
|
||||
: "如果任务失败,可以直接重新生成导入建议。"
|
||||
));
|
||||
}
|
||||
|
||||
JSONArray appliedProjectNames = draft.optJSONArray("appliedProjectNames");
|
||||
if (appliedProjectNames != null && appliedProjectNames.length() > 0) {
|
||||
appendContent(BossUi.buildCard(
|
||||
@@ -194,7 +234,14 @@ public class DeviceImportDraftActivity extends BossScreenActivity {
|
||||
}
|
||||
|
||||
Button reviewButton = BossUi.buildMiniActionButton(this, "生成导入建议", true);
|
||||
reviewButton.setEnabled(!selectedCandidateIds.isEmpty());
|
||||
reviewButton.setEnabled(!selectedCandidateIds.isEmpty() && !isReviewPending(draft, resolution, reviewTask));
|
||||
if (isReviewPending(draft, resolution, reviewTask)) {
|
||||
reviewButton.setText("主 Agent 审核中");
|
||||
} else if (reviewTask != null && "failed".equals(reviewTask.optString("status", ""))) {
|
||||
reviewButton.setText("重新生成导入建议");
|
||||
} else if ("resolved".equals(draft.optString("status", "")) || "applied".equals(draft.optString("status", ""))) {
|
||||
reviewButton.setText("重新生成导入建议");
|
||||
}
|
||||
reviewButton.setOnClickListener(v -> reviewSelection());
|
||||
Button clearButton = BossUi.buildMiniActionButton(this, "清空勾选", false);
|
||||
clearButton.setEnabled(!selectedCandidateIds.isEmpty());
|
||||
@@ -208,6 +255,9 @@ public class DeviceImportDraftActivity extends BossScreenActivity {
|
||||
applyButton.setOnClickListener(v -> applyResolution());
|
||||
appendContent(BossUi.buildInlineActionRow(this, reviewButton, clearButton, applyButton));
|
||||
setRefreshing(false);
|
||||
if (isReviewPending(draft, resolution, reviewTask)) {
|
||||
contentLayout.postDelayed(reviewPollRunnable, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveStatusTitle(@Nullable JSONObject draft) {
|
||||
@@ -233,7 +283,7 @@ public class DeviceImportDraftActivity extends BossScreenActivity {
|
||||
return "导入草稿";
|
||||
}
|
||||
|
||||
private String resolveStatusBody(@Nullable JSONObject draft, @Nullable JSONObject resolution) {
|
||||
private String resolveStatusBody(@Nullable JSONObject draft, @Nullable JSONObject resolution, @Nullable JSONObject reviewTask) {
|
||||
if (draft == null) {
|
||||
return "先让设备完成首次 heartbeat 并上报候选线程,导入草稿就会出现在这里。";
|
||||
}
|
||||
@@ -245,6 +295,12 @@ public class DeviceImportDraftActivity extends BossScreenActivity {
|
||||
return "先勾选想导入的线程,再生成导入建议。";
|
||||
}
|
||||
if ("pending_resolution".equals(status)) {
|
||||
if (isReviewPending(draft, resolution, reviewTask)) {
|
||||
return "勾选已保存,主 Agent 正在整理导入建议,页面会自动刷新。";
|
||||
}
|
||||
if (reviewTask != null && "failed".equals(reviewTask.optString("status", ""))) {
|
||||
return "主 Agent 这次没能生成导入建议。可以稍后重新生成,当前勾选会保留。";
|
||||
}
|
||||
return "勾选已保存,接下来会生成导入建议。";
|
||||
}
|
||||
if ("resolved".equals(status)) {
|
||||
@@ -306,14 +362,21 @@ public class DeviceImportDraftActivity extends BossScreenActivity {
|
||||
throw new IllegalStateException(reviewResponse.message());
|
||||
}
|
||||
runOnUiThread(() -> {
|
||||
showMessage("已生成导入建议");
|
||||
applyPayload(reviewResponse.json.optJSONObject("draft"), reviewResponse.json.optJSONObject("resolution"));
|
||||
boolean hasResolution = reviewResponse.json.optJSONObject("resolution") != null;
|
||||
showMessage(hasResolution ? "已生成导入建议" : "已提交给主 Agent 审核");
|
||||
applyPayload(
|
||||
reviewResponse.json.optJSONObject("draft"),
|
||||
reviewResponse.json.optJSONObject("resolution"),
|
||||
reviewResponse.json.optJSONObject("reviewTask") != null
|
||||
? reviewResponse.json.optJSONObject("reviewTask")
|
||||
: reviewResponse.json.optJSONObject("task")
|
||||
);
|
||||
});
|
||||
} catch (Exception error) {
|
||||
final JSONObject fallbackDraft = selectedDraft;
|
||||
runOnUiThread(() -> {
|
||||
if (fallbackDraft != null) {
|
||||
applyPayload(fallbackDraft, null);
|
||||
applyPayload(fallbackDraft, null, null);
|
||||
} else {
|
||||
setRefreshing(false);
|
||||
}
|
||||
@@ -337,7 +400,7 @@ public class DeviceImportDraftActivity extends BossScreenActivity {
|
||||
}
|
||||
runOnUiThread(() -> {
|
||||
showMessage("已清空当前勾选");
|
||||
applyPayload(response.json.optJSONObject("draft"), null);
|
||||
applyPayload(response.json.optJSONObject("draft"), null, null);
|
||||
});
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
@@ -362,7 +425,7 @@ public class DeviceImportDraftActivity extends BossScreenActivity {
|
||||
}
|
||||
runOnUiThread(() -> {
|
||||
showMessage("已应用导入");
|
||||
applyPayload(response.json.optJSONObject("draft"), response.json.optJSONObject("resolution"));
|
||||
applyPayload(response.json.optJSONObject("draft"), response.json.optJSONObject("resolution"), null);
|
||||
});
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
|
||||
@@ -36,6 +36,7 @@ public class DeviceImportDraftActivityTest {
|
||||
activity,
|
||||
"applyPayload",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildPendingDraft()),
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, null),
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, null)
|
||||
);
|
||||
|
||||
@@ -62,7 +63,8 @@ public class DeviceImportDraftActivityTest {
|
||||
activity,
|
||||
"applyPayload",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildAppliedDraft()),
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildAppliedResolution())
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildAppliedResolution()),
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, null)
|
||||
);
|
||||
|
||||
View content = activity.findViewById(R.id.screen_content);
|
||||
@@ -73,6 +75,32 @@ public class DeviceImportDraftActivityTest {
|
||||
assertTrue(viewTreeContainsText(content, "已导入"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void renderCurrentStateShowsQueuedReviewTaskCopy() throws Exception {
|
||||
TestDeviceImportDraftActivity activity = Robolectric
|
||||
.buildActivity(
|
||||
TestDeviceImportDraftActivity.class,
|
||||
new Intent()
|
||||
.putExtra(DeviceImportDraftActivity.EXTRA_DEVICE_ID, "device-1")
|
||||
.putExtra(DeviceImportDraftActivity.EXTRA_DEVICE_NAME, "Mac Studio")
|
||||
)
|
||||
.setup()
|
||||
.get();
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"applyPayload",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildPendingResolutionDraft()),
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, null),
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildQueuedReviewTask())
|
||||
);
|
||||
|
||||
View content = activity.findViewById(R.id.screen_content);
|
||||
assertTrue(viewTreeContainsText(content, "主 Agent 审核中"));
|
||||
assertTrue(viewTreeContainsText(content, "审核任务"));
|
||||
assertTrue(viewTreeContainsText(content, "状态:queued"));
|
||||
}
|
||||
|
||||
private static JSONObject buildPendingDraft() throws Exception {
|
||||
return new JSONObject()
|
||||
.put("draftId", "draft-1")
|
||||
@@ -125,6 +153,24 @@ public class DeviceImportDraftActivityTest {
|
||||
.put("suggestedImport", true)));
|
||||
}
|
||||
|
||||
private static JSONObject buildPendingResolutionDraft() throws Exception {
|
||||
return new JSONObject()
|
||||
.put("draftId", "draft-1")
|
||||
.put("deviceId", "device-1")
|
||||
.put("status", "pending_resolution")
|
||||
.put("selectedCandidateIds", new JSONArray().put("candidate-1"))
|
||||
.put("appliedProjectNames", new JSONArray())
|
||||
.put("candidates", new JSONArray()
|
||||
.put(new JSONObject()
|
||||
.put("candidateId", "candidate-1")
|
||||
.put("deviceId", "device-1")
|
||||
.put("folderName", "北区试产线")
|
||||
.put("threadId", "thread-1")
|
||||
.put("threadDisplayName", "北区试产线回归")
|
||||
.put("lastActiveAt", "2026-03-30T10:18:00+08:00")
|
||||
.put("suggestedImport", true)));
|
||||
}
|
||||
|
||||
private static JSONObject buildAppliedResolution() throws Exception {
|
||||
return new JSONObject()
|
||||
.put("resolutionId", "resolution-1")
|
||||
@@ -147,6 +193,13 @@ public class DeviceImportDraftActivityTest {
|
||||
.put("reason", "作为独立聊天窗口导入。")));
|
||||
}
|
||||
|
||||
private static JSONObject buildQueuedReviewTask() throws Exception {
|
||||
return new JSONObject()
|
||||
.put("taskId", "mastertask-1")
|
||||
.put("taskType", "device_import_resolution")
|
||||
.put("status", "queued");
|
||||
}
|
||||
|
||||
private static boolean viewTreeContainsText(View root, String expectedText) {
|
||||
if (root instanceof TextView) {
|
||||
CharSequence text = ((TextView) root).getText();
|
||||
|
||||
Reference in New Issue
Block a user