refactor(session): auto-create named browser sessions

- create and register named browser sessions on open/exec when missing

- align scenario step DTO/entity/service by removing sessionName usage

- update MCP scenario-step schemas to use optional title fields
This commit is contained in:
2026-04-10 21:06:00 +03:00
parent 1627733701
commit b45f95d8cf
9 changed files with 54 additions and 28 deletions
+49 -3
View File
@@ -9,8 +9,10 @@ import type { BrowserContext } from "playwright";
import { Readability } from "@mozilla/readability"; import { Readability } from "@mozilla/readability";
import { JSDOM } from "jsdom"; import { JSDOM } from "jsdom";
import { SessionContextService } from "../session/session-context.service"; import { SessionContextService } from "../session/session-context.service";
import { SessionService } from "../session/session.service";
import { CodeExecutorService } from "../code-executor/code-executor.service"; import { CodeExecutorService } from "../code-executor/code-executor.service";
import type { ExecResult } from "../code-executor/code-executor.service"; import type { ExecResult } from "../code-executor/code-executor.service";
import type { Cookie } from "playwright";
export type { ExecResult } from "../code-executor/code-executor.service"; export type { ExecResult } from "../code-executor/code-executor.service";
@@ -26,9 +28,54 @@ export class BrowserService {
constructor( constructor(
private readonly sessionContextService: SessionContextService, private readonly sessionContextService: SessionContextService,
private readonly sessionService: SessionService,
private readonly codeExecutor: CodeExecutorService, private readonly codeExecutor: CodeExecutorService,
) {} ) {}
private parseCookies(raw: string | null | undefined): Cookie[] {
if (!raw) return [];
try {
const parsed = JSON.parse(raw) as unknown;
return Array.isArray(parsed) ? (parsed as Cookie[]) : [];
} catch {
return [];
}
}
private async getOrCreateNamedHandle(sessionName: string) {
try {
return await this.sessionContextService.getHandle(sessionName);
} catch {
this.logger.log(
`[${sessionName}] creating session context automatically`,
);
const browser = await chromium.launch({
headless: true,
executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH,
});
const context = await browser.newContext();
const page = await context.newPage();
const existing = await this.sessionService.findBySessionName(sessionName);
const cookies = this.parseCookies(existing?.cookies);
if (cookies.length > 0) {
await context.addCookies(cookies);
}
this.sessionContextService.register(sessionName, browser, context, page);
await this.sessionService.upsert(
sessionName,
existing?.token ?? `auto:${sessionName}`,
await context.cookies(),
{},
);
return { browser, context, page };
}
}
private rethrow(err: unknown, label: string, operation: string): never { private rethrow(err: unknown, label: string, operation: string): never {
if (err instanceof HttpException) { if (err instanceof HttpException) {
throw err; throw err;
@@ -90,8 +137,7 @@ export class BrowserService {
this.logger.log(`[${label}] open: ${url}`); this.logger.log(`[${label}] open: ${url}`);
if (sessionName) { if (sessionName) {
const { context } = const { context } = await this.getOrCreateNamedHandle(sessionName);
await this.sessionContextService.getHandle(sessionName);
try { try {
return await this.extractContent(context, url, readerMode, selector); return await this.extractContent(context, url, readerMode, selector);
} catch (err) { } catch (err) {
@@ -123,7 +169,7 @@ export class BrowserService {
if (sessionName) { if (sessionName) {
const { page, context } = const { page, context } =
await this.sessionContextService.getHandle(sessionName); await this.getOrCreateNamedHandle(sessionName);
this.logger.log(`[${label}] exec: using persistent context`); this.logger.log(`[${label}] exec: using persistent context`);
try { try {
if (url) { if (url) {
+1 -1
View File
@@ -4,7 +4,7 @@ import { IsOptional, IsString, IsUrl } from "class-validator";
export class ExecDto { export class ExecDto {
@ApiPropertyOptional({ @ApiPropertyOptional({
description: description:
"Session name previously created by POST /login. If omitted, executes without a stored session.", "Optional named browser session. If it does not exist yet, it is created automatically.",
example: "test-session-1", example: "test-session-1",
}) })
@IsOptional() @IsOptional()
+1 -1
View File
@@ -4,7 +4,7 @@ import { IsBoolean, IsOptional, IsString, IsUrl } from "class-validator";
export class OpenDto { export class OpenDto {
@ApiPropertyOptional({ @ApiPropertyOptional({
description: description:
"Session name previously created by POST /login. If omitted, opens the URL without a stored session.", "Optional named browser session. If it does not exist yet, it is created automatically.",
example: "test-session-1", example: "test-session-1",
}) })
@IsOptional() @IsOptional()
+2 -2
View File
@@ -502,7 +502,7 @@ export class McpService {
.min(0) .min(0)
.describe("Execution order (ascending)"), .describe("Execution order (ascending)"),
type: z.enum(["login", "exec", "sign"]).describe("Step type"), type: z.enum(["login", "exec", "sign"]).describe("Step type"),
sessionName: z.string().describe("Session name used by this step"), title: z.string().optional().describe("Optional step title"),
execCode: z execCode: z
.string() .string()
.optional() .optional()
@@ -569,7 +569,7 @@ export class McpService {
.enum(["login", "exec", "sign"]) .enum(["login", "exec", "sign"])
.optional() .optional()
.describe("New step type"), .describe("New step type"),
sessionName: z.string().optional().describe("New session name"), title: z.string().optional().describe("New step title"),
execCode: z.string().optional().describe("New exec code"), execCode: z.string().optional().describe("New exec code"),
validateCode: z.string().optional().describe("New validation code"), validateCode: z.string().optional().describe("New validation code"),
}, },
@@ -5,6 +5,7 @@ export class AddScenarioCredentialDto {
@ApiProperty({ example: "uuid-here" }) @ApiProperty({ example: "uuid-here" })
@IsUUID() @IsUUID()
credentialId: string; credentialId: string;
@ApiProperty({ example: "api_key" }) @ApiProperty({ example: "api_key" })
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
@@ -7,16 +7,6 @@ export class CreateScenarioStepDto {
@IsString() @IsString()
title?: string; title?: string;
@ApiPropertyOptional({
description:
"Session name (deprecated — browser is created automatically per run).",
example: "my-session",
})
@IsOptional()
@IsString()
@IsNotEmpty()
sessionName?: string;
@ApiPropertyOptional({ example: "return await page.title();" }) @ApiPropertyOptional({ example: "return await page.title();" })
@IsOptional() @IsOptional()
@IsString() @IsString()
@@ -13,12 +13,6 @@ export class UpdateScenarioStepDto {
@Min(0) @Min(0)
order?: number; order?: number;
@ApiPropertyOptional()
@IsOptional()
@IsString()
@IsNotEmpty()
sessionName?: string;
@ApiPropertyOptional() @ApiPropertyOptional()
@IsOptional() @IsOptional()
@IsString() @IsString()
@@ -29,9 +29,6 @@ export class ScenarioStepEntity {
@Column({ type: "text", nullable: true }) @Column({ type: "text", nullable: true })
title: string | null; title: string | null;
@Column({ type: "text", nullable: true })
sessionName: string | null;
@Column({ type: "text", nullable: true }) @Column({ type: "text", nullable: true })
execCode: string | null; execCode: string | null;
-2
View File
@@ -120,7 +120,6 @@ export class ScenarioService {
order: currentCount, order: currentCount,
scenarioId, scenarioId,
title: dto.title ?? null, title: dto.title ?? null,
sessionName: dto.sessionName ?? null,
execCode: dto.execCode ?? null, execCode: dto.execCode ?? null,
validateCode: dto.validateCode ?? null, validateCode: dto.validateCode ?? null,
}), }),
@@ -153,7 +152,6 @@ export class ScenarioService {
...dto, ...dto,
order: step.order, order: step.order,
title: dto.title ?? step.title, title: dto.title ?? step.title,
sessionName: dto.sessionName ?? step.sessionName,
execCode: dto.execCode ?? step.execCode, execCode: dto.execCode ?? step.execCode,
validateCode: dto.validateCode ?? step.validateCode, validateCode: dto.validateCode ?? step.validateCode,
}); });