Skip to content

fix: 补全 Session 详情请求响应记录#495

Merged
ding113 merged 1 commit into
devfrom
fix/session-details-full-payload
Jan 1, 2026
Merged

fix: 补全 Session 详情请求响应记录#495
ding113 merged 1 commit into
devfrom
fix/session-details-full-payload

Conversation

@ding113

@ding113 ding113 commented Jan 1, 2026

Copy link
Copy Markdown
Owner

背景

Session 详情页此前仅展示 request 的 messages/input 子集,导致根级字段(如 model/instructions)丢失;同时 request/response headers 缺少端点与响应状态码,排查链路不完整。

相关 PR:

变更

  • Redis 临时记录(5 分钟 TTL):新增完整 requestBody 与请求/响应元信息(端点、方法、statusCode)。
  • Session 详情页:
    • 请求体展示完整 JSON(不再只是 messages)。
    • 增加 "请求 Messages" Tab 保留原有 messages 视图。
    • 请求头/响应头顶部增加 preamble(CLIENT/UPSTREAM 端点与响应状态码)。

影响范围

  • 仅影响 Session 详情调试数据记录与展示;不改变代理转发逻辑与业务语义。
  • Header 仍沿用既有脱敏策略(authorization/x-api-key 等遮罩)。

核心文件变更

Session 管理 (3 files)

  • src/lib/session-manager.ts - 新增 requestBody 及元信息存储/获取方法(+240/-1)
  • src/app/v1/_lib/proxy/session-guard.ts - 在格式转换前存储完整请求体(+31/-8)
  • src/app/v1/_lib/proxy/forwarder.ts - 记录上游请求/响应元信息(+20/-0)

UI 展示 (3 files)

  • src/actions/active-sessions.ts - 扩展 getSessionDetails 返回完整数据(+33/-2)
  • session-details-tabs.tsx - 新增 requestBody Tab 与 preamble 显示(+92/-8)
  • session-messages-client.tsx - 状态管理与数据传递(+23/-0)

测试 (1 file)

  • session-messages-client.test.tsx - 验证新增 Tab 与元信息渲染(+38/-0)

验证

自动化检查

  • bun run lint
  • bun run typecheck
  • bun run test
  • bun run build

手动测试步骤

  1. 发送包含 modelinstructions 的请求
  2. 进入 Session 详情页
  3. 验证 "请求体" Tab 显示完整 JSON(包含 model/instructions)
  4. 验证 "请求 Messages" Tab 保留原有 messages 视图
  5. 验证请求头/响应头顶部显示端点与状态码

备注

bun run test:coverage 当前因仓库全局覆盖率阈值不满足而失败(非本 PR 引入的问题)。


Description enhanced by Claude AI

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @ding113, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

此拉取请求旨在通过捕获和展示更全面的请求和响应数据,显著提升会话详情页的调试能力。它解决了之前数据不完整的问题,例如缺少完整的请求体和关键的请求/响应元数据。通过在Redis中临时存储这些额外信息,并在前端UI中以结构化和易读的方式呈现,开发者现在可以获得更完整的上下文来进行故障排除,而不会影响代理转发的核心逻辑。

Highlights

  • 增强会话详情页数据: 此前会话详情页仅展示请求消息的子集,导致根级字段(如 model/instructions)丢失。此PR通过记录完整的请求体和请求/响应元信息(端点、方法、状态码),极大地丰富了会话详情页的调试数据。
  • Redis临时记录: 新增了对完整请求体、客户端请求元信息、上游请求元信息和上游响应元信息的Redis临时记录,所有记录均设置5分钟的TTL(Time To Live),以确保数据的新鲜度和存储效率。
  • 会话详情页UI改进: 会话详情页现在能展示完整的请求JSON体,并新增了一个“请求 Messages”Tab以保留原有的消息视图。同时,请求头和响应头顶部增加了前导信息(preamble),显示CLIENT/UPSTREAM端点和响应状态码,使排查链路更加完整。
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions

github-actions Bot commented Jan 1, 2026

Copy link
Copy Markdown
Contributor

🧪 测试结果

测试类型 状态
代码质量
单元测试
集成测试
API 测试

总体结果: ✅ 所有测试通过

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

你好,感谢你为完善 Session 详情的调试信息所做出的贡献。这次变更增加了完整的请求体、端点和状态码记录,对于问题排查非常有帮助。整体代码实现清晰,新功能也添加了对应的测试用例,做得很好。我主要在代码的可维护性方面提出了一些建议,包括简化重复逻辑和重构相似的函数,希望能帮助你进一步提升代码质量。请查看具体的评论。

