diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 960a370..25f9fa3 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -31,6 +31,7 @@
+
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 087ebf0..b7499a8 100644
--- a/android/app/src/main/java/com/hyzq/boss/BossApiClient.java
+++ b/android/app/src/main/java/com/hyzq/boss/BossApiClient.java
@@ -94,11 +94,10 @@ public class BossApiClient {
return requestWithRestore("POST", "/api/v1/projects/" + encode(projectId) + "/messages", payload);
}
- public ApiResponse forwardProjectMessage(String projectId, String targetProjectId, String note) throws IOException, JSONException {
- JSONObject payload = new JSONObject();
- payload.put("targetProjectId", targetProjectId);
- payload.put("note", note);
- return requestWithRestore("POST", "/api/v1/projects/" + encode(projectId) + "/forwards", payload);
+ 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);
}
public ApiResponse getThreadDetail(String threadId) throws IOException, JSONException {
diff --git a/android/app/src/main/java/com/hyzq/boss/ForwardTargetActivity.java b/android/app/src/main/java/com/hyzq/boss/ForwardTargetActivity.java
new file mode 100644
index 0000000..6e7ba98
--- /dev/null
+++ b/android/app/src/main/java/com/hyzq/boss/ForwardTargetActivity.java
@@ -0,0 +1,318 @@
+package com.hyzq.boss;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.text.TextUtils;
+
+import androidx.annotation.Nullable;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ForwardTargetActivity extends BossScreenActivity {
+ public static final String EXTRA_SOURCE_PROJECT_ID = "source_project_id";
+ public static final String EXTRA_FORWARD_MODE = "forward_mode";
+ public static final String EXTRA_SOURCE_MESSAGE_ID = "source_message_id";
+ public static final String EXTRA_SOURCE_MESSAGE_IDS = "source_message_ids";
+
+ private String sourceProjectId;
+ private String forwardMode;
+ @Nullable
+ private String sourceMessageId;
+ private final ArrayList sourceMessageIds = new ArrayList<>();
+
+ @Override
+ protected int getLayoutResId() {
+ return R.layout.activity_forward_target;
+ }
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Intent intent = getIntent();
+ sourceProjectId = intent.getStringExtra(EXTRA_SOURCE_PROJECT_ID);
+ forwardMode = intent.getStringExtra(EXTRA_FORWARD_MODE);
+ sourceMessageId = intent.getStringExtra(EXTRA_SOURCE_MESSAGE_ID);
+ String[] messageIds = intent.getStringArrayExtra(EXTRA_SOURCE_MESSAGE_IDS);
+ if (messageIds != null) {
+ for (String messageId : messageIds) {
+ if (!TextUtils.isEmpty(messageId)) {
+ sourceMessageIds.add(messageId);
+ }
+ }
+ }
+
+ configureScreen("选择转发目标", buildSourceMeta());
+ reload();
+ }
+
+ @Override
+ protected void reload() {
+ if (isEmpty(sourceProjectId)) {
+ showMessage("缺少源会话");
+ finish();
+ return;
+ }
+ setRefreshing(true);
+ executor.execute(() -> {
+ try {
+ BossApiClient.ApiResponse response = apiClient.getConversations();
+ if (!response.ok()) {
+ throw new IllegalStateException(response.message());
+ }
+ JSONArray conversations = response.json.optJSONArray("conversations");
+ List targets = collectSelectableTargets(conversations, sourceProjectId);
+ runOnUiThread(() -> renderTargets(targets));
+ } catch (Exception error) {
+ runOnUiThread(() -> {
+ setRefreshing(false);
+ replaceContent(BossUi.buildEmptyCard(this, "转发目标加载失败:" + error.getMessage()));
+ });
+ }
+ });
+ }
+
+ public static List collectSelectableTargets(JSONArray conversations, String sourceProjectId) {
+ ArrayList result = new ArrayList<>();
+ if (conversations == null) {
+ return result;
+ }
+ for (int i = 0; i < conversations.length(); i++) {
+ JSONObject item = conversations.optJSONObject(i);
+ if (item == null) {
+ continue;
+ }
+ if (!isEmpty(sourceProjectId) && sourceProjectId.equals(item.optString("projectId", ""))) {
+ continue;
+ }
+ result.add(item);
+ }
+ return result;
+ }
+
+ 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);
+
+ 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;
+ }
+
+ private void renderTargets(List targets) {
+ replaceContent(
+ BossUi.buildCard(
+ this,
+ "正在选择转发目标",
+ buildSourceBody(),
+ buildSourceMeta()
+ )
+ );
+
+ if (targets.isEmpty()) {
+ appendContent(BossUi.buildEmptyCard(this, "当前没有可转发的目标会话。"));
+ setRefreshing(false);
+ return;
+ }
+
+ for (JSONObject target : targets) {
+ appendContent(BossUi.buildConversationRow(
+ this,
+ WechatSurfaceMapper.toConversationRow(target),
+ v -> forwardToTarget(target)
+ ));
+ }
+ setRefreshing(false);
+ }
+
+ private String buildSourceBody() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("源会话:").append(isEmpty(sourceProjectId) ? "-" : sourceProjectId);
+ builder.append("\n转发模式:").append(isEmpty(forwardMode) ? "single" : forwardMode);
+ return builder.toString();
+ }
+
+ private String buildSourceMeta() {
+ int messageCount = sourceMessageIds.size();
+ if (!isEmpty(sourceMessageId)) {
+ return "source_message_id 已就绪";
+ }
+ if (messageCount > 0) {
+ return "source_message_ids " + messageCount + " 条";
+ }
+ return "等待聊天页入口补充消息选择";
+ }
+
+ private void forwardToTarget(JSONObject target) {
+ if (target == null) {
+ showMessage("目标会话无效");
+ return;
+ }
+ String targetProjectId = target.optString("projectId", "");
+ if (isEmpty(targetProjectId)) {
+ showMessage("目标会话无效");
+ return;
+ }
+
+ try {
+ JSONObject payload = buildForwardPayload(
+ forwardMode,
+ sourceMessageId,
+ sourceMessageIds
+ );
+ setRefreshing(true);
+ executor.execute(() -> {
+ try {
+ BossApiClient.ApiResponse response = apiClient.forwardProjectMessage(sourceProjectId, targetProjectId, payload);
+ if (!response.ok()) {
+ throw new IllegalStateException(response.message());
+ }
+ boolean approvalRequired = response.json.optBoolean("approvalRequired", false);
+ runOnUiThread(() -> {
+ setRefreshing(false);
+ if (approvalRequired) {
+ showMessage("已提交主 Agent 审批");
+ } else {
+ showMessage("转发成功");
+ }
+ setResult(RESULT_OK);
+ finish();
+ });
+ } catch (Exception error) {
+ runOnUiThread(() -> {
+ setRefreshing(false);
+ showMessage("转发失败:" + error.getMessage());
+ });
+ }
+ });
+ } catch (JSONException error) {
+ showMessage("缺少源消息,暂无法转发");
+ }
+ }
+
+ private static boolean isEmpty(@Nullable String value) {
+ return value == null || value.length() == 0;
+ }
+
+ private static final class MutableJsonObject extends JSONObject {
+ private final java.util.Map values = new java.util.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;
+ }
+ }
+
+ private static final class MutableJsonArray extends JSONArray {
+ private final ArrayList