feat: add project-aware agent workspace
Generic Container CI/CD / test-build-publish (push) Successful in 2m9s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 2m39s

This commit is contained in:
2026-10-22 18:52:18 +08:00
parent c6a0e74e72
commit 2d8506fea0
191 changed files with 12373 additions and 3556 deletions
+81 -83
View File
@@ -3,88 +3,18 @@ import { expect, test } from "@playwright/test";
import { mockAgentApi } from "./support/mock-agent-api";
import { mockWorkbenchContext } from "./support/mock-workbench-context";
const block = {
version: "answer-ui@1",
block_id: "pressure",
fallback_text: "演练压力读数,仍待现场核验。",
spec: {
root: "root",
elements: {
root: {
type: "Stack",
props: { columns: "one", gap: "normal" },
children: ["metrics", "chart", "evidence", "location", "form"]
},
metrics: {
type: "Stack",
props: { columns: "two", gap: "compact" },
children: ["pressure", "flow"]
},
pressure: {
type: "Metric",
props: { label: "压力", value: "0.30", unit: "MPa", source: "演练数据" },
children: []
},
flow: {
type: "Metric",
props: { label: "流量", value: "18", unit: "m³/h", source: "演练数据" },
children: []
},
chart: {
type: "Chart",
props: {
title: "演练监测趋势",
chart_type: "line",
x_data: ["08:00", "09:00"],
series: [{ name: "压力(MPa)", data: [0.3, 0.28] }],
source: "浏览器验收演练数据"
},
children: []
},
evidence: {
type: "Evidence",
props: {
title: "查看演练证据",
summary: "仅用于组件验收,不代表已核验现场事实。",
source: "浏览器演练",
evidence_id: "DEMO-001",
observed_at: null,
kind: "demo"
},
children: []
},
location: {
type: "MapLocation",
props: {
label: "定位演练监测点",
layer: "scada",
feature_ids: ["DEMO-P001"],
source: "演练监测点"
},
children: []
},
form: {
type: "FollowUpForm",
props: {
title: "补充核验资料",
description: "填写后由你主动发送给 Agent。",
fields: [{ name: "location", label: "现场位置", kind: "text", required: true }]
},
children: []
}
}
}
};
import { answerUiDemoBlock as block } from "./support/answer-ui-demo";
import { captureReadingGeometry } from "./support/reading-geometry";
for (const width of [1440, 375, 320]) {
test(`local rich answers stream, restore and submit at ${width}px`, async ({ page }) => {
test(`local rich answers stream, restore and submit at ${width}px`, async ({ page, context }, testInfo) => {
await page.setViewportSize({ width, height: 900 });
await page.emulateMedia({ reducedMotion: "reduce" });
await mockWorkbenchContext(page);
await mockWorkbenchContext(page, undefined, undefined, {}, [], { runtimeMode: "backend" });
await mockAgentApi(page, {
sessions: [
{
session_id: "browser-session-1",
session_id: "browser-history-1",
title: "演练结构化回答",
created_at: Date.now(),
updated_at: Date.now()
@@ -100,12 +30,12 @@ for (const width of [1440, 375, 320]) {
data: { content: "仍需用户补充现场核验资料。", ui_block_ids: ["pressure"] }
}
];
await page.route("**/api/v1/session/browser-session-1", async (route) => {
await page.route("**/api/v1/session/browser-history-1?**", async (route) => {
if (route.request().method() !== "GET") return route.fallback();
await route.fulfill({
headers: { "Access-Control-Allow-Origin": "*" },
json: {
session_id: "browser-session-1",
session_id: "browser-history-1",
title: "演练结构化回答",
purpose: "ordinary_chat",
status: "active",
@@ -191,17 +121,43 @@ for (const width of [1440, 375, 320]) {
await page.mouse.down();
await page.mouse.move(panelWidth, box.y + box.height / 2);
await page.mouse.up();
await expect(panel).toHaveCSS("width", `${panelWidth}px`);
await expect.poll(async () => Math.round((await panel.boundingBox())!.width)).toBe(panelWidth);
const columns = await ui
.locator(".answer-ui-stack-comparison")
.evaluate((element) => getComputedStyle(element).gridTemplateColumns.split(" ").length);
expect(columns).toBe(panelWidth === 640 ? 2 : 1);
expect(columns).toBe(2);
}
}
await expect(ui.getByText("0.30", { exact: false }).first()).toBeVisible();
await ui.getByRole("button", { name: "数据", exact: true }).click();
await expect(panel.getByText("交互回答", { exact: true })).toBeVisible();
await panel.locator(".agent-conversation-scroll").evaluate((element) => { element.scrollTop = 0; });
await expect(panel.getByText("查看演练监测资料", { exact: true })).toBeVisible();
await panel.screenshot({ path: `test-results/agent-chat-redesign-${width}.png` });
const lineTab = ui.getByRole("tab", { name: "折线", exact: true });
await lineTab.focus();
await page.keyboard.press("ArrowRight");
await expect(ui.getByRole("tab", { name: "柱状", exact: true })).toBeFocused();
await page.keyboard.press("ArrowRight");
await expect(ui.getByRole("tab", { name: "数据", exact: true })).toHaveAttribute("aria-selected", "true");
await expect(ui.getByRole("table")).toContainText("0.28");
const evidence = ui.locator("summary", { hasText: "查看演练证据" });
await ui.getByRole("button", { name: "参考压力(MPa)", exact: true }).click();
await expect(ui.getByRole("columnheader", { name: /参考压力/ })).toHaveCount(0);
await ui.getByRole("button", { name: "参考压力(MPa)", exact: true }).click();
await expect(ui.getByRole("columnheader", { name: /参考压力/ })).toBeVisible();
await expect(ui.getByRole("combobox", { name: "区间起点" })).toBeHidden();
await ui.getByRole("button", { name: "筛选数据范围" }).click();
await ui.getByRole("combobox", { name: "区间起点" }).click();
await page.getByRole("option", { name: "09:00", exact: true }).click();
await expect(ui.getByRole("table")).not.toContainText("08:00");
await ui.getByRole("button", { name: "筛选数据范围" }).click();
await expect(ui.getByText("09:00 至 09:00", { exact: true })).toBeVisible();
await expect(ui.getByRole("table")).not.toContainText("08:00");
await ui.getByRole("button", { name: "筛选数据范围" }).click();
await ui.getByRole("button", { name: "重置范围" }).click();
await expect(ui.getByRole("table")).toContainText("08:00");
await ui.getByRole("button", { name: "筛选数据范围" }).click();
expect(prompts).toHaveLength(1);
const evidence = ui.getByRole("button", { name: /查看演练证据/ });
await evidence.focus();
await page.keyboard.press("Enter");
await expect(ui.getByText("仅用于组件验收,不代表已核验现场事实。")).toBeVisible();
@@ -214,9 +170,51 @@ for (const width of [1440, 375, 320]) {
expect(bounds).toBe(true);
await ui.getByRole("table").scrollIntoViewIfNeeded();
await panel.screenshot({ path: `test-results/answer-ui-${width}.png` });
// Restore from canonical persisted parts through the actual history selector.
const copy = panel.getByRole("button", { name: "复制回复", exact: true }).first();
await context.grantPermissions(["clipboard-read", "clipboard-write"]);
await copy.click();
await expect(panel.getByRole("button", { name: "回复已复制", exact: true }).first()).toBeVisible();
expect(await page.evaluate(() => navigator.clipboard.readText())).toContain("仍需用户补充现场核验资料。");
// The same editor and data selection move into the workspace and back.
await ui.getByLabel("现场位置(必填)").fill("尚未提交的现场位置");
await ui.getByRole("tab", { name: "数据", exact: true }).click();
await ui.getByRole("textbox", { name: "搜索演练监测趋势" }).fill("09:00");
await ui.getByRole("checkbox", { name: "选择第 2 行" }).check();
await panel.getByRole("button", { name: "展开查看", exact: true }).click();
const workspace = page.getByRole("region", { name: "展开报告工作区", exact: true });
await expect(workspace).toBeVisible();
await expect(page.getByLabel("现场位置(必填)")).toHaveCount(1);
await expect(workspace.getByLabel("现场位置(必填)")).toHaveValue("尚未提交的现场位置");
await expect(workspace.getByRole("textbox", { name: "搜索演练监测趋势" })).toHaveValue("09:00");
await expect(workspace.getByRole("checkbox", { name: "选择第 2 行" })).toBeChecked();
await workspace.getByRole("tab", { name: "折线", exact: true }).click();
for (const close of await page.getByRole("button", { name: "关闭通知", exact: true }).all()) await close.click();
await page.evaluate(() => document.fonts.ready);
const geometry = await captureReadingGeometry(workspace, testInfo);
expect(geometry.metricsHeight).toBeLessThan(145);
expect(geometry.chartOffset).toBeLessThan(430);
expect(geometry.horizontalOverflow).toBe(false);
await page.screenshot({ path: `test-results/answer-workspace-${width}.png` });
await workspace.getByRole("tab", { name: "数据", exact: true }).click();
await page.keyboard.press("Escape");
await expect(workspace).toHaveCount(0);
await expect(ui.getByLabel("现场位置(必填)")).toHaveValue("尚未提交的现场位置");
await expect(panel.getByRole("button", { name: "展开查看", exact: true })).toBeFocused();
await panel.getByRole("button", { name: "展开查看", exact: true }).click();
await workspace.locator("[data-interactive-table]").getByRole("button", { name: "针对这部分提问" }).click();
await expect(workspace).toHaveCount(0);
await panel.getByText("查看引用内容", { exact: true }).click();
await expect(panel.getByText("引用内容块:pressure", { exact: false })).toBeVisible();
expect(prompts).toHaveLength(1);
await panel.getByRole("button", { name: "移除内容引用" }).click();
// Leave the current editor before restoring canonical persisted history.
await panel.getByRole("button", { name: "新建 Agent 对话" }).click();
await expect(ui).toHaveCount(0);
await panel.getByRole("button", { name: /历史/ }).click();
await page.getByRole("button", { name: /演练结构化回答/ }).click();
await page
.locator("button.agent-history-session-trigger")
.filter({ hasText: "演练结构化回答" })
.click();
await expect(ui).toHaveCount(1);
await expect(ui.getByLabel("现场位置(必填)")).toHaveValue("");
expect(prompts).toHaveLength(1);