feat: 任务附件上传(图片/文件随任务提交)
B-① 附件上传(⑧ ★ POST /api/tasks/:id/attachments):
后端:
- 加 @fastify/multipart;schema/db.ts tasks.attachments 列(幂等迁移)
- types.Attachment{name,type,path} + Task.attachments;mappers 映射
- store.addAttachments(追加元数据 + 广播 task.updated)
- POST /api/tasks/:id/attachments:multipart files[](单文件≤25MB/单次≤10),
落盘 <data>/tasks/<id>/attachments/,文件名消毒,返回全部附件
前端:
- api.uploadAttachments(FormData multipart)
- 新建任务表单加文件选择 + 已选列表;创建任务后自动上传附件
验证:typecheck 干净;206 测试通过;前端 build 通过。
(端到端需重启 daemon 加载新端点——与后续 SSE/takeover 一并重启。)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+30
-1
@@ -13,9 +13,18 @@ import { git, removeWorktree } from '../executor/worktree.js';
|
||||
import { cleanupTaskRunArtifacts } from '../executor/cleanup.js';
|
||||
import { resolveLogo, LOGO_MIME } from './logo.js';
|
||||
import { readTranscript, TranscriptError } from '../executor/transcript.js';
|
||||
import { createReadStream } from 'node:fs';
|
||||
import { createReadStream, createWriteStream, mkdirSync } from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { join, basename } from 'node:path';
|
||||
import { pipeline } from 'node:stream/promises';
|
||||
import multipart from '@fastify/multipart';
|
||||
import { createUsageFetcher, type UsageInfo } from '../daemon/usage.js';
|
||||
|
||||
/** 数据根目录(与 protocol/worktree 同约定):<MAESTRO_DATA_DIR 或 ~/.maestro> */
|
||||
function dataDir(): string {
|
||||
return process.env.MAESTRO_DATA_DIR ?? join(homedir(), '.maestro');
|
||||
}
|
||||
|
||||
/** Project 出参:附加 hasTodoJson(<repoPath>/todo/todo.json 是否存在,每次序列化时算) */
|
||||
function projectOut(p: Project): Project & { hasTodoJson: boolean } {
|
||||
return { ...p, hasTodoJson: hasTodoJson(p.repoPath) };
|
||||
@@ -39,6 +48,9 @@ export function buildServer(opts: ApiOptions): FastifyInstance {
|
||||
const classify = opts.classify ?? ((title, description, cwd) => classifyComplexity(title, description, { cwd }));
|
||||
const app = Fastify({ logger: opts.logger ?? false });
|
||||
|
||||
// 附件上传:multipart/form-data(单文件 ≤25MB,单次 ≤10 个)
|
||||
app.register(multipart, { limits: { fileSize: 25 * 1024 * 1024, files: 10 } });
|
||||
|
||||
// Store 错误 → 400(业务校验),其余 → 500
|
||||
app.setErrorHandler((err, _req, reply) => {
|
||||
if (err instanceof StoreError) return reply.code(400).send({ error: err.message });
|
||||
@@ -218,6 +230,23 @@ export function buildServer(opts: ApiOptions): FastifyInstance {
|
||||
return t;
|
||||
});
|
||||
|
||||
// 附件上传(multipart files[]):落盘 <data>/tasks/<id>/attachments/,元数据写 tasks.attachments
|
||||
app.post('/api/tasks/:id/attachments', async (req, reply) => {
|
||||
const { id } = req.params as { id: string };
|
||||
if (!store.getTask(id)) return reply.code(404).send({ error: `任务不存在: ${id}` });
|
||||
const dir = join(dataDir(), 'tasks', id, 'attachments');
|
||||
mkdirSync(dir, { recursive: true });
|
||||
const saved: Array<{ name: string; type: string; path: string }> = [];
|
||||
for await (const part of req.files()) {
|
||||
const safe = basename(part.filename).replace(/[^\w.\-]/g, '_') || `file-${Date.now()}`;
|
||||
await pipeline(part.file, createWriteStream(join(dir, safe)));
|
||||
if (part.file.truncated) return reply.code(400).send({ error: `文件 ${part.filename} 超过 25MB 上限` });
|
||||
saved.push({ name: part.filename, type: part.mimetype, path: `tasks/${id}/attachments/${safe}` });
|
||||
}
|
||||
if (saved.length === 0) return reply.code(400).send({ error: '未收到文件' });
|
||||
return { attachments: store.addAttachments(id, saved) };
|
||||
});
|
||||
|
||||
// 部分更新任务(title/priority/complexity;complexity 重置逻辑在 Store.patchTask)
|
||||
app.patch('/api/tasks/:id', (req) => {
|
||||
const { id } = req.params as { id: string };
|
||||
|
||||
Reference in New Issue
Block a user