From ce5fbfb9a6d562e17779a1e226a269a0d4c8ff0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD?= <3401797899@qq.com> Date: Mon, 27 Jul 2026 16:14:50 +0800 Subject: [PATCH] =?UTF-8?q?fix(filelib):=20=E4=B8=8A=E4=BC=A0=E4=B8=8A?= =?UTF-8?q?=E9=99=90=E6=8A=AC=E5=88=B0=2050MiB,=E5=B9=B6=E6=8A=8A=E5=AE=83?= =?UTF-8?q?=E4=B8=8E=20body=20limit=20=E7=9A=84=E4=B8=B2=E8=81=94=E5=86=99?= =?UTF-8?q?=E6=B8=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 7.8MB 文件上传报 413:那是 Fastify 在 body 解析阶段拒的,不是 HUB_FILELIB_MAX_FILE_BYTES。上传把内容放在 JSON body 里、二进制过 base64 体积涨 4/3,所以有效上限是 min(该值, HUB_HTTP_BODY_LIMIT_BYTES × 3/4)。 原先 body limit 是 1MiB,10MiB 的文件上限根本不可达。 .env.example:body limit 1MiB → 70MiB,新增 HUB_FILELIB_MAX_FILE_BYTES=50MiB。 注意 body limit 同时是 ADR-0022 requestBodySize 维度的平台 ceiling,抬高它 对所有端点生效。 resolveMaxFileBytes 拆成 parseMaxFileBytes(纯解析)+ resolveMaxFileBytes(读 env):原先带默认参数,显式传 undefined 会回落到读 env,"没传值"与"读环境变量" 分不开,vitest 加载 .env 后测试会读到真实配置。 --- hub/.env.example | 8 +++++++- hub/src/database/filelib/fileService.ts | 22 +++++++++++++++++++--- hub/test/unit/filelib-file-limits.test.ts | 18 +++++++++--------- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/hub/.env.example b/hub/.env.example index e184460..d2d16a8 100644 --- a/hub/.env.example +++ b/hub/.env.example @@ -23,12 +23,18 @@ DATABASE_URL="postgresql://paradigm:paradigm@127.0.0.1:5432/paradigm" # HUB_AGENT_MAX_TURNS=25 HUB_AGENT_MAX_CONCURRENT_RUNS="1" HUB_AGENT_MAX_RUN_SECONDS="900" -HUB_HTTP_BODY_LIMIT_BYTES="1048576" +HUB_HTTP_BODY_LIMIT_BYTES="73400320" HUB_MAX_FILES_PER_MESSAGE="8" HUB_MAX_FILE_BYTES="26214400" HUB_HTTP_REQUESTS_PER_MINUTE="120" HUB_FEISHU_EVENTS_PER_MINUTE="120" +# 文件库单文件上限(缺省 10 MiB)。这两个值是串联的:上传把文件内容放在 +# JSON body 里,二进制过 base64 体积涨 4/3。所以有效上限是 +# min(本值, HUB_HTTP_BODY_LIMIT_BYTES × 3/4);body limit 太小时本值不可达, +# 且报错是 Fastify 的 413 Payload Too Large 而不是 file_too_large。 +HUB_FILELIB_MAX_FILE_BYTES="52428800" + # Persistent system-managed root for project workspaces. Production must use an # absolute path outside the deployment/release tree; install_service.sh defaults # to this path and rejects any overlap before installing the unit. diff --git a/hub/src/database/filelib/fileService.ts b/hub/src/database/filelib/fileService.ts index 02f7e4a..999192e 100644 --- a/hub/src/database/filelib/fileService.ts +++ b/hub/src/database/filelib/fileService.ts @@ -22,16 +22,32 @@ export const FILE_PATH_MAX_DEPTH = 32; export const FILE_CONTENT_MAX_BYTES_DEFAULT = 10 * 1024 * 1024; /** - * 单文件字节上限:`HUB_FILELIB_MAX_FILE_BYTES` 覆盖,缺省 10MiB。 - * 非法值(非正整数/NaN)按缺省处理 —— 配置写错不该让上传静默变成 0 上限。 + * 单文件字节上限的纯解析。非法值(非正整数/NaN)按缺省处理 —— + * 配置写错不该让上传静默变成 0 上限(那会把每次上传都拒掉)。 + * + * 与 `resolveMaxFileBytes` 分开是有意的:带默认参数的单函数版本里, + * 显式传 undefined 会触发默认值、回到读 env,于是“没传值”和“读环境变量” + * 永远分不开,测试也会被 vitest 加载的 .env 干扰。 */ -export function resolveMaxFileBytes(raw: string | undefined = process.env["HUB_FILELIB_MAX_FILE_BYTES"]): number { +export function parseMaxFileBytes(raw: string | undefined): number { if (raw === undefined || raw.trim() === "") return FILE_CONTENT_MAX_BYTES_DEFAULT; const parsed = Number(raw.trim()); if (!Number.isSafeInteger(parsed) || parsed <= 0) return FILE_CONTENT_MAX_BYTES_DEFAULT; return parsed; } +/** + * 生效上限:`HUB_FILELIB_MAX_FILE_BYTES` 覆盖,缺省 10MiB。 + * + * 注意它与 `HUB_HTTP_BODY_LIMIT_BYTES` 是串联的:上传把内容放在 JSON body 里, + * 二进制过 base64 体积涨 4/3,所以真正的天花板是 + * min(本值, bodyLimit × 3/4)。body limit 太小时本值不可达,而且报错发生在 + * Fastify 解析阶段(413 Payload Too Large),根本到不了下面的 checkSize。 + */ +export function resolveMaxFileBytes(): number { + return parseMaxFileBytes(process.env["HUB_FILELIB_MAX_FILE_BYTES"]); +} + const CONTROL_CHARS = /[\p{C}]/u; const FORBIDDEN_SEGMENTS = new Set(["", ".", "..", ".git"]); diff --git a/hub/test/unit/filelib-file-limits.test.ts b/hub/test/unit/filelib-file-limits.test.ts index c45490f..031afc4 100644 --- a/hub/test/unit/filelib-file-limits.test.ts +++ b/hub/test/unit/filelib-file-limits.test.ts @@ -9,26 +9,26 @@ import { describe, expect, it } from "vitest"; import { FILE_CONTENT_MAX_BYTES_DEFAULT, defaultCommitMessage, - resolveMaxFileBytes, + parseMaxFileBytes, } from "../../src/database/filelib/fileService.js"; -describe("resolveMaxFileBytes", () => { +describe("parseMaxFileBytes", () => { it("缺省 10MiB", () => { expect(FILE_CONTENT_MAX_BYTES_DEFAULT).toBe(10 * 1024 * 1024); - expect(resolveMaxFileBytes(undefined)).toBe(FILE_CONTENT_MAX_BYTES_DEFAULT); - expect(resolveMaxFileBytes("")).toBe(FILE_CONTENT_MAX_BYTES_DEFAULT); - expect(resolveMaxFileBytes(" ")).toBe(FILE_CONTENT_MAX_BYTES_DEFAULT); + expect(parseMaxFileBytes(undefined)).toBe(FILE_CONTENT_MAX_BYTES_DEFAULT); + expect(parseMaxFileBytes("")).toBe(FILE_CONTENT_MAX_BYTES_DEFAULT); + expect(parseMaxFileBytes(" ")).toBe(FILE_CONTENT_MAX_BYTES_DEFAULT); }); it("合法正整数生效(可上调也可下调)", () => { - expect(resolveMaxFileBytes("1048576")).toBe(1024 * 1024); - expect(resolveMaxFileBytes(" 52428800 ")).toBe(50 * 1024 * 1024); - expect(resolveMaxFileBytes("1")).toBe(1); + expect(parseMaxFileBytes("1048576")).toBe(1024 * 1024); + expect(parseMaxFileBytes(" 52428800 ")).toBe(50 * 1024 * 1024); + expect(parseMaxFileBytes("1")).toBe(1); }); it("非法值一律回退缺省,不产生 0 或负数上限", () => { for (const bad of ["0", "-1", "abc", "1.5", "NaN", "Infinity", "1e999", "10MB"]) { - expect(resolveMaxFileBytes(bad)).toBe(FILE_CONTENT_MAX_BYTES_DEFAULT); + expect(parseMaxFileBytes(bad)).toBe(FILE_CONTENT_MAX_BYTES_DEFAULT); } }); });