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,39 @@
|
||||
import type { ApiErrorCode } from "@umt/contracts";
|
||||
import type { z } from "zod";
|
||||
import { ApiRequestError, apiFetch, type FetcherArg } from "~/lib/api.ts";
|
||||
|
||||
export { ApiRequestError };
|
||||
|
||||
/** 请求并用 contracts 的 zod schema 校验 data;失败抛 ApiRequestError(给 react-query 的 error 态) */
|
||||
export async function apiParsed<S extends z.ZodType>(schema: S, path: string, arg?: FetcherArg): Promise<z.infer<S>> {
|
||||
const r = await apiFetch<unknown>(path, arg);
|
||||
if (!r.ok) throw new ApiRequestError(r.error.code, r.error.message);
|
||||
const parsed = schema.safeParse(r.data);
|
||||
if (!parsed.success) {
|
||||
const issue = parsed.error.issues[0];
|
||||
throw new ApiRequestError(
|
||||
"internal" satisfies ApiErrorCode,
|
||||
`响应格式与合同不符:${issue ? `${issue.path.join(".")} ${issue.message}` : "未知字段"}`,
|
||||
);
|
||||
}
|
||||
return parsed.data;
|
||||
}
|
||||
|
||||
/** 变更请求:返回 data,失败抛错(给 useMutation,由 onError 统一 toast) */
|
||||
export async function apiMutate<T = unknown>(path: string, arg: FetcherArg = {}): Promise<T> {
|
||||
const r = await apiFetch<T>(path, { method: "POST", ...arg });
|
||||
if (!r.ok) throw new ApiRequestError(r.error.code, r.error.message);
|
||||
return r.data;
|
||||
}
|
||||
|
||||
export const errorCode = (err: unknown): string | null => (err instanceof ApiRequestError ? err.code : null);
|
||||
|
||||
/** 端点是否不存在(后端尚未提供) */
|
||||
export const isMissingEndpoint = (err: unknown) => errorCode(err) === "not_found";
|
||||
|
||||
/** 查询字符串:去掉空值 */
|
||||
export function cleanParams(p: Record<string, string | number | boolean | undefined | null>) {
|
||||
const out: Record<string, string | number | boolean> = {};
|
||||
for (const [k, v] of Object.entries(p)) if (v !== undefined && v !== null && v !== "") out[k] = v;
|
||||
return out;
|
||||
}
|
||||
Reference in new issue
Block a user