fix(cli): add /_proxy/chatlab.fun reverse proxy for web mode

在 Web 生产模式(chatlab start)下,浏览器无法直连 chatlab.fun(CORS 限制),
导致首页底部社交链接(如小红书)及云端助手/技能预设拉取失败。
Vite dev proxy 已有对应配置,但生产 Fastify server 缺少该代理,现补全。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
diguaandClaude Sonnet 4.6 committed 2026-05-31 16:56:08 +08:00
1 parent d1637b41b8
commit b96ec69730
2 files changed
+47

No files matched your search

+5
View File
@@ -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,
+42
View File
@@ -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<string, string>,
).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),
})
}
})
}