Browse Source

feat: 前端状态、错误与可观测性

master
Senor-Liu 1 month ago
parent
commit
67e1eb3500
  1. 3
      api/.vscode/launch.json
  2. 4
      api/app/controllers/superAgentProtocol.js
  3. 7
      api/config/default.js
  4. 3
      web/client/src/sections/superAgent/components/SuperAgentSidebar.jsx
  5. 16
      web/client/src/sections/superAgent/constants.js
  6. 5
      web/client/src/sections/superAgent/container/index.jsx
  7. 2
      web/client/src/sections/superAgent/hooks/useSuperAgentChat.js
  8. 11
      web/client/src/sections/superAgent/hooks/useSuperAgentSidebar.js
  9. 69
      web/client/src/sections/superAgent/hooks/useSuperAgentStream.js
  10. 11
      web/client/src/sections/superAgent/hooks/useSuperAgentWorkspace.js
  11. 41
      web/client/src/sections/superAgent/utils/agentApi.js
  12. 27
      web/client/src/sections/superAgent/utils/agentApiParse.js
  13. 10
      web/client/src/sections/superAgent/utils/debugLog.js
  14. 22
      web/client/src/sections/superAgent/utils/session.js
  15. 21
      web/tests/agentApiAdapter.test.mjs
  16. 8
      web/tests/superAgentFlowContract.test.mjs
  17. 8
      web/tests/superAgentSidebarContract.test.mjs

3
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",
}
}

4
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;
};

7
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 || "",

3
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 ? (
<div className="super-history-empty">正在加载会话...</div>
) : sidebarError ? (
<div className="super-history-empty">{sidebarError.message}</div>
) : (
<>
<div className="sidebar-realm-block">

16
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 办公助手";

5
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,

2
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,

11
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,

69
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({

11
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;
}

41
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 = {}) => {

27
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();

10
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);

22
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 || {}));
};

21
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/);
});

8
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/);

8
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",

Loading…
Cancel
Save