97 lines
2.6 KiB
TypeScript
97 lines
2.6 KiB
TypeScript
/**
|
||
* 从 Claude projects 目录扫描已 /rename(jsonl 含 custom-title)且尚未登记到
|
||
* managed store 的会话,供「导入」候选列表使用。
|
||
*/
|
||
|
||
import { promises as fsp } from 'node:fs'
|
||
import * as path from 'node:path'
|
||
import { projectsDirFor } from '../cc/sessionWatcher'
|
||
import { readManagedSessions } from './managedStore'
|
||
|
||
export interface ImportableNamedSession {
|
||
id: string
|
||
name: string
|
||
mtimeMs: number
|
||
}
|
||
|
||
/** 从 jsonl 文本提取最后一条 custom-title(trim 后非空),否则 null */
|
||
export function extractLastCustomTitle(jsonlText: string): string | null {
|
||
let last: string | null = null
|
||
const lines = jsonlText.split(/\r?\n/)
|
||
for (const line of lines) {
|
||
const trimmed = line.trim()
|
||
if (!trimmed)
|
||
continue
|
||
let parsed: unknown
|
||
try {
|
||
parsed = JSON.parse(trimmed)
|
||
}
|
||
catch {
|
||
continue
|
||
}
|
||
if (!parsed || typeof parsed !== 'object')
|
||
continue
|
||
const rec = parsed as { type?: unknown, customTitle?: unknown }
|
||
if (rec.type !== 'custom-title')
|
||
continue
|
||
if (typeof rec.customTitle !== 'string')
|
||
continue
|
||
const title = rec.customTitle.trim()
|
||
if (!title)
|
||
continue
|
||
last = title
|
||
}
|
||
return last
|
||
}
|
||
|
||
/**
|
||
* 列出可导入命名会话:扫 projectsDir 顶层 *.jsonl,过滤已在 managed 的 id,mtime 降序。
|
||
* `projectsDir` 可选,便于测试注入;默认 `projectsDirFor(workspaceRoot)`。
|
||
*/
|
||
export async function listImportableNamedSessions(
|
||
workspaceRoot: string,
|
||
projectsDir?: string,
|
||
): Promise<ImportableNamedSession[]> {
|
||
const dir = projectsDir ?? projectsDirFor(workspaceRoot)
|
||
let entries: import('node:fs').Dirent[]
|
||
try {
|
||
entries = await fsp.readdir(dir, { withFileTypes: true })
|
||
}
|
||
catch {
|
||
return []
|
||
}
|
||
|
||
const managed = await readManagedSessions(workspaceRoot)
|
||
const managedIds = new Set(managed.sessions.map(s => s.id))
|
||
|
||
const results: ImportableNamedSession[] = []
|
||
for (const ent of entries) {
|
||
if (!ent.isFile() || !ent.name.endsWith('.jsonl'))
|
||
continue
|
||
const id = ent.name.slice(0, -'.jsonl'.length)
|
||
if (!id || managedIds.has(id))
|
||
continue
|
||
const filePath = path.join(dir, ent.name)
|
||
let text: string
|
||
let mtimeMs: number
|
||
try {
|
||
const [raw, st] = await Promise.all([
|
||
fsp.readFile(filePath, 'utf8'),
|
||
fsp.stat(filePath),
|
||
])
|
||
text = raw
|
||
mtimeMs = st.mtimeMs
|
||
}
|
||
catch {
|
||
continue
|
||
}
|
||
const name = extractLastCustomTitle(text)
|
||
if (!name)
|
||
continue
|
||
results.push({ id, name, mtimeMs })
|
||
}
|
||
|
||
results.sort((a, b) => b.mtimeMs - a.mtimeMs)
|
||
return results
|
||
}
|