feat(contracts,server,web): files G6–G8 fields and identify S-7 contract
- G6: FileRoot.pendingCount/pendingPartial for download roots (same rule as the unidentified filter, recursive, cached 5 min, invalidated by files.changed) - G7: FileEntry.parsedHint from parseReleasePath (file + parent dirs); the web subline uses it and no longer parses names itself - G8: FileIdentifyCandidate.posterUrl (TMDB poster_path via adapter and catalog.lookup, local entities from metadata.posterPath); thumbnails in the identify panel - S-7 contract: FilesIdentifyResult.remoteTaskId/remotePending, MediaServerInventory.partial, EventType identify.completed with IdentifyCompletedPayload; identify panel (useIdentifyLive) and organize wizard merge remote results by root + path, buffering events that beat the response Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
fd2f7bc33d
commit
abc69886ea
26 files changed
+510
-134
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import { sql } from "@umt/db";
|
||||
import type { Principal } from "../../http/auth.ts";
|
||||
import { actorOfP, FilesError, type FmDeps, hashJson, jsonb } from "./common.ts";
|
||||
import { invalidatePending } from "./list.ts";
|
||||
|
||||
// 计划冻结(artifact kind=plan, title=files_plan)、幂等键(artifact title=files_idem)、审计与 SSE。
|
||||
|
||||
@@ -44,12 +45,12 @@ export async function loadPlan<C>(d: FmDeps, planId: string, op: string, request
|
||||
.select(["id", "content", "content_hash", "expires_at", "title", "kind"])
|
||||
.where("id", "=", planId)
|
||||
.executeTakeFirst();
|
||||
if (row?.kind !== "plan" || row.title !== "files_plan") throw new FilesError("plan_stale", "planId 不存在,重新预览");
|
||||
if (row?.kind !== "plan" || row.title !== "files_plan") throw new FilesError("plan_stale", "planId 不存在,重新预览");
|
||||
const content = row.content as StoredPlan<C>["content"];
|
||||
if (content.op !== op) throw new FilesError("plan_stale", `planId 属于 ${content.op} 操作,不能用于 ${op}`);
|
||||
if (content.op !== op) throw new FilesError("plan_stale", `planId 属于 ${content.op} 操作,不能用于 ${op}`);
|
||||
if (!row.expires_at || row.expires_at.getTime() <= d.ao.ports.now().getTime())
|
||||
throw new FilesError("plan_stale", "计划已过期,重新预览");
|
||||
if (content.requestHash !== requestHash) throw new FilesError("plan_stale", "请求与预览时不一致,重新预览");
|
||||
throw new FilesError("plan_stale", "计划已过期,重新预览");
|
||||
if (content.requestHash !== requestHash) throw new FilesError("plan_stale", "请求与预览时不一致,重新预览");
|
||||
return { id: row.id, content, contentHash: row.content_hash, expiresAt: row.expires_at };
|
||||
}
|
||||
|
||||
@@ -100,7 +101,7 @@ export async function idempotent<T>(
|
||||
if (!row) return undefined;
|
||||
const c = row.content as { reqHash: string; state: string; result?: T };
|
||||
if (c.reqHash !== reqHash) throw new FilesError("exists", "idempotencyKey 已用于不同的请求");
|
||||
if (c.state !== "done") throw new FilesError("exists", "同一 idempotencyKey 的请求仍在执行,稍后重试");
|
||||
if (c.state !== "done") throw new FilesError("exists", "同一 idempotencyKey 的请求仍在执行,稍后重试");
|
||||
return c.result as T;
|
||||
};
|
||||
const prev = await replay();
|
||||
@@ -115,7 +116,7 @@ export async function idempotent<T>(
|
||||
if (!id) {
|
||||
const again = await replay();
|
||||
if (again !== undefined) return again;
|
||||
throw new FilesError("exists", "同一 idempotencyKey 的请求仍在执行,稍后重试");
|
||||
throw new FilesError("exists", "同一 idempotencyKey 的请求仍在执行,稍后重试");
|
||||
}
|
||||
try {
|
||||
const result = await fn();
|
||||
@@ -154,5 +155,6 @@ export async function audit(
|
||||
|
||||
/** SSE `files.changed`:受影响的根与目录(相对路径),前端据此刷新 */
|
||||
export function filesChanged(d: FmDeps, rootId: string, dirs: string[]): void {
|
||||
invalidatePending(rootId);
|
||||
void d.ctx.events.publish("files.changed", { rootId, dirs: [...new Set(dirs)] });
|
||||
}
|
||||
Reference in new issue
Block a user