feat: queue device import review tasks

This commit is contained in:
kris
2026-03-31 22:38:57 +08:00
parent dcbff3cc7d
commit 87ffe19f78
7 changed files with 347 additions and 117 deletions

View File

@@ -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(() -> {