Files
superwork/vscode/src/sessions/prFileSummaryStore.ts
T
cruldra 8c5cc90eda ✨ feat(vscode): 「改动」面板加审查勾 + deepseek 增量生成文件改动说明
右栏 diff 头部加一组按钮:
- 审查对勾:与左树同一状态源,勾上则文件路径标题置灰
- ✨ 一键调 deepseek 生成该文件「一句话改动目的」

增量复用会话:首个文件开新 cc 会话并把 sessionId 记到
.spx/pr-file-summaries.json,后续文件 --resume 接上去,省去每次重新
理解 PR 的开销;resume 失败则丢弃旧 id 重开一次。说明也落盘(跨 push
保留),重载后仍显示。

- spawnClaude 增 resumeSessionId(--resume)
- 新增 prFileSummaryStore;handler handleGeneratePrFileSummary
  用 jsdiff 拼 unified diff 喂模型
- 依赖 diff(jsdiff)
2026-06-29 15:24:43 +08:00

114 lines
3.5 KiB
TypeScript

/**
* Workspace 级「PR 文件改动说明 + 续会话 id」持久化。
*
* 落盘到 `<workspace>/.spx/pr-file-summaries.json`,形状 `Record<string, string>`。
* 用前缀给两类值分命名空间,避免「文件路径」与「会话 key」相撞:
* - `sum:${issueNumber}:${path}` → 该文件一句话改动说明(跨 push 保留,不随 head sha 失效)。
* - `ses:${issueNumber}` → 该工单最近一次 deepseek 生成会话的 sessionId,供下次 `--resume`。
*
* 文件不存在或解析失败时按空处理。
*/
import { promises as fsp } from 'node:fs'
import * as path from 'node:path'
type SummaryMap = Record<string, string>
function summaryFile(workspaceRoot: string): string {
return path.join(workspaceRoot, '.spx', 'pr-file-summaries.json')
}
function summaryKey(issueNumber: number, filePath: string): string {
return `sum:${issueNumber}:${filePath}`
}
function sessionKey(issueNumber: number): string {
return `ses:${issueNumber}`
}
/** 读出整张映射;文件不存在或解析失败返回空对象。 */
async function readMap(workspaceRoot: string): Promise<SummaryMap> {
let raw: string
try {
raw = await fsp.readFile(summaryFile(workspaceRoot), 'utf8')
}
catch {
return {}
}
try {
const parsed = JSON.parse(raw) as unknown
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
return {}
const out: SummaryMap = {}
for (const [k, v] of Object.entries(parsed as Record<string, unknown>)) {
if (typeof v === 'string')
out[k] = v
}
return out
}
catch {
return {}
}
}
async function writeMap(workspaceRoot: string, map: SummaryMap): Promise<void> {
const dir = path.join(workspaceRoot, '.spx')
await fsp.mkdir(dir, { recursive: true })
await fsp.writeFile(summaryFile(workspaceRoot), `${JSON.stringify(map, null, 2)}\n`, 'utf8')
}
/** 读某工单全部已生成的文件说明:`{ [path]: summary }`。无则空对象。 */
export async function readFileSummaries(
workspaceRoot: string,
issueNumber: number,
): Promise<Record<string, string>> {
const map = await readMap(workspaceRoot)
const prefix = `sum:${issueNumber}:`
const out: Record<string, string> = {}
for (const [k, v] of Object.entries(map)) {
if (k.startsWith(prefix))
out[k.slice(prefix.length)] = v
}
return out
}
/** 写某文件的说明。空串则删除该 key。 */
export async function writeFileSummary(
workspaceRoot: string,
issueNumber: number,
filePath: string,
summary: string,
): Promise<void> {
const map = await readMap(workspaceRoot)
const key = summaryKey(issueNumber, filePath)
if (summary.length > 0)
map[key] = summary
else
delete map[key]
await writeMap(workspaceRoot, map)
}
/** 读某工单最近一次生成会话的 sessionId;无则 undefined。 */
export async function readSummarySession(
workspaceRoot: string,
issueNumber: number,
): Promise<string | undefined> {
const map = await readMap(workspaceRoot)
return map[sessionKey(issueNumber)] || undefined
}
/** 写某工单续会话 sessionId。传 undefined / 空串则删除(下次从头开会话)。 */
export async function writeSummarySession(
workspaceRoot: string,
issueNumber: number,
sessionId: string | undefined,
): Promise<void> {
const map = await readMap(workspaceRoot)
const key = sessionKey(issueNumber)
if (sessionId && sessionId.length > 0)
map[key] = sessionId
else
delete map[key]
await writeMap(workspaceRoot, map)
}