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]>
This commit is contained in:
1 parent
6280d52ec3
commit
7103bc8f7c
35 files changed
+6134
-45
No files matched your search
@@ -0,0 +1,101 @@
|
||||
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;
|
||||
}
|
||||
Reference in new issue
Block a user