feat: 舆情分析智能体改为经 Gateway 转发外部 AG-UI 网关

前端不再直连上游 SSE,改为 POST /api/sentiment-agent/stream 由后台代理转发,支持关闭 TLS 校验并回传完整错误信息。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
董宏凯andCursor committed 2026-08-25 18:26:48 +08:00
1 parent 5c0864bf0a
commit 62a3faa7c8
17 files changed
+398 -130

No files matched your search

+40 -11
View File
@@ -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) {