♻️ fix(vscode): 「已查看」改为按文件变更指纹失效,PR 新 push 只摘变动文件的勾

原先按 head sha 存已查看,作者一推新提交 head sha 变了,全部勾一起清空——
哪怕只改了一个文件也把没动的勾全冲掉。改成 GitHub「Viewed」式的内容感知:
按每个文件的变更指纹 (status|additions|deletions) 存,拉文件时与当前指纹比对,
只有真改动过的文件掉勾,没动的保留;比对后顺手剪掉盘上失配/消失的条目。

Gitea 的 ChangedFile 不返回 blob sha、compare 端点也不支持任意 commit sha,
故用上述三元组作指纹(文件一改几乎必变其一),零额外请求。

- prReviewStore: 形状改为 issueNumber → {path: sig},导出 read/writeConfirmedSigs
- prFiles: handleGetPrFiles 按指纹过滤+剪枝,顺带去掉不再需要的 getPullRequest 调用
- 消息 pr-files/show 去掉无人消费的 headSha/mergeBase;pr-review/set 改发 {path: sig}
- usePrFiles: 删 headSha,setConfirmed 把路径映射成指纹再发
This commit is contained in:
cruldra committed 2026-06-29 16:55:11 +08:00
1 parent bd8ba714b3
commit 35085f72f7
6 files changed
+96 -75

No files matched your search

+34 -27
View File
@@ -1,27 +1,31 @@
/**
* Workspace 级「PR 审查已确认文件」持久化。
* Workspace 级「PR 审查已查看文件」持久化。
*
* 落盘到 `<workspace>/.spx/pr-review-confirmed.json`,形状
* `Record<string, string[]>`:key = `${issueNumber}:${sha}`(sha 取 PR 的 head
* sha,故新 push 换 key、已查看进度自然重置),value = 该 PR 里已确认(已审阅)
* 的文件完整路径数组。目录不单独记录——目录是否确认靠「其下可见文件是否全已
* 确认」推导。文件不存在或解析失败时按空处理。
* `Record<string, Record<string, string>>`:外层 key = `${issueNumber}`,内层
* `{ [filePath]: sig }`,sig 是该文件被标记已查看时记下的「变更指纹」。
*
* 为什么用指纹而非 blob sha:要做到 GitHub 式「文件没变就一直算已查看,只有它在新
* push 里真改动了才重新标未查看」,需要一个能判定文件是否变过的标识。但 Gitea 的
* ChangedFile 不返回每文件 blob sha、compare 端点也不支持任意 commit sha 比较,拿
* 不到现成的内容标识。故退而用 (status|additions|deletions) 作指纹——文件一旦改动,
* 这三者几乎必然变其一,据此判断是否需要重看,够用且零额外请求。
*
* 目录不单独记录——目录是否已查看靠「其下可见文件是否全已查看」推导。文件不存在或
* 解析失败时按空处理。
*/
import { promises as fsp } from 'node:fs'
import * as path from 'node:path'
type ConfirmedMap = Record<string, string[]>
/** 外层 key = issueNumber 字符串;内层 = 文件路径 → 变更指纹。 */
type ConfirmedMap = Record<string, Record<string, string>>
function confirmedFile(workspaceRoot: string): string {
return path.join(workspaceRoot, '.spx', 'pr-review-confirmed.json')
}
function makeKey(issueNumber: number, sha: string): string {
return `${issueNumber}:${sha}`
}
/** 读出整个映射;文件不存在或解析失败返回空对象。 */
/** 读出整个映射;文件不存在、解析失败或形状不符返回空对象。 */
async function readMap(workspaceRoot: string): Promise<ConfirmedMap> {
const file = confirmedFile(workspaceRoot)
let raw: string
@@ -37,8 +41,14 @@ async function readMap(workspaceRoot: string): Promise<ConfirmedMap> {
return {}
const out: ConfirmedMap = {}
for (const [key, value] of Object.entries(parsed as Record<string, unknown>)) {
if (Array.isArray(value))
out[key] = value.filter((p): p is string => typeof p === 'string')
if (!value || typeof value !== 'object' || Array.isArray(value))
continue
const sigs: Record<string, string> = {}
for (const [p, sig] of Object.entries(value as Record<string, unknown>)) {
if (typeof sig === 'string')
sigs[p] = sig
}
out[key] = sigs
}
return out
}
@@ -48,32 +58,29 @@ async function readMap(workspaceRoot: string): Promise<ConfirmedMap> {
}
/**
* 读取某 (issueNumber, sha) 下已确认的文件路径数组。文件缺失、解析失败或无该
* key 时返回 `[]`。
* 读取某工单已查看文件的「路径 → 指纹」映射。文件缺失、解析失败或无该工单时返回 `{}`。
*/
export async function readConfirmed(
export async function readConfirmedSigs(
workspaceRoot: string,
issueNumber: number,
sha: string,
): Promise<string[]> {
): Promise<Record<string, string>> {
const map = await readMap(workspaceRoot)
return map[makeKey(issueNumber, sha)] ?? []
return map[String(issueNumber)] ?? {}
}
/**
* 写入某 (issueNumber, sha) 下已确认的文件路径数组。空数组则删除该 key;非空则
* 设值。父目录不存在会自动创建。
* 写入某工单已查看文件的「路径 → 指纹」映射。空映射则删除该工单 key;非空则设值。
* 父目录不存在会自动创建。
*/
export async function writeConfirmed(
export async function writeConfirmedSigs(
workspaceRoot: string,
issueNumber: number,
sha: string,
paths: string[],
sigs: Record<string, string>,
): Promise<void> {
const map = await readMap(workspaceRoot)
const key = makeKey(issueNumber, sha)
if (paths.length > 0)
map[key] = paths
const key = String(issueNumber)
if (Object.keys(sigs).length > 0)
map[key] = sigs
else
delete map[key]
const dir = path.join(workspaceRoot, '.spx')