fix(frontend): route agent checks to gateway (#1572)

* fix(frontend): route agent checks to gateway

* fix(frontend): proxy langgraph requests locally

* fix(frontend): keep zh-CN text readable

* fix(frontend): add exact local api rewrites

* fix(frontend): support docker-safe internal rewrites

* Update frontend/src/core/agents/api.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Admire 2026-03-30 21:04:59 +08:00 committed by GitHub
parent 4bb3c101a8
commit 9e3d484858
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 97 additions and 9 deletions

View File

@ -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:

View File

@ -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:

View File

@ -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;

View File

@ -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);

View File

@ -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<Agent[]> {
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<void> {
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 }>;

View File

@ -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!",

View File

@ -183,7 +183,8 @@ export const zhCN: Translations = {
nameStepContinue: "继续",
nameStepInvalidError: "名称无效,只允许字母、数字和连字符",
nameStepAlreadyExistsError: "已存在同名智能体",
nameStepCheckError: "无法验证名称可用性,请稍后重试",
nameStepCheckError:
"无法连接 DeerFlow 后端来验证名称是否可用。请先启动后端,或配置 NEXT_PUBLIC_BACKEND_BASE_URL然后再重试。",
nameStepBootstrapMessage:
"新智能体的名称是 {name},现在开始为它生成 **SOUL**。",
agentCreated: "智能体已创建!",