Files
boss/docs/superpowers/plans/2026-03-28-wechat-message-forwarding.md

21 KiB
Raw Permalink Blame History

Boss 微信式消息转发 Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 把当前原生 Android 的“备注转发页”重构成微信式消息转发链,支持单条消息转发、多选消息合并转发、统一目标会话选择页,以及服务端 forwardSource / forwardBundle / approvalRequired 账本结构。

Architecture: 保留现有 BossState -> Next API -> BossApiClient -> 原生活动页 主链,不引入新基础设施。服务端把 POST /api/v1/projects/[projectId]/forwards 从“备注转发”升级成结构化转发接口;原生端在 ProjectDetailActivity 内补消息操作菜单、多选状态和目标会话选择页,并以 ForwardTargetActivity 承接统一转发目标选择。

Tech Stack: Next.js App Router, TypeScript, file-backed data/boss-state.json, 原生 Android AppCompat + XML, HttpURLConnection, JUnit4


File Structure

Backend / state / API

  • Modify: src/lib/boss-data.ts
    • 扩展 MessageKindMessage,增加 forwardSourceforwardBundle
    • forwardProjectMessage 升级成支持 single / bundle / approvalRequired
  • Modify: src/app/api/v1/projects/[projectId]/forwards/route.ts
    • 校验新的 single / bundle 输入结构
    • 返回 message / approvalRequired / approvalReason
  • Modify: src/lib/boss-projections.ts
    • 如列表预览或详情聚合需要,补 forwarded message 的预览摘要函数

Android native

  • Modify: android/app/src/main/java/com/hyzq/boss/BossApiClient.java
    • 支持新的 forward payload
  • Modify: android/app/src/main/java/com/hyzq/boss/ProjectChatUiState.java
    • 补多选模式、已选消息、转发入口守卫
  • Modify: android/app/src/test/java/com/hyzq/boss/ProjectChatUiStateTest.java
    • 单测先行覆盖多选状态切换
  • Modify: android/app/src/main/java/com/hyzq/boss/ProjectDetailActivity.java
    • 消息长按菜单、多选模式、跳转目标会话页、forward message 渲染
  • Modify: android/app/src/main/java/com/hyzq/boss/BossUi.java
    • 补消息操作菜单、多选勾选 row、聊天记录卡片消息
  • Create: android/app/src/main/java/com/hyzq/boss/ForwardTargetActivity.java
    • 统一目标会话选择页
  • Create: android/app/src/test/java/com/hyzq/boss/ForwardTargetActivityTest.java
    • 目标会话过滤与单选逻辑单测
  • Modify: android/app/src/main/java/com/hyzq/boss/ProjectForwardActivity.java
    • 降级为兼容跳转页,直接导向 ForwardTargetActivity
  • Modify: android/app/src/main/AndroidManifest.xml
    • 注册 ForwardTargetActivity
  • Modify: android/app/src/main/res/layout/activity_project_chat.xml
    • 补多选模式头部 / 底部动作容器
  • Create: android/app/src/main/res/layout/activity_forward_target.xml
    • 目标会话选择页布局

Docs / release

  • Modify: README.md
  • Modify: docs/architecture/current_runtime_and_deploy_status_cn.md
  • Modify: docs/architecture/api_and_service_inventory_cn.md

Task 1: 升级服务端转发账本和接口结构

Files:

  • Modify: src/lib/boss-data.ts

  • Modify: src/app/api/v1/projects/[projectId]/forwards/route.ts

  • Modify: src/lib/boss-projections.ts

  • Test: npm run build

  • Step 1: 先把新的消息结构写进 failing contract 注释和类型定义

src/lib/boss-data.tsMessageKindMessage 附近先写出新结构,让后续编译先报缺字段:

export type MessageKind =
  | "text"
  | "voice_intent"
  | "image_intent"
  | "video_intent"
  | "forward_notice"
  | "forward_single"
  | "forward_bundle";

export interface ForwardSource {
  sourceProjectId: string;
  sourceProjectName: string;
  sourceThreadId?: string;
  sourceThreadTitle?: string;
  sourceMessageId: string;
  forwardedBy: string;
  forwardedAt: string;
}

export interface ForwardBundleItem {
  messageId: string;
  senderLabel: string;
  body: string;
  kind: string;
  sentAt: string;
}

