fix(organize): 跨卷 Move 删源前全量校验、计划期 sha256 缓存、补全元数据带超时
- 跨卷移动删源前一律全量 sha256(不再抽样); - 计划期同尺寸目标的全量 sha256 按 (路径, 大小, 修改时间, inode) 缓存,向导预览 / 复核 / 执行不重复算; - metadata.resolve_organize 传 3 分钟超时的 signal; - 季号推断已由解析器统一推定(seasonInferred),补回归用例。 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
1 parent
4b49be7228
commit
d129a96110
5 files changed
+130
-11
No files matched your search
@@ -771,14 +771,8 @@ export async function transferFile(
|
||||
},
|
||||
};
|
||||
}
|
||||
const r = await px.crossDeviceMove(
|
||||
src,
|
||||
dst,
|
||||
{
|
||||
hash: d.ports.options.verifyHash,
|
||||
},
|
||||
call,
|
||||
);
|
||||
// 跨卷移动删源前一律全量 sha256 校验(抽样校验不够当删源依据;只影响跨卷 Move,复制 / 同卷仍按 verifyHash)
|
||||
const r = await px.crossDeviceMove(src, dst, { hash: "sha256" }, call);
|
||||
const failedStep = r.steps.find((st) => st.status === "failed");
|
||||
return {
|
||||
status: r.status === "completed" ? "succeeded" : r.status,
|
||||
|
||||
@@ -383,7 +383,8 @@ export async function planImport(d: AoDeps, req: ImportRequest): Promise<PlanRes
|
||||
actorId: req.actorId ?? "system:organize",
|
||||
taskId: req.taskId ?? `organize:${dt?.id ?? rel.id}`,
|
||||
scopeIds: [],
|
||||
signal: new AbortController().signal,
|
||||
// 带超时:上游(模型 / 网页搜索)挂住时不能一直占着 file 道
|
||||
signal: AbortSignal.timeout(RESOLVE_ORGANIZE_TIMEOUT_MS),
|
||||
source: "task",
|
||||
},
|
||||
)) as { mediaEntityId?: string | null; mode?: "tmdb" | "local" | null };
|
||||
@@ -855,9 +856,12 @@ export async function planImport(d: AoDeps, req: ImportRequest): Promise<PlanRes
|
||||
const e: ExistingTarget = { path: f.dst, size: st.facts.size };
|
||||
if (st.facts.size === f.size) {
|
||||
// 同尺寸已存在的目标:全量 sha256 判定是否同一文件(子进程,见 adapters sha256Full)
|
||||
e.sha256 = (await sha256Full(probe)).digest;
|
||||
e.sha256 = await cachedSha256(probe, st.facts);
|
||||
const src = files.find((x) => x.path === f.src);
|
||||
if (src) src.sha256 = (await sha256Full(src.path)).digest;
|
||||
if (src) {
|
||||
const ss = await statSafe(src.path);
|
||||
src.sha256 = ss.kind === "ok" ? await cachedSha256(src.path, ss.facts) : (await sha256Full(src.path)).digest;
|
||||
}
|
||||
}
|
||||
existing.push(e);
|
||||
}
|
||||
@@ -1236,6 +1240,32 @@ export async function emitRecord(d: AoDeps, exec: Exec, recordId: string): Promi
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 计划期全量 sha256 按 (路径, 大小, 修改时间, inode) 缓存:向导预览 / 复核 / 执行各算一遍同一对大文件时只算一次。
|
||||
* 文件被改动(大小 / 修改时间 / inode 变)就重算;只留最近的 SHA_CACHE_MAX 条。
|
||||
*/
|
||||
const SHA_CACHE_MAX = 2000;
|
||||
const shaCache = new Map<string, string>();
|
||||
export async function cachedSha256(
|
||||
path: string,
|
||||
facts: { size: number; mtimeMs: number; ino?: number | bigint | null },
|
||||
): Promise<string> {
|
||||
const key = `${path}\u0000${facts.size}\u0000${facts.mtimeMs}\u0000${facts.ino ?? ""}`;
|
||||
const hit = shaCache.get(key);
|
||||
if (hit) {
|
||||
shaCache.delete(key);
|
||||
shaCache.set(key, hit);
|
||||
return hit;
|
||||
}
|
||||
const digest = (await sha256Full(path)).digest;
|
||||
shaCache.set(key, digest);
|
||||
if (shaCache.size > SHA_CACHE_MAX) shaCache.delete(shaCache.keys().next().value!);
|
||||
return digest;
|
||||
}
|
||||
|
||||
/** 整理时补全元数据(metadata.resolve_organize:模型 + 网页搜索)的超时 */
|
||||
export const RESOLVE_ORGANIZE_TIMEOUT_MS = 3 * 60_000;
|
||||
|
||||
/** 作品标题 / 原名 / 别名 → 这批视频里标题明显不是本作品的(路径 → 文件标题) */
|
||||
async function fileTitleMismatches(
|
||||
d: AoDeps,
|
||||
|
||||
Reference in new issue
Block a user