From e4ff24a18f6ac40e0dfe9e0d54455eb1889ba244 Mon Sep 17 00:00:00 2001 From: kris Date: Sun, 29 Mar 2026 15:06:16 +0800 Subject: [PATCH] docs: add attachment storage implementation plan --- ...t-attachments-storage-and-ai-processing.md | 995 ++++++++++++++++++ 1 file changed, 995 insertions(+) create mode 100644 docs/superpowers/plans/2026-03-29-chat-attachments-storage-and-ai-processing.md diff --git a/docs/superpowers/plans/2026-03-29-chat-attachments-storage-and-ai-processing.md b/docs/superpowers/plans/2026-03-29-chat-attachments-storage-and-ai-processing.md new file mode 100644 index 0000000..f1c138d --- /dev/null +++ b/docs/superpowers/plans/2026-03-29-chat-attachments-storage-and-ai-processing.md @@ -0,0 +1,995 @@ +# Boss 聊天附件、双存储与 AI 处理 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:** 为 Boss 原生聊天链路补齐图片/视频/文件发送、默认服务器文件存储、可选阿里 OSS、统一附件消息模型、主 Agent 附件分析,以及 Web 端 `我的 > 附件与存储` 简化配置页。 + +**Architecture:** 保持现有 `boss-state.json + Next API + BossApiClient + 原生 Android` 路线,不引入新的数据库或消息队列。服务端新增统一附件存储抽象层,默认走服务器文件存储,可按用户切到阿里 OSS;原生端只与统一上传/下载接口交互,AI 分析统一走主 Agent 任务链。 + +**Tech Stack:** Next.js App Router、TypeScript、Node.js `fs`、阿里云 OSS Node SDK、原生 Android `AppCompatActivity + ActivityResultContracts + HttpURLConnection`、现有 `boss-master-agent` 队列、文件型持久化 `data/boss-state.json`。 + +--- + +## File Structure + +### 需要新增的主要文件 + +- `src/lib/boss-attachments.ts` + - 统一附件类型推断、大小阈值判断、文件名清洗、下载响应帮助函数。 +- `src/lib/boss-storage.ts` + - 定义 `AttachmentStorageProvider` 接口、按用户配置选择 `server_file / aliyun_oss`。 +- `src/lib/boss-storage-server-file.ts` + - 服务器本地文件存储实现。 +- `src/lib/boss-storage-aliyun-oss.ts` + - 阿里 OSS 上传、签名 URL、配置校验。 +- `src/app/api/v1/storage/config/route.ts` + - 当前登录用户的附件与存储配置读取/更新。 +- `src/app/api/v1/storage/config/validate/route.ts` + - 阿里 OSS 配置有效性验证。 +- `src/app/api/v1/projects/[projectId]/attachments/route.ts` + - 统一附件上传入口。 +- `src/app/api/v1/attachments/[attachmentId]/download/route.ts` + - 统一预览/下载入口。 +- `src/app/api/v1/projects/[projectId]/attachments/[attachmentId]/analyze/route.ts` + - 手动触发附件分析。 +- `src/app/me/storage/page.tsx` + - Web 端 `我的 > 附件与存储` 页面。 +- `android/app/src/test/java/com/hyzq/boss/AttachmentComposerStateTest.java` + - 原生附件入口与确认状态单测。 +- `android/app/src/test/java/com/hyzq/boss/BossApiClientAttachmentTest.java` + - 原生上传/下载/手动分析 API 客户端测试。 + +### 需要修改的主要文件 + +- `src/lib/boss-data.ts` + - 扩展用户级存储配置、附件消息模型、分析状态、主 Agent 附件任务数据。 +- `src/lib/boss-master-agent.ts` + - 补 `attachment_analysis` 任务类型、附件摘要结果回写。 +- `src/app/api/v1/projects/[projectId]/messages/route.ts` + - 保持文本消息主链,但允许附件分析结果回写卡片。 +- `src/lib/boss-projections.ts` + - 把附件消息和分析状态投影给 Web。 +- `src/components/app-ui.tsx` + - Web 会话页补附件消息展示和 `我的 > 附件与存储` 入口。 +- `src/app/me/page.tsx` + - 增加 `附件与存储` 菜单入口。 +- `android/app/src/main/java/com/hyzq/boss/BossApiClient.java` + - 增加附件上传、下载、手动分析调用。 +- `android/app/src/main/java/com/hyzq/boss/ProjectDetailActivity.java` + - 增加 `+` 按钮底部抽屉、图片/视频确认、文件发送、附件消息渲染与动作。 +- `android/app/src/main/java/com/hyzq/boss/BossUi.java` + - 增加附件气泡 / 卡片 UI 构造。 +- `android/app/src/main/res/layout/activity_project_chat.xml` + - 在输入区增加 `+` 和附件抽屉入口挂点。 +- `android/app/src/main/AndroidManifest.xml` + - 如需文件打开/下载支持,补 `provider` 和系统选择权限声明。 +- `android/app/build.gradle` + - 升版本号,必要时引入 Android 附件测试依赖。 +- `README.md` +- `docs/architecture/current_runtime_and_deploy_status_cn.md` +- `docs/architecture/api_and_service_inventory_cn.md` +- `docs/architecture/ai_handoff_index_cn.md` + +--- + +### Task 1: 扩展服务端数据模型与用户级存储配置 + +**Files:** +- Modify: `src/lib/boss-data.ts` +- Test: `src/lib/boss-data.ts`(先用 Node 侧最小读写回归;当前仓库没有单独 Vitest/Jest,先用 API 与状态读写验证) + +- [ ] **Step 1: 先写 failing test 思路并用最小状态回归脚本表达预期** + +在终端先确认当前状态模型还没有 `attachment` 和用户级存储配置。先准备一个最小断言脚本草稿,后面实现完成后执行: + +```bash +node - <<'EOF' +const fs = require('node:fs'); +const state = JSON.parse(fs.readFileSync('data/boss-state.json', 'utf8')); +if (!state.userAttachmentStorageConfigs) { + console.error('MISSING:userAttachmentStorageConfigs'); + process.exit(1); +} +const accountConfig = state.userAttachmentStorageConfigs.find((item) => item.account === '17600003315'); +if (!accountConfig) { + console.error('MISSING:account storage config'); + process.exit(1); +} +console.log('OK'); +EOF +``` + +- [ ] **Step 2: 运行脚本,确认当前会失败** + +Run: + +```bash +node - <<'EOF' +const fs = require('node:fs'); +const state = JSON.parse(fs.readFileSync('data/boss-state.json', 'utf8')); +if (!state.userAttachmentStorageConfigs) { + console.error('MISSING:userAttachmentStorageConfigs'); + process.exit(1); +} +EOF +``` + +Expected: FAIL,提示 `MISSING:userAttachmentStorageConfigs`。 + +- [ ] **Step 3: 在 `boss-data.ts` 增加附件类型和用户级存储配置模型** + +把 `MessageKind`、`Message`、用户配置和分析状态补成如下结构: + +```ts +export type MessageKind = + | "text" + | "voice_intent" + | "image_intent" + | "video_intent" + | "forward_notice" + | "forward_single" + | "forward_bundle" + | "attachment" + | "analysis_card"; + +export type AttachmentKind = "image" | "video" | "pdf" | "text" | "office" | "binary"; +export type AttachmentStorageBackend = "server_file" | "aliyun_oss"; +export type AttachmentAnalysisState = + | "not_applicable" + | "queued_auto" + | "ready_manual" + | "processing" + | "completed" + | "failed"; + +export interface MessageAttachment { + attachmentId: string; + fileName: string; + mimeType: string; + fileSizeBytes: number; + attachmentKind: AttachmentKind; + storageBackend: AttachmentStorageBackend; + storagePath: string; + previewAvailable: boolean; + uploadedAt: string; + uploadedBy: string; + analysisState: AttachmentAnalysisState; + analysisSummary?: string; + analysisCardId?: string; +} + +export interface UserAttachmentStorageConfig { + account: string; + mode: "server_file" | "oss"; + ossProvider?: "aliyun_oss"; + aliyunOss?: { + enabled: boolean; + accessKeyId: string; + accessKeySecretEncrypted: string; + bucket: string; + endpoint: string; + region: string; + prefix?: string; + }; + updatedAt: string; + validatedAt?: string; +} +``` + +- [ ] **Step 4: 给默认状态补上 `server_file` 配置和读写 helper** + +在默认状态初始化处加入: + +```ts +userAttachmentStorageConfigs: [ + { + account: "17600003315", + mode: "server_file", + updatedAt: nowIso(), + }, +], +``` + +并增加 helper: + +```ts +export async function getAttachmentStorageConfig(account: string) { + const state = await readState(); + return ( + state.userAttachmentStorageConfigs.find((item) => item.account === account) ?? { + account, + mode: "server_file" as const, + updatedAt: nowIso(), + } + ); +} + +export async function upsertAttachmentStorageConfig(config: UserAttachmentStorageConfig) { + return mutateState((state) => { + const index = state.userAttachmentStorageConfigs.findIndex((item) => item.account === config.account); + if (index >= 0) { + state.userAttachmentStorageConfigs[index] = config; + } else { + state.userAttachmentStorageConfigs.push(config); + } + return config; + }); +} +``` + +- [ ] **Step 5: 运行回归脚本,确认模型已经存在** + +Run: + +```bash +node - <<'EOF' +const fs = require('node:fs'); +const state = JSON.parse(fs.readFileSync('data/boss-state.json', 'utf8')); +console.log(Array.isArray(state.userAttachmentStorageConfigs) ? 'OK' : 'FAIL'); +EOF +``` + +Expected: 输出 `OK`。 + +- [ ] **Step 6: Commit** + +```bash +git add src/lib/boss-data.ts +git commit -m "feat: add attachment storage config model" +``` + +--- + +### Task 2: 先把统一附件工具和服务器文件存储跑通 + +**Files:** +- Create: `src/lib/boss-attachments.ts` +- Create: `src/lib/boss-storage.ts` +- Create: `src/lib/boss-storage-server-file.ts` +- Create: `src/app/api/v1/projects/[projectId]/attachments/route.ts` +- Create: `src/app/api/v1/attachments/[attachmentId]/download/route.ts` + +- [ ] **Step 1: 先写 failing API 验证脚本,确认上传接口还不存在** + +```bash +tmpdir=$(mktemp -d) +cookie="$tmpdir/cookies.txt" +curl -sS -c "$cookie" -b "$cookie" -H 'Content-Type: application/json' -d '{}' http://127.0.0.1:3000/api/auth/login >/dev/null +curl -sS -o /dev/null -w '%{http_code}\n' -c "$cookie" -b "$cookie" -F "file=@README.md" http://127.0.0.1:3000/api/v1/projects/boss-console/attachments +``` + +- [ ] **Step 2: 运行脚本,确认当前是 404 或未实现失败** + +Run: + +```bash +tmpdir=$(mktemp -d) +cookie="$tmpdir/cookies.txt" +curl -sS -c "$cookie" -b "$cookie" -H 'Content-Type: application/json' -d '{}' http://127.0.0.1:3000/api/auth/login >/dev/null +curl -sS -o /dev/null -w '%{http_code}\n' -c "$cookie" -b "$cookie" -F "file=@README.md" http://127.0.0.1:3000/api/v1/projects/boss-console/attachments +``` + +Expected: 不是 `200`,说明接口尚未实现。 + +- [ ] **Step 3: 在 `boss-attachments.ts` 实现类型归类和自动/手动分析判定** + +```ts +export function detectAttachmentKind(fileName: string, mimeType: string): AttachmentKind { + if (mimeType.startsWith("image/")) return "image"; + if (mimeType.startsWith("video/")) return "video"; + if (mimeType === "application/pdf") return "pdf"; + if (mimeType.startsWith("text/")) return "text"; + if ( + mimeType.includes("officedocument") || + mimeType.includes("msword") || + mimeType.includes("spreadsheet") || + mimeType.includes("presentation") + ) { + return "office"; + } + return "binary"; +} + +export function resolveAttachmentAnalysisState(kind: AttachmentKind, fileSizeBytes: number): AttachmentAnalysisState { + const isLarge = fileSizeBytes > 20 * 1024 * 1024; + if (isLarge) return "ready_manual"; + if (kind === "image" || kind === "pdf" || kind === "text") return "queued_auto"; + return "ready_manual"; +} +``` + +- [ ] **Step 4: 在 `boss-storage-server-file.ts` 实现本地文件上传与下载定位** + +```ts +export async function storeServerFileAttachment(params: { + account: string; + messageId: string; + fileName: string; + buffer: Buffer; +}) { + const now = new Date(); + const relativePath = path.join( + "data", + "uploads", + params.account, + String(now.getUTCFullYear()), + String(now.getUTCMonth() + 1).padStart(2, "0"), + `${params.messageId}-${sanitizeFileName(params.fileName)}`, + ); + const absolutePath = path.join(resolveRuntimeRoot(), relativePath); + await fs.mkdir(path.dirname(absolutePath), { recursive: true }); + await fs.writeFile(absolutePath, params.buffer); + return { + storageBackend: "server_file" as const, + storagePath: relativePath, + }; +} +``` + +- [ ] **Step 5: 在上传 route 里先实现 `server_file` 主链** + +核心逻辑最小实现: + +```ts +const form = await request.formData(); +const file = form.get("file"); +if (!(file instanceof File)) { + return NextResponse.json({ ok: false, message: "FILE_REQUIRED" }, { status: 400 }); +} +const bytes = Buffer.from(await file.arrayBuffer()); +const attachmentId = randomToken("att"); +const messageId = randomToken("msg"); +const attachmentKind = detectAttachmentKind(file.name, file.type || "application/octet-stream"); +const analysisState = resolveAttachmentAnalysisState(attachmentKind, bytes.byteLength); +const stored = await provider.storeAttachment(...); +const message = await appendAttachmentMessage(...); +``` + +- [ ] **Step 6: 在下载 route 里实现 `server_file` 流式返回** + +```ts +if (attachment.storageBackend === "server_file") { + const absolutePath = path.join(resolveRuntimeRoot(), attachment.storagePath); + const stream = createReadStream(absolutePath); + return new NextResponse(Readable.toWeb(stream) as ReadableStream, { + headers: { + "Content-Type": attachment.mimeType, + "Content-Disposition": `inline; filename="${attachment.fileName}"`, + }, + }); +} +``` + +- [ ] **Step 7: 启动本地服务并验证上传/下载通过** + +Run: + +```bash +npm run build +npm start +``` + +再执行: + +```bash +tmpdir=$(mktemp -d) +cookie="$tmpdir/cookies.txt" +curl -sS -c "$cookie" -b "$cookie" -H 'Content-Type: application/json' -d '{}' http://127.0.0.1:3000/api/auth/login >/dev/null +curl -sS -c "$cookie" -b "$cookie" -F "file=@README.md;type=text/plain" http://127.0.0.1:3000/api/v1/projects/boss-console/attachments +``` + +Expected: 返回 `ok:true` 且消息 `kind=attachment`。 + +- [ ] **Step 8: Commit** + +```bash +git add src/lib/boss-attachments.ts src/lib/boss-storage.ts src/lib/boss-storage-server-file.ts src/app/api/v1/projects/[projectId]/attachments/route.ts src/app/api/v1/attachments/[attachmentId]/download/route.ts +git commit -m "feat: add server file attachment pipeline" +``` + +--- + +### Task 3: 接入阿里 OSS 私有桶与配置校验 + +**Files:** +- Create: `src/lib/boss-storage-aliyun-oss.ts` +- Create: `src/app/api/v1/storage/config/route.ts` +- Create: `src/app/api/v1/storage/config/validate/route.ts` +- Modify: `src/lib/boss-storage.ts` +- Modify: `package.json` + +- [ ] **Step 1: 先写 failing config 路由验证** + +```bash +tmpdir=$(mktemp -d) +cookie="$tmpdir/cookies.txt" +curl -sS -c "$cookie" -b "$cookie" -H 'Content-Type: application/json' -d '{}' http://127.0.0.1:3000/api/auth/login >/dev/null +curl -sS -o /dev/null -w '%{http_code}\n' -c "$cookie" -b "$cookie" http://127.0.0.1:3000/api/v1/storage/config +``` + +- [ ] **Step 2: 运行,确认当前是 404** + +Run 同上。 +Expected: `404`。 + +- [ ] **Step 3: 安装阿里 OSS SDK 并实现 provider** + +```bash +npm install ali-oss +``` + +在 `boss-storage-aliyun-oss.ts` 最小实现: + +```ts +import OSS from "ali-oss"; + +export function createAliyunOssClient(config: UserAttachmentStorageConfig["aliyunOss"]) { + if (!config?.enabled) throw new Error("ALIYUN_OSS_NOT_ENABLED"); + return new OSS({ + accessKeyId: config.accessKeyId, + accessKeySecret: decryptStorageSecret(config.accessKeySecretEncrypted), + bucket: config.bucket, + endpoint: config.endpoint, + region: config.region, + }); +} +``` + +- [ ] **Step 4: 实现 `GET/PATCH /api/v1/storage/config`** + +要求: + +- `GET` 返回当前用户配置 +- `PATCH` 接受 `mode`、`ossProvider`、`aliyunOss` +- `PATCH` 时对 `AccessKey Secret` 做加密,不明文落库 + +最小返回结构: + +```ts +return NextResponse.json({ + ok: true, + config: sanitizeStorageConfig(savedConfig), +}); +``` + +- [ ] **Step 5: 实现 `POST /api/v1/storage/config/validate`** + +使用 OSS SDK 执行最小探针: + +```ts +await client.getBucketInfo(); +return NextResponse.json({ ok: true, provider: "aliyun_oss" }); +``` + +失败时返回: + +```ts +return NextResponse.json({ ok: false, message: normalizeStorageError(error) }, { status: 400 }); +``` + +- [ ] **Step 6: 在 `boss-storage.ts` 中按用户配置分流到 `server_file / aliyun_oss`** + +```ts +export async function resolveAttachmentStorageProvider(account: string): Promise { + const config = await getAttachmentStorageConfig(account); + if (config.mode === "oss" && config.ossProvider === "aliyun_oss") { + return createAliyunOssStorageProvider(config); + } + return createServerFileStorageProvider(); +} +``` + +- [ ] **Step 7: 运行接口验证** + +Run: + +```bash +tmpdir=$(mktemp -d) +cookie="$tmpdir/cookies.txt" +curl -sS -c "$cookie" -b "$cookie" -H 'Content-Type: application/json' -d '{}' http://127.0.0.1:3000/api/auth/login >/dev/null +curl -sS -c "$cookie" -b "$cookie" http://127.0.0.1:3000/api/v1/storage/config +``` + +Expected: 返回默认 `mode=server_file`。 + +- [ ] **Step 8: Commit** + +```bash +git add package.json package-lock.json src/lib/boss-storage.ts src/lib/boss-storage-aliyun-oss.ts src/app/api/v1/storage/config/route.ts src/app/api/v1/storage/config/validate/route.ts +git commit -m "feat: add aliyun oss storage config" +``` + +--- + +### Task 4: 打通附件消息创建、下载元数据和主 Agent 分析任务 + +**Files:** +- Modify: `src/lib/boss-data.ts` +- Modify: `src/lib/boss-master-agent.ts` +- Create: `src/app/api/v1/projects/[projectId]/attachments/[attachmentId]/analyze/route.ts` +- Modify: `src/app/api/v1/projects/[projectId]/attachments/route.ts` + +- [ ] **Step 1: 先写 failing API 行为验证,确认分析接口未实现** + +```bash +tmpdir=$(mktemp -d) +cookie="$tmpdir/cookies.txt" +curl -sS -c "$cookie" -b "$cookie" -H 'Content-Type: application/json' -d '{}' http://127.0.0.1:3000/api/auth/login >/dev/null +curl -sS -o /dev/null -w '%{http_code}\n' -c "$cookie" -b "$cookie" -X POST http://127.0.0.1:3000/api/v1/projects/boss-console/attachments/att-missing/analyze +``` + +- [ ] **Step 2: 运行,确认不是成功状态** + +Expected: 404/400。 + +- [ ] **Step 3: 在 `boss-data.ts` 中增加附件消息 helper** + +新增: + +```ts +export async function appendAttachmentMessage(payload: { + projectId: string; + senderLabel: string; + body: string; + attachment: MessageAttachment; +}) { + return appendProjectMessage({ + projectId: payload.projectId, + senderLabel: payload.senderLabel, + body: payload.body, + kind: "attachment", + attachment: payload.attachment, + }); +} +``` + +并把 `appendProjectMessage` 扩展为支持: + +```ts +attachment?: MessageAttachment; +``` + +- [ ] **Step 4: 在 `boss-master-agent.ts` 增加 `attachment_analysis` 任务类型** + +最小接口: + +```ts +export async function queueAttachmentAnalysisTask(params: AttachmentAnalysisTaskPayload) { + return queueMasterAgentTask({ + taskType: "attachment_analysis", + projectId: params.projectId, + requestText: `请分析附件:${params.fileName}`, + payload: params, + }); +} +``` + +并在任务完成回写时,新增: + +```ts +await appendProjectMessage({ + projectId: task.projectId, + sender: "master", + senderLabel: "主 Agent", + body: shortSummary, + kind: "text", +}); + +await appendProjectMessage({ + projectId: task.projectId, + sender: "master", + senderLabel: "主 Agent", + body: cardTitle, + kind: "analysis_card", +}); +``` + +- [ ] **Step 5: 在上传 route 中自动创建分析任务** + +规则: + +```ts +if (attachment.analysisState === "queued_auto") { + await queueAttachmentAnalysisTask(...); +} +``` + +- [ ] **Step 6: 实现手动分析接口** + +```ts +export async function POST(...) { + const attachment = await findProjectAttachment(projectId, attachmentId); + if (!attachment) return NextResponse.json({ ok: false, message: "ATTACHMENT_NOT_FOUND" }, { status: 404 }); + if (attachment.analysisState !== "ready_manual" && attachment.analysisState !== "failed") { + return NextResponse.json({ ok: false, message: "ATTACHMENT_ANALYZE_NOT_ALLOWED" }, { status: 400 }); + } + const task = await queueAttachmentAnalysisTask(...); + return NextResponse.json({ ok: true, taskId: task.taskId }); +} +``` + +- [ ] **Step 7: 验证自动/手动状态判定** + +Run: + +```bash +tmpdir=$(mktemp -d) +cookie="$tmpdir/cookies.txt" +curl -sS -c "$cookie" -b "$cookie" -H 'Content-Type: application/json' -d '{}' http://127.0.0.1:3000/api/auth/login >/dev/null +curl -sS -c "$cookie" -b "$cookie" -F "file=@README.md;type=text/plain" http://127.0.0.1:3000/api/v1/projects/boss-console/attachments +``` + +Expected: 返回的附件消息 `analysisState=queued_auto`。 + +- [ ] **Step 8: Commit** + +```bash +git add src/lib/boss-data.ts src/lib/boss-master-agent.ts src/app/api/v1/projects/[projectId]/attachments/route.ts src/app/api/v1/projects/[projectId]/attachments/[attachmentId]/analyze/route.ts +git commit -m "feat: add attachment analysis task flow" +``` + +--- + +### Task 5: Web 端补 `我的 > 附件与存储` + +**Files:** +- Create: `src/app/me/storage/page.tsx` +- Modify: `src/app/me/page.tsx` +- Modify: `src/components/app-ui.tsx` +- Modify: `src/app/api/v1/settings` only if existing menu projection needs an extra field (otherwise keep scope local) + +- [ ] **Step 1: 先写 failing 路由访问验证** + +```bash +tmpdir=$(mktemp -d) +cookie="$tmpdir/cookies.txt" +curl -sS -c "$cookie" -b "$cookie" -H 'Content-Type: application/json' -d '{}' http://127.0.0.1:3000/api/auth/login >/dev/null +curl -sS -o /dev/null -w '%{http_code}\n' -c "$cookie" -b "$cookie" http://127.0.0.1:3000/me/storage +``` + +- [ ] **Step 2: 运行,确认当前不是 200** + +Expected: `404`。 + +- [ ] **Step 3: 创建页面并只做两层交互** + +`page.tsx` 结构最小如下: + +```tsx +export default async function StoragePage() { + const config = await getAttachmentStorageConfigForSession(); + return ( +
+
+

