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 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; + } + } +} diff --git a/android/app/src/main/java/com/hyzq/boss/ProjectForwardActivity.java b/android/app/src/main/java/com/hyzq/boss/ProjectForwardActivity.java index ab05f50..404af17 100644 --- a/android/app/src/main/java/com/hyzq/boss/ProjectForwardActivity.java +++ b/android/app/src/main/java/com/hyzq/boss/ProjectForwardActivity.java @@ -1,106 +1,30 @@ package com.hyzq.boss; +import android.content.Intent; import android.os.Bundle; import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; - -import org.json.JSONArray; -import org.json.JSONObject; public class ProjectForwardActivity extends BossScreenActivity { public static final String EXTRA_PROJECT_ID = "project_id"; - public static final String EXTRA_PROJECT_NAME = "project_name"; private String projectId; - private String projectName; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); projectId = getIntent().getStringExtra(EXTRA_PROJECT_ID); - projectName = getIntent().getStringExtra(EXTRA_PROJECT_NAME); - configureScreen("消息转发", projectName == null ? "选择目标项目并写备注" : "源项目:" + projectName); - reload(); + configureScreen("消息转发", "正在切换到微信式转发"); + + Intent intent = new Intent(this, ForwardTargetActivity.class); + intent.putExtra(ForwardTargetActivity.EXTRA_SOURCE_PROJECT_ID, projectId); + intent.putExtra(ForwardTargetActivity.EXTRA_FORWARD_MODE, "single_legacy"); + startActivity(intent); + finish(); } @Override protected void reload() { - setRefreshing(true); - executor.execute(() -> { - try { - BossApiClient.ApiResponse response = apiClient.getConversations(); - if (!response.ok()) throw new IllegalStateException(response.message()); - runOnUiThread(() -> renderTargets(response.json.optJSONArray("conversations"))); - } catch (Exception error) { - runOnUiThread(() -> { - setRefreshing(false); - replaceContent(BossUi.buildEmptyCard(this, "转发目标加载失败:" + error.getMessage())); - }); - } - }); - } - - private void renderTargets(@Nullable JSONArray conversations) { - replaceContent(BossUi.buildCard( - this, - "原生转发入口", - "选择一个目标项目,填写备注后会走现有 `/api/v1/projects/{projectId}/forwards`。", - "源项目:" + (projectName == null ? projectId : projectName) - )); - if (conversations == null || conversations.length() == 0) { - appendContent(BossUi.buildEmptyCard(this, "当前没有可转发的目标项目。")); - setRefreshing(false); - return; - } - for (int i = 0; i < conversations.length(); i++) { - JSONObject item = conversations.optJSONObject(i); - if (item == null) continue; - String targetProjectId = item.optString("projectId"); - if (projectId.equals(targetProjectId)) continue; - appendContent(BossUi.buildCard( - this, - item.optString("projectTitle", "未命名项目"), - item.optString("preview", ""), - item.optString("latestReplyLabel", "最近更新"), - v -> openForwardDialog(targetProjectId, item.optString("projectTitle", targetProjectId)) - )); - } - setRefreshing(false); - } - - private void openForwardDialog(String targetProjectId, String targetTitle) { - final android.widget.EditText input = BossUi.buildInput(this, "请输入要附带的转发说明", true); - input.setText("请同步关注 " + targetTitle + " 的当前进展。"); - new AlertDialog.Builder(this) - .setTitle("转发到 " + targetTitle) - .setView(input) - .setNegativeButton("取消", null) - .setPositiveButton("转发", (dialog, which) -> forwardMessage(targetProjectId, input.getText().toString().trim())) - .show(); - } - - private void forwardMessage(String targetProjectId, String note) { - if (note.isEmpty()) { - showMessage("请先填写转发说明"); - return; - } - setRefreshing(true); - executor.execute(() -> { - try { - BossApiClient.ApiResponse response = apiClient.forwardProjectMessage(projectId, targetProjectId, note); - if (!response.ok()) throw new IllegalStateException(response.message()); - runOnUiThread(() -> { - setRefreshing(false); - showMessage("转发成功"); - finish(); - }); - } catch (Exception error) { - runOnUiThread(() -> { - setRefreshing(false); - showMessage("转发失败:" + error.getMessage()); - }); - } - }); + // 兼容页只负责跳转,不再承载旧的备注转发链路。 } } diff --git a/android/app/src/main/res/layout/activity_forward_target.xml b/android/app/src/main/res/layout/activity_forward_target.xml new file mode 100644 index 0000000..e4c23f3 --- /dev/null +++ b/android/app/src/main/res/layout/activity_forward_target.xml @@ -0,0 +1,109 @@ + + + + + +