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.
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user