附件与存储

+

默认使用服务器文件存储,也可以切到阿里 OSS。

+
+ + {config.mode === "oss" ? : null} +
+ ); +} +``` + +- [ ] **Step 4: 在 `我的` 根页加菜单入口** + +```tsx +附件与存储 +``` + +保持它与 `账号与安全 / AI 账号 / 技能 / 关于` 同级,继续微信式简单列表,不引入大面板。 + +- [ ] **Step 5: 用最小表单接 `GET/PATCH/validate`** + +至少支持: + +- 选择 `服务器文件存储 / OSS` +- 选 `OSS` 后只显示 `阿里 OSS` +- 填 `AK / SK / Bucket / Endpoint / Region / Prefix` +- `测试并保存` +- `切回服务器文件存储` + +- [ ] **Step 6: 验证页面和配置链** + +Run: + +```bash +curl -sS http://127.0.0.1:3000/api/health +curl -sS -H 'Content-Type: application/json' -d '{}' http://127.0.0.1:3000/api/auth/login +``` + +并在浏览器打开: + +```text +http://127.0.0.1:3000/me/storage +``` + +Expected: 页面可访问,能显示默认 `server_file`。 + +- [ ] **Step 7: Commit** + +```bash +git add src/app/me/storage/page.tsx src/app/me/page.tsx src/components/app-ui.tsx +git commit -m "feat: add attachment storage settings page" +``` + +--- + +### Task 6: 原生 Android 接入附件选择、上传和消息渲染 + +**Files:** +- Modify: `android/app/src/main/java/com/hyzq/boss/BossApiClient.java` +- 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` +- Create: `android/app/src/test/java/com/hyzq/boss/AttachmentComposerStateTest.java` +- Create: `android/app/src/test/java/com/hyzq/boss/BossApiClientAttachmentTest.java` + +- [ ] **Step 1: 先写 failing 原生状态测试** + +在 `AttachmentComposerStateTest.java` 先写: + +```java +@Test +public void imageAndVideoRequireConfirmationButFileDoesNot() { + assertTrue(ProjectChatUiState.requiresAttachmentConfirmation("image")); + assertTrue(ProjectChatUiState.requiresAttachmentConfirmation("video")); + assertFalse(ProjectChatUiState.requiresAttachmentConfirmation("file")); +} +``` + +- [ ] **Step 2: 运行测试,确认当前失败** + +Run: + +```bash +cd android +./gradlew testDebugUnitTest --tests com.hyzq.boss.AttachmentComposerStateTest --no-daemon +``` + +Expected: FAIL,提示 helper 未实现。 + +- [ ] **Step 3: 在 `ProjectChatUiState` / `ProjectDetailActivity` 实现附件入口状态** + +最小 helper: + +```java +static boolean requiresAttachmentConfirmation(String sourceType) { + return "image".equals(sourceType) || "video".equals(sourceType); +} +``` + +并在 `ProjectDetailActivity` 中增加: + +- 左侧 `+` 按钮 +- 底部抽屉容器 +- 三个入口:图片 / 视频 / 文件 +- `ActivityResultLauncher` 或 `OpenDocument` 注册器 + +- [ ] **Step 4: 在 `BossApiClient` 增加 multipart 上传** + +实现最小接口: + +```java +public ApiResponse uploadAttachment(String projectId, String fileName, String mimeType, byte[] bytes, String sourceType) throws Exception +``` + +以及: + +```java +public ApiResponse analyzeAttachment(String projectId, String attachmentId) throws Exception +``` + +- [ ] **Step 5: 在 `BossUi` 补附件消息卡片** + +新增: + +```java +buildAttachmentMessageCard(...) +buildAttachmentAnalysisStateChip(...) +``` + +要求: + +- 图片:缩略图占位 + 文件名 + 状态 +- 视频:封面占位 + 文件名 + 状态 +- 文件:文件图标 + 文件名 + 大小 + 状态 + +- [ ] **Step 6: 运行原生测试和 debug 构建** + +Run: + +```bash +cd android +./gradlew testDebugUnitTest --tests com.hyzq.boss.AttachmentComposerStateTest --tests com.hyzq.boss.BossApiClientAttachmentTest :app:compileDebugJavaWithJavac assembleDebug --no-daemon +``` + +Expected: BUILD SUCCESSFUL。 + +- [ ] **Step 7: Commit** + +```bash +git add android/app/src/main/java/com/hyzq/boss/BossApiClient.java 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/test/java/com/hyzq/boss/AttachmentComposerStateTest.java android/app/src/test/java/com/hyzq/boss/BossApiClientAttachmentTest.java +git commit -m "feat: add native chat attachment flow" +``` + +--- + +### Task 7: 原生端补附件动作、分析状态和下载/预览 + +**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/AndroidManifest.xml` +- Test: `android/app/src/test/java/com/hyzq/boss/ProjectDetailActivityUiTest.java` + +- [ ] **Step 1: 先写 failing UI 测试,验证附件状态动作** + +在 `ProjectDetailActivityUiTest.java` 增补: + +```java +@Test +public void manualAnalysisAttachmentShowsActionChip() { + // render 一个 analysisState=ready_manual 的 attachment message + // 断言 UI 中出现“让 AI 分析” +} +``` + +- [ ] **Step 2: 运行测试,确认当前失败** + +Run: + +```bash +cd android +./gradlew testDebugUnitTest --tests com.hyzq.boss.ProjectDetailActivityUiTest --no-daemon +``` + +Expected: FAIL。 + +- [ ] **Step 3: 在消息卡片中增加动作** + +规则: + +- `queued_auto`:显示“自动分析排队中” +- `processing`:显示“AI 分析中” +- `ready_manual`:显示按钮“让 AI 分析” +- `completed`:显示摘要 +- `failed`:显示“重试分析” + +- [ ] **Step 4: 接通下载/预览行为** + +最小行为: + +- 图片:打开下载 URL +- 视频:打开下载 URL +- 文件:打开下载 URL + +先用系统浏览器或下载器打开,不在这轮强做自定义预览器。 + +- [ ] **Step 5: 运行 UI 测试与编译** + +Run: + +```bash +cd android +./gradlew testDebugUnitTest --tests com.hyzq.boss.ProjectDetailActivityUiTest :app:compileDebugJavaWithJavac assembleDebug --no-daemon +``` + +Expected: BUILD SUCCESSFUL。 + +- [ ] **Step 6: 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/AndroidManifest.xml android/app/src/test/java/com/hyzq/boss/ProjectDetailActivityUiTest.java +git commit -m "feat: add attachment analysis states to native chat" +``` + +--- + +### Task 8: 文档、联调、发包、部署 + +**Files:** +- Modify: `README.md` +- Modify: `docs/architecture/current_runtime_and_deploy_status_cn.md` +- Modify: `docs/architecture/api_and_service_inventory_cn.md` +- Modify: `docs/architecture/ai_handoff_index_cn.md` +- Modify: `android/app/build.gradle` +- Modify: `public/downloads/*`(如果发 release) + +- [ ] **Step 1: 本地完整验证** + +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 +``` + +Expected: + +- lint 通过 +- build 通过 +- 两个 health 都返回 `ok:true` + +- [ ] **Step 2: Android 验证和打包** + +Run: + +```bash +cd /Users/kris/code/boss/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 +``` + +Expected: + +- Android 测试和构建成功 +- 产出新版本 APK / AAB + +- [ ] **Step 3: 部署服务器并验证** + +Run: + +```bash +cd /Users/kris/code/boss +./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: + +- 远端本机 health 正常 +- 公网 health 正常 + +- [ ] **Step 4: 文档同步** + +把以下事实写回文档: + +- 默认服务器文件存储已可用 +- `我的 > 附件与存储` 已上线 +- 阿里 OSS 私有桶已接入 +- 图片 / PDF / 文本自动分析 +- 视频 / Office / 大文件手动分析 +- 原生聊天附件入口和分析状态说明 + +- [ ] **Step 5: Commit** + +```bash +git add README.md docs/architecture/current_runtime_and_deploy_status_cn.md docs/architecture/api_and_service_inventory_cn.md docs/architecture/ai_handoff_index_cn.md android/app/build.gradle public/downloads +git commit -m "chore: publish attachment storage release" +``` + +--- + +## Self-Review + +### Spec coverage + +- 原生附件入口:Task 6、Task 7 覆盖 +- 默认服务器文件存储:Task 2 覆盖 +- 阿里 OSS:Task 3 覆盖 +- 用户级存储配置:Task 1、Task 3、Task 5 覆盖 +- 统一下载入口:Task 2 覆盖 +- 自动/手动分析:Task 4 覆盖 +- 主 Agent 分析回写:Task 4 覆盖 +- Web 简化配置页:Task 5 覆盖 +- 文档、部署、发包:Task 8 覆盖 + +### Placeholder scan + +- 没有 `TODO / TBD / implement later / similar to task N` +- 每个任务都给出了具体文件、命令和最小代码形状 + +### Type consistency + +- `AttachmentStorageMode` 统一使用 `server_file | oss` +- `OssProvider` 统一使用 `aliyun_oss` +- `MessageKind` 统一新增 `attachment / analysis_card` +- `AttachmentAnalysisState` 统一使用 `queued_auto / ready_manual / processing / completed / failed` +