✨ feat(vscode): 审查会话链接按本机 codex 会话文件存在判定,不再绑 worktree

This commit is contained in:
cruldra committed 2026-08-19 19:24:57 +08:00
1 parent bf3f7f9e67
commit 6251ba0d2a
12 files changed
+190 -21

No files matched your search

+85
View File
@@ -0,0 +1,85 @@
import type { Issue } from '../gitea/types'
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import * as os from 'node:os'
import * as path from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { annotateReviewSessionFileExists, listCodexSessionIds } from './codexSessions'
const tempDirs: string[] = []
afterEach(() => {
while (tempDirs.length > 0) {
const dir = tempDirs.pop()
if (dir)
rmSync(dir, { recursive: true, force: true })
}
})
function tempDir(prefix: string): string {
const dir = mkdtempSync(path.join(os.tmpdir(), prefix))
tempDirs.push(dir)
return dir
}
const UUID_A = '019dcb27-58a6-70a1-a5d1-bfc7f3ed9d0a'
const UUID_B = '11111111-2222-4333-8444-555555555555'
function writeRollout(baseDir: string, ymd: [string, string, string], uuid: string): void {
const dayDir = path.join(baseDir, ...ymd)
mkdirSync(dayDir, { recursive: true })
writeFileSync(path.join(dayDir, `rollout-2026-04-27T02-57-26-${uuid}.jsonl`), '{}\n')
}
describe('listCodexSessionIds', () => {
it('扫年/月/日三层目录,从文件名尾部提取全部会话 id', async () => {
const base = tempDir('codex-sessions-')
writeRollout(base, ['2026', '04', '27'], UUID_A)
writeRollout(base, ['2026', '05', '01'], UUID_B)
const ids = await listCodexSessionIds(base)
expect(ids).toEqual(new Set([UUID_A, UUID_B]))
})
it('忽略不符合 rollout-*-<uuid>.jsonl 的文件', async () => {
const base = tempDir('codex-sessions-')
const dayDir = path.join(base, '2026', '04', '27')
mkdirSync(dayDir, { recursive: true })
writeFileSync(path.join(dayDir, 'notes.txt'), 'x')
writeFileSync(path.join(dayDir, 'rollout-2026-04-27T02-57-26-not-a-uuid.jsonl'), '{}\n')
writeRollout(base, ['2026', '04', '27'], UUID_A)
const ids = await listCodexSessionIds(base)
expect(ids).toEqual(new Set([UUID_A]))
})
it('目录不存在返回空 Set', async () => {
const ids = await listCodexSessionIds(path.join(tempDir('codex-sessions-'), 'nope'))
expect(ids.size).toBe(0)
})
})
describe('annotateReviewSessionFileExists', () => {
const baseIssue = { number: 1, title: 't', column: 'review', htmlUrl: '' } as unknown as Issue
it('有会话文件的 issue 标 true,没有的标 false,无 reviewSessionId 的不加字段', async () => {
const base = tempDir('codex-sessions-')
writeRollout(base, ['2026', '04', '27'], UUID_A)
const issues: Issue[] = [
{ ...baseIssue, number: 1, reviewSessionId: UUID_A },
{ ...baseIssue, number: 2, reviewSessionId: UUID_B },
{ ...baseIssue, number: 3 },
]
const out = await annotateReviewSessionFileExists(issues, base)
expect(out[0].reviewSessionFileExists).toBe(true)
expect(out[1].reviewSessionFileExists).toBe(false)
expect('reviewSessionFileExists' in out[2]).toBe(false)
})
it('id 大小写不影响判定', async () => {
const base = tempDir('codex-sessions-')
writeRollout(base, ['2026', '04', '27'], UUID_A)
const out = await annotateReviewSessionFileExists(
[{ ...baseIssue, reviewSessionId: UUID_A.toUpperCase() }],
base,
)
expect(out[0].reviewSessionFileExists).toBe(true)
})
})
+55
View File
@@ -0,0 +1,55 @@
/**
* 枚举本机 codex 会话文件,判定「审查会话id」能否 resume。
*
* codex 把每个会话的 transcript 全局存在
* `~/.codex/sessions/YYYY/MM/DD/rollout-<ISO>-<uuid>.jsonl`,resume 只依赖
* 这个文件,与 worktree 无关——多人协作下 assignee 换机后,只要把会话文件
* 拷到目标机器就能 resume。
*/
import type { Issue } from '../gitea/types'
import { promises as fsp } from 'node:fs'
import * as os from 'node:os'
import * as path from 'node:path'
import { ROLLOUT_UUID_REGEX } from '../cc/codexSessionWatcher'
/** codex 会话根目录(全局,不随工作区变化)。 */
export function defaultCodexSessionsDir(): string {
return path.join(os.homedir(), '.codex', 'sessions')
}
/**
* 扫一遍 sessions 根目录(年/月/日三层),从文件名尾部提取所有会话 id
* 收进 Set(统一小写)。目录不存在或读取失败返回空 Set——调用方按
* 「本机没有会话文件」处理即可。
*/
export async function listCodexSessionIds(baseDir: string = defaultCodexSessionsDir()): Promise<Set<string>> {
const ids = new Set<string>()
let entries: string[]
try {
entries = await fsp.readdir(baseDir, { recursive: true })
}
catch {
return ids
}
for (const entry of entries) {
const m = path.basename(entry).match(ROLLOUT_UUID_REGEX)
if (m)
ids.add(m[1].toLowerCase())
}
return ids
}
/**
* 给带 reviewSessionId 的工单附加计算字段 `reviewSessionFileExists`
* (不落任何持久化)。整批工单只扫一次 sessions 目录;没有任何工单带
* id 时连扫都不扫。
*/
export async function annotateReviewSessionFileExists(issues: Issue[], baseDir?: string): Promise<Issue[]> {
if (!issues.some(i => typeof i.reviewSessionId === 'string' && i.reviewSessionId.length > 0))
return issues
const ids = await listCodexSessionIds(baseDir)
return issues.map(i => (i.reviewSessionId
? { ...i, reviewSessionFileExists: ids.has(i.reviewSessionId.toLowerCase()) }
: i))
}