diff --git a/apps/aevatar-console-web/src/pages/studio/index.test.tsx b/apps/aevatar-console-web/src/pages/studio/index.test.tsx index 16b888129..f845e593e 100644 --- a/apps/aevatar-console-web/src/pages/studio/index.test.tsx +++ b/apps/aevatar-console-web/src/pages/studio/index.test.tsx @@ -3201,7 +3201,49 @@ describe("StudioPage", () => { }); }); - it("strips legacy label params while preserving stable scope and member ids", async () => { + it("canonicalizes legacy service member params to real member ids", async () => { + mockStudioMembers = [ + ...mockStudioMembers, + { + memberId: "member-alpha", + scopeId: "scope-a", + displayName: "Member Alpha", + description: "Legacy service-backed member", + implementationKind: "workflow", + lifecycleStage: "bind_ready", + publishedServiceId: "service-alpha", + lastBoundRevisionId: "rev-alpha", + createdAt: "2026-04-27T08:00:00Z", + updatedAt: "2026-04-27T08:05:00Z", + }, + ]; + mockScopeRuntimeApi.listServices.mockResolvedValue([ + { + serviceId: "service-alpha", + displayName: "Member Alpha", + deploymentStatus: "Active", + primaryActorId: "actor-alpha", + endpoints: [ + { + endpointId: "chat", + displayName: "Chat", + kind: "chat", + description: "Chat with Alpha.", + requestTypeUrl: "", + responseTypeUrl: "", + }, + ], + }, + ]); + mockScopeRuntimeApi.getServiceRevisions.mockImplementation( + async () => + mockBuildServiceRevisionCatalog({ + serviceId: "service-alpha", + displayName: "Member Alpha", + workflowName: "workspace-demo", + }) + ); + renderStudioPage( "/studio?scopeId=scope-a&scopeLabel=%E5%9B%A2%E9%98%9F+A&memberId=service-alpha&memberLabel=%E6%88%90%E5%91%98+Alpha&focus=workflow%3Aworkflow-1&tab=studio" ); @@ -3209,6 +3251,7 @@ describe("StudioPage", () => { expect(await screen.findByRole("button", { name: "返回团队" })).toBeTruthy(); await waitFor(() => { expect(screen.getByTestId("studio-context-meta")).toHaveTextContent("service-alpha"); + expect(window.location.search).toContain("member=member%3Amember-alpha"); }); expect(screen.getByTestId("studio-context-meta")).not.toHaveTextContent("团队 A"); expect(screen.getByTestId("studio-context-meta")).not.toHaveTextContent("成员 Alpha"); @@ -3220,31 +3263,105 @@ describe("StudioPage", () => { const searchParams = new URLSearchParams(window.location.search); expect(searchParams.get("scopeId")).toBe("scope-a"); - expect(searchParams.get("member")).toBe("member:service-alpha"); + expect(searchParams.get("member")).toBe("member:member-alpha"); expect(searchParams.get("memberId")).toBeNull(); expect(searchParams.get("scopeLabel")).toBeNull(); expect(searchParams.get("memberLabel")).toBeNull(); expect(searchParams.get("focus")).toBe("workflow:workflow-1"); expect(searchParams.get("tab")).toBe("studio"); + expect(studioApi.getMember).toHaveBeenCalledWith("scope-a", "member-alpha"); + expect(studioApi.getMember).not.toHaveBeenCalledWith("scope-a", "service-alpha"); }); - it("resyncs the Studio state from stable scope and member ids when the route changes after mount", async () => { + it("keeps direct member route keys as canonical member ids", async () => { + renderStudioPage( + "/studio?scopeId=scope-1&member=member%3Aworkspace-demo&focus=workflow%3Aworkflow-1&tab=studio" + ); + + expect(await screen.findByRole("button", { name: "返回团队" })).toBeTruthy(); + await waitFor(() => { + expect(window.location.search).toContain("member=member%3Aworkspace-demo"); + }); + + const searchParams = new URLSearchParams(window.location.search); + expect(searchParams.get("member")).toBe("member:workspace-demo"); + expect(searchParams.get("memberId")).toBeNull(); + expect(studioApi.getMember).toHaveBeenCalledWith("scope-1", "workspace-demo"); + }); + + it("resyncs the Studio state from legacy service params when the route changes after mount", async () => { + mockStudioMembers = [ + ...mockStudioMembers, + { + memberId: "member-alpha", + scopeId: "scope-a", + displayName: "Member Alpha", + description: "Legacy service-backed member", + implementationKind: "workflow", + lifecycleStage: "bind_ready", + publishedServiceId: "service-alpha", + lastBoundRevisionId: "rev-alpha", + createdAt: "2026-04-27T08:00:00Z", + updatedAt: "2026-04-27T08:05:00Z", + }, + { + memberId: "member-beta", + scopeId: "scope-b", + displayName: "Member Beta", + description: "Legacy service-backed member", + implementationKind: "workflow", + lifecycleStage: "bind_ready", + publishedServiceId: "service-beta", + lastBoundRevisionId: "rev-beta", + createdAt: "2026-04-27T08:00:00Z", + updatedAt: "2026-04-27T08:05:00Z", + }, + ]; + mockScopeRuntimeApi.listServices.mockImplementation(async (scopeId: string) => [ + { + serviceId: scopeId === "scope-b" ? "service-beta" : "service-alpha", + displayName: scopeId === "scope-b" ? "Member Beta" : "Member Alpha", + deploymentStatus: "Active", + primaryActorId: scopeId === "scope-b" ? "actor-beta" : "actor-alpha", + endpoints: [ + { + endpointId: "chat", + displayName: "Chat", + kind: "chat", + description: "Chat with the member.", + requestTypeUrl: "", + responseTypeUrl: "", + }, + ], + }, + ]); + mockScopeRuntimeApi.getServiceRevisions.mockImplementation( + async (_scopeId: string, serviceId: string) => + mockBuildServiceRevisionCatalog({ + serviceId, + displayName: serviceId === "service-beta" ? "Member Beta" : "Member Alpha", + workflowName: "workspace-demo", + }) + ); + renderStudioPage( "/studio?scopeId=scope-a&scopeLabel=%E5%9B%A2%E9%98%9F+A&memberId=service-alpha&memberLabel=%E6%88%90%E5%91%98+Alpha&focus=workflow%3Aworkflow-1&tab=studio" ); expect(await screen.findByRole("button", { name: "返回团队" })).toBeTruthy(); - expect(screen.getByTestId("studio-context-meta")).toHaveTextContent("service-alpha"); + await waitFor(() => { + expect(window.location.search).toContain("member=member%3Amember-alpha"); + }); await replaceStudioRoute( "/studio?scopeId=scope-b&scopeLabel=%E5%9B%A2%E9%98%9F+B&memberId=service-beta&memberLabel=%E6%88%90%E5%91%98+Beta&tab=workflows" ); expect(await screen.findByRole("button", { name: "返回团队" })).toBeTruthy(); - expect(screen.getByTestId("studio-context-title")).toHaveTextContent( - "workspace-demo" - ); - expect(screen.getByTestId("studio-context-meta")).toHaveTextContent("service-beta"); + await waitFor(() => { + expect(screen.getByTestId("studio-context-meta")).toHaveTextContent("service-beta"); + expect(window.location.search).toContain("member=member%3Amember-beta"); + }); expect(screen.getByTestId("studio-context-meta")).not.toHaveTextContent("团队 B"); expect(screen.getByTestId("studio-context-meta")).not.toHaveTextContent("成员 Beta"); expect(screen.getByTestId("studio-workflow-build-panel")).toBeTruthy(); @@ -3260,12 +3377,14 @@ describe("StudioPage", () => { const searchParams = new URLSearchParams(window.location.search); expect(searchParams.get("scopeId")).toBe("scope-b"); - expect(searchParams.get("member")).toBe("member:service-beta"); + expect(searchParams.get("member")).toBe("member:member-beta"); expect(searchParams.get("memberId")).toBeNull(); expect(searchParams.get("scopeLabel")).toBeNull(); expect(searchParams.get("memberLabel")).toBeNull(); expect(searchParams.get("focus")).toBe("workflow:workflow-1"); expect(searchParams.get("tab")).toBe("studio"); + expect(studioApi.getMember).toHaveBeenCalledWith("scope-b", "member-beta"); + expect(studioApi.getMember).not.toHaveBeenCalledWith("scope-b", "service-beta"); }); it("ignores removed create-team route params and falls back to the explicit member-selection empty state", async () => { @@ -3659,7 +3778,7 @@ describe("StudioPage", () => { }); it("returns to canonical Team detail when Studio has Team context", async () => { - renderStudioPage("/studio?scopeId=scope-1&teamId=t-alpha&memberId=workspace-demo&focus=workflow%3Aworkflow-1&tab=studio"); + renderStudioPage("/studio?scopeId=scope-1&teamId=t-alpha&member=member%3Aworkspace-demo&focus=workflow%3Aworkflow-1&tab=studio"); fireEvent.click(await screen.findByRole("button", { name: "返回团队" })); @@ -4031,7 +4150,7 @@ describe("StudioPage", () => { }); it("carries the selected bind contract into invoke after continuing from build", async () => { - renderStudioPage("/studio?scopeId=scope-1&memberId=workspace-demo&focus=workflow%3Aworkflow-1&tab=studio"); + renderStudioPage("/studio?scopeId=scope-1&member=member%3Aworkspace-demo&focus=workflow%3Aworkflow-1&tab=studio"); expect(await screen.findByTestId("studio-workflow-build-panel")).toBeTruthy(); @@ -4115,6 +4234,21 @@ describe("StudioPage", () => { }); it("shows an invoke empty state when a bound member has no endpoint data", async () => { + mockStudioMembers = [ + ...mockStudioMembers, + { + memberId: "script-member", + scopeId: "scope-1", + displayName: "script-alpha", + description: "Script member with no endpoints", + implementationKind: "script", + lifecycleStage: "bind_ready", + publishedServiceId: "script-alpha", + lastBoundRevisionId: "rev-script-alpha", + createdAt: "2026-04-27T08:00:00Z", + updatedAt: "2026-04-27T08:05:00Z", + }, + ]; mockScopeRuntimeApi.listServices.mockResolvedValueOnce([ { serviceId: "script-alpha", @@ -4126,15 +4260,17 @@ describe("StudioPage", () => { ]); renderStudioPage( - "/studio?scopeId=scope-1&memberId=script-alpha&step=invoke&tab=invoke" + "/studio?scopeId=scope-1&member=member%3Ascript-member&step=invoke&tab=invoke" ); expect(await screen.findByTestId("studio-invoke-surface")).toBeTruthy(); - expect(screen.getByText("service:script-alpha")).toBeTruthy(); - expect(screen.getByText("member:script-alpha")).toBeTruthy(); - expect(screen.getByText("services:none")).toBeTruthy(); - expect(screen.getByText("endpoint:no-endpoint")).toBeTruthy(); - expect(screen.getByText(/empty:script-alpha 还不能直接调用。/)).toBeTruthy(); + await waitFor(() => { + expect(screen.getByText("service:script-alpha")).toBeTruthy(); + expect(screen.getByText("member:script-alpha")).toBeTruthy(); + expect(screen.getByText("services:none")).toBeTruthy(); + expect(screen.getByText("endpoint:no-endpoint")).toBeTruthy(); + expect(screen.getByText(/empty:script-alpha 还不能直接调用。/)).toBeTruthy(); + }); }); it("surfaces the current workflow as a bind candidate before any published service exists", async () => { @@ -4161,7 +4297,7 @@ describe("StudioPage", () => { ]); (studioApi.getScopeBinding as jest.Mock).mockResolvedValueOnce(null); - renderStudioPage("/studio?scopeId=scope-1&memberId=workspace-demo&focus=workflow%3Aworkflow-1&tab=studio"); + renderStudioPage("/studio?scopeId=scope-1&member=member%3Aworkspace-demo&focus=workflow%3Aworkflow-1&tab=studio"); expect(await screen.findByTestId("studio-workflow-build-panel")).toBeTruthy(); @@ -4235,7 +4371,7 @@ describe("StudioPage", () => { updatedAt: "2026-04-27T08:15:01Z", }); - renderStudioPage("/studio?scopeId=scope-1&memberId=workspace-demo&focus=workflow%3Aworkflow-1&tab=studio"); + renderStudioPage("/studio?scopeId=scope-1&member=member%3Aworkspace-demo&focus=workflow%3Aworkflow-1&tab=studio"); expect(await screen.findByTestId("studio-workflow-build-panel")).toBeTruthy(); @@ -4522,7 +4658,7 @@ describe("StudioPage", () => { ); renderStudioPage( - "/studio?scopeId=scope-1&memberId=draft1&focus=workflow%3Aworkflow-1&step=bind&tab=bindings" + "/studio?scopeId=scope-1&member=member%3Adraft1&focus=workflow%3Aworkflow-1&step=bind&tab=bindings" ); expect(await screen.findByTestId("studio-bind-surface")).toBeTruthy(); @@ -4645,7 +4781,7 @@ describe("StudioPage", () => { ); renderStudioPage( - "/studio?scopeId=scope-1&memberId=joker&focus=workflow%3Aworkflow-1&tab=studio" + "/studio?scopeId=scope-1&member=member%3Ajoker&focus=workflow%3Aworkflow-1&tab=studio" ); expect(await screen.findByTestId("studio-workflow-build-panel")).toBeTruthy(); @@ -4744,7 +4880,7 @@ describe("StudioPage", () => { }) ); - renderStudioPage("/studio?scopeId=scope-1&memberId=joker&step=bind&tab=bindings"); + renderStudioPage("/studio?scopeId=scope-1&member=member%3Ajoker&step=bind&tab=bindings"); expect(await screen.findByTestId("studio-bind-surface")).toBeTruthy(); await waitFor(() => { @@ -5004,7 +5140,7 @@ describe("StudioPage", () => { }) ); - renderStudioPage("/studio?scopeId=scope-1&memberId=joker&step=bind&tab=bindings"); + renderStudioPage("/studio?scopeId=scope-1&member=member%3Ajoker&step=bind&tab=bindings"); expect(await screen.findByTestId("studio-bind-surface")).toBeTruthy(); await waitFor(() => { @@ -5862,7 +5998,7 @@ describe("StudioPage", () => { ); renderStudioPage( - "/studio?scopeId=scope-1&memberId=script-member&step=bind&tab=bindings" + "/studio?scopeId=scope-1&member=member%3Ascript-member&step=bind&tab=bindings" ); expect(await screen.findByTestId("studio-bind-surface")).toBeTruthy(); @@ -6173,7 +6309,7 @@ describe("StudioPage", () => { }); it("opens the Studio invoke surface from the bind surface endpoint action", async () => { - renderStudioPage("/studio?scopeId=scope-1&teamId=t-alpha&memberId=workspace-demo&focus=workflow%3Aworkflow-1&tab=studio"); + renderStudioPage("/studio?scopeId=scope-1&teamId=t-alpha&member=member%3Aworkspace-demo&focus=workflow%3Aworkflow-1&tab=studio"); fireEvent.click(await screen.findByRole("button", { name: "Bind" })); await waitFor(() => { @@ -6330,7 +6466,7 @@ describe("StudioPage", () => { }); it("walks the lifecycle flow from build to bind to invoke to observe", async () => { - renderStudioPage("/studio?scopeId=scope-1&memberId=workspace-demo&focus=workflow%3Aworkflow-1&tab=studio"); + renderStudioPage("/studio?scopeId=scope-1&member=member%3Aworkspace-demo&focus=workflow%3Aworkflow-1&tab=studio"); expect(await screen.findByTestId("studio-workflow-build-panel")).toBeTruthy(); diff --git a/apps/aevatar-console-web/src/pages/studio/index.tsx b/apps/aevatar-console-web/src/pages/studio/index.tsx index 27a240867..61e9919f2 100644 --- a/apps/aevatar-console-web/src/pages/studio/index.tsx +++ b/apps/aevatar-console-web/src/pages/studio/index.tsx @@ -161,6 +161,7 @@ type StudioRouteState = { teamId: string; memberKey: string; memberId: string; + legacyServiceId: string; step: StudioStep; focusKey: string; tab: StudioTab; @@ -184,6 +185,7 @@ type StudioRouteMemberState = { value: string; memberId: string; serviceId: string; + legacyServiceId: string; }; type BuildMode = 'workflow' | 'script' | 'gagent'; @@ -800,8 +802,9 @@ function parseStudioRouteMember( value: workflowRouteValue, memberId: '', serviceId: '', + legacyServiceId: '', } - : { key: '', kind: 'none', value: '', memberId: '', serviceId: '' }; + : { key: '', kind: 'none', value: '', memberId: '', serviceId: '', legacyServiceId: '' }; } if (normalizedValue.startsWith('script:')) { @@ -813,8 +816,9 @@ function parseStudioRouteMember( value: scriptId, memberId: '', serviceId: '', + legacyServiceId: '', } - : { key: '', kind: 'none', value: '', memberId: '', serviceId: '' }; + : { key: '', kind: 'none', value: '', memberId: '', serviceId: '', legacyServiceId: '' }; } if (normalizedValue.startsWith('member:')) { @@ -826,8 +830,9 @@ function parseStudioRouteMember( value: memberId, memberId, serviceId: '', + legacyServiceId: '', } - : { key: '', kind: 'none', value: '', memberId: '', serviceId: '' }; + : { key: '', kind: 'none', value: '', memberId: '', serviceId: '', legacyServiceId: '' }; } return { @@ -836,6 +841,7 @@ function parseStudioRouteMember( value: '', memberId: '', serviceId: '', + legacyServiceId: '', }; } @@ -855,8 +861,15 @@ function readStudioRouteMemberFromParams( const legacyMemberId = trimOptional(params.get('memberId')); return legacyMemberId - ? parseStudioRouteMember(`member:${legacyMemberId}`) - : { key: '', kind: 'none', value: '', memberId: '', serviceId: '' }; + ? { + key: `member:${legacyMemberId}`, + kind: 'member', + value: legacyMemberId, + memberId: '', + serviceId: '', + legacyServiceId: legacyMemberId, + } + : { key: '', kind: 'none', value: '', memberId: '', serviceId: '', legacyServiceId: '' }; } function buildStudioBuildFocusKey(input: { @@ -1192,6 +1205,7 @@ function readStudioRouteState(search?: string): StudioRouteState { teamId: '', memberKey: '', memberId: '', + legacyServiceId: '', step: 'build', focusKey: '', tab: 'workflows', @@ -1216,6 +1230,7 @@ function readStudioRouteState(search?: string): StudioRouteState { teamId: trimOptional(params.get('teamId')), memberKey: routeMember.key, memberId: routeMember.memberId, + legacyServiceId: routeMember.legacyServiceId, step: parseStudioStep(params.get('step')), focusKey: buildFocus.key, tab: parseStudioTab(params.get('tab')), @@ -2017,6 +2032,7 @@ function buildStudioFocusKey(input: { activeBuildFocusKey?: string; routeMemberKey?: string; routeMemberId?: string; + routeLegacyServiceId?: string; }): string { const routeMemberKey = parseStudioRouteMember(input.routeMemberKey).key; if (routeMemberKey.startsWith('member:')) { @@ -2037,9 +2053,26 @@ function buildStudioFocusKey(input: { return `member:${routeMemberId}`; } + const routeLegacyServiceId = trimOptional(input.routeLegacyServiceId); + if (routeLegacyServiceId) { + return `member:${routeLegacyServiceId}`; + } + return ''; } +function shouldTreatRouteMemberAsBuildFocus(input: { + routeMemberKey?: string; + routeMemberId?: string; + routeLegacyServiceId?: string; +}): boolean { + return Boolean( + trimOptional(input.routeMemberKey) || + trimOptional(input.routeMemberId) || + trimOptional(input.routeLegacyServiceId), + ); +} + type PublishedStudioMemberRecord = { readonly memberSummary?: StudioMemberSummary | null; readonly service: { @@ -2056,6 +2089,78 @@ type PublishedStudioMemberRecord = { readonly revision?: StudioMemberBindingRevision | null; }; +function findDirectStudioMemberSummary( + memberId: string, + publishedMembers: readonly PublishedStudioMemberRecord[], + studioScopeMembers: readonly StudioMemberSummary[], +): StudioMemberSummary | null { + const normalizedMemberId = trimOptional(memberId); + if (!normalizedMemberId) { + return null; + } + + return ( + studioScopeMembers.find( + (member) => trimOptional(member.memberId) === normalizedMemberId, + ) ?? + publishedMembers.find( + ({ memberSummary }) => + trimOptional(memberSummary?.memberId) === normalizedMemberId, + )?.memberSummary ?? + null + ); +} + +function findLegacyServiceBackedStudioMemberSummary( + serviceId: string, + publishedMembers: readonly PublishedStudioMemberRecord[], + studioScopeMembers: readonly StudioMemberSummary[], +): StudioMemberSummary | null { + const normalizedServiceId = trimOptional(serviceId); + if (!normalizedServiceId) { + return null; + } + + const directRosterMatch = + studioScopeMembers.find( + (member) => + trimOptional(member.publishedServiceId) === normalizedServiceId, + ) ?? null; + if (directRosterMatch) { + return directRosterMatch; + } + + return ( + publishedMembers.find( + ({ service, memberSummary }) => + trimOptional(memberSummary?.publishedServiceId) === normalizedServiceId || + trimOptional(service.serviceId) === normalizedServiceId, + )?.memberSummary ?? null + ); +} + +function isKnownLegacyServiceMemberToken( + token: string, + publishedMembers: readonly PublishedStudioMemberRecord[], + studioScopeMembers: readonly StudioMemberSummary[], +): boolean { + const normalizedToken = trimOptional(token); + if (!normalizedToken) { + return false; + } + + return ( + studioScopeMembers.some( + (member) => trimOptional(member.publishedServiceId) === normalizedToken, + ) || + publishedMembers.some( + ({ service, memberSummary }) => + trimOptional(memberSummary?.publishedServiceId) === normalizedToken || + trimOptional(service.serviceId) === normalizedToken, + ) + ); +} + function resolveStudioMemberSummaryFromMemberKey( memberKey: string, publishedMembers: readonly PublishedStudioMemberRecord[], @@ -2063,31 +2168,26 @@ function resolveStudioMemberSummaryFromMemberKey( ): StudioMemberSummary | null { const parsedMember = parseStudioRouteMember(memberKey); if (parsedMember.kind === 'member') { - const directMemberMatch = - studioScopeMembers.find( - (member) => trimOptional(member.memberId) === parsedMember.memberId, - ) ?? null; + const directMemberMatch = findDirectStudioMemberSummary( + parsedMember.memberId, + publishedMembers, + studioScopeMembers, + ); if (directMemberMatch) { return directMemberMatch; } const legacyPublishedServiceMatch = - studioScopeMembers.find( - (member) => - trimOptional(member.publishedServiceId) === parsedMember.memberId, - ) ?? null; + findLegacyServiceBackedStudioMemberSummary( + parsedMember.memberId, + publishedMembers, + studioScopeMembers, + ); if (legacyPublishedServiceMatch) { return legacyPublishedServiceMatch; } - return ( - publishedMembers.find( - ({ service, memberSummary }) => - trimOptional(memberSummary?.memberId) === parsedMember.memberId || - trimOptional(memberSummary?.publishedServiceId) === parsedMember.memberId || - trimOptional(service.serviceId) === parsedMember.memberId, - )?.memberSummary ?? null - ); + return null; } const workflowRouteValue = readWorkflowMemberRouteValueFromMemberKey(memberKey); @@ -2114,7 +2214,7 @@ function resolveStudioMemberSummaryFromMemberKey( return null; } -function resolvePublishedMemberIdFromServiceId( +function resolvePublishedMemberIdFromLegacyServiceId( serviceId: string, publishedMembers: readonly PublishedStudioMemberRecord[], studioScopeMembers: readonly StudioMemberSummary[], @@ -2124,21 +2224,50 @@ function resolvePublishedMemberIdFromServiceId( return ''; } - const directRosterMatch = - studioScopeMembers.find( - (member) => trimOptional(member.publishedServiceId) === normalizedServiceId, - ) ?? null; - if (directRosterMatch) { - return trimOptional(directRosterMatch.memberId); + return trimOptional( + findLegacyServiceBackedStudioMemberSummary( + normalizedServiceId, + publishedMembers, + studioScopeMembers, + )?.memberId, + ); +} + +function resolveCanonicalMemberIdFromRouteMemberKey( + memberKey: string, + publishedMembers: readonly PublishedStudioMemberRecord[], + studioScopeMembers: readonly StudioMemberSummary[], +): string { + const parsedMember = parseStudioRouteMember(memberKey); + if (parsedMember.kind !== 'member') { + return ''; } - return trimOptional( - publishedMembers.find( - ({ memberSummary, service }) => - trimOptional(memberSummary?.publishedServiceId) === normalizedServiceId || - trimOptional(service.serviceId) === normalizedServiceId, - )?.memberSummary?.memberId, + const directMember = findDirectStudioMemberSummary( + parsedMember.memberId, + publishedMembers, + studioScopeMembers, + ); + if (directMember) { + return trimOptional(directMember.memberId); + } + + const legacyMemberId = resolvePublishedMemberIdFromLegacyServiceId( + parsedMember.memberId, + publishedMembers, + studioScopeMembers, ); + if (legacyMemberId) { + return legacyMemberId; + } + + return isKnownLegacyServiceMemberToken( + parsedMember.memberId, + publishedMembers, + studioScopeMembers, + ) + ? '' + : parsedMember.memberId; } function resolveStudioServiceDefaultEndpointId( @@ -2400,6 +2529,7 @@ const StudioPage: React.FC = () => { : ''), [routeState.memberId, routeState.memberKey], ); + const currentRouteMemberToken = readMemberIdFromMemberKey(routeSelectedMemberKey); const isStudioLocation = typeof window !== 'undefined' && window.location.pathname === '/studio'; const nyxIdConfig = useMemo(() => getNyxIDRuntimeConfig(), []); @@ -2475,10 +2605,15 @@ const StudioPage: React.FC = () => { const [recentlyBoundMemberKey, setRecentlyBoundMemberKey] = useState(''); const [recentlyBoundServiceId, setRecentlyBoundServiceId] = useState(''); const recentlyBoundServiceRef = useRef(null); + const legacyRouteServiceIdRef = useRef( + trimOptional(initialRouteState.legacyServiceId), + ); + const currentRouteLegacyServiceId = trimOptional(routeState.legacyServiceId); + if (currentRouteLegacyServiceId) { + legacyRouteServiceIdRef.current = currentRouteLegacyServiceId; + } const pinnedRouteBackendMemberIdRef = useRef( - initialSelectedMember.kind === 'member' - ? trimOptional(initialSelectedMember.memberId) - : '', + trimOptional(initialRouteState.memberId), ); const [pinnedRouteBackendMemberId, setPinnedRouteBackendMemberId] = useState( () => pinnedRouteBackendMemberIdRef.current, @@ -2897,33 +3032,33 @@ const StudioPage: React.FC = () => { return ''; } - const routeMemberToken = readMemberIdFromMemberKey(routeSelectedMemberKey); - const directRouteMember = studioScopeMembers.find( - (member) => trimOptional(member.memberId) === routeMemberToken, - ); - if (directRouteMember) { - return trimOptional(directRouteMember.memberId); + const routeLegacyServiceId = trimOptional(routeState.legacyServiceId); + if (routeLegacyServiceId) { + return resolvePublishedMemberIdFromLegacyServiceId( + routeLegacyServiceId, + publishedScopeMembers, + studioScopeMembers, + ); } - const serviceBackedRouteMember = studioScopeMembers.find( - (member) => trimOptional(member.publishedServiceId) === routeMemberToken, - ); - if (serviceBackedRouteMember) { - return trimOptional(serviceBackedRouteMember.memberId); + const legacyRouteServiceId = trimOptional(legacyRouteServiceIdRef.current); + if (legacyRouteServiceId && currentRouteMemberToken === legacyRouteServiceId) { + return resolvePublishedMemberIdFromLegacyServiceId( + legacyRouteServiceId, + publishedScopeMembers, + studioScopeMembers, + ); } - const routeMemberSummary = resolveStudioMemberSummaryFromMemberKey( + return resolveCanonicalMemberIdFromRouteMemberKey( routeSelectedMemberKey, publishedScopeMembers, studioScopeMembers, ); - - return ( - trimOptional(routeMemberSummary?.memberId) || - routeMemberToken - ); }, [ + currentRouteMemberToken, publishedScopeMembers, + routeState.legacyServiceId, routeSelectedMember.kind, routeSelectedMemberKey, studioScopeMembers, @@ -2946,10 +3081,21 @@ const StudioPage: React.FC = () => { ); }, [currentExplicitRouteBackendMemberId]); const routeSelectedBackendMemberId = useMemo( - () => - trimOptional(explicitRouteBackendMemberId) || - trimOptional(pinnedRouteBackendMemberId), - [explicitRouteBackendMemberId, pinnedRouteBackendMemberId], + () => { + const explicitMemberId = trimOptional(explicitRouteBackendMemberId); + if (explicitMemberId) { + return explicitMemberId; + } + + return routeSelectedMember.kind === 'member' + ? '' + : trimOptional(pinnedRouteBackendMemberId); + }, + [ + explicitRouteBackendMemberId, + pinnedRouteBackendMemberId, + routeSelectedMember.kind, + ], ); const routeSelectedBackendMemberKey = useMemo( () => buildBackendMemberKey(routeSelectedBackendMemberId), @@ -3373,7 +3519,8 @@ const StudioPage: React.FC = () => { templateWorkflow || routeBuildFocus.kind === 'workflow' || routeSelectedMember.kind === 'workflow' || - trimOptional(routeState.memberId) + trimOptional(routeState.memberId) || + trimOptional(routeState.legacyServiceId) ) { return; } @@ -3389,6 +3536,7 @@ const StudioPage: React.FC = () => { }, [ routeBuildFocus.kind, routeSelectedMember.kind, + routeState.legacyServiceId, routeState.memberId, selectedWorkflowId, templateWorkflow, @@ -4374,11 +4522,12 @@ const StudioPage: React.FC = () => { : '') || activeBuildFocusKey || (() => { - const resolvedBoundMemberId = resolvePublishedMemberIdFromServiceId( - boundServiceId, - publishedScopeMembers, - studioScopeMembers, - ); + const resolvedBoundMemberId = + resolvePublishedMemberIdFromLegacyServiceId( + boundServiceId, + publishedScopeMembers, + studioScopeMembers, + ); return resolvedBoundMemberId ? `member:${resolvedBoundMemberId}` : `member:${boundServiceId}`; @@ -4409,7 +4558,7 @@ const StudioPage: React.FC = () => { ); const resolvedBoundMemberId = resolvedBuildMemberId || - resolvePublishedMemberIdFromServiceId( + resolvePublishedMemberIdFromLegacyServiceId( boundServiceId, publishedScopeMembers, studioScopeMembers, @@ -5987,13 +6136,14 @@ const StudioPage: React.FC = () => { (serviceId: string, endpointId: string) => { const routeMemberSummary = resolveStudioMemberSummaryFromMemberKey( trimOptional(routeState.memberKey) || - buildBackendMemberKey(routeState.memberId), + buildBackendMemberKey(routeState.memberId) || + buildBackendMemberKey(routeState.legacyServiceId), publishedScopeMembers, studioScopeMembers, ); const resolvedMemberId = trimOptional(routeMemberSummary?.memberId) || - resolvePublishedMemberIdFromServiceId( + resolvePublishedMemberIdFromLegacyServiceId( serviceId, publishedScopeMembers, studioScopeMembers, @@ -6029,6 +6179,7 @@ const StudioPage: React.FC = () => { history, publishedScopeMembers, resolvedStudioScopeId, + routeState.legacyServiceId, routeState.memberId, routeState.memberKey, routeState.teamId, @@ -6057,15 +6208,36 @@ const StudioPage: React.FC = () => { : isObserveSurface ? 'observe' : 'build'; - const buildSurfaceMemberKey = useMemo( - () => - buildStudioFocusKey({ - activeBuildFocusKey, - routeMemberKey: routeSelectedMemberKey, - routeMemberId: routeState.memberId, - }), - [activeBuildFocusKey, routeSelectedMemberKey, routeState.memberId], - ); + const buildSurfaceMemberKey = useMemo(() => { + const routeMemberKey = trimOptional(routeSelectedMemberKey); + const routeMemberId = trimOptional(routeState.memberId); + const routeLegacyServiceId = trimOptional(routeState.legacyServiceId); + if ( + shouldTreatRouteMemberAsBuildFocus({ + routeMemberKey, + routeMemberId, + routeLegacyServiceId, + }) + ) { + return buildStudioFocusKey({ + routeMemberKey, + routeMemberId, + routeLegacyServiceId, + }); + } + + return buildStudioFocusKey({ + activeBuildFocusKey, + routeMemberKey, + routeMemberId, + routeLegacyServiceId, + }); + }, [ + activeBuildFocusKey, + routeSelectedMemberKey, + routeState.legacyServiceId, + routeState.memberId, + ]); const selectedWorkflowSummary = useMemo( () => visibleWorkflowSummaries.find( @@ -6268,6 +6440,14 @@ const StudioPage: React.FC = () => { : ''); const currentFocusMemberKey = studioSurface === 'build' ? buildSurfaceMemberKey : lifecycleSurfaceMemberKey; + const routeMemberToken = readMemberIdFromMemberKey(routeSelectedMemberKey); + const canonicalLegacyRouteMemberKey = + routeSelectedMember.kind === 'member' && + routeMemberToken && + routeSelectedBackendMemberId && + routeSelectedBackendMemberId !== routeMemberToken + ? buildBackendMemberKey(routeSelectedBackendMemberId) + : ''; useEffect(() => { if ( studioSurface !== 'build' || @@ -6390,11 +6570,12 @@ const StudioPage: React.FC = () => { pinnedRouteBackendMemberIdRef.current, ); const persistedMemberKey = - studioSurface === 'build' + canonicalLegacyRouteMemberKey || + (studioSurface === 'build' ? trimOptional(persistableBuildMemberKey) || undefined - : pinnedRouteBackendMemberKey || - trimOptional(lifecycleSurfaceMemberKey) || - undefined; + : trimOptional(lifecycleSurfaceMemberKey) || + pinnedRouteBackendMemberKey || + undefined); const persistedLifecycleFocus = studioSurface === 'bind' && routeBuildFocus.kind === 'script' ? (`script:${routeBuildFocus.value}` as const) @@ -6424,6 +6605,7 @@ const StudioPage: React.FC = () => { appliedRouteSnapshot, activeBuildFocusKey, buildSurface, + canonicalLegacyRouteMemberKey, isStudioLocation, lifecycleSurfaceMemberKey, locationSnapshot, @@ -6432,6 +6614,9 @@ const StudioPage: React.FC = () => { resolvedStudioScopeId, routeBuildFocus.kind, routeBuildFocus.value, + routeMemberToken, + routeSelectedBackendMemberId, + routeSelectedMember.kind, routeSelectedMemberKey, routeState.teamId, runPrompt, @@ -6470,17 +6655,26 @@ const StudioPage: React.FC = () => { [publishedScopeMembers, studioScopeMembers, workbenchMemberKey], ); const workbenchStudioMemberId = useMemo( - () => - trimOptional(routeSelectedBackendMemberId) || - trimOptional(workbenchStudioMemberSummary?.memberId) || - readMemberIdFromMemberKey(workbenchMemberKey) || - readMemberIdFromMemberKey(routeState.memberKey) || - trimOptional(routeState.memberId), + () => { + const legacyRouteServiceId = trimOptional(legacyRouteServiceIdRef.current); + if ( + legacyRouteServiceId && + currentRouteMemberToken === legacyRouteServiceId && + !routeSelectedBackendMemberId + ) { + return ''; + } + + return ( + trimOptional(routeSelectedBackendMemberId) || + trimOptional(workbenchStudioMemberSummary?.memberId) || + trimOptional(routeState.memberId) + ); + }, [ + currentRouteMemberToken, routeSelectedBackendMemberId, routeState.memberId, - routeState.memberKey, - workbenchMemberKey, workbenchStudioMemberSummary?.memberId, ], ); @@ -6942,6 +7136,11 @@ const StudioPage: React.FC = () => { const effectiveSelectedMemberKey = trimOptional( selectedRailMemberKey || currentFocusMemberKey, ); + const currentCanonicalMemberId = + trimOptional(workbenchStudioMemberId) || + trimOptional(workbenchStudioMember?.memberId) || + trimOptional(workbenchStudioMemberSummary?.memberId) || + trimOptional(routeState.memberId); const hasSelectedMemberFocus = Boolean(workbenchMemberKey); const currentSelectedMemberServiceId = workbenchPublishedServiceId; @@ -7229,7 +7428,8 @@ const StudioPage: React.FC = () => { currentInvokeSelectionServiceId || currentBindingSelectionServiceId || currentSelectedMemberServiceId || - trimOptional(routeState.memberId); + trimOptional(workbenchPublishedService?.serviceId) || + trimOptional(routeState.legacyServiceId); const invokeTargetService = useMemo( () => { if (!invokeTargetServiceId) { @@ -8374,8 +8574,10 @@ const StudioPage: React.FC = () => { : '成员工作台'; const studioBoundServiceLabel = hasSelectedMemberFocus - ? trimOptional(routeState.memberId) || - trimOptional(workbenchPublishedService?.serviceId) || + ? trimOptional(workbenchPublishedService?.serviceId) || + trimOptional(workbenchStudioMember?.publishedServiceId) || + trimOptional(workbenchStudioMemberSummary?.publishedServiceId) || + trimOptional(routeState.legacyServiceId) || 'No bound service' : ''; const studioContextMetaParts = [ @@ -8390,20 +8592,17 @@ const StudioPage: React.FC = () => { scopeId: resolvedStudioScopeId, teamId: routeState.teamId, tab: 'overview', - memberId: - trimOptional(routeState.memberId) || - readMemberIdFromMemberKey(routeState.memberKey) || - undefined, + memberId: currentCanonicalMemberId || undefined, serviceId: trimOptional(workbenchPublishedService?.serviceId) || undefined, }) : buildTeamDetailHref({ - scopeId: resolvedStudioScopeId, - tab: 'overview', - serviceId: - trimOptional(routeState.memberId) || - trimOptional(workbenchPublishedService?.serviceId) || - undefined, - }) + scopeId: resolvedStudioScopeId, + tab: 'overview', + serviceId: + trimOptional(workbenchPublishedService?.serviceId) || + trimOptional(routeState.legacyServiceId) || + undefined, + }) : buildTeamsHref(); const studioReturnLabel = '返回团队'; const currentStudioReturnTo =