- 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]>
102 lines
3.7 KiB
TypeScript
102 lines
3.7 KiB
TypeScript
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import { cursorPage, Id, Site, SiteAccount, SiteDefinitionView, SiteSnapshot } from "@umt/contracts";
|
|
import { z } from "zod";
|
|
import { TTL_SECOND } from "~/lib/api.ts";
|
|
import { apiMutate, apiParsed } from "./parse.ts";
|
|
|
|
// 站点、站点账号、站点快照(W2-B:/sites、/site-accounts、/sites/{id}/snapshot(s))
|
|
|
|
const SitePage = cursorPage(Site);
|
|
const AccountPage = cursorPage(SiteAccount);
|
|
const SnapshotPage = cursorPage(SiteSnapshot);
|
|
|
|
export const siteKeys = {
|
|
all: ["sites"] as const,
|
|
list: () => ["sites", "list"] as const,
|
|
one: (id: string) => ["sites", "one", id] as const,
|
|
accounts: (siteId?: string) => ["sites", "accounts", siteId ?? "all"] as const,
|
|
snapshots: (siteId: string) => ["sites", "snapshots", siteId] as const,
|
|
definitions: () => ["sites", "definitions"] as const,
|
|
};
|
|
|
|
export function useSites() {
|
|
return useQuery({
|
|
queryKey: siteKeys.list(),
|
|
queryFn: ({ signal }) => apiParsed(SitePage, "/api/v1/sites", { params: { limit: 200 }, signal }),
|
|
});
|
|
}
|
|
|
|
export function useSite(id: string | undefined) {
|
|
return useQuery({
|
|
queryKey: siteKeys.one(id ?? ""),
|
|
enabled: !!id && Id.safeParse(id).success,
|
|
queryFn: ({ signal }) => apiParsed(Site, "/api/v1/sites/:id", { pathParams: { id: id ?? "" }, signal }),
|
|
});
|
|
}
|
|
|
|
export function useSiteAccounts(siteId?: string) {
|
|
return useQuery({
|
|
queryKey: siteKeys.accounts(siteId),
|
|
queryFn: ({ signal }) =>
|
|
apiParsed(AccountPage, "/api/v1/site-accounts", { params: { limit: 200, siteId }, signal }),
|
|
});
|
|
}
|
|
|
|
export function useSiteDefinitions() {
|
|
return useQuery({
|
|
queryKey: siteKeys.definitions(),
|
|
staleTime: 5 * 60 * TTL_SECOND,
|
|
queryFn: ({ signal }) =>
|
|
apiParsed(z.object({ items: z.array(SiteDefinitionView) }), "/api/v1/site-definitions", { signal }),
|
|
});
|
|
}
|
|
|
|
/** 快照/补查记录;有 running 时 5 秒轮询 */
|
|
export function useSiteSnapshots(siteId: string | undefined, limit = 20) {
|
|
return useQuery({
|
|
queryKey: siteKeys.snapshots(siteId ?? ""),
|
|
enabled: !!siteId,
|
|
queryFn: ({ signal }) =>
|
|
apiParsed(SnapshotPage, "/api/v1/sites/:id/snapshots", {
|
|
pathParams: { id: siteId ?? "" },
|
|
params: { limit },
|
|
signal,
|
|
}),
|
|
refetchInterval: (q) => (q.state.data?.items.some((s) => s.status === "running") ? 5 * TTL_SECOND : false),
|
|
});
|
|
}
|
|
|
|
/** 启用 / 停用站点 */
|
|
export function useToggleSite() {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: ({ id, enabled }: { id: string; enabled: boolean }) =>
|
|
apiMutate(`/api/v1/sites/:id/${enabled ? "enable" : "disable"}`, { pathParams: { id } }).then((d) =>
|
|
Site.parse(d),
|
|
),
|
|
onSuccess: () => qc.invalidateQueries({ queryKey: siteKeys.all }),
|
|
});
|
|
}
|
|
|
|
/** 手动快照(受站点限流),返回任务 ID */
|
|
export function useSnapshotSite() {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: ({ id, maxPages }: { id: string; maxPages?: number }) =>
|
|
apiMutate(`/api/v1/sites/:id/snapshot`, { pathParams: { id }, body: maxPages ? { maxPages } : {} }).then((d) =>
|
|
z.object({ taskId: Id }).parse(d),
|
|
),
|
|
onSuccess: (_d, v) => {
|
|
qc.invalidateQueries({ queryKey: siteKeys.snapshots(v.id) });
|
|
qc.invalidateQueries({ queryKey: siteKeys.list() });
|
|
},
|
|
});
|
|
}
|
|
|
|
/** 快照新鲜度:从未快照或超过 2 个周期视为过期(与服务端 WorkbenchCoverage.stale 同口径) */
|
|
export function snapshotStale(site: Pick<Site, "lastSnapshotAt" | "snapshotIntervalSec">, now = Date.now()) {
|
|
if (!site.lastSnapshotAt) return true;
|
|
const interval = (site.snapshotIntervalSec ?? 3600) * 1000;
|
|
return now - new Date(site.lastSnapshotAt).getTime() > interval * 2;
|
|
}
|