🐛 fix(vscode): 移交只对负责人显示;接管后审查会话立即可点

Claude-Session: https://claude.ai/code/session_011cEyL6k351U2BzX1Qmygph
This commit is contained in:
2026-08-26 18:58:04 +08:00
parent 71fc38e0c5
commit 07c40897e2
4 changed files with 64 additions and 22 deletions
+34 -7
View File
@@ -11,14 +11,18 @@ import {
} from './handoff'
describe('canStartHandoff', () => {
it('gitea 工单、非 done、无待接管 → true', () => {
expect(canStartHandoff({ column: 'in-progress' })).toBe(true)
expect(canStartHandoff({ column: 'todo', source: 'gitea' })).toBe(true)
it('gitea 工单、非 done、无待接管、无 assignee 或 assignee 含我 → true', () => {
expect(canStartHandoff({ column: 'in-progress' }, 'me')).toBe(true)
expect(canStartHandoff({ column: 'todo', source: 'gitea' }, 'me')).toBe(true)
expect(canStartHandoff({ column: 'todo', assignees: [] }, 'me')).toBe(true)
expect(canStartHandoff({ column: 'todo', assignees: ['me', 'x'] }, 'me')).toBe(true)
})
it('done / youtrack / 已在移交中 → false', () => {
expect(canStartHandoff({ column: 'done' })).toBe(false)
expect(canStartHandoff({ column: 'review', source: 'youtrack' })).toBe(false)
expect(canStartHandoff({ column: 'review', handoffAttachmentId: '9' })).toBe(false)
it('done / youtrack / 已在移交中 / 不是我负责 → false', () => {
expect(canStartHandoff({ column: 'done' }, 'me')).toBe(false)
expect(canStartHandoff({ column: 'review', source: 'youtrack' }, 'me')).toBe(false)
expect(canStartHandoff({ column: 'review', handoffAttachmentId: '9' }, 'me')).toBe(false)
expect(canStartHandoff({ column: 'todo', assignees: ['x'] }, 'me')).toBe(false)
expect(canStartHandoff({ column: 'todo', assignees: ['me'] }, undefined)).toBe(false)
})
})
@@ -162,4 +166,27 @@ describe('payloads', () => {
worktreeExists: false,
})
})
it('接管 UI patch:带 reviewSessionFileExists 时一并下发,UI 不用等下次刷新才能点链接', () => {
expect(handoffAcceptedUiPatch({
sessions: { reviewSessionId: 'r' },
profiles: {},
worktreeExists: false,
reviewSessionFileExists: true,
})).toEqual({
handoffAttachmentId: null,
handoffFrom: null,
reviewSessionId: 'r',
worktreePath: null,
worktreeExists: false,
reviewSessionFileExists: true,
})
})
it('接管 UI patchreviewSessionFileExists 未传时不出现在 patch 里', () => {
const patch = handoffAcceptedUiPatch({
sessions: { sessionId: 's' },
profiles: {},
worktreeExists: false,
})
expect(patch).not.toHaveProperty('reviewSessionFileExists')
})
})
+7 -2
View File
@@ -6,8 +6,11 @@ export function canStartHandoff(issue: {
source?: 'gitea' | 'youtrack'
column: IssueColumn
handoffAttachmentId?: string
}): boolean {
return issue.source !== 'youtrack' && issue.column !== 'done' && !issue.handoffAttachmentId
assignees?: string[]
}, me: string | undefined): boolean {
const assignees = issue.assignees ?? []
const mine = assignees.length === 0 || (me !== undefined && assignees.includes(me))
return issue.source !== 'youtrack' && issue.column !== 'done' && !issue.handoffAttachmentId && mine
}
export function canAcceptHandoff(issue: {
@@ -105,6 +108,7 @@ export function handoffAcceptedUiPatch(local: {
profiles: HandoffProfiles
worktreePath?: string
worktreeExists: boolean
reviewSessionFileExists?: boolean
}): Record<string, unknown> {
return {
handoffAttachmentId: null,
@@ -115,5 +119,6 @@ export function handoffAcceptedUiPatch(local: {
// 与 worktreeExists: false 对不上。
worktreePath: local.worktreePath ?? null,
worktreeExists: local.worktreeExists,
...(local.reviewSessionFileExists !== undefined ? { reviewSessionFileExists: local.reviewSessionFileExists } : {}),
}
}
+22 -12
View File
@@ -152,17 +152,8 @@ export async function handleHandoffStart(panel: KanbanWebviewPanel, issueNumber:
return
}
// 前置复查:webview 按钮的显示条件可能已经过期(并发移交、工单已被拖
// 完成),用刚读到的最新 state 再判一次 canStartHandoff,避免重复移交
const knownColumns: IssueColumn[] = ['todo', 'in-progress', 'review', 'done']
const column = knownColumns.includes(state.column as IssueColumn) ? (state.column as IssueColumn) : 'todo'
if (!canStartHandoff({ source: 'gitea', column, handoffAttachmentId: str(state.handoffAttachmentId) })) {
toast(panel, 'error', `#${issueNumber} 已在移交中或已完成,不能再次移交`)
return
}
// column 兜底成 'todo' 时挡不住「Gitea 已关闭但从没拖进看板」的工单——
// 再拉一次远端 issue.state 兜底。
// 先拉远端 issue:既要兜底「Gitea 已关闭但从没拖进看板」的工单,也要拿
// assignees 判断「是否我负责」——两者都得在 canStartHandoff 判定前就绪
const issue = await getIssue({ host, token, owner, repo, index: issueNumber })
if (!issue) {
toast(panel, 'error', `工单 #${issueNumber} 不存在或已删除`)
@@ -173,6 +164,21 @@ export async function handleHandoffStart(panel: KanbanWebviewPanel, issueNumber:
return
}
// 前置复查:webview 按钮的显示条件可能已经过期(并发移交、工单已被拖到
// 完成、指派已变更),用刚读到的最新 state + issue 再判一次 canStartHandoff
// 避免重复移交或抢别人的工单。
const knownColumns: IssueColumn[] = ['todo', 'in-progress', 'review', 'done']
const column = knownColumns.includes(state.column as IssueColumn) ? (state.column as IssueColumn) : 'todo'
const assignees = (issue.assignees ?? []).map(a => a.login)
if (!canStartHandoff({ source: 'gitea', column, handoffAttachmentId: str(state.handoffAttachmentId), assignees }, me)) {
// 区分「不是我的工单」和「已在移交中/已完成」两种拒绝原因,措辞不同。
const notMine = assignees.length > 0 && !(me !== undefined && assignees.includes(me))
toast(panel, 'error', notMine
? `#${issueNumber} 不是你负责的工单,不能移交`
: `#${issueNumber} 已在移交中或已完成,不能再次移交`)
return
}
const branch = str(state.branch)
const worktreePath = str(state.worktreePath)
const sessions: HandoffSessions = {
@@ -456,6 +462,10 @@ export async function handleHandoffAccept(panel: KanbanWebviewPanel, issueNumber
})
// ④ 本机字段 + 清共享字段;⑤ 删附件(失败只 warn)
// reviewSessionFileExists 是计算字段,board 只在启动时算一次(annotateReviewSessionFileExists);
// 接管刚把 codex 会话文件装进本机,这里顺手算好一起下发,UI 不用等下次刷新才能点「审查会话」链接。
const reviewSessionFileExists = !!manifest.sessions.reviewSessionId
&& manifest.codex.some(c => c.id.toLowerCase() === manifest.sessions.reviewSessionId!.toLowerCase())
const local = { sessions: manifest.sessions, profiles: manifest.profiles, worktreePath: worktreeAbs }
await panel.mergeIssueState(issueNumber, handoffAcceptedStateExtra(local))
try {
@@ -469,7 +479,7 @@ export async function handleHandoffAccept(panel: KanbanWebviewPanel, issueNumber
panel.postMessage({
type: 'issue/patch',
issueNumber,
patch: handoffAcceptedUiPatch({ ...local, worktreeExists: !!worktreeAbs }),
patch: handoffAcceptedUiPatch({ ...local, worktreeExists: !!worktreeAbs, reviewSessionFileExists }),
})
toast(panel, 'success', `已接管 #${issueNumber}(来自 ${manifest.from}${worktreeAbs ? `worktree${worktreeAbs}` : ''}`)
logger.add({ level: 'info', source: 'panel', message: `接管 #${issueNumber} 完成`, details: JSON.stringify({ manifest, worktreeAbs }) })
@@ -597,7 +597,7 @@ export function IssueDetailPanel({
</button>
)}
{!issue.handoffAttachmentId && issue.column !== 'done' && (
{!issue.handoffAttachmentId && issue.column !== 'done' && ((issue.assignees ?? []).length === 0 || (me !== undefined && (issue.assignees ?? []).includes(me))) && (
<button
type="button"
disabled={isHandoffRunning(issue.number)}