Files
next-tjwater-frontend/src/features/workbench/api/place-search-client.ts
T
jiang ec23d44b02 feat(agent): support Responses steering and project-aware tools
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.
2026-10-19 18:46:58 +08:00

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;
}