feat(execution): support site session env default (#2221)

Co-authored-by: jackwener <jakevingoo@gmail.com>
This commit is contained in:
Jared Su
2026-08-24 20:17:00 +08:00
committed by GitHub
parent 9adf995c34
commit 14b9b9c595
4 changed files with 92 additions and 28 deletions
+1
View File
@@ -171,6 +171,7 @@ When the site you need is not yet covered, use the `opencli-adapter-author` skil
|----------|---------|-------------|
| `OPENCLI_PROFILE` | — | Browser Bridge profile alias/contextId to use when multiple Chrome profiles are connected |
| `OPENCLI_WINDOW` | command default | Set to `foreground` or `background` to override Browser Bridge window placement. Browser-backed commands also accept `--window <foreground\|background>`. |
| `OPENCLI_SITE_SESSION` | adapter default | Set to `ephemeral` or `persistent` to override `siteSession` metadata for browser-backed adapter commands. `ephemeral` closes the one-shot automation window when the command finishes; `persistent` reuses the site's session. Per-command `--site-session` takes precedence. |
| `OPENCLI_BROWSER_CONNECT_TIMEOUT` | `45` | Seconds to wait for browser connection |
| `OPENCLI_BROWSER_COMMAND_TIMEOUT` | `60` | Seconds to wait for a single browser command |
| `OPENCLI_CDP_ENDPOINT` | — | Chrome DevTools Protocol endpoint for remote browser or Electron apps |
+1
View File
@@ -156,6 +156,7 @@ Agent 在内部自动处理所有 `opencli browser` 命令——你只需用自
| 变量 | 默认值 | 说明 |
|------|--------|------|
| `OPENCLI_WINDOW` | 命令默认值 | 设为 `foreground``background` 来覆盖 Browser Bridge 窗口位置。浏览器型命令也支持 `--window <foreground\|background>` |
| `OPENCLI_SITE_SESSION` | adapter 默认值 | 设为 `ephemeral``persistent`,覆盖浏览器型 adapter 命令的 `siteSession` 元数据。`ephemeral` 会在命令结束时关闭一次性自动化窗口;`persistent` 会复用该站点的 session。命令级 `--site-session` 优先。 |
| `OPENCLI_BROWSER_CONNECT_TIMEOUT` | `45` | 浏览器连接超时(秒) |
| `OPENCLI_BROWSER_COMMAND_TIMEOUT` | `60` | 单个浏览器命令超时(秒) |
| `OPENCLI_CDP_ENDPOINT` | — | Chrome DevTools Protocol 端点,用于远程浏览器或 Electron 应用 |
+75 -22
View File
@@ -1,4 +1,4 @@
import { describe, expect, it, vi } from 'vitest';
import { afterEach, describe, expect, it, vi } from 'vitest';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
@@ -11,6 +11,13 @@ import * as runtime from './runtime.js';
import * as capRouting from './capabilityRouting.js';
import * as daemonClient from './browser/daemon-client.js';
import { BrowserCommandError } from './browser/daemon-client.js';
import { clearAllHooks, onBeforeExecute } from './hooks.js';
afterEach(() => {
clearAllHooks();
vi.unstubAllEnvs();
vi.restoreAllMocks();
});
describe('coerceAndValidateArgs', () => {
it('rejects fractional values for integer arguments', () => {
@@ -251,10 +258,16 @@ describe('executeCommand — non-browser timeout', () => {
vi.restoreAllMocks();
});
it('lets user --site-session ephemeral override adapter persistent metadata', async () => {
it.each([
{ label: 'lets ephemeral env override persistent metadata', env: 'ephemeral', metadata: 'persistent', explicit: undefined, expected: 'ephemeral' },
{ label: 'lets persistent env override ephemeral metadata', env: 'persistent', metadata: 'ephemeral', explicit: undefined, expected: 'persistent' },
{ label: 'lets ephemeral flag override persistent env', env: 'persistent', metadata: 'persistent', explicit: 'ephemeral', expected: 'ephemeral' },
{ label: 'lets persistent flag override ephemeral env', env: 'ephemeral', metadata: 'ephemeral', explicit: 'persistent', expected: 'persistent' },
{ label: 'does not parse a shadowed invalid env', env: '', metadata: 'ephemeral', explicit: 'persistent', expected: 'persistent' },
] as const)('$label', async ({ env, metadata, explicit, expected }) => {
const closeWindow = vi.fn().mockResolvedValue(undefined);
const mockPage = { closeWindow } as any;
const sessionOpts: Array<{ session?: string; idleTimeout?: number }> = [];
const sessionOpts: Array<{ session?: string; siteSession?: string }> = [];
vi.spyOn(capRouting, 'shouldUseBrowserSession').mockReturnValue(true);
vi.spyOn(runtime, 'browserSession').mockImplementation(async (_Factory, fn, opts) => {
@@ -262,28 +275,68 @@ describe('executeCommand — non-browser timeout', () => {
return fn(mockPage);
});
try {
const cmd = cli({
site: 'test-execution',
name: 'site-session-override-ephemeral', access: 'read',
description: 'test user site-session override',
browser: true,
strategy: Strategy.PUBLIC,
siteSession: 'persistent',
func: async () => [{ ok: true }],
});
vi.stubEnv('OPENCLI_SITE_SESSION', env);
const cmd = cli({
site: 'test-execution',
name: 'site-session-precedence', access: 'read',
description: 'test site-session precedence',
browser: true,
strategy: Strategy.PUBLIC,
siteSession: metadata,
func: async () => [{ ok: true }],
});
await executeCommand(cmd, {}, false, { siteSession: 'ephemeral' });
await executeCommand(cmd, {}, false, explicit === undefined ? {} : { siteSession: explicit });
expect(sessionOpts).toHaveLength(1);
expect(sessionOpts).toHaveLength(1);
expect(sessionOpts[0]?.siteSession).toBe(expected);
if (expected === 'persistent') {
expect(sessionOpts[0]?.session).toBe('site:test-execution');
expect(closeWindow).not.toHaveBeenCalled();
} else {
expect(sessionOpts[0]?.session).toMatch(/^site:test-execution:/);
expect(sessionOpts[0]?.idleTimeout).toBeUndefined();
expect(closeWindow).toHaveBeenCalledTimes(1);
} finally {
vi.restoreAllMocks();
}
});
it.each(['', ' ', 'Persistent'])('rejects invalid OPENCLI_SITE_SESSION=%j before hooks or browser setup', async (env) => {
const beforeHook = vi.fn();
const adapter = vi.fn(async () => [{ ok: true }]);
onBeforeExecute(beforeHook);
vi.spyOn(capRouting, 'shouldUseBrowserSession').mockReturnValue(true);
const browserSessionSpy = vi.spyOn(runtime, 'browserSession');
vi.stubEnv('OPENCLI_SITE_SESSION', env);
const cmd = cli({
site: 'test-execution',
name: 'site-session-invalid-env', access: 'read',
description: 'test invalid site-session env fails before side effects',
browser: true,
strategy: Strategy.PUBLIC,
navigateBefore: 'https://example.com/',
func: adapter,
});
await expect(executeCommand(cmd, {})).rejects.toMatchObject({ code: 'ARGUMENT' });
expect(beforeHook).not.toHaveBeenCalled();
expect(browserSessionSpy).not.toHaveBeenCalled();
expect(adapter).not.toHaveBeenCalled();
});
it('does not apply OPENCLI_SITE_SESSION to non-browser commands', async () => {
vi.stubEnv('OPENCLI_SITE_SESSION', 'invalid');
const cmd = cli({
site: 'test-execution',
name: 'site-session-node-only', access: 'read',
description: 'test browser env does not poison node-only commands',
browser: false,
strategy: Strategy.PUBLIC,
func: async () => [{ ok: true }],
});
await expect(executeCommand(cmd, {})).resolves.toEqual([{ ok: true }]);
});
it('skips repeated domain pre-navigation for persistent site sessions', async () => {
const closeWindow = vi.fn().mockResolvedValue(undefined);
const goto = vi.fn().mockResolvedValue(undefined);
@@ -431,7 +484,7 @@ describe('executeCommand — non-browser timeout', () => {
vi.restoreAllMocks();
});
it('calls closeWindow on browser command failure', async () => {
it('lets env ephemeral override persistent metadata and closes the window on failure', async () => {
const closeWindow = vi.fn().mockResolvedValue(undefined);
const mockPage = { closeWindow } as any;
@@ -446,16 +499,16 @@ describe('executeCommand — non-browser timeout', () => {
const cmd = cli({
site: 'test-execution',
name: 'browser-close-on-error', access: 'read',
description: 'test closeWindow on failure',
description: 'test env ephemeral closeWindow on failure',
browser: true,
strategy: Strategy.PUBLIC,
siteSession: 'persistent',
func: async () => { throw new Error('adapter failure'); },
});
vi.stubEnv('OPENCLI_SITE_SESSION', 'ephemeral');
await expect(executeCommand(cmd, {})).rejects.toThrow('adapter failure');
expect(closeWindow).toHaveBeenCalledTimes(1);
vi.restoreAllMocks();
});
it('skips closeWindow when --keep-tab=true (success path)', async () => {
+15 -6
View File
@@ -211,6 +211,13 @@ export async function executeCommand(
onTraceExport?: (trace: ObservationExportResult) => void;
} = {},
): Promise<unknown> {
// Resolve browser-only configuration before argument hooks or any browser
// lifecycle setup. Non-browser commands must not be affected by browser
// environment defaults, even when those defaults are invalid.
const siteSession = shouldUseBrowserSession(cmd)
? resolveSiteSession(cmd, opts.siteSession)
: null;
let kwargs: CommandArgs;
try {
kwargs = opts.prepared ? rawKwargs : prepareCommandArgs(cmd, rawKwargs);
@@ -236,7 +243,7 @@ export async function executeCommand(
let result: unknown;
try {
if (shouldUseBrowserSession(cmd)) {
if (siteSession !== null) {
const electron = isElectronApp(cmd.site);
let cdpEndpoint: string | undefined;
@@ -264,7 +271,6 @@ export async function executeCommand(
const profileRouting = profileRouteParams(profileSelection);
const contextId = profileSelection?.contextId;
const internal = cmd as InternalCliCommand;
const siteSession = resolveSiteSession(cmd, opts.siteSession);
const session = resolveAdapterBrowserSession(cmd, siteSession);
const keepTab = resolveKeepTab(siteSession, opts.keepTab);
const windowMode = resolveBrowserWindowMode(cmd.defaultWindowMode ?? 'background', opts.windowMode);
@@ -576,14 +582,17 @@ export function prepareCommandArgs(
*/
const RUNTIME_TIMEOUT_PADDING_SECONDS = 30;
function normalizeSiteSession(raw: unknown): SiteSessionMode | null {
if (raw === undefined || raw === null || raw === '') return null;
function normalizeSiteSession(name: string, raw: unknown): SiteSessionMode | null {
if (raw === undefined) return null;
if (raw === 'ephemeral' || raw === 'persistent') return raw;
throw new ArgumentError(`--site-session must be one of: ephemeral, persistent. Received: "${String(raw)}"`);
throw new ArgumentError(`${name} must be one of: ephemeral, persistent. Received: "${String(raw)}"`);
}
function resolveSiteSession(cmd: CliCommand, rawOption?: unknown): SiteSessionMode {
return normalizeSiteSession(rawOption) ?? cmd.siteSession ?? 'ephemeral';
return normalizeSiteSession('--site-session', rawOption)
?? normalizeSiteSession('OPENCLI_SITE_SESSION', process.env.OPENCLI_SITE_SESSION)
?? cmd.siteSession
?? 'ephemeral';
}
function resolveAdapterBrowserSession(cmd: CliCommand, siteSession: SiteSessionMode): string {