feat: 舆情分析智能体改为经 Gateway 转发外部 AG-UI 网关
前端不再直连上游 SSE,改为 POST /api/sentiment-agent/stream 由后台代理转发,支持关闭 TLS 校验并回传完整错误信息。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
5c0864bf0a
commit
62a3faa7c8
17 files changed
+398
-130
No files matched your search
@@ -22,7 +22,7 @@ import { SentimentAgentProfileDialog } from "./sentiment-agent-profile-dialog";
|
||||
|
||||
/**
|
||||
* 「舆情分析」类型下的虚拟智能体卡片:只存在于前端(不落库 /api/agents),
|
||||
* 对话直连外部 AG-UI 网关。名称 / 简介可编辑(localStorage),
|
||||
* 对话经 Gateway 转发外部 AG-UI 网关。名称 / 简介可编辑(localStorage),
|
||||
* 视觉与普通智能体卡片保持一致(网格 / 列表两种布局)。
|
||||
*/
|
||||
export function SentimentAgentCard({
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import type { Message } from "@langchain/langgraph-sdk";
|
||||
|
||||
import {
|
||||
buildSentimentAgentHeaders,
|
||||
readSentimentAgentEndpoint,
|
||||
} from "./config";
|
||||
import { apiFetch } from "@/core/api";
|
||||
import { getBackendBaseURL } from "@/core/config";
|
||||
import { readSentimentAgentEndpoint } from "./config";
|
||||
|
||||
/**
|
||||
* 舆情分析智能体走的是外部 AG-UI 协议网关(POST,SSE 返回):
|
||||
* 请求体为 { threadId, runId, state, tools, context, forwardedProps, messages },
|
||||
* 舆情分析智能体走 Gateway 反向代理(POST,SSE 返回):
|
||||
* 前端把 runtime-config.js 的 apiUrl / Basic Auth 随 AG-UI 体传给
|
||||
* `/api/sentiment-agent/stream`,由后台转发上游(不校验证书、不拦地址)。
|
||||
* 请求体业务字段仍为 { threadId, runId, state, tools, context, forwardedProps, messages },
|
||||
* 返回为 `data: {"type": ...}` 的 SSE 事件流:
|
||||
* RUN_STARTED / THINKING_TEXT_MESSAGE_START|CONTENT|END /
|
||||
* TEXT_MESSAGE_START|CONTENT|END / RUN_FINISHED(另有 RUN_ERROR 等异常事件)。
|
||||
@@ -91,6 +92,28 @@ export interface RunSentimentAgentOptions {
|
||||
handlers: SentimentAgentRunHandlers;
|
||||
}
|
||||
|
||||
/** 把 Gateway / FastAPI 错误体展开成可展示的完整字符串。 */
|
||||
async function readProxyError(response: Response): Promise<string> {
|
||||
const text = await response.text();
|
||||
if (!text.trim()) {
|
||||
return `舆情分析服务请求失败(HTTP ${response.status})。`;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(text) as { detail?: unknown };
|
||||
if (typeof parsed.detail === "string" && parsed.detail.trim()) {
|
||||
return parsed.detail;
|
||||
}
|
||||
if (parsed.detail != null) {
|
||||
return typeof parsed.detail === "object"
|
||||
? JSON.stringify(parsed.detail, null, 2)
|
||||
: String(parsed.detail);
|
||||
}
|
||||
} catch {
|
||||
// 非 JSON 时把原始响应原文抛给调用方。
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起一次舆情分析流式问答。resolve 于流正常结束(RUN_FINISHED 或连接关闭);
|
||||
* 网络错误 / 非 2xx / RUN_ERROR 事件都会 reject(AbortError 由调用方自行忽略)。
|
||||
@@ -99,16 +122,22 @@ export async function runSentimentAgent(
|
||||
options: RunSentimentAgentOptions,
|
||||
): Promise<void> {
|
||||
const { threadId, runId, messages, signal, handlers } = options;
|
||||
const { apiUrl } = readSentimentAgentEndpoint();
|
||||
if (!apiUrl) {
|
||||
const endpoint = readSentimentAgentEndpoint();
|
||||
if (!endpoint.apiUrl) {
|
||||
throw new Error("未配置舆情分析接口地址(runtime-config.js 的 VITE_SENTIMENT_AGENT_API_URL)。");
|
||||
}
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
const response = await apiFetch(`${getBackendBaseURL()}/api/sentiment-agent/stream`, {
|
||||
method: "POST",
|
||||
headers: buildSentimentAgentHeaders(),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "text/event-stream",
|
||||
},
|
||||
signal,
|
||||
body: JSON.stringify({
|
||||
apiUrl: endpoint.apiUrl,
|
||||
authUsername: endpoint.authUsername,
|
||||
authPassword: endpoint.authPassword,
|
||||
threadId,
|
||||
runId,
|
||||
state: {},
|
||||
@@ -119,7 +148,7 @@ export async function runSentimentAgent(
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`舆情分析服务请求失败(HTTP ${response.status})。`);
|
||||
throw new Error(await readProxyError(response));
|
||||
}
|
||||
if (!response.body) {
|
||||
throw new Error("舆情分析服务未返回流式响应。");
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { readRuntimeEnv } from "@/env";
|
||||
|
||||
/**
|
||||
* 舆情分析智能体(前端直连的虚拟智能体)的接入配置。
|
||||
* 舆情分析智能体(经 Gateway 转发的虚拟智能体)的接入配置。
|
||||
* 接口地址与 Basic Auth 凭据都来自 public/runtime-config.js,部署后可直接改
|
||||
* dist/runtime-config.js 切换环境,无需重新打包。
|
||||
* dist/runtime-config.js 切换环境,无需重新打包。前端把这三项随请求传给后台转发上游 SSE。
|
||||
*/
|
||||
|
||||
/** 该虚拟智能体在路由里的固定 id(/agents/{id}/chats/...)。 */
|
||||
@@ -35,25 +35,3 @@ export function isSentimentAgentVisible(): boolean {
|
||||
return readRuntimeEnv("VITE_SENTIMENT_AGENT_VISIBLE") !== "false";
|
||||
}
|
||||
|
||||
function toBase64(value: string): string {
|
||||
// 凭据按 UTF-8 字节做 Basic Auth base64,避免非 ASCII 字符导致 btoa 抛错。
|
||||
const bytes = new TextEncoder().encode(value);
|
||||
let binary = "";
|
||||
for (const byte of bytes) {
|
||||
binary += String.fromCharCode(byte);
|
||||
}
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
/** 组装请求头:Basic Auth(用户名/密码都配置了才带 Authorization)。 */
|
||||
export function buildSentimentAgentHeaders(): Record<string, string> {
|
||||
const { authUsername, authPassword } = readSentimentAgentEndpoint();
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "text/event-stream",
|
||||
};
|
||||
if (authUsername && authPassword) {
|
||||
headers.Authorization = `Basic ${toBase64(`${authUsername}:${authPassword}`)}`;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
@@ -8,7 +8,7 @@ const STORAGE_PREFIX = "workspace.sentiment-agent.";
|
||||
|
||||
/**
|
||||
* 舆情分析智能体的前端会话状态:消息只存在当前标签页的 sessionStorage,
|
||||
* 不创建后端 thread、不进 LangGraph —— 所有问答都直连外部 AG-UI 网关。
|
||||
* 不创建后端 thread、不进 LangGraph —— 问答经 Gateway 转发外部 AG-UI 网关。
|
||||
* 整体结构参照 use-single-round-rag 的「合成 LangGraph 消息 + 本地持久化」模式。
|
||||
*/
|
||||
export function useSentimentAgentChat(threadId: string) {
|
||||
|
||||
@@ -30,7 +30,8 @@ import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* 舆情分析智能体对话页:一个前端虚拟智能体(不在后端 /api/agents 落库),
|
||||
* 问答直连外部 AG-UI 网关(地址 + Basic Auth 在 runtime-config.js 配置)。
|
||||
* 问答经 Gateway POST /api/sentiment-agent/stream 转发外部 AG-UI 网关
|
||||
* (地址 + Basic Auth 在 runtime-config.js,随请求传给后台)。
|
||||
* 页面复用普通问答的消息渲染(MessageList),输入框只保留发送按钮;
|
||||
* 会话只存在当前标签页 sessionStorage,不创建后端 thread。
|
||||
*/
|
||||
|
||||
@@ -325,7 +325,7 @@ export default function StrategyRoutes() {
|
||||
|
||||
<Route path="agents" element={<AgentsPage />} />
|
||||
<Route path="agents/new" element={<NewAgentPage />} />
|
||||
{/* 舆情分析虚拟智能体:前端直连外部 AG-UI 网关的专用对话页。 */}
|
||||
{/* 舆情分析虚拟智能体:经 Gateway 转发外部 AG-UI 网关的专用对话页。 */}
|
||||
<Route
|
||||
path="agents/sentiment-analysis/chats"
|
||||
element={
|
||||
|
||||
@@ -478,8 +478,8 @@ export default function WorkspaceRoutes({
|
||||
</WorkspaceLayout>
|
||||
}
|
||||
/>
|
||||
{/* 舆情分析虚拟智能体:静态段优先于 :agent_id 命中,走前端直连外部
|
||||
AG-UI 网关的专用对话页(不在后端 /api/agents 落库)。 */}
|
||||
{/* 舆情分析虚拟智能体:静态段优先于 :agent_id 命中,走 Gateway 转发
|
||||
外部 AG-UI 网关的专用对话页(不在后端 /api/agents 落库)。 */}
|
||||
<Route
|
||||
path="agents/sentiment-analysis/chats"
|
||||
element={
|
||||
|
||||
Reference in new issue
Block a user