diff --git a/docker/docker-compose-dev.yaml b/docker/docker-compose-dev.yaml index 8b7b7da5d..0bf6c5fb6 100644 --- a/docker/docker-compose-dev.yaml +++ b/docker/docker-compose-dev.yaml @@ -100,6 +100,8 @@ services: - NODE_ENV=development - WATCHPACK_POLLING=true - CI=true + - DEER_FLOW_INTERNAL_GATEWAY_BASE_URL=http://gateway:8001 + - DEER_FLOW_INTERNAL_LANGGRAPH_BASE_URL=http://langgraph:2024 env_file: - ../frontend/.env networks: diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index dea4c98e0..ba84d3c12 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -50,6 +50,8 @@ services: container_name: deer-flow-frontend environment: - BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET} + - DEER_FLOW_INTERNAL_GATEWAY_BASE_URL=http://gateway:8001 + - DEER_FLOW_INTERNAL_LANGGRAPH_BASE_URL=http://langgraph:2024 env_file: - ../frontend/.env networks: diff --git a/frontend/next.config.js b/frontend/next.config.js index 7159179e4..d769c14a5 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -4,9 +4,51 @@ */ import "./src/env.js"; +function getInternalServiceURL(envKey, fallbackURL) { + const configured = process.env[envKey]?.trim(); + return configured && configured.length > 0 + ? configured.replace(/\/+$/, "") + : fallbackURL; +} + /** @type {import("next").NextConfig} */ const config = { devIndicators: false, + async rewrites() { + const rewrites = []; + const langgraphURL = getInternalServiceURL( + "DEER_FLOW_INTERNAL_LANGGRAPH_BASE_URL", + "http://127.0.0.1:2024", + ); + const gatewayURL = getInternalServiceURL( + "DEER_FLOW_INTERNAL_GATEWAY_BASE_URL", + "http://127.0.0.1:8001", + ); + + if (!process.env.NEXT_PUBLIC_LANGGRAPH_BASE_URL) { + rewrites.push({ + source: "/api/langgraph", + destination: langgraphURL, + }); + rewrites.push({ + source: "/api/langgraph/:path*", + destination: `${langgraphURL}/:path*`, + }); + } + + if (!process.env.NEXT_PUBLIC_BACKEND_BASE_URL) { + rewrites.push({ + source: "/api/agents", + destination: `${gatewayURL}/api/agents`, + }); + rewrites.push({ + source: "/api/agents/:path*", + destination: `${gatewayURL}/api/agents/:path*`, + }); + } + + return rewrites; + }, }; export default config; diff --git a/frontend/src/app/workspace/agents/new/page.tsx b/frontend/src/app/workspace/agents/new/page.tsx index 865416f7f..256489f8d 100644 --- a/frontend/src/app/workspace/agents/new/page.tsx +++ b/frontend/src/app/workspace/agents/new/page.tsx @@ -16,7 +16,11 @@ import { ArtifactsProvider } from "@/components/workspace/artifacts"; import { MessageList } from "@/components/workspace/messages"; import { ThreadContext } from "@/components/workspace/messages/context"; import type { Agent } from "@/core/agents"; -import { checkAgentName, getAgent } from "@/core/agents/api"; +import { + AgentNameCheckError, + checkAgentName, + getAgent, +} from "@/core/agents/api"; import { useI18n } from "@/core/i18n/hooks"; import { useThreadStream } from "@/core/threads/hooks"; import { uuid } from "@/core/utils/uuid"; @@ -76,8 +80,16 @@ export default function NewAgentPage() { setNameError(t.agents.nameStepAlreadyExistsError); return; } - } catch { - setNameError(t.agents.nameStepCheckError); + } catch (error) { + if (error instanceof AgentNameCheckError) { + setNameError( + error.reason === "backend_unreachable" + ? t.agents.nameStepCheckError + : error.message, + ); + } else { + setNameError(t.agents.nameStepCheckError); + } return; } finally { setIsCheckingName(false); diff --git a/frontend/src/core/agents/api.ts b/frontend/src/core/agents/api.ts index d9c2f1769..927b5f20b 100644 --- a/frontend/src/core/agents/api.ts +++ b/frontend/src/core/agents/api.ts @@ -2,6 +2,18 @@ import { getBackendBaseURL } from "@/core/config"; import type { Agent, CreateAgentRequest, UpdateAgentRequest } from "./types"; +const BACKEND_UNAVAILABLE_STATUSES = new Set([502, 503, 504]); + +export class AgentNameCheckError extends Error { + constructor( + message: string, + public readonly reason: "backend_unreachable" | "request_failed", + ) { + super(message); + this.name = "AgentNameCheckError"; + } +} + export async function listAgents(): Promise { const res = await fetch(`${getBackendBaseURL()}/api/agents`); if (!res.ok) throw new Error(`Failed to load agents: ${res.statusText}`); @@ -54,13 +66,29 @@ export async function deleteAgent(name: string): Promise { export async function checkAgentName( name: string, ): Promise<{ available: boolean; name: string }> { - const res = await fetch( - `${getBackendBaseURL()}/api/agents/check?name=${encodeURIComponent(name)}`, - ); + let res: Response; + try { + res = await fetch( + `${getBackendBaseURL()}/api/agents/check?name=${encodeURIComponent(name)}`, + ); + } catch { + throw new AgentNameCheckError( + "Could not reach the DeerFlow backend.", + "backend_unreachable", + ); + } + if (!res.ok) { const err = (await res.json().catch(() => ({}))) as { detail?: string }; - throw new Error( + if (BACKEND_UNAVAILABLE_STATUSES.has(res.status)) { + throw new AgentNameCheckError( + "Could not reach the DeerFlow backend.", + "backend_unreachable", + ); + } + throw new AgentNameCheckError( err.detail ?? `Failed to check agent name: ${res.statusText}`, + "request_failed", ); } return res.json() as Promise<{ available: boolean; name: string }>; diff --git a/frontend/src/core/i18n/locales/en-US.ts b/frontend/src/core/i18n/locales/en-US.ts index 1cf758a0a..61136693a 100644 --- a/frontend/src/core/i18n/locales/en-US.ts +++ b/frontend/src/core/i18n/locales/en-US.ts @@ -194,7 +194,8 @@ export const enUS: Translations = { nameStepInvalidError: "Invalid name — use only letters, digits, and hyphens", nameStepAlreadyExistsError: "An agent with this name already exists", - nameStepCheckError: "Could not verify name availability — please try again", + nameStepCheckError: + "Could not reach the DeerFlow backend to verify name availability. Start the backend or set NEXT_PUBLIC_BACKEND_BASE_URL, then try again.", nameStepBootstrapMessage: "The new custom agent name is {name}. Let's bootstrap it's **SOUL**.", agentCreated: "Agent created!", diff --git a/frontend/src/core/i18n/locales/zh-CN.ts b/frontend/src/core/i18n/locales/zh-CN.ts index bd3e3c0ae..fb322badf 100644 --- a/frontend/src/core/i18n/locales/zh-CN.ts +++ b/frontend/src/core/i18n/locales/zh-CN.ts @@ -183,7 +183,8 @@ export const zhCN: Translations = { nameStepContinue: "继续", nameStepInvalidError: "名称无效,只允许字母、数字和连字符", nameStepAlreadyExistsError: "已存在同名智能体", - nameStepCheckError: "无法验证名称可用性,请稍后重试", + nameStepCheckError: + "无法连接 DeerFlow 后端来验证名称是否可用。请先启动后端,或配置 NEXT_PUBLIC_BACKEND_BASE_URL,然后再重试。", nameStepBootstrapMessage: "新智能体的名称是 {name},现在开始为它生成 **SOUL**。", agentCreated: "智能体已创建!",