🐛 fix(vscode): 接管工单不再用同事机器上的模型配置盖掉本机的,移交出去也不清自己选的配置
This commit is contained in:
1 parent
7ca2a8db34
commit
1f161c7012
9 files changed
+90
-93
No files matched your search
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { parseHandoffManifest } from '../../cc/handoffManifest'
|
||||
import {
|
||||
canAcceptHandoff,
|
||||
canStartHandoff,
|
||||
@@ -68,6 +69,13 @@ describe('checkWorktreePushed', () => {
|
||||
})
|
||||
})
|
||||
|
||||
const PROFILE_FIELDS = ['profilePath', 'brainstormProfilePath', 'testProfilePath', 'prototypeProfilePath'] as const
|
||||
|
||||
function expectNoProfileFields(payload: Record<string, unknown>): void {
|
||||
for (const f of PROFILE_FIELDS)
|
||||
expect(payload).not.toHaveProperty(f)
|
||||
}
|
||||
|
||||
describe('payloads', () => {
|
||||
it('移交落盘:只写两个共享字段,本机字段留到本机清理跑完后再墓碑', () => {
|
||||
expect(handoffStartedStateExtra(KEY, 'chw')).toEqual({
|
||||
@@ -75,17 +83,13 @@ describe('payloads', () => {
|
||||
handoffFrom: 'chw',
|
||||
})
|
||||
})
|
||||
it('本机墓碑:keepWorktreePath=false 时本机字段全清', () => {
|
||||
it('本机墓碑:keepWorktreePath=false 时清会话 id、worktreePath、prDiffFile', () => {
|
||||
expect(handoffLocalTombstones({ keepWorktreePath: false })).toEqual({
|
||||
sessionId: '',
|
||||
implementSessionId: '',
|
||||
reviewSessionId: '',
|
||||
testSessionId: '',
|
||||
prototypeSessionId: '',
|
||||
profilePath: '',
|
||||
brainstormProfilePath: '',
|
||||
testProfilePath: '',
|
||||
prototypeProfilePath: '',
|
||||
worktreePath: '',
|
||||
prDiffFile: '',
|
||||
})
|
||||
@@ -99,13 +103,13 @@ describe('payloads', () => {
|
||||
reviewSessionId: '',
|
||||
testSessionId: '',
|
||||
prototypeSessionId: '',
|
||||
profilePath: '',
|
||||
brainstormProfilePath: '',
|
||||
testProfilePath: '',
|
||||
prototypeProfilePath: '',
|
||||
prDiffFile: '',
|
||||
})
|
||||
})
|
||||
it('本机墓碑不清 profile 字段:移交出去再接回来,还是自己原来选的模型配置', () => {
|
||||
expectNoProfileFields(handoffLocalTombstones({ keepWorktreePath: false }))
|
||||
expectNoProfileFields(handoffLocalTombstones({ keepWorktreePath: true }))
|
||||
})
|
||||
it('移交 UI patch 用 null 清字段并更新 assignees', () => {
|
||||
expect(handoffStartedUiPatch(KEY, 'chw', 'me', { keepWorktreePath: false })).toEqual({
|
||||
handoffBundleKey: KEY,
|
||||
@@ -116,10 +120,6 @@ describe('payloads', () => {
|
||||
reviewSessionId: null,
|
||||
testSessionId: null,
|
||||
prototypeSessionId: null,
|
||||
profilePath: null,
|
||||
brainstormProfilePath: null,
|
||||
testProfilePath: null,
|
||||
prototypeProfilePath: null,
|
||||
prDiffFile: null,
|
||||
worktreePath: null,
|
||||
worktreeExists: false,
|
||||
@@ -138,31 +138,43 @@ describe('payloads', () => {
|
||||
reviewSessionId: null,
|
||||
testSessionId: null,
|
||||
prototypeSessionId: null,
|
||||
profilePath: null,
|
||||
brainstormProfilePath: null,
|
||||
testProfilePath: null,
|
||||
prototypeProfilePath: null,
|
||||
prDiffFile: null,
|
||||
})
|
||||
})
|
||||
it('接管落盘:清两个共享字段,写本机 sid / profile / worktree', () => {
|
||||
it('移交 UI patch 不碰 profile 字段,卡片上仍显示自己选的模型配置', () => {
|
||||
expectNoProfileFields(handoffStartedUiPatch(KEY, 'chw', 'me', { keepWorktreePath: false }))
|
||||
expectNoProfileFields(handoffStartedUiPatch(KEY, 'chw', 'me', { keepWorktreePath: true }))
|
||||
})
|
||||
it('接管落盘:清两个共享字段,写本机 sid / worktree', () => {
|
||||
expect(handoffAcceptedStateExtra({
|
||||
sessions: { implementSessionId: 'i', reviewSessionId: 'r' },
|
||||
profiles: { profilePath: '/p.json' },
|
||||
worktreePath: '/wt',
|
||||
})).toEqual({
|
||||
handoffBundleKey: '',
|
||||
handoffFrom: '',
|
||||
implementSessionId: 'i',
|
||||
reviewSessionId: 'r',
|
||||
profilePath: '/p.json',
|
||||
worktreePath: '/wt',
|
||||
})
|
||||
})
|
||||
it('接管旧会话包:包里带着同事机器上的 profile 路径,本机 state 和 UI 都不写 profile 字段', () => {
|
||||
const manifest = parseHandoffManifest(JSON.stringify({
|
||||
version: 1,
|
||||
issue: 9,
|
||||
from: 'chehongweo',
|
||||
createdAt: '2026-10-08T01:02:03Z',
|
||||
sessions: { sessionId: 's' },
|
||||
profiles: { brainstormProfilePath: '/Users/chehongweo/Sources/cruldra-profile/claude-config/profiles/gpt-6.json' },
|
||||
claude: [],
|
||||
codex: [],
|
||||
}), 9)
|
||||
const local = { sessions: manifest.sessions, worktreePath: '/wt' }
|
||||
expectNoProfileFields(handoffAcceptedStateExtra(local))
|
||||
expectNoProfileFields(handoffAcceptedUiPatch({ ...local, worktreeExists: true, missingSessionIds: [] }))
|
||||
})
|
||||
it('接管 UI patch', () => {
|
||||
expect(handoffAcceptedUiPatch({
|
||||
sessions: { sessionId: 's' },
|
||||
profiles: {},
|
||||
worktreePath: '/wt',
|
||||
worktreeExists: true,
|
||||
missingSessionIds: [],
|
||||
@@ -178,7 +190,6 @@ describe('payloads', () => {
|
||||
it('接管 UI patch:无 worktreePath 时显式置 null,不留旧值', () => {
|
||||
expect(handoffAcceptedUiPatch({
|
||||
sessions: { sessionId: 's' },
|
||||
profiles: {},
|
||||
worktreeExists: false,
|
||||
missingSessionIds: [],
|
||||
})).toEqual({
|
||||
@@ -193,7 +204,6 @@ describe('payloads', () => {
|
||||
it('接管 UI patch:缺失的会话 id 原样下发,UI 立刻知道哪几行不能 resume', () => {
|
||||
expect(handoffAcceptedUiPatch({
|
||||
sessions: { sessionId: 's', reviewSessionId: 'r' },
|
||||
profiles: {},
|
||||
worktreeExists: false,
|
||||
missingSessionIds: ['r'],
|
||||
})).toEqual({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { HandoffProfiles, HandoffSessions } from '../../cc/handoffManifest'
|
||||
import type { HandoffSessions } from '../../cc/handoffManifest'
|
||||
import type { IssueColumn } from '../../gitea/types'
|
||||
import { formatByteSize } from '../../cc/handoffArchive'
|
||||
import { LOCAL_STATE_FIELDS } from '../../issues/localState'
|
||||
import { LOCAL_PROFILE_FIELDS, LOCAL_STATE_FIELDS } from '../../issues/localState'
|
||||
|
||||
export function canStartHandoff(issue: {
|
||||
source?: 'gitea' | 'youtrack'
|
||||
@@ -46,30 +46,35 @@ export function checkWorktreePushed(input: {
|
||||
}
|
||||
|
||||
/**
|
||||
* 共享字段:只写 handoffBundleKey / handoffFrom 两个。本机
|
||||
* sid/profile/worktreePath 墓碑要等本机清理跑完(成功指派后)才落盘,
|
||||
* 见 handoffLocalTombstones —— 否则④/⑤失败回滚时本机字段已经被清空,
|
||||
* 用户会话恢复不了。
|
||||
* 移交时写进共享 state 的只有 `handoffBundleKey` 和 `handoffFrom` 两个字段,让接管方知道有会话包等着接。
|
||||
*
|
||||
* 本机字段的墓碑要等本机清理跑完(指派成功之后)才落盘,见 [handoffLocalTombstones]:否则写状态或指派失败回滚时,
|
||||
* 本机字段已经清空,用户的会话就恢复不了。
|
||||
*/
|
||||
export function handoffStartedStateExtra(bundleKey: string, from: string): Record<string, unknown> {
|
||||
return { handoffBundleKey: bundleKey, handoffFrom: from }
|
||||
}
|
||||
|
||||
/**
|
||||
* 本机墓碑:移交流程最后一步(本机清理跑完之后)才调用。worktree 删除失败时
|
||||
* 保留 worktreePath(keepWorktreePath),别把用户还留在磁盘上的 worktree
|
||||
* 从 state 里抹掉。
|
||||
* 移交给别人后,把本机上会话 id、worktree 路径这些到了接管方就没用的字段写成 `''` 墓碑。
|
||||
*
|
||||
* 移交流程最后一步(本机清理跑完之后)才调用。[LOCAL_PROFILE_FIELDS] 不清:那是用户在这台机器上选的模型配置,
|
||||
* 工单以后接回来还照旧用。worktree 删除失败时(`keepWorktreePath`)也保留 `worktreePath`,
|
||||
* 别把用户还留在磁盘上的 worktree 从 state 里抹掉。
|
||||
*/
|
||||
export function handoffLocalTombstones(opts: { keepWorktreePath: boolean }): Record<string, string> {
|
||||
const kept = new Set<string>(LOCAL_PROFILE_FIELDS)
|
||||
if (opts.keepWorktreePath)
|
||||
kept.add('worktreePath')
|
||||
const extra: Record<string, string> = {}
|
||||
for (const f of LOCAL_STATE_FIELDS) {
|
||||
if (f === 'worktreePath' && opts.keepWorktreePath)
|
||||
continue
|
||||
extra[f] = ''
|
||||
if (!kept.has(f))
|
||||
extra[f] = ''
|
||||
}
|
||||
return extra
|
||||
}
|
||||
|
||||
/** 移交成功后发给看板的改动:标上待接管、换负责人、清掉本机会话 id 和 worktree,profile 不动,卡片上照旧是本机选的模型配置。 */
|
||||
export function handoffStartedUiPatch(bundleKey: string, from: string, to: string, opts: { keepWorktreePath: boolean }): Record<string, unknown> {
|
||||
return {
|
||||
handoffBundleKey: bundleKey,
|
||||
@@ -80,10 +85,6 @@ export function handoffStartedUiPatch(bundleKey: string, from: string, to: strin
|
||||
reviewSessionId: null,
|
||||
testSessionId: null,
|
||||
prototypeSessionId: null,
|
||||
profilePath: null,
|
||||
brainstormProfilePath: null,
|
||||
testProfilePath: null,
|
||||
prototypeProfilePath: null,
|
||||
prDiffFile: null,
|
||||
// worktree 没删掉时(opts.keepWorktreePath)不碰这两个字段,让 UI 继续显示孤儿 worktree。
|
||||
...(opts.keepWorktreePath ? {} : { worktreePath: null, worktreeExists: false }),
|
||||
@@ -99,23 +100,21 @@ function definedEntries(o: Record<string, any>): Record<string, string> {
|
||||
return out
|
||||
}
|
||||
|
||||
/** 接管后写进 state 的字段:清掉两个共享的移交字段,写入会话 id 和本机 worktree 路径;profile 不写,接管方用自己机器上的模型配置。 */
|
||||
export function handoffAcceptedStateExtra(local: {
|
||||
sessions: HandoffSessions
|
||||
profiles: HandoffProfiles
|
||||
worktreePath?: string
|
||||
}): Record<string, unknown> {
|
||||
return {
|
||||
handoffBundleKey: '',
|
||||
handoffFrom: '',
|
||||
...definedEntries(local.sessions),
|
||||
...definedEntries(local.profiles),
|
||||
...(local.worktreePath ? { worktreePath: local.worktreePath } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
export function handoffAcceptedUiPatch(local: {
|
||||
sessions: HandoffSessions
|
||||
profiles: HandoffProfiles
|
||||
worktreePath?: string
|
||||
worktreeExists: boolean
|
||||
missingSessionIds: string[]
|
||||
@@ -124,7 +123,6 @@ export function handoffAcceptedUiPatch(local: {
|
||||
handoffBundleKey: null,
|
||||
handoffFrom: null,
|
||||
...definedEntries(local.sessions),
|
||||
...definedEntries(local.profiles),
|
||||
// 显式 null 而非省略:省略会让 webview 侧留着接管前的旧 worktreePath,
|
||||
// 与 worktreeExists: false 对不上。
|
||||
worktreePath: local.worktreePath ?? null,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { HandoffManifest, HandoffProfiles, HandoffSessions } from '../../cc/handoffManifest'
|
||||
import type { HandoffManifest, HandoffSessions } from '../../cc/handoffManifest'
|
||||
import type { IssueColumn } from '../../gitea/types'
|
||||
import type { HandoffStore } from '../../oss/handoffStore'
|
||||
import type { KanbanWebviewPanel } from '../KanbanPanel'
|
||||
@@ -217,12 +217,6 @@ export async function handleHandoffStart(panel: KanbanWebviewPanel, issueNumber:
|
||||
...(str(state.prototypeSessionId) ? { prototypeSessionId: str(state.prototypeSessionId) } : {}),
|
||||
...(str(state.reviewSessionId) ? { reviewSessionId: str(state.reviewSessionId) } : {}),
|
||||
}
|
||||
const profiles: HandoffProfiles = {
|
||||
...(str(state.profilePath) ? { profilePath: str(state.profilePath) } : {}),
|
||||
...(str(state.brainstormProfilePath) ? { brainstormProfilePath: str(state.brainstormProfilePath) } : {}),
|
||||
...(str(state.testProfilePath) ? { testProfilePath: str(state.testProfilePath) } : {}),
|
||||
...(str(state.prototypeProfilePath) ? { prototypeProfilePath: str(state.prototypeProfilePath) } : {}),
|
||||
}
|
||||
|
||||
const fail = (message: string): HandoffStartResult => {
|
||||
progress.dismiss()
|
||||
@@ -292,7 +286,7 @@ export async function handleHandoffStart(panel: KanbanWebviewPanel, issueNumber:
|
||||
|
||||
// ③ 打包 + 上传
|
||||
say('收集会话')
|
||||
const handoffSrc = { issueNumber, from: me, branch, ...(worktreeAbs && fs.existsSync(worktreeAbs) ? { worktreePath: worktreeAbs } : {}), sessions, profiles }
|
||||
const handoffSrc = { issueNumber, from: me, branch, ...(worktreeAbs && fs.existsSync(worktreeAbs) ? { worktreePath: worktreeAbs } : {}), sessions }
|
||||
const handoffRoots = { projectsRoot: claudeProjectsRoot(), codexRoot: defaultCodexSessionsDir() }
|
||||
const bundleKey = handoffBundleKey({ owner, repo, issueNumber, at: new Date() })
|
||||
let manifest: HandoffManifest
|
||||
@@ -372,9 +366,10 @@ export async function handleHandoffStart(panel: KanbanWebviewPanel, issueNumber:
|
||||
await cleanupFeatureBranch({ workspaceRoot, branch, issueNumber, devBranch: settings.devBranch })
|
||||
}
|
||||
|
||||
// ⑦ 本机墓碑:sid/profile/worktreePath 归零。worktree 没删掉时保留
|
||||
// worktreePath,别把用户还留在磁盘上的 worktree 从 state 里抹掉。写墓碑
|
||||
// 失败只告警——远端移交已经生效,不回滚。
|
||||
// ⑦ 本机墓碑:会话 id、worktreePath、prDiffFile 归零,profile 留着——那是
|
||||
// 本机选的模型配置,工单接回来还照旧用。worktree 没删掉时保留 worktreePath,
|
||||
// 别把用户还留在磁盘上的 worktree 从 state 里抹掉。写墓碑失败只告警——
|
||||
// 远端移交已经生效,不回滚。
|
||||
await panel.mergeIssueState(issueNumber, handoffLocalTombstones({ keepWorktreePath: worktreeRemovalFailed })).catch((err) => {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
logger.add({ level: 'warn', source: 'panel', message: `移交 #${issueNumber}:写本机墓碑失败`, details: message })
|
||||
@@ -583,12 +578,13 @@ export async function handleHandoffAccept(panel: KanbanWebviewPanel, issueNumber
|
||||
...(pathRewrite ? { pathRewrite } : {}),
|
||||
})
|
||||
|
||||
// ④ 本机字段 + 清共享字段;⑤ 删 OSS 上的会话包(失败只 warn)
|
||||
// ④ 写本机会话 id 和 worktreePath、清共享字段,profile 不碰(用本机自己的
|
||||
// 模型配置);⑤ 删 OSS 上的会话包(失败只 warn)
|
||||
// missingSessionIds 是计算字段,board 只在加载时算;会话文件刚装进本机,
|
||||
// 这里按真实文件重算一起下发,UI 不用等下次刷新就能双击 resume。
|
||||
say('写入状态')
|
||||
const [{ missingSessionIds = [] }] = await annotateMissingSessionIds([manifest.sessions])
|
||||
const local = { sessions: manifest.sessions, profiles: manifest.profiles, worktreePath: worktreeAbs }
|
||||
const local = { sessions: manifest.sessions, worktreePath: worktreeAbs }
|
||||
await panel.mergeIssueState(issueNumber, handoffAcceptedStateExtra(local))
|
||||
try {
|
||||
await deleteHandoffBundle(store, bundleKey)
|
||||
|
||||
Reference in new issue
Block a user