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
+78 -3
View File
@@ -7,7 +7,7 @@ test.beforeEach(async ({ page }) => {
await page.emulateMedia({ reducedMotion: "reduce" });
await mockWorkbenchContext(page);
await mockAgentApi(page);
await page.route("**/api/v1/agent/chat/models", (route) => route.fulfill({
await page.route("**/api/v1/models", (route) => route.fulfill({
headers: { "Access-Control-Allow-Origin": "*" },
json: {
default_model: "test/fast",
@@ -59,8 +59,8 @@ for (const width of [1440, 375]) {
const trigger = page.locator('button[aria-label="权限批准模式"]');
await trigger.click();
await waitForMenuAnimations(page.getByRole("menu"));
const request = page.getByRole("menuitem", { name: /工具权限逐次确认/ });
const always = page.getByRole("menuitem", { name: /自动允许本轮权限请求/ });
const request = page.getByRole("menuitem", { name: /需要审批的请求逐次确认/ });
const always = page.getByRole("menuitem", { name: /自动批准本轮工具权限请求/ });
for (const item of [request, always]) {
await item.hover();
await expectThemeColor(item, "background-color", "--action-selection-soft-hover");
@@ -85,6 +85,81 @@ for (const width of [1440, 375]) {
});
}
test("sends the selected approval mode and can return to manual approval", async ({ page }) => {
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click();
const trigger = page.getByRole("button", { name: "权限批准模式" });
for (const [label, mode] of [
["自动批准本轮工具权限请求", "always"],
["需要审批的请求逐次确认", "request"],
] as const) {
await expect(trigger).toBeEnabled();
await trigger.click();
await page.getByRole("menuitem", { name: new RegExp(label) }).click();
await page.getByRole("textbox", { name: "询问供水问题,或描述需要处理的任务" }).fill(`验证权限模式 ${mode}`);
const sent = page.waitForRequest((request) => request.method() === "POST" && request.url().endsWith("/api/v1/responses"));
await page.getByRole("button", { name: "发送 Agent 指令" }).click();
expect((await sent).postDataJSON().tjwater.approval_mode).toBe(mode);
}
await expect(trigger).toBeEnabled();
});
test("replaces a removed model before sending and shows the model actually submitted", async ({ page }) => {
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click();
const trigger = page.getByRole("button", { name: "选择 Agent 模型" });
await expect(trigger).toHaveAccessibleDescription("当前模型:快速");
await trigger.click();
await page.getByRole("menuitem", { name: /深入分析和复杂任务处理/ }).click();
await expect(trigger).toHaveAccessibleDescription("当前模型:专家");
// Simulate an Agent restart with a new catalog while this page stays open.
await page.route("**/api/v1/models", (route) => route.fulfill({
headers: { "Access-Control-Allow-Origin": "*" },
json: {
default_model: "waternetwork/gpt-6-luna",
models: [{ id: "waternetwork/gpt-6-luna", label: "快速", description: "当前可用模型", icon: "bolt" }]
}
}));
await page.getByRole("textbox", { name: "询问供水问题,或描述需要处理的任务" }).fill("检查更新后的快速模型");
const sent = page.waitForRequest((request) => request.method() === "POST" && request.url().endsWith("/api/v1/responses"));
await page.getByRole("button", { name: "发送 Agent 指令" }).click();
expect((await sent).postDataJSON().model).toBe("waternetwork/gpt-6-luna");
await expect(trigger).toHaveAccessibleDescription("当前模型:快速");
});
test("keeps an explicitly selected model when it remains available", async ({ page }) => {
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click();
const trigger = page.getByRole("button", { name: "选择 Agent 模型" });
await expect(trigger).toHaveAccessibleDescription("当前模型:快速");
await trigger.click();
await page.getByRole("menuitem", { name: /深入分析和复杂任务处理/ }).click();
await page.getByRole("textbox", { name: "询问供水问题,或描述需要处理的任务" }).fill("保留专家模型选择");
const sent = page.waitForRequest((request) => request.method() === "POST" && request.url().endsWith("/api/v1/responses"));
await page.getByRole("button", { name: "发送 Agent 指令" }).click();
expect((await sent).postDataJSON().model).toBe("test/expert");
await expect(trigger).toHaveAccessibleDescription("当前模型:专家");
});
test("does not send a generation request when the refreshed catalog is empty", async ({ page }) => {
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click();
await expect(page.getByRole("button", { name: "选择 Agent 模型" })).toHaveAccessibleDescription("当前模型:快速");
await page.route("**/api/v1/models", (route) => route.fulfill({
headers: { "Access-Control-Allow-Origin": "*" }, json: { default_model: "test/fast", models: [] }
}));
const requests: string[] = [];
page.on("request", (request) => {
if (request.method() === "POST" && request.url().endsWith("/api/v1/responses")) requests.push(request.url());
});
await page.getByRole("textbox", { name: "询问供水问题,或描述需要处理的任务" }).fill("检查空模型目录");
await page.getByRole("button", { name: "发送 Agent 指令" }).click();
await expect(page.getByText("Agent 后端当前没有可用模型,请检查模型配置。").first()).toBeVisible();
expect(requests).toEqual([]);
});
async function waitForMenuAnimations(menu: Locator) {
await expect(menu).toBeVisible();
await menu.evaluate((element) => Promise.all(