From 13c67425ab3e3fa1cf70bb3892a61599c82ad24e Mon Sep 17 00:00:00 2001 From: kris Date: Sat, 28 Mar 2026 08:29:05 +0800 Subject: [PATCH] refactor: isolate forward payload serialization --- .../java/com/hyzq/boss/BossApiClient.java | 21 +- .../java/com/hyzq/boss/ForwardPayloads.java | 248 ++++++++++++++++++ .../com/hyzq/boss/ForwardTargetActivity.java | 192 +------------- .../hyzq/boss/ForwardTargetActivityTest.java | 27 +- 4 files changed, 293 insertions(+), 195 deletions(-) create mode 100644 android/app/src/main/java/com/hyzq/boss/ForwardPayloads.java diff --git a/android/app/src/main/java/com/hyzq/boss/BossApiClient.java b/android/app/src/main/java/com/hyzq/boss/BossApiClient.java index b7499a8..84dba9b 100644 --- a/android/app/src/main/java/com/hyzq/boss/BossApiClient.java +++ b/android/app/src/main/java/com/hyzq/boss/BossApiClient.java @@ -4,6 +4,8 @@ import android.content.Context; import android.content.SharedPreferences; import android.net.Uri; +import androidx.annotation.Nullable; + import org.json.JSONException; import org.json.JSONObject; @@ -95,9 +97,8 @@ public class BossApiClient { } public ApiResponse forwardProjectMessage(String projectId, String targetProjectId, JSONObject payload) throws IOException, JSONException { - JSONObject requestPayload = payload == null ? new JSONObject() : payload; - requestPayload.put("targetProjectId", targetProjectId); - return requestWithRestore("POST", "/api/v1/projects/" + encode(projectId) + "/forwards", requestPayload); + String requestBody = ForwardPayloads.toRequestBody(targetProjectId, payload); + return requestWithRestoreRaw("POST", "/api/v1/projects/" + encode(projectId) + "/forwards", requestBody); } public ApiResponse getThreadDetail(String threadId) throws IOException, JSONException { @@ -244,17 +245,25 @@ public class BossApiClient { } private ApiResponse requestWithRestore(String method, String path, JSONObject body) throws IOException, JSONException { - ApiResponse response = request(method, path, body, true); + return requestWithRestoreRaw(method, path, body == null ? null : body.toString()); + } + + private ApiResponse requestWithRestoreRaw(String method, String path, @Nullable String body) throws IOException, JSONException { + ApiResponse response = requestRaw(method, path, body, true); if (response.statusCode == 401 && !getRestoreToken().isEmpty()) { ApiResponse restored = restoreSession(); if (restored.ok()) { - return request(method, path, body, true); + return requestRaw(method, path, body, true); } } return response; } private ApiResponse request(String method, String path, JSONObject body, boolean expectProtected) throws IOException, JSONException { + return requestRaw(method, path, body == null ? null : body.toString(), expectProtected); + } + + private ApiResponse requestRaw(String method, String path, @Nullable String body, boolean expectProtected) throws IOException, JSONException { HttpURLConnection connection = (HttpURLConnection) new URL(baseUrl + path).openConnection(); connection.setRequestMethod(method); connection.setConnectTimeout(12000); @@ -274,7 +283,7 @@ public class BossApiClient { connection.setRequestProperty("Content-Type", "application/json"); try (OutputStream outputStream = connection.getOutputStream(); BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8))) { - writer.write(body.toString()); + writer.write(body); } } diff --git a/android/app/src/main/java/com/hyzq/boss/ForwardPayloads.java b/android/app/src/main/java/com/hyzq/boss/ForwardPayloads.java new file mode 100644 index 0000000..3aad6c6 --- /dev/null +++ b/android/app/src/main/java/com/hyzq/boss/ForwardPayloads.java @@ -0,0 +1,248 @@ +package com.hyzq.boss; + +import androidx.annotation.Nullable; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public final class ForwardPayloads { + private ForwardPayloads() {} + + public static JSONObject build( + String mode, + @Nullable String sourceMessageId, + @Nullable List sourceMessageIds + ) throws JSONException { + MutableJsonObject payload = new MutableJsonObject(); + String normalizedMode = isEmpty(mode) ? "single" : mode; + payload.put("mode", normalizedMode); + + if (normalizedMode.startsWith("single")) { + String resolvedSourceMessageId = sourceMessageId; + if (isEmpty(resolvedSourceMessageId) && sourceMessageIds != null && sourceMessageIds.size() == 1) { + resolvedSourceMessageId = sourceMessageIds.get(0); + } + if (isEmpty(resolvedSourceMessageId)) { + throw new JSONException("sourceMessageId required"); + } + payload.put("sourceMessageId", resolvedSourceMessageId); + return payload; + } + + MutableJsonArray orderedIds = new MutableJsonArray(); + if (sourceMessageIds != null) { + for (String messageId : sourceMessageIds) { + if (!isEmpty(messageId)) { + orderedIds.put(messageId); + } + } + } + if (orderedIds.length() == 0) { + throw new JSONException("sourceMessageIds required"); + } + payload.put("sourceMessageIds", orderedIds); + return payload; + } + + public static String toRequestBody(String targetProjectId, @Nullable JSONObject payload) throws JSONException { + MutableJsonObject requestPayload = new MutableJsonObject(); + requestPayload.put("targetProjectId", targetProjectId); + if (payload == null) { + return requestPayload.toString(); + } + + String mode = payload.optString("mode", ""); + if (!isEmpty(mode)) { + requestPayload.put("mode", mode); + } + + String sourceMessageId = payload.optString("sourceMessageId", ""); + if (!isEmpty(sourceMessageId)) { + requestPayload.put("sourceMessageId", sourceMessageId); + } + + JSONArray sourceMessageIds = payload.optJSONArray("sourceMessageIds"); + if (sourceMessageIds != null && sourceMessageIds.length() > 0) { + MutableJsonArray orderedIds = new MutableJsonArray(); + for (int i = 0; i < sourceMessageIds.length(); i++) { + String messageId = sourceMessageIds.optString(i); + if (!isEmpty(messageId)) { + orderedIds.put(messageId); + } + } + if (orderedIds.length() > 0) { + requestPayload.put("sourceMessageIds", orderedIds); + } + } + + return requestPayload.toString(); + } + + public static boolean isApprovalRequired(@Nullable JSONObject responseJson) { + return responseJson != null && responseJson.optBoolean("approvalRequired", false); + } + + private static boolean isEmpty(@Nullable String value) { + return value == null || value.length() == 0; + } + + private static final class MutableJsonObject extends JSONObject { + private final Map values = new LinkedHashMap<>(); + + @Override + public JSONObject put(String key, boolean value) { + values.put(key, value); + return this; + } + + @Override + public JSONObject put(String key, int value) { + values.put(key, value); + return this; + } + + @Override + public JSONObject put(String key, long value) { + values.put(key, value); + return this; + } + + @Override + public JSONObject put(String key, Object value) { + values.put(key, value); + return this; + } + + @Override + public String optString(String key) { + Object value = values.get(key); + return value instanceof String ? (String) value : ""; + } + + @Override + public String optString(String key, String fallback) { + String value = optString(key); + return value.isEmpty() ? fallback : value; + } + + @Override + public JSONArray optJSONArray(String key) { + Object value = values.get(key); + return value instanceof JSONArray ? (JSONArray) value : null; + } + + @Override + public boolean optBoolean(String key, boolean fallback) { + Object value = values.get(key); + return value instanceof Boolean ? (Boolean) value : fallback; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("{"); + boolean first = true; + for (Map.Entry entry : values.entrySet()) { + if (!first) { + builder.append(","); + } + first = false; + builder.append("\"").append(escape(entry.getKey())).append("\":"); + builder.append(stringify(entry.getValue())); + } + builder.append("}"); + return builder.toString(); + } + } + + private static final class MutableJsonArray extends JSONArray { + private final ArrayList values = new ArrayList<>(); + + @Override + public JSONArray put(boolean value) { + values.add(value); + return this; + } + + @Override + public JSONArray put(int value) { + values.add(value); + return this; + } + + @Override + public JSONArray put(long value) { + values.add(value); + return this; + } + + @Override + public JSONArray put(Object value) { + values.add(value); + return this; + } + + @Override + public int length() { + return values.size(); + } + + @Override + public JSONObject optJSONObject(int index) { + if (index < 0 || index >= values.size()) { + return null; + } + Object value = values.get(index); + return value instanceof JSONObject ? (JSONObject) value : null; + } + + @Override + public String optString(int index) { + if (index < 0 || index >= values.size()) { + return ""; + } + Object value = values.get(index); + return value instanceof String ? (String) value : ""; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("["); + for (int i = 0; i < values.size(); i++) { + if (i > 0) { + builder.append(","); + } + builder.append(stringify(values.get(i))); + } + builder.append("]"); + return builder.toString(); + } + } + + private static String stringify(@Nullable Object value) { + if (value == null) { + return "null"; + } + if (value instanceof String) { + return "\"" + escape((String) value) + "\""; + } + if (value instanceof Number || value instanceof Boolean) { + return String.valueOf(value); + } + return value.toString(); + } + + private static String escape(String value) { + return value + .replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t"); + } +} diff --git a/android/app/src/main/java/com/hyzq/boss/ForwardTargetActivity.java b/android/app/src/main/java/com/hyzq/boss/ForwardTargetActivity.java index 7a4b407..ee06e15 100644 --- a/android/app/src/main/java/com/hyzq/boss/ForwardTargetActivity.java +++ b/android/app/src/main/java/com/hyzq/boss/ForwardTargetActivity.java @@ -11,9 +11,7 @@ import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; public class ForwardTargetActivity extends BossScreenActivity { public static final String EXTRA_SOURCE_PROJECT_ID = "source_project_id"; @@ -98,35 +96,11 @@ public class ForwardTargetActivity extends BossScreenActivity { public static JSONObject buildForwardPayload(String mode, @Nullable String sourceMessageId, List sourceMessageIds) throws JSONException { - MutableJsonObject payload = new MutableJsonObject(); - String normalizedMode = isEmpty(mode) ? "single" : mode; - payload.put("mode", normalizedMode); + return ForwardPayloads.build(mode, sourceMessageId, sourceMessageIds); + } - if (normalizedMode.startsWith("single")) { - String resolvedSourceMessageId = sourceMessageId; - if (isEmpty(resolvedSourceMessageId) && sourceMessageIds != null && sourceMessageIds.size() == 1) { - resolvedSourceMessageId = sourceMessageIds.get(0); - } - if (isEmpty(resolvedSourceMessageId)) { - throw new JSONException("sourceMessageId required"); - } - payload.put("sourceMessageId", resolvedSourceMessageId); - return payload; - } - - MutableJsonArray orderedIds = new MutableJsonArray(); - if (sourceMessageIds != null) { - for (String messageId : sourceMessageIds) { - if (!isEmpty(messageId)) { - orderedIds.put(messageId); - } - } - } - if (orderedIds.length() == 0) { - throw new JSONException("sourceMessageIds required"); - } - payload.put("sourceMessageIds", orderedIds); - return payload; + static String resolveForwardResultMessage(JSONObject responseJson) { + return ForwardPayloads.isApprovalRequired(responseJson) ? "已提交主 Agent 审批" : "转发成功"; } private void renderTargets(List targets) { @@ -197,14 +171,9 @@ public class ForwardTargetActivity extends BossScreenActivity { if (!response.ok()) { throw new IllegalStateException(response.message()); } - boolean approvalRequired = response.json.optBoolean("approvalRequired", false); runOnUiThread(() -> { setRefreshing(false); - if (approvalRequired) { - showMessage("已提交主 Agent 审批"); - } else { - showMessage("转发成功"); - } + showMessage(resolveForwardResultMessage(response.json)); setResult(RESULT_OK); finish(); }); @@ -223,155 +192,4 @@ public class ForwardTargetActivity extends BossScreenActivity { private static boolean isEmpty(@Nullable String value) { return value == null || value.length() == 0; } - - private static final class MutableJsonObject extends JSONObject { - private final Map values = new LinkedHashMap<>(); - - @Override - public JSONObject put(String key, boolean value) { - values.put(key, value); - return this; - } - - @Override - public JSONObject put(String key, int value) { - values.put(key, value); - return this; - } - - @Override - public JSONObject put(String key, long value) { - values.put(key, value); - return this; - } - - @Override - public JSONObject put(String key, Object value) { - values.put(key, value); - return this; - } - - @Override - public String optString(String key) { - Object value = values.get(key); - return value instanceof String ? (String) value : ""; - } - - @Override - public String optString(String key, String fallback) { - String value = optString(key); - return value.isEmpty() ? fallback : value; - } - - @Override - public JSONArray optJSONArray(String key) { - Object value = values.get(key); - return value instanceof JSONArray ? (JSONArray) value : null; - } - - @Override - public boolean optBoolean(String key, boolean fallback) { - Object value = values.get(key); - return value instanceof Boolean ? (Boolean) value : fallback; - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder("{"); - boolean first = true; - for (Map.Entry entry : values.entrySet()) { - if (!first) { - builder.append(","); - } - first = false; - builder.append("\"").append(escape(entry.getKey())).append("\":"); - builder.append(stringify(entry.getValue())); - } - builder.append("}"); - return builder.toString(); - } - } - - private static final class MutableJsonArray extends JSONArray { - private final ArrayList values = new ArrayList<>(); - - @Override - public JSONArray put(boolean value) { - values.add(value); - return this; - } - - @Override - public JSONArray put(int value) { - values.add(value); - return this; - } - - @Override - public JSONArray put(long value) { - values.add(value); - return this; - } - - @Override - public JSONArray put(Object value) { - values.add(value); - return this; - } - - @Override - public int length() { - return values.size(); - } - - @Override - public JSONObject optJSONObject(int index) { - if (index < 0 || index >= values.size()) { - return null; - } - Object value = values.get(index); - return value instanceof JSONObject ? (JSONObject) value : null; - } - - @Override - public String optString(int index) { - if (index < 0 || index >= values.size()) { - return ""; - } - Object value = values.get(index); - return value instanceof String ? (String) value : ""; - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder("["); - for (int i = 0; i < values.size(); i++) { - if (i > 0) { - builder.append(","); - } - builder.append(stringify(values.get(i))); - } - builder.append("]"); - return builder.toString(); - } - } - - private static String stringify(@Nullable Object value) { - if (value == null) { - return "null"; - } - if (value instanceof String) { - return "\"" + escape((String) value) + "\""; - } - if (value instanceof Number || value instanceof Boolean) { - return String.valueOf(value); - } - return value.toString(); - } - - private static String escape(String value) { - return value - .replace("\\", "\\\\") - .replace("\"", "\\\""); - } } diff --git a/android/app/src/test/java/com/hyzq/boss/ForwardTargetActivityTest.java b/android/app/src/test/java/com/hyzq/boss/ForwardTargetActivityTest.java index c1d8f9d..845a965 100644 --- a/android/app/src/test/java/com/hyzq/boss/ForwardTargetActivityTest.java +++ b/android/app/src/test/java/com/hyzq/boss/ForwardTargetActivityTest.java @@ -28,7 +28,10 @@ public class ForwardTargetActivityTest { assertEquals("single", payload.optString("mode")); assertEquals("m1", payload.optString("sourceMessageId")); - assertEquals("{\"mode\":\"single\",\"sourceMessageId\":\"m1\"}", payload.toString()); + assertEquals( + "{\"targetProjectId\":\"target\",\"mode\":\"single\",\"sourceMessageId\":\"m1\"}", + ForwardPayloads.toRequestBody("target", payload) + ); } @Test @@ -39,7 +42,16 @@ public class ForwardTargetActivityTest { assertEquals(2, payload.optJSONArray("sourceMessageIds").length()); assertEquals("m1", payload.optJSONArray("sourceMessageIds").optString(0)); assertEquals("m2", payload.optJSONArray("sourceMessageIds").optString(1)); - assertEquals("{\"mode\":\"bundle\",\"sourceMessageIds\":[\"m1\",\"m2\"]}", payload.toString()); + assertEquals( + "{\"targetProjectId\":\"target\",\"mode\":\"bundle\",\"sourceMessageIds\":[\"m1\",\"m2\"]}", + ForwardPayloads.toRequestBody("target", payload) + ); + } + + @Test + public void approvalRequiredResponseUsesApprovalMessage() { + StubJSONObject response = new StubJSONObject().withBoolean("approvalRequired", true); + assertEquals("已提交主 Agent 审批", ForwardTargetActivity.resolveForwardResultMessage(response)); } private static final class StubJSONObject extends JSONObject { @@ -50,6 +62,11 @@ public class ForwardTargetActivityTest { return this; } + StubJSONObject withBoolean(String key, boolean value) { + values.put(key, value); + return this; + } + @Override public String optString(String key) { Object value = values.get(key); @@ -61,6 +78,12 @@ public class ForwardTargetActivityTest { Object value = values.get(key); return value instanceof String ? (String) value : fallback; } + + @Override + public boolean optBoolean(String key, boolean fallback) { + Object value = values.get(key); + return value instanceof Boolean ? (Boolean) value : fallback; + } } private static final class StubJSONArray extends JSONArray {