diff --git a/apps/cli/src/http/index.ts b/apps/cli/src/http/index.ts index 04b8e58f..7268ba4d 100644 --- a/apps/cli/src/http/index.ts +++ b/apps/cli/src/http/index.ts @@ -25,6 +25,7 @@ import { registerWebRoutes } from './routes/web' import { registerNlpRoutes } from './routes/nlp' import { registerAiRoutes } from './routes/ai' import { registerPreferencesRoutes } from './routes/preferences' +import { registerProxyRoutes } from './routes/proxy' import { initServerAiLogger, closeServerAiLogger } from '../ai/logger' import { initSync, cleanupSync } from '../sync' import { resolveCliPath } from '../paths' @@ -140,6 +141,10 @@ export async function startHttpServer(options?: HttpServerOptions): Promise<{ if (options?.webRoot && fs.existsSync(options.webRoot)) { setWebMode(true) + // 注册反向代理:将 /_proxy/chatlab.fun/* 转发至 https://chatlab.fun, + // 行为与 vite dev proxy 一致,解决浏览器 CORS 问题(见 vite.web.config.mts:138-144)。 + // 必须在 @fastify/static 之前注册,确保显式路由优先于静态文件/SPA fallback。 + registerProxyRoutes(server) const fastifyStatic = await import('@fastify/static') await server.register(fastifyStatic.default, { root: options.webRoot, diff --git a/apps/cli/src/http/routes/proxy.ts b/apps/cli/src/http/routes/proxy.ts new file mode 100644 index 00000000..a6eb8de6 --- /dev/null +++ b/apps/cli/src/http/routes/proxy.ts @@ -0,0 +1,42 @@ +/** + * 反向代理路由 — /_proxy/chatlab.fun/* + * + * 在 Web 生产模式(chatlab start)下,将 /_proxy/chatlab.fun/* 代理到 + * https://chatlab.fun,行为与 vite.web.config.mts 中的 dev proxy 保持一致, + * 以解决浏览器 CORS 限制。 + * + * 目标 host 硬编码为 https://chatlab.fun,不允许外部指定,避免开放代理风险。 + */ + +import type { FastifyInstance } from 'fastify' + +const PROXY_TARGET = 'https://chatlab.fun' + +export function registerProxyRoutes(server: FastifyInstance): void { + server.get<{ Params: { '*': string } }>('/_proxy/chatlab.fun/*', async (request, reply) => { + const subPath = request.params['*'] ?? '' + const queryString = new URLSearchParams( + request.query as Record, + ).toString() + const targetUrl = queryString + ? `${PROXY_TARGET}/${subPath}?${queryString}` + : `${PROXY_TARGET}/${subPath}` + + try { + const upstream = await fetch(targetUrl, { + headers: { Accept: request.headers.accept ?? '*/*' }, + signal: AbortSignal.timeout(10_000), + }) + + const contentType = upstream.headers.get('content-type') ?? 'application/octet-stream' + const body = Buffer.from(await upstream.arrayBuffer()) + + reply.code(upstream.status).header('content-type', contentType).send(body) + } catch (err) { + reply.code(502).send({ + error: 'Proxy error', + message: err instanceof Error ? err.message : String(err), + }) + } + }) +}