



























@@ -168,6 +168,10 @@ function createQaLabConfig(baseUrl: string): OpenClawConfig {
168168return createQaChannelGatewayConfig({ baseUrl });
169169}
170170171+function normalizeQaLabCleanupError(error: unknown): Error {
172+return error instanceof Error ? error : new Error(formatErrorMessage(error));
173+}
174+171175async function startQaGatewayLoop(params: { state: QaBusState; baseUrl: string }) {
172176const runtime = createQaRunnerRuntime();
173177setQaChannelRuntime(runtime);
@@ -242,7 +246,10 @@ export async function startQaLabServer(
242246| undefined;
243247const embeddedGatewayEnabled = params?.embeddedGateway !== "disabled";
244248let labHandle: QaLabServerHandle | null = null;
249+let captureStoreReleased = false;
250+let serverListening = false;
245251252+let listenUrl = "";
246253let publicBaseUrl = "";
247254let runnerModelCatalogPromise: Promise<void> | null = null;
248255let runnerModelCatalogAbort: AbortController | null = null;
@@ -628,82 +635,107 @@ export async function startQaLabServer(
628635})();
629636});
630637631-await new Promise<void>((resolve, reject) => {
632-server.once("error", reject);
633-server.listen(params?.port ?? 0, params?.host ?? "127.0.0.1", () => resolve());
634-});
635-const address = server.address();
636-if (!address || typeof address === "string") {
637-throw new Error("qa-lab failed to bind");
638-}
639-const listenUrl = resolveAdvertisedBaseUrl({
640-bindHost: params?.host ?? "127.0.0.1",
641-bindPort: address.port,
642-});
643-publicBaseUrl = resolveAdvertisedBaseUrl({
644-bindHost: params?.host ?? "127.0.0.1",
645-bindPort: address.port,
646-advertiseHost: params?.advertiseHost,
647-advertisePort: params?.advertisePort,
648-});
649-if (embeddedGatewayEnabled) {
650-gateway = await startQaGatewayLoop({ state, baseUrl: listenUrl });
651-}
652-if (params?.sendKickoffOnStart) {
653-injectKickoffMessage({
654- state,
655-defaults: bootstrapDefaults,
656-kickoffTask: scenarioCatalog.kickoffTask,
657-});
658-}
659-660-server.on("upgrade", (req, socket, head) => {
661-const url = new URL(req.url ?? "/", "http://127.0.0.1");
662-if (!controlUiProxyTarget || !isControlUiProxyPath(url.pathname)) {
663-socket.destroy();
638+const releaseCaptureStore = () => {
639+if (captureStoreReleased) {
664640return;
665641}
666-proxyUpgradeRequest({
667- req,
668- socket,
669- head,
670-target: controlUiProxyTarget,
671-authorizationToken: controlUiProxyToken,
672-});
673-});
642+captureStoreReleased = true;
643+captureStoreLease.release();
644+};
674645675-const lab = {
676-baseUrl: publicBaseUrl,
677- listenUrl,
678- state,
679-setControlUi(next: {
680-controlUiUrl?: string | null;
681-controlUiProxyToken?: string | null;
682-controlUiProxyTarget?: string | null;
683-}) {
684-controlUiUrl = sanitizeControlUiPublicUrl(next.controlUiUrl?.trim() || null);
685-controlUiProxyToken = next.controlUiProxyToken?.trim() || null;
686-controlUiProxyTarget = next.controlUiProxyTarget?.trim()
687- ? new URL(next.controlUiProxyTarget)
688- : null;
689-},
690-setScenarioRun(next: Omit<QaLabScenarioRun, "counts"> | null) {
691-latestScenarioRun = next ? withQaLabRunCounts(next) : null;
692-},
693-setLatestReport(next: QaLabLatestReport | null) {
694-latestReport = next;
695-},
696- runSelfCheck,
697-async stop() {
698-runnerModelCatalogAbort?.abort();
699-await runnerModelCatalogPromise?.catch(() => undefined);
700-await gateway?.stop();
701-await closeQaHttpServer(server);
702-captureStoreLease.release();
703-},
646+const stopLabServerResources = async (): Promise<Error | undefined> => {
647+runnerModelCatalogAbort?.abort();
648+await runnerModelCatalogPromise?.catch(() => undefined);
649+const results = await Promise.allSettled([
650+Promise.resolve().then(() => gateway?.stop()),
651+Promise.resolve().then(() => (serverListening ? closeQaHttpServer(server) : undefined)),
652+Promise.resolve().then(releaseCaptureStore),
653+]);
654+const failed = results.find((result) => result.status === "rejected");
655+return failed ? normalizeQaLabCleanupError(failed.reason) : undefined;
704656};
705-labHandle = lab;
706-return lab;
657+658+try {
659+await new Promise<void>((resolve, reject) => {
660+server.once("error", reject);
661+server.listen(params?.port ?? 0, params?.host ?? "127.0.0.1", () => resolve());
662+});
663+serverListening = true;
664+const address = server.address();
665+if (!address || typeof address === "string") {
666+throw new Error("qa-lab failed to bind");
667+}
668+listenUrl = resolveAdvertisedBaseUrl({
669+bindHost: params?.host ?? "127.0.0.1",
670+bindPort: address.port,
671+});
672+publicBaseUrl = resolveAdvertisedBaseUrl({
673+bindHost: params?.host ?? "127.0.0.1",
674+bindPort: address.port,
675+advertiseHost: params?.advertiseHost,
676+advertisePort: params?.advertisePort,
677+});
678+if (embeddedGatewayEnabled) {
679+gateway = await startQaGatewayLoop({ state, baseUrl: listenUrl });
680+}
681+if (params?.sendKickoffOnStart) {
682+injectKickoffMessage({
683+ state,
684+defaults: bootstrapDefaults,
685+kickoffTask: scenarioCatalog.kickoffTask,
686+});
687+}
688+689+server.on("upgrade", (req, socket, head) => {
690+const url = new URL(req.url ?? "/", "http://127.0.0.1");
691+if (!controlUiProxyTarget || !isControlUiProxyPath(url.pathname)) {
692+socket.destroy();
693+return;
694+}
695+proxyUpgradeRequest({
696+ req,
697+ socket,
698+ head,
699+target: controlUiProxyTarget,
700+authorizationToken: controlUiProxyToken,
701+});
702+});
703+704+const lab = {
705+baseUrl: publicBaseUrl,
706+ listenUrl,
707+ state,
708+setControlUi(next: {
709+controlUiUrl?: string | null;
710+controlUiProxyToken?: string | null;
711+controlUiProxyTarget?: string | null;
712+}) {
713+controlUiUrl = sanitizeControlUiPublicUrl(next.controlUiUrl?.trim() || null);
714+controlUiProxyToken = next.controlUiProxyToken?.trim() || null;
715+controlUiProxyTarget = next.controlUiProxyTarget?.trim()
716+ ? new URL(next.controlUiProxyTarget)
717+ : null;
718+},
719+setScenarioRun(next: Omit<QaLabScenarioRun, "counts"> | null) {
720+latestScenarioRun = next ? withQaLabRunCounts(next) : null;
721+},
722+setLatestReport(next: QaLabLatestReport | null) {
723+latestReport = next;
724+},
725+ runSelfCheck,
726+async stop() {
727+const cleanupError = await stopLabServerResources();
728+if (cleanupError) {
729+throw cleanupError;
730+}
731+},
732+};
733+labHandle = lab;
734+return lab;
735+} catch (error) {
736+await stopLabServerResources().catch(() => undefined);
737+throw error;
738+}
707739}
708740709741function serializeSelfCheck(result: QaSelfCheckResult) {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。