Use the dedicated TJWATER_AGENT_URL and Responses stream protocol. Keep running turns active while users steer or queue follow-ups and edit pending delivery. Refresh the model catalog before generation and preserve approval mode selection. Integrate scoped investigation messaging/presentation, geographic place selection, current Agent composer styles and supporting workbench contracts. Validation: 77 focused Vitest checks, 21 browser cases, lint, typecheck and production build. Preserve direct Server/Agent routing and project authentication.
54 lines
2.5 KiB
TypeScript
54 lines
2.5 KiB
TypeScript
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<typeof placeSchema>;
|
|
export type PlaceSearchResults = z.infer<typeof resultsSchema>;
|
|
|
|
export async function searchPlaces(
|
|
keyword: string,
|
|
projectId: string | null,
|
|
getAccessToken?: AccessTokenProvider,
|
|
signal?: AbortSignal
|
|
): 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 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;
|
|
}
|