quantumbyteQB
§ · Nested detail page

项目生命周期 深度解析

项目从一条意图起步:首条消息与 Project 一起创建;后续文件变成对象存储链接;生成应用通过 Worker 亲和端口提供预览。

§ 00 · IMPORT CHAIN

导入链与契约

列表接口提供搜索、分页和更新时间排序,创建接口则要求非空 prompt 并截断生成标题。项目不是空壳:首个 PENDING 消息在嵌套 create 中同步落库 对应源码落点是 src/app/api/projects/route.ts:5、src/app/api/projects/route.ts:7、src/app/api/projects/route.ts:8、src/app/api/projects/route.ts:11、src/app/api/projects/route.ts:15、src/app/api/projects/route.ts:18、src/app/api/projects/route.ts:25。把这些相邻行放在同一条执行链里看,可以确认它不是文档里的愿景描述,而是已经落到请求、状态和界面反馈中的实现;同时也能辨认出失败时由哪一层收敛、下一次刷新从何处恢复,以及扩容后最先需要替换的边界。

src/app/api/projects/route.ts · L32-60TypeScript · Python
  32  export async function POST(req: NextRequest) {
  33    const body = await req.json().catch(() => null);
  34    const prompt = typeof body?.prompt === 'string' ? body.prompt.trim() : '';
  35    if (!prompt) {
  36      return NextResponse.json({ error: 'prompt is required' }, { status: 400 });
  37    }
  38  
  39    const title = prompt.length > 48 ? `${prompt.slice(0, 48)}…` : prompt;
  40    const project = await prisma.project.create({
  41      data: {
  42        id: ulid(),
  43        prompt,
  44        title,
  45        generationStatus: 'DRAFT',
  46        // Seed the prompt as the first chat turn (PENDING) so the blueprint
  47        // chat renders it and the orchestrator triggers the agent immediately.
  48        chatMessages: {
  49          create: {
  50            id: ulid(),
  51            role: 'USER',
  52            content: prompt,
  53            status: 'PENDING',
  54          },
  55        },
  56      },
  57    });
  58  
  59    return NextResponse.json(project, { status: 201 });
  60  }
§ 01 · CORE IMPL

核心实现,逐段拆

▸附件先上传,Agent 只看 URL

附件接口先确认项目和 multipart 数据,再限制最多五个、单个十 MB。安全文件名只保留有限字符,实际对象以 ULID 前缀存入项目专属路径 对应源码落点是 src/app/api/projects/[id]/attachments/route.ts:6、src/app/api/projects/[id]/attachments/route.ts:11、src/app/api/projects/[id]/attachments/route.ts:12、src/app/api/projects/[id]/attachments/route.ts:14、src/app/api/projects/[id]/attachments/route.ts:18、src/app/api/projects/[id]/attachments/route.ts:23、src/app/api/projects/[id]/attachments/route.ts:28。把这些相邻行放在同一条执行链里看,可以确认它不是文档里的愿景描述,而是已经落到请求、状态和界面反馈中的实现;同时也能辨认出失败时由哪一层收敛、下一次刷新从何处恢复,以及扩容后最先需要替换的边界。

src/app/api/projects/[id]/attachments/route.ts · L32-75TypeScript · Python
  32    const files = form.getAll('files').filter((f): f is File => f instanceof File);
  33    if (files.length === 0) {
  34      return NextResponse.json({ error: 'no files provided' }, { status: 400 });
  35    }
  36    if (files.length > MAX_FILES) {
  37      return NextResponse.json({ error: `max ${MAX_FILES} files per upload` }, { status: 400 });
  38    }
  39    for (const file of files) {
  40      if (file.size > MAX_FILE_BYTES) {
  41        return NextResponse.json(
  42          { error: `${file.name} exceeds the 10 MB limit` },
  43          { status: 413 }
  44        );
  45      }
  46    }
  47  
  48    const uploaded = [];
  49    for (const file of files) {
  50      const assetId = ulid();
  51      const filename = `${assetId}-${safeName(file.name)}`;
  52      const key = `attachments/${id}/${filename}`;
  53      const mimeType = file.type || 'application/octet-stream';
  54      const url = await uploadToS3(key, Buffer.from(await file.arrayBuffer()), mimeType);
  55      const asset = await prisma.asset.create({
  56        data: {
  57          id: assetId,
  58          projectId: id,
  59          filename,
  60          originalFilename: file.name,
  61          url,
  62          size: file.size,
  63          mimeType,
  64        },
  65      });
  66      uploaded.push({
  67        id: asset.id,
  68        url: asset.url,
  69        filename: file.name,
  70        mimeType,
  71        size: file.size,
  72      });
  73    }
  74  
  75    return NextResponse.json({ attachments: uploaded }, { status: 201 });

