Files
UHD_Media_Tool/apps/web/app/api/inventory.ts
T
rootandClaude Opus 5.5 7103bc8f7c feat(web): W3-3 site workbench, sources/feeds/snapshots, downloads with business timeline, organization records and agent sessions
- sites + /sites/:siteId: organized view with snapshot-first release list, inventory overlay,
  candidate reasons, volatile-field recheck, stale marking that keeps existing data, live search,
  unified download entry; origin view placeholder (M2)
- sources: feed CRUD/enable/fetch/reset, cursor/health/gap hints; per-site snapshot history
- tasks: download tasks with pause/resume (threshold pause cannot be bypassed), plans,
  cache volume thresholds and cleanup, business timeline (observation → plan → download →
  organization → agent/engine)
- organize: record list/detail with file-level results, manual organize with dryRun preview
- agent: session list/page and layout drawer with context prefix; streaming events,
  tool cards, rejection alerts, compaction, budget meters, model text vs server report;
  mock switch (?mock=1) until W2-D endpoints land

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 20:52:32 +08:00

53 lines
2.4 KiB
TypeScript

import { useQuery } from "@tanstack/react-query";
import { type InventoryEntry, InventoryResponse, LibraryDetail } from "@umt/contracts";
import { apiParsed } from "./parse.ts";
// 站点工作台库存叠加(W2-C:/inventory?entityIds=,最多 200 个)
export function useInventory(entityIds: string[]) {
const ids = [...new Set(entityIds)].sort().slice(0, 200);
return useQuery({
queryKey: ["inventory", ids],
enabled: ids.length > 0,
queryFn: ({ signal }) =>
apiParsed(InventoryResponse, "/api/v1/inventory", { params: { entityIds: ids.join(",") }, signal }),
select: (d) => new Map<string, InventoryEntry>(d.items.map((i) => [i.entityId, i])),
});
}
/** 叠加标记:只来自本地身份/Slot/任务,不从网页标题猜确定事实 */
export type OverlayFlag = { key: string; label: string; tone: "success" | "warning" | "neutral" };
export function inventoryFlags(entry: InventoryEntry | undefined, releaseResolution?: string | null): OverlayFlag[] {
if (!entry) return [];
const flags: OverlayFlag[] = [];
const available = entry.versions.filter((v) => v.available);
if (available.length) {
const res = [...new Set(available.map((v) => v.resolution ?? "未知分辨率"))].join(" / ");
flags.push({ key: "have", label: `已有 ${res}`, tone: "success" });
}
const has4k = available.some((v) => /2160|4k|uhd/i.test(v.resolution ?? ""));
const wants4k = entry.gaps.some((g) => /2160|4k|uhd/i.test(g.name));
if (wants4k && !has4k) flags.push({ key: "no4k", label: "缺 4K", tone: "warning" });
for (const g of entry.gaps.filter((g) => !/2160|4k|uhd/i.test(g.name) || has4k)) {
flags.push({ key: `gap-${g.slotId}`, label: `缺口:${g.name}`, tone: "warning" });
}
if (entry.downloading.length) {
flags.push({ key: "dl", label: `下载中 ${entry.downloading.length}`, tone: "neutral" });
}
if (releaseResolution && releaseResolution === "resolved" && flags.length === 0) {
flags.push({ key: "tracked", label: "已识别,无版本目标", tone: "neutral" });
}
return flags;
}
/** 作品详情(候选理由、Slot、下载历史);工作台详情抽屉用它回答"为什么选/不选" */
export function useEntityDetail(entityId: string | null | undefined) {
return useQuery({
queryKey: ["library", "detail", entityId ?? ""],
enabled: !!entityId,
queryFn: ({ signal }) =>
apiParsed(LibraryDetail, "/api/v1/library/:id", { pathParams: { id: entityId ?? "" }, signal }),
});
}