✨ 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)
This commit is contained in:
1 parent
fb505d59c5
commit
8c5cc90eda
11 files changed
+387
-37
No files matched your search
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
Reference in new issue
Block a user