fix(server): 使用 lossless-json 无损处理 summary.json Long 精度

This commit is contained in:
2026-03-10 16:10:25 +08:00
parent 42bc8605b4
commit 9a2bd5c43a
4 changed files with 31 additions and 4 deletions

View File

@@ -37,6 +37,7 @@
"@tanstack/react-start": "catalog:", "@tanstack/react-start": "catalog:",
"drizzle-orm": "catalog:", "drizzle-orm": "catalog:",
"jszip": "catalog:", "jszip": "catalog:",
"lossless-json": "catalog:",
"react": "catalog:", "react": "catalog:",
"react-dom": "catalog:", "react-dom": "catalog:",
"systeminformation": "catalog:", "systeminformation": "catalog:",

View File

@@ -10,12 +10,28 @@ import {
} from '@furtherverse/crypto' } from '@furtherverse/crypto'
import { ORPCError } from '@orpc/server' import { ORPCError } from '@orpc/server'
import JSZip from 'jszip' import JSZip from 'jszip'
import {
isInteger,
isSafeNumber,
LosslessNumber,
parse as losslessParse,
stringify as losslessStringify,
} from 'lossless-json'
import { z } from 'zod' import { z } from 'zod'
import { extractSafeZipFiles, ZipValidationError } from '@/server/safe-zip' import { extractSafeZipFiles, ZipValidationError } from '@/server/safe-zip'
import { getUxConfig } from '@/server/ux-config' import { getUxConfig } from '@/server/ux-config'
import { db } from '../middlewares' import { db } from '../middlewares'
import { os } from '../server' import { os } from '../server'
const safeNumberParser = (value: string): number | string => {
if (isSafeNumber(value)) return Number(value)
if (isInteger(value)) return value
return Number(value)
}
const toLosslessNumber = (value: string): LosslessNumber | string =>
value !== '' && /^-?\d+$/.test(value) ? new LosslessNumber(value) : value
const summaryPayloadSchema = z const summaryPayloadSchema = z
.object({ .object({
taskId: z.string().min(1, 'summary.json must contain a non-empty taskId'), taskId: z.string().min(1, 'summary.json must contain a non-empty taskId'),
@@ -94,7 +110,7 @@ export const signAndPackReport = os.crypto.signAndPackReport.use(db).handler(asy
let rawJson: unknown let rawJson: unknown
try { try {
rawJson = JSON.parse(Buffer.from(summaryFile.bytes).toString('utf-8')) rawJson = losslessParse(Buffer.from(summaryFile.bytes).toString('utf-8'), undefined, safeNumberParser)
} catch { } catch {
throw new ORPCError('BAD_REQUEST', { throw new ORPCError('BAD_REQUEST', {
message: 'summary.json in the ZIP is not valid JSON', message: 'summary.json in the ZIP is not valid JSON',
@@ -138,15 +154,21 @@ export const signAndPackReport = os.crypto.signAndPackReport.use(db).handler(asy
// Build final summary.json with flat structure (matching Kotlin reference) // Build final summary.json with flat structure (matching Kotlin reference)
const finalSummary = { const finalSummary = {
orgId, orgId: toLosslessNumber(String(orgId)),
checkId, checkId: toLosslessNumber(checkId),
taskId: summaryPayload.taskId, taskId: summaryPayload.taskId,
licence: config.licence, licence: config.licence,
fingerprint: config.fingerprint, fingerprint: config.fingerprint,
deviceSignature, deviceSignature,
summary: summaryPayload.summary ?? '', summary: summaryPayload.summary ?? '',
} }
const summaryBytes = Buffer.from(JSON.stringify(finalSummary), 'utf-8') const summaryJson = losslessStringify(finalSummary)
if (!summaryJson) {
throw new ORPCError('INTERNAL_SERVER_ERROR', {
message: 'Failed to serialize summary.json',
})
}
const summaryBytes = Buffer.from(summaryJson, 'utf-8')
// Build manifest.json (fixed file list, matching Kotlin reference) // Build manifest.json (fixed file list, matching Kotlin reference)
const manifestFiles: Record<string, string> = { const manifestFiles: Record<string, string> = {

View File

@@ -49,6 +49,7 @@
"@tanstack/react-start": "catalog:", "@tanstack/react-start": "catalog:",
"drizzle-orm": "catalog:", "drizzle-orm": "catalog:",
"jszip": "catalog:", "jszip": "catalog:",
"lossless-json": "^4.3.0",
"react": "catalog:", "react": "catalog:",
"react-dom": "catalog:", "react-dom": "catalog:",
"systeminformation": "catalog:", "systeminformation": "catalog:",
@@ -1078,6 +1079,8 @@
"log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], "log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="],
"lossless-json": ["lossless-json@4.3.0", "", {}, "sha512-ToxOC+SsduRmdSuoLZLYAr5zy1Qu7l5XhmPWM3zefCZ5IcrzW/h108qbJUKfOlDlhvhjUK84+8PSVX0kxnit0g=="],
"lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="], "lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="],
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],

View File

@@ -53,6 +53,7 @@
"electron-builder": "^26.8.1", "electron-builder": "^26.8.1",
"electron-vite": "^5.0.0", "electron-vite": "^5.0.0",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"lossless-json": "^4.3.0",
"motion": "^12.35.0", "motion": "^12.35.0",
"nitro": "npm:nitro-nightly@3.0.1-20260227-181935-bfbb207c", "nitro": "npm:nitro-nightly@3.0.1-20260227-181935-bfbb207c",
"openpgp": "^6.0.1", "openpgp": "^6.0.1",