server.ts 2.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
  1. import fs from 'node:fs'
  2. import path from 'node:path'
  3. import { fileURLToPath } from 'node:url'
  4. import cookie from '@fastify/cookie'
  5. import multipart from '@fastify/multipart'
  6. import fastifyStatic from '@fastify/static'
  7. import Fastify, { type FastifyServerOptions } from 'fastify'
  8. import type { ApiErrorBody } from '@agent-server/shared'
  9. import type { App } from '../app'
  10. import { config } from '../config'
  11. import { AppError } from '../lib/errors'
  12. import adminRoutes from './routes/admin'
  13. import v1Routes from './routes/v1'
  14. import { registerGateway } from '../mcp-gateway/server'
  15. export async function buildServer(app: App, opts: { logger: FastifyServerOptions['loggerInstance'] }) {
  16. const f = Fastify({ loggerInstance: opts.logger, trustProxy: config.trustProxy, bodyLimit: 2 * 1048576 })
  17. await f.register(cookie)
  18. await f.register(multipart)
  19. f.setErrorHandler((err, req, reply) => {
  20. if (err instanceof AppError) {
  21. const body: ApiErrorBody = { error: { code: err.code, message: err.message, details: err.details } }
  22. return reply.code(err.status).send(body)
  23. }
  24. const status = (err as { statusCode?: number }).statusCode ?? 500
  25. if (status >= 500) req.log.error({ err }, 'unhandled error')
  26. const body: ApiErrorBody = { error: { code: status >= 500 ? 'internal_error' : 'bad_request', message: status >= 500 ? '服务器内部错误' : (err as Error).message } }
  27. return reply.code(status).send(body)
  28. })
  29. f.get('/healthz', async () => ({ ok: true }))
  30. f.get('/readyz', async () => {
  31. app.db.$client.prepare('select 1').get()
  32. return { ok: true, runs: app.runs.stats() }
  33. })
  34. await f.register(v1Routes, { prefix: '/v1', app })
  35. registerGateway(f, app.gatewayTokens)
  36. await f.register(adminRoutes, { prefix: '/admin/v1', app })
  37. // 生产环境由服务托管后台静态资源,同源访问
  38. // 源码运行时位于 src/http,打包后位于 dist,两种情况都向上寻找 apps/admin/dist
  39. const here = path.dirname(fileURLToPath(import.meta.url))
  40. const adminDist = config.adminDist
  41. ?? [path.resolve(here, '../../../admin/dist'), path.resolve(here, '../../admin/dist')].find(p => fs.existsSync(path.join(p, 'index.html')))
  42. if (adminDist && fs.existsSync(path.join(adminDist, 'index.html'))) {
  43. await f.register(fastifyStatic, { root: adminDist, prefix: '/admin/', wildcard: false })
  44. f.get('/admin/*', (_req, reply) => reply.sendFile('index.html'))
  45. f.get('/', (_req, reply) => reply.redirect('/admin/'))
  46. }
  47. return f
  48. }