Files
boss/docs/superpowers/plans/2026-03-29-chat-attachments-storage-and-ai-processing.md

996 lines
29 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 聊天附件、双存储与 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<AttachmentStorageProvider> {
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 (
<main className="space-y-4">
<section>
<h1>附件与存储</h1>
<p>默认使用服务器文件存储,也可以切到阿里 OSS</p>
</section>
<StorageModeCard config={config} />
{config.mode === "oss" ? <AliyunOssForm config={config} /> : null}
</main>
);
}
```
- [ ] **Step 4: 在 `我的` 根页加菜单入口**
```tsx
<Link href="/me/storage">附件与存储</Link>
```
保持它与 `账号与安全 / 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<String>``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 覆盖
- 阿里 OSSTask 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`