import { NextResponse } from 'next/server' import { getServerSession } from 'next-auth' import { authOptions } from '@/lib/auth' import { desktopAssert, desktopPortal, DesktopClientError, DESKTOP_CLIENT_ID } from '@/lib/desktop-client/contract.mjs' import {isAllowedCorsOrigin} from '@/lib/cors-origin' import { approveDesktop, desktopUser, exchangeDesktop, getDesktopAsset, getDesktopProject, listDesktopDevices, listDesktopProjects, putDesktopAsset, putDesktopProject, requireDesktop, revokeDesktop, deleteDesktopProject } from '@/lib/desktop-client/service' export const runtime='nodejs' export const dynamic='force-dynamic' const headers={'Cache-Control':'private, no-store','X-Content-Type-Options':'nosniff'} async function body(request:Request,max=1600000){ desktopAssert(Number(request.headers.get('content-length')||0)<=max,'请求内容过大',413) const reader=request.body?.getReader();const chunks:Uint8Array[]=[];let size=0 if(reader)while(true){const part=await reader.read();if(part.done)break;size+=part.value.length;if(size>max){await reader.cancel();throw new DesktopClientError('请求内容过大',413)}chunks.push(part.value)} return Buffer.concat(chunks) } async function browserOwner(request:Request){ const origin=request.headers.get('origin') desktopAssert(origin&&isAllowedCorsOrigin(request,origin),'请从素材库授权页面操作',403,'invalid_origin') desktopPortal(origin) const session=await getServerSession(authOptions);return desktopUser((session?.user as {id?:string})?.id||'') } async function route(request:Request,{params}:{params:{path:string[]}}){ try{ const parts=params.path,url=new URL(request.url),method=request.method let value:unknown if(parts.length===1&&parts[0]==='config'&&method==='GET')return NextResponse.json({clientId:DESKTOP_CLIENT_ID,enabled:process.env.DETAIL_GENERATOR_ENABLED==='true',pkce:'S256'},{headers}) if(parts.length===1&&parts[0]==='authorize'&&method==='POST'){const user=await browserOwner(request);value=await approveDesktop(user.id,JSON.parse((await body(request,5000)).toString()))} else if(parts.length===1&&parts[0]==='token'&&method==='POST')value=await exchangeDesktop(JSON.parse((await body(request,5000)).toString())) else if(parts[0]==='devices'){ const session=await getServerSession(authOptions);const user=await desktopUser((session?.user as {id?:string})?.id||'') if(method==='GET'&&parts.length===1)value={devices:await listDesktopDevices(user.id)} else if(method==='DELETE'&&parts.length===2){await browserOwner(request);await revokeDesktop(parts[1],user.id);value={revoked:true}} else throw new DesktopClientError('接口不存在',404) }else{ const auth=await requireDesktop(request),ownerId=auth.account.id if(parts[0]==='session'&&parts.length===1){if(method==='GET')value={account:auth.account,expiresAt:auth.expiresAt};else if(method==='DELETE'){await revokeDesktop(auth.id,ownerId);value={revoked:true}}else throw new DesktopClientError('接口不存在',404)} else if(parts[0]==='capabilities'&&parts.length===1&&method==='GET')value={documentVersions:[1,2,3],projectDeletion:true,nativeFiles:{streaming:true,maxFileBytes:536870912,maxProjectBytes:1073741824,source:'sha256',range:true},catalog:{brands:true,materials:true,products:true},modelExecution:'client',creationTasks:{version:1,leases:true,recovery:'read-only'}} else if(parts[0]==='tasks'){ const tasks=await import('@/lib/desktop-client/tasks') if(parts.length===2&&method==='GET')value=await tasks.listCreationTasks(auth,parts[1]) else if(parts.length===3&&method==='GET')value=await tasks.getCreationTask(auth,parts[1],parts[2]) else if(parts.length===2&&method==='POST')value=await tasks.createCreationTask(auth,parts[1],JSON.parse((await body(request,900000)).toString())) else if(parts.length===4&&parts[3]==='claim'&&method==='POST')value=await tasks.claimCreationTask(auth,parts[1],parts[2]) else if(parts.length===3&&method==='PUT')value=await tasks.checkpointCreationTask(auth,parts[1],parts[2],JSON.parse((await body(request,900000)).toString())) else throw new DesktopClientError('接口不存在',404) } else if(parts[0]==='catalog'&&method==='GET'){ const catalog=await import('@/lib/desktop-client/catalog') if(parts.length===2&&parts[1]==='brands')value=await catalog.desktopBrands(url.searchParams) else if(parts.length===3&&parts[1]==='brands')value=await catalog.desktopBrand(parts[2]) else if(parts.length===2&&parts[1]==='materials')value=await catalog.desktopMaterials(ownerId,url.searchParams) else if(parts.length===2&&parts[1]==='products')value=await catalog.desktopProducts(ownerId,url.searchParams) else { let image if(parts.length===4&&parts[1]==='materials'&&parts[3]==='thumbnail')image=await catalog.desktopMaterialThumbnail(ownerId,parts[2]) else if(parts.length===5&&parts[1]==='products')image=await catalog.desktopProductImage(ownerId,parts[2],parts[3],parts[4]) else throw new DesktopClientError('接口不存在',404) return new NextResponse(Uint8Array.from(image.bytes).buffer,{headers:{...headers,'Content-Type':image.mime}}) } } else if(parts[0]==='models'&&method==='GET'&&parts.length===1){const {getAiModelRuntimeState}=await import('@/lib/ai-model-service');value=await getAiModelRuntimeState()} else if(parts[0]==='execution-models'&&method==='GET'&&parts.length===1){const {desktopExecutionModels}=await import('@/lib/desktop-client/models');value=await desktopExecutionModels()} else if(parts[0]==='agent-models'&&parts.length===1&&method==='GET'){ const {getAiModelRuntimeState,resolveAiModel}=await import('@/lib/ai-model-service');const config=await getAiModelRuntimeState();const models=[] for(const option of config.modelOptions.copy){const resolved=await resolveAiModel('copy',option.value);const base=new URL(resolved.binding.baseUrl);if(base.hostname==='api.vip98k.com'&&base.protocol==='https:'&&['openai-chat','anthropic-messages'].includes(resolved.protocol.id))models.push({key:option.value,id:resolved.binding.modelId,label:option.label,protocol:resolved.protocol.id,path:resolved.protocol.path})} value={models} } else if(parts[0]==='projects'){ if(method==='GET'&&parts.length===1)value=await listDesktopProjects(ownerId,url.searchParams.get('cursor')) else if(parts.length===3&&method==='GET')value=await getDesktopProject(ownerId,parts[1],parts[2]) else if(parts.length===3&&method==='DELETE')value=await deleteDesktopProject(ownerId,parts[1],parts[2],JSON.parse((await body(request,1000)).toString())) else if(parts.length===3&&parts[1]==='desktop'&&method==='PUT')value=await putDesktopProject(ownerId,parts[2],JSON.parse((await body(request)).toString())) else if(parts.length===5&&parts[1]==='desktop'&&parts[3]==='native'){ const native=await import('@/lib/desktop-client/native-storage') const reauthorize=async()=>{const current=await requireDesktop(request);desktopAssert(current.account.id===ownerId,'账户已变更',401);return current} if(method==='PUT')value=await native.putNativeFile(ownerId,parts[2],parts[4],request,reauthorize) else if(method==='GET')return await native.getNativeFile(ownerId,parts[2],parts[4],request,reauthorize) else throw new DesktopClientError('接口不存在',404) } else if(parts.length===5&&parts[3]==='assets'){ if(method==='PUT'&&parts[1]==='desktop')value=await putDesktopAsset(ownerId,parts[2],parts[4],await body(request,20*1024*1024),request.headers.get('content-type')||'') else if(method==='GET'){const asset=await getDesktopAsset(ownerId,parts[1],parts[2],parts[4]);return new NextResponse(Uint8Array.from(asset.bytes).buffer,{headers:{...headers,'Content-Type':asset.mime,'Content-Disposition':'attachment'}})} else throw new DesktopClientError('接口不存在',404) }else throw new DesktopClientError('接口不存在',404) }else throw new DesktopClientError('接口不存在',404) } return NextResponse.json(value,{headers}) }catch(error){ const known=error instanceof DesktopClientError||Boolean(error&&typeof error==='object'&&'status'in error) const item=error as {status?:number;code?:string;message?:string} return NextResponse.json({error:known?item.message:'客户端服务暂不可用,请稍后重试',code:known?item.code||'invalid_request':'service_unavailable'},{status:known?item.status||400:503,headers}) } } export {route as GET,route as POST,route as PUT,route as DELETE}