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:
2026-10-19 18:46:58 +08:00
parent 2c5bf15cce
commit ec23d44b02
131 changed files with 6663 additions and 1204 deletions
+82 -18
View File
@@ -102,16 +102,28 @@ test.describe("外围工单测试", () => {
await expect(panel.getByText("工单接口测试", { exact: true })).toBeVisible();
await expect(panel.getByText("历史监测回放", { exact: true })).toHaveCount(0);
await expect(panel.getByText("测试数据", { exact: true })).toHaveCount(0);
const workOrderList = page.getByLabel("工单列表");
await expect(workOrderList).toContainText("合成通信核查");
await expect(workOrderList).toContainText("合成压力偏差复核");
await expect(workOrderList).toContainText("合成零流量样本确认");
await expect(workOrderList).toContainText("待处理");
await expect(workOrderList).toContainText("处理中");
await expect(workOrderList).toContainText("已完成");
await expect(workOrderList.getByText("工单报告", { exact: true })).toHaveCount(3);
await expect(workOrderList.getByText("暂无现场反馈", { exact: true })).toHaveCount(3);
await expect(workOrderList.getByText("活动记录", { exact: true })).toHaveCount(0);
const appointmentList = panel.getByLabel("预约工单列表");
const complaintList = panel.getByLabel("投诉工单列表");
const burstList = panel.getByLabel("爆管上报工单列表");
await expect(appointmentList).toContainText("合成通信核查");
await expect(appointmentList).toContainText("合成压力偏差复核");
await expect(appointmentList).toContainText("合成零流量样本确认");
await expect(appointmentList).toContainText("待处理");
await expect(appointmentList).toContainText("处理中");
await expect(appointmentList).toContainText("已完成");
await expect(appointmentList.getByText("工单报告", { exact: true })).toHaveCount(3);
await expect(appointmentList.getByText("暂无现场反馈", { exact: true })).toHaveCount(3);
await expect(complaintList).toContainText("水质投诉:演示小区");
await expect(complaintList.getByRole("region", { name: "投诉工单信息" })).toContainText("发生时间");
await expect(complaintList.getByText("工单报告", { exact: true })).toHaveCount(0);
await expect(burstList).toContainText("爆管上报:演示路口");
await expect(burstList.getByRole("region", { name: "爆管上报工单信息" })).toContainText("发生时间");
await expect(complaintList).toHaveAttribute("open", "");
await complaintList.locator("summary").click();
await expect(complaintList).not.toHaveAttribute("open", "");
await expect(complaintList.getByText("水质投诉:演示小区")).toBeHidden();
await complaintList.locator("summary").click();
await expect(complaintList.getByText("水质投诉:演示小区")).toBeVisible();
expect(requestedPaths).toEqual(expect.arrayContaining([
"/api/sample/v1/context",
"/api/sample/v1/work-orders"
@@ -257,7 +269,12 @@ test.describe("外围工单测试", () => {
if (path.endsWith("/work-orders") && request.method() === "POST") {
const body = request.postDataJSON();
writes.push(body);
const item = { ...body, work_order_id: "50000000-0000-4000-8000-000000000001", project_id: projectId, revision: 1, origin: "synthetic", created_at: now, updated_at: now, created_by: "tester", updated_by: "tester", activity: [{ sequence: 1, occurred_at: now, actor: "tester", kind: "created", title: "工单创建", detail: "预约工单已创建。" }] };
const createdDetail = body.work_order_type === "water_quality_complaint"
? "投诉工单已创建。"
: body.work_order_type === "burst_report"
? "爆管上报工单已创建。"
: "预约工单已创建。";
const item = { ...body, work_order_id: "50000000-0000-4000-8000-000000000001", project_id: projectId, revision: 1, origin: "synthetic", created_at: now, updated_at: now, created_by: "tester", updated_by: "tester", activity: [{ sequence: 1, occurred_at: now, actor: "tester", kind: "created", title: "工单创建", detail: createdDetail }] };
delete item.request_id; workOrders = [item]; return route.fulfill({ status: 201, json: item });
}
if (path.includes("/work-orders/") && request.method() === "PATCH") {
@@ -280,7 +297,8 @@ test.describe("外围工单测试", () => {
await page.getByRole("button", { name: "外围样板管理" }).click();
const panel = page.getByRole("complementary", { name: "外围工单测试" });
await expect(panel.getByText("工单接口测试", { exact: true })).toBeVisible();
await expect(panel.getByLabel("工单列表")).toContainText("暂无测试工单");
await expect(panel.getByLabel("预约工单列表")).toContainText("暂无预约工单");
await expect(panel.getByLabel("投诉工单列表")).toContainText("暂无投诉工单");
await expect(panel.getByRole("textbox", { name: "标题" })).toBeHidden();
await panel.getByRole("button", { name: "新增预约工单" }).click();
let editor = panel.getByRole("region", { name: "新增预约工单" });
@@ -302,7 +320,7 @@ test.describe("外围工单测试", () => {
await panel.getByRole("button", { name: "编辑工单 合成预约维修" }).click();
editor = panel.getByRole("region", { name: "编辑工单 合成预约维修" });
await expect(panel.getByLabel("工单列表").getByRole("region", { name: "编辑工单 合成预约维修" })).toBeVisible();
await expect(panel.getByLabel("预约工单列表").getByRole("region", { name: "编辑工单 合成预约维修" })).toBeVisible();
await expect(editor.locator("xpath=ancestor::article")).toContainText("合成预约维修");
await expect(editor.getByRole("textbox", { name: "标题" })).toBeFocused();
await expect(panel.getByRole("button", { name: "编辑工单 合成预约维修" })).toHaveCount(0);
@@ -387,7 +405,9 @@ test.describe("外围工单测试", () => {
await panel.getByRole("textbox", { name: "事件位置" }).fill("演示路与样板路交叉口");
await panel.getByRole("textbox", { name: "现场描述" }).fill("路面积水并伴随压力突降");
await panel.getByRole("button", { name: "创建事件工单" }).click();
await expect(panel.getByText("爆管上报", { exact: true }).last()).toBeVisible();
await expect(panel.getByText("爆管上报工单", { exact: true }).last()).toBeVisible();
await expect(panel.getByLabel("爆管上报工单列表")).toContainText("爆管上报:演示路与样板路交叉口");
await expect(panel.getByLabel("爆管上报工单列表")).toContainText("爆管上报工单已创建。");
expect(workOrders[0]).toMatchObject({
work_order_type: "burst_report",
scenario_code: "BURST-REPORT-001",
@@ -439,7 +459,7 @@ test.describe("外围工单测试", () => {
page.on("request", (request) => {
if (
request.method() === "POST" &&
request.url().includes("/api/v1/agent/chat/") &&
request.url().includes("/api/v1/") &&
request.postData()
) {
agentRequests.push(request.postDataJSON() as Record<string, unknown>);
@@ -450,7 +470,7 @@ test.describe("外围工单测试", () => {
body: `globalThis.__TJWATER_CONFIG__=${JSON.stringify({
TJWATER_AUTH_MODE: "disabled",
TJWATER_SERVER_API_BASE_URL: "http://server.test",
TJWATER_AGENT_API_BASE_URL: "http://127.0.0.1:8787",
TJWATER_AGENT_URL: "http://127.0.0.1:8787",
TJWATER_PERIPHERAL_SAMPLE_API_BASE_URL: "http://peripheral.test"
})}`
}));
@@ -643,7 +663,7 @@ test.describe("外围工单测试", () => {
status: "open"
});
expect(agentRequests.filter((body) => "trigger_context" in body)).toHaveLength(0);
await page.route("**/api/v1/agent/chat/sessions", (route) => route.fulfill({ json: { sessions: [{
await page.route("**/api/v1/sessions", (route) => route.fulfill({ json: { sessions: [{
id: "browser-session-1", title: "水质投诉调查", created_at: "2026-09-23T01:00:00Z", updated_at: "2026-09-23T01:00:00Z",
trigger: { subject_id: workOrderId, workflow: "water_quality_complaint", occurred_at: "2026-09-23T01:00:00Z" }
}] } }));
@@ -735,7 +755,7 @@ function createDemoDataset(projectId: string) {
}
function createSourceWorkOrders(projectId: string) {
return [
const appointments = [
["合成通信核查", "核查监测点通信和数据上送状态", "open", "high"],
["合成压力偏差复核", "复核压力偏差与历史回放证据", "in_progress", "urgent"],
["合成零流量样本确认", "确认零流量样本及质量标记", "completed", "normal"]
@@ -760,4 +780,48 @@ function createSourceWorkOrders(projectId: string) {
created_by: "synthetic-generator",
updated_by: "synthetic-generator"
}));
const events = [
{
work_order_id: "50000000-0000-4000-8000-000000000004",
title: "水质投诉:演示小区",
summary: "居民反映晨间用水有异味",
priority: "high",
work_order_type: "water_quality_complaint",
location: "演示小区",
scenario_code: "WQ-SECONDARY-SUPPLY-001",
activity_detail: "投诉工单已创建。"
},
{
work_order_id: "50000000-0000-4000-8000-000000000005",
title: "爆管上报:演示路口",
summary: "路面积水并伴随压力突降",
priority: "urgent",
work_order_type: "burst_report",
location: "演示路口",
scenario_code: "BURST-REPORT-001",
activity_detail: "爆管上报工单已创建。"
}
].map(({ activity_detail, ...event }) => ({
...event,
project_id: projectId,
revision: 1,
status: "open",
assignee: null,
scheduled_at: "2026-09-17T08:30:00Z",
feedback_requirements: [],
activity: [{
sequence: 1,
occurred_at: "2026-09-17T08:30:00Z",
actor: "synthetic-generator",
kind: "created",
title: "工单创建",
detail: activity_detail
}],
origin: "synthetic",
created_at: "2026-09-17T08:30:00Z",
updated_at: "2026-09-17T08:30:00Z",
created_by: "synthetic-generator",
updated_by: "synthetic-generator"
}));
return [...appointments, ...events];
}