refactor: isolate forward payload serialization

This commit is contained in:
kris
2026-03-28 08:29:05 +08:00
parent 0783f4da14
commit 13c67425ab
4 changed files with 293 additions and 195 deletions

View File

@@ -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);
}
}

View File

@@ -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<String> 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<String, Object> 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<String, Object> 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<Object> 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");
}
}

View File

@@ -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<String> 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<JSONObject> 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<String, Object> 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<String, Object> 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<Object> 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("\"", "\\\"");
}
}

View File

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