🐛 fix(vscode): 接管工单不再用同事机器上的模型配置盖掉本机的,移交出去也不清自己选的配置
This commit is contained in:
1 parent
7ca2a8db34
commit
1f161c7012
9 files changed
+89
-92
No files matched your search
@@ -51,16 +51,14 @@ codex/YYYY/MM/DD/rollout-*.jsonl 审查会话(存在才有)
|
||||
"sessions": {
|
||||
"sessionId": "…", "implementSessionId": "…", "testSessionId": "…", "reviewSessionId": "…"
|
||||
},
|
||||
"profiles": {
|
||||
"profilePath": "/Users/chw/.claude/profiles/x.json",
|
||||
"brainstormProfilePath": "…", "testProfilePath": "…"
|
||||
},
|
||||
"claude": [ { "id": "<sid>", "kind": "brainstorm" | "implement" | "test" } ],
|
||||
"codex": [ { "id": "<uuid>", "relPath": "2026/08/26/rollout-….jsonl" } ]
|
||||
}
|
||||
```
|
||||
|
||||
`sessions` / `profiles` 只放存在的键;`claude` / `codex` 只列实际打进包里的文件。profile 路径原样带发送方绝对路径,接管方沿用现有 `resolveProfilePath` 按文件名重映射。
|
||||
`sessions` 只放存在的键;`claude` / `codex` 只列实际打进包里的文件。
|
||||
|
||||
profile(`profilePath` / `brainstormProfilePath` / `testProfilePath` / `prototypeProfilePath`,各类会话用哪份模型配置)是每台机器自己的选择,不进会话包:发送方不打包,接管方不写入,旧包里残留的 `profiles` 解析时直接忽略。接管方的工单没选过 profile 就用自己设置里的默认值。
|
||||
|
||||
## 共享 state JSON 新字段
|
||||
|
||||
@@ -89,15 +87,15 @@ codex/YYYY/MM/DD/rollout-*.jsonl 审查会话(存在才有)
|
||||
|
||||
扩展端 `handleHandoffStart(panel, issueNumber, to)` 顺序:
|
||||
|
||||
1. 前置:移交存储已配置;workspaceRoot / `detectRepo` / token / `loginForToken` 得 `me`。读 `readIssueState`,取本机字段(四个 sid、三个 profile、`worktreePath`)与共享 `branch`。
|
||||
1. 前置:移交存储已配置;workspaceRoot / `detectRepo` / token / `loginForToken` 得 `me`。读 `readIssueState`,取本机的会话 id、`worktreePath` 与共享 `branch`(profile 不取)。
|
||||
2. **worktree 守卫**(有 `worktreePath` 且目录存在时):`git status --porcelain` 必须为空;`git fetch origin <branch>` 后 `HEAD` 必须等于 `origin/<branch>`。否则 toast「有未提交/未推送改动」并终止,什么都不改。
|
||||
3. 关闭该工单全部终端(`terminalOrigin` 同 issueNumber + 名称匹配 `issue-N-冲突解决`),等 600ms 让 jsonl 刷盘。
|
||||
4. 在 scratch 临时目录收集会话包:同一卷上用硬链接收文件,跨卷才复制。每个 sid 在 `~/.claude/projects/*/` 里找 `<sid>.jsonl`(多份取最新 mtime),连同 `<sid>/` 一起拷入 `claude/`(跳过 `subagents/`,子代理对话不移交);`reviewSessionId` 在 `~/.codex/sessions` 里按文件名尾部 uuid 找到 rollout 拷入 `codex/<relPath>`。找不到的会话不进清单,只 log。写 `handoff.json`,`tar --options xz:compression-level=1 -cJf`。
|
||||
5. 用阿里云官方 SDK `ali-oss` 的分片上传传到 OSS,带 `x-oss-object-acl: private`;中途失败把已传的分片清掉(abort)。
|
||||
6. `mergeIssueState(issueNumber, { handoffBundleKey, handoffFrom: me })`。
|
||||
7. `updateIssueAssignees(… [to])`。
|
||||
8. 本机清理(失败只 warn,不回滚——远端移交已生效):`pre-remove` 钩子 → `killProcessesUsingWorktree` → `removeWorktreeDir`;`cleanupFeatureBranch` **不传 remote/token**(只删本地分支,远端分支必须保留给接管方);本机字段全部写 `''` 墓碑。发送方磁盘上的会话文件保留。
|
||||
9. `issue/patch { handoffBundleKey, handoffFrom, worktreePath: null, worktreeExists: false, …sid: null }` + 成功 toast。
|
||||
8. 本机清理(失败只 warn,不回滚——远端移交已生效):`pre-remove` 钩子 → `killProcessesUsingWorktree` → `removeWorktreeDir`;`cleanupFeatureBranch` **不传 remote/token**(只删本地分支,远端分支必须保留给接管方);本机的会话 id、`worktreePath`、`prDiffFile` 写 `''` 墓碑(worktree 没删掉时保留 `worktreePath`),四个 profile 字段不动,工单以后接回来还是自己原来选的模型配置。发送方磁盘上的会话文件保留。
|
||||
9. `issue/patch { handoffBundleKey, handoffFrom, worktreePath: null, worktreeExists: false, …sid: null }` + 成功 toast;patch 里没有 profile 字段。
|
||||
|
||||
进度 toast 从核对分支开始,同一条提示随步骤改文案(核对分支、关闭终端、收集会话、`打包 x MB`、`上传 x MB / y MB`、写入状态、指派、清理本机)。打包时按包文件已写出的大小报,上传时每传完一片报一次。移交结束前不自动消失。
|
||||
|
||||
@@ -115,7 +113,7 @@ codex/YYYY/MM/DD/rollout-*.jsonl 审查会话(存在才有)
|
||||
2. 从 OSS 边下边写到 scratch 临时目录(文件名固定,不拿 key 拼本机路径;连续 60 秒收不到数据就放弃),`tar -xJf`,解析并校验 `handoff.json`(`version === 1`、`issue === issueNumber`)。
|
||||
3. **重建 worktree**(`manifest.branch ?? state.branch` 存在时):`ensureWorktreeFromRemote({ workspaceRoot, worktreePath, branch })`——`git fetch origin <branch>`;该分支已有 live worktree → 直接用;模板路径被垃圾目录占着 → kill + 删;`git worktree add -B <branch> <path> origin/<branch>`。模板路径用设置 `worktreeDirectory`,slug = `branch` 去掉 `feature/` 前缀。之后跑 `post-create` 钩子。分支不存在于远端 → toast 终止(会话包保留,可重试)。
|
||||
4. **安装会话**:`brainstorm` 会话装到 `projectsDirFor(workspaceRoot)`,`implement` / `test` 装到 `projectsDirFor(worktreeAbs)`(无 worktree 时也装到 workspaceRoot)。安装前先删本机所有 `~/.claude/projects/*/<sid>.jsonl` 与 `<sid>/`(避免重复 sid),再拷 jsonl + 目录。codex 文件按 `relPath` 拷到 `~/.codex/sessions/` 下。
|
||||
5. 写本机字段:清单里的四个 sid、三个 profile 路径、`worktreePath`。
|
||||
5. 写本机字段:清单里的会话 id、`worktreePath`。profile 不写,本机原有的选择和设置里的默认值照旧生效。
|
||||
6. `mergeIssueState(issueNumber, { handoffBundleKey: '', handoffFrom: '' })`;删 OSS 对象(失败只 warn)。
|
||||
7. `issue/patch`(sid、worktreePath、worktreeExists、两个 handoff 字段置 null)+ 成功 toast。
|
||||
|
||||
|
||||
@@ -51,7 +51,6 @@ describe('stageHandoffBundle', () => {
|
||||
from: 'chw',
|
||||
branch: 'feature/x',
|
||||
sessions: { sessionId: BRAIN, implementSessionId: IMPL, testSessionId: 'missing-0000-4000-8000-000000000000', reviewSessionId: REVIEW },
|
||||
profiles: { profilePath: '/Users/chw/p.json' },
|
||||
}, staging, roots, new Date('2026-08-26T00:00:00Z'))
|
||||
expect(manifest.claude).toEqual([{ id: BRAIN, kind: 'brainstorm' }, { id: IMPL, kind: 'implement' }])
|
||||
expect(manifest.codex).toEqual([{ id: REVIEW, relPath: path.join('2026', '08', '26', `rollout-2026-08-26T01-02-03-${REVIEW}.jsonl`) }])
|
||||
@@ -63,6 +62,12 @@ describe('stageHandoffBundle', () => {
|
||||
expect(existsSync(path.join(staging, 'codex', manifest.codex[0].relPath))).toBe(true)
|
||||
expect(parseHandoffManifest(readFileSync(path.join(staging, HANDOFF_MANIFEST_FILE), 'utf8'), 7)).toEqual(manifest)
|
||||
})
|
||||
it('清单不带 profiles:profile 路径是发送方本机的配置,不随会话包移交', async () => {
|
||||
const staging = tempDir('staging-')
|
||||
const manifest = await stageHandoffBundle({ issueNumber: 7, from: 'chw', sessions: { sessionId: BRAIN } }, staging, senderRoots())
|
||||
expect(Object.keys(manifest)).not.toContain('profiles')
|
||||
expect(Object.keys(JSON.parse(readFileSync(path.join(staging, HANDOFF_MANIFEST_FILE), 'utf8')))).not.toContain('profiles')
|
||||
})
|
||||
})
|
||||
|
||||
describe('pack → unpack → install', () => {
|
||||
@@ -74,7 +79,6 @@ describe('pack → unpack → install', () => {
|
||||
from: 'chw',
|
||||
branch: 'feature/x',
|
||||
sessions: { sessionId: BRAIN, implementSessionId: IMPL, reviewSessionId: REVIEW },
|
||||
profiles: {},
|
||||
}, staging, roots)
|
||||
const archive = path.join(tempDir('archive-'), 'issue-7-20261008T010203Z.tar.xz')
|
||||
await packHandoff(staging, archive)
|
||||
@@ -104,7 +108,7 @@ describe('pack → unpack → install', () => {
|
||||
it('无 worktreeProjectsDir 时实施会话也装到工作区目录', async () => {
|
||||
const roots = senderRoots()
|
||||
const staging = tempDir('staging-')
|
||||
const manifest = await stageHandoffBundle({ issueNumber: 7, from: 'chw', sessions: { implementSessionId: IMPL }, profiles: {} }, staging, roots)
|
||||
const manifest = await stageHandoffBundle({ issueNumber: 7, from: 'chw', sessions: { implementSessionId: IMPL } }, staging, roots)
|
||||
const projectsRoot = tempDir('receiver-projects-')
|
||||
const workspaceProjectsDir = path.join(projectsRoot, '-home-me-proj')
|
||||
await installHandoffBundle(staging, manifest, { projectsRoot, codexRoot: tempDir('receiver-codex-'), workspaceProjectsDir })
|
||||
@@ -128,7 +132,6 @@ describe('pack → unpack → install', () => {
|
||||
from: 'chw',
|
||||
worktreePath: WORKTREE_FROM,
|
||||
sessions: { implementSessionId: IMPL, reviewSessionId: REVIEW },
|
||||
profiles: {},
|
||||
}, staging, roots)
|
||||
expect(manifest.worktreePath).toBe(WORKTREE_FROM)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { HandoffManifest, HandoffProfiles, HandoffSessions } from './handoffManifest'
|
||||
import type { HandoffManifest, HandoffSessions } from './handoffManifest'
|
||||
import type { ClaudeSessionFiles } from './sessionBundle'
|
||||
import { promises as fsp } from 'node:fs'
|
||||
import * as path from 'node:path'
|
||||
@@ -19,7 +19,6 @@ export interface HandoffSource {
|
||||
branch?: string
|
||||
worktreePath?: string
|
||||
sessions: HandoffSessions
|
||||
profiles: HandoffProfiles
|
||||
}
|
||||
|
||||
export interface HandoffRoots {
|
||||
@@ -106,7 +105,6 @@ export async function stageHandoffBundle(
|
||||
...(src.branch ? { branch: src.branch } : {}),
|
||||
...(src.worktreePath ? { worktreePath: src.worktreePath } : {}),
|
||||
sessions: src.sessions,
|
||||
profiles: src.profiles,
|
||||
claude,
|
||||
codex,
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ const good = {
|
||||
createdAt: '2026-08-26T00:00:00Z',
|
||||
branch: 'feature/x',
|
||||
sessions: { implementSessionId: 'a' },
|
||||
profiles: {},
|
||||
claude: [{ id: UUID, kind: 'implement' }],
|
||||
codex: [],
|
||||
}
|
||||
@@ -57,4 +56,10 @@ describe('parseHandoffManifest', () => {
|
||||
it('不带 worktreePath → 结果里没有这个 key', () => {
|
||||
expect(parseHandoffManifest(JSON.stringify(good), 42)).not.toHaveProperty('worktreePath')
|
||||
})
|
||||
it('旧会话包带 profiles → 丢掉,接管方拿不到发送方机器上的 profile 路径', () => {
|
||||
const old = { ...good, profiles: { brainstormProfilePath: '/Users/chehongweo/Sources/cruldra-profile/claude-config/profiles/gpt-6.json' } }
|
||||
const parsed = parseHandoffManifest(JSON.stringify(old), 42)
|
||||
expect(parsed).not.toHaveProperty('profiles')
|
||||
expect(parsed).toEqual(good)
|
||||
})
|
||||
})
|
||||
@@ -15,13 +15,6 @@ export interface HandoffSessions {
|
||||
reviewSessionId?: string
|
||||
}
|
||||
|
||||
export interface HandoffProfiles {
|
||||
profilePath?: string
|
||||
brainstormProfilePath?: string
|
||||
testProfilePath?: string
|
||||
prototypeProfilePath?: string
|
||||
}
|
||||
|
||||
export interface HandoffManifest {
|
||||
version: 1
|
||||
issue: number
|
||||
@@ -30,7 +23,6 @@ export interface HandoffManifest {
|
||||
branch?: string
|
||||
worktreePath?: string
|
||||
sessions: HandoffSessions
|
||||
profiles: HandoffProfiles
|
||||
claude: Array<{ id: string, kind: ClaudeSessionKind }>
|
||||
codex: Array<{ id: string, relPath: string }>
|
||||
}
|
||||
@@ -68,7 +60,6 @@ export function parseHandoffManifest(json: string, expectedIssue: number): Hando
|
||||
if (!from || !createdAt)
|
||||
throw new Error('handoff.json 缺少 from / createdAt')
|
||||
const sessionsRaw = (raw.sessions ?? {}) as Record<string, unknown>
|
||||
const profilesRaw = (raw.profiles ?? {}) as Record<string, unknown>
|
||||
const claudeRaw = Array.isArray(raw.claude) ? raw.claude : []
|
||||
const codexRaw = Array.isArray(raw.codex) ? raw.codex : []
|
||||
const claude = claudeRaw.map((e) => {
|
||||
@@ -93,12 +84,6 @@ export function parseHandoffManifest(json: string, expectedIssue: number): Hando
|
||||
if (v)
|
||||
sessions[k] = v
|
||||
}
|
||||
const profiles: HandoffProfiles = {}
|
||||
for (const k of ['profilePath', 'brainstormProfilePath', 'testProfilePath', 'prototypeProfilePath'] as const) {
|
||||
const v = optString(profilesRaw[k])
|
||||
if (v)
|
||||
profiles[k] = v
|
||||
}
|
||||
return {
|
||||
version: 1,
|
||||
issue: expectedIssue,
|
||||
@@ -107,7 +92,6 @@ export function parseHandoffManifest(json: string, expectedIssue: number): Hando
|
||||
...(optString(raw.branch) ? { branch: optString(raw.branch) } : {}),
|
||||
...(optString(raw.worktreePath) ? { worktreePath: optString(raw.worktreePath) } : {}),
|
||||
sessions,
|
||||
profiles,
|
||||
claude,
|
||||
codex,
|
||||
}
|
||||
|
||||
@@ -12,16 +12,21 @@ import type { Issue } from '../gitea/types'
|
||||
import * as fs from 'node:fs'
|
||||
import { resolveWorktreePath } from '../git/worktree'
|
||||
|
||||
/** 工单上选的各类会话用哪份模型配置(profile 文件路径),是这台机器自己的选择,工单移交时既不带走也不清掉。 */
|
||||
export const LOCAL_PROFILE_FIELDS = [
|
||||
'profilePath',
|
||||
'brainstormProfilePath',
|
||||
'testProfilePath',
|
||||
'prototypeProfilePath',
|
||||
] as const
|
||||
|
||||
export const LOCAL_STATE_FIELDS = [
|
||||
'sessionId',
|
||||
'implementSessionId',
|
||||
'reviewSessionId',
|
||||
'testSessionId',
|
||||
'prototypeSessionId',
|
||||
'profilePath',
|
||||
'brainstormProfilePath',
|
||||
'testProfilePath',
|
||||
'prototypeProfilePath',
|
||||
...LOCAL_PROFILE_FIELDS,
|
||||
'worktreePath',
|
||||
'prDiffFile',
|
||||
] as const
|
||||
|
||||
@@ -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
|
||||
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