✨ feat(vscode): 移交时显示步骤,会话包超限先停

This commit is contained in:
cruldra committed 2026-09-27 18:36:01 +08:00
1 parent 9220bad599
commit 162134dcc0
6 files changed
+412 -32

No files matched your search

+97 -6
View File
@@ -1,8 +1,37 @@
import { execFile } from 'node:child_process'
import { promises as fsp } from 'node:fs'
import { execFile, spawn } from 'node:child_process'
import { promises as fsp, statSync } from 'node:fs'
import * as path from 'node:path'
function tar(args: string[]): Promise<void> {
/** Gitea 工单附件上限。压缩后的 tgz 超过这个值,服务器会拒收。 */
export const HANDOFF_ATTACHMENT_MAX_BYTES = 100 * 1024 * 1024
export function formatByteSize(bytes: number): string {
const mb = bytes / (1024 * 1024)
if (mb >= 10)
return `${Math.round(mb)}MB`
if (mb >= 1)
return `${mb.toFixed(1)}MB`
const kb = bytes / 1024
if (kb >= 10)
return `${Math.round(kb)}KB`
if (kb >= 1)
return `${kb.toFixed(1)}KB`
return `${bytes}B`
}
export class HandoffArchiveTooLargeError extends Error {
readonly packedBytes: number
readonly limitBytes: number
constructor(packedBytes: number, limitBytes: number) {
super(`压缩后超过 Gitea 附件上限 ${formatByteSize(limitBytes)}`)
this.name = 'HandoffArchiveTooLargeError'
this.packedBytes = packedBytes
this.limitBytes = limitBytes
}
}
function tarUnpack(args: string[]): Promise<void> {
return new Promise((resolve, reject) => {
execFile('tar', args, { timeout: 120_000 }, (err, _stdout, stderr) => {
if (err) {
@@ -14,12 +43,74 @@ function tar(args: string[]): Promise<void> {
})
}
export async function packHandoff(stagingDir: string, outFile: string): Promise<void> {
export async function packHandoff(
stagingDir: string,
outFile: string,
opts?: { maxBytes?: number, onProgress?: (packedBytes: number) => void },
): Promise<void> {
const maxBytes = opts?.maxBytes ?? HANDOFF_ATTACHMENT_MAX_BYTES
await fsp.mkdir(path.dirname(outFile), { recursive: true })
await tar(['-czf', outFile, '-C', stagingDir, '.'])
await new Promise<void>((resolve, reject) => {
const child = spawn('tar', ['-czf', outFile, '-C', stagingDir, '.'], { stdio: ['ignore', 'ignore', 'pipe'] })
const decode = new TextDecoder()
let stderr = ''
let settled = false
let poll: ReturnType<typeof setInterval> | undefined
child.stderr?.on('data', (chunk: Uint8Array) => {
stderr += decode.decode(chunk, { stream: true })
})
const finish = (err?: Error): void => {
if (settled)
return
settled = true
if (poll)
clearInterval(poll)
if (err)
reject(err)
else
resolve()
}
const checkSize = (): void => {
let size = 0
try {
size = statSync(outFile).size
}
catch {
return
}
opts?.onProgress?.(size)
// 产出一超过上限就杀掉。等 tar 自己打完再失败,大会话会卡很久。
if (size > maxBytes) {
child.kill('SIGKILL')
finish(new HandoffArchiveTooLargeError(size, maxBytes))
}
}
poll = setInterval(checkSize, 200)
child.on('error', err => finish(err))
child.on('close', (code) => {
if (settled)
return
let size = 0
try {
size = statSync(outFile).size
}
catch {
// 包还没写出来
}
opts?.onProgress?.(size)
if (size > maxBytes) {
finish(new HandoffArchiveTooLargeError(size, maxBytes))
return
}
if (code === 0)
finish()
else
finish(new Error(`tar -czf 失败: ${stderr.trim() || `exit ${code}`}`))
})
})
}
export async function unpackHandoff(archive: string, dstDir: string): Promise<void> {
await fsp.mkdir(dstDir, { recursive: true })
await tar(['-xzf', archive, '-C', dstDir])
await tarUnpack(['-xzf', archive, '-C', dstDir])
}