▸预览状态压缩成三态

预览接口读取项目亲和端口:没有端口就 hidden,有端口但访问失败是 starting,只要收到任何 HTTP 响应即 ready。编译错误页也被认为比空白更有用 对应源码落点是 src/app/api/projects/[id]/preview/route.ts:4、src/app/api/projects/[id]/preview/route.ts:7、src/app/api/projects/[id]/preview/route.ts:9、src/app/api/projects/[id]/preview/route.ts:10、src/app/api/projects/[id]/preview/route.ts:11、src/app/api/projects/[id]/preview/route.ts:12、src/app/api/projects/[id]/preview/route.ts:15。把这些相邻行放在同一条执行链里看,可以确认它不是文档里的愿景描述,而是已经落到请求、状态和界面反馈中的实现;同时也能辨认出失败时由哪一层收敛、下一次刷新从何处恢复,以及扩容后最先需要替换的边界。

src/app/api/projects/[id]/preview/route.ts · L17-46TypeScript · Python
  17  export async function GET(
  18    _req: NextRequest,
  19    { params }: { params: Promise<{ id: string }> }
  20  ) {
  21    const { id } = await params;
  22    const project = await prisma.project.findUnique({
  23      where: { id },
  24      select: { previewPort: true },
  25    });
  26    if (!project) {
  27      return NextResponse.json({ error: 'project not found' }, { status: 404 });
  28    }
  29  
  30    const port = project.previewPort;
  31    if (port === null) {
  32      return NextResponse.json({ state: 'hidden', port: null, previewUrl: null });
  33    }
  34  
  35    const previewUrl = `http://localhost:${port}/`;
  36    try {
  37      await fetch(`http://127.0.0.1:${port}/`, {
  38        method: 'GET',
  39        signal: AbortSignal.timeout(PING_TIMEOUT_MS),
  40        cache: 'no-store',
  41      });
  42      return NextResponse.json({ state: 'ready', port, previewUrl });
  43    } catch {
  44      return NextResponse.json({ state: 'starting', port, previewUrl });
  45    }
  46  }
§ 02 · KEY FLOW

关键流程

prompt → Project + PENDING chat → worker workspace
assets → S3 URL → chat markdown
affinity port → health proxy → iframe

后续消息把已上传附件渲染成 Markdown 链接块,与文本合并后入库。若文本和附件都为空返回 400,项目不存在返回 404;成功时只确认消息已接受,而不等待 Agent 对应源码落点是 src/app/api/projects/[id]/chat/route.ts:21、src/app/api/projects/[id]/chat/route.ts:22、src/app/api/projects/[id]/chat/route.ts:23、src/app/api/projects/[id]/chat/route.ts:27、src/app/api/projects/[id]/chat/route.ts:28、src/app/api/projects/[id]/chat/route.ts:37、src/app/api/projects/[id]/chat/route.ts:41。把这些相邻行放在同一条执行链里看,可以确认它不是文档里的愿景描述,而是已经落到请求、状态和界面反馈中的实现;同时也能辨认出失败时由哪一层收敛、下一次刷新从何处恢复,以及扩容后最先需要替换的边界。

§ 03 · EDGE CASES

边界 / 失败模式

  • 预览目前依赖共享 localhost,跨主机部署需要服务发现或反向代理。
  • 任何 HTTP 状态都算 ready,健康检查不代表应用业务可用。
  • 附件大小与数量有界,但源码未显示病毒扫描和 MIME 白名单。
Sub-page · 2026-07-15 Source · QuantumByteOSS/quantumbyte ↑ quantumbyte 主页面