fix: 补全 Session 详情请求响应记录#495
Conversation
Summary of ChangesHello @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
Using Gemini Code AssistThe 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
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 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
|
🧪 测试结果
总体结果: ✅ 所有测试通过 |
| 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]); |
There was a problem hiding this comment.
这部分用于构建请求头前缀的逻辑存在重复代码,可以进行简化,提高代码的可读性和可维护性。
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]);
| 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]); |
There was a problem hiding this comment.
这部分构建响应头前缀的逻辑可以写得更简洁。目前的多个 if 和 return 语句可以合并成一个更清晰的逻辑块,从而提高代码的可读性。
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]);
| } 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")); |
There was a problem hiding this comment.
在 else 和 catch 块中,重置状态的逻辑是重复的。建议将这部分逻辑提取到一个独立的函数中,例如 resetDetailsData。这不仅可以减少代码重复,还能确保在出错时所有相关状态都能被一致地重置,提高代码的可维护性。
| } 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")); |
| 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)); | ||
| } |
| // 可选:存储 messages(受环境变量控制,按请求序号独立存储) | ||
| if (messages !== undefined) { | ||
| void SessionManager.storeSessionMessages( | ||
| session.sessionId, | ||
| messages, | ||
| requestSequence | ||
| ).catch((err) => { | ||
| logger.error("[ProxySessionGuard] Failed to store session messages:", err); | ||
| }); | ||
| } |
| /** | ||
| * 存储 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; | ||
| } | ||
| } |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
- 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 thesize/Mlabel. - 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.
背景
Session 详情页此前仅展示 request 的 messages/input 子集,导致根级字段(如 model/instructions)丢失;同时 request/response headers 缺少端点与响应状态码,排查链路不完整。
相关 PR:
变更
影响范围
核心文件变更
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)验证
自动化检查
手动测试步骤
model和instructions的请求备注
bun run test:coverage 当前因仓库全局覆盖率阈值不满足而失败(非本 PR 引入的问题)。
Description enhanced by Claude AI