import { z } from "zod"; import type { AccessTokenProvider } from "@/shared/auth/keycloak-auth"; import { env } from "@/shared/config/env"; import { createWorkbenchHeaders, findOwnedWorkbenchProject, getServerError } from "./project-client"; export const MAX_PLACE_SEARCH_RESULTS = 10; 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("奉贤区") }); 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("奉贤区") }); export type PlaceSearchItem = z.infer; export type PlaceSearchResults = z.infer; export async function searchPlaces( keyword: string, projectId: string | null, getAccessToken?: AccessTokenProvider, signal?: AbortSignal ): Promise { 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 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) { throw new Error("未获得有效的地名搜索结果,请稍后重试"); } return parsed.data; }