- 站点工作台: 登记站点 dialog (built-in site definitions or custom). - Site detail tabs: 整理视图 / 原站视图 / 快照与 Feed / 账号 (account list + credential form). - 来源健康 (was 来源/Feed/快照): cross-site health table + all feeds; site names deep-link to the site's 快照与 Feed tab. Feed table extracted to components/workbench/feed-table.tsx. Co-Authored-By: Claude Opus 5.5 <[email protected]>
111 lines
4.0 KiB
TypeScript
111 lines
4.0 KiB
TypeScript
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import { cursorPage, Id, Site, SiteAccount, type SiteCreate, 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;
|
|
}
|
|
|
|
/** 登记站点(POST /sites) */
|
|
export function useCreateSite() {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (body: SiteCreate) => apiMutate("/api/v1/sites", { body }).then((d) => Site.parse(d)),
|
|
onSuccess: () => qc.invalidateQueries({ queryKey: siteKeys.all }),
|
|
});
|
|
}
|