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:
@@ -1,3 +1,4 @@
|
||||
import { responsesFixture } from "./responses-fixture";
|
||||
import type { Page } from "@playwright/test";
|
||||
|
||||
type MockAgentApiOptions = {
|
||||
@@ -19,7 +20,7 @@ export async function mockAgentApi(page: Page, options: MockAgentApiOptions = {}
|
||||
icon: "bolt" as const
|
||||
};
|
||||
const triggeredSessions = new Map<string, { sessionId: string; triggerId: string }>();
|
||||
await page.route("**/api/v1/agent/chat/**", async (route) => {
|
||||
await page.route("http://127.0.0.1:8787/api/v1/**", async (route) => {
|
||||
if (route.request().method() === "OPTIONS") {
|
||||
await route.fulfill({
|
||||
status: 204,
|
||||
@@ -34,6 +35,18 @@ export async function mockAgentApi(page: Page, options: MockAgentApiOptions = {}
|
||||
|
||||
const pathname = new URL(route.request().url()).pathname;
|
||||
|
||||
if (pathname.endsWith("/suggestions") && route.request().method() === "POST") {
|
||||
await route.fulfill({ headers: corsHeaders, json: {
|
||||
suggestions: [
|
||||
{ question: "分析当前管网需要优先核对的压力监测点。", activity_id: null },
|
||||
{ question: "检查近期供水任务的证据与待确认事项。", activity_id: null },
|
||||
{ question: "解读近期分析成果并列出进一步验证的问题。", activity_id: null },
|
||||
],
|
||||
generated_at: "2026-10-09T07:00:00Z",
|
||||
} });
|
||||
return;
|
||||
}
|
||||
|
||||
const collaborationMatch = pathname.match(/\/task\/([^/]+)\/collaboration\/session$/);
|
||||
if (collaborationMatch && route.request().method() === "POST") {
|
||||
await route.fulfill({
|
||||
@@ -99,24 +112,21 @@ export async function mockAgentApi(page: Page, options: MockAgentApiOptions = {}
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname.endsWith("/stream")) {
|
||||
if (pathname.endsWith("/responses")) {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
headers: {
|
||||
...corsHeaders,
|
||||
"Cache-Control": "no-cache",
|
||||
"Content-Type": "text/event-stream",
|
||||
"X-Vercel-AI-UI-Message-Stream": "v1"
|
||||
"Content-Type": "text/event-stream"
|
||||
},
|
||||
body: [
|
||||
body: responsesFixture([
|
||||
{ type: "start", messageId: "assistant-browser-test" },
|
||||
{ type: "text-start", id: "answer" },
|
||||
{ type: "text-delta", id: "answer", delta: "已收到工况汇总请求。" },
|
||||
{ type: "text-end", id: "answer" },
|
||||
{ type: "finish", finishReason: "stop" }
|
||||
]
|
||||
.map((part) => `data: ${JSON.stringify(part)}\n\n`)
|
||||
.join("")
|
||||
])
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -161,6 +171,6 @@ export async function mockAgentApi(page: Page, options: MockAgentApiOptions = {}
|
||||
return;
|
||||
}
|
||||
|
||||
await route.fulfill({ headers: corsHeaders, json: {} });
|
||||
await route.fallback();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export async function mockWorkbenchContext(
|
||||
body: `globalThis.__TJWATER_CONFIG__ = ${JSON.stringify({
|
||||
TJWATER_AUTH_MODE: "disabled",
|
||||
TJWATER_SERVER_API_BASE_URL: "https://tjwater-api.invalid",
|
||||
TJWATER_AGENT_API_BASE_URL: "http://127.0.0.1:8787",
|
||||
TJWATER_AGENT_URL: "http://127.0.0.1:8787",
|
||||
TJWATER_MAP_URL: "https://workbench-map.invalid/geoserver",
|
||||
TJWATER_GEOSERVER_WORKSPACE: "tjwater_next",
|
||||
TJWATER_MAPBOX_ACCESS_TOKEN: "",
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// Translate existing UI scenarios into Responses wire fixtures. Production
|
||||
// clients accept Responses only; this helper belongs exclusively to tests.
|
||||
export function responseFixtureWriter() {
|
||||
let sequence = 0;
|
||||
let id = "resp_browser_fixture";
|
||||
let messageId = "msg_browser_fixture";
|
||||
const texts = new Map<string, string>();
|
||||
const contentIndices = new Map<string, number>();
|
||||
const closed = new Set<string>();
|
||||
let error: string | undefined;
|
||||
const event = (type: string, fields: Record<string, unknown>) => `event: ${type}\ndata: ${JSON.stringify({ type, sequence_number: sequence++, ...fields })}\n\n`;
|
||||
const textFields = (key: string) => ({ item_id: messageId, output_index: 0, content_index: contentIndices.get(key) ?? 0 });
|
||||
return (chunk: Record<string, unknown>) => {
|
||||
if (chunk.type === "start") {
|
||||
messageId = String(chunk.messageId); id = `resp_${messageId}`;
|
||||
return event("response.created", { response: { id, object: "response", status: "in_progress" } })
|
||||
+ event("response.output_item.added", { output_index: 0, item: { type: "message", role: "assistant", id: messageId, content: [] } });
|
||||
}
|
||||
if (chunk.type === "text-start") {
|
||||
const key = String(chunk.id);
|
||||
contentIndices.set(key, contentIndices.size); texts.set(key, "");
|
||||
return event("response.content_part.added", { ...textFields(key), part: { type: "output_text", text: "" } });
|
||||
}
|
||||
if (chunk.type === "text-delta") {
|
||||
const key = String(chunk.id); texts.set(key, `${texts.get(key) ?? ""}${String(chunk.delta)}`);
|
||||
return event("response.output_text.delta", { ...textFields(key), delta: chunk.delta });
|
||||
}
|
||||
if (chunk.type === "text-end") { const key = String(chunk.id); closed.add(key); return event("response.output_text.done", { ...textFields(key), text: texts.get(key) ?? "" }); }
|
||||
if (typeof chunk.type === "string" && chunk.type.startsWith("data-")) return event(`tjwater.${chunk.type.slice(5)}`, { response_id: id, data: chunk.data, ...(chunk.id ? { id: chunk.id } : {}) });
|
||||
if (chunk.type === "error") { error = String(chunk.errorText); return ""; }
|
||||
if (chunk.type === "finish") {
|
||||
const rest = [...texts.keys()].filter((key) => !closed.has(key)).map((key) => event("response.output_text.done", { ...textFields(key), text: texts.get(key) })).join("");
|
||||
return rest + event(error ? "response.failed" : "response.completed", { response: { id, object: "response", status: error ? "failed" : "completed", ...(error ? { error: { message: error } } : {}) } });
|
||||
}
|
||||
return "";
|
||||
};
|
||||
}
|
||||
|
||||
export function responsesFixture(chunks: Record<string, unknown>[]) {
|
||||
const write = responseFixtureWriter();
|
||||
return chunks.map(write).join("");
|
||||
}
|
||||
Reference in New Issue
Block a user