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

690 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`
- 扩展 `MessageKind``Message`,增加 `forwardSource``forwardBundle`
-`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.ts``MessageKind``Message` 附近先写出新结构,让后续编译先报缺字段:
```ts
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:
```bash
cd /Users/kris/code/boss
npm run build
```
Expected: 先因为 `forwardProjectMessage` 和相关消息使用点不完整而失败,或者至少需要补 route / render 类型。
- [ ] **Step 3: 用最小实现升级 `forwardProjectMessage` 输入结构**
`src/lib/boss-data.ts` 的旧签名:
```ts
export async function forwardProjectMessage(payload: {
sourceProjectId: string;
targetProjectId: string;
note: string;
})
```
改成:
```ts
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
```ts
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` 内先加最小判定:
```ts
function requiresForwardApproval(source: Project, target: Project) {
return source.collaborationMode === "approval_required" && target.id !== "master-agent";
}
```
并让 `forwardProjectMessage` 在命中审批时返回:
```ts
return {
approvalRequired: true,
approvalReason: "NON_DEVELOPMENT_THREAD_FORWARD",
};
```
要求:
- 审批场景下不写入目标消息账本
- 正常场景才写入目标消息账本并返回 `message`
- [ ] **Step 5: 升级 route 输入校验**
`src/app/api/v1/projects/[projectId]/forwards/route.ts` 里把旧输入:
```ts
{
targetProjectId?: string;
note?: string;
}
```
替换成:
```ts
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 })`
- 返回:
```ts
return NextResponse.json({
ok: true,
message: result.message ?? null,
approvalRequired: Boolean(result.approvalRequired),
approvalReason: result.approvalReason ?? null,
});
```
- [ ] **Step 6: 重新构建,确认类型闭合**
Run:
```bash
cd /Users/kris/code/boss
npm run build
```
Expected: `Compiled successfully`
- [ ] **Step 7: Commit**
```bash
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` 先补这些测试:
```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:
```bash
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
```java
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:
```bash
cd /Users/kris/code/boss/android
./gradlew testDebugUnitTest --tests com.hyzq.boss.ProjectChatUiStateTest --no-daemon
```
Expected: PASS
- [ ] **Step 5: Commit**
```bash
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` 先写:
```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:
```bash
cd /Users/kris/code/boss/android
./gradlew testDebugUnitTest --tests com.hyzq.boss.ForwardTargetActivityTest --no-daemon
```
Expected: FAIL提示 `ForwardTargetActivity` helper 未实现。
- [ ] **Step 3: 在 `BossApiClient.java` 补结构化转发方法**
把旧方法:
```java
public ApiResponse forwardProjectMessage(String projectId, String targetProjectId, String note)
```
替换为:
```java
public ApiResponse forwardProjectMessage(String projectId, String targetProjectId, JSONObject payload)
```
方法内最小逻辑:
```java
JSONObject requestPayload = payload == null ? new JSONObject() : payload;
requestPayload.put("targetProjectId", targetProjectId);
return requestWithRestore("POST", "/api/v1/projects/" + encode(projectId) + "/forwards", requestPayload);
```
- [ ] **Step 4: 写 `ForwardTargetActivity` 最小实现**
活动页至少需要:
```java
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` 中删除旧备注输入主链,保留:
```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:
```bash
cd /Users/kris/code/boss/android
./gradlew testDebugUnitTest --tests com.hyzq.boss.ForwardTargetActivityTest --no-daemon
```
Expected: PASS
- [ ] **Step 7: Commit**
```bash
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` 追加:
```java
@Test
public void singleForwardMessageUsesSingleModeLabel() {
assertEquals("转发", ProjectChatUiState.labelForForwardKind("forward_single"));
}
@Test
public void bundleForwardMessageUsesBundleModeLabel() {
assertEquals("聊天记录", ProjectChatUiState.labelForForwardKind("forward_bundle"));
}
```
- [ ] **Step 2: 跑单测确认先红**
Run:
```bash
cd /Users/kris/code/boss/android
./gradlew testDebugUnitTest --tests com.hyzq.boss.ProjectChatUiStateTest --no-daemon
```
Expected: FAIL提示 `labelForForwardKind` 未定义。
- [ ] **Step 3: 在 `BossUi.java` 增加转发消息和聊天记录卡片**
新增两个 builder
```java
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. 底部输入区切换为单按钮 `转发`
关键入口:
```java
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(...)` 和消息渲染分支补成:
```java
case "forward_single":
return BossUi.buildForwardSingleBubble(...);
case "forward_bundle":
return BossUi.buildForwardBundleCard(...);
```
并让 `ProjectChatUiState.labelForForwardKind(...)` 提供:
```java
"forward_single" -> "转发"
"forward_bundle" -> "聊天记录"
```
- [ ] **Step 6: 跑 Android 编译和单测**
Run:
```bash
cd /Users/kris/code/boss/android
./gradlew testDebugUnitTest :app:compileDebugJavaWithJavac assembleDebug --no-daemon
```
Expected: `BUILD SUCCESSFUL`
- [ ] **Step 7: 跑 Web 构建与接口烟测**
Run:
```bash
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包含 `message``approvalRequired`
- [ ] **Step 8: Commit**
```bash
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:
```bash
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**
```bash
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"
```