diff --git a/server/src/browser/browser.service.ts b/server/src/browser/browser.service.ts index 5485dcd..ddc5767 100644 --- a/server/src/browser/browser.service.ts +++ b/server/src/browser/browser.service.ts @@ -9,8 +9,10 @@ import type { BrowserContext } from "playwright"; import { Readability } from "@mozilla/readability"; import { JSDOM } from "jsdom"; import { SessionContextService } from "../session/session-context.service"; +import { SessionService } from "../session/session.service"; import { CodeExecutorService } 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"; @@ -26,9 +28,54 @@ export class BrowserService { constructor( private readonly sessionContextService: SessionContextService, + private readonly sessionService: SessionService, 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 { if (err instanceof HttpException) { throw err; @@ -90,8 +137,7 @@ export class BrowserService { this.logger.log(`[${label}] open: ${url}`); if (sessionName) { - const { context } = - await this.sessionContextService.getHandle(sessionName); + const { context } = await this.getOrCreateNamedHandle(sessionName); try { return await this.extractContent(context, url, readerMode, selector); } catch (err) { @@ -123,7 +169,7 @@ export class BrowserService { if (sessionName) { const { page, context } = - await this.sessionContextService.getHandle(sessionName); + await this.getOrCreateNamedHandle(sessionName); this.logger.log(`[${label}] exec: using persistent context`); try { if (url) { diff --git a/server/src/browser/dto/exec.dto.ts b/server/src/browser/dto/exec.dto.ts index 2109bbc..a1968e8 100644 --- a/server/src/browser/dto/exec.dto.ts +++ b/server/src/browser/dto/exec.dto.ts @@ -4,7 +4,7 @@ import { IsOptional, IsString, IsUrl } from "class-validator"; export class ExecDto { @ApiPropertyOptional({ 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", }) @IsOptional() diff --git a/server/src/browser/dto/open.dto.ts b/server/src/browser/dto/open.dto.ts index e571b06..87afeb1 100644 --- a/server/src/browser/dto/open.dto.ts +++ b/server/src/browser/dto/open.dto.ts @@ -4,7 +4,7 @@ import { IsBoolean, IsOptional, IsString, IsUrl } from "class-validator"; export class OpenDto { @ApiPropertyOptional({ 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", }) @IsOptional() diff --git a/server/src/mcp/mcp.service.ts b/server/src/mcp/mcp.service.ts index 6980a95..16d71c8 100644 --- a/server/src/mcp/mcp.service.ts +++ b/server/src/mcp/mcp.service.ts @@ -502,7 +502,7 @@ export class McpService { .min(0) .describe("Execution order (ascending)"), 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 .string() .optional() @@ -569,7 +569,7 @@ export class McpService { .enum(["login", "exec", "sign"]) .optional() .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"), validateCode: z.string().optional().describe("New validation code"), }, diff --git a/server/src/scenario/dto/add-scenario-credential.dto.ts b/server/src/scenario/dto/add-scenario-credential.dto.ts index 8ce1b9f..b7cdb96 100644 --- a/server/src/scenario/dto/add-scenario-credential.dto.ts +++ b/server/src/scenario/dto/add-scenario-credential.dto.ts @@ -5,6 +5,7 @@ export class AddScenarioCredentialDto { @ApiProperty({ example: "uuid-here" }) @IsUUID() credentialId: string; + @ApiProperty({ example: "api_key" }) @IsString() @IsNotEmpty() diff --git a/server/src/scenario/dto/create-scenario-step.dto.ts b/server/src/scenario/dto/create-scenario-step.dto.ts index f95f394..bf77853 100644 --- a/server/src/scenario/dto/create-scenario-step.dto.ts +++ b/server/src/scenario/dto/create-scenario-step.dto.ts @@ -7,16 +7,6 @@ export class CreateScenarioStepDto { @IsString() 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();" }) @IsOptional() @IsString() diff --git a/server/src/scenario/dto/update-scenario-step.dto.ts b/server/src/scenario/dto/update-scenario-step.dto.ts index cddb843..11af1c2 100644 --- a/server/src/scenario/dto/update-scenario-step.dto.ts +++ b/server/src/scenario/dto/update-scenario-step.dto.ts @@ -13,12 +13,6 @@ export class UpdateScenarioStepDto { @Min(0) order?: number; - @ApiPropertyOptional() - @IsOptional() - @IsString() - @IsNotEmpty() - sessionName?: string; - @ApiPropertyOptional() @IsOptional() @IsString() diff --git a/server/src/scenario/scenario-step.entity.ts b/server/src/scenario/scenario-step.entity.ts index cd9de41..76112f9 100644 --- a/server/src/scenario/scenario-step.entity.ts +++ b/server/src/scenario/scenario-step.entity.ts @@ -29,9 +29,6 @@ export class ScenarioStepEntity { @Column({ type: "text", nullable: true }) title: string | null; - @Column({ type: "text", nullable: true }) - sessionName: string | null; - @Column({ type: "text", nullable: true }) execCode: string | null; diff --git a/server/src/scenario/scenario.service.ts b/server/src/scenario/scenario.service.ts index 0fe4945..f9d8931 100644 --- a/server/src/scenario/scenario.service.ts +++ b/server/src/scenario/scenario.service.ts @@ -120,7 +120,6 @@ export class ScenarioService { order: currentCount, scenarioId, title: dto.title ?? null, - sessionName: dto.sessionName ?? null, execCode: dto.execCode ?? null, validateCode: dto.validateCode ?? null, }), @@ -153,7 +152,6 @@ export class ScenarioService { ...dto, order: step.order, title: dto.title ?? step.title, - sessionName: dto.sessionName ?? step.sessionName, execCode: dto.execCode ?? step.execCode, validateCode: dto.validateCode ?? step.validateCode, });