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
+72
View File
@@ -0,0 +1,72 @@
export const answerUiDemoBlock = {
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] }, { name: "参考压力(MPa)", data: [0.29, 0.26] }],
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: []
}
}
}
};
+6 -2
View File
@@ -34,6 +34,9 @@ export async function mockAgentApi(page: Page, options: MockAgentApiOptions = {}
}
const pathname = new URL(route.request().url()).pathname;
if (pathname.endsWith("/topics") && route.request().method() === "GET") {
await route.fulfill({ headers: corsHeaders, json: { topics: [] } }); return;
}
if (pathname.endsWith("/suggestions") && route.request().method() === "POST") {
await route.fulfill({ headers: corsHeaders, json: {
@@ -94,13 +97,14 @@ export async function mockAgentApi(page: Page, options: MockAgentApiOptions = {}
const sessionMatch = pathname.match(/\/session\/([^/]+)$/);
if (sessionMatch && route.request().method() === "GET") {
const collaboration = sessionMatch[1].startsWith("collaboration-");
const stored = options.sessions?.find((session) => (session.session_id ?? session.id) === sessionMatch[1]);
await route.fulfill({
headers: corsHeaders,
json: {
id: sessionMatch[1],
session_id: sessionMatch[1],
title: collaboration ? "任务协作问答" : "水质投诉事件调查",
purpose: collaboration ? "task_collaboration" : "ordinary_chat",
title: stored?.title ?? (collaboration ? "任务协作问答" : "水质投诉事件调查"),
purpose: stored?.purpose ?? (collaboration ? "task_collaboration" : "ordinary_chat"),
task_id: collaboration ? sessionMatch[1].slice("collaboration-".length) : undefined,
status: "active",
created_at: "2026-09-22T02:00:00Z",
+20
View File
@@ -0,0 +1,20 @@
import type { Locator, TestInfo } from "@playwright/test";
/** Measure the actual reading surface so density changes can be compared with screenshots. */
export async function captureReadingGeometry(workspace: Locator, testInfo: TestInfo) {
const geometry = await workspace.evaluate((element) => {
const scroll = element.querySelector("[data-reading-scroll]")!;
const metrics = element.querySelector(".answer-ui-stack-comparison")!;
const chart = element.querySelector('.answer-ui-chart [role="img"]')!;
const bounds = scroll.getBoundingClientRect();
return {
viewportWidth: window.innerWidth,
readingWidth: Math.round(bounds.width),
metricsHeight: Math.round(metrics.getBoundingClientRect().height),
chartOffset: Math.round(chart.getBoundingClientRect().top - bounds.top + scroll.scrollTop),
horizontalOverflow: document.documentElement.scrollWidth > window.innerWidth,
};
});
await testInfo.attach("reading-geometry", { body: JSON.stringify(geometry, null, 2), contentType: "application/json" });
return geometry;
}
+2 -2
View File
@@ -1,6 +1,6 @@
// Translate existing UI scenarios into Responses wire fixtures. Production
// clients accept Responses only; this helper belongs exclusively to tests.
export function responseFixtureWriter() {
export function responseFixtureWriter(conversationId?: string) {
let sequence = 0;
let id = "resp_browser_fixture";
let messageId = "msg_browser_fixture";
@@ -13,7 +13,7 @@ export function responseFixtureWriter() {
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" } })
return event("response.created", { response: { id, object: "response", status: "in_progress", ...(conversationId ? { conversation: { id: conversationId } } : {}) } })
+ event("response.output_item.added", { output_index: 0, item: { type: "message", role: "assistant", id: messageId, content: [] } });
}
if (chunk.type === "text-start") {