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:
parent
4e47e04cb6
commit
3c3b4d2684
|
|
@ -100,6 +100,8 @@ services:
|
||||||
- NODE_ENV=development
|
- NODE_ENV=development
|
||||||
- WATCHPACK_POLLING=true
|
- WATCHPACK_POLLING=true
|
||||||
- CI=true
|
- CI=true
|
||||||
|
- DEER_FLOW_INTERNAL_GATEWAY_BASE_URL=http://gateway:8001
|
||||||
|
- DEER_FLOW_INTERNAL_LANGGRAPH_BASE_URL=http://langgraph:2024
|
||||||
env_file:
|
env_file:
|
||||||
- ../frontend/.env
|
- ../frontend/.env
|
||||||
networks:
|
networks:
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,8 @@ services:
|
||||||
container_name: deer-flow-frontend
|
container_name: deer-flow-frontend
|
||||||
environment:
|
environment:
|
||||||
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET}
|
- 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:
|
env_file:
|
||||||
- ../frontend/.env
|
- ../frontend/.env
|
||||||
networks:
|
networks:
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,51 @@
|
||||||
*/
|
*/
|
||||||
import "./src/env.js";
|
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} */
|
/** @type {import("next").NextConfig} */
|
||||||
const config = {
|
const config = {
|
||||||
devIndicators: false,
|
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;
|
export default config;
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,11 @@ import { ArtifactsProvider } from "@/components/workspace/artifacts";
|
||||||
import { MessageList } from "@/components/workspace/messages";
|
import { MessageList } from "@/components/workspace/messages";
|
||||||
import { ThreadContext } from "@/components/workspace/messages/context";
|
import { ThreadContext } from "@/components/workspace/messages/context";
|
||||||
import type { Agent } from "@/core/agents";
|
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 { useI18n } from "@/core/i18n/hooks";
|
||||||
import { useThreadStream } from "@/core/threads/hooks";
|
import { useThreadStream } from "@/core/threads/hooks";
|
||||||
import { uuid } from "@/core/utils/uuid";
|
import { uuid } from "@/core/utils/uuid";
|
||||||
|
|
@ -76,8 +80,16 @@ export default function NewAgentPage() {
|
||||||
setNameError(t.agents.nameStepAlreadyExistsError);
|
setNameError(t.agents.nameStepAlreadyExistsError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (error) {
|
||||||
setNameError(t.agents.nameStepCheckError);
|
if (error instanceof AgentNameCheckError) {
|
||||||
|
setNameError(
|
||||||
|
error.reason === "backend_unreachable"
|
||||||
|
? t.agents.nameStepCheckError
|
||||||
|
: error.message,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setNameError(t.agents.nameStepCheckError);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
} finally {
|
} finally {
|
||||||
setIsCheckingName(false);
|
setIsCheckingName(false);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,18 @@ import { getBackendBaseURL } from "@/core/config";
|
||||||
|
|
||||||
import type { Agent, CreateAgentRequest, UpdateAgentRequest } from "./types";
|
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[]> {
|
export async function listAgents(): Promise<Agent[]> {
|
||||||
const res = await fetch(`${getBackendBaseURL()}/api/agents`);
|
const res = await fetch(`${getBackendBaseURL()}/api/agents`);
|
||||||
if (!res.ok) throw new Error(`Failed to load agents: ${res.statusText}`);
|
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(
|
export async function checkAgentName(
|
||||||
name: string,
|
name: string,
|
||||||
): Promise<{ available: boolean; name: string }> {
|
): Promise<{ available: boolean; name: string }> {
|
||||||
const res = await fetch(
|
let res: Response;
|
||||||
`${getBackendBaseURL()}/api/agents/check?name=${encodeURIComponent(name)}`,
|
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) {
|
if (!res.ok) {
|
||||||
const err = (await res.json().catch(() => ({}))) as { detail?: string };
|
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}`,
|
err.detail ?? `Failed to check agent name: ${res.statusText}`,
|
||||||
|
"request_failed",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return res.json() as Promise<{ available: boolean; name: string }>;
|
return res.json() as Promise<{ available: boolean; name: string }>;
|
||||||
|
|
|
||||||
|
|
@ -194,7 +194,8 @@ export const enUS: Translations = {
|
||||||
nameStepInvalidError:
|
nameStepInvalidError:
|
||||||
"Invalid name — use only letters, digits, and hyphens",
|
"Invalid name — use only letters, digits, and hyphens",
|
||||||
nameStepAlreadyExistsError: "An agent with this name already exists",
|
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:
|
nameStepBootstrapMessage:
|
||||||
"The new custom agent name is {name}. Let's bootstrap it's **SOUL**.",
|
"The new custom agent name is {name}. Let's bootstrap it's **SOUL**.",
|
||||||
agentCreated: "Agent created!",
|
agentCreated: "Agent created!",
|
||||||
|
|
|
||||||
|
|
@ -183,7 +183,8 @@ export const zhCN: Translations = {
|
||||||
nameStepContinue: "继续",
|
nameStepContinue: "继续",
|
||||||
nameStepInvalidError: "名称无效,只允许字母、数字和连字符",
|
nameStepInvalidError: "名称无效,只允许字母、数字和连字符",
|
||||||
nameStepAlreadyExistsError: "已存在同名智能体",
|
nameStepAlreadyExistsError: "已存在同名智能体",
|
||||||
nameStepCheckError: "无法验证名称可用性,请稍后重试",
|
nameStepCheckError:
|
||||||
|
"无法连接 DeerFlow 后端来验证名称是否可用。请先启动后端,或配置 NEXT_PUBLIC_BACKEND_BASE_URL,然后再重试。",
|
||||||
nameStepBootstrapMessage:
|
nameStepBootstrapMessage:
|
||||||
"新智能体的名称是 {name},现在开始为它生成 **SOUL**。",
|
"新智能体的名称是 {name},现在开始为它生成 **SOUL**。",
|
||||||
agentCreated: "智能体已创建!",
|
agentCreated: "智能体已创建!",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue