✨ feat(vscode): sessionBundle 会话文件查找/安装/去重原语

Claude-Session: https://claude.ai/code/session_011cEyL6k351U2BzX1Qmygph
This commit is contained in:
cruldra committed 2026-08-26 15:44:00 +08:00
1 parent ad82808dd0
commit 569f24c11a
2 files changed
+247

No files matched your search

+122
View File
@@ -0,0 +1,122 @@
/**
* claude / codex 会话文件的搬运原语。
*
* claude 会话 = `<projectsDir>/<sid>.jsonl` + 可选 `<projectsDir>/<sid>/`
* (subagents、tool-results),两者必须一起搬。新版 claude 在任意目录
* `--resume` 时会搜全部 project 目录,同一 sid 出现两份就拒绝 resume——
* 所以「安装」总是先拷再删本机其它副本。
*/
import { promises as fsp } from 'node:fs'
import * as os from 'node:os'
import * as path from 'node:path'
import { ROLLOUT_UUID_REGEX } from './codexSessionWatcher'
export interface ClaudeSessionFiles {
projectsDir: string
jsonl: string
dir?: string
}
export function claudeProjectsRoot(): string {
return path.join(os.homedir(), '.claude', 'projects')
}
async function exists(p: string): Promise<boolean> {
try {
await fsp.stat(p)
return true
}
catch {
return false
}
}
async function sessionFilesIn(projectsDir: string, sid: string): Promise<ClaudeSessionFiles | undefined> {
const jsonl = path.join(projectsDir, `${sid}.jsonl`)
if (!(await exists(jsonl)))
return undefined
const dir = path.join(projectsDir, sid)
return (await exists(dir)) ? { projectsDir, jsonl, dir } : { projectsDir, jsonl }
}
export async function listClaudeSessionCopies(sid: string, projectsRoot: string): Promise<ClaudeSessionFiles[]> {
let entries: string[]
try {
entries = await fsp.readdir(projectsRoot)
}
catch {
return []
}
const out: ClaudeSessionFiles[] = []
for (const name of entries) {
const found = await sessionFilesIn(path.join(projectsRoot, name), sid)
if (found)
out.push(found)
}
return out
}
export async function findClaudeSessionFiles(sid: string, projectsRoot: string): Promise<ClaudeSessionFiles | undefined> {
const copies = await listClaudeSessionCopies(sid, projectsRoot)
if (copies.length === 0)
return undefined
const stamped = await Promise.all(copies.map(async c => ({ c, mtime: (await fsp.stat(c.jsonl)).mtimeMs })))
stamped.sort((a, b) => b.mtime - a.mtime)
return stamped[0].c
}
export async function copyClaudeSessionFiles(files: ClaudeSessionFiles, dstDir: string): Promise<void> {
await fsp.mkdir(dstDir, { recursive: true })
await fsp.copyFile(files.jsonl, path.join(dstDir, path.basename(files.jsonl)))
if (files.dir)
await fsp.cp(files.dir, path.join(dstDir, path.basename(files.dir)), { recursive: true, force: true })
}
async function removeClaudeSessionFiles(files: ClaudeSessionFiles): Promise<void> {
await fsp.rm(files.jsonl, { force: true })
if (files.dir)
await fsp.rm(files.dir, { recursive: true, force: true })
}
export async function installClaudeSession(opts: {
sid: string
srcDir: string
dstProjectsDir: string
projectsRoot: string
}): Promise<void> {
const src = await sessionFilesIn(path.resolve(opts.srcDir), opts.sid)
if (!src)
throw new Error(`源目录缺少会话文件 ${opts.sid}.jsonl:${opts.srcDir}`)
const dst = path.resolve(opts.dstProjectsDir)
if (src.projectsDir !== dst)
await copyClaudeSessionFiles(src, dst)
for (const copy of await listClaudeSessionCopies(opts.sid, opts.projectsRoot)) {
if (path.resolve(copy.projectsDir) !== dst)
await removeClaudeSessionFiles(copy)
}
}
/** codex rollout 全局存放,按文件名尾部 uuid 定位;返回相对 codexRoot 的路径。 */
export async function codexRolloutRelPathFor(sid: string, codexRoot: string): Promise<string | undefined> {
const want = sid.toLowerCase()
let entries: string[]
try {
entries = await fsp.readdir(codexRoot, { recursive: true })
}
catch {
return undefined
}
for (const entry of entries) {
const m = path.basename(entry).match(ROLLOUT_UUID_REGEX)
if (m && m[1].toLowerCase() === want)
return entry
}
return undefined
}
export async function installCodexSession(relPath: string, srcFile: string, codexRoot: string): Promise<void> {
const dst = path.join(codexRoot, relPath)
await fsp.mkdir(path.dirname(dst), { recursive: true })
await fsp.copyFile(srcFile, dst)
}