feat(fs): W4-E4 fsproxy hang isolation, partial sweep, replace-with-verify, TIMEOUT retry

- adapters/fs: FsProxy + fsworker run each file op in a killable subprocess;
  progress-stall watchdog (FS_HANG_TIMEOUT_MS) for copy/hash, total timeout for
  quick ops; SIGTERM -> SIGKILL -> FsError('TIMEOUT'); stat/checkRoot timeouts
  never read as missing
- copyWithVerify: onExisting conflict/skip/replace (hardlink backup + atomic
  rename, old file to .umt-trash or deleted), sweep stale partials before write,
  document why permissions/owner are not copied
- filesMatch/filesMatchSha256 throw FsError on read failure instead of
  match=false; crossDeviceMove skips redundant re-hash after sha256 copy
- FsErrorCode TIMEOUT + fsErrorClass; organize executeRecord routes fs ops
  through fsproxy, retries TIMEOUT/IO up to FS_TRANSFER_MAX_ATTEMPTS then opens
  a decision without deleting the record; reconcile errors no longer reported
  as EEXIST
- domain: subtitle .default marker (ORGANIZE_DEFAULT_SUBTITLE_LANG) and
  findOtherVersions (same season/episode/part, video only) as upgrade
  disposal candidates
- scripts/bench/fs-bench.ts and benchmark record in porting doc

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-27 23:15:05 +08:00
1 parent 36dc048e89
commit 2e2d56184c
21 files changed
+1970 -179

No files matched your search

+17 -3
View File
@@ -67,8 +67,22 @@ export function sidecarStem(name: string): { stem: string; tags: string[] } {
return { stem: stem.toLowerCase(), tags };
}
/** 目标字幕后缀(对照 MP:繁体优先判定 → .zh-tw;简体 → .chi.zh-cn;英 → .eng;日 → .ja) */
export function sidecarLanguageSuffix(tags: readonly string[]): string {
/** 默认字幕语言(对照 MP DEFAULT_SUB):命中的随行字幕加 .default,Jellyfin/Plex/Emby 都认 */
export type DefaultSubtitleLang = "zh-cn" | "zh-tw" | "en" | "ja" | "ko";
const DEFAULT_LANG_SUFFIX: Record<DefaultSubtitleLang, string> = {
"zh-cn": ".chi.zh-cn",
"zh-tw": ".zh-tw",
en: ".eng",
ja: ".ja",
ko: ".ko",
};
/**
* 目标字幕后缀(对照 MP:繁体优先判定 → .zh-tw;简体 → .chi.zh-cn;英 → .eng;日 → .ja)。
* 源文件自带 .default,或语言与 defaultLang 一致时加 .default。
*/
export function sidecarLanguageSuffix(tags: readonly string[], defaultLang?: DefaultSubtitleLang): string {
const langs = tags.map((t) => LANG_TAG[t]);
let s = "";
if (langs.includes("zh-Hant")) s = ".zh-tw";
@@ -76,7 +90,7 @@ export function sidecarLanguageSuffix(tags: readonly string[]): string {
else if (langs.includes("en")) s = ".eng";
else if (langs.includes("ja")) s = ".ja";
else if (langs.includes("ko")) s = ".ko";
if (tags.includes("default")) s += ".default";
if (tags.includes("default") || (defaultLang && s !== "" && s === DEFAULT_LANG_SUFFIX[defaultLang])) s += ".default";
if (tags.includes("forced")) s += ".forced";
return s;
}