feat: add project-aware agent workspace
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { getWorkbenchAccessToken } from "./request-context";
|
||||
import { z } from "zod";
|
||||
import type { AccessTokenProvider } from "@/shared/auth/keycloak-auth";
|
||||
import type { WorkbenchAccess } from "./request-context";
|
||||
import { env } from "@/shared/config/env";
|
||||
import { createWorkbenchHeaders, findOwnedWorkbenchProject, getServerError } from "./project-client";
|
||||
import { createWorkbenchHeaders, findOwnedWorkbenchProject, getServerError, type PlaceSearchConfiguration } from "./project-client";
|
||||
|
||||
export const MAX_PLACE_SEARCH_RESULTS = 10;
|
||||
|
||||
@@ -9,11 +10,11 @@ const placeSchema = z.object({
|
||||
id: z.string().min(1), name: z.string().trim().min(1), address: z.string(),
|
||||
longitude: z.number().finite().min(-180).max(180),
|
||||
latitude: z.number().finite().min(-90).max(90),
|
||||
category: z.string().nullable(), district: z.literal("奉贤区")
|
||||
category: z.string().nullable(), district: z.string().trim().min(1)
|
||||
});
|
||||
const resultsSchema = z.object({
|
||||
items: z.array(placeSchema).max(MAX_PLACE_SEARCH_RESULTS), total: z.number().int().min(0).max(MAX_PLACE_SEARCH_RESULTS),
|
||||
truncated: z.boolean().default(false), region: z.literal("奉贤区")
|
||||
truncated: z.boolean().default(false), region: z.string().trim().min(1)
|
||||
});
|
||||
export type PlaceSearchItem = z.infer<typeof placeSchema>;
|
||||
export type PlaceSearchResults = z.infer<typeof resultsSchema>;
|
||||
@@ -21,33 +22,70 @@ export type PlaceSearchResults = z.infer<typeof resultsSchema>;
|
||||
export async function searchPlaces(
|
||||
keyword: string,
|
||||
projectId: string | null,
|
||||
getAccessToken?: AccessTokenProvider,
|
||||
signal?: AbortSignal
|
||||
getAccessToken?: WorkbenchAccess,
|
||||
signal?: AbortSignal,
|
||||
configuration?: PlaceSearchConfiguration | null
|
||||
): Promise<PlaceSearchResults> {
|
||||
const query = keyword.trim();
|
||||
if (!query) throw new Error("请输入需要搜索的地址或地名");
|
||||
if (query.length > 120) throw new Error("查询关键字不能超过120个字符");
|
||||
const requestSignal = signal
|
||||
? AbortSignal.any([signal, AbortSignal.timeout(20_000)])
|
||||
: AbortSignal.timeout(20_000);
|
||||
const ownedProject = projectId ? null : await findOwnedWorkbenchProject(getAccessToken, requestSignal);
|
||||
const accessToken = ownedProject?.accessToken ?? await getAccessToken?.();
|
||||
requestSignal.throwIfAborted();
|
||||
const resolvedProjectId = projectId ?? ownedProject?.project.project_id;
|
||||
if (!resolvedProjectId) throw new Error("未找到地名搜索所属项目");
|
||||
const response = await fetch(`${env.TJWATER_SERVER_API_BASE_URL.replace(/\/$/, "")}/api/v1/place-search-requests`, {
|
||||
method: "POST",
|
||||
headers: createWorkbenchHeaders(accessToken, resolvedProjectId, true),
|
||||
body: JSON.stringify({ keyword: query }),
|
||||
signal: requestSignal
|
||||
});
|
||||
const payload: unknown = await response.json().catch(() => null);
|
||||
if (!response.ok) throw new Error(getServerError(payload, response.status, "地名搜索"));
|
||||
const payload = await requestPlaceData("place-search-requests", { keyword: query }, projectId, getAccessToken, signal);
|
||||
const parsed = resultsSchema.safeParse(payload);
|
||||
if (!parsed.success
|
||||
|| new Set(parsed.data.items.map((item) => item.id)).size !== parsed.data.items.length
|
||||
|| parsed.data.total !== parsed.data.items.length) {
|
||||
|| parsed.data.total !== parsed.data.items.length
|
||||
|| (configuration && (parsed.data.region !== configuration.region
|
||||
|| (!configuration.admin_code.endsWith("00") && parsed.data.items.some((item) => item.district !== configuration.region))))) {
|
||||
throw new Error("未获得有效的地名搜索结果,请稍后重试");
|
||||
}
|
||||
return parsed.data;
|
||||
}
|
||||
|
||||
|
||||
export async function reverseGeocodePoint(
|
||||
longitude: number,
|
||||
latitude: number,
|
||||
projectId: string | null,
|
||||
getAccessToken?: WorkbenchAccess,
|
||||
signal?: AbortSignal,
|
||||
configuration?: PlaceSearchConfiguration | null
|
||||
): Promise<PlaceSearchItem> {
|
||||
const coordinates = z.object({
|
||||
longitude: z.number().finite().min(-180).max(180),
|
||||
latitude: z.number().finite().min(-90).max(90)
|
||||
}).safeParse({ longitude, latitude });
|
||||
if (!coordinates.success) throw new Error("地图取点坐标无效");
|
||||
const payload = await requestPlaceData("reverse-geocoding-requests", coordinates.data, projectId, getAccessToken, signal);
|
||||
const parsed = placeSchema.safeParse(payload);
|
||||
if (!parsed.success || parsed.data.longitude !== longitude || parsed.data.latitude !== latitude
|
||||
|| (configuration && !configuration.admin_code.endsWith("00") && parsed.data.district !== configuration.region)) {
|
||||
throw new Error("未获得有效的地图取点结果,请重新取点");
|
||||
}
|
||||
return parsed.data;
|
||||
}
|
||||
|
||||
async function requestPlaceData(
|
||||
path: string,
|
||||
body: Record<string, string | number>,
|
||||
projectId: string | null,
|
||||
getAccessToken?: WorkbenchAccess,
|
||||
signal?: AbortSignal
|
||||
): Promise<unknown> {
|
||||
const requestSignal = signal
|
||||
? AbortSignal.any([signal, AbortSignal.timeout(20_000)])
|
||||
: AbortSignal.timeout(20_000);
|
||||
const ownedProject = projectId ? null : await findOwnedWorkbenchProject(getAccessToken, requestSignal);
|
||||
const accessToken = ownedProject?.accessToken ?? await getWorkbenchAccessToken(getAccessToken);
|
||||
requestSignal.throwIfAborted();
|
||||
const resolvedProjectId = projectId ?? ownedProject?.project.project_id;
|
||||
if (!resolvedProjectId) throw new Error("未找到地名搜索所属项目");
|
||||
const response = await fetch(`${env.TJWATER_SERVER_API_BASE_URL.replace(/\/$/, "")}/api/v1/${path}`, {
|
||||
method: "POST",
|
||||
headers: createWorkbenchHeaders(accessToken, resolvedProjectId, true),
|
||||
body: JSON.stringify(body),
|
||||
signal: requestSignal
|
||||
});
|
||||
const payload: unknown = await response.json().catch(() => null);
|
||||
if (!response.ok) throw new Error(getServerError(payload, response.status, "地名搜索"));
|
||||
return payload;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user