62 lines
5.3 KiB
TypeScript
62 lines
5.3 KiB
TypeScript
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 } 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]==='models'&&method==='GET'&&parts.length===1){const {getAiModelRuntimeState}=await import('@/lib/ai-model-service');value=await getAiModelRuntimeState()}
|
|
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&&parts[1]==='desktop'&&method==='PUT')value=await putDesktopProject(ownerId,parts[2],JSON.parse((await body(request)).toString()))
|
|
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}
|