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,85 @@
|
||||
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 窗口与上次零重叠时服务端发 feed.gap_suspected 并排补漏。
|
||||
* 列表字段里没有显式标记,这里按 cursor 里的 gapSuspectedAt(若服务端写入)或"距上次成功超过 3 个周期"推断,并标"疑似"。
|
||||
*/
|
||||
export function feedGapHint(f: FeedSource, now = Date.now()): string | null {
|
||||
const gapAt = typeof f.cursor.gapSuspectedAt === "string" ? f.cursor.gapSuspectedAt : null;
|
||||
if (gapAt) return `疑似缺口(${new Date(gapAt).toLocaleString("zh-CN", { hour12: false })} 检测,已排补漏)`;
|
||||
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;
|
||||
}
|
||||
Reference in new issue
Block a user