diff --git a/frontend/src/flows/services/custom-service.ts b/frontend/src/flows/services/custom-service.ts index db388e0..c51d591 100644 --- a/frontend/src/flows/services/custom-service.ts +++ b/frontend/src/flows/services/custom-service.ts @@ -144,40 +144,31 @@ export class CustomService { // 构造 WS URL const base = (api.defaults.baseURL || '') as string; // 可能是 /api 或 http(s)://host/api - function toWsUrl(httpUrl: string) { - if (httpUrl.startsWith('https://')) return 'wss://' + httpUrl.slice('https://'.length); - if (httpUrl.startsWith('http://')) return 'ws://' + httpUrl.slice('http://'.length); - // 相对路径:拼 window.location - const origin = window.location.origin; // http(s)://host:port - const full = origin.replace(/^http/, 'ws') + (httpUrl.startsWith('/') ? httpUrl : '/' + httpUrl); - return full; - } - const path = `/flows/${id}/run/ws`; - // 取 token 放到查询参数(WS 握手无法自定义 Authorization 头部) - const token = getToken(); - - // 新增:WS 使用独立端口,默认 8855,可通过 VITE_WS_PORT 覆盖 - const wsPort = (import.meta as any).env?.VITE_WS_PORT || '8855'; - let wsBase: string; + // 解析出 API 的路径前缀(用于生产环境相对路径),默认 /api + let apiPathPrefix = '/api'; if (base.startsWith('http://') || base.startsWith('https://')) { try { const u = new URL(base); - const proto = u.protocol === 'https:' ? 'wss:' : 'ws:'; - u.protocol = proto; - u.port = wsPort; // 改为 WS 端口 - wsBase = `${u.protocol}//${u.host}${u.pathname.replace(/\/$/, '')}`; - } catch { - const loc = window.location; - const proto = loc.protocol === 'https:' ? 'wss:' : 'ws:'; - wsBase = `${proto}//${loc.hostname}:${wsPort}${base.startsWith('/') ? base : '/' + base}`; - } - } else { - const loc = window.location; - const proto = loc.protocol === 'https:' ? 'wss:' : 'ws:'; - wsBase = `${proto}//${loc.hostname}:${wsPort}${base.startsWith('/') ? base : '/' + base}`; + apiPathPrefix = (u.pathname.replace(/\/$/, '') || '/api'); + } catch {} + } else if (base.startsWith('/')) { + apiPathPrefix = (base.replace(/\/$/, '') || '/api'); } - const wsUrl = wsBase + path + (token ? (wsBase.includes('?') ? `&access_token=${encodeURIComponent(token)}` : `?access_token=${encodeURIComponent(token)}`) : ''); + const path = `/flows/${id}/run/ws`; + // 取 token 放到查询参数(WS 握手无法自定义 Authorization 头部) + const token = getToken(); + const isDev = !!((import.meta as any).env?.DEV); + // 开发:走 /ws 前缀(由 Vite 代理到 8855 并 rewrite 到 /api) + // 生产:走同域 /api 前缀,由 Nginx/网关反代,不显式暴露端口 + const prefix = isDev ? '/ws' : apiPathPrefix; + const qs = token ? `?access_token=${encodeURIComponent(token)}` : ''; + + // 始终构造绝对 WS URL,避免浏览器兼容性问题 + const loc = window.location; + const proto = loc.protocol === 'https:' ? 'wss:' : 'ws:'; + const origin = `${proto}//${loc.host}`; // host 里已包含端口(开发时 5173),线上通常无端口 + const wsUrl = `${origin}${prefix}${path}${qs}`; let ws: WebSocket | null = null; let resolveDone: (v: RunResult | null) => void; @@ -253,27 +244,34 @@ export class CustomService { return { cancel: () => {}, done: Promise.resolve(null) } as const; } - const base = (api.defaults.baseURL || '') as string; - // 参照 WS:SSE 使用独立端口,默认 8866,可通过 VITE_SSE_PORT 覆盖 - const ssePort = (import.meta as any).env?.VITE_SSE_PORT || '8866'; - let sseBase: string; - if (base.startsWith('http://') || base.startsWith('https://')) { - try { - const u = new URL(base); - // 协议保持与 base 一致,仅替换端口 - u.port = ssePort; - sseBase = `${u.protocol}//${u.host}${u.pathname.replace(/\/$/, '')}`; - } catch { + // 在开发环境通过 Vite 代理前缀 /sse 转发到 8866(vite.config.ts 已配置 rewrite 到 /api) + const useSseProxy = !!((import.meta as any).env?.DEV); + let url: string; + + if (useSseProxy) { + url = `/sse/flows/${id}/run/stream`; + } else { + const base = (api.defaults.baseURL || '') as string; + // 参照 WS:SSE 使用独立端口,默认 8866,可通过 VITE_SSE_PORT 覆盖 + const ssePort = (import.meta as any).env?.VITE_SSE_PORT || '8866'; + let sseBase: string; + if (base.startsWith('http://') || base.startsWith('https://')) { + try { + const u = new URL(base); + // 协议保持与 base 一致,仅替换端口 + u.port = ssePort; + sseBase = `${u.protocol}//${u.host}${u.pathname.replace(/\/$/, '')}`; + } catch { + const loc = window.location; + sseBase = `${loc.protocol}//${loc.hostname}:${ssePort}${base.startsWith('/') ? base : '/' + base}`; + } + } else { const loc = window.location; sseBase = `${loc.protocol}//${loc.hostname}:${ssePort}${base.startsWith('/') ? base : '/' + base}`; } - } else { - const loc = window.location; - sseBase = `${loc.protocol}//${loc.hostname}:${ssePort}${base.startsWith('/') ? base : '/' + base}`; + url = sseBase + `/flows/${id}/run/stream`; } - const url = sseBase + `/flows/${id}/run/stream`; - const { cancel, done } = postSSE(url, { input }, { onMessage: (json: any) => { try {