docs(storage): Alist 使用与真机验收 runbook,整理 / 文件管理 / MP 对照更新

- 新增 docs/runbook/storage-alist.md:登记、行为要点、临时 OpenList 容器真机验收步骤、排查
- organize.md 加「Alist / OpenList 存储」;moviepilot-filemanager.md 末节改为已做;
  v3 对照保留 rclone 不照抄、网盘改为 Alist 已接;file-manager-v2 存储切换与目的存储两行更新
- 设计文档补实现落点与偏离;矩阵测试补原地整理与目标已存在替换

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-07 00:04:19 +08:00
1 parent fed2202e74
commit adbb9b59a0
7 files changed
+225 -8

No files matched your search

@@ -253,6 +253,69 @@ describe.skipIf(skip)("整理矩阵:本地 ↔ Alist ↔ Alist", () => {
a.mock.failNext(0);
});
it("目标已存在选替换:旧文件移进 Alist 上的回收站,新文件上传到位", async () => {
const f = await film("local");
const first = await organize(f, "copy", alib.libraryRootId);
const target = `/s1/library/${first.main.target_path}`;
a.mock.put(target, "old-version-content");
await writeFile(join(h.dl, f.path), `${f.body}-v2`);
const items = [{ root: f.root, path: f.path, mediaEntityId: f.id }];
const base = { items, action: "copy", targetLibraryRootId: alib.libraryRootId, onExisting: "replace_with_verify" };
const dry = await h.api("POST", "/files/organize", { ...base, idempotencyKey: key(), dryRun: true });
expect(dry.body.data.groups[0].files[0]).toMatchObject({ existing: "replace", action: "replace" });
const exec = await h.api("POST", "/files/organize", {
...base,
idempotencyKey: key(),
dryRun: false,
planIds: [dry.body.data.groups[0].planId],
});
const recordId = exec.body.data.recordIds[0] as string;
await executeRecord(h.module.deps().ao, recordId);
const rec = await h.db
.selectFrom("organization_record")
.select("status")
.where("id", "=", recordId)
.executeTakeFirstOrThrow();
expect(rec.status).toBe("succeeded");
expect(text(a.mock.read(target))).toBe(`${f.body}-v2`);
const trash = await h.db
.selectFrom("trash_item")
.select(["trash_path", "original_path"])
.where("original_path", "=", first.main.target_path!)
.executeTakeFirstOrThrow();
expect(trash.trash_path.startsWith(a.v("/s1/library/.umt-trash/"))).toBe(true);
const parked = trash.trash_path.replace(a.v(""), "");
expect(text(a.mock.read(parked))).toBe("old-version-content");
});
it("原地整理(Alist 媒体库里的文件):只登记,不动文件", async () => {
n++;
const id = await seedEntity(h.db, { kind: "movie", title: "In Place Film", year: 2015 });
a.mock.put("/s1/library/In Place Film (2015)/In.Place.Film.2015.1080p.mkv", "in-place-video");
const items = [{ root: alib.root, path: "In Place Film (2015)/In.Place.Film.2015.1080p.mkv", mediaEntityId: id }];
const base = { items, action: "in_place", minSizeBytes: 0, scrapeAfterImport: false };
const dry = await h.api("POST", "/files/organize", { ...base, idempotencyKey: key(), dryRun: true });
expect(dry.body.data.groups[0].error, JSON.stringify(dry.body)).toBeNull();
const exec = await h.api("POST", "/files/organize", {
...base,
idempotencyKey: key(),
dryRun: false,
planIds: [dry.body.data.groups[0].planId],
});
const recordId = exec.body.data.recordIds[0] as string;
await executeRecord(h.module.deps().ao, recordId);
const rec = await h.db
.selectFrom("organization_record")
.select(["status", "action"])
.where("id", "=", recordId)
.executeTakeFirstOrThrow();
expect(rec).toEqual({ status: "succeeded", action: "in_place" });
expect(text(a.mock.read("/s1/library/In Place Film (2015)/In.Place.Film.2015.1080p.mkv"))).toBe("in-place-video");
expect((await assetAt(alib.libraryRootId, "In Place Film (2015)/In.Place.Film.2015.1080p.mkv"))?.status).toBe(
"available",
);
});
it("撤销:删除整理记录连带媒体库文件 → Alist 上移进回收站(可还原)", async () => {
const f = await film("local");
const r = await organize(f, "copy", alib.libraryRootId);