docs: add attachment storage implementation plan
This commit is contained in:
@@ -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<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 覆盖
|
||||
- 阿里 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`
|
||||
|
||||
Reference in New Issue
Block a user