mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-15 11:45:52 +00:00
fix(kap-server): remove the 64 MiB web session export limit (#2910)
This commit is contained in:
parent
325913a532
commit
eb72aebeeb
8 changed files with 7 additions and 67 deletions
5
.changeset/export-log-limit.md
Normal file
5
.changeset/export-log-limit.md
Normal 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.
|
||||
|
|
@ -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.
|
||||
:::
|
||||
|
|
|
|||
|
|
@ -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 提示
|
||||
导出文件可能包含代码、命令输出和路径等敏感信息,分享前请先确认内容。
|
||||
:::
|
||||
|
|
|
|||
|
|
@ -58,7 +58,6 @@ export interface ExportSessionResult {
|
|||
export interface ExportSessionOptions {
|
||||
readonly webLog?: string;
|
||||
readonly signal?: AbortSignal;
|
||||
readonly maxArchiveBytes?: number;
|
||||
}
|
||||
|
||||
export interface ISessionExportService {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue