Files
2026-07-13 13:15:38 +08:00

209 lines
7.6 KiB
JavaScript

import http from 'node:http'
import { API_HOST, API_PORT, FAL_API_KEY, HUNYUAN_API_BASE, RODIN_API_KEY, TRIPO_API_KEY } from './server/config.mjs'
import { assertLocalDiagnosticsRequest, readJsonBody, sendJson, setCorsHeaders } from './server/http-utils.mjs'
import { createRequestId, logEvent, readRecentLogs, summarizeError, summarizePayload } from './server/logger.mjs'
import { importLocalModel, proxyModel, serveLocalModel } from './server/model-store.mjs'
import { createFalTask, getFalHealth, getFalTask } from './server/providers/fal.mjs'
import { createHunyuanTask, getHunyuanHealth, getHunyuanTask } from './server/providers/hunyuan.mjs'
import { createRodinTask, getRodinHealth, getRodinTask } from './server/providers/rodin.mjs'
import { createTripoTask, getTripoHealth, getTripoTask } from './server/providers/tripo.mjs'
import { analyzeAssetImage, getVisionHealth } from './server/providers/vision.mjs'
const DEFAULT_GENERATION_PROVIDER = 'rodin'
const server = http.createServer(async (request, response) => {
const requestId = createRequestId()
const startedAt = Date.now()
let url = null
try {
setCorsHeaders(response)
response.setHeader('X-Request-Id', requestId)
if (request.method === 'OPTIONS') {
response.writeHead(204)
response.end()
return
}
url = new URL(request.url, `http://${request.headers.host}`)
await logEvent('info', 'http.request', {
requestId,
method: request.method,
path: url.pathname,
query: Object.fromEntries(url.searchParams.entries()),
})
if (request.method === 'GET' && url.pathname === '/api/3d/health') {
const payload = {
ok: true,
providers: {
tripo: getTripoHealth(),
rodin: getRodinHealth(),
hunyuan: getHunyuanHealth(),
fal: getFalHealth(),
vision: getVisionHealth(),
},
}
sendJson(response, 200, payload)
await logEvent('info', 'http.response', { requestId, path: url.pathname, status: 200, durationMs: Date.now() - startedAt })
return
}
if (request.method === 'GET' && url.pathname === '/api/3d/logs') {
assertLocalDiagnosticsRequest(request)
const payload = await readRecentLogs(url.searchParams.get('limit') || 100)
sendJson(response, 200, payload)
await logEvent('info', 'http.response', { requestId, path: url.pathname, status: 200, durationMs: Date.now() - startedAt, entries: payload.entries.length })
return
}
if (request.method === 'POST' && url.pathname === '/api/3d/analyze') {
const payload = await readJsonBody(request)
await logEvent('info', 'asset.analyze.start', {
requestId,
payload: summarizePayload(payload),
})
const insight = await analyzeAssetImage(payload)
sendJson(response, 200, insight)
await logEvent('info', 'asset.analyze.success', {
requestId,
provider: insight.provider,
configured: insight.configured,
status: insight.status,
categoryId: insight.categoryId,
durationMs: Date.now() - startedAt,
})
return
}
if (request.method === 'POST' && url.pathname === '/api/3d/generate') {
const payload = await readJsonBody(request)
const provider = payload.provider || DEFAULT_GENERATION_PROVIDER
await logEvent('info', 'generation.create.start', {
requestId,
provider,
payload: summarizePayload(payload),
})
const task = await createGenerationTask(provider, payload)
sendJson(response, 200, task)
await logEvent('info', 'generation.create.success', {
requestId,
provider,
taskId: task.taskId,
status: task.status,
durationMs: Date.now() - startedAt,
})
return
}
if (request.method === 'GET' && url.pathname.startsWith('/api/3d/status/')) {
const taskId = decodeURIComponent(url.pathname.replace('/api/3d/status/', ''))
const provider = url.searchParams.get('provider') || DEFAULT_GENERATION_PROVIDER
const task = await getGenerationTask(provider, taskId)
sendJson(response, 200, task)
await logEvent('info', 'generation.status', {
requestId,
provider,
taskId,
status: task.status,
progress: task.progress,
hasModelUrl: Boolean(task.modelUrl),
error: task.error,
durationMs: Date.now() - startedAt,
})
return
}
if (request.method === 'GET' && url.pathname === '/api/3d/model') {
await proxyModel(url, response)
await logEvent('info', 'model.proxy.success', { requestId, durationMs: Date.now() - startedAt })
return
}
if (request.method === 'POST' && url.pathname === '/api/3d/local-model') {
const model = await importLocalModel(request, url)
sendJson(response, 200, model)
await logEvent('info', 'model.import.success', {
requestId,
taskId: model.taskId,
modelUrl: model.modelUrl,
fileName: model.fileName,
durationMs: Date.now() - startedAt,
})
return
}
if (request.method === 'GET' && url.pathname.startsWith('/api/3d/local-model/')) {
await serveLocalModel(url, response)
await logEvent('info', 'model.local.success', { requestId, path: url.pathname, durationMs: Date.now() - startedAt })
return
}
sendJson(response, 404, { error: 'Not found' })
await logEvent('warn', 'http.not_found', { requestId, path: url.pathname, durationMs: Date.now() - startedAt })
} catch (error) {
if (response.headersSent) {
await logEvent('error', 'http.stream_error', {
requestId,
path: url?.pathname,
durationMs: Date.now() - startedAt,
error: summarizeError(error),
})
response.destroy(error)
return
}
const status = error.status || 500
sendJson(response, status, {
error: error.message || 'Server error',
detail: error.detail,
})
await logEvent(status >= 500 ? 'error' : 'warn', 'http.error', {
requestId,
method: request.method,
path: url?.pathname,
status,
durationMs: Date.now() - startedAt,
error: summarizeError(error),
})
}
})
server.listen(API_PORT, API_HOST, () => {
console.log(`Bio demo API running at http://${API_HOST}:${API_PORT}`)
console.log(TRIPO_API_KEY ? 'Tripo API key loaded from environment.' : 'TRIPO_API_KEY is missing. Add it to .env.local.')
console.log(RODIN_API_KEY ? 'Rodin API key loaded from environment.' : 'RODIN_API_KEY is missing. Add it to .env.local.')
console.log(FAL_API_KEY ? 'Fal API key loaded from environment.' : 'FAL_API_KEY is missing. Add it to .env.local.')
console.log(getVisionHealth().configured ? 'Vision analysis provider configured.' : 'Vision analysis is not configured. Add OPENAI_API_KEY to .env.local.')
console.log(`Hunyuan3D local provider: ${HUNYUAN_API_BASE}`)
logEvent('info', 'api.start', {
host: API_HOST,
port: API_PORT,
providers: {
tripo: Boolean(TRIPO_API_KEY),
rodin: Boolean(RODIN_API_KEY),
fal: Boolean(FAL_API_KEY),
hunyuan: Boolean(HUNYUAN_API_BASE),
vision: getVisionHealth().configured,
},
})
})
function createGenerationTask(provider, payload) {
if (provider === 'hunyuan') return createHunyuanTask(payload)
if (provider === 'fal') return createFalTask(payload)
if (provider === 'tripo') return createTripoTask(payload)
return createRodinTask(payload)
}
function getGenerationTask(provider, taskId) {
if (provider === 'hunyuan') return getHunyuanTask(taskId)
if (provider === 'fal') return getFalTask(taskId)
if (provider === 'tripo') return getTripoTask(taskId)
return getRodinTask(taskId)
}