export interface ForwardBundlePayload {
  sourceProjectId: string;
  sourceProjectName: string;
  sourceThreadId?: string;
  sourceThreadTitle?: string;
  itemCount: number;
  startedAt: string;
  endedAt: string;
  items: ForwardBundleItem[];
}

export interface Message {
  id: string;
  sender: MessageSender;
  senderLabel: string;
  body: string;
  sentAt: string;
  kind?: MessageKind;
  forwardSource?: ForwardSource;
  forwardBundle?: ForwardBundlePayload;
}
  • Step 2: 运行构建,确认当前实现还不支持这些结构

Run:

cd /Users/kris/code/boss
npm run build

Expected: 先因为 forwardProjectMessage 和相关消息使用点不完整而失败,或者至少需要补 route / render 类型。

  • Step 3: 用最小实现升级 forwardProjectMessage 输入结构

src/lib/boss-data.ts 的旧签名:

export async function forwardProjectMessage(payload: {
  sourceProjectId: string;
  targetProjectId: string;
  note: string;
})

改成:

export async function forwardProjectMessage(payload:
  | {
      sourceProjectId: string;
      mode: "single";
      targetProjectId: string;
      sourceMessageId: string;
      requestedBy: string;
    }
  | {
      sourceProjectId: string;
      mode: "bundle";
      targetProjectId: string;
      sourceMessageIds: string[];
      requestedBy: string;
    }
) {}

并补 3 个最小 helper

function findProjectMessage(project: Project, messageId: string) {}
function buildForwardSingleMessage(input: { source: Project; target: Project; message: Message; requestedBy: string }) {}
function buildForwardBundleMessage(input: { source: Project; target: Project; messages: Message[]; requestedBy: string }) {}

最小行为要求:

  • single 生成 kind: "forward_single",并带 forwardSource

  • bundle 生成 kind: "forward_bundle",并带 forwardBundle

  • target.preview 更新为新消息正文或卡片摘要

  • source 侧继续写一条“已转发到《目标会话》”的轻量日志

  • Step 4: 在 forwardProjectMessage 里补审批闸口最小返回

src/lib/boss-data.ts 内先加最小判定:

function requiresForwardApproval(source: Project, target: Project) {
  return source.collaborationMode === "approval_required" && target.id !== "master-agent";
}

并让 forwardProjectMessage 在命中审批时返回:

return {
  approvalRequired: true,
  approvalReason: "NON_DEVELOPMENT_THREAD_FORWARD",
};

要求:

  • 审批场景下不写入目标消息账本

  • 正常场景才写入目标消息账本并返回 message

  • Step 5: 升级 route 输入校验

src/app/api/v1/projects/[projectId]/forwards/route.ts 里把旧输入:

{
  targetProjectId?: string;
  note?: string;
}

替换成:

type ForwardBody =
  | {
      mode?: "single";
      targetProjectId?: string;
      sourceMessageId?: string;
    }
  | {
      mode?: "bundle";
      targetProjectId?: string;
      sourceMessageIds?: string[];
    };

route 最小逻辑:

  • mode=single 时要求 sourceMessageId
  • mode=bundle 时要求 sourceMessageIds.length > 1
  • 调用 forwardProjectMessage({ ..., requestedBy: session.account })
  • 返回:
return NextResponse.json({
  ok: true,
  message: result.message ?? null,
  approvalRequired: Boolean(result.approvalRequired),
  approvalReason: result.approvalReason ?? null,
});
  • Step 6: 重新构建,确认类型闭合

Run:

cd /Users/kris/code/boss
npm run build

Expected: Compiled successfully

  • Step 7: Commit
git add src/lib/boss-data.ts src/app/api/v1/projects/[projectId]/forwards/route.ts src/lib/boss-projections.ts
git commit -m "feat: add structured message forwarding payloads"

Task 2: 先用单测拉出原生多选转发状态机

Files:

  • Modify: android/app/src/main/java/com/hyzq/boss/ProjectChatUiState.java

  • Modify: android/app/src/test/java/com/hyzq/boss/ProjectChatUiStateTest.java

  • Step 1: 先写 failing test覆盖多选模式切换

android/app/src/test/java/com/hyzq/boss/ProjectChatUiStateTest.java 先补这些测试:

@Test
public void entersMultiSelectModeAfterFirstToggle() {
    ProjectChatUiState.SelectionState state = ProjectChatUiState.toggleSelection(null, "m1");
    assertTrue(state.multiSelecting);
    assertEquals(1, state.selectedMessageIds.size());
    assertTrue(state.selectedMessageIds.contains("m1"));
}

