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
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user