gongxiangp2/lib/desktop-client/project-viewer.ts

61 lines
4.9 KiB
TypeScript

import { NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { isAllowedCorsOrigin } from '@/lib/cors-origin'
import { desktopAssert, DesktopClientError } from './contract.mjs'
import { deleteDesktopProject, desktopUser, getDesktopAsset, getDesktopProject, listDesktopProjects } from './service'
const headers = { 'Cache-Control': 'private, no-store', 'X-Content-Type-Options': 'nosniff' }
const viewerBrief = (brief: any) => brief ? {
name: brief.name, brandName: brief.brandName, identifier: brief.identifier, colorName: brief.colorName,
dimensions: brief.dimensions, note: brief.note,
...(['upload', 'library'].includes(brief.source) ? { source: brief.source } : {}),
...(['sku', 'spu', 'barcode'].includes(brief.identifierType) ? { identifierType: brief.identifierType } : {}),
...(Array.isArray(brief.images) ? { referenceImageCount: brief.images.length } : {}),
} : undefined
/** Cookie-authenticated viewing. Desktop write tokens are never issued to this page. */
export async function projectViewerRoute(request: Request, parts: string[] = []) {
try {
const session = await getServerSession(authOptions)
const account = await desktopUser((session?.user as { id?: string })?.id || '')
const method = request.method
if (method === 'GET' && parts.length === 0) {
const page = await listDesktopProjects(account.id, new URL(request.url).searchParams.get('cursor'))
return NextResponse.json({ items: page.items, nextCursor: page.nextCursor }, { headers })
}
desktopAssert(['web', 'desktop'].includes(parts[0]), '项目来源无效', 404)
if (parts.length === 2 && method === 'GET') {
const value = await getDesktopProject(account.id, parts[0], parts[1])
// Do not expose storage keys, model bindings, provider diagnostics or task internals.
const d = value.project.document
const document = d.version === 3 ? { version: 3, name: d.name, screens: d.screens.map((screen: any) => ({ id: screen.id, name: screen.name, width: screen.width, height: screen.height, previewSource: screen.previewSource })),
brief: viewerBrief(d.creation?.brief),
facts: d.creation?.facts, sellingPoints: d.creation?.sellingPoints, plan: d.creation?.plan } :
{ name: d.name, screens: d.screens || [], product: d.product, brief: viewerBrief(d.brief), brand: d.brand ? { name: d.brand.name } : undefined,
...(['library', 'new'].includes(d.inputSource) ? { inputSource: d.inputSource } : {}),
analysis: d.analysis, planning: d.planning, facts: d.facts, sellingPoints: d.sellingPoints, plan: d.plan }
return NextResponse.json({ source: value.source, project: { id: value.project.id, revision: value.project.revision, status: 'status' in value.project ? value.project.status : 'draft', updatedAt: value.project.updatedAt, document }, assets: value.assets.filter((a: any) => ['image/png','image/jpeg','image/webp'].includes(a.mime)).map((a: any) => ({ source: a.source, mime: a.mime })) }, { headers })
}
if (parts.length === 4 && parts[2] === 'assets' && method === 'GET') {
const asset = await getDesktopAsset(account.id, parts[0], parts[1], parts[3])
desktopAssert(['image/png', 'image/jpeg', 'image/webp'].includes(asset.mime), '此文件不能在网页预览', 415)
return new NextResponse(Uint8Array.from(asset.bytes).buffer, { headers: { ...headers, 'Content-Type': asset.mime, 'Content-Disposition': 'inline' } })
}
if (parts.length === 2 && method === 'DELETE') {
const origin = request.headers.get('origin')
desktopAssert(origin && isAllowedCorsOrigin(request, origin), '请从素材库页面删除项目', 403, 'invalid_origin')
desktopAssert(Number(request.headers.get('content-length') || 0) <= 1000, '删除参数过大', 413)
const reader = request.body?.getReader(); let bytes = 0; const chunks: Uint8Array[] = []
if (reader) while (true) { const part = await reader.read(); if (part.done) break; bytes += part.value.length; if (bytes > 1000) { await reader.cancel(); throw new DesktopClientError('删除参数过大', 413) } chunks.push(part.value) }
let input: unknown
try { input = JSON.parse(Buffer.concat(chunks).toString('utf8')) } catch { throw new DesktopClientError('删除参数无效') }
return NextResponse.json(await deleteDesktopProject(account.id, parts[0], parts[1], input), { headers })
}
throw new DesktopClientError('网页仅支持项目查看和删除;请在客户端编辑', 405, 'client_required')
} catch (error) {
const item = error as { status?: number; code?: string; message?: string }
const known = Number.isInteger(item?.status)
return NextResponse.json({ error: known ? item.message : '项目暂时无法读取,请重试', code: known ? item.code || 'invalid_request' : 'service_unavailable' }, { status: known ? item.status : 503, headers })
}
}