@Test
public void deselectingLastMessageExitsMultiSelectMode() {
    ProjectChatUiState.SelectionState state = new ProjectChatUiState.SelectionState(true, java.util.Set.of("m1"));
    ProjectChatUiState.SelectionState next = ProjectChatUiState.toggleSelection(state, "m1");
    assertFalse(next.multiSelecting);
    assertTrue(next.selectedMessageIds.isEmpty());
}

@Test
public void bundleForwardRequiresAtLeastTwoMessages() {
    ProjectChatUiState.SelectionState state = new ProjectChatUiState.SelectionState(true, java.util.Set.of("m1"));
    assertFalse(ProjectChatUiState.canForwardSelection(state));
}
  • Step 2: 跑单测确认先红

Run:

cd /Users/kris/code/boss/android
./gradlew testDebugUnitTest --tests com.hyzq.boss.ProjectChatUiStateTest --no-daemon

Expected: FAIL提示 SelectionState / toggleSelection / canForwardSelection 尚未实现。

  • Step 3: 在 ProjectChatUiState.java 写最小实现

补最小状态对象与 helper

public static final class SelectionState {
    public final boolean multiSelecting;
    public final java.util.Set<String> selectedMessageIds;

    public SelectionState(boolean multiSelecting, java.util.Set<String> selectedMessageIds) {
        this.multiSelecting = multiSelecting;
        this.selectedMessageIds = selectedMessageIds;
    }
}

public static SelectionState emptySelection() {
    return new SelectionState(false, new java.util.LinkedHashSet<>());
}

public static SelectionState toggleSelection(@Nullable SelectionState current, String messageId) {}

public static boolean canForwardSelection(@Nullable SelectionState state) {
    return state != null && state.selectedMessageIds.size() >= 2;
}

要求:

  • 第一次 toggle 进入多选

  • 取消最后一条选中后退出多选

  • 保持插入顺序,后面 bundle 卡片会用到

  • Step 4: 跑单测确认转绿

Run:

cd /Users/kris/code/boss/android
./gradlew testDebugUnitTest --tests com.hyzq.boss.ProjectChatUiStateTest --no-daemon

Expected: PASS

  • Step 5: Commit
git add android/app/src/main/java/com/hyzq/boss/ProjectChatUiState.java android/app/src/test/java/com/hyzq/boss/ProjectChatUiStateTest.java
git commit -m "feat: add native chat forward selection state"

Task 3: 先做会话选择页与 API payload builder再接聊天页入口

Files:

  • Modify: android/app/src/main/java/com/hyzq/boss/BossApiClient.java

  • Create: android/app/src/main/java/com/hyzq/boss/ForwardTargetActivity.java

  • Create: android/app/src/test/java/com/hyzq/boss/ForwardTargetActivityTest.java

  • Modify: android/app/src/main/java/com/hyzq/boss/ProjectForwardActivity.java

  • Modify: android/app/src/main/AndroidManifest.xml

  • Create: android/app/src/main/res/layout/activity_forward_target.xml

  • Step 1: 先写 failing test覆盖目标会话过滤和单选规则

android/app/src/test/java/com/hyzq/boss/ForwardTargetActivityTest.java 先写:

@Test
public void filtersOutSourceConversationFromTargets() {
    JSONArray conversations = new JSONArray()
            .put(new StubJSONObject().withString("projectId", "source").withString("projectTitle", "源会话"))
            .put(new StubJSONObject().withString("projectId", "target").withString("projectTitle", "目标会话"));

    java.util.List<JSONObject> result = ForwardTargetActivity.collectSelectableTargets(conversations, "source");

    assertEquals(1, result.size());
    assertEquals("target", result.get(0).optString("projectId"));
}

@Test
public void singleModeRequiresOneMessageId() throws Exception {
    JSONObject payload = ForwardTargetActivity.buildForwardPayload("single", "m1", java.util.List.of());
    assertEquals("single", payload.optString("mode"));
    assertEquals("m1", payload.optString("sourceMessageId"));
}

@Test
public void bundleModeUsesOrderedMessageIds() throws Exception {
    JSONObject payload = ForwardTargetActivity.buildForwardPayload("bundle", null, java.util.List.of("m1", "m2"));
    assertEquals("bundle", payload.optString("mode"));
    assertEquals(2, payload.optJSONArray("sourceMessageIds").length());
}
  • Step 2: 跑单测确认先红

