✨ 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

+111
View File
@@ -0,0 +1,111 @@
import type { HandoffRoots, HandoffSource } from './handoffBundle'
import { promises as fsp } from 'node:fs'
import * as os from 'node:os'
import * as path from 'node:path'
import { formatByteSize, HANDOFF_ATTACHMENT_MAX_BYTES, HandoffArchiveTooLargeError, packHandoff } from './handoffArchive'
import { stageHandoffBundle } from './handoffBundle'
import { CLAUDE_SESSION_FIELDS } from './handoffManifest'
import { codexRolloutRelPathFor, findClaudeSessionFiles } from './sessionBundle'
const ROLE_LABEL = {
brainstorm: '头脑风暴',
implement: '实施',
test: '测试',
prototype: '原型',
review: '审查',
} as const
export interface HandoffSourceSize {
role: string
bytes: number
}
export class HandoffSourcesTooLargeError extends Error {
readonly items: HandoffSourceSize[]
readonly limitBytes: number
constructor(items: HandoffSourceSize[], limitBytes: number) {
super(formatHandoffTooLarge(items, limitBytes))
this.name = 'HandoffSourcesTooLargeError'
this.items = items
this.limitBytes = limitBytes
}
}
export function formatHandoffTooLarge(items: HandoffSourceSize[], limitBytes: number): string {
const parts = items.filter(item => item.bytes > 0).map(item => `${item.role} ${formatByteSize(item.bytes)}`)
const total = items.reduce((sum, item) => sum + item.bytes, 0)
return `会话太大,没有开始移交:${parts.join('、')},合计 ${formatByteSize(total)}。压缩后超过 Gitea 附件上限 ${formatByteSize(limitBytes)}`
}
async function treeBytes(abs: string): Promise<number> {
const st = await fsp.lstat(abs)
if (st.isSymbolicLink())
return 0
if (!st.isDirectory())
return st.size
let total = 0
for (const name of await fsp.readdir(abs))
total += await treeBytes(path.join(abs, name))
return total
}
/** 统计即将打进会话包的文件。缺失的会话不出现。 */
export async function measureHandoffSources(src: HandoffSource, roots: HandoffRoots): Promise<HandoffSourceSize[]> {
const items: HandoffSourceSize[] = []
for (const { field, kind } of CLAUDE_SESSION_FIELDS) {
const id = src.sessions[field]
if (!id)
continue
const files = await findClaudeSessionFiles(id, roots.projectsRoot)
if (!files)
continue
let bytes = await treeBytes(files.jsonl)
if (files.dir)
bytes += await treeBytes(files.dir)
items.push({ role: ROLE_LABEL[kind], bytes })
}
if (src.sessions.reviewSessionId) {
const relPath = await codexRolloutRelPathFor(src.sessions.reviewSessionId, roots.codexRoot)
if (relPath) {
items.push({
role: ROLE_LABEL.review,
bytes: await treeBytes(path.join(roots.codexRoot, relPath)),
})
}
}
return items
}
/**
* 关终端、上传之前调用。
* 原始大小不超过上限时直接过(压缩只会更小,最多多出可忽略的包头)。
* 超过才真正压缩,产出一过上限就停。
*/
export async function ensureHandoffFits(
src: HandoffSource,
roots: HandoffRoots,
opts?: { maxBytes?: number, onProgress?: (packedBytes: number) => void },
): Promise<HandoffSourceSize[]> {
const maxBytes = opts?.maxBytes ?? HANDOFF_ATTACHMENT_MAX_BYTES
const items = await measureHandoffSources(src, roots)
const total = items.reduce((sum, item) => sum + item.bytes, 0)
if (total <= maxBytes)
return items
const staging = await fsp.mkdtemp(path.join(os.tmpdir(), 'spx-handoff-size-'))
const outDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'spx-handoff-size-out-'))
try {
await stageHandoffBundle(src, staging, roots)
await packHandoff(staging, path.join(outDir, 'probe.tgz'), { maxBytes, onProgress: opts?.onProgress })
return items
}
catch (err) {
if (err instanceof HandoffArchiveTooLargeError)
throw new HandoffSourcesTooLargeError(items, maxBytes)
throw err
}
finally {
await fsp.rm(staging, { recursive: true, force: true }).catch(() => {})
await fsp.rm(outDir, { recursive: true, force: true }).catch(() => {})
}
}