Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions messages/en/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,24 @@
"batchTerminateNone": "No sessions were terminated",
"noSelection": "Please select at least one session"
},
"codeDisplay": {
"raw": "Raw",
"pretty": "Pretty",
"searchPlaceholder": "Search",
"expand": "Expand",
"collapse": "Collapse",
"themeAuto": "Auto",
"themeLight": "Light",
"themeDark": "Dark",
"noMatches": "No matches",
"onlyMatches": "Only matches",
"showAll": "Show all",
"prevPage": "Prev",
"nextPage": "Next",
"pageInfo": "Page {page} / {total}",
"sseEvent": "Event",
"sseData": "Data"
},
"status": {
"loading": "Loading...",
"loadError": "Load failed",
Expand Down
18 changes: 18 additions & 0 deletions messages/ja/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,24 @@
"batchTerminateNone": "終了できたセッションはありません",
"noSelection": "少なくとも1つのセッションを選択してください"
},
"codeDisplay": {
"raw": "Raw",
"pretty": "Pretty",
"searchPlaceholder": "検索",
"expand": "展開",
"collapse": "折りたたむ",
"themeAuto": "自動",
"themeLight": "ライト",
"themeDark": "ダーク",
"noMatches": "一致する結果はありません",
"onlyMatches": "一致のみ",
"showAll": "すべて表示",
"prevPage": "前へ",
"nextPage": "次へ",
"pageInfo": "{page} / {total} ページ",
"sseEvent": "イベント",
"sseData": "データ"
},
"status": {
"loading": "読み込み中...",
"loadError": "読み込み失敗",
Expand Down
18 changes: 18 additions & 0 deletions messages/ru/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,24 @@
"batchTerminateNone": "Не удалось прервать ни одной сессии",
"noSelection": "Выберите хотя бы одну сессию"
},
"codeDisplay": {
"raw": "Сырой",
"pretty": "Форматированный",
"searchPlaceholder": "Поиск",
"expand": "Развернуть",
"collapse": "Свернуть",
"themeAuto": "Авто",
"themeLight": "Светлая",
"themeDark": "Тёмная",
"noMatches": "Нет совпадений",
"onlyMatches": "Только совпадения",
"showAll": "Показать всё",
"prevPage": "Назад",
"nextPage": "Вперёд",
"pageInfo": "Страница {page} / {total}",
"sseEvent": "Событие",
"sseData": "Данные"
},
"status": {
"loading": "Загрузка...",
"loadError": "Ошибка загрузки",
Expand Down
18 changes: 18 additions & 0 deletions messages/zh-CN/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,24 @@
"batchTerminateNone": "没有任何 Session 被终止",
"noSelection": "请至少选择一个 Session"
},
"codeDisplay": {
"raw": "原始",
"pretty": "美化",
"searchPlaceholder": "搜索",
"expand": "展开",
"collapse": "收起",
"themeAuto": "跟随系统",
"themeLight": "浅色",
"themeDark": "深色",
"noMatches": "无匹配结果",
"onlyMatches": "仅匹配行",
"showAll": "显示全部",
"prevPage": "上一页",
"nextPage": "下一页",
"pageInfo": "第 {page} / {total} 页",
"sseEvent": "事件",
"sseData": "数据"
},
"status": {
"loading": "加载中...",
"loadError": "加载失败",
Expand Down
18 changes: 18 additions & 0 deletions messages/zh-TW/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,24 @@
"batchTerminateNone": "沒有任何會話被終止",
"noSelection": "請至少選擇一個會話"
},
"codeDisplay": {
"raw": "原始",
"pretty": "美化",
"searchPlaceholder": "搜尋",
"expand": "展開",
"collapse": "收起",
"themeAuto": "跟隨系統",
"themeLight": "淺色",
"themeDark": "深色",
"noMatches": "沒有符合結果",
"onlyMatches": "僅符合行",
"showAll": "顯示全部",
"prevPage": "上一頁",
"nextPage": "下一頁",
"pageInfo": "第 {page} / {total} 頁",
"sseEvent": "事件",
"sseData": "資料"
},
"status": {
"loading": "載入中...",
"loadError": "載入失敗",
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"clean:cache": "rm -rf .next tsconfig.tsbuildinfo node_modules/.cache",
"test": "vitest run",
"test:ui": "vitest --ui --watch",
"test:e2e": "vitest run tests/e2e/ --reporter=verbose",
"test:e2e": "vitest run --config vitest.e2e.config.ts --reporter=verbose",
"test:coverage": "vitest run --coverage",
"test:ci": "vitest run --reporter=default --reporter=junit --outputFile.junit=reports/vitest-junit.xml",
"cui": "npx cui-server --host 0.0.0.0 --port 30000 --token a7564bc8882aa9a2d25d8b4ea6ea1e2e",
Expand Down Expand Up @@ -75,6 +75,7 @@
"react-day-picker": "^9",
"react-dom": "^19",
"react-hook-form": "^7",
"react-syntax-highlighter": "^16.1.0",
"recharts": "^3",
"safe-regex": "^2",
"server-only": "^0.0.1",
Expand All @@ -94,6 +95,7 @@
"@types/pg": "^8",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/react-syntax-highlighter": "^15.5.13",
"@typescript/native-preview": "7.0.0-dev.20251219.1",
"@vitest/coverage-v8": "^4.0.16",
"@vitest/ui": "^4.0.16",
Expand Down
19 changes: 18 additions & 1 deletion src/actions/active-sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,20 @@ export async function getSessionDetails(
const normalizedSequence = normalizeRequestSequence(requestSequence);
const effectiveSequence = normalizedSequence ?? (requestCount > 0 ? requestCount : undefined);

const parseJsonStringOrNull = (value: unknown): unknown => {
if (typeof value !== "string") return value;
try {
return JSON.parse(value) as unknown;
} catch (error) {
logger.warn("getSessionDetails: failed to parse session messages JSON string", {
sessionId,
requestSequence: effectiveSequence ?? null,
error,
});
return null;
}
};

// 6. 并行获取 messages 和 response(不缓存,因为这些数据较大)
const [messages, response, requestHeaders, responseHeaders] = await Promise.all([
SessionManager.getSessionMessages(sessionId, effectiveSequence),
Expand All @@ -589,10 +603,13 @@ export async function getSessionDetails(
SessionManager.getSessionResponseHeaders(sessionId, effectiveSequence),
]);

// 兼容:历史/异常数据可能是 JSON 字符串(前端需要根级对象/数组)
const normalizedMessages = parseJsonStringOrNull(messages);

return {
ok: true,
data: {
messages,
messages: normalizedMessages,
response,
requestHeaders,
responseHeaders,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"use client";

import { useTranslations } from "next-intl";
import { useMemo } from "react";
import { CodeDisplay } from "@/components/ui/code-display";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { isSSEText } from "@/lib/utils/sse";

export type SessionMessages = Record<string, unknown> | Record<string, unknown>[];

function formatHeaders(headers: Record<string, string> | null): string | null {
if (!headers || Object.keys(headers).length === 0) return null;
return Object.entries(headers)
.map(([key, value]) => `${key}: ${value}`)
.join("\n");
}

interface SessionMessagesDetailsTabsProps {
messages: SessionMessages | null;
requestHeaders: Record<string, string> | null;
responseHeaders: Record<string, string> | null;
response: string | null;
}

export function SessionMessagesDetailsTabs({
messages,
response,
requestHeaders,
responseHeaders,
}: SessionMessagesDetailsTabsProps) {
const t = useTranslations("dashboard.sessions");

const requestBodyContent = useMemo(() => {
if (messages === null) return null;
return JSON.stringify(messages, null, 2);
}, [messages]);

const formattedRequestHeaders = useMemo(() => formatHeaders(requestHeaders), [requestHeaders]);
const formattedResponseHeaders = useMemo(() => formatHeaders(responseHeaders), [responseHeaders]);

const responseLanguage = response && isSSEText(response) ? "sse" : "json";

return (
<Tabs defaultValue="requestBody" className="w-full" data-testid="session-details-tabs">
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="requestHeaders" data-testid="session-tab-trigger-request-headers">
{t("details.requestHeaders")}
</TabsTrigger>
<TabsTrigger value="requestBody" data-testid="session-tab-trigger-request-body">
{t("details.requestBody")}
</TabsTrigger>
<TabsTrigger value="responseHeaders" data-testid="session-tab-trigger-response-headers">
{t("details.responseHeaders")}
</TabsTrigger>
<TabsTrigger value="responseBody" data-testid="session-tab-trigger-response-body">
{t("details.responseBody")}
</TabsTrigger>
</TabsList>

<TabsContent value="requestHeaders" data-testid="session-tab-request-headers">
{formattedRequestHeaders === null ? (
<div className="text-muted-foreground p-4">{t("details.noHeaders")}</div>
) : (
<CodeDisplay
content={formattedRequestHeaders}
language="text"
fileName="request.headers"
maxHeight="600px"
/>
)}
</TabsContent>

<TabsContent value="requestBody" data-testid="session-tab-request-body">
{requestBodyContent === null ? (
<div className="text-muted-foreground p-4">{t("details.noData")}</div>
) : (
<CodeDisplay
content={requestBodyContent}
language="json"
fileName="request.json"
maxHeight="600px"
/>
)}
</TabsContent>

<TabsContent value="responseHeaders" data-testid="session-tab-response-headers">
{formattedResponseHeaders === null ? (
<div className="text-muted-foreground p-4">{t("details.noHeaders")}</div>
) : (
<CodeDisplay
content={formattedResponseHeaders}
language="text"
fileName="response.headers"
maxHeight="600px"
/>
)}
</TabsContent>

<TabsContent value="responseBody" data-testid="session-tab-response-body">
{response === null ? (
<div className="text-muted-foreground p-4">{t("details.noData")}</div>
) : (
<CodeDisplay
content={response}
language={responseLanguage}
fileName={responseLanguage === "sse" ? "response.sse" : "response.json"}
maxHeight="600px"
/>
)}
</TabsContent>
</Tabs>
);
}
Loading
Loading