docs: add wechat forwarding implementation plan
This commit is contained in:
689
docs/superpowers/plans/2026-03-28-wechat-message-forwarding.md
Normal file
689
docs/superpowers/plans/2026-03-28-wechat-message-forwarding.md
Normal file
@@ -0,0 +1,689 @@
|
||||
# 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"
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user