- POST /acquisition-plans: manual submit via download.plan -> download.submit
(same authorization/audit/idempotency path as the agent), slotId or entityId
- read-only /media-assets, /asset-locations, /asset-unit-coverage, /path-bindings,
/reconcile-runs, /server-mappings, /artifacts, GET /title-aliases/{id};
unimplemented write endpoints removed from api.ts; registry test now checks both directions
- candidate evaluation upsert per (slot, source_release) + migration 0022 dedupe/unique index
- organization records expose downloadTaskId (+ list filter); inventory adds
subscribed/candidateReasons; feed sources expose derived possibleGap
- GET /events?after=&limit= JSON replay page for API key callers (same sse_event table)
- preference tag revision history from audit_event; subscriptions/identities/aliases
lists carry mediaEntityTitle; subscription q/mediaKind search; slot subscriptionId
filter + currentAsset summary; policy preview samples carry title/reasons
- DecisionSummary, IdentityRevisionList, ReleaseIdentityResult, PolicyPreviewSummary exported from contracts
Co-Authored-By: Claude Opus 5.5 <[email protected]>
88 lines
3.1 KiB
TypeScript
88 lines
3.1 KiB
TypeScript
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import {
|
|
cursorPage,
|
|
type FeedHealth,
|
|
FeedSource,
|
|
type FeedSourceCreate,
|
|
type FeedSourceUpdate,
|
|
Id,
|
|
} from "@umt/contracts";
|
|
import { z } from "zod";
|
|
import { TTL_SECOND } from "~/lib/api.ts";
|
|
import { apiMutate, apiParsed, cleanParams } from "./parse.ts";
|
|
|
|
// Feed 来源(W2-B:/feed-sources)
|
|
|
|
const FeedPage = cursorPage(FeedSource);
|
|
|
|
export const feedKeys = {
|
|
all: ["feeds"] as const,
|
|
list: (f: { siteId?: string; health?: FeedHealth }) => ["feeds", "list", f] as const,
|
|
};
|
|
|
|
export function useFeeds(f: { siteId?: string; health?: FeedHealth } = {}) {
|
|
return useQuery({
|
|
queryKey: feedKeys.list(f),
|
|
queryFn: ({ signal }) =>
|
|
apiParsed(FeedPage, "/api/v1/feed-sources", { params: cleanParams({ ...f, limit: 200 }), signal }),
|
|
refetchInterval: 30 * TTL_SECOND,
|
|
});
|
|
}
|
|
|
|
export function useCreateFeed() {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (body: FeedSourceCreate) =>
|
|
apiMutate("/api/v1/feed-sources", { body }).then((d) => FeedSource.parse(d)),
|
|
onSuccess: () => qc.invalidateQueries({ queryKey: feedKeys.all }),
|
|
});
|
|
}
|
|
|
|
export function useUpdateFeed() {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: ({ id, body }: { id: string; body: FeedSourceUpdate }) =>
|
|
apiMutate("/api/v1/feed-sources/:id", { method: "PATCH", pathParams: { id }, body }).then((d) =>
|
|
FeedSource.parse(d),
|
|
),
|
|
onSuccess: () => qc.invalidateQueries({ queryKey: feedKeys.all }),
|
|
});
|
|
}
|
|
|
|
/** 启停 / 熔断复位 */
|
|
export function useFeedAction() {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: ({ id, action }: { id: string; action: "enable" | "disable" | "reset" }) =>
|
|
apiMutate(`/api/v1/feed-sources/:id/${action}`, { pathParams: { id } }).then((d) => FeedSource.parse(d)),
|
|
onSuccess: () => qc.invalidateQueries({ queryKey: feedKeys.all }),
|
|
});
|
|
}
|
|
|
|
/** 立即抓取(仍受站点限流),返回任务 ID */
|
|
export function useFetchFeedNow() {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (id: string) =>
|
|
apiMutate("/api/v1/feed-sources/:id/fetch", { pathParams: { id } }).then((d) =>
|
|
z.object({ taskId: Id }).parse(d),
|
|
),
|
|
onSuccess: () => qc.invalidateQueries({ queryKey: feedKeys.all }),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 疑似缺口:服务端在 Feed 窗口与上次零重叠时写 possibleGap 并排站点补漏;下一次抓取衔接上后清空。
|
|
* 没有服务端标记但"距上次成功超过 3 个周期"时,只作推断提示(标"可能")。
|
|
*/
|
|
export function feedGapHint(f: FeedSource, now = Date.now()): string | null {
|
|
if (f.possibleGap) {
|
|
const at = new Date(f.possibleGap.detectedAt).toLocaleString("zh-CN", { hour12: false });
|
|
return `疑似缺口(${at} 检测,${f.possibleGap.backfillQueued ? "已排补漏" : "Feed 未关联站点,无法自动补漏"})`;
|
|
}
|
|
if (!f.enabled || !f.lastSuccessAt) return null;
|
|
const gapMs = now - new Date(f.lastSuccessAt).getTime();
|
|
if (gapMs > f.pollIntervalSec * 1000 * 3) return "距上次成功已超过 3 个周期,可能漏项";
|
|
return null;
|
|
}
|