右栏 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)
114 lines
3.5 KiB
TypeScript
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)
|
|
}
|