Run:

cd /Users/kris/code/boss/android
./gradlew testDebugUnitTest --tests com.hyzq.boss.ForwardTargetActivityTest --no-daemon

Expected: FAIL提示 ForwardTargetActivity helper 未实现。

  • Step 3: 在 BossApiClient.java 补结构化转发方法

把旧方法:

public ApiResponse forwardProjectMessage(String projectId, String targetProjectId, String note)

替换为:

public ApiResponse forwardProjectMessage(String projectId, String targetProjectId, JSONObject payload)

方法内最小逻辑:

JSONObject requestPayload = payload == null ? new JSONObject() : payload;
requestPayload.put("targetProjectId", targetProjectId);
return requestWithRestore("POST", "/api/v1/projects/" + encode(projectId) + "/forwards", requestPayload);
  • Step 4: 写 ForwardTargetActivity 最小实现

活动页至少需要:

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";

static java.util.List<JSONObject> collectSelectableTargets(JSONArray conversations, String sourceProjectId) {}
static JSONObject buildForwardPayload(String mode, @Nullable String sourceMessageId, java.util.List<String> sourceMessageIds) throws JSONException {}

页面行为最小版:

  • 拉取 apiClient.getConversations()

  • 过滤源会话

  • 列出微信式会话 cell

  • 点中某个目标会话后调用新的 forwardProjectMessage

  • approvalRequired=true 时先提示“已提交主 Agent 审批”

  • 正常成功时 setResult(RESULT_OK) 后 finish

  • Step 5: 让旧 ProjectForwardActivity 只做兼容跳转

android/app/src/main/java/com/hyzq/boss/ProjectForwardActivity.java 中删除旧备注输入主链,保留:

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();

并把标题副文案改成“正在切换到微信式转发”。

  • Step 6: 跑单测确认转绿

Run:

cd /Users/kris/code/boss/android
./gradlew testDebugUnitTest --tests com.hyzq.boss.ForwardTargetActivityTest --no-daemon

Expected: PASS

  • Step 7: Commit
git add android/app/src/main/java/com/hyzq/boss/BossApiClient.java android/app/src/main/java/com/hyzq/boss/ForwardTargetActivity.java android/app/src/main/java/com/hyzq/boss/ProjectForwardActivity.java android/app/src/main/AndroidManifest.xml android/app/src/main/res/layout/activity_forward_target.xml android/app/src/test/java/com/hyzq/boss/ForwardTargetActivityTest.java
git commit -m "feat: add native forward target picker"

Task 4: 把消息长按、多选和转发结果真正接进聊天页

Files:

  • Modify: android/app/src/main/java/com/hyzq/boss/ProjectDetailActivity.java

  • Modify: android/app/src/main/java/com/hyzq/boss/BossUi.java

  • Modify: android/app/src/main/res/layout/activity_project_chat.xml

  • Modify: android/app/src/main/java/com/hyzq/boss/ProjectChatUiState.java

  • Modify: android/app/src/test/java/com/hyzq/boss/ProjectChatUiStateTest.java

  • Step 1: 先写 failing test覆盖 forward kind 的 UI 标签

先在 android/app/src/test/java/com/hyzq/boss/ProjectChatUiStateTest.java 追加:

@Test
public void singleForwardMessageUsesSingleModeLabel() {
    assertEquals("转发", ProjectChatUiState.labelForForwardKind("forward_single"));
}

@Test
public void bundleForwardMessageUsesBundleModeLabel() {
    assertEquals("聊天记录", ProjectChatUiState.labelForForwardKind("forward_bundle"));
}
  • Step 2: 跑单测确认先红

Run:

cd /Users/kris/code/boss/android
./gradlew testDebugUnitTest --tests com.hyzq.boss.ProjectChatUiStateTest --no-daemon

Expected: FAIL提示 labelForForwardKind 未定义。

  • Step 3: 在 BossUi.java 增加转发消息和聊天记录卡片

新增两个 builder

public static LinearLayout buildForwardSingleBubble(
        Context context,
        String senderLabel,
        String body,
        @Nullable String meta,
        @Nullable String sourceLabel,
        boolean outgoing
) {}

public static LinearLayout buildForwardBundleCard(
        Context context,
        String senderLabel,
        String cardTitle,
        String summary,
        @Nullable String meta,
        boolean outgoing
) {}