Comment on lines +61 to +79
const requestHeadersPreamble = useMemo(() => {
const lines: string[] = [];
const method = requestMeta.method?.trim() || null;

if (requestMeta.upstreamUrl) {
lines.push(
method
? `UPSTREAM: ${method} ${requestMeta.upstreamUrl}`
: `UPSTREAM: ${requestMeta.upstreamUrl}`
);
}
if (requestMeta.clientUrl) {
lines.push(
method ? `CLIENT: ${method} ${requestMeta.clientUrl}` : `CLIENT: ${requestMeta.clientUrl}`
);
}

return lines;
}, [requestMeta.clientUrl, requestMeta.method, requestMeta.upstreamUrl]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

这部分用于构建请求头前缀的逻辑存在重复代码,可以进行简化,提高代码的可读性和可维护性。

  const requestHeadersPreamble = useMemo(() => {
    const lines: string[] = [];
    const method = requestMeta.method?.trim();
    const methodPrefix = method ? `${method} ` : "";

    if (requestMeta.upstreamUrl) {
      lines.push(`UPSTREAM: ${methodPrefix}${requestMeta.upstreamUrl}`);
    }
    if (requestMeta.clientUrl) {
      lines.push(`CLIENT: ${methodPrefix}${requestMeta.clientUrl}`);
    }

    return lines;
  }, [requestMeta.clientUrl, requestMeta.method, requestMeta.upstreamUrl]);

Comment on lines +81 to +97
const responseHeadersPreamble = useMemo(() => {
const lines: string[] = [];

if (responseMeta.statusCode !== null && responseMeta.upstreamUrl) {
lines.push(`UPSTREAM: HTTP ${responseMeta.statusCode} ${responseMeta.upstreamUrl}`);
return lines;
}
if (responseMeta.statusCode !== null) {
lines.push(`UPSTREAM: HTTP ${responseMeta.statusCode}`);
return lines;
}
if (responseMeta.upstreamUrl) {
lines.push(`UPSTREAM: ${responseMeta.upstreamUrl}`);
}

return lines;
}, [responseMeta.statusCode, responseMeta.upstreamUrl]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

这部分构建响应头前缀的逻辑可以写得更简洁。目前的多个 ifreturn 语句可以合并成一个更清晰的逻辑块,从而提高代码的可读性。

  const responseHeadersPreamble = useMemo(() => {
    const { statusCode, upstreamUrl } = responseMeta;
    if (statusCode === null && !upstreamUrl) {
      return [];
    }

    const parts = ["UPSTREAM:"];
    if (statusCode !== null) {
      parts.push(`HTTP ${statusCode}`);
    }
    if (upstreamUrl) {
      parts.push(upstreamUrl);
    }
    return [parts.join(" ")];
  }, [responseMeta.statusCode, responseMeta.upstreamUrl]);

Comment on lines 129 to 140
} else {
setRequestBody(null);
setRequestMeta({ clientUrl: null, upstreamUrl: null, method: null });
setResponseMeta({ upstreamUrl: null, statusCode: null });
setError(result.error || t("status.fetchFailed"));
}
} catch (err) {
if (cancelled) return;
setRequestBody(null);
setRequestMeta({ clientUrl: null, upstreamUrl: null, method: null });
setResponseMeta({ upstreamUrl: null, statusCode: null });
setError(err instanceof Error ? err.message : t("status.unknownError"));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

elsecatch 块中,重置状态的逻辑是重复的。建议将这部分逻辑提取到一个独立的函数中,例如 resetDetailsData。这不仅可以减少代码重复,还能确保在出错时所有相关状态都能被一致地重置,提高代码的可维护性。

Suggested change
} else {
setRequestBody(null);
setRequestMeta({ clientUrl: null, upstreamUrl: null, method: null });
setResponseMeta({ upstreamUrl: null, statusCode: null });
setError(result.error || t("status.fetchFailed"));
}
} catch (err) {
if (cancelled) return;
setRequestBody(null);
setRequestMeta({ clientUrl: null, upstreamUrl: null, method: null });
setResponseMeta({ upstreamUrl: null, statusCode: null });
setError(err instanceof Error ? err.message : t("status.unknownError"));
} else {
resetDetailsData();
setError(result.error || t("status.fetchFailed"));
}
} catch (err) {
if (cancelled) return;
resetDetailsData();
setError(err instanceof Error ? err.message : t("status.unknownError"));

Comment on lines +1109 to +1115
if (session.sessionId) {
void SessionManager.storeSessionUpstreamRequestMeta(
session.sessionId,
{ url: proxyUrl, method: session.method },
session.requestSequence
).catch((err) => logger.error("Failed to store upstream request meta:", err));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

这部分存储上游请求元信息的代码与 Gemini 处理分支中的逻辑(882-886行)完全重复。为了遵循 DRY (Don't Repeat Yourself) 原则,建议将这段逻辑移到 if/else 块之后,统一执行一次。这样可以减少代码冗余,使逻辑更清晰。

Comment on lines +89 to +98
// 可选:存储 messages(受环境变量控制,按请求序号独立存储)
if (messages !== undefined) {
void SessionManager.storeSessionMessages(
session.sessionId,
messages,
requestSequence
).catch((err) => {
logger.error("[ProxySessionGuard] Failed to store session messages:", err);
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

注意到这里存储 messages 的逻辑从之前的 executeWithRetry 改为了简单的 void...catch。这移除了失败重试机制。虽然存储调试信息不是关键路径,但移除重试可能会降低在网络不稳定时成功记录调试信息的概率。请确认这是否是预期行为。如果不是,建议保留重试逻辑。

Comment on lines +1359 to +1586
/**
* 存储 session 完整请求体(客户端原始请求体,临时存储,5分钟过期)
*
* 注意:此数据可能较大且包含用户输入,仅在 STORE_SESSION_MESSAGES=true 时启用。
*
* @param sessionId - Session ID
* @param requestBody - 请求体(完整 JSON)
* @param requestSequence - 可选,请求序号
*/
static async storeSessionRequestBody(
sessionId: string,
requestBody: unknown,
requestSequence?: number
): Promise<void> {
if (!SessionManager.STORE_MESSAGES) {
logger.trace("SessionManager: STORE_SESSION_MESSAGES is disabled, skipping request body");
return;
}

const redis = getRedisClient();
if (!redis || redis.status !== "ready") return;

try {
const sequence = normalizeRequestSequence(requestSequence) ?? 1;
const key = `session:${sessionId}:req:${sequence}:requestBody`;
const payload = JSON.stringify(requestBody);
await redis.setex(key, SessionManager.SESSION_TTL, payload);
logger.trace("SessionManager: Stored session request body", {
sessionId,
requestSequence: sequence,
key,
size: payload.length,
});
} catch (error) {
logger.error("SessionManager: Failed to store session request body", { error, sessionId });
}
}

/**
* 获取 session 完整请求体(客户端原始请求体)
*
* @param sessionId - Session ID
* @param requestSequence - 请求序号
* @returns 解析后的 JSON 对象
*/
static async getSessionRequestBody(
sessionId: string,
requestSequence?: number
): Promise<unknown | null> {
if (!SessionManager.STORE_MESSAGES) {
logger.warn("SessionManager: STORE_SESSION_MESSAGES is disabled");
return null;
}

const redis = getRedisClient();
if (!redis || redis.status !== "ready") return null;

try {
const sequence = normalizeRequestSequence(requestSequence);
if (!sequence) return null;
const key = `session:${sessionId}:req:${sequence}:requestBody`;
const value = await redis.get(key);
if (!value) return null;
return JSON.parse(value) as unknown;
} catch (error) {
logger.error("SessionManager: Failed to get session request body", { error, sessionId });
return null;
}
}

/**
* 存储客户端请求元信息(端点/方法,临时存储,5分钟过期)
*
* @param sessionId - Session ID
* @param meta - 元信息
* @param requestSequence - 请求序号
*/
static async storeSessionClientRequestMeta(
sessionId: string,
meta: { url: string | URL; method: string },
requestSequence?: number
): Promise<void> {
const redis = getRedisClient();
if (!redis || redis.status !== "ready") return;

try {
const sequence = normalizeRequestSequence(requestSequence) ?? 1;
const key = `session:${sessionId}:req:${sequence}:clientReqMeta`;
const payload: SessionRequestMeta = {
url: sanitizeUrl(meta.url),
method: meta.method,
};
await redis.setex(key, SessionManager.SESSION_TTL, JSON.stringify(payload));
} catch (error) {
logger.error("SessionManager: Failed to store client request meta", { error, sessionId });
}
}

static async getSessionClientRequestMeta(
sessionId: string,
requestSequence?: number
): Promise<SessionRequestMeta | null> {
const redis = getRedisClient();
if (!redis || redis.status !== "ready") return null;

try {
const sequence = normalizeRequestSequence(requestSequence);
if (!sequence) return null;
const key = `session:${sessionId}:req:${sequence}:clientReqMeta`;
const value = await redis.get(key);
if (!value) return null;

const parsed: unknown = JSON.parse(value);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
const obj = parsed as Record<string, unknown>;
if (typeof obj.url !== "string" || typeof obj.method !== "string") return null;
return { url: obj.url, method: obj.method };
} catch (error) {
logger.error("SessionManager: Failed to get client request meta", { error, sessionId });
return null;
}
}

/**
* 存储上游请求元信息(端点/方法,临时存储,5分钟过期)
*
* @param sessionId - Session ID
* @param meta - 元信息
* @param requestSequence - 请求序号
*/
static async storeSessionUpstreamRequestMeta(
sessionId: string,
meta: { url: string | URL; method: string },
requestSequence?: number
): Promise<void> {
const redis = getRedisClient();
if (!redis || redis.status !== "ready") return;

try {
const sequence = normalizeRequestSequence(requestSequence) ?? 1;
const key = `session:${sessionId}:req:${sequence}:upstreamReqMeta`;
const payload: SessionRequestMeta = {
url: sanitizeUrl(meta.url),
method: meta.method,
};
await redis.setex(key, SessionManager.SESSION_TTL, JSON.stringify(payload));
} catch (error) {
logger.error("SessionManager: Failed to store upstream request meta", { error, sessionId });
}
}

static async getSessionUpstreamRequestMeta(
sessionId: string,
requestSequence?: number
): Promise<SessionRequestMeta | null> {
const redis = getRedisClient();
if (!redis || redis.status !== "ready") return null;

try {
const sequence = normalizeRequestSequence(requestSequence);
if (!sequence) return null;
const key = `session:${sessionId}:req:${sequence}:upstreamReqMeta`;
const value = await redis.get(key);
if (!value) return null;

const parsed: unknown = JSON.parse(value);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
const obj = parsed as Record<string, unknown>;
if (typeof obj.url !== "string" || typeof obj.method !== "string") return null;
return { url: obj.url, method: obj.method };
} catch (error) {
logger.error("SessionManager: Failed to get upstream request meta", { error, sessionId });
return null;
}
}

/**
* 存储上游响应元信息(端点/状态码,临时存储,5分钟过期)
*
* @param sessionId - Session ID
* @param meta - 元信息
* @param requestSequence - 请求序号
*/
static async storeSessionUpstreamResponseMeta(
sessionId: string,
meta: { url: string | URL; statusCode: number },
requestSequence?: number
): Promise<void> {
const redis = getRedisClient();
if (!redis || redis.status !== "ready") return;

try {
const sequence = normalizeRequestSequence(requestSequence) ?? 1;
const key = `session:${sessionId}:req:${sequence}:upstreamResMeta`;
const payload: SessionResponseMeta = {
url: sanitizeUrl(meta.url),
statusCode: meta.statusCode,
};
await redis.setex(key, SessionManager.SESSION_TTL, JSON.stringify(payload));
} catch (error) {
logger.error("SessionManager: Failed to store upstream response meta", { error, sessionId });
}
}

static async getSessionUpstreamResponseMeta(
sessionId: string,
requestSequence?: number
): Promise<SessionResponseMeta | null> {
const redis = getRedisClient();
if (!redis || redis.status !== "ready") return null;

try {
const sequence = normalizeRequestSequence(requestSequence);
if (!sequence) return null;
const key = `session:${sessionId}:req:${sequence}:upstreamResMeta`;
const value = await redis.get(key);
if (!value) return null;

const parsed: unknown = JSON.parse(value);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
const obj = parsed as Record<string, unknown>;
if (typeof obj.url !== "string" || typeof obj.statusCode !== "number") return null;
return { url: obj.url, statusCode: obj.statusCode };
} catch (error) {
logger.error("SessionManager: Failed to get upstream response meta", { error, sessionId });
return null;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

SessionManager 中新增的 store*get* 方法之间存在大量重复的模板代码(如获取 Redis 客户端、处理序列号、错误处理等)。为了提高代码的可维护性和减少冗余,建议将这些通用逻辑提取到私有的辅助函数中,例如 _storeSessionData_getSessionData。然后让公开的 store/get 方法调用这些辅助函数,只传入各自特定的参数(如 key 后缀和解析器)。

@github-actions github-actions Bot added the size/M Medium PR (< 500 lines) label Jan 1, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

No significant issues identified in this PR.

PR Size: M

  • Lines changed: 496
  • Files changed: 7

Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - Clean
  • Error handling - Clean
  • Type safety - Clean
  • Documentation accuracy - Clean
  • Test coverage - Adequate
  • Code clarity - Good

Automated review by Codex AI

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Reviewed PR #495 (fix: 补全 Session 详情请求响应记录) and pulled full metadata/diff.
  • Calculated PR size as M (477 additions + 19 deletions = 496 lines, 7 files) and applied the size/M label.
  • Completed the 6-perspective review strictly on diff lines; no findings cleared the ≥80 confidence threshold, so no inline comments were posted.
  • Submitted the required summary review on the PR via gh pr review.

@ding113
ding113 merged commit e6a3b5c into dev Jan 1, 2026
22 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Claude Code Hub Roadmap Jan 1, 2026
@github-actions github-actions Bot mentioned this pull request Jan 1, 2026
10 tasks
@ding113
ding113 deleted the fix/session-details-full-payload branch January 27, 2026 09:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core area:session area:UI bug Something isn't working size/M Medium PR (< 500 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant