refactor: isolate forward payload serialization
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
248
android/app/src/main/java/com/hyzq/boss/ForwardPayloads.java
Normal file
248
android/app/src/main/java/com/hyzq/boss/ForwardPayloads.java
Normal 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");
|
||||
}
|
||||
}
|
||||
@@ -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("\"", "\\\"");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user