Connects to user's running Brave via Chrome DevTools Protocol to automate ChatGPT interaction. Uses puppeteer-core to open a tab, send the prompt, wait for response, and extract the result. No cookies, no separate profiles, no copy/paste. Just connects to the browser where the user is already logged in. One-time setup: relaunch Brave with --remote-debugging-port=9222 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
331 lines
14 KiB
JavaScript
331 lines
14 KiB
JavaScript
"use strict";
|
|
/*
|
|
* Copyright 2024 Google LLC.
|
|
* Copyright (c) Microsoft Corporation.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*
|
|
*/
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.NavigationTracker = exports.NavigationState = exports.NavigationResult = void 0;
|
|
const protocol_js_1 = require("../../../protocol/protocol.js");
|
|
const Deferred_js_1 = require("../../../utils/Deferred.js");
|
|
const log_js_1 = require("../../../utils/log.js");
|
|
const time_js_1 = require("../../../utils/time.js");
|
|
const urlHelpers_js_1 = require("../../../utils/urlHelpers.js");
|
|
const uuid_js_1 = require("../../../utils/uuid.js");
|
|
class NavigationResult {
|
|
eventName;
|
|
message;
|
|
constructor(eventName, message) {
|
|
this.eventName = eventName;
|
|
this.message = message;
|
|
}
|
|
}
|
|
exports.NavigationResult = NavigationResult;
|
|
class NavigationState {
|
|
navigationId = (0, uuid_js_1.uuidv4)();
|
|
#browsingContextId;
|
|
#started = false;
|
|
#finished = new Deferred_js_1.Deferred();
|
|
url;
|
|
loaderId;
|
|
#isInitial;
|
|
#eventManager;
|
|
committed = new Deferred_js_1.Deferred();
|
|
isFragmentNavigation;
|
|
get finished() {
|
|
return this.#finished;
|
|
}
|
|
constructor(url, browsingContextId, isInitial, eventManager) {
|
|
this.#browsingContextId = browsingContextId;
|
|
this.url = url;
|
|
this.#isInitial = isInitial;
|
|
this.#eventManager = eventManager;
|
|
}
|
|
navigationInfo() {
|
|
return {
|
|
context: this.#browsingContextId,
|
|
navigation: this.navigationId,
|
|
timestamp: (0, time_js_1.getTimestamp)(),
|
|
url: this.url,
|
|
};
|
|
}
|
|
start() {
|
|
if (
|
|
// Initial navigation should not be reported.
|
|
!this.#isInitial &&
|
|
// No need in reporting started navigation twice.
|
|
!this.#started &&
|
|
// No need for reporting fragment navigations. Step 13 vs step 16 of the spec:
|
|
// https://html.spec.whatwg.org/#beginning-navigation:webdriver-bidi-navigation-started
|
|
!this.isFragmentNavigation) {
|
|
this.#eventManager.registerEvent({
|
|
type: 'event',
|
|
method: protocol_js_1.ChromiumBidi.BrowsingContext.EventNames.NavigationStarted,
|
|
params: this.navigationInfo(),
|
|
}, this.#browsingContextId);
|
|
}
|
|
this.#started = true;
|
|
}
|
|
#finish(navigationResult) {
|
|
this.#started = true;
|
|
if (!this.#isInitial &&
|
|
!this.#finished.isFinished &&
|
|
navigationResult.eventName !== "browsingContext.load" /* NavigationEventName.Load */) {
|
|
this.#eventManager.registerEvent({
|
|
type: 'event',
|
|
method: navigationResult.eventName,
|
|
params: this.navigationInfo(),
|
|
}, this.#browsingContextId);
|
|
}
|
|
this.#finished.resolve(navigationResult);
|
|
}
|
|
frameNavigated() {
|
|
this.committed.resolve();
|
|
if (!this.#isInitial) {
|
|
this.#eventManager.registerEvent({
|
|
type: 'event',
|
|
method: protocol_js_1.ChromiumBidi.BrowsingContext.EventNames.NavigationCommitted,
|
|
params: this.navigationInfo(),
|
|
}, this.#browsingContextId);
|
|
}
|
|
}
|
|
fragmentNavigated() {
|
|
this.committed.resolve();
|
|
this.#finish(new NavigationResult("browsingContext.fragmentNavigated" /* NavigationEventName.FragmentNavigated */));
|
|
}
|
|
load() {
|
|
this.#finish(new NavigationResult("browsingContext.load" /* NavigationEventName.Load */));
|
|
}
|
|
fail(message) {
|
|
this.#finish(new NavigationResult(this.committed.isFinished
|
|
? "browsingContext.navigationAborted" /* NavigationEventName.NavigationAborted */
|
|
: "browsingContext.navigationFailed" /* NavigationEventName.NavigationFailed */, message));
|
|
}
|
|
}
|
|
exports.NavigationState = NavigationState;
|
|
/**
|
|
* Keeps track of navigations. Details: http://go/webdriver:bidi-navigation
|
|
*/
|
|
class NavigationTracker {
|
|
#eventManager;
|
|
#logger;
|
|
#loaderIdToNavigationsMap = new Map();
|
|
#browsingContextId;
|
|
/**
|
|
* Last committed navigation is committed, but is not guaranteed to be finished, as it
|
|
* can still wait for `load` or `DOMContentLoaded` events.
|
|
*/
|
|
#lastCommittedNavigation;
|
|
/**
|
|
* Pending navigation is a navigation that is started but not yet committed.
|
|
*/
|
|
#pendingNavigation;
|
|
// Flags if the initial navigation to `about:blank` is in progress.
|
|
#isInitialNavigation = true;
|
|
constructor(url, browsingContextId, eventManager, logger) {
|
|
this.#browsingContextId = browsingContextId;
|
|
this.#eventManager = eventManager;
|
|
this.#logger = logger;
|
|
this.#isInitialNavigation = true;
|
|
// The initial navigation is always committed.
|
|
this.#lastCommittedNavigation = new NavigationState(url, browsingContextId, (0, urlHelpers_js_1.urlMatchesAboutBlank)(url), this.#eventManager);
|
|
}
|
|
/**
|
|
* Returns current started ongoing navigation. It can be either a started pending
|
|
* navigation, or one is already navigated.
|
|
*/
|
|
get currentNavigationId() {
|
|
if (this.#pendingNavigation?.isFragmentNavigation === false) {
|
|
// Use pending navigation if it is started and it is not a fragment navigation.
|
|
return this.#pendingNavigation.navigationId;
|
|
}
|
|
// If the pending navigation is a fragment one, or if it is not exists, the last
|
|
// committed navigation should be used.
|
|
return this.#lastCommittedNavigation.navigationId;
|
|
}
|
|
/**
|
|
* Flags if the current navigation relates to the initial to `about:blank` navigation.
|
|
*/
|
|
get isInitialNavigation() {
|
|
return this.#isInitialNavigation;
|
|
}
|
|
/**
|
|
* Url of the last navigated navigation.
|
|
*/
|
|
get url() {
|
|
return this.#lastCommittedNavigation.url;
|
|
}
|
|
/**
|
|
* Creates a pending navigation e.g. when navigation command is called. Required to
|
|
* provide navigation id before the actual navigation is started. It will be used when
|
|
* navigation started. Can be aborted, failed, fragment navigated, or became a current
|
|
* navigation.
|
|
*/
|
|
createPendingNavigation(url, canBeInitialNavigation = false) {
|
|
this.#logger?.(log_js_1.LogType.debug, 'createCommandNavigation');
|
|
this.#isInitialNavigation =
|
|
canBeInitialNavigation &&
|
|
this.#isInitialNavigation &&
|
|
(0, urlHelpers_js_1.urlMatchesAboutBlank)(url);
|
|
this.#pendingNavigation?.fail('navigation canceled by concurrent navigation');
|
|
const navigation = new NavigationState(url, this.#browsingContextId, this.#isInitialNavigation, this.#eventManager);
|
|
this.#pendingNavigation = navigation;
|
|
return navigation;
|
|
}
|
|
dispose() {
|
|
this.#pendingNavigation?.fail('navigation canceled by context disposal');
|
|
this.#lastCommittedNavigation.fail('navigation canceled by context disposal');
|
|
}
|
|
// Update the current url.
|
|
onTargetInfoChanged(url) {
|
|
this.#logger?.(log_js_1.LogType.debug, `onTargetInfoChanged ${url}`);
|
|
this.#lastCommittedNavigation.url = url;
|
|
}
|
|
#getNavigationForFrameNavigated(url, loaderId) {
|
|
if (this.#loaderIdToNavigationsMap.has(loaderId)) {
|
|
return this.#loaderIdToNavigationsMap.get(loaderId);
|
|
}
|
|
if (this.#pendingNavigation !== undefined &&
|
|
this.#pendingNavigation.loaderId === undefined) {
|
|
// This can be a pending navigation to `about:blank` created by a command. Use the
|
|
// pending navigation in this case.
|
|
return this.#pendingNavigation;
|
|
}
|
|
// Create a new pending navigation.
|
|
return this.createPendingNavigation(url, true);
|
|
}
|
|
/**
|
|
* @param {string} unreachableUrl indicated the navigation is actually failed.
|
|
*/
|
|
frameNavigated(url, loaderId, unreachableUrl) {
|
|
this.#logger?.(log_js_1.LogType.debug, `frameNavigated ${url}`);
|
|
if (unreachableUrl !== undefined) {
|
|
// The navigation failed.
|
|
const navigation = this.#loaderIdToNavigationsMap.get(loaderId) ??
|
|
this.#pendingNavigation ??
|
|
this.createPendingNavigation(unreachableUrl, true);
|
|
navigation.url = unreachableUrl;
|
|
navigation.start();
|
|
navigation.fail('the requested url is unreachable');
|
|
return;
|
|
}
|
|
const navigation = this.#getNavigationForFrameNavigated(url, loaderId);
|
|
if (navigation !== this.#lastCommittedNavigation) {
|
|
// Even though the `lastCommittedNavigation` is navigated, it still can be waiting
|
|
// for `load` or `DOMContentLoaded` events.
|
|
this.#lastCommittedNavigation.fail('navigation canceled by concurrent navigation');
|
|
}
|
|
navigation.url = url;
|
|
navigation.loaderId = loaderId;
|
|
this.#loaderIdToNavigationsMap.set(loaderId, navigation);
|
|
navigation.start();
|
|
navigation.frameNavigated();
|
|
this.#lastCommittedNavigation = navigation;
|
|
if (this.#pendingNavigation === navigation) {
|
|
this.#pendingNavigation = undefined;
|
|
}
|
|
}
|
|
navigatedWithinDocument(url, navigationType) {
|
|
this.#logger?.(log_js_1.LogType.debug, `navigatedWithinDocument ${url}, ${navigationType}`);
|
|
// Current navigation URL should be updated.
|
|
this.#lastCommittedNavigation.url = url;
|
|
if (navigationType !== 'fragment') {
|
|
// TODO: check for other navigation types, like `javascript`.
|
|
return;
|
|
}
|
|
// There is no way to map `navigatedWithinDocument` to a specific navigation. Consider
|
|
// it is the pending navigation, if it is a fragment one.
|
|
const fragmentNavigation = this.#pendingNavigation?.isFragmentNavigation === true
|
|
? this.#pendingNavigation
|
|
: new NavigationState(url, this.#browsingContextId, false, this.#eventManager);
|
|
// Finish ongoing navigation.
|
|
fragmentNavigation.fragmentNavigated();
|
|
if (fragmentNavigation === this.#pendingNavigation) {
|
|
this.#pendingNavigation = undefined;
|
|
}
|
|
}
|
|
/**
|
|
* Required to mark navigation as fully complete.
|
|
* TODO: navigation should be complete when it became the current one on
|
|
* `Page.frameNavigated` or on navigating command finished with a new loader Id.
|
|
*/
|
|
loadPageEvent(loaderId) {
|
|
this.#logger?.(log_js_1.LogType.debug, 'loadPageEvent');
|
|
// Even if it was an initial navigation, it is finished.
|
|
this.#isInitialNavigation = false;
|
|
this.#loaderIdToNavigationsMap.get(loaderId)?.load();
|
|
}
|
|
/**
|
|
* Fail navigation due to navigation command failed.
|
|
*/
|
|
failNavigation(navigation, errorText) {
|
|
this.#logger?.(log_js_1.LogType.debug, 'failCommandNavigation');
|
|
navigation.fail(errorText);
|
|
}
|
|
/**
|
|
* Updates the navigation's `loaderId` and sets it as current one, if it is a
|
|
* cross-document navigation.
|
|
*/
|
|
navigationCommandFinished(navigation, loaderId) {
|
|
this.#logger?.(log_js_1.LogType.debug, `finishCommandNavigation ${navigation.navigationId}, ${loaderId}`);
|
|
if (loaderId !== undefined) {
|
|
navigation.loaderId = loaderId;
|
|
this.#loaderIdToNavigationsMap.set(loaderId, navigation);
|
|
}
|
|
navigation.isFragmentNavigation = loaderId === undefined;
|
|
}
|
|
frameStartedNavigating(url, loaderId, navigationType) {
|
|
this.#logger?.(log_js_1.LogType.debug, `frameStartedNavigating ${url}, ${loaderId}`);
|
|
if (this.#pendingNavigation &&
|
|
this.#pendingNavigation?.loaderId !== undefined &&
|
|
this.#pendingNavigation?.loaderId !== loaderId) {
|
|
// If there is a pending navigation with loader id set, but not equal to the new
|
|
// loader id, cancel pending navigation.
|
|
this.#pendingNavigation?.fail('navigation canceled by concurrent navigation');
|
|
this.#pendingNavigation = undefined;
|
|
}
|
|
if (this.#loaderIdToNavigationsMap.has(loaderId)) {
|
|
const existingNavigation = this.#loaderIdToNavigationsMap.get(loaderId);
|
|
// Navigation can be changed from `sameDocument` to `differentDocument`.
|
|
existingNavigation.isFragmentNavigation =
|
|
NavigationTracker.#isFragmentNavigation(navigationType);
|
|
this.#pendingNavigation = existingNavigation;
|
|
return;
|
|
}
|
|
const pendingNavigation = this.#pendingNavigation ?? this.createPendingNavigation(url, true);
|
|
this.#loaderIdToNavigationsMap.set(loaderId, pendingNavigation);
|
|
pendingNavigation.isFragmentNavigation =
|
|
NavigationTracker.#isFragmentNavigation(navigationType);
|
|
pendingNavigation.url = url;
|
|
pendingNavigation.loaderId = loaderId;
|
|
pendingNavigation.start();
|
|
}
|
|
static #isFragmentNavigation(navigationType) {
|
|
// Page.frameStartedNavigating.navigationType can be one of the following values:
|
|
// reload, reloadBypassingCache, restore, restoreWithPost, historySameDocument,
|
|
// historyDifferentDocument, sameDocument, differentDocument.
|
|
// https://chromedevtools.github.io/devtools-protocol/tot/Page/#event-frameStartedNavigating
|
|
return ['historySameDocument', 'sameDocument'].includes(navigationType);
|
|
}
|
|
/**
|
|
* If there is a navigation with the loaderId equals to the network request id, it means
|
|
* that the navigation failed.
|
|
*/
|
|
networkLoadingFailed(loaderId, errorText) {
|
|
this.#loaderIdToNavigationsMap.get(loaderId)?.fail(errorText);
|
|
}
|
|
}
|
|
exports.NavigationTracker = NavigationTracker;
|
|
//# sourceMappingURL=NavigationTracker.js.map
|