diff --git a/api/.vscode/launch.json b/api/.vscode/launch.json index a68174c..e2bbc65 100644 --- a/api/.vscode/launch.json +++ b/api/.vscode/launch.json @@ -115,6 +115,9 @@ "IMAGE_ASSETS_DATABASE_URL":"postgres://postgres:postgres@10.8.16.77:5434/deep_agent_db", /* 阿里云百炼 API KEY */ "DASHSCOPE_API_KEY": "", + "OCR_API_URL": "http://112.4.244.131:8056", + "FS_SUPER_AGENT_BUSINESS_API": "http://localhost:8000", + "FS_SUPER_AGENT_FRAMEWORK_API": "http://localhost:8005", "AUTH_JWT_VERIFICATION_KEY": "local-development-only-long-random-secret", } } diff --git a/api/app/controllers/superAgentProtocol.js b/api/app/controllers/superAgentProtocol.js index a927022..2d55534 100644 --- a/api/app/controllers/superAgentProtocol.js +++ b/api/app/controllers/superAgentProtocol.js @@ -55,10 +55,6 @@ const buildTrustedHeaders = (ctx, hasJsonBody = false) => { Accept: 'application/json, text/event-stream', Authorization: `Bearer ${issued.token}`, }; - const searchToken = String(ctx.fs?.curUser?.token || '').trim(); - if (searchToken) { - headers['X-PEP-Token'] = searchToken; - } if (hasJsonBody) headers['Content-Type'] = 'application/json'; return headers; }; diff --git a/api/config/default.js b/api/config/default.js index 9b4d83e..be45fad 100644 --- a/api/config/default.js +++ b/api/config/default.js @@ -406,7 +406,12 @@ module.exports = { hostUrl: process.env.XFYUN_SPEECH_HOST_URL || "wss://iat-api.xfyun.cn/v2/iat", }, superAgent: { - baseUrl: process.env.FS_SUPER_AGENT_API || "http://localhost:8080", + baseUrl: process.env.FS_SUPER_AGENT_FRAMEWORK_API || "http://127.0.0.1:8005", + businessBaseUrl: + process.env.FS_SUPER_AGENT_BUSINESS_API || + process.env.FS_SUPER_AGENT_API || + "http://127.0.0.1:8000", + frameworkBaseUrl: process.env.FS_SUPER_AGENT_FRAMEWORK_API || "http://127.0.0.1:8005", jwt: { algorithm: process.env.FS_SUPER_AGENT_JWT_ALGORITHM || "HS256", secret: process.env.AUTH_JWT_VERIFICATION_KEY || "", diff --git a/web/client/src/sections/superAgent/components/SuperAgentSidebar.jsx b/web/client/src/sections/superAgent/components/SuperAgentSidebar.jsx index 495bd60..0c5123d 100644 --- a/web/client/src/sections/superAgent/components/SuperAgentSidebar.jsx +++ b/web/client/src/sections/superAgent/components/SuperAgentSidebar.jsx @@ -73,6 +73,7 @@ const SuperAgentSidebar = ({ collapsedFolderMap, activeSessionId, isLoadingSessions, + sidebarError, isCreatingChat, isCreatingTask, isLoggedIn, @@ -259,6 +260,8 @@ const SuperAgentSidebar = ({ {isLoadingSessions ? (
正在加载会话...
+ ) : sidebarError ? ( +
{sidebarError.message}
) : ( <>
diff --git a/web/client/src/sections/superAgent/constants.js b/web/client/src/sections/superAgent/constants.js index 01736f7..ae09877 100644 --- a/web/client/src/sections/superAgent/constants.js +++ b/web/client/src/sections/superAgent/constants.js @@ -23,17 +23,25 @@ import NetworkIcon from "@assets/img/newAiChat/icon (11).png"; import ShortcutArticleIcon from "@assets/img/newAiChat/icon (4).png"; import ShortcutWriteIcon from "@assets/img/newAiChat/icon (7).png"; -export const FOLDER_COLLAPSED_STORAGE_KEY = "superAgent_folder_collapsed"; +export const FOLDER_COLLAPSED_STORAGE_KEY = "superAgent_v2_folder_collapsed"; export const SIDEBAR_SESSION_DRAG_TYPE = "application/x-super-sidebar-session"; /** 虚拟默认文件夹(folder_id = null 的任务归入此处,不落库) */ export const DEFAULT_TASK_FOLDER_ID = "__default__"; export const DEFAULT_TASK_FOLDER_NAME = "默认任务集"; -export const TASK_STORAGE_KEY = "superAgent_tender_task_list"; -export const TASK_SESSION_STORAGE_KEY = "superAgent_tender_session_ids"; -export const PENDING_EMPTY_SESSION_STORAGE_KEY = "superAgent_pending_empty_session"; +export const TASK_STORAGE_KEY = "superAgent_v2_tender_task_list"; +export const TASK_SESSION_STORAGE_KEY = "superAgent_v2_tender_session_ids"; +export const PENDING_EMPTY_SESSION_STORAGE_KEY = "superAgent_v2_pending_empty_session"; export const ACTIVE_SESSION_STORAGE_KEY = "superAgent_v2_active_conversation_id"; + +export const LEGACY_SUPER_AGENT_STORAGE_KEYS = [ + "superAgent_folder_collapsed", + "superAgent_tender_task_list", + "superAgent_tender_session_ids", + "superAgent_pending_empty_session", + "superAgent_active_session_id", +]; export const PHONE_REGEXP = /^1\d{10}$/; export const CHAT_WELCOME_TITLE = "你好,我是飞小尚 AI 办公助手"; diff --git a/web/client/src/sections/superAgent/container/index.jsx b/web/client/src/sections/superAgent/container/index.jsx index e080310..1ff21a3 100644 --- a/web/client/src/sections/superAgent/container/index.jsx +++ b/web/client/src/sections/superAgent/container/index.jsx @@ -56,6 +56,7 @@ import { normalizeCheckpoint, replaceCheckpointData, } from "../utils/checkpoint"; +import { getSuperAgentErrorMessage } from "../utils/agentApiParse"; import { shouldShowBodyWorkspaceEntry } from "../utils/workspace"; import { buildStreamConversationMessages, @@ -457,6 +458,7 @@ const SuperAgent = () => { activeSessionId, activeConversationId, isLoadingSessions, + sidebarError, isUploadingFile, isCreatingChat, isCreatingTask, @@ -799,7 +801,7 @@ const SuperAgent = () => { ]); const handleProtocolError = useCallback((error) => { - message.error(error?.message || "智能体服务暂不可用"); + message.error(getSuperAgentErrorMessage(error, "智能体服务暂不可用")); }, []); const stream = useSuperAgentStream({ @@ -2444,6 +2446,7 @@ const SuperAgent = () => { collapsedFolderMap, activeSessionId: activeConversationId || activeSessionId, isLoadingSessions, + sidebarError, isCreatingChat, isCreatingTask, isLoggedIn, diff --git a/web/client/src/sections/superAgent/hooks/useSuperAgentChat.js b/web/client/src/sections/superAgent/hooks/useSuperAgentChat.js index 9878694..6d798c1 100644 --- a/web/client/src/sections/superAgent/hooks/useSuperAgentChat.js +++ b/web/client/src/sections/superAgent/hooks/useSuperAgentChat.js @@ -110,6 +110,7 @@ const useSuperAgentChat = ({ sidebarTree, taskList, ordinaryChatList, + sidebarError, isLoadingSidebar, collapsedFolderMap, loadSidebar, @@ -1865,6 +1866,7 @@ const useSuperAgentChat = ({ uploadedFileList, uploadProgress, ordinaryChatList, + sidebarError, sidebarTree, collapsedFolderMap, activeSessionId, diff --git a/web/client/src/sections/superAgent/hooks/useSuperAgentSidebar.js b/web/client/src/sections/superAgent/hooks/useSuperAgentSidebar.js index 73afd52..23a2af0 100644 --- a/web/client/src/sections/superAgent/hooks/useSuperAgentSidebar.js +++ b/web/client/src/sections/superAgent/hooks/useSuperAgentSidebar.js @@ -13,6 +13,7 @@ import { } from "../actions/api"; import { listAllConversations, listAllTasksets } from "../actions/agentResources"; import { computeSessionDrop } from "../utils/sidebarDrag"; +import { getSuperAgentErrorMessage } from "../utils/agentApiParse"; import { normalizeSessionTitle, buildSidebarTreeFromAgentResources, @@ -44,6 +45,7 @@ const EMPTY_TREE = { realms: [] }; const useSuperAgentSidebar = ({ currentUserId, actions, dispatch }) => { const [sidebarTree, setSidebarTree] = useState(EMPTY_TREE); const [isLoadingSidebar, setIsLoadingSidebar] = useState(false); + const [sidebarError, setSidebarError] = useState(null); const [collapsedFolderMap, setCollapsedFolderMap] = useState(() => readCollapsedFolderMap()); const taskList = useMemo( @@ -73,9 +75,15 @@ const useSuperAgentSidebar = ({ currentUserId, actions, dispatch }) => { ]); const nextTree = buildSidebarTreeFromAgentResources(tasksets, conversations); setSidebarTree(nextTree); + setSidebarError(null); return nextTree; } catch (error) { - message.error(error?.message || "获取侧边栏失败"); + const normalizedError = { + ...error, + message: getSuperAgentErrorMessage(error, "获取侧边栏失败"), + }; + setSidebarError(normalizedError); + message.error(normalizedError.message); return EMPTY_TREE; } finally { if (shouldShowLoading) { @@ -236,6 +244,7 @@ const useSuperAgentSidebar = ({ currentUserId, actions, dispatch }) => { return { sidebarTree, + sidebarError, taskList, ordinaryChatList, isLoadingSidebar, diff --git a/web/client/src/sections/superAgent/hooks/useSuperAgentStream.js b/web/client/src/sections/superAgent/hooks/useSuperAgentStream.js index 1f31090..1204aea 100644 --- a/web/client/src/sections/superAgent/hooks/useSuperAgentStream.js +++ b/web/client/src/sections/superAgent/hooks/useSuperAgentStream.js @@ -20,11 +20,13 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useStream } from "@langchain/react"; import { ProtocolSseTransportAdapter } from "@langchain/langgraph-sdk"; import { ROOT_PUMP_CHANNELS } from "@langchain/langgraph-sdk/stream"; -import { getCompatibleToken } from "../../../utils/authCompat"; import { + clearSuperAgentAccessToken, getSuperAgentFrameworkApiUrl, withAgentAuthorization, } from "../utils/agentApi"; +import { getSuperAgentErrorMessage } from "../utils/agentApiParse"; +import { superAgentLog, superAgentWarn } from "../utils/debugLog"; const BUSINESS_CHANNELS = [ "custom", @@ -254,9 +256,16 @@ class SingleSubscriberSseTransportAdapter extends ProtocolSseTransportAdapter { async getState() { const requestVersion = ++this.stateRequestVersion; const requestedThreadId = String(this.threadId || "").trim(); - // Agent 当前未提供 /threads/:threadId/state;useStream hydrate 只能使用 - // 已从事件流获得的快照,不能为新会话触发一个必然失败的 HTTP 请求。 - const state = this.latestState; + if (!requestedThreadId) { + this.latestState = null; + this.onStateChange?.(); + return null; + } + + // ProtocolSseTransportAdapter 的 getState() 会请求当前 thread 的 + // /state;其 fetchImpl 已绑定 fetchUseStreamTransport,因此会复用 + // 8005 代理、Agent JWT 和 401 换票逻辑。 + const state = await super.getState(); // 旧 state 快照不能在切换 thread 后覆盖新会话。 if ( requestVersion === this.stateRequestVersion && @@ -534,14 +543,21 @@ const getEventDedupeKey = (event) => { const normalizeStreamError = (error) => { const status = error?.status || error?.response?.status; - if (status === 401 || status === 403) return new Error("登录状态已失效,请重新登录"); - if (status === 404) return new Error("智能体服务暂不可用"); + const normalized = error instanceof Error ? error : new Error("智能体协议响应错误"); + normalized.status = status || normalized.status || 0; + normalized.code = error?.code || normalized.code || ""; + normalized.requestId = error?.requestId || error?.request_id || normalized.requestId || ""; + normalized.message = getSuperAgentErrorMessage(normalized, "智能体协议响应错误"); + if (status === 404) normalized.message = "智能体服务暂不可用"; const message = String(error?.message || ""); if (status === 429 || /(?:^|\D)429(?:\D|$)|too many requests|rate limit/i.test(message)) { - return new Error("事件流连接繁忙,正在自动恢复,请稍候"); + normalized.message = "事件流连接繁忙,正在自动恢复,请稍候"; } - if (/sse|stream|network|fetch/i.test(message)) return new Error("智能体流式连接异常"); - return error instanceof Error ? error : new Error("智能体协议响应错误"); + if (/sse|stream|network|fetch/i.test(message)) normalized.message = "智能体流式连接异常"; + if (normalized.requestId && !normalized.message.includes(normalized.requestId)) { + normalized.message = `${normalized.message}(request_id: ${normalized.requestId})`; + } + return normalized; }; const useSuperAgentStream = ({ @@ -584,16 +600,31 @@ const useSuperAgentStream = ({ const localRunHasSeenLoadingRef = useRef(false); const activeSessionIdRef = useRef(""); activeSessionIdRef.current = activeSessionId; - const loginToken = getCompatibleToken(); - // 保持登录态变化时重建 transport;Agent JWT 在 fetch 拦截器中换票。 - // 会话切换由 callerOptions 的会话级 fetch 变化触发新 controller。 + // Agent 请求只使用短时 Agent JWT;ai-center token 只用于 query api 换票。 const defaultHeaders = useMemo( () => ({ Accept: "application/json, text/event-stream", - ...(loginToken ? { "X-PEP-Token": loginToken } : {}), }), - [loginToken] + [] ); + const fetchAuthorizedStream = useCallback(async (input, requestInit) => { + let authorizedInit = await withAgentAuthorization(requestInit); + let response = await fetchEventStreamWithRecovery(input, authorizedInit); + if (response.status !== 401 || requestInit?.signal?.aborted) return response; + + superAgentWarn("stream.auth", "agent_token_refresh", { + endpoint: String(input?.url || input || "").split("?")[0], + status: response.status, + }); + clearSuperAgentAccessToken(); + authorizedInit = await withAgentAuthorization(requestInit); + response = await fetchEventStreamWithRecovery(input, authorizedInit); + superAgentLog("stream.auth", "agent_token_refresh_result", { + endpoint: String(input?.url || input || "").split("?")[0], + status: response.status, + }); + return response; + }, []); const fetchUseStreamTransport = useCallback(async (...args) => { const [input, init = {}] = args; if (isUseStreamHistoryDiscoveryRequest(input)) { @@ -629,9 +660,8 @@ const useSuperAgentStream = ({ }; } - const authorizedInit = await withAgentAuthorization(requestInit); - return fetchEventStreamWithRecovery(input, authorizedInit); - }, []); + return fetchAuthorizedStream(input, requestInit); + }, [fetchAuthorizedStream]); const eventTransport = useMemo( () => { const transport = new SingleSubscriberSseTransportAdapter({ @@ -898,6 +928,11 @@ const useSuperAgentStream = ({ // 切换会话后旧 Thread 的监听器可能在清理前再收到一个事件, // 不能把旧会话的事件写入新会话的流式消息状态。 if (activeSessionIdRef.current !== listenerSessionId) return; + superAgentLog("stream.event", "received", { + threadId: listenerSessionId, + eventType: event?.method || "unknown", + lifecycleType: event?.params?.data?.event || "", + }); handleMessageEvent(event); const eventPayload = getEventPayload(event) || {}; if (shouldRefreshRuntimeState({ diff --git a/web/client/src/sections/superAgent/hooks/useSuperAgentWorkspace.js b/web/client/src/sections/superAgent/hooks/useSuperAgentWorkspace.js index a3c438f..56f365d 100644 --- a/web/client/src/sections/superAgent/hooks/useSuperAgentWorkspace.js +++ b/web/client/src/sections/superAgent/hooks/useSuperAgentWorkspace.js @@ -58,6 +58,7 @@ import { isSupportedCheckpointUiType, normalizeCheckpoint, } from "../utils/checkpoint"; +import { getSuperAgentErrorMessage } from "../utils/agentApiParse"; import { renderSuperAgentMarkdown } from "../utils/tenderMarkdown"; const buildSectionParams = (sessionId, sectionId) => ({ @@ -647,7 +648,7 @@ const useSuperAgentWorkspace = ({ error, }); if (!silent) { - message.error(error?.message || "刷新正文工作区失败"); + message.error(getSuperAgentErrorMessage(error, "刷新正文工作区失败")); } return false; } @@ -1332,7 +1333,7 @@ const useSuperAgentWorkspace = ({ workspaceType: workspaceData.type, error, }); - message.error(error?.message || "保存修改失败"); + message.error(getSuperAgentErrorMessage(error, "保存修改失败")); } }; @@ -1662,7 +1663,7 @@ const useSuperAgentWorkspace = ({ await refreshSectionAfterConflict(sessionId, { id: sectionId }); message.warning("章节已被智能体更新,请刷新后重新编辑"); } else { - message.error(error?.message || "章节保存失败,请稍后重试"); + message.error(getSuperAgentErrorMessage(error, "章节保存失败,请稍后重试")); } return false; } @@ -1716,7 +1717,7 @@ const useSuperAgentWorkspace = ({ sessionId, error, }); - message.error(error?.message || "导出失败"); + message.error(getSuperAgentErrorMessage(error, "导出失败")); } }; @@ -1802,7 +1803,7 @@ const useSuperAgentWorkspace = ({ error, }); if (!silent) { - message.error(error?.message || "获取正文章节失败"); + message.error(getSuperAgentErrorMessage(error, "获取正文章节失败")); } return false; } diff --git a/web/client/src/sections/superAgent/utils/agentApi.js b/web/client/src/sections/superAgent/utils/agentApi.js index 02e9234..fdd2d2a 100644 --- a/web/client/src/sections/superAgent/utils/agentApi.js +++ b/web/client/src/sections/superAgent/utils/agentApi.js @@ -25,6 +25,7 @@ import { parseAgentErrorPayload, shouldSendIdempotencyKey, } from "./agentApiParse"; +import { superAgentLog, superAgentWarn } from "./debugLog"; const ACCESS_TOKEN_PATH = "/_api/super-agent/access-token"; const TOKEN_REFRESH_SKEW_MS = 30 * 1000; @@ -125,7 +126,15 @@ export const issueSuperAgentAccessToken = async ({ force = false } = {}) => { const data = await readJsonSafe(response); if (!response.ok || !data?.token) { clearSuperAgentAccessToken(); - throw parseAgentErrorPayload(data, response.status, "签发 Agent 访问令牌失败"); + const error = parseAgentErrorPayload(data, response.status, "签发 Agent 访问令牌失败"); + error.requestId = + error.requestId || + response.headers.get("x-request-id") || + response.headers.get("request-id") || + ""; + error.code = "AGENT_TOKEN_ISSUE_FAILED"; + error.message = "Agent JWT 签发失败,请刷新页面重试"; + throw error; } const expiresAt = data.expires_at @@ -155,13 +164,21 @@ const parseAgentResponse = async (response, fallbackMessage, returnMeta = false) } const data = await readJsonSafe(response); if (!response.ok) { - throw parseAgentErrorPayload(data, response.status, fallbackMessage); + const error = parseAgentErrorPayload(data, response.status, fallbackMessage); + error.requestId = + error.requestId || + response.headers.get("x-request-id") || + response.headers.get("request-id") || + ""; + throw error; } return returnMeta ? { data, headers: response.headers, status: response.status } : data; }; +const getRequestPath = (path, params = {}) => buildSuperAgentPath(path, params); + export const requestAgentJson = async ({ method, path, @@ -206,7 +223,19 @@ export const requestAgentJson = async ({ } ); + superAgentLog("api.request", "response", { + endpoint: getRequestPath(path, params), + service, + method: methodName, + status: response.status, + }); + if (response.status === 401 && retryOnUnauthorized) { + superAgentWarn("api.auth", "agent_token_refresh", { + endpoint: getRequestPath(path, params), + service, + status: response.status, + }); clearSuperAgentAccessToken(); return requestAgentJson({ method: methodName, @@ -224,7 +253,13 @@ export const requestAgentJson = async ({ }); } - return parseAgentResponse(response, fallbackMessage, returnMeta); + return parseAgentResponse( + response, + response.status === 401 + ? "Agent 访问令牌已失效,换票失败,请刷新页面重试" + : fallbackMessage, + returnMeta + ); }; export const withAgentAuthorization = async (init = {}) => { diff --git a/web/client/src/sections/superAgent/utils/agentApiParse.js b/web/client/src/sections/superAgent/utils/agentApiParse.js index 4725d17..8774d84 100644 --- a/web/client/src/sections/superAgent/utils/agentApiParse.js +++ b/web/client/src/sections/superAgent/utils/agentApiParse.js @@ -63,6 +63,33 @@ export const parseAgentErrorPayload = (data, status, fallbackMessage) => { }); }; +export const getSuperAgentErrorMessage = (error, fallbackMessage = "超级智能体请求失败") => { + const status = Number(error?.status || error?.response?.status || 0); + const code = String(error?.code || "").toUpperCase(); + let message = String(error?.message || fallbackMessage); + + if (code === "AGENT_TOKEN_ISSUE_FAILED") { + message = "Agent JWT 签发失败,请刷新页面重试"; + } else if (code === "AUTHENTICATION_REQUIRED") { + message = "请先登录后再使用飞小尚超级智能体"; + } else if (status === 401 || code === "UNAUTHORIZED") { + message = "Agent 访问令牌已失效,换票失败,请刷新页面重试"; + } else if (status === 403) { + message = "当前账号没有执行此操作的权限"; + } else if (status === 409) { + message = "请求与当前运行冲突,请稍后重试"; + } else if (status === 412) { + message = "内容已被更新,请刷新后重试"; + } else if (status === 422) { + message = message || "请求参数校验失败"; + } else if (!message || /failed to fetch|networkerror/i.test(message)) { + message = "网络连接异常,请检查网络后重试"; + } + + const requestId = String(error?.requestId || error?.request_id || "").trim(); + return requestId ? `${message}(request_id: ${requestId})` : message; +}; + export const createIdempotencyKey = () => { if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") { return crypto.randomUUID(); diff --git a/web/client/src/sections/superAgent/utils/debugLog.js b/web/client/src/sections/superAgent/utils/debugLog.js index c39ab87..ce29b73 100644 --- a/web/client/src/sections/superAgent/utils/debugLog.js +++ b/web/client/src/sections/superAgent/utils/debugLog.js @@ -5,7 +5,6 @@ */ "use strict"; -const MAX_PREVIEW = 160; const MAX_KEYS = 14; const getWindowEnv = () => { @@ -33,7 +32,6 @@ const summarizeString = (value) => { return { type: "string", len: text.length, - preview: text.length > MAX_PREVIEW ? `${text.slice(0, MAX_PREVIEW)}...` : text, }; }; @@ -44,8 +42,10 @@ export const summarizeSuperAgentPayload = (value, depth = 0) => { if (value instanceof Error) { return { name: value.name, - message: value.message, - stack: value.stack ? summarizeString(value.stack) : undefined, + message: summarizeString(value.message), + status: value.status || value.response?.status || 0, + code: value.code || "", + requestId: value.requestId || "", }; } if (typeof File !== "undefined" && value instanceof File) { @@ -71,7 +71,7 @@ export const summarizeSuperAgentPayload = (value, depth = 0) => { }; if (depth >= 2) return out; keys.slice(0, MAX_KEYS).forEach((key) => { - if (["content", "text", "message", "raw", "html", "markdown"].includes(key)) { + if (/(?:token|authorization|secret|password|content|text|message|raw|html|markdown|input|body|file)/i.test(key)) { out[key] = summarizeString(value[key]); } else if (["payload", "data", "checkpoint", "task", "edits", "sections", "chapters"].includes(key)) { out[key] = summarizeSuperAgentPayload(value[key], depth + 1); diff --git a/web/client/src/sections/superAgent/utils/session.js b/web/client/src/sections/superAgent/utils/session.js index 1c28f7d..174dae8 100644 --- a/web/client/src/sections/superAgent/utils/session.js +++ b/web/client/src/sections/superAgent/utils/session.js @@ -23,10 +23,19 @@ import { ACTIVE_SESSION_STORAGE_KEY, DEFAULT_TASK_FOLDER_ID, DEFAULT_TASK_FOLDER_NAME, + LEGACY_SUPER_AGENT_STORAGE_KEYS, TASK_SESSION_STORAGE_KEY, TASK_STORAGE_KEY, } from "../constants"; +let hasClearedLegacyStorage = false; + +const ensureVersionedStorage = () => { + if (hasClearedLegacyStorage || typeof window === "undefined") return; + LEGACY_SUPER_AGENT_STORAGE_KEYS.forEach((key) => window.localStorage.removeItem(key)); + hasClearedLegacyStorage = true; +}; + export const normalizeSessionTitle = (title, fallbackTitle) => { const sessionTitle = String(title || "").trim(); if (sessionTitle.toLowerCase() === "new session") return "新对话"; @@ -35,6 +44,7 @@ export const normalizeSessionTitle = (title, fallbackTitle) => { }; export const readTaskListFromStorage = () => { + ensureVersionedStorage(); try { const rawValue = window.localStorage.getItem(TASK_STORAGE_KEY); const parsedValue = JSON.parse(rawValue || "[]"); @@ -49,10 +59,12 @@ export const readTaskListFromStorage = () => { }; export const saveTaskListToStorage = (taskList) => { + ensureVersionedStorage(); window.localStorage.setItem(TASK_STORAGE_KEY, JSON.stringify(taskList)); }; export const readTenderSessionIds = () => { + ensureVersionedStorage(); try { const rawValue = window.localStorage.getItem(TASK_SESSION_STORAGE_KEY); const parsedValue = JSON.parse(rawValue || "[]"); @@ -65,12 +77,14 @@ export const readTenderSessionIds = () => { }; export const markTenderSessionId = (sessionId) => { + ensureVersionedStorage(); if (!sessionId) return; const nextIds = Array.from(new Set([String(sessionId), ...readTenderSessionIds()])); window.localStorage.setItem(TASK_SESSION_STORAGE_KEY, JSON.stringify(nextIds)); }; export const removeTenderSessionId = (sessionId) => { + ensureVersionedStorage(); if (!sessionId) return; const nextIds = readTenderSessionIds().filter((item) => item !== String(sessionId)); window.localStorage.setItem(TASK_SESSION_STORAGE_KEY, JSON.stringify(nextIds)); @@ -101,6 +115,7 @@ const normalizePendingEmptySessionMap = (parsedValue = {}) => { }; export const readPendingEmptySessionMap = () => { + ensureVersionedStorage(); try { const rawValue = window.localStorage.getItem(PENDING_EMPTY_SESSION_STORAGE_KEY); const parsedValue = JSON.parse(rawValue || "{}"); @@ -117,6 +132,7 @@ export const readPendingEmptySession = (type = "") => { }; export const markPendingEmptySession = (sessionId, type, folderId = null) => { + ensureVersionedStorage(); if (!sessionId || !type) return; const sessionMap = readPendingEmptySessionMap(); const nextSessionMap = { @@ -134,6 +150,7 @@ export const markPendingEmptySession = (sessionId, type, folderId = null) => { }; export const clearPendingEmptySession = (sessionId = "") => { + ensureVersionedStorage(); if (!sessionId) { window.localStorage.removeItem(PENDING_EMPTY_SESSION_STORAGE_KEY); return; @@ -159,6 +176,7 @@ export const clearPendingEmptySession = (sessionId = "") => { }; export const readActiveSessionId = () => { + ensureVersionedStorage(); try { return String(window.localStorage.getItem(ACTIVE_SESSION_STORAGE_KEY) || "").trim(); } catch (error) { @@ -167,11 +185,13 @@ export const readActiveSessionId = () => { }; export const saveActiveSessionId = (sessionId) => { + ensureVersionedStorage(); if (!sessionId) return; window.localStorage.setItem(ACTIVE_SESSION_STORAGE_KEY, String(sessionId)); }; export const clearActiveSessionId = (sessionId = "") => { + ensureVersionedStorage(); if (!sessionId) { window.localStorage.removeItem(ACTIVE_SESSION_STORAGE_KEY); return; @@ -183,6 +203,7 @@ export const clearActiveSessionId = (sessionId = "") => { }; export const readCollapsedFolderMap = () => { + ensureVersionedStorage(); try { const rawValue = window.localStorage.getItem(FOLDER_COLLAPSED_STORAGE_KEY); const parsedValue = JSON.parse(rawValue || "{}"); @@ -193,6 +214,7 @@ export const readCollapsedFolderMap = () => { }; export const saveCollapsedFolderMap = (folderMap) => { + ensureVersionedStorage(); window.localStorage.setItem(FOLDER_COLLAPSED_STORAGE_KEY, JSON.stringify(folderMap || {})); }; diff --git a/web/tests/agentApiAdapter.test.mjs b/web/tests/agentApiAdapter.test.mjs index 5efcdfb..54677af 100644 --- a/web/tests/agentApiAdapter.test.mjs +++ b/web/tests/agentApiAdapter.test.mjs @@ -12,6 +12,7 @@ const { isAgentUuid, parseAgentErrorPayload, parseAgentPage, + getSuperAgentErrorMessage, shouldSendIdempotencyKey, } = await import("../client/src/sections/superAgent/utils/agentApiParse.js"); const { @@ -81,6 +82,16 @@ test("attachment upload intent follows the v2 three-stage payload", () => { assert.equal(isUploadIntentExpired(intent, Date.parse("2031-01-01T00:00:00Z")), true); }); +test("agent errors keep request_id without exposing credentials", () => { + const error = parseAgentErrorPayload( + { code: "UNAUTHORIZED", message: "expired", request_id: "req_auth_1" }, + 401, + "fallback", + ); + assert.match(getSuperAgentErrorMessage(error), /换票失败/); + assert.match(getSuperAgentErrorMessage(error), /req_auth_1/); +}); + test("adapter source exchanges login token for agent jwt and omits identity headers", () => { const table = read("client", "src", "utils", "superAgentApiTable.js"); const agentApi = read("client", "src", "sections", "superAgent", "utils", "agentApi.js"); @@ -113,6 +124,9 @@ test("adapter source exchanges login token for agent jwt and omits identity head assert.match(agentApi, /buildSuperAgentServiceUrl = \(path = "", service = superAgentBusinessService\)/); assert.match(agentApi, /superAgentFrameworkProxyMatch/); assert.match(agentApi, /getSuperAgentFrameworkApiUrl/); + assert.match(agentApi, /response\.status === 401 && retryOnUnauthorized/); + assert.match(streamHook, /fetchAuthorizedStream/); + assert.doesNotMatch(streamHook, /X-PEP-Token/); assert.match(resources, /completeAttachmentUpload/); assert.match(resources, /listAttachments/); assert.match(resources, /getArtifactChapter/); @@ -133,3 +147,10 @@ test("adapter source exchanges login token for agent jwt and omits identity head assert.match(uploadUtils, /xhr\.send\(file\)/); assert.match(resources, /cancelThreadRun/); }); + +test("superAgent diagnostics redact message and file payload text", () => { + const debugLog = read("client", "src", "sections", "superAgent", "utils", "debugLog.js"); + assert.doesNotMatch(debugLog, /preview:/); + assert.match(debugLog, /token|authorization/); + assert.match(debugLog, /requestId/); +}); diff --git a/web/tests/superAgentFlowContract.test.mjs b/web/tests/superAgentFlowContract.test.mjs index 6e647db..fdeb77c 100644 --- a/web/tests/superAgentFlowContract.test.mjs +++ b/web/tests/superAgentFlowContract.test.mjs @@ -32,7 +32,8 @@ assert.match(streamHook, /stream\.submit\(/); assert.match(streamHook, /stream\.respond\(/); assert.match(streamHook, /role:\s*"user"/); assert.doesNotMatch(streamHook, /type:\s*"human"/); -assert.doesNotMatch(streamHook, /super\.getState\(\)/); + assert.match(streamHook, /const state = await super\.getState\(\)/); + assert.match(streamHook, /hydratedState\?\.values\?\.messages/); assert.match(streamHook, /const stopRun =/); assert.match(streamHook, /await stream\.stop\(\)/); assert.match(streamHook, /disconnect:\s*stream\.disconnect/); @@ -42,6 +43,11 @@ assert.match(streamHook, /status: 204/); assert.match(streamHook, /currentThread\.close/); assert.match(streamHook, /event\?\.method === "input\.requested"/); assert.match(streamHook, /runtimeInterrupts/); +assert.match(streamHook, /setRunPhase\("failed"\)/); +assert.match(streamHook, /setIsBusinessRunning\(false\)/); +assert.match(streamHook, /eventType: event\?\.method/); +assert.match(streamHook, /requestId/); +assert.match(container, /getSuperAgentErrorMessage/); assert.doesNotMatch(container, /useSuperAgentChat/); assert.doesNotMatch(container, /respondSuperAgentCheckpoint/); assert.doesNotMatch(container, /sendSuperAgentChat/); diff --git a/web/tests/superAgentSidebarContract.test.mjs b/web/tests/superAgentSidebarContract.test.mjs index b9f8e43..e9595d0 100644 --- a/web/tests/superAgentSidebarContract.test.mjs +++ b/web/tests/superAgentSidebarContract.test.mjs @@ -10,6 +10,7 @@ const sidebarHook = read("client", "src", "sections", "superAgent", "hooks", "us const chatHook = read("client", "src", "sections", "superAgent", "hooks", "useSuperAgentChat.js"); const actions = read("client", "src", "sections", "superAgent", "actions", "api.js"); const router = read("client", "src", "sections", "superAgent", "router.js"); +const constants = read("client", "src", "sections", "superAgent", "constants.js"); function loadSessionUtils() { const transformed = @@ -30,6 +31,13 @@ test("chat route remains /chat", () => { assert.match(router, /path: "\/chat"/); }); +test("superAgent storage keys are versioned and legacy ids are cleared", () => { + assert.match(constants, /superAgent_v2_active_conversation_id/); + assert.match(constants, /LEGACY_SUPER_AGENT_STORAGE_KEYS/); + assert.match(sessionSource, /ensureVersionedStorage/); + assert.match(sessionSource, /removeItem\(key\)/); +}); + test("sidebar session keeps conversation id and thread id separate", () => { const session = normalizeSidebarSession({ id: "11111111-1111-4111-8111-111111111111",