chore: checkpoint Boss app v2.5.11

This commit is contained in:
AI Bot
2026-06-08 12:22:50 +08:00
parent bddbe8b5ba
commit 3b51641d99
78 changed files with 5706 additions and 954 deletions

View File

@@ -172,11 +172,23 @@ public class BossApiClient {
}
public ApiResponse getProjectDetail(String projectId) throws IOException, JSONException {
return requestWithRestore("GET", "/api/v1/projects/" + encode(projectId), null);
return requestWithRestoreRaw(
"GET",
"/api/v1/projects/" + encode(projectId),
null,
DEFAULT_CONNECT_TIMEOUT_MS,
CONVERSATIONS_READ_TIMEOUT_MS
);
}
public ApiResponse getProjectMessages(String projectId) throws IOException, JSONException {
return requestWithRestore("GET", "/api/v1/projects/" + encode(projectId) + "/messages", null);
return requestWithRestoreRaw(
"GET",
"/api/v1/projects/" + encode(projectId) + "/messages",
null,
DEFAULT_CONNECT_TIMEOUT_MS,
CONVERSATIONS_READ_TIMEOUT_MS
);
}
public ApiResponse getDispatchPlans(String projectId) throws IOException, JSONException {
@@ -560,6 +572,14 @@ public class BossApiClient {
return requestWithRestore("GET", "/api/v1/devices/" + encode(deviceId) + "/skills", null);
}
public ApiResponse getSkillLifecycleRequests() throws IOException, JSONException {
return requestWithRestore("GET", "/api/v1/admin/skills/requests", null);
}
public ApiResponse createSkillLifecycleRequest(JSONObject payload) throws IOException, JSONException {
return requestWithRestore("POST", "/api/v1/admin/skills/requests", payload == null ? new JSONObject() : payload);
}
public ApiResponse getDeviceEnrollments() throws IOException, JSONException {
return requestWithRestore("GET", "/api/v1/devices/enrollments", null);
}

View File

@@ -29,6 +29,7 @@ public final class BossMarkdown {
private static final Pattern BULLET_PATTERN = Pattern.compile("^[-*]\\s+(.+)$");
private static final Pattern ORDERED_PATTERN = Pattern.compile("^(\\d+)\\.\\s+(.+)$");
private static final Pattern LABEL_SECTION_PATTERN = Pattern.compile("^([^:\\n]{1,24})[:]\\s*(.+)$");
private static final Pattern MARKDOWN_LINK_PATTERN = Pattern.compile("\\[([^\\]\\n]{1,90})\\]\\((https?://[^\\s)]+)\\)");
private static final Pattern INLINE_TOKEN_PATTERN = Pattern.compile("(\\*\\*([^*]+)\\*\\*)|(`([^`]+)`)");;
private static final LruCache<String, CharSequence> RENDER_CACHE = new LruCache<>(180);
@@ -45,7 +46,7 @@ public final class BossMarkdown {
}
Palette palette = Palette.resolve(context, outgoing);
SpannableStringBuilder builder = new SpannableStringBuilder();
String normalized = markdown.replace("\r\n", "\n").replace('\r', '\n');
String normalized = normalizeMarkdownLinks(markdown).replace("\r\n", "\n").replace('\r', '\n');
String[] lines = normalized.split("\n", -1);
boolean inCodeFence = false;
List<String> codeLines = new ArrayList<>();
@@ -117,6 +118,21 @@ public final class BossMarkdown {
return (outgoing ? "out" : "in") + "|" + uiMode + "|" + markdown;
}
private static String normalizeMarkdownLinks(String markdown) {
Matcher matcher = MARKDOWN_LINK_PATTERN.matcher(markdown);
StringBuffer buffer = new StringBuffer();
while (matcher.find()) {
String label = matcher.group(1) == null ? "链接" : matcher.group(1).trim();
label = label.replace("`", "").trim();
if (TextUtils.isEmpty(label)) {
label = "链接";
}
matcher.appendReplacement(buffer, Matcher.quoteReplacement(label));
}
matcher.appendTail(buffer);
return buffer.toString();
}
private static void appendHeading(SpannableStringBuilder builder, String text, int level, Palette palette) {
ensureBlockSeparation(builder, true);
int start = builder.length();

View File

@@ -136,6 +136,7 @@ final class BossRealtimeClient {
connection.setRequestProperty("Accept", "text/event-stream");
connection.setRequestProperty("Cache-Control", "no-cache");
connection.setRequestProperty("x-boss-native-app", "1");
connection.setRequestProperty("x-boss-realtime-capabilities", "message-patch-v1");
String cookie = apiClient.getSessionCookie();
if (!cookie.isEmpty()) {
connection.setRequestProperty("Cookie", cookie);

View File

@@ -1340,6 +1340,7 @@ public final class BossUi {
card.addView(titleRow);
String phase = progress == null ? "" : progress.optString("phase", "").trim();
String progressStatus = progress == null ? "" : progress.optString("status", "").trim();
if (!TextUtils.isEmpty(phase)) {
TextView phaseView = secondaryText(context, "当前状态:" + executionPhaseLabel(phase));
phaseView.setPadding(0, dp(context, 8), 0, 0);
@@ -1872,9 +1873,14 @@ public final class BossUi {
}
}
card.addView(divider(context));
card.addView(sectionTitle(context, "分支详情"));
JSONObject branch = progress == null ? null : progress.optJSONObject("branch");
boolean shouldShowBranchFallback = TextUtils.isEmpty(progressStatus) ||
"queued".equals(progressStatus) ||
"running".equals(progressStatus);
if (branch != null || shouldShowBranchFallback) {
card.addView(divider(context));
card.addView(sectionTitle(context, "分支详情"));
}
if (branch != null) {
String changeText = formatChangeText(branch);
if (!TextUtils.isEmpty(changeText)) {
@@ -1888,7 +1894,7 @@ public final class BossUi {
} else if ("available".equals(ghStatus)) {
card.addView(detailRow(context, "", "GitHub CLI 可用", "", false));
}
} else {
} else if (shouldShowBranchFallback) {
card.addView(detailRow(context, "", "Git 操作", "等待执行器回写", false));
}

View File

@@ -128,6 +128,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
private boolean pendingReload;
private boolean pendingReloadMessagesOnly;
private boolean pendingReloadForcedScrollToBottom;
private boolean pendingReloadShowRefreshIndicator;
private volatile boolean activityDestroyed;
private volatile boolean markConversationReadInFlight;
private @Nullable AlertDialog activeDialogGuardDialog;
@@ -141,7 +142,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
return;
}
if (!reloadInFlight && !isComposerBusy() && shouldAutoRefreshConversation()) {
reload(false);
reloadInBackground(false);
}
armConversationAutoRefresh();
}
@@ -543,6 +544,10 @@ public class ProjectDetailActivity extends BossScreenActivity {
if (event == null || !"project.messages.updated".equals(event.eventName)) {
return false;
}
JSONObject projectMessagesPatch = event.payload.optJSONObject("projectMessagesPatch");
if (projectMessagesPatch != null) {
return tryApplyRealtimeMessageWindowPatch(projectMessagesPatch);
}
JSONObject projectMessagesPayload = event.payload.optJSONObject("projectMessagesPayload");
if (projectMessagesPayload == null) {
return false;
@@ -565,6 +570,167 @@ public class ProjectDetailActivity extends BossScreenActivity {
return true;
}
private boolean tryApplyRealtimeMessageWindowPatch(JSONObject projectMessagesPatch) {
if (projectMessagesPatch == null || !"message_window".equals(projectMessagesPatch.optString("patchKind", ""))) {
return false;
}
if (currentRenderedProjectPayload == null || contentLayout == null) {
scheduleRealtimeReload(false);
return true;
}
if (selectionState != null && selectionState.multiSelecting) {
scheduleRealtimeReload(false);
return true;
}
JSONObject patchProject = projectMessagesPatch.optJSONObject("project");
JSONArray patchMessages = projectMessagesPatch.optJSONArray("messages");
if (patchProject == null || patchMessages == null) {
scheduleRealtimeReload(false);
return true;
}
JSONObject currentPayload = copyJson(currentRenderedProjectPayload);
JSONObject currentProject = currentPayload.optJSONObject("project");
if (currentProject == null || !TextUtils.equals(
currentProject.optString("id", "").trim(),
projectMessagesPatch.optString("projectId", "").trim()
)) {
scheduleRealtimeReload(false);
return true;
}
JSONArray currentMessages = currentProject.optJSONArray("messages");
if (currentMessages == null) {
scheduleRealtimeReload(false);
return true;
}
int expectedMessageCount = projectMessagesPatch.optInt("messageCount", currentMessages.length());
if (expectedMessageCount < currentMessages.length()) {
scheduleRealtimeReload(false);
return true;
}
PatchMergeResult mergeResult = mergeMessageWindowPatch(currentMessages, patchMessages, expectedMessageCount);
if (!mergeResult.ok) {
scheduleRealtimeReload(false);
return true;
}
try {
copyProjectPatchMetadata(currentProject, patchProject);
currentProject.put("messages", mergeResult.messages);
JSONArray devices = projectMessagesPatch.optJSONArray("devices");
if (devices != null) {
currentPayload.put("devices", devices);
}
} catch (org.json.JSONException ignored) {
scheduleRealtimeReload(false);
return true;
}
JSONObject nextPayload = currentPayload;
runOnUiThread(() -> {
renderNearBottom = isChatNearBottom();
renderForcedScrollToBottom = false;
renderLoadedProjectSnapshot(new ProjectSnapshot(nextPayload, null, null));
});
return true;
}
private PatchMergeResult mergeMessageWindowPatch(
JSONArray currentMessages,
JSONArray patchMessages,
int expectedMessageCount
) {
JSONArray nextMessages = new JSONArray();
for (int i = 0; i < currentMessages.length(); i++) {
JSONObject message = currentMessages.optJSONObject(i);
if (message != null) {
nextMessages.put(copyJson(message));
}
}
boolean changed = false;
Map<String, Integer> currentIndexById = new HashMap<>();
for (int i = 0; i < nextMessages.length(); i++) {
JSONObject message = nextMessages.optJSONObject(i);
String messageId = message == null ? "" : message.optString("id", "");
if (!TextUtils.isEmpty(messageId)) {
currentIndexById.put(messageId, i);
}
}
String currentLastId = latestMessageId(nextMessages);
int currentLastIndexInPatch = -1;
for (int i = 0; i < patchMessages.length(); i++) {
JSONObject patchMessage = patchMessages.optJSONObject(i);
String patchMessageId = patchMessage == null ? "" : patchMessage.optString("id", "");
if (TextUtils.isEmpty(patchMessageId)) {
continue;
}
Integer existingIndex = currentIndexById.get(patchMessageId);
if (existingIndex != null) {
JSONObject existing = nextMessages.optJSONObject(existingIndex);
if (existing == null || !TextUtils.equals(existing.toString(), patchMessage.toString())) {
try {
nextMessages.put(existingIndex, copyJson(patchMessage));
changed = true;
} catch (org.json.JSONException ignored) {
return PatchMergeResult.failed();
}
}
}
if (TextUtils.equals(currentLastId, patchMessageId)) {
currentLastIndexInPatch = i;
}
}
if (expectedMessageCount > nextMessages.length()) {
if (currentLastIndexInPatch < 0) {
return PatchMergeResult.failed();
}
for (int i = currentLastIndexInPatch + 1; i < patchMessages.length(); i++) {
JSONObject patchMessage = patchMessages.optJSONObject(i);
String patchMessageId = patchMessage == null ? "" : patchMessage.optString("id", "");
if (TextUtils.isEmpty(patchMessageId) || currentIndexById.containsKey(patchMessageId)) {
continue;
}
nextMessages.put(copyJson(patchMessage));
currentIndexById.put(patchMessageId, nextMessages.length() - 1);
changed = true;
}
}
if (nextMessages.length() != expectedMessageCount) {
return PatchMergeResult.failed();
}
return new PatchMergeResult(true, changed, nextMessages);
}
private String latestMessageId(@Nullable JSONArray messages) {
if (messages == null || messages.length() == 0) {
return "";
}
JSONObject message = messages.optJSONObject(messages.length() - 1);
return message == null ? "" : message.optString("id", "");
}
private void copyProjectPatchMetadata(JSONObject currentProject, JSONObject patchProject) {
putIfPresent(currentProject, patchProject, "name");
putIfPresent(currentProject, patchProject, "threadMeta");
putIfPresent(currentProject, patchProject, "unreadCount");
putIfPresent(currentProject, patchProject, "isGroup");
putIfPresent(currentProject, patchProject, "collaborationMode");
putIfPresent(currentProject, patchProject, "approvalState");
putIfPresent(currentProject, patchProject, "lightDispatchReminderEnabled");
putIfPresent(currentProject, patchProject, "lastMessageAt");
putIfPresent(currentProject, patchProject, "updatedAt");
}
private void putIfPresent(JSONObject target, JSONObject source, String key) {
if (target == null || source == null || !source.has(key)) {
return;
}
try {
target.put(key, source.opt(key));
} catch (org.json.JSONException ignored) {
}
}
private boolean trySkipUnchangedRealtimeMessagesPatch(JSONObject projectMessagesPayload) {
if (currentRenderedProjectPayload == null || projectMessagesPayload == null) {
return false;
@@ -939,7 +1105,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
void triggerRealtimeReload(boolean requireFullSnapshot) {
if (requireFullSnapshot) {
reload();
reloadInBackground(false);
return;
}
reloadMessagesOnly();
@@ -960,14 +1126,22 @@ public class ProjectDetailActivity extends BossScreenActivity {
}
private void reload(boolean forcedScrollToBottom) {
reloadSnapshot(forcedScrollToBottom, false);
reloadSnapshot(forcedScrollToBottom, false, true);
}
private void reloadInBackground(boolean forcedScrollToBottom) {
reloadSnapshot(forcedScrollToBottom, false, false);
}
private void reloadMessagesOnly() {
reloadSnapshot(false, true);
reloadSnapshot(false, true, false);
}
private void reloadSnapshot(boolean forcedScrollToBottom, boolean messagesOnly) {
reloadSnapshot(forcedScrollToBottom, messagesOnly, true);
}
private void reloadSnapshot(boolean forcedScrollToBottom, boolean messagesOnly, boolean showRefreshIndicator) {
if (shouldSkipAsyncUiWork()) {
return;
}
@@ -983,12 +1157,15 @@ public class ProjectDetailActivity extends BossScreenActivity {
}
pendingReload = true;
pendingReloadForcedScrollToBottom = pendingReloadForcedScrollToBottom || forcedScrollToBottom;
pendingReloadShowRefreshIndicator = pendingReloadShowRefreshIndicator || showRefreshIndicator;
return;
}
renderNearBottom = isChatNearBottom();
renderForcedScrollToBottom = forcedScrollToBottom;
reloadInFlight = true;
setRefreshing(true);
if (showRefreshIndicator) {
setRefreshing(true);
}
try {
executor.execute(() -> {
try {
@@ -1004,7 +1181,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
if (messagesOnly) {
reloadInFlight = false;
setRefreshing(false);
reload(forcedScrollToBottom);
reloadSnapshot(forcedScrollToBottom, false, showRefreshIndicator);
return;
}
handleProjectReloadFailure(error);
@@ -1014,7 +1191,9 @@ public class ProjectDetailActivity extends BossScreenActivity {
});
} catch (RejectedExecutionException ignored) {
reloadInFlight = false;
setRefreshing(false);
if (showRefreshIndicator) {
setRefreshing(false);
}
}
}
@@ -1062,10 +1241,12 @@ public class ProjectDetailActivity extends BossScreenActivity {
}
boolean forcedScrollToBottom = pendingReloadForcedScrollToBottom;
boolean messagesOnly = pendingReloadMessagesOnly;
boolean showRefreshIndicator = pendingReloadShowRefreshIndicator;
pendingReload = false;
pendingReloadMessagesOnly = false;
pendingReloadForcedScrollToBottom = false;
reloadSnapshot(forcedScrollToBottom, messagesOnly);
pendingReloadShowRefreshIndicator = false;
reloadSnapshot(forcedScrollToBottom, messagesOnly, showRefreshIndicator);
}
@Override
@@ -1271,7 +1452,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
&& !reloadInFlight
&& refreshLayout != null
&& !isComposerBusy()) {
reload();
reloadInBackground(false);
}
updateConversationAutoRefresh();
}
@@ -4110,4 +4291,20 @@ public class ProjectDetailActivity extends BossScreenActivity {
return kind;
}
}
private static final class PatchMergeResult {
final boolean ok;
final boolean changed;
final JSONArray messages;
PatchMergeResult(boolean ok, boolean changed, JSONArray messages) {
this.ok = ok;
this.changed = changed;
this.messages = messages == null ? new JSONArray() : messages;
}
static PatchMergeResult failed() {
return new PatchMergeResult(false, false, new JSONArray());
}
}
}

View File

@@ -1,12 +1,18 @@
package com.hyzq.boss;
import android.app.AlertDialog;
import android.content.Intent;
import android.os.Bundle;
import android.text.InputType;
import android.text.TextUtils;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.LinkedHashMap;
@@ -58,8 +64,18 @@ public class SkillInventoryActivity extends BossScreenActivity {
String targetDeviceId = resolveTargetDeviceId();
BossApiClient.ApiResponse response = apiClient.getDeviceSkills(targetDeviceId);
if (!response.ok()) throw new IllegalStateException(response.message());
JSONObject lifecyclePayload = null;
try {
BossApiClient.ApiResponse lifecycleResponse = apiClient.getSkillLifecycleRequests();
if (lifecycleResponse.ok()) {
lifecyclePayload = lifecycleResponse.json;
}
} catch (Exception ignored) {
lifecyclePayload = null;
}
deviceId = targetDeviceId;
runOnUiThread(() -> renderSkills(response.json));
JSONObject finalLifecyclePayload = lifecyclePayload;
runOnUiThread(() -> renderSkills(response.json, finalLifecyclePayload));
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
@@ -203,9 +219,14 @@ public class SkillInventoryActivity extends BossScreenActivity {
}
private void renderSkills(JSONObject payload) {
renderSkills(payload, null);
}
private void renderSkills(JSONObject payload, @Nullable JSONObject lifecyclePayload) {
replaceContent();
JSONObject device = payload.optJSONObject("device");
JSONArray skills = payload.optJSONArray("skills");
boolean canManageLifecycle = lifecyclePayload != null;
if (device != null) {
deviceName = device.optString("name", deviceId);
@@ -220,6 +241,10 @@ public class SkillInventoryActivity extends BossScreenActivity {
));
}
if (canManageLifecycle) {
appendSkillManagementWorkspace(lifecyclePayload);
}
if (skills == null || skills.length() == 0) {
appendContent(BossUi.buildEmptyCard(this, "当前设备还没有同步 Skill。"));
setRefreshing(false);
@@ -244,8 +269,217 @@ public class SkillInventoryActivity extends BossScreenActivity {
Button copyPath = BossUi.buildMiniActionButton(this, "复制路径", false);
copyPath.setOnClickListener(v -> BossUi.copyText(this, "Skill 路径", skill.optString("path", "")));
card.addView(BossUi.buildInlineActionRow(this, copyInvocation, copyPath));
if (canManageLifecycle) {
Button update = BossUi.buildMiniActionButton(this, "更新下发", true);
update.setOnClickListener(v -> queueSkillLifecycleRequest("update", skill, null, null, null, null, null));
Button rollback = BossUi.buildMiniActionButton(this, "回滚", false);
rollback.setOnClickListener(v -> showVersionedSkillRequestDialog("rollback", skill, "回滚", "rollbackToVersion"));
Button versionLock = BossUi.buildMiniActionButton(this, "版本锁定", false);
versionLock.setOnClickListener(v -> showVersionedSkillRequestDialog("version_lock", skill, "版本锁定", "lockedVersion"));
card.addView(BossUi.buildInlineActionRow(this, update, rollback, versionLock));
}
appendContent(card);
}
setRefreshing(false);
}
private void appendSkillManagementWorkspace(JSONObject lifecyclePayload) {
JSONArray requests = lifecyclePayload.optJSONArray("requests");
int requestCount = requests == null ? 0 : requests.length();
int queuedCount = countRequestsByStatus(requests, "queued");
int runningCount = countRunningRequests(requests);
LinearLayout card = new LinearLayout(this);
card.setOrientation(LinearLayout.VERTICAL);
card.addView(BossUi.buildWechatMenuRow(
this,
"Skill 管理分发",
"安装、更新、回滚、版本锁定和账号权限分配统一在这里处理。",
"Skill 请求状态:待执行 " + queuedCount + " · 执行中 " + runningCount + " · 最近请求 " + requestCount,
null,
null
));
Button installRemote = BossUi.buildMiniActionButton(this, "安装远端 Skill", true);
installRemote.setOnClickListener(v -> showInstallSkillDialog());
Button grantPermission = BossUi.buildMiniActionButton(this, "分配权限", false);
grantPermission.setOnClickListener(v -> startActivity(new Intent(this, AccessManagementActivity.class)));
card.addView(BossUi.buildInlineActionRow(this, installRemote, grantPermission));
if (requests != null && requests.length() > 0) {
int maxRows = Math.min(3, requests.length());
for (int index = 0; index < maxRows; index += 1) {
JSONObject request = requests.optJSONObject(index);
if (request == null) continue;
card.addView(BossUi.buildWechatMenuRow(
this,
"Skill 请求状态",
request.optString("action", "-") + " · " + request.optString("status", "-"),
request.optString("skillId", request.optString("sourceUrl", "-"))
+ " · " + request.optString("requestedAt", "-"),
null,
null
));
}
}
appendContent(card);
}
private void showInstallSkillDialog() {
LinearLayout form = new LinearLayout(this);
form.setOrientation(LinearLayout.VERTICAL);
int padding = BossUi.dp(this, 12);
form.setPadding(padding, padding, padding, 0);
EditText sourceUrl = buildSingleLineInput("Git URL 或可信来源 URL");
EditText targetVersion = buildSingleLineInput("目标版本,可选");
EditText checksum = buildSingleLineInput("SHA256 校验和,可选");
form.addView(sourceUrl);
form.addView(targetVersion);
form.addView(checksum);
new AlertDialog.Builder(this)
.setTitle("安装远端 Skill")
.setView(form)
.setNegativeButton("取消", null)
.setPositiveButton("下发", (dialog, which) -> {
String source = sourceUrl.getText().toString().trim();
if (TextUtils.isEmpty(source)) {
showMessage("请输入 Skill 来源 URL");
return;
}
queueSkillLifecycleRequest(
"install",
null,
source,
targetVersion.getText().toString().trim(),
checksum.getText().toString().trim(),
null,
null
);
})
.show();
}
private void showVersionedSkillRequestDialog(
String action,
JSONObject skill,
String title,
String versionField
) {
EditText input = buildSingleLineInput("请输入版本号");
new AlertDialog.Builder(this)
.setTitle(title)
.setView(input)
.setNegativeButton("取消", null)
.setPositiveButton("下发", (dialog, which) -> {
String version = input.getText().toString().trim();
if (TextUtils.isEmpty(version)) {
showMessage("请输入版本号");
return;
}
if ("rollbackToVersion".equals(versionField)) {
queueSkillLifecycleRequest(action, skill, null, null, null, version, null);
} else {
queueSkillLifecycleRequest(action, skill, null, null, null, null, version);
}
})
.show();
}
private EditText buildSingleLineInput(String hint) {
EditText input = new EditText(this);
input.setHint(hint);
input.setSingleLine(true);
input.setInputType(InputType.TYPE_CLASS_TEXT);
int verticalPadding = BossUi.dp(this, 8);
input.setPadding(0, verticalPadding, 0, verticalPadding);
return input;
}
private void queueSkillLifecycleRequest(
String action,
@Nullable JSONObject skill,
@Nullable String sourceUrl,
@Nullable String targetVersion,
@Nullable String checksum,
@Nullable String rollbackToVersion,
@Nullable String lockedVersion
) {
try {
JSONObject payload = new JSONObject();
payload.put("action", action);
payload.put("deviceId", deviceId == null ? "" : deviceId);
if (skill != null) {
putIfNotBlank(payload, "skillId", skill.optString("skillId", ""));
}
putIfNotBlank(payload, "sourceUrl", sourceUrl);
putIfNotBlank(payload, "targetVersion", targetVersion);
putIfNotBlank(payload, "checksum", checksum);
putIfNotBlank(payload, "rollbackToVersion", rollbackToVersion);
putIfNotBlank(payload, "lockedVersion", lockedVersion);
putIfNotBlank(payload, "note", "boss-app-skill-management");
submitSkillLifecycleRequest(payload);
} catch (JSONException error) {
showMessage("Skill 请求构建失败:" + error.getMessage());
}
}
private void putIfNotBlank(JSONObject payload, String key, @Nullable String value) throws JSONException {
if (!TextUtils.isEmpty(value)) {
payload.put(key, value);
}
}
private void submitSkillLifecycleRequest(JSONObject payload) {
setRefreshing(true);
executor.execute(() -> {
try {
BossApiClient.ApiResponse response = apiClient.createSkillLifecycleRequest(payload);
if (!response.ok()) throw new IllegalStateException(response.message());
runOnUiThread(() -> {
showMessage("Skill 请求已下发");
reload();
});
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
showMessage("Skill 请求失败:" + error.getMessage());
});
}
});
}
private static int countRequestsByStatus(@Nullable JSONArray requests, String status) {
if (requests == null) {
return 0;
}
int count = 0;
for (int index = 0; index < requests.length(); index += 1) {
JSONObject request = requests.optJSONObject(index);
if (request != null && status.equalsIgnoreCase(request.optString("status", ""))) {
count += 1;
}
}
return count;
}
private static int countRunningRequests(@Nullable JSONArray requests) {
if (requests == null) {
return 0;
}
int count = 0;
for (int index = 0; index < requests.length(); index += 1) {
JSONObject request = requests.optJSONObject(index);
if (request == null) continue;
String status = request.optString("status", "");
if ("claimed".equalsIgnoreCase(status)
|| "running".equalsIgnoreCase(status)
|| "processing".equalsIgnoreCase(status)) {
count += 1;
}
}
return count;
}
}