feat(web): settings with 9 sidebar sections and Agent upstream management
- /console/settings#<section>[/<anchor>]; legacy ?tab= redirected in clientLoader - SETTINGS_SECTIONS in contracts with field-ownership test - Agent section: upstream list, create wizard (provider -> credentials/model -> login/test/default), edit drawer with OAuth browser/device login, test, set default, delete - sites section with site registration, snapshot/rate-limit drawer and account credentials - path mappings moved into downloader / media-server drawers; metadata and account sections Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
459991421e
commit
c67f6a9682
25 files changed
+3842
-942
No files matched your search
@@ -16,8 +16,11 @@ import type {
|
||||
PathMappingUpdate,
|
||||
Site,
|
||||
SiteAccount,
|
||||
SiteAccountUpdate,
|
||||
SiteCreate,
|
||||
SiteCredentialsPut,
|
||||
SiteCredentialsResult,
|
||||
SiteUpdate,
|
||||
} from "@umt/contracts";
|
||||
import { apiData } from "~/lib/api.ts";
|
||||
import { type QueryArea, qk } from "~/lib/query-keys.ts";
|
||||
@@ -115,6 +118,52 @@ export function usePutSiteCredentials() {
|
||||
apiData<SiteCredentialsResult>("/sites/{id}/credentials", { method: "PUT", pathParams: { id: siteId }, body }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: qk.all("site-accounts") });
|
||||
qc.invalidateQueries({ queryKey: qk.all("sites") });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ─── 站点登记与参数(设置"站点与账号";浏览在站点工作台) ───
|
||||
|
||||
function useInvalidateSites() {
|
||||
const qc = useQueryClient();
|
||||
return () => {
|
||||
// sites.ts 用 ["sites", …],这里的列表用 qk.list("sites"),两者同前缀
|
||||
qc.invalidateQueries({ queryKey: qk.all("sites") });
|
||||
qc.invalidateQueries({ queryKey: qk.all("site-accounts") });
|
||||
};
|
||||
}
|
||||
|
||||
export function useCreateSite() {
|
||||
const inv = useInvalidateSites();
|
||||
return useMutation({
|
||||
mutationFn: (body: SiteCreate) => apiData<Site>("/sites", { method: "POST", body }),
|
||||
onSuccess: inv,
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateSite() {
|
||||
const inv = useInvalidateSites();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, body }: { id: string; body: SiteUpdate }) =>
|
||||
apiData<Site>("/sites/{id}", { method: "PATCH", pathParams: { id }, body }),
|
||||
onSuccess: inv,
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateSiteAccount() {
|
||||
const inv = useInvalidateSites();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, body }: { id: string; body: SiteAccountUpdate }) =>
|
||||
apiData<SiteAccount>("/site-accounts/{id}", { method: "PATCH", pathParams: { id }, body }),
|
||||
onSuccess: inv,
|
||||
});
|
||||
}
|
||||
|
||||
export function useRemoveSiteAccount() {
|
||||
const inv = useInvalidateSites();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => apiData<unknown>("/site-accounts/{id}", { method: "DELETE", pathParams: { id } }),
|
||||
onSuccess: inv,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type {
|
||||
PiOAuthCompleteRequest,
|
||||
PiOAuthCompleteResult,
|
||||
PiOAuthStartRequest,
|
||||
PiOAuthStartResult,
|
||||
PiProvidersResponse,
|
||||
PiUpstream,
|
||||
PiUpstreamCreate,
|
||||
PiUpstreamList,
|
||||
PiUpstreamModels,
|
||||
PiUpstreamTestRequest,
|
||||
PiUpstreamTestResult,
|
||||
PiUpstreamUpdate,
|
||||
} from "@umt/contracts";
|
||||
import { apiData, TTL_MINUTE } from "~/lib/api.ts";
|
||||
import { qk } from "~/lib/query-keys.ts";
|
||||
|
||||
// Agent 上游(W7-A 的 /pi/*):能力表、上游 CRUD、测试、模型表、订阅登录。
|
||||
// 写操作后刷新上游列表与全局设置(默认上游存在 settings.pi.defaultUpstreamId)。
|
||||
|
||||
export const piKeys = {
|
||||
all: ["pi"] as const,
|
||||
providers: (withModels: boolean) => ["pi", "providers", withModels] as const,
|
||||
upstreams: ["pi", "upstreams"] as const,
|
||||
models: (id: string) => ["pi", "models", id] as const,
|
||||
};
|
||||
|
||||
/** 能力表;withModels = 带静态模型表(约 370 KB,只在新建向导里要) */
|
||||
export function usePiProviders(withModels = false, enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: piKeys.providers(withModels),
|
||||
enabled,
|
||||
staleTime: 30 * TTL_MINUTE,
|
||||
queryFn: ({ signal }) =>
|
||||
apiData<PiProvidersResponse>("/pi/providers", { params: withModels ? {} : { models: "false" }, signal }),
|
||||
});
|
||||
}
|
||||
|
||||
export function usePiUpstreams() {
|
||||
return useQuery({
|
||||
queryKey: piKeys.upstreams,
|
||||
queryFn: ({ signal }) => apiData<PiUpstreamList>("/pi/upstreams", { signal }),
|
||||
});
|
||||
}
|
||||
|
||||
export function usePiUpstreamModels(id: string | null) {
|
||||
return useQuery({
|
||||
queryKey: piKeys.models(id ?? ""),
|
||||
enabled: !!id,
|
||||
staleTime: 5 * TTL_MINUTE,
|
||||
queryFn: ({ signal }) =>
|
||||
apiData<PiUpstreamModels>("/pi/upstreams/{id}/models", { pathParams: { id: id ?? "" }, signal }),
|
||||
});
|
||||
}
|
||||
|
||||
function useInvalidatePi() {
|
||||
const qc = useQueryClient();
|
||||
return () => {
|
||||
qc.invalidateQueries({ queryKey: piKeys.upstreams });
|
||||
qc.invalidateQueries({ queryKey: qk.settings });
|
||||
};
|
||||
}
|
||||
|
||||
export function useCreatePiUpstream() {
|
||||
const inv = useInvalidatePi();
|
||||
return useMutation({
|
||||
mutationFn: (body: PiUpstreamCreate) => apiData<PiUpstream>("/pi/upstreams", { method: "POST", body }),
|
||||
onSuccess: inv,
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdatePiUpstream() {
|
||||
const inv = useInvalidatePi();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, body }: { id: string; body: PiUpstreamUpdate }) =>
|
||||
apiData<PiUpstream>("/pi/upstreams/{id}", { method: "PATCH", pathParams: { id }, body }),
|
||||
onSuccess: inv,
|
||||
});
|
||||
}
|
||||
|
||||
export function useRemovePiUpstream() {
|
||||
const inv = useInvalidatePi();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => apiData<unknown>("/pi/upstreams/{id}", { method: "DELETE", pathParams: { id } }),
|
||||
onSuccess: inv,
|
||||
});
|
||||
}
|
||||
|
||||
export function useTestPiUpstream() {
|
||||
const inv = useInvalidatePi();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, body }: { id: string; body: PiUpstreamTestRequest }) =>
|
||||
apiData<PiUpstreamTestResult>("/pi/upstreams/{id}/test", { method: "POST", pathParams: { id }, body }),
|
||||
onSettled: inv,
|
||||
});
|
||||
}
|
||||
|
||||
export function useStartPiOAuth() {
|
||||
return useMutation({
|
||||
mutationFn: ({ id, body }: { id: string; body: PiOAuthStartRequest }) =>
|
||||
apiData<PiOAuthStartResult>("/pi/upstreams/{id}/oauth/start", { method: "POST", pathParams: { id }, body }),
|
||||
});
|
||||
}
|
||||
|
||||
/** 完成 / 轮询一次登录(device 模式前端反复调用直到终态) */
|
||||
export function completePiOAuth(id: string, body: PiOAuthCompleteRequest, signal?: AbortSignal) {
|
||||
return apiData<PiOAuthCompleteResult>("/pi/upstreams/{id}/oauth/complete", {
|
||||
method: "POST",
|
||||
pathParams: { id },
|
||||
body,
|
||||
signal,
|
||||
});
|
||||
}
|
||||
|
||||
export function useLogoutPiOAuth() {
|
||||
const inv = useInvalidatePi();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) =>
|
||||
apiData<PiUpstream>("/pi/upstreams/{id}/oauth/logout", { method: "POST", pathParams: { id }, body: {} }),
|
||||
onSuccess: inv,
|
||||
});
|
||||
}
|
||||
|
||||
export { useInvalidatePi };
|
||||
Reference in new issue
Block a user