From 797e28287710b2822cdfe980ec743ddcf3ecde57 Mon Sep 17 00:00:00 2001 From: potter Date: Thu, 14 May 2026 18:47:54 +0800 Subject: [PATCH] Strengthen team action handoffs --- .../teams/components/TeamDetailChrome.tsx | 36 +++++++++++++ .../src/pages/teams/detail.test.tsx | 51 +++++++++++++++++-- .../src/pages/teams/detail.tsx | 32 +++++++++--- .../src/pages/teams/home.test.tsx | 17 +++++++ .../src/pages/teams/home.tsx | 2 + .../shared/navigation/runtimeRoutes.test.ts | 21 +++++--- .../src/shared/navigation/runtimeRoutes.ts | 2 + 7 files changed, 144 insertions(+), 17 deletions(-) diff --git a/apps/aevatar-console-web/src/pages/teams/components/TeamDetailChrome.tsx b/apps/aevatar-console-web/src/pages/teams/components/TeamDetailChrome.tsx index a8d9917f8..0d8207220 100644 --- a/apps/aevatar-console-web/src/pages/teams/components/TeamDetailChrome.tsx +++ b/apps/aevatar-console-web/src/pages/teams/components/TeamDetailChrome.tsx @@ -18,11 +18,19 @@ type TeamActionRailProps = { readonly archiveTeamDisabled?: boolean; readonly archiveTeamHint?: string; readonly conversationActionLabel: string; + readonly deploymentsActionLabel?: string; + readonly deploymentsDisabled?: boolean; + readonly deploymentsHint?: string; readonly editTeamDisabled?: boolean; readonly editTeamLabel: string; readonly editTeamHint?: string; + readonly governanceActionLabel?: string; + readonly governanceDisabled?: boolean; + readonly governanceHint?: string; readonly onArchiveTeam?: () => void; readonly onOpenConversation: () => void; + readonly onOpenDeployments?: () => void; + readonly onOpenGovernance?: () => void; readonly onOpenServiceMapping: () => void; readonly onOpenTeamEditor: () => void; readonly onOpenTeamBuilder: () => void; @@ -72,11 +80,19 @@ export const TeamActionRail: React.FC = ({ archiveTeamDisabled = false, archiveTeamHint, conversationActionLabel, + deploymentsActionLabel, + deploymentsDisabled = false, + deploymentsHint, editTeamDisabled = false, editTeamLabel, editTeamHint, + governanceActionLabel, + governanceDisabled = false, + governanceHint, onArchiveTeam, onOpenConversation, + onOpenDeployments, + onOpenGovernance, onOpenServiceMapping, onOpenTeamEditor, onOpenTeamBuilder, @@ -106,6 +122,26 @@ export const TeamActionRail: React.FC = ({ + {governanceActionLabel && onOpenGovernance ? ( + + ) : null} + {deploymentsActionLabel && onOpenDeployments ? ( + + ) : null} diff --git a/apps/aevatar-console-web/src/pages/teams/detail.test.tsx b/apps/aevatar-console-web/src/pages/teams/detail.test.tsx index a272836e0..a08cba7f0 100644 --- a/apps/aevatar-console-web/src/pages/teams/detail.test.tsx +++ b/apps/aevatar-console-web/src/pages/teams/detail.test.tsx @@ -822,8 +822,10 @@ describe("TeamDetailPage", () => { expect(screen.getByText("Runtime deltas")).toBeTruthy(); expect(await screen.findByText("Step deltas")).toBeTruthy(); expect(await screen.findByText("Handoff deltas")).toBeTruthy(); - expect(screen.getByRole("button", { name: "本次成员对话" })).toBeTruthy(); + expect(screen.getByRole("button", { name: "处理等待 Run" })).toBeTruthy(); expect(screen.getByRole("button", { name: "服务映射" })).toBeTruthy(); + expect(screen.getByRole("button", { name: "治理绑定" })).toBeTruthy(); + expect(screen.getByRole("button", { name: "部署记录" })).toBeTruthy(); expect(screen.getByRole("button", { name: "高级编辑" })).toBeTruthy(); expect(studioApi.getTeam).toHaveBeenCalledWith("scope-1", "t-alpha"); }); @@ -1358,12 +1360,16 @@ describe("TeamDetailPage", () => { await screen.findByRole("button", { name: "服务映射" }); fireEvent.click(screen.getByRole("button", { name: "事件流" })); await screen.findAllByText(/risk_review/); - fireEvent.click(screen.getAllByRole("button", { name: "本次成员对话" })[0]); + fireEvent.click(screen.getAllByRole("button", { name: "处理等待 Run" })[0]); await waitFor(() => { expect(window.location.pathname).toBe("/runtime/runs"); }); - const draftKey = new URLSearchParams(window.location.search).get("draftKey"); + const runsParams = new URLSearchParams(window.location.search); + expect(runsParams.get("runId")).toBe("run-current"); + expect(runsParams.get("scopeId")).toBe("scope-1"); + expect(runsParams.get("serviceOverrideId")).toBe("default"); + const draftKey = runsParams.get("draftKey"); expect(draftKey).toBeTruthy(); expect(loadDraftRunPayload(draftKey)).toMatchObject({ kind: "observed_run_session", @@ -1394,6 +1400,45 @@ describe("TeamDetailPage", () => { expect(params.get("serviceId")).toBe("default"); }); + it("opens Platform governance and deployments from top attention actions", async () => { + renderWithQueryClient(React.createElement(TeamDetailPage)); + + await screen.findByRole("button", { name: "服务映射" }); + await waitFor(() => { + expect(screen.getByRole("button", { name: "治理绑定" })).toBeEnabled(); + expect(screen.getByRole("button", { name: "部署记录" })).toBeEnabled(); + }); + + fireEvent.click(screen.getByRole("button", { name: "治理绑定" })); + + await waitFor(() => { + expect(window.location.pathname).toBe("/governance"); + }); + let params = new URLSearchParams(window.location.search); + expect(params.get("tenantId")).toBe("scope-1"); + expect(params.get("appId")).toBe("default"); + expect(params.get("namespace")).toBe("default"); + expect(params.get("serviceId")).toBe("default"); + expect(params.get("revisionId")).toBe("rev-2"); + expect(params.get("view")).toBe("bindings"); + + act(() => { + history.push("/teams/scope-1/t-alpha"); + }); + await screen.findByRole("button", { name: "部署记录" }); + fireEvent.click(screen.getByRole("button", { name: "部署记录" })); + + await waitFor(() => { + expect(window.location.pathname).toBe("/deployments"); + }); + params = new URLSearchParams(window.location.search); + expect(params.get("tenantId")).toBe("scope-1"); + expect(params.get("appId")).toBe("default"); + expect(params.get("namespace")).toBe("default"); + expect(params.get("serviceId")).toBe("default"); + expect(params.get("deploymentId")).toBe("dep-2"); + }); + it("opens Mission Control from the team event stream with run context", async () => { renderWithQueryClient(React.createElement(TeamDetailPage)); diff --git a/apps/aevatar-console-web/src/pages/teams/detail.tsx b/apps/aevatar-console-web/src/pages/teams/detail.tsx index a73dbdd63..3602c987d 100644 --- a/apps/aevatar-console-web/src/pages/teams/detail.tsx +++ b/apps/aevatar-console-web/src/pages/teams/detail.tsx @@ -3,9 +3,6 @@ import { AGUIEventType, CustomEventName, } from "@aevatar-react-sdk/types"; -import { - DeploymentUnitOutlined, -} from "@ant-design/icons"; import type { Edge, Node } from "@xyflow/react"; import { Input, Modal, Space, Typography, message, theme } from "antd"; import { useQuery, useQueryClient } from "@tanstack/react-query"; @@ -1607,6 +1604,7 @@ const TeamDetailPage: React.FC = () => { endpointId: "chat", endpointKind: "chat", prompt: lens.playback.launchPrompt || undefined, + runId, route: lens.playback.workflowName || undefined, scopeId, serviceId: runtimeServiceId, @@ -1776,6 +1774,11 @@ const TeamDetailPage: React.FC = () => { focusedOperationalUnit?.latestRun?.runId || ""; const currentRevisionId = trimText(lens.activeRevision?.revisionId) || "--"; + const currentGovernanceRevisionId = + trimText(lens.activeRevision?.revisionId) || + trimText(lens.currentService?.activeServingRevisionId) || + trimText(lens.currentService?.defaultServingRevisionId) || + ""; const currentRevisionStatus = trimText(lens.activeRevision?.servingState) || trimText(lens.activeRevision?.status) || @@ -3140,6 +3143,7 @@ const TeamDetailPage: React.FC = () => { scopeId, serviceId: runtimeServiceId, actorId: lens.currentRun?.actorId || undefined, + runId: lens.currentRun?.runId || undefined, }), ); }, [ @@ -3149,8 +3153,16 @@ const TeamDetailPage: React.FC = () => { runtimeServiceId, scopeId, ]); - const conversationActionLabel = lens.playback.currentRunId ? "本次成员对话" : "成员运行记录"; + const conversationActionLabel = lens.playback.currentRunId + ? lens.healthStatus === "blocked" + ? "处理等待 Run" + : lens.healthStatus === "degraded" + ? "排查失败 Run" + : "本次成员对话" + : "成员运行记录"; const serviceMappingActionLabel = "服务映射"; + const governanceActionLabel = "治理绑定"; + const deploymentsActionLabel = "部署记录"; const teamBuilderActionLabel = "高级编辑"; const editTeamActionLabel = "Edit Team"; const canEditSelectedTeam = Boolean(teamSummaryQuery.data && selectedTeamId); @@ -3293,11 +3305,11 @@ const TeamDetailPage: React.FC = () => { history.push( buildPlatformGovernanceHref({ ...platformRouteIdentity, - revisionId: currentRevisionId !== "--" ? currentRevisionId : undefined, + revisionId: currentGovernanceRevisionId || undefined, view: "bindings", }), ); - }, [currentRevisionId, platformRouteIdentity]); + }, [currentGovernanceRevisionId, platformRouteIdentity]); const handleOpenDeployments = React.useCallback(() => { history.push( buildPlatformDeploymentsHref({ @@ -3503,11 +3515,19 @@ const TeamDetailPage: React.FC = () => { archiveTeamDisabled={!teamSummaryQuery.data || teamArchiving} archiveTeamHint={archiveTeamHint} conversationActionLabel={conversationActionLabel} + deploymentsActionLabel={deploymentsActionLabel} + deploymentsDisabled={currentDeploymentId === "--"} + deploymentsHint="当前还没有可定位的部署记录。" editTeamDisabled={!canEditSelectedTeam} editTeamHint={editTeamHint} editTeamLabel={editTeamActionLabel} + governanceActionLabel={governanceActionLabel} + governanceDisabled={!runtimeServiceId} + governanceHint="当前还没有可定位的服务绑定。" onArchiveTeam={openTeamArchive} onOpenConversation={handleOpenConversation} + onOpenDeployments={handleOpenDeployments} + onOpenGovernance={handleOpenGovernance} onOpenServiceMapping={handleOpenServiceMapping} onOpenTeamEditor={openTeamEditor} onOpenTeamBuilder={() => history.push(teamBuilderRoute)} diff --git a/apps/aevatar-console-web/src/pages/teams/home.test.tsx b/apps/aevatar-console-web/src/pages/teams/home.test.tsx index 259f54ce2..d58df5a76 100644 --- a/apps/aevatar-console-web/src/pages/teams/home.test.tsx +++ b/apps/aevatar-console-web/src/pages/teams/home.test.tsx @@ -213,6 +213,23 @@ describe("TeamsHomePage", () => { expect(screen.queryByText("查看运行")).toBeNull(); }); + it("opens the default member run shortcut with current run context", async () => { + renderWithQueryClient(React.createElement(TeamsHomePage)); + + fireEvent.click(await screen.findByRole("button", { name: "更多" })); + fireEvent.click(await screen.findByText("查看默认成员运行")); + + await waitFor(() => { + expect(window.location.pathname).toBe("/runtime/runs"); + }); + + const params = new URLSearchParams(window.location.search); + expect(params.get("actorId")).toBe("actor://workflow-alpha"); + expect(params.get("runId")).toBe("run-latest"); + expect(params.get("scopeId")).toBe("scope-a"); + expect(params.get("serviceOverrideId")).toBe("service-alpha"); + }); + it("routes Create Team to the real create-team page", async () => { renderWithQueryClient(React.createElement(TeamsHomePage)); diff --git a/apps/aevatar-console-web/src/pages/teams/home.tsx b/apps/aevatar-console-web/src/pages/teams/home.tsx index 7fe1c6345..23bef0201 100644 --- a/apps/aevatar-console-web/src/pages/teams/home.tsx +++ b/apps/aevatar-console-web/src/pages/teams/home.tsx @@ -533,6 +533,7 @@ function buildMemberRosterPreview(input: { latestRun?.actorId || matchedService?.primaryActorId || undefined, + runId: latestRun?.runId || undefined, scopeId: input.scopeId, serviceId, }) @@ -650,6 +651,7 @@ function buildTeamRosterPreview(input: { primaryMemberPreview?.serviceId && primaryMemberPreview.serviceId.length > 0 ? buildRuntimeRunsHref({ actorId: latestRun?.actorId || undefined, + runId: latestRun?.runId || undefined, scopeId: input.scopeId, serviceId: primaryMemberPreview.serviceId, }) diff --git a/apps/aevatar-console-web/src/shared/navigation/runtimeRoutes.test.ts b/apps/aevatar-console-web/src/shared/navigation/runtimeRoutes.test.ts index c97a2da9c..f42ed58b8 100644 --- a/apps/aevatar-console-web/src/shared/navigation/runtimeRoutes.test.ts +++ b/apps/aevatar-console-web/src/shared/navigation/runtimeRoutes.test.ts @@ -23,16 +23,21 @@ describe('runtimeRoutes', () => { }); it('lets runs return back to topology detail routes', () => { - expect( - buildRuntimeRunsHref({ + const href = buildRuntimeRunsHref({ + actorId: 'actor://selected', + runId: 'run-1', + returnTo: buildRuntimeExplorerHref({ actorId: 'actor://selected', - returnTo: buildRuntimeExplorerHref({ - actorId: 'actor://selected', - runId: 'run-1', - }), + runId: 'run-1', }), - ).toContain( - 'returnTo=%2Fruntime%2Fexplorer%2Fdetail%3FactorId%3Dactor%253A%252F%252Fselected%26runId%3Drun-1', + }); + const url = new URL(href, 'https://console.aevatar.test'); + + expect(url.pathname).toBe('/runtime/runs'); + expect(url.searchParams.get('actorId')).toBe('actor://selected'); + expect(url.searchParams.get('runId')).toBe('run-1'); + expect(url.searchParams.get('returnTo')).toBe( + '/runtime/explorer/detail?actorId=actor%3A%2F%2Fselected&runId=run-1', ); }); diff --git a/apps/aevatar-console-web/src/shared/navigation/runtimeRoutes.ts b/apps/aevatar-console-web/src/shared/navigation/runtimeRoutes.ts index a4ae11e67..e2df19c04 100644 --- a/apps/aevatar-console-web/src/shared/navigation/runtimeRoutes.ts +++ b/apps/aevatar-console-web/src/shared/navigation/runtimeRoutes.ts @@ -50,6 +50,7 @@ export function buildRuntimeRunsHref(options?: { route?: string; workflow?: string; prompt?: string; + runId?: string; scopeId?: string; serviceOverrideId?: string; serviceId?: string; @@ -64,6 +65,7 @@ export function buildRuntimeRunsHref(options?: { return buildHref(runtimePaths.runs, { route: options?.route ?? options?.workflow, prompt: options?.prompt, + runId: options?.runId, scopeId: options?.scopeId, serviceOverrideId: options?.serviceOverrideId ?? options?.serviceId, endpointId: options?.endpointId,