要求:

  • forward_single 仍看起来像普通消息 bubble

  • forward_bundle 明显是聊天记录卡片,但不能长成控制台卡片

  • Step 4: 在 ProjectDetailActivity.java 接入长按与多选

最小实现顺序:

  1. 为每条消息 view 绑定 messageId
  2. 长按消息时弹出原生 AlertDialog 操作菜单:转发 / 多选 / 复制 / 删除 / 取消
  3. 转发 时直接打开 ForwardTargetActivity
  4. 多选 时切换 SelectionState
  5. 多选模式下顶部切为 取消 + 已选数量
  6. 底部输入区切换为单按钮 转发

关键入口:

private void openSingleForwardTarget(String sourceMessageId) {}
private void openBundleForwardTarget(java.util.List<String> sourceMessageIds) {}
private void enterMultiSelectFromMessage(String messageId) {}
private void exitMultiSelect() {}
  • Step 5: 在消息渲染分支中接入新 kind

把现有 labelForMessageKind(...) 和消息渲染分支补成:

case "forward_single":
    return BossUi.buildForwardSingleBubble(...);
case "forward_bundle":
    return BossUi.buildForwardBundleCard(...);

并让 ProjectChatUiState.labelForForwardKind(...) 提供:

"forward_single" -> "转发"
"forward_bundle" -> "聊天记录"
  • Step 6: 跑 Android 编译和单测

Run:

cd /Users/kris/code/boss/android
./gradlew testDebugUnitTest :app:compileDebugJavaWithJavac assembleDebug --no-daemon

Expected: BUILD SUCCESSFUL

  • Step 7: 跑 Web 构建与接口烟测

Run:

cd /Users/kris/code/boss
npm run lint
npm run build
npm start
curl -sS http://127.0.0.1:3000/api/health
curl -sS -H 'Content-Type: application/json' -d '{"mode":"single","targetProjectId":"master-agent","sourceMessageId":"m-test"}' http://127.0.0.1:3000/api/v1/projects/boss-console-ui/forwards

Expected:

  • lint 通过

  • build 通过

  • /api/health 返回 { ok: true }

  • /forwards 返回结构化 JSON包含 messageapprovalRequired

  • Step 8: Commit

git add android/app/src/main/java/com/hyzq/boss/ProjectDetailActivity.java android/app/src/main/java/com/hyzq/boss/BossUi.java android/app/src/main/res/layout/activity_project_chat.xml android/app/src/main/java/com/hyzq/boss/ProjectChatUiState.java android/app/src/test/java/com/hyzq/boss/ProjectChatUiStateTest.java
git commit -m "feat: add wechat style native message forwarding"

Task 5: 文档、发布和完整验证

Files:

  • Modify: README.md

  • Modify: docs/architecture/current_runtime_and_deploy_status_cn.md

  • Modify: docs/architecture/api_and_service_inventory_cn.md

  • Step 1: 同步文档

把以下事实写回文档:

  • 原生 Android 已支持单条消息转发

  • 原生 Android 已支持多选合并转发

  • 新增 ForwardTargetActivity

  • POST /api/v1/projects/[projectId]/forwards 已支持 single / bundle

  • 单条消息落 forwardSource

  • 多条消息落 forwardBundle

  • 审批闸口已预留

  • Step 2: 完整验证

Run:

cd /Users/kris/code/boss
npm run lint
npm run build
curl -sS http://127.0.0.1:3000/api/health
curl -sS http://127.0.0.1:4317/health
cd android && ./gradlew testDebugUnitTest :app:compileDebugJavaWithJavac assembleDebug --no-daemon
cd /Users/kris/code/boss
JAVA_HOME=$(/usr/libexec/java_home) npm run apk:release
JAVA_HOME=$(/usr/libexec/java_home) npm run aab:release
./scripts/deploy-server.sh
"$HOME/.codex/skills/boss-server-debug/scripts/server_ssh.sh" exec "curl -sS http://127.0.0.1:3000/api/health"
curl -sS https://boss.hyzq.net/api/health

Expected:

  • 全部成功

  • 公网元数据刷新到新版本

  • Step 3: Commit

git add README.md docs/architecture/current_runtime_and_deploy_status_cn.md docs/architecture/api_and_service_inventory_cn.md
git commit -m "docs: update forwarding architecture and runtime status"