fix(kap-server): remove the 64 MiB web session export limit (#2910)

This commit is contained in:
Haozhe 2026-08-14 11:48:56 +08:00 committed by GitHub
parent 325913a532
commit eb72aebeeb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 7 additions and 67 deletions

View file

@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---
Remove the 64 MiB limit on web session exports, so large sessions no longer fail with a file-too-large error when downloaded from the web UI.

View file

@ -110,8 +110,6 @@ You can also export from inside the TUI without leaving the interactive session:
In the web UI, `/export` downloads the current session as a diagnostic ZIP. It includes the persisted session data, diagnostic logs, and a bounded metadata-only `logs/kimi-web.jsonl` record of key browser events. Prompt text, WebSocket payloads, and console arguments are not copied into this browser log. This web command differs from the TUI `/export` alias above.
The browser buffers the ZIP before saving it, so web exports are limited to 64 MiB. For a larger session, use `kimi export <sessionId>` or the TUI `/export-debug-zip` command.
::: tip
Exported files may contain code, command output, and file paths that are sensitive. Review the content before sharing.
:::

View file

@ -110,8 +110,6 @@ kimi export <sessionId> -o ~/Desktop/my-session.zip
在 web UI 中,`/export` 会把当前会话下载为诊断 ZIP。压缩包包含持久化的会话数据、诊断日志以及记录浏览器关键事件且大小有上限、只含元数据的 `logs/kimi-web.jsonl`提示词正文、WebSocket 内容和 console 参数不会写入这份浏览器日志。这里的 web 命令与上面的 TUI `/export` 别名行为不同。
浏览器需要先把 ZIP 缓存在内存中再保存,因此 web 导出上限为 64 MiB。更大的会话请使用 `kimi export <sessionId>` 或 TUI 的 `/export-debug-zip`
::: tip 提示
导出文件可能包含代码、命令输出和路径等敏感信息,分享前请先确认内容。
:::

View file

@ -58,7 +58,6 @@ export interface ExportSessionResult {
export interface ExportSessionOptions {
readonly webLog?: string;
readonly signal?: AbortSignal;
readonly maxArchiveBytes?: number;
}
export interface ISessionExportService {

View file

@ -99,7 +99,6 @@ export class SessionExportService implements ISessionExportService {
: undefined,
webLog: options.webLog,
signal: options.signal,
maxArchiveBytes: options.maxArchiveBytes,
});
}
@ -185,7 +184,6 @@ export async function exportSessionDirectory(input: {
readonly desktopLogPath?: string | undefined;
readonly webLog?: string;
readonly signal?: AbortSignal;
readonly maxArchiveBytes?: number;
}): Promise<ExportSessionResult> {
input.signal?.throwIfAborted();
const sessionDir = input.summary.sessionDir;
@ -265,7 +263,6 @@ export async function exportSessionDirectory(input: {
sessionFiles: selectedSessionFiles,
extraEntries: extras,
signal: input.signal,
maxArchiveBytes: input.maxArchiveBytes,
});
sessionLogSourceTransferred = sessionLogSource !== undefined;
globalSourceTransferred = globalSource !== undefined;

View file

@ -8,7 +8,7 @@
import { createWriteStream } from 'node:fs';
import { mkdir, mkdtemp, readdir, rename, rm, stat } from 'node:fs/promises';
import { Readable, Transform } from 'node:stream';
import { Readable } from 'node:stream';
import { pipeline } from 'node:stream/promises';
import { dirname, join, relative, resolve } from 'pathe';
@ -49,7 +49,6 @@ export async function writeExportZip(args: {
readonly sessionFiles: readonly SessionZipEntry[];
readonly extraEntries?: readonly ExtraZipEntry[];
readonly signal?: AbortSignal;
readonly maxArchiveBytes?: number;
}): Promise<readonly string[]> {
const unusedSources = new Set<ZipSource>([
...args.sessionFiles.flatMap((entry) => (typeof entry === 'string' ? [] : [entry.source])),
@ -110,12 +109,7 @@ export async function writeExportZip(args: {
args.signal?.addEventListener('abort', onAbort, { once: true });
const destination = createWriteStream(tempOutputPath, { flags: 'wx' });
writing =
args.maxArchiveBytes === undefined
? pipeline(output, destination, { signal: args.signal })
: pipeline(output, createArchiveLimit(args.maxArchiveBytes), destination, {
signal: args.signal,
});
writing = pipeline(output, destination, { signal: args.signal });
const activate = (source: ZipSource): Readable => {
unusedSources.delete(source);
@ -263,26 +257,6 @@ function abortReason(signal: AbortSignal): Error {
: new DOMException('The operation was aborted.', 'AbortError');
}
function createArchiveLimit(maxArchiveBytes: number): Transform {
let archiveBytes = 0;
return new Transform({
transform(chunk: Buffer, _encoding, callback) {
archiveBytes += chunk.length;
if (archiveBytes > maxArchiveBytes) {
callback(
new Error2(
ErrorCodes.SESSION_EXPORT_TOO_LARGE,
`Session export exceeds the ${maxArchiveBytes} byte archive limit.`,
{ details: { archiveBytes, maxArchiveBytes } },
),
);
return;
}
callback(null, chunk);
},
});
}
async function findConflictingSource(args: {
readonly outputPath: string;
readonly sessionFiles: readonly SessionZipEntry[];

View file

@ -710,23 +710,6 @@ describe('sessionExport', () => {
expect((await readdir(tmp)).toSorted()).toEqual(['export.zip', 'safe-output', 'state.json']);
});
it('rejects with a coded error when compressed output exceeds the configured limit', async () => {
const tmp = await mkdtemp(join(tmpdir(), 'session-export-test-'));
await expect(
writeExportZip({
outputPath: join(tmp, 'too-large.zip'),
manifest: testManifest('ses_too_large'),
sessionDir: tmp,
sessionFiles: [],
maxArchiveBytes: 1,
}),
).rejects.toMatchObject({
code: 'session.export_too_large',
details: { maxArchiveBytes: 1 },
});
});
it('throws a coded error when the session is unknown', async () => {
const tmp = await mkdtemp(join(tmpdir(), 'session-export-test-'));
ix = createTestServices(tmp, {

View file

@ -29,8 +29,6 @@ import {
exportSessionRequestSchema,
} from '../protocol/rest-session';
const MAX_WEB_SESSION_EXPORT_BYTES = 64 * 1024 * 1024;
interface SessionExportRouteHost {
post(
path: string,
@ -64,7 +62,6 @@ export function registerSessionExportRoute(
errors: {
[ErrorCode.VALIDATION_FAILED]: {},
[ErrorCode.SESSION_NOT_FOUND]: {},
[ErrorCode.FILE_TOO_LARGE]: {},
[ErrorCode.INTERNAL_ERROR]: {},
},
description: 'Export a session and diagnostic logs as a zip archive',
@ -132,7 +129,6 @@ export function registerSessionExportRoute(
{
webLog: req.body.web_log,
signal: exportAbort.signal,
maxArchiveBytes: MAX_WEB_SESSION_EXPORT_BYTES,
},
);
if (aborted) {
@ -200,16 +196,6 @@ function sendMappedError(reply: SessionExportReply, req: { id: string }, error:
reply.send(errEnvelope(ErrorCode.SESSION_NOT_FOUND, error.message, requestId));
return;
}
if (error.code === ErrorCodes.SESSION_EXPORT_TOO_LARGE) {
reply.send(
errEnvelope(
ErrorCode.FILE_TOO_LARGE,
'session export exceeds the 64 MiB web limit',
requestId,
),
);
return;
}
}
requestLog(req)?.error({ err: error }, 'session export failed');
reply.send(