diff --git a/mindspace-assets.test.mjs b/mindspace-assets.test.mjs
index 325f1a2..02200b0 100644
--- a/mindspace-assets.test.mjs
+++ b/mindspace-assets.test.mjs
@@ -6,6 +6,7 @@ import test from 'node:test';
import sharp from 'sharp';
import { assetInternals, createAssetService, validateUploadRequest } from './mindspace-assets.mjs';
import { DEFAULT_MAX_FILE_BYTES } from './mindspace.mjs';
+import { DEFAULT_IMAGE_UPLOAD_MAX_BYTES } from './user-image-normalize.mjs';
function createMockPool(state) {
return {
@@ -240,7 +241,7 @@ test('validateUploadRequest rejects traversal filenames', () => {
);
});
-test('validateUploadRequest allows 5MB uploads by default', () => {
+test('validateUploadRequest allows 30MB document uploads by default', () => {
assert.doesNotThrow(() =>
validateUploadRequest({
filename: 'handbook.pdf',
@@ -261,14 +262,14 @@ test('validateUploadRequest keeps image uploads below the image-specific limit',
assert.doesNotThrow(() =>
validateUploadRequest({
filename: 'cover.jpg',
- sizeBytes: 4 * 1024 * 1024,
+ sizeBytes: DEFAULT_IMAGE_UPLOAD_MAX_BYTES,
}),
);
assert.throws(
() =>
validateUploadRequest({
filename: 'cover.jpg',
- sizeBytes: 4 * 1024 * 1024 + 1,
+ sizeBytes: DEFAULT_IMAGE_UPLOAD_MAX_BYTES + 1,
}),
(error) => error.code === 'file_too_large',
);
diff --git a/mindspace.mjs b/mindspace.mjs
index d3e3538..a70f42d 100644
--- a/mindspace.mjs
+++ b/mindspace.mjs
@@ -2,7 +2,7 @@ import crypto from 'node:crypto';
import { loadMindSpaceConfig } from './mindspace-config.mjs';
export const DEFAULT_SPACE_QUOTA_BYTES = 5 * 1024 * 1024;
-export const DEFAULT_MAX_FILE_BYTES = 5 * 1024 * 1024;
+export const DEFAULT_MAX_FILE_BYTES = 30 * 1024 * 1024;
export const SYSTEM_CATEGORIES = Object.freeze([
{
diff --git a/server.mjs b/server.mjs
index 629cd5a..7d4d261 100644
--- a/server.mjs
+++ b/server.mjs
@@ -148,6 +148,7 @@ import {
} from './mindspace-chat-docx-package.mjs';
import { scanContent } from './mindspace-content-scan.mjs';
import { renderImageAssetViewerHtml, wantsInlineImageViewer } from './mindspace-asset-preview.mjs';
+import { DEFAULT_IMAGE_UPLOAD_MAX_BYTES } from './user-image-normalize.mjs';
import { createRechargeService } from './billing-recharge.mjs';
import { createSubscriptionService, createPlanCatalogService, ensurePlanCatalogSchema, PLAN_CATALOG } from './billing-subscription.mjs';
import {
@@ -268,7 +269,7 @@ const jsonUnlessMultipart = (req, res, next) => {
};
const rawUploadBody = express.raw({
type: 'application/octet-stream',
- limit: mindSpaceServerRuntime.maxFileBytes,
+ limit: Math.max(mindSpaceServerRuntime.maxFileBytes, DEFAULT_IMAGE_UPLOAD_MAX_BYTES),
});
const wikiAuth = createWikiAuth(path.join(resolveMindSpacePublishRoot(__dirname), 'wiki-db'));
@@ -2188,6 +2189,28 @@ function mindSpaceError(res, req, error) {
return sendError(res, req, status, code, message, error?.details);
}
+function isRequestBodyTooLarge(error) {
+ return (
+ error?.type === 'entity.too.large' ||
+ error?.status === 413 ||
+ error?.statusCode === 413
+ );
+}
+
+function apiRequestBodyError(error, req, res, next) {
+ if (!isRequestBodyTooLarge(error)) return next(error);
+ const isMindSpaceUpload = req.path.startsWith('/mindspace/v1/uploads/');
+ return sendError(
+ res,
+ req,
+ 413,
+ isMindSpaceUpload ? 'file_too_large' : 'request_body_too_large',
+ isMindSpaceUpload
+ ? '上传文件超过单文件大小限制,请压缩后重试'
+ : '请求内容过大,请缩小后重试',
+ );
+}
+
function ensureMindSpaceEnabled(res, req, { upload = false, agent = false } = {}) {
try {
assertMindSpaceRoute(msFlags, upload ? 'upload' : agent ? 'agent' : undefined);
@@ -4573,6 +4596,8 @@ api.get('/sessions/:sessionId/events', async (req, res, next) => {
});
});
+api.use(apiRequestBodyError);
+
api.use(async (req, res, next) => {
await userAuthReady;
if (!userAuth || !tkmindProxy) return next();
diff --git a/src/api/client.ts b/src/api/client.ts
index 6216639..d415ce2 100644
--- a/src/api/client.ts
+++ b/src/api/client.ts
@@ -139,6 +139,70 @@ export class ApiError extends Error {
}
}
+function formatBytes(bytes: number) {
+ if (!Number.isFinite(bytes) || bytes < 0) return '0B';
+ if (bytes >= 1024 * 1024) {
+ const mb = bytes / 1024 / 1024;
+ return `${Number.isInteger(mb) ? mb : mb.toFixed(1)}MB`;
+ }
+ if (bytes >= 1024) {
+ const kb = bytes / 1024;
+ return `${Number.isInteger(kb) ? kb : kb.toFixed(1)}KB`;
+ }
+ return `${bytes}B`;
+}
+
+function readNumberDetail(details: ApiError['details'], key: string) {
+ if (!details || typeof details !== 'object') return null;
+ const value = (details as Record
支持 {UPLOAD_FILE_TYPE_LABEL},文档最大 {formatBytes(maxUploadFileBytes)},图片最大{' '} - {formatBytes(Math.min(maxUploadFileBytes, MINDSPACE_IMAGE_UPLOAD_MAX_BYTES))}。 + {formatBytes(MINDSPACE_IMAGE_UPLOAD_MAX_BYTES)}。