✨ feat(vscode): 移交时显示步骤,会话包超限先停
This commit is contained in:
1 parent
9220bad599
commit
162134dcc0
6 files changed
+412
-32
No files matched your search
@@ -9,9 +9,10 @@ import * as path from 'node:path'
|
||||
import { window, workspace } from 'vscode'
|
||||
import { loginForToken } from '../../auth/identity'
|
||||
import { getToken } from '../../auth/secrets'
|
||||
import { packHandoff, unpackHandoff } from '../../cc/handoffArchive'
|
||||
import { formatByteSize, HandoffArchiveTooLargeError, packHandoff, unpackHandoff } from '../../cc/handoffArchive'
|
||||
import { installHandoffBundle, stageHandoffBundle } from '../../cc/handoffBundle'
|
||||
import { HANDOFF_MANIFEST_FILE, handoffAttachmentName, parseHandoffManifest } from '../../cc/handoffManifest'
|
||||
import { ensureHandoffFits, HandoffSourcesTooLargeError } from '../../cc/handoffSize'
|
||||
import { claudeProjectsRoot } from '../../cc/sessionBundle'
|
||||
import { projectsDirFor } from '../../cc/sessionWatcher'
|
||||
import { detectRepo } from '../../git/remote'
|
||||
@@ -191,27 +192,34 @@ export async function handleHandoffStart(panel: KanbanWebviewPanel, issueNumber:
|
||||
...(str(state.prototypeProfilePath) ? { prototypeProfilePath: str(state.prototypeProfilePath) } : {}),
|
||||
}
|
||||
|
||||
const say = (step: string): string => toast(panel, 'info', `正在移交 #${issueNumber} 给 ${to}:${step}`, { id: spinner, spinner: true, dismissOnTimer: 0 })
|
||||
const fail = (message: string): void => {
|
||||
if (spinner)
|
||||
dismiss(panel, spinner)
|
||||
toast(panel, 'error', message)
|
||||
}
|
||||
// ① worktree 守卫:干净 + 与远端一致。worktree 存在但 state 没有 branch 时
|
||||
// 没法做 push 校验——不能放行,否则 ⑥ 清理会在没校验过的情况下把它删掉。
|
||||
// 没有 worktree 但本地还留着同名分支时,⑥ 会直接 `git branch -D` 掉它——
|
||||
// 分支若领先远端(未 push)就会丢工作,必须在这里单独拦一次。
|
||||
spinner = say('核对分支是否已推送')
|
||||
const worktreeAbs = worktreePath ? resolveWorktreePath(worktreePath, workspaceRoot) : undefined
|
||||
if (worktreeAbs && fs.existsSync(worktreeAbs)) {
|
||||
if (!branch) {
|
||||
toast(panel, 'error', `无法移交 #${issueNumber}:state 缺少 branch,无法校验是否已 push`)
|
||||
fail(`无法移交 #${issueNumber}:state 缺少 branch,无法校验是否已 push`)
|
||||
return
|
||||
}
|
||||
const status = await git(worktreeAbs, ['status', '--porcelain'])
|
||||
const fetched = await git(worktreeAbs, ['fetch', 'origin', branch])
|
||||
if (!status.ok || !fetched.ok) {
|
||||
toast(panel, 'error', `检查 worktree 失败:${(status.stderr || fetched.stderr).trim()}`)
|
||||
fail(`检查 worktree 失败:${(status.stderr || fetched.stderr).trim()}`)
|
||||
return
|
||||
}
|
||||
const localHead = await git(worktreeAbs, ['rev-parse', 'HEAD'])
|
||||
const remoteHead = await git(worktreeAbs, ['rev-parse', `origin/${branch}`])
|
||||
const check = checkWorktreePushed({ statusPorcelain: status.stdout, localHead: localHead.stdout, remoteHead: remoteHead.stdout })
|
||||
if (!check.ok) {
|
||||
toast(panel, 'error', `无法移交 #${issueNumber}:${check.reason}`)
|
||||
fail(`无法移交 #${issueNumber}:${check.reason}`)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -223,15 +231,39 @@ export async function handleHandoffStart(panel: KanbanWebviewPanel, issueNumber:
|
||||
const localHead = await git(workspaceRoot, ['rev-parse', `refs/heads/${branch}`])
|
||||
const remoteHead = await git(workspaceRoot, ['rev-parse', `refs/remotes/origin/${branch}`])
|
||||
if (!fetched.ok || localHead.stdout.trim() !== remoteHead.stdout.trim()) {
|
||||
toast(panel, 'error', `无法移交 #${issueNumber}:本地分支 ${branch} 与远端不一致,请先 push`)
|
||||
fail(`无法移交 #${issueNumber}:本地分支 ${branch} 与远端不一致,请先 push`)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
spinner = toast(panel, 'info', `正在移交 #${issueNumber} 给 ${to}…`, { spinner: true, dismissOnTimer: 120_000 })
|
||||
// 关终端之前查大小。太大就停,避免白关终端再打包很久。
|
||||
const handoffSrc = { issueNumber, from: me, branch, ...(worktreeAbs && fs.existsSync(worktreeAbs) ? { worktreePath: worktreeAbs } : {}), sessions, profiles }
|
||||
const handoffRoots = { projectsRoot: claudeProjectsRoot(), codexRoot: defaultCodexSessionsDir() }
|
||||
let packedMbShown = -1
|
||||
const reportPacked = (label: string, withLimit: boolean) => (packedBytes: number): void => {
|
||||
const mb = Math.floor(packedBytes / (1024 * 1024))
|
||||
if (mb === packedMbShown)
|
||||
return
|
||||
packedMbShown = mb
|
||||
say(withLimit ? `${label} ${mb}MB / 100MB` : `${label} ${mb}MB`)
|
||||
}
|
||||
say('检查会话大小')
|
||||
try {
|
||||
await ensureHandoffFits(handoffSrc, handoffRoots, { onProgress: reportPacked('压缩检查', true) })
|
||||
}
|
||||
catch (err) {
|
||||
if (err instanceof HandoffSourcesTooLargeError) {
|
||||
fail(`#${issueNumber} ${err.message}`)
|
||||
return
|
||||
}
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
fail(`移交失败(检查会话大小):${message}`)
|
||||
return
|
||||
}
|
||||
|
||||
// ② 关终端,等 jsonl 刷盘
|
||||
say('关闭终端')
|
||||
let disposedAny = false
|
||||
for (const [terminal, origin] of panel.terminalOrigin) {
|
||||
if (origin.issueNumber === issueNumber) {
|
||||
@@ -257,20 +289,19 @@ export async function handleHandoffStart(panel: KanbanWebviewPanel, issueNumber:
|
||||
await new Promise<void>(r => setTimeout(r, 600))
|
||||
|
||||
// ③ 打包 + 上传
|
||||
say('收集会话')
|
||||
let attachmentId: number
|
||||
let manifest: HandoffManifest
|
||||
// tgz 必须放在 staging 之外,否则 tar 会把半成品包也打进去。
|
||||
const staging = await scratchDir('spx-handoff-')
|
||||
const outDir = await scratchDir('spx-handoff-out-')
|
||||
try {
|
||||
manifest = await stageHandoffBundle(
|
||||
{ issueNumber, from: me, branch, ...(worktreeAbs && fs.existsSync(worktreeAbs) ? { worktreePath: worktreeAbs } : {}), sessions, profiles },
|
||||
staging,
|
||||
{ projectsRoot: claudeProjectsRoot(), codexRoot: defaultCodexSessionsDir() },
|
||||
)
|
||||
manifest = await stageHandoffBundle(handoffSrc, staging, handoffRoots)
|
||||
const name = handoffAttachmentName(issueNumber)
|
||||
const archive = path.join(outDir, name)
|
||||
await packHandoff(staging, archive)
|
||||
packedMbShown = -1
|
||||
await packHandoff(staging, archive, { onProgress: reportPacked('打包', false) })
|
||||
say(`上传 ${formatByteSize((await fsp.stat(archive)).size)}`)
|
||||
for (const old of await listIssueAttachments({ host, token, owner, repo, index: issueNumber })) {
|
||||
if (old.name === name)
|
||||
await deleteIssueAttachment({ host, token, owner, repo, index: issueNumber, attachmentId: old.id })
|
||||
@@ -280,10 +311,13 @@ export async function handleHandoffStart(panel: KanbanWebviewPanel, issueNumber:
|
||||
logger.add({ level: 'info', source: 'panel', message: `移交 #${issueNumber}:会话包已上传 (attachment ${attachmentId}, ${uploaded.size} bytes)`, details: JSON.stringify(manifest) })
|
||||
}
|
||||
catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
const message = err instanceof HandoffArchiveTooLargeError
|
||||
? err.message
|
||||
: (err instanceof Error ? err.message : String(err))
|
||||
const prefix = err instanceof HandoffArchiveTooLargeError ? '打包' : '打包/上传'
|
||||
logger.add({ level: 'error', source: 'panel', message: `移交 #${issueNumber}:打包/上传失败`, details: message })
|
||||
dismiss(panel, spinner)
|
||||
toast(panel, 'error', `移交失败(打包/上传):${message}`)
|
||||
toast(panel, 'error', `移交失败(${prefix}):${message}`)
|
||||
return
|
||||
}
|
||||
finally {
|
||||
@@ -294,6 +328,7 @@ export async function handleHandoffStart(panel: KanbanWebviewPanel, issueNumber:
|
||||
|
||||
// ④ 写共享字段(只 handoffAttachmentId / handoffFrom,本机墓碑留到 ⑦);
|
||||
// ⑤ 重新指派。失败回滚远端改动。
|
||||
say('写入状态')
|
||||
try {
|
||||
await panel.mergeIssueState(issueNumber, handoffStartedStateExtra(attachmentId, me))
|
||||
}
|
||||
@@ -305,6 +340,7 @@ export async function handleHandoffStart(panel: KanbanWebviewPanel, issueNumber:
|
||||
toast(panel, 'error', `移交失败(写状态):${message}`)
|
||||
return
|
||||
}
|
||||
say('指派')
|
||||
try {
|
||||
await updateIssueAssignees({ host, token, owner, repo, index: issueNumber, assignees: [to] })
|
||||
}
|
||||
@@ -320,6 +356,8 @@ export async function handleHandoffStart(panel: KanbanWebviewPanel, issueNumber:
|
||||
|
||||
// ⑥ 本机清理:worktree + 本地分支(远端分支留给接管方)。失败只告警——
|
||||
// 远端移交已经生效,这里不回滚。
|
||||
if ((worktreeAbs && fs.existsSync(worktreeAbs)) || branch)
|
||||
say('清理本机')
|
||||
let worktreeRemovalFailed = false
|
||||
if (worktreeAbs && fs.existsSync(worktreeAbs)) {
|
||||
const settings = getSettings(panel.context)
|
||||
|
||||
Reference in new issue
Block a user