From 21222cf9f4e7ce0680617a806949e69c38942936 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Sun, 26 Apr 2026 20:15:13 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(redis):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E6=A8=A1=E5=BC=8F=20JSON=20=E5=A4=A7?= =?UTF-8?q?=E6=95=B4=E6=95=B0=E7=B2=BE=E5=BA=A6=E4=B8=A2=E5=A4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 保留 Redis JSON 值中的大整数原始字面量 - 避免自动格式化时通过 JSON.stringify 改写超出安全范围的数字 - 补充自动模式大整数与字符串转义展示回归测试 Refs #400 --- frontend/src/utils/redisValueDisplay.test.ts | 20 +++ frontend/src/utils/redisValueDisplay.ts | 134 ++++++++++++++++++- 2 files changed, 148 insertions(+), 6 deletions(-) diff --git a/frontend/src/utils/redisValueDisplay.test.ts b/frontend/src/utils/redisValueDisplay.test.ts index 499c31e..8b66b6e 100644 --- a/frontend/src/utils/redisValueDisplay.test.ts +++ b/frontend/src/utils/redisValueDisplay.test.ts @@ -20,6 +20,26 @@ describe('redisValueDisplay', () => { }); }); + it('preserves large integer literals when formatting json in auto mode', () => { + const value = '{"subSessionIds":["java.util.ArrayList",[1494694751571226624]],"currentSubSessionId":1494694751571226624}'; + const formatted = formatRedisStringValue(value); + + expect(formatted).toMatchObject({ + isBinary: false, + isJson: true, + encoding: 'UTF-8', + }); + expect(formatted.displayValue).toContain('1494694751571226624'); + expect(formatted.displayValue).not.toContain('1494694751571226600'); + }); + + it('keeps json string escape rendering consistent in auto mode', () => { + const formatted = formatRedisStringValue('{"name":"\\u4e2d\\u6587","id":1494694751571226624}'); + + expect(formatted.displayValue).toContain('"name": "中文"'); + expect(formatted.displayValue).toContain('"id": 1494694751571226624'); + }); + it('falls back to hex for obvious binary values', () => { expect(formatRedisStringValue('\u0000\u0001\u0002abc')).toMatchObject({ isBinary: true, diff --git a/frontend/src/utils/redisValueDisplay.ts b/frontend/src/utils/redisValueDisplay.ts index fdf7a77..4ae5649 100644 --- a/frontend/src/utils/redisValueDisplay.ts +++ b/frontend/src/utils/redisValueDisplay.ts @@ -88,13 +88,135 @@ const tryDecodeValue = (value: string): { displayValue: string; encoding: string return { displayValue: toHexDisplay(value), encoding: 'HEX', needsHex: true }; }; -const tryFormatJson = (value: string): { isJson: boolean; formatted: string } => { - try { - const parsed = JSON.parse(value); - return { isJson: true, formatted: JSON.stringify(parsed, null, 2) }; - } catch { - return { isJson: false, formatted: value }; +const findNextNonWhitespace = (value: string, startIndex: number): string => { + for (let i = startIndex; i < value.length; i++) { + if (!/\s/.test(value[i])) { + return value[i]; + } } + return ''; +}; + +const readJsonStringToken = (value: string, startIndex: number): { token: string; nextIndex: number } => { + let index = startIndex + 1; + let escaped = false; + while (index < value.length) { + const char = value[index]; + if (escaped) { + escaped = false; + index++; + continue; + } + if (char === '\\') { + escaped = true; + index++; + continue; + } + if (char === '"') { + return { token: value.slice(startIndex, index + 1), nextIndex: index + 1 }; + } + index++; + } + return { token: value.slice(startIndex), nextIndex: value.length }; +}; + +const readJsonPrimitiveToken = (value: string, startIndex: number): { token: string; nextIndex: number } => { + let index = startIndex; + while (index < value.length && !/[\s,\]}]/.test(value[index])) { + index++; + } + return { token: value.slice(startIndex, index), nextIndex: index }; +}; + +const formatJsonStringToken = (token: string): string => { + try { + return JSON.stringify(JSON.parse(token)); + } catch { + return token; + } +}; + +const formatJsonPreservingNumberLiterals = (value: string): string | null => { + try { + JSON.parse(value); + } catch { + return null; + } + + const indentUnit = ' '; + const indent = (depth: number) => indentUnit.repeat(Math.max(0, depth)); + let result = ''; + let depth = 0; + let index = 0; + let lastToken: 'open' | 'value' | 'close' | 'comma' | 'colon' | '' = ''; + + while (index < value.length) { + const char = value[index]; + if (/\s/.test(char)) { + index++; + continue; + } + + if (char === '"') { + const { token, nextIndex } = readJsonStringToken(value, index); + result += formatJsonStringToken(token); + lastToken = 'value'; + index = nextIndex; + continue; + } + + if (char === '{' || char === '[') { + const closeChar = char === '{' ? '}' : ']'; + result += char; + depth++; + lastToken = 'open'; + if (findNextNonWhitespace(value, index + 1) !== closeChar) { + result += `\n${indent(depth)}`; + } + index++; + continue; + } + + if (char === '}' || char === ']') { + depth--; + if (lastToken !== 'open') { + result += `\n${indent(depth)}`; + } + result += char; + lastToken = 'close'; + index++; + continue; + } + + if (char === ',') { + result += `,\n${indent(depth)}`; + lastToken = 'comma'; + index++; + continue; + } + + if (char === ':') { + result += ': '; + lastToken = 'colon'; + index++; + continue; + } + + const { token, nextIndex } = readJsonPrimitiveToken(value, index); + result += token; + lastToken = 'value'; + index = nextIndex; + } + + return result; +}; + +const tryFormatJson = (value: string): { isJson: boolean; formatted: string } => { + const formatted = formatJsonPreservingNumberLiterals(value); + if (formatted !== null) { + return { isJson: true, formatted }; + } + return { isJson: false, formatted: value }; }; export const toHexDisplay = (value: string): string => {